Skip to content

child_process via require(): null bytes in file/args are not validated — 'spawn /bin/true UNKNOWN' instead of ERR_INVALID_ARG_VALUE; the named-import path validates with a non-node message #9537

Description

@proggeramlug

Found through the MCP debug logs that #9491 unlocked (#9500 / #9531): the harness's null-byte case (E62_mcp_add_json_unicode, a server arg containing \x00) logs

node : Connection failed after 0ms: The argument 'args[1]' must be a string without null bytes. Received 'a\x00b'
perry: Connection failed after 1ms: spawn /bin/true UNKNOWN

Two paths, two behaviours (the #9485 family again)

import { createRequire } from "node:module";
const cp: any = createRequire(import.meta.url)("child_process");   // cross-spawn's / the MCP SDK's shape
cp.spawn("/bin/true", ["a\x00b"]);          // perry: returns a ChildProcess, then 'error' UNKNOWN "spawn /bin/true UNKNOWN"
cp.spawn("/bin/tr\x00ue", []);              // perry: 'error' UNKNOWN "spawn /bin/tr ue UNKNOWN"
cp.spawnSync("/bin/true", ["a\x00b"]);      // perry: returns a result object
cp.execFile("/bin/true", ["a\x00b"], cb);   // perry: cb(err) with code UNKNOWN

Node throws synchronously from all four, ERR_INVALID_ARG_VALUE, message The argument 'args[0]' must be a string without null bytes. Received 'a\x00b' (or 'file' for the command). Perry's CJS-default-namespace path performs no validation, hands the string to the OS, and reports the CString failure as UNKNOWN.

The named-import path (import { spawn, spawnSync } from "node:child_process") does validate — it throws ERR_INVALID_ARG_VALUE synchronously — but with a different message:

node : "The argument 'args[0]' must be a string without null bytes. Received 'a\\x00b'"
perry: "The argument must not contain null bytes"

So: (1) the validation lives on one lowering path and not the other — the dispatch-router / CJS-namespace path (#9498 made these calls reachable) skips it; the check belongs in the runtime entry points (js_child_process_spawn_streams, js_child_process_spawn_sync, js_child_process_exec_file, and exec's command) so every spelling shares it; (2) the message should name the argument ('file', 'args[i]', 'options.cwd', 'options.env'...) and echo the received value with \x00 escaping, as node's validateArgumentNullCheck does.

Byte-compare bar: both scripts above print node's lines under node --experimental-strip-types.

Activity

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