# DF-0055 — PoC (REPRODUCED)

`udev_uaf.c` — deterministic trigger for the use-after-free of the shared
udev event dictionary via the multi-reader event-replication path.

## Verdict

**REPRODUCED.** 3/3 fresh-VM runs panicked the DragonFlyBSD master DEV
kernel in the exact cited path
(`udev_dev_read → udev_event_externalize → prop_object_release`). Two
distinct panic signatures (malloc double-free; proplib refcount-underflow),
same root cause. See `VERDICT.md` and `panic.txt`.

## The bug

The udev event queue replicates every event to all openers via per-softc
marker nodes spliced into the shared `udev_evq` TAILQ.
`udev_event_externalize()` (`sys/kern/kern_udev.c:548`) at `:571` calls
`prop_object_release(ev->ev.ev_dict)` on the **first** reader to
externalize an event, freeing the shared proplib dict while the event
remains on `udev_evq`. The field is never `NULL`ed, so:

- `udev_clean_events_locked()` (`:535`, predicate `ev_dict != NULL` at
  `:540`) leaves the event in the queue (it is stopped at the trailing
  reader's marker, which has a NULL `ev_dict`).
- The next reader's marker-walk (`:838-840`) finds the same event
  (dangling non-NULL `ev_dict` passes the skip loop), calls
  `udev_event_externalize()` again, which at `:566` passes the freed
  pointer to `prop_dictionary_set()` → `prop_object_retain(freed)` →
  **UAF write**; the subsequent `:571` release then triggers a double-free
  / refcount underflow.

The sequence is **deterministic** (`udev_lk` serializes the two reads; a
reader cannot advance another reader's marker, so reader 2's marker is
guaranteed to still precede the event).

## Privilege

`/dev/udev` is `0600 root:wheel` (`kern_udev.c:1039-1041`). Trigger must
run as root (uid 0). `maxx` (uid 1001, not in wheel) cannot open it.
Realistic impact: **reliable local kernel panic (DoS) from root** —
matching the finding's Medium / `CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H`.

## Build & run (root, disposable VM)

```
./build.sh         # cc -O2 -o udev_uaf udev_uaf.c
./run.sh           # ./udev_uaf   (panics a vulnerable kernel in ~1-2s)
```

## Expected output

**Bug present** — kernel panic (captured on the serial console /
`dfbsd-qemu/boot.log`):

```
panic: memory chunk 0xfffff80067b36860 is already free!
chunk_mark_free() at chunk_mark_free+0xae
_kfree() at _kfree+0x262
_prop_dictionary_free() at _prop_dictionary_free+0xe0
prop_object_release() at prop_object_release+0xfd
udev_dev_read() at udev_dev_read+0x14f
```
or the equivalent refcount-underflow form:
```
panic: assertion "ocnt != 0" failed in prop_object_release at .../prop_object.c:1085
```

**Bug fixed** — runs ~12s, prints
`no panic after 80 create/destroy cycles`, exits 0.

## Trigger mechanism

The PoC forks two readers, each opening `/dev/udev` (own softc/marker)
and blocking in `read()` (which auto-inserts both markers at
`TAILQ_HEAD`). The parent then runs `ifconfig tap<N> create`/`destroy`
in a loop — each `make_dev`/`destroy_dev` on `tap_ops` generates a
`UDEV_EVENT_ATTACH`/`_DETACH` via `udev_event_insert`
(`sys/vfs/devfs/devfs_core.c:1428,1451`; `sys/net/tap/if_tap.c:289,491`).
The first event the second reader reaches fires the UAF.

## Fix

See `fix.diff` (matches the finding's proposal): move the
`prop_object_release` from `udev_event_externalize:571` into
`udev_clean_events_locked` (release + NULL before `TAILQ_REMOVE`), so the
shared dict is released exactly once when the event is reaped (after all
readers have passed it), and each reader merely borrows it.
