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