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
7 changes: 6 additions & 1 deletion src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3955,6 +3955,11 @@ mod windows_impl {
// CRT-fd-backed `Fd` and ignores the directory/nofollow flags.
super::openat_windows_a(dir, path.as_bytes(), flags, mode)
}
/// `DuplicateHandle` with `bInheritHandle = FALSE`, the equivalent of the
/// POSIX arm's `F_DUPFD_CLOEXEC`: libuv spawns children with
/// `bInheritHandles = TRUE`, so an inheritable duplicate would leak into
/// every process spawned while it is open. Stdio handed to a child is
/// duplicated again (inheritable) by libuv itself, so nothing needs it.
pub fn dup(fd: Fd) -> Maybe<Fd> {
// DuplicateHandle on the underlying HANDLE.
let process = w::kernel32::GetCurrentProcess();
Expand All @@ -3966,7 +3971,7 @@ mod windows_impl {
process,
&mut target,
0,
w::TRUE,
w::FALSE,
w::DUPLICATE_SAME_ACCESS,
)
};
Expand Down
80 changes: 80 additions & 0 deletions test/js/bun/spawn/spawn.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ArrayBufferSink, readableStreamToText, spawn, spawnSync } from "bun";
import { dlopen } from "bun:ffi";
import { beforeAll, describe, expect, it } from "bun:test";
import {
gcTick as _gcTick,
Expand All @@ -12,6 +13,7 @@ import {
isPosix,
isWindows,
shellExe,
tempDir,
tmpdirSync,
withoutAggressiveGC,
} from "harness";
Expand Down Expand Up @@ -1312,6 +1314,84 @@ it.skipIf(isWindows)("leaves a Bun.file(fd) stdout open when stdin stream setup
expect(exitCode).toBe(0);
});

// Bun.file(fd).stream() (like the shell's stdio and cwd handles) works on a
// dup() of the descriptor. On Windows that duplicate used to be created
// inheritable, and libuv spawns with bInheritHandles=TRUE, so every child
// started while one was open got a copy and kept the file open after the
// parent closed it. POSIX dup() uses F_DUPFD_CLOEXEC; the Windows side must match.
it.if(isWindows)("handles duplicated for Bun.file(fd).stream() are not inherited by children", async () => {
const N = 64;
// Bigger than the stream's high-water mark, so each reader parks on its
// duplicate instead of reading to EOF and closing it.
using dir = tempDir("spawn-dup-inherit", { "data.bin": Buffer.alloc(1024 * 1024) });

const k32 = dlopen("kernel32.dll", {
GetCurrentProcess: { args: [], returns: "ptr" },
GetProcessHandleCount: { args: ["ptr", "ptr"], returns: "i32" },
});
const ownHandleCount = () => {
const out = new Uint32Array(1);
if (k32.symbols.GetProcessHandleCount(k32.symbols.GetCurrentProcess(), out) === 0) {
throw new Error("GetProcessHandleCount failed");
}
return out[0];
};

// The child reports how many handles it was started with.
const spawnHandleCounter = () =>
spawn({
cmd: [
bunExe(),
"-e",
`
import { dlopen } from "bun:ffi";
const k32 = dlopen("kernel32.dll", {
GetCurrentProcess: { args: [], returns: "ptr" },
GetProcessHandleCount: { args: ["ptr", "ptr"], returns: "i32" },
});
const out = new Uint32Array(1);
if (k32.symbols.GetProcessHandleCount(k32.symbols.GetCurrentProcess(), out) === 0) {
throw new Error("GetProcessHandleCount failed");
}
console.log(out[0]);
`,
],
env: bunEnv,
stdio: ["ignore", "pipe", "pipe"],
});
const reportedHandleCount = async (proc: ReturnType<typeof spawnHandleCounter>) => {
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(exitCode).toBe(0);
return Number(stdout.trim());
};

const fds = Array.from({ length: N }, () => openSync(join(String(dir), "data.bin"), "r"));
const readers: ReadableStreamDefaultReader<Uint8Array>[] = [];
try {
// Plain descriptors are already non-inheritable; this child is the baseline.
await using control = spawnHandleCounter();

const before = ownHandleCount();
for (const fd of fds) readers.push(Bun.file(fd).stream().getReader());
// getReader() starts the stream, which dup()s the descriptor: the
// duplicates exist in this process while the next child is created.
expect(ownHandleCount() - before).toBeGreaterThanOrEqual(N);
await using withDuplicates = spawnHandleCounter();

const [controlCount, withDuplicatesCount] = await Promise.all([
reportedHandleCount(control),
reportedHandleCount(withDuplicates),
]);
// An inheritable dup() hands every one of the N duplicates to the child,
// so the difference used to be exactly N.
expect(withDuplicatesCount - controlCount).toBeLessThan(N / 2);
} finally {
await Promise.all(readers.map(reader => reader.cancel()));
for (const fd of fds) closeSync(fd);
}
});

it.if(isWindows)("throws a spawn error for a cwd longer than the maximum path length", async () => {
const fixture = `
try {
Expand Down