Repository navigation
Conversation
|
No actionable comments were generated in the recent review.
|
| Layer / File(s) | Summary |
|---|---|
Reuse descriptors for matching output paths src/spawn/process.rs, src/spawn_sys/spawn_process.rs |
When stdout and stderr target the same path, both spawn implementations use the already-open stdout descriptor for stderr. |
Test and document shared file output test/js/bun/spawn/spawn.test.ts, docs/runtime/child-process.mdx, packages/bun-types/bun.d.ts |
Tests cover shared and separate file objects, distinct paths, stderr-only output, and independent read offsets. Runtime and type documentation describe shared output files. |
Suggested reviewers: jarred-sumner
Priority:
Merge Risk: 
Minimal · up to bf87d
-
The reviewed change has no identified merge-blocking issue; it is ready for normal merge checks.
Pre-merge checks: 
4
-
Full details: Description check
-
Explanation
The description clearly explains the problem, the implementation, limitations, testing, platform coverage, and verification results. It does not use the template headings exactly, but it provides the information required by "What does this PR do?" and "How did you verify your code works?".
-
- Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.
Comment @coderabbitai help to get the list of available commands.
|
Updated 2:26 AM PT - Oct 24th, 2026
✅ @robobun, your commit bf87d1a50cf868cd6652cd6c50ef4885d000ab1c passed in 🧪 To try this PR locally: bunx bun-pr 46664That installs a local version of the PR into your bun-46664 --bun |
|
Status: ready for review. How it was reproduced (Linux x64): a child writes lines to stdout and stderr by turns with The 8 same-path rows in CI ran the 12 new rows on Windows x64 and aarch64 and on macOS aarch64. All 12 pass on each, and that includes the |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline findings, I also checked two things and ruled them out: the POSIX action ordering (slot 1's open is queued before slot 2's dup2(1, 2) in the same loop, and the dup_stdout_to_stderr hack only applies to the Dup2 kind, so stderr cannot dup a not-yet-opened fd 1), and the Windows handle lifetime (the shared uv_file is pushed to uv_files_to_close only once from the stdout slot, so the stderr slot reusing it cannot double-close).
Extended reasoning...
The change touches the POSIX and libuv spawn stdio setup in src/spawn_sys/spawn_process.rs and src/spawn/process.rs, plus .d.ts, docs, and a new describe.each test block in test/js/bun/spawn/spawn.test.ts; it touches no auth, crypto, or input-parsing surface. Two inline findings were already posted (the undocumented Windows skip on the stdio[3] row and the byte-equality-only path compare), so a human look is already signalled; this note only records the ordering and handle-lifetime checks that came up clean.
… path Each path slot got its own open(), so stdout and stderr had two file offsets that both started at 0, and each stream overwrote the other. When stderr holds the same path bytes as stdout, the stderr slot is now a dup of the stdout slot (dup2(1, 2) on POSIX, the same uv_file on Windows), so both streams share one open file description.
The Windows spawn code opens a path at an extra slot as well, so the row does not need a platform skip.
ebb7dcd to
bf87d1a
Compare
There was a problem hiding this comment.
I re-reviewed after the second push and found no bugs; the stdio[3] row now runs on Windows, which closes the only nit from my earlier pass. Because this changes how Bun.spawn wires child stdio across both platform backends, a human look at the semantics would still be worthwhile.
What was reviewed:
- POSIX arm (src/spawn_sys/spawn_process.rs:802): the dup2(1, 2) action is appended after slot 1's open action, and both the Linux action list and the macOS posix_spawn file-actions run in order, so fd 1 already names the file when the dup runs.
- Windows arm (src/spawn/process.rs:2121): the guard can only match at slot 2 since
stdout_path_fileis set after slot 1's own arm; the uv_file is pushed touv_files_to_closeonce, so no double close. The if-let match guard is not compiled on this Linux host, so I could not confirm it builds beyond the let-chains already used elsewhere in src/. - Tests: the describe.each matrix covers both entry points, same object vs same path, distinct paths, stderr-only, and the stdin/stdio[3] negative contract; same-path rows read back exact bytes, so they fail on the base behavior.
Extended reasoning...
The change touches the child-stdio setup in both spawn backends (src/spawn_sys/spawn_process.rs for POSIX, src/spawn/process.rs for Windows libuv), plus docs, the .d.ts JSDoc, and 109 lines of new tests in test/js/bun/spawn/spawn.test.ts; the production diff is about 20 lines. It touches no auth, crypto, or input-parsing surface; the new comparison is a plain byte equality of two user-supplied paths that were already being opened. The second commit (bf87d1a) addressed the only inline nit from the prior run by dropping the Windows skip on the stdio[3] row. Deferring rather than approving because this is a user-visible semantic change to Bun.spawn stdio handling and the Windows arm is cfg-gated code that cannot be type-checked from this host.
|
The Windows arm compiles. The For the human look at the semantics, there is one decision to confirm. When |
Problem
Bun.spawn(cmd, { stdout: log, stderr: log })with oneBun.file(path)loses output in silence. A child writes six lines to stdout and stderr by turns, three remain, and the exit code is 0.Bun.spawnSyncdoes the same.open()(src/spawn_sys/spawn_process.rs:800). The two file offsets both start at 0, so each stream overwrites the other.Fix
dup2(1, 2). Windows uses the sameuv_file.test/js/bun/spawn/spawn.test.ts, 12 new rows. The 8 same-path rows fail on main. Alsobun-types.test.tsandrust:check-all.Background
open()makes a new one.dup2makes a second descriptor for the same one.O_APPEND: the second open stays, and the child sees the flag. Considered an inode compare: it needs the parent-side open of spawn: open Bun.file() stdio paths in the parent so a blocking open cannot freeze the runtime #37641.Downsides
stdio[3], one path given to two spawns. Notes has each case.spawn_process_posixruns 5 more instructions (1,820 to 1,825). Text grows 512 bytes.Notes
Origin. Found by a fuzz run. No user reported it, so there is no
Fixesline. A search of the tracker found no earlier report.Reproduction (Linux x64).
OUT-lines.print, perlprint) loses all of stderr on main: its stdout is flushed at exit, over the stderr lines.What the change gives. Every byte of both streams is kept. The order in the file is the order of the child's
writecalls. A python3 or perl child writes stderr first, because it buffers stdout. One fd number in both slots and> file 2>&1in a shell give the same order. A caller that read the old file saw one stream on top of the other. It now sees both.This already works with one fd. The change adds no capability. It stops the
Bun.file(path)spelling from failing in silence.Cases that still lose output (release build of this PR, exit code 0 in each).
Bun.file(abs), stderrBun.file("./log.txt"),dir//log.txt, or a symlinkerr 0\nerr 1\nerr 2\nut line 1\nstdout line 2\nchdir. A string normalisation in the parent can merge two different files. An inode compare needs both descriptors in the parent, which is #37641.Bun.fileat stdout andstdio[3], both writtenfd3 0\nfd3 1\nfd3 2\nut line 1\nstdout line 2\nO_RDWR, and Bun cannot tell a reader from a writer. A shared offset breaks a child that reads the file through fd 3 and writes it through fd 1.Bun.fileat stdout and stderr of two spawns that overlapO_APPENDreaches that case.spawnSynccalls in a row: each spawn opens at offset 0. Bun.spawn: truncate stdout/stderr when redirected to Bun.file(path) #35352 covers truncation.stdio[3]cases: a read through fd 0 or fd 3 starts at offset 0.file:URL,.slice(0).Prior art. systemd does the same for
StandardOutput=file:PwithStandardError=file:P:can_inherit_stderr_from_stdout()and thendup2(STDOUT_FILENO, fileno)(src/core/exec-invoke.c). Its compare ispath_equal. execa documents one file at both options as the way to get one log, and comparespath.resolve()of the two (lib/stdio/duplicate.js). Node has no path-typed stdio.An in-repo sighting.
scripts/darwin-ci/lib/tart.ts:25-28passes oneBun.fileas stdout and stderr for the VM boot log. A review comment on #37633 reported that log as overwritten ("verified: only stderr survives"). This PR does not fix that script: the tart hosts run the bun thatscripts/darwin-ci/host.shpins, and a retried boot spawns a secondtart runonto the same log. The script needs its own change to one fd.Measurements (Linux x64, release builds of main 5c6d4e2 and of that commit plus this diff). The merge base is one commit later (6292c65). That commit does not touch
src/spawn_sys,src/spawnorbun-spawn.cpp./proc/self/fdinfoin the child after 6 bytes to each stream. Same forspawnSyncand thestdioarray form.WNOHANGpolls that find the child still running, because their number depends on timing. 3 runs per shape agree. In the same-file shape the child'sopenat+dup2(8, 2)+close(8)becomedup2(1, 2).stepiover the third call, callees included (valgrind is not installed). 5 runs per shape. 2 of the 50 runs differ by 86 instructions inside mimalloc. The instructions in the function's own code are the same in all runs: 578 -> 583, 487 -> 499, 487 -> 497, 357 -> 387, 357 -> 385.bunis the same: 107,808,480 bytes.src/hunks. macOS and Windows were not run by hand.Self-review. 13 concerns, all addressed.
stdio[3]rows.Related open PRs.
O_TRUNCto the same open. It composes: one open withO_TRUNC, then the dup.open_stdio_pathcall. The new rows carry the behaviour across.Test runs (Linux x64, debug build with ASAN, last rebased onto a8fcd31).
test/js/bun/spawn/spawn.test.ts, 4 had no failure. 3 had a 5 s timeout in an existing test:Bun.file() works as stdoutin one,stdout can be readin two.stdout can be readstarts 10 debug children and takes 1.8 to 4.3 s of its 5 s in the runs that pass. Neither test has a path at stderr, so neither takes the new branch. Each passes alone.spawnSyncrows run one at a time, becausespawnSyncblocks the test process.test/integration/bun-types/bun-types.test.ts: 24 pass.no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/bun/spawn/spawn.test.ts