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. POSIX and a userspace ABI stay Planned. “EL0 isolated” is Verified only as the accepted ADR-091 sentence (one core, TCG default smoke machine; limits there).
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 (A8 rebuild recipes). In-tree index: user/README.md. This page is extra stance (walkthroughs). HTTPS at https://ctos.artof.link is Verified (2026-09-11 after #30).
One rebuild for every sample: cargo build then ./scripts/qemu-smoke.sh. There is no separate app runner. A8 documents that path. A9 first cut adds slot:* when FAT /hello loads. Cross-update (same ELF on this OS and ba6541c) is ledger-Verified (ADR-032). Product freestanding app hosting is Verified (ADR-048 / ADR-052); not Linux/POSIX/containers.
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.
Rebuild. Edit those functions. cargo build then ./scripts/qemu-smoke.sh. Recipe hub: what-can-run.md.
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.
Rebuild. Same cargo build + ./scripts/qemu-smoke.sh (the script injects 0x41). Recipe: what-can-run.md.
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. After A1 the standing stub also runs the documented ABI trip (svc: yield / svc: user-hi / svc: uart / svc: exit / svc: ok) — syscall.md. After A2 a hello linked against libctos prints libctos: hi / libctos: ok (ADR-022). After A3 the same ELF is guest-parsed (loader: ok, ADR-023). After A4 that loaded image is a standing task until exit (el0: task-ok, ADR-024); an unexpected fault restores fail-closed (el0: restore-fail). Lower-EL IRQ still parks. PAN is enabled on the default -cpu cortex-a76 (ADR-080; PAN enable was a non-goal on the historical -cpu cortex-a57, ADR-047; a57 ID-field pan: absent Verified). This is not a user process and not POSIX; “EL0 isolated” means only the accepted ADR-091 sentence. The ABI + CRT + loader + standing-task miles are not app hosting (ADR-048).
Rebuild the hello. Source is user/hello-libctos/ (user/hello-libctos/README.md). A kernel cargo build publishes it via build.rs onto FAT /hello. Standalone:
cargo build --release \
--manifest-path user/hello-libctos/Cargo.toml \
--target user/hello-libctos/aarch64-ctos-user.json
That ELF still has to land on FAT /hello to run. The hello does not call fs_open. A second freestanding sample (user/fs-libctos, FAT /fsdemo) does exercise create/open/read/write/close on /memdemo (ADR-059; markers libctos: fs-hi / libctos: fs-ok / fsdemo: ok). A third (user/fat-libctos, FAT /fatdemo) opens/reads FAT /probe (fat-hi) via thin VFS (ADR-061; markers libctos: fat-hi / libctos: fat-ok / fatdemo: ok). A fourth (user/yield-libctos, FAT /yldemo) exercises several cooperative yield_now() rounds (ADR-062; markers libctos: yld-hi / libctos: beat / libctos: yld-ok / yldemo: ok). A fifth (user/net-libctos, FAT /netdemo) exercises EL0 net SVCs (ADR-068; markers libctos: net-hi / libctos: net-mac / libctos: net-ok / netdemo: ok). A sixth (user/udp-libctos, FAT /udpdemo) exercises EL0 net_udp_dns (ADR-071; markers libctos: udp-hi / libctos: udp-ok / udpdemo: ok). A seventh (user/mkdir-libctos, FAT /mkdemo) exercises EL0 fs_mkdir (ADR-075; markers libctos: mkdir-hi / libctos: mkdir-ok / mkdemo: ok). An eighth (user/tcp-libctos, FAT /tcpdemo) exercises EL0 net_tcp_echo (ADR-076; markers libctos: tcp-hi / libctos: tcp-ok / tcpdemo: ok). The kernel /eprobe trampoline (fs: el0) remains a separate Verified trip. Not POSIX. Not getdents. Not preemption. Not sockets.
Sample: freestanding libctos VFS (fs-libctos, ADR-059)
What it is. Second freestanding EL0 ELF linked against libctos. Creates /memdemo on memfs (ADR-058 /mem prefix), writes fs-hi, reads it back, prints libctos: fs-ok. Loaded from FAT /fsdemo.
Where. user/fs-libctos/, src/fsdemo.rs, scripts/mkfat16.py --app2. Decision: ADR-059.
Rebuild. Same cargo build + ./scripts/qemu-smoke.sh.
Probe. libctos: fs-hi / libctos: fs-ok / fsdemo: fat / fsdemo: mapped / fsdemo: ok. Keep slot: ok / /hello. Not POSIX. Not getdents.
Sample: freestanding libctos FAT (fat-libctos, ADR-061)
What it is. Third freestanding EL0 ELF linked against libctos. Opens FAT /probe, reads fat-hi through thin VFS (/ → FAT16), prints libctos: fat-ok. Loaded from FAT /fatdemo. Deeper than fs-libctos (memfs only).
Where. user/fat-libctos/, src/fatdemo.rs, scripts/mkfat16.py --app3. Decision: ADR-061.
Rebuild. Same cargo build + ./scripts/qemu-smoke.sh.
Probe. libctos: fat-hi / libctos: fat-ok / fatdemo: fat / fatdemo: mapped / fatdemo: ok. Keep slot: ok / fsdemo: ok / /hello / /fsdemo. Not POSIX. Not getdents. Fourth sample: Recipe yield below.
Sample: freestanding libctos yield rounds (yield-libctos, ADR-062)
What it is. Fourth freestanding EL0 ELF linked against libctos. Prints libctos: yld-hi, then several libctos: beat lines across yield_now() rounds (deeper than hello’s single yield), then libctos: yld-ok. Loaded from FAT /yldemo. Cooperative only — not preemption, not multi-task EL0, not a process table.
Where. user/yield-libctos/, src/yldemo.rs, scripts/mkfat16.py --app4. Decision: ADR-062.
Rebuild. Same cargo build + ./scripts/qemu-smoke.sh.
Probe. libctos: yld-hi / ≥3× libctos: beat / libctos: yld-ok / yldemo: fat / yldemo: mapped / yldemo: ok. Keep slot: ok / fsdemo: ok / fatdemo: ok / /hello / /fsdemo / /fatdemo.
Sample: freestanding libctos net SVCs (net-libctos, ADR-068)
What it is. Fifth freestanding EL0 ELF linked against libctos. Prints libctos: net-hi, reads guest MAC (libctos: net-mac), asks the kernel for an ICMP echo to SLIRP (net_ping), then libctos: net-ok. Loaded from FAT /netdemo. Kernel owns virtio-net — no guest driver in EL0.
Where. user/net-libctos/, src/netdemo.rs, scripts/mkfat16.py --app5. Decision: ADR-068.
Probe. libctos: net-hi / libctos: net-mac / libctos: net-ok / netdemo: fat / netdemo: mapped / netdemo: ok. Keep slot: ok / fsdemo: ok / fatdemo: ok / yldemo: ok / N1 net: ok / N2 net: ping-ok / N3 net: udp-ok. Not TCP product. Not sockets. Not “has networking.”
Sample: freestanding libctos UDP DNS SVC (udp-libctos, ADR-071)
What it is. Sixth freestanding EL0 ELF linked against libctos. Prints libctos: udp-hi, asks the kernel for a quiet ARP + UDP DNS probe vs SLIRP (net_udp_dns), then libctos: udp-ok. Loaded from FAT /udpdemo. Kernel owns virtio-net — no guest driver in EL0. DNS is probe bait only.
Where. user/udp-libctos/, src/udpdemo.rs, scripts/mkfat16.py --app6. Decision: ADR-071.
Probe. libctos: udp-hi / libctos: udp-ok / udpdemo: fat / udpdemo: mapped / udpdemo: ok. Keep slot: ok / fsdemo: ok / fatdemo: ok / yldemo: ok / netdemo: ok / N1 net: ok / N2 net: ping-ok / N3 net: udp-ok. Not TCP product. Not sockets. Not a DNS product. Not “has networking.”
Freestanding mkdir-libctos (EL0 fs_mkdir)
What it is. Seventh freestanding EL0 ELF linked against libctos. Prints libctos: mkdir-hi, creates FAT root /edir via fs_mkdir (SVC 27), proves Exists/BadPath mapping, then libctos: mkdir-ok. Loaded from FAT /mkdemo (8.3 — not /mkdirdemo).
Where. user/mkdir-libctos/, src/mkdemo.rs, scripts/mkfat16.py --app7. Decision: ADR-075.
Probe. libctos: mkdir-hi / libctos: mkdir-ok / mkdemo: fat / mkdemo: mapped / mkdemo: ok. Keep fat: mkdir / prior six samples / net. Not POSIX mkdir. Not a nested tree.
Freestanding tcp-libctos (EL0 net_tcp_echo)
What it is. Eighth freestanding EL0 ELF linked against libctos. Prints libctos: tcp-hi, runs quiet thin TCP guestfwd echo via net_tcp_echo (SVC 28), then libctos: tcp-ok. Loaded from FAT /tcpdemo.
Where. user/tcp-libctos/, src/tcpdemo.rs, scripts/mkfat16.py --app8. Decision: ADR-076.
Probe. libctos: tcp-hi / libctos: tcp-ok / tcpdemo: fat / tcpdemo: mapped / tcpdemo: ok. Keep net: tcp-ok / prior seven samples. Not BSD sockets. Not listen/accept. Not TLS.
Sample: memfs named-buffer probe (A6)
What it is. Thin VFS + in-RAM buffers. Kernel creates /kprobe, writes, reads. An EL0 trampoline does the same for /eprobe (memfs-el0). Not POSIX. Not a directory tree.
Where. src/vfs.rs, src/syscall.rs (el0_fs_svc_roundtrip). Decision: ADR-027.
Rebuild. Same cargo build + ./scripts/qemu-smoke.sh.
Probe. fs: create / fs: write / fs: read / fs: el0 / fs: ok. File presence is not that probe.
Sample: FAT16 /probe on virtio-blk (A7)
What it is. Guest programs virtio-mmio block and vfs::open("/probe") reads fat-hi from a host-built FAT16 image. Same open / write as memfs. Write depth + small create: ADR-050. Same-boot CNTPCT vs memfs write: ADR-051.
Where. src/virtio.rs, src/fat.rs, scripts/mkfat16.py. Decision: ADR-028.
Rebuild. Same smoke. The script writes target/fat16.img and attaches -drive if=none,file=…,id=hd0 -device virtio-blk-device,drive=hd0. Host -drive without fat: ok is not the probe.
Probe. blk: virtio / blk: cap / blk: rw / blk: ok and fat: mount / fat: read / fat: write / fat: create / fat: ok, plus perf: fat-write / perf: memfs-write / perf: fs-write-delta when the ledger has ADR-051. Do not say “supports FAT.”
Also on the same hello path
These are kernel sensors, not apps. Cite the ledger SHA if you quote a number. Recent A7 smoke still printed:
- 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: rodata/ident: oksvc: ok/libctos: ok/loader: ok/el0: task-okfs: ok/blk: ok/fat: okexception: sync BRKthenexception: fatal nested
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. |
| POSIX / Linux disk apps | memfs (A6) + FAT16 on virtio-blk (A7 / ADR-028 + write ADR-050). Not POSIX. Not FAT32. 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. “EL0 isolated” is Verified only as the accepted ADR-091 sentence (one core, TCG; limits there): PAN enabled on a76 (ADR-080); _start stays; leftover identity RAM torn (ADR-049). Identity .rodata/.data/heap are torn (ADR-025/037/038). Gaps: host-apps.md. |
| OCI / Docker / k8s in the guest | No — non-goal (ADR-029). 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. |
Umbrella “EL0 isolated” is Verified only as the accepted ADR-091 sentence (M6). PAN is enabled on default cortex-a76 (ADR-080). Identity .rodata / .data / heap / leftover RAM tears are probed (ADR-025/037/038/049); _start stays. See el0.md.