Version
Current main by source inspection. This appears to affect releases since #55514.
Platform
Microsoft Windows. The behavior is in the _WIN32 path in src/process_wrap.cc.
Subsystem
child_process
What steps will reproduce the bug?
Run this on Windows:
// test.mjs
import {spawn} from 'node:child_process';
const subprocess = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)']);
subprocess.on('error', error => {
console.log('error', error.code);
});
subprocess.on('exit', (exitCode, signalCode) => {
console.log('exit', {exitCode, signalCode});
});
console.log('kill returned:', subprocess.kill('SIGWINCH'));
setTimeout(() => {
console.log('still running:', !subprocess.killed && subprocess.exitCode === undefined && subprocess.signalCode === undefined);
subprocess.kill('SIGKILL');
}, 1000);
How often does it reproduce? Is there a required condition?
Always on Windows.
What is the expected behavior? Why is that the expected behavior?
SIGWINCH should not forcefully terminate the subprocess on Windows.
Before #55514, unsupported signals such as SIGWINCH reached libuv and returned UV_ENOSYS. In JavaScript, ChildProcess.prototype.kill() handles UV_ENOSYS by throwing an ErrnoException.
That behavior was safer for SIGWINCH: it did not terminate the subprocess.
What do you see instead?
Since #55514, src/process_wrap.cc remaps every Windows signal other than SIGKILL, SIGTERM, SIGINT, SIGQUIT, and 0 to SIGKILL before calling libuv:
#ifdef _WIN32
if (signal != SIGKILL && signal != SIGTERM && signal != SIGINT &&
signal != SIGQUIT && signal != 0) {
signal = SIGKILL;
}
#endif
int err = uv_process_kill(&wrap->process_, signal);
The current vendored libuv Windows implementation only forcefully terminates for SIGQUIT, SIGTERM, SIGKILL, and SIGINT, treats 0 as a health check, and returns UV_ENOSYS for unsuported signals:
default:
/* Unsupported signal. */
return UV_ENOSYS;
Because Node now remaps SIGWINCH before libuv sees it, subprocess.kill('SIGWINCH') terminates the subprocess as if SIGKILL had been sent.
This is surprising because SIGWINCH is not a termination signal. Users forwarding terminal resize notifications can accidentally kill a child process on Windows.
Additional information
It was already commented here: #42923 (comment) but it was ignored.
Possible fix: exempt SIGWINCH from the Windows remapping in src/process_wrap.cc, so it keeps the previous unsupported-signal behavior instead of becoming SIGKILL.
Version
Current
mainby source inspection. This appears to affect releases since #55514.Platform
Microsoft Windows. The behavior is in the
_WIN32path insrc/process_wrap.cc.Subsystem
child_process
What steps will reproduce the bug?
Run this on Windows:
How often does it reproduce? Is there a required condition?
Always on Windows.
What is the expected behavior? Why is that the expected behavior?
SIGWINCHshould not forcefully terminate the subprocess on Windows.Before #55514, unsupported signals such as
SIGWINCHreached libuv and returnedUV_ENOSYS. In JavaScript,ChildProcess.prototype.kill()handlesUV_ENOSYSby throwing anErrnoException.That behavior was safer for
SIGWINCH: it did not terminate the subprocess.What do you see instead?
Since #55514,
src/process_wrap.ccremaps every Windows signal other thanSIGKILL,SIGTERM,SIGINT,SIGQUIT, and0toSIGKILLbefore calling libuv:The current vendored libuv Windows implementation only forcefully terminates for
SIGQUIT,SIGTERM,SIGKILL, andSIGINT, treats0as a health check, and returnsUV_ENOSYSfor unsuported signals:Because Node now remaps
SIGWINCHbefore libuv sees it,subprocess.kill('SIGWINCH')terminates the subprocess as ifSIGKILLhad been sent.This is surprising because
SIGWINCHis not a termination signal. Users forwarding terminal resize notifications can accidentally kill a child process on Windows.Additional information
It was already commented here: #42923 (comment) but it was ignored.
Possible fix: exempt
SIGWINCHfrom the Windows remapping insrc/process_wrap.cc, so it keeps the previous unsupported-signal behavior instead of becomingSIGKILL.