gdb: Fix assertion failure when inline frame #0 is duplicated

Modifying inline-frame-cycle-unwind.exp to use `bt -no-filters` produces
the following incorrect backtrace:

  #0  inline_func () at .../gdb/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c:49
  #1  normal_func () at .../gdb/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c:32
  #2  0x000055555555517f in inline_func () at .../gdb/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c:50
  #3  normal_func () at .../gdb/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c:32
  Backtrace stopped: previous frame identical to this frame (corrupt stack?)
  (gdb) FAIL: gdb.base/inline-frame-cycle-unwind.exp: cycle at level 1: backtrace when the unwind is broken at frame 1

The expected output, which we get with `bt`, is:

  #0  inline_func () at .../gdb/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c:49
  #1  normal_func () at .../gdb/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.c:32
  Backtrace stopped: previous frame identical to this frame (corrupt stack?)
  (gdb) PASS: gdb.base/inline-frame-cycle-unwind.exp: cycle at level 1: backtrace when the unwind is broken at frame 1

The cycle checking in `get_prev_frame_maybe_check_cycle` relies on newer
frame ids having already been computed and stashed.  Unlike other
frames, frame #0's id does not get computed immediately.

The test passes with `bt` because when applying python frame filters,
the call to `bootstrap_python_frame_filters` happens to compute the id
of frame #0.  When `get_prev_frame_maybe_check_cycle` later tries to
stash frame #2's id, the cycle is detected.

The test fails with `bt -no-filters` because frame #0's id has not been
stashed by the time `get_prev_frame_maybe_check_cycle` tries to stash
frame #2's id which succeeds and the cycle is only detected later when
trying to stash frame #4's id.  Doing `stepi` after the incorrect
backtrace would then trigger an assertion failure when trying to stash
frame #0's id because it is a duplicate of #2's already stashed id.

In `get_prev_frame_always_1`, if this_frame is inline frame 0, then
compute and stash its frame id before returning the previous frame.
This ensures that the id of inline frame 0 has been stashed before
`get_prev_frame_maybe_check_cycle` is called on older frames.

The test case has been updated to run both `bt` and `bt -no-filters`.

Co-authored-by: Andrew Burgess <aburgess@redhat.com>
Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=32757
This commit is contained in:
Craig Blackmore
2025-02-13 15:53:34 +00:00
committed by Andrew Burgess
parent 66150ea536
commit c295d55246
2 changed files with 96 additions and 69 deletions

View File

