An OS written in Rust on the web. Inspired by blog_os.
# install dependencies
rustup toolchain install nightly # at least nightly 2020-07-15
cargo install bootimage
cargo install --locked bacon
npm install
uv venv
uv pip install pre-commit
source .venv/bin/activate
pre-commit install
flyctl auth login
# test and build the kernel
cargo test -p os --target crates/os/x86_64-os.json -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem
cargo bootimage -p os --target crates/os/x86_64-os.json -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem
# run the kernel
qemu-system-x86_64 -drive format=raw,file=target/x86_64-os/debug/bootimage-os.bin # runs in terminal
cargo run -p os --target crates/os/x86_64-os.json -Z build-std=core,compiler_builtins,alloc -Z build-std-features=compiler-builtins-mem # runs in separate window
# test the app
cargo test -p app
# serve the app
mprocs
# run pre-commit hooks
uvx pre-commit run --all-files
# deploy to fly.io
flyctl launch # once
flyctl deploy-
Kernel Core
- Bootloader integration: bootable disk image via
bootloadercrate - CPU setup: GDT, TSS (double-fault IST), IDT
- Interrupts: PIC remap, timer + keyboard interrupts, basic exception handlers
- RTC:
gettimeofday - Memory management: 4-level page tables, frame allocator from
BootInfo, heap allocator - User heap:
brk,sbrk - Console & serial: VGA text mode, UART, serial-based logging
- Telemetry:
[[SYSINFO …]]and[[PROC …]]markers for frontend stats
- Bootloader integration: bootable disk image via
-
Async Runtime & Input
- Cooperative executor: async/await task system with wakers
- Keyboard pipeline: interrupt → scancode queue → async stream → VGA
- Spinning mutex: interrupt-safe locking primitive
-
Web App & Browser UX
- Axum backend: serve static assets, manage shared QEMU bridge state
- QEMU bridge: spawn QEMU, capture serial stream, decode
[[VGA_FRAME]]/[[VGA_CELL]]/[[VGA_SCROLL]] - WebSocket VGA stream: push frames/cells to browser, cache latest frame for new clients
- Browser terminal UI: Tailwind CRT-style 80×25 grid, responsive scaling, auto-reconnect
- Keyboard bridge: browser
keydown→ QMPsendkey - QEMU controls: reset, pause/resume via QMP
- Stats & IPC panels: live OS stats and scheduler/IPC views via
/api/os/*endpoints - Multi-session runtime: multiple isolated QEMU instances (sandboxed containers or microVMs)
-
User Mode & Syscalls
- Syscall entry:
syscall/sysret, MSR setup (IA32_STAR,IA32_LSTAR,IA32_FMASK) - Ring 3 transition: enter user mode and return via syscall
- Console I/O:
write(fd=1, …)to VGA,read(fd=0, …)from keyboard - Process info:
getpid,getppid - IPC:
channel_send,channel_recv - Termination:
exit(code)
- Syscall entry:
-
Processes & Scheduling
- PCB: per-process metadata (PID, parent, stacks, entry, state, saved context)
- Process table: kernel-managed collection with current-process tracking
- Preemptive scheduler: timer-driven round-robin
- Context switching: save/restore registers and CR3
- Program loading: flat/minimal ELF blobs into heap-backed regions
- Process lifecycle:
wait,waitpid - Process blocking:
nanosleep
-
IPC
- Message-passing: per-process mailboxes with
send/recv - PID-based addressing: target processes by PID
- Blocking IPC:
recvsleeps until message arrives -
pipe: anonymous unidirectional byte stream between processes
- Message-passing: per-process mailboxes with
-
File System
- In-memory FS (ramfs): inode table, directory entries, root directory
- Per-process FD table: integrate with PCB, FD allocation/deallocation
- Path resolution: absolute/relative parsing,
.and.. - On-disk FS: virtio-blk backend or FAT32 via
fatfscrate - File I/O:
open,close,read(file),write(file),lseek - File metadata:
stat,fstat - Directories:
mkdir,rmdir,readdir/getdents - File management:
unlink,dup,dup2 - Working directory:
chdir,getcwd
-
Device Drivers
- VGA text mode: character output mirrored to browser
- Serial (UART): logging and VGA frame transport
- PS/2 keyboard: scancode reading and decoding
- Timer: tick counter and uptime
- Block device: virtio-blk over PCI (
virtio-spec-rs,pci_types)
-
Program Execution
- Proper ELF loader: parse ELF headers, load segments, support static executables
- Simple shell: command interpreter that can run programs
- Program execution:
exec
-
Microkernel Services
- User-space console server: move VGA/keyboard handling to user space
- User-space FS server: move ramfs to user space, communicate over IPC
-
Tooling & Testing
- Kernel tests: custom test framework with QEMU exit codes
- Syscall tests:
open/read/write/closeround-trips - Scheduler tests: process creation, blocking, wake-up
- FS tests: file/directory operations, path resolution
Some interesting features (beyond that covered in the blog post series):
- The kernel’s
vga_buffermodule mirrors screen state over the serial port using three compact text formats: a full-screen snapshot ([[VGA_FRAME …]]), single-cell changes ([[VGA_CELL …]]), and scroll events ([[VGA_SCROLL …]]). The HTTP/WebSocket server in theappcrate tails QEMU’s stdout/stderr, parses those markers inparse_vga_line, and turns them into typedVgaUpdatemessages (Frame,Cell,Scroll) that are broadcast over/ws/vga. The 80×25 grid of<span>s is built once at startup. Sinceframemessages fully (re)paint the grid,cellmessages only touch a single DOM node, andscrollmessages reuse and rotate existing row elements, only the handful of cells that actually changed are updated. - The timer interrupt uses
sysinfoto maintain an uptime/tick counter and periodically emit compact telemetry lines ([[SYSINFO uptime_ms=… usable_mb=… total_mb=… ticks=…]]and[[PROC …]]) alongside the VGA stream. The Axum bridge parses those withparse_sysinfo_lineandparse_process_line, caches the latestOsStats/ProcessesSnapshotinAppState, and exposes them over/api/os/statsand/api/os/processesfor the OS Stats and Scheduler/IPC panels. - The kernel exposes a tiny
syscall/sysretq-based ABI:init_syscallsprograms the relevant MSRs (STAR/LSTAR/EFER/FM AKS), and the nakedsyscall_entrystub saves registers, marshals arguments into aSyscallArgsstruct, and calls a Rustsyscall_handler. The current interface supports three calls:exit(code)to terminate a user program and drop back into a kernel monitor,write(fd, buf, len)to send bytes from user space to the VGA/serial console (supportingfd = 1only, with safe copying and a 1 KiB cap), andread(fd, buf, len)to synchronously pull bytes from the asynchronous keyboard pipeline into a user buffer until newline or EOF (fd = 0only). - The process subsystem keeps a heap-backed PCB table (
pid, parent, kernel stack pointer, entry point, run state, saved CPU context, and owning code/stack buffers) plus a preemptive round-robin scheduler wired to the timer IRQ. Each tick saves the outgoing registers, restores the next ready process (including CR3, RIP/RSP, RFLAGS), and ensures we only context-switch on ring‑3 frames so syscalls can run uninterrupted. The user-mode loader copies flat/minimal ELF blobs into per-process images, records the entry/stack pointers, and hands the PCBs to the scheduler so multiple user binaries (echo/ticker) can time-slice instead of being baked into the kernel. - A tiny message-passing subsystem gives each process a kernel-managed mailbox.
sys_send(target_pid, buf, len)copies user payloads into the target's bounded queue;sys_recv(buf, len)polls the caller's own mailbox (non-blocking, returns EAGAIN if empty).
The important files are:
crates/
app/
src/
handlers.rs HTTP + WebSocket handlers for VGA, QEMU control, and OS stats/IPC APIs
qmp.rs QMP client for sending sendkey and other monitor commands
tests/
handlers.rs smoke tests for static asset and API endpoints
os/
src/
boot/
gdt.rs GDT/TSS setup (kernel + user segments, IST stack)
interrupts.rs IDT, PIC, exception and IRQ handlers
memory.rs paging setup and BootInfo-based frame allocator
drivers/
serial.rs UART (COM1) initialization and serial_print macros
vga.rs VGA text console + serial mirroring (`[[VGA_FRAME]]`, `[[VGA_CELL]]`, `[[VGA_SCROLL]]`)
process/
mod.rs PCB table, PID management, context save/restore
scheduler.rs preemptive round-robin scheduler
programs/
binaries.rs baked-in user binaries (ticker + IPC pinger/ponger) as raw assembly
loader.rs copies flat/minimal ELF images into per-process text/stack regions
user_mode.rs kicks off user space, wires the loader, hands control to scheduler
task/
executor.rs cooperative async executor (task queue + Wakers)
keyboard.rs async keyboard pipeline (scancodes → characters)
allocator.rs heap allocator wiring + fixed-size block allocator
context.rs saved CPU context (registers, stack pointer, etc.)
ipc.rs message-passing IPC with per-process mailboxes
syscall.rs syscall entry/handler and syscall ABI
sysinfo.rs computes memory totals and emits `[[SYSINFO]]`/`[[PROC]]` telemetry for the frontend stats panels
tests/
basic_boot.rs println smoke test
heap_allocation.rs heap allocator stress/regression tests
stack_overflow.rs double-fault IST coverage
should_panic.rs ensures panic path exits QEMU as expected
program_loader.rs validates spawn_program stacks + ELF guards
x86_64-os.json custom target specification for the kernel
src/
index.html CRT-styled layout with `#vga-grid`, OS Stats and Scheduler/IPC panels, and QEMU control buttons
app.ts builds the 80×25 grid, manages `/ws/vga` WebSocket + keyboard → JSON, polls `/api/os/stats` and `/api/os/processes`, and wires QEMU reset/pause controls
