feat(ffi): C ABI for streaming-stdio popen — sandlock_popen + handle_kill#120
Conversation
…kill Second piece of the streaming-stdio process API (RFC multikernel#67): expose the core Sandbox::popen over the C ABI so the Python/Go bindings can drive a confined process's stdio while it runs. The Python/Go wrapper follows as the next PR in the series; this lands the C surface it builds on. - sandlock_popen(policy, name, argv, argc, stdin_mode, stdout_mode, stderr_mode, out_stdin_fd, out_stdout_fd, out_stderr_fd): create+start a live handle with per-stream StdioMode (0=inherit, 1=piped, 2=null). Each piped stream's owned fd is returned through its out pointer; inherit/null yield -1. Uses the multi-threaded live runtime so the supervisor keeps pumping while the caller blocks on the fds. - sandlock_handle_kill: SIGKILL the handle's process group without freeing the handle, so the caller can still collect the exit status via sandlock_handle_wait. - Existing sandlock_handle_wait/_pid/_free already cover popen handles; their safety docs now name sandlock_popen as a second handle source. Fail-loud / no-leak (the binding boundary): an unknown StdioMode returns a null handle with every out fd left -1 and logs the rejected discriminant to stderr (not swallowed into an indistinguishable null); a piped fd whose out pointer is null is closed rather than leaked. The owned fds are converted to raw ints only on the infallible side of the result match, so an error return drops and closes them instead of leaking a dangling fd. Because the FFI always takes each piped stream, the core wait()-time stdin EOF safety net cannot fire here, so the deadlock warning (close a piped stdin and drain piped stdout/stderr before wait) is carried explicitly on the sandlock_popen and sandlock_handle_wait docs. Header regenerated with cbindgen (additive only, doc-only delta). Tests drive the symbols directly: stdout streaming + exit, stdin/stdout roundtrip through cat, stderr piped, Null yields no fd, unknown mode rejected with out fds reset, the kill lifecycle (kill->wait->free, kill->free without wait, idempotent kill), and a destructive fd-leak test (PIPED stream with a null out pointer must close its fd — verified to fail if the close branch regresses). Refs multikernel#67
|
Thanks for the contribution! This is a carefully written PR: the up-front reset of the out fds, the fail-loud rejection of unknown modes, and converting the owned fds to raw ints only on the infallible branch (so error returns drop-close them) all land cleanly, and the tests genuinely exercise the streaming paths. One design note on Duplicated create prologue. The cost is drift: the next change to policy handling, name validation, or argv parsing (say, Not blocking, just a maintainability cleanup worth considering while the surface is fresh. |
sandlock_popen re-inlined almost the entire prologue of sandlock_create_with_runtime (null check, optional_name, read_argv, runtime build, with_name), risking drift: a future change to name validation or argv parsing had to be applied in two places. Extract prepare(policy, name, argv, argc, build_rt) -> Option<(Sandbox, Runtime, Vec<String>)> and have both entry points call it. Each keeps its own tail: create* drive sb.create(), popen drives sb.popen() + fd handoff. Behavior-preserving: out-fd reset and the fail-loud unknown-StdioMode rejection stay ahead of the shared prologue in popen. No ABI/header change (prepare is private). All sandlock-ffi tests green.
|
Done in |
Second PR in the streaming-stdio process API series (RFC #67). The core landed
in #118; this exposes it over the C ABI so the language bindings can drive a
confined process's stdio while it runs. The Python/Go wrapper follows as the
next PR in the chain — this lands the C surface it builds on.
What
sandlock_popen(policy, name, argv, argc, stdin_mode, stdout_mode, stderr_mode, out_stdin_fd, out_stdout_fd, out_stderr_fd): create+start a livehandle with per-stream
StdioMode(0=inherit, 1=piped, 2=null). Each pipedstream's owned fd is returned through its out pointer; inherit/null yield -1.
Uses the multi-threaded live runtime so the supervisor keeps pumping while the
caller blocks on the fds.
sandlock_handle_kill: SIGKILL the handle's process group without freeing thehandle, so the caller can still collect the exit status via
sandlock_handle_wait.sandlock_handle_wait/_pid/_freealready cover popen handles;their safety docs now name
sandlock_popenas a second handle source.Fail-loud / no-leak at the binding boundary
StdioModereturns a null handle with every out fd left -1 andlogs the rejected discriminant to stderr — not swallowed into an
indistinguishable null (matching the
build_runtimeprecedent).result match, so an error return drops and closes them instead of leaking a
dangling fd.
EOF safety net cannot fire here, so the deadlock warning (close a piped stdin
and drain piped stdout/stderr before wait) is carried explicitly on the
sandlock_popenandsandlock_handle_waitdocs.Tests
Drive the symbols directly (no C compilation step): stdout streaming + exit,
stdin/stdout roundtrip through
cat, stderr piped,Nullyields no fd, unknownmode rejected with out fds reset, the kill lifecycle (kill→wait→free, kill→free
without wait, idempotent kill), and a destructive fd-leak test — a
Pipedstream with a null out pointer must close its fd; the test fails if that close
branch regresses.
Header regenerated with cbindgen (additive, doc-only delta); CI's header-drift
check passes.
Refs #67