Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e2df61c
Fix null deref in Bun.sql/Bun.SQL lazy property callbacks
robobun May 4, 2026
c5c1f5c
Also fix Bun.$ PropertyCallback and forEachProperty with Proxy protot…
robobun May 4, 2026
f2190e1
[autofix.ci] apply automated fixes
autofix-ci[bot] May 4, 2026
6630bf6
Use test.concurrent.each for independent subprocess tests
robobun May 4, 2026
da65188
Retrigger CI (previous build had expired agents)
robobun May 4, 2026
aad7f3e
PropertyCallbacks: catch and clear exceptions instead of propagating
robobun May 4, 2026
4639d3c
Merge remote-tracking branch 'origin/main' into farm/8928bc04/fix-sql…
robobun May 12, 2026
e0ef811
Tighten 19650 regression test to assert positive stdout marker
robobun May 12, 2026
756fa7e
Merge remote-tracking branch 'origin/main' into farm/8928bc04/fix-sql…
robobun May 20, 2026
0e7406d
Merge remote-tracking branch 'origin/main' into farm/8928bc04/fix-sql…
robobun May 21, 2026
ae3fc29
Address review: fix get-trap test coverage, report Bun.redis init errors
robobun May 21, 2026
9dffcd1
Merge origin/main into farm/8928bc04/fix-sql-lazy-init-null-deref
robobun Jun 5, 2026
43f57e5
Merge origin/main into farm/8928bc04/fix-sql-lazy-init-null-deref
robobun Jul 12, 2026
10d35e0
test: add issue URL comment to 19650.test.ts
robobun Jul 12, 2026
bf98514
Merge remote-tracking branch 'origin/main' into farm/8928bc04/fix-sql…
robobun Aug 14, 2026
873cddc
test: Bun.redis getter is Rust-backed
robobun Aug 14, 2026
6ecdfcd
Shorten comments on the lazy property fold helpers
robobun Aug 14, 2026
5414053
Drop the Bun object lazy-init changes; JSC now propagates a throwing …
robobun Aug 14, 2026
230e894
test: drain stderr in the lazy property subprocess test
robobun Aug 14, 2026
2a20b8d
test: pin inspect output for the Proxy prototype cases; fix comment o…
robobun Aug 14, 2026
bafa86f
test: inspect keeps walking when a lazy property builder throws mid-walk
robobun Aug 15, 2026
bfb3414
test: load node:util before clobbering Symbol so Bun.env's custom ins…
robobun Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions src/jsc/bindings/BunObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,6 @@ static JSValue defaultBunSQLObject(VM& vm, JSObject* bunObject)
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(bunObject->globalObject());
JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql);
#if BUN_DEBUG
if (scope.exception()) globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception());
#endif
RETURN_IF_EXCEPTION(scope, {});
RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, vm.propertyNames->defaultKeyword));
}
Expand All @@ -332,9 +329,6 @@ static JSValue constructBunSQLObject(VM& vm, JSObject* bunObject)
auto scope = DECLARE_THROW_SCOPE(vm);
auto* globalObject = defaultGlobalObject(bunObject->globalObject());
JSValue sqlValue = globalObject->internalModuleRegistry()->requireId(globalObject, vm, InternalModuleRegistry::BunSql);
#if BUN_DEBUG
if (scope.exception()) globalObject->reportUncaughtExceptionAtEventLoop(globalObject, scope.exception());
#endif
RETURN_IF_EXCEPTION(scope, {});
auto clientData = WebCore::clientData(vm);
RELEASE_AND_RETURN(scope, sqlValue.getObject()->get(globalObject, clientData->builtinNames().SQLPublicName()));
Expand Down
10 changes: 7 additions & 3 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5616,10 +5616,11 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC:
}

JSC::PropertySlot slot(object, PropertySlot::InternalMethodType::Get);
if (!object->getPropertySlot(globalObject, property, slot))
continue;
bool found = object->getPropertySlot(globalObject, property, slot);
// Ignore exceptions from "Get" proxy traps.
CLEAR_IF_EXCEPTION(scope);
if (!found)
continue;

if ((slot.attributes() & PropertyAttribute::DontEnum) != 0) {
if (property == propertyNames->underscoreProto
Expand Down Expand Up @@ -5691,7 +5692,10 @@ static void JSC__JSValue__forEachPropertyImpl(JSC::EncodedJSValue JSValue0, JSC:
break;
if (iterating == globalObject)
break;
iterating = iterating->getPrototype(globalObject).getObject();
JSValue proto = iterating->getPrototype(globalObject);
// Ignore exceptions from Proxy "getPrototypeOf" trap.
CLEAR_IF_EXCEPTION(scope);
iterating = proto ? proto.getObject() : nullptr;
Comment thread
robobun marked this conversation as resolved.
}
}

Expand Down
37 changes: 37 additions & 0 deletions test/js/bun/util/BunObject.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { env } from "bun";
import { hasNonReifiedStatic } from "bun:internal-for-testing";
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("hasNonReifiedStatic", () => {
expect(hasNonReifiedStatic(Bun), "do not eagerly initialize the Bun object. This will make Bun much slower.").toBe(
true,
Expand Down Expand Up @@ -33,3 +34,39 @@ test("await import('bun')", async () => {
}
expect(BunESM.default).toBe(Bun);
});

test("a lazy property whose builtin fails to load throws from the read", async () => {
// The shell builtin ($) and bun:sql's module body (sql, SQL, postgres) call Symbol(), so
// clobbering it makes each builder throw. The read must throw that error and the slot
// must stay unreified so a later read runs the builder again.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`globalThis.Symbol = NaN;
const results = {};
for (const name of ["$", "sql", "SQL", "postgres"]) {
const names = [];
for (let i = 0; i < 2; i++) {
try { Bun[name]; names.push("no throw"); } catch (e) { names.push(e.constructor.name); }
}
results[name] = names;
}
console.log(JSON.stringify(results));`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({
stdout: JSON.stringify({
$: ["TypeError", "TypeError"],
sql: ["TypeError", "TypeError"],
SQL: ["TypeError", "TypeError"],
postgres: ["TypeError", "TypeError"],
}),
stderr: "",
exitCode: 0,
});
});
67 changes: 67 additions & 0 deletions test/js/bun/util/inspect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,73 @@ import { describe, expect, it } from "bun:test";
import { bunEnv, bunExe, isASAN, isWindows, normalizeBunSnapshot, tmpdirSync } from "harness";
import { join } from "path";
import util from "util";

it("Proxy prototype with throwing getPrototypeOf trap does not crash", () => {
const obj = {};
Object.setPrototypeOf(
obj,
new Proxy(
{},
{
getPrototypeOf() {
throw new Error("trap threw");
},
},
),
);
expect(Bun.inspect(obj)).toBe("{}");
});

it("Proxy prototype with throwing get trap does not crash", () => {
// A throwing `get` trap makes getPropertySlot return false with an exception
// pending; ownKeys/getOwnPropertyDescriptor make sure the names get enumerated
// so the walk reaches the trap.
const obj = {};
Object.setPrototypeOf(
obj,
new Proxy(
{},
{
ownKeys: () => ["x", "foo"],
getOwnPropertyDescriptor: () => ({ configurable: true, enumerable: true, value: 1 }),
get(_, p) {
if (p === "x") throw new Error("trap threw");
return 1;
},
},
),
);
expect(Bun.inspect(obj)).toBe("{\n foo: 1,\n}");
});

it("skips a lazy property whose builder throws and keeps walking", async () => {
// Same leak as the `get` trap above, without a Proxy: the Bun object's $ and
// sql builders throw once Symbol is clobbered, and the next property's builder
// must not be entered with that exception still pending. node:util is loaded
// first because Bun.env's custom inspect (Windows) needs it and it no longer
// loads once Symbol is clobbered.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`require("node:util");
globalThis.Symbol = NaN;
const lines = Bun.inspect(Bun).split("\\n").map(line => line.trim());
const has = key => lines.some(line => line.startsWith(key + ": "));
console.log(JSON.stringify({ $: has("$"), sql: has("sql"), Glob: has("Glob"), write: has("write") }));`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({
stdout: JSON.stringify({ $: false, sql: false, Glob: true, write: true }),
stderr: "",
exitCode: 0,
});
});

it("prototype", () => {
const prototypes = [
Request.prototype,
Expand Down
Loading