Skip to content

Case study

Ziran OS

A hobbyist, from-scratch x86_64 operating system in Rust, written to understand what is actually happening beneath every abstraction I'd been trusting on faith. There is no OS underneath it and no abstraction in it I didn't deliberately choose — hand-written bootloader through paging, a preemptive scheduler, a filesystem, an interactive shell, and a hardware-enforced privilege boundary. You can boot the real 64-bit kernel in your browser and then try to break it.

Why it exists

Turning abstractions into mechanisms

The name is 自然 (zìrán) — a Daoist term for "self-so": that which arises naturally, without external forcing. What I set out to learn wasn't "how to build an OS." It was what actually happens beneath every abstraction I'd trusted on faith — and the way to learn that is to turn each abstraction into a mechanism small enough to hold in your head:

  • A task is just a saved stack pointer; switching is swapping rsp so a ret resumes someone else's call stack.
  • A file is a header's lie about a flat run of bytes; a directory is the same lie, one level down.
  • Preemption is the exact switch a task would make voluntarily — forced by a timer it can't refuse.

It is deliberately not meant for real-world use. Success is measured in understanding gained, in a system that boots and runs a simple file manager, and in an honest public trail of how it was built — including the parts that didn't work. It's proof of range: from a PWA down to bare metal.

What I built

Bootloader to userspace, 17 milestones

About 6,300 lines of Rust (no_std, targeting x86_64-unknown-none on stable) plus ~640 lines of boot assembly, built over an intense stretch and public on GitHub from the first commit — before it even booted — so the commit history is part of the story. The arc, one milestone at a time:

  • A hand-written Multiboot2 bootloader and the 32-bit → long-mode transition in assembly (GRUB does the real-mode dance; the instructive handoff is mine, with no bootimage magic hiding it).
  • VGA text + serial output, then a full IDT — all 256 vectors, a breakpoint that recovers, a page fault that reports CR2 instead of silently rebooting.
  • A PS/2 keyboard driver over a remapped PIC; a bitmap physical frame allocator; paging — the kernel builds its own 4-level page tables and switches CR3 off the boot map; a linked-list heap so Vec/Box/String work.
  • A preemptive scheduler driven by the 100 Hz PIT; an interactive shell as a scheduled task, fed by a lock-free ring the keyboard IRQ only produces into.
  • A read-only filesystem with a custom on-disk format, then a file manager — subdirectories, cd/pwd/ls/cat — the stated end goal, reached.
  • Ring 3 / syscalls: the first hardware-enforced privilege boundary, via a TSS, the USER page bit, and an int 0x80 gate. Plus an in-kernel loopback socket stub — deliberately scoped to a named endpoint with a receive queue, no IP or TCP.

Why it isn't AI slop

This is the harness

It's built with AI, openly — and that's the interesting part, not something to hide. Slop is unverified, unreviewed, undocumented output. This is the opposite, and you can check it:

  • Verified. Every milestone ships a deterministic self-test that runs on boot and is asserted by CI — not "looks fine" but proves specific properties: a freed frame is reused, a file's exact bytes round-trip, ten malformed disk images are rejected (a directory cycle among them, without hanging). GitHub Actions boots the image under headless QEMU on every push.
  • Reviewed. Every milestone got an adversarial, multi-agent code review that found real latent bugs before they shipped — a deadlock that only triggers when the timer preempts mid-lock; a filesystem validator a crafted image could hang or stack-overflow.
  • Honest. Every post documents what broke, what was deliberately deferred, and the checks left undone — the opposite of a highlight reel.

The claim isn't "I hand-typed an operating system." It's that this is what disciplined AI-assisted systems engineering looks like: the human owns the vision, the non-goals, and a mandatory Think → Plan → Build → Review → Reflect loop; the AI does the mechanical work inside that harness. Slop is what you get when you skip the harness.

The through-line

A bounds check is only as good as the bound it trusts

The security track is where the project's real lessons live, and they rhyme. Two milestones attack the kernel's own boundaries:

  • The confused deputy. A flag on a kernel-only page faults if ring 3 reads it directly — but the kernel's first pointer-carrying syscall leaks it with ring-0 power, while its validated twin (a copy_from_user that walks the page tables) rejects the same pointer. The CPU stops unauthorized access; only software stops authorized misuse.
  • The same mistake, one layer down. The filesystem mount bounded a file extent only against the whole image, so a crafted entry aliasing a hidden secret leaks it. The fix confines every extent — file and directory table — to a real data ceiling.
  • A termination proof is not a safety proof. The recursive filesystem validator was proven to terminate on any input — and still had two latent denial-of-service defects (a diamond-DAG hang, a deep-chain stack overflow) that review caught. Terminating is not the same as safe.

See it run

The real kernel, in your browser