@@ -2325,7 +2325,22 @@ get_prev_frame_always_1 (const frame_info_ptr &this_frame)
until we have unwound all the way down to the previous non-inline until we have unwound all the way down to the previous non-inline
frame. */ frame. */
if (get_frame_type (this_frame) == INLINE_FRAME) if (get_frame_type (this_frame) == INLINE_FRAME)
return get_prev_frame_maybe_check_cycle (this_frame); {
frame_info_ptr fi = get_prev_frame_maybe_check_cycle (this_frame);
/* If this_frame is the current frame, then compute and stash its frame
id so that the cycle check in get_prev_frame_maybe_check_cycle works
correctly in the case where inline frame 0 has been duplicated.
The this_id.p check is required to avoid recursion as computing the
frame id results in a call to inline_frame_this_id which calls back
into get_prev_frame_always. */
if (this_frame->level == 0
&& this_frame->this_id.p != frame_id_status::COMPUTING)
get_frame_id (this_frame);
return fi;
}
/* If this_frame is the current frame, then compute and stash its /* If this_frame is the current frame, then compute and stash its
frame id prior to fetching and computing the frame id of the frame id prior to fetching and computing the frame id of the

View File

@@ -72,77 +72,89 @@ gdb_continue_to_breakpoint "stop at test breakpoint"
gdb_test_no_output "source ${pyfile}"\ gdb_test_no_output "source ${pyfile}"\
"import python scripts" "import python scripts"
# Check the unbroken stack. # Test with and without filters.
gdb_test_sequence "bt" "backtrace when the unwind is left unbroken" { foreach bt_cmd { "bt" "bt -no-filters" } {
"\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at " with_test_prefix "$bt_cmd" {
"\\r\\n#1 \[^\r\n\]* normal_func \\(\\) at "
"\\r\\n#2 \[^\r\n\]* inline_func \\(\\) at "
"\\r\\n#3 \[^\r\n\]* normal_func \\(\\) at "
"\\r\\n#4 \[^\r\n\]* inline_func \\(\\) at "
"\\r\\n#5 \[^\r\n\]* normal_func \\(\\) at "
"\\r\\n#6 \[^\r\n\]* main \\(\\) at "
}
with_test_prefix "cycle at level 5" { # Check the unbroken stack.
# Arrange to introduce a stack cycle at frame 5. gdb_test_sequence "$bt_cmd" "backtrace when the unwind is left unbroken" {
gdb_test_no_output "python stop_at_level=5" "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
gdb_test "maint flush register-cache" \ "\\r\\n#1 \[^\r\n\]* normal_func \\(\\) at "
"Register cache flushed\\." "\\r\\n#2 \[^\r\n\]* inline_func \\(\\) at "
gdb_test_lines "bt" "backtrace when the unwind is broken at frame 5" \ "\\r\\n#3 \[^\r\n\]* normal_func \\(\\) at "
[multi_line \ "\\r\\n#4 \[^\r\n\]* inline_func \\(\\) at "
"#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \ "\\r\\n#5 \[^\r\n\]* normal_func \\(\\) at "
"#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \ "\\r\\n#6 \[^\r\n\]* main \\(\\) at "
"#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \ }
"#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
"#4 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
"#5 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
"Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
}
with_test_prefix "cycle at level 3" { with_test_prefix "cycle at level 5" {
# Arrange to introduce a stack cycle at frame 3. # Arrange to introduce a stack cycle at frame 5.
gdb_test_no_output "python stop_at_level=3" gdb_test_no_output "python stop_at_level=5"
gdb_test "maint flush register-cache" \ gdb_test "maint flush register-cache" \
"Register cache flushed\\." "Register cache flushed\\."
gdb_test_lines "bt" "backtrace when the unwind is broken at frame 3" \ gdb_test_lines "$bt_cmd" "backtrace when the unwind is broken at frame 5" \
[multi_line \ [multi_line \
"#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \ "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
"#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \ "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
"#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \ "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
"#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \ "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
"Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"] "#4 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
} "#5 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
"Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
}
with_test_prefix "cycle at level 1" { with_test_prefix "cycle at level 3" {
# Arrange to introduce a stack cycle at frame 1. # Arrange to introduce a stack cycle at frame 3.
gdb_test_no_output "python stop_at_level=1" gdb_test_no_output "python stop_at_level=3"
gdb_test "maint flush register-cache" \ gdb_test "maint flush register-cache" \
"Register cache flushed\\." "Register cache flushed\\."
gdb_test_lines "bt" "backtrace when the unwind is broken at frame 1" \ gdb_test_lines "$bt_cmd" "backtrace when the unwind is broken at frame 3" \
[multi_line \ [multi_line \
"#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \ "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
"#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \ "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
"Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"] "#2 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
} "#3 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
"Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
}
# Flush the register cache (which also flushes the frame cache) so we with_test_prefix "cycle at level 1" {
# get a full backtrace again, then switch on frame debugging and try # Arrange to introduce a stack cycle at frame 1.
# to back trace. At one point this triggered an assertion. gdb_test_no_output "python stop_at_level=1"
gdb_test "maint flush register-cache" \ gdb_test "maint flush register-cache" \
"Register cache flushed\\." "" "Register cache flushed\\."
gdb_test_no_output "set debug frame 1" gdb_test_lines "$bt_cmd" "backtrace when the unwind is broken at frame 1" \
set ok 1 [multi_line \
gdb_test_multiple "bt" "backtrace with debugging on" { "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
-re "^$gdb_prompt $" { "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
gdb_assert { $ok } $gdb_test_name "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
} }
-re "Python Exception <class 'gdb.error'>: \[^\r\n\]*\r\n" {
set ok 0 # Flush the register cache (which also flushes the frame cache) so we
exp_continue # get a full backtrace again, then switch on frame debugging and try
} # to back trace. At one point this triggered an assertion.
-re "\[^\r\n\]+\r\n" { gdb_test "maint flush register-cache" \
exp_continue "Register cache flushed\\." ""
gdb_test_no_output "set debug frame 1"
set ok 1
gdb_test_multiple "$bt_cmd" "backtrace with debugging on" {
-re "^$gdb_prompt $" {
gdb_assert { $ok } $gdb_test_name
}
-re "Python Exception <class 'gdb.error'>: \[^\r\n\]*\r\n" {
set ok 0
exp_continue
}
-re "\[^\r\n\]+\r\n" {
exp_continue
}
}
gdb_test "p 1 + 2 + 3" " = 6" \
"ensure GDB is still alive"
# Prepare for the next iteration of the test loop
gdb_test_no_output "set debug frame 0"
gdb_test_no_output "python stop_at_level=None"
gdb_test "maint flush register-cache" \
"Register cache flushed\\." "maint flush register-cache at (loop end)"
} }
} }
gdb_test "p 1 + 2 + 3" " = 6" \
"ensure GDB is still alive"