[binutils-gdb] gdb/python: fix 'exited' event when GDB exits from core file debugging

Andrew Burgess via Gdb-cvs <[email protected]> Sun, 14 Jun 2026 21:04:55 +0000 (GMT)
Newsgroups gmane.comp.gdb.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=c311f4d37f31ff3fbb5db6923abcdf93bb75a37b

commit c311f4d37f31ff3fbb5db6923abcdf93bb75a37b
Author: Andrew Burgess <[email protected]>
Date:   Thu Jun 4 11:02:27 2026 +0100

    gdb/python: fix 'exited' event when GDB exits from core file debugging
    
    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 <[email protected]>
    Approved-By: Pedro Alves <[email protected]>

Diff:
---
 gdb/corelow.c                                      |  13 +-
 gdb/testsuite/gdb.python/py-inf-exited-at-exit.c   |  44 ++++++
 gdb/testsuite/gdb.python/py-inf-exited-at-exit.exp | 156 +++++++++++++++++++++
 gdb/testsuite/gdb.python/py-inf-exited-at-exit.py  |  22 +++
 4 files changed, 227 insertions(+), 8 deletions(-)

diff --git a/gdb/corelow.c b/gdb/corelow.c
index d5a724ab551..a87e082db87 100644
--- a/gdb/corelow.c
+++ b/gdb/corelow.c
@@ -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
index 00000000000..6731fbdd7a9
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-inf-exited-at-exit.c
@@ -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
index 00000000000..820858bd75d
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-inf-exited-at-exit.exp
@@ -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
index 00000000000..9a5440b083f
--- /dev/null
+++ b/gdb/testsuite/gdb.python/py-inf-exited-at-exit.py
@@ -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)