Almost every hobby OS you've seen, you've seen behind glass — a screenshot in a README. I wanted a living specimen you can type at. ziranos.pages.dev boots the actual 64-bit kernel to an interactive shell in a tab — real QEMU compiled to WebAssembly (the ktock/qemu-wasm project), because the lightweight browser emulators are all 32-bit and can't reach long mode at all. Next to it sits a lighter faithful-reconstruction tour and a genuine recorded serial session — all three labelled, so you always know whether you're looking at the living thing or a good drawing of it. Bringing up the browser boot flushed out two latent bugs (an unloaded GOT, SSE never enabled) that the normal boot path had been quietly papering over for milestones.

ziran-os — make console — serial ✓ real recording
 
loading…
A byte-for-byte recording of the actual kernel over the serial line — the boot log, the scheduler's tasks under ps, live memory under mem, and walking the on-disk tree with cd/ls/cat. Not a reconstruction. Boot the real kernel live →

Then there's a two-tier CTF against the kernel's own filesystem:

  • Tier 1 — white-box, in your browser (/ctf): craft a disk image whose header lies about where its data ends, and make the filesystem hand you a hidden flag that ls can't see. The whole trick is in front of you — the point is the exploit primitive, not secrecy.
  • Tier 2 — a real remote pwn target (/ctf-remote): a unique flag is minted per session and lives only in one server-side QEMU instance's RAM — not in the repo, not in the page, not in any bytes you're given. Steal it over the wire. The kernel itself gains zero networking; all the isolation lives outside QEMU, in a hardened, network-islanded VM — because standing up a safe place for strangers to run ring-0 exploits is far harder than the exploit.

Deep dives

The build log, one milestone at a time

  • Setting Up to Build Nothing

    I'm building an x86-64 operating system from scratch in Rust, and this is milestone zero: a toolchain and an empty kernel that compiles for a machine that isn't my machine. No boot, no output — just the unglamorous groundwork where most OS projects quietly die.

  • Teaching It to Fail Gracefully

    Milestone 4 of my from-scratch Rust kernel: the IDT and exception handling — where the machine stops silently rebooting on a mistake and starts telling me what went wrong. The whole thing lives or dies on assembly and Rust agreeing about the shape of the stack.

  • It Listens Now

    Milestone 5 of my from-scratch Rust kernel: PS/2 keyboard input — the first time the machine reacts to the outside world instead of just talking at it. Remapping the PIC, enabling interrupts for the very first time, and learning why forgetting one EOI makes the keyboard die after exactly one keypress.

  • It knows what it has now

    Milestone 6 of my AI-assisted OS build: physical memory management. The kernel finally learns how much RAM the machine has, and hands it out one 4 KiB frame at a time.

  • The illusion of memory

    Milestone 7 of my AI-assisted OS build: paging and virtual memory. The kernel stops using the map the bootloader handed it and starts drawing its own — and survives the CR3 switch on the first try.

  • Room to grow

    Milestone 8 of my AI-assisted OS build: the heap allocator. The kernel gains dynamic memory — Vec, Box, and String start working with no operating system underneath.

  • Multiple things at once, sort of

    I gave my from-scratch kernel a timer and its first scheduler. Two tasks take turns on one CPU — first politely, then by force — and the preemption step turned out to be a concurrency trap wearing a simple mechanism's face.

  • It has a prompt now

    My from-scratch kernel runs its first interactive program: a shell, spawned as an ordinary task on the scheduler. The keyboard interrupt shrinks to 'enqueue one byte and return' — and the 'interactive code can't be tested' excuse falls apart.

  • Files are just convincing lies about disk layout

    I built a read-only filesystem for my from-scratch kernel, and the lesson is what a 'file' actually is: a directory table of {name, offset, length} over a flat run of bytes. The hard, security-relevant code isn't the reading — it's the refusing.

  • The whole point, arrived at

    Subdirectories, cd/pwd/ls/cat, and the moment the core arc — boot, memory, tasks, shell, files — is complete. Along the way I learned the hard way that a termination proof is not a safety proof: my validator provably terminated and still hid two denial-of-service bugs.

  • The first wall

    Ring 3, a syscall gate, and the two bits that turn 'the kernel is the only program' into 'the kernel runs code it doesn't trust.' The first time in this project that the hardware, not my kernel's goodwill, holds code back.

  • The confused deputy

    I planted a flag on a kernel-only page and wrote a ring-3 program to steal it. The CPU stopped the direct read cold — then my own syscall handed the flag over the moment it dereferenced a caller-chosen pointer. The one door the hardware cannot guard for you.

Stack

What it's built with

Rust (no_std, stable)x86_64 assembly (Multiboot2, long mode)QEMUQEMU → WebAssembly (Emscripten)GitHub Actions (boot-in-CI)Cloudflare Pages

← Back to projects

Newsletter

Notes from building & climbing

New essays and short notes — privacy-first software, AI, security, and the occasional rambling from the trail. Every other week, no filler.