landlock/
compat.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2
3use crate::{uapi, Access, CompatError};
4
5#[cfg(test)]
6use std::convert::TryInto;
7#[cfg(test)]
8use strum::{EnumCount, IntoEnumIterator};
9#[cfg(test)]
10use strum_macros::{EnumCount as EnumCountMacro, EnumIter};
11
12/// Version of the Landlock [ABI](http://en.wikipedia.org/wiki/Application_binary_interface).
13///
14/// `ABI` enables getting the features supported by a specific Landlock ABI
15/// (without relying on the kernel version which may not be accessible or patched).
16/// For example, [`AccessFs::from_all(ABI::V1)`](Access::from_all)
17/// gets all the file system access rights defined by the first version.
18///
19/// Without `ABI`, it would be hazardous to rely on the the full set of access flags
20/// (e.g., `BitFlags::<AccessFs>::all()` or `BitFlags::ALL`),
21/// a moving target that would change the semantics of your Landlock rule
22/// when migrating to a newer version of this crate.
23/// Indeed, a simple `cargo update` or `cargo install` run by any developer
24/// can result in a new version of this crate (fixing bugs or bringing non-breaking changes).
25/// This crate cannot give any guarantee concerning the new restrictions resulting from
26/// these unknown bits (i.e. access rights) that would not be controlled by your application but by
27/// a future version of this crate instead.
28/// Because we cannot know what the effect on your application of an unknown restriction would be
29/// when handling an untested Landlock access right (i.e. denied-by-default access),
30/// it could trigger bugs in your application.
31///
32/// This crate provides a set of tools to sandbox as much as possible
33/// while guaranteeing a consistent behavior thanks to the [`Compatible`] methods.
34/// You should also test with different relevant kernel versions,
35/// see [landlock-test-tools](http://github.com/landlock-lsm/landlock-test-tools) and
36/// [CI integration](http://github.com/landlock-lsm/rust-landlock/pull/41).
37///
38/// This way, we can have the guarantee that the use of a set of tested Landlock ABI works as
39/// expected because features brought by newer Landlock ABI will never be enabled by default
40/// (cf. [Linux kernel compatibility contract](http://docs.kernel.org/userspace-api/landlock.html#compatibility)).
41///
42/// In a nutshell, test the access rights you request on a kernel that support them and
43/// on a kernel that doesn't support them.
44#[cfg_attr(
45    test,
46    derive(Debug, PartialEq, Eq, PartialOrd, EnumIter, EnumCountMacro)
47)]
48#[derive(Copy, Clone)]
49#[non_exhaustive]
50pub enum ABI {
51    /// Kernel not supporting Landlock, either because it is not built with Landlock
52    /// or Landlock is not enabled at boot.
53    Unsupported = 0,
54    /// First Landlock ABI, introduced with
55    /// [Linux 5.13](http://git.kernel.org/stable/c/17ae69aba89dbfa2139b7f8024b757ab3cc42f59).
56    V1 = 1,
57    /// Second Landlock ABI, introduced with
58    /// [Linux 5.19](http://git.kernel.org/stable/c/cb44e4f061e16be65b8a16505e121490c66d30d0).
59    V2 = 2,
60    /// Third Landlock ABI, introduced with
61    /// [Linux 6.2](http://git.kernel.org/stable/c/299e2b1967578b1442128ba8b3e86ed3427d3651).
62    V3 = 3,
63    /// Fourth Landlock ABI, introduced with
64    /// [Linux 6.7](http://git.kernel.org/stable/c/136cc1e1f5be75f57f1e0404b94ee1c8792cb07d).
65    V4 = 4,
66    /// Fifth Landlock ABI, introduced with
67    /// [Linux 6.10](http://git.kernel.org/stable/c/2fc0e7892c10734c1b7c613ef04836d57d4676d5).
68    V5 = 5,
69    /// Sixth Landlock ABI, introduced with
70    /// [Linux 6.12](http://git.kernel.org/stable/c/e1b061b444fb01c237838f0d8238653afe6a8094).
71    V6 = 6,
72}
73
74impl ABI {
75    // Must remain private to avoid inconsistent behavior by passing Ok(self) to a builder method,
76    // e.g. to make it impossible to call ruleset.handle_fs(ABI::new_current()?)
77    fn new_current() -> Self {
78        ABI::from(unsafe {
79            // Landlock ABI version starts at 1 but errno is only set for negative values.
80            uapi::landlock_create_ruleset(
81                std::ptr::null(),
82                0,
83                uapi::LANDLOCK_CREATE_RULESET_VERSION,
84            )
85        })
86    }
87
88    #[cfg(test)]
89    fn is_known(value: i32) -> bool {
90        value > 0 && value < ABI::COUNT as i32
91    }
92}
93
94/// Converting from an integer to an ABI should only be used for testing.
95/// Indeed, manually setting the ABI can lead to inconsistent and unexpected behaviors.
96/// Instead, just use the appropriate access rights, this library will handle the rest.
97impl From<i32> for ABI {
98    fn from(value: i32) -> ABI {
99        match value {
100            // The only possible error values should be EOPNOTSUPP and ENOSYS, but let's interpret
101            // all kind of errors as unsupported.
102            n if n <= 0 => ABI::Unsupported,
103            1 => ABI::V1,
104            2 => ABI::V2,
105            3 => ABI::V3,
106            4 => ABI::V4,
107            5 => ABI::V5,
108            // Returns the greatest known ABI.
109            _ => ABI::V6,
110        }
111    }
112}
113
114#[test]
115fn abi_from() {
116    // EOPNOTSUPP (-95), ENOSYS (-38)
117    for n in [-95, -38, -1, 0] {
118        assert_eq!(ABI::from(n), ABI::Unsupported);
119    }
120
121    let mut last_i = 1;
122    let mut last_abi = ABI::Unsupported;
123    for (i, abi) in ABI::iter().enumerate() {
124        last_i = i.try_into().unwrap();
125        last_abi = abi;
126        assert_eq!(ABI::from(last_i), last_abi);
127    }
128
129    assert_eq!(ABI::from(last_i + 1), last_abi);
130    assert_eq!(ABI::from(9), last_abi);
131}
132
133#[test]
134fn known_abi() {
135    assert!(!ABI::is_known(-1));
136    assert!(!ABI::is_known(0));
137    assert!(!ABI::is_known(99));
138
139    let mut last_i = -1;
140    for (i, _) in ABI::iter().enumerate().skip(1) {
141        last_i = i as i32;
142        assert!(ABI::is_known(last_i));
143    }
144    assert!(!ABI::is_known(last_i + 1));
145}
146
147#[cfg(test)]
148lazy_static! {
149    static ref TEST_ABI: ABI = match std::env::var("LANDLOCK_CRATE_TEST_ABI") {
150        Ok(s) => {
151            let n = s.parse::<i32>().unwrap();
152            if ABI::is_known(n) || n == 0 {
153                ABI::from(n)
154            } else {
155                panic!("Unknown ABI: {n}");
156            }
157        }
158        Err(std::env::VarError::NotPresent) => ABI::new_current(),
159        Err(e) => panic!("Failed to read LANDLOCK_CRATE_TEST_ABI: {e}"),
160    };
161}
162
163#[cfg(test)]
164pub(crate) fn can_emulate(mock: ABI, partial_support: ABI, full_support: Option<ABI>) -> bool {
165    mock < partial_support
166        || mock <= *TEST_ABI
167        || if let Some(full) = full_support {
168            full <= *TEST_ABI
169        } else {
170            partial_support <= *TEST_ABI
171        }
172}
173
174#[cfg(test)]
175pub(crate) fn get_errno_from_landlock_status() -> Option<i32> {
176    use std::io::Error;
177
178    match ABI::new_current() {
179        ABI::Unsupported => match Error::last_os_error().raw_os_error() {
180            // Returns ENOSYS when the kernel is not built with Landlock support,
181            // or EOPNOTSUPP when Landlock is supported but disabled at boot time.
182            ret @ Some(libc::ENOSYS | libc::EOPNOTSUPP) => ret,
183            // Other values can only come from bogus seccomp filters or debug tampering.
184            _ => unreachable!(),
185        },
186        _ => None,
187    }
188}
189
190#[test]
191fn current_kernel_abi() {
192    // Ensures that the tested Landlock ABI is the latest known version supported by the running
193    // kernel.  If this test failed, you need set the LANDLOCK_CRATE_TEST_ABI environment variable
194    // to the Landlock ABI version supported by your kernel.  With a missing variable, the latest
195    // Landlock ABI version known by this crate is automatically set.
196    // From Linux 5.13 to 5.18, you need to run: LANDLOCK_CRATE_TEST_ABI=1 cargo test
197    let test_abi = *TEST_ABI;
198    let current_abi = ABI::new_current();
199    println!(
200        "Current kernel version: {}",
201        std::fs::read_to_string("/proc/version")
202            .unwrap_or_else(|_| "unknown".into())
203            .trim()
204    );
205    println!("Expected Landlock ABI {test_abi:?} whereas the current ABI is {current_abi:#?}");
206    assert_eq!(test_abi, current_abi);
207}
208
209// CompatState is not public outside this crate.
210/// Returned by ruleset builder.
211#[cfg_attr(test, derive(Debug))]
212#[derive(Copy, Clone, PartialEq, Eq)]
213pub enum CompatState {
214    /// Initial undefined state.
215    Init,
216    /// All requested restrictions are enforced.
217    Full,
218    /// Some requested restrictions are enforced, following a best-effort approach.
219    Partial,
220    /// The running system doesn't support Landlock.
221    No,
222    /// Final unsupported state.
223    Dummy,
224}
225
226impl CompatState {
227    fn update(&mut self, other: Self) {
228        *self = match (*self, other) {
229            (CompatState::Init, other) => other,
230            (CompatState::Dummy, _) => CompatState::Dummy,
231            (_, CompatState::Dummy) => CompatState::Dummy,
232            (CompatState::No, CompatState::No) => CompatState::No,
233            (CompatState::Full, CompatState::Full) => CompatState::Full,
234            (_, _) => CompatState::Partial,
235        }
236    }
237}
238
239#[test]
240fn compat_state_update_1() {
241    let mut state = CompatState::Full;
242
243    state.update(CompatState::Full);
244    assert_eq!(state, CompatState::Full);
245
246    state.update(CompatState::No);
247    assert_eq!(state, CompatState::Partial);
248
249    state.update(CompatState::Full);
250    assert_eq!(state, CompatState::Partial);
251
252    state.update(CompatState::Full);
253    assert_eq!(state, CompatState::Partial);
254
255    state.update(CompatState::No);
256    assert_eq!(state, CompatState::Partial);
257
258    state.update(CompatState::Dummy);
259    assert_eq!(state, CompatState::Dummy);
260
261    state.update(CompatState::Full);
262    assert_eq!(state, CompatState::Dummy);
263}
264
265#[test]
266fn compat_state_update_2() {
267    let mut state = CompatState::Full;
268
269    state.update(CompatState::Full);
270    assert_eq!(state, CompatState::Full);
271
272    state.update(CompatState::No);
273    assert_eq!(state, CompatState::Partial);
274
275    state.update(CompatState::Full);
276    assert_eq!(state, CompatState::Partial);
277}
278
279#[cfg_attr(test, derive(Debug, PartialEq))]
280#[derive(Copy, Clone)]
281pub(crate) struct Compatibility {
282    abi: ABI,
283    pub(crate) level: Option<CompatLevel>,
284    pub(crate) state: CompatState,
285}
286
287impl From<ABI> for Compatibility {
288    fn from(abi: ABI) -> Self {
289        Compatibility {
290            abi,
291            level: Default::default(),
292            state: CompatState::Init,
293        }
294    }
295}
296
297impl Compatibility {
298    // Compatibility is a semi-opaque struct.
299    #[allow(clippy::new_without_default)]
300    pub(crate) fn new() -> Self {
301        ABI::new_current().into()
302    }
303
304    pub(crate) fn update(&mut self, state: CompatState) {
305        self.state.update(state);
306    }
307
308    pub(crate) fn abi(&self) -> ABI {
309        self.abi
310    }
311}
312
313pub(crate) mod private {
314    use crate::CompatLevel;
315
316    pub trait OptionCompatLevelMut {
317        fn as_option_compat_level_mut(&mut self) -> &mut Option<CompatLevel>;
318    }
319}
320
321/// Properly handles runtime unsupported features.
322///
323/// This guarantees consistent behaviors across crate users
324/// and runtime kernels even if this crate get new features.
325/// It eases backward compatibility and enables future-proofness.
326///
327/// Landlock is a security feature designed to help improve security of a running system
328/// thanks to application developers.
329/// To protect users as much as possible,
330/// compatibility with the running system should then be handled in a best-effort way,
331/// contrary to common system features.
332/// In some circumstances
333/// (e.g. applications carefully designed to only be run with a specific set of kernel features),
334/// it may be required to error out if some of these features are not available
335/// and will then not be enforced.
336pub trait Compatible: Sized + private::OptionCompatLevelMut {
337    /// To enable a best-effort security approach,
338    /// Landlock features that are not supported by the running system
339    /// are silently ignored by default,
340    /// which is a sane choice for most use cases.
341    /// However, on some rare circumstances,
342    /// developers may want to have some guarantees that their applications
343    /// will not run if a certain level of sandboxing is not possible.
344    /// If we really want to error out when not all our requested requirements are met,
345    /// then we can configure it with `set_compatibility()`.
346    ///
347    /// The `Compatible` trait is implemented for all object builders
348    /// (e.g. [`Ruleset`](crate::Ruleset)).
349    /// Such builders have a set of methods to incrementally build an object.
350    /// These build methods rely on kernel features that may not be available at runtime.
351    /// The `set_compatibility()` method enables to control the effect of
352    /// the following build method calls starting after the `set_compatibility()` call.
353    /// Such effect can be:
354    /// * to silently ignore unsupported features
355    ///   and continue building ([`CompatLevel::BestEffort`]);
356    /// * to silently ignore unsupported features
357    ///   and ignore the whole build ([`CompatLevel::SoftRequirement`]);
358    /// * to return an error for any unsupported feature ([`CompatLevel::HardRequirement`]).
359    ///
360    /// Taking [`Ruleset`](crate::Ruleset) as an example,
361    /// the [`handle_access()`](crate::RulesetAttr::handle_access()) build method
362    /// returns a [`Result`] that can be [`Err(RulesetError)`](crate::RulesetError)
363    /// with a nested [`CompatError`].
364    /// Such error can only occur with a running Linux kernel not supporting the requested
365    /// Landlock accesses *and* if the current compatibility level is
366    /// [`CompatLevel::HardRequirement`].
367    /// However, such error is not possible with [`CompatLevel::BestEffort`]
368    /// nor [`CompatLevel::SoftRequirement`].
369    ///
370    /// The order of this call is important because
371    /// it defines the behavior of the following build method calls that return a [`Result`].
372    /// If `set_compatibility(CompatLevel::HardRequirement)` is called on an object,
373    /// then a [`CompatError`] may be returned for the next method calls,
374    /// until the next call to `set_compatibility()`.
375    /// This enables to change the behavior of a set of build method calls,
376    /// for instance to be sure that the sandbox will at least restrict some access rights.
377    ///
378    /// New objects inherit the compatibility configuration of their parents, if any.
379    /// For instance, [`Ruleset::create()`](crate::Ruleset::create()) returns
380    /// a [`RulesetCreated`](crate::RulesetCreated) object that inherits the
381    /// `Ruleset`'s compatibility configuration.
382    ///
383    /// # Example with `SoftRequirement`
384    ///
385    /// Let's say an application legitimately needs to rename files between directories.
386    /// Because of [previous Landlock limitations](http://docs.kernel.org/userspace-api/landlock.html#file-renaming-and-linking-abi-2),
387    /// this was forbidden with the [first version of Landlock](ABI::V1),
388    /// but it is now handled starting with the [second version](ABI::V2).
389    /// For this use case, we only want the application to be sandboxed
390    /// if we have the guarantee that it will not break a legitimate usage (i.e. rename files).
391    /// We then create a ruleset which will either support file renaming
392    /// (thanks to [`AccessFs::Refer`](crate::AccessFs::Refer)) or silently do nothing.
393    ///
394    /// ```
395    /// use landlock::*;
396    ///
397    /// fn ruleset_handling_renames() -> Result<RulesetCreated, RulesetError> {
398    ///     Ok(Ruleset::default()
399    ///         // This ruleset must either handle the AccessFs::Refer right,
400    ///         // or it must silently ignore the whole sandboxing.
401    ///         .set_compatibility(CompatLevel::SoftRequirement)
402    ///         .handle_access(AccessFs::Refer)?
403    ///         // However, this ruleset may also handle other (future) access rights
404    ///         // if they are supported by the running kernel.
405    ///         .set_compatibility(CompatLevel::BestEffort)
406    ///         .handle_access(AccessFs::from_all(ABI::V6))?
407    ///         .create()?)
408    /// }
409    /// ```
410    ///
411    /// # Example with `HardRequirement`
412    ///
413    /// Security-dedicated applications may want to ensure that
414    /// an untrusted software component is subject to a minimum of restrictions before launching it.
415    /// In this case, we want to create a ruleset which will at least support
416    /// all restrictions provided by the [first version of Landlock](ABI::V1),
417    /// and opportunistically handle restrictions supported by newer kernels.
418    ///
419    /// ```
420    /// use landlock::*;
421    ///
422    /// fn ruleset_fragile() -> Result<RulesetCreated, RulesetError> {
423    ///     Ok(Ruleset::default()
424    ///         // This ruleset must either handle at least all accesses defined by
425    ///         // the first Landlock version (e.g. AccessFs::WriteFile),
426    ///         // or the following handle_access() call must return a wrapped
427    ///         // AccessError<AccessFs>::Incompatible error.
428    ///         .set_compatibility(CompatLevel::HardRequirement)
429    ///         .handle_access(AccessFs::from_all(ABI::V1))?
430    ///         // However, this ruleset may also handle new access rights
431    ///         // (e.g. AccessFs::Refer defined by the second version of Landlock)
432    ///         // if they are supported by the running kernel,
433    ///         // but without returning any error otherwise.
434    ///         .set_compatibility(CompatLevel::BestEffort)
435    ///         .handle_access(AccessFs::from_all(ABI::V6))?
436    ///         .create()?)
437    /// }
438    /// ```
439    fn set_compatibility(mut self, level: CompatLevel) -> Self {
440        *self.as_option_compat_level_mut() = Some(level);
441        self
442    }
443
444    /// Cf. [`set_compatibility()`](Compatible::set_compatibility()):
445    ///
446    /// - `set_best_effort(true)` translates to `set_compatibility(CompatLevel::BestEffort)`.
447    ///
448    /// - `set_best_effort(false)` translates to `set_compatibility(CompatLevel::HardRequirement)`.
449    #[deprecated(note = "Use set_compatibility() instead")]
450    fn set_best_effort(self, best_effort: bool) -> Self
451    where
452        Self: Sized,
453    {
454        self.set_compatibility(match best_effort {
455            true => CompatLevel::BestEffort,
456            false => CompatLevel::HardRequirement,
457        })
458    }
459}
460
461#[test]
462#[allow(deprecated)]
463fn deprecated_set_best_effort() {
464    use crate::{CompatLevel, Compatible, Ruleset};
465
466    assert_eq!(
467        Ruleset::default().set_best_effort(true).compat,
468        Ruleset::default()
469            .set_compatibility(CompatLevel::BestEffort)
470            .compat
471    );
472    assert_eq!(
473        Ruleset::default().set_best_effort(false).compat,
474        Ruleset::default()
475            .set_compatibility(CompatLevel::HardRequirement)
476            .compat
477    );
478}
479
480/// See the [`Compatible`] documentation.
481#[cfg_attr(test, derive(EnumIter))]
482#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
483pub enum CompatLevel {
484    /// Takes into account the build requests if they are supported by the running system,
485    /// or silently ignores them otherwise.
486    /// Never returns a compatibility error.
487    #[default]
488    BestEffort,
489    /// Takes into account the build requests if they are supported by the running system,
490    /// or silently ignores the whole build object otherwise.
491    /// Never returns a compatibility error.
492    /// If not supported,
493    /// the call to [`RulesetCreated::restrict_self()`](crate::RulesetCreated::restrict_self())
494    /// will return a
495    /// [`RestrictionStatus { ruleset: RulesetStatus::NotEnforced, no_new_privs: false, }`](crate::RestrictionStatus).
496    SoftRequirement,
497    /// Takes into account the build requests if they are supported by the running system,
498    /// or returns a compatibility error otherwise ([`CompatError`]).
499    HardRequirement,
500}
501
502impl From<Option<CompatLevel>> for CompatLevel {
503    fn from(opt: Option<CompatLevel>) -> Self {
504        match opt {
505            None => CompatLevel::default(),
506            Some(ref level) => *level,
507        }
508    }
509}
510
511// TailoredCompatLevel could be replaced with AsMut<Option<CompatLevel>>, but only traits defined
512// in the current crate can be implemented for types defined outside of the crate.  Furthermore it
513// provides a default implementation which is handy for types such as BitFlags.
514pub trait TailoredCompatLevel {
515    fn tailored_compat_level<L>(&mut self, parent_level: L) -> CompatLevel
516    where
517        L: Into<CompatLevel>,
518    {
519        parent_level.into()
520    }
521}
522
523impl<T> TailoredCompatLevel for T
524where
525    Self: Compatible,
526{
527    // Every Compatible trait implementation returns its own compatibility level, if set.
528    fn tailored_compat_level<L>(&mut self, parent_level: L) -> CompatLevel
529    where
530        L: Into<CompatLevel>,
531    {
532        // Using a mutable reference is not required but it makes the code simpler (no double AsRef
533        // implementations for each Compatible types), and more importantly it guarantees
534        // consistency with Compatible::set_compatibility().
535        match self.as_option_compat_level_mut() {
536            None => parent_level.into(),
537            // Returns the most constrained compatibility level.
538            Some(ref level) => parent_level.into().max(*level),
539        }
540    }
541}
542
543#[test]
544fn tailored_compat_level() {
545    use crate::{AccessFs, PathBeneath, PathFd};
546
547    fn new_path(level: CompatLevel) -> PathBeneath<PathFd> {
548        PathBeneath::new(PathFd::new("/").unwrap(), AccessFs::Execute).set_compatibility(level)
549    }
550
551    for parent_level in CompatLevel::iter() {
552        assert_eq!(
553            new_path(CompatLevel::BestEffort).tailored_compat_level(parent_level),
554            parent_level
555        );
556        assert_eq!(
557            new_path(CompatLevel::HardRequirement).tailored_compat_level(parent_level),
558            CompatLevel::HardRequirement
559        );
560    }
561
562    assert_eq!(
563        new_path(CompatLevel::SoftRequirement).tailored_compat_level(CompatLevel::SoftRequirement),
564        CompatLevel::SoftRequirement
565    );
566
567    for child_level in CompatLevel::iter() {
568        assert_eq!(
569            new_path(child_level).tailored_compat_level(CompatLevel::BestEffort),
570            child_level
571        );
572        assert_eq!(
573            new_path(child_level).tailored_compat_level(CompatLevel::HardRequirement),
574            CompatLevel::HardRequirement
575        );
576    }
577}
578
579// CompatResult is not public outside this crate.
580pub enum CompatResult<A>
581where
582    A: Access,
583{
584    // Fully matches the request.
585    Full,
586    // Partially matches the request.
587    Partial(CompatError<A>),
588    // Doesn't matches the request.
589    No(CompatError<A>),
590}
591
592// TryCompat is not public outside this crate.
593pub trait TryCompat<A>
594where
595    Self: Sized + TailoredCompatLevel,
596    A: Access,
597{
598    fn try_compat_inner(&mut self, abi: ABI) -> Result<CompatResult<A>, CompatError<A>>;
599
600    // Default implementation for objects without children.
601    //
602    // If returning something other than Ok(Some(self)), the implementation must use its own
603    // compatibility level, if any, with self.tailored_compat_level(default_compat_level), and pass
604    // it with the abi and compat_state to each child.try_compat().  See PathBeneath implementation
605    // and the self.allowed_access.try_compat() call.
606    //
607    // # Warning
608    //
609    // Errors must be prioritized over incompatibility (i.e. return Err(e) over Ok(None)) for all
610    // children.
611    fn try_compat_children<L>(
612        self,
613        _abi: ABI,
614        _parent_level: L,
615        _compat_state: &mut CompatState,
616    ) -> Result<Option<Self>, CompatError<A>>
617    where
618        L: Into<CompatLevel>,
619    {
620        Ok(Some(self))
621    }
622
623    // Update compat_state and return an error according to try_compat_*() error, or to the
624    // compatibility level, i.e. either route compatible object or error.
625    fn try_compat<L>(
626        mut self,
627        abi: ABI,
628        parent_level: L,
629        compat_state: &mut CompatState,
630    ) -> Result<Option<Self>, CompatError<A>>
631    where
632        L: Into<CompatLevel>,
633    {
634        let compat_level = self.tailored_compat_level(parent_level);
635        let some_inner = match self.try_compat_inner(abi) {
636            Ok(CompatResult::Full) => {
637                compat_state.update(CompatState::Full);
638                true
639            }
640            Ok(CompatResult::Partial(error)) => match compat_level {
641                CompatLevel::BestEffort => {
642                    compat_state.update(CompatState::Partial);
643                    true
644                }
645                CompatLevel::SoftRequirement => {
646                    compat_state.update(CompatState::Dummy);
647                    false
648                }
649                CompatLevel::HardRequirement => {
650                    compat_state.update(CompatState::Dummy);
651                    return Err(error);
652                }
653            },
654            Ok(CompatResult::No(error)) => match compat_level {
655                CompatLevel::BestEffort => {
656                    compat_state.update(CompatState::No);
657                    false
658                }
659                CompatLevel::SoftRequirement => {
660                    compat_state.update(CompatState::Dummy);
661                    false
662                }
663                CompatLevel::HardRequirement => {
664                    compat_state.update(CompatState::Dummy);
665                    return Err(error);
666                }
667            },
668            Err(error) => {
669                // Safeguard to help for test consistency.
670                compat_state.update(CompatState::Dummy);
671                return Err(error);
672            }
673        };
674
675        // At this point, any inner error have been returned, so we can proceed with
676        // try_compat_children()?.
677        match self.try_compat_children(abi, compat_level, compat_state)? {
678            Some(n) if some_inner => Ok(Some(n)),
679            _ => Ok(None),
680        }
681    }
682}