[1/1] gdb, infcall, breakpoint: Handle cycles in conditional expr evaluation

Message ID 20261002143505.38658-1-mohamed.bouhaouel@intel.com
State New
Headers
Series [1/1] gdb, infcall, breakpoint: Handle cycles in conditional expr evaluation |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Build passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Build passed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 success Test passed
linaro-tcwg-bot/tcwg_gdb_check--master-arm success Test passed

Commit Message

Mohamed Bouhaouel Oct. 2, 2026, 2:35 p.m. UTC
  Take the following scenario:

  (gdb) break foo if foo (1) == 2
  (gdb) run

The program stops at 'foo', then GDB tries to evaluate the condition.
The condition triggers an inferior call of 'foo', which causes a stop at
'foo' again, and GDB is stuck in this infinite cycle.

The same issue exists also for indirect cycles, e.g.,

  (gdb) break foo if bar (1)
  (gdb) break bar if foo (1)
  (gdb) run

To avoid this, check for cycles just before evaluating a breakpoint
condition.  If a breakpoint is hit again while its own condition is
being evaluated, the evaluation of that condition is aborted with an
error message.

With the patch, the behavior becomes:

  (gdb) break foo if foo (1) == 2
  Breakpoint 2 at 0x55555555514c: file infcall-from-bp-cond-cycle.c, line 27.
  (gdb) continue
  Continuing.
  Error in testing condition for breakpoint 2:
  Aborting expression evaluation for the conditional bp:
  A cycle was detected during the evaluation.

  Breakpoint 2, foo (a=1) at infcall-from-bp-cond-cycle.c:27
  27	  return a + add_one (a);
  Error in testing condition for breakpoint 2:
  The program being debugged stopped while in a function called from GDB.
  Evaluation of the expression containing the function
  (foo) will be abandoned.
  When the function is done executing, GDB will silently stop.
  (gdb) backtrace
  #0  foo (a=1) at infcall-from-bp-cond-cycle.c:27
  #1  <function called from gdb>
  #2  foo (a=11) at infcall-from-bp-cond-cycle.c:27
  #3  0x00005555555551af in main () at infcall-from-bp-cond-cycle.c:47
---
 gdb/breakpoint.c                              | 14 +++-
 gdb/gdbthread.h                               | 10 ++-
 gdb/infcall.c                                 |  2 +-
 gdb/infrun.c                                  |  6 +-
 .../gdb.base/infcall-from-bp-cond-cycle.c     | 51 +++++++++++++
 .../gdb.base/infcall-from-bp-cond-cycle.exp   | 71 +++++++++++++++++++
 6 files changed, 147 insertions(+), 7 deletions(-)
 create mode 100644 gdb/testsuite/gdb.base/infcall-from-bp-cond-cycle.c
 create mode 100644 gdb/testsuite/gdb.base/infcall-from-bp-cond-cycle.exp
  

Patch

