Skip to content

v0.3.0

Choose a tag to compare

@sirrobot01 sirrobot01 released this 31 Jul 02:08
· 11 commits to main since this release

Makes the NFS package usable from a Linux client, which v0.2.0 was not, and roughly quadruples the speed of serving a real directory.

Fixed: a Linux client could not write at all

Linux creates an ordinary file with the EXCLUSIVE4 open mode, which v0.2.0 refused with NFS4ERR_NOTSUPP. Every write from a Linux client failed, so the export was effectively read-only. The mode is now served from a table of create verifiers, which is what tells a retransmitted create apart from a second create of the same name.

No in-process test found this. Mounting from a real client did.

New

  • facetfs.OpenDir returns a *facetfs.Root that serves a directory tree from a handle it holds open. facetfs.Dir reopened the tree on every operation, which cost two extra system calls each time. Prefer OpenDir in a server; Dir still works and now delegates to the same implementation.
  • facetfs.RemoveFS is an optional interface with a non-recursive Remove. NFS REMOVE and SFTP remove and rmdir use it when the filesystem provides it, so refusing a directory that holds entries is the filesystem's own answer rather than an emptiness check in front of a recursive delete. Dir and NewMemFS implement it.

Faster

Serving a real directory over NFS, against v0.2.0:

Operation v0.2.0 v0.3.0
GETATTR 172 us 42 us
READ 64 KiB 208 MB/s 798 MB/s
READ 1 MiB 1.7 GB/s 3.3 GB/s
Listing 4000 entries 21.8 ms 3.0 ms
Lease sweep, 1000 clients 12.9 us per request 49 ns

A READ copied its payload three times on the way out; the compound now encodes into the reply buffer and READ reads straight into it. Directory listings are served from a snapshot named by the cookie verifier, so a resume no longer re-reads the whole directory: the cost of a listing is now linear rather than quadratic, and one listing is stable while it runs. Reply buffers are reused between requests. The lease sweep no longer scans every client on every request.

Correctness

Found by review and by the client mounts, each with a regression test:

  • A LOCK could name one owner through both stateid slots and make the server lock a single mutex twice, hanging that owner permanently.
  • A LOCK that repeated one sequence id while advancing the other was answered from the replay cache, reporting a byte range the client never held.
  • A WRITE at the advertised maxwrite exceeded the record cap and closed the connection, so no full-size write could succeed.
  • RENAME applied no type rules, so a file could replace a directory and a directory could replace a file, destroying the target.
  • COMMIT and stable WRITE failed on any backend whose File lacks Sync, and COMMIT flushed a fresh handle rather than the file the client wrote through.
  • Lock and share state were keyed by path and did not follow a rename.
  • A truncating open needed SetStatFS, so a filesystem implementing only the core interface could not be truncated.
  • Conditions the io/fs sentinels cannot express, a full disk or a read-only export, all reached the client as a plain I/O error.
  • The special stateids skipped the share-reservation check.
  • READ and WRITE refused a transfer above the advertised size instead of doing a short one.

Hardening

Every variable-length decode already carried an explicit bound. The state a client can grow now does too: opens per client, which is what holds file descriptors, owners per client, lock ranges per owner, cached listings, and create verifiers. An RPC fragment is checked against the cap on its own as well as against the running total, which would overflow on a 32-bit platform.

CI gained a fuzz job over the XDR primitives and whole connections. make fuzz runs longer campaigns and make bench reports the numbers above.

Verified

Linux and macOS clients both mount and pass a file, directory, and listing workflow. Two processes contend for a byte-range lock through the server and observe the documented result. scripts/nfs-smoke-linux.sh needs only Docker; scripts/nfs-smoke-macos.sh needs root.

Status

The FileSystem contract is unchanged and remains frozen. webdav and sftp are unchanged apart from SFTP using RemoveFS.

nfs4 stays experimental. Two acceptance criteria are only partly met: a client that loses its connection with work in flight, and a lease that expires while another client waits on the lock it holds. Delegations are not implemented, so a client re-reading a file reaches the server every time.