]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdb/testsuite: add test for corefile with no threads
authorAndrew Burgess <aburgess@redhat.com>
Sun, 23 Nov 2025 16:30:26 +0000 (16:30 +0000)
committerAndrew Burgess <aburgess@redhat.com>
Wed, 4 Feb 2026 11:53:10 +0000 (11:53 +0000)
This commit adds a test for the recent commit:

  commit af7fe6fff91a61f30f9adddc7ff2f8852dc6482a
  Date:   Fri Nov 21 14:38:01 2025 +0100

      [gdb/corefiles] Fix segfault in add_thread_silent

The previous commit fixed an issue where BFD was failing to parse the
NT_PRSTATUS notes in a core file.  As a result of this the general
purpose registers were not available to GDB, and as a result GDB would
not add any threads to the inferior when opening a core file.

As GDB requires each inferior to have at least one thread, there is
code in GDB to handle this no-registers case, and add a dummy thread
to the inferior.  However, a bug in this code was causing GDB to
crash.  It is this crash that the above commit fixed.

This commit adds a test for this fix.

The test contains a Python script which adds a new Python command to
GDB.  This Python command can be used to modify an on-disk core file.
The command opens the core file, locates the notes segments, and then
looks through the notes to find NT_PRSTATUS notes.  The type value for
these NT_PRSTATUS notes is then changed to a value that GDB doesn't
understand (0xffffffff).

Now when GDB opens the core file the NT_PRSTATUS are no longer
NT_PRSTATUS notes; the core file will appear to have no such notes,
and the previous bug will be triggered.

Running this new test with GDB 16 will trigger the crash.  With
current HEAD of master, which includes the above commit, this test
should pass.

I originally wrote the Python script in this test as a standalone
Python script, not as a GDB command, but some of the gcc compiler farm
test machines still have Python 2 as the default.  This means that the
corefile-no-threads.exp script would have to figure out which Python
executable to use to run the script.

Much easier to just use GDB's builtin Python interpreter.  We know
that this will be at least Python 3.4 (GDB's minimum Python
requirement).

I've tested this on x86-64 in 64-bit and 32-bit mode.  And on a big
endian PPC64 machine.

gdb/testsuite/gdb.base/corefile-no-threads.c [new file with mode: 0644]
gdb/testsuite/gdb.base/corefile-no-threads.exp [new file with mode: 0644]
gdb/testsuite/gdb.base/corefile-no-threads.py [new file with mode: 0644]

diff --git a/gdb/testsuite/gdb.base/corefile-no-threads.c b/gdb/testsuite/gdb.base/corefile-no-threads.c
new file mode 100644 (file)
index 0000000..bdcc34e
--- /dev/null
@@ -0,0 +1,32 @@
+/* Copyright (C) 2025-2026 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+#include <stdlib.h>
+
+static int crashfunc_global = 4321;
+
+static int
+crashfunc (void)
+{
+  abort ();
+  return crashfunc_global;
+}
+
+int
+main (void)
+{
+  int ret = crashfunc ();
+  return ret;
+}
diff --git a/gdb/testsuite/gdb.base/corefile-no-threads.exp b/gdb/testsuite/gdb.base/corefile-no-threads.exp
new file mode 100644 (file)
index 0000000..dfb2f80
--- /dev/null
@@ -0,0 +1,81 @@
+# Copyright 2025-2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Check how GDB handles a core file which appears to have no threads
+# within it.  We do this by asking the kernel to create a "normal"
+# core file, then use a Python script modify the core file, hiding the
+# NT_PRSTATUS notes.  The NT_PRSTATUS notes contain the general
+# purpose registers for each thread, so with these gone GDB will
+# believe the core file has no threads.
+#
+# In this situation, GDB should create a single dummy thread.  There's
+# not much a user can do to debug such a core file, with no
+# NT_PRSTATUS the general registers will be missing, which means no
+# $pc value, no backtrace, etc.
+#
+# The critical thing though, is that GDB doesn't crash.  That would be
+# bad.
+
+require allow_python_tests
+
+standard_testfile
+
+set pyfile [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+if {[build_executable "build executable" $testfile $srcfile] == -1} {
+    return
+}
+
+set generated_core [core_find $binfile]
+if {$generated_core == ""} {
+    untested "unable to create corefile"
+    return
+}
+set corefile [standard_output_file $testfile.core]
+remote_exec build "mv $generated_core $corefile"
+
+# Now try loading the core file.
+clean_restart
+
+# Load the Python script, and run the command which modifies the core
+# file.
+gdb_test_no_output "source $::pyfile" "import python scripts"
+gdb_test "modify-core-file \"$corefile\" 0x1" ".*" \
+    "update core file"
+
+set saw_no_regs_warning false
+set saw_generated_by_line false
+gdb_test_multiple "core-file $corefile" "load core file" {
+    -re "Core was generated by `\[^\r\n\]+$testfile'\\.\r\n" {
+       set saw_generated_by_line true
+       exp_continue
+    }
+
+    -re "^warning: Couldn't find general-purpose registers in core file\\.\r\n" {
+       set saw_no_regs_warning true
+       exp_continue
+    }
+
+    -re "^$gdb_prompt $" {
+       gdb_assert { $saw_no_regs_warning && $saw_generated_by_line } \
+           $gdb_test_name
+    }
+
+    -re "^\[^\r\n\]+\r\n" {
+       exp_continue
+    }
+}
+
+gdb_test "p 1 + 1" " = 2" "gdb is still alive"
diff --git a/gdb/testsuite/gdb.base/corefile-no-threads.py b/gdb/testsuite/gdb.base/corefile-no-threads.py
new file mode 100644 (file)
index 0000000..ad53de2
--- /dev/null
@@ -0,0 +1,369 @@
+# Copyright 2025-2026 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Add a new GDB command which modifies a core file on disk prior to
+# GDB loading it.  Usage:
+#
+# modify-core-file <corefile> <note type> [ <name regex> ]
+#
+# Find all notes in the core file <corefile> that have the type value
+# <note type>.  Change the type of those notes to 0xffffffff, which
+# should prevent GDB from processing the note.  The <corefile> is
+# modified in place.
+#
+# The optional argument <name regex> is a regular expression to match
+# against the name of the note in addition to the type value check.
+# Only notes whose name matches <name regex> will be changed.
+#
+# If <name regex> is missing, or is the empty string, then any note
+# with the required type will be modified.
+
+import sys
+import struct
+import os
+import re
+
+# Constants taken from the ELF spec.
+ELF_CLASS_32 = 0x1
+ELF_CLASS_64 = 0x2
+ELF_DATA_2_LSB = 0x1
+ELF_DATA_2_MSB = 0x2
+ELF_VERSION_CURRENT = 0x1
+ELF_TYPE_CORE = 0x4
+
+# Program Header Type for notes.
+PT_NOTE = 0x4
+
+
+# Helper function to read a single value from DATA at byte OFFSET.
+# The FMT specifies the format of the data (see struct.unpack).  A
+# single value is extracted and returned.
+def read_field(data, offset, fmt):
+    size = struct.calcsize(fmt)
+    if offset + size > len(data):
+        raise ValueError(
+            "Read operation at 0x{:x}, length {}, exceeds file boundaries.".format(
+                offset, size
+            )
+        )
+    return struct.unpack(fmt, data[offset : offset + size])[0]
+
+
+# Helper function to read the name of a note from DATA.  The name
+# string starts at OFFSET and is SIZE bytes long.  The last byte of
+# the name should be the NULL character.
+#
+# Return the name (excluding the NULL character).  Raises a ValueError
+# if anything goes wrong.
+def read_note_name(data, offset, size):
+    if offset + size > len(data):
+        raise ValueError(
+            "Read operation at {:x}, length {}, exceeds file boundaries.".format(
+                offset, size
+            )
+        )
+    name = ""
+    for i in range(size):
+        b = struct.unpack("=b", data[offset + i : offset + i + 1])[0]
+        if i == size - 1:
+            if b != 0:
+                raise ValueError(
+                    "Last byte of name for note at 0x{:x} is not NULL".format(offset)
+                )
+        else:
+            name = name + chr(b)
+    return name
+
+
+# Open core file CORE_FILEPATH and find all the PT_NOTE segments.
+# Within these segments find the notes with type value TYPE_VAL and
+# change the type of these notes to 0xffffffff, which should mean that
+# BFD/GDB doesn't process them.
+#
+# If NAME_RE is not None, then it is a regexp that must match against
+# the note's name in order for the note to be modified, this regexp
+# check is in addition to the type check.  If NAME_RE is None, then
+# the note's name is ignored, only TYPE_VAL is checked.
+def invalidate_corefile_notes(core_filepath, type_val, name_re):
+    # Value used as the replacement note type.
+    CORRUPT_TYPE = 0xFFFFFFFF
+
+    # Open the core file and read it into one huge data block.  This
+    # will be fine so long as the core file isn't too large.
+    try:
+        with open(core_filepath, "r+b") as f:
+            core_data = bytearray(f.read())
+    except FileNotFoundError:
+        raise gdb.GdbError("Error: File not found at '%s'" % (core_filepath))
+    except Exception as e:
+        raise gdb.GdbError("Error reading file: %s" % (str(e)))
+
+    file_size = len(core_data)
+
+    # Confirm that this is an ELF.
+    elf_magic = [0x7F, ord("E"), ord("L"), ord("F")]
+    for idx, magic_value in enumerate(elf_magic):
+        v = read_field(core_data, idx, "=b")
+        if v != magic_value:
+            raise gdb.GdbError(
+                "Unexpected magic ELF header value at offset %d: %d vs %d"
+                % (idx, magic_value, v)
+            )
+
+    # Check that the ELF is of a format that we can understand.  The
+    # restrictions encoded here could be relaxed by making this script
+    # smarter.
+    ei_class = read_field(core_data, 4, "=b")
+    ei_data = read_field(core_data, 5, "=b")
+    ei_version = read_field(core_data, 6, "=b")
+
+    # Based on the endinanness of the core file, select a character to
+    # use with the 'struct' module for unpacking multi-byte fields.
+    if ei_data == ELF_DATA_2_LSB:
+        endian_char = "<"
+    elif ei_data == ELF_DATA_2_MSB:
+        endian_char = ">"
+    else:
+        raise gdb.GdbError("Unsupported ELF data %d" % (ei_data))
+
+    # Based on the ELF class, setup some constants.  We define some
+    # offsets into the ELF header, and into the program header
+    # structure.  We setup some format strings used with the 'struct'
+    # module for unpacking bytes from the ELF.  And we define the size
+    # of the program header structure.  All of these constants are
+    # based on the ELF specification, and were created using the
+    # structures found in include/elf/external.h.
+    #
+    # Note: ELF_NOTE_WORD_FMT is the same for 32 and 64 bit ELFs as
+    # every part of the note header is a 4-byte word.
+    if ei_class == ELF_CLASS_32:
+        # Structure offsets.
+        ELF_HDR_TYPE_OFFSET = 0x10
+        ELF_HDR_PHOFF_OFFSET = 0x1C
+        ELF_HDR_PHNUM_OFFSET = 0x2C
+        PHDR_OFFSET_OF_OFFSET_FIELD = 0x04
+        PHDR_OFFSET_OF_FILESZ_FIELD = 0x10
+
+        # Format specifiers for unpacking fields.
+        ELF_HALF_FMT = endian_char + "H"
+        ELF_WORD_FMT = endian_char + "I"
+        ELF_OFF_FMT = endian_char + "I"
+        ELF_ADDR_FMT = ELF_OFF_FMT
+
+        ELF_NOTE_WORD_FMT = endian_char + "I"
+
+        # Program Header size assuming 32-bit ELF.
+        PHDR_SIZE = 32
+    elif ei_class == ELF_CLASS_64:
+        # Structure offsets.
+        ELF_HDR_TYPE_OFFSET = 0x10
+        ELF_HDR_PHOFF_OFFSET = 0x20
+        ELF_HDR_PHNUM_OFFSET = 0x38
+        PHDR_OFFSET_OF_OFFSET_FIELD = 0x08
+        PHDR_OFFSET_OF_FILESZ_FIELD = 0x20
+
+        # Format specifiers for unpacking fields.
+        ELF_HALF_FMT = endian_char + "H"
+        ELF_WORD_FMT = endian_char + "I"
+        ELF_OFF_FMT = endian_char + "Q"
+        ELF_ADDR_FMT = ELF_OFF_FMT
+
+        ELF_NOTE_WORD_FMT = endian_char + "I"
+
+        # Program Header size assuming 64-bit ELF.
+        PHDR_SIZE = 56
+    else:
+        raise gdb.GdbError("Unsupported ELF class %d" % (ei_class))
+
+    if ei_version != ELF_VERSION_CURRENT:
+        raise gdb.GdbError("Unsupported ELF version %d" % (ei_version))
+
+    # Read the e_type field and check this is a core file.
+    e_type = read_field(core_data, ELF_HDR_TYPE_OFFSET, ELF_HALF_FMT)
+    if e_type != ELF_TYPE_CORE:
+        raise gdb.GdbError("Unsuported ELF e_type %d" % (e_type))
+
+    # Read the offset to the program header table, and the number of
+    # entries in the program header table.
+    e_phoff = read_field(core_data, ELF_HDR_PHOFF_OFFSET, ELF_OFF_FMT)
+    e_phnum = read_field(core_data, ELF_HDR_PHNUM_OFFSET, ELF_HALF_FMT)
+
+    # Iterate through the program header table looking for and
+    # segments with type NOTE.  Record the offset and size of those
+    # NOTE segments.
+    note_segments = []
+
+    for i in range(e_phnum):
+        phdr_offset = e_phoff + i * PHDR_SIZE
+
+        # First word of the program header entry is the type.
+        p_type = read_field(core_data, phdr_offset, ELF_WORD_FMT)
+
+        if p_type == PT_NOTE:
+            # Read the file offset and file size.  The offsets used
+            # here are valid for 64-bit ELF only.
+            p_offset = read_field(
+                core_data, phdr_offset + PHDR_OFFSET_OF_OFFSET_FIELD, ELF_ADDR_FMT
+            )
+            p_filesz = read_field(
+                core_data, phdr_offset + PHDR_OFFSET_OF_FILESZ_FIELD, ELF_ADDR_FMT
+            )
+            note_segments.append((p_offset, p_filesz))
+
+    # Maybe there were no NOTE segments?
+    if len(note_segments) == 0:
+        gdb.warning("No PT_NOTE segments founds")
+        return
+
+    # Iterate through the notes within the PT_NOTE segment.
+    for note_start_offset, note_segment_size in note_segments:
+        print(
+            "Located PT_NOTE segment: Offset 0x%x, Size 0x%x"
+            % (note_start_offset, note_segment_size)
+        )
+
+        current_offset = note_start_offset
+        end_offset = note_start_offset + note_segment_size
+        if end_offset > file_size:
+            end_offset = file_size
+        count = 0
+
+        # This is the size of the first 3 fields within a note; the
+        # namesz, descsz, and type.  These fields are all 4 bytes,
+        # even for a 64-bit ELF.
+        NOTE_HEADER_SIZE = 12
+
+        while current_offset <= end_offset - NOTE_HEADER_SIZE:
+            # Read namesz, descsz, and type.  These fields are all
+            # 32-bit, even for 64-bit ELF format, as a result, the
+            # offsets are all hard-coded here.
+            namesz = read_field(core_data, current_offset, ELF_NOTE_WORD_FMT)
+            descsz = read_field(core_data, current_offset + 4, ELF_NOTE_WORD_FMT)
+            note_type = read_field(core_data, current_offset + 8, ELF_NOTE_WORD_FMT)
+
+            is_matching_note = note_type == type_val
+
+            if is_matching_note and name_re is not None:
+                name_str = read_note_name(core_data, current_offset + 12, namesz)
+                if not re.match(name_re, name_str):
+                    is_matching_note = False
+
+            # Check if this note type is of the required type.
+            if is_matching_note:
+                print(
+                    "  Found note with type 0x%x at file offset 0x%x."
+                    % (type_val, current_offset)
+                )
+
+                # Overwrite the 4 bytes starting at the type field offset (current_offset + 8)
+                type_offset = current_offset + 8
+                corrupted_bytes = struct.pack(ELF_NOTE_WORD_FMT, CORRUPT_TYPE)
+                core_data[type_offset : type_offset + 4] = corrupted_bytes
+                count += 1
+
+            # Move to the next note record, accounting for 4-byte alignment.
+
+            # Advance past header, this is namesz, descsz, and type.
+            next_offset = current_offset + NOTE_HEADER_SIZE
+
+            # Advance past name field, aligned up to the next 4-byte boundary.
+            next_offset += (namesz + 3) & ~0x3
+
+            # Advance past descriptor field, aligned up to the next 4-byte boundary.
+            next_offset += (descsz + 3) & ~0x3
+
+            # We've now found the next note entry.  Or we're at the
+            # end of the segment.  The while loop condition should
+            # figure this out for us.
+            current_offset = next_offset
+
+        if count > 0:
+            # Write the modified bytearray back to the file.
+            try:
+                with open(core_filepath, "wb") as f:
+                    f.write(core_data)
+                    print(
+                        "Successfully updated %d note(s) in '%s'."
+                        % (count, core_filepath)
+                    )
+            except Exception as e:
+                raise gdb.GdbError("Error writing to file: %s" % (str(e)))
+        else:
+            gdb.warning(
+                "No notes with type 0x%x found within the segment. File was not modified."
+                % (type_val)
+            )
+
+
+class modify_core_file(gdb.Command):
+    """Update notes within a core file.
+
+    Usage:
+    modify-core-file COREFILE NOTE_TYPE [ NAME_REGEX ]
+
+    Within COREFILE, find any notes matching NOTE_TYPE, which should
+    be an integer.  Change the type of these notes to 0xffffffff.
+
+    If NAME_REGEX is supplied, and is not the empty string, then only
+    notes whose type matches NOTE_TYPE, and whose name matches
+    NAME_REGEX, are modified."""
+
+    def __init__(self):
+        gdb.Command.__init__(self, "modify-core-file", gdb.COMMAND_USER)
+
+    def invoke(self, args, from_tty):
+        argv = gdb.string_to_argv(args)
+
+        if len(argv) != 2 and len(argv) != 3:
+            raise gdb.GdbError(
+                "Invalid argument count.  Usage modify-core-file COREFILE TYPE [ REGEX ]"
+            )
+
+        filename = argv[0]
+        type_str = argv[1]
+        if len(argv) == 3:
+            name_re_str = argv[2]
+        else:
+            name_re_str = ""
+
+        if not os.path.isfile(filename):
+            raise gdb.GdbError("Error: File '%s' doesn't exist." % (filename))
+
+        if not os.access(filename, os.R_OK):
+            raise gdb.GdbError(
+                "Error: Cannot read '%s'. Check file permissions." % (filename)
+            )
+
+        if not os.access(filename, os.W_OK):
+            raise gdb.GdbError(
+                "Error: Cannot write to '%s'. Check file permissions." % (filename)
+            )
+
+        try:
+            type_val = int(type_str, 0)
+        except ValueError as e:
+            raise gdb.GdbError(
+                "Error: Unable to parse '%s' as number: %s" % (type_str, str(e))
+            )
+
+        if name_re_str != "":
+            name_re = re.compile(name_re_str)
+        else:
+            name_re = None
+
+        invalidate_corefile_notes(filename, type_val, name_re)
+
+
+modify_core_file()