DF-0246 / eh_uaf.c
/* * 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; } |