]> git.ipfire.org Git - thirdparty/binutils-gdb.git/commitdiff
gdb/python: fix 'exited' event when GDB exits from core file debugging
authorAndrew Burgess <aburgess@redhat.com>
Thu, 4 Jun 2026 10:02:27 +0000 (11:02 +0100)
committerAndrew Burgess <aburgess@redhat.com>
Sun, 14 Jun 2026 21:03:57 +0000 (22:03 +0100)
This fixes an issue that was reported here:

  https://inbox.sourceware.org/gdb-patches/v3x4md2dg6rflq35ymzwrmmqf5uaem5exrnlbsp5dmhph2vihy@lq22ncu774yu

After commit:

  commit 3780b9993c973a2b68b496b80eddb820c0932cc0
  Date:   Fri Mar 27 11:29:07 2026 +0000

    gdb: refactor core_target ::close and ::detach functions

it was observed that the Python 'exited' event was no longer being
emitted when debugging a core file, and then exiting GDB.

The problem is that, when GDB is exiting we eventually end up in
quit_force (in top.c), which calls kill_or_detach for every inferior.

In kill_or_detach we call either target_detach or target_kill, but
only for non-core file targets.  For core file targets, neither of
these is called and kill_or_detach does nothing of interest.

After the call to kill_or_detach, we call inferior::pop_all_targets,
which calls inferior::pop_all_targets_above the dummy_stratum target,
which means popping all targets.

In inferior::pop_all_targets_above (in inferior.c), we call
switch_to_inferior_no_thread, which ensures the correct inferior is
selected, but makes it so that no thread is selected.  Switching to no
thread sets inferior_ptid to null_ptid.

Now popping the core_target calls core_target::close, and within
core_target::close we currently check inferior_ptid in order to
determine if exit_core_file_inferior has already been called or not.
We only call exit_core_file_inferior if inferior_ptid is not
null_ptid, so in this case we will not call exit_core_file_inferior.

The only other place that exit_core_file_inferior can be called from
is core_target::detach, but remember we specifically avoided calling
target_detach earlier in kill_or_detach.  This means that
exit_core_file_inferior ends up never being called.

It is exit_core_file_inferior that calls exit_inferior, and it is from
here that the Python 'exited' event is emitted.

I don't see any reason why kill_or_detach couldn't call target_detach
for a core file target, but I don't propose making that change in this
commit.

The check against inferior_ptid in core_target::close is clearly
incorrect, checking this requires that a suitable thread within the
inferior be selected, and that is not really a requirement for closing
a core_target.  Instead, we can just check the inferior::pid field.
When we open a core_target we always set inferior::pid, even if we
just assign a fake CORELOW_PID value, so checking inferior::pid
against zero will tell us if the inferior has already been exited.
Fixing this check is enough to resolve the reported bug and ensure
that the 'exited' event is always emitted, which is why I don't
propose changing kill_or_detach in this commit.

An assert in core_target::exit_core_file_inferior has to go too for
the same reason, the assert is checking that a thread is currently
selected, and as discussed above, this is not always the case.

There's a new test which checks that the 'exited' event is emitted for
a core file debug session, a native debug session where the inferior
is started by GDB, and a native debug session where GDB attaches to an
already running inferior.

Only the core file case was broken before this commit, but more
testing is always a good thing.

Reviewed-By: Lancelot Six <lancelot.six@amd.com>
Approved-By: Pedro Alves <pedro@palves.net>
gdb/corelow.c
gdb/testsuite/gdb.python/py-inf-exited-at-exit.c [new file with mode: 0644]
gdb/testsuite/gdb.python/py-inf-exited-at-exit.exp [new file with mode: 0644]
gdb/testsuite/gdb.python/py-inf-exited-at-exit.py [new file with mode: 0644]

index d5a724ab551e6c266ddba0849e74cef9fdcf5429..a87e082db870f6a0521577a7306f6e867a7119fc 100644 (file)
@@ -629,10 +629,6 @@ core_target::build_file_mappings ()
 void
 core_target::exit_core_file_inferior ()
 {
-  /* Opening a core file ensures that some thread, even if it's just a
-     "fake" thread, will have been selected.  */
-  gdb_assert (inferior_ptid != null_ptid);
-
   /* Avoid confusion from thread stuff.  */
   switch_to_no_thread ();
 
@@ -665,10 +661,11 @@ core_target::close ()
      mostly harmless except it causes two 'exited' events to be emitted in
      the Python API, which isn't ideal.
 
-     As opening a core_target always ensures that some thread is selected,
-     then we can tell if exit_core_file_inferior has already been called by
-     checking if no thread is now selected.  */
-  if (inferior_ptid != null_ptid)
+     As opening a core_target always ensures that a pid is assigned to the
+     core file inferior, even if it is the fake CORELOW_PID, then we can
+     tell if exit_core_file_inferior has already been called by checking if
+     the inferior has a non-zero pid or not.  */
+  if (current_inferior ()->pid != 0)
     exit_core_file_inferior ();
 
   /* Core targets are heap-allocated (see core_target_open), so here
diff --git a/gdb/testsuite/gdb.python/py-inf-exited-at-exit.c b/gdb/testsuite/gdb.python/py-inf-exited-at-exit.c
new file mode 100644 (file)
index 0000000..6731fbd
--- /dev/null
@@ -0,0 +1,44 @@
+/* This testcase is part of GDB, the GNU debugger.
+
+   Copyright 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 <unistd.h>
+#include "gdb_watchdog.h"
+
+/* GDB can set GLOBAL_VAR to non-zero to cause the inferior to exit.  */
+volatile int global_var = 0;
+
+/* This is used just to create some content that GDB can break on.  */
+volatile int other_var = 0;
+
+void
+foo (void)
+{
+  while (global_var == 0)
+    {
+      sleep (1);
+      other_var = 42;  /* Break here.  */
+    }
+}
+
+int
+main (void)
+{
+  gdb_watchdog (300);
+
+  foo ();
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.python/py-inf-exited-at-exit.exp b/gdb/testsuite/gdb.python/py-inf-exited-at-exit.exp
new file mode 100644 (file)
index 0000000..820858b
--- /dev/null
@@ -0,0 +1,156 @@
+# Copyright (C) 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 that the 'exited' event triggers when GDB exits.  Test for
+# both live inferiors, and for core files.
+
+require allow_python_tests
+
+load_lib gdb-python.exp
+
+standard_testfile
+
+if {[build_executable "build executable" $testfile $srcfile] == -1} {
+    return
+}
+
+set remote_python_file \
+    [gdb_remote_download host ${srcdir}/${subdir}/${testfile}.py]
+
+# Load the Python script for this test.  Record the string
+# representation of the current inferior.  Then exit GDB.  Ensure that
+# during the exit we see a single Python 'exited' event associated
+# with the expected inferior.
+proc source_py_script_and_exit_checking_event {} {
+    gdb_test_no_output "source $::remote_python_file" \
+       "load python script"
+
+    set expected_inferior_string \
+       [capture_command_output \
+            "python print(str(gdb.selected_inferior()))" ""]
+
+    set inferior_string ""
+    set event_count 0
+    gdb_test_multiple "with confirm off -- exit" "exit gdb" -lbl {
+       -re "\r\nEVENT: inferior exited event\\.  Inferior is (\[^\r\n\]+)(?=\r\n)" {
+           set inferior_string $expect_out(1,string)
+           incr event_count
+           exp_continue
+       }
+
+       eof {
+           verbose -log "GDB has now exited"
+           gdb_assert { $expected_inferior_string eq $inferior_string \
+                            && $event_count == 1 } $gdb_test_name
+
+           # Clean up now that GDB has gone away.  This prevents the
+           # generic support code from trying to shut down GDB again.
+           catch {wait -nowait -i $::gdb_spawn_id}
+           clean_up_spawn_id host $::gdb_spawn_id
+           unset ::gdb_spawn_id
+       }
+    }
+}
+
+# Clean restart using global TESTFILE as the executable, then run to
+# 'foo'.  Return true on success, otherwise, return false.
+proc clean_restart_and_runto_foo {} {
+    if {[clean_restart $::testfile] == -1} {
+       return false
+    }
+
+    return [runto foo]
+}
+
+# Check that the current inferior's backtrace is 'main -> foo'.
+proc check_backtrace { testname } {
+    gdb_test "bt" \
+       [multi_line \
+            "#0  (?:$::hex in )?foo \\(\\) at \[^\r\n\]+" \
+            "#1  (?:$::hex in )?main \\(\\) at \[^\r\n\]+"] \
+       $testname
+}
+
+# Create a core file.  Start GDB and load the core file.  Exit GDB.
+# Check that we see an 'exited' event, and that it is associated with
+# the correct gdb.Inferior.
+proc_with_prefix check_with_corefile {} {
+    if {![clean_restart_and_runto_foo]} {
+       return
+    }
+
+    check_backtrace "backtrace before generating core file"
+
+    set corefile [host_standard_output_file $::testfile.core]
+    if {![gdb_gcore_cmd $corefile "dump core file"]} {
+       return
+    }
+
+    clean_restart $::testfile
+
+    gdb_core_cmd $corefile "load corefile"
+
+    check_backtrace "backtrace after loading core file"
+
+    source_py_script_and_exit_checking_event
+}
+
+# Start the test program, attach to it, and then exit GDB.  Check that
+# we see an 'exited' event, and that it is associated with the correct
+# gdb.Inferior.
+proc_with_prefix check_with_attach {} {
+    if {![can_spawn_for_attach]} {
+       return
+    }
+
+    set test_spawn_id [spawn_wait_for_attach $::binfile]
+    set testpid [spawn_id_get_pid $test_spawn_id]
+
+    clean_restart $::testfile
+
+    gdb_breakpoint [gdb_get_line_number "Break here."]
+
+    gdb_test "attach $testpid"
+
+    gdb_continue_to_breakpoint "continue to b/p in foo"
+
+    check_backtrace "backtrace after attaching"
+
+    # When GDB detaches on exit, this should ensure the test program
+    # runs to completion.
+    gdb_test "set global_var = 1"
+
+    source_py_script_and_exit_checking_event
+
+    # In case the test program doesn't self-terminate after detach,
+    # kill it.
+    kill_wait_spawned_process $test_spawn_id
+}
+
+# Start a running inferior.  Exit GDB.  Check that we see an 'exited'
+# event, and that it is associated with the correct gdb.Inferior.
+proc_with_prefix check_with_live {} {
+    if {![clean_restart_and_runto_foo]} {
+       return
+    }
+
+    check_backtrace "backtrace before exiting"
+
+    source_py_script_and_exit_checking_event
+}
+
+check_with_live
+check_with_corefile
+check_with_attach
diff --git a/gdb/testsuite/gdb.python/py-inf-exited-at-exit.py b/gdb/testsuite/gdb.python/py-inf-exited-at-exit.py
new file mode 100644 (file)
index 0000000..9a5440b
--- /dev/null
@@ -0,0 +1,22 @@
+# Copyright (C) 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/>.
+
+
+def exit_event_handler(event):
+    inf = event.inferior
+    print("EVENT: inferior exited event.  Inferior is " + str(inf))
+
+
+gdb.events.exited.connect(exit_event_handler)