Skip to content

install: don't leak package extraction temp directories into $TMPDIR - #33979

Open
robobun wants to merge 7 commits into
mainfrom
farm/d13ddef3/fix-install-tempdir-leak
Open

install: don't leak package extraction temp directories into $TMPDIR#33979
robobun wants to merge 7 commits into
mainfrom
farm/d13ddef3/fix-install-tempdir-leak

Conversation

@robobun

@robobun robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #33977
Fixes #28062

Problem

bun install leaks package-extraction temp directories (e.g. .5dfdff2c5fefbff7-00000006.tmp, .79dbbadf3f7fb2bb-7.react-native) into $TMPDIR. On long-lived CI containers the reporter measured ~25 GB over 40 hours.

Reproduction: run two concurrent bun install processes sharing the same BUN_INSTALL_CACHE_DIR. Every package whose cache publish they race leaks one fully extracted copy per install, even though both installs exit 0. Error paths (a tarball that fails to extract, a patch that fails to apply) also leak one temp directory per attempt.

Cause

Extraction writes the package into a temp dir, then publishes it into the cache via renameat_concurrently (renameat2 with RENAME_NOREPLACE). When a concurrent install already published the entry, that fails with EEXIST and the code fell back to RENAME_EXCHANGE, which swaps the temp dir with the existing cache entry and returns success. The swapped-out tree now sits in $TMPDIR under the temp name and nothing ever deletes it. The patched-dependency apply path and the streaming extractor share the same helper and leaked the same way.

Deleting the swapped-out tree after the exchange turns the leak into a correctness bug: the process that won the race can still hold an open directory fd into that tree while hardlinking it into node_modules, and deleting the entries under it makes its install fail with ENOENT: failed copying files from cache to destination (observed while testing that variant).

The Windows arm of move_to_cache_directory already had that stronger form of the bug: on a publish collision it renamed the existing cache entry out of the cache and deleted it before retrying its own move, yanking the entry out from under the concurrent install that published it. That is the mechanism behind #28062 (ENOENT: failed opening cache/package/version dir on parallel installs sharing a cache on Windows).

Fix

Treat the cache publish race as first-writer-wins on every platform. RenameatConcurrentlyOptions gains keep_existing_destination: when the destination already exists, keep it (it is an equivalent tree extracted from the same tarball, and other processes may be mid-read of it) and delete the caller's private temp copy, which no other process can have open. The tarball extract path (buffered and streaming) and the patch apply path opt in; the bun patch working-directory renames keep the old replace semantics. The Windows arm applies the same rule when the destination directory exists; its rename-out-and-delete fallback block is now dead and deleted.

Error paths are covered by scope guards: ExtractTarball::extract and PatchTask::apply now delete their temp directory when they fail before the rename. This also stops the stranded temp directories reported in #11250, though not the underlying EPERM there, so that issue is not closed by this PR.

Verification

New tests in test/cli/install/bun-install-tempdir-cleanup.test.ts (all three fail on the unfixed build on Linux, leaking 40/2/1 temp dirs respectively against a local registry; the two error-path tests also fail on unfixed Windows):

  • two concurrent installs sharing a cache, 5 iterations with cache eviction: temp dir must be empty afterwards and all installs exit 0
  • a tarball with valid integrity that fails to extract: temp dir must be empty
  • a patch that parses but fails to apply: temp dir must be empty

Plus a bun_sys unit test that keep_existing_destination keeps the destination's contents and removes the source. Existing suites bun-install-patch, bun-patch, bun-install-streaming-extract, and bun-install-tarball-integrity pass. The full test file was also run against a Windows debug build (3/3 clean runs).


[review] gate passed · iteration 3 · 5 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/bun-install-tempdir-cleanup.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (c0b8c8d59)

test/cli/install/bun-install-tempdir-cleanup.test.ts:
213 |   const tmpDir = join(String(dir), "tmp");
214 | 
215 |   const { stderr, exitCode } = await runInstall(join(String(dir), "proj"), join(String(dir), "cache"), tmpDir);
216 |   expect(stderr).toContain("failed applying patch file");
217 | 
218 |   expect(await readdirSorted(tmpDir)).toEqual([".keep"]);
                                            ^
error: expect(received).toEqual(expected)

  [
+   ".3aebc7ff13efbdef-2.tmp",
    ".keep",
  ]

- Expected  - 0
+ Received  + 1

      at <anonymous> (/workspace/bun/test/cli/install/bun-install-tempdir-cleanup.test.ts:218:39)
177 |   const tmpDir = join(String(dir), "tmp");
178 | 
179 |   const { std
... (truncated)

release without fix: all passed
bun test v1.4.0-canary.1 (61c7ea43e)

test/cli/install/bun-install-tempdir-cleanup.test.ts:
(pass) a tarball that fails to extract does not leak its temp directory [13.45ms]
(pass) a patch that fails to apply does not leak its temp directory [11.31ms]
(pass) re-extracting replaces an invalid cache entry [28.01ms]
(pass) concurrent installs sharing a cache do not leak temp directories [471.02ms]

 4 pass
 0 fail
 23 expect() calls
Ran 4 tests across 1 file. [622.00ms]
__F:0:S:0
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/cli/install/bun-install-tempdir-cleanup.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (c0b8c8d59)

test/cli/install/bun-install-tempdir-cleanup.test.ts:
(pass) a patch that fails to apply does not leak its temp directory [260.61ms]
(pass) a tarball that fails to extract does not leak its temp directory [311.73ms]
(pass) re-extracting replaces an invalid cache entry [422.28ms]
(pass) concurrent installs sharing a cache do not leak temp directories [6573.42ms]

 4 pass
 0 fail
 23 expect() calls
Ran 4 tests across 1 file. [8.81s]
__F:0:S:0

release with fix: all passed
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 732ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: component rust-std is up to date

  nightly-2026-05-06-x86_64-unknown-linux-gnu unchanged - rustc 1.97.0-nightly (e95e73209 2026-05-05)

info: checking for self-update (current version: 1.29.0)
�[1m�[92m   Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
�[1m�[92m   Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
�[1m�[92m   Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
�[1m�[92m   Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl
... (truncated)
diff hotspot
src/install/PackageManager/patchPackage.rs         |   7 +-
 src/install/extract_tarball.rs                     |  80 ++++---
 src/install/patch_install.rs                       |   8 +-
 src/sys/lib.rs                                     |  93 +++++++-
 .../install/bun-install-tempdir-cleanup.test.ts    | 259 +++++++++++++++++++++
 5 files changed, 409 insertions(+), 38 deletions(-)

gate history · 4 passed · 0 rejected · iteration 3

evidence per changed file
file                                                  reads  edits  tests
src/install/PackageManager/patchPackage.rs                1      0      0
src/install/extract_tarball.rs                            8      6      0
src/install/patch_install.rs                              2      2      0
src/sys/lib.rs                                            7     16      0
test/cli/install/bun-install-tempdir-cleanup.test.ts      4      8      0

root cause · written by the author bot

The root cause was that bun install's extract-then-rename flow assumed the rename into the global cache would always consume the temporary extraction directory, but on paths where the destination already existed, the rename failed, or the install errored out early, the temp directory was simply abandoned in $TMPDIR and accumulated indefinitely. The fix adds a keep-existing-destination mode to the concurrent rename helper that deletes the source temp directory when another writer has already populated the cache entry (first-writer-wins instead of swap, including on the Windows collision path…

When two bun install processes raced the same cache entry, the loser's
rename fell back to RENAME_EXCHANGE, which swapped its temp directory
with the winner's cache entry and left the swapped-out tree stranded in
the temp dir forever. On long-lived machines this accumulated a full
extracted package copy per raced install (reported at ~25 GB over 40
hours of CI).

Deleting the swapped-out tree after the exchange is not safe either:
the winner may still hold an open fd into that tree while hardlinking
it into node_modules, and deleting it under the winner fails its
install with ENOENT. Instead, treat the publish race as first writer
wins: a new keep_existing_destination option on renameat_concurrently
keeps the existing (equivalent) destination and deletes the caller's
private temp copy, which no other process can be reading.

Also clean up the temp directory on extraction and patch-apply error
paths, which previously leaked a partially extracted tree per failed
attempt.

Fixes #33977
@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:27 AM PT - Jul 11th, 2026

@robobun, your commit c0b8c8d has 4 failures in Build #71937 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33979

That installs a local version of the PR into your bun-33979 executable, so you can run:

bun-33979 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Windows: parallel bun install --no-cache with shared BUN_INSTALL_CACHE_DIR can fail with ENOENT opening cache/package/version dir #28062 - Parallel bun install --no-cache with shared BUN_INSTALL_CACHE_DIR fails with ENOENT — the concurrent rename race this PR fixes (RENAME_EXCHANGE swapping out a tree still in use) matches the reported failure mode
  2. bun install fails on Windows: Operation not permitted (NtSetInformationFile()) #11250 - bun install fails with EPERM moving package to cache dir, leaving .1ffdbfffec475ade-00000001.tmp-style temp directories stranded in $TMPDIR — the error-path cleanup this PR adds would prevent the leak

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #28062
Fixes #11250

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The install pipeline now cleans temporary extraction and patch directories on failure, preserves destinations during concurrent cache publication, initializes rename options with defaults, and adds coverage for concurrent installs and cleanup after extraction or patch failures.

Install cleanup and concurrent publication

Layer / File(s) Summary
Concurrent rename behavior
src/sys/lib.rs
RenameatConcurrentlyOptions preserves an existing destination, removes the source after EEXIST or ENOTEMPTY, and is covered by tests.
Extraction and patch cleanup
src/install/extract_tarball.rs, src/install/patch_install.rs
Temporary directories are cleaned on failure, and concurrent cache moves preserve existing destinations.
Patch rename option initialization
src/install/PackageManager/patchPackage.rs
Patch-related rename operations now populate unspecified options from Default.
Cleanup integration coverage
test/cli/install/bun-install-tempdir-cleanup.test.ts
Tests cover concurrent installs, invalid tarballs, and failed patch application while verifying temporary-directory cleanup.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address the reported temp-directory leak, concurrent cache publication behavior, and failure-path cleanup described in #33977.
Out of Scope Changes check ✅ Passed The PR stays focused on install temp-directory cleanup and related rename semantics; no unrelated code changes stand out.
Title check ✅ Passed The title clearly summarizes the main change: preventing package extraction temp directory leaks during install.
Description check ✅ Passed The description is detailed and covers the problem, cause, fix, and verification, matching the template's intent.

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: 3

🤖 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-tempdir-cleanup.test.ts`:
- Around line 122-219: Mark all three independent tests—“concurrent installs
sharing a cache do not leak temp directories,” “a tarball that fails to extract
does not leak its temp directory,” and “a patch that fails to apply does not
leak its temp directory”—as concurrent tests using the repository’s established
test.concurrent pattern. Preserve their isolated temp directories, registries,
and existing assertions.
- Around line 179-184: Reorder the assertions in the install cleanup test so the
readdirSorted(tmpDir) filesystem validation runs before the exitCode assertion.
Keep the stderr assertion where it is, and make expect(exitCode).not.toBe(0) the
final assertion in the test.
- Around line 215-219: Update the test around runInstall to destructure exitCode
alongside stderr, then assert that exitCode is non-zero (or matches the expected
failure code) in addition to checking the patch error message and
temporary-directory cleanup. Keep the existing stderr and readdirSorted
assertions unchanged.
🪄 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: a0b4c152-430e-45dc-99c6-1458281ee815

📥 Commits

Reviewing files that changed from the base of the PR and between 9657f37 and 3921ba8.

📒 Files selected for processing (5)
  • src/install/PackageManager/patchPackage.rs
  • src/install/extract_tarball.rs
  • src/install/patch_install.rs
  • src/sys/lib.rs
  • test/cli/install/bun-install-tempdir-cleanup.test.ts

Comment thread test/cli/install/bun-install-tempdir-cleanup.test.ts Outdated
Comment thread test/cli/install/bun-install-tempdir-cleanup.test.ts
Comment thread test/cli/install/bun-install-tempdir-cleanup.test.ts Outdated
The Windows arm of move_to_cache_directory handled a destination that
already exists by renaming it out of the cache and deleting it, then
retrying its own move. When the existing entry was published by a
concurrent install that is still copying from it, this yanks the tree
out from under that process and fails it with ENOENT (#28062). Apply
the same first-writer-wins rule as the POSIX path: if the destination
directory exists, delete our own temp copy and use the existing entry.
This deletes the rename-out-and-delete fallback block.

Also address review feedback on the tests: run them concurrently,
assert the patch failure exit code, and move exit-code assertions
after filesystem checks.
Comment thread src/sys/lib.rs
When the filesystem lacks RENAME_NOREPLACE (EOPNOTSUPP), the errno does
not say whether the destination exists, and the fallback deleted it
unconditionally before renaming. With keep_existing_destination set,
check for the destination first and keep it, deleting the source
instead, so the option's contract holds on those filesystems too.
@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

Checked both suggested issues:

#28062: confirmed related. The Windows arm of move_to_cache_directory handled a publish collision by renaming the existing cache entry out of the cache and deleting it, which is exactly the mechanism that issue's analysis points at (and the same failure I reproduced on Linux when testing a delete-based variant of this fix: the winning install fails with ENOENT while copying from the entry it just published). Commit 120895b applies first-writer-wins on Windows too and removes that fallback, so I added Fixes #28062 to the description. My concurrent-install test did not trigger the ENOENT on an unfixed Windows release build in 6 runs (the race window there is narrow), so the claim rests on the mechanism being removed rather than on a reproduced failure.

#11250: only partially addressed. The stranded temp directories after a failed move are now cleaned up by the new error-path guards, but the underlying EPERM from NtSetInformationFile is not fixed here, so I did not add it as auto-close.

Comment thread src/sys/lib.rs
keep_existing_destination assumed an existing cache entry is always an
equivalent copy from a concurrent install, but package_missing_from_cache
treats an npm entry without package.json as absent and re-extracts to
heal it. Keeping such an entry broke that heal (caught by
bun-install-registry.test.ts on the Windows lanes). Delete an existing
npm entry that is missing package.json before publishing the fresh copy;
no process reads from an entry that fails the cache check.

Also make the keep_existing_destination fallback existence check
directory-aware: Windows exists_at returns true only for files, so the
guard never fired for directory destinations there.
Comment thread src/install/extract_tarball.rs
Comment thread src/sys/lib.rs
Windows exists_at is file-only, so the assertion was vacuous there.

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

No blocking issues found after four rounds of feedback (all addressed), but this changes the concurrent cache-publish semantics from swap to first-writer-wins across Linux/macOS/Windows and rewrites the Windows collision path in extract_tarball.rs, so it's worth a human look at the design.

Reviewed: the new keep_existing_destination path in renameat_concurrently_without_fallback (default-off callers keep old RENAME_EXCHANGE behavior; all call sites are in this PR's files). Checked the scopeguard placement in extract/apply disarms only on success and covers every early return. Checked the Windows arm's directory_exists_at_w + delete-source replaces the old rename-out-and-delete without leaving the retry loop unbounded. The narrow TOCTOU on the invalid-entry pre-delete (flagged inline earlier) remains as an accepted followup.

Extended reasoning...

Overview

Five files touched: src/sys/lib.rs gains a keep_existing_destination flag on RenameatConcurrentlyOptions and a new branch in renameat_concurrently_without_fallback that deletes the source on EEXIST/ENOTEMPTY (and, after follow-up commits, on the racy fallback path via a directory-aware existence check). src/install/extract_tarball.rs and src/install/patch_install.rs opt into that flag, add scopeguard-based cleanup of the extraction/patch temp dir on every failure exit, and (on Windows) replace the rename-out-and-delete fallback with first-writer-wins. src/install/PackageManager/patchPackage.rs is purely mechanical ..Default::default() additions required by the new struct field. A new integration test file plus a bun_sys unit test cover the concurrent-race, corrupt-tarball, failed-patch, and heal-invalid-entry cases.

Security risks

None identified. The change operates on Bun's own private temp directories and package cache; no new user-controlled paths are introduced, and the deleted trees are the caller's own freshly-created temp copies. The delete_tree targets are derived from FileSystem::tmpname / cached_*_folder_name_print, not from tarball contents.

Level of scrutiny

High. This is production package-manager code on a hot path (bun install), it changes the concurrency contract of a shared syscall helper, and it has per-platform arms (Linux renameat2 flags, Windows NtSetInformationFile errno handling, an EOPNOTSUPP fallback for NFS/FUSE). The design decision — keep the existing cache entry rather than swap or replace it — trades the old behavior's temp-dir leak for a narrow TOCTOU on the invalid-entry pre-delete (flagged inline as non-blocking; requires a pre-corrupted cache entry plus two installs preempted between adjacent syscalls). That trade-off is reasonable but should be a maintainer's call, not an automated approval.

Other factors

I've reviewed this PR across four prior runs; each raised a specific gap (EOPNOTSUPP fallback bypassing the guard; Windows exists_at being file-only in both the production sad path and the unit test; the pre-delete TOCTOU) and each was fixed or acknowledged in a follow-up commit (f55cf7e, 9943a49, 61c7ea4). All CodeRabbit test-convention nits are resolved. Every call site of renameat_concurrently* lives in files this PR touches, so the signature change has no untouched consumers. The evidence block shows the new tests fail on main and pass on the PR under both ASAN-debug and release builds. Given the cross-platform surface and the semantic shift, deferring to a human reviewer.

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

No bugs found, but this changes concurrent cache-publish semantics in renameat_concurrently (first-writer-wins replacing the RENAME_EXCHANGE fallback) and rewrites the Windows collision arm in extract_tarball.rs, so it's worth a human look at the cross-process behavior.

Checked: the new keep_existing_destination path deletes only the caller's private temp copy on EEXIST/ENOTEMPTY and on the sad-path fallback; the bun patch working-dir renames in patchPackage.rs correctly do not opt in; the extract/patch scopeguards are defused only after the rename succeeds; move_file_z and the existing rename callers keep their old semantics via Default::default(). The earlier inline nits (EOPNOTSUPP fallback, Windows file-only exists_at, vacuous unit-test assertion) were all addressed in f55cf7e / 9943a49 / 61c7ea4. One acknowledged narrow TOCTOU remains in the invalid-cache-entry pre-delete as a followup.

Extended reasoning...

Overview

The PR stops bun install from leaking package-extraction temp directories into $TMPDIR by (1) adding a keep_existing_destination option to renameat_concurrently that, on a publish collision, keeps the existing cache entry and deletes the caller's private temp copy instead of swapping/replacing it, (2) opting the tarball-extract and patch-apply paths into that option, (3) wrapping both extraction paths in scopeguards so error exits also clean up the temp dir, and (4) rewriting the Windows collision arm of move_to_cache_directory to the same first-writer-wins policy (removing the old rename-out-and-delete fallback). It also adds a pre-delete of npm cache entries that lack package.json so a corrupt entry isn't preserved forever by the new keep-existing semantics. ~410 lines across src/sys/lib.rs, src/install/extract_tarball.rs, src/install/patch_install.rs, src/install/PackageManager/patchPackage.rs, plus a new 4-test integration file and a Rust unit test.

Security risks

None identified. The change operates on Bun-created temp directories under $BUN_TMPDIR and the install cache; no new user-controlled paths reach delete_tree. The patchPackage.rs hunks are purely mechanical ..Default::default() additions.

Level of scrutiny

High. This is production-critical package-manager code with cross-process concurrency semantics on two very different platforms. The signature of the shared renameat_concurrently_without_fallback helper changed, and the Windows extract arm was substantially rewritten. Three rounds of prior review on this PR each caught a real (if narrow) semantic gap — EOPNOTSUPP falling through to the destructive sad path, Windows exists_at being file-only so the sad-path guard was a no-op there, and the same issue in the unit test — which is a signal that the interactions here are subtle enough to merit human eyes on the final state.

Other factors

  • All prior review comments (CodeRabbit and mine) are resolved; the diff reflects the fixes.
  • One inline note I left on extract_tarball.rs (a narrow check-then-act TOCTOU when two installs both encounter a pre-existing corrupt cache entry) was explicitly called out as a non-blocking followup and left as-is; that's a reasonable trade-off but a maintainer should confirm.
  • Tests are hermetic (local in-process registry, per-test tempDir/cache), use test.concurrent, and were shown to fail on the unfixed build. The concurrent-install test relies on timing to hit the race and iterates 5× to make it reliable; that's inherently probabilistic but the assertion ("tmp dir is empty afterwards") can't false-fail if the race isn't hit.
  • The RENAME_EXCHANGE fallback branch in renameat_concurrently_without_fallback is now only reachable when keep_existing_destination is false — a maintainer may want to weigh in on whether any remaining caller actually wants the old swap-and-strand behavior, or whether that branch should be removed too.

@robobun

robobun commented Jul 11, 2026

Copy link
Copy Markdown
Collaborator Author

CI status across the last three runs: every install-related suite is green (bun-install-registry, bun-install-patch, bun-patch, bun-install-streaming-extract, bun-install-tarball-integrity, and the new bun-install-tempdir-cleanup tests). The remaining red lanes are unrelated, pre-existing flaky tests that change from run to run (napi on Windows baseline, worker-message-port and fetch-backpressure, then no-orphans on macOS 26 and net-mongodb-pattern-leak), each already tracked separately. The diff itself is ready for review.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

One interaction to be aware of when reviewing this: keeping the existing destination unconditionally is not right for file: and URL tarballs. Their cache folder is named after the hash of the path or URL (@T@<hash>@@@1), not the contents, and every install without a lockfile re-extracts them over that folder. With first-wins, a repacked tarball reinstalled after removing bun.lock keeps installing the previous extraction while the new sha512 is written to the lockfile (on main the fresh copy replaces the folder via RENAME_EXCHANGE; what leaks is the displaced folder).

#38702 fixes that leak for tarballs by deciding before extraction whether the folder already existed (CachePublish::Supersede, replace and delete the displaced folder) or not (KeepExisting, first-wins via a keep_existing_destination option on renameat_concurrently). It leaves npm and GitHub publishing as is, so the two changes are complementary; the first-wins path here should apply to tarballs only in the KeepExisting case.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Another data point for this one, from a report against current main (ada2a67), plain npm packages only: four cold bun install runs of the same five dependencies (typescript, react, react-dom, vite, eslint) started at the same time against one BUN_INSTALL_CACHE_DIR left 209 to 268 directories (98 to 180 MB) in <cache>/.tmp per run, with every install exiting 0. The same four installs run one after another leave 0, and re-running them in parallel on the warm cache adds 0, so it is exactly the lost-publish-race case this PR describes (the RENAME_EXCHANGE fallback succeeds, and the copy left under the staging name is the one it displaced). Hoisted and isolated installs behave the same, and 1.3.14 matches, so this is long-standing rather than a regression.

For whoever reviews: this PR, #36229 and #38702 each merge cleanly onto main today, but any two of them conflict with each other in src/install/extract_tarball.rs and src/sys/lib.rs (and #36229 also in src/install/patch_install.rs), so whichever lands first, the other two need a rebase. On current main both the buffered path and the streaming extractor (TarballStream.rs) still publish through move_to_cache_directory, so the keep_existing_destination change here covers both.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant