From 38627f194bf6118bddaf69a90ad8f9e9052c9238 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Wed, 5 Aug 2026 21:33:11 +0500 Subject: [PATCH 1/3] src: don't kill own process group on failed spawn 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 --- src/process_wrap.cc | 6 ++++- .../test-child-process-kill-spawn-error.js | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 test/parallel/test-child-process-kill-spawn-error.js diff --git a/src/process_wrap.cc b/src/process_wrap.cc index 21ccb2a9989b..21209554c163 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -112,6 +112,7 @@ class ProcessWrap : public HandleWrap { object, reinterpret_cast(&process_), AsyncWrap::PROVIDER_PROCESSWRAP) { + process_.pid = 0; MarkAsUninitialized(); } @@ -355,7 +356,10 @@ class ProcessWrap : public HandleWrap { signal = SIGKILL; } #endif - int err = uv_process_kill(&wrap->process_, signal); + // uv_spawn() only assigns a pid when it succeeds, and kill(0, signal) + // signals every process in our own process group. + int err = wrap->process_.pid > 0 ? uv_process_kill(&wrap->process_, signal) + : UV_ESRCH; args.GetReturnValue().Set(err); } diff --git a/test/parallel/test-child-process-kill-spawn-error.js b/test/parallel/test-child-process-kill-spawn-error.js new file mode 100644 index 000000000000..380d09513d14 --- /dev/null +++ b/test/parallel/test-child-process-kill-spawn-error.js @@ -0,0 +1,22 @@ +'use strict'; +const common = require('../common'); +const assert = require('assert'); +const { spawn } = require('child_process'); + +// Killing a child process that never spawned must not signal the process +// group of the caller. The check runs in a detached child so that a +// regression cannot take the test runner down with it. +// Refs: https://github.com/nodejs/node/issues/65052 +const script = ` + const { spawn } = require('child_process'); + const child = spawn('foo123'); + child.on('error', () => {}); + if (child.kill() !== false || child.killed !== false) process.exit(1); +`; + +const child = spawn(process.execPath, ['-e', script], { detached: true }); + +child.on('exit', common.mustCall((code, signal) => { + assert.strictEqual(signal, null); + assert.strictEqual(code, 0); +})); From 2029ea6ba23524898ab71dafc24334ecc31f50ea Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Wed, 5 Aug 2026 21:57:48 +0500 Subject: [PATCH 2/3] test: drop redundant Refs comment from kill-spawn-error test --- test/parallel/test-child-process-kill-spawn-error.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/parallel/test-child-process-kill-spawn-error.js b/test/parallel/test-child-process-kill-spawn-error.js index 380d09513d14..d8e25cc0f8bf 100644 --- a/test/parallel/test-child-process-kill-spawn-error.js +++ b/test/parallel/test-child-process-kill-spawn-error.js @@ -6,7 +6,6 @@ const { spawn } = require('child_process'); // Killing a child process that never spawned must not signal the process // group of the caller. The check runs in a detached child so that a // regression cannot take the test runner down with it. -// Refs: https://github.com/nodejs/node/issues/65052 const script = ` const { spawn } = require('child_process'); const child = spawn('foo123'); From 3174c46cf6ffc9b81584ccef8b68612d54196c80 Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Wed, 5 Aug 2026 23:22:53 +0500 Subject: [PATCH 3/3] test: move inline kill-spawn-error script to a fixture --- test/fixtures/child-process-kill-spawn-error.js | 4 ++++ test/parallel/test-child-process-kill-spawn-error.js | 11 +++-------- 2 files changed, 7 insertions(+), 8 deletions(-) create mode 100644 test/fixtures/child-process-kill-spawn-error.js diff --git a/test/fixtures/child-process-kill-spawn-error.js b/test/fixtures/child-process-kill-spawn-error.js new file mode 100644 index 000000000000..6dffe5b27efc --- /dev/null +++ b/test/fixtures/child-process-kill-spawn-error.js @@ -0,0 +1,4 @@ +const { spawn } = require('child_process'); +const child = spawn('foo123'); +child.on('error', () => {}); +if (child.kill() !== false || child.killed !== false) process.exit(1); diff --git a/test/parallel/test-child-process-kill-spawn-error.js b/test/parallel/test-child-process-kill-spawn-error.js index d8e25cc0f8bf..5889e169492f 100644 --- a/test/parallel/test-child-process-kill-spawn-error.js +++ b/test/parallel/test-child-process-kill-spawn-error.js @@ -1,19 +1,14 @@ 'use strict'; const common = require('../common'); +const fixtures = require('../common/fixtures'); const assert = require('assert'); const { spawn } = require('child_process'); // Killing a child process that never spawned must not signal the process // group of the caller. The check runs in a detached child so that a // regression cannot take the test runner down with it. -const script = ` - const { spawn } = require('child_process'); - const child = spawn('foo123'); - child.on('error', () => {}); - if (child.kill() !== false || child.killed !== false) process.exit(1); -`; - -const child = spawn(process.execPath, ['-e', script], { detached: true }); +const childPath = fixtures.path('child-process-kill-spawn-error.js'); +const child = spawn(process.execPath, [childPath], { detached: true }); child.on('exit', common.mustCall((code, signal) => { assert.strictEqual(signal, null);