# DF-2804 — Permanent kernel memory leak of undelivered udev event dictionaries on last-reader close

## What

Every queued udev event owns exactly **one** reference on its prop
dictionary: the `prop_dictionary_copy()` taken in `udev_event_insert()`
(sys/kern/kern_udev.c:508-512). That reference is released in exactly one
place: `udev_event_externalize()` (kern_udev.c:571), i.e. only when a
reader *reads* the event.

`udev_clean_events_locked()` (kern_udev.c:534-545) reaps events from the
queue head with a bare `objcache_put()` and **no `prop_object_release()`**,
and `udev_dev_close()` (kern_udev.c:709-744) merely removes the closing
reader's marker and calls that function. When the **last** initiated
reader closes with events still queued (its marker never reached them,
nobody externalized them), those events are reaped — or simply abandoned
on `udev_evq` — with their dictionary reference never dropped. The dict
and every object it retains (key strings, numbers) leak **permanently**;
there is no reclaim path.

Trigger (root/wheel, same `/dev/udev` 0600 gate as DF-0055): open
`/dev/udev`, issue `UDEVPROP getdevs` (initiates the reader), let device
events queue (an *unprivileged* pty-churn process suffices as the event
source — `/dev/ptmx` is 0666), then `close()` without reading. Repeat at
will: the leak accumulates monotonically.

## Verified impact (guest, stock INVARIANTS kernel)

```
S0 baseline:                    propdict  333 in-use (62.4K)
cycle 1 (30k pty cycles,
 reader closes w/o reading):
 S1 after close:                propdict 58.8K in-use (11.0M) + 10.1M prop_dictionary
 S2 after 10s idle:             identical  (no reclaim — permanent)
cycle 2 (same):
 S3:                            propdict  117K in-use (22.0M) + 20.2M  (~2x, monotone)
control (same churn, ACTIVE reader drains 60,000 events):
 S4:                            propdict  117K in-use (22.0M)  (flat — churn+drain does NOT leak)
```

~24 MB leaked per 30k-cycle run (~400 B/event), unreclaimable, repeatable
without limit → kernel memory exhaustion by a root/wheel process (or by a
udev-consuming daemon that restarts with a backlog).

## Reproduce (guest)

```
./build.sh              # as root: builds /tmp/harness + /tmp/churn
sh /tmp/df2804_run.sh   # as root: 2 leak cycles (churn as user maxx +
                        # stalled root reader that closes without reading)
                        # + active-drain control
```

Expected: `S1` ≈ +24 MB retained after close; `S2 == S1` after idle;
`S3` ≈ 2x the `S1` delta; `S4 == S3` (control flat).

Files: `harness.c` (reader: `stall` mode = initiate, sleep, close without
reading), `churn.c` (unprivileged pty churn), `run.sh`, `run.log`
(decisive run), `build.log`, `fix.diff`.
