Skip to content
All writing
Technical · 10 min

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.

Contents

The file manager: subdirectories, cd/pwd/ls/cat, and the moment the core arc — boot → memory → tasks → shell → files — is complete.

Last milestone gave the kernel read-only files: ls and cat over a flat directory. This one gives it a navigable tree and the shell to walk it — cd into a subdirectory, pwd to see where you are, ls/cat scoped to the current directory. That sounds like a small delta, and at the format level it is: one byte. But reaching it is the thing the whole project was pointed at. The stated success criterion was never “a filesystem” — it was a machine that boots, manages its own memory, runs concurrent tasks, presents a shell, and lets you browse files from that shell. This milestone closes that loop. The boot → memory → tasks → shell → files arc that the project set as its primary goal is, as of this milestone, complete.

The one thing I understand now that I didn’t before: a directory is the same lie as a file, told recursively — a directory is just a file whose bytes are more {name, offset, length} entries — so hierarchy costs almost nothing at the format level. The genuinely hard part isn’t reading the tree but validating it: a mount that recurses an attacker-controlled structure and provably cannot loop, hang, or overflow the stack. The reading is a descent; the substance is proving the descent is safe on hostile input.

I held scope tight to “list, navigate, read”: one kind byte (reusing a reserved field), recursive validation, a pure string canonicalizer for ./.., cd/pwd/ls/cat with a cwd, and a depth-2 boot tree. Read-only. Everything else got deferred deliberately — write and mkdir drag in a free-space allocator; symlinks break the forward-ordering guarantee by turning the tree into a DAG; permissions and timestamps are the multi-user non-goal; no path cache, no deep trees. Shipping and testing depth 2 while the format admits any depth was the disciplined call: it proves the recursion without inviting a “deep trees” rabbit hole.

The design: a recursive read of a recursive lie

  • The v2 format. ZranFS gains a single kind byte per directory entry (reusing a reserved field): 0 = file, 1 = directory. A directory’s “data” region is just another run of {name, offset, length, kind} entries. The format now admits arbitrary depth; the boot image ships depth 2.
  • validate_dir — the trust boundary, now recursive. It descends the tree, and its safety rests on three separate guarantees, not one: the forward-ordering rule (a child directory’s table must start strictly after its parent’s) proves the recursion terminates; a monotonic high-water mark (each table is validated at most once, and re-entry is rejected) makes total work linear and rejects diamond DAGs; a MAX_DEPTH = 32 cap bounds the stack against a deep chain. Violations are specific errors — DirNotForward, TooDeep — never a hang or an overflow.
  • The resolver. resolve walks a canonical path to a Node; list_dir and read_path hand back the entries or bytes at a resolved node. DirEntry grows an is_dir flag. The descent is pure — no parent pointers, so there are no cycles to chase at read time.
  • canonicalize — pure string math. Path normalization (., .., empty segments, absolute vs. relative against cwd) is a pure function over strings, unit-tested with no filesystem in the loop.
  • The shell. Grows a cwd, a cwd-aware prompt (ziran:/docs>), and cd/pwd/ls/cat, all routed through canonicalize then the resolver. Marker: M12: file manager online.

The split is the point: string resolution is testable without a keyboard, and the FS resolver stays a pure descent.

What got built

  • The ZranFS v2 format (the kind byte) and the recursive validate_dir with the forward-ordering rule.
  • The depth-2 boot tree plus a hidden secret; the resolve/list_dir/read_path resolver (Node, DirEntry.is_dir); and the shell navigation (cwd, the pure canonicalize, cd/pwd/ls/cat, the cwd-aware prompt). Marker M12: file manager online. Completes the core arc.
  • An adversarial two-lens review of the validator and the shell navigation. Shell nav shipped as-is. The validator surfaced two latent DoS defects: a crafted deep chain overflows the guard-page-less 16 KiB task stack, and a diamond DAG makes mount hang via exponential re-validation. Fixed with the monotonic high-water mark (linear; rejects DAGs) and the MAX_DEPTH = 32 cap (TooDeep). Corrupt-image rejections grew from 8 to 10.
  • An interactive serial console — the shell reads and echoes over the serial line (serial::read_byte plus shp!/shln! dual-output macros), so it can be driven from a terminal (make console) where the VGA text console isn’t displayed (macOS UEFI). Verified by driving it over serial.

There’s also a security seed planted here: the boot image contains a secret, FLAG{ziran-boundary-leak}, with no directory entry — unreachable by any path. Its full adversarial turn comes later in the series, but the exfiltration target and the 10 corrupt-image rejections are already in place. The whole filesystem stays pure safe Rust — a bad offset is a loud panic or a caught Err, never a silent success.

What I verified — and what I didn’t

Boot-tested live under headless QEMU. The self-test serial log:

[ok] fs: mounted a v2 tree (4 root entries incl. docs/), read /docs/filesystem.txt byte-for-byte, secret unreachable, 10 corrupt images rejected (cycle, deep chain, and diamond DAG among them -- no hang, no stack overflow)
M12: file manager online
[ok] shell: editing, parsing, path normalization, input ring, and ps/mem accessors verified
[boot test] PASS -- reached long mode and 'M12: file manager online'

And — new this milestone — the OS is now interactively drivable. The serial console makes the shell usable from a terminal (make console), which matters because on macOS UEFI the VGA text console isn’t displayed. Here is a real interactive session, captured by driving the shell over -serial stdio:

ziran:/> ps
tasks: 2
  [0] running
  [1] running  (current)
ziran:/> mem
  frames: 30793 free (120 MiB)
  heap:   16592 used / 1031984 free / 1048576 total bytes
  uptime: 2 s (208 ticks @ 100 Hz)
ziran:/> ls
  motd.txt   13 bytes
  docs       <dir>
ziran:/> cd docs
ziran:/docs> cat filesystem.txt
A file is a lie a header tells about bytes.
ziran:/> cat docs
cat: is a directory: /docs

What each piece proves — and what it doesn’t:

  • The self-test pins the tree navigation and the 10 rejections deterministically on serial: a v2 tree mounts, a file is read byte-exact through a subdirectory, and 10 corrupt images (cycle, deep chain, diamond DAG among them) are refused with clean errors — no hang, no stack overflow.
  • The serial console makes the same capstone interactively drivable, proven by the captured session above — ps, mem, ls, cd, cat, and the is a directory refusal, driven over the wire, autonomously, no screen required.
  • The planted secret FLAG{ziran-boundary-leak} is present in the image yet reachable by no path — ls/cat can’t surface it. The boundary holds; that’s the honesty check for the capstone and the concrete target the security milestones will attack.

The run had no panics and no faults.

What broke — and what a review caught instead

Nothing broke at runtime — zero debugging cycles, the fourth milestone in a row. The plan was accurate: the kind byte, the forward-ordering rule, the shell/fs split, the pure canonicalize all landed as designed. The one place the plan was elegant but incomplete: the forward-ordering termination proof.

The two validator defects weren’t crashes; they were latent, unreachable with the fixed boot image, found by review. Which is exactly why they matter: they’re the future fuzzing surface, found before an adversary would have.

The root cause: the termination proof reasoned about depth but not work or stack. A diamond DAG re-validates shared subtrees exponentially, so mount would hang; a deep chain recurses past the guard-page-less 16 KiB task stack, so mount would overflow. The fix: a monotonic high-water mark — every directory table is validated at most once, and any re-entry is rejected — which makes total work linear and turns a diamond DAG into a clean rejection; plus a MAX_DEPTH = 32 cap that bounds the stack (TooDeep).

The assumption worth re-examining — the lede: “a termination proof is a safety proof.” It isn’t. The forward-ordering rule really did prove the recursion terminates — finite depth, no possibility of looping — and the code comment said exactly that. But terminating in bounded depth says nothing about bounded time (a diamond DAG terminates and still runs exponentially) or bounded stack (a deep chain terminates and still overflows). “Terminates,” “bounded time,” and “bounded stack” are three separate guarantees. Conflating them is precisely how a validator ships a denial-of-service while looking correct.

Three things earned their keep. The shell/fs split with a pure canonicalize — all ./.. handling as string math — which is testable without a keyboard and keeps the FS resolver a pure descent (no parent pointers, no cycles to chase). The adversarial review, which turned a “provably terminates” claim into an honestly-bounded one and caught two DoS defects before they could be triggered. And the serial console, which proved the capstone works interactively — autonomously, over the wire — without a display.

One thing I’d do differently: when writing a “provably X” claim for a validator, enumerate the three resource bounds separately — depth (terminates), work (time), and stack (space) — rather than assuming the first implies the others. The forward-ordering comment asserted one and implied three; the review had to split them apart.

Takeaway for the next person

A directory is the same lie as a file, told recursively — so hierarchy is nearly free at the format level, and the real work is validating a tree you don’t trust. And validation is three guarantees, not one: it must terminate, run in bounded time, and use bounded stack. A forward-ordering rule buys you the first; a high-water mark buys the second; a depth cap buys the third. Prove them separately or you ship a denial-of-service that passes every “does it work” test.

With this milestone, the core arc is complete: boot → memory → tasks → shell → files, all present, all drivable from a prompt. That was the project’s stated primary success criterion, and it’s met here. What remains is explicitly stretch and security: userspace and syscalls (the first real privilege boundary — ring 3), the networking stub, and the security capstone — where the FLAG{ziran-boundary-leak} planted in this milestone’s image becomes the thing to capture, and the validator’s now-honestly-bounded guarantees become the thing to fuzz. The whole point has been arrived at; everything after it is a bonus round.

Share LinkedInXBlueskyReddit