[binutils-gdb] gdb/tui: prevent TUI activation from a secondary prompt

Andrew Burgess via Gdb-cvs <[email protected]> Mon, 27 Jul 2026 13:48:58 +0000 (GMT)
Newsgroups gmane.comp.gdb.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=3Dbinutils-gdb.git;h=3Dbbf826734305=
50060bbf31271cd4c81568b26cf5

commit bbf82673430550060bbf31271cd4c81568b26cf5
Author: Andrew Burgess <[email protected]>
Date:   Tue Apr 28 11:06:48 2026 +0100

    gdb/tui: prevent TUI activation from a secondary prompt
   =20
    The TUI can be activated with key combinations like 'C-x C-a'.  This
    is handled by readline calling the function tui_rl_switch_mode, or
    various other functions which indirectly call that function.
   =20
    These multi-key combinations can be used even at a secondary prompt,
    e.g. the:
   =20
      Make breakpoint pending on future shared library load? (y or [n])
   =20
    The problem with this is that when the TUI activates CLI content
    doesn't carry over into the `cmd` window, so the secondary prompt is
    not visible to the user after the mode switch.  Worse, because the
    content doesn't carry over we clear the readline state, and this
    involves sending a '\n' to readline.  This newline will select the
    default action at the secondary prompt, which might not be what the
    user actually wants.
   =20
    Now, we could imagine trying to "fix" this so that the CLI content is
    copied over into the `cmd` window, and the secondary prompt is
    represented to the user, so they can then make the choice they want,
    but implementing this fix would be a big job, for very little gain.
   =20
    I think it is easier to just prevent the user switching to TUI mode
    while at a secondary prompt.
   =20
    I created a new templated wrapper function tui_rl_keybinding, which is
    then used to wrap every function that is bound to a readline multi-key
    combination.  The wrapper function checks if we are in a secondary
    prompt, and if we are, performs an early return.
   =20
    For completeness, I've added an assert that we are not in a secondary
    prompt to all of the wrapped functions, this (hopefully) will help
    catch cases where these functions are called directly without going
    through the wrapper.  I also added the same assert to
    tui_rl_command_key and tui_rl_command_mode which are not themselves
    wrapped functions, but are only used when in single key mode, and it
    is not possible to enter single key mode when at a separate prompt,
    see tui_rl_startup_hook (which checks for being at a secondary prompt)
    and tui_rl_next_keymap (which is wrapped).
   =20
    There's a new helper proc added to lib/gdb.exp, this will be used by
    additional tests later in this series.
   =20
    The user can still switch to TUI mode at the primary '(gdb)' prompt.

Diff:
---
 .../gdb.tui/activate-from-secondary-prompt.c       |  22 +++
 .../gdb.tui/activate-from-secondary-prompt.exp     | 179 +++++++++++++++++=
++++
 gdb/testsuite/lib/gdb.exp                          |  27 ++++
 gdb/tui/tui.c                                      |  74 ++++++---
 4 files changed, 280 insertions(+), 22 deletions(-)

