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)
PoC verification
Evidence pack
findings/poc/DF-0823 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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):
- Guard xop_groups allocation in
hammer2_xop_helper_createβ only allocate ifxop_groups == NULL, preventing the double-alloc leak and thread orphaning - Fix cleanup loop β change
pfs_nmasterstocluster.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.
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
- Create two vn-backed hammer2 images,
newfs_hammer2each - Mount vn0@DATA β create MASTER PFS "testpfs" on vn0
- Mount vn0@testpfs β PMP created (nchains=1),
xop_helper_createcall #1 - Mount vn1@DATA β
hammer2_update_pmpsfinds SLAVE testpfs with matching clid β adds chain to PMP (nchains=2) βxop_helper_createcall #3 (overwrites xop_groups!) - Create SLAVE PFS on vn1 with matching clid
- I/O on /mnt/test to spin up XOP threads
umount /mnt/h2b(vn1@DATA) βmount_count > 0βpfsfree_scanskipped β SLAVE chain staysumount /mnt/test(vn0@testpfs) βxop_helper_cleanup: stops thread[0] only (pfs_nmasters=1),kfree(xop_groups_B)β orphan threads surviveumount /mnt/h2a(vn0@DATA) β device cleanup triggerspfsfree_scanwhich 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
fixedVALIDATED 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
Confirmed kernel references
- sys/vfs/hammer2/hammer2_admin.c:434
- sys/vfs/hammer2/hammer2_admin.c:437
- sys/vfs/hammer2/hammer2_admin.c:461
- sys/vfs/hammer2/hammer2_admin.c:468
- sys/vfs/hammer2/hammer2_admin.c:1159
- sys/vfs/hammer2/hammer2_admin.c:1220
- sys/vfs/hammer2/hammer2_admin.c:1243
- sys/vfs/hammer2/hammer2_vfsops.c:536
- sys/vfs/hammer2/hammer2_vfsops.c:541
- sys/vfs/hammer2/hammer2_vfsops.c:588
- sys/vfs/hammer2/hammer2_vfsops.c:1708
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.
No comments yet.