[binutils-gdb] gdbserver/linux-low: carry over stop_expected flag after exec (avoid spurious SIGSTOPs)

Simon Marchi via Gdb-cvs <[email protected]> Sat, 27 Jun 2026 02:37:28 +0000 (GMT)
Newsgroups gmane.comp.gdb.cvs
Message-ID <[email protected]>
https://sourceware.org/git/gitweb.cgi?p=binutils-gdb.git;h=e17f386d7ffdd9604a0aad15a76606b846a2b9ed

commit e17f386d7ffdd9604a0aad15a76606b846a2b9ed
Author: Simon Marchi <[email protected]>
Date:   Fri Jun 26 10:32:28 2026 -0400

    gdbserver/linux-low: carry over stop_expected flag after exec (avoid spurious SIGSTOPs)
    
    Running test gdb.base/vfork-follow-parent.exp on the native-gdbserver
    board is flaky.  This test has GDB debugging a vfork parent and child.
    When resuming the child, we expect it to run through an exec, and then
    exit, like so:
    
        continue
        Continuing.
        [New inferior 2 (process 3286309)]
        process 3286309 is executing new program: /home/simark/build/binutils-gdb/gdb/testsuite/outputs/gdb.base/vfork-follow-parent/vforked-prog
        [Inferior 2 (process 3286309) exited normally]
        (gdb) PASS: gdb.base/vfork-follow-parent.exp: exec_file=vfork-follow-parent-exec: target-non-stop=off: non-stop=off: resolution_method=schedule-multiple: continue to end of inferior 2
    
    Instead, we sometimes see it reporting a spurious SIGSTOP:
    
        continue
        Continuing.
        [New inferior 2 (process 3281383)]
        process 3281383 is executing new program: /home/simark/build/binutils-gdb/gdb/testsuite/outputs/gdb.base/vfork-follow-parent/vforked-prog
    
        Thread 2.1 "vforked-prog" received signal SIGSTOP, Stopped (signal).
        Cannot remove breakpoints because program is no longer writable.
        Further execution is probably impossible.
        0x00007ffff7fe0300 in ?? () from /lib64/ld-linux-x86-64.so.2
        (gdb) FAIL: gdb.base/vfork-follow-parent.exp: exec_file=vfork-follow-parent-exec: target-non-stop=off: non-stop=off: resolution_method=schedule-multiple: continue to end of inferior 2
    
    Note that this case uses "target-non-stop=off", meaning that when one
    thread reports a stop, GDBserver attempts to stop the other threads
    itself before reporting the stop to GDB.  The bug is related to
    GDBserver's bookkeeping of the threads.
    
    When running the test 100 times, it failed 6 times.  Using taskset to
    pin the process subtree to a single CPU seemed to make it more likely
    to fail: on 100 runs, it failed 30 times.
    
    The cause
    =========
    
    The steps occurring in the "pass" case are:
    
     1. Process P (Parent) is blocked inside the vfork system call, waiting
        for process C (Child) to exec or exit.
     2. Process C is currently stopped at its first instruction (right out
        of vfork).
     3. We resume process C (the "continue" seen in the logs above).
     4. Process C calls exec, as a result the kernel produces two events to
        be consumed by the tracer (GDBserver):
    
         - For P, a vfork done event (PTRACE_EVENT_VFORK_DONE)
         - For C, an exec event (PTRACE_EVENT_EXEC)
    
     5. Let's suppose here that waitpid happens to return the
        PTRACE_EVENT_EXEC for C first.
     6. Upon receiving the event, GDBserver calls stop_all_lwps, which sends
        a SIGSTOP to P, and sets `lwp->stop_expected` for P.
     7. GDBserver waits for P to stop, waitpid returns the
        PTRACE_EVENT_VFORK_DONE event.  GDBserver stashes it in
        lwp->status_pending.  The SIGSTOP for P is still pending at the
        kernel level.
     8. When we resume C, everything is fine, it runs until exit.
     9. When we resume P, GDBserver receives the SIGSTOP event for P, but
        suppresses it because `lwp->stop_expected` is set for P.
    
    Now, for the failing case, imagine that at step 5 the kernel decided to
    return the PTRACE_EVENT_VFORK_DONE event for P first.  The following
    steps would play out like this:
    
     6. Upon receiving the event, GDBserver calls stop_all_lwps, which sends
        a SIGSTOP to C, and sets `lwp->stop_expected` for C.
     7. GDBserver waits for C to stop, waitpid returns the PTRACE_EVENT_EXEC
        event.
     8. In the handling of PTRACE_EVENT_EXEC (in
        linux_process_target::handle_extended_wait), GDBserver deletes the
        process and its threads (the mourn call), and creates a brand new
        process and lwp_info for C.  Note that this loses the previously set
        `lwp->stop_expected`.  The SIGSTOP for C is still pending at the
        kernel level.
     9. When we resume C, GDBserver receives the SIGSTOP event for C, and
        because `lwp->stop_expected` is not set, GDBserver doesn't recognize
        it as its own, and the stop is presented to the user.
    
    The fix
    =======
    
    The (simplest) fix is to transfer the `lwp->stop_expected` from the old
    lwp_info to the new one.  When doing so, when we resume C and GDBserver
    receives the SIGSTOP event for C, it recognizes it as its own and
    suppresses it.
    
    Some care is needed in case process C is multi-threaded and the exec is
    done by a non-leader thread.  When a non-leader thread execs, the kernel
    scraps all other threads and renumbers this one to the tgid, so that it
    becomes the new leader.  The PTRACE_EVENT_EXEC event is reported using
    that new renumbered id.  But if a SIGSTOP was pending for the non-leader
    exec'ing thread when the exec happened, it will still be pending
    post-exec for that thread under its new post-exec leader identity.
    
    Here is a hypothetical but more concrete scenario:
    
     - There are two threads, 100.100 (the leader) and 100.101.
     - Thread 100.101 is stopped at the entry of the execve system call (so
       the effects of execve haven't occurred yet) when GDBserver sends it a
       SIGSTOP and sets `lwp->stop_expected`.  The SIGSTOP becomes pending
       in the kernel.
     - When 100.101 is resumed, the exec occurs, the kernel deletes thread
       100.100 and renumbers 100.101 to 100.100.  The latter still has the
       SIGSTOP pending.
     - GDBserver receives a PTRACE_EVENT_EXEC event for thread 100.100.
     - Upon resumption, GDBserver then receives an event for the SIGSTOP,
       for thread 100.100.
    
    All this to say that when transferring the `lwp->stop_expected` flag
    from the old lwp_info to the new, we must take care to read the exec'ing
    thread's flag.  If we use the id reported for the PTRACE_EVENT_EXEC to
    look up an lwp_info, then we'll get the leader's lwp_info, which may not
    have `lwp->stop_expected` set.  Instead, we must get the exec'ing
    thread's original id using PTRACE_GETEVENTMSG, and use that to source
    the right lwp_info to transfer the `lwp->stop_expected` flag.
    
    There is a comment about using PTRACE_GETEVENTMSG with PTRACE_EVENT_EXEC
    in gdb/linux-nat.c:
    
        ...
        tid to the tgid, and the previous leader vanishes.  Since
        Linux 3.0, the former thread ID can be retrieved with
        PTRACE_GETEVENTMSG, but since we support older kernels, don't
        bother with it, and just walk the LWP list.  Even with
        ...
    
    Linux 3.0 was released in 2011, so I think it's fine to use that.
    
    With the fix, I don't get any failures after 100 test runs (even with
    taskset).
    
    Kernel behavior experiments
    ===========================
    
    The fix relies on assumptions about how the kernel orders ptrace events
    and handles pending signals across exec.  I experimented with these
    using standalone ptrace programs (mostly written by my buddy Claude).
    The programs would drive a thread of the tracee to the execve syscall
    entry, deliver a SIGSTOP to a thread, resume things and then look at
    what events would come out of waitpid.  Here are the scenarios I tried:
    
      - Single-threaded: a tracee stopped at execve syscall entry is sent a
        SIGSTOP, then allowed to exec.  The kernel reports PTRACE_EVENT_EXEC first,
        then the SIGSTOP.
    
      - Multi-threaded, non-leader exec: a non-leader thread stopped at
        execve syscall entry is sent a SIGSTOP, then allowed to exec.  The
        kernel reports PTRACE_EVENT_EXEC, then the SIGSTOP, both under the
        leader id (the exec'ing non-leader got renumbered).
    
      - Multi-threaded, leader has a pending SIGSTOP while a non-leader
        execs: the leader is sent a SIGSTOP, which stays pending, then we
        let the non-leader thread exec (giving it a few seconds to be sure).
        The kernel reports PTRACE_EVENT_EXEC under the leader id (the
        exec'ing non-leader got renumbered) and the SIGSTOP has vanished.
    
    What about GDB
    ==============
    
    I tried to check if the same bug could happen with GDB's linux-nat
    target, and if the same fix was needed.  linux-nat takes a different
    approach when handling PTRACE_EVENT_EXEC.  It wipes all lwp_infos except
    the leader:
    
          for (lwp_info &other_lp : all_lwps_safe ())
            if (&other_lp != lp && other_lp.ptid.pid () == lp->ptid.pid ())
              exit_lwp (&other_lp);
    
    Here, LP is an lwp_info obtained using the event ptid of the
    PTRACE_EVENT_EXEC, therefore the leader's lwp_info (even if the exec was
    done by a non-leader).
    
    If the exec is done by the leader, as is the case with
    gdb.base/vfork-follow-parent.exp, we are ok.  Because GDB doesn't delete
    and re-create the lwp_info, the equivalent of GDBserver's
    `lwp_info::stop_expected`, `lwp_info::signalled`, survives the exec.
    
    If the exec is done by a non-leader, then we could be in trouble.  If
    the leader's signalled flag is not set, but the exec'ing non-leader's
    flag is set, then we'll lose it.  I suppose we could fix GDB to use
    PTRACE_GETEVENTMSG to get the exec'ing thread former id, look up the
    lwp_info for that id, and preserve that lwp_info.
    
    Change-Id: Iaebd1d2cf813dcad35d7d8639bbaed80d40b7d1e
    Approved-By: Pedro Alves <[email protected]>

Diff:
---
 gdbserver/linux-low.cc | 37 +++++++++++++++++++++++++++++++++++++
 1 file changed, 37 insertions(+)

diff --git a/gdbserver/linux-low.cc b/gdbserver/linux-low.cc
index ade5e9e2a1c..86ea2451c1c 100644
--- a/gdbserver/linux-low.cc
+++ b/gdbserver/linux-low.cc
@@ -732,6 +732,40 @@ linux_process_target::handle_extended_wait (lwp_info **orig_event_lwp,
       event_ptid = event_thr->id;
       event_pid = event_ptid.pid ();
 
+      /* If GDBserver had sent the exec'ing thread a SIGSTOP and the thread
+	 exec'ed before consuming it, the SIGSTOP stays pending across the exec.
+	 The pending SIGSTOP is reported once the post-exec thread is resumed.
+	 Carry the expectation over to the new lwp_info below, so that it is
+	 filtered out instead of being reported to GDB.
+
+	 Things get interesting if a non-leader thread does the exec.  The
+	 kernel deletes the other threads, changes the exec'ing thread's id
+	 so it matches the tgid (becomes the new leader), and reports the
+	 PTRACE_EVENT_EXEC for that leader id.  If a SIGSTOP was pending on
+	 the exec'ing non-leader as the exec happened, it will still be pending
+	 post-exec, but it will be reported under leader id.
+
+	 Since EVENT_LWP was looked up using the leader id, it represents the
+	 former leader, not necessarily the exec'ing thread.  To properly carry
+	 the stop expectation from the exec'ing thread's lwp_info to the new
+	 lwp_info, use PTRACE_GETEVENTMSG to obtain the exec'ing thread's former
+	 id, and look up the lwp_info from that.
+
+	 If a SIGSTOP is pending (in the kernel) on the leader when a non-leader
+	 execs, then that SIGSTOP disappears with the thread.  */
+      unsigned long execing_tid;
+      if (ptrace (PTRACE_GETEVENTMSG, event_ptid.lwp (), (PTRACE_TYPE_ARG3) 0,
+		  &execing_tid) != 0)
+	{
+	  /* If ptrace fails, fall back to using the leader.  */
+	  execing_tid = event_ptid.lwp ();
+	}
+
+      lwp_info *execing_lwp
+	= find_lwp_pid (ptid_t (event_pid, execing_tid));
+      bool stop_expected
+	= execing_lwp != nullptr && execing_lwp->stop_expected;
+
       /* Save the syscall list from the execing process.  */
       process_info *proc = event_thr->process ();
       syscalls_to_catch = std::move (proc->syscalls_to_catch);
@@ -747,6 +781,9 @@ linux_process_target::handle_extended_wait (lwp_info **orig_event_lwp,
       gdb_assert (current_thread == event_thr);
       arch_setup_thread (event_thr);
 
+      /* Carry over a possible SIGSTOP expectation.  */
+      event_lwp->stop_expected = stop_expected;
+
       /* Set the event status.  */
       event_lwp->waitstatus.set_execd
 	(make_unique_xstrdup