Skip to content

feat(nfs): NFSv4.1 alongside NFSv3 - #7

Merged
pi0 merged 14 commits into
mainfrom
feat/nfsv4
Jul 29, 2026
Merged

feat(nfs): NFSv4.1 alongside NFSv3#7
pi0 merged 14 commits into
mainfrom
feat/nfsv4

Conversation

@pi0x

@pi0x pi0x commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

NFSv4.1 served from mountx/nfs beside NFSv3 — one port, one socket, one shared FileHandleTable; a client picks the version and nothing about a v3 mount changes.

  • Structure: src/nfs/v3/ (RFC 1813, moved intact — proven byte-identical across a 5,440-record differential against HEAD) and src/nfs/v4/ (RFC 8881, transcribed with sections named), with a thin version router in the shared layer peeking (prog, vers) at fixed offsets. v3/ and v4/ never import from each other; what both need lives in the shared layer.
  • The v4.1 half: the full 41-op COMPOUND codec both directions (byte-verified against independently-built decoders from RFC 5662's XDR — 352 constants + 78 + 121 op cases, 0 mismatches), the bitmap4/fattr4 codec (37 attributes), a pure state machine (EXCHANGE_ID → CREATE_SESSION → SEQUENCE with an exactly-once slot replay cache, courteous leases, share reservations, advisory byte-range locks, special/current stateids), and the session wiring it to any FsDriver — open-state driver handles, ID mapping (Nfs4IdMap), GRACE/RECLAIM_COMPLETE gating per §18.51.3.
  • Deliberately out: NFSv4.0, delegations (every OPEN declines), pNFS, backchannel use, RPCSEC_GSS — refused with the statuses §15.2 sanctions.
  • Surface: mountNfs({ version: "4.1" }) (Linux-only, default 3 unchanged), vers=4.1,proto=tcp,port=N mount options (no mountport, no nolock), nfsClientProbe().v4, NfsVersion. The conformance matrix gained an NFSv4.1 column at parity with v3 (122 passed / 4 skipped, same skip reasons); Tier-1 JS 4.1 client + deterministic session/socket fuzz included.

How it was built and verified

Twelve steps, each implemented by one agent and adversarially verified by an independent one before commit (mechanical RFC cross-checks, scenario suites up to 386 assertions, driver-handle leak audits, mutation testing, differential runs against HEAD). The verification rounds caught and fixed real defects pre-commit, including a wire-reachable crash on special stateids, a READDIR dircount MUST violation, and two factual errors in the docs. 1505 tests passing.

Still owed before release claims

  • A1 (assumed): macOS treated as 4.0-only, so v4.1 targets Linux — verify on a mac; adding a darwin branch later is additive.
  • A2 (assumed): a Linux client accepts a negotiated-but-unused backchannel — locally verifiable with a real vers=4.1 mount once the host gains mount.nfs (nfs-common); the Tier-2 v4.1 mount column is pending on that.
  • No perf claims are made for v4 anywhere; pnpm bench hasn't run a v4 column.

🤖 Generated with Claude Code

pi0 and others added 12 commits July 28, 2026 22:58
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… rpc.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…mapping

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mountx Ready Ready Preview, Comment Jul 29, 2026 8:25am

Request Review

Brings the 9P2000.L transport (mountx/9p) and the S3 gateway (mountx/s3)
onto the NFSv4.1 branch, plus main's three fixes (short-write completion
in the loopback harness, open files preserved across rename in the
unstorage driver, retained decoder bytes copied in the FUSE codec).

Conflicts and how each was resolved:

- AGENTS.md — woven, not picked: the intro names FUSE, 9P, NFS (v3 and
  v4.1) and the S3 gateway; the tests section keeps both the v3/v4 nfs/
  layout and main's s3/ entry; the wire-constants and FUSE_DESTROY
  invariants carry both the RFC 8881/5662 and the 9p.h clauses; the
  errno-discipline invariant now names all five reply shapes; the docs
  paragraph lists the 9P, NFS and S3 transport pages.
- test/matrix.ts — six columns, union of both COLUMNS arrays: loopback,
  FUSE, 9P, NFSv3, NFSv4.1, S3. The doc comment lists them in that order
  and the THROUGH_* list names THROUGH_NFS4 as well. Git treated this
  file as binary (the merge base carried a literal NUL that both sides
  independently replaced with the two-character "\0"), so it was merged
  by hand through git merge-file.
- .agents/conformance-matrix.md — generated, so regenerated with
  `pnpm matrix` after the merge rather than resolved by hand.
- src/nfs/mount.ts — main moved run()/describe()/errorMessage() into
  src/fuse/exec.ts and gave run() an options object; the v4 mount(8) call
  keeps this branch's `-t nfs` comment and takes main's { stdio: CAPTURE }.
- README.md — transports line names FUSE, 9P and NFS, and keeps main's
  S3 Gateway link.
- docs/.config/docs.yaml — description and the "one API, three transports"
  feature name FUSE, 9P and NFS.
- docs/1.guide/0.index.md — the mermaid transport node and the comparison
  table both gain the 9P column while keeping the NFS column's "NFSv3 or
  NFSv4.1 client".
- docs/1.guide/3.drivers/2.custom.md — the conformance suite now runs six
  ways (loopback, FUSE, 9P, NFSv3, NFSv4.1, S3).
- docs/1.guide/6.troubleshooting.md — the ESTALE answer keeps this
  branch's v4.1 explanation and main's "FUSE and 9P both keep it
  readable".
- docs/2.transports/0.index.md — six conflicts, all union: the mermaid
  graph gains the 9P and S3 nodes and the NFS node keeps NFSv4.1; the
  comparison table is the three-column one with the NFS column's v4.1
  wording and a state row; the prose keeps 9P's paragraph, NFS's
  two-versions paragraph and the stateless trade-off with v4.1's note.
- docs/2.transports/1.auto.md, 2.fuse.md, 3.reference/0.index.md — link
  lists and the subpath table carry mountx/9p, mountx/nfs (v3 + v4.1)
  and mountx/s3.

Semantic (non-textual) resolutions, found by grepping main's new files
for paths this branch moved:

- src/9p/protocol.ts, test/9p/{client,conformance,golden}.test.ts and
  test/s3/client.ts referenced src/nfs/protocol.ts, test/nfs/client.ts,
  test/nfs/conformance.test.ts and test/nfs/golden.test.ts, all of which
  moved under v3/ in 5dabd79. Repointed. (.agents/9p-plan.md and
  .agents/s3-plan.md keep the old paths deliberately — they are records
  of a plan as authored.)
- Checked and confirmed unchanged: mountx/auto stays NFSv3 (it never
  passes `version`, and nfsClientProbe's new `v4` field is not consulted
  by probeNfs); main's two new conformance cases run in the NFSv4.1
  column and skip there, since THROUGH_NFS4 declares handles: false.

pnpm lint, pnpm typecheck and pnpm build pass. pnpm matrix ran all six
columns (FUSE 130 passed under sudo, NFSv4.1 122 passed / 8 skipped).
vitest: 2808 passed, 233 skipped, 2 failed — the two failures are
main's own, reproduced verbatim at origin/main (test/s3/conformance
"keeps an open file attached across rename"), and are untouched here.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`test/s3/client.ts`'s `s3Driver` buffers an object whole while it is open,
one shared `OpenFile` per open path, exactly as `src/drivers/unstorage.ts`
does — and it had the same defect the unstorage driver had before #3: the
map was keyed by the path the file was opened under, so a `rename` orphaned
the buffer instead of moving it. The two conformance cases #3 added
("keeps an open file attached across rename" and "… across ancestor
rename") therefore failed in the S3 column, which is the only column that
was red.

Mirrors #3's fix method for method:

- `OpenFile` carries its own `path`, so a handle reads the name off the
  entry rather than closing over the one it was opened under.
- `flush()`/`release()`/`createFileHandle()` lose their `path` parameter;
  a failed flush re-marks the entry dirty and rethrows, and `removeClosed()`
  keeps a closed-but-dirty entry rather than dropping the only copy of
  bytes that never landed. `truncate` clears such an entry once its bytes
  do land.
- `movePaths(from, to)` refiles every open buffer for `from` and its
  subtree under `to`, scanning the map in full before rewriting any of it.
  `rename` calls it in both branches: the single-file one after the
  copy/delete pair (orphaning only the *displaced* destination, whose bytes
  must stay readable but must never be written back), and the directory one
  after `copyTree`/`removeTree`, which is what makes an ancestor rename
  invisible to an open handle.
- `removeTree()` no longer orphans the files it deletes. Its only caller is
  `rename`, which has already copied them elsewhere, so orphaning there
  would throw away the very entries about to be moved.

`handles: true` stays declared: the gateway honestly preserves the buffer,
and the matrix's capability-loss row for S3 is unchanged.

pnpm test: 2810 passed, 233 skipped, 0 failed (was 2808 / 233 / 2).
pnpm matrix S3 column: 48 passed, 17 skipped, 0 failed (was 46 / 17 / 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@pi0
pi0 merged commit 0f359e7 into main Jul 29, 2026
12 checks passed
pi0x pushed a commit that referenced this pull request Jul 29, 2026
Adds `docs/1.guide/5.vms.md`: serve a driver on the host, mount it inside
a VM with one command and nothing installed in the guest.

QEMU gets 9P over `trans=tcp`. Its default user-mode networking already
routes 10.0.2.2 to the host's loopback, so there is no tap device, no
bridge, no host-side `ip` command and no root on the host — the server
stays bound to 127.0.0.1. Verified end to end against an Alpine 3.21.3
`virt` guest (kernel 6.12.13-0-virt): mount with three options and no
`modprobe`, then ls/read/write/mkdir/dd/rename/unlink/umount.

Firecracker gets NFSv4.1, because it cannot do 9P at all: it emulates no
`virtio-9p` device, and its published guest kernels ship
`# CONFIG_NET_9P is not set`. Those kernels do carry NFSv4.1 (and not
NFSv3), which is exactly the half that landed in #7. The tap wiring that
Firecracker requires is not yet witnessed here, and the page says so.

Also records both in `.agents/environment.md`, and amends the NFS note in
AGENTS.md: NFSv4.1 was listed as having no real-client verification for
want of a `mount.nfs` on this host, and a VM guest supplies one — the
same server mounted `vers=4.1` from a real Linux kernel client and passed
the same workload. Still not a Tier-2 column; that would mean carrying a
VM in the suite.

Guide pages renumbered to slot it after Mounting; URLs are unaffected,
since the numeric prefix is routing-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants