⬢ DragonFlyBSD Kernel Audit
DF-4797 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-4797",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "panic",
  "confidence": "certain",
  "verdict": "Reproduced 2/2 in situ on the audit guest booted with its system disk as a QEMU PIIX3 IDE disk (root on nata ad0): 16 unprivileged raw-pread threads on /dev/ad0 plus a KLD calling device_detach(ad0) \u2014 the exact PCI-hot-unplug/kldunload teardown path \u2014 produce Fatal trap 12: page fault while in kernel mode, fault virtual address = 0xc, Stopped at dscheck+0x48: movl 0xc(%r13),%ecx with the panicking thread a storm pread in diskstrategy->dscheck, because ad_detach() (ata-disk.c:167-203) frees dp->d_slice via disk_invalidate()->dsgone() and kfrees the ad_softc (with the embedded struct disk that the still-open cdevs walk) with no serialization against the data path; the driver-level sinks (devstat_start_transaction on the freed softc at :360, ad_dones NULL-ivars devstat write at :368/:376) sit on the same unserialized path. The ad(4) twin of validated DF-4646 (nvme). Fix validated on a rebuilt kernel (#2 with fix.diff v2): detach returns EBUSY while descriptors are open (d_opencount gate + AD_F_DETACHING under queue_mtx + in-flight ref drain + ata_fail_requests before disk teardown + diskstrategy NULL-d_slice guard); baseline panic gone, storm (86981 IOs) completes, guest healthy; a first fix iteration (v1, funnel-only) still trapped in dsclose+0x30, proving the embedded-struct-disk lifetime issue and driving the v2 open-count refusal.",
  "exploit_chain": "",
  "evidence": "panic.txt (full fatal-trap-12 capture, fault addr 0xc, dscheck+0x48, process df4797_storm pid 906); run.log (full serial of baseline run incl. df4797 detach lines); ddb-trace.txt (dscheck/diskstrategy/dev_dstrategy/physio/dev_dread/devfs_fo_read/kern_preadv/sys_extpread/syscall2); run3.out + run.fixed.log (patched kernel: device_detach -> 16 = EBUSY, storm total_io=86981, no trap); fix.diff (v2, git-apply-checked against sys/); env.txt",
  "kernel_refs": [
    "sys/dev/disk/nata/ata-disk.c:167",
    "sys/dev/disk/nata/ata-disk.c:191",
    "sys/dev/disk/nata/ata-disk.c:201",
    "sys/dev/disk/nata/ata-disk.c:360",
    "sys/dev/disk/nata/ata-disk.c:368",
    "sys/dev/disk/nata/ata-disk.c:376",
    "sys/kern/subr_disk.c:1246",
    "sys/kern/subr_diskslice.c:348"
  ],
  "poc_changes": "The findings sketch had no harness; built from scratch: df4797_storm.c (16 threads hold /dev/ad0 open, raw 512B preads, reopen on ENXIO to survive cycles) and df4797_detach.c KLD (devclass_find(\"ad\") unit 0, device_detach in a kernel thread, cycles + re-attach via device_probe_and_attach). KLD needed device_if.h/bus_if.h generated sources and <sys/kthread.h> for -Werror. Guest required booting the system qcow2 as a QEMU IDE disk (SeaBIOS on this host never falls through to virtio when an IDE hd exists) with fstab/loader.conf vbd0->ad0 edits and a clean shutdown so they persist.",
  "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 (baseline); #2: Wed Sep 23 22:49:37 UTC 2026 (fix v2)",
  "runtime_sec": 7800,
  "guest_dirty": 0,
  "build_cmd": "sh build.sh  (make -m /usr/share/mk KMODDIR=/root/poc/DF-4797; cc -O2 -pthread -o df4797_storm df4797_storm.c)",
  "run_cmd": "sh run.sh   (kldload df4797.ko; ./df4797_storm 16 25 & sleep 4; sysctl kern.df4797.run=1)",
  "code_hash": "d23116429499a270008be30c9c0b38979e16ac66af6c4c72235a0d397a3bd006",
  "notes": "uid0 escalation not pursued: the trigger side (device_detach of an attached ata disk) requires root/hypervisor, so the primitive is privileged-teardown-induced memory corruption/DoS (same threat model as DF-4646, rated High). Impact of the UAF write (devstat counters into freed M_AD chunk) is limited by DFlys shared slab zones but was not groomed to root here. Baseline reproduced twice; fix v1 battery trapped in dsclose+0x30 (documented in VERDICT.md), fix v2 battery clean. Guest left on kernel #2 (fix v2); a final vm.sh reset with-src restores the stock guest.",
  "recommended_fix": "ad_detach: refuse EBUSY while d_opencount!=0; AD_F_DETACHING + ivars-NULL under queue_mtx with in-flight ref drain in ad_strategy/ad_done; ata_fail_requests before disk_invalidate/destroy; NULL d_slice guard in diskstrategy (see fix.diff)",
  "fix_status": "fixed",
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #2: Wed Sep 23 22:49:37 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_verdict": "Baseline kernel panics 2/2 (fatal trap 12, dscheck+0x48, addr 0xc). Kernel rebuilt with fix.diff v2 via make -j6 nativekernel KERNCONF=X86_64_GENERIC (RC=0) + installkernel: identical PoC battery yields device_detach -> 16 (EBUSY) with the storms 16 fds open, storm completes 86981 IOs, serial clean of any trap, guest healthy; post-storm detach also EBUSY (root slice mounted \u2014 correct). Residual close-vs-detach window after d_opencount hits 0 requires disk-layer lifetime work (documented in VERDICT.md).",
  "fix_evidence": "VERDICT.md (baseline + v1-trap + v2-clean narrative); run.log (baseline panic); run.fixed.log + run3.out (patched battery); buildkernel2.log RC=0; fix.diff"
}