node:vm compatibility - #19703
node:vm compatibility#19703
Conversation
|
|
||
| static RefPtr<JSC::CachedBytecode> getBytecode(JSGlobalObject* globalObject, JSC::ProgramExecutable* executable, JSC::SourceCode source); | ||
| static JSC::EncodedJSValue createCachedData(JSGlobalObject* globalObject, JSC::SourceCode source); | ||
| bool extractCachedData(JSValue cachedDataValue, WTF::Vector<uint8_t>& outCachedData) |
There was a problem hiding this comment.
Can you add a TODO to investigate if we can avoid this clone?
| auto _ = ERR::INVALID_ARG_TYPE(scope, globalObject, "options"_s, "object"_s, optionsArg); | ||
| return false; | ||
| } | ||
| SourceCode sourceCode( |
There was a problem hiding this comment.
| SourceCode sourceCode( | |
| RETURN_IF_EXCEPTION(scope, nullptr); | |
| SourceCode sourceCode( |
| vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "isModuleNamespaceObject"_s)), | ||
| JSC::JSFunction::create(vm, globalObject, 0, "isModuleNamespaceObject"_s, vmIsModuleNamespaceObject, ImplementationVisibility::Public), 1); | ||
| obj->putDirect( | ||
| vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "kUnlinked"_s)), |
There was a problem hiding this comment.
Can we use a single const enum in the file for these values instead and then it'll participate in constant folding and avoid the dynamic property lookup?
| lexicallyScopedFeatures, JSParserScriptMode::Classic, SourceParseMode::ProgramMode, | ||
| FunctionMode::None, SuperBinding::NotNeeded, ConstructorKind::None, DerivedContextType::None, | ||
| isEvalNode, EvalContextType::None, nullptr); | ||
| if (JSValue lineOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "lineOffset"_s))) { |
There was a problem hiding this comment.
| if (JSValue lineOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "lineOffset"_s))) { | |
| RETURN_IF_EXCEPTION(scope, false); | |
| if (JSValue lineOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "lineOffset"_s))) { |
|
|
||
| program = parser.parse<ProgramNode>(error, name, ParsingContext::Normal); | ||
| } | ||
| if (JSValue columnOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "columnOffset"_s))) { |
There was a problem hiding this comment.
| if (JSValue columnOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "columnOffset"_s))) { | |
| RETURN_IF_EXCEPTION(scope, false); | |
| if (JSValue columnOffsetOpt = options->getIfPropertyExists(globalObject, Identifier::fromString(vm, "columnOffset"_s))) { |
| return NodeVMSourceTextModule::create(vm, globalObject, args); | ||
| } | ||
|
|
||
| if (disambiguator.inherits(JSArray::info())) { |
There was a problem hiding this comment.
| if (disambiguator.inherits(JSArray::info())) { | |
| if (JSC::isJSArray(disambiguator)) { |
|
|
||
| JSArray* array = constructEmptyArray(globalObject, nullptr, requests.size()); | ||
|
|
||
| for (unsigned i = 0; const NodeVMModuleRequest& request : requests) { |
There was a problem hiding this comment.
I suggest constructing the array at the end of this, appending to a MarkedArgumentsBuffer, checking for exceptions and then passing the MarkedArgumentsBuffer to constructEmptyArray.
| for (unsigned i = 0; const NodeVMModuleRequest& request : requests) { | |
| MarkedArgumentsBuffer array; | |
| for ( const NodeVMModuleRequest& request : requests) { | |
| array.append(request.toJS(globalObject)); | |
| RETURN_IF_EXCEPTION(scope, {}); | |
| } | |
| return JSValue::encode(constructEmptyArray(globalObject, array)); |
|
|
||
| JSC::LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? JSC::TaintedByWithScopeLexicallyScopedFeature : JSC::NoLexicallyScopedFeatures; | ||
| JSC::SourceCodeKey key(source, {}, JSC::SourceCodeType::ProgramType, lexicallyScopedFeatures, JSC::JSParserScriptMode::Classic, JSC::DerivedContextType::None, JSC::EvalContextType::None, false, {}, std::nullopt); | ||
| Ref<JSC::CachedBytecode> cachedBytecode = JSC::CachedBytecode::create(std::span(cachedData), nullptr, {}); |
There was a problem hiding this comment.
| Ref<JSC::CachedBytecode> cachedBytecode = JSC::CachedBytecode::create(std::span(cachedData), nullptr, {}); | |
| // TODO: check if this clones the cached data an extra time | |
| Ref<JSC::CachedBytecode> cachedBytecode = JSC::CachedBytecode::create(std::span(cachedData), nullptr, {}); |
| ASSERT(m_cachedBytecode); | ||
|
|
||
| std::span<const uint8_t> bytes = m_cachedBytecode->span(); | ||
| m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject(), bytes)); |
There was a problem hiding this comment.
this needs an exception scope technically
| m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject(), bytes)); | |
| m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject(), bytes)); |
| uint32_t lineOffset = lineOffsetValue.toUInt32(globalObject); | ||
| uint32_t columnOffset = columnOffsetValue.toUInt32(globalObject); | ||
|
|
||
| Ref<StringSourceProvider> sourceProvider = StringSourceProvider::create(sourceTextValue.toWTFString(globalObject), SourceOrigin {}, String {}, SourceTaintedOrigin::Untainted, |
There was a problem hiding this comment.
Does the SourceOrigin need to be set here too?
| Ref<StringSourceProvider> sourceProvider = StringSourceProvider::create(sourceTextValue.toWTFString(globalObject), SourceOrigin {}, String {}, SourceTaintedOrigin::Untainted, | |
| auto&& sourceCodeString = sourceTextValue.toWTFString(globalObject); | |
| RETURN_IF_EXCEPTION(scope, nullptr); | |
| Ref<StringSourceProvider> sourceProvider = StringSourceProvider::create(sourceCodeString, SourceOrigin {}, String {}, SourceTaintedOrigin::Untainted, |
| return constructEmptyArray(globalObject, nullptr, 0); | ||
| } | ||
|
|
||
| JSArray* requestsArray = constructEmptyArray(globalObject, nullptr, requests.size()); |
There was a problem hiding this comment.
| JSArray* requestsArray = constructEmptyArray(globalObject, nullptr, requests.size()); | |
| MarkedArgumentsBuffer requestsArray; |
|
|
||
| requestObject->putDirect(vm, attributesIdentifier, attributesObject); | ||
| addModuleRequest({ WTF::String(*request.m_specifier), WTFMove(attributeMap) }); | ||
| requestsArray->putDirectIndex(globalObject, i, requestObject); |
There was a problem hiding this comment.
| requestsArray->putDirectIndex(globalObject, i, requestObject); | |
| requestsArray.append(requestObject); |
| requestsArray->putDirectIndex(globalObject, i, requestObject); | ||
| } | ||
|
|
||
| m_moduleRequestsArray.set(vm, this, requestsArray); |
There was a problem hiding this comment.
| m_moduleRequestsArray.set(vm, this, requestsArray); | |
| auto* jsArray = JSC::constructArray(globalObject, requestsArray)) | |
| m_moduleRequestsArray.set(vm, this, jsArray); | |
| return jsArray; |
| if (!m_cachedBytecodeBuffer) { | ||
| RefPtr<CachedBytecode> cachedBytecode = bytecode(globalObject); | ||
| std::span<const uint8_t> bytes = cachedBytecode->span(); | ||
| m_cachedBytecodeBuffer.set(vm(), this, WebCore::createBuffer(globalObject, bytes)); |
There was a problem hiding this comment.
technically needs an exception check if bytes > 4.7 GB
| WriteBarrier<ModuleProgramExecutable> m_cachedExecutable; | ||
| WriteBarrier<JSUint8Array> m_cachedBytecodeBuffer; | ||
| WriteBarrier<Exception> m_evaluationException; | ||
| RefPtr<CachedBytecode> m_bytecode; |
There was a problem hiding this comment.
| RefPtr<CachedBytecode> m_bytecode; | |
| // TODO: investigate if we're cloning the source code and the bytecode multiple times and figure out how to not do that | |
| RefPtr<CachedBytecode> m_bytecode; |
| std::atomic_bool m_installed = false; | ||
| std::atomic_flag m_waiting {}; | ||
| Semaphore m_semaphore; | ||
| std::mutex m_globalObjectsMutex; |
There was a problem hiding this comment.
| std::mutex m_globalObjectsMutex; | |
| WTF::Lock m_globalObjectsMutex; |
|
|
||
| // Iterates the module requests and links with the linker. | ||
| // Specifiers should be aligned with the moduleRequests array in order. | ||
| const specifiers = Array(moduleRequests.length); |
There was a problem hiding this comment.
| const specifiers = Array(moduleRequests.length); | |
| const moduleRequestsCount = moduleRequests.length; | |
| const specifiers = Array(moduleRequestsCount); |
| const specifiers = Array(moduleRequests.length); | ||
| const modulePromises = Array(moduleRequests.length); | ||
| // Iterates with index to avoid calling into userspace with `Symbol.iterator`. | ||
| for (let idx = 0; idx < moduleRequests.length; idx++) { |
There was a problem hiding this comment.
| for (let idx = 0; idx < moduleRequests.length; idx++) { | |
| for (let idx = 0; idx < moduleRequestsCount; idx++) { |
| // Iterates the module requests and links with the linker. | ||
| // Specifiers should be aligned with the moduleRequests array in order. | ||
| const specifiers = Array(moduleRequests.length); | ||
| const modulePromises = Array(moduleRequests.length); |
There was a problem hiding this comment.
| const modulePromises = Array(moduleRequests.length); | |
| const modulePromises = Array(moduleRequestsCount); |
Jarred-Sumner
left a comment
There was a problem hiding this comment.
A few small nitpicky comments but very close!
…d::mutex and std::unique_lock
…itions (#38228) ### Problem - `new vm.Script('1', { lineOffset: 2147483647 })` and `vm.compileFunction('return 1', [], { lineOffset: 2147483647 })` abort on assertion-enabled builds with `ASSERTION FAILED: line >= 0` in `JSC::JSTextPosition::checkConsistency()` (ParserTokens.h:231). Release builds survive and print wrapped line numbers (`big.js:-2147483648`). - `lineOffset: 2147483646` plus a two-line source, and `new vm.SourceTextModule('1;\n2;', { lineOffset: 2147483647 })`, abort the same way. - Node's validator accepts any int32 for `lineOffset` / `columnOffset`, and the bindings hand the value to JSC unchanged (`NodeVMScript.cpp` `makeSource(...)`, `NodeVM.cpp` `constructAnonymousFunction`, `NodeVMSourceTextModule.cpp` `create`). JSC stores positions as `int`: `OrdinalNumber::oneBasedInt()` is `value + 1`, and the lexer does `++m_lineNumber` per line terminator starting from that (`Lexer.cpp` `setCode` / `shiftLineTerminator`), so any offset within (source line count + 1) of `INT32_MAX` overflows. The `+ 1` overflow is undefined behaviour, which is why a debug build and the optimized asan build fail on different inputs. ### Fix - Adds `clampOffsetForSource(offset, sourceLength)` (`NodeVM.h` / `NodeVM.cpp`) and applies it to both offsets in `vm.Script`, `vm.compileFunction` and `SourceTextModule` before the `SourceCode` is built: the offset is capped at `INT32_MAX - 1 - sourceLength`. - Correct because a source cannot hold more line terminators, or a longer first line, than it has code units, so every position JSC derives (`+1` for one-based, per-line increments, first-line column additions, and `decorateParseErrorStack`'s own `line + offset`) stays within `int`. For any realistic offset the clamp is a no-op; an absurd one now reports positions just below `INT32_MAX` instead of undefined behaviour (e.g. `big.js:2147483627` for the 20-character repro). - `compileFunction` now builds its wrapper program before the standalone parse of the body and clamps against the wrapper's length, since that is the longest text it parses (the body is re-parsed as line 2 of it). The early `RETURN_IF_EXCEPTION` after building the wrapper replaces continuing with a null string. - The options struct itself is clamped, so `decorateParseErrorStack` and the provider's start position see the same value as the parser. `SourceTextModule` still passes its (zero-based) values to `SourceCode` exactly as before; the off-by-one there is fixed separately in #38235, which touches the same lines. Whichever of the two lands second only has to wrap that PR's `TextPosition` operands in `clampOffsetForSource`; the module test case here uses `INT32_MAX - 1` with a three-line source so it fails without the clamp under either line-numbering. - Removes the native `runInNewContext` / `runInThisContext` host functions from `NodeVM.cpp` (second commit), plus the `BaseVMOptions` constructor only they used. `vm.ts` has built both APIs on `Script` since #19703, so they were unreachable, and they were the only remaining places that built a `SourceCode` from unclamped offsets. - Verified with `test/js/node/vm/vm.test.ts` (`describe("node:vm lineOffset/columnOffset at the edge of int32")`): 11 subprocess cases covering construction of all three APIs and the reported line/column of runtime and compile-time errors. Without the `src/` change 9 of the 11 fail on the debug build (7 abort with the assertion above; the line/column cases otherwise report `1` / `16`, i.e. the offset silently lost); of the remaining 2, the one-line `Script` case (the reported repro) only fails on optimized assertion builds, and the `columnOffset` construction case never aborted (the column value itself is checked separately and did fail). With the change all 11 pass. - Also ran the rest of `vm.test.ts` (225 pass), `vm-sourceUrl.test.ts`, and all 95 `test/js/node/test/parallel/test-vm-*.js` files (covers `runInNewContext` / `runInThisContext` after the removal); normal offsets (`lineOffset: 5`, negative offsets, `columnOffset: 10`) report the same lines and columns as before. ### Background - `lineOffset` / `columnOffset` tell `node:vm` where a snippet sits inside a larger file so error positions line up with that file; they are zero-based and may be negative. Node validates them as int32 and otherwise passes them through, so `2147483647` is a legal input. - `OrdinalNumber` (WTF) wraps an `int` that can be read as zero- or one-based; `TextPosition` is a line/column pair of them. A JSC `SourceCode` carries the one-based first line and column to start counting from, and the lexer counts lines upward from there in a plain `int`. - `JSTextPosition::checkConsistency()` is a debug-only assertion that every token position the lexer produces has a non-negative line; it is what turns the overflow into an abort on assertion builds. - `vm.compileFunction` is implemented by wrapping the body in `(function (params) {\n<body>\n})` and compiling that as a program; the user's body is therefore line 2 of what JSC actually parses. <!-- robobun:evidence:begin --> --- **no test proof** · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/vm/vm.test.ts <!-- robobun:evidence:end -->
What does this PR do?
Adds support for vm.SourceTextModule and some other features in node:vm.
How did you verify your code works?
Added Node.js tests