โฌข DragonFlyBSD Kernel Audit
DF-0918 / harness.c
โ† back to finding โ†“ download raw
/*
 * DF-0918 โ€” Deterministic proof of the NULL-ohd / KKASSERT(ohd) panic in
 * fuse_ipc_wait()'s early-return path.
 *
 * THE BUG (sys/vfs/fuse/fuse_ipc.c:158-204, via fuse_device.c:99-116 +
 * fuse_ipc.c:246-287):
 *
 *   fuse_ipc_wait() has THREE early-return paths that test the `replied`
 *   flag (fip->done) and `return 0` WITHOUT re-checking the mount's `dead`
 *   flag and WITHOUT verifying that fip->reply.buf was populated:
 *
 *     fuse_ipc.c:169   if (fuse_ipc_test_replied(fip))  return 0;
 *     fuse_ipc.c:173   if (fuse_ipc_test_replied(fip))  return 0;  (post-interlock)
 *     (the goto-again loop at :171/:184 re-runs :172-173 up to 6x, widening
 *      the window)
 *
 *   fuse_device_clear() (fuse_device.c:99-116) โ€” invoked by the daemon
 *   reader (fuse_device_read:137) when it observes `dead` during teardown โ€”
 *   walks fmp->reply_head and for every pending fip calls
 *   fuse_ipc_test_and_set_replied(fip) + wakeup(fip) WITHOUT assigning
 *   fip->reply.buf.  fip->reply.buf stays NULL (set to NULL by fuse_ipc_get
 *   at fuse_ipc.c:104).  fuse_mount_kill (fuse_vfsops.c:65-76) has ALREADY
 *   set dead=1 by the time fuse_device_clear runs.
 *
 *   RACE: a tx waiter (fuse_ipc_tx -> fuse_ipc_wait) that has passed the
 *   line-163 dead check (dead was 0 then), but is between :169 and the
 *   line-198 post-tsleep dead recheck, can observe `replied==1` at :169 or
 *   :173 (set by fuse_device_clear underneath it) and `return 0`.  Back in
 *   fuse_ipc_tx:
 *
 *       fuse_ipc.c:274   ohd = fuse_out(fip);          // == fip->reply.buf == NULL
 *       fuse_ipc.c:275   KKASSERT(ohd);                // PANIC (INVARIANTS ON)
 *                              // or, INVARIANTS OFF:
 *       fuse_ipc.c:276   error = ohd->error;           // NULL deref -> SIGSEGV
 *
 *   The live race window is sub-microsecond (tx waiter between tsleep_interlock
 *   and the replied check).  Winning it live needs many thousands of attempts
 *   against a 5*hz tsleep.  This harness DETERMINISTICALLY reproduces the
 *   pattern (option (b) in the verifier playbook).
 *
 * Build the UNFIXED model (reproduces the NULL-ohd primitive):
 *   cc -O2 -o harness harness.c -lpthread
 * Build the FIXED model (applies fix.diff's dead-recheck-after-replied):
 *   cc -O2 -DFIXED -o harness_fixed harness.c -lpthread
 * Run:   ./harness         -> "NULL-ohd PRIMITIVE CONFIRMED" (exit 0)
 *        ./harness_fixed   -> "NO NULL-ohd (fix rechecks dead)" (exit 0)
 */
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdatomic.h>

struct fuse_buf { void *buf; size_t len; };

struct fuse_mount { atomic_int dead; };

struct fuse_ipc {
	struct fuse_mount *fmp;
	struct fuse_buf    request;
	struct fuse_buf    reply;     /* fuse_device_clear leaves .buf = NULL */
	atomic_int         done;      /* `replied` flag (fip->done) */
};

static pthread_barrier_t b1, b2;
static struct fuse_mount g_fmp;
static struct fuse_ipc  *g_fip;

static int  test_dead(struct fuse_mount *f) { return atomic_load(&f->dead); }
static int  test_replied(struct fuse_ipc *f){ return atomic_load(&f->done); }
static int  tas_replied(struct fuse_ipc *f) { return atomic_compare_exchange_strong(&f->done, &(int){0}, 1); }
static void set_dead(struct fuse_mount *f)  { atomic_store(&f->dead, 1); }

static struct fuse_ipc *ipc_get(struct fuse_mount *fmp)
{
	struct fuse_ipc *fip = calloc(1, sizeof(*fip));
	fip->fmp = fmp;
	fip->request.buf = (void*)0x1000;
	fip->reply.buf   = NULL;        /* <<< the dangerous initial state */
	atomic_init(&fip->done, 0);
	return fip;
}

/* Thread B: models fuse_mount_kill (sets dead=1) followed by the daemon
 * reader waking and running fuse_device_clear (sets replied=1 on every
 * pending fip WITHOUT populating reply.buf, then wakes the waiter).
 *
 * In the real kernel fuse_mount_kill runs BEFORE fuse_device_clear, so by
 * the time replied is set, dead is already 1. */
