3 of 4

Application DebuggingLesson 3 of 4

Crashes: core dumps, coredumpctl and gdb

Capture a crashed process with systemd-coredump, read its backtrace with gdb, and tell a crash from a clean error.

  • coredumpctl
  • coredumpctl list
  • coredumpctl info
  • coredumpctl debug
  • coredumpctl dump
  • gdb
  • gdb -batch -ex bt
  • /proc/sys/kernel/core_pattern
  • /etc/systemd/coredump.conf
  • ulimit
  • dnf debuginfo-install
  • kill -SEGV

50 min3 exercises

After this lesson you can

  • Explain what a core dump is and how RHEL decides where it goes (core_pattern and systemd-coredump).
  • List and inspect recent crashes with coredumpctl and open one in gdb.
  • Read a backtrace to name the function, source line and bad value behind a segmentation fault.

The idea

A program that dies from a signal such as SIGSEGV (it touched memory it does not own), SIGABRT (it called abort(), often after a failed assertion) or SIGBUS leaves the kernel the option to save a core dump: a copy of the process’s memory and registers at the moment of death. A core dump is not a log message; it is the frozen process. A debugger can then show which function was running, what called it, and the values of variables.

A crash and an error are different things. A program that prints an error and exits with status 1 has chosen to stop; the reason is in its output or its log. A program killed by a signal did not choose, the shell reports status 128 plus the signal number (139 for SIGSEGV), and systemctl status shows status=11/SEGV. Only the second kind produces a core dump.

How RHEL decides what happens to a dump

Four things must all permit it:

GateWhereDefault on RHEL
Size limit of the processulimit -c, /proc/PID/limits, LimitCORE= in unitsShells usually have a soft limit of 0; for services read the live value with systemctl show UNIT -p LimitCORE
Handler/proc/sys/kernel/core_pattern|/usr/lib/systemd/systemd-coredump %P %u %g %s %t %c %h
Storage and caps/etc/systemd/coredump.confStorage=external, ProcessSizeMax= and ExternalSizeMax= caps; compression on
Space/var/lib/systemd/coredump/Cleaned by systemd-tmpfiles after about three days

The leading | in core_pattern pipes the dump to a program instead of writing a core file in the working directory. systemd-coredump saves it as a compressed file, logs a journal entry with a short backtrace, and coredumpctl lets you query it.

coredumpctl list                       # recent crashes: time, PID, UID, signal, present?, size, program
coredumpctl info PROGRAM|PID           # details, including a stack trace if debug info is installed
coredumpctl debug PROGRAM|PID          # open the dump in gdb
coredumpctl dump PROGRAM -o core.file  # extract the core file itself

Reading a backtrace

The key tool is gdb:

gdb commandMeaning
btBacktrace: the chain of function calls at the crash, innermost (frame 0) first
frame NSelect a frame
listShow source code around the line
info locals, info argsVariables in the selected frame
print EXPR, print *ptrEvaluate an expression
info registers ripThe instruction that faulted

A backtrace shows symbols and source lines only when debug information is present. Compiling with -g provides it for your own code. For distribution packages, debug information is separate: dnf debuginfo-install PKG (repository *-debuginfo-rpms enabled), or gdb can fetch it on demand from Red Hat’s debuginfod server when DEBUGINFOD_URLS is set. Without it you still get function names for exported symbols, which is often enough to recognise a known bug in a package.

Try it

These outputs are real, from gdb 13 on a core file of a small C program with -g. (The machine in the test writes a plain core file; on RHEL the same file comes out of coredumpctl dump, and coredumpctl debug opens it for you.)

static int load(struct conf *c, const char *key) {
  if (strcmp(key, "name") == 0) { c->name = "labapp"; return 0; }
  return -1;           /* never sets c->name for other keys */
}
static void show(struct conf *c) { printf("%s:%d\n", c->name, c->port); }

The crash

./crash; echo "rc=$?"
Segmentation fault (core dumped)
rc=139

Status 139 is 128 + 11: killed by SIGSEGV. Now the dump:

gdb -batch -ex bt -ex 'print *c' -ex 'info registers rip' ./crash core
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000056031214720f in show (c=0x7ffc2143de60) at crash.c:8
#0  0x000056031214720f in show (c=0x7ffc2143de60) at crash.c:8
#1  0x000056031214727b in main () at crash.c:12
$1 = {name = 0x0, port = 0}
rip            0x56031214720f      0x56031214720f <show+60>

Read it from the top. Program terminated with signal SIGSEGV confirms the kind of failure. Frame 0 is show at crash.c:8, called from main at crash.c:12. The structure *c contains name = 0x0: a NULL pointer, and show used it in printf. The cause is upstream: load(&c, "port") returned -1 without setting name, and nobody checked the result. A fix is in the program, not in the system, but the diagnosis (and the evidence to report to the vendor or developer) came entirely from the dump.

On RHEL with coredumpctl

coredumpctl list --no-pager | tail -2
coredumpctl info crash --no-pager | sed -n 1,18p
TIME                          PID  UID  GID SIG     COREFILE EXE             SIZE
Wed 2026-11-04 12:21:09 +03  4410    0    0 SIGSEGV present  /tmp/crash     34.2K
           PID: 4410 (crash)
           UID: 0 (root)
        Signal: 11 (SEGV)
     Timestamp: Wed 2026-11-04 12:21:09 +03 (12s ago)
  Command Line: ./crash
    Executable: /tmp/crash
       Storage: /var/lib/systemd/coredump/core.crash.0.....zst (present)
       Message: Process 4410 (crash) of user 0 dumped core.
                Stack trace of thread 4410:
                #0  0x000055f1... show (/tmp/crash + 0x120f)
                #1  0x000055f1... main (/tmp/crash + 0x127b)

(Representative output.) The column COREFILE says present while the file is still on disk and missing once it has been cleaned up. If you need a dump later, copy it: coredumpctl dump crash -o /root/crash.core.

A crash is also a service event

Because the kernel hands the dump to systemd, the crash appears in the journal even if the application kept no log:

journalctl -u myapp.service -b --no-pager | tail
systemctl status myapp.service
myapp.service: Main process exited, code=dumped, status=11/SEGV
myapp.service: Failed with result 'core-dump'.

code=dumped and Result: core-dump mark a crash. With Restart=on-failure the service will start again; repeated crashes show as a loop of these lines, so count them (coredumpctl list shows how many).

When there is no dump

CauseCheckFix
Core limit is 0grep core /proc/PID/limitsLimitCORE=infinity in a unit drop-in; ulimit -c unlimited in a shell
Handler is not systemd-coredumpcat /proc/sys/kernel/core_patternInstall the package providing it and restart; do not point it at a world-writable path
Dump larger than the caps/etc/systemd/coredump.confRaise ProcessSizeMax= and ExternalSizeMax=
Disk fulldf -h /varFree space; coredumpctl also has a KeepFree= setting
Privilege-dropping or setuid programsysctl fs.suid_dumpableValue 2 allows dumps, but protects them from ordinary users
The process was killed by SIGKILLstatus=9/KILLSIGKILL produces no dump; look for the OOM killer or a human
Container or PID namespacecoredumpctl works on the host for host-visible processes

Going further

Sensitive data. A core dump contains everything the process had in memory: keys, passwords, session data. Treat dumps like secrets. They are stored root-only, and Storage=none in coredump.conf turns storage off for machines where that is unacceptable. Beyond the exam: gdb can attach to a live process (gdb -p PID) to take a backtrace without stopping the service permanently (gcore PID writes a core of a running process), valgrind finds invalid memory use before it becomes a crash, and abrt may be installed to report crashes to Red Hat automatically on systems that use it.

Practice

0 of 3 done

Do these on your own system. They get harder. Check each result, then tick it off.

  1. Warm-up

    Crash a process and find the record

    Start sleep 300 &, kill it with signal 11 (kill -SEGV), then use coredumpctl to show that the crash was recorded: the program, the signal, the time and the size of the dump. Print the value of kernel.core_pattern and explain what it means.

    Check your result
    • coredumpctl list shows a line for sleep with signal SIGSEGV (11).
    • cat /proc/sys/kernel/core_pattern begins with |/usr/lib/systemd/systemd-coredump.
    • coredumpctl info sleep prints Signal: 11 (SEGV) and a Storage: line.
    Hint

    The leading | in core_pattern means "pipe the dump to this program". On RHEL that program is systemd-coredump, which stores dumps in /var/lib/systemd/coredump and logs the event in the journal.

    Solution
    sleep 300 & PID=$!
    kill -SEGV $PID
    sleep 1
    coredumpctl list --no-pager | tail -3
    coredumpctl info sleep --no-pager | sed -n 1,20p
    cat /proc/sys/kernel/core_pattern
  2. Practice

    Why did it segfault?

    Setup: save, compile with debug information, and run:

    cat > /tmp/crash.c <<'EOT'
    #include <stdio.h>
    #include <string.h>
    struct conf { char *name; int port; };
    static int load(struct conf *c, const char *key) {
      if (strcmp(key, "name") == 0) { c->name = "labapp"; return 0; }
      return -1;
    }
    static void show(struct conf *c) { printf("%s:%d\n", c->name, c->port); }
    int main(void) {
      struct conf c = {0};
      load(&c, "port");
      show(&c);
      return 0;
    }
    EOT
    gcc -g -O0 -o /tmp/crash /tmp/crash.c && /tmp/crash

    Open the resulting core dump in gdb (through coredumpctl debug, or directly with the core file) and report: the function and line where it crashed, the full call chain, and the value that caused it. State the root cause in one sentence.

    Check your result
    • The backtrace shows #0 ... in show (c=...) at crash.c:8 and #1 ... in main () at crash.c:12.
    • print *c gives {name = 0x0, port = 0}.
    • Your sentence names the NULL name pointer: load() did not set it for the key "port" and show() printed it without checking.
    Hint

    coredumpctl debug crash starts gdb on the newest matching dump. Inside gdb: bt (backtrace), frame 0, info locals, print *c, list. Compiling with -g gives source lines; without it you see only addresses and function names.

    Solution
    /tmp/crash; echo "rc=$?"          # 139 = 128 + 11 (SIGSEGV)
    coredumpctl list --no-pager | tail -2
    coredumpctl debug crash -A "-batch -ex bt -ex 'print *c' -ex 'frame 1' -ex 'info locals'"
  3. Challenge

    The service crashed, but there is no dump

    A daemon crashes with SIGSEGV (systemctl status says status=11/SEGV) but coredumpctl list shows nothing for it. Write the checks you would make, in order, and the configuration lines that fix each cause: (1) core size limit zero; (2) systemd-coredump not the handler or not installed; (3) storage limit or size cap in coredump.conf; (4) disk full in /var/lib/systemd/coredump; (5) the process dropped privileges or is setuid, which the kernel dumps differently (fs.suid_dumpable). Then verify the fix by crashing a test service started with systemd-run.

    Check your result
    • You check grep core /proc/PID/limits or systemctl show UNIT -p LimitCORE, and raise with LimitCORE=infinity in a drop-in.
    • You check cat /proc/sys/kernel/core_pattern and that rpm -q systemd-udev / the systemd-coredump unit exists.
    • You check /etc/systemd/coredump.conf for Storage=, ProcessSizeMax= and ExternalSizeMax=.
    • A test unit crashed by kill -SEGV appears in coredumpctl list.
    Hint

    Four independent gates: the limit (RLIMIT_CORE), the handler (core_pattern), the size caps (ProcessSizeMax, ExternalSizeMax), and space (df -h /var). A dump is silently skipped when any gate stops it.

    Solution
    systemd-run --unit=crashme --property=LimitCORE=infinity sleep 300
    sleep 1; sudo kill -SEGV $(systemctl show crashme -p MainPID --value)
    sleep 1; coredumpctl list --no-pager | tail -2
    cat /proc/sys/kernel/core_pattern
    grep -vE '^\s*(#|$)' /etc/systemd/coredump.conf
    df -h /var/lib/systemd/coredump
    systemctl show crashme -p LimitCORE