Host-side crates (everything except kernel/ and xtask/):
cargo build --workspace
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
The kernel needs nightly (see kernel/rust-toolchain.toml) — not for
x86_64-unknown-none itself (that's a tier-2 target with precompiled
core/alloc, stable is fine for that), but because exception handlers use
extern "x86-interrupt", which is still unstable
(rust-lang/rust#40180)
despite being the standard way every x86_64 Rust kernel defines them.
--target x86_64-unknown-none is explicit everywhere below — deliberately
not a .cargo/config.toml default; see the "config leak" note in
STATUS.md:
cd kernel
cargo build --target x86_64-unknown-none
cargo clippy --target x86_64-unknown-none --bins --lib -- -D warnings
# --bins --lib, not --all-targets: the *default* libtest harness still
# can't run on a bare target — `cargo test` (below) is what actually
# exercises tests/*.rs, via its own non-libtest mechanism.
cargo test (also from kernel/) builds each kernel/tests/*.rs as its
own bootable binary, boots it for real in QEMU, and reads pass/fail from the
isa-debug-exit device (kernel/src/qemu_exit.rs) — a real exit code, not
grepped serial text. This is what "Test harness — QEMU integration tests"
(kernel build stage 9, below) actually means, and it's why plain
cargo test --workspace on the host isn't enough: it can't see
architecture-specific bugs like the GDT/segment-register one in
STATUS.md. --test <name> is required, one per file under
tests/ — a bare cargo test (or even cargo test --tests, despite the
name) also tries to build the library's own unit-test harness, which needs
test/panic-unwind that doesn't exist on a bare-metal target regardless of
harness = false on the integration tests themselves:
cargo test --target x86_64-unknown-none --test basic_boot
grid-sandbox-host must be built first: kernel/src/main.rs now
include_bytes!s its compiled output directly (the CITADEL-gated real
module load, Phase B7 — see kernel/src/citadel.rs's doc comment), so
kernel/ itself won't compile without it, the same requirement
kernel/tests/grid_sandbox_wasm.rs already has:
cd grid-sandbox-host
cargo build --target x86_64-unknown-none --release
Building a bootable image and running it in QEMU by hand (this needs
nightly too, separately — transitively through the bootloader crate's
build script, see xtask/rust-toolchain.toml):
cd xtask
cargo run -- build # -> ../target/runix-bios.img
cargo run -- run # build + boot in QEMU, serial on stdio
A legacy BIOS boot through this crate's own multi-stage loader (SeaBIOS ->
stage 2/3/4 -> the kernel ELF, now with grid-sandbox-host's ~2 MB
embedded) can take well over the naive-looking default QEMU timeout on a
slow or nested-virtualization host — give it real time (a minute-plus) in
scripts before concluding a boot has hung, not just crashed silently; see
.github/workflows/ci.yml's boot job for the timeout this project
actually uses in CI.
On a Windows dev box with no MSVC Build Tools (link.exe) installed, the
host-default nightly resolves to -msvc and fails to link. Force GNU
explicitly instead:
rustup run nightly-x86_64-pc-windows-gnu cargo run -- build
- Toolchain & target bring-up —
x86_64-unknown-none,no_std/no_main,bootloader_apifor the entry point. Done. - "Hello kernel" — serial (UART/COM1) output so boot is verifiable from the
host instead of trusting a black QEMU window. Done (
kernel/src/serial.rs). - CPU init — GDT with a TSS (dedicated IST stack for double faults), IDT
with breakpoint/double-fault/page-fault/GPF handlers. Done
(
kernel/src/gdt.rs,kernel/src/interrupts.rs) — verified by deliberately triggering a breakpoint exception and confirming execution resumes afterward instead of faulting. - Memory management — physical frame allocator over the bootloader's memory
map, an
OffsetPageTablemapper over the physical-memory mapping the bootloader is now configured to provide, and a kernel heap (linked_list_allocator,allocator::HEAP_SIZE— grown from an initial 100 KiB once later phases' allocations outgrew it, see STATUS.md) backingalloc::{Vec, Box}. Done (kernel/src/memory.rs,kernel/src/allocator.rs) — verified by actually allocating aBoxand a growingVecat boot and checking their values, not just thatinit_heap()returnedOk. - Interrupts — PIC remapped past the exception vectors (0-31), timer
(PIT/IRQ0) unmasked and wired to a handler that ticks an atomic counter,
every other IRQ line left masked (an unmasked line with no handler would
general-protection-fault the kernel the moment it fired). Done
(
kernel/src/interrupts.rs,kernel/src/boot.rs) — verified by enabling interrupts (sti) and spin-waiting on the tick counter actually advancing, not just onstinot faulting. - Threading & scheduler — fixed-layout
Contextstruct (callee-saved registers + return address, matching whatpop/retexpect off a thread's stack) built by hand for freshly spawned threads; a nakedswitch_tofunction that saves the caller's registers, swapsrsp, and restores the target's; a cooperative (not yet timer-preemptive) round-robin run queue. Done (kernel/src/scheduler.rs) — verified by spawning three threads that each print 3 tagged messages and yield between them, and confirming the output actually interleaves (A0,B0,C0,A1,B1,C1,A2,B2,C2) instead of running one thread to completion before the next starts — proof each thread's context truly resumes where it left off, not from scratch. - Basic IPC — syscall ABI over
int 0x80(syscall number in RAX, up to 3 args in RDI/RSI/RDX, return in RAX), and fixed-count byte channels addressed by port id, both callable from kernel code today since there's no ring 3 yet to require the syscall path — the ABI itself doesn't care which ring issuesint 0x80. Done (kernel/src/syscall.rs,kernel/src/ipc.rs) — verified by two threads exchanging three bytes over a channel, driven entirely throughSYS_IPC_SEND/SYS_IPC_RECVsyscalls (not directipc::send/recvcalls), and checking the receiver got exactly['X', 'Y', 'Z'], in order — proof the syscall gate, the register remapping, and the channel all compose correctly, not just that each works in isolation. (SYS_IPC_SENDlater became capability-gated — see STATUS.md — but that's later work layered on top of this stage, not a change to what "basic IPC" itself means here.) - Ring 3 / user-space transition — a user-accessible stack mapped fresh
(so
Mapper::map_to's flag propagation marks every page-table levelUSER_ACCESSIBLEas it creates them), ring 3 access granted to the one 4 KiB code page a hand-written nakeduser_hellofunction lives on (allow_user_accessfixes up each existing page-table level by hand, since those predate the grant and don't get the propagation trick), and aniretq-based jump that drops CPL from 0 to 3. The syscall gate's DPL also had to move from Ring0 to Ring3 (interrupts.rs) — left at the default, a ring 3int 0x80general-protection-faults before ever reaching the handler; theintinstruction checks CPL against the gate's DPL, not just whether a handler exists. Done (kernel/src/userspace.rs) — verified byuser_hellowriting"USR\n"one byte at a time throughSYS_WRITE, proving the syscall path accepts a genuine CPL 3 caller, not just a kernel-mode one — the same gate that was tested from ring 0 in stage 7 now has to work from a strictly lower privilege level, which is exactly what the DPL fix above makes possible. - Test harness —
cargo test(fromkernel/, targetingx86_64-unknown-none) builds eachkernel/tests/*.rsas its own bootable binary (harness = false— no libtest, there's notest/panic-unwind runtime on bare metal), boots it for real in QEMU via therunnerinkernel/.cargo/config.toml, and reads pass/fail from the isa-debug-exit device. Done (kernel/src/qemu_exit.rs,kernel/tests/basic_boot.rs,xtask'stest-runnersubcommand) — a realcargo testexit code, not grepped serial text, and it's what.github/workflows/ci.yml'skernel-testsjob runs. (Thebootjob alongside it still greps the fullmain.rsdemo's serial output — coarser, but it's the only thing exercising every phase's feature together in one run;basic_boot.rsis deliberately minimal, and moretests/*.rsfiles can grow alongside it as later phases land, rather than everything routing throughmain.rs.)
Three real bugs caught along the way, each worth knowing before touching the relevant code again — full detail in STATUS.md:
- Stale segment registers after loading a new GDT (
gdt.rs) — the bootloader's leftover SS collided with our TSS descriptor, and the nextiretqgeneral-protection-faulted. Fix: explicitly null SS/DS/ES/FS/GS ingdt::init(). - Stack alignment for freshly spawned threads (
scheduler.rs) — a thread's initialrsphas to land at the same offset (mod 16) a realcallwould leave it at, or a stack-spilled SSE register faults later, not on the first context switch. .cargo/config.tomltarget leaking across processes — an ambient[build] targetinkernel/.cargo/config.tomlbrokextask's nestedcargoinvocations (wrong target, then a confusing codegen error rather than "wrong target"). Fix: no ambient default:--target x86_64-unknown-noneexplicit everywhere instead.