Skip to content

install: count directories.bin when bin is an empty string - #43145

Merged
Jarred-Sumner merged 1 commit into
mainfrom
robobun/61defef4/count-directories-bin-after-empty-bin
Sep 17, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
robobun/61defef4/count-directories-bin-after-empty-bin

Conversation

@robobun

@robobun robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A package.json or a registry manifest with "bin": "" and a directories.bin aborts bun install on every build, release builds included: panic: range end index 516 out of range for slice of length 0.
  • An empty bin names no file, so the build pass reads directories.bin and appends it. The counting pass stopped at the empty bin (src/install/npm.rs:2184, src/install/lockfile/Package.rs:2264) and reserved nothing for it, so the append ran past the end of the string buffer.
  • The input comes from the project, from a dependency folder, or from a registry. npm installs the same package with exit code 0.

Fix

  • Each counting pass now reads bin the way its build pass does: an empty string falls through to directories.bin.
  • Both parsers that size a buffer from a counting pass had the mismatch. PackageManifest::parse reads a registry manifest, Package::parse_with_json_impl reads a package.json. Every other reader of bin appends into a growable buffer and cannot drift.
  • Verified: 3 new tests in test/cli/install/bun-install-registry.test.ts, one per source of the file. All 3 abort on release 1.4.3-canary (c6b7fcb) and pass here. The rest of that file passes (258 tests), plus bun-install, bun-lock, bun-pm and isolated-install.
  • Found by fuzzing the manifest parser. No user report exists.

Background

  • bin names the executables of a package. A string names one file. An object maps each name to a file. directories.bin names a folder, and every file in it becomes an executable. npm treats an empty bin as absent.
  • Both parsers run in two passes. The counting pass sums the length of every string it will store, the caller allocates one buffer of that size, and the build pass appends into it. A string that the counting pass did not sum has no room.
  • The manifest buffer gets slack (npm.rs:2319), so a short uncounted string can fit. The lockfile buffer gets none (lockfile.rs:2671).
Notes

Reach. bun publish sends "bin": "" with no check, so a registry can serve it. The shape also comes from a hand-written package.json, a workspace, or a file: folder dependency. With the lockfile parser the abort needs only 9 bytes of directories.bin past the slack of the other strings, and the slack is what the rest of that package.json counted. The smallest case I reproduced is a 90-byte root package.json with no dependencies.

Tests. Each test uses a directories.bin of 523 bytes, which is longer than any slack. The path is ./ repeated 256 times plus the folder name, so it resolves to the same folder and the bins still link. Two tests assert the linked bin, one asserts that an install with no dependencies completes.

Excluded on purpose.

Sibling PR. #43035 removes the debug-only assertions of the same manifest parser. This PR is the release-build crash, and the two do not share a hunk.


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-install-registry.test.ts

A package.json or a registry manifest with `"bin": ""` and a
`directories.bin` aborts `bun install` on every build, release builds
included:

    panic: range end index 516 out of range for slice of length 0

An empty `bin` names no file, so the build pass of each parser reads
`directories.bin` and appends it to the string buffer. The counting pass
stopped at the empty `bin` and reserved nothing for that string, so the
append ran past the end of the buffer.

Both parsers that size their string buffer from a counting pass had the
same mismatch: `PackageManifest::parse` for a registry manifest, and
`Package::parse_with_json_impl` for a package.json. Each counting pass
now reads `bin` the way its build pass does.
@robobun

robobun commented Sep 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed in #43145, waiting for CI.

Reproduction (no registry needed). A package.json with an empty bin and a directories.bin:

mkdir /tmp/r && cd /tmp/r
echo '{"name":"foo","bin":"","directories":{"bin":"some-long-dir"}}' > package.json
bun install

Release 1.4.3-canary.1 (c6b7fcb) ends with panic: range end index 13 out of range for slice of length 0 and SIGABRT. A registry manifest with the same two fields aborts the same way, with the slack of the manifest string buffer in the length.

Cause. The build pass of each parser reads directories.bin when bin names no file, and appends it to the string buffer. The counting pass that sizes that buffer stopped at the empty bin.

Fix. Each counting pass reads bin the way its build pass does. Two sites: src/install/npm.rs:2184 for a registry manifest, src/install/lockfile/Package.rs:2264 for a package.json.

Verification. 3 new tests in test/cli/install/bun-install-registry.test.ts, one per source of the file: the root package.json, a file: folder dependency, a registry manifest.

Build Result
Linux x64, release canary c6b7fcb 3 of 3 abort (SIGABRT)
Linux x64, debug build of this branch 3 of 3 pass
Windows x64, release canary 630e921 3 of 3 abort, exit code 3
Windows x64, debug build of this branch 3 of 3 pass, bins linked as .bunx and .exe shims

The rest of bun-install-registry.test.ts passes (258 tests), plus bun-install, bun-lock, bun-pm and isolated-install.

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: f002d2e6-91a9-4d33-ace9-1a33f9844a60

📥 Commits

Reviewing files that changed from the base of the PR and between 0d3492e and 373be2a.

📒 Files selected for processing (3)
  • src/install/lockfile/Package.rs
  • src/install/npm.rs
  • test/cli/install/bun-install-registry.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 1 remains after this review.


Walkthrough

Empty string bin values no longer stop bin resolution. Package counting and npm installation now fall through to directories.bin, with tests covering root, folder, and registry installations.

Changes

Empty bin fallback

Layer / File(s) Summary
Bin resolution fallback
src/install/lockfile/Package.rs, src/install/npm.rs
Empty scalar bin strings are ignored, so directories.bin can be processed. Non-empty strings retain the existing behavior.
Installation validation
test/cli/install/bun-install-registry.test.ts
Tests cover root packages, folder dependencies, and registry manifests. They verify successful installation, binDir lockfile entries, executable linking, and long directories.bin paths.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 373be

The empty-bin fallback change has matching counting and resolution updates with coverage for the stated installation paths, so no merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly and concisely describes the main fix: counting directories.bin when bin is an empty string.
Description check ✅ Passed The description explains the problem, root cause, fix, affected code paths, tests, and verification results. It does not use the template headings exactly, but it provides the required information in …

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

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

The review raised no findings, so the diff is unchanged. I added the Windows side of the proof to the status comment above: the three new tests abort on the Windows x64 canary (630e921) and pass on a Windows x64 build of this branch, where the bins link as .bunx and .exe shims.

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

The two count-pass changes now mirror their build passes exactly (!is_empty() gate, fall-through to directories.bin), and StringBuilder::append in src/bun_core/string/StringBuilder.rs slices dst[..slice.len()], so the uncounted append is a release-build panic, not just a debug assert. Beyond the inline finding, I also checked the npm.rs object-bin arm for the same non-string-value shape — there the extern slice is pre-sized in the count pass and extern_strings_bin_entries_cursor only advances after the loop, so a break 'bin leaves bin unset without stray entries. I also confirmed the manifest builder's doubling slack (npm.rs:2323) stays well under the 523-byte directories.bin for the registry test's small manifest, so that test does exercise the npm.rs half of the fix.

Extended reasoning...

The fix itself is a two-site mirroring of an existing build-pass condition and is correct as written; both Package.rs and npm.rs count passes now skip the empty bin string and fall through to count directories.bin, matching the build passes at Package.rs:2724 and npm.rs:2550. The one inline finding concerns the pre-existing object-bin n arm in Package.rs, which this PR does not touch but which lives in the same 'bin block; the npm.rs sibling of that arm was checked and does not share the lockfile-buffer corruption because its extern slice is pre-sized and the cursor is not advanced on early break. The test's long-path construction was checked against the manifest builder's slack formula and against toBeValidBin/Windows handling; the 523-byte ./-padded path resolves to a short folder name and is not comparable to the skipIf(isWindows) suite at line 3496, which targets the path buffer limit.

One verified lower-impact observation (a convention, logging or cleanup point) was not posted.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/install/lockfile/Package.rs — A package whose bin object has two or more entries and one non-string value loses every bin after install and leaves empty extern strings in the lockfile. At Package.rs:2690-2696 the build pass grows lockfile.buffers.extern_strings by n*2 default entries before the loop, then Package.rs:2703 break 'bin on the first non-string value with self.bin still unset. npm links the valid entries. Fix: validate the rows before growing extern_strings, or truncate extern_strings back to current_len on the early break and link the entries that were valid.

    Extended reasoning...

    A folder or workspace package.json has {"bin": {"a": "a.js", "b": 5}}. The count pass at Package.rs:2255-2262 counts 'a', 'a.js', 'b' and breaks. The build pass reaches the n arm at Package.rs:2689. It reserves and grow_default's n*2 = 4 entries in lockfile.buffers.extern_strings at 2692-2696. The loop appends 'a' and 'a.js' into slots 0 and 1, appends 'b' into slot 2, then hits let Some(v) = v else { break 'bin; } at 2702-2703. self.bin is never assigned, so the package has Tag::None and no bin is linked. extern_strings keeps four entries, the last one default, which are serialized into bun.lockb and bun.lock but referenced by nothing. The npm.rs build pass at 2499-2500 breaks the same way but never advances extern_strings_bin_entries_cursor, so it does not leak slots; the two parsers diverge. This is pre-existing, but it is the same count/build pair the PR audits and the same 'value is not a string' input class. Remedy: on the early break, truncate extern_strings to current_len, or better, skip the non-string entry and still set Tag::Map for the valid pairs as npm does.

    Verification: pre-existing — the base commit (0d3492e) has byte-identical code for both the object branch of the count pass and the n arm of the build pass; this PR only edits the string branch of the count pass (Package.rs:2264-2272, npm.rs:2184-2191), so it neither introduces, widens nor adds traffic to this path, and it is a different class from the count/build size mismatch the PR fixes (here count and…

@robobun

robobun commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

The finding on the bin object arm is real, and it predates this PR. It is now #43148. The diff here is unchanged.

What I ran, with a file: folder dependency whose package.json has "bin": {"mixed-a": "a.js", "mixed-b": 5}:

Result
release 1.4.3-canary (c6b7fcb) exit 0, no warning, no node_modules/.bin, bun.lock has "mixed": ["mixed@file:mixed", {}]
debug build of this branch the same
npm 11.16.0, same tree links mixed-a, skips mixed-b

One part of the finding I could not confirm. The build pass does grow lockfile.buffers.extern_strings before the loop, and the early break 'bin leaves those slots in memory. They do not reach the disk in my runs: bun.lockb is 1193 bytes for this package, the same as for a package with no bin, and 1258 bytes when both values are strings. bun.lock is byte-identical to the no-bin case.

Why it is not in this PR. This PR fixes a counting pass that reserves less than its build pass appends, which aborts the process. In the object arm the two passes agree on every size and nothing aborts. The defect is a choice about which entries to keep, it is the same in both parsers (Package.rs:2699 and npm.rs:2495 on main), and a change to it alters what bun install links and what it writes to the lockfile. That needs its own tests for the package.json path, the manifest path and the lockfile output.

@Jarred-Sumner
Jarred-Sumner merged commit 0ffd5b2 into main Sep 17, 2026
11 of 12 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the robobun/61defef4/count-directories-bin-after-empty-bin branch September 17, 2026 21:26
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