# DF-2803 VERDICT — reproduced (dos)

## Bottom line

**Reproduced.** With a single initiated-but-stalled root reader on
`/dev/udev` and a single unprivileged pty-churn process (user `maxx`,
`/dev/ptmx` 0666), the kernel udev event queue grew to **240,136 live
events (~90 MB of kernel allocations across the propdict /
prop_dictionary / propstng / udev malloc domains)** in 20 seconds, with
linear growth and no bound. This is a local kernel-memory-exhaustion DoS:
the queue is bounded by nothing (`udev_evqlen` is incremented at
sys/kern/kern_udev.c:517 with no limit check) and is drained only up to the
oldest reader marker (kern_udev.c:539-545).

## How it reproduces

1. `harness stallcount 30` (root): `open("/dev/udev")` + `ioctl(UDEVPROP,
   {"command":"getdevs"})`. The ioctl marks the reader *initiated*
   (kern_udev.c:974-978), inserting its marker at the queue head — from
   now on `udev_event_insert()` (kern_udev.c:507) queues every device
   event. The harness then just sleeps.
2. `churn 20` (user maxx): `posix_openpt()`/`close()` loop. Each cycle
   creates + destroys ptm/pts cdevs on the devfs core thread; each create
   fires `udev_event_attach()` (sys/vfs/devfs/devfs_core.c:1428) and each
   destroy fires `udev_event_detach()` (devfs_core.c:1451), each queueing
   an event carrying a full prop_dictionary copy (kern_udev.c:508-512).
3. Because the only reader never advances its marker,
   `udev_clean_events_locked()` reaps nothing; `udev_evqlen` and the
   prop-dict allocations grow linearly with churn (measured ~12k events/s,
   ~370 B retained per event, plus per-event dict copies of ~190 B).
4. After the stall window the harness switches the fd to non-blocking and
   drains: **240,136 events, 185 MB of XML** — the direct measure of the
   queue backlog. Counters return to baseline after the drain (no leak in
   this variant; the leak variant is DF-2804).

## Why the "needs root reader" gate does not neutralize it

- `/dev/udev` is 0600 root:wheel (kern_udev.c:1039-1041), identical gate
  to DF-0055 (filed Medium). The *producer* side is unprivileged and
  unlimited; the *consumer* side (devd/udevd) is a root daemon whose stall
  or slowness (blocked client, SIGSTOP, CPU starvation) turns any user's
  hotplug churn into unbounded kernel allocation. A root/wheel attacker
  triggers it trivially and deterministically as shown.
- The same churn with an actively-draining reader was measured as the
  control in the DF-2804 pack (`S4` sample, flat) — the unbounded growth
  is specifically a function of reader lag, which the kernel never
  bounds.

## Evidence

- `run.log` — baseline → t+6s/t+14s/t+19s samples during the stall
  (propdict 288 → 72.4K → 167K → 226K in-use; udev 139 → 226K), churn
  log (120,068 cycles / 20 s as user maxx), backlog drain count
  (240,136 events / 185,144,856 bytes), post-drain baseline return.
- `harness.c` (`stallcount` mode), `churn.c`.
- `build.log` — compiler output + guest environment.

## Exploit chain (DoS)

`posix_openpt()` loop as any user + any initiated-but-lagging `/dev/udev`
reader → linear, unbounded kernel heap consumption (~44 MB/min per churn
process measured) → vm exhaustion → `M_WAITOK` sleeps / panic-wedge. No
privesc path: the primitive is allocation pressure only; no controllable
overflow exists in this queue (all copies are refcounted prop objects).

## Fix

`fix.diff` — cap the queue (`UDEV_EVQ_MAX`) checked under `udev_lk` before
consuming the copy; when full, drop the event (bump `udev_seq` and wake
`UDEVWAIT`/kqueue listeners so consumers observe the drop) instead of
growing unboundedly. Fix validated by reasoning only (DoS-class finding;
kernel rebuild validation was not performed — `fix_status:
not_testable`).

## Kernel references

- sys/kern/kern_udev.c:500-532 — `udev_event_insert`, no bound
- sys/kern/kern_udev.c:517 — `++udev_evqlen` (only accounting)
- sys/kern/kern_udev.c:534-545 — reap stops at oldest marker
- sys/kern/kern_udev.c:826-830, 973-979 — reader initiation
- sys/vfs/devfs/devfs_core.c:1428,1451 — event producers
- sys/kern/tty_pty.c:176 — unprivileged clone source (via /dev/ptmx 0666)
