Skip to content

gamescopereaper's CloseAllFds() closes fds it doesn't own, silently breaking LD_PRELOAD-ed libraries (e.g. gamemode, when launched as gamemoderun gamescope ...) #2422

Closed

Description

@Frankenpi

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 else

This 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

  1. Steam launch options: gamemoderun gamescope -W 2560 -H 1440 -r 360 -f -- mangohud %command% (note gamemoderun wraps gamescope, not the inner command).
  2. Launch any game this way with gamemoded running.
  3. Let the game exit normally.
  4. Before FeralInteractive/gamemode#575 lands: gamescopereaper SIGABRTs. After it lands: gamescopereaper exits cleanly, but the UnregisterGame D-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


Filed by Claude Sonnet 5 via Claude Code.

Activity

  1. connormclaud commented on Oct 9, 2026

    @connormclaud

    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 to gamemoderun gamescopereaper -- true) reliably reproduces the crash on exit:

    dbus[26391]: arguments to dbus_pending_call_block() were incorrect, assertion "pending != NULL" failed in file ../../dbus/dbus-pending-call.c line 768.
    This is normally a bug in some application using the D-Bus library.
    
      D-Bus not built with -rdynamic so unable to print a backtrace
    

    Here is the GDB backtrace from gamescopereaper on exit:

    Program received signal SIGABRT, Aborted.
    #0  __pthread_kill_implementation (threadid=<optimized out>, signo=6, no_tid=0) at ./nptl/pthread_kill.c:44
    #1  __pthread_kill_internal (threadid=<optimized out>, signo=6) at ./nptl/pthread_kill.c:89
    #2  __GI___pthread_kill (threadid=<optimized out>, signo=signo@entry=6) at ./nptl/pthread_kill.c:100
    #3  __GI_raise (sig=sig@entry=6) at ../sysdeps/posix/raise.c:26
    #4  __GI_abort () at ./stdlib/abort.c:77
    #5  libdbus-1.so.3
    #6  _dbus_warn_check_failed () from /usr/lib/x86_64-linux-gnu/libdbus-1.so.3
    #7  libgamemode.so.0
    #8  gamemode_request_end () from /usr/lib/x86_64-linux-gnu/libgamemodeauto.so.0
    #9  _dl_call_fini (closure_map=closure_map@entry=0x7ffff7fb6170) at ./elf/dl-call_fini.c:43
    #10 _dl_fini () at ./elf/dl-fini.c:120
    #11 __run_exit_handlers (status=0, ...) at ./stdlib/exit.c:118
    #12 __GI_exit (status=<optimized out>) at ./stdlib/exit.c:148
    #13 __libc_start_call_main (...) at ../sysdeps/nptl/libc_start_call_main.h:83
    #14 __libc_start_main_impl (...) at ../csu/libc-start.c:360
    #15 _start ()
    

    As diagnosed, gamescopereaper's main() calls Process::CloseAllFds( nExcludedFds ) near startup, closing the session D-Bus socket descriptor opened by libgamemodeauto.so.0's constructor. When gamescopereaper exits, _dl_fini executes gamemode_request_end(), which trips the D-Bus assertion failure on the closed socket.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions