Skip to content

crash_handler: put the executable's debug id in the trace string - #38838

Open
robobun wants to merge 5 commits into
mainfrom
farm/6bb11dbc/trace-string-debug-id
Open

robobun wants to merge 5 commits into
mainfrom
farm/6bb11dbc/trace-string-debug-id

Conversation

@robobun

@robobun robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Blocked on oven-sh/bun.report#31 (the decoder) being deployed first; see Landing order below.

Problem

  • A trace string identifies the build it came from by the platform character and the 7-character sha (encode_trace_string, src/crash_handler/lib.rs). That is not a binary. Platform::CURRENT only encodes os and arch, so the glibc, musl and android builds of a commit all report 'l'/'L', while .buildkite/scripts/upload-release.sh publishes all three per arch (bun-linux-x64-profile.zip, -musl-profile.zip, -android-profile.zip), and bun.report downloads the glibc one for every 'l' trace. Every musl or android crash report is remapped against a different binary's symbols today, and the output looks like a normal remap.
  • The same failure with a separately built -baseline binary is where the Windows x64 reports a downstream build of commit 8bb8d04c4 sends come from (BUN-4B1Q, BUN-4BST, BUN-4B64, BUN-4BW6, BUN-4B2Y, about 60 events a day): both of its Windows x64 links report 'w', and symbolized against the first link's PDB every frame lands mid-instruction and the groups read as date/time-zone crashes; against the right PDB they are a GC marking-thread crash and an Intl.Segmenter crash (evidence in crash_handler(windows): unwind through LLInt and vmEntryToJavaScript frames and stop the walk at non-code PCs #38789). Upstream CI has shipped one x64 binary under both names since ci: single arm64 debian-13 build host; ThinLTO everywhere; baseline-only x64; rust+link merge; sysroots; WebKit a36c188; rust 2026-07-20 #34782, so re-emitting the old baseline platform characters would fix neither case.

Fix

  • Trace format '4': after the sha, a header of tagged fields: a VLQ field count, then per field a VLQ tag, a VLQ character count and that many characters (HeaderField, write_header_field). Tag 0 is the build flags VLQ (bit 0 = canary, replacing the '1'/'2' version-character split), tag 1 the executable's debug id in lowercase hex, omitted when the executable has none. Everything after the header is unchanged.
  • The id is read in the crash handler from the loader-mapped image headers (src/crash_handler/debug_id.rs): the PDB GUID of the PE CodeView record on Windows, the NT_GNU_BUILD_ID note on ELF, LC_UUID on Mach-O. Every offset is bounds-checked and nothing allocates. The bytes are emitted in the order the platform's tools print them, so the hex in a URL compares directly with readelf -n, llvm-readobj --coff-debug-directory / dumpbin, or dwarfdump --uuid output.
  • Why the id: it is the one value the linker writes into both the executable and its debug file, so it names exactly the build a trace came from whatever the artifacts are called and however many of them a commit has. With it, bun.report#31 checks the debug file it downloaded and tries the commit's other builds for the same platform character (musl, android, a separate baseline) until one carries the id, and leaves the frames raw, tagged, when none does; a triager can also tell from the URL alone which build a report belongs to.
  • Why tagged fields: this would otherwise be the third version-character bump in five months to add one field after the sha (two register-block attempts, then this), each needing its own decoder deploy before the emitter could ship. Decoders skip tags they do not know, so the next field (registers, for instance) is an append here and a later decoder change, with no new version character and no ordering constraint. Only this one bump has to be sequenced. Cost: four characters per trace over a fixed layout.
  • Also in test/cli/run/run-crash-handler.test.ts: the pre-existing system-PowerShell upload test awaits the ack instead of racing a 2s sleep (same change as test(crash_handler): await the PowerShell ack instead of racing a 2s sleep #36550). On the Windows 2019 agents without AVX PowerShell takes longer than 2s to start, so that test failed on every run of this file there, including this PR's first two CI runs, while the new tests and the auto-reporter tests passed.
  • Landing order: Decode trace format 4 and pick the debug file by the executable's debug id bun.report#31 has to be deployed before this merges, or new canaries' reports stop parsing. The in-repo consumer is the bun-tracestrings pin in package.json that scripts/runner.node.mjs uses for the CI remap server (already a year behind: it predates the 'a'/'b' reasons and the FreeBSD characters); it gets bumped to the bun.report commit in this PR once add npm install command before running bun #31 lands, so both consumers switch in the same merge. Open PRs whose tests decode trace strings positionally (crash_handler(windows): unwind through LLInt and vmEntryToJavaScript frames and stop the walk at non-code PCs #38789, crash_handler: symbolize frame 0 of a fault trace at the fault pc, not one byte before it #37533) will need to read past the header once this lands; sys/exe_format: LoadCommand borrows its bytes; patch commands by offset #37569 changes the LoadCommandIterator API debug_id.rs uses, so whichever lands second adapts.
  • Verified:
    • test/cli/run/run-crash-handler.test.ts, trace string identifies the build: crashes via panic and segfault, checks the version character and sha, decodes the header generically and requires exactly a build-flags field matching getFeatureData().is_canary and a debug-id field equal to an independent read of the executable file (ELF program headers, PE debug directory via PointerToRawData where the handler uses the RVA, Mach-O load commands), then decodes the features, frame list and reason payload to show nothing after the header moved. Fails on the current build (Expected: "4", Received: "2"), passes with bun bd test (debug + ASAN) on Linux x64.
    • Windows x64 debug build (fixed-layout revision of the header; the PE reader is unchanged since): the test passes, and the id in an emitted trace (944668034eead8624c4c44205044422e) equals the PDBGUID llvm-readobj prints for bun-debug.exe and the GUID llvm-pdbutil prints for bun-debug.pdb. The darwin CI lanes passed the test as well, covering the Mach-O reader.
    • Linux x64 debug build: the id in an emitted trace equals the Build ID line of readelf -n; the stripped release binary carries the same note as bun-profile (-Wl,--build-id=sha1 is unconditional in scripts/build/flags.ts), which is what lets bun.report match a user's binary to the profile artifact. The trace from this build is a parse fixture in bun.report#31.
    • cargo check -p bun_crash_handler for the linux, windows-msvc, apple-darwin, freebsd and android targets; cargo clippy clean on linux.

Background

  • Trace string: the https://bun.report/<version>/<payload> URL the crash handler prints and uploads. The payload is positional: platform character, command character, format version character, 7-character sha, then VLQ-encoded fields (features bitset, image-relative frame addresses, reason). bun.report's lib/parser.ts decodes it and downloads the commit's debug file to symbolize the addresses; scripts/runner.node.mjs runs the same decoder locally against the binary under test.
  • VLQ: the base64 variable-length integer encoding source maps use; every number in the payload is one, which is why new data cannot be inserted without a version character the decoder knows.
  • Debug id: the per-link identifier a linker stores in both the executable and its debug info so debuggers can pair them. PE keeps a GUID in a CodeView (RSDS) record referenced from the debug data directory and repeats it in the PDB; ELF keeps a hash of the output in a .note.gnu.build-id note inside a PT_NOTE segment; Mach-O keeps a UUID in an LC_UUID load command that the dSYM repeats. All three live in the image headers, which the loader maps read-only, so they can be read after a crash without trusting heap state.
  • Builds per commit: upstream publishes glibc, musl and android builds per Linux arch, plus -baseline copies of the x64 zips that have been the same binary since ci: single arm64 debian-13 build host; ThinLTO everywhere; baseline-only x64; rust+link merge; sysroots; WebKit a36c188; rust 2026-07-20 #34782 (they used to be a separate no-AVX build with its own platform characters, and some downstream trees still build one).

…mat 4)

A trace string identified its build by platform char plus 7-char sha, which
is not unique: one commit can be published as more than one link of the same
platform (the x64 and x64-baseline zips of a commit, or a re-run release
step), and bun.report had no way to tell which one a trace came from, so it
symbolized against whichever debug file the platform char mapped to.

Format 4 adds two fields after the sha: a build-flags VLQ (bit 0 = canary,
replacing the 1/2 version-char split) and the id the linker stamps into both
the executable and its debug info (PDB GUID, GNU build-id, LC_UUID) as a VLQ
byte count plus lowercase hex, in the byte order the platform tools print.
The id is read from the loader-mapped image headers inside the crash handler
without allocating.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4a14c2c8-162d-4d84-8471-9a00b97cf095

📥 Commits

Reviewing files that changed from the base of the PR and between 7adb357 and 8120f23.

📒 Files selected for processing (3)
  • src/crash_handler/debug_id.rs
  • src/crash_handler/lib.rs
  • test/cli/run/run-crash-handler.test.ts

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

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:53 AM PT - Aug 15th, 2026

✅ @robobun, your commit 8120f233079e95c99b2e22212cac586c55850239 passed in Build #97809! 🎉


🧪   To try this PR locally:

bunx bun-pr 38838

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

bun-38838 --bun

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: fixed here (encoder) and in oven-sh/bun.report#31 (decoder + picking the build by id); blocked on #31 being deployed before this merges.

  • Reproduced how: the new trace string identifies the build tests in test/cli/run/run-crash-handler.test.ts fail on the current build (Expected: "4", Received: "2", i.e. the trace carries nothing that names the binary) and pass with this branch on Linux x64 (debug + ASAN); the id they check is read independently from the executable file. A Windows x64 debug build emitted the PDB GUID llvm-readobj and llvm-pdbutil print for it, and the darwin CI lanes pass the test, covering the Mach-O reader. The same bug is live upstream for musl and android traces (all report 'l'), not only in the downstream tree the Sentry groups came from.
  • Current shape (8120f23): the format-4 header is tagged fields so later additions need no further version bump; details in the body. CI on 8120f23: every job that ran passed; the only ones not run are the two darwin 14 aarch64 shards no agent picks up, in every build of this PR. Review threads are all resolved.
  • Remaining: once add npm install command before running bun #31 lands, bump the bun-tracestrings pin in package.json to its commit in this PR.

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

I reviewed this PR and didn't find any bugs. Because it changes the trace-string wire format (with an explicit landing-order dependency on bun.report accepting '4' first) and adds ~250 lines of new platform-specific unsafe code that runs inside the crash handler across PE/ELF/Mach-O, a human should sign off on the format change and coordinate the merge with the companion bun.report PR.

Checked: every offset read in debug_id.rs is bounds-checked against the mapped image range and nothing allocates; BoundedArray::from_slice/Deref, bun_fmt::bytes_to_hex_lower, exe_image_range, LoadCommandIterator::cast, and callback_ctx all exist with matching signatures; tracePayload() pipes stdout without draining it but fixture-crash.js writes nothing there so it cannot deadlock; the test's independent on-disk PE/ELF/Mach-O readers use file offsets (PointerToRawData, p_offset) while the handler uses RVAs, so they genuinely cross-check rather than duplicate.

Extended reasoning...

Overview

This PR bumps the crash-handler trace-string format to version '4' and inserts two new fields between the 7-char sha and the features bitset: a build-flags VLQ (bit 0 = canary, replacing the old '1'/'2' version-char split) and the executable's debug id as a VLQ byte count followed by lowercase hex. The debug id is read at crash time from loader-mapped image headers by a new src/crash_handler/debug_id.rs (~250 lines): the PE CodeView RSDS GUID on Windows via exe_image_range(), LC_UUID on Mach-O via _dyld_get_image_header(0) + LoadCommandIterator, and the NT_GNU_BUILD_ID note on ELF via dl_iterate_phdr. encode_trace_string in lib.rs gains ~10 lines to emit the new fields. A ~200-line test in run-crash-handler.test.ts crashes via panic and segfault, independently reads the debug id from the executable file on disk (separate PE/ELF/Mach-O parsers using file offsets rather than RVAs), and decodes the full payload end-to-end to prove nothing after the new fields shifted.

Security risks

None user-facing. The new code only reads the process's own loader-mapped image headers; it accepts no external input. Every offset dereference in the PE and ELF-note walkers goes through bounds-checked .get(..) / checked_add, and the Mach-O path reuses the existing LoadCommandIterator::cast which length-checks before read_unaligned. The output is a fixed-size hex string written into a stack buffer ([0u8; 2 * MAX_LEN]), so there's no allocation inside the signal handler.

Level of scrutiny

High. This is (a) new unsafe code across three binary formats that runs inside a signal handler after the process is already in a bad state, and (b) a wire-format change to the trace string that every crash report carries. The PR description is explicit that bun.report must land its '4' decoder first or new canaries' reports stop parsing, and that the in-repo bun-tracestrings pin needs a follow-up bump. That landing-order coordination is a human decision, not something an automated review can green-light.

Other factors

The change is very well-executed: careful bounds-checking, SAFETY comments on every unsafe block, reuse of existing helpers (exe_image_range, LoadCommandIterator, callback_ctx, bytes_to_hex_lower, BoundedArray), and a strong test that cross-checks the in-memory reader against an independent on-disk reader and round-trips the reason payload. The PR description documents verification on Linux x64 and Windows x64 debug builds plus cargo check across all five target OSes. I found no correctness issues. The one candidate raised by finders — tracePayload() piping stdout without draining it — is not a problem because fixture-crash.js writes nothing to stdout. Deferring purely on scope (cross-platform unsafe crash-path code) and the stated external merge-order dependency, not on any defect.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

The decoder side is up as oven-sh/bun.report#31: it parses format 4, reads the same id out of the bun-profile executable in the profile zip it downloads, falls back to the other x64 link of the commit when the trace's own arch does not carry the id, and leaves the frames raw (tagged in Sentry) when nothing does. That is the half that actually fixes the mis-symbolized groups; this PR only makes the information available.

On the review note about tracePayload() not draining stdout: correct that the fixture writes nothing there, but I will drain it anyway in the next push, which will also bump the bun-tracestrings pin once bun.report#31 lands.

CI on 605e221: the two hard failures are both on the Windows 2019 x64 lane and unrelated to this diff. run-crash-handler.test.ts failed in the pre-existing PowerShell upload test that races a 2s sleep against PowerShell cold start on the no-AVX agent (#36550 fixes it); the two tests added here and the three upload tests passed on that lane, and the format-4 traces they printed carry the id e4509f66f3b4e4984c4c44205044422e, which is the CI binary's PDB GUID. test/bake/deinitialization.test.ts is a dev server crash on the same lane that this diff does not touch (reported separately).

Comment thread src/crash_handler/debug_id.rs Outdated
Comment thread src/crash_handler/debug_id.rs Outdated
Comment thread src/crash_handler/debug_id.rs Outdated
Comment thread src/crash_handler/debug_id.rs Outdated
Comment thread src/crash_handler/debug_id.rs Outdated
Comment thread src/crash_handler/debug_id.rs Outdated
Comment thread src/crash_handler/lib.rs Outdated
Comment thread src/crash_handler/lib.rs Outdated
Comment thread src/crash_handler/lib.rs Outdated
Comment thread src/crash_handler/debug_id.rs
Comment thread src/crash_handler/debug_id.rs
Comment thread src/crash_handler/lib.rs Outdated
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

27a7178 trims the comments the comment linter flagged: debug_id.rs is down to the struct offsets it reads and the GUID byte-order note, the module doc is four lines, and the format table on VERSION_CHAR keeps one line per format, which is the existing shape of that comment. The remaining flagged blocks are that table, the module doc, and the two-line note layout; none of them is justifying a workaround, they are the wire format and the header layouts the code indexes into, so I am leaving those as they are. Everything else in this push is comment-only; the code is unchanged from 605e221 apart from the test draining stdout (d711c28).

…g a 2s sleep

The Windows 2019 agents without AVX take longer than 2s to cold-start
PowerShell, so the system-PowerShell upload test failed on every run of this
file there while the three auto-reporter tests, which await the ack, passed.
Same change as #36550.

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

I reviewed this PR and the automated bug-hunt found no issues. Deferring to a human because this bumps the trace-string wire format (with an explicit cross-repo landing-order dependency on oven-sh/bun.report#31) and adds ~230 lines of new unsafe image-header parsing across three platforms inside the crash handler — both warrant a maintainer sign-off on timing and design.

What was reviewed:

  • encode_trace_string format-4 layout vs. the test decoder — build-flags VLQ and length-prefixed hex id sit between the sha and features without shifting later fields.
  • Bounds-checking in debug_id.rs: PE RVA reads use checked_add and slice get(); ELF note walk aligns to 4 and clamps the tail; Mach-O goes through LoadCommandIterator — none allocate.
  • The PowerShell test change replaces the 2s sleep race with awaiting the ack, matching the auto-reporter tests above it.
Extended reasoning...

Overview

This PR introduces trace-string format '4', which appends a build-flags VLQ and the executable's linker debug id (PDB GUID / GNU build-id / LC_UUID) after the 7-char sha in crash-report URLs. It adds a new src/crash_handler/debug_id.rs (~230 lines) that reads the id from the loader-mapped image headers on Windows (PE CodeView), macOS (Mach-O LC_UUID), and ELF (NT_GNU_BUILD_ID via dl_iterate_phdr), wires it into encode_trace_string in lib.rs, and adds a ~200-line test in run-crash-handler.test.ts that independently re-derives the id from the executable file on disk and decodes the full payload end-to-end. It also replaces a 2s-sleep race in the pre-existing PowerShell upload test with an awaited ack.

Security risks

Low. The new code only reads from the process's own read-only mapped image headers; there is no attacker-controlled input. Every offset dereference goes through bounds-checked slice get() / checked_add, and nothing allocates (fixed BoundedArray<u8, 20> + a 40-byte stack hex buffer), which is what the crash-handler context requires. The unsafe blocks are for from_raw_parts over loader-guaranteed mappings and each carries a SAFETY comment naming the guarantee.

Level of scrutiny

High. Three independent reasons: (1) the author states a hard landing-order requirement — bun.report#31 must be deployed first or new canaries' crash reports stop parsing, and the bun-tracestrings pin bump is still pending; a human needs to coordinate merge timing. (2) This is new unsafe binary-format parsing that runs after a crash, where a wrong offset would fault inside the handler and lose the report entirely — the ELF path's PT_NOTE-inside-PT_LOAD assumption and the PE AddressOfRawData != 0 guard are the kind of thing a maintainer familiar with these formats should eyeball. (3) It changes a wire format with an external consumer, replacing the '1'/'2' canary encoding with a flags VLQ.

Other factors

The test coverage is strong: it re-implements the id extraction independently (file offsets rather than mapped RVAs) for all three formats and round-trips the entire payload including the reason bytes, so a mis-encoded field would show up. The comment-cop bot flags were addressed in 27a7178 and the remaining ones are format-layout tables the author reasonably kept. No human reviewer has looked at this yet.

Instead of two fixed fields after the hash, the header is a VLQ field count
followed by (tag, char count, chars) fields: tag 0 the build flags VLQ, tag 1
the debug id in hex. A decoder skips tags it does not know, so the next field
added to a trace string needs neither a new version char nor a decoder deploy
ahead of the emitter.
Comment thread src/crash_handler/lib.rs
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Two changes since the last round, both from going back over whether this PR is worth having in this shape; the body is rewritten to match.

  1. The problem is live upstream, not only in the downstream tree the Sentry groups came from. Platform::CURRENT encodes os and arch only, so the glibc, musl and android builds of a commit all report 'l'/'L', upload-release.sh publishes all three per arch, and bun.report downloads the glibc one for every 'l' trace. Every musl or android trace is symbolized against the wrong binary today. bun.report#31 now tries each of a commit's builds for the platform character (musl, android, then the -baseline name on x64) until one carries the id, and reports which one matched as Sentry's dist.

  2. 8120f23 makes the format-4 header tagged fields (field count, then tag / length / chars per field; tag 0 build flags, tag 1 debug id) instead of two fixed fields. Decoders skip tags they do not know, so this is the last version-character bump needed to add a header field: the register block that has been attempted twice, or anything else, becomes an append on this side and a later decoder change, with no deploy ordering. Four characters per trace over the fixed layout. The decoder in bun.report#31 is updated to match, with a test that unknown tags are skipped and a real trace from this build as a fixture.

Test coverage here is unchanged in kind: the new tests decode the header generically and require exactly the two fields, with the id checked against an independent read of the executable file. Still blocked on bun.report#31 deploying; the bun-tracestrings pin bump goes into this PR at that point.

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

I reviewed this PR and didn't find any bugs. Deferring to a human because it introduces a new trace-string wire format ('4') with a cross-repo landing-order dependency (bun.report#31 must be deployed first, and the bun-tracestrings pin bump is still pending), and adds ~230 lines of new unsafe Rust that walks PE/ELF/Mach-O image headers inside the crash handler — both the format design and the merge timing want a maintainer sign-off.

What was reviewed:

  • debug_id.rs: bounds-checking on all header offset reads (checked_add, slice::get) — every deref is guarded; no allocation in the crash path.
  • encode_trace_string: the new tagged-header encoding — field count matches emitted fields, and the features/frames/reason tail is unchanged, confirmed by the test decoding it end-to-end.
  • The independent on-disk PE/ELF/Mach-O readers in the test vs. the in-memory readers — same byte-order handling for the PDB GUID's little-endian fields.
  • The PowerShell test change replaces the 2s sleep race with the same await acked.promise pattern the auto-reporter tests already use.
Extended reasoning...

Overview

This PR bumps the crash-handler trace-string format to '4' and inserts a tagged header after the 7-char sha: a VLQ field count followed by (tag, char-count, chars) triples, currently carrying build flags (bit 0 = canary, replacing the '1'/'2' split) and the executable's debug id in lowercase hex. A new src/crash_handler/debug_id.rs reads that id from the loader-mapped image headers — the PE CodeView RSDS GUID on Windows, the NT_GNU_BUILD_ID note on ELF, LC_UUID on Mach-O — with every offset bounds-checked and no allocation. test/cli/run/run-crash-handler.test.ts gains a describe block that crashes via panic and segfault, decodes the emitted URL byte-by-byte, and compares the id against an independent on-disk read of the executable file. It also fixes the pre-existing PowerShell upload test to await the ack instead of racing a 2s sleep.

Security risks

None user-facing. The new code reads the process's own image headers (already mapped read-only by the loader) and emits a hex string into the crash URL. The unsafe blocks are carefully scoped: from_raw_parts over ranges the loader guarantees mapped, with all subsequent reads going through bounds-checked .get() / checked_add. The worst case for a malformed header is None, which just omits the debug-id field. This runs post-crash, so it does not expand the attack surface of a healthy process.

Level of scrutiny

High. This is not a mechanical change: it defines a new extensible wire format that an external service (bun.report) must decode, with an explicit landing-order constraint the author calls out — bun.report#31 must be deployed before this merges or new canaries' reports stop parsing, and the in-repo bun-tracestrings pin still needs bumping. The tagged-header design (skip-unknown-tags forward compatibility) is a real API decision a maintainer should ratify. The debug-id readers are ~230 lines of platform-conditional unsafe code that run inside a signal handler after a crash; while the bounds-checking looks thorough and the tests cross-validate against an independent file-based reader, this is exactly the kind of code that benefits from a second pair of eyes on the PE/ELF/Mach-O offset arithmetic.

Other factors

The tests are strong: they decode the full payload (header, features, frame list, reason) to prove nothing after the new fields shifted, and read the id independently from disk on all three platforms. CI passed on darwin (Mach-O) and the author verified Windows and Linux manually against llvm-readobj/llvm-pdbutil/readelf -n. The one open comment-cop flag on lib.rs:2407 targets the VERSION_CHAR format table, which the author has already argued (and I agree) is documenting the wire format rather than justifying a workaround. Given the cross-repo coordination and the wire-format design decision, a human should confirm the bun.report side is deployed and sign off on the format shape before merging.

dylan-conway pushed a commit that referenced this pull request Aug 21, 2026
…g platform characters (#39801)

Merge after oven-sh/bun.report#32 is deployed. bun.report rejects the
new characters until then.

### Problem
- `Platform::CURRENT` (src/crash_handler/lib.rs) emits `'l'`/`'L'` for
every Linux build. The glibc, musl and android builds of a commit are
three binaries with three profile zips, and bun.report picks one by this
character. Every musl or android crash report is symbolicated against
the glibc binary and shows plausible but wrong frames.
- BUN-4MRJ is one: reported as a segfault in
`NestedRuleParser::parse_block` (`css_parser.rs:1738`), it is a
`VariableEnvironment` lookup on a bad `UniquedStringImpl*` against the
musl profile (notes).

### Fix
- musl builds emit `'u'`/`'U'`, android builds `'a'`/`'A'`, and
`'l'`/`'L'` mean glibc. Lowercase is x86_64, uppercase aarch64.
bun.report#32 decodes them.
- Shipped binaries keep emitting `'l'`/`'L'`, which bun.report keeps
decoding as glibc.
- The format is unchanged. #38838 (a debug id in a new format) verifies
the downloaded file instead. The two compose: here the first download is
right, and the URL says which build crashed.
- Verified: CI build 101799 ran an assertion on the Alpine x64 and
aarch64 lanes that the build emits `'u'`/`'U'`. It passed and was then
removed at review, since it only mirrored the table. Also `cargo check
-p bun_crash_handler` for gnu, musl and android, and clippy.

### Background
- Trace string: the `https://bun.report/<version>/<payload>` URL in a
crash report. The payload starts with the platform character, then
command character, format version and sha. bun.report maps the character
to an artifact name.
- The baseline characters (`'B'`, `'b'`, `'e'`) did the same for the old
baseline binaries until #34782 made x64 one binary.

<details><summary>Notes</summary>

The human-readable report header already says `musl` or `Android ...
bionic`; only the trace string lacked the distinction.

BUN-4MRJ, trace `1.4.0/L_134cbb9aEggggC+98pvDA2Dhggw6jC` (commit
`34cbb9a40`), one address `0x37a79df`, fault address
`0xFFFFFFFFBC2C0000`. `llvm-symbolizer --inlines --relative-address`
against the 1.4.0 release profiles:

- `bun-linux-aarch64-profile.zip` (what bun.report used):
`<NestedRuleParser<BundlerAtRuleParser> as AtRuleParser>::parse_block`,
`src/css/css_parser.rs:1738`.
- `bun-linux-aarch64-musl-profile.zip`, fault pc `0x37a79e0`:
`WTF::InlineMap<PackedRefPtr<UniquedStringImpl>,
JSC::VariableEnvironmentEntry, 9>::findKeyOrEmpty<const
UniquedStringImpl*>` (`InlineMap.h:725`) > `findKeyOrEmptyInStorage`
(`InlineMap.h:703`) > `JSC::IdentifierRepHash::hash`
(`Identifier.h:235`) > `SymbolImpl::existingSymbolAwareHash` >
`StringImpl::isSymbol` (`StringImpl.h:334`). That is the load of
`m_hashAndFlags` (offset 0x10) through the lookup key, so the key the
caller passed was `0xFFFFFFFFBC2BFFF0`: a user-space pointer truncated
to 32 bits and sign-extended. The scope had more than 9 variables
(out-of-line storage). The trace has no caller frame, so the report says
nothing more about the crash. It is not a CSS crash. It is almost
certainly a musl build: the musl reading accounts for the fault address
exactly (key + 0x10), the glibc reading needs a frame pointer that
changed between two adjacent loads, and in the android build the address
is data (`.text` ends at 0x32ea060).

Which byte values: `'u'`/`'U'` and `'a'`/`'A'` are unused in
bun.report's table (`w e W m b M l B L f F`). The reason and command
characters are separate positions, so they do not constrain the choice.

`scripts/ci-remap-server` pins `bun-tracestrings` at bun.report 912ca63,
which does not decode these characters (nor the `'a'`/`'b'` reasons or
FreeBSD from #29 and bun.report#29). A crash on the Alpine CI lanes
prints the raw trace string in the annotation instead of a remap until
the pin is bumped, as SIGABRT and SIGTRAP crashes do on every lane
today. #38838 bumps the pin as part of its landing.

</details>

This branch has not been deployed

No deployments
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