Summary

  • NFS used pathnames to discover files but opaque file handles for later operations. Clients could return a handle to the server, yet could not treat its bytes as an inode, disk address or portable identity.
  • NFSv4 divided handles into persistent and volatile classes. A persistent handle should remain fixed through restart and migration for an object’s lifetime; a volatile one may expire under conditions the server discloses.
  • NFS4ERR_STALE and NFS4ERR_FHEXPIRED preserve different truths. One says the referent is gone or unavailable; the other can say that a temporary reference expired even though the object may survive. Recovery means resolving names again, not guessing what old bytes meant.

The file whose name moved

Imagine a client resolves /projects/atlas/report, receives a file handle, and begins reading. An administrator then renames report to archive. The route through the directory tree has changed, but the open work need not instantly become work on a different object. A hard link makes the distinction even clearer: two pathnames can lead to one file. The name is a route through a namespace; the file handle is the server’s reference to what was found.

That division was present in the early Network File System. RFC 1094, published for NFS version 2 in 1989, described pathname traversal one component at a time. A client obtained an initial root handle through the separate mount protocol, sent a directory handle and a component name to LOOKUP, and received another handle. Calls such as GETATTR, READ and WRITE then carried the handle rather than the full pathname.

The version 2 handle was exactly 32 bytes and explicitly opaque. “Opaque” did not mean encrypted or secret. It meant that the client had no protocol right to decode the value into a device number, inode, table slot or storage address. Only the server had to know how the reference mapped back to a file.

RFC 1813 made the version 3 handle variable in length but preserved the same allocation of knowledge. An nfs_fh3 contained whatever the server needed to distinguish an individual file. Operations including LOOKUP, CREATE and READDIRPLUS returned handles that clients could cache and present in later requests. The wire contract described the boundary; it did not freeze the server’s internal filesystem representation.

Equality without interpretation

Opacity creates a tempting shortcut. If clients cannot inspect a handle, perhaps they can at least compare its bytes. NFS permits that comparison, but gives it deliberately narrow meaning.

For two handles received from the same server, equality means the handles designate the same file. Inequality does not prove that the files differ. A server is not required to maintain a one-to-one mapping between objects and handle byte strings. RFC 1813 therefore calls comparison a performance optimisation, not a correctness mechanism. Later NFSv4 specifications retain the rule. Hard-linked names should produce the same handle, but the wider principle remains: a client cannot build its own identity system by reversing or over-interpreting server-issued bytes.

The restriction has practical force. A cache may avoid duplicate work when it sees an equal handle. It must not infer from unequal handles that two writes necessarily target different underlying objects. Nor can a handle be compared across arbitrary servers as though it were a global content identifier. The reference exists inside a server’s identity domain.

Persistence became an explicit promise

Early versions had a stale-handle result, but NFSv4 made handle lifetime a more articulate part of the contract. RFC 3010 distinguished persistent from volatile file handles; RFC 7530 and RFC 8881 preserve and refine that model.

A persistent handle is supposed to remain fixed for the lifetime of the object. Server restart does not invalidate it. Migration of the object to another server does not, under the NFSv4 promise, require the client to accept a different protocol identity. Persistence allowed clients to retain references across failures and movement without learning how storage had been rearranged.

Persistent did not mean immortal. If the object is deleted, or if its filesystem becomes unavailable, the server returns NFS4ERR_STALE. The failure is part of the persistence contract, not an exception to it: the old reference does not silently attach itself to whatever later occupies a reused internal location.

Some servers cannot uphold that invariant for every object. Hierarchical storage, migration machinery, operating-system interfaces or internal table designs may issue references whose continuity is conditional. NFSv4 calls these volatile handles and makes the server report expiry properties through fh_expire_type. Volatility is not hidden behind a fiction of permanence; it is exposed so a client can plan recovery.

The standards give an illustrative design in which a volatile handle carries a boot time, a table slot and a generation number. After a restart or slot reuse, the old generation no longer matches. That layout is only an example. The interoperability requirement is the visible behaviour, not the choice of internal fields.

Stale is not the same as expired

The split between NFS4ERR_STALE and NFS4ERR_FHEXPIRED is a compact piece of evidence design. If a server knows the referenced object has been removed, it returns stale. A persistent reference can also be stale when the filesystem it identifies is no longer available. The server is saying that the old reference no longer reaches its referent.

An expired result says something narrower. A volatile handle has lost continuity, but the underlying object may still exist. A restart may have erased the table that interpreted the reference; a generation may have changed; migration may have crossed a boundary the handle type did not promise to survive. The server cannot honour the old token, yet it need not claim that the file is gone.

Collapsing these results would force a false conclusion. Treat every expiry as deletion and a client may report lost data that still exists. Treat every stale reference as a temporary token problem and the client may retry forever against an object that has been removed. The two errors let higher layers preserve uncertainty at the point where it arises.

Recovery begins with names again

NFSv4 reduces dependence on the separate mount protocol by defining special root and public handles. PUTROOTFH establishes the current root; successive LOOKUP operations walk the namespace. Those primitives also show how a client can recover after a volatile handle expires.

A robust client retains the component names that led to a cached handle. It can return to a known root, walk the components again and obtain a new handle. But this is a fresh observation, not reconstruction of the expired bytes. Another client may have renamed the object. The file may have been deleted. The old name may now designate a replacement. Recovery code must revalidate the object and any state that depended on the old identity.

That limit is especially important for state-changing operations. A client must not assume that re-resolving a pathname makes a previously interrupted write safe to replay. Identity continuity, operation replay and authorisation are separate questions. NFS servers evaluate access for operations; possession of a valid handle does not itself grant permission.

The historical contribution is therefore not a magical permanent identifier. It is a disciplined division of responsibility. The server controls the mapping and declares its continuity. The protocol standardises the smallest evidence a client needs: opacity, bounded equality, persistence properties and truthful failure. The client stores enough namespace context to recover and refuses to attach old state to a newly resolved object without checking.

A path can change while an object persists. An object can persist while a handle expires. A handle can remain valid while an operation is forbidden. NFS became safer by keeping those propositions separate.

Sources