Repro (no harness, plain PHP)
```php
$proc = proc_open([$php, '-r', 'usleep(200000);'],
[0 => ['pipe','r'], 1 => ['pipe','w'], 2 => ['pipe','w']], $pipes);
$r = spawn(fn() => fread($pipes[1], 4096)); // parks on stdout
$k = spawn(function() use ($proc) {
delay(50);
proc_terminate($proc, 15); // SIGTERM child
proc_close($proc); // reap
});
await_all([$r, $k]);
```
`R` parks in `fread()` on the child's stdout pipe. After `K` runs
`proc_terminate` + `proc_close`, the child is dead and the kernel has
closed the child's write-end of the pipe — `R` should wake with EOF
(`fread() == ""` or `false`).
Instead, `R` stays parked forever. The deadlock detector eventually
kicks in, fires `Async\DeadlockError`, and only then `fread()` returns
`false`.
Observed output
```
R: pre-fread
K: terminate
K: close
K: done
=== DEADLOCK REPORT START ===
Coroutines waiting: 2, active_events: 1
…
Coroutine 4 … waiting for: IO(type=pipe, fd=16)
=== DEADLOCK REPORT END ===
R: post-fread: false
Fatal error: Uncaught Async\DeadlockError…
```
Why it matters
This is the missing-wakeup mirror of #129/#133 (spurious-wakeup on shared
handle). For a typical pattern — "stream output of a child process while
something else can kill the child" — every parked reader hangs until the
deadlock detector aborts the request.
Same dance with `stream_socket_pair` + `fclose($write_end)` (covered by
`fuzzy-tests/io/stream_close_during_read.feature`) works correctly, so
this is specific to the pipe(2)-backed reader half of `proc_open` —
likely a `uv_pipe` / `uv_poll` mismatch on POLLHUP, or proc-event
cleanup not notifying the per-pipe poll watcher.
Found by
Discovered while drafting `fuzzy-tests/exec/proc_open_chaos.feature` for
umbrella #143 (chaos coverage gaps). The two cancel-vs-close scenarios
that exercise this race are blocked on the fix; the rapid-storm scenario
(no parked reader) is unaffected.
Acceptance
A parked `fread()` on a proc_open pipe wakes with EOF when the child is
externally terminated and `proc_close`'d. The minimal repro above
prints "R: post-fread: ''" (or "…: false") and "OK" without firing
the deadlock detector.
Repro (no harness, plain PHP)
```php
$proc = proc_open([$php, '-r', 'usleep(200000);'],
[0 => ['pipe','r'], 1 => ['pipe','w'], 2 => ['pipe','w']], $pipes);
$r = spawn(fn() => fread($pipes[1], 4096)); // parks on stdout
$k = spawn(function() use ($proc) {
delay(50);
proc_terminate($proc, 15); // SIGTERM child
proc_close($proc); // reap
});
await_all([$r, $k]);
```
`R` parks in `fread()` on the child's stdout pipe. After `K` runs
`proc_terminate` + `proc_close`, the child is dead and the kernel has
closed the child's write-end of the pipe — `R` should wake with EOF
(`fread() == ""` or `false`).
Instead, `R` stays parked forever. The deadlock detector eventually
kicks in, fires `Async\DeadlockError`, and only then `fread()` returns
`false`.
Observed output
```
R: pre-fread
K: terminate
K: close
K: done
=== DEADLOCK REPORT START ===
Coroutines waiting: 2, active_events: 1
…
Coroutine 4 … waiting for: IO(type=pipe, fd=16)
=== DEADLOCK REPORT END ===
R: post-fread: false
Fatal error: Uncaught Async\DeadlockError…
```
Why it matters
This is the missing-wakeup mirror of #129/#133 (spurious-wakeup on shared
handle). For a typical pattern — "stream output of a child process while
something else can kill the child" — every parked reader hangs until the
deadlock detector aborts the request.
Same dance with `stream_socket_pair` + `fclose($write_end)` (covered by
`fuzzy-tests/io/stream_close_during_read.feature`) works correctly, so
this is specific to the pipe(2)-backed reader half of `proc_open` —
likely a `uv_pipe` / `uv_poll` mismatch on POLLHUP, or proc-event
cleanup not notifying the per-pipe poll watcher.
Found by
Discovered while drafting `fuzzy-tests/exec/proc_open_chaos.feature` for
umbrella #143 (chaos coverage gaps). The two cancel-vs-close scenarios
that exercise this race are blocked on the fix; the rapid-storm scenario
(no parked reader) is unaffected.
Acceptance
A parked `fread()` on a proc_open pipe wakes with EOF when the child is
externally terminated and `proc_close`'d. The minimal repro above
prints "R: post-fread: ''" (or "…: false") and "OK" without firing
the deadlock detector.