Project PhilosophyREADME.md

Most hobby operating systems are built on top of something: a bootloader crate that hides the transition into long mode, a uart_16550 crate for the serial port, an x86_64 crate for the page tables and interrupt descriptors, spin for the mutex. That is a perfectly reasonable way to get an operating system running quickly — but it also means the most interesting ten percent of the work happens inside somebody else's code.

Sword Operating System takes the opposite approach. The 16-bit boot sector, the 16→32→64-bit trampoline, the GDT and IDT, the VGA and framebuffer drivers, the IDE and AHCI disk drivers, the filesystem, the scheduler, and even the cryptography (SHA-256, HMAC, PBKDF2) are all written from scratch, in this repository, with zero external crates. It is no_std. The kernel does now own a small hand-written heap (added as part of the userspace arc described below — the ELF loader needs it to build error messages), but the tables the kernel keeps (the file table, the user table, the task table) are still fixed-size static arrays, the same discipline a microcontroller firmware would use — and the three programs that actually run in ring 3 are no_std and have no allocator at all.

That constraint is not nostalgia for its own sake. It is what makes the Incident Log further down the page possible at all: when something breaks, there is nowhere else in the stack to go looking.

Every byte that runs in real mode, protected mode, and long mode was written by hand for this project.
— the project's own README
Technical note · the July 2026 audit

A full technical audit of this codebase wrapped up in July 2026: 30-plus real defects found and fixed, ranging from a filesystem block allocator that could silently corrupt a different file than the one being written, to a guaranteed stack overflow on real UEFI hardware, to a handful of authentication weaknesses around the user table. None of that is presented here as “now it's bug-free.” A project that hand-writes its own mutex, IDT, and cryptography stack accumulates real defects as a simple consequence of its own ambition — finding and fixing them honestly is the actual engineering work. Every fix from this pass landed with a comment in the source explaining the bug it was written against, so it doesn't quietly come back.

One gap the audit could not close: hardware coverage. Every fix has been re-verified end-to-end on the BIOS boot path on real hardware. The UEFI path builds cleanly and every change to it has been reasoned through by hand, but it has not yet been re-run start-to-finish on a physical UEFI machine since these fixes landed.

[ back to top ]
Three Ways To Bootboot/

The same kernel is reachable through three different boot paths, because three different kinds of hardware — plus one emulator — each disagree about how a machine is supposed to wake up.

Boot images produced by build.ps1
ImageBoot modePartition schemeBuilt by
sos.img BIOS / Legacy raw, no partition table hand-written 512-byte MBR (stage1.asm)
sos.iso BIOS / Legacy, via El Torito ISO 9660 + hybrid MBR a from-scratch ISO builder — no mkisofs, no xorriso
sos_uefi.img UEFI only GPT + FAT32 ESP a separate x86_64-unknown-uefi binary, hand-speaking just enough of the UEFI Boot Services protocol to read its own kernel off disk
Technical note · the video problem

The kernel draws the same 80×25 console through two entirely different code paths, and which one runs is decided at boot. Under BIOS, it writes character codes directly into 0xB8000 — classic VGA text mode, and the hardware turns those bytes into glyphs on its own. Under UEFI, that memory window simply does not exist on real firmware, so the bootloader instead hands the kernel the firmware's graphics framebuffer (the Graphics Output Protocol), and the kernel draws every glyph itself, pixel by pixel, with its own bitmap font.

QEMU's emulated hardware happens to support classic VGA text mode even when booted via UEFI, because the emulated card is a genuine Bochs-VBE-capable VGA chip underneath. Real UEFI laptops are not that generous. The first time this kernel booted on an actual UEFI notebook, it came up perfectly — keyboard, disk, shell, all of it — and showed nothing whatsoever on the screen, because it was faithfully writing to a memory address that nobody on that machine was reading.

That per-pixel drawing also used to make scrolling visibly slow on real UEFI hardware: advancing one line meant re-rasterizing all 1,920 glyphs on screen from the bitmap font, roughly a million individual pixel writes per line feed at 1080p. The console now moves whole scanlines instead of redrawing every glyph — the difference between a laggy terminal and a normal one, on the same firmware.

Technical note · the storage problem

Until this audit, AHCI/SATA disks only worked when SOS booted via UEFI. The reason lived one layer below the driver itself: stage2, the 16→32→64-bit trampoline, only ever identity-mapped the first 4 MB of physical memory, and an AHCI controller's memory-mapped registers sit well above that on real hardware. Touching them from the BIOS path was an instant page fault, so the driver was gated to UEFI only — which meant any modern PC booted in Legacy mode, still the far more common case in the wild, saw no usable disk at all: nothing persisted, and the installer had nowhere to install to. It was the single largest functional gap in the project.

stage2 now builds three additional page directories and identity-maps a full 4 GB, so AHCI/SATA registers are reachable from either boot path, and both now have the same disk support.

Technical note · why the UEFI image shrank

sos_uefi.img dropped from 315 MB to 72 MB, and the smallest disk it can install onto from roughly 300 MB down to about 70 MB. FAT32, the format required for the EFI System Partition, refuses to format a volume with fewer than 65,525 clusters; at the previous 4 KB cluster size, that floor forced a 268 MB partition no matter how little the ESP actually held. Shrinking the cluster size to 1 KB moves the same floor down to 64 MB.

[ back to top ]
Kernel Corekernel/src/arch/x86_64/

Once the trampoline lands the CPU in 64-bit mode, the kernel's own _start takes over and rebuilds everything the trampoline had only improvised: its own Global Descriptor Table (the segments the CPU actually trusts from here on), its own Interrupt Descriptor Table (256 gates — one per CPU exception and hardware IRQ), and its own remapping of the two chained 8259 PIC controllers. The timer and keyboard IRQs arrive on vectors 8–15 by default, which collide head-on with CPU exceptions like the double-fault and general-protection vectors — so the very first thing the kernel does with the PIC is move them out of the way, onto vectors 32–47.

This audit also added a Task State Segment with an Interrupt Stack Table: the double-fault and page-fault handlers now run on their own dedicated stack, instead of continuing on whatever stack was active when the fault hit. That is the difference between a readable diagnostic message when the kernel corrupts its own stack, and a silent triple fault that just resets the machine with no clue why.

From there, the Programmable Interval Timer is the closest thing this kernel has to a pulse: reprogrammed to fire one hundred times a second, its handler does nothing more than increment a tick counter, and that single counter is what every notion of elapsed time in the system — sleep, disk-command timeouts, the scheduler's own bookkeeping — is built on.

Multitasking stays deliberately modest: a cooperative, round-robin scheduler in which each task owns a small dedicated stack and voluntarily hands control back with yield_now(). There is no preemption, no priorities, no timer-driven context switch — just enough to run a background task alongside the interactive shell without either one blocking the other.

Technical note · memory management, in three layers

The audit added the three pieces a kernel needs before it can host userland at all, in the strict order they have to come up: a physical memory manager (mm::pmm) that harvests UEFI's free-RAM map into a bump allocator; a kernel-owned virtual memory manager (mm::paging) that replaces the firmware's inherited page tables with ones the kernel built itself and so can safely hand out high userland addresses without clobbering its own identity map; and a heap (mm::heap) on top of the PMM, used narrowly — most kernel state still lives in fixed-size statics. pmm → paging → heap is the order mm::init() runs them in, and the order that matters: the heap is page-mapped, and paging needs the PMM to give it pages.

Technical note · zero_bss

The file the bootloader actually reads off disk, kernel.bin, does not contain the kernel's .bss section at all. Any static without an explicit non-zero initializer — the IDT table, the scheduler's task list, every fixed-size buffer in the shell — occupies memory once the kernel is running, but zero bytes in the file on disk. The very first line of _start, before anything else runs, is a hand-written zero_bss() that walks from the linker-provided _bss_start to _bss_end and blanks it by hand. Skip that step, and every one of those structures boots with whatever happened to already be sitting in RAM.

For most of this project's life, that also meant .bss had almost nowhere to grow on the BIOS boot path: it started right after the kernel's own loaded code and ran up against the VGA memory window at 0xA0000, leaving only about 20 KB of headroom. A completely unrelated one-line change — doubling Papyrus's text buffer from 256 KB to 512 KB — would have grown .bss straight through that ceiling and started corrupting VGA memory, breaking the BIOS boot silently while the UEFI build, which has no such low-memory ceiling, kept working and testing green. The fix relocates the kernel's .bss above the 1 MiB mark, growing the BIOS path's headroom from about 20 KB to 640 KB, and build.ps1 now checks both boot paths so a regression like that fails loudly instead of only on somebody's BIOS machine.

[ back to top ]
Userspace — Ring 3, ELF & Syscallskernel/src/arch/x86_64/usermode.rs · elf.rs · mm/paging.rs

Up to this audit, the most interesting code in the kernel all ran at CPL=0. There was no userspace, no syscall boundary, and no reason to have one — the shell dispatched commands that lived in the same address space as the kernel itself. That is now no longer true. The kernel builds its own page tables, drops into ring 3 with a hand-written iretq frame, parses a real compiled ELF, and services a small syscall ABI for it when it traps back. Three programs — hello, papyrus, sush — are built as standalone no_std ELF64 executables, embedded into the kernel via include_bytes! at compile time, and loaded as real ring-3 processes. The whole arc is UEFI only: it needs mm::paging, which needs a real firmware memory map. On the BIOS build, the same commands print a controlled "not available" message rather than reaching for hardware that is not there.

Technical note · dropping privilege, the long way round

enter_ring3 is hand-written assembly for the same reason context_switch is: the moment of actually dropping CPL requires a frame on the stack laid out exactly the way iretq expects (SS, RSP, RFLAGS, CS, RIP, in that order), with the ring-3 selectors gdt::load() installed at boot baked into CS and SS — there is no compiler intrinsic for that, and Rust's x86-interrupt ABI gives no way to redirect the eventual return. So the kernel saves the six callee-saved registers and its own RSP, builds that frame, sets IF=1 in the flags it pushes (ring-3 code starts with interrupts enabled), and iretqs. The return is just as deliberate: vector 0x80 is an interrupt-gate-at-DPL-3, so ring-3 code reaches it with a plain int 0x80; a cmp rax, 0 in the handler decides whether that trap is SYS_EXIT (abandon ring 3 entirely, restore the saved RSP and registers, ret) or any other syscall (dispatch in ring 0, then iretq right back where the int 0x80 came from).

The ABI itself borrows its register choice from Linux x86-64 on purpose: rax is the call number, rdi/rsi/rdx/r10/r8 carry up to five arguments (R10 instead of RCX for the fourth, exactly as syscall clobbers RCX and R11 in Linux — this kernel uses int 0x80 instead, but the convention costs nothing and avoids inventing a second one), and the return value lands in rax. RBX, RBP, R12–R15 and RSP are deliberately never touched by the handler's argument shuffle — they survive a syscall exactly as an ordinary call would leave them, which the Rust compiler relies on for callee-saved locals.

The syscall ABI (vector 0x80)
#NameSignatureWhat it does
0SYS_EXITAbandons ring 3, resumes the caller of enter_ring3
1SYS_WRITE(ptr, len)Prints a UTF-8 byte range to the console
2SYS_READKEY()Non-blocking pop from the keyboard queue; 0 means "no key waiting"
3SYS_READFILE(path, plen, buf, cap)Reads a whole file into a caller-owned buffer; 0 if missing
4SYS_WRITEFILE(path, plen, buf, n)Replaces a file's contents (creates it if needed)
5SYS_PUT_ROW_STR(row, col, color, ptr, len)Draws a string at a screen position, in one of three palette colours
6SYS_CLEAR_ROW(row, color)Blanks an entire screen row
7SYS_SET_CURSOR(row, col, offset)Repositions the logical cursor
8SYS_RUN_LINE(line, len, err, cap)Tokenizes and dispatches a shell line, exactly like the interactive shell — the foundation SuSH is built on
9SYS_GET_CURSOR()Returns (row << 32) | col; lets a ring-3 program do its own visual backspace

The ELF loader (kernel/src/elf.rs) parses ELF64 static executables field by field, with explicit bounds checks before every slice index — the same discipline sfs.rs applies to on-disk binary formats nobody controls, and for the same reason: a malformed or truncated file fails with a readable ElfError, never a panic, and there is no transmute anywhere in it. Each PT_LOAD segment is read onto fresh PMM pages, zeroed, the p_filesz bytes copied in (.bss stays zero), and mapped with USER (and WRITABLE when the segment says so) at the virtual address its own linker script chose.

Technical note · every program at its own address

The three programs used to share a single fixed virtual address (0x4000000000) on the assumption that "only one ring-3 program runs at a time." SYS_RUN_LINE breaks that assumption on purpose: a running SuSH script can dispatch a line like papyrus file.txt as one of its OWN lines, which loads a SECOND program into ring 3 while the first (SuSH) is merely paused, not finished. These are ET_EXEC binaries with no relocations, so their machine code contains absolute addresses baked in at link time — reusing the same address would have the inner program overwrite the outer program's still-needed code/data/stack. Each program now has its own disjoint range:

Per-program address layout (high userland, far above the kernel's 0–4 GiB identity map)
ProgramCode baseStack baseStackPurpose
hello0x40_0000_00000x50_0000_000016 KBThe minimal syscall proof — Hello from ring 3!
papyrus0x41_0000_00000x51_0000_000016 KBThe full-screen editor, moved out of the kernel (see below)
sush0x42_0000_00000x52_0000_0000256 KBThe interpreter, moved out of the kernel; larger stack because it recursesthat's how sush otherscript.sush is handled

The one case that layout alone cannot solve — a SuSH script invoking sush another.sush, i.e. the SAME program nested inside itself, which would still collide with itself — is handled differently: userland/sush treats sush <path> as a built-in of the interpreter (alongside if/while/read) and recurses with an ordinary Rust function call inside the SAME ring-3 excursion. No second ELF is loaded, no second enter_ring3 happens. The nesting limit is eight deep, enforced by one counter in enter_ring3_guarded — now the only way into ring 3, so the two commands that previously reached the raw assembly directly (ring3test/ring3hello) cannot bypass it the way they used to.

Technical note · two bugs the ring-3 audit itself uncovered

Closing a Papyrus session with Ctrl+Q used to lock up the whole machine. The cause is a one-instruction detail of how x86_64 privilege gates work: vector 0x80 is an interrupt gate (Type 0xE), and the CPU hardware-clears IF on entry to one, exactly as for a hardware IRQ. The normal syscall path restores IF via its own iretq, which pops ring 3's saved RFLAGS (IF=1, forced by enter_ring3). The SYS_EXIT path never executes an iretq at all — it discards that frame and does a plain ret — so IF stayed cleared for the rest of the kernel's life the instant any ring-3 code exited. No keyboard IRQ, no timer tick, nothing, ever again. The diagnostic commands never surfaced it because nobody typed right after them; closing Papyrus is exactly when a user reaches for the keyboard. A single sti before that ret closed it.

The second is one an earlier audit cleanly missed. Saving the outer excursion's RSP was necessary but not sufficient, because the CPU also reloads RSP from TSS.rsp0 on every ring 3→ring 0 transition — with no notion of nesting. With rsp0 a boot-time constant, a nested program's first int 0x80 pushed its interrupt frame at exactly the address the outer excursion's own frame, and the entire live call chain below it, already occupied — SYS_EXIT then faithfully popped garbage into the callee-saved registers and reted to a garbage address. enter_ring3 now repoints TSS.rsp0 at its own current RSP per nesting level and the SYS_EXIT path puts the previous value back — the ordinary "one kernel stack per thread of control" arrangement, achieved in hand-written assembly.

[ back to top ]
Storage & Filesystemkernel/src/fs/ · drivers/block.rs

block.rs is the single door the filesystem and the installer knock on to read or write a sector, and behind that door the kernel supports two entirely different disk controllers: IDE PIO, which moves every byte through the CPU via port 0x1F0, the way a machine from the IDE era would; and AHCI/SATA, located by walking the PCI bus, driven through memory-mapped registers and transferred by DMA — the way essentially every machine built in the last fifteen years actually works. As of this audit both are available from either boot path — see the Boot section above for why that wasn't always true.

On top of that sits SFS (Sword FileSystem), a from-scratch persistent filesystem with a real directory hierarchy and a FAT-like block table that doubles as both the free-space allocator and each file's own chain of blocks.

SOS will not format a disk that isn't its own.
— the one rule sfs::init() never breaks
Technical note · rmdir and the block-table cache

The one thing every earlier version of SFS could do but never undo was making a directory: mkdir created a permanent entry, with no rmdir to remove it, so every directory's block stayed allocated forever even once it was empty. This audit adds rmdir.

Writing a file was also far more expensive than it needed to be. The block table lives in 512-byte sectors, but each write only touches the 4 bytes belonging to one file's chain entry — so saving a 256 KB file, 512 blocks of data, cost roughly 2,560 sector operations, a full sector read-or-rewrite of the block table for every single block written. A one-sector write-back cache in front of the block table brings that down to about 520.

At boot, SFS only ever does one of three things: mount its own superblock if it finds one, format a disk that is provably blank, or — on finding an MBR, a GPT table, or filesystem debris belonging to something else entirely — stop and report it, untouched. That third case used to be untestable: before the AHCI driver existed, the only disk QEMU ever exposed for testing was an empty scratch image, so a bug that formatted "anything unrecognized" was invisible. The moment AHCI could see a real internal disk, that same bug would have erased a stranger's Windows partition the first time they booted the USB drive out of curiosity.

[ back to top ]
Shell, SuSH & Papyruskernel/src/shell/

The interactive layer is a single-line editor with a real cursor (arrow keys, Home/End/Delete, insertion at any position — not only at the end), command history, and a scrollback buffer pageable with Shift+Up/Down without losing the live prompt.

$ cat count.sush
set i 0
while $i < 5
    echo tick $i
    set i $i + 1
endwhile

SuSH began as a way to replay a fixed list of shell commands, and grew into an actual small language: variables with $name substitution, integer arithmetic inside set, if/else/endif, while/endwhile, a blocking read for interactive scripts, and comments — all interpreted directly over a flat script buffer with no heap allocation, and abortable with Ctrl+C, so a script whose loop condition never goes false doesn't require a reboot to escape.

Papyrus is the full-screen counterpart: a text editor that takes over the entire display to edit a file in place, with cursor navigation aware of multi-byte UTF-8 so accented text never gets split mid-character, Tab-to-indent that snaps to the next four-column stop, and Ctrl+S/Ctrl+Q to save and quit.

Technical note · Papyrus and the stack

Papyrus used to keep its entire editing buffer, 256 KB, on the stack. On the BIOS path the kernel's own stack is generous enough that this was invisible. On UEFI, it was a guaranteed overflow: the UEFI loader handed control to the kernel without ever setting up its own stack pointer, so the kernel simply kept running on whatever stack the firmware itself was using — and the UEFI specification only promises 128 KB of that. A 256 KB local buffer on top of it was arithmetically certain to run past the end. The fix has two halves: the loader now reserves and switches to a full 1 MB stack of its own before jumping into the kernel, and Papyrus no longer keeps its buffer on the stack at all.

Technical note · Papyrus and SuSH moved out of the kernel

Both of these tools used to live inside the kernel as ordinary ring-0 code — shell/editor.rs and shell/sush.rs, neither of which exists anymore. They are now standalone no_std ELF64 executables in userland/papyrus/ and userland/sush/, embedded into the kernel at compile time and loaded as real ring-3 processes (see the Userspace section above). The shell command papyrus file.txt no longer calls editor code directly — it maps a one-page argument buffer, hands the file path to it through enter_ring3's single u64 argument, and the editor reads the file with SYS_READFILE, draws to the screen with SYS_PUT_ROW_STR/SYS_CLEAR_ROW, and saves with SYS_WRITEFILE. SuSH is the same: every line that is not a built-in of the interpreter goes to the kernel through SYS_RUN_LINE, so the interpreter no longer reimplements any shell command itself. The cost is documented and accepted: ring-3 code cannot execute hlt, so both programs poll the keyboard queue in a tight loop instead of parking — full CPU while they wait for a key.

[ back to top ]
Keyboard & Mousekernel/src/drivers/keyboard.rs · mouse.rs

The keyboard driver is the only module in the kernel that knows what a scancode is. It decodes Scan Code Set 1 off the PS/2 controller — modifiers, extended keys behind the 0xE0 prefix, two full layouts (US and Latin American Spanish) — and hands the rest of the system an already-resolved key: a Unicode character or a named special key, never a raw code.

The LatAm layout also implements genuine dead keys: striking the acute-accent/diaeresis position prints nothing yet — it arms a pending state that the next keystroke resolves, producing an accented vowel on a compatible letter, or the bare accent mark followed by whatever was actually typed if it wasn't one.

Selected scancodes
ScancodeKeyNote
0x1A´ / ¨ (LatAm only)dead key — combines with the following vowel
0x2A / 0x36Left / Right Shiftignored when prefixed by 0xE0 — see the incident log
0x56< / >the ISO key beside Left Shift, absent from US ANSI boards
Technical note · the mouse, and a framebuffer to draw a cursor on

The audit added a PS/2 mouse driver (drivers/mouse.rs) alongside the keyboard, speaking the same controller's aux channel. Movement and buttons come off the IRQ as a small packet and are exposed to the kernel as a single MouseEvent. There is a cursor — but no classic-VGA cursor was possible here: the text-mode VGA window at 0xB8000 that the BIOS path uses has no concept of a mouse pointer. The cursor only exists on the UEFI path, where the framebuffer driver (drivers/fb.rs) actually owns every pixel on the screen and the cursor can be drawn as a small bitmap, moved, and restored byte-for-byte underneath. That is part of why the framebuffer primitives were extended at the same time: not just a glyph rasteriser any more, but per-row and per-pixel drawing primitives the mouse cursor — and the ring-3 drawing syscalls — can build on.

[ back to top ]
Network — virtio-net (phase 1)kernel/src/drivers/virtio_net.rs

The newest driver speaks network rather than disk: a virtio-net device, located by walking the PCI bus, brought up through the legacy/transitional I/O-BAR interface (the same one QEMU's virtio-net-pci speaks by default, so no extra flags are needed on the emulator command line). Phase 1 is discovery, bring-up, and MAC address read-out — the driver recognises the device, resets it, negotiates the feature bits it understands, and prints what it found.

The actual RX and TX queues are not implemented yet: there is no ARP, no IP, no ICMP, no ping. The netinfo shell command shows the device's MAC and the negotiated features, which is what phase 1 is for — proving the device handshake works end to end before the harder half of the driver comes in. QEMU's user-mode networking (SLIRP, outbound NAT only) gives it something to find without touching any real host interface or needing elevated privileges.

[ back to top ]
Cryptographykernel/src/crypto/ · users/

Accounts authenticate against PBKDF2-HMAC-SHA256, and all three layers — SHA-256, HMAC, and PBKDF2 itself — are implemented from scratch, with no hardware acceleration and no crate standing in for any of it. Each account gets its own 16-byte salt, so two identical passwords never produce the same stored key, and verification runs in constant time: the comparison walks the entire derived key regardless of where the first mismatched byte falls, so a login attempt's timing cannot leak how much of a guessed password was already correct.

This replaced an earlier, unsalted FNV-1a hash: fast, general purpose, and exactly wrong for passwords — cheap enough to try billions of guesses a second on a commodity GPU. PBKDF2 is slow on purpose (ten thousand rounds of HMAC-SHA256 per attempt): imperceptible for one interactive login, expensive enough to blunt an offline brute force.

This audit also made login about twice as fast without weakening that derivation at all: PBKDF2's inner loop was rebuilding HMAC's key schedule from scratch on every one of the ten thousand iterations, even though the key never changes between them. Precomputing that schedule once and reusing it across all ten thousand rounds produces the identical bytes in roughly half the time.

Technical note · other weaknesses this audit closed

A handful of other authentication weaknesses were found and closed in the same pass. A timing oracle in the login path let an attacker tell a valid username from an invalid one purely by how long the “Login incorrect” response took to arrive. The default root account's salt was practically the same on every fresh install, because it was derived from the boot timer's tick count at install time — a value that barely varies from one boot to the next. Passwords typed into useradd, passwd, and login were being retained in plaintext in the shell's own command history. And diskread, the raw sector-read command, didn't require administrator privileges, so any logged-in user could dump the account table off the disk sector by sector regardless of their own permissions.

Separately, the audit also flagged — without yet closing — that deleting the system's last administrator account leaves it permanently locked out, with no way to recover from inside the running system.

[ back to top ]
Automated Testscommon/kernel_logic_tests/ · common/diskfmt/

Until this audit, the project had exactly zero automated tests — every incident in the log below was found by booting the thing and watching what broke. That changed to 37 tests across two suites, and build.ps1 now runs them as its first step: if they don't pass, nothing gets built.

common/diskfmt (12 tests) formats a disk entirely in memory and checks the exact bytes a real firmware would read back: GPT header CRCs recomputed the same way firmware recomputes them, agreement between the primary and backup partition tables, partitions that stay inside the disk's usable range, and the FAT32 cluster-count floor behind the UEFI image-size fix described above.

common/kernel_logic_tests (25 tests) pulls in the kernel's own source files directly with Rust's #[path] attribute — the same code that boots, not a copy of it — and checks the hand-written cryptography against published test vectors: SHA-256 against FIPS 180-4, HMAC against RFC 4231, and PBKDF2 built on both. It's the first time that stack has been checked against anything other than “does login still work.”

[ back to top ]
Incident LogFound & Fixed

Every row below is a real bug, found on a real boot. Most of them only ever surfaced once actual hardware, not QEMU, was in the loop — though a growing share now get caught earlier, by the automated tests described above.

Bugs found while building this kernel
SymptomRoot causeFix
Passwords reverted to root/root on every reboot auth::load() compared the size of what it read against the expected 664-byte user table — but the filesystem delivers file contents in chunks of up to 512 bytes, not the whole file at once, so that comparison could never succeed for a two-block table. Accumulate the chunks into a buffer before comparing the total size.
Silent reset loop after enlarging the Papyrus text buffer The kernel's growing .bss overlapped the BIOS trampoline's own page tables and boot stack, fixed at 0x70000/0x90000 for a much smaller kernel. zero_bss() overwrote them while the CPU was still using them. Relocate the trampoline's page tables and stack to 0x200000/0x300000; extend the identity map to 4 MB.
The LatAm keyboard's </> key typed nothing Its scancode, 0x56, falls outside the 0x00–0x39 range the layout tables cover — a key US ANSI boards don't even have, which is why it went unnoticed under every US-layout test. A dedicated case for scancode 0x56, independent of the layout tables.
Up-arrow scrolled the screen instead of recalling history With Num Lock active, the Up arrow arrives as E0 2A E0 48 instead of a plain E0 48 — a decades-old compatibility trick the driver mistook for a real Shift press. A fake shift only counts as real when it arrives without the 0xE0 prefix.
Booted perfectly under OVMF, invisible on real firmware rust-lld leaves the UEFI PE header's CheckSum field at zero; stricter real firmware refused to load the image at all. The build script computes and patches in a real PE checksum after linking.
Writing a file to a fragmented disk silently corrupted a different, existing file The block allocator checked that a candidate block was free, then went on to claim a different block — one block off from the one it had just checked — as the actual allocation. Allocate the exact block that was checked as free. A test bench confirmed the before/after: 19 of 20 blocks corrupted before the fix, 0 of 20 after.
Closing Papyrus with Ctrl+Q froze the whole machine Vector 0x80 is an interrupt gate (Type 0xE), which the CPU hardware-clears IF on entry to just like for a hardware IRQ. The normal syscall path restores IF via its own iretq, which pops ring 3's saved RFLAGS (IF=1, forced by enter_ring3). The SYS_EXIT escape path never executes an iretq at all — it discards that frame and does a plain ret — so IF stayed cleared for the rest of the kernel's life the instant any ring-3 program exited. No timer tick, no keyboard IRQ, ever again. The diagnostic commands never surfaced it because nobody typed right after them; closing Papyrus is exactly when a user reaches for the keyboard. A single sti before that ret. Its one-instruction interrupt shadow makes it safe to place directly before the return.
A SuSH script line running papyrus file.txt crashed the inner program on its first syscall TSS.rsp0 was set once at boot, at the top of the kernel's ring-0 entry stack, and never moved. The CPU reloads RSP from rsp0 on every ring 3→ring 0 transition with no notion of nesting, so a nested program's first int 0x80 pushed its interrupt frame at exactly the address the OUTER excursion's own frame — and its entire live call chain below it — already occupied. SYS_EXIT then faithfully popped garbage into the callee-saved registers and reted to a garbage address. enter_ring3 repoints TSS.rsp0 at its own current RSP per nesting level, and the SYS_EXIT path restores the previous value — the ordinary "one kernel stack per thread of control" arrangement, achieved in hand-written assembly.
A ring-3 program passing a kernel address to SYS_READFILE's buffer could write anywhere in ring 0 memory; SYS_WRITE could read it back and print it — the user table, salts and PBKDF2 keys included The module comment claimed a wild pointer "faults reading unmapped memory, caught by the existing #PF handler rather than corrupting something silently." True for a wild pointer, false for the case that mattered: the entire kernel lives inside the 0–4 GiB identity map, which is PRESENT and WRITABLE, and these syscalls run in ring 0 where the USER bit of a leaf entry imposes nothing. One user_range_ok bound against USER_MIN = 0x40_0000_0000 on every ring-3-supplied pointer — a single comparison per argument that rejects anything in the kernel range. Per-process address-space tracking would be the real fix and does not exist yet; this closes the "kernel memory as a syscall I/O target" class today.
[ back to top ]