DragonFlyBSD Kernel Audit
DF-2621 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-2621",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "dos",
 "confidence": "certain",
 "verdict": "REPRODUCED. hammer2_xop_helper_create() (sys/vfs/hammer2/hammer2_admin.c:434-436) re-kmallocs pmp->xop_groups unconditionally; a second same-pfs_clid device merging into a mounted pmp (hammer2_pfsalloc, vfsops.c:588-589; mount then fails EBUSY at vfsops.c:1454-1459) provably triggers a second create on the SAME pmp: instrumented kernel shows old_groups=0xfffff80118b4e000 (non-NULL) overwritten by a fresh array, leaking the 18432-byte array, its 36 kernel threads and 36x128KB scratch buffers. Deterministic on stock across 3 runs: thread census 36 -> 108 per clone-merge; with 4 devices 0 -> 36 -> 108 -> 216 -> 360 (216 permanently-orphaned, unsignalable threads parked in the 30s 'h2idle' poll); vmstat -m HAMMER2-mount zone 13.0M -> 130M (+117MB) from eight mount commands, no unmount required - a repeatable kernel memory/thread-exhaustion DoS gated on mount privilege (vfs.usermount=1 + user-owned devices still EPERM on this build, so root/privileged-automount position required). The claimed post-pmp-kfree UAF is real in code - orphaned workers unconditionally execute hammer2_spin_ex(&pmp->xop_spin) (admin.c:1218 falls through to 1074-1079) on thr->pmp every 30s and no teardown path can ever signal them STOP (all teardown walks only the CURRENT pmp->xop_groups) - but it could NOT be observed live: every 2-chain pmp teardown on this kernel wedges earlier in hammer2_pfsfree_scan()'s freeze phase (vfsops.c:783-793, breadcrumb-localized; a separate pre-existing defect also documented with DF-2620), so hammer2_pfsfree()/kfree(pmp) (vfsops.c:722) is never reached and the instrumented freed-pmp detector never fired. No panic attributable to DF-2621 occurred. Fix (guard the kmalloc with xop_groups==NULL) validated by guest rebuild: T2 108->72, orphan threads 36->0, leak eliminated; the unrelated teardown wedge persists identically on the fixed kernel.",
 "exploit_chain": "mount(testvol) -> mount(byte-identical dd clone@testvol) -> hammer2_pfsalloc clid-merge -> hammer2_xop_helper_create #2 on the same pmp -> previous xop_groups leaked + its 36 threads orphaned forever (each holds 128KB scratch) -> repeat per same-clid device (even past cluster-full: helper_create sits outside the nchains guard) -> unbounded kernel memory + kernel-thread exhaustion. UAF escalation path (orphan 30s poll spin-locks pmp->xop_spin after pmp kfree) exists in code but is unreachable on this kernel generation because the separate teardown wedge blocks hammer2_pfsfree() first.",
 "evidence": [
  "instrumented_console.log: 'DF2621: helper_create pmp=0xfffff80118c80000 old_groups=0xfffff80118b4e000 nchains=2 -> new_groups=0xfffff80119a70000 (OLD LEAKED)' - the double-create on the same pmp",
  "run3.log: thread census T0=0, T1=36, T_m1=108, T_m2=216, T_m3=360 and vmstat -m HAMMER2-mount 13.0M -> 130M (+117MB) from 8 mount commands on stock",
  "run.log/run2.log: MOUNT2_RC=1 (EBUSY 'PFS already mounted!') with console showing both mounts binding the same pmp; 36 orphan threads persist in 'h2idle' through every teardown attempt",
  "fix_run.log: fix kernel census threads=0/36/72 (vs 108 stock) and 0 h2idle orphans during the teardown wedge; fix_build.log BUILD_OK",
  "run4.log: unprivileged usermount attempt EPERM despite vfs.usermount=1 + owned devices (threat model check)"
 ],
 "kernel_refs": [
  "sys/vfs/hammer2/hammer2_admin.c:434-436",
  "sys/vfs/hammer2/hammer2_admin.c:439-444",
  "sys/vfs/hammer2/hammer2_admin.c:461-469",
  "sys/vfs/hammer2/hammer2_admin.c:491-492",
  "sys/vfs/hammer2/hammer2_admin.c:1074-1079",
  "sys/vfs/hammer2/hammer2_admin.c:1211-1218",
  "sys/vfs/hammer2/hammer2_admin.c:1244",
  "sys/vfs/hammer2/hammer2_admin.c:233-234",
  "sys/vfs/hammer2/hammer2_vfsops.c:399-410",
  "sys/vfs/hammer2/hammer2_vfsops.c:505-506",
  "sys/vfs/hammer2/hammer2_vfsops.c:588-589",
  "sys/vfs/hammer2/hammer2_vfsops.c:1454-1459",
  "sys/vfs/hammer2/hammer2_vfsops.c:1708",
  "sys/vfs/hammer2/hammer2_vfsops.c:722",
  "sys/vfs/hammer2/hammer2.h:1244-1245"
 ],
 "poc_changes": "No forged images needed (finding sketched hexedit clid copying): a dd clone of a newfs_hammer2 image is byte-identical and merges by construction. Trigger rewritten as sh scripts with thread census (ps axlw grep h2xop-testvol) and vmstat -m HAMMER2-mount zone measurement; instrumentation (kprintf breadcrumbs + freed-pmp ring checked in hammer2_xop_next) added as a separate in-guest-only kernel build.",
 "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": 6100,
 "guest_dirty": 0,
 "build_cmd": "cd /usr/src && patch -p1 < fix.diff && make nativekernel && make installkernel",
 "run_cmd": "sh /root/poc/df2621/trigger2.sh   # mount vn0@testvol /mnt/h2; mount vn1@testvol /mnt/h2b (dd clone) -> EBUSY; census 36+72; umount -f",
 "code_hash": "50e18a511e9f8a71f31a3fc54f8e159f2c195d529e40fad67b144b8da3577872",
 "notes": "amplification trigger3.sh hash 99b6550693745e766c78b0c137505b087ebcbb35bc5663d4fafc65e626e94231; fix.diff hash 0c04d51a4a6eeb2ea5607ef93146572e3b5f634a5b4e21b97f5d5cac1b1a2a6c. The teardown wedge (umount D-state in h2twait; blocks all later hammer2 mounts via mntlk; hangs shutdown) reproduces IDENTICALLY on the fixed kernel and without any double-create - it is the pre-existing multi-chain teardown defect noted in DF-2620 (lazy xop_start re-create mid-pfsfree_scan + freeze-phase hang) and deserves its own finding. Fix keys: fix_status=fixed, fix_kernel_uname='DragonFly 6.5-DEVELOPMENT #1: Fri Aug 28 16:51:27 UTC 2026', fix_baseline_reproduced=1 (T2=108, 36 orphans, +117MB), fix_patched_reproduced=0 (T2=72, 0 orphans, no leak growth; wedge remains, pre-existing), logs: fix_run.log, fix_build.log.",
 "recommended_fix": "Guard the array allocation in hammer2_xop_helper_create(): only kmalloc pmp->xop_groups when it is NULL; keep the existing create-missing-threads loop (fix.diff).",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly 6.5-DEVELOPMENT #1: Fri Aug 28 16:51:27 UTC 2026 (in-guest nativekernel + fix.diff)",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "fix.diff (guard xop_groups kmalloc on NULL) validated by in-guest rebuild: clone-merge census 36->72 threads vs 108 stock, 0 orphan h2idle threads, no M_HAMMER2 growth on repeat mounts. Pre-existing multi-chain teardown wedge (DF-2631) persists identically on fixed kernel - unrelated.",
 "fix_evidence": "fix_run.log (census 0/36/72, 0 orphans); fix_build.log (BUILD_OK)"
}