DragonFlyBSD Kernel Audit
DF-0246 / eh_uaf.c
← back to finding ↓ download raw
/*
 * DF-0246 PoC: eventhandler dispatch (EVENTHANDLER_INVOKE) traverses the
 * entry list WITHOUT holding evlist_token, while eventhandler_deregister
 * does TAILQ_REMOVE+kfree UNDER the token.  This is a use-after-free:
 * dispatch holds a raw TAILQ_NEXT pointer; concurrent deregister+kfree
 * on another CPU (module detach / device detach) frees the entry the
 * dispatcher is about to follow.
 *
 * This is a CONCURRENCY bug.  The deregister side is initiated by root
 * (kldunload / device detach).  All 30+ EVENTHANDLER_DEREGISTER call sites
 * in sys/ are in module detach or device detach handlers (root context).
 * There is no unprivileged syscall path that triggers deregister.
 *
 * The invoke side runs frequently (exec/exit/shutdown/dev_clone/vlan events)
 * but is always paired with a root-initiated deregister for the UAF to fire.
 *
 * This PoC is a CODE-PATH CONFIRMATION harness, not a live trigger:
 * it demonstrates that the dispatch macro holds no lock by showing the
 * generated expansion.  A live trigger requires kldunload racing an
 * eventhandler invoke — a root-context race.
 *
 * Build:  cc -o eh_uaf eh_uaf.c
 * Run:    ./eh_uaf
 */
#include <stdio.h>

/*
 * The vulnerable macro (sys/sys/eventhandler.h:114-126):
 *
 *   #define EVENTHANDLER_INVOKE(name, args...)
 *   do {
 *       struct eventhandler_list *_el;
 *       struct eventhandler_entry *_ep;
 *
 *       if ((_el = eventhandler_find_list(#name)) != NULL) {
 *           for (_ep = TAILQ_FIRST(&(_el->el_entries));   <-- NO TOKEN HELD
 *                _ep != NULL;
 *                _ep = TAILQ_NEXT(_ep, ee_link)) {          <-- raw ptr, can be freed
 *               ((... *)_ep)->eh_func(_ep->ee_arg, ##args); <-- deref freed entry
 *           }
 *       }
 *   } while (0)
 *
 * eventhandler_find_list() acquires+RELEASES evlist_token internally (line 146/153),
 * then returns.  The for-loop traverses el_entries with NO token held.
 *
 * Concurrent eventhandler_deregister (line 120-135):
 *   lwkt_gettoken(&evlist_token);
 *   TAILQ_REMOVE(&list->el_entries, ep, ee_link);
 *   kfree(ep, M_EVENTHANDLER);               <-- entry freed while dispatcher may hold it
 *   lwkt_reltoken(&evlist_token);
 *
 * The deregister holds the token but the DISPATCH does not, so the token
 * provides NO protection against the race.
 */

int
main(void)
{
	printf("DF-0246: eventhandler UAF race confirmation\n");
	printf("============================================\n\n");
	printf("Bug: EVENTHANDLER_INVOKE (sys/sys/eventhandler.h:114-126)\n");
	printf("     traverses el_entries WITHOUT evlist_token.\n");
	printf("     eventhandler_deregister (subr_eventhandler.c:120-126)\n");
	printf("     does TAILQ_REMOVE+kfree UNDER the token.\n\n");
	printf("Race window: dispatch holds raw TAILQ_NEXT pointer to entry E;\n");
	printf("  deregister on another CPU removes+kfree(E) under token;\n");
	printf("  dispatcher follows freed pointer -> UAF.\n\n");
	printf("Trigger requirement: concurrent deregister (kldunload/device detach).\n");
	printf("All 30+ EVENTHANDLER_DEREGISTER sites are root-context (module detach).\n");
	printf("No unprivileged path to deregister -> root-initiated race only.\n\n");
	printf("EVENTHANDLER_FAST_INVOKE (header:81-90) has the same tokenless traverse.\n");
	printf("Fix: hold evlist_token during the traverse (see fix.diff).\n");
	return 0;
}