Skip to main content

sandlock_core/seccomp/
dispatch.rs

1// Table-driven syscall dispatch — routes seccomp notifications to handler chains.
2//
3// Each syscall number maps to an ordered chain of handlers.  The chain is walked
4// until a handler returns a non-Continue action (or the chain is exhausted, in
5// which case Continue is returned).
6//
7// Continue safety (issue #27):
8//   - The chain walker treats Continue as "this handler did not intervene,
9//     try the next one." A final Continue (no handler intervened, or chain
10//     exhausted) means the syscall passes through to the kernel as-issued.
11//     The kernel still enforces Landlock and the BPF filter on the
12//     untouched syscall, so dispatch-level Continue is not a security
13//     decision — it's the absence of one.
14//   - The conditional shim closures (random/hostname/etc_hosts opens) that
15//     wrap an Option-returning helper translate `None` into Continue,
16//     which is the same "not my path, next handler" semantics. None of
17//     them approve a syscall based on user-memory contents.
18
19use std::collections::HashMap;
20use std::os::unix::io::RawFd;
21use std::sync::Arc;
22
23use super::ctx::SupervisorCtx;
24use super::notif::{NotifAction, NotifPolicy};
25use super::state::ResourceState;
26use super::syscall::SyscallError;
27use crate::arch;
28use crate::sys::structs::SeccompNotif;
29
30use thiserror::Error;
31use tokio::sync::Mutex;
32
33// ============================================================
34// Types
35// ============================================================
36
37// ============================================================
38// Handler trait — the new public extension API.
39// ============================================================
40
41/// Public extension trait for sandlock seccomp-notif handlers.
42///
43/// Each implementor is registered against a [`crate::seccomp::syscall::Syscall`]
44/// through [`crate::Sandbox::run_with_handlers`] /
45/// [`crate::Sandbox::run_interactive_with_handlers`].  Receives
46/// `&HandlerCtx` borrowed for the call; cannot outlive the dispatch
47/// invocation.
48///
49/// State lives on the implementor — no `Arc::clone` ladders, no
50/// closure ceremony at registration time.
51///
52/// `handle` returns a boxed `Future` so the trait stays dyn-compatible
53/// (the supervisor stores user handlers as `Vec<Arc<dyn Handler>>`,
54/// keyed by syscall number).  Returning `impl Future` directly via
55/// RPITIT would be more efficient but is not object-safe, and changing
56/// the storage to a non-erased shape would force a generic dispatch
57/// chain incompatible with arbitrary user handler types.
58pub trait Handler: Send + Sync + 'static {
59    fn handle<'a>(
60        &'a self,
61        cx: &'a HandlerCtx,
62    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NotifAction> + Send + 'a>>;
63}
64
65/// Context passed to `Handler::handle`.
66///
67/// `notif` is the kernel notification (owned by value — it's a small
68/// `repr(C)` struct, cheap to copy).  `notif_fd` is the supervisor's
69/// seccomp listener fd, used by helpers like `read_child_mem` /
70/// `write_child_mem` / `read_child_cstr` for TOCTOU-safe child memory
71/// access.
72///
73/// Handler state lives on the implementor (`&self`).  Supervisor-internal
74/// state is intentionally not exposed here so the `SupervisorCtx`
75/// internal fields are not part of the downstream extension contract.
76pub struct HandlerCtx {
77    pub notif: SeccompNotif,
78    pub notif_fd: RawFd,
79}
80
81// Blanket impl: any Fn(&HandlerCtx) -> Future is a Handler.
82//
83// Lets lightweight closure-style handlers work without ceremony at the
84// call site.  Handlers that need state should use `struct + explicit
85// impl Handler` instead.
86impl<F, Fut> Handler for F
87where
88    F: Fn(&HandlerCtx) -> Fut + Send + Sync + 'static,
89    Fut: std::future::Future<Output = NotifAction> + Send + 'static,
90{
91    fn handle<'a>(
92        &'a self,
93        cx: &'a HandlerCtx,
94    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NotifAction> + Send + 'a>> {
95        Box::pin((self)(cx))
96    }
97}
98
99// Concrete impls for `Box<dyn Handler>` and `Arc<dyn Handler>` so callers
100// can erase concrete handler types behind a smart pointer when mixing
101// different handler shapes in one `IntoIterator` passed to
102// `run_with_handlers` — e.g. `Vec<(i64, Box<dyn Handler>)>` lets a
103// downstream register handlers of different concrete types without
104// writing a per-crate wrapper enum.
105//
106// These are concrete `Box<dyn Handler>` / `Arc<dyn Handler>` rather than
107// `<H: Handler + ?Sized>` blankets to avoid coherence overlap with the
108// `impl<F, Fut> Handler for F where F: Fn(&HandlerCtx) -> Fut` blanket
109// above.
110impl Handler for Box<dyn Handler> {
111    fn handle<'a>(
112        &'a self,
113        cx: &'a HandlerCtx,
114    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NotifAction> + Send + 'a>> {
115        (**self).handle(cx)
116    }
117}
118
119impl Handler for std::sync::Arc<dyn Handler> {
120    fn handle<'a>(
121        &'a self,
122        cx: &'a HandlerCtx,
123    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NotifAction> + Send + 'a>> {
124        (**self).handle(cx)
125    }
126}
127
128/// Errors raised when registering user handlers via
129/// [`crate::Sandbox::run_with_handlers`].
130#[derive(Debug, Error, PartialEq, Eq)]
131pub enum HandlerError {
132    #[error("invalid syscall in handler registration: {0}")]
133    InvalidSyscall(#[from] SyscallError),
134
135    #[error(
136        "handler on syscall {syscall_nr} conflicts with the policy syscall blocklist \
137         and would let user code bypass it via SECCOMP_USER_NOTIF_FLAG_CONTINUE"
138    )]
139    OnDenySyscall { syscall_nr: i64 },
140}
141
142/// Reject handler registrations that would weaken sandlock's confinement
143/// guarantees.
144///
145/// The cBPF program emits notif JEQs *before* deny JEQs, so a syscall
146/// present in both lists hits `SECCOMP_RET_USER_NOTIF` first.  A handler
147/// registered on a syscall that is on the blocklist would therefore
148/// convert a kernel-deny into a user-supervised path: a handler returning
149/// `NotifAction::Continue` becomes `SECCOMP_USER_NOTIF_FLAG_CONTINUE` and
150/// the kernel actually runs the syscall — silently bypassing deny.
151///
152/// The blocklist is whatever [`crate::context::blocklist_syscall_numbers`]
153/// resolves from Sandlock's default syscall blocklist plus policy extras.
154///
155/// Every open-family syscall a path-keyed handler must intercept to be
156/// leak-proof: `open` (legacy), `openat`, and `openat2`. A handler that
157/// only registers `openat` is bypassed by any libc that picks one of
158/// the others. The corresponding BPF notif list (in `context::notif_syscalls`)
159/// must register the same set so the kernel actually produces a
160/// notification — otherwise `RET_ALLOW` makes the handler unreachable.
161fn open_family_syscalls() -> Vec<i64> {
162    let mut v = vec![libc::SYS_openat, arch::SYS_OPENAT2];
163    if let Some(legacy_open) = arch::SYS_OPEN {
164        v.push(legacy_open);
165    }
166    v
167}
168
169/// Takes only the syscall numbers because that's all it needs to check.
170/// Called from the `run_with_handlers` entry points before any
171/// handler is registered against the dispatch table.
172///
173/// Returns the offending syscall number on rejection so the caller can
174/// surface it to the end user.
175pub(crate) fn validate_handler_syscalls_against_policy(
176    syscall_nrs: &[i64],
177    policy: &crate::sandbox::Sandbox,
178) -> Result<(), i64> {
179    let blocklist: std::collections::HashSet<u32> =
180        crate::context::blocklist_syscall_numbers(policy).into_iter().collect();
181    for &nr in syscall_nrs {
182        if blocklist.contains(&(nr as u32)) {
183            return Err(nr);
184        }
185    }
186    Ok(())
187}
188
189
190/// Ordered chain of handlers for a single syscall number.
191struct HandlerChain {
192    handlers: Vec<std::sync::Arc<dyn Handler>>,
193}
194
195/// Maps syscall numbers to handler chains.
196pub struct DispatchTable {
197    chains: HashMap<i64, HandlerChain>,
198}
199
200impl DispatchTable {
201    /// Create an empty dispatch table.
202    pub fn new() -> Self {
203        Self {
204            chains: HashMap::new(),
205        }
206    }
207
208    /// Register a handler for the given syscall number.  Handlers are
209    /// called in registration order; the first non-Continue result wins.
210    ///
211    /// Generic over `H: Handler` — accepts either a struct with explicit
212    /// `impl Handler for ...` or a closure (via blanket impl).
213    pub fn register<H: Handler>(&mut self, syscall_nr: i64, handler: H) {
214        self.register_arc(syscall_nr, std::sync::Arc::new(handler));
215    }
216
217    /// Register a pre-`Arc`'d handler.  Used both by builtin chunks
218    /// that share state via `Arc::clone` (one `ForkHandler` instance
219    /// registers against `SYS_clone`/`SYS_clone3`/`SYS_vfork`) and by
220    /// `run_with_handlers` when each item already arrives as
221    /// `Arc<dyn Handler>`.
222    pub(crate) fn register_arc(
223        &mut self,
224        syscall_nr: i64,
225        handler: std::sync::Arc<dyn Handler>,
226    ) {
227        self.chains
228            .entry(syscall_nr)
229            .or_insert_with(|| HandlerChain { handlers: Vec::new() })
230            .handlers
231            .push(handler);
232    }
233
234    /// Dispatch a notification through the handler chain for its syscall number.
235    pub(crate) async fn dispatch(
236        &self,
237        notif: SeccompNotif,
238        notif_fd: RawFd,
239    ) -> NotifAction {
240        let nr = notif.data.nr as i64;
241        if let Some(chain) = self.chains.get(&nr) {
242            let handler_ctx = HandlerCtx { notif, notif_fd };
243            for handler in &chain.handlers {
244                let action = handler.handle(&handler_ctx).await;
245                if !matches!(action, NotifAction::Continue) {
246                    return action;
247                }
248            }
249        }
250        NotifAction::Continue
251    }
252}
253
254// ============================================================
255// Table builder — mechanical translation of old dispatch()
256// ============================================================
257
258/// Build the dispatch table from a `NotifPolicy`.  Every branch from the old
259/// monolithic `dispatch()` function is translated into a `table.register()` call.
260/// Priority is preserved by registration order.
261///
262/// `pending_handlers` are appended **after** all builtin handlers, so they
263/// observe the post-builtin view (e.g. `chroot`-normalized paths on
264/// `openat`).  Builtins cannot be overridden or removed — this is the
265/// security boundary for downstream crates.
266pub(crate) fn build_dispatch_table(
267    policy: &Arc<NotifPolicy>,
268    resource: &Arc<Mutex<ResourceState>>,
269    ctx: &Arc<SupervisorCtx>,
270    pending_handlers: Vec<(i64, std::sync::Arc<dyn Handler>)>,
271) -> DispatchTable {
272    let mut table = DispatchTable::new();
273
274    // ------------------------------------------------------------------
275    // Fork/clone family (always on)
276    // ------------------------------------------------------------------
277    for &nr in arch::FORK_LIKE_SYSCALLS {
278        let policy_for_fork = Arc::clone(policy);
279        let resource_for_fork = Arc::clone(resource);
280        table.register(nr, move |cx: &HandlerCtx| {
281            let notif = cx.notif;
282            let notif_fd = cx.notif_fd;
283            let policy = Arc::clone(&policy_for_fork);
284            let resource = Arc::clone(&resource_for_fork);
285            async move {
286                crate::resource::handle_fork(&notif, notif_fd, &resource, &policy).await
287            }
288        });
289    }
290
291    // ------------------------------------------------------------------
292    // Wait family (always on)
293    // ------------------------------------------------------------------
294    for &nr in &[libc::SYS_wait4, libc::SYS_waitid] {
295        let resource_for_wait = Arc::clone(resource);
296        table.register(nr, move |cx: &HandlerCtx| {
297            let notif = cx.notif;
298            let resource = Arc::clone(&resource_for_wait);
299            async move {
300                crate::resource::handle_wait(&notif, &resource).await
301            }
302        });
303    }
304
305    // ------------------------------------------------------------------
306    // Memory management (conditional on has_memory_limit)
307    // ------------------------------------------------------------------
308    if policy.has_memory_limit {
309        for &nr in &[
310            libc::SYS_mmap, libc::SYS_munmap, libc::SYS_brk,
311            libc::SYS_mremap, libc::SYS_shmget,
312        ] {
313            let policy_for_mem = Arc::clone(policy);
314            let __sup = Arc::clone(ctx);
315            table.register(nr, move |cx: &HandlerCtx| {
316                let notif = cx.notif;
317                let sup = Arc::clone(&__sup);
318                let policy = Arc::clone(&policy_for_mem);
319                async move {
320                    crate::resource::handle_memory(&notif, &sup, &policy).await
321                }
322            });
323        }
324    }
325
326    // ------------------------------------------------------------------
327    // Network (conditional on has_net_allowlist || has_http_acl)
328    // ------------------------------------------------------------------
329    if policy.has_net_allowlist || policy.has_http_acl {
330        for &nr in &[
331            libc::SYS_connect,
332            libc::SYS_sendto,
333            libc::SYS_sendmsg,
334            libc::SYS_sendmmsg,
335        ] {
336            let __sup = Arc::clone(ctx);
337            table.register(nr, move |cx: &HandlerCtx| {
338                let notif = cx.notif;
339                let sup = Arc::clone(&__sup);
340                let notif_fd = cx.notif_fd;
341                async move {
342                    crate::network::handle_net(&notif, &sup, notif_fd).await
343                }
344            });
345        }
346    }
347
348    // ------------------------------------------------------------------
349    // Deterministic random — getrandom()
350    // ------------------------------------------------------------------
351    if policy.has_random_seed {
352        let __sup = Arc::clone(ctx);
353        table.register(libc::SYS_getrandom, move |cx: &HandlerCtx| {
354            let notif = cx.notif;
355            let sup = Arc::clone(&__sup);
356            let notif_fd = cx.notif_fd;
357            async move {
358                let mut tr = sup.time_random.lock().await;
359                if let Some(ref mut rng) = tr.random_state {
360                    crate::random::handle_getrandom(&notif, rng, notif_fd)
361                } else {
362                    NotifAction::Continue
363                }
364            }
365        });
366    }
367
368    // ------------------------------------------------------------------
369    // Deterministic random — /dev/urandom and /dev/random opens.
370    // Registered for every open-family syscall so dirfd-relative and
371    // legacy `open` spellings can't slip past the seed and read the
372    // kernel's real entropy.
373    // ------------------------------------------------------------------
374    if policy.has_random_seed {
375        for nr in open_family_syscalls() {
376            let __sup = Arc::clone(ctx);
377            table.register(nr, move |cx: &HandlerCtx| {
378                let notif = cx.notif;
379                let sup = Arc::clone(&__sup);
380                let notif_fd = cx.notif_fd;
381                async move {
382                    let mut tr = sup.time_random.lock().await;
383                    if let Some(ref mut rng) = tr.random_state {
384                        if let Some(action) = crate::random::handle_random_open(&notif, rng, notif_fd) {
385                            return action;
386                        }
387                    }
388                    NotifAction::Continue
389                }
390            });
391        }
392    }
393
394    // ------------------------------------------------------------------
395    // Timer adjustment (conditional on has_time_start)
396    // ------------------------------------------------------------------
397    if policy.has_time_start {
398        let time_offset = policy.time_offset;
399        for &nr in &[
400            libc::SYS_clock_nanosleep as i64,
401            libc::SYS_timerfd_settime as i64,
402            libc::SYS_timer_settime as i64,
403        ] {
404            table.register(nr, move |cx: &HandlerCtx| {
405                let notif = cx.notif;
406                let notif_fd = cx.notif_fd;
407                async move {
408                    crate::time::handle_timer(&notif, time_offset, notif_fd)
409                }
410            });
411        }
412    }
413
414    // ------------------------------------------------------------------
415    // /etc/hosts virtualization: always on. The synthetic file contains
416    // the loopback base (or, in chroot/image mode, the image's own
417    // `/etc/hosts` merged with a loopback fallback) plus any concrete
418    // hostnames resolved from `net_allow`, so the host's on-disk
419    // `/etc/hosts` never leaks in and image-baked entries are preserved.
420    //
421    // Registered for every open-family syscall (see `open_family_syscalls`).
422    // Must run *before* the chroot handler so that in chroot mode the
423    // synthetic memfd wins over a direct open of `<chroot>/etc/hosts` —
424    // the chroot handler always intercepts opens within the chroot and
425    // would otherwise serve the raw image file, defeating the merge.
426    // ------------------------------------------------------------------
427    {
428        let etc_hosts = policy.virtual_etc_hosts.clone();
429        for nr in open_family_syscalls() {
430            let etc_hosts = etc_hosts.clone();
431            table.register(nr, move |cx: &HandlerCtx| {
432                let notif = cx.notif;
433                let notif_fd = cx.notif_fd;
434                let etc_hosts = etc_hosts.clone();
435                async move {
436                    if let Some(action) = crate::procfs::handle_etc_hosts_open(&notif, &etc_hosts, notif_fd) {
437                        action
438                    } else {
439                        NotifAction::Continue
440                    }
441                }
442            });
443        }
444    }
445
446    // ------------------------------------------------------------------
447    // Chroot path interception (before COW)
448    // ------------------------------------------------------------------
449    if policy.chroot_root.is_some() {
450        register_chroot_handlers(&mut table, policy, ctx);
451    }
452
453    // ------------------------------------------------------------------
454    // COW filesystem interception
455    // ------------------------------------------------------------------
456    if policy.cow_enabled {
457        register_cow_handlers(&mut table, ctx);
458    }
459
460    // ------------------------------------------------------------------
461    // /proc virtualization (always on). The handler does both
462    // sensitive-path blocking and per-PID filtering, both of which are
463    // security boundaries, so it has to catch every open-family spelling.
464    // ------------------------------------------------------------------
465    for nr in open_family_syscalls() {
466        let policy_for_proc_open = Arc::clone(policy);
467        let resource_for_proc_open = Arc::clone(resource);
468        let __sup = Arc::clone(ctx);
469        table.register(nr, move |cx: &HandlerCtx| {
470            let notif = cx.notif;
471            let sup = Arc::clone(&__sup);
472            let notif_fd = cx.notif_fd;
473            let policy = Arc::clone(&policy_for_proc_open);
474            let resource = Arc::clone(&resource_for_proc_open);
475            async move {
476                let processes = Arc::clone(&sup.processes);
477                let network = Arc::clone(&sup.network);
478                crate::procfs::handle_proc_open(&notif, &processes, &resource, &network, &policy, notif_fd).await
479            }
480        });
481    }
482    let mut getdents_nrs = vec![libc::SYS_getdents64];
483    if let Some(getdents) = arch::SYS_GETDENTS {
484        getdents_nrs.push(getdents);
485    }
486    for nr in getdents_nrs {
487        let policy_for_getdents = Arc::clone(policy);
488        let __sup = Arc::clone(ctx);
489        table.register(nr, move |cx: &HandlerCtx| {
490            let notif = cx.notif;
491            let sup = Arc::clone(&__sup);
492            let notif_fd = cx.notif_fd;
493            let policy = Arc::clone(&policy_for_getdents);
494            async move {
495                let processes = Arc::clone(&sup.processes);
496                crate::procfs::handle_getdents(&notif, &processes, &policy, notif_fd).await
497            }
498        });
499    }
500
501    // ------------------------------------------------------------------
502    // Virtual CPU count
503    // ------------------------------------------------------------------
504    if let Some(n) = policy.num_cpus {
505        table.register(libc::SYS_sched_getaffinity, move |cx: &HandlerCtx| {
506            let notif = cx.notif;
507            let notif_fd = cx.notif_fd;
508            async move {
509                crate::procfs::handle_sched_getaffinity(&notif, n, notif_fd)
510            }
511        });
512    }
513
514    // ------------------------------------------------------------------
515    // Hostname virtualization. The `/etc/hostname` shim is registered
516    // for every open-family syscall so dirfd-relative and legacy `open`
517    // spellings can't leak the host's real hostname.
518    // ------------------------------------------------------------------
519    if let Some(ref hostname) = policy.virtual_hostname {
520        let hostname_for_uname = hostname.clone();
521        let hostname_for_open = hostname.clone();
522        table.register(libc::SYS_uname, move |cx: &HandlerCtx| {
523            let notif = cx.notif;
524            let notif_fd = cx.notif_fd;
525            let hostname = hostname_for_uname.clone();
526            async move {
527                crate::procfs::handle_uname(&notif, &hostname, notif_fd)
528            }
529        });
530        for nr in open_family_syscalls() {
531            let hostname = hostname_for_open.clone();
532            table.register(nr, move |cx: &HandlerCtx| {
533                let notif = cx.notif;
534                let notif_fd = cx.notif_fd;
535                let hostname = hostname.clone();
536                async move {
537                    if let Some(action) = crate::procfs::handle_hostname_open(&notif, &hostname, notif_fd) {
538                        action
539                    } else {
540                        NotifAction::Continue
541                    }
542                }
543            });
544        }
545    }
546
547    // /etc/hosts is registered above the chroot block — see the comment there.
548
549    // ------------------------------------------------------------------
550    // Deterministic directory listing
551    // ------------------------------------------------------------------
552    if policy.deterministic_dirs {
553        let mut getdents_nrs = vec![libc::SYS_getdents64];
554        if let Some(getdents) = arch::SYS_GETDENTS {
555            getdents_nrs.push(getdents);
556        }
557        for nr in getdents_nrs {
558            let __sup = Arc::clone(ctx);
559            table.register(nr, move |cx: &HandlerCtx| {
560                let notif = cx.notif;
561                let sup = Arc::clone(&__sup);
562                let notif_fd = cx.notif_fd;
563                async move {
564                    let processes = Arc::clone(&sup.processes);
565                    crate::procfs::handle_sorted_getdents(&notif, &processes, notif_fd).await
566                }
567            });
568        }
569    }
570
571    // ------------------------------------------------------------------
572    // NETLINK_ROUTE virtualization (always on).
573    //
574    // Send/recv traffic flows through a `socketpair(AF_UNIX,
575    // SOCK_SEQPACKET)` whose supervisor-side end is driven by a tokio
576    // task spawned in `handle_socket`.  Only `socket`, `bind`,
577    // `getsockname`, `recvmsg`/`recvfrom`, and `close` need supervisor
578    // intercepts; send uses the kernel directly.
579    //
580    // Must register before `port_remap` so the netlink `bind` handler
581    // runs first and returns `Continue` for non-cookie fds.
582    // ------------------------------------------------------------------
583    {
584        let __sup = Arc::clone(ctx);
585        table.register(libc::SYS_socket, move |cx: &HandlerCtx| {
586            let notif = cx.notif;
587            let sup = Arc::clone(&__sup);
588            async move {
589                let state = Arc::clone(&sup.netlink);
590                crate::netlink::handlers::handle_socket(&notif, &state).await
591            }
592        });
593        let __sup = Arc::clone(ctx);
594        table.register(libc::SYS_bind, move |cx: &HandlerCtx| {
595            let notif = cx.notif;
596            let sup = Arc::clone(&__sup);
597            async move {
598                let state = Arc::clone(&sup.netlink);
599                crate::netlink::handlers::handle_bind(&notif, &state).await
600            }
601        });
602        let __sup = Arc::clone(ctx);
603        table.register(libc::SYS_getsockname, move |cx: &HandlerCtx| {
604            let notif = cx.notif;
605            let sup = Arc::clone(&__sup);
606            let notif_fd = cx.notif_fd;
607            async move {
608                let state = Arc::clone(&sup.netlink);
609                crate::netlink::handlers::handle_getsockname(&notif, &state, notif_fd).await
610            }
611        });
612        // Zero the msg_name region on recv so glibc sees nl_pid=0
613        // (the kernel only writes sun_family on unix socketpair recvmsg,
614        //  leaving the rest of the buffer as stack garbage otherwise).
615        for &nr in &[libc::SYS_recvfrom, libc::SYS_recvmsg] {
616            let __sup = Arc::clone(ctx);
617            table.register(nr, move |cx: &HandlerCtx| {
618                let notif = cx.notif;
619                let sup = Arc::clone(&__sup);
620                let notif_fd = cx.notif_fd;
621                async move {
622                    let state = Arc::clone(&sup.netlink);
623                    crate::netlink::handlers::handle_netlink_recvmsg(&notif, &state, notif_fd).await
624                }
625            });
626        }
627        // Unregister on close so the (pid, fd) slot isn't left in the
628        // cookie set once the child reuses the fd for something else.
629        let __sup = Arc::clone(ctx);
630        table.register(libc::SYS_close, move |cx: &HandlerCtx| {
631            let notif = cx.notif;
632            let sup = Arc::clone(&__sup);
633            async move {
634                let state = Arc::clone(&sup.netlink);
635                crate::netlink::handlers::handle_close(&notif, &state).await
636            }
637        });
638    }
639
640    // ------------------------------------------------------------------
641    // Bind — on-behalf
642    // ------------------------------------------------------------------
643    if policy.port_remap || policy.has_net_allowlist {
644        let __sup = Arc::clone(ctx);
645        table.register(libc::SYS_bind, move |cx: &HandlerCtx| {
646            let notif = cx.notif;
647            let sup = Arc::clone(&__sup);
648            let notif_fd = cx.notif_fd;
649            async move {
650                crate::port_remap::handle_bind(&notif, &sup.network, notif_fd).await
651            }
652        });
653    }
654
655    // ------------------------------------------------------------------
656    // getsockname — port remap
657    // ------------------------------------------------------------------
658    if policy.port_remap {
659        let __sup = Arc::clone(ctx);
660        table.register(libc::SYS_getsockname, move |cx: &HandlerCtx| {
661            let notif = cx.notif;
662            let sup = Arc::clone(&__sup);
663            let notif_fd = cx.notif_fd;
664            async move {
665                crate::port_remap::handle_getsockname(&notif, &sup.network, notif_fd).await
666            }
667        });
668    }
669
670    // ------------------------------------------------------------------
671    // Pending user handlers — appended after builtins so builtin handlers
672    // keep their security-critical priority (chroot path normalization,
673    // COW writes, resource accounting).
674    // ------------------------------------------------------------------
675    for (nr, h) in pending_handlers {
676        table.register_arc(nr, h);
677    }
678
679    table
680}
681
682// ============================================================
683// Chroot handler registration
684// ============================================================
685
686fn register_chroot_handlers(
687    table: &mut DispatchTable,
688    policy: &Arc<NotifPolicy>,
689    ctx: &Arc<SupervisorCtx>,
690) {
691    use crate::chroot::dispatch::ChrootCtx;
692
693    // Helper macro — produces a closure satisfying Handler via blanket impl.
694    // The closure clones `policy` (Arc) before the async block; inside the
695    // async block it borrows fields of that cloned Arc to build `ChrootCtx`.
696    macro_rules! chroot_handler {
697        ($policy:expr, $handler:expr) => {{
698            let policy = Arc::clone($policy);
699            let chroot_state = Arc::clone(&ctx.chroot);
700            let cow_state = Arc::clone(&ctx.cow);
701            move |cx: &HandlerCtx| {
702                let notif = cx.notif;
703                let chroot_state = Arc::clone(&chroot_state);
704                let cow_state = Arc::clone(&cow_state);
705                let notif_fd = cx.notif_fd;
706                let policy = Arc::clone(&policy);
707                async move {
708                    let chroot_ctx = ChrootCtx {
709                        root: policy.chroot_root.as_ref().unwrap(),
710                        readable: &policy.chroot_readable,
711                        writable: &policy.chroot_writable,
712                        denied: &policy.chroot_denied,
713                        mounts: &policy.chroot_mounts,
714                    };
715                    $handler(&notif, &chroot_state, &cow_state, notif_fd, &chroot_ctx).await
716                }
717            }
718        }};
719    }
720
721    // Same shape for fall-through variants (semantically identical here;
722    // kept separate for symmetry with the old code).
723    macro_rules! chroot_handler_fallthrough {
724        ($policy:expr, $handler:expr) => {{
725            let policy = Arc::clone($policy);
726            let chroot_state = Arc::clone(&ctx.chroot);
727            let cow_state = Arc::clone(&ctx.cow);
728            move |cx: &HandlerCtx| {
729                let notif = cx.notif;
730                let chroot_state = Arc::clone(&chroot_state);
731                let cow_state = Arc::clone(&cow_state);
732                let notif_fd = cx.notif_fd;
733                let policy = Arc::clone(&policy);
734                async move {
735                    let chroot_ctx = ChrootCtx {
736                        root: policy.chroot_root.as_ref().unwrap(),
737                        readable: &policy.chroot_readable,
738                        writable: &policy.chroot_writable,
739                        denied: &policy.chroot_denied,
740                        mounts: &policy.chroot_mounts,
741                    };
742                    $handler(&notif, &chroot_state, &cow_state, notif_fd, &chroot_ctx).await
743                }
744            }
745        }};
746    }
747
748    // openat — fallthrough if Continue
749    table.register(libc::SYS_openat, chroot_handler_fallthrough!(policy,
750        crate::chroot::dispatch::handle_chroot_open));
751
752    // open (legacy) — fallthrough if Continue
753    if let Some(open) = arch::SYS_OPEN {
754        table.register(open, chroot_handler_fallthrough!(policy,
755            crate::chroot::dispatch::handle_chroot_legacy_open));
756    }
757
758    // execve, execveat — unconditional return
759    for &nr in &[libc::SYS_execve, libc::SYS_execveat] {
760        table.register(nr, chroot_handler!(policy,
761            crate::chroot::dispatch::handle_chroot_exec));
762    }
763
764    // Modern write syscalls
765    for &nr in &[
766        libc::SYS_unlinkat, libc::SYS_mkdirat, libc::SYS_renameat2,
767        libc::SYS_symlinkat, libc::SYS_linkat, libc::SYS_fchmodat,
768        libc::SYS_fchownat, libc::SYS_truncate,
769    ] {
770        table.register(nr, chroot_handler!(policy,
771            crate::chroot::dispatch::handle_chroot_write));
772    }
773
774    // Legacy write syscalls
775    if let Some(nr) = arch::SYS_UNLINK {
776        table.register(nr, chroot_handler!(policy,
777            crate::chroot::dispatch::handle_chroot_legacy_unlink));
778    }
779    if let Some(nr) = arch::SYS_RMDIR {
780        table.register(nr, chroot_handler!(policy,
781            crate::chroot::dispatch::handle_chroot_legacy_rmdir));
782    }
783    if let Some(nr) = arch::SYS_MKDIR {
784        table.register(nr, chroot_handler!(policy,
785            crate::chroot::dispatch::handle_chroot_legacy_mkdir));
786    }
787    if let Some(nr) = arch::SYS_RENAME {
788        table.register(nr, chroot_handler!(policy,
789            crate::chroot::dispatch::handle_chroot_legacy_rename));
790    }
791    if let Some(nr) = arch::SYS_SYMLINK {
792        table.register(nr, chroot_handler!(policy,
793            crate::chroot::dispatch::handle_chroot_legacy_symlink));
794    }
795    if let Some(nr) = arch::SYS_LINK {
796        table.register(nr, chroot_handler!(policy,
797            crate::chroot::dispatch::handle_chroot_legacy_link));
798    }
799    if let Some(nr) = arch::SYS_CHMOD {
800        table.register(nr, chroot_handler!(policy,
801            crate::chroot::dispatch::handle_chroot_legacy_chmod));
802    }
803
804    // chown — non-follow
805    if let Some(chown) = arch::SYS_CHOWN {
806        let policy_for_chown = Arc::clone(policy);
807        let __sup = Arc::clone(ctx);
808        table.register(chown, move |cx: &HandlerCtx| {
809            let notif = cx.notif;
810            let sup = Arc::clone(&__sup);
811            let notif_fd = cx.notif_fd;
812            let policy = Arc::clone(&policy_for_chown);
813            async move {
814                let chroot_ctx = ChrootCtx {
815                    root: policy.chroot_root.as_ref().unwrap(),
816                    readable: &policy.chroot_readable,
817                    writable: &policy.chroot_writable,
818                    denied: &policy.chroot_denied,
819                    mounts: &policy.chroot_mounts,
820                };
821                crate::chroot::dispatch::handle_chroot_legacy_chown(&notif, &sup.chroot, &sup.cow, notif_fd, &chroot_ctx, false).await
822            }
823        });
824    }
825
826    // lchown — follow
827    if let Some(lchown) = arch::SYS_LCHOWN {
828        let policy_for_lchown = Arc::clone(policy);
829        let __sup = Arc::clone(ctx);
830        table.register(lchown, move |cx: &HandlerCtx| {
831            let notif = cx.notif;
832            let sup = Arc::clone(&__sup);
833            let notif_fd = cx.notif_fd;
834            let policy = Arc::clone(&policy_for_lchown);
835            async move {
836                let chroot_ctx = ChrootCtx {
837                    root: policy.chroot_root.as_ref().unwrap(),
838                    readable: &policy.chroot_readable,
839                    writable: &policy.chroot_writable,
840                    denied: &policy.chroot_denied,
841                    mounts: &policy.chroot_mounts,
842                };
843                crate::chroot::dispatch::handle_chroot_legacy_chown(&notif, &sup.chroot, &sup.cow, notif_fd, &chroot_ctx, true).await
844            }
845        });
846    }
847
848    // stat family
849    for &nr in &[
850        libc::SYS_newfstatat,
851        libc::SYS_faccessat,
852        crate::chroot::dispatch::SYS_FACCESSAT2,
853    ] {
854        table.register(nr, chroot_handler!(policy,
855            crate::chroot::dispatch::handle_chroot_stat));
856    }
857
858    // Legacy stat
859    if let Some(nr) = arch::SYS_STAT {
860        table.register(nr, chroot_handler!(policy,
861            crate::chroot::dispatch::handle_chroot_legacy_stat));
862    }
863    if let Some(nr) = arch::SYS_LSTAT {
864        table.register(nr, chroot_handler!(policy,
865            crate::chroot::dispatch::handle_chroot_legacy_lstat));
866    }
867    if let Some(nr) = arch::SYS_ACCESS {
868        table.register(nr, chroot_handler!(policy,
869            crate::chroot::dispatch::handle_chroot_legacy_access));
870    }
871
872    // statx
873    table.register(libc::SYS_statx, chroot_handler!(policy,
874        crate::chroot::dispatch::handle_chroot_statx));
875
876    // readlink
877    table.register(libc::SYS_readlinkat, chroot_handler!(policy,
878        crate::chroot::dispatch::handle_chroot_readlink));
879    if let Some(nr) = arch::SYS_READLINK {
880        table.register(nr, chroot_handler!(policy,
881            crate::chroot::dispatch::handle_chroot_legacy_readlink));
882    }
883
884    // getdents
885    let mut getdents_nrs = vec![libc::SYS_getdents64];
886    if let Some(getdents) = arch::SYS_GETDENTS {
887        getdents_nrs.push(getdents);
888    }
889    for nr in getdents_nrs {
890        table.register(nr, chroot_handler!(policy,
891            crate::chroot::dispatch::handle_chroot_getdents));
892    }
893
894    // chdir, getcwd, statfs, utimensat
895    table.register(libc::SYS_chdir as i64, chroot_handler!(policy,
896        crate::chroot::dispatch::handle_chroot_chdir));
897    table.register(libc::SYS_getcwd as i64, chroot_handler!(policy,
898        crate::chroot::dispatch::handle_chroot_getcwd));
899    table.register(libc::SYS_statfs as i64, chroot_handler!(policy,
900        crate::chroot::dispatch::handle_chroot_statfs));
901    table.register(libc::SYS_utimensat as i64, chroot_handler!(policy,
902        crate::chroot::dispatch::handle_chroot_utimensat));
903}
904
905// ============================================================
906// COW handler registration
907// ============================================================
908
909fn register_cow_handlers(table: &mut DispatchTable, ctx: &Arc<SupervisorCtx>) {
910    // Helper that captures `ctx.cow` and `ctx.processes` once at table-build
911    // time, then re-clones the per-handler `Arc`s on each invocation.
912    macro_rules! cow_call {
913        ($handler:expr) => {{
914            let cow_state = Arc::clone(&ctx.cow);
915            let processes_state = Arc::clone(&ctx.processes);
916            move |cx: &HandlerCtx| {
917                let notif = cx.notif;
918                let cow_state = Arc::clone(&cow_state);
919                let processes_state = Arc::clone(&processes_state);
920                let notif_fd = cx.notif_fd;
921                async move {
922                    $handler(&notif, &cow_state, &processes_state, notif_fd).await
923                }
924            }
925        }};
926    }
927
928    // Write syscalls (*at variants + legacy)
929    let mut write_nrs = vec![
930        libc::SYS_unlinkat, libc::SYS_mkdirat, libc::SYS_renameat2,
931        libc::SYS_symlinkat, libc::SYS_linkat, libc::SYS_fchmodat,
932        libc::SYS_fchownat, libc::SYS_truncate,
933    ];
934    write_nrs.extend([
935        arch::SYS_UNLINK, arch::SYS_RMDIR, arch::SYS_MKDIR, arch::SYS_RENAME,
936        arch::SYS_SYMLINK, arch::SYS_LINK, arch::SYS_CHMOD, arch::SYS_CHOWN,
937        arch::SYS_LCHOWN,
938    ].into_iter().flatten());
939    for nr in write_nrs {
940        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_write));
941    }
942
943    table.register(libc::SYS_utimensat, cow_call!(crate::cow::dispatch::handle_cow_utimensat));
944
945    let mut access_nrs = vec![libc::SYS_faccessat, crate::cow::dispatch::SYS_FACCESSAT2];
946    access_nrs.extend(arch::SYS_ACCESS);
947    for nr in access_nrs {
948        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_access));
949    }
950
951    let mut open_nrs = vec![libc::SYS_openat];
952    open_nrs.extend(arch::SYS_OPEN);
953    for nr in open_nrs {
954        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_open));
955    }
956
957    let mut stat_nrs = vec![libc::SYS_newfstatat, libc::SYS_faccessat];
958    stat_nrs.extend([arch::SYS_STAT, arch::SYS_LSTAT, arch::SYS_ACCESS].into_iter().flatten());
959    for nr in stat_nrs {
960        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_stat));
961    }
962
963    table.register(libc::SYS_statx, cow_call!(crate::cow::dispatch::handle_cow_statx));
964
965    let mut readlink_nrs = vec![libc::SYS_readlinkat];
966    readlink_nrs.extend(arch::SYS_READLINK);
967    for nr in readlink_nrs {
968        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_readlink));
969    }
970
971    let mut getdents_nrs = vec![libc::SYS_getdents64];
972    getdents_nrs.extend(arch::SYS_GETDENTS);
973    for nr in getdents_nrs {
974        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_getdents));
975    }
976
977    table.register(libc::SYS_chdir, cow_call!(crate::cow::dispatch::handle_cow_chdir));
978    table.register(libc::SYS_getcwd, cow_call!(crate::cow::dispatch::handle_cow_getcwd));
979
980    for &nr in &[libc::SYS_execve, libc::SYS_execveat] {
981        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_exec));
982    }
983}
984
985// ============================================================
986// Tests
987// ============================================================
988
989#[cfg(test)]
990mod handler_tests {
991    //! Unit tests for the user-supplied handler extension API.
992    //!
993    //! Drive the actual `DispatchTable::dispatch` walker against a minimal
994    //! `SupervisorCtx` constructed from default-state pieces.  Handler
995    //! closures here ignore the context (no notif fd, no real child), so
996    //! the dispatch invariants under test (registration order, chain
997    //! short-circuit on first non-`Continue`, append-after-builtin
998    //! placement) are exercised end-to-end without needing a live
999    //! Landlock+seccomp sandbox — those scenarios live under
1000    //! `crates/sandlock-core/tests/integration/test_handlers.rs`.
1001    use super::*;
1002    use crate::netlink::NetlinkState;
1003    use crate::seccomp::ctx::SupervisorCtx;
1004    use crate::seccomp::notif::NotifPolicy;
1005    use crate::seccomp::state::{
1006        ChrootState, CowState, NetworkState, PolicyFnState, ProcessIndex, ProcfsState,
1007        ResourceState, TimeRandomState,
1008    };
1009    use crate::sys::structs::{SeccompData, SeccompNotif};
1010    use std::sync::atomic::{AtomicUsize, Ordering};
1011
1012    fn fake_notif(nr: i32) -> SeccompNotif {
1013        SeccompNotif {
1014            id: 0,
1015            pid: 1,
1016            flags: 0,
1017            data: SeccompData {
1018                nr,
1019                arch: 0,
1020                instruction_pointer: 0,
1021                args: [0; 6],
1022            },
1023        }
1024    }
1025
1026    /// Minimal `SupervisorCtx` for unit tests.  Every field is built from
1027    /// the corresponding state's `new()`/default constructor — no syscalls,
1028    /// no fds, no spawned children.  Handlers in these tests do not
1029    /// actually inspect the context, so the values do not need to match
1030    /// any real run; they only need to satisfy the type signature so we
1031    /// can call `dispatch()`.
1032    fn fake_supervisor_ctx() -> Arc<SupervisorCtx> {
1033        Arc::new(SupervisorCtx {
1034            resource: Arc::new(Mutex::new(ResourceState::new(0, 0))),
1035            cow: Arc::new(Mutex::new(CowState::new())),
1036            procfs: Arc::new(Mutex::new(ProcfsState::new())),
1037            network: Arc::new(Mutex::new(NetworkState::new())),
1038            time_random: Arc::new(Mutex::new(TimeRandomState::new(None, None))),
1039            policy_fn: Arc::new(Mutex::new(PolicyFnState::new())),
1040            chroot: Arc::new(Mutex::new(ChrootState::new())),
1041            netlink: Arc::new(NetlinkState::new()),
1042            processes: Arc::new(ProcessIndex::new()),
1043            policy: Arc::new(NotifPolicy {
1044                max_memory_bytes: 0,
1045                max_processes: 0,
1046                has_memory_limit: false,
1047                has_net_allowlist: false,
1048                has_random_seed: false,
1049                has_time_start: false,
1050                time_offset: 0,
1051                num_cpus: None,
1052                argv_safety_required: false,
1053                port_remap: false,
1054                cow_enabled: false,
1055                chroot_root: None,
1056                chroot_readable: Vec::new(),
1057                chroot_writable: Vec::new(),
1058                chroot_denied: Vec::new(),
1059                chroot_mounts: Vec::new(),
1060                deterministic_dirs: false,
1061                virtual_hostname: None,
1062                has_http_acl: false,
1063                virtual_etc_hosts: String::new(),
1064            }),
1065            child_pidfd: None,
1066            notif_fd: -1,
1067        })
1068    }
1069
1070    /// All registered handlers run, in registration order, when each
1071    /// returns `Continue`.  Verifies that `register` appends to the
1072    /// underlying `Vec` and that `dispatch` walks it front-to-back.
1073    #[tokio::test]
1074    async fn dispatch_walks_chain_in_registration_order() {
1075        let mut table = DispatchTable::new();
1076        let order = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
1077
1078        for tag in [1u8, 2u8, 3u8] {
1079            let order_clone = Arc::clone(&order);
1080            table.register(
1081                libc::SYS_openat,
1082                move |_cx: &HandlerCtx| {
1083                    let order = Arc::clone(&order_clone);
1084                    async move {
1085                        order.lock().unwrap().push(tag);
1086                        NotifAction::Continue
1087                    }
1088                },
1089            );
1090        }
1091
1092        let _ctx = fake_supervisor_ctx();
1093        let action = table
1094            .dispatch(fake_notif(libc::SYS_openat as i32), -1)
1095            .await;
1096
1097        assert!(matches!(action, NotifAction::Continue));
1098        let recorded = order.lock().unwrap();
1099        assert_eq!(
1100            *recorded,
1101            [1u8, 2u8, 3u8],
1102            "every handler must run, in the order it was registered"
1103        );
1104    }
1105
1106    /// Append-after-builtin contract: when a user handler is registered
1107    /// after a builtin, dispatch invokes the builtin first and the
1108    /// user handler second.  This is the security-load-bearing invariant —
1109    /// a builtin returning a non-`Continue` `NotifAction` must short-circuit
1110    /// before the user handler runs (covered by
1111    /// `dispatch_stops_at_first_non_continue`); when the builtin returns
1112    /// `Continue`, the user handler observes the post-builtin view.
1113    #[tokio::test]
1114    async fn dispatch_runs_builtin_before_extra() {
1115        let mut table = DispatchTable::new();
1116        let order = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
1117
1118        // Builtin first, tagged 'B'.
1119        let order_builtin = Arc::clone(&order);
1120        table.register(
1121            libc::SYS_openat,
1122            move |_cx: &HandlerCtx| {
1123                let order = Arc::clone(&order_builtin);
1124                async move {
1125                    order.lock().unwrap().push(b'B');
1126                    NotifAction::Continue
1127                }
1128            },
1129        );
1130
1131        // Extra after, tagged 'E'.  Registered after builtin to mirror
1132        // append-after-builtin placement from `build_dispatch_table`.
1133        let order_extra = Arc::clone(&order);
1134        table.register(
1135            libc::SYS_openat,
1136            move |_cx: &HandlerCtx| {
1137                let order = Arc::clone(&order_extra);
1138                async move {
1139                    order.lock().unwrap().push(b'E');
1140                    NotifAction::Continue
1141                }
1142            },
1143        );
1144
1145        let _ctx = fake_supervisor_ctx();
1146        let action = table
1147            .dispatch(fake_notif(libc::SYS_openat as i32), -1)
1148            .await;
1149
1150        assert!(matches!(action, NotifAction::Continue));
1151        let recorded = order.lock().unwrap();
1152        assert_eq!(
1153            *recorded,
1154            [b'B', b'E'],
1155            "builtin must run before extra (insertion order preserved)"
1156        );
1157    }
1158
1159    /// First non-`Continue` wins: a handler returning `Errno` short-circuits
1160    /// the chain, and subsequent handlers must not run.  This is the
1161    /// invariant that prevents a user-supplied extra from being observed
1162    /// (or, in the inverse direction, prevents an extra's `Errno` from
1163    /// being silently overridden by a later handler that happens to also
1164    /// be registered for the same syscall).
1165    #[tokio::test]
1166    async fn dispatch_stops_at_first_non_continue() {
1167        let mut table = DispatchTable::new();
1168        let calls = Arc::new(AtomicUsize::new(0));
1169
1170        // First handler — returns Errno, must terminate the chain.
1171        let calls_first = Arc::clone(&calls);
1172        table.register(
1173            libc::SYS_openat,
1174            move |_cx: &HandlerCtx| {
1175                let calls = Arc::clone(&calls_first);
1176                async move {
1177                    calls.fetch_add(1, Ordering::SeqCst);
1178                    NotifAction::Errno(libc::EACCES)
1179                }
1180            },
1181        );
1182
1183        // Second handler — must NOT be called.
1184        let calls_second = Arc::clone(&calls);
1185        table.register(
1186            libc::SYS_openat,
1187            move |_cx: &HandlerCtx| {
1188                let calls = Arc::clone(&calls_second);
1189                async move {
1190                    calls.fetch_add(1, Ordering::SeqCst);
1191                    NotifAction::Continue
1192                }
1193            },
1194        );
1195
1196        let _ctx = fake_supervisor_ctx();
1197        let action = table
1198            .dispatch(fake_notif(libc::SYS_openat as i32), -1)
1199            .await;
1200
1201        match action {
1202            NotifAction::Errno(e) => assert_eq!(e, libc::EACCES),
1203            other => panic!("expected Errno(EACCES), got {:?}", other),
1204        }
1205        assert_eq!(
1206            calls.load(Ordering::SeqCst),
1207            1,
1208            "second handler must not run after first returned non-Continue"
1209        );
1210    }
1211
1212    /// `validate_handler_syscalls_against_policy` must reject handlers whose
1213    /// syscall is in the policy's user-specified blocklist, with the same
1214    /// rationale as DEFAULT_BLOCKLIST: the BPF program emits notif JEQs before
1215    /// deny JEQs, so a user handler returning `Continue` would translate into
1216    /// `SECCOMP_USER_NOTIF_FLAG_CONTINUE` and silently bypass the kernel-level
1217    /// block.
1218    ///
1219    /// Uses `mremap` because it is in `syscall_name_to_nr` but not in
1220    /// `DEFAULT_BLOCKLIST_SYSCALLS` — putting it into `extra_deny_syscalls` is the only
1221    /// way it ends up on the extra blocklist, so the test isolates the user-supplied
1222    /// path of `blocklist_syscall_numbers` from the default branch covered by
1223    /// `handler_on_default_blocklist_syscall_is_rejected`.
1224    ///
1225    /// Pure-logic counterpart to the integration test of the same name —
1226    /// runs without a live sandbox so the contract is enforced even on
1227    /// hosts where seccomp integration tests are skipped.
1228    #[test]
1229    fn validate_extras_rejects_user_specified_blocklist() {
1230        let policy = crate::sandbox::Sandbox::builder()
1231            .extra_deny_syscalls(vec!["mremap".into()])
1232            .build()
1233            .expect("policy builds");
1234
1235        let result = validate_handler_syscalls_against_policy(&[libc::SYS_mremap], &policy);
1236        assert_eq!(
1237            result,
1238            Err(libc::SYS_mremap),
1239            "handler on user-specified blocklist must be rejected, naming the offending syscall"
1240        );
1241    }
1242
1243    // ---- Handler trait tests --------------------------------------
1244
1245    #[tokio::test]
1246    async fn handler_via_blanket_impl_dispatches_closures() {
1247        use std::sync::atomic::{AtomicU64, Ordering};
1248        let counter = Arc::new(AtomicU64::new(0));
1249        let counter_clone = Arc::clone(&counter);
1250
1251        let h = move |cx: &HandlerCtx| {
1252            let counter = Arc::clone(&counter_clone);
1253            async move {
1254                counter.fetch_add(1, Ordering::SeqCst);
1255                let _ = cx.notif.pid; // touch ctx so it's exercised
1256                NotifAction::Continue
1257            }
1258        };
1259
1260        let _sup = fake_supervisor_ctx();
1261        let notif = fake_notif(libc::SYS_openat as i32);
1262        let cx = HandlerCtx { notif, notif_fd: -1 };
1263
1264        let action = h.handle(&cx).await;
1265        assert!(matches!(action, NotifAction::Continue));
1266        assert_eq!(counter.load(Ordering::SeqCst), 1);
1267    }
1268
1269    /// Struct-based `Handler` registered through `DispatchTable::register`
1270    /// MUST be invoked when `dispatch()` walks the chain — and `&self`
1271    /// state MUST persist across notifications.  Bridges the gap between
1272    /// the trait-shape unit tests above (which call `.handle()` directly)
1273    /// and the dispatch ordering tests (which use closures via blanket
1274    /// impl).  Without this test, a regression where the dispatch walker
1275    /// dropped `Arc<dyn Handler>` calls but kept closures working would
1276    /// not be caught at the unit layer.
1277    #[tokio::test]
1278    async fn dispatch_invokes_struct_handler_with_persistent_self_state() {
1279        use std::sync::atomic::{AtomicU64, Ordering};
1280
1281        struct StructHandler {
1282            calls: AtomicU64,
1283        }
1284
1285        impl Handler for StructHandler {
1286            fn handle<'a>(
1287                &'a self,
1288                _cx: &'a HandlerCtx,
1289            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NotifAction> + Send + 'a>> {
1290                Box::pin(async move {
1291                    self.calls.fetch_add(1, Ordering::SeqCst);
1292                    NotifAction::Continue
1293                })
1294            }
1295        }
1296
1297        let mut table = DispatchTable::new();
1298        let handler = std::sync::Arc::new(StructHandler {
1299            calls: AtomicU64::new(0),
1300        });
1301        table.register_arc(libc::SYS_openat, handler.clone() as std::sync::Arc<dyn Handler>);
1302
1303        let _sup = fake_supervisor_ctx();
1304        let notif = fake_notif(libc::SYS_openat as i32);
1305
1306        // Three independent dispatches against the same registered handler.
1307        // Walker MUST hit the struct's handle() each time, accumulating
1308        // state on &self.calls.
1309        for _ in 0..3 {
1310            let action = table.dispatch(notif, -1).await;
1311            assert!(matches!(action, NotifAction::Continue));
1312        }
1313
1314        assert_eq!(
1315            handler.calls.load(Ordering::SeqCst),
1316            3,
1317            "dispatch must invoke the struct-based handler on every walk"
1318        );
1319    }
1320}