sys(windows): create dup() handles non-inheritable so children stop inheriting them - #37523
Conversation
|
Warning Review limit reached
Next review available in: 6 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Status: reproduced and fixed, waiting on CI. Reproduced on Windows x64 with the released The test is Windows only: the POSIX |
There was a problem hiding this comment.
LGTM — one-bool flip aligning Windows dup() with the POSIX arm's F_DUPFD_CLOEXEC.
What was reviewed
- Confirmed the POSIX arm at
src/sys/lib.rs:2624already usesF_DUPFD_CLOEXEC; this makes Windows match. - Checked
dup()callers (FileReader, fetch body, FileSink, shell stdio/cwd, Terminal) — none rely on the duplicate itself being inheritable; stdio handed to children goes through libuv'sUV_INHERIT_FD, which re-duplicates inheritable. - Test:
dlopen("kernel32.dll")is inside the.if(isWindows)body so the top-levelbun:ffiimport is inert on POSIX; readers/fds are released infinally; the differential assertion (< N/2) against a control child avoids absolute-count flakiness.
Extended reasoning...
Overview
Single functional change: src/sys/lib.rs Windows dup() now passes bInheritHandle = FALSE to DuplicateHandle instead of TRUE, plus a doc comment explaining why. A Windows-only regression test in test/js/bun/spawn/spawn.test.ts opens 64 fds, starts Bun.file(fd).stream() on each (which dup()s), and uses GetProcessHandleCount via bun:ffi to assert a child spawned while those duplicates are open does not inherit ~64 extra handles compared to a control child.
Security risks
None introduced. This removes unintended handle inheritance into child processes, which is strictly a reduction in exposure. No new attack surface, no user-controlled input reaches the changed line.
Level of scrutiny
Low-to-medium. The runtime change is a one-line constant flip in a platform-gated function whose POSIX twin already has the equivalent semantics (F_DUPFD_CLOEXEC). The PR description enumerates every dup() caller and every process-creation path and explains why none need the inheritable flag on the source handle — libuv's uv__duplicate_handle re-duplicates stdio as inheritable regardless, so UV_INHERIT_FD still works. I spot-checked the caller list with grep and it matches. src/sys/lib.rs shows this is the only DuplicateHandle call in src/.
Other factors
The test follows harness conventions: tempDir with using, bunEnv, concurrent drain of stdout/stderr/exited, cleanup in finally registered before assertions, await using on subprocesses. It asserts its own precondition (ownHandleCount() - before >= N) so it can't pass vacuously if stream() stops dup()ing. The < N/2 differential margin is wide enough to tolerate a few incidental handles without admitting the +64 regression. The top-level import { dlopen } from "bun:ffi" is safe on POSIX because the dlopen("kernel32.dll", ...) call itself is inside the .if(isWindows) body. PR description reports the test fails on unfixed canary and passes on the debug build, and that the rest of spawn.test.ts still passes on Windows.
Problem
On Windows, every handle returned by
bun_sys::dup()was created inheritable (DuplicateHandle(..., bInheritHandle = TRUE, ...)). libuv'suv_spawncallsCreateProcessWwithbInheritHandles = TRUEand no handle list (vendor/libuv/src/win/process.c), so any process Bun spawned while such a duplicate was open received a copy of it, and kept the underlying file / pipe / directory open after Bun itself had closed the duplicate, for as long as the child lived.dup()backsBun.file(fd).stream()(FileReader.rs), afetch()body made fromBun.file(fd),FileSinkstarted on an fd (io/openForWriting.rs), and the shell: its duplicates of stdin/stdout/stderr and of the cwd directory handle for every subshell / pipeline (shell/interpreter.rs,shell_dup/dupe_for_subshell). The POSIX arm of the same function usesfcntl(F_DUPFD_CLOEXEC)precisely so this does not happen.Measured on Windows x64 with the released
1.4.0-canary(da3851e): abun -echild that prints its ownGetProcessHandleCount(), spawned five times each way while the parent held 64 open fds:Fix
src/sys/lib.rs, Windowsdup(): passFALSEforbInheritHandle. Nothing relies on these duplicates being inheritable:bInheritHandle = TRUE(uv__duplicate_handleinvendor/libuv/src/win/process-stdio.c), regardless of the source handle's flag. This coversBun.spawn/child_process(UV_INHERIT_FDinspawn/process.rs) and every shell command, since the shell spawns through the same path.--watchmanager, the bin shim, the crash reporter,std::process::Commandinbun_core::util) do not involvedup()at all; the first two hand the child the process's real std handles and set those inheritable themselves.NtCreateFilepaths, libuv'suv_fs_open) is already non-inheritable, which is what the 164 / 164 control above shows.dup()was the onlyDuplicateHandlecall in the tree.The POSIX arm is unchanged; it already has the CLOEXEC behaviour this gives Windows.
Verification
New test in
test/js/bun/spawn/spawn.test.ts(Windows only, since the POSIXdup()is already close-on-exec): opens 64 fds, spawns a control child, startsBun.file(fd).stream()on each fd, checks withGetProcessHandleCountthat this process now holds at least 64 more handles, spawns a second child, and asserts that the second child did not start with (about) 64 more handles than the first.Expected: < 32, Received: 64.spawn.test.tspasses there as well (125 pass, 19 platform skips, 3 todo).dup()caller still works:Bun.file(fd).stream().text(),fetch()with aBun.file(fd)body,Bun.spawnwith an fd asstdout, and shell commands with inherited stdio, pipelines and subshells.The same function is touched by #37516 (EBADF for a descriptor that is not open); the two changes are on different lines and
git merge-treereports a clean merge either way.