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

Craig Blackmore craig.blackmore@embecosm.com
Tue Jan 28 17:10:55 GMT 2025


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`, get the frame id of the current frame to ensure it
has been computed and 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`.
---
 gdb/frame.c                                   |   5 +
 .../gdb.base/inline-frame-cycle-unwind.exp    | 148 ++++++++++--------
 2 files changed, 85 insertions(+), 68 deletions(-)

diff --git a/gdb/frame.c b/gdb/frame.c
index a5246780962..c30bb4eb2aa 100644
--- a/gdb/frame.c
+++ b/gdb/frame.c
@@ -2657,6 +2657,11 @@ get_prev_frame (const frame_info_ptr &this_frame)
      get_current_frame().  */
   gdb_assert (this_frame != NULL);
 
+  /* Make sure the current frame id has been computed so that the cycle check in
+     get_prev_frame_maybe_check_cycle works for the case where frame 0 has been
+     duplicated.  */
+  get_frame_id (get_current_frame ());
+
   frame_pc_p = get_frame_pc_if_available (this_frame, &frame_pc);
 
   /* tausq/2004-12-07: Dummy frames are skipped because it doesn't make much
diff --git a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
index 45086f69a39..46561a9c9fb 100644
--- a/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
+++ b/gdb/testsuite/gdb.base/inline-frame-cycle-unwind.exp
@@ -72,77 +72,89 @@ gdb_continue_to_breakpoint "stop at test breakpoint"
 gdb_test_no_output "source ${pyfile}"\
     "import python scripts"
 
-# Check the unbroken stack.
-gdb_test_sequence "bt" "backtrace when the unwind is left unbroken" {
-    "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
-    "\\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 "
-}
+# Test with and without filters.
+foreach bt_cmd { "bt" "bt -no-filters" } {
+    with_test_prefix "$bt_cmd" {
 
-with_test_prefix "cycle at level 5" {
-    # Arrange to introduce a stack cycle at frame 5.
-    gdb_test_no_output "python stop_at_level=5"
-    gdb_test "maint flush register-cache" \
-	"Register cache flushed\\."
-    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 5" \
-	[multi_line \
-	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\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\]+" \
-	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
-}
+	# Check the unbroken stack.
+	gdb_test_sequence "$bt_cmd" "backtrace when the unwind is left unbroken" {
+	    "\\r\\n#0 \[^\r\n\]* inline_func \\(\\) at "
+	    "\\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 3" {
-    # Arrange to introduce a stack cycle at frame 3.
-    gdb_test_no_output "python stop_at_level=3"
-    gdb_test "maint flush register-cache" \
-	"Register cache flushed\\."
-    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 3" \
-	[multi_line \
-	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\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\]+" \
-	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
-}
+	with_test_prefix "cycle at level 5" {
+	    # Arrange to introduce a stack cycle at frame 5.
+	    gdb_test_no_output "python stop_at_level=5"
+	    gdb_test "maint flush register-cache" \
+		"Register cache flushed\\."
+	    gdb_test_lines "$bt_cmd" "backtrace when the unwind is broken at frame 5" \
+		[multi_line \
+		    "#0 \[^\r\n\]* inline_func \\(\\) at \[^\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\]+" \
+		    "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+	}
 
-with_test_prefix "cycle at level 1" {
-    # Arrange to introduce a stack cycle at frame 1.
-    gdb_test_no_output "python stop_at_level=1"
-    gdb_test "maint flush register-cache" \
-	"Register cache flushed\\."
-    gdb_test_lines "bt" "backtrace when the unwind is broken at frame 1" \
-	[multi_line \
-	     "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
-	     "#1 \[^\r\n\]* normal_func \\(\\) at \[^\r\n\]+" \
-	     "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
-}
+	with_test_prefix "cycle at level 3" {
+	    # Arrange to introduce a stack cycle at frame 3.
+	    gdb_test_no_output "python stop_at_level=3"
+	    gdb_test "maint flush register-cache" \
+		"Register cache flushed\\."
+	    gdb_test_lines "$bt_cmd" "backtrace when the unwind is broken at frame 3" \
+		[multi_line \
+		    "#0 \[^\r\n\]* inline_func \\(\\) at \[^\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\]+" \
+		    "Backtrace stopped: previous frame identical to this frame \\(corrupt stack\\?\\)"]
+	}
 
-# Flush the register cache (which also flushes the frame cache) so we
-# get a full backtrace again, then switch on frame debugging and try
-# to back trace.  At one point this triggered an assertion.
-gdb_test "maint flush register-cache" \
-    "Register cache flushed\\." ""
-gdb_test_no_output "set debug frame 1"
-set ok 1
-gdb_test_multiple "bt" "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
+	with_test_prefix "cycle at level 1" {
+	    # Arrange to introduce a stack cycle at frame 1.
+	    gdb_test_no_output "python stop_at_level=1"
+	    gdb_test "maint flush register-cache" \
+		"Register cache flushed\\."
+	    gdb_test_lines "$bt_cmd" "backtrace when the unwind is broken at frame 1" \
+		[multi_line \
+		    "#0 \[^\r\n\]* inline_func \\(\\) at \[^\r\n\]+" \
+		    "#1 \[^\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
+	# get a full backtrace again, then switch on frame debugging and try
+	# to back trace.  At one point this triggered an assertion.
+	gdb_test "maint flush register-cache" \
+	    "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"
-- 
2.43.0



More information about the Gdb-patches mailing list