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)
PoC verification
Evidence pack
findings/poc/DF-3095 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 realhammer_disk.h).mount_hammer /dev/vn0 /mntsucceeds ("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/bshowscreate_tid = 0x0000000000008010on a filesystem whose TID space starts at0x0000000100000000(newfs sets next_tid to1<<32). - Snapshot leak proof: with T1 =
hammer synctid=0x0000000100008080taken when only fileaexisted, then wrapping the (unmounted, re-mounted RW) image and creating fileb(create_tid0x8010),mount_hammer -T 0x0000000100008080showsbinside the T1 snapshot (ls -lin run.log:aANDb,cat /mnt/bβBBB). Btree visibility iscreate_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
panicandwrapvariants βmount: Result too large, consolemount rejected vol0_next_tid ffffffffff000000, guest stays up (fix_run.log). - regression: stock
newfs_hammerimage 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
fixedfix.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
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
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
No comments yet.