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 8bd08ee92c
  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 43db8f70d8
  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 <simon.marchi@efficios.com>
Reviewed-By: Guinevere Larsen <guinevere@redhat.com>
This commit is contained in:
Andrew Burgess 2025-10-13 14:43:51 +01:00
parent 7be6efd7d4
commit e0f0649602
5 changed files with 74 additions and 5 deletions

71
gdb/finish-thread-state.h Normal file
View 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 */

View file

@ -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;

View file

@ -57,6 +57,7 @@
#include "source.h"
#include "cli/cli-style.h"
#include "gdbsupport/selftest.h"
#include "finish-thread-state.h"
/* Local functions: */

View file

@ -75,6 +75,7 @@
#include "extension.h"
#include "disasm.h"
#include "interps.h"
#include "finish-thread-state.h"
/* Prototypes for local functions */

View file

@ -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. */