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

```c
	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.
