From 007e181f680250f8b2325c6e0c2d993058d0dd2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E4=BD=B3=E6=B5=A9Alben?= <35291372+XiaoBinGan@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:11:35 +0800 Subject: [PATCH] fix(javascript): require exactly one fork IPC channel (#8017) * fix(javascript): require exactly one fork IPC channel Node's fork() rejects an explicit stdio array that carries more than one IPC channel, and the throw is synchronous, before any child process exists: if (ArrayPrototypeFilter(options.stdio, ...'ipc').length > 1) throw new ERR_IPC_ONE_PIPE() The analysis already dropped an array with no "ipc" (#7992) but still reported a resolved fork for an array carrying it twice. Measured on Node 24.22.1: fork({stdio: ["ipc", "ipc"]}) -> THROWS ERR_IPC_ONE_PIPE fork({stdio: ["pipe", "ipc", "ipc"]}) -> THROWS ERR_IPC_ONE_PIPE fork({stdio: ["ipc"]}) -> SPAWNED connected=true fork({stdio: ["pipe", "ipc"]}) -> SPAWNED connected=true fork({stdio: ["pipe", "pipe", "pipe", "ipc"]})-> SPAWNED connected=true So the required count is exactly one, not at least one. Count the resolved "ipc" literals and reject the candidate unless the count is one; a spread or an unreadable element may still change the count and is left alone, as before. * chore: retrigger title and readiness checks after the title was corrected --- ...riptSemanticAnalysis.fork_stdio_ipc.test.ts | 12 ++++++++++++ .../javascriptSemanticChildProcesses.ts | 18 ++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/domain/javascript/javascriptSemanticAnalysis.fork_stdio_ipc.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.fork_stdio_ipc.test.ts index 0d3e845a3d..fb9e369ec8 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.fork_stdio_ipc.test.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.fork_stdio_ipc.test.ts @@ -37,6 +37,18 @@ it.each([ ]); }); +/** + * Exactly one channel is required: naming `"ipc"` twice throws ERR_IPC_ONE_PIPE + * for the same synchronous reason, so those calls are not spawns either. + */ +it.each([ + ["ipc twice", `["ipc", "ipc"]`], + ["three ipc entries", `["ipc", "ipc", "ipc"]`], + ["a pipe between two ipc entries", `["pipe", "ipc", "ipc"]`], +] as const)("emits no fork for a stdio array with %s", (_name, stdio) => { + expect(spawnsFor(`fork("worker.js", { stdio: ${stdio} })`)).toEqual([]); +}); + it.each([ ["a string stdio", `fork("worker.js", { stdio: "pipe" })`], ["omitted stdio", `fork("worker.js", {})`], diff --git a/src/domain/javascript/javascriptSemanticChildProcesses.ts b/src/domain/javascript/javascriptSemanticChildProcesses.ts index ea8c872e19..ea7fb69ab9 100644 --- a/src/domain/javascript/javascriptSemanticChildProcesses.ts +++ b/src/domain/javascript/javascriptSemanticChildProcesses.ts @@ -450,12 +450,13 @@ const stdioMode = ( }; /** - * Node's `fork` requires an explicit `stdio` array to carry the IPC channel. An - * array that provably omits `"ipc"` throws ERR_CHILD_PROCESS_IPC_REQUIRED - * synchronously, before any child process exists, so it is not a spawn at all. - * A string `stdio` is normalised with `"ipc"` appended, so only the array form - * can omit it; a spread or unreadable element may still supply the channel and - * is therefore left alone. + * Node's `fork` requires an explicit `stdio` array to carry exactly one IPC + * channel. An array that provably omits `"ipc"` throws + * ERR_CHILD_PROCESS_IPC_REQUIRED, and one that names `"ipc"` twice throws + * ERR_IPC_ONE_PIPE; both throws are synchronous, before any child process + * exists, so such a call is not a spawn at all. A string `stdio` is normalised + * with a single `"ipc"` appended, so only the array form can be wrong; a spread + * or unreadable element may still change the count and is therefore left alone. */ const forkRejectsStdioArray = ( candidate: SpawnCandidate, @@ -477,10 +478,11 @@ const forkRejectsStdioArray = ( ); if (literals.some((literal) => literal === undefined || !literal.found)) return false; - return !literals.some( + const ipcCount = literals.filter( (literal) => literal !== undefined && literal.found && literal.value === "ipc", - ); + ).length; + return ipcCount !== 1; }; /**