Skip to content

publish: report ENAMETOOLONG for a tarball path that does not fit the path buffer - #38753

Closed
robobun wants to merge 2 commits into
mainfrom
farm/bfbe29d0/publish-tarball-path-too-long
Closed

robobun wants to merge 2 commits into
mainfrom
farm/bfbe29d0/publish-tarball-path-too-long

Conversation

@robobun

@robobun robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun publish <tarball> aborts with a crash report when the tarball argument does not fit the path buffer. bun publish "$(head -c 5000 /dev/zero | tr '\0' d).tgz" --dry-run prints panic: range end index 5017 out of range for slice of length 4095 and exits 134.
  • An argument whose resolved path is exactly the buffer size panics one line later instead, on the NUL terminator write: panic: index out of bounds: the len is 4096 but the index is 4096.
  • Cause: Context::from_tarball_path (src/runtime/cli/publish_command.rs:144) resolves the raw positional with join_abs_string_buf_z into a PathBuffer. That helper assumes the result fits and indexes past the buffer when it does not.
  • Found by reading the code while looking at the neighbouring pack code; there is no issue or crash report for it that I know of. The site has been like this since bun publish was added. It is reachable on every platform: on Windows the buffer is sized for the longest command line as UTF-8, so an argument of three byte characters overflows it as well (panic: range end index 98303 out of range for slice of length 98302, exit code 3, reproduced on Windows Server 2019 with the current canary).
  • One byte shorter than the buffer already fails cleanly today, because the open wrapper rejects it: ENAMETOOLONG: File name too long: failed to read tarball: '...' (open), exit 1. Only the lengths past that point crash.

Fix

  • Resolve the argument with join_abs_string_buf_checked, which returns None when the normalized result does not fit, and in that case report ENAMETOOLONG through the existing failed to read tarball error and exit 1.
  • Correct because the join buffer and the open wrapper's limit are the same constant (MAX_PATH_BYTES): a resolved path that does not fit the buffer is exactly a path File::read_from would have refused with ENAMETOOLONG (bun_sys::openat_a synthesizes that errno for len >= MAX_PATH_BYTES before calling the OS). Reporting it one layer earlier gives the same message the lengths that already fail today get, so what the user sees no longer depends on which layer happened to catch the overflow. build_command.rs builds the same error for an over-long asset path.
  • The checked helper bounds the normalized path, not the argument as written, so x/../x/../.../pkg.tgz longer than the buffer still resolves and publishes (as before). The test pins that, so a plain length check on the argument is not an acceptable fix.
  • File::read_from takes &[u8] and the context stores a ZStr::boxed copy, so nothing needed the NUL-terminated _z flavour; the unchecked join was the only thing providing it.
  • Relation to the open PRs in this area: bun test: stop panicking on a path argument or tree entry longer than the path buffer #35863 bounds the unchecked primitive itself and makes it return an empty path on overflow, which this site would then report as ENOENT for an empty path; this change keeps ENAMETOOLONG for it, so the two are complementary and either order works. publish: resolve a relative tarball path against the invoking directory #38704 (resolve against the invoking directory) and pack/publish: stop panicking on package.json bin and files entries longer than the path buffer #38784 (bin and files entries) touch the same import line and the same test file; the conflicts are textual. pack: report an error instead of panicking when --destination does not fit the path buffer #38749 adds a shared MAX_PATH_BYTES to the test harness; this test keeps a local constant rather than adding a competing export, and can switch once that lands.
  • Verified with test/cli/install/bun-publish.test.ts, describe("tarball path"):
    • a resolved path one byte longer than the buffer reports ENAMETOOLONG ... failed to read tarball and exits 1 on every platform. On Windows the argument and the directory it is resolved against are made of three byte characters; the test asserts the command line stays within the 32767 units CreateProcess accepts (about 140 units are left for the path of bun.exe).
    • a 5000 byte argument and a resolved path exactly the buffer size do the same; POSIX only, because an argument longer than the Windows buffer does not fit in a command line, and the exact fit case is refused by the open wrapper, which on Windows reports it as ENOSYS today (it takes the errno number from the C runtime, whose numbering differs from bun's table; pre-existing and reported separately, this change uses bun's own constant and is not affected).
    • an over-long argument that normalizes down is read and dry-run published (passes before and after).
    • Linux: the three error cases fail on the unfixed binary with the panics quoted above (output below) and pass with the fix. Windows Server 2019 x64: the shared case fails on the current canary with the panic quoted above and passes with this branch built there; the two POSIX cases skip.
  • The rest of the file passes with the debug build, apart from the pre-existing lifecycle scripts test hitting the 5 s default timeout on this machine (six debug bun spawns at about 1.1 s each) and taking verdaccio down for the tests after it; unrelated to this change.

Background

  • PathBuffer / MAX_PATH_BYTES: bun resolves paths into fixed stack buffers (4096 bytes on Linux, 1024 on macOS, 32767 * 3 + 1 on Windows, i.e. the longest UTF-16 command line at three UTF-8 bytes per unit) instead of allocating. The unchecked join_abs_string_buf* helpers are for parts known to fit; join_abs_string_buf_checked is the variant documented for user-controlled parts of arbitrary length. It normalizes into a heap scratch when the concatenation is too big and only then checks whether the result fits, which is why .. segments still work. When the input would have fit anyway it takes the same path as the unchecked helper.
  • bun_sys::openat_a copies a &[u8] path into a PathBuffer to NUL terminate it and returns a synthesized ENAMETOOLONG with syscall tag open when len >= MAX_PATH_BYTES. Output::err prints a bun_sys::Error as <errno>: <description>: <message> (<syscall>), which is the format the tests match.
  • Global::crash() is the CLI's print-an-error-and-exit-1 helper used by every other error path in from_tarball_path.
New tests on the unfixed binary (Linux)
(fail) tarball path > longer than the path buffer fails with ENAMETOOLONG
  panic: range end index 5041 out of range for slice of length 4095
(fail) tarball path > one byte longer than the path buffer once resolved fails with ENAMETOOLONG
  panic: range end index 4096 out of range for slice of length 4095
(fail) tarball path > as long as the path buffer once resolved fails with ENAMETOOLONG
  panic: index out of bounds: the len is 4096 but the index is 4096
(pass) tarball path > that only fits the path buffer once resolved is read

5041 rather than the 5017 in the report above because the test resolves against a longer temporary directory.

Shared case on Windows Server 2019 x64

Directory: 195 UTF-16 units, 495 bytes. Argument: 32603 units, 97807 bytes. Resolved path: 98303 bytes against a 98302 byte buffer.

canary 1.4.0-canary.1 (39fb3c103):
  panic: range end index 98303 out of range for slice of length 98302   (exit code 3)
this branch (debug build):
  ENAMETOOLONG: File name too long: failed to read tarball: '...' (open)   (exit code 1)
same argument one character shorter (resolved path exactly 98302 bytes), this branch:
  ENOSYS: Function not implemented: failed to read tarball: '...' (open)   (the open wrapper's pre-existing errno mix-up, not covered by the test)

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

… path buffer

`bun publish <tarball>` resolved the positional argument with
join_abs_string_buf_z, which indexes past the fixed PathBuffer when the
resolved path does not fit, so an over-long argument aborted with a bounds
panic and a crash report. Resolve it with join_abs_string_buf_checked and
report ENAMETOOLONG through the existing "failed to read tarball" error
instead, which is what the open syscall wrapper already reports for a path
that fills the buffer.
@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: 5 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: bd17d0e2-9185-4204-988f-82ea00769826

📥 Commits

Reviewing files that changed from the base of the PR and between c2fa121 and 41ebf08.

📒 Files selected for processing (2)
  • src/runtime/cli/publish_command.rs
  • test/cli/install/bun-publish.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:05 PM PT - Aug 14th, 2026

@robobun, your commit 07af90d is building: #96898

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: fix and tests pushed, waiting on CI.

Reproduced on the current release binary (1.4.0-canary.1) and on main at 056491f with a package.json in the cwd and bun publish "$(head -c 5000 /dev/zero | tr '\0' d).tgz" --dry-run: panic: range end index 5017 out of range for slice of length 4095, exit 134. Also reproduced on Windows Server 2019 x64 with a three byte per character argument (panic: range end index 98303 out of range for slice of length 98302, exit code 3). With this branch both print ENAMETOOLONG: File name too long: failed to read tarball: '...' (open) and exit 1.

Tests are in test/cli/install/bun-publish.test.ts, describe("tarball path"). The one byte over case runs on every platform and fails on the unfixed binary on Linux and on Windows; the 5000 byte and exact fit cases are POSIX only (reasons in the description); the normalizes-down case passes before and after and pins that the bound applies to the resolved path. All verified locally with the debug build on Linux and on Windows x64.

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

LGTM — small, well-scoped crash fix that swaps an unchecked path join for the checked variant.

What was reviewed:

  • join_abs_string_buf_checked returns Option<&[u8]>; File::read_from and ZStr::boxed both take &[u8], so dropping the _z flavour and .as_bytes() is type-correct.
  • Error construction matches the existing build_command.rs ENAMETOOLONG pattern and the sibling File::read_from error arm, so all overflow lengths now report the same message.
  • Tests cover the boundary (5000 bytes, buffer+1, exactly buffer) and pin that ..-normalized over-long args still resolve; hermetic via --dry-run against a closed port, Windows-skipped with a stated reason.
Extended reasoning...

Overview

The PR replaces join_abs_string_buf_z with join_abs_string_buf_checked in Context::from_tarball_path (src/runtime/cli/publish_command.rs:144) and reports ENAMETOOLONG via Output::err + Global::crash() when the resolved tarball path does not fit the PathBuffer. The only other source change is dropping .as_bytes() at the ZStr::boxed call site since the checked variant already returns &[u8]. A new describe("tarball path") block in bun-publish.test.ts covers three overflow lengths and one over-long-but-normalizes-down argument.

Security risks

None. The change converts a user-reachable panic (exit 134) into a recoverable error with exit 1 — strictly a hardening. No new input is trusted, no new I/O paths, and --dry-run in the tests keeps them off the network.

Level of scrutiny

Low-to-medium. It is a ~10-line targeted fix in a CLI error path, mirroring an existing pattern in build_command.rs. I verified the helper signatures: join_abs_string_buf_checked returns Option<&[u8]> (None when joined.len() > buf.len()), File::read_from(dir, &[u8]), and ZStr::boxed(&[u8]), so the type flow after dropping _z is correct. The exactly-buffer-size case (4096 on Linux) still returns Some from the checked join and is caught one layer down by openat_a's len >= MAX_PATH_BYTES synthesis, which the third test case exercises — both paths format the same message.

Other factors

Tests follow the repo conventions closely: describe.concurrent, tempDir, Buffer.alloc(n, fill) over .repeat, exit code asserted last, Windows skip commented, and a positive test pinning that a naive raw-argument length check would be wrong. The PR description records the unfixed-binary panic output for each case, satisfying the "prove the test fails for the right reason" bar. No prior human reviews, no CODEOWNER-gated files, no outstanding comments.

The Windows buffer is sized for the longest command line as UTF-8, so an
argument made of three byte characters, resolved against a directory made
of them as well, still overflows it. Run the one byte over case on every
platform and keep the two cases that cannot be built there POSIX only.

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

LGTM — converts a user-reachable panic into the same ENAMETOOLONG error the shorter-by-one case already reports.

What was reviewed:

  • join_abs_string_buf_checked return type (Option<&[u8]>) and that ZStr::boxed/File::read_from both accept &[u8], so dropping the _z flavour is safe.
  • Error construction matches the precedent in build_command.rs:1335 and the existing File::read_from failure branch, so the message is identical regardless of which layer catches it.
  • The exact-buffer-length case: _checked returns Some (no NUL write), then openat_a's len >= MAX_PATH_BYTES check produces the same errno — test covers this path too.
  • Tests: concurrent, hermetic (--dry-run + closed port 1), boundary cases at buf/+1/way-over plus the negative case pinning that normalization still shrinks long args; Windows skips are documented.
Extended reasoning...

Overview

Two files: src/runtime/cli/publish_command.rs swaps join_abs_string_buf_z for join_abs_string_buf_checked in Context::from_tarball_path and adds a let-else that reports ENAMETOOLONG via Output::err + Global::crash() when the resolved tarball path overflows the PathBuffer. The abs_tarball_path local changes from &ZStr to &[u8], so ZStr::boxed(abs_tarball_path.as_bytes()) becomes ZStr::boxed(abs_tarball_path). test/cli/install/bun-publish.test.ts gains a describe.concurrent("tarball path") block with four cases.

Security risks

None. This tightens input validation on a CLI positional argument — a user-reachable panic (which the review guide flags as a DoS) becomes a recoverable error with exit 1. No new trust boundaries, no auth/crypto/network changes.

Level of scrutiny

Low-to-medium. The Rust change is ~12 lines and follows the exact pattern already used at build_command.rs:1333-1337 for over-long asset paths. join_abs_string_buf_checked is the documented helper for user-controlled path parts and is used the same way at half a dozen other call sites (WorkspaceMap, node_fs, node_fs_watcher, BunObject). I verified its signature returns Option<&[u8]>, that ZStr::boxed takes &[u8], and that the fast/slow path in _checked normalizes before bounding so x/../-padded arguments still resolve.

Other factors

The tests are well-constructed per the repo guide: describe.concurrent for independent spawns, tempDir from harness, hermetic via --dry-run against a closed port with embedded credentials (so the auth check passes without a request), exact-message assertions on stderr before exit code, boundary coverage at exactly-the-buffer / +1 / 5000, and a negative test pinning that the bound applies to the resolved path. Windows skips are commented with the concrete reason (command-line length limit vs 96 KiB path buffer; ENOSYS/ENAMETOOLONG errno-table mismatch on the exact-length case). The PR description shows the three failing cases panic on the unfixed binary. No prior review comments to address.

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #43067. It fixes this trigger with the shared checked path helpers and carries the tests from this pull request.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant