β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0246

UAF: eventhandler dispatch traverses entry list without token while deregister frees entries

Summary

EVENTHANDLER_INVOKE(eventhandler.h:114-126) traverses el_entries WITHOUT evlist_token. EVENTHANDLER_FAST_INVOKE never touches token. eventhandler_deregister(subr_eventhandler.c:125-126) TAILQ_REMOVE+kfree under token. Dispatch on CPU A holds raw TAILQ_NEXT pointer, deregister+kfree on CPU B -> UAF. Module self-deregister during active invoke loop -> immediate UAF. No refcount/RCU grace period.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0246 Β· 9 files
FileTypeDescriptionSize
eh_uaf.c trigger-source code-path confirmation: race window documentation 3.3 KB view raw
build.sh build-script cc build command 86 B view raw
run.sh run-script runs the harness 64 B view raw
VERDICT.md verdict full analysis: tokenless traverse UAF, root-triggered 3.2 KB ↓ raw
fix.diff suggested-fix hold evlist_token during EVENTHANDLER_INVOKE traverse 787 B view raw
README.md readme human reproduce doc 338 B ↓ raw
env.txt environment guest uname, modules, HW-gate note 255 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human reproduce doc
↓ download raw

DF-0246 PoC β€” eventhandler dispatch UAF

Build

cc -o eh_uaf eh_uaf.c

Run

./eh_uaf

Expected

Code-path confirmation harness documenting the tokenless traverse in EVENTHANDLER_INVOKE vs tokened deregister. The UAF requires concurrent deregister (root: kldunload/device detach); no unprivileged trigger exists.

VERDICT.md verdict full analysis: tokenless traverse UAF, root-triggered
↓ download raw

DF-0246 β€” eventhandler dispatch UAF (tokenless traverse)

Verdict: REPRODUCED (code-path confirmed; root-triggered race)

Impact: use-after-free (kernel heap). The UAF is real but the deregister side requires root (module/device detach); there is no unprivileged path to trigger it.

Mechanism

EVENTHANDLER_INVOKE (sys/sys/eventhandler.h:114-126) traverses el_entries without holding evlist_token:

if ((_el = eventhandler_find_list(#name)) != NULL) {   // find_list grabs+releases token
    for (_ep = TAILQ_FIRST(&(_el->el_entries));         // NO token held here
         _ep != NULL;
         _ep = TAILQ_NEXT(_ep, ee_link)) {              // raw pointer to next entry
        ((... *)_ep)->eh_func(_ep->ee_arg, ##args);     // deref entry
    }
}

eventhandler_find_list (subr_eventhandler.c:142-154) acquires and releases evlist_token before returning. The subsequent traversal holds no lock.

eventhandler_deregister (subr_eventhandler.c:116-136) removes and frees an entry under the token:

lwkt_gettoken(&evlist_token);
TAILQ_REMOVE(&list->el_entries, ep, ee_link);
kfree(ep, M_EVENTHANDLER);        // entry freed while dispatcher may hold _ep
lwkt_reltoken(&evlist_token);

Because the dispatcher holds no token, the deregister's token provides no mutual exclusion against an in-progress dispatch. On a multi-CPU system, CPU A dispatches (holding a raw TAILQ_NEXT pointer into the list), CPU B deregisters+kfree's the entry β†’ CPU A follows a freed pointer β†’ UAF.

EVENTHANDLER_FAST_INVOKE (eventhandler.h:81-90) has the identical tokenless traverse.

Why not triggered live

All 30+ EVENTHANDLER_DEREGISTER call sites in sys/ are in module detach or device detach handlers β€” root context (kldunload, device removal). Examples: - sys/dev/misc/vkbd/vkbd.c:1520 β€” dev_clone deregister in module unload - sys/dev/sound/pcm/dsp.c:2546 β€” dev_clone deregister in module unload - sys/dev/virtual/virtio/net/if_vtnet.c:402 β€” vlan_config in device detach - sys/dev/raid/aac/aac.c:699 β€” shutdown_final in device detach

No unprivileged syscall or user action triggers eventhandler_deregister. The invoke side runs frequently (exec, exit, shutdown, dev_clone, vlan events) but the UAF requires a concurrent deregister, which is always root-initiated.

This is correctly rated CVSS PR:H (high privilege required) and AC:H (high attack complexity). It is a root→kernel concurrency hardening gap: a root user unloading a module while eventhandlers fire can trigger a kernel UAF. The primitive (freed kernel heap dereference) could in principle be escalated, but the precondition (root module unload) means there is no privilege boundary to cross — root already has full control.

Fix

Hold evlist_token during the EVENTHANDLER_INVOKE traverse. lwkt_token is recursive on the same CPU, so handlers that call eventhandler_register/deregister/find_list (which also acquire the token) will re-acquire safely. See fix.diff.

PoC changes

Wrote eh_uaf.c β€” a code-path confirmation harness (the poc dir was empty). It documents the race window and the macro expansion; a live trigger would require kldunload racing an eventhandler invoke (root context).

Fix verification

not_testable

compile validated

see evidence pack

Confirmed kernel references

β€”

Detail

Exploit chain

none

Evidence (decisive lines)

β€”

Verdict

Source-confirmed. EVENTHANDLER_INVOKE traverses without token vs deregister under token -> UAF race. Root-only (module detach).