Skip to content

crash_handler: tag JSC JIT-pool frames instead of discarding them - #35440

Closed
robobun wants to merge 3 commits into
mainfrom
farm/b3086b2f/crash-handler-tag-jit-frames
Closed

robobun wants to merge 3 commits into
mainfrom
farm/b3086b2f/crash-handler-tag-jit-frames

Conversation

@robobun

@robobun robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

StackLine::from_address (src/crash_handler/lib.rs) discards any captured PC it cannot map to a loaded image: on Windows GetModuleHandleExW(FROM_ADDRESS), on Linux dl_iterate_phdr, on macOS the _dyld image walk. The encoder writes a single _ for those frames and the raw address is thrown away; bun.report renders them as pkg:\"?\" addr:0x0000.

JSC's fixed executable-memory pool (Baseline/DFG/FTL/Wasm/Yarr, plus host-call thunks) is a bare VirtualAlloc/mmap reservation, not a loaded image, so every JIT frame hits that path. In Sentry, 9,122 of 58,414 Windows crash events over the last 7 days (~16%) carried at least one such blank frame; e.g. BUN-2PYE events on 1.4.0-canary+892b1dabc show 19/20 frames as pkg:\"?\" addr:0x0000 for a user running JIT'd code with process_dlopen.

Fix

