DF-2534 / verdict.json
{ "finding_id": "DF-2534", "status": "reproduced", "reproduced": 1, "impact": "leak", "confidence": "certain", "verdict": "Both cited missing-lifetime-lock defects confirmed against the real functions (verbatim sha256-gated xdisk.c compiled into the guest kernel). (1) xa_size:833-835 NULL-checks si_drv1 then dereferences sc->info.d_media_blocks with no lock while xa_terminate_check NULLs si_drv1 (:665) and kfree(sc)s (:680) under xdisk_lk. Hammer race with the real xa_size on cpu0 and the real xa_terminate_check on cpu1: 157,024,256 reader iterations vs 51,550,501 frees (100 percent same-address slab reuse), 664 UAF READS captured, each proven by observing 0xC0FFEE00DF2534, a value planted only AFTER the kfree plus reallocation, so the deref provably landed on freed heap through the stale pointer. Production window quantified: diskpsize re-dispatches psize to the raw device (DSO_RAWPSIZE set by xdisk at :491; subr_disk.c:1273) and dev_doperate has no drain, so psize genuinely races teardown; primitive is an attacker-groomable 8-byte fixed-offset kernel heap disclosure returned as the device size (info leak, Low ceiling, no write/control-flow primitive, no uid=0 chain). (2) xa_strategy:801/805 has no NULL check at all: with si_drv1 NULL it write-faults at 0x90 in devstat_start_transaction (Fatal trap 12, supervisor write) - demonstrated with fabricated state and honestly labeled post-drain-only in production because disk_destroy at :663 drains strategy before si_drv1 is NULLed at :665. Fix takes xdisk_lk and re-checks si_drv1 under it in both functions; A/B validated: 0 UAF reads over 82M iterations on the patched kernel and clean ENXIO biodone from strategy.", "exploit_chain": "Root creates an xa device via dmsg; any holder of the /dev/xa%d node can race size queries (dssize/DIOCGPART paths reach xa_size via diskpsize raw re-dispatch) against span-loss teardown. The reader captures sc from si_drv1 before xa_terminate_check NULLs it, then dereferences after the free: it reads whatever now occupies the freed M_XDISK slab slot (8 bytes at offsetof d_media_blocks) and that value is returned to userspace as the device size. Grooming the freed slot with a chosen kernel object converts this into a repeatable fixed-offset heap info leak. Read-only primitive; escalation ceiling is information disclosure, not code execution.", "evidence": "run.log (DF2534-RACE summary: 157024256 iterations, 664 UAF READS CAPTURED; DF2534-S Fatal trap 12 at 0x90 in devstat_start_transaction+0xa), panic.txt (full trap dump), run.patched.log (0 UAF reads; xa_strategy returned 0 with biodone b_error=6 b_flags=0x800), fix_build.log, env.txt", "kernel_refs": [ "sys/dev/disk/xdisk/xdisk.c:801", "sys/dev/disk/xdisk/xdisk.c:805", "sys/dev/disk/xdisk/xdisk.c:833", "sys/dev/disk/xdisk/xdisk.c:835", "sys/dev/disk/xdisk/xdisk.c:665", "sys/dev/disk/xdisk/xdisk.c:680", "sys/kern/subr_disk.c:1273", "sys/dev/disk/xdisk/xdisk.c:491" ], "poc_changes": "Authored the race harness fresh (no seed): magic-value method (plant 0xC0FFEE00DF2534 into the freed slot after same-address reallocation; si_drv1 never points at the reused block, so observation cannot false-positive). Two bring-up fixes: lwkt_yield() did not prevent CPU starvation under the cooperative LWKT scheduler (spinning kthread never gives the cpu back to user threads) - replaced with a 1-tick tsleep every 256K iterations; first frozen run still captured 3 UAF reads before the wedge (serial_console.baseline.log), final run completed with the full summary and the guest healthy.", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Mon Oct 19 20:55:04 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 5400, "guest_dirty": 0, "build_cmd": "cd findings/poc/DF-2533 && ./build.sh (shared harness; sha256 gate 58b0ed1672b5fb63f7d484a7fae6ba04484372c962e13d9c573b69790cfa55c9; kernel-compiled)", "run_cmd": "dfbsd-qemu/vm.sh run_root sysctl -w debug.df2534_race=12 ; (fresh boot) sysctl -w debug.df2534_strat_null=1 (panics; capture via vm.sh log); patched build: same triggers, 0 hits, no fault", "code_hash": "dd2289882cb7b55b3f57fdd81c548be2ef1ad7add4aa04ed6e811419aaaee03c", "notes": "Impact set to leak (the production-reachable primitive is the UAF read); the strategy NULL write-fault panic is demonstrated but requires beating the disk_destroy drain, so it is quantified as defense-in-depth evidence. Patched-kernel race iteration counts are lower (82M vs 157M) because fixed xa_size serializes on xdisk_lk, as expected.", "recommended_fix": "Take xdisk_lk and re-check si_drv1 under it in xa_size and xa_strategy (queue the bio before dropping the lock), matching the lifetime lock xa_terminate_check already uses." } |