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

Unvalidated vol0_next_tid at mount: deterministic kernel panic on first write (TID exhaustion) and silent TID wraparound that leaks new records into every historical snapshot

Summary

hammer_vfs_mount() copies the on-disk root volume header's vol0_next_tid into hmp->next_tid with no validation (:731). hammer_alloc_tid() increments it with zero wrap handling and its only guard is a post-increment hpanic. Because vol_crc is never verified (DF-3042), an 8-byte header patch fully controls this state: 0xFFFFFFFFFF000000 makes the FIRST file creation after a clean-looking RW mount panic ('Ran out of TIDs!'); 0xFFFFFFFFFFFFFFFF wraps the counter to 0 (default master_id=0 path: (next+16)&~15), after which all new records carry create_tid ~0 and sort before every real record (fs TID base 0x100000000) - a file created AFTER a synctid T1 appears inside the mount -T T1 as-of view; no error reported anywhere in the wrap case. VERIFIED guest: panic variant (echo hello > /mnt/x -> panic: hammer_alloc_tid: Ran out of TIDs!, backtrace hammer_alloc_objid<-hammer_create_inode<-hammer_vop_ncreate); wrap variant (write succeeds, create_tid 0x8010, mount -T <pre-wrap synctid> lists the post-wrap file). Fix validated in-guest (mount-time ERANGE rejection >= 0xFFFFFFFFFF000000): both variants rejected, stock images mount/write/umount normally.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3095 Β· 12 files
FileTypeDescriptionSize
tid_craft.c β€” 3.1 KB view raw
build.sh β€” 66 B view raw
run.sh β€” 178 B view raw
fix_run.sh β€” 1.4 KB view raw
fix.diff β€” 1017 B view raw
build.log β€” 663 B view raw
run.log β€” 2.4 KB view raw
fix_run.log β€” 1.1 KB view raw
panic.txt β€” 1.1 KB view raw
env.txt β€” 378 B view raw
VERDICT.md β€” 4.3 KB ↓ raw
verdict.json β€” 4.4 KB view raw
VERDICT.md
↓ download raw

DF-3095 verdict β€” REPRODUCED (panic/DoS + snapshot-isolation break)

What was claimed

hammer_vfs_mount() copies rootvol->ondisk->vol0_next_tid into hmp->next_tid (sys/vfs/hammer/hammer_vfsops.c:731) without any validation. hammer_alloc_tid() (sys/vfs/hammer/hammer_transaction.c:162-181) increments that value with no wrap handling:

    if (hmp->master_id < 0) {
        tid = hmp->next_tid + 1;
        hmp->next_tid = tid + count;
    } else {
        tid = (hmp->next_tid + HAMMER_MAX_MASTERS) &
              ~(hammer_tid_t)(HAMMER_MAX_MASTERS - 1);
        hmp->next_tid = tid + count * HAMMER_MAX_MASTERS;
        tid |= hmp->master_id;
    }
    if (tid >= 0xFFFFFFFFFF000000ULL)
        hpanic("Ran out of TIDs!");

Default mounts have master_id = 0 (hammer_vfsops.c:396), i.e. the second branch. Since vol_crc is never verified (DF-3042), an 8-byte patch to the volume header controls this state fully.

How it was verified (guest, stock INVARIANTS kernel #0/#5, X86_64_GENERIC)

Variant 1 β€” panic (reproduced 1/1)

  • tid_craft panic β†’ vol0_next_tid = 0xFFFFFFFFFF000000 (header offset 0xF8/decimal 248, confirmed by offsetof against the real hammer_disk.h).
  • mount_hammer /dev/vn0 /mnt succeeds ("mounted clean, no recovery needed").
  • echo hello > /mnt/x β†’ panic: hammer_alloc_tid: Ran out of TIDs!

Backtrace (panic.txt):

panic: hammer_alloc_tid: Ran out of TIDs!
hammer_alloc_tid() at hammer_alloc_tid+0xa7
hammer_alloc_objid() at hammer_alloc_objid+0x246
hammer_create_inode() at hammer_create_inode+0x365
hammer_vop_ncreate() at hammer_vop_ncreate+0xa8
vop_ncreate() at vop_ncreate+0x5b

The trigger is an ordinary open(O_CREAT) β€” reachable by any user with write permission on any directory of the mounted filesystem after a privileged mount of suspect media.

Variant 2 β€” wrap to 0 (reproduced 1/1, no panic, silent)

  • tid_craft wrap β†’ vol0_next_tid = 0xFFFFFFFFFFFFFFFF.
  • Mount RW + write: no error anywhere. hammer history /mnt/b shows create_tid = 0x0000000000008010 on a filesystem whose TID space starts at 0x0000000100000000 (newfs sets next_tid to 1<<32).
  • Snapshot leak proof: with T1 = hammer synctid = 0x0000000100008080 taken when only file a existed, then wrapping the (unmounted, re-mounted RW) image and creating file b (create_tid 0x8010), mount_hammer -T 0x0000000100008080 shows b inside the T1 snapshot (ls -l in run.log: a AND b, cat /mnt/b β†’ BBB). Btree visibility is create_tid <= asof < delete_tid, so post-wrap records are visible in every historical asof view/snapshot of the filesystem, and sort before all real records for mirror/prune semantics.

Impact

  • Panic variant: deterministic kernel DoS (mount of crafted media + any subsequent create; the mount itself reports success).
  • Wrap variant: silent corruption of HAMMER's temporal model β€” new records leak into all snapshots (confidentiality: snapshot consumers see data that post-dates the snapshot), mirroring/pruning TID ordering assumptions break.
  • Preconditions: privileged mount of attacker-crafted media (root or physical access), matching the DF-3040/3067/3068 family bar.

Fix validation

fix.diff (authored against the read-only sys/ tree, git-apply clean) rejects vol0_next_tid >= 0xFFFFFFFFFF000000ULL at mount time (ERANGE, alongside the existing vol_version check at hammer_vfsops.c:677-683). Applied to the guest's /usr/src copy, make nativekernel KERNCONF=X86_64_GENERIC, reboot:

  • baseline (stock): panic variant β†’ panic (above).
  • patched kernel: both panic and wrap variants β†’ mount: Result too large, console mount rejected vol0_next_tid ffffffffff000000, guest stays up (fix_run.log).
  • regression: stock newfs_hammer image mounts RW, file create/sync/umount normal on the patched kernel (fix_run.log).

Not this bug

vol0_next_tid write-back during flusher finalize is a different sink; only the mount-time load is addressed here (the MNT_UPDATE path never reloads the field).

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src, make nativekernel + installkernel, reboot into #1: panic variant -> mount rejected (ERANGE), guest up; wrap variant -> mount rejected; stock newfs_hammer image -> mount RW, root+user writes, sync, umount all clean.

findings/poc/DF-3095/fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Sun Sep 6 07:33:35 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Exploit chain

crafted HAMMER1 media (8-byte header patch, no CRC fixup needed per DF-3042) -> privileged mount_hammer RW succeeds cleanly -> any user with write access to any directory on the mount creates a file -> [panic variant] kernel panic 'Ran out of TIDs!' (deterministic DoS); [wrap variant] silent TID-space wrap -> new records visible in every snapshot/asof view of the filesystem (temporal isolation break).

Evidence (decisive lines)

['findings/poc/DF-3095/panic.txt - console panic + backtrace (panic variant)', 'findings/poc/DF-3095/run.log - both variants + the -T asof snapshot-leak proof (ls shows b inside the pre-b snapshot)', 'findings/poc/DF-3095/fix_run.log - patched kernel: both mounts rejected ERANGE, stock-image regression clean', 'findings/poc/DF-3095/fix.diff - mount-time validation of vol0_next_tid']

PoC changes

Wrote tid_craft.c fresh (no prior seed): 8-byte patch of vol0_next_tid at header offset 0xF8 (248, verified by offsetof against hammer_disk.h); no CRC fixup required. Discovered during verification that mount_hammer's as-of option is -T (not -o asof=) and that 'hammer synctid ' prints a bare 0xTID line; also that default mounts use master_id=0, so the wrap goes through the (next+16)&~15 branch.

Verified recommended fix

Validate vol0_next_tid at mount: reject vol0_next_tid >= 0xFFFFFFFFFF000000ULL with ERANGE (fix.diff, validated on the guest)

Verdict

REPRODUCED on the stock INVARIANTS guest. hammer_vfs_mount() loads vol0_next_tid into hmp->next_tid without validation (hammer_vfsops.c:731); hammer_alloc_tid() (hammer_transaction.c:162-181) has no wrap handling and can only hpanic. Variant 1 (next_tid=0xFFFFFFFFFF000000): RW mount succeeds, the first ordinary file creation panics the kernel ('panic: hammer_alloc_tid: Ran out of TIDs!', backtrace hammer_alloc_objid<-hammer_create_inode<-hammer_vop_ncreate; panic.txt). Variant 2 (next_tid=0xFFFFFFFFFFFFFFFF): no panic, no error - with the default master_id=0 the 16-alignment add wraps the TID counter to 0 and post-wrap records carry create_tid ~0x8010 on a filesystem whose TID space starts at 0x100000000; demonstrated snapshot-isolation break by mounting -T and observing b inside the historical view (run.log). Fix validated: patched kernel rejects both variants at mount (ERANGE 'mount: Result too large') and a stock image mounts/operates/umounts normally (fix_run.log).