โฌข DragonFlyBSD Kernel Audit
DF-0917 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0917 โ€” Deterministic proof of the use-after-free pattern in the
 * FUSE IPC transaction-completion path.
 *
 * The finding (sys/vfs/fuse/fuse_ipc.c:112-270, exercised via
 * sys/vfs/fuse/fuse_device.c:118-223) is a RACE: the /dev/fuse read/write
 * completion path removes the fuse_ipc (fip) from the reply/request TAILQ
 * under ipc_lock, DROPS the lock, and then dereferences fip (fip->reply,
 * fip->request.buf via fuse_in(fip), and fip->done via
 * fuse_ipc_test_and_set_replied) WITHOUT holding a reference on fip.
 * Concurrently the tx originator (sleeping in fuse_ipc_wait) may TIME OUT
 * (~35s), at which point fuse_ipc_remove()+fuse_ipc_set_replied()+return
 * ETIMEDOUT is followed by fuse_ipc_tx:270 fuse_ipc_put(fip) which drops the
 * LAST reference and FREES fip โ€” while the device path is still mid-access.
 *
 * The live race window is sub-microsecond (it requires the tx waiter's final
 * 5*hz tsleep to return EWOULDBLOCK at the precise instant the daemon's
 * write syscall holds fip between lock-drop and set_replied).  Winning it
 * live needs many thousands of 35-second attempts.
 *
 * This harness DETERMINISTICALLY reproduces the *pattern* (option (b) in the
 * verifier's playbook: a code-level harness reproducing the
 * drop-lock-then-access-fip logic).  It models the two competing paths with
 * pthreads and forces the dangerous interleaving with a barrier placed
 * exactly where the kernel drops ipc_lock (fuse_device.c:197) before
 * touching fip again (fuse_device.c:205-219).  If the access-after-free is
 * real, the device-path thread observes the poison marker written by the
 * timeout/free path โ€” proving the UAF.
 *
 * Build the UNFIXED model (reproduces the UAF):
 *   cc -O2 -o harness harness.c -lpthread
 * Build the FIXED model (applies the fix.diff refcount_hold-across-window):
 *   cc -O2 -DFIXED -o harness_fixed harness.c -lpthread
 * Run:   ./harness         -> "UAF CONFIRMED" (exit 0)
 *        ./harness_fixed   -> "NO UAF (fix holds ref across window)" (exit 0)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <stdatomic.h>

/* ---- minimal model of the kernel structures (fuse.h:129-145) ---- */
struct fuse_buf { void *buf; size_t len; };

struct fuse_ipc {
	void              *fmp;        /* fuse_mount*  */
	struct fuse_buf    request;
	struct fuse_buf    reply;
	void              *request_entry;
	void              *reply_entry;
	atomic_uint        refcnt;
	unsigned long      unique;
	int                sent;
	int                done;
};

/* poison marker written into a freed fip, like the kernel slab's
 * WEIRD_ADDR (0xdeadc0de) INVARIANTS poisoning of freed fuse_ipc slots
 * (kern_slaballoc.c).  The device path detecting this == observing a UAF. */
#define POISON 0xdeadc0de

static pthread_mutex_t ipc_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_barrier_t b1, b2;   /* two barriers => deterministic interleaving */
static struct fuse_ipc *g_fip;
static int g_in_list;

/* model of fuse_ipc_get (fuse_ipc.c:91) */
static struct fuse_ipc *ipc_get(void)
{
	struct fuse_ipc *fip = calloc(1, sizeof(*fip));
	atomic_store(&fip->refcnt, 1);
	fip->request.buf = (void*)0x1000;
	fip->request.len = 64;
	return fip;
}

/* model of fuse_ipc_put (fuse_ipc.c:110) โ€” frees on last ref */
static void ipc_put(struct fuse_ipc *fip)
{
	if (atomic_fetch_sub(&fip->refcnt, 1) == 1) {
		/* kernel: fuse_buf_free + objcache_put -> slab frees the slot */
		/* mark every field with POISON so a later deref is detectable */
		unsigned int *p = (unsigned int *)fip;
		size_t i;
		for (i = 0; i < sizeof(*fip)/sizeof(*p); i++)
			p[i] = POISON;
		/* NOTE: we deliberately do NOT free() โ€” keep the slot so the
		 * device thread can observe the poisoned contents, exactly as
		 * a freed-then-poisoned kernel slab slot would look before
		 * reallocation. */
	}
}

/* ---- Thread B: the tx originator's TIMEOUT path ----
 * models fuse_ipc_wait EWOULDBLOCK final-retry (fuse_ipc.c:179-195) +
 * fuse_ipc_tx:268-271 error-put. */
static void *tx_waiter_timeout(void *arg)
{
	(void)arg;
	/* B1: wait until the device thread has removed fip from the list and
	 * released ipc_lock (mirrors fuse_device.c:193+197). */
	pthread_barrier_wait(&b1);

	/* === the kernel drops the ref here on timeout === */
	/* fuse_ipc_remove: lock, find fip in heads (no-op, device already
	 *                  removed it), unlock */
	pthread_mutex_lock(&ipc_lock);
	g_in_list = 0;            /* already removed by device thread */
	pthread_mutex_unlock(&ipc_lock);
	/* fuse_ipc_set_replied(fip) -- writes fip->done */
	/* fuse_ipc_tx: error -> fuse_ipc_put(fip) -> LAST ref -> FREE */
	ipc_put(g_fip);
	printf("[tx-waiter] timed out: removed+replied+put fip=%p (NOW FREED)\n",
	    (void*)g_fip);

	/* B2: signal the device thread that fip has been freed, so its
	 * subsequent access is deterministically after the free. */
	pthread_barrier_wait(&b2);
	return NULL;
}

/* ---- Thread A: the daemon /dev/fuse write completion path ----
 * models fuse_device_write (fuse_device.c:164-223). */
static void *device_write(void *arg)
{
	(void)arg;
	struct fuse_ipc *fip;
	struct fuse_buf fb = { (void*)0x2000, 48 };

	/* fuse_device.c:190-197 */
	pthread_mutex_lock(&ipc_lock);
	fip = g_fip;                       /* TAILQ find by unique */
	if (fip && g_in_list) {
		g_in_list = 0;              /* TAILQ_REMOVE reply_head */
#ifdef FIXED
		/* === THE FIX (fix.diff): take a reference BEFORE dropping the
		 * lock, so the tx-waiter timeout put cannot free fip while the
		 * device path still dereferences it. */
		atomic_fetch_add(&fip->refcnt, 1);
#endif
	}
	pthread_mutex_unlock(&ipc_lock);   /* <<< LOCK DROPPED (fuse_device.c:197) */

	if (!fip) {
		printf("[device] fip not found -> ENOMSG (no UAF this time)\n");
		return NULL;
	}

	/* === FROM HERE the kernel touches fip with NO lock and NO ref ===
	 * B1 releases the tx-waiter to free fip; B2 guarantees the free is
	 * complete before we access fip (deterministic worst-case). */
	pthread_barrier_wait(&b1);
	pthread_barrier_wait(&b2);

	/* fuse_device.c:205 */ fip->reply = fb;
	/* fuse_device.c:206 */ void *in = fip->request.buf;   /* fuse_in(fip) */
	/* fuse_device.c:212 */ (void)in;
	/* fuse_device.c:219 */ /* fuse_ipc_test_and_set_replied(fip): fip->done */

	/* detect whether fip was freed (POISON'd) underneath us */
	int uaf = (fip->request.len == POISON ||
	           *(unsigned int *)&fip->done == POISON ||
	           *(unsigned int *)&fip->request.buf == POISON);
	if (uaf) {
		printf("[device] *** UAF CONFIRMED *** accessed fip=%p AFTER it was "
		       "freed by the tx-waiter timeout:\n", (void*)fip);
		printf("          fip->request.buf = %p (poisoned? %s)\n",
		    fip->request.buf,
		    *(unsigned int*)&fip->request.buf == POISON ? "YES" : "no");
		printf("          fip->request.len = 0x%zx (poisoned? %s)\n",
		    fip->request.len,
		    (unsigned)fip->request.len == POISON ? "YES" : "no");
		printf("          fip->done        = 0x%x (poisoned? %s)\n",
		    fip->done, (unsigned)fip->done == POISON ? "YES" : "no");
		printf("          => daemon wrote fip->reply + read fip->request.buf "
		       "into a FREED fuse_ipc slab slot (16B write + ptr read UAF).\n");
#ifdef FIXED
		atomic_fetch_sub(&fip->refcnt, 1); /* mirror put */
		free(g_fip);
		return NULL;  /* unreachable: FIXED model never UAFs */
#else
		free(g_fip);
		return (void*)1;
#endif
	}
#ifdef FIXED
	printf("[device] (FIXED model) accessed fip=%p cleanly: ref held across "
	       "window (refcnt=%u) -> tx-waiter put could not free it.\n",
	       (void*)fip, atomic_load(&fip->refcnt));
	atomic_fetch_sub(&fip->refcnt, 1); /* mirror the fix's fuse_ipc_put */
#else
	printf("[device] accessed fip=%p cleanly (timeout did not fire in window)\n",
	    (void*)fip);
#endif
	free(g_fip);
	return NULL;
}

int main(void)
{
	pthread_t ta, tb;
	void *ra = NULL;

	g_fip = ipc_get();
	g_in_list = 1;
	pthread_barrier_init(&b1, NULL, 2);
	pthread_barrier_init(&b2, NULL, 2);

	printf("DF-0917 deterministic UAF harness\n");
	printf("modeling fuse_device_write (fuse_device.c:190-219) vs\n");
	printf("         fuse_ipc_wait timeout+put (fuse_ipc.c:179-195,266-271)\n");
	printf("fip=%p refcnt=1 (single owner, no extra ref held across lock-drop)\n\n",
	    (void*)g_fip);

	pthread_create(&ta, NULL, device_write, NULL);
	pthread_create(&tb, NULL, tx_waiter_timeout, NULL);
	pthread_join(ta, &ra);
	pthread_join(tb, NULL);
	pthread_barrier_destroy(&b1);
	pthread_barrier_destroy(&b2);

	printf("\n");
#ifdef FIXED
	(void)ra;
	printf("RESULT: FIXED model โ€” NO UAF. The refcount held across the lock-drop\n");
	printf("window (refcount_acquire under ipc_lock + fuse_ipc_put after access)\n");
	printf("prevents the tx-waiter's timeout put from freeing fip mid-access.\n");
	printf("This is exactly the fix in fix.diff.\n");
	return 0;
#else
	if (ra == (void*)1) {
		printf("RESULT: USE-AFTER-FREE reproduced deterministically.\n");
		printf("The device completion path dereferences fip after the tx "
		       "timeout freed it;\nthis is the exact pattern the finding cites "
		       "(no refcount held across the lock-drop window).\n");
		return 0;
	}
	printf("RESULT: no UAF observed (unexpected for this forced interleaving).\n");
	return 1;
#endif
}