-
Notifications
You must be signed in to change notification settings - Fork 13k
System-wide PTY exhaustion due to file descriptor leak #15945
Description
What happened?
gemini-cli (v0.22.5) appears to be leaking Pseudo-Terminal (PTY) master handles (/dev/ptmx). Over time, this exhausts the macOS system limit (kern.tty.ptmx_max), which is typically 511. When I started debugging the problem i had 1058.
I think when my count crossed 1023 PTYs, I got the following problems in creating new tabs/sessions:
- iTerm2 presented a dialog when trying to open a new session:
"A session ended very soon after starting. Check that the command in profile "Default" is correct." - Terminal.app displayed:
[forkpty: Device not configured]and[Could not create a new process and open a pseudo-tty.]
Investigation via lsof revealed that multiple node processes running gemini were holding hundreds of open handles to /dev/ptmx. One specific session had leaked 155 PTY handles and was holding a total of 495 file descriptors. Some of these sessions had been running for over two weeks (since Dec 22). (Though I had run /clear a few times, based on what i'm seeing in the json trajectories)
What did you expect to happen?
I expected gemini-cli and its associated processes (including MCP servers and node-pty instances) to correctly close file descriptors and PTY handles when they are no longer in use, and to terminate cleanly instead of remaining alive as idle processes that accumulate leaked resources. ;)
Client information
Client Information
> /about
│ CLI Version 0.22.5 │
│ Git Commit 8daf2d34b │
│ Model gemini-3-flash-preview │
│ Sandbox no sandbox │
│ OS darwin │
│ Auth Method gemini-api-key- Platform: macOS 15.7.3 (arm64)
- Binary Path:
~/.homebrew/bin/gemini- installed via npm install -g - Node version: v24.11.1
- Dependency: @lydell/node-pty-darwin-arm64 (version: 1.1.0)
Login information
Logged in via GEMINI_API_KEY environment variable.
Anything else we need to know?
The leak may be correlated with the usage of MCP servers. In one instance, a gemini process (PID 46302) was the parent of an MCP server process, yet the parent gemini process was the one accumulating the /dev/ptmx handles. (I only have 1 mcp server enabled currently and it rarely is used so... not sure about this)
Observations in shellExecutionService.ts
I noticed some logic in packages/core/src/services/shellExecutionService.ts that might be worth investigating:
- In
executeWithPty(around line 467),ptyInfo.module.spawn(...)is called. - If
spawnfails (e.g., throwing "posix_spawnp failed"), the error is caught by thetry...catchblock (around line 733). - The catch block issues a warning:
[GEMINI_CLI_WARNING] PTY execution failed, falling back to child_process. - It's possible that if
node-ptyopens the PTY master (/dev/ptmx) before theposix_spawnsyscall actually fails, the resulting file descriptor might not be getting closed during the fallback tochild_process.
Technical Evidence (lsof and Stack Traces)
PTY Handle Count per Process:
PID 46302: PTMX=155, FDs=495 | /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
PID 98406: PTMX=74, FDs=255 | /usr/local/bin/node ~/.homebrew/bin/gemini
PID 94695: PTMX=51, FDs=183 | /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
... [multiple other sessions with 10-30 PTMX handles each] ...
LSOF Detailed view for PID 46302:
node 46302 [USER] txt REG 1,18 85160 188016135 ~/homebrew/lib/node_modules/@google/gemini-cli/node_modules/@lydell/node-pty-darwin-arm64/pty.node
node 46302 [USER] 17u CHR 15,155 0t0 605 /dev/ptmx
node 46302 [USER] 20u CHR 15,151 0t0 605 /dev/ptmx
node 46302 [USER] 23u CHR 15,152 0t0 605 /dev/ptmx
... [truncated 150+ similar entries] ...
Stack Trace (Main Thread) of Leaking Process:
The process appears idle in the event loop while holding the leaked FDs:
883 Thread_28021267 DispatchQueue_1: com.apple.main-thread (serial)
+ 883 start (in dyld) + 6076 [0x18a23ab98]
+ 883 node::Start(int, char**) (in node) + 604 [0x1048298a8]
+ 883 node::NodeMainInstance::Run() (in node) + 276 [0x1048c3744]
+ 883 node::SpinEventLoopInternal(node::Environment*) (in node) + 256 [0x104771b7c]
+ 883 uv_run (in node) + 408 [0x1056c5d1c]
+ 883 uv__io_poll (in node) + 784 [0x1056d9874]
+ 883 kevent (in libsystem_kernel.dylib) + 8 [0x18a59fd04]
System Limits:
sysctl kern.tty.ptmx_max = 511
Diagnostic Script
I generated this to help investigate PTY usage
pty_report.sh
#!/bin/bash
# PTY Usage Detailed Report
echo "PTY Usage Report"
echo "================="
# Get system PTY limits
PTY_MAX=$(sysctl -n kern.tty.ptmx_max 2>/dev/null || echo "Unknown")
PTY_TOTAL=$(lsof -n /dev/ptmx 2>/dev/null | awk 'NR>1' | wc -l | xargs)
echo "System Max PTYs (kern.tty.ptmx_max): $PTY_MAX"
echo "Current Total Open PTY Handles: $PTY_TOTAL"
echo ""
printf "%-8s %-6s %-25s %-35s %s\n" "PID" "PTYs" "Binary" "CWD" "Full Command"
echo "------------------------------------------------------------------------------------------------------------------------"
# Get PIDs and their ptmx counts from lsof
lsof -n /dev/ptmx 2>/dev/null | awk 'NR>1 {print $2}' | sort | uniq -c | sort -nr | while read count pid; do
if [ -z "$pid" ]; then continue; fi
# Get command name
cmd=$(ps -p $pid -o comm= 2>/dev/null)
# Get full command line
args=$(ps -p $pid -o args= 2>/dev/null)
# Get CWD
cwd=$(lsof -a -p $pid -d cwd -Fn 2>/dev/null | sed -n 's/^n//p')
# Format and print the row
printf "%-8s %-6s %-25.25s %-35.35s %.100s\n" "$pid" "$count" "$cmd" "$cwd" "$args"
doneMy results:
PTY Usage Report
=================
PID PTYs Binary CWD Full Command
------------------------------------------------------------------------------------------------------------------------
1572 351 /Applications/iTerm.app/C / /Applications/iTerm.app/Contents/MacOS/iTerm2
46302 155 /usr/local/bin/node ~/src/numderscore /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
98406 74 /usr/local/bin/node ~/src/chrome-devtoo /usr/local/bin/node ~/.homebrew/bin/gemini
94695 51 /usr/local/bin/node ~/src/nemo /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
34569 48 /Applications/Visual Stud / /Applications/Visual Studio Code.app/Contents/Frameworks/Code Helper.app/Contents/MacOS/Code Helper
1724 39 ~/Library/ / ~/Library/Application Support/iTerm2/iTermServer-3.6.4 ~/Library/Appli
30521 34 /usr/local/bin/node ~/src/proj2 /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
38568 30 /usr/local/bin/node ~/src/proj3 /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
22155 30 /usr/local/bin/node ~/src/proj4 /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
88722 23 /usr/local/bin/node ~/src/proj5 /usr/local/bin/node ~/src/gemini-cli/bundle/gemini.js
86950 19 /usr/local/bin/node ~/devtools/ /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview
17628 8 /usr/local/bin/node ~/src/proj5 /usr/local/bin/node ~/.homebrew/bin/gemini --model gemini-3-flash-preview