⬢ DragonFlyBSD Kernel Audit
DF-5059 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-5059",
  "status": "reproduced",
  "reproduced": true,
  "impact": "heap-overflow-write + UAF-read (kernel-space primitive reproduced with the verbatim drm_sysctl.c:260-301 walk/copy body, real M_DRM allocator and real struct drm_file[544B]: opens landing between the count walk (:271) and copy walk (:281) made the copy write 544B-elem structs past the exact-size kmalloc; 138 organic hits per 100k unprivileged sysctl reads with no forced interleave; a groomed exact-size allocation clobbered an adjacent kernel heap object (guard[9]) - real cross-object corruption; end-to-end hw.dri path hardware-gated: QEMU guest has no GPU/device, so no real drm clients exist and a uid0 chain on the live path was not exercisable)",
  "confidence": "certain",
  "verdict": "REPRODUCED at the primitive level in kernel space, with the unprivileged trigger demonstrated. drm_clients_info() walks dev->filelist under DRM_LOCK(dev)=dev->struct_mutex (drm_sysctl.c:268, drmP.h:114) but the list is protected by dev->filelist_mutex (drm_device.h:88, drm_file.h:243) and its only mutators - drm_open (drm_file.c:514-516 list_add) and drm_close (drm_file.c:380-382 list_del, then drm_file_free->kfree at :267 with no RCU) - take filelist_mutex exclusively, so the handlers two walks run unserialized against open/close. The copy walk is bounded by the list, not by privcount, so growth between the walks overflows kmalloc(sizeof(struct drm_file)*privcount) (drm_sysctl.c:274) with pointer-rich drm_file copies; a close mid-walk yields a UAF read that is copied out and partially printed. On the stock-kernel guest (no DRM device possible), a kld harness ran the verbatim handler body against the real allocators: deterministic interleave -> copies=6 vs counted=4, +2x544B past the array containing pid/magic and a kernel heap pointer; uid=1001 sysctl reads fired it on every read and organically 138/100k times vs a free-running open/close churn; a groomed exact-size 2176B allocation clobbered the adjacent M_DRM object (guard[9] marker overwritten). The identical interleave under the fix pattern (filelist_mutex held across both walks) stayed in bounds with the mutator blocked, and the patched drm_sysctl.c rebuilt into drm.ko with -Werror rc=0 and loaded. Full uid0 escalation is the expected class for this primitive on any machine with a DRM device (world-readable sysctl + video-group opens) but was not demonstrable on the GPU-less guest.",
  "exploit_chain": "unpriv local user (video group) opens/closes /dev/dri/cardN in a loop (drm_file.c:514/380 mutate filelist under filelist_mutex) while reading hw.dri.N.clients (CTLFLAG_RD, world-readable, drm_sysctl.c:90-99) -> count walk (:271) sizes tempprivs=kmalloc(544*privcount) (:274, M_WAITOK window stretchable under memory pressure) -> open lands between walks -> copy walk (:281, list-bounded) writes 544B drm_file structs past the buffer into the adjacent M_DRM object (steerable size class via privcount; contents include kernel pointers, attackers pid/magic) -> slab-groomed victim object -> corruption; close-side variant: list_del+kfree between walks -> list_for_each_entry derefs freed drm_file -> UAF read printed as pid/magic. uid0 chain not exercised end-to-end: guest has no DRM hardware.",
  "evidence": "run.log: S1 OOB WRITE copies=6 counted=4 (+2 elems x 544B) ... pid=40002 magic=0x6d610002 ... w3=fffff801179e7ed8; S3a clients pass: copies=6 counted=4 <-- OOB WRITE (uid=1001, every read); S3b rounds_oob=138 oob_max_elems=4 + leaked module pointer ffffffff831ded20; S2 GUARD DAMAGE ... guard[9] word 0: expected a5a5000900000000 got 0000000000000000 (1 damaged guard, groomed exact-size 2176B alloc); S4 fixmode: copies=4 == counted=4 (churn blocked=1) -- IN BOUNDS; fix_build.log (patched drm_sysctl.c -> drm.ko, -Werror, MAKE_RC=0, 0 errors) + PATCHED_DRM_LOADED; dmesg.txt (139 OOB WRITE lines); status.txt; env.txt",
  "kernel_refs": [
    "sys/dev/drm/drm_sysctl.c:268",
    "sys/dev/drm/drm_sysctl.c:271-272",
    "sys/dev/drm/drm_sysctl.c:274-275",
    "sys/dev/drm/drm_sysctl.c:281-282",
    "sys/dev/drm/include/drm/drmP.h:114-115",
    "sys/dev/drm/include/drm/drm_device.h:88-89",
    "sys/dev/drm/include/drm/drm_file.h:243",
    "sys/dev/drm/drm_file.c:514-516",
    "sys/dev/drm/drm_file.c:380-382",
    "sys/dev/drm/drm_file.c:614",
    "sys/dev/drm/drm_file.c:221-267",
    "sys/dev/drm/drm_drv.c:1178-1179"
  ],
  "poc_changes": "No seed PoC existed (finding new in pass 2). The harness embeds the verbatim handler body; marked [ADAPT]ations: fake dev struct carrying only filelist + the two real locks; DRM_SYSCTL_PRINT compressed to kprintf; observation shadow behind the exact-size kmalloc (loop bounds untouched); a sync window modeling opens landing in the M_WAITOK sleep (with a documented inline fallback using the exact drm_file.c:514-516 critical section when the churn thread misses the 1s handshake); guard spray/punch for the groomed run. Harness-side iterations that were MY bugs, not the targets: initial teardown NULL-deref (uninitialized list, panicked guest), unbounded churn loop livelock (redesigned to bounded 6ms bursts + 10s deadline), stale-module/missing-forward-decl build failure, cur_len desync after organic runs (added resync_len).",
  "attempts": 6,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul  2 06:02:54 UTC 2026     root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC  x86_64 (stock kernel; drm.ko + clirace.ko kldloaded)",
  "runtime_sec": 210,
  "guest_dirty": 1,
  "build_cmd": "guest# cd /root/clirace && make   (bsd.kmod.mk, -Werror); guest# cc -O2 -Wall -o race_clients race_clients.c",
  "run_cmd": "guest# kldload /root/clirace/clirace.ko; sysctl hw.clirace.mode=1 hw.clirace.n0=4 hw.clirace.m=2 hw.clirace.shadow=32 hw.clirace.groom=0; sysctl hw.clirace.go=1   (S1) | fixmode=1 (S4) | run_user: sysctl -n hw.clirace.clients (S3a) / ./race_clients 100000 (S3b) | mode=1 shadow=0 groom=1; sysctl go=1 (S2); dmesg | grep CLIRACE",
  "code_hash": "05e8062206ec0fa661696c8d72e3f3738bda9189cffadb6ef3df96761c5a98c7",
  "notes": "Sibling handlers re-checked: drm_vm_infos maplist walk IS serialized (all maplist mutators take struct_mutex - drm_bufs.c:347,464,561,574,611); only the known DF-2075 unlock-leak applies there; drm_bufs_info NULL-write is DF-2076 (known). Device perms: /dev/dri/cardN root:video 0660 (drmP.h DRM_DEV_MODE/GID_VIDEO; drm_drv.c:1178) - trigger side needs no privileges (world-readable sysctl), mutator side needs video-group membership. Organic hit-rate variance across boots: 7/64/138 per 100k reads. sizeof(struct drm_file)=544. Guest must be reverted (S2 corrupted a guard by design; kldunload refused).",
  "recommended_fix": "drm_sysctl.c drm_clients_info: hold mutex_lock(&dev->filelist_mutex) across BOTH walks (count + kmalloc + copy) instead of DRM_LOCK(dev) - see fix.diff",
  "fix_status": "fixed",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_kernel_uname": "same stock X86_64_GENERIC guest; behavioral fix-pattern proof via harness S4 (real lockmgr locks) + patched drm_sysctl.c rebuilt into drm.ko on the guests /usr/src copy",
  "fix_verdict": "Fixed. Behavioral: the identical interleave with the fix locking (mutex_lock(&dev->filelist_mutex) across both walks, real lockmgr locks) stays in bounds with the mutator legitimately blocked (S4: copies=4 == counted=4 (churn blocked=1) -- IN BOUNDS) while the same interleave without the lock overflows deterministically (S1/S3a), organically (S3b: 138/100k) and corrupts a neighbor (S2). Compile: fix.diff applied to the guests /usr/src, drm.ko rebuilt under -Werror rc=0 with 0 errors/warnings and the patched module loads (PATCHED_DRM_LOADED). The literal hw.dri.N.clients OID cannot exist on the GPU-less guest, so the patched sysctl path itself was exercised via the harnesss identical code shape, not the production OID.",
  "fix_evidence": "run.log S4 line; fix_build.log (MAKE_RC=0, 0 error: lines, -c /usr/src/sys/dev/drm/drm/../drm_sysctl.c recompiled, drm.ko relinked 780208 bytes); PATCHED_DRM_LOADED in the run transcript; fix.diff (lock swap at drm_sysctl.c:268-291)"
}