diff --git a/gdb/testsuite/gdb.tui/activate-from-secondary-prompt.c b/gdb/t=
estsuite/gdb.tui/activate-from-secondary-prompt.c
new file mode 100644
index 00000000000..6a0e311ef41
--- /dev/null
+++ b/gdb/testsuite/gdb.tui/activate-from-secondary-prompt.c
@@ -0,0 +1,22 @@
+/* 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/>.  =
*/
+
+int
+main (void)
+{
+  return 0;
+}
diff --git a/gdb/testsuite/gdb.tui/activate-from-secondary-prompt.exp b/gdb=
/testsuite/gdb.tui/activate-from-secondary-prompt.exp
new file mode 100644
index 00000000000..3bfc7b1156d
--- /dev/null
+++ b/gdb/testsuite/gdb.tui/activate-from-secondary-prompt.exp
@@ -0,0 +1,179 @@
+# 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/>.
+
+# Check that the TUI can NOT be activated using 'C-x C-a', and similar
+# key combinations, from a secondary prompt.  Activating from the
+# secondary prompt has the same effect as pressing 'Return' at the
+# prompt, which might not be what the user wants.
+#
+# We could imagine a world where, when activating the TUI from a
+# secondary prompt works as we might expect, the prompt is not
+# cancelled, but reprinted within the TUI, and the user can then make
+# their choice and continue with their debug session.  If we ever
+# implement this, then this test will need to change.
+#
+# For now though we take the easy way and just ban the user from
+# activating the TUI from a secondary prompt.
+
+load_lib debuginfod-support.exp
+
+tuiterm_env
+
+require allow_tui_tests
+
+standard_testfile
+
+if {[build_executable "build executable" ${testfile} ${srcfile}] =3D=3D -1=
} {
+    return
+}
+
+# Check that readline is in use.
+clean_restart
+if { ![readline_is_used] } {
+    unsupported "readline is required"
+    return
+}
+
+# Assuming that GDB is at a secondary prompt, try various multi-key
+# combinations and check that the screen doesn't change, these should
+# all be ignored.
+proc test_multi_key_combos {} {
+    foreach mk { \030\001 \030a \030A \0302 \0301 \030o \030s } {
+	set before [Term::get_region 0 0 80 24 "\r\n"]
+	send_gdb $mk
+
+	# It's not easy to check that pressing a key doesn't cause the
+	# display to change.  If we read the output then either the
+	# display didn't change, or we read the display to quickly,
+	# and the changes haven't been drawn yet.
+	#
+	# We could just throw a 'sleep 1' in here, which will mostly
+	# work, but still depends on GDB responding fast enough.
+	#
+	# To avoid this race we send the string '123' to GDB.  As
+	# GDB's UI is single threaded, by the time '123' is drawn then
+	# any effects from the multi-key combination should also have
+	# been drawn.
+	#
+	# We can then delete the '123' by sending 'C-u' to clear the
+	# line, and after this we can check that the display is
+	# exactly as it was before we started, this means that the
+	# multi-key combination didn't result in a UI change.
+
+	set a_string 123
+	send_gdb $a_string
+	Term::wait_for_region_contents 0 0 80 24 $a_string
+
+	send_gdb "\025"
+	Term::wait_for_region_contents 0 0 80 24 [string_to_regexp $before]
+
+	set after [Term::get_region 0 0 80 24 "\r\n"]
+	gdb_assert { $before eq $after } \
+	    "no changes after sending '[unprintable_to_octal $mk]'"
+    }
+}
+
+# Send CHAR followed by a newline to GDB, this should quit the
+# currently active secondary prompt.  Wait for the GDB prompt to
+# present, then send a basic print command to GDB to ensure that GDB
+# is still interactive.
+proc quit_prompt { char } {
+    send_gdb "${char}\n"
+    Term::wait_for ""
+
+    gdb_test "print 1" " =3D 1"
+}
+
+# Trigger the pagination prompt, then check that the multi-key
+# combinations don't result in a change to TUI mode.  Finally, check
+# that GDB is still responsive.
+proc_with_prefix test_from_pager_prompt {} {
+    Term::clean_restart 24 80 $::testfile
+
+    set cmd "echo "
+    for { set i 0 } { $i < 100 } { incr i } {
+	set cmd "${cmd}${i}\\n"
+    }
+
+    gdb_test_no_output "set pagination on"
+    gdb_test_no_output "set height 24"
+    send_gdb "$cmd\n"
+
+    if {![Term::wait_for_region_contents 0 0 80 24 \
+	      [string_to_regexp $::pagination_prompt_str]]} {
+	fail "spot secondary prompt"
+	return
+    }
+
+    test_multi_key_combos
+    quit_prompt q
+}
+
+# Trigger the debuginfod prompt, then check that the multi-key
+# combinations don't result in a change to TUI mode.  Finally, check
+# GDB is still responsive.
+proc_with_prefix test_from_debuginfod_prompt {} {
+    if { ![allow_debuginfod_tests] } {
+	return
+    }
+
+    set stripped_binfile [standard_output_file ${::testfile}-stripped]
+    file copy -force $::binfile $stripped_binfile
+    if {[gdb_gnu_strip_debug $stripped_binfile no-debuglink]} {
+	unsupported "produce separate debug info for [file tail $stripped_binfile=
]"
+	return
+    }
+
+    if {[section_get $stripped_binfile ".gnu_debuglink"] ne ""} {
+	unsupported "debug information has already been split out"
+	return
+    }
+
+    save_vars { env(DEBUGINFOD_URLS) } {
+	setenv DEBUGINFOD_URLS "foo"
+	Term::clean_restart 24 80
+    }
+
+    send_gdb "file \"$stripped_binfile\"\n"
+    if {![Term::wait_for_region_contents 0 0 80 24 \
+	      [string_to_regexp {(y or [n]) }]]} {
+	fail "spot secondary prompt"
+	return
+    }
+
+    test_multi_key_combos
+    quit_prompt n
+}
+
+# Trigger the pending breakpoint prompt, then check that the multi-key
+# combinations don't result in a change to TUI mode.  Finally, check
+# that GDB is still responsive.
+proc_with_prefix test_from_breakpoint_prompt {} {
+    Term::clean_restart 24 80 $::testfile
+
+    send_gdb "break _a_function_that_hopefully_doesnt_exist_\n"
+    if {![Term::wait_for_region_contents 0 0 80 24 \
+	      [string_to_regexp {(y or [n]) }]]} {
+	fail "spot secondary prompt"
+	return
+    }
+
+    test_multi_key_combos
+    quit_prompt n
+}
+
+test_from_pager_prompt
+test_from_debuginfod_prompt
+test_from_breakpoint_prompt
diff --git a/gdb/testsuite/lib/gdb.exp b/gdb/testsuite/lib/gdb.exp
index 41e9a8c721e..6fb04869605 100644
--- a/gdb/testsuite/lib/gdb.exp
+++ b/gdb/testsuite/lib/gdb.exp
@@ -12347,6 +12347,33 @@ proc dwz_version_at_least { ver } {
     return [version_compare [split $ver .] <=3D [dwz_version]]
 }
=20
+# Take a string containing unprintable characters and return a string
+# with the unprintable characters represented as 3 octal bytes,
+# e.g. "\030".
+#
+# To avoid having to escape backslashes, backslashes themselves are
+# also represented as octal.
+proc unprintable_to_octal { input_string } {
+    set result ""
+
+    foreach char [split $input_string ""] {
+	# Convert character to its integer ASCII value.
+	scan $char %c ascii_val
+
+	# Include printable characters as literals.  Exclude backslash
+	# to avoid needing to escape it, we don't actually expect to
+	# see that in INPUT_STRING though.
+	if {[string is print $char] && $char ne "\\"} {
+	    append result $char
+	} else {
+	    # Include non-printable characters as a 3-digit octal.
+	    append result [format "\\%03o" $ascii_val]
+	}
+    }
+
+    return $result
+}
+
 require {tcl_version_at_least 8 6 2}
=20
 # Always load compatibility stuff.
diff --git a/gdb/tui/tui.c b/gdb/tui/tui.c
index 958fc47b7b6..4c61e4b879c 100644
--- a/gdb/tui/tui.c
+++ b/gdb/tui/tui.c
@@ -114,6 +114,7 @@ static Keymap tui_readline_standard_keymap;
 static int
 tui_rl_switch_mode (int notused1 =3D 0, int notused2 =3D 0)
 {
+  gdb_assert (!gdb_in_secondary_prompt_p (current_ui));
=20
   /* Don't let exceptions escape.  We're in the middle of a readline
      callback that isn't prepared for that.  */
@@ -198,6 +199,8 @@ tui_try_activate ()
 static int
 tui_rl_change_windows (int notused1, int notused2)
 {
+  gdb_assert (!gdb_in_secondary_prompt_p (current_ui));
+
   if (tui_try_activate ())
     tui_next_layout ();
=20
@@ -209,6 +212,8 @@ tui_rl_change_windows (int notused1, int notused2)
 static int
 tui_rl_delete_other_windows (int notused1, int notused2)
 {
+  gdb_assert (!gdb_in_secondary_prompt_p (current_ui));
+
   if (tui_try_activate ())
     tui_remove_some_windows ();
=20
@@ -220,6 +225,8 @@ tui_rl_delete_other_windows (int notused1, int notused2)
 static int
 tui_rl_other_window (int count, int key)
 {
+  gdb_assert (!gdb_in_secondary_prompt_p (current_ui));
+
   if (tui_try_activate ())
     tui_set_win_focus_to (tui_next_win (tui_win_with_focus ()));
=20
@@ -231,10 +238,10 @@ tui_rl_other_window (int count, int key)
 static int
 tui_rl_command_key (int count, int key)
 {
-  int i;
+  gdb_assert (!gdb_in_secondary_prompt_p (current_ui));
=20
   reinitialize_more_filter ();
-  for (i =3D 0; tui_commands[i].cmd; i++)
+  for (int i =3D 0; tui_commands[i].cmd; i++)
     {
       if (tui_commands[i].key =3D=3D key)
 	{
@@ -262,6 +269,8 @@ tui_rl_command_key (int count, int key)
 static int
 tui_rl_command_mode (int count, int key)
 {
+  gdb_assert (!gdb_in_secondary_prompt_p (current_ui));
+
   tui_set_key_mode (TUI_ONE_COMMAND_MODE);
   return rl_insert (count, key);
 }
@@ -271,6 +280,8 @@ tui_rl_command_mode (int count, int key)
 static int
 tui_rl_next_keymap (int notused1, int notused2)
 {
+  gdb_assert (!gdb_in_secondary_prompt_p (current_ui));
+
   if (!tui_try_activate ())
     return 0;
=20
@@ -310,6 +321,20 @@ tui_set_key_mode (enum tui_key_mode mode)
   tui_show_status_content ();
 }
=20
+/* Wrapper around function FPTR, used to add common checks before
+   functions that are bound to readline multi-key combinations.  */
+
+template<int (*FPTR) (int, int)>
+int
+tui_rl_keybinding (int count, int key)
+{
+  /* Don't allow TUI changes while we're at an interactive prompt.  */
+  if (gdb_in_secondary_prompt_p (current_ui))
+    return 0;
+
+  return FPTR (count, key);
+}
+
 /* Initialize readline and configure the keymap for the switching
    key shortcut.  */
 void
@@ -324,11 +349,16 @@ tui_ensure_readline_initialized ()
   int i;
   Keymap tui_ctlx_keymap;
=20
-  rl_add_defun ("tui-switch-mode", tui_rl_switch_mode, -1);
-  rl_add_defun ("next-keymap", tui_rl_next_keymap, -1);
-  rl_add_defun ("tui-delete-other-windows", tui_rl_delete_other_windows, -=
1);
-  rl_add_defun ("tui-change-windows", tui_rl_change_windows, -1);
-  rl_add_defun ("tui-other-window", tui_rl_other_window, -1);
+  rl_add_defun ("tui-switch-mode",
+		tui_rl_keybinding<tui_rl_switch_mode>, -1);
+  rl_add_defun ("next-keymap",
+		tui_rl_keybinding<tui_rl_next_keymap>, -1);
+  rl_add_defun ("tui-delete-other-windows",
+		tui_rl_keybinding<tui_rl_delete_other_windows>, -1);
+  rl_add_defun ("tui-change-windows",
+		tui_rl_keybinding<tui_rl_change_windows>, -1);
+  rl_add_defun ("tui-other-window",
+		tui_rl_keybinding<tui_rl_other_window>, -1);
=20
   tui_keymap =3D rl_make_bare_keymap ();
=20
@@ -361,21 +391,21 @@ tui_ensure_readline_initialized ()
       rl_bind_key_in_map (i, tui_rl_command_mode, tui_keymap);
     }
=20
-  rl_bind_key_in_map ('a', tui_rl_switch_mode, emacs_ctlx_keymap);
-  rl_bind_key_in_map ('a', tui_rl_switch_mode, tui_ctlx_keymap);
-  rl_bind_key_in_map ('A', tui_rl_switch_mode, emacs_ctlx_keymap);
-  rl_bind_key_in_map ('A', tui_rl_switch_mode, tui_ctlx_keymap);
-  rl_bind_key_in_map (c_ctrl ('A'), tui_rl_switch_mode, emacs_ctlx_keymap);
-  rl_bind_key_in_map (c_ctrl ('A'), tui_rl_switch_mode, tui_ctlx_keymap);
-  rl_bind_key_in_map ('1', tui_rl_delete_other_windows, emacs_ctlx_keymap);
-  rl_bind_key_in_map ('1', tui_rl_delete_other_windows, tui_ctlx_keymap);
-  rl_bind_key_in_map ('2', tui_rl_change_windows, emacs_ctlx_keymap);
-  rl_bind_key_in_map ('2', tui_rl_change_windows, tui_ctlx_keymap);
-  rl_bind_key_in_map ('o', tui_rl_other_window, emacs_ctlx_keymap);
-  rl_bind_key_in_map ('o', tui_rl_other_window, tui_ctlx_keymap);
-  rl_bind_key_in_map ('q', tui_rl_next_keymap, tui_keymap);
-  rl_bind_key_in_map ('s', tui_rl_next_keymap, emacs_ctlx_keymap);
-  rl_bind_key_in_map ('s', tui_rl_next_keymap, tui_ctlx_keymap);
+  rl_bind_key_in_map ('a', tui_rl_keybinding<tui_rl_switch_mode>, emacs_ct=
lx_keymap);
+  rl_bind_key_in_map ('a', tui_rl_keybinding<tui_rl_switch_mode>, tui_ctlx=
_keymap);
+  rl_bind_key_in_map ('A', tui_rl_keybinding<tui_rl_switch_mode>, emacs_ct=
lx_keymap);
+  rl_bind_key_in_map ('A', tui_rl_keybinding<tui_rl_switch_mode>, tui_ctlx=
_keymap);
+  rl_bind_key_in_map (c_ctrl ('A'), tui_rl_keybinding<tui_rl_switch_mode>,=
 emacs_ctlx_keymap);
+  rl_bind_key_in_map (c_ctrl ('A'), tui_rl_keybinding<tui_rl_switch_mode>,=
 tui_ctlx_keymap);
+  rl_bind_key_in_map ('1', tui_rl_keybinding<tui_rl_delete_other_windows>,=
 emacs_ctlx_keymap);
+  rl_bind_key_in_map ('1', tui_rl_keybinding<tui_rl_delete_other_windows>,=
 tui_ctlx_keymap);
+  rl_bind_key_in_map ('2', tui_rl_keybinding<tui_rl_change_windows>, emacs=
_ctlx_keymap);
+  rl_bind_key_in_map ('2', tui_rl_keybinding<tui_rl_change_windows>, tui_c=
tlx_keymap);
+  rl_bind_key_in_map ('o', tui_rl_keybinding<tui_rl_other_window>, emacs_c=
tlx_keymap);
+  rl_bind_key_in_map ('o', tui_rl_keybinding<tui_rl_other_window>, tui_ctl=
x_keymap);
+  rl_bind_key_in_map ('q', tui_rl_keybinding<tui_rl_next_keymap>, tui_keym=
ap);
+  rl_bind_key_in_map ('s', tui_rl_keybinding<tui_rl_next_keymap>, emacs_ct=
lx_keymap);
+  rl_bind_key_in_map ('s', tui_rl_keybinding<tui_rl_next_keymap>, tui_ctlx=
_keymap);
=20
   /* Initialize readline after the above.  */
   rl_initialize ();