Fix double fault: reserve thread-stack region's P4 slot before use #4
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 | |
| - name: install QEMU | |
| run: sudo apt-get update && sudo apt-get install -y qemu-system-x86 | |
| - 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 | |
| # 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 | |
| - 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 | |
| - name: boot in QEMU and check for kernel serial output | |
| run: | | |
| timeout 20 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 | |
| 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 |