Skip to main content

sandlock_core/seccomp/
notif.rs

1// Seccomp user notification supervisor — async event loop that receives
2// notifications from the kernel, dispatches them to handler functions, and
3// sends responses.
4
5use std::collections::{HashMap, HashSet};
6use std::future::Future;
7use std::io;
8use std::net::IpAddr;
9use std::os::unix::io::{AsRawFd, OwnedFd, RawFd};
10use std::pin::Pin;
11use std::sync::Arc;
12
13use crate::error::NotifError;
14use crate::arch;
15use crate::sys::structs::{
16    SeccompNotif, SeccompNotifAddfd, SeccompNotifResp,
17    SECCOMP_ADDFD_FLAG_SEND, SECCOMP_IOCTL_NOTIF_ADDFD, SECCOMP_IOCTL_NOTIF_ID_VALID, SECCOMP_IOCTL_NOTIF_RECV,
18    SECCOMP_IOCTL_NOTIF_SEND, SECCOMP_IOCTL_NOTIF_SET_FLAGS,
19    SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP, SECCOMP_USER_NOTIF_FLAG_CONTINUE,
20    ENOMEM,
21};
22
23// ============================================================
24// NotifAction — how the supervisor should respond
25// ============================================================
26
27/// A one-shot callback invoked with the child-side fd number returned by
28/// `SECCOMP_IOCTL_NOTIF_ADDFD` after a successful `InjectFdSendTracked`.
29/// Wraps a boxed closure with a manual `Debug` impl so that `NotifAction`
30/// can keep deriving `Debug`.  The closure is both `Send` and `Sync` so
31/// that `&NotifAction` remains `Send` (required because `NotifAction` is
32/// borrowed across `.await` points in the notifier loop).
33pub struct OnInjectSuccess(pub Box<dyn FnOnce(i32) + Send + Sync>);
34
35impl std::fmt::Debug for OnInjectSuccess {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.write_str("OnInjectSuccess(<callback>)")
38    }
39}
40
41impl OnInjectSuccess {
42    pub fn new<F: FnOnce(i32) + Send + Sync + 'static>(f: F) -> Self {
43        Self(Box::new(f))
44    }
45}
46
47/// A deferred decision: an owned, `'static` future that produces the real
48/// [`NotifAction`] off the supervisor's notification loop.
49///
50/// A handler returns [`NotifAction::Defer`] when computing the response is
51/// slow (a network round-trip, a blocking syscall) and must not stall the
52/// single supervisor task that gates every other trapped syscall.  The
53/// supervisor moves the future onto a worker, lets the loop proceed, and
54/// sends the response (via the still-valid `notif.id`) when the future
55/// resolves.  The future is `'static` because it outlives the borrowed
56/// `HandlerCtx` — capture what you need (`notif` is `Copy`, `notif_fd` is a
57/// `RawFd`) by value rather than borrowing `&self`.
58///
59/// The deferred future need only be `Send` (not `Sync`): the supervisor
60/// moves it onto a worker task and never shares it by reference.  Requiring
61/// `Sync` of user futures would be a leaky bound (it would reject a future
62/// capturing, say, a `Cell`), so it is not required.
63pub struct Deferred(Pin<Box<dyn Future<Output = NotifAction> + Send + 'static>>);
64
65// Safety: `NotifAction` must stay `Sync` so it can live in `Sync` contexts
66// (handler `&self` state, etc.; the `Handler` trait is `Send + Sync`), which
67// requires `Deferred: Sync`.  A `Send`-only future is not `Sync`, but the
68// boxed future is unreachable through a shared `&Deferred`: the field is
69// private, `Debug` touches only a static string, and `run(self)` consumes
70// the value (it is never callable through `&self`).  With no path to poll or
71// read the future via a shared reference, sharing `&Deferred` across threads
72// cannot race, so asserting `Sync` is sound while keeping user futures
73// `Send`-only.
74unsafe impl Sync for Deferred {}
75
76impl std::fmt::Debug for Deferred {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.write_str("Deferred(<future>)")
79    }
80}
81
82impl Deferred {
83    pub fn new<F: Future<Output = NotifAction> + Send + 'static>(f: F) -> Self {
84        Self(Box::pin(f))
85    }
86
87    /// Drive the deferred future to its terminal action.  Consumes `self`
88    /// because the future is run exactly once, on a worker task.
89    pub async fn run(self) -> NotifAction {
90        self.0.await
91    }
92}
93
94/// How the supervisor should respond to a notification.
95#[derive(Debug)]
96pub enum NotifAction {
97    /// SECCOMP_USER_NOTIF_FLAG_CONTINUE — let the syscall proceed.
98    Continue,
99    /// Return -1 with the given errno.
100    Errno(i32),
101    /// Inject a file descriptor into the child, then continue.
102    InjectFd { srcfd: RawFd, targetfd: i32 },
103    /// Inject a file descriptor using SECCOMP_ADDFD_FLAG_SEND (atomically responds).
104    /// The child sees the injected fd as the return value of the syscall.
105    /// The `OwnedFd` is closed automatically after the ioctl completes.
106    /// `newfd_flags` controls flags on the injected fd (e.g. O_CLOEXEC).
107    InjectFdSend { srcfd: OwnedFd, newfd_flags: u32 },
108    /// Like `InjectFdSend`, but also invokes `on_success` with the
109    /// child-side fd number that `SECCOMP_IOCTL_NOTIF_ADDFD` returned.
110    /// Used when the caller needs to track the exact fd number allocated
111    /// in the child (e.g. to key per-fd state without TOCTOU).
112    InjectFdSendTracked {
113        srcfd: OwnedFd,
114        newfd_flags: u32,
115        on_success: OnInjectSuccess,
116    },
117    /// Synthetic return value (the child sees this as the syscall result).
118    ReturnValue(i64),
119    /// Don't respond — used for checkpoint/freeze.
120    Hold,
121    /// Kill the child process group (OOM-kill semantics).
122    /// Fields: signal, process group leader pid.
123    Kill { sig: i32, pgid: i32 },
124    /// Defer the response: run the carried future on a worker task and
125    /// send its terminal action later, keyed by `notif.id`.  Non-`Continue`,
126    /// so it short-circuits the handler chain — a deferring handler makes a
127    /// terminal decision.  See [`Deferred`].
128    Defer(Deferred),
129}
130
131impl NotifAction {
132    /// Construct a [`NotifAction::Defer`] from a `'static` future.  Ergonomic
133    /// shorthand for `NotifAction::Defer(Deferred::new(fut))`.
134    pub fn defer<F: Future<Output = NotifAction> + Send + 'static>(fut: F) -> Self {
135        NotifAction::Defer(Deferred::new(fut))
136    }
137
138    /// Inject `content` into the child as the syscall's returned fd, backed by
139    /// a sealed (read-only, fixed-size), `O_CLOEXEC` in-memory file.
140    ///
141    /// The fd is created, populated, sealed, and owned end to end by sandlock;
142    /// the caller never sees or closes it. On allocation failure this collapses
143    /// to `Errno(EIO)`, so a handler can return it directly:
144    ///
145    /// ```ignore
146    /// return NotifAction::inject_bytes(&secret);
147    /// ```
148    ///
149    /// For a *writable* injected fd, build one with
150    /// [`content_memfd(content, false)`](content_memfd) and pass it to
151    /// [`NotifAction::InjectFdSend`] yourself.
152    pub fn inject_bytes(content: &[u8]) -> NotifAction {
153        match content_memfd(content, true) {
154            Ok(fd) => NotifAction::InjectFdSend {
155                srcfd: fd,
156                newfd_flags: libc::O_CLOEXEC as u32,
157            },
158            Err(_) => NotifAction::Errno(libc::EIO),
159        }
160    }
161}
162
163/// Create an anonymous in-memory file ("memfd") populated with `content` and
164/// rewound to offset 0, ready to inject as a syscall's returned fd via
165/// [`NotifAction::InjectFdSend`].
166///
167/// When `seal` is true the fd is sealed read-only and fixed-size
168/// (`F_SEAL_SEAL | F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK`) so the guest
169/// cannot modify or resize the content it is handed. Sealing is best-effort:
170/// on a kernel without sealing support the fd is still returned, bounded by
171/// the rest of the policy. Pass `false` only when the guest genuinely needs a
172/// writable injected fd.
173///
174/// Most callers want [`NotifAction::inject_bytes`], which wraps this in the
175/// common sealed + `O_CLOEXEC` configuration.
176pub fn content_memfd(content: &[u8], seal: bool) -> io::Result<OwnedFd> {
177    use std::io::{Seek, SeekFrom, Write};
178    use std::os::unix::io::FromRawFd;
179
180    let flags = if seal {
181        (libc::MFD_CLOEXEC | libc::MFD_ALLOW_SEALING) as u32
182    } else {
183        libc::MFD_CLOEXEC as u32
184    };
185    let memfd = crate::sys::syscall::memfd_create("sandlock-content", flags)?;
186
187    // Write the content and rewind. Borrow the raw fd for File I/O without
188    // transferring ownership: `memfd` (the OwnedFd) keeps owning it.
189    {
190        let raw = memfd.as_raw_fd();
191        let mut file = unsafe { std::fs::File::from_raw_fd(raw) };
192        let res = file
193            .write_all(content)
194            .and_then(|()| file.seek(SeekFrom::Start(0)).map(|_| ()));
195        std::mem::forget(file); // don't close `raw`; `memfd` still owns it
196        res?;
197    }
198
199    if seal {
200        // Best-effort: ignore failure on kernels lacking sealing support.
201        let seals =
202            libc::F_SEAL_SEAL | libc::F_SEAL_WRITE | libc::F_SEAL_GROW | libc::F_SEAL_SHRINK;
203        unsafe { libc::fcntl(memfd.as_raw_fd(), libc::F_ADD_SEALS, seals) };
204    }
205
206    Ok(memfd)
207}
208
209/// Collapse a deferred future's resolved action into a sendable terminal
210/// action.  A deferred future that itself resolves to `Defer` is a bug
211/// (no nested deferral); collapse it to `EIO` so the trapped child gets a
212/// definite response instead of wedging forever waiting for one.
213fn finalize_deferred(action: NotifAction) -> NotifAction {
214    match action {
215        NotifAction::Defer(_) => NotifAction::Errno(libc::EIO),
216        other => other,
217    }
218}
219
220// ============================================================
221// NetworkPolicy — network access policy enum
222// ============================================================
223
224/// Per-IP port allowlist. `Any` is used by `policy_fn` IP-only
225/// overrides (legacy `restrict_network(ips)` API where the user
226/// restricts the destination IP set but not ports).
227#[derive(Debug, Clone)]
228pub enum PortAllow {
229    /// Any port permitted to this IP.
230    Any,
231    /// Only these ports permitted to this IP.
232    Specific(HashSet<u16>),
233}
234
235/// Global network policy for the sandbox.
236#[derive(Debug, Clone)]
237pub enum NetworkPolicy {
238    /// No IP-level restriction for this protocol. On the on-behalf path this
239    /// is only ever reached *with* a destination policy active (e.g. a `*:*` /
240    /// `:*` allow-all-ports rule), so here it means "allow any destination."
241    /// The empty-`net_allow` deny-all case never reaches this arm: with no
242    /// destination policy the connect is returned to the kernel and Landlock's
243    /// `CONNECT_TCP` deny-all governs it.
244    Unrestricted,
245    /// Endpoint-level allowlist: a connection is permitted iff the
246    /// destination IP and port match at least one entry below.
247    AllowList {
248        /// Per-IP port rules. From `--net-allow host:ports` after
249        /// hostname resolution, or from `policy_fn` overrides.
250        per_ip: HashMap<IpAddr, PortAllow>,
251        /// (network, allowed-ports) rules from `--net-allow` IP/CIDR
252        /// targets, matched by containment with no DNS. `PortAllow::Any`
253        /// permits every port to the range.
254        cidrs: Vec<(crate::network::IpCidr, PortAllow)>,
255        /// Ports permitted for any IP (from `--net-allow :port` /
256        /// `*:port`).
257        any_ip_ports: HashSet<u16>,
258    },
259    /// Default-allow denylist: a connection is permitted unless the
260    /// destination IP/port matches a deny rule. From `--net-deny`.
261    DenyList {
262        /// (network, denied-ports) rules. `PortAllow::Any` denies every
263        /// port to the network; `Specific` denies only those ports.
264        cidrs: Vec<(crate::network::IpCidr, PortAllow)>,
265        /// Ports denied for any IP (the `:port` form).
266        any_ip_ports: HashSet<u16>,
267        /// Deny everything (the `:*` / `*:*` form). Rare; here for
268        /// completeness so the form is not silently a no-op.
269        deny_all: bool,
270    },
271}
272
273impl NetworkPolicy {
274    /// True iff a connection to (ip, port) should be permitted.
275    pub fn allows(&self, ip: IpAddr, port: u16) -> bool {
276        // `::ffff:a.b.c.d` is the same destination as `a.b.c.d` (a
277        // dual-stack socket reaches it over IPv4), and CIDR matching is
278        // family-exact: match rules against the canonical form so the
279        // mapped spelling can't bypass a v4 rule.
280        let ip = ip.to_canonical();
281        match self {
282            NetworkPolicy::Unrestricted => true,
283            NetworkPolicy::AllowList { per_ip, cidrs, any_ip_ports } => {
284                if any_ip_ports.contains(&port) {
285                    return true;
286                }
287                match per_ip.get(&ip) {
288                    Some(PortAllow::Any) => return true,
289                    Some(PortAllow::Specific(s)) if s.contains(&port) => return true,
290                    _ => {}
291                }
292                for (net, allowed) in cidrs {
293                    if net.contains(ip) {
294                        match allowed {
295                            PortAllow::Any => return true,
296                            PortAllow::Specific(s) => {
297                                if s.contains(&port) {
298                                    return true;
299                                }
300                            }
301                        }
302                    }
303                }
304                false
305            }
306            NetworkPolicy::DenyList { cidrs, any_ip_ports, deny_all } => {
307                if *deny_all {
308                    return false;
309                }
310                if any_ip_ports.contains(&port) {
311                    return false;
312                }
313                for (net, denied) in cidrs {
314                    if net.contains(ip) {
315                        match denied {
316                            PortAllow::Any => return false,
317                            PortAllow::Specific(s) => {
318                                if s.contains(&port) {
319                                    return false;
320                                }
321                            }
322                        }
323                    }
324                }
325                true
326            }
327        }
328    }
329}
330
331/// Check if a path-bearing notification targets a denied path.
332///
333/// For two-path syscalls (renameat2, linkat), checks both source and
334/// destination paths — a denied file must not be linked, renamed, or
335/// overwritten.
336///
337/// Each resolved path is checked both as-is (lexical normalization) and
338/// after following symlinks via `canonicalize`.  This prevents bypass via
339/// pre-existing symlinks, relative symlinks, or symlink chains that
340/// ultimately resolve to a denied path.
341pub(crate) fn is_path_denied_for_notif(
342    policy_fn_state: &super::state::PolicyFnState,
343    notif: &SeccompNotif,
344    notif_fd: RawFd,
345) -> bool {
346    if let Some(path) = resolve_path_for_notif(notif, notif_fd) {
347        if is_denied_with_symlink_resolve(policy_fn_state, &path) {
348            return true;
349        }
350    }
351    // For two-path syscalls, also check the second (destination) path.
352    if let Some(path) = resolve_second_path_for_notif(notif, notif_fd) {
353        if is_denied_with_symlink_resolve(policy_fn_state, &path) {
354            return true;
355        }
356    }
357    false
358}
359
360/// Check a path against denied entries, also resolving symlinks.
361///
362/// First checks the lexical path, then `canonicalize`s to follow symlinks
363/// and checks the real path.  This catches pre-existing symlinks, relative
364/// symlinks, and symlink chains that resolve to a denied file.
365fn is_denied_with_symlink_resolve(
366    policy_fn_state: &super::state::PolicyFnState,
367    path: &str,
368) -> bool {
369    // Check the literal (lexically normalized) path first.
370    if policy_fn_state.is_path_denied(path) {
371        return true;
372    }
373    // Follow symlinks and re-check against denied entries.
374    if let Ok(real) = std::fs::canonicalize(path) {
375        if policy_fn_state.is_path_denied(&real.to_string_lossy()) {
376            return true;
377        }
378        // Identity check: catches a denied file reached via a hardlink or a
379        // pre-existing alias, where the resolved path itself is not denied
380        // but the file identity is. Best-effort here (path-based precheck);
381        // the race-free authority is the fd identity check in the on-behalf
382        // open.
383        if let Some(id) = super::state::file_id_of_path(&real.to_string_lossy()) {
384            if policy_fn_state.is_id_denied(&id) {
385                return true;
386            }
387        }
388    }
389    false
390}
391
392/// `RESOLVE_NO_MAGICLINKS` — forbid `/proc` magic-link redirection during
393/// on-behalf resolution while still following ordinary symlinks the way the
394/// child's own open would.
395const RESOLVE_NO_MAGICLINKS: u64 = 0x02;
396
397/// Kernel `struct open_how` for `openat2`.
398#[repr(C)]
399struct OpenHow {
400    flags: u64,
401    mode: u64,
402    resolve: u64,
403}
404
405fn last_errno(fallback: i32) -> i32 {
406    io::Error::last_os_error().raw_os_error().unwrap_or(fallback)
407}
408
409/// `openat2` relative to `dirfd`. Returns an owned fd or the errno.
410fn openat2_at(dirfd: RawFd, path: &std::ffi::CStr, flags: u64, mode: u64, resolve: u64)
411    -> Result<OwnedFd, i32>
412{
413    use std::os::unix::io::FromRawFd;
414    let how = OpenHow { flags, mode, resolve };
415    let fd = unsafe {
416        libc::syscall(
417            arch::SYS_OPENAT2,
418            dirfd,
419            path.as_ptr(),
420            &how as *const OpenHow,
421            std::mem::size_of::<OpenHow>(),
422        )
423    } as i32;
424    if fd < 0 {
425        Err(last_errno(libc::ENOENT))
426    } else {
427        Ok(unsafe { OwnedFd::from_raw_fd(fd) })
428    }
429}
430
431/// Capture an `O_PATH` fd to the directory the child's open resolves against,
432/// taken from the child's own view so a concurrent `chdir`/dirfd swap cannot
433/// move the resolution base after we read it.
434fn open_base_dir(pid: u32, dirfd: i64) -> Result<OwnedFd, i32> {
435    use std::os::unix::io::FromRawFd;
436    if dirfd as i32 == libc::AT_FDCWD {
437        let cwd = std::ffi::CString::new(format!("/proc/{}/cwd", pid)).map_err(|_| libc::EINVAL)?;
438        let fd = unsafe {
439            libc::open(cwd.as_ptr(), libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC)
440        };
441        if fd < 0 {
442            return Err(last_errno(libc::EACCES));
443        }
444        Ok(unsafe { OwnedFd::from_raw_fd(fd) })
445    } else {
446        dup_fd_from_pid(pid, dirfd as i32).map_err(|_| libc::EBADF)
447    }
448}
449
450/// Real path of an open fd via its `/proc/self/fd` magic link.
451fn realpath_of_fd(fd: RawFd) -> Option<std::path::PathBuf> {
452    std::fs::read_link(format!("/proc/self/fd/{}", fd)).ok()
453}
454
455
456fn path_under_any(path: &std::path::Path, list: &[std::path::PathBuf]) -> bool {
457    list.iter().any(|p| path.starts_with(p))
458}
459
460/// Decide whether `realpath` may be opened with `flags` under the deny set
461/// and the (conservative) grant lists. Returns `Some(errno)` to refuse,
462/// `None` to allow. Never over-allows relative to the configured grants: a
463/// path outside every grant is refused, so this can only be stricter than
464/// Landlock, never looser (an over-deny is a functional gap, an over-allow
465/// would be an escape).
466fn deny_open_verdict(
467    realpath: &std::path::Path,
468    flags: u64,
469    policy: &NotifPolicy,
470    pfs: &super::state::PolicyFnState,
471) -> Option<i32> {
472    if pfs.is_path_denied(&realpath.to_string_lossy()) {
473        return Some(libc::EACCES);
474    }
475    let acc = flags as i32 & libc::O_ACCMODE;
476    let is_write = acc == libc::O_WRONLY
477        || acc == libc::O_RDWR
478        || (flags & libc::O_TRUNC as u64) != 0
479        || (flags & libc::O_CREAT as u64) != 0;
480    let allowed = if is_write {
481        path_under_any(realpath, &policy.chroot_writable)
482    } else {
483        path_under_any(realpath, &policy.chroot_readable)
484            || path_under_any(realpath, &policy.chroot_writable)
485    };
486    if allowed { None } else { Some(libc::EACCES) }
487}
488
489/// open/openat/openat2 argument layout, normalized across the spellings.
490struct OpenArgs {
491    dirfd: i64,
492    path_ptr: u64,
493    flags: u64,
494    mode: u64,
495    /// `openat2` `resolve` flags (`RESOLVE_*`); 0 for `open`/`openat`.
496    resolve: u64,
497}
498
499/// Decode the open arguments. `openat2` carries flags/mode/resolve inside a
500/// `struct open_how` in child memory, so its decode reads child memory and
501/// can fail; `None` means "could not decode" and the caller soft-falls-through
502/// (the kernel's own re-read fails the same way).
503fn decode_open_args(notif: &SeccompNotif, notif_fd: RawFd) -> Option<OpenArgs> {
504    let a = &notif.data.args;
505    let nr = notif.data.nr as i64;
506    if nr == libc::SYS_openat {
507        Some(OpenArgs { dirfd: a[0] as i64, path_ptr: a[1], flags: a[2], mode: a[3], resolve: 0 })
508    } else if nr == arch::SYS_OPENAT2 {
509        // openat2(dirfd, pathname, struct open_how *how, size_t size),
510        // open_how = { u64 flags, u64 mode, u64 resolve }.
511        let how_ptr = a[2];
512        let want = (a[3] as usize).min(std::mem::size_of::<OpenHow>());
513        if how_ptr == 0 || want < 16 {
514            return None; // need at least flags + mode
515        }
516        let bytes = read_child_mem(notif_fd, notif.id, notif.pid, how_ptr, want).ok()?;
517        if bytes.len() < 16 {
518            return None;
519        }
520        let flags = u64::from_ne_bytes(bytes[0..8].try_into().ok()?);
521        let mode = u64::from_ne_bytes(bytes[8..16].try_into().ok()?);
522        let resolve = if bytes.len() >= 24 {
523            u64::from_ne_bytes(bytes[16..24].try_into().ok()?)
524        } else {
525            0
526        };
527        Some(OpenArgs { dirfd: a[0] as i64, path_ptr: a[1], flags, mode, resolve })
528    } else {
529        // legacy open(path, flags, mode) — AT_FDCWD implied.
530        Some(OpenArgs { dirfd: libc::AT_FDCWD as i64, path_ptr: a[0], flags: a[1], mode: a[2], resolve: 0 })
531    }
532}
533
534/// Wrap a freshly opened raw fd into an `InjectFdSend`, honoring the child's
535/// `O_CLOEXEC` request. Ownership of `raw_fd` moves into the action.
536fn inject_open_result(raw_fd: i32, flags: u64) -> NotifAction {
537    use std::os::unix::io::FromRawFd;
538    if raw_fd < 0 {
539        return NotifAction::Errno(last_errno(libc::EACCES));
540    }
541    let owned = unsafe { OwnedFd::from_raw_fd(raw_fd) };
542    let newfd_flags = if flags & libc::O_CLOEXEC as u64 != 0 {
543        libc::O_CLOEXEC as u32
544    } else {
545        0
546    };
547    NotifAction::InjectFdSend { srcfd: owned, newfd_flags }
548}
549
550/// Existing-file branch: vet the pinned inode behind `probe`, then reopen it
551/// race-free via its `/proc/self/fd` magic link with the child's real access
552/// mode (binds to the inode, not the original path).
553fn reopen_existing_on_behalf(
554    probe: OwnedFd,
555    flags: u64,
556    policy: &NotifPolicy,
557    pfs: &super::state::PolicyFnState,
558) -> NotifAction {
559    // File exists. Refuse O_CREAT|O_EXCL the way the kernel would.
560    if (flags & libc::O_CREAT as u64) != 0 && (flags & libc::O_EXCL as u64) != 0 {
561        return NotifAction::Errno(libc::EEXIST);
562    }
563    let realpath = match realpath_of_fd(probe.as_raw_fd()) {
564        Some(p) => p,
565        None => return NotifAction::Errno(libc::EACCES),
566    };
567    if let Some(errno) = deny_open_verdict(&realpath, flags, policy, pfs) {
568        return NotifAction::Errno(errno);
569    }
570    // Identity check on the pinned file: a denied file reached via a hardlink,
571    // a rename to a non-denied name, or a pre-existing alias has a realpath
572    // that is not denied, but its handle identity is. Race-free — this is the
573    // exact file the child will receive.
574    if let Some(id) = super::state::file_id_of_fd(probe.as_raw_fd()) {
575        if pfs.is_id_denied(&id) {
576            return NotifAction::Errno(libc::EACCES);
577        }
578    }
579    // Resolution-only flags are stripped from the reopen.
580    let reopen_flags =
581        flags as i32 & !(libc::O_CREAT | libc::O_EXCL | libc::O_PATH | libc::O_NOFOLLOW);
582    let proc_path = match std::ffi::CString::new(format!("/proc/self/fd/{}", probe.as_raw_fd())) {
583        Ok(c) => c,
584        Err(_) => return NotifAction::Errno(libc::EIO),
585    };
586    let fd = unsafe { libc::open(proc_path.as_ptr(), reopen_flags) };
587    inject_open_result(fd, flags)
588}
589
590/// O_CREAT branch: resolve the parent directory race-free, vet the would-be
591/// target, then create the leaf inside that pinned parent (the dir inode is
592/// fixed, only the leaf name is appended).
593fn create_new_on_behalf(
594    base: &OwnedFd,
595    path: &str,
596    flags: u64,
597    mode: u64,
598    resolve: u64,
599    policy: &NotifPolicy,
600    pfs: &super::state::PolicyFnState,
601) -> NotifAction {
602    let p = std::path::Path::new(path);
603    let file_name = match p.file_name() {
604        Some(f) => f,
605        None => return NotifAction::Errno(libc::ENOENT),
606    };
607    let parent = p.parent().unwrap_or(std::path::Path::new("."));
608    let parent_str = match parent.to_str() {
609        Some("") | None => ".",
610        Some(s) => s,
611    };
612    let c_parent = match std::ffi::CString::new(parent_str) {
613        Ok(c) => c,
614        Err(_) => return NotifAction::Errno(libc::EINVAL),
615    };
616    let parent_fd = match openat2_at(
617        base.as_raw_fd(),
618        &c_parent,
619        (libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC) as u64,
620        0,
621        RESOLVE_NO_MAGICLINKS | resolve,
622    ) {
623        Ok(f) => f,
624        Err(e) => return NotifAction::Errno(e),
625    };
626    let parent_real = match realpath_of_fd(parent_fd.as_raw_fd()) {
627        Some(p) => p,
628        None => return NotifAction::Errno(libc::EACCES),
629    };
630    if let Some(errno) = deny_open_verdict(&parent_real.join(file_name), flags, policy, pfs) {
631        return NotifAction::Errno(errno);
632    }
633    let c_name = match std::ffi::CString::new(file_name.as_encoded_bytes()) {
634        Ok(c) => c,
635        Err(_) => return NotifAction::Errno(libc::EINVAL),
636    };
637    let create_flags = flags as i32 & !(libc::O_PATH | libc::O_NOFOLLOW);
638    let fd = unsafe { libc::openat(parent_fd.as_raw_fd(), c_name.as_ptr(), create_flags, mode) };
639    inject_open_result(fd, flags)
640}
641
642/// Perform `openat`/`open` on behalf of the child, race-free, when a deny is
643/// active. Resolves once (pinning the inode), enforces deny + grant on the
644/// pinned target, then hands the child an fd to that exact inode via
645/// `InjectFdSend`. Returns `Continue` only when no allow/deny decision was
646/// made on content we resolved (unreadable path / no allowlist configured),
647/// matching the precheck's existing soft fall-through.
648fn on_behalf_open_for_deny(
649    notif: &SeccompNotif,
650    policy: &NotifPolicy,
651    pfs: &super::state::PolicyFnState,
652    notif_fd: RawFd,
653) -> NotifAction {
654    // No allowlist configured (Landlock is not allowlisting the filesystem):
655    // there is no grant to check against, so taking over the open could only
656    // wrongly deny. Leave it to the existing precheck/kernel path.
657    if policy.chroot_readable.is_empty() && policy.chroot_writable.is_empty() {
658        return NotifAction::Continue;
659    }
660
661    let OpenArgs { dirfd, path_ptr, flags, mode, resolve } =
662        match decode_open_args(notif, notif_fd) {
663            Some(a) => a,
664            None => return NotifAction::Continue, // kernel's re-read fails the same way
665        };
666
667    let path = match read_child_cstr(notif_fd, notif.id, notif.pid, path_ptr, 4096) {
668        Some(p) => p,
669        None => return NotifAction::Continue, // kernel's re-read fails the same way
670    };
671    let c_path = match std::ffi::CString::new(path.clone()) {
672        Ok(c) => c,
673        Err(_) => return NotifAction::Errno(libc::EINVAL),
674    };
675    let base = match open_base_dir(notif.pid, dirfd) {
676        Ok(b) => b,
677        Err(e) => return NotifAction::Errno(e),
678    };
679
680    // Side-effect-free probe; mirror the child's no-follow intent for the
681    // final component and any `openat2` RESOLVE_* flags it requested.
682    let probe_flags = (libc::O_PATH | libc::O_CLOEXEC) as u64 | (flags & libc::O_NOFOLLOW as u64);
683    match openat2_at(base.as_raw_fd(), &c_path, probe_flags, 0, RESOLVE_NO_MAGICLINKS | resolve) {
684        Ok(probe) => reopen_existing_on_behalf(probe, flags, policy, pfs),
685        Err(errno) if errno == libc::ENOENT && (flags & libc::O_CREAT as u64) != 0 => {
686            create_new_on_behalf(&base, &path, flags, mode, resolve, policy, pfs)
687        }
688        Err(errno) => NotifAction::Errno(errno),
689    }
690}
691
692/// Read the thread-group leader (Tgid) of a thread from `/proc/<tid>/status`.
693fn tgid_of(tid: u32) -> Option<u32> {
694    let status = std::fs::read_to_string(format!("/proc/{}/status", tid)).ok()?;
695    status
696        .lines()
697        .find_map(|l| l.strip_prefix("Tgid:").and_then(|r| r.trim().parse().ok()))
698}
699
700/// Duplicate a file descriptor from an arbitrary process (by PID/TID) into the supervisor.
701///
702/// `pidfd_getfd` (Linux 5.6+) needs a pidfd for the owning *process*. All threads
703/// of a process share one fd table, so the process's pidfd dups any thread's fd:
704/// `pidfd_open(pid, 0)` gives it directly when `pid` is a thread-group leader,
705/// otherwise we resolve the leader via `Tgid` in `/proc/<pid>/status` and open
706/// that. The triggering thread is frozen on the seccomp notification, so its
707/// Tgid cannot race with pid reuse. Works on any kernel with `pidfd_getfd`.
708pub(crate) fn dup_fd_from_pid(pid: u32, target_fd: i32) -> io::Result<OwnedFd> {
709    use crate::sys::syscall::{pidfd_getfd, pidfd_open};
710    let pidfd = pidfd_open(pid, 0).or_else(|e| match tgid_of(pid) {
711        Some(tgid) if tgid != pid => pidfd_open(tgid, 0),
712        _ => Err(e),
713    })?;
714    pidfd_getfd(&pidfd, target_fd, 0)
715}
716
717// ============================================================
718// NotifPolicy — policy for the notification supervisor
719// ============================================================
720
721/// Policy for the notification supervisor.
722pub struct NotifPolicy {
723    pub max_memory_bytes: u64,
724    pub max_processes: u32,
725    pub has_memory_limit: bool,
726    /// A **network destination policy** is active: a `net_allow` allowlist, a
727    /// `net_deny` denylist, an HTTP ACL, or a live `policy_fn` (i.e.
728    /// `network_destination_policy`). Despite the historical singular framing
729    /// this is *not* only an allowlist. When set, the on-behalf path is the
730    /// IP-level enforcer for connect/sendto/sendmsg and those handlers must
731    /// never `Continue`; when clear, the syscalls are trapped only to gate
732    /// named `AF_UNIX` sockets and IP destinations are returned to the kernel
733    /// so Landlock/BPF govern them.
734    pub has_net_destination_policy: bool,
735    /// `--net-deny-bind` is active: trap `bind()` and register the on-behalf
736    /// handler so denied TCP ports can be refused (independent of the
737    /// connect-side `has_net_destination_policy`).
738    pub has_bind_denylist: bool,
739    /// Named (pathname) `AF_UNIX` connect gate. When true, `connect()` to a
740    /// named unix socket whose path is not covered by an fs-write grant is
741    /// denied with EACCES. Landlock has no access right for unix-socket
742    /// connect, so the seccomp layer closes the escape; abstract sockets are
743    /// handled by `LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET` instead.
744    pub has_unix_fs_gate: bool,
745    pub has_random_seed: bool,
746    pub has_time_start: bool,
747    /// Argv-safety gate: the supervisor must freeze every task that
748    /// could mutate argv before any consumer reads it. True when
749    /// `policy_fn` is active or when a handler is bound to
750    /// execve/execveat (such handlers can call `read_child_mem`).
751    /// Also gates ptrace fork-event tracking so `ProcessIndex` is
752    /// complete when the freeze enumerates it.
753    pub argv_safety_required: bool,
754    pub time_offset: i64,
755    pub num_cpus: Option<u32>,
756    pub port_remap: bool,
757    pub cow_enabled: bool,
758    pub chroot_root: Option<std::path::PathBuf>,
759    /// Virtual paths allowed for reading under chroot (original user-specified paths).
760    pub chroot_readable: Vec<std::path::PathBuf>,
761    /// Virtual paths allowed for writing under chroot (original user-specified paths).
762    pub chroot_writable: Vec<std::path::PathBuf>,
763    /// Virtual paths explicitly denied under chroot.
764    pub chroot_denied: Vec<std::path::PathBuf>,
765    /// Mount mappings: (virtual_path, host_path) pairs.
766    pub chroot_mounts: Vec<(std::path::PathBuf, std::path::PathBuf)>,
767    /// Virtual paths of mounts that are read-only: writes are denied even
768    /// though the path is mounted (and therefore readable). Used for the host
769    /// procfs mount and OCI `ro` bind mounts so a writable rootfs cannot make
770    /// e.g. `/proc/sys/*` writable.
771    pub chroot_mount_ro: Vec<std::path::PathBuf>,
772    pub deterministic_dirs: bool,
773    pub virtual_hostname: Option<String>,
774    pub has_http_acl: bool,
775    /// Synthetic `/etc/hosts` served to the sandbox. Always populated:
776    /// `openat("/etc/hosts")` returns a memfd with this content so the
777    /// host's on-disk `/etc/hosts` never leaks in. The content is the
778    /// loopback base plus any concrete hostnames resolved from `net_allow`.
779    pub virtual_etc_hosts: String,
780    /// User-declared trust-bundle paths to splice the MITM CA into.
781    pub ca_inject_paths: Vec<std::path::PathBuf>,
782    /// Active MITM CA public cert (PEM bytes) to inject. `Some` only when
783    /// HTTPS MITM is active (BYO or generated).
784    pub ca_inject_pem: Option<std::sync::Arc<Vec<u8>>>,
785}
786
787impl NotifPolicy {
788    /// Whether an IP-family `connect()` must be handled on-behalf by the
789    /// supervisor, given the destination's loopback-ness.
790    ///
791    /// This is the connect-side form of the invariant the `sendto`/`sendmsg`
792    /// handlers already follow: with [`Self::has_net_destination_policy`] the
793    /// on-behalf path is the IP-level enforcer and must never `Continue`;
794    /// without it there is nothing to enforce and the syscall is returned to
795    /// the kernel. `connect()` adds one wrinkle over datagram sends —
796    /// `port_remap` rewrites the destination, but only for **loopback** — so
797    /// that is the sole extra reason to supervise an otherwise-unpoliced IP
798    /// connect. When this returns `false`, the connect was trapped purely to
799    /// gate named `AF_UNIX` sockets, and deferring to the kernel lets the
800    /// child's own Landlock `CONNECT_TCP` rules govern it (deny-all for an
801    /// empty `net_allow`); handling it on-behalf would run it in the
802    /// unconfined supervisor and bypass that decision.
803    pub(crate) fn ip_connect_supervised(&self, dest_is_loopback: bool) -> bool {
804        self.has_net_destination_policy || (self.port_remap && dest_is_loopback)
805    }
806}
807
808// ============================================================
809// Low-level ioctl helpers
810// ============================================================
811
812/// Receive a seccomp notification from the kernel.
813/// ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV, &notif)
814fn recv_notif(fd: RawFd) -> io::Result<SeccompNotif> {
815    let mut notif: SeccompNotif = unsafe { std::mem::zeroed() };
816    let ret = unsafe {
817        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV as libc::c_ulong, &mut notif as *mut _)
818    };
819    if ret < 0 {
820        Err(io::Error::last_os_error())
821    } else {
822        Ok(notif)
823    }
824}
825
826/// Result of a non-blocking probe on the seccomp notif fd.
827enum NotifFdState {
828    /// At least one INIT-state notification is queued. `recv_notif`
829    /// will return without blocking.
830    Pending,
831    /// No notifications and no terminal flags. Wait for the next
832    /// epoll edge before probing again.
833    Empty,
834    /// `POLLHUP`/`POLLERR`/`POLLNVAL` set, or `poll(2)` itself failed:
835    /// filter has been released or the fd is invalid. The supervisor
836    /// should exit; subsequent waits would busy-spin because epoll
837    /// keeps reporting the fd ready.
838    Terminal,
839}
840
841/// Non-blocking probe of the seccomp notif fd.
842///
843/// `SECCOMP_IOCTL_NOTIF_RECV` ignores `O_NONBLOCK` and calls
844/// `wait_event_interruptible` unconditionally (kernel/seccomp.c
845/// `seccomp_notify_recv`). So `recv_notif` cannot be invoked
846/// speculatively to detect an empty queue. This helper uses
847/// `poll(timeout=0)` as a non-blocking predictor: if POLLIN is set
848/// the kernel will hand us a notification without blocking; if a
849/// terminal flag is set the fd will keep waking AsyncFd until the
850/// supervisor exits.
851fn probe_notif_fd(fd: RawFd) -> NotifFdState {
852    let mut pfd = libc::pollfd {
853        fd,
854        events: libc::POLLIN,
855        revents: 0,
856    };
857    let r = unsafe { libc::poll(&mut pfd, 1, 0) };
858    if r > 0 && (pfd.revents & libc::POLLIN) != 0 {
859        return NotifFdState::Pending;
860    }
861    if r < 0 || (pfd.revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL)) != 0 {
862        return NotifFdState::Terminal;
863    }
864    NotifFdState::Empty
865}
866
867/// Send a response with SECCOMP_USER_NOTIF_FLAG_CONTINUE.
868fn respond_continue(fd: RawFd, id: u64) -> io::Result<()> {
869    let resp = SeccompNotifResp {
870        id,
871        val: 0,
872        error: 0,
873        flags: SECCOMP_USER_NOTIF_FLAG_CONTINUE,
874    };
875    send_resp_raw(fd, &resp)
876}
877
878/// Send a response that returns -1 with the given errno.
879fn respond_errno(fd: RawFd, id: u64, errno: i32) -> io::Result<()> {
880    let resp = SeccompNotifResp {
881        id,
882        val: 0,
883        error: -errno,
884        flags: 0,
885    };
886    send_resp_raw(fd, &resp)
887}
888
889/// Send a response with a synthetic return value.
890fn respond_value(fd: RawFd, id: u64, val: i64) -> io::Result<()> {
891    let resp = SeccompNotifResp {
892        id,
893        val,
894        error: 0,
895        flags: 0,
896    };
897    send_resp_raw(fd, &resp)
898}
899
900/// Fail-closed response used when fd injection fails.
901///
902/// Denies the syscall with `EACCES` rather than letting it continue: a
903/// `SECCOMP_USER_NOTIF_FLAG_CONTINUE` here would let the child's original
904/// syscall run unmediated against the host path, silently bypassing
905/// chroot/file confinement. (Regression guard: this must never be a CONTINUE
906/// response.)
907fn inject_failure_resp(id: u64) -> SeccompNotifResp {
908    SeccompNotifResp {
909        id,
910        val: 0,
911        error: -libc::EACCES,
912        flags: 0,
913    }
914}
915
916/// Inject a file descriptor into the child process using SECCOMP_ADDFD_FLAG_SEND.
917///
918/// Uses the SEND flag to atomically inject the fd and respond to the syscall.
919/// The ioctl return value is the fd number assigned in the child process.
920/// After this call, no additional SECCOMP_IOCTL_NOTIF_SEND is needed.
921fn inject_fd_and_send(fd: RawFd, id: u64, srcfd: RawFd, newfd_flags: u32) -> io::Result<i32> {
922    let addfd = SeccompNotifAddfd {
923        id,
924        flags: SECCOMP_ADDFD_FLAG_SEND,
925        srcfd: srcfd as u32,
926        newfd: 0,   // ignored when SECCOMP_ADDFD_FLAG_SETFD is not set
927        newfd_flags,
928    };
929    let ret = unsafe {
930        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, &addfd as *const _)
931    };
932    if ret < 0 {
933        Err(io::Error::last_os_error())
934    } else {
935        Ok(ret as i32)
936    }
937}
938
939/// Inject a file descriptor into the child process (without responding).
940/// ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd)
941fn inject_fd(fd: RawFd, id: u64, srcfd: RawFd, targetfd: i32) -> io::Result<()> {
942    let addfd = SeccompNotifAddfd {
943        id,
944        flags: 0,
945        srcfd: srcfd as u32,
946        newfd: targetfd as u32,
947        newfd_flags: 0,
948    };
949    let ret = unsafe {
950        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, &addfd as *const _)
951    };
952    if ret < 0 {
953        Err(io::Error::last_os_error())
954    } else {
955        Ok(())
956    }
957}
958
959/// Raw ioctl to send a notification response.
960fn send_resp_raw(fd: RawFd, resp: &SeccompNotifResp) -> io::Result<()> {
961    let ret = unsafe {
962        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SEND as libc::c_ulong, resp as *const _)
963    };
964    if ret < 0 {
965        Err(io::Error::last_os_error())
966    } else {
967        Ok(())
968    }
969}
970
971/// Check whether a notification ID is still valid (TOCTOU guard).
972/// ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID, &id)
973pub(crate) fn id_valid(fd: RawFd, id: u64) -> io::Result<()> {
974    let ret = unsafe {
975        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID as libc::c_ulong, &id as *const _)
976    };
977    if ret < 0 {
978        Err(io::Error::last_os_error())
979    } else {
980        Ok(())
981    }
982}
983
984/// Try to enable sync wakeup (Linux 6.7+). Ignores errors.
985fn try_set_sync_wakeup(fd: RawFd) {
986    let flags: u64 = SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP as u64;
987    unsafe {
988        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS as libc::c_ulong, &flags as *const _);
989    }
990}
991
992// ============================================================
993// Child memory access helpers
994// ============================================================
995
996/// Read bytes from a child process via process_vm_readv (single syscall).
997fn read_child_mem_vm(pid: u32, addr: u64, len: usize) -> Result<Vec<u8>, NotifError> {
998    let mut buf = vec![0u8; len];
999    let local_iov = libc::iovec {
1000        iov_base: buf.as_mut_ptr() as *mut libc::c_void,
1001        iov_len: len,
1002    };
1003    let remote_iov = libc::iovec {
1004        iov_base: addr as *mut libc::c_void,
1005        iov_len: len,
1006    };
1007    let ret = unsafe {
1008        libc::process_vm_readv(pid as i32, &local_iov, 1, &remote_iov, 1, 0)
1009    };
1010    if ret < 0 {
1011        Err(NotifError::ChildMemoryRead(io::Error::last_os_error()))
1012    } else {
1013        buf.truncate(ret as usize);
1014        Ok(buf)
1015    }
1016}
1017
1018/// Write bytes to a child process via process_vm_writev (single syscall).
1019fn write_child_mem_vm(pid: u32, addr: u64, data: &[u8]) -> Result<(), NotifError> {
1020    let local_iov = libc::iovec {
1021        iov_base: data.as_ptr() as *mut libc::c_void,
1022        iov_len: data.len(),
1023    };
1024    let remote_iov = libc::iovec {
1025        iov_base: addr as *mut libc::c_void,
1026        iov_len: data.len(),
1027    };
1028    let ret = unsafe {
1029        libc::process_vm_writev(pid as i32, &local_iov, 1, &remote_iov, 1, 0)
1030    };
1031    if ret < 0 {
1032        Err(NotifError::ChildMemoryRead(io::Error::last_os_error()))
1033    } else if (ret as usize) < data.len() {
1034        Err(NotifError::ChildMemoryRead(io::Error::new(
1035            io::ErrorKind::WriteZero,
1036            format!("short write: {} of {} bytes", ret, data.len()),
1037        )))
1038    } else {
1039        Ok(())
1040    }
1041}
1042
1043/// Read bytes from a child process via `process_vm_readv` with TOCTOU validation.
1044///
1045/// Calls `id_valid` before and after the read to ensure the notification is
1046/// still live (kernel did not abort or release the trapped syscall while the
1047/// supervisor was reading guest memory).
1048///
1049/// Public — used by downstream `Handler` implementations to read syscall
1050/// arguments that the kernel passes by pointer (paths in `openat`, buffers
1051/// in `write`/`writev`, etc.).
1052pub fn read_child_mem(
1053    notif_fd: RawFd,
1054    id: u64,
1055    pid: u32,
1056    addr: u64,
1057    len: usize,
1058) -> Result<Vec<u8>, NotifError> {
1059    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1060    let result = read_child_mem_vm(pid, addr, len)?;
1061    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1062    Ok(result)
1063}
1064
1065/// Read a NUL-terminated string from child memory without crossing unmapped
1066/// page boundaries in a single `process_vm_readv` call.
1067///
1068/// TOCTOU-safe — internally calls [`read_child_mem`], inheriting the
1069/// `id_valid` checks bracketing each `process_vm_readv` call.
1070///
1071/// Page-aware: reads up to a page boundary at a time and stops at the
1072/// first NUL byte, never crossing into unmapped memory.  Returns
1073/// `None` for `addr == 0`, `max_len == 0`, a read failure, or a string
1074/// that exceeds `max_len` without a NUL.
1075///
1076/// Public — used by downstream `Handler` implementations that read
1077/// path arguments from notifications (`openat`, `unlinkat`, `statx`,
1078/// `newfstatat`, etc.).
1079pub fn read_child_cstr(
1080    notif_fd: RawFd,
1081    id: u64,
1082    pid: u32,
1083    addr: u64,
1084    max_len: usize,
1085) -> Option<String> {
1086    if addr == 0 || max_len == 0 {
1087        return None;
1088    }
1089
1090    const PAGE_SIZE: u64 = 4096;
1091    let mut result = Vec::with_capacity(max_len.min(256));
1092    let mut cur = addr;
1093    while result.len() < max_len {
1094        let page_remaining = PAGE_SIZE - (cur % PAGE_SIZE);
1095        let remaining = max_len - result.len();
1096        let to_read = page_remaining.min(remaining as u64) as usize;
1097        let bytes = read_child_mem(notif_fd, id, pid, cur, to_read).ok()?;
1098        if let Some(nul) = bytes.iter().position(|&b| b == 0) {
1099            result.extend_from_slice(&bytes[..nul]);
1100            return String::from_utf8(result).ok();
1101        }
1102        result.extend_from_slice(&bytes);
1103        cur += to_read as u64;
1104    }
1105
1106    String::from_utf8(result).ok()
1107}
1108
1109/// Write bytes to a child process via `process_vm_writev` with TOCTOU validation.
1110///
1111/// Same TOCTOU contract as [`read_child_mem`].  Public for downstream
1112/// `Handler` implementations that synthesise syscall results into
1113/// guest memory (e.g. fake `getdents64` listings populated from a
1114/// virtual directory index, or synthesised `stat` buffers).
1115pub fn write_child_mem(
1116    notif_fd: RawFd,
1117    id: u64,
1118    pid: u32,
1119    addr: u64,
1120    data: &[u8],
1121) -> Result<(), NotifError> {
1122    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1123    write_child_mem_vm(pid, addr, data)?;
1124    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1125    Ok(())
1126}
1127
1128/// Write bytes to a child, forcing past read-only page protections.
1129///
1130/// [`write_child_mem`] uses `process_vm_writev`, which honors the target VMA's
1131/// protection bits and so returns `EFAULT` when the destination page is
1132/// read-only — e.g. a `.rodata` path literal a program hands to
1133/// `chdir`/`execve`. This variant writes through `/proc/<pid>/mem`, whose writes
1134/// use `FOLL_FORCE` and therefore copy-on-write past a read-only mapping (the
1135/// same mechanism a debugger uses to plant a breakpoint in read-only `.text`).
1136/// It handles writable and read-only destinations through one path, so the
1137/// argument-rewrite sites need no `EFAULT` fallback.
1138///
1139/// Use ONLY for rewriting an *input* path argument the child owns that the
1140/// kernel re-reads under `SECCOMP_USER_NOTIF_FLAG_CONTINUE` (the `chdir`/`execve`
1141/// path rewrites). Do NOT use it for syscall *output* buffers (`stat`,
1142/// `getdents`, `getcwd`, `readlink`, the `getsockname`/`recvfrom` source
1143/// address): if a child supplies a read-only output buffer the real syscall
1144/// would `EFAULT`, and faithful emulation must too — keep those on
1145/// [`write_child_mem`]. (`bind`/`connect` need nothing here: they are emulated
1146/// on-behalf on a duped fd and never rewrite the child's sockaddr.)
1147///
1148/// Opening `/proc/<pid>/mem` for write needs `PTRACE_MODE_ATTACH_FSCREDS` over
1149/// the child (no actual attach or stop): satisfied by the supervisor as the
1150/// child's same-uid parent under the common Yama scopes. Returns `Err` (so the
1151/// caller can still surface `EFAULT`) if the `mem` file can't be opened or
1152/// written — it never silently no-ops. TOCTOU-safe: brackets the write with
1153/// `id_valid` like [`write_child_mem`].
1154pub fn write_child_mem_force(
1155    notif_fd: RawFd,
1156    id: u64,
1157    pid: u32,
1158    addr: u64,
1159    data: &[u8],
1160) -> Result<(), NotifError> {
1161    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1162    write_child_mem_proc(pid, addr, data)?;
1163    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1164    Ok(())
1165}
1166
1167/// Write bytes to a process's memory via `/proc/<pid>/mem` (open + `pwrite`).
1168///
1169/// Inner helper for [`write_child_mem_force`]; the file offset is the target
1170/// virtual address. Writes here use `FOLL_FORCE`, so they copy-on-write past a
1171/// read-only destination page where [`write_child_mem_vm`] (`process_vm_writev`)
1172/// returns `EFAULT`.
1173fn write_child_mem_proc(pid: u32, addr: u64, data: &[u8]) -> Result<(), NotifError> {
1174    use std::os::unix::fs::FileExt;
1175    let mem = std::fs::OpenOptions::new()
1176        .write(true)
1177        .open(format!("/proc/{}/mem", pid))
1178        .map_err(NotifError::ChildMemoryWrite)?;
1179    mem.write_all_at(data, addr)
1180        .map_err(NotifError::ChildMemoryWrite)?;
1181    Ok(())
1182}
1183
1184/// Kernel limit on a single argv/envp string (MAX_ARG_STRLEN, 32 pages).
1185/// A longer string makes execve fail with E2BIG, so no string that could
1186/// matter to a successful exec exceeds this.
1187const EXEC_MAX_ARG_STRLEN: usize = 32 * 4096;
1188
1189/// Upper bound on argv/envp entries scanned. The kernel's argument budget
1190/// (pointers count toward it) keeps any exec that could succeed far below
1191/// this; it only stops a runaway scan of a corrupt, unterminated array,
1192/// whose exec the kernel would fail anyway.
1193const EXEC_MAX_PTR_ENTRIES: usize = 1 << 20;
1194
1195/// Byte range `[start, end)` that the path rewrite will overwrite in the
1196/// child, plus the relocation buffer being assembled for it.
1197struct ExecRewritePlan {
1198    /// Bytes to write at the path pointer: the fd path, then every
1199    /// relocated string, all NUL-terminated.
1200    buf: Vec<u8>,
1201    /// Pointer-slot patches: (slot address in the argv/envp array,
1202    /// relocated string address).
1203    patches: Vec<(u64, u64)>,
1204}
1205
1206/// Read a NULL-terminated pointer array (argv or envp) from child memory.
1207/// Chunked at page boundaries because a single straddling read fails whole
1208/// if any page is unmapped, and mappings are page-granular.
1209fn read_exec_ptr_array(
1210    read: &mut impl FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1211    base: u64,
1212) -> Result<Vec<u64>, NotifError> {
1213    if base == 0 {
1214        return Ok(Vec::new());
1215    }
1216    const PAGE: u64 = 4096;
1217    let mut ptrs = Vec::new();
1218    let mut pending: Vec<u8> = Vec::new();
1219    let mut cur = base;
1220    loop {
1221        let chunk = (PAGE - cur % PAGE) as usize;
1222        let bytes = read(cur, chunk)?;
1223        cur += chunk as u64;
1224        pending.extend_from_slice(&bytes);
1225        let mut consumed = 0;
1226        while pending.len() - consumed >= 8 {
1227            let ptr = u64::from_ne_bytes(pending[consumed..consumed + 8].try_into().unwrap());
1228            consumed += 8;
1229            if ptr == 0 {
1230                return Ok(ptrs);
1231            }
1232            ptrs.push(ptr);
1233            if ptrs.len() >= EXEC_MAX_PTR_ENTRIES {
1234                return Ok(ptrs);
1235            }
1236        }
1237        pending.drain(..consumed);
1238    }
1239}
1240
1241/// Read exactly `len` bytes starting at `addr`, chunked at page boundaries.
1242fn read_exec_range(
1243    read: &mut impl FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1244    addr: u64,
1245    len: usize,
1246) -> Result<Vec<u8>, NotifError> {
1247    let mut out = Vec::with_capacity(len);
1248    let mut cur = addr;
1249    while out.len() < len {
1250        let chunk = ((4096 - cur % 4096) as usize).min(len - out.len());
1251        out.extend_from_slice(&read(cur, chunk)?);
1252        cur += chunk as u64;
1253    }
1254    Ok(out)
1255}
1256
1257/// Read a NUL-terminated string (NUL excluded) of at most
1258/// `EXEC_MAX_ARG_STRLEN` bytes, chunked at page boundaries.
1259fn read_exec_cstr(
1260    read: &mut impl FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1261    addr: u64,
1262) -> Result<Vec<u8>, NotifError> {
1263    let mut out = Vec::new();
1264    let mut cur = addr;
1265    while out.len() < EXEC_MAX_ARG_STRLEN {
1266        let chunk = ((4096 - cur % 4096) as usize).min(EXEC_MAX_ARG_STRLEN - out.len());
1267        let bytes = read(cur, chunk)?;
1268        if let Some(n) = bytes.iter().position(|&b| b == 0) {
1269            out.extend_from_slice(&bytes[..n]);
1270            return Ok(out);
1271        }
1272        out.extend_from_slice(&bytes);
1273        cur += chunk as u64;
1274    }
1275    Err(NotifError::Supervisor(format!(
1276        "exec arg string at {addr:#x} exceeds MAX_ARG_STRLEN"
1277    )))
1278}
1279
1280/// Compute the write buffer and pointer patches for rewriting an exec path
1281/// to `fd_path` without corrupting any argv/envp string.
1282///
1283/// The rewrite overwrites `[path_ptr, path_ptr + buf.len())`. Any argv/envp
1284/// string overlapping that window is appended to the buffer (preserving its
1285/// original bytes, read before any write happens) and every pointer slot
1286/// referencing it is patched to the relocated copy. Appending grows the
1287/// window, which can pull further strings in, hence the fixpoint loop.
1288///
1289/// Overlap comes in two shapes:
1290/// - a string starting inside the window (the common shell aliasing where
1291///   path == argv[0]);
1292/// - a string starting below `path_ptr` whose tail reaches it (the caller
1293///   passed a path pointer into the middle of a longer string). Reaching
1294///   means no NUL between its start and `path_ptr`; candidates are walked
1295///   downward so each gap segment is read once, and once a NUL is seen every
1296///   lower string is known to terminate before the window.
1297///
1298/// Fails if the argv/envp pointer arrays themselves fall inside the final
1299/// window: the kernel reads those arrays after we return, and they cannot be
1300/// moved because their addresses live in syscall registers.
1301fn plan_exec_rewrite(
1302    read: &mut impl FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1303    path_ptr: u64,
1304    fd_path: &[u8],
1305    argv_ptr: u64,
1306    envp_ptr: u64,
1307) -> Result<ExecRewritePlan, NotifError> {
1308    let argv = read_exec_ptr_array(read, argv_ptr)?;
1309    let envp = read_exec_ptr_array(read, envp_ptr)?;
1310    let slots: Vec<(u64, u64)> = argv
1311        .iter()
1312        .enumerate()
1313        .map(|(i, &p)| (argv_ptr + 8 * i as u64, p))
1314        .chain(envp.iter().enumerate().map(|(i, &p)| (envp_ptr + 8 * i as u64, p)))
1315        .collect();
1316
1317    let mut buf = fd_path.to_vec();
1318    let mut relocated: std::collections::BTreeMap<u64, u64> = std::collections::BTreeMap::new();
1319    let relocate = |buf: &mut Vec<u8>,
1320                        relocated: &mut std::collections::BTreeMap<u64, u64>,
1321                        read: &mut dyn FnMut(u64, usize) -> Result<Vec<u8>, NotifError>,
1322                        ptr: u64|
1323     -> Result<(), NotifError> {
1324        let s = read_exec_cstr(&mut |a, l| read(a, l), ptr)?;
1325        relocated.insert(ptr, path_ptr + buf.len() as u64);
1326        buf.extend_from_slice(&s);
1327        buf.push(0);
1328        Ok(())
1329    };
1330
1331    // Strings whose tail reaches path_ptr from below. Anything further than
1332    // MAX_ARG_STRLEN below cannot reach it within a string the kernel would
1333    // accept.
1334    let mut below: Vec<u64> = slots
1335        .iter()
1336        .map(|&(_, p)| p)
1337        .filter(|&p| p < path_ptr && path_ptr - p < EXEC_MAX_ARG_STRLEN as u64)
1338        .collect();
1339    below.sort_unstable();
1340    below.dedup();
1341    let mut nul_free_from = path_ptr;
1342    for &p in below.iter().rev() {
1343        let seg = read_exec_range(read, p, (nul_free_from - p) as usize)?;
1344        if seg.contains(&0) {
1345            break;
1346        }
1347        relocate(&mut buf, &mut relocated, read, p)?;
1348        nul_free_from = p;
1349    }
1350
1351    // Strings starting inside the (growing) window.
1352    loop {
1353        let end = path_ptr + buf.len() as u64;
1354        let mut grew = false;
1355        for &(_, p) in &slots {
1356            if !relocated.contains_key(&p) && p >= path_ptr && p < end {
1357                relocate(&mut buf, &mut relocated, read, p)?;
1358                grew = true;
1359            }
1360        }
1361        if !grew {
1362            break;
1363        }
1364    }
1365
1366    let end = path_ptr + buf.len() as u64;
1367    for (base, n) in [(argv_ptr, argv.len()), (envp_ptr, envp.len())] {
1368        if base != 0 && base < end && base + 8 * (n as u64 + 1) > path_ptr {
1369            return Err(NotifError::Supervisor(
1370                "execve argv/envp pointer array overlaps the path rewrite window".into(),
1371            ));
1372        }
1373    }
1374
1375    let patches = slots
1376        .iter()
1377        .filter_map(|&(slot, p)| relocated.get(&p).map(|&np| (slot, np)))
1378        .collect();
1379    Ok(ExecRewritePlan { buf, patches })
1380}
1381
1382/// Rewrite an execve/execveat path argument to `/proc/self/fd/<child_fd>`,
1383/// preserving every argv/envp string the rewrite would clobber.
1384///
1385/// A user-notif supervisor cannot change syscall registers, so redirecting
1386/// an exec to an injected fd means overwriting the child's path buffer in
1387/// place. Shells and `execvp`-style callers commonly pass the same buffer as
1388/// both the exec path and argv[0] (dash execs `./m` with path == argv[0]),
1389/// and libcs may pack other argv/envp strings right after it; a blind
1390/// overwrite corrupts whatever the fd path lands on. Multicall binaries
1391/// (busybox, uutils coreutils) then dispatch on basename(argv[0]) = "N" and
1392/// fail. [`plan_exec_rewrite`] relocates every affected string past the fd
1393/// path and patches the pointer slots, so the exec'd program sees its
1394/// original arguments for any string layout.
1395///
1396/// Writing past the original path buffer is safe because execve replaces the
1397/// whole address space on success, and the kernel copies the path and
1398/// argv/envp strings out of the old address space only after this returns
1399/// Continue, before anything else runs in the child. If the exec fails
1400/// instead (e.g. the injected fd is not executable), the child keeps running
1401/// with the rewritten buffer and patched pointers; argv is preserved so the
1402/// damage is confined to the path string, same as before this helper.
1403///
1404/// Pointer slots are 8 bytes: the BPF filter kills non-native-arch syscalls
1405/// before they reach the notif fd, and every supported native arch is
1406/// 64-bit.
1407///
1408/// `argv_ptr`/`envp_ptr` are the syscall's argv/envp arguments (args[1]/[2]
1409/// for execve, args[2]/[3] for execveat); 0 (NULL) arrays are skipped.
1410pub(crate) fn rewrite_exec_path_to_fd(
1411    notif_fd: RawFd,
1412    id: u64,
1413    pid: u32,
1414    path_ptr: u64,
1415    argv_ptr: u64,
1416    envp_ptr: u64,
1417    child_fd: i32,
1418) -> Result<(), NotifError> {
1419    let fd_path = format!("/proc/self/fd/{}\0", child_fd);
1420    let mut read = |addr: u64, len: usize| read_child_mem(notif_fd, id, pid, addr, len);
1421    let plan = plan_exec_rewrite(&mut read, path_ptr, fd_path.as_bytes(), argv_ptr, envp_ptr)?;
1422    write_child_mem_force(notif_fd, id, pid, path_ptr, &plan.buf)?;
1423    for (slot, new_ptr) in plan.patches {
1424        write_child_mem_force(notif_fd, id, pid, slot, &new_ptr.to_ne_bytes())?;
1425    }
1426    Ok(())
1427}
1428
1429// ============================================================
1430// Response dispatch
1431// ============================================================
1432
1433/// Dispatch a `NotifAction` to the appropriate low-level response function.
1434fn send_response(fd: RawFd, id: u64, action: NotifAction) -> io::Result<()> {
1435    match action {
1436        NotifAction::Continue => respond_continue(fd, id),
1437        NotifAction::Errno(errno) => respond_errno(fd, id, errno),
1438        NotifAction::InjectFd { srcfd, targetfd } => {
1439            inject_fd(fd, id, srcfd, targetfd)?;
1440            respond_continue(fd, id)
1441        }
1442        NotifAction::InjectFdSend { srcfd, newfd_flags } => {
1443            // SECCOMP_ADDFD_FLAG_SEND atomically injects the fd and responds.
1444            // No separate NOTIF_SEND needed after this.
1445            // On failure, deny (fail closed) rather than letting the original
1446            // syscall continue unmediated against the host path.
1447            // srcfd (OwnedFd) is dropped at end of this arm, closing the fd.
1448            match inject_fd_and_send(fd, id, srcfd.as_raw_fd(), newfd_flags) {
1449                Ok(_new_fd) => Ok(()),
1450                Err(_) => send_resp_raw(fd, &inject_failure_resp(id)),
1451            }
1452        }
1453        NotifAction::InjectFdSendTracked { srcfd, newfd_flags, on_success } => {
1454            match inject_fd_and_send(fd, id, srcfd.as_raw_fd(), newfd_flags) {
1455                Ok(new_fd) => {
1456                    (on_success.0)(new_fd);
1457                    Ok(())
1458                }
1459                Err(_) => send_resp_raw(fd, &inject_failure_resp(id)),
1460            }
1461        }
1462        NotifAction::ReturnValue(val) => respond_value(fd, id, val),
1463        NotifAction::Hold => Ok(()), // Don't send a response.
1464        NotifAction::Defer(_) => {
1465            // Defer is intercepted in `handle_notification` and never reaches
1466            // here on the normal path. If it ever does, fail closed with EIO
1467            // rather than dropping the future and wedging the child.
1468            debug_assert!(false, "Defer reached send_response; should be intercepted earlier");
1469            respond_errno(fd, id, libc::EIO)
1470        }
1471        NotifAction::Kill { sig, pgid } => {
1472            // Kill the entire process group, then return ENOMEM so the
1473            // seccomp notification is resolved (avoids a kernel warning).
1474            unsafe { libc::killpg(pgid, sig) };
1475            respond_errno(fd, id, ENOMEM)
1476        }
1477    }
1478}
1479
1480// ============================================================
1481// vDSO re-patching after exec
1482// ============================================================
1483
1484/// Re-patch the vDSO if the base address changed (e.g. after exec replaces it).
1485fn maybe_patch_vdso(pid: i32, procfs: &mut super::state::ProcfsState, policy: &NotifPolicy) {
1486    let base = match crate::vdso::find_vdso_base(pid) {
1487        Ok(addr) => addr,
1488        Err(_) => return,
1489    };
1490    if base == procfs.vdso_patched_addr {
1491        return; // already patched this vDSO
1492    }
1493    let time_offset = if policy.has_time_start { Some(policy.time_offset) } else { None };
1494    if crate::vdso::patch(pid, time_offset, policy.has_random_seed).is_ok() {
1495        procfs.vdso_patched_addr = base;
1496    }
1497}
1498
1499// ============================================================
1500// Policy event emission
1501// ============================================================
1502
1503/// Map a syscall number to a human-readable name for the policy callback.
1504fn syscall_name(nr: i64) -> &'static str {
1505    match nr {
1506        n if n == libc::SYS_openat => "openat",
1507        n if n == libc::SYS_connect => "connect",
1508        n if n == libc::SYS_sendto => "sendto",
1509        n if n == libc::SYS_sendmsg => "sendmsg",
1510        n if n == libc::SYS_sendmmsg => "sendmmsg",
1511        n if n == libc::SYS_bind => "bind",
1512        n if n == libc::SYS_clone => "clone",
1513        n if n == libc::SYS_clone3 => "clone3",
1514        n if Some(n) == arch::sys_vfork() => "vfork",
1515        n if Some(n) == arch::sys_fork() => "fork",
1516        n if n == libc::SYS_execve => "execve",
1517        n if n == libc::SYS_execveat => "execveat",
1518        n if n == libc::SYS_mmap => "mmap",
1519        n if n == libc::SYS_munmap => "munmap",
1520        n if n == libc::SYS_brk => "brk",
1521        n if n == libc::SYS_getrandom => "getrandom",
1522        n if n == libc::SYS_unlinkat => "unlinkat",
1523        n if n == libc::SYS_mkdirat => "mkdirat",
1524        _ => "unknown",
1525    }
1526}
1527
1528/// Map a syscall number to a high-level category.
1529fn syscall_category(nr: i64) -> crate::policy_fn::SyscallCategory {
1530    use crate::policy_fn::SyscallCategory;
1531    match nr {
1532        n if n == libc::SYS_openat || n == libc::SYS_unlinkat
1533            || n == libc::SYS_mkdirat || n == libc::SYS_renameat2
1534            || n == libc::SYS_symlinkat || n == libc::SYS_linkat
1535            || n == libc::SYS_fchmodat || n == libc::SYS_fchownat
1536            || n == libc::SYS_truncate || n == libc::SYS_readlinkat
1537            || n == libc::SYS_newfstatat || n == libc::SYS_statx
1538            || n == libc::SYS_faccessat || n == libc::SYS_getdents64
1539            || Some(n) == arch::sys_getdents() => SyscallCategory::File,
1540        n if n == libc::SYS_connect || n == libc::SYS_sendto
1541            || n == libc::SYS_sendmsg || n == libc::SYS_sendmmsg
1542            || n == libc::SYS_bind
1543            || n == libc::SYS_getsockname => SyscallCategory::Network,
1544        n if n == libc::SYS_clone || n == libc::SYS_clone3
1545            || Some(n) == arch::sys_vfork() || Some(n) == arch::sys_fork()
1546            || n == libc::SYS_execve || n == libc::SYS_execveat => SyscallCategory::Process,
1547        n if n == libc::SYS_mmap || n == libc::SYS_munmap
1548            || n == libc::SYS_brk || n == libc::SYS_mremap
1549            => SyscallCategory::Memory,
1550        _ => SyscallCategory::File, // default
1551    }
1552}
1553
1554/// Read the parent PID from /proc/{pid}/stat.
1555fn read_ppid(pid: u32) -> Option<u32> {
1556    let stat = std::fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?;
1557    // Format: "pid (comm) state ppid ..."
1558    // Find the closing ')' then split the rest
1559    let close_paren = stat.rfind(')')?;
1560    let rest = &stat[close_paren + 2..]; // skip ") "
1561    let fields: Vec<&str> = rest.split_whitespace().collect();
1562    // fields[0] = state, fields[1] = ppid
1563    fields.get(1)?.parse().ok()
1564}
1565
1566/// Read a NUL-terminated path from child memory (up to 256 bytes).
1567fn read_path_for_event(notif: &SeccompNotif, addr: u64, notif_fd: RawFd) -> Option<String> {
1568    if addr == 0 { return None; }
1569    let bytes = read_child_mem(notif_fd, notif.id, notif.pid, addr, 256).ok()?;
1570    let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1571    String::from_utf8(bytes[..nul].to_vec()).ok()
1572}
1573
1574fn normalize_path(path: &std::path::Path) -> String {
1575    use std::path::{Component, PathBuf};
1576
1577    let mut normalized = PathBuf::new();
1578    let absolute = path.is_absolute();
1579    if absolute {
1580        normalized.push("/");
1581    }
1582
1583    for component in path.components() {
1584        match component {
1585            Component::RootDir | Component::CurDir => {}
1586            Component::ParentDir => {
1587                normalized.pop();
1588            }
1589            Component::Normal(part) => normalized.push(part),
1590            Component::Prefix(_) => {}
1591        }
1592    }
1593
1594    if normalized.as_os_str().is_empty() {
1595        if absolute { "/".into() } else { ".".into() }
1596    } else {
1597        normalized.to_string_lossy().into_owned()
1598    }
1599}
1600
1601fn resolve_at_path_for_event(notif: &SeccompNotif, dirfd: i64, path: &str) -> Option<String> {
1602    use std::path::Path;
1603
1604    if Path::new(path).is_absolute() {
1605        return Some(normalize_path(Path::new(path)));
1606    }
1607
1608    let dirfd32 = dirfd as i32;
1609    let base = if dirfd32 == libc::AT_FDCWD {
1610        std::fs::read_link(format!("/proc/{}/cwd", notif.pid)).ok()?
1611    } else {
1612        std::fs::read_link(format!("/proc/{}/fd/{}", notif.pid, dirfd32)).ok()?
1613    };
1614
1615    Some(normalize_path(&base.join(path)))
1616}
1617
1618fn resolve_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Option<String> {
1619    let nr = notif.data.nr as i64;
1620    match nr {
1621        n if n == libc::SYS_openat || n == arch::SYS_OPENAT2 => {
1622            // openat(dirfd, pathname, flags, mode) and
1623            // openat2(dirfd, pathname, how, size) share (dirfd, pathname).
1624            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1625            resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1626        }
1627        n if Some(n) == arch::sys_open() || n == libc::SYS_execve => {
1628            let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1629            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1630        }
1631        n if n == libc::SYS_execveat => {
1632            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1633            resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1634        }
1635        // linkat(olddirfd, oldpath, newdirfd, newpath, flags)
1636        // Check the source (old) path — deny if it's a denied file being linked away.
1637        n if n == libc::SYS_linkat => {
1638            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1639            resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1640        }
1641        // renameat2(olddirfd, oldpath, newdirfd, newpath, flags)
1642        // Check the source (old) path — deny if a denied file is being renamed away.
1643        n if n == libc::SYS_renameat2 => {
1644            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1645            resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1646        }
1647        // symlinkat/symlink intentionally omitted: creating a symlink does
1648        // not access its target, so there is nothing to gate here. Any later
1649        // open through the symlink resolves to the real target and is denied
1650        // race-free on the open path (issue #111). See `on_behalf_open_for_deny`.
1651        // link(oldpath, newpath) — legacy, AT_FDCWD implied for both
1652        n if Some(n) == arch::sys_link() => {
1653            let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1654            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1655        }
1656        // rename(oldpath, newpath) — legacy, AT_FDCWD implied for both
1657        n if Some(n) == arch::sys_rename() => {
1658            let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1659            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1660        }
1661        _ => None,
1662    }
1663}
1664
1665/// Resolve the second (destination) path for two-path syscalls.
1666///
1667/// Returns `None` for syscalls that only have a single path argument.
1668fn resolve_second_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Option<String> {
1669    let nr = notif.data.nr as i64;
1670    match nr {
1671        // renameat2(olddirfd, oldpath, newdirfd, newpath, flags)
1672        n if n == libc::SYS_renameat2 => {
1673            let path = read_path_for_event(notif, notif.data.args[3], notif_fd)?;
1674            resolve_at_path_for_event(notif, notif.data.args[2] as i64, &path)
1675        }
1676        // linkat(olddirfd, oldpath, newdirfd, newpath, flags)
1677        // Destination of a hardlink to a denied file should also be denied
1678        // (prevents overwriting a denied file via linkat).
1679        n if n == libc::SYS_linkat => {
1680            let path = read_path_for_event(notif, notif.data.args[3], notif_fd)?;
1681            resolve_at_path_for_event(notif, notif.data.args[2] as i64, &path)
1682        }
1683        // rename(oldpath, newpath) — legacy
1684        n if Some(n) == arch::sys_rename() => {
1685            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1686            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1687        }
1688        // link(oldpath, newpath) — legacy
1689        n if Some(n) == arch::sys_link() => {
1690            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1691            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1692        }
1693        _ => None,
1694    }
1695}
1696
1697/// Extract IP and port from a sockaddr in child memory. Parsing (including
1698/// v4-mapped canonicalization) is shared with the enforcement path so the
1699/// policy_fn callback judges the same address the policy layer does.
1700fn read_sockaddr_for_event(notif: &SeccompNotif, addr: u64, len: usize, notif_fd: RawFd)
1701    -> (Option<std::net::IpAddr>, Option<u16>)
1702{
1703    if addr == 0 || len < 4 { return (None, None); }
1704    let bytes = match read_child_mem(notif_fd, notif.id, notif.pid, addr, len.min(128)) {
1705        Ok(b) => b,
1706        Err(_) => return (None, None),
1707    };
1708    let ip = crate::network::materialize::parse_ip_from_sockaddr(&bytes);
1709    let port = crate::network::materialize::parse_port_from_sockaddr(&bytes);
1710    (ip, port.filter(|&p| p > 0))
1711}
1712
1713/// Read argv (NULL-terminated array of char* in child memory) for execve.
1714/// Capped at 64 entries × 256 bytes/entry as a safety bound.
1715fn read_argv_for_event(notif: &SeccompNotif, argv_ptr: u64, notif_fd: RawFd) -> Option<Vec<String>> {
1716    if argv_ptr == 0 { return None; }
1717    let mut args = Vec::new();
1718    let ptr_size = std::mem::size_of::<u64>();
1719
1720    for i in 0..64u64 {
1721        let ptr_addr = argv_ptr + i * ptr_size as u64;
1722        let ptr_bytes = read_child_mem(notif_fd, notif.id, notif.pid, ptr_addr, ptr_size).ok()?;
1723        let str_ptr = u64::from_ne_bytes(ptr_bytes[..8].try_into().ok()?);
1724        if str_ptr == 0 { break; } // NULL terminator
1725
1726        if let Some(s) = read_path_for_event(notif, str_ptr, notif_fd) {
1727            args.push(s);
1728        } else {
1729            break;
1730        }
1731    }
1732
1733    if args.is_empty() { None } else { Some(args) }
1734}
1735
1736/// Resolve a held syscall's policy_fn gate outcome into a verdict.
1737///
1738/// `received` is the verdict the callback sent, or `None` if the gate timed
1739/// out or its channel closed before a decision arrived. A held syscall is one
1740/// whose verdict matters (execve, connect, openat, ...); when no decision
1741/// arrives we fail closed and deny rather than letting the syscall proceed.
1742fn resolve_held_gate(
1743    received: Option<crate::policy_fn::Verdict>,
1744) -> Option<crate::policy_fn::Verdict> {
1745    match received {
1746        Some(v) => Some(v),
1747        None => Some(crate::policy_fn::Verdict::Deny),
1748    }
1749}
1750
1751/// Emit a syscall event to the policy_fn callback thread (if active).
1752/// Returns the callback's verdict for held syscalls.
1753async fn emit_policy_event(
1754    notif: &SeccompNotif,
1755    action: &NotifAction,
1756    policy_fn_state: &Arc<tokio::sync::Mutex<super::state::PolicyFnState>>,
1757    notif_fd: RawFd,
1758) -> Option<crate::policy_fn::Verdict> {
1759    let pfs = policy_fn_state.lock().await;
1760    let tx = match pfs.event_tx.as_ref() {
1761        Some(tx) => tx.clone(),
1762        None => return None,
1763    };
1764    drop(pfs);
1765
1766    let nr = notif.data.nr as i64;
1767    let denied = matches!(action, NotifAction::Errno(_));
1768    let name = syscall_name(nr);
1769    let category = syscall_category(nr);
1770    let parent_pid = read_ppid(notif.pid);
1771
1772    // Extract metadata based on syscall type.
1773    //
1774    // Path strings are deliberately NOT extracted: the kernel re-reads
1775    // user-memory pointers after Continue, so any path-string-based
1776    // decision is racy (issue #27). Path-based access control belongs
1777    // in static Landlock rules.
1778    //
1779    // argv IS extracted for allowed execve/execveat notifications:
1780    // the supervisor freezes every task in the sandbox (siblings +
1781    // peers) before this callback reads argv and keeps that freeze
1782    // through Continue, so the post-Continue re-read sees the same
1783    // memory we read here.
1784    //
1785    // Network fields are TOCTOU-safe because connect/sendto/bind are
1786    // performed on-behalf via pidfd_getfd; the kernel never re-reads
1787    // child memory for those syscalls.
1788    let mut host = None;
1789    let mut port = None;
1790    let mut size = None;
1791    let mut argv = None;
1792
1793    if !denied && (nr == libc::SYS_execve || nr == libc::SYS_execveat) {
1794        // execve(pathname, argv, envp):       args[1] = argv ptr
1795        // execveat(dirfd, pathname, argv, ..): args[2] = argv ptr
1796        let argv_ptr = if nr == libc::SYS_execveat {
1797            notif.data.args[2]
1798        } else {
1799            notif.data.args[1]
1800        };
1801        argv = read_argv_for_event(notif, argv_ptr, notif_fd);
1802    }
1803
1804    if nr == libc::SYS_connect || nr == libc::SYS_sendto || nr == libc::SYS_bind {
1805        // connect(fd, addr, addrlen): args[1]=addr, args[2]=len
1806        let addr_ptr = notif.data.args[1];
1807        let addr_len = notif.data.args[2] as usize;
1808        let (h, p) = read_sockaddr_for_event(notif, addr_ptr, addr_len, notif_fd);
1809        host = h;
1810        port = p;
1811    }
1812
1813    if nr == libc::SYS_mmap {
1814        // mmap(addr, length, ...): args[1] = length
1815        size = Some(notif.data.args[1]);
1816    }
1817
1818    let event = crate::policy_fn::SyscallEvent {
1819        syscall: name.to_string(),
1820        category,
1821        pid: notif.pid,
1822        parent_pid,
1823        host,
1824        port,
1825        size,
1826        argv,
1827        denied,
1828    };
1829
1830    // Hold syscalls where the callback's verdict matters.
1831    // The child is blocked until the callback returns.
1832    let is_held = nr == libc::SYS_execve || nr == libc::SYS_execveat
1833        || nr == libc::SYS_connect || nr == libc::SYS_sendto
1834        || nr == libc::SYS_bind || nr == libc::SYS_openat;
1835
1836    if is_held {
1837        let (gate_tx, gate_rx) = tokio::sync::oneshot::channel();
1838        let _ = tx.send(crate::policy_fn::PolicyEvent {
1839            event,
1840            gate: Some(gate_tx),
1841        });
1842        let received = match tokio::time::timeout(std::time::Duration::from_secs(5), gate_rx).await {
1843            Ok(Ok(verdict)) => Some(verdict),
1844            _ => None, // timeout or channel closed
1845        };
1846        resolve_held_gate(received)
1847    } else {
1848        let _ = tx.send(crate::policy_fn::PolicyEvent {
1849            event,
1850            gate: None,
1851        });
1852        None
1853    }
1854}
1855
1856// ============================================================
1857// Per-notification handler (runs in a spawned task)
1858// ============================================================
1859
1860/// Process a single seccomp notification: vDSO re-patch, path denial check,
1861/// dispatch, policy event emission, and response.
1862/// Maximum number of deferred handler futures running concurrently. Caps
1863/// the worker fan-out (and any resources those workers hold, e.g. memfds or
1864/// sockets) so a burst of deferrals cannot exhaust the supervisor process.
1865const DEFER_MAX_INFLIGHT: usize = 64;
1866
1867/// Maximum time a deferred handler future may run before the supervisor gives
1868/// up and fails the trapped syscall closed. Bounds the worst case so a hung
1869/// future (e.g. a stalled network fetch in a token-injection handler) cannot
1870/// park the child forever or permanently leak its `DEFER_MAX_INFLIGHT` slot.
1871const DEFER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
1872
1873/// Drive a deferred future to its terminal action, bounded by `limit`.
1874///
1875/// On timeout, fail closed with `EIO` so the trapped child gets a definite
1876/// response instead of parking forever; `finalize_deferred` still guards a
1877/// future that resolves to a nested `Defer`.
1878async fn run_deferred_within(deferred: Deferred, limit: std::time::Duration) -> NotifAction {
1879    match tokio::time::timeout(limit, deferred.run()).await {
1880        Ok(action) => finalize_deferred(action),
1881        Err(_) => {
1882            eprintln!(
1883                "sandlock: deferred handler exceeded {:?}; failing syscall with EIO",
1884                limit
1885            );
1886            NotifAction::Errno(libc::EIO)
1887        }
1888    }
1889}
1890
1891/// Spawn a worker task that drives a deferred handler future to its terminal
1892/// action and sends the seccomp response, keyed by `id`. The `permit` is
1893/// held for the worker's lifetime, releasing its `DEFER_MAX_INFLIGHT` slot on
1894/// completion. A stale `id` (child exited mid-defer) makes `send_response`
1895/// a no-op, matching the inline path's "child may have exited" tolerance.
1896fn spawn_deferred(
1897    fd: RawFd,
1898    id: u64,
1899    deferred: Deferred,
1900    permit: tokio::sync::OwnedSemaphorePermit,
1901) {
1902    tokio::spawn(async move {
1903        let _permit = permit; // released when the worker finishes
1904        let action = run_deferred_within(deferred, DEFER_TIMEOUT).await;
1905        let _ = send_response(fd, id, action);
1906    });
1907}
1908
1909async fn handle_notification(
1910    notif: SeccompNotif,
1911    ctx: &Arc<super::ctx::SupervisorCtx>,
1912    dispatch_table: &super::dispatch::DispatchTable,
1913    fd: RawFd,
1914    defer_sem: &Arc<tokio::sync::Semaphore>,
1915) {
1916    let policy = &ctx.policy;
1917
1918    // Ensure every pid that produces a notification has per-process
1919    // supervisor state and an exit watcher. The fork handler runs on
1920    // the *parent* pid (the child doesn't exist yet at clone-time), so
1921    // the child gets registered the first time it issues a notified
1922    // syscall.
1923    crate::resource::register_child_if_new(ctx, notif.pid as i32).await;
1924
1925    // Re-patch vDSO if needed (exec replaces it with a fresh copy).
1926    if policy.has_time_start || policy.has_random_seed {
1927        let mut pfs = ctx.procfs.lock().await;
1928        maybe_patch_vdso(notif.pid as i32, &mut pfs, policy);
1929    }
1930
1931    // Check dynamic path denials before dispatch
1932    let mut action = {
1933        let nr = notif.data.nr as i64;
1934        // symlinkat/symlink are not gated: creating a symlink does not access
1935        // its target (any open through it is denied race-free on the open
1936        // path). See `resolve_path_for_notif`.
1937        let mut path_check_nrs = vec![
1938            libc::SYS_openat, arch::SYS_OPENAT2, libc::SYS_execve, libc::SYS_execveat,
1939            libc::SYS_linkat, libc::SYS_renameat2,
1940        ];
1941        path_check_nrs.extend([
1942            arch::sys_open(), arch::sys_link(), arch::sys_rename(),
1943        ].into_iter().flatten());
1944        let should_precheck_denied = policy.chroot_root.is_none()
1945            && path_check_nrs.contains(&nr);
1946        if should_precheck_denied {
1947            let pfs = ctx.policy_fn.lock().await;
1948            if is_path_denied_for_notif(&pfs, &notif, fd) {
1949                NotifAction::Errno(libc::EACCES)
1950            } else {
1951                let has_denied = pfs.has_denied_paths();
1952                drop(pfs);
1953                // Let normal dispatch run first so /proc virtualization and
1954                // other handlers still win for their paths.
1955                let action = dispatch_table.dispatch(notif, fd).await;
1956                // A bare `Continue` for openat/open is the racy window: the
1957                // supervisor's resolution said "not denied", but the kernel
1958                // re-resolves after Continue and a racing thread can swap a
1959                // symlink to reach a denied carve-out inside a granted tree
1960                // (issue #111). Run the open on-behalf against the pinned
1961                // inode and inject the fd so the kernel never re-resolves.
1962                // Other path syscalls keep the best-effort precheck above
1963                // (documented follow-up — they return no fd to inject).
1964                let is_openat_family = nr == libc::SYS_openat
1965                    || nr == arch::SYS_OPENAT2
1966                    || Some(nr) == arch::sys_open();
1967                if matches!(action, NotifAction::Continue) && is_openat_family && has_denied {
1968                    let pfs = ctx.policy_fn.lock().await;
1969                    on_behalf_open_for_deny(&notif, policy, &pfs, fd)
1970                } else {
1971                    action
1972                }
1973            }
1974        } else {
1975            dispatch_table.dispatch(notif, fd).await
1976        }
1977    };
1978
1979    let nr = notif.data.nr as i64;
1980    let fork_counted = matches!(action, NotifAction::Continue)
1981        && crate::resource::fork_counted_on_continue(&notif, fd);
1982
1983    // TOCTOU-close for execve (issue #27): freeze every sandbox task
1984    // that could mutate argv before policy_fn reads argv and before the
1985    // kernel re-reads it after Continue. This covers two writer classes:
1986    //   1. Sibling threads of the calling tid (same TGID, share mm).
1987    //   2. Peer processes in other TGIDs that alias argv pages via
1988    //      MAP_SHARED mappings or share mm via clone(CLONE_VM).
1989    //
1990    // The freeze enumerates ProcessIndex. With policy_fn active, that
1991    // index is complete: fork-like syscalls are traced at creation time
1992    // below, before new children can run user code.
1993    //
1994    // Strict on failure: if we cannot establish the freeze, we cannot
1995    // safely expose argv or allow execve, so we deny with EPERM.
1996    let mut exec_freeze = None;
1997    if matches!(action, NotifAction::Continue)
1998        && policy.argv_safety_required
1999        && crate::freeze::requires_freeze_on_continue(nr)
2000    {
2001        match crate::freeze::freeze_sandbox_for_execve(
2002            &ctx.processes,
2003            notif.pid as i32,
2004        ) {
2005            Ok(outcome) => {
2006                exec_freeze = Some(outcome);
2007            }
2008            Err(e) => {
2009                eprintln!(
2010                    "sandlock: argv-safety freeze failed for pid {}: {} \
2011                     — denying execve to preserve TOCTOU invariant",
2012                    notif.pid, e
2013                );
2014                action = NotifAction::Errno(libc::EPERM);
2015            }
2016        }
2017    }
2018
2019    // Emit event to policy_fn callback if active. For execve, argv is
2020    // only populated after `exec_freeze` has stopped every possible
2021    // writer, and those tasks stay stopped until after NOTIF_SEND.
2022    if let Some(verdict) = emit_policy_event(&notif, &action, &ctx.policy_fn, fd).await {
2023        use crate::policy_fn::Verdict;
2024        match verdict {
2025            Verdict::Deny => { action = NotifAction::Errno(libc::EPERM); }
2026            Verdict::DenyWith(errno) => { action = NotifAction::Errno(errno); }
2027            Verdict::Audit => { /* allow, but could log here */ }
2028            Verdict::Allow => {}
2029        }
2030    }
2031
2032    if fork_counted && !matches!(action, NotifAction::Continue) {
2033        crate::resource::rollback_fork_count(&ctx.resource).await;
2034    }
2035
2036    // With policy_fn active, fork-like syscalls are traced for exactly
2037    // one ptrace event so ProcessIndex becomes complete before the new
2038    // child can run user code. That closes the race where a peer
2039    // process could exist without ever having produced a notification.
2040    let mut creation_trace = None;
2041    if matches!(action, NotifAction::Continue)
2042        && crate::resource::requires_process_creation_tracking(&notif, fd, policy)
2043    {
2044        match crate::resource::prepare_process_creation_tracking(ctx, notif.pid as i32).await {
2045            Ok(trace) => {
2046                creation_trace = Some(trace);
2047            }
2048            Err(e) => {
2049                eprintln!(
2050                    "sandlock: process-creation tracking failed for pid {}: {} \
2051                     — denying fork-like syscall to preserve argv TOCTOU invariant",
2052                    notif.pid, e
2053                );
2054                if fork_counted {
2055                    crate::resource::rollback_fork_count(&ctx.resource).await;
2056                }
2057                action = NotifAction::Errno(libc::EPERM);
2058            }
2059        }
2060    }
2061
2062    // Deferred response: run the handler's future on a worker task so the
2063    // single supervisor loop is not blocked waiting for slow work (a network
2064    // round-trip, a blocking syscall). The trapped child stays parked in the
2065    // syscall; the worker sends the real response later, keyed by notif.id.
2066    //
2067    // Deferral is refused on syscalls whose Continue path requires the
2068    // execve argv-safety freeze or fork creation-tracking: sending the
2069    // response off-loop would skip that TOCTOU-closing work. (When `action`
2070    // is Defer it is not Continue, so `exec_freeze`/`creation_trace` above
2071    // are already None — there is nothing to unwind here.)
2072    if let NotifAction::Defer(deferred) = action {
2073        if crate::freeze::requires_freeze_on_continue(nr)
2074            || crate::resource::requires_process_creation_tracking(&notif, fd, policy)
2075        {
2076            let _ = send_response(fd, notif.id, NotifAction::Errno(libc::EPERM));
2077            return;
2078        }
2079        match Arc::clone(defer_sem).try_acquire_owned() {
2080            Ok(permit) => spawn_deferred(fd, notif.id, deferred, permit),
2081            // Too many deferrals in flight: fail fast with EAGAIN rather than
2082            // blocking the loop or letting unbounded workers accrete.
2083            Err(_) => {
2084                let _ = send_response(fd, notif.id, NotifAction::Errno(libc::EAGAIN));
2085            }
2086        }
2087        return;
2088    }
2089
2090    // Ignore error — child may have exited between recv and response.
2091    let exec_continued = exec_freeze.is_some() && matches!(action, NotifAction::Continue);
2092    let send_result = send_response(fd, notif.id, action);
2093
2094    if let Some(trace) = creation_trace {
2095        if send_result.is_ok() {
2096            match crate::resource::finish_process_creation_tracking(trace).await {
2097                Ok(true) => {}
2098                Ok(false) => {
2099                    crate::resource::rollback_fork_count(&ctx.resource).await;
2100                }
2101                Err(e) => {
2102                    crate::resource::rollback_fork_count(&ctx.resource).await;
2103                    eprintln!(
2104                        "sandlock: process-creation tracking completion failed for pid {}: {}",
2105                        notif.pid, e
2106                    );
2107                }
2108            }
2109        } else {
2110            crate::resource::rollback_fork_count(&ctx.resource).await;
2111            crate::resource::abort_process_creation_tracking(trace).await;
2112        }
2113    }
2114
2115    if let Some(freeze) = exec_freeze {
2116        if exec_continued && send_result.is_ok() {
2117            crate::freeze::detach_peers(&freeze.peer_tids);
2118        } else {
2119            crate::freeze::detach_all(&freeze);
2120        }
2121    }
2122}
2123
2124// ============================================================
2125// Main supervisor loop
2126// ============================================================
2127
2128/// Async event loop that processes seccomp notifications.
2129///
2130/// Runs until the notification fd is closed (child exits or filter is removed).
2131///
2132/// `pending_handlers` are user-supplied syscall handlers registered after all
2133/// builtin handlers.  For the default behaviour without any custom handlers
2134/// pass an empty `Vec`.
2135pub async fn supervisor(
2136    notif_fd: OwnedFd,
2137    ctx: Arc<super::ctx::SupervisorCtx>,
2138    pending_handlers: Vec<(i64, std::sync::Arc<dyn super::dispatch::Handler>)>,
2139    startup: tokio::sync::oneshot::Sender<io::Result<()>>,
2140) {
2141    // Register the notif fd with the Tokio IO driver so we can wait for
2142    // readiness via epoll instead of a dedicated blocking thread.
2143    let async_fd = match tokio::io::unix::AsyncFd::with_interest(
2144        notif_fd,
2145        tokio::io::Interest::READABLE,
2146    ) {
2147        Ok(fd) => fd,
2148        Err(err) => {
2149            let _ = startup.send(Err(err));
2150            return;
2151        }
2152    };
2153    let fd = async_fd.get_ref().as_raw_fd();
2154
2155    // Build the dispatch table once at startup.
2156    let dispatch_table = Arc::new(super::dispatch::build_dispatch_table(
2157        &ctx.policy,
2158        &ctx.resource,
2159        &ctx,
2160        pending_handlers,
2161    ));
2162
2163    // Try to enable sync wakeup (Linux 6.7+, ignore error on older kernels).
2164    try_set_sync_wakeup(fd);
2165
2166    // The IO driver has the fd registered; subsequent block_on cycles
2167    // can resume this task and pick up readiness events. Tell the
2168    // caller it is safe to release the child.
2169    let _ = startup.send(Ok(()));
2170
2171    // Periodic sweep as a defensive backstop in case pidfd-based
2172    // lifecycle cleanup misses an entry (e.g. pidfd_open failed for a
2173    // child on an old kernel, or its watcher panicked). At 5 minutes
2174    // this is cheap enough to leave on; the primary cleanup path is
2175    // still per-child pidfd readiness in `spawn_pid_watcher`.
2176    let gc = tokio::spawn(process_index_gc(Arc::clone(&ctx.processes)));
2177
2178    // Bounds the number of in-flight deferred handler futures (see
2179    // `DEFER_MAX_INFLIGHT`). Shared across all notifications this supervisor
2180    // processes.
2181    let defer_sem = Arc::new(tokio::sync::Semaphore::new(DEFER_MAX_INFLIGHT));
2182
2183    // Edge-triggered drain: each `readable().await` returns once per
2184    // epoll edge, then we drain the kernel queue via `probe_notif_fd`
2185    // until empty. The drain is necessary because tokio's AsyncFd is
2186    // edge-triggered and `recv_notif` does not signal "would block",
2187    // so a burst of arrivals between two `readable().await` calls
2188    // would coalesce into a single wake event.
2189    //
2190    // Notifications are processed sequentially (not spawned) to avoid
2191    // mutex contention between concurrent handlers.
2192    'outer: loop {
2193        let mut ready = match async_fd.readable().await {
2194            Ok(r) => r,
2195            Err(_) => break 'outer,
2196        };
2197        ready.clear_ready();
2198        drop(ready);
2199
2200        loop {
2201            match probe_notif_fd(fd) {
2202                NotifFdState::Pending => {
2203                    let notif = match recv_notif(fd) {
2204                        Ok(n) => n,
2205                        Err(e) if e.raw_os_error() == Some(libc::EINTR) => continue,
2206                        Err(_) => break 'outer,
2207                    };
2208                    handle_notification(notif, &ctx, &dispatch_table, fd, &defer_sem).await;
2209                }
2210                NotifFdState::Empty => break,
2211                NotifFdState::Terminal => break 'outer,
2212            }
2213        }
2214    }
2215
2216    gc.abort();
2217}
2218
2219/// Periodic sweep that drops `ProcessIndex` entries for exited PIDs.
2220/// Per-process state hangs off these entries via `Arc`, so dropping
2221/// them releases everything in one step.
2222async fn process_index_gc(processes: Arc<super::state::ProcessIndex>) {
2223    let interval = std::time::Duration::from_secs(300);
2224    loop {
2225        tokio::time::sleep(interval).await;
2226        if processes.len() == 0 {
2227            continue;
2228        }
2229        processes.prune_dead();
2230    }
2231}
2232
2233/// Spawn a per-child task that awaits the pidfd becoming readable
2234/// (process exit) and then runs unified cleanup across every
2235/// per-process supervisor map.
2236///
2237/// The watcher *owns* the pidfd via `AsyncFd<OwnedFd>` — the kernel
2238/// fd stays alive for as long as tokio's IO driver has it registered,
2239/// and is closed exactly once when the watcher task ends. This avoids
2240/// a TOCTOU where dropping the fd from a separate map could let a
2241/// recycled fd be deregistered from epoll.
2242pub(crate) fn spawn_pid_watcher(
2243    ctx: Arc<super::ctx::SupervisorCtx>,
2244    key: super::state::PidKey,
2245    pidfd: std::os::unix::io::OwnedFd,
2246) {
2247    tokio::spawn(async move {
2248        let async_fd = match tokio::io::unix::AsyncFd::with_interest(
2249            pidfd,
2250            tokio::io::Interest::READABLE,
2251        ) {
2252            Ok(f) => f,
2253            Err(_) => {
2254                // AsyncFd registration failed (extremely unusual);
2255                // fall back to immediate cleanup so we don't leak the
2256                // index entry. The OwnedFd we passed in is consumed
2257                // by `with_interest`'s Err return and will close on
2258                // drop here.
2259                cleanup_pid(&ctx, key).await;
2260                return;
2261            }
2262        };
2263        // pidfd becomes readable when the process exits; we don't
2264        // read any data, so `readable()` is just an await point.
2265        let _ = async_fd.readable().await;
2266        cleanup_pid(&ctx, key).await;
2267        // async_fd drops here, closing the pidfd.
2268    });
2269}
2270
2271/// Drop the supervisor's per-process state for `key`. With every
2272/// per-process map living inside `PerProcessState` (owned by
2273/// `ProcessIndex`), this is a single unregister — the entry's `Arc`
2274/// drops here, and remaining clones held by in-flight handlers will
2275/// drop with their tasks, freeing `PerProcessState` automatically.
2276pub(crate) async fn cleanup_pid(ctx: &super::ctx::SupervisorCtx, key: super::state::PidKey) {
2277    ctx.processes.unregister(key);
2278}
2279
2280// ============================================================
2281// Tests
2282// ============================================================
2283
2284#[cfg(test)]
2285mod tests {
2286    use super::*;
2287    use std::os::unix::io::FromRawFd;
2288
2289    fn gettid() -> u32 {
2290        (unsafe { libc::syscall(libc::SYS_gettid) }) as u32
2291    }
2292
2293    // ---- plan_exec_rewrite ----
2294
2295    /// Fake child memory: whole zero-filled pages at FAKE_BASE, so the
2296    /// page-chunked readers never run off a mapped page mid-scan and a
2297    /// zero word naturally terminates a pointer array.
2298    const FAKE_BASE: u64 = 0x10000;
2299    const FD: &[u8] = b"/proc/self/fd/3\0"; // 16 bytes
2300
2301    fn put(mem: &mut [u8], addr: u64, bytes: &[u8]) {
2302        let off = (addr - FAKE_BASE) as usize;
2303        mem[off..off + bytes.len()].copy_from_slice(bytes);
2304    }
2305
2306    fn put_ptrs(mem: &mut [u8], addr: u64, ptrs: &[u64]) {
2307        for (i, &p) in ptrs.iter().enumerate() {
2308            put(mem, addr + 8 * i as u64, &p.to_ne_bytes());
2309        }
2310    }
2311
2312    fn plan(
2313        mem: &[u8],
2314        path_ptr: u64,
2315        argv_ptr: u64,
2316        envp_ptr: u64,
2317    ) -> Result<ExecRewritePlan, NotifError> {
2318        let mut read = |addr: u64, len: usize| {
2319            let off = addr
2320                .checked_sub(FAKE_BASE)
2321                .ok_or_else(|| NotifError::Supervisor("read below fake memory".into()))?
2322                as usize;
2323            mem.get(off..off + len)
2324                .map(|s| s.to_vec())
2325                .ok_or_else(|| NotifError::Supervisor("read past fake memory".into()))
2326        };
2327        plan_exec_rewrite(&mut read, path_ptr, FD, argv_ptr, envp_ptr)
2328    }
2329
2330    #[test]
2331    fn exec_rewrite_plain_when_no_string_overlaps() {
2332        let mut mem = vec![0u8; 4096];
2333        put(&mut mem, FAKE_BASE, b"./m\0");
2334        put(&mut mem, FAKE_BASE + 0x200, b"prog\0");
2335        put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE + 0x200]);
2336        let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, 0).unwrap();
2337        assert_eq!(p.buf, FD);
2338        assert!(p.patches.is_empty());
2339    }
2340
2341    #[test]
2342    fn exec_rewrite_relocates_aliased_argv0() {
2343        // The common shell case: execve path and argv[0] are the same buffer.
2344        let mut mem = vec![0u8; 4096];
2345        put(&mut mem, FAKE_BASE, b"./m\0");
2346        put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE]);
2347        let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, 0).unwrap();
2348        assert_eq!(p.buf, [FD, b"./m\0".as_slice()].concat());
2349        assert_eq!(p.patches, vec![(FAKE_BASE + 0x800, FAKE_BASE + 16)]);
2350    }
2351
2352    #[test]
2353    fn exec_rewrite_relocates_packed_argv1() {
2354        // argv[1] sits right after the path string, inside the fd-path
2355        // window; both strings must survive.
2356        let mut mem = vec![0u8; 4096];
2357        put(&mut mem, FAKE_BASE, b"./echo\0EXEC_OK\0");
2358        put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE, FAKE_BASE + 7]);
2359        let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, 0).unwrap();
2360        assert_eq!(p.buf, [FD, b"./echo\0EXEC_OK\0".as_slice()].concat());
2361        assert_eq!(
2362            p.patches,
2363            vec![
2364                (FAKE_BASE + 0x800, FAKE_BASE + 16),
2365                (FAKE_BASE + 0x808, FAKE_BASE + 23),
2366            ]
2367        );
2368    }
2369
2370    #[test]
2371    fn exec_rewrite_relocates_string_reaching_window_from_below() {
2372        // The path pointer aims into the tail of argv[0] ("echo" inside
2373        // "/bin/echo"): the whole string must be relocated.
2374        let mut mem = vec![0u8; 4096];
2375        put(&mut mem, FAKE_BASE, b"/bin/echo\0");
2376        put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE]);
2377        let p = plan(&mem, FAKE_BASE + 5, FAKE_BASE + 0x800, 0).unwrap();
2378        assert_eq!(p.buf, [FD, b"/bin/echo\0".as_slice()].concat());
2379        assert_eq!(p.patches, vec![(FAKE_BASE + 0x800, FAKE_BASE + 5 + 16)]);
2380    }
2381
2382    #[test]
2383    fn exec_rewrite_skips_terminated_string_below_window() {
2384        let mut mem = vec![0u8; 4096];
2385        put(&mut mem, FAKE_BASE, b"a\0");
2386        put(&mut mem, FAKE_BASE + 8, b"./m\0");
2387        put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE]);
2388        let p = plan(&mem, FAKE_BASE + 8, FAKE_BASE + 0x800, 0).unwrap();
2389        assert_eq!(p.buf, FD);
2390        assert!(p.patches.is_empty());
2391    }
2392
2393    #[test]
2394    fn exec_rewrite_relocates_envp_string() {
2395        let mut mem = vec![0u8; 4096];
2396        put(&mut mem, FAKE_BASE, b"./m\0K=V\0");
2397        put(&mut mem, FAKE_BASE + 0x200, b"prog\0");
2398        put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE + 0x200]);
2399        put_ptrs(&mut mem, FAKE_BASE + 0x900, &[FAKE_BASE + 4]);
2400        let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, FAKE_BASE + 0x900).unwrap();
2401        assert_eq!(p.buf, [FD, b"K=V\0".as_slice()].concat());
2402        assert_eq!(p.patches, vec![(FAKE_BASE + 0x900, FAKE_BASE + 16)]);
2403    }
2404
2405    #[test]
2406    fn exec_rewrite_window_growth_pulls_in_later_string() {
2407        // argv[1] starts past the fd path but inside the window once the
2408        // relocated argv[0] extends it.
2409        let mut mem = vec![0u8; 4096];
2410        put(&mut mem, FAKE_BASE, b"./m\0");
2411        put(&mut mem, FAKE_BASE + 18, b"Z\0");
2412        put_ptrs(&mut mem, FAKE_BASE + 0x800, &[FAKE_BASE, FAKE_BASE + 18]);
2413        let p = plan(&mem, FAKE_BASE, FAKE_BASE + 0x800, 0).unwrap();
2414        assert_eq!(p.buf, [FD, b"./m\0".as_slice(), b"Z\0".as_slice()].concat());
2415        assert_eq!(
2416            p.patches,
2417            vec![
2418                (FAKE_BASE + 0x800, FAKE_BASE + 16),
2419                (FAKE_BASE + 0x808, FAKE_BASE + 20),
2420            ]
2421        );
2422    }
2423
2424    #[test]
2425    fn exec_rewrite_rejects_pointer_array_inside_window() {
2426        // The arrays cannot be moved (their addresses live in registers), so
2427        // a layout where the rewrite would smash them must fail closed.
2428        let mut mem = vec![0u8; 4096];
2429        put(&mut mem, FAKE_BASE, b"./m\0");
2430        put_ptrs(&mut mem, FAKE_BASE + 8, &[FAKE_BASE + 0x200]);
2431        put(&mut mem, FAKE_BASE + 0x200, b"prog\0");
2432        assert!(plan(&mem, FAKE_BASE, FAKE_BASE + 8, 0).is_err());
2433    }
2434
2435    #[test]
2436    fn exec_rewrite_null_arrays() {
2437        let mut mem = vec![0u8; 4096];
2438        put(&mut mem, FAKE_BASE, b"./m\0");
2439        let p = plan(&mem, FAKE_BASE, 0, 0).unwrap();
2440        assert_eq!(p.buf, FD);
2441        assert!(p.patches.is_empty());
2442    }
2443
2444    #[test]
2445    fn inject_failure_response_denies_not_continues() {
2446        // When fd injection fails, the supervisor must fail closed: deny the
2447        // syscall instead of letting it continue unmediated against the host
2448        // path (which would silently bypass chroot/file confinement).
2449        let resp = inject_failure_resp(123);
2450        assert_eq!(resp.id, 123);
2451        assert_eq!(
2452            resp.flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE,
2453            0,
2454            "fd-injection failure must not respond with CONTINUE"
2455        );
2456        assert_ne!(resp.error, 0, "fd-injection failure must be a denial");
2457        assert_eq!(resp.error, -libc::EACCES);
2458    }
2459
2460    #[test]
2461    fn held_gate_no_decision_denies() {
2462        use crate::policy_fn::Verdict;
2463        // A held syscall whose policy_fn gate times out or whose channel closes
2464        // (received == None) must fail closed: deny, not allow the syscall.
2465        assert!(matches!(resolve_held_gate(None), Some(Verdict::Deny)));
2466    }
2467
2468    #[test]
2469    fn held_gate_passes_through_callback_verdict() {
2470        use crate::policy_fn::Verdict;
2471        // A real verdict from the callback is forwarded unchanged.
2472        assert!(matches!(
2473            resolve_held_gate(Some(Verdict::Allow)),
2474            Some(Verdict::Allow)
2475        ));
2476        assert!(matches!(
2477            resolve_held_gate(Some(Verdict::Deny)),
2478            Some(Verdict::Deny)
2479        ));
2480        assert!(matches!(
2481            resolve_held_gate(Some(Verdict::DenyWith(13))),
2482            Some(Verdict::DenyWith(13))
2483        ));
2484    }
2485
2486    #[test]
2487    fn tgid_of_main_thread_is_own_pid() {
2488        // The main thread's tid equals the process pid, and its Tgid is the pid.
2489        assert_eq!(tgid_of(gettid()), Some(std::process::id()));
2490    }
2491
2492    #[test]
2493    fn tgid_of_worker_thread_resolves_to_process() {
2494        // A non-leader thread's Tgid is the process pid, not its own tid.
2495        let (tid_tx, tid_rx) = std::sync::mpsc::channel();
2496        let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
2497        let h = std::thread::spawn(move || {
2498            tid_tx.send(gettid()).unwrap();
2499            done_rx.recv().ok(); // stay alive until the test has read /proc
2500        });
2501        let worker_tid = tid_rx.recv().unwrap();
2502        let pid = std::process::id();
2503        assert_ne!(worker_tid, pid, "worker tid must differ from pid");
2504        assert_eq!(tgid_of(worker_tid), Some(pid));
2505        done_tx.send(()).ok();
2506        h.join().unwrap();
2507    }
2508
2509    #[test]
2510    fn dup_fd_from_pid_handles_worker_thread_fd() {
2511        use std::os::unix::io::AsRawFd;
2512        // Open an fd in a non-leader worker thread, then duplicate it by that
2513        // thread's tid. Exercises the tid->process pidfd resolution end to end
2514        // (PIDFD_THREAD on >=6.9, the /proc Tgid fallback on older kernels).
2515        let (info_tx, info_rx) = std::sync::mpsc::channel();
2516        let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
2517        let h = std::thread::spawn(move || {
2518            let f = std::fs::File::open("/dev/null").unwrap();
2519            info_tx.send((gettid(), f.as_raw_fd())).unwrap();
2520            done_rx.recv().ok();
2521            drop(f);
2522        });
2523        let (worker_tid, fd) = info_rx.recv().unwrap();
2524        let dup = dup_fd_from_pid(worker_tid, fd);
2525        done_tx.send(()).ok();
2526        h.join().unwrap();
2527        assert!(dup.is_ok(), "dup_fd_from_pid for a worker-thread fd failed: {:?}", dup.err());
2528    }
2529
2530    #[test]
2531    fn read_child_cstr_returns_none_for_null_addr_or_zero_max_len() {
2532        // Smoke: addr == 0 short-circuits without touching the child.
2533        assert!(read_child_cstr(-1, 0, 0, 0, 4096).is_none());
2534        // max_len == 0 also short-circuits.
2535        assert!(read_child_cstr(-1, 0, 0, 0xdeadbeef, 0).is_none());
2536    }
2537
2538    #[test]
2539    fn test_notif_action_debug() {
2540        // Ensure all variants implement Debug.
2541        let _ = format!("{:?}", NotifAction::Continue);
2542        let _ = format!("{:?}", NotifAction::Errno(1));
2543        let _ = format!("{:?}", NotifAction::InjectFd { srcfd: 3, targetfd: 4 });
2544        // Use a real fd (dup'd from stderr) so OwnedFd can safely close it.
2545        let test_fd = unsafe { OwnedFd::from_raw_fd(libc::dup(2)) };
2546        let _ = format!("{:?}", NotifAction::InjectFdSend { srcfd: test_fd, newfd_flags: 0 });
2547        let _ = format!("{:?}", NotifAction::ReturnValue(42));
2548        let _ = format!("{:?}", NotifAction::Hold);
2549        let _ = format!("{:?}", NotifAction::Kill { sig: 9, pgid: 1 });
2550        let _ = format!("{:?}", NotifAction::defer(async { NotifAction::Continue }));
2551    }
2552
2553    #[tokio::test]
2554    async fn deferred_future_need_not_be_sync() {
2555        // A deferred future may capture Send-but-not-Sync state across an
2556        // await. `Cell` is Send but never Sync; holding it across `.await`
2557        // makes the future !Sync. Only `Send` is required (the supervisor
2558        // moves the future to a worker, never shares it by reference).
2559        use std::cell::Cell;
2560        let action = NotifAction::defer(async move {
2561            let counter = Cell::new(0);
2562            counter.set(counter.get() + 41);
2563            tokio::task::yield_now().await; // hold the !Sync Cell across await
2564            NotifAction::ReturnValue(counter.get() + 1)
2565        });
2566        let NotifAction::Defer(d) = action else { panic!("expected Defer") };
2567        assert!(matches!(d.run().await, NotifAction::ReturnValue(42)));
2568    }
2569
2570    #[tokio::test]
2571    async fn deferred_runs_to_its_terminal_action() {
2572        // A Defer carries a future; running it yields the deferred decision.
2573        let action = NotifAction::defer(async { NotifAction::ReturnValue(7) });
2574        let NotifAction::Defer(deferred) = action else {
2575            panic!("defer() must construct a NotifAction::Defer");
2576        };
2577        assert!(matches!(deferred.run().await, NotifAction::ReturnValue(7)));
2578    }
2579
2580    #[tokio::test(start_paused = true)]
2581    async fn deferred_times_out_to_eio() {
2582        // A deferred future that exceeds its limit must fail closed (EIO) so
2583        // the trapped child gets a definite response instead of parking
2584        // forever (and leaking its DEFER_MAX_INFLIGHT slot).
2585        let slow = Deferred::new(async {
2586            tokio::time::sleep(std::time::Duration::from_secs(60)).await;
2587            NotifAction::ReturnValue(7)
2588        });
2589        let action = run_deferred_within(slow, std::time::Duration::from_secs(1)).await;
2590        assert!(matches!(action, NotifAction::Errno(e) if e == libc::EIO));
2591    }
2592
2593    #[tokio::test(start_paused = true)]
2594    async fn deferred_within_limit_passes_through() {
2595        // A future that resolves within the limit returns its terminal action.
2596        let fast = Deferred::new(async { NotifAction::ReturnValue(7) });
2597        let action = run_deferred_within(fast, std::time::Duration::from_secs(1)).await;
2598        assert!(matches!(action, NotifAction::ReturnValue(7)));
2599    }
2600
2601    #[test]
2602    fn finalize_deferred_collapses_nested_defer_to_eio() {
2603        // A deferred future that itself resolves to Defer is a bug: collapse
2604        // to EIO so the trapped child is never wedged waiting for a response.
2605        let nested = NotifAction::defer(async { NotifAction::Continue });
2606        assert!(matches!(finalize_deferred(nested), NotifAction::Errno(e) if e == libc::EIO));
2607        // Non-nested terminal actions pass through unchanged.
2608        assert!(matches!(finalize_deferred(NotifAction::Continue), NotifAction::Continue));
2609        assert!(matches!(
2610            finalize_deferred(NotifAction::ReturnValue(3)),
2611            NotifAction::ReturnValue(3)
2612        ));
2613    }
2614
2615    #[test]
2616    fn content_memfd_roundtrips_content() {
2617        use std::io::Read;
2618        let fd = content_memfd(b"hello world", true).expect("content_memfd");
2619        // The fd is rewound to offset 0, so a plain read returns the content.
2620        let mut f = std::fs::File::from(fd);
2621        let mut buf = String::new();
2622        f.read_to_string(&mut buf).unwrap();
2623        assert_eq!(buf, "hello world");
2624    }
2625
2626    #[test]
2627    fn content_memfd_sealed_applies_write_seal() {
2628        let fd = content_memfd(b"data", true).expect("content_memfd");
2629        let seals = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GET_SEALS) };
2630        assert!(seals >= 0, "F_GET_SEALS failed");
2631        assert!(
2632            seals & libc::F_SEAL_WRITE != 0,
2633            "expected F_SEAL_WRITE on a sealed memfd, got {seals:#x}"
2634        );
2635    }
2636
2637    #[test]
2638    fn content_memfd_unsealed_has_no_write_seal() {
2639        let fd = content_memfd(b"data", false).expect("content_memfd");
2640        let seals = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GET_SEALS) };
2641        assert!(seals >= 0, "F_GET_SEALS failed");
2642        assert_eq!(
2643            seals & libc::F_SEAL_WRITE,
2644            0,
2645            "unsealed memfd must not carry a write seal, got {seals:#x}"
2646        );
2647    }
2648
2649    #[test]
2650    fn inject_bytes_produces_sealed_cloexec_injectfdsend() {
2651        use std::io::Read;
2652        match NotifAction::inject_bytes(b"payload") {
2653            NotifAction::InjectFdSend { srcfd, newfd_flags } => {
2654                assert_eq!(newfd_flags, libc::O_CLOEXEC as u32);
2655                let seals = unsafe { libc::fcntl(srcfd.as_raw_fd(), libc::F_GET_SEALS) };
2656                assert!(seals & libc::F_SEAL_WRITE != 0, "inject_bytes must seal");
2657                let mut f = std::fs::File::from(srcfd);
2658                let mut buf = String::new();
2659                f.read_to_string(&mut buf).unwrap();
2660                assert_eq!(buf, "payload");
2661            }
2662            other => panic!("expected InjectFdSend, got {other:?}"),
2663        }
2664    }
2665
2666    #[test]
2667    fn test_network_state_new() {
2668        let ns = super::super::state::NetworkState::new();
2669        assert!(matches!(ns.tcp_policy, NetworkPolicy::Unrestricted));
2670        assert!(matches!(ns.udp_policy, NetworkPolicy::Unrestricted));
2671        assert!(matches!(ns.icmp_policy, NetworkPolicy::Unrestricted));
2672        assert!(ns.port_map.bound_ports.is_empty());
2673    }
2674
2675    #[test]
2676    fn test_time_random_state_new() {
2677        let tr = super::super::state::TimeRandomState::new(None, None);
2678        assert!(tr.time_offset.is_none());
2679        assert!(tr.random_state.is_none());
2680    }
2681
2682    #[test]
2683    fn test_resource_state_new() {
2684        let rs = super::super::state::ResourceState::new(1024 * 1024, 10);
2685        assert_eq!(rs.mem_used, 0);
2686        assert_eq!(rs.max_memory_bytes, 1024 * 1024);
2687        assert_eq!(rs.max_processes, 10);
2688        assert!(!rs.hold_forks);
2689        assert!(rs.held_notif_ids.is_empty());
2690    }
2691
2692    #[test]
2693    fn test_process_vm_readv_self() {
2694        let data: u64 = 0xDEADBEEF_CAFEBABE;
2695        let addr = &data as *const u64 as u64;
2696        let pid = std::process::id();
2697        let result = read_child_mem_vm(pid, addr, 8);
2698        assert!(result.is_ok());
2699        let bytes = result.unwrap();
2700        let read_val = u64::from_ne_bytes(bytes[..8].try_into().unwrap());
2701        assert_eq!(read_val, 0xDEADBEEF_CAFEBABE);
2702    }
2703
2704    #[test]
2705    fn test_process_vm_writev_self() {
2706        let mut data: u64 = 0;
2707        let addr = &mut data as *mut u64 as u64;
2708        let pid = std::process::id();
2709        let payload = 0x1234567890ABCDEFu64.to_ne_bytes();
2710        let result = write_child_mem_vm(pid, addr, &payload);
2711        assert!(result.is_ok());
2712        assert_eq!(data, 0x1234567890ABCDEF);
2713    }
2714
2715    /// The force-write path (`/proc/<pid>/mem`, FOLL_FORCE) must overwrite a
2716    /// read-only page where `process_vm_writev` (`write_child_mem_vm`) refuses.
2717    /// A read-only private page stands in for the `.rodata` path literal a child
2718    /// hands to chdir/execve — the exact case the chroot/cow rewrite sites hit.
2719    #[test]
2720    fn write_child_mem_proc_forces_past_readonly_page() {
2721        const PAGE: usize = 4096;
2722        let orig = b"/some/rodata/path\0";
2723        let newb = b"/proc/self/fd/7\0\0"; // fits within the page, near orig's len
2724
2725        let addr = unsafe {
2726            libc::mmap(
2727                std::ptr::null_mut(),
2728                PAGE,
2729                libc::PROT_READ | libc::PROT_WRITE,
2730                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
2731                -1,
2732                0,
2733            )
2734        };
2735        assert_ne!(addr, libc::MAP_FAILED, "mmap failed");
2736        unsafe { std::ptr::copy_nonoverlapping(orig.as_ptr(), addr as *mut u8, orig.len()) };
2737
2738        // Flip the page read-only: a normal store would now SIGSEGV, and
2739        // process_vm_writev honors the protection and fails.
2740        assert_eq!(
2741            unsafe { libc::mprotect(addr, PAGE, libc::PROT_READ) },
2742            0,
2743            "mprotect PROT_READ failed"
2744        );
2745
2746        let pid = std::process::id();
2747        let uaddr = addr as u64;
2748
2749        assert!(
2750            write_child_mem_vm(pid, uaddr, newb).is_err(),
2751            "process_vm_writev must fail on a read-only page"
2752        );
2753
2754        // FOLL_FORCE via /proc/<pid>/mem copies-on-write past the RO page.
2755        write_child_mem_proc(pid, uaddr, newb)
2756            .expect("force-write through /proc/pid/mem must succeed on a read-only page");
2757
2758        // The write landed at the target address (page is still RO-mapped, so a
2759        // plain read is fine and reflects the COW'd contents).
2760        let got = unsafe { std::slice::from_raw_parts(addr as *const u8, newb.len()) };
2761        assert_eq!(got, newb, "forced write must be visible at the target address");
2762
2763        unsafe { libc::munmap(addr, PAGE) };
2764    }
2765
2766    #[test]
2767    fn denylist_blocks_matching_cidr_allows_rest() {
2768        use crate::network::IpCidr;
2769        let policy = NetworkPolicy::DenyList {
2770            cidrs: vec![(IpCidr::parse("10.0.0.0/8").unwrap(), PortAllow::Any)],
2771            any_ip_ports: HashSet::new(),
2772            deny_all: false,
2773        };
2774        assert!(!policy.allows("10.1.2.3".parse().unwrap(), 443)); // denied
2775        assert!(policy.allows("8.8.8.8".parse().unwrap(), 443));   // allowed
2776    }
2777
2778    #[test]
2779    fn denylist_blocks_any_ip_port() {
2780        let mut ports = HashSet::new();
2781        ports.insert(25u16);
2782        let policy = NetworkPolicy::DenyList {
2783            cidrs: Vec::new(),
2784            any_ip_ports: ports,
2785            deny_all: false,
2786        };
2787        assert!(!policy.allows("8.8.8.8".parse().unwrap(), 25)); // denied
2788        assert!(policy.allows("8.8.8.8".parse().unwrap(), 80));  // allowed
2789    }
2790
2791    #[test]
2792    fn denylist_specific_ports_on_cidr() {
2793        use crate::network::IpCidr;
2794        let mut ports = HashSet::new();
2795        ports.insert(443u16);
2796        let policy = NetworkPolicy::DenyList {
2797            cidrs: vec![(IpCidr::parse("1.2.3.4/32").unwrap(), PortAllow::Specific(ports))],
2798            any_ip_ports: HashSet::new(),
2799            deny_all: false,
2800        };
2801        assert!(!policy.allows("1.2.3.4".parse().unwrap(), 443)); // denied
2802        assert!(policy.allows("1.2.3.4".parse().unwrap(), 80));   // allowed
2803    }
2804
2805    #[test]
2806    fn allowlist_permits_matching_cidr_only() {
2807        use crate::network::IpCidr;
2808        let mut ports = HashSet::new();
2809        ports.insert(80u16);
2810        let policy = NetworkPolicy::AllowList {
2811            per_ip: HashMap::new(),
2812            cidrs: vec![(IpCidr::parse("10.0.0.0/8").unwrap(), PortAllow::Specific(ports))],
2813            any_ip_ports: HashSet::new(),
2814        };
2815        assert!(policy.allows("10.1.2.3".parse().unwrap(), 80));   // in range, port ok
2816        assert!(!policy.allows("10.1.2.3".parse().unwrap(), 443)); // in range, wrong port
2817        assert!(!policy.allows("8.8.8.8".parse().unwrap(), 80));   // out of range
2818    }
2819
2820    #[test]
2821    fn allowlist_cidr_all_ports() {
2822        use crate::network::IpCidr;
2823        let policy = NetworkPolicy::AllowList {
2824            per_ip: HashMap::new(),
2825            cidrs: vec![(IpCidr::parse("192.168.0.0/16").unwrap(), PortAllow::Any)],
2826            any_ip_ports: HashSet::new(),
2827        };
2828        assert!(policy.allows("192.168.5.5".parse().unwrap(), 9999)); // any port in range
2829        assert!(!policy.allows("10.0.0.1".parse().unwrap(), 9999));   // out of range
2830    }
2831
2832    #[test]
2833    fn denylist_blocks_v4_mapped_ipv6_form_of_denied_ip() {
2834        // A dual-stack AF_INET6 socket connecting to ::ffff:169.254.169.254
2835        // reaches 169.254.169.254 over IPv4, so a v4 deny rule must match
2836        // the mapped form or the denylist is bypassable.
2837        use crate::network::IpCidr;
2838        let policy = NetworkPolicy::DenyList {
2839            cidrs: vec![(IpCidr::parse("169.254.169.254").unwrap(), PortAllow::Any)],
2840            any_ip_ports: HashSet::new(),
2841            deny_all: false,
2842        };
2843        assert!(!policy.allows("::ffff:169.254.169.254".parse().unwrap(), 80));
2844    }
2845
2846    #[test]
2847    fn allowlist_accepts_v4_mapped_ipv6_form_of_allowed_ip() {
2848        // Mirror of the deny case: the mapped form is the same destination,
2849        // so a v4 allow entry must match it (per_ip and cidr paths both).
2850        use crate::network::IpCidr;
2851        let mut per_ip = HashMap::new();
2852        per_ip.insert("1.2.3.4".parse().unwrap(), PortAllow::Any);
2853        let policy = NetworkPolicy::AllowList {
2854            per_ip,
2855            cidrs: vec![(IpCidr::parse("10.0.0.0/8").unwrap(), PortAllow::Any)],
2856            any_ip_ports: HashSet::new(),
2857        };
2858        assert!(policy.allows("::ffff:1.2.3.4".parse().unwrap(), 443));
2859        assert!(policy.allows("::ffff:10.1.2.3".parse().unwrap(), 443));
2860        assert!(!policy.allows("::ffff:8.8.8.8".parse().unwrap(), 443));
2861    }
2862}