binutils-gdb/gdb/run-on-main-thread.c
Tom de Vries 4314167cec [gdb] Handle pagination prompt during posted event
PR python/24796 reports the following problem.

We have a python file test.py:
...
import gdb
def Event():
  for x in range(0, 10):
    print("Line")
gdb.post_event(Event)
...

Without pagination, we simple have:
...
(gdb) source test.py
(gdb) Line
Line
  ...
Line
Line
print 1
$1 = 1
(gdb)
...

But with pagination (and height set to 8), we get instead:
...
(gdb) source test.py
(gdb) Line
Line
  ...
Line
--Type <RET> for more, q to quit, c to continue without paging--c
Line
Line
Line
print 1
readline: readline_callback_read_char() called with no handler!

Fatal signal: Aborted
----- Backtrace -----
0x651099 gdb_internal_backtrace_1
	/data/vries/gdb/src/gdb/bt-utils.c:122
0x651137 _Z22gdb_internal_backtracev
	/data/vries/gdb/src/gdb/bt-utils.c:175
0x9462aa handle_fatal_signal
	/data/vries/gdb/src/gdb/event-top.c:1008
0x7f4efaf5f08f ???
	/usr/src/debug/glibc-2.40/signal/../sysdeps/unix/sysv/linux/x86_64/libc_sigaction.c:0
0x7f4efafb245c __pthread_kill_implementation
	/usr/src/debug/glibc-2.40/nptl/pthread_kill.c:44
0x7f4efaf5efc5 __GI_raise
	../sysdeps/posix/raise.c:26
0x7f4efaf46916 __GI_abort
	/usr/src/debug/glibc-2.40/stdlib/abort.c:79
0x117502b rl_callback_read_char
	/data/vries/gdb/src/readline/readline/callback.c:139
0x944bc2 gdb_rl_callback_read_char_wrapper_sjlj
	/data/vries/gdb/src/gdb/event-top.c:197
0x944cd3 gdb_rl_callback_read_char_wrapper_noexcept
	/data/vries/gdb/src/gdb/event-top.c:240
0x944d51 gdb_rl_callback_read_char_wrapper
	/data/vries/gdb/src/gdb/event-top.c:252
0x10623e3 stdin_event_handler
	/data/vries/gdb/src/gdb/ui.c:154
0x1a06e9c handle_file_event
	/data/vries/gdb/src/gdbsupport/event-loop.cc:551
0x1a074df gdb_wait_for_event
	/data/vries/gdb/src/gdbsupport/event-loop.cc:672
0x1a063bc _Z16gdb_do_one_eventi
	/data/vries/gdb/src/gdbsupport/event-loop.cc:263
0x6d547f _ZN6interp12do_one_eventEi
	/data/vries/gdb/src/gdb/interps.h:93
0xb77fb6 start_event_loop
	/data/vries/gdb/src/gdb/main.c:403
0xb781a4 captured_command_loop
	/data/vries/gdb/src/gdb/main.c:468
0xb7a10d captured_main
	/data/vries/gdb/src/gdb/main.c:1381
0xb7a209 _Z8gdb_mainP18captured_main_args
	/data/vries/gdb/src/gdb/main.c:1400
0x419704 main
	/data/vries/gdb/src/gdb/gdb.c:38
...

What leads to the crash, is that this code in gdb_readline_wrapper_line:
...
  /* Prevent parts of the prompt from being redisplayed if annotations
     are enabled, and readline's state getting out of sync.  We'll
     reinstall the callback handler, which puts the terminal in raw
     mode (or in readline lingo, in prepped state), when we're next
     ready to process user input, either in display_gdb_prompt, or if
     we're handling an asynchronous target event and running in the
     background, just before returning to the event loop to process
     further input (or more target events).  */
  if (current_ui->command_editing)
    gdb_rl_callback_handler_remove ();
...
when activated by the pagination prompt removes the callback handler (which
sets rl_linefunc to NULL).

The comment mentions re-installing the callback handler, but that doesn't
happen, so when calling rl_callback_read_char we run into:
...
  if (rl_linefunc == NULL)
    {
      _rl_errmsg ("readline_callback_read_char() called with no handler!");
      abort ();
    }
...

An example of re-installation is found here in infrun.c:
...
/* Cleanup that reinstalls the readline callback handler, if the
   target is running in the background.  If while handling the target
   event something triggered a secondary prompt, like e.g., a
   pagination prompt, we'll have removed the callback handler (see
   gdb_readline_wrapper_line).  Need to do this as we go back to the
   event loop, ready to process further input.  Note this has no
   effect if the handler hasn't actually been removed, because calling
   rl_callback_handler_install resets the line buffer, thus losing
   input.  */

static void
void
reinstall_readline_callback_handler_cleanup ()
...

Fix the crash by adding the same cleanup in run_events.

Tested on x86_64-linux.

Approved-By: Andrew Burgess <aburgess@redhat.com>

Bug: https://sourceware.org/bugzilla/show_bug.cgi?id=24796
2026-02-20 12:21:05 +01:00

144 lines
4.1 KiB
C

/* Run a function on the main thread
Copyright (C) 2019-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/>. */
#include "run-on-main-thread.h"
#include "ser-event.h"
#include "gdbsupport/cleanups.h"
#include "gdbsupport/cxx-thread.h"
#include "gdbsupport/event-loop.h"
#include "infrun.h"
#include "gdbsupport/scope-exit.h"
/* The serial event used when posting runnables. */
static struct serial_event *runnable_event;
/* Runnables that have been posted. */
static std::vector<std::function<void ()>> runnables;
/* Mutex to hold when handling RUNNABLE_EVENT or RUNNABLES. */
static gdb::mutex runnable_mutex;
/* The main thread's thread id. */
static gdb::thread::id main_thread_id;
/* Run all the queued runnables. */
static void
run_events (int error, gdb_client_data client_data)
{
std::vector<std::function<void ()>> local;
/* Hold the lock while changing the globals, but not while running
the runnables. */
{
gdb::lock_guard<gdb::mutex> lock (runnable_mutex);
/* Clear the event fd. Do this before flushing the events list,
so that any new event post afterwards is sure to re-awaken the
event loop. */
serial_event_clear (runnable_event);
/* Move the vector in case running a runnable pushes a new
runnable. */
local = std::move (runnables);
}
/* Schedule cleanup in case secondary prompts (for instance, the pagination
prompt) happened while running events. */
SCOPE_EXIT { reinstall_readline_callback_handler_cleanup (); };
for (auto &item : local)
{
try
{
item ();
}
catch (const gdb_exception_forced_quit &e)
{
/* GDB is terminating, so:
- make sure this is propagated, and
- no need to keep running things, so propagate immediately. */
throw;
}
catch (const gdb_exception_quit &e)
{
/* Should cancellation of a runnable event cancel the execution of
the following one? The answer is not clear, so keep doing what
we've done so far: ignore this exception. */
}
catch (const gdb_exception &)
{
/* Ignore exceptions in the callback. */
}
}
}
/* See run-on-main-thread.h. */
void
run_on_main_thread (std::function<void ()> &&func)
{
gdb::lock_guard<gdb::mutex> lock (runnable_mutex);
runnables.emplace_back (std::move (func));
serial_event_set (runnable_event);
}
static bool main_thread_id_initialized = false;
/* See run-on-main-thread.h. */
bool
is_main_thread ()
{
/* Initialize main_thread_id on first use of is_main_thread. */
if (!main_thread_id_initialized)
{
main_thread_id_initialized = true;
main_thread_id = gdb::this_thread::get_id ();
}
return gdb::this_thread::get_id () == main_thread_id;
}
INIT_GDB_FILE (run_on_main_thread)
{
/* The variable main_thread_id should be initialized when entering main, or
at an earlier use, so it should already be initialized here. */
gdb_assert (main_thread_id_initialized);
/* Assume that we execute this in the main thread. */
gdb_assert (is_main_thread ());
runnable_event = make_serial_event ();
add_file_handler (serial_event_fd (runnable_event), run_events, nullptr,
"run-on-main-thread");
/* A runnable may refer to an extension language. So, we want to
make sure any pending ones have been deleted before the extension
languages are shut down. */
add_final_cleanup ([] ()
{
gdb::lock_guard<gdb::mutex> lock (runnable_mutex);
runnables.clear ();
});
}