# DF-3097 — HAMMER1 NFS file handles carry no entropy: forgeable fhs give arbitrary as-of (historical) inode access and cross-PFS reach on exported mounts

**Target:** `sys/vfs/hammer/hammer_vfsops.c:1083-1141`
(`hammer_vfs_vptofp`/`hammer_vfs_fhtovp`).

## Mechanism

`vptofp` encodes the entire handle as three *predictable* fields:

```c
1090:	fhp->fid_len = offsetof(struct fid, fid_data[16]);	/* fixed 20 */
1091:	fhp->fid_ext = lo_to_pfs(ip->obj_localization);	/* PFS id 0..65535 */
1092:	bcopy(&ip->obj_id, fhp->fid_data + 0, 8);		/* sequential */
1093:	bcopy(&ip->obj_asof, fhp->fid_data + 8, 8);		/* constant for live files */
```

`fhtovp` trusts all three fields from the wire with **no validation
whatsoever** — not even `fid_len`:

```c
1115:	bcopy(fhp->fid_data + 0, &info.obj_id, 8);
1116:	bcopy(fhp->fid_data + 8, &info.obj_asof, 8);
1117:	if (rootvp) localization = VTOI(rootvp)->obj_localization;
1119:	else        localization = pfs_to_lo(fhp->fid_ext);
1130:	ip = hammer_get_inode(&trans, NULL, info.obj_id,
1131:				info.obj_asof, localization, 0, &error);
```

* `obj_id` is allocated sequentially (`hammer_alloc_objid`,
  hammer_transaction.c:184+) — guessable in O(#inodes) tries.
* `obj_asof` for a live file is `HAMMER_MAX_TID` (constant), so there
  is no per-inode generation/entropy secret (contrast UFS's 32-bit
  random `gen` in its fid — the classic mitigation for NFS fh
  guessing).
* `obj_asof` is honored verbatim: putting any historical TID in the fh
  makes `hammer_get_inode` return the inode **as of that transaction**
  (cursor.asof / HAMMER_CURSOR_ASOF), i.e. a forged fh yields any
  historical version of any file — including versions a snapshot
  policy never exposed, and content that was later deleted
  (delete_tid > asof while asof < now).
* `fid_ext` selects the PFS freely when the hammer mount is exported
  directly (the `rootvp` nullfs-isolation argument only applies when
  the export goes through the nullfs layer; `nfsrv_fhtovp` calls
  `VFS_FHTOVP(mp, NULL, ...)` — nfs_subs.c:1243).

## Impact

On a NFS-exported HAMMER1 mount, any client permitted to talk to the
server can, without ever seeing a legitimate fh for the target,
fabricate handles to (a) enumerate every inode in the mount, (b) read
**any historical version** of any inode by TID, and (c) reach inodes
in any PFS of the filesystem via `fid_ext` regardless of the nullfs
export topology the operator configured. This is the classic
NFS-fh-guessing exposure class (the missing `gen`-style secret is the
defect; the unvalidated `asof` turns it into a time machine).

## Preconditions

The hammer mount itself must be NFS-exported (mountd exports are
per-mount). Exports deliberately restricted to a nullfs PFS view are
not affected (the raw hammer mount answers EACCES in CHECKEXP) — the
finding bites when the hammer mount is exported directly, which is the
common single-PFS-does-everything setup.

## Status: untested

Full verification requires an NFS server + client pair against a
hammer1 export on the guest; the fh semantics above are taken directly
from the code paths cited (mechanism: certain; exposure: depends on
export configuration). Not exercised this run — recorded for triage.

## Suggested fix

Mix non-guessable entropy into the handle and validate on decode:

* fold a per-mount random 32-bit secret (or the inode's on-disk
  `crc` field) into the fh (e.g. xor into `obj_asof`'s high bits),
  verify on fhtovp;
* reject `fid_len != 20`;
* cap `obj_asof` to the mount's `hmp->asof` (a snapshot-export
  capability should be an explicit export flag, not a wire 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.

(No fix.diff for this item: documented recommendation only - see Fix above.)
