Skip to content

Commit ebf1b13

Browse files
committed
compat,sandboxer: Add LandlockStatus and print hints
Add LandlockStatus type to query the running kernel and display information about the available Landlock features. Replace ABI in Compatibility with LandlockStatus to be able to return this information with RestrictionStatus. Implement Display for ABI. Update the sandboxer to print hints about the status of Landlock, including the supported version, similar to the C example. Replace ABI::new_current() with LandlockStatus::current().into(). Add a new test_current_landlock_status() test. Signed-off-by: Mickaël Salaün <mic@digikod.net>
1 parent f79da18 commit ebf1b13

File tree

4 files changed

+207
-43
lines changed

4 files changed

+207
-43
lines changed

examples/sandboxer.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
use anyhow::{anyhow, bail, Context};
77
use landlock::{
8-
path_beneath_rules, Access, AccessFs, AccessNet, BitFlags, NetPort, PathBeneath, PathFd,
9-
Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetStatus, Scope, ABI,
8+
path_beneath_rules, Access, AccessFs, AccessNet, BitFlags, LandlockStatus, NetPort,
9+
PathBeneath, PathFd, Ruleset, RulesetAttr, RulesetCreatedAttr, RulesetStatus, Scope, ABI,
1010
};
1111
use std::env;
1212
use std::ffi::OsStr;
@@ -168,8 +168,51 @@ fn main() -> anyhow::Result<()> {
168168
.restrict_self()
169169
.expect("Failed to enforce ruleset");
170170

171+
match status.landlock {
172+
// This should never happen because of the previous check:
173+
LandlockStatus::NotEnabled => {
174+
eprintln!(
175+
"Hint: Landlock is currently disabled. \
176+
It can be enabled in the kernel configuration by prepending \"landlock,\"
177+
to the content of CONFIG_LSM, or at boot time by setting the same content to
178+
the \"lsm\" kernel parameter."
179+
);
180+
}
181+
LandlockStatus::NotImplemented => {
182+
eprintln!(
183+
"Hint: Landlock is not built into the current kernel. \
184+
To support it, build the kernel with CONFIG_SECURITY_LANDLOCK=y and \
185+
prepend \"landlock,\" to the content of CONFIG_LSM."
186+
);
187+
}
188+
LandlockStatus::Available(_, Some(raw_abi)) => {
189+
eprintln!(
190+
"Hint: This sandboxer only supports Landlock ABI version up to {abi} \
191+
whereas the current kernel supports Landlock ABI version {raw_abi}. \
192+
To leverage all Landlock features, update this sandboxer."
193+
);
194+
}
195+
LandlockStatus::Available(current_abi, None) => {
196+
if current_abi < abi {
197+
eprintln!(
198+
"Hint: This sandboxer supports Landlock ABI version up to {abi} \
199+
but the current kernel only supports Landlock ABI version {current_abi}. \
200+
To leverage all Landlock features, update the kernel."
201+
);
202+
} else if current_abi > abi {
203+
// This should not happen because the ABI used by the sandboxer
204+
// should be the latest supported by the Landlock crate, and
205+
// they should be updated at the same time.
206+
eprintln!(
207+
"Warning: This sandboxer only supports Landlock ABI version up to {abi} \
208+
but the current kernel supports Landlock ABI version {current_abi}. \
209+
To leverage all Landlock features, update this sandboxer."
210+
);
211+
}
212+
}
213+
}
171214
if status.ruleset == RulesetStatus::NotEnforced {
172-
bail!("Landlock is not supported by the running kernel.");
215+
bail!("The ruleset cannot be enforced at all");
173216
}
174217

175218
eprintln!("Executing the sandboxed command...");

src/compat.rs

Lines changed: 143 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: Apache-2.0 OR MIT
22

33
use crate::{uapi, Access, CompatError};
4+
use std::fmt::{self, Display, Formatter};
5+
use std::io::Error;
46

57
#[cfg(test)]
68
use std::convert::TryInto;
@@ -70,20 +72,10 @@ pub enum ABI {
7072
V6 = 6,
7173
}
7274

75+
// ABI should not be dynamically created (in other crates) according to the running kernel
76+
// to avoid inconsistent behaviors and non-determinism. Creating ABIs based on runtime detection
77+
// can lead to unreliable sandboxing where rules might differ between executions.
7378
impl 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-
8779
#[cfg(test)]
8880
fn is_known(value: i32) -> bool {
8981
value > 0 && value < ABI::COUNT as i32
@@ -96,8 +88,6 @@ impl ABI {
9688
impl From<i32> for ABI {
9789
fn from(value: i32) -> ABI {
9890
match value {
99-
// The only possible error values should be EOPNOTSUPP and ENOSYS, but let's interpret
100-
// all kind of errors as unsupported.
10191
n if n <= 0 => ABI::Unsupported,
10292
1 => ABI::V1,
10393
2 => ABI::V2,
@@ -126,14 +116,14 @@ fn abi_from() {
126116
}
127117

128118
assert_eq!(ABI::from(last_i + 1), last_abi);
129-
assert_eq!(ABI::from(9), last_abi);
119+
assert_eq!(ABI::from(999), last_abi);
130120
}
131121

132122
#[test]
133123
fn known_abi() {
134124
assert!(!ABI::is_known(-1));
135125
assert!(!ABI::is_known(0));
136-
assert!(!ABI::is_known(99));
126+
assert!(!ABI::is_known(999));
137127

138128
let mut last_i = -1;
139129
for (i, _) in ABI::iter().enumerate().skip(1) {
@@ -143,6 +133,108 @@ fn known_abi() {
143133
assert!(!ABI::is_known(last_i + 1));
144134
}
145135

136+
impl Display for ABI {
137+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
138+
match self {
139+
ABI::Unsupported => write!(f, "unsupported"),
140+
v => (*v as u32).fmt(f),
141+
}
142+
}
143+
}
144+
145+
/// Status of Landlock support for the running system.
146+
///
147+
/// This enum is used to represent the status of the Landlock support for the system where the code
148+
/// is executed. It can indicate whether Landlock is available or not.
149+
///
150+
/// # Warning
151+
///
152+
/// Sandboxed programs should only use this data to log or provide information to users,
153+
/// not to change their behavior according to this status. Indeed, the `Ruleset` and the other
154+
/// types are designed to handle the compatibility in a simple and safe way.
155+
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
156+
pub enum LandlockStatus {
157+
/// Landlock is supported but not enabled (`EOPNOTSUPP`).
158+
NotEnabled,
159+
/// Landlock is not implemented (i.e. not built into the running kernel: `ENOSYS`).
160+
NotImplemented,
161+
/// Landlock is available and supported up to the given ABI.
162+
///
163+
/// `Option<i32>` contains the raw ABI value if it's greater than the greatest known ABI,
164+
/// which would mean that the running kernel is newer than the Landlock crate.
165+
Available(ABI, Option<i32>),
166+
}
167+
168+
impl LandlockStatus {
169+
// Must remain private to avoid inconsistent behavior using such unknown-at-build-time ABI
170+
// e.g., AccessFs::from_all(ABI::new_current())
171+
//
172+
// This should not be Default::default() because the returned value would may not be the same
173+
// for all users.
174+
fn current() -> Self {
175+
// Landlock ABI version starts at 1 but errno is only set for negative values.
176+
let v = unsafe {
177+
uapi::landlock_create_ruleset(
178+
std::ptr::null(),
179+
0,
180+
uapi::LANDLOCK_CREATE_RULESET_VERSION,
181+
)
182+
};
183+
if v < 0 {
184+
// The only possible error values should be EOPNOTSUPP and ENOSYS.
185+
match Error::last_os_error().raw_os_error() {
186+
Some(libc::EOPNOTSUPP) => Self::NotEnabled,
187+
_ => Self::NotImplemented,
188+
}
189+
} else {
190+
let abi = ABI::from(v);
191+
Self::Available(abi, (v != abi as i32).then_some(v))
192+
}
193+
}
194+
}
195+
196+
// Test against the running kernel.
197+
#[test]
198+
fn test_current_landlock_status() {
199+
let status = LandlockStatus::current();
200+
if ABI::from(*TEST_ABI) == ABI::Unsupported {
201+
assert_eq!(status, LandlockStatus::NotImplemented);
202+
} else {
203+
assert!(matches!(status, LandlockStatus::Available(abi, _) if abi == (*TEST_ABI).into()));
204+
if std::env::var(TEST_ABI_ENV_NAME).is_ok() {
205+
// We cannot reliably check for unknown kernel.
206+
assert!(matches!(status, LandlockStatus::Available(_, None)));
207+
}
208+
}
209+
}
210+
211+
impl From<LandlockStatus> for ABI {
212+
fn from(status: LandlockStatus) -> Self {
213+
match status {
214+
// The only possible error values should be EOPNOTSUPP and ENOSYS,
215+
// but let's convert all kind of errors as unsupported.
216+
LandlockStatus::NotEnabled | LandlockStatus::NotImplemented => ABI::Unsupported,
217+
LandlockStatus::Available(abi, _) => abi,
218+
}
219+
}
220+
}
221+
222+
// This is only useful to tests and should not be exposed publicly because
223+
// the mapping can only be partial.
224+
#[cfg(test)]
225+
impl From<ABI> for LandlockStatus {
226+
fn from(abi: ABI) -> Self {
227+
match abi {
228+
// Convert to ENOSYS because of check_ruleset_support() and ruleset_unsupported() tests.
229+
ABI::Unsupported => Self::NotImplemented,
230+
_ => Self::Available(abi, None),
231+
}
232+
}
233+
}
234+
235+
#[cfg(test)]
236+
static TEST_ABI_ENV_NAME: &str = "LANDLOCK_CRATE_TEST_ABI";
237+
146238
#[cfg(test)]
147239
lazy_static! {
148240
static ref TEST_ABI: ABI = match std::env::var("LANDLOCK_CRATE_TEST_ABI") {
@@ -154,7 +246,7 @@ lazy_static! {
154246
panic!("Unknown ABI: {n}");
155247
}
156248
}
157-
Err(std::env::VarError::NotPresent) => ABI::new_current(),
249+
Err(std::env::VarError::NotPresent) => LandlockStatus::current().into(),
158250
Err(e) => panic!("Failed to read LANDLOCK_CRATE_TEST_ABI: {e}"),
159251
};
160252
}
@@ -172,17 +264,21 @@ pub(crate) fn can_emulate(mock: ABI, partial_support: ABI, full_support: Option<
172264

173265
#[cfg(test)]
174266
pub(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,
267+
match LandlockStatus::current() {
268+
LandlockStatus::NotImplemented | LandlockStatus::NotEnabled => {
269+
match Error::last_os_error().raw_os_error() {
270+
// Returns ENOSYS when the kernel is not built with Landlock support,
271+
// or EOPNOTSUPP when Landlock is supported but disabled at boot time.
272+
ret @ Some(libc::ENOSYS | libc::EOPNOTSUPP) => ret,
273+
// Other values can only come from bogus seccomp filters or debugging tampering.
274+
ret => {
275+
eprintln!("Current kernel should support this Landlock ABI according to $LANDLOCK_CRATE_TEST_ABI");
276+
eprintln!("Unexpected result: {ret:?}");
277+
unreachable!();
278+
}
279+
}
280+
}
281+
LandlockStatus::Available(_, _) => None,
186282
}
187283
}
188284

@@ -194,7 +290,7 @@ fn current_kernel_abi() {
194290
// Landlock ABI version known by this crate is automatically set.
195291
// From Linux 5.13 to 5.18, you need to run: LANDLOCK_CRATE_TEST_ABI=1 cargo test
196292
let test_abi = *TEST_ABI;
197-
let current_abi = ABI::new_current();
293+
let current_abi = LandlockStatus::current().into();
198294
println!(
199295
"Current kernel version: {}",
200296
std::fs::read_to_string("/proc/version")
@@ -277,34 +373,45 @@ fn compat_state_update_2() {
277373
#[cfg_attr(test, derive(PartialEq))]
278374
#[derive(Copy, Clone, Debug)]
279375
pub(crate) struct Compatibility {
280-
abi: ABI,
376+
status: LandlockStatus,
281377
pub(crate) level: Option<CompatLevel>,
282378
pub(crate) state: CompatState,
283379
}
284380

285-
impl From<ABI> for Compatibility {
286-
fn from(abi: ABI) -> Self {
381+
impl From<LandlockStatus> for Compatibility {
382+
fn from(status: LandlockStatus) -> Self {
287383
Compatibility {
288-
abi,
384+
status,
289385
level: Default::default(),
290386
state: CompatState::Init,
291387
}
292388
}
293389
}
294390

391+
#[cfg(test)]
392+
impl From<ABI> for Compatibility {
393+
fn from(abi: ABI) -> Self {
394+
Self::from(LandlockStatus::from(abi))
395+
}
396+
}
397+
295398
impl Compatibility {
296399
// Compatibility is a semi-opaque struct.
297400
#[allow(clippy::new_without_default)]
298401
pub(crate) fn new() -> Self {
299-
ABI::new_current().into()
402+
LandlockStatus::current().into()
300403
}
301404

302405
pub(crate) fn update(&mut self, state: CompatState) {
303406
self.state.update(state);
304407
}
305408

306409
pub(crate) fn abi(&self) -> ABI {
307-
self.abi
410+
self.status.into()
411+
}
412+
413+
pub(crate) fn status(&self) -> LandlockStatus {
414+
self.status
308415
}
309416
}
310417

src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
extern crate lazy_static;
8383

8484
pub use access::{Access, HandledAccess};
85-
pub use compat::{CompatLevel, Compatible, ABI};
85+
pub use compat::{CompatLevel, Compatible, LandlockStatus, ABI};
8686
pub use enumflags2::{make_bitflags, BitFlags};
8787
pub use errors::{
8888
AccessError, AddRuleError, AddRulesError, CompatError, CreateRulesetError, Errno,
@@ -166,13 +166,16 @@ mod tests {
166166
} else {
167167
RulesetStatus::NotEnforced
168168
};
169+
let landlock_status = abi.into();
169170
println!("Expecting ruleset status {ruleset_status:?}");
171+
println!("Expecting Landlock status {landlock_status:?}");
170172
assert!(matches!(
171173
ret,
172174
Ok(RestrictionStatus {
173175
ruleset,
176+
landlock,
174177
no_new_privs: true,
175-
}) if ruleset == ruleset_status
178+
}) if ruleset == ruleset_status && landlock == landlock_status
176179
))
177180
}
178181
} else {

0 commit comments

Comments
 (0)