hammer2_xop_helper_create() re-kmallocs pmp->xop_groups unconditionally β leaked thread array and orphaned threads that use-after-free the pmp
| Field | Value |
|---|---|
| ID | DF-2621 |
| Status | new |
| Severity | High |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:N/I:H/A:H |
| CWE | CWE-401 Memory Leak / CWE-416 Use After Free |
| File | sys/vfs/hammer2/hammer2_admin.c |
| Lines | 431-447 |
| Area | vfs |
| Confidence | certain |
| Discovered | 2026-08-28 |
| Pass | 2 (GLM 5.3 second pass; found via sys/vfs/hammer2/hammer2_vfsops.c:588/1708) |
| Bucket | hammer2 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
hammer2_xop_helper_create (hammer2_admin.c:426-448) assigns
pmp->xop_groups = kmalloc(...) with no NULL check, overwriting (and
leaking) any previous array and orphaning the threads living in it. It is
called twice on the same pmp whenever hammer2_pfsalloc aggregates a second
same-pfs_clid PFS element into a pmp that already has xop threads β
reachable from the mount path (vfsops.c:588-589 then vfsops.c:1708) with a
single crafted image containing two PFS inodes with identical pfs_clid,
from a dd-duplicated device, or from HAMMER2IOC_PFS_CREATE with a
duplicate clid (hammer2_ioctl.c:698). Each occurrence leaks ~16 KB of thread
array, 64 kernel threads, and 64Γ128 KB scratch buffers (~8 MB), and the
orphaned threads keep spin-locking pmp->xop_spin every 30 s after the pmp
is kfree'd in hammer2_pfsfree β a write into freed kernel memory.
Root cause
hammer2_admin.c:431-447: lockmgr(&pmp->lock, LK_EXCLUSIVE);
pmp->has_xop_threads = 1; pmp->xop_groups = kmalloc(...). Double-invocation
path: fresh-device mount β hammer2_vfs_mount:1340 hammer2_update_pmps β
for each PFS inode under the super-root hammer2_pfsalloc (vfsops.c:1561).
pfsalloc matches existing pmps by pfs_clid (vfsops.c:399-410), appends the
chain (nchains=2), and at vfsops.c:588-589 if (pmp->mp ||
iroot->cluster.nchains >= 2) hammer2_xop_helper_create(pmp); runs even when
threads already exist. The mount then finishes with hammer2_mount_helper
(vfsops.c:1485) β hammer2_xop_helper_create(pmp) (vfsops.c:1708) β
unconditional second call. The old array pointer is lost (never kfree'd, its
threads never signalled HAMMER2_THREAD_STOP because all teardown paths walk
the CURRENT pmp->xop_groups only). Each orphaned thread holds
thr->scratch = kmalloc(MAXPHYS) (admin.c:233-234) and its main loop
hammer2_primary_xops_thread (admin.c:1158-1246) wakes every hz*30 and
calls hammer2_xop_next(thr) β hammer2_spin_ex(&pmp->xop_spin)
(admin.c:1074-1079) β after the final unmount frees the pmp (vfsops.c:722),
that is a spinlock write into freed, reusable heap memory, forever, per
orphan thread.
Threat model & preconditions
- Attacker position: unprivileged local user with
vfs.usermount=1and an accessible block device mounts a crafted image containing two PFS inodes with identicalpfs_clid(hexedit: copy one PFS inode's 16-byte pfs_clid into another's); or root mounting a dd-duplicated device / usinghammer2 pfs-createwith a duplicated clid. - Privileges gained or impact: (1) unbounded kernel memory + thread leak (~8 MB and 64 threads per mount, repeatable) β memory-exhaustion DoS; (2) after unmount, orphaned threads perform repeated writes (spinlock) and reads (TAILQ walk) on freed kernel heap β corruption of whatever reallocated the memory β potential privesc; (3) 128 threads competing on inconsistent group arrays while mounted.
- Required config or capabilities: mount of crafted/duplicated image.
- Reachability: one mount of the image; UAF manifests within 30 s after the final unmount.
Proof of concept
Build & run
newfs_hammer2 -L A img; pfs-create B; umount; offline-patch B's PFS inode pfs_clid := A's pfs_clid (16 bytes); mount -t hammer2 /dev/vn0s0@A /mnt; ps -a | grep -c h2xop # 2x expected threads umount /mnt; sleep 35; watch for panic in hammer2_xop_next
Expected output
doubled h2xop thread count after one mount; ~8 MB kernel memory gone per iteration (loop to exhaust); panic in hammer2_xop_next / hammer2_primary_xops_thread within a minute of the last umount.
Impact
Kernel memory/thread exhaustion plus post-unmount UAF writes by orphaned threads β kernel memory corruption with attacker-influenced timing.
Recommended fix
Guard the allocation so the second call only creates missing threads:
--- a/sys/vfs/hammer2/hammer2_admin.c
+++ b/sys/vfs/hammer2/hammer2_admin.c
@@ -431,8 +431,15 @@ hammer2_xop_helper_create(hammer2_pfs_t *pmp)
lockmgr(&pmp->lock, LK_EXCLUSIVE);
pmp->has_xop_threads = 1;
- pmp->xop_groups = kmalloc(hammer2_xop_nthreads *
- sizeof(hammer2_xop_group_t),
- M_HAMMER2, M_WAITOK | M_ZERO);
+ /*
+ * The array may already exist (e.g. pfsalloc aggregated a new
+ * cluster element while the PFS was mounted, or during the
+ * update_pmps scan of the same mount). Never overwrite it:
+ * the threads living in the old array would be orphaned
+ * (leaked) and would keep running against the pmp after it
+ * is freed. Just create any threads that are missing; the
+ * loop below already skips slots with td != NULL.
+ */
+ if (pmp->xop_groups == NULL)
+ pmp->xop_groups = kmalloc(hammer2_xop_nthreads *
+ sizeof(hammer2_xop_group_t),
+ M_HAMMER2, M_WAITOK | M_ZERO);
for (i = 0; i < pmp->iroot->cluster.nchains; ++i) {
References
- hammer2_admin.c:426-469, 1074-1079, 1158-1246
- vfsops.c:588-589, 657-662, 776-793, 1708
Timeline
- 2026-08-28 Discovered during automated audit (pass 2, GLM 5.3).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2621 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.6 KB | β raw | |
| VERDICT.md | β | 10.8 KB | β raw | |
| trigger.sh | β | 2.4 KB | view raw | |
| trigger2.sh | β | 1.6 KB | view raw | |
| trigger3.sh | β | 1.5 KB | view raw | |
| trigger4.sh | β | 1.2 KB | view raw | |
| instrument.diff | β | 4.2 KB | view raw | |
| fix.diff | β | 1.1 KB | view raw | |
| run.log | β | 7.5 KB | view raw | |
| run2_instrumented.log | β | 2.7 KB | view raw | |
| run3.log | β | 8.1 KB | view raw | |
| run4.log | β | 2.1 KB | view raw | |
| instrumented_console.log | β | 1.3 KB | view raw | |
| fix_run.log | β | 2.7 KB | view raw | |
| fix_console.log | β | 161 B | view raw | |
| fix_build.log | β | 5.2 MB | β download | |
| env.txt | β | 853 B | view raw | |
| verdict.json | β | 6.5 KB | view raw |
DF-2621 β hammer2_xop_helper_create() unconditional re-kmalloc leaks thread
arrays + orphan kernel threads (verified)
Claim under test (from findings/DF-2621-hammer2-xop-helper-create-double-alloc-uaf.md):
hammer2_xop_helper_create() (sys/vfs/hammer2/hammer2_admin.c:425-448) assigns
pmp->xop_groups = kmalloc(...) unconditionally. When called twice on the same
pmp β which happens whenever a second same-pfs_clid device merges into a pmp
that already has xop threads (hammer2_pfsalloc, vfsops.c:588-589, then
hammer2_mount_helper, vfsops.c:1708) β the old array pointer is lost: the
~18 KB array, its 36 kernel threads and their 36 x 128 KB scratch buffers are
leaked, and the threads are orphaned (all teardown paths walk only the CURRENT
pmp->xop_groups). The finding further claims the orphans use-after-free the
pmp after hammer2_pfsfree() kfrees it (30 s poll loop touches
pmp->xop_spin, admin.c:1244/1079).
Verified results (full story: VERDICT.md)
- Double-create on the same pmp β PROVEN (stock console: both mounts bind
the same pmp; instrumented kernel:
old_groupsnon-NULL, overwritten). - Deterministic leak + orphan threads β PROVEN on stock: thread census 0 -> 36 -> 108 per single clone-merge; with 4 devices 0 -> 36 -> 108 -> 216 -> 360 (creates of 36/72/108/144 threads; 216 orphaned), HAMMER2-mount malloc zone 13.0M -> 130M (+117 MB) in eight mount commands, no unmount involved, guest otherwise idle. Orphan threads survive every teardown path.
- UAF-write component β code-proven, runtime-blocked: orphan threads
unconditionally execute
hammer2_spin_ex(&pmp->xop_spin)every 30 s poll (admin.c:1218 falls through into 1074-1079); nothing ever signals them STOP.hammer2_pfsfree()would kfree the pmp while they poll. On this guest the pmp never reaches kfree: every 2-chain teardown wedges earlier inhammer2_pfsfree_scan()'s freeze phase (a separate, pre-existing multi-chain teardown defect, already documented with DF-2620), so the freed-pmp write could not be observed live. See VERDICT.md. - Fix validated by rebuild: guarding the kmalloc with
if (pmp->xop_groups == NULL)removes the double-create (T2 108 -> 72), leaves zero orphan threads, and stops the leak growth. fix.diff.
Reproduce
Images need NO forged bytes β a dd clone shares the pfs_clid by construction.
# in-guest as root (stock INVARIANTS kernel #0):
sh trigger2.sh # mount vn0@testvol; mount vn1@testvol (dd clone) -> EBUSY;
# census shows 36+72 threads; umount -f wedges (separate bug)
sh trigger3.sh # amplification: 4 devices, census to 360 threads,
# vmstat -m HAMMER2-mount 13M -> 130M
Success criteria:
* MOUNT2_RC=1 with console hammer2_mount: ... pmp=<same addr> +
PFS already mounted!
* thread census T2=108 (36 orphans + 72) vs fixed kernel T2=72
* vmstat -m | grep HAMMER2-mount grows monotonically per clone-merge
Instrumented proof build
instrument.diff adds kprintf breadcrumbs (helper_create old/new pointers,
cleanup progress, pfsfree_scan phases, pfsfree kfree) and a freed-pmp ring
checked in hammer2_xop_next() that would report an orphan spinning on a
freed pmp. Applied to the guest /usr/src only, never to the audit tree.
Decisive output in instrumented_console.log:
DF2621: helper_create pmp=0xfffff80118c80000 old_groups=0 nchains=1 DF2621: helper_create pmp=0xfffff80118c80000 new_groups=0xfffff80118b4e000 DF2621: helper_create pmp=0xfffff80118c80000 old_groups=0xfffff80118b4e000 nchains=2 DF2621: helper_create pmp=0xfffff80118c80000 new_groups=0xfffff80119a70000 (OLD LEAKED)
DF-2621 β hammer2_xop_helper_create() unconditional re-kmalloc: leaked thread arrays + orphaned kernel threads (pmp UAF blocked at runtime) =====================================================================
VERDICT: REPRODUCED (leak + permanent orphan kernel threads, deterministic,
root-gated on this build). The claimed use-after-free of the kfree'd pmp is
PROVEN IN CODE and fully instrumented, but could not be observed live on
this guest because an independent, pre-existing multi-chain teardown defect
wedges every 2-chain pmp teardown in hammer2_pfsfree_scan()'s freeze phase
before hammer2_pfsfree() ever runs. Fix validated by guest rebuild: the
double-create, the leak and the orphan threads are gone.
1. Root cause confirmed in source
- sys/vfs/hammer2/hammer2_admin.c:425-448 β
hammer2_xop_helper_create()executespmp->xop_groups = kmalloc(...)(admin.c:434-436) with no NULL check, overwriting any previous array. The inner per-slot guard (if (pmp->xop_groups[j].thrs[i].td) continue;, admin.c:439) is defeated because the fresh array is M_ZERO'd. - Call sites that collide on one pmp:
- sys/vfs/hammer2/hammer2_vfsops.c:588-589 β
hammer2_pfsalloc()merging a same-pfs_clidchain into a pmp:if (pmp->mp || iroot->cluster.nchains >= 2) hammer2_xop_helper_create(pmp); - sys/vfs/hammer2/hammer2_vfsops.c:1708 β
hammer2_mount_helper()(unconditional), - sys/vfs/hammer2/hammer2_admin.c:491-492 β lazy re-create from
hammer2_xop_start_except()whenhas_xop_threads == 0. - Merge reachability:
hammer2_pfsalloc()matches pmps bypfs_clid(vfsops.c:399-410), appends the chain (nchains 1β2) and bumps the new device's mount_count (vfsops.c:505-506). The mount then fails EBUSY at vfsops.c:1454-1459 β but the merge (and the helper_create calls) already happened and persist. - Teardown only ever walks the CURRENT
pmp->xop_groups: hammer2_xop_helper_cleanup (admin.c:461-469), hammer2_pfsdealloc (vfsops.c:657-662), hammer2_pfsfree_scan (vfsops.c:776-819), hammer2_pfsfree (vfsops.c:693-699). The previous array's threads are never signalled HAMMER2_THREAD_STOP; nothing in the kernel retains a pointer to them. Each orphan holdsthr->scratch= kmalloc(MAXPHYS = 128 KB) (admin.c:233-234), freed only by hammer2_thr_delete (admin.c:261-263) β never called for orphans.
2. What was reproduced, on which kernel
All runs: QEMU/KVM guest, DragonFly 6.5-DEVELOPMENT x86_64, stock INVARIANTS
kernel #0 (and rebuilt #1 kernels), 6 vCPUs, hammer2 root fs.
hammer2_xop_nthreads = 36 on this box (vfsops.c:258-267 β 6 cpus);
xop_groups array = 36 Γ 8 Γ 64 B = 18432 B.
Trigger (NO forged bytes needed): truncate -s 64M base.img;
newfs_hammer2 -L testvol base.img; dd a byte-identical clone (identical
pfs_clid by construction); vnconfig -c vn0/vn1; mount vn0@testvol, then
attempt mount vn1@testvol.
STOCK kernel #0:
* mount1 β 36 threads ("h2xop-testvol.00..35"). mount2 β EBUSY, census 108 =
36 orphans + 72 new (create at nchains=2 makes 2Γ36). Console shows both
mounts binding the SAME pmp (run1: 0xfffff80118d80000, run2:
0xfffff80118be0000, run3: 0xfffff80118c40000). Deterministic across all
three vulnerable-kernel runs (run.log, run2.log, run3.log).
* Amplification (run3.log): 4 devices β census 0 β 36 β 108 β 216 β 360
(sequential creates at nchains=1,2,3,4 add 36/72/108/144 threads; 216 of
them orphaned forever, all parked in "h2idle" β the 30 s poll sleep).
vmstat -m HAMMER2-mount malloc zone: 13.0M β 130M (+117 MB) from eight
mount commands, with NO unmount and NO filesystem activity. Leak is
unbounded in device count (cluster-full merges still call helper_create β
it sits outside the nchains guard at vfsops.c:583-589).
* Orphans are permanent: they appear in every post-teardown census for the
life of the boot; no signal path exists that could stop them.
INSTRUMENTED kernel #1 (instrument.diff, in-guest only): decisive console trace (instrumented_console.log): DF2621: helper_create pmp=0xfffff80118c80000 old_groups=0 nchains=1 DF2621: helper_create pmp=0xfffff80118c80000 new_groups=0xfffff80118b4e000 DF2621: helper_create pmp=0xfffff80118c80000 old_groups=0xfffff80118b4e000 nchains=2 DF2621: helper_create pmp=0xfffff80118c80000 new_groups=0xfffff80119a70000 (OLD LEAKED) [... mount2 β "PFS already mounted!" ...] DF2621: helper_cleanup enter pmp=... groups=0xfffff80119a70000 nmasters=2 DF2621: helper_cleanup col 0 deleted DF2621: helper_cleanup col 1 deleted DF2621: helper_cleanup freeing groups=0xfffff80119a70000 DF2621: pfsfree_scan hmp=0xfffff801184a0000 which=0 enter DF2621: helper_create pmp=0xfffff80118c80000 old_groups=0 nchains=2 DF2621: helper_create pmp=0xfffff80118c80000 new_groups=0xfffff80118e00000 (OLD LEAKED) DF2621: pfsfree_scan freeze begin pmp=0xfffff80118c80000 groups=0xfffff80118e00000 [umount wedges here forever] This proves: (a) double-create on the same pmp (old_groups non-NULL, overwritten); (b) the CURRENT array is torn down correctly; (c) a THIRD array is lazily re-created mid-teardown by xop_start (admin.c:491) during pfsfree_scan's own sync β the source of the 72 "frozen" threads seen in the stock wedges; (d) the wedge is in pfsfree_scan's freeze loop (vfsops.c:783-793), BEFORE any pmp kfree.
3. The UAF claim β honest assessment
Code path (verified line-by-line): an orphaned worker's poll loop
(admin.c:1158) calls hammer2_xop_next(thr) UNCONDITIONALLY each iteration
β the HAMMER2_THREAD_XOPQ check at admin.c:1211 only clears the flag and
falls through β and hammer2_xop_next executes
hammer2_spin_ex(&pmp->xop_spin) (admin.c:1074-1079) before scanning
thr->xopq (which lives in the leaked-but-still-allocated array, so that
part is not a fault). After tsleep(..., "h2idle", hz*30) (admin.c:1244)
each orphan therefore performs a spinlock acquire+release WRITE against
thr->pmp β every 30 seconds, forever. If hammer2_pfsfree() kfrees the
pmp (vfsops.c:722) while orphans exist, those are writes into freed kernel
heap at a fixed offset inside a several-KB M_HAMMER2 block, plus a TAILQ
scan; a reused word that resembles a held spinlock would additionally make
an orphan spin forever against a real victim object.
Runtime: NOT observed. hammer2_pfsfree() is unreachable for any 2-chain
pmp on this kernel because every teardown (plain umount, umount -f, and
shutdown) wedges earlier in hammer2_pfsfree_scan()'s freeze phase. This
wedge is a SEPARATE, pre-existing defect (DF-2620 already observed the
identical h2twait wedge with an unrelated fix applied; my breadcrumbs
localize it to the freeze loop after the mid-teardown lazy re-create). The
instrumented freed-pmp ring + detector I added (df2621_check_freed() in
hammer2_xop_next) never fired for exactly this reason β the pmp never
entered the ring. The double-create is a necessary but not sufficient
condition for the UAF on this kernel generation; on a kernel where
multi-chain teardown completes, the orphan writes to the freed pmp follow
mechanically from admin.c:1074-1079 + vfsops.c:722.
Also NOT observed: any panic attributable to DF-2621. (The stock umount wedge leaves the system otherwise usable, but hammer2 mntlk stays held by the wedged unmount thread, blocking all later hammer2 mounts/unmounts, and shutdown hangs β the guest had to be force-killed on every reset.)
4. Threat model check (run4.log)
The finding's unprivileged precondition did NOT hold on this guest:
with vfs.usermount=1, /dev/vn0 and /dev/vn1 chown'd (and chmod 660) to
the unprivileged user, and a user-owned mountpoint, mount -t hammer2
still returns EPERM for the user. The trigger therefore requires mount
privilege on this build (root, or a privileged automount path mounting
attacker-supplied media such as a dd-duplicated device). The double-create
also fires from ordinary privileged cluster administration (mounting a
second device carrying the same clid), making the leak a correctness bug
even absent an attacker.
5. Fix validation (guest rebuild)
fix.diff guards the allocation (if (pmp->xop_groups == NULL)) and keeps
the existing create-missing-threads loop. Applied to a pristine guest
/usr/src, make nativekernel && make installkernel completed BUILD_OK
(fix_build.log), rebooted into kernel #1.
Baseline (stock #0, same trigger): T2 = 108 threads (36 orphans + 72), post-wedge census 36 h2idle orphans + 72 frozen. Patched (#1, same trigger): T2 = 72 threads (fix_run.log: threads=72, idle=72, frozen=0) β the merge added ONLY the missing clindex-1 column to the EXISTING array; during the (still present, pre-existing) teardown wedge the census is 0 h2idle orphans + 72 frozen. No orphan threads exist at any point. The pre-existing teardown wedge itself is unaffected by the guard, as expected β it is a separate defect (lazy re-create mid-teardown + freeze-phase hang) and out of scope for this finding.
fix verdict: fixed β the double-create, the array/thread/scratch leak and the orphan threads are eliminated; the unrelated teardown wedge remains (recommend a separate finding; see note in fix.diff header).
6. Primitive characterization (if the UAF were reachable)
Write primitive per orphan per 30 s: spin_lock/spin_unlock pair on the 32-bit word at offsetof(hammer2_pfs, xop_spin) (hammer2.h:1244) of a freed, reusable M_HAMMER2 block of several KB (sizeof(hammer2_pfs) with 8 embedded sync threads + inumhash). Lock value transitions 0β1β0 (plus spin-loop reads while contended). Secondary: TAILQ_FOREACH over thr->xopq (leaked array β allocated, not freed). A uid=0 chain was not developed: the write is a transient bit pattern on a fixed offset with a 30 s cadence and no length control β corruption-class, not a clean arbitrary-write β and on this guest the pmp never reaches kfree (hard blocker, documented above).
7. Files
trigger.sh stock v1: mount/merge/EBUSY/umount (plain) β first wedge trigger2.sh stock v2: umount -f variant + idle/frozen census trigger3.sh stock v3: 4-device amplification (leak quantification) trigger4.sh usermount/unprivileged attempt (EPERM β threat model check) instrument.diff kprintf breadcrumbs + freed-pmp ring/UAF detector (guest only) fix.diff the validated one-line-guard fix (+ comment) run.log / run2.log(=fix_run.log counterpart) / run3.log / run4.log run2_instrumented.log full trigger2 run on instrumented kernel instrumented_console.log decisive breadcrumb trace fix_run.log trigger2 on fix kernel (T2=72, orphans=0) fix_console.log fix-kernel mount console (same pmp both mounts) fix_build.log full untrimmed fix-kernel build env.txt guest environment
Fix verification
fixedfix.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_run.log (census 0/36/72, 0 orphans); fix_build.log (BUILD_OK)
Confirmed kernel references
- 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
Detail
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 (decisive lines)
["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)']
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.
Verified 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).
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.
No comments yet.