The branch stable/14 has been updated by kevans: URL: https://cgit.FreeBSD.org/src/commit/?id=462b13ebf6c6d71613ab88464b907e38492cf726
commit 462b13ebf6c6d71613ab88464b907e38492cf726 Author: Kyle Evans <kev...@freebsd.org> AuthorDate: 2025-06-19 15:31:58 +0000 Commit: Kyle Evans <kev...@freebsd.org> CommitDate: 2025-07-21 02:12:27 +0000 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 (cherry picked from commit ee9895e10d266b7bb0a25677aee3debdc17ab4bf) --- lib/libc/sys/ptrace.2 | 12 +++++++- lib/libc/sys/wait.2 | 8 ++++-- sys/kern/kern_sig.c | 5 ++-- sys/kern/sys_process.c | 11 +++++-- tests/sys/kern/ptrace_test.c | 68 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 7 deletions(-) diff --git a/lib/libc/sys/ptrace.2 b/lib/libc/sys/ptrace.2 index 9b789a0e45b3..7aa24a3f820b 100644 --- a/lib/libc/sys/ptrace.2 +++ b/lib/libc/sys/ptrace.2 @@ -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 diff --git a/lib/libc/sys/wait.2 b/lib/libc/sys/wait.2 index b62ed585a333..165b79a7f36f 100644 --- a/lib/libc/sys/wait.2 +++ b/lib/libc/sys/wait.2 @@ -27,7 +27,7 @@ .\" .\" @(#)wait.2 8.2 (Berkeley) 4/19/94 .\" -.Dd June 24, 2022 +.Dd July 3, 2025 .Dt WAIT 2 .Os .Sh NAME @@ -275,6 +275,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. @@ -452,7 +456,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. diff --git a/sys/kern/kern_sig.c b/sys/kern/kern_sig.c index d8be29779386..eb5d0815caf8 100644 --- a/sys/kern/kern_sig.c +++ b/sys/kern/kern_sig.c @@ -2458,8 +2458,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); @@ -3796,6 +3794,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); } diff --git a/sys/kern/sys_process.c b/sys/kern/sys_process.c index a33e3fa4b73c..3fb4595bb5f7 100644 --- a/sys/kern/sys_process.c +++ b/sys/kern/sys_process.c @@ -1330,8 +1330,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; } diff --git a/tests/sys/kern/ptrace_test.c b/tests/sys/kern/ptrace_test.c index 50acda94642f..8831d298d554 100644 --- a/tests/sys/kern/ptrace_test.c +++ b/tests/sys/kern/ptrace_test.c @@ -4525,6 +4525,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); @@ -4594,6 +4661,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()); }