RFC 3530
Network File System (NFS) version 4 Protocol
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 Notice ๐ฅ
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:
- Media commit of data, that is, the modified data has been successfully written to the disk media, for example, the disk platter.
- An immediate reply disk drive with battery-backed on-drive intermediate storage or uninterruptible power system (UPS).
- Server commit of data with battery-backed intermediate storage and recovery software.
- 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 openFH4_VOL_MIGRATION: the filehandle will expire as a result of migration ๐ฆ if FH4_VOL_ANY is set, FH4_VOL_MIGRATION is redundantFH4_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]
slotis an index in the server volatile filehandle tablegeneration numberis 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 โจ
5.2. Recommended Attributes ๐ฆ
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).
5.6. Recommended Attributes - Definitions ๐
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 withNFS4ERR_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 thanname_maxis 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'tNF4BLKorNF4CHR, 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 forSETATTRuse 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 UNIXctime("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 forSETATTRuse only ๐mounted_on_fileid(55,uint64, READ) ๐ณ: likefileid, except that if the target filehandle is the root of a filesystem, it returns thefileidof 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
idstring 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
idstring, there's a real risk. after the client gives up that network address, another client using a similar algorithm could generate a conflictingidstring ๐
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_STATEIDshould 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_STATEIDshould 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_STATEIDshould 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_PREVIOUSorCLAIM_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:
- Client A acquires a lock.
- Client A and server experience a mutual network partition, so client A is unable to renew its lease.
- Client A's lease expires, so the server releases the lock.
- Client B acquires a lock that would have conflicted with that of Client A.
- Client B releases the lock.
- Server reboots.
- The network partition between client A and server heals.
- Client A issues a
RENEWoperation and gets backNFS4ERR_STALE_CLIENTID. - 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:
- Client A acquires a lock.
- Server reboots.
- Client A and server experience a mutual network partition, so client A is unable to reclaim its lock within the grace period.
- The server's reclaim grace period ends. Client A has no locks recorded on the server.
- Client B acquires a lock that would have conflicted with that of Client A.
- Client B releases the lock.
- Server reboots a second time.
- The network partition between client A and server heals.
- Client A issues a
RENEWoperation and gets backNFS4ERR_STALE_CLIENTID. - 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):
- 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. ๐ค - 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 anNFS4ERR_LEASE_MOVEDerror, it should perform an operation on each filesystem associated with the server in question. once the client gets anNFS4ERR_MOVEDerror, 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 eitherNFS4ERR_STALE_CLIENTIDorNFS4ERR_STALE_STATEIDfrom 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
nfsace4specifying 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
nfsace4indicates that the open may be done, then it should be granted without reference to the server ๐. - If the
nfsace4indicates 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
RENEWoperation and the server has returned anNFS4ERR_CB_PATH_DOWNerror. 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
RENEWoperation 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 thelease_timeattribute. ๐ฆ
- The client has issued a
- 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 useRENEWto 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
changeto figure out whether its cache is stale. A server that knows the file is memory mapped could always pessimistically return updated values forchange, 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_accessattribute. 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 ๐.
- 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 ๐งฑ.
- 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 ๐ฉ.
- 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
- Minor versions may not modify the structure of existing attributes.
- Minor versions may not delete operations.
This prevents the potential reuse of a particular operation "slot" in a future minor version ๐.
- Minor versions may not delete attributes.
- Minor versions may not delete flag bits or enumeration values.
- 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.
- Minor versions may downgrade features from mandatory to recommended, or recommended to optional.
- Minor versions may upgrade features from optional to recommended or recommended to mandatory.
- A client and server that support minor version X must support minor versions 0 (zero) through X-1 as well ๐ค.
- 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 ๐งช.
- 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 ๐
11.4. UTF-8 Related Errors
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_ACCESSNFS4ERR_BADHANDLENFS4ERR_BADXDRNFS4ERR_DELAYNFS4ERR_FHEXPIREDNFS4ERR_INVALNFS4ERR_IONFS4ERR_MOVEDNFS4ERR_NOFILEHANDLENFS4ERR_RESOURCENFS4ERR_SERVERFAULTNFS4ERR_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
14.2.9. Operation 11: LINK - Create Link to a File ๐
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_REVOKEDNFS4ERR_BADHANDLENFS4ERR_BAD_SEQIDNFS4ERR_BAD_STATEIDNFS4ERR_BADXDRNFS4ERR_EXPIREDNFS4ERR_FHEXPIREDNFS4ERR_INVALNFS4ERR_ISDIRNFS4ERR_MOVEDNFS4ERR_NOFILEHANDLENFS4ERR_OLD_STATEIDNFS4ERR_RESOURCENFS4ERR_SERVERFAULTNFS4ERR_STALENFS4ERR_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_REVOKEDNFS4ERR_BADHANDLENFS4ERR_BAD_SEQIDNFS4ERR_BAD_STATEIDNFS4ERR_BADXDRNFS4ERR_EXPIREDNFS4ERR_FHEXPIREDNFS4ERR_INVALNFS4ERR_MOVEDNFS4ERR_NOFILEHANDLENFS4ERR_OLD_STATEIDNFS4ERR_RESOURCENFS4ERR_SERVERFAULTNFS4ERR_STALENFS4ERR_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_BADHANDLENFS4ERR_BADXDRNFS4ERR_FHEXPIREDNFS4ERR_MOVEDNFS4ERR_RESOURCENFS4ERR_SERVERFAULTNFS4ERR_STALENFS4ERR_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_RESOURCENFS4ERR_SERVERFAULTNFS4ERR_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_RESOURCENFS4ERR_SERVERFAULTNFS4ERR_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
14.2.25. Operation 27: READLINK - Read Symbolic Link ๐
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_WRITEorOPEN4_SHARE_DENY_BOTH. - If the file was not opened with
OPEN4_SHARE_DENY_WRITEorOPEN4_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:
xis the value of the client.id subfield of the SETCLIENTID4args structure.vis the value of the client.verifier subfield of the SETCLIENTID4args structure.cis the value of the clientid field returned in the SETCLIENTID4resok structure.krepresents the value combination of the callback and callback_ident fields of the SETCLIENTID4args structure.sis 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 }, wherelmay or may not equalk. If so, and since the id verifiervof 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 thatt != s. It doesn't matter whetherkequalslor 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 thatv != u, wherelmay or may not equalk, and it has not recorded any unconfirmed{ *, x, *, *, * }record forx. The server records an unconfirmed{ v, x, d, k, t }(whered != candt != 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 thatv != u, wherelmay or may not equalk, and it recorded an unconfirmed{ w, x, d, m, t }record such thatc != dandt != s. Heremmay or may not equalk,mmay or may not equall, andkmay or may not equall. Whetherw == vorw != vmakes 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 thate != d,e != c,r != t, andr != 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, *, *, * }forx. It may or may not have recorded an unconfirmed{ u, x, c, l, s }, wherelmay or may not equalk, andumay or may not equalv. Any unconfirmed record{ u, x, c, l, * }, regardless of whetheru == vorl == k, is replaced with an unconfirmed record{ v, x, d, k, t }whered != candt != 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_BADXDRNFS4ERR_CLID_INUSENFS4ERR_INVALNFS4ERR_RESOURCENFS4ERR_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 toSETCLIENTID) and a newcallback_ident(as specified in the arguments toSETCLIENTID) value. The client's use ofSETCLIENTID_CONFIRMin 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 theCLAIM_DELEGATE_PREVclaim type, delegations. If the server supportsCLAIM_DELEGATE_PREV, thenSETCLIENTID_CONFIRMMUST 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_identvalue. The client's use ofSETCLIENTID_CONFIRMin 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_identinformation for client{ x }as represented by the shorthand valuec.
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 thats != t. If the principals of the records do not match that of theSETCLIENTID_CONFIRM, the server returnsNFS4ERR_CLID_INUSE, and no relevant leased client state is removed and no recorded callback andcallback_identinformation 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 andcallback_identinformation 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 ofSETCLIENTID_CONFIRMdo not match, the server returnsNFS4ERR_CLID_INUSEwithout removing any relevant leased client state and without changing recorded callback andcallback_identvalues 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 firstSETCLIENTID_CONFIRMattempt 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 theSETCLIENTID_CONFIRMrequest the server returnsNFS4ERR_CLID_INUSEwithout 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 returnsNFS4ERR_STALE_CLIENTID. The server does not remove any relevant leased client state, nor does it modify any recorded callback andcallback_identinformation 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_BADXDRNFS4ERR_CLID_INUSENFS4ERR_RESOURCENFS4ERR_SERVERFAULTNFS4ERR_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_ACCESSNFS4ERR_ATTRNOTSUPPNFS4ERR_BADCHARNFS4ERR_BADHANDLENFS4ERR_BADXDRNFS4ERR_DELAYNFS4ERR_FHEXPIREDNFS4ERR_INVALNFS4ERR_MOVEDNFS4ERR_NOFILEHANDLENFS4ERR_NOT_SAMENFS4ERR_RESOURCENFS4ERR_SERVERFAULTNFS4ERR_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:
- Repeated power failures.
- Hardware failures (of any board, power supply, etc.).
- 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_BADHANDLENFS4ERR_BAD_STATEIDNFS4ERR_BADXDRNFS4ERR_OP_ILLEGALNFS4ERR_RESOURCENFS4ERR_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_BADHANDLENFS4ERR_BADXDRNFS4ERR_RESOURCENFS4ERR_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_BADHANDLENFS4ERR_BAD_STATEIDNFS4ERR_BADXDRNFS4ERR_RESOURCENFS4ERR_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.
- More information available at:
http://www.unicode.org/
- More information available at:
- [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.
- HTML version available:
http://www.opengroup.org
- HTML version available:
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
23. Full Copyright Statement โญ
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
- ietf: rfc3530 2003-04
- ๐ girlified by claude-haiku-5-5 on : a language model's rewrite of the original, under its terms