From 4f94d44aaa6c594485ff25546e87cbdcc72edcbd Mon Sep 17 00:00:00 2001 From: Ayush Anand Date: Wed, 11 Nov 2026 03:05:27 +0000 Subject: [PATCH] vm: fix compileFunction abort on invalid arrays Reject null entries in options.contextExtensions and read params and contextExtensions only once before passing them to the binding. Fixes: https://github.com/nodejs/node/issues/67366 Signed-off-by: Ayush Anand --- lib/vm.js | 19 ++++- .../test-vm-compile-function-validation.js | 84 +++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 test/parallel/test-vm-compile-function-validation.js diff --git a/lib/vm.js b/lib/vm.js index 2c7446af6076..2fec1c444089 100644 --- a/lib/vm.js +++ b/lib/vm.js @@ -23,6 +23,7 @@ const { ArrayPrototypeForEach, + ArrayPrototypePush, ObjectFreeze, ObjectPrototype, PromiseReject, @@ -51,7 +52,6 @@ const { validateStringArray, validateUint32, kValidateObjectAllowArray, - kValidateObjectAllowNullable, } = require('internal/validators'); const { emitExperimentalWarning, @@ -318,10 +318,20 @@ function runInThisContext(code, options) { return createScript(code, options).runInThisContext(options); } +function copyArray(array) { + const copy = []; + for (let i = 0; i < array.length; ++i) { + ArrayPrototypePush(copy, array[i]); + } + return copy; +} + function compileFunction(code, params, options = kEmptyObject) { validateString(code, 'code'); validateObject(options, 'options'); if (params !== undefined) { + validateArray(params, 'params'); + params = copyArray(params); validateStringArray(params, 'params'); } const { @@ -355,9 +365,10 @@ function compileFunction(code, params, options = kEmptyObject) { } } validateArray(contextExtensions, 'options.contextExtensions'); - ArrayPrototypeForEach(contextExtensions, (extension, i) => { + const extensions = copyArray(contextExtensions); + ArrayPrototypeForEach(extensions, (extension, i) => { const name = `options.contextExtensions[${i}]`; - validateObject(extension, name, kValidateObjectAllowNullable); + validateObject(extension, name); }); const hostDefinedOptionId = @@ -365,7 +376,7 @@ function compileFunction(code, params, options = kEmptyObject) { return internalCompileFunction( code, filename, lineOffset, columnOffset, - cachedData, produceCachedData, parsingContext, contextExtensions, + cachedData, produceCachedData, parsingContext, extensions, params, hostDefinedOptionId, importModuleDynamically, ).function; } diff --git a/test/parallel/test-vm-compile-function-validation.js b/test/parallel/test-vm-compile-function-validation.js new file mode 100644 index 000000000000..a61e75ad3564 --- /dev/null +++ b/test/parallel/test-vm-compile-function-validation.js @@ -0,0 +1,84 @@ +'use strict'; + +// Refs: https://github.com/nodejs/node/issues/67366 + +const common = require('../common'); +const assert = require('assert'); +const vm = require('vm'); + +assert.throws(() => { + vm.compileFunction('return 1', [], { contextExtensions: [null] }); +}, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.contextExtensions[0]" property must be of type ' + + 'object. Received null', +}); + +assert.throws(() => { + vm.compileFunction('return 1', [], { contextExtensions: [{}, null] }); +}, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "options.contextExtensions[1]" property must be of type ' + + 'object. Received null', +}); + +{ + let reads = 0; + const params = []; + Object.defineProperty(params, 0, { + __proto__: null, + enumerable: true, + configurable: true, + get: common.mustCall(() => (++reads === 1 ? 'x' : 42)), + }); + + const fn = vm.compileFunction('return x', params); + assert.strictEqual(fn(5), 5); +} + +{ + let reads = 0; + const contextExtensions = []; + Object.defineProperty(contextExtensions, 0, { + __proto__: null, + enumerable: true, + configurable: true, + get: common.mustCall(() => (++reads === 1 ? { a: 5 } : null)), + }); + + const fn = vm.compileFunction('return a', [], { contextExtensions }); + assert.strictEqual(fn(), 5); +} + +{ + const params = []; + Object.defineProperty(params, 0, { + __proto__: null, + enumerable: true, + configurable: true, + get: common.mustCall(() => 42), + }); + + assert.throws(() => vm.compileFunction('return 1', params), { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + message: 'The "params[0]" argument must be of type string. ' + + 'Received type number (42)', + }); +} + +{ + let reads = 0; + const params = new Proxy(['x'], { + get(target, key, receiver) { + if (key === '0') return ++reads === 1 ? 'x' : 42; + return Reflect.get(target, key, receiver); + }, + }); + + const fn = vm.compileFunction('return x', params); + assert.strictEqual(fn(7), 7); + assert.strictEqual(reads, 1); +}