Skip to content

Fix use-after-free in Error.appendStackTrace when source is destination - #32098

Closed
robobun wants to merge 6 commits into
mainfrom
farm/57db67a7/fix-appendstacktrace-self-uaf
Closed

Fix use-after-free in Error.appendStackTrace when source is destination#32098
robobun wants to merge 6 commits into
mainfrom
farm/57db67a7/fix-appendstacktrace-self-uaf

Conversation

@robobun

@robobun robobun commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes a use-after-free found by fuzzing (fingerprint 35392e9d08621196).

Root cause

Error.appendStackTrace(source, destination) merges stack traces with:

destination->stackTrace()->appendVector(*source->stackTrace());
source->stackTrace()->clear();

When source === destination, appendVector appends the vector to itself. Vector::appendVector goes through the span-based append, and when the append grows the vector past its capacity, reserveCapacity allocates a new buffer and frees the old one. The span path passes a const T* source pointer, which binds to the generic expandCapacity(size_t, U*) overload that returns the pointer unadjusted (only the non-const T* overload relocates interior pointers). The subsequent copy then reads the StackFrames from the freed buffer.

Reallocation only happens once the trace has 9 or more frames (initial capacity is 16), and WTF allocations normally come from bmalloc where ASan cannot see the freed memory, which is why the fuzzer only hit this intermittently: the stale data is usually copied back intact before the block is reused. When the block does get reused or decommitted in that window, the copy constructs StackFrames (a Variant holding a RefPtr in its wasm alternative) from garbage, corrupting memory.

With Malloc=1 (forces bmalloc through the system allocator) the unfixed ASan build reports it deterministically:

==ERROR: AddressSanitizer: heap-use-after-free ...
READ of size 1 at 0x74c8df8e1618 thread T0
    #0 mpark::detail::base<JSC::JSFrameData, JSC::WasmFrameData>::valueless_by_exception() wtf/Variant.h:1465
    #4 JSC::StackFrame::StackFrame(JSC::StackFrame const&)
    #5 WTF::VectorCopier<JSC::StackFrame>::uninitializedCopy(...)
    #6 WTF::Vector<JSC::StackFrame, ...>::append(std::span<JSC::StackFrame const, ...>) wtf/Vector.h:1516
    ...
    #9 errorConstructorFuncAppendStackTrace

Fix

The fixing change is the source == destination early return in errorConstructorFuncAppendStackTrace: appending an error's trace to itself and then clearing it has no useful effect, so it is now a no-op that leaves the trace unchanged.

Tests

Added test/js/bun/util/error-append-stack-trace.test.ts:

  • a spawned repro of the self-append with a deep enough stack to force the reallocation, run with Malloc=1 so ASan builds catch the use-after-free (symbolize=0 keeps the failing child fast); it fails on the unfixed ASan build and passes with the fix
  • a behavior test that the normal two-error Error.appendStackTrace still merges the source frames into the destination

Repro script:

function f(n) {
  if (n > 0) return f(n - 1) + 1;
  try { null(); } catch (e) {
    Error.appendStackTrace(e, e);
  }
  return 0;
}
f(64);

Error.appendStackTrace(e, e) called appendVector on the error's stack
trace vector with itself as the source. When the append grows the vector
past its capacity, WTF::Vector frees the old buffer and the span append
path does not adjust the source pointer, so the copy reads from freed
memory. Skip the append when source and destination are the same error.
@robobun

robobun commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:14 AM PT - Jun 11th, 2026

@robobun, your commit 5f49d6d has 3 failures in Build #61881 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32098

That installs a local version of the PR into your bun-32098 executable, so you can run:

bun-32098 --bun

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f4dbc3a-ed98-455b-b0ff-dba704aa1f14

📥 Commits

Reviewing files that changed from the base of the PR and between d95f8c5 and 19e2d39.

📒 Files selected for processing (1)
  • test/js/bun/util/error-append-stack-trace.test.ts

Walkthrough

The patch prevents self-appending an Error's stack trace by returning early when source and destination are identical, and adds integration and unit tests validating self-append safety and normal append behavior.

Changes

Error.appendStackTrace self-append safety

Layer / File(s) Summary
Guard against self-appending error stack traces
src/jsc/bindings/FormatStackTraceForJS.cpp
errorConstructorFuncAppendStackTrace now checks if source and destination are the same Error instance and returns undefined immediately to prevent unsafe self-append.
Tests: ASan subprocess + in-process unit
test/js/bun/util/error-append-stack-trace.test.ts
Adds imports and a comment describing the prior use-after-free mode. Integration test spawns Bun with ASan-friendly env vars to run code that calls Error.appendStackTrace(e, e) and asserts clean exit and expected stdout. In-process unit test appends a source error's stack into a separate destination and asserts the destination stack contains the source function name.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: fixing a use-after-free bug in Error.appendStackTrace when source equals destination.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering root cause analysis, the fix, test details, and reproduction steps, exceeding the basic template requirements.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

The heavy GC loop tests in error-gc-test.test.js exceed the per-test
timeout on slow debug ASAN runs regardless of this change, so the new
tests now live in their own file. Also pass symbolize=0 to the spawned
child so an ASan abort on the unfixed build fails the test quickly
instead of hitting the test timeout during report symbolization.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/util/error-append-stack-trace.test.ts`:
- Around line 32-34: The test currently ignores proc.stderr and checks
stdout/exitCode only; change the assertion order to first capture and assert
proc.stderr (the stderr variable from proc.stderr.text()) for ASAN/sanitizer
signatures (e.g., "ERROR: AddressSanitizer" or "SUMMARY: AddressSanitizer") and
fail if present, then proceed to assert stdout and exitCode; locate the
Promise.all usage that assigns [stdout, , exitCode] and modify it to capture
stderr (e.g., [stdout, stderr, exitCode]) and add a short assertion that fails
on sanitizer output before the existing expect(stdout).toBe("ok\n") and
expect(exitCode).toBe(0).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fb4a20e8-c8a1-47f1-8523-e30683c29d15

📥 Commits

Reviewing files that changed from the base of the PR and between 861eda8 and 414fde0.

📒 Files selected for processing (1)
  • test/js/bun/util/error-append-stack-trace.test.ts

Comment thread test/js/bun/util/error-append-stack-trace.test.ts Outdated
robobun added 4 commits June 11, 2026 06:10
Checking stderr first surfaces the ASan report in the failure diff
instead of an empty-stdout mismatch, and still fails if a future ASan
configuration reports without aborting. Matches only report markers so
benign ASan warnings on debug builds cannot trip it.
With Malloc=1 the child's JSC allocations go through the system
allocator, so LeakSanitizer reports JSC's intentional exit-time
allocations on the release ASan build and the stderr assertion matched
that unrelated report. detect_leaks=0 silences LSan; heap-use-after-free
detection is unaffected.
Forcing the system allocator made the spawned child produce no output
on the Windows release build, and only ASan builds can observe the
use-after-free anyway. Non-ASan builds now spawn with plain bunEnv.
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Consolidated into #37370, which now also carries the source === destination early return from this PR along with its two tests (the Malloc=1 self-append repro, extended to assert the frames survive, and the plain two-error merge), moved into test/js/node/v8/capture-stack-trace.test.js next to the other appendStackTrace cases. Verified the self-append test still reports the heap-use-after-free on an unfixed debug + ASAN build of main. Closing.

@robobun robobun closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant