Skip to content

ErrorInstance: keep a lock that the error info callback adds to the error - #644

Open
robobun wants to merge 2 commits into
mainfrom
robobun/4a09b593/error-info-honor-locks
Open

ErrorInstance: keep a lock that the error info callback adds to the error#644
robobun wants to merge 2 commits into
mainfrom
robobun/4a09b593/error-info-honor-locks

Conversation

@robobun

@robobun robobun commented Sep 12, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • ErrorInstance::materializeErrorInfoIfNeeded (ErrorInstance.cpp:432, the USE(BUN_JSC_ADDITIONS) branch) calls VM::onComputeErrorInfoJSValue() and then stores line, column, sourceURL and stack with putDirect(). In Bun the callback runs Error.prepareStackTrace, so user code runs between the two steps.
  • putDirect() checks nothing. An error that the callback froze gains writable properties, and a stack that the callback made non-configurable is replaced. Node keeps both locks. Error.prepareStackTrace = e => { Object.freeze(e); return "P" }; const a = new Error("a"); a.stack; Object.isFrozen(a) is false in Bun and true in Node.

Fix

  • A property that is absent is not added to an error that became non-extensible while the callback ran.
  • A non-configurable property keeps its attributes. It takes the value only if it is a plain writable data property. A read-only property and an accessor are skipped.
  • An error that is non-extensible before the callback still gets the four properties, as today and as in Node. No write throws: the function runs inside a property lookup.
  • Verified: Bun linked against this change passes the new tests of Keep a lock that Error.prepareStackTrace adds when error.stack is first read (WebKit bump) bun#42533. They pass with the preview build autobuild-preview-pr-644-6024d7f1 (55 pass in test/js/node/v8/capture-stack-trace.test.js). Bun 1.4.3 fails 8 of the 9 new tests.

Background

  • An ErrorInstance creates the four properties on the first lookup of one of them. m_errorInfoMaterialized is set before the callback, so a lookup from inside the callback does not start over.
  • putDirect() is the internal store of JSC. It adds to a non-extensible object and replaces the attributes of a property that exists.
  • V8 has no such window. It keeps the formatted stack in an internal slot, behind an accessor that the constructor installed.
  • The m_stackString branch below runs no user code before its writes. It is unchanged.
Notes

Decision for a maintainer. No user reported this. It was found by audit while working on oven-sh/bun#33412, which is the same class on the Error.captureStackTrace path and is also open. The rule that the two PRs follow together: a read of stack never throws and skips a write that a lock forbids, and an explicit Error.captureStackTrace on a locked target throws the TypeError of V8. If Bun's stack machinery is not meant to honor integrity levels, both PRs close.

Results with Error.prepareStackTrace = e => { lock(e); return "P" }, then e.stack, on Bun linked against this change:

lock stack after other lazy properties Node 26.3
Object.freeze the default string the callback was handed, frozen not added, isFrozen is true isFrozen is true, stack is "P"
Object.seal "P", still non-configurable not added, isSealed is true the same
Object.preventExtensions "P" not added stack is "P"
defineProperty(e, "stack", { value: "LOCKED", writable: false, configurable: false }) "LOCKED", attributes kept added the same
defineProperty(e, "stack", { value: "LOCKED", writable: true, configurable: false }) "P", attributes kept added "LOCKED", attributes kept
defineProperty(e, "stack", { get, set, configurable: false }) the accessor, set is not called added the accessor
defineProperty(e, "line", { value: -1, configurable: false }) "P" line keeps -1 no line in V8
  • Known difference from Node: for a frozen error, or a stack that the callback made read-only, Node returns the result of the callback. V8 can do that because its stack is an accessor over an internal slot. A data property cannot take a new value once it is read-only and non-configurable, so the string that the callback froze stays. To match Node there, stack must become an accessor over a private slot. That is a larger change and nothing asks for it today.
  • The writable, non-configurable row returns "P", as Bun does today. Only the attributes are now kept. Node keeps the value of the callback's define.
  • stack exists when the callback runs because Bun stores the default string on the error before it calls Error.prepareStackTrace, so that error.stack inside the callback is a string as in Node.
  • The same read also follows Error.captureStackTrace(error) on an error whose stack is still lazy. The companion tests cover that row.
  • A first touch by assignment, define or delete also runs this function, and so the callback. With a freezing callback the touch now fails (Reflect.set returns false), because the freeze holds. V8 runs the callback only on a read.
  • One corner stays as it is: an error that is non-extensible before the first read and that the callback also freezes. The properties are added, because the check compares extensibility before and after the callback.
  • Not covered, same class, different code: the lazy stack accessor that Bun's Error.captureStackTrace installs when it captures no frames (errorInstanceLazyStackCustomGetter in Bun's FormatStackTraceForJS.cpp), and the node:vm arrow decoration (writeArrowHeaderStack in NodeVM.cpp). Both are Bun-side putDirect() calls and need no WebKit change.
  • The jsc shell does not set the callback, so there is no test under JSTests/. The tests are in test/js/node/v8/capture-stack-trace.test.js of the Bun PR.
  • The change is inside the USE(BUN_JSC_ADDITIONS) block, so it touches no line that exists upstream.

…rror

materializeErrorInfoIfNeeded() calls VM::onComputeErrorInfoJSValue() and then
stores "line", "column", "sourceURL" and "stack" with putDirect(). In Bun the
callback runs Error.prepareStackTrace, so user code runs between the two steps.
putDirect() checks nothing. It added the properties to an error that the
callback froze, and it replaced a "stack" that the callback made
non-configurable.

Skip each write that an ordinary [[DefineOwnProperty]] would reject: the
property exists and is non-configurable, or the property is absent and the
error became non-extensible while the callback ran. An error that is
non-extensible before the callback still gets the properties, because they
count as present from creation.
@coderabbitai

coderabbitai Bot commented Sep 13, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Essentials

Run ID: dab07095-fffa-4708-87d5-1211c662813b

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8a638 and 6024d7f.

📒 Files selected for processing (1)
  • Source/JavaScriptCore/runtime/ErrorInstance.cpp

Included review availability: Your plan provides up to 5 included reviews per hour; 1 remains after this review.


Walkthrough

Bun-specific lazy Error property materialization now records extensibility before the stack-preparation callback. It skips writes to restricted properties when the callback freezes the Error or makes properties non-configurable.

Changes

Error property materialization

Layer / File(s) Summary
Capture extensibility and guard property writes
Source/JavaScriptCore/runtime/ErrorInstance.cpp
The materialization path records initial extensibility before the callback. It guards writes to line, column, sourceURL, and stack based on property configurability and extensibility changes.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 6024d

The guarded materialization behavior preserves callback-defined restricted properties, with no remaining actionable merge risk identified.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives a detailed problem statement, fix, behavior changes, testing results, and implementation context. It does not provide the required Bugzilla bug link or the required reviewed-by l… Add the associated Bugzilla URL, include a "Reviewed by NOBODY (OOPS!)." line or actual reviewer, and add the required changed-file and function list. Ensure the pull request is linked to the Bugzilla bug and uses the required repository me…
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: preserving locks applied by the error-info callback during lazy error property materialization.
Full details: Description check

Explanation

The description gives a detailed problem statement, fix, behavior changes, testing results, and implementation context. It does not provide the required Bugzilla bug link or the required reviewed-by line and changed-file/function list from the repository template.

Resolution

Add the associated Bugzilla URL, include a "Reviewed by NOBODY (OOPS!)." line or actual reviewer, and add the required changed-file and function list. Ensure the pull request is linked to the Bugzilla bug and uses the required repository metadata.

  • Fix all pre-merge checks with AI

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 path_filters to narrow the review scope.


Comment @coderabbitai help to get the list of available commands.

Comment thread Source/JavaScriptCore/runtime/ErrorInstance.cpp Outdated
@github-actions

github-actions Bot commented Sep 13, 2026

Copy link
Copy Markdown

Preview Builds

Commit Release Date
6024d7f1 autobuild-preview-pr-644-6024d7f1 2026-09-13 10:12:21 UTC
7c8a6389 autobuild-preview-pr-644-7c8a6389 2026-09-13 00:17:46 UTC

robobun added a commit to oven-sh/bun that referenced this pull request Sep 13, 2026
The preview build carries the ErrorInstance::materializeErrorInfoIfNeeded
change that the new tests in capture-stack-trace.test.js cover. Swap the pin
for the merged sha before this lands.
…alue

A property that the callback made non-configurable keeps its attributes. If it
is a plain writable data property, as "stack" is after Object.seal(), it still
takes the value, because an assignment could store it. Only a read-only
property and an accessor are skipped. A sealed error then has the result of
Error.prepareStackTrace as its "stack", as in Node.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — commit 6024d7f addresses the seal case I flagged: a DontDelete-but-writable slot now takes the callback's value via putDirect with currentAttributes, so isSealed holds and stack matches Node. I didn't find further issues; given this sits on the object-invariant path a maintainer glance is still worthwhile.

What was reviewed:

  • putUnlessLocked against Object.freeze / seal / preventExtensions and non-configurable accessor/data redefinitions — each now respects the existing descriptor.
  • becameNonExtensible gating only newly-absent properties, so pre-existing-non-extensible errors still receive the lazy four as before.
  • ReadOnlyOrAccessorOrCustomAccessorOrValue is the existing composite in PropertySlot.h; structure()->get + putDirect mirrors the pattern used elsewhere in runtime/.
Extended reasoning...

Overview

The PR modifies a single function, ErrorInstance::materializeErrorInfoIfNeeded() in Source/JavaScriptCore/runtime/ErrorInstance.cpp, entirely inside the USE(BUN_JSC_ADDITIONS) block. It replaces four unconditional putDirect() calls (for line, column, sourceURL, stack) with a putUnlessLocked lambda that checks the current structure entry and the object's extensibility before writing, so that user code run via Error.prepareStackTrace cannot have its integrity-level changes (freeze/seal/preventExtensions, non-configurable defines) silently overwritten.

Security risks

None identified. The change is strictly more conservative than the base: where the old code unconditionally wrote and could violate [[DefineOwnProperty]] invariants (writable/configurable properties reappearing on a frozen object), the new code either skips the write or preserves existing attributes. There is no new allocation, no new user-code reentry point, and the lookup uses the standard structure()->get(vm, name, attrs) path. The worst failure mode is a lazy property not being populated in an edge case, which is a compat nit rather than a memory-safety or invariant-violation risk.

Level of scrutiny

Moderate. This is JSC runtime code on the object-model path, where mistakes around property attributes can produce spec-observable invariant violations or confuse inline caches. That said, the change is ~20 lines, localized to a Bun-only lazy-materialization slow path, and only narrows when writes happen. The first revision skipped writes on DontDelete alone, regressing the seal case versus Node; the follow-up commit now writes the value with currentAttributes when the slot is non-configurable but still a writable data property, which was the fix I suggested. I re-read the updated lambda against freeze (ReadOnly|DontDelete → skip), seal (DontDelete only → write with preserved attrs), preventExtensions-during-callback (absent + becameNonExtensible → skip), non-configurable accessor (Accessor|DontDelete → skip), and the pre-existing-non-extensible case (wasExtensible false → becameNonExtensible false → still adds), and each behaves as described.

Other factors

Tests live in the companion Bun PR (test/js/node/v8/capture-stack-trace.test.js) rather than JSTests, since the jsc shell doesn't install the callback — so a maintainer should confirm those pass against this build. The PR description's results table still lists the seal row as "the default string", which appears stale relative to the new code (seal should now yield "P"); worth a quick sanity check by whoever merges. The acknowledged corner (already-non-extensible error that the callback then freezes) remains, but is documented and is no worse than base behavior.

robobun added a commit to oven-sh/bun that referenced this pull request Sep 13, 2026
…t, define or delete keeps the lock

Follows oven-sh/WebKit#644 at 6024d7f151: a non-configurable property keeps its attributes and takes the value only if it is writable. Adds the read after Error.captureStackTrace on a still-lazy stack, and the setter-not-called check for a locked accessor.
robobun added a commit to oven-sh/bun that referenced this pull request Sep 13, 2026
Draft only. Swap for the merged sha before landing.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant