Skip to content

fix(server): preserve PTY exit status from the zombie reaper#77

Merged
pcarrier merged 1 commit into
mainfrom
jsegaran/blit-preserve-pty-exit-status
Jul 13, 2026
Merged

fix(server): preserve PTY exit status from the zombie reaper#77
pcarrier merged 1 commit into
mainfrom
jsegaran/blit-preserve-pty-exit-status

Conversation

@jsegaran

Copy link
Copy Markdown
Contributor

The bug

reap_zombies() runs every 5s (crates/server/src/lib.rs:2464) and called waitpid(-1, …, WNOHANG), discarding each child's status. When it reaped a PTY's child before that PTY's collect_exit_status() ran, collect_exit_status() had nothing left to wait on and returned EXIT_STATUS_UNKNOWN. The client maps EXIT_STATUS_UNKNOWNexit code 1 — a fabricated failure even for commands that succeeded.

This surfaced as use_integration "git clone failed but produced no output": in the read replica, of the exit-1 clones, 24 have complete, successful git output (Resolving deltas: 100% … done., a fast-forwarding git pull, even Already up to date.) yet reported exit 1. git returns 0 on those — so the 1 was never git's. It's regular, not flaky: it's a race against a 5s timer, and it widens when the box is busy (cleanup is delayed under load).

The fix

reap_zombies() now records each reaped child's status in a small shared table (reaped_statuses), and collect_exit_status() reads that table first. collect_exit_status() holds the table lock across its own waitpid so the two reapers can't interleave — whichever reaps the child, the real status survives. The table is drained by collect_exit_status(), so in the normal EOF path (where a PTY reaps its own child) it stays near-empty.

Unix-only; Windows reap_zombies is already a no-op.

Repro test

collect_exit_status_survives_backstop_reap forks a child that exits 42, lets reap_zombies() reap it first (the production race), then asserts collect_exit_status returns 42.

  • Before: fails — left: -2147483648 (EXIT_STATUS_UNKNOWN) vs 42.
  • After: passes. Full blit-server suite green (156 tests), fmt + clippy clean.

View in Indent
Tag @indent to continue the conversation here.

The periodic reap_zombies() backstop called waitpid(-1) and discarded the
status. When it reaped a PTY's child before that PTY's collect_exit_status()
ran, collect_exit_status() found nothing to wait on and returned
EXIT_STATUS_UNKNOWN, which the client maps to exit code 1 — a bogus failure
even for commands that succeeded (e.g. a completed git clone reported as
'clone failed').

Record each reaped child's status in a small shared table that
collect_exit_status() drains, and hold the table lock across its own waitpid
so the two reapers can't race. Adds a fork-based repro test.
@github-actions

Copy link
Copy Markdown

Coverage

Crate Lines Functions Regions
alacritty-driver 69.7% (698/1002) 72.0% (54/75) 71.7% (1050/1464)
browser 0.0% (0/807) 0.0% (0/65) 0.0% (0/1370)
cli 28.1% (1208/4299) 41.3% (171/414) 31.1% (2182/7019)
compositor 1.0% (93/9245) 2.0% (8/400) 1.2% (146/12422)
fonts 76.8% (486/633) 85.5% (47/55) 77.9% (922/1183)
gateway 25.7% (362/1411) 29.0% (36/124) 19.4% (449/2318)
proxy 18.3% (150/818) 20.9% (24/115) 20.4% (260/1277)
remote 71.7% (1985/2768) 81.8% (189/231) 74.2% (3749/5051)
sd-notify 73.9% (68/92) 100.0% (6/6) 83.2% (109/131)
server 19.0% (2230/11741) 34.3% (258/753) 20.4% (3728/18242)
ssh 1.9% (7/374) 3.2% (1/31) 0.7% (4/613)
webrtc-forwarder 2.7% (72/2624) 2.1% (4/187) 1.2% (50/4335)
webserver 63.5% (753/1185) 70.8% (121/171) 67.8% (1380/2034)
Total 21.9% (8112/36999) 35.0% (919/2627) 24.4% (14029/57459)

@pcarrier pcarrier merged commit 4ae8cc4 into main Jul 13, 2026
10 of 11 checks passed
@pcarrier pcarrier deleted the jsegaran/blit-preserve-pty-exit-status branch July 13, 2026 09:42
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.

2 participants