Skip to content

Inconsistent validation of custom signals #44749

Closed

Description

@fasttime

Version

18.9.0

Platform

Darwin

Subsystem

child_process
os

What steps will reproduce the bug?

In the current implementation, spawn and related functions throw synchronous errors for invalid signals, e.g.:

import { spawn } from 'node:child_process';

// TypeError [ERR_UNKNOWN_SIGNAL]
spawn(process.execPath, ['-v'], { killSignal: 12345 });

Anyway, it's easy to define custom signals that pass the validation:

import { spawn } from 'node:child_process';
import { constants } from 'node:os';

constants.signals.FOOBAR = 12345;
// no error
spawn(process.execPath, ['-v'], { killSignal: 12345 });

The same applies when signals are identified by name, i.e:

import { spawn } from 'node:child_process';

// TypeError [ERR_UNKNOWN_SIGNAL]
spawn(process.execPath, ['-v'], { killSignal: 'FOOBAR' });

and

import { spawn } from 'node:child_process';
import { constants } from 'node:os';

constants.signals.FOOBAR = 12345;
// no error
spawn(process.execPath, ['-v'], { killSignal: 'FOOBAR' });

but if a custom signal is added after the internal mapping of numeric codes to names signals has been generated, validation fails for numeric codes (but not for signal names):

import { spawn } from 'node:child_process';
import { constants } from 'node:os';

spawn(process.execPath, ['-v'], { killSignal: 1 });
constants.signals.FOOBAR = 12345;
spawn(process.execPath, ['-v'], { killSignal: 'FOOBAR' }); // no error
spawn(process.execPath, ['-v'], { killSignal: 12345 }); // TypeError [ERR_UNKNOWN_SIGNAL]

This strikes to me as issue of improper memoization:

node/lib/internal/util.js

Lines 272 to 283 in 9ae2af4

let signalsToNamesMapping;
function getSignalsToNamesMapping() {
if (signalsToNamesMapping !== undefined)
return signalsToNamesMapping;
signalsToNamesMapping = ObjectCreate(null);
for (const key in signals) {
signalsToNamesMapping[signals[key]] = key;
}
return signalsToNamesMapping;
}

where signalsToNamesMapping never changes once created.

On the other hand, I'm not convinced that the current behavior - being able to add, remove, and change signals at will - is intended. If it is, then the validation logic should be fixed. If it is not, then constants.signals should be better frozen to prevent changes.

How often does it reproduce? Is there a required condition?

All the time.

What is the expected behavior?

Either custom numeric signals should always be accepted as valid, or they should always be rejected. If they should be rejected, then custom string signals should be rejected, too.

What do you see instead?

Inconsistent behavior, depending on (possibly unrelated) previous operations.

Additional information

No response

Activity

  1. kvakil commented on Oct 1, 2022

    @kvakil
    Contributor

    Do you have a usecase for "custom signals"? My gut says that it makes more sense to freeze all of the constants objects to prevent changing it. We already prevent changing/deleting the properties of constants objects, but presumably it's an oversight that the object itself is not frozen. It sounds like that would match your "expected behavior"?

  2. fasttime commented on Oct 1, 2022

    @fasttime
    ContributorAuthor

    @kvakil I'm also under the impression that allowing changes to the the predefined signals was never intended. This has been possible ever since os.constants.signals was made public in Node 6.3, so I imagine that someone could be relying on the current behavior, but I don't know any examples. Freezing signals would be probably a breaking change, but also the safest solution.

  3. added
    child_processIssues and PRs related to the child_process subsystem.
    good first issueIssues that are suitable for first-time contributors.
    on Sep 12, 2025
  4. developeruzairsaleem commented on Sep 26, 2025

    @developeruzairsaleem

    Hi I would love to work on this issue.

  5. BharathPESU commented on Nov 4, 2025

    @BharathPESU

    Hi — I’d like to take this one.

    Summary: spawn/exec validation for killSignal is inconsistent when os.constants.signals is mutated at runtime. If a numeric signal is added via constants.signals.FOO = before internal memoization runs, numeric validation accepts it; if added after memoization, numeric validation rejects it while name-based validation accepts it.

    Root cause (brief): getSignalsToNamesMapping() memoizes a numeric→name mapping at first call and never refreshes it, so subsequent mutations of constants.signals are not reflected.

    Proposed plan: either (A) remove the memoization and compute the mapping on each validation call (keeps dynamic behavior but consistent), or (B) freeze constants.signals at start-up so it cannot be mutated (prevent dynamic changes). I’ll prepare a PR with both alternatives (defaulting to A) and tests.

    Could you assign this to me? I’ll open the PR with a proposed patch + unit tests and mark it for review.

  6. added a commit that references this issue on Dec 13, 2025
  7. added a commit that references this issue on Dec 22, 2025
  8. added a commit that references this issue on Jan 13, 2026
  9. added a commit that references this issue on Jan 19, 2026
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

    child_processIssues and PRs related to the child_process subsystem.good first issueIssues that are suitable for first-time contributors.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions