Check syntax eagerly in vm.Script constructor - #28667
Conversation
|
Updated 8:06 AM PT - May 5th, 2026
❌ @robobun, your commit 68b2343 has 3 failures in
🧪 To try this PR locally: bunx bun-pr 28667That installs a local version of the PR into your bun-28667 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds an explicit JavaScript syntax validation step to vm.Script construction so invalid source throws a SyntaxError during Script construction. Tests updated to expect constructor-time syntax errors and a new regression suite verifies multiple invalid-source cases and that valid source does not throw. Changes
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
LGTM — the fix is correct and follows established patterns; the inline nit on test 4 doesn't affect correctness.
Extended reasoning...
Overview
The PR adds a call to JSC::checkSyntax() in constructScript() (NodeVMScript.cpp) immediately after creating the SourceCode, before constructing the NodeVMScript object. This mirrors the identical pattern already used for compileFunction in NodeVM.cpp. Two test files are updated: the existing vm.test.ts has one expectation corrected to reflect the new construction-time error, and a new regression test file with 5 tests is added.
Security risks
None. The change adds eager syntax validation that was already happening at run-time; it only moves when the error is surfaced, not whether it is.
Level of scrutiny
Low — this is a small, self-contained correctness fix on a well-tested code path. The pattern is already proven by NodeVM.cpp. All 198 existing vm.test.ts tests continue to pass per the PR description.
Other factors
The one bug flagged is a [Nit]: test 4 of 5 in the regression file wraps both the constructor and runInThisContext() in a single try-catch, so it cannot distinguish construction-time from run-time errors. However, tests 1–3 already cover the construction-time requirement correctly using expect(() => new Script(...)).toThrow(SyntaxError), so regression coverage is intact. The nit is worth fixing but is not a blocker.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@test/regression/issue/28666.test.ts`:
- Around line 22-32: The test currently treats any caught error as success by
calling script.runInThisContext() and marking the test passed on any exception;
instead, assert that a construction-time (compilation) error is thrown by
creating the VM Script itself. Replace the runtime invocation with an explicit
construction/assertion: call new vm.Script(source) (or assert.throws(() => new
vm.Script(source), SyntaxError)) so the test fails if syntax errors are deferred
to runtime, and remove the logic that treats any caught runtime error as a
passing condition.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: d69bb7cc-d6ea-422d-935c-fa477a6c507e
📥 Commits
Reviewing files that changed from the base of the PR and between 17616ae and 4088e5dcc4a0ea51e6c880878cd6e8891f8af8e6.
📒 Files selected for processing (3)
src/bun.js/bindings/NodeVMScript.cpptest/js/node/vm/vm.test.tstest/regression/issue/28666.test.ts
There was a problem hiding this comment.
LGTM — the fix is correct and follows the established pattern.
Extended reasoning...
Overview
This PR adds 10 lines to src/bun.js/bindings/NodeVMScript.cpp that call JSC::checkSyntax() inside constructScript(), converting a ParserError into a thrown JS exception and aborting construction. This mirrors the identical pattern already present in NodeVM.cpp for compileFunction. Two test files are updated: a test in vm.test.ts that previously expected a createCachedData failed error now expects the correct SyntaxError from construction, and a new regression file test/regression/issue/28666.test.ts adds five targeted tests.
Security risks
None. This change makes error reporting more eager; it does not affect sandbox isolation, permissions, or any security-sensitive code paths.
Level of scrutiny
Low. The change is a small, self-contained addition to a constructor, touches no shared state, and follows a well-established pattern in the same codebase. The regression test suite adequately covers the new behavior.
Other factors
A prior concern I raised (test 4 in 28666.test.ts not distinguishing construction-time from run-time errors) was addressed in commit 7c4b105 via the reachedRun flag approach. A secondary nit I flagged about dead code in vm.test.ts (lines after `new Script("export default {};")" in the lambda) is still present but does not affect test correctness or regression protection — the test passes and the SyntaxError is caught either way.
There was a problem hiding this comment.
LGTM — both prior concerns addressed, implementation follows established pattern.
Extended reasoning...
Overview
Adds eager syntax validation in 's by calling after the is created but before constructing the object. Three files changed: the C++ binding, an updated unit test in , and a new regression test file for issue #28666.
Security risks
None. This is a correctness fix that makes error behavior match Node.js — errors are surfaced earlier (at construction) rather than later (at execution). No auth, crypto, or permissions code is touched.
Level of scrutiny
Low. The change is small (~10 lines of C++), follows the identical pattern already used in for , and is covered by both the existing vm test suite (198 tests passing per PR description) and new regression tests.
Other factors
Two reviewer concerns were raised and resolved: (1) the flawed test that couldn't distinguish construction-time vs runtime errors — fixed with the flag; (2) dead code in from unreachable / calls — removed in a00a96e. The final state of both test files is clean and correct.
There was a problem hiding this comment.
LGTM — both test issues from prior reviews are resolved, and the C++ fix follows the established pattern from NodeVM.cpp.
Extended reasoning...
Overview
The PR adds eager syntax checking in NodeVMScript.cpp's constructScript() by calling JSC::checkSyntax(vm, source, error) after SourceCode creation, converting a ParserError into a thrown JS SyntaxError. Two test files are updated: vm.test.ts removes previously-dead runInThisContext/createCachedData calls, and a new regression file 28666.test.ts is added.
Security risks
None. This change only makes errors earlier (construction-time vs. run-time) and does not affect sandboxing, auth, or permissions.
Level of scrutiny
Low. The fix is a small, focused change to a single function in a non-security-critical code path. It mirrors an existing pattern already used in NodeVM.cpp for compileFunction. The previous two review rounds caught and resolved test correctness issues; those fixes are verified in the current diff.
Other factors
Both issues I identified in prior rounds were addressed: the reachedRun sentinel correctly enforces construction-time semantics, and the dead method calls were removed. No bugs were found by the bug hunting system. Tests pass per the PR description (198 pass, 0 fail).
3d49109 to
c7aed54
Compare
There was a problem hiding this comment.
LGTM — prior concerns addressed, implementation is correct.
Extended reasoning...
Overview
The PR adds a single syntax-check call () in after the is created but before the object is allocated. This matches the pattern already used in for . Three files are modified: the C++ binding, an existing vm test, and a new regression test suite.
Security risks
None. The change throws a earlier (at construction time instead of execution time), which is strictly a bug-fix alignment with Node.js behavior. No new attack surface is introduced.
Level of scrutiny
Low. The change is small, self-contained, and follows an established in-repo pattern. The regression test covers multiple invalid-syntax cases and a valid-syntax non-throw case.
Other factors
Two test-quality issues I flagged in earlier reviews were both fixed prior to this review: the construction-time distinction is now enforced via a reachedRun flag, and dead method calls after a throwing constructor were removed. No bugs were found by the automated bug-hunting system. CI is passing.
c7aed54 to
d2fb91f
Compare
There was a problem hiding this comment.
LGTM — the core fix correctly mirrors the pattern.
Extended reasoning...
Overview
PR adds call in (NodeVMScript.cpp) between SourceCode creation and NodeVMScript object creation, matching the existing pattern in NodeVM.cpp for compileFunction. Changes touch NodeVMScript.cpp, the existing vm.test.ts test, and a new regression suite (28666.test.ts).
Security risks
None. The change adds a syntax-validation step that rejects invalid source earlier; it does not alter sandbox isolation, permissions, or data exposure.
Level of scrutiny
Low. This is a small, well-scoped C++ change (~10 lines) following an already-established pattern. The regression test file properly validates construction-time throwing (the reachedRun flag correctly enforces the regression boundary). The only outstanding item is a nit (missing ASSERT(error.isValid())) already posted as an inline comment.
Other factors
Prior feedback I left was addressed: the test-validity issue on 28666.test.ts was fixed with the reachedRun flag. The dead-code note on vm.test.ts was acknowledged but the calls remain; however, the tests still pass and the regression is correctly caught by the other tests. Both residual items are defensive-programming nits that do not affect correctness.
d2fb91f to
5ca28b2
Compare
There was a problem hiding this comment.
LGTM
Extended reasoning...
Overview
This PR adds eager syntax validation to vm.Script's constructor by calling JSC::checkSyntax() after creating the SourceCode object but before constructing the NodeVMScript instance. It fixes issue #28666 where Bun deferred syntax errors to execution time instead of throwing at construction, diverging from Node.js behavior.
Modified files: NodeVMScript.cpp (core fix), test/js/node/vm/vm.test.ts (updated assertion), test/regression/issue/28666.test.ts (new regression tests).
Security risks
None. This is a JavaScript VM behavior compatibility fix with no authentication, crypto, or permission-sensitive code involved.
Level of scrutiny
Low-to-medium. The change is a self-contained, well-scoped addition following an established pattern already used in NodeVM.cpp for compileFunction. The ASSERT(error.isValid()) guard (matching the analogous call site) was added after my prior review.
Other factors
All three issues I raised in prior reviews have been addressed: the misleading threwAtConstruction pattern in the regression test (fixed with reachedRun flag), the missing ASSERT(error.isValid()), and the test assertion message update. The remaining dead code (runInThisContext() / createCachedData() calls on lines 694/699 of vm.test.ts) is a test-clarity nit — the tests remain functionally correct since the constructor throws and .toThrow() catches it as expected. Regression coverage is adequate.
5ca28b2 to
d6aa759
Compare
There was a problem hiding this comment.
LGTM — all previously flagged issues have been addressed.
Extended reasoning...
Overview
This PR adds eager syntax validation to the vm.Script constructor by calling JSC::checkSyntax() on the SourceCode before creating the NodeVMScript object. Three files are touched: the C++ constructor (NodeVMScript.cpp), an existing vm test (vm.test.ts), and a new regression suite (28666.test.ts).
Security risks
No security-sensitive code paths are affected. The change narrows the error surface (syntax errors surface earlier) and does not touch auth, crypto, or permissions.
Level of scrutiny
This is a focused correctness fix for a Node.js compatibility gap. The pattern precisely mirrors what NodeVM.cpp already does for compileFunction. The risk is low — the only observable behavior change is that new Script(invalidSyntax) now throws instead of succeeding silently.
Other factors
All three issues from my prior reviews were resolved: the reachedRun flag correctly enforces the construction-time assertion, the dead runtime-method calls were removed, and ASSERT(error.isValid()) was added to match the established codebase pattern. The remaining open bug report is a performance nit (double-parse) that is an acknowledged tradeoff of this approach and is suitable as a follow-up.
| { | ||
| JSC::ParserError error; | ||
| if (!JSC::checkSyntax(vm, source, error)) { | ||
| ASSERT(error.isValid()); | ||
| auto exception = error.toErrorObject(globalObject, source); | ||
| RETURN_IF_EXCEPTION(scope, {}); | ||
| JSC::throwException(globalObject, scope, exception); | ||
| return {}; | ||
| } |
There was a problem hiding this comment.
🟡 Every followed by execution now incurs two full JSC parses instead of one: in the constructor fully parses the source and discards the AST, then re-parses from scratch at run time. Pre-fix Bun did one parse (at run time); Node.js/V8 does one compile at construction time and reuses cached bytecode — this PR is strictly worse than both for the common case of valid scripts that are executed.
Extended reasoning...
What the bug is and how it manifests
The new call added to (NodeVMScript.cpp lines 134–142) performs a full source parse internally — it calls the JSC parser, builds a complete AST, validates it, and then discards everything. When the script is later executed via , , or , creates a new and re-parses the same source from scratch to produce bytecode. This means every object that is created and then run now incurs two full parses instead of one.
The specific code path that triggers it
- calls → JSC parser runs a full pass, builds an AST, returns true/false, discards the AST.
- Later, calls → JSC creates a new , re-parses the same source string via the same parser path, and compiles to bytecode.
There is no mechanism to pass any intermediate representation between these two calls.
Why existing code doesn't prevent it / addressing the refutation
The refutation suggests "JSC may have internal caching that mitigates the double-parse cost." JSC does have a that caches bytecode keyed by source. However, does NOT produce bytecode — it produces only a parsed AST that is immediately discarded. The sourceProviderCache is populated only by successful compilation (via or similar). Therefore, the cache cannot be populated by , and the second parse in is a full cold parse. The double-parse is confirmed real.
The refutation is correct that this is intentional and mirrors the pattern in NodeVM.cpp. This is a known tradeoff accepted by the PR author to achieve eager correctness checking. It is not a hidden defect.
What the impact would be
For valid scripts (the common case — invalid scripts throw at construction and are never run), every construction + execution now costs ~2× the parsing work compared to pre-fix Bun. For large scripts or hot paths that create many instances (template engines, test runners, repeated patterns), this is a measurable regression. Node.js/V8 avoids this entirely by eagerly compiling in the constructor (producing cacheable bytecode), so V8 does one compile and zero re-parses on execution. Pre-fix Bun did one parse (deferred to run time). This PR makes Bun do two parses for every valid script that gets executed.
How to fix it and step-by-step proof
The correct fix is to replace + discard with an eager compile that stores the resulting or its bytecode for reuse by subsequent calls — matching Node.js/V8 behavior. This is a larger change outside this PR's scope and is noted as a follow-up in the synthesis description. A concrete step-by-step showing the regression: (1) is called; (2) parses , builds AST, returns , AST is freed; (3) the object is created storing only the string; (4) is called; (5) creates a fresh , parses again from the source string, compiles to bytecode, runs it. Two parses for one script execution — where pre-fix Bun did one and Node.js does zero (after construction-time compile).
d6aa759 to
b0a60b7
Compare
b0a60b7 to
febc56a
Compare
| import { expect, test } from "bun:test"; | ||
| import { Script } from "node:vm"; |
There was a problem hiding this comment.
🟡 Per root CLAUDE.md (line 66) and test/CLAUDE.md (line 153), test/regression/issue/${issueNumber}.test.ts is reserved for true regressions — behavior that worked in a previous release and then broke. Issue #28666 is a Node.js-compat gap (vm.Script never eagerly checked syntax in any prior Bun release), so these five tests belong in test/js/node/vm/vm.test.ts, which this PR already modifies.
Extended reasoning...
What the issue is
Both governing CLAUDE.md files for this directory tree state the same rule:
CLAUDE.mdline 66: "test/regression/issue/${issueNumber}.test.tsis reserved for bugs that have a GitHub issue number and are true regressions (worked in a previous release, then broke). An issue number alone is not enough — if the behavior was never correct, it's not a regression and the test belongs in the existing file for that module."test/CLAUDE.mdline 153: "An issue number alone does not qualify — if it was never correct, put the test in the module's existing test file instead."
Issue #28666 is not a regression. The PR description itself states "The source was stored but only parsed at execution time" and "Bun silently created the Script object" — i.e., the eager syntax check never existed in any prior Bun release. The pre-PR assertion in vm.test.ts (createCachedData() throwing "createCachedData failed" rather than the constructor throwing) confirms the constructor never threw before. This is a never-implemented Node.js-compat behavior, not something that worked and then broke.
Why the convention matters here
The repo's documented intent is that compat-gap coverage lives alongside the module's other tests so future readers find all vm.Script behavior in one place, and so test/regression/issue/ remains a clean signal of "this once worked, don't let it break again." Filing compat gaps under regression/ dilutes that signal.
Addressing the objection
One reviewer noted that test/regression/issue/ already contains many compat-gap tests, suggesting loose enforcement, and that file placement is purely stylistic. That is true historically, but the rule is now explicitly documented in two CLAUDE.md files — including the very sentence "An issue number alone does not qualify" — which exists precisely to stop new instances of this pattern. Since this PR already touches test/js/node/vm/vm.test.ts, moving the five tests there is essentially zero-cost. This is not a functional defect, hence nit severity, but it is a documented convention being newly violated rather than a pre-existing one.
Step-by-step
test/regression/issue/28666.test.tsis created with 5 tests covering construction-timeSyntaxErrorbehavior.- Per
CLAUDE.md:66andtest/CLAUDE.md:153, this path is reserved for behavior that worked in a prior release and then broke. new Script(invalidSource)has never thrown at construction time in any released Bun — confirmed by the pre-PRvm.test.tsexpectation and the PR's own problem statement.- Therefore Bun does not throw when using invalid Syntax within
vm.Script#28666 is a compat gap, not a regression, and the tests belong intest/js/node/vm/vm.test.ts.
How to fix
Move the five test cases from test/regression/issue/28666.test.ts into test/js/node/vm/vm.test.ts (e.g., a describe("Script constructor syntax checking", ...) block near the existing "can't use export syntax in vm.Script" test) and delete the new regression file.
|
CI infrastructure has been unreliable on this PR — most recent build (#51555) has most jobs "Expired" due to Buildkite queue timeouts (not code failures). Previous builds had different Windows test runners flake with exit status 2, while other runners passed. Local verification:
The fix itself is correct and tested; awaiting stable CI run. |
vm.Script was deferring syntax checking to execution time. Node.js checks syntax at construction time and throws SyntaxError immediately. Add a checkSyntax call in the Script constructor to match. Fixes #28666
Constructor now throws SyntaxError before createCachedData is reached.
1a5e330 to
1a654fd
Compare
|
#28666 was fixed on main by #31827: This PR's Closing as already landed. |
Fixes #28666
Problem
vm.Scriptwas not throwingSyntaxErrorat construction time for invalid JavaScript syntax. The source was stored but only parsed at execution time (runInThisContext, etc.), deferring the error.Node.js eagerly compiles in the
Scriptconstructor, catching syntax errors immediately:Bun silently created the Script object and only threw when
runInThisContext()was called.Fix
Call
JSC::checkSyntax()inconstructScript()after creating theSourceCodebut before creating theNodeVMScriptobject. This follows the same pattern already used inNodeVM.cppforcompileFunction.Verification
USE_SYSTEM_BUN=1 bun test test/regression/issue/28666.test.ts→ 3 fail (bug present)bun bd test test/regression/issue/28666.test.ts→ 5 passbun bd test test/js/node/vm/vm.test.ts→ 198 pass, 0 fail