diff --git a/Cargo.lock b/Cargo.lock index 1575ce0..996638a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1589,6 +1589,7 @@ dependencies = [ "rand_chacha", "serde", "serde_json", + "syscalls", "tempfile", "thiserror 2.0.18", "tokio", @@ -1801,6 +1802,12 @@ dependencies = [ "syn", ] +[[package]] +name = "syscalls" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81c645a4de0d803ced6ef0388a2646aa1ef8467173b5d59a2c33c88de4ab76e7" + [[package]] name = "tagptr" version = "0.2.0" diff --git a/crates/sandlock-core/Cargo.toml b/crates/sandlock-core/Cargo.toml index 8eddc2a..4e83919 100644 --- a/crates/sandlock-core/Cargo.toml +++ b/crates/sandlock-core/Cargo.toml @@ -10,6 +10,7 @@ description = "Lightweight process sandbox using Landlock, seccomp-bpf, and secc [dependencies] libc = "0.2" +syscalls = { version = "0.8", default-features = false } nix = { version = "0.29", features = ["process", "signal", "fs", "ioctl", "poll"] } tokio = { version = "1", features = ["rt", "net", "time", "sync", "macros", "io-util"] } serde = { version = "1", features = ["derive"] } diff --git a/crates/sandlock-core/src/arch.rs b/crates/sandlock-core/src/arch.rs index 7d599e8..7fd8d0b 100644 --- a/crates/sandlock-core/src/arch.rs +++ b/crates/sandlock-core/src/arch.rs @@ -1,148 +1,100 @@ -//! Architecture-specific syscall and seccomp helpers. +//! Architecture-specific seccomp helpers. +//! +//! Syscall numbers come from the `syscalls` crate (generated from the kernel +//! ABI tables). The only genuinely per-architecture datum that the crate does +//! not provide is `AUDIT_ARCH` (a `linux/audit.h` token, not a syscall +//! number), so that is the sole hand-maintained per-arch constant here. -/// `faccessat2(2)` syscall number on Sandlock's supported Linux architectures. -/// -/// The `libc` crate does not expose this constant on all supported build -/// targets yet, but the seccomp filters and path virtualization handlers need -/// to intercept it because glibc 2.33+ may prefer it over `faccessat`. -pub const SYS_FACCESSAT2: i64 = 439; +use syscalls::Sysno; + +// Numbers for syscalls that exist on every architecture Sandlock targets, so a +// single definition resolves to the correct per-arch number at compile time. +// The `tests` module pins the resolved values to the historical constants. +pub const SYS_FACCESSAT2: i64 = Sysno::faccessat2 as i64; +pub const SYS_OPENAT2: i64 = Sysno::openat2 as i64; +pub const SYS_SECCOMP: i64 = Sysno::seccomp as i64; +pub const SYS_MEMFD_CREATE: i64 = Sysno::memfd_create as i64; +pub const SYS_PIDFD_OPEN: i64 = Sysno::pidfd_open as i64; +pub const SYS_PIDFD_GETFD: i64 = Sysno::pidfd_getfd as i64; #[cfg(target_arch = "x86_64")] mod imp { pub const AUDIT_ARCH: u32 = 0xC000_003E; - pub const MAX_SYSCALL_NR: i64 = 462; - pub const SYS_SECCOMP: i64 = 317; - pub const SYS_MEMFD_CREATE: i64 = 319; - pub const SYS_PIDFD_OPEN: i64 = 434; - pub const SYS_PIDFD_GETFD: i64 = 438; - pub const SYS_OPENAT2: i64 = 437; - - pub const SYS_OPEN: Option = Some(libc::SYS_open); - pub const SYS_STAT: Option = Some(libc::SYS_stat); - pub const SYS_LSTAT: Option = Some(libc::SYS_lstat); - pub const SYS_ACCESS: Option = Some(libc::SYS_access); - pub const SYS_READLINK: Option = Some(libc::SYS_readlink); - pub const SYS_GETDENTS: Option = Some(libc::SYS_getdents); - pub const SYS_UNLINK: Option = Some(libc::SYS_unlink); - pub const SYS_RMDIR: Option = Some(libc::SYS_rmdir); - pub const SYS_MKDIR: Option = Some(libc::SYS_mkdir); - pub const SYS_RENAME: Option = Some(libc::SYS_rename); - pub const SYS_SYMLINK: Option = Some(libc::SYS_symlink); - pub const SYS_LINK: Option = Some(libc::SYS_link); - pub const SYS_CHMOD: Option = Some(libc::SYS_chmod); - pub const SYS_CHOWN: Option = Some(libc::SYS_chown); - pub const SYS_LCHOWN: Option = Some(libc::SYS_lchown); - pub const SYS_VFORK: Option = Some(libc::SYS_vfork); - pub const SYS_FUTIMESAT: Option = Some(libc::SYS_futimesat); - pub const SYS_FORK: Option = Some(libc::SYS_fork); - pub const SYS_IOPERM: Option = Some(libc::SYS_ioperm); - pub const SYS_IOPL: Option = Some(libc::SYS_iopl); - pub const SYS_TIME: Option = Some(libc::SYS_time); - - /// Every syscall the kernel will dispatch through `handle_fork`. - /// Single source of truth for callers that enumerate fork-class - /// syscalls (BPF notif registration in `seccomp::dispatch`, - /// classification in `resource::is_process_creation_notif`). - pub const FORK_LIKE_SYSCALLS: &[i64] = &[ - libc::SYS_clone, - libc::SYS_clone3, - libc::SYS_vfork, - libc::SYS_fork, - ]; } #[cfg(target_arch = "aarch64")] mod imp { pub const AUDIT_ARCH: u32 = 0xC000_00B7; - pub const MAX_SYSCALL_NR: i64 = 463; - pub const SYS_SECCOMP: i64 = 277; - pub const SYS_MEMFD_CREATE: i64 = 279; - pub const SYS_PIDFD_OPEN: i64 = 434; - pub const SYS_PIDFD_GETFD: i64 = 438; - pub const SYS_OPENAT2: i64 = 437; - - pub const SYS_OPEN: Option = None; - pub const SYS_STAT: Option = None; - pub const SYS_LSTAT: Option = None; - pub const SYS_ACCESS: Option = None; - pub const SYS_READLINK: Option = None; - pub const SYS_GETDENTS: Option = None; - pub const SYS_UNLINK: Option = None; - pub const SYS_RMDIR: Option = None; - pub const SYS_MKDIR: Option = None; - pub const SYS_RENAME: Option = None; - pub const SYS_SYMLINK: Option = None; - pub const SYS_LINK: Option = None; - pub const SYS_CHMOD: Option = None; - pub const SYS_CHOWN: Option = None; - pub const SYS_LCHOWN: Option = None; - pub const SYS_VFORK: Option = None; - pub const SYS_FUTIMESAT: Option = None; - pub const SYS_FORK: Option = None; - pub const SYS_IOPERM: Option = None; - pub const SYS_IOPL: Option = None; - pub const SYS_TIME: Option = None; - - /// Every syscall the kernel will dispatch through `handle_fork`. - /// aarch64 has no `fork`/`vfork` (glibc emulates via `clone`). - pub const FORK_LIKE_SYSCALLS: &[i64] = &[ - libc::SYS_clone, - libc::SYS_clone3, - ]; } #[cfg(target_arch = "riscv64")] mod imp { // AUDIT_ARCH_RISCV64 = EM_RISCV(243) | __AUDIT_ARCH_64BIT | __AUDIT_ARCH_LE. pub const AUDIT_ARCH: u32 = 0xC000_00F3; - pub const MAX_SYSCALL_NR: i64 = 463; - pub const SYS_SECCOMP: i64 = 277; - pub const SYS_MEMFD_CREATE: i64 = 279; - pub const SYS_PIDFD_OPEN: i64 = 434; - pub const SYS_PIDFD_GETFD: i64 = 438; - pub const SYS_OPENAT2: i64 = 437; - - // riscv64 uses the generic syscall ABI: no legacy open/stat/fork/etc. - pub const SYS_OPEN: Option = None; - pub const SYS_STAT: Option = None; - pub const SYS_LSTAT: Option = None; - pub const SYS_ACCESS: Option = None; - pub const SYS_READLINK: Option = None; - pub const SYS_GETDENTS: Option = None; - pub const SYS_UNLINK: Option = None; - pub const SYS_RMDIR: Option = None; - pub const SYS_MKDIR: Option = None; - pub const SYS_RENAME: Option = None; - pub const SYS_SYMLINK: Option = None; - pub const SYS_LINK: Option = None; - pub const SYS_CHMOD: Option = None; - pub const SYS_CHOWN: Option = None; - pub const SYS_LCHOWN: Option = None; - pub const SYS_VFORK: Option = None; - pub const SYS_FUTIMESAT: Option = None; - pub const SYS_FORK: Option = None; - pub const SYS_IOPERM: Option = None; - pub const SYS_IOPL: Option = None; - pub const SYS_TIME: Option = None; - - /// Every syscall the kernel will dispatch through `handle_fork`. - /// riscv64 has no `fork`/`vfork` (glibc emulates via `clone`). - pub const FORK_LIKE_SYSCALLS: &[i64] = &[ - libc::SYS_clone, - libc::SYS_clone3, - ]; } pub use imp::*; -/// True if `nr` is plausibly a syscall number on the current architecture. +/// Resolve a syscall name to its number on the current architecture, or `None` +/// if this architecture's ABI does not provide it. +fn sysno(name: &str) -> Option { + name.parse::().ok().map(|s| s.id() as i64) +} + +macro_rules! legacy_syscall { + ($fn:ident, $name:literal) => { + #[doc = concat!( + "`", $name, "` syscall number on this architecture, or `None` if ", + "the generic syscall ABI (aarch64, riscv64) omits it." + )] + pub fn $fn() -> Option { + sysno($name) + } + }; +} + +// Legacy (pre-generic-ABI) syscalls: present on x86_64, absent on the +// generic-ABI architectures. Presence is derived from the crate's per-arch +// tables rather than hand-maintained. +legacy_syscall!(sys_open, "open"); +legacy_syscall!(sys_stat, "stat"); +legacy_syscall!(sys_lstat, "lstat"); +legacy_syscall!(sys_access, "access"); +legacy_syscall!(sys_readlink, "readlink"); +legacy_syscall!(sys_getdents, "getdents"); +legacy_syscall!(sys_unlink, "unlink"); +legacy_syscall!(sys_rmdir, "rmdir"); +legacy_syscall!(sys_mkdir, "mkdir"); +legacy_syscall!(sys_rename, "rename"); +legacy_syscall!(sys_symlink, "symlink"); +legacy_syscall!(sys_link, "link"); +legacy_syscall!(sys_chmod, "chmod"); +legacy_syscall!(sys_chown, "chown"); +legacy_syscall!(sys_lchown, "lchown"); +legacy_syscall!(sys_vfork, "vfork"); +legacy_syscall!(sys_fork, "fork"); + +/// Fork-class syscalls present on this architecture: `clone`/`clone3` always, +/// plus `fork`/`vfork` only where the legacy ABI provides them. Single source +/// of truth for callers enumerating fork-class syscalls (BPF notif +/// registration in `seccomp::dispatch`, classification in +/// `resource::is_process_creation_notif`). +pub fn fork_like_syscalls() -> Vec { + ["clone", "clone3", "vfork", "fork"] + .into_iter() + .filter_map(sysno) + .collect() +} + +/// True if `nr` is a real syscall number on the current architecture. /// Used by [`crate::seccomp::syscall::Syscall::checked`] to reject foot-gun /// cases like negative or arch-mismatched numbers. /// -/// Conservative: validates `0 <= nr <= MAX_SYSCALL_NR`. Doesn't enumerate -/// every nr — kernel's seccomp filter rejects unknowns at JEQ stage anyway. +/// Exact: backed by the `syscalls` crate's per-arch table, so unassigned +/// numbers within the table's range are rejected too (unlike a bare range +/// check against the highest known number). pub fn is_known_syscall(nr: i64) -> bool { - nr >= 0 && nr <= imp::MAX_SYSCALL_NR + nr >= 0 && Sysno::new(nr as usize).is_some() } pub fn push_optional_syscall(v: &mut Vec, nr: Option) { @@ -150,3 +102,69 @@ pub fn push_optional_syscall(v: &mut Vec, nr: Option) { v.push(nr as u32); } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Pin the crate-sourced syscall numbers to the values Sandlock used + /// before adopting the crate, per architecture. A divergence here means a + /// crate upgrade changed an ABI number out from under the seccomp filters. + #[test] + fn crate_sourced_consts_match_historical_values() { + #[cfg(target_arch = "x86_64")] + { + assert_eq!(SYS_SECCOMP, 317); + assert_eq!(SYS_MEMFD_CREATE, 319); + assert_eq!(SYS_PIDFD_OPEN, 434); + assert_eq!(SYS_PIDFD_GETFD, 438); + assert_eq!(SYS_OPENAT2, 437); + assert_eq!(SYS_FACCESSAT2, 439); + } + #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] + { + assert_eq!(SYS_SECCOMP, 277); + assert_eq!(SYS_MEMFD_CREATE, 279); + assert_eq!(SYS_PIDFD_OPEN, 434); + assert_eq!(SYS_PIDFD_GETFD, 438); + assert_eq!(SYS_OPENAT2, 437); + assert_eq!(SYS_FACCESSAT2, 439); + } + } + + /// The legacy-syscall accessors must reflect this arch's ABI: present on + /// x86_64, absent on the generic-ABI arches. + #[test] + fn legacy_accessors_match_arch() { + #[cfg(target_arch = "x86_64")] + { + assert_eq!(sys_open(), Some(libc::SYS_open)); + assert_eq!(sys_fork(), Some(libc::SYS_fork)); + assert_eq!(sys_vfork(), Some(libc::SYS_vfork)); + assert_eq!( + fork_like_syscalls(), + vec![ + libc::SYS_clone, + libc::SYS_clone3, + libc::SYS_vfork, + libc::SYS_fork + ] + ); + } + #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] + { + assert_eq!(sys_open(), None); + assert_eq!(sys_fork(), None); + assert_eq!(sys_vfork(), None); + assert_eq!(fork_like_syscalls(), vec![libc::SYS_clone, libc::SYS_clone3]); + } + } + + #[test] + fn is_known_syscall_accepts_real_and_rejects_bogus() { + assert!(is_known_syscall(libc::SYS_openat)); + assert!(is_known_syscall(libc::SYS_clone)); + assert!(!is_known_syscall(-1)); + assert!(!is_known_syscall(99_999)); + } +} diff --git a/crates/sandlock-core/src/chroot/resolve.rs b/crates/sandlock-core/src/chroot/resolve.rs index d002bcd..932c06e 100644 --- a/crates/sandlock-core/src/chroot/resolve.rs +++ b/crates/sandlock-core/src/chroot/resolve.rs @@ -40,8 +40,8 @@ pub fn to_virtual_path(chroot_root: &Path, host_path: &Path) -> Option // openat2(RESOLVE_IN_ROOT) based resolution // ============================================================ -/// openat2 syscall number (same on x86_64 and aarch64). -const SYS_OPENAT2: libc::c_long = 437; +/// openat2 syscall number, sourced from the `syscalls` crate via `arch`. +const SYS_OPENAT2: libc::c_long = crate::arch::SYS_OPENAT2; /// RESOLVE_IN_ROOT — treat the dirfd as the filesystem root for resolution. const RESOLVE_IN_ROOT: u64 = 0x10; diff --git a/crates/sandlock-core/src/context.rs b/crates/sandlock-core/src/context.rs index c4ba86a..fff11e2 100644 --- a/crates/sandlock-core/src/context.rs +++ b/crates/sandlock-core/src/context.rs @@ -5,6 +5,8 @@ use std::ffi::CString; use std::io; use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; +use syscalls::{Sysno, SysnoSet}; + use crate::arch; use crate::sandbox::Sandbox; use crate::seccomp::bpf::{self, stmt, jump}; @@ -119,7 +121,8 @@ pub(crate) fn read_u32_fd(fd: RawFd) -> io::Result { Ok(u32::from_le_bytes(buf)) } -use crate::seccomp::syscall_names::syscall_name_to_nr; +#[cfg(test)] +use crate::seccomp::syscall::syscall_name_to_nr; // ============================================================ // Sandbox → syscall lists @@ -151,11 +154,6 @@ impl SyscallList { } } - fn extend_optional(&mut self, syscalls: &[Option]) { - for &nr in syscalls { - self.push_optional(nr); - } - } fn finish(mut self) -> Vec { self.nrs.sort_unstable(); @@ -207,15 +205,11 @@ const TIME_NOTIF_SYSCALLS: &[i64] = &[ // check, so we have to put every variant on the notif list -- otherwise // a caller that picks `open` or `openat2` slips past virtualization // and reads the real on-disk file. -const PROCFS_HOSTS_NOTIF_SYSCALLS: &[i64] = &[ - libc::SYS_openat, - arch::SYS_OPENAT2, - libc::SYS_getdents64, -]; -const PROCFS_HOSTS_OPTIONAL_SYSCALLS: &[Option] = &[ - arch::SYS_OPEN, - arch::SYS_GETDENTS, -]; +fn procfs_hosts_notif_syscalls() -> Vec { + let mut v = vec![libc::SYS_openat, arch::SYS_OPENAT2, libc::SYS_getdents64]; + v.extend([arch::sys_open(), arch::sys_getdents()].into_iter().flatten()); + v +} // Netlink virtualization (always on): // socket, bind, getsockname -- swap in a unix socketpair for AF_NETLINK @@ -234,101 +228,122 @@ const NETLINK_NOTIF_SYSCALLS: &[i64] = &[ libc::SYS_close, ]; -const COW_PATH_SYSCALLS: &[i64] = &[ - libc::SYS_openat, - libc::SYS_execve, - libc::SYS_execveat, - libc::SYS_unlinkat, - libc::SYS_mkdirat, - libc::SYS_renameat2, - libc::SYS_symlinkat, - libc::SYS_linkat, - libc::SYS_fchmodat, - libc::SYS_fchownat, - libc::SYS_truncate, - libc::SYS_utimensat, - libc::SYS_newfstatat, - libc::SYS_statx, - libc::SYS_faccessat, - arch::SYS_FACCESSAT2, - libc::SYS_readlinkat, - libc::SYS_getdents64, - libc::SYS_chdir, - libc::SYS_getcwd, -]; -const COW_LEGACY_PATH_SYSCALLS: &[Option] = &[ - arch::SYS_OPEN, - arch::SYS_UNLINK, - arch::SYS_RMDIR, - arch::SYS_MKDIR, - arch::SYS_RENAME, - arch::SYS_SYMLINK, - arch::SYS_LINK, - arch::SYS_CHMOD, - arch::SYS_CHOWN, - arch::SYS_LCHOWN, - arch::SYS_STAT, - arch::SYS_LSTAT, - arch::SYS_ACCESS, - arch::SYS_READLINK, - arch::SYS_GETDENTS, -]; +fn cow_path_syscalls() -> Vec { + let mut v = vec![ + libc::SYS_openat, + libc::SYS_execve, + libc::SYS_execveat, + libc::SYS_unlinkat, + libc::SYS_mkdirat, + libc::SYS_renameat2, + libc::SYS_symlinkat, + libc::SYS_linkat, + libc::SYS_fchmodat, + libc::SYS_fchownat, + libc::SYS_truncate, + libc::SYS_utimensat, + libc::SYS_newfstatat, + libc::SYS_statx, + libc::SYS_faccessat, + arch::SYS_FACCESSAT2, + libc::SYS_readlinkat, + libc::SYS_getdents64, + libc::SYS_chdir, + libc::SYS_getcwd, + ]; + v.extend( + [ + arch::sys_open(), + arch::sys_unlink(), + arch::sys_rmdir(), + arch::sys_mkdir(), + arch::sys_rename(), + arch::sys_symlink(), + arch::sys_link(), + arch::sys_chmod(), + arch::sys_chown(), + arch::sys_lchown(), + arch::sys_stat(), + arch::sys_lstat(), + arch::sys_access(), + arch::sys_readlink(), + arch::sys_getdents(), + ] + .into_iter() + .flatten(), + ); + v +} -const CHROOT_PATH_SYSCALLS: &[i64] = &[ - libc::SYS_openat, - libc::SYS_execve, - libc::SYS_execveat, - libc::SYS_unlinkat, - libc::SYS_mkdirat, - libc::SYS_renameat2, - libc::SYS_symlinkat, - libc::SYS_linkat, - libc::SYS_fchmodat, - libc::SYS_fchownat, - libc::SYS_truncate, - libc::SYS_newfstatat, - libc::SYS_statx, - libc::SYS_faccessat, - arch::SYS_FACCESSAT2, - libc::SYS_readlinkat, - libc::SYS_getdents64, - libc::SYS_chdir, - libc::SYS_getcwd, - libc::SYS_statfs, - libc::SYS_utimensat, -]; -const CHROOT_LEGACY_PATH_SYSCALLS: &[Option] = &[ - arch::SYS_OPEN, - arch::SYS_STAT, - arch::SYS_LSTAT, - arch::SYS_ACCESS, - arch::SYS_READLINK, - arch::SYS_GETDENTS, - arch::SYS_UNLINK, - arch::SYS_RMDIR, - arch::SYS_MKDIR, - arch::SYS_RENAME, - arch::SYS_SYMLINK, - arch::SYS_LINK, - arch::SYS_CHMOD, - arch::SYS_CHOWN, - arch::SYS_LCHOWN, -]; +fn chroot_path_syscalls() -> Vec { + let mut v = vec![ + libc::SYS_openat, + libc::SYS_execve, + libc::SYS_execveat, + libc::SYS_unlinkat, + libc::SYS_mkdirat, + libc::SYS_renameat2, + libc::SYS_symlinkat, + libc::SYS_linkat, + libc::SYS_fchmodat, + libc::SYS_fchownat, + libc::SYS_truncate, + libc::SYS_newfstatat, + libc::SYS_statx, + libc::SYS_faccessat, + arch::SYS_FACCESSAT2, + libc::SYS_readlinkat, + libc::SYS_getdents64, + libc::SYS_chdir, + libc::SYS_getcwd, + libc::SYS_statfs, + libc::SYS_utimensat, + ]; + v.extend( + [ + arch::sys_open(), + arch::sys_stat(), + arch::sys_lstat(), + arch::sys_access(), + arch::sys_readlink(), + arch::sys_getdents(), + arch::sys_unlink(), + arch::sys_rmdir(), + arch::sys_mkdir(), + arch::sys_rename(), + arch::sys_symlink(), + arch::sys_link(), + arch::sys_chmod(), + arch::sys_chown(), + arch::sys_lchown(), + ] + .into_iter() + .flatten(), + ); + v +} -const FS_DENIED_PATH_SYSCALLS: &[i64] = &[ - libc::SYS_openat, - libc::SYS_execve, - libc::SYS_execveat, - libc::SYS_linkat, - libc::SYS_renameat2, - libc::SYS_symlinkat, -]; -const FS_DENIED_LEGACY_PATH_SYSCALLS: &[Option] = &[ - arch::SYS_OPEN, - arch::SYS_LINK, - arch::SYS_RENAME, - arch::SYS_SYMLINK, -]; +fn fs_denied_path_syscalls() -> Vec { + let mut v = vec![ + libc::SYS_openat, + libc::SYS_execve, + libc::SYS_execveat, + libc::SYS_linkat, + libc::SYS_renameat2, + libc::SYS_symlinkat, + ]; + v.extend( + [ + arch::sys_open(), + arch::sys_link(), + arch::sys_rename(), + arch::sys_symlink(), + ] + .into_iter() + .flatten(), + ); + v +} const POLICY_EVENT_SYSCALLS: &[i64] = &[ libc::SYS_openat, @@ -354,7 +369,7 @@ fn needs_network_supervision(policy: &Sandbox) -> bool { /// Determine which syscalls need `SECCOMP_RET_USER_NOTIF`. pub fn notif_syscalls(policy: &Sandbox, sandbox_name: Option<&str>) -> Vec { let mut nrs = SyscallList::with(BASE_NOTIF_SYSCALLS); - nrs.push_optional(arch::SYS_VFORK); + nrs.push_optional(arch::sys_vfork()); // Bare fork(2) carries none of the namespace/process-limit risk of // clone/clone3 and was historically left out of the BPF filter so @@ -363,7 +378,7 @@ pub fn notif_syscalls(policy: &Sandbox, sandbox_name: Option<&str>) -> Vec // supervisor can register the new child via ptrace fork events // before it can run user code (argv-safety invariant). if policy.policy_fn.is_some() { - nrs.push_optional(arch::SYS_FORK); + nrs.push_optional(arch::sys_fork()); } if policy.max_memory.is_some() { @@ -390,8 +405,7 @@ pub fn notif_syscalls(policy: &Sandbox, sandbox_name: Option<&str>) -> Vec nrs.extend(TIME_NOTIF_SYSCALLS); } - nrs.extend(PROCFS_HOSTS_NOTIF_SYSCALLS); - nrs.extend_optional(PROCFS_HOSTS_OPTIONAL_SYSCALLS); + nrs.extend(&procfs_hosts_notif_syscalls()); nrs.extend(NETLINK_NOTIF_SYSCALLS); // Virtualize sched_getaffinity so nproc/sysconf agree with /proc/cpuinfo @@ -404,20 +418,17 @@ pub fn notif_syscalls(policy: &Sandbox, sandbox_name: Option<&str>) -> Vec // COW filesystem interception (seccomp-based, unprivileged) if policy.workdir.is_some() { - nrs.extend(COW_PATH_SYSCALLS); - nrs.extend_optional(COW_LEGACY_PATH_SYSCALLS); + nrs.extend(&cow_path_syscalls()); } // Chroot path interception if policy.chroot.is_some() { - nrs.extend(CHROOT_PATH_SYSCALLS); - nrs.extend_optional(CHROOT_LEGACY_PATH_SYSCALLS); + nrs.extend(&chroot_path_syscalls()); } // Explicit deny-paths need path-bearing syscalls intercepted. if !policy.fs_denied.is_empty() { - nrs.extend(FS_DENIED_PATH_SYSCALLS); - nrs.extend_optional(FS_DENIED_LEGACY_PATH_SYSCALLS); + nrs.extend(&fs_denied_path_syscalls()); } // Dynamic policy callback — intercept key syscalls for event emission. @@ -433,28 +444,36 @@ pub fn notif_syscalls(policy: &Sandbox, sandbox_name: Option<&str>) -> Vec nrs.finish() } -/// Resolve `NO_SUPERVISOR_BLOCKLIST_SYSCALLS` names to numbers, plus -/// SysV IPC syscalls when `policy.allows_sysv_ipc()` is false. -pub fn no_supervisor_blocklist_syscall_numbers(policy: &Sandbox) -> Vec { - use crate::sys::structs::NO_SUPERVISOR_BLOCKLIST_SYSCALLS; - let mut nrs: Vec = NO_SUPERVISOR_BLOCKLIST_SYSCALLS +/// Resolve `base` syscall names plus policy extras (and SysV IPC syscalls when +/// `policy.allows_sysv_ipc()` is false) to a deduplicated, ascending list of +/// numbers for the current architecture. +/// +/// A `SysnoSet` accumulates the membership: it dedups inherently (so SysV IPC +/// folds in with a plain `insert`) and iterates in ascending syscall order. +/// Names that do not exist on this architecture resolve to nothing and are +/// skipped, so the result stays arch-correct. +fn resolve_blocklist(base: &[&str], policy: &Sandbox) -> Vec { + let mut set: SysnoSet = base .iter() .copied() .chain(policy.extra_deny_syscalls.iter().map(String::as_str)) - .filter_map(|n| syscall_name_to_nr(n)) + .filter_map(|n| n.parse::().ok()) .collect(); if !policy.allows_sysv_ipc() { for name in SYSV_IPC_BLOCKLIST_SYSCALLS { - if let Some(nr) = syscall_name_to_nr(name) { - if !nrs.contains(&nr) { - nrs.push(nr); - } + if let Ok(sysno) = name.parse::() { + set.insert(sysno); } } } - nrs.sort_unstable(); - nrs.dedup(); - nrs + set.iter().map(|s| s.id() as u32).collect() +} + +/// Resolve `NO_SUPERVISOR_BLOCKLIST_SYSCALLS` names to numbers, plus +/// SysV IPC syscalls when `policy.allows_sysv_ipc()` is false. +pub fn no_supervisor_blocklist_syscall_numbers(policy: &Sandbox) -> Vec { + use crate::sys::structs::NO_SUPERVISOR_BLOCKLIST_SYSCALLS; + resolve_blocklist(NO_SUPERVISOR_BLOCKLIST_SYSCALLS, policy) } /// Resolve the default syscall blocklist plus policy extras to numbers. @@ -462,24 +481,7 @@ pub fn no_supervisor_blocklist_syscall_numbers(policy: &Sandbox) -> Vec { /// SysV IPC syscalls are appended to the resolved blocklist when /// `policy.allows_sysv_ipc()` is false. pub fn blocklist_syscall_numbers(policy: &Sandbox) -> Vec { - let mut nrs: Vec = DEFAULT_BLOCKLIST_SYSCALLS - .iter() - .copied() - .chain(policy.extra_deny_syscalls.iter().map(String::as_str)) - .filter_map(|n| syscall_name_to_nr(n)) - .collect(); - if !policy.allows_sysv_ipc() { - for name in SYSV_IPC_BLOCKLIST_SYSCALLS { - if let Some(nr) = syscall_name_to_nr(name) { - if !nrs.contains(&nr) { - nrs.push(nr); - } - } - } - } - nrs.sort_unstable(); - nrs.dedup(); - nrs + resolve_blocklist(DEFAULT_BLOCKLIST_SYSCALLS, policy) } /// Build argument-level seccomp filter instructions matching the Python @@ -928,7 +930,7 @@ pub(crate) fn confine_child(args: ChildSpawnArgs<'_>) -> ! { n == libc::SYS_execve as u32 || n == libc::SYS_execveat as u32 }); if exec_extra { - arch::push_optional_syscall(&mut notif, arch::SYS_FORK); + arch::push_optional_syscall(&mut notif, arch::sys_fork()); } notif.sort_unstable(); notif.dedup(); @@ -1084,21 +1086,21 @@ mod tests { let nrs = notif_syscalls(&policy, None); assert!(nrs.contains(&(libc::SYS_clone as u32))); assert!(nrs.contains(&(libc::SYS_clone3 as u32))); - if let Some(vfork) = arch::SYS_VFORK { + if let Some(vfork) = arch::sys_vfork() { assert!(nrs.contains(&(vfork as u32))); } // Bare fork(2) is intercepted only when policy_fn is active — // see notif_syscalls. The default policy has no policy_fn, so // fork stays out of the BPF filter and hot fork-loops keep // bypassing the supervisor. - if let Some(fork) = arch::SYS_FORK { + if let Some(fork) = arch::sys_fork() { assert!(!nrs.contains(&(fork as u32))); } } #[test] fn test_notif_syscalls_fork_gated_on_policy_fn() { - let Some(fork) = arch::SYS_FORK else { return }; + let Some(fork) = arch::sys_fork() else { return }; let policy = Sandbox::builder() .policy_fn(|_event, _ctx| crate::policy_fn::Verdict::Allow) .build() @@ -1338,8 +1340,10 @@ mod tests { fn test_syscall_name_to_nr_covers_defaults() { // Every name in DEFAULT_BLOCKLIST_SYSCALLS should resolve unless the // running architecture does not expose that syscall. + // `nfsservctl` now resolves: the syscalls crate carries it (kernel + // returns ENOSYS, but the ABI number exists), so it is enforced in the + // blocklist rather than silently dropped. `ioperm`/`iopl` are x86-only. let expected_unresolved: &[&str] = &[ - "nfsservctl", #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] "ioperm", #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] diff --git a/crates/sandlock-core/src/cow/dispatch.rs b/crates/sandlock-core/src/cow/dispatch.rs index 1b9004b..d6659d7 100644 --- a/crates/sandlock-core/src/cow/dispatch.rs +++ b/crates/sandlock-core/src/cow/dispatch.rs @@ -164,7 +164,7 @@ pub(crate) async fn handle_cow_open( // open(path, flags, mode): args[0]=path, args[1]=flags, args[2]=mode // openat(dirfd, path, flags, mode): args[0]=dirfd, args[1]=path, args[2]=flags, args[3]=mode - let (path_ptr, dirfd, flags, mode) = if Some(nr) == arch::SYS_OPEN { + let (path_ptr, dirfd, flags, mode) = if Some(nr) == arch::sys_open() { (notif.data.args[0], libc::AT_FDCWD as i64, notif.data.args[1], notif.data.args[2]) } else { (notif.data.args[1], notif.data.args[0] as i64, notif.data.args[2], notif.data.args[3]) @@ -377,43 +377,43 @@ fn parse_cow_write( } // Legacy variants (path in args[0], no dirfd) - if Some(nr) == arch::SYS_UNLINK { + if Some(nr) == arch::sys_unlink() { return Some(CowWriteOp::Unlink { path: read_resolved(notif, 0, None, notif_fd, virtual_cwd)?, is_dir: false, }); } - if Some(nr) == arch::SYS_RMDIR { + if Some(nr) == arch::sys_rmdir() { return Some(CowWriteOp::Unlink { path: read_resolved(notif, 0, None, notif_fd, virtual_cwd)?, is_dir: true, }); } - if Some(nr) == arch::SYS_MKDIR { + if Some(nr) == arch::sys_mkdir() { return Some(CowWriteOp::Mkdir { path: read_resolved(notif, 0, None, notif_fd, virtual_cwd)?, }); } - if Some(nr) == arch::SYS_RENAME { + if Some(nr) == arch::sys_rename() { let old_path = read_resolved(notif, 0, None, notif_fd, virtual_cwd)?; let new_path = read_resolved(notif, 1, None, notif_fd, virtual_cwd)?; return Some(CowWriteOp::Rename { old_path, new_path }); } - if Some(nr) == arch::SYS_SYMLINK { + if Some(nr) == arch::sys_symlink() { let target = read_path(notif, notif.data.args[0], notif_fd)?; let linkpath = read_resolved(notif, 1, None, notif_fd, virtual_cwd)?; return Some(CowWriteOp::Symlink { target, linkpath }); } - if Some(nr) == arch::SYS_LINK { + if Some(nr) == arch::sys_link() { let old_path = read_resolved(notif, 0, None, notif_fd, virtual_cwd)?; let new_path = read_resolved(notif, 1, None, notif_fd, virtual_cwd)?; return Some(CowWriteOp::Link { old_path, new_path }); } - if Some(nr) == arch::SYS_CHMOD { + if Some(nr) == arch::sys_chmod() { let path = read_resolved(notif, 0, None, notif_fd, virtual_cwd)?; return Some(CowWriteOp::Chmod { path, mode: (notif.data.args[1] & 0o7777) as u32 }); } - if Some(nr) == arch::SYS_CHOWN || Some(nr) == arch::SYS_LCHOWN { + if Some(nr) == arch::sys_chown() || Some(nr) == arch::sys_lchown() { let path = read_resolved(notif, 0, None, notif_fd, virtual_cwd)?; return Some(CowWriteOp::Chown { path, uid: notif.data.args[1] as u32, gid: notif.data.args[2] as u32 }); } @@ -604,7 +604,7 @@ pub(crate) async fn handle_cow_access( // access(pathname, mode): args[0]=path, args[1]=mode // faccessat(dirfd, pathname, mode, flags): args[0]=dirfd, args[1]=path, args[2]=mode - let (path, mode) = if Some(nr) == arch::SYS_ACCESS { + let (path, mode) = if Some(nr) == arch::sys_access() { let p = match read_path(notif, notif.data.args[0], notif_fd) { Some(p) => resolve_at_path_with_virtual( notif, diff --git a/crates/sandlock-core/src/procfs.rs b/crates/sandlock-core/src/procfs.rs index 9c33a12..fe3320d 100644 --- a/crates/sandlock-core/src/procfs.rs +++ b/crates/sandlock-core/src/procfs.rs @@ -615,7 +615,7 @@ pub(crate) fn resolve_open_target( notif_fd: RawFd, ) -> Option { let nr = notif.data.nr as i64; - let (dirfd, path_ptr): (i64, u64) = if Some(nr) == crate::arch::SYS_OPEN { + let (dirfd, path_ptr): (i64, u64) = if Some(nr) == crate::arch::sys_open() { // open(path, flags, mode) — no dirfd, behaves as AT_FDCWD. (libc::AT_FDCWD as i64, notif.data.args[0]) } else if nr == libc::SYS_openat || nr == crate::arch::SYS_OPENAT2 { diff --git a/crates/sandlock-core/src/resource.rs b/crates/sandlock-core/src/resource.rs index b318e69..e3b7e64 100644 --- a/crates/sandlock-core/src/resource.rs +++ b/crates/sandlock-core/src/resource.rs @@ -60,7 +60,7 @@ fn clone_flags(notif: &SeccompNotif, notif_fd: RawFd) -> Option { let arr: [u8; 8] = buf.as_slice().try_into().ok()?; return Some(u64::from_ne_bytes(arr)); } - if Some(nr) == crate::arch::SYS_VFORK || Some(nr) == crate::arch::SYS_FORK { + if Some(nr) == crate::arch::sys_vfork() || Some(nr) == crate::arch::sys_fork() { return Some(0); } None @@ -204,7 +204,7 @@ impl Drop for ProcessCreationTrace { } fn is_process_creation_notif(notif: &SeccompNotif) -> bool { - crate::arch::FORK_LIKE_SYSCALLS.contains(&(notif.data.nr as i64)) + crate::arch::fork_like_syscalls().contains(&(notif.data.nr as i64)) } /// True when `handle_fork` would have incremented the concurrent @@ -699,12 +699,12 @@ mod tests { assert!(requires_process_creation_tracking(&clone3, fd, &argv_safety)); assert!(!requires_process_creation_tracking(&openat, fd, &argv_safety)); - if let Some(fork_nr) = crate::arch::SYS_FORK { + if let Some(fork_nr) = crate::arch::sys_fork() { let fork = fake_notif(fork_nr, 0); assert!(fork_counted_on_continue(&fork, fd)); assert!(requires_process_creation_tracking(&fork, fd, &argv_safety)); } - if let Some(vfork_nr) = crate::arch::SYS_VFORK { + if let Some(vfork_nr) = crate::arch::sys_vfork() { let vfork = fake_notif(vfork_nr, 0); assert!(fork_counted_on_continue(&vfork, fd)); assert!(requires_process_creation_tracking(&vfork, fd, &argv_safety)); diff --git a/crates/sandlock-core/src/sandbox.rs b/crates/sandlock-core/src/sandbox.rs index 3ef66ab..dc613ae 100644 --- a/crates/sandlock-core/src/sandbox.rs +++ b/crates/sandlock-core/src/sandbox.rs @@ -1674,7 +1674,7 @@ fn validate_syscall_names(names: &[String]) -> Result<(), SandboxError> { let unknown: Vec<&str> = names .iter() .map(String::as_str) - .filter(|name| crate::seccomp::syscall_names::syscall_name_to_nr(name).is_none()) + .filter(|name| crate::seccomp::syscall::syscall_name_to_nr(name).is_none()) .collect(); if unknown.is_empty() { Ok(()) diff --git a/crates/sandlock-core/src/seccomp/dispatch.rs b/crates/sandlock-core/src/seccomp/dispatch.rs index 9c96d1c..4f3b6ba 100644 --- a/crates/sandlock-core/src/seccomp/dispatch.rs +++ b/crates/sandlock-core/src/seccomp/dispatch.rs @@ -160,7 +160,7 @@ pub enum HandlerError { /// notification — otherwise `RET_ALLOW` makes the handler unreachable. fn open_family_syscalls() -> Vec { let mut v = vec![libc::SYS_openat, arch::SYS_OPENAT2]; - if let Some(legacy_open) = arch::SYS_OPEN { + if let Some(legacy_open) = arch::sys_open() { v.push(legacy_open); } v @@ -274,7 +274,7 @@ pub(crate) fn build_dispatch_table( // ------------------------------------------------------------------ // Fork/clone family (always on) // ------------------------------------------------------------------ - for &nr in arch::FORK_LIKE_SYSCALLS { + for nr in arch::fork_like_syscalls() { let policy_for_fork = Arc::clone(policy); let resource_for_fork = Arc::clone(resource); table.register(nr, move |cx: &HandlerCtx| { @@ -480,7 +480,7 @@ pub(crate) fn build_dispatch_table( }); } let mut getdents_nrs = vec![libc::SYS_getdents64]; - if let Some(getdents) = arch::SYS_GETDENTS { + if let Some(getdents) = arch::sys_getdents() { getdents_nrs.push(getdents); } for nr in getdents_nrs { @@ -551,7 +551,7 @@ pub(crate) fn build_dispatch_table( // ------------------------------------------------------------------ if policy.deterministic_dirs { let mut getdents_nrs = vec![libc::SYS_getdents64]; - if let Some(getdents) = arch::SYS_GETDENTS { + if let Some(getdents) = arch::sys_getdents() { getdents_nrs.push(getdents); } for nr in getdents_nrs { @@ -750,7 +750,7 @@ fn register_chroot_handlers( crate::chroot::dispatch::handle_chroot_open)); // open (legacy) — fallthrough if Continue - if let Some(open) = arch::SYS_OPEN { + if let Some(open) = arch::sys_open() { table.register(open, chroot_handler_fallthrough!(policy, crate::chroot::dispatch::handle_chroot_legacy_open)); } @@ -772,37 +772,37 @@ fn register_chroot_handlers( } // Legacy write syscalls - if let Some(nr) = arch::SYS_UNLINK { + if let Some(nr) = arch::sys_unlink() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_unlink)); } - if let Some(nr) = arch::SYS_RMDIR { + if let Some(nr) = arch::sys_rmdir() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_rmdir)); } - if let Some(nr) = arch::SYS_MKDIR { + if let Some(nr) = arch::sys_mkdir() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_mkdir)); } - if let Some(nr) = arch::SYS_RENAME { + if let Some(nr) = arch::sys_rename() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_rename)); } - if let Some(nr) = arch::SYS_SYMLINK { + if let Some(nr) = arch::sys_symlink() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_symlink)); } - if let Some(nr) = arch::SYS_LINK { + if let Some(nr) = arch::sys_link() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_link)); } - if let Some(nr) = arch::SYS_CHMOD { + if let Some(nr) = arch::sys_chmod() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_chmod)); } // chown — non-follow - if let Some(chown) = arch::SYS_CHOWN { + if let Some(chown) = arch::sys_chown() { let policy_for_chown = Arc::clone(policy); let __sup = Arc::clone(ctx); table.register(chown, move |cx: &HandlerCtx| { @@ -824,7 +824,7 @@ fn register_chroot_handlers( } // lchown — follow - if let Some(lchown) = arch::SYS_LCHOWN { + if let Some(lchown) = arch::sys_lchown() { let policy_for_lchown = Arc::clone(policy); let __sup = Arc::clone(ctx); table.register(lchown, move |cx: &HandlerCtx| { @@ -856,15 +856,15 @@ fn register_chroot_handlers( } // Legacy stat - if let Some(nr) = arch::SYS_STAT { + if let Some(nr) = arch::sys_stat() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_stat)); } - if let Some(nr) = arch::SYS_LSTAT { + if let Some(nr) = arch::sys_lstat() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_lstat)); } - if let Some(nr) = arch::SYS_ACCESS { + if let Some(nr) = arch::sys_access() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_access)); } @@ -876,14 +876,14 @@ fn register_chroot_handlers( // readlink table.register(libc::SYS_readlinkat, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_readlink)); - if let Some(nr) = arch::SYS_READLINK { + if let Some(nr) = arch::sys_readlink() { table.register(nr, chroot_handler!(policy, crate::chroot::dispatch::handle_chroot_legacy_readlink)); } // getdents let mut getdents_nrs = vec![libc::SYS_getdents64]; - if let Some(getdents) = arch::SYS_GETDENTS { + if let Some(getdents) = arch::sys_getdents() { getdents_nrs.push(getdents); } for nr in getdents_nrs { @@ -932,9 +932,9 @@ fn register_cow_handlers(table: &mut DispatchTable, ctx: &Arc) { libc::SYS_fchownat, libc::SYS_truncate, ]; write_nrs.extend([ - arch::SYS_UNLINK, arch::SYS_RMDIR, arch::SYS_MKDIR, arch::SYS_RENAME, - arch::SYS_SYMLINK, arch::SYS_LINK, arch::SYS_CHMOD, arch::SYS_CHOWN, - arch::SYS_LCHOWN, + arch::sys_unlink(), arch::sys_rmdir(), arch::sys_mkdir(), arch::sys_rename(), + arch::sys_symlink(), arch::sys_link(), arch::sys_chmod(), arch::sys_chown(), + arch::sys_lchown(), ].into_iter().flatten()); for nr in write_nrs { table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_write)); @@ -943,19 +943,19 @@ fn register_cow_handlers(table: &mut DispatchTable, ctx: &Arc) { table.register(libc::SYS_utimensat, cow_call!(crate::cow::dispatch::handle_cow_utimensat)); let mut access_nrs = vec![libc::SYS_faccessat, arch::SYS_FACCESSAT2]; - access_nrs.extend(arch::SYS_ACCESS); + access_nrs.extend(arch::sys_access()); for nr in access_nrs { table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_access)); } let mut open_nrs = vec![libc::SYS_openat]; - open_nrs.extend(arch::SYS_OPEN); + open_nrs.extend(arch::sys_open()); for nr in open_nrs { table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_open)); } let mut stat_nrs = vec![libc::SYS_newfstatat, libc::SYS_faccessat]; - stat_nrs.extend([arch::SYS_STAT, arch::SYS_LSTAT, arch::SYS_ACCESS].into_iter().flatten()); + stat_nrs.extend([arch::sys_stat(), arch::sys_lstat(), arch::sys_access()].into_iter().flatten()); for nr in stat_nrs { table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_stat)); } @@ -963,13 +963,13 @@ fn register_cow_handlers(table: &mut DispatchTable, ctx: &Arc) { table.register(libc::SYS_statx, cow_call!(crate::cow::dispatch::handle_cow_statx)); let mut readlink_nrs = vec![libc::SYS_readlinkat]; - readlink_nrs.extend(arch::SYS_READLINK); + readlink_nrs.extend(arch::sys_readlink()); for nr in readlink_nrs { table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_readlink)); } let mut getdents_nrs = vec![libc::SYS_getdents64]; - getdents_nrs.extend(arch::SYS_GETDENTS); + getdents_nrs.extend(arch::sys_getdents()); for nr in getdents_nrs { table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_getdents)); } diff --git a/crates/sandlock-core/src/seccomp/mod.rs b/crates/sandlock-core/src/seccomp/mod.rs index b92cc5e..231fc86 100644 --- a/crates/sandlock-core/src/seccomp/mod.rs +++ b/crates/sandlock-core/src/seccomp/mod.rs @@ -4,4 +4,3 @@ pub mod dispatch; pub mod notif; pub(crate) mod state; pub mod syscall; -pub mod syscall_names; diff --git a/crates/sandlock-core/src/seccomp/notif.rs b/crates/sandlock-core/src/seccomp/notif.rs index 3a565fc..8c29664 100644 --- a/crates/sandlock-core/src/seccomp/notif.rs +++ b/crates/sandlock-core/src/seccomp/notif.rs @@ -792,8 +792,8 @@ fn syscall_name(nr: i64) -> &'static str { n if n == libc::SYS_bind => "bind", n if n == libc::SYS_clone => "clone", n if n == libc::SYS_clone3 => "clone3", - n if Some(n) == arch::SYS_VFORK => "vfork", - n if Some(n) == arch::SYS_FORK => "fork", + n if Some(n) == arch::sys_vfork() => "vfork", + n if Some(n) == arch::sys_fork() => "fork", n if n == libc::SYS_execve => "execve", n if n == libc::SYS_execveat => "execveat", n if n == libc::SYS_mmap => "mmap", @@ -817,13 +817,13 @@ fn syscall_category(nr: i64) -> crate::policy_fn::SyscallCategory { || n == libc::SYS_truncate || n == libc::SYS_readlinkat || n == libc::SYS_newfstatat || n == libc::SYS_statx || n == libc::SYS_faccessat || n == libc::SYS_getdents64 - || Some(n) == arch::SYS_GETDENTS => SyscallCategory::File, + || Some(n) == arch::sys_getdents() => SyscallCategory::File, n if n == libc::SYS_connect || n == libc::SYS_sendto || n == libc::SYS_sendmsg || n == libc::SYS_sendmmsg || n == libc::SYS_bind || n == libc::SYS_getsockname => SyscallCategory::Network, n if n == libc::SYS_clone || n == libc::SYS_clone3 - || Some(n) == arch::SYS_VFORK || Some(n) == arch::SYS_FORK + || Some(n) == arch::sys_vfork() || Some(n) == arch::sys_fork() || n == libc::SYS_execve || n == libc::SYS_execveat => SyscallCategory::Process, n if n == libc::SYS_mmap || n == libc::SYS_munmap || n == libc::SYS_brk || n == libc::SYS_mremap @@ -904,7 +904,7 @@ fn resolve_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Option { + n if Some(n) == arch::sys_open() || n == libc::SYS_execve => { let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?; resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path) } @@ -932,17 +932,17 @@ fn resolve_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Option { + n if Some(n) == arch::sys_link() => { let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?; resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path) } // rename(oldpath, newpath) — legacy, AT_FDCWD implied for both - n if Some(n) == arch::SYS_RENAME => { + n if Some(n) == arch::sys_rename() => { let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?; resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path) } // symlink(target, linkpath) — legacy - n if Some(n) == arch::SYS_SYMLINK => { + n if Some(n) == arch::sys_symlink() => { let target = read_path_for_event(notif, notif.data.args[0], notif_fd)?; resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &target) } @@ -969,12 +969,12 @@ fn resolve_second_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Optio resolve_at_path_for_event(notif, notif.data.args[2] as i64, &path) } // rename(oldpath, newpath) — legacy - n if Some(n) == arch::SYS_RENAME => { + n if Some(n) == arch::sys_rename() => { let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?; resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path) } // link(oldpath, newpath) — legacy - n if Some(n) == arch::SYS_LINK => { + n if Some(n) == arch::sys_link() => { let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?; resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path) } @@ -1236,7 +1236,7 @@ async fn handle_notification( libc::SYS_linkat, libc::SYS_renameat2, libc::SYS_symlinkat, ]; path_check_nrs.extend([ - arch::SYS_OPEN, arch::SYS_LINK, arch::SYS_RENAME, arch::SYS_SYMLINK, + arch::sys_open(), arch::sys_link(), arch::sys_rename(), arch::sys_symlink(), ].into_iter().flatten()); let should_precheck_denied = policy.chroot_root.is_none() && path_check_nrs.contains(&nr); diff --git a/crates/sandlock-core/src/seccomp/syscall.rs b/crates/sandlock-core/src/seccomp/syscall.rs index 86fc232..6a6bf17 100644 --- a/crates/sandlock-core/src/seccomp/syscall.rs +++ b/crates/sandlock-core/src/seccomp/syscall.rs @@ -1,11 +1,43 @@ -//! `Syscall` — checked syscall number newtype. +//! Syscall identity: name-to-number resolution and the checked `Syscall` +//! number newtype. //! -//! Closes the footgun where `add_handler(-5, h)` would compile but -//! silently never fire because the cBPF filter cannot emit a JEQ for -//! an architecture-unknown syscall number. +//! The newtype closes the footgun where `add_handler(-5, h)` would compile but +//! silently never fire because the cBPF filter cannot emit a JEQ for an +//! architecture-unknown syscall number. use thiserror::Error; +/// Map a syscall name to its number on the current architecture. +/// +/// Returns `None` for names that are not syscalls on this architecture (for +/// example legacy `open`/`stat` on the generic-ABI arches) or are not syscall +/// names at all. Backed by the `syscalls` crate's kernel-ABI tables, so this +/// covers every syscall, not a curated subset. +/// +/// Sandlock's public API and presets use libc's `SYS_*` spellings; where the +/// crate's per-arch table spells a syscall differently, [`libc_name_alias`] +/// bridges the gap so those names keep resolving. +pub fn syscall_name_to_nr(name: &str) -> Option { + name.parse::() + .ok() + .or_else(|| libc_name_alias(name).and_then(|aka| aka.parse::().ok())) + .map(|s| s.id() as u32) +} + +/// Maps a libc `SYS_*` syscall name to the `syscalls` crate's name where the +/// two diverge. Sandlock callers spell syscalls the libc way, but the crate's +/// tables use the kernel-canonical name on some architectures. +/// +/// Currently only `newfstatat`: the crate spells syscall 79 `fstatat` on +/// aarch64 (libc, and the crate's own x86_64 and riscv64 tables, use +/// `newfstatat`). Returns `None` when no alias is needed. +fn libc_name_alias(name: &str) -> Option<&'static str> { + match name { + "newfstatat" => Some("fstatat"), + _ => None, + } +} + #[derive(Debug, Error, PartialEq, Eq)] pub enum SyscallError { #[error("syscall number {0} is negative")] @@ -61,7 +93,7 @@ mod tests { #[test] fn checked_rejects_arch_unknown() { - // 99_999 is above any reasonable MAX_SYSCALL_NR. + // 99_999 is not a real syscall number on any supported arch. match Syscall::checked(99_999) { Err(SyscallError::UnknownForArch(99_999)) => {} other => panic!("expected UnknownForArch(99_999), got {:?}", other), @@ -75,4 +107,52 @@ mod tests { let bad: Result = (-1i64).try_into(); assert!(matches!(bad, Err(SyscallError::Negative(-1)))); } + + /// Independent cross-check that the crate's ABI tables agree with the + /// system `libc::SYS_*` constants. Only names libc and the crate spell + /// identically on every target arch belong here; `newfstatat` (which the + /// crate spells `fstatat` on aarch64) resolves through the alias path and + /// is covered by `name_to_nr_resolves_newfstatat_alias` instead. + #[test] + fn name_to_nr_matches_libc_for_stable_names() { + let cases: &[(&str, i64)] = &[ + ("mount", libc::SYS_mount), + ("openat", libc::SYS_openat), + ("connect", libc::SYS_connect), + ("clone", libc::SYS_clone), + ("clone3", libc::SYS_clone3), + ("execve", libc::SYS_execve), + ("ioctl", libc::SYS_ioctl), + ("ptrace", libc::SYS_ptrace), + ("userfaultfd", libc::SYS_userfaultfd), + ("bpf", libc::SYS_bpf), + ("statx", libc::SYS_statx), + ("getrandom", libc::SYS_getrandom), + ("io_uring_setup", libc::SYS_io_uring_setup), + ]; + for &(name, expected) in cases { + assert_eq!( + syscall_name_to_nr(name), + Some(expected as u32), + "{name} should resolve to libc::SYS_{name} = {expected}" + ); + } + } + + #[test] + fn name_to_nr_rejects_non_syscall_names() { + assert_eq!(syscall_name_to_nr("definitely_not_a_syscall"), None); + assert_eq!(syscall_name_to_nr(""), None); + } + + /// `newfstatat` must resolve on every arch even though the crate spells it + /// `fstatat` on aarch64. Regression guard: the `COMMON_PATH_SYSCALLS` + /// preset and other callers pass the libc name through the FFI. + #[test] + fn name_to_nr_resolves_newfstatat_alias() { + assert_eq!( + syscall_name_to_nr("newfstatat"), + Some(libc::SYS_newfstatat as u32) + ); + } } diff --git a/crates/sandlock-core/src/seccomp/syscall_names.rs b/crates/sandlock-core/src/seccomp/syscall_names.rs deleted file mode 100644 index d6cfc35..0000000 --- a/crates/sandlock-core/src/seccomp/syscall_names.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::arch; - -/// Map a syscall name to its `libc::SYS_*` number. -/// -/// Covers all names in `DEFAULT_BLOCKLIST_SYSCALLS` plus extras needed for -/// notif and arg-filter lists. -pub fn syscall_name_to_nr(name: &str) -> Option { - let nr: i64 = match name { - "mount" => libc::SYS_mount, - "umount2" => libc::SYS_umount2, - "pivot_root" => libc::SYS_pivot_root, - "swapon" => libc::SYS_swapon, - "swapoff" => libc::SYS_swapoff, - "reboot" => libc::SYS_reboot, - "sethostname" => libc::SYS_sethostname, - "setdomainname" => libc::SYS_setdomainname, - "kexec_load" => libc::SYS_kexec_load, - "init_module" => libc::SYS_init_module, - "finit_module" => libc::SYS_finit_module, - "delete_module" => libc::SYS_delete_module, - "unshare" => libc::SYS_unshare, - "setns" => libc::SYS_setns, - "perf_event_open" => libc::SYS_perf_event_open, - "bpf" => libc::SYS_bpf, - "userfaultfd" => libc::SYS_userfaultfd, - "keyctl" => libc::SYS_keyctl, - "add_key" => libc::SYS_add_key, - "request_key" => libc::SYS_request_key, - "ptrace" => libc::SYS_ptrace, - "process_vm_readv" => libc::SYS_process_vm_readv, - "process_vm_writev" => libc::SYS_process_vm_writev, - "open_by_handle_at" => libc::SYS_open_by_handle_at, - "name_to_handle_at" => libc::SYS_name_to_handle_at, - "ioperm" => arch::SYS_IOPERM?, - "iopl" => arch::SYS_IOPL?, - "quotactl" => libc::SYS_quotactl, - "acct" => libc::SYS_acct, - "lookup_dcookie" => libc::SYS_lookup_dcookie, - // nfsservctl was removed in Linux 3.1; no libc constant -- skip. - "personality" => libc::SYS_personality, - "io_uring_setup" => libc::SYS_io_uring_setup, - "io_uring_enter" => libc::SYS_io_uring_enter, - "io_uring_register" => libc::SYS_io_uring_register, - // Additional syscalls for notif/arg filters. - "clone" => libc::SYS_clone, - "clone3" => libc::SYS_clone3, - "vfork" => arch::SYS_VFORK?, - "mmap" => libc::SYS_mmap, - "munmap" => libc::SYS_munmap, - "brk" => libc::SYS_brk, - "mremap" => libc::SYS_mremap, - "connect" => libc::SYS_connect, - "sendto" => libc::SYS_sendto, - "sendmsg" => libc::SYS_sendmsg, - "sendmmsg" => libc::SYS_sendmmsg, - "ioctl" => libc::SYS_ioctl, - "socket" => libc::SYS_socket, - "prctl" => libc::SYS_prctl, - "getrandom" => libc::SYS_getrandom, - "openat" => libc::SYS_openat, - "open" => arch::SYS_OPEN?, - "getdents64" => libc::SYS_getdents64, - "getdents" => arch::SYS_GETDENTS?, - "bind" => libc::SYS_bind, - "getsockname" => libc::SYS_getsockname, - "clock_gettime" => libc::SYS_clock_gettime, - "gettimeofday" => libc::SYS_gettimeofday, - "time" => arch::SYS_TIME?, - "clock_nanosleep" => libc::SYS_clock_nanosleep, - "timerfd_settime" => libc::SYS_timerfd_settime, - "timer_settime" => libc::SYS_timer_settime, - "execve" => libc::SYS_execve, - "execveat" => libc::SYS_execveat, - // COW filesystem syscalls. - "unlinkat" => libc::SYS_unlinkat, - "mkdirat" => libc::SYS_mkdirat, - "renameat2" => libc::SYS_renameat2, - "newfstatat" => libc::SYS_newfstatat, - "statx" => libc::SYS_statx, - "faccessat" => libc::SYS_faccessat, - "symlinkat" => libc::SYS_symlinkat, - "linkat" => libc::SYS_linkat, - "fchmodat" => libc::SYS_fchmodat, - "fchownat" => libc::SYS_fchownat, - "readlinkat" => libc::SYS_readlinkat, - "truncate" => libc::SYS_truncate, - "utimensat" => libc::SYS_utimensat, - "unlink" => arch::SYS_UNLINK?, - "rmdir" => arch::SYS_RMDIR?, - "mkdir" => arch::SYS_MKDIR?, - "rename" => arch::SYS_RENAME?, - "stat" => arch::SYS_STAT?, - "lstat" => arch::SYS_LSTAT?, - "access" => arch::SYS_ACCESS?, - "symlink" => arch::SYS_SYMLINK?, - "link" => arch::SYS_LINK?, - "chmod" => arch::SYS_CHMOD?, - "chown" => arch::SYS_CHOWN?, - "lchown" => arch::SYS_LCHOWN?, - "readlink" => arch::SYS_READLINK?, - "futimesat" => arch::SYS_FUTIMESAT?, - "fork" => arch::SYS_FORK?, - // SysV IPC (gated by extra_allow_syscalls=["sysv_ipc"]; denied by default). - "shmget" => libc::SYS_shmget, - "shmat" => libc::SYS_shmat, - "shmdt" => libc::SYS_shmdt, - "shmctl" => libc::SYS_shmctl, - "msgget" => libc::SYS_msgget, - "msgsnd" => libc::SYS_msgsnd, - "msgrcv" => libc::SYS_msgrcv, - "msgctl" => libc::SYS_msgctl, - "semget" => libc::SYS_semget, - "semop" => libc::SYS_semop, - "semctl" => libc::SYS_semctl, - "semtimedop" => libc::SYS_semtimedop, - _ => return None, - }; - Some(nr as u32) -} diff --git a/crates/sandlock-ffi/src/lib.rs b/crates/sandlock-ffi/src/lib.rs index 003958a..ca9526a 100644 --- a/crates/sandlock-ffi/src/lib.rs +++ b/crates/sandlock-ffi/src/lib.rs @@ -533,7 +533,7 @@ pub unsafe extern "C" fn sandlock_syscall_nr(name: *const c_char) -> i64 { Ok(s) => s, Err(_) => return -1, }; - match sandlock_core::seccomp::syscall_names::syscall_name_to_nr(name) { + match sandlock_core::seccomp::syscall::syscall_name_to_nr(name) { Some(nr) => i64::from(nr), None => -1, }