Release notes
QSOE 0.3
A disk you can write to, check, and keep — and QSOE/L running on every hart it was given.
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
| 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: shell, drivers, servers, utilities | 0.14 | 0.18 |
mr-bml |
the bootloader (separate project, shipped alongside) | 0.9 | 1.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.cnames, 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 inMsgReceiveand 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
EPERMabout 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
timeCSR of that instant, as a snapshotrtcadvances 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).
KernelIRQReportingis 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 carryMsgSendv,MsgReadv/MsgWritev,MsgSavereplyandMsgReply;MsgErrorexists. - 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 tolibc/api/, as on Skimmer. - seL4 pinned to 16.0.0; the version string comes from
git describe, and the kernel's own is16.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_ATTACHanswers the hart the attaching thread runs on, for a main thread and a worker alike, andsysinfoshows it. - The wall clock (above): taskman holds the epoch offset, sets it on
TM_REQ_CLOCK_SETTIMEas 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 everyclock_gettimeon the FU740.
Memory and the loader
- taskman loads
DT_NEEDEDon both kernels, each library at0x68000000 + 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
mmapcost: a recycled megapage is zeroed only as far as the request — 13 ms per 4 KiBmmapbecame 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.
waitpidkeeps its word on QSOE/L — every form of its first argument, the pid reported;wait4is shared libc.cmd > /dev/nullwrites on QSOE/N. It answeredEROFS: 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 lsworks.setitimerandSIGALRM;TimerCreate/TimerDestroyon 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
qrvfswrites: 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 anELOOPcap,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:
cp2.7 s → 0.7 s, the stress 98 s → 26 s. fscheck, on target and host, with-pto repair and-cto check only when the volume was left mounted or the mount count says so;initruns it before the mount.- A volume carries a label.
mkfs-qrv -L,qrvlabel;mount,fscheck,statvfsand/sys/mountsreport it. The release image isqsoe-usr. sync(),fsync(),fdatasync(); every server answers an_IO_SYNC, one flush per disk, and the flush runs before halt and reboot._IO_SETATTRand_IO_NOTIFYin the framework:chmod/chown/truncatereach a filesystem, andpoll()is real — pipes, serial lines and the resource-server framework notify; the video console tells a poll the same truth a read does.fs-tmpfsshrinks a file (a partial truncate returnedEINVAL) and honorschownandutimes.- readdir while unlinking no longer skips every other name (
rm -rhad left half a tree). A Provider with noreadlinkanswersEINVALsilently, 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'smsglenis what landed in its buffer, not what the sender said.
Drivers and hardware
- The VisionFive 2 on both kernels:
LOAD_PAat0x48000000because the EFI memory map, not the device tree, is the account that matters; a PLDA XpressRICH PCIe host inpci-server; DesignWare Ethernet throughdevn-dwmac; the on-screen console;PLAT_VF2selecting seL4's own star64 platform with no new patch. - The block drivers gained a write path:
devb-nvmeanddevb-virtiowrite, 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_carvekeeps 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 underlib/. 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,READDIRunder/net/<station>, read-only, scoped;qfetchworks against it. A path-formstatbelow 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, withREG_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,getgrnamask taskman (TM_REQ_ACCOUNT_QUERY) instead of walking the files; the hash never travels.poll()stops guessing:_IO_NOTIFYand 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_asserttells the two wire-frame families apart, which had been a recurring source of offset bugs. qsoe_conn_window(),qsoe_proc_self_info(),qsoe_errprintf();statvfscarries the volume's label.uname()keeps its POSIX shape and reads/sys/osversion; the kernel pair is not folded intoreleaseorversion, 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,manthrough the pager inPAGER,fscheck,qrvlabel,test_fsstress,test_bench,test_liveness.qviewscrolls and wraps and is a symbolic link beside the program it names. ps -Hshows the hart a thread runs on;mountshows the label;/sys/statscounts taskman calls per bucket.- qsh's
timeprinted “1m37.-95s user” — a macro right only at 1000 Hz; and/usr/bin/time lsfindsls. - New:
date— the POSIX shape,date [-u] [+format]to print anddate mmddHHMM[[cc]yy][.ss]to set the system clock (rtc datestores it into the hardware);uname -kand-K, appended to-athe way GNU carries its own extension last.rtcopens a gated clock before the first access and advances a boot-time snapshot;sysinfoshows 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 stepsCLOCK_REALTIMEan hour forward and back whileCLOCK_MONOTONICstands still. It is fully green on both kernels, on every board.
Boot, build and tooling
- Every image is checked before it boots:
make nvmerunsfscheckover the image it lays;make -C boot deployputs kernels and the partition image on a board through mr-bml. - Versions come from
git describein 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_irqcoresays 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 intnames 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_benchand/sys/statsrather 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:
nanosleepon seL4 is a spin onrdtimeand 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,TimerTimeoutfor a condvar, a boundedInterruptWait— absent, which is also why a boundedpoll()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(), nopwrite(), 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/con1can map it;fs-tmpfsdoes 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 datesays so. - No interrupt fire count on QSOE/L. seL4 signals the driver's
notification itself and keeps no tally;
sysinfoshows “-” 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.