β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2803

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2803 Β· 12 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-2803

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.