Mirror g_jscConfig.{start,end}ExecutableMemory (JSC's isJITPC bounds) into the crash handler via a new Bun__setJITPoolRange called once from JSCInitialize after JSC::initialize(). StackLine::from_address range-checks each PC against it before the per-platform image lookup (two pointer compares, no locks, no allocation). A match is encoded with object: \"JIT\" and address = offset into the pool, which reuses the existing foreign-module trace-string slot (VLQ(1) + VLQ(len) + name + VLQ(addr)), so bun.report needs no decoder change and will render pkg:\"JIT\" addr:0x<offset>.

Pool bounds live in cli_state alongside CMD_CHAR/MAIN_THREAD_ID (same write-once-from-a-higher-tier-crate pattern as #31688).

Verification

New cross-platform test in test/cli/run/run-crash-handler.test.ts via two bun:internal-for-testing hooks (jitPoolRange(), stackLineObject(addr)):

$ USE_SYSTEM_BUN=1 bun test test/cli/run/run-crash-handler.test.ts -t 'JIT pool'
(fail) JIT pool addresses are tagged in crash-report frames
  TypeError: jitPoolRange is not a function

$ bun bd test test/cli/run/run-crash-handler.test.ts -t 'JIT pool'
(pass) JIT pool addresses are tagged in crash-report frames [4.42ms]

Full run-crash-handler.test.ts: 15 pass / 8 skip / 0 fail. crash-report-command-char.test.ts: 3 pass. bun run rust:check-all: 10/10 targets clean.

Deriving JIT tier / JS function name from the PC (via VMInspector::codeBlockForMachinePC or a vm.topCallFrame walk) is a larger follow-up.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/run/run-crash-handler.test.ts

StackLine::from_address drops any captured PC that cannot be mapped to a
loaded image (GetModuleHandleExW / dl_iterate_phdr / dyld): the encoder
writes a single '_' and the raw address is lost, which bun.report renders
as pkg:"?" addr:0x0000. JSC's fixed executable-memory pool (Baseline/
DFG/FTL/Wasm/Yarr and host-call thunks) is a bare VirtualAlloc/mmap, not
an image, so every JIT frame hit that path. ~16% of Windows crash events
in the last 7 days carried at least one such frame.

Mirror g_jscConfig.{start,end}ExecutableMemory into the crash handler at
JSCInitialize time and range-check each captured PC against it before the
image lookup. A match is encoded with object "JIT" and address =
offset-into-pool, reusing the existing foreign-module trace-string slot so
bun.report needs no decoder change.
@coderabbitai

coderabbitai Bot commented Jul 24, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

JSC registers its fixed JIT memory pool with the crash handler. Stack traces encode in-range addresses as stable offsets labeled "JIT". Testing bindings and boundary coverage expose and validate this behavior.

JIT pool crash-frame tagging

Layer / File(s) Summary
Range tracking and frame encoding
src/crash_handler/lib.rs
Pool bounds, offset calculation, JIT frame encoding, C ABI registration, Cow-based object storage, and symbolizer filtering are added.
JSC startup range registration
src/jsc/bindings/ZigGlobalObject.cpp
JSCInitialize forwards fixed executable-memory pool boundaries to the crash handler when JIT is enabled.
Testing bindings and validation
src/runtime/api/crash_handler_jsc.rs, src/js/internal-for-testing.ts, test/cli/run/run-crash-handler.test.ts
Bindings expose pool and stack-line data, while tests verify JIT labeling, boundaries, and invalid addresses.

Suggested reviewers: jarred-sumner, dylan-conway

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 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 summarizes the main change: tagging JSC JIT-pool frames instead of dropping them.
Description check ✅ Passed The description covers the problem, fix, and verification, though it uses custom headings instead of the template's exact section names.

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

@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: 4

🤖 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 `@src/crash_handler/lib.rs`:
- Around line 2565-2576: Update the JIT-frame handling in
cli_state::jit_pool_offset to avoid allocating when constructing StackLine;
replace Box::<[u8]>::from(&b"JIT"[..]) with the existing static, borrowed, or
non-allocating representation supported by StackLine while preserving the "JIT"
label and pool offset.

In `@src/runtime/api/crash_handler_jsc.rs`:
- Around line 35-36: Update the internal testing binding declaration in
internal-for-testing.ts to include the new crash_handler members: jitPoolRange
returning a [bigint, bigint] tuple and stackLineObject accepting a bigint
address and returning string | null | undefined. Preserve the existing
declarations.

In `@test/cli/run/run-crash-handler.test.ts`:
- Around line 16-21: Remove the historical “~16% of Windows crash events”
statistic from the comment above the crash-report trace-string encoder. Preserve
the durable explanation that JSC JIT memory is not a loaded image and that
unmapped JIT frames were previously encoded as `_` and rendered incorrectly.
- Around line 29-35: Strengthen the assertions in the pool-address test around
stackLineObject so they validate the encoded offset, not just the "JIT" label:
assert start encodes as 0 and end - 1 encodes as end - start - 1. Expose the
encoded address or assert the resulting trace representation, while preserving
the existing exclusive-end and zero-PC checks.
🪄 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: ee0ced41-ad5c-4c56-9e92-017a57a43e67

📥 Commits

Reviewing files that changed from the base of the PR and between 028f7a3 and 3d3269e.

📒 Files selected for processing (4)
  • src/crash_handler/lib.rs
  • src/jsc/bindings/ZigGlobalObject.cpp
  • src/runtime/api/crash_handler_jsc.rs
  • test/cli/run/run-crash-handler.test.ts

Comment thread src/crash_handler/lib.rs
Comment thread src/runtime/api/crash_handler_jsc.rs Outdated
Comment thread test/cli/run/run-crash-handler.test.ts Outdated
Comment thread test/cli/run/run-crash-handler.test.ts Outdated
Comment thread src/crash_handler/lib.rs Outdated
Comment thread src/crash_handler/lib.rs
@robobun

robobun commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:31 PM PT - Jul 24th, 2026

✅ @robobun, your commit f09345b6eb2acc09d9be17593bf9d391d24c972b passed in Build #79671! 🎉


🧪   To try this PR locally:

bunx bun-pr 35440

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

bun-35440 --bun

…_symbolizer, return numbers from jitPoolRange

- StackLine.object is now Option<Cow<'static, [u8]>> so the JIT branch
  stays alloc-free on the POSIX signal-handler path (only the Windows DLL
  branch owns).
- spawn_symbolizer skips frames with object.is_some() so a JIT pool offset
  (or a Windows DLL offset, pre-existing) is not fed to llvm-symbolizer /
  pdb-addr2line against bun's own image.
- jitPoolRange returns doubles (user-space addrs are <2^48) instead of
  BigInt; avoids the unchecked JSBigInt::tryCreateFrom throw scope the
  ASAN lane's validateExceptionChecks tripped on.
- stackLine hook now returns {address, object}; test asserts the pool
  offset as well as the label.
- internal-for-testing.ts type annotations updated; dropped the incident
  stat from the test comment.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/crash_handler/lib.rs (1)

565-586: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Publish the JIT range with release/acquire ordering. src/crash_handler/lib.rs:565-586 The two Relaxed stores/loads can expose a mixed (start, end) snapshot during startup, so a valid JIT PC may be classified as unknown. Store end with Release and load it with Acquire before reading start in both readers.

🤖 Prompt for 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.

In `@src/crash_handler/lib.rs` around lines 565 - 586, Update set_jit_pool_range,
jit_pool_range, and jit_pool_offset to publish JIT_POOL_END with Release
ordering and load it with Acquire ordering before reading JIT_POOL_START.
Preserve the existing range and offset behavior while ensuring readers observe a
consistent initialized JIT range.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@src/crash_handler/lib.rs`:
- Around line 565-586: Update set_jit_pool_range, jit_pool_range, and
jit_pool_offset to publish JIT_POOL_END with Release ordering and load it with
Acquire ordering before reading JIT_POOL_START. Preserve the existing range and
offset behavior while ensuring readers observe a consistent initialized JIT
range.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9f5bb8ef-5982-4908-9ce8-2ad0ef973f88

📥 Commits

Reviewing files that changed from the base of the PR and between 457303f and f09345b.

📒 Files selected for processing (4)
  • src/crash_handler/lib.rs
  • src/js/internal-for-testing.ts
  • src/runtime/api/crash_handler_jsc.rs
  • test/cli/run/run-crash-handler.test.ts

@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Re the release/acquire suggestion for JIT_POOL_START/END: leaving these as Relaxed, matching the adjacent CMD_CHAR/MAIN_THREAD_ID.

Bun__setJITPoolRange runs once inside std::call_once(JSCInitialize) on the main thread before any VM (and therefore any JIT code) exists. Every thread that can have a JIT PC on its stack was spawned after JSCInitialize via pthread_create/CreateThread, which already provides the happens-before for both stores. Threads that predate JSCInitialize (if any) do not run JS and have no JIT frames for this to classify.

A reader racing the two stores can only observe a combination where start <= addr && addr < end is false, so jit_pool_offset returns None and the frame is encoded as _ (the pre-PR behavior). There is no state in which a non-JIT PC is mis-tagged as JIT.

@claude claude 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.

No issues found on the follow-up commit, but this touches the async-signal-safe crash-report encoding path and adds a C++→Rust FFI symbol called from JSCInitialize, so it's worth a human look before merging.

What was reviewed:

  • The Cow::Borrowed(b"JIT") change keeps the POSIX from_address path alloc-free; write_encoded derefs Cow<[u8]> identically to the old Box<[u8]>.
  • spawn_symbolizer now skips foreign-object frames, restoring the pre-PR Linux behavior and fixing the pre-existing Windows DLL-offset case.
  • Torn read of JIT_POOL_START/END during a mid-set_jit_pool_range crash is benign: start != 0, end == 0 fails the range check and falls through to the platform lookup.
  • #if ENABLE(JIT) + start == 0 sentinel handles JIT-disabled builds and runtime --jitless.
Extended reasoning...

Overview

The PR mirrors JSC's fixed executable-memory pool bounds (g_jscConfig.{start,end}ExecutableMemory) into two AtomicUsizes in crash_handler::cli_state, written once from JSCInitialize after JSC::initialize(). StackLine::from_address now range-checks each captured PC against these bounds before the per-platform module lookup and, on match, encodes the frame as object: "JIT" with address = pc - pool_start. StackLine.object was widened from Option<Box<[u8]>> to Option<Cow<'static, [u8]>> so the JIT label is a static borrow. Two bun:internal-for-testing hooks and a boundary-focused test cover the classification.

Security risks

None identified. The change is read-only diagnostic labeling of already-captured PCs; it does not alter fault handling, signal disposition, or the trace-string wire format (it reuses the existing foreign-module VLQ(1) + name + addr slot). The FFI symbol takes two usize values and stores them in atomics — no pointer dereference.

Level of scrutiny

Higher than average. from_address runs inside the POSIX SIGSEGV/SIGABRT/SIGTRAP handler on the release path, where the surrounding code is deliberately alloc-free and lock-free. The first-round review caught that the original Box::from(b"JIT") allocation violated that invariant, and that spawn_symbolizer would mis-symbolize JIT offsets against bun's own binary; both were fixed in f09345b. The Cow change is correct (write_encoded uses writer.write_all(object) which derefs Cow<[u8]> to &[u8] identically), and spawn_symbolizer now continues on object.is_some(). The atomics use Relaxed ordering, which is correct for this write-once/read-many pattern with a zero sentinel; a partially-observed update degrades to "not classified" rather than misbehaving.

Other factors

  • #include <JavaScriptCore/ExecutableAllocator.h> was moved out of the Windows-only guard; startOfFixedExecutableMemoryPool is already used unconditionally-per-ENABLE(JIT) elsewhere in JSC and the PR description reports rust:check-all (10/10 targets) clean.
  • The test asserts exact offsets at start, start+0x100, and end-1, plus the exclusive-end and zero-PC boundaries, and uses ?.object on stackLine(end) so it tolerates whatever the platform lookup returns for the byte after the pool.
  • All prior CodeRabbit and claude[bot] inline comments are marked resolved with corresponding fixes in f09345b.

Deferring rather than approving because crash-handler signal-path changes plus a new cross-language FFI symbol warrant a maintainer glance, even though the mechanism is simple and the failure mode (mis-tagged frame) is diagnostic-only.

@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing as part of a cleanup of stale pull requests. This PR has had no new commits since 2026-07-24 and it conflicts with main. This is not a judgment on the fix itself. If the problem still reproduces on a current build, reopen this PR after a rebase or open a new one against main.

@robobun robobun closed this Sep 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.

2 participants