diff options
| author | Calvin <calvinm@kissingerassoc.com> | 2026-08-17 12:26:43 -0400 |
|---|---|---|
| committer | Calvin <calvinm@kissingerassoc.com> | 2026-08-17 12:26:43 -0400 |
| commit | 441c64d81594bb521350dbee6348f552401a1a2e (patch) | |
| tree | 23f8f2f666a211d621738f1ad186d4cdf14f64ae | |
| parent | 7ba5bfafc5a8b0f09c4b88aea62e7db9206cfa38 (diff) | |
doc: the manifesto
Why this exists, what it replaces and what it does not, and the positions
that decide later arguments. Claims are cited to file:line in the 9front
tree or to Security in Plan 9, so they can be checked rather than believed.
| -rw-r--r-- | doc/manifesto.md | 236 |
1 files changed, 236 insertions, 0 deletions
diff --git a/doc/manifesto.md b/doc/manifesto.md new file mode 100644 index 0000000..96cfcb3 --- /dev/null +++ b/doc/manifesto.md @@ -0,0 +1,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. |
