Skip to content

fix(install): guard against overlong PAX paths in tarball extraction - #31160

Closed
pc-style wants to merge 1 commit into
oven-sh:mainfrom
pc-style:fix-install-overlong-pax-path-crash
Closed

fix(install): guard against overlong PAX paths in tarball extraction#31160
pc-style wants to merge 1 commit into
oven-sh:mainfrom
pc-style:fix-install-overlong-pax-path-crash

Conversation

@pc-style

Copy link
Copy Markdown

Summary

Fixes a crash in bun install when extracting a malicious npm tarball containing an overlong PAX pathname.

normalizeBufT writes into a fixed-size OSPathBuffer (4096 bytes on macOS) and assumes callers provide enough space. A tarball can provide an arbitrarily long PAX path, causing extraction to write past the buffer and crash with a bus error. In local testing with Bun 1.3.12, the fault address was attacker-controlled-looking (0x6161616161616161).

This PR adds bounds checks before normalizeBufT in both extraction paths:

  • Archiver.extractToDir buffered extraction
  • TarballStream streaming extraction

It also adds leading .. rejection to the buffered extraction path to match the existing TarballStream.beginEntry traversal guard.

Security impact

Crash-only / denial of service. This does not claim RCE.

Requirements:

  • User runs bun install on a project depending on a malicious npm package tarball
  • The tarball contains a PAX path longer than OSPathBuffer

Test

Added a regression test that generates a gzipped tarball with a 5000-byte PAX pathname and verifies bun install exits successfully without printing the crash banner.

Validation

  • git diff --check passed for touched files
  • Zig files were formatted with zig fmt --stdin
  • Rust files were formatted with the repo's pinned toolchain: rustup run nightly-2026-05-06 cargo fmt
  • TypeScript test file passes Prettier formatting

I also attempted bun bd, but current main failed in an unrelated subsystem before reaching these files:

error: No matching export in "hmr-module.ts" for import "emitEvent"
    at src/runtime/bake/hmr-runtime-client.ts:18:3

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented May 21, 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: 3cb7fadf-09a7-4074-980c-b48dc9700561

📥 Commits

Reviewing files that changed from the base of the PR and between 4c6995c and d4538b5.

📒 Files selected for processing (3)
  • src/install/TarballStream.rs
  • src/libarchive/lib.rs
  • test/cli/install/bun-install.test.ts

Walkthrough

This PR hardens tar processing by adding early bounds checks against fixed-size path buffers during streaming and extraction, plus a post-normalization check that rejects paths whose first component is ... A new test builds a gzipped PAX tarball with an overlong path and verifies install succeeds.

Changes

Tarball path validation security hardening

Layer / File(s) Summary
Tarball stream path bounds validation
src/install/TarballStream.rs
Early capacity guard in begin_entry checks whether the raw path would overflow the fixed-size OSPathBuffer and skips the entry if it would, preventing buffer write overflow before normalization is attempted.
Archive extraction path validation
src/libarchive/lib.rs
Two sequential checks in extract_to_dir: bounds check rejects paths too large for the normalized buffer, and post-normalization check rejects paths starting with leading .. segments to prevent directory traversal escape.
Test for PAX header overflow handling
test/cli/install/bun-install.test.ts
Constructs a gzipped tarball with TAR/PAX binary helpers containing an intentionally overlong path, runs bun install --no-progress against it, and verifies successful completion without buffer overflow or path escape.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix(install): guard against overlong PAX paths in tarball extraction' clearly and specifically describes the main security fix—adding guards against overlong PAX paths in tarball extraction to prevent crashes during bun install.
Description check ✅ Passed The PR description is comprehensive, covering the bug, root cause, changes made (bounds checks and leading .. rejection), security impact, test added, and validation steps performed, though it deviates slightly from the minimal template structure by providing extensive detail.
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.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@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/cli/install/bun-install.test.ts`:
- Around line 130-131: The test asserts the subprocess exit code (expect(await
exited).toBe(0)) before consuming/asserting output which hampers diagnostics;
reorder the assertions so you first read/assert output (e.g., expect(await new
Response(stderr).text()).not.toContain("Bun has crashed") and any stdout checks)
and only after those checks await and assert the exit code (exited) to be
0—locate the usages of exited, stderr and any stdout assertions in the test and
move the exit-code assertion to the very end.
🪄 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: 51aeaa0d-b2fe-4e0f-9094-b040d9997c05

📥 Commits

Reviewing files that changed from the base of the PR and between 753f1c4 and 405c653.

📒 Files selected for processing (5)
  • src/install/TarballStream.rs
  • src/install/TarballStream.zig
  • src/libarchive/lib.rs
  • src/libarchive/libarchive.zig
  • test/cli/install/bun-install.test.ts

@pc-style
pc-style force-pushed the fix-install-overlong-pax-path-crash branch from 927838a to 8774f0f Compare May 21, 2026 03:51

@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/install/TarballStream.zig`:
- Around line 551-558: The added guard in TarballStream.zig that checks `if
(pathname.len >= norm_buf.len) { this.phase = .want_data; this.out_fd = null;
return; }` must not be kept in the Zig port; remove or revert this change so the
Zig reference remains unchanged and leave the security/length-guard fix only in
the Rust implementation. Locate the code around `normalizeBufT` and the
references to `pathname`, `norm_buf`, `this.phase`, and `this.out_fd` in
`TarballStream.zig` and restore the original behavior (no new length-check
sentinel rejection) so the Zig file remains a non-shipping porting reference
while the .rs sibling contains the actual fix.

