From 776a00d5645519a5b843f075a881f5a2f9dec7b0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:03:49 +0000 Subject: [PATCH] node:vm: allow a Proxy in the prototype chain of a global object JavaScriptCore rejects a program when any link of the global object's prototype chain is a Proxy. Every Bun global clears IsImmutablePrototypeExoticObject from its structure, so user code can set that prototype, and jsdom 28+ puts a Proxy in the chain of its window. Opt every Bun global out of the check, as V8 permits it. This needs oven-sh/WebKit#626. --- scripts/build/deps/webkit.ts | 2 +- src/jsc/bindings/BunGlobalScope.cpp | 5 +++ test/js/node/vm/vm.test.ts | 59 ++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 359f8a466e32..eb9d21630866 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -3,7 +3,7 @@ * for local mode. Override via `--webkit-version=` to test a branch. * From https://github.com/oven-sh/WebKit releases. */ -export const WEBKIT_VERSION = "cf1b36ec8703d8e87436094d21d478d358c7d886"; +export const WEBKIT_VERSION = "autobuild-preview-pr-626-f1e23f97"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/jsc/bindings/BunGlobalScope.cpp b/src/jsc/bindings/BunGlobalScope.cpp index 694e720fe5f2..b576f6b9f5fd 100644 --- a/src/jsc/bindings/BunGlobalScope.cpp +++ b/src/jsc/bindings/BunGlobalScope.cpp @@ -18,6 +18,11 @@ void GlobalScope::finishCreation(JSC::VM& vm) Base::finishCreation(vm); ASSERT(inherits(info())); + // Every Bun global clears IsImmutablePrototypeExoticObject from its structure, so user + // code can put a Proxy in its prototype chain (jsdom does, for its window). V8 runs + // programs against such a chain, so JSC must not reject it. + setAllowsProxyInPrototypeChain(true); + m_encodeIntoObjectStructure.initLater( [](const JSC::LazyProperty::Initializer& init) { auto& vm = init.vm; diff --git a/test/js/node/vm/vm.test.ts b/test/js/node/vm/vm.test.ts index c32e0253f5da..50f33767915d 100644 --- a/test/js/node/vm/vm.test.ts +++ b/test/js/node/vm/vm.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe, isWindows, normalizeBunSnapshot } from "harness"; +import { bunEnv, bunExe, isWindows, normalizeBunSnapshot, tempDir } from "harness"; import { compileFunction, constants, @@ -1318,6 +1318,63 @@ describe("DONT_CONTEXTIFY", () => { }); }); +// https://github.com/oven-sh/bun/issues/42331 +describe("a Proxy in the prototype chain of the context global", () => { + // jsdom 28+ puts a Proxy (WindowProperties) in the prototype chain of its + // window. V8 runs a program against such a global, so node:vm must too. + function installProxy(ctx: object) { + const target = { fromProxy: 7 }; + const g = runInContext("this", ctx); + Object.setPrototypeOf(g, Object.create(new Proxy(target, {}))); + return target; + } + + test.each([ + ["DONT_CONTEXTIFY", () => createContext(constants.DONT_CONTEXTIFY)], + ["contextified sandbox", () => createContext({})], + ])("%s: programs run and resolve names through the Proxy", (_, makeContext) => { + const ctx = makeContext(); + const target = installProxy(ctx); + + expect(runInContext("1 + 1", ctx)).toBe(2); + expect(new Script("2 + 2").runInContext(ctx)).toBe(4); + expect(runInContext("fromProxy", ctx)).toBe(7); + expect(runInContext("typeof missingName", ctx)).toBe("undefined"); + expect(runInContext("undeclaredAssign = 9; undeclaredAssign", ctx)).toBe(9); + expect(runInContext("fromProxy = 8; fromProxy", ctx)).toBe(8); + expect(target.fromProxy).toBe(7); + }); + + test("contextified sandbox: declarations land on the global, not on the Proxy", () => { + const ctx = createContext({}); + const target = installProxy(ctx); + + expect(runInContext("var declared = 5; function fn() { return 6 } declared + fn()", ctx)).toBe(11); + expect(new Script("declared * 2").runInContext(ctx)).toBe(10); + expect(target).toEqual({ fromProxy: 7 }); + }); + + test("main realm: runInThisContext and require() run with a Proxy in the chain of globalThis", async () => { + using dir = tempDir("vm-proxy-global", { "dep.cjs": "module.exports = 42;" }); + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const vm = require("node:vm"); + Object.setPrototypeOf(globalThis, Object.create(new Proxy({ viaProxy: 3 }, {}))); + console.log(vm.runInThisContext("1 + 1"), vm.runInThisContext("viaProxy"), require("./dep.cjs"));`, + ], + env: bunEnv, + cwd: String(dir), + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(stdout).toBe("2 3 42\n"); + expect(exitCode).toBe(0); + }); +}); + describe("defineProperty errors use vm-realm global", () => { test("data descriptor on sandbox-only property", () => { const sandbox = {};