Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
4 changes: 3 additions & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "34c01d13391e00c06862a3d2c5b7fff350ac87e0";
// Preview of oven-sh/WebKit#376: exception checks in jsToInspectorValue so
// Console.enable replay of buffered messages survives validateExceptionChecks=1.
export const WEBKIT_VERSION = "autobuild-preview-pr-376-e68eb1fd";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
111 changes: 110 additions & 1 deletion test/cli/inspect/inspect.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Subprocess, spawn } from "bun";
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
import fs from "fs";
import { bunEnv, bunExe, isPosix, randomPort, tempDir } from "harness";
import { bunEnv, bunExe, isASAN, isDebug, isPosix, randomPort, tempDir } from "harness";
import { join } from "node:path";
import stripAnsi from "strip-ansi";
import { WebSocket } from "ws";
Expand Down Expand Up @@ -300,6 +300,115 @@ describe("websocket", () => {
});
});

// jsToInspectorValue (InjectedScriptBase.cpp) called getOwnPropertyNames and then object.get()
// on each property with no exception check between them. The live console.log path has a JS
// topEntryFrame on the stack so the inner ThrowScope destructor skips the simulated throw, but
// InspectorConsoleAgent::enable()'s replay of buffered messages runs from backend dispatch with
// no JS frame, so validation trips: "getOwnNonIndexPropertyNames ... unchecked as of get".
// ENABLE_EXCEPTION_SCOPE_VERIFICATION is (ASSERT_ENABLED || ASAN_ENABLED), so this only runs on
// debug / asan builds.
test.skipIf(!isDebug && !isASAN)(
"Console.enable replay of buffered messages does not trip exception-check validation",
async () => {
await using child = spawn({
cmd: [
bunExe(),
"--inspect-wait=127.0.0.1:0",
"-e",
`console.log("BUFFER-A"); console.log("BUFFER-B"); setInterval(()=>{},1000);`,
],
env: {
...bunEnv,
BUN_JSC_validateExceptionChecks: "1",
BUN_JSC_dumpSimulatedThrows: "1",
},
stdout: "pipe",
stderr: "pipe",
});

let stderr = "";
let stdout = "";
const decoder = new TextDecoder();
const { promise: urlPromise, resolve: resolveUrl, reject: rejectUrl } = Promise.withResolvers<URL>();
const stderrDrained = (async () => {
for await (const chunk of child.stderr) {
stderr += decoder.decode(chunk);
const m = stderr.match(/ws:\/\/[^\s]+/);
if (m) resolveUrl(new URL(m[0]));
}
rejectUrl(new Error("inspectee exited before printing inspector URL:\n" + stderr));
})();
const { promise: bufferedPromise, resolve: resolveBuffered } = Promise.withResolvers<void>();
const stdoutDrained = (async () => {
for await (const chunk of child.stdout) {
stdout += decoder.decode(chunk);
if (stdout.includes("BUFFER-A") && stdout.includes("BUFFER-B")) resolveBuffered();
}
})();

const url = await urlPromise;
const ws = new WebSocket(url);
const replayed: string[] = [];
let reply: unknown;
try {
let nextId = 1;
let failed: unknown;
const pending = new Map<number, (x: unknown) => void>();
const send = (method: string, params: object = {}) =>
new Promise<unknown>(resolve => {
if (failed) return resolve(failed);
const id = nextId++;
pending.set(id, resolve);
ws.send(JSON.stringify({ id, method, params }));
});
const fail = (r: unknown) => {
failed ??= r;
for (const p of pending.values()) p(r);
pending.clear();
};
ws.addEventListener("message", ev => {
const msg = JSON.parse(String(ev.data));
if (msg.id && pending.has(msg.id)) {
pending.get(msg.id)!(msg);
pending.delete(msg.id);
} else if (msg.method === "Console.messageAdded") {
replayed.push(msg.params.message.text);
}
});
ws.addEventListener("close", ({ code, reason }) => fail({ closed: { code, reason } }));
ws.addEventListener("error", cause => fail({ error: String(cause) }));
await new Promise<void>((resolve, reject) => {
ws.addEventListener("open", () => resolve());
ws.addEventListener("error", cause => reject(new Error("WebSocket error", { cause })));
});

// Let user code run and buffer the two console.log calls in InspectorConsoleAgent,
// then send Console.enable so enable() replays them with no JS on the stack.
await send("Inspector.enable");
await send("Inspector.initialized");
await Promise.race([bufferedPromise, child.exited]);
// enable() dispatches the buffered Console.messageAdded events synchronously before
// replying, so once this resolves the replayed[] array is complete. A second round-trip
// guards against any cross-thread delivery reordering.
reply = await send("Console.enable");
await send("Runtime.evaluate", { expression: "1" });
} finally {
ws.close();
child.kill();
}

await Promise.all([child.exited, stderrDrained.catch(() => {}), stdoutDrained]);
// Without the WebKit-side fix the inspectee SIGABRTs ("Unchecked JS exception:
// getOwnNonIndexPropertyNames ... unchecked as of get") inside toInspectorValue before
// replying to Console.enable, so the socket closes 1006 and reply is { closed: ... }.
if (child.signalCode === "SIGABRT") {
throw new Error(`inspectee aborted under validateExceptionChecks (reply=${JSON.stringify(reply)}):\n${stderr}`);
}
expect(reply).toMatchObject({ id: expect.any(Number), result: {} });
expect(replayed).toEqual(expect.arrayContaining(["BUFFER-A", "BUFFER-B"]));
},
);

describe("http metadata endpoint", () => {
let metadataInspectee: Subprocess | undefined;

Expand Down
7 changes: 7 additions & 0 deletions test/internal/source-lints/webkit-prebuilt-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,11 @@
test("WEBKIT_VERSION is either a 40-hex sha or an autobuild-* tag", () => {
expect(/^[0-9a-f]{40}$/.test(WEBKIT_VERSION) || WEBKIT_VERSION.startsWith("autobuild-")).toBe(true);
});

// autobuild-preview-pr-* releases are deleted when the oven-sh/WebKit PR
// merges or closes, which would 404 every fresh build of main. Preview pins
// are fine on a branch while iterating; this test is the merge gate.
test("WEBKIT_VERSION is not an autobuild-preview-* tag (preview releases are deleted on upstream merge)", () => {
expect(WEBKIT_VERSION.startsWith("autobuild-preview-")).toBe(false);

Check failure on line 131 in test/internal/source-lints/webkit-prebuilt-url.test.ts

View workflow job for this annotation

GitHub Actions / Source lints

error: expect(received).toBe(expected)

Expected: false Received: true at <anonymous> (/home/runner/work/bun/bun/test/internal/source-lints/webkit-prebuilt-url.test.ts:131:61)
});
});
Loading