In `@src/libarchive/libarchive.zig`:
- Around line 415-419: Remove the mirrored hardening checks from the Zig
reference implementation: delete the bounds-check branches that reject long PAX
paths (the `if (pathname.len >= normalized_buf.len) continue :loop;` check and
the similar checks around lines 425-431) in the
`normalizeBufT`/path-normalization section so the .zig file remains a
non-compiled reference; ensure no behavioral changes are made in the shipped
Rust port (the Rust code should retain the actual runtime checks), and leave a
brief comment noting that bounds/hardening belongs in the Rust implementation
rather than the .zig reference.

In `@test/cli/install/bun-install.test.ts`:
- Line 114: In the test fixture where you build the long package path (the
tarEntry call that uses paxEntry and currently uses "a".repeat(5000)), replace
the `.repeat()` usage with Buffer.alloc(count, fill).toString() to generate the
repetitive string; update the tarEntry("PaxHeader",
paxEntry(`package/${"a".repeat(5000)}`), "x") invocation to use
Buffer.alloc(5000, "a").toString() (keeping the same template and arguments) so
the test follows the repo pattern and avoids `.repeat()` in test fixtures.
- Line 131: Replace the negative crash-banner assertion that inspects stderr
(the line using new Response(stderr).text() and .not.toContain("Bun has
crashed")) with deterministic positive assertions: assert the process exit code
is 0 (use the test's exit/status variable, e.g., exitCode or status) and assert
stdout contains the expected install success message (read via new
Response(stdout).text()); remove the not.toContain("Bun has crashed") check and
use these two positive checks instead, updating the assertion targets (stderr ->
stdout) and the referenced variables in the current test.
🪄 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: c3fb06a9-ac2d-4cb0-9059-fe3aabacd7ba

📥 Commits

Reviewing files that changed from the base of the PR and between 405c653 and 8774f0f.

📒 Files selected for processing (5)
  • src/install/TarballStream.rs
  • src/install/TarballStream.zig
  • src/libarchive/lib.rs
  • src/libarchive/libarchive.zig
  • test/cli/install/bun-install.test.ts

Comment thread src/install/TarballStream.zig Outdated
Comment thread src/libarchive/libarchive.zig Outdated
Comment thread test/cli/install/bun-install.test.ts Outdated
Comment thread test/cli/install/bun-install.test.ts Outdated
@pc-style
pc-style force-pushed the fix-install-overlong-pax-path-crash branch from 12f2596 to bad2c42 Compare May 21, 2026 03:57

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

♻️ Duplicate comments (2)
test/cli/install/bun-install.test.ts (2)

130-131: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid negative crash-banner assertion; assert deterministic success output and keep exit code last.

The not.toContain("Bun has crashed") check is disallowed and can be non-actionable in CI. Assert expected success output first, then assert exit code.

Suggested diff
-    expect(await exited).toBe(0);
-    expect(await new Response(stderr).text()).not.toContain("Bun has crashed");
+    expect(await new Response(stderr).text()).toContain("Saved lockfile");
+    expect(await exited).toBe(0);

As per coding guidelines: "Never write tests that check for no 'panic' or 'uncaught exception' or similar in output - these will never fail in CI" and "Assert the exit code last in tests - this gives a more useful error message on test failure".

🤖 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 `@test/cli/install/bun-install.test.ts` around lines 130 - 131, The test
currently asserts a negative crash-banner check using expect(await new
Response(stderr).text()).not.toContain("Bun has crashed") and then checks exit
code via exited; remove the negative assertion and instead assert a
deterministic success message from the CLI (use expect(await new
Response(stderr).text()).toContain("<EXPECTED_SUCCESS_TEXT>") or similar) before
finally asserting the exit code with expect(await exited).toBe(0); update the
test to reference the same symbols (stderr, new Response(stderr).text(), and
exited) so the success output is checked deterministically and the exit code
assertion remains last.

114-114: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Replace .repeat() in test fixture string generation.

Use the repo-required Buffer.alloc(...).toString() pattern for long repeated strings.

Suggested diff
-        tarEntry("PaxHeader", paxEntry(`package/${"a".repeat(5000)}`), "x"),
+        tarEntry("PaxHeader", paxEntry(`package/${Buffer.alloc(5000, "a").toString()}`), "x"),

As per coding guidelines: "To create repetitive strings in tests, use Buffer.alloc(count, fill).toString() instead of "string".repeat(count) for better performance in debug builds".

🤖 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 `@test/cli/install/bun-install.test.ts` at line 114, In the test fixture where
tarEntry("PaxHeader", paxEntry(`package/${"a".repeat(5000)}`), "x") is used,
replace the `"a".repeat(5000)` call with the repo-approved pattern using
Buffer.alloc to build the long string (e.g. Buffer.alloc(5000, "a").toString())
so the paxEntry input uses Buffer.alloc(...).toString() instead of
String.prototype.repeat; update the expression passed into paxEntry accordingly
while keeping the surrounding tarEntry and paxEntry calls unchanged.
🤖 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.

Duplicate comments:
In `@test/cli/install/bun-install.test.ts`:
- Around line 130-131: The test currently asserts a negative crash-banner check
using expect(await new Response(stderr).text()).not.toContain("Bun has crashed")
and then checks exit code via exited; remove the negative assertion and instead
assert a deterministic success message from the CLI (use expect(await new
Response(stderr).text()).toContain("<EXPECTED_SUCCESS_TEXT>") or similar) before
finally asserting the exit code with expect(await exited).toBe(0); update the
test to reference the same symbols (stderr, new Response(stderr).text(), and
exited) so the success output is checked deterministically and the exit code
assertion remains last.
- Line 114: In the test fixture where tarEntry("PaxHeader",
paxEntry(`package/${"a".repeat(5000)}`), "x") is used, replace the
`"a".repeat(5000)` call with the repo-approved pattern using Buffer.alloc to
build the long string (e.g. Buffer.alloc(5000, "a").toString()) so the paxEntry
input uses Buffer.alloc(...).toString() instead of String.prototype.repeat;
update the expression passed into paxEntry accordingly while keeping the
surrounding tarEntry and paxEntry calls unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ff9910e-daaf-4fc7-aaf0-a961e5cc63d7

📥 Commits

Reviewing files that changed from the base of the PR and between 8774f0f and bad2c42.

📒 Files selected for processing (5)
  • src/install/TarballStream.rs
  • src/install/TarballStream.zig
  • src/libarchive/lib.rs
  • src/libarchive/libarchive.zig
  • test/cli/install/bun-install.test.ts

@pc-style
pc-style force-pushed the fix-install-overlong-pax-path-crash branch from bad2c42 to 4c6995c Compare May 21, 2026 04:07

@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/cli/install/bun-install.test.ts`:
- Around line 130-132: Move the exit-code assertion to the end of the test:
after you compute err from stderr and assert output contents (e.g.,
expect(err).toContain("Saved lockfile")), defer expect(await exited).toBe(0)
until after those output assertions so the test reports output mismatches before
exit-code failures; update the assertions around the variables stderr, err and
exited accordingly in the test block.
🪄 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: 5864e9c2-540c-408b-8483-9def4ba65dea

📥 Commits

Reviewing files that changed from the base of the PR and between bad2c42 and 4c6995c.

📒 Files selected for processing (3)
  • src/install/TarballStream.rs
  • src/libarchive/lib.rs
  • test/cli/install/bun-install.test.ts

Comment thread test/cli/install/bun-install.test.ts
Tarballs with PAX pathnames longer than OSPathBuffer (4096 bytes) cause normalizeBufT to write past the buffer, crashing Bun with SIGBUS. Add bounds checks before calling normalizeBufT in both buffered (Archiver.extractToDir) and streaming (TarballStream) extraction paths.

Also add leading '..' rejection to the buffered extraction path to match the existing guard in TarballStream.beginEntry.

Adds regression test for bun install on tarballs with 5000-byte PAX filenames.
@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this, and sorry it sat for so long. Both halves of the fix have since landed on main through the hardening passes: the buffered (libarchive) extractor got the same length guard in cd1ad59 (#31339), and the streaming extractor got it in ff512ea (#36165), each with a regression test (test/js/bun/archive.test.ts and test/cli/install/bun-install-streaming-extract.test.ts). I re-ran this PR's repro (a file: tarball whose PAX path is 5000 characters, and again with 40000) against a build of current main: the entry is skipped and bun install exits 0.

The additional leading .. check in the libarchive extractor turned out not to be needed: entry names are relative once the leading component is stripped, and normalize_buf_t drops leading .. segments from relative input, so an entry named package/../x extracts as x inside the package directory (also checked against main). The streaming extractor has an explicit check for this case already.

Closing since the branch now conflicts with main and there is nothing left for it to change, but the fix itself is in. Thanks again for the report and the patch.

@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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants