Quick answer: Build with DWARF debug symbols split into a .debug file, configure your game to write crash dumps to $XDG_DATA_HOME/YourGame/crashes, handle Steam Runtime's library path sandbox, and upload pending dumps on next launch. Steam Deck works the same way with user-writable paths.

Linux is under 2% of your Steam audience but produces 10% of your useful crash reports. Linux players file detailed, technical bug reports at a rate that would make a QA lead weep with gratitude. They copy stack traces, they attach dmesg output, they can tell you which library version caused the crash. The catch is that your crash reporting has to be set up to receive their dumps in a format they can produce — which is different from everything Windows does. Here is the full setup.

Understand the Linux Crash Landscape

On Windows, a crash produces a minidump (.dmp) file via the structured exception handling system. On Linux, a crash produces an ELF core dump via the kernel. These formats are completely different, and the tools that read them are different too.

Your options for capturing crashes on Linux:

For most games, Breakpad or Crashpad is the right answer because it unifies your crash format across platforms. If you already have a pipeline that handles Windows minidumps, Breakpad lets you reuse it.

Step 1: Build With Split Debug Symbols

Never ship a binary with embedded debug symbols. It is huge, and it gives attackers visibility into your code. Build normally with -g, then extract the symbols:

# Compile with optimization and debug info
g++ -g -O2 -o mygame src/*.cpp

# Extract debug info into a separate file
objcopy --only-keep-debug mygame mygame.debug

# Strip debug info from the shipped binary
strip --strip-debug --strip-unneeded mygame

# Link the binary to its debug file (by name, for GDB)
objcopy --add-gnu-debuglink=mygame.debug mygame

# Check the result
file mygame  # should say "stripped"
readelf -S mygame.debug | head  # debug sections present

Ship mygame to players. Keep mygame.debug in your build artifacts and upload it to your crash reporting tool, indexed by the binary's build ID:

# Get the build ID
readelf -n mygame | grep "Build ID"
# Build ID: a7c3f2b9...

# Upload symbols indexed by build ID
curl -F "symfile=@mygame.debug" \
     -F "build_id=a7c3f2b9..." \
     -F "version=1.2.0" \
     https://symbols.yourcrashtool.com/upload

When a crash comes in, the tool looks up the symbols by build ID and symbolicates the dump automatically.

Step 2: Install a Crash Handler in the Game

Using Google Breakpad, a minimal crash handler looks like this:

#include "client/linux/handler/exception_handler.h"

static google_breakpad::ExceptionHandler* g_handler = nullptr;

static bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
                         void* context, bool succeeded)
{
    // Keep this callback minimal: we're in a signal handler
    return succeeded;
}

void InstallCrashHandler()
{
    const char* home = getenv("HOME");
    std::string dumpPath = std::string(home) +
        "/.local/share/MyGame/crashes";
    mkdir(dumpPath.c_str(), 0755);

    google_breakpad::MinidumpDescriptor descriptor(dumpPath);
    g_handler = new google_breakpad::ExceptionHandler(
        descriptor, nullptr, DumpCallback,
        nullptr, true, -1
    );
}

Call InstallCrashHandler() as early as possible in main(), before any other initialization. When the game crashes, Breakpad writes a minidump to the configured directory and re-raises the signal.

Step 3: Follow the XDG Base Directory Spec

Do not write crash dumps to /tmp (cleared on reboot), to /var (not writable), or to your Steam install directory (cleared on verify). Follow the XDG spec and write to $XDG_DATA_HOME:

std::string GetCrashDir() {
    const char* xdg = getenv("XDG_DATA_HOME");
    std::string base = xdg
        ? std::string(xdg)
        : std::string(getenv("HOME")) + "/.local/share";
    return base + "/MyGame/crashes";
}

This works identically on desktop Linux, Steam Deck, and inside the Steam Runtime sandbox. It survives reboots and is not cleared by Steam verify.

Step 4: Upload Dumps on Next Launch

You cannot reliably upload from a signal handler — the process may be too broken to open a socket. Instead, write the dump to disk, let the crash kill the process, and upload pending dumps the next time the game starts:

void UploadPendingCrashes() {
    std::string dir = GetCrashDir();
    for (const auto& entry : std::filesystem::directory_iterator(dir)) {
        if (entry.path().extension() == ".dmp") {
            if (UploadDump(entry.path())) {
                std::filesystem::remove(entry.path());
            }
        }
    }
}

int main(int argc, char** argv) {
    InstallCrashHandler();
    std::thread(UploadPendingCrashes).detach();
    return RunGame();
}

Run the upload on a background thread so the game launches immediately. If the player reports a crash and asks why you have not seen their dump yet, remind them that dumps upload on the next launch.

Step 5: Handle Steam Runtime

Steam Linux builds run inside the Steam Runtime, a sandboxed environment with a bundled set of libraries. The path your game sees is not the normal system path — LD_LIBRARY_PATH is rewritten, /usr/lib is overlaid with Steam's own libraries, and certain syscalls are intercepted.

Two things to test:

  1. Your crash handler library loads correctly inside the Runtime. Breakpad depends on libc symbols that must be available in the runtime. Test by launching your game through ~/.steam/steam/steamapps/common/SteamLinuxRuntime_sniper/_v2-entry-point and confirming a test crash produces a dump.
  2. Your symbol paths are correct. The binary path in the dump will be the Runtime-internal path, not the real filesystem path. Your symbolication pipeline must handle this mapping.

Step 6: Test on Steam Deck

The Steam Deck runs SteamOS with an immutable root filesystem. A few Deck-specific considerations:

Step 7: Link Crashes to Players

Include the Steam ID hash in your dump metadata so you can correlate dumps with in-game bug reports:

google_breakpad::ExceptionHandler::WriteMinidump(
    dumpPath,
    MinidumpCallback,
    new CrashContext {
        .build_version = "1.2.0",
        .steam_id_hash  = GetHashedSteamId(),
        .platform       = "linux-steam",
        .session_id     = GetSessionId(),
    }
);

This lets you join crash dumps with player-submitted reports and with your gameplay metrics, which makes triage dramatically faster.

"Linux players are the best QA you will ever have. They will even attach gdb output if you ask nicely. The only thing stopping you from using that signal is whether your crash pipeline can ingest ELF dumps. Build it."

Related Issues

For collecting logs specifically from Godot Linux builds see how to collect log files from a crashed Godot game on Linux. For Steam Deck testing strategy more broadly read how to test Steam Deck compatibility.

Linux crashes are cheaper to debug than Windows crashes because the tools are better. Take advantage.