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        match self {
277            NetworkPolicy::Unrestricted => true,
278            NetworkPolicy::AllowList { per_ip, cidrs, any_ip_ports } => {
279                if any_ip_ports.contains(&port) {
280                    return true;
281                }
282                match per_ip.get(&ip) {
283                    Some(PortAllow::Any) => return true,
284                    Some(PortAllow::Specific(s)) if s.contains(&port) => return true,
285                    _ => {}
286                }
287                for (net, allowed) in cidrs {
288                    if net.contains(ip) {
289                        match allowed {
290                            PortAllow::Any => return true,
291                            PortAllow::Specific(s) => {
292                                if s.contains(&port) {
293                                    return true;
294                                }
295                            }
296                        }
297                    }
298                }
299                false
300            }
301            NetworkPolicy::DenyList { cidrs, any_ip_ports, deny_all } => {
302                if *deny_all {
303                    return false;
304                }
305                if any_ip_ports.contains(&port) {
306                    return false;
307                }
308                for (net, denied) in cidrs {
309                    if net.contains(ip) {
310                        match denied {
311                            PortAllow::Any => return false,
312                            PortAllow::Specific(s) => {
313                                if s.contains(&port) {
314                                    return false;
315                                }
316                            }
317                        }
318                    }
319                }
320                true
321            }
322        }
323    }
324}
325
326/// Check if a path-bearing notification targets a denied path.
327///
328/// For two-path syscalls (renameat2, linkat), checks both source and
329/// destination paths — a denied file must not be linked, renamed, or
330/// overwritten.
331///
332/// Each resolved path is checked both as-is (lexical normalization) and
333/// after following symlinks via `canonicalize`.  This prevents bypass via
334/// pre-existing symlinks, relative symlinks, or symlink chains that
335/// ultimately resolve to a denied path.
336pub(crate) fn is_path_denied_for_notif(
337    policy_fn_state: &super::state::PolicyFnState,
338    notif: &SeccompNotif,
339    notif_fd: RawFd,
340) -> bool {
341    if let Some(path) = resolve_path_for_notif(notif, notif_fd) {
342        if is_denied_with_symlink_resolve(policy_fn_state, &path) {
343            return true;
344        }
345    }
346    // For two-path syscalls, also check the second (destination) path.
347    if let Some(path) = resolve_second_path_for_notif(notif, notif_fd) {
348        if is_denied_with_symlink_resolve(policy_fn_state, &path) {
349            return true;
350        }
351    }
352    false
353}
354
355/// Check a path against denied entries, also resolving symlinks.
356///
357/// First checks the lexical path, then `canonicalize`s to follow symlinks
358/// and checks the real path.  This catches pre-existing symlinks, relative
359/// symlinks, and symlink chains that resolve to a denied file.
360fn is_denied_with_symlink_resolve(
361    policy_fn_state: &super::state::PolicyFnState,
362    path: &str,
363) -> bool {
364    // Check the literal (lexically normalized) path first.
365    if policy_fn_state.is_path_denied(path) {
366        return true;
367    }
368    // Follow symlinks and re-check against denied entries.
369    if let Ok(real) = std::fs::canonicalize(path) {
370        if policy_fn_state.is_path_denied(&real.to_string_lossy()) {
371            return true;
372        }
373        // Identity check: catches a denied file reached via a hardlink or a
374        // pre-existing alias, where the resolved path itself is not denied
375        // but the file identity is. Best-effort here (path-based precheck);
376        // the race-free authority is the fd identity check in the on-behalf
377        // open.
378        if let Some(id) = super::state::file_id_of_path(&real.to_string_lossy()) {
379            if policy_fn_state.is_id_denied(&id) {
380                return true;
381            }
382        }
383    }
384    false
385}
386
387/// `RESOLVE_NO_MAGICLINKS` — forbid `/proc` magic-link redirection during
388/// on-behalf resolution while still following ordinary symlinks the way the
389/// child's own open would.
390const RESOLVE_NO_MAGICLINKS: u64 = 0x02;
391
392/// Kernel `struct open_how` for `openat2`.
393#[repr(C)]
394struct OpenHow {
395    flags: u64,
396    mode: u64,
397    resolve: u64,
398}
399
400fn last_errno(fallback: i32) -> i32 {
401    io::Error::last_os_error().raw_os_error().unwrap_or(fallback)
402}
403
404/// `openat2` relative to `dirfd`. Returns an owned fd or the errno.
405fn openat2_at(dirfd: RawFd, path: &std::ffi::CStr, flags: u64, mode: u64, resolve: u64)
406    -> Result<OwnedFd, i32>
407{
408    use std::os::unix::io::FromRawFd;
409    let how = OpenHow { flags, mode, resolve };
410    let fd = unsafe {
411        libc::syscall(
412            arch::SYS_OPENAT2,
413            dirfd,
414            path.as_ptr(),
415            &how as *const OpenHow,
416            std::mem::size_of::<OpenHow>(),
417        )
418    } as i32;
419    if fd < 0 {
420        Err(last_errno(libc::ENOENT))
421    } else {
422        Ok(unsafe { OwnedFd::from_raw_fd(fd) })
423    }
424}
425
426/// Capture an `O_PATH` fd to the directory the child's open resolves against,
427/// taken from the child's own view so a concurrent `chdir`/dirfd swap cannot
428/// move the resolution base after we read it.
429fn open_base_dir(pid: u32, dirfd: i64) -> Result<OwnedFd, i32> {
430    use std::os::unix::io::FromRawFd;
431    if dirfd as i32 == libc::AT_FDCWD {
432        let cwd = std::ffi::CString::new(format!("/proc/{}/cwd", pid)).map_err(|_| libc::EINVAL)?;
433        let fd = unsafe {
434            libc::open(cwd.as_ptr(), libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC)
435        };
436        if fd < 0 {
437            return Err(last_errno(libc::EACCES));
438        }
439        Ok(unsafe { OwnedFd::from_raw_fd(fd) })
440    } else {
441        dup_fd_from_pid(pid, dirfd as i32).map_err(|_| libc::EBADF)
442    }
443}
444
445/// Real path of an open fd via its `/proc/self/fd` magic link.
446fn realpath_of_fd(fd: RawFd) -> Option<std::path::PathBuf> {
447    std::fs::read_link(format!("/proc/self/fd/{}", fd)).ok()
448}
449
450
451fn path_under_any(path: &std::path::Path, list: &[std::path::PathBuf]) -> bool {
452    list.iter().any(|p| path.starts_with(p))
453}
454
455/// Decide whether `realpath` may be opened with `flags` under the deny set
456/// and the (conservative) grant lists. Returns `Some(errno)` to refuse,
457/// `None` to allow. Never over-allows relative to the configured grants: a
458/// path outside every grant is refused, so this can only be stricter than
459/// Landlock, never looser (an over-deny is a functional gap, an over-allow
460/// would be an escape).
461fn deny_open_verdict(
462    realpath: &std::path::Path,
463    flags: u64,
464    policy: &NotifPolicy,
465    pfs: &super::state::PolicyFnState,
466) -> Option<i32> {
467    if pfs.is_path_denied(&realpath.to_string_lossy()) {
468        return Some(libc::EACCES);
469    }
470    let acc = flags as i32 & libc::O_ACCMODE;
471    let is_write = acc == libc::O_WRONLY
472        || acc == libc::O_RDWR
473        || (flags & libc::O_TRUNC as u64) != 0
474        || (flags & libc::O_CREAT as u64) != 0;
475    let allowed = if is_write {
476        path_under_any(realpath, &policy.chroot_writable)
477    } else {
478        path_under_any(realpath, &policy.chroot_readable)
479            || path_under_any(realpath, &policy.chroot_writable)
480    };
481    if allowed { None } else { Some(libc::EACCES) }
482}
483
484/// open/openat/openat2 argument layout, normalized across the spellings.
485struct OpenArgs {
486    dirfd: i64,
487    path_ptr: u64,
488    flags: u64,
489    mode: u64,
490    /// `openat2` `resolve` flags (`RESOLVE_*`); 0 for `open`/`openat`.
491    resolve: u64,
492}
493
494/// Decode the open arguments. `openat2` carries flags/mode/resolve inside a
495/// `struct open_how` in child memory, so its decode reads child memory and
496/// can fail; `None` means "could not decode" and the caller soft-falls-through
497/// (the kernel's own re-read fails the same way).
498fn decode_open_args(notif: &SeccompNotif, notif_fd: RawFd) -> Option<OpenArgs> {
499    let a = &notif.data.args;
500    let nr = notif.data.nr as i64;
501    if nr == libc::SYS_openat {
502        Some(OpenArgs { dirfd: a[0] as i64, path_ptr: a[1], flags: a[2], mode: a[3], resolve: 0 })
503    } else if nr == arch::SYS_OPENAT2 {
504        // openat2(dirfd, pathname, struct open_how *how, size_t size),
505        // open_how = { u64 flags, u64 mode, u64 resolve }.
506        let how_ptr = a[2];
507        let want = (a[3] as usize).min(std::mem::size_of::<OpenHow>());
508        if how_ptr == 0 || want < 16 {
509            return None; // need at least flags + mode
510        }
511        let bytes = read_child_mem(notif_fd, notif.id, notif.pid, how_ptr, want).ok()?;
512        if bytes.len() < 16 {
513            return None;
514        }
515        let flags = u64::from_ne_bytes(bytes[0..8].try_into().ok()?);
516        let mode = u64::from_ne_bytes(bytes[8..16].try_into().ok()?);
517        let resolve = if bytes.len() >= 24 {
518            u64::from_ne_bytes(bytes[16..24].try_into().ok()?)
519        } else {
520            0
521        };
522        Some(OpenArgs { dirfd: a[0] as i64, path_ptr: a[1], flags, mode, resolve })
523    } else {
524        // legacy open(path, flags, mode) — AT_FDCWD implied.
525        Some(OpenArgs { dirfd: libc::AT_FDCWD as i64, path_ptr: a[0], flags: a[1], mode: a[2], resolve: 0 })
526    }
527}
528
529/// Wrap a freshly opened raw fd into an `InjectFdSend`, honoring the child's
530/// `O_CLOEXEC` request. Ownership of `raw_fd` moves into the action.
531fn inject_open_result(raw_fd: i32, flags: u64) -> NotifAction {
532    use std::os::unix::io::FromRawFd;
533    if raw_fd < 0 {
534        return NotifAction::Errno(last_errno(libc::EACCES));
535    }
536    let owned = unsafe { OwnedFd::from_raw_fd(raw_fd) };
537    let newfd_flags = if flags & libc::O_CLOEXEC as u64 != 0 {
538        libc::O_CLOEXEC as u32
539    } else {
540        0
541    };
542    NotifAction::InjectFdSend { srcfd: owned, newfd_flags }
543}
544
545/// Existing-file branch: vet the pinned inode behind `probe`, then reopen it
546/// race-free via its `/proc/self/fd` magic link with the child's real access
547/// mode (binds to the inode, not the original path).
548fn reopen_existing_on_behalf(
549    probe: OwnedFd,
550    flags: u64,
551    policy: &NotifPolicy,
552    pfs: &super::state::PolicyFnState,
553) -> NotifAction {
554    // File exists. Refuse O_CREAT|O_EXCL the way the kernel would.
555    if (flags & libc::O_CREAT as u64) != 0 && (flags & libc::O_EXCL as u64) != 0 {
556        return NotifAction::Errno(libc::EEXIST);
557    }
558    let realpath = match realpath_of_fd(probe.as_raw_fd()) {
559        Some(p) => p,
560        None => return NotifAction::Errno(libc::EACCES),
561    };
562    if let Some(errno) = deny_open_verdict(&realpath, flags, policy, pfs) {
563        return NotifAction::Errno(errno);
564    }
565    // Identity check on the pinned file: a denied file reached via a hardlink,
566    // a rename to a non-denied name, or a pre-existing alias has a realpath
567    // that is not denied, but its handle identity is. Race-free — this is the
568    // exact file the child will receive.
569    if let Some(id) = super::state::file_id_of_fd(probe.as_raw_fd()) {
570        if pfs.is_id_denied(&id) {
571            return NotifAction::Errno(libc::EACCES);
572        }
573    }
574    // Resolution-only flags are stripped from the reopen.
575    let reopen_flags =
576        flags as i32 & !(libc::O_CREAT | libc::O_EXCL | libc::O_PATH | libc::O_NOFOLLOW);
577    let proc_path = match std::ffi::CString::new(format!("/proc/self/fd/{}", probe.as_raw_fd())) {
578        Ok(c) => c,
579        Err(_) => return NotifAction::Errno(libc::EIO),
580    };
581    let fd = unsafe { libc::open(proc_path.as_ptr(), reopen_flags) };
582    inject_open_result(fd, flags)
583}
584
585/// O_CREAT branch: resolve the parent directory race-free, vet the would-be
586/// target, then create the leaf inside that pinned parent (the dir inode is
587/// fixed, only the leaf name is appended).
588fn create_new_on_behalf(
589    base: &OwnedFd,
590    path: &str,
591    flags: u64,
592    mode: u64,
593    resolve: u64,
594    policy: &NotifPolicy,
595    pfs: &super::state::PolicyFnState,
596) -> NotifAction {
597    let p = std::path::Path::new(path);
598    let file_name = match p.file_name() {
599        Some(f) => f,
600        None => return NotifAction::Errno(libc::ENOENT),
601    };
602    let parent = p.parent().unwrap_or(std::path::Path::new("."));
603    let parent_str = match parent.to_str() {
604        Some("") | None => ".",
605        Some(s) => s,
606    };
607    let c_parent = match std::ffi::CString::new(parent_str) {
608        Ok(c) => c,
609        Err(_) => return NotifAction::Errno(libc::EINVAL),
610    };
611    let parent_fd = match openat2_at(
612        base.as_raw_fd(),
613        &c_parent,
614        (libc::O_PATH | libc::O_DIRECTORY | libc::O_CLOEXEC) as u64,
615        0,
616        RESOLVE_NO_MAGICLINKS | resolve,
617    ) {
618        Ok(f) => f,
619        Err(e) => return NotifAction::Errno(e),
620    };
621    let parent_real = match realpath_of_fd(parent_fd.as_raw_fd()) {
622        Some(p) => p,
623        None => return NotifAction::Errno(libc::EACCES),
624    };
625    if let Some(errno) = deny_open_verdict(&parent_real.join(file_name), flags, policy, pfs) {
626        return NotifAction::Errno(errno);
627    }
628    let c_name = match std::ffi::CString::new(file_name.as_encoded_bytes()) {
629        Ok(c) => c,
630        Err(_) => return NotifAction::Errno(libc::EINVAL),
631    };
632    let create_flags = flags as i32 & !(libc::O_PATH | libc::O_NOFOLLOW);
633    let fd = unsafe { libc::openat(parent_fd.as_raw_fd(), c_name.as_ptr(), create_flags, mode) };
634    inject_open_result(fd, flags)
635}
636
637/// Perform `openat`/`open` on behalf of the child, race-free, when a deny is
638/// active. Resolves once (pinning the inode), enforces deny + grant on the
639/// pinned target, then hands the child an fd to that exact inode via
640/// `InjectFdSend`. Returns `Continue` only when no allow/deny decision was
641/// made on content we resolved (unreadable path / no allowlist configured),
642/// matching the precheck's existing soft fall-through.
643fn on_behalf_open_for_deny(
644    notif: &SeccompNotif,
645    policy: &NotifPolicy,
646    pfs: &super::state::PolicyFnState,
647    notif_fd: RawFd,
648) -> NotifAction {
649    // No allowlist configured (Landlock is not allowlisting the filesystem):
650    // there is no grant to check against, so taking over the open could only
651    // wrongly deny. Leave it to the existing precheck/kernel path.
652    if policy.chroot_readable.is_empty() && policy.chroot_writable.is_empty() {
653        return NotifAction::Continue;
654    }
655
656    let OpenArgs { dirfd, path_ptr, flags, mode, resolve } =
657        match decode_open_args(notif, notif_fd) {
658            Some(a) => a,
659            None => return NotifAction::Continue, // kernel's re-read fails the same way
660        };
661
662    let path = match read_child_cstr(notif_fd, notif.id, notif.pid, path_ptr, 4096) {
663        Some(p) => p,
664        None => return NotifAction::Continue, // kernel's re-read fails the same way
665    };
666    let c_path = match std::ffi::CString::new(path.clone()) {
667        Ok(c) => c,
668        Err(_) => return NotifAction::Errno(libc::EINVAL),
669    };
670    let base = match open_base_dir(notif.pid, dirfd) {
671        Ok(b) => b,
672        Err(e) => return NotifAction::Errno(e),
673    };
674
675    // Side-effect-free probe; mirror the child's no-follow intent for the
676    // final component and any `openat2` RESOLVE_* flags it requested.
677    let probe_flags = (libc::O_PATH | libc::O_CLOEXEC) as u64 | (flags & libc::O_NOFOLLOW as u64);
678    match openat2_at(base.as_raw_fd(), &c_path, probe_flags, 0, RESOLVE_NO_MAGICLINKS | resolve) {
679        Ok(probe) => reopen_existing_on_behalf(probe, flags, policy, pfs),
680        Err(errno) if errno == libc::ENOENT && (flags & libc::O_CREAT as u64) != 0 => {
681            create_new_on_behalf(&base, &path, flags, mode, resolve, policy, pfs)
682        }
683        Err(errno) => NotifAction::Errno(errno),
684    }
685}
686
687/// Read the thread-group leader (Tgid) of a thread from `/proc/<tid>/status`.
688fn tgid_of(tid: u32) -> Option<u32> {
689    let status = std::fs::read_to_string(format!("/proc/{}/status", tid)).ok()?;
690    status
691        .lines()
692        .find_map(|l| l.strip_prefix("Tgid:").and_then(|r| r.trim().parse().ok()))
693}
694
695/// Duplicate a file descriptor from an arbitrary process (by PID/TID) into the supervisor.
696///
697/// `pidfd_getfd` (Linux 5.6+) needs a pidfd for the owning *process*. All threads
698/// of a process share one fd table, so the process's pidfd dups any thread's fd:
699/// `pidfd_open(pid, 0)` gives it directly when `pid` is a thread-group leader,
700/// otherwise we resolve the leader via `Tgid` in `/proc/<pid>/status` and open
701/// that. The triggering thread is frozen on the seccomp notification, so its
702/// Tgid cannot race with pid reuse. Works on any kernel with `pidfd_getfd`.
703pub(crate) fn dup_fd_from_pid(pid: u32, target_fd: i32) -> io::Result<OwnedFd> {
704    use crate::sys::syscall::{pidfd_getfd, pidfd_open};
705    let pidfd = pidfd_open(pid, 0).or_else(|e| match tgid_of(pid) {
706        Some(tgid) if tgid != pid => pidfd_open(tgid, 0),
707        _ => Err(e),
708    })?;
709    pidfd_getfd(&pidfd, target_fd, 0)
710}
711
712// ============================================================
713// NotifPolicy — policy for the notification supervisor
714// ============================================================
715
716/// Policy for the notification supervisor.
717pub struct NotifPolicy {
718    pub max_memory_bytes: u64,
719    pub max_processes: u32,
720    pub has_memory_limit: bool,
721    /// A **network destination policy** is active: a `net_allow` allowlist, a
722    /// `net_deny` denylist, an HTTP ACL, or a live `policy_fn` (i.e.
723    /// `network_destination_policy`). Despite the historical singular framing
724    /// this is *not* only an allowlist. When set, the on-behalf path is the
725    /// IP-level enforcer for connect/sendto/sendmsg and those handlers must
726    /// never `Continue`; when clear, the syscalls are trapped only to gate
727    /// named `AF_UNIX` sockets and IP destinations are returned to the kernel
728    /// so Landlock/BPF govern them.
729    pub has_net_destination_policy: bool,
730    /// `--net-deny-bind` is active: trap `bind()` and register the on-behalf
731    /// handler so denied TCP ports can be refused (independent of the
732    /// connect-side `has_net_destination_policy`).
733    pub has_bind_denylist: bool,
734    /// Named (pathname) `AF_UNIX` connect gate. When true, `connect()` to a
735    /// named unix socket whose path is not covered by an fs-write grant is
736    /// denied with EACCES. Landlock has no access right for unix-socket
737    /// connect, so the seccomp layer closes the escape; abstract sockets are
738    /// handled by `LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET` instead.
739    pub has_unix_fs_gate: bool,
740    pub has_random_seed: bool,
741    pub has_time_start: bool,
742    /// Argv-safety gate: the supervisor must freeze every task that
743    /// could mutate argv before any consumer reads it. True when
744    /// `policy_fn` is active or when a handler is bound to
745    /// execve/execveat (such handlers can call `read_child_mem`).
746    /// Also gates ptrace fork-event tracking so `ProcessIndex` is
747    /// complete when the freeze enumerates it.
748    pub argv_safety_required: bool,
749    pub time_offset: i64,
750    pub num_cpus: Option<u32>,
751    pub port_remap: bool,
752    pub cow_enabled: bool,
753    pub chroot_root: Option<std::path::PathBuf>,
754    /// Virtual paths allowed for reading under chroot (original user-specified paths).
755    pub chroot_readable: Vec<std::path::PathBuf>,
756    /// Virtual paths allowed for writing under chroot (original user-specified paths).
757    pub chroot_writable: Vec<std::path::PathBuf>,
758    /// Virtual paths explicitly denied under chroot.
759    pub chroot_denied: Vec<std::path::PathBuf>,
760    /// Mount mappings: (virtual_path, host_path) pairs.
761    pub chroot_mounts: Vec<(std::path::PathBuf, std::path::PathBuf)>,
762    /// Virtual paths of mounts that are read-only: writes are denied even
763    /// though the path is mounted (and therefore readable). Used for the host
764    /// procfs mount and OCI `ro` bind mounts so a writable rootfs cannot make
765    /// e.g. `/proc/sys/*` writable.
766    pub chroot_mount_ro: Vec<std::path::PathBuf>,
767    pub deterministic_dirs: bool,
768    pub virtual_hostname: Option<String>,
769    pub has_http_acl: bool,
770    /// Synthetic `/etc/hosts` served to the sandbox. Always populated:
771    /// `openat("/etc/hosts")` returns a memfd with this content so the
772    /// host's on-disk `/etc/hosts` never leaks in. The content is the
773    /// loopback base plus any concrete hostnames resolved from `net_allow`.
774    pub virtual_etc_hosts: String,
775    /// User-declared trust-bundle paths to splice the MITM CA into.
776    pub ca_inject_paths: Vec<std::path::PathBuf>,
777    /// Active MITM CA public cert (PEM bytes) to inject. `Some` only when
778    /// HTTPS MITM is active (BYO or generated).
779    pub ca_inject_pem: Option<std::sync::Arc<Vec<u8>>>,
780}
781
782impl NotifPolicy {
783    /// Whether an IP-family `connect()` must be handled on-behalf by the
784    /// supervisor, given the destination's loopback-ness.
785    ///
786    /// This is the connect-side form of the invariant the `sendto`/`sendmsg`
787    /// handlers already follow: with [`Self::has_net_destination_policy`] the
788    /// on-behalf path is the IP-level enforcer and must never `Continue`;
789    /// without it there is nothing to enforce and the syscall is returned to
790    /// the kernel. `connect()` adds one wrinkle over datagram sends —
791    /// `port_remap` rewrites the destination, but only for **loopback** — so
792    /// that is the sole extra reason to supervise an otherwise-unpoliced IP
793    /// connect. When this returns `false`, the connect was trapped purely to
794    /// gate named `AF_UNIX` sockets, and deferring to the kernel lets the
795    /// child's own Landlock `CONNECT_TCP` rules govern it (deny-all for an
796    /// empty `net_allow`); handling it on-behalf would run it in the
797    /// unconfined supervisor and bypass that decision.
798    pub(crate) fn ip_connect_supervised(&self, dest_is_loopback: bool) -> bool {
799        self.has_net_destination_policy || (self.port_remap && dest_is_loopback)
800    }
801}
802
803// ============================================================
804// Low-level ioctl helpers
805// ============================================================
806
807/// Receive a seccomp notification from the kernel.
808/// ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV, &notif)
809fn recv_notif(fd: RawFd) -> io::Result<SeccompNotif> {
810    let mut notif: SeccompNotif = unsafe { std::mem::zeroed() };
811    let ret = unsafe {
812        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_RECV as libc::c_ulong, &mut notif as *mut _)
813    };
814    if ret < 0 {
815        Err(io::Error::last_os_error())
816    } else {
817        Ok(notif)
818    }
819}
820
821/// Result of a non-blocking probe on the seccomp notif fd.
822enum NotifFdState {
823    /// At least one INIT-state notification is queued. `recv_notif`
824    /// will return without blocking.
825    Pending,
826    /// No notifications and no terminal flags. Wait for the next
827    /// epoll edge before probing again.
828    Empty,
829    /// `POLLHUP`/`POLLERR`/`POLLNVAL` set, or `poll(2)` itself failed:
830    /// filter has been released or the fd is invalid. The supervisor
831    /// should exit; subsequent waits would busy-spin because epoll
832    /// keeps reporting the fd ready.
833    Terminal,
834}
835
836/// Non-blocking probe of the seccomp notif fd.
837///
838/// `SECCOMP_IOCTL_NOTIF_RECV` ignores `O_NONBLOCK` and calls
839/// `wait_event_interruptible` unconditionally (kernel/seccomp.c
840/// `seccomp_notify_recv`). So `recv_notif` cannot be invoked
841/// speculatively to detect an empty queue. This helper uses
842/// `poll(timeout=0)` as a non-blocking predictor: if POLLIN is set
843/// the kernel will hand us a notification without blocking; if a
844/// terminal flag is set the fd will keep waking AsyncFd until the
845/// supervisor exits.
846fn probe_notif_fd(fd: RawFd) -> NotifFdState {
847    let mut pfd = libc::pollfd {
848        fd,
849        events: libc::POLLIN,
850        revents: 0,
851    };
852    let r = unsafe { libc::poll(&mut pfd, 1, 0) };
853    if r > 0 && (pfd.revents & libc::POLLIN) != 0 {
854        return NotifFdState::Pending;
855    }
856    if r < 0 || (pfd.revents & (libc::POLLHUP | libc::POLLERR | libc::POLLNVAL)) != 0 {
857        return NotifFdState::Terminal;
858    }
859    NotifFdState::Empty
860}
861
862/// Send a response with SECCOMP_USER_NOTIF_FLAG_CONTINUE.
863fn respond_continue(fd: RawFd, id: u64) -> io::Result<()> {
864    let resp = SeccompNotifResp {
865        id,
866        val: 0,
867        error: 0,
868        flags: SECCOMP_USER_NOTIF_FLAG_CONTINUE,
869    };
870    send_resp_raw(fd, &resp)
871}
872
873/// Send a response that returns -1 with the given errno.
874fn respond_errno(fd: RawFd, id: u64, errno: i32) -> io::Result<()> {
875    let resp = SeccompNotifResp {
876        id,
877        val: 0,
878        error: -errno,
879        flags: 0,
880    };
881    send_resp_raw(fd, &resp)
882}
883
884/// Send a response with a synthetic return value.
885fn respond_value(fd: RawFd, id: u64, val: i64) -> io::Result<()> {
886    let resp = SeccompNotifResp {
887        id,
888        val,
889        error: 0,
890        flags: 0,
891    };
892    send_resp_raw(fd, &resp)
893}
894
895/// Fail-closed response used when fd injection fails.
896///
897/// Denies the syscall with `EACCES` rather than letting it continue: a
898/// `SECCOMP_USER_NOTIF_FLAG_CONTINUE` here would let the child's original
899/// syscall run unmediated against the host path, silently bypassing
900/// chroot/file confinement. (Regression guard: this must never be a CONTINUE
901/// response.)
902fn inject_failure_resp(id: u64) -> SeccompNotifResp {
903    SeccompNotifResp {
904        id,
905        val: 0,
906        error: -libc::EACCES,
907        flags: 0,
908    }
909}
910
911/// Inject a file descriptor into the child process using SECCOMP_ADDFD_FLAG_SEND.
912///
913/// Uses the SEND flag to atomically inject the fd and respond to the syscall.
914/// The ioctl return value is the fd number assigned in the child process.
915/// After this call, no additional SECCOMP_IOCTL_NOTIF_SEND is needed.
916fn inject_fd_and_send(fd: RawFd, id: u64, srcfd: RawFd, newfd_flags: u32) -> io::Result<i32> {
917    let addfd = SeccompNotifAddfd {
918        id,
919        flags: SECCOMP_ADDFD_FLAG_SEND,
920        srcfd: srcfd as u32,
921        newfd: 0,   // ignored when SECCOMP_ADDFD_FLAG_SETFD is not set
922        newfd_flags,
923    };
924    let ret = unsafe {
925        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, &addfd as *const _)
926    };
927    if ret < 0 {
928        Err(io::Error::last_os_error())
929    } else {
930        Ok(ret as i32)
931    }
932}
933
934/// Inject a file descriptor into the child process (without responding).
935/// ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD, &addfd)
936fn inject_fd(fd: RawFd, id: u64, srcfd: RawFd, targetfd: i32) -> io::Result<()> {
937    let addfd = SeccompNotifAddfd {
938        id,
939        flags: 0,
940        srcfd: srcfd as u32,
941        newfd: targetfd as u32,
942        newfd_flags: 0,
943    };
944    let ret = unsafe {
945        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ADDFD as libc::c_ulong, &addfd as *const _)
946    };
947    if ret < 0 {
948        Err(io::Error::last_os_error())
949    } else {
950        Ok(())
951    }
952}
953
954/// Raw ioctl to send a notification response.
955fn send_resp_raw(fd: RawFd, resp: &SeccompNotifResp) -> io::Result<()> {
956    let ret = unsafe {
957        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SEND as libc::c_ulong, resp as *const _)
958    };
959    if ret < 0 {
960        Err(io::Error::last_os_error())
961    } else {
962        Ok(())
963    }
964}
965
966/// Check whether a notification ID is still valid (TOCTOU guard).
967/// ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID, &id)
968pub(crate) fn id_valid(fd: RawFd, id: u64) -> io::Result<()> {
969    let ret = unsafe {
970        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_ID_VALID as libc::c_ulong, &id as *const _)
971    };
972    if ret < 0 {
973        Err(io::Error::last_os_error())
974    } else {
975        Ok(())
976    }
977}
978
979/// Try to enable sync wakeup (Linux 6.7+). Ignores errors.
980fn try_set_sync_wakeup(fd: RawFd) {
981    let flags: u64 = SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP as u64;
982    unsafe {
983        libc::ioctl(fd, SECCOMP_IOCTL_NOTIF_SET_FLAGS as libc::c_ulong, &flags as *const _);
984    }
985}
986
987// ============================================================
988// Child memory access helpers
989// ============================================================
990
991/// Read bytes from a child process via process_vm_readv (single syscall).
992fn read_child_mem_vm(pid: u32, addr: u64, len: usize) -> Result<Vec<u8>, NotifError> {
993    let mut buf = vec![0u8; len];
994    let local_iov = libc::iovec {
995        iov_base: buf.as_mut_ptr() as *mut libc::c_void,
996        iov_len: len,
997    };
998    let remote_iov = libc::iovec {
999        iov_base: addr as *mut libc::c_void,
1000        iov_len: len,
1001    };
1002    let ret = unsafe {
1003        libc::process_vm_readv(pid as i32, &local_iov, 1, &remote_iov, 1, 0)
1004    };
1005    if ret < 0 {
1006        Err(NotifError::ChildMemoryRead(io::Error::last_os_error()))
1007    } else {
1008        buf.truncate(ret as usize);
1009        Ok(buf)
1010    }
1011}
1012
1013/// Write bytes to a child process via process_vm_writev (single syscall).
1014fn write_child_mem_vm(pid: u32, addr: u64, data: &[u8]) -> Result<(), NotifError> {
1015    let local_iov = libc::iovec {
1016        iov_base: data.as_ptr() as *mut libc::c_void,
1017        iov_len: data.len(),
1018    };
1019    let remote_iov = libc::iovec {
1020        iov_base: addr as *mut libc::c_void,
1021        iov_len: data.len(),
1022    };
1023    let ret = unsafe {
1024        libc::process_vm_writev(pid as i32, &local_iov, 1, &remote_iov, 1, 0)
1025    };
1026    if ret < 0 {
1027        Err(NotifError::ChildMemoryRead(io::Error::last_os_error()))
1028    } else if (ret as usize) < data.len() {
1029        Err(NotifError::ChildMemoryRead(io::Error::new(
1030            io::ErrorKind::WriteZero,
1031            format!("short write: {} of {} bytes", ret, data.len()),
1032        )))
1033    } else {
1034        Ok(())
1035    }
1036}
1037
1038/// Read bytes from a child process via `process_vm_readv` with TOCTOU validation.
1039///
1040/// Calls `id_valid` before and after the read to ensure the notification is
1041/// still live (kernel did not abort or release the trapped syscall while the
1042/// supervisor was reading guest memory).
1043///
1044/// Public — used by downstream `Handler` implementations to read syscall
1045/// arguments that the kernel passes by pointer (paths in `openat`, buffers
1046/// in `write`/`writev`, etc.).
1047pub fn read_child_mem(
1048    notif_fd: RawFd,
1049    id: u64,
1050    pid: u32,
1051    addr: u64,
1052    len: usize,
1053) -> Result<Vec<u8>, NotifError> {
1054    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1055    let result = read_child_mem_vm(pid, addr, len)?;
1056    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1057    Ok(result)
1058}
1059
1060/// Read a NUL-terminated string from child memory without crossing unmapped
1061/// page boundaries in a single `process_vm_readv` call.
1062///
1063/// TOCTOU-safe — internally calls [`read_child_mem`], inheriting the
1064/// `id_valid` checks bracketing each `process_vm_readv` call.
1065///
1066/// Page-aware: reads up to a page boundary at a time and stops at the
1067/// first NUL byte, never crossing into unmapped memory.  Returns
1068/// `None` for `addr == 0`, `max_len == 0`, a read failure, or a string
1069/// that exceeds `max_len` without a NUL.
1070///
1071/// Public — used by downstream `Handler` implementations that read
1072/// path arguments from notifications (`openat`, `unlinkat`, `statx`,
1073/// `newfstatat`, etc.).
1074pub fn read_child_cstr(
1075    notif_fd: RawFd,
1076    id: u64,
1077    pid: u32,
1078    addr: u64,
1079    max_len: usize,
1080) -> Option<String> {
1081    if addr == 0 || max_len == 0 {
1082        return None;
1083    }
1084
1085    const PAGE_SIZE: u64 = 4096;
1086    let mut result = Vec::with_capacity(max_len.min(256));
1087    let mut cur = addr;
1088    while result.len() < max_len {
1089        let page_remaining = PAGE_SIZE - (cur % PAGE_SIZE);
1090        let remaining = max_len - result.len();
1091        let to_read = page_remaining.min(remaining as u64) as usize;
1092        let bytes = read_child_mem(notif_fd, id, pid, cur, to_read).ok()?;
1093        if let Some(nul) = bytes.iter().position(|&b| b == 0) {
1094            result.extend_from_slice(&bytes[..nul]);
1095            return String::from_utf8(result).ok();
1096        }
1097        result.extend_from_slice(&bytes);
1098        cur += to_read as u64;
1099    }
1100
1101    String::from_utf8(result).ok()
1102}
1103
1104/// Write bytes to a child process via `process_vm_writev` with TOCTOU validation.
1105///
1106/// Same TOCTOU contract as [`read_child_mem`].  Public for downstream
1107/// `Handler` implementations that synthesise syscall results into
1108/// guest memory (e.g. fake `getdents64` listings populated from a
1109/// virtual directory index, or synthesised `stat` buffers).
1110pub fn write_child_mem(
1111    notif_fd: RawFd,
1112    id: u64,
1113    pid: u32,
1114    addr: u64,
1115    data: &[u8],
1116) -> Result<(), NotifError> {
1117    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1118    write_child_mem_vm(pid, addr, data)?;
1119    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1120    Ok(())
1121}
1122
1123/// Write bytes to a child, forcing past read-only page protections.
1124///
1125/// [`write_child_mem`] uses `process_vm_writev`, which honors the target VMA's
1126/// protection bits and so returns `EFAULT` when the destination page is
1127/// read-only — e.g. a `.rodata` path literal a program hands to
1128/// `chdir`/`execve`. This variant writes through `/proc/<pid>/mem`, whose writes
1129/// use `FOLL_FORCE` and therefore copy-on-write past a read-only mapping (the
1130/// same mechanism a debugger uses to plant a breakpoint in read-only `.text`).
1131/// It handles writable and read-only destinations through one path, so the
1132/// argument-rewrite sites need no `EFAULT` fallback.
1133///
1134/// Use ONLY for rewriting an *input* path argument the child owns that the
1135/// kernel re-reads under `SECCOMP_USER_NOTIF_FLAG_CONTINUE` (the `chdir`/`execve`
1136/// path rewrites). Do NOT use it for syscall *output* buffers (`stat`,
1137/// `getdents`, `getcwd`, `readlink`, the `getsockname`/`recvfrom` source
1138/// address): if a child supplies a read-only output buffer the real syscall
1139/// would `EFAULT`, and faithful emulation must too — keep those on
1140/// [`write_child_mem`]. (`bind`/`connect` need nothing here: they are emulated
1141/// on-behalf on a duped fd and never rewrite the child's sockaddr.)
1142///
1143/// Opening `/proc/<pid>/mem` for write needs `PTRACE_MODE_ATTACH_FSCREDS` over
1144/// the child (no actual attach or stop): satisfied by the supervisor as the
1145/// child's same-uid parent under the common Yama scopes. Returns `Err` (so the
1146/// caller can still surface `EFAULT`) if the `mem` file can't be opened or
1147/// written — it never silently no-ops. TOCTOU-safe: brackets the write with
1148/// `id_valid` like [`write_child_mem`].
1149pub fn write_child_mem_force(
1150    notif_fd: RawFd,
1151    id: u64,
1152    pid: u32,
1153    addr: u64,
1154    data: &[u8],
1155) -> Result<(), NotifError> {
1156    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1157    write_child_mem_proc(pid, addr, data)?;
1158    id_valid(notif_fd, id).map_err(NotifError::Ioctl)?;
1159    Ok(())
1160}
1161
1162/// Write bytes to a process's memory via `/proc/<pid>/mem` (open + `pwrite`).
1163///
1164/// Inner helper for [`write_child_mem_force`]; the file offset is the target
1165/// virtual address. Writes here use `FOLL_FORCE`, so they copy-on-write past a
1166/// read-only destination page where [`write_child_mem_vm`] (`process_vm_writev`)
1167/// returns `EFAULT`.
1168fn write_child_mem_proc(pid: u32, addr: u64, data: &[u8]) -> Result<(), NotifError> {
1169    use std::os::unix::fs::FileExt;
1170    let mem = std::fs::OpenOptions::new()
1171        .write(true)
1172        .open(format!("/proc/{}/mem", pid))
1173        .map_err(NotifError::ChildMemoryWrite)?;
1174    mem.write_all_at(data, addr)
1175        .map_err(NotifError::ChildMemoryWrite)?;
1176    Ok(())
1177}
1178
1179// ============================================================
1180// Response dispatch
1181// ============================================================
1182
1183/// Dispatch a `NotifAction` to the appropriate low-level response function.
1184fn send_response(fd: RawFd, id: u64, action: NotifAction) -> io::Result<()> {
1185    match action {
1186        NotifAction::Continue => respond_continue(fd, id),
1187        NotifAction::Errno(errno) => respond_errno(fd, id, errno),
1188        NotifAction::InjectFd { srcfd, targetfd } => {
1189            inject_fd(fd, id, srcfd, targetfd)?;
1190            respond_continue(fd, id)
1191        }
1192        NotifAction::InjectFdSend { srcfd, newfd_flags } => {
1193            // SECCOMP_ADDFD_FLAG_SEND atomically injects the fd and responds.
1194            // No separate NOTIF_SEND needed after this.
1195            // On failure, deny (fail closed) rather than letting the original
1196            // syscall continue unmediated against the host path.
1197            // srcfd (OwnedFd) is dropped at end of this arm, closing the fd.
1198            match inject_fd_and_send(fd, id, srcfd.as_raw_fd(), newfd_flags) {
1199                Ok(_new_fd) => Ok(()),
1200                Err(_) => send_resp_raw(fd, &inject_failure_resp(id)),
1201            }
1202        }
1203        NotifAction::InjectFdSendTracked { srcfd, newfd_flags, on_success } => {
1204            match inject_fd_and_send(fd, id, srcfd.as_raw_fd(), newfd_flags) {
1205                Ok(new_fd) => {
1206                    (on_success.0)(new_fd);
1207                    Ok(())
1208                }
1209                Err(_) => send_resp_raw(fd, &inject_failure_resp(id)),
1210            }
1211        }
1212        NotifAction::ReturnValue(val) => respond_value(fd, id, val),
1213        NotifAction::Hold => Ok(()), // Don't send a response.
1214        NotifAction::Defer(_) => {
1215            // Defer is intercepted in `handle_notification` and never reaches
1216            // here on the normal path. If it ever does, fail closed with EIO
1217            // rather than dropping the future and wedging the child.
1218            debug_assert!(false, "Defer reached send_response; should be intercepted earlier");
1219            respond_errno(fd, id, libc::EIO)
1220        }
1221        NotifAction::Kill { sig, pgid } => {
1222            // Kill the entire process group, then return ENOMEM so the
1223            // seccomp notification is resolved (avoids a kernel warning).
1224            unsafe { libc::killpg(pgid, sig) };
1225            respond_errno(fd, id, ENOMEM)
1226        }
1227    }
1228}
1229
1230// ============================================================
1231// vDSO re-patching after exec
1232// ============================================================
1233
1234/// Re-patch the vDSO if the base address changed (e.g. after exec replaces it).
1235fn maybe_patch_vdso(pid: i32, procfs: &mut super::state::ProcfsState, policy: &NotifPolicy) {
1236    let base = match crate::vdso::find_vdso_base(pid) {
1237        Ok(addr) => addr,
1238        Err(_) => return,
1239    };
1240    if base == procfs.vdso_patched_addr {
1241        return; // already patched this vDSO
1242    }
1243    let time_offset = if policy.has_time_start { Some(policy.time_offset) } else { None };
1244    if crate::vdso::patch(pid, time_offset, policy.has_random_seed).is_ok() {
1245        procfs.vdso_patched_addr = base;
1246    }
1247}
1248
1249// ============================================================
1250// Policy event emission
1251// ============================================================
1252
1253/// Map a syscall number to a human-readable name for the policy callback.
1254fn syscall_name(nr: i64) -> &'static str {
1255    match nr {
1256        n if n == libc::SYS_openat => "openat",
1257        n if n == libc::SYS_connect => "connect",
1258        n if n == libc::SYS_sendto => "sendto",
1259        n if n == libc::SYS_sendmsg => "sendmsg",
1260        n if n == libc::SYS_sendmmsg => "sendmmsg",
1261        n if n == libc::SYS_bind => "bind",
1262        n if n == libc::SYS_clone => "clone",
1263        n if n == libc::SYS_clone3 => "clone3",
1264        n if Some(n) == arch::sys_vfork() => "vfork",
1265        n if Some(n) == arch::sys_fork() => "fork",
1266        n if n == libc::SYS_execve => "execve",
1267        n if n == libc::SYS_execveat => "execveat",
1268        n if n == libc::SYS_mmap => "mmap",
1269        n if n == libc::SYS_munmap => "munmap",
1270        n if n == libc::SYS_brk => "brk",
1271        n if n == libc::SYS_getrandom => "getrandom",
1272        n if n == libc::SYS_unlinkat => "unlinkat",
1273        n if n == libc::SYS_mkdirat => "mkdirat",
1274        _ => "unknown",
1275    }
1276}
1277
1278/// Map a syscall number to a high-level category.
1279fn syscall_category(nr: i64) -> crate::policy_fn::SyscallCategory {
1280    use crate::policy_fn::SyscallCategory;
1281    match nr {
1282        n if n == libc::SYS_openat || n == libc::SYS_unlinkat
1283            || n == libc::SYS_mkdirat || n == libc::SYS_renameat2
1284            || n == libc::SYS_symlinkat || n == libc::SYS_linkat
1285            || n == libc::SYS_fchmodat || n == libc::SYS_fchownat
1286            || n == libc::SYS_truncate || n == libc::SYS_readlinkat
1287            || n == libc::SYS_newfstatat || n == libc::SYS_statx
1288            || n == libc::SYS_faccessat || n == libc::SYS_getdents64
1289            || Some(n) == arch::sys_getdents() => SyscallCategory::File,
1290        n if n == libc::SYS_connect || n == libc::SYS_sendto
1291            || n == libc::SYS_sendmsg || n == libc::SYS_sendmmsg
1292            || n == libc::SYS_bind
1293            || n == libc::SYS_getsockname => SyscallCategory::Network,
1294        n if n == libc::SYS_clone || n == libc::SYS_clone3
1295            || Some(n) == arch::sys_vfork() || Some(n) == arch::sys_fork()
1296            || n == libc::SYS_execve || n == libc::SYS_execveat => SyscallCategory::Process,
1297        n if n == libc::SYS_mmap || n == libc::SYS_munmap
1298            || n == libc::SYS_brk || n == libc::SYS_mremap
1299            => SyscallCategory::Memory,
1300        _ => SyscallCategory::File, // default
1301    }
1302}
1303
1304/// Read the parent PID from /proc/{pid}/stat.
1305fn read_ppid(pid: u32) -> Option<u32> {
1306    let stat = std::fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?;
1307    // Format: "pid (comm) state ppid ..."
1308    // Find the closing ')' then split the rest
1309    let close_paren = stat.rfind(')')?;
1310    let rest = &stat[close_paren + 2..]; // skip ") "
1311    let fields: Vec<&str> = rest.split_whitespace().collect();
1312    // fields[0] = state, fields[1] = ppid
1313    fields.get(1)?.parse().ok()
1314}
1315
1316/// Read a NUL-terminated path from child memory (up to 256 bytes).
1317fn read_path_for_event(notif: &SeccompNotif, addr: u64, notif_fd: RawFd) -> Option<String> {
1318    if addr == 0 { return None; }
1319    let bytes = read_child_mem(notif_fd, notif.id, notif.pid, addr, 256).ok()?;
1320    let nul = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
1321    String::from_utf8(bytes[..nul].to_vec()).ok()
1322}
1323
1324fn normalize_path(path: &std::path::Path) -> String {
1325    use std::path::{Component, PathBuf};
1326
1327    let mut normalized = PathBuf::new();
1328    let absolute = path.is_absolute();
1329    if absolute {
1330        normalized.push("/");
1331    }
1332
1333    for component in path.components() {
1334        match component {
1335            Component::RootDir | Component::CurDir => {}
1336            Component::ParentDir => {
1337                normalized.pop();
1338            }
1339            Component::Normal(part) => normalized.push(part),
1340            Component::Prefix(_) => {}
1341        }
1342    }
1343
1344    if normalized.as_os_str().is_empty() {
1345        if absolute { "/".into() } else { ".".into() }
1346    } else {
1347        normalized.to_string_lossy().into_owned()
1348    }
1349}
1350
1351fn resolve_at_path_for_event(notif: &SeccompNotif, dirfd: i64, path: &str) -> Option<String> {
1352    use std::path::Path;
1353
1354    if Path::new(path).is_absolute() {
1355        return Some(normalize_path(Path::new(path)));
1356    }
1357
1358    let dirfd32 = dirfd as i32;
1359    let base = if dirfd32 == libc::AT_FDCWD {
1360        std::fs::read_link(format!("/proc/{}/cwd", notif.pid)).ok()?
1361    } else {
1362        std::fs::read_link(format!("/proc/{}/fd/{}", notif.pid, dirfd32)).ok()?
1363    };
1364
1365    Some(normalize_path(&base.join(path)))
1366}
1367
1368fn resolve_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Option<String> {
1369    let nr = notif.data.nr as i64;
1370    match nr {
1371        n if n == libc::SYS_openat || n == arch::SYS_OPENAT2 => {
1372            // openat(dirfd, pathname, flags, mode) and
1373            // openat2(dirfd, pathname, how, size) share (dirfd, pathname).
1374            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1375            resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1376        }
1377        n if Some(n) == arch::sys_open() || n == libc::SYS_execve => {
1378            let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1379            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1380        }
1381        n if n == libc::SYS_execveat => {
1382            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1383            resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1384        }
1385        // linkat(olddirfd, oldpath, newdirfd, newpath, flags)
1386        // Check the source (old) path — deny if it's a denied file being linked away.
1387        n if n == libc::SYS_linkat => {
1388            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1389            resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1390        }
1391        // renameat2(olddirfd, oldpath, newdirfd, newpath, flags)
1392        // Check the source (old) path — deny if a denied file is being renamed away.
1393        n if n == libc::SYS_renameat2 => {
1394            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1395            resolve_at_path_for_event(notif, notif.data.args[0] as i64, &path)
1396        }
1397        // symlinkat/symlink intentionally omitted: creating a symlink does
1398        // not access its target, so there is nothing to gate here. Any later
1399        // open through the symlink resolves to the real target and is denied
1400        // race-free on the open path (issue #111). See `on_behalf_open_for_deny`.
1401        // link(oldpath, newpath) — legacy, AT_FDCWD implied for both
1402        n if Some(n) == arch::sys_link() => {
1403            let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1404            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1405        }
1406        // rename(oldpath, newpath) — legacy, AT_FDCWD implied for both
1407        n if Some(n) == arch::sys_rename() => {
1408            let path = read_path_for_event(notif, notif.data.args[0], notif_fd)?;
1409            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1410        }
1411        _ => None,
1412    }
1413}
1414
1415/// Resolve the second (destination) path for two-path syscalls.
1416///
1417/// Returns `None` for syscalls that only have a single path argument.
1418fn resolve_second_path_for_notif(notif: &SeccompNotif, notif_fd: RawFd) -> Option<String> {
1419    let nr = notif.data.nr as i64;
1420    match nr {
1421        // renameat2(olddirfd, oldpath, newdirfd, newpath, flags)
1422        n if n == libc::SYS_renameat2 => {
1423            let path = read_path_for_event(notif, notif.data.args[3], notif_fd)?;
1424            resolve_at_path_for_event(notif, notif.data.args[2] as i64, &path)
1425        }
1426        // linkat(olddirfd, oldpath, newdirfd, newpath, flags)
1427        // Destination of a hardlink to a denied file should also be denied
1428        // (prevents overwriting a denied file via linkat).
1429        n if n == libc::SYS_linkat => {
1430            let path = read_path_for_event(notif, notif.data.args[3], notif_fd)?;
1431            resolve_at_path_for_event(notif, notif.data.args[2] as i64, &path)
1432        }
1433        // rename(oldpath, newpath) — legacy
1434        n if Some(n) == arch::sys_rename() => {
1435            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1436            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1437        }
1438        // link(oldpath, newpath) — legacy
1439        n if Some(n) == arch::sys_link() => {
1440            let path = read_path_for_event(notif, notif.data.args[1], notif_fd)?;
1441            resolve_at_path_for_event(notif, libc::AT_FDCWD as i64, &path)
1442        }
1443        _ => None,
1444    }
1445}
1446
1447/// Extract IP and port from a sockaddr in child memory.
1448fn read_sockaddr_for_event(notif: &SeccompNotif, addr: u64, len: usize, notif_fd: RawFd)
1449    -> (Option<std::net::IpAddr>, Option<u16>)
1450{
1451    if addr == 0 || len < 4 { return (None, None); }
1452    let bytes = match read_child_mem(notif_fd, notif.id, notif.pid, addr, len.min(128)) {
1453        Ok(b) => b,
1454        Err(_) => return (None, None),
1455    };
1456    if bytes.len() < 4 { return (None, None); }
1457    let family = u16::from_ne_bytes([bytes[0], bytes[1]]);
1458    let port = u16::from_be_bytes([bytes[2], bytes[3]]);
1459    let ip = match family as u32 {
1460        f if f == crate::sys::structs::AF_INET && bytes.len() >= 8 => {
1461            Some(std::net::IpAddr::V4(std::net::Ipv4Addr::new(
1462                bytes[4], bytes[5], bytes[6], bytes[7],
1463            )))
1464        }
1465        f if f == crate::sys::structs::AF_INET6 && bytes.len() >= 24 => {
1466            let mut addr = [0u8; 16];
1467            addr.copy_from_slice(&bytes[8..24]);
1468            Some(std::net::IpAddr::V6(std::net::Ipv6Addr::from(addr)))
1469        }
1470        _ => None,
1471    };
1472    (ip, if port > 0 { Some(port) } else { None })
1473}
1474
1475/// Read argv (NULL-terminated array of char* in child memory) for execve.
1476/// Capped at 64 entries × 256 bytes/entry as a safety bound.
1477fn read_argv_for_event(notif: &SeccompNotif, argv_ptr: u64, notif_fd: RawFd) -> Option<Vec<String>> {
1478    if argv_ptr == 0 { return None; }
1479    let mut args = Vec::new();
1480    let ptr_size = std::mem::size_of::<u64>();
1481
1482    for i in 0..64u64 {
1483        let ptr_addr = argv_ptr + i * ptr_size as u64;
1484        let ptr_bytes = read_child_mem(notif_fd, notif.id, notif.pid, ptr_addr, ptr_size).ok()?;
1485        let str_ptr = u64::from_ne_bytes(ptr_bytes[..8].try_into().ok()?);
1486        if str_ptr == 0 { break; } // NULL terminator
1487
1488        if let Some(s) = read_path_for_event(notif, str_ptr, notif_fd) {
1489            args.push(s);
1490        } else {
1491            break;
1492        }
1493    }
1494
1495    if args.is_empty() { None } else { Some(args) }
1496}
1497
1498/// Resolve a held syscall's policy_fn gate outcome into a verdict.
1499///
1500/// `received` is the verdict the callback sent, or `None` if the gate timed
1501/// out or its channel closed before a decision arrived. A held syscall is one
1502/// whose verdict matters (execve, connect, openat, ...); when no decision
1503/// arrives we fail closed and deny rather than letting the syscall proceed.
1504fn resolve_held_gate(
1505    received: Option<crate::policy_fn::Verdict>,
1506) -> Option<crate::policy_fn::Verdict> {
1507    match received {
1508        Some(v) => Some(v),
1509        None => Some(crate::policy_fn::Verdict::Deny),
1510    }
1511}
1512
1513/// Emit a syscall event to the policy_fn callback thread (if active).
1514/// Returns the callback's verdict for held syscalls.
1515async fn emit_policy_event(
1516    notif: &SeccompNotif,
1517    action: &NotifAction,
1518    policy_fn_state: &Arc<tokio::sync::Mutex<super::state::PolicyFnState>>,
1519    notif_fd: RawFd,
1520) -> Option<crate::policy_fn::Verdict> {
1521    let pfs = policy_fn_state.lock().await;
1522    let tx = match pfs.event_tx.as_ref() {
1523        Some(tx) => tx.clone(),
1524        None => return None,
1525    };
1526    drop(pfs);
1527
1528    let nr = notif.data.nr as i64;
1529    let denied = matches!(action, NotifAction::Errno(_));
1530    let name = syscall_name(nr);
1531    let category = syscall_category(nr);
1532    let parent_pid = read_ppid(notif.pid);
1533
1534    // Extract metadata based on syscall type.
1535    //
1536    // Path strings are deliberately NOT extracted: the kernel re-reads
1537    // user-memory pointers after Continue, so any path-string-based
1538    // decision is racy (issue #27). Path-based access control belongs
1539    // in static Landlock rules.
1540    //
1541    // argv IS extracted for allowed execve/execveat notifications:
1542    // the supervisor freezes every task in the sandbox (siblings +
1543    // peers) before this callback reads argv and keeps that freeze
1544    // through Continue, so the post-Continue re-read sees the same
1545    // memory we read here.
1546    //
1547    // Network fields are TOCTOU-safe because connect/sendto/bind are
1548    // performed on-behalf via pidfd_getfd; the kernel never re-reads
1549    // child memory for those syscalls.
1550    let mut host = None;
1551    let mut port = None;
1552    let mut size = None;
1553    let mut argv = None;
1554
1555    if !denied && (nr == libc::SYS_execve || nr == libc::SYS_execveat) {
1556        // execve(pathname, argv, envp):       args[1] = argv ptr
1557        // execveat(dirfd, pathname, argv, ..): args[2] = argv ptr
1558        let argv_ptr = if nr == libc::SYS_execveat {
1559            notif.data.args[2]
1560        } else {
1561            notif.data.args[1]
1562        };
1563        argv = read_argv_for_event(notif, argv_ptr, notif_fd);
1564    }
1565
1566    if nr == libc::SYS_connect || nr == libc::SYS_sendto || nr == libc::SYS_bind {
1567        // connect(fd, addr, addrlen): args[1]=addr, args[2]=len
1568        let addr_ptr = notif.data.args[1];
1569        let addr_len = notif.data.args[2] as usize;
1570        let (h, p) = read_sockaddr_for_event(notif, addr_ptr, addr_len, notif_fd);
1571        host = h;
1572        port = p;
1573    }
1574
1575    if nr == libc::SYS_mmap {
1576        // mmap(addr, length, ...): args[1] = length
1577        size = Some(notif.data.args[1]);
1578    }
1579
1580    let event = crate::policy_fn::SyscallEvent {
1581        syscall: name.to_string(),
1582        category,
1583        pid: notif.pid,
1584        parent_pid,
1585        host,
1586        port,
1587        size,
1588        argv,
1589        denied,
1590    };
1591
1592    // Hold syscalls where the callback's verdict matters.
1593    // The child is blocked until the callback returns.
1594    let is_held = nr == libc::SYS_execve || nr == libc::SYS_execveat
1595        || nr == libc::SYS_connect || nr == libc::SYS_sendto
1596        || nr == libc::SYS_bind || nr == libc::SYS_openat;
1597
1598    if is_held {
1599        let (gate_tx, gate_rx) = tokio::sync::oneshot::channel();
1600        let _ = tx.send(crate::policy_fn::PolicyEvent {
1601            event,
1602            gate: Some(gate_tx),
1603        });
1604        let received = match tokio::time::timeout(std::time::Duration::from_secs(5), gate_rx).await {
1605            Ok(Ok(verdict)) => Some(verdict),
1606            _ => None, // timeout or channel closed
1607        };
1608        resolve_held_gate(received)
1609    } else {
1610        let _ = tx.send(crate::policy_fn::PolicyEvent {
1611            event,
1612            gate: None,
1613        });
1614        None
1615    }
1616}
1617
1618// ============================================================
1619// Per-notification handler (runs in a spawned task)
1620// ============================================================
1621
1622/// Process a single seccomp notification: vDSO re-patch, path denial check,
1623/// dispatch, policy event emission, and response.
1624/// Maximum number of deferred handler futures running concurrently. Caps
1625/// the worker fan-out (and any resources those workers hold, e.g. memfds or
1626/// sockets) so a burst of deferrals cannot exhaust the supervisor process.
1627const DEFER_MAX_INFLIGHT: usize = 64;
1628
1629/// Maximum time a deferred handler future may run before the supervisor gives
1630/// up and fails the trapped syscall closed. Bounds the worst case so a hung
1631/// future (e.g. a stalled network fetch in a token-injection handler) cannot
1632/// park the child forever or permanently leak its `DEFER_MAX_INFLIGHT` slot.
1633const DEFER_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
1634
1635/// Drive a deferred future to its terminal action, bounded by `limit`.
1636///
1637/// On timeout, fail closed with `EIO` so the trapped child gets a definite
1638/// response instead of parking forever; `finalize_deferred` still guards a
1639/// future that resolves to a nested `Defer`.
1640async fn run_deferred_within(deferred: Deferred, limit: std::time::Duration) -> NotifAction {
1641    match tokio::time::timeout(limit, deferred.run()).await {
1642        Ok(action) => finalize_deferred(action),
1643        Err(_) => {
1644            eprintln!(
1645                "sandlock: deferred handler exceeded {:?}; failing syscall with EIO",
1646                limit
1647            );
1648            NotifAction::Errno(libc::EIO)
1649        }
1650    }
1651}
1652
1653/// Spawn a worker task that drives a deferred handler future to its terminal
1654/// action and sends the seccomp response, keyed by `id`. The `permit` is
1655/// held for the worker's lifetime, releasing its `DEFER_MAX_INFLIGHT` slot on
1656/// completion. A stale `id` (child exited mid-defer) makes `send_response`
1657/// a no-op, matching the inline path's "child may have exited" tolerance.
1658fn spawn_deferred(
1659    fd: RawFd,
1660    id: u64,
1661    deferred: Deferred,
1662    permit: tokio::sync::OwnedSemaphorePermit,
1663) {
1664    tokio::spawn(async move {
1665        let _permit = permit; // released when the worker finishes
1666        let action = run_deferred_within(deferred, DEFER_TIMEOUT).await;
1667        let _ = send_response(fd, id, action);
1668    });
1669}
1670
1671async fn handle_notification(
1672    notif: SeccompNotif,
1673    ctx: &Arc<super::ctx::SupervisorCtx>,
1674    dispatch_table: &super::dispatch::DispatchTable,
1675    fd: RawFd,
1676    defer_sem: &Arc<tokio::sync::Semaphore>,
1677) {
1678    let policy = &ctx.policy;
1679
1680    // Ensure every pid that produces a notification has per-process
1681    // supervisor state and an exit watcher. The fork handler runs on
1682    // the *parent* pid (the child doesn't exist yet at clone-time), so
1683    // the child gets registered the first time it issues a notified
1684    // syscall.
1685    crate::resource::register_child_if_new(ctx, notif.pid as i32).await;
1686
1687    // Re-patch vDSO if needed (exec replaces it with a fresh copy).
1688    if policy.has_time_start || policy.has_random_seed {
1689        let mut pfs = ctx.procfs.lock().await;
1690        maybe_patch_vdso(notif.pid as i32, &mut pfs, policy);
1691    }
1692
1693    // Check dynamic path denials before dispatch
1694    let mut action = {
1695        let nr = notif.data.nr as i64;
1696        // symlinkat/symlink are not gated: creating a symlink does not access
1697        // its target (any open through it is denied race-free on the open
1698        // path). See `resolve_path_for_notif`.
1699        let mut path_check_nrs = vec![
1700            libc::SYS_openat, arch::SYS_OPENAT2, libc::SYS_execve, libc::SYS_execveat,
1701            libc::SYS_linkat, libc::SYS_renameat2,
1702        ];
1703        path_check_nrs.extend([
1704            arch::sys_open(), arch::sys_link(), arch::sys_rename(),
1705        ].into_iter().flatten());
1706        let should_precheck_denied = policy.chroot_root.is_none()
1707            && path_check_nrs.contains(&nr);
1708        if should_precheck_denied {
1709            let pfs = ctx.policy_fn.lock().await;
1710            if is_path_denied_for_notif(&pfs, &notif, fd) {
1711                NotifAction::Errno(libc::EACCES)
1712            } else {
1713                let has_denied = pfs.has_denied_paths();
1714                drop(pfs);
1715                // Let normal dispatch run first so /proc virtualization and
1716                // other handlers still win for their paths.
1717                let action = dispatch_table.dispatch(notif, fd).await;
1718                // A bare `Continue` for openat/open is the racy window: the
1719                // supervisor's resolution said "not denied", but the kernel
1720                // re-resolves after Continue and a racing thread can swap a
1721                // symlink to reach a denied carve-out inside a granted tree
1722                // (issue #111). Run the open on-behalf against the pinned
1723                // inode and inject the fd so the kernel never re-resolves.
1724                // Other path syscalls keep the best-effort precheck above
1725                // (documented follow-up — they return no fd to inject).
1726                let is_openat_family = nr == libc::SYS_openat
1727                    || nr == arch::SYS_OPENAT2
1728                    || Some(nr) == arch::sys_open();
1729                if matches!(action, NotifAction::Continue) && is_openat_family && has_denied {
1730                    let pfs = ctx.policy_fn.lock().await;
1731                    on_behalf_open_for_deny(&notif, policy, &pfs, fd)
1732                } else {
1733                    action
1734                }
1735            }
1736        } else {
1737            dispatch_table.dispatch(notif, fd).await
1738        }
1739    };
1740
1741    let nr = notif.data.nr as i64;
1742    let fork_counted = matches!(action, NotifAction::Continue)
1743        && crate::resource::fork_counted_on_continue(&notif, fd);
1744
1745    // TOCTOU-close for execve (issue #27): freeze every sandbox task
1746    // that could mutate argv before policy_fn reads argv and before the
1747    // kernel re-reads it after Continue. This covers two writer classes:
1748    //   1. Sibling threads of the calling tid (same TGID, share mm).
1749    //   2. Peer processes in other TGIDs that alias argv pages via
1750    //      MAP_SHARED mappings or share mm via clone(CLONE_VM).
1751    //
1752    // The freeze enumerates ProcessIndex. With policy_fn active, that
1753    // index is complete: fork-like syscalls are traced at creation time
1754    // below, before new children can run user code.
1755    //
1756    // Strict on failure: if we cannot establish the freeze, we cannot
1757    // safely expose argv or allow execve, so we deny with EPERM.
1758    let mut exec_freeze = None;
1759    if matches!(action, NotifAction::Continue)
1760        && policy.argv_safety_required
1761        && crate::freeze::requires_freeze_on_continue(nr)
1762    {
1763        match crate::freeze::freeze_sandbox_for_execve(
1764            &ctx.processes,
1765            notif.pid as i32,
1766        ) {
1767            Ok(outcome) => {
1768                exec_freeze = Some(outcome);
1769            }
1770            Err(e) => {
1771                eprintln!(
1772                    "sandlock: argv-safety freeze failed for pid {}: {} \
1773                     — denying execve to preserve TOCTOU invariant",
1774                    notif.pid, e
1775                );
1776                action = NotifAction::Errno(libc::EPERM);
1777            }
1778        }
1779    }
1780
1781    // Emit event to policy_fn callback if active. For execve, argv is
1782    // only populated after `exec_freeze` has stopped every possible
1783    // writer, and those tasks stay stopped until after NOTIF_SEND.
1784    if let Some(verdict) = emit_policy_event(&notif, &action, &ctx.policy_fn, fd).await {
1785        use crate::policy_fn::Verdict;
1786        match verdict {
1787            Verdict::Deny => { action = NotifAction::Errno(libc::EPERM); }
1788            Verdict::DenyWith(errno) => { action = NotifAction::Errno(errno); }
1789            Verdict::Audit => { /* allow, but could log here */ }
1790            Verdict::Allow => {}
1791        }
1792    }
1793
1794    if fork_counted && !matches!(action, NotifAction::Continue) {
1795        crate::resource::rollback_fork_count(&ctx.resource).await;
1796    }
1797
1798    // With policy_fn active, fork-like syscalls are traced for exactly
1799    // one ptrace event so ProcessIndex becomes complete before the new
1800    // child can run user code. That closes the race where a peer
1801    // process could exist without ever having produced a notification.
1802    let mut creation_trace = None;
1803    if matches!(action, NotifAction::Continue)
1804        && crate::resource::requires_process_creation_tracking(&notif, fd, policy)
1805    {
1806        match crate::resource::prepare_process_creation_tracking(ctx, notif.pid as i32).await {
1807            Ok(trace) => {
1808                creation_trace = Some(trace);
1809            }
1810            Err(e) => {
1811                eprintln!(
1812                    "sandlock: process-creation tracking failed for pid {}: {} \
1813                     — denying fork-like syscall to preserve argv TOCTOU invariant",
1814                    notif.pid, e
1815                );
1816                if fork_counted {
1817                    crate::resource::rollback_fork_count(&ctx.resource).await;
1818                }
1819                action = NotifAction::Errno(libc::EPERM);
1820            }
1821        }
1822    }
1823
1824    // Deferred response: run the handler's future on a worker task so the
1825    // single supervisor loop is not blocked waiting for slow work (a network
1826    // round-trip, a blocking syscall). The trapped child stays parked in the
1827    // syscall; the worker sends the real response later, keyed by notif.id.
1828    //
1829    // Deferral is refused on syscalls whose Continue path requires the
1830    // execve argv-safety freeze or fork creation-tracking: sending the
1831    // response off-loop would skip that TOCTOU-closing work. (When `action`
1832    // is Defer it is not Continue, so `exec_freeze`/`creation_trace` above
1833    // are already None — there is nothing to unwind here.)
1834    if let NotifAction::Defer(deferred) = action {
1835        if crate::freeze::requires_freeze_on_continue(nr)
1836            || crate::resource::requires_process_creation_tracking(&notif, fd, policy)
1837        {
1838            let _ = send_response(fd, notif.id, NotifAction::Errno(libc::EPERM));
1839            return;
1840        }
1841        match Arc::clone(defer_sem).try_acquire_owned() {
1842            Ok(permit) => spawn_deferred(fd, notif.id, deferred, permit),
1843            // Too many deferrals in flight: fail fast with EAGAIN rather than
1844            // blocking the loop or letting unbounded workers accrete.
1845            Err(_) => {
1846                let _ = send_response(fd, notif.id, NotifAction::Errno(libc::EAGAIN));
1847            }
1848        }
1849        return;
1850    }
1851
1852    // Ignore error — child may have exited between recv and response.
1853    let exec_continued = exec_freeze.is_some() && matches!(action, NotifAction::Continue);
1854    let send_result = send_response(fd, notif.id, action);
1855
1856    if let Some(trace) = creation_trace {
1857        if send_result.is_ok() {
1858            match crate::resource::finish_process_creation_tracking(trace).await {
1859                Ok(true) => {}
1860                Ok(false) => {
1861                    crate::resource::rollback_fork_count(&ctx.resource).await;
1862                }
1863                Err(e) => {
1864                    crate::resource::rollback_fork_count(&ctx.resource).await;
1865                    eprintln!(
1866                        "sandlock: process-creation tracking completion failed for pid {}: {}",
1867                        notif.pid, e
1868                    );
1869                }
1870            }
1871        } else {
1872            crate::resource::rollback_fork_count(&ctx.resource).await;
1873            crate::resource::abort_process_creation_tracking(trace).await;
1874        }
1875    }
1876
1877    if let Some(freeze) = exec_freeze {
1878        if exec_continued && send_result.is_ok() {
1879            crate::freeze::detach_peers(&freeze.peer_tids);
1880        } else {
1881            crate::freeze::detach_all(&freeze);
1882        }
1883    }
1884}
1885
1886// ============================================================
1887// Main supervisor loop
1888// ============================================================
1889
1890/// Async event loop that processes seccomp notifications.
1891///
1892/// Runs until the notification fd is closed (child exits or filter is removed).
1893///
1894/// `pending_handlers` are user-supplied syscall handlers registered after all
1895/// builtin handlers.  For the default behaviour without any custom handlers
1896/// pass an empty `Vec`.
1897pub async fn supervisor(
1898    notif_fd: OwnedFd,
1899    ctx: Arc<super::ctx::SupervisorCtx>,
1900    pending_handlers: Vec<(i64, std::sync::Arc<dyn super::dispatch::Handler>)>,
1901    startup: tokio::sync::oneshot::Sender<io::Result<()>>,
1902) {
1903    // Register the notif fd with the Tokio IO driver so we can wait for
1904    // readiness via epoll instead of a dedicated blocking thread.
1905    let async_fd = match tokio::io::unix::AsyncFd::with_interest(
1906        notif_fd,
1907        tokio::io::Interest::READABLE,
1908    ) {
1909        Ok(fd) => fd,
1910        Err(err) => {
1911            let _ = startup.send(Err(err));
1912            return;
1913        }
1914    };
1915    let fd = async_fd.get_ref().as_raw_fd();
1916
1917    // Build the dispatch table once at startup.
1918    let dispatch_table = Arc::new(super::dispatch::build_dispatch_table(
1919        &ctx.policy,
1920        &ctx.resource,
1921        &ctx,
1922        pending_handlers,
1923    ));
1924
1925    // Try to enable sync wakeup (Linux 6.7+, ignore error on older kernels).
1926    try_set_sync_wakeup(fd);
1927
1928    // The IO driver has the fd registered; subsequent block_on cycles
1929    // can resume this task and pick up readiness events. Tell the
1930    // caller it is safe to release the child.
1931    let _ = startup.send(Ok(()));
1932
1933    // Periodic sweep as a defensive backstop in case pidfd-based
1934    // lifecycle cleanup misses an entry (e.g. pidfd_open failed for a
1935    // child on an old kernel, or its watcher panicked). At 5 minutes
1936    // this is cheap enough to leave on; the primary cleanup path is
1937    // still per-child pidfd readiness in `spawn_pid_watcher`.
1938    let gc = tokio::spawn(process_index_gc(Arc::clone(&ctx.processes)));
1939
1940    // Bounds the number of in-flight deferred handler futures (see
1941    // `DEFER_MAX_INFLIGHT`). Shared across all notifications this supervisor
1942    // processes.
1943    let defer_sem = Arc::new(tokio::sync::Semaphore::new(DEFER_MAX_INFLIGHT));
1944
1945    // Edge-triggered drain: each `readable().await` returns once per
1946    // epoll edge, then we drain the kernel queue via `probe_notif_fd`
1947    // until empty. The drain is necessary because tokio's AsyncFd is
1948    // edge-triggered and `recv_notif` does not signal "would block",
1949    // so a burst of arrivals between two `readable().await` calls
1950    // would coalesce into a single wake event.
1951    //
1952    // Notifications are processed sequentially (not spawned) to avoid
1953    // mutex contention between concurrent handlers.
1954    'outer: loop {
1955        let mut ready = match async_fd.readable().await {
1956            Ok(r) => r,
1957            Err(_) => break 'outer,
1958        };
1959        ready.clear_ready();
1960        drop(ready);
1961
1962        loop {
1963            match probe_notif_fd(fd) {
1964                NotifFdState::Pending => {
1965                    let notif = match recv_notif(fd) {
1966                        Ok(n) => n,
1967                        Err(e) if e.raw_os_error() == Some(libc::EINTR) => continue,
1968                        Err(_) => break 'outer,
1969                    };
1970                    handle_notification(notif, &ctx, &dispatch_table, fd, &defer_sem).await;
1971                }
1972                NotifFdState::Empty => break,
1973                NotifFdState::Terminal => break 'outer,
1974            }
1975        }
1976    }
1977
1978    gc.abort();
1979}
1980
1981/// Periodic sweep that drops `ProcessIndex` entries for exited PIDs.
1982/// Per-process state hangs off these entries via `Arc`, so dropping
1983/// them releases everything in one step.
1984async fn process_index_gc(processes: Arc<super::state::ProcessIndex>) {
1985    let interval = std::time::Duration::from_secs(300);
1986    loop {
1987        tokio::time::sleep(interval).await;
1988        if processes.len() == 0 {
1989            continue;
1990        }
1991        processes.prune_dead();
1992    }
1993}
1994
1995/// Spawn a per-child task that awaits the pidfd becoming readable
1996/// (process exit) and then runs unified cleanup across every
1997/// per-process supervisor map.
1998///
1999/// The watcher *owns* the pidfd via `AsyncFd<OwnedFd>` — the kernel
2000/// fd stays alive for as long as tokio's IO driver has it registered,
2001/// and is closed exactly once when the watcher task ends. This avoids
2002/// a TOCTOU where dropping the fd from a separate map could let a
2003/// recycled fd be deregistered from epoll.
2004pub(crate) fn spawn_pid_watcher(
2005    ctx: Arc<super::ctx::SupervisorCtx>,
2006    key: super::state::PidKey,
2007    pidfd: std::os::unix::io::OwnedFd,
2008) {
2009    tokio::spawn(async move {
2010        let async_fd = match tokio::io::unix::AsyncFd::with_interest(
2011            pidfd,
2012            tokio::io::Interest::READABLE,
2013        ) {
2014            Ok(f) => f,
2015            Err(_) => {
2016                // AsyncFd registration failed (extremely unusual);
2017                // fall back to immediate cleanup so we don't leak the
2018                // index entry. The OwnedFd we passed in is consumed
2019                // by `with_interest`'s Err return and will close on
2020                // drop here.
2021                cleanup_pid(&ctx, key).await;
2022                return;
2023            }
2024        };
2025        // pidfd becomes readable when the process exits; we don't
2026        // read any data, so `readable()` is just an await point.
2027        let _ = async_fd.readable().await;
2028        cleanup_pid(&ctx, key).await;
2029        // async_fd drops here, closing the pidfd.
2030    });
2031}
2032
2033/// Drop the supervisor's per-process state for `key`. With every
2034/// per-process map living inside `PerProcessState` (owned by
2035/// `ProcessIndex`), this is a single unregister — the entry's `Arc`
2036/// drops here, and remaining clones held by in-flight handlers will
2037/// drop with their tasks, freeing `PerProcessState` automatically.
2038pub(crate) async fn cleanup_pid(ctx: &super::ctx::SupervisorCtx, key: super::state::PidKey) {
2039    ctx.processes.unregister(key);
2040}
2041
2042// ============================================================
2043// Tests
2044// ============================================================
2045
2046#[cfg(test)]
2047mod tests {
2048    use super::*;
2049    use std::os::unix::io::FromRawFd;
2050
2051    fn gettid() -> u32 {
2052        (unsafe { libc::syscall(libc::SYS_gettid) }) as u32
2053    }
2054
2055    #[test]
2056    fn inject_failure_response_denies_not_continues() {
2057        // When fd injection fails, the supervisor must fail closed: deny the
2058        // syscall instead of letting it continue unmediated against the host
2059        // path (which would silently bypass chroot/file confinement).
2060        let resp = inject_failure_resp(123);
2061        assert_eq!(resp.id, 123);
2062        assert_eq!(
2063            resp.flags & SECCOMP_USER_NOTIF_FLAG_CONTINUE,
2064            0,
2065            "fd-injection failure must not respond with CONTINUE"
2066        );
2067        assert_ne!(resp.error, 0, "fd-injection failure must be a denial");
2068        assert_eq!(resp.error, -libc::EACCES);
2069    }
2070
2071    #[test]
2072    fn held_gate_no_decision_denies() {
2073        use crate::policy_fn::Verdict;
2074        // A held syscall whose policy_fn gate times out or whose channel closes
2075        // (received == None) must fail closed: deny, not allow the syscall.
2076        assert!(matches!(resolve_held_gate(None), Some(Verdict::Deny)));
2077    }
2078
2079    #[test]
2080    fn held_gate_passes_through_callback_verdict() {
2081        use crate::policy_fn::Verdict;
2082        // A real verdict from the callback is forwarded unchanged.
2083        assert!(matches!(
2084            resolve_held_gate(Some(Verdict::Allow)),
2085            Some(Verdict::Allow)
2086        ));
2087        assert!(matches!(
2088            resolve_held_gate(Some(Verdict::Deny)),
2089            Some(Verdict::Deny)
2090        ));
2091        assert!(matches!(
2092            resolve_held_gate(Some(Verdict::DenyWith(13))),
2093            Some(Verdict::DenyWith(13))
2094        ));
2095    }
2096
2097    #[test]
2098    fn tgid_of_main_thread_is_own_pid() {
2099        // The main thread's tid equals the process pid, and its Tgid is the pid.
2100        assert_eq!(tgid_of(gettid()), Some(std::process::id()));
2101    }
2102
2103    #[test]
2104    fn tgid_of_worker_thread_resolves_to_process() {
2105        // A non-leader thread's Tgid is the process pid, not its own tid.
2106        let (tid_tx, tid_rx) = std::sync::mpsc::channel();
2107        let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
2108        let h = std::thread::spawn(move || {
2109            tid_tx.send(gettid()).unwrap();
2110            done_rx.recv().ok(); // stay alive until the test has read /proc
2111        });
2112        let worker_tid = tid_rx.recv().unwrap();
2113        let pid = std::process::id();
2114        assert_ne!(worker_tid, pid, "worker tid must differ from pid");
2115        assert_eq!(tgid_of(worker_tid), Some(pid));
2116        done_tx.send(()).ok();
2117        h.join().unwrap();
2118    }
2119
2120    #[test]
2121    fn dup_fd_from_pid_handles_worker_thread_fd() {
2122        use std::os::unix::io::AsRawFd;
2123        // Open an fd in a non-leader worker thread, then duplicate it by that
2124        // thread's tid. Exercises the tid->process pidfd resolution end to end
2125        // (PIDFD_THREAD on >=6.9, the /proc Tgid fallback on older kernels).
2126        let (info_tx, info_rx) = std::sync::mpsc::channel();
2127        let (done_tx, done_rx) = std::sync::mpsc::channel::<()>();
2128        let h = std::thread::spawn(move || {
2129            let f = std::fs::File::open("/dev/null").unwrap();
2130            info_tx.send((gettid(), f.as_raw_fd())).unwrap();
2131            done_rx.recv().ok();
2132            drop(f);
2133        });
2134        let (worker_tid, fd) = info_rx.recv().unwrap();
2135        let dup = dup_fd_from_pid(worker_tid, fd);
2136        done_tx.send(()).ok();
2137        h.join().unwrap();
2138        assert!(dup.is_ok(), "dup_fd_from_pid for a worker-thread fd failed: {:?}", dup.err());
2139    }
2140
2141    #[test]
2142    fn read_child_cstr_returns_none_for_null_addr_or_zero_max_len() {
2143        // Smoke: addr == 0 short-circuits without touching the child.
2144        assert!(read_child_cstr(-1, 0, 0, 0, 4096).is_none());
2145        // max_len == 0 also short-circuits.
2146        assert!(read_child_cstr(-1, 0, 0, 0xdeadbeef, 0).is_none());
2147    }
2148
2149    #[test]
2150    fn test_notif_action_debug() {
2151        // Ensure all variants implement Debug.
2152        let _ = format!("{:?}", NotifAction::Continue);
2153        let _ = format!("{:?}", NotifAction::Errno(1));
2154        let _ = format!("{:?}", NotifAction::InjectFd { srcfd: 3, targetfd: 4 });
2155        // Use a real fd (dup'd from stderr) so OwnedFd can safely close it.
2156        let test_fd = unsafe { OwnedFd::from_raw_fd(libc::dup(2)) };
2157        let _ = format!("{:?}", NotifAction::InjectFdSend { srcfd: test_fd, newfd_flags: 0 });
2158        let _ = format!("{:?}", NotifAction::ReturnValue(42));
2159        let _ = format!("{:?}", NotifAction::Hold);
2160        let _ = format!("{:?}", NotifAction::Kill { sig: 9, pgid: 1 });
2161        let _ = format!("{:?}", NotifAction::defer(async { NotifAction::Continue }));
2162    }
2163
2164    #[tokio::test]
2165    async fn deferred_future_need_not_be_sync() {
2166        // A deferred future may capture Send-but-not-Sync state across an
2167        // await. `Cell` is Send but never Sync; holding it across `.await`
2168        // makes the future !Sync. Only `Send` is required (the supervisor
2169        // moves the future to a worker, never shares it by reference).
2170        use std::cell::Cell;
2171        let action = NotifAction::defer(async move {
2172            let counter = Cell::new(0);
2173            counter.set(counter.get() + 41);
2174            tokio::task::yield_now().await; // hold the !Sync Cell across await
2175            NotifAction::ReturnValue(counter.get() + 1)
2176        });
2177        let NotifAction::Defer(d) = action else { panic!("expected Defer") };
2178        assert!(matches!(d.run().await, NotifAction::ReturnValue(42)));
2179    }
2180
2181    #[tokio::test]
2182    async fn deferred_runs_to_its_terminal_action() {
2183        // A Defer carries a future; running it yields the deferred decision.
2184        let action = NotifAction::defer(async { NotifAction::ReturnValue(7) });
2185        let NotifAction::Defer(deferred) = action else {
2186            panic!("defer() must construct a NotifAction::Defer");
2187        };
2188        assert!(matches!(deferred.run().await, NotifAction::ReturnValue(7)));
2189    }
2190
2191    #[tokio::test(start_paused = true)]
2192    async fn deferred_times_out_to_eio() {
2193        // A deferred future that exceeds its limit must fail closed (EIO) so
2194        // the trapped child gets a definite response instead of parking
2195        // forever (and leaking its DEFER_MAX_INFLIGHT slot).
2196        let slow = Deferred::new(async {
2197            tokio::time::sleep(std::time::Duration::from_secs(60)).await;
2198            NotifAction::ReturnValue(7)
2199        });
2200        let action = run_deferred_within(slow, std::time::Duration::from_secs(1)).await;
2201        assert!(matches!(action, NotifAction::Errno(e) if e == libc::EIO));
2202    }
2203
2204    #[tokio::test(start_paused = true)]
2205    async fn deferred_within_limit_passes_through() {
2206        // A future that resolves within the limit returns its terminal action.
2207        let fast = Deferred::new(async { NotifAction::ReturnValue(7) });
2208        let action = run_deferred_within(fast, std::time::Duration::from_secs(1)).await;
2209        assert!(matches!(action, NotifAction::ReturnValue(7)));
2210    }
2211
2212    #[test]
2213    fn finalize_deferred_collapses_nested_defer_to_eio() {
2214        // A deferred future that itself resolves to Defer is a bug: collapse
2215        // to EIO so the trapped child is never wedged waiting for a response.
2216        let nested = NotifAction::defer(async { NotifAction::Continue });
2217        assert!(matches!(finalize_deferred(nested), NotifAction::Errno(e) if e == libc::EIO));
2218        // Non-nested terminal actions pass through unchanged.
2219        assert!(matches!(finalize_deferred(NotifAction::Continue), NotifAction::Continue));
2220        assert!(matches!(
2221            finalize_deferred(NotifAction::ReturnValue(3)),
2222            NotifAction::ReturnValue(3)
2223        ));
2224    }
2225
2226    #[test]
2227    fn content_memfd_roundtrips_content() {
2228        use std::io::Read;
2229        let fd = content_memfd(b"hello world", true).expect("content_memfd");
2230        // The fd is rewound to offset 0, so a plain read returns the content.
2231        let mut f = std::fs::File::from(fd);
2232        let mut buf = String::new();
2233        f.read_to_string(&mut buf).unwrap();
2234        assert_eq!(buf, "hello world");
2235    }
2236
2237    #[test]
2238    fn content_memfd_sealed_applies_write_seal() {
2239        let fd = content_memfd(b"data", true).expect("content_memfd");
2240        let seals = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GET_SEALS) };
2241        assert!(seals >= 0, "F_GET_SEALS failed");
2242        assert!(
2243            seals & libc::F_SEAL_WRITE != 0,
2244            "expected F_SEAL_WRITE on a sealed memfd, got {seals:#x}"
2245        );
2246    }
2247
2248    #[test]
2249    fn content_memfd_unsealed_has_no_write_seal() {
2250        let fd = content_memfd(b"data", false).expect("content_memfd");
2251        let seals = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GET_SEALS) };
2252        assert!(seals >= 0, "F_GET_SEALS failed");
2253        assert_eq!(
2254            seals & libc::F_SEAL_WRITE,
2255            0,
2256            "unsealed memfd must not carry a write seal, got {seals:#x}"
2257        );
2258    }
2259
2260    #[test]
2261    fn inject_bytes_produces_sealed_cloexec_injectfdsend() {
2262        use std::io::Read;
2263        match NotifAction::inject_bytes(b"payload") {
2264            NotifAction::InjectFdSend { srcfd, newfd_flags } => {
2265                assert_eq!(newfd_flags, libc::O_CLOEXEC as u32);
2266                let seals = unsafe { libc::fcntl(srcfd.as_raw_fd(), libc::F_GET_SEALS) };
2267                assert!(seals & libc::F_SEAL_WRITE != 0, "inject_bytes must seal");
2268                let mut f = std::fs::File::from(srcfd);
2269                let mut buf = String::new();
2270                f.read_to_string(&mut buf).unwrap();
2271                assert_eq!(buf, "payload");
2272            }
2273            other => panic!("expected InjectFdSend, got {other:?}"),
2274        }
2275    }
2276
2277    #[test]
2278    fn test_network_state_new() {
2279        let ns = super::super::state::NetworkState::new();
2280        assert!(matches!(ns.tcp_policy, NetworkPolicy::Unrestricted));
2281        assert!(matches!(ns.udp_policy, NetworkPolicy::Unrestricted));
2282        assert!(matches!(ns.icmp_policy, NetworkPolicy::Unrestricted));
2283        assert!(ns.port_map.bound_ports.is_empty());
2284    }
2285
2286    #[test]
2287    fn test_time_random_state_new() {
2288        let tr = super::super::state::TimeRandomState::new(None, None);
2289        assert!(tr.time_offset.is_none());
2290        assert!(tr.random_state.is_none());
2291    }
2292
2293    #[test]
2294    fn test_resource_state_new() {
2295        let rs = super::super::state::ResourceState::new(1024 * 1024, 10);
2296        assert_eq!(rs.mem_used, 0);
2297        assert_eq!(rs.max_memory_bytes, 1024 * 1024);
2298        assert_eq!(rs.max_processes, 10);
2299        assert!(!rs.hold_forks);
2300        assert!(rs.held_notif_ids.is_empty());
2301    }
2302
2303    #[test]
2304    fn test_process_vm_readv_self() {
2305        let data: u64 = 0xDEADBEEF_CAFEBABE;
2306        let addr = &data as *const u64 as u64;
2307        let pid = std::process::id();
2308        let result = read_child_mem_vm(pid, addr, 8);
2309        assert!(result.is_ok());
2310        let bytes = result.unwrap();
2311        let read_val = u64::from_ne_bytes(bytes[..8].try_into().unwrap());
2312        assert_eq!(read_val, 0xDEADBEEF_CAFEBABE);
2313    }
2314
2315    #[test]
2316    fn test_process_vm_writev_self() {
2317        let mut data: u64 = 0;
2318        let addr = &mut data as *mut u64 as u64;
2319        let pid = std::process::id();
2320        let payload = 0x1234567890ABCDEFu64.to_ne_bytes();
2321        let result = write_child_mem_vm(pid, addr, &payload);
2322        assert!(result.is_ok());
2323        assert_eq!(data, 0x1234567890ABCDEF);
2324    }
2325
2326    /// The force-write path (`/proc/<pid>/mem`, FOLL_FORCE) must overwrite a
2327    /// read-only page where `process_vm_writev` (`write_child_mem_vm`) refuses.
2328    /// A read-only private page stands in for the `.rodata` path literal a child
2329    /// hands to chdir/execve — the exact case the chroot/cow rewrite sites hit.
2330    #[test]
2331    fn write_child_mem_proc_forces_past_readonly_page() {
2332        const PAGE: usize = 4096;
2333        let orig = b"/some/rodata/path\0";
2334        let newb = b"/proc/self/fd/7\0\0"; // fits within the page, near orig's len
2335
2336        let addr = unsafe {
2337            libc::mmap(
2338                std::ptr::null_mut(),
2339                PAGE,
2340                libc::PROT_READ | libc::PROT_WRITE,
2341                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
2342                -1,
2343                0,
2344            )
2345        };
2346        assert_ne!(addr, libc::MAP_FAILED, "mmap failed");
2347        unsafe { std::ptr::copy_nonoverlapping(orig.as_ptr(), addr as *mut u8, orig.len()) };
2348
2349        // Flip the page read-only: a normal store would now SIGSEGV, and
2350        // process_vm_writev honors the protection and fails.
2351        assert_eq!(
2352            unsafe { libc::mprotect(addr, PAGE, libc::PROT_READ) },
2353            0,
2354            "mprotect PROT_READ failed"
2355        );
2356
2357        let pid = std::process::id();
2358        let uaddr = addr as u64;
2359
2360        assert!(
2361            write_child_mem_vm(pid, uaddr, newb).is_err(),
2362            "process_vm_writev must fail on a read-only page"
2363        );
2364
2365        // FOLL_FORCE via /proc/<pid>/mem copies-on-write past the RO page.
2366        write_child_mem_proc(pid, uaddr, newb)
2367            .expect("force-write through /proc/pid/mem must succeed on a read-only page");
2368
2369        // The write landed at the target address (page is still RO-mapped, so a
2370        // plain read is fine and reflects the COW'd contents).
2371        let got = unsafe { std::slice::from_raw_parts(addr as *const u8, newb.len()) };
2372        assert_eq!(got, newb, "forced write must be visible at the target address");
2373
2374        unsafe { libc::munmap(addr, PAGE) };
2375    }
2376
2377    #[test]
2378    fn denylist_blocks_matching_cidr_allows_rest() {
2379        use crate::network::IpCidr;
2380        let policy = NetworkPolicy::DenyList {
2381            cidrs: vec![(IpCidr::parse("10.0.0.0/8").unwrap(), PortAllow::Any)],
2382            any_ip_ports: HashSet::new(),
2383            deny_all: false,
2384        };
2385        assert!(!policy.allows("10.1.2.3".parse().unwrap(), 443)); // denied
2386        assert!(policy.allows("8.8.8.8".parse().unwrap(), 443));   // allowed
2387    }
2388
2389    #[test]
2390    fn denylist_blocks_any_ip_port() {
2391        let mut ports = HashSet::new();
2392        ports.insert(25u16);
2393        let policy = NetworkPolicy::DenyList {
2394            cidrs: Vec::new(),
2395            any_ip_ports: ports,
2396            deny_all: false,
2397        };
2398        assert!(!policy.allows("8.8.8.8".parse().unwrap(), 25)); // denied
2399        assert!(policy.allows("8.8.8.8".parse().unwrap(), 80));  // allowed
2400    }
2401
2402    #[test]
2403    fn denylist_specific_ports_on_cidr() {
2404        use crate::network::IpCidr;
2405        let mut ports = HashSet::new();
2406        ports.insert(443u16);
2407        let policy = NetworkPolicy::DenyList {
2408            cidrs: vec![(IpCidr::parse("1.2.3.4/32").unwrap(), PortAllow::Specific(ports))],
2409            any_ip_ports: HashSet::new(),
2410            deny_all: false,
2411        };
2412        assert!(!policy.allows("1.2.3.4".parse().unwrap(), 443)); // denied
2413        assert!(policy.allows("1.2.3.4".parse().unwrap(), 80));   // allowed
2414    }
2415
2416    #[test]
2417    fn allowlist_permits_matching_cidr_only() {
2418        use crate::network::IpCidr;
2419        let mut ports = HashSet::new();
2420        ports.insert(80u16);
2421        let policy = NetworkPolicy::AllowList {
2422            per_ip: HashMap::new(),
2423            cidrs: vec![(IpCidr::parse("10.0.0.0/8").unwrap(), PortAllow::Specific(ports))],
2424            any_ip_ports: HashSet::new(),
2425        };
2426        assert!(policy.allows("10.1.2.3".parse().unwrap(), 80));   // in range, port ok
2427        assert!(!policy.allows("10.1.2.3".parse().unwrap(), 443)); // in range, wrong port
2428        assert!(!policy.allows("8.8.8.8".parse().unwrap(), 80));   // out of range
2429    }
2430
2431    #[test]
2432    fn allowlist_cidr_all_ports() {
2433        use crate::network::IpCidr;
2434        let policy = NetworkPolicy::AllowList {
2435            per_ip: HashMap::new(),
2436            cidrs: vec![(IpCidr::parse("192.168.0.0/16").unwrap(), PortAllow::Any)],
2437            any_ip_ports: HashSet::new(),
2438        };
2439        assert!(policy.allows("192.168.5.5".parse().unwrap(), 9999)); // any port in range
2440        assert!(!policy.allows("10.0.0.1".parse().unwrap(), 9999));   // out of range
2441    }
2442}