DF-2938 / verdict.json
{ "finding_id": "DF-2938", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "Reproduced on the stock INVARIANTS guest with an unprivileged (uid=65534) trigger: an OBJT_DEVICE mapping of a device that is later destroy_dev()'d leaves the pager object holding a dangling cdev pointer. The next fault on the mapping calls dev->si_ops->d_mmap through the FREED cdev (demonstrated: last_a_dev == old_chunk, the stale mapping was served a page belonging to the replacement device object that took over the chunk -- cross-object type confusion), and the unprivileged munmap drove old_dev_pager_dtor into the freed-and-reused chunk producing panic: assertion \"dev->si_object\" failed in old_dev_pager_dtor at device_pager.c:343. Unprivileged real-world vectors: /dev/dsp*.* 0666 clones (old pager via dsp_mmap_single->dev_pager_alloc, destroyed by the clone GC on last close) and uvc /dev/video* 0666 on unplug; all other old-pager devices via privileged teardown with unprivileged fault/munmap. fix.diff (reference_dev at object creation + release_dev in old_dev_pager_dtor) was built as kernel #1 and validated: 6/6 cpus reused=0, clean munmaps, no panic.", "exploit_chain": "unpriv: open+mmap old-pager device (2 pages) -> fault p0 -> root-side teardown (kldunload / clone GC / USB unplug; PoC: module destroy_dev) -> cdev freed (sysref -> objcache) -> unpriv fault p1: vm_fault -> dev_pager_getpage -> old_dev_pager_fault -> dev_dmmap(freed dev) -> dev->si_ops->d_mmap indirect call through freed heap; unpriv munmap: vm_map_entry_dispose -> vm_object_deallocate -> dev_pager_dealloc -> old_dev_pager_dtor -> KKASSERT read + si_object=NULL write through freed chunk (panic when chunk reused). uid0 continuation (not completed): reclaim chunk with controlled bytes (objcache->kmalloc drain under memory pressure + same-size user-byte spray), point stale si_ops at user-mapped fake dev_ops (guest has no SMAP), fault-time d_mmap call executes ring-0 payload; no KASLR/SMEP on this guest.", "evidence": [ "run.log: STATUS old_chunk=0xfffff80116a0ef80 last_a_dev=0xfffff80116a0ef80 VERDICT: d_mmap last served by THE FREED CDEV; page0=A page1=B uid=65534", "run.baseline-cpu1.log + panic.txt: devuaf: SWAP old=0xfffff8004f2a7c80 new=0xfffff8004f2a7c80 reused=1 (cpu 1); panic: assertion \"dev->si_object\" failed in old_dev_pager_dtor at /usr/src/sys/vm/device_pager.c:343; backtrace old_dev_pager_dtor <- dev_pager_dealloc <- vm_object_terminate <- vm_object_deallocate <- vm_map_entry_dispose", "fixed.[0-5].log: patched kernel #1, all cpus reused=0, trig_exit=0, guest up", "VERDICT.md: full narrative + fix validation", "fix.diff: reference_dev/release_dev pair" ], "kernel_refs": [ "sys/vm/device_pager.c:124", "sys/vm/device_pager.c:138-141", "sys/vm/device_pager.c:146-149", "sys/vm/device_pager.c:211", "sys/vm/device_pager.c:312-335", "sys/vm/device_pager.c:337-346", "sys/vm/device_pager.c:348-361", "sys/kern/kern_device.c:262-283", "sys/vm/vm_mmap.c:1363-1381", "sys/vm/vm_mmap.c:1478", "sys/dev/sound/pcm/dsp.c:2357", "sys/dev/sound/pcm/dsp.c:2505-2508", "sys/dev/sound/clone.c:420-434", "sys/bus/u4b/uvc/uvc_v4l2.c:763", "sys/bus/u4b/uvc/uvc_v4l2.c:783-784" ], "poc_changes": "Rewrote the seed concept into a loadable-module PoC: world-mappable d_mmap device + root-side destroy_dev ioctl standing in for driver teardown/dsp clone GC/USB unplug, unprivileged trig.c doing mmap/fault/munmap. First iteration used lwkt_migratecpu() inside the ioctl which independently panicked the dfly scheduler (usched_dfly.c:453) -- replaced with usched_set(USCHED_SET_CPU) user-side binding. Removed sys/ioctl.h include (kernel uses sys/ioccom.h), added sys/devfs.h for devfs_config().", "attempts": 4, "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 (baseline) / #1: Thu Sep 3 18:35:46 UTC 2026 (fix build)", "runtime_sec": 2100, "guest_dirty": 0, "build_cmd": "sh setup.sh (in-guest: make KMOD devuaf + cc trig.c ctl.c)", "run_cmd": "sh /tmp/df2938/orch.sh <cpu>", "code_hash": "c3de1df78ce9f0b6363e4d5444e86aa4cb888c7d86759c9bf66b36c1b6ca1aa9", "notes": "Fixed-kernel verification: fix.diff applied to guest /usr/src, make -j6 nativekernel + installkernel (INSTALL_RC=0), reboot into #1, PoC re-run on all 6 cpus: reused=0 everywhere, no panic, clean munmaps. Baseline panic captured from serial console (panic.txt). Guest reset with-src afterwards (guest_dirty=0 at return). DF-2939 (same-file si_object ordering race) is a separate root cause not fixed by this diff.", "recommended_fix": "Take reference_dev(dev) when cdev_pager_allocate creates the OBJT_DEVICE object for old_dev_pager_ops and release_dev(dev) in old_dev_pager_dtor, so the pager object pins the cdev for the lifetime of the mapping", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Thu Sep 3 18:35:46 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff (reference_dev at device_pager.c:156 / release_dev at :359) applied in-guest, kernel rebuilt and booted; the exact baseline PoC no longer frees the cdev while the mapping exists: 6/6 cpu rounds reused=0 (chunk cannot be re-used), post-teardown faults served through the pinned (valid) cdev, all unprivileged munmaps complete cleanly, no panic. Baseline on stock #0 had reused=1 + panic at device_pager.c:343.", "fix_evidence": "fixed.0.log fixed.1.log fixed.2.log fixed.3.log fixed.4.log fixed.5.log (patched kernel runs); run.log + panic.txt (baseline); fix.diff" } |