Phase 403 — bootstrap serial root console proof
| Item | Value |
|---|---|
| Command | make phase 403 |
| Underlying make target/scripts | vm/phase4/materialize-etc.sh --serial-console, then vm/phase4/serial-console-probe.sh |
| Mutates disk/image? | Yes, it mounts artifacts/onix-image/onix.raw and installs the bootstrap console pieces |
| Boots QEMU? | Yes, it runs an automated serial-interaction proof |
| Main proof | QEMU boots ONIX, the image prints ONIX_BOOTSTRAP_SERIAL_CONSOLE_READY, the host sends a command over serial, and ONIX returns ONIX_SERIAL_COMMAND_OK uid=0. |
The important honesty rule
Phase 403 is not the final ONIX login design.
It is a bootstrap serial root console.
That means:
useful for early bring-up
not authenticated
not acceptable as the final installed-system policy
This distinction matters.
Phase 402 created real users and groups, but root still has:
/usr/sbin/nologin
as its account shell in /etc/passwd.
Phase 403 does not change that.
Instead, Phase 403 creates a temporary systemd service that directly starts a root shell on the serial port.
So the normal account policy still says:
root is not a normal interactive login account yet
while the bootstrap policy says:
for early OS bring-up, give the developer a root shell on ttyS1
Why we need this phase
Until now, ONIX could boot to systemd multi-user mode.
That proves the kernel and PID 1 handoff.
But a system that boots and cannot be inspected is painful to develop.
We need a way to type commands inside the booted image:
id
mount
cat /etc/os-release
systemctl status
Serial is the simplest early interface because QEMU can wire it directly to the host terminal or to a script.
Why not proper login yet?
Proper login needs more pieces:
- a getty or terminal service
- a login/authentication program
- a password, passwordless, or first-boot auth decision
- a real shell listed in
/etc/shells - a decision about root login
The current bootstrap image may also start generated serial-getty units, but those are not enough for a usable login stack yet.
Phase 403 chooses a smaller, more honest proof:
Can ONIX start a shell on a serial port and can the host send commands to it?
That answer should become yes.
What provides /bin/sh
ONIX is staying musl-based.
So Phase 403 builds or fetches:
pkgs.pkgsMusl.busybox
from the pinned nixpkgs in flake.lock.
BusyBox is useful here because one small binary can provide many early system commands:
sh
ls
cat
mount
ps
id
uname
poweroff
Phase 403 copies the BusyBox closure into the image and exposes:
/bin/busybox -> /nix/store/...-busybox-.../bin/busybox
/bin/sh -> busybox
It also creates a set of BusyBox applet symlinks under /bin.
This is still bootstrap glue. Later, ONIX should own shell packages through real stones.
What owns the serial ports
In this proof we deliberately split the serial jobs:
ttyS0 = boot log / kernel console
ttyS1 = temporary bootstrap root shell
Why split them?
Because the boot console is noisy. The kernel, firmware, systemd status output, and generated serial getty can all write there. Trying to use the same TTY as a clean automated shell makes the proof flaky and hard to read.
Phase 403 masks the generated serial getties for the two serial ports used by the proof:
/etc/systemd/system/serial-getty@ttyS0.service -> /dev/null
/etc/systemd/system/serial-getty@ttyS1.service -> /dev/null
Then it enables:
onix-bootstrap-serial-shell.service
under:
multi-user.target
There is one ugly bootstrap detail here.
The current systemd payload is still the Phase 213 Nix-store bootstrap payload,
not a real onix-systemd stone. Its active unit tree lives under a copied path
like:
/nix/store/...-systemd-.../example/systemd/system
So Phase 403 installs the temporary unit into that copied unit tree inside the image.
That is not the final architecture. It is just the correct place for this specific bootstrap systemd to see the unit today.
That service runs the pinned musl BusyBox directly:
/nix/store/...-busybox.../bin/busybox sh /usr/lib/onix/bootstrap-serial-shell
We run BusyBox explicitly instead of relying only on the script’s #!/bin/sh
line. That makes the bootstrap dependency visible in the unit.
The wrapper prints:
ONIX_BOOTSTRAP_SERIAL_CONSOLE_READY tty=... uid=0 shell=/bin/sh
and then executes:
/bin/sh -l
The /persist gotcha
This phase also taught an important image-building rule.
Inside the root filesystem tree there is a directory named:
/persist
But at boot, ONIX mounts a separate partition there:
LABEL=ONIX-PERSIST /persist xfs ...
That means this is not enough while building the image:
copy BusyBox into root-tree /persist/nix/store
Why not?
Because when the real ONIX-PERSIST partition mounts, it covers that directory.
Anything copied only into the root tree’s /persist directory becomes hidden.
Then /nix is bind-mounted from:
/persist/nix
So Phase 403 must:
- mount the root partition,
- mount the actual
ONIX-PERSISTpartition at/persist, - copy the BusyBox closure into the real
/persist/nix/store, - copy the systemd unit into both the root Nix tree and the persist Nix tree.
That is why the successful output includes:
mount : ONIX-PERSIST -> /persist
unit : /persist/nix/store/.../onix-bootstrap-serial-shell.service
The automated proof
make phase 403 does not stop after installing files.
It then starts QEMU with serial connected to the probe script.
The proof script waits for:
ONIX_BOOTSTRAP_SERIAL_CONSOLE_READY
Then the host sends this command over the ttyS1 serial line:
echo ONIX_SERIAL_COMMAND_OK uid=$(/bin/id -u) kernel=$(/bin/uname -s) pwd=$(pwd)
The proof passes only if the serial output contains:
ONIX_SERIAL_COMMAND_OK uid=0
That proves:
- ONIX booted far enough to start the bootstrap console service
/bin/shexists and can run- the host can send input over serial
- command output comes back over serial
- the shell is running as root
What the script writes as proof
Phase 403 writes:
/usr/share/onix/bootstrap/serial-console.txt
That file records:
- this is a temporary unauthenticated bootstrap console
- root’s normal account shell remains
/usr/sbin/nologin /bin/shcomes from musl BusyBoxttyS0is kept as the boot log consolettyS1is used as the temporary bootstrap shell consoleserial-getty@ttyS0.serviceandserial-getty@ttyS1.serviceare masked for nowonix-bootstrap-serial-shell.serviceowns the bootstrap shell console- the probe marker is
ONIX_BOOTSTRAP_SERIAL_CONSOLE_READY
Run it
From the repo root:
make phase 403
Expected output includes:
shell : /bin/busybox -> /nix/store/.../bin/busybox
shell : /bin/sh -> busybox
mount : ONIX-PERSIST -> /persist
unit : /nix/store/...-systemd-.../example/systemd/system/onix-bootstrap-serial-shell.service
unit : /persist/nix/store/...-systemd-.../example/systemd/system/onix-bootstrap-serial-shell.service
mask : /etc/systemd/system/serial-getty@ttyS0.service -> /dev/null
mask : /etc/systemd/system/serial-getty@ttyS1.service -> /dev/null
proof : /usr/share/onix/bootstrap/serial-console.txt
Then the QEMU probe should end with:
console : observed ONIX_BOOTSTRAP_SERIAL_CONSOLE_READY
command : observed ONIX_SERIAL_COMMAND_OK uid=0
==> success
Phase 403 proved two-way bootstrap serial root console access.
How to watch it manually
Phase 403 is automated.
The proof creates two serial channels:
ttyS0 -> vm/state/phase403.boot.log
ttyS1 -> vm/state/phase403.serial.log and vm/state/phase403.serial.sock
The QEMU process is stopped after the proof, so this phase is currently a test/proof target rather than a long-running interactive session.
You can still use the older attached boot view for the Phase 212 image boot:
ATTACHED=1 make phase 212
That connects the QEMU boot serial console to your terminal. It does not
show the Phase 403 ttyS1 bootstrap shell socket.
To exit the attached QEMU serial session:
Ctrl-a then x
What this phase does not do
Phase 403 does not solve final authentication.
It does not decide:
- root password policy
- whether root can log in normally
- whether first boot creates a user
- whether SSH keys are the first real auth method
- whether getty/login should come from util-linux, shadow, BusyBox, or ONIX packages
Those are later decisions.
Phase 403 only proves:
we can interact with the booted ONIX image over serial