ErrorInstance: report "not found" when the error info callback throws in getOwnPropertySlot and deleteProperty - #687
Conversation
|
Preview build of 740d34a: |
… in getOwnPropertySlot and deleteProperty
materializeErrorInfoIfNeeded() calls VM::onComputeErrorInfoJSValue() under
USE(BUN_JSC_ADDITIONS). In Bun that callback runs Error.prepareStackTrace,
so it can throw. defineOwnProperty() and put() already return right after
it when an exception is pending. getOwnPropertySlot() and deleteProperty()
did not.
getOwnPropertySlot() went on to the base lookup, which finds the "stack"
that materializeErrorInfoIfNeeded() just stored, and returned true with
the exception pending. JSObject::getOwnPropertyDescriptor() asserts
against that:
ASSERTION FAILED: !scope.exception() || !result
runtime/JSObject.cpp(3958) : bool JSC::JSObject::getOwnPropertyDescriptor(JSGlobalObject *, PropertyName, PropertyDescriptor &)
deleteProperty() went on to delete the property with the exception
pending.
Both now return false when this call created the lazy properties and an
exception is pending. They read VM::exceptionForInspection() and declare
no ThrowScope, as setUpStaticFunctionSlot() does since #306. A ThrowScope
here simulates a throw on every lookup of a property of an error, and
callers such as JSModuleLoader::isFetchError() do not check for one:
with validateExceptionChecks=1 a module that rejects after an await
aborted in runInternalMicrotask.
getOwnSpecialPropertyNames() needs no change: it returns void, and its
caller JSObject::getOwnNonIndexPropertyNames() checks right after it.
ba6248d to
740d34a
Compare
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. Because it changes exception handling in a core JSC property hook (with one observable behavior change for delete e.stack under a throwing callback) and its tests live in the Bun repo rather than here, a human look would still be worthwhile.
What was reviewed:
- The
exceptionForInspection()-without-ThrowScope check mirrors the existing pattern inLookup.cppsetUpStaticFunctionSlot, and callers such asJSObject::getNonIndexPropertySlotdoRETURN_IF_EXCEPTIONright after the hook, so thefalsereturn propagates correctly. - Checked the pre-existing-exception concern: the check only fires when this call ran the callback, and a lookup with an unrelated exception already pending would have been invalid before the callback ran, so nothing new is misattributed.
- Checked the fast path after a failed materialization:
m_errorInfoMaterializedstays true, so later lookups skip the check and serve the storedstack/line/columndirectly; private-name lookups (isFetchError) never materialize and never hit the check.
Extended reasoning...
Overview
The PR is a single commit touching Source/JavaScriptCore/runtime/ErrorInstance.cpp (+12/-2). ErrorInstance::getOwnPropertySlot and ErrorInstance::deleteProperty now capture the return of materializeErrorInfoIfNeeded(vm, propertyName) and, when materialization ran this call and vm.exceptionForInspection() reports a pending exception, return false before touching the base implementation. The two siblings put and defineOwnProperty already had a DECLARE_THROW_SCOPE + RETURN_IF_EXCEPTION check; after this change all four property hooks refuse to proceed with a pending exception from the Bun-only VM::onComputeErrorInfoJSValue() callback.
Security risks
None specific to this change. It does not touch auth, crypto, or memory-unsafe operations; the only effect is returning "not found"/"not deleted" when an exception is already pending, which is the state JSC's own EXCEPTION_ASSERT(!scope.exception() || !result) contract in JSObject::getOwnPropertyDescriptor requires. Skipping Base::getOwnPropertySlot leaves the slot untouched, which is the correct state for a false return.
Level of scrutiny
Moderate. The diff is small and the reasoning is well documented, and the ThrowScope-free exceptionForInspection() pattern is already established in this fork (Lookup.cpp setUpStaticFunctionSlot, with the same justification about validateExceptionChecks). I verified that JSObject::getNonIndexPropertySlot checks for an exception immediately after calling an overriding getOwnPropertySlot, so the false return propagates correctly rather than continuing up the prototype chain. The three candidate issues from the hunt (misattributing an unrelated pending exception, the changed delete result, and the worker-termination case) are all either pre-existing invalid states or intentional consistency with put/defineOwnProperty, as the description states. Still, it is a core engine hook with subtle exception-scope semantics and a user-visible change to delete error.stack when Error.prepareStackTrace throws, so a maintainer's nod on the design choice is worthwhile.
Other factors
No tests are added in this repository, and none can be: the jsc shell cannot install the Bun callback, so verification is in oven-sh/bun#43096 against the preview build, which this review cannot run. The bug hunt ran dry (dry_streak) with no findings. No CODEOWNERS file constrains the path. The PR notes potential rebase against #644, which edits the adjacent function but not these two hooks.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review. Walkthrough
ChangesError property operation handling
Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to The change addresses pending exceptions during lazy Error property operations, and no current production or workflow risk requiring a pre-merge fix is identified. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Warning Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use Comment |
Problem
ErrorInstance::materializeErrorInfoIfNeededcallsVM::onComputeErrorInfoJSValue()(a Bun addition). In Bun that callback runsError.prepareStackTrace, so it can throw. The function storesstack,lineandcolumnin both cases.ErrorInstance::getOwnPropertySlot(ErrorInstance.cpp:511) then finds the stored property and returnstruewith the exception pending. A debug build of Bun aborts:Error.prepareStackTrace = () => { throw new Error("hook-throw") }; Object.getOwnPropertyDescriptor(new Error("x"), "stack").ErrorInstance::deletePropertyhas the same gap: it deletes with the exception pending.Fix
falsewhen this call created the lazy properties and an exception is pending. The exception still reaches the caller.VM::exceptionForInspection()and declare noThrowScope, assetUpStaticFunctionSlotdoes since Drop the ThrowScope from the reifyStaticProperty call sites #306. AThrowScopehere makesvalidateExceptionChecks=1abort in callers that never check, such asJSModuleLoader::isFetchError.delete error.stackno longer removesstack, as withputanddefineOwnProperty.autobuild-preview-pr-687-740d34a2passes the 4 new tests of Fix a debug assertion when Error.prepareStackTrace throws during getOwnPropertyDescriptor on an error (WebKit bump) bun#43096. All 4 fail on the current pin. Thejscshell cannot set the callback, so the tests are in Bun.Background
ErrorInstancecreatesstack,line,columnandsourceURLon the first touch of any of them. Upstream builds a string there and cannot throw. The fork added an exception check todefineOwnPropertyandputonly.getOwnPropertySlotthat throws must returnfalse.EXCEPTION_ASSERTchecks that in debug builds only.validateExceptionChecks=1makes eachThrowScopedestructor record a simulated throw, and aborts if the caller does not check. Bun's ASAN lane runs most tests with it.Notes
Why no
ThrowScope. The first version of this PR declared one at the top ofgetOwnPropertySlot, asJSFunction::getOwnPropertySlotdoes. WithvalidateExceptionChecks=1, an entry module withawait 1; throw new Error()then aborted: "This scope can throw a JS exception: getOwnPropertySlot @ ErrorInstance.cpp:514 ... unchecked as of this scope: runInternalMicrotask".JSModuleLoader::isFetchErrorandmaybeDuplicateFetchErrorcallhasOwnProperty()on an error with a private name and do not check afterwards, which is correct because that lookup cannot throw. With this version and the validator on, these pass in Bun:capture-stack-trace.test.js(61),structured-clone.test.ts(235),type-export.test.ts(70),worker-top-level-await.test.ts(6), and nine module scripts (a throw after a top-levelawait, a failed import, a link failure, a syntax error, arequirethat throws).Found by fuzzing a debug build of Bun. No user reported it, and only a build with assertions aborts. oven-sh/bun#34104 (merged) already names this gap and says that the proper fix is a check in
ErrorInstance::getOwnPropertySloton the WebKit side. oven-sh/bun#34095 shows the same assertion text, but its cause was the lazyprocessproperties, so it is not a report of this path.A termination is the same case with no throw in user code.
worker.terminate()while the worker is inside the callback leaves aTerminationExceptionpending, and the descriptor lookup aborts the same way. The tests in oven-sh/bun#43096 cover it with a callback that loops until the worker is terminated. On the current pin that crashes on every run.Operations that abort a debug build of Bun (
mainat812799ce8c, WebKitc28156899e) with a throwingError.prepareStackTrace, out of about 115 that were tried on a freshnew Error("x"):Object.getOwnPropertyDescriptor(e, "stack"), and the same for"line"and"column"Reflect.getOwnPropertyDescriptor(e, "stack")e.propertyIsEnumerable("stack")Object.getOwnPropertyDescriptor(new Proxy(e, {}), "stack")All six reach the assertion through
JSObject::getOwnPropertyDescriptor. With the preview build the same six throwhook-throw, and no other row of the 115 changes. The others throwhook-throwand do not abort:e.stack,Object.hasOwn,in,Object.getOwnPropertyNames,delete, assignment,Object.defineProperty,Object.freeze,structuredClone,postMessage. Their callers check for an exception right after the lookup (JSObject::getNonIndexPropertySlotdoes so, for example).structuredClone(error)andpostMessage(error)do not abort because Bun already works around this bug:SerializedScriptValue.cppcallsmaterializeErrorInfoIfNeeded()and checks for an exception before it asks for a descriptor.Node 26.3 calls
Error.prepareStackTraceonly for a read ofstack. It calls it 0 times for the six descriptor lookups above and for adelete, and itsdeleteremovesstack. Bun calls it once for each of them, because any first touch creates all four properties. That difference is older than this change, and this change does not try to remove it. With a throwing callback, Bun 1.4.3 throwshook-throwfromdelete e.stackandstackis gone afterwards. With this change it throws andstackstays, with the default string that Bun stores before it calls the callback.Why the check is only made when this call created the properties: a lookup of any other name cannot throw, and
isFetchErrorandmaybeDuplicateFetchErrorrely on that. The check also leaves a lookup alone when it is made from inside the callback, wherematerializeErrorInfoIfNeeded()returnsfalse.getOwnSpecialPropertyNamesneeds no change. It returnsvoid, and its callerJSObject::getOwnNonIndexPropertyNameschecks for an exception right after it.This branch starts at
000c489972, the commit that Bun'smainpins today, so the preview build carries this change alone.Related open PRs in the same file: #644 (integrity levels in
materializeErrorInfoIfNeeded), #511, #535 (addErrorInfoundervalidateExceptionChecks=1, a different caller of the same callback). None of them touches these two functions. #644 edits the function right above, so the second of the two to land needs a rebase with no conflict expected.