Pseudo-Filesystem¶
NFSv4 replaces the MOUNT EXPORT mechanism with a pseudo-filesystem: a server-constructed virtual namespace that bridges all exports into a single tree rooted at a well-known handle (RFC 7530 sec. 7.3). Clients navigate from the pseudo-root into any export using standard LOOKUP operations, with no separate discovery protocol.
How the pseudo-FS works¶
On v3, exports were isolated islands. Each had to be discovered via MOUNT EXPORT and individually mounted via MOUNT MNT. There was no structural relationship between exports; the server presented them as independent paths.
NFSv4 changes this by constructing a synthetic directory tree that connects all exports. The server creates intermediate directories (pseudo-FS entries) to bridge any gaps in the pathname hierarchy between exports.
Example¶
Given these server exports:
The server constructs this pseudo-FS tree:
/ (pseudo-root)
├── srv/ ← pseudo-FS directory
│ └── nfs/ ← pseudo-FS directory
│ ├── public/ ← junction → real filesystem
│ └── internal/ ← junction → real filesystem
└── data/ ← pseudo-FS directory
└── backups/ ← junction → real filesystem
The directories /, /srv, /srv/nfs, and /data are synthetic pseudo-FS entries. They exist only to provide a traversable path from the pseudo-root to each export. The actual exports (public, internal, backups) are junction points where the pseudo-FS crosses into real exported filesystems.
Accessing the pseudo-root¶
The PUTROOTFH operation (op 24, RFC 7530 sec. 16.22) sets the current filehandle to the pseudo-root. No authentication check, no export path, no prior state required. Every v4 server must support it.
This is the v4 equivalent of MOUNT MNT, except there is no access control at this stage. The handle points to the virtual namespace root, not to any real filesystem.
Identifying the pseudo-root¶
Linux knfsd uses a well-known fsid for the pseudo-root: 39c6b5c1-3f24-4f4e-977c-7fe6546b8a25. nfswolf detects the pseudo-root by checking the fsid from GETATTR against this UUID. When the fsid matches, the current directory is the pseudo-root, not a real export.
Other NFS server implementations use different pseudo-root identifiers, but the general principle is the same: the pseudo-root has a unique fsid that differs from all real filesystems.
Browsing exports¶
From the pseudo-root, READDIR lists the top-level directories, and successive LOOKUPs navigate down to export junction points:
COMPOUND([PUTROOTFH, READDIR])
→ ["srv", "data"] (top-level pseudo-FS entries)
COMPOUND([PUTROOTFH, LOOKUP("srv"), LOOKUP("nfs"), READDIR])
→ ["public", "internal"] (exports under /srv/nfs)
This replaces the MOUNT EXPORT procedure entirely. On v3, the client had to call MOUNT EXPORT to get a list of paths, then MOUNT MNT for each one. On v4, one READDIR chain discovers everything.
Pseudo-FS leaks export names (F-5.5)
The pseudo-FS exposes the names and directory structure of all exports to any client that can reach port 2049, even if the client lacks access to the exports themselves. The server returns directory entries for the pseudo-FS tree without checking whether the client is authorized to access the underlying exports. This is finding F-5.5.
Export boundaries and fsid¶
The client detects export boundaries by monitoring the fsid attribute during LOOKUP traversal (RFC 7530 sec. 7.7). When the fsid returned by GETATTR changes between a parent directory and a child entry, the client has crossed from the pseudo-FS (or one export) into a different export.
COMPOUND([PUTROOTFH, GETATTR(fsid)])
→ fsid = A (pseudo-FS)
COMPOUND([PUTROOTFH, LOOKUP("srv"), LOOKUP("nfs"), LOOKUP("public"), GETATTR(fsid)])
→ fsid = B (real filesystem -- different from A)
The fsid change from A to B indicates that /srv/nfs/public is a mount point crossing into a real filesystem. The mounted_on_fileid attribute provides the fileid of the junction directory in the parent filesystem, allowing the client to distinguish the junction entry from the real filesystem root.
Multiple pseudo-FS segments¶
When exports are separated by non-exported directories, the server may create multiple pseudo-FS segments, each with a unique fsid (RFC 7530 sec. 7.3):
/a pseudo-FS (fsid = P1)
/a/b real FS (fsid = R1)
/a/b/c pseudo-FS (fsid = P2) -- gap between exports
/a/b/c/d real FS (fsid = R2)
Each pseudo-FS segment is a separate entity. The path /a/b/c is a pseudo-FS bridge that exists only because /a/b/c/d is exported and /a/b/c itself is not. The fsid changes at each boundary.
LOOKUPP traversal¶
LOOKUPP (op 16, RFC 7530 sec. 16.14) resolves the parent directory of the current filehandle. Unlike NFSv3's LOOKUP(".."), which depends on server interpretation, LOOKUPP is a protocol-defined operation with guaranteed semantics.
Upward traversal from export to pseudo-root¶
LOOKUPP can traverse upward from an export's root directory into the pseudo-FS:
COMPOUND([PUTFH(export_root_fh), LOOKUPP, GETFH])
→ parent directory handle in the pseudo-FS
COMPOUND([PUTFH(parent_fh), LOOKUPP, GETFH])
→ grandparent handle (climbing toward pseudo-root)
This traversal continues until reaching the pseudo-root, where LOOKUPP returns NFS4ERR_NOENT (no parent above the root).
Cross-export lateral movement (F-2.12)¶
LOOKUPP enables a critical attack path: a client with access to any one export can traverse up to the pseudo-root junction, then LOOKUP down into sibling exports, even those with different IP-based access restrictions.
graph TD
PR["/ (pseudo-root)"] --> SRV["srv/"]
SRV --> NFS["nfs/"]
NFS --> PUB["public/ *(rw)"]
NFS --> INT["internal/ 10.0.0.0/24(rw)"]
NFS --> SEC["secrets/ 10.0.0.50(rw)"]
style PUB fill:#4caf50,color:#fff
style INT fill:#ff9800,color:#fff
style SEC fill:#f44336,color:#fff
style PR fill:#9e9e9e,color:#fff
style SRV fill:#9e9e9e,color:#fff
style NFS fill:#9e9e9e,color:#fff
An attacker with access to public (open to everyone) can:
- LOOKUPP from
publicto reach/srv/nfsin the pseudo-FS - LOOKUP from
/srv/nfstointernalorsecrets - Access files in the restricted exports
The server does not enforce per-export access controls on LOOKUPP/LOOKUP traversal through the pseudo-FS tree. This is fundamentally different from F-2.8 (sibling export lateral access via handle reuse) because no handle construction or filesystem escape is required; the client uses standard NFSv4 directory operations.
nfswolf's exports shell command uses this technique to discover sibling exports from any v4 session.
Filehandle volatility¶
Pseudo-FS entries may have volatile filehandles (RFC 7530 sec. 7.5). Unlike real filesystem handles, which are typically persistent (surviving server restarts), pseudo-FS handles are dynamically generated and may change when the server restarts or its export configuration changes.
The fh_expire_type attribute indicates whether a handle is persistent or volatile. When a volatile handle expires, the server returns NFS4ERR_FHEXPIRED, and the client must re-resolve the path using LOOKUP from a known handle.
Volatile handles and brute-forcing
The potential for volatile pseudo-FS handles is one reason handle brute-forcing is less effective on NFSv4. A handle that was valid during one probing session may become invalid on the next server restart, and the server's error response may not provide the clean STALE/BADHANDLE oracle that makes v3 brute-forcing efficient.
nfswolf implementation¶
nfswolf interacts with the pseudo-FS in several contexts:
| Component | How it uses the pseudo-FS |
|---|---|
Scanner (probe_nfs4()) |
COMPOUND([PUTROOTFH]) to detect v4 support |
| Scanner (data collection) | READDIR on pseudo-root to enumerate exports |
Shell (V4Ops) |
PUTROOTFH + LOOKUP chain for path resolution |
Shell (exports command) |
LOOKUPP traversal for sibling export discovery (F-2.12) |
Escape engine (Nfs4EscapeProbe) |
PUTROOTFH + LOOKUP to acquire seed handles |
| Analyzer | SECINFO per export path for auth flavor probing |
| Pseudo-root detection | fsid check against 39c6b5c1-3f24-4f4e-977c-7fe6546b8a25 |