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 <pedro@palves.net>