Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions crates/openshell-sandbox/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ fn qualify_runtime() -> Result<(openshell_sandbox::RuntimeQualification, Qualifi
// broker is intentionally nondumpable, so that fallback is inaccessible
// even though /proc/<dumpable-child>/mem remains available to mediation.
let task_memory_copy = true;
// Check the ABI first: on an older kernel the allow/deny probe would fail
// with an opaque ruleset error instead of naming the ABI it needs.
let landlock_abi =
openshell_sandbox::sandbox::linux::require_landlock_abi().wrap_err("Landlock ABI probe")?;
probe_landlock_allow_deny().wrap_err("Landlock allow/deny probe")?;
let notification = openshell_sandbox::linux::seccomp_notify::probe_notification_api()
.into_diagnostic()
Expand All @@ -163,14 +167,6 @@ fn qualify_runtime() -> Result<(openshell_sandbox::RuntimeQualification, Qualifi
.into_diagnostic()
.wrap_err("socket loopback confinement probe")?;
probe_dns_relay_bind().wrap_err("DNS relay bind probe")?;
let landlock_abi = openshell_sandbox::linux::landlock::abi_version()
.into_diagnostic()
.wrap_err("Landlock ABI probe")?;
if landlock_abi < 3 {
return Err(miette::miette!(
"sandbox self-protection requires Landlock ABI v3 or newer (including truncation), found v{landlock_abi}"
));
}

let groups = nix::unistd::getgroups()
.into_diagnostic()?
Expand Down
82 changes: 57 additions & 25 deletions crates/openshell-sandbox/src/sandbox/linux/landlock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,38 +54,38 @@ impl std::fmt::Display for LandlockAvailability {
/// This is safe to call from the parent process and does not create any file
/// descriptors or modify process state.
pub fn probe_availability() -> LandlockAvailability {
// landlock_create_ruleset syscall number (same on x86_64 and aarch64).
const SYS_LANDLOCK_CREATE_RULESET: libc::c_long = 444;
// Flag: return the highest supported ABI version instead of creating a ruleset.
const LANDLOCK_CREATE_RULESET_VERSION: libc::c_uint = 1 << 0;

// SAFETY: landlock_create_ruleset(NULL, 0, LANDLOCK_CREATE_RULESET_VERSION)
// is a read-only probe that returns the ABI version or an error code.
// It does not allocate file descriptors or modify process state.
#[allow(unsafe_code)]
let ret = unsafe {
libc::syscall(
SYS_LANDLOCK_CREATE_RULESET,
std::ptr::null::<libc::c_void>(),
0_usize,
LANDLOCK_CREATE_RULESET_VERSION,
)
};

if ret >= 0 {
#[allow(clippy::cast_possible_truncation)]
LandlockAvailability::Available { abi: ret as i32 }
} else {
let errno = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
match errno {
match crate::linux::landlock::abi_version() {
Ok(abi) => LandlockAvailability::Available {
abi: i32::try_from(abi).unwrap_or(i32::MAX),
},
Err(error) => match error.raw_os_error().unwrap_or(0) {
libc::ENOSYS => LandlockAvailability::NotImplemented,
libc::EOPNOTSUPP => LandlockAvailability::NotEnabled,
libc::EPERM => LandlockAvailability::Blocked,
other => LandlockAvailability::Unknown(other),
}
},
}
}

/// Lowest Landlock ABI the mandatory baseline accepts (matches [`ABI::V3`]).
/// ABI v3, introduced in Linux 6.2, adds pathname truncation.
const REQUIRED_ABI: i32 = 3;

fn check_abi(availability: &LandlockAvailability) -> Result<u32> {
match availability {
LandlockAvailability::Available { abi } if *abi >= REQUIRED_ABI => Ok(abi.unsigned_abs()),
_ => Err(miette::miette!(
"Landlock ABI v{REQUIRED_ABI} or newer (Linux 6.2+) is required, but Landlock is {availability}"
)),
}
}

/// Probe the kernel and fail, naming the required and detected ABI, unless it
/// supports the mandatory baseline. Returns the detected ABI.
pub fn require_abi() -> Result<u32> {
check_abi(&probe_availability())
}

/// A prepared Landlock ruleset ready to be enforced via `restrict_self()`.
///
/// Path FDs are opened before enforcement. The capability-free launch path
Expand Down Expand Up @@ -141,6 +141,7 @@ pub fn prepare_capability_free_baseline() -> Result<PreparedRuleset> {
fn prepare_capability_free_baseline_at(root: &Path) -> Result<PreparedRuleset> {
// Unlike optional filesystem policy, self-protection must cover pathname
// truncation as well as opens. Never silently downgrade this ABI requirement.
require_abi()?;
let abi = ABI::V3;
let access = AccessFs::from_all(abi);
let mut ruleset = Ruleset::default()
Expand Down Expand Up @@ -922,4 +923,35 @@ mod tests {
"probe_availability Debug should produce output"
);
}

#[test]
fn check_abi_accepts_the_required_abi_or_newer() {
for abi in [REQUIRED_ABI, REQUIRED_ABI + 4] {
let detected = check_abi(&LandlockAvailability::Available { abi }).unwrap();
assert_eq!(detected, abi.unsigned_abs());
}
}

#[test]
fn check_abi_names_the_required_and_detected_abi() {
let err = check_abi(&LandlockAvailability::Available { abi: 2 })
.unwrap_err()
.to_string();
assert!(err.contains("ABI v3 or newer"), "{err}");
assert!(err.contains("ABI v2"), "{err}");
}

#[test]
fn check_abi_rejects_unavailable_landlock() {
for availability in [
LandlockAvailability::NotImplemented,
LandlockAvailability::NotEnabled,
LandlockAvailability::Blocked,
LandlockAvailability::Unknown(libc::EIO),
] {
let err = check_abi(&availability).unwrap_err().to_string();
assert!(err.contains("ABI v3 or newer"), "{err}");
assert!(err.contains(&availability.to_string()), "{err}");
}
}
}
2 changes: 2 additions & 0 deletions crates/openshell-sandbox/src/sandbox/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ use openshell_core::policy::SandboxPolicy;
use std::path::PathBuf;
use std::sync::Once;

pub use landlock::require_abi as require_landlock_abi;

/// Opaque handle to a prepared-but-not-yet-enforced sandbox.
/// Holds the Landlock ruleset with `PathFds` opened before child exec.
pub struct PreparedSandbox {
Expand Down
7 changes: 4 additions & 3 deletions docs/how-it-works/policies/schema.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,10 @@ fails to enforce the policy's rules:
| `best_effort` | The sandbox runs without the filesystem rules and logs a high-severity finding. |
| `hard_requirement` | The sandbox fails to start. |

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

## Process

Expand Down
Loading