Technical architecture
ctos is a #![no_std] #![no_main] binary. There is no Rust standard library and no OS underneath. The crate builds for a custom target (aarch64-ctos.json): os: none, panic abort, red zone off, static relocation, soft-float (no early SIMD/FP). See ADR-003. Guest samples (not “apps”): apps-today.md. Porting stance: building-or-porting.md.
Boot path
qemu-system-aarch64 -machine virt -cpu cortex-a57(ormax;gic-version=3is allowed).-kernelloads the kernel ELF into virt RAM (linker base0x40080000).- Entry is
_start(assembly insrc/main.rs): set SP, zero BSS, callkernel_main. - Early output is the virt PL011 UART at
0x0900_0000(src/uart.rs). kernel_maininstallsVBAR_EL1(src/exception.rs, ADR-004) after UART init, then the EL1 identity map plus a TTBR1 private page and a cloned TTBR1 RAM alias (ADR-008, W^X split ADR-012, stack guards ADR-014, RO+NX ADR-015, TTBR1 first cut ADR-016, EL1 fetch mile ADR-017, identity-tear first cut ADR-018, identity.textrange tear ADR-019, live.texttear ADR-020, identity.rodatatear ADR-025), then the frame pool after MMU +SCTLR.C(pre-MMU.bssstores can vanish), then the first-fit heap (ADR-009), then the cooperative scheduler (ADR-010), then GICv2 + CNTP (ADR-006). AfterHello World!it proves map/unmap,Box/Vec, two tasks, heap NX, linker-stack guards, the EL0 first mile + standing context, ASID isolation, the TTBR1 private page and high-VA EL1 fetch, torn identity.textand.rodata, the PAN ID field, CNTPCT + IRQ-delta, several timer ticks, and polls PL011 RX (ADR-007).
flowchart LR Q["QEMU virt<br/>cortex-a57"] --> K["-kernel ELF<br/>@ 0x40080000"] K --> S["_start<br/>SP + BSS"] S --> U["PL011 UART"] U --> M["kernel_main"]
QEMU loads one OS ELF. Early print is UART; everything else starts in kernel_main. No bootloader crate, no VGA.
There is no bootloader 0.9 crate and no VGA buffer. The x86_64 phil-opp path was deleted when this ADR landed.
.cargo/config.toml still needs json-target-spec = true on rustc 1.100 nightly. The AArch64 JSON is taken from aarch64-unknown-none-softfloat plus os: none / numeric widths. That nightly rejected the file until both "abi": "softfloat" and "rustc-abi": "softfloat" were set. Behavior is bare-metal AArch64, abort, rust-lld.
This architecture does not claim Raspberry Pi or other SoC support.
Privilege: EL1 kernel vs standing EL0
The kernel runs at EL1. A freestanding image can stand at EL0 and ask the kernel for help with SVC (exit / uart_write / yield / thin-VFS numbers). That is the ctos process model (ADR-035) — not Linux fork / execve. Many isolation miles are Verified; the umbrella “EL0 isolated” is Verified only as the accepted ADR-091 sentence (M6, 2026-09-28; one core, TCG default smoke machine; not speculative-execution / side-channel, not real hardware, not a certification) (ADR-055; sponsor leftovers closure ADR-060). Details: el0.md.
flowchart TD EL1["EL1 — kernel<br/>page tables · SVC handler · VFS"] EL0["EL0 — standing task<br/>loaded freestanding image"] SVC["SVC<br/>exit · uart_write · yield · fs_*"] UMB["Umbrella “EL0 isolated”<br/>Verified: ADR-091 sentence only — M6"] EL0 -->|"asks"| SVC --> EL1 EL1 -->|"ERET back"| EL0 EL0 -.-> UMB
Standing EL0 + SVC is real. “EL0 isolated” is claimed only as the accepted ADR-091 sentence (one core, TCG; not side-channel, not real hardware, not a certification). Stock-QEMU taken SError still parks, and _start is never yanked (ADR-089 exception).
Memory sketch (identity / TTBR0 / TTBR1)
Simplified view of what the guest maps after bring-up. Live identity .text / .rodata / .data / heap / leftover RAM tears are Verified miles; the QEMU -kernel stub at _start stays mapped (ident: start-stay). High twins live under TTBR1. User TTBR0 is what standing EL0 walks.
flowchart TD
subgraph ttbr0k ["TTBR0 kernel (EL1)"]
ID["Identity map<br/>torn live ranges"]
ST["_start stub stays<br/>0x40080000"]
end
subgraph ttbr0u ["TTBR0 user (EL0)"]
UW["User window<br/>text/rodata + trampoline<br/>omits kernel .data/heap"]
end
subgraph ttbr1 ["TTBR1 high"]
HI["High RAM alias + VBAR<br/>EL1 fetch twin"]
PRIV["TTBR1_PRIV<br/>EL1-only page"]
end
ID --> ST
ID -.->|"high twin"| HI
UW -->|"SVC / ERET"| ID
_start stays while live identity ranges are torn. User TTBR0 is not a full process mm. Do not say “the kernel moved.”
OS vs app slots
QEMU still -kernels one OS ELF. Published freestanding EL0 samples live on the same FAT16 volume: /hello (A9 / UART+one yield), /fsdemo (ADR-059 / memfs via libctos), /fatdemo (ADR-061 / FAT /probe via libctos), /yldemo (ADR-062 / cooperative yield rounds). A2–A4 load /hello (no production embed). Cross-update is the same hello ELF on this OS and prior OS ba6541c. Product freestanding “app hosting done” is Verified under ADR-048 / ADR-052 — not Linux/POSIX/containers/OTA. Recipes: what-can-run.md. Companion: hosting-apps.md.
flowchart LR OS["OS ELF<br/>QEMU -kernel"] --> FAT["FAT16 volume<br/>virtio-blk"] H["/hello<br/>UART + yield"] --> FAT F["/fsdemo<br/>memfs VFS"] --> FAT D["/fatdemo<br/>FAT /probe"] --> FAT Y["/yldemo<br/>yield rounds"] --> FAT FAT --> LOAD["Guest PT_LOAD<br/>→ standing EL0"]
One guest volume, three freestanding samples. Slots disconnect OS rebuilds from the published ELFs. Not a container runtime. Not POSIX.
Thin VFS stack
One thin VFS surface (open / read / write / close / readdir / unlink) with two backends and a prefix mount table: in-RAM memfs and FAT16 on virtio-blk. /mem (+ A6 names) → memfs; / → FAT16. Read, write, root listing, delete, and mounts are depth miles — not POSIX. Stance: filesystem.md. ADR-058.
flowchart TD APP2["Standing EL0 / EL1 callers"] --> VFS["Thin VFS<br/>open · read · write · close · readdir · unlink"] VFS --> MT["Prefix mount table<br/>ADR-058"] MT -->|"/mem · A6 names"| MEM["memfs<br/>in-RAM named buffers"] MT -->|"/"| FAT2["FAT16<br/>read · write · root readdir · unlink"] FAT2 --> BLK["virtio-blk<br/>host fat16.img"]
Same API, two backends, prefix mounts. Say “routed path prefixes through a mount table” when vfs: mounts passes — not mount(2) or “supports FAT” as a product.
Track N — virtio-net path (ARP + ICMP + UDP + EL0 net/UDP SVC)
QEMU user netdev (SLIRP) plus guest virtio-net-mmio. N1 exchanges one ARP request/reply with gateway 10.0.2.2 (ADR-066). N2 sends an ICMP echo and expects a reply (ADR-067). N3 sends one UDP datagram to SLIRP DNS 10.0.2.3:53 and expects a UDP reply (ADR-070) — DNS is probe bait, not a DNS product. N4 exposes tiny EL0 SVCs (net_mac / net_ping) and FAT /netdemo (ADR-068). N3.x adds net_udp_dns and FAT /udpdemo (ADR-071) — kernel still owns virtio-net. No BSD sockets / TCP product. Spec/ADR wins on conflict with this sketch.
flowchart LR QN["QEMU user netdev<br/>SLIRP 10.0.2.2 / 10.0.2.3"] --> VN["virtio-net-device<br/>mmio"] VN --> GTX["Guest TX<br/>ARP / ICMP / UDP"] VN --> GRX["Guest RX<br/>ARP / ICMP / UDP reply"] GTX --> VN GRX --> OK["Markers<br/>net: ok · net: ping-ok · net: udp-ok"] OK --> EL0["EL0 SVCs<br/>net_mac / net_ping<br/>/netdemo"] OK --> EL0U["EL0 SVC<br/>net_udp_dns<br/>/udpdemo"]
Host flags alone are not a probe. Do not say “has networking” or “has sockets.” Cite ADR-063 / ADR-066 / ADR-067 / ADR-070 / ADR-071 / ADR-068 / ADR-071.
Current stage (UART hello + M2–M9 + ADR-011 pillars)
Pl011writer with TX-full wait and\n→\r\n;try_recvonUARTFR.RXFE/UARTDRprint!/println!viaspin::Mutex; fatal / unhandled / IRQ paths write the PL011 without the mutex- After EL1 + VBAR, a bump frame allocator (
src/frame.rs) and identity map (src/paging.rs) turn the MMU on (ADR-008); RAM after__kernel_endis PXN (ADR-012); 4 KiB holes sit under the linker stacks (ADR-014) - First-fit
GlobalAlloc(src/heap.rs) on a 64 KiB identity-mapped frame run (ADR-009);build-stdincludesalloc - Cooperative round-robin (
src/sched.rs): idle on the linker thread stack, two heap-backed workers, AAPCS64 callee-saved yield (ADR-010) kernel_mainprintsHello World!, proves map/unmap (serialpaging: ok), provesBox/Vec(serialheap: ok), proves two tasks (serialsched: task a/sched: task b/sched: ok), proves heap NX (serialwx: ok), proves linker-stack guards (serialguard: ok), proves the EL0 first mile + standing context (serialel0: ok/el0: standing/el0: restored), proves the SVC ABI (serialsvc: yield/svc: user-hi/svc: uart/svc: exit/svc: ok), proves thelibctosCRT (seriallibctos: hi/libctos: ok/libctos: linked), proves the guest ELF PT_LOAD loader (serialloader: mapped/loader: ok), proves standing EL0 as a loaded task untilexit(serialel0: task-enter/el0: task-active/el0: task-exit/el0: task-restored/el0: restore-fail/el0: task-ok), proves ASID isolation (serialasid: ok), proves the TTBR1 private page and high-VA EL1 fetch (serialttbr1: ok/ttbr1: el1 exec/ttbr1: vbar), proves the identity-tear first cut,.textrange tear, vtable rewrite, live.texttear, and identity.rodatatear (serialident: ok/ident: jump/ident: reloc/ident: range/ident: live/ident: rodata/ident: rodata-fault/ident: rodata-high/ident: text/ident: split/ident: fault/ident: high/ident: no el0), proves the PAN ID field (serialpan: id=/pan: absent), proves CNTPCT advances (serialperf: cntpct delta=…), observes CNTP ticks (serialtimer: tick) plus IRQ-to-handler deltas (serialperf: irq-delta), polls one host-injected RX byte (serialinput: rx 0x41), fires one healthy-stackBRK #0(serialexception: sync BRK), then the FR-07 nest probe (serialexception: fatal nested)VBAR_EL1vector table (high alias after MMU, ADR-017); kernel runs onSP_EL0; first-level current-EL sync (SP_EL0 bank) handles AArch64BRK, heap NX, guard-page data aborts, and the identity-tear IABORT; first-level IRQ handles GICv2 PPI 30; lower-EL AArch64 sync handles the EL0 SVC / UXN IABORT / standing dual-SVC / public SVC ABI / TTBR1 DABORT / torn-page DABORT; nested current-EL (SP_ELx bank) switches to the fatal stack (ADR-004, ADR-005, ADR-006, ADR-007, ADR-013, ADR-016, ADR-017, ADR-018, ADR-019, ADR-020, ADR-021)cargo testuses#![feature(custom_test_frameworks)]and#[test_case](including VBAR, BRK, SPSel, stack ranges, guards, GIC TYPER, CNTFRQ, timer tick, IRQ-delta samples, empty UART RX FIFO, MMU on, frames, map/unmap, heapBox/Vec, two-task yield, CNTPCT loop, W^X flags + execute-from-heap, EL0 first mile + standing enter/leave, SVC ABI yield/uart/exit + kernel-.data/ TTBR1-alias reject, libctos hello trip, guest ELF loader trip, standing-task until exit + fail-closed fault restore, ASID isolation, TTBR1 private page + high-VA EL1 fetch, identity-tear first cut +.textrange tear + vtable rewrite + live.texttear;is_active()is false at rest)- QEMU exit is ARM semihosting
SYS_EXIT/hlt #0xf000(src/qemu.rs), notisa-debug-exit. Needs-semihostingon the QEMU line (scripts/qemu-aarch64.sh). - Host smoke:
scripts/qemu-smoke.sh(hello + paging + heap + two-task sched + W^X + guards + EL0 first mile + standing + SVC ABI + libctos CRT + guest ELF loader + standing-task (A4) + ASID isolation + TTBR1 private page + high-VA EL1 fetch + identity-tear first cut + identity.textrange tear + vtable rewrite + live.texttear + CNTPCT baseline + IRQ-delta + host ELF size + timer tick + injected UART RX + BRK + fatal nested strings + tests +force-failmust be non-zero) - Docker:
Dockerfile/scripts/docker-smoke.sh(linux/arm64-friendly; do not pin amd64) - GHA:
.github/workflows/smoke.yml(ubuntu-24.04-armandubuntu-24.04)
Source + local smoke were first probed on 2026-09-08 (see the honesty ledger). Current idle tip is main ≈ 190ef5a (Merge PR #106 / docs polish after ADR-061). Recent docs/miles: ADR-058–061 plus ADR-062 yield-libctos (this PR when merged). Earlier Docker Verified on tip e80dc93 (Merge PR #28 / ADR-020; 50 tests, ident: reloc n=12, live pages=37, force-fail ok) and prior SHAs 24d94e6 (ADR-019), b0f0ee5 (#24–#26), 71ee15f (layout L3). Keep the b2bbb99 Failed row. GHA merge-commit 34651404108 on e80dc93 grepped that ADR-020 class of markers. A Route 53 CNAME ctos.artof.link → artofdream.github.io. exists; https://ctos.artof.link HTTPS is Verified (2026-09-11 after #30). Do not claim “secure OS” or “the kernel moved”; say “EL0 isolated” only as the ADR-091 sentence. Tip smoke / Docker / GHA on 97a8559 itself: see the honesty ledger (do not invent Verified from this docs edit).
Planned stages
| Stage | Domain work |
|---|---|
| Custom test framework | Landed (M2): #[test_case], semihosting exit, UART |
| CPU exceptions | M3: VBAR_EL1, resumable BRK. M4: dedicated exception + fatal stacks (FR-07) — cloud qemu-smoke Verified (honesty ledger); GHA Unknown until a run URL |
| Hardware interrupts | M5: GICv2 + CNTP tick (FR-08) — cloud qemu-smoke + GHA Verified (honesty ledger). M6: PL011 UART RX (FR-08 input / ADR-007) |
| Paging | M7: EL1 identity map + bump frames (FR-09 / ADR-008) — probe status in the honesty ledger. DTB walk staged. |
| Heap | M8: first-fit GlobalAlloc on identity-mapped frames (FR-10 / ADR-009) — probe status in the honesty ledger. |
| Scheduler | M9: cooperative EL1 yield (FR-11 / ADR-010) — probe status in the honesty ledger. Not preemptive. |
| Pillars | ADR-011: antifragility / security / performance. Threat-model v1.37 (security.md). Heap NX (ADR-012). Linker-stack guards (ADR-014). EL0 first mile + standing + ASID TLB mile; TTBR1 first cut (ADR-016); EL1 high-VA fetch (ADR-017); identity-tear first cut (ADR-018); identity .text range tear (ADR-019); live .text tear (ADR-020); SVC ABI (ADR-021); libctos CRT (ADR-022); guest ELF PT_LOAD loader (ADR-023); standing EL0 as normal mode (ADR-024); identity .rodata tear (ADR-025); PAN capability (ADR-026, enable locked non-goal on default a57 per ADR-054); thin VFS + memfs (ADR-027); virtio-blk + FAT16 (ADR-028); FAT16 write (ADR-050); FAT16 readdir (ADR-056); OS/app slot first cut (ADR-030); leftover cross-update + FAT-only hello (ADR-032); freestanding sample catalog /hello + /fsdemo + /fatdemo + /yldemo (ADR-059 / ADR-061 / ADR-062); umbrella “EL0 isolated” Verified only as the ADR-091 sentence since M6 (ADR-013 / ADR-047 / ADR-055; sponsor leftovers closure ADR-060). Product freestanding app hosting Verified under ADR-048 / ADR-052 (Track A #31; not Linux/POSIX/containers). |
| Filesystem | Thin VFS + memfs (ADR-027) + virtio-blk / FAT16 read (ADR-028) + write (ADR-050) + root readdir (ADR-056) + root delete (ADR-057) + prefix mounts (ADR-058). FAT also carries freestanding samples /hello / /fsdemo / /fatdemo / /yldemo (catalog, not a second FS). Stance: filesystem.md. |
| Host apps / containers | Gaps to Linux/shell/Python: host-apps.md. Linux-compat is a research frame (ADR-031); not claiming Linux userspace yet. Guest container runtime is a non-goal (ADR-029). Host docker-smoke is unrelated. A1 is a kernel SVC ABI mile. A2 is a libctos CRT mile. A3 is a guest ELF PT_LOAD loader mile. A4 is standing EL0 as normal mode for a loaded image. A6 is thin VFS + memfs. A7 is virtio-blk + FAT16. A8/A9 recipes + slot; freestanding catalog /hello + /fsdemo + /fatdemo + /yldemo (ADR-059 / ADR-061 / ADR-062). |
| Immutability | Scoped RO only (immutability.md). Absolute “immutable OS” is incompatible. Track A #31 / Track B #40. |
Each stage is one loop unit on the roadmap.
x86_64 remains a possible future secondary ISA. It is not a current tree.
Harness mapping (short)
Hardware and QEMU are the domain. Docs, ADRs, and this architecture note are shared understanding. Guides, sensors, the one-PR loop, second-brain vaults, merge permissions, and the honesty ledger are the outer harness. Details: formula.md.