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.
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) logsTwo paths, two behaviours (the #9485 family again)
Node throws synchronously from all four,
ERR_INVALID_ARG_VALUE, messageThe 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 theCStringfailure asUNKNOWN.The named-import path (
import { spawn, spawnSync } from "node:child_process") does validate — it throwsERR_INVALID_ARG_VALUEsynchronously — but with a different message: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, andexec'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\x00escaping, as node'svalidateArgumentNullCheckdoes.Byte-compare bar: both scripts above print node's lines under
node --experimental-strip-types.