# 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.