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
This commit is contained in:
吴佳浩Alben authored and GitHub committed 2026-11-04 19:11:35 +08:00
1 parent f8ec48095a
commit 007e181f68
2 files changed
+22 -8

No files matched your search

@@ -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", {})`],
@@ -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;
};
/**