Bump WebKit: console functions release their ThrowScope before the console hook
Problem
- A failing
console.assertaborts a debug or ASAN build underBUN_JSC_validateExceptionChecks=1(CI's ASAN lane sets it):But the exception was unchecked as of this scope: consoleProtoFuncAssert @ runtime/ConsoleObject.cpp:218. consoleProtoFuncAssert(JavaScriptCore) holds aThrowScopeacross Bun's console hook and returns with no check or release. The hook can throw and has its own scope since #40410.- Eight more console functions have this shape (11 exits), inert while their hooks declare no scope.
Fix
- oven-sh/WebKit#790:
assertholds no scope. The eight others callscope.release()before the hook, so their caller owns the check. WEBKIT_VERSIONis that PR's preview build (the pin plus one commit). Draft until the pin can move to the merged commit.- Verified:
test/js/bun/jsc/exception-checks.test.ts(4 new rows, each exits 134 on the current pin). Also the console suites under the validator.
Background
- Under the validator, a
ThrowScopedestructor marks a check as needed unless it returns to JIT code. The next unreleased scope asserts on that mark. - Rejected: removing the hook's scope in Bun (hides the 12 exits), and
RETURN_IF_EXCEPTIONafter each call (a load and a branch in release code,scope.release()is free).
Downsides
console.assertno longer polls VM traps. Its loop does (check_traps). A Worker spinning inconsole.assert(true)terminates in 10 of 10 runs at both pins.- None found in release code:
assertshrinks from 522 to 404 bytes, the other eight are identical.
Notes
Builds. "Current pin" is 0c06faadf65bf8e8c8ad3a5a8aca83e1e9ed653f. The preview is autobuild-preview-pr-790-cc38fc87: 1 commit on top of the pin, Source/JavaScriptCore/runtime/ConsoleObject.cpp +11/-3, no other file. The preview release has the same 42 assets as the release of the pin. All runs below are debug ASAN builds of this branch on Linux x64 that differ only in the WebKit build.
Fail-before on the current pin, one of the four rows:
error: expect(received).toEqual(expected)
{
- "exitCode": 0,
- "stdout": "returned",
- "unchecked": [],
+ "exitCode": 134,
+ "stdout": "",
+ "unchecked": [
+ "This scope can throw a JS exception: messageWithTypeAndLevel @ unified/../../../src/jsc/bindings/ConsoleObject.cpp:38",
+ "But the exception was unchecked as of this scope: consoleProtoFuncAssert @ vendor/WebKit/Source/JavaScriptCore/runtime/ConsoleObject.cpp:218",
+ ],
}
(fail) console.assert with a falsy condition
4 pass
4 fail
With the preview build: 8 pass, 0 fail.
Forms. Each run alone under the validator: console.assert(false), console.assert(), console.assert(false, "x"), console.assert(0, {a:1}), console.assert(null, "%s:%d", "a", 1), console.assert("", new Error("e")), Reflect.apply(console.assert, console, [false, "x"]), [0].forEach(console.assert), JSON.stringify({ toJSON: console.assert }). 9 of 9 exit 134 on the current pin. 0 of 9 on the preview. console.assert(true) exits 0 on both.
The 11 exits that no test can hold today. count, countReset, profile (2), profileEnd (2), takeHeapSnapshot (2), time, timeLog, timeEnd. Their Bun hooks declare no ThrowScope, so nothing marks a check after they return.
scripts/jsc-exception-linton the JavaScriptCore file, with summaries forConsoleClient.cpp,ScriptArguments.cppandScriptCallStackFactory.cpp: 12 findings before (allunchecked-exit, one for each exit), 0 after.- Local experiment, not in this PR: a
ThrowScopewithRELEASE_AND_RETURNin each of the eight Bun hooks. One snippet for each exit, each alone under the validator. 12 of 12 abort on the current pin, each at its own scope (consoleProtoFuncCount @ ConsoleObject.cpp:235,consoleProtoFuncTime @ :326, and so on). 0 of 12 abort on the preview.
Simulated throws for each call under the validator (gdb hit count on JSC::ThrowScope::simulateThrow, 1,001 calls minus 101 calls, divided by 900). console.count("x"): 0 before and after. console.log("x"): 0 and 0. console.timeLog("l", {}): 5 and 5. console.assert(true): 0 and 0. console.assert(false): abort at the first call before, 0 after. In a validator build each of the 11 sibling call sites now also stores one flag byte (m_isReleased).
Release code. ConsoleObject.cpp of the pin and of the WebKit PR, each compiled as one file with clang 23.1.2 and the flags of the bun-webkit-linux-amd64-lto build (-O3, without -flto). consoleProtoFuncAssert: 522 to 404 bytes, 149 to 119 instructions. The path of console.assert(true) through it: 55 to 46 instructions, 8 to 7 conditional branches. The other 26 functions of the file: 0 differing objdump lines. I did not make a release build of Bun with the preview.
VM traps. RETURN_IF_EXCEPTION also handles pending VM traps (termination, the debugger, the watchdog). The removed one in assert was such a poll. for (let i = 0; i < n; i++) console.assert(true) compiles to loop_hint, check_traps, ..., call_ignore_result, so the loop polls on each iteration. Worker test: the worker posts one message and spins in for (;;) console.assert(true), the parent waits 300 ms and calls terminate(). Interleaved, 10 runs for each build: 10 of 10 terminate on both. Median 380 ms (241 to 491) on the current pin, 430 ms (237 to 550) on the preview. The difference is inside the spread.
Other suites on the preview build, with BUN_JSC_validateExceptionChecks=1. test/js/web/console/, test/js/bun/console/ and test/js/bun/jsc/exception-checks.test.ts: 109 pass, 1 skip, 2 todo, 0 fail. Node's test/js/node/test/parallel/test-console-*.js: 16 of 16 exit 0.
Not changed.
console.takeHeapSnapshot()still aborts under the validator. Its cause is in Bun:JSC__JSGlobalObject__generateHeapSnapshot @ src/jsc/bindings/bindings.cpp:4108is unchecked insrc/jsc/JSValue.rs:1091. The WebKit change only closes the two exits inconsoleProtoFuncTakeHeapSnapshotthat #37070 works around.console.assert(false, "x")printsx. Node.js printsAssertion failed: x(#19953).
[policy-decision:webkit] gate passed · iteration 0 · 2 files touched
passes on PR (with fix)
Test-only change.
Debug/ASAN (expected pass):
$ bun bd test 'test/js/bun/jsc/exception-checks.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/bun/jsc/exception-checks.test.ts
bun test v1.4.4 (367d939d9)
test/js/bun/jsc/exception-checks.test.ts:
(pass) Bun.deepEquals with one argument [1279.79ms]
(pass) console.assert with a falsy condition [1043.65ms]
(pass) process.exitCode assigned a rope string [1109.72ms]
(pass) process.kill with an unknown rope signal name [1100.83ms]
(pass) process.umask with a rope string [1143.90ms]
(pass) console.assert called by native code [472.93ms]
(pass) console.assert with a falsy condition and a message [664.78ms]
(pass) console.assert with a message whose custom inspect function throws [1455.06ms]
8 pass
0 fail
8 expect() calls
Ran 8 tests across 1 file. [11.28s]
Exit: 0
diff hotspot
scripts/build/deps/webkit.ts | 2 +-
test/js/bun/jsc/exception-checks.test.ts | 18 ++++++++++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
gate history · 1 passed · 0 rejected · iteration 0
evidence per changed file
file reads edits tests
scripts/build/deps/webkit.ts 1 1 7
test/js/bun/jsc/exception-checks.test.ts 1 1 7