Skip to content

Commit 7b0da8b

Browse files
committed
fix(sandbox): name Landlock ABI in startup error
Signed-off-by: Eric Curtin <[email protected]>
1 parent 77c5ff6 commit 7b0da8b

4 files changed

Lines changed: 67 additions & 36 deletions

File tree

‎crates/openshell-sandbox/src/main.rs‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ fn qualify_runtime() -> Result<(openshell_sandbox::RuntimeQualification, Qualifi
154154
// broker is intentionally nondumpable, so that fallback is inaccessible
155155
// even though /proc/<dumpable-child>/mem remains available to mediation.
156156
let task_memory_copy = true;
157+
// Check the ABI first: on an older kernel the allow/deny probe would fail
158+
// with an opaque ruleset error instead of naming the ABI it needs.
159+
let landlock_abi =
160+
openshell_sandbox::sandbox::linux::require_landlock_abi().wrap_err("Landlock ABI probe")?;
157161
probe_landlock_allow_deny().wrap_err("Landlock allow/deny probe")?;
158162
let notification = openshell_sandbox::linux::seccomp_notify::probe_notification_api()
159163
.into_diagnostic()
@@ -163,14 +167,6 @@ fn qualify_runtime() -> Result<(openshell_sandbox::RuntimeQualification, Qualifi
163167
.into_diagnostic()
164168
.wrap_err("socket loopback confinement probe")?;
165169
probe_dns_relay_bind().wrap_err("DNS relay bind probe")?;
166-
let landlock_abi = openshell_sandbox::linux::landlock::abi_version()
167-
.into_diagnostic()
168-
.wrap_err("Landlock ABI probe")?;
169-
if landlock_abi < 3 {
170-
return Err(miette::miette!(
171-
"sandbox self-protection requires Landlock ABI v3 or newer (including truncation), found v{landlock_abi}"
172-
));
173-
}
174170

175171
let groups = nix::unistd::getgroups()
176172
.into_diagnostic()?

‎crates/openshell-sandbox/src/sandbox/linux/landlock.rs‎

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -54,38 +54,38 @@ impl std::fmt::Display for LandlockAvailability {
5454
/// This is safe to call from the parent process and does not create any file
5555
/// descriptors or modify process state.
5656
pub fn probe_availability() -> LandlockAvailability {
57-
// landlock_create_ruleset syscall number (same on x86_64 and aarch64).
58-
const SYS_LANDLOCK_CREATE_RULESET: libc::c_long = 444;
59-
// Flag: return the highest supported ABI version instead of creating a ruleset.
60-
const LANDLOCK_CREATE_RULESET_VERSION: libc::c_uint = 1 << 0;
61-
62-
// SAFETY: landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION)
63-
// is a read-only probe that returns the ABI version or an error code.
64-
// It does not allocate file descriptors or modify process state.
65-
#[allow(unsafe_code)]
66-
let ret = unsafe {
67-
libc::syscall(
68-
SYS_LANDLOCK_CREATE_RULESET,
69-
std::ptr::null::<libc::c_void>(),
70-
0_usize,
71-
LANDLOCK_CREATE_RULESET_VERSION,
72-
)
73-
};
74-
75-
if ret >= 0 {
76-
#[allow(clippy::cast_possible_truncation)]
77-
LandlockAvailability::Available { abi: ret as i32 }
78-
} else {
79-
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
80-
match errno {
57+
match crate::linux::landlock::abi_version() {
58+
Ok(abi) => LandlockAvailability::Available {
59+
abi: i32::try_from(abi).unwrap_or(i32::MAX),
60+
},
61+
Err(error) => match error.raw_os_error().unwrap_or(0) {
8162
libc::ENOSYS => LandlockAvailability::NotImplemented,
8263
libc::EOPNOTSUPP => LandlockAvailability::NotEnabled,
8364
libc::EPERM => LandlockAvailability::Blocked,
8465
other => LandlockAvailability::Unknown(other),
85-
}
66+
},
8667
}
8768
}
8869

70+
/// Lowest Landlock ABI the mandatory baseline accepts (matches [`ABI::V3`]).
71+
/// ABI v3, introduced in Linux 6.2, adds pathname truncation.
72+
const REQUIRED_ABI: i32 = 3;
73+
74+
fn check_abi(availability: &LandlockAvailability) -> Result<u32> {
75+
match availability {
76+
LandlockAvailability::Available { abi } if *abi >= REQUIRED_ABI => Ok(abi.unsigned_abs()),
77+
_ => Err(miette::miette!(
78+
"Landlock ABI v{REQUIRED_ABI} or newer (Linux 6.2+) is required, but Landlock is {availability}"
79+
)),
80+
}
81+
}
82+
83+
/// Probe the kernel and fail, naming the required and detected ABI, unless it
84+
/// supports the mandatory baseline. Returns the detected ABI.
85+
pub fn require_abi() -> Result<u32> {
86+
check_abi(&probe_availability())
87+
}
88+
8989
/// A prepared Landlock ruleset ready to be enforced via `restrict_self()`.
9090
///
9191
/// Path FDs are opened before enforcement. The capability-free launch path
@@ -141,6 +141,7 @@ pub fn prepare_capability_free_baseline() -> Result<PreparedRuleset> {
141141
fn prepare_capability_free_baseline_at(root: &Path) -> Result<PreparedRuleset> {
142142
// Unlike optional filesystem policy, self-protection must cover pathname
143143
// truncation as well as opens. Never silently downgrade this ABI requirement.
144+
require_abi()?;
144145
let abi = ABI::V3;
145146
let access = AccessFs::from_all(abi);
146147
let mut ruleset = Ruleset::default()
@@ -922,4 +923,35 @@ mod tests {
922923
"probe_availability Debug should produce output"
923924
);
924925
}
926+
927+
#[test]
928+
fn check_abi_accepts_the_required_abi_or_newer() {
929+
for abi in [REQUIRED_ABI, REQUIRED_ABI + 4] {
930+
let detected = check_abi(&LandlockAvailability::Available { abi }).unwrap();
931+
assert_eq!(detected, abi.unsigned_abs());
932+
}
933+
}
934+
935+
#[test]
936+
fn check_abi_names_the_required_and_detected_abi() {
937+
let err = check_abi(&LandlockAvailability::Available { abi: 2 })
938+
.unwrap_err()
939+
.to_string();
940+
assert!(err.contains("ABI v3 or newer"), "{err}");
941+
assert!(err.contains("ABI v2"), "{err}");
942+
}
943+
944+
#[test]
945+
fn check_abi_rejects_unavailable_landlock() {
946+
for availability in [
947+
LandlockAvailability::NotImplemented,
948+
LandlockAvailability::NotEnabled,
949+
LandlockAvailability::Blocked,
950+
LandlockAvailability::Unknown(libc::EIO),
951+
] {
952+
let err = check_abi(&availability).unwrap_err().to_string();
953+
assert!(err.contains("ABI v3 or newer"), "{err}");
954+
assert!(err.contains(&availability.to_string()), "{err}");
955+
}
956+
}
925957
}

‎crates/openshell-sandbox/src/sandbox/linux/mod.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use openshell_core::policy::SandboxPolicy;
1111
use std::path::PathBuf;
1212
use std::sync::Once;
1313

14+
pub use landlock::require_abi as require_landlock_abi;
15+
1416
/// Opaque handle to a prepared-but-not-yet-enforced sandbox.
1517
/// Holds the Landlock ruleset with `PathFds` opened before child exec.
1618
pub struct PreparedSandbox {

‎docs/how-it-works/policies/schema.mdx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ fails to enforce the policy's rules:
7777
| `best_effort` | The sandbox runs without the filesystem rules and logs a high-severity finding. |
7878
| `hard_requirement` | The sandbox fails to start. |
7979

80-
In either mode, a sandbox requires a kernel with Landlock ABI v3 or later. The
81-
sandbox log's `Landlock ruleset built` event reports how many paths were applied
82-
and skipped.
80+
In either mode, a sandbox requires a kernel with Landlock ABI v3 or later. On an
81+
older kernel, startup fails with an error that names the required and detected
82+
ABI. The sandbox log's `Landlock ruleset built` event reports how many paths
83+
were applied and skipped.
8384

8485
## Process
8586

0 commit comments

Comments
 (0)