Skip to content

Commit

Permalink
compat: Fix SoftRequirement consistency
Browse files Browse the repository at this point in the history
Complete CompatState with the Init state to fully rely on CompatState
instead of ABI or is_mooted(), and rename Final to Dummy.  This enables
to not change a ruleset ABI once set.

Always update CompatState for any BitFlags<Access>::try_compat() call.

Make can_emulate() handles ABIs not supporting a minimal supported
version, which is required for the new abi_v2_refer_only() test.

Signed-off-by: Mickaël Salaün <mic@digikod.net>
  • Loading branch information
l0kod committed Aug 4, 2023
1 parent 8d54427 commit 3f2eabc
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 107 deletions.
62 changes: 36 additions & 26 deletions src/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,47 +66,52 @@ where
T: Access,
{
fn try_compat(self, compat: &mut Compatibility) -> Result<Option<Self>, CompatError<T>> {
let (state, new_access) = if self.is_empty() {
let (state, ret) = if self.is_empty() {
// Empty access-rights would result to a runtime error.
return Err(AccessError::Empty.into());
(CompatState::Dummy, Err(AccessError::Empty.into()))
} else if !Self::all().contains(self) {
// Unknown access-rights (at build time) would result to a runtime error.
// This can only be reached by using the unsafe BitFlags::from_bits_unchecked().
return Err(AccessError::Unknown {
access: self,
unknown: self & full_negation(Self::all()),
}
.into());
(
CompatState::Dummy,
Err(AccessError::Unknown {
access: self,
unknown: self & full_negation(Self::all()),
}
.into()),
)
} else {
let compat_bits = self & T::from_all(compat.abi());
if compat_bits.is_empty() {
match compat.level {
// Empty access-rights are ignored to avoid an error when passing them to
// landlock_add_rule().
CompatLevel::BestEffort => (CompatState::No, None),
CompatLevel::SoftRequirement => (CompatState::Final, None),
CompatLevel::HardRequirement => {
return Err(AccessError::Incompatible { access: self }.into());
}
CompatLevel::BestEffort => (CompatState::No, Ok(None)),
CompatLevel::SoftRequirement => (CompatState::Dummy, Ok(None)),
CompatLevel::HardRequirement => (
CompatState::Dummy,
Err(AccessError::Incompatible { access: self }.into()),
),
}
} else if compat_bits != self {
match compat.level {
CompatLevel::BestEffort => (CompatState::Partial, Some(compat_bits)),
CompatLevel::SoftRequirement => (CompatState::Final, None),
CompatLevel::HardRequirement => {
return Err(AccessError::PartiallyCompatible {
CompatLevel::BestEffort => (CompatState::Partial, Ok(Some(compat_bits))),
CompatLevel::SoftRequirement => (CompatState::Dummy, Ok(None)),
CompatLevel::HardRequirement => (
CompatState::Dummy,
Err(AccessError::PartiallyCompatible {
access: self,
incompatible: self & full_negation(compat_bits),
}
.into());
}
.into()),
),
}
} else {
(CompatState::Full, Some(compat_bits))
(CompatState::Full, Ok(Some(compat_bits)))
}
};
compat.update(state);
Ok(new_access)
ret
}
}

Expand All @@ -115,13 +120,14 @@ fn compat_bit_flags() {
use crate::ABI;

let mut compat: Compatibility = ABI::V1.into();
assert!(!compat.is_mooted());
assert!(compat.state == CompatState::Init);

let ro_access = make_bitflags!(AccessFs::{Execute | ReadFile | ReadDir});
assert_eq!(
ro_access,
ro_access.try_compat(&mut compat).unwrap().unwrap()
);
assert!(compat.state == CompatState::Full);

let empty_access = BitFlags::<AccessFs>::empty();
assert!(matches!(
Expand All @@ -134,23 +140,27 @@ fn compat_bit_flags() {
all_unknown_access.try_compat(&mut compat).unwrap_err(),
CompatError::Access(AccessError::Unknown { access, unknown }) if access == all_unknown_access && unknown == all_unknown_access
));
// An error makes the state final.
assert!(compat.state == CompatState::Dummy);

let some_unknown_access = unsafe { BitFlags::<AccessFs>::from_bits_unchecked(1 << 63 | 1) };
assert!(matches!(
some_unknown_access.try_compat(&mut compat).unwrap_err(),
CompatError::Access(AccessError::Unknown { access, unknown }) if access == some_unknown_access && unknown == all_unknown_access
));

assert!(!compat.is_mooted());
assert!(compat.state == CompatState::Dummy);

compat = ABI::Unsupported.into();
assert!(!compat.is_mooted());

// Tests that the ruleset is marked as unsupported.
assert!(compat.state == CompatState::No);

// Access-rights are valid (but ignored) when they are not required for the current ABI.
assert_eq!(None, ro_access.try_compat(&mut compat).unwrap());

// Tests that a ruleset in an unsupported state doesn't get spuriously mooted.
assert!(!compat.is_mooted());
// Tests that the ruleset is in an unsupported state, which is important to be able to still
// enforce no_new_privs.
assert!(compat.state == CompatState::No);

// Access-rights are not valid when they are required for the current ABI.
compat.level = CompatLevel::HardRequirement;
Expand Down
38 changes: 14 additions & 24 deletions src/compat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ lazy_static! {
}

#[cfg(test)]
pub(crate) fn can_emulate(mock: ABI, full_support: ABI) -> bool {
mock <= *TEST_ABI || full_support <= *TEST_ABI
pub(crate) fn can_emulate(mock: ABI, partial_support: ABI, full_support: ABI) -> bool {
mock < partial_support || mock <= *TEST_ABI || full_support <= *TEST_ABI
}

#[cfg(test)]
Expand Down Expand Up @@ -163,21 +163,24 @@ fn current_kernel_abi() {
#[cfg_attr(test, derive(Debug))]
#[derive(Copy, Clone, PartialEq)]
pub(crate) enum CompatState {
/// Initial undefined state.
Init,
/// All requested restrictions are enforced.
Full,
/// Some requested restrictions are enforced, following a best-effort approach.
Partial,
/// The running system doesn't support Landlock.
No,
/// Final unsupported state.
Final,
Dummy,
}

impl CompatState {
fn update(&mut self, other: Self) {
*self = match (*self, other) {
(CompatState::Final, _) => CompatState::Final,
(_, CompatState::Final) => CompatState::Final,
(CompatState::Init, other) => other,
(CompatState::Dummy, _) => CompatState::Dummy,
(_, CompatState::Dummy) => CompatState::Dummy,
(CompatState::No, CompatState::No) => CompatState::No,
(CompatState::Full, CompatState::Full) => CompatState::Full,
(_, _) => CompatState::Partial,
Expand All @@ -204,11 +207,11 @@ fn compat_state_update_1() {
state.update(CompatState::No);
assert_eq!(state, CompatState::Partial);

state.update(CompatState::Final);
assert_eq!(state, CompatState::Final);
state.update(CompatState::Dummy);
assert_eq!(state, CompatState::Dummy);

state.update(CompatState::Full);
assert_eq!(state, CompatState::Final);
assert_eq!(state, CompatState::Dummy);
}

#[test]
Expand All @@ -232,10 +235,6 @@ pub struct Compatibility {
abi: ABI,
pub(crate) level: CompatLevel,
pub(crate) state: CompatState,
// is_mooted is required to differenciate a kernel not supporting Landlock from an error that
// occured with CompatLevel::SoftRequirement. is_mooted is only changed with update() and only
// used to not set no_new_privs in RulesetCreated::restrict_self().
is_mooted: bool,
}

impl From<ABI> for Compatibility {
Expand All @@ -244,11 +243,10 @@ impl From<ABI> for Compatibility {
abi,
level: CompatLevel::default(),
state: match abi {
// Forces the state as unsupported because all possible types will be useless.
ABI::Unsupported => CompatState::Final,
_ => CompatState::Full,
// Don't forces the state as Dummy because no_new_privs may still be legitimate.
ABI::Unsupported => CompatState::No,
_ => CompatState::Init,
},
is_mooted: false,
}
}
}
Expand All @@ -262,19 +260,11 @@ impl Compatibility {

pub(crate) fn update(&mut self, state: CompatState) {
self.state.update(state);
if state == CompatState::Final {
self.abi = ABI::Unsupported;
self.is_mooted = true;
}
}

pub(crate) fn abi(&self) -> ABI {
self.abi
}

pub(crate) fn is_mooted(&self) -> bool {
self.is_mooted
}
}

/// Properly handles runtime unsupported features.
Expand Down
2 changes: 1 addition & 1 deletion src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ where
self.allowed_access = match self.compat_level {
CompatLevel::BestEffort => valid_access,
CompatLevel::SoftRequirement => {
compat.update(CompatState::Final);
compat.update(CompatState::Dummy);
return Ok(None);
}
CompatLevel::HardRequirement => {
Expand Down
22 changes: 19 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,8 @@ mod tests {
let ret = check(Ruleset::from(abi));

// Useful for failed tests and with cargo test -- --show-output
println!("Checking ABI {abi:?}, expecting {ret:#?}");
if can_emulate(abi, full) {
println!("Checking ABI {abi:?}: received {ret:#?}");
if can_emulate(abi, partial, full) {
if abi < partial && error_if_abi_lt_partial {
// TODO: Check exact error type; this may require better error types.
assert!(matches!(ret, Err(TestRulesetError::Ruleset(_))));
Expand Down Expand Up @@ -222,7 +222,7 @@ mod tests {
}

#[test]
fn abi_v2_refer() {
fn abi_v2_exec_refer() {
check_ruleset_support(
ABI::V1,
ABI::V2,
Expand All @@ -237,4 +237,20 @@ mod tests {
false,
);
}

#[test]
fn abi_v2_refer_only() {
// When no access is handled, do not try to create a ruleset without access.
check_ruleset_support(
ABI::V2,
ABI::V2,
|ruleset: Ruleset| -> _ {
Ok(ruleset
.handle_access(AccessFs::Refer)?
.create()?
.restrict_self()?)
},
false,
);
}
}
Loading

0 comments on commit 3f2eabc

Please sign in to comment.