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.
| Principle | In everyday words | Frozen ID |
|---|---|---|
| Honesty | If we did not run a check, we do not say it works. Unprobed stays Unknown. | NFR-06 · ledger |
| Antifragility | The same miss twice becomes an automated sensor, not another README paragraph. | NFR-05 · Antifragility |
| Security | Write down what we fear, then prove a slice. Not a “secure OS” slogan. | NFR-10 · Security |
| Performance | Measure a known path first. No invented benches. | NFR-07 · Performance |
| Document-first | Write 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.
- Two kernel tasks that take turns — they print on the serial port and yield. Not preemptive. Not two CPUs.
- A serial echo gadget — one byte in, a line out. No terminal, no line editor.
- 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.”
Read next
| If you want… | Go here |
|---|---|
| What is in vs out | What can run today · Drawbacks / limits |
| Why POSIX does not port | Building or porting |
| Files later (not now) | Filesystem (Planned) |
| App host / containers | Hosting apps — containers: no |
| How we measure | KPIs |
| Why the repo is run this way | Advantages |
| Deep dives | Vision · FR / NFR · Architecture · Roadmap · Ledger |
Kernel build notes also live in the GitHub README.
URLs
| URL | Honesty |
|---|---|
https://ctos.artof.link | Verified. HTTPS 200, cert for this name, landing shows Driving principles. Main deploy 34653046584 after #30. |
https://artofdream.github.io/ctos | Redirect. 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)
| Need | Why |
|---|---|
Nightly Rust + rust-src + llvm-tools-preview | Freestanding build-std for the custom aarch64-ctos.json target. Pin is rust-toolchain.toml. |
qemu-system-aarch64 | Primary guest is QEMU -machine virt (ADR-003). Debian/Ubuntu package is often qemu-system-arm. |
| A host that can run those tools | Windows, 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
| Extra | When |
|---|---|
Docker linux/arm64 | cts-ai (Windows ARM64) path: ./scripts/docker-smoke.sh. Do not pass --platform linux/amd64. |
| mdBook 0.5.4 + mdbook-mermaid 0.17.1 | Local 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;yieldto 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_DYNor a LinuxET_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):
- 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) - A freestanding C runtime /
libctosfor user mode - Link a freestanding AArch64 user-mode binary
- 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.
| Fit | What | Why |
|---|---|---|
| Best first | Ramdisk / memfs | Named 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-disk | virtio-blk + FAT16/32 or a tiny xv6-like inode FS | FAT 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, optional | ctos-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.”
- VFS ADR — thin interface (lookup / read / write / a path). IDs unchanged until an issue + ADR says otherwise.
- memfs Verified — in-RAM named buffers; serial /
#[test_case]that do not exist yet. - virtio-blk — virtqueues + sector I/O on QEMU
virt. - On-disk FS — FAT16/32 or tiny xv6-like, as that ADR decides.
- 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: okwith that PR, fail-closed.
Until those probes exist, status stays Planned.
Honesty
| Claim | Probe | Status |
|---|---|---|
| No FS/VFS/block stack in tree | Source absence in src/ | Verified (absence) |
| memfs create/lookup/read/write | Serial + #[test_case] | Planned |
| virtio-blk | QEMU disk + guest driver + marker | Planned |
| FAT or xv6-like | Format + read-back / host image check | Planned |
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.
| Gap | Why it blocks hosting | Status |
|---|---|---|
| Stable SVC ABI | Need documented syscall numbers, not the test pair | Planned (direction on the porting page) |
| ELF / user loader | No loader for a freestanding user-mode binary, let alone a Linux ELF | Not built |
| Standing user mode as normal | Standing enter/leave is a stub mile, not the default way code runs | First mile Verified; normal userspace Planned |
| Stronger isolation | Umbrella user-mode isolation needs PAN + fuller identity teardown | Planned — do not say “EL0 isolated” |
| VFS + memfs | No files, no paths; see Filesystem | Planned |
| libctos / CRT | Nothing to link a freestanding user-mode program against | Not built |
| Richer I/O | UART byte in/out only; no TTY, disk, or sockets | UART probed; the rest unbuilt |
| Preemption / extra CPUs / net | Cooperative one-CPU yield; no NIC | Later — 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
| Claim | Status |
|---|---|
| ctos hosts third-party apps | No — gaps above |
| ctos hosts OCI/Docker containers | No — not a goal on this page |
Host Docker runs ctos-smoke | Separate 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.
| Probe | What it is | What it is not |
|---|---|---|
Cycle-counter loop (perf: cntpct) | The counter is readable and moves over a fixed trivial loop | A 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 ticks | A 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 build | A 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.”
| Kind | Honest guess (unmeasured) | What would make it a claim |
|---|---|---|
| Possible cost | Extra return-to-user / supervisor call, a user page-table switch, mapping an app slot — vs today’s in-tree function call | Cycle-counter (or irq-delta) around a real load + enter/leave once Track A exists |
| Possible neutral | Steady-state UART print / yield after the app is mapped, if the hot path stays similar | Same probes on the new path vs the in-tree workers; no win claimed without a delta |
| Build-time, not a bench | Kernel ELF no longer contains the “app”; you rebuild slots separately | perf: 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).
| Mechanism | What it proves |
|---|---|
scripts/qemu-smoke.sh | Build + required serial strings + tests exit 0. Missing a marker is a fail, not a skip. |
cargo test --features force-fail | The panic path is fail-closed (host/QEMU exit 1). A green suite that cannot fail is not a sensor. |
| Honesty ledger | Every 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 ratchets | When 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
| ID | Requirement | Priority | Stage |
|---|---|---|---|
| FR-01 | Kernel builds as a freestanding no_std / no_main binary for a custom aarch64-*-none target. | Must | Now |
| FR-02 | QEMU (-kernel on virt) loads the kernel ELF and transfers control to a stable _start entry. | Must | Now |
| FR-03 | Kernel can write text to the virt PL011 UART (MMIO, or later earlycon) via print! / println!. Not VGA 0xb8000. | Must | Now |
| FR-04 | Panic handler prints a message (best-effort) and halts without unwinding. | Must | Now |
| FR-05 | Developer can produce a runnable AArch64 kernel ELF and boot it under qemu-system-aarch64 (-machine virt). | Must | Now |
| FR-06 | Synchronous exceptions are handled via VBAR_EL1 vectors (at least a breakpoint / fault path). | Must | Now |
| FR-07 | A fatal exception uses a dedicated stack so overflow does not silently lock the VM. | Must | Now |
| FR-08 | Hardware 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. | Should | Now |
| FR-09 | Kernel 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. | Must | Now |
| FR-10 | GlobalAlloc heap so alloc types (Box, Vec) work in kernel. M8: first-fit list on a 64 KiB identity-mapped frame run (ADR-009). | Should | Now |
| FR-11 | Cooperative or simple round-robin task switching (threads or async tasks). M9: EL1 yield of AAPCS64 callee-saved GPRs; two heap-backed workers (ADR-010). | Should | Now |
| FR-12 | Serial remains usable for headless/CI logs (PL011 or extra earlycon), including later test-exit telemetry. | Should | Next |
| FR-13 | Integration tests that boot in QEMU virt and exit with a deterministic success/fail code (ARM semihosting SYS_EXIT, not x86 isa-debug-exit). | Must | Now |
| FR-14 | Documented milestone path (vision → architecture → ADR → roadmap) stays ahead of code for each stage. | Must | Now |
| FR-15 | Honesty ledger records each major capability as claim + probe + status (Verified / Unknown / Planned). | Must | Now |
Non-functional requirements
| ID | Requirement | Priority | Stage |
|---|---|---|---|
| NFR-01 Safety | Prefer safe Rust; unsafe only at hardware/FFI boundaries, minimized and justified. | Must | Now |
| NFR-02 Reproducibility | Toolchain pinned (rust-toolchain.toml); target JSON + .cargo/config.toml checked in. | Must | Now |
| NFR-03 Portability (dev) | Build+QEMU path works on Windows/macOS/Linux (host tools); kernel target remains freestanding AArch64. | Must | Now |
| NFR-04 CI / sensors | PR gate runs scripts/qemu-smoke.sh (build + UART hello string + cargo test exit codes). GitHub Actions + optional Docker. | Should | Now |
| NFR-05 Antifragility | First-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. | Must | Now |
| NFR-06 Honesty | Status words in docs/PRs need a probe; unprobed stays Unknown; no self-merge as “verified.” | Must | Now |
| NFR-07 Performance | First-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. | Should | Now |
| NFR-08 Footprint | Debug image size and boot time tracked once measurable; no premature optimization. | Could | Later |
| NFR-09 Maintainability | Clear module layout; ADRs for boot-path, console, or allocator choices. | Must | Now |
| NFR-10 Security | First-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. | Must | Now |
| NFR-11 Observability | QEMU UART + explicit test exit codes are first telemetry; host Grafana out of scope. | Should | Next |
| NFR-12 Multi-agent | Thin roles (engineer / knowledge / coherence / MRC); implementer ≠ merge approver. | Should | Now |
| NFR-13 Document-first | Written milestone acceptance criteria match what code/CI actually prove. | Must | Now |
| NFR-14 Second brain | Session memory + daily briefs in git so agents inherit state without chat archaeology. | Should | Now |
Acceptance sketch (Now)
cargo buildsucceeds with pinned nightly foraarch64-ctos.json.qemu-system-aarch64 -machine virtshows the hello line on serial or honesty ledger says Unknown until probed.scripts/qemu-smoke.shfails closed if the hello string is missing orcargo testdoes not exit 0.cargo testuses ARM semihosting so QEMU exits 0 on pass and 1 on panic (force-fail).- Current-EL
BRKis handled viaVBAR_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/Vecon 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-
.dataand 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-kernelstub at0x40080000stays. - 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.texttear (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
qemu-system-aarch64 -machine virt -cpu cortex-a57(ormax;gic-version=3is allowed).-kernelloads the kernel ELF into virt RAM (linker base0x40080000).- Entry is
_start(assembly insrc/main.rs): set SP, zero BSS, callkernel_main. - Early output is the virt PL011 UART at
0x0900_0000(src/uart.rs). kernel_maininstallsVBAR_EL1(src/exception.rs, ADR-004) after UART init, then the EL1 identity map plus a TTBR1 private page and a cloned TTBR1 RAM alias (ADR-008, W^X split ADR-012, stack guards ADR-014, RO+NX ADR-015, TTBR1 first cut ADR-016, EL1 fetch mile ADR-017, identity-tear first cut ADR-018, identity.textrange tear ADR-019, live.texttear ADR-020), then the frame pool after MMU +SCTLR.C(pre-MMU.bssstores can vanish), then the first-fit heap (ADR-009), then the cooperative scheduler (ADR-010), then GICv2 + CNTP (ADR-006). AfterHello World!it proves map/unmap,Box/Vec, two tasks, heap NX, linker-stack guards, the EL0 first mile + standing context, ASID isolation, the TTBR1 private page and high-VA EL1 fetch, torn identity.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)
Pl011writer with TX-full wait and\n→\r\n;try_recvonUARTFR.RXFE/UARTDRprint!/println!viaspin::Mutex; fatal / unhandled / IRQ paths write the PL011 without the mutex- After EL1 + VBAR, a bump frame allocator (
src/frame.rs) and identity map (src/paging.rs) turn the MMU on (ADR-008); RAM after__kernel_endis PXN (ADR-012); 4 KiB holes sit under the linker stacks (ADR-014) - First-fit
GlobalAlloc(src/heap.rs) on a 64 KiB identity-mapped frame run (ADR-009);build-stdincludesalloc - Cooperative round-robin (
src/sched.rs): idle on the linker thread stack, two heap-backed workers, AAPCS64 callee-saved yield (ADR-010) kernel_mainprintsHello World!, proves map/unmap (serialpaging: ok), provesBox/Vec(serialheap: ok), proves two tasks (serialsched: task a/sched: task b/sched: ok), proves heap NX (serialwx: ok), proves linker-stack guards (serialguard: ok), proves the EL0 first mile + standing context (serialel0: ok/el0: standing/el0: restored), proves ASID isolation (serialasid: ok), proves the TTBR1 private page and high-VA EL1 fetch (serialttbr1: ok/ttbr1: el1 exec/ttbr1: vbar), proves the identity-tear first cut,.textrange tear, vtable rewrite, and live.texttear (serialident: ok/ident: jump/ident: reloc/ident: range/ident: live/ident: text/ident: split/ident: fault/ident: high/ident: no el0), proves CNTPCT advances (serialperf: cntpct delta=…), observes CNTP ticks (serialtimer: tick) plus IRQ-to-handler deltas (serialperf: irq-delta), polls one host-injected RX byte (serialinput: rx 0x41), fires one healthy-stackBRK #0(serialexception: sync BRK), then the FR-07 nest probe (serialexception: fatal nested)VBAR_EL1vector table (high alias after MMU, ADR-017); kernel runs onSP_EL0; first-level current-EL sync (SP_EL0 bank) handles AArch64BRK, heap NX, guard-page data aborts, and the identity-tear IABORT; first-level IRQ handles GICv2 PPI 30; lower-EL AArch64 sync handles the EL0 SVC / UXN IABORT / standing dual-SVC / 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 testuses#![feature(custom_test_frameworks)]and#[test_case](including VBAR, BRK, SPSel, stack ranges, guards, GIC TYPER, CNTFRQ, timer tick, IRQ-delta samples, empty UART RX FIFO, MMU on, frames, map/unmap, heapBox/Vec, two-task yield, CNTPCT loop, W^X flags + execute-from-heap, EL0 first mile + standing enter/leave, ASID isolation, TTBR1 private page + high-VA EL1 fetch, identity-tear first cut +.textrange tear + vtable rewrite + live.texttear;is_active()is false at rest)- QEMU exit is ARM semihosting
SYS_EXIT/hlt #0xf000(src/qemu.rs), notisa-debug-exit. Needs-semihostingon the QEMU line (scripts/qemu-aarch64.sh). - Host smoke:
scripts/qemu-smoke.sh(hello + paging + heap + two-task sched + W^X + guards + EL0 first mile + standing + ASID isolation + TTBR1 private page + high-VA EL1 fetch + identity-tear first cut + identity.textrange tear + vtable rewrite + live.texttear + CNTPCT baseline + IRQ-delta + host ELF size + timer tick + injected UART RX + BRK + fatal nested strings + tests +force-failmust be non-zero) - Docker:
Dockerfile/scripts/docker-smoke.sh(linux/arm64-friendly; do not pin amd64) - GHA:
.github/workflows/smoke.yml(ubuntu-24.04-armandubuntu-24.04)
Source + local smoke were first probed on 2026-09-08 (see the honesty ledger). Current idle tip is main ≈ e80dc93 (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.link → artofdream.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
| Stage | Domain work |
|---|---|
| Custom test framework | Landed (M2): #[test_case], semihosting exit, UART |
| CPU exceptions | M3: VBAR_EL1, resumable BRK. M4: dedicated exception + fatal stacks (FR-07) — cloud qemu-smoke Verified (honesty ledger); GHA Unknown until a run URL |
| Hardware interrupts | M5: GICv2 + CNTP tick (FR-08) — cloud qemu-smoke + GHA Verified (honesty ledger). M6: PL011 UART RX (FR-08 input / ADR-007) |
| Paging | M7: EL1 identity map + bump frames (FR-09 / ADR-008) — probe status in the honesty ledger. DTB walk staged. |
| Heap | M8: first-fit GlobalAlloc on identity-mapped frames (FR-10 / ADR-009) — probe status in the honesty ledger. |
| Scheduler | M9: cooperative EL1 yield (FR-11 / ADR-010) — probe status in the honesty ledger. Not preemptive. |
| Pillars | ADR-011: antifragility / security / performance. Threat-model v1.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). |
| Filesystem | None today. Planned order (no FR ID): in-RAM memfs → virtio-blk → FAT or xv6-like. Stance: filesystem.md. |
| Host apps / containers | Gaps to Linux/shell/Python: host-apps.md. Guest container runtime is a non-goal. Host docker-smoke is unrelated. |
| Immutability | Scoped RO only (immutability.md). Absolute “immutable OS” is incompatible. Track A #31 / Track B #40. |
Each stage is one loop unit on the roadmap.
x86_64 remains a possible future secondary ISA. It is not a current tree.
Harness mapping (short)
Hardware and QEMU are the domain. Docs, ADRs, and this architecture note are shared understanding. Guides, sensors, the one-PR loop, second-brain vaults, merge permissions, and the honesty ledger are the outer harness. Details: formula.md.
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:
- Honesty — every status word is a claim; the honesty ledger records the probe. Unprobed = Unknown. Never round Unknown up to Verified.
- Document-first — vision, architecture, ADRs, and roadmap land before more kernel features.
- Second brain — four vaults under
research/(procedure via skills, correction via constraints/rules, relationship via doc links, daily brief / session memory). - Thin roles — Knowledge Guardian, Coherence Guardian, Kernel Engineer, MR Coordinator (
ctos-*skills only). - Loop — one milestone → one branch → one GitHub PR. The author does not self-approve or merge (see ADR-002).
- 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 runwithout 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 introduceaea-*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:
- Do not enable GitHub author self-APPROVE. Do not add a required-approval ruleset until a second human exists.
- Author does not merge their own PR.
- Identities:
artofdream(owner) andcursor[bot](Cursor GitHub App, already used for cloud PRs). Do not install another App or mint a second personal account for this. - Written MRC (
ctos-mr-coordinator) is the review-in-the-room. PreferCOMMENT/REQUEST_CHANGES. Record who authored / who reviewed / who merges on the PR. - If
cursor[bot]RESTAPPROVEreturns 403 (probed on Café Fausse #27; unprobed on ctos), do not block a validcursor[bot]merge of anartofdream-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. - Owner PAT /
artofdreamstill must notAPPROVEor merge anartofdream-authored PR.
| Who opened the PR | Who writes the review | Who merges |
|---|---|---|
artofdream | New MRC session (COMMENT) | cursor[bot] after green + Bugbot terminal |
cursor[bot] (cloud / this agent) | Owner, optionally plus MRC COMMENT | artofdream |
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 AppAPPROVEon 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
- Primary ISA is AArch64 (arm64). The checked-in custom target is
aarch64-ctos.json(aarch64-unknown-nonestyle:os: none, panic abort, static relocation, soft-float so early boot does not require enabling SIMD/FP inCPACR_EL1). - Boot path is QEMU
-kernelof the kernel ELF on-machine virt(orvirt,gic-version=3) with-cpu cortex-a57ormax. Entry is_startat a linker-script address in virt RAM. Nobootloader0.9 crate and nobootimagedisk as the primary path. - Console is the virt PL011 UART (MMIO
0x0900_0000) viaprint!/println!. Not VGA0xb8000.earlyconremains an allowed later probe; this ADR does not claim it. - Do not claim Raspberry Pi or other board support until a board-specific probe exists. virt is the supported guest.
- 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.tomldefault target isaarch64-ctos.json.json-target-specstays for current nightly.- Roadmap M0/M1 probes are UART +
qemu-system-aarch64, not a VGA dump at0xb8e60. - 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+BRKpath. - 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
- Live at EL1.
exception::initcallsensure_el1: ifCurrentELis EL2, setHCR_EL2.RW, copySPtoSP_EL1, andERETto EL1h (DAIF masked). If the EL is not 1 after that, print and park. Do not treatVBAR_EL2as the primary table. - One 2 KiB-aligned table at
exception_vectors, written toVBAR_EL1. All sixteen AArch64 slots exist. Current EL / SP_EL0 / synchronous saves a frame and mayERET. Current EL / SP_EL0 / IRQ is live as of ADR-006. Every other slot parks (UART line +wfe, or semihosting fail undercargo test/force-fail). - Context format (current-EL sync only):
x0–x29,x30,ELR_EL1,SPSR_EL1,ESR_EL1. No SIMD/FP save (soft-float, ADR-003). BRKis resumable. ESR exception class0x3C(AArch64BRK) increments a counter, printsexception: sync BRK, adds 4 toELR_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 executebrk #0and continue. The hello kernel fires oneBRKsoscripts/qemu-smoke.shcan 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) splitsSP_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
- Thread stack on
SP_EL0. AfterVBAR_EL1is installed, copy the_startstack intoSP_EL0(MSR SP_EL0is legal at EL1), write__exc_stack_topintoSPwhileSPSelis still 1 (that isSP_EL1—MSR SP_EL1at EL1 is UNDEF), thenmsr spsel, #0. Normal kernel code uses the 64 KiB thread stack. - 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 offset0x200(M3 / SP_ELx) to0x000. - Fatal stack before any nested store. Current-EL / SP_ELx slots (
0x200–0x380) loadSPfrom__fatal_stack_top(8 KiB) in asm, then callhandle_fatal_exception. Do not push a frame on the exception stack that may already be exhausted. - Raw UART on fatal / unhandled paths. Write the PL011 without
spin::Mutexso a nest duringprintln!still produces serial. - Probe is nested
BRKafter a near-empty thread SP. The hello kernel fires a healthy-stackBRK(M3 string), then sets a flag, movesSP_EL0to__stack_bottom + 64(less than the 272-byte frame), andBRKs again. The first-level handler printsexception: sync BRKfromSP_EL1, thenBRKs while SPSel = 1. That nest must printexception: fatal nestedand park. This is not an MMU stack-overflow fault and not a GIC path (M5 / FR-08).
Consequences
#[test_case]can stillbrk #0and return. Tests do not set the nest flag.scripts/qemu-smoke.shrequires the fatal marker in addition to hello + BRK. Afatal probe missedline 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-
BRKprobe 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
- Program GICv2, matching default
-machine virt. Do not add a GICv3 redistributor driver in this milestone. If a later QEMU default flips to v3, pingic-version=2on the smoke line or add a v3 driver in a new ADR. - Use the non-secure EL1 physical timer (
CNTP_CTL_EL0/CNTP_TVAL_EL0/CNTPCT_EL0) and PPI 30. Ifensure_el1drops from EL2, setCNTHCTL_EL2.EL1PCTEN|EL1PCENand zeroCNTVOFF_EL2so EL1 can use the counter and physical timer. - Take IRQs on the current-EL / SP_EL0 bank (vector offset
0x080). The kernel runs withSPSel = 0, so first-level IRQs useSP_EL1(the M4 exception stack). Save the same GPR + ELR/SPSR/ESR frame as sync, then EOI. - Keep DAIF.I masked except for an observe window. Hello and
#[test_case]unmask, wait for one tick (or aCNTPCTtimeout), then remask and stop the timer so M3BRK/ M4 nested fatal are not interrupted. - Raw UART on the IRQ path. The handler must not take
spin::Mutex(same reason as ADR-005). First tick printstimer: tick.
Consequences
scripts/qemu-smoke.shrequirestimer: tickand rejectstimer: 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:
- PL011 UART RX at
0x0900_0000(already the console). - virtio-input / virtio-keyboard (virtio-mmio, virtqueues, DMA). No paging or heap yet (M7/M8). Too much device stack for one milestone.
- 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.cstill 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
- 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.
- Prove a received byte with a host inject:
scripts/qemu-serial-inject.pywrites0x41('A') to QEMU stdin after it seesHello World!. The hello kernel pollsUARTFR.RXFE/UARTDR(DAIF.I still masked after the timer window) and printsinput: rx 0x41. - Fail closed. Missing marker or
input: rx missedfailsscripts/qemu-smoke.sh.#[test_case]only asserts the FIFO is empty when cargo test does not inject — the character proof is the serial smoke. - Do not claim Raspberry Pi UART, virtio-input, GICv3 UART SPI, or QEMU LBE.
Consequences
scripts/qemu-smoke.shneedspython3to drive the inject (Dockerfile installs it).cargo test/scripts/qemu-aarch64.shstay 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):
| Region | Physical |
|---|---|
| flash / boot ROM | 0x0000_0000 |
| GICv2 distributor / CPU interface | 0x0800_0000 / 0x0801_0000 |
| PL011 UART | 0x0900_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 GiB L1 identity blocks (39-bit VA,
T0SZ=25, 4 KiB granule) plus a dedicated 4 KiB map window. - Fine-grained 4 KiB identity of every used page (more tables, same probe).
- Higher-half kernel (breaks the current linker / VBAR / UART absolute addresses).
- Parse the DTB before enabling the MMU (correct long-term; not the M7 fail-closed proof).
Decision
- Enable the MMU at EL1 after
exception::init(ensure_el1). ProgramMAIR_EL1(Attr0 Device-nGnRnE, Attr1 Normal WB),TCR_EL1(TTBR0 only, 40-bit IPS, inner-shareable WB),TTBR0_EL1, then setSCTLR_EL1.M|C|I|SA. Identity: VA == PA for everything the kernel already touches. - L1 blocks, not a full 4 KiB identity. Entry 0 (
0x0–1 GiB) is Device-nGnRnE + XN (UART, GIC, flash). Entry 1 (0x4000_0000–0x7FFF_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 pastRAM_BASE + 128 MiB. - Frame pool from linker + virt convention.
__kernel_end(after image + stacks, 4 KiB-aligned) through0x4000_0000 + 128 MiB. Bump cursor plus a 16-entry free stack. Do not allocate0x4000_0000–0x4008_0000(DTB / TEXT_OFFSET hole). Smoke pins-m 128Mso the pool matches the VM. - Map/unmap probe uses a third GiB window (
0x8000_0000) via one L2 + L3 table, not by splitting the RAM block. Serial markerpaging: ok. Do not load an unmapped VA (that would take an unhandled data abort). - 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. - No
GlobalAlloc. Frames are physical pages only.
Consequences
scripts/qemu-smoke.shrequirespaging: okand rejectspaging: probe missed, then still requires M2–M6 strings.#[test_case]can assertSCTLR_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:
- Bump-only heap (no
dealloc).Box/Vecconstruct, but drop leaks. Reuse is unproved. linked_list_allocatorcrate. Common rust-osdev choice; extra dependency for one milestone.- First-fit free list on a fixed contiguous frame run taken from the bump pool after the MMU is on.
- Grow-on-demand via the M7 map window (
0x8000_0000). Extra paging surface; not needed to prove FR-10. - Buddy / slab. Overkill for a Box/Vec smoke.
Decision
- Fixed 64 KiB heap (16 frames) via
frame::alloc_contiguousafterpaging::init. Identity VA == PA. Do not map through the M7 probe window. - First-fit + address-sorted coalesce in
src/heap.rs.deallocis real; the hello probe requires a freedBoxpointer to be reused. #[global_allocator]+extern crate alloc..cargo/config.tomlbuild-stdincludesalloc. OOM returns a null pointer;#[alloc_error_handler]printsheap: oomon the raw UART and parks (orSYS_EXIT1 under test). The handler must not format — formatting an OOM can allocate again.- Serial marker
heap: ok. Fail closed onheap: probe missed.#[test_case]covers pool bounds,Boxreuse, andVecgrowth. - 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.shrequiresheap: okafterpaging: 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:
- Async/await executor. Extra
Futureinfrastructure for two markers. - Timer preemption. Would steal the M5 CNTP path and needs IRQ-safe yield. Not required to prove FR-11.
- Cooperative yield that saves AAPCS64 callee-saved GPRs on a per-task stack and switches
SP. - 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 x0–x30 + ELR/SPSR/ESR on SP_EL1). A yield must not touch the exception stacks.
Decision
- 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). - AAPCS64 callee-saved switch in
context_switch: save/restorex19–x28,x29,x30on the task stack; store SP in the task slot; load the next SP. No SIMD/FP (ADR-003 soft-float). Not the exception context. - 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 overflowSP_EL0duringBox::new). Identity VA == PA. - Round-robin among
Readyslots. A worker that returns isDone(trampoline). Idle staysReadyso control returns to the caller ofyield_now. Unlock the scheduler mutex beforecontext_switch. - Serial markers
sched: task a,sched: task b, thensched: okafter both SPs land on distinct heap stacks. Fail closed onsched: probe missed. - Not preemptive, not SMP, not EL0, not async. A later preemptive or process ADR is a new file.
Consequences
scripts/qemu-smoke.shrequires the threesched:strings afterheap: ok, then still requires M2–M8 strings.- Two 8 KiB worker stacks come out of the 64 KiB heap. M8
Box/Vecprobes 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
-
Three first-class pillars, same weight, different sensors:
- Antifragility — keep the existing harness: honesty ledger, fail-closed
scripts/qemu-smoke.sh, GHAsmoke.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, nosched::yield_nowfrom 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.
- Antifragility — keep the existing harness: honesty ledger, fail-closed
-
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.”
-
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.
-
One small code ratchet may land with this docs PR. This change lands a baseline
CNTPCTloop delta (serialperf: cntpct+#[test_case]). It does not split the L1 RAM block to fake W^X. Honesty over heroics. -
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
.textrange tear is ADR-019. The live.texttear 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
- 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 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_startandKERNEL_TEXT, which can be two blocks). - 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 below0x4008_0000when that hole is in the straddling L3. Device L1 stays XN. The M7 map window L3 pages are PXN (data-only). - 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).
- Fail-closed probe: walk PXN on kernel text (clear) and heap (set), then write
RETon the heap,blrto it, and catch a current-EL permission instruction abort. Serialwx: nx heap+wx: ok.scripts/qemu-smoke.shgrepswx: okand rejectswx: probe missed. - 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.rsgrowsL2_RAMand 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.WXNare 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
- First mile (this tree). Deliberate
ERETto EL0t (DAIF masked) on one map-window page that is UXN-clear and PXN (EL1 must not fetch it). Two paths:SVC #0taken on the lower-EL AArch64 sync slot; handler printsel0: svcandERETs back to EL1t.BR X0to a kernel.databait; UXN on the identity image must take a lower-EL permission IABORT; handler printsel0: nx kerneland returns to EL1t. Serialel0: okafter the first-mile pair and the later miles on this path. The first mile itself does not install a standing context.
- 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. - User TTBR0 window (this tree, post-#19). A second L1 (
L1_USER) is installed onERETto EL0 (ASID=1 in TTBR0[63:48]). It maps kernel.text/.rodataand the exception stack so the lower-EL handler can restore kernel TTBR0 (kept inTPIDR_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 EL0LDRfrom kernel.datais a lower-EL translation (or permission) DABORT (el0: no kernel read). The EL0 trampoline stillTLBI VMALLE1because kernel.dataleaves are global (nG=0); dropping that flush would leak a cached.datatranslation into the user ASID. Programming ASID=1 on that path is still not the isolation mile. - 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 isMSR TTBR0+ISB— noTLBI 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.” - Standing EL0 (this tree). A bounded user context on the user TTBR0:
SVC #1printsel0: standingand ERETs back to EL0; the userMOVZmust run;SVC #2printsel0: restoredand 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. - Isolation remains Planned. Missing: PAN (typically unimplemented on
-cpu cortex-a57/ ARMv8.0 — readID_AA64MMFR1_EL1.PANbefore 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. - 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).
- 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.textrange tear is ADR-019. The live.texttear 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:
- Unmapped 4 KiB guard page(s) below each linker stack. Overflow becomes a current-EL data abort (translation). Observable serial +
#[test_case]. - PXN on stack pages after splitting them from text. Stops execute-from-stack; overflow still smashes mapped data unless we also unmap something.
SCTLR_EL1.WXN— still unusable while kernel text pages are writable.
Decision
- Option 1. Insert a 4 KiB linker hole below the thread, exception, and fatal stacks (
__stack_guard,__exc_stack_guard,__fatal_stack_guard). Afterfill_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. - 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.shgrepsguard: okand rejectsguard: probe missed. - 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. - NFR-10 text is revised in place (ID unchanged): mention the guard-page overflow cut. Do not mint NFR-15+.
- The ADR-005 nested-
BRKfatal probe stays. It does not write below__stack_bottom.
Consequences
src/paging.rsmay add one or two extra L3 tables to split an executable L2 block that is not the__kernel_endstraddle.- 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
- Page-align
__data_startinlinker.ldso.text/.rodatanever share a 4 KiB page with.data. - 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).
SCTLR_EL1.WXNon once text is RO. Writable pages are treated as XN even if a descriptor forgets PXN.- Fail-closed probe: execute-from-
.data(blrto aRETbait) is a current-EL permission IABORT (ro: nx data). Store to RO text is a current-EL permission DABORT (ro: write fault). Serialro: ok.scripts/qemu-smoke.shgreps those strings and rejectsro: probe missed.#[test_case]covers flags + both faults. - NFR-10 text is revised in place (ID unchanged): mention the RO+NX image cut. Do not mint NFR-15+.
- 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_startand the first RAM 2 MiB that containsKERNEL_TEXT(0x4008_0000). Those are different blocks once.text/.rodata(or a linker ratchet) crosses0x4020_0000. Reusing oneL3_RAMoverwrites 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. - Post-MMU
.bsspublishes (same follow-up).USER_MAP_OKandframe::initrun afterSCTLR.C. A pre-MMU store to.bsscan be invisible to a later cached read (boot-delta on the PR #20 test image; cts-ai Docker hello onb2bbb99lost 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.rssets AP[2] on text and PXN on data/stacks.src/ro.rsowns 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.ldparks__data_startat0x4020_1000so 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
- 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). - High-half base. The TTBR1 region is
0xFFFF_FF80_0000_0000…0xFFFF_FFFF_FFFF_FFFF. One private page lives atTTBR1_PRIV = 0xFFFF_FF80_0000_0000(src/paging.rs). - 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.
- Fail-closed probe. EL1 store/load via the high VA prints
ttbr1: el1. An EL0LDRofTTBR1_PRIVis a lower-EL permission (or translation) DABORT (ttbr1: no el0). Serialttbr1: ok.scripts/qemu-smoke.shgreps those strings and rejectsttbr1: probe missed/ttbr1: leaked.#[test_case]covers the same path. - Identity map stays.
_start, vectors, linker stacks, PL0110x0900_0000, and the multi-L3 / layout ratchets from #21 keep using TTBR0 identity VAs. Full higher-half + identity teardown is Planned. - 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::initprogramsTTBR1_EL1and leavesEPD1clear.src/ttbr1.rsowns 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.textafter 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
- RAM alias.
L1_HIGH[1]points at the sameL2_RAMused by identity TTBR0. High VA isidentity + TTBR1_BASE(0x4008_0000→0xFFFF_FF80_4008_0000). Permissions match the identity image (text RO+X, data/stacks/heap RW+NX, guards unmapped). - 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. - EL1 exec probe.
ttbr1_high_el1_pathis invoked through its high VA (BLR/ fn pointer). It captures PC withADR, printsttbr1: el1 execfrom that path, and returns the PC. The caller checksPCis in the TTBR1 window and near the high entry. UART MMIO stays the absolute identity0x0900_0000. VBAR_EL1high. After the MMU is on,VBAR_EL1is programmed to the high alias ofexception_vectors(ttbr1: vbar). Later BRK / IRQ / EL0 sync fetch the table via TTBR1. Identityexception::initstill installs the link address first (pre-MMU).- Identity boot stub stays.
_start, QEMU-kernelload, and link-timefnitems remain at0x4008_0000. Most EL1 data access still uses identity VAs. Do not say the kernel moved. - 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. - NFR-10 text is revised in place (ID unchanged). Do not mint NFR-15+.
- 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 programmingTTBR0or comparingFAR_EL1. First attempts Failed: guardFARcompared to a high__stack_guard, then standingstay_at_el0programmed a highL1_USERas TTBR0.paging::identity_pa/linker_symare 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::initinstalls the RAM alias and relocates VBAR.src/ttbr1.rsowns 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
.textafter 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
- Split RAM tables. After identity W^X + stack guards, clone
L2_RAM(and every L3 it points at) intoL2_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. - Dedicated tear page. The linker keeps a 4 KiB
ident_tearsection (__ident_tear_start…__ident_tear_end) in the RO+X image, after.rodataand before__data_start. It is not the0x4008_0000_startpage. - Unmap after MMU + high VBAR.
paging::initturns the MMU on, programs the high VBAR, then clears the identity and user-TTBR0 L3 slots for that page andTLBI VAAE1s the low VA. The high twin stays PXN-clear. - Fail-closed probe. Serial
ident: split(clone + torn low / live high walks). EL1BLRof the low VA is a current-EL translation IABORT (ident: fault). EL1BLRof the high twin still runsident_tear_el1_path(ident: high). EL0LDRof the low VA is a lower-EL translation DABORT (ident: no el0). Serialident: ok.scripts/qemu-smoke.shgreps those strings and rejectsident: probe missed/ident: leaked. - Identity boot stub stays.
_start, QEMU-kernelload, remaining identity.text/.data/heap, PL0110x0900_0000, and later probes keep using TTBR0 identity VAs. Do not say the kernel moved. - 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. - 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::initclones RAM tables and unmaps__ident_tear_*.src/teardown.rsowns 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
- High-VA continuation. After
paging::init(MMU on, highVBAR_EL1, ADR-018 first tear page),kernel_mainBRs to the TTBR1 alias ofkernel_main_high. Serialident: jump. DirectBLstays in the high window (PC-relative). UART MMIO stays the absolute identity0x0900_0000. Stacks / heap /.datastay identity VAs. - Boot stub stays mapped. The
0x4008_0000page (_start+exception_vectors) stays identity-mapped. Live.textafter that page also stays — rustc fmt /dynstill needs those identity fn pointers. - Dedicated 16 KiB tear range. Linker
__ident_tear_start…__ident_tear_endis four pages: page 0 holdsident_tear_el1_path(ADR-018), page 1 holdsident_range_el1_path, pages 2–3 are aligned pad. After the high jump, unmap the whole range from kernel and user TTBR0 andTLBI VAAE1each low VA. High twins stay PXN-clear. - Keep live
.text/.rodata/.dataidentity-mapped. Do not yank them here. A botched full.textyank is worse than a smaller Verified slice. - Identity
fnpointers used after the jump go high where we control them (scheduler trampoline / task entries; custom test runnerdyn Testablemethod). That is preparation, not a claim that rustc fmt is high-only. - Fail-closed probe. Serial
ident: jump.ident: range lo=… hi=… pages=NwithN >= 4. Existingident: split/fault/high/no el0/okstay.ident: textis EL1 fetch of the second torn page via the high twin.scripts/qemu-smoke.shgreps those strings and rejectsident: probe missed/ident: leaked/ident: range missed. - Identity boot stub stays.
_start, QEMU-kernelload, live identity.text/.rodata/.data/heap, PL0110x0900_0000. Do not say the kernel moved. - Still Planned. Unmap live identity
.textafter fmt /dyncalls are proven high-only; then.rodata/.data/heap; PAN on-cpu cortex-a57; umbrella EL0 isolation. Do not change default-cpu. - 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
.textafter_startwas 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_rangeown the cut.src/teardown.rsextends 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
.textafter the boot stub..rodata/.data/ heap stay.
ADR-020 — High-VA fn-pointer rewrite + live identity .text tear
- Status: Accepted (live
.textafter 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 Writevtables live in.rodata. ThePl011table isdrop=0 / size=8 / align=8 / write_str / write_char / write_fmtwith identity method addresses (write_charat0x4008_1084, immediately after the vectors page). - The hello
.rodataheld a handful of other identity.textwords (Debug::fmt,PadAdapterasWrite)..datahad none..texthad no 8-byte identity literal pools. - AArch64 codegen used
ADRP(PC-relative), notMOVZ/MOVKof0x4008. After the high-VA jump, newfn()values are already high; tables linked asR_AARCH64_ABS64stay identity until rewritten. The ELF isET_EXEC/relocation-model: static— no leftover reloc records to apply. .textand.rodatashared page0x400a4000on the ADR-019 layout (vtable at0x400a4338). 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
- Page-split
.text/.rodata. Linker__text_end/__rodata_startare 4 KiB aligned so live.textand fmt tables do not share a page. - 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.textor__ident_tear_*is rewritten to its TTBR1 alias. RO pages are made writable only for that store, then restored (SCTLR.WXNmakes a writable page XN — they are not fetched). Serialident: reloc n=NwithN >= 1. APl011 as dyn Writefat-pointer read must see a highwrite_str. - Live identity
.texttear. Unmap[0x4008_1000, __text_end)from kernel and user TTBR0 (TLBI VAAE1each low VA). High twins stay PXN-clear. The0x4008_0000page (_start+exception_vectors) stays identity-mapped. Serialident: live lo=… hi=… pages=NwithN >= 8. - Keep
.rodata/.data/ heap identity-mapped. String literals and rewritten vtables still live there. Do not yank them here. - Keep
_start/ QEMU-kernelat0x4008_0000. Do not change default-cpu. Do not claim PAN. - Fail-closed probe. Existing
ident: jump/range/split/fault/high/text/no el0/okstay. Newident: reloc/ident: live.scripts/qemu-smoke.shgreps those strings and rejectsident: reloc missed/ident: live missed.#[test_case]covers the rewrite and the live unmap. - Still Planned. Unmap identity
.rodata/.data/ heap after those accesses are proven high-only; PAN on-cpu cortex-a57; umbrella EL0 isolation. - 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_textown the cut. ADR-019 remains the high-VA jump + dedicated 16 KiB range.- A later ADR may unmap
.rodataonce 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.
| ID | Milestone | Probe that closes it | Status |
|---|---|---|---|
| M0 | Kernel tree + harness scaffold on GitHub | Files on the default-target PR; ledger started | In harness PRs #2/#3 (source). Merge is a human/MRC job. |
| M1 | QEMU virt boots UART “Hello World!” | qemu-system-aarch64 serial shows the string; see honesty ledger | Verified in the 2026-09-08 cloud probe (not CI). x86 VGA probe is historical only. |
| M2 | Integration test harness | QEMU virt ARM semihosting exit + #[test_case]; scripts/qemu-smoke.sh fail-closed | Verified in the 2026-09-08 cloud probe (not CI). Sponsor stacked this on the ISA PR. |
| M3 | Exception vectors + breakpoint | Test or QEMU serial proof the handler runs | Verified: 2026-09-09 cloud qemu-smoke + GHA smoke.yml on the M3 PR (see honesty ledger). |
| M4 | Fatal exception stack | Fatal path does not silently lock the VM | Verified: 2026-09-09 cloud qemu-smoke (see honesty ledger). GHA on the M4 PR branch still Unknown. |
| M5 | Hardware 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). |
| M6 | Input | Injected 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. |
| M7 | Paging + frame allocator | Map/unmap or allocator test | Verified: 2026-09-09 cloud qemu-smoke (see honesty ledger). GHA on the M7 PR branch still Unknown. |
| M8 | Heap (alloc) | Box/vec smoke on the heap | Verified: 2026-09-09 cloud qemu-smoke + GHA smoke.yml on the M8 PR (see honesty ledger). |
| M9 | Cooperative scheduler | Two tasks observed to run | Verified: 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.
| ID | Work | Probe that closes it | Status |
|---|---|---|---|
| P-SEC-1 | Threat-model v1.1 slice (NFR-10) | Read security.md; no “secure OS” claim | Verified (file + review). v1.1 adds linker stacks, guards, EL0 first mile, remaining W^X gaps. |
| P-SEC-2 | W^X / NX heap + coop stacks | Page-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-2b | Linker-stack guard pages | Unmapped 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-1 | Baseline CNTPCT probe (NFR-07) | Serial perf: cntpct and/or #[test_case]; not a published bench | Verified: 2026-09-09 cloud qemu-smoke + GHA (see honesty ledger). |
| P-PERF-2 | IRQ-to-handler CNTPCT delta | Serial perf: irq-delta + samples max >= min; not a latency budget | Verified: 2026-09-10 cloud qemu-smoke (see honesty ledger). |
| P-PERF-3 | Host debug ELF size (NFR-08) | Host marker perf: elf-size bytes=<n>; not a budget | Verified: 2026-09-10 cloud qemu-smoke host print (see honesty ledger). |
| P-SEC-3 | EL0 first mile | el0: svc + el0: nx kernel + el0: ok | First mile Verified: 2026-09-10 cloud qemu-smoke (see honesty ledger). |
| P-SEC-2c | RO+NX text/data (ADR-015) | ro: nx data + ro: write fault + ro: ok | Verified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Identity image W^X on virt; not “secure OS.” |
| P-PERF-4 | Boot-to-ready CNTPCT (NFR-08) | perf: boot-delta ticks=<n>; not a budget | Verified: 2026-09-11 cloud qemu-smoke (see honesty ledger). |
| P-SEC-3b | User TTBR0 + EL0 cannot read kernel .data | el0: no kernel read; user table omits .data | Verified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Isolation / PAN stay Planned. |
| P-SEC-3c | ASID-tagged TLB isolation | asid: dual + asid: conflict + asid: ok; no TLBI VMALLE1 on the switch | Verified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Umbrella isolation / PAN stay Planned. |
| P-SEC-3d | Standing EL0 context | el0: standing + el0: restored; is_active() true only while standing | Verified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Not POSIX. Lower-EL IRQ still parks. |
| P-SEC-3e | TTBR1 kernel-private page (ADR-016 first cut) | ttbr1: el1 + ttbr1: no el0 + ttbr1: ok | Verified: 2026-09-11 cloud qemu-smoke (see honesty ledger). |
| P-SEC-3f | EL1 fetch from TTBR1 high VA (ADR-017) | ttbr1: el1 exec + ttbr1: vbar + existing ttbr1: ok | Verified: 2026-09-11 cloud qemu-smoke (see honesty ledger). Identity boot stub stays. |
| P-SEC-3g | Identity-tear first cut (ADR-018) | ident: split + ident: fault + ident: high + ident: no el0 + ident: ok | Verified: 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-3h | High-VA jump + 16 KiB identity text range (ADR-019) | ident: jump + ident: range + ident: text + existing ident: ok | Verified: 2026-09-11 cloud qemu-smoke + cts-ai Docker on 24d94e6 (see honesty ledger). Do not claim “the kernel moved.” |
| P-SEC-3i | High-VA vtable rewrite + live identity .text tear (ADR-020) | ident: reloc + ident: live + existing ident: ok; println! after the tear | Verified: 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-3j | Identity .rodata / .data / heap tear | Those identity ranges unmapped; accesses proven high-only | Planned. After live .text (ADR-020), not instead of it. |
| P-SEC-3k | PAN on virt cortex-a57 | ID_AA64MMFR1_EL1.PAN != 0 and an EL1-vs-EL0 access fault | Planned. ARMv8.0 cortex-a57. Do not switch -cpu silently. |
| P-SEC-3l | Umbrella EL0 isolation | Standing + PAN + full TTBR1 / identity teardown | Planned. 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.
| ID | Work | Status (2026-09-11) |
|---|---|---|
| A1 | Stable SVC / syscall ABI + docs | Planned (#32) |
| A2 | Freestanding CRT / libctos | Planned (#33) |
| A3 | ELF (or raw) loader into user TTBR0 | Planned (#34) |
| A4 | Standing EL0 as normal mode | Planned (#35) |
| A5 | Isolation completion (remaining identity tear; PAN only if CPU + ADR) | Planned (#36) |
| A6 | Thin VFS + memfs | Planned (#37) |
| A7 | virtio-blk + FAT or xv6-like | Planned (#38) |
| A8 | Documented sample apps | Planned (#39) |
| A9 | Disconnect OS image from app payloads | Planned 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
| ID | Work | Status (2026-09-11) |
|---|---|---|
| B1 | ADR: Linux-compat goals & non-goals | Planned (#41) |
| B2 | Syscall surface map (Linux aarch64 vs ctos SVC) | Planned (#42) |
| B3 | Process model vs Linux (fork/exec/wait) | Planned (#43) |
| B4 | Linux ELF / auxv / PT_INTERP vs freestanding loader | Planned (#44) |
| B5 | Linux VFS concepts vs thin ctos VFS | Planned (#45) |
| B6 | Decision: compat layer vs reimplement vs never | Planned (#46) |
| B7 | Containers 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.
| Principle | What it means | Home |
|---|---|---|
| Honesty ledger | Status words need a probe. Unprobed stays Unknown. No rounding up. | honesty-ledger.md |
| Antifragility | Fail-closed sensors. Repeated failures become ratchets, not README advice. | antifragility.md / NFR-05 |
| Security | Written threat model. Mitigations are claimed only when probed. No “secure OS.” | security.md / NFR-10 |
| Performance | Measure first. No invented benches. Optimize only after a probe. | performance.md / NFR-07 |
| Document-first / one-PR loops | One 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)
| Layer | On ctos | Prevents |
|---|---|---|
| Guides | AGENTS.md, .cursor/rules/, .cursor/skills/ctos-* | Out-of-scope work (wrong crate era, shop content, GitLab SOPs) |
| Sensors | scripts/qemu-smoke.sh, cargo test semihosting exit, GHA smoke.yml, optional Docker | Rounding “source exists” up to “it boots” |
| Loop | One milestone → one branch → one GitHub PR | Sprawling PRs that mix UART, paging, and docs rewrites |
| Memory | research/ vaults | Session amnesia; stuffing raw chat into the next prompt |
| Permissions | Author ≠ merger (artofdream vs cursor[bot]); MRC writes COMMENT; no GitHub self-APPROVE (ADR-002) | Same-login stamp counted as a second review |
| Observability | Honesty ledger + PR text that lists probed vs Unknown | Status 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).
| Claim | Probe | Status | Notes |
|---|---|---|---|
UART hello source present (println!("Hello World!"); + PL011 writer) | Read src/main.rs and src/uart.rs on this branch | Verified | Source inspection. Boot is a separate row. |
.cargo/config.toml present (build-std, aarch64-ctos.json, qemu runner, json-target-spec) | Read .cargo/config.toml | Verified | Presence. Build success is the next row. |
cargo build for aarch64-ctos.json | cargo +nightly build on 2026-09-08 (rustc 1.100.0-nightly cea272fa3) | Verified | First 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 World | qemu-system-aarch64 8.2.2, -machine virt -cpu cortex-a57 -display none -serial stdio -kernel target/aarch64-ctos/debug/ctos; timeout 4 | Verified | Serial 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 path | Historical probes on 2026-09-08 (PR #2) | Historical | Path removed by ADR-003. Those Verified rows do not apply to this tree. |
| CI on GitHub | Workflow file exists and a run is green | Verified | .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 0 | Verified | After 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 Brief | Verified | Structure present. Not a claim that vaults are richly filled. Optional Obsidian UI is structure-only (PR #3). |
Thin ctos-* roles | AGENTS.md + .cursor/skills/ctos-*/SKILL.md exist | Verified | Four roles. No aea-* names. |
Frozen FR/NFR IDs (FR-01–FR-15, NFR-01–NFR-14) | Read docs/02-requirements/fr-nfr.md; IDs present; ISA text revised under ADR-003 | Verified | Frozen 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.mdc | Verified | Docs 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 AArch64 | Read ADR-003; x86_64-ctos.json / src/vga_buffer.rs absent | Verified | Decision + 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 0 | Verified | 2026-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 panic | cargo +nightly test --features force-fail | Verified | 2026-09-08 and 2026-09-09 cloud: panic force-fail, QEMU/host exit 1. |
scripts/qemu-smoke.sh | Ran on this cloud VM | Verified | 2026-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_table | Verified | 2026-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 serial | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps exception: sync BRK | Verified | 2026-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 park | Source read of vector table; taken path is the EL0 first-mile row | Verified | Source: 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-bd73 | Verified | Push 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_aligned | Verified | 2026-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.sh | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps exception: fatal nested and rejects fatal probe missed | Verified | 2026-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-c8b7 | Unknown | Not 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_observable | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps timer: tick and rejects timer: tick missed | Verified | 2026-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-d5a6 | Verified | Push/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.py | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps input: rx 0x41 and rejects input: rx missed | Verified | 2026-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_byte | Verified | 2026-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-7ef6 | Unknown | No 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_enabled | Verified | 2026-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_probe | Verified | 2026-09-09 cloud: both [ok]. Write-through window VA 0x8000_0000 matches identity PA. |
| qemu-smoke requires paging string | scripts/qemu-smoke.sh hello phase greps paging: ok and rejects paging: probe missed | Verified | 2026-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-b567 | Unknown | No 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_pool | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps heap: ok and rejects heap: probe missed | Verified | 2026-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-0ee8 | Verified | Push 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_both | Verified | 2026-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 strings | scripts/qemu-smoke.sh hello phase greps sched: ok and both task markers; rejects sched: probe missed | Verified | 2026-09-09 cloud: qemu-smoke: scheduler strings present. Extends the M8 sensor (FR-11 / NFR-04). |
| qemu-smoke requires CNTPCT perf string | scripts/qemu-smoke.sh hello phase greps perf: cntpct and rejects perf: probe missed | Verified | 2026-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-7da9 | Verified | Folded 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 place | Read ADR-011 and fr-nfr.md; IDs still NFR-05/NFR-07/NFR-10 | Verified | Document inspection. Not a kernel boot claim. |
| Threat-model v1 exists (NFR-10) | Read security.md (assets, adversaries, trust boundaries, non-goals, mitigations→probes) | Verified | File + 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) | Verified | File + 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) | Verified | File + 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) | Verified | File + 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) | Verified | File + 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) | Verified | File + 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_caught | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps wx: ok and rejects wx: probe missed | Verified | 2026-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_loop | Verified | 2026-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_recorded | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps perf: irq-delta and rejects perf: irq-delta missed | Verified | 2026-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_caught | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps guard: ok and rejects guard: probe missed | Verified | 2026-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 < 4096 | Verified | 2026-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_roundtrip | Verified | 2026-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 data | Hello-kernel serial el0: nx kernel; #[test_case] el0_cannot_execute_kernel_data | Verified | 2026-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 teardown | Planned | Direction 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-text | Verified | 2026-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 strings | scripts/qemu-smoke.sh hello phase greps ro: ok / ro: nx data / ro: write fault | Verified | 2026-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_exists | Verified | 2026-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 string | scripts/qemu-smoke.sh hello phase greps perf: boot-delta | Verified | 2026-09-11 cloud: qemu-smoke: boot-delta perf string present. |
User TTBR0 omits kernel .data | #[test_case] user_ttbr0_omits_kernel_data; walk L1_USER | Verified | 2026-09-11 cloud cargo test: user_ttbr0_omits_kernel_data [ok]. ASID=1 programmed. Not ASID isolation. |
EL0 cannot read kernel .data | Hello-kernel serial el0: no kernel read; #[test_case] el0_cannot_read_kernel_data | Verified | 2026-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-a57 | ID_AA64MMFR1_EL1.PAN != 0 and an EL1-vs-EL0 access fault | Planned | ARMv8.0 cortex-a57. Do not claim PAN. |
| ASID isolation | Hello-kernel serial asid: dual + asid: conflict + asid: ok; #[test_case] asid_isolation_without_vmalle1; no TLBI VMALLE1 on the switch | Verified | 2026-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 context | Hello-kernel serial el0: standing + el0: restored; #[test_case] standing_el0_enter_leave; is_active() true only while standing | Verified | 2026-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_not | Verified | 2026-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 strings | scripts/qemu-smoke.sh hello phase greps el0: standing / el0: restored | Verified | 2026-09-11 cloud: qemu-smoke: EL0 first-mile + read-mile + standing strings present. |
| qemu-smoke requires TTBR1 strings | scripts/qemu-smoke.sh hello phase greps ttbr1: ok / ttbr1: el1 / ttbr1: no el0; rejects ttbr1: leaked and ttbr1: probe missed | Verified | 2026-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_text | Verified | 2026-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 strings | scripts/qemu-smoke.sh hello phase greps ttbr1: el1 exec / ttbr1: vbar; rejects ttbr1: exec missed | Verified | 2026-09-11 cloud: qemu-smoke: TTBR1 private-page + high-VA exec strings present. |
| Full higher-half / identity teardown | Identity .rodata/.data/heap unmapped after those accesses are proven high-only | Planned | ADR-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 tear | Those identity ranges unmapped; accesses proven high-only | Planned | After live .text. Not started on main e80dc93. |
| Docs site at https://ctos.artof.link | HTTP(S) fetch of the published Pages site on that host | Verified | 2026-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_stays | Verified | 2026-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_stays | Verified | 2026-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_stays | Verified | 2026-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 strings | scripts/qemu-smoke.sh hello phase greps ident: ok / ident: split / ident: fault / ident: high / ident: no el0; rejects ident: leaked and ident: probe missed | Verified | 2026-09-11 cloud: qemu-smoke: identity-tear strings present. |
| Obsidian open-vault checklist | Read research/obsidian-checklist.md; .gitignore has .obsidian/ | Verified | Structure 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-325f | Verified | Cloud-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 VM | Verified | 2026-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 VM | Verified | 2026-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-9bc0 | Verified | Cloud-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-48ae | Unknown | No 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 VM | Verified | 2026-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-aadc | Verified | Cloud-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 b2bbb99 | Failed | Sponsor 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 VM | Verified | 2026-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-c915 | Verified | Cloud-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 merge | Verified | Sponsor 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 strings | scripts/qemu-smoke.sh hello phase greps asid: ok / asid: dual / asid: conflict; rejects asid: stale and asid: probe missed | Verified | 2026-09-11 cloud: qemu-smoke: ASID isolation strings present. |
scripts/qemu-smoke.sh (ASID isolation / this branch) | Ran on this cloud VM | Verified | 2026-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-f98d | Unknown | No 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 VM | Verified | 2026-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-5ce0 | Unknown | No 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 VM | Verified | 2026-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-136c | Unknown | No 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 VM | Verified | 2026-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-c0fd | Unknown | No 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 merge | Verified | Sponsor 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 strings | scripts/qemu-smoke.sh hello phase greps ident: jump / ident: range / ident: text; rejects ident: range missed | Verified | 2026-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 VM | Verified | 2026-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 @ ac3ad0b | Failed | Both 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 24d94e6 | Verified | Push 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 merge | Verified | Sponsor 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 strings | scripts/qemu-smoke.sh hello phase greps ident: reloc / ident: live; rejects ident: reloc missed / ident: live missed | Verified | 2026-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 VM | Verified | 2026-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-322c | Unknown | No 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 e80dc93 | Verified | Push 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 merge | Verified | Sponsor 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.link | Verified | 2026-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 exists | Read .github/workflows/pages.yml | Verified | Workflow 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-c371 | Verified | PR 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-e4a9 | Verified | PR 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.link | Verified | 2026-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.link → artofdream.github.io | dig CNAME ctos.artof.link +short | Verified | 2026-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 Z1178AFMV41RWP | Sponsor stated CREATE done (zone Z1178AFMV41RWP); API list-resource-record-sets as that account | Unknown | This 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 reachability | Pages Settings/API lists the hostname and curl -sSI https://ctos.artof.link is HTTP 200 with a cert for that name | Verified | 2026-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 stack | Planned | Site: 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 marker | Planned | Site: 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/ | Verified | Absence + 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.md | Planned | Non-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) | Planned | After Track A ABI/loader (#31 A1–A4). Issue A9 #48. Today: one linked ELF — not Verified. |
| A9 slot-disconnect performance delta | perf: boot-delta plus a new app-load CNTPCT marker on a two-artifact boot | Planned | Expected 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
- Run or cite the probe (command, file path + revision, or
gh runURL). - Change only the rows you probed.
- 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.
| Pillar | Frozen ID | Home | What “done” looks like |
|---|---|---|---|
| Antifragility | NFR-05 | antifragility.md | Repeated failures become sensors/gates, not extra README advice |
| Security | NFR-10 | security.md | Threat-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. |
| Performance | NFR-07 | performance.md | Measurable 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
- Name the failure once in
research/random-thoughts/(what broke, command, error). - If it repeats, add a sensor: a script, a
#[test_case], a CI check, or a ledger row with a real probe command. - If people keep skipping the sensor, add a gate: CI required, or MRC refuses the PR.
- Only then tighten a guide (
AGENTS.mdor 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
b2bbb99Failed after PR #20 (paging: probe missed/heap: probe missed/sched: probe missed/el0: probe missedwhile guard/RO still printed). Same class as the PR #20 pre-MMU.bssmiss plus a single shared L3 / first-2-MiB user map. Sensor: layout L3 pool + post-MMU frame init (#21). Docker on71ee15fthen Verified. Keep the Failed row in the honesty ledger. - Live identity
.textyank Failed (unhandled sync on firstprintln!— rustcdyn Writevtables are identity fn pointers). Sensor / cut: ADR-019 kept live.textand tore a 16 KiB dedicated range. ADR-020 then rewrote those vtables to high aliases and tore live.text. cts-ai Docker + GHA one80dc93Verified that cut (ident: reloc n=12, live pages=37, 50 tests). Keep the Failed first yank. - PR #27
ac3ad0bGHA Failed (ident: rangethenident: probe missed— init still required a 4 KiB tear page). Sensor: 16 KiB range + relaxed init. Merge24d94e6GHA 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
| Asset | Why it matters | Where it lives |
|---|---|---|
| Kernel integrity | Text, 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-none | Credentials 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 guest | Smoke / cargo test must still print markers and exit. A silent lockup hides bugs. | UART, GIC, timer, fail-closed scripts/qemu-smoke.sh |
| Heap + cooperative stacks | Writable 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 context | One 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 page | One EL1-only high page. Proves EL0 cannot load it. Not a relocated kernel. | paging::TTBR1_PRIV (ADR-016) |
| TTBR1 RAM alias / high VBAR | EL1 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 page | One dedicated identity text page unmapped from TTBR0. High twin stays. Not a relocated kernel. | __ident_tear_* (ADR-018) |
| Identity text range | Dedicated 16 KiB identity text range unmapped. High twins stay. Not a relocated kernel. | __ident_tear_* 16 KiB (ADR-019) |
Live identity .text | rustc 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 gaps | Shared 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 / sensors | PL011 is how we see whether a probe ran. | Device MMIO 0x0900_0000 |
Adversaries
| Adversary | In scope? | Notes |
|---|---|---|
Buggy kernel code (wrong store, bad unsafe, execute-from-heap, stack smash) | Yes | Primary 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 bounded | First 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 tree | Mostly out | M7 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 devices | Out | The guest does not program a DMA master. UART/GIC/timer are MMIO. A malicious virtio device is future surface. |
| Hostile QEMU or CI host | Out | Hypervisor / runner is trusted. Repo-secret leak is a host control (.gitignore, review), not a guest mitigation. |
| Network attacker | Out | No 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
.textrange tear + live.texttear, isolation Planned. Lower-EL AArch64 sync is taken (SVC / IABORT / DABORT). IRQ/FIQ/SError lower-EL slots still park. Dual ASID withoutTLBI VMALLE1is a probed mile (asid: ok). Standing dual-SVC flipsis_active(). TTBR1 private page is EL1-only (ttbr1: ok). EL1 can fetch a real path from the high RAM alias (ttbr1: el1 exec);VBAR_EL1is that alias (ttbr1: vbar). A 16 KiB dedicated identity text range is unmapped (ident: range); live identity.textafter_startis unmapped after a vtable rewrite (ident: reloc/ident: live);_startstays. Closing isolation still needs PAN and a full identity teardown. A caught load of.dataor ofTTBR1_PRIVis 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
.textafter a high-VA vtable rewrite;.rodata/.data/heap stay;_startstays at0x4008_0000).
Mitigations mapped to probes
| Mitigation | Probe | Ledger |
|---|---|---|
| Threat-model v1.8 written | Read this file | Verified (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 PXN | Serial wx: ok; #[test_case] flags + execute-from-heap IABORT | Verified: 2026-09-10 cloud qemu-smoke (honesty ledger). |
| Kernel text still executable | is_executable(0x4008_0000) | Same W^X probe. |
| Execute-from-writable heap forbidden | Armed permission IABORT → wx: nx heap then wx: ok | Fail-closed in scripts/qemu-smoke.sh. |
| Linker-stack guard holes | Serial guard: fault / guard: ok; store to __stack_guard | Verified: 2026-09-10 cloud qemu-smoke (honesty ledger). |
Minimize unsafe | Review (NFR-01). Count is not a proof. | Policy. |
| Fail-closed smoke | scripts/qemu-smoke.sh greps + force-fail exit ≠ 0 | NFR-04 / NFR-05. |
| No secrets in repo | Policy + .gitignore (including .obsidian/) | Host control. |
| IRQ least privilege | IRQ path does not allocate or yield_now | Convention. Ratchet if it fails twice. |
| EL0 entered and returned | Serial el0: svc / el0: ok; #[test_case] | Verified first mile — not isolation. |
| EL0 cannot execute kernel data | Serial el0: nx kernel; lower-EL IABORT | Verified first mile. |
EL0 cannot read kernel .data | Serial el0: no kernel read; user TTBR0 omits .data | Verified read mile. Isolation stays Planned. |
ASID isolation (no VMALLE1) | Serial asid: dual / asid: conflict / asid: ok | Specific mile. Umbrella isolation stays Planned. |
| Standing EL0 context | Serial el0: standing / el0: restored; is_active() flips | Specific mile. Not POSIX. Not isolation. |
| TTBR1 private page | Serial ttbr1: el1 / ttbr1: no el0 / ttbr1: ok | First cut (ADR-016). |
| EL1 fetch from TTBR1 high VA | Serial ttbr1: el1 exec / ttbr1: vbar | Exec mile (ADR-017). |
| Identity-tear first cut | Serial ident: split / ident: fault / ident: high / ident: no el0 / ident: ok | First cut (ADR-018). Full teardown Planned. |
| Identity text range tear | Serial ident: jump / ident: range / ident: text | Range cut (ADR-019). |
High-VA vtable rewrite + live .text tear | Serial ident: reloc / ident: live | Live .text cut (ADR-020). .rodata/.data/heap stay. |
| RO+NX text/data | Serial ro: ok; execute-from-.data + write-to-RO-text | Verified: 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. ERETto 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 fromTPIDR_EL1before touching kernel data. That path stillTLBI VMALLE1(kernel.dataleaves are global).- Dual EL1 ASIDs (1 vs 2) with
nGprobe pages switch withoutTLBI VMALLE1(src/asid.rs). - A bounded standing user context on that TTBR0:
SVC #1stays at EL0 (el0: standing), userMOVZruns,SVC #2restores 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 atva + TTBR1_BASEvia cloned RAM tables; EL1 can fetch a real path there andVBAR_EL1is the high alias. After a high-VA jump, rustc vtables are rewritten to high aliases (ident: reloc) and live identity.textafter 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-kernelstay at0x4008_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 standingSVC #1). Other lower-EL slots still park (ADR-004).
Probed miles
| Probe | What closes it | Honesty |
|---|---|---|
| EL0 entered and returned | Serial el0: svc + el0: ok; #[test_case] el0_svc_roundtrip | Entered and left. Not a user process. |
| EL0 cannot execute kernel data | Serial el0: nx kernel; #[test_case] el0_cannot_execute_kernel_data | IABORT on .data (UXN and/or unmapped). Not isolation. |
User TTBR0 omits kernel .data | Walk L1_USER; #[test_case] user_ttbr0_omits_kernel_data | Distinct user table. Text is still mapped so the handler can run. |
EL0 cannot read kernel .data | Serial el0: no kernel read; #[test_case] el0_cannot_read_kernel_data | Translation/permission DABORT. Not PAN. |
| Standing EL0 context | Serial el0: standing / el0: restored; #[test_case] standing_el0_enter_leave | Real enter/leave on user TTBR0. is_active() true only while standing. Not POSIX. Lower-EL IRQ still parks. |
| ASID field programmed | user_ttbr0() >> 48 == 1 | Programming fact on the EL0 trampoline. Not the isolation mile. |
| ASID isolation | Serial asid: dual / asid: conflict / asid: ok; #[test_case] asid_isolation_without_vmalle1 | Dual TTBR0 without TLBI VMALLE1. Stale ASID-1 data under ASID 2 is Failed. Not “EL0 isolated.” |
| TTBR1 private page | Serial ttbr1: el1 / ttbr1: no el0 / ttbr1: ok; #[test_case] ttbr1_el1_sees_priv_el0_does_not | EL1-only high page. Not a relocated kernel. |
| EL1 fetch from TTBR1 high VA | Serial ttbr1: el1 exec / ttbr1: vbar; #[test_case] el1_executes_from_ttbr1_high_va | Real EL1 path + high VBAR. Identity boot stub stays. |
| Identity-tear first cut | Serial ident: split / ident: fault / ident: high / ident: no el0 / ident: ok; #[test_case] identity_tear_el1_faults_high_stays | One identity text page unmapped; high twin still fetches. Not a relocated kernel. Full teardown Planned. |
| Identity text range tear | Serial ident: jump / ident: range / ident: text; #[test_case] identity_text_range_unmapped_boot_stub_stays | High-VA continuation + 16 KiB dedicated range unmapped; high twins still fetch. Not a relocated kernel. |
High-VA vtable rewrite + live .text tear | Serial ident: reloc / ident: live; #[test_case] identity_fn_ptrs_rewritten_high + live_identity_text_unmapped_boot_stub_stays | rustc 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)
| Probe | What would close it |
|---|---|
| PAN | ID_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 tear | Those identity ranges unmapped; accesses proven high-only. After live .text (ADR-020). |
| Full higher-half / identity teardown | The 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 TLBI | User TTBR0 switch that does not TLBI VMALLE1 (needs nG on kernel .data or an ASID-specific invalidate). |
| Lower-EL IRQ while standing | Timer (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
- A latency, cycle, or “faster than” sentence is a claim. It needs a probe in the honesty ledger.
- Optimize only after a probe shows a cost. Do not rewrite the scheduler or heap “for speed” on a hunch.
- QEMU
virtnumbers 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 onperf: 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 onperf: irq-delta missed). #[test_case]asserts samples exist andmax >= 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 onperf: 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.
| Class | What we expect (hypothesis, unmeasured) | Honesty |
|---|---|---|
| Costs | Extra boot/load work; each SVC crossing; ASID/TTBR0 switches into the app map; optional COW later if payloads are shared | Cost only after a probe. Not a budget. |
| Neutral / wins | Steady 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 win | Operational ≠ measured latency. |
| Gate | Keep perf: boot-delta. Add a new app-load CNTPCT probe (perf: app-load or similar) when the loader exists. Fail closed on miss | Marker 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.dataslot 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 watch | Probe (do not invent a target) |
|---|---|
| Physical counter moves | Serial perf: cntpct delta=<n> + #[test_case] |
| IRQ-to-handler spread on this virt guest | Serial perf: irq-delta min=… max=… spread=… n=… |
| Debug ELF byte size | Host perf: elf-size bytes=<n> (measurement, not “smaller is better”) |
kernel_main after-MMU → after-init | Serial 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 watch | Probe |
|---|---|
| Hello + milestone strings still print | scripts/qemu-smoke.sh greps; missing string → fail |
| Tests stay fail-closed | cargo test exit 0; --features force-fail must be non-zero |
| Same failure twice | Becomes a sensor/gate, not another README paragraph |
| Failed history kept | Example: 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_stdcoop EL1 onaarch64-ctos.json, proven withcargo/qemu-smoke/docker-smoke. - POSIX / glibc = not easy, not started.
- SVC ABI +
libctosfor 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 oftenqemu-system-arm)- Optional: Docker linux/arm64 (
./scripts/docker-smoke.sh) — do not pinlinux/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_stdtree on one ISA (AArch64 / QEMUvirt/ 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-
.textvtable yank, CRLF shebang) - Three pillars named up front — security and speed still need probes
Drawbacks
- QEMU
virtonly. 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
.textafter the boot stub is torn (ADR-020);.rodata/.data/heap stay - Scoped immutability only (immutability.md): RO+NX / WXN / live
.texttear 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.
- Heap must already be up (
src/heap.rs). Worker stacks are 8 KiBVec<u8>boxes, not the linkerSP_EL0stack. spawn(task_a)andspawn(task_b)install AAPCS64 callee-saved frames. After ADR-020 the firstretlands on the high-VA alias of the trampoline.- Idle (
kernel_main/ the test runner) callsyield_now()until both atomics are set, or a bound is hit. - Each worker stores its SP, writes a raw UART line, sets its flag, and returns (
Done).
Probe (do not invent extra markers).
| Serial | Meaning |
|---|---|
sched: task a | Worker A ran on a heap stack |
sched: task b | Worker B ran on a heap stack |
sched: ok | Both 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.
- After the hello path has printed earlier markers,
observe_probe_bytespins onUARTFR.RXFEuntil a byte arrives orCNTPCTtimes out. - IRQs stay masked on this path. There is no UART IRQ in M6. Do not
wfiwaiting for RX. - The expected byte prints
input: rx 0x41. Any other byte printsinput: rx 0xNN (unexpected)and fails the probe. Timeout printsinput: rx missed.
Probe.
| Serial | Meaning |
|---|---|
input: rx 0x41 | Host-injected 0x41 landed in the FIFO |
input: rx missed | Fail-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.
- First miles already proved enter/return (
el0: svc), UXN fetch (el0: nx kernel), and no kernel.dataread (el0: no kernel read). - Standing payload on the EL0 window:
SVC #1→ serialel0: standing(stay at EL0) →MOVZ X1, #0x51A4→SVC #2→ serialel0: restored(back to EL1). - Saved user PC / SP / TTBR0 live only while
is_active()is true. The handler clears that flag on restore.
Probe.
| Serial | Meaning |
|---|---|
el0: standing | SVC #1 taken; still at EL0 |
el0: restored | SVC #2 taken; back to EL1 |
el0: ok | First-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/Vecon the first-fit heap)timer: tick(GICv2 + CNTP)wx: ok,guard: ok,ro: okident: reloc/ident: live/ident: ok(ADR-020 live.texttear)exception: sync BRKthenexception: 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.”
| Want | Why not |
|---|---|
| A Linux ELF / glibc / musl binary | No exec, no ELF loader, no syscall table. Custom target is os: none. |
A shell (sh, bash) or line-oriented TTY | RX probe is one injected byte. No line discipline. |
| Python, Node, or any hosted interpreter | Needs a process ABI, heap policy, and usually a filesystem. |
| Network / sockets / HTTP | No 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 threads | M9 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 guest | No. Host docker-smoke.sh only builds the kernel. |
Raspberry Pi or any board other than QEMU virt | Unprobed. Do not copy virt Verified onto hardware. |
| GPU / desktop / windowing / virtio devices | Out 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
| Piece | What it is |
|---|---|
aarch64-ctos.json | Custom rustc target: os: none, abort, soft-float, static, rust-lld. Not aarch64-unknown-linux-gnu. |
.cargo/config.toml | Pins that JSON, build-std (core / alloc / compiler_builtins), -Tlinker.ld, QEMU runner. |
rust-toolchain.toml | Nightly + 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):
- Write a
fn my_task()that usesalloc/ UART /sched::yield_now()only. Nostd, no files, no sockets. spawn(my_task)next to the existing workers (heap must be up; stacks are heapVecs).- Print a new serial marker (
sched: beat n=…or similar). Teachscripts/qemu-smoke.shto grep it. Fail closed on miss. - 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):
- Isolation miles first: PAN (usually absent on
cortex-a57), identity.rodata/.data/ heap tear, umbrella EL0 isolation (el0.md, ADR-013). - A stable SVC ABI written as a later ADR (numbers, registers, error model). Do not silently grow
#1/#2into POSIX. - A freestanding
libctos(name reserved here as intent only — no crate today) that a future EL0 program could link againstno_std, talking that ABI. Still not glibc. Still notexecof 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.
| Step | What it would be | Why this order | Status |
|---|---|---|---|
| 1. memfs | In-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-blk | QEMU 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-like | An 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
-drivewithout 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
| Claim | Probe | Status |
|---|---|---|
| No filesystem in this tree | Source: no VFS / memfs / virtio-blk / FAT in src/ | Verified (absence) |
| Guest can open a file | Serial + #[test_case] that do not exist yet | Planned |
| virtio-blk works | QEMU disk + guest driver + marker | Planned |
| FAT or xv6-like on a block device | Format + read-back probe | Planned |
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 today | Status |
|---|---|---|
A process you exec | One linked kernel ELF. Standing EL0 is a dual-SVC stub | Planned (loader + ABI). Not started. |
| POSIX / glibc / musl | Custom aarch64-ctos.json, os: none, no libc | Not easy, not started. |
Files (open / a disk) | No VFS | Planned memfs → virtio-blk → FAT/xv6-like |
| Sockets / HTTP | No virtio-net, no stack | Planned at best; not a Now mile |
| Shell / TTY / Python | One injected UART byte; no interpreter | No until ABI + FS + line discipline |
| Isolated userspace | First miles + live .text tear; PAN / full teardown missing | Isolation Planned |
| Preemption / SMP | Cooperative EL1, one vCPU | No on this horizon |
| Containers (OCI / Docker / k8s as the guest) | Nothing | No. 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
virta 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
| Claim | Probe | Status |
|---|---|---|
| Guest is not a container host | Source: no OCI/runc/cgroup/namespace code in src/ | Verified (absence) |
Host docker-smoke.sh builds the kernel | Ledger Docker rows (sponsor / GHA) | Separate claim — host harness only |
| A host app (Linux ELF, shell, Python) runs in the guest | No such serial marker | Planned |
| Guest is a container host | No OCI/runc/cgroup code | Verified 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.
| Scope | Probe | ADR |
|---|---|---|
Identity .text/.rodata RO+X; .data/heap RW+NX | ro: nx data / ro: write fault / ro: ok | ADR-015 |
SCTLR.WXN on | same RO+NX smoke | ADR-015 |
Live identity .text torn after high-VA jump + vtable rewrite | ident: reloc / ident: live / ident: ok | ADR-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:
- Name the scope (which pages, which image).
- Cite a probe (serial +
#[test_case], or source absence). - Only then Verified. Unprobed stays Unknown. Future work stays Planned.
- 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 page | Source file |
|---|---|
| What can run today | docs/overview/what-can-run.md |
| Building or porting | docs/overview/porting.md |
| Filesystem (Planned) | docs/overview/filesystem.md |
| Hosting apps / containers | docs/overview/hosting-apps.md |
| KPIs / how we measure | docs/overview/measure.md |
| Prerequisites | docs/overview/prerequisites.md |
| Advantages | docs/overview/advantages.md |
| Drawbacks / limits | docs/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
| URL | Role | Status word |
|---|---|---|
https://ctos.artof.link | Custom domain | Verified. 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/ctos | Project-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 outputCNAMEisctos.artof.link. Does not deploy. - Push to
main: build, uploadactions/upload-pages-artifact, deploy withactions/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.
| Field | Value |
|---|---|
| AWS account | 737290977112 |
| Region for CLI | us-east-1 (Route 53 API is global) |
| Hosted zone name | artof.link |
| Hosted zone ID | Z1178AFMV41RWP (sponsor-stated) |
| Record | CNAME 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
| Claim | Probe | Until then |
|---|---|---|
| mdBook builds this tree | ./scripts/docs-build.sh (or mdbook build with mermaid preprocessor) exit 0 | — |
| Pages workflow exists | Read .github/workflows/pages.yml | File presence only |
| Pages workflow builds a PR | Green pages run on this branch (build job; deploy skipped) | See honesty ledger |
| Docs website published | Green pages workflow on main and HTTPS fetch of https://ctos.artof.link | Verified — deploy 34653046584 + HTTPS 200 + Driving principles |
Public CNAME ctos.artof.link | dig CNAME ctos.artof.link +short | Verified — DNS in place (artofdream.github.io.) |
Route 53 API row in 737290977112 / Z1178AFMV41RWP | list-resource-record-sets as that account | Unknown here (no AWS CLI). Sponsor states CREATE already done. Do not CREATE again. |
| Custom domain reachability | Pages lists the hostname and HTTPS 200 with a matching cert | Verified — 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.
| Vault | Where | Job |
|---|---|---|
| Procedure | .cursor/skills/ctos-*/ | How to do a repeatable job |
| Correction | .cursor/rules/ + hard constraints in AGENTS.md | Mistakes we must not repeat |
| Relationship | Docs cross-links (vision → architecture → ADR → roadmap → ledger) | How pieces connect |
| Daily Brief | research/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).