From 3db987d470e3d2a07f170b4271b66daf4309c8c5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:04:22 +0000 Subject: [PATCH 1/5] node:vm: keep lineOffset/columnOffset from overflowing JSC's parser positions Node's validator accepts any int32 for these options, but JSC stores positions as ints, converts the start position to one-based and counts the source's own lines on top of it. An offset near INT32_MAX therefore overflowed in the lexer (ASSERTION FAILED: line >= 0 in JSTextPosition::checkConsistency on assertion builds, wrapped negative line numbers on release builds). Clamp both offsets in vm.Script, vm.compileFunction and SourceTextModule so that offset + 1 + source length still fits in an int; a source cannot contain more line terminators or first-line columns than it has code units, so this is a no-op for every realistic value. compileFunction builds its wrapper program first and clamps against that, since the wrapper is the longest text it parses. --- src/jsc/bindings/NodeVM.cpp | 26 +++++-- src/jsc/bindings/NodeVM.h | 6 ++ src/jsc/bindings/NodeVMScript.cpp | 2 + src/jsc/bindings/NodeVMSourceTextModule.cpp | 10 ++- test/js/node/vm/vm.test.ts | 79 +++++++++++++++++++++ 5 files changed, 114 insertions(+), 9 deletions(-) diff --git a/src/jsc/bindings/NodeVM.cpp b/src/jsc/bindings/NodeVM.cpp index 976c08c36479..d6595c4fef11 100644 --- a/src/jsc/bindings/NodeVM.cpp +++ b/src/jsc/bindings/NodeVM.cpp @@ -130,6 +130,17 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c VM& vm = globalObject->vm(); auto throwScope = DECLARE_THROW_SCOPE(vm); + // wrap the arguments in an anonymous function expression + int startOffset = 0; + String program = stringifyAnonymousFunction(globalObject, args, throwScope, &startOffset); + EXCEPTION_ASSERT(!!throwScope.exception() == program.isNull()); + RETURN_IF_EXCEPTION(throwScope, nullptr); + + // The wrapped program is the longest text parsed here, so bounding the + // offsets against it also covers the standalone parse of the body below. + options.lineOffset = clampOffsetForSource(options.lineOffset, program.length()); + options.columnOffset = clampOffsetForSource(options.columnOffset, program.length()); + TextPosition position(options.lineOffset, options.columnOffset); LexicallyScopedFeatures lexicallyScopedFeatures = globalObject->globalScopeExtension() ? TaintedByWithScopeLexicallyScopedFeature : NoLexicallyScopedFeatures; @@ -180,11 +191,6 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c } } - // wrap the arguments in an anonymous function expression - int startOffset = 0; - String code = stringifyAnonymousFunction(globalObject, args, throwScope, &startOffset); - EXCEPTION_ASSERT(!!throwScope.exception() == code.isNull()); - // The user's body starts on line 2 of the wrapped program (after the // "(function () {\n" prefix). Shift the provider's start position up one // line so reported positions line up with the body the way V8's @@ -196,7 +202,7 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c TextPosition wrappedPosition(OrdinalNumber::fromZeroBasedInt(lineZeroBased > 0 ? lineZeroBased - 1 : lineZeroBased), position.m_column); SourceCode sourceCode( - JSC::StringSourceProvider::create(code, sourceOrigin, WTF::move(options.filename), sourceTaintOrigin, wrappedPosition, SourceProviderSourceType::Program), + JSC::StringSourceProvider::create(program, sourceOrigin, WTF::move(options.filename), sourceTaintOrigin, wrappedPosition, SourceProviderSourceType::Program), wrappedPosition.m_line.oneBasedInt(), wrappedPosition.m_column.oneBasedInt()); CodeCache* cache = vm.codeCache(); @@ -638,6 +644,14 @@ void decorateParseErrorStack(JSGlobalObject* globalObject, VM& vm, JSObject* err writeArrowHeaderStack(vm, errorInstance, url, reportedLine, sourceLineText, caretColumn, stack); } +OrdinalNumber clampOffsetForSource(OrdinalNumber offset, unsigned sourceLength) +{ + int64_t maxOffset = std::max(static_cast(std::numeric_limits::max()) - 1 - sourceLength, 0); + if (offset.zeroBasedInt() <= maxOffset) + return offset; + return OrdinalNumber::fromZeroBasedInt(static_cast(maxOffset)); +} + void getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey, JSValue* importer) { if (importer) { diff --git a/src/jsc/bindings/NodeVM.h b/src/jsc/bindings/NodeVM.h index 7796b7941aa2..4925f51bb620 100644 --- a/src/jsc/bindings/NodeVM.h +++ b/src/jsc/bindings/NodeVM.h @@ -33,6 +33,12 @@ bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr // when no filename was provided; compileFunction has no such default. void decorateParseErrorStack(JSGlobalObject* globalObject, VM& vm, JSObject* error, StringView sourceString, const String& url, const JSC::ParserError& parseError, OrdinalNumber lineOffset); +// Node accepts any int32 lineOffset/columnOffset, but JSC keeps positions in +// ints and adds one (one-based) plus the source's own line terminators or +// first-line columns on top of the offset, so values near INT_MAX overflow in the +// parser. Neither count can exceed the source's length in code units, so an +// offset of at most INT_MAX - 1 - sourceLength keeps every derived position in range. +OrdinalNumber clampOffsetForSource(OrdinalNumber offset, unsigned sourceLength); void getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey, JSValue* importer); NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSValue contextValue, bool canThrow); JSC::EncodedJSValue INVALID_ARG_VALUE_VM_VARIATION(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value); diff --git a/src/jsc/bindings/NodeVMScript.cpp b/src/jsc/bindings/NodeVMScript.cpp index 9d8dc9de7954..c03eb3fb9bcc 100644 --- a/src/jsc/bindings/NodeVMScript.cpp +++ b/src/jsc/bindings/NodeVMScript.cpp @@ -117,6 +117,8 @@ constructScript(JSGlobalObject* globalObject, CallFrame* callFrame, JSValue newT } else if (!options.fromJS(globalObject, vm, scope, optionsArg, &importer)) { RETURN_IF_EXCEPTION(scope, JSValue::encode(jsUndefined())); } + options.lineOffset = clampOffsetForSource(options.lineOffset, sourceString.length()); + options.columnOffset = clampOffsetForSource(options.columnOffset, sourceString.length()); auto* zigGlobalObject = defaultGlobalObject(globalObject); Structure* structure = zigGlobalObject->NodeVMScriptStructure(); diff --git a/src/jsc/bindings/NodeVMSourceTextModule.cpp b/src/jsc/bindings/NodeVMSourceTextModule.cpp index 2076d21145d8..cae93410ed9a 100644 --- a/src/jsc/bindings/NodeVMSourceTextModule.cpp +++ b/src/jsc/bindings/NodeVMSourceTextModule.cpp @@ -96,10 +96,14 @@ NodeVMSourceTextModule* NodeVMSourceTextModule::create(VM& vm, JSGlobalObject* g WTF::String sourceText = sourceTextValue.toWTFString(globalObject); RETURN_IF_EXCEPTION(scope, nullptr); - Ref sourceProvider = StringSourceProvider::create(WTF::move(sourceText), sourceOrigin, String {}, SourceTaintedOrigin::Untainted, - TextPosition { OrdinalNumber::fromZeroBasedInt(lineOffset), OrdinalNumber::fromZeroBasedInt(columnOffset) }, SourceProviderSourceType::Module); + TextPosition startPosition { + clampOffsetForSource(OrdinalNumber::fromZeroBasedInt(lineOffset), sourceText.length()), + clampOffsetForSource(OrdinalNumber::fromZeroBasedInt(columnOffset), sourceText.length()), + }; - SourceCode sourceCode(WTF::move(sourceProvider), lineOffset, columnOffset); + Ref sourceProvider = StringSourceProvider::create(WTF::move(sourceText), sourceOrigin, String {}, SourceTaintedOrigin::Untainted, startPosition, SourceProviderSourceType::Module); + + SourceCode sourceCode(WTF::move(sourceProvider), startPosition.m_line.zeroBasedInt(), startPosition.m_column.zeroBasedInt()); auto* zigGlobalObject = defaultGlobalObject(globalObject); WTF::String identifier = identifierValue.toWTFString(globalObject); diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index 1676eab88caf..dc46079dbc30 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -1735,3 +1735,82 @@ test.concurrent("timeout during a nested event-loop wait beneath the script", as expect(stdout).toBe("ERR_SCRIPT_EXECUTION_TIMEOUT\n"); expect(exitCode).toBe(0); }); + +describe("node:vm lineOffset/columnOffset at the edge of int32", () => { + // Node's validator accepts any int32 here. JSC stores positions as ints, + // converts the offset to one-based and counts the source's own lines on top + // of it, so an offset this large used to overflow in the parser: assertion + // builds abort in JSTextPosition::checkConsistency ("line >= 0"), release + // builds report wrapped negative line numbers. Each case gets its own + // process because the failure mode is an abort. + const INT32_MAX = 2147483647; + + async function runFixture(body: string) { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", `const vm = require("node:vm");\n${body}`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + return stdout; + } + + test.concurrent.each([ + ["new Script, one-line source", `new vm.Script("1", { lineOffset: ${INT32_MAX} })`], + [ + "new Script, second line steps past INT32_MAX", + `new vm.Script(${JSON.stringify("1;\n2;")}, { lineOffset: ${INT32_MAX - 1} })`, + ], + ["new Script, columnOffset", `new vm.Script(${JSON.stringify("1;\n2;")}, { columnOffset: ${INT32_MAX} })`], + ["compileFunction", `vm.compileFunction("return 1", [], { lineOffset: ${INT32_MAX} })`], + [ + "compileFunction with params and both offsets", + `vm.compileFunction("return a", ["a"], { lineOffset: ${INT32_MAX}, columnOffset: ${INT32_MAX} })`, + ], + ["SourceTextModule", `new vm.SourceTextModule(${JSON.stringify("1;\n2;")}, { lineOffset: ${INT32_MAX} })`], + ])("%s compiles", async (_, expression) => { + const stdout = await runFixture(`${expression};\nconsole.log("ok");`); + expect(stdout).toBe("ok\n"); + }); + + test.concurrent.each([ + [ + "line of a runtime error thrown by a Script", + `new vm.Script(${JSON.stringify('1;\nthrow new Error("q")')}, { filename: "big.js", lineOffset: ${INT32_MAX - 1} }).runInThisContext()`, + /big\.js:(-?\d+)/, + ], + [ + "line of a compile-time SyntaxError from a Script", + `new vm.Script(${JSON.stringify("1;\n%%")}, { filename: "big.js", lineOffset: ${INT32_MAX - 1} })`, + /big\.js:(-?\d+)/, + ], + [ + "column of a runtime error thrown on the first line of a Script", + `new vm.Script('throw new Error("q")', { filename: "big.js", columnOffset: ${INT32_MAX} }).runInThisContext()`, + /big\.js:1:(-?\d+)/, + ], + [ + "line of a runtime error thrown by a compileFunction body", + `vm.compileFunction('throw new Error("q")', [], { filename: "big.js", lineOffset: ${INT32_MAX} })()`, + /big\.js:(-?\d+)/, + ], + [ + "line of a compile-time SyntaxError from compileFunction", + `vm.compileFunction("%%", [], { filename: "big.js", lineOffset: ${INT32_MAX} })`, + /big\.js:(-?\d+)/, + ], + ])("%s stays near the requested offset", async (_, expression, pattern) => { + const stdout = await runFixture(`try { ${expression}; } catch (e) { console.log(e.stack); }`); + const match = pattern.exec(stdout); + expect(match).not.toBeNull(); + // The offset is only pulled down by as much as the (tiny) source could + // possibly add to it, so the reported position stays just below INT32_MAX + // rather than wrapping negative or being dropped. + const position = Number(match![1]); + expect(position).toBeGreaterThan(INT32_MAX - 100); + expect(position).toBeLessThanOrEqual(INT32_MAX); + }); +}); From 1d17387e2d6c152c3aefc75b32282c931383021d Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:28:52 +0000 Subject: [PATCH 2/5] node:vm: remove the unused native runInNewContext/runInThisContext bindings vm.ts has implemented both on top of Script since the node:vm rewrite, so these host functions (and the BaseVMOptions constructor only they used) were unreachable. They also built their SourceCode from the raw offsets, so dropping them leaves no unclamped path. --- src/jsc/bindings/NodeVM.cpp | 129 ------------------------------------ src/jsc/bindings/NodeVM.h | 3 - 2 files changed, 132 deletions(-) diff --git a/src/jsc/bindings/NodeVM.cpp b/src/jsc/bindings/NodeVM.cpp index d6595c4fef11..9c4533d17b31 100644 --- a/src/jsc/bindings/NodeVM.cpp +++ b/src/jsc/bindings/NodeVM.cpp @@ -1419,122 +1419,6 @@ void NodeVMGlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_dynamicImportCallback); } -JSC_DEFINE_HOST_FUNCTION(vmModuleRunInNewContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - VM& vm = globalObject->vm(); - auto scope = DECLARE_THROW_SCOPE(vm); - - JSValue code = callFrame->argument(0); - if (!code.isString()) - return ERR::INVALID_ARG_TYPE(scope, globalObject, "code"_s, "string"_s, code); - - JSValue contextArg = callFrame->argument(1); - bool notContextified = getContextArg(globalObject, contextArg); - - if (!contextArg.isObject()) { - return ERR::INVALID_ARG_TYPE(scope, globalObject, "context"_s, "object"_s, contextArg); - } - - JSObject* sandbox = asObject(contextArg); - - JSValue contextOptionsArg = callFrame->argument(2); - NodeVMContextOptions contextOptions {}; - - JSValue globalObjectDynamicImportCallback; - - getNodeVMContextOptions(globalObject, vm, scope, contextOptionsArg, contextOptions, "contextCodeGeneration", &globalObjectDynamicImportCallback); - RETURN_IF_EXCEPTION(scope, {}); - - contextOptions.notContextified = notContextified; - - // Create context and run code - auto* context = NodeVMGlobalObject::create(vm, - defaultGlobalObject(globalObject)->NodeVMGlobalObjectStructure(), - contextOptions, globalObjectDynamicImportCallback); - - context->setContextifiedObject(sandbox); - - JSValue optionsArg = callFrame->argument(2); - JSValue scriptDynamicImportCallback; - - ScriptOptions options(optionsArg.toWTFString(globalObject), OrdinalNumber::fromZeroBasedInt(0), OrdinalNumber::fromZeroBasedInt(0)); - if (optionsArg.isString()) { - options.filename = optionsArg.toWTFString(globalObject); - RETURN_IF_EXCEPTION(scope, {}); - } else if (!options.fromJS(globalObject, vm, scope, optionsArg, &scriptDynamicImportCallback)) { - RETURN_IF_EXCEPTION(scope, {}); - } - - RefPtr fetcher(NodeVMScriptFetcher::create(vm, scriptDynamicImportCallback, jsUndefined())); - - SourceCode sourceCode( - JSC::StringSourceProvider::create( - code.toString(globalObject)->value(globalObject), - JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename), *fetcher), - options.filename, - JSC::SourceTaintedOrigin::Untainted, - TextPosition(options.lineOffset, options.columnOffset)), - options.lineOffset.zeroBasedInt(), - options.columnOffset.zeroBasedInt()); - - NakedPtr exception; - JSValue result = JSC::evaluate(context, sourceCode, context, exception); - - if (exception) [[unlikely]] { - if (handleException(globalObject, vm, exception, scope)) { - return {}; - } - JSC::throwException(globalObject, scope, exception.get()); - return {}; - } - - return JSValue::encode(result); -} - -JSC_DEFINE_HOST_FUNCTION(vmModuleRunInThisContext, (JSGlobalObject * globalObject, CallFrame* callFrame)) -{ - VM& vm = JSC::getVM(globalObject); - auto sourceStringValue = callFrame->argument(0); - auto throwScope = DECLARE_THROW_SCOPE(vm); - - if (!sourceStringValue.isString()) { - return ERR::INVALID_ARG_TYPE(throwScope, globalObject, "code"_s, "string"_s, sourceStringValue); - } - - String sourceString = sourceStringValue.toWTFString(globalObject); - RETURN_IF_EXCEPTION(throwScope, encodedJSUndefined()); - - JSValue importer; - - JSValue optionsArg = callFrame->argument(1); - ScriptOptions options(optionsArg.toWTFString(globalObject), OrdinalNumber::fromZeroBasedInt(0), OrdinalNumber::fromZeroBasedInt(0)); - if (optionsArg.isString()) { - options.filename = optionsArg.toWTFString(globalObject); - RETURN_IF_EXCEPTION(throwScope, {}); - } else if (!options.fromJS(globalObject, vm, throwScope, optionsArg, &importer)) { - RETURN_IF_EXCEPTION(throwScope, encodedJSUndefined()); - } - - RefPtr fetcher(NodeVMScriptFetcher::create(vm, importer, jsUndefined())); - - SourceCode source( - JSC::StringSourceProvider::create(sourceString, JSC::SourceOrigin(WTF::URL::fileURLWithFileSystemPath(options.filename), *fetcher), options.filename, JSC::SourceTaintedOrigin::Untainted, TextPosition(options.lineOffset, options.columnOffset)), - options.lineOffset.zeroBasedInt(), options.columnOffset.zeroBasedInt()); - - WTF::NakedPtr exception; - JSValue result = JSC::evaluate(globalObject, source, globalObject, exception); - - if (exception) [[unlikely]] { - if (handleException(globalObject, vm, exception, throwScope)) { - return {}; - } - JSC::throwException(globalObject, throwScope, exception.get()); - return {}; - } - - return JSValue::encode(result); -} - JSC_DEFINE_HOST_FUNCTION(vmModuleCompileFunction, (JSGlobalObject * globalObject, CallFrame* callFrame)) { VM& vm = globalObject->vm(); @@ -1833,12 +1717,6 @@ JSC::JSValue createNodeVMBinding(Zig::GlobalObject* globalObject) obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "isContext"_s)), JSC::JSFunction::create(vm, globalObject, 0, "isContext"_s, vmModule_isContext, ImplementationVisibility::Public), 0); - obj->putDirect( - vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "runInNewContext"_s)), - JSC::JSFunction::create(vm, globalObject, 0, "runInNewContext"_s, vmModuleRunInNewContext, ImplementationVisibility::Public), 0); - obj->putDirect( - vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "runInThisContext"_s)), - JSC::JSFunction::create(vm, globalObject, 0, "runInThisContext"_s, vmModuleRunInThisContext, ImplementationVisibility::Public), 0); obj->putDirect( vm, JSC::PropertyName(JSC::Identifier::fromString(vm, "compileFunction"_s)), JSC::JSFunction::create(vm, globalObject, 0, "compileFunction"_s, vmModuleCompileFunction, ImplementationVisibility::Public), 0); @@ -1937,13 +1815,6 @@ BaseVMOptions::BaseVMOptions(String filename) { } -BaseVMOptions::BaseVMOptions(String filename, OrdinalNumber lineOffset, OrdinalNumber columnOffset) - : filename(WTF::move(filename)) - , lineOffset(lineOffset) - , columnOffset(columnOffset) -{ -} - bool BaseVMOptions::fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg) { JSObject* options = nullptr; diff --git a/src/jsc/bindings/NodeVM.h b/src/jsc/bindings/NodeVM.h index 4925f51bb620..6bef45798268 100644 --- a/src/jsc/bindings/NodeVM.h +++ b/src/jsc/bindings/NodeVM.h @@ -61,7 +61,6 @@ class BaseVMOptions { BaseVMOptions() = default; BaseVMOptions(String filename); - BaseVMOptions(String filename, OrdinalNumber lineOffset, OrdinalNumber columnOffset); bool fromJS(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSC::JSValue optionsArg); bool validateProduceCachedData(JSC::JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSObject* options, bool& outProduceCachedData); @@ -178,7 +177,5 @@ void configureNodeVM(JSC::VM&, Zig::GlobalObject*); // VM module functions JSC_DECLARE_HOST_FUNCTION(vmModule_createContext); JSC_DECLARE_HOST_FUNCTION(vmModule_isContext); -JSC_DECLARE_HOST_FUNCTION(vmModuleRunInNewContext); -JSC_DECLARE_HOST_FUNCTION(vmModuleRunInThisContext); } // namespace Bun From c1cb06c60ed965daf0092a3ea9778e3dfef1962f Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:32:31 +0000 Subject: [PATCH 3/5] node:vm: shorten the clampOffsetForSource comments --- src/jsc/bindings/NodeVM.cpp | 3 +-- src/jsc/bindings/NodeVM.h | 7 ++----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/jsc/bindings/NodeVM.cpp b/src/jsc/bindings/NodeVM.cpp index 9c4533d17b31..c802bb93f76f 100644 --- a/src/jsc/bindings/NodeVM.cpp +++ b/src/jsc/bindings/NodeVM.cpp @@ -136,8 +136,7 @@ JSC::JSFunction* constructAnonymousFunction(JSC::JSGlobalObject* globalObject, c EXCEPTION_ASSERT(!!throwScope.exception() == program.isNull()); RETURN_IF_EXCEPTION(throwScope, nullptr); - // The wrapped program is the longest text parsed here, so bounding the - // offsets against it also covers the standalone parse of the body below. + // The wrapper is the longest text parsed below (the body alone is parsed first). options.lineOffset = clampOffsetForSource(options.lineOffset, program.length()); options.columnOffset = clampOffsetForSource(options.columnOffset, program.length()); diff --git a/src/jsc/bindings/NodeVM.h b/src/jsc/bindings/NodeVM.h index 6bef45798268..ef90becf0a9a 100644 --- a/src/jsc/bindings/NodeVM.h +++ b/src/jsc/bindings/NodeVM.h @@ -33,11 +33,8 @@ bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr // when no filename was provided; compileFunction has no such default. void decorateParseErrorStack(JSGlobalObject* globalObject, VM& vm, JSObject* error, StringView sourceString, const String& url, const JSC::ParserError& parseError, OrdinalNumber lineOffset); -// Node accepts any int32 lineOffset/columnOffset, but JSC keeps positions in -// ints and adds one (one-based) plus the source's own line terminators or -// first-line columns on top of the offset, so values near INT_MAX overflow in the -// parser. Neither count can exceed the source's length in code units, so an -// offset of at most INT_MAX - 1 - sourceLength keeps every derived position in range. +// JSC counts positions in ints, starting one past the offset and advancing at +// most once per code unit of source; cap the offset so that cannot overflow. OrdinalNumber clampOffsetForSource(OrdinalNumber offset, unsigned sourceLength); void getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey, JSValue* importer); NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSValue contextValue, bool canThrow); From 4efc567b9d8c4815a3657ad84f0b5e64b95fbee3 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:34:45 +0000 Subject: [PATCH 4/5] node:vm: describe clampOffsetForSource in one line --- src/jsc/bindings/NodeVM.h | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/jsc/bindings/NodeVM.h b/src/jsc/bindings/NodeVM.h index ef90becf0a9a..4a99a1997b77 100644 --- a/src/jsc/bindings/NodeVM.h +++ b/src/jsc/bindings/NodeVM.h @@ -33,8 +33,7 @@ bool handleException(JSGlobalObject* globalObject, VM& vm, NakedPtr // when no filename was provided; compileFunction has no such default. void decorateParseErrorStack(JSGlobalObject* globalObject, VM& vm, JSObject* error, StringView sourceString, const String& url, const JSC::ParserError& parseError, OrdinalNumber lineOffset); -// JSC counts positions in ints, starting one past the offset and advancing at -// most once per code unit of source; cap the offset so that cannot overflow. +// Lowers offset if needed so that offset + 1 + sourceLength fits in an int, JSC's position type. OrdinalNumber clampOffsetForSource(OrdinalNumber offset, unsigned sourceLength); void getNodeVMContextOptions(JSGlobalObject* globalObject, JSC::VM& vm, JSC::ThrowScope& scope, JSValue optionsArg, NodeVMContextOptions& outOptions, ASCIILiteral codeGenerationKey, JSValue* importer); NodeVMGlobalObject* getGlobalObjectFromContext(JSGlobalObject* globalObject, JSValue contextValue, bool canThrow); From e9cbc739d4b04d09b230c08ace2eed1882d7c9ac Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:56:04 +0000 Subject: [PATCH 5/5] node:vm: make the multi-line overflow test cases independent of where line counting starts Use lineOffset INT32_MAX - 1 with one more line of source for the SourceTextModule and multi-line compileFunction cases, so the counter still steps past INT32_MAX without the clamp whether the first line is numbered lineOffset or lineOffset + 1. --- test/js/node/vm/vm.test.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index dc46079dbc30..8f5b858a0a15 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -1767,10 +1767,12 @@ describe("node:vm lineOffset/columnOffset at the edge of int32", () => { ["new Script, columnOffset", `new vm.Script(${JSON.stringify("1;\n2;")}, { columnOffset: ${INT32_MAX} })`], ["compileFunction", `vm.compileFunction("return 1", [], { lineOffset: ${INT32_MAX} })`], [ - "compileFunction with params and both offsets", - `vm.compileFunction("return a", ["a"], { lineOffset: ${INT32_MAX}, columnOffset: ${INT32_MAX} })`, + "compileFunction with params, a multi-line body and both offsets", + `vm.compileFunction(${JSON.stringify("a;\nreturn a;")}, ["a"], { lineOffset: ${INT32_MAX - 1}, columnOffset: ${INT32_MAX} })`, ], - ["SourceTextModule", `new vm.SourceTextModule(${JSON.stringify("1;\n2;")}, { lineOffset: ${INT32_MAX} })`], + // Three lines so the counter steps past INT32_MAX whether the module's + // first line is taken as lineOffset or, like Script, as lineOffset + 1. + ["SourceTextModule", `new vm.SourceTextModule(${JSON.stringify("1;\n2;\n3;")}, { lineOffset: ${INT32_MAX - 1} })`], ])("%s compiles", async (_, expression) => { const stdout = await runFixture(`${expression};\nconsole.log("ok");`); expect(stdout).toBe("ok\n");