blob: 6a4eaaaafcce4d97805a0f5d9c6122eb2dca6677 [file] [log] [blame]
# 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/>. */
# This test confirms how GDB handles a badly behaving remote target. The
# remote target reports a stop event (signal delivery), then, as GDB is
# processing the stop it syncs the thread list with the remote.
#
# The badly behaving remote target was dropping the signaled thread from the
# thread list at this point, that is, the thread appeared to exit before an
# exit event had been sent to (and seen by) GDB.
#
# At one point this was causing an assertion failed. GDB would try to
# process the signal stop event, and to do this would try to read some
# registers. Reading registers requires a regcache, and GDB will only
# create a regcache for a non-exited thread.
load_lib gdbserver-support.exp
load_lib gdbreplay-support.exp
require allow_gdbserver_tests
require has_gdbreplay
standard_testfile
if { [build_executable "failed to build exec" $testfile $srcfile {debug pthreads}] } {
return -1
}
# Start the inferior and record a remote log for our interaction with it.
# All we do is start the inferior and wait for thread 2 to receive a signal.
# Check that GDB correctly shows the signal as received. LOG_FILENAME is
# where we should write the remote log.
proc_with_prefix record_initial_logfile { log_filename } {
clean_restart $::testfile
# Make sure we're disconnected, in case we're testing with an
# extended-remote board, therefore already connected.
gdb_test "disconnect" ".*"
gdb_test_no_output "set sysroot" \
"setting sysroot before starting gdbserver"
# Start gdbserver like:
# gdbserver :PORT ....
set res [gdbserver_start "" $::binfile]
set gdbserver_protocol [lindex $res 0]
set gdbserver_gdbport [lindex $res 1]
gdb_test_no_output "set remotelogfile $log_filename" \
"setup remotelogfile"
# Connect to gdbserver.
if {![gdb_target_cmd $gdbserver_protocol $gdbserver_gdbport] == 0} {
unsupported "couldn't start gdbserver"
return
}
gdb_breakpoint main
gdb_continue_to_breakpoint "continuing to main"
gdb_test "continue" \
"Thread $::decimal \[^\r\n\]+ received signal SIGTRAP, .*"
gdb_test "disconnect" ".*" \
"disconnect after seeing signal"
}
# Copy the remote log from IN_FILENAME to OUT_FILENAME, but modify one
# particular line.
#
# The line to be modified is the last <threads>...</threads> line, this is
# the reply from the remote that indicates the thread list. It is expected
# that the thread list will contain two threads.
#
# When DROP_BOTH is true then both threads will be removed from the modified
# line. Otherwise, only the second thread is removed.
proc update_replay_log { in_filename out_filename drop_both } {
# Read IN_FILENAME into a list.
set fd [open $in_filename]
set data [read $fd]
close $fd
set lines [split $data "\n"]
# Find the last line in LINES that contains the <threads> list.
set idx -1
for { set i 0 } { $i < [llength $lines] } { incr i } {
if { [regexp "^r.*<threads>.*</threads>" [lindex $lines $i]] } {
set idx $i
}
}
# Modify the line by dropping the second thread. This does assume
# the thread order as seen in the <threads>...</threads> list, but
# this seems stable for now.
set line [lindex $lines $idx]
set fixed_log false
if {[regexp "^(r .*<threads>\\\\n)(<thread id.*/>\\\\n)(<thread id.*/>\\\\n)(</threads>.*)$" $line \
match part1 part2 part3 part4]} {
if { $drop_both } {
set line $part1$part4
} else {
set line $part1$part2$part4
}
set lines [lreplace $lines $idx $idx $line]
set fixed_log true
}
# Write all the lines to OUT_FILENAME
set fd [open $out_filename "w"]
foreach l $lines {
puts $fd $l
}
close $fd
# Did we manage to update the log file?
return $fixed_log
}
# Replay the test process using REMOTE_LOG as the logfile to replay. If
# EXPECT_ERROR is true then after the final 'continue' we expect GDB to give
# an error as the required thread is missing. When EXPECT_ERROR is false
# then we expect the test to complete as normal. NON_STOP is eithe 'on' or
# 'off' and indicates GDBs non-stop mode.
proc_with_prefix replay_with_log { remote_log expect_error non_stop } {
clean_restart $::testfile
# Make sure we're disconnected, in case we're testing with an
# extended-remote board, therefore already connected.
gdb_test "disconnect" ".*"
gdb_test_no_output "set sysroot"
set res [gdbreplay_start $remote_log]
set gdbserver_protocol [lindex $res 0]
set gdbserver_gdbport [lindex $res 1]
# Connect to gdbserver.
if {![gdb_target_cmd $gdbserver_protocol $gdbserver_gdbport] == 0} {
fail "couldn't connect to gdbreplay"
return
}
gdb_breakpoint main
gdb_continue_to_breakpoint "continuing to main"
if { $expect_error } {
set expected_output \
[list \
"\\\[Thread \[^\r\n\]+ exited\\\]" \
"warning: command aborted, Thread \[^\r\n\]+ unexpectedly exited after signal stop event"]
if { !$non_stop } {
lappend expected_output "\\\[Switching to Thread \[^\r\n\]+\\\]"
}
gdb_test "continue" [multi_line {*}$expected_output]
} else {
# This is the original behaviour, we see this when running
# with the unmodified log.
gdb_test "continue" \
"Thread ${::decimal}(?: \[^\r\n\]+)? received signal SIGTRAP, .*"
}
gdb_test "disconnect" ".*" \
"disconnect after seeing signal"
}
# Run the complete test cycle; generate an initial log file, modify the log
# file, then check that GDB correctly handles replaying the modified log
# file.
#
# NON_STOP is either 'on' or 'off' and indicates GDB's non-stop mode.
proc run_test { non_stop } {
if { $non_stop } {
set suffix "-ns"
} else {
set suffix ""
}
# The replay log is placed in 'replay.log'.
set remote_log [standard_output_file replay${suffix}.log]
set missing_1_log [standard_output_file replay-missing-1${suffix}.log]
set missing_2_log [standard_output_file replay-missing-2${suffix}.log]
record_initial_logfile $remote_log
if { ![update_replay_log $remote_log $missing_1_log false] } {
fail "couldn't update remote replay log (drop 1 case)"
}
if { ![update_replay_log $remote_log $missing_2_log true] } {
fail "couldn't update remote replay log (drop 2 case)"
}
with_test_prefix "with unmodified log" {
# Replay with the unmodified log. This confirms that we can replay this
# scenario correctly.
replay_with_log $remote_log false $non_stop
}
with_test_prefix "missing 1 thread log" {
# Now replay with the modified log, this time the thread that receives
# the event should be missing from the thread list, GDB will give an
# error when the inferior stops.
replay_with_log $missing_1_log true $non_stop
}
with_test_prefix "missing 2 threads log" {
# When we drop both threads from the <threads> reply, GDB doesn't
# actually remove both threads from the inferior; an inferior must
# always have at least one thread. So in this case, as the primary
# thread is first, GDB drops this, then retains the second thread, which
# is the one we're stopping in, and so, we don't expect to see the error
# in this case.
replay_with_log $missing_2_log false $non_stop
}
}
# Run the test twice, with non-stop on and off.
foreach_with_prefix non_stop { on off } {
save_vars { ::GDBFLAGS } {
append ::GDBFLAGS " -ex \"set non-stop $non_stop\""
run_test $non_stop
}
}