mirror of
https://sourceware.org/git/binutils-gdb.git
synced 2026-08-27 00:26:02 -04:00
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: commit8bd08ee92cDate: 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: commit43db8f70d8Date: 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 <simon.marchi@efficios.com> Reviewed-By: Guinevere Larsen <guinevere@redhat.com>
This commit is contained in:
parent
7be6efd7d4
commit
e0f0649602
5 changed files with 74 additions and 5 deletions
71
gdb/finish-thread-state.h
Normal file
71
gdb/finish-thread-state.h
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>. */
|
||||
|
||||
#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<process_stratum_target *> (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<detail::scoped_finish_thread_state_policy>;
|
||||
|
||||
#endif /* GDB_FINISH_THREAD_STATE_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;
|
||||
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
#include "source.h"
|
||||
#include "cli/cli-style.h"
|
||||
#include "gdbsupport/selftest.h"
|
||||
#include "finish-thread-state.h"
|
||||
|
||||
/* Local functions: */
|
||||
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@
|
|||
#include "extension.h"
|
||||
#include "disasm.h"
|
||||
#include "interps.h"
|
||||
#include "finish-thread-state.h"
|
||||
|
||||
/* Prototypes for local functions */
|
||||
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue