kern: send parent a SIGCHLD when the debugger has detached

The practical scenario that leads to this is porch(1) spawning some
utility and sending it a SIGSTOP as a debugging aide.  The user then
attaches a debugger and walks through how some specific input is
processed, then detaches to allow the script to continue.  When ptrace
is detached, the process resumes execution but the parent is never
notified and may be stuck in wait(2) for it to continue or terminate.

Other platforms seem to re-suspend the process after the debugger is
detached, but neither behavior seems unreasonable.  Just notifying the
parent that the child has resumed is a relatively low-risk departure
from our current behavior and had apparently been considered in the
past, based on pre-existing comments.

Move p_flag and p_xsig handling into childproc_continued(), as just
sending the SIGCHLD here isn't really useful without P_CONTINUED set
and the other caller already sets these up as well.

Reviewed by:	kib, markj
Differential Revision:	https://reviews.freebsd.org/D50917
This commit is contained in:
Kyle Evans 2025-06-19 10:31:58 -05:00
parent 5110a74afe
commit ee9895e10d
5 changed files with 97 additions and 7 deletions

View file

@ -1,7 +1,7 @@
.\" $NetBSD: ptrace.2,v 1.2 1995/02/27 12:35:37 cgd Exp $
.\"
.\" This file is in the public domain.
.Dd August 18, 2023
.Dd June 19, 2025
.Dt PTRACE 2
.Os
.Sh NAME
@ -473,6 +473,16 @@ This request is like PT_CONTINUE, except that it does not allow
specifying an alternate place to continue execution, and after it
succeeds, the traced process is no longer traced and continues
execution normally.
.Pp
The parent of the traced process will be sent a
.Dv SIGCHLD
to indicate that the process has continued from a stopped state regardless of
whether the process was in a stopped state prior to the corresponding
.Dv PT_ATTACH
request.
A
.Xr wait 2
for the traced process would indicate that it had been continued.
.It Dv PT_GETREGS
This request reads the traced process's machine registers into the
.Do

View file

@ -25,7 +25,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.Dd August 27, 2024
.Dd June 19, 2025
.Dt WAIT 2
.Os
.Sh NAME
@ -273,6 +273,10 @@ Report the status of selected processes that
have continued from a job control stop by receiving a
.Dv SIGCONT
signal.
.Xr ptrace 2
can also cause a process to be continued, when a
.Dv PT_DETACH
request is issued to detach the debugger.
.It Dv WNOHANG
Do not block when
there are no processes wishing to report status.
@ -450,7 +454,7 @@ value:
.Bl -tag -width Ds
.It Fn WIFCONTINUED status
True if the process has not terminated, and
has continued after a job control stop.
has continued after a job control stop or detach of a debugger.
This macro can be true only if the wait call specified the
.Dv WCONTINUED
option.

View file

@ -2461,8 +2461,6 @@ tdsendsignal(struct proc *p, struct thread *td, int sig, ksiginfo_t *ksi)
PROC_SLOCK(p);
if (p->p_numthreads == p->p_suspcount) {
PROC_SUNLOCK(p);
p->p_flag |= P_CONTINUED;
p->p_xsig = SIGCONT;
PROC_LOCK(p->p_pptr);
childproc_continued(p);
PROC_UNLOCK(p->p_pptr);
@ -3778,6 +3776,9 @@ childproc_stopped(struct proc *p, int reason)
void
childproc_continued(struct proc *p)
{
PROC_LOCK_ASSERT(p, MA_OWNED);
p->p_flag |= P_CONTINUED;
p->p_xsig = SIGCONT;
childproc_jobstate(p, CLD_CONTINUED, SIGCONT);
}

View file

@ -1324,8 +1324,15 @@ kern_ptrace(struct thread *td, int req, pid_t pid, void *addr, int data)
p->p_flag2 &= ~P2_PTRACE_FSTP;
}
/* should we send SIGCHLD? */
/* childproc_continued(p); */
/*
* Send SIGCHLD and wakeup the parent as needed. It
* may be the case that they had stopped the child
* before it got ptraced, and now they're in the middle
* of a wait(2) for it to continue.
*/
PROC_LOCK(p->p_pptr);
childproc_continued(p);
PROC_UNLOCK(p->p_pptr);
break;
}

View file

@ -4523,6 +4523,73 @@ ATF_TC_BODY(ptrace__PT_ATTACH_no_EINTR, tc)
ATF_REQUIRE(timespeccmp(&shm->sleep_time, &twelve_sec, <=));
}
ATF_TC_WITHOUT_HEAD(ptrace__PT_DETACH_continued);
ATF_TC_BODY(ptrace__PT_DETACH_continued, tc)
{
char buf[256];
pid_t debuggee, debugger;
int dpipe[2] = {-1, -1}, status;
/* Setup the debuggee's pipe, which we'll use to let it terminate. */
ATF_REQUIRE(pipe(dpipe) == 0);
ATF_REQUIRE((debuggee = fork()) != -1);
if (debuggee == 0) {
ssize_t readsz;
/*
* The debuggee will just absorb everything until the parent
* closes it. In the process, we expect it to get SIGSTOP'd,
* then ptrace(2)d and finally, it should resume after we detach
* and the parent will be notified.
*/
close(dpipe[1]);
while ((readsz = read(dpipe[0], buf, sizeof(buf))) != 0) {
if (readsz > 0 || errno == EINTR)
continue;
_exit(1);
}
_exit(0);
}
close(dpipe[0]);
ATF_REQUIRE(kill(debuggee, SIGSTOP) == 0);
REQUIRE_EQ(waitpid(debuggee, &status, WUNTRACED), debuggee);
ATF_REQUIRE(WIFSTOPPED(status));
/* Child is stopped, enter the debugger to attach/detach. */
ATF_REQUIRE((debugger = fork()) != -1);
if (debugger == 0) {
REQUIRE_EQ(ptrace(PT_ATTACH, debuggee, 0, 0), 0);
REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee);
ATF_REQUIRE(WIFSTOPPED(status));
REQUIRE_EQ(WSTOPSIG(status), SIGSTOP);
REQUIRE_EQ(ptrace(PT_DETACH, debuggee, 0, 0), 0);
_exit(0);
}
REQUIRE_EQ(waitpid(debugger, &status, 0), debugger);
ATF_REQUIRE(WIFEXITED(status));
REQUIRE_EQ(WEXITSTATUS(status), 0);
REQUIRE_EQ(waitpid(debuggee, &status, WCONTINUED), debuggee);
ATF_REQUIRE(WIFCONTINUED(status));
/*
* Closing the pipe will trigger the debuggee to exit now that the
* child has resumed following detach.
*/
close(dpipe[1]);
REQUIRE_EQ(waitpid(debuggee, &status, 0), debuggee);
ATF_REQUIRE(WIFEXITED(status));
REQUIRE_EQ(WEXITSTATUS(status), 0);
}
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, ptrace__parent_wait_after_trace_me);
@ -4592,6 +4659,7 @@ ATF_TP_ADD_TCS(tp)
ATF_TP_ADD_TC(tp, ptrace__PT_SC_REMOTE_getpid);
ATF_TP_ADD_TC(tp, ptrace__reap_kill_stopped);
ATF_TP_ADD_TC(tp, ptrace__PT_ATTACH_no_EINTR);
ATF_TP_ADD_TC(tp, ptrace__PT_DETACH_continued);
return (atf_no_error());
}