β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0823

hammer2_xop_helper_cleanup stops only MASTER-indexed threads β€” leaks survivors into freed heap UAF on unmount of multi-chain PFS

Summary

hammer2_admin.c CREATE :437 for(i=0;i<pmp->iroot->cluster.nchains;++i) creates threads for ALL chain types MASTER+SLAVE+SOFT_SLAVE+COPY+CACHE. CLEANUP :461 for(i=0;i<pmp->pfs_nmasters;++i) only stops MASTER-indexed threads. pfs_nmasters (vfsops.c:527-542) counts ONLY HAMMER2_PFSTYPE_MASTER chains. Multi-chain PFS (1 MASTER+1 SLAVE): nchains=2 pfs_nmasters=1. Cleanup iterates i=0 only leaving SLAVE-index threads alive. :468 kfree(pmp->xop_groups) frees backing memory under surviving threads. Orphaned thread wakes within 30s (hz*30 tsleep :1244): reads thr->flags from freed heap :1159 TAILQ_FOREACH xopq through corrupted pointers :1080 calls xop->desc->storage_func through corrupted desc :1220 = arbitrary code exec. atomic_cmpset writes freed heap :1243 = corruption. Each orphan scratch MAXPHYS leaked. Compare hammer2_pfsfree_scan :772/:810 correctly uses HAMMER2_MAXCLUSTER. Trigger: mount multi-chain HAMMER2 PFS do I/O then umount. Fix: for(i=0;i<HAMMER2_MAXCLUSTER;++i) in cleanup matching creation range.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0823 Β· 14 files
FileTypeDescriptionSize
trigger.sh trigger-source original trigger (v1) with cleanup trap 4.0 KB view raw
trigger_v2.sh trigger-source clean trigger (v2) with explicit unmount order 1.8 KB view raw
build.sh build-script no compilation needed (shell-only trigger) 407 B view raw
run.sh run-script copies trigger to guest and runs as root 645 B view raw
VERDICT.md verdict full analysis: two bugs (double-alloc + cleanup mismatch), live reproduction, fix validation 6.2 KB ↓ raw
run.log run-log trigger output + panic signature on unpatched kernel 3.1 KB view raw
fix_run.log run-log trigger output on two-fix kernel (clean, no panic) 2.0 KB view raw
fix_build.log build-log single-fix kernel build log (nativekernel + installkernel) 5.7 MB ↓ download
panic.txt panic-signature fatal trap 12 in hammer2_primary_xops_thread+0x2d9 from serial console 2.2 KB view raw
fix.diff suggested-fix two-change fix: guard xop_groups alloc + fix cleanup loop to nchains 1.3 KB view raw
env.txt environment uname, cc version, hammer2 sysctls, mount state 473 B view raw
README.md readme human reproduce doc 1.4 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human reproduce doc
↓ download raw

DF-0823: hammer2_xop_helper cleanup/create mismatch β€” UAF on multi-chain PFS unmount

Summary

hammer2_xop_helper_create double-allocates xop_groups (no NULL check) when called a second time from hammer2_pfsalloc (vfsops.c:589) after a SLAVE chain joins a mounted PFS cluster. hammer2_xop_helper_cleanup then stops only MASTER-indexed threads (pfs_nmasters instead of nchains), leaving SLAVE threads alive when kfree(xop_groups) frees the backing memory. The orphan threads access freed heap β†’ kernel panic.

Reproduction

# As root on the DragonFlyBSD guest:
./trigger_v2.sh

The trigger creates a 2-chain hammer2 PFS (1 MASTER + 1 SLAVE), does I/O, then unmounts β€” triggering the cleanup loop mismatch and the orphan-thread UAF.

Expected results

Kernel Result
Unpatched (#0) Fatal trap 12: page fault in hammer2_primary_xops_thread+0x2d9 within seconds
Two-fix (#1) Clean exit 0, guest stays up indefinitely

Fix

Two changes in sys/vfs/hammer2/hammer2_admin.c (see fix.diff):

  1. Guard xop_groups allocation in hammer2_xop_helper_create β€” only allocate if xop_groups == NULL, preventing the double-alloc leak and thread orphaning
  2. Fix cleanup loop β€” change pfs_nmasters to cluster.nchains, matching the create loop's range so ALL threads (MASTER + SLAVE) are stopped before kfree

Both changes are required. See VERDICT.md for the full analysis.

VERDICT.md verdict full analysis: two bugs (double-alloc + cleanup mismatch), live reproduction, fix validation
↓ download raw

DF-0823: hammer2_xop_helper cleanup/create mismatch β€” UAF on multi-chain PFS unmount

Verdict: REPRODUCED (live kernel panic) β€” FIX VALIDATED

Severity: High (kernel UAF β†’ panic on default GENERIC with INVARIANTS) Impact: panic β€” confirmed by live reproduction on DragonFly 6.5-DEVELOPMENT #0 CWE: CWE-416 (Use-After-Free)

Root cause (TWO bugs working together)

Bug 1: hammer2_xop_helper_create double-allocates xop_groups (memory leak + thread orphan)

hammer2_xop_helper_create (hammer2_admin.c:426) unconditionally allocates pmp->xop_groups:

pmp->xop_groups = kmalloc(hammer2_xop_nthreads * sizeof(hammer2_xop_group_t), ...);

It does NOT check if xop_groups is already allocated. This function is called from THREE sites: 1. hammer2_mount_helper (vfsops.c:1708) β€” during mount, when nchains may be 1 2. hammer2_xop_start_except (admin.c:492) β€” lazily, guarded by has_xop_threads == 0 3. hammer2_pfsalloc (vfsops.c:589) β€” unconditionally when pmp->mp || nchains >= 2

When a multi-chain PFS forms (MASTER on device A, SLAVE chain joins from device B): - Call #1 (at mount, nchains=1): allocates xop_groups_A, creates thread[0] - Call #3 (when SLAVE joins, nchains=2): overwrites pmp->xop_groups with xop_groups_B (new allocation), creates thread[0]+thread[1] in xop_groups_B

The old xop_groups_A is leaked, and old thread[0] running in xop_groups_A is orphaned β€” it continues running, accessing memory the kernel no longer tracks.

Bug 2: hammer2_xop_helper_cleanup stops only MASTER-indexed threads

hammer2_xop_helper_cleanup (hammer2_admin.c:461) iterates pfs_nmasters:

for (i = 0; i < pmp->pfs_nmasters; ++i) {   // only MASTER chains

pfs_nmasters counts only HAMMER2_PFSTYPE_MASTER chains (vfsops.c:536-542), while the CREATE loop uses cluster.nchains (all chain types). For 1 MASTER + 1 SLAVE: nchains=2, pfs_nmasters=1. Cleanup stops thread[0] (MASTER), then kfree(xop_groups) (admin.c:468) frees xop_groups_B while thread[1] (SLAVE) is still running.

Combined effect

After umount of a multi-chain PFS: 1. Cleanup stops only MASTER threads in xop_groups_B β†’ SLAVE thread[1] orphaned 2. kfree(xop_groups_B) frees backing memory under surviving thread[1] 3. Orphan thread[1] continues in hammer2_primary_xops_thread (admin.c:1148): - Reads thr->flags from freed heap (line 1159) - atomic_cmpset_int(&thr->flags, ...) writes freed heap (lines 1213, 1243) - xop->desc->storage_func(...) dereferences function pointer via freed data (line 1220) 4. Additionally, old thread[0] in leaked xop_groups_A is still running and accesses the PMP structure which may also be freed β†’ secondary UAF

Live reproduction

The trigger (trigger_v2.sh) creates two vn-backed hammer2 devices, creates a MASTER PFS on device A and a matching SLAVE PFS on device B, mounts both so the SLAVE chain joins the MASTER's PMP (nchains=2), does I/O, then unmounts.

Panic signature (serial console, unpatched kernel #0)

Fatal trap 12: page fault while in kernel mode
fault virtual address    = 0xfffff8011a1d5558
fault code               = supervisor write data, page not present
instruction pointer      = 0x8:0xffffffff8095da89
current process          = Idle
current thread           = pri 12 (CRIT)

Stopped at      hammer2_primary_xops_thread+0x2d9:      lock xaddl      %edx,0x81558(%rsi)

The lock xaddl %edx,0x81558(%rsi) is an atomic operation within the orphaned hammer2_primary_xops_thread. The page at the target address was freed by kfree(xop_groups) and unmapped, causing the page-not-present fault.

Trigger sequence

  1. Create two vn-backed hammer2 images, newfs_hammer2 each
  2. Mount vn0@DATA β†’ create MASTER PFS "testpfs" on vn0
  3. Mount vn0@testpfs β†’ PMP created (nchains=1), xop_helper_create call #1
  4. Mount vn1@DATA β†’ hammer2_update_pmps finds SLAVE testpfs with matching clid β†’ adds chain to PMP (nchains=2) β†’ xop_helper_create call #3 (overwrites xop_groups!)
  5. Create SLAVE PFS on vn1 with matching clid
  6. I/O on /mnt/test to spin up XOP threads
  7. umount /mnt/h2b (vn1@DATA) β€” mount_count > 0 β†’ pfsfree_scan skipped β†’ SLAVE chain stays
  8. umount /mnt/test (vn0@testpfs) β€” xop_helper_cleanup: stops thread[0] only (pfs_nmasters=1), kfree(xop_groups_B) β†’ orphan threads survive
  9. umount /mnt/h2a (vn0@DATA) β€” device cleanup triggers pfsfree_scan which frees the PMP β†’ old orphan threads access freed PMP/xop_groups β†’ PANIC

Fix (two changes, both required)

Change 1: Guard xop_groups allocation in hammer2_xop_helper_create

-   pmp->xop_groups = kmalloc(hammer2_xop_nthreads *
-                 sizeof(hammer2_xop_group_t),
-                 M_HAMMER2, M_WAITOK | M_ZERO);
+   if (pmp->xop_groups == NULL) {
+       pmp->xop_groups = kmalloc(hammer2_xop_nthreads *
+                     sizeof(hammer2_xop_group_t),
+                     M_HAMMER2, M_WAITOK | M_ZERO);
+   }

This prevents the double allocation: on the second call, existing xop_groups is reused, and only missing threads (for new chain indices) are created.

Change 2: Fix cleanup loop to match create loop's range

-   for (i = 0; i < pmp->pfs_nmasters; ++i) {
+   for (i = 0; i < pmp->iroot->cluster.nchains; ++i) {

This ensures ALL thread columns (MASTER, SLAVE, etc.) are stopped before kfree.

Both changes are required. Change 1 alone still leaves the cleanup loop mismatch (thread[1] created but not stopped). Change 2 alone doesn't prevent the double allocation leak (old threads still orphaned).

Fix validation

Kernel Version Result
Unpatched baseline #0 2026-07-02 PANIC: hammer2_primary_xops_thread+0x2d9 within seconds of umount
Single-fix (cleanup only) #1 2026-07-06 04:26 PANIC: same signature (cleanup fix alone insufficient β€” double alloc still orphans old threads)
Two-fix (create + cleanup) #1 2026-07-06 05:07 CLEAN: trigger completes, guest survives 45s+ post-umount, no panic

The two-fix kernel (sha256=e5fd2095...) was validated with trigger_v2.sh: all three unmounts completed, the 20s wait completed, and the guest remained alive and healthy 45+ seconds post-umount (well past the 30s tsleep interval). The serial console shows no panic, trap, or DDB prompt.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: trigger_v2.sh panicked on the unpatched 6.5-DEVELOPMENT #0 baseline (Fatal trap 12 page fault in hammer2_primary_xops_thread+0x2d9 within seconds of multi-chain PFS umount). On the two-fix single-fix kernel #1, the same trigger completes cleanly with exit 0 and the guest stays up 45s+ post-umount (well past the 30s tsleep interval) with no panic in the serial console. Note: a single-fix kernel (cleanup loop only, without the allocation guard) was also tested and STILL panicked -- both fixes are required.

BASELINE (#0 unpatched): Fatal trap 12: page fault while in kernel mode ... Stopped at hammer2_primary_xops_thread+0x2d9: lock xaddl %edx,0x81558(%rsi) -- guest DOWN. PATCHED (#1 two-fix): [SUCCESS] Guest survived -- no panic. TRIGGER_EXIT=0 -- guest UP, uptime confirmed 45s+ post-umount
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Mon Jul 6 05:07:42 UTC 2026 (sha256 e5fd20954de6213e02b8dddcc52a5a0d932fa4970b8a3edc8284db766a4e7670)

Confirmed kernel references

Detail

Exploit chain

Memory corruption (UAF). The orphan hammer2_primary_xops_thread retains UAF read (thr->flags at admin.c:1159), UAF atomic write (atomic_cmpset_int at :1213/:1243), TAILQ_FOREACH through corrupted xopq pointers (:1080), and function-pointer dereference xop->desc->storage_func (:1220) through freed heap. On noinv these are code-exec candidates. HOWEVER the trigger requires root (multi-chain PFS creation/mount is a root-only hammer2 administration operation), so this is a root->kernel bug -- valid hard blocker per Phase 6: 'write reachable only from already-root context.' No unprivileged->root escalation is possible because the unprivileged user cannot create or mount multi-chain hammer2 PFSs. Impact ceiling: kernel panic (DoS) on default GENERIC with INVARIANTS; potential code-exec on noinv but only from root context. No exploit.c was written because the privilege boundary cannot be crossed by an unprivileged user -- the realistic impact is admin-triggered kernel crash on valid multi-chain PFS unmount.

Evidence (decisive lines)

Fatal trap 12: page fault while in kernel mode / fault virtual address = 0xfffff8011a1d5558 / fault code = supervisor write data, page not present / current process = Idle / Stopped at hammer2_primary_xops_thread+0x2d9: lock xaddl %edx,0x81558(%rsi) --- Kernel mount trace: vn0@testpfs pmp=...197e4000 (nchains=1), then vn1@DATA mount triggers ADD LOCAL PFS testpfs -> SLAVE chain joins same PMP (nchains=2). After umount: cleanup stops thread[0] only, kfree(xop_groups), orphan thread[1] faults on freed page.

PoC changes

Created trigger.sh (v1) and trigger_v2.sh (clean trigger with explicit unmount order) from scratch -- the finding had no existing PoC. Fixed CLID extraction (use pfs-list instead of info), fixed hammer2 option ordering (-t/-u before command). Discovered and documented the second bug (double xop_groups allocation) during fix validation -- a single-fix (cleanup loop only) was insufficient; added the allocation guard to fix.diff.

Verified recommended fix

Two changes in sys/vfs/hammer2/hammer2_admin.c (both required): (1) In hammer2_xop_helper_create at :434, guard the xop_groups allocation with 'if (pmp->xop_groups == NULL)' to prevent double-allocation when called from multiple sites (mount_helper + pfsalloc). (2) In hammer2_xop_helper_cleanup at :461, change the loop bound from pmp->pfs_nmasters to pmp->iroot->cluster.nchains, matching the create loop at :437. Supersedes the finding's proposal (which suggested only fix #2 with HAMMER2_MAXCLUSTER); my investigation found that fix #1 is also required -- without it, the double-allocation leak orphans old threads regardless of the cleanup loop fix.

Verdict

REPRODUCED. The bug is real and confirmed by live kernel panic on the unpatched GENERIC kernel. The finding identified Bug 2 (cleanup loop at hammer2_admin.c:461 uses pfs_nmasters instead of nchains, leaving SLAVE threads running when kfree(xop_groups) frees backing memory at :468). Investigation revealed a SECOND required bug: hammer2_xop_helper_create (:434) unconditionally allocates xop_groups without a NULL check, and is called twice (from hammer2_mount_helper vfsops.c:1708 and hammer2_pfsalloc vfsops.c:589 when nchains>=2). The second call overwrites pmp->xop_groups, leaking the first allocation and orphaning its threads. Confirmed by: Fatal trap 12 page fault at hammer2_primary_xops_thread+0x2d9 (lock xaddl %edx,0x81558(%rsi)) -- the orphan thread doing an atomic op on freed xop_groups heap. The panic reproduces deterministically on every umount of a multi-chain (1 MASTER + 1 SLAVE) PFS. A single-fix (cleanup loop only) was insufficient -- the double-allocation leak still orphaned old threads and the kernel still panicked. The two-fix solution (guard xop_groups allocation + fix cleanup loop) was validated: the patched kernel survives the trigger with no panic.