static void *kill_and_clear(void *arg)
{
	(void)arg;
	/* B1: wait until the tx waiter has passed its line-163 dead check
	 * (dead was 0) and is sitting right before its line-169/173 replied
	 * test โ€” the race window. */
	pthread_barrier_wait(&b1);

	/* === fuse_mount_kill (fuse_vfsops.c:69) === */
	set_dead(&g_fmp);
	/* === fuse_device_clear (fuse_device.c:111-115) ===
	 * reply.buf is NOT assigned here โ€” only fuse_device_write does that
	 * (fuse_device.c:205).  It stays NULL. */
	tas_replied(g_fip);
	/* wakeup(fip) โ€” modelled by the b2 barrier */

	/* B2: release the tx waiter to observe replied=1 (and, in FIXED, dead=1). */
	pthread_barrier_wait(&b2);
	return NULL;
}

/* Thread A: fuse_ipc_wait() early-return -> fuse_ipc_tx:274-275. */
static void *tx_waiter(void *arg)
{
	(void)arg;
	long rc = 0;
	void *ohd;

	/* fuse_ipc.c:163 โ€” dead is 0 at this point (race not yet triggered) */
	if (test_dead(g_fip->fmp)) { rc = -1; goto out; }

	/* RACE WINDOW: kill_and_clear sets dead=1 AND replied=1 underneath us */
	pthread_barrier_wait(&b1);
	pthread_barrier_wait(&b2);

	/* fuse_ipc.c:169 (and :173 โ€” same shape) */
#ifdef FIXED
	if (test_replied(g_fip)) {
		/* THE FIX (fix.diff): recheck dead; fuse_device_clear implies
		 * dead=1 (fuse_mount_kill ran first). */
		if (test_dead(g_fip->fmp)) {
			printf("[tx-waiter] replied set by teardown + dead=1 -> ENOTCONN (fix)\n");
			rc = -2;   /* ENOTCONN */
			goto out;
		}
		printf("[tx-waiter] replied set by real reply + dead=0 -> return 0\n");
		rc = 0;
		goto out;
	}
#else
	if (test_replied(g_fip)) {
		/* UNFIXED: trust replied blindly, return 0. */
		printf("[tx-waiter] replied set -> return 0 (NO dead recheck)\n");
		/* fall through to fuse_ipc_tx:274-275 */
		ohd = g_fip->reply.buf;        /* fuse_out(fip) == NULL */
		if (ohd == NULL) {
			printf("[tx-waiter] *** NULL-ohd CONFIRMED *** fuse_out(fip)=NULL;\n");
			printf("          fuse_ipc.c:275 KKASSERT(ohd) PANIC (INVARIANTS) or\n");
			printf("          fuse_ipc.c:276 ohd->error NULL-deref.\n");
			rc = 1;
		}
		goto out;
	}
#endif
out:
	if (rc == 1)
		printf("[tx-waiter] RESULT: primitive reproduced (NULL fuse_out -> panic).\n");
	else if (rc == -2)
		printf("[tx-waiter] RESULT: fix returned ENOTCONN (no NULL deref).\n");
	return (void*)rc;
}

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

	atomic_init(&g_fmp.dead, 0);   /* mount alive at start */
	g_fip = ipc_get(&g_fmp);
	pthread_barrier_init(&b1, NULL, 2);
	pthread_barrier_init(&b2, NULL, 2);

	printf("DF-0918 deterministic NULL-ohd harness\n");
	printf("modeling fuse_ipc_wait early-return (fuse_ipc.c:163,169,173)\n");
	printf("         vs fuse_mount_kill+fuse_device_clear (fuse_vfsops.c:69 + fuse_device.c:111-115)\n");
	printf("         + fuse_ipc_tx:274-275 (ohd = fuse_out(fip); KKASSERT(ohd))\n");
	printf("fip=%p reply.buf=%p (NULL by design, fuse_ipc.c:104)\n\n",
	    (void*)g_fip, g_fip->reply.buf);
#ifdef FIXED
	printf("(FIXED model: waiter rechecks dead after replied โ€” fix.diff)\n\n");
#else
	printf("(UNFIXED model: waiter trusts replied without dead recheck)\n\n");
#endif

	pthread_create(&ta, NULL, tx_waiter, NULL);
	pthread_create(&tb, NULL, kill_and_clear, NULL);
	pthread_join(ta, &ra);
	pthread_join(tb, NULL);
	pthread_barrier_destroy(&b1);
	pthread_barrier_destroy(&b2);

	printf("\n");
#ifdef FIXED
	if (ra == (void*)-2) {
		printf("RESULT: FIXED model โ€” NO NULL-deref.  dead-recheck after replied\n");
		printf("(fix.diff) returns ENOTCONN when fuse_device_clear set replied\n");
		printf("during teardown, so fuse_ipc_tx:275 never derefs NULL ohd.\n");
		free(g_fip);
		return 0;
	}
	printf("RESULT: FIXED model โ€” unexpected ra=%p\n", ra);
	free(g_fip);
	return 1;
#else
	if (ra == (void*)1) {
		printf("RESULT: NULL-ohd PRIMITIVE reproduced deterministically.\n");
		printf("The early-return trusted `replied` without rechecking dead;\n");
		printf("fuse_device_clear set replied with reply.buf still NULL, so\n");
		printf("fuse_ipc.c:275 KKASSERT(ohd) fires (panic on default GENERIC).\n");
		free(g_fip);
		return 0;
	}
	printf("RESULT: no primitive observed (unexpected for this forced interleaving).\n");
	free(g_fip);
	return 1;
#endif
}