Skip to content

subprocess.kill('SIGWINCH') forcefully terminates subprocesses on Windows #64324

Description

@sindresorhus

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions