Live workaround for Discord (and any Electron app's) Xorg-client leak per
electron/electron#2922:
every desktop notification leaks one Xorg client connection (~10 yr unfixed
upstream). On this machine Discord drips ~13 leaks/h from its main browser
process; once Xorg's 256-client table fills, all new X clients on the
system start failing with Maximum number of clients reached.
discord-x-reap reaps the leaked connections without restarting Discord
(so voice calls aren't dropped). It identifies the orphaned X11 socket file
descriptors in Discord's main browser process and uses gdb to inject
shutdown(fd, SHUT_RDWR) on each. Xorg sees EOF on its side and frees the
client slot.
# Safe to run any time. Defaults: keep 10 oldest legit conns, close the rest.
./discord-x-reap
# Show what would be closed, do nothing — read-only, no gdb attach.
./discord-x-reap --dry-run
# Safety test: close only the single newest candidate.
./discord-x-reap --one
# Tune how many oldest connections are treated as "legit Chromium startup".
./discord-x-reap --keep 12No sudo needed. ss -x (without -p) shows all unix sockets — including
the X server's — to any user; we don't need the per-process attribution that
would require root because we read /proc/$pid/fd directly (same uid as
Discord). gdb attach to a same-uid process works without sudo because
kernel.yama.ptrace_scope=0 is set on this box.
Logs to ~/.cache/discord-x-reap.log (timestamp, pid, mode, before/after
client counts, fds closed).
- Find Discord's main browser pid — the one whose cmdline contains
discord/Discordand has no--type=arg (which identifies zygote/renderer/utility/GPU children). - Snapshot the set of X11 client-side socket inodes from
/usr/bin/ss -xp's view of/tmp/.X11-unix/X0(peer inodes in* Nnotation). - Attach gdb to Discord — this suspends the process. Everything below
happens while Discord is paused, so the fd→inode mapping can't change
under us between enumeration and the
shutdown()call (TOCTOU-safe). - Inside the gdb session a Python helper enumerates
/proc/$pid/fd, intersects with the X11-inode set from step 2, sorts ascending by inode, and keeps the N oldest (default 10 — Hereket measured 7 for legit Chromium startup; 10 is a safety margin). The rest are leaked. - The helper then calls
(int) shutdown(fd, 2)(SHUT_RDWR) on each leaked fd viagdb.execute(), counting how many returned 0 vs an error code. Failed calls are reported with the fd and inode for diagnosis. - gdb detaches; Discord resumes. The script verifies the X server's client count actually dropped and exits non-zero if it didn't.
- Triggers Xorg to see EOF and free its client slot — the actually-scarce resource (Xorg's 256-client table, not Discord's per-process fd limit which is 64k+).
- Leaves Discord's fd table entry intact, so the fd number can't be reassigned to something unrelated while code that still thinks it's an X connection might touch it.
- If Discord ever does touch a reaped fd, it gets a clean
EPIPE/ECONNRESETrather than reading garbage from an unrelated socket.
The Electron #2922 pattern is the friendly case: leaks happen because the
in-memory Display* / xcb_connection_t* reference is dropped on the
floor after a transient operation (notification path). The kernel fd is
orphaned — no Discord code holds a pointer to anything that references it.
Closing it externally has nothing to break.
We additionally keep the oldest 10 connections untouched, which are Chromium's legitimate startup connections (window manager, input, GLX/rendering, plus the 4 idle ones it always opens — see Hereket's analysis at https://hereket.com/posts/monitoring-raw-x11-communication/).
./discord-x-reap --dry-run # confirm candidate count makes sense (~117 - 10 = ~107)
./discord-x-reap --one # close just one; verify Discord still responsive
# and `ss -x | grep -c X11-unix` dropped by 1
./discord-x-reap # full reapIf anything goes wrong, the fallback is ~/bin/discord-noupdate which
killalls Discord and relaunches it with the existing --disable-gpu flags.
- It only fixes the current leak, not the cause. Discord will keep leaking from new notifications. Best run periodically — e.g. via a systemd user timer every 30 min (see "Stretch" below).
- Requires
kernel.yama.ptrace_scope=0(already set on this box at/etc/sysctl.d/10-ptrace.conf). On a stricter system you'd needsudo gdband a different invocation. - Generalises to other Electron apps suffering the same bug (Slack etc.) by tweaking the pid-discovery step.
# ~/.config/systemd/user/discord-x-reap.service
[Service]
Type=oneshot
ExecStart=%h/junkcode/discord/discord-x-reap
# ~/.config/systemd/user/discord-x-reap.timer
[Timer]
OnBootSec=10min
OnUnitActiveSec=30min
[Install]
WantedBy=timers.targetsystemctl --user enable --now discord-x-reap.timer once. With ~13 leaks/h
and a 30-min cadence, Discord's slot usage stays bounded at ~10 (legit) +
~7 (half-hour drip) ≈ 17, comfortably under 256 indefinitely without
restarting Discord.