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());
 }
+

Reply via email to