Skip to content

fix(ext/node): support fd 3 pipes in spawned Deno children#34133

Merged
bartlomieju merged 5 commits into
mainfrom
orch/issue-105
Jul 6, 2026
Merged

fix(ext/node): support fd 3 pipes in spawned Deno children#34133
bartlomieju merged 5 commits into
mainfrom
orch/issue-105

Conversation

@fibibot

@fibibot fibibot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Fixes #33368.
Closes bartlomieju/orchid-inbox#105.

This registers extra stdio fds installed by Deno's Node child_process spawn path in the child process FdTable. The parent annotates Deno children with the exact inherited fd slots it creates, and startup validates those descriptors before exposing them to node:fs fd APIs.

Inherited extra stdio fds are backed by duplicated handles for node:fs, so the original fd can still be claimed by libuv APIs such as net.Socket({ fd }) used by fork and cluster stdio tests.

This avoids scanning arbitrary process fds and clears the marker for child processes that do not receive extra stdio. The regression test is Unix-only because this bug is about inherited numeric fd slots installed with dup2.

Tests:

  • cargo fmt --check
  • deno fmt --check tests/unit_node/child_process_test.ts tests/unit_node/testdata/child_process_extra_stdio_fd3.js
  • cargo check -p deno_io -p deno_process
  • cargo check -p deno_io -p deno_node
  • ./x test-node unit_node::child_process_test

AI assistance was used to prepare this PR.

@fibibot

fibibot commented May 16, 2026

Copy link
Copy Markdown
Contributor Author

@bartlomieju CI is green.

@bartlomieju bartlomieju changed the title fix(node): support fd 3 pipes in spawned Deno children fix(ext/node): support fd 3 pipes in spawned Deno children May 26, 2026

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean fix for #33368. The key trick — duping the inherited fd into a separate handle so node:fs operations go through the dup while the original fd stays free for libuv (net.Socket({ fd })) to claim later via pipe_wrap.rs::open removing it from the table — is the right shape and matches what Node expects.

CI is green. A few non-blocking suggestions inline; the env-var leak is the only one with real teeth, the rest are quality/test-coverage nits.

Comment thread ext/process/lib.rs Outdated
Comment thread ext/io/lib.rs Outdated
Comment thread ext/io/lib.rs
Comment thread tests/unit_node/child_process_test.ts

@bartlomieju bartlomieju left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Traced the full mechanism — this is a solid fix, and the dup-vs-original split (node:fs uses a dup(), the original numeric fd stays open so libuv can reclaim it via net.Socket({ fd })) is the right design. Rc refcounting also correctly avoids a premature close when the fs stream and socket overlap. Windows cfg gating checks out.

Gate note: the API shows "No checks found" and the "CI is green" comment predates the two later merges from main (last on 2026-07-01). Please confirm CI is green on the current head before merge — I can't verify it from here.

Details inline below. Nothing blocking beyond #1 + CI confirmation.

Comment thread ext/node/ops/pipe_wrap.rs
Comment thread ext/io/fd_table.rs
Comment thread ext/io/lib.rs
- PipeWrap::open: consume the InheritedExtraStdio entry only after
  uv_pipe_open succeeds. Previously the entry (and its dup) was removed
  up front, so a failed uv_pipe_open left the fd untracked and unusable
  by node:fs. Now the fd stays usable when the open fails.
- fd_table: document that InheritedExtraStdio holds a dup(), so dropping
  the entry closes only the dup and deliberately keeps the original fd
  open for libuv to reclaim (a trade-off vs Node's autoClose behavior).
- io init: make the remove_var SAFETY comment accurate (runs
  synchronously on the main thread during extension init, before any
  code that could touch the environment concurrently).
@bartlomieju
bartlomieju merged commit 4b3cf68 into main Jul 6, 2026
136 checks passed
@bartlomieju
bartlomieju deleted the orch/issue-105 branch July 6, 2026 11:58
bartlomieju added a commit that referenced this pull request Jul 7, 2026
Follow-up to #34133, which registers inherited extra stdio fds in the
child's FdTable so node:fs fd APIs can use them. That registration broke
adopting an inherited fd that is a TCP socket: `net.Socket({ fd })` routes through
`TCPWrap::open`, whose duplicate-fd check was not taught about
`InheritedExtraStdio` entries and rejected the fd with `EEXIST`
(`PipeWrap` got the exemption, `TCPWrap` did not). This mirrors the `PipeWrap` logic
in `TCPWrap::open`: inherited extra stdio fds are allowed, and their entry
(with the node:fs dup it holds) is consumed only once `uv_tcp_open` actually
claims the fd. The new regression test fails with `EEXIST` without this change.

The startup dup is now also created with `F_DUPFD_CLOEXEC` instead of
`dup()`. It exists purely for in-process node:fs use, but without
close-on-exec it leaked into spawned grandchildren, holding pipe ends
open and delaying EOF for the parent past what Node exhibits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

node:child_process: fd 3 pipe broken in spawn()

2 participants