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)
PoC verification
Evidence pack
findings/poc/DF-5686 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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.
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
fixedPatched 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
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 (
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.
No comments yet.