Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 34 additions & 34 deletions src/dotenv/env_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1395,44 +1395,44 @@ impl Map {
})
}

/// Write the Windows environment block into a buffer
/// This can be passed to CreateProcessW's lpEnvironment parameter
pub fn write_windows_env_block(
&mut self,
result: &mut [u16; 32767],
) -> Result<*const u16, bun_core::Error> {
let mut i: usize = 0;
let mut it = self.map.iterator();
while let Some(pair) = it.next() {
if i + pair.key_ptr.len() + 7 >= result.len() {
return Err(bun_core::Error::from_name("TooManyEnvironmentVariables"));
}
i += strings::convert_utf8_to_utf16_in_buffer(&mut result[i..], pair.key_ptr).len();
if i + 7 >= result.len() {
return Err(bun_core::Error::from_name("TooManyEnvironmentVariables"));
}
result[i] = b'=' as u16;
i += 1;
if i + pair.value_ptr.value.len() + 5 >= result.len() {
return Err(bun_core::Error::from_name("TooManyEnvironmentVariables"));
/// Build a heap-allocated Windows environment block suitable for
/// `CreateProcessW`'s `lpEnvironment` with `CREATE_UNICODE_ENVIRONMENT`.
///
/// The 32,767-character limit applies to `CreateProcessA` (ANSI) only; the
/// Unicode block has no documented size limit, so this sizes the buffer to
/// the actual contents instead of failing when the environment is large.
pub fn write_windows_env_block(&mut self) -> Vec<u16> {
// UTF-16 output is at most one code unit per UTF-8 input byte (ASCII
// is 1:1; multi-byte sequences shrink; surrogate pairs are 2 units
// from 4 bytes), so the UTF-8 byte length is a safe upper bound.
let mut capacity: usize = 4;
{
let mut it = self.map.iterator();
while let Some(pair) = it.next() {
capacity += pair.key_ptr.len() + 1 + pair.value_ptr.value.len() + 1;
}
i += strings::convert_utf8_to_utf16_in_buffer(&mut result[i..], &pair.value_ptr.value)
}

let mut result = vec![0u16; capacity];
let mut i: usize = 0;
{
let mut it = self.map.iterator();
while let Some(pair) = it.next() {
i += strings::convert_utf8_to_utf16_in_buffer(&mut result[i..], pair.key_ptr).len();
result[i] = b'=' as u16;
i += 1;
i += strings::convert_utf8_to_utf16_in_buffer(
&mut result[i..],
&pair.value_ptr.value,
)
.len();
if i + 5 >= result.len() {
return Err(bun_core::Error::from_name("TooManyEnvironmentVariables"));
result[i] = 0;
i += 1;
}
result[i] = 0;
i += 1;
}
result[i] = 0;
i += 1;
result[i] = 0;
i += 1;
result[i] = 0;
i += 1;
result[i] = 0;

Ok(result.as_ptr())
// Terminator: four trailing NUL u16s (already zero-initialized above).
result.truncate(i + 4);
result
}

pub fn iterator(&mut self) -> <HashTable as ArrayHashMapExt>::Iterator<'_> {
Expand Down
19 changes: 2 additions & 17 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3930,9 +3930,6 @@ mod bunx_fast_path_buffers {
// buffers (bunx fast-path runs once on the main thread) → RacyCell.
pub(super) static DIRECT_LAUNCH_BUFFER: bun_core::RacyCell<WPathBuffer> =
bun_core::RacyCell::new(WPathBuffer::ZEROED);
// Same `[PATH_MAX_WIDE]u16` shape as the launch buffer.
pub(super) static ENVIRONMENT_BUFFER: bun_core::RacyCell<WPathBuffer> =
bun_core::RacyCell::new(WPathBuffer::ZEROED);
}

impl BunXFastPath {
Expand Down Expand Up @@ -4069,19 +4066,7 @@ impl BunXFastPath {
// contract is that *this* `passthrough` wins.
ctx.passthrough = passthrough.to_vec();

// SAFETY: process-lifetime static, single-threaded CLI dispatch.
let env_buf = unsafe { &mut *bunx_fast_path_buffers::ENVIRONMENT_BUFFER.get() };
let environment = match env.map.write_windows_env_block(&mut env_buf.0) {
Ok(env) => Some(env),
Err(_) => {
// The shim's `NtClose(metadata_handle)` only
// runs if `try_startup_from_bun_js` is reached. Close it
// explicitly so the slow-path fallback doesn't inherit a
// dangling open HANDLE for the process lifetime.
Fd::from_native(handle as u64).close();
return;
}
};
let env_block = env.map.write_windows_env_block();

let run_ctx = bun_install::windows_shim::bun_shim_impl::FromBunRunContext {
handle,
Expand All @@ -4092,7 +4077,7 @@ impl BunXFastPath {
force_use_bun: ctx.debug.run_in_bun,
direct_launch_with_bun_js: Self::direct_launch_callback,
cli_context: ::core::ptr::from_mut(ctx),
environment,
environment: Some(env_block.as_ptr()),
};

bun_core::scoped_log!(
Expand Down
62 changes: 62 additions & 0 deletions test/cli/install/bun-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,4 +1006,66 @@ describe.concurrent("bun run", () => {
expect(exitCode).toBe(1);
});
});

// BUN-3MAQ: guards the user-visible contract that a >32,767-u16 environment
// block reaches a .bunx child intact. CreateProcessW with
// CREATE_UNICODE_ENVIRONMENT has no documented block-size limit. This does
// not assert which spawn path (fast .bunx shim vs. libuv fallback) was
// taken, since both are correct; it fails only if the child loses env data
// or the process crashes. POSIX hits E2BIG first, so Windows-only.
it.if(isWindows)(
"runs a node_modules/.bin entry with an environment block larger than 32,767 wide chars",
async () => {
Comment thread
robobun marked this conversation as resolved.
using dir = tempDir("bun-run-large-env", {
"package.json": JSON.stringify({
name: "consumer",
version: "0.0.0",
dependencies: { "print-env-len": "file:./print-env-len" },
}),
"print-env-len": {
"package.json": JSON.stringify({
name: "print-env-len",
version: "0.0.0",
bin: { "print-env-len": "./bin.js" },
}),
"bin.js": `#!/usr/bin/env node\nprocess.stdout.write(String((process.env.HUGE_A || "").length + (process.env.HUGE_B || "").length));\n`,
},
});

{
await using install = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([
install.stdout.text(),
install.stderr.text(),
install.exited,
]);
expect({ stdout, stderr, exitCode }).toMatchObject({ exitCode: 0 });
}

expect(await Bun.file(join(String(dir), "node_modules", ".bin", "print-env-len.bunx")).exists()).toBe(true);

// Two 25,000-char values plus the rest of bunEnv push the serialized
// block past 32,767 u16s without approaching any per-variable or
// per-block ceiling Windows actually enforces.
const chunk = Buffer.alloc(25_000, "a").toString();
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "print-env-len"],
cwd: String(dir),
env: { ...bunEnv, HUGE_A: chunk, HUGE_B: chunk },
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

expect(stderr).not.toContain("error");
expect(stdout).toBe(String(chunk.length * 2));
expect(exitCode).toBe(0);
},
);
});
Loading