Skip to content

Instantly share code, notes, and snippets.

@sevki
Last active February 25, 2026 09:56
Show Gist options
  • Select an option

  • Save sevki/b9dd3367f9762d2b51166deaefd34da9 to your computer and use it in GitHub Desktop.

Select an option

Save sevki/b9dd3367f9762d2b51166deaefd34da9 to your computer and use it in GitHub Desktop.

+++ title = "9P2000.L in Practice: Wire Protocol, Semantics, and Linux Kernel Implementation" description = "A deep dive into the wire protocol, semantics, and Linux kernel implementation of the 9P2000.L dialect of the Plan 9 file protocol." +++

9P2000.L in Practice: Wire Protocol, Semantics, and Linux Kernel Implementation

Executive summary

9P2000.L is a Linux-oriented dialect (“binding”) of the Plan 9 file protocol (9P) that aims to map Linux VFS semantics onto a compact, stateful, request/response RPC protocol. Its core design keeps the original 9P concurrency model (tagged transactions over an ordered byte stream) and extends functionality primarily by introducing new opcodes rather than altering baseline ones—an approach discussed explicitly in the VirtFS work that motivated much of the Linux adoption.

At the wire level, everything is built out of little‑endian integer fields, length‑prefixed UTF‑8 strings, and a fixed 7‑byte message header (size[4] type[1] tag[2]). A connection is initialised by Tversion/Rversion (negotiating msize and the dialect string), optionally followed by authentication (Tauth) and then one or more attaches (Tattach) that create a “root” fid for a user/session. Fids are the core state handles: they name remote objects and have lifecycle constraints (e.g., a fid must not be walked after it has been opened).

What makes 9P2000.L distinct is its Linux‑centric surface area: numeric errno-style failures via Rlerror, Linux‑style open flags via Tlopen, explicit attribute get/set via Tgetattr/Tsetattr with bitmasks, separate directory enumeration via Treaddir, and dedicated operations for symlinks, device nodes, xattrs, locks, renameat/unlinkat, etc. In the upstream Linux tree, this is implemented by the v9fs filesystem (fs/9p/…) layered atop a generic 9P client and transport stack (net/9p/…) supporting multiple transports (virtio, fd, RDMA, Xen, etc.).

Foundations of 9P: transactions, tags, fids, qids, and constraints

The Plan 9 protocol model is a transactional one: the client sends typed requests (“T‑messages”), the server replies with typed responses (“R‑messages”). The protocol permits multiple outstanding requests as long as they use distinct tags; responses may arrive out of order relative to requests (except where specific semantics—like flush—depend on ordering).

Core wire primitives

The canonical Plan 9 description defines:

  • Little‑endian unsigned integers in 2/4/8‑byte widths.
  • Variable‑length data items encoded as n[2] followed by n bytes.
  • Strings as a special case of variable‑length data: s[2] then s bytes of UTF‑8 (no NUL termination).

The baseline message header is always:

  • size[4]: total message length including the size field itself.
  • type[1]: message type (opcode).
  • tag[2]: client-chosen transaction identifier.

Linux’s header constants match this: P9_HDRSZ is 7 bytes, and P9_NOTAG is ~0 for the special tag value used during version negotiation.

Fids and their lifecycle rules

A fid is a 32‑bit client-chosen handle naming a server-side “current file” (not necessarily open for I/O). The protocol is stateful: operations create and alter fid bindings.

Important lifecycle constraints that become very relevant for Linux mapping:

  • Twalk requires that the referenced fid be valid in the current session and not opened by open/create; the same constraint applies to the newfid unless it equals fid.
  • Topen/Tcreate must not be applied to a fid that is already the product of a successful open/create.
  • Twalk with nwname=0 is explicitly allowed and clones the fid to newfid (“walk to dot”). This is widely used by Linux clients to duplicate a fid for concurrent operations.

Qids and caching implications

Many successful replies return a qid (13 bytes: type[1] version[4] path[8]), the server’s unique identifier for an object. The Plan 9 manual explains that path should be unique across the hierarchy and version typically changes on modification.

Linux’s 9P header comments explicitly describe qid.version as something that “can be used to maintain cache coherency,” and note a common convention: purely synthetic entities may set version to 0 to signal they should not be cached. The v9fs mount option ignoreqv is directly related to that convention.

Session establishment: version, msize, auth/attach, ordering, and negotiation behaviour

Mandatory initialisation: Tversion/Rversion

A 9P session begins with Tversion/Rversion. The Plan 9 definition states that Tversion identifies the protocol version and negotiates msize, and that it initialises the connection and aborts all outstanding I/O, freeing active fids (“clunked automatically”).

Plan 9’s version(5) also specifies the critical msize constraint:

  • The client proposes an msize representing the maximum message length it will generate or accept, including the size field and all protocol data (but excluding any outer transport framing).
  • The server replies with its maximum, which must be ≤ client’s msize.
  • Both sides must honour the negotiated limit thereafter.

Linux’s v9fs documentation exposes an msize= mount option described as “the number of bytes to use for 9p packet payload.” This wording is slightly different from the Plan 9 definition (which counts the whole message), but the wire negotiation still happens via Tversion/Rversion and the negotiated value bounds complete 9P messages. In practice, Linux also tracks header overhead separately (e.g., P9_HDRSZ=7, P9_IOHDRSZ=24 for read/write headers) when calculating safe payload sizes.

Wire formats: field-level byte layouts for Tversion/Rversion

Below is an exact byte layout for Tversion and Rversion on the wire, consistent with the Plan 9 manual and Linux constants (P9_HDRSZ=7, length‑prefixed string encoding).

Tversion / Rversion wire layout (little-endian)

Offset  Size  Field
======  ====  ==============================
0x00    4     size        (u32, total bytes including this field)
0x04    1     type        (u8, e.g., TVERSION=100 / RVERSION=101 in Linux enum)
0x05    2     tag         (u16; for version, typically NOTAG = 0xFFFF)
0x07    4     msize       (u32; negotiated maximum 9P message size)
0x0B    2     ver_len     (u16; number of bytes in version string)
0x0D    N     version     (UTF-8 bytes; N = ver_len)

In the Plan 9 spec, NOTAG is explicitly called out as an exception for the tag-matching rule in version messages. Linux defines P9_NOTAG as ~0 and (in its client machinery) allocates tags so that Tversion uses the NOTAG value.

Version strings and dialect compatibility rules

The Plan 9 version description (both in the Plan 9 manual and the RFC-style text) explains how version strings work:

  • Version strings must begin with "9P".
  • If the server does not understand the client’s version string, it should reply with Rversion (not Rerror) with version "unknown".
  • If the client string contains a period (.), the substring up to the period defines a base version; the server may respond with that base (or another earlier form), and the connection then uses the version defined by the server’s response.

The 9P2000.u draft explicitly leverages this “period suffix” convention to negotiate optional extensions: a client requesting 9P2000.u may get 9P2000 back if the server declines, and clients should degrade gracefully.

For 9P2000.L specifically, common Linux servers (e.g., diod) and Linux’s own v9fs usage expect the version string to be exactly 9P2000.L when the dialect is in use.

Authentication and attach: sequencing and 9P2000.L specifics

In baseline 9P2000, the connection establishment pattern is:

  1. Tversion/Rversion (initialise session and negotiate msize and version).
  2. Optionally Tauth/Rauth to establish an authentication fid (afid).
  3. Tattach/Rattach to introduce a user and obtain a fid representing the root of the selected tree.

The Plan 9 attach/auth manual states:

  • If authentication is not required, the server returns Rerror to Tauth.
  • If authentication is required, Rauth returns a qid for a QTAUTH file; the authentication protocol itself is outside 9P and proceeds by reading/writing the authentication fid.
  • If the client does not authenticate, it sets afid in Tattach to NOFID (~0).

9P2000.u and 9P2000.L commonly use an extended Tauth/Tattach that includes n_uname[4] (numeric uid hint). The 9P2000.u draft specifies this field and states that n_uname should be preferred unless it is unspecified (~0).

A Linux-specific deviation that implementers must notice: the diod 9P2000.L protocol summary describes returning Rlerror (numeric errno form) when authentication is not required, rather than Rerror. That is not the baseline Plan 9 behaviour, and it matters because clients must be prepared for the dialect’s chosen error path.

Order and timing requirements

Two ordering constraints are “hard” in the core spec:

  • A session must start with version, and version resets session state (aborting outstanding I/O and clunking active fids).
  • flush relies on messages arriving in order. The Plan 9 flush(5) page explicitly states the semantics depend on ordered delivery, and it requires clients to wait for Rflush before reusing the flushed tag.

In other words: transports suitable for 9P must provide an in-order byte stream (TCP, virtio queues with ordered semantics, etc.). Linux’s transport implementations in net/9p/ reflect this assumption.

Connection handshake sequence diagram

sequenceDiagram
    autonumber
    participant C as Client
    participant S as Server

    Note over C,S: Session initialisation
    C->>S: TVERSION(msize, "9P2000.L", tag=NOTAG)
    S-->>C: RVERSION(msize<=client, "9P2000.L" or fallback/base)

    alt Authentication required
        C->>S: TAUTH(afid, uname/aname, n_uname)
        S-->>C: RAUTH(aqid)
        Note over C,S: Auth protocol is out-of-scope for 9P; uses read/write on afid
        C->>S: TREAD/TWRITE(afid, ...)
        S-->>C: RREAD/RWRITE(...)
        C->>S: TREAD or TWRITE(afid, ...)
        S-->>C: RREAD or RWRITE(...)
    else Authentication not required
        C->>S: TAUTH(...)
        S-->>C: RERROR or RLERROR(errno=...)  %% depending on dialect/server
    end

    Note over C,S: Attach establishes root fid
    C->>S: TATTACH(fid=rootfid, afid|NOFID, uname/aname, n_uname)
    S-->>C: RATTACH(qid)

    Note over C,S: Normal operations begin
Loading

File and directory operations in 9P2000.L: end-to-end mechanics

This section treats 9P2000.L as “the whole system”: the baseline 9P transactions plus the .L opcode set that directly mirrors Linux VFS needs.

Name resolution and fid strategy: Twalk/Rwalk as the universal “path” primitive

9P does not send full paths in most file operations; instead, clients resolve names by walking directory hierarchies using Twalk. The Plan 9 walk specification:

  • Walks a sequence of name elements (wname[]) starting from an existing directory fid.
  • Updates newfid only if the entire sequence succeeds.
  • Allows nwname=0 as a legal clone (“walk to dot”).

Linux 9P clients lean heavily on nwname=0 cloning to create per-operation fids (e.g., one fid for a directory inode, cloned into a separate fid for an open directory handle). The diod session trace demonstrates exactly that pattern: clone with Twalk … nwname 0, then operate on the cloned fid.

Opening and creating files: Tlopen/Tlcreate versus Topen/Tcreate

Baseline Plan 9 uses:

  • Topen with a small “mode” (OREAD/OWRITE/ORDWR/OEXEC plus a few plan9 flags like OTRUNC, ORCLOSE).
  • Tcreate to create a new file and simultaneously open it, with perm and mode controlling permissions and kind (directory via DMDIR bit).

9P2000.L introduces Linux‑style variants:

  • Tlopen carries Linux open(2)-style flags (flags[4]) and replies with Rlopen(qid, iounit).
  • Tlcreate creates a regular file in a directory fid and prepares it for I/O; it carries name, flags, mode (creat mode bits), and gid, and it replies Rlcreate(qid, iounit). Importantly, after Tlcreate, the same fid now represents the new file (not the parent directory).

Linux’s 9P header enumerations include the .L message types (P9_TLOPEN, P9_TLCREATE, etc.) and also include a defined set of “9p2000.L open flags” constants mirroring many Linux open flags (READ/WRITE, CREATE, EXCL, TRUNC, APPEND, NONBLOCK, NOFOLLOW, CLOEXEC, SYNC, etc.).

Reading and writing: segmentation, iounit, and msize constraints

Baseline Tread/Twrite are:

  • Tread(fid, offset[8], count[4]) -> Rread(count, data[count])
  • Twrite(fid, offset[8], count[4], data[count]) -> Rwrite(count)

iounit, returned by open/create (and by lopen/lcreate in .L), is a server hint: if non‑zero, it is the maximum number of bytes guaranteed to be transferred atomically without splitting into multiple protocol messages.

In Linux, the header defines P9_IOHDRSZ as “ample room for Twrite/Rread header” (24 bytes). This is exactly the kind of constant used when computing maximum safe data payload sizes under a negotiated msize.

Diod’s 9P2000.L summary also notes a practical Linux client behaviour: if a read(2)/write(2) request does not fit in a single 9P request (due to msize/header limits), Linux v9fs splits it into multiple 9P transactions.

Directory enumeration: why 9P2000.L uses Treaddir not Tread

Plan 9 encodes directories as special files: reading a directory yields a stream of directory entries in a defined encoding.

9P2000.L changes this usage pattern: the 9P2000.L summary explicitly states that read cannot be used on directories and introduces Treaddir/Rreaddir for directory listing; it also specifies the per-entry encoding qid[13] offset[8] type[1] name[s] and an offset continuation rule (“offset returned in the last entry”).

Linux’s message type enum includes P9_TREADDIR/P9_RREADDIR, reflecting this as a first-class opcode rather than an overloaded file read.

Closing and removing: Tclunk, Tremove, and .L fallbacks

  • Tclunk destroys a fid and signals it is no longer needed.
  • Tremove removes the filesystem object represented by a fid.

9P2000.L adds Tunlinkat as a closer analogue to Linux unlinkat(2), carrying dirfd, name, and flags. The diod protocol summary notes explicit fallback guidance: if the server returns ENOTSUPP, the client should fall back to remove.

Similarly, Trenameat is defined with a fallback to older rename on ENOTSUPP.

Abort semantics: Tflush/Rflush

Flush is one of the subtler parts of 9P because it interacts with out-of-order transactions and server-side state changes. The Plan 9 flush(5) specification is precise:

  • The server should answer Tflush immediately and must never reply to Tflush with Rerror.
  • The server may still reply to the flushed request before replying to the flush.
  • The client must wait for Rflush before reusing oldtag, even if the original reply arrives in the meantime; if the original reply arrives first, the client must honour it because it may represent a server-side state change (e.g., a successful create allocating a fid).

These rules are directly relevant to Linux’s multiplexed request machinery, which uses tags as request IDs and must coordinate completion/wakeup correctly across concurrent operations.

9P2000.L extensions: message set, Linux-VFS mapping, errors, attributes, xattrs, locks

Extension philosophy: new opcodes rather than changed opcodes

The VirtFS paper (Linux Symposium 2010) clearly articulates the design intent for 9P2000.L: keep “core protocol elements” (size‑prefixed packets, tagged concurrency) and add Linux functionality as new operations in a complementary opcode namespace, avoiding overlaps with existing ones.

Linux’s enum p9_msg_t reflects this split: classic 9P2000 messages occupy the 100+ range (TVERSION=100, TAUTH=102, etc.), while 9P2000.L operations occupy other ranges (TLOPEN=12, TGETATTR=24, TREADDIR=40, TRENAMEAT=74, etc.).

Error model: Rerror strings vs Rlerror numeric errno

Baseline Plan 9 uses Rerror(tag, ename[s]) where ename is a human-readable string.

Linux has to map this into -errno returns. The Linux 9P subsystem includes explicit mapping code: net/9p/error.c describes the problem (“Plan 9 uses error strings, Unix uses error numbers”) and implements a string→errno hash mapping (p9_errstr2errno). If an error string is unmapped, it logs “unknown error” and returns a generic server-fault errno.

9P2000.u tried to bridge the gap by adding an errno[4] field to Rerror while still encouraging preference for the string due to cross-UNIX inconsistencies.

9P2000.L goes further and defines Rlerror(tag, ecode[4]), where ecode is a numerical Linux errno.

On the Linux client side, some error codes are “taken directly from the server replies,” and Linux includes a sanity check to ensure an error code is within a valid range; otherwise it reports a protocol error (-EPROTO).

Attributes and metadata: Tgetattr/Tsetattr vs Tstat/Twstat

Baseline 9P uses Tstat/Rstat and Twstat/Rwstat carrying a packed stat[n] blob. The Plan 9 manual notes, for example, that stat data is a variable-length datum and thus is limited to 65535 bytes.

9P2000.L adopts explicit attribute operations:

  • Tgetattr(fid, request_mask[8]) -> Rgetattr(valid[8], qid, mode, uid, gid, nlink, rdev, size, blksize, blocks, atime/mtime/ctime (sec+nsec), plus reserved fields btime/gen/data_version).
  • Tsetattr(fid, valid[4], mode, uid, gid, size, atime/mtime (sec+nsec)) -> Rsetattr. The diod summary specifies a semantic rule: if a time bit is set without the corresponding “SET” bit, the server uses its current time instead of the provided value.

Linux’s header defines struct p9_stat_dotl and struct p9_iattr_dotl, along with bitmask constants (P9_STATS_*) and shows how these correspond to Linux inode metadata fields at a structural level.

At the VFS integration layer, Linux has a dedicated file explicitly labelled “vfs inode ops for the 9P2000.L protocol” (fs/9p/vfs_inode_dotl.c), and the v9fs_vfs_setattr_dotl implementation constructs a p9_iattr_dotl with proper defaults (e.g., invalid uid/gid placeholders).

A documented kernel bugfix shows this area has had correctness-sensitive changes: a stable changelog notes that the 9P2000.L setattr method previously copied struct iattr fields without checking validity, causing uninitialised data to be sent; a patch “only copy valid iattrs” and ensures other fields are safely set. This change is recorded in stable releases (example: 5.15.16 changelog entry).

Xattrs: Txattrwalk/Txattrcreate and the “clunk commits” semantic

9P2000.L adds:

  • Txattrwalk(fid, newfid, name[s]) -> Rxattrwalk(size[8])
  • Txattrcreate(fid, name[s], attr_size[8], flags[4]) -> Rxattrcreate

The diod summary highlights a crucial semantic quirk that Linux clients/servers rely on: for xattrcreate, “the actual setxattr operation happens when the fid is clunked,” and a mismatch between the written byte count and attr_size should error.

This is a good example of 9P’s fid lifecycle being used as a commit boundary: the protocol often encodes multi-step operations as “create a special fid state, write data to it, then clunk to finalise”.

Links, symlinks, devices, mkdir: dedicated opcodes instead of mode-bit encodings

9P2000.u handled special files largely by encoding them using mode bits plus an extension string (e.g., symlink target in the extension string).

9P2000.L instead introduces explicit operations:

  • Tsymlink(dfid, name, symtgt, gid) -> Rsymlink(qid)
  • Tmknod(dfid, name, mode, major, minor, gid) -> Rmknod(qid)
  • Tmkdir(dfid, name, mode, gid) -> Rmkdir(qid)
  • Tlink(dfid, fid, name) -> Rlink
  • Treadlink(fid) -> Rreadlink(target)

These operations are enumerated in Linux’s message type enum, confirming they are first-class parts of the dialect from the kernel’s perspective.

statfs and fsync

9P2000.L adds:

  • Tstatfs(fid) -> Rstatfs(type, bsize, blocks, bfree, bavail, files, ffree, fsid, namelen) mapping closely to Linux statfs(2) fields as shown in the diod summary.
  • Tfsync(fid) -> Rfsync to flush cached data associated with an opened fid.

Locking: Tlock/Tgetlock and Linux behavioural notes

9P2000.L defines Tlock and Tgetlock with fields that mirror POSIX record locking (start, length, proc_id, etc.) and includes a client_id string to identify the requester. The diod summary documents Linux v9fs behaviour: it sets client_id to the nodename, implements blocking locks by retrying when status is “blocked,” and maps BSD flock to whole-file POSIX record locks. It also states the Linux client does not implement mandatory locks and returns ENOLCK if attempted.

Linux’s header defines the lock constants (P9_LOCK_TYPE_*, status codes, flag bits) and the structures used to represent them.

Protocol version comparison table: 9P2000 vs 9P2000.u vs 9P2000.L

Aspect 9P2000 (baseline) 9P2000.u (UNIX extension) 9P2000.L (Linux binding)
Negotiation string "9P2000" is the defined baseline in Plan 9 docs/RFC-style text. "9P2000.u" negotiated as a suffix; server may downgrade to "9P2000". Commonly "9P2000.L"; Linux v9fs supports selecting it via mount option.
Error reporting Rerror with string ename. Rerror extended with errno[4] hint; prefer string. Rlerror with numeric Linux errno.
UID/GID representation Strings (owner/group names). Adds numeric hints (e.g., n_uname in attach/auth; numeric fields in stat), alongside strings. Uses 9P2000.u-style attach/auth (incl. n_uname) and .L getattr/setattr numeric uid/gid fields.
Special files / symlinks Not first-class; relies on Plan 9 namespace model. Encodes symlinks/devices/pipes/sockets using mode bits + extension[s]. Dedicated opcodes: Tsymlink, Tmknod, Treadlink, etc.
Directory listing Tread on directories returns dir-entry stream. Same baseline mechanism. Treaddir opcode; .L summary states read cannot be used on directories.
Attributes stat/wstat packed structures. Extends stat structure with new fields. getattr/setattr with explicit bitmasks; maps to Linux stat/iattr.
Extension strategy Fixed baseline ops. “Minimal syntax” changes to existing ops (draft approach). New “complementary opcode namespace” (VirtFS stated aim).

Linux implementation: kernel architecture, source files, behavioural evolution, and documentation trail

Layering model in the upstream kernel

In Linux, “9P2000.L support” is not a single file; it is the combination of:

  • A generic 9P client core and marshalling layer in net/9p/… (message packing/unpacking, tag management, request multiplexing, error conversion, transports).
  • A filesystem client “v9fs” in fs/9p/… that maps Linux VFS operations onto 9P client RPCs and manages inode/dentry caches, fid lifetimes, mount options, and (optionally) caching integration.

The codebrowser directory listings provide an authoritative snapshot of the key files (here indexed from Linux v6.19-rc8 in the browsing instance):

  • fs/9p/: v9fs.c, vfs_inode.c, vfs_inode_dotl.c, vfs_file.c, vfs_dir.c, xattr.c, cache.c, fid.c, ACL support, superblock handling, etc.
  • net/9p/: client.c, protocol.c, error.c, and transports (trans_virtio.c, trans_fd.c, trans_rdma.c, trans_xen.c, trans_usbg.c, plus common transport helpers).

Linux’s public 9P protocol definitions live in include/net/9p/9p.h, which defines message type IDs, magic values (P9_NOTAG, P9_NOFID), header sizes, and data structures like p9_qid, p9_stat_dotl, p9_iattr_dotl, and lock structures.

Documented kernel/user documentation sources you should treat as “spec-adjacent”

For the baseline protocol text and message formats, the usable “spec” remains the Plan 9 manual section 5 pages (intro/version/walk/open/read/flush/etc.). The intro(5) page alone enumerates the baseline message formats and core semantics.

For the Linux client behaviour and mount options, the kernel documentation page “v9fs: Plan 9 Resource Sharing for Linux” (docs.kernel.org) and the historical Documentation/filesystems/9p.txt are the closest thing to official Linux-side user documentation. They enumerate important mount options such as:

  • version=9p2000|9p2000.u|9p2000.L
  • msize=
  • trans=virtio|tcp|fd|…
  • access modes (user, <uid>, any, client)
  • debugging flags and caching-related options.

For how/why 9P2000.L emerged, the VirtFS paper explicitly frames it as a Linux binding designed to cover more Linux VFS functionality and discusses extension namespaces and performance motivations.

Behavioural evolution and notable kernel-version waypoints

A fully exhaustive “every semantic change by kernel version” history is not centrally catalogued as a single document; it is spread across commits, changelogs, stable backports, and mailing list discussions. What is solidly supported by primary sources includes:

  • Mainline inclusion era: VirtFS documentation notes that 9P was incorporated into mainline Linux as a network-based distributed filesystem in the 2.6.14 timeframe (as referenced in the VirtFS paper).
  • 9P2000.L completeness milestone: The diod protocol summary states that the Linux v9fs client in kernel version 2.6.38 onward includes a “more or less complete 9P2000.L implementation.”
  • Correctness fix example in modern kernels: Stable changelog entries show ongoing maintenance of .L-specific paths, e.g. a fix to only copy valid iattrs in 9P2000.L setattr implementation (backported into stable series such as 5.15.x).
  • Ongoing performance and msize-related work: Maintainer discussions and patch series (example: 2023 performance fixes) explicitly mention leveraging “recently increased MSIZE limits” and adjusting caching/writeback behaviour, implying that practical/operational semantics (especially caching and I/O size handling) continue to evolve.
  • Mount API modernisation: A 2025 patch series shows work to convert 9p/v9fs to the newer mount API (fs_context-style parsing), indicating ongoing refactoring that can affect option handling and internal wiring (even if the wire protocol stays constant).

Linux-specific “quirks” that matter in real deployments

Linux’s own documentation calls out behavioural differences compared to local filesystems. One explicit example: setting O_NONBLOCK can change read behaviour so that client reads return as soon as the server returns some data rather than attempting to fill the user buffer (or reach EOF).

Caching modes are also explicitly caveated: “loose caches … do not necessarily validate cached values on the server,” so changes on the server may not be reflected on the client unless you have an exclusive mount. This is a Linux client property (v9fs caching strategy interacting with qid/version semantics), not a property of the 9P wire format itself.

Minimal state machine: connection and fid states

To reason about correctness (and to implement a server/client), it helps to carry an explicit state model. The following state graph is derived from the Plan 9 constraints on version, walk, and open/create, plus the .L pattern of cloning fids before applying I/O operations.

stateDiagram-v2
    [*] --> Uninitialised

    Uninitialised --> Negotiated: TVERSION/RVERSION ok
    Negotiated --> AuthInProgress: TAUTH/RAUTH (optional)
    AuthInProgress --> Negotiated: auth protocol complete
    Negotiated --> Attached: TATTACH/RATTACH ok

    state Attached {
        [*] --> FidAllocated
        FidAllocated --> FidWalked: TWALK/RWALK (nwname>=0)
        FidWalked --> FidOpened: TLOPEN/RLOPEN or TOPEN/ROPEN
        FidWalked --> FidCreatedOpen: TLCREATE/RLCREATE (fid becomes file)
        FidOpened --> FidClunked: TCLUNK/RCLUNK
        FidCreatedOpen --> FidClunked: TCLUNK/RCLUNK
        FidWalked --> FidRemoved: TREMOVE/RREMOVE or TUNLINKAT/RUNLINKAT
        FidAllocated --> FidClunked: TCLUNK/RCLUNK
    }

    Attached --> Negotiated: TVERSION resets session (clunks fids)
Loading

The key point: Tversion is a global reset (“session initialisation”), while most other operations are fid-scoped but constrained by fid state (walk vs open vs clunk).

Notes on what is not specified (and must be treated as implementation-defined)

Some important behaviours are intentionally outside the 9P spec surface:

  • The authentication protocol carried over the afid channel is not defined by 9P; it is an external protocol negotiated by convention.
  • Cache coherency is only loosely addressed by qid versioning conventions; the Linux client provides multiple caching modes and caveats, but “strong” cache consistency rules are not a property of the wire protocol itself.
  • For some .L operations, fallback rules (e.g., renameatrename on ENOTSUPP) are described in secondary protocol summaries (like diod’s), but are not presented as a single normative, Plan 9-style manual page for 9P2000.L. Treat them as de-facto interoperability rules: widely followed, but not universally guaranteed.

References

  1. Plan 9 Manual -- intro(5) -- Baseline 9P2000 message formats and core semantics
  2. diod 9P2000.L protocol summary -- De-facto reference for 9P2000.L message definitions
  3. Linux include/net/9p/9p.h -- Kernel header defining message types, constants, and data structures
  4. Plan 9 Manual -- walk(5) -- Walk semantics and fid lifecycle constraints
  5. Plan 9 Manual -- open(5) -- Open/create semantics and mode flags
  6. Plan 9 Manual -- attach(5) -- Authentication and attach sequencing
  7. Plan 9 Manual -- flush(5) -- Flush/abort semantics and ordering rules
  8. Plan 9 Manual -- stat(5) -- Stat structure and metadata operations
  9. Plan 9 Remote Resource Protocol 9P2000 -- RFC-format 9P2000 specification
  10. 9P2000.u Unix Extension -- Draft specification for the Unix extension dialect
  11. v9fs: Plan 9 Resource Sharing for Linux -- Kernel documentation for v9fs mount options and behaviour
  12. Linux net/9p/ source tree -- 9P client core, transports, and error mapping
  13. Linux fs/9p/ source tree -- v9fs filesystem implementation
  14. Linux fs/9p/vfs_inode_dotl.c -- VFS inode operations for 9P2000.L
  15. Linux net/9p/error.c -- Plan 9 error string to errno mapping
  16. VirtFS -- OLS 2010 -- Paper describing the design motivation for 9P2000.L
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment