Skip to content

node:vm: read contextName/contextOrigin in Script#runInNewContext() instead of name/origin - #38409

Open
robobun wants to merge 1 commit into
mainfrom
farm/843896a6/vm-run-in-new-context-context-name-origin
Open

robobun wants to merge 1 commit into
mainfrom
farm/843896a6/vm-run-in-new-context-context-name-origin

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • new vm.Script("1").runInNewContext({}, { contextName: 1 }) runs and returns 1; Node throws ERR_INVALID_ARG_TYPE: The "options.contextName" property must be of type string. Received type number (1). Same for contextOrigin.
  • new vm.Script("1").runInNewContext({}, { name: 1 }) throws The "options.name" property must be of type string; Node runs it, name/origin are not runInNewContext() options. Same for origin.
  • Cause: getNodeVMContextOptions() (src/jsc/bindings/NodeVM.cpp:659, :669) always reads the name/origin keys, which are createContext()'s spelling, but it is also the option parser for Script#runInNewContext() (src/jsc/bindings/NodeVMScript.cpp:645), where Node reads contextName/contextOrigin instead. Only the codeGeneration key was switched per caller.
  • The contextName/contextOrigin check that made vm.runInNewContext() appear to work was in ScriptOptions::fromJS() (src/jsc/bindings/NodeVMScript.cpp:35), the Script constructor's option parser. Node's Script constructor has no such check, so new vm.Script("1", { contextName: 1 }), vm.runInThisContext("1", { contextName: 1 }) and vm.runInContext("1", ctx, { contextName: 1 }) threw in Bun and run in Node.

Fix

  • getNodeVMContextOptions() takes a NodeVMContextOptionKeys (name, origin, codeGeneration key names) instead of just the codeGeneration key. createContext() passes { name, origin, codeGeneration }, the two runInNewContext() entry points pass { contextName, contextOrigin, contextCodeGeneration }; error messages are built from the key that was read.
  • ScriptOptions::fromJS() no longer reads contextName/contextOrigin. Script#runInNewContext() validates them itself now, so vm.runInNewContext() (which calls it) still rejects them with the same message, and the constructor / runInThisContext() / runInContext() ignore them like Node.
  • Why this is right: it matches Node's lib/vm.js, where createContext() reads name/origin and getContextOptions() (used by runInNewContext()) reads contextName/contextOrigin, and nothing else reads either pair. Every expectation in the new tests was run against node v26.3.0 as well.
  • Test: test/js/node/vm/vm.test.ts, context name/origin options (9 of its 17 tests fail on the unfixed binary), plus the throwing-getter matrix in the same file now lists the keys each entry point reads.
  • All 97 test/js/node/test/parallel/test-vm-* files pass with the debug build (test-vm-basic.js covers vm.runInNewContext('', {}, { contextName: null })).
  • vm.runInNewContext() still passes its raw options to createContext(), so { name: 1 } through that wrapper still throws; that wrapper-level remap is what node:vm: run runInNewContext() in the sandbox's existing context #38326 adds, and it composes with this change (its native Script#runInNewContext() path still goes through getNodeVMContextOptions()).

Background

  • node:vm has two ways to configure a context. vm.createContext(sandbox, options) takes { name, origin, codeGeneration, microtaskMode }. vm.runInNewContext() and Script#runInNewContext() create a context and run in one call, so they take those same options on the combined options object under the names contextName, contextOrigin, contextCodeGeneration (and microtaskMode), next to the script/run options like filename and timeout. Node's lib/vm.js getContextOptions() does this remap and validates under the options.contextName spelling.
  • In Bun both entry points share the native parser getNodeVMContextOptions(); the name/origin values are only validated (JSC has no use for a context name), so the bug is purely about which key is checked and which message is thrown.
  • ScriptOptions::fromJS() parses the options given to new vm.Script(). Bun's JS wrappers (vm.runInNewContext() etc.) hand the same options object to new Script() and then to the run method, which is how a check in the constructor could stand in for the missing one in runInNewContext().
Behavior matrix, node v26.3.0 vs bun before / after
case                                                  node      bun before                 bun after
Script#runInNewContext({}, { name: 1 })               runs      throws options.name        runs
Script#runInNewContext({}, { origin: 1 })             runs      throws options.origin      runs
Script#runInNewContext({}, { contextName: 1 })        throws    runs                       throws options.contextName
Script#runInNewContext({}, { contextOrigin: 1 })      throws    runs                       throws options.contextOrigin
Script#runInNewContext(existingCtx, { contextName: 1 }) throws  runs                       throws
Script#runInNewContext(DONT_CONTEXTIFY, { contextName: 1 }) throws runs                    throws
vm.runInNewContext("1", {}, { contextName: 1 })       throws    throws (from new Script)   throws (from runInNewContext)
new Script("1", { contextName: 1 })                   ok        throws                     ok
vm.runInThisContext("1", { contextName: 1 })          runs      throws                     runs
vm.runInContext("1", ctx, { contextName: 1 })         runs      throws                     runs
createContext({}, { name: 1 })                        throws    throws                     throws (unchanged)
createContext({}, { contextName: 1 })                 ok        ok                         ok (unchanged)

…t name/origin

