fix(windows): avoid standalone worker dotenv crash (#27434)

### What does this PR do?

Fixes #27431.

- fixes a Windows standalone executable crash when
`compile.autoloadDotenv = false`, a `.env` file exists in the runtime
cwd, and the executable spawns a `Worker`
- gives worker startup its own cloned `DotEnv.Loader` before
`configureDefines()`, so dotenv loading does not mutate env state owned
by another thread
- aligns worker startup with other Bun runtime paths by wiring
`resolver.env_loader = transpiler.env`
- extracts standalone runtime flag propagation into
`applyStandaloneRuntimeFlags(...)` so main and worker startup share the
same env/tsconfig/package.json behavior
- adds regression coverage in `test/regression/issue/27431.test.ts` and
bundler coverage in `test/bundler/bundler_compile_autoload.test.ts`

### How did you verify your code works?

- reproduced the original crash with `bun test
regression/issue/27431.test.ts` on stock `1.3.10-canary.104`; the test
fails on unpatched Bun
- rebuilt `build/debug/bun-debug.exe` with this patch and ran
`build/debug/bun-debug.exe test regression/issue/27431.test.ts`; the
test passes on the patched build
- manually validated the minimal repro from
`https://github.com/Hona/bun1310-minimal-repro` against the patched
`bun-debug.exe`; the standalone executable no longer crashes and still
keeps dotenv disabled (`process.env` does not pick up `.env`)
This commit is contained in:
Luke Parker
2026-02-26 13:49:56 +10:00
committed by GitHub
parent 89c70a76e8
commit 84e4a5ce9c
4 changed files with 112 additions and 24 deletions

View File

@@ -3,6 +3,17 @@ pub const webcore = @import("./bun.js/webcore.zig");
pub const api = @import("./bun.js/api.zig");
pub const bindgen = @import("./bun.js/bindgen.zig");
pub fn applyStandaloneRuntimeFlags(b: *bun.Transpiler, graph: *const bun.StandaloneModuleGraph) void {
b.options.env.disable_default_env_files = graph.flags.disable_default_env_files;
b.options.env.behavior = if (graph.flags.disable_default_env_files)
.disable
else
.load_all_without_inlining;
b.resolver.opts.load_tsconfig_json = !graph.flags.disable_autoload_tsconfig;
b.resolver.opts.load_package_json = !graph.flags.disable_autoload_package_json;
}
pub const Run = struct {
ctx: Command.Context,
vm: *VirtualMachine,
@@ -82,18 +93,7 @@ pub const Run = struct {
.unspecified => {},
}
// If .env loading is disabled, only load process env vars
// Otherwise, load all .env files
if (graph_ptr.flags.disable_default_env_files) {
b.options.env.behavior = .disable;
} else {
b.options.env.behavior = .load_all_without_inlining;
}
// Control loading of tsconfig.json and package.json at runtime
// By default, these are disabled for standalone executables
b.resolver.opts.load_tsconfig_json = !graph_ptr.flags.disable_autoload_tsconfig;
b.resolver.opts.load_package_json = !graph_ptr.flags.disable_autoload_package_json;
applyStandaloneRuntimeFlags(b, graph_ptr);
b.configureDefines() catch {
failWithBuildError(vm);

View File

@@ -325,16 +325,30 @@ pub fn start(
}
this.arena = bun.MimallocArena.init();
const allocator = this.arena.?.allocator();
const map = try allocator.create(bun.DotEnv.Map);
map.* = try this.parent.transpiler.env.map.cloneWithAllocator(allocator);
const loader = try allocator.create(bun.DotEnv.Loader);
loader.* = bun.DotEnv.Loader.init(map, allocator);
var vm = try jsc.VirtualMachine.initWorker(this, .{
.allocator = this.arena.?.allocator(),
.allocator = allocator,
.args = transform_options,
.env_loader = loader,
.store_fd = this.store_fd,
.graph = this.parent.standalone_module_graph,
});
vm.allocator = this.arena.?.allocator();
vm.allocator = allocator;
vm.arena = &this.arena.?;
var b = &vm.transpiler;
b.resolver.env_loader = b.env;
if (this.parent.standalone_module_graph) |graph| {
bun.bun_js.applyStandaloneRuntimeFlags(b, graph);
}
b.configureDefines() catch {
this.flushLogs();
@@ -342,16 +356,6 @@ pub fn start(
return;
};
// TODO: we may have to clone other parts of vm state. this will be more
// important when implementing vm.deinit()
const map = try vm.allocator.create(bun.DotEnv.Map);
map.* = try vm.transpiler.env.map.cloneWithAllocator(vm.allocator);
const loader = try vm.allocator.create(bun.DotEnv.Loader);
loader.* = bun.DotEnv.Loader.init(map, vm.allocator);
vm.transpiler.env = loader;
vm.loadExtraEnvAndSourceCodePrinter();
vm.is_main_thread = false;
jsc.VirtualMachine.is_main_thread_vm = false;

View File

@@ -168,6 +168,40 @@ console.log("PRELOAD");
},
});
// Regression test: standalone workers must not load .env when autoloadDotenv is disabled
itBundled("compile/AutoloadDotenvDisabledWorkerCLI", {
compile: {
autoloadDotenv: false,
},
backend: "cli",
files: {
"/entry.ts": /* js */ `
import { rmSync } from "fs";
rmSync("./worker.ts", { force: true });
const worker = new Worker("./worker.ts");
console.log(await new Promise(resolve => {
worker.onmessage = event => resolve(event.data);
}));
worker.terminate();
`,
"/worker.ts": /* js */ `
postMessage(process.env.TEST_VAR || "not found");
`,
},
entryPointsRaw: ["./entry.ts", "./worker.ts"],
outfile: "dist/out",
runtimeFiles: {
"/.env": `TEST_VAR=from_dotenv`,
},
run: {
stdout: "not found",
file: "dist/out",
setCwd: true,
},
});
// Test CLI backend with autoloadDotenv: true
itBundled("compile/AutoloadDotenvEnabledCLI", {
compile: {

View File

@@ -0,0 +1,50 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
import { join } from "path";
test.if(isWindows)("standalone worker does not crash when autoloadDotenv is disabled and .env exists", async () => {
const target = process.arch === "arm64" ? "bun-windows-aarch64" : "bun-windows-x64";
using dir = tempDir("issue-27431", {
".env": "TEST_VAR=from_dotenv\n",
"entry.ts": 'console.log(process.env.TEST_VAR || "not found")\nnew Worker("./worker.ts")\n',
"worker.ts": "",
"build.ts": `
await Bun.build({
entrypoints: ["./entry.ts", "./worker.ts"],
compile: {
autoloadDotenv: false,
target: "${target}",
outfile: "./app.exe",
},
});
`,
});
await using build = Bun.spawn({
cmd: [bunExe(), join(String(dir), "build.ts")],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [, buildStderr, buildExitCode] = await Promise.all([build.stdout.text(), build.stderr.text(), build.exited]);
expect(buildExitCode).toBe(0);
expect(buildStderr).toBe("");
await using proc = Bun.spawn({
cmd: [join(String(dir), "app.exe")],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("not found");
expect(exitCode).toBe(0);
expect(stderr).toBe("");
});