Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[darwin] pty_posix_spawn leaks the pty master+slave when posix_spawn fails, and one ptmx fd on every successful spawn

Open
#950 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
68/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
cpp, macos

Research direction

Start in src/unix/pty.cc at pty_posix_spawn and inspect its low-fd prologue, error returns, done: path, and the PtyFork failure path. Reproduce failed and successful spawns with the provided oversized-argv script, then verify with lsof that all master, slave, and low_fds descriptors are closed and that the existing spawn behavior still works.

Written by the indexing model from the issue text.

Description

Version: node-pty 1.1.0, macOS (arm64, also reproduced on macOS 26), Node 22 / Electron 42.

On darwin, pty_posix_spawn (src/unix/pty.cc) allocates a pty before spawning and never releases it on any error path — and its low-fd prologue leaks one fd even when everything succeeds.

Leak 1 — failed spawn leaks the master and the slave (2 pty devices per failure)
  • *master = posix_openpt(O_RDWR) (pty.cc:707) and slave = open(slave_pty_name, …) (pty.cc:725) have no matching parent-side close() anywhere: every early return (L709, 714, 722, 727, 733, 740) and the done: path (L777) leave both fds open. The posix_spawn_file_actions_addclose calls at L749-750 apply only to the child.
  • PtyFork then throws (pty.cc:372-374, "posix_spawnp failed.") without cleanup, and since the JS ctor never receives term.fd, the caller cannot close anything either.
Leak 2 — successful spawn leaks one ptmx fd (off-by-one)

The low-fd prologue (L694-701) opens ptys until one lands at fd >= STDERR_FILENO, then breaks. Cleanup is:

for (; count > 0; count--) close(low_fds[count]);

In the common case the loop breaks with count == 0, so the body never runs and low_fds[0] leaks on every spawn. It also skips index 0 whenever count > 0, and reads low_fds[3] out of bounds if the prologue loop completes without breaking.

Reproduction

Forcing a real posix_spawn failure needs more than a nonexistent binary (on macOS argv[0] is the spawn-helper, which exists — the failure then happens in the child). E2BIG via an oversized argv works:

const pty = require('node-pty')
const huge = 'x'.repeat(3 * 1024 * 1024)
for (let i = 0; i < 25; i++) {
  try { pty.spawn('/bin/echo', [huge], { cwd: '/tmp' }) } catch (e) { /* posix_spawnp failed. */ }
}
// lsof -p <pid>: 50 /dev/ptmx + 25 /dev/ttysNNN still open

Measured on macOS: 25 failed spawns → +50 /dev/ptmx fds, +25 /dev/ttys* fds, system-wide device count 84 → 134 (2 devices per failure). 25 successful spawns, each fully exited and destroy()ed, still left 25 /dev/ptmx fds open (1 device each).

Impact

macOS caps pty devices system-wide at kern.tty.ptmx_max (default 511). Because failures leak two devices each, exhaustion is self-amplifying: at the ceiling every spawn fails, every failure consumes two more devices, and the process never recovers. An Electron app of ours accumulated 479 open masters in a 31-minute session against 28 real terminals, after which every spawn on the machine failed until the process exited.

Suggested fix
  1. In pty_posix_spawn, close *master (and slave once opened) on every error path, setting *master = -1; close slave in the parent unconditionally after posix_spawn, and close *master too when *err != 0.
  2. Track how many low_fds were actually opened and close all of them, e.g. size_t opened = count < 3 ? count + 1 : 3; for (size_t i = 0; i < opened; i++) if (low_fds[i] >= 0) close(low_fds[i]);
  3. Ideally include the posix_spawn errno in the thrown message — "posix_spawnp failed." currently discards the one datum (EMFILE vs EAGAIN vs ENOENT) that would let applications diagnose this.
Dominant language
TypeScript
Stars
2k
Forks
337
Avg merge
21h 58m
Merged PRs (30d)
3

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from microsoft/node-pty

All issues in microsoft/node-pty

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.