HAMMER1 NFS file handles carry zero entropy and fhtovp honors an attacker-chosen as-of TID: forged handles yield historical-version reads and cross-PFS reach on exported mounts
Summary
hammer_vfs_vptofp (:1083-1095) builds the entire file handle from three predictable fields - sequential obj_id, constant obj_asof (HAMMER_MAX_TID for live files), small PFS id in fid_ext - with no per-inode generation secret (contrast UFS's random gen field). hammer_vfs_fhtovp (:1104-1141) validates nothing from the wire: no fid_len check, fid_ext maps straight to a PFS localization (:1120), and the attacker-chosen obj_asof (:1116) is passed verbatim to hammer_get_inode (:1130-1131), returning the inode as of ANY historical transaction. On an NFS-exported hammer1 mount, any permitted client can forge handles (root handle is obj_id=1/asof=MAX_TID/pfs=0) to enumerate inodes, read any historical/deleted version of any file, and reach other PFSs via fid_ext - bypassing snapshot policy and nullfs-based export isolation. Not executed (needs NFS rig; mechanism code-certain; recipe in pack). Fix: mix per-mount random entropy into unused fid bits + verify in fhtovp; reject fid_len mismatch; clamp obj_asof to the mount's asof.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3097 Β· 3 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | β | 4.0 KB | β raw | |
| verdict.json | β | 2.1 KB | view raw | |
| manifest.json | β | 360 B | view raw |
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:
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:
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_idis allocated sequentially (hammer_alloc_objid, hammer_transaction.c:184+) β guessable in O(#inodes) tries.obj_asoffor a live file isHAMMER_MAX_TID(constant), so there is no per-inode generation/entropy secret (contrast UFS's 32-bit randomgenin its fid β the classic mitigation for NFS fh guessing).obj_asofis honored verbatim: putting any historical TID in the fh makeshammer_get_inodereturn 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_extselects the PFS freely when the hammer mount is exported directly (therootvpnullfs-isolation argument only applies when the export goes through the nullfs layer;nfsrv_fhtovpcallsVFS_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
crcfield) into the fh (e.g. xor intoobj_asof's high bits), verify on fhtovp; - reject
fid_len != 20; - cap
obj_asofto the mount'shmp->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.)
Fix verification
not_testableConfirmed kernel references
Detail
Evidence (decisive lines)
['findings/poc/DF-3097/VERDICT.md - line-cited fh encode/decode analysis and fix sketch']
PoC changes
n/a - no PoC authored this run
Verified recommended fix
Add per-inode non-guessable entropy to the handle (e.g. xor the leaf record CRC or a per-mount secret into obj_asof high bits) and verify it in fhtovp; validate fid_len; clamp obj_asof to hmp->asof
Verdict
Not exercised on the guest (requires an NFS server + client pair against a hammer1 export). Mechanism is code-certain: hammer_vfs_vptofp (vfsops:1083-1095) builds file handles from three fully predictable fields (sequential obj_id, constant obj_asof=HAMMER_MAX_TID for live files, small PFS id in fid_ext) with zero per-inode entropy - unlike UFS's random generation number - and hammer_vfs_fhtovp (vfsops:1104-1141) validates NOTHING from the wire: not fid_len, not the PFS id (fid_ext -> pfs_to_lo, vfsops:1120), and honors an arbitrary attacker-chosen obj_asof (vfsops:1116,1130-1131) which returns the inode as of ANY historical transaction. On an NFS-exported hammer1 mount any permitted client can therefore forge handles to (a) enumerate inodes, (b) read any historical version of any file (snapshot policy bypass, deleted-content disclosure), (c) cross PFS boundaries via fid_ext.
No comments yet.