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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
|
# A sane 9front
## What this is
An experiment: a 9front system whose userland assumes the machines we
actually have, rather than the ones Bell Labs had.
It is not a fork, not a research OS, and not an attempt to be different for
its own sake. The kernel is 9front's and stays 9front's. What gets replaced
is the layer above it — init, boot, service management, network
configuration, the identity model — because that layer encodes assumptions
about computing that stopped being true.
The first brick was init, because nothing else can be supervised without it.
That was not the goal. It was the only place to start.
## The vocabulary problem
Plan 9 asks what *kind* of machine you have. Answer with a noun — `cpu`,
`terminal`, `auth` — and the noun decides everything downstream.
This is not an abstraction. It is a switch statement, and you can read it:
- `sys/src/9/boot/bootrc:151` — `if(~ $service cpu) init=($init -c)`,
otherwise `-t`. One word in `plan9.ini` picks the flag.
- `sys/src/cmd/init.c:61` — `iscpu = strcmp(service, "cpu")==0`, then
`fexec(cpustart)` or `fexec(rcexec)`.
- `/rc/bin/termrc` (99 lines) and `/rc/bin/cpurc` (121 lines) are roughly 80%
identical. The difference between a machine you can log into and one you
cannot is which of two nearly identical scripts runs.
The consequence is visible in `ps`: a machine has no `aux/listen` not because
anyone decided it should not accept logins, but because `plan9.ini` says
`terminal`.
Nouns are the wrong interface because real machines are several things at
once, or change what they are, or are things the noun list never anticipated.
A machine bridging two networks is not a `cpu` server or a `terminal`. Asking
which one it is has no answer.
**Replace the noun with the list.** A machine is what it runs. Not a role, a
service directory. `is keyfs enabled` replaces `is this the auth server`.
## Three tiers
The scope line, so this stays an experiment and not a fork:
| tier | policy | why |
|---|---|---|
| kernel | take as-is | it is what 9front actively develops, and the thing we would most regret diverging from |
| toolchain, libc, core commands | take as-is | `6c`, `mk`, `rc`, `ls`, `sam`, `ed` are solved. Rewriting them is the gatekeeping failure mode |
| system layer | replace entirely | init, boot, `termrc`/`cpurc`, service management, ndb conventions, `listen`, namespace construction |
What must be tracked upstream is the kernel's interface to userland:
`#`-devices, the syscall surface, 9P, and the boot protocol — `plan9.ini`
keys, bootargs, what the kernel expects of the first process.
## Abandon, do not fork
The Plan 9 orbit is mostly a graveyard of forks that could not afford
themselves. Every one bought a permanent rebase tax on day one and paid it
out of the budget meant for the actual idea.
The distinction that keeps this cheap: **a fork modifies their files and must
track them forever. Abandoning means writing our own and never looking
upstream again.** A patched `termrc` is a liability. An init that never reads
`termrc` is free.
So: keep an explicit, short list of files we modify in the 9front tree.
Everything else is ours or theirs, never both. If that list stays at three or
four entries, this is honest. If it grows, we have quietly become a fork and
will see it early rather than in two years.
There is a second reason, and it is the better one: **if the thesis is that
namespaces are the right abstraction, then needing a fork refutes the
thesis.** A patched tree proves the namespace was insufficient.
## Principles
1. **Adopt the solved interface; invent only where Plan 9 actually wins.**
Two-button mice, TCP, TLS, file extensions, `/sys/log` — solved elsewhere,
and being different is pure cost. Namespaces and 9P are where Plan 9 is
genuinely better, and inventing there earns its keep. Inventing in the
first category is most of why Plan 9 has the users it has.
2. **Sane defaults over configurable everything.** A default that is wrong
for a rare case is better than a knob every user must understand. No
plaintext outside the loopback or the LAN. No config that suggests
otherwise.
3. **The identifier carries the fact.** A file extension says what a file is
without opening it. A port number should say what transport it speaks: if
9543 always means TLS and nothing ever listens plaintext there, then
"accidentally exporting unencrypted to the WAN" stops being a
misconfiguration to guard against. (9543 is unused in the entire 9front
tree — verified.)
4. **Structured, not textual.** ndb and ctl files, not scripts with
conditionals. Configuration should be readable by something other than a
shell.
5. **Declare, do not script.** The failure mode of shell startup is not
length, it is opacity. `bootrc` declares `fn configlocal {}` empty and then
sources `/rc/lib/*.rc`, which redefine it — a plugin system built on
function shadowing. You cannot know what boot does by reading it. Two
symptoms of the same disease: `diskparts` runs twice (`local.rc:44` and
`termrc`), and `mntgen`, `nusbrc` and the device binds each run two or
three times, because each pivot loses track of what was already done.
6. **Namespaces are the capability set.** Privilege is what is in your
namespace, not what your uid is. You do not protect the raw disk with
permission bits; you leave `#S` out of the namespace. Capability by
absence.
7. **Build for namespacing, in both directions.** A sandboxed application
gets a namespace built for it and cannot name anything else. Export should
be the same mechanism pointed outward: `exportfs` should not hand out "my
root, minus permissions" — it should hand out a namespace constructed for
that client. Same code, same profiles, same audit story.
8. **Per-server authentication, not one realm.** Factotum keys are already
`(proto, dom, user)` tuples and nothing about factotum assumes a single
realm. What assumes it is the convention built on top: one `authdom` in
ndb, `cpurc` deciding it is *the* auth server by matching `$sysname`
against an ndb attribute. The mechanism is fine. The convention is
centralised, and that is a much smaller thing to fix.
## Boot
Root is a service. The boot method is that service's dependencies.
```
svc=root svc=root
provider=hjfs provider=mount
needs=diskparts needs=ipconfig
needs=factotum
```
Local boot and network boot stop being two code paths and become two
dependency sets. No method table, no `$mt`, no function shadowing, no
`bootargs is (tcp, tls, il, local!device)` prompt.
The phase split is derived, not declared: **phase 1 is the transitive
dependencies of `root`.** Everything else is post-pivot. init's one built-in
action is the pivot itself, triggered when `root` becomes ready.
`bootfs.proto` is already the phase-1 palette. Its contents — `rc`, `bind`,
`mount`, `srv`, `mntgen`, `ipconfig`, `tlsclient`, `kbdfs`, plus `factotum`
and `paqfs` from `bootdir`, plus `hjfs`/`gefs` from `disk.proto` — are
precisely "what you might need before root exists." The constraint on a
phase-1 service is that its `exec=` is in the archive, which is checkable at
build time.
Two independent arguments put the console before root: an encrypted root must
prompt for a passphrase, and a phase-1 failure has no filesystem to log to.
`console=` already does this — `bootrc` runs `aux/kbdfs -q -s cons /dev/eiaN`
before `exec $init`, giving a real console with working interrupt (`Kdel` is
0x7f, and kbdfs turns it into a note to the process group).
Being first is cheap: `boot.c` *execs* `/bin/bootrc` rather than forking it,
so whatever sits at that path is pid 1. Stock init is pid 2 only because
`bootrc` runs its `main` as `@{main}`, in a subshell. But pid 1 carries no
kernel privilege in Plan 9 — what matters is what exists before you, because
you can never `exec=` those. That is what `adopt=` is for, and being pid 1
does not shorten the list by one process.
**What we give up by being first:** `bootrc`'s `while(){ @{main} }` loop is a
supervisor of last resort — it retries a failed boot and re-prompts. Taking
its place means answering "who restarts init?" ourselves rather than
inheriting an answer.
## Security
Plan 9's model assumes booting a terminal gets you nothing worth having:
your data is on the file server, and reaching it requires authenticating.
The host owner is, in the words of the authors, "just a regular user that
happens to own the resources of the local machine" — no setuid, no ambient
superuser, privilege granted through a capability rather than a uid check.
That part is better than Unix and we keep it.
The premise it rests on is dead. A machine with a local filesystem, keys in
nvram, and a position on several networks is not a thin client. Booting it
hands over all three.
Bell Labs knew and said so. *Security in Plan 9*, §2.5:
> We have not addressed leaks possible as a result of someone rebooting or
> resetting a machine running factotum. [...] We have not found a
> satisfactory solution to this problem.
They defended thoroughly against an attacker with a process on a *running*
machine — `private` and `noswap` keys, `/dev/caphash` openable once, a
`confirm` attribute for high-value keys — and then stopped at the machine's
power switch. Their unsolved case is our default case.
It is more solvable now than in 2002, and the parts already ship:
`disk/cryptsetup` for data at rest (and it is in `disk.proto`, so an
encrypted root is already a supported boot path), password-protected nvram
for machine identity, `dp9ik` in place of the weak `p9sk1`, TLS. What is
missing is a default that assembles them. That is policy, not research.
**A machine that boots should be inert until someone gives it a secret.**
Two secrets, actually, and they are different: the data (disk) and the
identity (nvram). Losing the second is worse.
And nobody should be running as the host owner all day. The paper worries
about exactly this — an attacker with a host-owner process cannot read the
keys but can ask the local factotum to authenticate as you — and answers it
with a GUI confirmation prompt, which is a band-aid on a structural problem.
The host owner owns hardware and starts services. Humans log in as a
separate identity, with their own factotum and a namespace that does not
contain the disk. `auth/login` already does the mechanism; what was missing
is a reason to have identities other than the owner.
## What this is not
- Not a performance project.
- Not a compatibility project. Boot methods, filesystems and transports we
do not need are not ported. IL is not removed from 9front; it is simply
never used. TCP works.
- Not a rewrite of things that work. If a 9front command does its job, it
keeps doing its job.
- Not a distribution yet. It becomes one when there is something worth
installing.
## Open questions
Live in `design.md` and `todo.md`. The two that block the most:
- **The provider circularity.** A service that provides a namespace to
others must both mount and post to `/srv`, making it the exception to
"do not bind `#s`" and to freezing the namespace. Root is the archetype of
exactly this, which means the boot design and this question are the same
problem. Namespace profiles built before it is settled get built twice.
- **Who restarts init?** Unanswered, and being pid 1 makes it urgent rather
than academic.
|