# 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`:

```c
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:

```c
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).
