Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "ceb9f90fb774fdb1ebf1275ae1aaf136ec66c754";
// Preview of oven-sh/WebKit#475 (on top of ceb9f90f, the previous pin here): a property
// lookup stops at a static-table lazy property whose builder threw, instead of walking on
// to the prototype and, on the megamorphic slow paths, recording the property as missing,
// and reifyAllStaticProperties checks each builder's exception scope. Swap in the merged
// sha once that PR lands.
export const WEBKIT_VERSION = "autobuild-preview-pr-475-94c5a2d5";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
145 changes: 138 additions & 7 deletions test/js/bun/util/BunObject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,14 @@ test("a lazy property whose builtin fails to load throws from the read", async (
// The shell builtin ($) and the sql module body (sql, SQL, postgres) call Symbol(), so
// breaking it makes each builder throw. The read must throw that error (debug builds used to
// report the still-pending exception from inside the sql builders and abort) and the slot
// must stay unreified so a later read runs the builder again.
//
// process.env is read first because the shell builtin reads it before calling Symbol(), and
// building it on Windows reifies another property of the Bun object; doing that in the middle
// of the throwing read trips a separate structure assertion in debug builds.
// must stay unreified so a later read runs the builder again. On Windows the shell builtin also
// builds process.env before it calls Symbol(), which reifies Bun.inspect, so there the throwing
// read of $ transitions the Bun object mid-lookup as well: the case the next test sets up by hand.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`process.env;
globalThis.Symbol = NaN;
`globalThis.Symbol = NaN;
const results = {};
for (const name of ["$", "sql", "SQL", "postgres"]) {
results[name] = [];
Expand All @@ -75,3 +72,137 @@ test("a lazy property whose builtin fails to load throws from the read", async (
exitCode: 0,
});
});

test.concurrent("a lazy property builder that reifies another property of Bun and then throws", async () => {
// The sql module body reads Error.prototype (class ... extends Error). The proxy makes that read
// reify Bun.semver, which transitions the Bun object while the Bun.sql lookup is still running,
// and then throw. Debug builds used to abort in the lookup's prototype step, which still held
// the structure from before the transition (ASSERT in Structure::storedPrototype); the lookup now
// ends as soon as the builder has thrown. "phase" 1 proves the trap ran: if the sql module stops
// reading Error.prototype while it is evaluated, this test needs a new trigger.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const RealError = Error;
let phase = 0;
globalThis.Error = new Proxy(function () {}, {
get(target, key, receiver) {
if (key === "prototype" && phase === 0) {
phase = 1;
Bun.semver;
throw "boom";
}
return Reflect.get(target, key, receiver);
},
});
let thrown;
try { Bun.sql; thrown = "no throw"; } catch (e) { thrown = e; }
globalThis.Error = RealError;
console.log(JSON.stringify([thrown, phase, typeof Bun.sql]));`,
],
env: { ...bunEnv, BUN_JSC_validateExceptionChecks: "1" },
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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(["boom", 1, "function"]),
stderr: "",
exitCode: 0,
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

// A read of an unreified lazy property whose builder throws has to end the lookup at the Bun
// object. It used to go on to Bun's prototype with the exception still pending. The Proxy put
// behind Bun here has its own getOwnPropertySlot, and running it with a pending exception is what
// BUN_JSC_validateExceptionChecks=1 aborts on in debug builds. Both prototype walk loops are
// covered: a plain receiver uses JSObject::getPropertySlot, and a receiver that overrides
// getOwnPropertySlot (a function) sends the rest of the walk through
// JSObject::getNonIndexPropertySlot. The second read, which lets the builder succeed, used to
// abort the same way through the function: that loop did not check after running a builder at all.
test.concurrent.each([
["Bun itself", "Bun"],
["a function that inherits from Bun", "Object.setPrototypeOf(function () {}, Bun)"],
])("a lazy property builder that throws ends the lookup when the receiver is %s", async (_, receiver) => {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`Object.setPrototypeOf(Bun, new Proxy(Object.prototype, {}));
const receiver = ${receiver};
const RealSymbol = Symbol;
globalThis.Symbol = NaN;
let thrown;
try { receiver.sql; thrown = "no throw"; } catch (e) { thrown = e.constructor.name; }
globalThis.Symbol = RealSymbol;
console.log(JSON.stringify([thrown, typeof receiver.sql]));`,
],
env: { ...bunEnv, BUN_JSC_validateExceptionChecks: "1" },
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", "function"]),
stderr: "",
exitCode: 0,
});
});

test.concurrent("a lazy property builder that throws is not recorded as a missing property", async () => {
// Each access site is trained on many object shapes first, so that it is megamorphic by the
// time it sees Bun and the access takes the megamorphic slow path. That path used to walk past
// the builder that threw and record "not present" for Bun's structure. A builder that throws
// stores nothing, so Bun kept that structure and every later access from a megamorphic site got
// undefined (false for `in`) instead of running the builder again. So the second access of each
// site has to throw like the first one. Symbol stays broken throughout because the three sql
// properties share one module, which would stop throwing once any of them loaded it. Each site
// has its own property because the record is per property name.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`const byValKey = "postgres";
const inByValKey = "$";
const sites = {
get_by_id: o => typeof o.sql,
get_by_val: o => typeof o[byValKey],
in_by_id: o => "SQL" in o,
in_by_val: o => inByValKey in o,
};
const shapes = [];
for (let i = 0; i < 32; i++) {
const o = { sql: 0, postgres: 0, SQL: 0, $: 0 };
o["shape" + i] = i;
shapes.push(o);
}
for (let i = 0; i < 200; i++) {
for (const o of shapes) for (const name in sites) sites[name](o);
}
const access = site => { try { return site(Bun); } catch (e) { return e.constructor.name; } };
const RealSymbol = Symbol;
globalThis.Symbol = NaN;
const results = {};
for (const name in sites) results[name] = [access(sites[name]), access(sites[name])];
globalThis.Symbol = RealSymbol;
results.afterwards = typeof Bun.sql;
Comment thread
robobun marked this conversation as resolved.
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({
get_by_id: ["TypeError", "TypeError"],
get_by_val: ["TypeError", "TypeError"],
in_by_id: ["TypeError", "TypeError"],
in_by_val: ["TypeError", "TypeError"],
afterwards: "function",
}),
stderr: "",
exitCode: 0,
});
});
1 change: 0 additions & 1 deletion test/no-validate-exceptions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
test/bake/dev/production.test.ts
test/integration/vite-build/vite-build.test.ts
test/js/bun/test/parallel/test-integration-rspack.ts
test/js/bun/util/BunObject.test.ts
test/js/bun/util/fuzzy-wuzzy.test.ts
test/js/node/module/node-module-module.test.js
test/js/node/test/parallel/test-vm-module-referrer-realm.mjs
Expand Down
Loading