changed the old name to the new one #18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| jobs: | |
| # Every crate except kernel/ — see kernel/Cargo.toml's `[workspace]` stanza | |
| # for why it's a standalone package instead of a member here. | |
| host: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: stable | |
| components: clippy, rustfmt | |
| - uses: Swatinem/rust-cache@v2 | |
| - name: fmt check | |
| run: cargo fmt --check | |
| - name: clippy | |
| run: cargo clippy --workspace --all-targets -- -D warnings | |
| - name: test | |
| run: cargo test --workspace | |
| # Freestanding x86_64-unknown-none build. Needs nightly from Phase 2 | |
| # onward: exception handlers use `extern "x86-interrupt"`, which is still | |
| # unstable (rust-lang/rust#40180) despite being the standard way every | |
| # x86_64 Rust kernel defines them — see kernel/src/lib.rs's | |
| # `#![feature(abi_x86_interrupt)]`. `--target` is explicit rather than | |
| # relying on a `.cargo/config.toml` default: kernel/.cargo/config.toml | |
| # deliberately has none (see the comment there — an ambient default here | |
| # once leaked into the `kernel-tests` job's nested xtask invocation below, | |
| # forcing a host-side tool to try building for a bare-metal target). | |
| # `--bins --lib` (not `--all-targets`): the *default* libtest harness | |
| # still can't run on a bare target — `kernel-tests` below is what | |
| # actually exercises `tests/*.rs`, via its own non-libtest mechanism. | |
| kernel: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: kernel | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@nightly | |
| with: | |
| toolchain: nightly | |
| targets: x86_64-unknown-none | |
| components: clippy, rustfmt | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: kernel | |
| - name: fmt check | |
| run: cargo fmt --check | |
| - name: clippy (lib + bin only) | |
| run: cargo clippy --target x86_64-unknown-none --bins --lib -- -D warnings | |
| - name: build | |
| run: cargo build --target x86_64-unknown-none --bins --lib | |
| # The real "Phase 8" harness: `cargo test` (run from kernel/, targeting | |
| # x86_64-unknown-none) builds each `kernel/tests/*.rs` as its own bootable | |
| # binary (`harness = false` in Cargo.toml — no libtest, since there's no | |
| # `test`/panic-unwind runtime on bare metal), boots it for real in QEMU | |
| # via kernel/.cargo/config.toml's `runner` (which delegates to xtask's | |
| # `test-runner` subcommand), and reads pass/fail from the isa-debug-exit | |
| # device (kernel/src/qemu_exit.rs) — a real exit code, not string-matched | |
| # serial output. This is what catches architecture-specific bugs (like the | |
| # GDT/segment-register one in the README) that `cargo test --workspace` on | |
| # the host never could. | |
| kernel-tests: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@nightly | |
| with: | |
| toolchain: nightly | |
| targets: x86_64-unknown-none | |
| # `rust-src`: the `bootloader` crate's build script (invoked via | |
| # `xtask` in this job) compiles its own boot-sector/stage-2/3/4 | |
| # helper binaries for bare-metal targets, which needs to build | |
| # core/alloc from source — without this component that fails with | |
| # "does not exist, unable to build with the standard library". | |
| components: llvm-tools, rust-src | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| kernel | |
| xtask | |
| grid-sandbox-host | |
| - name: install QEMU | |
| run: sudo apt-get update && sudo apt-get install -y qemu-system-x86 | |
| # `grid_sandbox_wasm` below needs this binary already built on disk | |
| # (`include_bytes!` embeds it at kernel-test compile time) — see | |
| # `kernel/tests/grid_sandbox_wasm.rs`'s own doc comment. Built here, | |
| # not as part of the `kernel-tests` `cargo test` invocation, since | |
| # it's a separate standalone package (own `[workspace]`, same | |
| # reasoning as `kernel`/`xtask` — see its `Cargo.toml`). | |
| - name: build grid-sandbox-host (payload for grid_sandbox_wasm) | |
| working-directory: grid-sandbox-host | |
| run: cargo build --target x86_64-unknown-none --release | |
| - name: cargo test (boots every kernel/tests/*.rs in QEMU) | |
| working-directory: kernel | |
| # `--test basic_boot`, not a bare `cargo test`: with no target | |
| # selection, cargo also tries to build the lib's own unit-test | |
| # harness (`--lib`), which needs `test`/panic-unwind — unavailable | |
| # on a bare-metal target regardless of `harness = false` on the | |
| # integration tests. Add `--test <name>` for each new file under | |
| # tests/ as they're added; there's no single flag that means "every | |
| # tests/*.rs but not lib/bin unit tests" (`--tests`, despite the | |
| # name, still pulls in `--lib`/`--bins` too). | |
| run: | | |
| cargo test --target x86_64-unknown-none --test basic_boot | |
| cargo test --target x86_64-unknown-none --test guard_page | |
| cargo test --target x86_64-unknown-none --test thread_reclaim | |
| cargo test --target x86_64-unknown-none --test watchdog | |
| cargo test --target x86_64-unknown-none --test pci_scan | |
| cargo test --target x86_64-unknown-none --test process_isolation | |
| cargo test --target x86_64-unknown-none --test elf_loader | |
| cargo test --target x86_64-unknown-none --test scheduler_address_space | |
| cargo test --target x86_64-unknown-none --test ring3_cooperative | |
| cargo test --target x86_64-unknown-none --test grid_sandbox_wasm | |
| cargo test --target x86_64-unknown-none --test citadel_demo | |
| # Mobile L1 (aarch64/TrustZone), separate from the x86_64 `kernel` job | |
| # above — different target triple, different toolchain requirement (this | |
| # one builds on stable). Alpha scope so far: EL3 boot, a real exception | |
| # vector table (catches and resumes a deliberate fault with full context | |
| # preserved), a fully proven GIC bring-up (SGI delivered through the IRQ | |
| # vector, acknowledged and EOI'd), the EL3->EL1 Non-secure drop (the | |
| # actual TrustZone boundary), EL1's own MMU (identity-mapped, verified | |
| # with AT S1E1R actually translating, not just enabling without | |
| # crashing), and the RIL isolation boundary itself (EL1->EL0 drop, SVC | |
| # syscall gate, capability-token-gated resource check reusing | |
| # capability-manager) — see kernel-arm/src/main.rs's doc comment for | |
| # full status and the "why a separate crate from kernel/" decision. | |
| kernel-arm: | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: kernel-arm | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| toolchain: stable | |
| targets: aarch64-unknown-none | |
| components: clippy, rustfmt | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: kernel-arm | |
| - name: fmt check | |
| run: cargo fmt --check | |
| - name: clippy | |
| run: cargo clippy --target aarch64-unknown-none -- -D warnings | |
| - name: build | |
| run: cargo build --target aarch64-unknown-none --release | |
| - name: install QEMU (aarch64) | |
| run: sudo apt-get update && sudo apt-get install -y qemu-system-arm | |
| - name: boot in QEMU and check EL3 boot, exception resume, GIC, EL3->EL1 drop | |
| run: | | |
| timeout 10 qemu-system-aarch64 \ | |
| -M virt,secure=on,gic-version=2 -cpu cortex-a53 -nographic \ | |
| -kernel target/aarch64-unknown-none/release/runix-kernel-arm \ | |
| | tee /tmp/arm-boot.log || true | |
| grep -q "Runix ARM kernel: boot OK" /tmp/arm-boot.log | |
| grep -q "Runix ARM kernel: CurrentEL = EL3" /tmp/arm-boot.log | |
| grep -q "Runix ARM kernel: VBAR_EL3 installed" /tmp/arm-boot.log | |
| # Proves the exception vector table actually catches a fault and | |
| # resumes with the interrupted register context intact, not just | |
| # that installing VBAR_EL3 didn't crash on its own. | |
| grep -q "EXCEPTION: vector 4 (Synchronous (current EL, SPx))" /tmp/arm-boot.log | |
| grep -q "Runix ARM kernel: EXCEPTION test OK (resumed after brk)" /tmp/arm-boot.log | |
| # Proves a Software Generated Interrupt is actually delivered | |
| # through the GIC into the IRQ vector, not just pending at the | |
| # distributor (see gic.rs's doc comment for what that distinction | |
| # cost to find). | |
| grep -q "EXCEPTION: IRQ fired, GIC interrupt id = 0" /tmp/arm-boot.log | |
| grep -q "Runix ARM kernel: GIC test OK" /tmp/arm-boot.log | |
| # The actual TrustZone boundary. | |
| grep -q "Runix ARM kernel: reached EL1 (dropped from EL3, SCR_EL3.NS=1)" /tmp/arm-boot.log | |
| grep -q "Runix ARM kernel: CurrentEL = EL1" /tmp/arm-boot.log | |
| grep -q "Runix ARM kernel: VBAR_EL1 installed" /tmp/arm-boot.log | |
| grep -q "Runix ARM kernel: MMU enabled" /tmp/arm-boot.log | |
| # Proves the MMU is actually translating, not just that | |
| # SCTLR_EL1.M's write didn't crash: AT S1E1R asks the hardware | |
| # itself to translate UART0's VA and report the result. | |
| grep -q "fault=false, PA=0x9000000" /tmp/arm-boot.log | |
| # The RIL isolation boundary: EL1->EL0 drop, SVC gate, and a real | |
| # capability-manager token check distinguishing an authorized | |
| # channel from an unauthorized one — not just that EL0 code ran. | |
| grep -q "Runix ARM kernel: heap initialized" /tmp/arm-boot.log | |
| grep -q "Runix ARM kernel: RIL capability issued" /tmp/arm-boot.log | |
| # "U" (SYS_WRITE gate proof) immediately followed by "B" (0x42, | |
| # a real EL0 stack strb/ldrb round-trip) on the same line, since | |
| # neither syscall emits a newline -- proves real EL0/EL1 page- | |
| # granular memory isolation actually grants data access on | |
| # el0_demo's own page, not just that instruction fetch works. | |
| grep -q "^UB$" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_RIL_ACCESS channel 0 authorized" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_RIL_ACCESS channel 99 DENIED" /tmp/arm-boot.log | |
| # The check gates real per-operation I/O, not just a one-time | |
| # access decision: a byte sent on the authorized channel round | |
| # -trips back out (0x41, echoed as 'A'), the same operation on | |
| # the unauthorized channel is denied outright. | |
| grep -q "SVC: SYS_RIL_SEND channel 0 byte 0x41 authorized" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_RIL_RECV channel 0 authorized, byte 0x41" /tmp/arm-boot.log | |
| grep -q "^A$" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_RIL_SEND channel 99 DENIED" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_RIL_RECV channel 99 DENIED" /tmp/arm-boot.log | |
| # Basic SIM provisioning: the same capability check gating a | |
| # real state machine (sim.rs) instead of RIL's byte mailbox -- | |
| # a full Uninitialized -> Provisioned -> Activated walk on an | |
| # authorized slot, denied outright on an unauthorized one. | |
| grep -q "SVC: SYS_SIM_STATUS slot 0 authorized, state Uninitialized" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_SIM_PROVISION slot 0 identity 0x1234 authorized" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_SIM_STATUS slot 0 authorized, state Provisioned" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_SIM_ACTIVATE slot 0 authorized" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_SIM_STATUS slot 0 authorized, state Activated" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_SIM_PROVISION slot 99 DENIED" /tmp/arm-boot.log | |
| grep -q "SVC: SYS_SIM_STATUS slot 99 DENIED" /tmp/arm-boot.log | |
| - name: boot in QEMU without secure=on and check the no-EL3 path | |
| # Regression check for a real, previously-shipped bug: this used to | |
| # produce zero UART output (rust_start ran the EL3-only phase | |
| # unconditionally, which UNDEFINED-traps from EL1 with no VBAR_EL1 | |
| # installed yet to catch it) -- see main.rs's doc comment and | |
| # docs/STATUS.md's "Real bugs" section for the full account. | |
| run: | | |
| timeout 8 qemu-system-aarch64 \ | |
| -M virt,gic-version=2 -cpu cortex-a53 -nographic \ | |
| -kernel target/aarch64-unknown-none/release/runix-kernel-arm \ | |
| | tee /tmp/arm-boot-nosecure.log || true | |
| grep -q "Runix ARM kernel: boot OK" /tmp/arm-boot-nosecure.log | |
| grep -q "Runix ARM kernel: CurrentEL = EL1" /tmp/arm-boot-nosecure.log | |
| grep -q "no EL3 present -- skipping" /tmp/arm-boot-nosecure.log | |
| # Must NOT claim a security-state switch that never happened. | |
| grep -q "no EL3 ever ran, so no security-state switch to report" \ | |
| /tmp/arm-boot-nosecure.log | |
| grep -q "Runix ARM kernel: MMU enabled" /tmp/arm-boot-nosecure.log | |
| grep -q "^UB$" /tmp/arm-boot-nosecure.log | |
| grep -q "SVC: SYS_RIL_ACCESS channel 0 authorized" /tmp/arm-boot-nosecure.log | |
| grep -q "SVC: SYS_RIL_RECV channel 0 authorized, byte 0x41" \ | |
| /tmp/arm-boot-nosecure.log | |
| grep -q "^A$" /tmp/arm-boot-nosecure.log | |
| grep -q "SVC: SYS_SIM_STATUS slot 0 authorized, state Activated" \ | |
| /tmp/arm-boot-nosecure.log | |
| grep -q "SVC: SYS_SIM_PROVISION slot 99 DENIED" /tmp/arm-boot-nosecure.log | |
| # Complements kernel-tests above: boots the *full* main.rs demo (every | |
| # phase's feature, not just basic_boot's minimal boot+CPU-init smoke | |
| # test) and greps its serial output. Coarser and more brittle than a real | |
| # exit code, but it's the only thing here that actually exercises | |
| # everything main.rs does end to end in one run. | |
| boot: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: dtolnay/rust-toolchain@nightly | |
| with: | |
| toolchain: nightly | |
| targets: x86_64-unknown-none | |
| # `rust-src`: the `bootloader` crate's build script (invoked via | |
| # `xtask` in this job) compiles its own boot-sector/stage-2/3/4 | |
| # helper binaries for bare-metal targets, which needs to build | |
| # core/alloc from source — without this component that fails with | |
| # "does not exist, unable to build with the standard library". | |
| components: llvm-tools, rust-src | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: | | |
| kernel | |
| xtask | |
| grid-sandbox-host | |
| # main.rs now `include_bytes!`s grid-sandbox-host's compiled output | |
| # directly (Phase B7 — see citadel.rs's doc comment), so the kernel | |
| # itself won't even compile without this built first, the same | |
| # ordering requirement kernel-tests' grid_sandbox_wasm job already | |
| # has for the same reason. | |
| - name: build grid-sandbox-host (payload for Phase B7) | |
| working-directory: grid-sandbox-host | |
| run: cargo build --target x86_64-unknown-none --release | |
| - name: install QEMU | |
| run: sudo apt-get update && sudo apt-get install -y qemu-system-x86 | |
| - name: build boot image | |
| working-directory: xtask | |
| run: cargo run -- build --release | |
| - name: boot in QEMU and check for kernel serial output | |
| run: | | |
| # 90s, not 20s: a legacy multi-stage BIOS boot (SeaBIOS -> this | |
| # crate's own stage 2/3/4 -> kernel ELF load) reading a | |
| # multi-megabyte payload has real, observed variance depending | |
| # on the runner's disk/CPU emulation path -- 20s was cutting it | |
| # close even before Phase B7 added a ~2 MB embedded binary to | |
| # read. | |
| timeout 90 qemu-system-x86_64 \ | |
| -drive format=raw,file=target/runix-bios.img \ | |
| -serial stdio -display none -no-reboot -m 128M \ | |
| | tee /tmp/boot.log || true | |
| grep -q "Runix kernel: boot OK" /tmp/boot.log | |
| grep -q "Runix kernel: CPU init OK" /tmp/boot.log | |
| grep -q "EXCEPTION: BREAKPOINT" /tmp/boot.log | |
| grep -q "Runix kernel: breakpoint exception handled, execution resumed" /tmp/boot.log | |
| grep -q "Runix kernel: memory init OK" /tmp/boot.log | |
| grep -q "Runix kernel: heap alloc test OK (box=41, vec_len=100, vec_sum=4950)" /tmp/boot.log | |
| grep -q "Runix kernel: interrupts enabled" /tmp/boot.log | |
| grep -q "Runix kernel: timer interrupt OK" /tmp/boot.log | |
| grep -q "Runix kernel: scheduler test OK" /tmp/boot.log | |
| # Fair round-robin, not just "didn't crash": each of the three | |
| # spawned threads must get all 3 of its turns, in order, before | |
| # the run ends — a scheduler that starves one thread or replays | |
| # another's context would produce a different sequence here. | |
| grep -q "thread A: iteration 2" /tmp/boot.log | |
| grep -q "thread B: iteration 2" /tmp/boot.log | |
| grep -q "thread C: iteration 2" /tmp/boot.log | |
| grep -q "Runix kernel: syscall ABI OK" /tmp/boot.log | |
| # The exact bytes, in order — proves the channel didn't drop, | |
| # duplicate, or reorder anything crossing the syscall boundary. | |
| grep -q "Runix kernel: IPC test OK (Phase 6: received \['X', 'Y', 'Z'\] via port 0)" /tmp/boot.log | |
| # Capability gate (B4): authorized sender's send lands (return 0), | |
| # unauthorized sender's is denied (return u64::MAX) before it ever | |
| # reaches the channel — only 'K' shows up, never 'X'. | |
| grep -q "thread sender_authorized: SYS_IPC_SEND returned 0" /tmp/boot.log | |
| grep -q "thread sender_unauthorized: SYS_IPC_SEND returned 18446744073709551615" /tmp/boot.log | |
| grep -q "Runix kernel: capability gate OK (Phase B4: port 1 received Some('K')" /tmp/boot.log | |
| # Revocation (B5): a token valid on every count verify() itself | |
| # checks (signature, expiry, resource) is still denied once | |
| # explicitly revoked — port 2 gets nothing. | |
| grep -q "thread sender_revoked: SYS_IPC_SEND returned 18446744073709551615" /tmp/boot.log | |
| grep -q "Runix kernel: capability revocation OK (Phase B5: port 2 received None" /tmp/boot.log | |
| # CITADEL (B6): demo call proves the allowlist gate itself works | |
| # — a module signed for its exact bytes is accepted, tampered | |
| # bytes are refused. | |
| grep -q "Runix kernel: CITADEL boot authorization OK (Phase B6: authorized=Ok(())" /tmp/boot.log | |
| # CITADEL (B7): the same gate, now actually deciding whether a | |
| # real binary (grid-sandbox-host) gets loaded and run at all — | |
| # authorized, then parsed/loaded/run as an isolated ring 3 | |
| # process hosting wasmi, printing "Hi" via two host.print calls | |
| # that round-trip through the syscall gate. | |
| grep -q "Runix kernel: grid-sandbox-host authorized by CITADEL allowlist" /tmp/boot.log | |
| grep -q "Runix kernel: grid-sandbox-host loaded, entry point" /tmp/boot.log | |
| grep -q "^Hi" /tmp/boot.log | |
| grep -q "Runix kernel: grid-sandbox-host ran wasmi in an isolated ring 3 process" /tmp/boot.log | |
| grep -q "Runix kernel: entering ring 3" /tmp/boot.log | |
| # Printed by user_hello() running at CPL 3, one byte per SYS_WRITE | |
| # syscall — proves the syscall gate's DPL actually admits a ring 3 | |
| # caller, not just that the ring 0 -> ring 3 jump didn't fault. | |
| grep -q "^USR$" /tmp/boot.log |