Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ctos (ctsOS)

ctos is a small operating-system kernel you can study. It is written in Rust for 64-bit ARM (AArch64). You run it in the QEMU emulator, not as a desktop or phone OS. The nickname ctsOS is only for display; the repo and crate stay ctos.

This book is the website. The same markdown lives under docs/ in the GitHub repo. There is no second marketing copy.

It is not a product site. Do not say “secure OS,” “production ready,” or “EL0 isolated.” Status words need a probe.

What it is

ctos boots under QEMU’s virt machine and prints on a serial port (the PL011 UART). After that it grows one honest mile at a time: exceptions, paging, a heap, a tiny scheduler, then measured security and performance cuts.

Core principles drive. Roadmap tracks (A = loader and syscall ABI, B = later slots and a filesystem) are subordinate. A track does not outrank a principle.

Primary path: QEMU virt + UART (ADR-003). Frozen requirement IDs: FR-01–FR-15 and NFR-01–NFR-14.

Driving principles

Everyday meaning first; frozen IDs second. Same list as the pillars hub. This landing is the visitor-facing source. docs/framework/ stays for deep links — do not keep a second marketing copy.

PrincipleIn everyday wordsFrozen ID
HonestyIf we did not run a check, we do not say it works. Unprobed stays Unknown.NFR-06 · ledger
AntifragilityThe same miss twice becomes an automated sensor, not another README paragraph.NFR-05 · Antifragility
SecurityWrite down what we fear, then prove a slice. Not a “secure OS” slogan.NFR-10 · Security
PerformanceMeasure a known path first. No invented benches.NFR-07 · Performance
Document-firstWrite the decision, then the code. One milestone → one branch → one PR.FR-14 / NFR-13
flowchart TD
  P["Core principles<br/>honesty · antifragility · security<br/>performance · document-first"]
  L["Three pillars<br/>antifragility · security · performance"]
  T["Tracks A / B<br/>loader, ABI, later slots / FS<br/>subordinate — not the driver"]
  P --> L --> T

Principles sit above pillars. Tracks sit below both. Not a claim that Track A or B is built.

What runs today

Three samples that already have probes. Details: What can run today.

  1. Two kernel tasks that take turns — they print on the serial port and yield. Not preemptive. Not two CPUs.
  2. A serial echo gadget — one byte in, a line out. No terminal, no line editor.
  3. A short lower-privilege stub — a few instructions in the CPU’s user mode, then a call back into the kernel. Not a process. No libc, no files, no apps.

Cannot run: Linux programs, a shell, Python, network servers, filesystem apps, extra CPUs, or containers.

flowchart LR
  subgraph today ["Verified today"]
    W["Two UART workers"]
    E["Serial echo gadget"]
    S["Short user-mode stub"]
  end
  subgraph no ["Cannot run"]
    L["Linux binaries / shell / Python"]
    N["Network / files / extra CPUs"]
    C["Containers"]
  end
  today -.-> no

Left side matches existing smoke markers. Right side is out of scope. Do not say “apps run.”

How to build

You need nightly Rust and QEMU. Pages is not required for kernel work. Full list: Prerequisites.

rustup toolchain install nightly
rustup component add rust-src llvm-tools-preview
cargo build                 # ELF at target/aarch64-ctos/debug/ctos
./scripts/qemu-smoke.sh     # fail-closed serial + tests
flowchart LR
  R["rustup nightly"] --> B["cargo build"] --> Q["qemu-smoke"]

That rebuilds the kernel, including any in-tree code you add. It is not “port an app.”

If you want…Go here
What is in vs outWhat can run today · Drawbacks / limits
Why POSIX does not portBuilding or porting
Files later (not now)Filesystem (Planned)
App host / containersHosting apps — containers: no
How we measureKPIs
Why the repo is run this wayAdvantages
Deep divesVision · FR / NFR · Architecture · Roadmap · Ledger

Kernel build notes also live in the GitHub README.

URLs

URLHonesty
https://ctos.artof.linkVerified. HTTPS 200, cert for this name, landing shows Driving principles. Main deploy 34653046584 after #30.
https://artofdream.github.io/ctosRedirect. This path (no trailing slash) 301s to the custom domain. A trailing slash 404’d on the 2026-09-11 probe — do not treat github.io as a second live tree.

Publish mechanics: Docs website + DNS.

What can run today

Plain English. These are in-kernel samples (or one short user-mode stub) that already have probes on this tree. They are not third-party applications and not a product runtime.

Status of each probe: honesty ledger. How we measure: measure.md. Isolation of user programs stays Planned. Do not say “apps,” “userspace,” or “secure OS” as if a general-purpose OS existed.

Privilege — where code runs

The CPU has privilege levels. EL1 is kernel privilege (where ctos runs). EL0 is lower privilege (user mode). A process would be a loaded program with its own address space, files, and a public ABI. That process does not exist yet.

flowchart TD
  EL1["EL1 — kernel privilege<br/>Verified: this is where ctos runs"]
  EL0["EL0 — lower privilege<br/>Verified: a short standing stub only"]
  PROC["A process / application<br/>Planned: not built"]
  EL1 --> EL0
  EL0 -.-> PROC

The stub is not a process. Umbrella “EL0 isolated” stays Planned.

A supervisor call (SVC) is the instruction the stub uses to ask the kernel for something. Today’s SVC #1 / #2 are test miles, not a public syscall list.

1. Cooperative UART workers

Two tasks on heap stacks that print a line and yield to each other. Same class as the smoke markers sched: task a / sched: task b / sched: ok (ADR-010, cooperative scheduling — FR-11).

A natural variant is a serial heartbeat or counter: print a tick, yield, repeat. Still cooperative EL1. Still UART text. Not preemptive. Not two CPUs.

2. UART RX echo gadget

Read a byte from the serial receive path (PL011 RX) and print it. Same class as input: rx 0x41 (ADR-007).

That is a byte in, a line out. No TTY, no line editor, no canonical mode, no virtio-keyboard.

3. Standing EL0 stub

A short payload in user mode (EL0) that does an SVC round-trip and returns. Same class as el0: standing / el0: restored (ADR-013, el0.md).

This is not a process. There is no libc, no files, no argv, no loader for a foreign ELF. “EL0 isolated” stays Planned.

What cannot run

flowchart TD
  Q{"Want to run it on ctos today?"}
  Q -->|UART worker / echo / stub| Y["Yes — extend the kernel in-tree"]
  Q -->|Linux binary, shell, Python| N1["No"]
  Q -->|Network server or files| N2["No — no NIC, no filesystem"]
  Q -->|Docker / OCI container| N3["No — not a goal"]

“Yes” means rebuild the kernel. It does not mean drop in an app.

Do not imply these work:

  • Linux binaries (no Linux ABI, no ELF loader for third-party programs)
  • A shell
  • Python (or any hosted language runtime)
  • Network servers (no NIC, no sockets, no DMA)
  • Filesystem apps (no block device, no VFS — Filesystem (Planned))
  • Extra-CPU workloads (one CPU, cooperative yield only)

Also not claimed: POSIX, GPU, Raspberry Pi, certified security, “production ready,” or containers (Hosting apps / containers).

How you would add something in-tree (and why Linux apps do not port): Building or porting.

Prerequisites

What you need to build and run the kernel. Publishing this docs site is optional. GitHub Pages is not required for kernel work.

Kernel (required)

NeedWhy
Nightly Rust + rust-src + llvm-tools-previewFreestanding build-std for the custom aarch64-ctos.json target. Pin is rust-toolchain.toml.
qemu-system-aarch64Primary guest is QEMU -machine virt (ADR-003). Debian/Ubuntu package is often qemu-system-arm.
A host that can run those toolsWindows, macOS, or Linux. The kernel target stays AArch64.
rustup toolchain install nightly
rustup component add rust-src llvm-tools-preview
# Debian/Ubuntu: sudo apt-get install qemu-system-arm

cargo build          # ELF at target/aarch64-ctos/debug/ctos
cargo run            # qemu-system-aarch64 -machine virt
./scripts/qemu-smoke.sh
flowchart LR
  R["rustup nightly + rust-src"] --> B["cargo build"]
  B --> Q["qemu-smoke / cargo run"]

That rebuilds the kernel, including any in-tree no_std code you add. It is not a port of a Linux app. See Building or porting.

Commands and honesty notes: GitHub README. A successful cargo build on your machine is not a copied Verified boot from another host.

Optional

ExtraWhen
Docker linux/arm64cts-ai (Windows ARM64) path: ./scripts/docker-smoke.sh. Do not pass --platform linux/amd64.
mdBook 0.5.4 + mdbook-mermaid 0.17.1Local docs site only: ./scripts/docs-build.sh. See Docs website + DNS.

Not required

  • GitHub Pages, a custom domain, or ctos.artof.link (the site is live; you still do not need it to hack the kernel)
  • Raspberry Pi hardware, an x86_64 boot path, or VGA
  • Obsidian (.obsidian/ is gitignored)
  • AWS / Route 53 credentials (DNS is a publish concern, not a kernel build concern)

Building or porting something to ctos

Plain English. There is no easy POSIX port today. ctos is a kernel you extend in-tree, not a host you drop apps onto.

What already runs: What can run today. How to build the kernel: prerequisites. Probes: honesty ledger.

Do not claim an “easy port” path that does not exist.

Today (honest)

Nothing POSIX ports easily.

There is no C library (libc), no dynamic linker, no filesystem, and no stable public application ABI. There is no compiler target that produces a ctos userspace binary, and no loader that would run one if you built it elsewhere.

A Linux, musl, or glibc program is a different contract. Recompiling it “for AArch64” does not make it a ctos program.

Easiest today

Write in-tree no_std Rust and ship it as part of the kernel image.

Typical shape:

  • A cooperative kernel task (same class as sched: task a/b) or a small kernel module
  • Use UART / println! for output; yield to other tasks
  • Optional: serial receive for a byte-in gadget

Rebuild the whole guest with the existing target:

cargo build                 # aarch64-ctos.json
./scripts/qemu-smoke.sh     # or ./scripts/docker-smoke.sh on linux/arm64

That is rebuild the kernel, not “port an app.” The new code lives in src/ and links into target/aarch64-ctos/debug/ctos.

flowchart LR
  R["rustup nightly"] --> C["cargo build"] --> S["qemu-smoke"]
  S --> E["one linked ELF<br/>Verified today"]

One image, one -kernel load. An OS slot vs a separate app slot is Planned.

Not easy

  • Recompile Linux / C / Rust apps against glibc or musl
  • Drop in a userspace ELF (ET_DYN or a Linux ET_EXEC)
  • Expect std, files, sockets, threads, or a process table

Those need an ABI, a loader, and a userspace that ctos does not have. The standing user-mode stub is a test mile, not that runtime (el0.md).

Later (Planned)

A path that is not built. Call this Track A when talking about an OS slot vs app slot (Immutability):

  1. A stable SVC ABI — documented syscall numbers, not today’s test SVC #1 / #2 (a supervisor call is how user-mode code asks the kernel for help)
  2. A freestanding C runtime / libctos for user mode
  3. Link a freestanding AArch64 user-mode binary
  4. Map it into the user page table and return to user mode

Until those exist and have ledger probes, do not say applications “port to ctos.” You extend the kernel. Isolation and a real userspace stay Planned. Gaps before hosting, and why containers are no: Hosting apps / containers.

A filesystem is the same story: Planned, not present. Direction: Filesystem: new vs extend.

Filesystem: new vs extend

Today there is no filesystem. No virtual file layer (VFS), no block driver, no on-disk format, no open/read of a named file. Nothing is compatible with Linux, Windows, or a USB stick out of the box.

This page is Planned direction, not a probe that a guest can open a file. Do not say “supports FAT,” “has ext,” or “ctos has files.” Hub: honesty ledger, What can run today.

A grep of src/ for memfs / virtio-blk / FAT / inode code staying empty is the probe for “none now.”

New vs extend

Prefer implementing a known small filesystem behind a thin VFS (that VFS needs its own ADR when it lands) over inventing a novel on-disk format.

FitWhatWhy
Best firstRamdisk / memfsNamed buffers on the existing heap. Prove create / lookup / read / write without DMA or a disk image. Same “extend the kernel in-tree” shape as a coop task.
Best next on-diskvirtio-blk + FAT16/32 or a tiny xv6-like inode FSFAT if we want a host-visible image; xv6-like if we want a teaching inode layout. Pick in the ADR that lands it — this page does not ship a format.
Later, optionalctos-specific virtual mounts (memfs + one on-disk FS under one VFS)Only after memfs and a block FS have probes. Not a new magic format.

Avoid early

Do not start with ext4, btrfs, ZFS, or NTFS. Those are large, journaled or feature-heavy, and hide the VFS/block miles. They are not a first cut.

Host QEMU -drive without guest code is not a filesystem.

Roadmap order (Planned)

One milestone → one branch → one PR. Do not stack a later step on an open earlier one.

flowchart LR
  V["1. VFS ADR<br/>Planned"] --> M["2. memfs<br/>Planned"]
  M --> B["3. virtio-blk<br/>Planned"]
  B --> F["4. FAT or xv6-like<br/>Planned"]
  F --> H["5. host image probe<br/>Planned"]

Every box is Planned. File presence of this page is not a filesystem. Do not say “supports FAT.”

  1. VFS ADR — thin interface (lookup / read / write / a path). IDs unchanged until an issue + ADR says otherwise.
  2. memfs Verified — in-RAM named buffers; serial / #[test_case] that do not exist yet.
  3. virtio-blk — virtqueues + sector I/O on QEMU virt.
  4. On-disk FS — FAT16/32 or tiny xv6-like, as that ADR decides.
  5. Host-checkable image probe — a disk image the host can inspect (for FAT) or a guest round-trip the smoke script greps. Invent fs: ok / blk: ok with that PR, fail-closed.

Until those probes exist, status stays Planned.

Honesty

ClaimProbeStatus
No FS/VFS/block stack in treeSource absence in src/Verified (absence)
memfs create/lookup/read/writeSerial + #[test_case]Planned
virtio-blkQEMU disk + guest driver + markerPlanned
FAT or xv6-likeFormat + read-back / host image checkPlanned

Do not claim compatibility with anyone’s existing disk.

Hosting applications — gaps, and containers

What is missing before ctos could host an application (a loaded user-mode binary with a stable ABI), and whether it can host containers.

Today you extend the kernel (Building or porting). Samples that exist: What can run today. Isolation notes: el0.md.

Do not say ctos is an app host or a container runtime.

Today vs the slot split (A9 direction)

Today: one linked ELF. Kernel code and any “app-shaped” experiment ship together. QEMU -kernel loads that one image.

Later (Planned): an OS slot (the kernel you update) and an app slot (a loaded user-mode binary that can survive an OS swap). That split is the useful meaning of immutability. It depends on Track A (loader + stable ABI + CRT). It is not containers and not over-the-air firmware updates.

flowchart TD
  TODAY["Today: one linked ELF<br/>kernel + in-tree code<br/>Verified"]
  LATER["Later: OS slot + app slot<br/>update OS without rebuilding apps<br/>Planned — Track A / A9 direction"]
  TODAY -.-> LATER

Do not say the slot split exists. Runtime cost vs “same as today” is unmeasured.

Gaps before hosting applications

These are missing pieces, not a schedule. Rows without a probe stay Planned or unbuilt. “Later” is not a promise.

A supervisor call (SVC) is how user-mode code asks the kernel for help. Today’s SVC #1 / #2 are test miles, not a documented syscall contract.

GapWhy it blocks hostingStatus
Stable SVC ABINeed documented syscall numbers, not the test pairPlanned (direction on the porting page)
ELF / user loaderNo loader for a freestanding user-mode binary, let alone a Linux ELFNot built
Standing user mode as normalStanding enter/leave is a stub mile, not the default way code runsFirst mile Verified; normal userspace Planned
Stronger isolationUmbrella user-mode isolation needs PAN + fuller identity teardownPlanned — do not say “EL0 isolated”
VFS + memfsNo files, no paths; see FilesystemPlanned
libctos / CRTNothing to link a freestanding user-mode program againstNot built
Richer I/OUART byte in/out only; no TTY, disk, or socketsUART probed; the rest unbuilt
Preemption / extra CPUs / netCooperative one-CPU yield; no NICLater — not a near hosting gate

Until the first block has probes, “host an application” is a sentence we do not use. That first block is Track A. See Immutability.

Containers

No. ctos cannot host OCI / Docker / Kubernetes workloads.

Those need Linux kernel features (namespaces, cgroups, a Linux ABI, usually overlay or equivalent, a rich syscall surface) that this learning kernel does not have and is not aiming at soon.

Today the arrow is the other way: Docker on a host (cts-ai linux/arm64) runs the ctos smoke image. That is “Docker hosts ctos,” not “ctos hosts containers.” See the README Docker notes and the Docker rows in the ledger.

Container support is not Planned on this page. Do not add a Planned row unless the sponsor asks for that direction in an ADR.

Honesty

ClaimStatus
ctos hosts third-party appsNo — gaps above
ctos hosts OCI/Docker containersNo — not a goal on this page
Host Docker runs ctos-smokeSeparate ledger row (host tool, not a guest runtime)

KPIs / how we measure

Plain English. These are the probes this repo actually runs. They are not a product dashboard, a latency promise, or invented KPIs. Numbers live in the honesty ledger as one environment and one revision.

The three first-class pillars are antifragility (NFR-05), security (NFR-10), and performance (NFR-07). “Application support” below is an honest scope statement, not a new requirement ID.

Performance

What we measure today on QEMU virt (serial markers + tests, fail-closed in scripts/qemu-smoke.sh).

CNTPCT is the CPU’s physical cycle counter — a hardware clock we read to see that time moved, not a published bench.

ProbeWhat it isWhat it is not
Cycle-counter loop (perf: cntpct)The counter is readable and moves over a fixed trivial loopA microsecond budget, interrupt latency, or “faster than X”
IRQ-to-handler delta (perf: irq-delta)Counter minus the timer’s compare value; min / max / spread on several ticksA latency SLA, SPEC, or a comparison to other kernels
Boot-delta (perf: boot-delta)Cycle count from after paging init to after Hello World!QEMU process start time, a boot budget, or a published bench
Host ELF size (perf: elf-size)Byte size of the debug ctos ELF after cargo buildA size budget or “smaller is better”

QEMU virt is one guest. It is not Raspberry Pi, not real silicon, and not SPEC. Optimize only after a probe shows a cost. Details: performance.md.

OS slot vs app slot (performance)

Direction only — Track A is not built. Immutability means disconnect OS update from apps. That can cost at runtime or be neutral. We do not invent a percentage, a budget, or “faster than linking the app into the kernel.”

KindHonest guess (unmeasured)What would make it a claim
Possible costExtra return-to-user / supervisor call, a user page-table switch, mapping an app slot — vs today’s in-tree function callCycle-counter (or irq-delta) around a real load + enter/leave once Track A exists
Possible neutralSteady-state UART print / yield after the app is mapped, if the hot path stays similarSame probes on the new path vs the in-tree workers; no win claimed without a delta
Build-time, not a benchKernel ELF no longer contains the “app”; you rebuild slots separatelyperf: elf-size is still one image’s byte count, not “smaller is better”

Measure first (NFR-07). Do not tune a loader “for speed” on a hunch. Do not copy QEMU ticks into a product slide.

Stability / antifragility

We do not publish an uptime KPI. We measure whether sensors stay fail-closed and whether a repeated miss becomes a ratchet (NFR-05, antifragility.md).

MechanismWhat it proves
scripts/qemu-smoke.shBuild + required serial strings + tests exit 0. Missing a marker is a fail, not a skip.
cargo test --features force-failThe panic path is fail-closed (host/QEMU exit 1). A green suite that cannot fail is not a sensor.
Honesty ledgerEvery status word is a claim. Verified needs a probe. Unknown = unprobed. Planned = not built yet. Failed = the probe ran and lost. File presence is not QEMU boot.
Host ratchetsWhen a machine finds what CI missed, we keep the Failed row and add a sensor. Example: cts-ai Docker on main b2bbb99 missed paging: ok after a larger nightly layout; GHA had been green.

A green GitHub Actions run is one pair of runners. It is not “every host.” Docker on cts-ai is a separate row.

Application support

Scope only — not a new requirement ID. Concrete examples and the cannot-run list: What can run today.

Security work is probed (heap not-executable, stack guards, read-only code / not-executable data, user-mode miles) — that is not a product security KPI. See security.md. Do not say “production ready.”

Advantages

These are properties of how the repo is run, not a product pitch. They are useful if you want a small AArch64 kernel you can study without inflated status.

The landing Driving principles block (honesty, antifragility, security, performance, document-first) is the force; Track A/B stay subordinate. Visitor-facing source: landing. Deep pillar notes: pillars.md.

Document-first

Vision → architecture → ADR → roadmap stay ahead of code for each loop (document-first — FR-14, NFR-13). Frozen IDs are FR-01–FR-15 and NFR-01–NFR-14. New IDs are not invented in chat.

Probed claims

Status words need a command, a serial capture, a gh run URL, or a file read that matches the claim (honesty — NFR-06, honesty ledger). Unprobed stays Unknown. That is slower to write and harder to game than a green README badge.

QEMU virt learning scope

Primary ISA is AArch64 on QEMU virt + serial UART (ADR-003). The scope is small on purpose: UART, exceptions, paging, heap, a cooperative scheduler, then pillar cuts. It is not a distro and not a board bring-up until a board probe exists.

Three pillars as requirements

After the cooperative scheduler (M9), antifragility, security, and performance are first-class (ADR-011, pillars.md). They still need probes. “Secure” and “fast” are not default adjectives.

Author ≠ merger

ADR-002: the author does not merge their own PR. artofdream vs cursor[bot]. Same-login Approve is still self-review.

Fail-closed sensors

scripts/qemu-smoke.sh and force-fail are meant to break when a marker disappears. Repeated host misses become ratchets, not extra paragraphs of advice (antifragility.md).

Immutability

Scoped yes. Absolute no. Two different sentences:

Page-table scope (probed today). Some kernel memory is read-only and executable, other memory is writable and not executable (W^X on that image slice — write or execute, not both). Smoke markers: ro: ok (ADR-015) and identity .text tear (ident: range / ident: live, ADR-019, ADR-020) on this QEMU virt guest. Not “the kernel is immutable” or “W^X everywhere.” Claim only with a ledger probe.

OS slot vs app slot (direction, not built). The useful product meaning is: update the OS without rebuilding the apps, and the reverse. That needs a separate OS slot (the kernel image you -kernel today) and an app slot (a loaded user-mode binary that survives an OS swap). That slot split depends on Track A — the Planned loader + stable supervisor-call ABI + libctos path on Building or porting and Hosting apps. Until Track A has probes, there is only one slot: in-tree kernel code.

This is not containers, and not OTA / A-B firmware updates. Those are later and unclaimed. Do not say “immutable OS updates” until an OS-slot/app-slot probe exists.

Runtime cost vs neutral is unmeasured. Measure first; no invented numbers. KPIs — OS slot vs app slot.

Limits of this shape: Drawbacks / limits.

Drawbacks / limits

What this project is not, and what is still unfinished. Pair with KPIs / how we measure and product vision.

Learning kernel, not a product

ctos is a research / teaching AArch64 kernel. It is not production-ready, not a desktop, not POSIX, not a container host, and not a “secure OS.” Do not treat a green QEMU smoke as certification.

Boot contract still starts at 0x4008_0000

QEMU -kernel and _start stay at 0x4008_0000. High-address work (kernel page-table alias, live code tear after a vtable rewrite) does not mean the kernel moved. Some data can still be identity-mapped (virtual address equals physical). Full identity teardown is Planned. See el0.md and ADR-020.

Privileged Access Never unclaimed on cortex-a57

PAN is a hardware feature that would stop the kernel from casually reading user memory. Default probe CPU is -cpu cortex-a57 (ARMv8.0). Do not claim PAN. The ledger row stays Planned until the CPU reports the feature and an access fault is probed. Do not silently switch -cpu.

No network, disk, or DMA

There is no NIC driver, no virtio-net, no block device, no VFS, and no DMA API. Input on virt is serial receive. Timer is the virt interrupt controller plus the generic timer. That is the I/O surface. Filesystem direction (Planned only): Filesystem: new vs extend.

No real userspace apps

The standing user-mode stub is a mile, not an application runtime. No ELF loader for third-party programs, no libc, no extra CPUs, no GPU. Concrete examples: What can run today. How to extend the kernel (not port POSIX): Building or porting.

Docs URL

https://ctos.artof.link serves this book (HTTPS Verified after #30 — website.md). A trailing-slash github.io/ctos/ path 404’d on the 2026-09-11 probe; use the custom domain. Publishing mechanics can still lag a new commit until the next main Pages deploy.

Immutability is not absolute

Read-only code / not-executable data, and a torn identity code range, are scoped probes. Do not upgrade them to “immutable kernel,” “W^X everywhere,” or “OS updates without touching apps.” An OS slot vs app slot waits on Track A and is not OTA or containers. Details: Advantages — Immutability.

Other honest gaps

  • Umbrella user-mode isolation: Planned (PAN + full identity teardown still missing).
  • x86_64 is not a primary path.
  • Raspberry Pi / other boards: unprobed.
  • CI on GitHub is a separate ledger row from a cloud-VM qemu-smoke.

Product vision

Core principles are the driving force for ctos. Tracks and nicknames are subordinate. Non-negotiable: honesty ledger, antifragility (fail-closed + ratchets), security (threat model, probed mitigations only), performance (measure first), document-first / one-PR loops. principles.md.

What ctos is

ctos is a learning and research bare-metal OS kernel written in Rust for AArch64 (arm64 primary; ADR-003). The near-term product is a QEMU virt guest that owns the machine after -kernel load: UART text, then exceptions, paging, a heap, and a tiny scheduler — in that order.

It exists so we can study kernel mechanics with a document-first harness: claims stay honest, failures become sensors, and agents do not merge their own work. After M9 that harness sits on three first-class pillars — antifragility, security, and performance (ADR-011). Security and speed are not afterthoughts; they still need probes before anyone writes “secure” or “fast.”

What ctos is not

  • Not a production operating system, desktop, or app runtime. Probed guest samples: apps-today.md.
  • Not a Linux distro, not POSIX, not a container host. Porting stance: building-or-porting.md. No filesystem today: filesystem.md. Gaps to host apps: host-apps.md.
  • Not a florist / commerce platform and not a clone of any shop case study.
  • Not a Raspberry Pi (or other board) port until a board probe exists.
  • Not a claim that QEMU boot, CI, hardware bring-up, a “secure OS,” an “immutable OS,” or a published bench is finished until a probe says so. Scoped immutability: immutability.md.
  • Not an x86_64-primary kernel. x86_64 may become a secondary target later; it is not implemented now.

Success (current horizon)

A new session can read the vision, principles, overview, what can run today, building or porting, host-app gaps, architecture, honesty ledger, and latest daily brief, then take one roadmap milestone to a GitHub PR without inventing status.

ctos — Functional & Non-Functional Requirements

Learning/research AArch64 (arm64) Rust bare-metal kernel (not a general-purpose desktop OS).

ISA revision: ADR-003 (2026-09-08) replaced x86_64 / VGA / bootimage wording with AArch64 / UART / QEMU virt. IDs are unchanged. Sponsor authorized the text revision; do not mint new FR/NFR IDs here. The sponsor brief called this “ADR-002”; that number was already the identity-split ADR on the harness tip.

FR-06 stage note (2026-09-09): Stage moved Next → Now when M3 landed VBAR_EL1 + resumable BRK (ADR-004). ID unchanged.

FR-07 stage note (2026-09-09): Stage moved Next → Now when M4 landed the dedicated exception / fatal stacks (ADR-005). ID unchanged.

FR-08 stage note (2026-09-09): Stage moved Next → Now when M5 landed GICv2 + the EL1 physical timer (ADR-006). ID unchanged. FR-08 “later input” is M6 / ADR-007 (PL011 RX on virt). No new FR ID.

FR-09 stage note (2026-09-09): Stage moved Next → Now when M7 landed the EL1 identity map + bump frame allocator (ADR-008). ID unchanged. DTB parse remains staged (Unknown). Heap is M8 / FR-10.

FR-10 stage note (2026-09-09): Stage moved Next → Now when M8 landed GlobalAlloc on a first-fit heap backed by identity-mapped frames (ADR-009). ID unchanged. Growing / slab heaps remain later.

FR-11 stage note (2026-09-09): Stage moved Later → Now when M9 landed cooperative round-robin yield on EL1 (ADR-010). ID unchanged. Preemption / SMP / EL0 remain later.

NFR pillars note (2026-09-09): ADR-011 revises NFR-05, NFR-07, and NFR-10 text in place (antifragility, performance, security as first-class pillars). IDs unchanged. Do not mint NFR-15+.

NFR-10 W^X note (2026-09-10): ADR-012 revises NFR-10 text in place for the heap / coop-stack NX cut. ADR-013 records EL0 direction; the first mile is enter/return + UXN fetch, not isolation. ADR-014 adds unmapped linker-stack guards. IDs unchanged.

NFR-10 / NFR-08 note (2026-09-11): ADR-015 revises NFR-10 text in place for the RO+NX text/data cut (SCTLR.WXN on). ADR-013 adds a user-TTBR0 read mile, an ASID-tagged TLB mile, and a standing EL0 context; ADR-016 adds a TTBR1 private-page first cut; ADR-017 adds EL1 fetch from the TTBR1 RAM alias; ADR-018 adds the first identity-tear cut (split tables + one torn text page); ADR-019 jumps the post-MMU continuation to high VA and unmaps a 16 KiB dedicated identity text range; ADR-020 rewrites rustc vtables to high aliases and unmaps live identity .text after the boot stub (.rodata/.data/heap stay). The umbrella isolation row stays Planned (PAN + full identity teardown still missing). NFR-08 boot-to-ready CNTPCT is a measurement, not a budget. IDs unchanged.

Status legend: Now = hello-UART / QEMU virt / smoke sensors · Next = near roadmap · Later = aspirational.

Tracker is GitHub (gh). New IDs go through a GitHub issue plus an ADR/docs change — not chat.

Scope

In: freestanding no_std Rust, QEMU -kernel_start, UART/earlycon output, exceptions/interrupts, paging, heap, basic multitasking, reproducible build/QEMU probes, docs-first harness (honesty, three pillars — antifragility / security / performance — second brain, thin multi-agent).

Out (for now): userspace processes, POSIX, networking stack, GPU, SMP, real-hardware certification, secure-boot productization, Raspberry Pi or other board bring-up (unprobed), x86_64 as a primary path.

IDs below are frozen. Do not invent new FR/NFR IDs in chat; add via issue + ADR/docs change.

Functional requirements

IDRequirementPriorityStage
FR-01Kernel builds as a freestanding no_std / no_main binary for a custom aarch64-*-none target.MustNow
FR-02QEMU (-kernel on virt) loads the kernel ELF and transfers control to a stable _start entry.MustNow
FR-03Kernel can write text to the virt PL011 UART (MMIO, or later earlycon) via print! / println!. Not VGA 0xb8000.MustNow
FR-04Panic handler prints a message (best-effort) and halts without unwinding.MustNow
FR-05Developer can produce a runnable AArch64 kernel ELF and boot it under qemu-system-aarch64 (-machine virt).MustNow
FR-06Synchronous exceptions are handled via VBAR_EL1 vectors (at least a breakpoint / fault path).MustNow
FR-07A fatal exception uses a dedicated stack so overflow does not silently lock the VM.MustNow
FR-08Hardware interrupts: timer via the virt GIC (M5). Input on QEMU virt is PL011 UART RX (M6). The x86-era keyboard wording is this UART path; virtio-keyboard remains later.ShouldNow
FR-09Kernel reads the firmware/QEMU memory map (DTB when probed) and establishes paging / virtual memory. M7: virt RAM convention + linker __kernel_end + EL1 identity map (ADR-008). DTB walk is staged.MustNow
FR-10GlobalAlloc heap so alloc types (Box, Vec) work in kernel. M8: first-fit list on a 64 KiB identity-mapped frame run (ADR-009).ShouldNow
FR-11Cooperative or simple round-robin task switching (threads or async tasks). M9: EL1 yield of AAPCS64 callee-saved GPRs; two heap-backed workers (ADR-010).ShouldNow
FR-12Serial remains usable for headless/CI logs (PL011 or extra earlycon), including later test-exit telemetry.ShouldNext
FR-13Integration tests that boot in QEMU virt and exit with a deterministic success/fail code (ARM semihosting SYS_EXIT, not x86 isa-debug-exit).MustNow
FR-14Documented milestone path (vision → architecture → ADR → roadmap) stays ahead of code for each stage.MustNow
FR-15Honesty ledger records each major capability as claim + probe + status (Verified / Unknown / Planned).MustNow

Non-functional requirements

IDRequirementPriorityStage
NFR-01 SafetyPrefer safe Rust; unsafe only at hardware/FFI boundaries, minimized and justified.MustNow
NFR-02 ReproducibilityToolchain pinned (rust-toolchain.toml); target JSON + .cargo/config.toml checked in.MustNow
NFR-03 Portability (dev)Build+QEMU path works on Windows/macOS/Linux (host tools); kernel target remains freestanding AArch64.MustNow
NFR-04 CI / sensorsPR gate runs scripts/qemu-smoke.sh (build + UART hello string + cargo test exit codes). GitHub Actions + optional Docker.ShouldNow
NFR-05 AntifragilityFirst-class pillar (ADR-011). Repeated failures become ratchets (#[test_case], scripts/qemu-smoke.sh grep, Dockerfile pin, GHA) — not README-only fixes. Fail-closed sensors stay on the strongest layer.MustNow
NFR-06 HonestyStatus words in docs/PRs need a probe; unprobed stays Unknown; no self-merge as “verified.”MustNow
NFR-07 PerformanceFirst-class pillar (ADR-011). Performance claims need a measurable probe (CNTPCT delta around a known path; IRQ-to-handler CNTPCT−CVAL min/max/spread). No fake benches and no “faster than X.” Optimize only after a probe shows a cost. Not a hard latency budget.ShouldNow
NFR-08 FootprintDebug image size and boot time tracked once measurable; no premature optimization.CouldLater
NFR-09 MaintainabilityClear module layout; ADRs for boot-path, console, or allocator choices.MustNow
NFR-10 SecurityFirst-class pillar (ADR-011). No secrets in repo. Do not claim “secure OS” / “hardened” / “the kernel is W^X” without a written threat model and a probe. Prefer minimize unsafe (NFR-01). Heap and heap-backed cooperative stacks are PXN (ADR-012). Linker-stack downward overflow hits an unmapped 4 KiB guard (ADR-014). .text/.rodata are RO+X and .data/.bss/live linker stacks are RW+NX; SCTLR.WXN is on (ADR-015). That scoped image cut is not a “secure OS” sentence. Device MMIO XN. Least privilege on IRQ paths. EL0 first mile (enter/return + cannot execute kernel data), the user-TTBR0 read mile, the ASID-tagged TLB mile, and the standing EL0 context are probes (ADR-013). TTBR1 private-page first cut is a probe (ADR-016). EL1 fetch from the TTBR1 RAM alias (plus high VBAR_EL1) is a probe (ADR-017). The first identity-tear cut (split TTBR1 RAM tables + one unmapped identity text page) is a probe (ADR-018). A 16 KiB dedicated identity text range plus a high-VA continuation is a probe (ADR-019). A high-VA vtable / fn-pointer rewrite plus live identity .text tear after the boot stub is a probe (ADR-020); identity -kernel stub stays; .rodata/.data/heap stay; full identity teardown stays Planned. Umbrella isolation stays Planned. Do not claim PAN on -cpu cortex-a57.MustNow
NFR-11 ObservabilityQEMU UART + explicit test exit codes are first telemetry; host Grafana out of scope.ShouldNext
NFR-12 Multi-agentThin roles (engineer / knowledge / coherence / MRC); implementer ≠ merge approver.ShouldNow
NFR-13 Document-firstWritten milestone acceptance criteria match what code/CI actually prove.MustNow
NFR-14 Second brainSession memory + daily briefs in git so agents inherit state without chat archaeology.ShouldNow

Acceptance sketch (Now)

  • cargo build succeeds with pinned nightly for aarch64-ctos.json.
  • qemu-system-aarch64 -machine virt shows the hello line on serial or honesty ledger says Unknown until probed.
  • scripts/qemu-smoke.sh fails closed if the hello string is missing or cargo test does not exit 0.
  • cargo test uses ARM semihosting so QEMU exits 0 on pass and 1 on panic (force-fail).
  • Current-EL BRK is handled via VBAR_EL1 (serial string and/or #[test_case]) or the honesty ledger says Unknown until probed.
  • Nested / fatal exception prints a serial marker from a dedicated stack (FR-07) or the honesty ledger says Unknown until probed.
  • A timer tick is observable on serial and/or #[test_case] via the virt GIC (FR-08 / M5) or the honesty ledger says Unknown until probed.
  • A received PL011 byte is observable on serial (FR-08 input / M6) or the honesty ledger says Unknown until probed.
  • MMU-on + allocate-frame / map-unmap is observable on serial and/or #[test_case] (FR-09 / M7) or the honesty ledger says Unknown until probed.
  • Box / Vec on the kernel heap is observable on serial and/or #[test_case] (FR-10 / M8) or the honesty ledger says Unknown until probed.
  • Two cooperative tasks are observable on serial and/or #[test_case] (FR-11 / M9) or the honesty ledger says Unknown until probed.
  • A CNTPCT baseline delta is observable on serial and/or #[test_case] (NFR-07) or the honesty ledger says Unknown / Planned until probed.
  • An IRQ-to-handler CNTPCT delta (min/max/spread) is observable on serial and/or #[test_case] (NFR-07) or the honesty ledger says Unknown until probed.
  • Threat-model v1.1 exists under docs/framework/security.md; “secure OS” stays unclaimed (NFR-10).
  • Heap NX / execute-from-heap caught is observable on serial and/or #[test_case] (NFR-10 / ADR-012) or the honesty ledger says Unknown until probed.
  • Linker-stack guard store faults observably (NFR-10 / ADR-014) or the honesty ledger says Unknown until probed.
  • EL0 first mile (SVC return and/or UXN IABORT) is observable on serial and/or #[test_case] (NFR-10 / ADR-013) or the honesty ledger says Unknown / Planned until probed.
  • Host debug ELF size is printed by scripts/qemu-smoke.sh (NFR-08) or the honesty ledger says Unknown until probed.
  • Boot-to-ready CNTPCT (perf: boot-delta) is observable on serial and/or #[test_case] (NFR-08) or the honesty ledger says Unknown until probed.
  • RO+NX text/data (execute-from-.data and write-to-RO-text) is observable on serial and/or #[test_case] (NFR-10 / ADR-015) or the honesty ledger says Unknown until probed.
  • EL0 cannot-read-kernel-data (user TTBR0) is observable on serial and/or #[test_case] (NFR-10 / ADR-013) or the honesty ledger says Unknown / Planned until probed.
  • ASID isolation (dual ASID without TLBI VMALLE1 + conflict/stale-entry probe) is observable on serial and/or #[test_case] (NFR-10 / ADR-013) or the honesty ledger says Unknown / Planned until probed.
  • Standing EL0 (el0: standing / el0: restored, is_active() flips) is observable on serial and/or #[test_case] (NFR-10 / ADR-013) or the honesty ledger says Unknown / Planned until probed.
  • TTBR1 private page (ttbr1: el1 / ttbr1: no el0 / ttbr1: ok) is observable on serial and/or #[test_case] (NFR-10 / ADR-016) or the honesty ledger says Unknown / Planned until probed.
  • EL1 fetch from the TTBR1 RAM alias (ttbr1: el1 exec / ttbr1: vbar) is observable on serial and/or #[test_case] (NFR-10 / ADR-017) or the honesty ledger says Unknown / Planned until probed. Identity -kernel stub at 0x40080000 stays.
  • Identity-tear first cut (ident: split / ident: fault / ident: high / ident: no el0 / ident: ok) is observable on serial and/or #[test_case] (NFR-10 / ADR-018) or the honesty ledger says Unknown / Planned until probed. High-VA continuation plus a 16 KiB dedicated identity text range (ident: jump / ident: range / ident: text) is observable on serial and/or #[test_case] (NFR-10 / ADR-019) or the honesty ledger says Unknown / Planned until probed. High-VA vtable rewrite plus live identity .text tear (ident: reloc / ident: live) is observable on serial and/or #[test_case] (NFR-10 / ADR-020) or the honesty ledger says Unknown / Planned until probed. Full identity teardown (.rodata/.data/heap) stays Planned. Umbrella isolation stays Planned. Do not claim PAN.
  • Panic path compiles and is reachable in principle.
  • This FR/NFR file + honesty ledger live under docs/.

Traceability

Map roadmap milestones and PRs to FR/NFR IDs in PR descriptions when touching behavior.

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

  1. qemu-system-aarch64 -machine virt -cpu cortex-a57 (or max; gic-version=3 is allowed).
  2. -kernel loads the kernel ELF into virt RAM (linker base 0x40080000).
  3. Entry is _start (assembly in src/main.rs): set SP, zero BSS, call kernel_main.
  4. Early output is the virt PL011 UART at 0x0900_0000 (src/uart.rs).
  5. kernel_main installs VBAR_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 .text range tear ADR-019, live .text tear ADR-020), then the frame pool after MMU + SCTLR.C (pre-MMU .bss stores can vanish), then the first-fit heap (ADR-009), then the cooperative scheduler (ADR-010), then GICv2 + CNTP (ADR-006). After Hello 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 .text, CNTPCT + IRQ-delta, several timer ticks, and polls PL011 RX (ADR-007).

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.

Current stage (UART hello + M2–M9 + ADR-011 pillars)

  • Pl011 writer with TX-full wait and \n\r\n; try_recv on UARTFR.RXFE / UARTDR
  • print! / println! via spin::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_end is 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-std includes alloc
  • Cooperative round-robin (src/sched.rs): idle on the linker thread stack, two heap-backed workers, AAPCS64 callee-saved yield (ADR-010)
  • kernel_main prints Hello World!, proves map/unmap (serial paging: ok), proves Box/Vec (serial heap: ok), proves two tasks (serial sched: task a / sched: task b / sched: ok), proves heap NX (serial wx: ok), proves linker-stack guards (serial guard: ok), proves the EL0 first mile + standing context (serial el0: ok / el0: standing / el0: restored), proves ASID isolation (serial asid: ok), proves the TTBR1 private page and high-VA EL1 fetch (serial ttbr1: ok / ttbr1: el1 exec / ttbr1: vbar), proves the identity-tear first cut, .text range tear, vtable rewrite, and live .text tear (serial ident: ok / ident: jump / ident: reloc / ident: range / ident: live / ident: text / ident: split / ident: fault / ident: high / ident: no el0), proves CNTPCT advances (serial perf: cntpct delta=…), observes CNTP ticks (serial timer: tick) plus IRQ-to-handler deltas (serial perf: irq-delta), polls one host-injected RX byte (serial input: rx 0x41), fires one healthy-stack BRK #0 (serial exception: sync BRK), then the FR-07 nest probe (serial exception: fatal nested)
  • VBAR_EL1 vector table (high alias after MMU, ADR-017); kernel runs on SP_EL0; first-level current-EL sync (SP_EL0 bank) handles AArch64 BRK, 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 / 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)
  • cargo test uses #![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, heap Box/Vec, two-task yield, CNTPCT loop, W^X flags + execute-from-heap, EL0 first mile + standing enter/leave, ASID isolation, TTBR1 private page + high-VA EL1 fetch, identity-tear first cut + .text range tear + vtable rewrite + live .text tear; is_active() is false at rest)
  • QEMU exit is ARM semihosting SYS_EXIT / hlt #0xf000 (src/qemu.rs), not isa-debug-exit. Needs -semihosting on 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 + ASID isolation + TTBR1 private page + high-VA EL1 fetch + identity-tear first cut + identity .text range tear + vtable rewrite + live .text tear + CNTPCT baseline + IRQ-delta + host ELF size + timer tick + injected UART RX + BRK + fatal nested strings + tests + force-fail must be non-zero)
  • Docker: Dockerfile / scripts/docker-smoke.sh (linux/arm64-friendly; do not pin amd64)
  • GHA: .github/workflows/smoke.yml (ubuntu-24.04-arm and ubuntu-24.04)

Source + local smoke were first probed on 2026-09-08 (see the honesty ledger). Current idle tip is maine80dc93 (Merge PR #28 / ADR-020). cts-ai Docker Desktop linux/arm64 ./scripts/docker-smoke.sh is Verified on that SHA (50 tests, ident: reloc n=12, live pages=37, force-fail ok). Earlier Docker Verified: 24d94e6 (ADR-019), b0f0ee5 (#24–#26), 71ee15f (layout L3). Keep the b2bbb99 Failed row. GHA merge-commit 34651404108 on e80dc93 grepped the same class of markers. A Route 53 CNAME ctos.artof.linkartofdream.github.io. exists; https://ctos.artof.link HTTPS is Verified (2026-09-11 after #30). Do not claim “secure OS,” “the kernel moved,” or “EL0 isolated.”

Planned stages

StageDomain work
Custom test frameworkLanded (M2): #[test_case], semihosting exit, UART
CPU exceptionsM3: VBAR_EL1, resumable BRK. M4: dedicated exception + fatal stacks (FR-07) — cloud qemu-smoke Verified (honesty ledger); GHA Unknown until a run URL
Hardware interruptsM5: GICv2 + CNTP tick (FR-08) — cloud qemu-smoke + GHA Verified (honesty ledger). M6: PL011 UART RX (FR-08 input / ADR-007)
PagingM7: EL1 identity map + bump frames (FR-09 / ADR-008) — probe status in the honesty ledger. DTB walk staged.
HeapM8: first-fit GlobalAlloc on identity-mapped frames (FR-10 / ADR-009) — probe status in the honesty ledger.
SchedulerM9: cooperative EL1 yield (FR-11 / ADR-010) — probe status in the honesty ledger. Not preemptive.
PillarsADR-011: antifragility / security / performance. Threat-model v1.8 (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). Still Planned: .rodata/.data/heap tear, PAN on cortex-a57, umbrella isolation (ADR-013).
FilesystemNone today. Planned order (no FR ID): in-RAM memfs → virtio-blk → FAT or xv6-like. Stance: filesystem.md.
Host apps / containersGaps to Linux/shell/Python: host-apps.md. Guest container runtime is a non-goal. Host docker-smoke is unrelated.
ImmutabilityScoped 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.

ADR-001 — Apply a honesty/harness practice to ctos

  • Status: Accepted
  • Date: 2026-09-08

Context

ctos is a bare-metal kernel. Agents and humans will extend it across sessions. Status words drift: “it boots” gets written because source exists, or because a README says cargo run. Without a probe, that is fiction.

Harness-engineering practice (guides, sensors, a tight loop, persistent memory, no self-merge, observability of claims) is useful here. Prior art: architecture.artof.link. This ADR does not adopt that site’s product, florist domain, or stakeholder roster.

Decision

Apply a ctos-native honesty/harness practice:

  1. Honesty — every status word is a claim; the honesty ledger records the probe. Unprobed = Unknown. Never round Unknown up to Verified.
  2. Document-first — vision, architecture, ADRs, and roadmap land before more kernel features.
  3. Second brain — four vaults under research/ (procedure via skills, correction via constraints/rules, relationship via doc links, daily brief / session memory).
  4. Thin roles — Knowledge Guardian, Coherence Guardian, Kernel Engineer, MR Coordinator (ctos-* skills only).
  5. Loop — one milestone → one branch → one GitHub PR. The author does not self-approve or merge (see ADR-002).
  6. Sensors over vibes — when a failure repeats, strengthen a sensor or gate (see antifragility.md).

Tracker and reviews stay on GitHub (gh). No GitLab workflow is part of this repo.

Consequences

  • README and docs may teach cargo run without claiming QEMU boot is Verified.
  • New kernel work cites a roadmap milestone and updates the ledger when something is actually probed.
  • Role names stay ctos-*. Do not introduce aea-* hats or shop case-study content.

ADR-002 — PR author ≠ merger (two GitHub identities)

  • Status: Accepted
  • Date: 2026-09-08

Context

NFR-12 says implementer ≠ merge approver. On a solo GitHub login, a second Cursor agent cannot APPROVE a PR that login opened (GitHub self-APPROVE rule). Enabling “authors can approve their own PRs” would count same-login review as a gate. That is theater.

Café Fausse (artofdream/aea-interactive-design, .cursor/skills/pr-coordinator/SKILL.md) already probed this: cursor[bot] is the distinct second identity. Same principle applies here. This ADR does not import that repo’s restaurant/SRS content.

Decision

Reuse that split on ctos:

  1. Do not enable GitHub author self-APPROVE. Do not add a required-approval ruleset until a second human exists.
  2. Author does not merge their own PR.
  3. Identities: artofdream (owner) and cursor[bot] (Cursor GitHub App, already used for cloud PRs). Do not install another App or mint a second personal account for this.
  4. Written MRC (ctos-mr-coordinator) is the review-in-the-room. Prefer COMMENT / REQUEST_CHANGES. Record who authored / who reviewed / who merges on the PR.
  5. If cursor[bot] REST APPROVE returns 403 (probed on Café Fausse #27; unprobed on ctos), do not block a valid cursor[bot] merge of an artofdream-authored PR after this-run green checks (when CI exists) and Bugbot resolved-or-declined. Missing Approve is not a reason to treat that merge as invalid.
  6. Owner PAT / artofdream still must not APPROVE or merge an artofdream-authored PR.
Who opened the PRWho writes the reviewWho merges
artofdreamNew MRC session (COMMENT)cursor[bot] after green + Bugbot terminal
cursor[bot] (cloud / this agent)Owner, optionally plus MRC COMMENTartofdream

Consequences

  • #2 is cursor[bot]-authored scaffold: owner merges after the MRC write-up.
  • CI is still Planned on ctos. “Green checks” cannot be claimed until a workflow is probed. Fail closed: no CI yet is not “green.”
  • cursor[bot] merge and App APPROVE on this repo stay Unknown until probed here. Do not copy Café Fausse #16/#27 as a ctos probe.

ADR-003 — Primary ISA is AArch64

  • Status: Accepted
  • Date: 2026-09-08

The sponsor brief named this decision “ADR-002.” On the harness tip, ADR-002 is already the author ≠ merger identity split. This file is ADR-003 so that accepted decision stays intact. Frozen FR/NFR IDs are unchanged; ISA-specific text is revised under this ADR.

Context

The sponsor host (cts-ai) is Windows ARM64 with Docker linux/arm64. Native AArch64 QEMU is the right guest. Keeping x86_64 as primary would mean a translated or foreign ISA on that machine.

The previous primary path (phil-opp VGA text at 0xb8000, bootloader 0.9, bootimage, qemu-system-x86_64) is superseded. It was a tutorial-era lift, not a host-ISA decision.

x86_64 may be noted later as a secondary target. It is not implemented in the change that lands this ADR.

Decision

  1. Primary ISA is AArch64 (arm64). The checked-in custom target is aarch64-ctos.json (aarch64-unknown-none style: os: none, panic abort, static relocation, soft-float so early boot does not require enabling SIMD/FP in CPACR_EL1).
  2. Boot path is QEMU -kernel of the kernel ELF on -machine virt (or virt,gic-version=3) with -cpu cortex-a57 or max. Entry is _start at a linker-script address in virt RAM. No bootloader 0.9 crate and no bootimage disk as the primary path.
  3. Console is the virt PL011 UART (MMIO 0x0900_0000) via print! / println!. Not VGA 0xb8000. earlycon remains an allowed later probe; this ADR does not claim it.
  4. Do not claim Raspberry Pi or other board support until a board-specific probe exists. virt is the supported guest.
  5. Delete the x86-only primary tree (x86_64-ctos.json, src/vga_buffer.rs) rather than leave a broken dual tree.

Consequences

  • New target JSON, linker script, and UART writer replace the VGA / bootloader 0.9 stack.
  • .cargo/config.toml default target is aarch64-ctos.json. json-target-spec stays for current nightly.
  • Roadmap M0/M1 probes are UART + qemu-system-aarch64, not a VGA dump at 0xb8e60.
  • FR-01, FR-02, FR-03, FR-05 (and other x86-specific wording) are revised in place. No new FR/NFR IDs.
  • Later exception/interrupt work is VBAR / GIC, not IDT / PIC / TSS. M3 / ADR-004 is the VBAR_EL1 + BRK path.
  • Honesty ledger rows for the x86 VGA path are historical. They do not verify the AArch64 path.

ADR-004 — EL1 VBAR and resumable BRK

  • Status: Accepted
  • Date: 2026-09-09

Context

FR-06 requires synchronous exceptions via VBAR_EL1 (at least a breakpoint / fault path). Roadmap M3 is that path. M4 (FR-07) is a dedicated fatal stack. M5 (FR-08) is GIC IRQs.

QEMU virt -kernel usually starts the guest at EL1. -machine virt,virtualization=on starts at EL2. Exceptions taken to the current EL use that EL’s VBAR_*. Staying at EL2 and programming only VBAR_EL2 would not satisfy FR-06.

Decision

  1. Live at EL1. exception::init calls ensure_el1: if CurrentEL is EL2, set HCR_EL2.RW, copy SP to SP_EL1, and ERET to EL1h (DAIF masked). If the EL is not 1 after that, print and park. Do not treat VBAR_EL2 as the primary table.
  2. One 2 KiB-aligned table at exception_vectors, written to VBAR_EL1. All sixteen AArch64 slots exist. Current EL / SP_EL0 / synchronous saves a frame and may ERET. Current EL / SP_EL0 / IRQ is live as of ADR-006. Every other slot parks (UART line + wfe, or semihosting fail under cargo test / force-fail).
  3. Context format (current-EL sync only): x0x29, x30, ELR_EL1, SPSR_EL1, ESR_EL1. No SIMD/FP save (soft-float, ADR-003).
  4. BRK is resumable. ESR exception class 0x3C (AArch64 BRK) increments a counter, prints exception: sync BRK, adds 4 to ELR_EL1, and returns. Other synchronous exceptions are fatal for this milestone (print + park). That is not FR-07’s dedicated overflow stack.

Consequences

  • #[test_case] can execute brk #0 and continue. The hello kernel fires one BRK so scripts/qemu-smoke.sh can require the handler string on serial.
  • Lower-EL and FIQ/SError stubs are parks. Current-EL IRQ is M5 / ADR-006, not a syscall ABI.
  • A nested fault while println! holds the UART mutex can deadlock. ADR-005 (M4 / FR-07) splits SP_EL0 / SP_EL1, adds a fatal stack, and uses a raw UART write on that path.
  • This ADR does not claim Raspberry Pi, EL0, or a taken lower-EL exception.

ADR-005 — Dedicated exception and fatal stacks

  • Status: Accepted
  • Date: 2026-09-09

Context

FR-07 requires a fatal exception to use a dedicated stack so overflow does not silently lock the VM. Roadmap M4 is that path. ADR-004 already notes that a nested fault while println! holds the UART mutex can deadlock.

M3 ran the kernel and first-level current-EL exceptions on the same SP_EL1 (SPSel = 1). A nested sync exception would store another 272-byte frame on that stack. Without paging a downward overflow is not a hardware fault on QEMU virt — it smashes .bss / code with no serial evidence. M7 identity-maps RAM as one Normal block, so this is still true (no guard pages).

A data abort to an unused physical hole is QEMU-map-dependent. A nested BRK from the first-level handler is a real current-EL exception we already know how to take (FR-06).

Decision

  1. Thread stack on SP_EL0. After VBAR_EL1 is installed, copy the _start stack into SP_EL0 (MSR SP_EL0 is legal at EL1), write __exc_stack_top into SP while SPSel is still 1 (that is SP_EL1MSR SP_EL1 at EL1 is UNDEF), then msr spsel, #0. Normal kernel code uses the 64 KiB thread stack.
  2. Exception stack on SP_EL1. First-level current-EL exceptions (vector bank “Current EL, SP_EL0”) use the 16 KiB __exc_stack_* region automatically. The live sync slot moves from offset 0x200 (M3 / SP_ELx) to 0x000.
  3. Fatal stack before any nested store. Current-EL / SP_ELx slots (0x2000x380) load SP from __fatal_stack_top (8 KiB) in asm, then call handle_fatal_exception. Do not push a frame on the exception stack that may already be exhausted.
  4. Raw UART on fatal / unhandled paths. Write the PL011 without spin::Mutex so a nest during println! still produces serial.
  5. Probe is nested BRK after a near-empty thread SP. The hello kernel fires a healthy-stack BRK (M3 string), then sets a flag, moves SP_EL0 to __stack_bottom + 64 (less than the 272-byte frame), and BRKs again. The first-level handler prints exception: sync BRK from SP_EL1, then BRKs while SPSel = 1. That nest must print exception: fatal nested and park. This is not an MMU stack-overflow fault and not a GIC path (M5 / FR-08).

Consequences

  • #[test_case] can still brk #0 and return. Tests do not set the nest flag.
  • scripts/qemu-smoke.sh requires the fatal marker in addition to hello + BRK. A fatal probe missed line is fail-closed.
  • Lower-EL and first-level FIQ/SError stubs still park. First-level IRQ is M5 / ADR-006. Nested IRQ/FIQ/SError still use the fatal stack. Taking a nested IRQ remains unprobed.
  • This ADR does not claim Raspberry Pi or x86. Unmapped linker-stack guard pages are ADR-014 (a later cut; the nested-BRK probe here still stands).

ADR-006 — GICv2 and the EL1 physical timer

  • Status: Accepted
  • Date: 2026-09-09

Context

FR-08 requires hardware interrupts via the virt GIC, with a timer tick observable. Roadmap M5 is that path. ADR-004 installed VBAR_EL1; ADR-005 split stacks. First-level IRQ was still a park.

QEMU -machine virt can expose GICv2 or GICv3 (gic-version=2|3|4|host|max). The current smoke line is -machine virt with no gic-version override. QEMU 8.2’s virt default is GICv2 (distributor at 0x0800_0000, CPU interface at 0x0801_0000). GICv3 uses a redistributor at 0x080A_0000 instead of that CPU interface.

The ARM generic timer is already in the core (CNTP_*_EL0 / CNTV_*_EL0). No extra virtio device is required.

Decision

  1. Program GICv2, matching default -machine virt. Do not add a GICv3 redistributor driver in this milestone. If a later QEMU default flips to v3, pin gic-version=2 on the smoke line or add a v3 driver in a new ADR.
  2. Use the non-secure EL1 physical timer (CNTP_CTL_EL0 / CNTP_TVAL_EL0 / CNTPCT_EL0) and PPI 30. If ensure_el1 drops from EL2, set CNTHCTL_EL2.EL1PCTEN|EL1PCEN and zero CNTVOFF_EL2 so EL1 can use the counter and physical timer.
  3. Take IRQs on the current-EL / SP_EL0 bank (vector offset 0x080). The kernel runs with SPSel = 0, so first-level IRQs use SP_EL1 (the M4 exception stack). Save the same GPR + ELR/SPSR/ESR frame as sync, then EOI.
  4. Keep DAIF.I masked except for an observe window. Hello and #[test_case] unmask, wait for one tick (or a CNTPCT timeout), then remask and stop the timer so M3 BRK / M4 nested fatal are not interrupted.
  5. Raw UART on the IRQ path. The handler must not take spin::Mutex (same reason as ADR-005). First tick prints timer: tick.

Consequences

  • scripts/qemu-smoke.sh requires timer: tick and rejects timer: tick missed, then still requires the M3/M4 strings.
  • #[test_case] can wait for a tick with IRQs unmasked and continue.
  • FIQ, SError, lower-EL, and unexpected IRQ IDs stay parks / raw markers. UART input is M6 / ADR-007.
  • This ADR does not claim Raspberry Pi, GICv3, virtualization=on as the primary path, or a taken FIQ.

ADR-007 — PL011 UART RX as virt input

  • Status: Accepted
  • Date: 2026-09-09

Context

FR-08 already covers hardware interrupts (timer via the virt GIC, M5 / ADR-006) and named “later input” as the rest of that ID. Roadmap M6 is that input path. The x86-era reading of FR-08 was a keyboard. Frozen IDs stay; the arm64 text is revised here.

Options on QEMU virt:

  1. PL011 UART RX at 0x0900_0000 (already the console).
  2. virtio-input / virtio-keyboard (virtio-mmio, virtqueues, DMA). No paging or heap yet (M7/M8). Too much device stack for one milestone.
  3. PL011 UARTCR.LBE loopback as a self-test. QEMU 8.2 (Ubuntu 24.04 / this repo’s GHA and cloud probe) does not implement LBE — hw/char/pl011.c still says the loopback bit is unimplemented. LBE landed in later QEMU. A loopback-only probe would be a false pass on newer hosts and a false fail on 8.2.

Host stdin on -serial stdio does reach pl011_receive → the RX FIFO. UARTLCR_H.FEN toggles reset that FIFO, so a byte sent before uart::init is lost. Inject after Hello World! (init already ran).

Decision

  1. M6 input is PL011 RX poll, not virtio-keyboard and not a UART RX IRQ. FR-08’s “via the virt GIC” remains the M5 timer. Input does not take a new FR ID.
  2. Prove a received byte with a host inject: scripts/qemu-serial-inject.py writes 0x41 ('A') to QEMU stdin after it sees Hello World!. The hello kernel polls UARTFR.RXFE / UARTDR (DAIF.I still masked after the timer window) and prints input: rx 0x41.
  3. Fail closed. Missing marker or input: rx missed fails scripts/qemu-smoke.sh. #[test_case] only asserts the FIFO is empty when cargo test does not inject — the character proof is the serial smoke.
  4. Do not claim Raspberry Pi UART, virtio-input, GICv3 UART SPI, or QEMU LBE.

Consequences

  • scripts/qemu-smoke.sh needs python3 to drive the inject (Dockerfile installs it).
  • cargo test / scripts/qemu-aarch64.sh stay inject-free so M2–M5 cases do not wait on stdin.
  • A later virtio-keyboard or UART-RX-via-GIC path needs a new ADR; it is not this milestone.

ADR-008 — Identity map and bump frame allocator

  • Status: Accepted
  • Date: 2026-09-09

Context

FR-09 requires the kernel to read a firmware/QEMU memory map (DTB when probed) and establish paging / virtual memory. Roadmap M7 is that path. Heap GlobalAlloc is M8; the scheduler is M9.

QEMU -machine virt memory (see hw/arm/virt.c):

RegionPhysical
flash / boot ROM0x0000_0000
GICv2 distributor / CPU interface0x0800_0000 / 0x0801_0000
PL011 UART0x0900_0000
RAM (VIRT_MEM)0x4000_0000, default 128 MiB
Kernel (-kernel TEXT_OFFSET 0x80000)0x4008_0000
DTB (typical -kernel placement)RAM base 0x4000_0000, below the kernel

_start currently overwrites x0 (the firmware DTB pointer) to set SP. A full FDT memory-node walk is extra surface for one milestone and is not required to prove MMU-on + allocate-frame on this machine.

Options:

  1. 1 GiB L1 identity blocks (39-bit VA, T0SZ=25, 4 KiB granule) plus a dedicated 4 KiB map window.
  2. Fine-grained 4 KiB identity of every used page (more tables, same probe).
  3. Higher-half kernel (breaks the current linker / VBAR / UART absolute addresses).
  4. Parse the DTB before enabling the MMU (correct long-term; not the M7 fail-closed proof).

Decision

  1. Enable the MMU at EL1 after exception::init (ensure_el1). Program MAIR_EL1 (Attr0 Device-nGnRnE, Attr1 Normal WB), TCR_EL1 (TTBR0 only, 40-bit IPS, inner-shareable WB), TTBR0_EL1, then set SCTLR_EL1.M|C|I|SA. Identity: VA == PA for everything the kernel already touches.
  2. L1 blocks, not a full 4 KiB identity. Entry 0 (0x01 GiB) is Device-nGnRnE + XN (UART, GIC, flash). Entry 1 (0x4000_00000x7FFF_FFFF) was a Normal WB executable 1 GiB RAM block in M7. ADR-012 supersedes that RAM block: L1 slot 1 is now an L2 table, only 128 MiB is mapped, and __kernel_end…RAM-end is PXN. The allocator still must not hand out frames past RAM_BASE + 128 MiB.
  3. Frame pool from linker + virt convention. __kernel_end (after image + stacks, 4 KiB-aligned) through 0x4000_0000 + 128 MiB. Bump cursor plus a 16-entry free stack. Do not allocate 0x4000_00000x4008_0000 (DTB / TEXT_OFFSET hole). Smoke pins -m 128M so the pool matches the VM.
  4. Map/unmap probe uses a third GiB window (0x8000_0000) via one L2 + L3 table, not by splitting the RAM block. Serial marker paging: ok. Do not load an unmapped VA (that would take an unhandled data abort).
  5. DTB parse is staged. M7 does not walk FDT. FR-09’s “DTB when probed” stays Unknown until a later ADR. The probed map is the virt table above plus __kernel_end.
  6. No GlobalAlloc. Frames are physical pages only.

Consequences

  • scripts/qemu-smoke.sh requires paging: ok and rejects paging: probe missed, then still requires M2–M6 strings.
  • #[test_case] can assert SCTLR_EL1.M, distinct aligned frames, and a map/unmap write-through.
  • Stack overflow was not a translation fault in M7 (RAM was one Normal block). Guard pages are ADR-014, not this milestone.
  • A later DTB walker or higher-half map needs a new ADR; it is not M8’s heap.
  • This ADR does not claim Raspberry Pi, GICv3, EL0 user maps, or ASID isolation. Heap NX is ADR-012, not M7.

ADR-009 — First-fit heap on identity-mapped frames

  • Status: Accepted
  • Date: 2026-09-09

Context

FR-10 requires a GlobalAlloc heap so alloc types (Box, Vec) work in the kernel. Roadmap M8 is that path. The scheduler is M9.

M7 already identity-maps virt RAM and hands out 4 KiB frames from __kernel_end through 128 MiB (ADR-008). Frames are physical pages, not a byte allocator.

Options:

  1. Bump-only heap (no dealloc). Box/Vec construct, but drop leaks. Reuse is unproved.
  2. linked_list_allocator crate. Common rust-osdev choice; extra dependency for one milestone.
  3. First-fit free list on a fixed contiguous frame run taken from the bump pool after the MMU is on.
  4. Grow-on-demand via the M7 map window (0x8000_0000). Extra paging surface; not needed to prove FR-10.
  5. Buddy / slab. Overkill for a Box/Vec smoke.

Decision

  1. Fixed 64 KiB heap (16 frames) via frame::alloc_contiguous after paging::init. Identity VA == PA. Do not map through the M7 probe window.
  2. First-fit + address-sorted coalesce in src/heap.rs. dealloc is real; the hello probe requires a freed Box pointer to be reused.
  3. #[global_allocator] + extern crate alloc. .cargo/config.toml build-std includes alloc. OOM returns a null pointer; #[alloc_error_handler] prints heap: oom on the raw UART and parks (or SYS_EXIT 1 under test). The handler must not format — formatting an OOM can allocate again.
  4. Serial marker heap: ok. Fail closed on heap: probe missed. #[test_case] covers pool bounds, Box reuse, and Vec growth.
  5. Not a growing heap, not userspace, not the scheduler. A later slab / grow-on-demand / higher-half heap needs a new ADR.

Consequences

  • scripts/qemu-smoke.sh requires heap: ok after paging: ok, then still requires M2–M7 strings.
  • The frame pool shrinks by 64 KiB at boot. M7 frame/map tests still have the rest of the 128 MiB guest.
  • This ADR does not claim Raspberry Pi or a production allocator. M9 task stacks on this heap are ADR-010.

ADR-010 — Cooperative round-robin on EL1

  • Status: Accepted
  • Date: 2026-09-09

Context

FR-11 requires cooperative or simple round-robin task switching. Roadmap M9 is that path. The heap already exists (ADR-009); paging is an identity map (ADR-008). The kernel runs at EL1 with SPSel = 0 so SP is SP_EL0 (ADR-005).

Options:

  1. Async/await executor. Extra Future infrastructure for two markers.
  2. Timer preemption. Would steal the M5 CNTP path and needs IRQ-safe yield. Not required to prove FR-11.
  3. Cooperative yield that saves AAPCS64 callee-saved GPRs on a per-task stack and switches SP.
  4. SMP / EL0 processes. Out of scope (vision Out list).

The callee-saved layout is not obvious next to the exception frame in ADR-004 (that frame is x0x30 + ELR/SPSR/ESR on SP_EL1). A yield must not touch the exception stacks.

Decision

  1. Cooperative only. Tasks call sched::yield_now(). No timer slice. DAIF.I stays masked during the hello/test probe (M5 still remasks after its own tick).
  2. AAPCS64 callee-saved switch in context_switch: save/restore x19x28, x29, x30 on the task stack; store SP in the task slot; load the next SP. No SIMD/FP (ADR-003 soft-float). Not the exception context.
  3. Idle slot 0 is kernel_main / the test runner on the linker thread stack. Two workers get 8 KiB stacks from the M8 heap (Vec<u8>, not a stack-allocated [u8; N] that would overflow SP_EL0 during Box::new). Identity VA == PA.
  4. Round-robin among Ready slots. A worker that returns is Done (trampoline). Idle stays Ready so control returns to the caller of yield_now. Unlock the scheduler mutex before context_switch.
  5. Serial markers sched: task a, sched: task b, then sched: ok after both SPs land on distinct heap stacks. Fail closed on sched: probe missed.
  6. Not preemptive, not SMP, not EL0, not async. A later preemptive or process ADR is a new file.

Consequences

  • scripts/qemu-smoke.sh requires the three sched: strings after heap: ok, then still requires M2–M8 strings.
  • Two 8 KiB worker stacks come out of the 64 KiB heap. M8 Box/Vec probes drop before spawn.
  • Exception / fatal stacks are unchanged. A yield never runs from an IRQ handler.
  • This ADR does not claim Raspberry Pi, preemption, or userspace.

ADR-011 — Three pillars: antifragility, security, performance

  • Status: Accepted
  • Date: 2026-09-09

Frozen FR/NFR IDs are unchanged. This ADR revises NFR-05, NFR-07, and NFR-10 text in place. Do not mint FR-16+ or NFR-15+.

Context

Bring-up M0–M9 is on main (UART through a cooperative EL1 scheduler). The first-class practice so far was the honesty/harness loop (ADR-001): probes, ratchets, fail-closed CI, no self-merge.

Sponsor direction after M9: antifragility is not enough by itself. Security and performance are first-class pillars too — not afterthoughts bolted on when the kernel “feels done.”

Risks if we leave the NFR text as-is:

  • NFR-07 is still “Could / Later / learning-only.” That invites fake benches or silent “it’s fast enough” claims.
  • NFR-10 is mostly “no secrets in repo.” That is necessary and not a threat model.
  • NFR-05 already names ratchets, but it reads like a Next-stage SOP rather than a standing pillar.

This ADR does not clone florist / AEA vocabulary. The three pillars are ctos-native names for existing IDs.

Decision

  1. Three first-class pillars, same weight, different sensors:

    • Antifragility — keep the existing harness: honesty ledger, fail-closed scripts/qemu-smoke.sh, GHA smoke.yml, Docker smoke, #[test_case]. Repeated failures become ratchets, not README-only advice (antifragility.md).
    • Security — no “secure OS” / “hardened” / “W^X done” claim without a written threat model and a probe. Prefer minimize unsafe (already NFR-01). Prefer W^X / NX stacks and heap when paging can express it. No execute-from-writable heap by default once maps can mark NX. Least privilege on IRQ paths (no heap alloc, no sched::yield_now from an IRQ). Future EL0 isolation is Planned.
    • Performance — no fake benches and no invented latency numbers. Add honest measurable probes (CNTPCT delta around a known path; later timer-tick jitter). Optimize only after a probe shows a cost.
  2. Revise frozen NFR text in place (IDs stay NFR-05 / NFR-07 / NFR-10):

    • NFR-05: Must / Now. Ratchets stay the mechanism.
    • NFR-07: Should / Now. Probe language; not a hard latency budget.
    • NFR-10: Must / Now. Threat-model stub + claim gate, not only “no secrets.”
  3. W^X is Planned on this identity map. ADR-008 maps virt RAM as one executable 1 GiB L1 Normal block. The heap (ADR-009) and cooperative stacks (ADR-010) live in that block, so they are W+X today. Forbidding execute-from-heap needs an L2/L3 split (or a later higher-half map). Device MMIO is already XN. Do not claim heap NX because the L3 map window can set PXN — that window is not the heap.

  4. One small code ratchet may land with this docs PR. This change lands a baseline CNTPCT loop delta (serial perf: cntpct + #[test_case]). It does not split the L1 RAM block to fake W^X. Honesty over heroics.

  5. Post-M9 work is a pillars section, not a second bring-up stack on an open PR. Threat-model v1, W^X / NX heap+stacks, and further perf probes were listed as later loop units. The sponsor later asked for one coherent follow-up (ADR-012, ADR-013, irq-delta) rather than conflicting parallel branches.

Consequences

  • README, AGENTS.md, the roadmap, and the honesty ledger point at pillars.md.
  • A PR that says “secure” or “faster” without a ledger row is a coherence fail (NFR-06 / NFR-13).
  • The CNTPCT probe (when landed) is a baseline that the counter advances. It is not a published benchmark and not a comparison to other kernels.
  • PAN, full higher-half identity teardown, and preemption remain later. Standing EL0 and the ASID-tagged TLB mile are ADR-013. TTBR1 private page is ADR-016. EL1 high-VA fetch is ADR-017. The first identity-tear cut is ADR-018. The identity .text range tear is ADR-019. The live .text tear after a high-VA vtable rewrite is ADR-020. Umbrella isolation still Planned. RO+NX text/data is ADR-015. Stack guard pages are ADR-014. EL0 first mile + user-TTBR0 read mile are also ADR-013.

ADR-012 — W^X: NX heap and cooperative stacks

  • Status: Accepted
  • Date: 2026-09-10

Context

NFR-10 and ADR-011 require a probe before any W^X claim. ADR-008 mapped virt RAM as one executable 1 GiB L1 Normal block. The heap (ADR-009) and cooperative stacks (ADR-010) live in that block, so they were W+X. Setting PXN only on the M7 map window (0x8000_0000) would have been a fake “NX heap” claim.

Device MMIO (L1 block 0) was already XN. The missing cut is: heap and heap-backed stacks NX, kernel text still executable.

SCTLR_EL1.WXN is not usable while kernel text pages are writable — it would make text NX too.

Decision

  1. Replace the RAM L1 block with an L2 table (still 39-bit / 4 KiB / TTBR0). Map only the 128 MiB guest (0x4000_0000+128 MiB). The rest of that GiB stays invalid.
  2. 2 MiB L2 blocks where a block is entirely kernel-image or entirely frame-pool. One L3 table per mixed 2 MiB (originally the single block that straddled __kernel_end; ADR-015 splits on __data_start and KERNEL_TEXT, which can be two blocks).
  3. Executable: pages in [0x4008_0000, align_4k(__kernel_end)) (text, rodata, data, BSS, linker stacks). PXN+UXN: [__kernel_end, RAM end) (frame pool, heap, cooperative stacks) and the DTB hole below 0x4008_0000 when that hole is in the straddling L3. Device L1 stays XN. The M7 map window L3 pages are PXN (data-only).
  4. Do not claim the linker stacks NX. SP_EL0 / SP_EL1 / fatal stacks sit in the executable image. Overflow there is still not a translation fault (ADR-005).
  5. Fail-closed probe: walk PXN on kernel text (clear) and heap (set), then write RET on the heap, blr to it, and catch a current-EL permission instruction abort. Serial wx: nx heap + wx: ok. scripts/qemu-smoke.sh greps wx: ok and rejects wx: probe missed.
  6. NFR-10 text is revised in place (ID unchanged): heap + coop-stack NX is the scoped W^X claim, not “the kernel is W^X.”

Consequences

  • src/paging.rs grows L2_RAM and a kernel L3 pool (one table per mixed 2 MiB). Identity VA == PA is unchanged.
  • Linker-stack guard pages are ADR-014. The RO+NX text/data split and SCTLR.WXN are ADR-015.
  • EL0 isolation is ADR-013 (Planned). UXN is already set on RAM so a future EL0 cannot fetch kernel or heap by accident; that is not an EL0 probe.
  • This ADR does not claim a secure OS, Raspberry Pi, or side-channel resistance.

ADR-013 — EL0 isolation direction (first mile)

  • Status: Accepted (direction + first mile + user-TTBR0 read mile + ASID isolation mile + standing EL0; umbrella isolation still Planned)
  • Date: 2026-09-10
  • Updated: 2026-09-11 (standing EL0 dual-SVC; TTBR1 first cut is ADR-016; isolation still Planned)

Context

Vision and FR-11 leave userspace / EL0 as later work. NFR-10 names EL0 isolation as Planned. ADR-011 and the threat model (security.md) treat “malicious EL0” as a named adversary, not a standing userspace.

The first revision of this ADR recorded direction only (is_active() == false, lower-EL slots parked). The post-#18 deepen implements the smallest honest mile toward the closing probe. It does not invent a POSIX process model.

Decision

  1. First mile (this tree). Deliberate ERET to EL0t (DAIF masked) on one map-window page that is UXN-clear and PXN (EL1 must not fetch it). Two paths:
    • SVC #0 taken on the lower-EL AArch64 sync slot; handler prints el0: svc and ERETs back to EL1t.
    • BR X0 to a kernel .data bait; UXN on the identity image must take a lower-EL permission IABORT; handler prints el0: nx kernel and returns to EL1t. Serial el0: ok after the first-mile pair and the later miles on this path. The first mile itself does not install a standing context.
  2. Closing probe for “cannot execute kernel data.” The UXN / translation IABORT is that instruction-fetch probe. Mark that mile Verified when the serial / #[test_case] pass.
  3. User TTBR0 window (this tree, post-#19). A second L1 (L1_USER) is installed on ERET to EL0 (ASID=1 in TTBR0[63:48]). It maps kernel .text/.rodata and the exception stack so the lower-EL handler can restore kernel TTBR0 (kept in TPIDR_EL1), plus the shared map-window L2 for the trampoline. Coverage is every 2 MiB that holds those ranges, not only the first RAM 2 MiB. It omits .data / .bss / heap. An EL0 LDR from kernel .data is a lower-EL translation (or permission) DABORT (el0: no kernel read). The EL0 trampoline still TLBI VMALLE1 because kernel .data leaves are global (nG=0); dropping that flush would leak a cached .data translation into the user ASID. Programming ASID=1 on that path is still not the isolation mile.
  4. ASID isolation mile (this tree). Two EL1 TTBR0 values (kernel L1 + ASID 1 vs a clone L1 + ASID 2) map one window VA to different PAs with nG=1, and a second VA only under ASID 1. The switch is MSR TTBR0 + ISBno TLBI VMALLE1. Dual read must see the active ASID’s magic (asid: dual). The ASID-1-only VA must take a current-EL translation DABORT under ASID 2 (asid: conflict). Seeing ASID 1’s magic under ASID 2 is Failed (asid: stale). This is TLB ASID isolation on this virt guest, not “EL0 isolated.”
  5. Standing EL0 (this tree). A bounded user context on the user TTBR0: SVC #1 prints el0: standing and ERETs back to EL0; the user MOVZ must run; SVC #2 prints el0: restored and returns to EL1t. is_active() is true only between install and teardown (saved user PC / SP / TTBR0). A trampoline-only flag is not this mile. Lower-EL IRQ / FIQ / SError still park (not exercised). Not a POSIX process. Not preemption.
  6. Isolation remains Planned. Missing: PAN (typically unimplemented on -cpu cortex-a57 / ARMv8.0 — read ID_AA64MMFR1_EL1.PAN before claiming it), full TTBR1 / higher-half identity teardown (ADR-016 is the private-page cut; ADR-017 is the EL1-fetch cut; ADR-018 is the first identity-tear cut; ADR-019 jumps the post-MMU continuation high and unmaps a 16 KiB dedicated text range; ADR-020 rewrites rustc vtables and unmaps live identity .text.rodata/.data/heap stay), EL0 entry without a full TLBI. Do not move FR IDs. Do not mint FR-16+. Do not claim PAN.
  7. Lower-EL slots. AArch64 sync is live for SVC / IABORT / DABORT (including standing dual-SVC and the TTBR1 private-page load). Lower-EL IRQ / FIQ / SError and all AArch32 slots stay parks (ADR-004).
  8. Honesty. Docs and PRs may say “EL0 entered and returned,” “EL0 cannot execute kernel data,” “EL0 cannot read kernel .data,” “user TTBR0 omits kernel data,” “standing EL0 context,” or “ASID isolation” when those probes pass. The TTBR1 private-page mile is ADR-016. The EL1 high-VA fetch mile is ADR-017. The identity-tear first cut is ADR-018. The identity .text range tear is ADR-019. The live .text tear after a high-VA vtable rewrite is ADR-020. They may not say “EL0 works,” “userspace,” “EL0 isolated,” or “the kernel moved.”

Consequences

  • el0.md splits first-mile vs isolation rows.
  • Preemption and SMP remain separate later ADRs (ADR-010).
  • This ADR does not claim Raspberry Pi or a POSIX process model.

ADR-014 — Unmapped guard pages under linker stacks

  • Status: Accepted
  • Date: 2026-09-10

Context

ADR-012 made the heap and heap-backed cooperative stacks PXN. It explicitly left SP_EL0 / SP_EL1 / fatal stacks in the executable kernel image and named stack guard pages as a later ADR. ADR-005 still mitigates overflow with a dedicated fatal stack and a nested-BRK probe — that is not a translation fault.

A downward overflow of a linker stack today smashes the previous image bytes (.bss or the stack above). QEMU virt will not notice.

Options:

  1. Unmapped 4 KiB guard page(s) below each linker stack. Overflow becomes a current-EL data abort (translation). Observable serial + #[test_case].
  2. PXN on stack pages after splitting them from text. Stops execute-from-stack; overflow still smashes mapped data unless we also unmap something.
  3. SCTLR_EL1.WXN — still unusable while kernel text pages are writable.

Decision

  1. Option 1. Insert a 4 KiB linker hole below the thread, exception, and fatal stacks (__stack_guard, __exc_stack_guard, __fatal_stack_guard). After fill_ram_wx, split any 2 MiB L2 block that contains a guard into L3 and clear that L3 slot (invalid). Identity VA == PA is unchanged.
  2. Fail-closed probe: arm a current-EL translation data-abort catch, store to the thread-stack guard, resume at LR. Serial guard: fault + guard: ok. scripts/qemu-smoke.sh greps guard: ok and rejects guard: probe missed.
  3. Guard pages are holes, not NX stacks. This ADR does not by itself make live stack pages NX. ADR-015 maps those live pages RW+NX with .data. Do not claim “the kernel is W^X” from guard holes alone.
  4. NFR-10 text is revised in place (ID unchanged): mention the guard-page overflow cut. Do not mint NFR-15+.
  5. The ADR-005 nested-BRK fatal probe stays. It does not write below __stack_bottom.

Consequences

  • src/paging.rs may add one or two extra L3 tables to split an executable L2 block that is not the __kernel_end straddle.
  • Overflow of a heap-backed cooperative stack is still not this probe (those stacks live in PXN frame-pool RAM with no guard holes).
  • This ADR does not claim a secure OS, Raspberry Pi, or ASAN-quality stack checking.

ADR-015 — RO+NX text / data split

  • Status: Accepted
  • Date: 2026-09-11

Context

ADR-012 made the heap and heap-backed cooperative stacks PXN. ADR-014 punched unmapped holes under the linker stacks. Both left .text / .rodata / .data / .bss / live linker-stack pages in one executable, writable image mapping. SCTLR_EL1.WXN was unusable while text was writable.

NFR-10 still forbids saying “the kernel is W^X” without a scoped probe. This ADR is the next cut: split RO+X from RW+NX.

Decision

  1. Page-align __data_start in linker.ld so .text/.rodata never share a 4 KiB page with .data.
  2. Identity flags (still 39-bit / 4 KiB / TTBR0):
    • [KERNEL_TEXT, __data_start) — RO+X (AP[2]=1, PXN clear, UXN set)
    • [__data_start, RAM end) — RW+NX (including .data, .bss, live linker stacks, frame pool / heap)
    • Device L1 stays XN. Guard holes stay invalid (ADR-014).
  3. SCTLR_EL1.WXN on once text is RO. Writable pages are treated as XN even if a descriptor forgets PXN.
  4. Fail-closed probe: execute-from-.data (blr to a RET bait) is a current-EL permission IABORT (ro: nx data). Store to RO text is a current-EL permission DABORT (ro: write fault). Serial ro: ok. scripts/qemu-smoke.sh greps those strings and rejects ro: probe missed. #[test_case] covers flags + both faults.
  5. NFR-10 text is revised in place (ID unchanged): mention the RO+NX image cut. Do not mint NFR-15+.
  6. One L3 per mixed 2 MiB (2026-09-11 follow-up). ADR-012 assumed a single L3 for the 2 MiB that straddles __kernel_end. After this ADR the mixed block is the one that contains __data_start and the first RAM 2 MiB that contains KERNEL_TEXT (0x4008_0000). Those are different blocks once .text/.rodata (or a linker ratchet) crosses 0x4020_0000. Reusing one L3_RAM overwrites the kernel-text walk. The kernel L3 pool hands out a fresh table per straddle / guard split. The user TTBR0 maps every 2 MiB that holds text or the exception stack, not only the first RAM block.
  7. Post-MMU .bss publishes (same follow-up). USER_MAP_OK and frame::init run after SCTLR.C. A pre-MMU store to .bss can be invisible to a later cached read (boot-delta on the PR #20 test image; cts-ai Docker hello on b2bbb99 lost the frame pool / user-map ready flag while guard/ro still passed). Do not pin an old nightly for this.

Honesty — is this “the kernel is W^X”?

On this QEMU virt guest, the identity image plus heap is W^X: text/rodata are RO+X, data/bss/live linker stacks/heap are RW+NX, WXN is on. That is not:

  • a “secure OS” / “hardened” claim
  • a promise that every future mapping (DMA, new windows) stays W^X
  • Raspberry Pi or a second ISA
  • a reason to drop the heap / guard probes

Say “identity image is W^X on this virt guest (ADR-015 probe)” only when the serial / tests pass. Do not say “the kernel is W^X” as a product sentence.

Consequences

  • src/paging.rs sets AP[2] on text and PXN on data/stacks. src/ro.rs owns the two faults.
  • Live linker stacks are NX here. If a later change must execute from a linker stack, this ADR is the one to revisit.
  • EL0 user TTBR0 (ADR-013 mile) still maps kernel text so the lower-EL handler can restore kernel TTBR0; it omits .data. Coverage follows the image across L2 blocks. That is isolation-adjacent, not this ADR.
  • linker.ld parks __data_start at 0x4020_1000 so GHA / Docker / this cloud always exercise two kernel L3s. File presence of that address is not a boot probe.
  • This ADR does not claim PAN, ASID isolation, or a higher-half kernel.

ADR-016 — TTBR1 kernel-private page (first cut)

  • Status: Accepted (first cut; identity teardown Planned)
  • Date: 2026-09-11

Context

ADR-013 left TTBR1 / higher-half as a missing isolation mile. The kernel still runs from the QEMU virt identity map at 0x4008_0000 (TTBR0, T0SZ=25). TCR.EPD1 was set, so TTBR1 walks were disabled. EL0 and EL1 share the same translation regime: a high mapping is not automatically invisible to EL0 — access is an AP / UXN question.

A full higher-half relocate (kernel fetch, VBAR_EL1, stacks, UART MMIO, then tearing down the identity image) is too large for one honest PR. This ADR is the smallest Verified cut: enable TTBR1 and prove one kernel-private high page.

Decision

  1. Enable TTBR1 walks. Clear TCR.EPD1. Set T1SZ=25 (same 39-bit window as T0SZ) with WBWA / inner-shareable attrs on TTBR1. ASID still comes from TTBR0 (TCR.A1 = 0).
  2. High-half base. The TTBR1 region is 0xFFFF_FF80_0000_00000xFFFF_FFFF_FFFF_FFFF. One private page lives at TTBR1_PRIV = 0xFFFF_FF80_0000_0000 (src/paging.rs).
  3. EL1-only leaf. The page is Normal, RW, PXN, UXN, AP[2:1]=00 (EL1 RW, EL0 no data access). Backing store is a frame-pool page, also identity-mapped for EL1. User TTBR0 does not describe this VA.
  4. Fail-closed probe. EL1 store/load via the high VA prints ttbr1: el1. An EL0 LDR of TTBR1_PRIV is a lower-EL permission (or translation) DABORT (ttbr1: no el0). Serial ttbr1: ok. scripts/qemu-smoke.sh greps those strings and rejects ttbr1: probe missed / ttbr1: leaked. #[test_case] covers the same path.
  5. Identity map stays. _start, vectors, linker stacks, PL011 0x0900_0000, and the multi-L3 / layout ratchets from #21 keep using TTBR0 identity VAs. Full higher-half + identity teardown is Planned.
  6. NFR-10 text is revised in place (ID unchanged). Do not mint NFR-15+. Do not claim “the kernel runs in the high half” or “EL0 isolated.”

Honesty

Say “TTBR1 maps a kernel-private page EL0 cannot access” only when the serial / tests pass. Do not say:

  • the kernel has moved to a high VA
  • identity mappings were torn down
  • “secure OS” / “hardened” / “EL0 isolated”
  • PAN (still unclaimed on -cpu cortex-a57)

Consequences

  • paging::init programs TTBR1_EL1 and leaves EPD1 clear. src/ttbr1.rs owns the probe.
  • Standing EL0 (ADR-013) is a separate mile. Umbrella isolation stays Planned while PAN is unclaimed.
  • ADR-017 aliases identity RAM in TTBR1 and fetches a real EL1 path (plus high VBAR_EL1). ADR-018 splits those tables and unmaps one identity text page. ADR-019 unmaps a 16 KiB dedicated text range after a high-VA jump. ADR-020 unmaps live identity .text after a vtable rewrite. Full identity teardown (.rodata/.data/heap) is still Planned.

ADR-017 — EL1 fetch from the TTBR1 RAM alias

  • Status: Accepted (exec mile; first identity-tear cut is ADR-018)
  • Date: 2026-09-11

Context

ADR-016 enabled TTBR1 walks and proved one EL1-only data page at TTBR1_PRIV. The kernel still fetched instructions from the QEMU virt identity map at 0x4008_0000. Full higher-half relocate (every pointer, every fn item, then tearing down identity) is still too large for one honest PR: rustc emits link-time identity addresses (relocation-model: static), and -kernel loads _start at TEXT_OFFSET.

This ADR is the largest Verified cut that still keeps the boot stub: alias identity RAM in TTBR1 and run a real EL1 code path (instruction fetch, not a private store) at high VA.

Decision

  1. RAM alias. L1_HIGH[1] points at the same L2_RAM used by identity TTBR0. High VA is identity + TTBR1_BASE (0x4008_00000xFFFF_FF80_4008_0000). Permissions match the identity image (text RO+X, data/stacks/heap RW+NX, guards unmapped).
  2. Shared tables (superseded for the tear mile). This ADR reused L2_RAM. ADR-018 clones RAM tables so one identity text page can be unmapped without dropping the high twin. Full identity teardown is still Planned.
  3. EL1 exec probe. ttbr1_high_el1_path is invoked through its high VA (BLR / fn pointer). It captures PC with ADR, prints ttbr1: el1 exec from that path, and returns the PC. The caller checks PC is in the TTBR1 window and near the high entry. UART MMIO stays the absolute identity 0x0900_0000.
  4. VBAR_EL1 high. After the MMU is on, VBAR_EL1 is programmed to the high alias of exception_vectors (ttbr1: vbar). Later BRK / IRQ / EL0 sync fetch the table via TTBR1. Identity exception::init still installs the link address first (pre-MMU).
  5. Identity boot stub stays. _start, QEMU -kernel load, and link-time fn items remain at 0x4008_0000. Most EL1 data access still uses identity VAs. Do not say the kernel moved.
  6. Still Planned. Full identity teardown (unmap all low .text/.data/heap after a complete high-VA jump); PAN on -cpu cortex-a57; umbrella EL0 isolation. ADR-018 is the first cut (one torn text page). User TTBR0 still maps most kernel text so a missed high-VBAR path can fetch. Do not change default -cpu.
  7. NFR-10 text is revised in place (ID unchanged). Do not mint NFR-15+.
  8. ADRP is not a PA. After VBAR is high, handler code that does addr_of!(table) / user_ttbr0() must mask to the 39-bit identity VA before programming TTBR0 or comparing FAR_EL1. First attempts Failed: guard FAR compared to a high __stack_guard, then standing stay_at_el0 programmed a high L1_USER as TTBR0. paging::identity_pa / linker_sym are the ratchet.

Honesty

Say “EL1 fetched a real path from a TTBR1 high VA” or “VBAR lives at the high alias” only when the serial / tests pass. Do not say:

  • the kernel has moved to the high half
  • identity mappings were torn down
  • “secure OS” / “hardened” / “EL0 isolated”
  • PAN (still unclaimed on -cpu cortex-a57)

Consequences

  • paging::init installs the RAM alias and relocates VBAR. src/ttbr1.rs owns both the private-page probe and the exec probe.
  • ADR-016 remains the private-page first cut. This ADR is the exec mile.
  • ADR-018 splits TTBR1 RAM tables and unmaps one identity text page. ADR-019 jumps the post-MMU continuation to high VA and unmaps a 16 KiB dedicated text range. ADR-020 unmaps live identity .text after a vtable rewrite. Unmapping .rodata/.data/heap is still later.

ADR-018 — Identity teardown first cut (split tables + one torn text page)

  • Status: Accepted (partial tear; full identity teardown still Planned)
  • Date: 2026-09-11

Context

ADR-017 proved EL1 can fetch a real path from the TTBR1 RAM alias and moved VBAR_EL1 there. The alias reused identity L2_RAM, so unmapping a low page also dropped the high twin. _start / QEMU -kernel still load at 0x4008_0000. rustc emits link-time identity addresses (relocation-model: static), so a complete high-VA jump of kernel_main plus unmap of all identity .text/.data/heap is still too large for one honest PR.

This ADR is the largest Verified cut that still boots on virt: split the RAM tables, then unmap one defined identity text page that EL1 no longer needs for fetch.

Decision

  1. Split RAM tables. After identity W^X + stack guards, clone L2_RAM (and every L3 it points at) into L2_HIGH_RAM / L3_HIGH_RAM. L1_HIGH[1] points at the clone. L2 block descriptors are copied; L3 tables are duplicated. Unmapping an identity page no longer unmaps the high twin.
  2. Dedicated tear page. The linker keeps a 4 KiB ident_tear section (__ident_tear_start__ident_tear_end) in the RO+X image, after .rodata and before __data_start. It is not the 0x4008_0000 _start page.
  3. Unmap after MMU + high VBAR. paging::init turns the MMU on, programs the high VBAR, then clears the identity and user-TTBR0 L3 slots for that page and TLBI VAAE1s the low VA. The high twin stays PXN-clear.
  4. Fail-closed probe. Serial ident: split (clone + torn low / live high walks). EL1 BLR of the low VA is a current-EL translation IABORT (ident: fault). EL1 BLR of the high twin still runs ident_tear_el1_path (ident: high). EL0 LDR of the low VA is a lower-EL translation DABORT (ident: no el0). Serial ident: ok. scripts/qemu-smoke.sh greps those strings and rejects ident: probe missed / ident: leaked.
  5. Identity boot stub stays. _start, QEMU -kernel load, remaining identity .text/.data/heap, PL011 0x0900_0000, and later probes keep using TTBR0 identity VAs. Do not say the kernel moved.
  6. Still Planned. Full identity teardown (unmap all low .text/.data/heap after a complete high-VA jump); PAN on -cpu cortex-a57; umbrella EL0 isolation. Do not change default -cpu.
  7. NFR-10 text is revised in place (ID unchanged). Do not mint NFR-15+.

Honesty

Say “TTBR1 RAM tables are independent of identity” or “one identity text page was unmapped while EL1 still fetched the high twin” only when the serial / tests pass. Do not say:

  • the kernel has moved to the high half
  • identity mappings were fully torn down
  • “secure OS” / “hardened” / “EL0 isolated”
  • PAN (still unclaimed on -cpu cortex-a57)

Consequences

  • paging::init clones RAM tables and unmaps __ident_tear_*. src/teardown.rs owns the serial probe.
  • ADR-017 remains the exec mile. This ADR is the first identity-teardown cut.
  • ADR-019 jumps the post-MMU continuation to high VA and expands the dedicated tear to 16 KiB. ADR-020 rewrites rustc vtables and unmaps live identity .text. .rodata/.data/heap stay.

ADR-019 — High-VA continuation + dedicated identity text range

  • Status: Accepted (partial tear; live .text / .rodata / .data / heap still identity-mapped)
  • Date: 2026-09-11

Context

ADR-018 cloned TTBR1 RAM tables and unmapped one dedicated identity text page. rustc still emits link-time identity addresses (relocation-model: static). _start / QEMU -kernel still load at 0x4008_0000.

The preferred next cut was to unmap contiguous live identity .text after the boot stub once EL1 fetched from the high alias. A first attempt on this cloud VM Failed: after unmapping [0x4008_1000, __text_end), the first writeln! / println! took an unhandled current-EL sync. core::fmt::write takes &mut dyn Write; those vtable methods are identity fn pointers. Unmapping live .text is therefore blocked until fmt / dyn dispatch is proven high-only.

This ADR is the largest Verified cut that still boots: jump the post-MMU continuation to its TTBR1 alias, then unmap a 16 KiB dedicated identity text range (four pages), not one probe page.

Decision

  1. High-VA continuation. After paging::init (MMU on, high VBAR_EL1, ADR-018 first tear page), kernel_main BRs to the TTBR1 alias of kernel_main_high. Serial ident: jump. Direct BL stays in the high window (PC-relative). UART MMIO stays the absolute identity 0x0900_0000. Stacks / heap / .data stay identity VAs.
  2. Boot stub stays mapped. The 0x4008_0000 page (_start + exception_vectors) stays identity-mapped. Live .text after that page also stays — rustc fmt / dyn still needs those identity fn pointers.
  3. Dedicated 16 KiB tear range. Linker __ident_tear_start__ident_tear_end is four pages: page 0 holds ident_tear_el1_path (ADR-018), page 1 holds ident_range_el1_path, pages 2–3 are aligned pad. After the high jump, unmap the whole range from kernel and user TTBR0 and TLBI VAAE1 each low VA. High twins stay PXN-clear.
  4. Keep live .text / .rodata / .data identity-mapped. Do not yank them here. A botched full .text yank is worse than a smaller Verified slice.
  5. Identity fn pointers used after the jump go high where we control them (scheduler trampoline / task entries; custom test runner dyn Testable method). That is preparation, not a claim that rustc fmt is high-only.
  6. Fail-closed probe. Serial ident: jump. ident: range lo=… hi=… pages=N with N >= 4. Existing ident: split / fault / high / no el0 / ok stay. ident: text is EL1 fetch of the second torn page via the high twin. scripts/qemu-smoke.sh greps those strings and rejects ident: probe missed / ident: leaked / ident: range missed.
  7. Identity boot stub stays. _start, QEMU -kernel load, live identity .text/.rodata/.data/heap, PL011 0x0900_0000. Do not say the kernel moved.
  8. Still Planned. Unmap live identity .text after fmt / dyn calls are proven high-only; then .rodata/.data/heap; PAN on -cpu cortex-a57; umbrella EL0 isolation. Do not change default -cpu.
  9. NFR-10 text is revised in place (ID unchanged). Do not mint NFR-15+.

Honesty

Say “the post-MMU continuation ran at a high VA” or “a 16 KiB dedicated identity text range was unmapped while EL1 still fetched the high twins” only when the serial / tests pass. Do not say:

  • the kernel has moved to the high half
  • identity .text after _start was fully torn down
  • identity mappings were fully torn down
  • “secure OS” / “hardened” / “EL0 isolated”
  • PAN (still unclaimed on -cpu cortex-a57)

Consequences

  • paging::jump_high / tear_identity_text_range own the cut. src/teardown.rs extends the serial probe.
  • ADR-018 remains the split-tables + first dedicated page cut.
  • ADR-020 rewrites rustc vtables to high aliases and unmaps live identity .text after the boot stub. .rodata / .data / heap stay.

ADR-020 — High-VA fn-pointer rewrite + live identity .text tear

  • Status: Accepted (live .text after the boot stub; .rodata / .data / heap still identity-mapped)
  • Date: 2026-09-11

Context

ADR-019 jumps the post-MMU continuation to its TTBR1 alias and unmaps a 16 KiB dedicated identity text range. A first attempt to unmap live .text after _start Failed: the first writeln! / println! took an unhandled current-EL sync.

This ADR investigates that failure and takes the largest honest Verified cut toward tearing live identity .text.

Probed root cause (this cloud VM, main 24d94e6 hello ELF, then this branch):

  • rustc dyn Write vtables live in .rodata. The Pl011 table is drop=0 / size=8 / align=8 / write_str / write_char / write_fmt with identity method addresses (write_char at 0x4008_1084, immediately after the vectors page).
  • The hello .rodata held a handful of other identity .text words (Debug::fmt, PadAdapter as Write). .data had none. .text had no 8-byte identity literal pools.
  • AArch64 codegen used ADRP (PC-relative), not MOVZ/MOVK of 0x4008. After the high-VA jump, new fn() values are already high; tables linked as R_AARCH64_ABS64 stay identity until rewritten. The ELF is ET_EXEC / relocation-model: static — no leftover reloc records to apply.
  • .text and .rodata shared page 0x400a4000 on the ADR-019 layout (vtable at 0x400a4338). Unmapping “all of .text” without a page split would also drop the vtable.

relocation-model: pic was considered and not taken: static + a post-jump patcher matches the existing -kernel / 0x4008_0000 contract and does not need a dynamic linker.

Decision

  1. Page-split .text / .rodata. Linker __text_end / __rodata_start are 4 KiB aligned so live .text and fmt tables do not share a page.
  2. High-VA fn-pointer rewrite. After ident: jump, walk .rodata (through __ident_tear_start) and .data. Every 8-byte word that is an identity address in .text or __ident_tear_* is rewritten to its TTBR1 alias. RO pages are made writable only for that store, then restored (SCTLR.WXN makes a writable page XN — they are not fetched). Serial ident: reloc n=N with N >= 1. A Pl011 as dyn Write fat-pointer read must see a high write_str.
  3. Live identity .text tear. Unmap [0x4008_1000, __text_end) from kernel and user TTBR0 (TLBI VAAE1 each low VA). High twins stay PXN-clear. The 0x4008_0000 page (_start + exception_vectors) stays identity-mapped. Serial ident: live lo=… hi=… pages=N with N >= 8.
  4. Keep .rodata / .data / heap identity-mapped. String literals and rewritten vtables still live there. Do not yank them here.
  5. Keep _start / QEMU -kernel at 0x4008_0000. Do not change default -cpu. Do not claim PAN.
  6. Fail-closed probe. Existing ident: jump / range / split / fault / high / text / no el0 / ok stay. New ident: reloc / ident: live. scripts/qemu-smoke.sh greps those strings and rejects ident: reloc missed / ident: live missed. #[test_case] covers the rewrite and the live unmap.
  7. Still Planned. Unmap identity .rodata / .data / heap after those accesses are proven high-only; PAN on -cpu cortex-a57; umbrella EL0 isolation.
  8. NFR-10 text is revised in place (ID unchanged). Do not mint NFR-15+.

Honesty

Say “rustc vtable / fn-pointer words were rewritten to high aliases” or “live identity .text after the boot stub was unmapped while println! still ran” only when the serial / tests pass. Do not say:

  • the kernel has moved to the high half
  • identity mappings were fully torn down (.rodata / .data / heap stay)
  • “secure OS” / “hardened” / “EL0 isolated”
  • PAN (still unclaimed on -cpu cortex-a57)

Consequences

  • paging::rewrite_identity_fn_ptrs / paging::tear_live_identity_text own the cut. ADR-019 remains the high-VA jump + dedicated 16 KiB range.
  • A later ADR may unmap .rodata once string / table accesses are high-only, then .data / heap. That work is not this cut.

Roadmap

Each row is one loop unit: one milestone, one branch, one GitHub PR. Do not stack a later stage onto an unclosed earlier PR. Cite frozen FR/NFR IDs in the PR when touching behavior; do not invent new IDs in chat.

Primary ISA is AArch64 (ADR-003). M0/M1 probes are UART + qemu-system-aarch64, not VGA 0xb8000.

IDMilestoneProbe that closes itStatus
M0Kernel tree + harness scaffold on GitHubFiles on the default-target PR; ledger startedIn harness PRs #2/#3 (source). Merge is a human/MRC job.
M1QEMU virt boots UART “Hello World!”qemu-system-aarch64 serial shows the string; see honesty ledgerVerified in the 2026-09-08 cloud probe (not CI). x86 VGA probe is historical only.
M2Integration test harnessQEMU virt ARM semihosting exit + #[test_case]; scripts/qemu-smoke.sh fail-closedVerified in the 2026-09-08 cloud probe (not CI). Sponsor stacked this on the ISA PR.
M3Exception vectors + breakpointTest or QEMU serial proof the handler runsVerified: 2026-09-09 cloud qemu-smoke + GHA smoke.yml on the M3 PR (see honesty ledger).
M4Fatal exception stackFatal path does not silently lock the VMVerified: 2026-09-09 cloud qemu-smoke (see honesty ledger). GHA on the M4 PR branch still Unknown.
M5Hardware interrupts (GIC + timer)Timer tick observable (serial or test)Verified: 2026-09-09 cloud qemu-smoke + GHA smoke.yml on the M5 PR (see honesty ledger).
M6InputInjected PL011 RX byte observable on serial (or test)Verified: 2026-09-09 cloud qemu-smoke (see honesty ledger). GHA on the M6 PR branch still Unknown.
M7Paging + frame allocatorMap/unmap or allocator testVerified: 2026-09-09 cloud qemu-smoke (see honesty ledger). GHA on the M7 PR branch still Unknown.
M8Heap (alloc)Box/vec smoke on the heapVerified: 2026-09-09 cloud qemu-smoke + GHA smoke.yml on the M8 PR (see honesty ledger).
M9Cooperative schedulerTwo tasks observed to runVerified: 2026-09-09 cloud qemu-smoke + GHA smoke.yml (see honesty ledger).

M0–M9 are on main (M9 = merge of PR #15 / FR-11). Pillar work through ADR-020 (#17–#28) is also on main. Idle tip ≈ e80dc93 (Merge PR #28). Merge of any open PR is still a human/MRC job (ADR-002).

Pillars (post-M9)

Bring-up M0–M9 stays one loop unit each. After M9, work is grouped under the three pillars.

IDWorkProbe that closes itStatus
P-SEC-1Threat-model v1.1 slice (NFR-10)Read security.md; no “secure OS” claimVerified (file + review). v1.1 adds linker stacks, guards, EL0 first mile, remaining W^X gaps.
P-SEC-2W^X / NX heap + coop stacksPage-table PXN on heap + caught execute-from-heap IABORT (wx: ok)Verified: 2026-09-10 cloud qemu-smoke (see honesty ledger). Map: ADR-012. Linker stack pages still X.
P-SEC-2bLinker-stack guard pagesUnmapped 4 KiB holes; store faults (guard: ok)Verified: 2026-09-10 cloud qemu-smoke (see honesty ledger). ADR-014. Not kernel W^X.
P-PERF-1Baseline CNTPCT probe (NFR-07)Serial perf: cntpct and/or #[test_case]; not a published benchVerified: 2026-09-09 cloud qemu-smoke + GHA (see honesty ledger).
P-PERF-2IRQ-to-handler CNTPCT deltaSerial perf: irq-delta + samples max >= min; not a latency budgetVerified: 2026-09-10 cloud qemu-smoke (see honesty ledger).
P-PERF-3Host debug ELF size (NFR-08)Host marker perf: elf-size bytes=<n>; not a budgetVerified: 2026-09-10 cloud qemu-smoke host print (see honesty ledger).
P-SEC-3EL0 first mileel0: svc + el0: nx kernel + el0: okFirst mile Verified: 2026-09-10 cloud qemu-smoke (see honesty ledger).
P-SEC-2cRO+NX text/data (ADR-015)ro: nx data + ro: write fault + ro: okVerified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Identity image W^X on virt; not “secure OS.”
P-PERF-4Boot-to-ready CNTPCT (NFR-08)perf: boot-delta ticks=<n>; not a budgetVerified: 2026-09-11 cloud qemu-smoke (see honesty ledger).
P-SEC-3bUser TTBR0 + EL0 cannot read kernel .datael0: no kernel read; user table omits .dataVerified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Isolation / PAN stay Planned.
P-SEC-3cASID-tagged TLB isolationasid: dual + asid: conflict + asid: ok; no TLBI VMALLE1 on the switchVerified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Umbrella isolation / PAN stay Planned.
P-SEC-3dStanding EL0 contextel0: standing + el0: restored; is_active() true only while standingVerified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Not POSIX. Lower-EL IRQ still parks.
P-SEC-3eTTBR1 kernel-private page (ADR-016 first cut)ttbr1: el1 + ttbr1: no el0 + ttbr1: okVerified: 2026-09-11 cloud qemu-smoke (see honesty ledger).
P-SEC-3fEL1 fetch from TTBR1 high VA (ADR-017)ttbr1: el1 exec + ttbr1: vbar + existing ttbr1: okVerified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Identity boot stub stays.
P-SEC-3gIdentity-tear first cut (ADR-018)ident: split + ident: fault + ident: high + ident: no el0 + ident: okVerified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Full identity teardown / PAN / umbrella isolation stay Planned. Do not claim “the kernel moved.”
P-SEC-3hHigh-VA jump + 16 KiB identity text range (ADR-019)ident: jump + ident: range + ident: text + existing ident: okVerified: 2026-09-11 cloud qemu-smoke + cts-ai Docker on 24d94e6 (see honesty ledger). Do not claim “the kernel moved.”
P-SEC-3iHigh-VA vtable rewrite + live identity .text tear (ADR-020)ident: reloc + ident: live + existing ident: ok; println! after the tearVerified: 2026-09-11 cloud qemu-smoke + cts-ai Docker on e80dc93 + GHA merge-commit 34651404108 (see honesty ledger). .rodata / .data / heap stay. Do not claim “the kernel moved.”
P-SEC-3jIdentity .rodata / .data / heap tearThose identity ranges unmapped; accesses proven high-onlyPlanned. After live .text (ADR-020), not instead of it.
P-SEC-3kPAN on virt cortex-a57ID_AA64MMFR1_EL1.PAN != 0 and an EL1-vs-EL0 access faultPlanned. ARMv8.0 cortex-a57. Do not switch -cpu silently.
P-SEC-3lUmbrella EL0 isolationStanding + PAN + full TTBR1 / identity teardownPlanned. Specific miles (P-SEC-3…P-SEC-3k) are not this row. Do not claim “EL0 isolated.”

Hub: pillars.md.

Filesystem work is Planned and is not a row above. Intended order (one PR each, after a VFS ADR): memfs → virtio-blk → on-disk FAT or xv6-like → host-checkable image. Do not claim FAT. See Filesystem: new vs extend.

Tracks (subordinate to principles)

Track A (freestanding apps, #31) and Track B (Linux-compat research, #40) do not override principles.md. A9 is after Track A ABI/loader.

Docs website

mdBook + GitHub Pages (not a kernel milestone, not a new FR/NFR ID). https://ctos.artof.link HTTPS is Verified after #30 (deploy 34653046584 + HTTPS 200). Route 53 CNAME remains in place (zone Z1178AFMV41RWP). Overview: what can run today. Publish notes: website.md.

Track A — freestanding app hosting

Tracker epic: issue #31. Child issues A1–A9.

Driving force (non-negotiable)

This track is subordinate to ctos core principles (principles.md). Do not skip a probe, drop a ratchet, or invent a “secure/fast app runtime” to finish a row faster.

  • Honesty ledger — Verified only with a probe
  • Antifragility — fail-closed smoke + ratchets
  • Security — threat model; probed mitigations only
  • Performance — measure first; no invented benches
  • Document-first / one milestone → one branch → one PR

A9 (OS/app slot disconnect) sits after A1–A4. It does not override the list above.

Goal

A freestanding (non-POSIX) application can be loaded and run with honest probes. Not Linux containers. Not glibc.

Ordered work

One loop unit each. Each row needs a fail-closed ledger probe.

IDWorkStatus (2026-09-11)
A1Stable SVC / syscall ABI + docsPlanned (#32)
A2Freestanding CRT / libctosPlanned (#33)
A3ELF (or raw) loader into user TTBR0Planned (#34)
A4Standing EL0 as normal modePlanned (#35)
A5Isolation completion (remaining identity tear; PAN only if CPU + ADR)Planned (#36)
A6Thin VFS + memfsPlanned (#37)
A7virtio-blk + FAT or xv6-likePlanned (#38)
A8Documented sample appsPlanned (#39)
A9Disconnect OS image from app payloadsPlanned after A1–A4 (#48). Today: one linked ELF — not Verified.

Out of scope for Track A

OCI/Docker containers, glibc/musl ports, SMP, networking (unless a later ADR). See host-apps.md.

Samples today: site what-can-run.md (extra: apps-today.md). Porting: site porting.md (extra: building-or-porting.md).

Track B — Linux-compat research (keep ctos specificity)

Tracker epic: issue #40. Child issues B1–B7.

Driving force (non-negotiable)

This track is subordinate to ctos core principles (principles.md). Linux-compat research must not abandon honesty, fail-closed sensors, the three pillars, or virt learning scope. Do not override principles for a “runs Linux” headline.

  • Honesty ledger — Verified only with a probe
  • Antifragility — fail-closed smoke + ratchets
  • Security — threat model; probed mitigations only
  • Performance — measure first; no invented benches
  • Document-first / one milestone → one branch → one PR

Track A (track-a.md) should largely complete before deep Linux ABI work. Reuse A1–A7 where they help. Full Linux ABI + containers is not a promise.

Goal

Explore what Linux userspace / ABI-subset work would take without dropping ctos specificity. This is an ADR/research ladder, not a distro.

Keep

Honesty ledger, fail-closed smoke (including Docker/cts-ai ratchets), pillars (antifragility / security / performance), ADR-gated ISA/memory/EL0 decisions, QEMU virt until an ADR widens it.

Ordered research

IDWorkStatus (2026-09-11)
B1ADR: Linux-compat goals & non-goalsPlanned (#41)
B2Syscall surface map (Linux aarch64 vs ctos SVC)Planned (#42)
B3Process model vs Linux (fork/exec/wait)Planned (#43)
B4Linux ELF / auxv / PT_INTERP vs freestanding loaderPlanned (#44)
B5Linux VFS concepts vs thin ctos VFSPlanned (#45)
B6Decision: compat layer vs reimplement vs neverPlanned (#46)
B7Containers remain a non-goal (OCI needs a Linux host)Planned as documentation (#47); guest runtime already no in site hosting-apps.md (extra: host-apps.md)

Do not claim Docker/OCI host without namespaces/cgroups/overlay. That stays out or far-later — not a Track B “win.”

Core principles (driving force)

The project name is ctos. ctsOS is an optional display nickname only — use ctos in docs, issues, and code.

These principles are the driving force. They are non-negotiable. Track A, Track B, and A9 are subordinate: they never override principles for speed or marketing.

PrincipleWhat it meansHome
Honesty ledgerStatus words need a probe. Unprobed stays Unknown. No rounding up.honesty-ledger.md
AntifragilityFail-closed sensors. Repeated failures become ratchets, not README advice.antifragility.md / NFR-05
SecurityWritten threat model. Mitigations are claimed only when probed. No “secure OS.”security.md / NFR-10
PerformanceMeasure first. No invented benches. Optimize only after a probe.performance.md / NFR-07
Document-first / one-PR loopsOne milestone → one branch → one GitHub PR. Author ≠ merger.ADR-002, roadmap

Landing page (same list, site SoT): Driving principles. Hub: overview.md. Pillars: pillars.md. Vision: product-vision.md.

ctos harness map

A reliable kernel session is:

Shared understanding + domain + outer harness.

That three-part split is common harness-engineering language (prior art: architecture.artof.link). On ctos it is not a product name. It is a map for this repo.

Shared understanding

The session’s reviewable model of intent and state:

  • Vision, architecture, ADRs, roadmap
  • Honesty ledger (what we claim vs what we probed)
  • Daily briefs and random-thoughts (handoff, not source of truth)

If a claim is only in chat, it is not shared understanding.

After M9, shared understanding also names the three pillars (antifragility, security, performance). A “secure” or “faster” sentence without a ledger row is not shared understanding.

Domain

The thing that is allowed to decide what is true about the machine:

  • Kernel source and the custom AArch64 target
  • QEMU virt (and later real hardware, if probed)
  • Kernel ELF loaded by -kernel
  • CPU, UART, interrupt, and memory behavior

Agents interpret. The domain (a build, a QEMU probe, a test) decides. Do not “confirm boot” from a README sentence.

Outer harness (six layers, kernel-scaled)

LayerOn ctosPrevents
GuidesAGENTS.md, .cursor/rules/, .cursor/skills/ctos-*Out-of-scope work (wrong crate era, shop content, GitLab SOPs)
Sensorsscripts/qemu-smoke.sh, cargo test semihosting exit, GHA smoke.yml, optional DockerRounding “source exists” up to “it boots”
LoopOne milestone → one branch → one GitHub PRSprawling PRs that mix UART, paging, and docs rewrites
Memoryresearch/ vaultsSession amnesia; stuffing raw chat into the next prompt
PermissionsAuthor ≠ merger (artofdream vs cursor[bot]); MRC writes COMMENT; no GitHub self-APPROVE (ADR-002)Same-login stamp counted as a second review
ObservabilityHonesty ledger + PR text that lists probed vs UnknownStatus theater

Fail closed: if QEMU was not run, boot status is Unknown. Do not write Verified.

ctos honesty ledger

Status words are claims. Each row needs a probe. Unprobed = Unknown. Never round Unknown up to Verified because the file exists, a PR is open, or a README teaches cargo run.

Allowed flags: Verified (probe passed), Unknown (no probe or probe blocked), Planned (not built yet), Failed (probe ran and lost).

ClaimProbeStatusNotes
UART hello source present (println!("Hello World!"); + PL011 writer)Read src/main.rs and src/uart.rs on this branchVerifiedSource inspection. Boot is a separate row.
.cargo/config.toml present (build-std, aarch64-ctos.json, qemu runner, json-target-spec)Read .cargo/config.tomlVerifiedPresence. Build success is the next row.
cargo build for aarch64-ctos.jsoncargo +nightly build on 2026-09-08 (rustc 1.100.0-nightly cea272fa3)VerifiedFirst attempt failed: invalid aarch64 ABI combination until the JSON had both "abi": "softfloat" and "rustc-abi": "softfloat". Then the ELF built (target/aarch64-ctos/debug/ctos, AArch64, entry 0x40080000).
QEMU aarch64 serial shows Hello Worldqemu-system-aarch64 8.2.2, -machine virt -cpu cortex-a57 -display none -serial stdio -kernel target/aarch64-ctos/debug/ctos; timeout 4VerifiedSerial printed Hello World! then the VM was killed (exit 124). Same string on -machine virt,gic-version=3 and -cpu max. One cloud environment, not CI. Not a Raspberry Pi probe.
x86_64 VGA / bootimage pathHistorical probes on 2026-09-08 (PR #2)HistoricalPath removed by ADR-003. Those Verified rows do not apply to this tree.
CI on GitHubWorkflow file exists and a run is greenVerified.github/workflows/smoke.yml: push run 34285784458 success (ubuntu-24.04 52s, ubuntu-24.04-arm 1m9s). PR run 34285786641 success. ubuntu-24.04-arm label is available on this repo.
Docker smoke (Dockerfile / scripts/docker-smoke.sh)cts-ai Docker Desktop linux/arm64, 2026-09-09: docker build -t ctos-smoke . && docker run --rm ctos-smoke → exit 0VerifiedAfter three Failed runs, ratchets landed in git and the full smoke passed on cts-ai: serial Hello World! (hello QEMU timeout 124, expected); cargo test [ok]; force-fail exit 1; qemu-smoke: ok. Failures that were ratcheted: (1) CRLF shebang → exec ./scripts/qemu-smoke.sh: no such file or directory (.gitattributes *.sh/Dockerfile eol=lf, image sed, CMD bash); (2) linker cc not found on compiler_builtins (build-essential); (3) failed to find romfile "efi-virtio.rom" (qemu-efi-aarch64 + ipxe-qemu). One sponsor host, not CI and not this cloud VM (still no Docker engine here).
Second-brain vaults (research/)Paths exist; README explains Procedure / Correction / Relationship / Daily BriefVerifiedStructure present. Not a claim that vaults are richly filled. Optional Obsidian UI is structure-only (PR #3).
Thin ctos-* rolesAGENTS.md + .cursor/skills/ctos-*/SKILL.md existVerifiedFour roles. No aea-* names.
Frozen FR/NFR IDs (FR-01FR-15, NFR-01NFR-14)Read docs/02-requirements/fr-nfr.md; IDs present; ISA text revised under ADR-003VerifiedFrozen IDs. Text now AArch64/UART. Do not invent extra FR/NFR IDs in chat. File presence is not a claim that every Now row is implemented.
PR identity split (author ≠ merger; artofdream vs cursor[bot])Read ADR-002, AGENTS.md, .cursor/rules/pr-identity-no-self-merge.mdcVerifiedDocs present. Principle reused from Café Fausse pr-coordinator (identity only). cursor[bot] merge / App APPROVE on this repo: Unknown until probed on artofdream/ctos.
Primary ISA is AArch64Read ADR-003; x86_64-ctos.json / src/vga_buffer.rs absentVerifiedDecision + file tree. QEMU boot is a separate row.
Integration tests / QEMU test exit (M2)cargo +nightly test → QEMU virt + -semihosting; #[test_case]; QEMU host exit 0Verified2026-09-08 cloud: Running 2 tests / [ok]. 2026-09-09 M3: 4 tests. 2026-09-09 M4 re-probe: Running 6 tests / all [ok] (adds SPSel + stack-range cases).
Fail-closed test paniccargo +nightly test --features force-failVerified2026-09-08 and 2026-09-09 cloud: panic force-fail, QEMU/host exit 1.
scripts/qemu-smoke.shRan on this cloud VMVerified2026-09-09 M4 (qemu-system-aarch64 8.2.2, rustc 1.100.0-nightly 4aa1fbcf4): hello (timeout 124), exception: sync BRK (esr=0xf2000000), exception: fatal nested (kind=0x200), cargo test 0 (6 cases), force-fail 1, qemu-smoke: ok. First hello run Failed: MSR SP_EL1 at EL1 is UNDEF (esr=0x2000000) before Hello World! — fixed by setting SP_EL1 via mov sp while SPSel=1 (ADR-005). Not Docker. GHA on this PR is a separate row.
VBAR_EL1 installed at 2 KiB-aligned exception_vectors (M3 / FR-06)cargo +nightly test vbar_el1_points_at_tableVerified2026-09-09 cloud: [ok]. CurrentEL==1 and mrs vbar_el1 equals exception_vectors. ELF also had the table at 0x40080800.
Current-EL sync BRK handler runs and returns (M3 / FR-06)cargo +nightly test breakpoint_from_current_el; hello-kernel serialVerified2026-09-09 cloud: test [ok]; hello serial exception: sync BRK esr=0xf2000000 elr=0x40081190 then timeout 124 (wfe loop). Other sync classes and lower-EL slots are parks — taking those is unprobed.
qemu-smoke requires BRK handler stringscripts/qemu-smoke.sh hello phase greps exception: sync BRKVerified2026-09-09 cloud: qemu-smoke: BRK handler string present. Extends the M2 sensor (FR-06 / NFR-04).
Lower-EL AArch64 sync is live (SVC / IABORT); other lower-EL slots parkSource read of vector table; taken path is the EL0 first-mile rowVerifiedSource: sync_lower_el. IRQ/FIQ/SError lower-EL and AArch32 still park. Taken lower-EL IRQ: Unknown.
CI on GitHub (M3 PR / this branch).github/workflows/smoke.yml on cursor/m3-vbar-el1-exceptions-bd73VerifiedPush 34389110709 success (ubuntu-24.04 + ubuntu-24.04-arm). PR 34389132580 success. Same qemu-smoke.sh as the cloud probe (hello + BRK string + 4 tests + force-fail). Commit 72ecd37.
Dedicated SP_EL1 exception stack + SPSel=0 thread stack (M4 / FR-07)cargo +nightly test spsel_uses_thread_stack + stacks_are_distinct_and_alignedVerified2026-09-09 cloud: both [ok]. SPSel==0 and current SP in __stack_*; three stack ranges 16-byte aligned and disjoint.
Nested current-EL exception uses fatal stack and is observable (M4 / FR-07)Hello-kernel serial exception: fatal nested via scripts/qemu-smoke.shVerified2026-09-09 cloud: after two exception: sync BRK lines, serial exception: fatal nested then exception: fatal esr=0xf2000000 elr=0x400816cc kind=0x200. Nested AArch64 BRK after near-empty thread SP (ADR-005). Not an MMU overflow fault. Not GIC (M5).
qemu-smoke requires fatal nested stringscripts/qemu-smoke.sh hello phase greps exception: fatal nested and rejects fatal probe missedVerified2026-09-09 cloud: qemu-smoke: fatal nested string present. Extends the M3 sensor (FR-07 / NFR-04).
CI on GitHub (M4 PR / this branch).github/workflows/smoke.yml on cursor/m4-fatal-exception-stack-c8b7UnknownNot a green GHA URL on the post-fix revision before merge. M4 merged as PR #7 (180dbf2).
GICv2 init + CNTP PPI 30 tick observable (M5 / FR-08)Hello-kernel serial timer: tick via scripts/qemu-smoke.sh; #[test_case] timer_tick_is_observableVerified2026-09-09 cloud (QEMU 8.2.2, rustc 1.100.0-nightly 4aa1fbcf4): after Hello World!, serial timer: tick, then M3/M4 BRK + fatal. cargo test Running 9 tests all [ok] including gicd_typer_readable, cntfrq_is_nonzero, timer_tick_is_observable. Not GICv3. Not UART input (M6).
qemu-smoke requires timer tick stringscripts/qemu-smoke.sh hello phase greps timer: tick and rejects timer: tick missedVerified2026-09-09 cloud: qemu-smoke: timer tick string present. Extends the M4 sensor (FR-08 / NFR-04).
CI on GitHub (M5 PR / this branch).github/workflows/smoke.yml on cursor/m5-hardware-interrupts-d5a6VerifiedPush/PR run 34391557761 success (ubuntu-24.04 + ubuntu-24.04-arm) on the cloud-probe commit. Implementation commit run 34391552019 also success. Same qemu-smoke.sh (hello + tick + BRK + fatal + 9 tests + force-fail). Commit e28e4bb.
PL011 RX of injected 0x41 observable (M6 / FR-08 input)Hello-kernel serial input: rx 0x41 via scripts/qemu-smoke.sh + qemu-serial-inject.pyVerified2026-09-09 cloud (QEMU 8.2.2, rustc 1.100.0-nightly 4aa1fbcf4): after Hello World! and timer: tick, serial input: rx 0x41, then M3/M4 BRK + fatal. Not virtio-keyboard. Not QEMU LBE (unimplemented on 8.2).
qemu-smoke requires UART RX stringscripts/qemu-smoke.sh hello phase greps input: rx 0x41 and rejects input: rx missedVerified2026-09-09 cloud: qemu-smoke: UART RX string present. Extends the M5 sensor (FR-08 input / NFR-04).
Empty RX FIFO under cargo test (M6)#[test_case] uart_rx_fifo_empty_without_host_byteVerified2026-09-09 cloud: cargo test Running 10 tests all [ok]. Character proof is the serial row.
CI on GitHub (M6 PR / this branch).github/workflows/smoke.yml on cursor/m6-uart-rx-input-7ef6UnknownNo green GHA URL on the cloud-probe commit yet.
EL1 identity map + MMU on (M7 / FR-09)Hello-kernel serial paging: ok via scripts/qemu-smoke.sh; #[test_case] mmu_is_enabledVerified2026-09-09 cloud (QEMU 8.2.2, rustc 1.100.0-nightly 4aa1fbcf4): after Hello World!, serial paging: ok, then M5/M6/M3/M4 markers. cargo test mmu_is_enabled [ok]. Identity 1 GiB blocks (ADR-008). Not a DTB walk.
Frame alloc + map/unmap window (M7 / FR-09)#[test_case] frame_alloc_aligned_and_distinct + map_unmap_roundtrip; serial marker from paging::observe_probeVerified2026-09-09 cloud: both [ok]. Write-through window VA 0x8000_0000 matches identity PA.
qemu-smoke requires paging stringscripts/qemu-smoke.sh hello phase greps paging: ok and rejects paging: probe missedVerified2026-09-09 cloud: qemu-smoke: paging string present. Extends the M6 sensor (FR-09 / NFR-04).
CI on GitHub (M7 PR / this branch).github/workflows/smoke.yml on cursor/m7-paging-frame-allocator-b567UnknownNo green GHA URL on the cloud-probe commit yet.
GlobalAlloc + Box/Vec on identity-mapped frames (M8 / FR-10)Hello-kernel serial heap: ok via scripts/qemu-smoke.sh; #[test_case] box_alloc_roundtrip + vec_grows + heap_lives_in_frame_poolVerified2026-09-09 cloud (QEMU 8.2.2, rustc 1.100.0-nightly 4aa1fbcf4): after paging: ok, serial heap: ok, then M5/M6/M3/M4 markers. cargo test Running 17 tests all [ok]. First-fit + coalesce (ADR-009). Not a growing heap. Not M9.
qemu-smoke requires heap stringscripts/qemu-smoke.sh hello phase greps heap: ok and rejects heap: probe missedVerified2026-09-09 cloud: qemu-smoke: heap string present. Extends the M7 sensor (FR-10 / NFR-04).
CI on GitHub (M8 PR / this branch).github/workflows/smoke.yml on cursor/m8-heap-globalalloc-0ee8VerifiedPush run 34394697056 success (ubuntu-24.04-arm 59s, ubuntu-24.04 1m36s) on the cloud-probe commit ba6096a. Same qemu-smoke.sh (hello + paging + heap + tick + RX + BRK + fatal + 17 tests + force-fail).
Cooperative two-task yield (M9 / FR-11)Hello-kernel serial sched: task a / sched: task b / sched: ok via scripts/qemu-smoke.sh; #[test_case] two_tasks_run_on_distinct_heap_stacks + yield_round_robin_resumes_bothVerified2026-09-09 cloud (QEMU 8.2.2, rustc 1.100.0-nightly 4aa1fbcf4): after heap: ok, serial sched: task a, sched: task b, sched: ok, then M5/M6/M3/M4 markers. cargo test Running 19 tests all [ok]. Cooperative EL1 yield (ADR-010). Not preemptive. Not SMP.
qemu-smoke requires scheduler stringsscripts/qemu-smoke.sh hello phase greps sched: ok and both task markers; rejects sched: probe missedVerified2026-09-09 cloud: qemu-smoke: scheduler strings present. Extends the M8 sensor (FR-11 / NFR-04).
qemu-smoke requires CNTPCT perf stringscripts/qemu-smoke.sh hello phase greps perf: cntpct and rejects perf: probe missedVerified2026-09-09 cloud: qemu-smoke: CNTPCT perf string present. Extends the M9 sensor (NFR-07 / NFR-04).
CI on GitHub (M9 PR / this branch).github/workflows/smoke.yml on cursor/m9-cooperative-scheduler-7da9VerifiedFolded from #16 (not re-probed here): push 34395786658 and PR 34395791288 success (ubuntu-24.04-arm + ubuntu-24.04) on 88a9305. Merge-commit push 34396135202 success on 4e3b732. Same qemu-smoke.sh (hello + paging + heap + two-task sched + tick + RX + BRK + fatal + 19 tests + force-fail).
Three pillars accepted (ADR-011); NFR-05 / NFR-07 / NFR-10 text revised in placeRead ADR-011 and fr-nfr.md; IDs still NFR-05/NFR-07/NFR-10VerifiedDocument inspection. Not a kernel boot claim.
Threat-model v1 exists (NFR-10)Read security.md (assets, adversaries, trust boundaries, non-goals, mitigations→probes)VerifiedFile + review (2026-09-10, #18). Replaces the ADR-011 stub. “Secure OS” / “hardened”: unclaimed.
Threat-model v1.1 slice (NFR-10)Read security.md (linker stacks, guards, EL0 first mile, remaining W^X gaps)VerifiedFile + review. v1.1 update, not “secure.”
Threat-model v1.5 slice (NFR-10)Read security.md (standing EL0, TTBR1 first cut, EL1 high-VA fetch, PAN/identity teardown still Planned)VerifiedFile + PR #25. v1.5 update, not “secure.”
Threat-model v1.6 slice (NFR-10)Read security.md (identity-tear first cut, PAN/full teardown still Planned)VerifiedFile + PR #26. v1.6 update, not “secure.”
Threat-model v1.7 slice (NFR-10)Read security.md (high-VA jump + 16 KiB dedicated identity text range, live .text stays, PAN/full teardown still Planned)VerifiedFile + PR #27 (on main 24d94e6). v1.7 update, not “secure.”
Threat-model v1.8 slice (NFR-10)Read security.md (high-VA vtable rewrite + live identity .text tear, .rodata/.data/heap stay, PAN/full teardown still Planned)VerifiedFile + PR #28 (on main e80dc93). v1.8 update, not “secure.”
W^X / NX heap + coop stacks (ADR-012)Hello-kernel serial wx: nx heap + wx: ok via scripts/qemu-smoke.sh; #[test_case] heap_and_mmio_are_pxn + kernel_text_is_executable + execute_from_heap_is_caughtVerified2026-09-10 cloud (QEMU 8.2.2, rustc 1.100.0-nightly a36d05efa): after sched: ok, serial wx: nx heap then wx: ok. cargo test Running 25 tests all [ok]. L2/L3 PXN on [__kernel_end, RAM end) (ADR-012). Linker SP_EL0 / SP_EL1 / fatal stacks stay in the executable image. Device MMIO L1 is XN. Not a “secure OS” or “the kernel is W^X” claim.
qemu-smoke requires W^X stringscripts/qemu-smoke.sh hello phase greps wx: ok and rejects wx: probe missedVerified2026-09-10 cloud: qemu-smoke: W^X string present. Extends the pillars sensor (NFR-10 / NFR-04).
Baseline CNTPCT loop probe (NFR-07)Hello-kernel serial perf: cntpct via scripts/qemu-smoke.sh; #[test_case] cntpct_advances_over_loopVerified2026-09-10 cloud re-probe (QEMU 8.2.2, rustc 1.100.0-nightly a36d05efa): after wx: ok, serial perf: cntpct delta=22968. Prior 2026-09-09 pillars probe also Verified. Counter-advances baseline. Not a published bench.
IRQ-to-handler CNTPCT delta (NFR-07)Hello-kernel serial perf: irq-delta via scripts/qemu-smoke.sh; #[test_case] irq_delta_samples_recordedVerified2026-09-10 cloud (QEMU 8.2.2): after timer: tick, serial perf: irq-delta min=12973 max=35391 spread=22418 n=8. cargo test irq_delta_samples_recorded [ok]. CNTPCT−CVAL spread on this QEMU virt guest. Not a latency budget. Not “faster than X.”
qemu-smoke requires IRQ-delta stringscripts/qemu-smoke.sh hello phase greps perf: irq-delta and rejects perf: irq-delta missedVerified2026-09-10 cloud: qemu-smoke: IRQ-delta perf string present. Extends the NFR-07 sensor (NFR-04).
Linker-stack guard pages (ADR-014)Hello-kernel serial guard: fault + guard: ok via scripts/qemu-smoke.sh; #[test_case] linker_stack_guards_unmapped + store_to_thread_guard_is_caughtVerified2026-09-10 cloud (QEMU 8.2.2, rustc 1.100.0-nightly a36d05efa): after wx: ok, serial guard: fault then guard: ok. cargo test both guard cases [ok]. Unmapped 4 KiB holes (ADR-014). Live stack pages stay executable. Not “the kernel is W^X.”
qemu-smoke requires guard stringscripts/qemu-smoke.sh hello phase greps guard: ok and rejects guard: probe missedVerified2026-09-10 cloud: qemu-smoke: stack-guard string present.
Host debug ELF size (NFR-08)scripts/qemu-smoke.sh prints perf: elf-size bytes=<n> and rejects < 4096Verified2026-09-10 cloud: host perf: elf-size bytes=3752056 after cargo +nightly build. Measurement only. Not a budget. Not a bench.
EL0 entered and returned (SVC)Hello-kernel serial el0: svc + el0: ok; #[test_case] el0_svc_roundtripVerified2026-09-10 cloud: after guard: ok, serial el0: svc then el0: ok. cargo test el0_svc_roundtrip [ok]. is_active() stays false. Not userspace.
EL0 cannot execute kernel dataHello-kernel serial el0: nx kernel; #[test_case] el0_cannot_execute_kernel_dataVerified2026-09-10 cloud: serial el0: nx kernel. cargo test [ok]. Lower-EL UXN IABORT on kernel .data. 2026-09-11 hello re-probe (this branch) still prints el0: nx kernel (now also a translation IABORT under user TTBR0). Not isolation.
EL0 isolation (P-SEC-3)Standing EL0 + PAN + full TTBR1/higher-half teardownPlannedDirection ADR-013. Standing + 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) are different rows. PAN and full identity teardown (.rodata/.data/heap) still missing. Do not claim “EL0 isolated.”
RO+NX text/data (ADR-015)Hello-kernel serial ro: nx data + ro: write fault + ro: ok; #[test_case] execute-from-.data + write-to-RO-textVerified2026-09-11 cloud qemu-smoke (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e, -cpu cortex-a57): after guard: ok, serial ro: nx data then ro: write fault then ro: ok. cargo test both RO cases [ok]. Identity image W^X on this virt guest (text RO+X, data/stacks/heap RW+NX, SCTLR.WXN on). Not a “secure OS.”
qemu-smoke requires RO+NX stringsscripts/qemu-smoke.sh hello phase greps ro: ok / ro: nx data / ro: write faultVerified2026-09-11 cloud: qemu-smoke: RO+NX string present.
Boot-to-ready CNTPCT (NFR-08)Hello-kernel serial perf: boot-delta; #[test_case] boot_delta_sample_existsVerified2026-09-11 cloud qemu-smoke: perf: boot-delta ticks=111019 (after paging::init → after Hello World!). First attempt Failed: pre-MMU .bss store lost on the test image. Measurement only. Not a budget. Not a bench.
qemu-smoke requires boot-delta stringscripts/qemu-smoke.sh hello phase greps perf: boot-deltaVerified2026-09-11 cloud: qemu-smoke: boot-delta perf string present.
User TTBR0 omits kernel .data#[test_case] user_ttbr0_omits_kernel_data; walk L1_USERVerified2026-09-11 cloud cargo test: user_ttbr0_omits_kernel_data [ok]. ASID=1 programmed. Not ASID isolation.
EL0 cannot read kernel .dataHello-kernel serial el0: no kernel read; #[test_case] el0_cannot_read_kernel_dataVerified2026-09-11 cloud qemu-smoke: after el0: nx kernel, serial el0: no kernel read then el0: ok. cargo test [ok]. User TTBR0 omits .data. is_active() stays false. Not isolation. Not PAN.
PAN on virt cortex-a57ID_AA64MMFR1_EL1.PAN != 0 and an EL1-vs-EL0 access faultPlannedARMv8.0 cortex-a57. Do not claim PAN.
ASID isolationHello-kernel serial asid: dual + asid: conflict + asid: ok; #[test_case] asid_isolation_without_vmalle1; no TLBI VMALLE1 on the switchVerified2026-09-11 cloud qemu-smoke (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e, -cpu cortex-a57): after el0: ok, serial asid: dual then asid: conflict then asid: ok. cargo test asid_isolation_without_vmalle1 [ok] (Running 39 tests). Dual TTBR0 (ASID 1 vs 2) with nG pages; switch is MSR TTBR0 + ISB. EL0 trampoline still TLBI VMALLE1 (.data leaves are global). Not “EL0 isolated.” Not PAN.
Standing EL0 contextHello-kernel serial el0: standing + el0: restored; #[test_case] standing_el0_enter_leave; is_active() true only while standingVerified2026-09-11 cloud qemu-smoke (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e, -cpu cortex-a57): after el0: no kernel read, serial el0: standing then el0: restored then el0: ok. cargo test standing_el0_enter_leave [ok] (Running 42 tests). Dual-SVC on user TTBR0 (not a trampoline flag). Lower-EL IRQ still parks. Not POSIX. Not isolation.
TTBR1 kernel-private page (ADR-016 first cut)Hello-kernel serial ttbr1: el1 + ttbr1: no el0 + ttbr1: ok; #[test_case] ttbr1_el1_sees_priv_el0_does_notVerified2026-09-11 cloud qemu-smoke: after asid: ok, serial ttbr1: el1 then ttbr1: no el0 then ttbr1: ok. cargo test ttbr1_el1_sees_priv_el0_does_not [ok]. EL1-only high page. Identity teardown Planned. Not a relocated kernel. Not “EL0 isolated.”
qemu-smoke requires standing EL0 stringsscripts/qemu-smoke.sh hello phase greps el0: standing / el0: restoredVerified2026-09-11 cloud: qemu-smoke: EL0 first-mile + read-mile + standing strings present.
qemu-smoke requires TTBR1 stringsscripts/qemu-smoke.sh hello phase greps ttbr1: ok / ttbr1: el1 / ttbr1: no el0; rejects ttbr1: leaked and ttbr1: probe missedVerified2026-09-11 cloud: qemu-smoke: TTBR1 private-page strings present.
EL1 fetch from TTBR1 high VA (ADR-017)Hello-kernel serial ttbr1: el1 exec + ttbr1: vbar; #[test_case] el1_executes_from_ttbr1_high_va + high_alias_maps_kernel_textVerified2026-09-11 cloud qemu-smoke (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e, -cpu cortex-a57): after asid: ok, serial ttbr1: el1 exec then ttbr1: vbar then the ADR-016 private-page lines. cargo test both new cases [ok] (Running 44 tests). _start stays at 0x4008_0000. Nested fatal ELR can be the high alias (elr=0xffffff80400839e4). Not a relocated kernel. Not “EL0 isolated.”
qemu-smoke requires TTBR1 high-VA exec stringsscripts/qemu-smoke.sh hello phase greps ttbr1: el1 exec / ttbr1: vbar; rejects ttbr1: exec missedVerified2026-09-11 cloud: qemu-smoke: TTBR1 private-page + high-VA exec strings present.
Full higher-half / identity teardownIdentity .rodata/.data/heap unmapped after those accesses are proven high-onlyPlannedADR-020 tears live identity .text after a vtable rewrite (separate Verified row). _start stays at 0x4008_0000. Do not claim the kernel moved.
Identity .rodata / .data / heap tearThose identity ranges unmapped; accesses proven high-onlyPlannedAfter live .text. Not started on main e80dc93.
Docs site at https://ctos.artof.linkHTTP(S) fetch of the published Pages site on that hostVerified2026-09-11 after #30: curl -sL https://ctos.artof.link/ → HTTP 200; HTML title/menu ctos (ctsOS); landing #driving-principles. This VM repeat: same 200 + title + heading. Also 200: overview/what-can-run.html, overview/porting.html, framework/honesty-ledger.html. Deploy cited on the published-site row. Not a kernel boot claim.
High-VA vtable rewrite + live identity .text tear (ADR-020)Hello-kernel serial ident: reloc + ident: live + existing ident: ok; #[test_case] identity_fn_ptrs_rewritten_high + live_identity_text_unmapped_boot_stub_staysVerified2026-09-11 cloud qemu-smoke (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e, -cpu cortex-a57): ident: jump then ident: reloc n=12 then ident: range lo=0x400ae000 hi=0x400b2000 pages=4 then ident: live lo=0x40081000 hi=0x400a6000 pages=37 then Hello World! then later ident: split / ident: fault / ident: high / ident: text / ident: no el0 / ident: ok. Hello BRK ELRs high (elr=0xffffff8040082990). cargo test Running 50 tests all [ok] (ident: reloc n=74, ident: live pages=45). Not a relocated kernel. Not “EL0 isolated.” .rodata/.data/heap stay. Full teardown Planned.
Identity text range tear (ADR-019)Hello-kernel serial ident: jump + ident: range + ident: text + existing ident: ok; #[test_case] identity_text_range_unmapped_boot_stub_staysVerified2026-09-11 cloud qemu-smoke (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e, -cpu cortex-a57): ident: jump then ident: range lo=0x400ac000 hi=0x400b0000 pages=4 then later ident: split / ident: fault / ident: high / ident: text / ident: no el0 / ident: ok. Hello BRK ELRs high (elr=0xffffff8040082758). cargo test identity_text_range_unmapped_boot_stub_stays [ok] (Running 48 tests). Live .text stays. First attempt to yank live .text after the stub Failed (unhandled sync on println! — rustc dyn Write vtables). Not a relocated kernel. Not “EL0 isolated.” Full teardown Planned.
Identity-tear first cut (ADR-018)Hello-kernel serial ident: split + ident: fault + ident: high + ident: no el0 + ident: ok; #[test_case] high_ram_tables_are_independent + identity_tear_page_unmapped_high_stays + identity_tear_el1_faults_high_staysVerified2026-09-11 cloud qemu-smoke (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e, -cpu cortex-a57): after ttbr1: ok, serial ident: split then ident: fault then ident: high then ident: no el0 then ident: ok. cargo test all three new cases [ok] (Running 47 tests). _start stays at 0x4008_0000. Nested fatal ELR can be the high alias (elr=0xffffff8040083bec). Not a relocated kernel. Not “EL0 isolated.” Full teardown Planned.
qemu-smoke requires identity-tear stringsscripts/qemu-smoke.sh hello phase greps ident: ok / ident: split / ident: fault / ident: high / ident: no el0; rejects ident: leaked and ident: probe missedVerified2026-09-11 cloud: qemu-smoke: identity-tear strings present.
Obsidian open-vault checklistRead research/obsidian-checklist.md; .gitignore has .obsidian/VerifiedStructure only. .obsidian/ not committed. Not a claim that Obsidian is installed or synced.
CI on GitHub (pillars PR / ADR-011).github/workflows/smoke.yml on cursor/pillars-adr-011-325fVerifiedCloud-probe commit cdf75eb: push 34396574483 and PR 34396590545 success. Ledger follow-up d716599: push 34396774022 and PR 34396779239 success. Same qemu-smoke.sh (hello + paging + heap + sched + CNTPCT + tick + RX + BRK + fatal + 20 tests + force-fail).
scripts/qemu-smoke.sh (pillars follow-up / #18)Ran on that cloud VMVerified2026-09-10 (QEMU 8.2.2, rustc 1.100.0-nightly a36d05efa): hello (timeout 124) with paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, perf: cntpct delta=22968, timer: tick, perf: irq-delta min=12973 max=35391 spread=22418 n=8, input: rx 0x41, two exception: sync BRK, exception: fatal nested / kind=0x200. cargo test Running 25 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker.
scripts/qemu-smoke.sh (pillar deepen / this branch)Ran on this cloud VMVerified2026-09-10 (QEMU 8.2.2, rustc 1.100.0-nightly a36d05efa): host perf: elf-size bytes=3752056; hello (timeout 124) with paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, guard: fault / guard: ok, el0: svc / el0: nx kernel / el0: ok, perf: cntpct delta=20592, timer: tick, perf: irq-delta min=14477 max=24149 spread=9672 n=8, input: rx 0x41, two exception: sync BRK, exception: fatal nested / kind=0x200. cargo test Running 29 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker. GHA on this PR is a separate row.
CI on GitHub (pillars follow-up PR / #18).github/workflows/smoke.yml on cursor/pillars-sec-perf-el0-9bc0VerifiedCloud-probe commit 8ae63bb: push 34541457222 and PR 34541473721 success (ubuntu-24.04 + ubuntu-24.04-arm). Ledger follow-up 4c610a2: push 34541534491 and PR 34541537733 success. Same qemu-smoke.sh (hello + paging + heap + sched + W^X + CNTPCT + IRQ-delta + tick + RX + BRK + fatal + 25 tests + force-fail).
CI on GitHub (pillar deepen PR / #19).github/workflows/smoke.yml on cursor/pillar-loops-deepen-48aeUnknownNo green GHA URL on the cloud-probe commit recorded here. Merged as #19.
scripts/qemu-smoke.sh (RO+NX / boot-delta / user TTBR0 / PR #20)Ran on that cloud VMVerified2026-09-11 (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e): host perf: elf-size bytes=3785520; hello (timeout 124) with paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, guard: fault / guard: ok, ro: nx data / ro: write fault / ro: ok, el0: svc / el0: nx kernel / el0: no kernel read / el0: ok, perf: cntpct delta=20506, perf: boot-delta ticks=111019, timer: tick, perf: irq-delta min=4885 max=26301 spread=21416 n=8, input: rx 0x41, two exception: sync BRK, exception: fatal nested / kind=0x200. cargo test Running 36 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker. GHA on that PR is a separate row.
CI on GitHub (RO+NX / boot-delta / user TTBR0 / PR #20).github/workflows/smoke.yml on cursor/pillar-deepen-ro-boot-el0-aadcVerifiedCloud-probe commit 6606be8: push 34561716967 and PR 34561719542 success (ubuntu-24.04 + ubuntu-24.04-arm). Host ELF there ~3.69 MiB — smaller than cts-ai Docker 3786384. Do not copy that Verified onto the Docker host.
Docker smoke on main b2bbb99 (post PR #20)cts-ai Docker Desktop linux/arm64 ./scripts/docker-smoke.sh after sync to b2bbb99FailedSponsor serial: perf: elf-size bytes=3786384; Hello World!; paging: probe missed; heap: probe missed; sched: probe missed; wx: probe missed; guard: fault / guard: ok; ro: nx data / ro: write fault / ro: ok; el0: probe missed; then CNTPCT / boot-delta / tick / irq-delta / RX / BRK / fatal still printed. qemu-smoke: missing 'paging: ok' EXIT 1. Guard/ro are current-EL identity walks — they do not need frame::ALLOC or USER_MAP_OK. Same class as the PR #20 pre-MMU .bss miss, plus a single shared L3 / first-2-MiB user map that a larger nightly layout can also break. Antifragility: this host found what GHA missed. Not this cloud VM.
scripts/qemu-smoke.sh (layout L3 pool / post-MMU frame init / this branch)Ran on this cloud VMVerified2026-09-11 (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e): host perf: elf-size bytes=3837736; hello (timeout 124) with paging: layout data=0x40201000 end=0x4023c000 pool=0x4023c000 user=1, paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, guard: fault / guard: ok, ro: nx data / ro: write fault / ro: ok, el0: svc / el0: nx kernel / el0: no kernel read / el0: ok, perf: cntpct delta=21094, perf: boot-delta ticks=115251, timer: tick, perf: irq-delta min=12053 max=24899 spread=12846 n=8, input: rx 0x41, two exception: sync BRK, exception: fatal nested / kind=0x200. cargo test Running 38 tests all [ok] (adds layout_stress_crosses_first_ram_l2 + frame_allocator_ready_after_mmu). force-fail exit 1. qemu-smoke: ok. Loaded layout larger than cts-ai Docker’s 3.78 MiB class (__data_start in the second RAM 2 MiB). Not Docker. GHA on this PR is a separate row.
CI on GitHub (layout L3 pool / PR #21).github/workflows/smoke.yml on cursor/layout-l3-user-map-c915VerifiedCloud-probe commit c04b84b: push 34563006005 and PR 34563008516 success (ubuntu-24.04 + ubuntu-24.04-arm). Merge-commit push 34563104020 success on 71ee15f. MRC grepped both-matrix serial: paging: layout data=0x40201000 … user=1, paging: ok, heap: ok, sched: ok, wx: ok, el0: ok, 38 tests, force-fail fail-closed. Do not copy that Verified onto cts-ai Docker.
Docker smoke after the layout fix (main 71ee15f)cts-ai Docker Desktop linux/arm64 ./scripts/docker-smoke.sh after #21 mergeVerifiedSponsor re-run on 71ee15f: perf: elf-size bytes=3838592; Hello World!; paging: layout data=0x40201000 end=0x4023c000 pool=0x4023c000 user=1; paging: ok; heap: ok; sched: ok; wx: ok; guard: ok; ro: ok; el0: svc / el0: nx kernel / el0: no kernel read / el0: ok; perf: cntpct …; perf: boot-delta ticks=217103; perf: irq-delta …; input: rx 0x41; exception: sync BRK … / fatal nested …; Running 38 tests all [ok]; force-fail exit 1; qemu-smoke: ok; EXIT 0. Keep the b2bbb99 Failed row (antifragility history). Not this cloud VM.
qemu-smoke requires ASID isolation stringsscripts/qemu-smoke.sh hello phase greps asid: ok / asid: dual / asid: conflict; rejects asid: stale and asid: probe missedVerified2026-09-11 cloud: qemu-smoke: ASID isolation strings present.
scripts/qemu-smoke.sh (ASID isolation / this branch)Ran on this cloud VMVerified2026-09-11 (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e): host perf: elf-size bytes=3853144; hello (timeout 124) with paging: layout data=0x40201000 end=0x4023f000 pool=0x4023f000 user=1, paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, guard: fault / guard: ok, ro: nx data / ro: write fault / ro: ok, el0: svc / el0: nx kernel / el0: no kernel read / el0: ok, asid: dual / asid: conflict / asid: ok, perf: cntpct delta=21093, perf: boot-delta ticks=115983, timer: tick, perf: irq-delta min=9499 max=29721 spread=20222 n=8, input: rx 0x41, two exception: sync BRK, exception: fatal nested / kind=0x200. cargo test Running 39 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker. GHA on this PR is a separate row.
CI on GitHub (ASID isolation / PR #23).github/workflows/smoke.yml on cursor/el0-asid-isolation-f98dUnknownNo green GHA URL on the cloud-probe commit recorded here. Merged as #23 (fb050b7).
scripts/qemu-smoke.sh (standing EL0 + TTBR1 / this branch)Ran on this cloud VMVerified2026-09-11 (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e): host perf: elf-size bytes=3872120; hello (timeout 124) with paging: layout data=0x40201000 end=0x40243000 pool=0x40243000 user=1, paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, guard: fault / guard: ok, ro: nx data / ro: write fault / ro: ok, el0: svc / el0: nx kernel / el0: no kernel read / el0: standing / el0: restored / el0: ok, asid: dual / asid: conflict / asid: ok, ttbr1: el1 / ttbr1: no el0 / ttbr1: ok, perf: cntpct delta=23782, perf: boot-delta ticks=127971, timer: tick, perf: irq-delta min=11604 max=27616 spread=16012 n=8, input: rx 0x41, two exception: sync BRK, exception: fatal nested / kind=0x200. cargo test Running 42 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker. GHA on this PR is a separate row.
CI on GitHub (standing EL0 + TTBR1 / PR #24).github/workflows/smoke.yml on cursor/el0-standing-ttbr1-5ce0UnknownNo green GHA URL on the cloud-probe commit recorded here. Merged as #24 (8745939).
scripts/qemu-smoke.sh (TTBR1 high-VA exec / this branch)Ran on this cloud VMVerified2026-09-11 (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e): host perf: elf-size bytes=3877712; hello (timeout 124) with paging: layout data=0x40201000 end=0x40243000 pool=0x40243000 user=1, paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, guard: fault / guard: ok, ro: nx data / ro: write fault / ro: ok, el0: svc / el0: nx kernel / el0: no kernel read / el0: standing / el0: restored / el0: ok, asid: dual / asid: conflict / asid: ok, ttbr1: el1 exec / ttbr1: vbar / ttbr1: el1 / ttbr1: no el0 / ttbr1: ok, perf: cntpct delta=20622, perf: boot-delta ticks=117383, timer: tick, perf: irq-delta min=11983 max=21932 spread=9949 n=8, input: rx 0x41, two exception: sync BRK, exception: fatal nested / kind=0x200 (fatal elr=0xffffff80400839e4). cargo test Running 44 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker. GHA on this PR is a separate row.
CI on GitHub (TTBR1 high-VA exec / PR #25).github/workflows/smoke.yml on cursor/ttbr1-high-el1-exec-136cUnknownNo green GHA URL on the cloud-probe commit recorded here. Merged as #25 (008f7c7).
scripts/qemu-smoke.sh (identity-tear first cut / this branch)Ran on this cloud VMVerified2026-09-11 (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e): host perf: elf-size bytes=3895632; hello (timeout 124) with paging: layout data=0x40201000 end=0x40255000 pool=0x40255000 user=1, paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, guard: fault / guard: ok, ro: nx data / ro: write fault / ro: ok, el0: svc / el0: nx kernel / el0: no kernel read / el0: standing / el0: restored / el0: ok, asid: dual / asid: conflict / asid: ok, ttbr1: el1 exec / ttbr1: vbar / ttbr1: el1 / ttbr1: no el0 / ttbr1: ok, ident: split / ident: fault / ident: high / ident: no el0 / ident: ok, perf: cntpct delta=25355, perf: boot-delta ticks=130631, timer: tick, perf: irq-delta min=14217 max=34342 spread=20125 n=8, input: rx 0x41, two exception: sync BRK, exception: fatal nested / kind=0x200 (fatal elr=0xffffff8040083bec). cargo test Running 47 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker. GHA on this PR is a separate row.
CI on GitHub (identity-tear first cut / PR #26).github/workflows/smoke.yml on cursor/identity-teardown-c0fdUnknownNo green GHA URL on the cloud-probe commit recorded here. Merged as #26 (b0f0ee5).
Docker smoke on main b0f0ee5 (post PR #26 / ADR-018)cts-ai Docker Desktop linux/arm64 ./scripts/docker-smoke.sh after #26 mergeVerifiedSponsor re-run on b0f0ee5: 47 tests, ident: ok, fatal nested ELR high alias. Not this cloud VM. Keep prior Failed b2bbb99 row (antifragility history).
qemu-smoke requires identity .text range-tear stringsscripts/qemu-smoke.sh hello phase greps ident: jump / ident: range / ident: text; rejects ident: range missedVerified2026-09-11 cloud: qemu-smoke: identity-tear strings present (includes jump/range/text).
scripts/qemu-smoke.sh (identity text range tear / this branch)Ran on this cloud VMVerified2026-09-11 3e710e1 (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e): host perf: elf-size bytes=3905712; hello (timeout 124) with ident: jump, ident: range lo=0x400ac000 hi=0x400b0000 pages=4, paging: layout data=0x40201000 end=0x40255000 pool=0x40255000 user=1, paging: ok, heap: ok, sched: ok, wx: ok, guard: ok, ro: ok, el0: ok, asid: ok, ttbr1: ok, ident: split / ident: fault / ident: high / ident: text / ident: no el0 / ident: ok, perf: cntpct delta=20293, perf: boot-delta ticks=92466, timer: tick, perf: irq-delta min=12711 max=26328 spread=13617 n=8, input: rx 0x41, two exception: sync BRK (ELRs 0xffffff8040082758 / 0xffffff8040082730), exception: fatal nested / kind=0x200 (fatal elr=0xffffff8040083bf4). cargo test Running 48 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker. GHA on this PR is a separate row.
CI on GitHub (identity text range tear / PR #27 ac3ad0b).github/workflows/smoke.yml on cursor/identity-tear-text-range-ea15 @ ac3ad0bFailedBoth matrices: hello printed ident: jump / ident: range then ident: probe missed (init still required a 4 KiB __ident_tear_*; ADR-019 is 16 KiB). Keep this Failed row.
CI on GitHub (identity text range tear / main 24d94e6).github/workflows/smoke.yml merge-commit push on 24d94e6VerifiedPush 34640667227 success (ubuntu-24.04 + ubuntu-24.04-arm). Grepped both jobs: hello ident: jump / ident: range lo=0x400ac000 hi=0x400b0000 pages=4 / ident: text / ident: ok; BRK/fatal ELRs high; Running 48 tests; force-fail exit 1; qemu-smoke: ok.
Docker smoke on main 24d94e6 (post PR #27 / ADR-019)cts-ai Docker Desktop linux/arm64 ./scripts/docker-smoke.sh after #27 mergeVerifiedSponsor re-run on 24d94e6: ident: jump, 16 KiB range tear, ident: ok, BRK/fatal on high ELRs, 48 tests + force-fail. Not this cloud VM. Keep the b2bbb99 Failed row.
qemu-smoke requires identity reloc + live .text stringsscripts/qemu-smoke.sh hello phase greps ident: reloc / ident: live; rejects ident: reloc missed / ident: live missedVerified2026-09-11 cloud: qemu-smoke: identity-tear strings present (includes reloc/live).
scripts/qemu-smoke.sh (ADR-020 live .text tear / this branch)Ran on this cloud VMVerified2026-09-11 0fda690 (QEMU 8.2.2, rustc 1.100.0-nightly 67eda617e): host perf: elf-size bytes=3988408; hello (timeout 124) with ident: jump, ident: reloc n=12, ident: range lo=0x400ae000 hi=0x400b2000 pages=4, ident: live lo=0x40081000 hi=0x400a6000 pages=37, paging: layout data=0x40201000 end=0x40255000 pool=0x40255000 user=1, paging: ok, heap: ok, sched: ok, wx: nx heap / wx: ok, guard: fault / guard: ok, ro: nx data / ro: write fault / ro: ok, el0: svc / el0: nx kernel / el0: no kernel read / el0: standing / el0: restored / el0: ok, asid: dual / asid: conflict / asid: ok, ttbr1: el1 exec / ttbr1: vbar / ttbr1: el1 / ttbr1: no el0 / ttbr1: ok, ident: split / ident: fault / ident: high / ident: text / ident: no el0 / ident: ok, perf: cntpct delta=21002, perf: boot-delta ticks=94258, timer: tick, perf: irq-delta min=10686 max=27025 spread=16339 n=8, input: rx 0x41, two exception: sync BRK (ELRs 0xffffff8040082990 / 0xffffff8040082968), exception: fatal nested / kind=0x200 (fatal elr=0xffffff8040083e2c). cargo test Running 50 tests all [ok]. force-fail exit 1. qemu-smoke: ok. Not Docker. GHA on this PR is a separate row.
CI on GitHub (ADR-020 live .text tear / this branch).github/workflows/smoke.yml on cursor/adr-020-live-text-reloc-322cUnknownNo green GHA URL on the cloud-probe commit recorded here. Merged as #28 (e80dc93).
CI on GitHub (ADR-020 live .text tear / main e80dc93).github/workflows/smoke.yml merge-commit push on e80dc93VerifiedPush 34651404108 success (ubuntu-24.04 + ubuntu-24.04-arm). Grepped both jobs: hello ident: jump / ident: reloc n=12 / ident: live lo=0x40081000 hi=0x400a6000 pages=37 / ident: ok; Hello World! after the tear; Running 50 tests; force-fail exit 1; qemu-smoke: ok.
Docker smoke on main e80dc93 (post PR #28 / ADR-020)cts-ai Docker Desktop linux/arm64 ./scripts/docker-smoke.sh after #28 mergeVerifiedSponsor re-run on e80dc93: 50 tests, ident: reloc n=12, live pages=37, force-fail ok. Not this cloud VM. Keep the earlier 24d94e6 Docker Verified row and the b2bbb99 Failed row.
mdBook docs site builds locally./scripts/docs-build.sh (mdBook 0.5.4 + mdbook-mermaid 0.17.1) writes book/ and book/CNAME is ctos.artof.linkVerified2026-09-11 this cloud VM (plain-English / mermaid branch): mdbook v0.5.4; mdbook-mermaid 0.17.1; docs-build: ok; mermaid class="mermaid" nodes on landing + overview + pillars. Generator only. Live URL is a separate row.
Pages workflow file existsRead .github/workflows/pages.ymlVerifiedWorkflow present: PR builds the book; main uploads + actions/deploy-pages. Not a live URL.
Pages workflow builds this PR.github/workflows/pages.yml on cursor/docs-pages-site-c371VerifiedPR run 34651704287 success (mdBook build 7s). Deploy job skipped on pull_request (expected). Not a live site.
Pages workflow builds this PR (plain-English / mermaid).github/workflows/pages.yml on cursor/docs-plain-english-diagrams-e4a9VerifiedPR run 34654325467 success (mdBook build 5s). Deploy job skipped on pull_request (expected). Not a new live-site deploy.
Docs website published (GitHub Pages)Green pages workflow on main and an HTTPS fetch of https://ctos.artof.linkVerified2026-09-11 this cloud VM after #30: main deploy 34653046584 success (mdBook build + Deploy GitHub Pages). curl -sSI https://ctos.artof.link HTTP 200; body includes Driving principles. https://artofdream.github.io/ctos (no trailing slash) 301s to the custom domain; .../ctos/ 404’d — not a second live tree.
Public CNAME ctos.artof.linkartofdream.github.iodig CNAME ctos.artof.link +shortVerified2026-09-11 this cloud VM (repeat): artofdream.github.io. DNS is in place at the public resolver. Serving the book is the Verified docs-site / reachability rows.
Route 53 ctos CNAME in account 737290977112 zone Z1178AFMV41RWPSponsor stated CREATE done (zone Z1178AFMV41RWP); API list-resource-record-sets as that accountUnknownThis environment still has no AWS CLI / credentials. Sponsor named the zone and said the record exists. Do not CREATE again. Public dig is the Verified row above, not this API probe.
Custom domain ctos.artof.link reachabilityPages Settings/API lists the hostname and curl -sSI https://ctos.artof.link is HTTP 200 with a cert for that nameVerified2026-09-11 this cloud VM: Pages API cname=ctos.artof.link, https_enforced=true, cert state=approved (expires 2026-12-10). HTTPS 200; TLS CN=ctos.artof.link (Let’s Encrypt). Route 53 API LIST stays the Unknown row above.
Guest filesystem (VFS / memfs / virtio-blk / FAT)Serial + #[test_case] that do not exist; src/ has no FS stackPlannedSite: docs/overview/filesystem.md. Extra stance: filesystem.md. Order Planned: memfs → virtio-blk → FAT or xv6-like. Do not say “supports FAT.” File presence is not a guest open.
Guest runs host apps (Linux ELF / shell / Python)No exec, libc, or app-load serial markerPlannedSite: docs/overview/hosting-apps.md. Extra: host-apps.md. Today: in-tree samples only.
Guest is a container host (OCI / Docker / k8s)Source: no OCI, runc, cgroup, or namespace code in src/VerifiedAbsence + non-goal. Site: hosting-apps.md. Host docker-smoke.sh is a build harness, not a guest runtime. Not a later Planned feature.
Product “immutable OS”Marketing sentence vs immutability.mdPlannedNon-claim. Absolute immutability is incompatible (heap/PTEs/devices mutate). Scoped RO is ADR-015 / ADR-020. Site: advantages.md. File presence is not Verified.
OS image vs app payloads disconnected (A9)Two artifacts + load path + cross-update probe (app on OS n and n+1)PlannedAfter Track A ABI/loader (#31 A1–A4). Issue A9 #48. Today: one linked ELF — not Verified.
A9 slot-disconnect performance deltaperf: boot-delta plus a new app-load CNTPCT marker on a two-artifact bootPlannedExpected costs: boot/load, SVC, ASID/TTBR, optional COW. Neutral/win: steady EL0 compute; smaller OS updates operational. No Verified delta — still one ELF. Do not invent a bench. performance.md.

How to update

  1. Run or cite the probe (command, file path + revision, or gh run URL).
  2. Change only the rows you probed.
  3. If you could not run QEMU, leave boot Unknown and say so in the PR. A prior Verified row is one environment and one boot path; do not copy the 2026-09-08 x86 VGA probe forward.

Three pillars

After the cooperative scheduler (M9), ctos treats antifragility, security, and performance as first-class pillars (ADR-011). In everyday words: we turn repeated misses into sensors, we prove security slices instead of saying “secure OS,” and we measure before we tune. They share one rule: a status word needs a probe (honesty ledger).

Principles drive; pillars sit under them; tracks sit under both. Visitor-facing list: landing — Driving principles. In-repo list: principles.md. This page is the deep hub, not a second marketing copy. Extra stance: overview.md, apps-today.md, building-or-porting.md, immutability.md.

flowchart TD
  P["Core principles<br/>honesty · antifragility · security<br/>performance · document-first"]
  L["Three pillars — this page<br/>NFR-05 · NFR-10 · NFR-07"]
  T["Tracks A / B<br/>subordinate workstreams"]
  P --> L --> T

Tracks (loader/ABI, later slots/FS) do not outrank a principle.

PillarFrozen IDHomeWhat “done” looks like
AntifragilityNFR-05antifragility.mdRepeated failures become sensors/gates, not extra README advice
SecurityNFR-10security.mdThreat-model v1.8 + probes before any “secure OS” claim. EL0 miles: el0.md. On main e80dc93: standing + ASID + TTBR1 first cut + EL1 high-VA fetch + identity-tear first cut + 16 KiB range + live .text tear (ADR-020) are Verified. Still Planned: .rodata/.data/heap tear, PAN on cortex-a57, umbrella isolation.
PerformanceNFR-07performance.mdMeasurable CNTPCT + IRQ-delta + host ELF size + boot-delta; optimize only with a probe

Bring-up M0–M9 stays on the roadmap. Pillar work after M9 is listed there as a separate section so a docs PR does not pretend to close paging or a scheduler.

Immutability is the same rule: scoped RO+NX / text-tear probes are fine; absolute “immutable OS” is not. Overview: Advantages — Immutability.

Do not import florist / AEA role names. These pillars are ctos-native.

Antifragility SOP

First-class pillar (ADR-011, NFR-05). Hub: pillars.md.

When the same failure happens twice, strengthen the strongest layer (a sensor or a gate), not another paragraph of advice.

Fail closed

  • Unprobed boot → Unknown, not Verified.
  • Missing nightly / rust-src / QEMU → say the environment blocked the probe; do not invent success.
  • MRC / author conflict → do not merge.

Ratchet

  1. Name the failure once in research/random-thoughts/ (what broke, command, error).
  2. If it repeats, add a sensor: a script, a #[test_case], a CI check, or a ledger row with a real probe command.
  3. If people keep skipping the sensor, add a gate: CI required, or MRC refuses the PR.
  4. Only then tighten a guide (AGENTS.md or a skill). Guides without sensors rot.

Recent ratchets (keep Failed history)

These are history rows, not a claim that the current tip is broken.

  • cts-ai Docker on b2bbb99 Failed after PR #20 (paging: probe missed / heap: probe missed / sched: probe missed / el0: probe missed while guard/RO still printed). Same class as the PR #20 pre-MMU .bss miss plus a single shared L3 / first-2-MiB user map. Sensor: layout L3 pool + post-MMU frame init (#21). Docker on 71ee15f then Verified. Keep the Failed row in the honesty ledger.
  • Live identity .text yank Failed (unhandled sync on first println! — rustc dyn Write vtables are identity fn pointers). Sensor / cut: ADR-019 kept live .text and tore a 16 KiB dedicated range. ADR-020 then rewrote those vtables to high aliases and tore live .text. cts-ai Docker + GHA on e80dc93 Verified that cut (ident: reloc n=12, live pages=37, 50 tests). Keep the Failed first yank.
  • PR #27 ac3ad0b GHA Failed (ident: range then ident: probe missed — init still required a 4 KiB tear page). Sensor: 16 KiB range + relaxed init. Merge 24d94e6 GHA 34640667227 Verified. Keep the Failed SHA.

No self-approval / no self-merge

The author does not APPROVE or merge their own PR. Same GitHub login is not a second identity. MRC writes COMMENT and names author / reviewer / merger. Merge hat is the other identity (artofdream vs cursor[bot]). See ADR-002 and .cursor/skills/ctos-mr-coordinator/SKILL.md. Do not enable GitHub author self-APPROVE to make the gate “count.”

Security — threat model v1.8 (NFR-10 / ADR-011 / ADR-012 / ADR-013 / ADR-014 / ADR-015 / ADR-016 / ADR-017 / ADR-018 / ADR-019 / ADR-020)

This is a written threat model for a QEMU virt learning kernel. It is not a certification, not an audit, and not a “secure OS” / “hardened” claim. File presence is not W^X. Image W^X is a separate ledger row that needs a QEMU probe.

Version: v1.8 (2026-09-11). Slice/update of v1.7 (adds high-VA vtable / fn-pointer rewrite + live identity .text tear after the boot stub; .rodata/.data/heap stay; PAN and full identity teardown still Planned). Not a v2 model and not “secure.”

Scope

In: the ctos guest as built for qemu-system-aarch64 -machine virt (EL1, identity map + TTBR1 private page + TTBR1 RAM alias for EL1 fetch, PL011, GICv2, CNTP, first-fit heap, cooperative EL1 tasks, a deliberate EL0 first mile and a bounded standing context). The git repo (no secrets).

Out: Raspberry Pi or other boards, a second ISA, networking, multi-tenant hosting, secure / measured boot, physical side channels, a hostile hypervisor.

QEMU and the host are the TCB we do not defend against. If the emulator or the CI runner is hostile, the guest cannot recover.

Assets

AssetWhy it mattersWhere it lives
Kernel integrityText, VBAR_EL1 table, page tables. Corruption ends the learning probe.EL1 identity map (0x4008_0000__kernel_end) plus TTBR1 RAM alias / high VBAR (ADR-017)
Secrets-noneCredentials in git would be a host/CI incident, not a guest exploit.Repo policy (NFR-10). The guest stores no keys.
Availability of the virt guestSmoke / cargo test must still print markers and exit. A silent lockup hides bugs.UART, GIC, timer, fail-closed scripts/qemu-smoke.sh
Heap + cooperative stacksWritable RAM used by Box/Vec and M9 workers. Execute-from-here is the W^X question.Frame pool after __kernel_end (ADR-012)
Linker stacks (SP_EL0 / SP_EL1 / fatal)Thread, first-level exception, and nested-fatal stacks. Live pages are RW+NX with .data (ADR-015).Linker holes after .bss (ADR-005)
Guard pages (when mapped invalid)4 KiB unmapped holes below each linker stack. Downward overflow should become a translation fault, not a silent smash of the previous image bytes.__stack_guard / __exc_stack_guard / __fatal_stack_guard (ADR-014)
EL0 trampoline + bait + standing contextOne UXN-clear map-window page and a kernel .data bait. Proves enter/return, “cannot execute kernel data,” “cannot read kernel .data,” and a bounded standing dual-SVC. Not POSIX, not isolation.paging::EL0_PAGE + KERNEL_DATA_BAIT + L1_USER (ADR-013)
TTBR1 private pageOne EL1-only high page. Proves EL0 cannot load it. Not a relocated kernel.paging::TTBR1_PRIV (ADR-016)
TTBR1 RAM alias / high VBAREL1 can fetch .text at identity + TTBR1_BASE. VBAR_EL1 is that alias. Identity -kernel stub stays.L1_HIGH[1] → cloned L2_HIGH_RAM (ADR-017, ADR-018)
Identity-tear pageOne dedicated identity text page unmapped from TTBR0. High twin stays. Not a relocated kernel.__ident_tear_* (ADR-018)
Identity text rangeDedicated 16 KiB identity text range unmapped. High twins stay. Not a relocated kernel.__ident_tear_* 16 KiB (ADR-019)
Live identity .textrustc vtables rewritten to high aliases; live identity .text after _start unmapped. High twins stay. .rodata/.data/heap stay. Not a relocated kernel.[0x4008_1000, __text_end) (ADR-020)
Remaining isolation gapsShared boot-stub text in the user table (handler must fetch if VBAR were still identity). No PAN on cortex-a57. EL0 trampoline still TLBI VMALLE1 (.data leaves are global). Identity .rodata/.data/heap still live. Lower-EL IRQ still parks.User TTBR0 + ASID + TTBR1 first cut + exec mile + torn live .text; isolation Planned
Console / sensorsPL011 is how we see whether a probe ran.Device MMIO 0x0900_0000

Adversaries

AdversaryIn scope?Notes
Buggy kernel code (wrong store, bad unsafe, execute-from-heap, stack smash)YesPrimary adversary today. Mitigate with PXN on heap/frames, unmapped linker-stack guards, minimize unsafe (NFR-01), fail-closed smoke.
Malicious EL0 (standing or trampoline task executing kernel data or escalating via a bad map)Named; standing context is boundedFirst mile + user-TTBR0 read mile + ASID TLB mile + standing dual-SVC + TTBR1 private page + EL1 high-VA fetch exist. That is not “EL0 isolated” (kernel text still in the user table, no PAN, identity map still live, EL0 path still full-TLBI, lower-EL IRQ still parks).
Compromised device treeMostly outM7 does not walk FDT. DTB sits at RAM base below the image. A hostile DTB is a QEMU/host problem until a walker exists; then it becomes an input-validation ADR.
DMA / virtio devicesOutThe guest does not program a DMA master. UART/GIC/timer are MMIO. A malicious virtio device is future surface.
Hostile QEMU or CI hostOutHypervisor / runner is trusted. Repo-secret leak is a host control (.gitignore, review), not a guest mitigation.
Network attackerOutNo stack.

Trust boundaries

[ host / QEMU / CI ]  --trusted config--  [ virt guest ]
                                              |
                                         EL1 kernel  ← TCB today
                                              |
                                         EL0 first mile + standing  ← entered / stood / left
                                              |                       (not a POSIX user)
                                         EL0 isolation    ← Planned (PAN / full higher-half)
  • EL1 now. Page tables distinguish execute (RO+X text vs RW+NX data/stacks/heap vs Device XN), write (AP[2] on text), and presence (guard holes).
  • EL0 first mile + user TTBR0 + ASID TLB + standing + TTBR1 first cut + EL1 high-VA fetch + identity-tear first cut + identity .text range tear + live .text tear, isolation Planned. Lower-EL AArch64 sync is taken (SVC / IABORT / DABORT). IRQ/FIQ/SError lower-EL slots still park. Dual ASID without TLBI VMALLE1 is a probed mile (asid: ok). Standing dual-SVC flips is_active(). TTBR1 private page is EL1-only (ttbr1: ok). EL1 can fetch a real path from the high RAM alias (ttbr1: el1 exec); VBAR_EL1 is that alias (ttbr1: vbar). A 16 KiB dedicated identity text range is unmapped (ident: range); live identity .text after _start is unmapped after a vtable rewrite (ident: reloc / ident: live); _start stays. Closing isolation still needs PAN and a full identity teardown. A caught load of .data or of TTBR1_PRIV is that mile, not “EL0 isolated.”
  • Repo vs guest. “No secrets in repo” is a host boundary. It does not harden the UART.

Non-goals

  • Not a certified secure OS (no Common Criteria, no PSA, no “hardened”).
  • Not side-channel complete (no cache/timing/Spectre story; QEMU TCG is the wrong lab).
  • Not a product “the kernel is W^X” sentence. The identity image on this virt guest is RO+X / RW+NX with WXN (ADR-015). Future mappings are not automatically covered. Guard pages remain holes.
  • Not ASAN / canaries / heap-stack guards. Coop worker stacks have no unmapped holes. Overflow there is still image-adjacent PXN RAM.
  • Not secure boot, PAN, or a fully torn-down identity map (ADR-020 unmaps live identity .text after a high-VA vtable rewrite; .rodata/.data/heap stay; _start stays at 0x4008_0000).

Mitigations mapped to probes

MitigationProbeLedger
Threat-model v1.8 writtenRead this fileVerified (file + review). Still no “secure OS”.
Device MMIO XN (L1 block 0)pxn_for(0x0900_0000) == Some(true)Covered by the W^X tests when they run.
Heap + coop stacks PXNSerial wx: ok; #[test_case] flags + execute-from-heap IABORTVerified: 2026-09-10 cloud qemu-smoke (honesty ledger).
Kernel text still executableis_executable(0x4008_0000)Same W^X probe.
Execute-from-writable heap forbiddenArmed permission IABORT → wx: nx heap then wx: okFail-closed in scripts/qemu-smoke.sh.
Linker-stack guard holesSerial guard: fault / guard: ok; store to __stack_guardVerified: 2026-09-10 cloud qemu-smoke (honesty ledger).
Minimize unsafeReview (NFR-01). Count is not a proof.Policy.
Fail-closed smokescripts/qemu-smoke.sh greps + force-fail exit ≠ 0NFR-04 / NFR-05.
No secrets in repoPolicy + .gitignore (including .obsidian/)Host control.
IRQ least privilegeIRQ path does not allocate or yield_nowConvention. Ratchet if it fails twice.
EL0 entered and returnedSerial el0: svc / el0: ok; #[test_case]Verified first mile — not isolation.
EL0 cannot execute kernel dataSerial el0: nx kernel; lower-EL IABORTVerified first mile.
EL0 cannot read kernel .dataSerial el0: no kernel read; user TTBR0 omits .dataVerified read mile. Isolation stays Planned.
ASID isolation (no VMALLE1)Serial asid: dual / asid: conflict / asid: okSpecific mile. Umbrella isolation stays Planned.
Standing EL0 contextSerial el0: standing / el0: restored; is_active() flipsSpecific mile. Not POSIX. Not isolation.
TTBR1 private pageSerial ttbr1: el1 / ttbr1: no el0 / ttbr1: okFirst cut (ADR-016).
EL1 fetch from TTBR1 high VASerial ttbr1: el1 exec / ttbr1: vbarExec mile (ADR-017).
Identity-tear first cutSerial ident: split / ident: fault / ident: high / ident: no el0 / ident: okFirst cut (ADR-018). Full teardown Planned.
Identity text range tearSerial ident: jump / ident: range / ident: textRange cut (ADR-019).
High-VA vtable rewrite + live .text tearSerial ident: reloc / ident: liveLive .text cut (ADR-020). .rodata/.data/heap stay.
RO+NX text/dataSerial ro: ok; execute-from-.data + write-to-RO-textVerified: 2026-09-11 cloud qemu-smoke (honesty ledger).

Claim gate

A PR may say “threat-model v1.8 exists” after a file read. It may say “heap NX” / “identity image is W^X on this virt guest” only when the honesty ledger has a matching Verified QEMU probe. It may say “linker-stack guard faults” only with a matching translation-abort probe. It may say “EL0 entered and returned,” “EL0 cannot execute kernel data,” “EL0 cannot read kernel .data,” “standing EL0 context,” “ASID isolation,” “TTBR1 private page,” “EL1 fetched from a TTBR1 high VA,” “one identity text page was unmapped,” “a 16 KiB dedicated identity text range was unmapped,” “rustc vtables were rewritten to high aliases,” or “live identity .text after the boot stub was unmapped” only for the mile that actually passed.

It may not say “secure OS,” “hardened,” “EL0 works,” “EL0 isolated,” “the kernel moved,” or “immutable OS.” Scoped RO is immutability.md. Unprobed stays Unknown. https://ctos.artof.link HTTPS is Verified (2026-09-11 after #30; see the honesty ledger).

EL0 isolation (P-SEC-3 / ADR-013)

Isolation: Planned. A first mile, a user-TTBR0 read mile, an ASID TLB mile, a standing EL0 context, a TTBR1 private-page first cut, an EL1 high-VA fetch mile, an identity-tear first cut, an identity .text range tear, and a live identity .text tear after a high-VA vtable rewrite exist. Do not claim userspace or “EL0 isolated.” Standing enter/leave as a guest sample: apps-today.md. A stable SVC ABI / libctos is later Planned: building-or-porting.md.

Direction: ADR-013. TTBR1 first cut: ADR-016. EL1 fetch mile: ADR-017. Identity-tear first cut: ADR-018. Identity .text range tear: ADR-019. Live .text tear: ADR-020. Threat model: security.md. Code: src/el0.rs, src/asid.rs, src/ttbr1.rs, src/teardown.rs.

What exists today

  • Kernel runs at EL1 (SPSel = 0).
  • One map-window page (paging::EL0_PAGE) can be UXN-clear / PXN for a trampoline or standing payload.
  • ERET to EL0 switches to a user TTBR0 (L1_USER, ASID=1) that maps kernel text/rodata + the exception stack + the trampoline window (every 2 MiB those ranges occupy), and omits .data / .bss / heap. The lower-EL handler restores kernel TTBR0 from TPIDR_EL1 before touching kernel data. That path still TLBI VMALLE1 (kernel .data leaves are global).
  • Dual EL1 ASIDs (1 vs 2) with nG probe pages switch without TLBI VMALLE1 (src/asid.rs).
  • A bounded standing user context on that TTBR0: SVC #1 stays at EL0 (el0: standing), user MOVZ runs, SVC #2 restores EL1 (el0: restored). is_active() is true only for that lifetime.
  • TTBR1 walks are enabled. One kernel-private high page (TTBR1_PRIV) is EL1-only; EL0 load faults. Identity RAM is aliased at va + TTBR1_BASE via cloned RAM tables; EL1 can fetch a real path there and VBAR_EL1 is the high alias. After a high-VA jump, rustc vtables are rewritten to high aliases (ident: reloc) and live identity .text after the boot stub is unmapped (ident: live), plus the dedicated 16 KiB range (ident: range / ident: ok). .rodata / .data / heap stay identity-mapped. _start / QEMU -kernel stay at 0x4008_0000. Full identity teardown is Planned.
  • Lower-EL AArch64 sync handles SVC, a kernel-data IABORT, a kernel-data DABORT, and the TTBR1 private-page DABORT, then returns to EL1t (or stays at EL0 on standing SVC #1). Other lower-EL slots still park (ADR-004).

Probed miles

ProbeWhat closes itHonesty
EL0 entered and returnedSerial el0: svc + el0: ok; #[test_case] el0_svc_roundtripEntered and left. Not a user process.
EL0 cannot execute kernel dataSerial el0: nx kernel; #[test_case] el0_cannot_execute_kernel_dataIABORT on .data (UXN and/or unmapped). Not isolation.
User TTBR0 omits kernel .dataWalk L1_USER; #[test_case] user_ttbr0_omits_kernel_dataDistinct user table. Text is still mapped so the handler can run.
EL0 cannot read kernel .dataSerial el0: no kernel read; #[test_case] el0_cannot_read_kernel_dataTranslation/permission DABORT. Not PAN.
Standing EL0 contextSerial el0: standing / el0: restored; #[test_case] standing_el0_enter_leaveReal enter/leave on user TTBR0. is_active() true only while standing. Not POSIX. Lower-EL IRQ still parks.
ASID field programmeduser_ttbr0() >> 48 == 1Programming fact on the EL0 trampoline. Not the isolation mile.
ASID isolationSerial asid: dual / asid: conflict / asid: ok; #[test_case] asid_isolation_without_vmalle1Dual TTBR0 without TLBI VMALLE1. Stale ASID-1 data under ASID 2 is Failed. Not “EL0 isolated.”
TTBR1 private pageSerial ttbr1: el1 / ttbr1: no el0 / ttbr1: ok; #[test_case] ttbr1_el1_sees_priv_el0_does_notEL1-only high page. Not a relocated kernel.
EL1 fetch from TTBR1 high VASerial ttbr1: el1 exec / ttbr1: vbar; #[test_case] el1_executes_from_ttbr1_high_vaReal EL1 path + high VBAR. Identity boot stub stays.
Identity-tear first cutSerial ident: split / ident: fault / ident: high / ident: no el0 / ident: ok; #[test_case] identity_tear_el1_faults_high_staysOne identity text page unmapped; high twin still fetches. Not a relocated kernel. Full teardown Planned.
Identity text range tearSerial ident: jump / ident: range / ident: text; #[test_case] identity_text_range_unmapped_boot_stub_staysHigh-VA continuation + 16 KiB dedicated range unmapped; high twins still fetch. Not a relocated kernel.
High-VA vtable rewrite + live .text tearSerial ident: reloc / ident: live; #[test_case] identity_fn_ptrs_rewritten_high + live_identity_text_unmapped_boot_stub_staysrustc dyn Write / fmt tables patched to high aliases; live identity .text after _start unmapped; println! still runs. .rodata/.data/heap stay. Not a relocated kernel.

Still Planned (isolation)

ProbeWhat would close it
PANID_AA64MMFR1_EL1.PAN != 0 and an EL1 access to an EL0-accessible page faults. -cpu cortex-a57 is ARMv8.0 — usually unimplemented. Do not claim PAN.
Identity .rodata / .data / heap tearThose identity ranges unmapped; accesses proven high-only. After live .text (ADR-020).
Full higher-half / identity teardownThe row above plus a guest that no longer fetches identity .text after the boot stub (ADR-020 tears live .text after a vtable rewrite, not this).
EL0 entry without full TLBIUser TTBR0 switch that does not TLBI VMALLE1 (needs nG on kernel .data or an ASID-specific invalidate).
Lower-EL IRQ while standingTimer (or other) IRQ taken from EL0 and returned. Still parked.

Unprobed stays Unknown. The umbrella isolation row stays Planned until PAN + full identity teardown have probes (standing + TTBR1 first cut + EL1 high-VA fetch + torn live identity .text are not enough). Do not say “EL0 works,” “EL0 isolated,” or “the kernel moved.”

Performance (NFR-07 / ADR-011)

Performance is a first-class pillar, not a Later learning-only note. That does not authorize fake benches.

Rules

  1. A latency, cycle, or “faster than” sentence is a claim. It needs a probe in the honesty ledger.
  2. Optimize only after a probe shows a cost. Do not rewrite the scheduler or heap “for speed” on a hunch.
  3. QEMU virt numbers are one environment. They are not a Raspberry Pi probe and not a published SPEC run.

Baseline probe (this tree)

CNTPCT_EL0 is already used to timeout the M5 timer observe window. The pillars ratchet (when landed) measures a fixed trivial loop:

  • Serial marker perf: cntpct delta=<n> (fail closed on perf: probe missed).
  • #[test_case] asserts the counter advanced.

That probe proves the physical counter is readable and moves. It does not claim a microsecond budget, interrupt latency, or a comparison to other kernels.

IRQ-to-handler probe (this tree)

When CNTP fires, the handler records CNTPCT − CNTP_CVAL before rearm. After several ticks the hello kernel prints:

  • Serial marker perf: irq-delta min=<a> max=<b> spread=<b-a> n=<n> (fail closed on perf: irq-delta missed).
  • #[test_case] asserts samples exist and max >= min.

That is a spread of IRQ-to-handler counter deltas on this QEMU virt guest. It is not a latency budget, not “faster than X,” and not a published bench. QEMU TCG jitter is one environment.

Host debug ELF size (NFR-08, this tree)

scripts/qemu-smoke.sh prints the host byte size of target/aarch64-ctos/debug/ctos after cargo build:

  • Host marker perf: elf-size bytes=<n> (fail closed if missing or < 4096).
  • This is a measurement, not a size budget and not a “smaller is better” claim.

It does not time QEMU boot.

Boot-to-ready CNTPCT (NFR-08, this tree)

kernel_main samples CNTPCT_EL0 after paging::init (MMU + D-cache on) and again after Hello World! (init complete):

  • Serial marker perf: boot-delta ticks=<n> (fail closed on perf: boot-delta missed).
  • #[test_case] asserts a sample exists and the counter advanced.

That is kernel_main-entry to after-init on this QEMU virt guest. It is not a latency budget, not QEMU startup time, not a published bench, and not criterion.

OS/app slot disconnect (A9) — expected shape, not a bench

A9 #48 would load a separate app payload after the OS image (immutability.md, site measure.md). That is Planned after Track A ABI/loader. Today is still one linked ELF. There is no Verified delta. Do not invent a “faster/slower than linked-in” number.

ClassWhat we expect (hypothesis, unmeasured)Honesty
CostsExtra boot/load work; each SVC crossing; ASID/TTBR0 switches into the app map; optional COW later if payloads are sharedCost only after a probe. Not a budget.
Neutral / winsSteady EL0 compute (once mapped) should look like today’s standing stub, not like a new ISA. Smaller OS updates are an operational win (rebuild kernel without apps), not a CNTPCT winOperational ≠ measured latency.
GateKeep perf: boot-delta. Add a new app-load CNTPCT probe (perf: app-load or similar) when the loader exists. Fail closed on missMarker does not exist today.

Until that app-load probe prints on a two-artifact boot, A9 performance stays Planned. QEMU TCG jitter is still one lab. No criterion crate. No “slot disconnect is free.”

Later probes (Planned)

  • A tighter “first instruction of _start” sample if someone maps a .data slot that BSS-clear will not wipe.
  • App-load CNTPCT (A9) — only after a real loader; see the table above. Site KPI page: measure.md. Do not invent a percent.

Do not add a host criterion crate or a “bench.yml” that prints invented numbers.

Overview (plain English)

Core principles drive ctos. Track A / Track B / A9 are subordinate and never override this list for speed or marketing. Full list: principles.md.

  • Honesty ledger — Verified only with a probe
  • Antifragility — fail-closed sensors + ratchets
  • Security — threat model; probed mitigations only
  • Performance — measure first; no invented benches
  • Document-first / one milestone → one PR

ctos is a learning AArch64 kernel for QEMU virt. It is not a desktop, not POSIX, and not a “secure OS.” Status words need a probe in the honesty ledger. This note does not invent latency, size, or “faster than” numbers. Measured markers live in the ledger; they are one environment each.

Vision: product-vision.md. Pillars: pillars.md. Frozen IDs: fr-nfr.md. Samples: apps-today.md. Porting: building-or-porting.md. Immutability: immutability.md (scoped only). Tracks: A / B (subordinate).

Site chapters (source of truth for the published book): Overview. HTTPS at https://ctos.artof.link is Verified (2026-09-11 after #30). See website.md.

KPIs (what we actually measure)

These are sensors, not product SLOs. A missing marker is a fail. A printed number is not a budget.

Performance (NFR-07 / performance.md)

What we watchProbe (do not invent a target)
Physical counter movesSerial perf: cntpct delta=<n> + #[test_case]
IRQ-to-handler spread on this virt guestSerial perf: irq-delta min=… max=… spread=… n=…
Debug ELF byte sizeHost perf: elf-size bytes=<n> (measurement, not “smaller is better”)
kernel_main after-MMU → after-initSerial perf: boot-delta ticks=<n>
App-load after OS/app split (A9)Planned perf: app-load CNTPCT + existing boot-delta. No marker today.

QEMU TCG jitter is one lab. These are not Raspberry Pi numbers and not a published bench. Read the ledger row for the SHA you care about. A9 expected costs (boot/load, SVC, ASID/TTBR, later COW) vs steady EL0 compute: performance.md. No Verified delta — still one ELF.

Antifragility (NFR-05 / antifragility.md)

What we watchProbe
Hello + milestone strings still printscripts/qemu-smoke.sh greps; missing string → fail
Tests stay fail-closedcargo test exit 0; --features force-fail must be non-zero
Same failure twiceBecomes a sensor/gate, not another README paragraph
Failed history keptExample: Docker b2bbb99 Failed, then layout fix Verified — the Failed row stays

Unprobed boot stays Unknown. File presence is not QEMU boot.

Application-support scope

Do not say “applications run on ctos.” First-class samples and the cannot-run list live in apps-today.md. Porting stance: building-or-porting.md.

  • Can run (probed): coop EL1 UART workers (sched: task a/b/ok); one-byte UART RX (input: rx 0x41); standing EL0 stub (el0: standing / el0: restored). A heartbeat/counter variant is the same shape — not in tree until a probe greps it.
  • Cannot run: Linux ELF, shell, Python, network, filesystem, SMP, isolated userspace. Isolation / PAN / .rodata/.data/heap tear stay Planned. Filesystem stance: filesystem.md (memfs → virtio-blk → FAT/xv6-like; none today). Gaps to host apps + containers: no: host-apps.md.

Building or porting

First-class page: building-or-porting.md. Short honesty:

  • Easiest = in-tree no_std coop EL1 on aarch64-ctos.json, proven with cargo / qemu-smoke / docker-smoke.
  • POSIX / glibc = not easy, not started.
  • SVC ABI + libctos for freestanding EL0 = later Planned (standing dual-SVC is a stub, not a syscall table).

OS image vs app payloads (A9)

The sponsor goal for “immutability” here is not a frozen kernel. It is to disconnect OS updates from apps: one OS image artifact, separate app payloads, so you can replace the kernel without rebuilding apps and replace apps without rebuilding the kernel.

  • Today: one linked kernel ELF (target/aarch64-ctos/debug/ctos). Sample tasks and the standing EL0 stub are compiled in. Not Verified as a slot/split.
  • Planned after Track A ABI + loader (A1–A4 on #31): issue A9 #48. Stance: immutability.md.
  • Do not say “immutable OS” or “apps update independently” until a probe shows two artifacts and a load path.
  • Performance (unmeasured): expect costs at boot/load, SVC, ASID/TTBR switches, optional later COW; steady EL0 compute similar; smaller OS updates are operational, not a bench. Gate: boot-delta + a new app-load CNTPCT probe. No Verified delta today. Details: performance.md.

Prerequisites

  • Nightly Rust (rust-toolchain.toml), rust-src, llvm-tools-preview
  • qemu-system-aarch64 (Debian package is often qemu-system-arm)
  • Optional: Docker linux/arm64 (./scripts/docker-smoke.sh) — do not pin linux/amd64
  • To change the kernel: one milestone → one branch → one GitHub PR; author ≠ merger (ADR-002)

A machine that has not run scripts/qemu-smoke.sh (or Docker/GHA equivalent) has Unknown boot.

Advantages

  • Small, readable no_std tree on one ISA (AArch64 / QEMU virt / PL011 UART)
  • Fail-closed smoke: serial markers, cargo test, force-fail
  • Honesty ledger: Verified / Unknown / Planned / Failed, with a probe each
  • Failures become sensors (layout miss, live-.text vtable yank, CRLF shebang)
  • Three pillars named up front — security and speed still need probes

Drawbacks

  • QEMU virt only. No Raspberry Pi or board claim
  • Not POSIX, not multi-tenant, not a product runtime. No filesystem today (filesystem.md). Not a container host (host-apps.md); host docker-smoke ≠ guest Docker.
  • Isolation is Planned. Live identity .text after the boot stub is torn (ADR-020); .rodata/.data/heap stay
  • Scoped immutability only (immutability.md): RO+NX / WXN / live .text tear are probed. Absolute “immutable OS” is incompatible (heap/PTEs/devices must mutate). OS/app slot disconnect (A9) is Planned after Track A ABI/loader — still one ELF today.
  • Performance numbers are guest counter deltas, not a latency budget
  • Docs website / custom domain: HTTPS serving the book is Verified (website.md)
  • Same-login cannot self-merge; a second identity has to land the PR

Honesty

Do not say “secure OS,” “the kernel moved,” “EL0 isolated,” “immutable OS,” or “apps update independently of the OS.” Point at the ledger for any number you quote. The live docs URL is a separate Verified row — do not invent other site claims.

What can run today

Do not say “applications run on ctos.” These are the probed guest examples on QEMU virt. Each needs a matching honesty ledger row. Isolation, POSIX, and a userspace ABI stay Planned.

Hub: overview.md. How to add something: building-or-porting.md. Frozen IDs: FR-08 (UART RX), FR-11 (coop yield).

Site source of truth: What can run today. This page is extra stance (sample walkthroughs). HTTPS at https://ctos.artof.link is Verified (2026-09-11 after #30).

Sample: cooperative EL1 UART workers (M9)

What it is. Two heap-backed EL1 workers take turns. Each prints one UART line, then the idle thread sees both flags and prints sched: ok. Cooperative only — a task runs until it returns or calls yield_now(). Not a timer slice. Not SMP. Not EL0.

Where. src/sched.rs (task_a / task_b / run_two_tasks). Decision: ADR-010.

How it works.

  1. Heap must already be up (src/heap.rs). Worker stacks are 8 KiB Vec<u8> boxes, not the linker SP_EL0 stack.
  2. spawn(task_a) and spawn(task_b) install AAPCS64 callee-saved frames. After ADR-020 the first ret lands on the high-VA alias of the trampoline.
  3. Idle (kernel_main / the test runner) calls yield_now() until both atomics are set, or a bound is hit.
  4. Each worker stores its SP, writes a raw UART line, sets its flag, and returns (Done).

Probe (do not invent extra markers).

SerialMeaning
sched: task aWorker A ran on a heap stack
sched: task bWorker B ran on a heap stack
sched: okBoth ran; smoke greps all three

#[test_case] two_tasks_run_on_distinct_heap_stacks and yield_round_robin_resumes_both close the same claim. Idle tip Docker on main e80dc93: those strings still print on the hello path (50 tests). File presence is not that probe.

Heartbeat / counter variant (not in tree)

Same shape as task_a / task_b, not a new subsystem. A worker that loops, increments an AtomicU64, prints sched: beat n=<n>, and yields would be a counter / heartbeat gadget.

That variant is not in src/sched.rs today. There is no sched: beat smoke string. Adding it is the easiest in-tree exercise (building-or-porting.md): one extra fn, one spawn, one milestone → one PR. Until a probe greps the new marker, treat it as Planned, not Verified.

Sample: UART RX echo gadget (M6)

What it is. The guest polls the virt PL011 RX FIFO and prints the first byte the host injects. On the smoke path that byte is 0x41 ('A'). That is one-shot observe, not a line editor and not a shell.

Where. src/uart.rs (observe_rx / observe_probe_byte). Host inject: scripts/qemu-serial-inject.py (default CTOS_INPUT_BYTE=0x41). Decision: ADR-007.

How it works.

  1. After the hello path has printed earlier markers, observe_probe_byte spins on UARTFR.RXFE until a byte arrives or CNTPCT times out.
  2. IRQs stay masked on this path. There is no UART IRQ in M6. Do not wfi waiting for RX.
  3. The expected byte prints input: rx 0x41. Any other byte prints input: rx 0xNN (unexpected) and fails the probe. Timeout prints input: rx missed.

Probe.

SerialMeaning
input: rx 0x41Host-injected 0x41 landed in the FIFO
input: rx missedFail-closed (smoke rejects this)

#[test_case] uart_rx_fifo_empty_without_host_byte only proves the FIFO is empty under cargo test (no inject). Character proof is the hello serial row.

This is not interactive echo, virtio-keyboard, or a TTY. A later line-oriented gadget would be a new milestone with its own marker.

Sample: standing EL0 stub (SVC enter / leave)

What it is. A bounded user context on the user TTBR0. The kernel ERETs to EL0, the payload announces with SVC #1, runs one user MOVZ, then SVC #2 restores EL1. el0::is_active() is true only for that lifetime.

Where. src/el0.rs (standing install) and the lower-EL sync path in src/exception.rs. Direction: ADR-013. Narrative: el0.md.

How it works.

  1. First miles already proved enter/return (el0: svc), UXN fetch (el0: nx kernel), and no kernel .data read (el0: no kernel read).
  2. Standing payload on the EL0 window: SVC #1 → serial el0: standing (stay at EL0) → MOVZ X1, #0x51A4SVC #2 → serial el0: restored (back to EL1).
  3. Saved user PC / SP / TTBR0 live only while is_active() is true. The handler clears that flag on restore.

Probe.

SerialMeaning
el0: standingSVC #1 taken; still at EL0
el0: restoredSVC #2 taken; back to EL1
el0: okFirst-mile bundle including standing

#[test_case] standing_el0_enter_leave closes enter/leave. Lower-EL IRQ still parks. PAN on -cpu cortex-a57 is Planned. This is not a user process, not POSIX, not a syscall table, and not “EL0 isolated.”

Also on the same hello path

These are kernel sensors, not apps. They still print on a Verified e80dc93 smoke:

  • UART Hello World!
  • paging: ok, heap: ok (Box / Vec on the first-fit heap)
  • timer: tick (GICv2 + CNTP)
  • wx: ok, guard: ok, ro: ok
  • ident: reloc / ident: live / ident: ok (ADR-020 live .text tear)
  • exception: sync BRK then exception: fatal nested

Cite the ledger SHA if you quote a number.

What cannot run today

Explicit no. Do not paper over these with a “porting guide.”

WantWhy not
A Linux ELF / glibc / musl binaryNo exec, no ELF loader, no syscall table. Custom target is os: none.
A shell (sh, bash) or line-oriented TTYRX probe is one injected byte. No line discipline.
Python, Node, or any hosted interpreterNeeds a process ABI, heap policy, and usually a filesystem.
Network / sockets / HTTPNo virtio-net, no stack, no sockets.
Filesystem (open/read/write files)No VFS, no block device, no FAT/memfs. Planned order: memfs → virtio-blk → FAT or xv6-like. Stance: filesystem.md.
SMP / a second CPU / preemptive threadsM9 is cooperative EL1 on one vCPU.
Isolated userspace / “an app you compile and exec”Standing EL0 is a stub. PAN + .rodata/.data/heap tear + umbrella isolation stay Planned. Gaps: host-apps.md.
OCI / Docker / k8s in the guestNo. Host docker-smoke.sh only builds the kernel.
Raspberry Pi or any board other than QEMU virtUnprobed. Do not copy virt Verified onto hardware.
GPU / desktop / windowing / virtio devicesOut of scope on this horizon.

Isolation, PAN, and tearing identity .rodata / .data / heap stay Planned. See el0.md.

Building or porting

Honesty first: there is no userspace ABI to compile against, and no libc. ctos is a freestanding no_std kernel on a custom target. Status words need a probe in the honesty ledger.

What already runs: apps-today.md. KPIs and trade-offs: overview.md.

Site source of truth: Building or porting. This page is extra stance. HTTPS at https://ctos.artof.link is Verified (2026-09-11 after #30).

Easiest path — in-tree no_std cooperative EL1

The only path that matches today’s probes is another kernel task in this repo, built for aarch64-ctos.json, proven with the same smoke you already have.

Target and build

PieceWhat it is
aarch64-ctos.jsonCustom rustc target: os: none, abort, soft-float, static, rust-lld. Not aarch64-unknown-linux-gnu.
.cargo/config.tomlPins that JSON, build-std (core / alloc / compiler_builtins), -Tlinker.ld, QEMU runner.
rust-toolchain.tomlNightly + rust-src / llvm-tools-preview.
cargo build                 # ELF at target/aarch64-ctos/debug/ctos
cargo run                   # qemu-system-aarch64 -machine virt
./scripts/qemu-smoke.sh     # hello greps + cargo test + force-fail
./scripts/docker-smoke.sh   # same smoke in linux/arm64 (do not pin amd64)

A machine that has not run qemu-smoke.sh (or Docker / GHA equivalent) has Unknown boot. Idle tip: sponsor Docker is Verified on main e80dc93 (50 tests, ident: reloc n=12, live pages=37, force-fail). That is one host, not this VM and not a Pi.

Add a cooperative EL1 task

Same shape as apps-today.md (src/sched.rs task_a / task_b):

  1. Write a fn my_task() that uses alloc / UART / sched::yield_now() only. No std, no files, no sockets.
  2. spawn(my_task) next to the existing workers (heap must be up; stacks are heap Vecs).
  3. Print a new serial marker (sched: beat n=… or similar). Teach scripts/qemu-smoke.sh to grep it. Fail closed on miss.
  4. One milestone → one branch → one GitHub PR. Author ≠ merger (ADR-002).

A heartbeat / counter loop is the natural variant. It is not in the tree until that PR lands. Do not claim it is Verified from this paragraph.

Stay on AArch64 QEMU virt / PL011 (ADR-003). Do not restore bootloader 0.9 or VGA as primary.

POSIX / glibc port — not easy, not started

A Linux, musl, or glibc binary will not run. Missing, among other things:

  • exec / ELF loader / dynamic linker
  • syscall table (read / write / open / mmap / clone / …)
  • filesystem (none today — filesystem.md; Planned memfs → virtio-blk → FAT/xv6-like), signals, sockets, environ, TLS as Linux defines them
  • a C runtime (crt0, libgcc helpers as a POSIX process)

Do not publish a “port busybox / musl to ctos” guide that skips those gaps. That work would be many ADRs, not a weekend #ifdef. Frozen Out list: fr-nfr.md (userspace processes, POSIX, networking).

Later Planned — SVC ABI + libctos (freestanding EL0)

Standing EL0 is a dual-SVC stub (SVC #1 stay / SVC #2 restore) plus first-mile SVC #0. It is not a syscall table and not a libc.

Planned (not in tree, not Verified):

  1. Isolation miles first: PAN (usually absent on cortex-a57), identity .rodata / .data / heap tear, umbrella EL0 isolation (el0.md, ADR-013).
  2. A stable SVC ABI written as a later ADR (numbers, registers, error model). Do not silently grow #1 / #2 into POSIX.
  3. A freestanding libctos (name reserved here as intent only — no crate today) that a future EL0 program could link against no_std, talking that ABI. Still not glibc. Still not exec of a Linux ELF.

Until those probes exist, “write a user program for ctos” is Planned. The easiest thing you can do today remains an in-tree EL1 task.

A later OS image vs app payload split (A9 #48) is Planned after that ABI/loader. Today is still one linked ELF — not Verified. See overview.md and immutability.md.

Do not invent

  • A porting guide that assumes POSIX, a shell, Python, or containers (host-apps.md: containers are no)
  • A claim about the docs URL that skips the ledger (HTTPS is Verified as of 2026-09-11; do not invent extra site KPIs)
  • “Secure OS,” “the kernel moved,” or “EL0 isolated”

Cite the ledger for any Verified SHA you quote.

Filesystem stance

No filesystem today. There is no VFS, no open/read/write of named files, no directory, no block device, and no on-disk format. A grep of src/ finds no memfs, virtio-blk, FAT, or inode code. That absence is the probe for “none now.” The capability itself is Planned.

Do not say “ctos has files.” Do not mint a new FR/NFR ID in chat. Frozen Out list already names POSIX / userspace as later (fr-nfr.md). Hub: overview.md. Samples that do run: apps-today.md.

Site source of truth: Filesystem (Planned). This page is extra stance. HTTPS at https://ctos.artof.link is Verified (2026-09-11 after #30).

Intended order (not started)

Each step is one milestone → one branch → one PR, with its own ADR when it lands. Later steps do not start on an unclosed earlier one.

StepWhat it would beWhy this orderStatus
1. memfsIn-RAM named buffers on the existing first-fit heap (src/heap.rs). Prove create / lookup / read / write of a path without DMA.Same “easiest in-tree” shape as a coop EL1 task. No virtqueue, no disk image.Planned. Not in tree.
2. virtio-blkQEMU virt virtio block: virtqueues, a guest-visible disk, read/write sectors.Paging and a heap already exist, but virtio-mmio / DMA is its own mile. Do not pretend PL011 RX is a block device.Planned. After memfs (or justified in that ADR if a probe needs a disk first).
3. FAT or xv6-likeAn on-disk layout on top of the block device. FAT if we want a host-visible image; xv6-like if we want a tiny teaching inode FS.Choose in the ADR that lands it. This page does not pick.Planned. After a block device.

A smoke string such as fs: ok / blk: ok would be invented with that PR, fail-closed in scripts/qemu-smoke.sh. Until then, there is nothing to grep.

What this is not

  • Not POSIX open / stat / mount. Those wait on a later SVC ABI (building-or-porting.md).
  • Not virtio-net, 9p, or a Linux rootfs.
  • Not “we have a disk because QEMU can attach one.” Host -drive without guest code is not a probe.
  • Not a claim that Python, a shell, or a package manager becomes possible once memfs exists. Those still need an ABI, a process, and usually more than a RAM tree.

Honesty

ClaimProbeStatus
No filesystem in this treeSource: no VFS / memfs / virtio-blk / FAT in src/Verified (absence)
Guest can open a fileSerial + #[test_case] that do not exist yetPlanned
virtio-blk worksQEMU disk + guest driver + markerPlanned
FAT or xv6-like on a block deviceFormat + read-back probePlanned

Unprobed stays Unknown. File presence of this note is not a filesystem. See the honesty ledger.

Gaps to host apps — and no containers

Do not say “you can run host apps on ctos.” A host app here means a program you already run on Linux or macOS: a shell, Python, a browser, a Docker/OCI container, a glibc ELF you exec. Today’s guest can run the probed samples only. Closing the gap is Planned in pieces, not a product claim.

./scripts/docker-smoke.sh is a host harness (build the kernel inside a Linux container, then QEMU). It is not a guest container runtime. ctos is not a container host.

Hub: overview.md. Porting: building-or-porting.md. Filesystem: filesystem.md.

Site source of truth: Hosting apps / containers. This page is extra stance. HTTPS at https://ctos.artof.link is Verified (2026-09-11 after #30).

What a host app needs vs what exists

Need (typical host app)On ctos todayStatus
A process you execOne linked kernel ELF. Standing EL0 is a dual-SVC stubPlanned (loader + ABI). Not started.
POSIX / glibc / muslCustom aarch64-ctos.json, os: none, no libcNot easy, not started.
Files (open / a disk)No VFSPlanned memfs → virtio-blk → FAT/xv6-like
Sockets / HTTPNo virtio-net, no stackPlanned at best; not a Now mile
Shell / TTY / PythonOne injected UART byte; no interpreterNo until ABI + FS + line discipline
Isolated userspaceFirst miles + live .text tear; PAN / full teardown missingIsolation Planned
Preemption / SMPCooperative EL1, one vCPUNo on this horizon
Containers (OCI / Docker / k8s as the guest)NothingNo. See below.

The easiest thing you can add today is still an in-tree no_std coop EL1 task — not a host binary.

Containers: no

ctos will not run containers as a guest feature on this horizon.

  • No OCI image pull, no runc, no cgroups, no Linux namespaces, no overlay FS, no containerd/CRI.
  • A Linux Docker/Podman host that builds this kernel is unrelated. That smoke does not make QEMU virt a container host.
  • “Run Alpine on ctos” / “k8s node” is the same class of claim as “POSIX port is easy.” It is not.

Do not write a container roadmap that skips process ABI, a filesystem, and isolation. Those are earlier Planned gaps. Do not mint a new FR ID for containers.

Honesty

ClaimProbeStatus
Guest is not a container hostSource: no OCI/runc/cgroup/namespace code in src/Verified (absence)
Host docker-smoke.sh builds the kernelLedger Docker rows (sponsor / GHA)Separate claim — host harness only
A host app (Linux ELF, shell, Python) runs in the guestNo such serial markerPlanned
Guest is a container hostNo OCI/runc/cgroup codeVerified absence; non-goal (not a later Planned feature)

File presence of this note is not an app runtime. See the honesty ledger.

Immutability (scoped, not absolute)

Compatible with ctos principles only as scoped immutability: code and other RO regions stay non-writable after a probed lock-down; later, loaded app images can be RO too. That lines up with security (NFR-10), honesty (NFR-06), and antifragility (NFR-05) — a region is RO when a probe says so, and a Failed write-to-RO stays in the ledger.

Incompatible if absolute. A kernel must mutate heap, page tables, device MMIO, and task state. “Immutable OS” as marketing is a status inflation. Do not write it.

Hub: overview.md. Site SoT: Advantages — Immutability. Pillars: pillars.md. Threat model: security.md.

Plan issues (open on 2026-09-11): Track A #31 / in-repo track-a.md, Track B #40 / track-b.md. A9: #48. Tracks are subordinate to principles.md.

Already practiced (probed)

These are scoped cuts on QEMU virt. They are not an immutable kernel.

ScopeProbeADR
Identity .text/.rodata RO+X; .data/heap RW+NXro: nx data / ro: write fault / ro: okADR-015
SCTLR.WXN onsame RO+NX smokeADR-015
Live identity .text torn after high-VA jump + vtable rewriteident: reloc / ident: live / ident: okADR-020

Heap, PTEs, UART/GIC, and coop stacks stay writable on purpose. .rodata/.data/heap identity tear is still Planned.

Track A — RO app payloads, then A9 slot disconnect

The goal of this stance (sponsor clarification) is to disconnect OS updates from apps: a separate OS image vs app payloads. Update/replace the kernel without rebuilding apps, and the reverse. That is A9 #48. It is not an “immutable OS” product sentence.

Track A #31 must land first: stable SVC ABI (A1), libctos (A2), ELF/raw loader into user TTBR0 (A3), standing EL0 as normal mode (A4). RO app payloads are that loader mapping an image RO+X. A9 is Planned after that ABI/loader, not instead of it.

Today: one linked kernel ELF. No OS-image artifact, no app payload slot, no cross-update probe. Not Verified. Performance impact is the same honesty: expected costs and a future app-load CNTPCT gate, no Verified delta (performance.md).

Track B #40 must not use Linux-compat research to claim an immutable or container host. Containers stay a non-goal.

Claim gate (ADR-style, no new ADR here)

Same rule as ADR-001 / ADR-011:

  1. Name the scope (which pages, which image).
  2. Cite a probe (serial + #[test_case], or source absence).
  3. Only then Verified. Unprobed stays Unknown. Future work stays Planned.
  4. Do not say “immutable OS,” “W^X kernel” as a product sentence, or “secure because RO.”

File presence of this note is not that probe. See the honesty ledger.

Docs website + DNS

The published site is this mdBook (book.toml, src = "docs"). The website and the repo are the same markdown. Visitor-facing source of truth is the landing plus docs/overview/*. docs/framework/* is for deep links (ledger, pillars, threat model). Do not keep a second marketing copy.

Required site chapters (sidebar + landing). A missing file fails mdbook build (create-missing = false) and scripts/docs-build.sh:

Website pageSource file
What can run todaydocs/overview/what-can-run.md
Building or portingdocs/overview/porting.md
Filesystem (Planned)docs/overview/filesystem.md
Hosting apps / containersdocs/overview/hosting-apps.md
KPIs / how we measuredocs/overview/measure.md
Prerequisitesdocs/overview/prerequisites.md
Advantagesdocs/overview/advantages.md
Drawbacks / limitsdocs/overview/limits.md

Frozen IDs touched by the publish path: document-first (FR-14), honesty (NFR-06), written acceptance matches what is proven (NFR-13). No new FR/NFR IDs.

Diagrams use mermaid fences. mdbook-mermaid 0.17.1 wraps them; docs/mermaid-init.js loads mermaid 11.6.0 from jsDelivr in the browser. A local mdbook build without mdbook-mermaid on PATH fails (book.toml lists the preprocessor). Use ./scripts/docs-build.sh.

Local build

You need mdBook 0.5.4 and mdbook-mermaid 0.17.1 (pinned in .github/workflows/pages.yml and scripts/docs-build.sh).

./scripts/docs-build.sh          # downloads both binaries if missing, then `mdbook build`
# or, if both are already on PATH:
mdbook build                     # writes ./book/
mdbook serve                     # http://localhost:3000

create-missing is off: a SUMMARY.md link to a missing file fails the build. Output directory book/ is gitignored.

A successful local mdbook build is a generator probe. It is not a new “the website is published” claim. The live URL is a separate ledger row.

Production URLs

URLRoleStatus word
https://ctos.artof.linkCustom domainVerified. HTTPS 200, cert for this name, landing includes Driving principles. Main deploy 34653046584 after #30. Pages API: cname=ctos.artof.link, https_enforced=true, cert approved (expires 2026-12-10).
https://artofdream.github.io/ctosProject-site path (no trailing slash)Redirect (301) to the custom domain on the 2026-09-11 probe. A trailing-slash .../ctos/ 404’d. Do not treat github.io as a second live tree.

Do not write Verified on a new deploy from file presence alone. Re-fetch after the next main Pages run if you change the claim.

GitHub Pages workflow

.github/workflows/pages.yml:

  • Pull requests: mdbook build + check that the output CNAME is ctos.artof.link. Does not deploy.
  • Push to main: build, upload actions/upload-pages-artifact, deploy with actions/deploy-pages.

Settings already in use after #30: Source = GitHub Actions, custom domain ctos.artof.link, Enforce HTTPS on. Do not flip Source away from Actions. A CNAME file in the artifact (from book.toml cname = "ctos.artof.link", also the repo-root CNAME) should stay in lockstep.

See GitHub: publishing with Actions.

DNS — Amazon Route 53 (CNAME in place)

artof.link is hosted on Amazon Route 53. The sponsor states the ctos CNAME is already created. Do not run CREATE again.

FieldValue
AWS account737290977112
Region for CLIus-east-1 (Route 53 API is global)
Hosted zone nameartof.link
Hosted zone IDZ1178AFMV41RWP (sponsor-stated)
RecordCNAME ctos.artof.link.artofdream.github.io. (trailing dot)

This is a project site on a subdomain. Do not CNAME to artofdream.github.io/ctos. Do not touch the artof.link apex.

Public dig CNAME ctos.artof.link returns artofdream.github.io. DNS is in place at the resolver. That is not a Route 53 API list-resource-record-sets from this environment (no AWS CLI / credentials here). Next session may LIST zone Z1178AFMV41RWP to confirm; skip change-resource-record-sets unless the row is missing.

export AWS_REGION=us-east-1
aws sts get-caller-identity --query Account --output text   # expect 737290977112
aws route53 list-resource-record-sets --hosted-zone-id Z1178AFMV41RWP \
  --query "ResourceRecordSets[?Name=='ctos.artof.link.']"

scripts/route53-ctos-cname.json is a leftover CREATE batch for recovery only.

Probes

dig CNAME ctos.artof.link +short    # expect artofdream.github.io.
curl -sSI https://ctos.artof.link   # expect HTTP 200 (Verified 2026-09-11)

Honesty

ClaimProbeUntil then
mdBook builds this tree./scripts/docs-build.sh (or mdbook build with mermaid preprocessor) exit 0
Pages workflow existsRead .github/workflows/pages.ymlFile presence only
Pages workflow builds a PRGreen pages run on this branch (build job; deploy skipped)See honesty ledger
Docs website publishedGreen pages workflow on main and HTTPS fetch of https://ctos.artof.linkVerified — deploy 34653046584 + HTTPS 200 + Driving principles
Public CNAME ctos.artof.linkdig CNAME ctos.artof.link +shortVerified — DNS in place (artofdream.github.io.)
Route 53 API row in 737290977112 / Z1178AFMV41RWPlist-resource-record-sets as that accountUnknown here (no AWS CLI). Sponsor states CREATE already done. Do not CREATE again.
Custom domain reachabilityPages lists the hostname and HTTPS 200 with a matching certVerified — Pages API cname + https_enforced + cert approved; curl -sSI https://ctos.artof.link HTTP 200

Do not say “secure OS,” “EL0 isolated,” or that QEMU boot was proven by this docs PR.

Second brain

Session memory lives in git under research/, not on a wiki. This page is a curated landing for the published book. Daily briefs and scratch notes stay on GitHub so the site does not pretend every session log is product documentation.

VaultWhereJob
Procedure.cursor/skills/ctos-*/How to do a repeatable job
Correction.cursor/rules/ + hard constraints in AGENTS.mdMistakes we must not repeat
RelationshipDocs cross-links (vision → architecture → ADR → roadmap → ledger)How pieces connect
Daily Briefresearch/daily-briefs/Handoff: where we stopped

Session scratch: research/random-thoughts/. Do not treat scratch as the honesty ledger.

Tracker is GitHub. Optional Obsidian UI is local-only (.obsidian/ is gitignored). See research/README.md on GitHub.

Route 53: CNAME already created (zone Z1178AFMV41RWP). Playbook / LIST-only: research/dns-route53-ctos.md. https://ctos.artof.link HTTPS is Verified after #30 (website.md).