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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3097 Β· 3 files
FileTypeDescriptionSize
VERDICT.md β€” 4.0 KB ↓ raw
verdict.json β€” 2.1 KB view raw
manifest.json β€” 360 B view raw
VERDICT.md
↓ download 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_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.)

Fix verification

not_testable
per-fix-DF-3097

Confirmed 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.