Engineering blog · community feedback · fixes shipped

The critics were right.

A tester on the Hungarian tech forum hup.hu ran EuroOS and posted exactly the kind of criticism that is worth more than any compliment: specific, technical, and correct. Two concrete bugs and a pointed question about Rust. We took all of it apart. Here is what was real, what the actual mechanism turned out to be, what we changed, and how you can check the result yourself.

Good criticism is a gift

It is easy to build for the people who already like what you do. It is far more useful to hand your work to someone who will boot it, watch the fan spin up, and tell you precisely why. That is what happened on hup.hu: a reader took the public EuroOS preview image for a spin and wrote up two hard technical complaints plus a fair philosophical jab. None of it was hand-waving. All of it deserved a real answer in code, not a forum reply.

Our house rule, restated up front: we under-claim on purpose, we never dress a demo up as the real thing, and we credit the people who find our bugs. This post is a worked example of all three. One of the two fixes is fully verified; the other we can reason about but honestly cannot confirm in our own test rig, and we will say so plainly and ask the reporter to help.

Complaint one: “it sits at 98% CPU while doing nothing”

The desktop is idle, nothing is happening on screen, and one host core is pinned at nearly 100%. A real operating system halts the CPU when there is no work; this looks like a busy-loop that never sleeps. Paraphrased from the hup.hu thread.

The observation was completely correct, and the instinct behind it was reasonable. The proposed mechanism, an idle loop that forgets to hlt, turned out to be wrong, and chasing down the actual cause is the interesting part.

Our compositor loop does halt: it executes hlt on every iteration and wakes on the next interrupt. So the first job was to prove where the cost really was, not to assume. We replaced the entire loop body with a bare halt-and-continue and measured again. Still pinned. That single experiment ruled out rendering, housekeeping, everything in the loop:

host · idle-CPU bisection
# desktop loop reduced to: hlt; continue;  (nothing else)
bare-hlt loop, steady state ......... host-CPU 82.1%
# so the cost is NOT the loop body. hlt is returning almost instantly.

If a halted CPU wakes instantly, something is delivering interrupts, or something else is running. So we instrumented the halt: how many times does the loop actually wake per second, and how does that compare to our 100 Hz timer?

host · wake accounting
[probe] timer ticks elapsed: 2982   loop wakes: 486   (~16 wakes/sec)
# the desktop wakes 16x/sec, but the timer fires 100x/sec.
# ~84% of scheduler slices are going to OTHER tasks.
[stall] task 6: Ready (cr3=0x144000)   task 7: Ready (cr3=0x14b000)
[stall] task 8: Ready   task 9: Ready   task 10: Ready
[stall] summary: 6 Ready, 4 Sleeping (of 27 tasks)

There it was. The “idle” desktop was not idle at all. It had twenty-seven tasks, and five of them were user processes stuck in the Ready state, running flat out. The scheduler was dutifully round-robining between them, so the desktop itself only got about one slice in six, which also explains why input felt sluggish. Under pure emulation, where a spinning guest task pins the host core one-for-one, five spinners are more than enough to hold a core at 90-plus percent.

Why were they there?

Because the public download image was, embarrassingly, our developer self-test build. Over months of bring-up we had accumulated a boot-time gauntlet that proves the process model works: counter tasks, a heartbeat daemon, per-process thread-local storage, memory-isolation probes, fork and waitpid, pipe IPC, pthreads, our own IPC ports. Most of those run once and exit. But a handful, the counters, the daemon, the TLS demos, are deliberately infinite loops. Wonderful as a proof under a debugger. Ruinous as the thing a stranger downloads and calls “the operating system.” The tester was not seeing an idle hang; they were watching our test suite run forever.

The fix is a clean separation. The entire demonstration and self-test workload now lives behind a build feature. Developer and CI builds keep every last probe. The public image is built with that feature switched off, so the only runnable task on a quiet desktop is the compositor, which halts. We measured the result on the exact image that ships:

MetricBeforeAfter
Idle host CPU (emulated)82-96%5.8%
Live processes at idle274
Desktop share of CPU slices~16%~100%

The desktop, the shell, the windows and the panels are all exactly as before; nothing visible was removed. What went away was fifteen background test processes the public had no reason to be running. The residual six percent is the honest cost of a 100 Hz timer and a periodic clock redraw under an emulator that runs the CPU roughly sixty times slower than metal. On real hardware it is a rounding error.

Complaint two: “under KVM the keyboard and mouse are dead or crawling”

