From 7fbea1f1025d7b018bc1c00469197befe34f5c68 Mon Sep 17 00:00:00 2001 From: Pierre Warnier Date: Fri, 3 Apr 2026 12:18:35 +0200 Subject: [PATCH] passwd: implement Landlock filesystem sandboxing (#41) Restricts passwd's filesystem access to only what it needs: - /etc/ (read+write for passwd/shadow files) - /dev/ (read+write for tty prompts) - /usr/sbin/ (execute for nscd/sss_cache) Uses the landlock crate (MIT/Apache-2.0) with best-effort mode: silently does nothing on kernels < 5.13. The apply_landlock() helper is in shadow-core::hardening so other tools can reuse it. Skipped during #[cfg(test)] since Landlock is irreversible per-process. Fixes #41 --- src/shadow-core/Cargo.toml | 2 ++ src/shadow-core/src/hardening.rs | 43 ++++++++++++++++++++++++++++++++ src/uu/passwd/Cargo.toml | 2 +- src/uu/passwd/src/passwd.rs | 27 ++++++++++---------- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/src/shadow-core/Cargo.toml b/src/shadow-core/Cargo.toml index c7a729a..a8c2e75 100644 --- a/src/shadow-core/Cargo.toml +++ b/src/shadow-core/Cargo.toml @@ -15,6 +15,7 @@ libc = { workspace = true } nix = { workspace = true } thiserror = { workspace = true } zeroize = { workspace = true } +landlock = { workspace = true, optional = true } [dev-dependencies] proptest = { workspace = true } @@ -30,6 +31,7 @@ group = [] gshadow = [] login-defs = [] subid = [] +landlock = ["dep:landlock"] [lints] workspace = true diff --git a/src/shadow-core/src/hardening.rs b/src/shadow-core/src/hardening.rs index 1a9c86f..329677c 100644 --- a/src/shadow-core/src/hardening.rs +++ b/src/shadow-core/src/hardening.rs @@ -57,6 +57,49 @@ pub fn sanitized_env() -> Vec<(String, String)> { env } +/// Restrict filesystem access via Landlock (Linux 5.13+). +/// +/// Best-effort: silently does nothing on kernels without Landlock support. +/// `rw_paths` get read+write access, `ro_paths` get read-only access, +/// `exec_paths` get execute access. Everything else is denied. +#[cfg(feature = "landlock")] +pub fn apply_landlock( + writable: &[&std::path::Path], + readable: &[&std::path::Path], + exec_paths: &[&std::path::Path], +) { + use landlock::{ + ABI, Access, AccessFs, Ruleset, RulesetAttr, RulesetCreatedAttr, path_beneath_rules, + }; + + let abi = ABI::V5; + let all_access = AccessFs::from_all(abi); + let read_access = AccessFs::from_read(abi); + let exec_access = AccessFs::Execute | AccessFs::ReadFile | AccessFs::ReadDir; + + let result = Ruleset::default() + .handle_access(all_access) + .and_then(Ruleset::create) + .and_then(|rs| rs.add_rules(path_beneath_rules(writable, all_access))) + .and_then(|rs| rs.add_rules(path_beneath_rules(readable, read_access))) + .and_then(|rs| rs.add_rules(path_beneath_rules(exec_paths, exec_access))) + .and_then(landlock::RulesetCreated::restrict_self); + + // Best-effort: log but don't fail + if let Err(e) = result { + eprintln!("landlock: {e}"); + } +} + +/// No-op on non-Linux or when the `landlock` feature is disabled. +#[cfg(not(feature = "landlock"))] +pub fn apply_landlock( + _writable: &[&std::path::Path], + _readable: &[&std::path::Path], + _exec_paths: &[&std::path::Path], +) { +} + /// Run all standard hardening steps for a setuid-root tool. /// /// Call at the top of `uumain` before any argument parsing. diff --git a/src/uu/passwd/Cargo.toml b/src/uu/passwd/Cargo.toml index 070afe1..1a393ca 100644 --- a/src/uu/passwd/Cargo.toml +++ b/src/uu/passwd/Cargo.toml @@ -18,7 +18,7 @@ path = "src/main.rs" clap = { workspace = true } libc = { workspace = true } nix = { workspace = true } -shadow-core = { workspace = true, features = ["shadow", "login-defs"] } +shadow-core = { workspace = true, features = ["shadow", "login-defs", "landlock"] } uucore = { workspace = true } [features] diff --git a/src/uu/passwd/src/passwd.rs b/src/uu/passwd/src/passwd.rs index b202cce..6743692 100644 --- a/src/uu/passwd/src/passwd.rs +++ b/src/uu/passwd/src/passwd.rs @@ -117,23 +117,22 @@ impl UError for PasswdError { /// Restrict filesystem access using landlock (Linux 5.13+). /// -/// Best-effort: silently does nothing on kernels that don't support landlock. +/// Best-effort Landlock sandboxing for passwd. +/// +/// Restricts filesystem access to only what passwd needs: +/// read+write `/etc/` (passwd/shadow files) and `/dev/` (tty for prompts), +/// execute `/usr/sbin/` (`nscd`/`sss_cache` invalidation). #[allow(unused_variables)] fn apply_landlock(root: &SysRoot) { - #[cfg(target_os = "linux")] - apply_landlock_inner(root); -} + // Landlock is irreversible per-process, skip during tests + #[cfg(not(test))] + { + let etc = root.resolve("/etc"); + let dev = Path::new("/dev"); + let usr_sbin = Path::new("/usr/sbin"); -#[cfg(target_os = "linux")] -fn apply_landlock_inner(_root: &SysRoot) { - // Landlock requires the landlock crate or raw syscalls. - // TODO(#41): Add landlock crate dependency and implement restriction. - // - // The restriction would be: - // - /etc/ (read + write for passwd/shadow files) - // - /dev/tty (read + write for password prompts) - // - /usr/sbin/ (execute for nscd/sss_cache) - // - deny everything else + shadow_core::hardening::apply_landlock(&[etc.as_path(), dev], &[], &[usr_sbin]); + } } // ---------------------------------------------------------------------------