From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from simark.ca by simark.ca with LMTP id uULEBLCNPmqFVBkAWB0awg (envelope-from ) for ; Fri, 26 Jun 2026 10:33:20 -0400 Authentication-Results: simark.ca; dkim=pass (2048-bit key; unprotected) header.d=polymtl.ca header.i=@polymtl.ca header.a=rsa-sha256 header.s=oct2025 header.b=AYUEWRCE; dkim-atps=neutral Received: by simark.ca (Postfix, from userid 112) id 0EDB81E024; Fri, 26 Jun 2026 10:33:20 -0400 (EDT) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on simark.ca X-Spam-Level: X-Spam-Status: No, score=-5.4 required=5.0 tests=ARC_SIGNED,ARC_VALID,BAYES_00, DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,MAILING_LIST_MULTI, RCVD_IN_DNSWL_MED autolearn=unavailable autolearn_force=no version=4.0.1 Received: from vm01.sourceware.org (vm01.sourceware.org [IPv6:2620:52:6:3111::32]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by simark.ca (Postfix) with ESMTPS id 28FC01E024 for ; Fri, 26 Jun 2026 10:33:19 -0400 (EDT) Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 4E8E64BA2E19 for ; Fri, 26 Jun 2026 14:33:18 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 4E8E64BA2E19 Authentication-Results: sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=polymtl.ca header.i=@polymtl.ca header.a=rsa-sha256 header.s=oct2025 header.b=AYUEWRCE Received: from smtp.polymtl.ca (smtp.polymtl.ca [132.207.4.11]) by sourceware.org (Postfix) with ESMTPS id AEDDC4BA2E09 for ; Fri, 26 Jun 2026 14:32:51 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org AEDDC4BA2E09 Authentication-Results: sourceware.org; dmarc=pass (p=none dis=none) header.from=polymtl.ca Authentication-Results: sourceware.org; spf=pass smtp.mailfrom=polymtl.ca ARC-Filter: OpenARC Filter v1.0.0 sourceware.org AEDDC4BA2E09 Authentication-Results: sourceware.org; arc=none smtp.remote-ip=132.207.4.11 ARC-Seal: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1782484371; cv=none; b=FhtIcjKXAoVrADcu8SyTABP1hUhz2KN+no5LAXQHV731X0b9RXrqbAz56wnKiwItZZpwZdriBqFaYb1gZLFc8qwwQyjNZBxT0aILJYsYm30i5BhMTAaKjYnjZOxTHhF+j0CVr8IqswBFydbPD+AO/wOkvKBCAjpVZuCsko5HWD8= ARC-Message-Signature: i=1; a=rsa-sha256; d=sourceware.org; s=key; t=1782484371; c=relaxed/simple; bh=iey1DpCcGuJDTTwZ4YWyX1dGeV0orzTr4Nzzk5EkD0U=; h=DKIM-Signature:From:To:Subject:Date:Message-ID:MIME-Version; b=r8gCvM66A2dpujIF3vEsqK9QLGzUk6Jt6c3WDqzc+WhZDOm5ezsY2fCj2E8d5Idfs1wZQ7zThLepL+h9TyBARpwWVPufZYcLnBqRXqdlWy5vRUM3hO9Ag4PJ/OTgSx4ALnxJ+h7m+jGPYSojUMbagGooxMnrUehUiqxJTCx0fGo= ARC-Authentication-Results: i=1; sourceware.org; dkim=pass (2048-bit key, unprotected) header.d=polymtl.ca header.i=@polymtl.ca header.a=rsa-sha256 header.s=oct2025 header.b=AYUEWRCE DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org AEDDC4BA2E09 Received: from simark.ca (simark.ca [158.69.221.121]) (authenticated bits=0) by smtp.polymtl.ca (8.14.7/8.14.7) with ESMTP id 65QEWh5J147452 (version=TLSv1/SSLv3 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Fri, 26 Jun 2026 10:32:48 -0400 DKIM-Filter: OpenDKIM Filter v2.11.0 smtp.polymtl.ca 65QEWh5J147452 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=polymtl.ca; s=oct2025; t=1782484368; bh=iCCtN4GBfIqRAPd1AStY5eBkfmcalM2wSNdlHvcwXTU=; h=From:To:Cc:Subject:Date:From; b=AYUEWRCEC8XMDdrOZUfca22X7NkSk1Yp0N9rFS+wDfS+dTf0w4dgwrMGA3akm+4iO 3Oc1MunoC4z061eLKwEpyTJv26hm38Nq1hTSDxSwyfI/D8WVSextgGyfmntdkQ5IhV iWJInQ1j/uu0+7iC0+mAW9CXJ8gEXlAwYH6ZerDrwgZrQQAMnK3cYmJI34Uy047t1U 2PfUwpG5dc//HySUO7kKzZDiWbGoCj0slJf4CBztFLfBDeZsqwE2l/cqqTptRdqSaP iQK1aQd2A6scAwwzwOcsdCxJVypMNfp0d8YXHVEPAYFguR58ViaK3MxTlhxzIHQRBF 1BZJMA5AHSo4g== Received: by simark.ca (Postfix) id 84E7B1E024; Fri, 26 Jun 2026 10:32:43 -0400 (EDT) From: simon.marchi@polymtl.ca To: gdb-patches@sourceware.org Cc: Simon Marchi Subject: [PATCH] gdbserver/linux-low: carry over stop_expected flag after exec (avoid spurious SIGSTOPs) Date: Fri, 26 Jun 2026 10:32:28 -0400 Message-ID: <20260626143242.4033142-1-simon.marchi@polymtl.ca> X-Mailer: git-send-email 2.54.0 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Poly-FromMTA: (simark.ca [158.69.221.121]) at Fri, 26 Jun 2026 14:32:43 +0000 X-BeenThere: gdb-patches@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Gdb-patches mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: gdb-patches-bounces~public-inbox=simark.ca@sourceware.org From: Simon Marchi 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 --- gdbserver/linux-low.cc | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/gdbserver/linux-low.cc b/gdbserver/linux-low.cc index ade5e9e2a1c0..0e19f09a23bb 100644 --- a/gdbserver/linux-low.cc +++ b/gdbserver/linux-low.cc @@ -732,6 +732,37 @@ 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 = event_ptid.lwp (); + if (ptrace (PTRACE_GETEVENTMSG, event_ptid.lwp (), (PTRACE_TYPE_ARG3) 0, + &execing_tid) != 0) + 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 +778,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 base-commit: 5c88753f7b52e9bfbaf0e26239b179e4a8bb1142 -- 2.54.0