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 ctx_for_fork = Arc::clone(ctx);
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 ctx = Arc::clone(&ctx_for_fork);
285            async move {
286                crate::resource::handle_fork(&notif, notif_fd, &ctx, &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_destination_policy || has_unix_fs_gate;
328    // the latter traps connect() to gate named unix sockets). has_http_acl is
329    // already subsumed by has_net_destination_policy, so it isn't listed.
330    // ------------------------------------------------------------------
331    if policy.has_net_destination_policy || policy.has_unix_fs_gate {
332        for &nr in &[
333            libc::SYS_connect,
334            libc::SYS_sendto,
335            libc::SYS_sendmsg,
336            libc::SYS_sendmmsg,
337        ] {
338            let __sup = Arc::clone(ctx);
339            table.register(nr, move |cx: &HandlerCtx| {
340                let notif = cx.notif;
341                let sup = Arc::clone(&__sup);
342                let notif_fd = cx.notif_fd;
343                async move {
344                    crate::network::handle_net(&notif, &sup, notif_fd).await
345                }
346            });
347        }
348    }
349
350    // ------------------------------------------------------------------
351    // Deterministic random — getrandom()
352    // ------------------------------------------------------------------
353    if policy.has_random_seed {
354        let __sup = Arc::clone(ctx);
355        table.register(libc::SYS_getrandom, move |cx: &HandlerCtx| {
356            let notif = cx.notif;
357            let sup = Arc::clone(&__sup);
358            let notif_fd = cx.notif_fd;
359            async move {
360                let mut tr = sup.time_random.lock().await;
361                if let Some(ref mut rng) = tr.random_state {
362                    crate::random::handle_getrandom(&notif, rng, notif_fd)
363                } else {
364                    NotifAction::Continue
365                }
366            }
367        });
368    }
369
370    // ------------------------------------------------------------------
371    // Deterministic random — /dev/urandom and /dev/random opens.
372    // Registered for every open-family syscall so dirfd-relative and
373    // legacy `open` spellings can't slip past the seed and read the
374    // kernel's real entropy.
375    // ------------------------------------------------------------------
376    if policy.has_random_seed {
377        for nr in open_family_syscalls() {
378            let __sup = Arc::clone(ctx);
379            let policy_rand = Arc::clone(policy);
380            table.register(nr, move |cx: &HandlerCtx| {
381                let notif = cx.notif;
382                let sup = Arc::clone(&__sup);
383                let policy = Arc::clone(&policy_rand);
384                let notif_fd = cx.notif_fd;
385                async move {
386                    let mut tr = sup.time_random.lock().await;
387                    if let Some(ref mut rng) = tr.random_state {
388                        if let Some(action) = crate::random::handle_random_open(
389                            &notif, rng, notif_fd,
390                            policy.chroot_root.as_deref(), &policy.chroot_mounts,
391                        ) {
392                            return action;
393                        }
394                    }
395                    NotifAction::Continue
396                }
397            });
398        }
399    }
400
401    // ------------------------------------------------------------------
402    // Timer adjustment (conditional on has_time_start)
403    // ------------------------------------------------------------------
404    if policy.has_time_start {
405        let time_offset = policy.time_offset;
406        for &nr in &[
407            libc::SYS_clock_nanosleep as i64,
408            libc::SYS_timerfd_settime as i64,
409            libc::SYS_timer_settime as i64,
410        ] {
411            table.register(nr, move |cx: &HandlerCtx| {
412                let notif = cx.notif;
413                let notif_fd = cx.notif_fd;
414                async move {
415                    crate::time::handle_timer(&notif, time_offset, notif_fd)
416                }
417            });
418        }
419    }
420
421    // ------------------------------------------------------------------
422    // /etc/hosts virtualization: always on. The synthetic file contains
423    // the loopback base (or, in chroot/image mode, the image's own
424    // `/etc/hosts` merged with a loopback fallback) plus any concrete
425    // hostnames resolved from `net_allow`, so the host's on-disk
426    // `/etc/hosts` never leaks in and image-baked entries are preserved.
427    //
428    // Registered for every open-family syscall (see `open_family_syscalls`).
429    // Must run *before* the chroot handler so that in chroot mode the
430    // synthetic memfd wins over a direct open of `<chroot>/etc/hosts` —
431    // the chroot handler always intercepts opens within the chroot and
432    // would otherwise serve the raw image file, defeating the merge.
433    // ------------------------------------------------------------------
434    {
435        let etc_hosts = policy.virtual_etc_hosts.clone();
436        for nr in open_family_syscalls() {
437            let etc_hosts = etc_hosts.clone();
438            let policy_hosts = Arc::clone(policy);
439            table.register(nr, move |cx: &HandlerCtx| {
440                let notif = cx.notif;
441                let notif_fd = cx.notif_fd;
442                let etc_hosts = etc_hosts.clone();
443                let policy = Arc::clone(&policy_hosts);
444                async move {
445                    if let Some(action) = crate::procfs::handle_etc_hosts_open(
446                        &notif, &etc_hosts, notif_fd,
447                        policy.chroot_root.as_deref(), &policy.chroot_mounts,
448                    ) {
449                        action
450                    } else {
451                        NotifAction::Continue
452                    }
453                }
454            });
455        }
456    }
457
458    // ------------------------------------------------------------------
459    // CA injection: splice the active MITM CA into user-declared trust
460    // bundles. Registered before chroot/COW so the substituted memfd wins
461    // over a real open of the bundle file. Only active when MITM is on and
462    // the user declared at least one --http-inject-ca path.
463    // ------------------------------------------------------------------
464    if let Some(ca_pem) = policy.ca_inject_pem.clone() {
465        if !policy.ca_inject_paths.is_empty() {
466            let inject_paths = std::sync::Arc::new(policy.ca_inject_paths.clone());
467            for nr in open_family_syscalls() {
468                let ca_pem = std::sync::Arc::clone(&ca_pem);
469                let inject_paths = std::sync::Arc::clone(&inject_paths);
470                let policy_ca = Arc::clone(policy);
471                table.register(nr, move |cx: &HandlerCtx| {
472                    let notif = cx.notif;
473                    let notif_fd = cx.notif_fd;
474                    let ca_pem = std::sync::Arc::clone(&ca_pem);
475                    let inject_paths = std::sync::Arc::clone(&inject_paths);
476                    let policy = Arc::clone(&policy_ca);
477                    async move {
478                        crate::ca_inject::handle_ca_inject_open(
479                            &notif, &inject_paths, &ca_pem, notif_fd,
480                            policy.chroot_root.as_deref(), &policy.chroot_mounts,
481                        )
482                        .unwrap_or(NotifAction::Continue)
483                    }
484                });
485            }
486        }
487    }
488
489    // ------------------------------------------------------------------
490    // /proc file virtualization (always on). Registered BEFORE chroot/COW so a
491    // synthesized /proc memfd wins over a real open of <chroot>/proc/* (the
492    // empty rootfs procfs) — mirroring the /etc/hosts and CA handlers above.
493    // The handler also does sensitive-path blocking and per-PID filtering
494    // (security boundaries), so it must run first and catch every open-family
495    // spelling. NOTE: only the open-family handler moves here; the getdents
496    // directory-listing handler stays after chroot (below) so a chrooted /proc
497    // dir fd lists the empty rootfs procfs rather than the host's entries.
498    // ------------------------------------------------------------------
499    for nr in open_family_syscalls() {
500        let policy_for_proc_open = Arc::clone(policy);
501        let resource_for_proc_open = Arc::clone(resource);
502        let __sup = Arc::clone(ctx);
503        table.register(nr, move |cx: &HandlerCtx| {
504            let notif = cx.notif;
505            let sup = Arc::clone(&__sup);
506            let notif_fd = cx.notif_fd;
507            let policy = Arc::clone(&policy_for_proc_open);
508            let resource = Arc::clone(&resource_for_proc_open);
509            async move {
510                let processes = Arc::clone(&sup.processes);
511                let network = Arc::clone(&sup.network);
512                crate::procfs::handle_proc_open(&notif, &processes, &resource, &network, &policy, notif_fd).await
513            }
514        });
515    }
516
517    // ------------------------------------------------------------------
518    // Chroot path interception (before COW)
519    // ------------------------------------------------------------------
520    if policy.chroot_root.is_some() {
521        register_chroot_handlers(&mut table, policy, ctx);
522    }
523
524    // ------------------------------------------------------------------
525    // COW filesystem interception
526    // ------------------------------------------------------------------
527    if policy.cow_enabled {
528        register_cow_handlers(&mut table, ctx);
529    }
530
531    // ------------------------------------------------------------------
532    // /proc directory-listing PID filter. Stays after chroot/COW: under chroot
533    // the /proc dir fd resolves to the empty rootfs procfs, so the listing is
534    // empty rather than leaking the host's /proc entry names (which are not all
535    // synthesized and would dangle on access).
536    // ------------------------------------------------------------------
537    let mut getdents_nrs = vec![libc::SYS_getdents64];
538    if let Some(getdents) = arch::sys_getdents() {
539        getdents_nrs.push(getdents);
540    }
541    for nr in getdents_nrs {
542        let policy_for_getdents = Arc::clone(policy);
543        let __sup = Arc::clone(ctx);
544        table.register(nr, move |cx: &HandlerCtx| {
545            let notif = cx.notif;
546            let sup = Arc::clone(&__sup);
547            let notif_fd = cx.notif_fd;
548            let policy = Arc::clone(&policy_for_getdents);
549            async move {
550                let processes = Arc::clone(&sup.processes);
551                crate::procfs::handle_getdents(&notif, &processes, &policy, notif_fd).await
552            }
553        });
554    }
555
556    // ------------------------------------------------------------------
557    // Virtual CPU count
558    // ------------------------------------------------------------------
559    if let Some(n) = policy.num_cpus {
560        table.register(libc::SYS_sched_getaffinity, move |cx: &HandlerCtx| {
561            let notif = cx.notif;
562            let notif_fd = cx.notif_fd;
563            async move {
564                crate::procfs::handle_sched_getaffinity(&notif, n, notif_fd)
565            }
566        });
567    }
568
569    // ------------------------------------------------------------------
570    // Hostname virtualization. The `/etc/hostname` shim is registered
571    // for every open-family syscall so dirfd-relative and legacy `open`
572    // spellings can't leak the host's real hostname.
573    // ------------------------------------------------------------------
574    if let Some(ref hostname) = policy.virtual_hostname {
575        let hostname_for_uname = hostname.clone();
576        let hostname_for_open = hostname.clone();
577        table.register(libc::SYS_uname, move |cx: &HandlerCtx| {
578            let notif = cx.notif;
579            let notif_fd = cx.notif_fd;
580            let hostname = hostname_for_uname.clone();
581            async move {
582                crate::procfs::handle_uname(&notif, &hostname, notif_fd)
583            }
584        });
585        for nr in open_family_syscalls() {
586            let hostname = hostname_for_open.clone();
587            let policy_hostname = Arc::clone(policy);
588            table.register(nr, move |cx: &HandlerCtx| {
589                let notif = cx.notif;
590                let notif_fd = cx.notif_fd;
591                let hostname = hostname.clone();
592                let policy = Arc::clone(&policy_hostname);
593                async move {
594                    if let Some(action) = crate::procfs::handle_hostname_open(
595                        &notif, &hostname, notif_fd,
596                        policy.chroot_root.as_deref(), &policy.chroot_mounts,
597                    ) {
598                        action
599                    } else {
600                        NotifAction::Continue
601                    }
602                }
603            });
604        }
605    }
606
607    // /etc/hosts is registered above the chroot block — see the comment there.
608
609    // ------------------------------------------------------------------
610    // Deterministic directory listing
611    // ------------------------------------------------------------------
612    if policy.deterministic_dirs {
613        let mut getdents_nrs = vec![libc::SYS_getdents64];
614        if let Some(getdents) = arch::sys_getdents() {
615            getdents_nrs.push(getdents);
616        }
617        for nr in getdents_nrs {
618            let __sup = Arc::clone(ctx);
619            table.register(nr, move |cx: &HandlerCtx| {
620                let notif = cx.notif;
621                let sup = Arc::clone(&__sup);
622                let notif_fd = cx.notif_fd;
623                async move {
624                    let processes = Arc::clone(&sup.processes);
625                    crate::procfs::handle_sorted_getdents(&notif, &processes, notif_fd).await
626                }
627            });
628        }
629    }
630
631    // ------------------------------------------------------------------
632    // NETLINK_ROUTE virtualization (always on).
633    //
634    // Send/recv traffic flows through a `socketpair(AF_UNIX,
635    // SOCK_SEQPACKET)` whose supervisor-side end is driven by a tokio
636    // task spawned in `handle_socket`.  Only `socket`, `bind`,
637    // `getsockname`, `recvmsg`/`recvfrom`, and `close` need supervisor
638    // intercepts; send uses the kernel directly.
639    //
640    // Must register before `port_remap` so the netlink `bind` handler
641    // runs first and returns `Continue` for non-cookie fds.
642    // ------------------------------------------------------------------
643    {
644        let __sup = Arc::clone(ctx);
645        table.register(libc::SYS_socket, move |cx: &HandlerCtx| {
646            let notif = cx.notif;
647            let sup = Arc::clone(&__sup);
648            async move {
649                let state = Arc::clone(&sup.netlink);
650                crate::netlink::handlers::handle_socket(&notif, &state).await
651            }
652        });
653        let __sup = Arc::clone(ctx);
654        table.register(libc::SYS_bind, move |cx: &HandlerCtx| {
655            let notif = cx.notif;
656            let sup = Arc::clone(&__sup);
657            async move {
658                let state = Arc::clone(&sup.netlink);
659                crate::netlink::handlers::handle_bind(&notif, &state).await
660            }
661        });
662        let __sup = Arc::clone(ctx);
663        table.register(libc::SYS_getsockname, move |cx: &HandlerCtx| {
664            let notif = cx.notif;
665            let sup = Arc::clone(&__sup);
666            let notif_fd = cx.notif_fd;
667            async move {
668                let state = Arc::clone(&sup.netlink);
669                crate::netlink::handlers::handle_getsockname(&notif, &state, notif_fd).await
670            }
671        });
672        // Zero the msg_name region on recv so glibc sees nl_pid=0
673        // (the kernel only writes sun_family on unix socketpair recvmsg,
674        //  leaving the rest of the buffer as stack garbage otherwise).
675        for &nr in &[libc::SYS_recvfrom, libc::SYS_recvmsg] {
676            let __sup = Arc::clone(ctx);
677            table.register(nr, move |cx: &HandlerCtx| {
678                let notif = cx.notif;
679                let sup = Arc::clone(&__sup);
680                let notif_fd = cx.notif_fd;
681                async move {
682                    let state = Arc::clone(&sup.netlink);
683                    crate::netlink::handlers::handle_netlink_recvmsg(&notif, &state, notif_fd).await
684                }
685            });
686        }
687        // Unregister on close so the (pid, fd) slot isn't left in the
688        // cookie set once the child reuses the fd for something else.
689        let __sup = Arc::clone(ctx);
690        table.register(libc::SYS_close, move |cx: &HandlerCtx| {
691            let notif = cx.notif;
692            let sup = Arc::clone(&__sup);
693            async move {
694                let state = Arc::clone(&sup.netlink);
695                crate::netlink::handlers::handle_close(&notif, &state).await
696            }
697        });
698    }
699
700    // ------------------------------------------------------------------
701    // Bind — on-behalf
702    // ------------------------------------------------------------------
703    if policy.port_remap || policy.has_net_destination_policy || policy.has_bind_denylist {
704        let __sup = Arc::clone(ctx);
705        table.register(libc::SYS_bind, move |cx: &HandlerCtx| {
706            let notif = cx.notif;
707            let sup = Arc::clone(&__sup);
708            let notif_fd = cx.notif_fd;
709            async move {
710                crate::port_remap::handle_bind(&notif, &sup.network, notif_fd).await
711            }
712        });
713    }
714
715    // ------------------------------------------------------------------
716    // getsockname — port remap
717    // ------------------------------------------------------------------
718    if policy.port_remap {
719        let __sup = Arc::clone(ctx);
720        table.register(libc::SYS_getsockname, move |cx: &HandlerCtx| {
721            let notif = cx.notif;
722            let sup = Arc::clone(&__sup);
723            let notif_fd = cx.notif_fd;
724            async move {
725                crate::port_remap::handle_getsockname(&notif, &sup.network, notif_fd).await
726            }
727        });
728    }
729
730    // ------------------------------------------------------------------
731    // Pending user handlers — appended after builtins so builtin handlers
732    // keep their security-critical priority (chroot path normalization,
733    // COW writes, resource accounting).
734    // ------------------------------------------------------------------
735    for (nr, h) in pending_handlers {
736        table.register_arc(nr, h);
737    }
738
739    table
740}
741
742// ============================================================
743// Chroot handler registration
744// ============================================================
745
746fn register_chroot_handlers(
747    table: &mut DispatchTable,
748    policy: &Arc<NotifPolicy>,
749    ctx: &Arc<SupervisorCtx>,
750) {
751    use crate::chroot::dispatch::ChrootCtx;
752
753    // Helper macro — produces a closure satisfying Handler via blanket impl.
754    // The closure clones `policy` (Arc) before the async block; inside the
755    // async block it borrows fields of that cloned Arc to build `ChrootCtx`.
756    macro_rules! chroot_handler {
757        ($policy:expr, $handler:expr) => {{
758            let policy = Arc::clone($policy);
759            let chroot_state = Arc::clone(&ctx.chroot);
760            let cow_state = Arc::clone(&ctx.cow);
761            move |cx: &HandlerCtx| {
762                let notif = cx.notif;
763                let chroot_state = Arc::clone(&chroot_state);
764                let cow_state = Arc::clone(&cow_state);
765                let notif_fd = cx.notif_fd;
766                let policy = Arc::clone(&policy);
767                async move {
768                    let chroot_ctx = ChrootCtx {
769                        root: policy.chroot_root.as_ref().unwrap(),
770                        readable: &policy.chroot_readable,
771                        writable: &policy.chroot_writable,
772                        denied: &policy.chroot_denied,
773                        mounts: &policy.chroot_mounts,
774                        mount_ro: &policy.chroot_mount_ro,
775                    };
776                    $handler(&notif, &chroot_state, &cow_state, notif_fd, &chroot_ctx).await
777                }
778            }
779        }};
780    }
781
782    // Same shape for fall-through variants (semantically identical here;
783    // kept separate for symmetry with the old code).
784    macro_rules! chroot_handler_fallthrough {
785        ($policy:expr, $handler:expr) => {{
786            let policy = Arc::clone($policy);
787            let chroot_state = Arc::clone(&ctx.chroot);
788            let cow_state = Arc::clone(&ctx.cow);
789            move |cx: &HandlerCtx| {
790                let notif = cx.notif;
791                let chroot_state = Arc::clone(&chroot_state);
792                let cow_state = Arc::clone(&cow_state);
793                let notif_fd = cx.notif_fd;
794                let policy = Arc::clone(&policy);
795                async move {
796                    let chroot_ctx = ChrootCtx {
797                        root: policy.chroot_root.as_ref().unwrap(),
798                        readable: &policy.chroot_readable,
799                        writable: &policy.chroot_writable,
800                        denied: &policy.chroot_denied,
801                        mounts: &policy.chroot_mounts,
802                        mount_ro: &policy.chroot_mount_ro,
803                    };
804                    $handler(&notif, &chroot_state, &cow_state, notif_fd, &chroot_ctx).await
805                }
806            }
807        }};
808    }
809
810    // openat — fallthrough if Continue
811    table.register(libc::SYS_openat, chroot_handler_fallthrough!(policy,
812        crate::chroot::dispatch::handle_chroot_open));
813
814    // open (legacy) — fallthrough if Continue
815    if let Some(open) = arch::sys_open() {
816        table.register(open, chroot_handler_fallthrough!(policy,
817            crate::chroot::dispatch::handle_chroot_legacy_open));
818    }
819
820    // execve, execveat — unconditional return
821    for &nr in &[libc::SYS_execve, libc::SYS_execveat] {
822        table.register(nr, chroot_handler!(policy,
823            crate::chroot::dispatch::handle_chroot_exec));
824    }
825
826    // Modern write syscalls
827    for &nr in &[
828        libc::SYS_unlinkat, libc::SYS_mkdirat, libc::SYS_renameat2,
829        libc::SYS_symlinkat, libc::SYS_linkat, libc::SYS_fchmodat,
830        libc::SYS_fchownat, libc::SYS_truncate,
831    ] {
832        table.register(nr, chroot_handler!(policy,
833            crate::chroot::dispatch::handle_chroot_write));
834    }
835
836    // Legacy write syscalls
837    if let Some(nr) = arch::sys_unlink() {
838        table.register(nr, chroot_handler!(policy,
839            crate::chroot::dispatch::handle_chroot_legacy_unlink));
840    }
841    if let Some(nr) = arch::sys_rmdir() {
842        table.register(nr, chroot_handler!(policy,
843            crate::chroot::dispatch::handle_chroot_legacy_rmdir));
844    }
845    if let Some(nr) = arch::sys_mkdir() {
846        table.register(nr, chroot_handler!(policy,
847            crate::chroot::dispatch::handle_chroot_legacy_mkdir));
848    }
849    if let Some(nr) = arch::sys_rename() {
850        table.register(nr, chroot_handler!(policy,
851            crate::chroot::dispatch::handle_chroot_legacy_rename));
852    }
853    if let Some(nr) = arch::sys_symlink() {
854        table.register(nr, chroot_handler!(policy,
855            crate::chroot::dispatch::handle_chroot_legacy_symlink));
856    }
857    if let Some(nr) = arch::sys_link() {
858        table.register(nr, chroot_handler!(policy,
859            crate::chroot::dispatch::handle_chroot_legacy_link));
860    }
861    if let Some(nr) = arch::sys_chmod() {
862        table.register(nr, chroot_handler!(policy,
863            crate::chroot::dispatch::handle_chroot_legacy_chmod));
864    }
865
866    // chown — non-follow
867    if let Some(chown) = arch::sys_chown() {
868        let policy_for_chown = Arc::clone(policy);
869        let __sup = Arc::clone(ctx);
870        table.register(chown, move |cx: &HandlerCtx| {
871            let notif = cx.notif;
872            let sup = Arc::clone(&__sup);
873            let notif_fd = cx.notif_fd;
874            let policy = Arc::clone(&policy_for_chown);
875            async move {
876                let chroot_ctx = ChrootCtx {
877                    root: policy.chroot_root.as_ref().unwrap(),
878                    readable: &policy.chroot_readable,
879                    writable: &policy.chroot_writable,
880                    denied: &policy.chroot_denied,
881                    mounts: &policy.chroot_mounts,
882                    mount_ro: &policy.chroot_mount_ro,
883                };
884                crate::chroot::dispatch::handle_chroot_legacy_chown(&notif, &sup.chroot, &sup.cow, notif_fd, &chroot_ctx, false).await
885            }
886        });
887    }
888
889    // lchown — follow
890    if let Some(lchown) = arch::sys_lchown() {
891        let policy_for_lchown = Arc::clone(policy);
892        let __sup = Arc::clone(ctx);
893        table.register(lchown, move |cx: &HandlerCtx| {
894            let notif = cx.notif;
895            let sup = Arc::clone(&__sup);
896            let notif_fd = cx.notif_fd;
897            let policy = Arc::clone(&policy_for_lchown);
898            async move {
899                let chroot_ctx = ChrootCtx {
900                    root: policy.chroot_root.as_ref().unwrap(),
901                    readable: &policy.chroot_readable,
902                    writable: &policy.chroot_writable,
903                    denied: &policy.chroot_denied,
904                    mounts: &policy.chroot_mounts,
905                    mount_ro: &policy.chroot_mount_ro,
906                };
907                crate::chroot::dispatch::handle_chroot_legacy_chown(&notif, &sup.chroot, &sup.cow, notif_fd, &chroot_ctx, true).await
908            }
909        });
910    }
911
912    // stat family
913    for &nr in &[
914        libc::SYS_newfstatat,
915        libc::SYS_faccessat,
916        arch::SYS_FACCESSAT2,
917    ] {
918        table.register(nr, chroot_handler!(policy,
919            crate::chroot::dispatch::handle_chroot_stat));
920    }
921
922    // Legacy stat
923    if let Some(nr) = arch::sys_stat() {
924        table.register(nr, chroot_handler!(policy,
925            crate::chroot::dispatch::handle_chroot_legacy_stat));
926    }
927    if let Some(nr) = arch::sys_lstat() {
928        table.register(nr, chroot_handler!(policy,
929            crate::chroot::dispatch::handle_chroot_legacy_lstat));
930    }
931    if let Some(nr) = arch::sys_access() {
932        table.register(nr, chroot_handler!(policy,
933            crate::chroot::dispatch::handle_chroot_legacy_access));
934    }
935
936    // statx
937    table.register(libc::SYS_statx, chroot_handler!(policy,
938        crate::chroot::dispatch::handle_chroot_statx));
939
940    // readlink
941    table.register(libc::SYS_readlinkat, chroot_handler!(policy,
942        crate::chroot::dispatch::handle_chroot_readlink));
943    if let Some(nr) = arch::sys_readlink() {
944        table.register(nr, chroot_handler!(policy,
945            crate::chroot::dispatch::handle_chroot_legacy_readlink));
946    }
947
948    // getdents
949    let mut getdents_nrs = vec![libc::SYS_getdents64];
950    if let Some(getdents) = arch::sys_getdents() {
951        getdents_nrs.push(getdents);
952    }
953    for nr in getdents_nrs {
954        table.register(nr, chroot_handler!(policy,
955            crate::chroot::dispatch::handle_chroot_getdents));
956    }
957
958    // chdir, getcwd, statfs, utimensat
959    table.register(libc::SYS_chdir as i64, chroot_handler!(policy,
960        crate::chroot::dispatch::handle_chroot_chdir));
961    table.register(libc::SYS_getcwd as i64, chroot_handler!(policy,
962        crate::chroot::dispatch::handle_chroot_getcwd));
963    table.register(libc::SYS_statfs as i64, chroot_handler!(policy,
964        crate::chroot::dispatch::handle_chroot_statfs));
965    table.register(libc::SYS_utimensat as i64, chroot_handler!(policy,
966        crate::chroot::dispatch::handle_chroot_utimensat));
967
968    // xattr family (path-based) — get/set/list/remove and their l* variants
969    for &nr in &[
970        libc::SYS_getxattr, libc::SYS_lgetxattr,
971        libc::SYS_setxattr, libc::SYS_lsetxattr,
972        libc::SYS_listxattr, libc::SYS_llistxattr,
973        libc::SYS_removexattr, libc::SYS_lremovexattr,
974    ] {
975        table.register(nr, chroot_handler!(policy,
976            crate::chroot::dispatch::handle_chroot_xattr));
977    }
978}
979
980// ============================================================
981// COW handler registration
982// ============================================================
983
984fn register_cow_handlers(table: &mut DispatchTable, ctx: &Arc<SupervisorCtx>) {
985    // Helper that captures `ctx.cow` and `ctx.processes` once at table-build
986    // time, then re-clones the per-handler `Arc`s on each invocation.
987    macro_rules! cow_call {
988        ($handler:expr) => {{
989            let cow_state = Arc::clone(&ctx.cow);
990            let processes_state = Arc::clone(&ctx.processes);
991            move |cx: &HandlerCtx| {
992                let notif = cx.notif;
993                let cow_state = Arc::clone(&cow_state);
994                let processes_state = Arc::clone(&processes_state);
995                let notif_fd = cx.notif_fd;
996                async move {
997                    $handler(&notif, &cow_state, &processes_state, notif_fd).await
998                }
999            }
1000        }};
1001    }
1002
1003    // Write syscalls (*at variants + legacy)
1004    let mut write_nrs = vec![
1005        libc::SYS_unlinkat, libc::SYS_mkdirat, libc::SYS_renameat2,
1006        libc::SYS_symlinkat, libc::SYS_linkat, libc::SYS_fchmodat,
1007        libc::SYS_fchownat, libc::SYS_truncate,
1008    ];
1009    write_nrs.extend([
1010        arch::sys_unlink(), arch::sys_rmdir(), arch::sys_mkdir(), arch::sys_rename(),
1011        arch::sys_symlink(), arch::sys_link(), arch::sys_chmod(), arch::sys_chown(),
1012        arch::sys_lchown(),
1013    ].into_iter().flatten());
1014    for nr in write_nrs {
1015        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_write));
1016    }
1017
1018    table.register(libc::SYS_utimensat, cow_call!(crate::cow::dispatch::handle_cow_utimensat));
1019
1020    let mut access_nrs = vec![libc::SYS_faccessat, arch::SYS_FACCESSAT2];
1021    access_nrs.extend(arch::sys_access());
1022    for nr in access_nrs {
1023        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_access));
1024    }
1025
1026    let mut open_nrs = vec![libc::SYS_openat];
1027    open_nrs.extend(arch::sys_open());
1028    for nr in open_nrs {
1029        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_open));
1030    }
1031
1032    let mut stat_nrs = vec![libc::SYS_newfstatat, libc::SYS_faccessat];
1033    stat_nrs.extend([arch::sys_stat(), arch::sys_lstat(), arch::sys_access()].into_iter().flatten());
1034    for nr in stat_nrs {
1035        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_stat));
1036    }
1037
1038    table.register(libc::SYS_statx, cow_call!(crate::cow::dispatch::handle_cow_statx));
1039
1040    let mut readlink_nrs = vec![libc::SYS_readlinkat];
1041    readlink_nrs.extend(arch::sys_readlink());
1042    for nr in readlink_nrs {
1043        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_readlink));
1044    }
1045
1046    let mut getdents_nrs = vec![libc::SYS_getdents64];
1047    getdents_nrs.extend(arch::sys_getdents());
1048    for nr in getdents_nrs {
1049        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_getdents));
1050    }
1051
1052    table.register(libc::SYS_chdir, cow_call!(crate::cow::dispatch::handle_cow_chdir));
1053    table.register(libc::SYS_getcwd, cow_call!(crate::cow::dispatch::handle_cow_getcwd));
1054
1055    for &nr in &[libc::SYS_execve, libc::SYS_execveat] {
1056        table.register(nr, cow_call!(crate::cow::dispatch::handle_cow_exec));
1057    }
1058}
1059
1060// ============================================================
1061// Tests
1062// ============================================================
1063
1064#[cfg(test)]
1065mod handler_tests {
1066    //! Unit tests for the user-supplied handler extension API.
1067    //!
1068    //! Drive the actual `DispatchTable::dispatch` walker against a minimal
1069    //! `SupervisorCtx` constructed from default-state pieces.  Handler
1070    //! closures here ignore the context (no notif fd, no real child), so
1071    //! the dispatch invariants under test (registration order, chain
1072    //! short-circuit on first non-`Continue`, append-after-builtin
1073    //! placement) are exercised end-to-end without needing a live
1074    //! Landlock+seccomp sandbox — those scenarios live under
1075    //! `crates/sandlock-core/tests/integration/test_handlers.rs`.
1076    use super::*;
1077    use crate::netlink::NetlinkState;
1078    use crate::seccomp::ctx::SupervisorCtx;
1079    use crate::seccomp::notif::NotifPolicy;
1080    use crate::seccomp::state::{
1081        ChrootState, CowState, NetworkState, PolicyFnState, ProcessIndex, ProcfsState,
1082        ResourceState, TimeRandomState,
1083    };
1084    use crate::sys::structs::{SeccompData, SeccompNotif};
1085    use std::sync::atomic::{AtomicUsize, Ordering};
1086
1087    fn fake_notif(nr: i32) -> SeccompNotif {
1088        SeccompNotif {
1089            id: 0,
1090            pid: 1,
1091            flags: 0,
1092            data: SeccompData {
1093                nr,
1094                arch: 0,
1095                instruction_pointer: 0,
1096                args: [0; 6],
1097            },
1098        }
1099    }
1100
1101    /// Minimal `SupervisorCtx` for unit tests.  Every field is built from
1102    /// the corresponding state's `new()`/default constructor — no syscalls,
1103    /// no fds, no spawned children.  Handlers in these tests do not
1104    /// actually inspect the context, so the values do not need to match
1105    /// any real run; they only need to satisfy the type signature so we
1106    /// can call `dispatch()`.
1107    fn fake_supervisor_ctx() -> Arc<SupervisorCtx> {
1108        Arc::new(SupervisorCtx {
1109            resource: Arc::new(Mutex::new(ResourceState::new(0, 0))),
1110            cow: Arc::new(Mutex::new(CowState::new())),
1111            procfs: Arc::new(Mutex::new(ProcfsState::new())),
1112            network: Arc::new(Mutex::new(NetworkState::new())),
1113            time_random: Arc::new(Mutex::new(TimeRandomState::new(None, None))),
1114            policy_fn: Arc::new(Mutex::new(PolicyFnState::new())),
1115            chroot: Arc::new(Mutex::new(ChrootState::new())),
1116            netlink: Arc::new(NetlinkState::new()),
1117            processes: Arc::new(ProcessIndex::new()),
1118            policy: Arc::new(NotifPolicy {
1119                max_memory_bytes: 0,
1120                max_processes: 0,
1121                has_memory_limit: false,
1122                has_net_destination_policy: false,
1123                has_bind_denylist: false,
1124                has_unix_fs_gate: false,
1125                has_random_seed: false,
1126                has_time_start: false,
1127                time_offset: 0,
1128                num_cpus: None,
1129                argv_safety_required: false,
1130                port_remap: false,
1131                cow_enabled: false,
1132                chroot_root: None,
1133                chroot_readable: Vec::new(),
1134                chroot_writable: Vec::new(),
1135                chroot_denied: Vec::new(),
1136                chroot_mounts: Vec::new(),
1137                chroot_mount_ro: Vec::new(),
1138                deterministic_dirs: false,
1139                virtual_hostname: None,
1140                has_http_acl: false,
1141                virtual_etc_hosts: String::new(),
1142                ca_inject_paths: Vec::new(),
1143                ca_inject_pem: None,
1144            }),
1145            child_pidfd: None,
1146            notif_fd: -1,
1147        })
1148    }
1149
1150    /// All registered handlers run, in registration order, when each
1151    /// returns `Continue`.  Verifies that `register` appends to the
1152    /// underlying `Vec` and that `dispatch` walks it front-to-back.
1153    #[tokio::test]
1154    async fn dispatch_walks_chain_in_registration_order() {
1155        let mut table = DispatchTable::new();
1156        let order = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
1157
1158        for tag in [1u8, 2u8, 3u8] {
1159            let order_clone = Arc::clone(&order);
1160            table.register(
1161                libc::SYS_openat,
1162                move |_cx: &HandlerCtx| {
1163                    let order = Arc::clone(&order_clone);
1164                    async move {
1165                        order.lock().unwrap().push(tag);
1166                        NotifAction::Continue
1167                    }
1168                },
1169            );
1170        }
1171
1172        let _ctx = fake_supervisor_ctx();
1173        let action = table
1174            .dispatch(fake_notif(libc::SYS_openat as i32), -1)
1175            .await;
1176
1177        assert!(matches!(action, NotifAction::Continue));
1178        let recorded = order.lock().unwrap();
1179        assert_eq!(
1180            *recorded,
1181            [1u8, 2u8, 3u8],
1182            "every handler must run, in the order it was registered"
1183        );
1184    }
1185
1186    /// Append-after-builtin contract: when a user handler is registered
1187    /// after a builtin, dispatch invokes the builtin first and the
1188    /// user handler second.  This is the security-load-bearing invariant —
1189    /// a builtin returning a non-`Continue` `NotifAction` must short-circuit
1190    /// before the user handler runs (covered by
1191    /// `dispatch_stops_at_first_non_continue`); when the builtin returns
1192    /// `Continue`, the user handler observes the post-builtin view.
1193    #[tokio::test]
1194    async fn dispatch_runs_builtin_before_extra() {
1195        let mut table = DispatchTable::new();
1196        let order = Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
1197
1198        // Builtin first, tagged 'B'.
1199        let order_builtin = Arc::clone(&order);
1200        table.register(
1201            libc::SYS_openat,
1202            move |_cx: &HandlerCtx| {
1203                let order = Arc::clone(&order_builtin);
1204                async move {
1205                    order.lock().unwrap().push(b'B');
1206                    NotifAction::Continue
1207                }
1208            },
1209        );
1210
1211        // Extra after, tagged 'E'.  Registered after builtin to mirror
1212        // append-after-builtin placement from `build_dispatch_table`.
1213        let order_extra = Arc::clone(&order);
1214        table.register(
1215            libc::SYS_openat,
1216            move |_cx: &HandlerCtx| {
1217                let order = Arc::clone(&order_extra);
1218                async move {
1219                    order.lock().unwrap().push(b'E');
1220                    NotifAction::Continue
1221                }
1222            },
1223        );
1224
1225        let _ctx = fake_supervisor_ctx();
1226        let action = table
1227            .dispatch(fake_notif(libc::SYS_openat as i32), -1)
1228            .await;
1229
1230        assert!(matches!(action, NotifAction::Continue));
1231        let recorded = order.lock().unwrap();
1232        assert_eq!(
1233            *recorded,
1234            [b'B', b'E'],
1235            "builtin must run before extra (insertion order preserved)"
1236        );
1237    }
1238
1239    /// First non-`Continue` wins: a handler returning `Errno` short-circuits
1240    /// the chain, and subsequent handlers must not run.  This is the
1241    /// invariant that prevents a user-supplied extra from being observed
1242    /// (or, in the inverse direction, prevents an extra's `Errno` from
1243    /// being silently overridden by a later handler that happens to also
1244    /// be registered for the same syscall).
1245    #[tokio::test]
1246    async fn dispatch_stops_at_first_non_continue() {
1247        let mut table = DispatchTable::new();
1248        let calls = Arc::new(AtomicUsize::new(0));
1249
1250        // First handler — returns Errno, must terminate the chain.
1251        let calls_first = Arc::clone(&calls);
1252        table.register(
1253            libc::SYS_openat,
1254            move |_cx: &HandlerCtx| {
1255                let calls = Arc::clone(&calls_first);
1256                async move {
1257                    calls.fetch_add(1, Ordering::SeqCst);
1258                    NotifAction::Errno(libc::EACCES)
1259                }
1260            },
1261        );
1262
1263        // Second handler — must NOT be called.
1264        let calls_second = Arc::clone(&calls);
1265        table.register(
1266            libc::SYS_openat,
1267            move |_cx: &HandlerCtx| {
1268                let calls = Arc::clone(&calls_second);
1269                async move {
1270                    calls.fetch_add(1, Ordering::SeqCst);
1271                    NotifAction::Continue
1272                }
1273            },
1274        );
1275
1276        let _ctx = fake_supervisor_ctx();
1277        let action = table
1278            .dispatch(fake_notif(libc::SYS_openat as i32), -1)
1279            .await;
1280
1281        match action {
1282            NotifAction::Errno(e) => assert_eq!(e, libc::EACCES),
1283            other => panic!("expected Errno(EACCES), got {:?}", other),
1284        }
1285        assert_eq!(
1286            calls.load(Ordering::SeqCst),
1287            1,
1288            "second handler must not run after first returned non-Continue"
1289        );
1290    }
1291
1292    /// A handler returning `Defer` is non-`Continue`, so it must short-circuit
1293    /// the chain exactly like `Errno`/`ReturnValue`: later handlers on the same
1294    /// syscall do not run.  Deferral is therefore a terminal decision.
1295    #[tokio::test]
1296    async fn dispatch_short_circuits_on_defer() {
1297        let mut table = DispatchTable::new();
1298        let later_ran = Arc::new(AtomicUsize::new(0));
1299
1300        table.register(
1301            libc::SYS_openat,
1302            |_cx: &HandlerCtx| async { NotifAction::defer(async { NotifAction::ReturnValue(1) }) },
1303        );
1304
1305        let later = Arc::clone(&later_ran);
1306        table.register(
1307            libc::SYS_openat,
1308            move |_cx: &HandlerCtx| {
1309                let later = Arc::clone(&later);
1310                async move {
1311                    later.fetch_add(1, Ordering::SeqCst);
1312                    NotifAction::Continue
1313                }
1314            },
1315        );
1316
1317        let _ctx = fake_supervisor_ctx();
1318        let action = table
1319            .dispatch(fake_notif(libc::SYS_openat as i32), -1)
1320            .await;
1321
1322        assert!(
1323            matches!(action, NotifAction::Defer(_)),
1324            "dispatch must return the Defer produced by the first handler"
1325        );
1326        assert_eq!(
1327            later_ran.load(Ordering::SeqCst),
1328            0,
1329            "Defer must short-circuit the chain like any non-Continue action"
1330        );
1331    }
1332
1333    /// `validate_handler_syscalls_against_policy` must reject handlers whose
1334    /// syscall is in the policy's user-specified blocklist, with the same
1335    /// rationale as DEFAULT_BLOCKLIST: the BPF program emits notif JEQs before
1336    /// deny JEQs, so a user handler returning `Continue` would translate into
1337    /// `SECCOMP_USER_NOTIF_FLAG_CONTINUE` and silently bypass the kernel-level
1338    /// block.
1339    ///
1340    /// Uses `mremap` because it is in `syscall_name_to_nr` but not in
1341    /// `DEFAULT_BLOCKLIST_SYSCALLS` — putting it into `extra_deny_syscalls` is the only
1342    /// way it ends up on the extra blocklist, so the test isolates the user-supplied
1343    /// path of `blocklist_syscall_numbers` from the default branch covered by
1344    /// `handler_on_default_blocklist_syscall_is_rejected`.
1345    ///
1346    /// Pure-logic counterpart to the integration test of the same name —
1347    /// runs without a live sandbox so the contract is enforced even on
1348    /// hosts where seccomp integration tests are skipped.
1349    #[test]
1350    fn validate_extras_rejects_user_specified_blocklist() {
1351        let policy = crate::sandbox::Sandbox::builder()
1352            .extra_deny_syscalls(vec!["mremap".into()])
1353            .build()
1354            .expect("policy builds");
1355
1356        let result = validate_handler_syscalls_against_policy(&[libc::SYS_mremap], &policy);
1357        assert_eq!(
1358            result,
1359            Err(libc::SYS_mremap),
1360            "handler on user-specified blocklist must be rejected, naming the offending syscall"
1361        );
1362    }
1363
1364    // ---- Handler trait tests --------------------------------------
1365
1366    #[tokio::test]
1367    async fn handler_via_blanket_impl_dispatches_closures() {
1368        use std::sync::atomic::{AtomicU64, Ordering};
1369        let counter = Arc::new(AtomicU64::new(0));
1370        let counter_clone = Arc::clone(&counter);
1371
1372        let h = move |cx: &HandlerCtx| {
1373            let counter = Arc::clone(&counter_clone);
1374            async move {
1375                counter.fetch_add(1, Ordering::SeqCst);
1376                let _ = cx.notif.pid; // touch ctx so it's exercised
1377                NotifAction::Continue
1378            }
1379        };
1380
1381        let _sup = fake_supervisor_ctx();
1382        let notif = fake_notif(libc::SYS_openat as i32);
1383        let cx = HandlerCtx { notif, notif_fd: -1 };
1384
1385        let action = h.handle(&cx).await;
1386        assert!(matches!(action, NotifAction::Continue));
1387        assert_eq!(counter.load(Ordering::SeqCst), 1);
1388    }
1389
1390    /// Struct-based `Handler` registered through `DispatchTable::register`
1391    /// MUST be invoked when `dispatch()` walks the chain — and `&self`
1392    /// state MUST persist across notifications.  Bridges the gap between
1393    /// the trait-shape unit tests above (which call `.handle()` directly)
1394    /// and the dispatch ordering tests (which use closures via blanket
1395    /// impl).  Without this test, a regression where the dispatch walker
1396    /// dropped `Arc<dyn Handler>` calls but kept closures working would
1397    /// not be caught at the unit layer.
1398    #[tokio::test]
1399    async fn dispatch_invokes_struct_handler_with_persistent_self_state() {
1400        use std::sync::atomic::{AtomicU64, Ordering};
1401
1402        struct StructHandler {
1403            calls: AtomicU64,
1404        }
1405
1406        impl Handler for StructHandler {
1407            fn handle<'a>(
1408                &'a self,
1409                _cx: &'a HandlerCtx,
1410            ) -> std::pin::Pin<Box<dyn std::future::Future<Output = NotifAction> + Send + 'a>> {
1411                Box::pin(async move {
1412                    self.calls.fetch_add(1, Ordering::SeqCst);
1413                    NotifAction::Continue
1414                })
1415            }
1416        }
1417
1418        let mut table = DispatchTable::new();
1419        let handler = std::sync::Arc::new(StructHandler {
1420            calls: AtomicU64::new(0),
1421        });
1422        table.register_arc(libc::SYS_openat, handler.clone() as std::sync::Arc<dyn Handler>);
1423
1424        let _sup = fake_supervisor_ctx();
1425        let notif = fake_notif(libc::SYS_openat as i32);
1426
1427        // Three independent dispatches against the same registered handler.
1428        // Walker MUST hit the struct's handle() each time, accumulating
1429        // state on &self.calls.
1430        for _ in 0..3 {
1431            let action = table.dispatch(notif, -1).await;
1432            assert!(matches!(action, NotifAction::Continue));
1433        }
1434
1435        assert_eq!(
1436            handler.calls.load(Ordering::SeqCst),
1437            3,
1438            "dispatch must invoke the struct-based handler on every walk"
1439        );
1440    }
1441}