What happens now, in one boot
Ninety seconds after an installed EuroOS reaches the desktop (the network needs a DHCP lease first), the kernel does this on its own, and again every six hours. This is the serial log of the test machine, unedited:
[euroupdate] boot from slot A (gen 1, 0 attempt(s) left, A=Good B=Empty)
[euroupdate] slot A confirmed GOOD (boot succeeded)
[euroupdate] euroupdate check: https://euro-os.eu:443/update channel 'stable' (Ed25519-signed manifest)…
[euroupdate] manifest OK (signature valid): version 20260908, running 20260907
[euroupdate] image 53421056 B fetched, sha256 pinned by manifest ✓
[euroupdate] euroupdate: 53421056 bytes written to the EuroSlot-B partition + read-back ✓
[euroupdate] euroupdate: image (53421056 bytes) verified + written to slot B
[euroupdate] next boot tries slot B (3 attempts, then automatic rollback)
[euroupdate] policy: ask
A notification appears on the desktop: version 20260908 is verified and staged, restart to apply. On the next boot the two-stage loader takes over:
[loader] on_boot: B tries 3 → 2
[loader] slot_config → boot slot B
[loader] kernel image from the slot partition (header + sha256 verified)
[loader] kernel image loaded — LoadImage + StartImage...
[euroupdate] boot from slot B (gen 2, 1 attempt(s) left, A=Good B=Trying)
[euroupdate] slot B confirmed GOOD (boot succeeded)
Had the new kernel failed to boot three times, the loader would have gone back to slot A by itself. The user's files live on a separate EuroFS partition and are never touched.
The security model
It is the APT model, not the app-store model. The channel manifest is Ed25519-signed and refused before anything is downloaded if the signature does not verify. The manifest pins the SHA-256 of the image; the image carries its own Ed25519 signature; both are checked against the key baked into the running kernel. The transport is EuroTLS 1.3 with the bundled root store (Let's Encrypt's ISRG roots included), so a hostile mirror or a man in the middle can at worst serve nothing. On disk, the slot image sits behind a header with its length and hash, and the loader recomputes that hash before it hands the image to the firmware. A torn write leaves a header that does not match, never a header pointing at garbage: the image is written first, the header last.
What was built today
- A public update channel at
euro-os.eu/update/, generated by the release script from the same kernel that goes into the download images, signed on the build host. - The kernel's version is now stamped at build time (
EUROOS_BUILD_VERSION, a date), so the version compare is real. - A periodic check with three policies:
ask(stage and notify, default),auto(stage and restart after two minutes),manual(only tell me).euroupdate statusshows version, policy, last result and what is staged. - The installer creates two 96 MiB slot partitions, and the ESP grew to 256 MiB, because two 53 MB kernels do not fit in the 40 MiB it used to get.
- The loader can boot a kernel from a slot partition, verifying the header hash first, and falls back to the ESP file on a fresh install.
- The network stack got a size-capped fetch for large bodies; the old path stopped at 512 KB, which is fine for a manifest and useless for a kernel.
The three bugs the chain found
None of these were visible while the pieces were tested one at a time. All three appeared the moment an installed disk really tried to update itself.
1. Two truths about which slot to boot
The loader reads \slot_config from the ESP. The kernel kept its own copy on a reserved raw block and mirrored it to the filesystem. They were never the same file. A staged update or a mark-good in the kernel never reached the loader, and a rollback decided by the loader never reached the kernel. Now the ESP copy is the one both act on, with the raw block and the filesystem as mirrors behind it.
2. The self-tests staged garbage into the real slot
The boot self-tests exercise the real staging code with a 70-byte test pattern. Before, that pattern landed in the real slot partition but the loader never looked there, so nobody noticed. Once the loader trusted a headered slot image and read the same slot_config, a fresh install went straight into three failed boot attempts of a "kernel" that was a test string. The self-tests now run in a dry mode that verifies and decides for real but never writes to a slot or a slot_config.
3. An exclusive protocol open kills your own filesystem
Both the loader and the installer opened the disk's Block I/O protocol exclusively, just to count disks. In UEFI an exclusive open disconnects every other driver on that handle, including the FAT driver serving the ESP you are booting from. The loader could no longer read the kernel file it had just decided to boot, and the installer saw no filesystem to read its media from. A non-exclusive open has no side effects.
The fourth thing was not a bug but a limit: building a 256 MiB FAT image in RAM to write it out is not something a 384 MiB kernel heap survives. The FAT builder now streams sectors straight to the disk and references the kernel bytes instead of copying them.
Update, later the same day
Three of the limits below did not survive the evening. The check now runs as a background job: one non-blocking slice per desktop iteration, so the desktop keeps drawing while the kernel comes in over TLS (the first version froze the screen for the duration of the download). The slot and ESP writes go through the disk that was actually booted, so the same install now updates and reboots on virtio-blk, AHCI/SATA and NVMe; all three were run against the live channel. And the kernel now trusts two keys: the daily key that signs releases, and a rotation key that lives encrypted on the build server with its passphrase only in a password manager. A leaked daily key is retired by shipping one rotation-signed release with a new one, without a second machine. The procedure is in docs/UPDATES.md.
Honest limits
- A full kernel is 51 MB per update. Delta updates come when that hurts.
- The public preview image (the one you boot from a USB stick or a VM without installing) has no slot partitions and does not update itself; install it first.
- The rotation key protects against a leaked daily key, not against a build server that is silently compromised and keeps signing with the daily key. Reproducible builds are the answer to that, and they exist for the kernel but are not yet checked by a second party.
The test that proved all of this is three QEMU runs, install, update, reboot, against the live server, repeated for the three disk buses. The serial logs are in docs/proof/ota-2026-09-08/ in the repository.