-
Notifications
You must be signed in to change notification settings - Fork 6
Description
Summary
PtyFork in src/unix/pty.cc calls openpty() which returns both master and slave file descriptors, then uses posix_spawn to pass the slave to the child process. However, the parent process never closes its copy of the slave fd after the spawn succeeds.
This causes one file descriptor to leak per PTY spawn.
Impact
On long-running processes (terminal multiplexers, IDE backends, agent frameworks), this eventually exhausts the system's PTY pool. On macOS, the default limit is 512 PTYs (kern.tty.ptmx_max). At typical usage rates, the pool is exhausted in 12-19 days, after which SSH, new terminal sessions, and any PTY-dependent operation fails.
Root cause
int master, slave;
int ret = pty_openpty(&master, &slave, nullptr, term, &winp);
// ...
auto error = posix_spawn(&pid, helper_path, &acts, &attrs, argv, env);
close(comms_pipe[1]);
// ❌ close(slave) is missing hereThe slave fd has already been dup2'd to stdin/stdout/stderr of the child via posix_spawn_file_actions_adddup2. The parent's copy is no longer needed and should be closed.
Reproduction
const pty = require("@lydell/node-pty");
const { execSync } = require("child_process");
const pid = process.pid;
const p = pty.spawn("/bin/sh", ["-c", "exit 0"], { cols: 80, rows: 24 });
p.onExit(() => {
setTimeout(() => {
p.destroy();
setTimeout(() => {
// This will show 1 leaked /dev/ptmx fd (the slave)
const fds = execSync(`lsof -p ${pid} 2>/dev/null | grep ptmx || true`).toString().trim();
console.log("Leaked ptmx FDs:", fds || "(none)");
process.exit(0);
}, 500);
}, 500);
});After spawn + exit + destroy, one /dev/ptmx fd remains open. On macOS, both master and slave sides of a PTY pair opened via openpty() appear as /dev/ptmx in lsof. The master is properly closed by destroy(), but the slave is never closed.
Fix
Add close(slave) after posix_spawn succeeds, alongside the existing close(comms_pipe[1]):
close(comms_pipe[1]);
close(slave);PR: #forthcoming
Environment
- macOS 15.3 (arm64)
- Node.js v22.22.0
- @lydell/node-pty-darwin-arm64 (latest as of 2026-03-15)