2 Commits
Author SHA1 Message Date
Neil 04ae62202a fix(ssh): close the macOS relay's per-terminal pty fd leak (#18534)
The relay asset from #17920 only rewrote the forkpty `default:` call site, which
sits in the `#else` arm of PtyFork's `#if defined(__APPLE__)`. macOS takes
`pty_posix_spawn`, so the asset had never patched anything a Mac executes -- and
`applyNodePtyMasterCloexecPatch` returned 'fixed' for any non-Linux host without
running the script at all, which is what publishes a tree to the shared
native-deps cache.

Stock `pty_posix_spawn` opens up to three throwaway ptys to push the real master
off fds 0-2 and never closes them: the cleanup loop is `for (; count > 0;
count--)`, but the first `posix_openpt()` in a running process already returns
>= 2, so it breaks with `count == 0` and the body never runs -- and where it does
run it closes `low_fds[count]`, never `low_fds[0]`. One orphaned /dev/ptmx fd per
terminal, for the life of the relay.

Ports the `low_fds` fix and the Apple-branch `pty_cloexec(master)` call from the
app's `config/patches/node-pty@1.1.0.patch`, byte-identical, and runs the gate on
darwin. macOS needs a different build layout than Linux: it has no `build/` at
all, so the fallback moved aside is `prebuilds/darwin-<arch>` -- which is also
what makes node-pty's install script fall through from "prebuild found" to
node-gyp -- and the compile writes a `build/Release` the loader checks first.
Verification is per-platform too: Linux's leak is inheritance (/proc), macOS's is
self-held (lsof).

Also corrects the asset's claim that "macOS re-opens the tty through uv_tty_init's
cloexec dup". Measured false: FD_CLOEXEC is not set on the master. What protects
it is POSIX_SPAWN_CLOEXEC_DEFAULT, one option away from gone since uid/gid drops
libuv back to fork()/exec() -- so the master is now marked there too.

Measured on darwin-arm64, one PTY per open/close cycle in a relay-shaped dir
running the relay's own commands:

  before  cycle:ptmx  1:1 2:2 3:3 ... 10:10   (10 after a settle)
  after   cycle:ptmx  1:0 2:0 3:0 ... 10:0    (0 after a settle)

Linux re-verified in docker node:22: inherited before, isolated after,
`already-patched` on the second run.

Refs #17915
Refs #8362
2026-09-03 16:13:07 -07:00
Neil aa3ae6f56e fix(ssh): close the pty master fd leak on relay hosts too (#17920)
* fix(ssh): close the pty master fd leak on Linux relay hosts

The app gets the FD_CLOEXEC patch through pnpm patchedDependencies (#17914);
the relay installs stock node-pty from npm, where no pnpm patch reaches. Linux
is where that matters -- it is the only relay platform that takes forkpty()'s
no-atomic-O_CLOEXEC path, and it is also the only one that already compiles
node-pty at install time, so the fix costs a second compile rather than a first.

Ships the patch as a relay asset applied like the existing Windows console-list
one, and rebuilds only after the probe has proven node-pty loadable. The rebuild
is non-fatal by construction: the working build is moved aside first and moved
back on any failure, a failed attempt drops a skip marker so the compile is
attempted at most once per relay directory, and the caller swallows the whole
step. macOS and Windows relays never run it.

Measured on node:22 with a relay-style npm install: before, the master is
cloexec=false and shows up as `26 -> /dev/pts/ptmx` in both a later pty child
and a later child_process child; after, cloexec=true and neither child sees it.

Closes #17915.

* test(ssh): feed the cloexec patch exec to the hand-rolled namespace fixtures

These sequences are positional, so the new Linux-only patch exec swallowed the
READY slot and every install/repair case timed out waiting for the relay.

* fix(ssh): patch the pty master before publishing the shared native-deps tree

* fix(ssh): refuse to publish a native-deps tree whose cloexec patch did not take
2026-09-02 03:02:27 -07:00