diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index fbb60710f023..38540d5f258a 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -10,7 +10,10 @@ // Windows ICU data table filtered + per-item zstd compressed, and Windows // unwind info (RtlAddGrowableFunctionTable) registered for the fixed JIT // pool (LLInt pending offlineasm .seh_* emission). -export const WEBKIT_VERSION = "a40d462206e1caf8388062120acde61e37a4ae7d"; +// Preview of oven-sh/WebKit#328: RELEASE_AND_RETURN on the inspector +// injected-script prototype host functions so Runtime.evaluate survives +// validateExceptionChecks=1. +export const WEBKIT_VERSION = "autobuild-preview-pr-328-31913c3e"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/test/cli/inspect/inspect.test.ts b/test/cli/inspect/inspect.test.ts index e0e6b8471fd3..975853422229 100644 --- a/test/cli/inspect/inspect.test.ts +++ b/test/cli/inspect/inspect.test.ts @@ -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, tempDirWithFiles } from "harness"; +import { bunEnv, bunExe, isASAN, isDebug, isPosix, randomPort, tempDirWithFiles } from "harness"; import { join } from "node:path"; import stripAnsi from "strip-ansi"; import { WebSocket } from "ws"; @@ -300,6 +300,73 @@ describe("websocket", () => { }); }); +// JSInjectedScriptHostPrototype / JSJavaScriptCallFramePrototype host functions declare a +// ThrowScope and then tail-call into an impl that declares its own; without a +// RELEASE_AND_RETURN the first Runtime.evaluate aborts under exception-check validation with +// "Unchecked JS exception ... jsInjectedScriptHostPrototypeFunctionEvaluateWithScopeExtension". +// ENABLE_EXCEPTION_SCOPE_VERIFICATION is (ASSERT_ENABLED || ASAN_ENABLED), so this runs on +// debug and asan builds; plain release compiles it out. +test.skipIf(!isDebug && !isASAN)("Runtime.evaluate does not trip exception-check validation", async () => { + await using child = spawn({ + cwd: import.meta.dir, + cmd: [bunExe(), "--inspect-wait=127.0.0.1:0", "inspectee.js"], + env: { + ...bunEnv, + BUN_JSC_validateExceptionChecks: "1", + BUN_JSC_dumpSimulatedThrows: "1", + }, + stdout: "ignore", + stderr: "pipe", + }); + + let stderr = ""; + const decoder = new TextDecoder(); + const { promise: urlPromise, resolve: resolveUrl, reject: rejectUrl } = Promise.withResolvers(); + const drained = (async () => { + for await (const chunk of child.stderr) { + stderr += decoder.decode(chunk); + for (const line of stderr.split("\n")) { + try { + const u = new URL(line.trim()); + if (u.protocol.includes("ws")) resolveUrl(u); + } catch {} + } + } + rejectUrl(new Error("inspectee exited before printing inspector URL:\n" + stderr)); + })(); + + const url = await urlPromise; + const ws = new WebSocket(url); + let reply: unknown; + try { + await new Promise((resolve, reject) => { + ws.addEventListener("open", () => resolve()); + ws.addEventListener("error", cause => reject(new Error("WebSocket error", { cause }))); + }); + + ws.send(JSON.stringify({ id: 1, method: "Runtime.evaluate", params: { expression: "1 + 1" } })); + reply = await new Promise((resolve, reject) => { + ws.addEventListener("message", ({ data }) => resolve(JSON.parse(String(data)))); + ws.addEventListener("close", ({ code, reason }) => resolve({ closed: { code, reason } })); + ws.addEventListener("error", cause => reject(new Error("WebSocket error", { cause }))); + }); + } finally { + ws.close(); + child.kill(); + } + + await Promise.all([child.exited, drained]); + // Without the WebKit-side fix the inspectee SIGABRTs ("Unchecked JS exception") + // before replying, so the socket closes 1006 and reply is { closed: { code: 1006, ... } }. + if (child.signalCode === "SIGABRT") { + throw new Error(`inspectee aborted under validateExceptionChecks (reply=${JSON.stringify(reply)}):\n${stderr}`); + } + expect(reply).toMatchObject({ + id: 1, + result: { result: { type: "number", value: 2 } }, + }); +}); + describe("http metadata endpoint", () => { let metadataInspectee: Subprocess | undefined; diff --git a/test/expectations.txt b/test/expectations.txt index 0e2990c51be7..d959e0c7b25a 100644 --- a/test/expectations.txt +++ b/test/expectations.txt @@ -81,7 +81,6 @@ test/js/bun/spawn/spawn-maxbuf.test.ts [ FLAKY ] # Tests timed out due to ASAN [ ASAN ] test/js/bun/spawn/spawn.test.ts [ TIMEOUT ] -[ ASAN ] test/cli/inspect/inspect.test.ts [ TIMEOUT ] # Tests failed due to memory leaks [ ASAN ] test/js/node/url/pathToFileURL.test.ts [ LEAK ] # pathToFileURL doesn't leak memory diff --git a/test/internal/source-lints/webkit-prebuilt-url.test.ts b/test/internal/source-lints/webkit-prebuilt-url.test.ts index 5a7ac2701e94..992518362ef1 100644 --- a/test/internal/source-lints/webkit-prebuilt-url.test.ts +++ b/test/internal/source-lints/webkit-prebuilt-url.test.ts @@ -123,4 +123,11 @@ describe("WebKit prebuilt URL", () => { 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); + }); });