Skip to content

windows: heap-allocate bunx fast-path environment block - #32644

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/7a701c79/windows-env-block-heap
Jun 23, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
farm/7a701c79/windows-env-block-heap

Conversation

@robobun

@robobun robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

What

write_windows_env_block (used by the Windows .bunx fast path in bun run / bunx) wrote the process environment into a fixed [u16; 32767] buffer. That 32,767-character limit belongs to CreateProcessA (ANSI); CreateProcessW with CREATE_UNICODE_ENVIRONMENT has no documented block-size limit, and CI environments routinely exceed 32 KB of env data.

This changes write_windows_env_block to size a Vec<u16> to the actual environment contents (UTF-8 byte length is a safe upper bound on UTF-16 code units) so the fast path works regardless of environment size. The now-unused 64 KB static scratch buffer and the error-handling branch in the caller are removed.

Background (BUN-3MAQ)

Sentry BUN-3MAQ reports the Zig-era panic in bun 1.3.14:

panic: index out of bounds: index 288019, len 32745
  dotenv/env_loader.zig:1283  writeWindowsEnvBlock
  string/immutable/unicode.zig:1547  convertUTF8toUTF16InBuffer

where a ~280 KB environment value overran the fixed buffer because the bounds check happened after the write. That crash was already prevented on main by the length pre-checks added in #30722: instead of overrunning, the Rust port returns TooManyEnvironmentVariables, the fast path bails, and run_binary falls through to the libuv uv_spawn slow path which heap-allocates the env block. So there is no crash on current main; this PR removes the remaining artificial cap so the fast path doesn't have to bail.

Gate note

The changed code is #[cfg(windows)], the new test is it.if(isWindows), and on current main (where #30722 already landed) a large environment silently falls back to the slow path rather than failing. So there is no fail-before to observe on the Linux gate; the Windows CI lane exercises the new test.

Verification

  • cargo check -p bun_dotenv -p bun_runtime --target x86_64-pc-windows-msvc
  • cargo check -p bun_dotenv -p bun_runtime (host)
  • bun bd test test/cli/install/bun-run.test.ts (291 pass, 1 skip on Linux)

The .bunx fast path on Windows wrote the process environment into a
fixed [u16; 32767] buffer. That limit belongs to CreateProcessA (ANSI);
CreateProcessW with CREATE_UNICODE_ENVIRONMENT has no documented block
size limit, and CI environments routinely exceed 32 KB of env data.

The panic reported in BUN-3MAQ (index out of bounds writing past the
buffer) was already prevented on main by the length pre-checks added in
#30722, which made the fast path bail and fall through to the libuv
slow path instead of crashing. This change removes the cap entirely:
write_windows_env_block now sizes a Vec<u16> to the actual contents so
the fast path works regardless of environment size, and the now-unused
64 KB static scratch buffer and error branch in the caller are removed.
@robobun

robobun commented Jun 23, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:34 AM PT - Jun 23rd, 2026

✅ @robobun, your commit ada400abcf12a4e4427b739719fa05023d781020 passed in Build #64244! 🎉


🧪   To try this PR locally:

bunx bun-pr 32644

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

bun-32644 --bun

@coderabbitai

coderabbitai Bot commented Jun 23, 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: 52741c60-39bd-4c91-bc88-c28ab75c3008

📥 Commits

Reviewing files that changed from the base of the PR and between 010361c and ada400a.

📒 Files selected for processing (1)
  • test/cli/install/bun-run.test.ts

Walkthrough

write_windows_env_block is rewritten to return a heap-allocated Vec<u16> computed from actual contents, removing the fixed 32,767-unit buffer parameter and error return. The global ENVIRONMENT_BUFFER scratch storage is deleted from the bunx fast-path, and the callsite is updated to use env_block.as_ptr(). A Windows-only test validates the path with an oversized environment block.

Changes

Heap-allocated Windows environment block

Layer / File(s) Summary
write_windows_env_block API rewrite and callsite update
src/dotenv/env_loader.rs, src/runtime/cli/run_command.rs
write_windows_env_block now computes UTF-16 capacity up front and returns a Vec<u16> with a four-NUL terminator, removing the &mut [u16; 32767] parameter and Result return. The global ENVIRONMENT_BUFFER scratch cell is removed from bunx_fast_path_buffers, and BunXFastPath::try_launch calls the new method and passes env_block.as_ptr() as environment in FromBunRunContext.
Windows oversized env block test
test/cli/install/bun-run.test.ts
Adds a Windows-only test that installs a fixture bin package, asserts the .bunx wrapper exists, runs the bin with two 25,000-character env vars, and checks stdout equals combined lengths with no stderr and exit code 0.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: heap-allocating the Windows bunx fast-path environment block instead of using a fixed buffer.
Description check ✅ Passed The description comprehensively covers both required sections with detailed explanations of what was changed, why it was necessary, and how it was verified.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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

Comment thread test/cli/install/bun-run.test.ts

@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 test-comment update in ada400a.

Extended reasoning...

Overview

Rewrites Map::write_windows_env_block (src/dotenv/env_loader.rs) to return a heap-allocated Vec<u16> sized to the actual environment contents instead of writing into a fixed [u16; 32767] and returning Result<*const u16, Error>. The single caller in BunXFastPath::try_launch (src/runtime/cli/run_command.rs) is updated to hold the Vec as a local and pass env_block.as_ptr() into FromBunRunContext.environment; the now-dead ENVIRONMENT_BUFFER static and the Err(_) => return fallback branch are removed. A Windows-only integration test is added to bun-run.test.ts.

