From fd5f743281640f20af1fe045a810387e960b6ea5 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Sat, 20 Jun 2026 14:20:34 -0700 Subject: [PATCH] fix: close kqueue fd in SetupExitCallback on macOS SetupExitCallback opens a kqueue() per spawned pty to wait on NOTE_EXIT, but never closes it before the watcher thread returns, leaking one kqueue fd per pty.spawn() for the host process lifetime. The Chromium kill_mac.cc this is based on closes the kqueue via ScopedFD; the equivalent here is an explicit close(kq) once the kevent wait completes. --- src/unix/pty.cc | 1 + src/unixTerminal.test.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/unix/pty.cc b/src/unix/pty.cc index 88b1a4c9..3b7d3dc1 100644 --- a/src/unix/pty.cc +++ b/src/unix/pty.cc @@ -203,6 +203,7 @@ void SetupExitCallback(Napi::Env env, Napi::Function cb, pid_t pid) { } } } + close(kq); #else while (true) { errno = 0; diff --git a/src/unixTerminal.test.ts b/src/unixTerminal.test.ts index f13e66d4..a666e91a 100644 --- a/src/unixTerminal.test.ts +++ b/src/unixTerminal.test.ts @@ -397,6 +397,37 @@ if (process.platform !== 'win32') { `Leaked ${finalCount - initialCount} /dev/ptmx FDs after spawning 20 PTYs (initial: ${initialCount}, final: ${finalCount})` ); }); + it('should not leak kqueue file descriptors after pty exit', async function(): Promise { + this.timeout(30000); + + const getKqueueFDCount = (): number => { + try { + const output = cp.execSync(`lsof -p ${process.pid} 2>/dev/null`, { encoding: 'utf8' }); + return output.split('\n').filter(line => line.includes('KQUEUE')).length; + } catch { + return 0; + } + }; + + const initialCount = getKqueueFDCount(); + for (let i = 0; i < 20; i++) { + const term = new UnixTerminal('/bin/bash', ['-c', 'echo hello']); + await new Promise(resolve => { + term.onExit(() => { + term.destroy(); + resolve(); + }); + }); + } + + await new Promise(r => setTimeout(r, 500)); + + const finalCount = getKqueueFDCount(); + assert.ok( + finalCount <= initialCount, + `Leaked ${finalCount - initialCount} kqueue FDs after spawning 20 PTYs (initial: ${initialCount}, final: ${finalCount})` + ); + }); } it('should handle exec() errors', (done) => { const term = new UnixTerminal('/bin/bogus.exe', []);