QSOE 0.3 released

 QSOE 0.3 is out today. In component terms:

Component What it is 0.2 0.3
nq QSOE/N — the Skimmer microkernel and its taskman 0.26 0.30
lq QSOE/L — the seL4 taskman and its kernel seam 0.22 0.26
libc the shared C library and crt0 0.15 0.19
quser the shared userspace 0.14 0.18
mr-bml the bootloader, shipped alongside 0.9 1.0

0.2 gave you somewhere to write a file. 0.3 gives you somewhere to keep it. The qrvfs root on the NVMe is read-write on both kernels now, a checker looks it over before every mount and repairs what a lost power supply leaves behind, and a stress that fills the disk to its last block and reads every byte back passes on the boards. That is the spine of the release. The other headline is that QSOE/L stopped being ten times slower than QSOE/N, for a reason that was hiding in plain sight. As always both kernels — QSOE/N on Skimmer, QSOE/L on seL4 — carry one identical userspace, and a good fraction of what follows is bugs that only surfaced because the same test finally runs against both.

The disk keeps what the system wrote

For the whole life of the project qrvfs was read-only: you could boot from /usr and you could not create a file on it that survived. 0.3 closes that top to bottom, and most of the work was in establishing one invariant and then refusing to break it — at every successful operation the on-disk inode, bitmap, directory entry, link count and block ownership agree. Extended to the failure points too, because a crash test is meaningless otherwise: a crash may leak, it must not corrupt. Allocated-but-unreferenced is what a checker reclaims; two files sharing a block is not.

The filesystem is write-through — a write() that has returned has reached the driver — and it publishes its metadata in an order a power cut cannot make inconsistent: data before the pointers that name it, a length published last. fscheck is a real checker, bitmap against ownership and block pointers against lengths, that runs on a device node on the target and on an image file on a development machine; init runs it before /usr is mounted. A volume shut down cleanly is mounted, one that was not is repaired, one that cannot be is mounted read-only so the system still comes up. Ninety-six power cuts under a filling stress produced only the leak-class states the design allows for, every one of them repaired.

QSOE/L stops being ten times slower

Since the seL4 MCS port, every thread QSOE/L ever ran had been placed on hart 0. seL4's default is that a thread runs where its creator does, and the creator is always taskman, so a four-hart Unmatched had been running the whole system on one core the entire time. taskman now places threads round-robin — the policy Skimmer already applies in its own kernel — and on the Unmatched the writable-filesystem stress went from 731 s to 162 s, and ls -la / /usr from 1.007 s to 0.12 s. Nothing in the emulator could show it. The board did, in one ps.

Two more things closed most of the remaining gap. On seL4 a message larger than the IPC buffer had been copied by taskman, which cannot be a party to a message it is itself waiting on — loading a program off disk, it had the filesystem send a block to the driver whose receive then asked taskman to copy. qsoe_conn_window() now has taskman map a run of frames into both ends of a connection once, at a program's start; from then on MsgSend writes into the window and the server reads out of it, and taskman is never asked. The filesystem moves whole blocks on both kernels — on Skimmer the same call is a no-op, since that kernel carries any size inline — and five messages per block became one: cp on QEMU went from 2.7 s to 0.7 s. And a recycled megapage on seL4 is now zeroed only as far as the request, turning a 13 ms mmap of one page into half a millisecond.

The last of it was measured rather than guessed. /sys/stats counts taskman calls per bucket and test_bench prints microseconds per primitive, and together they found the two things that made every QSOE/L taskman call cost a millisecond on the FU740 — a timer sweep that walked the process table twice over before every dispatch, and that ls -l read /etc/passwd twice per entry, which getpwuid() now asks taskman instead, from the cache it already keeps to authenticate logins. QSOE/L is still slower than QSOE/N, a little over twice on the same disk where it was ten times, and the notes say where the rest lives rather than implying it away.

taskman is the loader

No binary carries an interpreter any more. taskman walks a program's DT_NEEDED list, places and relocates each library, and starts the program at its entry; the run-time linker is neither built nor shipped, and the relocator is libtaskman's, written once for both kernels. Being the loader is also the thread that ties several of this release's constraints together — taskman reads a program as an ordinary client of the filesystem holding it, so while it is parked in that read it serves nobody. A worker pool to fix that exists on QSOE/N, but it hung the K3 at boot and is held back until the cross-hart wake is found, so for now taskman still dispatches from one thread on both kernels.

