Skip to content

node:vm: reject functions as the context argument like Node - #38414

Open
robobun wants to merge 1 commit into
mainfrom
farm/8d214d7e/vm-context-arg-rejects-functions
Open

robobun wants to merge 1 commit into
mainfrom
farm/8d214d7e/vm-context-arg-rejects-functions

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • vm.isContext(function () {}) returns false, vm.createContext(fn) contextifies the function, and new vm.Script("1").runInNewContext(fn) runs. Node throws TypeError [ERR_INVALID_ARG_TYPE]: The "object" argument must be of type object. Received function from all of them.
  • vm.runInContext(), vm.runInNewContext() and Script#runInContext() do reject a function, but with the second-tier error (The "contextifiedObject" argument must be an vm.Context) instead of the type error above.
  • Cause: the native checks use JSValue::isObject(), which is true for functions: vmModule_isContext (src/jsc/bindings/NodeVM.cpp:1718), vmModule_createContext (NodeVM.cpp:1657), scriptRunInNewContext (src/jsc/bindings/NodeVMScript.cpp:636) and getGlobalObjectFromContext (NodeVM.cpp:759, used by Script#runInContext). src/js/node/vm.ts does not need changes: its validateContext() delegates to the native isContext.
  • The same sites also differ from Node for non-objects: createContext(1) and script.runInContext(1) name the argument "context" (Node: "object"), and script.runInNewContext(1) throws a plain TypeError: Context must be an object with no code.

Fix

  • Adds NodeVM::validateContextArg(): rejects anything that is not an object or that is callable, with ERR_INVALID_ARG_TYPE for "object", and uses it at the four sites. getGlobalObjectFromContext() applies it on its throwing path (Script#runInContext); its non-throwing callers (the Module classes, which pass an already validated context or the main global) keep returning null for non-objects as before.
  • Matches Node: lib/vm.js isContext() validates with validateObject(object, 'object', kValidateObjectAllowArray), and createContext(), runInContext(), runInNewContext() and Script#runInContext()/runInNewContext() all validate the context through isContext(). isObject() && !isCallable() is typeof x === "object" && x !== null, which is exactly what that validator accepts: arrays still pass, functions, null and primitives do not.
  • vm.constants.DONT_CONTEXTIFY and undefined are still turned into a fresh sandbox by getContextArg() before the check runs, so those paths are unchanged (covered by the new tests and the existing DONT_CONTEXTIFY block).
  • The native vmModuleRunInNewContext has the same isObject() check but is not reachable (vm.ts implements runInNewContext itself) and is removed by node:vm: run runInNewContext() in the sandbox's existing context #38326, so it is left alone.
  • Tests: test/js/node/vm/vm.test.ts, "the context argument": for each of the six entry points, rejection of function declarations, anonymous/arrow/async functions, classes, bound functions and callable Proxies plus null and primitives with Node's exact message (checked against node v26.3.0), arrays/undefined/DONT_CONTEXTIFY still contextifying, a contextified array being accepted everywhere, and non-context objects still getting the vm.Context error. 58 of the new cases fail on the released binary, all pass with the fix.
  • Also run with the debug build: the rest of test/js/node/vm/, all 97 test/js/node/test/parallel/test-vm-* files (97 pass), and the new block plus test-vm-context.js/test-vm-is-context.js/test-vm-create-context-arg.js/test-vm-basic.js under BUN_JSC_validateExceptionChecks=1.

Background

  • A vm "context" is an ordinary object (the sandbox) that createContext() has associated with its own NodeVMGlobalObject realm; Bun records the association in a WeakMap on the main global, and isContext() is a lookup in that map. Contextifying a function would therefore "work" mechanically, which is why nothing crashed; it is simply an input Node rejects.
  • Node's validateObject flags: the default rejects null, arrays and functions; kValidateObjectAllowArray (used by isContext) additionally lets arrays through, so createContext([]) is valid and must stay valid.
  • getContextArg() is the shared pre-step for createContext()/Script#runInNewContext(): it replaces undefined and the DONT_CONTEXTIFY symbol with a fresh object, which is why the new check runs after it.
  • Two related cosmetic differences are intentionally not touched here and are tracked separately: how a bound function's name is rendered in the Received ... suffix (the new tests use a prefix match for those cases), and the missing Received ... suffix on the vm.Context error.

isContext(), createContext(), Script#runInNewContext() and
Script#runInContext() checked the context argument with
JSValue::isObject(), which is true for functions, so a function could be
contextified or silently reported as "not a context". Node validates the
argument in isContext() with validateObject(object, "object",
kValidateObjectAllowArray), which every other entry point goes through.

Add NodeVM::validateContextArg() implementing that check and use it at
the four native sites. This also gives createContext() and
Script#runInContext() Node's argument name ("object" instead of
"context") for primitives, and turns Script#runInNewContext()'s plain
TypeError ("Context must be an object") into the same ERR_INVALID_ARG_TYPE.
@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: 40 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: 61bb893a-06b1-4cf3-9336-ac941e65c62d

📥 Commits

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

📒 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 bun 1.4.0 and main with vm.isContext(function () {}) (returns false; node v26.3.0 throws ERR_INVALID_ARG_TYPE), vm.createContext(fn) (contextifies it) and new vm.Script("1").runInNewContext(fn) (runs). Fix is in this PR; the new "the context argument" block in test/js/node/vm/vm.test.ts has 58 cases failing on the released binary and all passing with the fix, and the 97 ported test-vm-* node tests still pass with the debug build.

Review so far has no findings to address; waiting on CI.

@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 — small, well-scoped Node compat fix.

What was reviewed

  • validateContextArg(): !isObject() || isCallable() correctly maps to Node's validateObject(..., kValidateObjectAllowArray) (arrays pass, functions/null/primitives rejected); isCallable() only reached when isObject() is true.
  • All four call sites: RETURN_IF_EXCEPTION after each call; downstream asObject(contextArg) remains safe since validation guarantees isObject().
  • getGlobalObjectFromContext non-throwing path (Module classes with pre-validated m_context) unchanged — still returns nullptr for non-objects without throwing.
  • src/js/node/vm.ts validateContext()/runInNewContext() route through native isContext, so the JS-level entry points inherit the fix without changes.
Extended reasoning...

Overview

Adds NodeVM::validateContextArg() (rejects non-objects and callables with ERR_INVALID_ARG_TYPE for argument name "object") and applies it at four native sites: vmModule_isContext, vmModule_createContext, scriptRunInNewContext, and the throwing branch of getGlobalObjectFromContext. Changes the argument name from "context" to "object" and replaces a plain TypeError in scriptRunInNewContext with the coded error, matching Node's lib/vm.js which routes every context argument through isContext() → validateObject(object, 'object', kValidateObjectAllowArray). ~25 lines of production change plus a ~100-line test block.

Security risks

None. This tightens input validation on a Node-compat surface; no new code paths execute user input, and rejecting functions where they were previously accepted only reduces the input space.

Level of scrutiny

Low-to-medium. Mechanical extraction of a shared validator applied at four sites, each already had an isObject() check that is being replaced/augmented. The predicate isObject() && !isCallable() is exactly typeof x === 'object' && x !== null, which is what Node's kValidateObjectAllowArray accepts. Checked that:

  • getContextArg() still runs first at the two contextifying sites, so undefined/DONT_CONTEXTIFY become a fresh object before validation (covered by tests).
  • The canThrow=false callers of getGlobalObjectFromContext (NodeVMModule/NodeVMSourceTextModule/NodeVMSyntheticModule, all passing an internal m_context) keep their prior silent-nullptr behavior.
  • Downstream asObject() calls are guarded by the new validation.
  • Exception scopes: every call site has RETURN_IF_EXCEPTION immediately after; PR states it was run under BUN_JSC_validateExceptionChecks=1.
  • The unused ArgList args in vmModule_isContext was correctly dead (callFrame->argument(0) is used directly).

Other factors

Test coverage is thorough for a validation change: 6 entry points × 12 rejected shapes (including bound functions, callable Proxies, classes) with exact Node v26.3.0 messages, plus positive cases (arrays contextify, contextified array works everywhere, non-context objects still get the second-tier vm.Context error). PR description confirms 58 new cases fail on the released binary and pass with the fix, and that all 97 ported test-vm-* Node tests still pass. The one intentionally-skipped sibling (vmModuleRunInNewContext, unreachable and being deleted in #38326) is called out explicitly.

@robobun

robobun commented Aug 14, 2026 •

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

@robobun, your commit 0760414 is building: #95656

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