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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions