镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
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.

Activity

  1. PickBas commented on Jul 9, 2026

    @PickBas
    Contributor

    @sindresorhus Just to confirm the behavior you expect, consider the following snippet, which is similar to yours:

    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});
    });
    
    try {
    	console.log('kill returned:', subprocess.kill('SIGWINCH'));
    } catch (err) {
    	console.log('kill failed:', err.code); // ENOSYS on Windows
    }
    
    setTimeout(() => {
    	const stillRunning = !subprocess.killed && subprocess.exitCode === null && subprocess.signalCode === null;
    	console.log('still running:', stillRunning);
    	subprocess.kill('SIGKILL');
    }, 1000);

    After running it with the official node binary, the result is the following:

    PS C:\64324> node .\index.mjs
    kill returned: true
    exit { exitCode: null, signalCode: 'SIGKILL' }
    still running: false

    After running it with the changes you proposed:

    PS C:\64324> ..\node\Release\node.exe .\index.mjs
    kill failed: ENOSYS
    still running: true
    exit { exitCode: null, signalCode: 'SIGKILL' }

    As you can see, in the second run, subprocess.kill('SIGWINCH') throws an exception. Is that the behavior you expect? It seems to contradict the requirements in the issue you referenced (#42923).

  2. sindresorhus commented on Jul 9, 2026

    @sindresorhus
    Author

    Yes, ENOSYS is the behavor I expect, and I don't think it contradicts #42923.

    That issue was about termination signals like SIGHUP, where "ignore the signal and kill like SIGKILL" is a reasonable Windows fallback. SIGWINCH is categorically different: it's a non-fatal notification (terminal resize) whose default action on POSIX is to be ignored. It should never terminate a process.

    So the two positions are compatible:

    Throwing ENOSYS (the pre-#55514 behavior) is fine and the simplest fix. A silent no-op would be even nicer, but the important thing is just that it doesn't silently SIGKILL the child.

  3. PickBas commented on Jul 10, 2026

    @PickBas
    Contributor

    @sindresorhus There was already documentation before the issue you're referencing.

    It explicitly states that, on Windows, the signal argument is ignored and the process is terminated forcefully and abruptly. So the expectations described in this issue are inconsistent with both the documentation and the current behavior, meaning the current behavior is expected.

    That makes this issue a feature request: start throwing exceptions for unsupported signals on Windows instead of treating them as SIGKILL. It also wouldn't make sense to special-case only SIGWINCH. If the behavior is changed, it should apply consistently to all unsupported non-terminating signals, not just SIGWINCH, and the documentation should be updated accordingly to reflect that behavior.

  4. sindresorhus commented on Jul 10, 2026

    @sindresorhus
    Author

    That doc line is aspirational, not descriptive. It was added in #34867 back in 2020, but the runtime never actually behaved that way for non-terminating signals. For over a decade kill('SIGWINCH') and kill('SIGHUP') returned UV_ENOSYS and threw. The author of #55514 even says so ("it throws an exception with the code ENOSYS. This is not consistent with the documentation"), and #42923 flagged the ambiguity directly. So #55514 changed the code to match the doc, it didn't document what the code already did, and it did that by making it worse: kill('SIGWINCH') went from throwing (harmless) to silently force-killing the child, with no notable-change flag.

    The doc wording is also contradictory. "Ignored ... similar to SIGKILL" are opposites, and on POSIX the default disposition of SIGWINCH is actually Ignore (same for SIGCHLD and SIGURG). Mapping it to SIGKILL inverts the signal, it doesn't ignore it.

    I agree it shouldn't special-case SIGWINCH. The consistent fix is just to revert what #55514 did and let unsupported signals throw UV_ENOSYS again. One line, backward compatible, non-destructive, same for every signal, and anyone who wants a hard kill still has SIGKILL and SIGTERM. Then update the docs to say unsupported signals throw instead of claiming they're ignored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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