RFC 3530

Network File System (NFS) version 4 Protocol

๐Ÿ“… published April 2003, from ietf

Network File System (NFS) version 4 Protocol ๐Ÿ’–

Okay bestie, this is RFC 3530 and it's the big one: the NFS version 4 protocol ๐Ÿฆ‹ ๐ŸŒธ Part 1 of 57, so get comfy โœจ

Here's the lowdown on who made it and what it's about ๐Ÿซถ

  • Number and title: RFC 3530, Network File System (NFS) version 4 Protocol
  • Stream: Network Working Group
  • Category: Standards Track, which means it's an official Internet standards track protocol ๐Ÿ‘‘
  • Date: April 2003 ๐ŸŒˆ
  • Authors: S. Shepler, B. Callaghan, D. Robinson, and R. Thurlow from Sun Microsystems, Inc.; C. Beame from Hummingbird Ltd.; and M. Eisler and D. Noveck from Network Appliance, Inc. ๐Ÿ’…
  • Obsoletes: RFC 3010 (this one replaces it, so RFC 3010 is out, periodt ๐Ÿ’€)
  • Updates: nothing is listed in the title block ๐Ÿ™ˆ

Status of this Memo โœจ

Ngl, this part is the fine print, but it matters. This document is an Internet standards track protocol for the Internet community, and it asks for discussion and suggestions for improvements ๐Ÿ’œ If you want to know where this protocol stands in the standardization process, check the current edition of the "Internet Official Protocol Standards" (STD 1) ๐Ÿ“š Distribution of this memo is unlimited, so share it with everyone ๐ŸŽ€

Copyright (C) The Internet Society (2003). All Rights Reserved. Be a good bestie and keep the rights notice intact ๐Ÿซถ

Abstract ๐Ÿงš

Okay so NFS version 4 is a distributed filesystem protocol, and it's giving heritage ๐Ÿ“ It owes its roots to NFS protocol version 2, which is RFC 1094, and version 3, which is RFC 1813 ๐ŸŒธ

The main glow-up is this: unlike earlier versions, NFS version 4 still does traditional file access, but now it also integrates file locking and the mount protocol โœจ It also adds strong security (plus the negotiation of that security), compound operations, client caching, and internationalization ๐Ÿ’– And of course, the designers put real effort into making NFS version 4 work well on the Internet ๐ŸŒˆ

This document replaces RFC 3010 as the definition of the NFS version 4 protocol. Iconic, no notes ๐Ÿ‘‘

Key Words ๐Ÿ’…

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC2119]. These are the capital-letter words that carry real weight, so when you see them, read them as the spec means them ๐Ÿ™ˆ

Table of Contents ๐Ÿฆ‹

The table of contents continues in the next part, so keep scrolling bestie ๐Ÿ’œ

Okay bestie, quick heads up ๐Ÿ’…๐ŸŽ€ The only text in this message is the Table of Contents, and that was already covered in part 1. The actual Section 1 (Introduction) body isn't here, so I can't rewrite part 2 without making things up, and I won't invent RFC 3530 content. ๐Ÿ™ˆ

Could you paste the original text starting at 1. Introduction (the body, not the TOC)? Once I have it, I'll pick up right there with the same headings, section numbers, BCP 14 keywords, grammar, and citations kept exact. โœจ๐Ÿฆ‹

1. Introduction โœจ

1.1. Changes since RFC 3010 ๐Ÿฆ‹

okay so this NFS version 4 definition is literally here to replace and obsolete the one in [RFC3010] ๐Ÿ’… a lot of the stuff stayed the same, but there are some substantive changes too. those changes came from implementation experience and more review of the protocol. some tweaks were for ease of implementation or just clarity, but most of the updates are fixing spots where the [RFC3010] definition was untenable ๐Ÿ˜ญ

the list below is not exhaustive, it's just the most notable changes and additions ๐ŸŽ€

  • The state model has added an open_owner4 identifier. This was done to accommodate Posix based clients and the model they use for file locking. For Posix clients, an open_owner4 would correspond to a file descriptor potentially shared amongst a set of processes, and the lock_owner4 identifier would correspond to a process that is locking a file. ๐Ÿ”
  • Clarifications and error conditions were added for the handling of the owner and group attributes. These attributes are string based now (not the numeric uid/gid from older versions of NFS), so translations may not be available, which is why these changes happened ๐Ÿงš
  • Clarifications for the ACL and mode attributes to address evaluation and partial support ๐ŸŒธ
  • For identifiers that are defined as XDR opaque, limits were set on their size. ๐Ÿ“
  • Added the mounted_on_filed attribute so Posix clients can correctly construct local mounts ๐Ÿ’œ
  • Modified the SETCLIENTID/SETCLIENTID_CONFIRM operations to deal correctly with confirmation details, plus the ability to specify new client callback information. Clarification of the callback information itself was added too ๐ŸŽ€
  • Added a new operation LOCKOWNER_RELEASE to let the server know that a lock_owner4 won't be used by the client anymore ๐Ÿซถ
  • RENEW operation changes to identify the client correctly and allow for additional error returns โญ
  • Verify error return possibilities for all operations. โœ…
  • Remove use of the pathname4 data type from LOOKUP and OPEN in favor of having the client construct a sequence of LOOKUP operations to achieve the same effect ๐Ÿงฉ
  • Clarification of the internationalization issues and adoption of the new stringprep profile framework ๐ŸŒˆ

1.2. NFS Version 4 Goals ๐Ÿ“

NFS version 4 is a further revision of the NFS protocol, which was already defined by versions 2 [RFC1094] and 3 [RFC1813]. it keeps the essential characteristics of the previous versions: easy recovery, independence from transport protocols, operating systems and filesystems, simplicity, and good performance. the NFS version 4 revision has these goals:

  • Improved access and good performance on the Internet.

the protocol is designed to transit firewalls easily, perform well where latency is high and bandwidth is low, and scale to very large numbers of clients per server. it's giving internet-ready ๐ŸŒโœจ

  • Strong security with negotiation built into the protocol.

the protocol builds on the work of the ONCRPC working group in supporting the RPCSEC_GSS protocol. on top of that, NFS version 4 provides a mechanism that lets clients and servers negotiate security, and it requires clients and servers to support a minimal set of security schemes. main character energy for security ๐Ÿ”ฅ๐Ÿ”

  • Good cross-platform interoperability.

the protocol features a filesystem model that offers a useful, common set of features without unduly favoring one filesystem or operating system over another. no favorites here, bestie ๐Ÿซถ๐ŸŒˆ

  • Designed for protocol extensions.

the protocol is designed to accept standard extensions that don't compromise backward compatibility. so future upgrades won't break what already works ๐Ÿ’–๐Ÿ‘‘

1.3. Inconsistencies of this Document with Section 18 โœจ

okay so Section 18, the RPC Definition File ๐Ÿ“œ, is where the constructs used by the protocol are written out in XDR description language. Before that, a few constructs get copied over just to explain things, so the reader is warned that those copies could have errors ๐Ÿ™ˆ. If any part of this document doesn't match Section 18, Section 18 is the authoritative one ๐Ÿ‘‘ so trust that one, bestie.

1.4. Overview of NFS version 4 Features ๐ŸŒธ

let's do a quick tour of the big NFS version 4 features ๐Ÿงš this is for both the people who already know the older NFS versions and the people who are brand new to NFS. the new people still need some background tho, no cap. you should be familiar with XDR and RPC as described in [RFC1831] and [RFC1832] ๐Ÿ“š. a basic understanding of filesystems and distributed filesystems is also expected ๐Ÿ’…

1.4.1. RPC and Security ๐Ÿ”

same as the older versions, NFS version 4 uses the External Data Representation (XDR) and Remote Procedure Call (RPC) mechanisms from [RFC1831] and [RFC1832] ๐Ÿซถ. for end to end security, the RPCSEC_GSS framework [RFC2203] extends the basic RPC security ๐Ÿ›ก๏ธ with RPCSEC_GSS, you can get authentication, integrity, and privacy for NFS version 4 โœจ. Kerberos V5 is one security framework, used as described in [RFC1964] ๐Ÿ‘‘. the LIPKEY GSS-API mechanism from [RFC2847] lets NFS version 4 use a user password and a server public key ๐Ÿ”‘. and honestly, other mechanisms can also be specified and used for NFS version 4 security with RPCSEC_GSS, which is iconic ๐Ÿ’–

for in-band security negotiation, NFS version 4 added a new operation ๐Ÿฆ‹ it lets the client ask the server about its policies on which security mechanisms must be used to access the server's filesystem resources ๐Ÿ‘€. with that info, the client can securely match a security mechanism that meets the policies set at both the client and the server. main character energy ๐Ÿ’…

1.4.2. Procedure and Operation Structure ๐ŸŽ€

the big departure from earlier NFS versions is the COMPOUND procedure ๐ŸŒˆ NFS version 4 only has two RPC procedures, NULL and COMPOUND ๐Ÿ™ˆ and COMPOUND is defined in terms of operations, which line up way more closely with the old traditional NFS procedures. it's giving organized โœจ

with COMPOUND, the client can build simple or complex requests ๐Ÿงฉ these save a bunch of RPCs for logical filesystem operations. like, without ever talking to the server before, a client can read data from a file in one request by combining LOOKUP, OPEN, and READ operations in a single COMPOUND RPC ๐Ÿ˜ญ older NFS versions couldn't do that single request at all, so this is huge.

the COMPOUND model is super simple tho ๐Ÿ’… there's no logical OR or AND of operations. the server evaluates the operations in the order they were combined. once an operation returns a failing result, evaluation stops, and the results of every operation that was evaluated get sent back to the client ๐Ÿ’€

NFS version 4 still has the client refer to a file or directory on the server by a "filehandle" ๐Ÿ“ COMPOUND can pass a filehandle from one operation to the next inside the sequence ๐Ÿฆ‹ there's a "current filehandle" and a "saved filehandle". most operations act on the "current filehandle" as the filesystem object to operate on ๐Ÿ’œ the "saved filehandle" works as temporary filehandle storage inside a COMPOUND procedure, and it's also an extra operand for certain operations. literally so useful ๐Ÿ˜ฉ

1.4.3. Filesystem Model ๐Ÿ—‚๏ธ

the general filesystem model for NFS version 4 is the same as before โœจ the server filesystem is hierarchical, and the regular files in it are treated as opaque byte streams. one small glow-up: file and directory names are encoded with UTF-8 to handle basic internationalization ๐ŸŒ

NFS version 4 doesn't need a separate protocol for the initial mapping between a path name and a filehandle ๐Ÿ™Œ so the older MOUNT protocol is out. instead, the server gives you a ROOT filehandle that represents the logical root, or top, of the filesystem tree it provides ๐ŸŒณ the server can offer multiple filesystems by gluing them together with pseudo filesystems ๐Ÿงš and those pseudo filesystems cover any gaps in the path names between the real filesystems. iykyk ๐Ÿซถ

1.4.3.1. Filehandle Types ๐Ÿท๏ธ

in older NFS versions, the server's filehandle was guaranteed to be valid, or persistent, for as long as the filesystem object it referred to lived ๐Ÿ•ฐ๏ธ some server implementations found that super hard to guarantee ๐Ÿ˜ญ so NFS version 4 relaxed that requirement by adding another filehandle type, called volatile ๐Ÿฆ‹ with the persistent and volatile filehandle types, a server can match what its filesystem and operating environment can actually do โœจ and the client knows which type it's getting and can be ready to handle that type's semantics ๐Ÿ’–

1.4.3.2. Attribute Types ๐ŸŽจ

NFS version 4 introduces three classes of filesystem or file attributes ๐ŸŒท just like the new filehandle type, this classification makes server implementations easier and expands what NFS can do overall. the attribute model is extensible too, so new attributes can show up in minor revisions of the protocol without a huge rework ๐Ÿ› ๏ธ

the three classes are mandatory, recommended, and named attributes ๐Ÿ’… this is a big change from the old attribute model, where the attributes for filesystem and file objects were a fixed set of mostly UNIX attributes ๐Ÿ™ƒ if a server or client didn't support one of those, it had to fake it as best it could ๐Ÿ˜ฌ

mandatory attributes are the minimal set of file or filesystem attributes that must be provided by the server and must be properly represented by it ๐Ÿ”’ recommended attributes cover different filesystem types and operating environments ๐ŸŒˆ they help with interoperability and let more operating environments in ๐Ÿซถ mandatory and recommended attributes are both traditional file or filesystem attributes. the third type is the named attribute, which is an opaque byte stream linked to a directory or file and referred to by a string name ๐ŸŽ€ named attributes are meant for client applications to attach application-specific data to a regular file or directory ๐Ÿ’œ

one major addition to the recommended file attributes is the Access Control List (ACL) attribute ๐Ÿ‘‘ it gives directory and file access control beyond what the older NFS versions could do, and the ACL definition allows specifying user and group level access control. fr fr, it's so good ๐Ÿ”ฅ

1.4.3.3. Filesystem Replication and Migration ๐Ÿงณ

with a special file attribute, you can migrate or replicate server filesystems in the protocol ๐Ÿšš the filesystem locations attribute lets the client ask the server where a filesystem is located ๐Ÿ—บ๏ธ if a filesystem gets migrated, the client gets an error when operating on it, and then it can ask where the new filesystem location is ๐Ÿ™ˆ replication works in a similar way: the client can ask the server for the multiple available locations of a particular filesystem ๐Ÿ“ with that info, the client uses its own policies to pick the right filesystem location to access โœจ

1.4.4. OPEN and CLOSE ๐Ÿšช

NFS version 4 introduces the OPEN and CLOSE operations ๐Ÿ”‘ OPEN is one single spot where file lookup, creation, and share semantics can all come together ๐Ÿซง CLOSE releases the state that OPEN accumulated ๐Ÿ’–

1.4.5. File locking ๐Ÿ”’

in NFS version 4, byte range file locking is part of the NFS protocol itself ๐Ÿ™Œ the locking support is built so that an RPC callback mechanism isn't required ๐Ÿฆ‹ that's a departure from the previous NFS file locking protocol, Network Lock Manager (NLM) ๐Ÿซถ the state for file locks is kept at the server under a lease-based model ๐Ÿ“† the server defines a single lease period for all the state held by an NFS client ๐Ÿ•’ if the client doesn't renew its lease within that period, the server may release all the state tied to the client's lease ๐Ÿ˜ฌ the client can renew its lease with the RENEW operation, or implicitly through other operations (mostly READ) ๐Ÿ’… slay ๐Ÿ‘‘

1.4.6. Client Caching and Delegation โœจ

okay bestie, caching time ๐Ÿ’– file, attribute, and directory caching in NFS version 4 is giving "same energy as previous versions" ๐Ÿซถ attributes and directory info get cached for however long the client decides. when that timeout runs out, the client asks the server if the filesystem object got updated ๐Ÿงš

for file data, the client checks its cache validity when the file is opened. it sends a query to the server to see if the file changed, and based on that answer it decides whether the data cache should be kept or released. and when the file gets closed, any modified data gets written back to the server ๐Ÿ’…

if an app wants to serialize access to file data, file locking on the file data ranges in question should be used ๐Ÿ”’

the major glow-up for caching in NFS version 4 is delegation ๐Ÿ‘‘ the server can delegate certain responsibilities to the client. when the server grants a delegation for a file, the client is guaranteed certain semantics about sharing that file with other clients. at OPEN, the server may hand the client either a read or a write delegation for the file. if the client gets a read delegation, it knows no other client can write to the file for the duration of the delegation. if the client gets a write delegation, it knows no other client has read or write access to the file. main character energy, ngl ๐Ÿ’œ

delegations can be recalled by the server ๐Ÿ“ž if another client asks for access in a way that conflicts with the granted delegation, the server can notify the original client and recall the delegation. this needs a callback path to exist between the server and client. if that callback path doesn't exist, delegations can not be granted ๐Ÿ™ˆ the whole essence of a delegation is that it lets the client locally service operations like OPEN, CLOSE, LOCK, LOCKU, READ, and WRITE without immediately checking in with the server. iconic ๐Ÿฆ‹

1.5. General Definitions ๐Ÿ“š

these definitions are here to give you the context you need as a reader ๐ŸŒธ

Client: the "client" is the entity that accesses the NFS server's resources. the client can be an application that has the logic to reach the NFS server directly. it can also be the traditional operating system client remote filesystem services for a bunch of applications.

when it comes to file locking, the client is the entity that keeps a set of locks on behalf of one or more applications. this client is responsible for crash or failure recovery for the locks it manages ๐Ÿ›ก๏ธ

also note that multiple clients can share the same transport, and multiple clients can exist on the same network node. not confusing at all ๐Ÿ˜ญ

Clientid: a 64-bit quantity used as a unique, shorthand reference to a client supplied Verifier and ID. the server is responsible for supplying the Clientid ๐ŸŽ€

Lease: an interval of time defined by the server, during which the client is irrevocably granted a lock. at the end of a lease period the lock may be revoked if the lease has not been extended. the lock must be revoked if a conflicting lock has been granted after the lease interval ๐Ÿ’€

all leases granted by a server have the same fixed interval. the fixed interval was chosen to ease the expense a server would have maintaining state about variable length leases across server failures. smart, right? ๐Ÿง โœจ

Lock: the term "lock" refers to both record (byte-range) locks and share reservations, unless it's specifically stated otherwise ๐Ÿ”

Server: the "Server" is the entity responsible for coordinating client access to a set of filesystems ๐Ÿ‘‘

Stable Storage: NFS version 4 servers must be able to recover without data loss from multiple power failures (including cascading power failures, that is, several power failures in quick succession), operating system failures, and hardware failure of components other than the storage medium itself (for example, disk, nonvolatile RAM). this is a whole vibe, the kind of reliability you'd want on your bestie's laptop ๐Ÿซ 

some examples of stable storage that are allowable for an NFS server include:

  1. Media commit of data, that is, the modified data has been successfully written to the disk media, for example, the disk platter.
  2. An immediate reply disk drive with battery-backed on-drive intermediate storage or uninterruptible power system (UPS).
  3. Server commit of data with battery-backed intermediate storage and recovery software.
  4. Cache commit with uninterruptible power system (UPS) and recovery software.

Stateid: a 128-bit quantity returned by a server that uniquely defines the open and locking state the server provides for a specific open or lock owner for a specific file. stateids made of all bits 0 or all bits 1 have special meaning and are reserved values ๐Ÿšซ

Verifier: a 64-bit quantity generated by the client that the server can use to figure out if the client has restarted and lost all previous lock state ๐Ÿ”„

2. Protocol Data Types ๐ŸŽ€

the syntax and semantics for describing the data types of the NFS version 4 protocol are defined in the XDR [RFC1832] and RPC [RFC1831] documents. the next sections build on those XDR data types to define types and structures specific to this protocol ๐Ÿง

2.1. Basic Data Types ๐Ÿ’–

   Data Type       Definition
   ____________________________________________________________________
   int32_t         typedef int             int32_t;

   uint32_t        typedef unsigned int    uint32_t;

   int64_t         typedef hyper           int64_t;

   uint64_t        typedef unsigned hyper  uint64_t;

   attrlist4       typedef opaque        attrlist4<>;
                   Used for file/directory attributes

   bitmap4         typedef uint32_t        bitmap4<>;
                   Used in attribute array encoding.

   changeid4       typedef       uint64_t        changeid4;
                   Used in definition of change_info

   clientid4       typedef uint64_t        clientid4;
                   Shorthand reference to client identification

   component4      typedef utf8str_cs      component4;
                   Represents path name components

   count4          typedef uint32_t        count4;
                   Various count parameters (READ, WRITE, COMMIT)

   length4         typedef uint64_t        length4;
                   Describes LOCK lengths

   linktext4       typedef utf8str_cs      linktext4;
                   Symbolic link contents

   mode4           typedef uint32_t        mode4;
                   Mode attribute data type

   nfs_cookie4     typedef uint64_t        nfs_cookie4;
                   Opaque cookie value for READDIR

   nfs_fh4         typedef opaque          nfs_fh4<NFS4_FHSIZE>;
                   Filehandle definition; NFS4_FHSIZE is defined as 128

   nfs_ftype4      enum nfs_ftype4;
                   Various defined file types

   nfsstat4        enum nfsstat4;
                   Return value for operations

   offset4         typedef uint64_t        offset4;
                   Various offset designations (READ, WRITE,
                   LOCK, COMMIT)

   pathname4       typedef component4      pathname4<>;
                   Represents path name for LOOKUP, OPEN and others

   qop4            typedef uint32_t        qop4;
                   Quality of protection designation in SECINFO

   sec_oid4        typedef opaque          sec_oid4<>;
                   Security Object Identifier
                   The sec_oid4 data type is not really opaque.
                   Instead contains an ASN.1 OBJECT IDENTIFIER as used
                   by GSS-API in the mech_type argument to
                   GSS_Init_sec_context.  See [RFC2743] for details.

   seqid4          typedef uint32_t        seqid4;
                   Sequence identifier used for file locking

   utf8string      typedef opaque          utf8string<>;
                   UTF-8 encoding for strings

   utf8str_cis     typedef opaque          utf8str_cis;
                   Case-insensitive UTF-8 string

   utf8str_cs      typedef opaque          utf8str_cs;
                   Case-sensitive UTF-8 string

   utf8str_mixed   typedef opaque          utf8str_mixed;
                   UTF-8 strings with a case sensitive prefix and
                   a case insensitive suffix.

   verifier4       typedef opaque        verifier4[NFS4_VERIFIER_SIZE];
                   Verifier used for various operations (COMMIT,
                   CREATE, OPEN, READDIR, SETCLIENTID,
                   SETCLIENTID_CONFIRM, WRITE) NFS4_VERIFIER_SIZE is
                   defined as 8.

2.2. Structured Data Types ๐ŸŽ€

okay so this is where the data types get dressed up ๐Ÿ’… we're talking structs, unions, and bitmaps. I'm obsessed.

nfstime4

struct nfstime4 {
        int64_t seconds;
        uint32_t nseconds;
}

nfstime4 is the timestamp struct. It counts seconds and nanoseconds from midnight, 0 hour January 1, 1970 Coordinated Universal Time (UTC) ๐Ÿ•›. Positive seconds mean dates after that moment, and negative seconds mean dates before it. Either way, you add nseconds to seconds to get the final time ๐Ÿงฎ. For example, half a second before 0 hour January 1, 1970 is seconds = -1 and nseconds = 500000000 (half a second) ๐ŸŒ™. Any nseconds value over 999,999,999 is invalid, so don't even try it ๐Ÿ™…โ€โ™€๏ธ

This type is how time and date info gets passed around. The server converts between its own local time format and this one, keeping as much accuracy as it can โฐ. If a filesystem stores timestamps with less precision than this type can hold, you can lose some precision, so be warned ๐Ÿ˜ญ. An adjunct time maintenance protocol is recommended to cut down on client and server time skew, which is a very real vibe-killer ๐Ÿซถ

time_how4

enum time_how4 {
        SET_TO_SERVER_TIME4 = 0,
        SET_TO_CLIENT_TIME4 = 1
};

settime4

union settime4 switch (time_how4 set_it) {
 case SET_TO_CLIENT_TIME4:
         nfstime4       time;
 default:
         void;
};

These are the building blocks for setting time values in attributes ๐Ÿ•ฐ๏ธ. If set_it is SET_TO_SERVER_TIME4, the server just uses its own local time, no questions asked ๐Ÿคทโ€โ™€๏ธ otherwise it's the client's time that gets used.

specdata4

struct specdata4 {
        uint32_t specdata1; /* major device number */
        uint32_t specdata2; /* minor device number */
};

specdata4 holds extra info for the device file types NF4CHR and NF4BLK ๐Ÿ”Œ (so character and block devices get their major and minor numbers ๐Ÿงš).

fsid4

struct fsid4 {
  uint64_t        major;
  uint64_t        minor;
};

fsid4 is the filesystem identifier, and it's a mandatory attribute, so every filesystem has to have one ๐Ÿ‘‘ no exceptions, bestie.

fs_location4

struct fs_location4 {
        utf8str_cis    server<>;
        pathname4     rootpath;
};

fs_locations4

struct fs_locations4 {
        pathname4     fs_root;
        fs_location4  locations<>;
};

These two are what the fs_locations recommended attribute runs on, and that attribute is what powers migration and replication ๐ŸŒโœจ moving and copying filesystems around like it's nothing.

fattr4

struct fattr4 {
        bitmap4       attrmask;
        attrlist4     attr_vals;
};

fattr4 is how file and directory attributes get represented ๐Ÿ“‚ the mask says what's there and the values hold the actual data ๐Ÿ’–

The bitmap is a counted array of 32-bit integers that holds bit values ๐Ÿงฉ. To find which integer holds bit n, you compute (n / 32), and the bit's position inside that integer is (n mod 32). Here's the layout:

                 0            1
   +-----------+-----------+-----------+--
   |  count    | 31  ..  0 | 63  .. 32 |
   +-----------+-----------+-----------+--

change_info4

struct change_info4 {
        bool          atomic;
        changeid4     before;
        changeid4     after;
};

This one's used with CREATE, LINK, REMOVE, and RENAME to tell the client what the change attribute is for the directory where the target object lives ๐Ÿ“ the client can see what changed without guessing ๐Ÿ‘€

clientaddr4

struct clientaddr4 {
        /* see struct rpcb in RFC 1833 */
        string r_netid<>;    /* network id */
        string r_addr<>;     /* universal address */
};

clientaddr4 is used in SETCLIENTID, either to say where the client lives or as part of callback registration ๐Ÿ“ž The r_netid and r_addr fields are defined in [RFC1833], but that spec is kind of vague about what they should look like for specific protocols, so this section fills in the gaps ๐Ÿซ 

For TCP over IPv4 and for UDP over IPv4, r_addr is a US-ASCII string in this shape:

h1.h2.h3.h4.p1.p2

The h1.h2.h3.h4 part is the normal text form of an IPv4 address, which is always four octets ๐ŸŒ With big-endian ordering, h1 through h4 are the first through fourth octets, each written in ASCII decimal. Same deal for p1 and p2, which are the first and second octets of the port. Example time: if a host has the address 0x0A010307 and a service is listening on port 0x020F (that's 527 in decimal), the full universal address is 10.1.3.7.2.15 ๐ŸŽฏ (2 ร— 256 + 15 = 527, so it checks out ๐Ÿค“).

For TCP over IPv4, r_netid is the string tcp. For UDP over IPv4, r_netid is the string udp ๐Ÿ’œ

For TCP over IPv6 and for UDP over IPv6, r_addr looks like this:

x1:x2:x3:x4:x5:x6:x7:x8.p1.p2

The p1.p2 suffix is the service port, computed the same way as the IPv4 universal addresses above ๐Ÿงš The x1:x2:x3:x4:x5:x6:x7:x8 prefix is the standard text form of an IPv6 address from Section 2.2 of [RFC2373]. The two alternative forms in that same Section 2.2 of [RFC2373] are also acceptable, so you've got options ๐ŸŒธ

For TCP over IPv6, r_netid is the string tcp6. For UDP over IPv6, it's the string udp6 ๐Ÿฆ‹

cb_client4

struct cb_client4 {
        unsigned int  cb_program;
        clientaddr4   cb_location;
};

The client uses this struct to tell the server its callback address, and it includes both the program number and the client address ๐Ÿ“ฌ

nfs_client_id4

struct nfs_client_id4 {
        verifier4     verifier;
        opaque        id<NFS4_OPAQUE_LIMIT>;
};

This one's part of the SETCLIENTID arguments ๐ŸŽ€ NFS4_OPAQUE_LIMIT is defined as 1024.

open_owner4

struct open_owner4 {
        clientid4     clientid;
        opaque        owner<NFS4_OPAQUE_LIMIT>;
};

open_owner4 identifies who owns open state ๐Ÿ”“ NFS4_OPAQUE_LIMIT is defined as 1024.

lock_owner4

struct lock_owner4 {
        clientid4     clientid;
        opaque        owner<NFS4_OPAQUE_LIMIT>;
};

lock_owner4 identifies who owns file locking state ๐Ÿ”’ NFS4_OPAQUE_LIMIT is defined as 1024.

open_to_lock_owner4

struct open_to_lock_owner4 {
        seqid4          open_seqid;
        stateid4        open_stateid;
        seqid4          lock_seqid;
        lock_owner4     lock_owner;
};

This is for the very first LOCK done for an open_owner4 โœจ It carries both the open_stateid and the lock_owner, so the move goes from a valid open_stateid sequence over to the new lock_stateid sequence, smoothly ๐Ÿงˆ Because it's tied to the open state that's already established (the open_stateid and open_seqid), it skips the need to confirm the lock_owner and lock_seqid pair. Main character energy ๐Ÿ‘‘

stateid4

struct stateid4 {
  uint32_t        seqid;
  opaque          other[12];
};

This structure is what the various state sharing mechanisms between client and server run on ๐Ÿค On the client side it's read-only, so hands off ๐Ÿ™Œ The starting value of seqid is undefined. The server is required to increment seqid monotonically at every transition of the stateid, and this matters a LOT because the client looks at the seqid in OPEN stateids to work out what order the server handled OPEN requests in ๐Ÿ”ฅ


3. RPC and Security Flavor ๐Ÿ›ก๏ธ

okay the big security moment is here ๐Ÿ˜ค NFS version 4 is a Remote Procedure Call (RPC) application. It uses RPC version 2 and the corresponding eXternal Data Representation (XDR) as defined in [RFC1831] and [RFC1832] ๐ŸŽ€ The RPCSEC_GSS security flavor from [RFC2203] MUST be used as the mechanism for delivering stronger security for NFS version 4. Not optional, not a suggestion, MUST ๐Ÿ”

3.1. Ports and Transports ๐Ÿฆ‹

Historically, NFS version 2 and version 3 servers have lived on port 2049 ๐Ÿ  The registered port 2049 from [RFC3232] for NFS should be the default configuration. Using that registered port means the NFS client doesn't need to use the RPC binding protocols from [RFC1833], and that lets NFS get through firewalls ๐Ÿงฑโžก๏ธโœจ

Where an NFS version 4 implementation runs over the IP network protocol, the transports between NFS and IP MUST be among the IETF-approved congestion control transport protocols, which include TCP and SCTP ๐ŸŒŠ To keep interoperability bussin, an NFS version 4 implementation MUST support operation over the TCP transport protocol, at least until a standards track RFC revises this requirement to use a different IETF-approved congestion control transport protocol ๐Ÿ’…

If TCP is the transport, the client and server SHOULD use persistent connections ๐Ÿ” This stops TCP's congestion control from getting weakened by short-lived connections, and it boosts performance over the WAN by getting rid of the need for SYN handshakes ๐Ÿš€

As the Security Considerations section notes, the authentication model for NFS version 4 has moved from machine-based to principal-based ๐Ÿ™ˆ But that change doesn't mean the TCP connection management model has to move from whole machine-based to per-user. NFS over TCP clients have traditionally multiplexed traffic for multiple users over one shared TCP connection between an NFS client and server, and that's true no matter whether the client is using AUTH_SYS, AUTH_DH, RPCSEC_GSS, or any other flavor ๐Ÿซง NFS over TCP servers have assumed that model too, so they scale their TCP connection management based on how many client machines they expect. NFS version 4 is not meant to change this connection management model. NFS version 4 clients that break this assumption can expect scaling issues on the server, which means reduced service ๐Ÿ˜ฌ so please don't.

One more thing: for various timers, the client and server should avoid inadvertent synchronization of those timers โฑ๏ธ For more on the general issue, check out [Floyd] ๐Ÿง โœจ

3.1.1. Client Retransmission Behavior ๐Ÿ”โœจ

okay so bestie, picture this: you're sending a request over a reliable transport like TCP, and the NFS version 4 server is supposed to answer. The server MUST NOT silently drop the request, unless the transport connection has already broken. That's the deal, and it's a big deal ๐Ÿ’…. Because of that deal, clients MUST NOT retry a request unless one or both of these are true:

  • The transport connection has been broken
  • The procedure being retried is the NULL procedure

Now here's the tea ๐Ÿต. TCP doesn't always tell the other side right away that the connection died. Like, if an NFS server reboots, the client might not find out for a while, which is so frustrating ๐Ÿ˜ญ. So the NFS version 4 client might want to actively "probe" the connection to check whether it's broken, and using the NULL procedure is one recommended way to do that ๐Ÿซถ. So when a client hits a remote procedure call timeout (the length is some arbitrary, implementation specific amount, no judgment), instead of retrying the call, it could send a NULL procedure call to the server instead. If the server has died, the transport connection break will eventually show up for the NFS version 4 client. Then the client can reconnect and retry the original request, periodt ๐Ÿฆ‹. If the NULL procedure call gets a response, the connection hasn't broken, and the client can decide to wait longer for the original response, or it can break the transport connection, reconnect, and then re-send the original request. Either choice is valid, the girl has options ๐ŸŒธ.

For callbacks from the server to the client, the exact same rules apply, but the server doing the callback becomes the client, and the client getting the callback becomes the server. Role reversal, iconic ๐Ÿ‘‘.


3.2. Security Flavors ๐Ÿ›ก๏ธ

okay so traditional RPC implementations had AUTH_NONE, AUTH_SYS, AUTH_DH, and AUTH_KRB4 as security flavors ๐ŸŽ€. Then [RFC2203] brought in a new one called RPCSEC_GSS, which uses the functionality of GSS-API [RFC2743]. This is so clever because the RPC layer can use all kinds of security mechanisms without every implementation having to add a whole new RPC security flavor for each one ๐Ÿงš. For NFS version 4, the RPCSEC_GSS security flavor MUST be used to enable the mandatory security mechanism. Other flavors, such as AUTH_NONE, AUTH_SYS, and AUTH_DH, MAY be implemented as well โœจ.

3.2.1. Security mechanisms for NFS version 4 ๐Ÿ”

Using RPCSEC_GSS means you have to select three things: a mechanism, a quality of protection, and a service (authentication, integrity, or privacy). From here on, the document calls these three parameters the security triple ๐Ÿ’œ.

3.2.1.1. Kerberos V5 as a security triple ๐ŸŽซ

The Kerberos V5 GSS-API mechanism described in [RFC1964] MUST be implemented, and it provides the following security triples ๐ŸŒˆ.

Here's how the columns are numbered, so you can read the table without getting lost:

column descriptions:

1 == number of pseudo flavor
2 == name of pseudo flavor
3 == mechanism's OID
4 == mechanism's algorithm(s)
5 == RPCSEC_GSS service

And here are the triples themselves:

1      2     3                    4             5
--------------------------------------------------------------------
390003 krb5  1.2.840.113554.1.2.2 DES MAC MD5   rpc_gss_svc_none
390004 krb5i 1.2.840.113554.1.2.2 DES MAC MD5   rpc_gss_svc_integrity
390005 krb5p 1.2.840.113554.1.2.2 DES MAC MD5   rpc_gss_svc_privacy
                                  for integrity,
                                  and 56 bit DES
                                  for privacy.

Quick note on the pseudo flavor: it's only here as a mapping aid for the implementor ๐Ÿง . This NFS protocol already has a way to negotiate security and understands the GSS-API mechanism, so the pseudo flavor isn't actually needed. It IS needed for NFS version 3, though, because security negotiation there happens through the MOUNT protocol ๐Ÿ“ฆ.

If you want the whole story on NFS, RPCSEC_GSS, and Kerberos V5, check out [RFC2623] ๐Ÿ“š.

Users and implementors, listen up ๐Ÿšจ: 56 bit DES is no longer considered state of the art when it comes to resisting brute force attacks, so this is NOT the vibe anymore. Once a revision to [RFC1964] shows up that adds support for AES, implementors are urged to put AES into their NFSv4 over Kerberos V5 protocol stacks, and users are similarly urged to move over to AES too ๐Ÿ’€๐Ÿซถ.

3.2.1.2. LIPKEY as a security triple ๐Ÿ—๏ธ

The LIPKEY GSS-API mechanism described in [RFC2847] MUST be implemented, and it provides the following security triples. The columns mean the same thing as in the previous subsection, "Kerberos V5 as security triple" ๐Ÿ’–.

1      2        3                   4              5
--------------------------------------------------------------------
390006 lipkey   1.3.6.1.5.5.9       negotiated  rpc_gss_svc_none
390007 lipkey-i 1.3.6.1.5.5.9       negotiated  rpc_gss_svc_integrity
390008 lipkey-p 1.3.6.1.5.5.9       negotiated  rpc_gss_svc_privacy

So why does the mechanism algorithm say "negotiated"? Because LIPKEY is layered on SPKM-3, and in SPKM-3 [RFC2847] the confidentiality and integrity algorithms get negotiated ๐Ÿค. SPKM-3 says HMAC-MD5 for integrity is MANDATORY, and 128 bit cast5CBC for confidentiality (for privacy) is MANDATORY too. It also says HMAC-MD5 and cast5CBC MUST be listed first, before any weaker algorithms. Because of all that, writing "negotiated" in column 4 does not hurt interoperability ๐Ÿ™Œ. If an SPKM-3 peer doesn't support the mandatory algorithms, the other peer is free to accept or reject the GSS-API context creation. Your call, bestie ๐Ÿ’….

Since SPKM-3 is the one negotiating the algorithms, later calls to LIPKEY's GSS_Wrap() and GSS_GetMIC() made by RPCSEC_GSS will use a quality of protection value of 0 (zero). See section 5.2 of [RFC2025] for an explanation ๐Ÿงพ.

LIPKEY uses SPKM-3 to set up a secure channel, and then it passes a user name and password from the client to the server through that channel ๐Ÿ”’. Once the server accepts the user name and password, calls to the LIPKEY context get redirected to the SPKM-3 context. Want more details? See [RFC2847] ๐Ÿ“–.

3.2.1.3. SPKM-3 as a security triple ๐ŸŒŸ

The SPKM-3 GSS-API mechanism described in [RFC2847] MUST be implemented, and it provides the following security triples. The columns match the "Kerberos V5 as security triple" subsection, so no new decoder ring needed ๐Ÿ’.

1      2        3                   4              5
--------------------------------------------------------------------
390009 spkm3    1.3.6.1.5.5.1.3     negotiated  rpc_gss_svc_none
390010 spkm3i   1.3.6.1.5.5.1.3     negotiated  rpc_gss_svc_integrity
390011 spkm3p   1.3.6.1.5.5.1.3     negotiated  rpc_gss_svc_privacy

For the reason the mechanism algorithm is listed as "negotiated," go back to the previous section, "LIPKEY as a security triple" ๐Ÿ”™.

Same deal as before: because SPKM-3 negotiates the algorithms, later calls to SPKM-3's GSS_Wrap() and GSS_GetMIC() made by RPCSEC_GSS will use a quality of protection value of 0 (zero). See section 5.2 of [RFC2025] for an explanation โœจ.

Even though LIPKEY sits on top of SPKM-3, SPKM-3 is still specified as a mandatory set of triples. Why? Two situations: when the initiator (the client) is anonymous, or when the initiator has its own certificate ๐Ÿชช. If the client is anonymous, there's no user name and password to send to the target (the server). And if the client has its own certificate, then using passwords is superfluous ๐Ÿ˜Œ.


3.3. Security Negotiation ๐Ÿค

okay so the NFS version 4 server might offer multiple security mechanisms, which means the client needs a way to figure out, or negotiate, which mechanism to use when talking to the server ๐Ÿ•ต๏ธโ€โ™€๏ธ. The NFS server can have several entry points within its filesystem name space that NFS clients can use, and the server might be set up so each entry point has different or multiple security mechanisms in use. Chaotic, but manageable ๐ŸŒ€.

The security negotiation between client and server must be done over a secure channel. Why? So nobody in the middle can intercept the negotiation and force the client and server to pick a lower level of security than they needed or wanted ๐Ÿšซ. See the section "Security Considerations" for further discussion ๐Ÿ”.

3.3.1. SECINFO ๐Ÿงญ

The new SECINFO operation lets the client figure out, on a per filehandle basis, which security triple to use for server access ๐Ÿ—บ๏ธ. Usually the client won't need SECINFO except during the first communication with the server, or when the client crosses policy boundaries at the server. Also, the server's policies can change while the client is interacting with it, which can force the client to negotiate a new security triple ๐Ÿ”„.

3.3.2. Security Error ๐Ÿšจ

So here's the assumption: every NFS version 4 client and server must support a minimum set of security, which is LIPKEY, SPKM-3, and Kerberos-V5, all under RPCSEC_GSS ๐Ÿ’ช. The NFS client starts talking to the server with one of those minimal security triples. Later on, the client might get an NFS error called NFS4ERR_WRONGSEC. That error is the server's way of saying, "hey, the security triple you're using isn't appropriate for accessing my filesystem resources" ๐Ÿ™…โ€โ™€๏ธ. The client is then responsible for finding out which security triples the server has available and picking one that works for it. For how the client responds to NFS4ERR_WRONGSEC and uses SECINFO, see the section on the "SECINFO" operation ๐Ÿ’–.

3.4. Callback RPC Authentication ๐ŸŽ€

okay so bestie, unless this section says otherwise (it does a couple of times, so pay attention ๐Ÿ‘€), the callback RPC (we'll get to it later ๐Ÿงš) MUST mutually authenticate the NFS server to the principal that acquired the clientid (also later!), using the same security flavor the original SETCLIENTID operation used.

For AUTH_NONE, there are no principals, so no drama here ๐Ÿ’…

AUTH_SYS doesn't do mutual authentication or server principals at all ๐Ÿ™ˆ so the callback from the server just uses whatever AUTH_SYS credential the user had when they set up the delegation.

For AUTH_DH, a commonly used convention is that the server uses the credential that matches this AUTH_DH principal:

unix.host@domain

Here host and domain are placeholders for the name of the server host and the directory services domain it lives in, like a Network Information System domain or a DNS domain ๐ŸŒธ

Because LIPKEY is layered over SPKM-3, it's totally fine for the server to use SPKM-3 and not LIPKEY for the callback, even if the client used LIPKEY for SETCLIENTID โœจ

Regardless of which security mechanism under RPCSEC_GSS is being used, the NFS server, MUST identify itself in GSS-API via a GSS_C_NT_HOSTBASED_SERVICE name type. These names look like this:

service@hostname

For NFS, the "service" element is:

nfs

Security mechanism implementations will convert nfs@hostname into lots of different forms ๐Ÿฆ‹ For Kerberos V5 and LIPKEY, the following form is RECOMMENDED:

nfs/hostname

For Kerberos V5, nfs/hostname is a server principal in the Kerberos Key Distribution Center database. This is the same principal the client got a GSS-API context for when it issued the SETCLIENTID operation, so the realm name for the server principal must be the same for the callback as it was for the SETCLIENTID. Realm names matter, bestie ๐Ÿ‘‘

For LIPKEY, this is the username passed to the target (the NFS version 4 client that receives the callback).

It should be noted that LIPKEY may not work for callbacks ๐Ÿ’€ The LIPKEY client uses a user id and password. If the NFS client receiving the callback can authenticate the NFS server's user name and password pair, and if the user that the NFS server is authenticating to has a public key certificate, then it works.

In situations where the NFS client uses LIPKEY and a per-host principal for SETCLIENTID (instead of using LIPKEY for SETCLIENTID), it is RECOMMENDED that SPKM-3 with mutual authentication be used. This basically means the client uses a certificate to authenticate and identify the initiator to the target on the NFS server ๐Ÿ”ฅ Using SPKM-3 and not LIPKEY has these advantages:

  • When the server does a callback, it must authenticate to the principal used in the SETCLIENTID. Even if LIPKEY is used, because LIPKEY is layered over SPKM-3, the NFS client will need a certificate that matches the principal used in the SETCLIENTID operation. Administratively, having a user name, password, and certificate for both the client and server is redundant ๐Ÿ™„
  • LIPKEY was meant to keep extra infrastructure to a minimum beyond a certificate for the target, and the idea is that existing password infrastructure can be reused for the initiator. In some environments, a per-host password doesn't exist yet. If certificates are used for any per-host principals, then extra password infrastructure isn't needed ๐Ÿงš
  • If a host is both an NFS client and an NFS server, it can share the same per-host certificate ๐Ÿซถ

4. Filehandles โœจ

The filehandle in the NFS protocol is a per-server unique identifier for a filesystem object ๐Ÿ’œ The contents of the filehandle are opaque to the client, so the server is responsible for translating the filehandle into an internal representation of the filesystem object.

4.1. Obtaining the First Filehandle ๐Ÿ—๏ธ

The operations of the NFS protocol are defined in terms of one or more filehandles, so the client needs a filehandle to start talking to the server. With the NFS version 2 protocol [RFC1094] and the NFS version 3 protocol [RFC1813], there's an ancillary protocol for getting this first filehandle. The MOUNT protocol, RPC program number 100005, translates a string-based filesystem path name into a filehandle that the NFS protocols can then use.

The MOUNT protocol has security and firewall problems ๐Ÿ™ˆ This is one reason the public filehandle was introduced in [RFC2054] and [RFC2055]. With the public filehandle plus the LOOKUP operation in NFS version 2 and 3, it was shown that the MOUNT protocol isn't actually needed for a workable interaction between NFS client and server ๐ŸŽ‰

So the NFS version 4 protocol won't use an ancillary protocol to translate string-based path names into filehandles. Instead there are two special filehandles that serve as starting points for the NFS client โœจ

4.1.1. Root Filehandle ๐ŸŒณ

The first special filehandle is the ROOT filehandle. It's the "conceptual" root of the filesystem name space at the NFS server. The client uses or starts with the ROOT filehandle by using the PUTROOTFH operation. PUTROOTFH tells the server to set the "current" filehandle to the ROOT of the server's file tree. Once PUTROOTFH has been used, the client can traverse the entire server file tree with the LOOKUP operation ๐Ÿงญ A full discussion of the server name space is in the section "NFS Server Name Space".

4.1.2. Public Filehandle ๐ŸŒธ

The second special filehandle is the PUBLIC filehandle. Unlike the ROOT filehandle, the PUBLIC filehandle may be bound to, or represent, an arbitrary filesystem object at the server. The server is responsible for this binding. The PUBLIC and ROOT filehandles might refer to the same filesystem object, but it's up to the server's administrative software and the server administrator's policies to define how the PUBLIC filehandle is bound to a server filesystem object. The client may not make any assumptions about this binding ๐Ÿ™…โ€โ™€๏ธ The client uses the PUBLIC filehandle via the PUTPUBFH operation.

4.2. Filehandle Types ๐Ÿ’…

In the NFS version 2 and 3 protocols, there was one type of filehandle with a single set of semantics. That type is called "persistent" in NFS version 4, and its semantics stay the same as before. The new type introduced in NFS version 4 is the "volatile" filehandle, which tries to accommodate certain server environments ๐Ÿฆ‹

The volatile filehandle type exists to deal with server functionality or implementation issues that make a correct persistent filehandle implementation infeasible. Some server environments don't provide a filesystem-level invariant that can be used to build a persistent filehandle. The underlying filesystem might not provide the invariant, or the server's filesystem programming interfaces might not give access to it. Volatile filehandles can make things like hierarchical storage management or filesystem reorganization or migration easier to implement on the server side. However, volatile filehandles make the client's job harder ๐Ÿ˜ญ

Since clients have to handle persistent and volatile filehandles differently, a file attribute is defined that clients can use to figure out which filehandle types the server is returning ๐Ÿ”

4.2.1. General Properties of a Filehandle ๐Ÿ”‘

The filehandle contains all the information the server needs to tell one file apart from another. To the client, the filehandle is opaque. The client stores filehandles for use in later requests, and it can compare two filehandles from the same server for equality with a byte-by-byte comparison. However, the client MUST NOT otherwise interpret the contents of filehandles. If two filehandles from the same server are equal, they MUST refer to the same file. Servers SHOULD try to keep a one-to-one correspondence between filehandles and files, but that's not required. Clients MUST use filehandle comparisons only to improve performance, not for correct behavior ๐Ÿš€ Every client needs to be ready for situations where it can't tell whether two filehandles denote the same object. In those cases, avoid making invalid assumptions that could cause incorrect behavior. More on filehandle and attribute comparison for data caching is in the section "Data Caching and File Identity" ๐Ÿ’œ

For example, if two different path names, when traversed at the server, end up at the same filesystem object, the server SHOULD return the same filehandle for each path. This can happen when a hard link is used to create two file names that refer to the same underlying file object and its data ๐Ÿ”— If paths /a/b/c and /a/d/c refer to the same file, the server SHOULD return the same filehandle for both path name traversals.

4.2.2. Persistent Filehandle ๐Ÿ”’

A persistent filehandle has a fixed value for the lifetime of the filesystem object it refers to. Once the server creates the filehandle for a filesystem object, the server MUST accept that same filehandle for the object for as long as the object exists. If the server restarts or reboots, the NFS server must honor the same filehandle value it used in its previous instantiation. Likewise, if the filesystem is migrated, the new NFS server must honor the same filehandle as the old NFS server ๐Ÿงณ

A persistent filehandle will become stale or invalid when the filesystem object is removed ๐Ÿ’€ When the server is handed a persistent filehandle that refers to a deleted object, it MUST return an error of NFS4ERR_STALE. A filehandle can also go stale when the filesystem containing the object is no longer available. That can happen if the filesystem lives on removable media and the media is no longer available at the server, if the filesystem as a whole has been destroyed, or if the filesystem has simply been removed from the server's name space (that is, unmounted in a UNIX environment) ๐Ÿ™ˆ

4.2.3. Volatile Filehandle โœจ

okay so a volatile filehandle does NOT have the same longevity as a persistent one ๐Ÿฆ‹ the server can decide it's no longer valid at a bunch of different moments. if the server can definitively tell that the object was removed, it should send back NFS4ERR_STALE (same as persistent filehandles ๐Ÿ’…). in every other case where it decides the volatile filehandle can't be used anymore, it should return NFS4ERR_FHEXPIRED ๐Ÿซถ

the mandatory attribute "fh_expire_type" is how the client figures out what kind of filehandle the server is handing out for a given filesystem ๐ŸŽ€ it's a bitmask with these values:

  • FH4_PERSISTENT: this one means a persistent filehandle, valid until the object is removed from the filesystem. the server will not return NFS4ERR_FHEXPIRED for it ๐ŸŒธ FH4_PERSISTENT is defined as the value where none of the bits below are set. no bits, pure vibes ๐Ÿงš
  • FH4_VOLATILE_ANY: the filehandle may expire at any time, except where it's specifically excluded (ie. FH4_NO_EXPIRE_WITH_OPEN) ๐Ÿ”ฅ
  • FH4_NOEXPIRE_WITH_OPEN: this can only be set when FH4_VOLATILE_ANY is set ๐Ÿ‘‘ when it's on, the FH4_VOLATILE_ANY meaning gets qualified so the filehandle won't expire while it's open
  • FH4_VOL_MIGRATION: the filehandle will expire as a result of migration ๐Ÿฆ‹ if FH4_VOL_ANY is set, FH4_VOL_MIGRATION is redundant
  • FH4_VOL_RENAME: the filehandle will expire during a rename ๐Ÿ’€ that includes a rename by the requesting client OR a rename by any other client. if FH4_VOL_ANY is set, FH4_VOL_RENAME is redundant
โš ๏ธ servers that hand out volatile filehandles which can expire while open (so, if FH4_VOL_MIGRATION or FH4_VOL_RENAME is set, or if FH4_VOLATILE_ANY is set and FH4_NOEXPIRE_WITH_OPEN is not set) should deny a RENAME or REMOVE that would affect an OPEN file, or any of the components leading to that OPEN file. on top of that, the server should deny all RENAME or REMOVE requests during the grace period after a server restart ๐Ÿ™ˆ

lowkey the bits FH4_VOL_MIGRATION and FH4_VOL_RENAME are super useful ๐Ÿ’– they let the client know that expiration happened whenever a specific event occurs, without the server ever sending an explicit filehandle expiration error. FH4_VOL_ANY doesn't give you that info ๐Ÿ˜ญ now, if the server is going to expire a lot of filehandles on migration but not all of them (like everything except the ones that are open), FH4_VOLATILE_ANY (paired with FH4_NOEXPIRE_WITH_OPEN) is the better pick. the client can't assume every filehandle will expire during migration, and extra expirations are likely to happen because of file CLOSE, separated in time from the migration itself ๐ŸŒˆ


4.2.4. One Method of Constructing a Volatile Filehandle ๐Ÿ“

so a volatile filehandle is opaque to the client, but it could look something like this ๐Ÿ‘€

[volatile bit = 1 | server boot time | slot | generation number]
  • slot is an index in the server volatile filehandle table
  • generation number is the generation number for the table entry/slot

when the client shows up with a volatile filehandle, the server runs these checks ๐Ÿง (they assume the volatile bit check already passed). if the server boot time is less than the current server boot time, return NFS4ERR_FHEXPIRED. if slot is out of range, return NFS4ERR_BADHANDLE. if the generation number doesn't match, return NFS4ERR_FHEXPIRED.

when the server reboots, the table is gone ๐Ÿ˜ญ (it's volatile, bestie)

if the volatile bit is 0, then it's a persistent filehandle with a different structure following it ๐Ÿ’…


4.3. Client Recovery from Filehandle Expiration ๐Ÿฆ‹

if it's possible, the client SHOULD recover from getting an NFS4ERR_FHEXPIRED error โœจ the client has to take on some extra responsibility so it can get ready to recover from a volatile filehandle expiring. if the server returns persistent filehandles, the client doesn't need any of these extra steps ๐Ÿซถ

for volatile filehandles, the client will most commonly need to store the component names leading up to and including the filesystem object in question ๐ŸŽ€ with those names, the client should be able to recover by finding a filehandle in the namespace that's still available, or by starting over at the root of the server's filesystem namespace ๐ŸŒธ

if the expired filehandle refers to an object that's been removed from the filesystem, obviously the client can't recover from it ๐Ÿ’€

it's also possible that the expired filehandle refers to a file that got renamed ๐Ÿ‘€ if another client did the renaming, the original client might not be able to recover. BUT if the client itself is renaming the file and the file is open, the client may be able to recover ๐Ÿ”ฅ the client can figure out the new pathname from the processing of the rename request, and then regenerate the new filehandle from that new pathname. the client could also use the compound operation mechanism to build a set of operations like this:

RENAME A B
LOOKUP B
GETFH
โš ๏ธ the COMPOUND procedure does NOT provide atomicity!! this example only cuts down on the overhead of recovering from an expired filehandle ๐Ÿ™ˆ

5. File Attributes ๐ŸŽ€

okay so attributes have to be handled in a flexible way โœจ this is about extensibility and better interoperability with non-UNIX platforms. the NFS version 3 fattr3 structure has a fixed list of attributes that not every client and server can support or even cares about ๐Ÿ˜ญ fattr3 can't be extended as new needs pop up, and it has no way to say "not supported." with NFS version 4, the client can ask what attributes the server supports and build requests with only those (or a subset of them) ๐Ÿ’–

so attributes are split into three groups: mandatory, recommended, and named ๐Ÿฆ‹ both mandatory and recommended attributes are supported by a specific, well-defined encoding in NFS version 4, and they're identified by number. you request them by setting a bit in the bit vector sent in the GETATTR request, and the server's response includes a bit vector listing which attributes actually came back ๐Ÿงš new mandatory or recommended attributes can be added between major revisions by publishing a standards-track RFC that allocates a new attribute number and defines its encoding ๐ŸŒˆ see the section "Minor Versioning" for more on that.

named attributes are accessed through the new OPENATTR operation, which opens a hidden directory of attributes attached to a filesystem object ๐Ÿ”ฅ OPENATTR takes a filehandle for the object and returns the filehandle for the attribute hierarchy. the filehandle for the named attributes is a directory object you can reach with LOOKUP or READDIR, and it contains files whose names are the named attributes and whose data bytes are the attribute's value ๐Ÿ“ for example:

LOOKUP     "foo"       ; look up file
GETATTR    attrbits
OPENATTR               ; access foo's named attributes
LOOKUP     "x11icon"   ; look up specific attribute
READ       0,4096      ; read stream of bytes

named attributes are meant for data that applications need, not data an NFS client implementation needs ๐Ÿ’… NFS implementors are strongly encouraged to define new attributes as recommended attributes by taking them through the IETF standards-track process ๐Ÿ‘‘

the mandatory set is deliberately small, since servers have to do whatever it takes to support them ๐Ÿ’ช a server SHOULD support as many recommended attributes as it can, but by definition it isn't required to support all of them. attributes are only deemed mandatory if the data is needed by a large number of clients AND isn't reasonably computable by the client when the server doesn't provide it ๐Ÿซถ

note that the hidden directory from OPENATTR is just a convenience for protocol processing ๐Ÿ™ˆ the client shouldn't make any assumptions about how the server implements named attributes, or whether the underlying filesystem even has a named attribute directory. so operations like SETATTR and GETATTR on the named attribute directory are undefined ๐Ÿ’€


5.1. Mandatory Attributes ๐Ÿ’œ

these MUST be supported by every NFS version 4 client and server so there's a minimum level of interoperability ๐ŸŽ€ the server must store and return these attributes, and the client must be able to function with an attribute set limited to just these. with only the mandatory attributes, some client functionality may be impaired or limited in some ways ๐ŸŒธ a client can ask for any of these attributes to be returned by setting a bit in the GETATTR request, and the server must return their value โœจ

these attributes are understood well enough to deserve support in the NFS version 4 protocol ๐Ÿ’– however, they may not be supported on every client and server. a client can ask for any of them by setting a bit in the GETATTR request, but it must handle the case where the server doesn't return them ๐Ÿซถ a client may ask for the set of attributes the server supports, and it SHOULD NOT request attributes the server doesn't support ๐Ÿ™ˆ

a server should be tolerant of requests for unsupported attributes and just not return them, rather than treating the request as an error ๐Ÿงš it's expected that servers will support all the attributes they comfortably can, and only skip the ones that are difficult to support in their operating environment ๐Ÿ“ a server should provide attributes whenever it doesn't have to "tell lies" to the client ๐Ÿ‘€ for example, a file modification time should either be an accurate time or not be supported at all ๐Ÿ’… this won't always feel comfy for clients, but the client is better positioned to decide whether and how to fabricate or construct an attribute, or whether to just do without it ๐Ÿ‘‘

5.3. Named Attributes ๐ŸŽ€

okay so these attributes don't have a number like the others ๐Ÿ”ข they get found by string names instead, and each one is just an uninterpreted stream of bytes stored right alongside the filesystem object ๐Ÿงš to reach the whole collection, you use OPENATTR, which hands you back a filehandle for a virtual "attribute directory" ๐Ÿ“ from there you can browse the name space with READDIR and LOOKUP on that filehandle. once you've found the one you want, you can examine or change it with normal READ, WRITE, and CREATE operations on the filehandles you got from READDIR and LOOKUP ๐Ÿ’… and named attributes can have attributes too, which is giving inception ๐ŸŒ€

the spec recommends that servers support arbitrary named attributes ๐Ÿซถ but a client should not count on being able to store any named attributes on the server's filesystem ๐Ÿ™ˆ if a server does support them, though, a client that can handle them should be able to copy a file's data and metadata with complete transparency from one location to another ๐Ÿฆ‹ which means names that are allowed for regular directory entries should also be valid for named attribute names โœจ no cap, that's the dream

names of attributes aren't controlled by this document or by other IETF standards track documents ๐Ÿ“œ check the section "IANA Considerations" if you want the deets ๐Ÿ”

5.4. Classification of Attributes ๐Ÿท๏ธ

every Mandatory and Recommended attribute gets sorted into one of three buckets: per server, per filesystem, or per filesystem object ๐Ÿชฃ bestie, heads up, some per filesystem attributes can vary within the filesystem, so check the "homogeneous" attribute for its definition ๐Ÿ‘€ also, time_access_set and time_modify_set aren't listed in this section because they're write-only attributes that correspond to time_access and time_modify, and they only show up in a special instance of SETATTR ๐Ÿ™ƒ

  • The per server attribute is:
    • lease_time ๐ŸŒธ
  • The per filesystem attributes are:
    • supp_attr, fh_expire_type, link_support, symlink_support, unique_handles, aclsupport, cansettime, case_insensitive, case_preserving, chown_restricted, files_avail, files_free, files_total, fs_locations, homogeneous, maxfilesize, maxname, maxread, maxwrite, no_trunc, space_avail, space_free, space_total, time_delta ๐Ÿ’œ
  • The per filesystem object attributes are:
    • type, change, size, named_attr, fsid, rdattr_error, filehandle, ACL, archive, fileid, hidden, maxlink, mimetype, mode, numlinks, owner, owner_group, rawdev, space_used, system, time_access, time_backup, time_create, time_metadata, time_modify, mounted_on_fileid ๐Ÿง

for quota_avail_hard, quota_avail_soft, and quota_used, check their definitions below for the right classification ๐ŸŽฏ


5.5. Mandatory Attributes - Definitions ๐Ÿ“‹

the mandatory attributes table is giving organized energy ๐Ÿ’… here's the full lineup:

Name              #    DataType     Access   Description
___________________________________________________________________
supp_attr         0    bitmap       READ     The bit vector which
                                              would retrieve all
                                              mandatory and
                                              recommended attributes
                                              that are supported for
                                              this object.  The
                                              scope of this
                                              attribute applies to
                                              all objects with a
                                              matching fsid.

type              1    nfs4_ftype   READ     The type of the object
                                              (file, directory,
                                              symlink, etc.)

fh_expire_type    2    uint32       READ     Server uses this to
                                              specify filehandle
                                              expiration behavior to
                                              the client.  See the
                                              section "Filehandles"
                                              for additional
                                              description.

change            3    uint64       READ     A value created by the
                                              server that the client
                                              can use to determine
                                              if file data,
                                              directory contents or
                                              attributes of the
                                              object have been
                                              modified.  The server
                                              may return the
                                              object's time_metadata
                                              attribute for this
                                              attribute's value but
                                              only if the filesystem
                                              object can not be
                                              updated more
                                              frequently than the
                                              resolution of
                                              time_metadata.

size              4    uint64       R/W      The size of the object
                                              in bytes.

link_support      5    bool         READ     True, if the object's
                                              filesystem supports
                                              hard links.

symlink_support   6    bool         READ     True, if the object's
                                              filesystem supports
                                              symbolic links.

named_attr        7    bool         READ     True, if this object
                                              has named attributes.
                                              In other words, object
                                              has a non-empty named
                                              attribute directory.

fsid              8    fsid4        READ     Unique filesystem
                                              identifier for the
                                              filesystem holding
                                              this object.  fsid
                                              contains major and
                                              minor components each
                                              of which are uint64.

unique_handles    9    bool         READ     True, if two distinct
                                              filehandles guaranteed
                                              to refer to two
                                              different filesystem
                                              objects.

lease_time        10   nfs_lease4   READ     Duration of leases at
                                              server in seconds.

rdattr_error      11   enum         READ     Error returned from
                                              getattr during
                                              readdir.

filehandle        19   nfs_fh4      READ     The filehandle of this
                                              object (primarily for
                                              readdir requests).

and now the recommended attributes, which are just as iconic ๐Ÿคฉ this table is still going, so stay tuned โœจ

Name                #    Data Type      Access   Description
_____________________________________________________________________
ACL                 12   nfsace4<>      R/W      The access control
                                                    list for the object.

aclsupport          13   uint32         READ     Indicates what types
                                                    of ACLs are
                                                    supported on the
                                                    current filesystem.

archive             14   bool           R/W      True, if this file
                                                    has been archived
                                                    since the time of
                                                    last modification
                                                    (deprecated in favor
                                                    of time_backup).

cansettime          15   bool           READ     True, if the server
                                                    is able to change
                                                    the times for a
                                                    filesystem object as
                                                    specified in a
                                                    SETATTR operation.

case_insensitive    16   bool           READ     True, if filename
                                                    comparisons on this
                                                    filesystem are case
                                                    insensitive.

case_preserving     17   bool           READ     True, if filename
                                                    case on this
                                                    filesystem are
                                                    preserved.

chown_restricted    18   bool           READ     If TRUE, the server
                                                    will reject any
                                                    request to change
                                                    either the owner or
                                                    the group associated
                                                    with a file if the
                                                    caller is not a
                                                    privileged user (for
                                                    example, "root" in
                                                    UNIX operating
                                                    environments or in
                                                    Windows 2000 the
                                                    "Take Ownership"
                                                    privilege).

fileid              20   uint64         READ     A number uniquely
                                                    identifying the file
                                                    within the
                                                    filesystem.

files_avail         21   uint64         READ     File slots available
                                                    to this user on the
                                                    filesystem
                                                    containing this
                                                    object - this should
                                                    be the smallest
                                                    relevant limit.

okay bestie, we're on the recommended attributes now and the list is giving filesystem stats ๐Ÿ“Šโœจ

  • files_free (22, uint64, READ) ๐Ÿ’…: free file slots on the filesystem that holds this object. This should be the smallest relevant limit, so the most restrictive number wins ๐Ÿซถ
  • files_total (23, uint64, READ) ๐Ÿงš: total file slots on that same filesystem.
  • fs_locations (24, fs_locations, READ) ๐Ÿฆ‹: the places this filesystem might be found. If the server answers with NFS4ERR_MOVED, this attribute MUST be supported. no cap, that's a hard rule ๐Ÿ’€
  • hidden (25, bool, R/W) ๐Ÿ™ˆ: true if the file counts as hidden from the Windows API.
  • homogeneous (26, bool, READ) ๐ŸŒธ: true if this object's filesystem is homogeneous. The question is whether the per-filesystem attributes are the same for every object on that filesystem ๐Ÿ‘€
  • maxfilesize (27, uint64, READ) ๐Ÿ“: the biggest file size the filesystem of this object supports.
  • maxlink (28, uint32, READ) ๐Ÿ’œ: the maximum number of links this object can have.
  • maxname (29, uint32, READ) ๐ŸŽ€: the largest filename size supported for this object.
  • maxread (30, uint64, READ) โญ: the largest read size supported for this object.
  • maxwrite (31, uint64, READ) ๐Ÿ”ฅ: the largest write size supported for this object. This one SHOULD be supported if the file is writable. Without it, the client can end up wasting bandwidth or missing out on the best performance, and that's not it ๐Ÿ˜ญ
  • mimetype (32, utf8<>, R/W) ๐ŸŒˆ: the MIME body type/subtype of this object.
  • mode (33, mode4, R/W) ๐Ÿ‘‘: the UNIX-style mode and permission bits for this object.
  • no_trunc (34, bool, READ) ๐Ÿ™…โ€โ™€๏ธ: true if a name longer than name_max is used, an error be returned and the name is not truncated. So it refuses instead of chopping the name, which is honestly iconic ๐Ÿ’…
  • numlinks (35, uint32, READ) ๐Ÿฆ‹: the number of hard links pointing to this object.
  • owner (36, utf8<>, R/W) ๐Ÿซถ: the string name of the owner of this object.
  • owner_group (37, utf8<>, R/W) ๐ŸŒธ: the string name of the group that owns this object.
  • quota_avail_hard (38, uint64, READ) โœจ: see the "Quota Attributes" section below for the definition.
  • quota_avail_soft (39, uint64, READ) โœจ: see the "Quota Attributes" section below for the definition.
  • quota_used (40, uint64, READ) โœจ: see the "Quota Attributes" section below for the definition.
  • rawdev (41, specdata4, READ) ๐Ÿ”ง: the raw device identifier, which is the UNIX device major/minor node info. If the type isn't NF4BLK or NF4CHR, the returned value SHOULD NOT be considered useful. so don't trust it outside those types, bestie ๐Ÿ™ˆ
  • space_avail (42, uint64, READ) ๐Ÿ’–: the disk space in bytes available to this user on the filesystem holding this object. Again, the smallest relevant limit is the one to care about.
  • space_free (43, uint64, READ) ๐Ÿ’–: the free disk space in bytes on the filesystem holding this object. Same deal, smallest relevant limit wins.
  • space_total (44, uint64, READ) ๐Ÿ’–: the total disk space in bytes on the filesystem holding this object.
  • space_used (45, uint64, READ) ๐Ÿ’–: the number of filesystem bytes allocated to this object.
  • system (46, bool, R/W) ๐Ÿงš: true if this file is a "system" file from the Windows API's point of view.
  • time_access (47, nfstime4, READ) โฐ: the time of the last access to the object by a read that the server satisfied.
  • time_access_set (48, settime4, WRITE) โฐ: sets the time of last access to the object. This is for SETATTR use only ๐Ÿ”’
  • time_backup (49, nfstime4, R/W) ๐Ÿ“ฆ: the time of the last backup of the object.
  • time_create (50, nfstime4, R/W) ๐ŸŽ‚: the time the object was created. This has no relation to the traditional UNIX ctime ("change time") attribute, so don't mix them up ๐Ÿ™ƒ
  • time_delta (51, nfstime4, READ) ๐Ÿ”ฌ: the smallest useful server time granularity.
  • time_metadata (52, nfstime4, READ) ๐Ÿท๏ธ: the time of the last metadata modification of the object.
  • time_modify (53, nfstime4, READ) โœ๏ธ: the time of the last modification to the object.
  • time_modify_set (54, settime4, WRITE) โœ๏ธ: sets the time of last modification to the object. This is for SETATTR use only ๐Ÿ”’
  • mounted_on_fileid (55, uint64, READ) ๐ŸŒณ: like fileid, except that if the target filehandle is the root of a filesystem, it returns the fileid of the underlying directory instead. main character energy for the root directory ๐Ÿ‘‘โœจ

5.7. Time Access โฐ

Okay so time_access is the time of last access to the object by a read that the server satisfied ๐Ÿ‘€ The big question is what counts as an "access," and that depends on the server's operating environment and/or its filesystem semantics ๐Ÿ’… For example, servers obeying POSIX semantics only update time_access for READLINK, READ, and READDIR, and NOT for any of the operations that modify the object's content ๐Ÿ™ˆ And of course, setting the matching time_access_set attribute is another way to modify time_access ๐ŸŽ€

Whenever the file object lives on a writable filesystem, the server should make best efforts to record time_access into stable storage ๐Ÿ’– BUT, to keep performance from tanking (especially when the server is serving the read from its cache), the server MAY cache access time updates and lazily write them to stable storage โœจ It's also totally acceptable to give server administrators the option to disable time_access updates ๐Ÿงš

5.8. Interpreting owner and owner_group ๐Ÿซถ

The recommended attributes owner and owner_group (and also the users and groups inside the acl attribute) are represented as a UTF-8 string ๐ŸŒˆ The UTF-8 string was chosen so nobody gets stuck with a representation tied to one particular underlying implementation at the client or server ๐Ÿ’œ Note that section 6.1 of [RFC2624] gives extra rationale for this, so go read it if you're curious ๐Ÿค“

Here's the vibe: the client and server will each have their own local representation of owner and owner_group, used for local storage or for showing things to the end user ๐Ÿ’… So when these attributes get transferred between client and server, the local representation gets translated into a syntax of the form user@dns_domain ๐ŸŽ€ That way a client and server that don't share a local representation can still translate to a common syntax that both of them can interpret ๐Ÿฆ‹

Security principals work the same way, lowkey: different security mechanisms can represent them in different ways ๐ŸŒธ Servers usually translate these representations into a common format (generally the one used by local storage) so they can figure out which users each principal corresponds to ๐Ÿ”ฅ When those local identifiers get translated into the form of the owner attribute, the files created by those principals end up associated with the users they identify, in a common format ๐Ÿ‘‘

The translation used to interpret owner and group strings is NOT specified as part of the protocol ๐Ÿ˜Œ This is intentional, so that various solutions can be employed ๐Ÿ“ For example, a local translation table might be consulted to map a numeric id to the user@dns_domain syntax, or a name service might do the job ๐Ÿงš A server can also provide a more general service that isn't limited by any particular translation (one that would only translate a limited set of possible strings) by storing the owner and owner_group attributes in local storage without any translation at all โœจ Or it can augment a translation method by storing the entire string for attributes where no translation is available, while using the local representation for the cases where a translation exists ๐Ÿ’…

Servers that don't support all possible values of owner and owner_group should return an error (NFS4ERR_BADOWNER) when a string with no translation is presented as the value to set in a SETATTR of the owner, owner_group, or acl attributes ๐Ÿ™…โ€โ™€๏ธ When a server does accept an owner or owner_group value as valid on a SETATTR (and similarly for the owner and group strings in an acl), it's making a promise to return that same string when a corresponding GETATTR is done ๐Ÿคž Configuration changes and ill-constructed name translations (the ones that contain aliasing) may make that promise impossible to keep ๐Ÿ˜ญ Servers should make appropriate efforts to avoid a situation where these attributes have their values changed when no real change to ownership has actually occurred ๐Ÿซถ

The "dns_domain" portion of the owner string is meant to be a DNS domain name, like user@ietf.org ๐ŸŒ Servers should accept as valid a set of users for at least one domain ๐Ÿ’– A server may treat other domains as having no valid translations ๐Ÿ™ˆ A more general service is provided when a server can accept users for multiple domains, or for all domains, subject to security constraints ๐Ÿ‘‘

In the case where there's no translation available to the client or server, the attribute value must be constructed WITHOUT the "@" ๐ŸŽ€ So the absence of the @ from the owner or owner_group attribute signifies that no translation was available at the sender, and the receiver should not use that string as a basis for translation into its own internal format ๐Ÿฆ‹ Even though the value can't be translated, it may still be useful ๐Ÿ’… For a client, the attribute string can be used for local display of ownership ๐ŸŒธ

To give a greater degree of compatibility with previous versions of NFS (v2 and v3), which identified users and groups by 32-bit unsigned uid's and gid's, owner and group strings made up of decimal numeric values with no leading zeros can be given a special interpretation by clients and servers that choose to support it ๐Ÿ”ฅ The receiver may treat such a user or group string as representing the same user that a v2/v3 uid or gid with that numeric value would represent ๐Ÿงš A server isn't obligated to accept such a string, but it may return an NFS4ERR_BADOWNER instead ๐Ÿคทโ€โ™€๏ธ To keep this mechanism from being used to subvert user and group translation (so a client could pass all of the owners and groups in numeric form), a server SHOULD return an NFS4ERR_BADOWNER error when there IS a valid translation for the user or owner designated in this way ๐Ÿšซ In that case, the client must use the appropriate name@domain string and NOT the special form for compatibility ๐Ÿ’€

The owner string "nobody" may be used to designate an anonymous user ๐Ÿฅน It's associated with a file created by a security principal that can't be mapped through normal means to the owner attribute ๐ŸŒˆ

5.9. Character Case Attributes ๐Ÿ”ค

For the case_insensitive and case_preserving attributes, each UCS-4 character (which UTF-8 encodes) has a "long descriptive name" [RFC1345] that may or may not include the word "CAPITAL" or "SMALL" โœจ The presence of SMALL or CAPITAL lets an NFS server implement unambiguous and efficient table-driven mappings for case-insensitive comparisons, and for non-case-preserving storage ๐Ÿง  For general character handling and internationalization issues, see the section "Internationalization" ๐ŸŒ

5.10. Quota Attributes ๐Ÿ’ธ

For the attributes related to filesystem quotas, the following definitions apply ๐Ÿ“

quota_avail_soft The value in bytes that represents the amount of additional disk space that can be allocated to this file or directory before the user may reasonably be warned ๐Ÿซง It's understood that this space may be consumed by allocations to other files or directories, though there is a rule as to which other files or directories ๐Ÿ‘€

quota_avail_hard The value in bytes that represents the amount of additional disk space beyond the current allocation that can be allocated to this file or directory before further allocations will be refused ๐Ÿšง It's understood that this space may be consumed by allocations to other files or directories ๐Ÿ’…

quota_used The value in bytes that represents the amount of disc space used by this file or directory and possibly a number of other similar files or directories, where the set of "similar" meets at least the criterion that allocating space to any file or directory in the set will reduce the quota_avail_hard of every other file or directory in the set ๐Ÿงฎ Note that there may be a number of distinct but overlapping sets of files or directories for which a quota_used value is maintained (for example, "all files with a given owner" or "all files with a given group owner") ๐Ÿฆ‹

The server is at liberty to choose any of those sets, but it should do so in a repeatable way ๐Ÿ” The rule may be configured per-filesystem, or it may be "choose the set with the smallest quota" ๐ŸŒธ

5.11. Access Control Lists โœจ

okay bestie, the NFS version 4 ACL attribute is an array of access control entries (ACE) ๐Ÿ’… The client is allowed to read and write it, but in the NFSv4 model the server is the one doing all the access control, based on its own interpretation of the ACL ๐Ÿ” Wanna check access without doing an operation that modifies or reads data or metadata? No problem, the client can use the OPEN and ACCESS operations for that ๐Ÿฆ‹ There are different ACE types, which you can see in Section "ACE type" below. The server tells the client which ACE types it supports by returning the right value in the aclsupport attribute ๐ŸŽ€ Each ACE covers one or more operations on a file or directory (see Section "ACE Access Mask"), and it can also have one or more flags that change how the ACE works (see Section "ACE flag") ๐Ÿ’–

The NFS ACE attribute is defined like this:

      typedef uint32_t        acetype4;
      typedef uint32_t        aceflag4;
      typedef uint32_t        acemask4;

      struct nfsace4 {
              acetype4        type;
              aceflag4        flag;
              acemask4        access_mask;
              utf8str_mixed   who;
      };

So here's the tea โ˜• To figure out if a request succeeds, the server goes through each nfsace4 entry in order. It only looks at ACEs whose "who" matches the requester. Each ACE gets processed until every bit of the requester's access has been ALLOWED ๐ŸŒธ Once a bit has been ALLOWED by an ACCESS_ALLOWED_ACE (see below), it's out of the picture for any later ACEs. If an ACCESS_DENIED_ACE shows up and the requester's access still has unALLOWED bits in common with that ACE's "access_mask", the request gets denied ๐Ÿ™…โ€โ™€๏ธ ALLOWED and DENIED are the main plot, but the ALARM and AUDIT ACE types don't change a requester's access at all. They're there for triggering events based on what the requester tries to do ๐Ÿšจ

So all AUDIT and ALARM ACEs get processed until the end of the ACL ๐Ÿ‘€ Once the whole ACL is processed, if there are bits in the requester's mask that never got considered, whether the server allows or denies that access is undefined ๐Ÿ’€ But if there's a mode attribute on the file, this can't happen, because the mode's MODE4_*OTH bits map to EVERYONE@ ACEs that clearly spell out the requester's access ๐Ÿงš

The NFS version 4 ACL model is honestly super rich ๐Ÿคฉ Some server platforms have access control that goes beyond the UNIX-style mode attribute, but it's not as rich as the NFS ACL model. So users can still benefit from that more limited functionality, the server may say it supports ACLs as long as it follows the guidelines for mapping its ACL model onto the NFS version 4 ACL model ๐Ÿ“

It gets a bit messy because a server can have multiple modules that enforce ACLs ๐Ÿ™ˆ For example, enforcement for NFS version 4 access might be different from enforcement for local access, and both might be different again from enforcement for access through other protocols like SMB. So it can be useful for a server to accept an ACL even if not all of its modules can support it ๐Ÿซถ

The guiding principle in all cases is that the server must not accept ACLs that appear to make the file more secure than it really is. ๐ŸŒˆ

5.11.1. ACE type ๐Ÿฆ‹

   Type         Description
   _____________________________________________________
   ALLOW        Explicitly grants the access defined in
                acemask4 to the file or directory.

   DENY         Explicitly denies the access defined in
                acemask4 to the file or directory.

   AUDIT        LOG (system dependent) any access
                attempt to a file or directory which
                uses any of the access methods specified
                in acemask4.

   ALARM        Generate a system ALARM (system
                dependent) when any access attempt is
                made to a file or directory for the
                access methods specified in acemask4.

A server doesn't have to support all of the ACE types above, no pressure ๐Ÿ’… The bitmask constants for those definitions, used inside the aclsupport attribute, look like this:

   const ACL4_SUPPORT_ALLOW_ACL    = 0x00000001;
   const ACL4_SUPPORT_DENY_ACL     = 0x00000002;
   const ACL4_SUPPORT_AUDIT_ACL    = 0x00000004;
   const ACL4_SUPPORT_ALARM_ACL    = 0x00000008;

The "type" field means exactly what the descriptions above say it means ๐ŸŽฏ The constants for the type field (acetype4) are:

   const ACE4_ACCESS_ALLOWED_ACE_TYPE      = 0x00000000;
   const ACE4_ACCESS_DENIED_ACE_TYPE       = 0x00000001;
   const ACE4_SYSTEM_AUDIT_ACE_TYPE        = 0x00000002;
   const ACE4_SYSTEM_ALARM_ACE_TYPE        = 0x00000003;

Clients should not try to set an ACE unless the server claims support for that ACE type ๐Ÿ™…โ€โ™€๏ธ If the server gets a request to set an ACE that it can't store, it MUST reject the request with NFS4ERR_ATTRNOTSUPP ๐Ÿ˜ญ If the server gets a request to set an ACE that it can store but can't enforce, the server SHOULD reject the request with NFS4ERR_ATTRNOTSUPP ๐Ÿšซ

Example time ๐ŸŽ€ Say a server can enforce NFS ACLs for NFS access but can't enforce ACLs for local access. If arbitrary processes can run on the server, then the server SHOULD NOT indicate ACL support ๐Ÿ˜ฌ But if only trusted administrative programs run locally, then the server may indicate ACL support โœจ

5.11.2. ACE Access Mask ๐ŸŽ€

The access_mask field holds values based on this list:

   Access                 Description
   _______________________________________________________________
   READ_DATA              Permission to read the data of the file
   LIST_DIRECTORY         Permission to list the contents of a
                          directory
   WRITE_DATA             Permission to modify the file's data
   ADD_FILE               Permission to add a new file to a
                          directory
   APPEND_DATA            Permission to append data to a file
   ADD_SUBDIRECTORY       Permission to create a subdirectory to a
                          directory
   READ_NAMED_ATTRS       Permission to read the named attributes
                          of a file
   WRITE_NAMED_ATTRS      Permission to write the named attributes
                          of a file
   EXECUTE                Permission to execute a file
   DELETE_CHILD           Permission to delete a file or directory
                          within a directory
   READ_ATTRIBUTES        The ability to read basic attributes
                          (non-acls) of a file
   WRITE_ATTRIBUTES       Permission to change basic attributes

                          (non-acls) of a file
   DELETE                 Permission to Delete the file
   READ_ACL               Permission to Read the ACL
   WRITE_ACL              Permission to Write the ACL
   WRITE_OWNER            Permission to change the owner
   SYNCHRONIZE            Permission to access file locally at the
                          server with synchronous reads and writes

The bitmask constants for the access mask field are:

   const ACE4_READ_DATA            = 0x00000001;
   const ACE4_LIST_DIRECTORY       = 0x00000001;
   const ACE4_WRITE_DATA           = 0x00000002;
   const ACE4_ADD_FILE             = 0x00000002;
   const ACE4_APPEND_DATA          = 0x00000004;
   const ACE4_ADD_SUBDIRECTORY     = 0x00000004;
   const ACE4_READ_NAMED_ATTRS     = 0x00000008;
   const ACE4_WRITE_NAMED_ATTRS    = 0x00000010;
   const ACE4_EXECUTE              = 0x00000020;
   const ACE4_DELETE_CHILD         = 0x00000040;
   const ACE4_READ_ATTRIBUTES      = 0x00000080;
   const ACE4_WRITE_ATTRIBUTES     = 0x00000100;
   const ACE4_DELETE               = 0x00010000;
   const ACE4_READ_ACL             = 0x00020000;
   const ACE4_WRITE_ACL            = 0x00040000;
   const ACE4_WRITE_OWNER          = 0x00080000;
   const ACE4_SYNCHRONIZE          = 0x00100000;

Server implementations don't have to offer the level of control these masks imply ๐Ÿง For example, POSIX-based systems might not tell APPEND_DATA (adding to a file) apart from WRITE_DATA (changing what's already in it), so both masks get tied to one "write" permission ๐Ÿ’œ When a server like that sends attributes back to the client, it shows both APPEND_DATA and WRITE_DATA if and only if the write permission is enabled ๐Ÿฐ

If a server gets a SETATTR request it can't implement accurately, it should err on the side of more restricted access ๐Ÿ”’ Say a server can't tell overwriting data apart from appending new data, like we just talked about. If a client submits an ACE where APPEND_DATA is set but WRITE_DATA isn't (or the other way around), the server should reject the request with NFS4ERR_ATTRNOTSUPP ๐Ÿ™…โ€โ™€๏ธ Still, if the ACE has type DENY, the server may quietly turn on the other bit, so that both APPEND_DATA and WRITE_DATA get denied ๐Ÿซฃ

5.11.3. ACE flag ๐ŸŽ€

okay so the "flag" field is where the ACE gets its extra powers, and the values are literally described right here, bestie ๐Ÿ’…

ACE4_FILE_INHERIT_ACE Can be placed on a directory, and it says this ACE should get copied into every new non-directory file that gets created in there ๐Ÿ“„

ACE4_DIRECTORY_INHERIT_ACE Also goes on a directory, and it says this ACE should get copied into every new directory created in there ๐Ÿ“

ACE4_INHERIT_ONLY_ACE Can be placed on a directory but it does NOT apply to that directory itself. It only shows up on the newly created files and directories that the two flags above describe ๐Ÿงš

ACE4_NO_PROPAGATE_INHERIT_ACE Can be placed on a directory too, and this one is giving plot twist ๐Ÿ™ˆ Normally, when a new directory gets made and the parent has an ACE marked ACL4_DIRECTORY_INHERIT_ACE, you end up with two ACEs on the new directory. One is for the directory itself, and the other is an inheritable ACE for any new directories created inside it. This flag tells the server to skip putting that second, inheritable ACE on the newly created directory, so it won't pass down to that directory's subdirectories โœ‚๏ธ

ACE4_SUCCESSFUL_ACCESS_ACE_FLAG

ACL4_FAILED_ACCESS_ACE_FLAG Okay, these two are the SUCCESS and FAILED flags, and they only matter for the AUDIT (ACE4_SYSTEM_AUDIT_ACE_TYPE) and ALARM (ACE4_SYSTEM_ALARM_ACE_TYPE) ACE types ๐Ÿ‘€ Here's the vibe: while the server is going through the file's ACL, if it runs into an AUDIT or ALARM ACE that matches the principal trying to OPEN, it makes a note of that, plus whichever SUCCESS and FAILED flags that ACE has (if any). Then, once it's finished with the ACL processing, the share reservation processing, and the OPEN call itself, it checks whether the OPEN worked or didn't. If the OPEN succeeded and the matching AUDIT or ALARM had SUCCESS set, the appropriate AUDIT or ALARM event happens. If the OPEN failed and the matching AUDIT or ALARM had FAILED set, the appropriate event happens. Totally fine to set either or both of them, but if neither is set, the AUDIT or ALARM ACE is basically useless ๐Ÿซ 

The same processing applies to the ACCESS operation too, with one twist. "Success" and "failure" don't mean whether ACCESS returns NFS4_OK. Success means ACCESS returns all of the requested and supported bits. Failure means ACCESS failed to return a bit that was requested and supported ๐ŸŒธ

ACE4_IDENTIFIER_GROUP Says that the "who" refers to a GROUP as defined under UNIX ๐ŸŒˆ

The bitmask constants for the flag field look like this:

const ACE4_FILE_INHERIT_ACE             = 0x00000001;
const ACE4_DIRECTORY_INHERIT_ACE        = 0x00000002;
const ACE4_NO_PROPAGATE_INHERIT_ACE     = 0x00000004;
const ACE4_INHERIT_ONLY_ACE             = 0x00000008;
const ACE4_SUCCESSFUL_ACCESS_ACE_FLAG   = 0x00000010;
const ACE4_FAILED_ACCESS_ACE_FLAG       = 0x00000020;
const ACE4_IDENTIFIER_GROUP             = 0x00000040;

A server doesn't have to support any of these flags at all. If a server supports flags that are similar to these but not exactly the same, it can define a mapping between the protocol flags and its own internal flags. The big guiding principle here is that the file should never appear more secure than it really is ๐Ÿ”

Here's an example. Say a client tries to set an ACE with ACE4_FILE_INHERIT_ACE on but not ACE4_DIRECTORY_INHERIT_ACE. If the server doesn't support any form of ACL inheritance, it should reject the request with NFS4ERR_ATTRNOTSUPP. If the server has a single "inherit ACE" flag that covers both files and directories, it may reject the request, which means making the client set both the file and directory inheritance flags. It may also accept the request and quietly turn on ACE4_DIRECTORY_INHERIT_ACE for you ๐Ÿคทโ€โ™€๏ธ


5.11.4. ACE who ๐Ÿ’œ

There are a few special identifiers ("who") that need to be understood universally, not just inside some particular DNS domain. Some of these can't be understood when an NFS client is accessing the server, but they DO have meaning when a local process accesses the file. Being able to display and modify these permissions is allowed over NFS, even if none of the access methods on the server understands the identifiers ๐Ÿฆ‹

Who                    Description
_______________________________________________________________
"OWNER"                The owner of the file.
"GROUP"                The group associated with the file.
"EVERYONE"             The world.
"INTERACTIVE"          Accessed from an interactive terminal.
"NETWORK"              Accessed via the network.
"DIALUP"               Accessed as a dialup user to the server.
"BATCH"                Accessed from a batch job.
"ANONYMOUS"            Accessed without any authentication.
"AUTHENTICATED"        Any authenticated user (opposite of
                       ANONYMOUS)
"SERVICE"              Access from a system service.

To avoid any conflicts, these special identifiers are told apart by tacking on an "@" and showing up in the form "xxxx@". There's no domain name after the "@", so it's just the bare name plus the @ sign. For example: ANONYMOUS@ โœจ


5.11.5. Mode Attribute ๐Ÿ“

The NFS version 4 mode attribute is based on the UNIX mode bits. These bits are defined:

   const MODE4_SUID = 0x800;  /* set user id on execution */
   const MODE4_SGID = 0x400;  /* set group id on execution */
   const MODE4_SVTX = 0x200;  /* save text even after use */
   const MODE4_RUSR = 0x100;  /* read permission: owner */
   const MODE4_WUSR = 0x080;  /* write permission: owner */
   const MODE4_XUSR = 0x040;  /* execute permission: owner */
   const MODE4_RGRP = 0x020;  /* read permission: group */
   const MODE4_WGRP = 0x010;  /* write permission: group */
   const MODE4_XGRP = 0x008;  /* execute permission: group */
   const MODE4_ROTH = 0x004;  /* read permission: other */
   const MODE4_WOTH = 0x002;  /* write permission: other */
   const MODE4_XOTH = 0x001;  /* execute permission: other */

MODE4_RUSR, MODE4_WUSR, and MODE4_XUSR apply to the principal identified in the owner attribute. MODE4_RGRP, MODE4_WGRP, and MODE4_XGRP apply to the principals identified in the owner_group attribute. MODE4_ROTH, MODE4_WOTH, and MODE4_XOTH apply to any principal that doesn't match the owner, and doesn't have a group matching the owner_group attribute ๐Ÿ‘‘

The remaining bits aren't defined by this protocol, and they MUST NOT be used. The minor version mechanism is the way to define any further bit usage ๐Ÿšซ

Also, a little UNIX lore: if a file has the MODE4_SGID bit set and no MODE4_XGRP bit set, then READ and WRITE must use mandatory file locking ๐Ÿ”’


5.11.6. Mode and ACL Attribute ๐Ÿซถ

A server that supports both mode and ACL needs to be careful to keep the MODE4_*USR, MODE4_*GRP, and MODE4_*OTH bits in sync with the ACEs whose who fields are "OWNER@", "GROUP@", and "EVERYONE@". That way, the client sees semantically equivalent access permissions whether it asks for the owner, owner_group, and mode attributes, or just the ACL ๐Ÿชž

Because the mode attribute includes bits (like MODE4_SVTX) that have nothing to do with ACL semantics, clients are allowed to specify both the ACL attribute and the mode in the same SETATTR operation. Here's the catch, though: there's no set order for processing attributes in a SETATTR. So the client has to make sure the ACL attribute, if given without mode, would produce the desired mode bits. And the other way around: the mode attribute, if given without ACL, would produce the desired "OWNER@", "GROUP@", and "EVERYONE@" ACEs. Real talk, it's a lot to juggle ๐Ÿ˜ญ


5.11.7. mounted_on_fileid ๐Ÿงšโ€โ™€๏ธ

UNIX-based operating environments hook a filesystem into the namespace by mounting it onto an existing file object (the mount point, usually a directory) of another filesystem. When you read the mount point's parent directory through an API like readdir(), the results are directory entries, and each one has a component name and a fileid. The fileid of the mount point's directory entry is different from the fileid that the stat() system call returns. That's because stat() returns the fileid of the root of the mounted filesystem, while readdir() returns the fileid that stat() would have returned before any filesystems were mounted on the mount point ๐ŸŒˆ

Unlike NFS version 3, NFS version 4 lets a client's LOOKUP request cross other filesystems. The client spots a filesystem crossing whenever the filehandle argument of LOOKUP has an fsid attribute that's different from the one on the filehandle LOOKUP returns. A UNIX-based client will call this a "mount point crossing" โœจ UNIX also has a legacy scheme for letting a process figure out its current working directory. It depends on readdir() of a mount point's parent and stat() of the mount point returning fileids the way we just talked about. The mounted_on_fileid attribute corresponds to the fileid that readdir() would have returned, as described above ๐Ÿฆ‹

Now, the NFS version 4 client could just make up a fileid matching whatever mounted_on_fileid provides. (And if the server doesn't support mounted_on_fileid, the client has no choice.) But that's risky, because the client might generate a fileid that clashes with one already assigned to another object in the filesystem ๐Ÿ˜ฌ So if the server can provide mounted_on_fileid, the potential for client operational problems in this area goes away.

If the server detects that there's no mount point at the target file object, the value of mounted_on_fileid that it returns is the same as the fileid attribute ๐ŸŒธ

The mounted_on_fileid attribute is RECOMMENDED, so the server SHOULD provide it if possible. For a UNIX-based server, this is pretty straightforward. Usually mounted_on_fileid gets requested during a READDIR operation, and then it's trivial (at least for UNIX-based servers) to return it, since it equals the fileid of a directory entry returned by readdir(). If mounted_on_fileid is requested in a GETATTR operation, the server should obey an invariant: it returns a value equal to the file object's entry in the object's parent directory, which is what readdir() would have returned. Some operating environments let a series of two or more filesystems be mounted onto a single mount point. In that case, for the server to obey the invariant we just talked about, it needs to find the base mount point, not the intermediate mount points ๐ŸŽ€

6. Filesystem Migration and Replication โœจ

okay so bestie, the recommended attribute fs_locations is how an NFS version 4 server gets to offer filesystem migration or replication services ๐Ÿฆ‹. For these purposes, a filesystem is defined as all the files that share a given fsid (the major and minor values are both the same) ๐Ÿ’….

The fs_locations attribute gives you a list of filesystem locations. Each location is specified by a server name (either a DNS domain or an IP address) plus the path name for the root of the filesystem ๐Ÿ—บ๏ธ. Depending on what kind of service is happening, the list might give a brand new location or a whole set of alternate locations for the filesystem. Then the client uses that info to redirect its requests to the new server ๐Ÿš€.

6.1. Replication

Filesystem replication is expected to be used for read-only data, so it's giving copies ๐Ÿ“š. Usually the filesystem gets replicated on two or more servers, and the fs_locations attribute hands the client the list of those spots. On first access of the filesystem, the client should grab the value of the fs_locations attribute ๐Ÿ™Œ. If the client later finds the server unresponsive, the client may try another server that's listed in fs_locations ๐Ÿƒโ€โ™€๏ธ.

If it applies, the client must take the proper steps to recover valid filehandles from the new server. This gets covered in more detail in the following sections ๐Ÿงš.

6.2. Migration

Migration is how you move a filesystem from one server to another ๐Ÿšš. It's typically used for a filesystem that is writable and has a single copy, and the expected use is load balancing or general resource reallocation โš–๏ธ. The protocol does NOT spell out how the filesystem gets moved between servers. That server-to-server transfer is left up to the server implementor ๐Ÿ› ๏ธ. What IS specified here is how the migration event gets communicated between client and server ๐Ÿ“ฃ.

Once the servers involved have finished moving the filesystem, the error NFS4ERR_MOVED gets returned for any later requests that hit the original server ๐Ÿ™…โ€โ™€๏ธ. NFS4ERR_MOVED is returned for all operations except PUTFH and GETATTR. When the client gets NFS4ERR_MOVED, it obtains the value of the fs_locations attribute, then uses the contents of that attribute to redirect its requests to the specified server ๐Ÿ’จ. To make GETATTR work smoothly, operations such as PUTFH also have to be accepted by the server for the migrated filesystem's filehandles ๐Ÿ’•. Note that if the server returns NFS4ERR_MOVED, the server MUST support the fs_locations attribute. No skipping this one, bestie ๐Ÿซถ.

If the client asks for more attributes than just fs_locations, the server may return fs_locations only. This is totally expected, since the server has already migrated the filesystem and may not have a way to get the extra attribute data ๐Ÿคทโ€โ™€๏ธ.

The server implementor really needs to be careful when building a migration solution ๐Ÿง. The server has to consider ALL of the state information clients might have outstanding at the server. That includes, but isn't limited to, locking/share state, delegation state, and asynchronous file writes, which are represented by WRITE and COMMIT verifiers ๐Ÿ”. The server should strive to minimize the impact on its clients during and after the migration process ๐ŸŒธ.

6.3. Interpretation of the fs_locations Attribute

The fs_location attribute is structured in the following way:

   struct fs_location {
           utf8str_cis     server<>;
           pathname4       rootpath;
   };

   struct fs_locations {
           pathname4       fs_root;
           fs_location     locations<>;
   };

The fs_location struct represents where a filesystem lives by providing a server name and the path to the root of the filesystem ๐Ÿ“. For a multi-homed server, or a set of servers that use the same rootpath, an array of server names may be provided. Each entry in the server array is a UTF8 string and represents one of: a traditional DNS host name, an IPv4 address, or an IPv6 address ๐ŸŒ. It's NOT a requirement that all servers sharing the same rootpath get listed in one fs_location struct. The array of server names is just there for convenience. Servers that share the same rootpath can also be listed in separate fs_location entries in the fs_locations attribute ๐Ÿงฉ.

The fs_locations struct and attribute then contain an array of locations. Since each server's name space might be built differently, the fs_root field is provided ๐ŸŒณ. The path represented by fs_root shows where the filesystem lives in the server's name space. So the fs_root path is only associated with the server that the fs_locations attribute was obtained from. The fs_root path is meant to help the client find the filesystem on the various servers listed ๐Ÿ”Ž.

Here's an example, bestie ๐ŸŽ€. There's a replicated filesystem on two servers, servA and servB. On servA the filesystem lives at path "/a/b/c". On servB it lives at path "/x/y/z". The client first accesses the filesystem at servA with a multi-component lookup path of "/a/b/c/d". Because the client used a multi-component lookup to get the filehandle at "/a/b/c/d", it doesn't know that the filesystem's root is at "/a/b/c" in servA's name space ๐Ÿ˜ต. When the client switches to servB, it needs to figure out that the directory it first referenced on servA is now represented by the path "/x/y/z/d" on servB. To make that work, the fs_locations attribute provided by servA would have an fs_root value of "/a/b/c" and two entries in fs_location. One entry is for servA itself, and the other is for servB with a path of "/x/y/z". With that info, the client can swap "/x/y/z" in for the "/a/b/c" at the beginning of its access path and build "/x/y/z/d" to use on the new server ๐Ÿง โœจ.

See the section "Security Considerations" for a discussion of the recommendations for the security flavor to be used by any GETATTR operation that requests the "fs_locations" attribute ๐Ÿ”’. Serious business, so read it ๐Ÿ’€.

6.4. Filehandle Recovery for Migration or Replication

Filehandles for filesystems that are replicated or migrated generally have the same semantics as filesystems that aren't ๐Ÿค. For example, if a filesystem has persistent filehandles and gets migrated to another server, the filehandle values for the filesystem will still be valid at the new server ๐Ÿ’–.

Volatile filehandles are a different story ๐ŸŒช๏ธ. The servers involved likely don't have a mechanism to transfer filehandle format and content between themselves. So a server might struggle to decide whether a volatile filehandle from an old server should return an error of NFS4ERR_FHEXPIRED. That's why the client gets told, through the fh_expire_type attribute, whether volatile filehandles will expire at the migration or replication event ๐Ÿ“ข. If the bit FH4_VOL_MIGRATION is set in the fh_expire_type attribute, the client must treat the volatile filehandle as if the server had returned the NFS4ERR_FHEXPIRED error ๐Ÿซ . At the migration or replication event, when FH4_VOL_MIGRATION is set, the client won't present the original or old volatile filehandle to the new server ๐Ÿ™…โ€โ™€๏ธ. Instead, the client starts talking to the new server by recovering its filehandles using the saved file names ๐Ÿ“‚.

7. NFS Server Name Space

7.1. Server Exports

On a UNIX server, the name space describes all the files reachable by pathnames under the root directory, or "/" ๐Ÿง. On a Windows NT server, the name space is all the files on disks named by mapped disk letters ๐Ÿ’ฟ. NFS server administrators rarely make the entire server's filesystem name space available to NFS clients ๐Ÿ™ˆ. More often, portions of the name space get shared through an "export" feature ๐Ÿ“ค. In previous versions of the NFS protocol, the root filehandle for each export was obtained through the MOUNT protocol: the client sent a string identifying the export of name space, and the server returned the root filehandle for it ๐ŸŽซ. The MOUNT protocol supports an EXPORTS procedure that enumerates the server's exports ๐Ÿ“‹.

7.2. Browsing Exports

The NFS version 4 protocol gives clients a root filehandle they can use to get filehandles for these exports through a multi-component LOOKUP ๐ŸŒฑ. A super common user experience is a graphical "Open" dialog where you progressively browse a directory tree to find a file ๐Ÿ—‚๏ธ. So the client has to be able to move from one export to another via single-component, progressive LOOKUP operations ๐Ÿฆ‹.

This style of browsing isn't well supported by NFS version 2 and 3 protocols ๐Ÿ˜ฌ. The client expects all LOOKUP operations to stay within a single server filesystem. For example, the device attribute won't change. That stops a client from taking name space paths that span exports ๐Ÿšง.

An automounter on the client can grab a snapshot of the server's name space using the EXPORTS procedure of the MOUNT protocol ๐Ÿ“ธ. If it understands the server's pathname syntax, it can create an image of the server's name space on the client ๐Ÿชž. The parts of the name space that the server doesn't export get filled in with a "pseudo filesystem" so the user can browse from one mounted filesystem to another ๐ŸŒ‰. The catch is that this representation is static ๐Ÿฅฒ. If the server administrator adds a new export, the client won't know about it.

7.3. Server Pseudo Filesystem

NFS version 4 servers dodge this name space inconsistency by presenting all the exports inside the framework of a single server name space ๐Ÿ™Œ. An NFS version 4 client uses LOOKUP and READDIR operations to browse seamlessly from one export to another ๐Ÿƒโ€โ™€๏ธ. Portions of the server name space that aren't exported get bridged by a "pseudo filesystem" that only provides a view of exported directories ๐ŸŒˆ. A pseudo filesystem has a unique fsid and behaves like a normal, read-only filesystem ๐Ÿ‘‘.

Depending on how the server's name space is built, it's possible that multiple pseudo filesystems exist ๐Ÿงฉ. For example:

   /a         pseudo filesystem
   /a/b       real filesystem
   /a/b/c     pseudo filesystem
   /a/b/c/d   real filesystem

Each of the pseudo filesystems is treated as a separate entity, so each one gets its own unique fsid ๐Ÿ’œ.

7.4. Multiple Roots

The DOS and Windows operating environments are sometimes described as having "multiple roots" ๐Ÿ’พ. Filesystems are commonly represented as disk letters, while MacOS represents filesystems as top-level names ๐ŸŽ. NFS version 4 servers for these platforms can build a pseudo filesystem above those root names, so disk letters or volume names are simply directory names in the pseudo root ๐ŸŽ€โœจ.


7.5. Filehandle Volatility โœจ

okay so the server's pseudo filesystem is basically a logical map of the filesystems the server offers, which means it's probably built on the fly when the server first spins up ๐Ÿงš it's lowkey possible there's no on-disk copy to build persistent filehandles from. persistent filehandles are preferable (obviously ๐Ÿ’…), but the NFS client should expect that pseudo filesystem filehandles are volatile. you can double check this by looking at the fh_expire_type attribute for those filehandles ๐Ÿ” if they're volatile, the client MUST... wait, no caps here, the original is lowercase: the NFS client must be prepared to recover a filehandle value (e.g., with a multi-component LOOKUP) when it gets an NFS4ERR_FHEXPIRED error. be ready to bounce back ๐Ÿฆ‹

7.6. Exported Root ๐Ÿ‘‘

so if the server's root filesystem is exported, you might think "oh cool, no pseudo-filesystem needed" ๐Ÿ™ˆ nope!! that's a trap. picture this server setup:

         /       disk1  (exported)
         /a      disk2  (not exported)
         /a/b    disk3  (exported)

because disk2 isn't exported, disk3 can't be reached with simple LOOKUPs ๐Ÿ’€ the server has to bridge that gap with a pseudo-filesystem. main character energy for the pseudo-filesystem, honestly ๐ŸŒธ

7.7. Mount Point Crossing ๐ŸŽ€

this one is so iconic. sometimes one filesystem has a directory that's been "covered" or mounted over by a second filesystem. like:

         /a/b            (filesystem 1)
         /a/b/c/d        (filesystem 2)

the pseudo filesystem for that server could look like:

         /               (place holder/not exported)
         /a/b            (filesystem 1)
         /a/b/c/d        (filesystem 2)

it is the server's job to present a complete pseudo filesystem to the client ๐Ÿซถ if the client sends a lookup for the path /a/b/c/d, the server answers with the filehandle of the filesystem /a/b/c/d. in earlier versions of the NFS protocol, the server would've answered with the filehandle of the directory /a/b/c/d inside the filesystem /a/b. totally different vibe ๐Ÿ˜ญ

and the NFS client can tell it's crossing a server mount point when the value of the fsid attribute changes โœจ that's how it knows ๐Ÿ”ฅ

7.8. Security Policy and Name Space Presentation ๐Ÿ”

the implementor has to think really carefully about how the server's security policy gets applied here ๐Ÿง  you might want to limit what parts of the pseudo filesystem a client can see, based on how the server thinks that client can authenticate itself. but with multiple security mechanisms and the ability to negotiate which one gets used, the server can't reliably tell whether a client will be able to authenticate. so if the server limits what's in the pseudo filesystem, it can effectively hide filesystems from a client that actually had legit access. yikes ๐Ÿ˜ฌ

as suggested practice, the server should apply the security policy of a shared resource to the components of that resource's ancestors. for example:

         /
         /a/b
         /a/b/c

/a/b/c is a real filesystem and it's the shared resource. its security policy is Kerberos with integrity. the server should apply that same policy to /, /a, and /a/b ๐Ÿ›ก๏ธ that way the protection of the server's namespace stretches out to the ancestors of the real shared resource. protective bestie behavior ๐Ÿ’œ

and when the server's resources use multiple, disjoint security mechanisms, the security for a particular object in the server's namespace should be the union of all security mechanisms of all of its direct descendants ๐ŸŒˆ

8. File Locking and Share Reservations ๐Ÿ”’

okay this section is where things get stateful, bestie ๐Ÿ’– bringing locking into the NFS protocol makes it stateful by nature. and once you add share reservations, the protocol becomes way more dependent on state than the old-school combo of NFS and NLM [XNFS]. three things keep all that state manageable:

  • Clear division between client and server
  • Ability to reliably detect inconsistency in state between client and server
  • Simple and robust recovery mechanisms

in this model, the server owns the state information ๐Ÿ‘‘ the client tells the server its view of that state whenever it needs to. the client can also spot inconsistent state before it modifies a file, which is so smart ๐Ÿงš

to support Win32 share reservations, the server has to be able to OPEN or CREATE files atomically ๐ŸŽ€ a separate share/unshare operation just wouldn't let you correctly implement the Win32 OpenFile API. so, to get share semantics right, the old NFS mechanisms used when opening or creating a file (LOOKUP, CREATE, ACCESS) had to be replaced. NFS version 4 has an OPEN operation that absorbs the NFS version 3 combo of LOOKUP, CREATE, and ACCESS ๐Ÿ’… but since so many operations need a filehandle, the traditional LOOKUP sticks around to map a file name to a filehandle without creating any state on the server. access and modification policy is managed by the server based on the client's state, and these mechanisms can cover everything from advisory-only locking all the way to full mandatory locking ๐Ÿฆ‹

8.1. Locking ๐Ÿ”

it's assumed that manipulating a lock is rare compared to READ and WRITE operations ๐Ÿ“š it's also assumed that crashes and network partitions are relatively rare. so it matters that READ and WRITE have a lightweight way to say whether they hold a lock. a lock request, on the other hand, carries the heavyweight info needed to establish a lock and uniquely define the lock owner ๐Ÿ“

the next sections walk through the shift from that heavyweight info to the eventual stateid, which is what most client and server locking and lease interactions run on from then on ๐ŸŒธ

8.1.1. Client ID โœจ

okay so, for every LOCK request, the client must identify itself to the server ๐Ÿซถ this is what makes correct lock identification and crash recovery possible. to get that identification set up on the server, you need a SETCLIENTID operation followed by a SETCLIENTID_CONFIRM operation. it's literally required, no skipping ๐Ÿ’…

when a new incarnation of the client does this, it also immediately breaks any leased state that an older incarnation of the client might still have on the server. the new client doesn't have to sit around waiting for those leases to expire, which is so iconic of it ๐Ÿ˜ญ breaking the lease state means the server removes all lock, share reservation, and, where the server is not supporting the CLAIM_DELEGATE_PREV claim type, all delegation state tied to that same client with the same identity. for how delegation state gets recovered, check the section "Delegation Recovery" ๐Ÿฆ‹

client identification is wrapped up in this structure:

         struct nfs_client_id4 {
                 verifier4     verifier;
                 opaque        id<NFS4_OPAQUE_LIMIT>;
         };

the first field, verifier, is a client incarnation verifier. it's how the server detects client reboots ๐Ÿ”ฅ the server only starts canceling the client's leased state if the verifier is different from the one it previously recorded for that client. the client is identified by the second field of the structure, id.

the second field, id, is a variable length string that uniquely defines the client. there are a few things to think about when the client generates this string, and honestly it's giving careful planning ๐ŸŒธ

  • the string should be unique so that multiple clients don't present the same string. if two clients do, the result can range from one client getting an error to one client having its leased state abruptly and unexpectedly canceled ๐Ÿ’€
  • the string should be selected so that later incarnations (like reboots) of the same client present the same string. implementors are cautioned against an approach that requires the string to be recorded in a local file, because that rules out environments with no local disk where all file access goes through an NFS version 4 server ๐Ÿ™ˆ
  • the string should be different for each server network address that the client accesses, rather than common to all of them. the reason is that the client may not be able to tell whether the same server is listening on multiple network addresses. if the client sends SETCLIENTID with the same id string to each of those addresses, the server thinks it's the same client, and each successive SETCLIENTID makes the server start removing the client's previous leased state ๐Ÿ˜ฑ
  • the algorithm for generating the string should not assume the client's network address won't change. that includes changes between client incarnations and even changes while the client is still running in its current incarnation. so if the client only puts its own and the server's network addresses in the id string, there's a real risk. after the client gives up that network address, another client using a similar algorithm could generate a conflicting id string ๐Ÿ“

with all that in mind, a well generated id string is one that includes:

  • the server's network address.
  • the client's network address.
  • for a user level NFS version 4 client, extra information that distinguishes it from other user level clients running on the same host, such as a process id or other unique sequence.
  • additional information that tends to be unique, such as one or more of:
    • the client machine's serial number (for privacy reasons, it's best to run some one way function on the serial number).
    • a MAC address.
    • the timestamp of when the NFS version 4 software was first installed on the client (though this has the same caution as before about information stored in a file, since that file might only be reachable over NFS version 4).
    • a true random number. but since this number ought to be the same between client incarnations, it has the same problem as using the timestamp of the software installation.
๐Ÿšจ security check, bestie: as a security measure, the server MUST NOT cancel a client's leased state if the principal that established the state for a given id string is not the same as the principal issuing the SETCLIENTID. no exceptions, no vibes-based overrides ๐Ÿ›ก๏ธ

also, SETCLIENTID and SETCLIENTID_CONFIRM have a secondary job: they set up the information the server needs to make callbacks to the client, which is what supports delegations ๐ŸŽ€ the client is allowed to change this information through SETCLIENTID and SETCLIENTID_CONFIRM within the same incarnation of the client, and it won't remove the client's leased state while doing it.

once a SETCLIENTID and SETCLIENTID_CONFIRM sequence has successfully completed, the client switches to a shorthand client identifier of type clientid4, instead of carrying around the longer and less compact nfs_client_id4 structure. the server assigns this shorthand (a clientid), and it should be chosen so it won't conflict with a clientid the server previously assigned. that holds across server restarts and reboots too ๐Ÿ‘‘

when a clientid is presented to a server and it isn't recognized (which would happen after a server reboot), the server rejects the request with the error NFS4ERR_STALE_CLIENTID. when that happens, the client must get a new clientid using the SETCLIENTID operation, and then move on to any other recovery needed for the server reboot case. (see the section "Server Failure and Recovery" ๐Ÿงš)

the client must also use the SETCLIENTID operation when it gets a NFS4ERR_STALE_STATEID error on a stateid derived from its current clientid. that's because it also means the server rebooted and invalidated the existing clientid. (see the next section, "lock_owner and stateid Definition", for the details ๐Ÿ’œ)

for the full spec on these operations, go to the detailed descriptions of SETCLIENTID and SETCLIENTID_CONFIRM. they're the source of truth ๐ŸŒˆ


8.1.2. Server Release of Clientid ๐Ÿฆ‹

if the server determines that the client holds no state associated with its clientid, the server may choose to release the clientid. this lets the server avoid wasting resources on clients that are only intermittently active ๐Ÿ’… but here's the tea: if the client contacts the server after a release, the server must make sure the client gets the appropriate error, so the client knows to use the SETCLIENTID/SETCLIENTID_CONFIRM sequence to set up a new identity.

it should be clear that the server has to be super hesitant about releasing a clientid. the work the client has to do to recover from that is the same burden it would face if the server had failed and restarted ๐Ÿ˜ฉ typically, a server wouldn't release a clientid unless there had been no activity from that client for many minutes.

now, if the id string in a SETCLIENTID request is built properly, and the client makes sure to use the same principal for each successive SETCLIENTID, then (barring an active denial of service attack) NFS4ERR_CLID_INUSE should never be returned. โœจ that's the dream.

however, client bugs, server bugs, or a deliberate change of the principal owning the id string can, in rare cases, still result in NFS4ERR_CLID_INUSE. one example is a client that changes security flavors, where under the new flavor there's no mapping to the previous owner ๐Ÿ™ˆ

in that event, when the server gets a SETCLIENTID for a client id that currently has no state, or has state but the lease has expired, the server MUST allow the SETCLIENTID instead of returning NFS4ERR_CLID_INUSE, and it'll confirm the new clientid if it's followed by the appropriate SETCLIENTID_CONFIRM. main character energy from the server here, fr ๐Ÿซถ


8.1.3. lock_owner and stateid Definition ๐Ÿงšโ€โ™€๏ธ

when requesting a lock, the client must present to the server the clientid and an identifier for the owner of the requested lock. those two fields together are called the lock_owner, and here's what each one is:

  • a clientid returned by the server as part of the client's use of the SETCLIENTID operation.
  • a variable length opaque array used to uniquely define the owner of a lock managed by the client. this may be a thread id, process id, or other unique value.

when the server grants the lock, it responds with a unique stateid. the stateid is a shorthand reference to the lock_owner, because the server keeps track of the correspondence between them ๐ŸŽ€

the server can form the stateid in any way it wants, as long as it can recognize invalid and out-of-date stateids. this includes stateids generated by earlier instances of the server. that's how the client gets properly notified of a server restart: the notification happens when the client presents a stateid from a previous instantiation to the server ๐Ÿ”ฅ

the server must be able to tell these situations apart and return the error specified for each:

  • the stateid was generated by an earlier server instance (i.e., before a server reboot). the error NFS4ERR_STALE_STATEID should be returned.
  • the stateid was generated by the current server instance, but it no longer designates the current locking state for the lockowner-file pair in question (i.e., one or more locking operations has occurred). the error NFS4ERR_OLD_STATEID should be returned.

this error condition only happens when the client issues a locking request that changes a stateid while an I/O request using that stateid is still outstanding ๐Ÿ˜ญ

  • the stateid was generated by the current server instance, but it doesn't designate a locking state for any active lockowner-file pair. the error NFS4ERR_BAD_STATEID should be returned.

this error condition happens when there's been a logic error on the part of the client or the server. this should not happen ๐Ÿ’€

one mechanism that can satisfy these requirements is for the server to:

  • divide the "other" field of each stateid into two fields:
    • a server verifier which uniquely designates a particular server instantiation.
    • an index into a table of locking-state structures.
  • utilize the "seqid" field of each stateid, so that seqid is monotonically incremented for each stateid associated with the same index into the locking-state table.

by matching an incoming stateid and its field values against the state held at the server, the server can easily tell whether a stateid is valid for its current instantiation and state. if it's not valid, the server can supply the appropriate error to the client. chef's kiss ๐Ÿ‘‘๐Ÿ’–

8.1.4. Use of the stateid and Locking ๐Ÿ”’โœจ

Okay so bestie, every single READ, WRITE, and SETATTR operation carries a stateid ๐Ÿฆ‹. For this section, a SETATTR that changes the size attribute counts as if it's writing the area between the old and new size (aka the range that gets chopped off or tacked onto the file through SETATTR) ๐Ÿ’…, even when SETATTR isn't explicitly mentioned in the text. Main character energy for SETATTR, no cap ๐Ÿ™ˆ.

If the lock_owner does a READ or WRITE while it has a lock or share reservation on the server (and any OPEN counts as a share reservation ๐ŸŽ€), then the stateid the server handed back earlier MUST be used to show which locks are being held by the lockowner, including both record locks and share reservations ๐Ÿซถ. If the client hasn't set up any state, meaning no record lock and no share reservation, then a stateid of all bits 0 is used ๐ŸŒธ. Either way, whether you're using an all-zeros stateid or one the server gave you, if there's a conflicting share reservation or mandatory record lock sitting on the file, the server MUST refuse to service that READ or WRITE operation ๐Ÿšซ๐Ÿ’€.

Share reservations get made by OPEN operations and they're mandatory by nature ๐Ÿ‘‘. So when OPEN denies READ or WRITE, those operations get rejected with error NFS4ERR_LOCKED ๐Ÿ˜ญ. Record locks can be mandatory or advisory, the server can pick one, or it can decide based on the file being accessed ๐ŸŒˆ. Like, some UNIX-based servers have a "mandatory lock bit" on the mode attribute, and if it's set, record locks are required on the file before any I/O is possible ๐Ÿงš. When record locks are advisory, they only stop conflicting lock requests from being granted and have zero effect on READs or WRITEs ๐Ÿ“. Mandatory record locks, though, stop conflicting I/O operations, and when those get attempted they're rejected with NFS4ERR_LOCKED ๐Ÿ”ฅ. If the client gets NFS4ERR_LOCKED on a file where it knows it really has the proper share reservation, it MUST... well, it will need to issue a LOCK request on the region of the file that covers the region the I/O was meant for ๐Ÿ’–, using an appropriate locktype (READ*_LT for a READ, WRITE*_LT for a WRITE) โœจ.

With NFS version 3 there was no stateid, so there was no way to tell whether the client's application process sending a READ or WRITE had also grabbed the right record lock on the file ๐Ÿ™ˆ. So mandatory locking just wasn't possible. The stateid construct finally removed that barrier ๐ŸŽ‰๐Ÿ’œ.

Note that for UNIX environments that support mandatory file locking, the difference between advisory and mandatory is kinda subtle ๐Ÿค“. Honestly, advisory and mandatory record locks are exactly the same as far as the APIs and implementation requirements go ๐Ÿฅฑ. If the mandatory lock attribute is set on the file, the server checks whether the lockowner has an appropriate shared (read) or exclusive (write) record lock on the region it wants to read or write ๐Ÿ”. If there's no appropriate lock, the server checks for a conflicting lock (it can do this by trying to acquire the conflicting lock on behalf of the lockowner, and if that works, releasing it once the READ or WRITE is done ๐Ÿ”„). If a conflicting lock exists, the server returns NFS4ERR_LOCKED ๐Ÿ˜ค.

For Windows environments, there are no advisory record locks, so the server always checks for record locks during I/O requests ๐ŸชŸโœจ.

So the NFS version 4 LOCK operation doesn't need to care about advisory vs. mandatory record locks ๐Ÿ’โ€โ™€๏ธ. The server's processing of READ and WRITE operations is what actually creates the distinction ๐Ÿ‘‘.

Every stateid except the special stateid values mentioned in this section, whether it came back from an OPEN-type operation (OPEN or OPEN_DOWNGRADE) or a LOCK-type operation (LOCK or LOCKU), defines an access mode for the file: READ, WRITE, or READ-WRITE ๐Ÿฆ‹. That access mode is set by the original OPEN that started the stateid sequence, and it can be changed by later OPENs and OPEN_DOWNGRADEs within that same sequence ๐ŸŒท. When a READ, WRITE, or a SETATTR that specifies the size attribute happens, the operation gets checked against the access mode to make sure it's appropriate for the OPEN it's tied to ๐Ÿ”๐Ÿ’….

For WRITE-type operations (WRITEs, and SETATTRs that set size), the server MUST verify that the access mode allows writing and return an NFS4ERR_OPENMODE error if it doesn't ๐Ÿšจ. For READ, the server can do that same access mode check, or it can choose to allow READ on opens that are WRITE only ๐Ÿซ . This is for clients whose write implementation might unavoidably do reads (for example, because of buffer cache constraints) ๐Ÿงƒ. Even so, if READs are allowed in these cases, the server MUST still check for locks that conflict with the READ (for example, another open that denies READs) ๐Ÿ›‘. A server that does enforce the access mode check on READs doesn't need to separately check for conflicting share reservations, because having an OPEN for read access already guarantees that no conflicting share reservation can exist ๐Ÿ’–.

A stateid of all bits 1 (one) MAY let READ operations skip locking checks at the server ๐Ÿฆ‹. WRITE operations with an all-bits-1 stateid, though, MUST NOT skip locking checks, and they get treated exactly the same as if an all-bits-0 stateid had been used ๐Ÿ™…โ€โ™€๏ธ.

A lock can't be granted while a READ or WRITE using one of the special stateids is happening, if the lock request's range conflicts with the READ or WRITE's range ๐Ÿšง. For this paragraph, a conflict happens when a shared lock is requested while a WRITE is in progress, or when an exclusive lock is requested while either a READ or a WRITE is in progress ๐ŸŒˆ. A SETATTR that sets size gets handled similarly to a WRITE, like we talked about above ๐Ÿซถ.


8.1.5. Sequencing of Lock Requests ๐Ÿ”ข๐Ÿ’•

Okay, locking is different from most NFS operations because it needs "at-most-one" semantics, and ONCRPC doesn't provide those ๐Ÿ˜ค. Running ONCRPC over a reliable transport isn't enough, because a sequence of locking requests can span multiple TCP connections ๐Ÿ›ค๏ธ. If there's retransmission or reordering going on, lock or unlock requests have to behave in a well-defined and consistent way ๐ŸŽ€. To make that happen, each lock request carries a sequence number, which is a consecutively increasing integer ๐Ÿ”ขโœจ. Different lock_owners get their own separate sequences ๐Ÿฆ‹. The server keeps track of the last sequence number it received (L) along with the response it sent back ๐Ÿ’œ. The very first request for any lock_owner goes out with a sequence number of zero ๐Ÿผ.

For requests that carry a sequence number, there should be no more than one outstanding request per lock_owner ๐Ÿ™Œ.

If a request (r) comes in with a sequence number earlier than the last one (r < L), the server rejects it with error NFS4ERR_BAD_SEQID ๐Ÿšซ. With a properly functioning client, the response to r must have arrived before the last request (L) was sent ๐Ÿง . If a duplicate of the last request comes in (r == L), the stored response gets returned ๐Ÿ”. If a request comes in beyond the next sequence number (r == L + 2), it's rejected with error NFS4ERR_BAD_SEQID ๐Ÿ˜ฌ. Sequence history gets reinitialized whenever the SETCLIENTID/SETCLIENTID_CONFIRM sequence changes the client verifier ๐ŸŒธ.

Because the sequence number is an unsigned 32-bit integer, all the arithmetic on it is mod 2^32 ๐Ÿงฎ. If you want an example of modulo arithmetic involving sequence numbers, check out [RFC793] ๐Ÿ“–.

It's critical that the server keeps the last response it sent to the client, because that gives a more reliable cache of duplicate non-idempotent requests than the traditional cache described in [Juszczak] ๐Ÿ›ก๏ธ๐Ÿ’–. The traditional duplicate request cache uses a least recently used algorithm to decide what to throw out ๐Ÿ—‘๏ธ. But the last lock request and response on a given lock_owner MUST be cached for as long as the lock state exists on the server ๐Ÿ”’โœจ.

The client MUST monotonically increment the sequence number for the CLOSE, LOCK, LOCKU, OPEN, OPEN_CONFIRM, and OPEN_DOWNGRADE operations ๐Ÿ“ˆ. This holds even if the previous operation that used the sequence number got an error ๐Ÿ˜ญ. The only exception is if the previous operation got one of these errors: NFS4ERR_STALE_CLIENTID, NFS4ERR_STALE_STATEID, NFS4ERR_BAD_STATEID, NFS4ERR_BAD_SEQID, NFS4ERR_BADXDR, NFS4ERR_RESOURCE, NFS4ERR_NOFILEHANDLE ๐Ÿ™.


8.1.6. Recovery from Replayed Requests ๐Ÿ”๐Ÿซง

As we said above, the sequence number is per lock_owner ๐Ÿฆ‹. As long as the server keeps the last sequence number it received and follows the methods above, there's no risk of a Byzantine router re-sending old requests ๐Ÿ›ก๏ธ. The server only needs to hold the (lock_owner, sequence number) state for as long as there are open files, or closed files with locks outstanding ๐Ÿงƒ.

LOCK, LOCKU, OPEN, OPEN_DOWNGRADE, and CLOSE each carry a sequence number, so the risk of these operations being replayed and causing undesired effects is non-existent while the server keeps the lock_owner state ๐Ÿ’…โœจ.


8.1.7. Releasing lock_owner State ๐Ÿซง๐ŸŒ™

When a particular lock_owner no longer holds any open or file locking state on the server, the server may choose to release the sequence number state tied to that lock_owner ๐ŸŒท. It might do this based on lease expiration, to reclaim server memory, or because of other implementation-specific details ๐Ÿงน. Either way, the server can only do this safely when the lock_owner isn't being used by the client anymore ๐Ÿ”. The server can also keep the lock_owner state in case retransmitted requests show up, but how long to hold onto it is implementation-specific ๐Ÿ•ฐ๏ธ.

If a LOCK, LOCKU, OPEN_DOWNGRADE, or CLOSE gets retransmitted after the server has already released the lock_owner state, the server will find that the lock_owner has no files open, and an error gets returned to the client ๐Ÿ˜ฑ. If the lock_owner does still have a file open, the stateid won't match, and again an error is returned to the client ๐Ÿšซ๐Ÿ’œ.

8.1.8. Use of Open Confirmation โœจ

okay so picture this: an OPEN gets retransmitted, and the lock_owner is brand new or the server already let go of its state ๐Ÿ˜ญ. That's where OPEN_CONFIRM saves the day and stops things from going wrong. The moment the server sees a lock_owner for the first time, it tells the client to do the OPEN_CONFIRM for that OPEN. That sequence sets up the lock_owner and its sequence number ๐Ÿ’…. Since it's linking a brand-new open_owner on the server to an existing open_owner on the client, the sequence number can be any value. The OPEN_CONFIRM step is how the server double-checks that the value it got is the correct one ๐Ÿซถ. For more details, check the section "OPEN_CONFIRM - Confirm Open" ๐Ÿฆ‹.

Now, there are a bunch of situations where making someone confirm an OPEN would be a total hassle. Both sides would be stuck waiting before they can act on info that's still provisional, and that info could get deleted if it never gets confirmed ๐Ÿ™ˆ. The good news is the server can skip the confirmation step when it's answering open requests in those cases. There are two constraints:

  • The server must not bestow a delegation for any open which would require confirmation.
  • The server MUST NOT require confirmation on a reclaim-type open (i.e., one specifying claim type CLAIM_PREVIOUS or CLAIM_DELEGATE_PREV).

These two are connected, because reclaim-type opens are the only ones where the server might have to send a delegation. For CLAIM_NULL, sending the delegation is optional, and for CLAIM_DELEGATE_CUR, no delegation gets sent ๐Ÿ“.

Delegations sent with an open that needs confirmation are messy ngl, because undoing things after a non-confirmation adds way too much complexity to the protocol ๐Ÿ˜ตโ€๐Ÿ’ซ. Requiring confirmation on reclaim-type opens is also a problem: you can't figure out the reclaim's status until the lease expires, so it's hard to know in time which locks are being reclaimed, and the grace period might run out before you do ๐Ÿ˜ญ.

Skipping confirmation on reclaim-type opens is totally doable because of where these opens happen. For CLAIM_PREVIOUS opens, that's right after a server reboot, so there's no time for lockowners to get created, found unused, and recycled ๐ŸŒธ. For CLAIM_DELEGATE_PREV opens, we're dealing with a client reboot. A server that supports delegation can be sure no lockowners for that client have been recycled since the client initialized, so it can make sure confirmation isn't required ๐Ÿ’œ.


8.2. Lock Ranges ๐ŸŽ€

The protocol lets a lock owner ask for a lock over a byte range, and then upgrade or unlock a sub-range of that initial lock. This is expected to be an uncommon kind of request ๐ŸŒˆ. Also, servers or server filesystems might not be able to support sub-range lock semantics at all. If a server gets a locking request that's a sub-range of the lock owner's current locking state, it's allowed to return the error NFS4ERR_LOCK_RANGE to say it doesn't support sub-range lock operations. So the client should be ready to get this error and, if it makes sense, pass it along to the requesting application โœจ.

The client is discouraged from mashing together several independent lock ranges that just happen to be adjacent into one request. That's because the server might not support sub-range requests, and also because of how file locking state gets recovered after a server failure. As the section "Server Failure and Recovery" explains below, the server may use certain optimizations during recovery that only work well when the client behaves during lock recovery the way it did before the failure ๐Ÿฆ‹.


8.3. Upgrading and Downgrading Locks ๐Ÿ’–

If a client holds a write lock on a record, it can ask for an atomic downgrade to a read lock through the LOCK request by setting the type to READ_LT. If the server supports atomic downgrade, the request succeeds. If not, it returns NFS4ERR_LOCK_NOTSUPP. The client should be ready for this error and, if it makes sense, report it to the requesting application ๐Ÿซถ.

If a client holds a read lock on a record, it can ask for an atomic upgrade to a write lock through the LOCK request by setting the type to WRITE_LT or WRITEW_LT. If the server doesn't support atomic upgrade, it returns NFS4ERR_LOCK_NOTSUPP. If the upgrade can happen without a conflict, the request succeeds. Otherwise the server returns either NFS4ERR_DENIED or NFS4ERR_DEADLOCK. NFS4ERR_DEADLOCK comes back when the client sent the LOCK request with type WRITEW_LT and the server has detected a deadlock ๐Ÿ˜ญ. The client should be ready to get these errors too and, where it fits, report them to the requesting application ๐Ÿ’….


8.4. Blocking Locks ๐Ÿ”ฅ

Some clients need blocking locks. NFS version 4 must not rely on a callback mechanism, so it has no way to tell a client when a lock it was previously denied has finally been granted. Clients are stuck continually polling for the lock, and that causes a fairness problem ๐Ÿ™ˆ. To deal with this, two new lock types, READW and WRITEW, were added. They tell the server the client wants a blocking lock. The server should keep an ordered list of pending blocking locks. When the conflicting lock gets released, the server may wait out the lease period for the first waiting client to re-request the lock. Once the lease period expires, the next waiting client's request is allowed to get the lock ๐Ÿฆ‹. Clients are required to poll at an interval small enough that they're likely to get the lock in a timely way. The server isn't required to keep a list of pending blocked locks, since that list only helps fairness and isn't needed for correct operation. Because crash recovery is unordered, you'd need to store lock state in stable storage to guarantee blocking locks get granted in order ๐Ÿงš.

Servers may also note the lock types and hold off on returning the denial of the request. That gives extra time for a conflicting lock to be released, so the request can succeed. This way, clients can skip the burden of needlessly frequent polling for blocking locks ๐ŸŒธ. The server should be careful about how long that delay is in case the client retransmits the request โญ.


8.5. Lease Renewal ๐ŸŒˆ

The whole point of a lease is to let a server clear out stale locks held by a client that crashed or is otherwise unreachable. It isn't a cache consistency mechanism, and lease renewals may not be denied while the lease interval hasn't expired ๐Ÿ’œ.

The following events cause implicit renewal of all the leases for a given client (so, all those sharing a given clientid). Each one is a positive sign that the client is still active and that the state the server holds for that client is still valid โœจ.

  • An OPEN with a valid clientid.
  • Any operation made with a valid stateid (CLOSE, DELEGPURGE, DELEGRETURN, LOCK, LOCKU, OPEN, OPEN_CONFIRM, OPEN_DOWNGRADE, READ, RENEW, SETATTR, WRITE). This does not include the special stateids of all bits 0 or all bits 1.

Note that if the client had restarted or rebooted, it wouldn't be making these requests without first going through the SETCLIENTID/SETCLIENTID_CONFIRM sequence. Using SETCLIENTID/SETCLIENTID_CONFIRM (one that changes the client verifier) tells the server to drop the locking state tied to the client ๐Ÿ™ˆ. SETCLIENTID/SETCLIENTID_CONFIRM never renews a lease.

If the server has rebooted, the stateids (NFS4ERR_STALE_STATEID error) or the clientid (NFS4ERR_STALE_CLIENTID error) won't be valid anymore, which stops spurious renewals ๐Ÿ’….

This approach gives low-overhead lease renewal that scales really well ๐Ÿฆ‹. In the typical case, no extra RPC calls are needed for lease renewal. In the worst case, one RPC is needed every lease period (that's a RENEW operation). The number of locks the client holds doesn't matter, because all of the client's state is part of the lease renewal action ๐Ÿซถ.

Since every operation that creates a new lease also renews existing leases, the server must keep one common lease expiration time for all valid leases for a given client. That lease time can then get updated easily whenever implicit lease renewal happens ๐Ÿ‘‘.


8.6. Crash Recovery ๐Ÿ’€

The big requirement in crash recovery is that both the client and the server know when the other one has failed ๐Ÿ˜ญ. On top of that, a client must see a consistent view of data across server restarts or reboots. Every READ and WRITE operation that might be queued in the client or network buffers has to wait until the client has successfully recovered the locks protecting those READ and WRITE operations ๐ŸŽ€.

8.6.1. Client Failure and Recovery ๐Ÿงš

If a client fails, the server may recover the client's locks once the associated leases have expired. Conflicting locks from another client can only be granted after that lease expiration. If the client manages to restart or reinitialize within the lease period, it may have to wait out the rest of the lease period before it can get new locks ๐ŸŒธ.

To keep client delay low on restart, lock requests are associated with an instance of the client through a client-supplied verifier. That verifier is part of the initial SETCLIENTID call the client makes. The server returns a clientid as the result of the SETCLIENTID operation. Then the client confirms the use of the clientid with SETCLIENTID_CONFIRM. The clientid combined with an opaque owner field is what the client uses to identify the lock owner for OPEN. That chain of associations is then used to identify all the locks for a particular client ๐Ÿ’–.

The verifier gets changed by the client every time it initializes. So the server can compare a new verifier to the verifier tied to the currently held locks and see that they don't match. That means a new instantiation of the client has happened and the old locking state is gone ๐Ÿ’”. As a result, the server is free to release all the held locks associated with the old clientid, which was derived from the old verifier โœจ.

Heads up, bestie: the verifier must have the same uniqueness properties as the verifier used for the COMMIT operation ๐Ÿšจ.

8.6.2. Server Failure and Recovery โœจ

okay bestie, what if the server is the one who blanks out? ๐Ÿ’€ Like it restarts or reboots and loses all its locking state. ๐Ÿ˜ญ Then it has to give clients time to notice and rebuild what got lost. The client needs to be able to re-establish its locking state without the server denying valid requests just because it already gave conflicting access to another client. ๐Ÿซถ And if there's any chance that clients haven't gotten their locking state back for a file yet, the server has to disallow READ and WRITE operations on that file. ๐Ÿšซ This recovery period lasts exactly as long as the lease period. โฐ

A client can tell that server failure happened (and so the locking state is gone) when it gets one of two errors. ๐Ÿ” NFS4ERR_STALE_STATEID means a stateid got wiped out by a reboot or restart. NFS4ERR_STALE_CLIENTID means a clientid got wiped out the same way. ๐Ÿ’… When either one shows up, the client has to set up a brand new clientid (see the section "Client ID") and then rebuild its locking state, which we'll get into below. ๐Ÿงš

The special handling period for locking and for READ and WRITE, which is the same length as the lease period, is called the "grace period". ๐ŸŒธ During the grace period, clients get their locks and related state back using reclaim-type locking requests. That means LOCK requests with reclaim set to true, and OPEN operations with a claim type of CLAIM_PREVIOUS. ๐Ÿ”‘ During the grace period, the server must reject READ and WRITE operations, plus any non-reclaim locking requests (the other LOCK and OPEN operations), with an error of NFS4ERR_GRACE. ๐Ÿ™…โ€โ™€๏ธ

Now, if the server can reliably figure out that handling a non-reclaim request won't clash with other clients reclaiming their locks, it doesn't have to send NFS4ERR_GRACE, and the non-reclaim request can just get served. ๐Ÿฆ‹ For the server to handle READ and WRITE during the grace period, it has to be able to guarantee that no possible conflict could pop up between an upcoming reclaim locking request and that READ or WRITE. ๐Ÿง  If the server can't make that guarantee, it must return NFS4ERR_GRACE to the client. ๐Ÿ˜ฌ

For a server that wants simple, totally valid handling during the grace period, the easiest move is to reject every non-reclaim locking request and every READ and WRITE with NFS4ERR_GRACE. ๐Ÿ’– But a server can also keep info about granted locks in stable storage. ๐Ÿ’พ With that info, the server can figure out whether a regular lock or a READ or WRITE can be safely processed. โœจ

For example, if a count of locks on a given file is sitting in stable storage, the server can track the reclaimed locks for that file. Once all the reclaims have been processed, non-reclaim locking requests may be processed. ๐ŸŽ€ This way the server can make sure non-reclaim locking requests won't conflict with any potential reclaim requests. ๐Ÿ›ก๏ธ As for I/O requests, if the server can tell from stable storage (or some other similar mechanism) that there are no outstanding reclaim requests for a file, then I/O requests for that file can keep going normally. ๐Ÿƒโ€โ™€๏ธ๐Ÿ’จ

Okay, to say it again, because it's super important: a server that lets non-reclaim lock and I/O requests get processed during the grace period MUST determine that no lock reclaimed later will get rejected, and that no lock reclaimed later would have blocked any I/O operation that was processed during the grace period. ๐Ÿšจ๐Ÿ˜ญ

Clients should be ready for NFS4ERR_GRACE errors on non-reclaim lock and I/O requests. ๐Ÿซ  When that happens, the client should use a retry mechanism for the request. Retries should be spaced out by a delay (on the order of several seconds) so the server doesn't get overwhelmed. ๐ŸŒˆ More discussion of the general issue is in [Floyd]. And the client has to account for servers that can handle I/O and non-reclaim locking requests during the grace period, as well as servers that can't. ๐Ÿงฉ

A reclaim-type locking request that shows up outside the server's grace period can only succeed if the server can guarantee that no conflicting lock or I/O request has been granted since the reboot or restart. ๐Ÿ”’ That's a big ask, bestie! ๐Ÿ‘‘

One more thing. A server might pick up a new lease period value when it restarts. ๐Ÿ”„ So once a client has a new clientid, it should refetch the lease_time attribute and use it as the basis for lease renewal for the lease tied to that server. ๐Ÿ“… However, for this restart event, the server must set up a grace period at least as long as the lease period from the previous server instance. ๐Ÿซถ That way, the client state from the previous instance can be reliably re-established. ๐Ÿ’œ

8.6.3. Network Partitions and Recovery ๐ŸŒง๏ธ

Okay bestie, so if a network partition lasts longer than the lease period the server gave the client, the server won't have received a lease renewal from the client. ๐Ÿ˜ญ If that happens, the server may free all locks held for the client, which means every stateid the client was holding becomes invalid or stale. Once the client can reach the server again, any I/O it sends with those now-invalid stateids will fail, and the server returns the error NFS4ERR_EXPIRED. When the client gets that error, it will suitably notify the application that held the lock. ๐Ÿ’”

As a courtesy to the client or as an optimization, the server may keep holding locks for a client whose recent communication has gone on beyond the lease period. ๐Ÿงš If the server receives a lock or I/O request that conflicts with one of these courtesy locks, the server must free the courtesy lock and grant the new request. Main character server energy ๐Ÿ‘‘

When a network partition happens together with a server reboot, there are edge conditions that put requirements on the server so it can avoid silent data corruption after the reboot. ๐Ÿ™ˆ Two of these edge conditions are known, and we'll go through them below.

The first edge condition goes like this:

  1. Client A acquires a lock.
  2. Client A and server experience a mutual network partition, so client A is unable to renew its lease.
  3. Client A's lease expires, so the server releases the lock.
  4. Client B acquires a lock that would have conflicted with that of Client A.
  5. Client B releases the lock.
  6. Server reboots.
  7. The network partition between client A and server heals.
  8. Client A issues a RENEW operation and gets back NFS4ERR_STALE_CLIENTID.
  9. Client A reclaims its lock within the server's grace period.

So at the final step, the server has erroneously granted client A's lock reclaim. ๐Ÿ˜ฑ If client B modified the object the lock was protecting, client A will experience object corruption. ๐Ÿ’€

The second known edge condition goes like this:

  1. Client A acquires a lock.
  2. Server reboots.
  3. Client A and server experience a mutual network partition, so client A is unable to reclaim its lock within the grace period.
  4. The server's reclaim grace period ends. Client A has no locks recorded on the server.
  5. Client B acquires a lock that would have conflicted with that of Client A.
  6. Client B releases the lock.
  7. Server reboots a second time.
  8. The network partition between client A and server heals.
  9. Client A issues a RENEW operation and gets back NFS4ERR_STALE_CLIENTID.
  10. Client A reclaims its lock within the server's grace period.

As with the first edge condition, the final step of the second edge condition's scenario has the server erroneously granting client A's lock reclaim. ๐Ÿซ 

Solving the first and second edge conditions requires that the server either assume, after it reboots, that one of these edge conditions has occurred and therefore return NFS4ERR_NO_GRACE for all reclaim attempts, or that the server record some information in stable storage. ๐Ÿ—‚๏ธ The amount of information the server records in stable storage is in inverse proportion to how harsh the server wants to be whenever the edge conditions occur. The server that is completely tolerant of all edge conditions will record in stable storage every lock that is acquired, and it will remove the lock record from stable storage only when the lock is unlocked by the client and the lock's lockowner advances the sequence number such that the lock release is not the last stateful event for the lockowner's sequence. โœจ

For the two edge conditions above, the harshest a server can be and still support a grace period for reclaims requires that the server record some minimal information in stable storage. For example, a server implementation could save, for each client, a record in stable storage containing:

  • the client's id string
  • a boolean that indicates if the client's lease expired or if there was administrative intervention (see the section, Server Revocation of Locks) to revoke a record lock, share reservation, or delegation
  • a timestamp that is updated the first time after a server boot or reboot that the client acquires record locking, share reservation, or delegation state on the server. The timestamp need not be updated on subsequent lock requests until the server reboots.

The server implementation would also record in stable storage the timestamps from the two most recent server reboots. ๐Ÿ“…

Assuming the above record keeping, for the first edge condition, after the server reboots, the record that client A's lease expired means that another client could have acquired a conflicting record lock, share reservation, or delegation. ๐Ÿšจ Hence the server must reject a reclaim from client A with the error NFS4ERR_NO_GRACE.

For the second edge condition, after the server reboots for a second time, the record that the client had an unexpired record lock, share reservation, or delegation established before the server's previous incarnation means that the server must reject a reclaim from client A with the error NFS4ERR_NO_GRACE. ๐Ÿ™…โ€โ™€๏ธ

Regardless of the level and approach to record keeping, the server MUST implement one of the following strategies (which apply to reclaims of share reservations, record locks, and delegations):

  1. Reject all reclaims with NFS4ERR_NO_GRACE. This is superharsh, but necessary if the server does not want to record lock state in stable storage. ๐Ÿ˜ค
  2. Record sufficient state in stable storage such that all known edge conditions involving server reboot, including the two noted in this section, are detected. False positives are acceptable. Note that at this time, it is not known if there are other edge conditions.

In the event that, after a server reboot, the server determines that there is unrecoverable damage or corruption to the the stable storage, then for all clients and/or locks affected, the server MUST return NFS4ERR_NO_GRACE. ๐Ÿงฏ

A mandate for the client's handling of the NFS4ERR_NO_GRACE error is outside the scope of this specification, since the strategies for such handling are very dependent on the client's operating environment. ๐Ÿ™ƒ However, one potential approach is described below.

When the client receives NFS4ERR_NO_GRACE, it could examine the change attribute of the objects the client is trying to reclaim state for, and use that to determine whether to re-establish the state via normal OPEN or LOCK requests. This is acceptable provided the client's operating environment allows it. In otherwords, the client implementor is advised to document the behavior for their users. ๐Ÿ“ The client could also inform the application that its record lock or share reservations (whether they were delegated or not) have been lost, such as via a UNIX signal, a GUI pop-up window, etc. See the section, "Data Caching and Revocation" for a discussion of what the client should do for dealing with unreclaimed delegations on client state. ๐ŸชŸ

For further discussion of revocation of locks, see the section "Server Revocation of Locks". ๐Ÿ”


8.7. Recovery from a Lock Request Timeout or Abort ๐Ÿ”

In the event a lock request times out, a client may decide not to retry the request. ๐Ÿฅฒ The client may also abort the request when the process for which it was issued is terminated (e.g., in UNIX due to a signal). It is possible, though, that the server received the request and acted upon it. This would change the state on the server without the client being aware of the change. ๐Ÿ‘€ It is paramount that the client re-synchronize state with the server before it attempts any other operation that takes a seqid and/or a stateid with the same lock_owner. This is straightforward to do without a special re-synchronize operation. ๐Ÿซถ

Since the server maintains the last lock request and response received on the lock_owner, for each lock_owner, the client should cache the last lock request it sent that did not receive a response. ๐Ÿง  From this, the next time the client does a lock operation for the lock_owner, it can send the cached request, if there is one. If the request was one that established state (e.g., a LOCK or OPEN operation), the server will return the cached result or, if it never saw the request, perform it. The client can follow up with a request to remove the state (e.g., a LOCKU or CLOSE operation). With this approach, the sequencing and stateid information on the client and server for the given lock_owner will re-synchronize, and in turn the lock state will re-synchronize. ๐Ÿ’–

8.8. Server Revocation of Locks โšก

okay so the server can revoke a client's locks at any point, and the client has to be prepared for that ๐Ÿ˜ณ When the client thinks its locks have been revoked (or might have been), it's on the client to validate the state info between itself and the server. Validating locking state means the client must verify or reclaim state for each lock it's currently holding ๐Ÿซถ

The first kind of revocation happens when the server reboots or gets re-initialized ๐Ÿ”„ The client gets an error (NFS4ERR_STALE_STATEID or NFS4ERR_STALE_CLIENTID) and then goes through normal crash recovery, which is the stuff from the previous section ๐Ÿงš

The second kind is when the lease can't be renewed before it expires ๐Ÿฅบ It's considered rare or unusual, but the client still needs to be ready to recover. Both the server and the client can notice the failed renewal, and both can recover without data corruption โœจ The server tracks the last renewal it handled for the client, so it knows when the lease will expire. The client has to keep track of the operations that renew the lease period too. By using the time each such request was sent and the time its reply came back, the client should bound the time the renewal could have happened on the server, and figure out whether a lease period expiration could have happened ๐Ÿ•ต๏ธ

The third kind can happen because of administrative intervention during the lease period ๐Ÿ‘ฉโ€๐Ÿ’ผ It's considered a rare event, but the server's administrator might have decided to release or revoke a particular lock the client holds. When that happens, the client gets an error of NFS4ERR_ADMIN_REVOKED. In this case the client may assume that only the lock_owner's locks have been lost ๐Ÿ’” The client notifies the lock holder appropriately. The client may not assume the lease period has been renewed as a result of the failed operation ๐Ÿ™…โ€โ™€๏ธ

When the client determines the lease period may have expired, it must mark all locks held for the associated lease as "unvalidated" ๐Ÿšจ This means the client hasn't been able to re-establish or confirm the right lock state with the server. Just like in the crash recovery section, the server may grant conflicting locks after the lease period has expired for a client. So when it's possible the lease period has expired, the client must validate each lock it holds to make sure no conflicting lock has been granted. The client can do this by issuing an I/O request (either a pending I/O or a zero-length read) that specifies the stateid associated with the lock in question. If the response is success, the client has validated all of the locks governed by that stateid and re-established the appropriate state with the server ๐ŸŽ‰

If the I/O request isn't successful, then one or more of the locks associated with the stateid was revoked by the server, and the client must notify the owner ๐Ÿ˜ฑ


8.9. Share Reservations ๐ŸŽ€

A share reservation is a mechanism for controlling access to a file ๐Ÿ’… It's separate and independent from record locking, so don't mix the two up ๐Ÿ™…โ€โ™€๏ธ When a client opens a file, it sends an OPEN operation to the server that specifies the type of access it needs (READ, WRITE, or BOTH) and the type of access it wants to deny others (deny NONE, READ, WRITE, or BOTH). If the OPEN fails, the client fails the application's open request ๐Ÿ’€

Pseudo-code definition of the semantics:

   if (request.access == 0)
      return (NFS4ERR_INVAL)

   else
      if ((request.access & file_state.deny)) ||
            (request.deny & file_state.access))
                    return (NFS4ERR_DENIED)

The share reservation checks on OPEN happen with no exceptions, even for an existing OPEN from the same open_owner ๐Ÿ‘€

The constants used for the access and deny fields in the OPEN and OPEN_DOWNGRADE operations are as follows:

   const OPEN4_SHARE_ACCESS_READ   = 0x00000001;
   const OPEN4_SHARE_ACCESS_WRITE  = 0x00000002;
   const OPEN4_SHARE_ACCESS_BOTH   = 0x00000003;

   const OPEN4_SHARE_DENY_NONE     = 0x00000000;
   const OPEN4_SHARE_DENY_READ     = 0x00000001;
   const OPEN4_SHARE_DENY_WRITE    = 0x00000002;
   const OPEN4_SHARE_DENY_BOTH     = 0x00000003;

8.10. OPEN/CLOSE Operations ๐Ÿšช

To get correct share semantics, a client MUST use the OPEN operation to obtain the initial filehandle and say what access it wants and whatever access it wants to deny, if any. Even if the client plans to use a stateid of all 0's or all 1's, it still has to get the filehandle for the regular file with OPEN so the share semantics can be applied ๐Ÿซ  For clients whose open programming interfaces don't have a deny mode built in, deny equal to NONE should be used.

The OPEN operation with the CREATE flag also takes over the CREATE operation for regular files from previous versions of the NFS protocol. That means a create with a share can be done atomically โš›๏ธ

The CLOSE operation removes all share reservations held by the lock_owner on that file. If record locks are held, the client SHOULD release all locks before issuing a CLOSE. The server MAY free all outstanding locks on CLOSE, but some servers may not support the CLOSE of a file that still has record locks held. The server MUST return failure, NFS4ERR_LOCKS_HELD, if any locks would exist after the CLOSE ๐Ÿ”’

The LOOKUP operation returns a filehandle without establishing any lock state on the server. Without a valid stateid, the server assumes the client has the least access. For example, a file opened with deny READ/WRITE can't be accessed using a filehandle obtained through LOOKUP, because that filehandle wouldn't have a valid stateid (i.e., using a stateid of all bits 0 or all bits 1) ๐Ÿ™ƒ

8.10.1. Close and Retention of State Information ๐Ÿง 

A CLOSE operation asks for a stateid to be deallocated, so dealing with a retransmitted CLOSE can be tricky ๐Ÿ˜ฌ The state info that would normally be used to figure out the state of the open file might already be deallocated, which would give an NFS4ERR_BAD_STATEID error.

Servers can handle this in a few different ways. To give the greatest assurance that the protocol is being used properly, a server should, rather than deallocate the stateid, mark it as close-pending and keep it with this status until it's deallocated later ๐Ÿง That way a retransmitted CLOSE can be recognized, since the stateid points to state info with this distinctive status, and it can be handled without error โœ…

When taking this approach, a server should retain the state information until the earliest of:

  • Another validly sequenced request for the same lockowner that is not a retransmission.
  • The time that a lockowner is freed by the server due to a period with no activity.
  • All locks for the client are freed as a result of a SETCLIENTID.

Servers may skip this complexity, at the cost of less complete protocol error checking, by simply responding NFS4_OK to a CLOSE for a deallocated stateid, on the assumption that this case must be caused by a retransmitted close ๐Ÿ™ˆ When doing this, it's desirable to at least log an error whenever a no-error indication is returned in this situation ๐Ÿ“ If the server maintains a reply-cache mechanism, it can verify that the CLOSE really is a retransmission and avoid error logging in most cases.


8.11. Open Upgrade and Downgrade ๐Ÿ†™

When an OPEN is done for a file, and the lockowner it's being done for already has the file open, the result is to upgrade the open file status maintained on the server. It now includes the access and deny bits from the new OPEN as well as those from the existing OPEN ๐ŸŒˆ As far as the protocol is concerned, there's one open file, and it has the union of the access and deny bits for all of the OPEN requests completed. Only a single CLOSE will be done to reset the effects of both OPENs. Note that the client may not know the same file is actually being opened when it issues the OPEN. The above only applies if both OPENs result in the OPENed object being designated by the same filehandle ๐Ÿค”

When the server chooses to export multiple filehandles corresponding to the same file object, and it returns different filehandles on two different OPENs of that file object, the server MUST NOT "OR" together the access and deny bits and coalesce the two open files ๐Ÿ™…โ€โ™€๏ธ Instead, the server must maintain separate OPENs with separate stateids, and it will require separate CLOSEs to free them.

When multiple open files on the client are merged into a single open file object on the server, closing one of the open files (on the client) may mean the access and deny status of the open file on the server needs to change. That's because the union of the access and deny bits for the remaining opens may be smaller (a proper subset) than it was before. The OPEN_DOWNGRADE operation is used to make the necessary change, and the client should use it to update the server so that share reservation requests from other clients are handled properly ๐Ÿ”ฝ


8.12. Short and Long Leases โณ

When figuring out the time period for the server lease, the usual lease tradeoffs apply. Short leases are good for fast server recovery, but they cost more RENEW or READ (with zero length) requests. Longer leases are kinder and gentler to servers trying to handle very large numbers of clients ๐Ÿฅฐ The number of RENEW requests drops in proportion to the lease time. The downsides of long leases are slower recovery after a server failure (the server must wait for the leases to expire and the grace period to elapse before granting new lock requests) and more file contention (if a client fails to transmit an unlock request, the server must wait for lease expiration before granting new locks) ๐ŸŒ

Long leases are usable if the server is able to store lease state in non-volatile memory. Upon recovery, the server can reconstruct the lease state from its non-volatile memory and keep operating with its clients, so long leases wouldn't be an issue ๐Ÿ’พโœจ

8.13. Clocks, Propagation Delay, and Calculating Lease Expiration โฐ

okay so the server hands out lease times as a time delta, not a timestamp, which means nobody needs perfectly synced clocks ๐Ÿซถ that's honestly iconic. BUT clocks still can't drift too much over the life of a lock, so that's a real requirement, not a vibe ๐Ÿ’€ and then there's propagation delay across the network, which can easily be several hundred milliseconds, plus requests can get lost and need retransmitting. messy but manageable โœจ

so the client should subtract propagation delay from the lease times. like, if the client guesses the one-way delay is 200 msec, it can assume the lease is already 200 msec old when it arrives ๐Ÿงš then it takes another 200 msec for the response to get back to the server, so the client must send a lock renewal or write data back to the server 400 msec before the lease would expire. the timing is giving precision ๐ŸŽ€

the server's lease period config should take into account the network distance of the clients that will be using its resources. the lease period is expected to account for propagation delays and other network delay factors for the client population. the protocol has no automatic way to figure out a good lease period, so the server's admin may have to tune it by hand ๐ŸŒธ

8.14. Migration, Replication and State ๐Ÿฆ‹

so when responsibility for a file system moves to a new server (migration), or the client decides to switch to an alternate server (like when the original stops responding) in the context of file system replication, the way state gets handled is described below. the shared state here is locks, leases, stateids, and clientids, and the handling is different for migration vs. replication. for more on file server state and recovery of it, check the sections under "File Locking and Share Reservations" ๐Ÿ’…

if a replica server or a server taking in a migrated filesystem agrees to, or is expected to, accept opaque values from the client that came from another server, then it's a wise implementation practice for the servers to encode those "opaque" values in network byte order ๐Ÿ”ฅ this way, the replica or immigrating server can parse things like stateids, directory cookies, filehandles, and so on, even if its native byte order is different from the other servers working together on the replication and migration of the filesystem. no cap, this is a lifesaver ๐Ÿ‘‘

8.14.1. Migration and State ๐ŸŒˆ

in the case of migration, the servers involved in moving a filesystem SHOULD transfer all server state from the original server to the new one. this has to happen in a way that's transparent to the client, which eases the client's transition when a migration occurs. if the servers succeed in transferring all state, the client keeps using the stateids the original server assigned, so the new server must recognize those stateids as valid. same goes for the clientid ๐Ÿซถ and since responsibility for an entire filesystem moves together in a migration, there's no chance of conflicts on the new server from transferred locks. iykyk, it's clean โœจ

leases get transferred too. the transferred leases will usually have a different expiration time than the client's leases already on the new server. to keep the property that all leases on a given server for a given client expire at the same time, the server should push the expiration time out to the later of the transferred leases or the leases already present. that way the client can keep renewing both kinds without any extra effort ๐Ÿ’–

the servers may choose not to transfer state during a migration, but this choice is discouraged ๐Ÿ™ˆ in that case, when the client presents state from the original server, the client must be ready to get either NFS4ERR_STALE_CLIENTID or NFS4ERR_STALE_STATEID from the new server. the client should then recover its state the way it normally would after a server failure. and the new server must take care to allow for state recovery just like it would after a server restart ๐Ÿงš

8.14.2. Replication and State ๐Ÿ“

client switch-over in replication isn't under server control, so state gets handled differently here ๐Ÿ’€ leases, stateids, and clientids are NOT valid across a move from one server to another, meaning the client must re-establish its locks on the new server. this is kinda like reclaiming locks after a server reboot, except the difference is the server has no way to tell reclaim requests apart from requests for new locks, or to hold off on the new ones. so a client re-establishing a lock on the new server (with a LOCK or OPEN request) might get denied because of a conflicting lock ๐Ÿ˜ญ but replication is meant for read-only use of filesystems, so those denials shouldn't cause huge problems in practice. and if an attempt to re-establish a lock on a new server gets denied, the client should treat it as if its original lock had been revoked ๐Ÿซ 

8.14.3. Notification of Migrated Lease ๐Ÿ’œ

with lease renewal, the client might not be sending requests for a filesystem that got migrated to another server. this can happen because of the implicit lease renewal mechanism, since the client renews leases for all filesystems whenever it submits a request to any one filesystem at the server ๐Ÿคญ

so the client has to find out about lease relocation before those leases expire, so it can schedule renewal for leases that may have moved to the new server. to make that happen, all the operations that implicitly renew a client's leases (OPEN, CLOSE, READ, WRITE, RENEW, LOCK, LOCKT, LOCKU) will return the error NFS4ERR_LEASE_MOVED if responsibility for any of the leases being renewed has been transferred to a new server. this keeps going until the client gets an NFS4ERR_MOVED error and the server receives the follow-up GETATTR(fs_locations) for an access to each filesystem that had a lease moved to a new server ๐Ÿ’…

โš ๏ธ when a client gets an NFS4ERR_LEASE_MOVED error, it should perform an operation on each filesystem associated with the server in question. once the client gets an NFS4ERR_MOVED error, it can follow the normal process to get the new server info (through the fs_locations attribute) and renew those leases on the new server. and if the server didn't get state transferred to it transparently, the client will see either NFS4ERR_STALE_CLIENTID or NFS4ERR_STALE_STATEID from the new server, like described above, and then it can recover its state the same way it does after a server failure ๐ŸŽ€

8.14.4. Migration and the Lease_time Attribute ๐Ÿฆ‹

so the client can manage its leases properly during a migration, the destination server has to set the right values for the lease_time attribute โœจ

when state is transferred transparently, that state should include the correct value of the lease_time attribute. the lease_time attribute on the destination server must never be less than the one on the source, because that would cause leases granted by the source server to expire early ๐Ÿ˜ฑ and when state is transferred transparently, the client doesn't have to re-fetch lease_time and can keep using the value it got from the source server ๐Ÿ‘‘

if state hasn't been transferred transparently (like when the client sees a real or simulated server reboot), the client should fetch the value of lease_time from the new (destination) server and use it for later locking requests. but the server must respect a grace period at least as long as the lease_time on the source server ๐Ÿซถ that way clients have plenty of time to reclaim their locks before potentially conflicting non-reclaimed locks get granted. how the new server gets the lease_time value from the old server is up to the server implementations. the NFS version 4 protocol doesn't specify it ๐Ÿงš

9. Client-Side Caching โœจ

client-side caching of data, file attributes, and file names is essential for good performance with NFS ๐Ÿ’– distributed cache coherence is a seriously hard problem, which is why earlier NFS versions never really tried it ๐Ÿ˜ญ instead, NFS client implementations used a bunch of techniques to soften the problems a lack of coherence causes for users. the earlier protocol specs never clearly defined those techniques, so it's often unclear what counts as valid or invalid client behavior ๐Ÿ™ˆ

the NFS version 4 protocol uses many techniques similar to the ones from earlier versions. it does NOT provide distributed cache coherence, but it defines a more limited set of caching guarantees so locks and share reservations can be used without destructive interference from client-side caching ๐Ÿ’… it also introduces a delegation mechanism that lets many decisions normally made by the server be made locally by clients ๐Ÿ”ฅ this gives efficient support for the common cases where sharing is infrequent or where sharing is read-only ๐ŸŒธ

9.1. Performance Challenges for Client-Side Caching

Okay bestie, the caching tricks from older NFS versions have been SO successful for performance ๐Ÿ’– But when you get tons of clients, scalability challenges start popping up ๐Ÿ˜ญ It's especially true when clients are spread out geographically, because that classically makes the latency for cache revalidation requests go up ๐ŸŒ

Previous NFS versions repeat their file data cache validation requests every time a file gets opened ๐Ÿ™ˆ That can cause serious performance drawbacks. A super common case is a file that only ever gets accessed by one client, so sharing is rare โœจ

In that situation, checking with the server over and over just to hear "no conflicts, bestie" is expensive ๐Ÿ’€ The better move is to let a client that keeps opening the same file do it without asking the server, until something potentially conflicting actually happens from another client ๐Ÿฆ‹

File locking has the same vibe. Sending lock and unlock requests to the server, plus the read and write requests needed to keep data caching consistent with the locking semantics (see the section "Data Caching and File Locking"), can seriously limit performance ๐Ÿฅฒ When locking is used to guard against infrequent conflicts, the penalty is HUGE, and that penalty may discourage apps from using file locking at all ๐Ÿ™ƒ

NFS version 4 goes for more aggressive caching strategies, with these design goals:

  • Compatibility with a large range of server semantics ๐ŸŽ€
  • Provide the same caching benefits as previous versions of the NFS protocol when unable to provide the more aggressive model ๐ŸŒธ
  • Requirements for aggressive caching are organized so that a large portion of the benefit can be obtained even when not all of the requirements can be met ๐Ÿ‘‘

The server requirements that apply are discussed in the later sections where specific forms of caching are covered (see the section "Open Delegation") ๐Ÿซถ

9.2. Delegation and Callbacks

Okay so, recallable delegation of server responsibilities for a file to a client improves performance by avoiding repeated requests to the server when there's no inter-client conflict โœจ With a "callback" RPC from server to client, the server can recall those delegated responsibilities when another client starts sharing a delegated file. It's giving efficiency ๐Ÿ’…

A delegation gets passed from the server to the client, and it specifies the object of the delegation and the type of delegation. There are different types, but every type carries a stateid to represent the delegation when doing operations that depend on it ๐Ÿงš This stateid is similar to the ones used for locks and share reservations, BUT it's different because the stateid for a delegation is associated with a clientid and can be used on behalf of all the open_owners for that client. A delegation is made to the client as a whole, not to any specific process or thread inside it ๐Ÿ’œ

Callback RPCs may not work in all environments (because of firewalls, for example), so correct protocol operation does not depend on them ๐Ÿ™ˆ Preliminary testing of callback functionality happens through a CB_NULL procedure, which checks whether callbacks can be supported. It checks the continuity of the callback path ๐Ÿ” The server makes a preliminary assessment of callback availability for a given client, and it avoids delegating responsibilities until it has determined that callbacks are supported ๐ŸŒˆ

Because granting a delegation is always conditional on the absence of conflicting access, clients must not assume that a delegation will be granted. They must always be prepared for OPENs to be processed without any delegations being granted ๐Ÿ’€

Once granted, a delegation behaves in most ways like a lock ๐Ÿ” There's an associated lease, which is subject to renewal together with all of the other leases held by that client.

Unlike locks, an operation by a second client on a delegated file will cause the server to recall the delegation through a callback ๐Ÿ“ž

When a recall happens, the client holding the delegation must flush modified state (such as modified data) to the server and return the delegation. The conflicting request won't get a response until the recall is complete ๐Ÿ˜ค The recall counts as complete when the client returns the delegation, or when the server times out on the recall and revokes the delegation because of that timeout. Once the recall is resolved, the server has the information it needs to grant or deny the second client's request โญ

When a client gets a delegation recall, it may have a lot of state that needs flushing to the server. So the server should allow sufficient time for the delegation to be returned, since it may involve numerous RPCs to the server ๐Ÿงƒ If the server can determine that the client is diligently flushing state as a result of the recall, the server may extend the usual time allowed for a recall. Still, the time allowed for recall completion should not be unbounded โณ

Here's an example: say responsibility to mediate opens on a given file has been delegated to a client (see the section "Open Delegation"). The server won't know what opens are in effect on that client. Without that knowledge, the server can't determine whether the access and deny state for the file allows any particular open until the delegation for the file has been returned ๐Ÿ™ƒ

A client failure or a network partition can result in a failure to respond to a recall callback. In that case, the server will revoke the delegation, which in turn renders useless any modified state still on the client ๐Ÿ˜ฑ

9.2.1. Delegation Recovery

There are three situations that delegation recovery has to deal with ๐Ÿงฉ

  • Client reboot or restart
  • Server reboot or restart
  • Network partition (full or callback-only)

If the client reboots or restarts, the failure to renew leases will result in the revocation of record locks and share reservations. Delegations, however, may be treated a bit differently ๐Ÿ‘€

Sometimes delegations need to be reestablished after a client reboots or restarts. The reason is that the client may have file data stored locally, and that data was associated with the previously held delegations ๐Ÿ’พ The client will need to reestablish the appropriate file state on the server.

To allow for this kind of client recovery, the server MAY extend the period for delegation recovery beyond the typical lease expiration period ๐Ÿ•ฐ๏ธ That means requests from other clients that conflict with these delegations will need to wait. Because the normal recall process may take significant time for the client to flush changed state to the server, other clients need to be prepared for delays caused by a conflicting delegation ๐Ÿ˜… This longer interval increases the window for clients to reboot and consult stable storage so the delegations can be reclaimed. For open delegations, the delegations are reclaimed using OPEN with a claim type of CLAIM_DELEGATE_PREV. (See the sections "Data Caching and Revocation" and "Operation 18: OPEN" for discussion of open delegation and the details of OPEN, respectively.) ๐Ÿ“š

A server MAY support a claim type of CLAIM_DELEGATE_PREV. But if it does, it MUST NOT remove delegations upon SETCLIENTID_CONFIRM. Instead, it MUST, for a period of time no less than the value of the lease_time attribute, maintain the client's delegations so the client has time to issue CLAIM_DELEGATE_PREV requests ๐ŸŽŸ๏ธ A server that supports CLAIM_DELEGATE_PREV MUST support the DELEGPURGE operation.

When the server reboots or restarts, delegations are reclaimed (using the OPEN operation with CLAIM_PREVIOUS) in a similar fashion to record locks and share reservations. But there's a slight semantic difference! โšก In the normal case, if the server decides a delegation should not be granted, it performs the requested action (e.g., OPEN) without granting any delegation. For a reclaim, the server grants the delegation, but a special designation is applied so the client treats the delegation as having been granted but recalled by the server. Because of this, the client has the duty to write all modified state to the server and then return the delegation ๐Ÿซ  This handling of delegation reclaim reconciles three principles of the NFS version 4 protocol:

  • Upon reclaim, a client reporting resources assigned to it by an earlier server instance must be granted those resources ๐ŸŽ
  • The server has unquestionable authority to determine whether delegations are to be granted and, once granted, whether they are to be continued ๐Ÿ‘‘
  • The use of callbacks is not to be depended upon until the client has proven its ability to receive them ๐Ÿ“ž

When a network partition occurs, delegations are subject to being freed by the server when the lease renewal period expires. This is similar to the behavior for locks and share reservations. For delegations, though, the server may extend the period in which conflicting requests are held off ๐Ÿ•Š๏ธ Eventually, a conflicting request from another client will cause revocation of the delegation. A loss of the callback path (e.g., because of a later network configuration change) will have the same effect. A recall request will fail, and revocation of the delegation will result ๐Ÿ’”

A client normally finds out about revocation of a delegation when it uses a stateid associated with a delegation and receives the error NFS4ERR_EXPIRED ๐Ÿ˜ญ It may also find out about a delegation revocation after a client reboot, when it attempts to reclaim a delegation and gets that same error.

Heads up: in the case of a revoked write open delegation, there are issues, because data may have been modified by the client whose delegation was revoked and separately by other clients ๐Ÿšจ See the section "Revocation Recovery for Write Open Delegation" for a discussion of those issues. Also note that when delegations are revoked, information about the revoked delegation will be written by the server to stable storage (as described in the section "Crash Recovery"). This is done to deal with the case where a server reboots after revoking a delegation but before the client holding the revoked delegation is notified about the revocation ๐Ÿง 

9.3. Data Caching

When applications share access to a set of files, they need to be implemented so they account for the possibility of conflicting access by another application ๐Ÿค That's true whether the applications run on different clients or live on the same client.

Share reservations and record locks are the facilities the NFS version 4 protocol provides so applications can coordinate access through mutual exclusion ๐Ÿ”’ The NFS version 4 protocol's data caching must be implemented so that it does not invalidate the assumptions that those using these facilities depend upon ๐Ÿ’–

9.3.1. Data Caching and OPENs โœจ

okay so the big rule here is that NFS version 4 clients should not provide cached data to apps, or change it for an app, when that wouldn't be valid if they grabbed or changed the same data with a READ or WRITE op ๐Ÿ’…. Apps count on certain sharing behavior, and we do NOT want to break that vibe ๐Ÿซถ

Now, if there's no open delegation (check out the "Open Delegation" section ๐Ÿฆ‹), two extra rules kick in. Honestly, lots of NFS version 2 and version 3 clients already follow these in practice, so they're not even new ๐ŸŒธ

  • Rule one: revalidate after an OPEN. Cached data on a client must be revalidated after an OPEN. Revalidating means the client grabs the change attribute from the server, compares it to the cached change attribute, and if they don't match, it marks the cached data (and cached attributes) as invalid ๐Ÿ’€ This makes sure the data for the OPENed file is still accurate in the cache. This check must happen at least when the client's OPEN includes DENY=WRITE or BOTH, because that ends the window where other clients might have opened the file with WRITE access ๐Ÿ‘‘ Clients can revalidate more often, like at OPENs with DENY=NONE, to match how NFS version 3 does it, for users who expect that level of cache freshness โœจ

Here's the tea ๐Ÿ“ Some implementors might be tempted to use time_modify instead of change to validate cached data, so metadata changes don't wrongly kill perfectly good clean data. Please don't do that ๐Ÿ™ˆ The change attribute is guaranteed to change on every update to the file, but time_modify is only guaranteed to change at the granularity of the time_delta attribute. Using time_modify instead of change for cache validation can make a client mark stale data as valid, and that's a whole mess ๐Ÿ˜ญ

  • Rule two: flush modified data before closing a file OPENed for write. This goes hand in hand with rule one ๐Ÿซถ If the data isn't flushed at CLOSE, the revalidation after an OPEN can't do its job. The data also has to be committed to stable storage at the server before the client sends the CLOSE op. Why? If the server reboots or restarts after a file is CLOSEd, it may be impossible to resend the data, so this rule is a must ๐Ÿงš

9.3.2. Data Caching and File Locking ๐ŸŽ€

Some apps would rather use file locking than share reservations to keep file access from getting messy ๐Ÿ”’ There's a matching set of caching constraints for that too. These rules only work if the locking is used in a way that exactly matches the actual READ and WRITE ops, as opposed to locking that just runs on convention ๐Ÿฆ‹

Here's an example. Say you have a two-megabyte file and you split it into two one-megabyte regions. You protect the regions with locks on byte zero and byte one. A write lock on byte zero means you get to READ and WRITE the first region, and a write lock on byte one means you get to READ and WRITE the second region โœจ If every app sticks to this convention, it works fine on a local filesystem. But it may NOT work with NFS version 4 unless clients stop caching data ๐Ÿ’€

The data caching rules for file locking are:

  • First, revalidate when you lock. When a client gets a file lock for a region, any cached data for that region must be revalidated. If the change attribute says the file might have been updated since the cached data was fetched, the client must flush or invalidate the cached data for the newly locked region. A client can choose to invalidate all of the non-modified cached data for the file, but the only requirement for correct operation is invalidating all of the data in the newly locked region ๐Ÿ’–
  • Second, flush before you unlock a write lock. Before a client releases a write lock for a region, all modified data for that region must be flushed to the server, and it must also be written to stable storage ๐ŸŒˆ

Heads up, bestie ๐Ÿšจ Flushing data and invalidating cached data have to match the actual byte ranges locked or unlocked. If you round those up or down to fit cache block boundaries, things can break if you're not super careful ๐Ÿ˜ญ For example, writing a modified block when only half of it is inside the area being unlocked could change the region outside the unlocked area. That region could be part of a region locked by another client, which is a disaster ๐Ÿ’€ Clients can avoid this by synchronously doing the parts of a write that overlap the initial or final portion that isn't a full block ๐Ÿซถ Similarly, invalidating a locked area that isn't a whole number of full buffer blocks could force the client to read one or two partial blocks from the server if the revalidation shows the data might be invalid ๐Ÿ“

The data written to the server before unlocking a region must be written to stable storage at the server. The client can do this with synchronous writes, or with asynchronous writes followed by a COMMIT operation. This is required, because if modified data gets retransmitted after a server reboot, it might conflict with a lock held by another client ๐Ÿ”ฅ

Now, an implementation can accommodate apps that use record locking in non-standard ways (like using a record lock as a global semaphore, which is iconic behavior tbh ๐Ÿ‘‘). It can do this by flushing more data to the server upon a LOCKU than the locked range covers. That extra data might include modified data in other files too โœจ In those cases, the client must not interfere with apps whose READs and WRITEs stay within the bounds of record locks the app holds ๐ŸŽ€

Here's an example. An app locks a single byte and writes that byte. A client that handles a LOCKU by flushing all modified data to the server could validly write that single byte in response to some unrelated unlock. But it would NOT be valid to write the entire block that byte lives in, because that block includes an area that isn't locked and might be locked by another client ๐Ÿ˜ญ Client implementations can avoid this by sorting files with modified data into two groups: files where every modification is in an area covered by an appropriate record lock, and files with modifications that aren't covered by a record lock ๐Ÿ’… Any writes for the first group must not include areas that aren't locked, and therefore aren't modified on the client ๐Ÿงš

9.3.3. Data Caching and Mandatory File Locking ๐Ÿ”’

Client side caching needs to respect mandatory file locking when it's in effect ๐Ÿ’œ A client can tell mandatory locking is in effect for a file when it gets NFS4ERR_LOCKED back from a READ or WRITE on a file it has an appropriate share reservation for ๐Ÿฆ‹

When mandatory locking is in effect for a file, the client must check for an appropriate file lock on the data being read or written. If a lock exists for the range being read or written, the client may satisfy the request using its validated cache โœจ If the client doesn't hold an appropriate file lock for that range, the read or write must not be satisfied from the cache, and the request has to go to the server for processing ๐Ÿ’€ When a read or write partially overlaps a locked region, the request should be split into multiple pieces, with each region (locked or not) handled appropriately ๐ŸŒธ

9.3.4. Data Caching and File Identity ๐Ÿงš

When clients cache data, the file data needs to be organized by the filesystem object it belongs to ๐Ÿ“ For NFS version 3 clients, the usual practice has been to assume, for caching purposes, that distinct filehandles represent distinct filesystem objects. The client can then choose to organize and maintain its data cache on that basis ๐Ÿ’–

In the NFS version 4 protocol, there's now a real possibility of big deviations from the "one filehandle per object" model, because a filehandle may be built from the object's pathname ๐ŸŒˆ So clients need a reliable way to figure out whether two filehandles designate the same filesystem object ๐Ÿซถ If clients just assumed that all distinct filehandles denote distinct objects and cached on that basis, caching inconsistencies could pop up between distinct client-side objects that map to the same server-side object ๐Ÿ˜ญ

The NFS version 4 protocol fixes a potential functional regression compared to NFS version 3 by providing a way to tell filehandles apart ๐Ÿ‘‘ Without that method, caching inconsistencies within the same client could happen, and that never showed up in earlier NFS versions ๐Ÿ’… It's possible to get such inconsistencies with apps running on multiple clients, but that's not what's being addressed here ๐ŸŽ€

So, for data caching, here are the steps an NFS version 4 client can use to figure out whether two distinct filehandles denote the same server side object โœจ

  • If GETATTR directed to two filehandles returns different values of the fsid attribute, the filehandles represent distinct objects.
  • If GETATTR for any file with an fsid matching the fsid of the two filehandles in question returns a unique_handles attribute with a value of TRUE, then the two objects are distinct.
  • If GETATTR directed to the two filehandles doesn't return the fileid attribute for both handles, then it can't be determined whether the two objects are the same. That means ops that depend on knowing this (like client side data caching) can't be done reliably ๐Ÿ™ˆ
  • If GETATTR directed to the two filehandles returns different values for the fileid attribute, they are distinct objects.
  • Otherwise, they are the same object ๐Ÿ’–

9.4. Open Delegation ๐Ÿฆ‹

okay so when a file is being OPENed, the server may hand the opening client the keys to handle further opens and closes for that file itself ๐Ÿ”‘. This delegation is recallable, bestie, because the situation that made it possible can change. Like, the server might get a conflicting OPEN from another client, and then the server must recall the delegation before deciding whether that other client's OPEN may be granted ๐Ÿ˜ญ. Making a delegation is totally up to the server, so clients should not assume that any particular OPEN either will or will not get an open delegation ๐Ÿคทโ€โ™€๏ธ. Here's a typical set of conditions servers might check when deciding whether an OPEN should be delegated ๐Ÿ“‹:

  • The client must be able to respond to the server's callback requests. The server will use the CB_NULL procedure for a test of callback ability ๐Ÿ“ž.
  • The client must have responded properly to previous recalls.
  • There must be no current open conflicting with the requested delegation.
  • There should be no current delegation that conflicts with the delegation being requested.
  • The probability of future conflicting open requests should be low based on the recent history of the file.
  • The existence of any server-specific semantics of OPEN/CLOSE that would make the required handling incompatible with the prescribed handling that the delegated client would apply (see below).

There are two types of open delegations, read and write ๐Ÿ’–. A read open delegation lets a client handle, all on its own, requests to open a file for reading that don't deny read access to anyone else. Multiple read open delegations can be outstanding at the same time and they don't conflict, so it's giving teamwork ๐Ÿซถ. A write open delegation lets the client handle all opens on its own. Only one write open delegation can exist for a given file at a time, and it is inconsistent with any read open delegations ๐Ÿ‘‘.

When a client has a read open delegation, it may not make any changes to the contents or attributes of the file, but it is assured that no other client may do so ๐Ÿงš. When a client has a write open delegation, it may modify the file data, since no other client will be accessing the file's data ๐Ÿ’…. The client holding a write delegation may only affect file attributes that are intimately connected with the file data: size, time_modify, change ๐ŸŽ€.

When a client has an open delegation, it does not send OPENs or CLOSEs to the server. It just updates the appropriate status internally โœจ. For a read open delegation, opens that can't be handled locally (opens for write or opens that deny read access) must be sent to the server ๐Ÿšซ.

When an open delegation is made, the response to the OPEN contains an open delegation structure that specifies the following ๐Ÿ“ฆ:

  • the type of delegation (read or write)
  • space limitation information to control flushing of data on close (write open delegation only, see the section "Open Delegation and Data Caching")
  • an nfsace4 specifying read and write permissions
  • a stateid to represent the delegation for READ and WRITE

The delegation stateid is separate and distinct from the stateid for the OPEN proper ๐Ÿซง. The standard stateid, unlike the delegation stateid, is associated with a particular lock_owner, and it will keep being valid after the delegation is recalled and the file remains open ๐ŸŒธ.

When a request internal to the client is made to open a file and open delegation is in effect, it will be accepted or rejected solely on the basis of the following conditions. Any requirement for other checks to be made by the delegate should result in open delegation being denied, so that the checks can be made by the server itself ๐Ÿ™ˆ.

  • The access and deny bits for the request and the file, as described in the section "Share Reservations".
  • The read and write permissions, as determined below.

The nfsace4 passed with the delegation can be used to avoid frequent ACCESS calls ๐Ÿ“. The permission check should work like this:

  • If the nfsace4 indicates that the open may be done, then it should be granted without reference to the server ๐Ÿ™Œ.
  • If the nfsace4 indicates that the open may not be done, then an ACCESS request must be sent to the server to get the definitive answer ๐Ÿ“จ.

The server may return an nfsace4 that is more restrictive than the actual ACL of the file. This includes an nfsace4 that specifies denial of all access ๐Ÿ”’. Note that some common practices, such as mapping the traditional user "root" to the user "nobody", may make it incorrect to return the actual ACL of the file in the delegation response ๐Ÿค“.

The use of delegation together with various other forms of caching creates the possibility that no server authentication will ever be performed for a given user, since all of the user's requests might be satisfied locally ๐Ÿซ . Where the client is depending on the server for authentication, the client should make sure authentication happens for each user by using the ACCESS operation. This should be the case even if an ACCESS operation wouldn't otherwise be required ๐Ÿ›ก๏ธ. As mentioned before, the server may enforce frequent authentication by returning an nfsace4 denying all access with every open delegation ๐Ÿ’œ.

9.4.1. Open Delegation and Data Caching โœจ

OPEN delegation lets a ton of the message overhead that comes with opening and closing files just disappear ๐Ÿงน. An open made while an open delegation is in effect does not require a validation message to be sent to the server, so it's giving efficiency ๐Ÿš€. The continued endurance of the "read open delegation" guarantees that no OPEN for write, and therefore no write, has occurred ๐Ÿ’–. Similarly, when closing a file that was opened for write while a write open delegation is in effect, the data written doesn't have to be flushed to the server until the open delegation is recalled ๐Ÿช„. The continued endurance of the open delegation guarantees that no open, and therefore no read or write, has been done by another client ๐Ÿซถ.

For the purposes of open delegation, READs and WRITEs done without an OPEN are treated as the functional equivalents of a corresponding type of OPEN ๐Ÿง . This refers to READs and WRITEs that use the special stateids consisting of all zero bits or all one bits 0๏ธโƒฃ1๏ธโƒฃ. So READs or WRITEs with a special stateid done by another client will force the server to recall a write open delegation ๐Ÿ“ฃ. A WRITE with a special stateid done by another client will force a recall of read open delegations ๐Ÿ’….

With delegations, a client can avoid writing data to the server when the CLOSE of a file is serviced ๐ŸŽ‰. The file close system call is the usual point where the client gets told about a lack of stable storage for the modified file data the application generated ๐Ÿ“ข. At close, file data is written to the server, and through normal accounting the server can tell whether the available filesystem space for the data has been exceeded (i.e., the server returns NFS4ERR_NOSPC or NFS4ERR_DQUOT). This accounting includes quotas ๐Ÿ“Š. Introducing delegations means an alternative method has to be in place for that same kind of communication to happen between client and server ๐Ÿ”.

In the delegation response, the server provides either the limit of the size of the file or the number of modified blocks and the associated block size ๐Ÿ“. The server must ensure that the client will be able to flush data to the server in an amount equal to what was provided in the original delegation. The server must make this assurance for all outstanding delegations ๐Ÿค. Therefore, the server must be careful in how it manages available space for new or modified data, taking into account available filesystem space and any applicable quotas ๐Ÿงฎ. The server can recall delegations as a result of managing the available filesystem space ๐Ÿ“ค. The client should abide by the server's state space limits for delegations ๐Ÿซก.

If the client exceeds the stated limits for the delegation, the server's behavior is undefined. Yikes, bestie, don't go over the limit ๐Ÿ’€.

Based on server conditions, quotas, or available filesystem space, the server may grant write open delegations with very restrictive space limitations ๐Ÿฅบ. The limitations may be defined in a way that will always force modified data to be flushed to the server on close ๐Ÿšฟ.

With respect to authentication, flushing modified data to the server after a CLOSE has occurred may be problematic ๐Ÿค”. For example, the user of the application may have logged off the client, and unexpired authentication credentials may not be present ๐Ÿ™ƒ. In this case, the client may need to take special care to make sure local unexpired credentials will actually be available. This can be done by tracking the expiration time of credentials and flushing data well before they expire, or by making private copies of credentials so they're available when needed ๐Ÿ”.

9.4.2. Open Delegation and File Locks ๐Ÿ”’

When a client holds a write open delegation, lock operations may be performed locally ๐Ÿ’ช. This includes the ones required for mandatory file locking ๐Ÿงฑ. That works because the delegation implies there can be no conflicting locks ๐ŸŒˆ. Similarly, all of the revalidations that would normally go along with obtaining locks, and the flushing of data that goes along with releasing locks, don't need to be done โœจ.

When a client holds a read open delegation, lock operations are not performed locally ๐Ÿ™…โ€โ™€๏ธ. All lock operations, including the ones requesting non-exclusive locks, are sent to the server for resolution ๐Ÿ“ฌ.

9.4.3. Handling of CB_GETATTR โœจ

okay bestie, the server needs to handle a GETATTR differently when the target file has a write open delegation in effect ๐ŸŽ€ why? because the client holding that write delegation may have changed the data, and the server has to show those changes to the second client that sent the GETATTR ๐Ÿ˜ญ so the server has to go ask the delegation holder what's up with the file using CB_GETATTR. the only attributes the server can reliably ask about with CB_GETATTR are size and change ๐Ÿ’…

the server doesn't need the whole story here. it only needs to know one thing: does the delegation holder have a modified version of the file? ๐Ÿง if the client's copy isn't modified (data or size), the server can answer the second client's GETATTR from the attributes it already has stored locally ๐Ÿฆ‹ if the file is modified, the server only needs to know that it's modified. then, if the server figures out the file is currently modified, it answers the second client's GETATTR as if the file had been changed locally at the server ๐Ÿซถ

the server and client have to agree on how to talk about the modified state, since the server picks the form of the change attribute and the client isn't supposed to peek inside it ๐Ÿ™ˆ the size part is easy: for the size attribute, the client just reports its current view of the file size ๐ŸŒธ

the change attribute is where it gets a little more involved, not gonna lie ๐Ÿ˜ญ

here's what the client does when it gets a write delegation:

  • the value of the change attribute is obtained from the server and cached. let's call this value c. ๐Ÿ’–
  • the client creates a value greater than c that is used to communicate that modified data is held at the client. let's call this value d. โœจ
  • when the client gets queried via CB_GETATTR for the change attribute, it checks whether it holds modified data. if the file is modified, the client returns d as the change attribute value. if the file isn't currently modified, the client returns c. ๐Ÿ“

for simplicity of implementation, the client MAY return the same value d for each CB_GETATTR ๐Ÿ’… even if the client modifies the file's data or metadata in its cache again between successive CB_GETATTR operations. this works because the only requirement is that the client can tell the server it holds modified data ๐ŸŽ€ so the value of d may always be c + 1. iykyk ๐Ÿงš

now the change attribute is opaque to the client in the sense that it has no idea what units of time, if any, the server is counting change with ๐ŸŒˆ but it is not opaque in that the client has to treat it as an unsigned integer, and the server has to be able to see the results of the client's changes to that integer ๐Ÿ‘‘ so the server MUST encode the change attribute in network order when sending it to the client. the client MUST decode it from network order to its native order when receiving it, and the client MUST encode it in network order when sending it to the server. this is exactly why change is defined as an unsigned integer and not an opaque array of octets ๐Ÿ”ฅ

on the server side, here's what happens when it provides a write delegation:

  • upon providing a write delegation, the server caches a copy of the change attribute in the data structure it uses to record the delegation. let's call this value sc. ๐Ÿ’œ
  • when a second client sends a GETATTR operation on the same file to the server, the server gets the change attribute from the first client. let's call this value cc. ๐Ÿ‘€
  • if cc is equal to sc, the file isn't modified, and the server returns the current values for change, time_metadata, and time_modify (for example) to the second client. ๐ŸŒธ
  • if cc is not equal to sc, the file is currently modified at the first client and will most likely be modified at the server at some future time. the server uses its current time to construct attribute values for time_metadata and time_modify. then it computes a new value of sc, which we'll call nsc, such that nsc >= sc + 1. the server returns the constructed time_metadata, time_modify, and nsc values to the requester, and replaces sc in the delegation record with nsc. โœจ
  • to keep time_modify, time_metadata, and change from appearing to go backward (which would happen if the client holding the delegation fails to write its modified data to the server before the delegation is revoked or returned) ๐Ÿ˜ญ, the server SHOULD update the file's metadata record with the constructed attribute values. for reasons of reasonable performance, committing the constructed attribute values to stable storage is OPTIONAL. ๐Ÿฅน

as we talked about earlier in this section, the client MAY return the same cc value on later CB_GETATTR calls, even if the file got modified in the client's cache again between successive CB_GETATTR calls ๐Ÿ™ˆ so the server has to assume the file has been modified yet again, and it MUST take care to make sure the new nsc it builds and returns is greater than the previous nsc it returned ๐Ÿซถ an example implementation can handle this by keeping a boolean field (let's call it "modified") that's set to false when the delegation is granted, plus an sc value set to the change attribute value at grant time ๐Ÿง the modified field gets set to true the first time cc != sc, and it stays true until the delegation is returned or revoked. the processing for building nsc, time_modify, and time_metadata looks like this:

if (!modified) {
    do CB_GETATTR for change and size;

       if (cc != sc)
           modified = TRUE;
   } else {
           do CB_GETATTR for size;
   }

   if (modified) {
       sc = sc + 1;
        time_modify = time_metadata = current_time;
        update sc, time_modify, time_metadata into file's metadata;
    }

    return to client (that sent GETATTR) the attributes
       it requested, but make sure size comes from what
       CB_GETATTR returned.  Do not update the file's metadata
       with the client's modified size.
  • in the case that the file attribute size is different from the server's current value, the server treats this as a modification regardless of the change attribute value retrieved via CB_GETATTR ๐Ÿ“ and then responds to the second client the same way as in the last step. ๐Ÿ’€

this whole methodology sorts out clock differences between client and server, plus other situations where using CB_GETATTR would otherwise break down ๐Ÿงš

one more thing ๐Ÿ‘€ the server isn't obligated to use CB_GETATTR at all, so the server MAY simply recall the delegation to avoid using it. ๐Ÿ’…

9.4.4. Recall of Open Delegation ๐Ÿ“ž

these events make it necessary to recall an open delegation:

  • a potentially conflicting OPEN request (or a READ/WRITE done with a "special" stateid) ๐Ÿ”ฅ
  • a SETATTR issued by another client ๐Ÿ™ˆ
  • a REMOVE request for the file ๐Ÿ’€
  • a RENAME request for the file as either the source or the target of the RENAME ๐Ÿ˜ญ

whether a RENAME of a directory in the path leading to the file results in recall of an open delegation depends on the semantics of the server's filesystem ๐ŸŒˆ if that filesystem denies such RENAMEs when a file is open, the recall must be performed to figure out whether the file is actually open. ๐Ÿง

on top of the situations above, the server may choose to recall open delegations at any time if resource constraints make it a good idea ๐ŸŽ€ clients should always be prepared for the possibility of recall. no surprises, bestie ๐Ÿ’–

when a client gets a recall for an open delegation, it needs to update state on the server before returning the delegation โœจ the same updates must be done whenever a client chooses to return a delegation voluntarily. these are the state items that need to be dealt with:

  • if the file associated with the delegation is no longer open and no previous CLOSE operation has been sent to the server, a CLOSE operation must be sent to the server. ๐Ÿ’…
  • if a file has other open references at the client, then OPEN operations must be sent to the server. the server provides the appropriate stateids for the client to use afterward, since the delegation stateid won't be valid anymore ๐Ÿซถ these OPEN requests use the claim type CLAIM_DELEGATE_CUR. this lets the client present the delegation stateid so it can establish the right to perform the OPEN. (see the section "Operation 18: OPEN" for details.) ๐ŸŽ€
  • if there are granted file locks, the corresponding LOCK operations need to be performed. this only applies to the write open delegation case. ๐Ÿงš
  • for a write open delegation, if the file isn't open for write at the time of recall, all modified data for the file must be flushed to the server. if the delegation hadn't existed, the client would have done this data flush before the CLOSE operation. ๐Ÿ“
  • for a write open delegation when the file is still open at the time of recall, any modified data for the file needs to be flushed to the server. ๐ŸŒธ
  • with a write open delegation in place, the file could have been truncated while the delegation was around ๐Ÿ˜ญ for example, the truncation could have happened because of an OPEN UNCHECKED with a size attribute value of zero. so if a truncation has occurred and hasn't been propagated to the server, the truncation must happen before any modified data is written to the server. ๐Ÿ’œ

file locking adds a few extra requirements for the write open delegation case ๐Ÿ”’ to precisely maintain the associated invariant, it's required to flush any modified data in any region where a write lock was released while the write delegation was in effect. but since a write open delegation means no other client is doing any locking, a simpler implementation is to flush all modified data for the file (as described just above) if any write lock was released while the write open delegation was in effect. โœจ

an implementation doesn't have to wait until the delegation is recalled (or until the client decides to return it voluntarily) to do any of the actions above, if things like resource availability make that more desirable ๐ŸŒˆ generally though, the actual open state of the file may keep changing, so it isn't really worth sending info about opens and closes to the server except as part of delegation return ๐Ÿง the only time clients are likely to do this early is when closing the open that got them the delegation, since that close won't be undone once it's done ๐Ÿ’… no matter how the client schedules these things, they all must be performed before the delegation is returned, including (when it applies) the close that matches the open that resulted in the delegation ๐Ÿ‘‘ and these actions can happen either in previous requests or in previous operations in the same COMPOUND request. ๐Ÿฆ‹

9.4.5. Clients that Fail to Honor Delegation Recalls ๐Ÿซถ

okay so a client can totally ignore a recall for all kinds of reasons, like the callback path from the server to the client just breaks ๐Ÿ’€. and the client might not even know its callback path is busted. so it finds out way later that its delegation got revoked, and meanwhile some other client changed the data it was holding a delegation for ๐Ÿ˜ญ. this is especially rough for the client that had a write delegation.

the server has its own mess to deal with too ๐Ÿ™ˆ. the client that didn't answer the recall could still be sending other NFS requests, including ones that renew the lease before it expires. if the server doesn't send back an error for those lease renewing operations, it basically tricks the client into thinking its delegation is still good ๐ŸŽ€.

so here's how the rules fix that ๐Ÿ’–:

  • When the callback path is down, the server MUST NOT revoke the delegation if one of the following occurs:
    • The client has issued a RENEW operation and the server has returned an NFS4ERR_CB_PATH_DOWN error. The server MUST renew the lease for any record locks and share reservations the client has that the server has known about (as opposed to those locks and share reservations the client has established but not yet sent to the server, due to the delegation). The server SHOULD give the client a reasonable time to return its delegations to the server before revoking the client's delegations. โœจ
    • The client has not issued a RENEW operation for some period of time after the server attempted to recall the delegation. This period of time MUST NOT be less than the value of the lease_time attribute. ๐Ÿฆ‹
  • When the client holds a delegation, it can not rely on operations, except for RENEW, that take a stateid, to renew delegation leases across callback path failures. the client that wants to keep delegations in force across callback path failures must use RENEW to do so ๐Ÿ‘‘.

9.4.6. Delegation Revocation ๐ŸŒธ

at the moment a delegation gets revoked, if there are opens on the client tied to it, the applications holding those opens need to hear about it ๐Ÿ“ฃ. this usually happens by returning errors for READ/WRITE operations, or when a close is attempted on the open file.

if no opens exist for the file when the delegation is revoked, then telling anyone about the revocation isn't necessary ๐Ÿ™ˆ. BUT if there's modified data sitting at the client for that file, the user of the application should be notified. the tricky part is that it might not be possible to notify the user, since active applications may not be present at the client ๐Ÿ’€. see the section "Revocation Recovery for Write Open Delegation" for the deets ๐Ÿ”.


9.5. Data Caching and Revocation ๐Ÿงš

okay this part is giving "the assumptions you were relying on are gone" energy ๐Ÿ˜ญ. when locks and delegations get revoked, the stuff that made caching safe is no longer guaranteed. any locks or share reservations that got revoked, the owner has to be notified. that includes applications that have a file open with a delegation that got revoked ๐Ÿ“ข. cached data tied to the revocation has to be removed from the client too. and if there's modified data in the client's cache, that data has to be removed WITHOUT being written to the server ๐Ÿšซ.

here's why that matters so much ngl ๐Ÿฅบ. another client might have been granted a conflicting lock after the first client's lock was revoked. so the data inside that lock range may have been changed by the other client. the first client just can't promise the application what happened to the file after a revocation ๐Ÿ’….

notifying a lock owner usually just means returning an error on the next READ/WRITE (and all the ones after it) to the open file, or on the close ๐ŸŽ€. but sometimes the client's methods can't deliver an error for certain operations, so something more dramatic like signals or process termination might be appropriate ๐Ÿ’ฅ. the reason is that an invariant the application depends on might have been broken. depending on how the client's environment usually handles errors, even more notification like logging, console messages, and GUI pop-ups might make sense ๐ŸŒˆ.

9.5.1. Revocation Recovery for Write Open Delegation ๐Ÿ“

revoking a write open delegation is a special case because there can be modified data in the client cache while the file isn't open ๐Ÿ˜ฌ. so any client that doesn't flush modified data to the server on each close has to make sure the user gets proper notification that the revocation caused a failure. since some of these problems need a human to fix them, notification schemes that alert the right user or administrator might be necessary. logs and console messages are classic examples ๐Ÿ“.

if there's modified data on the client, it must not be flushed normally to the server ๐Ÿ™…โ€โ™€๏ธ. to help with recovery, a client may try to save a copy of the file data as it was modified during the delegation, under a different name in the filesystem namespace ๐Ÿ’พ. this is especially useful when the client can tell that no other client has modified the file, or when the client has a complete cached copy of the file. a saved copy of the client's view can be really valuable in those cases ๐Ÿ’œ. but if it's a mix of the client's cached data and the server's copy as changed by other clients, recovering from that is anything but straightforward ๐Ÿ˜ต. so clients may skip saving file contents in those situations, or mark the results specially to warn users about possible problems โš ๏ธ.

saving that modified data during a delegation revocation might be limited to files of a certain size, or only happen when there's enough disk space in the target filesystem ๐Ÿงบ. it could also be restricted to times when the client has enough buffering resources to keep the cached copy around until it's properly stored to the target filesystem โœจ.

9.6. Attribute Caching ๐Ÿ’…

Okay bestie, quick heads up: named attributes don't count in this part ๐Ÿ™ˆ Each one is basically its own little file, so caching their data works exactly like caching ordinary file data. LOOKUP results from an OPENATTR directory get cached on the same basis as any other pathnames, and directory contents get the same treatment โœจ

Clients are allowed to cache file attributes they got from the server and use them to skip extra GETATTR requests ๐Ÿฆ‹ This is write through, which means changing attributes always goes through requests to the server, and they should not be changed locally and then cached. The exception is attributes that are tied up with data caching ๐Ÿงš So if you extend a file by writing data into the local data cache, the size shows the new value on the client right away, even though the server doesn't know yet. Usually those changes aren't sent straight to the server. Instead, when the modified data gets flushed to the server, matching attribute changes happen on the server too. When open delegation is in effect, the modified attributes may be sent back to the server in the response to a CB_RECALL call ๐ŸŒธ

Here's the tea: local attribute caching means each client's attribute cache won't be coherent ๐Ÿ˜ญ A change made in one order on the server could show up in a different order on one client and a third order on another. Typical filesystem APIs don't let you atomically change or check attributes for several files at once, so the rules below keep these incoherences under control. They come from how earlier NFS protocols did things ๐Ÿ’œ

  • All attributes for a given file (per-fsid attributes excepted) are cached as a unit at the client, so no non-serializability can show up within a single file. ๐ŸŽ€
  • There's an upper time boundary on how long a client cache entry can stay around without being refreshed from the server. โณ
  • When operations change attributes at the server, the updated attribute set is requested as part of the containing RPC. That includes directory operations that update attributes indirectly. This happens by following the modifying operation with a GETATTR operation, then using the GETATTR results to update the client's cached attributes. ๐Ÿ”ฅ

Also, if READDIR requests the full set of attributes to be cached, those results can be cached by the client on the same basis as attributes from GETATTR. Slay ๐Ÿ‘‘

A client can validate its cached attributes for a file by fetching just the change and time_access attributes. If change has the same value it had when the attributes were cached, it assumes no attributes other than time_access have changed. Why fetch time_access too? Because lots of servers run in environments where the operation that updates change doesn't update time_access. For example, POSIX file semantics don't update access time when a file is modified by the write system call ๐Ÿ’… So a client that wants a current time_access value should fetch it along with change during attribute cache validation and update its cached time_access.

A client may keep a cache of modified attributes for the ones tied to data of modified regular files: size, time_modify, and change ๐Ÿฆ‹ Other than those three, the client MUST NOT maintain a cache of modified attributes. Instead, attribute changes are immediately sent to the server.

In some operating environments, the equivalent of time_access gets updated implicitly by every read of the file object's content ๐ŸŒˆ If an NFS client is caching the content of a file object, whether that's a regular file, directory, or symbolic link, the client SHOULD NOT update the time_access attribute on the server (via SETATTR or a small READ or READDIR request) with each read satisfied from cache. Why? Because that can defeat the performance benefits of caching content, especially since an explicit SETATTR of time_access may alter the change attribute on the server. If change changes, clients caching the content will think the content has changed and will re-read unmodified data from the server ๐Ÿ˜ญ The client isn't encouraged to keep a modified version of time_access in its cache either. It would have to eventually write the access time to the server (bad performance vibes), or it would never update the server's time_access. That second option means an application caching access time between a close and open of the same file would see the access time oscillating between the past and present ๐Ÿ™ˆ The time_access attribute always means the time of last access to a file by a read that was satisfied by the server. That way, clients tend to see only time_access changes that go forward in time โญ


9.7. Data and Metadata Caching and Memory Mapped Files ๐Ÿงš

Some operating environments let an application map a file's content into its own address space ๐Ÿฆ‹ Every time the application touches a memory location that matches a block not yet loaded into the address space, a page fault happens and the file gets read. (If the block doesn't exist in the file, the block gets allocated and then instantiated in the application's address space.)

As long as each memory mapped access needs a page fault, the file attributes used to detect access and modification (time_access, time_metadata, time_modify, and change) will get updated ๐Ÿ’– But in lots of operating environments, when page faults aren't needed, these attributes won't be updated on reads or updates through memory access, whether the file is local or being accessed remotely. A client or server MAY fail to update attributes of a file being accessed via memory mapped I/O. This has a few implications ๐Ÿ‘‘

  • If there's an application on the server that has memory mapped a file a client is also accessing, the client might not be able to get a consistent value of change to figure out whether its cache is stale. A server that knows the file is memory mapped could always pessimistically return updated values for change, forcing the application to always get the freshest data and metadata for the file. But because of the negative performance implications, such behavior is OPTIONAL. ๐Ÿ™ˆ
  • If the memory mapped file isn't being modified on the server, and is only being read by an application through the memory mapped interface, the client won't see an updated time_access attribute. However, in lots of operating environments, neither will any process running on the server. So NFS clients aren't at any disadvantage compared to local processes โœจ
  • If another client is memory mapping the file, and that client holds a write delegation, the same issues as in the previous two bullets apply. So when a server does a CB_GETATTR to a file the client has modified in its cache, the response from CB_GETATTR won't necessarily be accurate. As discussed earlier, the client's obligation is to report that the file has been modified since the delegation was granted, not whether it's been modified again between successive CB_GETATTR calls. The server MUST assume that any file the client has modified in cache has been modified again between successive CB_GETATTR calls. Depending on the client's memory management system, this weak obligation may not be possible. A client MAY return stale information in CB_GETATTR whenever the file is memory mapped. ๐Ÿ˜ญ
  • Mixing memory mapping and file locking on the same file is problematic. Picture this scenario, where the page size on each client is 8192 bytes. ๐Ÿ’€
    • Client A memory maps first page (8192 bytes) of file X
    • Client B memory maps first page (8192 bytes) of file X
    • Client A write locks first 4096 bytes
    • Client B write locks second 4096 bytes
    • Client A, via a STORE instruction modifies part of its locked region.
    • Simultaneous to client A, client B issues a STORE on part of its locked region.

Here's the challenge: each client has to resynchronize to get a correct view of the first page ๐ŸŒธ In lots of operating environments, the virtual memory management systems on each client only know a page is modified, not that a subset of the page matching the respective lock regions has been modified. So each client can't do the right thing, which would be writing only its locked portion of the page to the server. For example, if client A simply writes out the page, and then client B writes out the page, client A's data is lost ๐Ÿ˜ญ

And if mandatory locking is enabled on the file, there's a whole other problem ๐Ÿ”ฅ When clients A and B issue the STORE instructions, the resulting page faults require a record lock on the entire page. Each client then tries to extend its locked range to the entire page, which results in a deadlock ๐Ÿ’€

Communicating the NFS4ERR_DEADLOCK error to a STORE instruction is difficult at best ๐Ÿ™ˆ

If a client locks the entire memory mapped file, there's no problem with advisory or mandatory record locking, at least until the client unlocks a region in the middle of the file ๐Ÿฆ‹

Given the issues above, the following are permitted:

  • Clients and servers MAY deny memory mapping a file they know has record locks. ๐ŸŽ€
  • Clients and servers MAY deny a record lock on a file they know is memory mapped. ๐ŸŽ€
  • A client MAY deny memory mapping a file that it knows requires mandatory locking for I/O. If mandatory locking is enabled after the file is opened and mapped, the client MAY deny the application further access to its mapped file. ๐Ÿซถ

9.8. Name Caching โœจ

okay so LOOKUP and READDIR results can get cached so the client doesn't have to keep asking the same question over and over ๐Ÿงš. Just like with attribute caching, the different client caches can end up disagreeing with each other ๐Ÿ™ˆ. To deal with that (and given how typical filesystem APIs work), there's an upper time boundary on how long a client name cache entry can hang around without checking that another client hasn't changed the directory in the meantime ๐Ÿ’….

When a client isn't changing a directory that has name cache entries, it needs to keep fetching attributes for that directory now and then to make sure nobody is messing with it ๐Ÿ‘€. Once it's confirmed nothing changed, the expiration time for those name cache entries can be bumped to the current time plus the name cache staleness bound ๐Ÿซถ.

When a client IS changing a directory, it needs to figure out whether other clients have changed it too. It does this by using the change attribute reported before and after the directory operation, in the change_info4 value returned for that operation ๐Ÿ”ฅ. The server can tell the client whether the change_info4 data is provided atomically with respect to the directory operation. If the change values are atomic, the client can compare the pre-operation change value with the one in its name cache. If that shows another client updated the directory, the name cache for that directory gets purged from the client ๐Ÿ’€. If it shows no modification, the client can update its name cache to reflect the directory operation and extend the timeout ๐ŸŒธ. The post-operation change value needs to be saved as the basis for future change_info4 comparisons ๐ŸŽ€.

As we just saw, name caching requires that the client revalidate name cache data by inspecting the change attribute of a directory at the point when the name cache item was cached. This means the server has to update the change attribute for directories whenever the contents of that directory get modified ๐Ÿง . For a client to use the change_info4 information appropriately and correctly, the server must report the pre and post operation change attribute values atomically. When the server is unable to report the before and after values atomically with respect to the directory operation, the server must indicate that fact in the change_info4 return value. When the information is not atomically reported, the client should not assume that other clients have not changed the directory ๐Ÿ˜ญ.


9.9. Directory Caching

the results of READDIR operations can be used to avoid later READDIR operations ๐Ÿ“š. Just like with attribute and name caching, the client caches can get out of sync ๐Ÿ™ƒ. To mitigate that, and given the context of typical filesystem APIs, the following rules should be followed:

  • Cached READDIR information for a directory which is not obtained in a single READDIR operation must always be a consistent snapshot of directory contents. This is determined by using a GETATTR before the first READDIR and after the last of READDIR that contributes to the cache.
  • An upper time boundary is maintained to indicate the length of time a directory cache entry is considered valid before the client must revalidate the cached information.

The revalidation technique is basically the same as the one for name caching ๐Ÿ”. When the client isn't changing the directory in question, checking the change attribute of the directory with GETATTR is good enough. The lifetime of the cache entry can be extended at these checkpoints โญ. When a client is modifying the directory, it needs to use the change_info4 data to determine whether other clients are modifying the directory too. If it's determined that no other client modifications are happening, the client may update its directory cache to reflect its own changes ๐Ÿ’œ.

As demonstrated previously, directory caching requires that the client revalidate directory cache data by inspecting the change attribute of a directory at the point when the directory was cached. This means the server has to update the change attribute for directories whenever the contents of the corresponding directory get modified ๐Ÿ’…. For a client to use the change_info4 information appropriately and correctly, the server must report the pre and post operation change attribute values atomically. When the server is unable to report the before and after values atomically with respect to the directory operation, the server must indicate that fact in the change_info4 return value. When the information is not atomically reported, the client should not assume that other clients have not changed the directory ๐Ÿซ .


10. Minor Versioning ๐Ÿฆ‹

okay so NFS version 4 needs to be able to evolve whenever it needs to, which is why it comes with the rules and framework for future minor changes or versioning ๐ŸŒˆ.

The base assumption is that any future accepted minor version has to follow the IETF process and be documented in a standards track RFC. So each minor version number corresponds to an RFC. Minor version zero of the NFS version 4 protocol is represented by this RFC ๐Ÿ“œ. The COMPOUND procedure will support encoding the minor version the client is requesting โœจ.

The basic rules for developing minor versions are below. Heads up: a future minor version may decide to modify or add to these rules as part of its own definition ๐Ÿ‘‘.

  1. Procedures are not added or deleted

To maintain the general RPC model, NFS version 4 minor versions will not add to or delete procedures from the NFS program ๐Ÿงฑ.

  1. Minor versions may add operations to the COMPOUND and CB_COMPOUND procedures.

Adding operations to the COMPOUND and CB_COMPOUND procedures doesn't affect the RPC model ๐Ÿซถ.

2.1 Minor versions may append attributes to GETATTR4args, bitmap4, and GETATTR4res.

This lets the attribute model grow and adapt in the future ๐ŸŒฑ.

2.2 Minor version X must append any new attributes after the last documented attribute.

Attribute results are specified as an opaque array of per-attribute XDR encoded results, so adding new attributes in the middle of the current definitions would be way too burdensome ๐Ÿ˜ฉ.

  1. Minor versions must not modify the structure of an existing operation's arguments or results.

Again, handling multiple structure definitions for a single operation is too burdensome ๐Ÿ™…โ€โ™€๏ธ. New operations should be added instead of modifying existing structures for a minor version.

This rule does not preclude the following adaptations in a minor version:

  • adding bits to flag fields such as new attributes to GETATTR's bitmap4 data type
  • adding bits to existing attributes like ACLs that have flag words
  • extending enumerated types (including NFS4ERR_*) with new values
  1. Minor versions may not modify the structure of existing attributes.
  2. Minor versions may not delete operations.

This prevents the potential reuse of a particular operation "slot" in a future minor version ๐Ÿ”’.

  1. Minor versions may not delete attributes.
  2. Minor versions may not delete flag bits or enumeration values.
  3. Minor versions may declare an operation as mandatory to NOT implement.

Specifying an operation as "mandatory to not implement" is equivalent to obsoleting an operation ๐Ÿชฆ. For the client, it means the operation should not be sent to the server. For the server, an NFS error can be returned as opposed to "dropping" the request as an XDR decode error. This approach lets an operation become obsolete while keeping its structure, so a future minor version can bring it back ๐Ÿ”„.

8.1 Minor versions may declare attributes mandatory to NOT implement.

8.2 Minor versions may declare flag bits or enumeration values as mandatory to NOT implement.

  1. Minor versions may downgrade features from mandatory to recommended, or recommended to optional.
  2. Minor versions may upgrade features from optional to recommended or recommended to mandatory.
  3. A client and server that support minor version X must support minor versions 0 (zero) through X-1 as well ๐Ÿค.
  4. No new features may be introduced as mandatory in a minor version.

This rule allows new functionality to be introduced and forces implementation experience to be gathered before a feature is designated as mandatory ๐Ÿงช.

  1. A client MUST NOT attempt to use a stateid, filehandle, or similar returned object from the COMPOUND procedure with minor version X for another COMPOUND procedure with minor version Y, where X != Y. ๐Ÿšซ

and that's the vibe for this part, bestie ๐ŸŒˆ๐Ÿ’–

11. Internationalization โœจ

Okay so the big I18N question for NFS version 4 (internationalization, if you're fancy ๐Ÿ’…) is all about file names and other strings that travel over the wire. Clients all over the world speak different languages, so the string format has to make sense for every one of them ๐ŸŒˆ The UTF-8 encoding of the UCS, as defined by [ISO10646], is how that happens, and it follows the policy in "IETF Policy on Character Sets and Languages", [RFC2277]. It's giving global ๐ŸŒธ

[RFC3454], also known as "stringprep", is a framework for using Unicode/UTF-8 in networking protocols. Its whole goal is to "increase the likelihood that string input and string comparison work in ways that make sense for typical users throughout the world." ๐Ÿซถ It also says a protocol has to define its own stringprep profile "in order to fully specify the processing options." This Internationalization section is where NFS version 4 spells out its stringprep profiles, and a lot of the vocab below comes straight from stringprep ๐Ÿงš

There are three UTF-8 string types defined for NFS version 4: utf8str_cs, utf8str_cis, and utf8str_mixed. Each one gets its own profile, and each profile covers the following, as stringprep requires:

  • The intended applicability of the profile
  • The character repertoire that is the input and output to stringprep (which is Unicode 3.2 for referenced version of stringprep)
  • The mapping tables from stringprep used (as described in section 3 of stringprep)
  • Any additional mapping tables specific to the profile
  • The Unicode normalization used, if any (as described in section 4 of stringprep)
  • The tables from stringprep listing of characters that are prohibited as output (as described in section 5 of stringprep)
  • The bidirectional string testing used, if any (as described in section 6 of stringprep)
  • Any additional characters that are prohibited as output specific to the profile

Stringprep talks about Unicode characters, but NFS version 4 deals in UTF-8 characters. There's a one-to-one mapping between UTF-8 and Unicode, so whenever the rest of this document mentions Unicode, just read it as UTF-8 ๐Ÿฆ‹

Much of the text for the profiles comes from [RFC3454] โœจ

11.1. Stringprep profile for the utf8str_cs type

Every use of the utf8str_cs type definition in the NFS version 4 protocol specification follows the profile named nfs4_cs_prep. ๐Ÿ’–

11.1.1. Intended applicability of the nfs4_cs_prep profile

The utf8str_cs type is a case-sensitive string of UTF-8 characters. Its main job in NFS Version 4 is naming components and pathnames, and those get stored on the server's filesystem ๐Ÿ“ Two different UTF-8 strings can end up being the same after they go through the utf8str_cs profile. If those two strings are names inside the same directory, the NFS version 4 server has to do one of the following:

  • Disallow creating a second name if its post-processed form collides with an existing name, or
  • Allow the second name to be created, but arrange things so that after post-processing, the second name is different from the post-processed form of the first name.

11.1.2. Character repertoire of nfs4_cs_prep

The nfs4_cs_prep profile uses Unicode 3.2, as defined in stringprep's Appendix A.1 ๐ŸŒธ

11.1.3. Mapping used by nfs4_cs_prep

The nfs4_cs_prep profile maps using the following table from stringprep:

Table B.1

Table B.2 isn't normally part of nfs4_cs_prep, since it's mainly for case-insensitive comparisons. BUT if the NFS version 4 file server supports the case_insensitive filesystem attribute, and case_insensitive is true, then the server MUST use Table B.2 (in addition to Table B.1) when processing utf8str_cs strings, and the client MUST assume that Table B.2 (in addition to Table B.1) is being used. ๐Ÿ™ˆ

If the case_preserving attribute is present and set to false, the NFS version 4 server MUST use Table B.2 to map case when processing utf8str_cs strings. Whether the server maps lower to upper case or upper to lower case is an implementation dependency ๐Ÿซ 

11.1.4. Normalization used by nfs4_cs_prep

The nfs4_cs_prep profile does not specify a normalization form. A later revision of this specification might specify one, so the server and client should expect that they may get unnormalized characters inside protocol requests and responses. If the operating environment needs normalization, the implementation must normalize utf8str_cs strings within the protocol before showing the information to an application (at the client) or the local filesystem (at the server) โœจ

11.1.5. Prohibited output for nfs4_cs_prep

The nfs4_cs_prep profile prohibits output using the following tables from stringprep:

Table C.3
Table C.4
Table C.5
Table C.6
Table C.7
Table C.8
Table C.9

11.1.6. Bidirectional output for nfs4_cs_prep

The nfs4_cs_prep profile doesn't specify any checking of bidirectional strings ๐Ÿง˜

11.2. Stringprep profile for the utf8str_cis type

Every use of the utf8str_cis type definition in the NFS version 4 protocol specification follows the profile named nfs4_cis_prep. ๐ŸŽ€

11.2.1. Intended applicability of the nfs4_cis_prep profile

The utf8str_cis type is a case-insensitive string of UTF-8 characters. Its main job in NFS Version 4 is naming NFS servers ๐Ÿ–ฅ๏ธ

11.2.2. Character repertoire of nfs4_cis_prep

The nfs4_cis_prep profile uses Unicode 3.2, as defined in stringprep's Appendix A.1 ๐Ÿ’…

11.2.3. Mapping used by nfs4_cis_prep

The nfs4_cis_prep profile maps using the following tables from stringprep:

Table B.1
Table B.2

11.2.4. Normalization used by nfs4_cis_prep

The nfs4_cis_prep profile uses Unicode normalization form KC, as described in stringprep ๐Ÿ“

11.2.5. Prohibited output for nfs4_cis_prep

The nfs4_cis_prep profile prohibits output using the following tables from stringprep:

Table C.1.2
Table C.2.2
Table C.3
Table C.4
Table C.5
Table C.6
Table C.7
Table C.8
Table C.9

11.2.6. Bidirectional output for nfs4_cis_prep

The nfs4_cis_prep profile checks bidirectional strings as described in stringprep's section 6 ๐Ÿ”ฅ

11.3. Stringprep profile for the utf8str_mixed type

Every use of the utf8str_mixed type definition in the NFS version 4 protocol specification follows the profile named nfs4_mixed_prep. ๐ŸŒˆ

11.3.1. Intended applicability of the nfs4_mixed_prep profile

The utf8str_mixed type is a string of UTF-8 characters with a case-sensitive prefix, a separator equal to '@', and a suffix that is a fully qualified domain name. Its main job in NFS Version 4 is naming principals identified in an Access Control Entry ๐Ÿ‘‘

11.3.2. Character repertoire of nfs4_mixed_prep

The nfs4_mixed_prep profile uses Unicode 3.2, as defined in stringprep's Appendix A.1 โญ

11.3.3. Mapping used by nfs4_cis_prep

For the prefix and the separator of a utf8str_mixed string, the nfs4_mixed_prep profile maps using the following table from stringprep:

Table B.1

For the suffix of a utf8str_mixed string, the nfs4_mixed_prep profile maps using the following tables from stringprep:

Table B.1
Table B.2

11.3.4. Normalization used by nfs4_mixed_prep

The nfs4_mixed_prep profile uses Unicode normalization form KC, as described in stringprep ๐Ÿงš

11.3.5. Prohibited output for nfs4_mixed_prep

The nfs4_mixed_prep profile prohibits output using the following tables from stringprep:

Table C.1.2
Table C.2.2
Table C.3
Table C.4
Table C.5
Table C.6
Table C.7
Table C.8
Table C.9

11.3.6. Bidirectional output for nfs4_mixed_prep

The nfs4_mixed_prep profile checks bidirectional strings as described in stringprep's section 6 ๐Ÿ’œ

If the client sends an invalid UTF-8 string, the server should return an NFS4ERR_INVAL error. That includes cases where inappropriate prefixes are detected, and cases where the count includes trailing bytes that don't make up a full UCS character ๐Ÿ˜ญ

If the client sent a string that is valid UTF-8 but has characters the server doesn't support for that string (for example, names with characters that take more than two octets, on a filesystem that only supports Unicode characters), the server should return an NFS4ERR_BADCHAR error ๐Ÿ™ƒ

If a UTF-8 string is used as a file name, and the filesystem supports every character in the name but still won't allow that particular name, the server should return NFS4ERR_BADNAME. This covers situations where the server filesystem imposes a normalization constraint on name strings. It also covers things like filesystems that prohibit "." and ".." as file names for certain operations, and other constraints like that ๐Ÿ’€

12. Error Definitions ๐Ÿšจ

NFS error numbers are assigned to failed operations inside a compound request. A compound request holds a bunch of NFS operations, and their results are encoded in order in a compound reply. Each successful operation returns an NFS4_OK status followed by its encoded results. If an NFS operation fails, an error status goes into the reply and the compound request is terminated ๐Ÿ›‘

Here's a description of each defined error:

NFS4_OK               Indicates the operation completed successfully.


NFS4ERR_ACCESS        Permission denied. The caller does not have the
                      correct permission to perform the requested
                      operation. Contrast this with NFS4ERR_PERM,
                      which restricts itself to owner or privileged
                      user permission failures.

NFS4ERR_ATTRNOTSUPP   An attribute specified is not supported by the
                      server.  Does not apply to the GETATTR
                      operation.

NFS4ERR_ADMIN_REVOKED Due to administrator intervention, the
                      lockowner's record locks, share reservations,
                      and delegations have been revoked by the
                      server.

NFS4ERR_BADCHAR       A UTF-8 string contains a character which is
                      not supported by the server in the context in
                      which it being used.

NFS4ERR_BAD_COOKIE    READDIR cookie is stale.

NFS4ERR_BADHANDLE     Illegal NFS filehandle. The filehandle failed
                      internal consistency checks.

NFS4ERR_BADNAME       A name string in a request consists of valid
                      UTF-8 characters supported by the server but
                      the name is not supported by the server as a
                      valid name for current operation.

NFS4ERR_BADOWNER      An owner, owner_group, or ACL attribute value
                      can not be translated to local representation.

NFS4ERR_BADTYPE       An attempt was made to create an object of a
                      type not supported by the server.

NFS4ERR_BAD_RANGE     The range for a LOCK, LOCKT, or LOCKU operation
                      is not appropriate to the allowable range of
                      offsets for the server.
  • NFS4ERR_BAD_SEQID ๐Ÿ’– okay so this one is all about sequence numbers in a locking request. The number has to be either the next one the server expects or the last one it already processed. Anything else and it's a no-go ๐Ÿ™…โ€โ™€๏ธ
  • NFS4ERR_BAD_STATEID ๐Ÿ” this is for a stateid the current server instance made, but that doesn't point to any locking state (current or superseded) for a current lockowner-file pair. Yikes, somebody used the wrong key ๐Ÿ’€
  • NFS4ERR_BADXDR ๐Ÿงฉ the server ran into an XDR decoding error while it was processing an operation. The bytes just didn't parse, bestie ๐Ÿ˜ญ
  • NFS4ERR_CLID_INUSE ๐Ÿ™…โ€โ™€๏ธ the SETCLIENTID operation noticed that a client id is already taken by a different client. Sharing is caring, but not with client ids ๐Ÿ’…
  • NFS4ERR_DEADLOCK ๐Ÿ”’ the server figured out that a file locking deadlock is happening for a blocking lock request. Two locks waiting on each other forever is so not it โœจ
  • NFS4ERR_DELAY โณ the server took the request, but couldn't finish it in a timely way. The client should wait a bit and then try again with a new RPC transaction ID. For example, a server that supports hierarchical storage might return this when a client asks for a file that's been migrated. In that case, the server should kick off the immigration process and reply to the client with this error. It can also show up when a necessary delegation recall means the request just can't be handled on time ๐Ÿ•ฐ๏ธ
  • NFS4ERR_DENIED ๐Ÿšซ an attempt to lock a file got denied. This might be temporary, so the client is encouraged to keep retrying the lock request until it's accepted ๐Ÿ”
  • NFS4ERR_DQUOT ๐Ÿ“ฆ the user has hit their hard resource (quota) limit on the server. Storage is full, babe ๐Ÿ˜ญ
  • NFS4ERR_EXIST ๐Ÿ“ File exists. The file you named is already there ๐Ÿ™ˆ
  • NFS4ERR_EXPIRED โŒ› a lease that the current operation is using has expired ๐Ÿ’€
  • NFS4ERR_FBIG ๐Ÿ˜ File too large. The operation would have made a file grow past the server's limit โœจ
  • NFS4ERR_FHEXPIRED ๐Ÿซ  the filehandle is volatile and has expired at the server. It's gone, no cap ๐Ÿ‘ป
  • NFS4ERR_FILE_OPEN ๐Ÿ“‚ the operation can't be processed successfully because a file involved in it is currently open ๐Ÿ™…โ€โ™€๏ธ
  • NFS4ERR_GRACE ๐ŸŒค๏ธ the server is in its recovery or grace period, which should match the server's lease period. Wait it out, queen ๐Ÿ‘‘
  • NFS4ERR_INVAL ๐Ÿง invalid argument, or an argument the operation doesn't support. Two examples: trying a READLINK on something that isn't a symbolic link, or setting an enum field to a value the protocol never defined (for example, an nfs_ftype4 value that doesn't exist) ๐Ÿ™ƒ
  • NFS4ERR_IO ๐Ÿ’ฅ I/O error. A hard error, like a disk failure, happened while the requested operation was running ๐Ÿ˜ฑ
  • NFS4ERR_ISDIR ๐Ÿ“‚ Is a directory. The caller gave a directory to an operation that only works on non-directories. Wrong vibe, wrong target ๐Ÿ™ˆ
  • NFS4ERR_LEASE_MOVED ๐Ÿšš a lease that's being renewed belongs to a filesystem that has been migrated to a new server. Moving day, lowkey chaotic ๐Ÿงณ
  • NFS4ERR_LOCKED ๐Ÿ” someone tried a read or write on a file that's locked. Not today ๐Ÿ™…โ€โ™€๏ธ
  • NFS4ERR_LOCK_NOTSUPP ๐Ÿ”„ the server doesn't support atomically upgrading or downgrading locks. It just can't do the glow-up ๐Ÿ’…
  • NFS4ERR_LOCK_RANGE ๐Ÿ“ a lock request is aiming at a sub-range of a current lock for the same lock owner, and the server doesn't support that kind of request. Not supported, sorry bestie ๐Ÿซถ
  • NFS4ERR_LOCKS_HELD ๐Ÿ” a CLOSE was attempted, but file locks would still exist after it. You can't just leave locks hanging around ๐Ÿ’€
  • NFS4ERR_MINOR_VERS_MISMATCH ๐Ÿ”ข the server got a request for a minor version it doesn't support. The server must return a COMPOUND4res with a zero length operations result array. Zero results, zero drama ๐Ÿซ 
  • NFS4ERR_MLINK ๐Ÿ”— Too many hard links ๐Ÿซฃ
  • NFS4ERR_MOVED ๐Ÿฆ‹ the filesystem that holds the current filehandle object has been relocated or migrated to another server. The client can find the new location by asking for the "fs_locations" attribute of the current filehandle. For more on this, check the section called "Filesystem Migration or Relocation" ๐Ÿงญ
  • NFS4ERR_NAMETOOLONG ๐Ÿ“ the filename in an operation was too long. Shorten it, babe โœ‚๏ธ
  • NFS4ERR_NOENT ๐Ÿ” No such file or directory. The name you gave doesn't exist ๐Ÿฅฒ
  • NFS4ERR_NOFILEHANDLE ๐Ÿซฅ the logical current filehandle (or, for RESTOREFH, the saved filehandle) hasn't been set properly. This can happen when a COMPOUND is malformed, meaning there's no PUTFH or PUTROOTFH before an operation that needs the current filehandle to be set ๐Ÿงฉ
  • NFS4ERR_NO_GRACE โฐ a reclaim of client state landed outside the server's grace period. Because of that, the server can't promise that conflicting state wasn't handed to another client. Too late, no cap ๐Ÿ˜ฌ
  • NFS4ERR_NOSPC ๐Ÿ’พ No space left on device. The operation would have pushed the server's filesystem past its limit ๐Ÿ“‰
  • NFS4ERR_NOTDIR ๐Ÿ“‚ Not a directory. The caller gave a non-directory to a directory operation ๐Ÿ™ƒ
  • NFS4ERR_NOTEMPTY ๐Ÿ—‘๏ธ someone tried to remove a directory that still has stuff in it ๐Ÿซ 
  • NFS4ERR_NOTSUPP ๐Ÿšซ the operation isn't supported. Not on the menu ๐Ÿฝ๏ธ
  • NFS4ERR_NOT_SAME ๐Ÿ” this one comes back from the VERIFY operation. It means the attributes that got compared weren't the same as what the client sent in its request ๐Ÿ™…โ€โ™€๏ธ
  • NFS4ERR_NXIO ๐ŸŒ€ I/O error. No such device or address ๐Ÿคทโ€โ™€๏ธ
  • NFS4ERR_OLD_STATEID ๐Ÿ•ฐ๏ธ the stateid that got used designates the locking state for a lockowner-file as it was at an earlier time. Old news, babe ๐Ÿ“œ
  • NFS4ERR_OPENMODE ๐Ÿ”“ the client tried a READ, WRITE, LOCK or SETATTR operation that the passed stateid doesn't allow. For example, writing to a file that was only opened for read. Hard pass ๐Ÿ™…โ€โ™€๏ธ
  • NFS4ERR_OP_ILLEGAL ๐Ÿšท an illegal operation value showed up in the argop field of a COMPOUND or CB_COMPOUND procedure. That op doesn't exist, bestie ๐Ÿซ 
  • NFS4ERR_PERM ๐Ÿ‘‘ Not owner. The operation wasn't allowed because the caller is neither a privileged user (root) nor the owner of the target. Hands off ๐Ÿ™…โ€โ™€๏ธ
  • NFS4ERR_RECLAIM_BAD ๐Ÿงพ the reclaim the client sent doesn't pass any of the server's state consistency checks, so it's bad news ๐Ÿ’€
  • NFS4ERR_RECLAIM_CONFLICT โš ๏ธ the client's reclaim ran into a conflict and can't be honored. This might mean the client is misbehaving ๐Ÿ‘€
  • NFS4ERR_RESOURCE ๐Ÿชซ while processing a COMPOUND procedure, the server can run out of resources and then can't keep going through the operations inside it. This error comes back in those resource-exhaustion moments tied to COMPOUND processing ๐Ÿ˜ฎโ€๐Ÿ’จ
  • NFS4ERR_RESTOREFH ๐Ÿซ™ the RESTOREFH operation doesn't have a saved filehandle (set by SAVEFH) to work with. Nothing to restore, babe ๐Ÿซค
  • NFS4ERR_ROFS ๐Ÿ“– Read-only filesystem. A modifying operation was attempted on a read-only filesystem. Look, but don't touch ๐Ÿ‘€
  • NFS4ERR_SAME ๐Ÿชž this one is returned by the NVERIFY operation. It means the attributes that got compared were the same as what the client sent ๐Ÿซถ
  • NFS4ERR_SERVERFAULT ๐Ÿค• an error happened on the server that doesn't match any of the legal NFS version 4 protocol error values. The client should translate it into an appropriate error. UNIX clients may choose to translate it to EIO ๐Ÿ› ๏ธ
  • NFS4ERR_SHARE_DENIED ๐Ÿค an attempt to OPEN a file with a share reservation failed because of a share conflict. Somebody already has that share ๐Ÿ™…โ€โ™€๏ธ
  • NFS4ERR_STALE ๐Ÿ‘ป Invalid filehandle. The filehandle in the arguments was invalid. Either the file it points to doesn't exist anymore, or access to it has been revoked ๐Ÿ’”
  • NFS4ERR_STALE_CLIENTID ๐Ÿซฅ a clientid the server doesn't recognize was used in a locking or SETCLIENTID_CONFIRM request. Who even are you? ๐Ÿคจ
  • NFS4ERR_STALE_STATEID ๐Ÿ•ธ๏ธ a stateid generated by an earlier server instance was used. It's from a past life, bestie ๐ŸŒ™
  • NFS4ERR_SYMLINK ๐Ÿ”— the current filehandle given to a LOOKUP isn't a directory but a symbolic link. This is also used when the final component of the OPEN path is a symbolic link ๐Ÿ™ƒ
  • NFS4ERR_TOOSMALL ๐Ÿ“ the encoded response to a READDIR request is bigger than the size limit set by the original request. Too big for the box ๐Ÿ“ฆ
  • NFS4ERR_WRONGSEC ๐Ÿ” the security mechanism the client is using for the operation doesn't match the server's security policy. The client should switch to a different security mechanism and retry the operation ๐Ÿ”
  • NFS4ERR_XDEV ๐ŸŒ attempting an operation between different fsids. Different worlds, no cross-dimensional vibes allowed ๐Ÿงš

13. NFS version 4 Requests โœจ

okay bestie, so the NFS version 4 RPC program only has two traditional RPC procedures: NULL and COMPOUND ๐Ÿ’… Everything else is defined as a set of operations, written in normal XDR/RPC syntax and semantics, but those operations get wrapped up inside COMPOUND. So the client has to bundle one or more NFS version 4 operations into a single request ๐Ÿ“ฆโœจ

The NFS4_CALLBACK program is how the server signals the client, and it's built pretty much the same way as the NFS version 4 program ๐Ÿ” It has CB_NULL and CB_COMPOUND, defined the same way NULL and COMPOUND are in the NFS program, and CB_COMPOUND holds the rest of the NFS4_CALLBACK operations. There's no predefined RPC program number for it, so the client picks one from the "transient" program range ๐ŸŽ€ The client also gives the program and port number for NFS4_CALLBACK during the SETCLIENTID/SETCLIENTID_CONFIRM sequence. You can change them with another SETCLIENTID/SETCLIENTID_CONFIRM sequence, and you can even do that within a client incarnation without removing the relevant leased client state. the way that's so flexible ๐Ÿ˜ญ๐Ÿฆ‹

13.1. Compound Procedure

the COMPOUND procedure is all about performance on high latency networks ๐Ÿš€ The client avoids stacking up latency from a bunch of separate RPCs by combining multiple dependent operations into one COMPOUND procedure. It can also simplify the protocol, since the client can mash basic procedures into one request that's tailored to its own environment ๐Ÿ’–

CB_COMPOUND works exactly like COMPOUND, with all the features we just covered ๐Ÿซถ

Here's the basic structure of the COMPOUND procedure:

+-----+--------------+--------+-----------+-----------+-----------+--
| tag | minorversion | numops | op + args | op + args | op + args |
+-----+--------------+--------+-----------+-----------+-----------+--

and the reply looks like this:

   +------------+-----+--------+-----------------------+--
   |last status | tag | numres | status + op + results |
   +------------+-----+--------+-----------------------+--

The numops and numres fields in those diagrams are the counts for the counted array encoding, which tell you how many arguments or results are in the request and response ๐Ÿ”ข With XDR encoding, those counts have to match EXACTLY the number of operation arguments or results that actually got encoded. no wiggle room, bestie ๐Ÿ™ˆ

13.2. Evaluation of a Compound Request

the server processes a COMPOUND procedure by evaluating each operation inside it, in order ๐Ÿ“‹ Each operation is a 32 bit operation code followed by an argument whose length depends on the type of operation. The results of each operation are encoded in sequence into a reply buffer, and each one is preceded by its opcode and a status code (normally zero) ๐ŸŒธ If an operation returns a non-zero status code, that status gets encoded, evaluation of the compound sequence halts, and the reply goes back right away ๐Ÿ›‘ Note that evaluation stops even for "non error" conditions like NFS4ERR_SAME, so don't be surprised when it stops early ๐Ÿ‘€

There are no atomicity requirements for the operations in a COMPOUND procedure. They can be evaluated at the same time as other COMPOUND requests the server gets ๐ŸŒˆ

It's the client's job to recover from any partially completed COMPOUND procedure ๐Ÿ’… Partial completion can happen at any point because of errors like NFS4ERR_RESOURCE and NFS4ERR_DELAY, and it can happen even with an otherwise valid operation string. A server reboot in the middle of processing a COMPOUND procedure can leave the client with the tough job of figuring out how far it got ๐Ÿ˜ญ So the client SHOULD avoid overly complex COMPOUND procedures in case an operation in the procedure fails ๐Ÿงš

Each operation assumes a "current" and "saved" filehandle that's part of the execution context of the compound request ๐Ÿ—‚๏ธ Operations can set, change, or return the current filehandle. The "saved" filehandle is for temporary storage of a filehandle value, and it's also used as the operand for the RENAME and LINK operations ๐Ÿฆ‹

13.3. Synchronous Modifying Operations

NFS version 4 operations that modify the filesystem are synchronous โœจ When an operation completes successfully at the server, the client can count on any data tied to that request being on stable storage. The one exception is file data in a WRITE operation with the UNSTABLE option specified ๐Ÿ’œ

This also means any earlier operations in the same compound request are reflected in stable storage too. That's what lets the client recover from a partially executed compound request caused by a server failure ๐Ÿ”ง For example, say a compound request has operations A and B, and the server can't send a response back to the client. Depending on how far the server got, the results of both operations might be in stable storage, or just operation A might be ๐Ÿ‘€ The server MUST NOT have just the results of operation B in stable storage. that's a hard no, bestie ๐Ÿšซ

13.4. Operation Values

The operations inside the COMPOUND procedure are identified by operation values ๐Ÿ”ข To avoid overlapping with the RPC procedure numbers, operations 0 (zero) and 1 aren't defined. Operation 2 is also not defined, but it's reserved for future use with minor versioning ๐ŸŽ€

14. NFS version 4 Procedures

14.1. Procedure 0: NULL - No Operation

SYNOPSIS

<null>

ARGUMENT

void;

RESULT

void;

DESCRIPTION

This is the standard NULL procedure: void argument, void response ๐Ÿซง It doesn't have any functionality attached to it. Because of that, people sometimes use it to measure the overhead of processing a service request. So the server SHOULD make sure it doesn't do any unnecessary work while servicing this procedure ๐Ÿ’–

ERRORS

None. ๐ŸŒธ

14.2. Procedure 1: COMPOUND - Compound Operations โœจ

SYNOPSIS

compoundargs -> compoundres

ARGUMENT

union nfs_argop4 switch (nfs_opnum4 argop) {
        case <OPCODE>: <argument>;
        ...
};

struct COMPOUND4args {
        utf8str_cs      tag;
        uint32_t        minorversion;
        nfs_argop4      argarray<>;
};

RESULT

union nfs_resop4 switch (nfs_opnum4 resop){
        case <OPCODE>: <result>;
        ...
};

struct COMPOUND4res {
        nfsstat4        status;
        utf8str_cs      tag;
        nfs_resop4      resarray<>;
};

DESCRIPTION

okay so COMPOUND is literally the bestie procedure ๐Ÿ’– It lets you bundle one or more NFS operations into a single RPC request. The main NFS RPC program only has two procedures, NULL and COMPOUND, and every other operation rides along inside COMPOUND as a wrapper ๐ŸŽ€

The server works through the operations one at a time. If an operation runs and its status is NFS4_OK, the server moves on to the next one. This keeps going until there's nothing left to run, or until one operation returns a status that isn't NFS4_OK, and then it stops ๐Ÿ›‘

Here's where it gets dramatic ๐Ÿ˜ญ While processing a COMPOUND, the server might realize it doesn't have enough resources to run some or all of the operations. When that happens, NFS4ERR_RESOURCE gets returned for the specific operation where things ran out. This only applies once every earlier operation in the sequence has been evaluated successfully. And the results for all of the operations that did get evaluated still have to go back to the client ๐Ÿซถ

The server will usually pick between two ways of decoding the client's request. The first is the traditional one-pass XDR decode, where the whole COMPOUND gets decoded before any operation runs. If XDR decoding fails in this case, an RPC XDR decode error comes back. The second is a two-step approach: first decode the basic COMPOUND request, then XDR decode each individual operation right before the server is ready to run it. The tricky part is that with this method, an XDR decode error can pop up in the middle of things, after some operations have already executed. Then the server returns NFS4ERR_BADXDR to signal the decode error ๐Ÿ™ˆ

The COMPOUND arguments have a "minorversion" field. Its initial and default value is 0 (zero). Future minor versions will use this field so the client can tell the server which minor version it's asking for ๐ŸŒธ If the server gets a COMPOUND with a minorversion value it doesn't support, it MUST return an error of NFS4ERR_MINOR_VERS_MISMATCH along with a zero length resultdata array. No exceptions, no vibes-based decisions ๐Ÿ’…

The COMPOUND results also contain a "status" field. If the results array isn't empty, this status must be equivalent to the status of the last operation that ran inside the COMPOUND. So if an operation failed, the "status" value is the same error that gets returned for that failed operation ๐Ÿ“

Some operation numbers are off limits here. Operations 0 (zero) and 1 (one) aren't defined for the COMPOUND procedure. Operation 2 isn't defined either, but it's reserved for future use with minor versioning โœจ If the server gets an operation array containing operation 2 and the minorversion field is 0 (zero), it returns NFS4ERR_OP_ILLEGAL, which is explained in the next paragraph. If the operation array contains operation 2 and the minorversion field is non-zero but the server doesn't support that minor version, the server returns NFS4ERR_MINOR_VERS_MISMATCH instead. So NFS4ERR_MINOR_VERS_MISMATCH takes precedence over every other error, full main character energy ๐Ÿ‘‘

It's also possible for the server to get an operation that's below the first legal operation (OP_ACCESS) or above the last legal operation (OP_RELEASE_LOCKOWNER) ๐Ÿ‘€

For those situations, the server's response encodes the opcode OP_ILLEGAL instead of the illegal opcode from the request. The status field in the ILLEGAL return results is set to NFS4ERR_OP_ILLEGAL, and the COMPOUND procedure's return results are NFS4ERR_OP_ILLEGAL too ๐Ÿšซ

The "tag" in the request is up to the implementor. It can summarize what the compound request is doing, which helps packet sniffers and engineers debugging implementations ๐Ÿ” The value of "tag" in the response SHOULD be the same value that was provided in the request. This goes for the tag field of the CB_COMPOUND procedure too ๐ŸŽ€

IMPLEMENTATION

An error of any type can show up after only some of the operations have been evaluated, so the client must be prepared to recover from any failure ๐Ÿ˜ญ If an NFS4ERR_RESOURCE error came from a complex or lengthy set of operations, the server would probably be able to run them successfully if there were fewer of them. That's why the client is responsible for handling this kind of complexity during recovery ๐Ÿงš

ERRORS

All errors defined in the protocol ๐Ÿ’œ


14.2.1. Operation 3: ACCESS - Check Access Rights

SYNOPSIS

(cfh), accessreq -> supported, accessrights

ARGUMENT

const ACCESS4_READ      = 0x00000001;
const ACCESS4_LOOKUP    = 0x00000002;
const ACCESS4_MODIFY    = 0x00000004;
const ACCESS4_EXTEND    = 0x00000008;
const ACCESS4_DELETE    = 0x00000010;
const ACCESS4_EXECUTE   = 0x00000020;

struct ACCESS4args {
        /* CURRENT_FH: object */
        uint32_t        access;
};

RESULT

struct ACCESS4resok {
        uint32_t        supported;
        uint32_t        access;
};

union ACCESS4res switch (nfsstat4 status) {
 case NFS4_OK:
         ACCESS4resok   resok4;
 default:
         void;
};

DESCRIPTION

ACCESS is the operation that checks what a user can actually do ๐Ÿฆ‹ The user is whoever is identified by the credentials in the RPC request, and the thing being checked is the file system object named by the current filehandle. The client puts the access rights it wants to check into the bit mask "access", and the server checks the permissions in that mask. If the status is NFS4_OK, the response includes two bit masks. The first, "supported", shows which access rights the server can verify reliably. The second, "access", shows which access rights the user has for the filehandle that was given ๐Ÿ’… On success, the current filehandle keeps its value โœจ

Quick heads up: the "supported" field only has as many values as were sent in the arguments. So if the client only sends ACCESS4_READ and the server supports it, the server returns just ACCESS4_READ, even if it could have checked other things too ๐Ÿ™ˆ

ngl, these results are only advisory ๐Ÿฅบ Getting NFS4_OK with the right bit set doesn't guarantee the access will still be allowed later, because the server can revoke access rights at any time ๐Ÿ’€

The access permissions that can be requested are:

ACCESS4_READ     Read data from file or read a directory.

ACCESS4_LOOKUP   Look up a name in a directory (no meaning for non-
                 directory objects).

ACCESS4_MODIFY   Rewrite existing file data or modify existing
                 directory entries.

ACCESS4_EXTEND   Write new data or add directory entries.

ACCESS4_DELETE   Delete an existing directory entry.

ACCESS4_EXECUTE  Execute file (no meaning for a directory).

Again, on success the current filehandle keeps its value ๐ŸŒˆ

IMPLEMENTATION

Clients really shouldn't try to figure out access permissions on their own by reading the uid, gid, and mode fields in the file attributes, or by trying to interpret the ACL attribute ๐Ÿ™…โ€โ™€๏ธ The server might be doing uid or gid mapping or adding extra access control restrictions. It's also possible the server isn't in the same ID space as the client. In these cases (and maybe others), the client can't reliably do an access check using only the current file attributes ๐Ÿ˜ญ

In NFS version 2, the only reliable way to find out if something was allowed was to just try it and see if it worked or failed. With the ACCESS operation in NFS version 4, the client can ask the server whether one or more classes of operations are permitted ๐ŸŽ€ ACCESS is there so clients can check first before running a series of operations that would hit an access failure. The OPEN operation is another spot where the server can verify access to the file object and send that info back to the client. ACCESS is still useful for directory operations, and for when the UNIX API "access" gets used on the client ๐Ÿงš

The info the server returns from an ACCESS call isn't permanent ๐Ÿซ  It was correct at the exact moment the server ran the checks, but it might not be correct after that, since the server can revoke access at any time.

The client should use the user's effective credentials to build the authentication information in the ACCESS request that determines access rights. Those effective user and group credentials are also what get used in later read and write operations ๐Ÿ’œ

Lowkey a fun fact: a lot of implementations don't directly support the ACCESS4_DELETE permission ๐Ÿ™ƒ Systems like UNIX ignore the ACCESS4_DELETE bit when it's set on an access request for a non-directory object. On those systems, deleting a file is controlled by the permissions on the directory the file lives in, not by the file's own permissions. So the mask returned to show which access rights can be determined has the ACCESS4_DELETE value set to 0. That tells the client the server couldn't check that particular access right. The client then ignores the ACCESS4_DELETE bit in the returned access mask ๐Ÿ“

ERRORS

  • NFS4ERR_ACCESS
  • NFS4ERR_BADHANDLE
  • NFS4ERR_BADXDR
  • NFS4ERR_DELAY
  • NFS4ERR_FHEXPIRED
  • NFS4ERR_INVAL
  • NFS4ERR_IO
  • NFS4ERR_MOVED
  • NFS4ERR_NOFILEHANDLE
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT
  • NFS4ERR_STALE

14.2.2. Operation 4: CLOSE - Close File โœจ

SYNOPSIS

(cfh), seqid, open_stateid -> open_stateid

ARGUMENT

struct CLOSE4args {
        /* CURRENT_FH: object */
        seqid4          seqid
        stateid4        open_stateid;
};

RESULT

union CLOSE4res switch (nfsstat4 status) {
 case NFS4_OK:
         stateid4       open_stateid;
 default:
         void;
};

DESCRIPTION

okay so CLOSE is the op that lets go of share reservations ๐Ÿ”“ for the regular or named attribute file that the current filehandle points at. The share reservations and any other state the server releases because of this CLOSE are tied only to the stateid you hand in. The seqid is what keeps everything in the right order ๐Ÿ’…, and state from other OPENs is totally untouched โœจ

If you're still holding record locks, the client SHOULD release all of them before sending a CLOSE ๐Ÿซถ The server MAY free every outstanding lock on CLOSE, but some servers won't CLOSE a file that still has record locks held ๐Ÿ˜ญ The server MUST return failure if any locks would still exist after the CLOSE, so no sneaking locks past it ๐Ÿ™ˆ

On success, the current filehandle keeps its value ๐ŸŒธ

IMPLEMENTATION

this part is lowkey a plot twist ๐ŸŽ€ Even though CLOSE hands back a stateid, that stateid isn't actually useful to the client, and it should be treated as deprecated. CLOSE "shuts down" the state for all OPENs on the file that belong to a single open_owner. As mentioned above, CLOSE will either release all of the file locking state or return an error, so the stateid it returns is not useful for whatever operations come after it ๐Ÿ’€

ERRORS

NFS4ERR_ADMIN_REVOKED
NFS4ERR_BADHANDLE
NFS4ERR_BAD_SEQID
NFS4ERR_BAD_STATEID
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_EXPIRED
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_ISDIR
NFS4ERR_LEASE_MOVED
NFS4ERR_LOCKS_HELD
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_OLD_STATEID
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_STALE_STATEID

14.2.3. Operation 5: COMMIT - Commit Cached Data ๐ŸŒˆ

SYNOPSIS

(cfh), offset, count -> verifier

ARGUMENT

struct COMMIT4args {
        /* CURRENT_FH: file */
        offset4         offset;
        count4          count;
};

RESULT

struct COMMIT4resok {
        verifier4       writeverf;
};

union COMMIT4res switch (nfsstat4 status) {
 case NFS4_OK:
         COMMIT4resok   resok4;
 default:
         void;
};

DESCRIPTION

COMMIT is the op that forces or flushes data out to stable storage ๐Ÿ’พ for the file the current filehandle points at. The data being flushed is whatever was earlier written with a WRITE operation whose stable field was set to UNSTABLE4 ๐Ÿฆ‹

The offset tells the server where in the file the flush should begin. An offset value of 0 (zero) means the flush starts at the very beginning of the file ๐Ÿงš The count is how many bytes of data to flush. If count is 0 (zero), the server flushes from the offset all the way to the end of the file โœจ

On successful completion, the server hands back a write verifier ๐ŸŽ€ The client uses that verifier to figure out whether the server restarted or rebooted between the original WRITE(s) and the COMMIT. It does this by comparing the verifier returned by the writes with the one returned by COMMIT. The server must vary the value of the write verifier at each server event or instantiation that may lead to a loss of uncommitted data. This most commonly happens when the server reboots, but other events at the server can cause uncommitted data loss too ๐Ÿ˜ญ

On success, the current filehandle keeps its value ๐ŸŒธ

IMPLEMENTATION

so COMMIT is basically the NFS cousin of the POSIX fsync(2) system call ๐Ÿซถ It synchronizes a file's state with the disk, meaning file data and metadata both get flushed to disk or stable storage. COMMIT does that same job for a client, flushing any unsynchronized data and metadata on the server to the server's disk or stable storage for the specified file. Just like fsync(2), there might be modified data or there might be none to sync. The server's normal periodic buffer synchronization may have already taken care of it ๐ŸŒ™ COMMIT should return NFS4_OK, unless there's been an unexpected error.

COMMIT differs from fsync(2) in one important way: the client can flush just a range of the file ๐Ÿ“ That's most likely triggered by a buffer-reclamation scheme on the client, before the file has been completely written.

The server side of COMMIT is pretty simple ๐Ÿ’… If the server gets a full file COMMIT request, meaning offset 0 and count 0, it should do the equivalent of fsync()'ing the file. Otherwise, it should arrange for the cached data in the range given by offset and count to be flushed to stable storage. In both cases, any metadata associated with the file must be flushed to stable storage before returning. It is not an error for there to be nothing to flush on the server. That just means the data and metadata that needed flushing were already flushed, or were lost during the last server failure ๐Ÿ’€

The client side is a little more complex ๐Ÿ”ฅ There are two reasons a client might want to commit a buffer to stable storage. The first is that the client wants to reuse a buffer. In that case, the offset and count of the buffer are sent to the server in the COMMIT request. The server flushes any cached data based on that offset and count, flushes the file's associated metadata too, and then returns the status of the flush along with the write verifier. The second reason is a full file flush, like what might happen at close. Here the client gathers all of the buffers for the file that contain uncommitted data, does a COMMIT with an offset of 0 and a count of 0, and then frees all of those buffers. Any other dirty buffers get sent to the server in the normal fashion ๐Ÿ‘‘

After a buffer is written by the client with the stable parameter set to UNSTABLE4, the buffer must be considered as modified by the client until it has either been flushed via a COMMIT operation or written via a WRITE operation with the stable parameter set to FILE_SYNC4 or DATA_SYNC4. This keeps the buffer from being freed and reused before the data can be flushed to stable storage on the server ๐Ÿงธ

When a response from either a WRITE or a COMMIT operation contains a write verifier that's different from one the server returned earlier, the client will need to retransmit all of the buffers that contain uncommitted cached data to the server. How that gets done is up to the implementor ๐ŸŒธ If there's only one buffer of interest, it should probably be sent back in a WRITE request with the appropriate stable parameter. If there's more than one buffer, it might be worthwhile to retransmit all of them in WRITE requests with the stable parameter set to UNSTABLE4, and then retransmit the COMMIT operation to flush all of the data on the server to stable storage. The timing of these retransmissions is left to the implementor โญ

The above description applies to page-cache-based systems as well as buffer-cache-based systems. In those systems, the virtual memory system will need to be modified instead of the buffer cache ๐Ÿฆ‹

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADHANDLE
NFS4ERR_BADXDR
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_ISDIR
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.4. Operation 6: CREATE - Create a Non-Regular File Object โœจ

SYNOPSIS

(cfh), name, type, attrs -> (cfh), change_info, attrs_set

ARGUMENT

union createtype4 switch (nfs_ftype4 type) {
 case NF4LNK:
         linktext4      linkdata;
 case NF4BLK:
 case NF4CHR:
         specdata4      devdata;
 case NF4SOCK:
 case NF4FIFO:
 case NF4DIR:
         void;
};

struct CREATE4args {
        /* CURRENT_FH: directory for creation */
        createtype4     objtype;
        component4      objname;
        fattr4          createattrs;
};

RESULT

struct CREATE4resok {
        change_info4    cinfo;
        bitmap4         attrset;        /* attributes set */

};

union CREATE4res switch (nfsstat4 status) {
 case NFS4_OK:
         CREATE4resok resok4;
 default:
         void;
};

DESCRIPTION

okay so CREATE is the op you use to make a non-regular file object inside a directory, bestie ๐Ÿ“โœจ and the rule is giving strict: the OPEN operation MUST be used to create a regular file. ๐Ÿ™…โ€โ™€๏ธ OPEN handles regular files, CREATE handles everything else, no mixing it up ๐Ÿ’…

The objname is the name for the new object, and objtype decides what kind of object gets made, like a directory, a symlink, and so on. ๐ŸŒธ

If there's already an object with that same name sitting in the directory, the server is gonna hit you with NFS4ERR_EXIST. ๐Ÿ˜ญ no duplicates, the girlies don't share names here ๐Ÿ’€

For the directory where the new object was created, the server sends back change_info4 info in cinfo. The atomic field in that struct tells you whether the before and after change attributes were obtained atomically with respect to the file object creation. ๐Ÿ” very precise, very iconic

If objname has a length of 0 (zero), or if objname doesn't follow the UTF-8 definition, you get NFS4ERR_INVAL. ๐Ÿ™ˆ not me forgetting the name is empty

The current filehandle gets swapped out for the new object's filehandle. ๐Ÿฆ‹

createattrs sets the starting attributes for the object. The set can include any writable attribute that's valid for that object type. โœจ When the operation works, the server sends back an attribute mask showing which attributes it actually managed to set. Main character reporting ๐Ÿ’œ

Now the owner stuff, which is kind of a big deal ๐Ÿ‘‘ If createattrs has neither the owner attribute nor an ACL with an ACE for the owner, AND the server's filesystem both supports and requires an owner attribute (or owner ACE), then the server MUST derive the owner (or the owner ACE). This would typically come from the principal in the RPC credentials of the call, but the server's operating environment or filesystem semantics may dictate other ways of deriving it. ๐Ÿงš

Same energy for the group: if createattrs has neither the group attribute nor a group ACE, and the filesystem both supports and requires the notion of a group attribute (or group ACE), the server MUST derive the group attribute (or the corresponding owner ACE) for the file. This could come from the RPC call's credentials, like the group principal if the credentials include it (such as with AUTH_SYS), or from the group identifier tied to the principal in the credentials (like POSIX systems, which keep a passwd database with the group identifier for every user identifier). It could also be inherited from the directory the object is created in, or whatever else the server's operating environment or filesystem semantics dictate. ๐ŸŒˆ This applies to the OPEN operation too, periodt.

Flip side: the client might ask for an owner attribute, group attribute, or ACL in createattrs that the principal in the RPC call's credentials isn't allowed to create files for. ๐Ÿšซ In that case the error is NFS4ERR_PERM. Also applies to OPEN, obvs.

IMPLEMENTATION

If the client wants attribute values set after the create, a SETATTR operation can get added to the COMPOUND request so those attributes get set. ๐ŸŽ€

ERRORS

NFS4ERR_ACCESS
NFS4ERR_ATTRNOTSUPP
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADNAME
NFS4ERR_BADOWNER
NFS4ERR_BADTYPE
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_DQUOT
NFS4ERR_EXIST
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NAMETOOLONG
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOSPC
NFS4ERR_NOTDIR
NFS4ERR_PERM
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.5. Operation 7: DELEGPURGE - Purge Delegations Awaiting Recovery ๐Ÿงน

SYNOPSIS

clientid ->

ARGUMENT

struct DELEGPURGE4args {
        clientid4       clientid;
};

RESULT

struct DELEGPURGE4res {
        nfsstat4        status;
};

DESCRIPTION

DELEGPURGE clears out all the delegations waiting on recovery for one client. ๐Ÿงผ It's super useful for clients that don't commit delegation info to stable storage, because it tells the server that conflicting requests don't need to be held up while it waits to recover that info. โณโžก๏ธโœจ

This operation should be used by clients that record delegation information on stable storage on the client. In that case, DELEGPURGE should be issued immediately after doing delegation recovery on all delegations known to the client. ๐ŸŽฏ Doing that tells the server that no more delegations for the client are gonna get recovered, so it can free up resources and stop holding up other clients whose requests conflict with the unrecovered delegations. ๐Ÿ™Œ The set of delegations the server knows about and the set the client knows about can be different, bc a client might crash after making a request that results in a delegation but before it gets the results and commits them to its stable storage. ๐Ÿ’€ relatable, honestly.

The server MAY support DELEGPURGE, but if it doesn't, it MUST NOT support CLAIM_DELEGATE_PREV. ๐Ÿ”’

ERRORS

NFS4ERR_BADXDR
NFS4ERR_NOTSUPP
NFS4ERR_LEASE_MOVED
NFS4ERR_MOVED
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE_CLIENTID

14.2.6. Operation 8: DELEGRETURN - Return Delegation ๐ŸŽ

SYNOPSIS

(cfh), stateid ->

ARGUMENT

struct DELEGRETURN4args {
        /* CURRENT_FH: delegated file */
        stateid4        stateid;
};

RESULT

struct DELEGRETURN4res {
        nfsstat4        status;
};

DESCRIPTION

This op gives back the delegation that's tied to the current filehandle and stateid. ๐Ÿ’Œ

Delegations can be returned when they get recalled, or voluntarily (aka before the server has recalled them). โœจ Either way, the client has to properly pass along any state that changed under the context of the delegation to the server before returning it. ๐Ÿซถ no skipping that step, bestie

ERRORS

NFS4ERR_ADMIN_REVOKED
NFS4ERR_BAD_STATEID
NFS4ERR_BADXDR
NFS4ERR_EXPIRED
NFS4ERR_INVAL
NFS4ERR_LEASE_MOVED
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOTSUPP
NFS4ERR_OLD_STATEID
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_STALE_STATEID

14.2.7. Operation 9: GETATTR - Get Attributes ๐Ÿ”Ž

SYNOPSIS

(cfh), attrbits -> attrbits, attrvals

ARGUMENT

struct GETATTR4args {
        /* CURRENT_FH: directory or file */
        bitmap4         attr_request;
};

RESULT

struct GETATTR4resok {
        fattr4          obj_attributes;
};

union GETATTR4res switch (nfsstat4 status) {
 case NFS4_OK:
         GETATTR4resok  resok4;
 default:
         void;
};

DESCRIPTION

GETATTR grabs attributes for the filesystem object behind the current filehandle. ๐Ÿ“ธ The client sets a bit in the bitmap argument for each attribute value it wants back. The server returns an attribute bitmap showing which attribute values it was able to return, followed by those values, ordered lowest attribute number first. ๐Ÿ“

The server must return a value for each attribute the client requests if the server supports that attribute. If the server doesn't support an attribute, or can't approximate a useful value, then it must not return that attribute value and must not set its bit in the result bitmap. ๐Ÿ™ƒ If the server supports an attribute but can't obtain its value, it must return an error, and in that case no attribute values come back at all. ๐Ÿ’…

All servers must support the mandatory attributes as specified in the section "File Attributes". โญ

On success, the current filehandle keeps its value. ๐ŸŒท

IMPLEMENTATION

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADHANDLE
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.8. Operation 10: GETFH - Get Current Filehandle ๐Ÿชช

SYNOPSIS

(cfh) -> filehandle

ARGUMENT

/* CURRENT_FH: */
void;

RESULT

struct GETFH4resok {
        nfs_fh4         object;
};

union GETFH4res switch (nfsstat4 status) {
 case NFS4_OK:
        GETFH4resok     resok4;
 default:
        void;
};

DESCRIPTION

This one just returns the current filehandle value. ๐Ÿงพ

On success, the current filehandle keeps its value. ๐Ÿ’–

IMPLEMENTATION

Ops that change the current filehandle, like LOOKUP or CREATE, don't automatically hand the new filehandle back as a result. ๐Ÿ™ˆ So if a client needs to look up a directory entry and get its filehandle, it needs this request:

PUTFH  (directory filehandle)
LOOKUP (entry name)
GETFH

ERRORS

NFS4ERR_BADHANDLE
NFS4ERR_FHEXPIRED
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

SYNOPSIS โœจ

(sfh), (cfh), newname -> (cfh), change_info

ARGUMENT ๐Ÿ’–

struct LINK4args {
        /* SAVED_FH: source object */
        /* CURRENT_FH: target directory */
        component4      newname;
};

RESULT ๐ŸŽ€

struct LINK4resok {
        change_info4    cinfo;
};

union LINK4res switch (nfsstat4 status) {
 case NFS4_OK:
         LINK4resok resok4;
 default:
         void;
};

DESCRIPTION ๐ŸŒธ

Okay so LINK is how you give an existing file a brand new name ๐Ÿฆ‹ The file you're linking is the one the saved filehandle points to (you set that up with SAVEFH first), and the new name goes into the directory the current filehandle points to. The existing file and the target directory have to live in the same filesystem on the server, or it's a no-go ๐Ÿ™…โ€โ™€๏ธ When it works, the current filehandle stays pointed at the target directory โœจ If there's already something in that directory with the same name as newname, the server has to hand back NFS4ERR_EXIST. Not cute, but that's the rule ๐Ÿ’…

For the target directory, the server sends back change_info4 info in cinfo. The atomic field in that struct tells you whether the before and after change attributes were grabbed atomically with respect to the link creation. Very detective energy ๐Ÿ•ต๏ธโ€โ™€๏ธ

If newname is zero length, or if it doesn't follow the UTF-8 definition, you get NFS4ERR_INVAL ๐Ÿฅฒ

IMPLEMENTATION ๐Ÿ”ฅ

Okay this part is so good. Any change to a property of a "hard" linked file shows up in all of its links, because they're literally the same file ๐Ÿ‘ฏโ€โ™€๏ธ When you make a new link, the file's numlinks attribute should go up by exactly one compared to what it was before the LINK operation ๐Ÿงš

The phrase "file and the target directory must reside within the same filesystem on the server" just means the fsid fields in the attributes for both objects are the same. If they're on different filesystems, the server returns NFS4ERR_XDEV ๐ŸŒˆ Also, on some servers the names "." and ".." aren't allowed as newname, so keep that in mind ๐Ÿ™ˆ

If newname is already linked to the file behind the saved filehandle, the server will return NFS4ERR_EXIST ๐Ÿ’€

Quick reminder: symbolic links aren't made with LINK. They're made with the CREATE operation ๐Ÿ“

ERRORS ๐Ÿ’œ

NFS4ERR_ACCESS
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADNAME
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_DQUOT
NFS4ERR_EXIST
NFS4ERR_FHEXPIRED
NFS4ERR_FILE_OPEN
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_ISDIR
NFS4ERR_MLINK
NFS4ERR_MOVED
NFS4ERR_NAMETOOLONG
NFS4ERR_NOENT
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOSPC
NFS4ERR_NOTDIR
NFS4ERR_NOTSUPP
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_WRONGSEC
NFS4ERR_XDEV

14.2.10. Operation 12: LOCK - Create Lock ๐Ÿ”

SYNOPSIS โœจ

(cfh) locktype, reclaim, offset, length, locker -> stateid

ARGUMENT ๐Ÿ’–

struct open_to_lock_owner4 {
        seqid4          open_seqid;
        stateid4        open_stateid;
        seqid4          lock_seqid;
        lock_owner4     lock_owner;
};

struct exist_lock_owner4 {
        stateid4        lock_stateid;
        seqid4          lock_seqid;
};

union locker4 switch (bool new_lock_owner) {
 case TRUE:
        open_to_lock_owner4     open_owner;
 case FALSE:
        exist_lock_owner4       lock_owner;
};

enum nfs_lock_type4 {
        READ_LT         = 1,
        WRITE_LT        = 2,
        READW_LT        = 3,    /* blocking read */
        WRITEW_LT       = 4     /* blocking write */
};

struct LOCK4args {
        /* CURRENT_FH: file */
        nfs_lock_type4  locktype;
        bool            reclaim;
        offset4         offset;
        length4         length;
        locker4         locker;
};

RESULT ๐ŸŽ€

struct LOCK4denied {
        offset4         offset;
        length4         length;
        nfs_lock_type4  locktype;
        lock_owner4     owner;
};

struct LOCK4resok {
        stateid4        lock_stateid;
};

union LOCK4res switch (nfsstat4 status) {
 case NFS4_OK:
         LOCK4resok     resok4;
 case NFS4ERR_DENIED:
         LOCK4denied    denied;
 default:
         void;
};

DESCRIPTION ๐ŸŒธ

LOCK is the operation where you ask for a record lock on a byte range ๐Ÿ”’ You pick the range with offset and length, and you also pick the lock type, which is one of the nfs_lock_type4 values. If this is a reclaim request, the reclaim parameter gets set to TRUE;

Here's the fun part: you can lock bytes even if they aren't actually allocated to the file yet ๐Ÿงš If you want to lock from a specific offset all the way to the end of the file (no matter how long it actually is), set the length to all bits set to 1 (one) ๐Ÿฆ‹ If the length is zero, or if it isn't all ones and adding it to the offset goes past the maximum 64-bit unsigned integer value, you get NFS4ERR_INVAL ๐Ÿฅฒ

Some servers only support locking byte offsets that fit within 32 bits ๐Ÿ™ˆ If the client asks for a range that includes a byte beyond the last offset of that 32-bit range, but doesn't include the last 32-bit offset and everything past it up to the end of the valid 64-bit range, then a 32-bit server MUST return NFS4ERR_BAD_RANGE. That one is a must-have, no exceptions ๐Ÿšซ

If the lock gets denied, the server sends back the owner, offset, and length of the conflicting lock ๐Ÿ’…

On success, the current filehandle keeps its value โœจ

IMPLEMENTATION ๐Ÿ”ฅ

If the server can't figure out the exact offset and length of the conflicting lock, it should just return the same offset and length that were in the arguments ๐Ÿซถ The File Locking section has the full scoop on this and all the other file locking operations, so go check that out ๐Ÿ“–

LOCK operations get permission checks and checks against the access type of the file ๐Ÿ›ก๏ธ But the exact rights and modes needed for each kind of lock depend on the semantics of the server-exported filesystem, and the protocol doesn't spell those out. For example, Windows 2000 lets you take a write lock on a file that's open for READ, while a POSIX-compliant system doesn't ๐Ÿ˜ญ Different vibes, different rules!

When the client asks for a lock on a range the lockowner has already locked (with the same or a different lock type), or on a sub-region of that range, or on a region that covers multiple locks the lockowner already holds (in whole or in part), and the server doesn't support that kind of locking (in other words, it doesn't do POSIX locking semantics), the server will return NFS4ERR_LOCK_RANGE ๐Ÿ’€ In that case the client may return an error, or it may emulate the operations by using only LOCK for ranges that don't include any bytes already locked by that lock_owner, plus LOCKU of the locks held by that lock_owner (with an exactly-matching range and type). Same deal if the client wants to upgrade (read lock to write lock) or downgrade (write lock to read lock) an existing record lock and the server doesn't support that: the server will return NFS4ERR_LOCK_NOTSUPP ๐Ÿ“ These workarounds might not perfectly match the real semantics when other clients send conflicting lock requests, so keep that in your back pocket ๐Ÿ‘€

The locker argument says which lock_owner goes with the LOCK request ๐Ÿงš The locker4 structure is a switched union that tells the server whether the lock_owner is already known to it or is brand new. If the lock_owner is known and already has an established lock_seqid, the argument is just the lock_owner and lock_seqid. If the lock_owner is not known to the server, the argument has the lock_owner and lock_seqid plus the open_stateid and open_seqid. The new lock_owner case covers the very first lock that lock_owner does, and it's the way to use the established state of the open_stateid to move over to using the lock_owner ๐ŸŒˆ

ERRORS ๐Ÿ’œ

NFS4ERR_ACCESS
NFS4ERR_ADMIN_REVOKED
NFS4ERR_BADHANDLE
NFS4ERR_BAD_RANGE
NFS4ERR_BAD_SEQID
NFS4ERR_BAD_STATEID
NFS4ERR_BADXDR
NFS4ERR_DEADLOCK
NFS4ERR_DELAY
NFS4ERR_DENIED
NFS4ERR_EXPIRED
NFS4ERR_FHEXPIRED
NFS4ERR_GRACE
NFS4ERR_INVAL
NFS4ERR_ISDIR
NFS4ERR_LEASE_MOVED
NFS4ERR_LOCK_NOTSUPP
NFS4ERR_LOCK_RANGE
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_NO_GRACE
NFS4ERR_OLD_STATEID
NFS4ERR_OPENMODE
NFS4ERR_RECLAIM_BAD
NFS4ERR_RECLAIM_CONFLICT
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_STALE_CLIENTID
NFS4ERR_STALE_STATEID

14.2.11. Operation 13: LOCKT - Test For Lock ๐Ÿ”

SYNOPSIS

(cfh) locktype, offset, length owner -> {void, NFS4ERR_DENIED ->
owner}

ARGUMENT

struct LOCKT4args {
        /* CURRENT_FH: file */
        nfs_lock_type4  locktype;
        offset4         offset;
        length4         length;
        lock_owner4     owner;
};

RESULT

struct LOCK4denied {
        offset4         offset;
        length4         length;
        nfs_lock_type4  locktype;
        lock_owner4     owner;
};

union LOCKT4res switch (nfsstat4 status) {
 case NFS4ERR_DENIED:
         LOCK4denied    denied;
 case NFS4_OK:
         void;
 default:
         void;
};

DESCRIPTION

Okay so LOCKT is basically the lock detective ๐Ÿ•ต๏ธโ€โ™€๏ธ. It checks whether a lock is already there. If there's a conflicting lock, it spills the owner, offset, length, and type of that lock. If nothing is held, you just get NFS4_OK and no drama โœจ. READ_LT and READW_LT get handled the same way, meaning the conflict test happens without caring about blocking or non-blocking. Same deal for WRITE_LT and WRITEW_LT ๐Ÿ’….

The ranges work exactly like they do for LOCK, so NFS4ERR_INVAL and NFS4ERR_BAD_RANGE show up under the same conditions as they do there ๐ŸŽ€.

On success, the current filehandle keeps its value ๐Ÿ’–.

IMPLEMENTATION

If the server can't figure out the exact offset and length of the conflicting lock, it should return the same offset and length you sent in the arguments as the denied results ๐Ÿซถ. The File Locking section has more deets on the file locking mechanisms, so go check that out ๐Ÿฆ‹.

LOCKT uses a lock_owner4 instead of a stateid4 (which is what LOCK uses to identify the owner). Why? Because the client doesn't have to open the file just to test for a lock, so a stateid might not even exist yet ๐ŸŒธ.

The test for conflicting locks should exclude locks held by the current lockowner. Since those aren't examined, possible overlapping ranges might not affect the LOCKT results at all ๐Ÿ™ˆ. If the server does examine locks that match the lockowner for range checking, NFS4ERR_LOCK_RANGE may be returned.. Then if it returns NFS4_OK, clients may still do a LOCK and get NFS4ERR_LOCK_RANGE back on that request, because the server has flexibility in how it handles this ๐Ÿ’€โœจ.

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADHANDLE
NFS4ERR_BAD_RANGE
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_DENIED
NFS4ERR_FHEXPIRED
NFS4ERR_GRACE
NFS4ERR_INVAL
NFS4ERR_ISDIR
NFS4ERR_LEASE_MOVED
NFS4ERR_LOCK_RANGE
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_STALE_CLIENTID

14.2.12. Operation 14: LOCKU - Unlock File ๐Ÿ”“

SYNOPSIS

(cfh) type, seqid, stateid, offset, length -> stateid

ARGUMENT

struct LOCKU4args {
        /* CURRENT_FH: file */
        nfs_lock_type4  locktype;
        seqid4          seqid;
        stateid4        stateid;
        offset4         offset;
        length4         length;
};

RESULT

union LOCKU4res switch (nfsstat4 status) {
 case   NFS4_OK:
         stateid4       stateid;
 default:
         void;
};

DESCRIPTION

The LOCKU operation unlocks the record lock specified by the parameters ๐ŸŒˆ. The client can set the locktype field to any value that's legal for the nfs_lock_type4 enumerated type, and the server MUST accept any legal value for locktype ๐Ÿ‘‘. Any legal value for locktype has no effect on whether the LOCKU operation succeeds or fails ๐Ÿ’….

The ranges work exactly like they do for LOCK, so NFS4ERR_INVAL and NFS4ERR_BAD_RANGE get returned under the same circumstances as they do there โœจ.

On success, the current filehandle keeps its value ๐Ÿฆ‹.

IMPLEMENTATION

If the area you want to unlock doesn't line up exactly with a lock the lockowner actually holds, the server may return NFS4ERR_LOCK_RANGE ๐Ÿ˜ญ. That covers a few cases: the area isn't locked at all, the area is a sub-range of the locked area, the area overlaps the locked area without matching it exactly, or the area includes multiple locks held by the lockowner. All of these are allowed by POSIX locking semantics ๐Ÿงš. So if a client gets this error and wants to support these operations, it should simulate the operation by using LOCKU on ranges that match locks it actually holds, possibly followed by LOCK requests for the sub-ranges that aren't being unlocked ๐Ÿ“.

ERRORS

NFS4ERR_ACCESS
NFS4ERR_ADMIN_REVOKED
NFS4ERR_BADHANDLE
NFS4ERR_BAD_RANGE
NFS4ERR_BAD_SEQID
NFS4ERR_BAD_STATEID
NFS4ERR_BADXDR
NFS4ERR_EXPIRED
NFS4ERR_FHEXPIRED
NFS4ERR_GRACE
NFS4ERR_INVAL
NFS4ERR_ISDIR
NFS4ERR_LEASE_MOVED
NFS4ERR_LOCK_RANGE
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_OLD_STATEID
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_STALE_STATEID

14.2.13. Operation 15: LOOKUP - Lookup Filename ๐Ÿ“‚

SYNOPSIS

(cfh), component -> (cfh)

ARGUMENT

struct LOOKUP4args {
        /* CURRENT_FH: directory */
        component4      objname;
};

RESULT

struct LOOKUP4res {
        /* CURRENT_FH: object */
        nfsstat4        status;
};

DESCRIPTION

This operation LOOKUPs, or finds, a filesystem object using the directory specified by the current filehandle ๐Ÿ”Ž. LOOKUP evaluates the component, and if the object exists, the current filehandle gets replaced with the component's filehandle ๐ŸŒธ.

If the component can't be evaluated, either because it doesn't exist or because the client doesn't have permission to evaluate it, then an error gets returned and the current filehandle stays unchanged ๐Ÿ™…โ€โ™€๏ธ.

If the component is a zero length string, or if any component doesn't obey the UTF-8 definition, the error NFS4ERR_INVAL gets returned ๐Ÿ’€.

IMPLEMENTATION

If the client wants a multi-component lookup, it can build a COMPOUND request like this one (and get each filehandle along the way) ๐Ÿงฉ:

PUTFH  (directory filehandle)
LOOKUP "pub"
GETFH
LOOKUP "foo"
GETFH
LOOKUP "bar"
GETFH

NFS version 4 servers are different from earlier NFS versions here, because they allow LOOKUP requests to cross mountpoints on the server ๐ŸŒ‰. The client can detect a mountpoint crossing by comparing the fsid attribute of the directory with the fsid attribute of the directory that was looked up. If the fsids are different, then the new directory is a server mountpoint. UNIX clients that detect a mountpoint crossing will need to mount the server's filesystem ๐Ÿ”ง. This needs to be done to keep the file object identity checking mechanisms that UNIX clients rely on working ๐Ÿซถ.

Servers that limit NFS access to "shares" or "exported" filesystems should provide a pseudo-filesystem that the exported filesystems can be integrated into, so clients can browse the server's name space ๐Ÿ—‚๏ธ. The clients' view of a pseudo filesystem will be limited to paths that lead to exported filesystems ๐Ÿฆ‹.

Note: previous versions of the protocol gave special semantics to the names "." and "..". NFS version 4 assigns no special semantics to these names ๐Ÿ™ƒ. The LOOKUPP operator must be used to look up a parent directory โœจ.

Note that this operation does not follow symbolic links ๐Ÿ”—. The client is responsible for all parsing of filenames, including filenames that get modified by symbolic links encountered during the lookup process ๐Ÿ’….

If the current filehandle supplied is not a directory but a symbolic link, the error NFS4ERR_SYMLINK is returned ๐Ÿ™ˆ. For all other non-directory file types, the error NFS4ERR_NOTDIR is returned ๐ŸŒˆ.

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADNAME
NFS4ERR_BADXDR
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NAMETOOLONG
NFS4ERR_NOENT
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOTDIR
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_SYMLINK
NFS4ERR_WRONGSEC

14.2.14. Operation 16: LOOKUPP - Lookup Parent Directory โฌ†๏ธ

SYNOPSIS

(cfh) -> (cfh)

ARGUMENT

/* CURRENT_FH: object */
void;

RESULT

struct LOOKUPP4res {
        /* CURRENT_FH: directory */
        nfsstat4        status;
};

DESCRIPTION

The current filehandle is assumed to refer to a regular directory or a named attribute directory ๐Ÿ“. LOOKUPP sets the filehandle for its parent directory as the current filehandle ๐Ÿช„. If there is no parent directory, an NFS4ERR_NOENT error must be returned ๐Ÿฅฒ. So the server will return NFS4ERR_NOENT when the current filehandle is at the root or top of the server's file tree ๐ŸŒณ.

IMPLEMENTATION

As with LOOKUP, LOOKUPP will also cross mountpoints ๐ŸŒ‰.

If the current filehandle is not a directory or named attribute directory, the error NFS4ERR_NOTDIR is returned ๐Ÿ™…โ€โ™€๏ธ.

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADHANDLE
NFS4ERR_FHEXPIRED
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NOENT
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOTDIR
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.15. Operation 17: NVERIFY - Verify Difference in Attributes ๐Ÿชž

SYNOPSIS

(cfh), fattr -> -

ARGUMENT

struct NVERIFY4args {
        /* CURRENT_FH: object */
        fattr4          obj_attributes;
};

RESULT

struct NVERIFY4res {
        nfsstat4        status;
};

DESCRIPTION

This operation is used to prefix a sequence of operations that get performed if one or more attributes have changed on some filesystem object ๐Ÿ”„. If all the attributes match, then the error NFS4ERR_SAME must be returned ๐Ÿซถ.

On success, the current filehandle keeps its value ๐Ÿ’–.

IMPLEMENTATION

This operation is super useful as a cache validation operator ๐Ÿง . If the object the attributes belong to has changed, then the following operations can obtain new data for that object โœจ. For instance, to check whether a file has changed and grab new data if it has, you could do this ๐Ÿ‘‡:

PUTFH  (public)
LOOKUP "foobar"
NVERIFY attrbits attrs
READ 0 32767

If a recommended attribute is specified in the NVERIFY operation, but the server doesn't support that attribute for the filesystem object, the error NFS4ERR_ATTRNOTSUPP is returned to the client ๐Ÿ˜ฌ.

When the attribute rdattr_error or any write-only attribute (for example, time_modify_set) is specified, the error NFS4ERR_INVAL is returned to the client ๐Ÿšซ.

ERRORS

NFS4ERR_ACCESS
NFS4ERR_ATTRNOTSUPP
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_RESOURCE
NFS4ERR_SAME
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.16. Operation 18: OPEN - Open a Regular File ๐Ÿšช

SYNOPSIS

(cfh), seqid, share_access, share_deny, owner, openhow, claim ->
(cfh), stateid, cinfo, rflags, open_confirm, attrset delegation

ARGUMENT

struct OPEN4args {
        seqid4          seqid;
        uint32_t        share_access;
        uint32_t        share_deny;
        open_owner4     owner;
        openflag4       openhow;
        open_claim4     claim;
};
     enum createmode4 {
             UNCHECKED4      = 0,
             GUARDED4        = 1,
             EXCLUSIVE4      = 2
     };

     union createhow4 switch (createmode4 mode) {
      case UNCHECKED4:
      case GUARDED4:
              fattr4         createattrs;
      case EXCLUSIVE4:
              verifier4      createverf;

     };

     enum opentype4 {
             OPEN4_NOCREATE  = 0,
             OPEN4_CREATE    = 1
     };

     union openflag4 switch (opentype4 opentype) {
      case OPEN4_CREATE:
              createhow4     how;
      default:
              void;
     };

     /* Next definitions used for OPEN delegation */
     enum limit_by4 {
             NFS_LIMIT_SIZE          = 1,
             NFS_LIMIT_BLOCKS        = 2
             /* others as needed */
     };

     struct nfs_modified_limit4 {
             uint32_t        num_blocks;
             uint32_t        bytes_per_block;
     };

     union nfs_space_limit4 switch (limit_by4 limitby) {
      /* limit specified as file size */
      case NFS_LIMIT_SIZE:
              uint64_t               filesize;
      /* limit specified by number of blocks */
      case NFS_LIMIT_BLOCKS:
              nfs_modified_limit4    mod_blocks;
     } ;

     enum open_delegation_type4 {
             OPEN_DELEGATE_NONE      = 0,
             OPEN_DELEGATE_READ      = 1,
             OPEN_DELEGATE_WRITE     = 2
     };

     enum open_claim_type4 {
             CLAIM_NULL              = 0,
             CLAIM_PREVIOUS          = 1,
             CLAIM_DELEGATE_CUR      = 2,
             CLAIM_DELEGATE_PREV     = 3
     };
     struct open_claim_delegate_cur4 {
             stateid4        delegate_stateid;
             component4      file;
     };

     union open_claim4 switch (open_claim_type4 claim) {
      /*
       * No special rights to file. Ordinary OPEN of the specified file.
       */
      case CLAIM_NULL:
              /* CURRENT_FH: directory */
              component4     file;

      /*
       * Right to the file established by an open previous to server
       * reboot.  File identified by filehandle obtained at that time
       * rather than by name.
       */
      case CLAIM_PREVIOUS:
              /* CURRENT_FH: file being reclaimed */
              open_delegation_type4   delegate_type;

      /*
       * Right to file based on a delegation granted by the server.
       * File is specified by name.
       */
      case CLAIM_DELEGATE_CUR:
              /* CURRENT_FH: directory */
              open_claim_delegate_cur4       delegate_cur_info;

      /* Right to file based on a delegation granted to a previous boot
       * instance of the client.  File is specified by name.
       */
      case CLAIM_DELEGATE_PREV:
              /* CURRENT_FH: directory */
              component4     file_delegate_prev;
     };

RESULT โœจ

   struct open_read_delegation4 {
           stateid4        stateid;        /* Stateid for delegation*/
           bool            recall;         /* Pre-recalled flag for
                                              delegations obtained
                                              by reclaim
                                              (CLAIM_PREVIOUS) */
           nfsace4         permissions;    /* Defines users who don't
                                              need an ACCESS call to
                                              open for read */
   };

   struct open_write_delegation4 {
           stateid4        stateid;        /* Stateid for delegation*/
           bool            recall;         /* Pre-recalled flag for
                                              delegations obtained
                                              by reclaim
                                              (CLAIM_PREVIOUS) */
           nfs_space_limit4 space_limit;   /* Defines condition that
                                              the client must check to
                                              determine whether the
                                              file needs to be flushed
                                              to the server on close.
                                              */
           nfsace4         permissions;    /* Defines users who don't
                                              need an ACCESS call as
                                              part of a delegated
                                              open. */
   };

   union open_delegation4
   switch (open_delegation_type4 delegation_type) {
           case OPEN_DELEGATE_NONE:
                   void;
           case OPEN_DELEGATE_READ:
                   open_read_delegation4 read;
           case OPEN_DELEGATE_WRITE:
                   open_write_delegation4 write;
   };

   const OPEN4_RESULT_CONFIRM      = 0x00000002;
   const OPEN4_RESULT_LOCKTYPE_POSIX = 0x00000004;

   struct OPEN4resok {
           stateid4        stateid;        /* Stateid for open */
           change_info4    cinfo;          /* Directory Change Info */
           uint32_t        rflags;         /* Result flags */
           bitmap4         attrset;        /* attributes on create */
           open_delegation4 delegation;    /* Info on any open
                                              delegation */
   };

   union OPEN4res switch (nfsstat4 status) {
    case NFS4_OK:
           /* CURRENT_FH: opened file */
           OPEN4resok      resok4;
    default:

           void;
   };

WARNING TO CLIENT IMPLEMENTORS ๐Ÿšจ

OPEN is kinda like LOOKUP in that it hands the client a filehandle to use. The big difference is that OPEN creates server state on that filehandle, and unlike LOOKUP, that state is a whole thing. In normal circumstances, the client can only let go of it with a CLOSE operation. CLOSE figures out which file to close by looking at the current filehandle, so the client MUST follow every OPEN operation with a GETFH operation in the same COMPOUND procedure. That way the client gets the filehandle it needs and CLOSE can work properly. ๐Ÿซถ

Just waiting for the lease on the file to expire is NOT enough, bestie. The server can keep that state around indefinitely as long as no other client tries to make a conflicting access to the same file. ๐Ÿ’€

DESCRIPTION ๐Ÿ’–

The OPEN operation creates and/or opens a regular file in a directory using the name you give it. If the file doesn't exist on the server and you want it created, the openhow parameter tells the server how to do the creating. You get to pick from three creation methods: UNCHECKED, GUARDED, or EXCLUSIVE. ๐ŸŽ€

If the current filehandle is a named attribute directory, OPEN will create or open a named attribute file. Heads up: exclusive create of a named attribute is not supported ๐Ÿ™…โ€โ™€๏ธ. So if the createmode is EXCLUSIVE4 and the current filehandle is a named attribute directory, the server will return EINVAL.

UNCHECKED means the file should be created if nothing with that name exists, and running into an existing regular file with that name is not an error. For this kind of create, createattrs sets up the initial attributes for the file. Those attributes can include any writable attribute that's valid for regular files. When an UNCHECKED create runs into an existing file, the attributes in createattrs get ignored, with one exception: if a size of zero is specified, the existing file gets truncated โœ‚๏ธ. GUARDED is the more careful sibling. The server checks for a duplicate object by name before doing anything, and if one exists you get NFS4ERR_EXIST as the status. If the object doesn't exist, the request is handled the same way as UNCHECKED. For both UNCHECKED and GUARDED, when the operation succeeds, the server sends back an attribute mask that shows which attributes were successfully set on the object. ๐Ÿงพ

EXCLUSIVE is the one with main character energy ๐Ÿ‘‘. It tells the server to follow exclusive creation semantics, and it uses the verifier to make sure the target gets created exclusively. The server should check for a duplicate object by name. If the object doesn't exist, the server creates it and stores the verifier with it. If the object does exist and the stored verifier matches the verifier the client sent, the server just uses the existing object as the newly created one. If the stored verifier doesn't match, you get NFS4ERR_EXIST back. In this case no attributes may be provided, because the server might use one of the target object's attributes to store the verifier. And if the server does use an attribute to store the exclusive create verifier, it signals which attribute by setting the matching bit in the attribute mask in the results. ๐Ÿ”

For the target directory, the server returns change_info4 information in cinfo. The atomic field of the change_info4 struct tells you whether the before and after change attributes were obtained atomically with respect to the link creation. ๐Ÿง 

When the create works, the current filehandle gets swapped for the one belonging to the new object ๐Ÿ”„.

The OPEN operation brings Windows share reservation capability with it, using the share_access and share_deny fields of the OPEN arguments. At OPEN, the client says which share_access and share_deny modes it needs. For clients that don't directly support SHAREs (so, UNIX), the expected deny value is DENY_NONE. If an existing SHARE reservation conflicts with the OPEN request, the server returns NFS4ERR_SHARE_DENIED ๐Ÿšซ. For a complete SHARE request, the client must give values for the owner and seqid fields of the OPEN argument. If you want more on SHARE semantics, check out the section on 'Share Reservations' ๐Ÿ“š.

Now, if the client is recovering state after a server failure, the claim field of the OPEN argument is what says "hey, this request is about reclaiming state I already had." ๐Ÿ”

The "claim" field of the OPEN argument is how the client specifies the file to open, plus the state information it claims to have. There are four basic claim types that cover the different OPEN situations. Here they are ๐Ÿ‘‡

  • CLAIM_NULL: For the client, this is a brand new OPEN request, and there's no previous state for this file on the client's side. ๐ŸŒฑ
  • CLAIM_PREVIOUS: The client is claiming basic OPEN state for a file it held before a server reboot. This usually happens when a server is returning persistent filehandles, since the client might not have the file name to reclaim the OPEN with. ๐Ÿงฉ
  • CLAIM_DELEGATE_CUR: The client is claiming a delegation for OPEN that the server granted. This is usually part of recalling a delegation. ๐Ÿ”™
  • CLAIM_DELEGATE_PREV: The client is claiming a delegation that was granted to a previous client instance, which comes up after the client reboots. The server MAY support CLAIM_DELEGATE_PREV. If it does, SETCLIENTID_CONFIRM MUST NOT remove the client's delegation state, and the server MUST support the DELEGPURGE operation. ๐Ÿฆ‹

For OPEN requests whose claim type is anything other than CLAIM_PREVIOUS (so, the ones that aren't reclaiming opens after a server reboot ๐Ÿ”„) that arrive during the server's grace or lease expiration period, the server returns an error of NFS4ERR_GRACE. ๐Ÿ™ˆ

For any OPEN request, the server may return an open delegation โœจ. That lets further opens and closes get handled locally on the client (see the section Open Delegation for the deets). Delegation is totally up to the server, so the client should never assume it will or won't get one. Always be ready for either outcome, bestie ๐Ÿ’…. The one partial exception is the reclaim case (CLAIM_PREVIOUS), where a delegation type is claimed. Here delegation will always be granted, although the server may specify an immediate recall in the delegation structure ๐Ÿ‘€.

The rflags returned by a successful OPEN let the server tell the client how the open file is supposed to be handled ๐Ÿ“ฆ.

OPEN4_RESULT_CONFIRM means the client MUST execute an OPEN_CONFIRM operation before using the open file ๐Ÿซถ. OPEN4_RESULT_LOCKTYPE_POSIX tells you the server's file locking supports the complete set of Posix locking techniques ๐Ÿ”. The client can use that to decide how to manage its file locking state, so it can handle any mismatch in how locking gets managed.

If the component is zero length, you get NFS4ERR_INVAL ๐Ÿ™…โ€โ™€๏ธ. The component also has to pass the normal UTF-8, character support, and name checks. Check the section "UTF-8 Related Errors" for the full tea ๐Ÿต.

When an OPEN is done and the specified lockowner already has the resulting filehandle open, the result is to "OR" the new share and deny status together with the existing status ๐Ÿซง. In this case only a single CLOSE needs to be done, even though multiple OPENs were completed. When that happens, checking share reservations for the new OPEN goes through normally, with no exception for the existing OPEN held by the same lockowner.

If the filesystem at the server is only accessible in read-only mode and the OPEN asks for ACCESS_WRITE or ACCESS_BOTH, the server returns NFS4ERR_ROFS to say "read-only filesystem" ๐Ÿ“–.

Just like with CREATE, the server MUST derive the owner, owner ACE, group, or group ACE if any of those four attributes are required and supported by the server's filesystem ๐Ÿง‘โ€๐Ÿ’ป. For an OPEN with the EXCLUSIVE4 createmode, the server has no choice, since those OPEN calls don't include the createattrs field ๐Ÿคทโ€โ™€๏ธ. But if createattrs is specified and includes an owner or group (or corresponding ACEs) that the principal in the RPC call's credentials isn't authorized to create files for, the server may return NFS4ERR_PERM ๐Ÿšซ.

If an OPEN specifies a size of zero (truncation, basically) and the file has named attributes, those named attributes are left as is. They are not removed. iykyk ๐Ÿงš

IMPLEMENTATION

The OPEN operation has support for EXCLUSIVE create, and it's giving NFS version 3 [RFC1813] energy. Like in NFS version 3, this gives reliable exclusive creation ๐Ÿ”’. It kicks in when the how parameter is EXCLUSIVE. The client provides a verifier that can reasonably be expected to be unique. A combo of a client identifier (maybe the client network address) and a unique number generated by the client (maybe the RPC transaction identifier) may be appropriate โœจ.

If the object doesn't exist, the server creates it and stores the verifier in stable storage. For filesystems that don't provide a mechanism for storing arbitrary file attributes, the server may use one or more elements of the object meta-data to store the verifier. The verifier must be stored in stable storage to prevent erroneous failure on retransmission of the request. It's assumed that an exclusive create is being performed because exclusive semantics are critical to the application ๐Ÿง . Because of that, exclusive CREATE does not rely solely on the normally volatile duplicate request cache for storage of the verifier. The duplicate request cache in volatile storage doesn't survive a crash and may actually flush on a long network partition, opening failure windows ๐Ÿ’€. In the UNIX local filesystem environment, the expected storage location for the verifier on creation is the meta-data (time stamps) of the object. For this reason, an exclusive object create may not include initial attributes, because the server would have nowhere to store the verifier ๐Ÿ•ฐ๏ธ.

If the server can not support these exclusive create semantics, possibly because of the requirement to commit the verifier to stable storage, it should fail the OPEN request with NFS4ERR_NOTSUPP ๐Ÿ™….

During an exclusive CREATE request, if the object already exists, the server reconstructs the object's verifier and compares it with the verifier in the request. If they match, the server treats the request as a success. The request is presumed to be a duplicate of an earlier, successful request whose reply was lost, and the server's duplicate request cache mechanism didn't detect it ๐Ÿ”. If the verifiers don't match, the request is rejected with NFS4ERR_EXIST โŒ.

Once the client has performed a successful exclusive create, it must issue a SETATTR to set the correct object attributes. Until it does so, it should not rely on any of the object attributes, since the server implementation may need to overload object meta-data to store the verifier ๐Ÿ˜ฌ. The subsequent SETATTR must not occur in the same COMPOUND request as the OPEN. This separation guarantees the exclusive create mechanism keeps working properly even in the face of retransmission of the request ๐Ÿซถ.

Use of the GUARDED attribute does not provide exactly-once semantics. In particular, if a reply is lost and the server doesn't detect the retransmission of the request, the operation can fail with NFS4ERR_EXIST, even though the create was performed successfully ๐Ÿ˜ญ. The client would use this behavior in the case that the application hasn't requested an exclusive create but has asked to have the file truncated when it's opened. If the client times out and retransmits the create request, the client can use GUARDED to prevent a sequence like: create, write, create (retransmitted) from happening ๐Ÿ›‘.

For SHARE reservations, the client must specify a value for share_access that is one of READ, WRITE, or BOTH. For share_deny, the client must specify one of NONE, READ, WRITE, or BOTH. If the client fails to do this, the server must return NFS4ERR_INVAL. ๐Ÿšซ

Based on the share_access value (READ, WRITE, or BOTH), the client should check that the requester has the proper access rights to perform the specified operation. This would generally be the result of applying the ACL access rules to the file for the current requester. However, just as with the ACCESS operation, the client should not attempt to second-guess the server's decisions, since access rights may change and may be subject to server administrative controls outside the ACL framework ๐Ÿ™ƒ. If the requester is not authorized to READ or WRITE (depending on the share_access value), the server must return NFS4ERR_ACCESS. Note that since the NFS version 4 protocol doesn't impose any requirement that READs and WRITEs issued for an open file have the same credentials as the OPEN itself, the server still must do appropriate access checking on the READs and WRITEs themselves ๐Ÿ”.

If the component provided to OPEN is a symbolic link, the error NFS4ERR_SYMLINK will be returned to the client. If the current filehandle is not a directory, the error NFS4ERR_NOTDIR will be returned ๐Ÿ“.

ERRORS

NFS4ERR_ACCESS
NFS4ERR_ADMIN_REVOKED
NFS4ERR_ATTRNOTSUPP
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADNAME
NFS4ERR_BADOWNER
NFS4ERR_BAD_SEQID
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_DQUOT
NFS4ERR_EXIST
NFS4ERR_EXPIRED
NFS4ERR_FHEXPIRED
NFS4ERR_GRACE
NFS4ERR_IO
NFS4ERR_INVAL
NFS4ERR_ISDIR
NFS4ERR_LEASE_MOVED
NFS4ERR_MOVED
NFS4ERR_NAMETOOLONG
NFS4ERR_NOENT
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOSPC
NFS4ERR_NOTDIR
NFS4ERR_NOTSUPP
NFS4ERR_NO_GRACE
NFS4ERR_PERM
NFS4ERR_RECLAIM_BAD
NFS4ERR_RECLAIM_CONFLICT
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_SHARE_DENIED
NFS4ERR_STALE
NFS4ERR_STALE_CLIENTID
NFS4ERR_SYMLINK
NFS4ERR_WRONGSEC

14.2.17. Operation 19: OPENATTR - Open Named Attribute Directory โœจ

SYNOPSIS

(cfh) createdir -> (cfh)

ARGUMENT

struct OPENATTR4args {
        /* CURRENT_FH: object */
        bool    createdir;
};

RESULT

struct OPENATTR4res {
        /* CURRENT_FH: named attr directory*/
        nfsstat4        status;
};

DESCRIPTION

The OPENATTR operation gets you the filehandle of the named attribute directory associated with the current filehandle ๐Ÿ—‚๏ธ. The result is a filehandle to an object of type NF4ATTRDIR. From that filehandle, READDIR and LOOKUP operations can be used to get filehandles for the various named attributes linked to the original filesystem object. Filehandles returned within the named attribute directory have a type of NF4NAMEDATTR ๐Ÿงš.

The createdir argument lets the client signify whether a named attribute directory should be created as a result of the OPENATTR operation. Some clients may use OPENATTR with a value of FALSE for createdir to check whether any named attributes exist for the object. If none exist, NFS4ERR_NOENT gets returned. If createdir has a value of TRUE and no named attribute directory exists, one is created. Creating one assumes the server has implemented named attribute support in this fashion, and it's not required to do so by this definition ๐Ÿค“.

IMPLEMENTATION

If the server doesn't support named attributes for the current filehandle, an error of NFS4ERR_NOTSUPP gets returned to the client ๐Ÿšซ.

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADHANDLE
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_DQUOT
NFS4ERR_FHEXPIRED
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NOENT
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOSPC
NFS4ERR_NOTSUPP
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.18. Operation 20: OPEN_CONFIRM - Confirm Open ๐Ÿ”

SYNOPSIS

(cfh), seqid, stateid-> stateid

ARGUMENT

struct OPEN_CONFIRM4args {
        /* CURRENT_FH: opened file */
        stateid4        open_stateid;
        seqid4          seqid;
};

RESULT

struct OPEN_CONFIRM4resok {
        stateid4        open_stateid;
};

union OPEN_CONFIRM4res switch (nfsstat4 status) {
 case NFS4_OK:
         OPEN_CONFIRM4resok     resok4;
 default:
         void;
};

DESCRIPTION

Okay so this operation is the first-time handshake ๐Ÿค for an open_owner. It's how the client confirms that it's actually using the sequence id for a given open_owner for the very first time. You pass in the stateid that came back from the OPEN operation, plus the next sequence id for that open_owner. And the seqid you send here MUST be 1 (one) greater than the seqid you sent in the OPEN operation that gave you the open_confirm value. Wait, that's not a typo, it's literally "must be 1 greater" and that's the rule ๐Ÿ™ˆ

Now here's the tea โ˜•: if the server gets a seqid that doesn't line up with the original open, it assumes you're not gonna confirm that original OPEN. So it releases all the state tied to that original OPEN. Harsh, but that's the vibe ๐Ÿ’€

On success, the current filehandle keeps its value โœจ

IMPLEMENTATION

Okay, this part is giving main character energy ๐Ÿ‘‘. A single client can make a bunch of open_owner4 data structures for one clientid. And honestly? The client is gonna dispose of some of them, or just stop using them for ages. That's exactly why NFS version 4 doesn't have an explicit operation to exit an open_owner4. Like, it would be pointless in that situation ๐Ÿ™„

So instead, the server has to avoid unbounded memory use. It needs a strategy for getting rid of open_owner4s that have no current lock, open, or delegation state for any files and haven't been used in a while. How long the server waits is totally up to the implementation ๐ŸŒธ. That said, the time period should certainly be no less than the lease time, plus any grace period the server wants to add on top of a lease time. The OPEN_CONFIRM operation is what lets the server safely clean up those unused open_owner4 structures ๐Ÿงน

Now, what if a client sends an OPEN and the server no longer has a record of that open_owner4? Then the server needs to make sure this is a brand new OPEN and not a replay or a retransmission ๐Ÿ‘€

Servers must not require confirmation on OPENs that grant delegations or are doing reclaim operations. Check out section "Use of Open Confirmation" for the details ๐Ÿ“–. The server can easily dodge this by just noting whether it has disposed of one open_owner4 for the given clientid. And if the server doesn't support delegation, it could simply keep one single bit that notes whether any open_owner4 (for any client) has been disposed of ๐Ÿ’…

The server must hold unconfirmed OPEN state until one of three events happens. ๐ŸŽฌ

First, the client sends an OPEN_CONFIRM request with the right sequence id and stateid within the lease period. Then the OPEN state on the server becomes confirmed, and the open_owner4 is fully established ๐Ÿฅณ

Second, the client sends another OPEN request with a sequence id that's out of sequence for that open_owner4. In this case, the server assumes the second OPEN request is the valid one and the first one was a replay ๐Ÿ”. So the server cancels the OPEN state from the first request, sets up unconfirmed OPEN state for the second request, and replies to the second request saying an OPEN_CONFIRM is needed. And then the whole cycle repeats itself ๐Ÿ˜ตโ€๐Ÿ’ซ. Now, there's a potential denial of service attack on the client here, but it gets mitigated if the client and server both require a security flavor based on Kerberos V5, LIPKEY, or some other flavor that uses cryptography ๐Ÿ”’

Here's a tricky one ๐Ÿง: what if the server is in the unconfirmed OPEN state for a given open_owner4, and it gets an operation on that open_owner4 that has a stateid but isn't OPEN? Or it's an OPEN_CONFIRM but with the wrong stateid? Then, even if the seqid is correct, the server returns NFS4ERR_BAD_STATEID. Why? Because the server assumes the operation is a replay. If there's no established OPEN state, then there's no way something like a LOCK operation could be valid ๐Ÿšซ

Third, neither of the first two events happens for the open_owner4 within the lease period. Then the OPEN state gets canceled, and the open_owner4 can be disposed of ๐Ÿ—‘๏ธ

ERRORS

  • NFS4ERR_ADMIN_REVOKED
  • NFS4ERR_BADHANDLE
  • NFS4ERR_BAD_SEQID
  • NFS4ERR_BAD_STATEID
  • NFS4ERR_BADXDR
  • NFS4ERR_EXPIRED
  • NFS4ERR_FHEXPIRED
  • NFS4ERR_INVAL
  • NFS4ERR_ISDIR
  • NFS4ERR_MOVED
  • NFS4ERR_NOFILEHANDLE
  • NFS4ERR_OLD_STATEID
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT
  • NFS4ERR_STALE
  • NFS4ERR_STALE_STATEID

14.2.19. Operation 21: OPEN_DOWNGRADE - Reduce Open File Access ๐ŸŽ€

SYNOPSIS

(cfh), stateid, seqid, access, deny -> stateid

ARGUMENT

struct OPEN_DOWNGRADE4args {
        /* CURRENT_FH: opened file */
        stateid4        open_stateid;
        seqid4          seqid;
        uint32_t        share_access;
        uint32_t        share_deny;
};

RESULT

struct OPEN_DOWNGRADE4resok {
        stateid4        open_stateid;
};

union OPEN_DOWNGRADE4res switch(nfsstat4 status) {
 case NFS4_OK:
        OPEN_DOWNGRADE4resok    resok4;
 default:
        void;
};

DESCRIPTION

Okay so this operation is all about tweaking the share_access and share_deny bits for a given open ๐Ÿฆ‹. Why would you need that? Well, say an openowner opens the same file multiple times with different share_access and share_deny flags. Then when you close one of those opens, the flags might need to change so the bits tied to opens that aren't in effect anymore get removed. Makes sense, right? ๐Ÿง 

The share_access and share_deny bits you send in this operation replace the current ones for the open file. And here's the strict part ๐Ÿซฃ: the share_access and share_deny bits you specify must be exactly equal to the union of the share_access and share_deny bits for some subset of the OPENs in effect for the current openowner on the current file. If that rule isn't followed, the server should return NFS4ERR_INVAL. Also, since these bits are subsets of what's already been granted, this request can't get denied because of conflicting share reservations. Bestie, that's a whole mood ๐ŸŒˆ

On success, the current filehandle keeps its value ๐Ÿ’–

ERRORS

  • NFS4ERR_ADMIN_REVOKED
  • NFS4ERR_BADHANDLE
  • NFS4ERR_BAD_SEQID
  • NFS4ERR_BAD_STATEID
  • NFS4ERR_BADXDR
  • NFS4ERR_EXPIRED
  • NFS4ERR_FHEXPIRED
  • NFS4ERR_INVAL
  • NFS4ERR_MOVED
  • NFS4ERR_NOFILEHANDLE
  • NFS4ERR_OLD_STATEID
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT
  • NFS4ERR_STALE
  • NFS4ERR_STALE_STATEID

14.2.20. Operation 22: PUTFH - Set Current Filehandle ๐ŸŒธ

SYNOPSIS

filehandle -> (cfh)

ARGUMENT

struct PUTFH4args {
        nfs_fh4         object;
};

RESULT

struct PUTFH4res {
        /* CURRENT_FH: */
        nfsstat4        status;
};

DESCRIPTION

This one's simple but iconic โœจ. It replaces the current filehandle with the filehandle you pass in as the argument. ๐Ÿซถ

Now, if the security mechanism the requester is using doesn't meet the requirements of the filehandle given to this operation, the server MUST return NFS4ERR_WRONGSEC ๐Ÿšจ. No exceptions, periodt ๐Ÿ’…

IMPLEMENTATION

This is commonly used as the first operator in an NFS request, because it sets the context for everything that follows ๐Ÿงš

ERRORS

  • NFS4ERR_BADHANDLE
  • NFS4ERR_BADXDR
  • NFS4ERR_FHEXPIRED
  • NFS4ERR_MOVED
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT
  • NFS4ERR_STALE
  • NFS4ERR_WRONGSEC

14.2.21. Operation 23: PUTPUBFH - Set Public Filehandle ๐Ÿฆ‹

SYNOPSIS

- -> (cfh)

ARGUMENT

void;

RESULT

struct PUTPUBFH4res {
        /* CURRENT_FH: public fh */
        nfsstat4        status;
};

DESCRIPTION

Okay, this one replaces the current filehandle with the filehandle that represents the public filehandle of the server's name space ๐ŸŒ. And this might be different from the "root" filehandle, which could be tied to some other directory on the server.

The public filehandle represents the concepts laid out in [RFC2054], [RFC2055], and [RFC2224] ๐Ÿ“š. The whole point for NFS version 4 is that the public filehandle (through the PUTPUBFH operation) gets used as a way to give WebNFS server compatibility with NFS versions 2 and 3 ๐Ÿ•ธ๏ธ.

The public filehandle and the root filehandle (represented by the PUTROOTFH operation) should be equivalent. But if they're not equivalent, then the public filehandle MUST be a descendant of the root filehandle ๐ŸŒณ. Lowkey important!

IMPLEMENTATION

This is used as the first operator in an NFS request to set the context for the operations that follow ๐ŸŽฌ.

With the NFS version 2 and 3 public filehandle, the client can specify whether the path name in the LOOKUP should be evaluated as an absolute path relative to the server's root, or as a path relative to the public filehandle. [RFC2224] has more discussion on that functionality ๐Ÿ“–. With NFS version 4, though, that kind of specification isn't directly available in the LOOKUP operation. Why? Because the component separators needed to specify absolute vs. relative aren't allowed in NFS version 4 ๐Ÿ™ƒ. So the client is responsible for building its request so that PUTROOTFH or PUTPUBFH is used to signify absolute or relative evaluation of an NFS URL, respectively.

Heads up โš ๏ธ: there are warnings mentioned in [RFC2224] about absolute evaluation, and about the restrictions the server may place on that evaluation based on how much of its namespace has been made available. Those same warnings apply to NFS version 4 too. It's likely, because of server implementation details, that an NFS version 3 absolute public filehandle lookup may behave differently than an NFS version 4 absolute resolution ๐Ÿคทโ€โ™€๏ธ.

There's a form of security negotiation described in [RFC2755] that uses the public filehandle as a method of employing SNEGO. This method isn't available with NFS version 4, though, since filehandles aren't overloaded with special meaning and so they don't provide the same framework as NFS versions 2 and 3 ๐Ÿ˜ค. Clients should therefore use the security negotiation mechanisms described in this RFC ๐Ÿ”.

ERRORS

  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT
  • NFS4ERR_WRONGSEC

14.2.22. Operation 24: PUTROOTFH - Set Root Filehandle ๐Ÿ‘‘

SYNOPSIS

- -> (cfh)

ARGUMENT

void;

RESULT

struct PUTROOTFH4res {
        /* CURRENT_FH: root fh */
        nfsstat4        status;
};

DESCRIPTION

This replaces the current filehandle with the filehandle that represents the root of the server's name space ๐ŸŒฒ. From this filehandle, a LOOKUP operation can find any other filehandle on the server, which is honestly so powerful ๐Ÿ˜ฎโ€๐Ÿ’จ. This filehandle might be different from the "public" filehandle, which could be tied to some other directory on the server ๐Ÿ’œ.

IMPLEMENTATION

This is commonly used as the first operator in an NFS request to set the context for the operations that follow ๐Ÿงšโ€โ™€๏ธ.

ERRORS

  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT
  • NFS4ERR_WRONGSEC

14.2.23. Operation 25: READ - Read from File โœจ

SYNOPSIS

(cfh), stateid, offset, count -> eof, data

ARGUMENT

struct READ4args {
        /* CURRENT_FH: file */
        stateid4        stateid;
        offset4         offset;
        count4          count;
};

RESULT

struct READ4resok {
        bool            eof;
        opaque          data<>;
};

 union READ4res switch (nfsstat4 status) {
  case NFS4_OK:
          READ4resok     resok4;
  default:
          void;
 };

DESCRIPTION

Okay, so READ is the operation that pulls data out of the regular file sitting in the current filehandle. Bestie, it's giving main character energy ๐Ÿ’…

The client picks where to start with an offset and how many bytes it wants with a count. An offset of 0 means "start at the very beginning of the file" ๐ŸŒธ If the offset is greater than or equal to the file's size, you still get NFS4_OK, but the data length comes back as 0 and eof is set to TRUE. And yes, every READ gets checked against access permissions ๐Ÿ”’

If the client asks for a count of 0, the READ still succeeds and hands back 0 bytes, and that's still subject to access permissions checking too. The server is allowed to send back fewer bytes than the client asked for, so the client has to check for that and handle it properly ๐Ÿงš

The stateid on a READ is the thing you got from an earlier record lock or share reservation request. The server uses it to confirm that the share reservation and any record locks are still valid, and it also uses it to bump the lease timeouts for the client โฐ

eof gets returned as TRUE if the read ends right at the end-of-file (formally, in a correctly formed READ, that's when offset + count is equal to the file size) or if the read goes past the end (offset + count is greater than the file size). Otherwise it's FALSE. And a successful READ on an empty file will always give you eof as TRUE ๐Ÿซถ

If the current filehandle isn't a regular file, the client gets an error back. If it's a directory, you'll see NFS4ERR_ISDIR. For anything else, it's NFS4ERR_INVAL ๐Ÿ™ˆ

For a READ with a stateid of all bits 0, the server MAY let the READ go ahead subject to mandatory file locks or the file's current share deny modes. For a READ with a stateid of all bits 1, the server MAY let READ operations skip the locking checks on the server ๐Ÿ”ฅ

When the READ succeeds, the current filehandle keeps its value. No drama ๐Ÿ’–

IMPLEMENTATION

Heads up: the server can return fewer bytes than count, and that's not a bug, it's just how it goes ๐Ÿ‘€

If the server gives back less than count and eof is FALSE, the client should send another READ to grab the rest. Servers might return less data for a few reasons. The file could have been truncated by another client, or by the server itself, so its size is no longer what the requesting client thinks it is, and that shrinks the amount of data the client can actually get. The server might also back off the transfer size and shorten the read response. And if the server is running low on resources, it might need to send a smaller read return too ๐Ÿ˜ญ

If mandatory file locking is on for the file, and the region you want to read is write locked by an owner that isn't associated with the stateid, the server returns the NFS4ERR_LOCKED error. Before retrying the READ, the client should try to get the right read record lock using the LOCK operation. Once the READ finishes, the client should release that record lock with LOCKU ๐Ÿ”โœจ

ERRORS

   NFS4ERR_ACCESS
   NFS4ERR_ADMIN_REVOKED
   NFS4ERR_BADHANDLE
   NFS4ERR_BAD_STATEID
   NFS4ERR_BADXDR
   NFS4ERR_DELAY
   NFS4ERR_EXPIRED
   NFS4ERR_FHEXPIRED
   NFS4ERR_GRACE
   NFS4ERR_IO
   NFS4ERR_INVAL
   NFS4ERR_ISDIR
   NFS4ERR_LEASE_MOVED
   NFS4ERR_LOCKED
   NFS4ERR_MOVED
   NFS4ERR_NOFILEHANDLE
   NFS4ERR_NXIO
   NFS4ERR_OLD_STATEID
   NFS4ERR_OPENMODE
   NFS4ERR_RESOURCE
   NFS4ERR_SERVERFAULT
   NFS4ERR_STALE
   NFS4ERR_STALE_STATEID

14.2.24. Operation 26: READDIR - Read Directory ๐Ÿ“‚โœจ

SYNOPSIS

(cfh), cookie, cookieverf, dircount, maxcount, attr_request ->
cookieverf { cookie, name, attrs }

ARGUMENT

struct READDIR4args {
        /* CURRENT_FH: directory */
        nfs_cookie4     cookie;
        verifier4       cookieverf;
        count4          dircount;
        count4          maxcount;
        bitmap4         attr_request;
};

RESULT

struct entry4 {
        nfs_cookie4     cookie;
        component4      name;
        fattr4          attrs;
        entry4          *nextentry;
};

struct dirlist4 {
        entry4          *entries;
        bool            eof;
};

struct READDIR4resok {
        verifier4       cookieverf;
        dirlist4        reply;
};


union READDIR4res switch (nfsstat4 status) {
 case NFS4_OK:
        READDIR4resok  resok4;
 default:
        void;
};

DESCRIPTION

READDIR is the op that grabs a batch of entries from a directory, along with whatever attributes the client asked for on each one, plus the info needed to ask for the next batch later. ๐Ÿ’…

The arguments carry a cookie that says where the READDIR should start. A cookie of 0 (zero) means "start at the very beginning," and for every follow-up READDIR, the client passes along a cookie the server gave it on a previous READDIR. ๐Ÿ”–

The cookieverf should be 0 (zero) when the cookie is 0 (zero), aka the first read. After that, it should be a cookieverf the server returned. And it must match the cookieverf from the READDIR where the cookie was acquired. If the server decides the cookieverf is no longer valid for the directory, it must return the error NFS4ERR_NOT_SAME. ๐Ÿ™ˆ

dircount is a hint for how many bytes of directory info the server should aim to send back. It counts the names and the cookies for those entries, measured as their XDR encoding, not however the server happens to store them natively. ๐Ÿงš

maxcount is the maximum number of bytes allowed for the result. That covers all the data inside READDIR4resok, XDR overhead included. The server is allowed to send back less. If it can't fit even one directory entry under maxcount, the client gets NFS4ERR_TOOSMALL. ๐Ÿ“

Finally, attr_request is the list of attributes the server should include for each directory entry it sends. โœจ

On a successful return, the server's response gives you a list of directory entries. Each one has the entry's name, a cookie for that entry, and the requested attributes. The eof flag is TRUE if there are no more entries in the directory. ๐Ÿฆ‹

The cookie is only meaningful to the server. It works as a "bookmark" for the directory entry, and the client uses it on later READDIRs to pick up where it left off. It's kind of like a READ offset, but the client should not interpret it as one. Ideally, the cookie value should not change if the directory gets modified, since clients may be caching these values. ๐ŸŽ€

Sometimes the server runs into an error while getting attributes for one directory entry. Instead of failing the whole READDIR, it can return the fattr4_rdattr_error attribute for that entry. That way, it tells the client what went wrong without tanking the entire operation, which is super helpful for what might be a transient failure. ๐Ÿซถ But the client has to request fattr4_rdattr_error for this to work. If the client doesn't ask for it, the server has no choice but to return failure for the entire READDIR operation. ๐Ÿ˜ญ

Some filesystems give the entries "." and ".." special meaning, and some don't. If the server supports these special entries, they should not be returned to the client as part of the READDIR response. Cookie values 0, 1, and 2 are reserved so that certain client environments can build a fully formed UNIX directory view. The UNIX client uses these values when it combines the server's response with its local representation of the directory. ๐ŸŒˆ

For READDIR arguments, cookie values of 1 and 2 should not be used. For READDIR results, cookie values of 0, 1, and 2 should not be returned. ๐Ÿšซ

On success, the current filehandle retains its value. ๐Ÿ’œ

IMPLEMENTATION

Servers can represent their directories super differently, and a client's programming interfaces may be tied to the local OS in ways that don't translate well into NFS. That's why dircount and maxcount exist: they let the client hand the server some guidelines. If the client is aggressive about attribute collection during a READDIR, the server knows to limit the encoded response. The dircount field is a hint for the number of entries, based solely on the names. Since it's only a hint, dircount may be zero. In that case, the server is free to ignore dircount and return directory information based on maxcount. ๐Ÿ‘‘

The server can use the cookieverf to help manage cookie values that may go stale. It should be a rare occurrence that a server can't continue reading a directory properly with the provided cookie/cookieverf pair. The server should make every effort to avoid this, since the client's application may not be able to handle that kind of failure properly. ๐Ÿ’…

The cookieverf also protects the client from using READDIR cookie values that may be stale. For example, if the filesystem has been migrated, the new server may or may not be able to use the same cookie values the previous server used. Because the client provides the cookieverf, the server can give the appropriate response. That stops the sketchy case where the server accepts a cookie, the underlying directory has changed, and the response makes no sense from the client's point of view of its previous READDIR. ๐Ÿง

Since some servers won't be returning "." and ".." entries like previous versions of the NFS protocol did, a client that requires those entries in READDIR responses must fabricate them itself. ๐Ÿ› ๏ธ

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADHANDLE
NFS4ERR_BAD_COOKIE
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOTDIR
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_TOOSMALL

SYNOPSIS

(cfh) -> linktext

ARGUMENT

/* CURRENT_FH: symlink */
void;

RESULT

struct READLINK4resok {
        linktext4       link;
};

union READLINK4res switch (nfsstat4 status) {
 case NFS4_OK:
        READLINK4resok resok4;
 default:
        void;
};

DESCRIPTION

READLINK reads the data associated with a symbolic link. The data is a UTF-8 string that is opaque to the server. Whether it was created by an NFS client or locally on the server, the data in a symbolic link isn't interpreted when it's created. It's just stored. ๐Ÿงšโ€โ™€๏ธ

On success, the current filehandle retains its value. ๐ŸŽ€

IMPLEMENTATION

A symbolic link is nominally a pointer to another file. The server doesn't necessarily interpret the data; it just stores it in the file. A client implementation can store a path name in a symbolic link that isn't meaningful to the server's operating system. READLINK returns the data to the client, which does the interpreting. If different implementations want to share access to symbolic links, they must agree on how to interpret the data in them. ๐Ÿค

READLINK is only allowed on objects of type NF4LNK. The server should return the error NFS4ERR_INVAL if the object is not of type NF4LNK. ๐Ÿ™…โ€โ™€๏ธ

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADHANDLE
NFS4ERR_DELAY
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_ISDIR
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOTSUPP
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.26. Operation 28: REMOVE - Remove Filesystem Object โœจ

SYNOPSIS ๐Ÿ”Ž

(cfh), filename -> change_info

ARGUMENT ๐ŸŽ€

struct REMOVE4args {
        /* CURRENT_FH: directory */
        component4       target;
};

RESULT ๐Ÿ’–

struct REMOVE4resok {
        change_info4    cinfo;
}

union REMOVE4res switch (nfsstat4 status) {
 case NFS4_OK:
         REMOVE4resok   resok4;
 default:
         void;
}

DESCRIPTION ๐Ÿ—‘๏ธ

okay so REMOVE deletes (ya know, nukes ๐Ÿ’€) the directory entry named by filename from the directory that the current filehandle points to. If that entry was the last reference to the filesystem object, the object may get destroyed. Yikes, but also iconic ๐Ÿ˜ญ

For the directory where the filename got removed, the server sends back change_info4 info in cinfo. The atomic field of that struct tells you whether the before and after change attributes were grabbed atomically with respect to the removal. Super important detail, bestie ๐Ÿงš

If target has a length of 0 (zero), or if target doesn't follow the UTF-8 definition, the server returns NFS4ERR_INVAL ๐Ÿ™ˆ

On success, the current filehandle keeps its value ๐Ÿฆ‹

IMPLEMENTATION ๐Ÿง 

NFS versions 2 and 3 used two separate ops: RMDIR for removing directories and REMOVE for non-directories. That meant clients could skip checking the file type when handed a non-directory delete system call (like unlink() in POSIX) to remove a directory, and the other way around (like a rmdir() on a non-directory), because they knew the server would check the file type ๐Ÿ‘‘

NFS version 4 REMOVE can delete any directory entry, no matter its file type. So if you're the implementor of an NFS version 4 client's entry points from the unlink() and rmdir() system calls, you should first check the file type against the types that system call is allowed to remove before issuing a REMOVE. Alternatively, you can produce a COMPOUND call that includes a LOOKUP/VERIFY sequence to verify the file type before a REMOVE operation in the same COMPOUND call. Two options, both giving main character energy โœจ

The concept of "last reference" is server specific. However, if the numlinks field in the previous attributes of the object had the value 1, the client should not rely on referring to the object via a filehandle. Likewise, the client should not rely on the resources (disk space, directory entry, and so on) formerly associated with the object becoming immediately available. So if a client needs to keep accessing a file after using REMOVE on it, the client should take steps to make sure that the file will still be accessible. The usual mechanism is to RENAME the file from its old name to a new hidden name ๐Ÿซถ

If the server finds that the file is still open when the REMOVE arrives:

  • The server SHOULD NOT delete the file's directory entry if the file was opened with OPEN4_SHARE_DENY_WRITE or OPEN4_SHARE_DENY_BOTH.
  • If the file was not opened with OPEN4_SHARE_DENY_WRITE or OPEN4_SHARE_DENY_BOTH, the server SHOULD delete the file's directory entry. However, until last CLOSE of the file, the server MAY continue to allow access to the file via its filehandle. ๐ŸŒธ

ERRORS ๐Ÿšจ

NFS4ERR_ACCESS
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADNAME
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_FHEXPIRED
NFS4ERR_FILE_OPEN
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NAMETOOLONG
NFS4ERR_NOENT
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOTDIR
NFS4ERR_NOTEMPTY
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.27. Operation 29: RENAME - Rename Directory Entry ๐Ÿฆ‹

SYNOPSIS ๐Ÿ”Ž

(sfh), oldname, (cfh), newname -> source_change_info,
target_change_info

ARGUMENT ๐ŸŽ€

struct RENAME4args {
        /* SAVED_FH: source directory */
        component4      oldname;
        /* CURRENT_FH: target directory */
        component4      newname;
};

RESULT ๐Ÿ’–

struct RENAME4resok {
        change_info4    source_cinfo;
        change_info4    target_cinfo;
};

union RENAME4res switch (nfsstat4 status) {
 case NFS4_OK:
         RENAME4resok   resok4;
 default:
         void;
};

DESCRIPTION ๐Ÿ“ฆ

The RENAME op renames the object named by oldname in the source directory (the one the saved filehandle points to, as set by the SAVEFH operation) to newname in the target directory (the current filehandle). The operation is required to be atomic to the client, which is the main vibe we want โœจ Source and target directories must live on the same filesystem on the server. On success, the current filehandle will still be the target directory ๐Ÿ’…

If the target directory already has an entry with the name newname, the source object has to be compatible with the target: either both are non-directories, or both are directories and the target must be empty. If they're compatible, the existing target gets removed before the rename happens. (Check the IMPLEMENTATION subsection of the section "Operation 28: REMOVE - Remove Filesystem Object" for what the client and server do whenever a target is removed ๐Ÿง ) If they're not compatible, or the target is a directory but not empty, the server returns NFS4ERR_EXIST ๐Ÿ˜ญ

If oldname and newname both refer to the same file (they might be hard links of each other), then RENAME should perform no action and return success. Lowkey simple ๐ŸŒˆ

For both directories involved in the RENAME, the server returns change_info4 info. The atomic field of the change_info4 struct tells you whether the before and after change attributes were obtained atomically with respect to the rename ๐Ÿงš

If oldname refers to a named attribute and the saved and current filehandles refer to different filesystem objects, the server returns NFS4ERR_XDEV, just as if the saved and current filehandles were directories on different filesystems ๐Ÿ™ˆ

If oldname or newname has a length of 0 (zero), or if either one doesn't obey the UTF-8 definition, the server returns NFS4ERR_INVAL ๐Ÿซถ

IMPLEMENTATION ๐Ÿง 

The RENAME operation must be atomic to the client. The statement "source and target directories must reside on the same filesystem on the server" means that the fsid fields in the attributes for the directories are the same. If they reside on different filesystems, the error NFS4ERR_XDEV is returned ๐Ÿ‘‘

Depending on the value of the fh_expire_type attribute for the object, the filehandle may or may not expire on a RENAME. However, server implementors are strongly encouraged to try to keep filehandles from expiring in this fashion. Pls do this, it's giving stability ๐Ÿ’–

On some servers, the file names "." and ".." are illegal as either oldname or newname, and using them results in NFS4ERR_BADNAME. On many servers, the case of oldname or newname being an alias for the source directory gets checked too. Those servers return NFS4ERR_INVAL in these cases ๐ŸŒธ

If either the source or target filehandle is not a directory, the server returns NFS4ERR_NOTDIR ๐Ÿ˜ญ

ERRORS ๐Ÿšจ

NFS4ERR_ACCESS
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADNAME
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_DQUOT
NFS4ERR_EXIST
NFS4ERR_FHEXPIRED
NFS4ERR_FILE_OPEN
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_MOVED
NFS4ERR_NAMETOOLONG
NFS4ERR_NOENT
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOSPC
NFS4ERR_NOTDIR
NFS4ERR_NOTEMPTY
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_WRONGSEC
NFS4ERR_XDEV

14.2.28. Operation 30: RENEW - Renew a Lease ๐Ÿ”

SYNOPSIS ๐Ÿ”Ž

clientid -> ()

ARGUMENT ๐ŸŽ€

struct RENEW4args {
        clientid4       clientid;
};

RESULT ๐Ÿ’–

struct RENEW4res {
        nfsstat4        status;
};

DESCRIPTION ๐Ÿ“ฆ

The RENEW op is how the client renews the leases it currently holds at a server. When the server processes a RENEW request, it renews all leases associated with the client. The associated leases are determined by the clientid provided via the SETCLIENTID operation ๐Ÿซถ

IMPLEMENTATION ๐Ÿง 

When the client holds delegations, it needs to use RENEW to detect when the server has determined that the callback path is down. Once the server has made that determination, only the RENEW operation will renew the lease on delegations. If the server determines the callback path is down, it returns NFS4ERR_CB_PATH_DOWN. Even though it returns NFS4ERR_CB_PATH_DOWN, the server MUST renew the lease on the record locks and share reservations that the client has established on the server. If, for some reason, the lock and share reservation lease cannot be renewed, then the server MUST return an error other than NFS4ERR_CB_PATH_DOWN, even if the callback path is also down.

Yikes, read that twice, bestie. Locks and share reservations MUST still get renewed even when the callback path is down ๐Ÿ™ˆ

The client that issues RENEW MUST choose the principal, RPC security flavor, and if applicable, GSS-API mechanism and service via one of the following algorithms:

  • The client uses the same principal, RPC security flavor -- and if the flavor was RPCSEC_GSS -- the same mechanism and service that was used when the client id was established via SETCLIENTID_CONFIRM.
  • The client uses any principal, RPC security flavor mechanism and service combination that currently has an OPEN file on the server. I.e., the same principal had a successful OPEN operation, the file is still open by that principal, and the flavor, mechanism, and service of RENEW match that of the previous OPEN. ๐Ÿ’…

The server MUST reject a RENEW that does not use one of the aforementioned algorithms, with the error NFS4ERR_ACCESS ๐Ÿ‘‘

ERRORS ๐Ÿšจ

NFS4ERR_ACCESS
NFS4ERR_ADMIN_REVOKED
NFS4ERR_BADXDR
NFS4ERR_CB_PATH_DOWN
NFS4ERR_EXPIRED
NFS4ERR_LEASE_MOVED
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE_CLIENTID

14.2.29. Operation 31: RESTOREFH - Restore Saved Filehandle ๐ŸŒˆ

SYNOPSIS ๐Ÿ”Ž

(sfh) -> (cfh)

ARGUMENT ๐ŸŽ€

/* SAVED_FH: */
void;

RESULT ๐Ÿ’–

struct RESTOREFH4res {
        /* CURRENT_FH: value of saved fh */
        nfsstat4        status;
};

DESCRIPTION ๐Ÿ“ฆ

Set the current filehandle to the value in the saved filehandle. If there is no saved filehandle, then return the error NFS4ERR_RESTOREFH ๐Ÿงš

IMPLEMENTATION ๐Ÿง 

Ops like OPEN and LOOKUP use the current filehandle to represent a directory and replace it with a new filehandle. If the previous filehandle was saved with a SAVEFH operator, the previous filehandle can be restored as the current filehandle. This is commonly used to get post-operation attributes for the directory, like this:

    PUTFH (directory filehandle)
    SAVEFH
    GETATTR attrbits     (pre-op dir attrs)
    CREATE optbits "foo" attrs
    GETATTR attrbits     (file attributes)
    RESTOREFH
    GETATTR attrbits     (post-op dir attrs)

ERRORS ๐Ÿšจ

NFS4ERR_BADHANDLE
NFS4ERR_FHEXPIRED
NFS4ERR_MOVED
NFS4ERR_RESOURCE
NFS4ERR_RESTOREFH
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_WRONGSEC

14.2.30. Operation 32: SAVEFH - Save Current Filehandle ๐Ÿ’พโœจ

SYNOPSIS

(cfh) -> (sfh)

ARGUMENT

/* CURRENT_FH: */
void;

RESULT

struct SAVEFH4res {
        /* SAVED_FH: value of current fh */
        nfsstat4        status;
};

DESCRIPTION

okay so SAVEFH is giving "bookmark this spot" ๐Ÿ”– It saves the current filehandle. If you already had a saved one, bestie, that old save is gone and no longer reachable ๐Ÿ˜ญ And you can bring the saved one back as the current filehandle later with the RESTOREFH operator, so the save isn't wasted ๐Ÿซถ

On success, the current filehandle keeps its value. Nothing changes for it, it's still the main character ๐Ÿ’…

IMPLEMENTATION

ERRORS

NFS4ERR_BADHANDLE
NFS4ERR_FHEXPIRED
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.31. Operation 33: SECINFO - Obtain Available Security ๐Ÿ”๐ŸŽ€

SYNOPSIS

(cfh), name -> { secinfo }

ARGUMENT

struct SECINFO4args {
        /* CURRENT_FH: directory */
        component4     name;
};

RESULT

enum rpc_gss_svc_t {/* From RFC 2203 */
        RPC_GSS_SVC_NONE        = 1,
        RPC_GSS_SVC_INTEGRITY   = 2,
        RPC_GSS_SVC_PRIVACY     = 3
};
struct rpcsec_gss_info {
        sec_oid4        oid;
        qop4            qop;
        rpc_gss_svc_t   service;
};

union secinfo4 switch (uint32_t flavor) {
 case RPCSEC_GSS:
         rpcsec_gss_info        flavor_info;
 default:
         void;
};

typedef secinfo4 SECINFO4resok<>;

union SECINFO4res switch (nfsstat4 status) {
 case NFS4_OK:
         SECINFO4resok resok4;
 default:
         void;
};

DESCRIPTION

so SECINFO is the client's "what security can I use here?" check ๐Ÿ‘€ It asks for the list of valid RPC authentication flavors for one directory filehandle plus one file name, as a pair ๐Ÿ’– It should use the same access rules as LOOKUP when it looks at that name. So if you couldn't LOOKUP the name, SECINFO has to act the same way and give back NFS4ERR_ACCESS. no sneaking past the bouncer ๐Ÿ™…โ€โ™€๏ธ

The result is an array of the security mechanisms the server has, in the server's preference order, with the most preferred one first ๐Ÿ† The client is free to pick whatever it wants and supports, or just go down the list and take the first one it supports ๐Ÿฆ‹ Each entry is a secinfo4 structure. The 'flavor' field will hold AUTH_NONE, AUTH_SYS (as defined in [RFC1831]), or RPCSEC_GSS (as defined in [RFC2203]).

For AUTH_NONE and AUTH_SYS, no extra security info comes back, so it's pretty chill ๐ŸŒธ For RPCSEC_GSS, you get a security triple: the mechanism object id (as defined in [RFC2743]), the quality of protection (as defined in [RFC2743]), and the service type (as defined in [RFC2203]). SECINFO can return several RPCSEC_GSS entries with different security triple values too, so yes, you can have multiple RPCSEC_GSS options at once, iconic ๐Ÿ‘‘

On success, the current filehandle keeps its value โœจ

If the name has a length of 0 (zero), or if the name doesn't follow the UTF-8 definition, the server returns NFS4ERR_INVAL. not it ๐Ÿ™ˆ

IMPLEMENTATION

okay so SECINFO is the move when the client gets NFS4ERR_WRONGSEC back from another NFS operation ๐Ÿšจ That error means the server's security policy isn't what the client is using right now. At that point the client should get a list of possible security flavors and pick whichever one fits its policies ๐Ÿ’…

The server's security policies decide when a client request gets NFS4ERR_WRONGSEC. The operations that can get this error are: LINK, LOOKUP, OPEN, PUTFH, PUTPUBFH, PUTROOTFH, RESTOREFH, RENAME, and indirectly READDIR. LINK and RENAME only get this error if the security used for the operation doesn't work for the saved filehandle. Except for READDIR, these are the moments where the client can make a filehandle into the "current filehandle" on the server. The filehandle either comes from the client (PUTFH, PUTPUBFH, PUTROOTFH) or gets made from a name-to-filehandle lookup (LOOKUP and OPEN). RESTOREFH is its own thing, since its filehandle came from an earlier SAVEFH ๐Ÿ”– Even if that filehandle already passed the server's security check before, the server checks it again on RESTOREFH to make sure the policy hasn't changed. no trust without re-checking ๐Ÿงš

If the client wants to deal with an NFS4ERR_WRONGSEC error, this is what happens:

  • For LOOKUP and OPEN, the client uses SECINFO with the same current filehandle and name that were in the original LOOKUP or OPEN, so it can list the available security triples.
  • For LINK, PUTFH, RENAME, and RESTOREFH, the client uses SECINFO and gives the parent directory filehandle plus the object name that matches the filehandle originally given by PUTFH or RESTOREFH, or by SAVEFH for LINK and RENAME.
  • For PUTROOTFH and PUTPUBFH, the client can't use SECINFO, because SECINFO needs a current filehandle and neither of these operations has one ๐Ÿ˜ญ So the client has to go through its own available security triples and try the PUTROOTFH or PUTPUBFH again. If the client and server don't share any MANDATORY security triples, the client SHOULD try others that support integrity. If that still doesn't work, the client can try AUTH_NONE, but since those forms have no integrity checks, that puts the client at risk ๐Ÿ™ƒ Even so, the server SHOULD let the client use whatever security form the client asks for and the server supports, because the risks are on the client to carry ๐Ÿ’…

The READDIR operation will not directly return NFS4ERR_WRONGSEC. Still, if the READDIR request asked for attributes, the READDIR request's security triple might not match a directory entry's. If that happens and the client asked for the rdattr_error attribute, the server returns NFS4ERR_WRONGSEC in rdattr_error for that entry ๐ŸŽ€

Sis, go read the "Security Considerations" section for the full scoop on which security flavors SECINFO should recommend. It's serious business ๐Ÿ”

ERRORS

NFS4ERR_ACCESS
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADNAME
NFS4ERR_BADXDR
NFS4ERR_FHEXPIRED
NFS4ERR_INVAL
NFS4ERR_MOVED
NFS4ERR_NAMETOOLONG
NFS4ERR_NOENT
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOTDIR
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE

14.2.32. Operation 34: SETATTR - Set Attributes โœจ

SYNOPSIS

(cfh), stateid, attrmask, attr_vals -> attrsset

ARGUMENT

struct SETATTR4args {
        /* CURRENT_FH: target object */
        stateid4        stateid;
        fattr4          obj_attributes;
};

RESULT

struct SETATTR4res {
        nfsstat4        status;
        bitmap4         attrsset;
};

DESCRIPTION

okay so SETATTR is the op that changes one or more attributes of a filesystem object ๐Ÿ’… The new attributes get listed in a bitmap, and their values follow in bit order, so it's giving neat and organized ๐ŸŽ€

the stateid argument is the file locking context ๐Ÿ”’ it's needed for SETATTR requests that set the size attribute. changing the size modifies the file's data, so it has the same locking requirements as a matching WRITE, no cap ๐Ÿ˜ค any SETATTR that sets size is incompatible with a share reservation that specifies DENY_WRITE ๐Ÿ™…โ€โ™€๏ธ the area between the old end-of-file and the new end-of-file counts as modified, just like it would if that area were the target of a WRITE. this matters for checking conflicts with record locks, in the cases where a server is doing mandatory record locking ๐Ÿฆ‹ a valid stateid should always be specified ๐Ÿ’œ when the file size attribute is not set, the special stateid made of all zero bits should be passed ๐Ÿงš

when the operation finishes, whether it works or fails, the server returns the attrsset bitmask to show what attributes (if any) were actually set ๐Ÿ“ the attrsset in the response is a subset of the bitmap4 that sits inside obj_attributes in the argument, so it can never claim more than you asked for โœจ

on success, the current filehandle keeps its value ๐Ÿ’–

IMPLEMENTATION

okay this part is spicy ๐ŸŒถ๏ธ if the request asks to set the owner attribute, the server should let the operation succeed when the current owner of the object matches the value in the request ๐Ÿซถ some servers might block setting owner unless the requester has the privilege to do it, which is valid too ๐Ÿ” but if the server is lenient in this one case of matching owner values, the client gets a simpler life when it creates an object and then does a SETATTR right after ๐Ÿฅน

the file size attribute is how you ask for a change in a file's size ๐Ÿ“ a value of 0 (zero) truncates the file ๐Ÿ’€ a value smaller than the current size throws away data from the new size to the end of the file ๐Ÿ—‘๏ธ and a size bigger than the current size adds logically zeroed data bytes onto the end ๐ŸŒˆ servers are free to do this with holes or with actual zero data bytes ๐Ÿ•ณ๏ธ clients should not make any assumptions about how a server implements this, beyond the fact that the bytes returned will be zeroed ๐Ÿ™ˆ servers must support extending the file size via SETATTR ๐Ÿ’ช

SETATTR is not guaranteed atomic ๐Ÿ˜ฌ a failed SETATTR may partially change a file's attributes, so don't be shocked if you see half a glow-up โš ๏ธ

changing the size of a file with SETATTR also indirectly changes the time_modify attribute โฐ a client must account for this, because size changes can result in data deletion ๐Ÿ—‘๏ธ

the attributes time_access_set and time_modify_set are write-only attributes. they're built as a switched union, which lets the client tell the server how to set the time values ๐ŸŽ€ if the switched union specifies SET_TO_CLIENT_TIME4, the client has provided an nfstime4 to use for the operation ๐Ÿ•ฐ๏ธ if the switch union doesn't specify SET_TO_CLIENT_TIME4, the server uses its own current time for the SETATTR operation ๐Ÿ–ฅ๏ธ

if server and client times differ, programs that compare client time to file times can break ๐Ÿ’” a time maintenance protocol should be used to limit client/server time skew โณ

using a COMPOUND that has a VERIFY operation which specifies only the change attribute, immediately followed by a SETATTR, gives the client a way to emulate the SETATTR guard mechanism from NFS version 3 ๐Ÿงฉ the whole point of the guard is to avoid changing file attributes based on stale information, so any delay between checking the guard condition and setting the attributes can compromise that, and the same delay would hurt the NFS version 4 emulation too ๐Ÿ˜ญ therefore, NFS version 4 servers should take care to avoid such delays, as much as possible, when executing this kind of request ๐Ÿซก

if the server doesn't support an attribute the client asked for, the server should return NFS4ERR_ATTRNOTSUPP ๐Ÿ™…โ€โ™€๏ธ

a mask of the attributes that were actually set gets returned by SETATTR in all cases ๐ŸŒธ that mask must not include attribute bits the client didn't ask to set, and it must equal the mask of requested attributes only if the SETATTR completes without error ๐Ÿ‘‘

ERRORS

NFS4ERR_ACCESS
NFS4ERR_ADMIN_REVOKED
NFS4ERR_ATTRNOTSUPP
NFS4ERR_BADCHAR
NFS4ERR_BADHANDLE
NFS4ERR_BADOWNER
NFS4ERR_BAD_STATEID
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_DQUOT
NFS4ERR_EXPIRED
NFS4ERR_FBIG
NFS4ERR_FHEXPIRED
NFS4ERR_GRACE
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_ISDIR
NFS4ERR_LOCKED
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOSPC
NFS4ERR_OLD_STATEID
NFS4ERR_OPENMODE
NFS4ERR_PERM
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_STALE_STATEID

14.2.33. Operation 35: SETCLIENTID - Negotiate Clientid โœจ

SYNOPSIS

client, callback, callback_ident -> clientid, setclientid_confirm

ARGUMENT

struct SETCLIENTID4args {
        nfs_client_id4  client;
        cb_client4      callback;
        uint32_t        callback_ident;
};

RESULT

struct SETCLIENTID4resok {
        clientid4       clientid;
        verifier4       setclientid_confirm;
};

union SETCLIENTID4res switch (nfsstat4 status) {
 case NFS4_OK:
         SETCLIENTID4resok      resok4;
 case NFS4ERR_CLID_INUSE:
         clientaddr4    client_using;
 default:
         void;
};

DESCRIPTION

Okay so SETCLIENTID is how the client basically tells the server, "hey, this is the client identifier I want to use, here's my callback and callback_ident, and I'm about to start making lock, share reservation, and delegation state with you" ๐Ÿ’…. If everything checks out, the server hands back a shorthand clientid. That clientid only gets used in later file locking and file open requests once it's confirmed, and the confirmation has to happen through the SETCLIENTID_CONFIRM operation, which sends the clientid and setclientid_confirm values back to the server as verifiers ๐ŸŽ€.

Now, why are there two verifiers? Bestie, it's because SETCLIENTID and SETCLIENTID_CONFIRM can be used to change the callback and callback_ident info without changing the shorthand clientid. So in that situation, setclientid_confirm is effectively the only verifier. Iconic, right? ๐Ÿ™ˆ

The callback info is what the server uses if the client ever gets an open delegation down the road, so the client must correctly reflect the program and port numbers for the callback program at the time SETCLIENTID is used. No messing this up, please ๐Ÿซถ.

The server uses callback_ident on the callback. The client can lean on it so it doesn't need more than one callback RPC program number, and the server still knows which server is calling back. Genius energy ๐Ÿง โœจ.

IMPLEMENTATION

Let's get into how to actually implement this, because it's giving complicated but it's really not ๐Ÿ’€. First, some notation:

  • x is the value of the client.id subfield of the SETCLIENTID4args structure.
  • v is the value of the client.verifier subfield of the SETCLIENTID4args structure.
  • c is the value of the clientid field returned in the SETCLIENTID4resok structure.
  • k represents the value combination of the callback and callback_ident fields of the SETCLIENTID4args structure.
  • s is the setclientid_confirm value returned in the SETCLIENTID4resok structure.

So the quintuple for a client record is { v, x, c, k, s }. A client record is confirmed if there has been a SETCLIENTID_CONFIRM operation to confirm it. Otherwise it's unconfirmed, and an unconfirmed record gets established by a SETCLIENTID call ๐Ÿฆ‹.

Since SETCLIENTID is a non-idempotent operation, let's assume the server is implementing the duplicate request cache (DRC) ๐Ÿ“.

When the server gets a SETCLIENTID { v, x, k } request, it processes it like this:

  • It first looks up the request in the DRC. If there's a hit, it returns the result cached in the DRC. The server does NOT remove client state (locks, shares, delegations), and it does NOT modify any recorded callback and callback_ident information for client { x }.

For any DRC miss, the server takes the client id string x and searches for client records for x that it may have recorded from previous SETCLIENTID calls. For any confirmed record with the same id string x, if the recorded principal does not match that of the SETCLIENTID call, then the server returns a NFS4ERR_CLID_INUSE error ๐Ÿšซ.

For brevity, the rest of this description assumes that there was a DRC miss, and that wherever the server has previously recorded a confirmed record for client x, the principal check above has passed.

  • The server checks whether it has recorded a confirmed record for { v, x, c, l, s }, where l may or may not equal k. If so, and since the id verifier v of the request matches the one that's confirmed and recorded, the server treats this as a probable callback information update. It records an unconfirmed { v, x, c, k, t } and leaves the confirmed { v, x, c, l, s } in place, such that t != s. It doesn't matter whether k equals l or not. Any pre-existing unconfirmed { v, x, c, *, * } is removed.

The server returns { c, t }. It's returning the old clientid4 value c because the client apparently only wants to update callback value k to value l. It's possible this request is one from the Byzantine router that has stale callback information, but that's not a problem. The callback information update is only confirmed if it's followed up by a SETCLIENTID_CONFIRM { c, t }.

The server awaits confirmation of k via SETCLIENTID_CONFIRM { c, t }.

The server does NOT remove client (lock/share/delegation) state for x.

  • The server has previously recorded a confirmed { u, x, c, l, s } record such that v != u, where l may or may not equal k, and it has not recorded any unconfirmed { *, x, *, *, * } record for x. The server records an unconfirmed { v, x, d, k, t } (where d != c and t != s).

The server returns { d, t }.

The server awaits confirmation of { d, k } via SETCLIENTID_CONFIRM { d, t }.

The server does NOT remove client (lock/share/delegation) state for x.

  • The server has previously recorded a confirmed { u, x, c, l, s } record such that v != u, where l may or may not equal k, and it recorded an unconfirmed { w, x, d, m, t } record such that c != d and t != s. Here m may or may not equal k, m may or may not equal l, and k may or may not equal l. Whether w == v or w != v makes no difference. The server simply removes the unconfirmed { w, x, d, m, t } record and replaces it with an unconfirmed { v, x, e, k, r } record, such that e != d, e != c, r != t, and r != s.

The server returns { e, r }.

The server awaits confirmation of { e, k } via SETCLIENTID_CONFIRM { e, r }.

The server does NOT remove client (lock/share/delegation) state for x.

  • The server has no confirmed { *, x, *, *, * } for x. It may or may not have recorded an unconfirmed { u, x, c, l, s }, where l may or may not equal k, and u may or may not equal v. Any unconfirmed record { u, x, c, l, * }, regardless of whether u == v or l == k, is replaced with an unconfirmed record { v, x, d, k, t } where d != c and t != s.

The server returns { d, t }.

The server awaits confirmation of { d, k } via SETCLIENTID_CONFIRM { d, t }. The server does NOT remove client (lock/share/delegation) state for x.

The server generates the clientid and setclientid_confirm values, and it must take care to ensure that these values are extremely unlikely to ever be regenerated. Lowkey the most important part, so please don't skip it ๐Ÿ”๐ŸŒธ.

ERRORS

  • NFS4ERR_BADXDR
  • NFS4ERR_CLID_INUSE
  • NFS4ERR_INVAL
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT

14.2.34. Operation 36: SETCLIENTID_CONFIRM - Confirm Clientid โœจ

SYNOPSIS

clientid, verifier -> -

ARGUMENT

struct SETCLIENTID_CONFIRM4args {
        clientid4       clientid;
        verifier4       setclientid_confirm;
};

RESULT

struct SETCLIENTID_CONFIRM4res {
        nfsstat4        status;
};

DESCRIPTION

okay so this is the op where the client confirms what it got back from an earlier SETCLIENTID call ๐Ÿ’…๐ŸŽ€. The client hands over the shorthand clientid that the server gave it in the SETCLIENTID response. The server answers with a plain success or failure, no drama ๐Ÿซถ.

IMPLEMENTATION

The client must use the SETCLIENTID_CONFIRM operation to confirm the following two distinct cases:

  • The client's use of a new shorthand client identifier (as returned from the server in the response to SETCLIENTID), a new callback value (as specified in the arguments to SETCLIENTID) and a new callback_ident (as specified in the arguments to SETCLIENTID) value. The client's use of SETCLIENTID_CONFIRM in this case also confirms the removal of any of the client's previous relevant leased state. Relevant leased client state includes record locks, share reservations, and where the server does not support the CLAIM_DELEGATE_PREV claim type, delegations. If the server supports CLAIM_DELEGATE_PREV, then SETCLIENTID_CONFIRM MUST NOT remove delegations for this client; relevant leased client state would then just include record locks and share reservations. ๐Ÿ”ฅ
  • The client's re-use of an old, previously confirmed, shorthand client identifier, a new callback value, and a new callback_ident value. The client's use of SETCLIENTID_CONFIRM in this case MUST NOT result in the removal of any previous leased state (locks, share reservations, and delegations) ๐Ÿ™ˆ

We use the same notation and definitions for v, x, c, k, s, and unconfirmed and confirmed client records as introduced in the description of the SETCLIENTID operation. The arguments to SETCLIENTID_CONFIRM are indicated by the notation { c, s }, where c is a value of type clientid4, and s is a value of type verifier4 corresponding to the setclientid_confirm field.

As with SETCLIENTID, SETCLIENTID_CONFIRM is a non-idempotent operation, and we assume that the server is implementing the duplicate request cache (DRC). ๐Ÿงš

When the server gets a SETCLIENTID_CONFIRM { c, s } request, it processes it in the following manner.

  • It first looks up the request in the DRC. If there is a hit, it returns the result cached in the DRC. The server does not remove any relevant leased client state nor does it modify any recorded callback and callback_ident information for client { x } as represented by the shorthand value c.

For a DRC miss, the server checks for client records that match the shorthand value c. The processing cases are as follows:

  • The server has recorded an unconfirmed { v, x, c, k, s } record and a confirmed { v, x, c, l, t } record, such that s != t. If the principals of the records do not match that of the SETCLIENTID_CONFIRM, the server returns NFS4ERR_CLID_INUSE, and no relevant leased client state is removed and no recorded callback and callback_ident information for client { x } is changed. Otherwise, the confirmed { v, x, c, l, t } record is removed and the unconfirmed { v, x, c, k, s } is marked as confirmed, thereby modifying recorded and confirmed callback and callback_ident information for client { x }.

The server does not remove any relevant leased client state.

The server returns NFS4_OK.

  • The server has not recorded an unconfirmed { v, x, c, *, * } and has recorded a confirmed { v, x, c, *, s }. If the principals of the record and of SETCLIENTID_CONFIRM do not match, the server returns NFS4ERR_CLID_INUSE without removing any relevant leased client state and without changing recorded callback and callback_ident values for client { x }.

If the principals match, then what has likely happened is that the client never got the response from the SETCLIENTID_CONFIRM, and the DRC entry has been purged. Whatever the scenario, since the principals match, as well as { c, s } matching a confirmed record, the server leaves client x's relevant leased client state intact, leaves its callback and callback_ident values unmodified, and returns NFS4_OK.

  • The server has not recorded a confirmed { *, *, c, *, * }, and has recorded an unconfirmed { *, x, c, k, s }. Even if this is a retry from client, nonetheless the client's first SETCLIENTID_CONFIRM attempt was not received by the server. Retry or not, the server doesn't know, but it processes it as if were a first try. If the principal of the unconfirmed { *, x, c, k, s } record mismatches that of the SETCLIENTID_CONFIRM request the server returns NFS4ERR_CLID_INUSE without removing any relevant leased client state.

Otherwise, the server records a confirmed { *, x, c, k, s }. If there is also a confirmed { *, x, d, *, t }, the server MUST remove the client x's relevant leased client state, and overwrite the callback state with k. The confirmed record { *, x, d, *, t } is removed.

Server returns NFS4_OK.

  • The server has no record of a confirmed or unconfirmed { *, *, c, *, s }. The server returns NFS4ERR_STALE_CLIENTID. The server does not remove any relevant leased client state, nor does it modify any recorded callback and callback_ident information for any client.

The server needs to cache unconfirmed { v, x, c, k, s } client records and await for some time their confirmation. As should be clear from the record processing discussions for SETCLIENTID and SETCLIENTID_CONFIRM, there are cases where the server does not deterministically remove unconfirmed client records. To avoid running out of resources, the server is not required to hold unconfirmed records indefinitely. One strategy the server might use is to set a limit on how many unconfirmed client records it will maintain, and then when the limit would be exceeded, remove the oldest record. Another strategy might be to remove an unconfirmed record when some amount of time has elapsed. The choice of the amount of time is fairly arbitrary but it is surely no higher than the server's lease time period. Consider that leases need to be renewed before the lease time expires via an operation from the client. If the client cannot issue a SETCLIENTID_CONFIRM after a SETCLIENTID before a period of time equal to that of a lease expires, then the client is unlikely to be able maintain state on the server during steady state operation. โณ

If the client does send a SETCLIENTID_CONFIRM for an unconfirmed record that the server has already deleted, the client will get NFS4ERR_STALE_CLIENTID back. If so, the client should then start over, and send SETCLIENTID to reestablish an unconfirmed client record and get back an unconfirmed clientid and setclientid_confirm verifier. The client should then send the SETCLIENTID_CONFIRM to confirm the clientid. ๐Ÿ”„

SETCLIENTID_CONFIRM does not establish or renew a lease. However, if SETCLIENTID_CONFIRM removes relevant leased client state, and that state does not include existing delegations, the server MUST allow the client a period of time no less than the value of lease_time attribute, to reclaim, (via the CLAIM_DELEGATE_PREV claim type of the OPEN operation) its delegations before removing unreclaimed delegations. ๐Ÿฆ‹

ERRORS

  • NFS4ERR_BADXDR
  • NFS4ERR_CLID_INUSE
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT
  • NFS4ERR_STALE_CLIENTID

14.2.35. Operation 37: VERIFY - Verify Same Attributes โœจ

SYNOPSIS

(cfh), fattr -> -

ARGUMENT

struct VERIFY4args {
        /* CURRENT_FH: object */
        fattr4          obj_attributes;
};

RESULT

struct VERIFY4res {
        nfsstat4        status;
};

DESCRIPTION

The VERIFY operation is used to verify that attributes have a value assumed by the client before proceeding with following operations in the compound request. If any of the attributes do not match then the error NFS4ERR_NOT_SAME must be returned. The current filehandle retains its value after successful completion of the operation. ๐ŸŒธ

IMPLEMENTATION

One possible use of the VERIFY operation is the following compound sequence. With this the client is attempting to verify that the file being removed will match what the client expects to be removed. This sequence can help prevent the unintended deletion of a file. ๐Ÿ›ก๏ธ

      PUTFH (directory filehandle)
      LOOKUP (file name)
      VERIFY (filehandle == fh)
      PUTFH (directory filehandle)
      REMOVE (file name)

This sequence does not prevent a second client from removing and creating a new file in the middle of this sequence but it does help avoid the unintended result. ๐Ÿ’€

In the case that a recommended attribute is specified in the VERIFY operation and the server does not support that attribute for the filesystem object, the error NFS4ERR_ATTRNOTSUPP is returned to the client.

When the attribute rdattr_error or any write-only attribute (e.g., time_modify_set) is specified, the error NFS4ERR_INVAL is returned to the client.

ERRORS

  • NFS4ERR_ACCESS
  • NFS4ERR_ATTRNOTSUPP
  • NFS4ERR_BADCHAR
  • NFS4ERR_BADHANDLE
  • NFS4ERR_BADXDR
  • NFS4ERR_DELAY
  • NFS4ERR_FHEXPIRED
  • NFS4ERR_INVAL
  • NFS4ERR_MOVED
  • NFS4ERR_NOFILEHANDLE
  • NFS4ERR_NOT_SAME
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT
  • NFS4ERR_STALE

14.2.36. Operation 38: WRITE - Write to File โœ๏ธ

SYNOPSIS

(cfh), stateid, offset, stable, data -> count, committed, writeverf

ARGUMENT

enum stable_how4 {
        UNSTABLE4       = 0,
        DATA_SYNC4      = 1,
        FILE_SYNC4      = 2
};

struct WRITE4args {
        /* CURRENT_FH: file */
        stateid4        stateid;
        offset4         offset;
        stable_how4     stable;
        opaque          data<>;
};

RESULT

struct WRITE4resok {
        count4          count;
        stable_how4     committed;
        verifier4       writeverf;
};

union WRITE4res switch (nfsstat4 status) {
 case NFS4_OK:
         WRITE4resok    resok4;
 default:
         void;
};

DESCRIPTION

Okay so WRITE is the op that puts data into a regular file! ๐Ÿ’– The file it writes to is whatever the current filehandle points at. The offset tells the server where the data should go, and an offset of 0 (zero) means "start at the very beginning of the file" ๐ŸŒธ. The count is encoded inside the opaque data parameter, and it's the number of bytes that are supposed to get written. If the count is 0 (zero), the WRITE will still succeed and return a count of 0 (zero), as long as the permissions check passes ๐Ÿ™ˆ. Also, the server is allowed to write fewer bytes than the client asked for, so don't be shook if that happens โœจ.

Part of the write request is deciding how the write gets done, and that's what the stable parameter is for ๐ŸŽ€. If stable is FILE_SYNC4, the server must commit the data written plus all filesystem metadata to stable storage before it returns results. This lines up with NFS version 2 protocol semantics, and anything else is a protocol violation ๐Ÿšจ. If stable is DATA_SYNC4, the server must commit all of the data to stable storage, plus enough of the metadata to get the data back, before it returns. The server implementor can totally do DATA_SYNC4 the same way as FILE_SYNC4, but there might be a performance drop ๐Ÿข. If stable is UNSTABLE4, the server is free to commit any part of the data and metadata to stable storage, including all of it or none of it, before it replies to the client ๐Ÿฆ‹. There's no guarantee about whether or when uncommitted data will ever get committed. The only promises the server makes are that it won't destroy any data without changing the value of verf, and that it won't commit the data and metadata at a level lower than what the client asked for ๐Ÿ’….

The stateid value in a WRITE request is a value that came back from an earlier record lock or share reservation request ๐Ÿ”. The server uses it to check that the matching share reservation and any record locks are still valid, and it uses it to refresh the client's lease timeouts โฐ.

When it works out, these results come back. The count result is the number of bytes of data written to the file. The server might write fewer bytes than requested, and if it does, the actual number of bytes written starting at the location offset is what gets returned ๐Ÿ’œ.

The server also tells you how committed the data and metadata are through committed ๐Ÿงš. If the server committed all data and metadata to stable storage, committed should be set to FILE_SYNC4. If the level of commitment was at least as strong as DATA_SYNC4, then committed should be set to DATA_SYNC4. Otherwise, committed must be returned as UNSTABLE4 ๐Ÿ‘‘. If stable was FILE4_SYNC, then committed must also be FILE_SYNC4, and anything else is a protocol violation. If stable was DATA_SYNC4, then committed may be FILE_SYNC4 or DATA_SYNC4, and anything else is a protocol violation. If stable was UNSTABLE4, then committed may be FILE_SYNC4, DATA_SYNC4, or UNSTABLE4 โญ.

The last part of the result is the write verifier ๐ŸŒˆ. The write verifier is a cookie the client can use to figure out whether the server has changed instance (boot) state between a WRITE call and a later WRITE or COMMIT call. This cookie must stay consistent during a single instance of the NFS version 4 protocol service, and it must be unique between instances of the NFS version 4 protocol server, which is where uncommitted data can get lost ๐Ÿ˜ญ.

If a client writes data with stable set to UNSTABLE4 and the reply comes back with a committed of DATA_SYNC4 or UNSTABLE4, the client will follow up some time later with a COMMIT operation. That COMMIT is what synchronizes the outstanding asynchronous data and metadata with the server's stable storage, barring client error ๐Ÿซถ. It's possible that because of a client crash or some other error, the server never gets that subsequent COMMIT ๐Ÿ’€.

For a WRITE with a stateid value of all bits 0 (zero), the server MAY allow the WRITE to be serviced, subject to mandatory file locks or the current share deny modes for the file ๐Ÿ”“. For a WRITE with a stateid value of all bits 1 (one), the server MUST NOT allow the WRITE operation to bypass locking checks at the server, and those are treated exactly the same as if a stateid of all bits 0 (zero) were used ๐Ÿ›ก๏ธ.

On success, the current filehandle keeps its value ๐ŸŒธ.

IMPLEMENTATION

Okay this section is where the real drama happens ๐ŸŽญ. It's possible for the server to write fewer bytes than the client requested. When that happens, the server should not return an error unless no data was written at all. If the server writes less than the number of bytes specified, the client should issue another WRITE to write the rest of the data ๐Ÿ’Œ.

It's assumed that writing data to a file will cause the time_modified of the file to be updated ๐Ÿ•ฐ๏ธ. However, time_modified should not be changed unless the contents of the file actually change. So a WRITE request with count set to 0 (zero) should not cause time_modified to be updated ๐Ÿ™…โ€โ™€๏ธ.

Stable storage has historically been a point of contention ๐ŸฅŠ. The following expected properties may help resolve design issues in the implementation. Stable storage is persistent storage that survives:

  1. Repeated power failures.
  2. Hardware failures (of any board, power supply, etc.).
  3. Repeated software crashes, including reboot cycle.

This definition does not address failure of the stable storage module itself ๐Ÿคทโ€โ™€๏ธ.

The verifier exists so a client can detect different instances of an NFS version 4 protocol server, which is how it can tell that cached, uncommitted data might have been lost ๐Ÿ”. In the most likely case, the verifier lets the client detect server reboots. This information is needed so the client can safely figure out whether the server could have lost cached data. If the server fails unexpectedly and the client has uncommitted data from earlier WRITE requests (done with stable set to UNSTABLE4, and where the result committed came back as UNSTABLE4 too), it may not have flushed that cached data to stable storage ๐Ÿ˜ฑ. The burden of recovery is on the client, and the client will need to retransmit the data to the server ๐Ÿ’ช.

A suggested verifier would be the time the server was booted, or the time the server was last started (if restarting the server without a reboot results in lost buffers) โณ.

The committed field in the results lets the client do more effective caching ๐Ÿง. If the server is committing all WRITE requests to stable storage, it should return committed set to FILE_SYNC4, regardless of the value of the stable field in the arguments. A server that uses an NVRAM accelerator may choose to implement this policy โšก. The client can use this to make its cache more effective by discarding cached data that has already been committed on the server โœจ.

Some implementations may return NFS4ERR_NOSPC instead of NFS4ERR_DQUOT when a user's quota is exceeded ๐Ÿ™ƒ. If the current filehandle is a directory, the server will return NFS4ERR_ISDIR. If the current filehandle is not a regular file or a directory, the server will return NFS4ERR_INVAL ๐Ÿง.

If mandatory file locking is on for the file, and the matching record of the data to be written is read or write locked by an owner that isn't associated with the stateid, the server will return NFS4ERR_LOCKED ๐Ÿ”’. If that happens, the client must check whether the owner corresponding to the stateid used with the WRITE operation has a conflicting read lock that overlaps the region that was to be written. If the stateid's owner has no conflicting read lock, then the client should try to get the appropriate write record lock via the LOCK operation before trying the WRITE again. When the WRITE completes, the client should release the record lock via LOCKU ๐ŸŽ€.

If the stateid's owner had a conflicting read lock, the client has no choice but to return an error to the application that attempted the WRITE. ๐Ÿ˜ค The reason is that since the owner had a read lock, the server either tried to temporarily upgrade that read lock to a write lock, or the server has no upgrade capability at all. If the server tried to upgrade the read lock and failed, it's pointless for the client to retry the upgrade via the LOCK operation, because another client might also be trying to upgrade. If two clients are blocked trying to upgrade the same lock, the clients deadlock ๐Ÿ’€. If the server has no upgrade capability, then trying a LOCK operation to upgrade is pointless too ๐Ÿ™…โ€โ™€๏ธ.

ERRORS

NFS4ERR_ACCESS
NFS4ERR_ADMIN_REVOKED
NFS4ERR_BADHANDLE
NFS4ERR_BAD_STATEID
NFS4ERR_BADXDR
NFS4ERR_DELAY
NFS4ERR_DQUOT
NFS4ERR_EXPIRED
NFS4ERR_FBIG
NFS4ERR_FHEXPIRED
NFS4ERR_GRACE
NFS4ERR_INVAL
NFS4ERR_IO
NFS4ERR_ISDIR
NFS4ERR_LEASE_MOVED
NFS4ERR_LOCKED
NFS4ERR_MOVED
NFS4ERR_NOFILEHANDLE
NFS4ERR_NOSPC
NFS4ERR_NXIO
NFS4ERR_OLD_STATEID
NFS4ERR_OPENMODE
NFS4ERR_RESOURCE
NFS4ERR_ROFS
NFS4ERR_SERVERFAULT
NFS4ERR_STALE
NFS4ERR_STALE_STATEID

14.2.37. Operation 39: RELEASE_LOCKOWNER - Release Lockowner State ๐Ÿงน

SYNOPSIS

lockowner -> ()

ARGUMENT

struct RELEASE_LOCKOWNER4args {
        lock_owner4     lock_owner;
};

RESULT

struct RELEASE_LOCKOWNER4res {
        nfsstat4        status;
};

DESCRIPTION

This operation is how the client tells the server that the lock_owner isn't being used anymore ๐ŸŒฟ. That lets the server release any cached state tied to that lock_owner. If file locks associated with the lock_owner are still held at the server, the error NFS4ERR_LOCKS_HELD gets returned and no further action is taken ๐Ÿšซ.

IMPLEMENTATION

The client can use this operation to shrink the amount of server state being held ๐Ÿง . Depending on how applications behave on the client, it might matter that the client uses this operation, because the server has certain obligations about holding a reference to a lock_owner for as long as the associated file is open ๐Ÿ“‚. So if the client knows for sure that the lock_owner won't be used anymore under the context of the associated open_owner4, it should use RELEASE_LOCKOWNER ๐ŸŽ‰.

ERRORS

NFS4ERR_ADMIN_REVOKED
NFS4ERR_BADXDR
NFS4ERR_EXPIRED
NFS4ERR_LEASE_MOVED
NFS4ERR_LOCKS_HELD
NFS4ERR_RESOURCE
NFS4ERR_SERVERFAULT
NFS4ERR_STALE_CLIENTID

14.2.38. Operation 10044: ILLEGAL - Illegal operation โœจ

SYNOPSIS

<null> -> ()

ARGUMENT

void;

RESULT

struct ILLEGAL4res {
        nfsstat4        status;
};

DESCRIPTION

Okay so ILLEGAL is basically a placeholder ๐ŸŽ€ It's there so the server has something to encode as a result when the client sneaks an unsupported operation code into a COMPOUND. Check the COMPOUND procedure description for the full tea.

The status field of ILLEGAL4res MUST be set to NFS4ERR_OP_ILLEGAL.

IMPLEMENTATION

Real talk, a client will probably never send OP_ILLEGAL ๐Ÿ™ˆ But if it does, the response is ILLEGAL4res, same as with any other invalid operation code. One thing to note: if the server gets an illegal code that isn't OP_ILLEGAL, and it checks for legal codes during the XDR decode phase, then ILLEGAL4res won't be returned.

ERRORS

  • NFS4ERR_OP_ILLEGAL

15. NFS version 4 Callback Procedures ๐Ÿฆ‹

The procedures used for callbacks are defined in the following sections ๐Ÿงš In the interest of clarity, "client" and "server" mean NFS clients and servers, even though for an individual callback RPC, those terms are basically flipped. Bestie, keep that in mind as you read. ๐Ÿ’œ

15.1. Procedure 0: CB_NULL - No Operation ๐ŸŒธ

SYNOPSIS

<null>

ARGUMENT

void;

RESULT

void;

DESCRIPTION

It's a standard NULL procedure: void argument, void response. It has no real functionality, but the server uses CB_NULL to confirm there's a working path for RPCs from server to client. Iconic check-in energy โœจ

ERRORS

None.

15.2. Procedure 1: CB_COMPOUND - Compound Operations ๐Ÿ‘‘

SYNOPSIS

compoundargs -> compoundres

ARGUMENT

enum nfs_cb_opnum4 {
        OP_CB_GETATTR           = 3,
        OP_CB_RECALL            = 4,
        OP_CB_ILLEGAL           = 10044
};

union nfs_cb_argop4 switch (unsigned argop) {
 case OP_CB_GETATTR:    CB_GETATTR4args opcbgetattr;
 case OP_CB_RECALL:     CB_RECALL4args  opcbrecall;
 case OP_CB_ILLEGAL:    void            opcbillegal;
};

struct CB_COMPOUND4args {
        utf8str_cs      tag;
        uint32_t        minorversion;
        uint32_t        callback_ident;
        nfs_cb_argop4   argarray<>;
};

RESULT

union nfs_cb_resop4 switch (unsigned resop){
 case OP_CB_GETATTR:    CB_GETATTR4res  opcbgetattr;
 case OP_CB_RECALL:     CB_RECALL4res   opcbrecall;
};

struct CB_COMPOUND4res {
        nfsstat4 status;
        utf8str_cs      tag;
        nfs_cb_resop4   resarray<>;
};

DESCRIPTION

CB_COMPOUND is the wrapper that combines one or more callback procedures into a single RPC request. The main callback RPC program has two main procedures, CB_NULL and CB_COMPOUND, and every other operation rides inside CB_COMPOUND. ๐ŸŽ€

If the client doesn't have the resources to run some or all of the operations in the CB_COMPOUND sequence, the error NFS4ERR_RESOURCE gets returned for the operation where the resource exhaustion happened. This assumes that all the operations before it were evaluated successfully.

The CB_COMPOUND results include a 'status' field. This status must be equivalent to the status of the last operation that was executed within CB_COMPOUND. So if an operation hit an error, 'status' carries that same error value. ๐Ÿ’…

For the definition of the "tag" field, see the section "Procedure 1: COMPOUND - Compound Operations".

The value of callback_ident is supplied by the client during SETCLIENTID. The server must use the client-supplied callback_ident during CB_COMPOUND so the client can properly identify the server. ๐Ÿซถ

Illegal operation codes are handled the same way they are for the COMPOUND procedure.

IMPLEMENTATION

Here's the vibe check โœจ CB_COMPOUND combines individual operations into a single RPC request. The client works through each operation in turn. If an operation runs on the client and its status is NFS4_OK, the next operation in CB_COMPOUND gets executed. The client keeps going until there are no operations left or one of them returns a status other than NFS4_OK. ๐Ÿ”ฅ

ERRORS

  • NFS4ERR_BADHANDLE
  • NFS4ERR_BAD_STATEID
  • NFS4ERR_BADXDR
  • NFS4ERR_OP_ILLEGAL
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT

15.2.1. Operation 3: CB_GETATTR - Get Attributes ๐Ÿงš

SYNOPSIS

fh, attr_request -> attrmask, attr_vals

ARGUMENT

struct CB_GETATTR4args {
        nfs_fh4 fh;
        bitmap4 attr_request;
};

RESULT

struct CB_GETATTR4resok {
        fattr4  obj_attributes;
};

union CB_GETATTR4res switch (nfsstat4 status) {
 case NFS4_OK:
         CB_GETATTR4resok       resok4;
 default:
         void;
};

DESCRIPTION

CB_GETATTR is how the server gets the current, modified state of a file that has been write delegated ๐Ÿ’– Only the attributes size and change are guaranteed to be serviced by the client. For a full rundown on how the client and server work together with CB_GETATTR, check the section "Handling of CB_GETATTR".

If the filehandle isn't one the client holds a write open delegation for, you get an NFS4ERR_BADHANDLE error. Oof. ๐Ÿ˜ญ

IMPLEMENTATION

The client returns attrmask bits and their attribute values only for the change attribute, plus the attributes it may change (time_modify and size). ๐Ÿ“

ERRORS

  • NFS4ERR_BADHANDLE
  • NFS4ERR_BADXDR
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT

15.2.2. Operation 4: CB_RECALL - Recall an Open Delegation ๐Ÿ”ฅ

SYNOPSIS

stateid, truncate, fh -> ()

ARGUMENT

struct CB_RECALL4args {
        stateid4        stateid;
        bool            truncate;
        nfs_fh4         fh;
};

RESULT

struct CB_RECALL4res {
        nfsstat4        status;
};

DESCRIPTION

CB_RECALL kicks off the process of recalling an open delegation and returning it to the server. ๐Ÿฆ‹

The truncate flag is an optimization for a file that's about to be truncated to zero. When it's set, the client is off the hook for pushing modified data for that file to the server, since that data is irrelevant anyway. Main character energy for the client ๐Ÿ‘‘

If the handle isn't one the client holds an open delegation for, you get an NFS4ERR_BADHANDLE error.

If the stateid doesn't correspond to an open delegation for the file named by the filehandle, NFS4ERR_BAD_STATEID is returned.

IMPLEMENTATION

The client should reply to the callback immediately. Replying doesn't complete the recall, except when an error was returned. The recall isn't done until the delegation is returned using a DELEGRETURN. โœจ

ERRORS

  • NFS4ERR_BADHANDLE
  • NFS4ERR_BAD_STATEID
  • NFS4ERR_BADXDR
  • NFS4ERR_RESOURCE
  • NFS4ERR_SERVERFAULT

15.2.3. Operation 10044: CB_ILLEGAL - Illegal Callback Operation ๐Ÿ™ˆ

SYNOPSIS

<null> -> ()

ARGUMENT

void;

RESULT

struct CB_ILLEGAL4res {
        nfsstat4        status;
};

DESCRIPTION

Same energy as ILLEGAL: this operation is a placeholder for encoding a result when the client sends an operation code inside COMPOUND that isn't supported. See the COMPOUND procedure description for more details. ๐ŸŒˆ

The status field of CB_ILLEGAL4res MUST be set to NFS4ERR_OP_ILLEGAL.

IMPLEMENTATION

A server will probably never send OP_CB_ILLEGAL ๐Ÿ’€ But if it does, the response is CB_ILLEGAL4res, just like with any other invalid operation code. Note that if the client gets an illegal operation code that isn't OP_ILLEGAL, and it checks for legal codes during the XDR decode phase, then CB_ILLEGAL4res won't be returned.

ERRORS

  • NFS4ERR_OP_ILLEGAL

16. Security Considerations โœจ

okay so security is where things get SERIOUS bestie ๐Ÿซถ back in the day NFS basically said "the client is the whole machine" ๐Ÿ’€ the server trusted the NFS client to vouch for the end-user, and it only handed files to specific clients based on their source IP address. in that world, AUTH_SYS just told the server which end-user was using the client ๐Ÿ™ˆ and when responses came back, the client checked that they came from the same IP address and port the request was sent to. ngl that model is super easy to set up and use, but it is NOT safe ๐Ÿ˜ญ

so NFSv4 is like "absolutely not, we're doing better" ๐Ÿ’… it mandates that implementations support end to end authentication, where an end-user on a client mutually authenticates to a principal on an NFS server ๐Ÿ” and the crypto schemes used must not expose passwords or keys in the clear on the network ๐Ÿ™…โ€โ™€๏ธ integrity and privacy for NFS requests and responses also deserve a LOT of thought ๐Ÿ’œ all of this (end to end mutual auth, integrity, and privacy) is talked about in the section called "RPC and Security Flavor" ๐ŸŽ€

now, the classic IP-address-and-AUTH_SYS vibe can still be supported ๐Ÿฆ‹ but with a big caveat: AUTH_SYS is neither MANDATORY nor RECOMMENDED by this spec ๐Ÿ‘€ so interop through AUTH_SYS is not assured. it's giving "you can do it, but don't count on everyone else doing it too" ๐Ÿ˜ฌ

for the sake of less admin work, better performance, and less CPU usage ๐Ÿ“ some folks might skip security mechanisms that do integrity protection on every RPC call and response. the problem? that leaves the customer open to an attacker sitting between the NFS client and server who modifies the RPC request and/or response ๐Ÿ’€ implementations are free to offer weaker options, BUT there are two operations that are so important they should override what the user picked ๐Ÿšจ

the first one is SECINFO ๐Ÿšจ the client should issue the SECINFO call protected by a security flavor that has integrity protection, like RPCSEC_GSS with a security triple using either rpc_gss_svc_integrity or rpc_gss_svc_privacy (privacy already includes integrity protection) ๐Ÿ”’ without integrity covering SECINFO and its results, a man in the middle could change those results so the client picks a weaker algorithm from the set the server allows ๐Ÿ˜ฑ which could make the client and/or server open to more attacks. not it!! ๐Ÿ™…โ€โ™€๏ธ

the second one that should definitely use integrity protection is any GETATTR for the fs_locations attribute ๐Ÿ’… the attack has two steps. step one: the attacker changes the unprotected results of some operation so they return NFS4ERR_MOVED ๐Ÿ˜ˆ step two: when the client follows up with a GETATTR for fs_locations, the attacker changes those results to trick the client into migrating its traffic to a server the attacker controls ๐Ÿ’€ the audacity ๐Ÿ˜ค

last but NOT least ๐Ÿ‘‘ SETCLIENTID and SETCLIENTID_CONFIRM are responsible for releasing client state, so it is imperative that the principal used for these operations gets checked against, and matches, the previous use of these operations ๐Ÿซถ see the section "Client ID" for more deets ๐Ÿงš


17. IANA Considerations ๐ŸŒธ

17.1. Named Attribute Definition

okay so NFS version 4 lets you attach named attributes to files ๐Ÿ“ the names are defined as string names, but the protocol doesn't set who gets which part of the name space for these file attributes ๐Ÿคทโ€โ™€๏ธ and even though nothing stops collisions in that name space, IANA has a registry for NFS version 4 named attributes now ๐ŸŽ€ to register one, you publish an Informational RFC that covers not just the name, but also the syntax and semantics of the named attribute contents ๐Ÿ“œ the goal? interoperability wherever people have shared interests ๐Ÿฆ‹ application developers are totally allowed to define and use attributes as they need ๐Ÿ’– they're just encouraged to register them with IANA ๐Ÿซถ

17.2. ONC RPC Network Identifiers (netids)

okay so the "Structured Data Types" section talked about the r_netid field and the matching r_addr field of a clientaddr4 structure ๐Ÿงš NFS version 4 depends on the syntax and semantics of those fields to send callback info between client and server, so IANA now has a registry ๐Ÿ’œ it includes the values defined in this document and leaves room for future expansion based on transport usage and availability โœจ additions to this ONC RPC Network Identifier registry must be done with the publication of an RFC ๐Ÿ“œ

here are the starting values (some of this text is copied from section 2.2 so it's clear ๐ŸŽ€):

The Network Identifier (or r_netid for short) is used to specify a transport protocol and associated universal address (or r_addr for short). The syntax of the Network Identifier is a US-ASCII string. The initial definitions for r_netid are:

"tcp"   - TCP over IP version 4
"udp"   - UDP over IP version 4
"tcp6"  - TCP over IP version 6
"udp6"  - UDP over IP version 6

Note: the '"' marks are used for delimiting the strings for this document and are not part of the Network Identifier string. ๐Ÿ™ˆ

for "tcp" and "udp" netids, the universal address (r_addr for IPv4) is a US-ASCII string in this form:

h1.h2.h3.h4.p1.p2

the prefix, "h1.h2.h3.h4", is the standard textual way to write an IPv4 address, which is always four octets long ๐ŸŒˆ assuming big-endian ordering, h1, h2, h3, and h4 are the first through fourth octets, each converted to ASCII-decimal ๐Ÿ”ข also assuming big-endian ordering, p1 and p2 are the first and second octets, each converted to ASCII-decimal ๐Ÿ’ซ for example, if a host has an address of 0x0A010307 (in big-endian order) and a service is listening on port 0x020F (decimal 527, also big-endian), then the complete universal address is 10.1.3.7.2.15 โœจ

for "tcp6" and "udp6" netids, the universal address (r_addr for IPv6) is a US-ASCII string in this form:

x1:x2:x3:x4:x5:x6:x7:x8.p1.p2

the suffix "p1.p2" is the service port, and it's computed the same way as universal addresses for "tcp" and "udp" ๐Ÿฆ‹ the prefix, "x1:x2:x3:x4:x5:x6:x7:x8", is the standard textual form for an IPv6 address as defined in Section 2.2 of [RFC2373] ๐ŸŒธ the two alternative forms specified in Section 2.2 of [RFC2373] are also acceptable ๐Ÿ’…

and finally, registering a new Network Identifier will require publication of an Information RFC with similar detail to what's listed above, covering the Network Identifier itself and its corresponding Universal Address ๐Ÿ“š๐Ÿซถ


18. RPC definition file ๐ŸŽ€

here's the RPC definition file, the nfs4_prot.x stuff, copy-pasted exactly so nothing gets lost in translation ๐Ÿ’œ

   /*
    *  Copyright (C) The Internet Society (1998,1999,2000,2001,2002).
    *  All Rights Reserved.
    */

   /*
    *      nfs4_prot.x
    *
    */

   %#pragma ident  "%W%"

   /*
    * Basic typedefs for RFC 1832 data type definitions
    */
   typedef int             int32_t;
   typedef unsigned int    uint32_t;
   typedef hyper           int64_t;
   typedef unsigned hyper  uint64_t;

   /*
    * Sizes
    */
   const NFS4_FHSIZE               = 128;
   const NFS4_VERIFIER_SIZE        = 8;
   const NFS4_OPAQUE_LIMIT         = 1024;

   /*
    * File types
    */
   enum nfs_ftype4 {
           NF4REG          = 1,    /* Regular File */
           NF4DIR          = 2,    /* Directory */
           NF4BLK          = 3,    /* Special File - block device */
           NF4CHR          = 4,    /* Special File - character device */
           NF4LNK          = 5,    /* Symbolic Link */
           NF4SOCK         = 6,    /* Special File - socket */
           NF4FIFO         = 7,    /* Special File - fifo */
           NF4ATTRDIR      = 8,    /* Attribute Directory */
           NF4NAMEDATTR    = 9     /* Named Attribute */
   };

okay bestie, this is the XDR, the type dictionary the whole protocol is built on ๐Ÿ“šโœจ every error code, basic type, file attribute and ACE constant lives in here, so let's go ๐Ÿ’…

   /*
    * Error status
    */
   enum nfsstat4 {
           NFS4_OK                 = 0,    /* everything is okay      */
           NFS4ERR_PERM            = 1,    /* caller not privileged   */
           NFS4ERR_NOENT           = 2,    /* no such file/directory  */
           NFS4ERR_IO              = 5,    /* hard I/O error          */
           NFS4ERR_NXIO            = 6,    /* no such device          */
           NFS4ERR_ACCESS          = 13,   /* access denied           */
           NFS4ERR_EXIST           = 17,   /* file already exists     */
           NFS4ERR_XDEV            = 18,   /* different filesystems   */
           /* Unused/reserved        19 */
           NFS4ERR_NOTDIR          = 20,   /* should be a directory   */
           NFS4ERR_ISDIR           = 21,   /* should not be directory */
           NFS4ERR_INVAL           = 22,   /* invalid argument        */
           NFS4ERR_FBIG            = 27,   /* file exceeds server max */
           NFS4ERR_NOSPC           = 28,   /* no space on filesystem  */
           NFS4ERR_ROFS            = 30,   /* read-only filesystem    */
           NFS4ERR_MLINK           = 31,   /* too many hard links     */
           NFS4ERR_NAMETOOLONG     = 63,   /* name exceeds server max */
           NFS4ERR_NOTEMPTY        = 66,   /* directory not empty     */
           NFS4ERR_DQUOT           = 69,   /* hard quota limit reached*/
           NFS4ERR_STALE           = 70,   /* file no longer exists   */
           NFS4ERR_BADHANDLE       = 10001,/* Illegal filehandle      */
           NFS4ERR_BAD_COOKIE      = 10003,/* READDIR cookie is stale */
           NFS4ERR_NOTSUPP         = 10004,/* operation not supported */
           NFS4ERR_TOOSMALL        = 10005,/* response limit exceeded */
           NFS4ERR_SERVERFAULT     = 10006,/* undefined server error  */
           NFS4ERR_BADTYPE         = 10007,/* type invalid for CREATE */
           NFS4ERR_DELAY           = 10008,/* file "busy" - retry     */
           NFS4ERR_SAME            = 10009,/* nverify says attrs same */
           NFS4ERR_DENIED          = 10010,/* lock unavailable        */
           NFS4ERR_EXPIRED         = 10011,/* lock lease expired      */
           NFS4ERR_LOCKED          = 10012,/* I/O failed due to lock  */
           NFS4ERR_GRACE           = 10013,/* in grace period         */
           NFS4ERR_FHEXPIRED       = 10014,/* filehandle expired      */
           NFS4ERR_SHARE_DENIED    = 10015,/* share reserve denied    */
           NFS4ERR_WRONGSEC        = 10016,/* wrong security flavor   */
           NFS4ERR_CLID_INUSE      = 10017,/* clientid in use         */
           NFS4ERR_RESOURCE        = 10018,/* resource exhaustion     */
           NFS4ERR_MOVED           = 10019,/* filesystem relocated    */
           NFS4ERR_NOFILEHANDLE    = 10020,/* current FH is not set   */
           NFS4ERR_MINOR_VERS_MISMATCH = 10021,/* minor vers not supp */
           NFS4ERR_STALE_CLIENTID  = 10022,/* server has rebooted     */
           NFS4ERR_STALE_STATEID   = 10023,/* server has rebooted     */
           NFS4ERR_OLD_STATEID     = 10024,/* state is out of sync    */
           NFS4ERR_BAD_STATEID     = 10025,/* incorrect stateid       */
           NFS4ERR_BAD_SEQID       = 10026,/* request is out of seq.  */
           NFS4ERR_NOT_SAME        = 10027,/* verify - attrs not same */
           NFS4ERR_LOCK_RANGE      = 10028,/* lock range not supported*/
           NFS4ERR_SYMLINK         = 10029,/* should be file/directory*/
           NFS4ERR_RESTOREFH       = 10030,/* no saved filehandle     */
           NFS4ERR_LEASE_MOVED     = 10031,/* some filesystem moved   */
           NFS4ERR_ATTRNOTSUPP     = 10032,/* recommended attr not sup*/
           NFS4ERR_NO_GRACE        = 10033,/* reclaim outside of grace*/
           NFS4ERR_RECLAIM_BAD     = 10034,/* reclaim error at server */
           NFS4ERR_RECLAIM_CONFLICT = 10035,/* conflict on reclaim    */
           NFS4ERR_BADXDR          = 10036,/* XDR decode failed       */
           NFS4ERR_LOCKS_HELD      = 10037,/* file locks held at CLOSE*/
           NFS4ERR_OPENMODE        = 10038,/* conflict in OPEN and I/O*/
           NFS4ERR_BADOWNER        = 10039,/* owner translation bad   */
           NFS4ERR_BADCHAR         = 10040,/* utf-8 char not supported*/
           NFS4ERR_BADNAME         = 10041,/* name not supported      */
           NFS4ERR_BAD_RANGE       = 10042,/* lock range not supported*/
           NFS4ERR_LOCK_NOTSUPP    = 10043,/* no atomic up/downgrade  */
           NFS4ERR_OP_ILLEGAL      = 10044,/* undefined operation     */
           NFS4ERR_DEADLOCK        = 10045,/* file locking deadlock   */
           NFS4ERR_FILE_OPEN       = 10046,/* open file blocks op.    */
           NFS4ERR_ADMIN_REVOKED   = 10047,/* lockowner state revoked */
           NFS4ERR_CB_PATH_DOWN    = 10048 /* callback path down      */
   };

   /*
    * Basic data types
    */
   typedef uint32_t        bitmap4<>;
   typedef uint64_t        offset4;
   typedef uint32_t        count4;
   typedef uint64_t        length4;
   typedef uint64_t        clientid4;
   typedef uint32_t        seqid4;
   typedef opaque          utf8string<>;
   typedef utf8string      utf8str_cis;
   typedef utf8string      utf8str_cs;
   typedef utf8string      utf8str_mixed;
   typedef utf8str_cs      component4;
   typedef component4      pathname4<>;
   typedef uint64_t        nfs_lockid4;
   typedef uint64_t        nfs_cookie4;
   typedef utf8str_cs      linktext4;
   typedef opaque          sec_oid4<>;
   typedef uint32_t        qop4;
   typedef uint32_t        mode4;
   typedef uint64_t        changeid4;
   typedef opaque          verifier4[NFS4_VERIFIER_SIZE];

   /*
    * Timeval
    */
   struct nfstime4 {
           int64_t         seconds;
           uint32_t        nseconds;
   };

   enum time_how4 {
           SET_TO_SERVER_TIME4 = 0,
           SET_TO_CLIENT_TIME4 = 1
   };

   union settime4 switch (time_how4 set_it) {
    case SET_TO_CLIENT_TIME4:
            nfstime4       time;
    default:
            void;
   };

   /*
    * File access handle
    */
   typedef opaque  nfs_fh4<NFS4_FHSIZE>;


   /*
    * File attribute definitions
    */

   /*
    * FSID structure for major/minor
    */
   struct fsid4 {
           uint64_t        major;
           uint64_t        minor;
   };

   /*

    * Filesystem locations attribute for relocation/migration
    */
   struct fs_location4 {
           utf8str_cis     server<>;
           pathname4       rootpath;
   };

   struct fs_locations4 {
           pathname4       fs_root;
           fs_location4    locations<>;
   };

   /*
    * Various Access Control Entry definitions
    */

   /*
    * Mask that indicates which Access Control Entries are supported.
    * Values for the fattr4_aclsupport attribute.
    */
   const ACL4_SUPPORT_ALLOW_ACL    = 0x00000001;
   const ACL4_SUPPORT_DENY_ACL     = 0x00000002;
   const ACL4_SUPPORT_AUDIT_ACL    = 0x00000004;
   const ACL4_SUPPORT_ALARM_ACL    = 0x00000008;


   typedef uint32_t        acetype4;
   /*
    * acetype4 values, others can be added as needed.
    */
   const ACE4_ACCESS_ALLOWED_ACE_TYPE      = 0x00000000;
   const ACE4_ACCESS_DENIED_ACE_TYPE       = 0x00000001;
   const ACE4_SYSTEM_AUDIT_ACE_TYPE        = 0x00000002;
   const ACE4_SYSTEM_ALARM_ACE_TYPE        = 0x00000003;


   /*
    * ACE flag
    */
   typedef uint32_t aceflag4;

   /*
    * ACE flag values
    */
   const ACE4_FILE_INHERIT_ACE             = 0x00000001;
   const ACE4_DIRECTORY_INHERIT_ACE        = 0x00000002;
   const ACE4_NO_PROPAGATE_INHERIT_ACE     = 0x00000004;
   const ACE4_INHERIT_ONLY_ACE             = 0x00000008;
   const ACE4_SUCCESSFUL_ACCESS_ACE_FLAG   = 0x00000010;
   const ACE4_FAILED_ACCESS_ACE_FLAG       = 0x00000020;
   const ACE4_IDENTIFIER_GROUP             = 0x00000040;


   /*
    * ACE mask
    */
   typedef uint32_t        acemask4;

   /*
    * ACE mask values
    */
   const ACE4_READ_DATA            = 0x00000001;
   const ACE4_LIST_DIRECTORY       = 0x00000001;
   const ACE4_WRITE_DATA           = 0x00000002;
   const ACE4_ADD_FILE             = 0x00000002;
   const ACE4_APPEND_DATA          = 0x00000004;
   const ACE4_ADD_SUBDIRECTORY     = 0x00000004;
   const ACE4_READ_NAMED_ATTRS     = 0x00000008;
   const ACE4_WRITE_NAMED_ATTRS    = 0x00000010;
   const ACE4_EXECUTE              = 0x00000020;
   const ACE4_DELETE_CHILD         = 0x00000040;
   const ACE4_READ_ATTRIBUTES      = 0x00000080;
   const ACE4_WRITE_ATTRIBUTES     = 0x00000100;

   const ACE4_DELETE               = 0x00010000;
   const ACE4_READ_ACL             = 0x00020000;
   const ACE4_WRITE_ACL            = 0x00040000;
   const ACE4_WRITE_OWNER          = 0x00080000;
   const ACE4_SYNCHRONIZE          = 0x00100000;

   /*
    * ACE4_GENERIC_READ -- defined as combination of
    *      ACE4_READ_ACL |
    *      ACE4_READ_DATA |
    *      ACE4_READ_ATTRIBUTES |
    *      ACE4_SYNCHRONIZE
    */

   const ACE4_GENERIC_READ = 0x00120081;

   /*
    * ACE4_GENERIC_WRITE -- defined as combination of
    *      ACE4_READ_ACL |
    *      ACE4_WRITE_DATA |
    *      ACE4_WRITE_ATTRIBUTES |
    *      ACE4_WRITE_ACL |
    *      ACE4_APPEND_DATA |
    *      ACE4_SYNCHRONIZE
    */
   const ACE4_GENERIC_WRITE = 0x00160106;


   /*
    * ACE4_GENERIC_EXECUTE -- defined as combination of
    *      ACE4_READ_ACL
    *      ACE4_READ_ATTRIBUTES
    *      ACE4_EXECUTE
    *      ACE4_SYNCHRONIZE
    */
   const ACE4_GENERIC_EXECUTE = 0x001200A0;


   /*
    * Access Control Entry definition
    */
   struct nfsace4 {
           acetype4        type;
           aceflag4        flag;
           acemask4        access_mask;
           utf8str_mixed   who;
   };

   /*
    * Field definitions for the fattr4_mode attribute
    */
   const MODE4_SUID = 0x800;  /* set user id on execution */
   const MODE4_SGID = 0x400;  /* set group id on execution */
   const MODE4_SVTX = 0x200;  /* save text even after use */
   const MODE4_RUSR = 0x100;  /* read permission: owner */
   const MODE4_WUSR = 0x080;  /* write permission: owner */
   const MODE4_XUSR = 0x040;  /* execute permission: owner */
   const MODE4_RGRP = 0x020;  /* read permission: group */
   const MODE4_WGRP = 0x010;  /* write permission: group */
   const MODE4_XGRP = 0x008;  /* execute permission: group */
   const MODE4_ROTH = 0x004;  /* read permission: other */
   const MODE4_WOTH = 0x002;  /* write permission: other */
   const MODE4_XOTH = 0x001;  /* execute permission: other */

   /*
    * Special data/attribute associated with
    * file types NF4BLK and NF4CHR.
    */
   struct specdata4 {
           uint32_t        specdata1;      /* major device number */
           uint32_t        specdata2;      /* minor device number */
   };

   /*
    * Values for fattr4_fh_expire_type
    */
   const   FH4_PERSISTENT          = 0x00000000;
   const   FH4_NOEXPIRE_WITH_OPEN  = 0x00000001;
   const   FH4_VOLATILE_ANY        = 0x00000002;
   const   FH4_VOL_MIGRATION       = 0x00000004;
   const   FH4_VOL_RENAME          = 0x00000008;


   typedef bitmap4         fattr4_supported_attrs;
   typedef nfs_ftype4      fattr4_type;
   typedef uint32_t        fattr4_fh_expire_type;
   typedef changeid4       fattr4_change;
   typedef uint64_t        fattr4_size;
   typedef bool            fattr4_link_support;
   typedef bool            fattr4_symlink_support;
   typedef bool            fattr4_named_attr;
   typedef fsid4           fattr4_fsid;
   typedef bool            fattr4_unique_handles;
   typedef uint32_t        fattr4_lease_time;
   typedef nfsstat4        fattr4_rdattr_error;

okay bestie, the typedef list is literally the whole vibe ๐Ÿ’… Every fattr4_ name is a different attribute type, so here they all are, lined up in order ๐ŸŽ€

   typedef nfsace4         fattr4_acl<>;
   typedef uint32_t        fattr4_aclsupport;
   typedef bool            fattr4_archive;
   typedef bool            fattr4_cansettime;
   typedef bool            fattr4_case_insensitive;
   typedef bool            fattr4_case_preserving;
   typedef bool            fattr4_chown_restricted;
   typedef uint64_t        fattr4_fileid;
   typedef uint64_t        fattr4_files_avail;
   typedef nfs_fh4         fattr4_filehandle;
   typedef uint64_t        fattr4_files_free;
   typedef uint64_t        fattr4_files_total;
   typedef fs_locations4   fattr4_fs_locations;
   typedef bool            fattr4_hidden;
   typedef bool            fattr4_homogeneous;
   typedef uint64_t        fattr4_maxfilesize;
   typedef uint32_t        fattr4_maxlink;
   typedef uint32_t        fattr4_maxname;
   typedef uint64_t        fattr4_maxread;
   typedef uint64_t        fattr4_maxwrite;
   typedef utf8str_cs      fattr4_mimetype;
   typedef mode4           fattr4_mode;
   typedef uint64_t        fattr4_mounted_on_fileid;
   typedef bool            fattr4_no_trunc;
   typedef uint32_t        fattr4_numlinks;
   typedef utf8str_mixed   fattr4_owner;
   typedef utf8str_mixed   fattr4_owner_group;
   typedef uint64_t        fattr4_quota_avail_hard;
   typedef uint64_t        fattr4_quota_avail_soft;
   typedef uint64_t        fattr4_quota_used;
   typedef specdata4       fattr4_rawdev;
   typedef uint64_t        fattr4_space_avail;
   typedef uint64_t        fattr4_space_free;
   typedef uint64_t        fattr4_space_total;
   typedef uint64_t        fattr4_space_used;
   typedef bool            fattr4_system;
   typedef nfstime4        fattr4_time_access;
   typedef settime4        fattr4_time_access_set;
   typedef nfstime4        fattr4_time_backup;
   typedef nfstime4        fattr4_time_create;
   typedef nfstime4        fattr4_time_delta;
   typedef nfstime4        fattr4_time_metadata;
   typedef nfstime4        fattr4_time_modify;
   typedef settime4        fattr4_time_modify_set;


   /*
    * Mandatory Attributes
    */
   const FATTR4_SUPPORTED_ATTRS    = 0;
   const FATTR4_TYPE               = 1;
   const FATTR4_FH_EXPIRE_TYPE     = 2;
   const FATTR4_CHANGE             = 3;
   const FATTR4_SIZE               = 4;
   const FATTR4_LINK_SUPPORT       = 5;
   const FATTR4_SYMLINK_SUPPORT    = 6;
   const FATTR4_NAMED_ATTR         = 7;
   const FATTR4_FSID               = 8;
   const FATTR4_UNIQUE_HANDLES     = 9;
   const FATTR4_LEASE_TIME         = 10;
   const FATTR4_RDATTR_ERROR       = 11;
   const FATTR4_FILEHANDLE         = 19;

   /*
    * Recommended Attributes
    */
   const FATTR4_ACL                = 12;
   const FATTR4_ACLSUPPORT         = 13;
   const FATTR4_ARCHIVE            = 14;
   const FATTR4_CANSETTIME         = 15;
   const FATTR4_CASE_INSENSITIVE   = 16;
   const FATTR4_CASE_PRESERVING    = 17;
   const FATTR4_CHOWN_RESTRICTED   = 18;
   const FATTR4_FILEID             = 20;
   const FATTR4_FILES_AVAIL        = 21;
   const FATTR4_FILES_FREE         = 22;
   const FATTR4_FILES_TOTAL        = 23;
   const FATTR4_FS_LOCATIONS       = 24;
   const FATTR4_HIDDEN             = 25;
   const FATTR4_HOMOGENEOUS        = 26;
   const FATTR4_MAXFILESIZE        = 27;
   const FATTR4_MAXLINK            = 28;
   const FATTR4_MAXNAME            = 29;
   const FATTR4_MAXREAD            = 30;
   const FATTR4_MAXWRITE           = 31;
   const FATTR4_MIMETYPE           = 32;
   const FATTR4_MODE               = 33;
   const FATTR4_NO_TRUNC           = 34;
   const FATTR4_NUMLINKS           = 35;
   const FATTR4_OWNER              = 36;
   const FATTR4_OWNER_GROUP        = 37;
   const FATTR4_QUOTA_AVAIL_HARD   = 38;
   const FATTR4_QUOTA_AVAIL_SOFT   = 39;
   const FATTR4_QUOTA_USED         = 40;
   const FATTR4_RAWDEV             = 41;
   const FATTR4_SPACE_AVAIL        = 42;
   const FATTR4_SPACE_FREE         = 43;
   const FATTR4_SPACE_TOTAL        = 44;
   const FATTR4_SPACE_USED         = 45;
   const FATTR4_SYSTEM             = 46;
   const FATTR4_TIME_ACCESS        = 47;
   const FATTR4_TIME_ACCESS_SET    = 48;
   const FATTR4_TIME_BACKUP        = 49;
   const FATTR4_TIME_CREATE        = 50;
   const FATTR4_TIME_DELTA         = 51;
   const FATTR4_TIME_METADATA      = 52;
   const FATTR4_TIME_MODIFY        = 53;
   const FATTR4_TIME_MODIFY_SET    = 54;
   const FATTR4_MOUNTED_ON_FILEID  = 55;

   typedef opaque  attrlist4<>;

   /*
    * File attribute container
    */
   struct fattr4 {
           bitmap4         attrmask;
           attrlist4       attr_vals;

   };

   /*
    * Change info for the client
    */
   struct change_info4 {
           bool            atomic;
           changeid4       before;
           changeid4       after;
   };

   struct clientaddr4 {
           /* see struct rpcb in RFC 1833 */
           string r_netid<>;               /* network id */
           string r_addr<>;                /* universal address */
   };

   /*
    * Callback program info as provided by the client
    */
   struct cb_client4 {
           uint32_t        cb_program;
           clientaddr4     cb_location;
   };

   /*
    * Stateid
    */
   struct stateid4 {
           uint32_t        seqid;
           opaque          other[12];
   };

   /*
    * Client ID
    */
   struct nfs_client_id4 {
           verifier4       verifier;
           opaque          id<NFS4_OPAQUE_LIMIT>;
   };

   struct open_owner4 {
           clientid4       clientid;
           opaque          owner<NFS4_OPAQUE_LIMIT>;
   };

   struct lock_owner4 {
           clientid4       clientid;
           opaque          owner<NFS4_OPAQUE_LIMIT>;
   };

   enum nfs_lock_type4 {
           READ_LT         = 1,
           WRITE_LT        = 2,
           READW_LT        = 3,    /* blocking read */
           WRITEW_LT       = 4     /* blocking write */
   };

   /*
    * ACCESS: Check access permission
    */
   const ACCESS4_READ      = 0x00000001;
   const ACCESS4_LOOKUP    = 0x00000002;
   const ACCESS4_MODIFY    = 0x00000004;
   const ACCESS4_EXTEND    = 0x00000008;
   const ACCESS4_DELETE    = 0x00000010;
   const ACCESS4_EXECUTE   = 0x00000020;

   struct ACCESS4args {
           /* CURRENT_FH: object */
           uint32_t        access;
   };

   struct ACCESS4resok {
           uint32_t        supported;
           uint32_t        access;
   };

   union ACCESS4res switch (nfsstat4 status) {
    case NFS4_OK:
            ACCESS4resok   resok4;
    default:
            void;
   };

   /*
    * CLOSE: Close a file and release share reservations
    */
   struct CLOSE4args {
           /* CURRENT_FH: object */
           seqid4          seqid;
           stateid4        open_stateid;
   };

   union CLOSE4res switch (nfsstat4 status) {
    case NFS4_OK:

            stateid4       open_stateid;
    default:
            void;
   };

   /*
    * COMMIT: Commit cached data on server to stable storage
    */
   struct COMMIT4args {
           /* CURRENT_FH: file */
           offset4         offset;
           count4          count;
   };

   struct COMMIT4resok {
           verifier4       writeverf;
   };


   union COMMIT4res switch (nfsstat4 status) {
    case NFS4_OK:
            COMMIT4resok   resok4;
    default:
            void;
   };

   /*
    * CREATE: Create a non-regular file
    */
   union createtype4 switch (nfs_ftype4 type) {
    case NF4LNK:
            linktext4      linkdata;
    case NF4BLK:
    case NF4CHR:
            specdata4      devdata;
    case NF4SOCK:
    case NF4FIFO:
    case NF4DIR:
            void;
    default:
            void;          /* server should return NFS4ERR_BADTYPE */
   };

   struct CREATE4args {
           /* CURRENT_FH: directory for creation */
           createtype4     objtype;
           component4      objname;
           fattr4          createattrs;

   };

   struct CREATE4resok {
           change_info4    cinfo;
           bitmap4         attrset;        /* attributes set */
   };

   union CREATE4res switch (nfsstat4 status) {
    case NFS4_OK:
            CREATE4resok resok4;
    default:
            void;
   };

   /*
    * DELEGPURGE: Purge Delegations Awaiting Recovery
    */
   struct DELEGPURGE4args {
           clientid4       clientid;
   };

   struct DELEGPURGE4res {
           nfsstat4        status;
   };

   /*
    * DELEGRETURN: Return a delegation
    */
   struct DELEGRETURN4args {
           /* CURRENT_FH: delegated file */
           stateid4        deleg_stateid;
   };

   struct DELEGRETURN4res {
           nfsstat4        status;
   };

   /*
    * GETATTR: Get file attributes
    */
   struct GETATTR4args {
           /* CURRENT_FH: directory or file */
           bitmap4         attr_request;
   };

   struct GETATTR4resok {
           fattr4          obj_attributes;
   };
   union GETATTR4res switch (nfsstat4 status) {
    case NFS4_OK:
            GETATTR4resok  resok4;
    default:
            void;
   };

   /*
    * GETFH: Get current filehandle
    */
   struct GETFH4resok {
           nfs_fh4         object;
   };

   union GETFH4res switch (nfsstat4 status) {
    case NFS4_OK:
           GETFH4resok     resok4;
    default:
           void;
   };

   /*
    * LINK: Create link to an object
    */
   struct LINK4args {
           /* SAVED_FH: source object */
           /* CURRENT_FH: target directory */
           component4      newname;
   };

   struct LINK4resok {
           change_info4    cinfo;
   };

   union LINK4res switch (nfsstat4 status) {
    case NFS4_OK:
            LINK4resok resok4;
    default:
            void;
   };

   /*
    * For LOCK, transition from open_owner to new lock_owner
    */
   struct open_to_lock_owner4 {
           seqid4          open_seqid;
           stateid4        open_stateid;
           seqid4          lock_seqid;
           lock_owner4     lock_owner;
   };

   /*
    * For LOCK, existing lock_owner continues to request file locks
    */
   struct exist_lock_owner4 {
           stateid4        lock_stateid;
           seqid4          lock_seqid;
   };

   union locker4 switch (bool new_lock_owner) {
    case TRUE:
           open_to_lock_owner4     open_owner;
    case FALSE:
           exist_lock_owner4       lock_owner;
   };

   /*
    * LOCK/LOCKT/LOCKU: Record lock management
    */
   struct LOCK4args {
           /* CURRENT_FH: file */
           nfs_lock_type4  locktype;
           bool            reclaim;
           offset4         offset;
           length4         length;
           locker4         locker;
   };

   struct LOCK4denied {
           offset4         offset;
           length4         length;
           nfs_lock_type4  locktype;
           lock_owner4     owner;
   };

   struct LOCK4resok {
           stateid4        lock_stateid;
   };

   union LOCK4res switch (nfsstat4 status) {
    case NFS4_OK:
            LOCK4resok     resok4;
    case NFS4ERR_DENIED:
            LOCK4denied    denied;
    default:
            void;

   };

   struct LOCKT4args {
           /* CURRENT_FH: file */
           nfs_lock_type4  locktype;
           offset4         offset;
           length4         length;
           lock_owner4     owner;
   };

   union LOCKT4res switch (nfsstat4 status) {
    case NFS4ERR_DENIED:
            LOCK4denied    denied;
    case NFS4_OK:
            void;
    default:
            void;
   };

   struct LOCKU4args {
           /* CURRENT_FH: file */
           nfs_lock_type4  locktype;
           seqid4          seqid;
           stateid4        lock_stateid;
           offset4         offset;
           length4         length;
   };

   union LOCKU4res switch (nfsstat4 status) {
    case   NFS4_OK:
            stateid4       lock_stateid;
    default:
            void;
   };

   /*
    * LOOKUP: Lookup filename
    */
   struct LOOKUP4args {
           /* CURRENT_FH: directory */
           component4      objname;
   };

that's the attribute and lock-owner plumbing, all lined up ๐Ÿงš

   struct LOOKUP4res {
           /* CURRENT_FH: object */
           nfsstat4        status;
   };
   /*
    * LOOKUPP: Lookup parent directory
    */
   struct LOOKUPP4res {
           /* CURRENT_FH: directory */
           nfsstat4        status;
   };

   /*
    * NVERIFY: Verify attributes different
    */
   struct NVERIFY4args {
           /* CURRENT_FH: object */
           fattr4          obj_attributes;
   };

   struct NVERIFY4res {
           nfsstat4        status;
   };

   /*
    * Various definitions for OPEN
    */
   enum createmode4 {
           UNCHECKED4      = 0,
           GUARDED4        = 1,
           EXCLUSIVE4      = 2
   };

   union createhow4 switch (createmode4 mode) {
    case UNCHECKED4:
    case GUARDED4:
            fattr4         createattrs;
    case EXCLUSIVE4:
            verifier4      createverf;
   };

   enum opentype4 {
           OPEN4_NOCREATE  = 0,
           OPEN4_CREATE    = 1
   };

   union openflag4 switch (opentype4 opentype) {
    case OPEN4_CREATE:
            createhow4     how;
    default:
            void;
   };
   /* Next definitions used for OPEN delegation */
   enum limit_by4 {
           NFS_LIMIT_SIZE          = 1,
           NFS_LIMIT_BLOCKS        = 2
           /* others as needed */
   };

   struct nfs_modified_limit4 {
           uint32_t        num_blocks;
           uint32_t        bytes_per_block;
   };

   union nfs_space_limit4 switch (limit_by4 limitby) {
    /* limit specified as file size */
    case NFS_LIMIT_SIZE:
            uint64_t               filesize;
    /* limit specified by number of blocks */
    case NFS_LIMIT_BLOCKS:
            nfs_modified_limit4    mod_blocks;
   } ;

   /*
    * Share Access and Deny constants for open argument
    */
   const OPEN4_SHARE_ACCESS_READ   = 0x00000001;
   const OPEN4_SHARE_ACCESS_WRITE  = 0x00000002;
   const OPEN4_SHARE_ACCESS_BOTH   = 0x00000003;

   const OPEN4_SHARE_DENY_NONE     = 0x00000000;
   const OPEN4_SHARE_DENY_READ     = 0x00000001;
   const OPEN4_SHARE_DENY_WRITE    = 0x00000002;
   const OPEN4_SHARE_DENY_BOTH     = 0x00000003;

   enum open_delegation_type4 {
           OPEN_DELEGATE_NONE      = 0,
           OPEN_DELEGATE_READ      = 1,
           OPEN_DELEGATE_WRITE     = 2
   };

   enum open_claim_type4 {
           CLAIM_NULL              = 0,
           CLAIM_PREVIOUS          = 1,
           CLAIM_DELEGATE_CUR      = 2,
           CLAIM_DELEGATE_PREV     = 3
   };

   struct open_claim_delegate_cur4 {
           stateid4        delegate_stateid;
           component4      file;
   };

   union open_claim4 switch (open_claim_type4 claim) {
    /*
     * No special rights to file. Ordinary OPEN of the specified file.
     */
    case CLAIM_NULL:
           /* CURRENT_FH: directory */
           component4      file;

    /*
     * Right to the file established by an open previous to server
     * reboot.  File identified by filehandle obtained at that time
     * rather than by name.
     */
    case CLAIM_PREVIOUS:
           /* CURRENT_FH: file being reclaimed */
           open_delegation_type4   delegate_type;

    /*
     * Right to file based on a delegation granted by the server.
     * File is specified by name.
     */
    case CLAIM_DELEGATE_CUR:
           /* CURRENT_FH: directory */
           open_claim_delegate_cur4        delegate_cur_info;

    /* Right to file based on a delegation granted to a previous boot
     * instance of the client.  File is specified by name.
     */
    case CLAIM_DELEGATE_PREV:
            /* CURRENT_FH: directory */
           component4      file_delegate_prev;
   };

   /*
    * OPEN: Open a file, potentially receiving an open delegation
    */
   struct OPEN4args {
           seqid4          seqid;
           uint32_t        share_access;
           uint32_t        share_deny;
           open_owner4     owner;
           openflag4       openhow;
           open_claim4     claim;
   };
   struct open_read_delegation4 {
           stateid4        stateid;        /* Stateid for delegation*/
           bool            recall;         /* Pre-recalled flag for
                                              delegations obtained
                                              by reclaim
                                              (CLAIM_PREVIOUS) */
           nfsace4         permissions;    /* Defines users who don't
                                              need an ACCESS call to
                                              open for read */
   };

   struct open_write_delegation4 {
           stateid4        stateid;        /* Stateid for delegation */
           bool            recall;         /* Pre-recalled flag for
                                              delegations obtained
                                              by reclaim
                                              (CLAIM_PREVIOUS) */
           nfs_space_limit4 space_limit;   /* Defines condition that
                                              the client must check to
                                              determine whether the
                                              file needs to be flushed
                                              to the server on close.
                                              */
           nfsace4         permissions;    /* Defines users who don't
                                              need an ACCESS call as
                                              part of a delegated
                                              open. */
   };

   union open_delegation4
   switch (open_delegation_type4 delegation_type) {
           case OPEN_DELEGATE_NONE:
                   void;
           case OPEN_DELEGATE_READ:
                   open_read_delegation4 read;
           case OPEN_DELEGATE_WRITE:
                   open_write_delegation4 write;
   };
   /*
    * Result flags
    */
   /* Client must confirm open */
   const OPEN4_RESULT_CONFIRM      = 0x00000002;
   /* Type of file locking behavior at the server */
   const OPEN4_RESULT_LOCKTYPE_POSIX = 0x00000004;

   struct OPEN4resok {
           stateid4        stateid;        /* Stateid for open */
           change_info4    cinfo;          /* Directory Change Info */
           uint32_t        rflags;         /* Result flags */
           bitmap4         attrset;        /* attribute set for create*/
           open_delegation4 delegation;    /* Info on any open
                                              delegation */
   };

   union OPEN4res switch (nfsstat4 status) {
    case NFS4_OK:
           /* CURRENT_FH: opened file */
           OPEN4resok      resok4;
    default:
           void;
   };

   /*
    * OPENATTR: open named attributes directory
    */
   struct OPENATTR4args {
           /* CURRENT_FH: object */
           bool    createdir;
   };

   struct OPENATTR4res {
           /* CURRENT_FH: named attr directory */
           nfsstat4        status;
   };

   /*
    * OPEN_CONFIRM: confirm the open
    */
   struct OPEN_CONFIRM4args {
           /* CURRENT_FH: opened file */
           stateid4        open_stateid;
           seqid4          seqid;
   };

   struct OPEN_CONFIRM4resok {
           stateid4        open_stateid;
   };

   union OPEN_CONFIRM4res switch (nfsstat4 status) {
       case NFS4_OK:
               OPEN_CONFIRM4resok     resok4;
    default:
            void;
   };
   /*
    * OPEN_DOWNGRADE: downgrade the access/deny for a file
    */
   struct OPEN_DOWNGRADE4args {
           /* CURRENT_FH: opened file */
           stateid4        open_stateid;
           seqid4          seqid;
           uint32_t        share_access;
           uint32_t        share_deny;
   };

   struct OPEN_DOWNGRADE4resok {
           stateid4        open_stateid;
   };

   union OPEN_DOWNGRADE4res switch(nfsstat4 status) {
    case NFS4_OK:
           OPEN_DOWNGRADE4resok    resok4;
    default:
            void;
   };

   /*
    * PUTFH: Set current filehandle
    */
   struct PUTFH4args {
           nfs_fh4         object;
   };

   struct PUTFH4res {
           /* CURRENT_FH: */
           nfsstat4        status;
   };

   /*
    * PUTPUBFH: Set public filehandle
    */
   struct PUTPUBFH4res {
           /* CURRENT_FH: public fh */
           nfsstat4        status;
   };

   /*
    * PUTROOTFH: Set root filehandle
    */
   struct PUTROOTFH4res {

           /* CURRENT_FH: root fh */
           nfsstat4        status;
   };

   /*
    * READ: Read from file
    */
   struct READ4args {
           /* CURRENT_FH: file */
           stateid4        stateid;
           offset4         offset;
           count4          count;
   };

   struct READ4resok {
           bool            eof;
           opaque          data<>;
   };

   union READ4res switch (nfsstat4 status) {
    case NFS4_OK:
            READ4resok     resok4;
    default:
            void;
   };

   /*
    * READDIR: Read directory
    */
   struct READDIR4args {
           /* CURRENT_FH: directory */
           nfs_cookie4     cookie;
           verifier4       cookieverf;
           count4          dircount;
           count4          maxcount;
           bitmap4         attr_request;
   };

   struct entry4 {
           nfs_cookie4     cookie;
           component4      name;
           fattr4          attrs;
           entry4          *nextentry;
   };

   struct dirlist4 {
           entry4          *entries;
           bool            eof;
   };
   struct READDIR4resok {
           verifier4       cookieverf;
           dirlist4        reply;
   };


   union READDIR4res switch (nfsstat4 status) {
    case NFS4_OK:
            READDIR4resok  resok4;
    default:
            void;
   };


   /*
    * READLINK: Read symbolic link
    */
   struct READLINK4resok {
           linktext4       link;
   };

   union READLINK4res switch (nfsstat4 status) {
    case NFS4_OK:
            READLINK4resok resok4;
    default:
            void;
   };

   /*
    * REMOVE: Remove filesystem object
    */
   struct REMOVE4args {
           /* CURRENT_FH: directory */
           component4      target;
   };

   struct REMOVE4resok {
           change_info4    cinfo;
   };

   union REMOVE4res switch (nfsstat4 status) {
    case NFS4_OK:
            REMOVE4resok   resok4;
    default:
            void;
   };

   /*

    * RENAME: Rename directory entry
    */
   struct RENAME4args {
           /* SAVED_FH: source directory */
           component4      oldname;
           /* CURRENT_FH: target directory */

           component4      newname;
   };

   struct RENAME4resok {
           change_info4    source_cinfo;
           change_info4    target_cinfo;
   };

   union RENAME4res switch (nfsstat4 status) {
    case NFS4_OK:
           RENAME4resok    resok4;
    default:
           void;
   };

   /*
    * RENEW: Renew a Lease
    */
   struct RENEW4args {
           clientid4       clientid;
   };

   struct RENEW4res {
           nfsstat4        status;
   };

   /*
    * RESTOREFH: Restore saved filehandle
    */

   struct RESTOREFH4res {
           /* CURRENT_FH: value of saved fh */
           nfsstat4        status;
   };

   /*
    * SAVEFH: Save current filehandle
    */
   struct SAVEFH4res {
           /* SAVED_FH: value of current fh */
           nfsstat4        status;

   };

   /*
    * SECINFO: Obtain Available Security Mechanisms
    */
   struct SECINFO4args {
           /* CURRENT_FH: directory */
           component4      name;
   };

   /*

    * From RFC 2203
    */
   enum rpc_gss_svc_t {
           RPC_GSS_SVC_NONE        = 1,
           RPC_GSS_SVC_INTEGRITY   = 2,
           RPC_GSS_SVC_PRIVACY     = 3
   };

   struct rpcsec_gss_info {
           sec_oid4        oid;
           qop4            qop;
           rpc_gss_svc_t   service;
   };

OKAY the OPEN stuff is literally so layered ๐Ÿ˜ญ๐Ÿซถ The OPEN4args struct, the open_claim4 union, and the delegation unions all snap together like a puzzle. Ngl the CLAIM_PREVIOUS vs CLAIM_DELEGATE_PREV naming is giving confusing energy ๐Ÿ’…

   /* RPCSEC_GSS has a value of '6' - See RFC 2203 */
   union secinfo4 switch (uint32_t flavor) {
    case RPCSEC_GSS:
            rpcsec_gss_info        flavor_info;
    default:
            void;
   };

   typedef secinfo4 SECINFO4resok<>;

   union SECINFO4res switch (nfsstat4 status) {
    case NFS4_OK:
            SECINFO4resok resok4;
    default:
            void;
   };

   /*
    * SETATTR: Set attributes
    */
   struct SETATTR4args {
           /* CURRENT_FH: target object */
           stateid4        stateid;
           fattr4          obj_attributes;
   };

   struct SETATTR4res {
           nfsstat4        status;
           bitmap4         attrsset;
   };

   /*
    * SETCLIENTID
    */
   struct SETCLIENTID4args {
           nfs_client_id4  client;
           cb_client4      callback;
           uint32_t        callback_ident;

   };

   struct SETCLIENTID4resok {
           clientid4       clientid;
           verifier4       setclientid_confirm;
   };

   union SETCLIENTID4res switch (nfsstat4 status) {
    case NFS4_OK:
            SETCLIENTID4resok      resok4;
    case NFS4ERR_CLID_INUSE:
            clientaddr4    client_using;
    default:
            void;
   };

   struct SETCLIENTID_CONFIRM4args {
           clientid4       clientid;
           verifier4       setclientid_confirm;
   };

   struct SETCLIENTID_CONFIRM4res {
           nfsstat4        status;
   };

   /*
    * VERIFY: Verify attributes same
    */
   struct VERIFY4args {
           /* CURRENT_FH: object */
           fattr4          obj_attributes;

   };

   struct VERIFY4res {
           nfsstat4        status;
   };

   /*
    * WRITE: Write to file
    */
   enum stable_how4 {
           UNSTABLE4       = 0,
           DATA_SYNC4      = 1,
           FILE_SYNC4      = 2
   };

   struct WRITE4args {
           /* CURRENT_FH: file */
           stateid4        stateid;
           offset4         offset;
           stable_how4     stable;
           opaque          data<>;
   };

   struct WRITE4resok {
           count4          count;
           stable_how4     committed;
           verifier4       writeverf;
   };

   union WRITE4res switch (nfsstat4 status) {
    case NFS4_OK:
            WRITE4resok    resok4;
    default:
            void;
   };

   /*
    * RELEASE_LOCKOWNER: Notify server to release lockowner
    */
   struct RELEASE_LOCKOWNER4args {
           lock_owner4     lock_owner;
   };

   struct RELEASE_LOCKOWNER4res {
           nfsstat4        status;
   };

   /*

    * ILLEGAL: Response for illegal operation numbers
    */
   struct ILLEGAL4res {
           nfsstat4        status;
   };

   /*
    * Operation arrays
    */

   enum nfs_opnum4 {
           OP_ACCESS               = 3,
           OP_CLOSE                = 4,
           OP_COMMIT               = 5,
           OP_CREATE               = 6,
           OP_DELEGPURGE           = 7,
           OP_DELEGRETURN          = 8,
           OP_GETATTR              = 9,
           OP_GETFH                = 10,
           OP_LINK                 = 11,
           OP_LOCK                 = 12,
           OP_LOCKT                = 13,
           OP_LOCKU                = 14,
           OP_LOOKUP               = 15,
           OP_LOOKUPP              = 16,
           OP_NVERIFY              = 17,
           OP_OPEN                 = 18,
           OP_OPENATTR             = 19,
           OP_OPEN_CONFIRM         = 20,
           OP_OPEN_DOWNGRADE       = 21,
           OP_PUTFH                = 22,
           OP_PUTPUBFH             = 23,
           OP_PUTROOTFH            = 24,
           OP_READ                 = 25,
           OP_READDIR              = 26,
           OP_READLINK             = 27,
           OP_REMOVE               = 28,
           OP_RENAME               = 29,
           OP_RENEW                = 30,
           OP_RESTOREFH            = 31,
           OP_SAVEFH               = 32,
           OP_SECINFO              = 33,
           OP_SETATTR              = 34,
           OP_SETCLIENTID          = 35,
           OP_SETCLIENTID_CONFIRM  = 36,
           OP_VERIFY               = 37,
           OP_WRITE                = 38,
           OP_RELEASE_LOCKOWNER    = 39,
           OP_ILLEGAL              = 10044
   };

   union nfs_argop4 switch (nfs_opnum4 argop) {
    case OP_ACCESS:        ACCESS4args opaccess;
    case OP_CLOSE:         CLOSE4args opclose;
    case OP_COMMIT:        COMMIT4args opcommit;
    case OP_CREATE:        CREATE4args opcreate;
    case OP_DELEGPURGE:    DELEGPURGE4args opdelegpurge;
    case OP_DELEGRETURN:   DELEGRETURN4args opdelegreturn;
    case OP_GETATTR:       GETATTR4args opgetattr;
    case OP_GETFH:         void;
    case OP_LINK:          LINK4args oplink;
    case OP_LOCK:          LOCK4args oplock;
    case OP_LOCKT:         LOCKT4args oplockt;
    case OP_LOCKU:         LOCKU4args oplocku;
    case OP_LOOKUP:        LOOKUP4args oplookup;
    case OP_LOOKUPP:       void;
    case OP_NVERIFY:       NVERIFY4args opnverify;
    case OP_OPEN:          OPEN4args opopen;
    case OP_OPENATTR:      OPENATTR4args opopenattr;
    case OP_OPEN_CONFIRM:  OPEN_CONFIRM4args opopen_confirm;
    case OP_OPEN_DOWNGRADE:        OPEN_DOWNGRADE4args opopen_downgrade;
    case OP_PUTFH:         PUTFH4args opputfh;
    case OP_PUTPUBFH:      void;
    case OP_PUTROOTFH:     void;
    case OP_READ:          READ4args opread;
    case OP_READDIR:       READDIR4args opreaddir;
    case OP_READLINK:      void;
    case OP_REMOVE:        REMOVE4args opremove;
    case OP_RENAME:        RENAME4args oprename;
    case OP_RENEW:         RENEW4args oprenew;
    case OP_RESTOREFH:     void;
    case OP_SAVEFH:        void;
    case OP_SECINFO:       SECINFO4args opsecinfo;
    case OP_SETATTR:       SETATTR4args opsetattr;
    case OP_SETCLIENTID:   SETCLIENTID4args opsetclientid;
    case OP_SETCLIENTID_CONFIRM:   SETCLIENTID_CONFIRM4args
                                           opsetclientid_confirm;
    case OP_VERIFY:        VERIFY4args opverify;
    case OP_WRITE:         WRITE4args opwrite;
    case OP_RELEASE_LOCKOWNER:     RELEASE_LOCKOWNER4args
                                       oprelease_lockowner;
    case OP_ILLEGAL:       void;
   };

   union nfs_resop4 switch (nfs_opnum4 resop){
    case OP_ACCESS:        ACCESS4res opaccess;
    case OP_CLOSE:         CLOSE4res opclose;
    case OP_COMMIT:        COMMIT4res opcommit;
    case OP_CREATE:        CREATE4res opcreate;
    case OP_DELEGPURGE:    DELEGPURGE4res opdelegpurge;
    case OP_DELEGRETURN:   DELEGRETURN4res opdelegreturn;
    case OP_GETATTR:       GETATTR4res opgetattr;
    case OP_GETFH:         GETFH4res opgetfh;
    case OP_LINK:          LINK4res oplink;
    case OP_LOCK:          LOCK4res oplock;
    case OP_LOCKT:         LOCKT4res oplockt;
    case OP_LOCKU:         LOCKU4res oplocku;
    case OP_LOOKUP:        LOOKUP4res oplookup;
    case OP_LOOKUPP:       LOOKUPP4res oplookupp;
    case OP_NVERIFY:       NVERIFY4res opnverify;
    case OP_OPEN:          OPEN4res opopen;
    case OP_OPENATTR:      OPENATTR4res opopenattr;
    case OP_OPEN_CONFIRM:  OPEN_CONFIRM4res opopen_confirm;
    case OP_OPEN_DOWNGRADE:        OPEN_DOWNGRADE4res opopen_downgrade;
    case OP_PUTFH:         PUTFH4res opputfh;
    case OP_PUTPUBFH:      PUTPUBFH4res opputpubfh;
    case OP_PUTROOTFH:     PUTROOTFH4res opputrootfh;
    case OP_READ:          READ4res opread;
    case OP_READDIR:       READDIR4res opreaddir;
    case OP_READLINK:      READLINK4res opreadlink;
    case OP_REMOVE:        REMOVE4res opremove;
    case OP_RENAME:        RENAME4res oprename;
    case OP_RENEW:         RENEW4res oprenew;
    case OP_RESTOREFH:     RESTOREFH4res oprestorefh;
    case OP_SAVEFH:        SAVEFH4res opsavefh;
    case OP_SECINFO:       SECINFO4res opsecinfo;
    case OP_SETATTR:       SETATTR4res opsetattr;
    case OP_SETCLIENTID:   SETCLIENTID4res opsetclientid;
    case OP_SETCLIENTID_CONFIRM:   SETCLIENTID_CONFIRM4res
                                           opsetclientid_confirm;
    case OP_VERIFY:        VERIFY4res opverify;
    case OP_WRITE:         WRITE4res opwrite;
    case OP_RELEASE_LOCKOWNER:     RELEASE_LOCKOWNER4res
                                       oprelease_lockowner;
    case OP_ILLEGAL:       ILLEGAL4res opillegal;
   };

   struct COMPOUND4args {
           utf8str_cs      tag;
           uint32_t        minorversion;
           nfs_argop4      argarray<>;
   };

   struct COMPOUND4res {

           nfsstat4 status;
           utf8str_cs      tag;
           nfs_resop4      resarray<>;
   };

   /*
    * Remote file service routines
    */
   program NFS4_PROGRAM {
           version NFS_V4 {
                   void
                           NFSPROC4_NULL(void) = 0;

                   COMPOUND4res
                           NFSPROC4_COMPOUND(COMPOUND4args) = 1;

           } = 4;
   } = 100003;



   /*
    * NFS4 Callback Procedure Definitions and Program
    */

   /*
    * CB_GETATTR: Get Current Attributes
    */
   struct CB_GETATTR4args {
           nfs_fh4 fh;
           bitmap4 attr_request;
   };

   struct CB_GETATTR4resok {
           fattr4  obj_attributes;
   };

   union CB_GETATTR4res switch (nfsstat4 status) {
    case NFS4_OK:
            CB_GETATTR4resok       resok4;
    default:
            void;
   };

   /*
    * CB_RECALL: Recall an Open Delegation
    */
   struct CB_RECALL4args {

           stateid4        stateid;
           bool            truncate;
           nfs_fh4         fh;
   };

   struct CB_RECALL4res {
           nfsstat4        status;
   };

   /*
    * CB_ILLEGAL: Response for illegal operation numbers
    */
   struct CB_ILLEGAL4res {
           nfsstat4        status;
   };

   /*
    * Various definitions for CB_COMPOUND
    */
   enum nfs_cb_opnum4 {
           OP_CB_GETATTR           = 3,
           OP_CB_RECALL            = 4,
           OP_CB_ILLEGAL           = 10044
   };

   union nfs_cb_argop4 switch (unsigned argop) {
    case OP_CB_GETATTR:    CB_GETATTR4args opcbgetattr;
    case OP_CB_RECALL:     CB_RECALL4args  opcbrecall;
    case OP_CB_ILLEGAL:    void;
   };

   union nfs_cb_resop4 switch (unsigned resop){
    case OP_CB_GETATTR:    CB_GETATTR4res  opcbgetattr;
    case OP_CB_RECALL:     CB_RECALL4res   opcbrecall;
    case OP_CB_ILLEGAL:    CB_ILLEGAL4res  opcbillegal;
   };

   struct CB_COMPOUND4args {
           utf8str_cs      tag;
           uint32_t        minorversion;
           uint32_t        callback_ident;
           nfs_cb_argop4   argarray<>;
   };

   struct CB_COMPOUND4res {
           nfsstat4 status;
           utf8str_cs      tag;
           nfs_cb_resop4   resarray<>;

   };


   /*
    * Program number is in the transient range since the client
    * will assign the exact transient program number and provide
    * that to the server via the SETCLIENTID operation.
    */
   program NFS4_CALLBACK {
           version NFS_CB {
                   void
                           CB_NULL(void) = 0;
                   CB_COMPOUND4res
                           CB_COMPOUND(CB_COMPOUND4args) = 1;
           } = 1;
   } = 0x40000000;

19. Acknowledgements โœจ

Okay, big thank-you time, bestie ๐Ÿซถ The authors want to shout out everyone who helped make this happen:

  • Neil Brown ๐Ÿ’–, for his extensive review and comments on various documents.
  • Rick Macklem at the University of Guelph ๐ŸŽ“, who gets a big thanks too.
  • Mike Frisch, Sergey Klyushin, and Dan Trufasiu of Hummingbird Ltd. ๐ŸŒธ, for their implementation efforts and feedback on the protocol specification.
  • Andy Adamson, Bruce Fields, Jim Rees, and Kendrick Smith from the CITI organization at the University of Michigan ๐Ÿฆ‹, also for their implementation efforts and feedback on the protocol specification.
  • Mike Kupfer ๐Ÿ”ฅ, for his review of the file locking and ACL mechanisms.
  • Alan Yoder โญ, for his input to ACL mechanisms.
  • Peter Astrand ๐Ÿงš, for his close review of the protocol specification.
  • Ran Atkinson ๐ŸŒˆ, for his constant reminder that users do matter. Iconic ๐Ÿ‘‘

20. Normative References โœจ

okay bestie, these are the docs you have to read before you can implement any of this ๐Ÿ“š they're the receipts ๐Ÿงพ the spec leans on, and every one of them is listed here with all its details ๐Ÿ’…

  • [ISO10646] "ISO/IEC 10646-1:1993. International Standard -- Information technology -- Universal Multiple-Octet Coded Character Set (UCS) -- Part 1: Architecture and Basic Multilingual Plane."
  • [RFC793] Postel, J., "Transmission Control Protocol", STD 7, RFC 793, September 1981.
  • [RFC1831] Srinivasan, R., "RPC: Remote Procedure Call Protocol Specification Version 2", RFC 1831, August 1995.
  • [RFC1832] Srinivasan, R., "XDR: External Data Representation Standard", RFC 1832, August 1995.
  • [RFC2373] Hinden, R. and S. Deering, "IP Version 6 Addressing Architecture", RFC 2373, July 1998.
  • [RFC1964] Linn, J., "The Kerberos Version 5 GSS-API Mechanism", RFC 1964, June 1996.
  • [RFC2025] Adams, C., "The Simple Public-Key GSS-API Mechanism (SPKM)", RFC 2025, October 1996.
  • [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997.
  • [RFC2203] Eisler, M., Chiu, A. and L. Ling, "RPCSEC_GSS Protocol Specification", RFC 2203, September 1997.
  • [RFC2277] Alvestrand, H., "IETF Policy on Character Sets and Languages", BCP 19, RFC 2277, January 1998.
  • [RFC2279] Yergeau, F., "UTF-8, a transformation format of ISO 10646", RFC 2279, January 1998.
  • [RFC2623] Eisler, M., "NFS Version 2 and Version 3 Security Issues and the NFS Protocol's Use of RPCSEC_GSS and Kerberos V5", RFC 2623, June 1999.
  • [RFC2743] Linn, J., "Generic Security Service Application Program Interface, Version 2, Update 1", RFC 2743, January 2000.
  • [RFC2847] Eisler, M., "LIPKEY - A Low Infrastructure Public Key Mechanism Using SPKM", RFC 2847, June 2000.
  • [RFC3010] Shepler, S., Callaghan, B., Robinson, D., Thurlow, R., Beame, C., Eisler, M. and D. Noveck, "NFS version 4 Protocol", RFC 3010, December 2000.
  • [RFC3454] Hoffman, P. and P. Blanchet, "Preparation of Internationalized Strings ("stringprep")", RFC 3454, December 2002.
  • [Unicode1] The Unicode Consortium, "The Unicode Standard, Version 3.0", Addison-Wesley Developers Press, Reading, MA, 2000. ISBN 0-201-61633-5.
  • [Unicode2] "Unsupported Scripts" Unicode, Inc., The Unicode Consortium, P.O. Box 700519, San Jose, CA 95710-0519 USA, September 1999. http://www.unicode.org/unicode/standard/unsupported.html

21. Informative References ๐Ÿ“š

these ones are the background reading ๐Ÿง  super helpful for the vibes and the history, but the spec doesn't require them to work, so no stress ๐Ÿซถ

  • [Floyd] S. Floyd, V. Jacobson, "The Synchronization of Periodic Routing Messages," IEEE/ACM Transactions on Networking, 2(2), pp. 122-136, April 1994.
  • [Gray] C. Gray, D. Cheriton, "Leases: An Efficient Fault-Tolerant Mechanism for Distributed File Cache Consistency," Proceedings of the Twelfth Symposium on Operating Systems Principles, p. 202-210, December 1989.
  • [Juszczak] Juszczak, Chet, "Improving the Performance and Correctness of an NFS Server," USENIX Conference Proceedings, USENIX Association, Berkeley, CA, June 1990, pages 53-63. Describes reply cache implementation that avoids work in the server by handling duplicate requests. More important, though listed as a side-effect, the reply cache aids in the avoidance of destructive non-idempotent operation re-application -- improving correctness.
  • [Kazar] Kazar, Michael Leon, "Synchronization and Caching Issues in the Andrew File System," USENIX Conference Proceedings, USENIX Association, Berkeley, CA, Dallas Winter 1988, pages 27-36. A description of the cache consistency scheme in AFS. Contrasted with other distributed file systems.
  • [Macklem] Macklem, Rick, "Lessons Learned Tuning the 4.3BSD Reno Implementation of the NFS Protocol," Winter USENIX Conference Proceedings, USENIX Association, Berkeley, CA, January 1991. Describes performance work in tuning the 4.3BSD Reno NFS implementation. Describes performance improvement (reduced CPU loading) through elimination of data copies.
  • [Mogul] Mogul, Jeffrey C., "A Recovery Protocol for Spritely NFS," USENIX File System Workshop Proceedings, Ann Arbor, MI, USENIX Association, Berkeley, CA, May 1992. Second paper on Spritely NFS proposes a lease-based scheme for recovering state of consistency protocol.
  • [Nowicki] Nowicki, Bill, "Transport Issues in the Network File System," ACM SIGCOMM newsletter Computer Communication Review, April 1989. A brief description of the basis for the dynamic retransmission work.
  • [Pawlowski] Pawlowski, Brian, Ron Hixon, Mark Stein, Joseph Tumminaro, "Network Computing in the UNIX and IBM Mainframe Environment," Uniforum `89 Conf. Proc., (1989) Description of an NFS server implementation for IBM's MVS operating system.
  • [RFC1094] Sun Microsystems, Inc., "NFS: Network File System Protocol Specification", RFC 1094, March 1989.
  • [RFC1345] Simonsen, K., "Character Mnemonics & Character Sets", RFC 1345, June 1992.
  • [RFC1813] Callaghan, B., Pawlowski, B. and P. Staubach, "NFS Version 3 Protocol Specification", RFC 1813, June 1995.
  • [RFC3232] Reynolds, J., Editor, "Assigned Numbers: RFC 1700 is Replaced by an On-line Database", RFC 3232, January 2002.
  • [RFC1833] Srinivasan, R., "Binding Protocols for ONC RPC Version 2", RFC 1833, August 1995.
  • [RFC2054] Callaghan, B., "WebNFS Client Specification", RFC 2054, October 1996.
  • [RFC2055] Callaghan, B., "WebNFS Server Specification", RFC 2055, October 1996.
  • [RFC2152] Goldsmith, D. and M. Davis, "UTF-7 A Mail-Safe Transformation Format of Unicode", RFC 2152, May 1997.
  • [RFC2224] Callaghan, B., "NFS URL Scheme", RFC 2224, October 1997.
  • [RFC2624] Shepler, S., "NFS Version 4 Design Considerations", RFC 2624, June 1999.
  • [RFC2755] Chiu, A., Eisler, M. and B. Callaghan, "Security Negotiation for WebNFS" , RFC 2755, June 2000.
  • [Sandberg] Sandberg, R., D. Goldberg, S. Kleiman, D. Walsh, B. Lyon, "Design and Implementation of the Sun Network Filesystem," USENIX Conference Proceedings, USENIX Association, Berkeley, CA, Summer 1985. The basic paper describing the SunOS implementation of the NFS version 2 protocol, and discusses the goals, protocol specification and trade-offs.
  • [Srinivasan] Srinivasan, V., Jeffrey C. Mogul, "Spritely NFS: Implementation and Performance of Cache Consistency Protocols", WRL Research Report 89/5, Digital Equipment Corporation Western Research Laboratory, 100 Hamilton Ave., Palo Alto, CA, 94301, May 1989. This paper analyzes the effect of applying a Sprite-like consistency protocol applied to standard NFS. The issues of recovery in a stateful environment are covered in [Mogul].
  • [XNFS] The Open Group, Protocols for Interworking: XNFS, Version 3W, The Open Group, 1010 El Camino Real Suite 380, Menlo Park, CA 94025, ISBN 1-85912-184-5, February 1998.

22. Authors' Information ๐ŸŽ€

and here's who wrote the thing ๐Ÿซถ the editor's contact info is right below, in case you want to say thanks ๐Ÿ’œ

22.1. Editor's Address ๐Ÿ’–

   Spencer Shepler
   Sun Microsystems, Inc.
   7808 Moonflower Drive
   Austin, Texas  78750

   Phone: +1 512-349-9376
   EMail: spencer.shepler@sun.com

22.2. Authors' Addresses ๐Ÿ’Œ

okay so here are the authors' contact deets, bestie ๐Ÿซถ I've kept every email, phone number, and address exactly as they were:

Carl Beame
Hummingbird Ltd.

EMail: beame@bws.com
Brent Callaghan
Sun Microsystems, Inc.
17 Network Circle
Menlo Park, CA  94025

Phone: +1 650-786-5067
EMail: brent.callaghan@sun.com
Mike Eisler
5765 Chase Point Circle
Colorado Springs, CO  80919

Phone: +1 719-599-9026
EMail: mike@eisler.com
David Noveck
Network Appliance
375 Totten Pond Road
Waltham, MA  02451

Phone: +1 781-768-5347
EMail: dnoveck@netapp.com
David Robinson
Sun Microsystems, Inc.
5300 Riata Park Court
Austin, TX  78727

Phone: +1 650-786-5088
EMail: david.robinson@sun.com
Robert Thurlow
Sun Microsystems, Inc.
500 Eldorado Blvd.
Broomfield, CO  80021

Phone: +1 650-786-5096
EMail: robert.thurlow@sun.com

Legal time, but it's still cute, I promise ๐ŸŽ€

The copyright is held by The Internet Society, and it's Copyright (C) The Internet Society (2003). All Rights Reserved. ๐Ÿ’œ

Here's the deal: you're allowed to copy this document and share it with others, and you can write derivative works that comment on it, explain it, or help people implement it. You can publish and distribute those, in whole or in part, with no real restrictions, as long as the copyright notice and this paragraph stay on every copy and derivative work. โœจ

BUT the document itself can't be changed in any way. That includes removing the copyright notice or the references to the Internet Society or other Internet organizations. The one exception is when you need to develop Internet standards, and then you have to follow the copyright procedures in the Internet Standards process. You're also allowed to translate it into languages other than English. ๐ŸŒธ

The permissions listed above are perpetual, meaning the Internet Society or its successors or assigns can't take them back. ๐Ÿฆ‹

The document and everything in it is provided "AS IS", and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. ๐Ÿ’€

Acknowledgement ๐Ÿ™Œ

Funding for the RFC Editor function is currently provided by the Internet Society. ๐Ÿ’–

๐Ÿ“ฆ where this page comes from