| 1 | //! Linux Landlock sandbox implementation. |
| 2 | //! |
| 3 | //! Landlock is a security mechanism introduced in Linux kernel 5.13 that allows |
| 4 | //! processes to restrict their own access rights. Unlike Seatbelt on macOS which |
| 5 | //! uses an external sandbox-exec wrapper, Landlock applies restrictions directly |
| 6 | //! to the current process. |
| 7 | //! |
| 8 | //! # Requirements |
| 9 | //! |
| 10 | //! - Linux kernel 5.13 or later with Landlock enabled |
| 11 | //! - The kernel must be compiled with `CONFIG_SECURITY_LANDLOCK=y` |
| 12 | //! |
| 13 | //! # How it works |
| 14 | //! |
| 15 | //! 1. Create a landlock ruleset with desired restrictions |
| 16 | //! 2. Add rules to allow specific file paths |
| 17 | //! 3. Restrict the process using the ruleset |
| 18 | //! |
| 19 | //! Note: Once restricted, the process cannot gain more privileges. |
| 20 | |
| 21 | use super::{CommandSpec, SandboxPolicy}; |
| 22 | use std::ffi::CString; |
| 23 | use std::path::Path; |
| 24 | |
| 25 | /// Check if Landlock is available on this system. |
| 26 | pub fn is_available() -> bool { |
| 27 | // Check if the landlock syscall is available |
| 28 | #[cfg(target_os = "linux")] |
| 29 | { |
| 30 | // Try to create a minimal ruleset to test availability |
| 31 | // Landlock ABI version check |
| 32 | // Safety: syscall uses a null ruleset pointer for ABI probing and does not dereference it. |
| 33 | unsafe { |
| 34 | let result = libc::syscall( |
| 35 | libc::SYS_landlock_create_ruleset, |
| 36 | std::ptr::null::<libc::c_void>(), |
| 37 | 0usize, |
| 38 | LANDLOCK_CREATE_RULESET_VERSION, |
| 39 | ); |
| 40 | result >= 0 |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | #[cfg(not(target_os = "linux"))] |
| 45 | { |
| 46 | false |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | /// Get the Landlock ABI version supported by the kernel. |
| 51 | #[cfg(target_os = "linux")] |
| 52 | pub fn get_abi_version() -> Option<i32> { |
| 53 | // Safety: syscall uses a null ruleset pointer for ABI probing and does not dereference it. |
| 54 | unsafe { |
| 55 | let result = libc::syscall( |
| 56 | libc::SYS_landlock_create_ruleset, |
| 57 | std::ptr::null::<libc::c_void>(), |
| 58 | 0usize, |
| 59 | LANDLOCK_CREATE_RULESET_VERSION, |
| 60 | ); |
| 61 | if result >= 0 { |
| 62 | i32::try_from(result).ok() |
| 63 | } else { |
| 64 | None |
| 65 | } |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | // Landlock syscall constants (not yet in libc crate) |
| 70 | #[cfg(target_os = "linux")] |
| 71 | const LANDLOCK_CREATE_RULESET_VERSION: u32 = 1 << 0; |
| 72 | |
| 73 | #[cfg(target_os = "linux")] |
| 74 | const LANDLOCK_ACCESS_FS_EXECUTE: u64 = 1 << 0; |
| 75 | #[cfg(target_os = "linux")] |
| 76 | const LANDLOCK_ACCESS_FS_WRITE_FILE: u64 = 1 << 1; |
| 77 | #[cfg(target_os = "linux")] |
| 78 | const LANDLOCK_ACCESS_FS_READ_FILE: u64 = 1 << 2; |
| 79 | #[cfg(target_os = "linux")] |
| 80 | const LANDLOCK_ACCESS_FS_READ_DIR: u64 = 1 << 3; |
| 81 | #[cfg(target_os = "linux")] |
| 82 | const LANDLOCK_ACCESS_FS_REMOVE_DIR: u64 = 1 << 4; |
| 83 | #[cfg(target_os = "linux")] |
| 84 | const LANDLOCK_ACCESS_FS_REMOVE_FILE: u64 = 1 << 5; |
| 85 | #[cfg(target_os = "linux")] |
| 86 | const LANDLOCK_ACCESS_FS_MAKE_CHAR: u64 = 1 << 6; |
| 87 | #[cfg(target_os = "linux")] |
| 88 | const LANDLOCK_ACCESS_FS_MAKE_DIR: u64 = 1 << 7; |
| 89 | #[cfg(target_os = "linux")] |
| 90 | const LANDLOCK_ACCESS_FS_MAKE_REG: u64 = 1 << 8; |
| 91 | #[cfg(target_os = "linux")] |
| 92 | const LANDLOCK_ACCESS_FS_MAKE_SOCK: u64 = 1 << 9; |
| 93 | #[cfg(target_os = "linux")] |
| 94 | const LANDLOCK_ACCESS_FS_MAKE_FIFO: u64 = 1 << 10; |
| 95 | #[cfg(target_os = "linux")] |
| 96 | const LANDLOCK_ACCESS_FS_MAKE_BLOCK: u64 = 1 << 11; |
| 97 | #[cfg(target_os = "linux")] |
| 98 | const LANDLOCK_ACCESS_FS_MAKE_SYM: u64 = 1 << 12; |
| 99 | #[cfg(target_os = "linux")] |
| 100 | const LANDLOCK_ACCESS_FS_REFER: u64 = 1 << 13; |
| 101 | #[cfg(target_os = "linux")] |
| 102 | const LANDLOCK_ACCESS_FS_TRUNCATE: u64 = 1 << 14; |
| 103 | |
| 104 | // Combinations |
| 105 | #[cfg(target_os = "linux")] |
| 106 | const LANDLOCK_ACCESS_FS_READ: u64 = LANDLOCK_ACCESS_FS_READ_FILE | LANDLOCK_ACCESS_FS_READ_DIR; |
| 107 | |
| 108 | #[cfg(target_os = "linux")] |
| 109 | const LANDLOCK_ACCESS_FS_WRITE: u64 = LANDLOCK_ACCESS_FS_WRITE_FILE |
| 110 | | LANDLOCK_ACCESS_FS_REMOVE_DIR |
| 111 | | LANDLOCK_ACCESS_FS_REMOVE_FILE |
| 112 | | LANDLOCK_ACCESS_FS_MAKE_DIR |
| 113 | | LANDLOCK_ACCESS_FS_MAKE_REG |
| 114 | | LANDLOCK_ACCESS_FS_MAKE_SYM |
| 115 | | LANDLOCK_ACCESS_FS_TRUNCATE; |
| 116 | |
| 117 | /// Landlock ruleset attribute structure |
| 118 | #[cfg(target_os = "linux")] |
| 119 | #[repr(C)] |
| 120 | struct LandlockRulesetAttr { |
| 121 | handled_access_fs: u64, |
| 122 | } |
| 123 | |
| 124 | /// Landlock path beneath attribute structure |
| 125 | #[cfg(target_os = "linux")] |
| 126 | #[repr(C)] |
| 127 | struct LandlockPathBeneathAttr { |
| 128 | allowed_access: u64, |
| 129 | parent_fd: i32, |
| 130 | } |
| 131 | |
| 132 | /// Rule type constants |
| 133 | #[cfg(target_os = "linux")] |
| 134 | const LANDLOCK_RULE_PATH_BENEATH: u32 = 1; |
| 135 | |
| 136 | /// A configured Landlock sandbox |
| 137 | #[cfg(target_os = "linux")] |
| 138 | pub struct LandlockSandbox { |
| 139 | ruleset_fd: i32, |
| 140 | policy: SandboxPolicy, |
| 141 | } |
| 142 | |
| 143 | #[cfg(target_os = "linux")] |
| 144 | impl LandlockSandbox { |
| 145 | /// Create a new Landlock sandbox from policy |
| 146 | pub fn from_policy(policy: &SandboxPolicy) -> std::io::Result<Self> { |
| 147 | // Determine what filesystem access to handle (restrict) |
| 148 | let handled_access = |
| 149 | LANDLOCK_ACCESS_FS_EXECUTE | LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_WRITE; |
| 150 | |
| 151 | let attr = LandlockRulesetAttr { |
| 152 | handled_access_fs: handled_access, |
| 153 | }; |
| 154 | |
| 155 | // Create the ruleset |
| 156 | // Safety: `attr` is a valid pointer for the syscall duration and size is correct. |
| 157 | let ruleset_fd = unsafe { |
| 158 | libc::syscall( |
| 159 | libc::SYS_landlock_create_ruleset, |
| 160 | &raw const attr, |
| 161 | std::mem::size_of::<LandlockRulesetAttr>(), |
| 162 | 0u32, |
| 163 | ) |
| 164 | }; |
| 165 | |
| 166 | if ruleset_fd < 0 { |
| 167 | return Err(std::io::Error::last_os_error()); |
| 168 | } |
| 169 | |
| 170 | let ruleset_fd = i32::try_from(ruleset_fd).map_err(|_| { |
| 171 | std::io::Error::other("Failed to create Landlock ruleset: file descriptor out of range") |
| 172 | })?; |
| 173 | |
| 174 | Ok(Self { |
| 175 | ruleset_fd, |
| 176 | policy: policy.clone(), |
| 177 | }) |
| 178 | } |
| 179 | |
| 180 | /// Add a read-only rule for a path |
| 181 | pub fn allow_read(&self, path: &Path) -> std::io::Result<()> { |
| 182 | self.add_rule(path, LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_EXECUTE) |
| 183 | } |
| 184 | |
| 185 | /// Add a read-write rule for a path |
| 186 | pub fn allow_write(&self, path: &Path) -> std::io::Result<()> { |
| 187 | self.add_rule( |
| 188 | path, |
| 189 | LANDLOCK_ACCESS_FS_READ | LANDLOCK_ACCESS_FS_WRITE | LANDLOCK_ACCESS_FS_EXECUTE, |
| 190 | ) |
| 191 | } |
| 192 | |
| 193 | /// Add a path rule to the ruleset |
| 194 | fn add_rule(&self, path: &Path, access: u64) -> std::io::Result<()> { |
| 195 | let path_cstr = CString::new(path.to_string_lossy().as_bytes()) |
| 196 | .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid path"))?; |
| 197 | |
| 198 | // Open the path to get a file descriptor |
| 199 | // Safety: `path_cstr` is NUL-terminated and lives for the duration of the call. |
| 200 | let fd = unsafe { libc::open(path_cstr.as_ptr(), libc::O_PATH | libc::O_CLOEXEC) }; |
| 201 | |
| 202 | if fd < 0 { |
| 203 | // Path doesn't exist, skip this rule |
| 204 | return Ok(()); |
| 205 | } |
| 206 | |
| 207 | let attr = LandlockPathBeneathAttr { |
| 208 | allowed_access: access, |
| 209 | parent_fd: fd, |
| 210 | }; |
| 211 | |
| 212 | // Safety: `attr` is a valid pointer for the syscall duration. |
| 213 | let result = unsafe { |
| 214 | libc::syscall( |
| 215 | libc::SYS_landlock_add_rule, |
| 216 | self.ruleset_fd, |
| 217 | LANDLOCK_RULE_PATH_BENEATH, |
| 218 | &raw const attr, |
| 219 | 0u32, |
| 220 | ) |
| 221 | }; |
| 222 | |
| 223 | // Safety: `fd` is a valid file descriptor from libc::open. |
| 224 | unsafe { |
| 225 | libc::close(fd); |
| 226 | } |
| 227 | |
| 228 | if result < 0 { |
| 229 | return Err(std::io::Error::last_os_error()); |
| 230 | } |
| 231 | |
| 232 | Ok(()) |
| 233 | } |
| 234 | |
| 235 | /// Apply the sandbox to the current process |
| 236 | /// |
| 237 | /// WARNING: This is irreversible for the current process! |
| 238 | pub fn apply(&self) -> std::io::Result<()> { |
| 239 | // First, drop privileges using prctl |
| 240 | // Safety: prctl call uses constant arguments and does not access memory. |
| 241 | let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) }; |
| 242 | if result < 0 { |
| 243 | return Err(std::io::Error::last_os_error()); |
| 244 | } |
| 245 | |
| 246 | // Now restrict the process |
| 247 | // Safety: syscall uses a valid ruleset fd and no pointer arguments. |
| 248 | let result = |
| 249 | unsafe { libc::syscall(libc::SYS_landlock_restrict_self, self.ruleset_fd, 0u32) }; |
| 250 | |
| 251 | if result < 0 { |
| 252 | return Err(std::io::Error::last_os_error()); |
| 253 | } |
| 254 | |
| 255 | Ok(()) |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | #[cfg(target_os = "linux")] |
| 260 | impl Drop for LandlockSandbox { |
| 261 | fn drop(&mut self) { |
| 262 | // Safety: `ruleset_fd` is a valid descriptor created by landlock. |
| 263 | unsafe { |
| 264 | libc::close(self.ruleset_fd); |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | /// Create a helper script that sets up Landlock before running the command. |
| 270 | /// |
| 271 | /// Since Landlock restricts the current process, we need a helper that: |
| 272 | /// 1. Sets up the Landlock ruleset |
| 273 | /// 2. Applies the restrictions |
| 274 | /// 3. Execs the target command |
| 275 | /// |
| 276 | /// This returns the command to run with the helper. |
| 277 | #[cfg(target_os = "linux")] |
| 278 | pub fn create_landlock_wrapper( |
| 279 | spec: &CommandSpec, |
| 280 | _writable_paths: &[std::path::PathBuf], |
| 281 | _readable_paths: &[std::path::PathBuf], |
| 282 | ) -> Vec<String> { |
| 283 | // For simplicity, we'll use a shell wrapper that applies Landlock via a helper binary |
| 284 | // In production, this would be a compiled binary that's part of the CLI |
| 285 | |
| 286 | // For now, just return the original command without sandboxing |
| 287 | // A full implementation would include a compiled landlock-helper binary |
| 288 | let mut cmd = vec![spec.program.clone()]; |
| 289 | cmd.extend(spec.args.clone()); |
| 290 | cmd |
| 291 | } |
| 292 | |
| 293 | /// Detect if a failure was caused by Landlock denial |
| 294 | #[cfg(target_os = "linux")] |
| 295 | pub fn detect_denial(exit_code: i32, stderr: &str) -> bool { |
| 296 | if exit_code == 0 { |
| 297 | return false; |
| 298 | } |
| 299 | |
| 300 | // Landlock denials typically result in EACCES or EPERM |
| 301 | stderr.contains("Permission denied") |
| 302 | || stderr.contains("Operation not permitted") |
| 303 | || stderr.contains("EACCES") |
| 304 | || stderr.contains("EPERM") |
| 305 | } |
| 306 | |
| 307 | // Stub implementations for non-Linux platforms |
| 308 | #[cfg(not(target_os = "linux"))] |
| 309 | pub fn get_abi_version() -> Option<i32> { |
| 310 | None |
| 311 | } |
| 312 | |
| 313 | #[cfg(not(target_os = "linux"))] |
| 314 | pub fn detect_denial(_exit_code: i32, _stderr: &str) -> bool { |
| 315 | false |
| 316 | } |
| 317 | |
| 318 | #[cfg(test)] |
| 319 | mod tests { |
| 320 | use super::*; |
| 321 | |
| 322 | #[test] |
| 323 | fn test_is_available() { |
| 324 | // This test will pass regardless of platform |
| 325 | let _ = is_available(); |
| 326 | } |
| 327 | |
| 328 | #[test] |
| 329 | #[cfg(target_os = "linux")] |
| 330 | fn test_get_abi_version() { |
| 331 | // May or may not be available depending on kernel |
| 332 | let _ = get_abi_version(); |
| 333 | } |
| 334 | |
| 335 | #[test] |
| 336 | fn test_detect_denial() { |
| 337 | #[cfg(target_os = "linux")] |
| 338 | { |
| 339 | assert!(detect_denial(1, "Permission denied")); |
| 340 | assert!(detect_denial(1, "Operation not permitted")); |
| 341 | assert!(!detect_denial(0, "Success")); |
| 342 | } |
| 343 | } |
| 344 | } |
| 345 |