Skip to content

Stop inlining process.env dot-reads in Worker-thread transpiles - #34211

Open
robobun wants to merge 5 commits into
mainfrom
farm/c9dda7bf/worker-env-inline
Open

Stop inlining process.env dot-reads in Worker-thread transpiles#34211
robobun wants to merge 5 commits into
mainfrom
farm/c9dda7bf/worker-env-inline

Conversation

@robobun

@robobun robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Fixes #34210

Repro

A module over the runtime transpiler cache minimum size, dynamically imported inside a Worker, run twice with different env values and a shared cache:

MY_TEST_IDENTITY=FIRST_VALUE bun main.ts   # DOT: FIRST_VALUE
MY_TEST_IDENTITY=SECOND_VALUE bun main.ts  # DOT: FIRST_VALUE  (stale, run 1's value)

where main.ts does new Worker(...) and the worker dynamically imports a large module that reads process.env.MY_TEST_IDENTITY. The cached .pile entry contains the first run's value as a string literal.

The same inlining also defeats new Worker(file, { env: {} }): the worker's process.env object is the requested empty map (enumeration, in, hasOwnProperty all agree), but a literal process.env.HOME read still returns the launch value because the transpiler substituted it at parse time.

// LAUNCH_SECRET=s3cr3t bun repro.mjs
const w = new Worker(`parentPort.postMessage({
  keys: Object.keys(process.env),
  secret: process.env.LAUNCH_SECRET,
  inSecret: "LAUNCH_SECRET" in process.env,
})`, { eval: true, env: {} });
// bun before: { keys: [], secret: "s3cr3t", inSecret: false }
// node/after: { keys: [], secret: undefined, inSecret: false }

Cause

The main thread sets env.behavior = DotEnvBehavior::LoadAllWithoutInlining before configure_defines() (src/runtime/cli/run_command.rs), and so do the test runner, repl, bake, and standalone paths. The Worker-thread VM startup path (WebWorker::start_vm in src/jsc/web_worker.rs) never did, so the worker's transpiler ran with the Target::Bun default of LoadAll and baked every process.env.X dot-read into the transpiled output as a DotDefine. That output lands in the shared on-disk runtime transpiler cache, which is keyed by content and features only, so every later process with different env values got the first process's values as compile-time constants. And because the define table is built from the worker's env_loader (cloned from the parent's launch environ) rather than the options.env map installed on m_processEnvObject, an explicit env: {} cannot scrub the literal reads.

Fix

Set LoadAllWithoutInlining on the worker VM's transpiler before configure_defines(), matching the main thread. This stops the inlining, stops poisoned cache entries from being written, and lets the plain process.env object installed for an explicit env: option be the sole source of truth for dot reads. The standalone-graph branch right below already set it via apply_standalone_runtime_flags.

Verification

  • test/cli/run/transpiler-cache.test.ts ("does not inline process.env in Worker threads") runs the two-process repro with a shared cache dir; fails on the unfixed build (prints the first run's value), passes with the fix.
  • test/js/node/worker_threads/worker_threads.test.ts ("env: {} scrubs the launch environment from the worker's process.env") spawns a worker with env: { ONLY: "1" } under a process that has LAUNCH_SECRET/NODE_ENV set, and asserts literal process.env.LAUNCH_SECRET / process.env.NODE_ENV reads come back null; fails before ("s3cr3t" / "production"), passes after.
  • Full test/js/node/worker_threads/worker_threads.test.ts (92 tests) and test/cli/run/transpiler-cache.test.ts pass on the debug build.

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:37 AM PT - Jul 25th, 2026

@robobun, your commit b2e3ba0 is building: #80594

@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 — one-line config assignment matching the established pattern, with a regression test.

What was reviewed:

  • Verified the new env.behavior = LoadAllWithoutInlining assignment matches the identical pre-configure_defines() setup in run_command.rs, test_command.rs, repl_command.rs, and bake/production.rs.
  • Checked ordering: set before configure_defines() and before apply_standalone_runtime_flags (which already set it for the standalone-graph branch), so non-standalone workers now get the same behavior.
  • Test mirrors the existing main-thread "does not inline process.env" test; bunRun throws on non-zero exit so a worker crash would fail the test; only big-env.js exceeds the 4 KiB cache threshold so newCacheCount() == 1 holds.
Extended reasoning...

Overview

Five-line addition to src/jsc/web_worker.rs (WebWorker::start_vm) setting transpiler.options.env.behavior = DotEnvBehavior::LoadAllWithoutInlining on the worker VM before configure_defines(), plus an 18-line regression test in test/cli/run/transpiler-cache.test.ts.

Security risks

None. This narrows behavior (stops inlining env values into transpiled output). No new inputs are parsed and no untrusted data is touched; it's a plain enum field assignment.

Level of scrutiny

Low-to-medium. The runtime change is a single field write in the worker VM setup path, following a pattern already used verbatim at five other VM-startup call sites (run_command.rs:1052, test_command.rs:2205, repl_command.rs:124, bake/production.rs:173, bun.js.rs:32). Placement immediately before configure_defines() matches run_command.rs exactly. The standalone-graph branch just below already reached the same state via apply_standalone_runtime_flags, so this only changes the non-standalone worker case — which is precisely the reported bug.

Other factors

  • The regression test is placed directly after the existing main-thread "does not inline process.env" test and uses the same dummyFile/bunRun/newCacheCount machinery, so it inherits proven test infrastructure.
  • bunRun throws on non-zero exit, and workers keep the parent event loop ref'd by default, so the two-process assertion (first then second) is deterministic.
  • The fully-qualified path bun_options_types::schema::api::DotEnvBehavior is used elsewhere in the tree (e.g. jsc_hooks.rs), so it compiles without new imports.
  • PR description states the full transpiler-cache suite and worker.test.ts pass, and that the test fails on the unfixed build.
  • The bug hunting system found no issues.

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. No action items raised; CI is running on the current commit.

@robobun

robobun commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

CI triage for build 73179: the only hard failure is test-net-connect-memleak.js on alpine x64-baseline, which is pre-existing on main and unrelated to this change. The remaining failures (spawn.test.ts timeout on Windows, 30205 napi finalizer leak on asan, repl-close EPIPE on Windows, serve.test.ts on macOS) are known flaky and passed on retry. The new transpiler-cache worker test passed on all lanes. Diff is ready for review.

@Lillious

Copy link
Copy Markdown
Contributor

@robobun Was this issue introduced in the Rust re-write or did it pre-exist in v1.3.14?

@robobun

robobun commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

It pre-existed. The issue was reported and reproduced on v1.3.14, and the Zig code before the rewrite had the same gap: the main-thread setup (src/bun.js.zig) set b.options.env.behavior = .load_all_without_inlining before configureDefines(), but the worker startup path (src/jsc/web_worker.zig) called configureDefines() without it, exactly like the Rust port. So workers have inlined env dot-reads (and written them into the shared transpiler cache) since before the rewrite.

robobun added 2 commits July 25, 2026 04:16
The main thread sets DotEnvBehavior::LoadAllWithoutInlining before
configure_defines(), but the Worker-thread VM startup path never did,
so workers inlined process.env.X dot-reads as string literals. Those
literals were then stored in the shared on-disk runtime transpiler
cache (keyed by content only), so later processes with different env
values executed the first process's values.

Fixes #34210
The worker transpiler inlined every launch-environ var as a process.env.X
define, so a worker spawned with env: {} still returned the real value
from a literal dot read even though enumeration/has reported the key
absent. Covered by the same LoadAllWithoutInlining fix as #34210; this
test exercises the node worker_threads env-option side of it.
@robobun
robobun force-pushed the farm/c9dda7bf/worker-env-inline branch from 39c8302 to 2f1fe79 Compare July 25, 2026 04:43
Comment thread src/jsc/web_worker.rs Outdated
@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main and added a second test in test/js/node/worker_threads/worker_threads.test.ts covering the other user-facing symptom of this same inlining: new Worker(file, { env: {} }) could not scrub the launch environment because literal process.env.X reads were substituted at parse time from the parent's env loader, so the worker saw process.env.LAUNCH_SECRET as the real value even though Object.keys(process.env) and "LAUNCH_SECRET" in process.env agreed it was absent. Both tests fail without the LoadAllWithoutInlining line and pass with it.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3bed38a3-7f52-4dda-bd53-50df9d61b164

📥 Commits

Reviewing files that changed from the base of the PR and between 2f1fe79 and b2e3ba0.

📒 Files selected for processing (4)
  • src/jsc/RuntimeTranspilerCache.rs
  • src/jsc/web_worker.rs
  • test/cli/run/transpiler-cache.test.ts
  • test/js/node/worker_threads/worker_threads.test.ts

Walkthrough

Worker VM startup now disables process.env dot-read inlining. Tests cover transpiler-cache behavior across worker runs with different environments and scrubbing launch-time variables when workers receive explicit environment overrides.

Changes

Worker environment handling

Layer / File(s) Summary
Prevent environment inlining in worker imports
src/jsc/web_worker.rs, test/cli/run/transpiler-cache.test.ts
Worker VM initialization uses LoadAllWithoutInlining, and cache tests verify distinct environment values across worker executions.
Validate explicit worker environment overrides
test/js/node/worker_threads/worker_threads.test.ts
Worker tests verify explicit environment overrides exclude launch-time variables and preserve the provided key.

Possibly related PRs

  • oven-sh/bun#35270: Updates the worker startup path with the same environment non-inlining behavior.

Suggested reviewers: cirospaciari, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: stopping process.env dot-read inlining in Worker-thread transpiles.
Description check ✅ Passed The description includes the bug repro, cause, fix, and verification, covering the repo's required intent and testing detail.
Linked Issues check ✅ Passed The code fixes the Worker-thread env inlining bug from #34210 and adds regression tests for cache and env scrubbing behavior.
Out of Scope Changes check ✅ Passed The changes stay focused on the Worker transpile env fix and related tests, with no clear unrelated additions.

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 `@test/cli/run/transpiler-cache.test.ts`:
- Around line 239-240: Remove the explanatory comment at
test/cli/run/transpiler-cache.test.ts lines 239-240, leaving the existing issue
URL comment as the sole regression-test comment. In
test/js/node/worker_threads/worker_threads.test.ts lines 1423-1426, replace the
prose comment with only the issue `#34210` URL comment.
- Around line 241-242: Strengthen the second-run assertions in the transpiler
cache test by capturing newCacheCount() after invoking bunRun for the “second”
worker and asserting it remains unchanged from the first run. Keep the existing
stdout assertion, and ensure the test verifies reuse of the shared cached module
rather than merely successful execution.
🪄 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: 95122bac-ab6d-4e61-a6d8-74de064b5e37

📥 Commits

Reviewing files that changed from the base of the PR and between df6c7ee and 2f1fe79.

📒 Files selected for processing (3)
  • src/jsc/web_worker.rs
  • test/cli/run/transpiler-cache.test.ts
  • test/js/node/worker_threads/worker_threads.test.ts

Comment thread test/cli/run/transpiler-cache.test.ts Outdated
Comment thread test/cli/run/transpiler-cache.test.ts
Comment thread src/jsc/web_worker.rs
Entries written by a pre-fix worker carry the writing process's env
values as string literals, and neither input_hash nor features_hash
changes with env.behavior, so a cache hit on an old entry reinstates
the bug. Also assert the second worker run reuses the cache entry, and
trim test comments.
Comment thread src/jsc/RuntimeTranspilerCache.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 — the EXPECTED_VERSION bump to 24 addresses the stale-cache concern from my earlier review; poisoned pre-fix .pile entries are now rejected on load. The comment-cop flag on RuntimeTranspilerCache.rs:48 is a false positive: the Version 24 history entry is the same 3-line shape as Version 22 (which is what I asked for), not a workaround justification.

Extended reasoning...

Overview

Two production lines and a constant bump: WebWorker::start_vm now sets b.options.env.behavior = LoadAllWithoutInlining before configure_defines(), matching run_command.rs:1044, test_command.rs:2323, repl_command.rs:124, and bake/production.rs:172. EXPECTED_VERSION in RuntimeTranspilerCache.rs is bumped 23→24 with a history entry so pre-fix cache entries (which baked env values as string literals) are rejected. Two regression tests cover both user-visible symptoms of #34210: cross-process cache poisoning and env: {} failing to scrub literal process.env.X reads.

Security risks

None introduced. The change removes a data leak: a worker with env: {} previously exposed the parent's launch environment via inlined dot-reads even though enumeration/in reported the key absent. No new attack surface.

Level of scrutiny

Low-to-medium. The fix is a one-line behavior flag matching five sibling call sites, placed at the same point in the setup sequence (after resolver.env_loader is wired, before configure_defines()). The standalone-graph branch immediately below already set the same value via apply_standalone_runtime_flags (bun.js.rs:32), so ordering is correct. The version bump is the standard mechanism for invalidating semantically-stale cache entries.

Other factors

  • My prior review's only finding (cache version not bumped → poisoned entries survive upgrade) is fully addressed in 7bca36c.
  • CodeRabbit's asks (issue-URL-only regression comments; second newCacheCount() assertion proving cache reuse) are both applied.
  • The outstanding comment-cop bot flag on the Version 24 history comment is a heuristic false positive — it matches the Version 22 entry's shape exactly and is a version log, not a workaround justification.
  • CI failures on build #80224 (mysql tests, macOS/musl build-bun infra) are unrelated to this diff.
  • Tests follow harness conventions: bunEnv spread, concurrent pipe drain, stdout asserted before exitCode, await using for the spawn.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks. All review findings are addressed: env inlining disabled for worker transpiles, cache version bumped to 24 so pre-fix entries are rejected, and the test assertions tightened. No open threads remain; ready for a maintainer.

@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 — the cache-version bump addresses my earlier finding.

What was reviewed:

  • Confirmed the new LoadAllWithoutInlining assignment mirrors run_command.rs / test_command.rs / repl_command.rs / bake, and that the standalone-graph branch below may override it to disable (unchanged behavior).
  • EXPECTED_VERSION 23→24 with a history entry matching the Version 22 format; rejects pre-fix .pile entries as intended.
  • Both new tests follow the existing harness patterns; the transpiler-cache test now asserts newCacheCount() === 0 on the second run so cache reuse is proven.
Extended reasoning...

Overview

Four files: a one-line behavior flag in src/jsc/web_worker.rs (set env.behavior = LoadAllWithoutInlining on the worker VM's transpiler before configure_defines()), a cache-format version bump 23→24 in src/jsc/RuntimeTranspilerCache.rs, and two regression tests in test/cli/run/transpiler-cache.test.ts and test/js/node/worker_threads/worker_threads.test.ts.

Prior review

I previously flagged that without a version bump, poisoned .pile entries written by pre-fix builds would still be accepted (the cache key is (input_hash, features_hash, EXPECTED_VERSION) and env.behavior participates in none of those). That was addressed in commit 7bca36c by bumping EXPECTED_VERSION to 24 with a changelog entry in the same shape as Version 22. CodeRabbit's two nits (regression-test comment style, second newCacheCount() assertion) and the comment-cop bot's length complaint are also addressed in the current diff.

Security risks

None. The change removes an env-value leak vector (a worker with env: {} could previously read the parent's launch environment via literal process.env.X because the value was baked in at transpile time). No new attack surface.

Level of scrutiny

Low-to-moderate. The runtime change is a single enum assignment that brings the worker VM startup path in line with every other runtime entry point (run_command.rs:1044, test_command.rs:2323, repl_command.rs:124, bake/production.rs:172) — grepped and confirmed. The standalone-graph branch immediately below may override it to disable (via apply_standalone_runtime_flags), which is unchanged and correct. The cache version bump is the established mechanism for invalidating semantically-stale entries.

Other factors

Tests follow file-local conventions exactly: the transpiler-cache test mirrors the adjacent "does not inline process.env" test but routes through a Worker, and asserts newCacheCount() on both runs so a hypothetical "workers don't cache at all" regression would fail it. The worker_threads test uses bunExe/bunEnv, await using proc, concurrent stdout/stderr/exited drain, and asserts stderr/stdout before exitCode. CI failures on the latest build (mysql integration tests, macOS/musl/freebsd "step failed outside runner") are unrelated infrastructure failures.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Build 80594 failed on CI infrastructure: every build-bun job timed out or expired before compiling, so no tests ran (no failure annotations). The change itself is unaffected; the previous run's only test failures were known-flaky or pre-existing on main, and both test files pass locally. Needs a CI retrigger or a maintainer review.

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.

Runtime transpiler cache inlines process.env dot-reads during Worker-thread imports — later processes execute the first process's env values

2 participants