Skip to content

Commit e7f2993

Browse files
dzerikcongwang-mk
authored andcommitted
feat(core): streaming-stdio popen → Process with per-stream StdioMode
Add Sandbox::popen, the first piece of the streaming-stdio process API (RFC #67). It generalizes the do_create capture/inherit boolean into a per-stream StdioMode { Inherit, Piped, Null } and returns a live Process whose Piped stream ends the caller owns (take_stdin/stdout/stderr), so a confined process can be driven over stdio while alive (MCP/LSP/REPL/ JSON-RPC) — which capture-mode run cannot express. - StdioMode is #[repr(u32)] with pinned discriminants so the FFI/Python bindings (later PRs) bind a stable C enum. - do_create is now a thin wrapper over do_create_stdio, so run/spawn/create and their interactive variants keep byte-identical capture/inherit behavior. - Child-side fd wiring is collision-safe. The post-fork path must survive a supervisor started with 0/1/2 closed (common for daemons): pipe2 can then allocate a pipe end onto a std fd, so a naive dup2 would alias the target, let a sibling stream clobber an end still needed, or act on a stale raw-fd snapshot. Each Piped source is first relocated to a fresh high fd (>= 3) via F_DUPFD_CLOEXEC, then dup2'd down onto its 0/1/2 target through one audited helper; the original O_CLOEXEC ends are mem::forget'd (they close at execve) so their Drop can't close a fd we reassigned. dup2 failure fails closed. - Null wires the stream to /dev/null and fails closed: if it cannot be opened the fd is closed, never left inherited from the supervisor. - Piped stdin adds a parent-held write end; wait() drops an untaken one so a child reading stdin still reaches EOF instead of hanging. A *taken* stdin the caller must close before wait() (documented; same contract as std). - popen does not poll for execve (a streaming reader blocks until bytes arrive, and the poll would spuriously time out on a fast-exiting child). - Process borrows the Sandbox (it does not own the process like std::process::Child); the child is killed and reaped by Sandbox::drop or Process::kill (whole process group). #[must_use] flags an abandoned handle. Folding the legacy spawn/wait into Process is deferred to a later phase. Tests (multi-threaded runtime — a blocking pipe read must not starve the seccomp supervisor, documented on popen): stdin+stdout roundtrip, stdout-only, stderr-only, all-three piped, Null stdout proven to be /dev/null, Null stdin EOF, untaken-stdin EOF, explicit kill + non-success status, whole-group kill-on-drop reaching a forked grandchild (polls for ESRCH to avoid racing zombie reaping), second-popen-on-used-Sandbox rejected, and a regression that capture-mode run still buffers stdout. Refs #67
1 parent 495902e commit e7f2993

5 files changed

Lines changed: 564 additions & 34 deletions

File tree

crates/sandlock-core/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ pub use error::SandlockError;
3535
pub use sys::structs::{SeccompData, SeccompNotif};
3636
pub use checkpoint::Checkpoint;
3737
pub use protection::{Protection, ProtectionState, ProtectionPolicy, ProtectionStatus};
38-
pub use sandbox::{Confinement, ConfinementBuilder, Sandbox, SandboxBuilder};
38+
pub use sandbox::{Confinement, ConfinementBuilder, Process, Sandbox, SandboxBuilder, StdioMode};
3939
pub use result::{RunResult, ExitStatus};
4040
pub use pipeline::{Stage, Pipeline, Gather};
4141
pub use dry_run::{Change, ChangeKind, DryRunResult};

0 commit comments

Comments
 (0)