Correctness

  • Capacity bound: 4 + Σ(key_bytes + 1 + value_bytes + 1) is a valid upper bound on UTF-16 code units because UTF-8 byte count ≥ UTF-16 code-unit count for every codepoint (1→1, 2→1, 3→1, 4→2). The buffer is zero-initialized via vec![0u16; capacity], so truncate(i + 4) leaves four real NUL terminators. The empty-map case yields [0,0,0,0], identical to the old code.
  • Pointer lifetime: env_block is a local that stays in scope across try_startup_from_bun_js(run_ctx) (run_command.rs:4089), which is where CreateProcessW is called synchronously and copies lpEnvironment before returning. The Vec is dropped only after that call returns, so no UAF.
  • Single caller: grep confirms write_windows_env_block has exactly one Rust call site, so the signature change is fully covered.

Security risks

None. This is buffer-sizing for an env block handed to CreateProcessW; no parsing, no auth, no untrusted input shape change. The data flowing through is the same process environment that was already being serialized.

Level of scrutiny

Low–medium. Windows-only (#[cfg(windows)]) fast-path code with a safe slow-path fallback that previously absorbed the failure case being removed here. The change is mechanical (fixed buffer → sized Vec) and mirrors what create_null_delimited_env_map already does for POSIX. No CODEOWNERS entry for the touched paths.

Other factors

My earlier nit about the test comment (it reads as if it asserts the fast path specifically, when it actually guards the user-visible contract) was addressed in ada400a — the comment now explicitly states it does not assert which spawn path was taken. The inline thread is resolved. No other reviewer comments are outstanding; CodeRabbit had no actionable findings; the bug-hunting system found nothing.

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

Thanks for the comment update — that addresses my earlier nit. I'd still like a human to glance at this since it rewires the Windows .bunx fast-path env-block construction (heap Vec → raw ptr into CreateProcessW) and drops the slow-path fallback branch; also worth confirming the musl build-bun failures on #64238 are unrelated infra before merging.

Extended reasoning...

Overview

This PR rewrites Map::write_windows_env_block in src/dotenv/env_loader.rs to return a heap-allocated Vec<u16> sized to the actual environment contents (UTF-8 byte length is a provably-safe upper bound on UTF-16 code units), instead of writing into a fixed [u16; 32767] and returning Err(TooManyEnvironmentVariables) on overflow. The sole caller in src/runtime/cli/run_command.rs (BunXFastPath::try_launch) is updated to drop the static ENVIRONMENT_BUFFER scratch cell and the Err(_) => { close handle; return } slow-path fallback, passing env_block.as_ptr() directly into FromBunRunContext.environment. A Windows-only integration test in test/cli/install/bun-run.test.ts exercises a ~50 KB env block through a .bunx bin.

I verified there is exactly one caller of write_windows_env_block and no remaining references to ENVIRONMENT_BUFFER. The env_block: Vec<u16> local outlives the synchronous try_startup_from_bun_js → launcher → CreateProcessW call (consumed at bun_shim_impl.rs:1381 as lpEnvironment), so the raw-pointer lifetime is sound. The capacity computation (4 + Σ(key_bytes + 1 + value_bytes + 1)) is a correct upper bound and truncate(i + 4) cannot exceed it, so the four zero-initialized trailing NULs are always present.

Security risks

None identified. The change relaxes an artificial size cap on the environment block passed to CreateProcessW with CREATE_UNICODE_ENVIRONMENT; the env contents themselves are unchanged (same iterator over self.map, same UTF-8→UTF-16 converter). No new untrusted input is introduced.

Level of scrutiny

Medium. This is a small, well-reasoned change, but it sits directly on the Windows process-spawn fast path and removes a defensive fallback (Err(_) => return → slow path). Post-PR, any future regression in env-block construction would surface as a fast-path failure rather than a silent fallthrough, which is arguably better but is a behavioral shift a maintainer should sign off on. The raw-pointer-into-local-Vec across the shim boundary is correct but is exactly the sort of thing worth a second pair of eyes.

Other factors

  • My earlier nit (test comment implied it was exercising the fast path specifically; on the parent commit it passes via slow-path fallthrough) was addressed in ada400a — the comment now states it guards the user-visible contract and does not assert which spawn path was taken. That thread is resolved.
  • robobun reports musl (x64-musl, x64-musl-baseline, aarch64-musl) build-bun failures on Build #64238 for commit 010361c. write_windows_env_block is not #[cfg(windows)]-gated so it does compile on Linux, but the author reports cargo check passes on host, and the failures are in scripts/build/ci.ts (build infra) rather than test failures — likely unrelated, but worth a human confirming before merge.
  • No CODEOWNERS entries match the touched paths.

@robobun

robobun commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Re the musl build-bun failures on #64238: that build was superseded by #64244 (for ada400a), where linux-x64-musl-baseline-build-bun passes and the other musl lanes are green so far. write_windows_env_block compiles on all targets (verified via cargo check host + x86_64-pc-windows-msvc); the earlier musl failures were in scripts/build/ci.ts, not a compile error in the diff.

All three Windows test lanes pass on #64244. test/cli/install/bun-run.test.ts ran 408 tests on windows-2019-x64 with 0 failures, so the new large-env test exercised the rewritten fast path end to end.

@Jarred-Sumner
Jarred-Sumner merged commit 1bd44db into main Jun 23, 2026
77 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/7a701c79/windows-env-block-heap branch June 23, 2026 21:06
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