summaryrefslogtreecommitdiff
path: root/9player/README.md
blob: 77081d6eac8ee456a6ad288c96bcb7ebbe90d179 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 9player

Mount a 9P2000 file tree into a fresh mount namespace and run a program in it,
as a plain user, without touching the host's mount table.

```sh
9player --unix /run/user/1000/acme -- fish        # a shell that sees the tree at /mnt/9p
9player --tcp 127.0.0.1:564 -- claude              # an agent that sees it too
9player --spawn 'introspect --stdio' -- bash       # start the server yourself, talk over a socketpair
```

Inside, the tree is ordinary files: `ls`, `cat`, `echo x > ctl`, editors,
`find`, `rsync`, whatever. `$NINEPLAYER_MOUNT` tells programs where it is
(default `/mnt/9p`). When the program exits, 9player exits with its status
and the namespace, mount and connection disappear.

## How it works

The kernel's own `9p` filesystem cannot be mounted inside an unprivileged user
namespace, so 9player is a small FUSE server that speaks 9P2000 to the real
server. There is no libfuse and no libc: `src/fuse.zig` implements the subset
of the kernel FUSE protocol needed, straight from `linux/fuse.h`.

```
 program (fish/bash/claude)      9player (parent)                9P server
 in a new user+mount namespace     │
   /mnt/9p ── FUSE ──▶ kernel ────▶│ fuse.zig ─▶ bridge.zig ─▶ nine.zig ──▶ unix / tcp / socketpair
                                   │ (framing)   (translation) (cloud9 Client)
```

1. The parent connects to the 9P server (version + attach) so failures are
   reported before anything is forked.
2. The child does `unshare(CLONE_NEWUSER|CLONE_NEWNS)`, maps its own uid/gid,
   makes every mount private, opens `/dev/fuse` (it must be opened inside the
   new user namespace), mounts it on the mountpoint and passes the descriptor
   back to the parent over `SCM_RIGHTS`, then execs the program.
3. The parent serves FUSE requests by translating them into 9P transactions
   (`Twalk`, `Topen`, `Tread`, `Twrite`, `Tcreate`, `Tremove`, `Tstat`,
   `Twstat`) until the child exits or the namespace disappears.

Files are opened with `FOPEN_DIRECT_IO`, so synthetic files that report length
0 (the 9P convention for control files) still read correctly, and `O_TRUNC`
travels inside the 9P open mode (`OTRUNC`) rather than as a separate
truncate. Repeated lookups of the same qid map to the same inode.

If `/mnt/9p` does not exist and cannot be created (the normal case), 9player
mounts a tmpfs over `/mnt` *inside the namespace only* and bind-mounts every
existing entry of `/mnt` back into it, so nothing is hidden. Pass `--mount DIR`
to use any other directory.

## Building and testing

9player lives in the [cloud9](../) repository as `cloud9/9player/`, beside
the 9P2000 protocol library it is built on, and is wired into cloud9's
`build.zig` through the fragment `9player/build.zig`. Everything is run from
the cloud9 root with Zig 0.16:

```sh
zig build                      # zig-out/bin/{9player,introspect,cloud9-http,cloud9-probe}
zig build 9player              # build and install only zig-out/bin/9player
zig build 9player-test         # unit tests (protocol structs, session, bridge, namespace helpers)
zig build 9player-itest        # integration tests: real namespaces, real FUSE,
                               # introspect over unix/tcp/socketpair, and plan9port's
                               # ramfs when /usr/lib/plan9/bin/ramfs is installed
zig build 9player-adv          # adversarial suites: hostile 9P servers, FUSE semantics,
                               # namespace/signal edge cases, stress (several minutes)
zig build -Doptimize=ReleaseSafe
zig build -D9player=false      # leave 9player out (the default on non-Linux targets)
```

The integration suites mount the `introspect` demo server (`../introspect`),
so they need `-Dintrospect=true` (the default on Linux), unprivileged user
namespaces (`kernel.unprivileged_userns_clone=1` on distributions that have
the knob), `/dev/fuse` and Python 3; they skip themselves otherwise.
`zig build programs-test` and `programs-itest` run the unit and integration
steps of every program in the repository.

## Usage

```
9player [options] -- PROGRAM [ARGS...]

Transport (exactly one):
  --unix PATH          Unix stream socket
  --tcp IP:PORT        TCP (IPv4/IPv6 literal)
  --fd N               an already-connected inherited descriptor
  --spawn CMD          run CMD via /bin/sh -c with a socketpair on its stdin/stdout

Options:
  --mount PATH         mountpoint inside the new namespace (default /mnt/9p)
  --uname NAME         9P user name (default $USER)
  --aname NAME         9P tree to attach (default "")
  --msize BYTES        maximum 9P message size to request (default 131072, max 16 MiB)
  --cache SECONDS      attr/entry cache validity, fractional allowed (default 1)
  --no-direct-io       let the kernel cache file pages (trusts stat length)
  --debug              trace FUSE and 9P operations on stderr
  --help, --version
```

PROGRAM defaults to `$SHELL`. Exit status is the program's (`128+signal` if it
was killed); 125 means 9player itself failed (usage, connect, namespace,
mount); 126/127 are exec failures as usual.

## introspect: a demo 9P server

`introspect` is a single-binary 9P2000 server whose file tree is the binary
itself: build-time facts, `comptime` reflection and live runtime state. It is
the demo of the [introspect library](../introspect) (`../introspect/demo/main.zig`),
built and installed by `zig build introspect`.

```
/README
/build/{zig_version,target,optimize,time,change}   captured by build.zig (jj change id, UTC time)
/comptime/types/<T>/{name,size,align,fields}        @sizeOf/@alignOf/@typeInfo, generated at comptime
/comptime/decls                                     pub declarations of the server module
/runtime/{pid,ppid,uptime,argv,cwd,env,clients}
/runtime/fn/<name>                                  reading calls a Zig function (hostname, now, random, uname, fib30);
                                                    the directory is generated from @typeInfo of the Fns struct
/runtime/ctl                                        write "fib N" | "add A B" | "echo TEXT" | "sleep-ms N", read the result
/scratch/                                           in-memory read/write tree
```

`/runtime/env` exposes the server's whole environment, so serve introspect on
a Unix socket or loopback only.

```sh
zig-out/bin/introspect --unix /tmp/intro.sock &
zig-out/bin/9player --unix /tmp/intro.sock -- sh -c '
  cat $NINEPLAYER_MOUNT/build/zig_version; echo
  cat $NINEPLAYER_MOUNT/comptime/types/Qid/fields
  echo "fib 20" > $NINEPLAYER_MOUNT/runtime/ctl; cat $NINEPLAYER_MOUNT/runtime/ctl'
```

The same command with `claude -p "explore /mnt/9p ..."` as the program gives an
agent a live, file-shaped view into a running process; that is the intended
use.

## Limitations

* One 9P request is in flight at a time; a server read that blocks (event
  files) stalls the mount until it returns.
* Base 9P2000 only: no symlinks, ownership, xattrs or locks. Every file is
  reported as owned by the invoking user. Cross-directory rename is `EXDEV`.
* No PID namespace and no `/proc` remount. `--tcp` takes IP literals only
  (no libc, no resolver).
* Linux only.

## Relation to cloud9

9player consumes cloud9 as the module `cloud9` and keeps all mounting,
namespace and process policy on its side, which is what cloud9's design asks
of applications. It ships from the cloud9 repository as the sibling directory
`9player/` (sources in `src/`, suites in `test/`, this README and
`docs/DESIGN.md`) with a build fragment that the root `build.zig` enables with
`-D9player` on Linux targets; nothing in the code depends on that layout.
`docs/DESIGN.md` has the full module contracts.