install: don't fail on an unresolved optional dependency with an empty name - #31655
Conversation
…y name
An optional dependency declared under an empty key
("optionalDependencies": { "": "..." }) whose target does not resolve on
the current platform keeps its empty name — the resolved-package name is
only substituted once the dependency resolves. The hoisting tree builder
then rejected that empty name with `error: Invalid dependency name ""`
and aborted the whole install.
A dependency with an empty name has no `node_modules/<name>` folder to
escape, so guard `!name.is_empty()` before the safety check, matching the
lockfile parser (bun.lock.rs) and isolated installer (isolated_install.rs)
which already skip empty names instead of failing.
Closes #31652
|
Updated 4:32 AM PT - Jun 1st, 2026
✅ @robobun, your commit 17c6b90ca3e8a9be96e5acd8164f48fc0483852e passed in 🧪 To try this PR locally: bunx bun-pr 31655That installs a local version of the PR into your bun-31655 --bun |
|
Warning Review limit reached
More reviews will be available in 42 minutes and 42 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThis PR fixes a bug where ChangesEmpty dependency name handling
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning Review ran into problems🔥 ProblemsStopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a Comment |
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/regression/issue/31652.test.ts`:
- Around line 120-122: Move the exit code assertion to the end of the test so
behavioral failures are reported first: place the expect(exitCode).toBe(0) after
the sanity check expect(emptyNameManifestRequested).toBe(true). Update the test
that references variables exitCode and emptyNameManifestRequested so the
emptyNameManifestRequested assertion runs before the exitCode assertion.
🪄 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: 9ecc6ef8-4b9b-4e12-a883-256a901784fa
📒 Files selected for processing (2)
src/install/lockfile/Tree.rstest/regression/issue/31652.test.ts
There was a problem hiding this comment.
LGTM — minimal one-line guard that brings Tree.rs in line with the two sibling call sites, with a solid regression test.
Extended reasoning...
Overview
This PR adds a !dependency_name.is_empty() && guard before the is_safe_install_folder_name check in src/install/lockfile/Tree.rs:process_subtree, and adds a regression test (test/regression/issue/31652.test.ts) that serves a package with optionalDependencies: { "": "1.0.0" } from a mock registry and verifies the install completes. The production change is effectively one added condition plus an explanatory comment; the rest is hoisting the name slice into a local.
Security risks
None. The modified check is the path-traversal guard from #31221; this change only exempts the empty string, which cannot produce a node_modules/<name> folder to escape. I verified the two sibling call sites the PR description cites already use the identical pattern — bun.lock.rs:2363 (!name_str.is_empty() && !is_safe_install_folder_name(...)) and isolated_install.rs:2147 (!name.is_empty() && !is_safe_install_folder_name(...)) — so this is a consistency fix, not a new exemption. Genuinely unsafe non-empty names (.., a/../b, etc.) still fail the check exactly as before, and is_safe_install_folder_name itself (dependency.rs:583) is unchanged.
Level of scrutiny
Low-to-medium. bun install is production-critical, but the change is a one-condition guard matching established code at two other sites, restores 1.3.x behavior that regressed on canary, and is covered by a targeted regression test that fails on main and passes with the fix. No CODEOWNERS entries cover src/install/.
Other factors
- The only reviewer feedback (CodeRabbit: assert
exitCodelast) was addressed in b669815 and the thread is resolved. - The CI failures reported by robobun are musl
build-cppand-no-pielinker warnings inbuild-rust— infrastructure noise unrelated to a Rust source change and a TS test file. - The PR description includes a clear root-cause analysis (empty alias only gets its real name substituted after resolution; unresolved optional deps keep the empty name and hit the new check) and confirms the existing
..path-traversal rejection tests still pass. - No bugs were found by the bug hunting system.
There was a problem hiding this comment.
I didn't find any issues — the empty-name guard matches the sibling call sites in bun.lock.rs and isolated_install.rs and the path-traversal tests still cover non-empty unsafe names — but since this relaxes a path-safety check in the hoisted install path, it's worth a quick human sanity check.
Extended reasoning...
Overview
This PR adds a single !dependency_name.is_empty() && guard before the is_safe_install_folder_name check in Tree::process_subtree (src/install/lockfile/Tree.rs), and adds a regression test (test/regression/issue/31652.test.ts). The fix restores 1.3.x behavior where an unresolved optional dependency declared under an empty key is silently skipped rather than aborting the whole install with error: Invalid dependency name "".
I verified the PR's claim that the two sibling call sites already carry this exact guard: bun.lock.rs:2363 (!name_str.is_empty() && !dependency::is_safe_install_folder_name(...)) and isolated_install.rs:2147 (!name.is_empty() && !crate::dependency::is_safe_install_folder_name(...)). is_safe_install_folder_name itself returns false for empty input, which is why the unguarded call site rejected it.
Security risks
This change narrows a path-traversal guard, which is the security-relevant part. The exemption is limited to the empty string, which cannot escape node_modules/<name> (there is no folder segment to traverse with), and in the motivating case (pkg_id == invalid_package_id, non-peer optional) the very next branch is continue 'dep so nothing is placed. Non-empty unsafe names like .. / ../.. still hit maybe_report_error exactly as before, and relative_path_and_depth retains its own folder_name_is_safe crash-guard for any tree node that does get placed. So I don't see a traversal vector being opened here, but it is still a relaxation of a safety check.
Level of scrutiny
Medium-high. The diff is tiny and mechanically consistent with established code, but it sits in the hoisted-install tree builder — a hot, security-adjacent path that runs on every bun install. Per my guidelines I don't auto-approve relaxations of path-safety / install-folder validation even when they look obviously correct.
Other factors
- Not covered by CODEOWNERS.
- The regression test is well-constructed (mock registry, asserts the empty-name manifest was actually requested, asserts
topstill installs, exit code last) and the one CodeRabbit nit (assert exitCode last) was addressed in b669815. - The single CI failure (
streams-leak.test.tson x64) is unrelated to install. - The bug-hunting system found nothing.
Closes #31652
Repro
bun install -g @openai/codexon1.4.0-canaryaborts with:On
1.3.xthe same command installs fine. Minimal reduction — a package whose manifest declares an optional dependency under an empty key whose target doesn't resolve on the current platform:Cause
When a dependency alias (the key) is empty, the real package name is only substituted back into
Dependency.nameif the dependency resolves (assign_resolution/assign_root_resolutioninPackageManagerResolution.rs). An optional dependency that fails to resolve keepspkg_id == invalid_package_idand an empty name.That empty-named dependency then reaches the hoisting tree builder, where a name-safety check added in #31221 (
src/install/lockfile/Tree.rs) treats the empty name as unsafe and callsmaybe_report_error("Invalid dependency name \"\""), aborting the install. The original Zig implementation had no such check here, which is why 1.3.x works.The check was also added inconsistently: the two sibling call sites already guard the empty case —
src/install/lockfile/bun.lock.rs:if !name_str.is_empty() && !is_safe_install_folder_name(name_str)src/install/isolated_install.rs:if !name.is_empty() && !is_safe_install_folder_name(name)— while
Tree.rswas the only one missing!name.is_empty().Fix
Guard
!name.is_empty()before the safety check inTree.rs, matching the sibling call sites. An empty alias has nonode_modules/<name>folder to escape, so it is skipped like an unresolved optional dep should be, instead of failing the whole install. Genuinely unsafe non-empty names (e.g...,../..) still error exactly as before.Verification
test/regression/issue/31652.test.tsserves a package withoptionalDependencies: { "": "1.0.0" }whose target 404s and asserts the install completes withoutInvalid dependency name ""and with exit 0.main:error: Invalid dependency name "", exit 1.rejects dependency aliases containing '..' path segments,does not create a cache index entry outside the cache directory for a dependency alias of '..', and the two sibling'..'tests) still pass — the empty-name guard does not weaken the security checks.