Skip to content

vm: populate Script#sourceMapURL before the script is run - #32534

Closed
robobun wants to merge 2 commits into
mainfrom
farm/099e96b1/vm-script-sourcemapurl
Closed

robobun wants to merge 2 commits into
mainfrom
farm/099e96b1/vm-script-sourcemapurl

Conversation

@robobun

@robobun robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

What / why

new vm.Script(code).sourceMapURL returned undefined in Bun until the script was first run. Node reports it right after construction.

import vm from "node:vm";

const script = new vm.Script(`
function myFunc() {}
//# sourceMappingURL=sourcemap.json
`);

console.log(script.sourceMapURL);
// Node:  sourcemap.json
// Bun:   undefined   (before this change)

Fixes #32530.

Cause

The sourceMapURL getter reads script->source().provider()->sourceMappingURLDirective(). That directive is only set while JSC's parser walks the source. Bun parses a vm.Script lazily (at first run), whereas Node compiles eagerly at construction, so the directive was empty until the script ran. Running the script first made the value appear, which pinned the cause.

The cachedData path had the same gap: the directive is stored in the serialized bytecode but was never copied back onto the source provider after decoding.

Fix

  • Parse the source the first time sourceMapURL is read, using checkSyntax (no bytecode generation), and cache the result with a flag so scripts that never read the property keep construction cheap and repeated reads do not re-parse.
  • When a script is built from cachedData, restore the sourceURL / sourceMappingURL directives recorded in the decoded bytecode onto the provider (mirrors what CodeCache does on a cache hit).

Verification

New tests in test/js/node/vm/vm.test.ts cover construction, post-run, produceCachedData, valid cachedData, rejected cachedData, and the no-directive case.

bun bd test test/js/node/vm/vm.test.ts -t sourceMapURL
6 pass, 0 fail

Without the fix (USE_SYSTEM_BUN=1), the construction, valid-cachedData, and rejected-cachedData cases fail with undefined. The full vm.test.ts suite stays green (212 pass).

new vm.Script(code).sourceMapURL returned undefined in Bun until the
script was first run, while Node reports it right after construction.
Bun parses the source lazily (at first run), so the sourceMappingURL
directive was not yet set on the source provider when the getter read
it.

Parse the source the first time sourceMapURL is read (without generating
bytecode) so the directive is populated on demand, and restore the
directive from cached bytecode when a script is built from cachedData.
This keeps construction cheap for scripts that never read the property.
@robobun

robobun commented Jun 20, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:42 AM PT - Jun 20th, 2026

❌ @robobun, your commit 966ccec has 2 failures in Build #63632 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32534

That installs a local version of the PR into your bun-32534 executable, so you can run:

bun-32534 --bun

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e90d913d-4b20-4b06-8945-bff606be420e

📥 Commits

Reviewing files that changed from the base of the PR and between 8841747 and 2977603.

📒 Files selected for processing (3)
  • src/jsc/bindings/NodeVMScript.cpp
  • src/jsc/bindings/NodeVMScript.h
  • test/js/node/vm/vm.test.ts

Walkthrough

NodeVMScript gains a m_directivesResolved boolean flag. Source mapping directives are now populated from decoded cached bytecode during construction, marked resolved after cacheBytecode(), and lazily resolved via JSC::checkSyntax in the sourceMapURL getter. Tests cover all resolution paths.

Changes

Script#sourceMapURL directive resolution

Layer / File(s) Summary
NodeVMScript directivesResolved flag
src/jsc/bindings/NodeVMScript.h
Adds private bool m_directivesResolved (initialized to false) and public directivesResolved() getter and directivesResolved(bool) setter.
Directive population: cached bytecode, produceCachedData, sourceMapURL getter
src/jsc/bindings/NodeVMScript.cpp
Includes ParserError.h; during cached-bytecode construction, restores sourceURLDirective and sourceMappingURLDirective from unlinkedBlock into the source provider and marks resolved; after cacheBytecode(), marks resolved; in sourceMapURL getter, lazily calls JSC::checkSyntax to populate directives if not yet resolved, then marks resolved.
Script#sourceMapURL tests
test/js/node/vm/vm.test.ts
New describe("Script#sourceMapURL") suite asserting correct directive availability on construction, after runInThisContext, when the comment is absent, with produceCachedData: true, when restoring from valid cachedData, and when cachedData is rejected.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'vm: populate Script#sourceMapURL before the script is run' clearly and specifically describes the main change: ensuring the sourceMapURL property is available immediately after construction rather than only after execution.
Description check ✅ Passed The PR description follows the template with 'What / why' and verification sections, providing clear context, root cause analysis, the fix approach, and testing results.
Linked Issues check ✅ Passed The PR successfully addresses issue #32530 by implementing lazy directive resolution for sourceMapURL and restoring directives from cachedData, making script.sourceMapURL available immediately after construction as expected.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the sourceMapURL issue: adding directive tracking, implementing lazy parsing via checkSyntax, restoring directives from cachedData, and comprehensive tests covering the fix scenarios.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. node:vm: pass the Node v26.3.0 vm test suite (+25 tests, ~65% → 97%) #32018 - Both PRs fix vm.Script#sourceMapURL being undefined before first run by eagerly parsing on property access and caching the result; node:vm: pass the Node v26.3.0 vm test suite (+25 tests, ~65% → 97%) #32018 uses parseRootNode<ProgramNode> while this PR uses checkSyntax()

🤖 Generated with Claude Code

@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

The duplicate detector is right: #32018 already fixes Script#sourceMapURL the same way this PR does. Both resolve the directive lazily on the first read of the getter, guarded by a flag (#32018 parses via parseRootNode<ProgramNode>, this one via JSC::checkSyntax); the observable result is identical. This PR additionally restores the directive from cachedData without re-parsing, but that is only an efficiency difference.

#32018 is a much larger effort (the full node:vm suite, +25 tests) and would supersede this. This PR is a focused standalone fix that closes #32530 on its own if landing the broader draft will take longer. Happy to close in favor of #32018 if you would rather keep it all in one place.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues — the fix is small, well-tested, and follows the existing checkSyntax pattern in NodeVM.cpp — but since it adds a parser invocation inside a JSC property getter and touches the cached-bytecode decode path, it's worth a quick human glance from someone familiar with the JSC bindings.

Extended reasoning...

Overview

This PR fixes #32530: new vm.Script(code).sourceMapURL returned undefined in Bun until the script was first run, whereas Node populates it at construction. The change touches three files:

  • src/jsc/bindings/NodeVMScript.h — adds a bool m_directivesResolved member with a getter/setter pair.
  • src/jsc/bindings/NodeVMScript.cpp — in the sourceMapURL custom getter, lazily calls JSC::checkSyntax(vm, source, parserError) the first time the property is read so the parser populates sourceMappingURLDirective() on the provider; in the cachedData construction path, copies sourceURLDirective/sourceMappingURLDirective from the decoded UnlinkedProgramCodeBlock back onto the provider; marks directives resolved after cacheBytecode() in the produceCachedData path.
  • test/js/node/vm/vm.test.ts — adds a 6-test describe block covering construction, post-run, no-directive, produceCachedData, valid cachedData, and rejected cachedData.

Security risks

None identified. The change does not touch auth, crypto, permissions, or sandbox boundaries. The only new behavior is invoking JSC's syntax checker on source text the caller already supplied, and copying string directives between JSC structures. The ParserError from checkSyntax is captured locally and not re-thrown, so a syntactically-invalid script just leaves the directive empty (returns undefined), which matches existing behavior.

Level of scrutiny

This is C++ runtime code in the JSC bindings layer — not a config tweak or mechanical change. The logic itself is small (~25 net lines), well-explained, and mirrors established patterns: checkSyntax(vm, source, error) is already used identically in NodeVM.cpp:144, and restoring directives from a decoded unlinked code block onto the provider is what JSC's own CodeCache does on a cache hit. The new bool member sits between two existing scalar members on a JSDestructibleObject and needs no GC visitation. I don't see correctness problems.

That said, JSC bindings are performance- and correctness-critical, and adding a parser invocation inside a property getter (even a cheap, once-only checkSyntax) is the kind of thing a maintainer familiar with the bindings should confirm — e.g., that there's no exception/scope subtlety, and that the lazy-parse strategy is preferred over eager-parse-at-construction here.

Other factors

No CODEOWNERS apply to these paths. The bug-hunting system found no issues. Test coverage for the new behavior is thorough across all construction paths. CI is building. There are no outstanding reviewer comments. Given the change is in native bindings rather than pure JS/config, I'm deferring rather than auto-approving.

@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks. For the two JSC-bindings points raised:

  • Exception/scope safety: the getter uses the non-throwing checkSyntax(VM&, const SourceCode&, ParserError&) overload (the same one already used in NodeVM.cpp). It reports parse errors through the ParserError out-param, never as a JS exception, so the ThrowScope stays clean and a malformed script just yields an empty directive (undefined), matching prior behavior. No coercions or user JS run during the parse, so script can't be freed underneath it.
  • Lazy vs eager: parsing on first read (rather than eagerly at construction) keeps construction free of extra parse/bytecode work for the common case where sourceMapURL is never read, while matching Node's observable value. The m_directivesResolved flag caps it at one parse per script.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues — the lazy checkSyntax approach and cachedData directive restoration look sound and are well-tested — but since this touches JSC bindings and overlaps with the open #32018, a maintainer should decide which to land.

Extended reasoning...

Overview

This PR fixes vm.Script#sourceMapURL returning undefined until the script is first run (issue #32530). It touches three files:

  • src/jsc/bindings/NodeVMScript.h — adds a m_directivesResolved bool with getter/setter, following the existing m_cachedDataProduced / m_cachedDataRejected pattern in the same class.
  • src/jsc/bindings/NodeVMScript.cpp — three small additions: (1) the sourceMapURL getter now lazily calls JSC::checkSyntax(vm, source, parserError) once to populate the provider's directive; (2) when decoding valid cachedData, the sourceURLDirective / sourceMappingURLDirective are copied from the UnlinkedProgramCodeBlock back onto the source provider (mirroring JSC's own CodeCache behavior); (3) after cacheBytecode() in the produceCachedData path, the flag is marked resolved.
  • test/js/node/vm/vm.test.ts — six new tests covering construction, post-run, no-directive, produceCachedData, valid cachedData, and rejected cachedData.

Security risks

None identified. The getter only triggers a syntax-only parse of source the user already supplied; the non-throwing checkSyntax(VM&, const SourceCode&, ParserError&) overload (already used in NodeVM.cpp) reports errors via the out-param rather than throwing, so no exception-scope hazard. No user JS or coercions run during the parse. The cachedData path reads two StringImpl* fields off an already-decoded UnlinkedProgramCodeBlock and stores them on the provider — no new attacker-controlled surface.

Level of scrutiny

Medium. The diff is small (~30 LOC of C++) and follows existing idioms in the file, but it lives in JSC bindings where mistakes around GC, exception scopes, or provider lifetime can be subtle. The author addressed the exception/GC-safety questions in the thread, and the checkSyntax overload used here is the same one already in NodeVM.cpp:144. I don't see anything wrong, but I'd prefer a human familiar with the JSC bindings to give it a quick pass rather than auto-approve.

Other factors

  • A duplicate-detector flagged overlap with the larger draft #32018, and the author has explicitly offered to close in favor of it. Whether to land this focused fix now or fold it into #32018 is a maintainer call that shouldn't be made by a bot.
  • Test coverage is thorough (6 cases covering every branch added) and the full vm.test.ts suite reportedly stays green.
  • No CODEOWNERS entry covers these paths.
  • The bug-hunting system found no issues.

@robobun

robobun commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

CI status for whoever picks this up: the diff is green where it matters. test/js/node/vm/vm.test.ts passes on every lane, and all build lanes (Rust/C++/bun, every platform) are green.

The two red runs failed on different, unrelated tests each time, none touching node:vm:

  • build 63625: test-tls-client-destroy-soon.js (TLS byte-count race), plus flaky bake/deinitialization and a bun install timeout.
  • build 63632: grpc-js/test-server (SIGTRAP), plus flaky update_interactive_install.

Disjoint failures across runs is the flake signature; a vm.Script change cannot affect TLS, gRPC, or install. I have used my one retrigger and will not push further empty commits. Ready from my side, pending the focused-vs-#32018 call.

@robobun

robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as redundant: #32018 landed on main (commit 0672e7d) and already fixes this. The sourceMapURL getter now lazily parses the source on first read (parseRootNode<ProgramNode>), so the directive is available right after construction, before the script runs, which is the same approach this PR took. It also ships coverage for the exact scenario in test/js/node/test/parallel/test-vm-source-map-url.js (construction-then-read, no magic comment, and malformed comment cases).

No need to force a rebase of a duplicate fix over the merged work. Thanks to @cirospaciari for folding this into the broader node:vm pass.

@robobun robobun closed this Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Script from node:vm does not update state based on the given input

1 participant