Skip to content

src: don't kill own process group on failed spawn - #65054

Open
lazerg wants to merge 3 commits into
nodejs:mainfrom
lazerg:fix/issue-65052-kill-unspawned-process
Open

src: don't kill own process group on failed spawn#65054
lazerg wants to merge 3 commits into
nodejs:mainfrom
lazerg:fix/issue-65052-kill-unspawned-process

Conversation

@lazerg

@lazerg lazerg commented Aug 5, 2026

Copy link
Copy Markdown

libuv only assigns a pid to the process handle once uv_spawn() succeeds, so a child that never started keeps pid 0. Calling kill() on that child still reached uv_process_kill(), which ended up in kill(0, signal) and signalled every process in the caller's own process group, Node included. The handle is now reported as ESRCH when there is no pid, so child.kill() just returns false. The pid is also zeroed in the constructor so the check never reads an unassigned value.

Reproducing it on its own needs no prototype tampering:

const { spawn } = require('child_process');
const child = spawn('foo123');
child.on('error', () => {});
child.kill();  // terminates the whole process group

In the report the spawn failed for a different reason: overriding Array.prototype[Symbol.iterator] makes normalizeSpawnArguments() build an empty env, so the command was no longer found through PATH. The dead handle is what took the parent down.

Behavior change

kill() on a child that failed to spawn used to return true on POSIX. It now returns false.

Windows was never affected by the process-group problem, since uv_process_kill() bails out with EINVAL when the handle has no process handle. It reached the throw new ErrnoException(err, 'kill') branch instead, so there kill() goes from throwing EINVAL to returning false. Both platforms now agree.

subprocess.killed is also left alone in this case, which matches what the docs already say about it being set only when a signal is sent successfully.

Fixes: #65052

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. child_process Issues and PRs related to the child_process subsystem. needs-ci PRs that need a full CI run. labels Aug 5, 2026
libuv only assigns a pid to the process handle once uv_spawn() has
succeeded, so a child that never started keeps pid 0. Calling kill()
on such a child still reached uv_process_kill(), which ended up in
kill(0, signal) and signalled every process in the caller's own
process group, Node included.

Return ESRCH when the handle has no pid, and zero the pid in the
constructor so the check never reads an unassigned value.

Signed-off-by: Lazizbek Ergashev <lazerg2@gmail.com>
@lazerg
lazerg force-pushed the fix/issue-65052-kill-unspawned-process branch from 1c77865 to 38627f1 Compare August 5, 2026 16:51
Comment thread src/process_wrap.cc
object,
reinterpret_cast<uv_handle_t*>(&process_),
AsyncWrap::PROVIDER_PROCESSWRAP) {
process_.pid = 0;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point this? Won't the PID be assigned to 0 anyway by libuv?

@lazerg lazerg Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked uv__handle_init: it only sets loop / type/ flags / handle_queue, never touches pid. So before this, pid was whatever was on the stack, not 0. Kept the explicit zero.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm thinking into this too much, but it seems redundant to initialize properties when other properties serve the same purpose. Surely elsewhere on the object some other indication of failure is present. If not, then I guess this is correct.

@lazerg lazerg Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, nothing else on the object marks it. state_ only reflects the handle's own open/close lifecycle, not whether uv_spawn set a pid. Without the explicit zero it's leftover stack memory.

btw thank you for putting html injection πŸ’€ . But I think most of the frontier models can notice it instantly :)

Comment thread test/parallel/test-child-process-kill-spawn-error.js Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. child_process Issues and PRs related to the child_process subsystem. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SIGTERM killing an invalid node:child_process spawn terminates main process

3 participants