From 8476e0eb86f434d2a2148423d78fc6643ffb54f6 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 20 Mar 2026 20:46:45 +0000 Subject: [PATCH 1/7] Strip static hash table attribute bits before storing in PropertySlot/Structure PropertySlot::setValue asserts that attributes have no static hash table bits (bits 8+) and no Accessor flag. Two places passed raw slot.attributes() which could contain Function, DOMAttribute, or other high bits: - NodeVM.cpp: getOwnPropertySlot for 'globalThis' passed slot.attributes() to setValue, but the slot was freshly constructed with potentially stale attributes. Use 0 since globalThis is a plain value property. - JSMockFunction.cpp: spyOn saved and restored slot.attributes() without stripping static hash table bits via attributesForStructure(). When spying on properties from static hash tables (e.g. Bun.gc), the Function bit would persist through putDirect and later trigger the assertion. --- src/bun.js/bindings/JSMockFunction.cpp | 6 +++--- src/bun.js/bindings/NodeVM.cpp | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/bun.js/bindings/JSMockFunction.cpp b/src/bun.js/bindings/JSMockFunction.cpp index 1c760357186..648f9fa277e 100644 --- a/src/bun.js/bindings/JSMockFunction.cpp +++ b/src/bun.js/bindings/JSMockFunction.cpp @@ -1529,12 +1529,12 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSpyOn, (JSC::JSGlobalObject * lexicalGlobalOb auto* mock = JSMockFunction::create(vm, globalObject, globalObject->mockModule.mockFunctionStructure.getInitializedOnMainThread(globalObject), CallbackKind::GetterSetter); mock->spyTarget = JSC::Weak(object, &weakValueHandleOwner(), nullptr); mock->spyIdentifier = propertyKey.isSymbol() ? Identifier::fromUid(vm, propertyKey.uid()) : Identifier::fromString(vm, propertyKey.publicName()); - mock->spyAttributes = hasValue ? slot.attributes() : 0; + mock->spyAttributes = hasValue ? attributesForStructure(slot.attributes()) : 0; unsigned attributes = 0; if (hasValue && ((slot.attributes() & PropertyAttribute::Function) != 0 || (value.isCell() && value.isCallable()))) { if (hasValue) - attributes = slot.attributes(); + attributes = attributesForStructure(slot.attributes()); mock->copyNameAndLength(vm, globalObject, value); @@ -1553,7 +1553,7 @@ BUN_DEFINE_HOST_FUNCTION(JSMock__jsSpyOn, (JSC::JSGlobalObject * lexicalGlobalOb pushImpl(mock, globalObject, JSMockImplementation::Kind::Call, value); } else { if (hasValue) - attributes = slot.attributes(); + attributes = attributesForStructure(slot.attributes()); attributes |= PropertyAttribute::Accessor; diff --git a/src/bun.js/bindings/NodeVM.cpp b/src/bun.js/bindings/NodeVM.cpp index df81b0daecc..545a63d8cf6 100644 --- a/src/bun.js/bindings/NodeVM.cpp +++ b/src/bun.js/bindings/NodeVM.cpp @@ -906,7 +906,7 @@ bool NodeVMSpecialSandbox::getOwnPropertySlot(JSObject* cell, JSGlobalObject* gl if (propertyName.uid()->utf8() == "globalThis") [[unlikely]] { slot.disableCaching(); slot.setThisValue(thisObject); - slot.setValue(thisObject, slot.attributes(), thisObject); + slot.setValue(thisObject, 0, thisObject); return true; } @@ -932,7 +932,7 @@ bool NodeVMGlobalObject::getOwnPropertySlot(JSObject* cell, JSGlobalObject* glob if (notContextified && propertyName.uid()->utf8() == "globalThis") [[unlikely]] { slot.disableCaching(); slot.setThisValue(thisObject); - slot.setValue(thisObject, slot.attributes(), thisObject->specialSandbox()); + slot.setValue(thisObject, 0, thisObject->specialSandbox()); return true; } From 6e085daa6b3f8be5bd41cf5e6fcb32be54c33274 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 20 Mar 2026 22:10:31 +0000 Subject: [PATCH 2/7] Add regression test for spyOn on static hash table properties --- .../js/bun/test/spyon-static-property.test.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 test/js/bun/test/spyon-static-property.test.ts diff --git a/test/js/bun/test/spyon-static-property.test.ts b/test/js/bun/test/spyon-static-property.test.ts new file mode 100644 index 00000000000..27ac2f62642 --- /dev/null +++ b/test/js/bun/test/spyon-static-property.test.ts @@ -0,0 +1,59 @@ +import { test, expect } from "bun:test"; +import { bunExe, bunEnv } from "harness"; + +test("spyOn works on static hash table function properties", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", ` + const { spyOn, jest } = Bun.jest(import.meta.path); + const spy = spyOn(Bun, "gc"); + Bun.gc(true); + if (spy.mock.calls.length !== 1) { + throw new Error("Expected 1 call, got " + spy.mock.calls.length); + } + spy.mockRestore(); + Bun.gc(true); + console.log("OK"); + `], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); +}); + +test("spyOn preserves correct attributes after mockRestore", async () => { + await using proc = Bun.spawn({ + cmd: [bunExe(), "-e", ` + const { spyOn, jest } = Bun.jest(import.meta.path); + const spy = spyOn(Bun, "peek"); + spy.mockRestore(); + // After restore, the property should still work + const p = Promise.resolve(42); + const val = Bun.peek(p); + if (val !== 42) { + throw new Error("Expected 42, got " + val); + } + console.log("OK"); + `], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + + expect(stdout.trim()).toBe("OK"); + expect(exitCode).toBe(0); +}); From 2834d499c0ed22dcca64dc417ecbfc18699cd257 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:12:34 +0000 Subject: [PATCH 3/7] [autofix.ci] apply automated fixes --- .../js/bun/test/spyon-static-property.test.ts | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/js/bun/test/spyon-static-property.test.ts b/test/js/bun/test/spyon-static-property.test.ts index 27ac2f62642..d4eb445a86b 100644 --- a/test/js/bun/test/spyon-static-property.test.ts +++ b/test/js/bun/test/spyon-static-property.test.ts @@ -1,9 +1,12 @@ -import { test, expect } from "bun:test"; -import { bunExe, bunEnv } from "harness"; +import { expect, test } from "bun:test"; +import { bunEnv, bunExe } from "harness"; test("spyOn works on static hash table function properties", async () => { await using proc = Bun.spawn({ - cmd: [bunExe(), "-e", ` + cmd: [ + bunExe(), + "-e", + ` const { spyOn, jest } = Bun.jest(import.meta.path); const spy = spyOn(Bun, "gc"); Bun.gc(true); @@ -13,17 +16,14 @@ test("spyOn works on static hash table function properties", async () => { spy.mockRestore(); Bun.gc(true); console.log("OK"); - `], + `, + ], env: bunEnv, stdout: "pipe", stderr: "pipe", }); - const [stdout, stderr, exitCode] = await Promise.all([ - proc.stdout.text(), - proc.stderr.text(), - proc.exited, - ]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); @@ -31,7 +31,10 @@ test("spyOn works on static hash table function properties", async () => { test("spyOn preserves correct attributes after mockRestore", async () => { await using proc = Bun.spawn({ - cmd: [bunExe(), "-e", ` + cmd: [ + bunExe(), + "-e", + ` const { spyOn, jest } = Bun.jest(import.meta.path); const spy = spyOn(Bun, "peek"); spy.mockRestore(); @@ -42,17 +45,14 @@ test("spyOn preserves correct attributes after mockRestore", async () => { throw new Error("Expected 42, got " + val); } console.log("OK"); - `], + `, + ], env: bunEnv, stdout: "pipe", stderr: "pipe", }); - const [stdout, stderr, exitCode] = await Promise.all([ - proc.stdout.text(), - proc.stderr.text(), - proc.exited, - ]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); From 72de0670049e75274934c5413b6bae496b58e3b3 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 20 Mar 2026 22:17:32 +0000 Subject: [PATCH 4/7] Add stderr assertions to test --- test/js/bun/test/spyon-static-property.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/js/bun/test/spyon-static-property.test.ts b/test/js/bun/test/spyon-static-property.test.ts index d4eb445a86b..ddaf94a4035 100644 --- a/test/js/bun/test/spyon-static-property.test.ts +++ b/test/js/bun/test/spyon-static-property.test.ts @@ -25,6 +25,7 @@ test("spyOn works on static hash table function properties", async () => { const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); }); @@ -54,6 +55,7 @@ test("spyOn preserves correct attributes after mockRestore", async () => { const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); expect(stdout.trim()).toBe("OK"); expect(exitCode).toBe(0); }); From fb78cf3f8ae65648cf5b4a18c24922859bc0ebc3 Mon Sep 17 00:00:00 2001 From: robobun Date: Fri, 20 Mar 2026 22:52:29 +0000 Subject: [PATCH 5/7] Use test.concurrent for independent subprocess tests --- test/js/bun/test/spyon-static-property.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/js/bun/test/spyon-static-property.test.ts b/test/js/bun/test/spyon-static-property.test.ts index ddaf94a4035..2bdc2a378d3 100644 --- a/test/js/bun/test/spyon-static-property.test.ts +++ b/test/js/bun/test/spyon-static-property.test.ts @@ -1,7 +1,7 @@ import { expect, test } from "bun:test"; import { bunEnv, bunExe } from "harness"; -test("spyOn works on static hash table function properties", async () => { +test.concurrent("spyOn works on static hash table function properties", async () => { await using proc = Bun.spawn({ cmd: [ bunExe(), @@ -30,7 +30,7 @@ test("spyOn works on static hash table function properties", async () => { expect(exitCode).toBe(0); }); -test("spyOn preserves correct attributes after mockRestore", async () => { +test.concurrent("spyOn preserves correct attributes after mockRestore", async () => { await using proc = Bun.spawn({ cmd: [ bunExe(), From e56ad3bbef8d4afc58982c884a62e18928c315ab Mon Sep 17 00:00:00 2001 From: robobun Date: Sat, 21 Mar 2026 02:08:25 +0000 Subject: [PATCH 6/7] Simplify test to use bun:test spyOn directly instead of subprocess --- .../js/bun/test/spyon-static-property.test.ts | 71 ++++--------------- 1 file changed, 14 insertions(+), 57 deletions(-) diff --git a/test/js/bun/test/spyon-static-property.test.ts b/test/js/bun/test/spyon-static-property.test.ts index 2bdc2a378d3..1aa2ece77ba 100644 --- a/test/js/bun/test/spyon-static-property.test.ts +++ b/test/js/bun/test/spyon-static-property.test.ts @@ -1,61 +1,18 @@ -import { expect, test } from "bun:test"; -import { bunEnv, bunExe } from "harness"; +import { expect, test, spyOn } from "bun:test"; -test.concurrent("spyOn works on static hash table function properties", async () => { - await using proc = Bun.spawn({ - cmd: [ - bunExe(), - "-e", - ` - const { spyOn, jest } = Bun.jest(import.meta.path); - const spy = spyOn(Bun, "gc"); - Bun.gc(true); - if (spy.mock.calls.length !== 1) { - throw new Error("Expected 1 call, got " + spy.mock.calls.length); - } - spy.mockRestore(); - Bun.gc(true); - console.log("OK"); - `, - ], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - - expect(stderr).toBe(""); - expect(stdout.trim()).toBe("OK"); - expect(exitCode).toBe(0); +test("spyOn works on static hash table function properties", () => { + const spy = spyOn(Bun, "gc"); + try { + Bun.gc(true); + expect(spy).toHaveBeenCalledTimes(1); + } finally { + spy.mockRestore(); + } }); -test.concurrent("spyOn preserves correct attributes after mockRestore", async () => { - await using proc = Bun.spawn({ - cmd: [ - bunExe(), - "-e", - ` - const { spyOn, jest } = Bun.jest(import.meta.path); - const spy = spyOn(Bun, "peek"); - spy.mockRestore(); - // After restore, the property should still work - const p = Promise.resolve(42); - const val = Bun.peek(p); - if (val !== 42) { - throw new Error("Expected 42, got " + val); - } - console.log("OK"); - `, - ], - env: bunEnv, - stdout: "pipe", - stderr: "pipe", - }); - - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - - expect(stderr).toBe(""); - expect(stdout.trim()).toBe("OK"); - expect(exitCode).toBe(0); +test("spyOn preserves correct attributes after mockRestore", () => { + const spy = spyOn(Bun, "peek"); + spy.mockRestore(); + const p = Promise.resolve(42); + expect(Bun.peek(p)).toBe(42); }); From b35c00109a0f09ebacb3b5addb7b202ce546595a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 02:10:11 +0000 Subject: [PATCH 7/7] [autofix.ci] apply automated fixes --- test/js/bun/test/spyon-static-property.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/bun/test/spyon-static-property.test.ts b/test/js/bun/test/spyon-static-property.test.ts index 1aa2ece77ba..55db6a52890 100644 --- a/test/js/bun/test/spyon-static-property.test.ts +++ b/test/js/bun/test/spyon-static-property.test.ts @@ -1,4 +1,4 @@ -import { expect, test, spyOn } from "bun:test"; +import { expect, spyOn, test } from "bun:test"; test("spyOn works on static hash table function properties", () => { const spy = spyOn(Bun, "gc");