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

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).

--- 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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2635 Β· 13 files
FileTypeDescriptionSize
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.

VERDICT.md
↓ download raw

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):

  1. mount_hammer2 /dev/vn1@testvol /mnt/h2635 β€” succeeded (console: hammer2_mount: ... rdonly=0, no recovery needed).
  2. 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).
  3. 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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)
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Sat Aug 29 12:37:23 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.