# DF-2804 VERDICT — reproduced (leak)

## Bottom line

**Reproduced.** A root reader that initiates the udev event queue
(`UDEVPROP getdevs`) and then closes `/dev/udev` **without reading the
queued backlog** permanently leaks every undelivered event's prop
dictionary. Measured on the guest: 58.8K prop-dict allocations
(~24 MB across propdict/prop_dictionary/propstng/prop_string) retained
after one 30k-cycle pty churn, zero reclaim after idle, exactly doubled
after a second cycle, while the identical churn drained by an active
reader leaked nothing (control flat). The leak is repeatable without
limit and unreachable by any reclaim path: the event's single
`ev->ev.ev_dict` reference (created at sys/kern/kern_udev.c:508-512) is
dropped only by `udev_event_externalize()` (kern_udev.c:571), and the
close path (`udev_dev_close` → `udev_clean_events_locked`,
kern_udev.c:721-726, 534-545) reaps/abandons events without any
`prop_object_release()`.

## Ownership trace (why it leaks)

1. `udev_event_insert()` (kern_udev.c:508-512): `dict_copy =
   prop_dictionary_copy(dict)` — the queued event owns one reference.
2. A reader releases that reference **only** by externalizing the event:
   `udev_dev_read()` → `udev_event_externalize()` →
   `prop_object_release(ev->ev.ev_dict)` (kern_udev.c:571).
3. `udev_dev_close()` of the last initiated reader removes its marker and
   calls `udev_clean_events_locked()`, which does
   `objcache_put(udev_event_kernel_cache, ev)` for head events — **no
   release** (kern_udev.c:541-543) — and leaves everything after the old
   marker position on `udev_evq` forever if no reader ever initiates
   again (udev_initiated_count == 0 ⇒ nothing consumes or reaps them).
4. Neither path drops the reference for events no reader externalized:
   refcount stays 1 → the dict and all objects it retains are never
   freed. Confirmed empirically: the `udev` malloc domain (event structs,
   objcache-backed) returns to baseline after close while `propdict` /
   `prop_dictionary` stay elevated — proving the *events* were reaped
   but their *dicts* were not released.

## Marker-protocol note

The queue interleaves reader **markers** (softc-embedded
`udev_event_kernel` with `ev_dict == NULL`, skipped by the reaper
predicate at kern_udev.c:540) with real events. The leak is therefore
exact, not heuristic: reaped-head events before the closing reader's
marker position were externalized by *someone* (balanced); events after
it (externalized by no one) either leak via reap-without-release when the
closing reader's marker was at the head, or leak via abandonment when
they sit behind markers of readers that also close without reading.

## Fix shape (fix.diff)

Move the single release to the reaper and drain everything when the last
reader leaves:

- `udev_event_externalize()`: stop releasing `ev->ev.ev_dict` there
  (readers borrow the dict; `prop_dictionary_set` retains it for the
  duration of externalization). This is the same protocol change
  recommended by DF-0055's fix — it fixes the multi-reader UAF *and*
  makes release ownership single-sited.
- `udev_clean_events_locked()`: release-and-NULL `ev->ev.ev_dict` before
  `objcache_put`, and when `udev_initiated_count == 0` (no markers can
  remain) drain the **entire** queue, releasing every undelivered dict.

Fix authored from line-accurate root cause; kernel rebuild validation not
performed (leak-class finding; the mandatory fix-build applies to
reproduced memory corruption) — `fix_status: not_testable`.

## Evidence

- `run.log` — S0/S1/S2/S3/S4 samples: propdict 333 → 58.8K (11.0M) →
  identical after 10s idle → 117K (22.0M) after cycle 2 → 117K (22.0M)
  after the active-drain control; churn logs (30k cycles as user maxx per
  cycle); control read log (`read 60000 events (46260000 bytes)`).
- `harness.c` (`stall` mode), `churn.c`, `build.log`.

## Exploit chain

Root/wheel (the `/dev/udev` 0600 gate, same as DF-0055): repeated
open → getdevs → unprivileged pty churn → close-without-read cycles leak
~24 MB per 4-second cycle → kernel memory exhaustion (wedge/panic).
No info disclosure or corruption component; impact is availability.

## Kernel references

- sys/kern/kern_udev.c:508-512 — the event's single dict reference
- sys/kern/kern_udev.c:571 — the ONLY release, on the read path
- sys/kern/kern_udev.c:534-545 — reap without release / stop at marker
- sys/kern/kern_udev.c:709-744 — close path (no walk of undelivered queue)
- sys/vfs/devfs/devfs_core.c:1428,1451 — producers (unpriv pty churn source)
