From: Andrew Burgess Date: Mon, 13 Oct 2025 13:43:51 +0000 (+0100) Subject: gdb: hold a target_ops_ref in scoped_finish_thread_state X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e0f06496026b516b6815438879f1e83573cf4eae;p=thirdparty%2Fbinutils-gdb.git gdb: hold a target_ops_ref in scoped_finish_thread_state This commit fixes a use after free issue that was reported here: https://inbox.sourceware.org/gdb-patches/68354b98-795a-4b50-9eac-e54aa1d01b9d@simark.ca This issue was exposed by the gdb.replay/missing-thread.exp test that was added in this commit: commit 8bd08ee92c4a7bf2ad9e29c4da32a276ef2257fc Date: Fri May 16 17:56:58 2025 +0100 gdb: crash if thread unexpectedly disappears from thread list It is worth pointing out that the use after free issue existed before this commit, this commit just introduced a test that exposed the issue when GDB is run with the address sanitizer. It has taken a while to get this fix ready for upstream as this fix depended on the recently committed patch: commit 43db8f70d86b2492b79f59342187b919fd58b3dd Date: Thu Oct 23 16:34:20 2025 +0100 gdbsupport: remove undefined behaviour from (forward_)scope_exit The problem is that the first commit above introduces a test which causes the remote target to disconnect while processing an inferior stop event, specifically, within normal_stop (infrun.c), GDB calls update_thread_list, and it is during this call that the inferior disconnects. When the remote target disconnects, GDB immediately unpushes the remote target. See remote_unpush_target and its uses in remote.c. If this is the last use of the remote target, then unpushing it will cause the target to be deleted. This is a problem, because in normal_stop, we have an RAII variable maybe_finish_thread_state, which is an optional scoped_finish_thread_state, and in some cases, this will hold a pointer to the process_startum_target which needs to be finished. So the order of events is: 1. Call to normal_stop. 2. Create maybe_finish_thread_state with a pointer to the current remote_target object. 3. Call update_thread_list. 4. Remote disconnects, GDB unpushes and deletes the current remote_target object. GDB throws an exception. 5. The exception propagates back to normal_stop. 6. The destructor for maybe_finish_thread_state runs, and tries to make use of its cached pointer to the (now deleted) remote_target object. Badness ensues. This bug isn't restricted to normal_stop. If a remote target disconnects anywhere where there is a scoped_finish_thread_state in the call stack then this issue could arise. I think what we need to do is to ensure that the remote_target is not actually deleted until after the scoped_finish_thread_state has been cleaned up. And so, to achieve this, I propose changing scoped_finish_thread_state to hold a target_ops_ref rather than a pointer to the target_ops object. Holding the reference will prevent the object from being deleted. The new scoped_finish_thread_state is defined within its own file, and is a drop in replacement for the existing class. On my local machine the gdb.replay/missing-thread.exp test passes cleanly after this commit (with address sanitizers), but when I test on some other machines with a more recent Fedora install, I'm still seeing test failures (both before and after this patch), though not relating to the address sanitizer (at least, I don't see an error from the sanitizer). I don't think these other issues are directly related to the problem being addressed in this commit, and so I'm proposing this patch for inclusion anyway. I'll continue to look at the test and see if I can fix the other failures too. Or maybe I'll end up having to back out the test. Approved-By: Simon Marchi Reviewed-By: Guinevere Larsen --- diff --git a/gdb/finish-thread-state.h b/gdb/finish-thread-state.h new file mode 100644 index 00000000000..60cd9be2b44 --- /dev/null +++ b/gdb/finish-thread-state.h @@ -0,0 +1,71 @@ +/* Copyright (C) 2025 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 . */ + +#ifndef GDB_FINISH_THREAD_STATE_H +#define GDB_FINISH_THREAD_STATE_H + +#include "gdbsupport/gdb-checked-static-cast.h" +#include "gdbsupport/scope-exit.h" +#include "gdbthread.h" +#include "target.h" +#include "process-stratum-target.h" + +namespace detail +{ + +/* Policy class for use with scope_exit_base. Calls finish_thread_state on + scope exit, unless release() is called to disengage. The release + mechanism is supplied by scope_exit_base. */ + +struct scoped_finish_thread_state_policy +{ + /* Store TARG and PTID for a later call to finish_thread_state. For the + meaning of these arguments, see the comments on finish_thread_state. */ + explicit scoped_finish_thread_state_policy (process_stratum_target *targ, + ptid_t ptid) + : m_ptid (ptid) + { + if (targ != nullptr) + m_target_ref = target_ops_ref::new_reference (targ); + } + + /* Called on scope exit unless release was called, see scope_exit_base + for details. Calls finish_thread_state with stored target and ptid. */ + void on_exit () + { + target_ops *t = m_target_ref.get (); + process_stratum_target *p_target + = gdb::checked_static_cast (t); + finish_thread_state (p_target, m_ptid); + } + +private: + + /* The process and target passed through to finish_thread_state. For + their use see the comments on that function. */ + ptid_t m_ptid; + target_ops_ref m_target_ref; +}; + +} + +/* Calls finish_thread_state on scope exit, unless release() is called to + disengage. */ +using scoped_finish_thread_state + = scope_exit_base; + +#endif /* GDB_FINISH_THREAD_STATE_H */ diff --git a/gdb/gdbthread.h b/gdb/gdbthread.h index 8ab5702d804..015da36ca71 100644 --- a/gdb/gdbthread.h +++ b/gdb/gdbthread.h @@ -859,11 +859,6 @@ extern bool threads_are_executing (process_stratum_target *targ); Notifications are only emitted if the thread state did change. */ extern void finish_thread_state (process_stratum_target *targ, ptid_t ptid); -/* Calls finish_thread_state on scope exit, unless release() is called - to disengage. */ -using scoped_finish_thread_state - = FORWARD_SCOPE_EXIT (finish_thread_state); - /* Commands with a prefix of `thread'. */ extern struct cmd_list_element *thread_cmd_list; diff --git a/gdb/infcmd.c b/gdb/infcmd.c index 5862b3e43ec..862cef6c1bb 100644 --- a/gdb/infcmd.c +++ b/gdb/infcmd.c @@ -57,6 +57,7 @@ #include "source.h" #include "cli/cli-style.h" #include "gdbsupport/selftest.h" +#include "finish-thread-state.h" /* Local functions: */ diff --git a/gdb/infrun.c b/gdb/infrun.c index f1ce7195530..6bcd8ec5cc0 100644 --- a/gdb/infrun.c +++ b/gdb/infrun.c @@ -75,6 +75,7 @@ #include "extension.h" #include "disasm.h" #include "interps.h" +#include "finish-thread-state.h" /* Prototypes for local functions */ diff --git a/gdb/remote.c b/gdb/remote.c index 8581d7d0a3b..1358e1c06b9 100644 --- a/gdb/remote.c +++ b/gdb/remote.c @@ -84,6 +84,7 @@ #include "cli/cli-style.h" #include "gdbsupport/remote-args.h" #include "gdbsupport/gdb_argv_vec.h" +#include "finish-thread-state.h" /* The remote target. */