diff --git a/README.md b/README.md index b5254fd..d491216 100644 --- a/README.md +++ b/README.md @@ -473,7 +473,7 @@ The async notification supervisor (tokio) handles intercepted syscalls: Downstream Rust crates can append their own seccomp-notification handlers to the supervisor chain alongside the builtins, registering for any syscall they care about via the `Handler` trait and -`Sandbox::run_with_extra_handlers`. The builtin chain runs first, so +`Sandbox::run_with_handlers`. The builtin chain runs first, so user handlers cannot subvert confinement; the registration step also rejects handlers on syscalls in the default blocklist or `extra_deny_syscalls`. See diff --git a/crates/sandlock-core/examples/openat_audit.rs b/crates/sandlock-core/examples/openat_audit.rs index b28a1ca..a6fa6da 100644 --- a/crates/sandlock-core/examples/openat_audit.rs +++ b/crates/sandlock-core/examples/openat_audit.rs @@ -1,6 +1,6 @@ //! Audit every `openat(2)` that a sandboxed process performs. //! -//! Demonstrates [`Sandbox::run_with_extra_handlers`]: a downstream crate +//! Demonstrates [`Sandbox::run_with_handlers`]: a downstream crate //! registers a user handler for `SYS_openat` that logs the call and falls //! through to default (builtin) processing. //! @@ -62,7 +62,7 @@ async fn main() -> Result<(), Box> { }; let result = policy.clone().with_name("openat-audit") - .run_with_extra_handlers( + .run_with_handlers( &cmd_ref, [(libc::SYS_openat, audit)], ) diff --git a/crates/sandlock-core/src/context.rs b/crates/sandlock-core/src/context.rs index 041c2ca..9941beb 100644 --- a/crates/sandlock-core/src/context.rs +++ b/crates/sandlock-core/src/context.rs @@ -976,7 +976,7 @@ pub(crate) fn confine_child(args: ChildSpawnArgs<'_>) -> ! { } else { // First-level sandbox: notif + deny filter with NEW_LISTENER. // - // Caller-supplied extra handlers must have their syscalls registered in + // Caller-supplied handlers must have their syscalls registered in // the BPF filter, otherwise the kernel never raises a notification for // them and the handler silently never fires. We merge `extra_syscalls` // into the notif list and dedup so each syscall produces exactly one @@ -986,7 +986,7 @@ pub(crate) fn confine_child(args: ChildSpawnArgs<'_>) -> ! { notif.extend_from_slice(extra_syscalls); } // Argv-safety gate (companion to the policy_fn case in - // notif_syscalls): an extra handler bound to execve/execveat + // notif_syscalls): a handler bound to execve/execveat // can call `read_child_mem` to inspect argv, so the supervisor // must register newly forked children before they can run user // code — same invariant policy_fn relies on. Bare fork(2) diff --git a/crates/sandlock-core/src/sandbox.rs b/crates/sandlock-core/src/sandbox.rs index 38ad659..6fd73d0 100644 --- a/crates/sandlock-core/src/sandbox.rs +++ b/crates/sandlock-core/src/sandbox.rs @@ -200,7 +200,7 @@ struct Runtime { http_acl_handle: Option, #[allow(clippy::type_complexity)] on_bind: Option) + Send + Sync>>, - extra_handlers: Vec<(i64, Arc)>, + handlers: Vec<(i64, Arc)>, ready_w: Option, } @@ -805,38 +805,38 @@ impl Sandbox { } /// One-shot run with user-supplied syscall handlers. - pub async fn run_with_extra_handlers( + pub async fn run_with_handlers( &mut self, cmd: &[&str], - extra_handlers: I, + handlers: I, ) -> Result where I: IntoIterator, S: TryInto, H: crate::seccomp::dispatch::Handler, { - let pending = sandbox_collect_extra_handlers(extra_handlers, self)?; + let pending = sandbox_collect_handlers(handlers, self)?; self.ensure_runtime()?; - self.rt_mut().extra_handlers = pending; + self.rt_mut().handlers = pending; self.do_create(cmd, true).await?; self.do_start()?; self.wait().await } - /// Interactive-stdio counterpart of `run_with_extra_handlers`. - pub async fn run_interactive_with_extra_handlers( + /// Interactive-stdio counterpart of `run_with_handlers`. + pub async fn run_interactive_with_handlers( &mut self, cmd: &[&str], - extra_handlers: I, + handlers: I, ) -> Result where I: IntoIterator, S: TryInto, H: crate::seccomp::dispatch::Handler, { - let pending = sandbox_collect_extra_handlers(extra_handlers, self)?; + let pending = sandbox_collect_handlers(handlers, self)?; self.ensure_runtime()?; - self.rt_mut().extra_handlers = pending; + self.rt_mut().handlers = pending; self.do_create(cmd, false).await?; self.do_start()?; self.wait().await @@ -1009,7 +1009,7 @@ impl Sandbox { extra_fds: Vec::new(), http_acl_handle: None, on_bind: None, - extra_handlers: Vec::new(), + handlers: Vec::new(), ready_w: None, })); clones.push(clone_sb); @@ -1094,7 +1094,7 @@ impl Sandbox { extra_fds: Vec::new(), http_acl_handle: None, on_bind: None, - extra_handlers: Vec::new(), + handlers: Vec::new(), ready_w: None, })); Ok(()) @@ -1265,7 +1265,7 @@ impl Sandbox { let gather_keep_fds: Vec = extra_fds_copy.iter().map(|&(target, _)| target).collect(); - let extra_syscalls: Vec = self.rt().extra_handlers + let extra_syscalls: Vec = self.rt().handlers .iter() .map(|h| h.0 as u32) .collect(); @@ -1346,7 +1346,7 @@ impl Sandbox { has_random_seed: self.random_seed.is_some(), has_time_start: self.time_start.is_some(), argv_safety_required: self.policy_fn.is_some() - || self.rt().extra_handlers.iter().any(|h| { + || self.rt().handlers.iter().any(|h| { h.0 == libc::SYS_execve || h.0 == libc::SYS_execveat }), time_offset: time_offset_val, @@ -1495,10 +1495,10 @@ impl Sandbox { notif_fd: notif_raw_fd, }); - let extra_handlers = std::mem::take(&mut self.rt_mut().extra_handlers); + let handlers = std::mem::take(&mut self.rt_mut().handlers); let (startup_tx, startup_rx) = tokio::sync::oneshot::channel(); self.rt_mut().notif_handle = Some(tokio::spawn( - notif::supervisor(notif_fd, ctx, extra_handlers, startup_tx), + notif::supervisor(notif_fd, ctx, handlers, startup_tx), )); // Wait for the supervisor to register the notif fd with the IO // driver before we release the child to execve. Otherwise an @@ -1696,8 +1696,8 @@ fn sandbox_wait_status_to_exit(status: i32) -> crate::result::ExitStatus { } } -fn sandbox_collect_extra_handlers( - extra_handlers: I, +fn sandbox_collect_handlers( + handlers: I, sandbox: &Sandbox, ) -> Result)>, crate::error::SandlockError> where @@ -1707,7 +1707,7 @@ where { use crate::seccomp::dispatch::{Handler, HandlerError}; - let pending: Vec<(i64, Arc)> = extra_handlers + let pending: Vec<(i64, Arc)> = handlers .into_iter() .map(|(syscall, handler)| { let nr = syscall.try_into().map_err(HandlerError::from)?.raw(); diff --git a/crates/sandlock-core/src/seccomp/dispatch.rs b/crates/sandlock-core/src/seccomp/dispatch.rs index d7d3786..894db39 100644 --- a/crates/sandlock-core/src/seccomp/dispatch.rs +++ b/crates/sandlock-core/src/seccomp/dispatch.rs @@ -41,8 +41,8 @@ use tokio::sync::Mutex; /// Public extension trait for sandlock seccomp-notif handlers. /// /// Each implementor is registered against a [`crate::seccomp::syscall::Syscall`] -/// through [`crate::Sandbox::run_with_extra_handlers`] / -/// [`crate::Sandbox::run_interactive_with_extra_handlers`]. Receives +/// through [`crate::Sandbox::run_with_handlers`] / +/// [`crate::Sandbox::run_interactive_with_handlers`]. Receives /// `&HandlerCtx` borrowed for the call; cannot outlive the dispatch /// invocation. /// @@ -99,7 +99,7 @@ where // Concrete impls for `Box` and `Arc` so callers // can erase concrete handler types behind a smart pointer when mixing // different handler shapes in one `IntoIterator` passed to -// `run_with_extra_handlers` — e.g. `Vec<(i64, Box)>` lets a +// `run_with_handlers` — e.g. `Vec<(i64, Box)>` lets a // downstream register handlers of different concrete types without // writing a per-crate wrapper enum. // @@ -126,7 +126,7 @@ impl Handler for std::sync::Arc { } /// Errors raised when registering user handlers via -/// [`crate::Sandbox::run_with_extra_handlers`]. +/// [`crate::Sandbox::run_with_handlers`]. #[derive(Debug, Error, PartialEq, Eq)] pub enum HandlerError { #[error("invalid syscall in handler registration: {0}")] @@ -153,7 +153,7 @@ pub enum HandlerError { /// resolves from Sandlock's default syscall blocklist plus policy extras. /// /// Takes only the syscall numbers because that's all it needs to check. -/// Called from the `run_with_extra_handlers` entry points before any +/// Called from the `run_with_handlers` entry points before any /// handler is registered against the dispatch table. /// /// Returns the offending syscall number on rejection so the caller can @@ -203,7 +203,7 @@ impl DispatchTable { /// Register a pre-`Arc`'d handler. Used both by builtin chunks /// that share state via `Arc::clone` (one `ForkHandler` instance /// registers against `SYS_clone`/`SYS_clone3`/`SYS_vfork`) and by - /// `run_with_extra_handlers` when each item already arrives as + /// `run_with_handlers` when each item already arrives as /// `Arc`. pub(crate) fn register_arc( &mut self, @@ -942,7 +942,7 @@ fn register_cow_handlers(table: &mut DispatchTable, ctx: &Arc) { // ============================================================ #[cfg(test)] -mod extra_handler_tests { +mod handler_tests { //! Unit tests for the user-supplied handler extension API. //! //! Drive the actual `DispatchTable::dispatch` walker against a minimal @@ -952,7 +952,7 @@ mod extra_handler_tests { //! short-circuit on first non-`Continue`, append-after-builtin //! placement) are exercised end-to-end without needing a live //! Landlock+seccomp sandbox — those scenarios live under - //! `crates/sandlock-core/tests/integration/test_extra_handlers.rs`. + //! `crates/sandlock-core/tests/integration/test_handlers.rs`. use super::*; use crate::netlink::NetlinkState; use crate::seccomp::ctx::SupervisorCtx; @@ -1175,7 +1175,7 @@ mod extra_handler_tests { /// `DEFAULT_BLOCKLIST_SYSCALLS` — putting it into `extra_deny_syscalls` is the only /// way it ends up on the extra blocklist, so the test isolates the user-supplied /// path of `blocklist_syscall_numbers` from the default branch covered by - /// `extra_handler_on_default_blocklist_syscall_is_rejected`. + /// `handler_on_default_blocklist_syscall_is_rejected`. /// /// Pure-logic counterpart to the integration test of the same name — /// runs without a live sandbox so the contract is enforced even on diff --git a/crates/sandlock-core/src/seccomp/notif.rs b/crates/sandlock-core/src/seccomp/notif.rs index 7727ec7..ca529fb 100644 --- a/crates/sandlock-core/src/seccomp/notif.rs +++ b/crates/sandlock-core/src/seccomp/notif.rs @@ -212,7 +212,7 @@ pub struct NotifPolicy { pub has_time_start: bool, /// Argv-safety gate: the supervisor must freeze every task that /// could mutate argv before any consumer reads it. True when - /// `policy_fn` is active or when an extra handler is bound to + /// `policy_fn` is active or when a handler is bound to /// execve/execveat (such handlers can call `read_child_mem`). /// Also gates ptrace fork-event tracking so `ProcessIndex` is /// complete when the freeze enumerates it. diff --git a/crates/sandlock-core/tests/integration.rs b/crates/sandlock-core/tests/integration.rs index b24b074..5c90f88 100644 --- a/crates/sandlock-core/tests/integration.rs +++ b/crates/sandlock-core/tests/integration.rs @@ -55,5 +55,5 @@ mod test_dry_run; #[path = "integration/test_http_acl.rs"] mod test_http_acl; -#[path = "integration/test_extra_handlers.rs"] -mod test_extra_handlers; +#[path = "integration/test_handlers.rs"] +mod test_handlers; diff --git a/crates/sandlock-core/tests/integration/test_extra_handlers.rs b/crates/sandlock-core/tests/integration/test_handlers.rs similarity index 89% rename from crates/sandlock-core/tests/integration/test_extra_handlers.rs rename to crates/sandlock-core/tests/integration/test_handlers.rs index 9678c40..6535924 100644 --- a/crates/sandlock-core/tests/integration/test_extra_handlers.rs +++ b/crates/sandlock-core/tests/integration/test_handlers.rs @@ -1,5 +1,5 @@ //! Integration tests for the user-supplied `Handler` extension API -//! (`Sandbox::run_with_extra_handlers`). +//! (`Sandbox::run_with_handlers`). //! //! These tests exercise the full plumbing through the kernel: the guest //! issues a syscall, the BPF filter raises a `USER_NOTIF`, the supervisor @@ -35,7 +35,7 @@ use sandlock_core::{ /// Read a NUL-terminated path from the sandboxed child's address space. /// /// Used by tests that need to inspect which `openat`s actually reached -/// their extra handler. Works without `CAP_SYS_PTRACE` because the test +/// their handler. Works without `CAP_SYS_PTRACE` because the test /// process and the sandboxed child share the same UID, which is the /// permission `process_vm_readv(2)` actually checks. fn read_path_from_child(pid: u32, addr: u64) -> Option { @@ -87,14 +87,14 @@ fn temp_out(name: &str) -> PathBuf { )) } -/// An extra handler registered on a syscall that the default policy +/// A handler registered on a syscall that the default policy /// does not intercept (`SYS_getcwd`) MUST receive notifications and its /// `NotifAction::Errno` MUST surface in the guest as the corresponding /// errno. This is the security contract: without BPF plumbing the /// kernel would never raise USER_NOTIF for `getcwd` and the handler /// would silently never fire — the maintainer-cited footgun. #[tokio::test] -async fn extra_handler_intercepts_syscall_outside_builtin_set() { +async fn handler_intercepts_syscall_outside_builtin_set() { let policy = base_policy().build().unwrap(); let out = temp_out("getcwd-eacces"); let cmd = format!("/bin/pwd; echo $? > {}", out.display()); @@ -109,7 +109,7 @@ async fn extra_handler_intercepts_syscall_outside_builtin_set() { } }; - let result = policy.clone().run_with_extra_handlers(&["sh", "-c", &cmd], + let result = policy.clone().run_with_handlers(&["sh", "-c", &cmd], [(libc::SYS_getcwd, handler)], ) .await @@ -122,11 +122,11 @@ async fn extra_handler_intercepts_syscall_outside_builtin_set() { assert!(result.success(), "shell wrapper should exit 0"); assert!( calls.load(Ordering::SeqCst) >= 1, - "extra handler must have fired at least once for SYS_getcwd" + "handler must have fired at least once for SYS_getcwd" ); assert_ne!( code, 0, - "getcwd must observe the errno injected by the extra handler" + "getcwd must observe the errno injected by the handler" ); } @@ -134,7 +134,7 @@ async fn extra_handler_intercepts_syscall_outside_builtin_set() { /// the guest receives the kernel's natural outcome. This guards an /// observe-only audit handler from accidentally wedging the guest. #[tokio::test] -async fn extra_handler_continue_lets_syscall_proceed() { +async fn handler_continue_lets_syscall_proceed() { let policy = base_policy().build().unwrap(); let out = temp_out("getcwd-continue"); let cmd = format!("/bin/pwd; echo $? > {}", out.display()); @@ -149,7 +149,7 @@ async fn extra_handler_continue_lets_syscall_proceed() { } }; - let result = policy.clone().run_with_extra_handlers(&["sh", "-c", &cmd], + let result = policy.clone().run_with_handlers(&["sh", "-c", &cmd], [(libc::SYS_getcwd, handler)], ) .await @@ -170,7 +170,7 @@ async fn extra_handler_continue_lets_syscall_proceed() { ); } -/// `Sandbox::run_with_extra_handlers(_, _, vec![])` must be observably +/// `Sandbox::run_with_handlers(_, _, vec![])` must be observably /// identical to `Sandbox::run(_, _)`. Guards the documented backwards /// compatibility contract. #[tokio::test] @@ -179,7 +179,7 @@ async fn empty_extras_preserves_default_behaviour() { let baseline = policy.clone().run(&["/bin/pwd"]).await.unwrap(); let no_handlers: [(i64, fn(&HandlerCtx) -> std::future::Ready); 0] = []; - let with_extras = policy.clone().run_with_extra_handlers(&["/bin/pwd"], no_handlers) + let with_extras = policy.clone().run_with_handlers(&["/bin/pwd"], no_handlers) .await .unwrap(); @@ -197,7 +197,7 @@ async fn empty_extras_preserves_default_behaviour() { /// Verifies the ordering contract end-to-end through the kernel — the /// unit tests only check `Vec` index ordering inside the dispatch table. #[tokio::test] -async fn extra_handler_runs_after_builtin_returns_continue() { +async fn handler_runs_after_builtin_returns_continue() { let policy = base_policy().build().unwrap(); let out = temp_out("openat-cross"); let cmd = format!("cat /etc/group; echo $? > {}", out.display()); @@ -215,7 +215,7 @@ async fn extra_handler_runs_after_builtin_returns_continue() { } }; - let result = policy.clone().run_with_extra_handlers(&["sh", "-c", &cmd], + let result = policy.clone().run_with_handlers(&["sh", "-c", &cmd], [(libc::SYS_openat, handler)], ) .await @@ -271,7 +271,7 @@ async fn builtin_non_continue_blocks_extra() { } }; - let _ = policy.clone().run_with_extra_handlers(&["sh", "-c", &cmd], + let _ = policy.clone().run_with_handlers(&["sh", "-c", &cmd], [(libc::SYS_openat, handler)], ) .await @@ -347,7 +347,7 @@ async fn chain_of_extras_runs_in_insertion_order() { action: NotifAction::Errno(libc::EACCES), }; - let result = policy.clone().run_with_extra_handlers(&["sh", "-c", &cmd], + let result = policy.clone().run_with_handlers(&["sh", "-c", &cmd], [(libc::SYS_getcwd, h1), (libc::SYS_getcwd, h2)], ) .await @@ -381,11 +381,11 @@ async fn chain_of_extras_runs_in_insertion_order() { /// `SECCOMP_USER_NOTIF_FLAG_CONTINUE` and the kernel would actually run /// `mount` — silently bypassing default blocklist. #[tokio::test] -async fn extra_handler_on_default_blocklist_syscall_is_rejected() { +async fn handler_on_default_blocklist_syscall_is_rejected() { let policy = base_policy().build().unwrap(); let handler = |_cx: &HandlerCtx| async { NotifAction::Continue }; - let result = policy.clone().run_with_extra_handlers(&["true"], + let result = policy.clone().run_with_handlers(&["true"], [(libc::SYS_mount, handler)], ) .await; @@ -408,21 +408,21 @@ async fn extra_handler_on_default_blocklist_syscall_is_rejected() { /// handler reach the deny-JEQ via the notif path and bypass the kernel /// rejection at user-space discretion. /// -/// Counterpart to `extra_handler_on_default_blocklist_syscall_is_rejected`, +/// Counterpart to `handler_on_default_blocklist_syscall_is_rejected`, /// driving the user-list branch of `blocklist_syscall_numbers` (see /// `crates/sandlock-core/src/context.rs`). Uses `SYS_mremap` because it is /// in `syscall_name_to_nr` but **not** in DEFAULT_BLOCKLIST — putting it into /// `extra_deny_syscalls` is the only way it lands on the blocklist, isolating the /// user-supplied branch under test from the default-blocklist branch. #[tokio::test] -async fn extra_handler_on_user_specified_blocklist_is_rejected() { +async fn handler_on_user_specified_blocklist_is_rejected() { let policy = base_policy() .extra_deny_syscalls(vec!["mremap".into()]) .build() .unwrap(); let handler = |_cx: &HandlerCtx| async { NotifAction::Continue }; - let result = policy.clone().run_with_extra_handlers(&["true"], + let result = policy.clone().run_with_handlers(&["true"], [(libc::SYS_mremap, handler)], ) .await; @@ -450,9 +450,9 @@ async fn extra_handler_on_user_specified_blocklist_is_rejected() { // ============================================================ /// A closure-shaped handler (via the blanket `impl Handler for F`) -/// passed to `run_with_extra_handlers` MUST observe notifications and the +/// passed to `run_with_handlers` MUST observe notifications and the /// guest MUST see the handler's `Errno`. This verifies the parameter-type -/// rework on `run_with_extra_handlers` doesn't drop notifications. +/// rework on `run_with_handlers` doesn't drop notifications. #[tokio::test] async fn handler_via_blanket_impl_dispatches_in_sandbox() { let policy = base_policy().build().unwrap(); @@ -469,11 +469,11 @@ async fn handler_via_blanket_impl_dispatches_in_sandbox() { } }; - let _result = policy.clone().run_with_extra_handlers(&["sh", "-c", &cmd], + let _result = policy.clone().run_with_handlers(&["sh", "-c", &cmd], [(libc::SYS_getcwd, handler)], ) .await - .expect("run_with_extra_handlers"); + .expect("run_with_handlers"); assert!( calls.load(Ordering::SeqCst) > 0, @@ -491,7 +491,7 @@ async fn handler_via_blanket_impl_dispatches_in_sandbox() { } /// A struct-based `Handler` (with state on `&self`, not captured `Arc`) -/// MUST be invocable through `run_with_extra_handlers` and accumulate +/// MUST be invocable through `run_with_handlers` and accumulate /// state across multiple notifications within one sandbox run. /// /// This exercises the full struct-impl-Handler shape end-to-end: the @@ -500,7 +500,7 @@ async fn handler_via_blanket_impl_dispatches_in_sandbox() { /// `GetcwdCounter::handle` on every notification. Returning `Errno(EPERM)` /// serialises the notification cycle (kernel waits for the response before /// letting the child proceed), so the counter is guaranteed observable -/// after `run_with_extra_handlers` returns. +/// after `run_with_handlers` returns. /// /// Without this test, a regression where dispatch dropped the /// struct-`Arc` path but kept closures-via-blanket-impl @@ -532,11 +532,11 @@ async fn struct_handler_state_persists_across_sandbox_calls() { let out = temp_out("struct-handler-counter"); let cmd = format!("/bin/pwd; /bin/pwd; echo done > {}", out.display()); - policy.clone().run_with_extra_handlers(&["sh", "-c", &cmd], + policy.clone().run_with_handlers(&["sh", "-c", &cmd], [(libc::SYS_getcwd, handler)], ) .await - .expect("run_with_extra_handlers"); + .expect("run_with_handlers"); let _ = std::fs::remove_file(&out); assert!( @@ -547,16 +547,16 @@ async fn struct_handler_state_persists_across_sandbox_calls() { ); } -/// `run_with_extra_handlers` with a negative syscall number MUST return +/// `run_with_handlers` with a negative syscall number MUST return /// `HandlerError::InvalidSyscall(SyscallError::Negative)` up-front, before /// fork. Closes the silent-never-fires footgun. #[tokio::test] -async fn run_with_extra_handlers_rejects_negative_syscall() { +async fn run_with_handlers_rejects_negative_syscall() { let policy = base_policy().build().unwrap(); let handler = |_cx: &HandlerCtx| async { NotifAction::Continue }; let result = - policy.clone().run_with_extra_handlers(&["true"], [(-5i64, handler)]).await; + policy.clone().run_with_handlers(&["true"], [(-5i64, handler)]).await; match result { Err(SandlockError::Handler(HandlerError::InvalidSyscall(SyscallError::Negative(-5)))) => {} @@ -569,12 +569,12 @@ async fn run_with_extra_handlers_rejects_negative_syscall() { /// Same as above but for an arch-unknown syscall number. #[tokio::test] -async fn run_with_extra_handlers_rejects_arch_unknown_syscall() { +async fn run_with_handlers_rejects_arch_unknown_syscall() { let policy = base_policy().build().unwrap(); let handler = |_cx: &HandlerCtx| async { NotifAction::Continue }; let result = - policy.clone().run_with_extra_handlers(&["true"], [(99_999i64, handler)]).await; + policy.clone().run_with_handlers(&["true"], [(99_999i64, handler)]).await; match result { Err(SandlockError::Handler(HandlerError::InvalidSyscall( @@ -595,7 +595,7 @@ async fn run_with_extra_handlers_rejects_arch_unknown_syscall() { /// `IntoIterator` parameter shape. Two instances of the /// same struct keep the iterator's `H` type homogeneous. #[tokio::test] -async fn run_with_extra_handlers_preserves_insertion_order_in_sandbox_chain() { +async fn run_with_handlers_preserves_insertion_order_in_sandbox_chain() { struct OrderTracker { id: u8, order: Arc>>, @@ -634,11 +634,11 @@ async fn run_with_extra_handlers_preserves_insertion_order_in_sandbox_chain() { action: NotifAction::Errno(libc::EACCES), }; - policy.clone().run_with_extra_handlers(&["sh", "-c", &cmd], + policy.clone().run_with_handlers(&["sh", "-c", &cmd], [(libc::SYS_getcwd, h1), (libc::SYS_getcwd, h2)], ) .await - .expect("run_with_extra_handlers"); + .expect("run_with_handlers"); let order = order.lock().unwrap(); assert!(order.len() >= 2, "expected at least 2 dispatches, got {:?}", *order); @@ -648,17 +648,17 @@ async fn run_with_extra_handlers_preserves_insertion_order_in_sandbox_chain() { let _ = std::fs::remove_file(&out); } -/// `run_with_extra_handlers` on a default-blocklist syscall MUST return +/// `run_with_handlers` on a default-blocklist syscall MUST return /// `HandlerError::OnDenySyscall` up-front (before fork) — closes the /// kernel-deny -> NOTIF_FLAG_CONTINUE bypass attack. #[tokio::test] -async fn run_with_extra_handlers_rejects_handler_on_default_blocklist_syscall() { +async fn run_with_handlers_rejects_handler_on_default_blocklist_syscall() { let policy = base_policy().build().unwrap(); let handler = |_cx: &HandlerCtx| async { NotifAction::Continue }; // SYS_mount is in DEFAULT_BLOCKLIST_SYSCALLS. let result = - policy.clone().run_with_extra_handlers(&["true"], [(libc::SYS_mount, handler)]).await; + policy.clone().run_with_handlers(&["true"], [(libc::SYS_mount, handler)]).await; match result { Err(SandlockError::Handler(HandlerError::OnDenySyscall { syscall_nr })) => { diff --git a/crates/sandlock-ffi/src/handler/run.rs b/crates/sandlock-ffi/src/handler/run.rs index 26889ab..321dfa0 100644 --- a/crates/sandlock-ffi/src/handler/run.rs +++ b/crates/sandlock-ffi/src/handler/run.rs @@ -121,14 +121,14 @@ fn block_on_run( // panics are intentionally allowed to propagate. crate::runtime::with_runtime_unwind(|rt| rt.block_on(async move { if interactive { - sb.run_interactive_with_extra_handlers(&cmd_refs, handlers).await + sb.run_interactive_with_handlers(&cmd_refs, handlers).await } else { - sb.run_with_extra_handlers(&cmd_refs, handlers).await + sb.run_with_handlers(&cmd_refs, handlers).await } })) } -/// Run the policy with extra C handlers. Returns NULL on failure. +/// Run the policy with C handlers. Returns NULL on failure. /// /// `name` may be NULL to auto-generate `sandbox-{pid}`, or a valid /// NUL-terminated UTF-8 C string; the placement mirrors the existing diff --git a/docs/extension-handlers.md b/docs/extension-handlers.md index 6cd92e4..d494a03 100644 --- a/docs/extension-handlers.md +++ b/docs/extension-handlers.md @@ -47,7 +47,7 @@ impl Handler for OpenAudit { } let policy = Policy::builder().fs_read("/usr").fs_write("/tmp").build()?; -Sandbox::run_with_extra_handlers( +Sandbox::run_with_handlers( &policy, None, &["python3", "-c", "print(42)"], @@ -70,7 +70,7 @@ let audit = |cx: &HandlerCtx| async move { NotifAction::Continue }; -Sandbox::run_with_extra_handlers(&policy, None, &cmd, [(libc::SYS_openat, audit)]).await?; +Sandbox::run_with_handlers(&policy, None, &cmd, [(libc::SYS_openat, audit)]).await?; ``` Use closures for prototyping or trivial state; switch to a struct as soon as the handler grows @@ -88,11 +88,11 @@ assert!(matches!(Syscall::checked(-5), Err(SyscallError::Negative(-5)))); assert!(matches!(Syscall::checked(99_999), Err(SyscallError::UnknownForArch(99_999)))); ``` -`run_with_extra_handlers` accepts an `IntoIterator` where `S: TryInto`, +`run_with_handlers` accepts an `IntoIterator` where `S: TryInto`, so callers can pass raw `i64`/`u32` syscall numbers and they are validated up-front: ```rust -Sandbox::run_with_extra_handlers(&policy, None, &cmd, [(libc::SYS_openat, openat_h)]).await?; +Sandbox::run_with_handlers(&policy, None, &cmd, [(libc::SYS_openat, openat_h)]).await?; ``` Without `Syscall::checked`, passing `-5` as a syscall number would compile but never fire — the @@ -104,8 +104,8 @@ There are two entry points; both spawn the sandbox, wait for it to exit, and ret | name | stdio | | --- | --- | -| `Sandbox::run_with_extra_handlers(policy, name, cmd, handlers)` | captured (returned in `RunResult`) | -| `Sandbox::run_interactive_with_extra_handlers(policy, name, cmd, handlers)` | inherited from the parent | +| `Sandbox::run_with_handlers(policy, name, cmd, handlers)` | captured (returned in `RunResult`) | +| `Sandbox::run_interactive_with_handlers(policy, name, cmd, handlers)` | inherited from the parent | `name: Option<&str>` is the sandbox instance name (also exposed as the virtual hostname when set); pass `None` when no name is needed. @@ -113,11 +113,11 @@ pass `None` when no name is needed. Both have the same generic shape: ```rust -pub async fn run_with_extra_handlers( +pub async fn run_with_handlers( policy: &Policy, name: Option<&str>, cmd: &[&str], - extra_handlers: I, + handlers: I, ) -> Result where I: IntoIterator, @@ -128,7 +128,7 @@ where Multiple handlers — passed in one array literal: ```rust -Sandbox::run_with_extra_handlers( +Sandbox::run_with_handlers( &policy, None, &cmd, @@ -149,7 +149,7 @@ implement `Handler` themselves, so `H` resolves to a single type: let openat_h: Box = Box::new(my_openat_handler); let close_h: Box = Box::new(MyCloseStruct { ... }); -Sandbox::run_with_extra_handlers( +Sandbox::run_with_handlers( &policy, None, &cmd, @@ -168,11 +168,11 @@ Errors at registration time, before fork: ### Interactive mode For REPL-like workflows (a sandboxed shell, a long-running supervised process whose stdin/stdout -should be inherited from the host), use `run_interactive_with_extra_handlers`. The handler API +should be inherited from the host), use `run_interactive_with_handlers`. The handler API is identical: ```rust -Sandbox::run_interactive_with_extra_handlers( +Sandbox::run_interactive_with_handlers( &policy, None, &["bash"], @@ -181,7 +181,7 @@ Sandbox::run_interactive_with_extra_handlers( .await?; // host stdin/stdout inherited ``` -`run_interactive_with_extra_handlers` does not capture stdout/stderr — the child sees the parent's +`run_interactive_with_handlers` does not capture stdout/stderr — the child sees the parent's terminal directly. ### Reading syscall arguments @@ -321,7 +321,7 @@ let stats = std::sync::Arc::new(CallStats { close: AtomicU64::new(0), }); -Sandbox::run_with_extra_handlers( +Sandbox::run_with_handlers( &policy, None, &cmd, @@ -346,7 +346,7 @@ For each intercepted syscall: 1. Builtin handlers registered inside [`build_dispatch_table`](../crates/sandlock-core/src/seccomp/dispatch.rs) run first, in their internal registration order. -2. Handlers passed to `run_with_extra_handlers` run afterwards, in iterator order. +2. Handlers passed to `run_with_handlers` run afterwards, in iterator order. 3. Multiple iterator entries on the same syscall run in insertion order. The chain stops as soon as a handler returns a non-`NotifAction::Continue` result; subsequent @@ -356,14 +356,14 @@ handlers, and the chain evaluator short-circuits on the first non-`Continue`. The contract is exercised at two layers: -- Unit, in [`seccomp::dispatch::extra_handler_tests`](../crates/sandlock-core/src/seccomp/dispatch.rs): +- Unit, in [`seccomp::dispatch::handler_tests`](../crates/sandlock-core/src/seccomp/dispatch.rs): `dispatch_walks_chain_in_registration_order`, `dispatch_runs_builtin_before_extra`, `dispatch_stops_at_first_non_continue` drive `dispatch()` walker against a minimal `SupervisorCtx`. -- End-to-end, in [`tests/integration/test_extra_handlers.rs`](../crates/sandlock-core/tests/integration/test_extra_handlers.rs): - `run_with_extra_handlers_preserves_insertion_order_in_sandbox_chain`, +- End-to-end, in [`tests/integration/test_handlers.rs`](../crates/sandlock-core/tests/integration/test_handlers.rs): + `run_with_handlers_preserves_insertion_order_in_sandbox_chain`, `builtin_non_continue_blocks_extra`, - `extra_handler_runs_after_builtin_returns_continue` drive a live Landlock+seccomp sandbox. + `handler_runs_after_builtin_returns_continue` drive a live Landlock+seccomp sandbox. ### Return values @@ -421,13 +421,13 @@ User handlers can: ### BPF coverage -`run_with_extra_handlers` collects the syscall numbers declared by the user-supplied handlers and merges them +`run_with_handlers` collects the syscall numbers declared by the user-supplied handlers and merges them into the cBPF notification list installed in the child before `execve`. Without this step the kernel never raises `SECCOMP_RET_USER_NOTIF` for a syscall that no builtin intercepts, and the user handler silently never fires. The merge is dedup-aware: an `openat` registered both by a builtin and a user handler produces a single JEQ in the assembled program. -Validation runs at registration time (before fork). If `Syscall::checked` fails, `run_with_extra_handlers` +Validation runs at registration time (before fork). If `Syscall::checked` fails, `run_with_handlers` returns the error without enqueueing the handler. ### Blocklist Bypass Guard @@ -440,16 +440,16 @@ path; a handler returning `NotifAction::Continue` would become `SECCOMP_USER_NOTIF_FLAG_CONTINUE` and the kernel would actually run the syscall, silently bypassing deny. -`run_with_extra_handlers` rejects this configuration at registration time and returns +`run_with_handlers` rejects this configuration at registration time and returns `HandlerError::OnDenySyscall { syscall_nr }`. The check is implemented in [`validate_handler_syscalls_against_policy`](../crates/sandlock-core/src/seccomp/dispatch.rs) and covers both the default blocklist (`DEFAULT_BLOCKLIST_SYSCALLS`) and the user-specified extras (`extra_deny_syscalls`); both branches are tested (`validate_extras_rejects_user_specified_blocklist`, -`extra_handler_on_default_blocklist_syscall_is_rejected`, -`run_with_extra_handlers_rejects_handler_on_default_blocklist_syscall`, -`run_with_extra_handlers_rejects_negative_syscall`, -`run_with_extra_handlers_rejects_arch_unknown_syscall`). +`handler_on_default_blocklist_syscall_is_rejected`, +`run_with_handlers_rejects_handler_on_default_blocklist_syscall`, +`run_with_handlers_rejects_negative_syscall`, +`run_with_handlers_rejects_arch_unknown_syscall`). Sandlock always installs its default syscall blocklist, so this guard is always active. @@ -485,7 +485,7 @@ impl Handler for PanicSafe { } } -Sandbox::run_with_extra_handlers( +Sandbox::run_with_handlers( &policy, None, &cmd, @@ -510,7 +510,7 @@ Wrap each handler in `Box` so the iterator's `H` parameter is unifo heterogeneous handler types: ```rust -Sandbox::run_with_extra_handlers( +Sandbox::run_with_handlers( &policy, None, &cmd, @@ -590,7 +590,7 @@ The host binary instantiates the handlers and passes them as one iterator's `H` parameter stays homogeneous: ```rust -Sandbox::run_with_extra_handlers( +Sandbox::run_with_handlers( &policy, None, &cmd,