Repository navigation
fix(pty): stop leaking one /dev/ptmx per macOS spawn (node-pty 1.1.0 low_fds[0]) - #1439
Merged
Juliusolsson05 merged 4 commits intoSep 27, 2026
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node-pty 1.1.0's pty_posix_spawn leaks one /dev/ptmx per spawn (low_fds[0] is never closed), so a long-running app exhausts kern.tty.ptmx_max and every spawn fails. Port upstream microsoft/node-pty#882 as a sha256-guarded source patch run from postinstall before electron-rebuild; pin node-pty to 1.1.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 27, 2026
Owner
Author
|
Real-PTY evidence, now that the q117 pause is lifted (one spawn at a time, 20 spawns,
🤖 Generated with Claude Code |
… symlink (#1439 review a, b) a: the unit test asserts the generated 'if (low_fds[i] != -1)' and final 'if (spawn_err != 0)' text, so an inversion fails CI even with the recorded hash updated (Linux CI never runs the macOS fd test). b: the CLI entry compares realpaths; through a symlinked path it used to exit 0 without patching. Pinned by running the real script via a symlink. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… real classifier (#1439 review c) The comment and plan claimed nothing in the app matched on 'posix_spawnp failed'. src/main/ipc/session.ts classifySpawnFailure keys the posix-spawnp signature on it. Both now say so, and session.test.ts pins that the patched node-pty messages ('posix_spawnp failed: <call>: <strerror>') keep that signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner
Author
|
Round-1 disposition. a, b and c are all MERGE-READY, each with one minor finding. All three are fixed, each pinned by a test that fails without its fix:
Head: 7e346a6. 🤖 Generated with Claude Code |
Juliusolsson05
changed the base branch from
main
to
integration/batch-2026-09-27-o
September 27, 2026 19:16
Juliusolsson05
merged commit Sep 27, 2026
54c63d5
into
integration/batch-2026-09-27-o
2 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1437
What was wrong
node-pty 1.1.0 (our pinned stable, and still npm
latest) leaks one/dev/ptmxper spawn on macOS. Inpty_posix_spawn, thelow_fdsguard always opens one ptmx atlow_fds[0](stdio is open, so the loop breaks atcount == 0). Its cleanup,for (; count > 0; count--) close(low_fds[count]), never closes index 0. macOS caps PTYs system-wide (kern.tty.ptmx_max= 511), so after about 500 agent or terminal spawns every spawn on the machine fails withposix_spawnp failed.Evidence, from the packaged app today (main pid 94543, 13.5 h uptime):
/dev/ptmxfds in main.low_fds[0]signature.Upstream fixed it in microsoft/node-pty#882 (
af053f22, for microsoft/vscode#182212), but only on the 1.2.0 beta line.Fix
scripts/patch-node-pty.mjsruns inpostinstallbeforeelectron-rebuild. It ports #882'spty_posix_spawnonto 1.1.0's source:low_fdsentry, including index 0;done:(1.1.0 leaked the master there);PtyForkclose the master on failure and throwposix_spawnp failed: <call>: <strerror>.Why this ships: node-pty is compiled from source in
postinstall, electron-builder rebuilds it per arch with prebuilds excluded, andlib/utils.jsloadsbuild/Releasefirst.Guarding:
pty.ccis identified by sha256. A pristine file gets patched; an already-patched file is a no-op; any other file fails the install.node-ptyis pinned to exactly1.1.0. The WHEN THIS FAILS note in the script says what to do when node-pty moves.Deliberate differences from #882 (see Execution notes in the plan):
i < 3, because feat(opencode): OpenCode Terminal reports status, turns, conditions and history through opencode-terminal-headless #882'si <= countcan readlow_fds[3];posix_spawnp failedprefix, whichsrc/main/ipc/session.tsclassifySpawnFailurekeys theposix-spawnpsignature on (fix(ipc): session:spawn launders provider exceptions before they cross IPC (#1267) #1324; pinned insession.test.ts).Why not bump to 1.2.0-beta.15: the beta line brings unrelated ConPTY and API churn to get one function. This is the same trade as
scripts/patch-xterm.mjs(#871).Tests
testing/unit/patchNodePty.test.ts(7) runs against the byte-exact published 1.1.0pty.cc, committed undertesting/fixtures/node-pty-1.1.0/because the installed copy is the patched one after postinstall. It covers:goto donecoverage;low_fds[i] != -1andspawn_err != 0guards; each inversion fails even with the hash updated;session.test.tspins that the patchedposix_spawnp failed: <call>: <strerror>messages keep theposix-spawnpsignature.testing/system/nodePtyPtmxLeak.test.ts(darwin only) spawns 20 real PTYs, one at a time, and assertslsof -pptmx fds return to the baseline. It is a port of feat(opencode): OpenCode Terminal reports status, turns, conditions and history through opencode-terminal-headless #882's test. It fails on the unpatched build (expected 20 to be less than or equal to 0). Run on an isolated patched rebuild, the same measurement gives 0 → 0 (see the evidence comment).Verification
pty.cccompiles with node-gyp (Node 24), in an isolated copy. The only warnings are the 2 pre-existing kqueue initialiser warnings.npx tsc -bclean.Boundary
lsof -p <main pid> | grep -c /dev/ptmxshould track the live session count.🤖 Generated with Claude Code