Check exceptions between static property builders, in the JSONL parse loop and in the JSONP fast path

RO
2mo ago
+18-6

Problem

  • Three JSC paths run a second throwing operation, or return from a ThrowScope, while the previous throwing call has not been checked. A debug build aborts on each under validateExceptionChecks=1: ERROR: Unchecked JS exception from JSC::VM::verifyExceptionCheckNeedIsSatisfied.
  • JSObject::reifyAllStaticProperties (JSObject.cpp:2978) and setUpStaticFunctionSlot (Lookup.cpp:73) read vm.exceptionForInspection() after a PropertyCallback builder. That read is not a check, so a builder with its own ThrowScope leaves the check pending and the next builder's scope constructor asserts. Bun's node:module and Bun.sql lazy properties are built this way (bun's test/no-validate-exceptions.txt lists 8 tests for this).
  • LiteralParser::tryStreamingParse (LiteralParser.cpp:1985) calls parseRecursively per line with no scope and no check. Interpreter::executeProgram's JSONP doGet lambda (Interpreter.cpp:1135) calls PropertySlot::getValue after getPropertySlot with no check.

Fix

  • The two reify sites use vm.exception(), which is the check the builder's scope needs. Behavior is unchanged: both already returned early on a pending exception.
  • tryStreamingParse declares a ThrowScope and checks after each value. An out-of-memory or stack-overflow exception is no longer reported as Status::Error with the exception still pending; the caller (Bun.JSONL.parse in bun, which checks right after streamingJSONParse) reports it.
  • doGet checks between getPropertySlot and getValue. Both call sites already check right after doGet.
  • Verified: read against the validator's rules in ThrowScope.cpp / VM::verifyExceptionCheckNeedIsSatisfied. I could not build JSC in the environment this was written in; CI is the build check. The bun side reproduces the first case with BUN_JSC_validateExceptionChecks=1 bun -e 'Bun.sql' on a debug build.

Background

  • BUN_JSC_validateExceptionChecks=1 makes every ThrowScope destructor set VM::m_needExceptionCheck. The next ThrowScope constructor or non-released destructor asserts if the bit is still set. Only VM::exception() (and clearException) clears it. exceptionForInspection() reads m_exception without touching the bit, for code that explicitly must not count as a check.
  • A static hash table PropertyCallback builder has the signature JSValue(VM&, JSObject*), so it cannot propagate an exception to reifyStaticProperty. The bun fork already lets it return empty with an exception pending; the two callers check and stop. This change only makes that check visible to the validator.