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

idr_remove()/idr_get_node() conflate per-leaf state with the cumulative Fenwick counter: phantom decrement yields duplicate-id issuance with silent kernel-object pointer overwrite and a deterministic KKASSERT panic

Summary

One int field serves both leaf-state and subtree-counter duty, so every consumer-guard asking 'is THIS id allocated' asks 'is anything under this node allocated': removing a never-allocated id with non-zero coverage performs a phantom decrement, corrupting the counters so idr_find_free returns already-allocated leaves - idr_alloc hands out a DUPLICATE id and silently overwrites nodes[id].data (two user handles alias one kernel object, lost references never dropped), or the undercount drives a KKASSERT panic. Unpriv reachability: DRM_IOCTL_SYNCOBJ_DESTROY feeds the raw u32 handle straight into idr_remove interleaved with SYNCOBJ_CREATE. Proven in-kernel: deterministic 5-op KKASSERT panic (backtrace captured) and a KLD demonstrating live duplicate issuance ('KERNEL MEMORY CORRUPTION (duplicate id) CONFIRMED'); 40M+ randomized differential ops on the rig, dup/replace-writes-free-leaf on every seed. uid0 chain sketched via duplicated GEM/syncobj handles -> drm-layer UAF; not driven only because the guest has no /dev/dri. per-leaf used-bit fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5688 Β· 16 files
FileTypeDescriptionSize
df5688_rig.c β€” 19.9 KB view raw
df5688_minimal.h β€” 4.1 KB view raw
df5688_mod.c β€” 3.2 KB view raw
df5688_panic_mod.c β€” 2.3 KB view raw
Makefile β€” 122 B ↓ download
Makefile.panic β€” 164 B ↓ download
shim/ β€” 4.0 KB ↓ download
build.sh β€” 190 B view raw
run.sh β€” 230 B view raw
run.battery.stock.log β€” 2.5 KB view raw
run.battery.fixed.log β€” 9.7 KB view raw
panic.txt β€” 1.1 KB view raw
inkernel-dup-dmesg.txt β€” 2.1 KB view raw
fix-kernel-module.log β€” 1.1 KB view raw
fix.diff β€” 2.7 KB view raw
env.txt β€” 1.5 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff (per-leaf used bit) validated three ways: (a) same-source userland rig β€” minimal_panic/minimal_dup/minimal all clean and 20 fuzz runs x 60k ops with zero divergence (baseline diverged on every seed); (b) rebuilt fixed kernel (nativekernel+installkernel+reboot) β€” df5688.ko loads and the duplicate-id sequence is a clean no-op (alloc(525,0)->527, find(525) still object B); (c) df5688p.ko, which panicked the stock kernel, runs harmlessly on the fixed kernel (guest stays up).

run.battery.fixed.log; fix-kernel-module.log; panic.txt (baseline kernel crash)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sat Oct 10 01:43:01 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC (fixed kernel)

Confirmed kernel references

Detail

Exploit chain

unprivileged /dev/dri client -> DRM_IOCTL_SYNCOBJ_CREATE (idr_alloc start=1) + SYNCOBJ_DESTROY with bogus handles (any id on a live handle's Fenwick chain, ~50% of ids) -> phantom counter decrement -> idr_find_free() returns a live id -> next SYNCOBJ_CREATE receives an in-use handle -> nodes[id].data silently rebinds to the new syncobj: the first syncobj's reference is lost (permanent kernel leak) and two handles alias one id (object-lifetime confusion on every idr consumer); alternatively the corruption drives a counter negative -> KKASSERT -> deterministic kernel panic (5 ops, no race).

Evidence (decisive lines)

panic.txt (kernel panic backtrace idr_reserve->idr_remove at linux_idr.c:197); inkernel-dup-dmesg.txt (in-kernel duplicate id 525 + object aliasing); run.battery.stock.log (minimal_panic abort, minimal_dup '>>> DUPLICATE ID 525', fuzz divergences); run.battery.fixed.log (all clean, 20x60k fuzz ops); fix-kernel-module.log (fixed kernel: phantom no-op, alloc(525,0)->527, panic module survives); df5688_minimal.h (the 4-op and 8-op minimized sequences)

PoC changes

authored the whole rig (the finding's seed sketch had no runnable code): shim headers compiling the kernel branch of linux_idr.c in userland, randomized differential harness with reference model + recomputed-true-counter check, ddmin shrinker (276->8 and 363->4 ops), soft/hard builds, and two KLDs calling the kernel's own idr_* symbols

Verified recommended fix

add a per-leaf 'used' bit to struct idr_node and make idr_remove()/idr_get_node()/idr_find()/idr_for_each() test leaf state instead of the cumulative 'allocated' counter; set it in idr_get_new_above()/idr_alloc(), clear it in idr_remove()

Verdict

REPRODUCED with certainty on the unmodified deployed source (md5 2d66ecf755e254d38a5c9e9e63992eaa) and on the live X86_64_GENERIC kernel. idr_remove()/idr_get_node() test the cumulative Fenwick counter (nodes[].allocated) as if it were per-leaf state, so idr_remove() of a never-allocated-but-covered id performs a phantom idr_reserve(id,-1) and returns NULL β€” the exact DRM_IOCTL_SYNCOBJ_DESTROY shape (raw u32 handle into idr_remove, drm_syncobj.c:369). Demonstrated: (1) deterministic 5-op kernel PANIC via the KKASSERT at linux_idr.c:197 (in-kernel backtrace in panic.txt); (2) deterministic 4-op DUPLICATE-ID issuance with silent overwrite of the live kernel object pointer, confirmed in-kernel by KLD (inkernel-dup-dmesg.txt: 'KERNEL MEMORY CORRUPTION (duplicate id) CONFIRMED', handle 525 mapped to two different objects, first pointer leaked); (3) 8-op sequence leaving a live object permanently unfindable (leak); (4) randomized differential fuzz diverging on every seed (counter corruption, duplicate ids, replace-writes-free-leaf). The write primitive itself is bounded to the idr's own counter fields; the security damage is handed-out-id aliasing at the drm layer (two user handles -> same id with different kernel objects; lost references never dropped). uid=0 was not completed: the audit guest has no GPU (/dev/dri absent), so the drm-side object-confusion cannot be driven there; the same-source rig + KLD are the reserved evidence classes and establish the kernel-side primitive.