Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan includes up to 5 reviews per rolling hour; 0 remain after this review. WalkthroughStandalone module graph deserialization now validates serialized ranges, pointers, NUL termination, entry-point IDs, and duplicate module names. It returns structured corruption errors. Regression tests cover corrupted standalone binaries. ChangesStandalone module graph corruption handling
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Added a test ( Checked the scenario by hand on macOS with a debug build of this branch plus |
There was a problem hiding this comment.
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 `@src/standalone_graph/StandaloneModuleGraph.rs`:
- Around line 735-736: Update the
CorruptedModuleGraphEntryPointIDIsGreaterThanModuleListCount diagnostic in
src/standalone_graph/error.rs to state that entry-point IDs greater than or
equal to the loaded module count are invalid, and keep StandaloneModuleGraph’s
boundary check mapped to that error. In test/bundler/bun-build-compile.test.ts
lines 181-185, assert the complete corrected corruption diagnostic instead of
only the generic “Corrupted module graph” text.
🪄 Autofix
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: b40e8a2d-b388-4820-986f-f2f47a2bffd0
📒 Files selected for processing (2)
src/standalone_graph/StandaloneModuleGraph.rstest/bundler/bun-build-compile.test.ts
There was a problem hiding this comment.
LGTM — thanks for adding the corrupted-trailer test.
What was reviewed
- The
>= modules.count()bound matches howentry_point()indexesfiles.values_mut(); moving it after the insert loop covers the dedup-by-name case. - Making
entry_point_idprivate: grepped for external readers — onlyentry_point()in the same file uses it. - Test offset math:
Offsetsis#[repr(C)]withusize + StringPointer{u32,u32}beforeentry_point_id, sotrailer - 16is correct; thetoBe(0)pre-assert guards a mis-located trailer. - macOS
skipIfhas a stated reason andfrom_bytesis platform-shared.
Extended reasoning...
Overview
Two-file change. In src/standalone_graph/StandaloneModuleGraph.rs, the entry_point_id bounds check inside from_bytes moves from before the module-record loop to after the StringArrayHashMap is populated, and tightens from > modules_list_count to >= modules.count(). The struct field goes pub → private. In test/bundler/bun-build-compile.test.ts, a new test compiles a single-module binary, byte-patches the trailer's entry_point_id from 0 to 1, and asserts the child prints the "Corrupted module graph" error and exits 1.
Security risks
None. The path is only reached when a bun build --compile output has been corrupted on disk; the change turns an index panic into a clean error return on that already-broken input. No new parsing, no new trust boundary.
Level of scrutiny
Low. The consumer of this field is a single line, files.values_mut()[entry_point_id] at StandaloneModuleGraph.rs:158, so the correct invariant (< files.count()) is directly readable. I verified the #[repr(C)] layout of Offsets (usize=8, StringPointer{u32,u32}=8, then u32 at +16; struct size 32) against the test's at - 16 patch offset, and confirmed to_bytes appends Offsets immediately before TRAILER (StandaloneModuleGraph.rs:1103-1104) so the backward scan lands on the payload copy. Field-visibility change: grep shows no reads of StandaloneModuleGraph::entry_point_id outside this file; cargo check was run per the description.
Other factors
My prior review asked for a test; the author added a spawned-fixture test that patches the crashing input directly, matching REVIEW.md's "crash fixes need the crashing input as a spawned fixture". The test drains stdout/stderr/exit concurrently, uses tempDir/bunEnv/using, and pre-asserts the un-patched value is 0 so a mis-located trailer would fail loudly rather than silently pass. The macOS skip carries a comment naming the re-signing constraint and notes the guarded code is platform-shared, which satisfies the "every skip needs a comment with the observed failure" rule.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
src/standalone_graph/StandaloneModuleGraph.rs (2)
732-738: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winRestore the early serialized-count check.
The post-insertion check is correct, but it runs after
modules.reserve, allread_unalignedcalls, allslice_to*calls, and everymodules.put. Retain the earlierentry_point_id >= modules_list_countguard before the loop. Keep thismodules.count()check as the second guard for records that collapse during insertion.A malformed binary should not enter the unsafe deserialization path when its entry-point ID is already impossible.
Preserve both validation phases
let modules_list_base = modules_list_bytes.as_ptr(); +if offsets.entry_point_id as usize >= modules_list_count { + return Err(crate::Error::CorruptedModuleGraphEntryPointIDOutOfRange); +} + let mut modules = StringArrayHashMap::<File>::new();As per coding guidelines: “Validate untrusted input before processing, allocation, or side effects.”
🤖 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/standalone_graph/StandaloneModuleGraph.rs` around lines 732 - 738, Restore the early entry-point validation before modules.reserve and the deserialization/insertion loop, checking entry_point_id against the serialized modules_list_count and returning CorruptedModuleGraphEntryPointIDOutOfRange when invalid. Retain the existing post-insertion offsets.entry_point_id versus modules.count() check to handle records that collapse during insertion.Source: Coding guidelines
732-738: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winReject duplicate module names during deserialization.
StringArrayHashMap::putreplaces duplicate keys without changing their ordinal.entry_point_idthen can select the wrongFile, or a valid serialized ordinal can fail themodules.count()check.🤖 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/standalone_graph/StandaloneModuleGraph.rs` around lines 732 - 738, Update the deserialization logic surrounding the entry-point validation to detect and reject duplicate module names when inserting into the StringArrayHashMap, rather than allowing put to replace an existing entry. Return the appropriate corrupted-module-graph error immediately on a duplicate, while preserving valid ordinal validation and entry_point() behavior for unique names.
🤖 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/standalone_graph/error.rs`:
- Around line 3-4: Update the corrupted module-graph error handling around
CorruptedModuleGraphEntryPointIDOutOfRange so the user-facing diagnostic
includes offsets.entry_point_id, modules.count(), and a hint to rebuild the
standalone executable. Modify the to_bytes() failure path to format the error
via Display rather than e.name(), while preserving Error::name() and its stable
ErrName value for callers.
---
Outside diff comments:
In `@src/standalone_graph/StandaloneModuleGraph.rs`:
- Around line 732-738: Restore the early entry-point validation before
modules.reserve and the deserialization/insertion loop, checking entry_point_id
against the serialized modules_list_count and returning
CorruptedModuleGraphEntryPointIDOutOfRange when invalid. Retain the existing
post-insertion offsets.entry_point_id versus modules.count() check to handle
records that collapse during insertion.
- Around line 732-738: Update the deserialization logic surrounding the
entry-point validation to detect and reject duplicate module names when
inserting into the StringArrayHashMap, rather than allowing put to replace an
existing entry. Return the appropriate corrupted-module-graph error immediately
on a duplicate, while preserving valid ordinal validation and entry_point()
behavior for unique names.
🪄 Autofix
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: de3c8092-7ffe-4019-9073-8a3397ea68d8
📒 Files selected for processing (3)
src/standalone_graph/StandaloneModuleGraph.rssrc/standalone_graph/error.rstest/bundler/bun-build-compile.test.ts
There was a problem hiding this comment.
LGTM — the earlier missing-test and error-wording feedback are both addressed; the remaining inline finding is a diagnostic-readability nit and non-blocking.
Extended reasoning...
Overview
This PR tightens the corrupted-trailer guard in StandaloneModuleGraph::from_bytes: the entry_point_id bound moves from > modules_list_count (checked before the map is built) to >= modules.count() (checked after insertion), the field is made module-private, and the error variant is renamed CorruptedModuleGraphEntryPointIDOutOfRange with an accurate message. A new regression test in test/bundler/bun-build-compile.test.ts compiles a one-module app, patches entry_point_id in the on-disk trailer to equal the module count, and asserts the binary exits 1 with the specific corruption message on stderr.
Prior feedback resolved
My earlier review flagged the absence of an automated test — that's now addressed with the spawned-fixture test (the shape I suggested). CodeRabbit flagged the misleading "greater than" wording — addressed by the rename to ...OutOfRange / "out of range for the module list", and the test asserts the full new message. Both threads are marked resolved. I confirmed no stale references to the old variant name remain anywhere in the tree.
Correctness verification
I checked the test's offset arithmetic against the source: Offsets is #[repr(C)] with byte_count: usize (8) + modules_ptr: StringPointer (8) placing entry_point_id at struct offset 16, and to_bytes writes the 32-byte Offsets immediately before TRAILER — so trailer_start - 16 is correct. The test also self-guards with expect(view.getUint32(at - 16, true)).toBe(0) before patching, so a future layout change fails the precondition rather than passing vacuously. The macOS skip is justified in a comment (patched Mach-O needs Bun's ad-hoc re-signer; from_bytes is platform-agnostic). I also re-checked the ruled-out raw_len == 0 early return — it constructs an empty graph with entry_point_id: 0, but that path is only taken when there's no embedded section at all, in which case entry_point() is never called on it; not a regression from this change.
Security risks
None. This path only runs when parsing a self-produced compiled binary's trailer, and the change strictly tightens validation (panic → clean error). Making entry_point_id private removes the only way to set it without going through the validated constructor.
Level of scrutiny & other factors
Low-risk error-path hardening: ~15 net lines, unreachable from any well-formed bun build --compile output. The one inline finding this run — collapsing stderr to a boolean in the combined-object assertion — is a legitimate test-diagnostic nit (the failure diff would show stderr: false instead of the actual output), but the assertion is not vacuous and passes/fails on the right condition. Worth a follow-up tweak to expect.stringContaining(...), not worth holding the merge.
|
Updated 10:58 PM PT - Aug 17th, 2026
✅ @robobun, your commit 7e1e636d4da78013cf8182bc5144ab1adf6fe4d7 passed in 🧪 To try this PR locally: bunx bun-pr 37639That installs a local version of the PR into your bun-37639 --bun |
There was a problem hiding this comment.
LGTM — thanks for addressing the earlier feedback. All three rounds are covered: the test now exists, the error variant/message name the bound it enforces, and the assertion keeps the raw stderr in the diff.
What was reviewed:
- Verified
entry_point_idhas no readers outside this module, so making it private is safe; the bundler.entry_point_id()hits are on a different type. - Checked the
#[repr(C)]Offsetslayout (usize + {u32,u32} + u32 …) —at - 16lands onentry_point_id, and thetoBe(0)precondition guards the offset arithmetic. - Confirmed the variant rename is complete (three sites) and no stale references to the old name remain.
- The unresolved CodeRabbit note about embedding the id/count in the diagnostic is a nice-to-have; the enum is
Copyunit variants and enriching one is out of scope here.
Extended reasoning...
Overview
This PR tightens a bounds check in StandaloneModuleGraph::from_bytes: the entry_point_id from a compiled binary's trailer is now validated as >= modules.count() after the module map is populated, rather than > modules_list_count before it. This closes an off-by-one (== count passed the old check) and a dedup gap (the map is keyed by name, so it can hold fewer entries than the raw record count). The field is made private since its only consumer is entry_point() in the same file. The error variant is renamed from ...IsGreaterThanModuleListCount to ...OutOfRange with matching Display and name() text. A regression test compiles a one-module binary, patches entry_point_id in the trailer to equal the module count, and asserts exit 1 with the specific corruption message on stderr (skipped on macOS because a patched Mach-O needs re-signing; from_bytes is platform-shared).
Security risks
None. This is defensive validation of a compiled binary's own embedded trailer — reachable only when the executable itself is corrupted. The change makes a panic into a clean error return; it doesn't widen any input surface.
Level of scrutiny
Low-to-moderate. The Rust change is a five-line move-and-tighten of an existing guard on an error path no well-formed binary hits. The test is more involved (hand-computed byte offset into a #[repr(C)] struct across Linux and Windows), but it self-checks the offset via expect(view.getUint32(at - 16, true)).toBe(0) and asserts the exact diagnostic string, so a layout drift or wrong-occurrence match would fail loudly rather than pass vacuously.
Other factors
I reviewed this PR twice previously. Both of my findings (missing test; boolean-collapsed stderr in the assertion) have been addressed in commits 3d556fd and 07641b7. CodeRabbit's wording nit was also addressed in 4870846. The one remaining unresolved CodeRabbit comment suggests embedding the actual id and count values in the diagnostic — a reasonable enhancement, but the Error enum here is a Copy set of unit variants (matching the file's existing pattern for the other corruption cases), and restructuring it is orthogonal to this bounds fix. I grepped for external readers of .entry_point_id on StandaloneModuleGraph (none — the bundler hits are on Chunk::entry_point) and for stale references to the old variant name (none).
…it checks - rename CorruptedModuleGraphEntryPointIDIsGreaterThanModuleListCount to ...OutOfRange and reword the message, since == count is now rejected too - test asserts the full diagnostic No-Verification-Needed: error text and test assertion only
…res show it No-Verification-Needed: test assertion shape only
07641b7 to
71fa7fb
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
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/bundler/bun-build-compile.test.ts`:
- Around line 150-186: Update the corrupted-entry test to build an artifact
containing at least two serialized module records, then patch one record’s name
to duplicate another and set entry_point_id to modules.count() while keeping it
below modules_list_count. Assert the corruption error and nonzero exit, ensuring
the fixture reaches the post-insertion bound guard rather than the existing
pre-insertion check; keep the test anchored to the current Bun.build and
binary-patching flow.
🪄 Autofix
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: 8f6e605d-014f-45e2-8b1c-d7f5a9afbf19
📒 Files selected for processing (3)
src/standalone_graph/StandaloneModuleGraph.rssrc/standalone_graph/error.rstest/bundler/bun-build-compile.test.ts
There was a problem hiding this comment.
LGTM — the earlier feedback (add a regression test, keep raw stderr in the assertion, rename the error to reflect the inclusive bound) has all been addressed and the resulting change is small and self-contained.
What was reviewed:
from_bytesnow boundsentry_point_idbymodules.count()after insertion with>=, matching the index inentry_point()(files.values_mut()[id]); making the field private is safe — its only reader is that method.- Verified
StringPointeris{u32,u32}andOffsetsis 32 bytes, so the test'sat - 16locatesentry_point_id; thegetUint32(...) === 0pre-check guards against future layout drift. - No stale references to the old
...IsGreaterThanModuleListCountvariant or message string remain in the tree.
Extended reasoning...
Overview
Three files: src/standalone_graph/StandaloneModuleGraph.rs moves the entry_point_id bounds check from before the module-record loop (> modules_list_count) to after the name-keyed map is built (>= modules.count()) and makes the struct field private; src/standalone_graph/error.rs renames the variant to CorruptedModuleGraphEntryPointIDOutOfRange and updates both the thiserror display string and name(); test/bundler/bun-build-compile.test.ts adds a test that compiles a one-module app, patches the trailer's entry_point_id to equal the module count, and asserts exit code 1 with the corruption message on stderr.
Security risks
None. This is defense-in-depth for a corrupted bun build --compile executable's own trailer — the input is the process's own image, not attacker-controlled network or file data, and the change turns an index-out-of-bounds panic into a typed error return. No auth, crypto, or permission surface is touched.
Level of scrutiny
Low. The code path is only reachable when a compiled standalone binary has been damaged; well-formed outputs never hit the guard. The fix is a two-line comparison change (> → >=, modules_list_count → modules.count()) placed after the map is populated, which is exactly what entry_point()'s files.values_mut()[self.entry_point_id as usize] requires. I confirmed entry_point() is the only reader of the now-private field, and grepped for the old variant name/message to confirm the rename is complete.
Other factors
All four prior review threads (mine and CodeRabbit's) are resolved: the test was added, the assertion now uses expect.stringContaining(...) so failure diffs surface the real stderr, the error wording was corrected for the inclusive bound, and the author's decision to keep the error fieldless (it's a Copy enum feeding ErrName) was accepted by CodeRabbit. The test's byte-offset arithmetic checks out against the #[repr(C)] Offsets layout (usize + {u32,u32} + u32 … = offset 16 in a 32-byte struct), and it self-guards by asserting the pre-patch value is 0 before overwriting. The macOS skip is justified (patched Mach-O needs re-signing) and from_bytes is platform-shared, so Linux/Windows coverage is sufficient.
|
@robobun get this mergeable |
|
Head is 29f98f5: the widened fix from 3a99fb3, plus a merge of main (the binary-size check on 100399 was comparing a branch 187 commits behind main, with per-target deltas from -2 MB to +0.5 MB) and the one review nit on the new source map exit. CI is running on it and no review threads are open. |
…loaded files The corrupted-graph test now also builds a two-module executable, points the second record's name at the first's so the graph loads a single file, and sets entry_point_id to 1, which is below the record count but past the files that were actually loaded. Both cases share one helper that locates the trailer, Offsets and the module records. The tests get the same 60s budget as the other tests in this file that read back and rewrite the whole executable.
There was a problem hiding this comment.
LGTM — the earlier feedback (missing test, boolean-collapsed stderr assertion) is addressed and the duplicate-name case is now covered.
What was reviewed:
>= modules.count()after insertion vs.entry_point()'s index at line 158;entry_point_idmade private with no external readers.Offsetslayout (32 bytes, id at +16),CompiledModuleGraphFile(52 bytes), andTRAILERmatch the test's byte-patching constants.- Error variant rename: only two reference sites, both updated.
Extended reasoning...
Overview
Tightens the corrupted-trailer guard in StandaloneModuleGraph::from_bytes: the entry_point_id bound moves from > modules_list_count (checked before the map is built) to >= modules.count() (checked after), closing both the off-by-one at == count and the case where duplicate module names collapse the map below the record count. entry_point_id is made module-private (its only reader is entry_point() at line 158). The error variant is renamed to CorruptedModuleGraphEntryPointIDOutOfRange with matching Display/name() text. A test.each in bun-build-compile.test.ts compiles a binary, byte-patches the trailer for each of the two failure modes, and asserts exit 1 with the corruption diagnostic on stderr.
Security risks
None. This is defensive validation of Bun's own embedded module graph — reachable only via a hand-corrupted bun build --compile output. The change strictly tightens a guard (panic → clean error) and does not touch auth, crypto, network, or user-controlled parsing paths.
Level of scrutiny
Low-to-moderate. The Rust change is a four-line guard relocation plus a visibility tweak and rename in an error path no well-formed binary reaches. I verified the struct layouts the test hard-codes against source (Offsets repr(C): usize + StringPointer + u32 + StringPointer + u32 = 32 bytes, entry_point_id at offset 16; CompiledModuleGraphFile = 6×StringPointer + 4×u8 = 52 bytes; TRAILER = b"\n---- Bun! ----\n"), confirmed the bundler's .entry_point_id() hits are on an unrelated Chunk type so the visibility change is safe, and confirmed the renamed variant has no other references.
Other factors
All four prior review threads are resolved: my request for a test (added), my note on the boolean-collapsed stderr assertion (now expect.stringContaining on the raw string), CodeRabbit's wording nit (message reworded to "out of range"), and CodeRabbit's request to exercise the post-insertion path (second test.each case duplicates a module name so entry_point_id = 1 < modules_list_count but >= modules.count()). robobun reports the prior build passed on all lanes except two cancelled darwin-26 jobs that are main-only. The macOS skip is justified in-comment (Mach-O re-signing) and from_bytes is platform-shared. The 60s per-test timeout is commented with the reason (~1 GB debug+ASAN executable read/rewritten).
There was a problem hiding this comment.
LGTM — the earlier feedback (add a regression test; keep raw stderr in the combined-object assertion; cover the duplicated-name dedup gap) has all been applied.
Checked the test's byte-offset arithmetic against the #[repr(C)] layouts in StandaloneModuleGraph.rs: Offsets is 32 bytes with entry_point_id at +16, CompiledModuleGraphFile is 52 bytes with name at +0, and the trailer string matches TRAILER. Confirmed entry_point_id's only reader is entry_point() in the same file, so narrowing it to private is safe. The renamed error variant is fully updated in both the #[error(...)] attribute and name().
Extended reasoning...
Overview
Tightens a corruption guard in StandaloneModuleGraph::from_bytes: the entry_point_id bound check moves from > modules_list_count (before the map is built) to >= modules.count() (after), closing an off-by-one and the case where duplicate module names collapse the map below the record count. The error variant is renamed from ...IsGreaterThanModuleListCount to ...OutOfRange with matching message text, and entry_point_id on the graph struct is made module-private since its only reader is entry_point(). A two-case regression test in test/bundler/bun-build-compile.test.ts compiles an executable, patches the trailer, and asserts the corruption error surfaces with exit code 1 instead of an index-out-of-bounds panic.
Security risks
None. The path validates Bun's own --compile output; the change turns a panic into a diagnostic on a corrupted trailer. It does not widen what is accepted, add parsing of external input, or touch auth/crypto/permissions.
Level of scrutiny
Low-to-medium. This is a small error-path hardening reachable only via a damaged compiled executable — well-formed binaries never hit the branch. The logic change is a two-line move-and-tighten; the rest is a rename and visibility narrowing. I verified the test's hard-coded offsets (OFFSETS_SIZE=32, MODULE_RECORD_SIZE=52, entryPointIdAt = offsetsAt+16, name pointer at record +0, trailer literal) against the #[repr(C)] structs and StringPointer {u32, u32} in source, and grepped for external readers of entry_point_id (none outside this file).
Other factors
All prior review threads are resolved: my request for a test and for expect.stringContaining on raw stderr were applied; CodeRabbit's request to also cover the post-insertion dedup gap was addressed with the second test.each case; the author declined to make the error carry the id/count with a stated reason (fieldless Copy enum feeding ErrName, numbers not actionable) and CodeRabbit withdrew. The test asserts its own precondition ({ moduleRecordCount, entryPointId: 0 }) before corrupting, drains all pipes concurrently, uses tempDir/bunEnv/await using, and justifies both the macOS skip and the 60s timeout in comments. robobun reports CI is rerunning after a clean merge of main and that only an approval remains.
There was a problem hiding this comment.
LGTM — both my earlier points (missing test, boolean-collapsed stderr assertion) are addressed, and CI is green on all lanes.
What was reviewed:
- The bounds check:
>= modules.count()after the map is built covers both the off-by-one and the duplicate-name-dedup gap;entry_point()indexesfiles.values_mut()[id], so this is the right bound. - Test byte-offset math against the
repr(C)Offsets(32 bytes,entry_point_idat +16) andCompiledModuleGraphFile(52 bytes,nameat +0) — matches the source structs. - The renamed error variant has no remaining references to the old name;
entry_point_idgoing private breaks no external readers.
Extended reasoning...
Overview
This PR tightens the corrupted-trailer guard in StandaloneModuleGraph::from_bytes (src/standalone_graph/StandaloneModuleGraph.rs). The old check entry_point_id > modules_list_count ran before the module map was built and had two gaps: it accepted id == count (off-by-one), and it compared against the raw record count rather than the number of files actually inserted into the name-keyed map (which can be smaller if records share a name). Either gap let a corrupted bun build --compile binary reach entry_point() and panic on an out-of-bounds index instead of returning the error the guard exists for. The fix moves the check after the map is built and uses >= modules.count(). The error variant is renamed to CorruptedModuleGraphEntryPointIDOutOfRange with matching message text, and entry_point_id on the graph struct is made module-private since its only reader is entry_point() in the same file. Two regression tests in test/bundler/bun-build-compile.test.ts compile a real executable, patch the trailer to hit each gap, and assert exit 1 with the corruption message on stderr.
Security risks
None introduced. This is defensive hardening on an error path that only fires for a damaged executable — the change strictly narrows what from_bytes accepts. No new parsing of untrusted data, no new allocation before validation, no auth/crypto/permissions surface.
Level of scrutiny
Low-to-medium. The Rust change is a one-condition bounds check moved and tightened, plus a mechanical variant rename; it only affects the error path for corrupted standalone binaries, never well-formed ones. I verified the test's hand-computed byte offsets against the repr(C) layouts in source: Offsets is 32 bytes with byte_count at +0, modules_ptr.{offset,length} at +8/+12, entry_point_id at +16; CompiledModuleGraphFile is 6×StringPointer (48) + 4 repr(u8) enums = 52 with name at +0. The duplicate-name case copies exactly the first record's 8-byte name pointer over the second record's, which is the right corruption to make the map dedup to one entry.
Other factors
Both of my prior review comments are addressed: the missing test was added (and expanded per the later CodeRabbit thread to cover the dedup gap with a second case), and the assertion now keeps raw stderr in the object with expect.stringContaining(...) on the expected side. All inline threads on the PR are resolved. The author declined to add dynamic id/count to the diagnostic with a sound reason (fieldless Copy enum feeding ErrName; the numbers don't help someone holding a damaged binary), and CodeRabbit withdrew that finding. robobun reports build 94851 passed on all 179 jobs. Grep confirms no remaining references to the old variant name and no external readers of the now-private entry_point_id field. The macOS skip is justified in a comment (patched Mach-O needs re-signing; from_bytes is platform-shared) and the 60s timeout is justified (compiles and rewrites a ~1GB debug+ASAN executable).
…ing through it, and reject a repeated module name
… absent LazySourceMap::load already switches itself to None when the mapping blob is missing or invalid; do the same when a source file name pointer is out of range, instead of returning early and redoing the work on the next call.
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
#41360 takes the offset checks from this PR and extends them to the records that #41360 uses the same shape as this PR: It also takes the |
… in 1.3.14) (#42894) ### Problem - On Linux, `Bun.build({ compile })` in a compiled app reports success, but the new executable crashes at startup: `panic(main thread): Segmentation fault at address 0x993CFF8` in `StandaloneModuleGraph::from_executable`. So do `BUN_BE_BUN=1 ./app build --compile` and `--compile-executable-path ./app`. - Regression: 1.3.11 works, 1.3.12 and 1.3.13 refuse (`NoGnuStackSegment`), 1.3.14 and later write the corrupt executable. chenxin-yan/crust#370 works around it. - Cause: `write_bun_section` (`src/exe_format/elf.rs:363`) finds `BUN_COMPILED` through the `.bun` section header. In a compiled executable that header names the payload. The new address lands on the old payload's length. `BUN_COMPILED` keeps the old address. ### Fix - The writer recognizes a payload by its layout and writes the new one at the same address, which `BUN_COMPILED` already holds. - It refuses any other non-zero `.bun` section: `BunSectionAlreadyWritten`, exit 1. That includes the corrupt executables from 1.3.14 to 1.4.x. - Correct because the output is byte-identical to a compile into a plain bun, which the tests assert. - Verified: `test/bundler/compile-elf-segment-layout.test.ts`. 17 of the 19 new tests fail on 1.4.3-canary.1+09bb54630. 18 of them run on every host (checked on Windows x64). Self-reviewed: two findings, both fixed (Notes). ### Background - `BUN_COMPILED` is an 8-byte variable in the `.bun` section of every bun. It holds 0 in a plain bun, and the address of the payload (`[u64 length][module graph]`) in a compiled executable. - `bun build --compile` copies an executable, appends the payload past everything it maps, and points the `.bun` section header at the payload. - A compiled executable that compiles copies itself. `BUN_BE_BUN=1` makes it act as the `bun` CLI. <details><summary>Notes</summary> **Repro** (Linux x64, `1.4.3-canary.1+09bb54630`): ```sh echo 'console.log("first");' > a.ts echo 'console.log("second");' > b.ts bun build --compile a.ts --outfile ./first BUN_BE_BUN=1 ./first build --compile b.ts --outfile ./second # exit 0 ./second # panic(main thread): Segmentation fault at address 0x993CFF8, exit 139 ``` A debug build reports a READ SEGV in `memcmp`, under `StandaloneModuleGraph::from_executable` (`src/standalone_graph/StandaloneModuleGraph.rs:2908`, the trailer compare). The bytes of `./second`: | file offset | what | value | | --- | --- | --- | | `0x47894f8` | `BUN_COMPILED` | `0x4c9e000`, the address of the first payload | | `0x49d1000` | length of the first payload | `0x4c9f000`, the address of the second payload | | `0x49d2000` | length of the second payload | `0xad` | The fault address is `0x4c9e000 + 8 + 0x4c9f000 - 16 = 0x993CFF8`. **Release window.** I ran the three routes on the release binaries: 1.3.11 produces working executables (it appended the graph to the end of the file). 1.3.12 and 1.3.13 print `Error writing .bun section to ELF: error.NoGnuStackSegment` and exit 1. 1.3.14 (the writer that grows the `PT_LOAD`, #29963) and the canary exit 0 and write the corrupt executable. **How a payload is recognized.** If the first word of `.bun` is 0, `.bun` is `BUN_COMPILED` and the payload is appended as before. A linked `.bun` always holds 0, and `to_executable` never writes an empty payload. If the first word is not 0, all of these must hold: the section is `[length][bytes]`, it starts a page, it starts at the first page boundary past the linked sections, its file offset agrees with the segment, the file backs all of the segment, the segment is the highest `PT_LOAD`, and the payload ends it. Then the payload is replaced. Anything else is refused. The test has one row for each condition. Each row changes a compiled executable so that only that condition fails. I checked with a standalone build of the writer that removing any one condition makes its row fail. **Executables that are already corrupt.** What 1.3.14 to 1.4.x wrote into a compiled executable has the second payload one or more pages past the sections. The writer refuses it (checked with executables made by 1.3.14 and by the canary). It does not repair it: nothing in the file says where `BUN_COMPILED` is. **Stripped executables.** GNU `strip` and `objcopy` rewrite a compiled executable from its sections. After a compile, no section header names the bytes of `BUN_COMPILED`, so these tools write 0 there. Such an executable already runs as a plain bun, with or without this PR. Its `PT_LOAD` no longer ends on a page, so the writer refuses it. That is the right result: a replaced payload would never load. `llvm-strip` keeps the bytes, and the writer accepts its output (the output runs). **A tool compiled by an affected bun stays affected.** The writer is part of the compiled tool. A CLI that was compiled by 1.3.14 to 1.4.x, and that compiles with itself, writes corrupt executables until the CLI is compiled again with a bun that has this fix. **Other platforms.** This PR changes only the ELF writer. - Windows refuses the same command: `Error adding Bun section to PE file: SectionExists`, exit 1 (run on `1.4.3-canary.1+c8b9b5853`). - Mach-O, writer level only (`--target=bun-darwin-x64` into a hand-built template, run on Linux, not executed on macOS): a payload that is not smaller replaces the old one, and the output is byte-identical to a direct compile. A smaller payload is refused: `Error writing standalone module graph: InvalidObject`, exit 1. This PR does not add shrinking to the Mach-O writer. **Overlap with open PRs.** - #37261 (and the same hunk in #37225) also lets `write_bun_section` replace a payload. It finds `BUN_COMPILED` through a trailer that it adds to each payload, and it sets `sh_size` to the aligned size. Executables compiled by 1.3.14 to 1.4.x have no trailer, so they take its append path, which is this bug. The two changes conflict in `write_bun_section`, and each refuses or misreads the payload format of the other. If this PR lands first, #37261 needs a rebase of its `elf.rs` hunk. Which recognizer stays is a maintainer decision. - #33621, #38936 and #35751 edit the same function. The conflicts are textual. - #31787, #41360, #37639 and #38251 harden the reader against a corrupt or truncated payload. This PR does not change the reader. **Self-review.** The first version of this change accepted an executable that the old writer had already corrupted: the compile exited 0 and the output crashed. It is refused now. Its tests covered one of the layout conditions. Now there is one row for each. **Checks.** - Real bun (Linux x64, debug build): `Bun.build({ compile })` in a compiled app, `BUN_BE_BUN=1`, and `--compile-executable-path` all produce executables that run. `first` -> `second` -> `third` chains, a payload that grows, a payload that shrinks, and `--bytecode` work. `cmp` reports no difference from a direct compile (same entry point and outfile name), for the release and the debug build. - Small C programs with the same `.bun` declaration, linked by lld as non-PIE and PIE, with and without `-z relro`, for x64 and aarch64: chains are byte-identical to direct compiles, and the x64 programs run and find the new payload, a zero `.bss`, and nothing of the old payload. - The Android builds of bun (PIE, x64 and aarch64): chains are byte-identical to direct compiles. - Payload sizes around a page boundary (`page - 9`, `page - 8`, `page - 7`, `2 * page - 8`, 1 byte), every old and new pair, for 4 KB and 64 KB pages: byte-identical to direct. - Suites on the debug build: `compile-elf-segment-layout`, `bun-build-compile`, `bundler_compile`, `compile-argv`, `compile-asset-bunfs`, `standalone`, and `test/internal/source-lints/`. In `bun-build-compile.test.ts`, four bytecode tests time out on the debug build with and without this change. </details> <!-- robobun:evidence:begin --> --- **no test proof** · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/bundler/compile-elf-segment-layout.test.ts <!-- robobun:evidence:end -->
A
bun build --compileexecutable whose embedded module graph is damaged should report "Corrupted module graph: ..." and exit 1.StandaloneModuleGraph::from_bytesonly checked one thing,entry_point_id > record count, and read every other offset in the graph throughdebug_asserts, so damage panicked (or in release builds read out of bounds) instead:>check and indexed one past the end inentry_point()filesmap, so an id that was in range for the records indexed past the end the same wayNow every offset the graph carries is checked against the section length (and NUL-terminated strings against their terminator) before it is read, through
checked_rangein the three slice helpers, which returnErr(Corruption::...)naming the field. A repeated module name is rejected, which is what makes the record count a valid bound forentry_point_id(>=, checked before loading).to_bytesnever writes a repeated name; the debug round-trip at the end of it already checks that.entry_point_idis private to the code that checks it.Only a corrupted binary can reach any of this; it is error-path quality, not something a well-formed build hits.
Tested in
test/bundler/bun-build-compile.test.tsby compiling an executable, patching the embedded graph one way per case (entry id == record count, repeated name, name pointer past the section, contents length past the section) and running it, expecting exit 1 with the matching message. Without this change the first case panics withindex out of bounds: the len is 1 but the index is 1, the repeated-name case silently runs, and the pointer cases trip the olddebug_asserts (a release build would read out of bounds). Runs on Linux and Windows; macOS is skipped because a patched Mach-O would need re-signing, and the parsing code is the same everywhere.no test proof · iteration 6 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bun-build-compile.test.ts