QSOE Systems

Release notes

QSOE 0.3

A disk you can write to, check, and keep — and QSOE/L running on every hart it was given.

September 2026 · succeeds 0.2 (September 2026) · Apache-2.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, a checker looks it over before every mount and repairs what a lost power supply leaves behind, and a stress that fills it to the last block and reads every byte back passes on the boards. Around that: a taskman that is the program loader and shares one copy of a library's text with every process, identity that reaches every server truthfully, a third board, and grep, mv, cp, chmod, man and symbolic links to use it all with.

The release also found the reason QSOE/L was slow: since the seL4 MCS port, every thread it ever ran had been placed on hart 0. One line of policy later, the writable-filesystem stress on the Unmatched went from 731 s to 162 s, and a directory listing from a second to a tenth of one. Where QSOE/L still lags QSOE/N it is said so below rather than implied away.

And on the last day, two questions a system should be able to answer got their answers: which kernel is this — named and versioned apart from QSOE itself, in /sys and by uname -k — and what time is it, which QSOE/L could not say at all and the K3 said wrongly.

The component set

ComponentWhat it is0.20.3
nq QSOE/N — the Skimmer microkernel and its taskman 0.260.30
lq QSOE/L — the seL4 taskman and its kernel seam 0.220.26
libc the shared C library and crt0 0.150.19
quser the shared userspace: shell, drivers, servers, utilities 0.140.18
mr-bml the bootloader (separate project, shipped alongside) 0.91.0

The umbrella records the tested set in component.list; make prepare checks out exactly these.

On this page

Highlights

Thirteen things that make 0.3 a different system to keep than 0.2.

A root filesystem that writes, and survives. qrvfs gained its write path: create, unlink, truncate, append, rename, symbolic links, directories that grow past their direct blocks, chmod/chown. It is write-through — a write() that returned has reached the driver — and it keeps its metadata in an order a power cut cannot make inconsistent: data before the pointers that name it, a length published last. fs-qrv mounts /usr read-write on QEMU, the Unmatched, the K3 and the VisionFive 2, on both kernels.

Checked before it is trusted. fscheck is a real checker — bitmap against ownership, block pointers against file lengths, link counts against names — that runs on a device node on the target and on an image file on a development machine, and 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 found only the leak-class states the design promises, every one repaired.

Filled, emptied, and read back. test_fsstress writes files of every awkward length across block boundaries, appends, grows a directory past its direct blocks, fills the disk to ENOSPC three times over and checks every byte it wrote. 157 checks, on both boards, under both kernels. It is also the instrument that measured the rest of this release.

QSOE/L on every hart. Every thread QSOE/L had ever run was on hart 0: the MCS port kept seL4's default that a thread runs where its creator does, and the creator is always taskman. taskman now places threads round-robin, the policy Skimmer applies in its kernel, and ps -H reports the hart a thread was made on. On the Unmatched ls -la / /usr went from 1.007 s to 0.12 s and the filesystem stress from 731 s to 162 s — from ten times QSOE/N to a little over two. Nothing in the emulator could show it; the board did, in one ps.

Windows: large messages without a third party. On seL4 a message larger than the IPC buffer was copied by taskman, which cannot be a party to a message it is itself waiting on — reading a program off the disk, it had the filesystem send a block to the driver whose receive then asked taskman to copy. qsoe_conn_window() 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, 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, because that kernel carries any size inline.

taskman is the loader, and a library's text is shared. 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. One copy of libc.so's text backs every process — on Skimmer through a page-table bit that marks a leaf a space maps but does not own, on seL4 through frames retyped from taskman's own pool that no process's exit can revoke.

Identity reaches every server, truthfully. 0.2 let a server ask who was talking; 0.3 made sure the answer was right on every path. On QSOE/N a path-form verb — unlink, rename, stat, chmod — goes from the client straight to the owning filesystem, and taskman refuses to relay anything. On QSOE/L, where a client cannot attach without taskman, the relay stays and is minted with the caller's credentials. A directory must be searchable to be crossed, and an ordinary user can no longer remove root's file from a 0700 directory — found by the suite, not by anyone shorter.

The kernel names a wait that goes on too long. 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: taskman's flush-before-reboot had sent _IO_SYNC to the very process that requested the reboot, 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 PCIe host, DesignWare Ethernet, the on-screen console through HFI. On Skimmer it cost the discovery that the EFI memory map, not the device tree, decides where an image may land; on seL4, that two boards must not share one generated header.

What a call costs, measured. /sys/stats counts taskman calls per bucket, and test_bench prints microseconds per primitive — a round trip, a stat, an open, an mmap, a password lookup — on whatever machine it runs on. 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 a 2 MiB zeroing per 4 KiB mmap), and the reason ls -l read /etc/passwd twice per entry — which getpwuid() now asks taskman instead, from the cache it already keeps to authenticate logins.

Which kernel this is. QSOE/L 0.26 said which QSOE you had and nothing about which seL4 it ran on; the two are versioned separately and can be loaded separately. /sys/kernelname and /sys/kernelversion now sit beside /sys/osname and /sys/osversion, uname -k and -K print them, and the login banner reads QSOE/L 0.26 (2026-09-16) on seL4 16.0.0-qsoe-lq-0.26, sifive,hifive-unmatched-a00. The kernel's version is the kernel's own answer: Skimmer publishes it in the information page it builds, and on QSOE/N a boot archive older or newer than its kernel now shows as such instead of passing for the same build. A patched seL4 says so in its version: 16.0.0-qsoe-lq-0.26 is upstream seL4 with this tree's PLIC patches.

The clock has a date. seL4 keeps no wall clock, and QSOE/L's CLOCK_REALTIME was 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 system page every process shares — one frame, mapped into all of them, rewritten in place when the clock is set — so a read costs a register and an addition, and a process started before the set sees the new time on its next read. 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 tree calls a clock, starts from zero at every reset and never was one. date prints and sets the clock on both kernels.

A wired interrupt lands on its thread's hart. seL4 enables a PLIC source on the hart that executes IRQControl_Get — taskman's, since only the root task holds it — and nothing on RISC-V moves it afterwards, so every device trap landed on taskman's hart and the interrupt thread was woken across harts, an IPI and a second kernel entry per interrupt. A fourth build-time patch gives the invocation an optional fifth word naming the target core (as ARM's GetTriggerCore does), taskman sends it for the thread it is placing, and test_irqcore proves the trap, the claim and the wake happen on one hart.

The full change list

Everything that changed since 0.2, by area. Open the parts you want — or open them all and read it as one document.

Kernels and IPC

Skimmer, the seL4 seam, and the message primitives above both.

Skimmer

  • Liveness. kernel/liveness.c names, once a second, any taskman thread blocked longer than five seconds in a send, a reply or a sync object, with what it waits on. A wait for work in MsgReceive and a sleep by choice are left alone; so are other processes.
  • A trap says what happened. A fatal user fault is reported in the vocabulary of the fault, with the registers named correctly; every trapframe offset is now verified by _Static_assert — 34 of 34, where five had been.
  • PTE_SW_SHARED. One of Sv39's supervisor-software bits marks a leaf an address space maps but does not own; teardown, unmap and protect honor it. It was needed before any sharing: a mapper of a published region freed the owner's pages when it exited.
  • Unlocking a mutex nobody holds is not an error on either kernel now; QSOE/L had answered EPERM about a mutex nobody owned — the last failing assertion of the shared suite there.
  • The kernel says which kernel it is: a new information-page tag carries Skimmer's own version, and taskman publishes it as /sys/kernelversion — never its own number, so a kernel older than the tag reads as unknown.
  • The kernel asks the platform for the date. Where the device tree names a clock kept by the platform's management processor (the K3), Skimmer reads it once at boot over SBI's Message Proxy and records it with the time CSR of that instant, as a snapshot rtc advances with no privilege at all. For a memory-mapped clock behind a gate the firmware leaves shut, the page also names the gate.

QSOE/L

  • Every thread gets a hart (above). KernelIRQReporting is off — spread threads exchange reschedule IPIs the kernel coalesces and then reports as spurious — and a third build-time patch to the PLIC driver enables a line on one hart only, the one that unmasked it.
  • Windows (above): TM_REQ_WINDOW_ATTACH / QUERY / DETACH, keyed by the connection badge, fenced on both sides, the server's side kept until the server exits, frames pooled and scrubbed. They carry MsgSendv, MsgReadv/MsgWritev, MsgSavereply and MsgReply; MsgError exists.
  • A client's identity rides in the badge, above bit 31, in a fail-safe layout: a badge lost or never stamped reads as zeroes, and taskman refuses to mint one for an id it cannot represent rather than truncate it into the superuser.
  • Relays carry the caller's credentials (ConnectAttachAs); a direct-pulse channel waits rather than binds, which is what a lost pulse notification had killed networking with.
  • One call for the whole seam. Forty-odd sites that each built an seL4 message by hand call qsoe_tm_call; the LQ-private opcodes live in one header. The seam moved to libc/api/, as on Skimmer.
  • seL4 pinned to 16.0.0; the version string comes from git describe, and the kernel's own is 16.0.0-qsoe-lq-<version>, stamped from the tree the build compiled and shown as /sys/kernelversion.
  • The target core of a wired interrupt (above): the fourth build-time patch; TM_REQ_IRQ_ATTACH answers the hart the attaching thread runs on, for a main thread and a worker alike, and sysinfo shows it.
  • The wall clock (above): taskman holds the epoch offset, sets it on TM_REQ_CLOCK_SETTIME as root's verb, and publishes it in the system page, which is one frame shared by every process now where each child used to get a private copy. The word is two halves under a sequence number: a tag body is never 8-aligned, and a misaligned 64-bit read would trap into firmware on every clock_gettime on the FU740.
Memory and the loader
  • taskman loads DT_NEEDED on both kernels, each library at 0x68000000 + n·2 MiB, relocated against the scope built so far, the program last. rtld is gone; the relocator is libtaskman's, written once.
  • One copy of a library's text backs every process: master frames on seL4, the shared PTE bit on Skimmer.
  • Regions carry their granule, so a 4 KiB-granule device region maps as such; a mapper's exit no longer frees the owner's pages.
  • QSOE/L mmap cost: a recycled megapage is zeroed only as far as the request — 13 ms per 4 KiB mmap became half a millisecond under QEMU. CSpace slots come back on detach and on exit; a shell loop no longer exhausts the 4096-slot CNode.
  • QSOE/L worker threads are recycled. A process could create 31 threads in its life — the seam's allocator was a bump and taskman freed nothing before exit. A joined or detached thread's slot goes back, and taskman parks the worker's objects for the slot's next occupant.
Processes, sessions and signals
  • The sync fan-out skips its requester and sends to each server once; a terminating process's path registrations go with it, so a server started again can take its own name back.
  • A detached daemon can exit and be killed on QSOE/L. It could not: terminate took “detached” for “already a zombie”, skipped the teardown and sent no reply, and the daemon lived on with its signal thread frozen.
  • waitpid keeps its word on QSOE/L — every form of its first argument, the pid reported; wait4 is shared libc.
  • cmd > /dev/null writes on QSOE/N. It answered EROFS: the dup path cloned every OCB kind it did not know as a cpio file.
  • posix_spawn's p form searches PATH — it never had, on either kernel; time ls works. setitimer and SIGALRM; TimerCreate/TimerDestroy on QSOE/L.
  • Stubs announce on the program's standard error, not the kernel debug console a release kernel refuses to user programs.
Filesystems and resource managers
  • qrvfs writes: create, unlink, truncate (rewritten once the first power-cut round found blocks freed before the pointers naming them were written), append, rename, directories past their direct blocks, symbolic links followed with an ELOOP cap, readlink, lstat; st_blocks; search permission checked on every directory a path crosses.
  • Whole blocks to the driver on both kernels (windows on seL4, inline on Skimmer): five messages per block became one. QEMU, QSOE/N: cp 2.7 s → 0.7 s, the stress 98 s → 26 s.
  • fscheck, on target and host, with -p to repair and -c to check only when the volume was left mounted or the mount count says so; init runs it before the mount.
  • A volume carries a label. mkfs-qrv -L, qrvlabel; mount, fscheck, statvfs and /sys/mounts report it. The release image is qsoe-usr.
  • sync(), fsync(), fdatasync(); every server answers an _IO_SYNC, one flush per disk, and the flush runs before halt and reboot.
  • _IO_SETATTR and _IO_NOTIFY in the framework: chmod/chown/truncate reach a filesystem, and poll() is real — pipes, serial lines and the resource-server framework notify; the video console tells a poll the same truth a read does.
  • fs-tmpfs shrinks a file (a partial truncate returned EINVAL) and honors chown and utimes.
  • readdir while unlinking no longer skips every other name (rm -r had left half a tree). A Provider with no readlink answers EINVAL silently, which is what an lstat probe expects.
  • The framework accepts a request of up to 64 KiB when a Provider declares QSOE_PROV_BULK_PUSH; a resource manager's msglen is what landed in its buffer, not what the sender said.
Drivers and hardware
  • The VisionFive 2 on both kernels: LOAD_PA at 0x48000000 because the EFI memory map, not the device tree, is the account that matters; a PLDA XpressRICH PCIe host in pci-server; DesignWare Ethernet through devn-dwmac; the on-screen console; PLAT_VF2 selecting seL4's own star64 platform with no new patch.
  • The block drivers gained a write path: devb-nvme and devb-virtio write, flush on _IO_SYNC, and declare bulk push.
  • QSOE/L's on-screen console on the Unmatched: the firmware-window overlay is enabled for the board kernel, and devmap_carve keeps a bounded window of frames below a request instead of discarding hundreds of megabytes of a gigabyte-wide PCI window, so the console no longer costs the NVMe root.
  • libserdev: the serial drivers share a library, and every library moved under lib/. The erase sequences the video console swallowed are drawn. USB keyboard interrupt attachments are per process and dropped at exit.
  • The HFI handoff block may say only the firmware can repaint; the kernel's video-controller handoff is found by the kernel.
Networking — QSP
  • A station serves files: CONNECT, READ, FSTAT, READDIR under /net/<station>, read-only, scoped; qfetch works against it. A path-form stat below a station answers.
  • The relay stops narrating itself at INFO; a lost pulse notification no longer kills QSOE/L networking (a direct-pulse channel waits).
  • The relay's identity is not yet the originating client's — carried, not enforced; see the gaps.
The C library
  • Regular expressions (<regex.h>, TRE, with REG_STARTEND), getopt_long, <assert.h>, setlocale (one locale, C.UTF-8), getprogname, mblen, setlinebuf, tcgetwinsize/tcsetwinsize.
  • pread, realpath, rename, fsync/fdatasync/sync, wait4, setitimer, mprotect; five more POSIX entry points moved into the shared body from the seams.
  • getpwuid, getpwnam, getgrgid, getgrnam ask taskman (TM_REQ_ACCOUNT_QUERY) instead of walking the files; the hash never travels.
  • poll() stops guessing: _IO_NOTIFY and a pulse to the caller. On QSOE/L the timeout is not yet honored.
  • Path-form verbs find their own way to the owning server on QSOE/N; every path buffer is sized from one number, TM_PATH_MAX, and a _Static_assert tells the two wire-frame families apart, which had been a recurring source of offset bugs.
  • qsoe_conn_window(), qsoe_proc_self_info(), qsoe_errprintf(); statvfs carries the volume's label.
  • uname() keeps its POSIX shape and reads /sys/osversion; the kernel pair is not folded into release or version, whose meaning scripts depend on. ClockCycles() exists on QSOE/N — declared, implemented on QSOE/L, never there, and the first caller found out. The information page gained the kernel version, the wall-clock offset, and a clock record that can name a gate or carry a snapshot.
Shell and utilities
  • New: grep, mv, cp, chmod, ldd, which, man through the pager in PAGER, fscheck, qrvlabel, test_fsstress, test_bench, test_liveness. qview scrolls and wraps and is a symbolic link beside the program it names.
  • ps -H shows the hart a thread runs on; mount shows the label; /sys/stats counts taskman calls per bucket.
  • qsh's time printed “1m37.-95s user” — a macro right only at 1000 Hz; and /usr/bin/time ls finds ls.
  • New: date — the POSIX shape, date [-u] [+format] to print and date mmddHHMM[[cc]yy][.ss] to set the system clock (rtc date stores it into the hardware); uname -k and -K, appended to -a the way GNU carries its own extension last. rtc opens a gated clock before the first access and advances a boot-time snapshot; sysinfo shows the system and the kernel as two lines, and prints a hart or a count the kernel does not report as such, never as a number.
  • The shared suite grew from 286 to 502 checks (509 on QSOE/N): windows, thread churn, rename, search permission, readdir while unlinking, the filesystem group running both mounts side by side, a [uname] group that holds the OS name to the kernel it implies, and a clock case that steps CLOCK_REALTIME an hour forward and back while CLOCK_MONOTONIC stands still. It is fully green on both kernels, on every board.
Boot, build and tooling
  • Every image is checked before it boots: make nvme runs fscheck over the image it lays; make -C boot deploy puts kernels and the partition image on a board through mr-bml.
  • Versions come from git describe in every repository, and a tag that touches nothing no longer leaves the version header stale.
  • The quser build's exit status agrees with its summary line — it used to report success with components broken and stage their previous binaries.
  • mr-bml 1.0 and HFI BIOS 1.4.1 (the 1.4 quiesce regression that withdrew the GK208's doorbell on the Unmatched is fixed there). Multiboot3, the boot protocol mr-bml implements, is at version 1.0 too and no longer a draft: every tag is two 64-bit words, the header lives in the ELF note only, and the specification ships with the loader. mr-bml is published as a Debian package rather than a bare EFI binary: the image is assembled on the board, for that board's volume, which a prebuilt one could only get right for one machine.
  • test_irqcore says whether a wired interrupt reaches the hart its interrupt thread was pinned to, on the kernel under it, by ringing QEMU virt's goldfish RTC alarm from a pinned worker; -d int names the hart per trap.
Bugs worth naming

Found in this cycle, each with what it taught.

  • Every QSOE/L thread on one hart. A default carried through a kernel port, invisible to an emulator, worth a factor of five on a board. Placement is policy, and on seL4 the policy is taskman's to state.
  • A timer sweep before every dispatch that walked the process table through a table-scanning lookup — 16k record touches per message, a millisecond per taskman call on the FU740, measured with test_bench and /sys/stats rather than guessed.
  • The Ctrl-Alt-Del wedge was a send-loop deadlock, not NVMe: the liveness line named it in one boot after a week of theories.
  • A region mapper's exit freed the owner's pages on Skimmer; nothing failed loudly, the owner's data quietly became whoever got the pages next.
  • Relayed verbs ran as root on both kernels — a hole through every filesystem's permission check, closed two different ways.
  • Blocks freed before the pointers naming them were written in itrunc — the first power-cut round found it; 96 rounds since found nothing else.
  • Two generated headers shared by two boards, so the second board built, linked, passed every check and booted the wrong image.
  • The K3 booted to 1970, twice over. Its counter block is gated by the firmware and read as zeroes — and zero seconds since the epoch passes every check a clock that was never set would pass. Opened, it counted: from zero, at every reset. It was never the date; the date lives in the management processor, and Linux's own driver comment said so all along.
  • ClockCycles() declared but never implemented on QSOE/N. The loader warned — reloc skip: NULL slot — and nothing called it until the snapshot clock did, on the board, and jumped to zero. The emulator never reached that path.
  • A 64-bit word in a page where no body is 8-aligned. The first wall-clock offset was one 64-bit field in a tag body, which begins four bytes into an eight-byte stride; taskman's own alignment guard refused it, honestly. Two halves under a sequence number instead.

Open every section · the same ground is covered in the per-component CHANGELOG.md files in the source repositories.

Known gaps

Stated rather than implied away. These are the things 0.3 does not do, and most of them have a release number against them.

  • QSOE/L is still slower than QSOE/N — a little over twice on the filesystem stress on the same disk, where it was ten times. What remains is understood and listed: nanosleep on seL4 is a spin on rdtime and a yield, because taskman has no timer of its own to wake a sleeper, so a periodically polling driver is a busy hart; every kernel entry reads a time CSR the U74 traps to firmware; and taskman's dispatch is single-threaded on both kernels.
  • No timed waits on either kernel. pthread_cond_timedwait, TimerTimeout for a condvar, a bounded InterruptWait — absent, which is also why a bounded poll() is unbounded on QSOE/L.
  • A client REPLY-blocked on a server that exits parks forever on QSOE/L; the suite skips the case before sending.
  • taskman serves from one thread. A worker pool exists on QSOE/N and hung the K3 at boot; it is held back until the cross-hart wake is found.
  • There is no dlopen(), no pwrite(), no libm. The C Library Reference's new appendix compiles SQLite against 0.3 and lists exactly what it finds missing.
  • The console's cell array is published to peer 0, so any opener of /dev/con1 can map it; fs-tmpfs does not check search permission on the path; credentials do not cross the net.
  • A thread spinning in U-mode without ever blocking cannot be killed. The blocked half of that shipped in 0.2; the spinning half is still the ThreadCancel work package.
  • A clock kept by the platform cannot be set from QSOE yet (the K3): the date is read once at boot and advanced; writing it back needs a kernel call there is not yet, and rtc date says so.
  • No interrupt fire count on QSOE/L. seL4 signals the driver's notification itself and keeps no tally; sysinfo shows “-” rather than a zero that would claim the source never fired. A count in the interrupt thread's own page is on the 0.4 list.
  • The K3 is QSOE/N only in this release.

Getting it

Pre-built images are published at github.com/qsoe-dev/dl; the front page lists what each file is and how to run it under QEMU or install it on a board. To build from source:

git clone https://gitlab.com/qsoe/os
cd os
make prepare   # fetch the 0.3 component set (see component.list)
make           # build both variants: QSOE/N, then QSOE/L
make nvme      # the disk image, checked by fscheck as it is laid
make dist      # optional: the QEMU disk images

The manuals — Design.pdf, UserGuide.pdf, ProgrammingBook.pdf, AppPortingGuide.pdf, Networking.pdf and LibcReference.pdf, the last now covering 0.1–0.3 with an appendix on what SQLite asks for — are published alongside each release at github.com/qsoe-dev/doc. Documentation is a release gate: no version ships before the manuals are brought up to the tree.

Next is 0.4 — sound out: the GK208's HDMI audio function brought up as deva-hdmi. Audio is the instrument for every measurement from here to 1.0 — a dropped buffer is audible long before it is visible — which is why it comes this early. With it, a real nanosleep on QSOE/L and the timed waits both kernels lack.

Back to QSOE Systems