From 2f715eefdc93cec534b474f1bbf33930a478571a Mon Sep 17 00:00:00 2001 From: dhruv7539 Date: Tue, 24 Feb 2026 17:07:00 -0800 Subject: [PATCH 1/4] src: validate stdio entries in process_wrap --- src/process_wrap.cc | 6 ++++ ...st-child-process-array-prototype-setter.js | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 test/parallel/test-child-process-array-prototype-setter.js diff --git a/src/process_wrap.cc b/src/process_wrap.cc index d27ca7da7b587b..c4e5911689f019 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -140,6 +140,12 @@ class ProcessWrap : public HandleWrap { if (!stdios->Get(context, i).ToLocal(&val)) { return Nothing(); } + if (!val->IsObject()) { + THROW_ERR_INVALID_ARG_TYPE(env, + "options.stdio[%u] must be an object", + i); + return Nothing(); + } Local stdio = val.As(); Local type; if (!stdio->Get(context, env->type_string()).ToLocal(&type)) { diff --git a/test/parallel/test-child-process-array-prototype-setter.js b/test/parallel/test-child-process-array-prototype-setter.js new file mode 100644 index 00000000000000..043869a8700d5f --- /dev/null +++ b/test/parallel/test-child-process-array-prototype-setter.js @@ -0,0 +1,29 @@ +'use strict'; +require('../common'); + +const assert = require('assert'); +const { spawnSyncAndAssert } = require('../common/child_process'); + +const script = ` +Object.defineProperty(Array.prototype, '2', { + __proto__: null, + set() {}, + configurable: true, +}); + +try { + require('child_process').spawn(process.execPath, ['-e', '0']); + console.log('NO_ERROR'); +} catch (error) { + console.log(error.code); +} +`; + +spawnSyncAndAssert(process.execPath, ['-e', script], { + stdout: (output) => { + assert.match(output, /^ERR_INVALID_ARG_TYPE\\r?\\n$/); + }, + stderr: (output) => { + assert.doesNotMatch(output, /FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal/); + }, +}); From 9aa1a1dcefa0aca48c2afd2e50811e0513726cd4 Mon Sep 17 00:00:00 2001 From: dhruv7539 Date: Wed, 25 Feb 2026 10:19:47 -0800 Subject: [PATCH 2/4] test: fix regex escaping in stdio setter test --- test/parallel/test-child-process-array-prototype-setter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-child-process-array-prototype-setter.js b/test/parallel/test-child-process-array-prototype-setter.js index 043869a8700d5f..c8250d9c056311 100644 --- a/test/parallel/test-child-process-array-prototype-setter.js +++ b/test/parallel/test-child-process-array-prototype-setter.js @@ -21,7 +21,7 @@ try { spawnSyncAndAssert(process.execPath, ['-e', script], { stdout: (output) => { - assert.match(output, /^ERR_INVALID_ARG_TYPE\\r?\\n$/); + assert.match(output, /^ERR_INVALID_ARG_TYPE\r?\n$/); }, stderr: (output) => { assert.doesNotMatch(output, /FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal/); From bdb66fbda68ae8250c06e3e8a0b3ede3d5e3cdd0 Mon Sep 17 00:00:00 2001 From: dhruv7539 Date: Wed, 25 Feb 2026 14:05:10 -0800 Subject: [PATCH 3/4] src: apply clang-format in process_wrap --- src/process_wrap.cc | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/process_wrap.cc b/src/process_wrap.cc index c4e5911689f019..e11d53f47ce4e3 100644 --- a/src/process_wrap.cc +++ b/src/process_wrap.cc @@ -141,9 +141,8 @@ class ProcessWrap : public HandleWrap { return Nothing(); } if (!val->IsObject()) { - THROW_ERR_INVALID_ARG_TYPE(env, - "options.stdio[%u] must be an object", - i); + THROW_ERR_INVALID_ARG_TYPE( + env, "options.stdio[%u] must be an object", i); return Nothing(); } Local stdio = val.As(); From da38e2ec670a60682665ad1b5415647e247a14b3 Mon Sep 17 00:00:00 2001 From: dhruv7539 Date: Mon, 2 Mar 2026 16:53:21 -0800 Subject: [PATCH 4/4] lib: handle sparse stdio arrays with prototype setters --- lib/internal/child_process.js | 97 +++++++++++-------- ...st-child-process-array-prototype-setter.js | 2 +- 2 files changed, 57 insertions(+), 42 deletions(-) diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index 45ae95614a88b5..efbd8ccaa68f79 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -3,10 +3,10 @@ const { ArrayIsArray, ArrayPrototypePush, - ArrayPrototypeReduce, ArrayPrototypeSlice, FunctionPrototype, FunctionPrototypeCall, + ObjectDefineProperty, ObjectSetPrototypeOf, ReflectApply, StringPrototypeSlice, @@ -975,40 +975,51 @@ function getValidStdio(stdio, sync) { throw new ERR_INVALID_ARG_VALUE('stdio', stdio); } - // At least 3 stdio will be created - // Don't concat() a new Array() because it would be sparse, and - // stdio.reduce() would skip the sparse elements of stdio. - // See https://stackoverflow.com/a/5501711/3561 + // At least 3 stdio will be created. while (stdio.length < 3) ArrayPrototypePush(stdio, undefined); // Translate stdio into C++-readable form // (i.e. PipeWraps or fds) - stdio = ArrayPrototypeReduce(stdio, (acc, stdio, i) => { - function cleanup() { - for (let i = 0; i < acc.length; i++) { - if ((acc[i].type === 'pipe' || acc[i].type === 'ipc') && acc[i].handle) - acc[i].handle.close(); - } + const acc = []; + + function cleanup() { + for (let i = 0; i < acc.length; i++) { + if ((acc[i].type === 'pipe' || acc[i].type === 'ipc') && acc[i].handle) + acc[i].handle.close(); } + } + + function pushAcc(entry) { + ObjectDefineProperty(acc, acc.length, { + __proto__: null, + value: entry, + configurable: true, + enumerable: true, + writable: true, + }); + } + + for (let i = 0; i < stdio.length; i++) { + let stdioValue = stdio[i]; // Defaults - stdio ??= i < 3 ? 'pipe' : 'ignore'; - - if (stdio === 'ignore') { - ArrayPrototypePush(acc, { type: 'ignore' }); - } else if (stdio === 'pipe' || stdio === 'overlapped' || - (typeof stdio === 'number' && stdio < 0)) { - const a = { - type: stdio === 'overlapped' ? 'overlapped' : 'pipe', + stdioValue ??= i < 3 ? 'pipe' : 'ignore'; + + if (stdioValue === 'ignore') { + pushAcc({ type: 'ignore' }); + } else if (stdioValue === 'pipe' || stdioValue === 'overlapped' || + (typeof stdioValue === 'number' && stdioValue < 0)) { + const entry = { + type: stdioValue === 'overlapped' ? 'overlapped' : 'pipe', readable: i === 0, writable: i !== 0, }; if (!sync) - a.handle = new Pipe(PipeConstants.SOCKET); + entry.handle = new Pipe(PipeConstants.SOCKET); - ArrayPrototypePush(acc, a); - } else if (stdio === 'ipc') { + pushAcc(entry); + } else if (stdioValue === 'ipc') { if (sync || ipc !== undefined) { // Cleanup previously created pipes cleanup(); @@ -1021,46 +1032,50 @@ function getValidStdio(stdio, sync) { ipc = new Pipe(PipeConstants.IPC); ipcFd = i; - ArrayPrototypePush(acc, { + pushAcc({ type: 'pipe', handle: ipc, ipc: true, }); - } else if (stdio === 'inherit') { - ArrayPrototypePush(acc, { + } else if (stdioValue === 'inherit') { + pushAcc({ type: 'inherit', fd: i, }); - } else if (typeof stdio === 'number' || typeof stdio.fd === 'number') { - ArrayPrototypePush(acc, { + } else if (typeof stdioValue === 'number' || + typeof stdioValue.fd === 'number') { + pushAcc({ type: 'fd', - fd: typeof stdio === 'number' ? stdio : stdio.fd, + fd: typeof stdioValue === 'number' ? stdioValue : stdioValue.fd, }); - } else if (getHandleWrapType(stdio) || getHandleWrapType(stdio.handle) || - getHandleWrapType(stdio._handle)) { - const handle = getHandleWrapType(stdio) ? - stdio : - getHandleWrapType(stdio.handle) ? stdio.handle : stdio._handle; - - ArrayPrototypePush(acc, { + } else if (getHandleWrapType(stdioValue) || + getHandleWrapType(stdioValue.handle) || + getHandleWrapType(stdioValue._handle)) { + const handle = getHandleWrapType(stdioValue) ? + stdioValue : + getHandleWrapType(stdioValue.handle) ? + stdioValue.handle : + stdioValue._handle; + + pushAcc({ type: 'wrap', wrapType: getHandleWrapType(handle), handle: handle, - _stdio: stdio, + _stdio: stdioValue, }); - } else if (isArrayBufferView(stdio) || typeof stdio === 'string') { + } else if (isArrayBufferView(stdioValue) || typeof stdioValue === 'string') { if (!sync) { cleanup(); - throw new ERR_INVALID_SYNC_FORK_INPUT(inspect(stdio)); + throw new ERR_INVALID_SYNC_FORK_INPUT(inspect(stdioValue)); } } else { // Cleanup cleanup(); - throw new ERR_INVALID_ARG_VALUE('stdio', stdio); + throw new ERR_INVALID_ARG_VALUE('stdio', stdioValue); } + } - return acc; - }, []); + stdio = acc; return { stdio, ipc, ipcFd }; } diff --git a/test/parallel/test-child-process-array-prototype-setter.js b/test/parallel/test-child-process-array-prototype-setter.js index c8250d9c056311..0f752731e96d87 100644 --- a/test/parallel/test-child-process-array-prototype-setter.js +++ b/test/parallel/test-child-process-array-prototype-setter.js @@ -21,7 +21,7 @@ try { spawnSyncAndAssert(process.execPath, ['-e', script], { stdout: (output) => { - assert.match(output, /^ERR_INVALID_ARG_TYPE\r?\n$/); + assert.match(output, /^NO_ERROR\r?\n$/); }, stderr: (output) => { assert.doesNotMatch(output, /FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal/);