Skip to main content

sandlock_core/
sandbox.rs

1use std::collections::HashMap;
2use std::os::fd::AsRawFd;
3use std::path::PathBuf;
4use std::sync::Arc;
5use std::time::SystemTime;
6
7use serde::{Deserialize, Serialize};
8use tokio::task::JoinHandle;
9
10use crate::context;
11use crate::error::SandboxError;
12pub use crate::http::{http_acl_check, normalize_path, prefix_or_exact_match, HttpRule};
13pub use crate::network::{IpCidr, NetAllow, NetDeny, NetRule, NetTarget, Protocol};
14use crate::protection::{Protection, ProtectionPolicy, ProtectionState, ProtectionStatus};
15
16mod builder;
17pub use builder::SandboxBuilder;
18
19/// A byte size value.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub struct ByteSize(pub u64);
22
23impl ByteSize {
24    pub fn bytes(n: u64) -> Self {
25        ByteSize(n)
26    }
27
28    pub fn kib(n: u64) -> Self {
29        ByteSize(n * 1024)
30    }
31
32    pub fn mib(n: u64) -> Self {
33        ByteSize(n * 1024 * 1024)
34    }
35
36    pub fn gib(n: u64) -> Self {
37        ByteSize(n * 1024 * 1024 * 1024)
38    }
39
40    pub fn parse(s: &str) -> Result<Self, SandboxError> {
41        let s = s.trim();
42        if s.is_empty() {
43            return Err(SandboxError::Invalid("empty byte size string".into()));
44        }
45
46        // Check for suffix
47        let last = s.chars().last().unwrap();
48        if last.is_ascii_alphabetic() {
49            let (num_str, suffix) = s.split_at(s.len() - 1);
50            let n: u64 = num_str
51                .trim()
52                .parse()
53                .map_err(|_| SandboxError::Invalid(format!("invalid byte size: {}", s)))?;
54            match suffix.to_ascii_uppercase().as_str() {
55                "K" => Ok(ByteSize::kib(n)),
56                "M" => Ok(ByteSize::mib(n)),
57                "G" => Ok(ByteSize::gib(n)),
58                other => Err(SandboxError::Invalid(format!("unknown byte size suffix: {}", other))),
59            }
60        } else {
61            let n: u64 = s
62                .parse()
63                .map_err(|_| SandboxError::Invalid(format!("invalid byte size: {}", s)))?;
64            Ok(ByteSize(n))
65        }
66    }
67}
68
69/// Identity to run the sandboxed process as.
70///
71/// Applied via a single-entry user-namespace map (`unshare(CLONE_NEWUSER)` +
72/// `uid_map`/`gid_map`), so it requires no host privilege.  Because an
73/// unprivileged user namespace can only map a single id and must deny
74/// `setgroups`, exactly one uid and one gid are representable (no supplementary
75/// groups, no id ranges).
76///
77/// Parsed from `UID:GID`; both ids are required (no implicit default).
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79pub struct RunAs {
80    pub uid: u32,
81    pub gid: u32,
82}
83
84impl std::str::FromStr for RunAs {
85    type Err = String;
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        let (u, g) = s
88            .split_once(':')
89            .ok_or_else(|| format!("expected UID:GID, got {:?}", s))?;
90        let uid = u.trim().parse::<u32>().map_err(|_| format!("invalid uid {:?}", u))?;
91        let gid = g.trim().parse::<u32>().map_err(|_| format!("invalid gid {:?}", g))?;
92        Ok(RunAs { uid, gid })
93    }
94}
95
96/// Confinement for confining the current process in place.
97#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
98pub struct Confinement {
99    pub fs_writable: Vec<PathBuf>,
100    pub fs_readable: Vec<PathBuf>,
101}
102
103impl Confinement {
104    pub fn builder() -> ConfinementBuilder {
105        ConfinementBuilder::default()
106    }
107}
108
109#[derive(Default)]
110pub struct ConfinementBuilder {
111    fs_writable: Vec<PathBuf>,
112    fs_readable: Vec<PathBuf>,
113}
114
115impl ConfinementBuilder {
116    pub fn fs_write(mut self, path: impl Into<PathBuf>) -> Self {
117        self.fs_writable.push(path.into());
118        self
119    }
120
121    pub fn fs_read(mut self, path: impl Into<PathBuf>) -> Self {
122        self.fs_readable.push(path.into());
123        self
124    }
125
126    pub fn build(self) -> Confinement {
127        Confinement {
128            fs_writable: self.fs_writable,
129            fs_readable: self.fs_readable,
130        }
131    }
132}
133
134impl TryFrom<&Sandbox> for Confinement {
135    type Error = SandboxError;
136
137    fn try_from(sandbox: &Sandbox) -> Result<Self, Self::Error> {
138        let mut unsupported = Vec::new();
139        if !sandbox.fs_denied.is_empty() { unsupported.push("fs_denied"); }
140        if !sandbox.extra_deny_syscalls.is_empty() { unsupported.push("extra_deny_syscalls"); }
141        if !sandbox.net_allow.is_empty() { unsupported.push("net_allow"); }
142        if !sandbox.net_deny.is_empty() { unsupported.push("net_deny"); }
143        if !sandbox.net_allow_bind.is_empty() { unsupported.push("net_allow_bind"); }
144        if !sandbox.net_deny_bind.is_empty() { unsupported.push("net_deny_bind"); }
145        if sandbox.allows_sysv_ipc() { unsupported.push("extra_allow_syscalls=[\"sysv_ipc\"]"); }
146        if !sandbox.http_allow.is_empty() { unsupported.push("http_allow"); }
147        if !sandbox.http_deny.is_empty() { unsupported.push("http_deny"); }
148        if !sandbox.http_ports.is_empty() { unsupported.push("http_ports"); }
149        if sandbox.http_ca.is_some() { unsupported.push("http_ca"); }
150        if sandbox.http_key.is_some() { unsupported.push("http_key"); }
151        if !sandbox.http_inject_ca.is_empty() { unsupported.push("http_inject_ca"); }
152        if sandbox.http_ca_out.is_some() { unsupported.push("http_ca_out"); }
153        if sandbox.max_memory.is_some() { unsupported.push("max_memory"); }
154        if sandbox.max_processes != 64 { unsupported.push("max_processes"); }
155        if sandbox.max_open_files.is_some() { unsupported.push("max_open_files"); }
156        if sandbox.max_cpu.is_some() { unsupported.push("max_cpu"); }
157        if sandbox.random_seed.is_some() { unsupported.push("random_seed"); }
158        if sandbox.time_start.is_some() { unsupported.push("time_start"); }
159        if sandbox.no_randomize_memory { unsupported.push("no_randomize_memory"); }
160        if sandbox.no_huge_pages { unsupported.push("no_huge_pages"); }
161        if sandbox.no_coredump { unsupported.push("no_coredump"); }
162        if sandbox.deterministic_dirs { unsupported.push("deterministic_dirs"); }
163        if sandbox.workdir.is_some() { unsupported.push("workdir"); }
164        if sandbox.cwd.is_some() { unsupported.push("cwd"); }
165        if sandbox.fs_storage.is_some() { unsupported.push("fs_storage"); }
166        if sandbox.max_disk.is_some() { unsupported.push("max_disk"); }
167        if sandbox.on_exit != BranchAction::Commit { unsupported.push("on_exit"); }
168        if sandbox.on_error != BranchAction::Abort { unsupported.push("on_error"); }
169        if !sandbox.fs_mount.is_empty() { unsupported.push("fs_mount"); }
170        if sandbox.chroot.is_some() { unsupported.push("chroot"); }
171        if sandbox.clean_env { unsupported.push("clean_env"); }
172        if !sandbox.env.is_empty() { unsupported.push("env"); }
173        if sandbox.gpu_devices.is_some() { unsupported.push("gpu_devices"); }
174        if sandbox.cpu_cores.is_some() { unsupported.push("cpu_cores"); }
175        if sandbox.num_cpus.is_some() { unsupported.push("num_cpus"); }
176        if sandbox.port_remap { unsupported.push("port_remap"); }
177        if sandbox.user.is_some() { unsupported.push("user"); }
178        if sandbox.policy_fn.is_some() { unsupported.push("policy_fn"); }
179
180        if !unsupported.is_empty() {
181            return Err(SandboxError::UnsupportedForConfine(unsupported.join(", ")));
182        }
183
184        Ok(Self {
185            fs_writable: sandbox.fs_writable.clone(),
186            fs_readable: sandbox.fs_readable.clone(),
187        })
188    }
189}
190
191/// Action to take on branch exit.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
193pub enum BranchAction {
194    #[default]
195    Commit,
196    Abort,
197    Keep,
198}
199
200// ============================================================
201// Runtime — private heap-allocated state, present only while running
202// ============================================================
203
204/// Private runtime state.  Only allocated after `start()` / `run()` is
205/// called; `None` for config-only `Sandbox` instances.
206struct Runtime {
207    name: String,
208    state: RuntimeState,
209    child_pid: Option<i32>,
210    pidfd: Option<std::os::fd::OwnedFd>,
211    notif_handle: Option<JoinHandle<()>>,
212    throttle_handle: Option<JoinHandle<()>>,
213    loadavg_handle: Option<JoinHandle<()>>,
214    _stdout_read: Option<std::os::fd::OwnedFd>,
215    _stderr_read: Option<std::os::fd::OwnedFd>,
216    seccomp_cow: Option<crate::cow::seccomp::SeccompCowBranch>,
217    supervisor_resource: Option<Arc<tokio::sync::Mutex<crate::seccomp::state::ResourceState>>>,
218    supervisor_cow: Option<Arc<tokio::sync::Mutex<crate::seccomp::state::CowState>>>,
219    supervisor_network: Option<Arc<tokio::sync::Mutex<crate::seccomp::state::NetworkState>>>,
220    ctrl_fd: Option<std::os::fd::OwnedFd>,
221    stdout_pipe: Option<std::os::fd::OwnedFd>,
222    io_overrides: Option<(Option<i32>, Option<i32>, Option<i32>)>,
223    extra_fds: Vec<(i32, i32)>,
224    http_acl_handle: Option<crate::transparent_proxy::HttpAclProxyHandle>,
225    #[allow(clippy::type_complexity)]
226    on_bind: Option<Box<dyn Fn(&HashMap<u16, u16>) + Send + Sync>>,
227    handlers: Vec<(i64, Arc<dyn crate::seccomp::dispatch::Handler>)>,
228    ready_w: Option<std::os::fd::OwnedFd>,
229}
230
231/// Lifecycle state for the runtime.
232enum RuntimeState {
233    Created,
234    Running,
235    Paused,
236    Stopped(crate::result::ExitStatus),
237}
238
239/// Sandbox configuration.
240#[derive(Serialize, Deserialize)]
241pub struct Sandbox {
242    // Filesystem access
243    pub fs_writable: Vec<PathBuf>,
244    pub fs_readable: Vec<PathBuf>,
245    pub fs_denied: Vec<PathBuf>,
246
247    // Extra syscall filtering on top of Sandlock's default blocklist.
248    pub extra_deny_syscalls: Vec<String>,
249    pub extra_allow_syscalls: Vec<String>,
250
251    /// Per-protection enforcement policy. Default
252    /// (`ProtectionPolicy::strict_all()`) preserves the historical hard
253    /// `MIN_ABI = 6` behaviour; `SandboxBuilder::allow_degraded` /
254    /// `::disable` deviate from strict-all per protection.
255    ///
256    /// Part of the checkpoint: a saved sandbox restores with its exact
257    /// protection posture. Without this, a sandbox built with a
258    /// `disable()` opt-out (required on, e.g., a v5 host that cannot
259    /// provide a v6 scope) would silently reset to `strict_all()` on
260    /// load and fail to restore.
261    pub protection_policy: ProtectionPolicy,
262
263    // Network
264    /// Outbound endpoint allowlist as a list of `(protocol, host?, ports)`
265    /// rules. Each rule names a protocol (TCP/UDP/ICMP) and either a
266    /// concrete host or "any IP." TCP and UDP rules carry ports; ICMP
267    /// rules have none.
268    ///
269    /// **Protocol gating falls out of rule presence.** Sandlock denies
270    /// UDP and ICMP socket creation by default; opting in is "list at
271    /// least one rule for that protocol" (e.g. `udp://*:*` for any UDP,
272    /// `icmp://*` for any ICMP echo). TCP is always permitted.
273    ///
274    /// Empty `net_allow` and empty `http_allow`/`http_deny` together
275    /// mean "deny all outbound" (Landlock direct path denies, no
276    /// on-behalf path is enabled). Otherwise, the on-behalf path
277    /// enforces these rules: a destination is permitted iff any rule
278    /// matches the protocol, destination IP (or has `host: None` = any
279    /// IP), and destination port (N/A for ICMP).
280    ///
281    /// HTTP rules with concrete hosts auto-add a matching
282    /// `(Tcp, host, [80])` (and `(Tcp, host, [443])` when `--http-ca`
283    /// is set) entry at build time so the proxy's intercept ports
284    /// remain reachable. HTTP rules with wildcard hosts auto-add
285    /// `(Tcp, None, [80])` instead.
286    pub net_allow: Vec<NetAllow>,
287    /// Parsed `--net-deny` rules (default-allow, IP/CIDR/port denylist).
288    /// Mutually exclusive with `net_allow`.
289    pub net_deny: Vec<NetDeny>,
290    /// `--net-allow-bind`: TCP ports the sandbox may bind (default-deny
291    /// allowlist, Landlock-enforced). Mutually exclusive with `net_deny_bind`.
292    pub net_allow_bind: Vec<u16>,
293    /// `--net-deny-bind`: TCP ports the sandbox may NOT bind (default-allow
294    /// denylist, enforced on the on-behalf `bind()` path). Mutually
295    /// exclusive with `net_allow_bind`.
296    pub net_deny_bind: Vec<u16>,
297    // HTTP ACL
298    pub http_allow: Vec<HttpRule>,
299    pub http_deny: Vec<HttpRule>,
300    /// TCP ports to intercept for HTTP ACL. Defaults to [80] (plus 443 when
301    /// http_ca is set). Override with `http_ports` to intercept custom ports.
302    pub http_ports: Vec<u16>,
303    /// PEM CA cert for HTTPS MITM. When set, port 443 is also intercepted.
304    pub http_ca: Option<PathBuf>,
305    /// PEM CA key for HTTPS MITM. Required when http_ca is set.
306    pub http_key: Option<PathBuf>,
307    /// Trust-bundle paths to splice the MITM CA into (zero-config HTTPS).
308    pub http_inject_ca: Vec<PathBuf>,
309    /// Path to write the active MITM CA public cert (PEM) for external trust
310    /// wiring (e.g. NODE_EXTRA_CA_CERTS). Never writes the private key.
311    pub http_ca_out: Option<PathBuf>,
312
313    // Resource limits
314    pub max_memory: Option<ByteSize>,
315    pub max_processes: u32,
316    pub max_open_files: Option<u32>,
317    pub max_cpu: Option<u8>,
318
319    // Reproducibility
320    pub random_seed: Option<u64>,
321    pub time_start: Option<SystemTime>,
322    pub no_randomize_memory: bool,
323    pub no_huge_pages: bool,
324    pub no_coredump: bool,
325    pub deterministic_dirs: bool,
326
327    // Filesystem branch
328    pub workdir: Option<PathBuf>,
329    pub cwd: Option<PathBuf>,
330    pub fs_storage: Option<PathBuf>,
331    pub max_disk: Option<ByteSize>,
332    pub on_exit: BranchAction,
333    pub on_error: BranchAction,
334
335    // Mount mappings: (virtual_path_inside_chroot, host_path_on_disk)
336    pub fs_mount: Vec<(PathBuf, PathBuf)>,
337    // Virtual paths (a subset of fs_mount destinations) mounted read-only:
338    // reads allowed, writes denied even on a writable rootfs.
339    pub fs_mount_ro: Vec<PathBuf>,
340
341    // Environment
342    pub chroot: Option<PathBuf>,
343
344    /// When set, the confined child runs this function in-process instead of
345    /// `execve`-ing a workload. Used to run an in-sandbox PID-1 (the OCI
346    /// `sandlock-init` control loop) without exec'ing a separate image: the
347    /// child is already a fork of the supervisor, so its code is mapped, and
348    /// because nothing is exec'd, Landlock has no execution to authorize. The
349    /// function must not return (it loops and `_exit`s); `confine_child` calls
350    /// `_exit(0)` if it does.
351    #[serde(skip)]
352    pub in_child_main: Option<fn()>,
353
354    pub clean_env: bool,
355    pub env: HashMap<String, String>,
356    // Devices
357    pub gpu_devices: Option<Vec<u32>>,
358
359    // CPU
360    pub cpu_cores: Option<Vec<u32>>,
361    pub num_cpus: Option<u32>,
362    pub port_remap: bool,
363
364    /// Skip the seccomp user-notification supervisor. The sandbox runs
365    /// with Landlock + a kernel-only deny filter, with none of the
366    /// supervisor-mediated features (IP allowlist, resource limits,
367    /// COW, chroot mediation, /proc virtualization, custom handlers).
368    /// Required when nesting inside another sandlock — the kernel only
369    /// allows one `SECCOMP_FILTER_FLAG_NEW_LISTENER` per task.
370    pub no_supervisor: bool,
371
372    // User-namespace identity (run-as uid/gid)
373    pub user: Option<RunAs>,
374
375    // Dynamic policy callback
376    #[serde(skip)]
377    pub policy_fn: Option<crate::policy_fn::PolicyCallback>,
378
379    // Sandbox instance name (exposed as virtual hostname; auto-generated if None).
380    // Not serialized — instance names are set at runtime, not in the policy file.
381    #[serde(skip)]
382    pub name: Option<String>,
383
384    // COW fork init function — runs once in the child before COW cloning.
385    // Not serialized; not cloned (FnOnce can't be cloned — drops to None on clone).
386    #[serde(skip)]
387    init_fn: Option<Box<dyn FnOnce() + Send + 'static>>,
388
389    // COW fork work function — runs in each COW clone.
390    // Not serialized; cloned via Arc (cheap).
391    #[serde(skip)]
392    work_fn: Option<Arc<dyn Fn(u32) + Send + Sync + 'static>>,
393
394    // Heap-allocated runtime state; `None` when not started.
395    #[serde(skip)]
396    runtime: Option<Box<Runtime>>,
397}
398
399impl std::fmt::Debug for Sandbox {
400    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
401        f.debug_struct("Sandbox")
402            .field("fs_readable", &self.fs_readable)
403            .field("fs_writable", &self.fs_writable)
404            .field("max_memory", &self.max_memory)
405            .field("max_processes", &self.max_processes)
406            .field("policy_fn", &self.policy_fn.as_ref().map(|_| "<callback>"))
407            .field("name", &self.name)
408            .field("runtime", &self.runtime.as_ref().map(|_| "<runtime>"))
409            .finish_non_exhaustive()
410    }
411}
412
413impl Clone for Sandbox {
414    /// Clone a `Sandbox` — config and runtime-kwargs fields are cloned; the
415    /// runtime state is not (the clone starts with `runtime: None`).
416    ///
417    /// Field clone semantics:
418    /// - `policy_fn` — Arc bump (cheap).
419    /// - `work_fn`   — Arc bump (cheap); multiple Sandboxes share the closure.
420    /// - `init_fn`   — **dropped to `None`** (FnOnce can't be cloned). If the
421    ///   clone also needs an init function, call `.init_fn(...)` on it
422    ///   separately or set it via `SandboxBuilder::init_fn`.
423    /// - `runtime`   — always `None`; the clone is a fresh, un-started Sandbox.
424    fn clone(&self) -> Self {
425        Self {
426            fs_writable: self.fs_writable.clone(),
427            fs_readable: self.fs_readable.clone(),
428            fs_denied: self.fs_denied.clone(),
429            extra_deny_syscalls: self.extra_deny_syscalls.clone(),
430            extra_allow_syscalls: self.extra_allow_syscalls.clone(),
431            protection_policy: self.protection_policy.clone(),
432            net_allow: self.net_allow.clone(),
433            net_deny: self.net_deny.clone(),
434            net_allow_bind: self.net_allow_bind.clone(),
435            net_deny_bind: self.net_deny_bind.clone(),
436            http_allow: self.http_allow.clone(),
437            http_deny: self.http_deny.clone(),
438            http_ports: self.http_ports.clone(),
439            http_ca: self.http_ca.clone(),
440            http_key: self.http_key.clone(),
441            http_inject_ca: self.http_inject_ca.clone(),
442            http_ca_out: self.http_ca_out.clone(),
443            max_memory: self.max_memory,
444            max_processes: self.max_processes,
445            max_open_files: self.max_open_files,
446            max_cpu: self.max_cpu,
447            random_seed: self.random_seed,
448            time_start: self.time_start,
449            no_randomize_memory: self.no_randomize_memory,
450            no_huge_pages: self.no_huge_pages,
451            no_coredump: self.no_coredump,
452            deterministic_dirs: self.deterministic_dirs,
453            workdir: self.workdir.clone(),
454            cwd: self.cwd.clone(),
455            fs_storage: self.fs_storage.clone(),
456            max_disk: self.max_disk,
457            on_exit: self.on_exit.clone(),
458            on_error: self.on_error.clone(),
459            fs_mount: self.fs_mount.clone(),
460            fs_mount_ro: self.fs_mount_ro.clone(),
461            chroot: self.chroot.clone(),
462            in_child_main: self.in_child_main,
463            clean_env: self.clean_env,
464            env: self.env.clone(),
465            gpu_devices: self.gpu_devices.clone(),
466            cpu_cores: self.cpu_cores.clone(),
467            num_cpus: self.num_cpus,
468            port_remap: self.port_remap,
469            no_supervisor: self.no_supervisor,
470            user: self.user,
471            policy_fn: self.policy_fn.clone(),
472            name: self.name.clone(),
473            // init_fn (FnOnce) cannot be cloned — the clone gets None.
474            // If the clone also needs an init function, set it explicitly.
475            init_fn: None,
476            // work_fn is Arc-wrapped — clone bumps the reference count.
477            work_fn: self.work_fn.clone(),
478            // Runtime is NOT cloned — the clone starts with no runtime.
479            runtime: None,
480        }
481    }
482}
483
484impl Sandbox {
485    pub fn builder() -> SandboxBuilder {
486        SandboxBuilder::default()
487    }
488
489    /// Returns true iff the policy grants the `sysv_ipc` syscall group.
490    pub fn allows_sysv_ipc(&self) -> bool {
491        self.extra_allow_syscalls.iter().any(|s| s == "sysv_ipc")
492    }
493
494    /// Validate cross-section invariants — checks that span multiple fields.
495    ///
496    /// Currently a no-op; retained as an extension point and for API
497    /// stability. Idempotent: calling repeatedly is safe.
498    pub fn validate(&self) -> Result<(), SandboxError> {
499        Ok(())
500    }
501
502    /// Resolve the per-protection state against the host's current
503    /// Landlock ABI. Returns one entry per `Protection`. Useful for
504    /// post-`build()` posture inspection.
505    pub fn active_protections(&self) -> Result<Vec<(Protection, ProtectionStatus)>, crate::error::SandlockError> {
506        let host_abi = crate::landlock::abi_version().map_err(|e| {
507            crate::error::SandlockError::Runtime(crate::error::SandboxRuntimeError::Confinement(e))
508        })?;
509        Ok(Protection::all()
510            .map(|p| (p, ProtectionStatus::resolve(p, host_abi, &self.protection_policy)))
511            .collect())
512    }
513
514    // ================================================================
515    // Runtime accessor helpers (private)
516    // ================================================================
517
518    fn rt(&self) -> &Runtime {
519        self.runtime.as_ref().expect("sandbox not started")
520    }
521
522    fn rt_mut(&mut self) -> &mut Runtime {
523        self.runtime.as_mut().expect("sandbox not started")
524    }
525
526    // ================================================================
527    // Runtime lifecycle API (public)
528    // ================================================================
529
530    /// Set the sandbox instance name (also exposed as the virtual hostname).
531    /// Auto-generated if not set.
532    pub fn set_name(&mut self, name: impl Into<String>) {
533        self.name = Some(name.into());
534    }
535
536    /// Set the sandbox instance name and return `self`. Convenience for
537    /// pipeline fan-out where a base config is cloned and each clone gets a
538    /// fresh name:
539    ///
540    /// ```ignore
541    /// let template = Sandbox::builder()...build()?;
542    /// let mut s1 = template.clone().with_name("worker-1");
543    /// let mut s2 = template.clone().with_name("worker-2");
544    /// ```
545    pub fn with_name(mut self, name: impl Into<String>) -> Self {
546        self.name = Some(name.into());
547        self
548    }
549
550    /// Set the COW-fork init function and return `self`.
551    ///
552    /// The init function runs once in the child process before any COW clones
553    /// are created. Use it to load expensive shared state.
554    pub fn with_init_fn(mut self, f: impl FnOnce() + Send + 'static) -> Self {
555        self.init_fn = Some(Box::new(f));
556        self
557    }
558
559    /// Set the COW-fork work function and return `self`.
560    ///
561    /// The work function runs in each COW clone (`fork(N)` produces N clones).
562    pub fn with_work_fn(mut self, f: impl Fn(u32) + Send + Sync + 'static) -> Self {
563        self.work_fn = Some(Arc::new(f));
564        self
565    }
566
567    /// Return the sandbox name if set, or `None` if not yet started.
568    pub fn instance_name(&self) -> Option<&str> {
569        self.runtime.as_ref().map(|r| r.name.as_str())
570            .or_else(|| self.name.as_deref())
571    }
572
573    /// Return the child PID if spawned.
574    pub fn pid(&self) -> Option<i32> {
575        self.runtime.as_ref().and_then(|r| r.child_pid)
576    }
577
578    /// Return whether the child is currently running or paused.
579    pub fn is_running(&self) -> bool {
580        self.runtime.as_ref().map(|r| {
581            matches!(r.state, RuntimeState::Running | RuntimeState::Paused)
582        }).unwrap_or(false)
583    }
584
585    /// Send SIGSTOP to the child's process group.
586    pub fn pause(&mut self) -> Result<(), crate::error::SandlockError> {
587        use crate::error::SandboxRuntimeError;
588        let pid = self.runtime.as_ref()
589            .and_then(|rt| rt.child_pid)
590            .ok_or(SandboxRuntimeError::NotRunning)?;
591        let ret = unsafe { libc::killpg(pid, libc::SIGSTOP) };
592        if ret < 0 {
593            return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
594        }
595        self.rt_mut().state = RuntimeState::Paused;
596        Ok(())
597    }
598
599    /// Send SIGCONT to the child's process group.
600    pub fn resume(&mut self) -> Result<(), crate::error::SandlockError> {
601        use crate::error::SandboxRuntimeError;
602        let pid = self.runtime.as_ref()
603            .and_then(|rt| rt.child_pid)
604            .ok_or(SandboxRuntimeError::NotRunning)?;
605        let ret = unsafe { libc::killpg(pid, libc::SIGCONT) };
606        if ret < 0 {
607            return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
608        }
609        self.rt_mut().state = RuntimeState::Running;
610        Ok(())
611    }
612
613    /// Send SIGKILL to the child's process group.
614    pub fn kill(&mut self) -> Result<(), crate::error::SandlockError> {
615        use crate::error::SandboxRuntimeError;
616        let pid = self.runtime.as_ref()
617            .and_then(|rt| rt.child_pid)
618            .ok_or(SandboxRuntimeError::NotRunning)?;
619        let ret = unsafe { libc::killpg(pid, libc::SIGKILL) };
620        if ret < 0 {
621            let err = std::io::Error::last_os_error();
622            if err.raw_os_error() != Some(libc::ESRCH) {
623                return Err(SandboxRuntimeError::Io(err).into());
624            }
625        }
626        Ok(())
627    }
628
629    /// Set a callback invoked whenever a port bind is recorded.
630    pub fn set_on_bind(&mut self, cb: impl Fn(&HashMap<u16, u16>) + Send + Sync + 'static) {
631        // Ensure runtime exists so we have somewhere to store the callback.
632        // In practice, set_on_bind is always called before spawn.
633        let _ = self.ensure_runtime();
634        self.rt_mut().on_bind = Some(Box::new(cb));
635    }
636
637    /// Return the current virtual-to-real port mappings.
638    pub async fn port_mappings(&self) -> HashMap<u16, u16> {
639        if let Some(ref rt) = self.runtime {
640            if let Some(ref net) = rt.supervisor_network {
641                let ns = net.lock().await;
642                return ns.port_map.virtual_to_real.clone();
643            }
644        }
645        HashMap::new()
646    }
647
648    /// Wait for the child process to exit.
649    pub async fn wait(&mut self) -> Result<crate::result::RunResult, crate::error::SandlockError> {
650        use crate::error::SandboxRuntimeError;
651        use crate::result::RunResult;
652
653        let pid = self.rt().child_pid.ok_or(SandboxRuntimeError::NotRunning)?;
654
655        if let RuntimeState::Stopped(ref es) = self.rt().state {
656            return Ok(RunResult {
657                exit_status: es.clone(),
658                stdout: None,
659                stderr: None,
660            });
661        }
662
663        // Wait for the top-level child to exit. Prefer the child's pidfd via
664        // `AsyncFd`: pidfd readiness fires only on *exit*, so — unlike a
665        // `waitpid` loop — it never consumes the child's ptrace-stops, which
666        // the `policy_fn` fork-tracking worker reaps (`waitpid` with any flags
667        // reaps a tracee's ptrace-stops, so a concurrent `waitpid` here would
668        // race the worker for fork events and hang it). Mirrors
669        // `spawn_pid_watcher`. Falls back to a blocking `waitpid` only when no
670        // pidfd is available (kernel without `pidfd_open`).
671        let exit_status = match self.rt_mut().pidfd.take() {
672            Some(pidfd) => wait_child_exit_via_pidfd(pidfd, pid).await,
673            None => wait_child_exit_blocking(pid).await,
674        };
675
676        self.rt_mut().state = RuntimeState::Stopped(exit_status.clone());
677
678        let rt = self.rt_mut();
679        if let Some(h) = rt.notif_handle.take() { h.abort(); }
680        if let Some(h) = rt.throttle_handle.take() { h.abort(); }
681        if let Some(h) = rt.loadavg_handle.take() { h.abort(); }
682
683        if let Some(ref cow_state) = self.rt().supervisor_cow.clone() {
684            let mut cow = cow_state.lock().await;
685            self.rt_mut().seccomp_cow = cow.branch.take();
686        }
687
688        let stdout = self.rt_mut()._stdout_read.take().map(sandbox_read_fd_to_end);
689        let stderr = self.rt_mut()._stderr_read.take().map(sandbox_read_fd_to_end);
690
691        Ok(RunResult { exit_status, stdout, stderr })
692    }
693
694    /// Fork the sandboxed child and install policy (seccomp + notif
695    /// supervisor + rlimits + landlock + COW + network/HTTP proxies).
696    /// The child is parked between policy install and `execve`; call
697    /// `start()` to release it. Stdout/stderr are captured for later
698    /// retrieval via `wait()`.
699    pub async fn create(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
700        self.do_create(cmd, true).await
701    }
702
703    /// Like `create` but inherits stdio (no capture).
704    pub async fn create_interactive(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
705        self.do_create(cmd, false).await
706    }
707
708    /// Release a previously `create()`d child to `execve` the configured
709    /// command. Returns immediately; use `wait()` to collect the exit
710    /// status when the child finishes.
711    pub fn start(&mut self) -> Result<(), crate::error::SandlockError> {
712        self.do_start()
713    }
714
715    /// Sugar for `create()` + `start()` that also blocks until the child
716    /// has completed `execve()` and is executing user code. After this
717    /// returns, operations that read user-code state (e.g. `checkpoint()`,
718    /// `/proc/<pid>/exe`) observe the requested binary rather than the
719    /// supervisor.
720    pub async fn spawn(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
721        self.create(cmd).await?;
722        self.start()?;
723        self.wait_until_exec().await
724    }
725
726    /// Like `spawn` but inherits stdio (no capture).
727    pub async fn spawn_interactive(&mut self, cmd: &[&str]) -> Result<(), crate::error::SandlockError> {
728        self.create_interactive(cmd).await?;
729        self.start()?;
730        self.wait_until_exec().await
731    }
732
733    /// Restore a checkpoint into a fresh, fully-sandboxed process.
734    ///
735    /// Reuses the normal create path to fork a child with the saved policy and the
736    /// full notify stack in place (the child parks before execve), then takes the
737    /// parked child over with ptrace and injects the checkpoint image over it via
738    /// `restore_into`, resuming it at the saved program counter. The process comes
739    /// up already sandboxed. Returns the list of non-transparently-restored fd
740    /// paths (skipped; the caller should log them). x86_64 restore engine only.
741    ///
742    /// Limitation: transparent restore currently works for vDSO-free programs.
743    /// libc/glibc programs that call vDSO functions (e.g. `clock_gettime`) crash
744    /// on resume because the vDSO is not yet relocated/restored (known limitation,
745    /// next milestone).
746    ///
747    /// On error the child may be left half-built; the caller should drop/kill the
748    /// Sandbox (Drop reaps it).
749    pub async fn restore_interactive(
750        &mut self,
751        cp: &crate::checkpoint::Checkpoint,
752    ) -> Result<Vec<String>, crate::error::SandlockError> {
753        use crate::error::SandboxRuntimeError;
754
755        // The exe to launch is the checkpoint's original binary (within the
756        // policy's fs_read/exec grant). It is never actually execve'd: the child
757        // parks blocked in read() on the ready-pipe, and we inject the checkpoint
758        // over it before it could ever be released. Fall back to a benign command
759        // only when the checkpoint recorded no exe path.
760        let exe = if cp.process_state.exe.is_empty() {
761            "/bin/true".to_string()
762        } else {
763            cp.process_state.exe.clone()
764        };
765        self.create_interactive(&[exe.as_str()]).await?;
766        let pid = self.pid().ok_or(SandboxRuntimeError::NotRunning)?;
767
768        // ptrace is per-thread: the seize, inject, and detach must all run on the
769        // SAME OS thread (the seizing thread becomes the tracer). Do the entire
770        // synchronous sequence inside one spawn_blocking closure with no awaits.
771        // `restore_into` borrows the checkpoint, and spawn_blocking requires a
772        // 'static closure, so move a clone of `cp` in. The clone resets the
773        // policy's runtime to None (Sandbox::clone), which is harmless here:
774        // restore_into reads only process_state + fd_table, never policy.
775        let cp = cp.clone();
776        let skipped = tokio::task::spawn_blocking(
777            move || -> Result<Vec<String>, crate::error::SandlockError> {
778                // PTRACE_SEIZE + PTRACE_INTERRUPT + waitpid to reach the ptrace-stop.
779                crate::checkpoint::capture::ptrace_seize(pid).map_err(|e| {
780                    SandboxRuntimeError::Child(format!("restore ptrace seize {pid}: {e}"))
781                })?;
782                // Inject the checkpoint image; leaves the child stopped with the
783                // saved registers (including rip at the checkpoint pc) loaded.
784                // On error, best-effort detach so the child is not left seized
785                // with a dangling tracer thread.
786                let skipped = match crate::checkpoint::resume::restore_into(pid, &cp) {
787                    Ok(s) => s,
788                    Err(e) => {
789                        let _ = crate::checkpoint::capture::ptrace_detach(pid);
790                        return Err(e);
791                    }
792                };
793                // PTRACE_DETACH resumes the child; because rip points at the
794                // checkpoint pc, it resumes the checkpointed program, abandoning
795                // the ready-pipe read, under the already-installed policy.
796                crate::checkpoint::capture::ptrace_detach(pid).map_err(|e| {
797                    SandboxRuntimeError::Child(format!("restore ptrace detach {pid}: {e}"))
798                })?;
799                Ok(skipped)
800            },
801        )
802        .await
803        .map_err(|e| SandboxRuntimeError::Child(format!("restore join error: {e}")))??;
804
805        Ok(skipped)
806    }
807
808    /// Wait for the child to finish `execve`. Detected by `/proc/<pid>/exe`
809    /// no longer matching `/proc/self/exe` (before execve the child still
810    /// shares the supervisor's binary). The kernel offers no direct event
811    /// for execve completion, so this polls every 1ms with a 5s ceiling.
812    async fn wait_until_exec(&self) -> Result<(), crate::error::SandlockError> {
813        use crate::error::SandboxRuntimeError;
814        let pid = self.pid().ok_or(SandboxRuntimeError::NotRunning)?;
815        let Some(our_exe) = std::fs::read_link("/proc/self/exe").ok() else {
816            return Ok(());
817        };
818        let child_link = format!("/proc/{}/exe", pid);
819        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
820        loop {
821            if let Ok(child_exe) = std::fs::read_link(&child_link) {
822                if child_exe != our_exe {
823                    return Ok(());
824                }
825            }
826            if std::time::Instant::now() >= deadline {
827                return Err(SandboxRuntimeError::Child(
828                    "child did not exec() within 5s".into(),
829                ).into());
830            }
831            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
832        }
833    }
834
835    /// Create with explicit stdin/stdout/stderr fd redirection. Child is
836    /// parked after policy install; call `start()` to release.
837    #[doc(hidden)]
838    pub async fn create_with_io(
839        &mut self,
840        cmd: &[&str],
841        stdin_fd: Option<std::os::unix::io::RawFd>,
842        stdout_fd: Option<std::os::unix::io::RawFd>,
843        stderr_fd: Option<std::os::unix::io::RawFd>,
844    ) -> Result<(), crate::error::SandlockError> {
845        self.ensure_runtime()?;
846        self.rt_mut().io_overrides = Some((stdin_fd, stdout_fd, stderr_fd));
847        self.do_create(cmd, false).await
848    }
849
850    /// Like `create_with_io` but also maps extra fds into the child.
851    #[doc(hidden)]
852    pub async fn create_with_gather_io(
853        &mut self,
854        cmd: &[&str],
855        stdin_fd: Option<std::os::unix::io::RawFd>,
856        stdout_fd: Option<std::os::unix::io::RawFd>,
857        stderr_fd: Option<std::os::unix::io::RawFd>,
858        extra_fds: Vec<(i32, i32)>,
859    ) -> Result<(), crate::error::SandlockError> {
860        self.ensure_runtime()?;
861        self.rt_mut().io_overrides = Some((stdin_fd, stdout_fd, stderr_fd));
862        self.rt_mut().extra_fds = extra_fds;
863        self.do_create(cmd, false).await
864    }
865
866    /// Create a confined child that, instead of `execve`-ing a workload, runs
867    /// `entrypoint` in-process after confinement is installed. The child is a
868    /// `fork()` of this process, so `entrypoint`'s code is already mapped; no
869    /// image is exec'd, so Landlock has nothing to authorize for the child's own
870    /// startup. `extra_fds` maps caller fds onto fixed fd numbers in the child
871    /// (e.g. the control channel). Used to run the OCI in-sandbox PID-1.
872    ///
873    /// `name` is not exec'd; it sets the child's process name
874    /// (`/proc/<pid>/comm`). `start()` releases the parked child to run
875    /// `entrypoint`.
876    pub async fn create_with_in_child_main(
877        &mut self,
878        name: &str,
879        extra_fds: Vec<(i32, i32)>,
880        entrypoint: fn(),
881    ) -> Result<(), crate::error::SandlockError> {
882        self.ensure_runtime()?;
883        self.in_child_main = Some(entrypoint);
884        self.rt_mut().extra_fds = extra_fds;
885        self.do_create(&[name], false).await
886    }
887
888    /// Freeze the sandbox: hold fork notifications + SIGSTOP the process group.
889    pub(crate) async fn freeze(&self) -> Result<(), crate::error::SandlockError> {
890        use crate::error::{SandboxRuntimeError, SandlockError};
891        let rt = self.runtime.as_ref().ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
892        let pid = rt.child_pid.ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
893        if let Some(ref resource) = rt.supervisor_resource {
894            let mut rs = resource.lock().await;
895            rs.hold_forks = true;
896        }
897        unsafe { libc::killpg(pid, libc::SIGSTOP); }
898        Ok(())
899    }
900
901    /// Thaw the sandbox: release held fork notifications + SIGCONT.
902    pub(crate) async fn thaw(&self) -> Result<(), crate::error::SandlockError> {
903        use crate::error::{SandboxRuntimeError, SandlockError};
904        let rt = self.runtime.as_ref().ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
905        let pid = rt.child_pid.ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
906        if let Some(ref resource) = rt.supervisor_resource {
907            let mut rs = resource.lock().await;
908            rs.hold_forks = false;
909            rs.held_notif_ids.clear();
910        }
911        unsafe { libc::killpg(pid, libc::SIGCONT); }
912        Ok(())
913    }
914
915    /// Capture a checkpoint of the running sandbox.
916    pub async fn checkpoint(&self) -> Result<crate::checkpoint::Checkpoint, crate::error::SandlockError> {
917        use crate::error::{SandboxRuntimeError, SandlockError};
918        let pid = self.runtime.as_ref()
919            .and_then(|rt| rt.child_pid)
920            .ok_or(SandlockError::Runtime(SandboxRuntimeError::NotRunning))?;
921        self.checkpoint_pid(pid).await
922    }
923
924    /// Capture a checkpoint targeting a specific pid instead of the sandbox's
925    /// direct child. The target must be a fork-descendant confined by the same
926    /// policy (e.g. the workload spawned by sandlock-init). `target_pid` must
927    /// be positive.
928    pub async fn checkpoint_pid(&self, target_pid: i32) -> Result<crate::checkpoint::Checkpoint, crate::error::SandlockError> {
929        use crate::error::{SandboxRuntimeError, SandlockError};
930        if target_pid <= 0 {
931            return Err(SandlockError::Runtime(SandboxRuntimeError::NotRunning));
932        }
933        self.freeze().await?;
934        let cp = crate::checkpoint::capture(target_pid, self);
935        self.thaw().await?;
936        cp
937    }
938
939    // ================================================================
940    // One-shot / lifecycle instance API
941    // ================================================================
942
943    /// One-shot: spawn, wait, and return the result. Stdout and stderr are
944    /// captured. This is the primary way to run a sandboxed command:
945    ///
946    /// ```ignore
947    /// let mut sandbox = Sandbox::builder()
948    ///     .fs_read("/usr")
949    ///     .name("my-sandbox")
950    ///     .build()?;
951    /// let result = sandbox.run(&["echo", "hello"]).await?;
952    /// ```
953    pub async fn run(
954        &mut self,
955        cmd: &[&str],
956    ) -> Result<crate::result::RunResult, crate::error::SandlockError> {
957        self.do_create(cmd, true).await?;
958        self.do_start()?;
959        self.wait().await
960    }
961
962    /// Run with inherited stdio (interactive mode).
963    pub async fn run_interactive(
964        &mut self,
965        cmd: &[&str],
966    ) -> Result<crate::result::RunResult, crate::error::SandlockError> {
967        self.do_create(cmd, false).await?;
968        self.do_start()?;
969        self.wait().await
970    }
971
972    /// One-shot run with user-supplied syscall handlers.
973    pub async fn run_with_handlers<I, S, H>(
974        &mut self,
975        cmd: &[&str],
976        handlers: I,
977    ) -> Result<crate::result::RunResult, crate::error::SandlockError>
978    where
979        I: IntoIterator<Item = (S, H)>,
980        S: TryInto<crate::seccomp::syscall::Syscall, Error = crate::seccomp::syscall::SyscallError>,
981        H: crate::seccomp::dispatch::Handler,
982    {
983        let pending = sandbox_collect_handlers(handlers, self)?;
984        self.ensure_runtime()?;
985        self.rt_mut().handlers = pending;
986        self.do_create(cmd, true).await?;
987        self.do_start()?;
988        self.wait().await
989    }
990
991    /// Interactive-stdio counterpart of `run_with_handlers`.
992    pub async fn run_interactive_with_handlers<I, S, H>(
993        &mut self,
994        cmd: &[&str],
995        handlers: I,
996    ) -> Result<crate::result::RunResult, crate::error::SandlockError>
997    where
998        I: IntoIterator<Item = (S, H)>,
999        S: TryInto<crate::seccomp::syscall::Syscall, Error = crate::seccomp::syscall::SyscallError>,
1000        H: crate::seccomp::dispatch::Handler,
1001    {
1002        let pending = sandbox_collect_handlers(handlers, self)?;
1003        self.ensure_runtime()?;
1004        self.rt_mut().handlers = pending;
1005        self.do_create(cmd, false).await?;
1006        self.do_start()?;
1007        self.wait().await
1008    }
1009
1010    /// Dry-run: create, start, wait, collect filesystem changes, then abort.
1011    pub async fn dry_run(
1012        &mut self,
1013        cmd: &[&str],
1014    ) -> Result<crate::dry_run::DryRunResult, crate::error::SandlockError> {
1015        self.on_exit = BranchAction::Keep;
1016        self.on_error = BranchAction::Keep;
1017        self.do_create(cmd, true).await?;
1018        self.do_start()?;
1019        let run_result = self.wait().await?;
1020        let changes = self.collect_changes().await;
1021        self.do_abort().await;
1022        Ok(crate::dry_run::DryRunResult { run_result, changes })
1023    }
1024
1025    /// Dry-run with inherited stdio.
1026    pub async fn dry_run_interactive(
1027        &mut self,
1028        cmd: &[&str],
1029    ) -> Result<crate::dry_run::DryRunResult, crate::error::SandlockError> {
1030        self.on_exit = BranchAction::Keep;
1031        self.on_error = BranchAction::Keep;
1032        self.do_create(cmd, false).await?;
1033        self.do_start()?;
1034        let run_result = self.wait().await?;
1035        let changes = self.collect_changes().await;
1036        self.do_abort().await;
1037        Ok(crate::dry_run::DryRunResult { run_result, changes })
1038    }
1039
1040    /// Create N COW clones of this sandbox.
1041    ///
1042    /// `fork()` requires `init_fn` and `work_fn` to be set on the sandbox (via
1043    /// `SandboxBuilder::init_fn` / `work_fn`, or `Sandbox::with_init_fn` /
1044    /// `with_work_fn`). Returns an error if either is missing.
1045    pub async fn fork(&mut self, n: u32) -> Result<Vec<Sandbox>, crate::error::SandlockError> {
1046        use crate::error::SandboxRuntimeError;
1047        use std::os::fd::{FromRawFd, OwnedFd};
1048
1049        // Pull init_fn / work_fn directly from self (they live on Sandbox, not
1050        // Runtime, so ensure_runtime hasn't consumed them yet).
1051        let init_fn = self.init_fn.take()
1052            .ok_or_else(|| SandboxRuntimeError::Child("fork() requires init_fn and work_fn — use SandboxBuilder::init_fn() / work_fn() or Sandbox::with_init_fn() / with_work_fn()".into()))?;
1053        let work_fn = self.work_fn.take()
1054            .ok_or_else(|| SandboxRuntimeError::Child("fork() requires init_fn and work_fn — use SandboxBuilder::init_fn() / work_fn() or Sandbox::with_init_fn() / with_work_fn()".into()))?;
1055
1056        // Initialize the runtime block so we can record child PID / state below.
1057        self.ensure_runtime()?;
1058
1059        let sandbox_cfg = self.clone(); // config only, no runtime
1060
1061        let mut ctrl_fds = [0i32; 2];
1062        if unsafe { libc::pipe2(ctrl_fds.as_mut_ptr(), 0) } < 0 {
1063            return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
1064        }
1065        let ctrl_parent = unsafe { OwnedFd::from_raw_fd(ctrl_fds[0]) };
1066        let ctrl_child_fd = ctrl_fds[1];
1067
1068        let mut pipe_read_ends: Vec<OwnedFd> = Vec::with_capacity(n as usize);
1069        let mut pipe_write_fds: Vec<i32> = Vec::with_capacity(n as usize);
1070        for _ in 0..n {
1071            let mut pfds = [0i32; 2];
1072            if unsafe { libc::pipe(pfds.as_mut_ptr()) } >= 0 {
1073                pipe_read_ends.push(unsafe { OwnedFd::from_raw_fd(pfds[0]) });
1074                pipe_write_fds.push(pfds[1]);
1075            } else {
1076                pipe_write_fds.push(-1);
1077            }
1078        }
1079
1080        let pid = unsafe { libc::fork() };
1081        if pid < 0 {
1082            unsafe { libc::close(ctrl_child_fd) };
1083            return Err(SandboxRuntimeError::Fork(std::io::Error::last_os_error()).into());
1084        }
1085
1086        if pid == 0 {
1087            drop(ctrl_parent);
1088            unsafe { libc::setpgid(0, 0) };
1089            unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) };
1090            unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
1091
1092            let _ = crate::landlock::confine(&sandbox_cfg);
1093
1094            let deny = crate::context::blocklist_syscall_numbers(&sandbox_cfg);
1095            let args = crate::context::arg_filters(&sandbox_cfg);
1096            let filter = match crate::seccomp::bpf::assemble_filter(&[], &deny, &args) {
1097                Ok(f) => f,
1098                Err(_) => unsafe { libc::_exit(1) },
1099            };
1100            let _ = crate::seccomp::bpf::install_deny_filter(&filter);
1101
1102            init_fn();
1103
1104            drop(pipe_read_ends);
1105            crate::fork::fork_ready_loop_fn(ctrl_child_fd, n, &*work_fn, &pipe_write_fds);
1106            unsafe { libc::_exit(0) };
1107        }
1108
1109        unsafe { libc::close(ctrl_child_fd) };
1110        for wfd in &pipe_write_fds {
1111            if *wfd >= 0 { unsafe { libc::close(*wfd) }; }
1112        }
1113        self.rt_mut().child_pid = Some(pid);
1114        self.rt_mut().state = RuntimeState::Running;
1115
1116        let ctrl_fd = ctrl_parent.as_raw_fd();
1117        let mut pid_buf = vec![0u8; n as usize * 4];
1118        sandbox_read_exact(ctrl_fd, &mut pid_buf);
1119
1120        let clone_pids: Vec<i32> = pid_buf.chunks(4)
1121            .map(|c| u32::from_be_bytes(c.try_into().unwrap_or([0; 4])) as i32)
1122            .collect();
1123        let live_count = clone_pids.iter().filter(|&&p| p > 0).count();
1124
1125        let mut code_buf = vec![0u8; live_count * 4];
1126        sandbox_read_exact(ctrl_fd, &mut code_buf);
1127        self.rt_mut().ctrl_fd = Some(ctrl_parent);
1128
1129        let mut status = 0i32;
1130        unsafe { libc::waitpid(pid, &mut status, 0) };
1131
1132        let mut code_idx = 0;
1133        let mut clones = Vec::with_capacity(live_count);
1134        let mut pipe_iter = pipe_read_ends.into_iter();
1135
1136        let rt_name = self.rt().name.clone();
1137        for &clone_pid in &clone_pids {
1138            let pipe = pipe_iter.next();
1139            if clone_pid <= 0 { continue; }
1140
1141            let code = i32::from_be_bytes(
1142                code_buf[code_idx * 4..(code_idx + 1) * 4].try_into().unwrap_or([0; 4])
1143            );
1144            code_idx += 1;
1145
1146            let mut clone_sb = sandbox_cfg.clone();
1147            let clone_name = format!("{}-fork-{}", rt_name, clone_pid);
1148            clone_sb.runtime = Some(Box::new(Runtime {
1149                name: clone_name,
1150                state: RuntimeState::Stopped(if code == 0 {
1151                    crate::result::ExitStatus::Code(0)
1152                } else if code > 0 {
1153                    crate::result::ExitStatus::Code(code)
1154                } else {
1155                    crate::result::ExitStatus::Killed
1156                }),
1157                child_pid: Some(clone_pid),
1158                pidfd: None,
1159                notif_handle: None,
1160                throttle_handle: None,
1161                loadavg_handle: None,
1162                _stdout_read: None,
1163                _stderr_read: None,
1164                seccomp_cow: None,
1165                supervisor_resource: None,
1166                supervisor_cow: None,
1167                supervisor_network: None,
1168                ctrl_fd: None,
1169                stdout_pipe: pipe,
1170                io_overrides: None,
1171                extra_fds: Vec::new(),
1172                http_acl_handle: None,
1173                on_bind: None,
1174                handlers: Vec::new(),
1175                ready_w: None,
1176            }));
1177            clones.push(clone_sb);
1178        }
1179
1180        Ok(clones)
1181    }
1182
1183    /// Reduce: wait for all clones, then run a reducer command.
1184    pub async fn reduce(
1185        &self,
1186        cmd: &[&str],
1187        clones: &mut [Sandbox],
1188    ) -> Result<crate::result::RunResult, crate::error::SandlockError> {
1189        use crate::error::SandboxRuntimeError;
1190
1191        let mut combined = Vec::new();
1192        for clone in clones.iter_mut() {
1193            if let Some(ref mut rt) = clone.runtime {
1194                if let Some(pipe) = rt.stdout_pipe.take() {
1195                    combined.extend_from_slice(&sandbox_read_fd_to_end(pipe));
1196                }
1197            }
1198        }
1199
1200        let mut stdin_fds = [0i32; 2];
1201        if unsafe { libc::pipe2(stdin_fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
1202            return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
1203        }
1204
1205        let write_fd = stdin_fds[1];
1206        let write_handle = tokio::task::spawn_blocking(move || {
1207            unsafe {
1208                libc::write(write_fd, combined.as_ptr() as *const _, combined.len());
1209                libc::close(write_fd);
1210            }
1211        });
1212
1213        let base_name = self.instance_name()
1214            .unwrap_or("sandbox")
1215            .to_owned();
1216        let reducer_name = base_name + "-reduce";
1217        let mut reducer = self.clone().with_name(reducer_name);
1218        reducer.ensure_runtime()?;
1219        reducer.rt_mut().io_overrides = Some((Some(stdin_fds[0]), None, None));
1220        reducer.do_create(cmd, true).await?;
1221        reducer.do_start()?;
1222        unsafe { libc::close(stdin_fds[0]) };
1223
1224        let _ = write_handle.await;
1225        reducer.wait().await
1226    }
1227
1228    /// Whether named (pathname) `AF_UNIX` connects should be gated by the
1229    /// fs-write grants (`has_unix_fs_gate`). Active whenever the sandbox
1230    /// confines the filesystem; Landlock cannot gate unix-socket connect, so
1231    /// the seccomp layer does. Single source of truth for both the
1232    /// `NotifPolicy` flag and the `notif_syscalls` BPF set.
1233    pub(crate) fn has_unix_fs_gate(&self) -> bool {
1234        !self.fs_readable.is_empty() || !self.fs_writable.is_empty()
1235    }
1236
1237    /// Lazily initialize the runtime block.
1238    ///
1239    /// Called by lifecycle methods (`spawn`, `run`, `fork`, etc.) on first
1240    /// use. Validates and resolves the sandbox name. Idempotent: returns
1241    /// immediately if runtime is already set.
1242    fn ensure_runtime(&mut self) -> Result<(), crate::error::SandlockError> {
1243        if self.runtime.is_some() {
1244            return Ok(());
1245        }
1246        let name = sandbox_resolve_name(self.name.as_deref())?;
1247        self.runtime = Some(Box::new(Runtime {
1248            name,
1249            state: RuntimeState::Created,
1250            child_pid: None,
1251            pidfd: None,
1252            notif_handle: None,
1253            throttle_handle: None,
1254            loadavg_handle: None,
1255            _stdout_read: None,
1256            _stderr_read: None,
1257            seccomp_cow: None,
1258            supervisor_resource: None,
1259            supervisor_cow: None,
1260            supervisor_network: None,
1261            ctrl_fd: None,
1262            stdout_pipe: None,
1263            io_overrides: None,
1264            extra_fds: Vec::new(),
1265            http_acl_handle: None,
1266            on_bind: None,
1267            handlers: Vec::new(),
1268            ready_w: None,
1269        }));
1270        Ok(())
1271    }
1272
1273    // ================================================================
1274    // Internal: collect_changes / do_abort
1275    // ================================================================
1276
1277    async fn collect_changes(&self) -> Vec<crate::dry_run::Change> {
1278        if let Some(ref rt) = self.runtime {
1279            if let Some(ref cow) = rt.seccomp_cow {
1280                return cow.changes().unwrap_or_default();
1281            }
1282        }
1283        Vec::new()
1284    }
1285
1286    async fn do_abort(&mut self) {
1287        if let Some(ref mut rt) = self.runtime {
1288            if let Some(ref mut cow) = rt.seccomp_cow {
1289                let _ = cow.abort();
1290            }
1291        }
1292    }
1293
1294    // ================================================================
1295    // Internal: do_create (fork + policy install; child parks at the
1296    // ready_r read, awaiting do_start to release it to execve).
1297    // ================================================================
1298
1299    async fn do_create(&mut self, cmd: &[&str], capture: bool) -> Result<(), crate::error::SandlockError> {
1300        use std::ffi::CString;
1301        use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
1302        use crate::error::SandboxRuntimeError;
1303        use crate::context::{PipePair, read_u32_fd};
1304        use crate::network;
1305        use crate::seccomp::ctx::SupervisorCtx;
1306        use crate::seccomp::notif::{self, NotifPolicy};
1307        use crate::seccomp::state::{ChrootState, CowState, NetworkState, PolicyFnState, ProcfsState, ResourceState, TimeRandomState};
1308        use crate::sys::syscall;
1309        use std::time::Duration;
1310
1311        self.ensure_runtime()?;
1312
1313        if !matches!(self.rt().state, RuntimeState::Created) {
1314            return Err(SandboxRuntimeError::Child("sandbox already spawned".into()).into());
1315        }
1316
1317        if cmd.is_empty() {
1318            return Err(SandboxRuntimeError::Child("empty command".into()).into());
1319        }
1320
1321        // Resolve the chroot root eagerly, before any fork or confinement work:
1322        // a configured-but-missing chroot must be a hard error, never a silent
1323        // drop to "no confinement".
1324        let chroot_root = crate::chroot::resolve::resolve_chroot_root(self.chroot.as_deref())?;
1325
1326        // Each --http-inject-ca target must exist in the sandbox's view, or the
1327        // CA cannot be spliced into it and TLS interception silently fails. A
1328        // configured-but-missing trust bundle is a hard error, resolved through
1329        // --fs-mount and chroot so the check matches the workload's view.
1330        if !self.http_inject_ca.is_empty() {
1331            let mounts = crate::chroot::resolve::resolve_chroot_mounts(&self.fs_mount);
1332            for p in &self.http_inject_ca {
1333                let host = resolve_sandbox_path_to_host(p, chroot_root.as_deref(), &mounts);
1334                if !host.exists() {
1335                    return Err(SandboxRuntimeError::Child(format!(
1336                        "--http-inject-ca {:?} not found in the sandbox view (resolved to {:?}); \
1337                         the CA cannot be injected into it. Point it at the trust bundle the \
1338                         workload actually reads (e.g. /etc/ssl/certs/ca-certificates.crt, or \
1339                         certifi's cacert.pem).",
1340                        p, host
1341                    ))
1342                    .into());
1343                }
1344            }
1345        }
1346
1347        let c_cmd: Vec<CString> = cmd
1348            .iter()
1349            .map(|s| CString::new(*s).map_err(|_| SandboxRuntimeError::Child("invalid command string".into())))
1350            .collect::<Result<Vec<_>, _>>()?;
1351
1352        let no_supervisor = self.no_supervisor;
1353
1354        let pipes = PipePair::new().map_err(SandboxRuntimeError::Io)?;
1355
1356        let resolved_net_allow = network::resolve_net_allow(&self.net_allow)
1357            .await
1358            .map_err(SandboxRuntimeError::Io)?;
1359        // In chroot/image mode, seed the synthetic /etc/hosts from the
1360        // rootfs's own file so entries baked into the image (private
1361        // registries, internal hostnames, etc.) survive virtualization.
1362        // Without a chroot, the helper returns the fixed loopback base.
1363        // Either way, concrete-host rules from `net_allow` are appended
1364        // on top.
1365        let virtual_etc_hosts = network::compose_virtual_etc_hosts(
1366            self.chroot.as_deref(),
1367            &resolved_net_allow.concrete_host_entries,
1368        );
1369
1370        let mut ca_inject_pem: Option<std::sync::Arc<Vec<u8>>> = None;
1371        if !self.http_allow.is_empty() || !self.http_deny.is_empty() {
1372            // Generate an ephemeral CA when injection is requested without BYO.
1373            let generate = !self.http_inject_ca.is_empty();
1374            let ca_material = crate::transparent_proxy::resolve_ca(
1375                self.http_ca.as_deref(),
1376                self.http_key.as_deref(),
1377                generate,
1378            )
1379            .map_err(SandboxRuntimeError::Io)?;
1380
1381            // Export the public cert if requested.
1382            if let (Some(out), Some(cm)) = (self.http_ca_out.as_deref(), ca_material.as_ref()) {
1383                std::fs::write(out, cm.cert_pem.as_bytes()).map_err(SandboxRuntimeError::Io)?;
1384            }
1385
1386            // Keep the public cert for trust injection (only when paths declared).
1387            if !self.http_inject_ca.is_empty() {
1388                if let Some(cm) = ca_material.as_ref() {
1389                    ca_inject_pem = Some(std::sync::Arc::new(cm.cert_pem.clone().into_bytes()));
1390                }
1391            }
1392
1393            let (cert_pem, key_pem) = match ca_material.as_ref() {
1394                Some(cm) => (Some(cm.cert_pem.as_str()), Some(cm.key_pem.as_str())),
1395                None => (None, None),
1396            };
1397
1398            let handle = crate::transparent_proxy::spawn_transparent_proxy(
1399                self.http_allow.clone(),
1400                self.http_deny.clone(),
1401                cert_pem,
1402                key_pem,
1403            )
1404            .await
1405            .map_err(SandboxRuntimeError::Io)?;
1406            self.rt_mut().http_acl_handle = Some(handle);
1407        }
1408
1409        // Seccomp COW: create the branch before fork so the child's Landlock
1410        // ruleset can include the upper layer. Binaries created inside the
1411        // workdir live in the upper dir, and Landlock checks EXECUTE on the
1412        // file's real path at execve time — so the upper dir must be granted
1413        // read+execute (READ_ACCESS) or `./created-binary` fails with EACCES.
1414        let seccomp_cow_branch = if !no_supervisor && self.workdir.is_some() {
1415            let workdir = self.workdir.as_ref().unwrap().clone();
1416            let storage = self.fs_storage.clone();
1417            let max_disk = self.max_disk.map(|b| b.0).unwrap_or(0);
1418            match crate::cow::seccomp::SeccompCowBranch::create(&workdir, storage.as_deref(), max_disk) {
1419                Ok(branch) => {
1420                    self.fs_readable.push(branch.upper_dir().to_path_buf());
1421                    Some(branch)
1422                }
1423                Err(e) => {
1424                    eprintln!("sandlock: seccomp COW branch creation failed: {}", e);
1425                    None
1426                }
1427            }
1428        } else {
1429            None
1430        };
1431
1432        let handler_syscalls: Vec<i64> = self.rt().handlers.iter().map(|(nr, _)| *nr).collect();
1433        let resolved_sandbox_name = self.rt().name.clone();
1434        let resolved = crate::resolved::ResolvedSandbox::from_sandbox(
1435            self,
1436            Some(resolved_sandbox_name.as_str()),
1437            &handler_syscalls,
1438        );
1439
1440        let (stdout_r, stderr_r) = if capture {
1441            let mut stdout_fds = [0i32; 2];
1442            let mut stderr_fds = [0i32; 2];
1443            if unsafe { libc::pipe2(stdout_fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
1444                return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
1445            }
1446            if unsafe { libc::pipe2(stderr_fds.as_mut_ptr(), libc::O_CLOEXEC) } < 0 {
1447                unsafe {
1448                    libc::close(stdout_fds[0]);
1449                    libc::close(stdout_fds[1]);
1450                }
1451                return Err(SandboxRuntimeError::Io(std::io::Error::last_os_error()).into());
1452            }
1453            (
1454                Some((
1455                    unsafe { OwnedFd::from_raw_fd(stdout_fds[0]) },
1456                    unsafe { OwnedFd::from_raw_fd(stdout_fds[1]) },
1457                )),
1458                Some((
1459                    unsafe { OwnedFd::from_raw_fd(stderr_fds[0]) },
1460                    unsafe { OwnedFd::from_raw_fd(stderr_fds[1]) },
1461                )),
1462            )
1463        } else {
1464            (None, None)
1465        };
1466
1467        // Capture our PID before fork so the child can detect parent death
1468        // without assuming PID 1 is always init (wrong in containers).
1469        let parent_pid = unsafe { libc::getpid() };
1470
1471        let pid = unsafe { libc::fork() };
1472        if pid < 0 {
1473            return Err(SandboxRuntimeError::Fork(std::io::Error::last_os_error()).into());
1474        }
1475
1476        if pid == 0 {
1477            // ===== CHILD PROCESS =====
1478            let io_overrides = self.rt().io_overrides;
1479            if let Some((stdin_fd, stdout_fd, stderr_fd)) = io_overrides {
1480                if let Some(fd) = stdin_fd { unsafe { libc::dup2(fd, 0) }; }
1481                if let Some(fd) = stdout_fd { unsafe { libc::dup2(fd, 1) }; }
1482                if let Some(fd) = stderr_fd { unsafe { libc::dup2(fd, 2) }; }
1483            }
1484
1485            let extra_fds_copy = self.rt().extra_fds.clone();
1486            for &(target_fd, source_fd) in &extra_fds_copy {
1487                unsafe { libc::dup2(source_fd, target_fd) };
1488            }
1489
1490            if let Some((_, ref stdout_w)) = stdout_r {
1491                unsafe { libc::dup2(stdout_w.as_raw_fd(), 1) };
1492            }
1493            if let Some((_, ref stderr_w)) = stderr_r {
1494                unsafe { libc::dup2(stderr_w.as_raw_fd(), 2) };
1495            }
1496            drop(stdout_r);
1497            drop(stderr_r);
1498
1499            let gather_keep_fds: Vec<i32> = extra_fds_copy.iter().map(|&(target, _)| target).collect();
1500
1501            let extra_syscalls: Vec<u32> = self.rt().handlers
1502                .iter()
1503                .map(|h| h.0 as u32)
1504                .collect();
1505
1506            let sandbox_name = self.rt().name.clone();
1507            // In-process entrypoint (OCI PID-1) names the process from cmd[0];
1508            // otherwise execve the command.
1509            let entry = match self.in_child_main {
1510                Some(run) => context::ChildEntry::InProcess { name: c_cmd[0].as_c_str(), run },
1511                None => context::ChildEntry::Exec(&c_cmd),
1512            };
1513            context::confine_child(context::ChildSpawnArgs {
1514                sandbox: self,
1515                entry,
1516                pipes: &pipes,
1517                no_supervisor,
1518                keep_fds: &gather_keep_fds,
1519                sandbox_name: Some(sandbox_name.as_str()),
1520                extra_syscalls: &extra_syscalls,
1521                parent_pid,
1522            });
1523        }
1524
1525        // ===== PARENT PROCESS =====
1526        drop(pipes.notif_w);
1527        drop(pipes.ready_r);
1528
1529        self.rt_mut()._stdout_read = stdout_r.map(|(r, _w)| r);
1530        self.rt_mut()._stderr_read = stderr_r.map(|(r, _w)| r);
1531
1532        self.rt_mut().child_pid = Some(pid);
1533        // State remains `Created` until `do_start` writes ready_w to release
1534        // the child to execve.
1535
1536        let pidfd = match syscall::pidfd_open(pid as u32, 0) {
1537            Ok(fd) => Some(fd),
1538            Err(_) => None,
1539        };
1540
1541        let notif_fd_num = read_u32_fd(pipes.notif_r.as_raw_fd())
1542            .map_err(|e| SandboxRuntimeError::Child(format!("read notif fd from child: {}", e)))?;
1543
1544        let is_nested_mode = notif_fd_num == 0;
1545
1546        let notif_fd = if is_nested_mode {
1547            None
1548        } else if let Some(ref pfd) = pidfd {
1549            Some(syscall::pidfd_getfd(pfd, notif_fd_num as i32, 0)
1550                .map_err(|e| SandboxRuntimeError::Child(format!("pidfd_getfd: {}", e)))?)
1551        } else {
1552            let path = format!("/proc/{}/fd/{}", pid, notif_fd_num);
1553            let cpath = CString::new(path).unwrap();
1554            let raw = unsafe { libc::open(cpath.as_ptr(), libc::O_RDWR) };
1555            if raw < 0 {
1556                return Err(SandboxRuntimeError::Child("failed to open notif fd from /proc".into()).into());
1557            }
1558            Some(unsafe { OwnedFd::from_raw_fd(raw) })
1559        };
1560
1561        if let Some(notif_fd) = notif_fd {
1562            if self.time_start.is_some() || self.random_seed.is_some() {
1563                let time_offset = self.time_start.map(|t| crate::time::calculate_time_offset(t));
1564                if let Err(e) = crate::vdso::patch(pid, time_offset, self.random_seed.is_some()) {
1565                    eprintln!("sandlock: pre-exec vDSO patching failed (will retry after exec): {}", e);
1566                }
1567            }
1568
1569            let time_offset_val = self.time_start
1570                .map(|t| crate::time::calculate_time_offset(t))
1571                .unwrap_or(0);
1572
1573            let rt_name = self.rt().name.clone();
1574            let notif_policy = NotifPolicy {
1575                max_memory_bytes: self.max_memory.map(|m| m.0).unwrap_or(0),
1576                max_processes: self.max_processes,
1577                has_memory_limit: resolved.features.memory_limit,
1578                has_net_destination_policy: resolved.features.network_destination_policy,
1579                has_bind_denylist: resolved.features.bind_denylist,
1580                has_unix_fs_gate: resolved.features.unix_fs_gate,
1581                has_random_seed: resolved.features.random_seed,
1582                has_time_start: resolved.features.time_start,
1583                argv_safety_required: resolved.features.argv_safety_required,
1584                time_offset: time_offset_val,
1585                num_cpus: self.num_cpus,
1586                port_remap: resolved.features.port_remap,
1587                cow_enabled: resolved.features.cow,
1588                chroot_root: chroot_root.clone(),
1589                chroot_readable: self.fs_readable.clone(),
1590                chroot_writable: self.fs_writable.clone(),
1591                chroot_denied: self.fs_denied.clone(),
1592                chroot_mounts: crate::chroot::resolve::resolve_chroot_mounts(&self.fs_mount),
1593                chroot_mount_ro: self.fs_mount_ro.clone(),
1594                deterministic_dirs: self.deterministic_dirs,
1595                virtual_hostname: Some(rt_name),
1596                has_http_acl: resolved.features.http_acl,
1597                virtual_etc_hosts,
1598                ca_inject_paths: self.http_inject_ca.clone(),
1599                ca_inject_pem: ca_inject_pem.clone(),
1600            };
1601
1602            use rand::SeedableRng;
1603            use rand_chacha::ChaCha8Rng;
1604
1605            let random_state = self.random_seed.map(|seed| ChaCha8Rng::seed_from_u64(seed));
1606            let time_offset = self.time_start.map(|t| crate::time::calculate_time_offset(t));
1607
1608            let time_random_state = TimeRandomState::new(time_offset, random_state);
1609
1610            let mut net_state = NetworkState::new();
1611            if !self.net_deny.is_empty() {
1612                let resolved_deny = network::resolve_net_deny(&self.net_deny);
1613                net_state.tcp_policy = resolved_deny.tcp;
1614                net_state.udp_policy = resolved_deny.udp;
1615                net_state.icmp_policy = resolved_deny.icmp;
1616            } else {
1617                let no_rules = self.net_allow.is_empty();
1618                let policy_from = |resolved: &network::ResolvedNetAllow| {
1619                    if no_rules || resolved.any_ip_all_ports {
1620                        crate::seccomp::notif::NetworkPolicy::Unrestricted
1621                    } else {
1622                        use crate::seccomp::notif::PortAllow;
1623                        let per_ip = resolved
1624                            .per_ip
1625                            .iter()
1626                            .map(|(ip, ports)| {
1627                                let allow = if resolved.per_ip_all_ports.contains(ip) {
1628                                    PortAllow::Any
1629                                } else {
1630                                    PortAllow::Specific(ports.clone())
1631                                };
1632                                (*ip, allow)
1633                            })
1634                            .collect();
1635                        crate::seccomp::notif::NetworkPolicy::AllowList {
1636                            per_ip,
1637                            cidrs: resolved.cidrs.clone(),
1638                            any_ip_ports: resolved.any_ip_ports.clone(),
1639                        }
1640                    }
1641                };
1642                net_state.tcp_policy = policy_from(&resolved_net_allow.tcp);
1643                net_state.udp_policy = policy_from(&resolved_net_allow.udp);
1644                net_state.icmp_policy = policy_from(&resolved_net_allow.icmp);
1645            }
1646            net_state.http_acl_addr = self.rt().http_acl_handle.as_ref().map(|h| h.addr);
1647            net_state.http_acl_ports = self.http_ports.iter().copied().collect();
1648            net_state.http_acl_orig_dest = self.rt().http_acl_handle.as_ref().map(|h| h.orig_dest.clone());
1649            net_state.bind_deny_ports = self.net_deny_bind.iter().copied().collect();
1650            if let Some(cb) = self.rt_mut().on_bind.take() {
1651                net_state.port_map.on_bind = Some(cb);
1652            }
1653
1654            let procfs_state = ProcfsState::new();
1655
1656            let mut res_state = ResourceState::new(
1657                notif_policy.max_memory_bytes,
1658                notif_policy.max_processes,
1659            );
1660            res_state.proc_count = 1;
1661
1662            let mut cow_state = CowState::new();
1663            cow_state.branch = seccomp_cow_branch;
1664
1665            let mut policy_fn_state = PolicyFnState::new();
1666
1667            for path in &self.fs_denied {
1668                // Captures the path prefix and the file's inode identity, so
1669                // the deny survives hardlinks/renames to a non-denied name.
1670                policy_fn_state.denied.deny(&path.to_string_lossy());
1671            }
1672
1673            if let Some(ref callback) = self.policy_fn {
1674                let mut allowed_ips: std::collections::HashSet<std::net::IpAddr> =
1675                    std::collections::HashSet::new();
1676                for p in [&net_state.tcp_policy, &net_state.udp_policy, &net_state.icmp_policy] {
1677                    if let crate::seccomp::notif::NetworkPolicy::AllowList { per_ip, cidrs, .. } = p {
1678                        allowed_ips.extend(per_ip.keys().copied());
1679                        // IP literals resolve to single-host CIDRs (/32 or
1680                        // /128); surface them as concrete allowed IPs too.
1681                        for (net, _) in cidrs {
1682                            if net.is_single_host() {
1683                                allowed_ips.insert(net.addr);
1684                            }
1685                        }
1686                    }
1687                }
1688                let live = crate::policy_fn::LivePolicy {
1689                    allowed_ips,
1690                    max_memory_bytes: notif_policy.max_memory_bytes,
1691                    max_processes: notif_policy.max_processes,
1692                };
1693                let ceiling = live.clone();
1694                let live = std::sync::Arc::new(std::sync::RwLock::new(live));
1695                let denied = policy_fn_state.denied.clone();
1696                let pid_overrides = net_state.pid_ip_overrides.clone();
1697                policy_fn_state.live_policy = Some(live.clone());
1698                let tx = crate::policy_fn::spawn_policy_fn(
1699                    callback.clone(), live, ceiling, pid_overrides, denied,
1700                );
1701                policy_fn_state.event_tx = Some(tx);
1702            }
1703
1704            let chroot_state = ChrootState::new();
1705
1706            let notif_raw_fd = notif_fd.as_raw_fd();
1707            let child_pidfd_raw = pidfd.as_ref().map(|pfd| pfd.as_raw_fd());
1708
1709            let res_state = Arc::new(tokio::sync::Mutex::new(res_state));
1710            self.rt_mut().supervisor_resource = Some(Arc::clone(&res_state));
1711
1712            let cow_state = Arc::new(tokio::sync::Mutex::new(cow_state));
1713            self.rt_mut().supervisor_cow = Some(Arc::clone(&cow_state));
1714
1715            let net_state = Arc::new(tokio::sync::Mutex::new(net_state));
1716            self.rt_mut().supervisor_network = Some(Arc::clone(&net_state));
1717
1718            let procfs_state = Arc::new(tokio::sync::Mutex::new(procfs_state));
1719            let time_random_state = Arc::new(tokio::sync::Mutex::new(time_random_state));
1720            let policy_fn_state = Arc::new(tokio::sync::Mutex::new(policy_fn_state));
1721            let chroot_state = Arc::new(tokio::sync::Mutex::new(chroot_state));
1722            let processes = Arc::new(crate::seccomp::state::ProcessIndex::new());
1723
1724            let ctx = Arc::new(SupervisorCtx {
1725                resource: Arc::clone(&res_state),
1726                cow: Arc::clone(&cow_state),
1727                procfs: Arc::clone(&procfs_state),
1728                network: Arc::clone(&net_state),
1729                time_random: Arc::clone(&time_random_state),
1730                policy_fn: Arc::clone(&policy_fn_state),
1731                chroot: Arc::clone(&chroot_state),
1732                netlink: Arc::new(crate::netlink::NetlinkState::new()),
1733                processes: Arc::clone(&processes),
1734                policy: Arc::new(notif_policy),
1735                child_pidfd: child_pidfd_raw,
1736                notif_fd: notif_raw_fd,
1737            });
1738
1739            let handlers = std::mem::take(&mut self.rt_mut().handlers);
1740            let (startup_tx, startup_rx) = tokio::sync::oneshot::channel();
1741            self.rt_mut().notif_handle = Some(tokio::spawn(
1742                notif::supervisor(notif_fd, ctx, handlers, startup_tx),
1743            ));
1744            // Wait for the supervisor to register the notif fd with the IO
1745            // driver before we release the child to execve. Otherwise an
1746            // early traced syscall would queue a notification on a fd no
1747            // one is polling, and the child would block until the next
1748            // `block_on` re-enters the runtime. Critical for current-thread
1749            // runtimes, harmless overhead for multi-thread.
1750            match startup_rx.await {
1751                Ok(Ok(())) => {}
1752                Ok(Err(e)) => return Err(SandboxRuntimeError::Io(e).into()),
1753                Err(_) => {
1754                    return Err(SandboxRuntimeError::Child(
1755                        "seccomp supervisor exited during startup".into(),
1756                    ).into());
1757                }
1758            }
1759
1760            let la_resource = Arc::clone(&res_state);
1761            self.rt_mut().loadavg_handle = Some(tokio::spawn(async move {
1762                let mut interval = tokio::time::interval(Duration::from_secs(5));
1763                interval.tick().await;
1764                loop {
1765                    interval.tick().await;
1766                    let mut rs = la_resource.lock().await;
1767                    let running = rs.proc_count;
1768                    rs.load_avg.sample(running);
1769                }
1770            }));
1771        }
1772
1773        if let Some(cpu_pct) = self.max_cpu {
1774            if cpu_pct < 100 {
1775                let child_pid = pid;
1776                self.rt_mut().throttle_handle = Some(tokio::spawn(sandbox_throttle_cpu(child_pid, cpu_pct)));
1777            }
1778        }
1779
1780        self.rt_mut().pidfd = pidfd;
1781        self.rt_mut().ready_w = Some(pipes.ready_w);
1782
1783        Ok(())
1784    }
1785
1786    // ================================================================
1787    // Internal: do_start (release the parked child to execve)
1788    // ================================================================
1789
1790    fn do_start(&mut self) -> Result<(), crate::error::SandlockError> {
1791        use std::os::fd::AsRawFd;
1792        use crate::context::write_u32_fd;
1793        use crate::error::SandboxRuntimeError;
1794
1795        if !matches!(self.rt().state, RuntimeState::Created) {
1796            return Err(SandboxRuntimeError::Child("start() requires a created sandbox".into()).into());
1797        }
1798        let ready_w = self.rt_mut().ready_w.take()
1799            .ok_or_else(|| SandboxRuntimeError::Child("start() called without a prior create()".into()))?;
1800        write_u32_fd(ready_w.as_raw_fd(), 1)
1801            .map_err(|e| SandboxRuntimeError::Child(format!("write ready signal: {}", e)))?;
1802        drop(ready_w);
1803        self.rt_mut().state = RuntimeState::Running;
1804        Ok(())
1805    }
1806}
1807
1808// ================================================================
1809// Drop for Sandbox — kills and reaps child if still running
1810// ================================================================
1811
1812impl Drop for Sandbox {
1813    fn drop(&mut self) {
1814        if let Some(ref mut rt) = self.runtime {
1815            if let Some(pid) = rt.child_pid {
1816                if matches!(rt.state, RuntimeState::Created | RuntimeState::Running | RuntimeState::Paused) {
1817                    unsafe { libc::killpg(pid, libc::SIGKILL) };
1818                    let mut status: i32 = 0;
1819                    unsafe { libc::waitpid(pid, &mut status, 0) };
1820                }
1821            }
1822
1823            if let Some(h) = rt.notif_handle.take() { h.abort(); }
1824            if let Some(h) = rt.throttle_handle.take() { h.abort(); }
1825            if let Some(h) = rt.loadavg_handle.take() { h.abort(); }
1826
1827            let is_error = matches!(
1828                rt.state,
1829                RuntimeState::Stopped(ref s) if !matches!(s, crate::result::ExitStatus::Code(0))
1830            );
1831            let action = if is_error { &self.on_error } else { &self.on_exit };
1832            let action = action.clone();
1833
1834            if let Some(ref mut cow) = rt.seccomp_cow {
1835                match action {
1836                    BranchAction::Commit => { let _ = cow.commit(); }
1837                    BranchAction::Abort => { let _ = cow.abort(); }
1838                    BranchAction::Keep => {}
1839                }
1840            }
1841        }
1842    }
1843}
1844
1845// ================================================================
1846// CPU throttle
1847// ================================================================
1848
1849async fn sandbox_throttle_cpu(pid: i32, cpu_pct: u8) {
1850    use std::time::Duration;
1851    let period = Duration::from_millis(100);
1852    let run_time = period * cpu_pct as u32 / 100;
1853    let stop_time = period - run_time;
1854    loop {
1855        tokio::time::sleep(run_time).await;
1856        if unsafe { libc::killpg(pid, libc::SIGSTOP) } < 0 { break; }
1857        tokio::time::sleep(stop_time).await;
1858        if unsafe { libc::killpg(pid, libc::SIGCONT) } < 0 { break; }
1859    }
1860}
1861
1862// ================================================================
1863// Process name resolution
1864// ================================================================
1865
1866static NEXT_SANDBOX_NAME: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
1867
1868fn sandbox_resolve_name(name: Option<&str>) -> Result<String, crate::error::SandlockError> {
1869    match name {
1870        Some(n) => sandbox_validate_name(n.to_string()),
1871        None => Ok(format!(
1872            "sandbox-{}-{}",
1873            std::process::id(),
1874            NEXT_SANDBOX_NAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
1875        )),
1876    }
1877}
1878
1879fn sandbox_validate_name(name: String) -> Result<String, crate::error::SandlockError> {
1880    use crate::error::SandboxRuntimeError;
1881    if name.is_empty() {
1882        return Err(SandboxRuntimeError::Child("sandbox name must not be empty".into()).into());
1883    }
1884    if name.len() > 64 {
1885        return Err(SandboxRuntimeError::Child("sandbox name must be at most 64 bytes".into()).into());
1886    }
1887    if name.as_bytes().contains(&0) {
1888        return Err(SandboxRuntimeError::Child("sandbox name must not contain NUL bytes".into()).into());
1889    }
1890    Ok(name)
1891}
1892
1893// ================================================================
1894// I/O helpers (private)
1895// ================================================================
1896
1897fn sandbox_read_exact(fd: i32, buf: &mut [u8]) {
1898    let mut off = 0;
1899    while off < buf.len() {
1900        let r = unsafe { libc::read(fd, buf[off..].as_mut_ptr() as *mut _, buf.len() - off) };
1901        if r <= 0 { break; }
1902        off += r as usize;
1903    }
1904}
1905
1906fn sandbox_read_fd_to_end(fd: std::os::fd::OwnedFd) -> Vec<u8> {
1907    use std::io::Read;
1908    use std::os::fd::IntoRawFd;
1909    use std::os::unix::io::FromRawFd;
1910    let mut file = unsafe { std::fs::File::from_raw_fd(fd.into_raw_fd()) };
1911    let mut buf = Vec::new();
1912    let _ = file.read_to_end(&mut buf);
1913    buf
1914}
1915
1916fn sandbox_wait_status_to_exit(status: i32) -> crate::result::ExitStatus {
1917    use crate::result::ExitStatus;
1918    if libc::WIFEXITED(status) {
1919        ExitStatus::Code(libc::WEXITSTATUS(status))
1920    } else if libc::WIFSIGNALED(status) {
1921        let sig = libc::WTERMSIG(status);
1922        if sig == libc::SIGKILL {
1923            ExitStatus::Killed
1924        } else {
1925            ExitStatus::Signal(sig)
1926        }
1927    } else {
1928        ExitStatus::Killed
1929    }
1930}
1931
1932/// Await the top-level child's exit via its `pidfd` (readable on exit only),
1933/// then reap the status. Because it never calls `waitpid` until the child has
1934/// already exited, it does not consume the child's ptrace-stops the way a
1935/// `waitpid`-loop would — so it doesn't race the `policy_fn` fork-tracking
1936/// worker. Falls back to the blocking waiter on any pidfd/`AsyncFd` error.
1937async fn wait_child_exit_via_pidfd(
1938    pidfd: std::os::unix::io::OwnedFd,
1939    pid: libc::pid_t,
1940) -> crate::result::ExitStatus {
1941    use crate::result::ExitStatus;
1942
1943    let async_fd = match tokio::io::unix::AsyncFd::with_interest(
1944        pidfd,
1945        tokio::io::Interest::READABLE,
1946    ) {
1947        Ok(fd) => fd,
1948        Err(_) => return wait_child_exit_blocking(pid).await,
1949    };
1950
1951    loop {
1952        // pidfd becomes readable when the process exits; no data is read.
1953        let mut guard = match async_fd.readable().await {
1954            Ok(g) => g,
1955            Err(_) => return ExitStatus::Killed,
1956        };
1957        let mut status: i32 = 0;
1958        // The child has exited and is reapable now, so this never blocks.
1959        let r = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) };
1960        if r > 0 {
1961            return sandbox_wait_status_to_exit(status);
1962        }
1963        if r == 0 {
1964            // Spurious readiness (not yet reapable): clear and re-await.
1965            guard.clear_ready();
1966            continue;
1967        }
1968        // r < 0 (e.g. ECHILD): already reaped elsewhere. Status is unavailable.
1969        return ExitStatus::Killed;
1970    }
1971}
1972
1973/// Blocking `waitpid` fallback for kernels without `pidfd_open`. Used only when
1974/// no pidfd is available; on such kernels `policy_fn` fork-tracking is the only
1975/// thing that could race it, and the lack of pidfd is itself rare.
1976async fn wait_child_exit_blocking(pid: libc::pid_t) -> crate::result::ExitStatus {
1977    use crate::result::ExitStatus;
1978    tokio::task::spawn_blocking(move || -> ExitStatus {
1979        let mut status: i32 = 0;
1980        loop {
1981            let ret = unsafe { libc::waitpid(pid, &mut status, 0) };
1982            if ret < 0 {
1983                if std::io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
1984                    continue;
1985                }
1986                return ExitStatus::Killed;
1987            }
1988            break;
1989        }
1990        sandbox_wait_status_to_exit(status)
1991    })
1992    .await
1993    .unwrap_or(ExitStatus::Killed)
1994}
1995
1996fn sandbox_collect_handlers<I, S, H>(
1997    handlers: I,
1998    sandbox: &Sandbox,
1999) -> Result<Vec<(i64, Arc<dyn crate::seccomp::dispatch::Handler>)>, crate::error::SandlockError>
2000where
2001    I: IntoIterator<Item = (S, H)>,
2002    S: TryInto<crate::seccomp::syscall::Syscall, Error = crate::seccomp::syscall::SyscallError>,
2003    H: crate::seccomp::dispatch::Handler,
2004{
2005    use crate::seccomp::dispatch::{Handler, HandlerError};
2006
2007    let pending: Vec<(i64, Arc<dyn Handler>)> = handlers
2008        .into_iter()
2009        .map(|(syscall, handler)| {
2010            let nr = syscall.try_into().map_err(HandlerError::from)?.raw();
2011            let h: Arc<dyn Handler> = Arc::new(handler);
2012            Ok::<_, HandlerError>((nr, h))
2013        })
2014        .collect::<Result<_, _>>()?;
2015
2016    let nrs: Vec<i64> = pending.iter().map(|(nr, _)| *nr).collect();
2017    crate::seccomp::dispatch::validate_handler_syscalls_against_policy(&nrs, sandbox)
2018        .map_err(|syscall_nr| HandlerError::OnDenySyscall { syscall_nr })?;
2019
2020    Ok(pending)
2021}
2022
2023fn validate_syscall_names(names: &[String]) -> Result<(), SandboxError> {
2024    let unknown: Vec<&str> = names
2025        .iter()
2026        .map(String::as_str)
2027        .filter(|name| crate::seccomp::syscall::syscall_name_to_nr(name).is_none())
2028        .collect();
2029    if unknown.is_empty() {
2030        Ok(())
2031    } else {
2032        Err(SandboxError::Invalid(format!(
2033            "unknown syscall name(s): {}",
2034            unknown.join(", ")
2035        )))
2036    }
2037}
2038
2039/// Expand `--net-allow-bind` specs into a sorted, deduplicated port list.
2040/// Each spec is a comma-separated list of single ports (`8080`) or inclusive
2041/// `lo-hi` ranges (`8000-8010`). Mirrors the Python SDK's `parse_ports`.
2042fn parse_bind_ports(specs: &[String], label: &str) -> Result<Vec<u16>, SandboxError> {
2043    let mut ports: std::collections::BTreeSet<u16> = std::collections::BTreeSet::new();
2044    for spec in specs {
2045        for part in spec.split(',') {
2046            let part = part.trim();
2047            if part.is_empty() {
2048                return Err(SandboxError::Invalid(format!(
2049                    "{}: empty port in `{}`",
2050                    label, spec
2051                )));
2052            }
2053            match part.split_once('-') {
2054                Some((lo, hi)) => {
2055                    let lo: u16 = lo.trim().parse().map_err(|_| {
2056                        SandboxError::Invalid(format!("{}: invalid port range `{}`", label, part))
2057                    })?;
2058                    let hi: u16 = hi.trim().parse().map_err(|_| {
2059                        SandboxError::Invalid(format!("{}: invalid port range `{}`", label, part))
2060                    })?;
2061                    if lo > hi {
2062                        return Err(SandboxError::Invalid(format!(
2063                            "{}: reversed port range `{}` (lo > hi)",
2064                            label, part
2065                        )));
2066                    }
2067                    ports.extend(lo..=hi);
2068                }
2069                None => {
2070                    let p: u16 = part.parse().map_err(|_| {
2071                        SandboxError::Invalid(format!("{}: invalid port `{}`", label, part))
2072                    })?;
2073                    ports.insert(p);
2074                }
2075            }
2076        }
2077    }
2078    Ok(ports.into_iter().collect())
2079}
2080
2081/// Resolve a path as seen inside the sandbox to its host-side location, so its
2082/// existence can be checked before spawn. Honors `--fs-mount` (virtual:host)
2083/// mappings (which take precedence) and chroot. Used to validate
2084/// `--http-inject-ca` targets.
2085fn resolve_sandbox_path_to_host(
2086    child_path: &std::path::Path,
2087    chroot_root: Option<&std::path::Path>,
2088    mounts: &[(std::path::PathBuf, std::path::PathBuf)],
2089) -> std::path::PathBuf {
2090    for (virt, host) in mounts {
2091        if let Ok(rest) = child_path.strip_prefix(virt) {
2092            return host.join(rest);
2093        }
2094    }
2095    if let Some(root) = chroot_root {
2096        if let Ok(rest) = child_path.strip_prefix("/") {
2097            return root.join(rest);
2098        }
2099    }
2100    child_path.to_path_buf()
2101}
2102
2103#[cfg(test)]
2104mod tests;