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

udev_getdevs_ioctl() leaks the cdevs prop_array β€” and permanently pins every cdev's si_dict β€” on each UDEVPROP getdevs ioctl (devd triggers it on default systems)

Summary

prop_dictionary_set retains its argument but udev_getdevs never releases its own cdevs reference: every getdevs leaks one array of all cdev-dict pointers AND takes an unboundedly-accumulating reference on each cdev's si_dict (device dicts can never be freed even after destruction). /dev/udev is 0600 but devd(8) issues the same ioctl during normal default-config operation (idle drift observed with no PoC running). Measured: 500 ioctls -> +625KB live (1.3KB/call), linear, unreclaimable; flat after the one-line release fix on the rebuilt kernel. Filed on the prop_kern.c reopen as the copyout-consumer counterpart (prop_kern.c:585 retain contract).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5686 Β· 11 files
FileTypeDescriptionSize
README.md β€” 1.5 KB ↓ raw
VERDICT.md β€” 1.7 KB ↓ raw
build.log β€” 127 B view raw
build.sh β€” 102 B view raw
env.txt β€” 225 B view raw
fix.diff β€” 222 B view raw
run.log β€” 679 B view raw
run.patched.log β€” 329 B view raw
run.sh β€” 150 B view raw
udev_getdevs_leak.c β€” 1.9 KB view raw
verdict.json β€” 2.5 KB view raw

DF-5686 β€” udev_getdevs_ioctl() leaks the cdevs prop_array (and pins every device dict) per UDEVPROP ioctl

What

udev_getdevs_ioctl() (sys/kern/kern_udev.c:961-1015), the consumer of prop_dictionary_copyout_ioctl() from sys/libprop/prop_kern.c:

kern_udev.c:985 ctx.cdevs = prop_array_create(); / refcnt 1 / kern_udev.c:992 devfs_scan_callback(..., &ctx); / fills array / kern_udev.c:1004 prop_dictionary_set(odict, "array", ctx.cdevs); / retain -> 2 / kern_udev.c:1013 prop_object_release(odict); / -> 1 /

prop_dictionary_set() does NOT transfer ownership β€” the caller's own reference is never released, so every UDEVPROP "getdevs" ioctl leaks the whole cdevs array plus one reference on every cdev's si_dict (device dicts can then never be freed, even on device destruction).

Reachability: /dev/udev is 0600 root:wheel (kern_udev.c:1039-1041), so the direct trigger is privileged β€” but devd(8) performs the same getdevs ioctl during normal operation on default systems (observed: proparay/prop_array counts creeping up from devd during an otherwise idle session).

Reproduce

$ cc -O2 -o udev_getdevs_leak udev_getdevs_leak.c $ vmstat -m | grep -i prop > b $ ./udev_getdevs_leak /dev/udev 500 $ vmstat -m | grep -i prop > a # proparay/prop_array InUse grew by ~490

Expected (vulnerable): proparay +490, prop_array +490 InUse / +625KB per 500 ioctls.

Expected (patched, fix.diff): prop_array InUse stays 0, proparay flat.

VERDICT.md
↓ download raw

DF-5686 VERDICT

Status: REPRODUCED (kernel heap leak per UDEVPROP getdevs ioctl; root-gated direct trigger, devd-triggered on default systems) β€” FIXED by fix.diff (validated on rebuilt kernel)

Reproduction (baseline kernel #0, as root β€” /dev/udev is 0600)

500 Γ— ioctl(/dev/udev, UDEVPROP, {"command":"getdevs"}):

before: proparay 1.97K InUse / 378K prop_array 1.96K InUse / 251K after: proparay 2.46K InUse / 472K prop_array 2.45K InUse / 876K

Delta for 500 ioctls: +490 proparay, +490 prop_array InUse, +625 KB β€” one leaked array-of-all-cdevs (~1.3 KB) per call, plus one permanent reference on every cdev's si_dict per call (unobservable in vmstat but strictly implied by the missing release: refcounts grow without bound, so device dicts outlive device destruction).

Idle-session observation (no PoC running): proparay/prop_array InUse crept 1β†’9 over ~10 minutes β€” devd(8)'s own getdevs polling leaking on a default install.

Fix validation (patched kernel #1, prop_object_release(ctx.cdevs) added at kern_udev.c:1013)

500 ioctls as root: before: proparay 2 / 384B prop_array 0 InUse after: proparay 2 / 384B prop_array 0 InUse FLAT β€” leak eliminated.

kernel_refs

sys/kern/kern_udev.c:985, 992, 1004-1008, 1011-1014 sys/libprop/prop_kern.c:585-590 (prop_dictionary_copyout_ioctl consumer contract)

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

Patched kernel #1 with prop_object_release(ctx.cdevs): 500 getdevs ioctls leave prop_array InUse at 0 and proparay flat. Leak eliminated, getdevs output path unchanged.

run.patched.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Oct 10 00:36:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (proparay 1.97K->2.46K InUse, prop_array +625KB per 500 ioctls), run.patched.log (flat after fix)

PoC changes

Hand-crafted proplib XML (commandgetdevs) since guest lacks userland proplib headers; plistref + UDEVPROP _IOWR defined locally; munmap of the kernel copyout region added per _prop_object_copyout contract.

Verified recommended fix

Add prop_object_release(ctx.cdevs) after prop_dictionary_copyout_ioctl

Verdict

REPRODUCED: udev_getdevs_ioctl() never releases ctx.cdevs after prop_dictionary_set(odict, "array", ctx.cdevs) (set retains; the caller's own reference leaks). 500 UDEVPROP getdevs ioctls as root -> +490 prop_array/proparay InUse, +625KB live kernel heap (~1.3KB per ioctl), plus one permanent reference on every cdev si_dict per call (pins device dicts past device destruction). Direct trigger is root-gated (/dev/udev 0600) but devd(8) performs the same ioctl on default systems (idle drift proparay 1->9 observed over ~10min). fix.diff (one-line prop_object_release(ctx.cdevs)) validated on rebuilt kernel: 500 ioctls -> counters flat.