getNodeVMContextOptions() always read the name and origin keys, which are
createContext()'s spelling. Script#runInNewContext() takes the same options
as contextName/contextOrigin (like contextCodeGeneration, which was already
switched per caller), so it rejected name/origin and ignored
contextName/contextOrigin. Replace the per-caller codeGeneration key with a
per-caller key set covering all three.

The contextName/contextOrigin check that compensated for this lived in the
Script constructor's option parser, where Node has none; it made
new Script(), vm.runInThisContext() and vm.runInContext() reject those keys.
Remove it now that runInNewContext() validates them itself.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d24f6dc0-fa91-4da9-82a9-ed60f98a729b

📥 Commits

Reviewing files that changed from the base of the PR and between 01aa7cd and b7ccc51.

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

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced on main (debug build) and bun 1.4.0 with new vm.Script("1").runInNewContext({}, { contextName: 1 }) running instead of throwing, and { name: 1 } throwing instead of running; node v26.3.0 does the opposite in both cases. The matrix in the PR description was run against node v26.3.0 and against this branch's debug build.

Fix is in this PR. Tests: test/js/node/vm/vm.test.ts ("context name/origin options"; 9 of its 17 cases fail without the src change), and all 97 test/js/node/test/parallel/test-vm-* files pass with the debug build.

@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.

LGTM — mechanical extension of the existing codeGenerationKey parameterization to cover name/origin, plus removal of misplaced validation from the Script constructor.

What was reviewed:

  • All three getNodeVMContextOptions() call sites updated with the correct key set; ERR::INVALID_ARG_TYPE has a WTF::String overload so makeString("options."_s, keys.name) is fine.
  • Removed contextName/contextOrigin block in ScriptOptions::fromJS() only validated (never stored), so no downstream consumer loses a value.
  • Traced the JS wrapper vm.runInNewContext → createContext + Script#runInNewContext to confirm the updated throwing-getter matrix still asserts the right key per entry point.
  • inline constexpr ASCIILiteral aggregate matches existing usage in the file.
Extended reasoning...

Overview

This PR fixes which option keys getNodeVMContextOptions() reads depending on its caller. Node's vm.createContext() takes { name, origin, codeGeneration } while Script#runInNewContext() and vm.runInNewContext() take { contextName, contextOrigin, contextCodeGeneration }. Bun's shared native parser previously hard-coded name/origin for both callers (only codeGeneration was already parameterized). The fix generalizes the existing single-key parameter to a three-key struct NodeVMContextOptionKeys, and removes a compensating-but-misplaced contextName/contextOrigin check from the Script constructor's option parser (ScriptOptions::fromJS), where Node has no such check. Files touched: NodeVM.h (struct + two inline constexpr instances + forward decl + signature), NodeVM.cpp (function body + two call sites), NodeVMScript.cpp (removed block + one call site), and vm.test.ts (new context name/origin options describe block + updated throwing-getter matrix).

Security risks

None. The name/origin values are only validated, never stored or used — JSC has no context-name concept, so this is purely about which key is read and which error message is thrown. No auth, crypto, permissions, or memory-safety-relevant code paths are touched. The change is pure option-name plumbing over compile-time ASCIILiteral constants.

Level of scrutiny

Low-to-moderate. This is a Node.js compat fix that follows the exact pattern already in place for codeGenerationKey — the diff is mostly codeGenerationKey → keys.codeGeneration, "name"_s → keys.name, "origin"_s → keys.origin, plus building error-message strings with makeString (I confirmed ERR::INVALID_ARG_TYPE has a const WTF::String& overload for arg_name at ErrorCode.h:89). The removed ScriptOptions::fromJS block only did validation and set any = true; it never wrote to a field, so removing it can't strand a downstream consumer. I grepped for all getNodeVMContextOptions call sites and confirmed all three are updated with the right key set.

Other factors

Test coverage is thorough: the new describe block covers every entry point (Script#runInNewContext, vm.runInNewContext, createContext, new Script, runInThisContext, runInContext, Script#runInContext/#runInThisContext), both key spellings, three non-string value types, validation ordering vs Node, sandbox variants (undefined/existing context/DONT_CONTEXTIFY), and the negative contract via throwing getters (unreadable()). The existing throwing-getter subprocess matrix was updated to reflect the new key set each entry point reads; I traced vm.runInNewContext through src/js/node/vm.ts to confirm the listed keys (contextName/contextOrigin/contextCodeGeneration/importModuleDynamically/microtaskMode) each throw with the expected message on that path. The PR description states 9/17 new tests fail on the unfixed binary and all 97 test-vm-* Node parallel tests pass, and notes composition with #38326 for the remaining wrapper-level { name: 1 } divergence. No outstanding reviewer comments.

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:05 AM PT - Aug 14th, 2026

❌ @robobun, your commit b7ccc51 has some failures in Build #95649 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 38409

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

bun-38409 --bun

@robobun

robobun commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Checked against #39588 on a build of its branch: not covered, so this still applies. #39588 validates contextName / contextOrigin in the vm.runInNewContext() wrapper only. The native parser behind Script#runInNewContext() still reads name / origin, and the Script constructor still rejects contextName / contextOrigin, so every row of the matrix in this description prints the same result on that branch as on main, and 8 of the tests from this PR fail there.

This branch has not been deployed

No deployments
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.

1 participant