Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

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

未关闭
#950 2 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
68/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
cpp, macos

调研方向

从 src/unix/pty.cc 中的 pty_posix_spawn 开始,检查其 low-fd 前导部分、错误返回、done: 路径以及 PtyFork 失败路径。使用提供的 oversized-argv 脚本复现失败和成功的 spawn,然后使用 lsof 验证 master、slave 和 low_fds 的所有描述符都已关闭,并确认现有的 spawn 行为仍然正常工作。

由索引模型根据 Issue 内容生成。

描述

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.
主要语言
TypeScript
星标
2k
派生
337
平均合并
23 小时 33 分钟
30 天内合并 PR
2

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

microsoft/node-pty 的其他 Issue

查看 microsoft/node-pty 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。