Diff below fixes multiple issues with traced process, exposed by the
regression test attached:
- When a debugging process exit, give back the traced process to its
original parent, if it exists, instead of init(8).
- When a traced process exit, make sure the original parent receives
the exit status only after the debugger has seen it. This is done
by keeping a list of 'orphaned' children in the original parent and
looking in it in dowait4().
The logic and most of the code comes from FreeBSD as pointed out by
guenther@.
Index: kern/kern_exit.c
===================================================================
RCS file: /cvs/src/sys/kern/kern_exit.c,v
retrieving revision 1.183
diff -u -p -r1.183 kern_exit.c
--- kern/kern_exit.c 12 Feb 2020 14:41:23 -0000 1.183
+++ kern/kern_exit.c 28 Feb 2020 16:55:11 -0000
@@ -253,21 +253,30 @@ exit1(struct proc *p, int xexit, int xsi
}
/*
- * Give orphaned children to init(8).
+ * Reparent children to their original parent, in case
+ * they were being traced, or to init(8).
*/
qr = LIST_FIRST(&pr->ps_children);
if (qr) /* only need this if any child is S_ZOMB */
wakeup(initprocess);
for (; qr != 0; qr = nqr) {
nqr = LIST_NEXT(qr, ps_sibling);
- proc_reparent(qr, initprocess);
/*
* Traced processes are killed since their
* existence means someone is screwing up.
*/
if (qr->ps_flags & PS_TRACED &&
!(qr->ps_flags & PS_EXITING)) {
+ struct process *tr = NULL;
+
+ if (qr->ps_oppid != 0 &&
+ (qr->ps_oppid != qr->ps_pptr->ps_pid))
+ tr = prfind(qr->ps_oppid);
+
+ proc_reparent(pr, tr ? tr : initprocess);
atomic_clearbits_int(&qr->ps_flags, PS_TRACED);
+ pr->ps_oppid = 0;
+
/*
* If single threading is active,
* direct the signal to the active
@@ -278,8 +287,20 @@ exit1(struct proc *p, int xexit, int xsi
STHREAD);
else
prsignal(qr, SIGKILL);
+ } else {
+ proc_reparent(qr, initprocess);
}
}
+
+ /*
+ * Make sure orphans won't remember the exiting process.
+ */
+ while ((qr = LIST_FIRST(&pr->ps_orphans)) != NULL) {
+ KASSERT(qr->ps_oppid == pr->ps_pid);
+ qr->ps_oppid = 0;
+ LIST_REMOVE(qr, ps_orphan);
+ atomic_clearbits_int(&qr->ps_flags, PS_ORPHAN);
+ }
}
/* add thread's accumulated rusage into the process's total */
@@ -562,6 +583,29 @@ loop:
return (0);
}
}
+ /*
+ * Look in the orphans list too, to allow the parent to
+ * collect it's child exit status even if child is being
+ * debugged.
+ *
+ * Debugger detaches from the parent upon successful
+ * switch-over from parent to child. At this point due to
+ * re-parenting the parent loses the child to debugger and a
+ * wait4(2) call would report that it has no children to wait
+ * for. By maintaining a list of orphans we allow the parent
+ * to successfully wait until the child becomes a zombie.
+ */
+ if (nfound == 0) {
+ LIST_FOREACH(pr, &q->p_p->ps_orphans, ps_orphan) {
+ if ((pr->ps_flags & PS_NOZOMBIE) ||
+ (pid != WAIT_ANY &&
+ pr->ps_pid != pid &&
+ pr->ps_pgid != -pid))
+ continue;
+ nfound++;
+ break;
+ }
+ }
if (nfound == 0)
return (ECHILD);
if (options & WNOHANG) {
@@ -613,6 +657,16 @@ proc_reparent(struct process *child, str
LIST_REMOVE(child, ps_sibling);
LIST_INSERT_HEAD(&parent->ps_children, child, ps_sibling);
+
+ if (ISSET(child->ps_flags, PS_ORPHAN)) {
+ LIST_REMOVE(child, ps_orphan);
+ atomic_clearbits_int(&child->ps_flags, PS_ORPHAN);
+ }
+ if (ISSET(child->ps_flags, PS_TRACED)) {
+ atomic_setbits_int(&child->ps_flags, PS_ORPHAN);
+ LIST_INSERT_HEAD(&child->ps_pptr->ps_orphans, child, ps_orphan);
+ }
+
child->ps_pptr = parent;
}
@@ -628,6 +682,10 @@ process_zap(struct process *pr)
*/
leavepgrp(pr);
LIST_REMOVE(pr, ps_sibling);
+ if (ISSET(pr->ps_flags, PS_ORPHAN)) {
+ LIST_REMOVE(pr, ps_orphan);
+ atomic_clearbits_int(&pr->ps_flags, PS_ORPHAN);
+ }
/*
* Decrement the count of procs running with this uid.
Index: kern/kern_fork.c
===================================================================
RCS file: /cvs/src/sys/kern/kern_fork.c,v
retrieving revision 1.223
diff -u -p -r1.223 kern_fork.c
--- kern/kern_fork.c 21 Feb 2020 11:10:23 -0000 1.223
+++ kern/kern_fork.c 28 Feb 2020 16:23:02 -0000
@@ -190,6 +190,7 @@ process_initialize(struct process *pr, s
KASSERT(p->p_ucred->cr_ref >= 2); /* new thread and new process */
LIST_INIT(&pr->ps_children);
+ LIST_INIT(&pr->ps_orphans);
LIST_INIT(&pr->ps_ftlist);
LIST_INIT(&pr->ps_sigiolst);
TAILQ_INIT(&pr->ps_tslpqueue);
Index: sys/proc.h
===================================================================
RCS file: /cvs/src/sys/sys/proc.h,v
retrieving revision 1.289
diff -u -p -r1.289 proc.h
--- sys/proc.h 21 Feb 2020 11:10:23 -0000 1.289
+++ sys/proc.h 28 Feb 2020 16:28:12 -0000
@@ -182,6 +182,15 @@ struct process {
LIST_HEAD(, process) ps_children;/* Pointer to list of children. */
LIST_ENTRY(process) ps_hash; /* Hash chain. */
+ /*
+ * An orphan is the child that has been re-parented to the
+ * debugger as a result of attaching to it. Need to keep
+ * track of them for parent to be able to collect the exit
+ * status of what used to be children.
+ */
+ LIST_ENTRY(process) ps_orphan; /* List of orphan processes. */
+ LIST_HEAD(, process) ps_orphans;/* Pointer to list of orphans. */
+
struct sigiolst ps_sigiolst; /* List of sigio structures. */
struct sigacts *ps_sigacts; /* Signal actions, state */
struct vnode *ps_textvp; /* Vnode of executable. */
@@ -303,6 +312,7 @@ struct process {
#define PS_PLEDGE 0x00100000 /* Has called pledge(2) */
#define PS_WXNEEDED 0x00200000 /* Process may violate W^X */
#define PS_EXECPLEDGE 0x00400000 /* Has exec pledges */
+#define PS_ORPHAN 0x00800000 /* Process is on an orphan list
*/
#define PS_BITS \
("\20" "\01CONTROLT" "\02EXEC" "\03INEXEC" "\04EXITING" "\05SUGID" \
Index: ptrace_test.c
===================================================================
RCS file: /cvs/src/regress/sys/kern/ptrace2/ptrace_test.c,v
retrieving revision 1.1
diff -u -p -r1.1 ptrace_test.c
--- ptrace_test.c 28 Feb 2020 12:48:30 -0000 1.1
+++ ptrace_test.c 28 Feb 2020 17:12:57 -0000
@@ -222,11 +222,221 @@ ATF_TC_BODY(ptrace__parent_wait_after_at
ATF_REQUIRE(errno == ECHILD);
}
+/*
+ * Verify that a parent process "sees" the exit of a debugged process only
+ * after the debugger has seen it.
+ */
+ATF_TC_WITHOUT_HEAD(ptrace__parent_sees_exit_after_child_debugger);
+ATF_TC_BODY(ptrace__parent_sees_exit_after_child_debugger, tc)
+{
+ pid_t child, debugger, wpid;
+ int cpipe[2], dpipe[2], status;
+ char c;
+
+ if (atf_tc_get_config_var_as_bool_wd(tc, "ci", false))
+ atf_tc_skip("https://bugs.freebsd.org/239399");
+
+ ATF_REQUIRE(pipe(cpipe) == 0);
+ ATF_REQUIRE((child = fork()) != -1);
+
+ if (child == 0) {
+ /* Child process. */
+ close(cpipe[0]);
+
+ /* Wait for parent to be ready. */
+ CHILD_REQUIRE(read(cpipe[1], &c, sizeof(c)) == sizeof(c));
+
+ _exit(1);
+ }
+ close(cpipe[1]);
+
+ ATF_REQUIRE(pipe(dpipe) == 0);
+ ATF_REQUIRE((debugger = fork()) != -1);
+
+ if (debugger == 0) {
+ /* Debugger process. */
+ close(dpipe[0]);
+
+ CHILD_REQUIRE(ptrace(PT_ATTACH, child, NULL, 0) != -1);
+
+ wpid = waitpid(child, &status, 0);
+ CHILD_REQUIRE(wpid == child);
+ CHILD_REQUIRE(WIFSTOPPED(status));
+ CHILD_REQUIRE(WSTOPSIG(status) == SIGSTOP);
+
+ CHILD_REQUIRE(ptrace(PT_CONTINUE, child, (caddr_t)1, 0) != -1);
+
+ /* Signal parent that debugger is attached. */
+ CHILD_REQUIRE(write(dpipe[1], &c, sizeof(c)) == sizeof(c));
+
+ /* Wait for parent's failed wait. */
+ CHILD_REQUIRE(read(dpipe[1], &c, sizeof(c)) == 0);
+
+ wpid = waitpid(child, &status, 0);
+ CHILD_REQUIRE(wpid == child);
+ CHILD_REQUIRE(WIFEXITED(status));
+ CHILD_REQUIRE(WEXITSTATUS(status) == 1);
+
+ _exit(0);
+ }
+ close(dpipe[1]);
+
+ /* Parent process. */
+
+ /* Wait for the debugger to attach to the child. */
+ ATF_REQUIRE(read(dpipe[0], &c, sizeof(c)) == sizeof(c));
+
+ /* Release the child. */
+ ATF_REQUIRE(write(cpipe[0], &c, sizeof(c)) == sizeof(c));
+ ATF_REQUIRE(read(cpipe[0], &c, sizeof(c)) == 0);
+ close(cpipe[0]);
+
+ wait_for_zombie(child);
+
+ /*
+ * This wait should return a pid of 0 to indicate no status to
+ * report. The parent should see the child as non-exited
+ * until the debugger sees the exit.
+ */
+ wpid = waitpid(child, &status, WNOHANG);
+ ATF_REQUIRE(wpid == 0);
+
+ /* Signal the debugger to wait for the child. */
+ close(dpipe[0]);
+
+ /* Wait for the debugger. */
+ wpid = waitpid(debugger, &status, 0);
+ ATF_REQUIRE(wpid == debugger);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE(WEXITSTATUS(status) == 0);
+
+ /* The child process should now be ready. */
+ wpid = waitpid(child, &status, WNOHANG);
+ ATF_REQUIRE(wpid == child);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE(WEXITSTATUS(status) == 1);
+}
+
+/*
+ * Verify that a parent process "sees" the exit of a debugged process
+ * only after a non-direct-child debugger has seen it. In particular,
+ * various wait() calls in the parent must avoid failing with ESRCH by
+ * checking the parent's orphan list for the debugee.
+ */
+ATF_TC_WITHOUT_HEAD(ptrace__parent_sees_exit_after_unrelated_debugger);
+ATF_TC_BODY(ptrace__parent_sees_exit_after_unrelated_debugger, tc)
+{
+ pid_t child, debugger, fpid, wpid;
+ int cpipe[2], dpipe[2], status;
+ char c;
+
+ ATF_REQUIRE(pipe(cpipe) == 0);
+ ATF_REQUIRE((child = fork()) != -1);
+
+ if (child == 0) {
+ /* Child process. */
+ close(cpipe[0]);
+
+ /* Wait for parent to be ready. */
+ CHILD_REQUIRE(read(cpipe[1], &c, sizeof(c)) == sizeof(c));
+
+ _exit(1);
+ }
+ close(cpipe[1]);
+
+ ATF_REQUIRE(pipe(dpipe) == 0);
+ ATF_REQUIRE((debugger = fork()) != -1);
+
+ if (debugger == 0) {
+ /* Debugger parent. */
+
+ /*
+ * Fork again and drop the debugger parent so that the
+ * debugger is not a child of the main parent.
+ */
+ CHILD_REQUIRE((fpid = fork()) != -1);
+ if (fpid != 0)
+ _exit(2);
+
+ /* Debugger process. */
+ close(dpipe[0]);
+
+ CHILD_REQUIRE(ptrace(PT_ATTACH, child, NULL, 0) != -1);
+
+ wpid = waitpid(child, &status, 0);
+ CHILD_REQUIRE(wpid == child);
+ CHILD_REQUIRE(WIFSTOPPED(status));
+ CHILD_REQUIRE(WSTOPSIG(status) == SIGSTOP);
+
+ CHILD_REQUIRE(ptrace(PT_CONTINUE, child, (caddr_t)1, 0) != -1);
+
+ /* Signal parent that debugger is attached. */
+ CHILD_REQUIRE(write(dpipe[1], &c, sizeof(c)) == sizeof(c));
+
+ /* Wait for parent's failed wait. */
+ CHILD_REQUIRE(read(dpipe[1], &c, sizeof(c)) == sizeof(c));
+
+ wpid = waitpid(child, &status, 0);
+ CHILD_REQUIRE(wpid == child);
+ CHILD_REQUIRE(WIFEXITED(status));
+ CHILD_REQUIRE(WEXITSTATUS(status) == 1);
+
+ _exit(0);
+ }
+ close(dpipe[1]);
+
+ /* Parent process. */
+
+ /* Wait for the debugger parent process to exit. */
+ wpid = waitpid(debugger, &status, 0);
+ ATF_REQUIRE(wpid == debugger);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE(WEXITSTATUS(status) == 2);
+
+ /* A WNOHANG wait here should see the non-exited child. */
+ wpid = waitpid(child, &status, WNOHANG);
+ ATF_REQUIRE(wpid == 0);
+
+ /* Wait for the debugger to attach to the child. */
+ ATF_REQUIRE(read(dpipe[0], &c, sizeof(c)) == sizeof(c));
+
+ /* Release the child. */
+ ATF_REQUIRE(write(cpipe[0], &c, sizeof(c)) == sizeof(c));
+ ATF_REQUIRE(read(cpipe[0], &c, sizeof(c)) == 0);
+ close(cpipe[0]);
+
+ wait_for_zombie(child);
+
+ /*
+ * This wait should return a pid of 0 to indicate no status to
+ * report. The parent should see the child as non-exited
+ * until the debugger sees the exit.
+ */
+ wpid = waitpid(child, &status, WNOHANG);
+ ATF_REQUIRE(wpid == 0);
+
+ /* Signal the debugger to wait for the child. */
+ ATF_REQUIRE(write(dpipe[0], &c, sizeof(c)) == sizeof(c));
+
+ /* Wait for the debugger. */
+ ATF_REQUIRE(read(dpipe[0], &c, sizeof(c)) == 0);
+ close(dpipe[0]);
+
+ /* The child process should now be ready. */
+ wpid = waitpid(child, &status, WNOHANG);
+ ATF_REQUIRE(wpid == child);
+ ATF_REQUIRE(WIFEXITED(status));
+ ATF_REQUIRE(WEXITSTATUS(status) == 1);
+}
+
ATF_TP_ADD_TCS(tp)
{
ATF_TP_ADD_TC(tp, ptrace__parent_wait_after_trace_me);
ATF_TP_ADD_TC(tp, ptrace__parent_wait_after_attach);
+ ATF_TP_ADD_TC(tp, ptrace__parent_sees_exit_after_child_debugger);
+ ATF_TP_ADD_TC(tp, ptrace__parent_sees_exit_after_unrelated_debugger);
return (atf_no_error());
}
+