On 9P Semantics
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 fid
s to
qids
, and this is it.
The defining feature 9P, IMHO, is it’s handling of fid
s and qid
s, 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, fid
s 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?