iroot meta.inum overwritten from media after inum-hash insertion β hammer2_inode_drop walks off the wrong bucket (NULL-deref panic at unmount plus in-memory inode aliasing)
| Field | Value |
|---|---|
| ID | DF-2635 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H |
| CWE | CWE-476 NULL Pointer Dereference (CWE-703 secondary) |
| File | sys/vfs/hammer2/hammer2_inode.c |
| Lines | 617-625 (walk), 933/954 (hash insert); cause vfsops.c:456,1975 |
| Area | vfs |
| Confidence | certain |
| Discovered | 2026-08-29 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | hammer2 |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
hammer2_inode_get(pmp, NULL, 1, -1) inserts the PFS root inode on the
inum hash under inum=1 (inode.c:933, 954-977). hammer2_pfsalloc
(vfsops.c:456) and hammer2_vfs_root (vfsops.c:1975) then overwrite
iroot->meta wholesale from the on-media inode β including meta.inum.
A crafted image with meta.inum != 1 leaves iroot on the wrong bucket; at
unmount, hammer2_inode_drop computes the bucket from the CURRENT
meta.inum (inode.c:617) and the removal walk (:624-625) runs off the end
of the wrong list. Because next is the first field of
struct hammer2_inode, xipp degenerates to NULL and *xipp reads
address 0. Secondary effect: inum lookups cannot find iroot and can
create duplicate in-memory inodes for the same media chain.
Root cause
Wholesale meta copy from media over an already-hashed inode: sys/vfs/hammer2/hammer2_vfsops.c:456, :1975; fragile walk at sys/vfs/hammer2/hammer2_inode.c:624-625, reached from unmount via hammer2_vfsops.c:707-711.
Threat model & preconditions
Crafted/checksum-forged hammer2 image (mount requires root β same trust boundary as the DF-0763/DF-2616/DF-2624 crafted-image family): guaranteed kernel NULL-deref panic at unmount (DoS) plus inode aliasing for the mount's lifetime.
Proof of concept
VERIFIED on the stock INVARIANTS guest: forge_2635.py (meta.inum 1 β
0x42, CHECK_NONE ancestors, volhdr CRC32C recompute); mount/ls/write/sync
succeed; umount panics: Fatal trap 12 ... fault virtual address = 0x0
... Stopped at hammer2_inode_drop+0x1e3 (current process umount).
Fix (preserve inum=1 across both overwrites + while (*xipp && *xipp !=
ip) walk hardening) validated on a nativekernel rebuild: UMOUNT_RC=0
on the same forged image. Evidence: findings/poc/DF-2635/.
Impact
Deterministic unmount-time kernel panic from a crafted image; in-memory inode aliasing. No uid=0 route (read-only deref of address 0).
Recommended fix
--- a/sys/vfs/hammer2/hammer2_vfsops.c
+++ b/sys/vfs/hammer2/hammer2_vfsops.c
@@
if ((iroot = pmp->iroot) == NULL) {
iroot = hammer2_inode_get(pmp, NULL, 1, -1);
- if (ripdata)
+ if (ripdata) {
iroot->meta = ripdata->meta;
+ /* iroot is hashed under inum 1; keep it */
+ iroot->meta.inum = 1;
+ }
@@
meta = &hammer2_xop_gdata(&xop->head)->ipdata.meta;
pmp->iroot->meta = *meta;
+ pmp->iroot->meta.inum = 1;
pmp->inode_tid = meta->pfs_inum + 1;
plus a not-found-safe walk in hammer2_inode_drop β full git-apply-able diff: findings/poc/DF-2635/fix.diff (validated).
References
- DF-0796 (the excluded NULL-slot sibling in the same function)
Timeline
- 2026-08-29 Discovered during pass-2 audit of hammer2_inode.c (GLM 5.3); verified reproduced + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2635 Β· 13 files| File | Type | Description | Size | |
|---|---|---|---|---|
| forge_2635.py | β | 3.1 KB | view raw | |
| run.sh | β | 605 B | view raw | |
| panic.txt | β | 2.2 KB | view raw | |
| run.log | β | 2.2 KB | view raw | |
| build.log | β | 338 B | view raw | |
| env.txt | β | 319 B | view raw | |
| fix.diff | β | 1.9 KB | view raw | |
| README.md | β | 2.0 KB | β raw | |
| VERDICT.md | β | 2.5 KB | β raw | |
| verdict.json | β | 4.1 KB | view raw | |
| fix_build.log | β | 1.4 KB | view raw | |
| fix_run.log | β | 286 B | view raw | |
| manifest.json | β | 1.3 KB | view raw |
DF-2635 β iroot meta.inum desync walks off the inum hash in hammer2_inode_drop
What
hammer2_inode_get(pmp, NULL, 1, -1) hashes the freshly-created PFS root
inode under inumhash(pmp, 1) (hammer2_inode.c:933, 954-977). Afterwards,
two mount paths overwrite the inode's meta wholesale from media:
hammer2_pfsalloc:iroot->meta = ripdata->meta;(hammer2_vfsops.c:456)hammer2_vfs_root:pmp->iroot->meta = *meta;(hammer2_vfsops.c:1975)
If the on-media PFS-root meta.inum is not 1 (crafted or corrupted image),
the in-memory inode now sits on the wrong inum-hash bucket. At unmount,
hammer2_inode_drop() computes the bucket from the current
ip->meta.inum (hammer2_inode.c:617) and walks the wrong list:
xipp = &hash->base;
while (*xipp != ip) /* hammer2_inode.c:624 */
xipp = &(*xipp)->next;
next is the first field of struct hammer2_inode, so once the walk falls
off the end, xipp degenerates to NULL and *xipp reads address 0 β
NULL-deref panic during umount (the drop is reached from
hammer2_vfsops.c:707-711 where unmount drops the last iroot ref).
Secondary effect: hammer2_inode_lookup(pmp, 1) can no longer find iroot
(its meta.inum no longer matches what lookups compare) while lookups of the
media inum find nothing either (iroot is on bucket 1) β in-memory inode
aliasing / inconsistent namespace semantics until the wedge.
Reproduce (guest, root)
python3 forge_2635.py h2base.img craft2635.img # host: inum 1 -> 0x42 scp craft2635.img dfbsd:/root/poc/df2635/ sh run.sh
run.sh: vnconfig + mount_hammer2 /dev/vn1@testvol /mnt/h2635 (RW),
ls, write+cat a file, sync, then umount.
Expected
Mount/ls/write/sync all succeed; umount panics:
Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 Stopped at hammer2_inode_drop+0x1e3: movq (%rax),%rdx (from umount)
Observed verbatim on the stock kernel β see panic.txt / run.log.
Fix
Keep the hashed inum stable across the meta overwrites and harden the drop-side walk β see fix.diff.
DF-2635 β VERDICT
Status: reproduced (impact: panic, confidence: certain).
Narrative
Forged image identical to DF-2636's recipe except the flipped field is
meta.inum (inode block +0x58), changed from 1 to 0x42 β type stays 1
(DIRECTORY) so the mount is fully functional.
Guest run (stock INVARIANTS kernel, DragonFly 6.5-DEVELOPMENT #0):
mount_hammer2 /dev/vn1@testvol /mnt/h2635β succeeded (console:hammer2_mount: ... rdonly=0,no recovery needed).ls -la /mnt/h2635,echo hello > /mnt/h2635/f,cat,syncβ all succeeded (the RW mount is fully usable; iroot's wrong meta.inum is not consulted on these paths).umount /mnt/h2635β panic, exactly the predicted walk-off:
Fatal user address access from kernel mode from umount at ffffffff80960353 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 instruction pointer = 0x8:0xffffffff80960353 current process = 852 (umount) Stopped at hammer2_inode_drop+0x1e3: movq (%rax),%rdx
fault virtual address = 0x0 confirms the degeneration:
struct hammer2_inode.next is at offset 0, so the runaway walk
xipp = &(*xipp)->next collapses to NULL and the loop condition *xipp
reads address 0. The trap is inside hammer2_inode_drop called from the
umount path β the exact code at sys/vfs/hammer2/hammer2_inode.c:624-625,
with the wrong bucket chosen at :617 because iroot->meta was overwritten
post-hash by hammer2_vfsops.c:456 (iroot->meta = ripdata->meta).
Pre-conditions / trust boundary
Crafted or checksum-forged hammer2 image (mount requires root β same boundary as DF-0763/DF-2616/DF-2624 family). All media bytes are otherwise structurally valid; only meta.inum != 1 on the PFS root inode.
Exploit ceiling
Controlled NULL-pointer read panic (DoS) at unmount, plus in-memory inode aliasing/lookup inconsistency for the mount's lifetime (iroot unreachable by inum; a second inode for the same media chain can be created by inum-based lookups such as nlookupdotdot). No write primitive.
Fix validation
fix.diff (a) preserves meta.inum = 1 across both wholesale meta
overwrites in hammer2_vfsops.c (the inode is hashed under 1), and
(b) hardens hammer2_inode_drop's removal walk against falling off the
list (while (*xipp && *xipp != ip) + warn on desync). Single-fix kernel
built with make nativekernel, installed, rebooted: the same forged image
mounts, is usable, and unmounts cleanly (fix_run.log) while the
baseline panicked in the same session.
Fix verification
fixedfix.diff (preserve meta.inum=1 across the two wholesale iroot->meta overwrites in hammer2_vfsops.c + harden hammer2_inode_drop's bucket walk with 'while (xipp && xipp != ip)') applied to the guest /usr/src, kernel rebuilt with 'make nativekernel' + installkernel, rebooted into kernel #1. Re-running the exact PoC: forged inum=0x42 image mounts RW, ls/write/cat/sync all work, and 'umount' completes cleanly (UMOUNT_RC=0) where the stock kernel panicked with Fatal trap 12 in hammer2_inode_drop. Guest stayed up (fix_run.log). Baseline panic confirmed on stock kernel #0 in the same session.
fix_build.log (install completed), fix_run.log (MOUNT_RC=0, write+cat 'hello', sync, UMOUNT_RC=0, no panic, vm up)
Confirmed kernel references
Detail
Evidence (decisive lines)
panic.txt lines 40-59 (Fatal trap 12, fault VA 0x0, Stopped at hammer2_inode_drop+0x1e3, current process umount), run.log (successful RW mount + 'no recovery needed' then the trap), forge_2635.py, fix.diff, fix_run.log (patched kernel unmounts cleanly)
PoC changes
Cribbed the DF-2616/DF-2624 forger harness; targeted meta.inum at inode block +0x58 of the 'testvol' PFS root; kept meta.type=1 so the mount is fully functional (RW) up to the unmount panic.
Verified recommended fix
Preserve meta.inum=1 across the two wholesale meta overwrites in hammer2_vfsops.c and harden hammer2_inode_drop's removal walk against falling off the list (see fix.diff).
Verdict
Reproduced on the stock INVARIANTS guest kernel. Forged image flips the PFS-root meta.inum from 1 to 0x42 (inode block +0x58). Mount (RW), ls, write, read, sync all succeed -- but the in-memory iroot was hashed under inum=1 at hammer2_inode_get (inode.c:933,954-977) and its meta was then wholesale overwritten from media (vfsops.c:456 pfsalloc, vfsops.c:1975 vfs_root), desyncing it from its hash bucket. umount drops the last iroot ref (vfsops.c:707-711), hammer2_inode_drop computes the bucket from the CURRENT meta.inum (inode.c:617) and the removal walk (inode.c:624-625) runs off the wrong bucket's list: 'Fatal trap 12: page fault while in kernel mode, fault virtual address = 0x0, Stopped at hammer2_inode_drop+0x1e3: movq (%rax),%rdx', current process = umount. Because struct hammer2_inode's first field is 'next', the runaway xipp degenerates to NULL exactly as predicted. Secondary impact: inum-based lookups (nlookupdotdot, vget) cannot find iroot for the mount's lifetime and can spawn duplicate in-memory inodes for the same media chain. Controlled NULL-read panic (DoS) + aliasing from a crafted image; mount requires root.
No comments yet.