Status: investigation and proposal only.
- The owner authorized this issue on 2026-09-28 as an independent CodeSpace correctness and hygiene investigation.
- It is not part of CS-RG, which stays suspended, and not a backend rewrite.
- Nothing here approves an implementation, a pin change or a dependency. Any implementation needs its own approval.
Problem
On macOS, a child spawned by one CodeSpace execution can inherit another execution's transient descriptors while
they are being created in the same process. Two facts combine:
- Creation is not atomic. Rust std and Tokio (via mio) create pipes and sockets, and portable-pty creates PTYs,
in two steps: the descriptor first, then close-on-exec. On macOS there is no atomic variant for these calls. In
between, the descriptor is inheritable.
- Children exclude nothing. std and Tokio
Command do not use POSIX_SPAWN_CLOEXEC_DEFAULT, so a child
inherits every descriptor that is inheritable at the moment it is spawned.
So, in the Gateway process (InProcess) or a Runner worker (UDS), a concurrent spawn can capture another execution's
pipe end or PTY descriptor. Linux is different: std uses pipe2, SOCK_CLOEXEC and accept4 there, which are
atomic. Linux was not examined further.
Why it matters to CodeSpace
This is inference; none of it has been measured in CodeSpace's own processes.
- Delayed output EOF. A stray child holding another execution's stdout or stderr write end delays that
execution's EOF until the stray child exits. Output completion can then wait on an unrelated process.
- Stdin EOF lost. A stray child holding another execution's stdin write end means closing stdin no longer gives
that child EOF.
- PTYs kept open. A stray holder of a PTY slave keeps the terminal open after its child exits, so the master sees
no EOF. A stray holder of a master keeps the terminal alive after CodeSpace drops it.
- Long-lived leaks. A long-lived child keeps whatever it inherited for its whole lifetime. An example is the UDS
worker the Gateway spawns.
Evidence so far
-
Source. Read at Rust 1.88.0, 1.95.0 and 1.98.1, mio 1.2.3, tokio 1.53.1 and Codex 72b8d8b: DevGuard handoff
packet section 8.
portable-pty 0.9.0, used by CodeSpace's pinned PTY path, calls openpty and then sets close-on-exec
(src/unix.rs:36, :64-65).
-
Test: BD-1. A scratch crate outside both repositories, Codex 72b8d8b, one host, one run. During concurrent
Codex PTY launches, unrelated std and Tokio children received:
- a transient PTY master in 104 of 2000 cases;
- a PTY slave in 4 of 2000;
- pipe ends in 39 of 2000.
See packet section 11.
-
Test: W3 experiment C1. A scratch harness, two runs, 400 children per cell.
-
Not run:
- CodeSpace's own processes;
- representative workloads;
- frequency of the effects above;
- Linux.
Spawn sites on macOS (main at ab0341b)
| Site |
Spawner |
Child-side exclusion |
Pipe execution, crates/runner/src/process.rs:283 |
Tokio Command, piped stdio |
none |
Patch helper, crates/runner/src/patch_helper.rs:50 |
Tokio Command, piped stdio |
none |
UDS worker, crates/server/src/runtime.rs:33 |
Tokio Command |
none |
PTY execution through codespace-pty (crates/pty/src/lib.rs:66) |
Codex portable path, then portable-pty |
best-effort sweep in the child (close_random_fds, portable-pty src/unix.rs:152, :276) |
The Linux sandbox helper is not probed or spawned on macOS (crates/runner/src/linux_sandbox.rs:60-69).
Candidate directions (none chosen)
- Kernel close-by-default for CodeSpace's own local spawns.
- Codex's
Command with DescriptorPolicy::Explicit does this.
- It does not exist at CodeSpace's pin (
6b9826e). It is at stable rust-v0.157.1 as DescriptorPolicy::StdioOnly,
and at the prerelease and Codex main as Explicit.
- Using it needs a reviewed pin PR under the existing pin process.
- A best-effort
pre_exec sweep of inheritable descriptors in CodeSpace's own spawns, as portable-pty does. This
moves std from posix_spawn to fork and exec.
- One CodeSpace-internal lock around its own descriptor creation and spawns. It covers only code that takes the
lock. Descriptors created inside Codex, Tokio or other libraries stay uncovered.
Every candidate must keep today's semantics:
- stdio wiring,
kill_on_drop, reaping, exit status and signals, process group, working directory, PATH and arg0
behaviour;
- error reporting;
- the governance-free
off path.
Proposed next steps (each needs its own approval)
- Inventory every descriptor-creating and spawning path in the Gateway and Runner processes on macOS, including
library-internal ones.
- A bounded diagnostic: measure whether the effects above occur with CodeSpace's own pipe and PTY paths under
concurrency, for example one execution's EOF latency while unrelated children spawn.
- Compare the candidates on preserved semantics and pin requirements, then choose, or decide no change is needed.
Out of scope
- DevGuard integration and CS-RG;
- pin changes;
- changes to
off semantics;
- any implementation.
Trackers: #76 and novelKR/DevGuard#14.
Status: investigation and proposal only.
Problem
On macOS, a child spawned by one CodeSpace execution can inherit another execution's transient descriptors while
they are being created in the same process. Two facts combine:
in two steps: the descriptor first, then close-on-exec. On macOS there is no atomic variant for these calls. In
between, the descriptor is inheritable.
Commanddo not usePOSIX_SPAWN_CLOEXEC_DEFAULT, so a childinherits every descriptor that is inheritable at the moment it is spawned.
So, in the Gateway process (InProcess) or a Runner worker (UDS), a concurrent spawn can capture another execution's
pipe end or PTY descriptor. Linux is different: std uses
pipe2,SOCK_CLOEXECandaccept4there, which areatomic. Linux was not examined further.
Why it matters to CodeSpace
This is inference; none of it has been measured in CodeSpace's own processes.
execution's EOF until the stray child exits. Output completion can then wait on an unrelated process.
that child EOF.
no EOF. A stray holder of a master keeps the terminal alive after CodeSpace drops it.
worker the Gateway spawns.
Evidence so far
Source. Read at Rust 1.88.0, 1.95.0 and 1.98.1, mio 1.2.3, tokio 1.53.1 and Codex
72b8d8b: DevGuard handoffpacket section 8.
portable-pty 0.9.0, used by CodeSpace's pinned PTY path, calls
openptyand then sets close-on-exec(
src/unix.rs:36,:64-65).Test: BD-1. A scratch crate outside both repositories, Codex
72b8d8b, one host, one run. During concurrentCodex PTY launches, unrelated std and Tokio children received:
See packet section 11.
Test: W3 experiment C1. A scratch harness, two runs, 400 children per cell.
CommandandDescriptorPolicy::Explicit, which usesPOSIX_SPAWN_CLOEXEC_DEFAULTon macOS, held none in any cell of either run.Not run:
Spawn sites on macOS (
mainatab0341b)crates/runner/src/process.rs:283Command, piped stdiocrates/runner/src/patch_helper.rs:50Command, piped stdiocrates/server/src/runtime.rs:33Commandcodespace-pty(crates/pty/src/lib.rs:66)close_random_fds, portable-ptysrc/unix.rs:152,:276)The Linux sandbox helper is not probed or spawned on macOS (
crates/runner/src/linux_sandbox.rs:60-69).Candidate directions (none chosen)
CommandwithDescriptorPolicy::Explicitdoes this.6b9826e). It is at stable rust-v0.157.1 asDescriptorPolicy::StdioOnly,and at the prerelease and Codex
mainasExplicit.pre_execsweep of inheritable descriptors in CodeSpace's own spawns, as portable-pty does. Thismoves std from
posix_spawnto fork and exec.lock. Descriptors created inside Codex, Tokio or other libraries stay uncovered.
Every candidate must keep today's semantics:
kill_on_drop, reaping, exit status and signals, process group, working directory, PATH andarg0behaviour;
offpath.Proposed next steps (each needs its own approval)
library-internal ones.
concurrency, for example one execution's EOF latency while unrelated children spawn.
Out of scope
offsemantics;Trackers: #76 and novelKR/DevGuard#14.