On a machine with hardware virtualization the input barely works, or stops entirely. The interrupt setup looks fragile: legacy virtual-wire, the IO-APIC, and the old PIC all in play at once. This is the classic way to get input that works under slow emulation and dies under KVM. Paraphrased from the hup.hu thread, and it was a good catch.

This reporter clearly knew exactly where to look, and pointed us straight at a real hazard. Part of the “crawling input” is simply the first bug: a desktop starved to one slice in six responds to keystrokes slowly. Fixing the CPU famine gives input back most of its responsiveness for free. But the “dead under KVM” half is a genuine, separate interrupt-routing problem, and the diagnosis was aimed at precisely the right place.

During early boot, before the IO-APIC exists, EuroOS runs the local APIC in virtual-wire mode: pin LINT0 is set to ExtINT so the legacy 8259 controller can still deliver the keyboard and mouse. Later, once the IO-APIC is up, we mask the 8259 completely and route the keyboard and mouse through the IO-APIC instead. The bug: we masked the old controller but left LINT0 configured as ExtINT.

That is a latent trap. An ExtINT delivery tells the CPU to run an interrupt-acknowledge cycle against the 8259 to fetch its vector number. But the 8259 is now masked and inert, so it never answers, and the core latches a garbage vector. Software emulation shrugs this off. A real KVM local APIC is stricter, and a stray ExtINT in that state can wedge further interrupt delivery, which is exactly the “input goes dead” symptom. The fix is one line of intent: once the IO-APIC owns the keyboard and mouse, mask LINT0 so that path cannot fire at all.

Here is where we keep ourselves honest. Our continuous-integration rig has no hardware virtualization at all; it emulates the CPU instruction by instruction. We cannot reproduce KVM here, which is precisely why this bug survived long enough for a reader to find it. We verified that the change does not regress the working path (keyboard interrupts still arrive correctly through the IO-APIC after the fix, and boot stays clean), and the reasoning is sound. But we have not, and in this environment cannot, confirm the cure on real KVM hardware. So this is an open call: if you saw dead input under KVM, please retest the new image and tell us. The person who reported it is better equipped to close it than we are.

The harder jab: “but it is written in Rust, so it must be safe, right?”

The thread also carried the familiar and fair criticism that Rust is oversold: that memory safety is not a magic wand, that unsafe still exists, that safe code can still leak memory and deadlock. All true, and worth saying clearly, because we agree.

Rust does not make bugs impossible. It makes a specific and common class of bug, use-after-free, buffer overruns, data races on shared memory, into compile errors instead of Tuesday-afternoon crashes. It does nothing about logic errors, and the two bugs above prove the point better than any slogan: one was a build-configuration mistake, the other an interrupt-routing oversight. No language would have caught either. In a companion post we describe a self-deadlock we wrote in perfectly safe Rust, a lock taken twice because a temporary held it too long. Safe, and still a bug.

What Rust buys a kernel is not infallibility; it is concentration. The hardware-touching, pointer-juggling, genuinely dangerous code is confined to small, marked unsafe islands we can enumerate and audit, while the large majority of the system, the filesystems, the compositor, the network stack, the capability engine, is memory-safe by construction. That is a real and valuable property. It is also not a substitute for testing, for review, or for handing the thing to a sharp stranger on a forum. Which is rather the theme of this entire post.

Try it yourself, and prove us wrong again

The fixes are in the public preview image, version 2026.07.22. You do not have to take our word for any of the numbers above:

Download the preview image Try it live in the browser

Honest footnotes, as always

  • Verified: on the shipping image, idle host CPU under emulation fell from 82-96% to about 5.8%, and the live process count fell from 27 to 4. Measured by sampling the QEMU process's CPU time over steady-state windows after boot. The desktop, shell and windows are unchanged.
  • Verified: developer and CI builds still run the full self-test and demonstration suite; only the public image omits it. The two build variants come from a single source tree behind one feature switch.
  • Reasoned but not verified: the LINT0 masking fix for input under KVM. Our CI is emulation-only and cannot run KVM, so we confirmed only that it does not regress the working interrupt path. Confirmation on real KVM hardware is genuinely wanted.
  • The quotes above are paraphrased from the thread for length and translation, not verbatim, and are meant to represent the reporters' points fairly rather than to put words in their mouths.
  • Thank you to the readers on hup.hu who took the time to be specific. Sharp criticism, delivered with a stack trace, is the most useful thing an open project can receive.
The Chromium bring-up The platform All posts