First, the pixels had to exist at all
Chromium refuses to produce a frame without GL. Four different ways of asking for pixels had all ended at the same diagnosis in our logs: frame production itself never happens. So before any multi-process work could pay off, EuroOS needed a working GL, and on a machine with no GPU driver that means SwiftShader, Google's software Vulkan implementation, running entirely on the CPU.
Two kernel changes made it real. SwiftShader is compiled for AVX2, so the kernel now enables AVX state management (CPUID-gated, with XSAVE context switching) when the CPU offers it. And a one-line-sounding bug with a big blast radius: the Vulkan loader opens its driver manifest as /pack/./vk_swiftshader_icd.json, with a /./ in the middle, and our byte-exact file lookup did not normalize paths. No manifest, no driver, no GL. The kernel now resolves paths the way POSIX means them, and SwiftShader came up: a real Vulkan device, on a kernel we wrote, with no GPU anywhere in sight.
Then the process model had to be true
Multi-process Chromium is a merciless test suite for an operating system. The browser forks helpers, each helper re-executes the browser binary as itself (execve("/proc/self/exe")), the browser kills and reaps them, and every one of them assumes Unix descriptor semantics down to the letter. Chromium ships with internal checks that crash the process the moment a kernel bends those rules. It found every shortcut we had.
What this phase forced into the kernel, each one found by a named crash and fixed as general semantics rather than browser glue:
A complete descriptor lifetime model
A file descriptor number may never be reused while any process still holds it. That one sentence unfolded into five layers: the shared table, marks for inherited descriptors, deferred parent closes across every descriptor class, child-owned descriptors that genuinely free on close and exit, and per-process aliases resolved for every descriptor-taking syscall. Chromium's own ownership checks now pass.
A real process lifecycle
fork, execve of /proc/self/exe, kill, wait4, and exit semantics where a thread ending its process ends only its own process. Per-process address-space state now follows the running task instead of being swapped around each syscall, which closed a whole class of cross-process corruption.
A cross-process page cache
Every helper re-executes the same 180 MB binary. Instead of each child re-reading it from disk, immutable pages are now cached once and shared read-only between processes, with copy-on-write when someone writes. Children start in seconds; a fork shares pages instead of copying hundreds of megabytes.
Unix sockets, to the letter
A message carrying only file descriptors and zero data bytes must still make the socket readable. A drained stream whose peer closed must read as end-of-file, never as try-again. Both are exactly the kind of detail Mojo, Chromium's IPC layer, silently depends on.
The week the machine froze, and how it confessed
Halfway through, a new failure appeared: the entire virtual machine would freeze. No panic, no log line, no survivor. A frozen from-scratch kernel with an empty serial port is about the least debuggable object in computing, so we built the debugger into the kernel itself: a non-maskable interrupt probe. Inject an NMI from outside the VM and the kernel prints where it was stuck, the machine code at that address, which task was running, its last syscall, and, after a few iterations of sharpening, the exact memory address the stuck loop was polling, matched against a table of every lock in the kernel.
========== NMI PROBE (wedge RIP capture) ========== [nmi] interrupted RIP=0x62a3878a RFLAGS=0x40002 (interrupts off) [nmi] current task 37 "DevToolsPipeHand" last-syscall=read(fd 3) -> 154 [nmi] polled static @ 0x668c9338 -> PIPE_WAITERS ========== END NMI PROBE ==========
The probe named the culprit in plain text: a reader thread spinning forever on the pipe-waiters lock, because the kernel-side pump that feeds that pipe had been holding the lock with interrupts enabled, got preempted by the timer inside the critical section, and could never run again. The fix became a rule, enforced at every lock in the subsystem: no spinlock is ever held with interrupts enabled. The freeze family died that day, and the probe stays in the kernel as a permanent instrument.
The frame that crossed a process boundary
With the semantics true and the freezes gone, the pieces simply worked in sequence, each visible in the logs: the browser forked its helpers, the helpers re-executed and connected their channels, navigation started, the out-of-process renderer parsed and laid out the page and answered JavaScript over the DevTools bridge, and then, on run 33 of the campaign:
And on the desktop, where it counts
Kernel milestones are only real when a person can see them. Type chrome in the EuroOS Terminal and the browser opens in a window on our own desktop, drawn by our own display stack: tab strip, address bar, navigation buttons, and the rendered page. In our verification run the first paint took about eight and a half minutes under CPU emulation, and the browser then sat there, pixel-identical and fault-free, for the next twenty-three minutes straight.
We also pointed it at the live internet again on the new kernel: DNS resolved through our stack, a TCP connection reached euro-os.eu, TLS came up on port 443, and the browser composited its first content block from the real site inside the scripted verification window. A fully rendered live page in that window is still ahead of us, and the August post already showed it is reachable with patience.
Honest footnotes, as always
- Verified: a multi-process Chromium session (browser plus out-of-process renderer and service processes) rendering and delivering a full frame; the desktop browser painting the complete UI and page and staying stable for 23+ minutes with zero faults; TLS established to euro-os.eu with a first composited block. All on the stock kernel under QEMU emulation, reproduced from scripts in the repository.
- The GPU work currently runs in the browser process. A dedicated GPU child process starts fast now and no longer breaks anything, but its own GL bring-up still outruns the browser's relaunch patience under emulation. Documented future work.
- All timings are under TCG CPU emulation, which runs roughly sixty times slower than native. On real hardware these minutes are seconds.
- The public download image is untouched by any of the experimental configuration; the multi-process path sits behind a development toggle.
- Every screenshot in this post is in the repository under
docs/proof/with the commit that produced it.