diff --git a/src/jsc/bindings/NodeVM.cpp b/src/jsc/bindings/NodeVM.cpp index 54a3d25d2947..fbb09addb190 100644 --- a/src/jsc/bindings/NodeVM.cpp +++ b/src/jsc/bindings/NodeVM.cpp @@ -721,7 +721,7 @@ void getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::Thr auto allowStringsValue = codeGenerationObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "strings"_s)); RETURN_IF_EXCEPTION(scope, ); - if (allowStringsValue) { + if (allowStringsValue && !allowStringsValue.isUndefined()) { if (!allowStringsValue.isBoolean()) { ERR::INVALID_ARG_TYPE(scope, globalObject, WTF::makeString("options."_s, codeGenerationKey, ".strings"_s), "boolean"_s, allowStringsValue); return; @@ -733,7 +733,7 @@ void getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::Thr auto allowWasmValue = codeGenerationObject->getIfPropertyExists(globalObject, Identifier::fromString(vm, "wasm"_s)); RETURN_IF_EXCEPTION(scope, ); - if (allowWasmValue) { + if (allowWasmValue && !allowWasmValue.isUndefined()) { if (!allowWasmValue.isBoolean()) { ERR::INVALID_ARG_TYPE(scope, globalObject, WTF::makeString("options."_s, codeGenerationKey, ".wasm"_s), "boolean"_s, allowWasmValue); return; diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index d3d04239d179..cccc84eff7e0 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -1010,6 +1010,48 @@ describe("codeGeneration options", () => { const evalResult = runInContext("eval('5 + 5');", context); expect(evalResult).toBe(10); }); + + describe("strings/wasm set to undefined", () => { + // In Node an undefined strings/wasm is the same as an omitted one: + // createContext() destructures them with `= true` defaults, and + // getContextOptions() only validates them when `!== undefined` (it then + // hands createContext() a `{ strings, wasm }` object with undefined holes). + const WASM_BYTES = new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]); + // Evaluated inside the context: reports which kinds of code generation it allows. + const probe = ` + let strings = true, wasm = true; + try { eval("1"); } catch { strings = false; } + try { new WebAssembly.Module(WASM_BYTES); } catch { wasm = false; } + "strings=" + strings + " wasm=" + wasm; + `; + + // createContext reads options.codeGeneration; vm.runInNewContext and + // Script#runInNewContext read options.contextCodeGeneration. + const entryPoints: Record string> = { + "createContext()": codeGeneration => runInContext(probe, createContext({ WASM_BYTES }, { codeGeneration })), + "runInNewContext()": contextCodeGeneration => runInNewContext(probe, { WASM_BYTES }, { contextCodeGeneration }), + "Script#runInNewContext()": contextCodeGeneration => + new Script(probe).runInNewContext({ WASM_BYTES }, { contextCodeGeneration }), + }; + + describe.each(Object.entries(entryPoints))("%s", (_, run) => { + test("undefined is accepted and means the default (allowed)", () => { + expect(run({ strings: undefined })).toBe("strings=true wasm=true"); + expect(run({ wasm: undefined })).toBe("strings=true wasm=true"); + expect(run({ strings: undefined, wasm: undefined })).toBe("strings=true wasm=true"); + }); + + test("undefined next to an explicit false leaves the false in effect", () => { + expect(run({ strings: false, wasm: undefined })).toBe("strings=false wasm=true"); + expect(run({ strings: undefined, wasm: false })).toBe("strings=true wasm=false"); + }); + + test.each([null, 0, 1, "false"])("%p is still rejected", value => { + expect(() => run({ strings: value })).toThrowWithCode(TypeError, "ERR_INVALID_ARG_TYPE"); + expect(() => run({ wasm: value })).toThrowWithCode(TypeError, "ERR_INVALID_ARG_TYPE"); + }); + }); + }); }); describe("context options with throwing getters", () => {