diff --git a/gdb/breakpoint.c b/gdb/breakpoint.c
index 0f5360c4ce5..a4ca10a95fe 100644
--- a/gdb/breakpoint.c
+++ b/gdb/breakpoint.c
@@ -6047,8 +6047,18 @@  bpstat_check_breakpoint_conditions (bpstat *bs, thread_info *thread)
 	{
 	  try
 	    {
-	      scoped_restore reset_in_cond_eval
-		= make_scoped_restore (&thread->control.in_cond_eval, true);
+	      /* Check if we are already evaluating the same condition.  This
+		 would lead to an infinite cycle.  */
+	      std::vector<breakpoint *> &cond_eval_bps
+		= thread->control.cond_eval_bps;
+	      if (std::find (cond_eval_bps.begin (), cond_eval_bps.end (), b)
+		  != cond_eval_bps.end ())
+		error (_("Aborting expression evaluation for the "
+			 "conditional bp:\n"
+			 "A cycle was detected during the evaluation."));
+
+	      cond_eval_bps.push_back (b);
+	      SCOPE_EXIT { cond_eval_bps.pop_back (); };
 	      condition_result = breakpoint_cond_eval (cond);
 	    }
 	  catch (const gdb_exception_error &ex)
diff --git a/gdb/gdbthread.h b/gdb/gdbthread.h
index 51e9ae1b3f0..640c4a0fc2e 100644
--- a/gdb/gdbthread.h
+++ b/gdb/gdbthread.h
@@ -234,8 +234,16 @@  struct thread_control_state
      step" behaves like "on" or "off".  */
   int stepping_command = 0;
 
+  /* A list of conditional breakpoints (if any) that the thread is evaluating.
+     This list is tracking breakpoint hits during expression evaluation to make
+     sure we don't run into cycles.  */
+  std::vector<breakpoint *> cond_eval_bps;
+
   /* True if the thread is evaluating a BP condition.  */
-  bool in_cond_eval = false;
+  bool in_cond_eval () const
+  {
+    return !cond_eval_bps.empty ();
+  }
 
 private:
   /* Function the thread was in as of last it started stepping.  */
diff --git a/gdb/infcall.c b/gdb/infcall.c
index 4c40f6cb06e..f4f15a946a6 100644
--- a/gdb/infcall.c
+++ b/gdb/infcall.c
@@ -865,7 +865,7 @@  run_inferior_call (std::unique_ptr<call_thread_fsm> sm,
 
       /* Setup a timer (if possible, and if the settings allow) to prevent
 	 the inferior call running forever.  */
-      bool direct_call_p = !call_thread->control.in_cond_eval;
+      bool direct_call_p = !call_thread->control.in_cond_eval ();
       infcall_timer_controller infcall_timer (call_thread, direct_call_p);
 
       /* Inferior function calls are always synchronous, even if the
diff --git a/gdb/infrun.c b/gdb/infrun.c
index 47966cf72db..c35e6d20532 100644
--- a/gdb/infrun.c
+++ b/gdb/infrun.c
@@ -2458,7 +2458,7 @@  user_visible_resume_ptid (int step)
       resume_ptid = inferior_ptid;
     }
   else if (inferior_ptid != null_ptid
-	   && inferior_thread ()->control.in_cond_eval)
+	   && inferior_thread ()->control.in_cond_eval ())
     {
       /* The inferior thread is evaluating a BP condition.  Other threads
 	 might be stopped or running and we do not want to change their
@@ -4711,7 +4711,7 @@  fetch_inferior_event ()
     /* Is the current thread performing an inferior function call as part
        of a breakpoint condition evaluation?  */
     bool in_cond_eval = (inferior_ptid != null_ptid
-			 && inferior_thread ()->control.in_cond_eval);
+			 && inferior_thread ()->control.in_cond_eval ());
 
     /* If the thread is in the middle of the condition evaluation, wait for
        an event from the current thread.  Otherwise, wait for an event from
@@ -4780,7 +4780,7 @@  fetch_inferior_event ()
 	       function call as part of a condition evaluation, then we
 	       don't want to stop all the other threads.  */
 	    if (ecs.event_thread == nullptr
-		|| !ecs.event_thread->control.in_cond_eval)
+		|| !ecs.event_thread->control.in_cond_eval ())
 	      {
 		stop_all_threads_if_all_stop_mode ();
 
diff --git a/gdb/testsuite/gdb.base/infcall-from-bp-cond-cycle.c b/gdb/testsuite/gdb.base/infcall-from-bp-cond-cycle.c
new file mode 100644
index 00000000000..a9e270dc968
--- /dev/null
+++ b/gdb/testsuite/gdb.base/infcall-from-bp-cond-cycle.c
@@ -0,0 +1,51 @@ 
+/* Copyright 2025-2026 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   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/>.  */
+
+int
+add_one (int a)
+{
+  return ++a; /* cond-bp-line.  */
+}
+
+int
+foo (int a)
+{
+  return a + add_one (a);
+}
+
+int
+bar (int a)
+{
+  return a + 100;
+}
+
+int
+baz (int a)
+{
+  return --a;
+}
+
+int
+main ()
+{
+  int a = 10;
+  int b = add_one (a);
+  b += foo (b);
+  b = bar (b) + baz (b);
+
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.base/infcall-from-bp-cond-cycle.exp b/gdb/testsuite/gdb.base/infcall-from-bp-cond-cycle.exp
new file mode 100644
index 00000000000..c67192165b0
--- /dev/null
+++ b/gdb/testsuite/gdb.base/infcall-from-bp-cond-cycle.exp
@@ -0,0 +1,71 @@ 
+# 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/>.
+#
+# Test that GDB is able to detect a cycle when evaluating a conditional
+# breakpoint.  In such a case, GDB should report the cycle and abort the
+# evaluation.
+
+require {!target_info exists gdb,cannot_call_functions}
+
+standard_testfile .c
+
+if { [prepare_for_testing "failed to prepare" $testfile $srcfile] } {
+    return -1
+}
+
+set bp_line [gdb_get_line_number "cond-bp-line"]
+set conditional_bps [dict create \
+    direct-line [list "$bp_line if foo (1) == 2"] \
+    direct-func [list "foo if foo (1) == 2"] \
+    indirect-2 [list "foo if bar (1)" \
+		     "bar if foo (1)"] \
+    indirect-3 [list "bar if baz (1)" \
+		     "baz if add_one (1)" \
+		     "add_one if bar (1)"]]
+
+foreach_with_prefix name [dict keys $conditional_bps] {
+    set bps [dict get $conditional_bps $name]
+    clean_restart $testfile
+    if {![runto_main]} {
+	return
+    }
+    foreach bp $bps {
+	gdb_breakpoint $bp
+    }
+    gdb_test "continue" "A cycle was detected during the evaluation.*"
+
+    delete_breakpoints
+    set keep_going 1
+    set itr 0
+    while {$keep_going && $itr < 10} {
+	set keep_going 0
+	gdb_test_multiple "backtrace" "check dummy frames: itr $itr" -lbl {
+	    -re "\r\n#$::decimal  <function called from gdb>(?=\r\n)" {
+		set keep_going 1
+		exp_continue
+	    }
+	    -re -wrap "" {
+		pass $gdb_test_name
+	    }
+	}
+	if {$keep_going} {
+	    gdb_test "continue" "Continuing\\." "skip dummy frame: itr $itr"
+	}
+	incr itr
+    }
+    gdb_assert {!$keep_going} "no dummy frames left"
+
+    gdb_continue_to_end
+}