⬢ DragonFlyBSD Kernel Audit
DF-4284 / midi_unload_model.c
← back to finding ↓ download raw
/*
 * midi_unload_model.c -- userland harness modeling sys/dev/sound/midi/midi.c
 *
 *   midi_unload()   [midi.c:1457-1495]:
 *       TAILQ_FOREACH(m, &midi_devs, link) {
 *               lockmgr(&m->lock, LK_EXCLUSIVE);
 *               ...
 *               retval = midi_destroy(m, 1);      <-- midi.c:1474
 *       }
 *   midi_destroy()  [midi.c:1418-1438]:
 *       ... TAILQ_REMOVE(&midi_devs, m, link);    <-- midi.c:1429
 *       ... kfree(m, M_MIDI);                     <-- midi.c:1436
 *
 * TAILQ_FOREACH advances with `m = TAILQ_NEXT(m, link)` AFTER the loop body
 * -- i.e. it dereferences m->link.tqe_next of the object that midi_destroy()
 * just removed from the list and freed.  That is a use-after-free read; the
 * value read is whatever now occupies the freed slab slot.  Any same-size
 * kernel allocation (M_MIDI zone reuse via attacker-groomable kmalloc churn)
 * controls it, so the unload loop then operates on an attacker-influenced
 * pointer (lockmgr(&m->lock, ...), m->busy, ... ).
 *
 * TAILQ macros below are verbatim from sys/sys/queue.h (TRACEBUF/QUEUEDEBUG
 * and _DIAGNOSTIC bits, inactive in kernel builds, omitted).
 *
 * Modes:
 *   reuse  - after each free, a same-size "attacker" chunk immediately
 *            reoccupies the memory with a forged link.tqe_next
 *            (0x4141414141414141) => loop follows ATTACKER pointer.
 *   stale  - plain free(); tqe_next reads allocator freelist garbage
 *            (still a UAF read, uncontrolled).
 *   fixed  - fix.diff semantics: stash TAILQ_NEXT before destroying;
 *            clean traversal of all devices.
 *
 * Exit codes: 0 = clean, 99 = SIGSEGV at the UAF-read pointer.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

/* canonical (bit47 extension zero) unmapped attacker pointer */
#define FAKE_NEXT	((struct snd_midi *)0x0000414141414140ULL)

/* ---------- sys/sys/queue.h TAILQ (verbatim, kernel build config) ----- */
#define TAILQ_HEAD(name, type)	struct name { struct type *tqh_first; \
					struct type **tqh_last; }
#define TAILQ_ENTRY(type)	struct { struct type *tqe_next; \
					struct type **tqe_prev; }
#define TAILQ_FIRST(head)	((head)->tqh_first)
#define TAILQ_NEXT(elm, field)	((elm)->field.tqe_next)
#define TAILQ_INIT(head) do {						\
	TAILQ_FIRST((head)) = NULL;					\
	(head)->tqh_last = &TAILQ_FIRST((head));			\
} while (0)
#define TAILQ_REMOVE(head, elm, field) do {				\
	if ((TAILQ_NEXT((elm), field)) != NULL)				\
		TAILQ_NEXT((elm), field)->field.tqe_prev =		\
		    (elm)->field.tqe_prev;				\
	*(elm)->field.tqe_prev = TAILQ_NEXT((elm), field);		\
} while (0)
#define TAILQ_FOREACH(var, head, field)					\
	for ((var) = TAILQ_FIRST((head));				\
	    (var);							\
	    (var) = TAILQ_NEXT(var, field))
#define TAILQ_INSERT_TAIL(head, elm, field) do {			\
	(elm)->field.tqe_next = NULL;					\
	(elm)->field.tqe_prev = (head)->tqh_last;			\
	*(head)->tqh_last = (elm);					\
	(head)->tqh_last = &TAILQ_NEXT((elm), field);			\
} while (0)

/* ---------- snd_midi (linkage members only) ---------- */
struct snd_midi {
	TAILQ_ENTRY(snd_midi)	link;	/* midi.c:118 */
	int			unit;
};

static TAILQ_HEAD(, snd_midi) midi_devs;	/* midi.c:169 */

static const char *g_mode;

static void
sigfault(int sig, siginfo_t *si, void *uc)
{
	fprintf(stderr, "[%s] signal %d at %p -- unload loop dereferenced the "
	    "value read from FREED memory (kernel: UAF read in "
	    "TAILQ_FOREACH advance; lockmgr(&m->lock) on attacker-chosen "
	    "pointer)\n", g_mode, sig, si->si_addr);
	exit(99);
}

/* midi.c:1436 kfree(m, M_MIDI) + simulated slab-zone reuse */
static void
model_kfree(struct snd_midi *m)
{
	free(m);
	if (strcmp(g_mode, "reuse") == 0) {
		/* same-size allocation immediately reoccupies the slot with
		 * attacker-controlled contents (kernel: another M_MIDI
		 * allocation, groomable by concurrent kmalloc churn) */
		struct snd_midi *re = malloc(sizeof(*re));
		memset(re, 0x41, sizeof(*re));
		re->link.tqe_next = FAKE_NEXT;
		printf("    [groom] freed slot reoccupied; forged "
		    "link.tqe_next = %p\n", (void *)FAKE_NEXT);
		fflush(stdout);
	}
}

/* midi.c:1418-1438 (essential steps verbatim) */
static int
midi_destroy(struct snd_midi *m, int midiuninit)
{
	(void)midiuninit;
	TAILQ_REMOVE(&midi_devs, m, link);
	model_kfree(m);
	return (0);
}

/* midi.c:1469-1477 (verbatim shape) */
static int
midi_unload(void)
{
	struct snd_midi *m;
	int retval;

	TAILQ_FOREACH(m, &midi_devs, link) {
		printf("  unload: visiting unit %d (%p)\n", m->unit, (void *)m);
		fflush(stdout);
		/* lockmgr(&m->lock, LK_EXCLUSIVE); if (m->busy) ... */
		retval = midi_destroy(m, 1);
		if (retval)
			return (retval);
		/* loop advance: m = TAILQ_NEXT(m, link)  <-- m is FREED */
	}
	return (0);
}

/* fix.diff semantics: save the next pointer before destruction */
static int
midi_unload_fixed(void)
{
	struct snd_midi *m, *next;

	for (m = TAILQ_FIRST(&midi_devs); m != NULL; m = next) {
		next = TAILQ_NEXT(m, link);
		printf("  unload: visiting unit %d (%p)\n", m->unit, (void *)m);
		fflush(stdout);
		midi_destroy(m, 1);
	}
	return (0);
}

int
main(int argc, char **argv)
{
	struct snd_midi *d[3];
	struct sigaction sa;
	int i;

	g_mode = argc > 1 ? argv[1] : "reuse";

	memset(&sa, 0, sizeof(sa));
	sa.sa_sigaction = sigfault;
	sa.sa_flags = SA_SIGINFO;
	sigaction(SIGSEGV, &sa, NULL);
	sigaction(SIGBUS, &sa, NULL);

	TAILQ_INIT(&midi_devs);
	for (i = 0; i < 3; i++) {
		d[i] = calloc(1, sizeof(*d[i]));
		d[i]->unit = i;
		TAILQ_INSERT_TAIL(&midi_devs, d[i], link);
	}

	if (strcmp(g_mode, "fixed") == 0) {
		printf("[%s] fixed midi_unload(): stash next before destroy\n",
		    g_mode);
		midi_unload_fixed();
		printf("[%s] clean: all devices destroyed, no UAF\n", g_mode);
		return (0);
	}

	printf("[%s] buggy midi_unload(): TAILQ_FOREACH + free-in-body\n",
	    g_mode);
	fflush(stdout);
	midi_unload();
	printf("[%s] NOT REACHED: unload returned\n", g_mode);
	return (1);
}