Skip to content

Fix #604: test_proc_stream 8a waits for child exit instead of spin-count#666

Merged
InauguralPhysicist merged 1 commit into
InauguralSystems:mainfrom
Nitjsefnie:fix-604-proc-stream-8a
Jul 18, 2026
Merged

Fix #604: test_proc_stream 8a waits for child exit instead of spin-count#666
InauguralPhysicist merged 1 commit into
InauguralSystems:mainfrom
Nitjsefnie:fix-604-proc-stream-8a

Conversation

@Nitjsefnie

Copy link
Copy Markdown
Contributor

Fixes #604 — case 8a's 100,000-iteration spin loop replaced with proc_wait of h[0] before the write, so "proc_write to a dead child returns -1" is pinned deterministically instead of by wall-clock luck.

Which branch of your sketch, and why

You flagged the proc_wait close-and-reap question as the decider. builtin_proc_wait (src/builtins.c:3548) is a bare waitpid(pid, &status, 0) EINTR loop — it never touches the handle's fds. So after the wait, the parent's write end is still open, the only reader is gone (parent's in_pipe[0] closed at spawn, FD_CLOEXEC on all four pipe fds, no SIGCHLD reaper anywhere in src/), SIGPIPE is already ignored, and the write hits EPIPE with 0 bytes written → -1. That's still the "write to dead child" property — not "closed handle" — so the plain proc_wait route works and no try-wait polling is needed. The trailing proc_wait at the end of case 8 is dropped since the child is now reaped earlier (keeping it would be a discarded-return double-wait via ECHILD).

One file, +6/-6; want=-1 untouched; no src/ changes.

Evidence the fix is load-bearing (this box: 5 cores, all pinned with busy-loops)

run result
unfixed control ×10 under load 6/10 failed, every failure the exact issue signature MISMATCH 8a: got=5 want=-1
fixed file ×10 under same load 10/10 pass
mutant (wait replaced by a no-op) ×10 under load 10/10 failed — the proc_wait line is the synchronization
independent re-verification (fresh runs) control 5/5 failed, fixed 5/5 passed

So the flake reproduces reliably under load on this box and the fix eliminates it — same failure your macOS ARM leg hit on #643.

Full suite: 3085/3085 before and after (release build, umask 022).


Generated by Claude Fable 5 (brief, review), Kimi K3 (implementation)

…stead of spin-count

proc_wait only waitpid()s the pid and leaves the parent's pipe fds open
(src/builtins.c builtin_proc_wait), so waiting first and then writing
pins the intended property: EPIPE from a dead child, not EBADF from a
closed handle. The 100k spin loop only hoped the child had been
scheduled; under load it hadn't, and the write returned 5.

Co-Authored-By: Kimi K3 <noreply@kimi.com>
@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

Verified and merging — this is the right fix: it removes the timing dependency entirely rather than widening the window.

The load-bearing claim checks out. I read builtin_proc_wait (src/builtins.c:3548) rather than trust the description: it is a bare waitpid(pid, &status, 0) EINTR loop and touches none of the handle's fds. So after the wait the child (the only reader) is reaped but the parent's write end is still open, SIGPIPE is ignored, and proc_write hits EPIPE → -1. That is still the "write to a dead child" property the case is asserting, not "write to a closed handle" — your reasoning about which branch this lands on is exactly right. Dropping the trailing proc_wait is correct too: the child is reaped earlier now, so a second wait would be a discarded ECHILD.

On the evidence: I confirmed the fixed test passes deterministically (8/8 under 5 pinned busy-loops here). I could not reproduce the flake on the unfixed version on this box — it passed 8/8 under the same load — but that is a scheduling difference, not a refutation: this machine happens to let /bin/true exit within the 100k-iteration window even loaded. The flake is real regardless — our own macOS ARM leg hit it on #643, and your 6/10-fail + mutant-10/10-fail table is exactly the shape of a genuine race. And none of that is load-bearing for the fix's correctness: a spin loop that hopes the child was scheduled is replaced by a primitive that guarantees it was reaped. That is unconditionally correct whether or not the race manifests on any given host.

The mutant test (wait → no-op, 10/10 fail) is the part I like most — it proves the proc_wait line is the synchronization and not incidental.

One file, +6/-6, no src changes, deterministic. Thank you.

If you find the project useful or enjoyed contributing, a star helps others discover it.

@InauguralPhysicist
InauguralPhysicist merged commit 1bb6b7b into InauguralSystems:main Jul 18, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test_proc_stream case 8a is timing-flaky under load: spin-wait for /bin/true exit

2 participants