Skip to content

F-2.10: SIGN_FH Root Handle Exemption (FILEID_ROOT MAC Bypass)

Classification

  • Severity: Medium
  • CVSS Vector: Network / Low Complexity / No Auth Required
  • Affected Versions: Linux knfsd with sign_fh enabled (kernel 6.x+)
  • RFC Reference: None (implementation-specific; sign_fh is a Linux-only extension)
  • Prerequisite: Export has sign_fh enabled with a valid fh_key
  • Kernel Code: fs/nfsd/nfsfh.c:nfsd_set_fh_dentry():294-296

Summary

Linux knfsd's sign_fh feature adds an HMAC to file handles, preventing handle construction and handle guessing attacks (F-2.1, F-2.2). However, the implementation explicitly skips MAC verification for FILEID_ROOT handles (fileid_type=0x00): the kernel comment reads "We don't sign or verify the root, no per-file identity." An attacker who knows the export's fsid can construct a valid root handle {version=1, auth_type=0, fsid_type=N, fileid_type=0, fsid=known_value} with no MAC, bypassing MOUNT ACLs even when sign_fh is deployed. From the root handle, LOOKUP returns legitimately signed child handles, granting access to the full export tree.

Technical detail

sign_fh background

The sign_fh export option was introduced to address the handle-as-bearer-token problem described in RFC 2623 S2.6. When enabled, knfsd appends an HMAC-SHA256 (truncated) to each file handle using a server-held fh_key. The MAC covers the fsid, fileid, and generation fields, preventing an attacker from constructing handles for arbitrary inodes.

The root exemption

In fs/nfsd/nfsfh.c, the function nfsd_set_fh_dentry() validates incoming file handles:

/* Line 294-296 */
if (fh->fh_fileid_type == FILEID_ROOT) {
    /* We don't sign or verify the root, no per-file identity */
    goto skip_mac_check;
}

The rationale is that FILEID_ROOT (0x00) identifies the filesystem root directory, which has no inode-specific identity embedded in the handle. Since there is nothing file-specific to MAC, the implementation skips both signing (on handle creation) and verification (on handle use).

Handle construction

A FILEID_ROOT handle has a minimal structure:

