Skip to content

install: stop panicking on workspaces entries longer than the path buffer - #37531

Merged
Jarred-Sumner merged 1 commit into
mainfrom
farm/2b6119d1/workspaces-entry-too-long
Aug 13, 2026
Merged

install: stop panicking on workspaces entries longer than the path buffer#37531
Jarred-Sumner merged 1 commit into
mainfrom
farm/2b6119d1/workspaces-entry-too-long

Conversation

@robobun

@robobun robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Repro

mkdir probe && cd probe
bun -e 'require("fs").writeFileSync("package.json", JSON.stringify({ name: "p", workspaces: ["a".repeat(5000)] }))'
bun install
panic: range end index 5010 out of range for slice of length 4095

bun install aborts (SIGABRT, exit 134). A glob entry ("a".repeat(5000) + "/*") aborts the same way with panic: range end index 5000 out of range for slice of length 4095. Reproduced with bun 1.4.0-canary.1 on Linux, where the buffer is 4096 bytes; it is 1024 on macOS and 32767 * 3 + 1 on Windows (the glob case is 4096 on every platform). A 2000 byte entry, which fits the buffer, already fails the way one would expect:

error: ENAMETOOLONG reading package.json for workspace package "aaa..." from "/tmp/probe"

Cause

WorkspaceMap::process_names_array joins every entry of the workspaces array with the unchecked join helpers, whose normalizer indexes past the output buffer when the result does not fit (resolve_path.rs, buf[buf_i..buf_i + count]; the "length 4095" is the buffer minus the leading separator):

  • path entries: join_abs_string_buf_z(project dir, PathBuffer, [entry, "package.json"])
  • glob entries: join([pattern, "package.json"]) into the 4096 byte thread local buffer, before the pattern ever reaches the glob walker
  • glob matches: join_abs_string_buf_z(project dir, PathBuffer, [matched dir, "package.json"]). The walker works relative to the project dir, so it can match a package.json whose path relative to the project fits while the absolute path does not. A member directory whose absolute path is 4090 bytes long (creatable, it fits PATH_MAX) with workspaces: ["**"] panics with range end index 4102 out of range for slice of length 4095.

The folder dependency arm in lockfile/Package.rs already uses join_abs_string_buf_checked for the same situation.

Fix

  • The two absolute joins use join_abs_string_buf_checked. When it returns None the entry takes the existing error path with Error::Sys(ENAMETOOLONG), so it prints exactly what an entry the OS rejects prints today (ENAMETOOLONG reading package.json for workspace package "...") and bun install exits 1. A path that does not fit the buffer does not fit PATH_MAX either, so this is the same answer one layer earlier. process_workspace_name takes &[u8] now; WorkspacePackageJSONCache::get_with_path never needed the NUL terminator.
  • The glob pattern join uses join_spill, the variant the glob walker itself uses for long paths, and the pattern is handed to the walker like any other. The walker already copes with long patterns: a pattern that matches nothing is skipped, the same as a 2000 byte one is today, and one that does match (a brace group padded past the buffer size) still finds its workspace. Rejecting long patterns outright would turn that second case into an error.

Both joins are checked on the normalized result, so an entry that is long as written but normalizes to a short path (x/../x/../.../pkgs/pkg1) keeps resolving, same as before.

Related: #37462 fixes the same kind of overflow for local tarball and workspace: dependency specs in other files; #35863 changes the shared join primitive to return an empty path on overflow, which would turn these panics into a misleading Workspace not found. This PR is independent of both: the call sites report ENAMETOOLONG either way.

Tests

test/cli/install/bad-workspace.test.ts (the existing file for bad workspaces entries), all spawning bun install without a registry:

  • 100 kB path entry: ENAMETOOLONG, exit 1 (every platform)
  • path entry whose joined package.json path is one byte below / exactly / one byte above the buffer size: Workspace not found (the path still reaches the OS) / ENAMETOOLONG / ENAMETOOLONG (POSIX; on Windows the OS rejects paths long before the buffer size)
  • 100 kB brace glob still matches pkgs/pkg1; 100 kB glob matching nothing leaves the other entries alone (every platform)
  • 100 kB x/../ path and glob entries that normalize to pkgs/pkg1 resolve (every platform, passes before and after: guards the normalized length semantics)
  • glob match whose absolute package.json path does not fit: ENAMETOOLONG, exit 1 (POSIX; the member directory is created at buffer size - 6 bytes and its package.json written relative to the parent directory)

On the unfixed build (USE_SYSTEM_BUN=1) six of these crash with the panics above (100035, 100008, 100000, index 4096 for the NUL write of the "exactly" row, 4096, 4102); the "one byte below" row and the two normalizing entries pass both ways. With bun bd test the file passes, as do bun-workspaces.test.ts (63 tests) and test/cli/run/workspaces.test.ts.

…tries longer than the path buffer

Every entry of the root package.json workspaces array was joined into a
fixed size path buffer with the unchecked join helpers, so an entry (or a
glob match) whose joined path did not fit indexed past the buffer and
aborted bun install. Use the checked joins and report the overflow through
the existing per-entry error path, and join glob patterns with the
spilling variant so long patterns are handed to the glob walker like any
other pattern.
@robobun

robobun commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Status: reproduced and fixed.

Reproduced with the released bun 1.4.0-canary.1 on Linux x64: a 5000 byte workspaces entry aborts bun install with panic: range end index 5010 out of range for slice of length 4095, a 5000 byte glob entry with range end index 5000 ..., and a glob match whose absolute package.json path is longer than the buffer (member directory at 4090 bytes, workspaces: ["**"]) with range end index 4102 .... All three sites are in WorkspaceMap::process_names_array; with this branch each reports ENAMETOOLONG for the entry and bun install exits 1, and long glob patterns are evaluated like any other pattern.

Tests: test/cli/install/bad-workspace.test.ts, six of the new tests crash on the unfixed build and pass with the fix.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Workspace package resolution now uses byte-slice paths and checked joins. Direct entries and glob matches propagate ENAMETOOLONG. Install tests cover path-length boundaries, normalization, unmatched globs, and deep glob matches.

Changes

Workspace path resolution

Layer / File(s) Summary
Checked workspace resolution
src/install/lockfile/Package/WorkspaceMap.rs
Workspace entries and glob matches use checked, spill-aware path joins. Resolved byte-slice paths flow through package processing and workspace directory derivation.
Path-length validation
test/cli/install/bad-workspace.test.ts
Install tests cover oversized paths, buffer boundaries, long and unmatched globs, normalized paths, and deep glob matches.

Possibly related PRs

  • oven-sh/bun#36324: Adds checked path-length handling and ENAMETOOLONG propagation in another subsystem.
  • oven-sh/bun#37424: Handles path-buffer overflow and ENAMETOOLONG in isolated linker operations.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description explains the failure, cause, fix, and verification results, including comprehensive test coverage.
Title check ✅ Passed The title clearly identifies the main fix: preventing panics from workspace entries that exceed the path buffer.
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.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 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/lockfile/Package/WorkspaceMap.rs`:
- Around line 260-263: Update the error-handling branch around the cwd lookup in
the workspace map logic to handle bun_sys::getcwd failure without
expect("unreachable"). Log the workspace error without cwd details when getcwd
fails, or propagate the getcwd error, while preserving the existing formatting
path for successful lookups.

In `@test/cli/install/bad-workspace.test.ts`:
- Around line 279-292: Update the writer subprocess assertions around spawn to
drain stdout, stderr, and exited concurrently, then assert the combined results
together. Preserve the expectation that both streams are empty and the process
exits with status 0, using the existing writer handles.
🪄 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: 68bf407b-6e3a-4cde-ac5c-b1b4e95ec6ac

📥 Commits

Reviewing files that changed from the base of the PR and between da3851e and d42eb41.

📒 Files selected for processing (2)
  • src/install/lockfile/Package/WorkspaceMap.rs
  • test/cli/install/bad-workspace.test.ts

Comment on lines +260 to +263
} else {
let mut cwd_buf = vec![0u8; MAX_PATH_BYTES];
let cwd_len = bun_sys::getcwd(&mut cwd_buf).expect("unreachable");
let _ = log.add_error_fmt(

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the getcwd expect() is pre-existing or introduced here.
rg -n -C4 'getcwd' src/install/lockfile/Package/WorkspaceMap.rs
git log -1 --format=%H
git diff --unified=6 "$(git merge-base HEAD origin/main 2>/dev/null || echo HEAD~1)" -- src/install/lockfile/Package/WorkspaceMap.rs | rg -n -C6 'getcwd'

Repository: oven-sh/bun

Length of output: 2234


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- WorkspaceMap.rs ---'
sed -n '210,300p' src/install/lockfile/Package/WorkspaceMap.rs
printf '%s\n' '--- getcwd definitions and call sites ---'
rg -n -C3 'fn getcwd|getcwd\(' src | head -200
printf '%s\n' '--- surrounding error handling ---'
rg -n -C5 'process_workspace_name|ENAMETOOLONG|Error::Sys' src/install/lockfile/Package/WorkspaceMap.rs

Repository: oven-sh/bun

Length of output: 21198


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- process_workspace_name and workspace-map control flow ---'
sed -n '100,190p' src/install/lockfile/Package/WorkspaceMap.rs
sed -n '420,500p' src/install/lockfile/Package/WorkspaceMap.rs
printf '%s\n' '--- analogous package-manager fallback ---'
sed -n '540,575p' src/runtime/cli/package_manager_command.rs
printf '%s\n' '--- relevant history for the expect ---'
git log --oneline -8 -- src/install/lockfile/Package/WorkspaceMap.rs
git blame -L 250,270 -- src/install/lockfile/Package/WorkspaceMap.rs

Repository: oven-sh/bun

Length of output: 11877


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- commit change ---'
git show --format= --unified=12 d42eb4104 -- src/install/lockfile/Package/WorkspaceMap.rs | sed -n '1,220p'
printf '%s\n' '--- getcwd implementation ---'
sed -n '2638,2665p' src/sys/lib.rs
printf '%s\n' '--- read-only control-flow verifier ---'
python3 - <<'PY'
from pathlib import Path
p = Path("src/install/lockfile/Package/WorkspaceMap.rs")
text = p.read_text()
assert 'None => Err(crate::Error::Sys(bun_errno::SystemErrno::ENAMETOOLONG))' in text
assert 'bun_sys::getcwd(&mut cwd_buf).expect("unreachable")' in text
print("ENAMETOOLONG is routed to the branch containing getcwd().expect(\"unreachable\").")
PY

Repository: oven-sh/bun

Length of output: 10578


Handle getcwd failure without panicking. The new ENAMETOOLONG result enters this branch, and getcwd can fail when the current directory was removed. Log the workspace error without the cwd, or propagate the getcwd error, instead of calling expect("unreachable").

🤖 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/install/lockfile/Package/WorkspaceMap.rs` around lines 260 - 263, Update
the error-handling branch around the cwd lookup in the workspace map logic to
handle bun_sys::getcwd failure without expect("unreachable"). Log the workspace
error without cwd details when getcwd fails, or propagate the getcwd error,
while preserving the existing formatting path for successful lookups.

Source: Coding guidelines

Comment on lines +279 to +292
await using writer = spawn({
cmd: [
bunExe(),
"-e",
`require("fs").writeFileSync(process.argv.at(-1) + "/package.json", JSON.stringify({ name: "deep" }))`,
names.at(-1)!,
],
cwd: dirname(workspaceDir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(await writer.stderr.text()).toBe("");
expect(await writer.exited).toBe(0);

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Drain the writer subprocess streams concurrently and assert the combined result.

The writer subprocess pipes stdout, but the test never reads stdout. The test also awaits stderr.text() and exited sequentially. Read both streams and the exit status concurrently, and assert them together so a failure shows all three values.

As per coding guidelines: "Subprocess tests must drain stdout, stderr, and process exit concurrently and assert the combined result and ordered stage outputs."

🧹 Proposed fix
-      expect(await writer.stderr.text()).toBe("");
-      expect(await writer.exited).toBe(0);
+      const [writerStdout, writerStderr, writerExitCode] = await Promise.all([
+        writer.stdout.text(),
+        writer.stderr.text(),
+        writer.exited,
+      ]);
+      expect({ stdout: writerStdout, stderr: writerStderr, exitCode: writerExitCode }).toEqual({
+        stdout: "",
+        stderr: "",
+        exitCode: 0,
+      });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await using writer = spawn({
cmd: [
bunExe(),
"-e",
`require("fs").writeFileSync(process.argv.at(-1) + "/package.json", JSON.stringify({ name: "deep" }))`,
names.at(-1)!,
],
cwd: dirname(workspaceDir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
expect(await writer.stderr.text()).toBe("");
expect(await writer.exited).toBe(0);
await using writer = spawn({
cmd: [
bunExe(),
"-e",
`require("fs").writeFileSync(process.argv.at(-1) + "/package.json", JSON.stringify({ name: "deep" }))`,
names.at(-1)!,
],
cwd: dirname(workspaceDir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [writerStdout, writerStderr, writerExitCode] = await Promise.all([
writer.stdout.text(),
writer.stderr.text(),
writer.exited,
]);
expect({ stdout: writerStdout, stderr: writerStderr, exitCode: writerExitCode }).toEqual({
stdout: "",
stderr: "",
exitCode: 0,
});
🤖 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/bad-workspace.test.ts` around lines 279 - 292, Update the
writer subprocess assertions around spawn to drain stdout, stderr, and exited
concurrently, then assert the combined results together. Preserve the
expectation that both streams are empty and the process exits with status 0,
using the existing writer handles.

Source: Coding guidelines

@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 — targeted crash fix that swaps three unchecked path joins for their checked/spilling variants and routes overflow through the existing error branch.

What was reviewed:

  • Verified join_abs_string_buf_checked normalizes into a scratch buffer before length-checking, so x/../-style entries that normalize short still resolve (guarded by the new "once normalized resolves" tests).
  • Confirmed get_with_path already takes &[u8], so dropping the ZStr wrapper is a no-op at the call site.
  • Checked that abs_workspace_dir_path moving below the error match is safe — it was only read on the success path.
  • Tests follow harness conventions (describe.concurrent, tempDir, Buffer.alloc fills, concurrent pipe drain, boundary at exactly/±1 byte) and test.skipIf(...).each(...) chaining is used elsewhere in the suite.
Extended reasoning...

Overview

The PR fixes three panic sites in WorkspaceMap::process_names_array where workspaces array entries longer than the fixed path buffer would index past it. Two join_abs_string_buf_z calls become join_abs_string_buf_checked (returning None on overflow, mapped to Error::Sys(ENAMETOOLONG) and fed into the existing error-message branch), and one join for glob patterns becomes join_spill so oversized patterns still reach the walker. process_workspace_name drops the &ZStr parameter in favour of &[u8] since the only consumer already took &[u8]. Tests are added to the existing bad-workspace.test.ts.

Security risks

None. The change turns a user-reachable panic (adversarial package.json could abort bun install) into a recoverable error, which is a strict hardening. No new parsing, no privilege boundaries, no network.

Level of scrutiny

Moderate. The diff is mostly reindentation from restructuring two match blocks; the substantive change is three helper swaps plus a signature relaxation. I verified: join_abs_string_buf_checked at resolve_path.rs:1616 normalizes into a heap scratch on the slow path and only returns None when the normalized result exceeds the buffer, matching the PR's claim and the "normalized" test row; join_spill at resolve_path.rs:1408 falls through to join when the input fits, so the common case is unchanged; WorkspacePackageJSONCache::get_with_path at line 137 already accepts &[u8]. The continue inside the Some(...) match arm correctly targets the enclosing for, and abs_workspace_dir_path was only used on the success path so moving its computation below the error handler is behaviour-preserving. The ENAMETOOLONG case lands in the existing catch-all else arm that formats {err.name()} reading package.json ..., which is exactly what a shorter-but-still-too-long entry already prints today (per the repro in the description).

Other factors

Test coverage is thorough: 100 kB entries on every platform, exact ±1-byte boundary on POSIX, long brace-group globs that must still match, long globs matching nothing, long entries that normalize short, and a deep-directory glob match whose absolute path overflows. The PR states six of these panic on the unfixed build. Tests use describe.concurrent, tempDir, Buffer.alloc(...).toString(), and concurrent stdout/stderr/exited draining per the repo conventions; skipIf gates carry a comment naming why. The pattern mirrors the existing join_abs_string_buf_checked usage the description points to in lockfile/Package.rs. No CODEOWNERS entry covers src/install/.

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