Skip to content

transpiler cache: give the Rust line its own .pile2 filename namespace#31803

Open
robobun wants to merge 6 commits into
mainfrom
farm/ad2450b3/transpiler-cache-version-namespace
Open

transpiler cache: give the Rust line its own .pile2 filename namespace#31803
robobun wants to merge 6 commits into
mainfrom
farm/ad2450b3/transpiler-cache-version-namespace

Conversation

@robobun

@robobun robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Symptom

Reported pattern (second-hand, via a user who tried the Rust-port canaries): a test suite worked once, started failing permanently on the second run, kept failing after downgrading bun, and was only "fixed" by recreating the project. Classic runtime transpiler cache poisoning — except downgrading should have invalidated the cache, and it didn't.

Cause

The Zig 1.3.x maintenance line and the Rust line share the runtime transpiler cache byte for byte: same directory (~/.cache/bun/@t@, …), same content-hash .pile filenames (wyhash seed 42, kept deliberately interchangeable), same metadata layout, same feature-hash inputs — but they transpile with different implementations and bump expected_version independently:

build cache version
bun-v1.3.12 (Zig) 18
bun-v1.3.13, bun-v1.3.14 (Zig) 20
Rust canaries May 14–19 (#30412 until #30975) 20
Rust canaries after #30975 / #30888 21 / 22

Two failure modes, both from sharing one filename namespace:

  • Colliding version numbers (the v20 overlap): each implementation fully trusts entries written by the other — input_hash, features_hash, and all payload hashes verify (they hash the entry's own payload) — so foreign transpiled output (including early-port transpiler bugs like bun test --parallel does not wait for async preload to finish #30887) is served indefinitely, never unlinked, and survives version up/downgrades within the line that trusts it.
  • Differing version numbers (the current 20-vs-22 state): each implementation treats the other's entries as stale and unlinks/rewrites them, so alternating installed bun versions means neither line's cache can ever persist — entries don't reliably load and are never consistent across runs.

Fix

Stop sharing the namespace: the Rust line writes <hash>.pile2 (debug builds: .debug.pile2). The two caches are fully disjoint — an entry only ever loads in the implementation line that wrote it, both caches coexist when bun versions alternate, and .pile files are never read, trusted, or deleted (they belong to the Zig line, which versions them independently). EXPECTED_VERSION stays a plain 23 and only has to be consistent with this implementation. Also deletes the dead RuntimeTranspilerCache* mirror structs (incl. a stale version constant) from bun_bundler::cache.

Verification

New test in test/cli/run/transpiler-cache.test.ts forges the entry a Zig-line bun would have written for the exact same source — legacy .pile name, matching input/feature hashes, valid self-hashes, different (sentinel) transpiled output:

  • Before the fix the forged entry is trusted and executes: Expected: "original-…" Received: "POISONED".
  • After the fix the source is re-transpiled (.pile2 written alongside), the .pile file is byte-identical afterwards (never loaded, never deleted), and the third run hits the .pile2 entry without the sentinel ever surfacing.

bun bd test test/cli/run/transpiler-cache.test.ts — 12/12 pass; test/regression/issue/30887.test.ts — 5/5 pass (its cache-file filter updated for the new suffix). Also swept ESM/CJS/TLA/import defer/TS-enum/non-ASCII/error-stack modules across 3 runs each against a fresh cache to confirm no run-to-run divergence.

Related but distinct: #31758 fixes a same-family poisoning between plain-run and bun test entries.

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review 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: Pro

Run ID: 7d2cd4a0-1a3c-4ba0-a921-0f062a77c3a8

📥 Commits

Reviewing files that changed from the base of the PR and between 319675f and c18495b.

📒 Files selected for processing (1)
  • docs/runtime/environment-variables.mdx

Walkthrough

This PR consolidates runtime transpiler cache type definitions into the canonical AST crate, bumps the cache format version to 23, migrates on-disk cache file suffixes from .pile to .pile2, updates documentation to reflect a new 4 KB caching threshold, and adds regression test coverage for legacy cache file handling during the migration.

Changes

Runtime transpiler cache format and type consolidation

Layer / File(s) Summary
Type canonicalization and ownership clarification
src/bundler/cache.rs, src/ast/transpiler_cache.rs
Remove bundler-local wire-format type definitions and consolidate on RuntimeTranspilerCache in the AST crate; update doc comments to clarify parser/bundler vs jsc ownership and the type-erased *mut () entry storage.
Cache version bump and .pile2 suffix migration
src/jsc/RuntimeTranspilerCache.rs
Bump EXPECTED_VERSION from 22 to 23, change write_cache_filename to emit .pile2/.debug.pile2, and document the breaking format change and deliberate incompatibility with legacy .pile caches.
Documentation updates for cache threshold and file format
docs/runtime/environment-variables.mdx
Change runtime transpiler cache threshold from 50 KB to 4 KB, state cached contents include transpiled output plus sourcemap for files >4 KB, and update example file extension references from .pile to .pile2 (note Bun 1.3.x used .pile).
Regression test coverage for legacy .pile cache handling
test/cli/run/transpiler-cache.test.ts, test/regression/issue/30887.test.ts
Add a test ensuring legacy .pile entries are neither loaded nor deleted and that a fresh .pile2 is written instead; update stale-cache test filename handling and selection to include .pile variants.

Possibly related PRs

  • oven-sh/bun#30888: Both PRs touch runtime transpiler cache format/versioning and tests around stale-cache behavior.

Suggested reviewers

  • RiskyMH
  • dylan-conway
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: moving the Rust runtime transpiler cache to a separate .pile2 namespace.
Description check ✅ Passed The description covers the change and verification details, with clear problem/cause/fix/verification sections that satisfy the template intent.
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.

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:24 PM PT - Jun 29th, 2026

@robobun, your commit c18495b has 1 failures in Build #67084 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31803

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

bun-31803 --bun

@github-actions github-actions Bot added the claude label Jun 4, 2026

@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 — effectively a version-constant bump into a reserved namespace, with a solid regression test; the flagged mirror constant is pre-existing dead code and non-blocking.

Extended reasoning...

Overview

This PR touches two files: src/jsc/RuntimeTranspilerCache.rs (changes EXPECTED_VERSION from 22 to 1_000_000 + 23, adds a RUST_VERSION_NAMESPACE constant and an explanatory doc comment) and test/cli/run/transpiler-cache.test.ts (adds one regression test). The functional change is a single constant value; Metadata::decode already rejects any cache_version != EXPECTED_VERSION with StaleCache, which triggers the existing unlink-and-rewrite path in from_file_with_cache_file_path. No control flow, layout, or hashing changes.

Security risks

None introduced. The change strictly tightens which on-disk cache entries are trusted — entries from the Zig 1.3.x line or pre-namespace canaries now fail the version check and are discarded/rewritten instead of being executed. The cache directory remains per-user; no new attack surface.

Level of scrutiny

Low. This is the canonical "bump cache version to invalidate stale entries" pattern, just with a large offset to permanently separate two independently-bumped numbering lines. The worst-case outcome is a one-time cold cache for users upgrading, which is the explicit intent. The new test is well-constructed: it writes a real entry, patches it to a colliding foreign version with a valid self-hash, and asserts the sentinel does not execute and the entry is rewritten under the new namespace.

Other factors

The one inline finding is explicitly pre-existing: a dead mirror constant in src/bundler/cache.rs that was already stale (20 vs 22) before this PR and has no live references — purely a hygiene note, not a blocker. No CODEOWNERS cover the touched paths, no outstanding human review comments, and the existing transpiler-cache test suite covers the surrounding hit/miss/invalidation behavior.

Comment thread src/jsc/RuntimeTranspilerCache.rs Outdated

@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 — thanks for the cleanup in 3e64804; the remaining //! header nit is cosmetic and non-blocking.

Extended reasoning...

Overview

The functional change is a single constant: EXPECTED_VERSION in src/jsc/RuntimeTranspilerCache.rs moves from 22 to 1_000_000 + 23, namespacing the Rust-line transpiler cache versions away from the Zig 1.3.x maintenance line so the two implementations can never accidentally trust each other's on-disk entries. The rest of the diff is (a) an explanatory doc comment on the constant, (b) a new regression test in transpiler-cache.test.ts that forges a colliding version-22 entry and asserts it's rejected and rewritten, and (c) the follow-up commit 3e64804 deleting dead mirror structs from src/bundler/cache.rs plus a doc-pointer fix in src/ast/transpiler_cache.rs — both done in response to my earlier review comment, which is now resolved.

Security risks

None introduced. The version field is already compared via strict equality in Metadata::decode (!= EXPECTED_VERSION → StaleCache), so changing its value only changes which entries match — there's no new parsing surface, no new file I/O, and no widening of trust. If anything this narrows the attack/confusion surface by guaranteeing entries written by a different bun implementation are unlinked rather than executed. The deleted bundler mirror structs were verifiably unreferenced (confirmed by grep: zero remaining hits for any of the removed symbols).

Level of scrutiny

Low-to-medium. The runtime transpiler cache is on the hot path for bun run, but the mechanism touched here is the existing version-mismatch → unlink → re-transpile path, which is already exercised by the existing test suite and the new test. The constant fits comfortably in u32, the encode/decode wire format is unchanged, and the only user-visible effect of the bump is a one-time cache invalidation on upgrade — which is the explicit goal. The dead-code deletion is pure subtraction with no callers.

Other factors

  • My previous inline comment was addressed and resolved by the author in 3e64804.
  • The one new finding is a doc-comment nit (the module-level //! header at src/ast/transpiler_cache.rs:10-11 still references bun_bundler::cache) — same class of stale-pointer cleanup already done twice in 3e64804, zero runtime impact, fine to fold in or leave for a follow-up.
  • CI failures on this build (musl LTO link error in bun_rust.lto.o.tmp, bunx.test.ts) are unrelated infrastructure/flakes — none touch the transpiler cache and none reference the changed files.
  • No CODEOWNERS cover the touched paths.

Comment thread src/ast/transpiler_cache.rs

@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 — both prior review notes addressed; the functional change is just the EXPECTED_VERSION namespace offset plus verified-dead-code removal and a regression test.

Extended reasoning...

Overview

The functional core of this PR is a single constant change in src/jsc/RuntimeTranspilerCache.rs: EXPECTED_VERSION moves from 22 to RUST_VERSION_NAMESPACE + 23 (= 1_000_023), reserving cache versions ≥ 1_000_000 for the Rust line so entries written by the Zig 1.3.x maintenance line (or by the v20–22 overlap-window canaries) always fail the existing cache_version != EXPECTED_VERSION check in Metadata::decode and get unlinked/rewritten. The wire format is unchanged — cache_version was already a u32 LE at offset 0, so the larger value round-trips identically. Bumping this constant is routine for this file (the doc comment lists 22 prior bumps); only the magnitude is novel, and it's well-motivated and thoroughly documented inline.

The remaining changes are non-functional: ~90 lines of dead mirror structs deleted from src/bundler/cache.rs (I flagged these in an earlier review round and re-verified zero references remain in src/), doc-comment corrections in src/ast/transpiler_cache.rs and src/bundler/cache.rs pointing at the canonical bun_jsc::runtime_transpiler_cache types, and a new regression test in transpiler-cache.test.ts that synthesizes a foreign-version-22 entry with valid self-hashes and asserts it is rejected and rewritten under the new namespace.

Security risks

None introduced. The change is strictly defensive — it narrows which on-disk cache entries the runtime will trust, closing a cross-implementation cache-poisoning window. No new file paths, no parsing of additional untrusted data, no auth/crypto/permission surface.

Level of scrutiny

Low-to-moderate. The transpiler cache is user-facing (cache poisoning produces persistent wrong behavior), but the actual mechanism touched is the existing version-mismatch → StaleCache → unlink path, which is unchanged; only the constant it compares against moved. The dead-code deletion was already traced to zero call sites in my first review round and re-confirmed by grep on the current tree. No CODEOWNERS paths match.

Other factors

Both of my earlier review comments (stale RUNTIME_TRANSPILER_CACHE_VERSION mirror, stale module //! header) were addressed in 3e64804 and d5fd546 respectively and are resolved. The bug-hunting system found nothing on the current revision. The CI failures reported by robobun on 3e64804 (musl LTO llvm-link data-layout error, bunx.test.ts) are unrelated infra/flake — none of these files touch the linker pipeline or bunx, and the only delta since then (d5fd546) is a doc-comment edit.

@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

CI triage (current: build 67084 on c18495b, rebased onto main 16a7269, all lanes finished): the diff is green — 72/72 individual lanes passed, including both touched test files (test/cli/run/transpiler-cache.test.ts, test/regression/issue/30887.test.ts) on every platform. The only error annotation in the entire build is unrelated:

  • test/js/node/test/parallel/test-net-connect-memleak.js on the alpine-3.23 x64/x64-baseline lanes: AssertionError: Expected values to be strictly equal: false !== true at the globalThis.gc() + setImmediate collection check. The same failure appears on every recent build that ran those lanes, across unrelated branches (e.g. builds 67072, 67077, 67086), so it is a pre-existing musl-lane GC issue, not something a cache-filename/constant/docs diff can reach. A retrigger would re-fail the same way, so I'm not pushing one.

Ready to merge over that lane, or to rebase once the memleak test is fixed on main.

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How is this expected to load and be consistent?

@robobun robobun changed the title transpiler cache: move Rust-line versions out of the Zig 1.3.x numbering space transpiler cache: give the Rust line its own .pile2 filename namespace Jun 4, 2026
@robobun

robobun commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator Author

How is this expected to load and be consistent?

You're right — it wasn't. With the version offset the two implementation lines still read and wrote the same <hash>.pile files, so whichever bun ran last treated the other line's entry as stale, unlinked it, and rewrote it. Anyone alternating 1.3.x and canary (or two projects/tools pinning different bun versions against the same $HOME) would never get entries that load consistently — the shared file just ping-pongs between formats.

Reworked in 559ef8f: the Rust line now writes <hash>.pile2 (.debug.pile2 for debug builds) instead of partitioning the version numbers. The two caches are fully disjoint:

  • an entry only ever loads in the implementation line that wrote it, every time — no cross-trust (the v20 poisoning that motivated this PR) and no cross-unlink thrash;
  • both caches coexist when installed bun versions alternate;
  • .pile files are never read, trusted, or deleted — they belong to the Zig 1.3.x line, which versions them independently;
  • EXPECTED_VERSION goes back to a plain 23 and only has to stay consistent with this implementation.

The regression test now forges the entry a Zig-line bun would have written for the exact same source (legacy .pile name, matching input/feature hashes, valid self-hashes, sentinel output) and asserts it's never served and byte-identical afterwards, while runs 2–3 re-transpile once and then hit the new .pile2 entry. Fails on a pre-fix build (sentinel executes), passes with the fix; 12/12 in transpiler-cache.test.ts and 5/5 in regression/issue/30887.test.ts (its filename filter updated).

Comment thread src/jsc/RuntimeTranspilerCache.rs
Comment thread src/jsc/RuntimeTranspilerCache.rs Outdated
Comment thread docs/runtime/environment-variables.mdx

@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: 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 `@docs/runtime/environment-variables.mdx`:
- Line 199: Reword the BUN_RUNTIME_TRANSPILER_CACHE_PATH description to improve
flow by consolidating the three "If" sentences into one clear sentence that
lists the conditions and outcomes for the variable; reference the environment
variable name BUN_RUNTIME_TRANSPILER_CACHE_PATH and state that setting it to an
empty string or "0" disables caching, leaving it unset uses the platform cache
directory, and any other non-empty value is treated as the explicit cache
directory path.
🪄 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: 67f13b7c-bd71-48ac-bf03-82e3bbdae685

📥 Commits

Reviewing files that changed from the base of the PR and between a3464c6 and 69110f9.

📒 Files selected for processing (6)
  • docs/runtime/environment-variables.mdx
  • src/ast/transpiler_cache.rs
  • src/bundler/cache.rs
  • src/jsc/RuntimeTranspilerCache.rs
  • test/cli/run/transpiler-cache.test.ts
  • test/regression/issue/30887.test.ts

Comment thread docs/runtime/environment-variables.mdx Outdated

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

All my earlier nits are addressed and the bug hunter found nothing this round; deferring to a human to confirm the .pile2 filename-namespace approach (the mid-PR redesign prompted by the "how is this expected to load and be consistent?" review) before it ships as the on-disk format.

Extended reasoning...

Overview

The runtime change is two string literals (b".pile"b".pile2", b".debug.pile"b".debug.pile2") in write_cache_filename and one constant bump (EXPECTED_VERSION 22 → 23) in src/jsc/RuntimeTranspilerCache.rs. Everything else is supporting work: dead mirror-struct removal in src/bundler/cache.rs, doc-comment updates in src/ast/transpiler_cache.rs, user-facing docs (.pile.pile2, 50 KB → 4 KB) in docs/runtime/environment-variables.mdx, a thorough new regression test in test/cli/run/transpiler-cache.test.ts that forges a colliding-version .pile entry and asserts it's never served or unlinked, and a filename-filter widening in test/regression/issue/30887.test.ts.

Security risks

None identified. The change narrows what the cache loader will read (only .pile2 files written by this implementation line), which strictly reduces the attack/poisoning surface relative to the status quo. No auth, crypto, or permission logic touched.

Level of scrutiny

Medium. The mechanical change is tiny and obviously correct, but it's a one-way compatibility commitment for the on-disk cache format, and the PR went through a mid-flight design pivot (version-offset namespace → disjoint filename suffix) in response to a human reviewer's "how is this expected to load and be consistent?" question. The author's reply and the in-code EXPECTED_VERSION doc comment explain the new design thoroughly, but the reviewer who prompted the redesign hasn't yet confirmed. That sign-off is the right gate, not mine.

Other factors

  • I left five inline comments across earlier revisions (stale mirror constant, stale module-header doc, stale PR description, stale .pile in docs, missed third "50 kb" instance); all five are addressed and resolved as of 5ee64ad + 69110f9 (autofix table reflow). Nothing remains from my side.
  • CI is red, but the author's triage attributes both failures to repo-wide breakage in this window (musl LTO data-layout mismatch; bunx.test.ts broken by upstream @angular/cli engine constraint) and not to this diff. I find that triage plausible — nothing here touches the linker or bunx — but it's another reason a human should make the merge-over-red call rather than a bot.
  • No CODEOWNERS entries cover the touched paths.
  • The new regression test is well-constructed (sentinel payload, byte-identical-after assertion, third-run cache-hit check) and would have caught the original poisoning.

@robobun robobun force-pushed the farm/ad2450b3/transpiler-cache-version-namespace branch from 69110f9 to 1466ba9 Compare June 5, 2026 06:21

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

All prior nits are resolved and I found nothing new — the runtime change is just the .pile.pile2 suffix plus the version-23 bump. Deferring only because the cross-implementation-line cache-namespace design (and leaving Zig-line .pile files untouched forever) is a maintainer-level call on a path that decides which transpiled bytes get executed.

Extended reasoning...

Overview

This PR separates the Rust-line runtime transpiler cache from the Zig 1.3.x maintenance line by changing the on-disk filename suffix from .pile to .pile2 and bumping EXPECTED_VERSION from 22 to 23. Around that two-line core change it: deletes ~85 lines of provably-dead mirror structs from src/bundler/cache.rs; updates three doc comments in src/ast/transpiler_cache.rs and src/bundler/cache.rs to point at the surviving bun_jsc::runtime_transpiler_cache types; updates docs/runtime/environment-variables.mdx (.pile.pile2, 50 KB → 4 KB threshold in three places); adds a forged-entry regression test in transpiler-cache.test.ts that plants a hash-valid .pile file and asserts it is never loaded, never deleted, and a fresh .pile2 is written alongside; and relaxes the 30887 regression test's filename filter from .endsWith(".pile") to .includes(".pile") so it matches .pile2/.debug.pile2.

Security risks

None introduced. The change strictly reduces cross-trust: foreign .pile entries are no longer opened at all, eliminating the cross-implementation cache-poisoning vector the PR was filed to fix. Cache-directory resolution, hash seeding, and metadata validation are unchanged. The deleted bundler structs were unreferenced (verified in my earlier reviews), so their removal cannot affect any code path.

Level of scrutiny

Moderate-to-high. The mechanical diff is tiny, but the transpiler cache sits on the module-loading hot path and decides which transpiled bytes JSC executes — a wrong answer here is silent miscompilation, which is exactly what the original bug report described. The PR also went through a mid-stream design pivot (version-number offset → disjoint filename suffix) in response to a "how is this expected to load and be consistent?" question, and the chosen answer (.pile2 coexists, .pile is never read/trusted/deleted) is a long-term cross-line compatibility commitment that a maintainer should ratify rather than a bot.

Other factors

I have reviewed this PR across four prior rounds; every comment I raised (dead mirror constant, stale module-doc pointers, stale PR description, .pile in user docs, the third 50 kb mention) is now resolved and visible in the diff. The bug-hunting system found nothing on the current head. CI was reported green for both touched test files across all 15 lanes, with the red lanes being repo-wide breakage (bunx Angular engine check, darwin PTY flake, windows baseline infra) unrelated to this diff. No CODEOWNERS apply to the touched paths. The only open thread is a CodeRabbit prose-style nitpick on a pre-existing docs sentence, which the author reasonably scoped out.

@robobun robobun force-pushed the farm/ad2450b3/transpiler-cache-version-namespace branch from 1466ba9 to 319675f Compare June 21, 2026 00:06
@mintlify

mintlify Bot commented Jun 21, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
bun 🟢 Ready View Preview Jun 21, 2026, 12:08 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@robobun

robobun commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (319675f). One conflict with de73fda resolved: the .debug. suffix gate moved from cfg!(debug_assertions) to bun_core::env::IS_DEBUG; kept that, kept .pile2. All 12 transpiler-cache tests + 5 30887-regression tests pass; the fail-before still serves the forged entry on a pre-fix build.


On consistent data layout (the question I initially misread as being about cross-line thrash):

Yes. Metadata is never #[repr(C)]-cast or memcpy'd to disk — encode()/decode() write/read each field individually in explicit little-endian (write_int_le::<u32/u8/u64>), so struct padding, field order, and host endianness cannot leak into the file. Metadata::SIZE is the 102-byte sum of exactly those writes (4 + 1 + 1 + 12×8), and under debug_assertions Entry::save does an encode→decode→== round-trip on every write. The three payload regions (output, sourcemap, esm_record) sit at byte offsets stored in the header and each carry a wyhash that load() verifies, so a truncated or shifted payload fails InvalidHash/MissingData rather than being mis-parsed. The only platform-dependent bit is the filename hex (input_hash.to_ne_bytes(), native-endian — matching Zig's std.mem.asBytes), which only affects which file is looked up, not its contents, and is uniform across every LE target Bun ships on.

I kept the .pile2 split even though it wasn't what the question was about — it still fixes the actual bug (v20-collision poisoning) and stops 1.3.x and canary unlinking each other's entries when users alternate versions. Happy to drop it for a plain version bump if you'd rather, but the filename split is what makes entries load consistently across both installed bun lines.

@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: 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 `@docs/runtime/environment-variables.mdx`:
- Line 199: The documentation on lines 199, 210, and 228 describing the
BUN_RUNTIME_TRANSPILER_CACHE_PATH behavior incorrectly states "larger than 4 KB"
when the actual runtime behavior is "4 KB or larger". Replace all three
instances of "larger than 4 KB" with "4 KB or larger" to accurately reflect that
files of exactly 4 KB are included in caching, not excluded. This clarifies the
threshold boundary from a greater-than comparison to a greater-than-or-equal
comparison.
🪄 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: 5f73b1bf-1941-418e-84a3-ea1bb118d2ca

📥 Commits

Reviewing files that changed from the base of the PR and between 1466ba9 and 319675f.

📒 Files selected for processing (3)
  • docs/runtime/environment-variables.mdx
  • src/ast/transpiler_cache.rs
  • src/bundler/cache.rs

Comment thread docs/runtime/environment-variables.mdx Outdated

@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 — all prior feedback addressed; the runtime delta is just the .pile.pile2 suffix and the 22 → 23 version bump, with a solid regression test.

Extended reasoning...

Overview

The load-bearing runtime change is two string literals in write_cache_filename (.pile/.debug.pile.pile2/.debug.pile2) plus EXPECTED_VERSION 22 → 23 in src/jsc/RuntimeTranspilerCache.rs, accompanied by a thorough doc comment explaining the Zig/Rust-line cache-poisoning failure mode. Everything else is cleanup that fell out of review iteration on this PR: deletion of the dead RuntimeTranspilerCache{Entry,Metadata} mirror block in src/bundler/cache.rs (verified unreferenced in earlier review rounds), three doc-comment pointer fixes in src/ast/transpiler_cache.rs / src/bundler/cache.rs, the user-facing docs page updated to .pile2 and the correct 4 KB threshold, a new regression test that forges a hash-valid .pile entry and asserts it's never loaded or deleted while a fresh .pile2 is written, and a one-line filter widening in test/regression/issue/30887.test.ts.

Security risks

None. The cache remains content-addressable under the same per-user directories and the same wyhash; only the filename suffix changes. No new inputs are trusted — if anything, strictly fewer on-disk files are read than before (legacy .pile files are now ignored entirely rather than version-checked-and-unlinked).

Level of scrutiny

Moderate code path (every >4 KB module on warm runs goes through this), but the code delta is mechanical: a constant bump and a suffix swap at the single filename-construction site. The conceptual design question (filename namespace vs. version-number offset) was already iterated on mid-PR, settled in 559ef8f/23d298a7, and is exhaustively documented in the EXPECTED_VERSION doc comment. The new test directly exercises the exact poisoning scenario the PR fixes and would fail on a pre-fix build.

Other factors

I left five rounds of inline comments on earlier revisions (dead mirror structs, stale module-header doc, stale PR metadata, stale .pile/50 KB docs, and the missed lowercase "50 kb" table row); every one has been addressed and resolved. The bug-hunting system found nothing on the current revision. CI was green on the touched test files per the earlier triage; remaining red was repo-wide and unrelated. No CODEOWNERS paths match. No outstanding human reviewer comments.

robobun added 6 commits June 30, 2026 02:29
…g space

The Zig 1.3.x maintenance line shares the runtime transpiler cache's
directory, filename scheme, metadata layout, and feature-hash inputs,
but transpiles with a different implementation and bumps its own
expected_version independently (18 in bun-v1.3.12, 20 in
bun-v1.3.13/1.3.14). When both lines use the same number — as happened
for version 20 — each implementation loads entries written by the
other: every stored hash verifies, so a foreign entry is served
indefinitely, never unlinked, and survives version up/downgrades within
the line that trusts it.

Reserve versions >= 1_000_000 for the Rust line so entries written by
the other line always fail the cache_version check, are unlinked, and
get rewritten.
RUNTIME_TRANSPILER_CACHE_VERSION (stale at 20), CacheEncoding,
MetadataModuleType, RuntimeTranspilerCacheEntry and
RuntimeTranspilerCacheMetadata in src/bundler/cache.rs had no
references; the real on-disk Entry/Metadata live in
src/jsc/RuntimeTranspilerCache.rs and reach the parser through the
vtable bridge. Also point the bun_ast::RuntimeTranspilerCache.entry doc
comment at the concrete type that is actually boxed there.
The //! header still pointed readers at bun_bundler::cache for the
concrete Entry/Metadata types deleted in the previous commit; they live
in bun_jsc::runtime_transpiler_cache.
…n offset

A version-number offset still left both implementation lines reading
and writing the same <hash>.pile files: whichever bun ran last unlinked
and rewrote the other line's entries, so alternating bun versions could
never keep a loadable, consistent cache.

Write <hash>.pile2 (debug: .debug.pile2) instead. The two caches are
now fully disjoint: an entry only ever loads in the implementation line
that wrote it, both coexist when installed bun versions alternate, and
.pile files are never read, trusted, or deleted. EXPECTED_VERSION
returns to a plain 23.
The runtime transpiler cache writes .pile2 files now (1.3.x uses
.pile), and the minimum cached source size is 4 KB, not 50 KB
(MINIMUM_CACHE_SIZE in src/jsc/RuntimeTranspilerCache.rs).
@robobun robobun force-pushed the farm/ad2450b3/transpiler-cache-version-namespace branch from 319675f to c18495b Compare June 30, 2026 02:37
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