DF-2132 / verdict.json
{ "finding_id": "DF-2132", "status": "reproduced", "reproduced": 1, "impact": "corruption", "confidence": "certain", "verdict": "REPRODUCED with production code and fix validated. dma_buf_get() (sys/dev/drm/linux_dma-buf.c:168-187) returns a struct dma_buf owning ZERO f_count references (holdfp borrow + dropfp return), while every drm_prime.c caller (drm_prime.c:802) later calls dma_buf_put() (include/linux/dma-buf.h:110-120, unconditional fdrop) -- each PRIME_FD_TO_HANDLE lookup-hit iteration decrements f_count by one nobody acquired. A KLD rig resolving the REAL shipped /boot/kernel/drm.ko symbols (dma_buf_export/dma_buf_fd/dma_buf_get/dmabuf_fileops; md5 + nm recorded in setup.log) and replaying the exact drm_prime.c:802->814->864 sequence in an unprivileged process context (/dev/df2132 mode 0666, run as nobody) shows: resting f_count drops by exactly 1 per import; after 3 imports the struct file is freed INSIDE dma_buf_put while the fd-table slot still points at it (slot_is_saved=1 measured under fd_spin); close() of the dangling fd does a silent atomic f_count write on freed memory (fclearcache, kern_descrip.c:247) and then panics fdrop: invalid f_count -1 via kern_close->closef->fdrop (both -1 and 0 variants captured). Exploit push demonstrated: the freed M_FILE chunk is reoccupied by a ~960-allocation spray (DF kmalloc_obj is a per-slab FIFO ring, not LIFO -- naive sprays miss, kern_kmalloc.c:680/926), a /dev/null open lands on the exact freed address, read() through the DANGLING dma-buf fd succeeds (functional cross-type alias), and closing the aliased fd silently frees the object again under the dangling fd (repeatable reference steal, silent on any kernel). Full uid=0 weaponization not completed this run: M_FILE is a dedicated MALLOC_DEFINE_OBJ zone (no arbitrary-content reclaim), and the INVARIANTS kernel trips KASSERTs at candidate confusion states by design; the demonstrated primitive set (deterministic premature free + cross-type aliasing + silent steal) is the standard starting point for file-UAF root chains -- remaining route (steal refs of a live file out from a raw-fp holder, reclaim with a different file type, f_ops/f_data confusion -> on this no-SMAP/SMEP/KASLR guest a direct function-pointer hijack) documented in VERDICT.md. On real hardware the unprivileged front door is any DRM render node (/dev/dri/renderD128); single-threaded, no race. FIX VALIDATED: fhold(fp) before dropfp in dma_buf_get (fix.diff); drm.ko rebuilt in-guest from patched /usr/src, rig re-run: resting count unchanged, 8 imports never free, all modes exit 0 (fix_run.log).", "exploit_chain": "unprivileged trigger -> production dma_buf_get returns 0-ref dmabuf -> production dma_buf_put fdrop per import -> f_count 3->2->1->0 -> struct file freed inside dma_buf_put while fd-table slot + thread fd-cache entry dangle -> [demonstrated] same-zone reclaim after ~960 falloc spray -> /dev/null lands on freed address -> read() through dangling fd (functional alias) -> close(aliased fd) = silent reference steal re-arming the dangling state -> close(dangling fd) = UAF write + panic; [assessed, not completed] repeat steal against a subsystem holding raw fp -> reclaim with different file type -> f_ops/f_data confusion -> function-pointer hijack (no SMAP/SMEP/KASLR) -> uid=0", "evidence": "run.log (combined baseline record); run_A.log (resting f_count 3->2 per import, 2 runs); logs/panic_modeB_final.txt + panic_modeB_full.txt ([import] f_count 1->0 *** STRUCT FILE FREED INSIDE dma_buf_put ***, [info] slot_is_saved=1, panic: fdrop: invalid f_count -1 in kern_close); logs/panic_modeA_closeafter2imports.txt (invalid f_count 0 variant); run_c.out + logs/dmesg_mode_c.txt (dummy reoccupies freed chunk ~960 allocs; /dev/null fp == freed fp 0xfffff80116daf700; read(dangling fd)=0; aliased close = silent steal); fix_run.log + fix_build.log (patched: oracle UNCHANGED, NO PREMATURE FREE, rc=0 A/B/C)", "kernel_refs": [ "sys/dev/drm/linux_dma-buf.c:168-187", "sys/dev/drm/linux_dma-buf.c:118-137", "sys/dev/drm/linux_dma-buf.c:139-166", "sys/dev/drm/linux_dma-buf.c:55-68", "sys/dev/drm/include/linux/dma-buf.h:104-120", "sys/dev/drm/drm_prime.c:795-866", "sys/dev/drm/drm_prime.c:802", "sys/dev/drm/drm_prime.c:814", "sys/dev/drm/drm_prime.c:831", "sys/dev/drm/drm_prime.c:848", "sys/dev/drm/drm_prime.c:857", "sys/dev/drm/drm_prime.c:864", "sys/dev/drm/drm_gem.c:271", "sys/kern/kern_descrip.c:2765", "sys/kern/kern_descrip.c:510", "sys/kern/kern_descrip.c:2089-2141", "sys/kern/kern_descrip.c:2186-2199", "sys/kern/kern_descrip.c:185-262", "sys/kern/kern_descrip.c:3107-3175", "sys/kern/kern_kmalloc.c:680", "sys/kern/kern_kmalloc.c:926" ], "poc_changes": "Replaced the pass-1 HW-gated, not reproduced verdict with a production-code KLD rig: (1) rig resolves dma_buf_export/dma_buf_fd/dma_buf_get/dmabuf_fileops from the shipped drm.ko (MODULE_DEPEND) and replays the drm_prime.c lookup-hit composition verbatim (dma_buf_put compiled from the production header); (2) trigger runs as nobody via /dev/df2132 (0666) mirroring a render node; (3) rig reports f_count before/after each import, the fd-table slot state (no deref), the ops->release counter, and kernel addresses; (4) exploitation push added: bare-falloc spray + /dev/null land + functional read through the dangling fd + silent steal (learned en route: DF kmalloc_obj is a per-slab FIFO ring so LIFO sprays miss -- documented); (5) durable console-side logging because guest-side files do not survive panics (UFS softupdates); (6) fix validation by in-guest drm.ko rebuild + rerun of all modes.", "attempts": 9, "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": 3600, "guest_dirty": 0, "build_cmd": "in-guest: cd /root/df2132 && sh ./build.sh (kld rig via kmod.mk + cc -O -o df2132_user df2132_user.c; setup.log records production md5s + nm of drm.ko)", "run_cmd": "in-guest root: kldload drm && kldload ./df2132.ko; unprivileged: su -m nobody -c /tmp/df2132_user A|B|c|C (A oracle no-crash, B premature-free + panic, c alias/steal park, C full panic)", "code_hash": "c97b296e065ad7e6cf11264854bd42f2c909a1c5f38ae20ae7ee09ab8a9dcaa9", "notes": "Guest reset with-src performed after validation (clean source md5 383b32f272ab0620dc703707223f6d9a re-verified, no modules loaded, no /dev/df2132). Two distinct panic signatures captured; production falloc-ref leak in dma_buf_export/dma_buf_fd (linux_dma-buf.c:123/163, never fdropd) absorbs exactly one over-drop -- worth a separate low-severity note upstream. usched_set pinning failed for nobody (rc=-1); reclaim worked regardless thanks to the FIFO-ring drain spray.", "recommended_fix": "In dma_buf_get(), take a caller-owned reference before returning the borrow: fhold(fp) immediately after extracting dmabuf->file and before dropfp() (mirrors Linux fget semantics; validated on a rebuilt drm.ko).", "fix_status": "fixed", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Single-fix kernel (patched drm.ko only, same base kernel): baseline shows resting f_count -1 per import, premature free at import #3 with slot_is_saved=1, and panic: fdrop: invalid f_count -1 on dangling close; patched shows resting count unchanged, 8 imports never free, modes A/B/C all exit rc=0. Bad behavior GONE with no regressions in the rig matrix.", "fix_evidence": "fix_run.log lines: ORACLE: resting count UNCHANGED (3) => get/put balanced: FIXED, NO PREMATURE FREE after 8 imports (f_count never reached 0) => FIXED kernel, rcA=0 rcB=0 rcC=0; contrast logs/panic_modeB_final.txt f_count 1->0 *** FREED *** + panic. fix_build.log: patched drm.ko linked (780104 bytes, md5 8cd358acbf4aeb34638f2e0008e3aab3 vs baseline f6bc054cefba22aafcbd30824438b742).", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 (X86_64_GENERIC baseline kernel + per-fix-DF-2132 patched drm.ko module)" } |