diff --git a/crates/busbar-kernel/src/plane_host/egress.rs b/crates/busbar-kernel/src/plane_host/egress.rs
index 89237b65f2..3a3058611a 100644
--- a/crates/busbar-kernel/src/plane_host/egress.rs
+++ b/crates/busbar-kernel/src/plane_host/egress.rs
@@ -555,11 +555,12 @@ pub(crate) extern "C-unwind" fn egress_open(
// socket rather than a child's stdio. The governance path is identical (resolve-then-pin,
// SPKI, mTLS, breaker, meter); joining it is append-only (no ABI change). Refused honestly.
EgressKind::RawConn => StatusClass::Unsupported,
- // A governed child process, its stdin/stdout the duplex `PipeId` — spawned ONLY under the
- // HOST program allowlist. FFI-F3: no operator config wires a subprocess program allowlist
- // over the FFI seam today, so the host authorizes NO program (`&[]`) and every plane-driven
- // subprocess open is REFUSED at the allowlist. See [`super::pipe`].
- EgressKind::Subprocess => super::pipe::open_subprocess(state, d, &[], out),
+ // A governed child process. FFI-F3: no operator configuration wires a subprocess program
+ // allowlist over the FFI seam, so the host authorizes NO program and every plane-driven
+ // subprocess open is REFUSED, before anything on the descriptor is decoded or resolved.
+ // The spawning path it once reached ran in no served path and is struck (row 166, F11):
+ // see [`super::pipe`].
+ EgressKind::Subprocess => StatusClass::Refused,
}
})
}
diff --git a/crates/busbar-kernel/src/plane_host/egress_tests.rs b/crates/busbar-kernel/src/plane_host/egress_tests.rs
index 6ab78246d8..b395c3fc16 100644
--- a/crates/busbar-kernel/src/plane_host/egress_tests.rs
+++ b/crates/busbar-kernel/src/plane_host/egress_tests.rs
@@ -287,8 +287,8 @@ fn open_and_poll_fail_closed_on_null_and_bad_kind() {
StatusClass::Unsupported
);
}
- // Subprocess IS wired (the pipe tier), so a non-command target is REFUSED at the host command
- // allowlist (undecodable blob / not an absolute program), never spawned.
+ // Subprocess is REFUSED at the seam: the host authorizes no plane-driven program (FFI-F3), so
+ // nothing on the descriptor is decoded, resolved or spawned.
{
let url = b"http://example.test/".to_vec();
let mut d = http_desc(&url);
diff --git a/crates/busbar-kernel/src/plane_host/pipe.rs b/crates/busbar-kernel/src/plane_host/pipe.rs
index 8f0f5818ff..87ec921cb5 100644
--- a/crates/busbar-kernel/src/plane_host/pipe.rs
+++ b/crates/busbar-kernel/src/plane_host/pipe.rs
@@ -1,406 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Busbar Inc and contributors
-//! The PIPE tier of the egress family: a governed byte-DUPLEX channel a plane frames on top of.
+//! The PIPE tier of the egress family: the two vtable slots a byte-DUPLEX channel is driven
+//! through ([`pipe_read`] / [`pipe_write`], keyed by a [`PipeId`]).
//!
-//! The raw-connection and subprocess egress tiers ([`EgressKind::RawConn`] / [`EgressKind::Subprocess`])
-//! are the SAME shape — a byte duplex keyed by a [`PipeId`], distinguished only by a field on the open
-//! POD, not by separate slots (the CLUSTER-3 (c) decision). This module owns the SUBPROCESS wiring: a
-//! governed child process whose stdin/stdout are the duplex, opened through the SAME `egress_open`
-//! chokepoint as an HTTP hop.
+//! NO TIER OPENS A PIPE. The raw-connection tier answers `Unsupported` and the subprocess tier
+//! answers `Refused` at the seam ([`super::egress`]): no operator configuration wires a subprocess
+//! program allowlist over the FFI seam, so the host authorizes no plane-driven program (FFI-F3). The
+//! spawning machinery this module held — the command decode, the host-side environment resolution
+//! (a secret reference resolved at the spawn, on the request it served) and the child's lifecycle
+//! — ran in no served path and is struck (F11; row 166). A member program a plane needs is spawned
+//! by the connector over its declared program need, its environment held since config time
+//! (`busbar_core_connector::spawn_env`).
//!
-//! ## The framing seam: the host is byte-level, the plane frames
-//!
-//! `pipe_read`/`pipe_write` move RAW BYTES. Line/message framing (the stdio transport's
-//! `read_capped_line` logic) stays PLANE-side, layered on top — the host never sees a line. So a
-//! JSON-RPC-over-stdio plane writes a framed message with `pipe_write` and reads bytes back with
-//! `pipe_read`, doing its own newline framing; the host governs only the CHANNEL.
-//!
-//! ## The command allowlist is HOST-side
-//!
-//! The open POD carries the program + argv as neutral DATA only — NO policy. The host enforces the
-//! command allowlist here ([`command_admissible`]) BEFORE spawning: the program must be an ABSOLUTE
-//! path (the stdio transport's own boot rule, restated at the seam) and the egress scope must permit
-//! the subprocess tier. A plane cannot smuggle a relative name or a shell metacharacter through — the
-//! program goes straight to `Command::new` with argv as a vector, never a shell string.
+//! So both slots answer as they did for every pipe a plane could name: `Gone` (no such pipe), after
+//! the host-context and out-pointer checks, and FAIL CLOSED on a caught panic.
-use super::{recover, HostState};
+use super::recover;
use busbar_contract::abi::hot::host::HostCtx;
-use busbar_contract::abi::hot::pod::POD_VERSION;
-use busbar_contract::abi::hot::{
- EgressDesc, EgressHead, EgressId, EgressOpen, PipeId, StatusClass,
-};
-use busbar_contract::abi::read_sized_field;
-use std::collections::HashMap;
-use std::io::{Read, Write};
-use std::mem::MaybeUninit;
+use busbar_contract::abi::hot::{PipeId, StatusClass};
use std::panic::{catch_unwind, AssertUnwindSafe};
-use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
-use std::sync::{Arc, LazyLock, Mutex};
-
-/// One open governed subprocess pipe the host owns end to end. The plane holds only its [`PipeId`] and
-/// moves bytes through [`pipe_read`]/[`pipe_write`]; the host owns the child's lifecycle.
-struct PipeBackend {
- /// The child process; taken and killed on close. Behind a `Mutex` because the vtable fns hold only
- /// a shared `&PipeBackend` (via the registry) yet must kill/wait.
- child: Mutex>,
- /// The child's stdin — the WRITE half of the duplex. `None` once closed.
- stdin: Mutex >,
- /// The child's stdout — the READ half of the duplex. `None` once closed.
- stdout: Mutex >,
-}
-
-impl PipeBackend {
- /// Read up to `cap` bytes from the child's stdout into `buf`. Blocks for output; `Ok(0)` is a
- /// clean end of stream (the child closed its stdout).
- ///
- /// # Safety
- /// `buf`/`cap` must describe a live writable range for the call (ABI discipline).
- unsafe fn read(&self, buf: *mut u8, cap: usize) -> (StatusClass, usize) {
- if buf.is_null() || cap == 0 {
- return (StatusClass::Refused, 0);
- }
- let mut guard = self.stdout.lock().unwrap_or_else(|e| e.into_inner());
- let Some(stdout) = guard.as_mut() else {
- return (StatusClass::Gone, 0); // closed / reclaimed underneath us.
- };
- // SAFETY: caller's contract — `buf` is writable for `cap` bytes.
- let slice = unsafe { std::slice::from_raw_parts_mut(buf, cap) };
- match stdout.read(slice) {
- Ok(n) => (StatusClass::Ok, n), // n == 0 ⇒ EOF (clean end of stream).
- Err(_) => (StatusClass::Fault, 0),
- }
- }
-
- /// Write `len` bytes to the child's stdin.
- ///
- /// # Safety
- /// `buf`/`len` must describe a live readable range for the call (ABI discipline).
- unsafe fn write(&self, buf: *const u8, len: usize) -> StatusClass {
- if buf.is_null() && len != 0 {
- return StatusClass::Refused;
- }
- let mut guard = self.stdin.lock().unwrap_or_else(|e| e.into_inner());
- let Some(stdin) = guard.as_mut() else {
- return StatusClass::Gone;
- };
- // SAFETY: caller's contract — `(buf, len)` is a live readable range.
- let slice = if len == 0 {
- &[][..]
- } else {
- unsafe { std::slice::from_raw_parts(buf, len) }
- };
- match stdin.write_all(slice).and_then(|()| stdin.flush()) {
- Ok(()) => StatusClass::Ok,
- Err(_) => StatusClass::Fault,
- }
- }
-
- /// Tear the pipe down: drop both stdio halves (signalling EOF to the child), then kill and reap the
- /// child so no zombie or wedged process leaks. Idempotent — a second call finds the child taken.
- fn close(&self) {
- // Drop stdin first so a child reading its input observes EOF and can exit cleanly.
- let _ = self.stdin.lock().unwrap_or_else(|e| e.into_inner()).take();
- let _ = self.stdout.lock().unwrap_or_else(|e| e.into_inner()).take();
- if let Some(mut child) = self.child.lock().unwrap_or_else(|e| e.into_inner()).take() {
- let _ = child.kill();
- let _ = child.wait();
- }
- }
-}
-
-/// The PROCESS-WIDE registry of open pipes, keyed by a GLOBALLY unique id (the same discipline as the
-/// egress [`REGISTRY`](super::egress): two concurrent dispatches each mint arena-local ids from `1`, so
-/// the id the plane holds is minted from a process atomic and the backend lives here; the arena still
-/// owns RECLAIM via a registered [`Closer`](super::scope::DispatchScope::register_pipe)).
-static REGISTRY: LazyLock>>> =
- LazyLock::new(|| Mutex::new(HashMap::new()));
-
-/// The next globally-unique pipe id. `0` is the reserved [`PipeId::NONE`] sentinel, so ids start at `1`.
-static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
-
-fn registry() -> std::sync::MutexGuard<'static, HashMap>> {
- REGISTRY.lock().unwrap_or_else(|e| e.into_inner())
-}
-
-/// Remove a pipe from the registry and close it. Idempotent: the map remove elects exactly one closer
-/// (the arena `Closer`); a second call finds nothing.
-fn close_and_remove(id: u64) -> bool {
- match registry().remove(&id) {
- Some(pipe) => {
- pipe.close();
- true
- }
- None => false,
- }
-}
-
-/// Decode the packed `program + argv` blob the [`EgressDesc::target`](EgressDesc) carries for a
-/// subprocess open: a sequence of records, each `u32 len` (LE) then `len` bytes. The FIRST record is
-/// the program path; the rest are argv. Malformed input yields `None` (fail-closed — an undecodable
-/// command is refused, never guessed into a spawn).
-///
-/// # Safety
-/// `(ptr, len)` MUST describe a live, initialized byte range for the call.
-unsafe fn decode_command(ptr: *const u8, len: usize) -> Option<(String, Vec)> {
- if ptr.is_null() || len == 0 {
- return None;
- }
- // SAFETY: caller's contract — `(ptr, len)` is a live borrowed range.
- let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
- let mut tokens: Vec = Vec::new();
- let mut i = 0usize;
- while i < bytes.len() {
- let end = i.checked_add(4)?;
- let word = bytes.get(i..end)?;
- i = end;
- let n = u32::from_le_bytes(word.try_into().ok()?) as usize;
- let tok_end = i.checked_add(n)?;
- let tok = bytes.get(i..tok_end)?;
- i = tok_end;
- tokens.push(String::from_utf8_lossy(tok).into_owned());
- }
- let mut it = tokens.into_iter();
- let program = it.next()?;
- Some((program, it.collect()))
-}
-/// The result of resolving the child's environment: the fully-resolved `(name, value)` pairs the child
-/// will get, or a fail-closed refusal (a malformed record, or a secret reference the host could not
-/// resolve — the child is NOT spawned with a missing variable).
-enum EnvOutcome {
- /// The child's whole environment, resolved. Applied under `env_clear()` so it is the ONLY
- /// environment the child sees.
- Ready(Vec<(String, String)>),
- /// A record was malformed or a secret could not be resolved — refuse the spawn.
- Refuse,
-}
-
-/// The child-environment VALUE-KIND byte in a packed env record: a literal value, or a host-resolved
-/// secret reference (see [`EgressDesc::env_ptr`]).
-const ENV_KIND_PLAIN: u8 = 0;
-const ENV_KIND_SECRET: u8 = 1;
-
-/// Decode + RESOLVE the packed subprocess environment the [`EgressDesc::env_ptr`] tail carries. Each
-/// record is `u32 name_len` (LE), `name_len` name bytes, a `u8` value-kind, a `u32 value_len` (LE),
-/// then `value_len` value bytes. A literal value is taken verbatim; a secret reference is the opaque
-/// JSON of a host secret-ref the host turns into plaintext HERE (never off a plane POD), through the
-/// SAME built-in resolver the in-process stdio transport uses — so a rotated secret needs no restart
-/// and the plaintext never crosses the seam. Fail-closed: a malformed record or an unresolvable secret
-/// refuses the whole spawn rather than starting the child with a missing variable.
-///
-/// # Safety
-/// `(ptr, len)`, when non-null, MUST describe a live, initialized byte range for the call.
-unsafe fn resolve_child_env(ptr: *const u8, len: usize) -> EnvOutcome {
- if ptr.is_null() || len == 0 {
- return EnvOutcome::Ready(Vec::new()); // no records ⇒ an empty (cleared) child environment.
- }
- // SAFETY: caller's contract — `(ptr, len)` is a live borrowed range.
- let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
- let mut out: Vec<(String, String)> = Vec::new();
- let mut i = 0usize;
- while i < bytes.len() {
- let Some(name) = read_len_prefixed(bytes, &mut i) else {
- return EnvOutcome::Refuse;
- };
- let Some(&kind) = bytes.get(i) else {
- return EnvOutcome::Refuse;
- };
- i += 1;
- let Some(value_bytes) = read_len_prefixed_bytes(bytes, &mut i) else {
- return EnvOutcome::Refuse;
- };
- let value = match kind {
- ENV_KIND_PLAIN => String::from_utf8_lossy(value_bytes).into_owned(),
- ENV_KIND_SECRET => {
- // The value is the OPAQUE JSON of a host secret-ref; deserialize and resolve it HERE
- // through the built-in resolver — the same `resolve_linked_string` the in-process
- // stdio spawn reads a `ChildEnvValue::Secret` with. A failure refuses the spawn.
- let Ok(secret_ref) =
- serde_json::from_slice::(value_bytes)
- else {
- return EnvOutcome::Refuse;
- };
- match crate::config::secret::resolve_linked_string(&secret_ref) {
- Ok(plaintext) => plaintext,
- Err(_) => return EnvOutcome::Refuse, // unresolvable secret ⇒ fail-closed.
- }
- }
- _ => return EnvOutcome::Refuse, // an unknown value-kind is never guessed.
- };
- out.push((String::from_utf8_lossy(name).into_owned(), value));
- }
- EnvOutcome::Ready(out)
-}
-
-/// Read a `u32 len` (LE) then `len` bytes as a borrowed slice, advancing `*i`; `None` on truncation.
-fn read_len_prefixed_bytes<'a>(bytes: &'a [u8], i: &mut usize) -> Option<&'a [u8]> {
- let end = i.checked_add(4)?;
- let word = bytes.get(*i..end)?;
- *i = end;
- let n = u32::from_le_bytes(word.try_into().ok()?) as usize;
- let tok_end = i.checked_add(n)?;
- let slice = bytes.get(*i..tok_end)?;
- *i = tok_end;
- Some(slice)
-}
-
-/// As [`read_len_prefixed_bytes`], but the name arm — kept a distinct helper for the read site's
-/// readability (a record reads its name, its kind, then its value).
-fn read_len_prefixed<'a>(bytes: &'a [u8], i: &mut usize) -> Option<&'a [u8]> {
- read_len_prefixed_bytes(bytes, i)
-}
-
-/// Read the subprocess working directory off the [`EgressDesc`] tail: `Some(dir)` when a non-empty cwd
-/// was written, `None` (⇒ inherit the host's cwd) when the field is absent or empty. Read only behind
-/// the sized-struct guard so a sender that predates the tail leaves the host's cwd untouched.
-fn read_child_cwd(d: &EgressDesc) -> Option {
- let ptr = read_sized_field!(d, d.size, EgressDesc, cwd_ptr)?;
- let len = read_sized_field!(d, d.size, EgressDesc, cwd_len)?;
- if ptr.is_null() || len == 0 {
- return None;
- }
- // SAFETY: a non-null `(cwd_ptr, cwd_len)` is a live borrowed range for the call (ABI discipline).
- let bytes = unsafe { std::slice::from_raw_parts(ptr, len) };
- Some(String::from_utf8_lossy(bytes).into_owned())
-}
-
-/// The HOST command allowlist: a program is admissible only when it is an ABSOLUTE path AND it is
-/// explicitly named on the HOST-supplied program allowlist. This is policy the HOST owns end to end —
-/// the plane's [`EgressDesc`] carries only DATA (never a capability), so the plane's `allowlist_scope`
-/// bit is IGNORED here (FFI-F2/F3): a plane cannot self-grant the subprocess tier.
-///
-/// The FFI vtable slot passes an EMPTY allowlist (`&[]`) because no operator config wires a subprocess
-/// program allowlist over the FFI seam today — so every plane-driven subprocess open is REFUSED
-/// (fail-closed by denial; the capability is disabled at the seam, not merely narrowed). A caller that
-/// legitimately owns a host-authored allowlist (the in-core stdio-transport posture, exercised in
-/// tests) passes it explicitly and only its named absolute programs are admissible.
-fn command_admissible(program: &str, program_allowlist: &[String]) -> bool {
- std::path::Path::new(program).is_absolute() && program_allowlist.iter().any(|p| p == program)
-}
-
-/// Open a governed SUBPROCESS pipe. Decodes + allowlist-checks the command, spawns the child with piped
-/// stdio, registers the backend + the arena closer, and writes an [`EgressOpen`] whose `pipe` is the
-/// duplex [`PipeId`]. Called from [`super::egress::egress_open`] for [`EgressKind::Subprocess`]; the
-/// caller owns the `catch_unwind` and the null checks.
-pub(super) fn open_subprocess(
- state: &HostState,
- d: &EgressDesc,
- program_allowlist: &[String],
- out: *mut MaybeUninit,
-) -> StatusClass {
- // SAFETY: `(target_ptr, target_len)` is a live borrowed range for the call (ABI discipline).
- let Some((program, argv)) = (unsafe { decode_command(d.target_ptr, d.target_len) }) else {
- return StatusClass::Refused;
- };
- if !command_admissible(&program, program_allowlist) {
- // Not absolute, or not on the HOST program allowlist. The FFI seam passes an empty allowlist,
- // so this REFUSES every plane-driven subprocess spawn (FFI-F3).
- return StatusClass::Refused;
- }
- // THE CHILD'S ENVIRONMENT, resolved host-side and applied under `env_clear()` so the child gets
- // ONLY these variables — NEVER the host's own environment, which holds provider API keys, store
- // credentials and the operator credential. Inheriting them (as this path once did) would make every governed
- // subprocess a silent credential-exfiltration primitive; clearing first is the fix the in-process
- // stdio transport already applies, restated at the seam. A malformed record or an unresolvable
- // secret refuses the spawn rather than starting the child with a missing variable.
- // SAFETY: `(env_ptr, env_len)`, when present, is a live borrowed range for the call (ABI).
- let env = match unsafe {
- resolve_child_env(
- read_sized_field!(d, d.size, EgressDesc, env_ptr).unwrap_or(std::ptr::null()),
- read_sized_field!(d, d.size, EgressDesc, env_len).unwrap_or(0),
- )
- } {
- EnvOutcome::Ready(env) => env,
- EnvOutcome::Refuse => return StatusClass::Refused,
- };
- // The working directory (empty ⇒ inherit the host's own) and the stderr disposition (default
- // discard; inherit sends the child's diagnostics to the host's stderr where an operator reads
- // them), both read only behind the sized-struct guard so a sender that predates the tail keeps the
- // pre-enrichment shape (an empty environment, the host's cwd, a discarded stderr).
- let cwd = read_child_cwd(d);
- let stderr = if read_sized_field!(d, d.size, EgressDesc, stderr_inherit) == Some(1) {
- Stdio::inherit()
- } else {
- Stdio::null()
- };
- // NO SHELL: the program goes to `Command::new` and argv as a vector — never a shell string, so a
- // metacharacter in an arg has no meaning.
- let mut builder = Command::new(&program);
- builder
- .args(&argv)
- // THE WHOLE environment, not additions to the host's — `env_clear()` first, then only the
- // resolved records. See the note above.
- .env_clear()
- .envs(env)
- .stdin(Stdio::piped())
- .stdout(Stdio::piped())
- .stderr(stderr);
- if let Some(dir) = &cwd {
- builder.current_dir(dir);
- }
- let child = builder.spawn();
- let mut child = match child {
- Ok(child) => child,
- Err(_) => return StatusClass::Fault, // spawn failed (e.g. ENOENT) — a host-side fault.
- };
- let (Some(stdin), Some(stdout)) = (child.stdin.take(), child.stdout.take()) else {
- let _ = child.kill();
- let _ = child.wait();
- return StatusClass::Fault;
- };
- let backend = Arc::new(PipeBackend {
- child: Mutex::new(Some(child)),
- stdin: Mutex::new(Some(stdin)),
- stdout: Mutex::new(Some(stdout)),
- });
-
- let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
- registry().insert(id, Arc::clone(&backend));
- // The arena reclaims on dispatch-drop: kill + reap the child, so a cancelled dispatch cannot leak a
- // process. (We hand the plane the global id and ignore the arena's own id, like `register_egress`.)
- let _ = state.scope.register_pipe(Box::new(move || {
- close_and_remove(id);
- }));
-
- let open = EgressOpen {
- size: std::mem::size_of::() as u32,
- version: POD_VERSION,
- _reserved: 0,
- // A subprocess is a duplex byte channel, not a one-shot egress: the plane drives the PipeId.
- id: EgressId::NONE,
- pipe: PipeId(id),
- head: EgressHead {
- size: std::mem::size_of::() as u32,
- version: POD_VERSION,
- status_code: 0, // a raw byte channel has no HTTP status.
- observed_spki_ptr: std::ptr::null(),
- observed_spki_len: 0,
- resp_headers_ptr: std::ptr::null(), // a raw byte channel surfaces no response headers.
- resp_headers_len: 0,
- client_identity_offered: 0, // a raw byte channel offers no client identity.
- _reserved4: [0; 7],
- },
- };
- // SAFETY: `out` is non-null (checked by the caller) and writable for one `EgressOpen`; written on Ok.
- unsafe {
- (*out).write(open);
- }
- StatusClass::Ok
-}
-
-// ─────────────────────────────────────────────────────────────────────────────────────────────
-// The two vtable slots. Each recovers the HostState FIRST, runs inside a MANDATORY catch_unwind, and
-// FAILS CLOSED (`Fault` / `Gone` / `Refused`) on a caught panic — never a permissive value.
-// ─────────────────────────────────────────────────────────────────────────────────────────────
-
-/// Read raw bytes from a governed pipe into the caller's buffer. Blocks for output; `Ok` with
-/// `out_written = 0` is a clean end of stream.
+/// Read raw bytes from a governed pipe into the caller's buffer: no pipe is ever open, so every
+/// named pipe is `Gone`.
pub(crate) extern "C-unwind" fn pipe_read(
host: HostCtx,
- pipe: PipeId,
- buf: *mut u8,
- buf_cap: usize,
+ _pipe: PipeId,
+ _buf: *mut u8,
+ _buf_cap: usize,
out_written: *mut usize,
) -> StatusClass {
catch_unwind(AssertUnwindSafe(|| {
@@ -411,39 +38,24 @@ pub(crate) extern "C-unwind" fn pipe_read(
if out_written.is_null() {
return StatusClass::Refused;
}
- let Some(backend) = registry().get(&pipe.0).map(Arc::clone) else {
- return StatusClass::Gone; // unknown / already closed / reclaimed.
- };
- // SAFETY: caller's `buf`/`buf_cap` describe a live writable range (ABI discipline).
- let (class, written) = unsafe { backend.read(buf, buf_cap) };
- if class == StatusClass::Ok {
- // SAFETY: `out_written` is non-null (checked) and writable for one `usize`.
- unsafe {
- *out_written = written;
- }
- }
- class
+ StatusClass::Gone
}))
.unwrap_or(StatusClass::Fault)
}
-/// Write raw bytes to a governed pipe (the child's stdin).
+/// Write raw bytes to a governed pipe: no pipe is ever open, so every named pipe is `Gone`.
pub(crate) extern "C-unwind" fn pipe_write(
host: HostCtx,
- pipe: PipeId,
- buf: *const u8,
- len: usize,
+ _pipe: PipeId,
+ _buf: *const u8,
+ _len: usize,
) -> StatusClass {
catch_unwind(AssertUnwindSafe(|| {
// SAFETY: recovery invariant (see `super::recover`).
let Some(_state) = (unsafe { recover(host) }) else {
return StatusClass::Refused;
};
- let Some(backend) = registry().get(&pipe.0).map(Arc::clone) else {
- return StatusClass::Gone;
- };
- // SAFETY: caller's `buf`/`len` describe a live readable range (ABI discipline).
- unsafe { backend.write(buf, len) }
+ StatusClass::Gone
}))
.unwrap_or(StatusClass::Fault)
}
diff --git a/crates/busbar-kernel/src/plane_host/pipe_tests.rs b/crates/busbar-kernel/src/plane_host/pipe_tests.rs
index 868d5b3dea..a76c3928fd 100644
--- a/crates/busbar-kernel/src/plane_host/pipe_tests.rs
+++ b/crates/busbar-kernel/src/plane_host/pipe_tests.rs
@@ -1,23 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (C) 2026 Busbar Inc and contributors
-//! PIPE-tier tests: a governed subprocess byte-duplex opened through the SAME `egress_open` seam, its
-//! stdin/stdout driven with `pipe_write`/`pipe_read`, its lifecycle reclaimed by the dispatch arena.
-//! `/bin/cat` is the echo-duplex: what the plane writes to stdin comes back on stdout, byte for byte,
-//! proving the host moves RAW BYTES (the plane frames on top).
-//!
-//! FFI-F3: the FFI vtable `egress_open` slot grants NO subprocess capability (it passes an EMPTY host
-//! program allowlist, so every plane-driven spawn is refused). The subprocess MECHANICS below are
-//! therefore driven through `open_subprocess` with a HOST-AUTHORED program allowlist — the in-core
-//! posture a first-party host holds — while `the_ffi_slot_refuses_a_plane_driven_subprocess` proves the
-//! untrusted seam refuses the spawn outright.
+//! PIPE-tier tests. FFI-F3: the FFI vtable `egress_open` slot grants NO subprocess capability, so
+//! every plane-driven spawn is refused — an absolute, runnable program with a secret environment
+//! record included, before anything is decoded or resolved — and no pipe is ever open, so both pipe
+//! slots answer `Gone` for any pipe a plane names (row 166: the unserved spawning path is struck).
-use super::*;
-use crate::plane_host::{recover, with_dispatch_scope, HostState};
-use busbar_contract::abi::hot::host::{HostCtx, PlaneHostVtable};
+use crate::plane_host::with_dispatch_scope;
use busbar_contract::abi::hot::pod::POD_VERSION;
use busbar_contract::abi::hot::{EgressDesc, EgressKind, EgressOpen, PipeId, StatusClass};
-use std::mem::MaybeUninit;
/// Pack a `program + argv` command into the length-prefixed wire form (`u32 len | bytes`, LE) that
/// [`EgressDesc::target`] carries for a subprocess open. The first token is the program.
@@ -31,7 +22,7 @@ fn pack_command(tokens: &[&str]) -> Vec {
}
/// A subprocess `EgressDesc` borrowing the packed `command` blob. `allowlist_scope` is DATA the host
-/// ignores for admission (FFI-F2/F3): the program allowlist, not a plane-asserted scope bit, decides.
+/// ignores (FFI-F2/F3): a plane-asserted scope bit grants nothing.
fn subprocess_desc(command: &[u8]) -> EgressDesc {
EgressDesc {
size: std::mem::size_of::() as u32,
@@ -68,243 +59,25 @@ fn subprocess_desc(command: &[u8]) -> EgressDesc {
}
}
-/// Open a governed subprocess over the HOST-AUTHORED program allowlist — the in-core posture a
-/// first-party host holds (the FFI vtable slot, by contrast, passes an EMPTY allowlist and refuses).
-fn host_open_subprocess(
- host: HostCtx,
- desc: &EgressDesc,
- program_allowlist: &[String],
- out: *mut MaybeUninit,
-) -> StatusClass {
- // SAFETY: live HostState minted by with_dispatch_scope.
- let state: &HostState =
- unsafe { recover(host) }.expect("host generation still live inside with_dispatch_scope");
- open_subprocess(state, desc, program_allowlist, out)
-}
-
-/// A packed child-environment record: `u32 name_len | name | u8 kind | u32 value_len | value`.
-fn env_record(name: &str, kind: u8, value: &[u8]) -> Vec {
- let mut out = Vec::new();
- out.extend_from_slice(&(name.len() as u32).to_le_bytes());
- out.extend_from_slice(name.as_bytes());
- out.push(kind);
- out.extend_from_slice(&(value.len() as u32).to_le_bytes());
- out.extend_from_slice(value);
- out
-}
-
-/// Read a subprocess's whole stdout to EOF (the child is expected to exit after printing).
-fn read_to_eof(vt: &PlaneHostVtable, host: HostCtx, pipe: PipeId) -> Vec {
- let mut got = Vec::new();
- let mut buf = [0u8; 256];
- for _ in 0..256 {
- let mut written: usize = 0;
- let class = (vt.pipe_read.unwrap())(host, pipe, buf.as_mut_ptr(), buf.len(), &mut written);
- assert_eq!(class, StatusClass::Ok, "pipe_read stays Ok until EOF");
- if written == 0 {
- break; // EOF
- }
- got.extend_from_slice(&buf[..written]);
- }
- got
-}
-
-/// Read up to `want` bytes from the pipe, accumulating across blocking reads until it has them or the
-/// stream ends. Asserts every read stays `Ok`.
-fn read_at_least(vt: &PlaneHostVtable, host: HostCtx, pipe: PipeId, want: usize) -> Vec {
- let mut got = Vec::new();
- let mut buf = [0u8; 64];
- for _ in 0..64 {
- if got.len() >= want {
- break;
- }
- let mut written: usize = 0;
- let class = (vt.pipe_read.unwrap())(host, pipe, buf.as_mut_ptr(), buf.len(), &mut written);
- assert_eq!(class, StatusClass::Ok, "pipe_read stays Ok until EOF");
- if written == 0 {
- break; // EOF
- }
- got.extend_from_slice(&buf[..written]);
- }
- got
-}
-
-#[test]
-fn subprocess_pipe_echoes_bytes_through_cat() {
- // `/bin/cat` exists on macOS and Linux alike; a runner without it is broken, so fail.
- assert!(
- std::path::Path::new("/bin/cat").exists(),
- "/bin/cat is required by this test"
- );
- let command = pack_command(&["/bin/cat"]);
- let desc = subprocess_desc(&command);
- let allowlist = vec!["/bin/cat".to_string()];
-
- let app = crate::test_support::TestApp::new().build();
- with_dispatch_scope(&app, |host, vt| {
- // SAFETY: live HostState minted by with_dispatch_scope.
- let state: &HostState = unsafe { recover(host) }
- .expect("host generation still live inside with_dispatch_scope");
- let scope = state.scope;
-
- let mut out = std::mem::MaybeUninit::::uninit();
- let class = host_open_subprocess(host, &desc, &allowlist, &mut out);
- assert_eq!(class, StatusClass::Ok, "the allowlisted subprocess opens");
- // SAFETY: Ok ⇒ initialized.
- let open = unsafe { out.assume_init() };
- assert!(!open.pipe.is_none(), "a real PipeId is handed back");
- assert!(
- open.id.is_none(),
- "a subprocess is a duplex pipe, not a one-shot egress"
- );
- assert_eq!(
- scope.registered(),
- 1,
- "open registers exactly one arena closer"
- );
-
- // WRITE → the child's stdin; READ ← its stdout. cat echoes byte for byte.
- let payload = b"ping-through-the-duplex\n";
- assert_eq!(
- (vt.pipe_write.unwrap())(host, open.pipe, payload.as_ptr(), payload.len()),
- StatusClass::Ok
- );
- let echoed = read_at_least(vt, host, open.pipe, payload.len());
- assert_eq!(
- &echoed[..payload.len()],
- payload,
- "the duplex echoed the bytes verbatim"
- );
-
- // A write to an unknown pipe is Gone.
- assert_eq!(
- (vt.pipe_write.unwrap())(host, PipeId(999_999), payload.as_ptr(), payload.len()),
- StatusClass::Gone
- );
- });
-}
-
-#[test]
-fn subprocess_env_is_cleared_and_selective_never_leaking_the_hosts() {
- // `/usr/bin/env` prints the child's whole environment, one `NAME=value` per line. Under the seam's
- // `env_clear()` + selective `envs`, the child must see ONLY the records the plane named — never the
- // host's own environment (which holds provider keys), so the marker is present and the host's
- // always-present `PATH` is not. This is the CONFIRMED regression this carrier closes.
- assert!(
- std::path::Path::new("/usr/bin/env").exists(),
- "/usr/bin/env is required by this test"
- );
- let command = pack_command(&["/usr/bin/env"]);
- let env = env_record("BUSBAR_ENV_MARKER", 0, b"present");
- let mut desc = subprocess_desc(&command);
- desc.env_ptr = env.as_ptr();
- desc.env_len = env.len();
- let allowlist = vec!["/usr/bin/env".to_string()];
-
- let app = crate::test_support::TestApp::new().build();
- with_dispatch_scope(&app, |host, vt| {
- let mut out = std::mem::MaybeUninit::::uninit();
- assert_eq!(
- host_open_subprocess(host, &desc, &allowlist, &mut out),
- StatusClass::Ok
- );
- // SAFETY: Ok ⇒ initialized.
- let open = unsafe { out.assume_init() };
- let printed = String::from_utf8_lossy(&read_to_eof(vt, host, open.pipe)).into_owned();
- assert!(
- printed.contains("BUSBAR_ENV_MARKER=present"),
- "the child sees the variable the plane named; got: {printed:?}"
- );
- assert!(
- !printed.contains("PATH="),
- "env_clear() ran first, so the host's own environment (its PATH, its secrets) never \
- reaches the child; got: {printed:?}"
- );
- });
-}
-
-#[test]
-fn subprocess_env_resolves_a_secret_reference_host_side() {
- // A `Secret` env record carries the OPAQUE JSON of a host secret-ref; the host resolves it to
- // plaintext at spawn through the built-in resolver, exactly as the in-process stdio spawn does.
- // The `env` module reads a host environment variable — `HOME` is present in any test environment —
- // so the child ends up with the resolved value the plane never held.
- assert!(
- std::path::Path::new("/usr/bin/env").exists(),
- "/usr/bin/env is required by this test"
- );
- let home = std::env::var("HOME").expect("HOME must be set for this test");
- assert!(!home.is_empty(), "HOME must be non-empty for this test");
- let command = pack_command(&["/usr/bin/env"]);
- // The sugar form the config layer accepts: `{ env: HOME }` ⇒ the `env` secret module, key `HOME`.
- let secret_json = br#"{"env":"HOME"}"#;
- let env = env_record("BUSBAR_INJECTED", 1, secret_json);
- let mut desc = subprocess_desc(&command);
- desc.env_ptr = env.as_ptr();
- desc.env_len = env.len();
- let allowlist = vec!["/usr/bin/env".to_string()];
-
- let app = crate::test_support::TestApp::new().build();
- with_dispatch_scope(&app, |host, vt| {
- let mut out = std::mem::MaybeUninit::::uninit();
- assert_eq!(
- host_open_subprocess(host, &desc, &allowlist, &mut out),
- StatusClass::Ok
- );
- // SAFETY: Ok ⇒ initialized.
- let open = unsafe { out.assume_init() };
- let printed = String::from_utf8_lossy(&read_to_eof(vt, host, open.pipe)).into_owned();
- assert!(
- printed.contains(&format!("BUSBAR_INJECTED={home}")),
- "the host resolved the secret reference to plaintext and handed it to the child; got: \
- {printed:?}"
- );
- });
-}
-
-#[test]
-fn open_refuses_a_relative_or_unlisted_command() {
- let app = crate::test_support::TestApp::new().build();
- with_dispatch_scope(&app, |host, _vt| {
- let mut out = std::mem::MaybeUninit::::uninit();
- let allowlist = vec!["/bin/cat".to_string()];
-
- // Absolute path but NOT on the host program allowlist → Refused (the allowlist gates the tier).
- let cmd = pack_command(&["/bin/ls"]);
- let unlisted = subprocess_desc(&cmd);
- assert_eq!(
- host_open_subprocess(host, &unlisted, &allowlist, &mut out),
- StatusClass::Refused,
- "a program the host did not allowlist is refused"
- );
-
- // On the allowlist, but a RELATIVE program name is refused (the host allowlist demands absolute).
- let relative = pack_command(&["cat"]);
- let rel = subprocess_desc(&relative);
- assert_eq!(
- host_open_subprocess(host, &rel, &["cat".to_string()], &mut out),
- StatusClass::Refused,
- "a relative program name is never admissible"
- );
-
- // An empty/undecodable command blob is refused, not spawned.
- let empty = subprocess_desc(&[]);
- assert_eq!(
- host_open_subprocess(host, &empty, &allowlist, &mut out),
- StatusClass::Refused
- );
- });
-}
-
/// FFI-F3 (subprocess egress no allowlist): the FFI vtable `egress_open` slot passes an EMPTY host
/// program allowlist, so a plane-driven subprocess spawn is REFUSED outright — even an absolute,
/// otherwise-runnable program. The capability is disabled at the untrusted seam, not merely narrowed.
#[test]
fn the_ffi_slot_refuses_a_plane_driven_subprocess() {
let command = pack_command(&["/bin/cat"]);
+ // A child-environment record naming a SECRET reference (kind 1): refused before it is read.
+ let secret = br#"{"module":"env","settings":{"name":"BUSBAR_PIPE_NEVER_RESOLVED"}}"#;
+ let mut env = Vec::new();
+ env.extend_from_slice(&5u32.to_le_bytes());
+ env.extend_from_slice(b"TOKEN");
+ env.push(1);
+ env.extend_from_slice(&(secret.len() as u32).to_le_bytes());
+ env.extend_from_slice(secret);
// The plane even asserts a (meaningless) privilege bit — it carries no authority over the seam.
let mut desc = subprocess_desc(&command);
desc.allowlist_scope = u32::MAX;
+ desc.env_ptr = env.as_ptr();
+ desc.env_len = env.len();
let app = crate::test_support::TestApp::new().build();
with_dispatch_scope(&app, |host, vt| {
@@ -314,32 +87,29 @@ fn the_ffi_slot_refuses_a_plane_driven_subprocess() {
StatusClass::Refused,
"the FFI seam admits no plane-driven subprocess — no host program allowlist grants it"
);
- });
-}
-
-#[test]
-fn arena_drop_reclaims_and_kills_an_unclosed_subprocess() {
- assert!(
- std::path::Path::new("/bin/cat").exists(),
- "/bin/cat is required by this test"
- );
- let command = pack_command(&["/bin/cat"]);
- let desc = subprocess_desc(&command);
- let allowlist = vec!["/bin/cat".to_string()];
-
- let app = crate::test_support::TestApp::new().build();
- let leaked = with_dispatch_scope(&app, |host, _vt| {
- let mut out = std::mem::MaybeUninit::::uninit();
+ // No pipe is ever open: both slots answer `Gone` for any pipe a plane names.
+ let mut buf = [0u8; 8];
+ let mut written = 0usize;
+ for pipe in [PipeId(0), PipeId(1), PipeId(u64::MAX)] {
+ assert_eq!(
+ (vt.pipe_read.unwrap())(host, pipe, buf.as_mut_ptr(), buf.len(), &mut written),
+ StatusClass::Gone
+ );
+ assert_eq!(
+ (vt.pipe_write.unwrap())(host, pipe, buf.as_ptr(), buf.len()),
+ StatusClass::Gone
+ );
+ }
assert_eq!(
- host_open_subprocess(host, &desc, &allowlist, &mut out),
- StatusClass::Ok
+ (vt.pipe_read.unwrap())(
+ host,
+ PipeId(1),
+ buf.as_mut_ptr(),
+ buf.len(),
+ std::ptr::null_mut()
+ ),
+ StatusClass::Refused,
+ "a null out-pointer is refused before the pipe is looked at"
);
- // SAFETY: Ok ⇒ initialized. Deliberately do NOT close — the dispatch ends with it open.
- unsafe { out.assume_init() }.pipe
});
- // The dispatch scope dropped: its arena Closer killed the child and removed the backend.
- assert!(
- !REGISTRY.lock().unwrap().contains_key(&leaked.0),
- "arena drop reclaims (kills + reaps) a subprocess the plane never closed"
- );
}
diff --git a/qa/kind-isolation.toml b/qa/kind-isolation.toml
index 1e97362c49..2f66e797a0 100644
--- a/qa/kind-isolation.toml
+++ b/qa/kind-isolation.toml
@@ -1806,8 +1806,8 @@ cite = "measured on predev f830faa691 (ARCHITECT 2026-10-07, Law 9); the drain
[[law0]]
crate = "busbar-kernel"
axis = "transport"
-count = "266"
-cite = "measured on predev f830faa691 (ARCHITECT 2026-10-07, Law 9); the drain is THE DESIGN §1 / DoD item 2: core names no instance; LOWERED 288 -> 286, predev 9c0a394771's measure: the drain landed without giving its row back (Law 9: the ceiling ratchets down); LOWERED 286 -> 285 by GetBusbar/busbar#570: the kernel's auth proof no longer cites the root's stdio-served proof; LOWERED 285 -> 284 by row 157: the hit drained with the same deletion; REBALANCED 284 -> 267 (-17), the verbatim listener serve loop moved out of busbar-kernel into the root (#710), paired with busbar x transport 208 -> 225 (+17) so the law0 total does not rise (ARCHITECT RULING 2026-10-10, https://github.com/GetBusbar/busbar/pull/710#issuecomment-6096420101; BUSBAR-1.6.0.md l.475-483 INBOUND-LISTEN: the root's stream hand-up); LOWERED 267 -> 266 by GetBusbar/busbar#532 (Q1): the kernel's cold auth path is deleted (auth/token.rs's `\"http\"` scheme check), a different hit from row 157's"
+count = "256"
+cite = "measured on predev f830faa691 (ARCHITECT 2026-10-07, Law 9); the drain is THE DESIGN §1 / DoD item 2: core names no instance; LOWERED 288 -> 286, predev 9c0a394771's measure: the drain landed without giving its row back (Law 9: the ceiling ratchets down); LOWERED 286 -> 285 by GetBusbar/busbar#570: the kernel's auth proof no longer cites the root's stdio-served proof; LOWERED 285 -> 284 by row 157: the hit drained with the same deletion; REBALANCED 284 -> 267 (-17), the verbatim listener serve loop moved out of busbar-kernel into the root (#710), paired with busbar x transport 208 -> 225 (+17) so the law0 total does not rise (ARCHITECT RULING 2026-10-10, https://github.com/GetBusbar/busbar/pull/710#issuecomment-6096420101; BUSBAR-1.6.0.md l.475-483 INBOUND-LISTEN: the root's stream hand-up); LOWERED 267 -> 266 by GetBusbar/busbar#532 (Q1): the kernel's cold auth path is deleted (auth/token.rs's `\"http\"` scheme check), a different hit from row 157's; LOWERED 266 -> 256 by GetBusbar/busbar#866 (row 166): the plane FFI subprocess spawn path (plane_host/egress.rs, plane_host/pipe.rs and their suites) ran in no served path and is struck, taking ten transport hits with it (the gate's measure on #866, law0-base: 10 drained; Law 9: the ceiling only falls)"
[[law0]]
crate = "busbar-kernel-breaker"