DF-4531 / verdict.json
{ "finding_id": "DF-4531", "status": "inconclusive", "reproduced": 0, "impact": "none", "confidence": "certain", "verdict": "REAL BUG, runtime-gated on absent hardware. drm_clients_info() (the world-readable hw.dri.N.clients sysctl, drm_sysctl.c:260-301) walks dev->filelist twice under dev->struct_mutex (DRM_LOCK, drmP.h:114): a count pass (271-272), a SLEEPING kmalloc(M_WAITOK) sized by that stale count (274-275), then an unbounded copy pass (281-282). Every list mutation runs under dev->filelist_mutex instead: drm_open_helper list_add (drm_file.c:514-516), drm_close list_del (drm_file.c:380-382), and the kfree(file_priv) (drm_file.c:430) under neither. A local user in the video group (card nodes are 0660 root:video, drmP.h:108-110) racing open()/close() against the sysctl read gets: growth-between-passes -> (growth x sizeof(struct drm_file)) heap OOB WRITE past an attacker-sized kmalloc with partially controlled content (pid/magic/bitfields plus kernel heap pointers), or shrink-between-passes -> use-after-free read of freed drm_file nodes. Algorithmic half proven deterministically on the guest 3/3 runs (logicproof.c: OOB WRITES DETECTED every run, 11.9-29.0MB modelled overflow); the kernel race itself could not run because the QEMU guest has no DRM device (/dev/dri absent, sysctl hw.dri ENOENT, vgapci0 with no drm driver attached; the tree has no virtio-gpu). End-to-end uid=0 chain (slab groom -> victim overwrite) was not developable for the same hardware reason; on the standard no-SMAP/SMEP/KASLR target this primitive is the expected starting point.", "exploit_chain": "", "evidence": "run.log (logicproof 3/3 OOB WRITES DETECTED + honest hw-gated trigger output); VERDICT.md (full path:line lock analysis); fix_build.log (patched drm_sysctl.c compiles RC=0 with exact kernel CFLAGS on guest); fix.diff (applies clean, verified via patch --dry-run on scratch copy)", "kernel_refs": [ "sys/dev/drm/drm_sysctl.c:268", "sys/dev/drm/drm_sysctl.c:271", "sys/dev/drm/drm_sysctl.c:274", "sys/dev/drm/drm_sysctl.c:281", "sys/dev/drm/drm_file.c:514", "sys/dev/drm/drm_file.c:380", "sys/dev/drm/drm_file.c:430", "sys/dev/drm/include/drm/drmP.h:114", "sys/dev/drm/include/linux/mutex.h:38", "sys/dev/drm/drm_sysctl.c:94" ], "poc_changes": "Wrote from scratch (no seed): trigger.c (sysctl-reader + open/close-grower threads using DFly sysctl(3)); logicproof.c (deterministic userland reproduction of the exact count->sleeping-alloc->unbounded-copy control flow with an OOB probe region).", "attempts": 3, "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", "runtime_sec": 240, "guest_dirty": 0, "build_cmd": "cc -Wall -Wextra -O2 -pthread -o trigger trigger.c && cc -Wall -Wextra -O2 -pthread -o logicproof logicproof.c", "run_cmd": "./logicproof; ./trigger 5", "code_hash": "badb6f867b5c8707f1fe2aed72c8e3e72e4929344088c332336ed95805835cb7", "notes": "Found during the pass-2 drm_file.c audit (the orchestrators list walks under the fd shims hunt): drm_getclient was hollowed out in this tree, so the only live dev->filelist walk is this sysctl handler, and it walks under the WRONG lock. The 2.9GB of modelled OOB writes in logicproof reflects unbounded userland growth in the harness window; in-kernel growth is bounded by RLIMIT_NOFILE/kern.maxfilesperproc but each overflow chunk is still a full drm_file struct. Known findings DF-2075/DF-2076 (drm_sysctl.c pass 1) cover different bugs (struct_mutex leak on SYSCTL_OUT failure; M_NULLOK NULL-deref).", "recommended_fix": "Serialize both walks of dev->filelist under dev->filelist_mutex (the lock every mutator already uses) and never let the copy pass exceed the count: see fix.diff.", "fix_status": "not_testable", "fix_baseline_reproduced": 0, "fix_patched_reproduced": 0, "fix_verdict": "Patch compiles clean (RC=0) as a translation unit against the pristine guest kernel tree with the exact X86_64_GENERIC CFLAGS (fix_build.log); git-apply verified on a scratch copy. Runtime before/after comparison impossible on this guest (no DRM device), so fix_status=not_testable; the fix makes both the OOB write and the UAF read unreachable by construction (single lock domain + hard bound).", "fix_evidence": "findings/poc/DF-4531/fix.diff; findings/poc/DF-4531/fix_build.log" } |