What being the loader did buy is that one copy of a library's code section can back every process. libc.so's read-only segment takes no relocations and lands at a fixed address, so it need not be copied per spawn — on Skimmer through a single supervisor page-table bit (PTE_SW_SHARED, one of the two Sv39 leaves to software) marking a leaf an address space maps but does not own, and on seL4 through frames retyped from taskman's own pool that no process's exit can revoke. That bit had to exist first to fix a quieter bug: a process that mapped a published region freed the owner's physical pages when it exited, because the teardown walk frees every valid user leaf and there was no refcount anywhere. Nothing failed loudly; the owner's data quietly became whoever got the pages next. With sharing in place, one boot dropped from 4384 mapped pages to 3514 — roughly eighteen processes holding one copy of libc instead of eighteen — and a qsh now costs 143 pages to spawn where it cost 200.

Identity, and the wedge that named itself

0.2 let a server ask who was talking; 0.3 made the answer right on every path. On QSOE/N a path-form verb — unlink, rename, stat, chmod — goes straight from the client to the owning filesystem, and taskman refuses to relay anything. On QSOE/L, where a client cannot attach without taskman, the relay stays but is minted with the caller's credentials rather than taskman's. Before this, relayed verbs ran as root on both kernels — a hole straight through every filesystem's permission check — and now a directory must be searchable to be crossed and an ordinary user can no longer remove root's file from a 0700 directory. That one was found by the suite, not by anyone shorter.

The kernel also learned to say when something is stuck. Once a second, Skimmer walks taskman's threads and names any that has waited more than five seconds on a party it does not control — the pid and channel, the reply it is owed. It read the K3's Ctrl-Alt-Del wedge off the console in one boot, after a week of theories that had blamed NVMe: taskman's flush-before-reboot had sent _IO_SYNC to the very process that requested the reboot, itself reply-blocked in taskman. The fan-out skips its requester now, on both kernels, and every mounted filesystem is flushed before the machine halts.

A third board

The StarFive VisionFive 2 (JH7110) boots both kernels: a PLDA XpressRICH PCIe host in pci-server, DesignWare Ethernet through devn-dwmac, and the on-screen console through HFI. On Skimmer it cost the discovery that the EFI memory map, not the device tree, is the account that decides where an image may land; on seL4, that two boards must not share one generated header — the second built, linked, passed every check and booted the wrong image. The root port's own BAR registers decode to the bridge's control block, so the BAR-sizing scan was writing all-ones into the aperture it was scanning through until those were parked at zero; and the PCIe MSI controller's INTx bits had been asserting since first boot, quietly pinning a hart, until they were actually acknowledged. Three boards now — Unmatched, K3, VisionFive 2 — though the K3 is QSOE/N only in this release.

The date, and which kernel this is

seL4 keeps no wall clock, so QSOE/L's CLOCK_REALTIME had been the count since boot: every timestamp said 1970 and clock_settime was "not implemented." taskman now holds the offset to the epoch and publishes it in the one system page every process shares, rewritten in place when the clock is set, so a read costs a register and an addition. On the K3 the date lives in the platform's management processor, reachable only over SBI, and Skimmer asks it once at boot — the SoC's own counter block, which the device tree calls a clock, starts from zero at every reset and never was one, which is why the K3 had been booting to 1970 twice over. date prints and sets the clock on both kernels now.

And the login banner can state two version numbers, because the OS and the kernel are two artifacts: QSOE/L 0.26 (2026-09-16) on seL4 16.0.0-qsoe-lq-0.26. A patched seL4 says so in its own version string; on QSOE/N a boot archive older or newer than its kernel now shows as such instead of passing for the same build. /sys/kernelname and /sys/kernelversion sit beside /sys/osname and /sys/osversion, uname -k and -K print them, and an identity taskman cannot source reads as an empty line, never as a number it made up.

The userspace to use all of this with grew too, mostly on the back of a grep port. QSOE had no regular expressions, so 0.3 imports musl's TRE engine with the REG_STARTEND a BSD grep needs, and the port then failed to link against a scattering of things simply absent — <assert.h>, getopt_long, setlocale honestly restricted to the one locale that exists, and mblen, which had been declared in a header and implemented nowhere so libc.so shipped an undefined symbol the loader reported on every spawn. grep, mv, cp, chmod, ldd, which and a man that pages through $PAGER all arrive on the back of that. The shared suite went from 286 checks to 502, fully green on both kernels, on every board.

Next

0.4 is sound — the GK208's HDMI audio function brought up as deva-hdmi. Audio comes this early on purpose: it is the instrument for every measurement from here to 1.0, because a dropped buffer is audible long before it is visible. With it come the two things both kernels still lack — a real nanosleep on QSOE/L, which is a spin on rdtime today because taskman has no timer of its own to wake a sleeper, and the timed waits neither kernel has yet.

Pre-built images are at github.com/qsoe-dev/dl, source at gitlab.com/qsoe/os (make prepare checks out exactly the 0.3 set), and the full release notes — with the exhaustive change list and the known gaps stated rather than implied away — are at qsoe.net/qsoe_0.3.html.

Comments

Popular posts from this blog

QSOE project v0.1 released

A free QNX-like operating system (2003)

How QSOE started