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