You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
child_process.spawnSync blocks until stdio EOF even after the child is reaped — please document (detached descendant can hold it open forever) #67197
Verdict framing (E3 diagnosis, ISSUE #241): this is NOT filed as a runtime bug.
The behavior is defensible (a pipe write-end held by a detached descendant is
indistinguishable from the child still writing), and timeout is the sanctioned
bound. The gap is documentation: nothing in the child_process.spawnSync docs
states that the call waits for stdio EOF, not merely child exit, and that
fds inherited by detached descendants of the child can hold the wait open
indefinitely after the child has been reaped.
Title:child_process.spawnSync blocks until stdio EOF even after the child is reaped — please document (detached descendant can hold it open forever)
Version: v24.19.0 Platform: Linux 7.0.0-28-generic x86_64
Minimal reproduction
// repro.mjs — run with: node repro.mjsimport{spawnSync}from'node:child_process';constchildScript=` const { spawn } = await import('node:child_process'); // grandchild: detached, inherits stdout/stderr write-ends, outlives the child const gc = spawn(process.execPath, ['-e', 'setInterval(()=>{},1e6)'], { detached: true, stdio: ['ignore', 'inherit', 'inherit'], }); gc.unref(); process.stdout.write('child-done\\n');`;constr=spawnSync(process.execPath,['-e',childScript],{encoding: 'utf8',stdio: ['ignore','pipe','pipe'],timeout: 15000,});console.log('status =',r.status);// 0 — child exit was collectedconsole.log('stdout =',r.stdout);// "child-done\n" — output fully receivedconsole.log('error =',r.error?.code);// ETIMEDOUT — yet the call still hit the full 15s timeout
Observed on v24.19.0: status is collected (child exited and was reaped) and the
child's stdout payload is fully received, but the call still blocks for the entire timeout window. Without timeout it blocks forever, with the main thread
native-blocked in SyncProcessRunner::Spawn → uv_run → epoll_pwait(-1). ps at hang time shows no child of the caller (the direct child is reaped; the
fd holder was reparented to init under its own session).
Why this matters in practice
Test harnesses (e.g. node --test suites) that call spawnSync/execFileSync
without timeout can hang irrevocably: a test-level timeout cannot fire
because the JS event loop is native-blocked inside the sync call.
The only in-band bound is the timeout option; nothing in the docs suggests
why it would ever be needed for a fast-exiting child.
Request
Document on child_process.spawnSync (and execFileSync/execSync):
the call returns only after the child exits and stdout/stderr reach EOF;
file descriptors inherited by any descendant the child leaves behind (e.g. detached grandchildren) hold the wait open after the child is reaped;
timeout is the way to bound this.
Happy to file as a PR against doc/api/child_process.md if a maintainer confirms
the semantics are as-intended.
Activity