NFS

This loopback Network File System (NFS) server does not use /dev/fuse or native code. NFS is the only mount transport that mountx supports on macOS.

mountx/nfs implements two standards versions over one Transmission Control Protocol (TCP) socket. It supports NFSv3 and MOUNTv3 from RFC 1813. It also supports NFSv4.1 from RFC 8881. Both versions use Open Network Computing Remote Procedure Call (ONC RPC) from RFC 5531. Their data uses External Data Representation (XDR) from RFC 4506. mountx encodes and decodes every structure in both versions.

A JavaScript client built from the same codecs can test the complete protocol.

import { mountNfs } from "mountx/nfs";
import { createMemoryDriver } from "mountx/drivers/memory";

await using mounted = await mountNfs(createMemoryDriver(), "/mnt/point");
mounted.port; // the port both programs are on

mountx/auto selects this transport automatically on macOS. It uses NFSv3 because that is the default and the only version requested on macOS. See Two versions. Under mountx/auto, put its options in nfs: {…}.

#Two versions

mountNfs() defaults to version: 3. Pass version: "4.1" to mount the same driver over NFSv4.1 instead:

await using mounted = await mountNfs(createMemoryDriver(), "/mnt/point", { version: "4.1" });

The server always handles both versions on one socket. The version option controls only which version the client requests.

A client can try to negotiate down from an unavailable version. In that case, RPC PROG_MISMATCH reports the supported range as {low: 3, high: 4}. Both versions remain available even though only one is mounted by default.

NFSv4.1 is Linux-only. This is assumption A1, not a protocol limit. mountx treats macOS as an NFSv4.0-only client because its support for 4.1 is not known. This server supports only minor version 1. Therefore, mountNfs({ version: "4.1" }) refuses on Darwin and recommends the default.

If a Mac turns out to speak 4.1, deleting that refusal (versionRefusal() in src/nfs/mount.ts) is the whole change.

NFSv3 keeps working on both hosts exactly as before.

The mount line differs only where the protocols differ. NFSv4.1 includes MOUNT and NLM, so mountport= and nolock disappear:

# Linux, NFSv4.1 — versions share the one port, so there is no mountport=
sudo mount -t nfs -o vers=4.1,proto=tcp,port=<p>,soft,timeo=50,retrans=2 127.0.0.1:/ /mnt

The other options are soft or hard, timeo, retrans, and ro. nfs(5) defines them as options supported by all versions. mountx emits them identically for both versions. The nfsMountOptions() function generates this string.

#The 4.1 session model, briefly

Unlike a v3 client, a v4.1 client keeps protocol state. It registers with EXCHANGE_ID and confirms with CREATE_SESSION. Each later COMPOUND starts with SEQUENCE, which names a (session, slot, sequence) triple.

For a retransmission on the same slot and sequence, the server returns the cached reply. It does not run the operations again. This provides exactly-once semantics for each slot.

A session has a lease of 90 seconds by default. Only SEQUENCE renews it. When the lease expires, the server keeps state for a courteous grace period instead of immediately dropping it.

There is no reclaim grace period after a restart. The server does not keep stable storage across a restart. Therefore, a reclaiming OPEN or LOCK always returns NFS4ERR_NO_GRACE. RECLAIM_COMPLETE gates ordinary locking as required by RFC 8881 §18.51.3.

NFSv4.1 also has real open state. OPEN takes share reservations and supports OPEN_DOWNGRADE. LOCK, LOCKT, and LOCKU grant advisory byte-range locks. As with Portable Operating System Interface (POSIX) locks, they gate competing LOCK requests. They do not gate READ or WRITE.

Special stateids are understood, including the READ-bypass one, and the COMPOUND's own "current stateid" cursor is tracked the way its current and saved filehandles are.

The server reports these unsupported features explicitly:

  • It does not support delegations. Each OPEN declines with OPEN_DELEGATE_NONE. If the client requested a delegation, OPEN uses OPEN_DELEGATE_NONE_EXT with a reason as specified in §18.16.3.
  • It does not support parallel NFS (pNFS).
  • It does not use a backchannel. The server never sets CREATE_SESSION4_FLAG_CONN_BACK_CHAN.
  • It does not support RPCSEC_GSS. It supports only AUTH_SYS and AUTH_NONE, as in v3.
  • It does not support NFSv4.0. This server supports only NFSv4.1. A client that names another minor version receives NFS4ERR_MINOR_VERS_MISMATCH.

#Owner strings

NFSv4 carries ownership as owner and owner_group strings, such as "1000@example.com". It does not use raw numbers. mountNfs({ nfs4: { idmap } }) accepts an Nfs4IdMap to translate these values. The map contains nameOf and idOf callbacks plus a domain.

Without this configuration, the session uses the numeric form that the protocol permits, such as "1000" without @. A Linux client uses the same form when its identity mapper has no result.

#Where it mounts

hostroot?notes
macOSnoyou must own the mount point; NFSv3 only — see Two versions
Linuxyesmount(8) has no setuid helper for NFS; NFSv3 or NFSv4.1
Windowsno NFS client worth the name, and no mount(8)

Linux requires root for this mount. The NFS protocol does not require root.

On macOS, /sbin/mount_nfs is not set-user-ID and has no entitlement. BSD permits a user to mount on a directory that the user owns. An unprivileged process uses the same mount(8) command as root. The kernel adds MNT_NOSUID|MNT_NODEV to the result.

The host probe cannot check whether the user owns the mount point because it has no path yet. mountNfs() checks ownership at mount time. If the check fails, its error names the owner.

On Linux there is no equivalent: an unprivileged mount(8) would need an fstab entry marked user, which is not something a library can arrange.

#nfsClientProbe(platform?)

function nfsClientProbe(platform?: NodeJS.Platform): NfsClientProbe;

interface NfsClientProbe {
  usable: boolean; // can this host mount NFS at all?
  platform: "linux" | "darwin" | undefined;
  helper: string | undefined; // path of mount.nfs / mount_nfs, if found
  kernel: boolean; // does the kernel have an NFS client?
  v4: boolean; // can this host mount NFSv4.1? Linux only — see below
  root: boolean; // are we root — required on Linux only
  reason: string | undefined; // everything missing, in a sentence
}

v4 and usable report separate facts. For example, an unprivileged Linux process can receive usable: false because it has no root access. The same result can contain v4: true because the host supports v4. On macOS, v4 is always false because of assumption A1 in Two versions.

This function is synchronous and imports only node:fs. Therefore, mountx/auto can call it before loading a transport. Tests can also use it as a gate. Its result lets mountx name a missing requirement instead of returning mount: wrong fs type. nfsPlatform(platform) provides the platform narrowing.

Note

The kernel check is deliberately weak. On Linux, /proc/filesystems lists nfs only after the module loads. mount.nfs can load it on demand. Therefore, a host with the helper and a loadable module can report kernel: false and remain usable. What is not usable is a host with neither.

#What it costs

NFSv3 is stateless. There is no open/release, so every request carries a file handle built from the driver's (dev, ino) identity rather than per-open state.

This changes one observed behavior: a file deleted while still open stays readable over FUSE and answers ESTALE here. All other tested behavior is the same. This includes reads, writes, directories, symbolic links, permissions, and statfs. The shared conformance suite verifies these cases.

File handles also contain a boot verifier, which is random by default. After a server restart, the changed verifier invalidates outstanding handles. The server does not answer them against a new tree.

NFSv4.1 pays the same cost, for a different reason. NFSv4.1 has real open state through OPEN, CLOSE, and share reservations. However, both versions share one path-keyed handle table in src/nfs/handles.ts. REMOVE immediately removes the path from its handle.

So an open stateid whose file was unlinked answers STALE too, structurally, rather than because the protocol has nothing to say about it.

The conformance matrix in .agents/conformance-matrix.md records the lost handles capability in both NFS columns. Each declaration explains the different protocol reason.

#mountNfs(driver, mountpoint, options?)

function mountNfs(
  driver: FsDriver,
  mountpoint: string,
  options?: MountNfsOptions,
): Promise<NfsMount>;

This function serves driver over NFSv3 by default. With version: "4.1", it uses NFSv4.1. It starts a kernel NFS client through mount(8).

This function supports Linux as root. It supports macOS without root when you own the mount point. NFSv4.1 also requires Linux. See Two versions.

Options are at the top level here. mountx/auto nests them under nfs: {…}.

It resolves after mount(8) succeeds, so the client has already contacted the server. NFSv3 has completed MOUNT and FSINFO. NFSv4.1 has completed EXCHANGE_ID, CREATE_SESSION, and its first COMPOUND.

interface NfsMount extends AsyncDisposable {
  readonly mountpoint: string;
  readonly server: NfsServer;
  readonly port: number; // the port both programs are on
  readonly source: string; // e.g. "127.0.0.1:/"
  readonly active: boolean;
  unmount(): Promise<void>;
}

unmount() is idempotent, concurrency-safe and retryable after a failure.

The promise always settles. A normal unmount settles within unmountTimeout. A forced unmount settles within twice that time because the timeout applies separately to each phase.

Each step receives only the time that remains in its phase. Steps include umount(8) and the macOS mount(8) command that reads the mount table. Server shutdown is also a step. It closes each file handle that the sessions still hold, which can call an unresponsive driver.

#MountNfsOptions

Extends NfsServerOptions (and through it NfsSessionOptions), so everything on this page is settable in one object.

optiondefault
version33 or "4.1" — which the client is told to ask for; see Two versions
serverserve an existing NfsServer; its listen() is still called
exportPath"/"directory of the driver the client starts at — see below
readOnlyfalsemount -o ro
hardfalseretry forever instead of soft — see below
timeo50tenths of a second before a request is retried
retrans2retries before a soft mount gives up with EIO
nobrowsetruemacOS only: -o nobrowse, hiding the volume from the GUI
mountOptionsnoneextra -o options, appended verbatim
signalstrueunmount on SIGINT/SIGTERM
unmountTimeout10_000ms each teardown phase may spend; 0/Infinity waits forever
onTransportErrornone(error, peer) for transport-level failures and forced teardown

Note

nobrowse on by default. Finder and Spotlight scan a visible volume. For a JavaScript driver, this causes a burst of traffic and unexpected .DS_Store writes. Set false if the point of the mount is for someone to open it in Finder.

#exportPath is a starting point

exportPath chooses the directory of the driver the mount lands on.

It is not a security boundary and does not confine the client. A client can reach everything under the driver's root, regardless of this setting.

It appears only in the host:path source argument. The server does not record which export produced a file handle.

Both protocol versions can reach the driver root as specified:

  • On NFSv3, LOOKUP of At the export root, .. resolves to the parent. It continues toward the driver root and stops there. This follows the mountx rule that every path clamps at /.
  • On NFSv4.1 no .. is needed. PUTROOTFH and PUTPUBFH are the protocol's own "start at the top of this server's tree", and this server has exactly one tree: the driver's.

This is an intended protocol property, not a security defect.

There is no additional privilege to gain.

The MOUNT program is unauthenticated, so anything that can reach the socket can ask for MNT "/" and be handed the driver root outright.

The socket is the boundary. The server binds loopback by default. The driver behind the socket is the exported data.

So if a subtree is what you mean to serve, scope the driver, not the export:

// Serves this and nothing above it: the passthrough resolves every path
// component against its own root, and `..` cannot leave it.
await mountNfs(createNodeFsDriver("/srv/share"), "/mnt/point");

// Serves the whole driver. `exportPath` only decides where the client lands.
await mountNfs(createNodeFsDriver("/srv"), "/mnt/point", { exportPath: "/share" });

#nfsMountOptions(port, options?, platform?)

function nfsMountOptions(port: number, options?: MountNfsOptions, platform?: NfsPlatform): string;

The exact -o string mountNfs() would pass, so you can see it, log it, or hand it to mount(8) yourself.

The function is pure and accepts a platform parameter. This lets tests check platform-specific spellings from either host. The differences are nolock versus nolocks, no hard option on macOS, and nobrowse.

options.version selects the branch. The v4.1 branch omits mountport and nolock. nfs(5) defines both as options only for versions 2 and 3. For the refused combination of "4.1" on Darwin, the function throws the same message as mountNfs().

#liveNfsMounts() / unmountAllNfs()

Every NFS mount this process has up, in creation order; and unmount them all, never rejecting.

#How it mounts

The mount uses four intentional rules:

  • No portmapper. Both programs use one port. For v3, the mount command receives both port= and mountport=. It does not contact rpcbind.
  • No locking (v3). nolock on Linux and nolocks on macOS disable lockd and rpc.statd. NFSv3 file locking uses the separate Network Lock Manager (NLM) protocol, which this server does not implement. Without this option, a client could wait forever for an unavailable service. If software on the mount needs flock, you can add macOS locallocks through mountOptions. This option applies locks in the client virtual file system (VFS) layer. NFSv4.1 includes locking in the main protocol, so this option does not exist there. See Two versions.
  • soft by default. A hard mount retries forever. This is suitable when a remote server can reboot. It is unsafe when the server is a JavaScript object in the mounting process. A driver bug could create processes blocked in D state instead of returning EIO. Set hard: true if you want the other trade. macOS has no explicit hard option because hard mounts are the default. Therefore, this option can request only a soft mount on macOS.
  • Versions, advertised rather than hidden. The server always answers both versions on one socket. If mount(8) requests an unavailable version, the server reports its range as {low: 3, high: 4}. The client can then unmount and request the other version. The library does not need to detect and perform that change.

#Serving without mounting

Serving needs no privileges and does not use mount(8). You can start only the server and connect any NFSv3 or NFSv4.1 client. The client can run on another host if you intentionally allow remote access:

import { createNfsServer } from "mountx/nfs";
import { createMemoryDriver } from "mountx/drivers/memory";

await using server = createNfsServer(createMemoryDriver());
await server.listen();
interface NfsServer extends AsyncDisposable {
  readonly session: NfsSession; // stats, handles, mount list
  readonly port: number; // 0 before listen(); ephemeral unless you set one
  readonly host: string; // "127.0.0.1" by default
  readonly connections: number;
  listen(): Promise<NfsServer>; // idempotent; resolves once bound
  close(): Promise<void>; // idempotent
}

createNfsServer() is the only API here that opens a socket. It runs on every Node platform, including Windows. Only mounting a kernel client has platform requirements.

Then mount it yourself:

# Linux, NFSv3
sudo mount -t nfs -o vers=3,proto=tcp,port=<p>,mountport=<p>,nolock,soft,timeo=50,retrans=2 127.0.0.1:/ /mnt

# macOS, NFSv3 — `nolocks`, and no `hard` option (hard is the default there)
mount -t nfs -o vers=3,proto=tcp,port=<p>,mountport=<p>,nolocks,soft,timeo=50,retrans=2,nobrowse 127.0.0.1:/ /mnt

# Linux, NFSv4.1 — Linux-only; no mountport=, no nolock (see Two versions)
sudo mount -t nfs -o vers=4.1,proto=tcp,port=<p>,soft,timeo=50,retrans=2 127.0.0.1:/ /mnt

<p> is server.port, or mounted.port if you used mountNfs(); nfsMountOptions() above returns the exact -o string mountx would use, if you want to see it.

#NfsServerOptions

optiondefault
port00 means an ephemeral port, which port then reports
host"127.0.0.1"address to bind
allowRemotefalseaccept connections from non-loopback addresses
maxRecord8 MiBlargest RPC record accepted
maxInFlight64 (DEFAULT_NFS_MAX_IN_FLIGHT)calls answered at once per connection before the rest wait
onTransportErrornone(error, peer) — a bad record, a socket error

Caution

Neither version provides secure authentication. Both support only AUTH_SYS and AUTH_NONE; NFSv4.1 does not support RPCSEC_GSS. Therefore, the server binds 127.0.0.1 and refuses non-loopback connections by default.

Reaching it from another machine takes both host and allowRemote: true, and that exports your driver to anything that can reach the port.

The largest legal record is a WRITE of wtmax plus headers. A larger record violates maxRecord, so the server closes the connection instead of skipping the record. After an invalid length, it cannot resynchronize the record-marked stream.

maxInFlight bounds memory, not the protocol. maxRecord limits one record, but it does not limit the number of concurrent replies. Pipelining is normal protocol behavior. Clients match replies by xid. NFSv4.1 also provides a slot table so that a client can fill multiple slots.

Ten thousand pipelined 1 MiB READ requests use 1.03 MiB on the v3 wire. With this server's 20-byte handle, each framed READ call is 108 bytes. The same workload uses 1.72 MiB on v4.1. Its SEQUENCE + PUTFH + READ COMPOUND is 180 bytes.

Either way the answer is ten gigabytes, a ~10,000× amplification the client pays nothing for.

The window limits active reply memory to maxInFlight multiplied by the largest reply. With the default 1 MiB rtmax or wtmax, the limit is approximately 64 MiB. A connection at the limit stops reading its socket. Waiting records use only their wire size.

It is not an ordering knob.

Replies use completion order, regardless of the window. A slow READ does not delay a later GETATTR. The xid field matches these replies. NFSv4.1 also uses (session, slot, sequenceid).

Each reply is still one contiguous record handed to one write(), so two of them cannot interleave their bytes.

The constant is DEFAULT_NFS_MAX_IN_FLIGHT, which names its transport like DEFAULT_NFS_PORT. 9P similarly has DEFAULT_P9_PORT, but its window constant is DEFAULT_MAX_IN_FLIGHT. The NFS and 9P window values differ, although their import names do not show that difference.

The option is maxInFlight on both, since there the interface says which server it belongs to.

The NFS default is 64, while the 9P default is 16. NFSv4.1 can offer up to 64 ca_maxrequests slots. A conforming client can keep every granted slot busy.

NFSv3 has no wire slot table. However, the Linux client sets dynamic sunrpc.tcp_slot_table_entries in the same range. Therefore, one default covers both versions.

#NfsSession

new NfsSession(driver: FsDriver, options?: NfsSessionOptions)

NfsSession converts bytes to bytes without a socket: handleCall(bytes)Promise<Uint8Array | null>, answering the MOUNT program, NFSv3 and NFSv4.1 alike. It never rejects. Every call that needs a reply gets exactly one.

NfsSession is the version router. It reads the (prog, vers) pair in an RPC call and sends the same raw bytes to the matching session. The router decodes the header when it must refuse a record itself. The selected version then decodes it to produce the reply.

Both versions share one FileHandleTable and one path lock. Therefore, a handle obtained through one version resolves through the other. A RENAME in either version also excludes readers in both.

session.driver; // the Loopback wrapping your driver
session.handles; // the FileHandleTable, shared by both versions
session.stats; // { requests, replies, errors, dropped, procedures }
session.v3; // the Nfs3Session underneath, for tests and the CLI
session.v4; // the Nfs4Session underneath, for tests

stats.procedures is a Map with keys such as "NFS:LOOKUP", "MOUNT:MNT", and "NFS4:COMPOUND". NFSv4.1 has one procedure because all operations travel inside COMPOUND.

#NfsSessionOptions

optiondefault
useDriverInotrueidentify files by the driver's (dev, ino), so hardlinks share a handle
verifierrandomboot verifier for file handles, so restarts invalidate them
rtmax / wtmaxlargest READ answered / WRITE accepted
snapshotCache64directory snapshots kept for readdir cookies
maxHandlesno capmost file handle table entries to keep, least recently used out first
claimOwnershiptruegive new entries to the request's AUTH_SYS caller
onErrornonecalled for every request that ends in an error status
nfs4Nfs4StateKnobs: NFSv4.1-only knobs, ignored by the v3 session

claimOwnership solves the same problem as the corresponding FUSE logic. The driver creates entries as the server process, but requests come from the user who mounted the share.

It does nothing when the driver has no lchown. It also does nothing when the server cannot transfer ownership. A driver without ownership support remains valid.

The group is the one POSIX gives, not simply the caller's.

A set-group-ID parent directory passes its group to a new entry. A new directory also inherits the set-group-ID bit. Therefore, a shared directory stays shared through its descendants. Linux applies this rule in inode_init_owner() for a local filesystem. mountx applies it because no other layer will.

A new group-executable file loses S_ISGID when its creator is in neither the effective nor the supplementary groups the AUTH_SYS credential carried.

Turning claimOwnership off leaves every new entry exactly as the driver made it, group and mode included.

maxHandles limits the file handle table. Without the limit, the server keeps one entry for each path that a client can name. A client that walks one million files leaves one million entries for the server lifetime.

It is disabled by default because a client can still hold an evicted handle. An NFSv3 client then receives ESTALE and must perform another LOOKUP.

It is a soft cap.

The table never evicts an entry that an NFSv4.1 client has open or locked. Share reservations use the entry ID. Eviction could let another client's write open bypass an active DENY_WRITE.

The cap is therefore soft. If a client holds more files open than the cap, the table exceeds the limit. It cannot return below the limit until those opens close.

Sizing it is therefore a judgement about the working set, not a hard ceiling on memory.

Keep the limit above the largest READDIRPLUS page that clients request. Each name in a page binds a handle. A smaller cap can evict handles from the reply that creates them.

Also prefer a plain / export. The mount root of a subdirectory export is an ordinary evictable entry. An NFSv3 client has no parent name that it can use to look up that root again.

nfs4 contains settings that an NFSv3 mount does not use. These include the lease length, which defaults to 90 seconds. It also contains the idmap: Nfs4IdMap hooks from Owner strings. Negotiated limits include maxSessions, maxForeSlots, maxOperations, and related values.

macOS puts network volumes behind a sandbox approval that is never prompted for a command-line process. Without approval, umount blocks and umount -f returns EPERM. The operating system does not show a dialog.

mountx names that case specifically (isConsentDenial()/consentAdvice()) instead of blaming your driver, and says plainly that the mount survived.

The library cannot bypass this restriction. Before mounting, grant Full Disk Access to the application that owns the process. This can be the terminal, your integrated development environment (IDE), or the continuous integration (CI) agent.

The escalation commands also differ by platform. macOS has umount -f but not umount -l. Therefore, lazy unmount and its recovery advice apply only to Linux.

#Teardown

Teardown follows the same rules as FUSE. It starts with umount(8) and treats the mount table as authoritative. It forces down the mount at the deadline. All steps are idempotent and retryable.

macOS has no /proc/self/mounts, so mountx starts mount(8) to read the table. This makes the read asynchronous. It also makes mount state a tri-state: mounted, not mounted, or unreadable.

A table that could not be read is undefined, never false.

Forcing an already-removed mount is harmless. In contrast, guessing that unmount succeeded can shut down the server while the mount is still active.

server.close() does not wait for clients to disconnect normally. A mounted NFS client keeps its connection open for the mount lifetime. Waiting for it would prevent close from returning.

#The layers below

Every file except server.ts and mount.ts runs on any operating system without privileges. Therefore, a JavaScript client built from the same codecs can test the protocol:

module
xdr.tsXDR (RFC 4506): bounds-checked reader/writer, 64-bit fields as bigint
rpc.tsONC RPC v2 (RFC 5531): call/reply, AUTH_NONE/AUTH_SYS, record marking, plus the constants both versions frame a call with
handles.tsthe file handle table and the readdir cookie scheme, shared by both versions
util.tswhat neither version owns: POSIX permission/mode-bit decisions, and the options bag + shared handle table/lock/counters the router hands both sessions
v3/constants.ts + v3/protocol.tsRFC 1813, every NFSv3 and MOUNTv3 struct both directions
v3/session.tsNfs3Session — MOUNT and NFSv3, bytes in, bytes out, no socket
v4/constants.tsRFC 8881 (+ RFC 5662 for its XDR) transcribed whole, incl. ops this server refuses
v4/attr.tsthe bitmap4/fattr4 sparse attribute codec (RFC 8881 §3.3.7, §18.7.3)
v4/protocol.tsthe COMPOUND framing and the stateless operations, both directions
v4/state.tsclient IDs, sessions and their reply caches, stateids, share reservations, locks, the lease clock — pure and synchronous, no I/O
v4/session.tsNfs4Session — COMPOUND dispatch, bytes in, bytes out, no socket
session.tsNfsSession(driver, options) — the version router
server.tsthe socket. mount.tsmount(8)

mountx/nfs re-exports the NFSv3 protocol layer by name. The public API does not yet export NFSv4.1 v4/protocol.ts or v4/constants.ts. For now, they are available only through session.v4. The v3 substructure helpers are also private, but for the different reason below.

  • xdr.ts: XdrReader/XdrWriter: bounds-checked, big-endian, 64-bit fields as bigint, and they only ever throw XdrError.
  • rpc.ts: call and reply, AUTH_NONE/AUTH_SYS, TCP record marking.
  • constants.ts + protocol.ts: RFC 1813 transcribed; every NFSv3 and MOUNTv3 struct encoded and decoded, plus fattrOf, nfsStatusOf, errnoOfStatus, statusName and the time conversions.
  • handles.ts: FileHandleTable and the readdir cookie scheme: the state a stateless protocol still needs.

The public API deliberately excludes the substructure helpers used by procedure codecs. These include readFattr and writeFattr, readSattr and writeSattr, and the post_op_* and wcc_data pairs. They are implementation pieces rather than APIs that a consumer combines.

#Next

  • FUSE: the preferred transport, when it can mount.
  • 9P2000.L: the other root-needing transport, and the stateful trade it makes instead.
  • Virtual machines: the transport a Firecracker guest has to use, since its kernel has no 9P.
  • Troubleshooting: ESTALE, EPERM, and hangs on macOS.
  • mountx/auto reference: the chooser that calls this.

mountx  Write a filesystem in JavaScript, mount it as a real folder.