On 9P Semantics

Posted on June 22, 2025 by Brandon Wilson
Tags: ,

Recently, I threw together a 9P file server for Dyalog Ride, in the spirit of webfs. It was my first foray into using 9P directly, and I found some of the semantics a bit confusing, so I am writing this up to document my newfound insights and throw them into the aether.

Conventions Make the Protocol

9P simply presents a handful of request-reply message pairs. The protocol itself is extremely parsimonious and almost too flexible to be interesting on its own. However, in quintessential Plan 9 style, the power comes from using the protocol according to “globally understood conventions” (cf Plan 9 From Bell Labs by Rob Pike). In other words, what breathes life into the protocol is the message dance that CLI tools like ls, cp, cat, etc. end up shaking out.

First, let’s start a server with chatty9p set to give debug output:

cpu% somefs -D
<-6- Tversion tag 65535 msize 32792 version '9P2000'
-6-> Rversion tag 65535 msize 32792 version '9P2000'
<-6- Tauth tag 5 afid 442 uname x aname 
-6-> Rerror tag 5 ename somefs: authentication not required
<-6- Tattach tag 5 fid 442 afid -1 uname x aname 
-6-> Rattach tag 5 qid (ffffffff00000000 0 d)

We see that initializtion cosists of a version → auth → attach triplet, just as documented. In this case, somefs automounts to /mnt/some:

cpu% file /mnt/some
/mnt/ride: <-6- Twalk tag 5 fid 442 newfid 427 nwname 0 
-6-> Rwalk tag 5 nwqid 0 
<-6- Topen tag 5 fid 427 mode 0
fid mode is 0
-6-> Ropen tag 5 qid (ffffffff00000000 0 d) iounit 0 
<-6- Tstat tag 5 fid 427
-6-> Rstat tag 5  stat '/' 'x' 'x' 'x' q (ffffffff00000000 0 d) m 020000000555 at 1750563921 mt 1750563921 l 0 t 0 d 0
directory
<-6- Tclunk tag 7 fid 427
-6-> Rclunk tag 7

We see a walk, open, stat, and clunk. Both open and stat are expected, as they’re essentially the bare minimum libc calls we’d need to read the directory information, and we can guess that clunk corresponds to close(2).

Why Walk?

The semantics of walk is what confused me, especially since implementations often provide a clone and walk1 pair of callbacks to answer this one request. What are we cloning, and what is walking where?

Leading with the lede, the answer is that walk(5) provides the path resolution mechanism. Said another way, we need some way to map fids to qids, and this is it.

The defining feature 9P, IMHO, is it’s handling of fids and qids, which are intended to be understood, respectively, as file descriptors and unique file identifiers. They’re morally equivalent to file descriptors and inode ids from Linux.

As a client connecting to a server for the first time, we have no idea what files it provides. Procedurally, we start by only knowing the fileserver root mount point, which we have to read(2) to see its content. The root could be a file, but in the case of a directory, reading returns a list of dirents. To open(2) any of these, we need to first assign a fid to its path—the role of Twalk—and resolve it to a qid—the role of Rwalk.

On the implementation side, fids are structs that hold data associated with all the in-flight file structures—think connection information in the case of webfs(4). Clone is our mechanism for producing a new fid from an old one, a la clone(2)ing a process. Since each fid and path component in a walk has fileserver-specific semantics, the clone and walk1 pairs ensure that the new fid can acquire all the necessary content needed to figure out which qid is should map to.

Here’s a typical access pattern on something like webfs(4) to intialize a new connection:

cpu% <clone {cd `{read}}
<-6- Twalk tag 5 fid 442 newfid 473 nwname 1 0:clone 
-6-> Rwalk tag 5 nwqid 1 0:(ffffffff00000002 0 ) 
<-6- Topen tag 5 fid 473 mode 0
fid mode is 0
-6-> Ropen tag 5 qid (0000000000000004 0 ) iounit 0 
<-6- Tread tag 5 fid 473 offset 0 count 1
-6-> Rread tag 5 count 1 '0'
<-6- Tread tag 5 fid 473 offset 1 count 1
-6-> Rread tag 5 count 1 '
'
<-6- Twalk tag 5 fid 442 newfid 488 nwname 1 0:0 
-6-> Rwalk tag 5 nwqid 1 0:(0000000000000003 0 d) 
<-6- Tclunk tag 5 fid 473
-6-> Rclunk tag 5
<-6- Twalk tag 8 fid 488 newfid 473 nwname 0 
-6-> Rwalk tag 8 nwqid 0 
<-6- Topen tag 8 fid 473 mode 0
fid mode is 0
-6-> Ropen tag 8 qid (0000000000000003 0 d) iounit 0 
<-6- Tclunk tag 8 fid 473
-6-> Rclunk tag 8

We 1) walk to clone, 2) open and read it, and 3) walk to the connection directory that cone assigned. The final walk then simply renames the original starting fid with the one current directory, supposedly cd semantics.

Final Thoughts

The mischievous side of me wants to know how we might creatively abuse 9P. There is nothing preventing a clients from issuing reads and writes before walks, and there is no obligation for the server to actually tell clients which dirents exist. We could easily imagine a port knocking-like scenario, where specific messages exchanges unlock hidden functionality.

What other creative fileservers can you think up to (ab)use 9P?