Byte 0:    fh_version    = 0x01 (knfsd v1)
Byte 1:    fh_auth_type  = 0x00
Byte 2:    fh_fsid_type  = N    (from the export's known fsid encoding)
Byte 3:    fh_fileid_type = 0x00 (FILEID_ROOT)
Bytes 4+:  fh_fsid       = <fsid value> (length determined by fsid_type)
           (no fileid, no MAC)

The fsid is obtainable from:

  1. MOUNT MNT reply: The root file handle returned by MOUNT contains the fsid in bytes 4 through 4+fsid_len. Even if the attacker cannot MOUNT (IP-based ACL denial), a prior successful MOUNT from another client may have leaked the handle (see F-3.6).
  2. NFSv3 GETATTR: The fattr3.fsid field in any GETATTR response reveals the kernel-level fsid (dev major:minor), which can be mapped to the handle's fh_fsid encoding.
  3. NFSv4 GETATTR: The FATTR4_MOUNTED_ON_FILEID and FATTR4_FSID attributes expose filesystem identity.

Attack flow

1. Obtain the export's fsid (from a prior MOUNT, GETATTR, or a sibling export on the same FS)
2. Determine the fsid_type and fsid_len from the handle header (or enumerate: 0-7)
3. Construct: [01 00 <fsid_type> 00 <fsid bytes>]  -- fileid_type=0, no MAC
4. Send GETATTR(constructed_handle) -> NFS3_OK (root attributes returned)
5. LOOKUP(constructed_handle, "etc") -> signed child handle for /etc
6. LOOKUP(etc_handle, "shadow") -> signed child handle for /etc/shadow
7. READ(shadow_handle) -> file contents (with appropriate uid/gid credential)

Step 4 succeeds because the MAC check is skipped for FILEID_ROOT. Steps 5-6 return legitimately signed handles because the server generates the MAC when creating LOOKUP response handles. The attacker now holds valid, signed handles for child files.

Scope of the bypass

The root exemption has a precise scope:

What the attacker can do What the attacker cannot do
Construct a root handle for any export whose fsid is known Construct handles for non-root inodes (MAC required)
Bypass MOUNT IP-based ACLs using --handle Forge a handle for a specific file without LOOKUP traversal
Obtain signed child handles via LOOKUP from the root Access files on a different filesystem (fsid mismatch)
Enumerate the full export tree from root down Skip the LOOKUP chain (each child handle is signed)

This is strictly weaker than F-2.1 (full export escape), which constructs handles for arbitrary inodes. With sign_fh, the attacker is limited to the export root and must LOOKUP through the tree, which means subtree_check (if enabled) could block access to files outside the exported subtree. Without subtree_check (the default), the entire filesystem is reachable through LOOKUP traversal from the root.

Interaction with MOUNT ACLs

The primary value of this bypass is circumventing MOUNT-level access controls. If an export is restricted to specific client IPs:

/srv/share  10.0.0.0/24(rw,sign_fh)

An attacker outside 10.0.0.0/24 cannot MOUNT the export. But if they know the fsid (obtained from a sibling export on the same filesystem, or from a prior network capture), they can construct a FILEID_ROOT handle and send NFS operations directly to port 2049, bypassing the MOUNT ACL entirely. This is the same attack vector as F-2.7 (nfsd ACL blindness), but sign_fh was supposed to prevent it.

Impact

  • MOUNT ACL bypass: The primary deployment goal of sign_fh (preventing unauthorized handle use) is defeated for the export root
  • False sense of security: Administrators who enable sign_fh may believe handle construction is fully prevented
  • Full export tree access: From the unsigned root handle, LOOKUP traversal reaches every file in the export (or the entire filesystem if no_subtree_check is set)
  • Defense-in-depth failure: sign_fh + IP-based MOUNT ACLs are both bypassed simultaneously

Detection (nfswolf)

nfswolf detects this finding through handle analysis in src/engine/file_handle.rs:

  1. Fingerprint sign_fh: If file handles from READDIRPLUS contain a MAC suffix (handle length exceeds the expected fsid+fileid size), the export likely has sign_fh enabled.
  2. Construct root probe: Build a FILEID_ROOT handle (fileid_type=0x00) using the known fsid and send GETATTR.
  3. Confirm bypass: If GETATTR succeeds on the constructed root handle while non-root constructed handles fail with NFS3ERR_BADHANDLE, the finding is confirmed.

The shell --handle <hex> command can be used to interactively demonstrate the bypass.

Remediation

  1. Enable subtree_check alongside sign_fh to limit LOOKUP traversal to the exported subtree even if the root handle is constructed:

    /srv/share  client(rw,sign_fh,subtree_check)
    

  2. Firewall NFS port 2049 to the same IP ranges as the MOUNT ACL, preventing direct NFS calls from unauthorized clients

  3. Use Kerberos (sec=krb5). Even with a valid handle, operations require a Kerberos ticket, which the attacker cannot forge

  4. Use NFSv4 with sec=krb5 -- eliminates the MOUNT protocol entirely and enforces per-operation authentication

  5. Kernel patch: The root exemption could be fixed by signing FILEID_ROOT handles over the fsid alone (no fileid needed). This is a straightforward change to nfsd_set_fh_dentry() that preserves the "no per-file identity" property while still binding the handle to the server's key.

Finding Relationship
F-2.1: Export Escape Full inode-level handle construction; sign_fh blocks F-2.1 but not F-2.10
F-2.2: File Handle Guessing Brute-force handle construction; sign_fh blocks F-2.2 but not F-2.10
F-2.3: Windows Handle Signing Windows NFS handle MAC; different implementation, no known root exemption
F-2.7: NFS Daemon ACL Blindness Direct NFS calls bypass MOUNT ACLs; sign_fh was meant to prevent this
F-2.9: WebNFS Public Handle Another MOUNT bypass via well-known handle construction