terminal: Fix procfs file descriptor leak in PTY process tracking (#58683)

Closes #58651

## Problem

On Linux, long-lived Zed sessions accumulate thousands of open procfs
handles (`/proc/<pid>/stat`, `/proc/<pid>/task/<tid>/stat`). A degraded
6-hour session was measured holding **9,675 FDs, 97% of them
never-closed procfs stat handles**, alongside system-wide clipboard
breakage (Wayland clipboard `send()` stretching from 13 ms to ~17 s,
which makes every other client abort the paste).

The leak comes from `PtyProcessInfo` in
`crates/terminal/src/pty_info.rs`, through three interacting sysinfo
0.37 behaviors:

1. **Hidden full-system snapshot per terminal.**
`System::new_with_specifics(RefreshKind::nothing().with_processes(…))`
immediately refreshes **all** processes. Each refreshed process gets a
`Process` entry that caches an **open `/proc/<pid>/stat` file** for the
lifetime of the entry (a documented sysinfo optimization).
2. **Task enumeration is opt-out.** `ProcessRefreshKind::nothing()`
keeps `tasks: true`, so that constructor snapshot also creates one entry
— and one open `/proc/<pid>/task/<tid>/stat` — **per thread on the
machine**, per terminal.
3. **No eviction outside the refreshed set.** The recurring refresh uses
`ProcessesToUpdate::Some(&[foreground_pid])`; sysinfo's dead-process
sweep only visits pids in that list. Every former foreground process
(one per command the user runs) therefore stays in the map forever,
pinning its FD.

sysinfo silently raises `RLIMIT_NOFILE` and budgets half of it (~262k)
for this cache, so the process never hits `EMFILE` — it just degrades.

Reproduction (standalone, sysinfo 0.37.2, Fedora 44): constructing one
`System` the way `PtyProcessInfo::new` does instantly retains **3,566
FDs**; each simulated shell command leaks one more, unbounded.

## Fix

All changes confined to `pty_info.rs`:

- `.without_tasks()` on the `ProcessRefreshKind` — eliminates the
per-thread handle class entirely.
- Replace the constructor's implicit all-process scan with
`System::new()` plus a refresh of just the spawned child, so
`kill_child_process()` still works before the first foreground refresh.
- Rebuild the `System` whenever the foreground pid changes, and refresh
`[foreground, child]` so the dead-process sweep can actually evict stale
entries. The map is now bounded at 2 entries per terminal.

With the fix, the same repro stays flat at ~6 FDs across arbitrary
command churn, and the foreground process info (name/cwd/argv for tab
titles) keeps working.

The regression test fails on `main` (3,747 retained entries on a desktop
system) and passes with the fix (≤ 2).

Note: the ~17 s clipboard `send()` on the degraded session is a coupled
symptom; the FD/process-map bloat is the verified defect fixed here. The
Wayland data-source lifecycle is tracked separately in #58446.

Release Notes:

- Fixed a file descriptor leak in terminal process tracking that
degraded long-running sessions on Linux, including system-wide clipboard
breakage ([#58651](https://github.com/zed-industries/zed/issues/58651)).

Signed-off-by: Florentin Dubois <florentin.dubois@clever.cloud>
7854e4535dFlorentin DUBOIS committed on 6/26/2026, 4:38:42 PM· committed by GitHubparent5989a36
1 file changedLine totals unavailable