Repository navigation
gamescopereaper's CloseAllFds() closes fds it doesn't own, silently breaking LD_PRELOAD-ed libraries (e.g. gamemode, when launched as gamemoderun gamescope ...) #2422
Description
What happened
Launching gamescope itself under gamemoderun (i.e. gamemoderun gamescope -- ... %command%, rather than wrapping just the game inside gamescope's --) causes gamescopereaper to silently close a file descriptor it doesn't own: the D-Bus session-bus socket opened by a preloaded library's constructor.
The visible symptom on my system was gamescopereaper SIGABRT-ing every time a game exited (46 times over ~7 weeks, identical stack each time) — but that turned out to be a separate bug in gamemode's D-Bus error handling (FeralInteractive/gamemode#347, #577, fixed in FeralInteractive/gamemode#575, currently open and mergeable). Once that fix lands, gamescopereaper will stop crashing — but it will still be closing a socket that a library it didn't ask to load into itself is actively using. That seems worth tracking independently of gamemode's crash, since the next library preloaded this way may not fail as loudly as gamemode did.
Root cause
gamemoderun sets LD_PRELOAD=libgamemodeauto.so.0. Because gamescopereaper is the process gamemoderun actually execs when it's placed before gamescope on the command line, that preload lands on gamescopereaper itself, not just on the wrapped game. The preloaded library's constructor opens a session D-Bus connection (unix:path=/run/user/1000/bus) and registers gamescopereaper's own PID with gamemoded before gamescopereaper's own main() runs.
Later, in gamescope/src/Apps/gamescopereaper.cpp, near the top of main():
Process::ResetSignals();
std::array<int, 3> nExcludedFds = {{ STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO }};
Process::CloseAllFds( nExcludedFds ); // walks /proc/self/fd, closes everything elseThis walks /proc/self/fd and closes everything gamescopereaper doesn't recognize as stdio — including the D-Bus socket the preloaded library opened before gamescopereaper's own code ever ran. gamescopereaper has no way to know that fd exists or that something else in its own address space depends on it; it's closed unconditionally.
The library only finds out its connection is dead the next time it tries to use it — in gamemode's case, when its _dl_fini destructor calls UnregisterGame on process exit, 14 minutes later in my reproduction. Until gamemode's fix lands, that surfaces as SIGABRT; even after the fix, it means gamemode's cleanup on exit silently fails (the daemon never gets told the game exited, and cleans up the stale registration on its own timeout instead).
Reproduction
- Steam launch options:
gamemoderun gamescope -W 2560 -H 1440 -r 360 -f -- mangohud %command%(notegamemoderunwrapsgamescope, not the inner command). - Launch any game this way with
gamemodedrunning. - Let the game exit normally.
- Before
FeralInteractive/gamemode#575lands:gamescopereaperSIGABRTs. After it lands:gamescopereaperexits cleanly, but theUnregisterGameD-Bus call silently fails because the socket was closed out from under it.
This does not reproduce when gamemoderun is placed inside gamescope's -- (i.e. gamescope ... -- gamemoderun mangohud %command%), because then the preload — and its D-Bus fd — lives in the game process, not in gamescopereaper, and gamescopereaper's own CloseAllFds() never touches it.
Suggested fix / discussion
I don't have a specific patch to propose — plausible directions might be marking fds FD_CLOEXEC at open time so a blanket cleanup pass doesn't need to touch them, or having CloseAllFds() respect a known-fd allowlist instead of closing everything indiscriminately — but I'd defer to whoever knows why CloseAllFds() is written to be exhaustive, since there may be a reason it's deliberately unconditional (e.g. distrust of inherited fds from the parent). Flagging mainly so the interaction with preloaded libraries is documented somewhere, since gamemoderun gamescope ... (gamemode wrapping gamescope, rather than the reverse) is a launch-option pattern that shows up in the wild.
Versions
- gamescope 3.16.28-1 (Arch/Omarchy)
- gamemode 1.8.2-3
- dbus 1.16.2-1
Related
- Should check return value of
dbus_connection_send_with_replyinlib/client_impl.cFeralInteractive/gamemode#347, Default screenshot hotkey registers keydown in game but not key up leading to temporarily stuck key in game. #577 (the resulting SIGABRT, with symbolized cores) - lib: don't abort when D-Bus send_with_reply leaves a NULL pending call FeralInteractive/gamemode#575 (the gamemode-side fix, open/mergeable)
Filed by Claude Sonnet 5 via Claude Code.
Activity
connormclaud commentedon Oct 9, 2026
Can confirm the reproduction on upstream
master(36848c2f) on Ubuntu 26.04.1 LTS (gamemode v1.8.1, libdbus 1.16.2).Running
gamemoderun gamescope -- <cmd>(or isolating directly togamemoderun gamescopereaper -- true) reliably reproduces the crash on exit:Here is the GDB backtrace from
gamescopereaperon exit:As diagnosed,
gamescopereaper'smain()callsProcess::CloseAllFds( nExcludedFds )near startup, closing the session D-Bus socket descriptor opened bylibgamemodeauto.so.0's constructor. Whengamescopereaperexits,_dl_finiexecutesgamemode_request_end(), which trips the D-Bus assertion failure on the closed socket.gamescopereaper: remove redundant in-main CloseAllFds
gamescopereaper: remove redundant in-main CloseAllFds