Unbounded udev event queue growth behind a slow/stalled initiated reader (no udev_evqlen cap, no backpressure)
| Field | Value |
|---|---|
| ID | DF-2803 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-770 Allocation of Resources Without Limits |
| File | sys/kern/kern_udev.c |
| Lines | 515-523 (reap block :534-545) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
udev_event_insert() queues a full prop_dictionary copy per device event with no limit on udev_evqlen and no backpressure. Events are reaped only up to the oldest reader marker, so one initiated-but-not-reading /dev/udev reader pins the queue head and kernel memory grows linearly for as long as device events are generated. Verified on the guest: 240,136 live events (~90 MB) in 20 s from ONE unprivileged pty-churn process, then drained cleanly. Producer side fully unprivileged (~12k events/s per pty-churn process, unlimited processes); consumer gate is /dev/udev 0600 root:wheel β identical gate to DF-0055 (filed Medium). Realistic non-malicious trigger: the udev daemon blocked or slow while any user generates hotplug churn. DF-2711 family.
Recommended fix
Bound the queue (UDEV_EVQ_MAX cap, drop observably beyond it) β validated fix.diff in findings/poc/DF-2803/.
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_udev.c (GLM 5.3); 90MB/20s growth reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2803 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 4.1 KB | β raw | |
| harness.c | β | 4.1 KB | view raw | |
| churn.c | β | 1.4 KB | view raw | |
| build.sh | β | 223 B | view raw | |
| run.sh | β | 1.1 KB | view raw | |
| build.log | β | 360 B | view raw | |
| run.log | β | 2.2 KB | view raw | |
| env.txt | β | 540 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| manifest.json | β | 1.0 KB | view raw | |
| verdict.json | β | 3.5 KB | view raw |
DF-2803 β Unbounded udev event-queue growth with an initiated-but-stalled reader
What
udev_event_insert() (sys/kern/kern_udev.c:500-532) queues every device
event with no cap on udev_evqlen and no backpressure. Events are only
reaped up to the oldest reader marker (udev_clean_events_locked,
kern_udev.c:534-545), so one /dev/udev reader that has initiated
(read() auto-initiates at kern_udev.c:826-830, UDEVPROP getdevs at
kern_udev.c:973-979) but then stops reading pins the queue head, and kernel
memory grows without bound for as long as device events are generated β
each queued event holds a full prop_dictionary copy plus retained
value objects.
The event source can be completely unprivileged: every
posix_openpt() cycle creates and destroys ptm/pts cdevs, and
devfs_create_dev_worker() fires udev_event_attach() for each
(sys/vfs/devfs/devfs_core.c:1428). /dev/ptmx is mode 0666. Measured on
the guest: ~12,000 events/second from one unprivileged process.
The reader gate is /dev/udev at 0600 root:wheel
(kern_udev.c:1039-1041) β the same gate as DF-0055. Realistic stalled
readers: a root/wheel attacker, or the udev-consuming daemon (devd) being
slow or blocked while any user generates hotplug events.
Verified impact (guest, stock INVARIANTS kernel)
One unprivileged pty-churn process + one initiated-but-sleeping root reader:
baseline: propdict 288 in-use (54.0K) t+6s: propdict 72.4K in-use (13.6M) udev 72.3K t+14s: propdict 167K in-use (31.4M) udev 167K t+19s: propdict 226K in-use (42.4M) udev 226K backlog drained after stall: 240,136 events (185 MB serialized) in 20s of churn
~90 MB of kernel memory queued in 19 s; growth is linear, unlimited, and
per-churn-process multiplicative. Left running it exhausts kernel memory
(objcache_get(M_WAITOK) sleeps / vm exhaustion wedge or panic).
Reproduce (guest)
./build.sh # as root: builds /tmp/harness (root reader) + /tmp/churn (unpriv source)
sh /tmp/df2803_run.sh # as root: baseline sample, 20s churn as user maxx,
# 30s stalled root reader, samples during stall,
# then non-blocking drain count of the backlog
Expected: harness: backlog drained: ~240000 events, vmstat -m propdict
in-use climbing linearly during the stall and returning to baseline after
the drain.
Files: harness.c (reader: stallcount mode), churn.c (unprivileged
pty churn), run.sh, run.log (decisive run), build.log, fix.diff.
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
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 onudev_event_insert()(kern_udev.c:507) queues every device event. The harness then just sleeps.churn 20(user maxx):posix_openpt()/close()loop. Each cycle creates + destroys ptm/pts cdevs on the devfs core thread; each create firesudev_event_attach()(sys/vfs/devfs/devfs_core.c:1428) and each destroy firesudev_event_detach()(devfs_core.c:1451), each queueing an event carrying a full prop_dictionary copy (kern_udev.c:508-512).- Because the only reader never advances its marker,
udev_clean_events_locked()reaps nothing;udev_evqlenand 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). - 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/udevis 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 (
S4sample, 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(stallcountmode),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)
Fix verification
not_testablefix.diff authored from line-accurate root cause (see pack). Kernel rebuild validation not performed: DoS-class finding; the mandatory fix-build cycle applies to reproduced memory-corruption findings. Guest was reset to the clean-source snapshot after evidence collection.
fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
['run.log: baseline propdict 288 in-use -> 72.4K (t+6s) -> 167K (t+14s) -> 226K (t+19s) while reader stalled; udev domain 139 -> 226K event structs', "run.log: 'churn: 120068 pty cycles in 20s' as user maxx", "run.log: 'harness: backlog drained: 240136 events (185144856 bytes)'", 'run.log: post-drain sample returns to baseline (propdict 333) - growth was queued events, not a leak', 'harness.c stallcount mode (initiate via UDEVPROP getdevs, stall 30s, then non-blocking drain-count), churn.c (posix_openpt loop)']
PoC changes
Harness written from scratch for this finding (no seed). Fixed two of my own bugs during bring-up: BSD signal() sets SA_RESTART so alarm could not interrupt the blocking /dev/udev read (switched to sigaction with sa_flags=0), and churn arg parsing accepted only '-n N'. Added stallcount mode (stall then non-blocking drain) to quantify the backlog without triggering the DF-2804 leak in the same run.
Verified recommended fix
Cap the queue: check udev_evqlen >= UDEV_EVQ_MAX under udev_lk in udev_event_insert() and drop (not queue) excess events, waking UDEVWAIT/kqueue consumers so the drop is observable.
Verdict
Reproduced on the stock INVARIANTS guest: one initiated-but-stalled root reader of /dev/udev plus one unprivileged pty-churn process (user maxx, /dev/ptmx 0666) grew the kernel udev event queue to 240,136 live events (~90 MB across propdict/prop_dictionary/propstng/udev malloc domains) in 20 seconds, linearly and without bound (udev_event_insert at sys/kern/kern_udev.c:500-532 increments udev_evqlen with no cap and no backpressure; udev_clean_events_locked reaps only up to the oldest reader marker). Kernel memory exhaustion DoS; reader side gated by /dev/udev 0600 root:wheel (same gate as DF-0055), producer side fully unprivileged.
No comments yet.