diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index c780b0dee34..df44470c7fe 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -67,7 +67,7 @@ above; otherwise it is a NOTE. with its tag in a trailing comment or names a local path that resolves. A file added to or deleted from `tests/testcases/tinycc/` moves the count `tests/testcases/LICENSE` states in the same diff, and `46_grep.c` never comes back. Nothing - else is tracked under `tests/testcases/` but that `LICENSE`, `system.toml` and `hello.c`. + else is tracked under `tests/testcases/` but that `LICENSE` and `system.toml`. - **What no gate reads.** A BLOCKER each: a diff that declares a retired ABI name or reuses a retired syscall, `SYS_DEBUG` action or inbox op number (the retired numbers are `kernel/src/syscall/dispatch.rs`'s `retired_syscalls!` and the "formerly …" and "retired and @@ -80,7 +80,7 @@ above; otherwise it is a NOTE. needs a reason you accept; a branch that could delete more than it adds and does not goes back with the deletion named. What could be deleted, merged into what exists, or made smaller? An abstraction with one caller, a parameter with one value, dead code. Size is never bought with a - weaker check: tests are cut only when they test nothing. A compromise the branch found is removed or + weaker check: a test is cut only when it tests nothing, or as **Guest tests** says. A compromise the branch found is removed or recorded in `issues/` with an owner, evidence and an exit condition. Code is liability: code that does not earn its keep is deleted or simplified, and code kept "just in case", or because nobody knows whether it is needed, is an instant delete. Doubt is @@ -92,6 +92,14 @@ above; otherwise it is a NOTE. held across a user copy or a device wait. Arithmetic on a value the caller chooses. A short read, an exit status nobody reads. An `at_most(::MAX)` or `index(usize::MAX)` on an `Untrusted` is an unwrap wearing a check's name. +- **Guest tests.** A behaviour is tested on the cheapest tier that reaches it: a type that makes + the bug unrepresentable, then a host test, then a metal row on the T14, and a QEMU guest test + last. A new guest test, or one whose behaviour changes, whose pull request body does not say + why a type, a host test and a metal row cannot reach its behaviour is a BLOCKER, and so is one + whose reason a cheaper tier answers. A guest test is cut for a cheaper tier only where that + tier already holds its behaviour, named in the pull request body, or where a stage of a track + names it, in the same diff, with the behaviour it guarded and an exit a build or test can fail; + any other cut is a BLOCKER. - **Waits.** A flat wait — sleep, then assume it happened — is a BLOCKER, in code and in tests, unless a hardware document mandates that time and offers no notification, cited at the site. Wait on the event itself, bounded by a timeout that fails loudly. Defensive code that hides a diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 36d2aa93359..3b7808ad94f 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -78,10 +78,6 @@ jobs: runs-on: ubuntu-24.04 # A wedge guard, not a budget. timeout-minutes: 60 - strategy: - fail-fast: false - matrix: - shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] # The digest is the instrument's one pin: a dated image names the snapshot # archive it was built from, and `deps` installs QEMU from that archive. # The node ships `crw-rw---- root:kvm` and root opens it. @@ -104,7 +100,8 @@ jobs: for attempt in 1 2 3; do apt-get -o Acquire::Check-Valid-Until=false update -qq > /tmp/apt.log 2>&1 \ && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git curl ca-certificates \ - zstd build-essential qemu-system-x86 ovmf-generic >> /tmp/apt.log 2>&1 \ + zstd build-essential qemu-system-x86 ovmf-generic qemu-system-arm \ + qemu-efi-aarch64 >> /tmp/apt.log 2>&1 \ && break [ "$attempt" = 3 ] && { cat /tmp/apt.log; exit 1; } sleep 20 @@ -131,12 +128,10 @@ jobs: userland/target tests/target tests/toyos-rust-tests/*/target - tests/https-fetch-host/target - tests/https-server-host/target key: guest-${{ github.run_id }} restore-keys: guest- - - run: cargo run -- --ci guest ${{ matrix.shard }}/12 + - run: cargo run -- --ci guest # Every boot's 16550 log: what a guest that died early still leaves. - &serial @@ -145,14 +140,14 @@ jobs: continue-on-error: true uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: serial-${{ github.job }}-${{ strategy.job-index }} + name: serial-${{ github.job }} path: target/red-run-serial/**/uart-*.log if-no-files-found: warn retention-days: 7 - # The only lane with no `/dev/kvm`, so the only one that decodes the paths a - # KVM host's CPU never does; and the guest cache's one writer, since what it - # builds does not depend on the accelerator. + # The guest suite again with no `/dev/kvm`, so the only lane that decodes the + # paths a KVM host's CPU never does; and the guest cache's one writer, since + # what it builds does not depend on the accelerator. tcg: needs: build runs-on: ubuntu-24.04 @@ -165,7 +160,7 @@ jobs: - *deps - *checkout - *guest-cache - - run: cargo run -- --ci tcg + - run: cargo run -- --ci guest # After the test: what is worth keeping is a tree that built and booted. - if: github.ref == 'refs/heads/main' uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 diff --git a/Cargo.lock b/Cargo.lock index f3c87c3c550..5a9bffbf3c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,16 +237,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "fatfs" version = "0.3.6" @@ -265,16 +255,6 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" -[[package]] -name = "filetime" -version = "0.2.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" -dependencies = [ - "cfg-if", - "libc", -] - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -499,12 +479,6 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "log" version = "0.4.29" @@ -716,19 +690,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -860,17 +821,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tar" -version = "0.4.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" -dependencies = [ - "filetime", - "libc", - "xattr", -] - [[package]] name = "thiserror" version = "2.0.18" @@ -985,7 +935,6 @@ version = "0.1.0" dependencies = [ "bcachefs", "fatfs", - "flate2", "fontdue", "getrandom 0.3.4", "gpt", @@ -994,7 +943,6 @@ dependencies = [ "serde", "serde_json", "sha2", - "tar", "toml", "toyos-abi", "toyos-blackbox", @@ -1640,16 +1588,6 @@ dependencies = [ "wasmparser", ] -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix", -] - [[package]] name = "zerocopy" version = "0.8.47" diff --git a/Cargo.toml b/Cargo.toml index c65155c62bd..f7859848977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,11 +160,8 @@ getrandom = "0.3" # disagree about which files on it are `logd`'s. toyos-wallclock = { path = "toyos-wallclock" } image = { version = "0.25", default-features = false, features = ["jpeg"] } -# The digest behind two readings that must not be able to agree by accident: -# `NOTICE`'s record of every committed binary file (`src/sourcegate.rs`), and a -# disk this system was not given, before and after a boot -# (`src/fingerprint.rs`). Already resolved here through a dev-dependency, so -# nothing new is fetched — but `cargo run` did not compile it before this. +# The digest behind `NOTICE`'s record of every committed binary file +# (`src/sourcegate.rs`). sha2 = "0.10" [dev-dependencies] @@ -180,12 +177,6 @@ toyos-sched = { path = "toyos-sched" } # The Bulk-Only phases, so the harness judging a wedge reads the word # `toyos_xhci::bot::Phase` declares instead of spelling it a second time. toyos-xhci = { path = "toyos-xhci" } -# The second reader `pkg_install_gbae` is judged against: what is read back off -# the guest's volume is compared with a third party's decoding of the committed -# archive, never with `userland/pkg`'s own. The archive itself is committed -# under `tests/fixtures` and no test fetches anything. -flate2 = { version = "1", default-features = false, features = ["rust_backend"] } -tar = "0.4" [patch.crates-io] loom = { git = "https://github.com/ToyOSOrg/loom", branch = "toyos" } diff --git a/NOTICE b/NOTICE index 1b5c29be846..8738752851e 100644 --- a/NOTICE +++ b/NOTICE @@ -215,61 +215,6 @@ and forbids selling them by themselves. The licence reserves no font name, and none of these files is modified. -tests/iced-counter/src/bin/iced-counter.rs — iced's own counter example, MIT ----------------------------------------------------------------------------- - - iced's `examples/counter/src/main.rs` at tag 0.14.0 - (commit 3997291f318a8bc06fa522f5579836fb3feb94df), https://github.com/iced-rs/iced, - byte for byte - sha256 f1dcea1c15ce264ba73db644d9ad0d7ce5507d88a6ec49f9c7273d834c6ffa99 - Copyright 2019 Héctor Ramón, Iced contributors - Licence text: licenses/MIT-iced.txt (upstream's LICENSE at that tag) - SPDX-License-Identifier: MIT - -`toolkit_iced` builds it and carries it into `tests/toolkitcase`; no image a -`system.toml` builds ships it. - - -tests/fixtures/gbae-v0.2.0-* — gbae, MIT ------------------------------------------ - - gbae-v0.2.0-toyos-x86_64.tar.gz 604,872 bytes - sha256 99fcd8a7263b5c25cd90cead1baaa7200ef272100fc2226e008a4e8205ba2916 - SHA256SUMS 394 bytes - sha256 b13611227aac3fddb6daa20fdc9929fc96eeea46270f168887f9f6acb7403861 - - Copyright (c) 2025-2026 japabu - Upstream: https://github.com/Japabu/gbae, release v0.2.0 - Licence text: licenses/MIT-gbae.txt - SPDX-License-Identifier: MIT - -A Game Boy Advance emulator built for `x86_64-unknown-toyos`, and the release's -own sums file covering every asset of that release. `pkg_install_gbae` installs -the archive with `/system/bin/pkg` and runs the binary, which is what makes the -package path a thing that has been done rather than a thing that compiles. - -gbae's own terms are read out of `gbae/LICENSE` **inside the archive**, so the notice -MIT requires travels with the bytes as well as sitting in `licenses/`. Copying, -distributing and sublicensing are permitted outright, so committing it here puts -no constraint on a ToyOS build the way `assets/DOOM1.WAD` does. - -**These are committed because a test may not fetch.** Owner ruling, 2026-09-05: -*"do not use external resources as gbae for testing. we must keep our -dependencies minimal and stay independent."* They were downloaded once, by hand, -with - - gh release download v0.2.0 --repo Japabu/gbae \ - --pattern 'gbae-v0.2.0-toyos-x86_64.tar.gz' --pattern 'SHA256SUMS' - -and checked with `shasum -a 256` against the release's own `SHA256SUMS`. Nothing -in this repository fetches them, and `gh` is a development tool no test runs. - -`SHA256SUMS` is text and outside `assets/`, so `src/sourcegate.rs`'s two -populations do not walk it and only the archive has a row there. What holds it is -`tests/common/pkg.rs`, which refuses to run unless the archive hashes to the -digest above and that file carries the matching line. - - tests/testcases/ — TinyCC's corpus, LGPL-2.1, and picoc, BSD-3-Clause --------------------------------------------------------------------- diff --git a/bootloader/src/bootnext.rs b/bootloader/src/bootnext.rs index 7627887f19b..b38e223a8bc 100644 --- a/bootloader/src/bootnext.rs +++ b/bootloader/src/bootnext.rs @@ -73,9 +73,9 @@ pub fn point_at_us(handle: Handle, system_table: &SystemTable) { /// The GPT partition GUID of the volume firmware loaded this image from. fn our_partition(handle: Handle, system_table: &SystemTable) -> Option<[u8; 16]> { let bs = system_table.boot_services(); - let image = bs.open_protocol_exclusive::(handle).ok()?; + let image = crate::protocol::exclusive::(bs, handle).ok()?; let device = image.device()?; - let path = bs.open_protocol_exclusive::(device).ok()?; + let path = crate::protocol::exclusive::(bs, device).ok()?; hard_drive_guid(path.node_iter()) } diff --git a/bootloader/src/loaderlog.rs b/bootloader/src/loaderlog.rs index 2d431a1c03c..27b40295364 100644 --- a/bootloader/src/loaderlog.rs +++ b/bootloader/src/loaderlog.rs @@ -17,7 +17,7 @@ use core::fmt; use uefi::proto::media::file::{Directory, File, FileAttribute, FileMode, RegularFile}; use uefi::proto::media::fs::SimpleFileSystem; use uefi::proto::media::partition::PartitionInfo; -use uefi::table::boot::{BootServices, OpenProtocolAttributes, OpenProtocolParams, SearchType}; +use uefi::table::boot::{BootServices, SearchType}; use uefi::{prelude::*, CStr16, Handle}; /// The loader's first line, which is also the file's: [`open`] runs before it. @@ -111,8 +111,7 @@ pub fn with_volume( ) -> Result { let bs = system_table.boot_services(); let handle = volume_handle(bs, guid)?; - let mut fs = bs - .open_protocol_exclusive::(handle) + let mut fs = crate::protocol::exclusive::(bs, handle) .map_err(|e| alloc::format!("the log partition would not open ({e})"))?; let mut root = fs .open_volume() @@ -138,7 +137,7 @@ pub fn open(system_table: &SystemTable, guid: &[u8; 16], truncate: bool) { Ok(handle) => handle, Err(why) => return refused(format_args!("{why}")), }; - let mut fs = match bs.open_protocol_exclusive::(handle) { + let mut fs = match crate::protocol::exclusive::(bs, handle) { Ok(fs) => fs, Err(e) => return refused(format_args!("the log partition would not open ({e})")), }; @@ -216,21 +215,7 @@ pub fn close() { /// The unique GUID of the GPT partition `handle` sits on. `None` is a handle /// that publishes no partition record, or one on a table that is not GPT. fn unique_guid(bs: &BootServices, handle: Handle) -> Option<[u8; 16]> { - // `GetProtocol`, never `Exclusive`: this runs over every filesystem on the - // machine, and EXCLUSIVE would call `Stop` on whatever driver holds each. - // - // SAFETY: `open_protocol`'s obligation is that this handle and its protocol - // stay installed until the `ScopedProtocol` drops. Nothing between the two - // can uninstall either: the loader is the one image running, it registers - // no event callback, and it calls no boot service that connects or - // disconnects a controller. - let info = unsafe { - bs.open_protocol::( - OpenProtocolParams { handle, agent: bs.image_handle(), controller: None }, - OpenProtocolAttributes::GetProtocol, - ) - } - .ok()?; + let info = crate::protocol::get::(bs, handle).ok()?; let entry = info.gpt_partition_entry()?; // A `repr(packed)` entry, where a reference into it would be unaligned. Some({ entry.unique_partition_guid }.to_bytes()) diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index b2b607b6714..d118e05c250 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -16,7 +16,7 @@ use uefi::{ proto::device_path::{media::{PartitionFormat, PartitionSignature}, DevicePath, DevicePathNode, DeviceType, DeviceSubType}, proto::loaded_image::LoadedImage, proto::media::file::{File, FileAttribute, FileInfo, FileMode}, - table::{boot::{MemoryAttribute, MemoryType, OpenProtocolAttributes, OpenProtocolParams, PAGE_SIZE}, cfg::ACPI2_GUID, runtime::ResetType}, + table::{boot::{MemoryAttribute, MemoryType, PAGE_SIZE}, cfg::ACPI2_GUID, runtime::ResetType}, Event, }; use toyos_abi::boot::{KernelArgs, MemoryMapEntry, RootBridgeWindow, MAX_ROOT_BRIDGE_WINDOWS}; @@ -42,6 +42,7 @@ mod arch; mod attempt; mod blackbox; mod bootnext; +mod protocol; mod floor; mod gcd; mod loaderlog; @@ -175,9 +176,9 @@ struct BootPartition { /// Every early-return below is one of those, so none of them panics. fn boot_partition(handle: Handle, system_table: &SystemTable) -> Option { let bs = system_table.boot_services(); - let image = bs.open_protocol_exclusive::(handle).ok()?; + let image = protocol::exclusive::(bs, handle).ok()?; let device = image.device()?; - let path = bs.open_protocol_exclusive::(device).ok()?; + let path = protocol::exclusive::(bs, device).ok()?; let is_hard_drive = |node: &&DevicePathNode| { node.full_type() == (DeviceType::MEDIA, DeviceSubType::MEDIA_HARD_DRIVE) @@ -377,22 +378,7 @@ struct GopInfo { fn query_gop(system_table: &SystemTable) -> Option { let bs = system_table.boot_services(); let gop_handle = bs.get_handle_for_protocol::().ok()?; - // Never `open_protocol_exclusive` here: EXCLUSIVE calls `Stop` on every - // driver holding this protocol BY_DRIVER, and the firmware's graphics - // console is one. - // - // SAFETY: `open_protocol`'s obligation is that this handle and its protocol - // stay installed until the `ScopedProtocol` drops. Nothing between the two - // can uninstall either: the loader is the one image running, it registers - // no event callback, and it calls no boot service that connects or - // disconnects a controller. - let mut gop = unsafe { - bs.open_protocol::( - OpenProtocolParams { handle: gop_handle, agent: bs.image_handle(), controller: None }, - OpenProtocolAttributes::GetProtocol, - ) - } - .ok()?; + let mut gop = protocol::get::(bs, gop_handle).ok()?; let mode = gop.current_mode_info(); let (width, height) = mode.resolution(); @@ -519,7 +505,7 @@ fn tsc() -> u64 { } #[allow(clippy::too_many_arguments)] -fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: vec::Vec, rsdp_addr: u64, gop: Option, boot_part: Option, log_partition_guid: [u8; 16], layout: u32, root_image: Option, entry_tsc: u64, system_table: SystemTable) -> ! { +fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: vec::Vec, rsdp_addr: u64, gop: Option, boot_part: Option, log_partition_guid: [u8; 16], root_image: rootimage::RootImage, entry_tsc: u64, system_table: SystemTable) -> ! { // Said before it is refused, for `report_reach`'s reason. match arch::cpu_as_entered() { Ok(None) => {} @@ -553,8 +539,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v // the boot map runs from: the map holds it wherever that is. let loader = { let bs = system_table.boot_services(); - let image = bs - .open_protocol_exclusive::(bs.image_handle()) + let image = protocol::exclusive::(bs, bs.image_handle()) .expect("firmware answers LoadedImage for the image it started"); let (base, size) = image.info(); (base as u64, size) @@ -612,8 +597,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v None => ([0u8; 16], 0, 0, 0), }; - let (root_image_addr, root_image_len, root_partition_guid, root_read_tsc) = - root_image.as_ref().map_or((0, 0, [0; 16], 0), rootimage::RootImage::handoff); + let (root_image_addr, root_image_len, root_partition_guid, root_read_tsc) = root_image.handoff(); // Built before the exit so the address the kernel is handed is one this // loader can still print and refuse on. @@ -641,7 +625,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec, cmdline: v boot_partition_guid, boot_partition_present, log_partition_guid, - layout, + layout: toyos_abi::boot::LAYOUT, cmdline_addr: cmdline.as_ptr() as u64, cmdline_len: cmdline.len() as u64, root_bridge_window_count, @@ -960,21 +944,6 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { .unwrap_or_else(|e| panic!("slot {}'s cmdline is not UTF-8: {e}", chosen.which.letter())); println!("Boot parameter: {params:?}"); - let layout = if toyos_abi::boot::actuators(params).any(|token| token == toyos_abi::boot::WRITE_NO_LAYOUT_PARAM) { - println!("Kernel arguments: layout 0 on {}", toyos_abi::boot::WRITE_NO_LAYOUT_PARAM); - 0 - } else { - toyos_abi::boot::LAYOUT - }; - - let root_image = if toyos_abi::boot::actuators(params).any(|token| token == toyos_abi::boot::WITHHOLD_ROOT_PARAM) { - println!("ROOT: withheld on {}; the kernel is handed no image", toyos_abi::boot::WITHHOLD_ROOT_PARAM); - chosen.root.free(system_table.boot_services()); - None - } else { - Some(chosen.root) - }; - println!("Loading kernel elf..."); let loaded_kernel = load_kernel_elf(&kernel_bytes); @@ -992,5 +961,5 @@ fn main(handle: Handle, mut system_table: SystemTable) -> Status { watchdog::arm(&system_table, rsdp_addr, params); println!("Starting kernel..."); - start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, layout, root_image, entry_tsc, system_table); + start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, chosen.root, entry_tsc, system_table); } diff --git a/bootloader/src/protocol.rs b/bootloader/src/protocol.rs new file mode 100644 index 00000000000..d8efcaf28a1 --- /dev/null +++ b/bootloader/src/protocol.rs @@ -0,0 +1,41 @@ +//! `clippy.toml` refuses both `BootServices` openers anywhere else, because +//! the attribute decides whose driver is stopped. EXCLUSIVE calls `Stop` on +//! every driver holding the protocol BY_DRIVER (UEFI 2.11 §7.3.9, +//! `OpenProtocol()`), and on `GraphicsOutput` that is the firmware's graphics +//! console, whose screen the loader's own lines are on. [`get`] opens +//! GET_PROTOCOL, which stops nothing; [`exclusive`] opens only a protocol no +//! firmware console drives. + +use uefi::proto::device_path::DevicePath; +use uefi::proto::loaded_image::LoadedImage; +use uefi::proto::media::fs::SimpleFileSystem; +use uefi::proto::ProtocolPointer; +use uefi::table::boot::{BootServices, OpenProtocolAttributes, OpenProtocolParams, ScopedProtocol}; +use uefi::Handle; + +/// A protocol this loader may hold EXCLUSIVE. +pub trait Exclusive: ProtocolPointer {} + +impl Exclusive for LoadedImage {} +impl Exclusive for DevicePath {} +impl Exclusive for SimpleFileSystem {} + +#[allow(clippy::disallowed_methods, reason = "`Exclusive` is the bound the refusal asks for")] +pub fn exclusive(bs: &BootServices, handle: Handle) -> uefi::Result> { + bs.open_protocol_exclusive::

(handle) +} + +#[allow(clippy::disallowed_methods, reason = "the one attribute it passes stops no driver")] +pub fn get(bs: &BootServices, handle: Handle) -> uefi::Result> { + // SAFETY: `open_protocol`'s obligation is that the handle and its protocol + // stay installed until the `ScopedProtocol` drops. Nothing between the two + // can uninstall either: the loader is the one image running, it registers + // no event callback, and it calls no boot service that connects or + // disconnects a controller. + unsafe { + bs.open_protocol::

( + OpenProtocolParams { handle, agent: bs.image_handle(), controller: None }, + OpenProtocolAttributes::GetProtocol, + ) + } +} diff --git a/bootloader/src/rootbridge.rs b/bootloader/src/rootbridge.rs index 1efe118b190..c1682f0d55b 100644 --- a/bootloader/src/rootbridge.rs +++ b/bootloader/src/rootbridge.rs @@ -19,7 +19,6 @@ use toyos_abi::boot::RootBridgeWindow; use toyos_acpi::{memory_windows, Phys, MAX_LIST_BYTES}; use uefi::prelude::*; use uefi::proto::unsafe_protocol; -use uefi::table::boot::{OpenProtocolAttributes, OpenProtocolParams}; const HEAD: &str = "Root bridge:"; @@ -92,21 +91,7 @@ pub fn windows(system_table: &SystemTable, out: &mut [RootBridgeWindow]) - let mut found = 0usize; for (index, handle) in handles.iter().enumerate() { - // Never `open_protocol_exclusive`: EXCLUSIVE stops every driver holding - // this protocol BY_DRIVER, and firmware's own PCI bus driver is one. - // - // SAFETY: `open_protocol`'s obligation is that the handle and its - // protocol stay installed until the `ScopedProtocol` drops. Nothing - // between the two can uninstall either: the loader is the one image - // running, it registers no event callback, and it calls no boot service - // that connects or disconnects a controller. - let bridge = unsafe { - bs.open_protocol::( - OpenProtocolParams { handle: *handle, agent: bs.image_handle(), controller: None }, - OpenProtocolAttributes::GetProtocol, - ) - }; - let bridge = match bridge { + let bridge = match crate::protocol::get::(bs, *handle) { Ok(bridge) => bridge, Err(e) => { println!("{HEAD} handle {index} would not open ({e}), so the kernel is handed no window"); diff --git a/bootloader/src/rootimage.rs b/bootloader/src/rootimage.rs index 3609e088bab..b511b67f143 100644 --- a/bootloader/src/rootimage.rs +++ b/bootloader/src/rootimage.rs @@ -28,7 +28,7 @@ use uefi::prelude::*; use uefi::proto::device_path::{DevicePath, DevicePathNode, DeviceSubType, DeviceType}; use uefi::proto::loaded_image::LoadedImage; use uefi::proto::media::block::{BlockIO, BlockIoProtocol}; -use uefi::table::boot::{AllocateType, MemoryType, OpenProtocolAttributes, OpenProtocolParams, ScopedProtocol}; +use uefi::table::boot::{AllocateType, MemoryType, ScopedProtocol}; /// The unit ROOT's filesystem is written in, and the alignment every buffer /// here is allocated at. @@ -92,11 +92,10 @@ impl RootImage { /// image from: the one handle whose device path is the partition's without /// its last node, the HARDDRIVE one. pub fn boot_disk(handle: Handle, bs: &BootServices) -> Result { - let image = bs - .open_protocol_exclusive::(handle) + let image = crate::protocol::exclusive::(bs, handle) .map_err(|e| alloc::format!("this image's LoadedImage: {e:?}"))?; let device = image.device().ok_or("firmware names no device this image was loaded from")?; - let path = try_get_protocol::(bs, device) + let path = crate::protocol::get::(bs, device) .map_err(|e| alloc::format!("the boot device's path: {e:?}"))?; let nodes: alloc::vec::Vec<&DevicePathNode> = path.node_iter().collect(); let Some((last, disk_nodes)) = nodes.split_last() else { @@ -116,7 +115,7 @@ pub fn boot_disk(handle: Handle, bs: &BootServices) -> Result { // close then fails. .filter(|&candidate| candidate != device) .filter(|&candidate| { - let Ok(path) = try_get_protocol::(bs, candidate) else { return false }; + let Ok(path) = crate::protocol::get::(bs, candidate) else { return false }; path.node_iter().eq(disk_nodes.iter().copied()) }) .collect(); @@ -126,25 +125,6 @@ pub fn boot_disk(handle: Handle, bs: &BootServices) -> Result { } } -fn try_get_protocol( - bs: &BootServices, - handle: Handle, -) -> uefi::Result> { - // SAFETY: `open_protocol`'s obligation is that the handle and protocol stay - // installed until the `ScopedProtocol` drops. This loader is the one image - // running, registers no callback that could uninstall either, and calls no - // boot service that connects or disconnects a controller. - // - // Never exclusive: EXCLUSIVE stops every driver holding the protocol, and - // on a disk that is the partition driver the ESP's filesystem sits on. - unsafe { - bs.open_protocol::

( - OpenProtocolParams { handle, agent: bs.image_handle(), controller: None }, - OpenProtocolAttributes::GetProtocol, - ) - } -} - /// The boot disk, read through the firmware's block I/O. pub struct Disk<'a> { io: ScopedProtocol<'a, BlockIO>, @@ -158,7 +138,7 @@ pub struct Disk<'a> { impl<'a> Disk<'a> { pub fn open(bs: &'a BootServices, handle: Handle) -> Result { - let io = try_get_protocol::(bs, handle).map_err(|e| alloc::format!("the boot disk's block I/O: {e:?}"))?; + let io = crate::protocol::get::(bs, handle).map_err(|e| alloc::format!("the boot disk's block I/O: {e:?}"))?; let media = io.media(); if !media.is_media_present() { return Err("the boot disk reports no media".into()); diff --git a/bootloader/src/slot.rs b/bootloader/src/slot.rs index 424bcfe8764..fd072b7c61d 100644 --- a/bootloader/src/slot.rs +++ b/bootloader/src/slot.rs @@ -246,8 +246,7 @@ enum FileRefused { /// `max` bytes. fn read_file(bs: &BootServices, guid: &[u8; 16], path: &str, max: u64) -> Result, FileRefused> { let handle = crate::loaderlog::volume_handle(bs, guid).map_err(FileRefused::Other)?; - let mut fs = bs - .open_protocol_exclusive::(handle) + let mut fs = crate::protocol::exclusive::(bs, handle) .map_err(|e| FileRefused::Other(alloc::format!("would not open its volume ({e})")))?; let mut root = fs.open_volume().map_err(|e| FileRefused::Other(alloc::format!("has no volume ({e})")))?; let name = CString16::try_from(path.replace('/', "\\").as_str()) diff --git a/clippy.toml b/clippy.toml index c83c33fcdf9..6f96d7356d0 100644 --- a/clippy.toml +++ b/clippy.toml @@ -4,4 +4,6 @@ disallowed-methods = [ { path = "alloc::sync::Arc::increment_strong_count", reason = "hand-rolled refcounting is the bug class the object layer deletes" }, { path = "alloc::sync::Arc::decrement_strong_count", reason = "hand-rolled refcounting, and the half that frees" }, { path = "core::mem::forget", reason = "a resource nobody gives back is a leak unless its site says why" }, + { path = "uefi::table::boot::BootServices::open_protocol_exclusive", reason = "EXCLUSIVE stops every driver holding the protocol, the firmware's graphics console among them: open through `protocol::exclusive`" }, + { path = "uefi::table::boot::BootServices::open_protocol", reason = "its caller picks the attribute, and EXCLUSIVE or BY_DRIVER stops the driver holding the protocol: open through `protocol::get`" }, ] diff --git a/issues/README.md b/issues/README.md index 2bf4ceac71c..a8a12f28438 100644 --- a/issues/README.md +++ b/issues/README.md @@ -151,5 +151,4 @@ the adapter's author did not have to rediscover it. the kernel writes no log file — `/system/bin/logd` does, an ordinary user process that owns "every policy about files — where they go, what they are called, how many there are, what happens when the stick stops answering" -(`userland/logd/src/main.rs:1-10`). Gated by -`kernel_log_file`, `log_partition_layout` and `log_partition_identity`. +(`userland/logd/src/main.rs:1-10`). diff --git a/issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md b/issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md index 9d619169588..fbd5375e4e7 100644 --- a/issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md +++ b/issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md @@ -58,4 +58,4 @@ this file is deleted. `bootloader/src/main.rs`; unheld. **Its test is deleted**: `86e606616` took `root_chunk_refused_on_a_usb_stick` -out, and `git revert 86e606616` brings it back. +out. diff --git a/issues/boot-media/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md b/issues/boot-media/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md index 362119af8f2..bb94c918608 100644 --- a/issues/boot-media/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md +++ b/issues/boot-media/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md @@ -70,6 +70,4 @@ The partition-claim code, held by the orchestrator. **Its test is deleted**: `0e19bf898` took `partition_claim_departure` out, host and guest halves; `b5c59cbcc` its actuator `usb-transport-break-owed`. -`git revert 9ebf080e8 b5c59cbcc 0e19bf898` brings it -back as it stood before #536, `log_flush_retry` with it; `git show 84471bc58:tests/common/partclaim.rs` holds #536's -adaptation of it. +`git show 84471bc58:tests/common/partclaim.rs` holds #536's adaptation of it. diff --git a/issues/boot-media/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md b/issues/boot-media/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md index 152f5ea2417..0d4a083f1e9 100644 --- a/issues/boot-media/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md +++ b/issues/boot-media/partition-claim-gives-up-reds-beside-other-guests-and-is-green-alone.md @@ -95,8 +95,7 @@ deadman path this file already names as the cause. No code change made on `partition_claim_gives_up` out with `partclaim-table-unanswered` and `partclaim-root-withheld`, the actuators only it armed. It also armed `fsync-budget-spent` and `fsync-deadman-now`, which `9ebf080e8` took out with -`log_flush_retry`, so `git revert 9ebf080e8 fa4c31409` brings it back, -`log_flush_retry` with it. +`log_flush_retry`. **Exit**: the fsync staging scoped to the claim it is staged for, so the boot's own `/log` fsync cannot meet it first, and the test restored and green diff --git a/issues/build/a-loaded-suite-reds-a-volume-checker-on-both-arms.md b/issues/build/a-loaded-suite-reds-a-volume-checker-on-both-arms.md index 9db9d198004..3fb7de7f953 100644 --- a/issues/build/a-loaded-suite-reds-a-volume-checker-on-both-arms.md +++ b/issues/build/a-loaded-suite-reds-a-volume-checker-on-both-arms.md @@ -190,4 +190,4 @@ its module `tests/common/toybox.rs`), `redirty_mid_flush` (`5c3f464a1`, with its binary and `test-small-caches`), `fs_rename_durable` (`63b0874ba`, with its binary), `esp_filesystem` (`27a926141`, with `esp_files`), `device_claim_lifetime` (`51cc87fcc`) and `screen_i8042_health` -(`b7f157a72`). `git revert` of each brings its test back. +(`b7f157a72`). diff --git a/issues/build/a-ready-marker-read-off-the-16550-file-can-end-the-boot-wait-mid-line.md b/issues/build/a-ready-marker-read-off-the-16550-file-can-end-the-boot-wait-mid-line.md new file mode 100644 index 00000000000..7da4252e47c --- /dev/null +++ b/issues/build/a-ready-marker-read-off-the-16550-file-can-end-the-boot-wait-mid-line.md @@ -0,0 +1,26 @@ +--- +status: open +kind: tooling +opened: 2026-10-01 +--- + +# A ready marker read off the 16550 file can end the boot wait mid-line + +`638L-638r5-whole.log` (`wt/toyos-tight` `59940c452`, "ceilings paid at 1.00x"): +`root_candidate_malformed` failed with "the loader did not refuse a ROOT its signature does not +cover: Slot A: REFUSED, its root is". The loader's line is "Slot A: REFUSED, its root is not the +bytes its signed header names" (`648-648-whole.log`, the same test passing), so the capture held +the first half of it. + +`wait_for_ready` (`tests/common/qemu.rs`), waiting for a marker other than the default, looks for +it in the 16550's log file once a second, and QEMU writes that file a byte at a time as the guest +prints. `ROOT_REFUSED` was "Slot A: REFUSED, ", so a read between those bytes and the rest of the +line ended the wait with the line cut short. `root_candidate_malformed` has left the guest suite +for a host test in `toyos-rootimage`; every guest test that waits on a 16550 marker still reads +the file the same way. + +Owner: the orchestrator. + +**Exit**: the boot wait ends on a whole line of the 16550 file, and a `tests/checks` case that +grows that file a byte at a time past a marker reds when the wait returns before the line's +newline. diff --git a/issues/build/a-test-asserts-a-daemons-line-off-a-boot-log-that-ends-before-it.md b/issues/build/a-test-asserts-a-daemons-line-off-a-boot-log-that-ends-before-it.md new file mode 100644 index 00000000000..d9d3493341f --- /dev/null +++ b/issues/build/a-test-asserts-a-daemons-line-off-a-boot-log-that-ends-before-it.md @@ -0,0 +1,36 @@ +--- +status: open +kind: tooling +opened: 2026-10-01 +--- + +# A test asserts a daemon's line off a boot log that ends before it + +`QemuInstance::boot_log` ends at test-runner's `===READY===`, and init starts the daemons before +test-runner, so a daemon's line can come after the marker. Two tests read one from it: + +- `iommu_virtio_platform`, red on four branches that do not touch it: `650-libcllvm-whole.log` + (`wt/toyos-libcllvm`) and `634r2-whole.log` (`wt/toyos-sk6` `fb0fc7b56`), `"netd: this claim + answers 4096 bytes of configuration space and refuses every access outside them" never reached + the boot console`; `637r2-whole-suite.log` (`wt/toyos-libcxx` `26a4bfbe1`) and + `641f-r3-whole.log` (`wt/toyos-tonefix` `597d60a4e`), `QEMU created 3 virtio function(s) … and + the guest negotiated features with 2`, the missing one netd's own. In the 650 run netd started + at 4.905 s and the marker came at 5.098 s with neither of netd's lines before it. +- `lan_dhcp_lease`, red in `638L-638r5-whole.log` (`wt/toyos-tight` `59940c452`): `"netd: ready, + at most " never reached the the lan boot after "netd: DHCP: lease "`. It awaits the lease line, + which the boot log already carries, so the wait returns at once and the capture is whatever + arrived before `===READY===`: + + ``` + {1.281 netd} netd: DHCP: lease 10.0.2.15/24 from 10.0.2.2, gateway 10.0.2.2, dns [10.0.2.3], 43 ms after netd came up + {1.290 test-runner} ===READY=== + ``` + +#639 has each `iommu_virtio_platform` arm wait on the guest for the daemon's lines it reads +(`d773a4306`). `lan_dhcp_lease` has left the guest suite: its T14 row +judges the whole readback, which ends at the boot's last word. + +Owner: the orchestrator. + +**Exit**: no guest test reads a daemon's line out of `boot_log()` without waiting on the guest +for it, and `iommu_virtio_platform` passes in a whole-suite run beside other worktrees' builds. diff --git a/issues/build/building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md b/issues/build/building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md deleted file mode 100644 index 7a5680e1a46..00000000000 --- a/issues/build/building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -status: none -kind: rejected -opened: 2026-08-15 ---- - -# Building the boot image once and shipping it to the shards cannot shorten the matrix - -Twelve shards each build the same tree before their first verdict, which reads -like twelve times the work — and it is, in runner minutes. It is not twelve -times the *wall clock*, because the twelve build concurrently and a job that -built once for all of them would sit on the same clock they do. Measured, so -nobody spends the day re-deriving it. - -**The floor, run `31896922288`** (`main` at `e064a96`, twelve KVM shards), -means over the twelve unless a range is given: - -| phase | s | -|---|---| -| `deps` (apt into `debian:sid`) | 52–59 | -| checkout + rustup + `install-toolchain.sh` | 14–20 | -| `actions/cache/restore` | 11–22 | -| **job setup, summed** | **83.9** (80–99) | -| `suite` step to `running N tests` — host crates, 110 C tests, `toyos-ld`, `toyos-cc`, 103 Rust test binaries | **76.4** (72.8–78.6) | -| `suite` step to the first `PASS` — the above plus the shipping image and its guest | **111.7** (105.2–119.2) | - -So a shard's first verdict lands at about **196 s** into its job, and 112 s of -that is a build every one of the twelve performs identically. - -**The arithmetic that declines it.** A dedicated builder job pays the same -83.9 s of setup and the same ~112 s of build, so the artifact cannot exist -before **T+196 s** — which is exactly when a shard that built it itself already -has it. `needs:` on such a job would idle every shard for its whole duration; -polling for the artifact instead (the shape `toolchain-ready` uses) reaches the -same instant, plus an upload and a download. `cache-writer` measures the -builder: its whole job, one fast test included, is **213 s**. - -Nor can the builder start earlier. Everything it compiles needs the toolchain, -and `toolchain-ready` is what gates the matrix in the first place. - -Nor does it help the shards that pay *more* than the floor: shipping -`metalcase`'s and `sshdcase`'s images too would put 198 s and 145 s of build -in series inside one job, past 500 s, against the 347 s widest shard it was -meant to shorten. - -**What the idea would buy is runner minutes** — about 11 × 112 s ≈ 1,230 s per -run — and `ci.yml` already records why that is not the currency: the repository -is public and its minutes are unmetered. The queue is the thing minutes buy, and -the `target/` cache the shards restore already spent the large one there — 3,036 -s of runner time and 228 s of critical path per run, measured on two consecutive -attempts of run `31389081797`. - -**What is still on the floor and is not this.** The 52–59 s `deps` step is a -package install repeated in every guest job on every run, and it is not a build -at all: `35383398^:.github/ci-image/Dockerfile` bakes those packages into a published -image, and the cutover retires the step once the first published digest exists -for the guest workflows to pin. - -Rejected on measurement, 2026-08-15, by the CI wall-clock task that was sent to -build it. diff --git a/issues/build/console-line-atomicity-loses-five-of-a-thousand-lines-on-ci.md b/issues/build/console-line-atomicity-loses-five-of-a-thousand-lines-on-ci.md index 08152da71bb..6f38cd52eaf 100644 --- a/issues/build/console-line-atomicity-loses-five-of-a-thousand-lines-on-ci.md +++ b/issues/build/console-line-atomicity-loses-five-of-a-thousand-lines-on-ci.md @@ -22,6 +22,5 @@ out the contention shape that file is about. **Exit condition.** The lost lines' cause is fixed, shown against `console_line_atomicity` as it stands at `1808fb8d` (its binary, the test -runner's `CONSOLE_JOBS` stdin and its harness arm), restored and green on CI's -`guest` shards, one guest per machine. +runner's `CONSOLE_JOBS` stdin and its harness arm), restored and green. Owner: orchestrator. diff --git a/issues/build/log-reserve-window-negative-times-out-beside-other-guests.md b/issues/build/log-reserve-window-negative-times-out-beside-other-guests.md index bef3870d713..c07e03e7f93 100644 --- a/issues/build/log-reserve-window-negative-times-out-beside-other-guests.md +++ b/issues/build/log-reserve-window-negative-times-out-beside-other-guests.md @@ -30,8 +30,7 @@ fixed at the cause. `log_reserve_window_negative` out with `log-unbracketed-reserve`, the actuator only it armed. `4db54ffa5` then took `log_reserve_window` and `log-nested-reserve`, and `3b8102cf5` `log_nested_emit` with the nest vector -and test-runner's `log-gate`, all of which it rode, so `git revert 3b8102cf5 -4db54ffa5 9ee7a7573` brings it back with the other two. +and test-runner's `log-gate`, all of which it rode. `blocked_dump`, the other name this file saw red, is deleted too: `issues/build/parallel-tests-red-under-other-suites.md` records the commit. diff --git a/issues/build/no-device-class-answers-for-a-block-device.md b/issues/build/no-device-class-answers-for-a-block-device.md deleted file mode 100644 index 083bdf45331..00000000000 --- a/issues/build/no-device-class-answers-for-a-block-device.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -status: open -kind: tooling -opened: 2026-09-02 ---- - -# `driver_wait_refused` cannot ask whether the stuck NVMe bound, because no class names one - -`driver_wait_refused` (`tests/toyos.rs`) boots with `nvme-rdy-stuck` and -`virtio-reset-stuck`, reads the two refusal lines off the console and reports -"the boot came up without them". Nothing asks the machine whether either device -is there. A kernel that names both refusals and still exposes the stuck NVMe -passes, and the success line says the opposite. - -**For the virtio half there is an oracle and it is simply not wired**: -`kernel/src/device.rs`'s `try_claim` answers `ClaimError::Absent` for a -`pci::` request naming a function this machine does not have, -and `/system/bin/init` prints `init: : no on this machine -()` per refused claim (`userland/init/src/main.rs`). This boot's config -claims no NIC, so that line is not on its console and reaching it means giving -the config a claimant. - -**For the NVMe half the oracle does not exist.** `DeviceType` has seven variants -and none of them is a block device: - - $ rg -n "^ *[A-Za-z]+ = [0-9]+ =>" toyos-abi/src/syscall.rs - Keyboard = 0 => "keyboard", - Mouse = 1 => "mouse", - Framebuffer = 2 => "framebuffer", - HdaAudio = 5 => "hda-audio", - VirtioSound = 6 => "virtio-sound", - PciFunction = 7 => "pci", - -(3 and 4 are retired.) `PciFunction` is not the answer either: it names one -function by vendor and device id and hands it to a process to drive, and what -this gate has to ask is whether a controller *the kernel* binds bound. So no -`SYS_DEVICE_CLAIM` can answer for the stuck controller, and -the only thing that changes when a block device binds is downstream — a mount, -a `/home`, a `NVMe: block device id=` line — all of which are the same class of -console evidence the gate already rests on. - -## Exit condition - -Either a class that names a block device, so the claim table can be asked, or a -different instrument that reports the machine's bound block devices to a guest. -Then `driver_wait_refused` requires both stuck devices absent rather than -requiring only that they were named. - -The sibling gate `hda_two_live_refused` is **not** in this record: init claims -`hda-audio` before it spawns soundd, and soundd reaches the null sink only where -that endowment is missing, so its existing `must_say(NULL_SINK)` already requires -`try_claim(HdaAudio)` to have answered `Absent`. diff --git a/issues/build/nothing-refuses-the-loader-an-exclusive-gop-open.md b/issues/build/nothing-refuses-the-loader-an-exclusive-gop-open.md deleted file mode 100644 index 3b5d7fac22a..00000000000 --- a/issues/build/nothing-refuses-the-loader-an-exclusive-gop-open.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -status: assigned -kind: tooling -opened: 2026-10-01 ---- - -# Nothing refuses the loader an exclusive GOP open - -`query_gop` (`bootloader/src/main.rs`) opens `GraphicsOutput` with -`GetProtocol`. An exclusive open calls `Stop` on every driver holding the -protocol BY_DRIVER, the firmware's graphics console among them, so the panel -stops at the GOP query and every later loader line is on serial alone. Only -the comment at the open says so. - -`screen_loader_lines` was the one check, and `212516e71` deletes it, red on -`main`'s nightly 36696295750 at `ace064f9d`, `guest (8)`: - -``` -FAIL screen_loader_lines: the panel carried 26 rows at the GOP query and 41 at the loader's last line, a growth of 15, where the loader printed 14 lines between them, 14 rows at 240 columns -``` - -`git revert 212516e71` brings it back. #640's head `6e0d7da82` counts the -rows without OVMF's boot logo, the cause it names for that red, and its -nightly 36763711317 passed the test in `guest (10)`. - -Held by #660 (`wt/toyos-guestcut`): `clippy.toml` refuses -`BootServices::open_protocol_exclusive`, and the loader's exclusive opens go -through `bootloader/src/exclusive.rs`, bounded by `Exclusive`, which -`GraphicsOutput` does not implement. - -**Exit**: #660 lands, and a build fails on an exclusive open of -`GraphicsOutput` in the loader: `cargo run -- --clippy` on -`bs.open_protocol_exclusive::(gop_handle)`, and the loader's -compile on `exclusive::open::(bs, gop_handle)`. diff --git a/issues/build/parallel-tests-red-under-other-suites.md b/issues/build/parallel-tests-red-under-other-suites.md index e0a0701ec73..246e92ddaa4 100644 --- a/issues/build/parallel-tests-red-under-other-suites.md +++ b/issues/build/parallel-tests-red-under-other-suites.md @@ -479,8 +479,7 @@ mechanism for it. ## Deleted as flaky tests -A flaky test is deleted at once. Each commit below takes one out, and -`git revert` of it brings it back: +A flaky test is deleted at once. Each commit below takes one out: - `metal_job_reboot` — `99ee9625d`, also on `issues/build/metal-job-reboot-drained-no-kernel-output-beside-other-guests.md`; @@ -489,12 +488,12 @@ A flaky test is deleted at once. Each commit below takes one out, and - `launcher_refusals` — `4c191469f`; - `screen_console_shell` — `958ada05e`; - `screen_console_clear` — `315526e83`, and `c7d9efeb1` retired `SYS_DEBUG` - action 8, which only it asked for: `git revert c7d9efeb1 315526e83`; + action 8, which only it asked for; - `fs_transactional` — `8e172f7a8`; - `fs_dirs_durable` — `690fa3e83`; - `i8042_undecoded_bytes` — `c6923cd50`, with `i8042-split-burst`; - `log_poll_outlives_a_close` — `ad6dc0781`, with test-runner's `log-close` - and `log-close-cancels-any-syscap`: `git revert 87f74892d ad6dc0781`; + and `log-close-cancels-any-syscap`; - `metal_sim_pointer_churn` — `525e59ad1`; - `blocked_dump` — `0a7fc5f70`, red after its retirement here on the sightings `issues/build/log-reserve-window-negative-times-out-beside-other-guests.md` @@ -512,5 +511,3 @@ Named in this file and not deleted: `desktop_typing_damage` waits on `terminal: ready` since its row; `i8042_absent` no longer has the two-boot allowance its row is about; `hda_tone`, `tlb_shootdown_waits` and `wake_storm_cost` are T14 rows and no QEMU guest runs them. - -`screen_loader_lines` is deleted; `issues/build/nothing-refuses-the-loader-an-exclusive-gop-open.md` records the commit that restores it. diff --git a/issues/build/process-stats-exits-101-beside-other-guests.md b/issues/build/process-stats-exits-101-beside-other-guests.md index f495381d1de..a024d03bcfc 100644 --- a/issues/build/process-stats-exits-101-beside-other-guests.md +++ b/issues/build/process-stats-exits-101-beside-other-guests.md @@ -19,13 +19,22 @@ gated goes near any of them. is its own entry. Nothing here investigates the mechanism: `ALONE: GREEN` is the harness naming a hypothesis, and one red is not a rate. -Exit: a rate — the same suite run repeatedly with and without a second -worktree's build on the host — that says whether this is contention the harness -should schedule around or a defect the guest has, and the name is -fixed at the cause. +Two more, each with its assertion. `640r3-loaderlines-r3-whole.log` (`wt/toyos-loaderlines` +`6e0d7da82`, "fastest boot 480 ms … ceilings paid at 1.00x") at `process_stats.rs:280`: "a +child that parked writing a full connection charged 0 ns to ipc and 0 ns to pipe". +`648-648-whole.log` (`wt/toyos-proclife1` `60ec86df3`, load average 84) at +`process_stats.rs:263`: "a child that parked reading a connection charged 0 ns to ipc and 0 ns +to pipe". Neither branch touches the test, `WaitClass` or the charge. The premise both arms read +is `roster::await_true` seeing the child's main thread `BLOCKED`, and nothing ties that park to +the connection: a park on anything else before the child reaches its `read` or `write` +satisfies it, the parent releases, and the connection's wait never parks. The assertion prints +two of the five classes, so which park was charged is not on record. Owner: the orchestrator. + +Exit: each arm waits for a park it can name as the connection's, so a park on anything else +fails the arm by name, and `process_stats` passes on the T14's shared boot. **Its test is deleted**, as a flaky test is: `4a5b228d2` took `process_stats` out, and moved the nightly `tcg` job's one test to -`empty_dir_stat`; `git revert 4a5b228d2` brings both back. `blocked_dump` is +`empty_dir_stat`. `blocked_dump` is deleted too: `issues/build/parallel-tests-red-under-other-suites.md` records the commit. diff --git a/issues/build/qemu-drops-console-output-the-harness-is-slow-to-read.md b/issues/build/qemu-drops-console-output-the-harness-is-slow-to-read.md index cb8e6605407..95eb2e38925 100644 --- a/issues/build/qemu-drops-console-output-the-harness-is-slow-to-read.md +++ b/issues/build/qemu-drops-console-output-the-harness-is-slow-to-read.md @@ -46,6 +46,18 @@ to stall, and its flood was made four times wider; twenty runs after that, at host loads of 3.9 to 9.0, were 20 green. Every other test still reads the stdio console. +**`blockd_serves_partitions` is a sighting of the same loss.** In `650-libcllvm-whole.log` +(`wt/toyos-libcllvm`, load average 66–74) test-runner printed `blockd_io: PASS bench` at +35.962 s and the bench's process exited `code=0` at 35.978 s, and `===TEST_END +test_rs_blockd_io exit=0===` never reached the harness, which waited 3559 s until the run was +ended by hand. That boot read the stdio console. +Every userland line stops at 35.962 s while the kernel's own ten-second `sched:` and `PMM:` lines +go on, and user `mmap` held rises from 23 to 30 between 74.9 s and 3026.8 s, so a process kept +running through the silence; the capture cannot tell a dropped marker from a `logd` that stopped +forwarding. The tree moved `rust` from `aca5f527f` to `9151571ca`, which changes std's exported C +`malloc`, `free` and `realloc` in every Rust guest program, so reading it as this loss rests on +that change being off its path. Owner: the orchestrator. + ## Exit condition The console chardev the harness reads cannot refuse a write — for example a @@ -55,6 +67,4 @@ runs beside a full suite. **`log_stream_stalled_reader` is deleted**, as a flaky test is, on the two reds recorded here: `96763794e` took it out, and `cc291947e` then deleted -`BootOptions::console_file`, which only it set. `git revert cc291947e -96763794e` brings both back, and the exit condition's twenty runs wait on that -restore. +`BootOptions::console_file`, which only it set. diff --git a/issues/build/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md b/issues/build/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md index 30ea7960c3c..de8d4dcd223 100644 --- a/issues/build/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md +++ b/issues/build/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md @@ -37,8 +37,7 @@ Not investigated further. `smp_failed_ap_leaves_no_hole` out with `smp_hole_shootdown`, the binary only it ran, and `git revert aedcf17dc` brings it back. `4db54ffa5` took `log_reserve_window` out with `log-nested-reserve`; `3b8102cf5` then took the -nest vector and the log gate it rode, so `git revert 3b8102cf5 4db54ffa5` -brings it back, `log_nested_emit` with it. +nest vector and the log gate it rode. **Exit**: a cause for `spawn_init`'s `WouldBlock` and for a root read that misses its budget under host load, and both tests restored and green beside diff --git a/issues/build/stdio-buffering-saw-one-long-line-arrive-as-two.md b/issues/build/stdio-buffering-saw-one-long-line-arrive-as-two.md index 9ea1342fc59..c7508767a2b 100644 --- a/issues/build/stdio-buffering-saw-one-long-line-arrive-as-two.md +++ b/issues/build/stdio-buffering-saw-one-long-line-arrive-as-two.md @@ -28,8 +28,7 @@ is the question; `console_line_atomicity` is the gate that should hold the first. **Its test is deleted**, as a flaky test is: `2dfe1008e` took -`90_stdio_buffering` out with its `C_METAL_SKIP` row, and `git revert -2dfe1008e` brings it back. +`90_stdio_buffering` out with its `C_METAL_SKIP` row. **Exit**: which side split the line named, the guest's two writes or the host's capture, and the case restored and green beside other guests. diff --git a/issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md b/issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md index dc50bcc3d45..b00a133dd5f 100644 --- a/issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md +++ b/issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md @@ -19,9 +19,9 @@ arrives and is not one. M4 and M5 are stages of `issues/build/toyos-builds-itsel | `sh` running LLVM's `config.guess`, and the POSIX tools and `cc` it runs | LLVM's CMake, whenever this host builds an LLVM, and the C++ runtime's, in every sysroot build, ask it the host's triple, unconditionally (`get_host_triple` in `rust/src/llvm-project/llvm/cmake/modules/GetHostTriple.cmake`, which runs `sh` by name) | refused: a Rust tool does the shell's part, brush 0.4.0: on the development host (macOS, arm64) `config.guess` printed `/bin/sh`'s triple under it, `arm64-apple-darwin27.0.0`, exit 0 each. The script runs `sed`, `uname`, `mktemp`, `grep`, `rm`, `rmdir` and `cc` there under either shell, and that `cc` is the `cc` rows'. Five of the other six are refused, uutils' doing each: under brush with sed 0.2.0, grep 0.2.0 and coreutils 0.12.0's `mktemp`, `rm` and `rmdir`, and nothing else on `PATH` but the host's `uname` and `cc`, it printed that triple, exit 0. `uname` is admitted: coreutils 0.12.0's answers `-p` with `unknown` where macOS's answers `arm`, and `config.guess` reads that as PowerPC, `powerpc-apple-darwin27.0.0`, exit 0 | CMake finds brush as its `sh`, uutils' `sed`, `grep`, `mktemp`, `rm` and `rmdir`, and a Rust `uname` that answers `-p` as the host's does; or M5 runs it in the guest | | `git` for worktrees, submodules, checkouts, fixtures and rustc's bootstrap | adds, removes and prunes worktrees (`src/worktree.rs`, `src/sysroot.rs`); updates submodules (`src/lib.rs`, `src/sysroot.rs`, `src/licence.rs`, `src/release.rs`); fetches the fork from the primary's and checks it out (`src/sysroot.rs`); fast-forwards the primary (`src/sync.rs`); makes the tests' fixture repositories; runs inside rustc's bootstrap | admitted: no Rust tool does the job, gitoxide 0.85 adds, removes and prunes no worktree, updates no submodule, stages, resets and pushes nothing, checks out only a fresh clone and fetches a local path by spawning `git`; a fixture must be what `git` makes, and bootstrap runs `git` itself | M4 runs it in the guest | | `git` for reads, a config write, a commit's paths written out, and clones and fetches over HTTPS | `rev-parse`, `show-ref`, `for-each-ref`, `rev-list`, `log`, `branch --contains`, `merge-base`, `ls-tree`, `ls-files`, `cat-file`, `config --get-regexp`, `worktree list`, `status`, `diff`, `ls-remote` and `grep`, in the build system and its tests; `config --global --add safe.directory` in the nightly's containers; `checkout -- ` through an index of its own, which writes the C++ runtime's sources out of the LLVM commit into the stored LLVM (`src/llvm.rs`); `src/sync.rs`'s fetch of `origin`; every workflow's checkout | refused: a Rust tool does it, gitoxide 0.85, which reads refs, objects, the index, config, worktrees and status, adds a value to a config file and writes it (gix-config 0.58's `File::section_mut_or_create_new`, `SectionMut::push`, `File::write_to`), walks history, diffs, and lists, fetches and clones a remote over HTTPS; `grep` is a search of the files its index names; and gitoxide's CLI 0.59 (gix 0.88) wrote the runtimes' sources of LLVM `849da7d6` into an empty directory, each path's tree through `gix rev parse`, `gix index from-tree` and `gix free index checkout-exclusive`, exit 0 each: the 18759 files `git` writes there, byte for byte and mode for mode | those are gitoxide's | -| `cc`, `c++` and `ar` on a Linux host, `build-essential` on the nightly's runners | rustc links every host binary through `cc`; `cc` and `c++` compile LLVM, clang, LLD and `rustc_llvm` (`src/llvm.rs` names both to bootstrap) and `ring`'s C for `tests/https-server-host` and `tests/https-fetch-host`; `ar` archives what `cc::Build` compiles | admitted: no Rust tool compiles C or C++, or takes rustc's host link | M5: no host in the loop | -| the toolchain's own `clang`, `llvm-ar`, `rust-lld` and `llvm-config`, built from `ToyOSOrg/llvm-project` | rustc links every guest binary with `rust-lld`; `clang` compiles the C corpus and `hello.c` (`tests/common/compile.rs`, `tests/common/clang.rs`) and, with `llvm-ar`, doomgeneric through `cc::Build` (`src/clang.rs`); rustc's bootstrap asks `llvm-config` how to link LLVM | admitted: our fork's C++, which ToyOS can one day build and run; no Rust tool compiles C, `cc::Build` archives with an `ar`, bootstrap reads LLVM through `llvm-config`, and `CLAUDE.md` links everything with `rust-lld` | M5: no host in the loop | -| `ovmf-generic` | the UEFI firmware of the nightly's guest containers (`src/firmware.rs`), packaged by Debian apart from QEMU | admitted: QEMU's own firmware, and no Rust firmware does its job | the instrument's QEMU carries its own firmware | +| `cc`, `c++` and `ar` on a Linux host, `build-essential` on the nightly's runners | rustc links every host binary through `cc`; `cc` and `c++` compile LLVM, clang, LLD and `rustc_llvm` (`src/llvm.rs` names both to bootstrap); `ar` archives what `cc::Build` compiles | admitted: no Rust tool compiles C or C++, or takes rustc's host link | M5: no host in the loop | +| the toolchain's own `clang`, `llvm-ar`, `rust-lld` and `llvm-config`, built from `ToyOSOrg/llvm-project` | rustc links every guest binary with `rust-lld`; `clang` compiles the C corpus (`tests/common/compile.rs`) and, with `llvm-ar`, doomgeneric through `cc::Build` (`src/clang.rs`); rustc's bootstrap asks `llvm-config` how to link LLVM | admitted: our fork's C++, which ToyOS can one day build and run; no Rust tool compiles C, `cc::Build` archives with an `ar`, bootstrap reads LLVM through `llvm-config`, and `CLAUDE.md` links everything with `rust-lld` | M5: no host in the loop | +| `ovmf-generic`, `qemu-efi-aarch64` | the x86-64 and AArch64 UEFI firmware of the nightly's guest containers (`src/firmware.rs`), packaged by Debian apart from QEMU | admitted: QEMU's own firmware, and no Rust firmware does its job | the instrument's QEMU carries its own firmware | | `ca-certificates` | the trust store `git` and `curl` verify against in the nightly's containers | admitted: data both of them need | goes when neither runs there | | `env` | the two portability jobs run `cargo run -- --build-only` under it, with `GITHUB_ACTIONS` and `CI` unset | refused: a Rust tool does it, the build system, which already runs that build with both removed (`src/release.rs`) | the build system removes both itself | | the T14's Ubuntu and every tool `src/metal.rs` runs on it over `ssh` | the metal loop, on the T14 and never on a development host | outside the rule: recovery equipment on a test machine, not the build's host | they leave with Ubuntu (`issues/boot-media/the-machine-updates-itself-without-ubuntu.md`) | diff --git a/issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md b/issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md new file mode 100644 index 00000000000..6d5c436518e --- /dev/null +++ b/issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md @@ -0,0 +1,304 @@ +--- +status: open +kind: track +opened: 2026-10-01 +--- + +# The guest suite runs only what no cheaper tier reaches + +The ladder is the review's (`.claude/agents/reviewer.md`, **Guest tests**). Each name below is a +guest test the first cut deleted whose behaviour no cheaper tier holds yet, with the tier that is +to hold it and the behaviour it guarded; a name that keeps a `METAL` row is listed for what its +row does not hold. Stages are independent and run in parallel; a stage's +pull request deletes the lines it meets, and the stage once it is empty. A row brings back from +`main` before the cut whatever actuator, feature or harness its arm needs; an item names what is +more than an actuator. Owner: the orchestrator, which dispatches each stage. + +## Stage A: boot, the loader and updates + +Host: `toyos-rootimage`, `toyos-update`, `toyos-gpt`. Metal: the T14's boot records. Exit: each +name is a `#[test]` in the named crate or a `METAL` row, and reverting the behaviour reds it. + +- host `root_candidate_malformed`: ROOT bytes the signed header does not cover are refused by name. +- host `root_named_but_absent`: a cmdline naming a partition that is not there is refused. +- host `root_chunk_refused`: an unreadable ROOT chunk is refused by name, never retried forever. +- host `root_candidate_overlaps`: an overlapping ROOT partition is refused. +- host `root_named_twice_on_the_boot_disk`, `root_named_twice`: a twin ROOT is never read. +- host `update_refusals_boot_the_other_slot`: every refused slot is refused by name and the other boots. +- host `update_grant_refuses_a_stray_partition`: init grants only the idle slot's partition. +- host `update_floor_is_the_images_own`: the anti-rollback floor is the key's and the image's. +- host `update_refused_pass_credits_no_image`: a refused pass credits nothing. +- host `screen_gop_firmware_mode`: the loader picks the largest mode both panel and firmware offer. +- host `boot_partition_identity`: the boot partition is found by its unique GUID. +- host `volume_from_another_disk`: DATA on a disk other than the boot disk is refused. +- host `broken_data_volume_is_absent`, `data_candidate_with_bad_geometry_is_absent`: a bad DATA + candidate is absent, not formatted. +- host `block_duplicate_id`: two disks with one identity are refused. +- metal `root_from_memory`: ROOT mounts from the loader's image with no storage command before init. +- metal `root_withheld_refused`: a handoff with no ROOT image is refused by name; brings back + `loader-withholds-root` in the loader as well as the kernel. +- metal `kernel_args_layout_refused`: a loader of another `KernelArgs` layout is refused first; + brings back `loader-writes-no-layout` in the loader as well as the kernel. +- metal `boot_from_power_on`: the power-on spans are the loader's counts at the kernel's rate. +- metal `shipped_config_boots`: the root `system.toml` boots to ready. +- metal `diskless_boot`, `internal_disk_boot`: a boot with no NVMe, and one off the internal disk. +- metal `update_boots_the_new_kernel`: an update over ssh is the kernel the next boot runs. +- metal `update_falls_back_from_a_dying_kernel`, `update_hang_kills_an_unproven_image`: a slot + that dies or hangs falls back, its death in the next boot's `/log`. +- metal `foreign_disk_untouched`: a disk the system was not given comes back byte for byte; + brings back `src/fingerprint.rs`. + +## Stage B: the panic path and the end of a machine + +Metal: the blackbox page and `loader.log` the next boot reads off the T14. Host: the panel +console's renderer. Exit: as stage A. + +- metal `panic_reboots`, `panic_before_peripherals_reboots`: a panic ends the boot inside its + bound, from `percpu::init_bsp` on. +- metal `blackbox_panic_chain`, `blackbox_early_panic_sealed` (and + `blackbox_early_panic_sealed_muted`, the T14's own shape), `blackbox_fault_sealed`: the report + is sealed on the page and the next pass reads it. +- metal `panic_outlives_the_deadline`: a panic crosses the deadline's reset as a panic report. +- metal `hang_bounded_by_the_stick`: a hanging image costs one boot and never traps the machine. +- metal `double_fault_stack`, `syscall_panic_halts`, `syscall_fault_halts`, + `heap_over_ceiling_halts`, `klogd_fault_halts`: each fatal path halts with its own report. +- metal `reentry_names_the_first_panic`, `double_panic_names_the_fault`, + `nested_fault_is_recursive`: a fault inside the report names the first. +- metal `pre_idle_wedge_speaks`: a boot stopped in phase 3 says where. +- metal `panic_halts_the_others_first`: no other CPU's record follows the fatal line. +- metal `watchdog_resets`: the TCO ends the machine. +- metal `quiesce_refuses_a_second_shutdown`: the machine has one shutdown, and a second caller is + refused while the first holds it. +- metal `screen_pager_keys`: PageUp pages a halted report. +- host `screen_paged_scrollback`: a report longer than the panel pages with no input. +- host `screen_early_panel`: the panel repaints after each committed record. +- host `screen_log_absent`: a `/log` that did not mount is said on the panel. +- host `screen_blocked_dump`: Ctrl+Alt+D's summary tells the three states apart. +- host `screen_late_panic`: the fatal report is painted from the snapshot `capture()` froze, so a + record committed after the capture is absent from the panel; and a frame wider than the panel + wraps, its tail on the grid before the next frame (`check_wrap`). `screen_panic_muted` holds + neither: a muted boot refreshes the capture in `halt_all_cpus`. Exit: two host tests over the + renderer and its capture lifted out of the kernel crate, red when `capture` is a no-op and when + a wrapped row is clipped. + +## Stage C: CPUs, memory, scheduling and the kernel log + +Metal: test kernels armed on the T14. Host: `toyos-sched` and its sim, `kernel-loom`, +`toyos-wallclock`. Exit: as stage A; an unrepresentable name is a type the kernel builds against +and a compile-fail case. + +- metal `fpu_isolation`, `gsbase_locked`, first: x86-64 FPU isolation runs nowhere until this + lands. One process's FPU state and GS base never reach another; brings back the + `fpu-save-nothing` and `user-writable-gsbase` features, their negative controls. +- metal `control_regs_negative`: an AP left without the declared control registers is caught, + under `no-ap-control-regs`. +- metal `va_exhaustion`, `heap_ceiling_bounds`: an exhausted address space and heap refuse by name. +- metal `idle_stack_guard`: the idle stack's guard page catches an overflow. +- metal `dump_left_pending_is_owed`: a dump asked in a pass that may not serve it is served later. +- metal `handler_post_without_a_pass`: a claim vector's watch post lands before any pass. +- metal `tls_rebase_window`: a thread's TLS block is never reachable before its rebase. +- metal `kernel_heartbeat`: the kernel's heartbeat record arrives on its period; brings back + `kernel/src/heartbeat.rs`. +- metal `wall_clock_utc`: the wall clock reads the RTC as UTC. +- metal `spawn_cwd`: a spawn's working directory. +- host `log_conservation_smp2`: every record is read or counted lost. +- host `wall_clock_rtc_dead`, `wall_clock_rtc_unstable`, `wall_clock_no_century`, + `wall_clock_century_register`: each RTC shape decodes or is refused. +- unrepresentable `lock_across_switch_halts`: a lock guard cannot be held across a switch. +- unrepresentable `hash_seed_precedes_every_map`: no kernel map is built before its seed. + +## Stage D: storage and filesystems + +Host: `toyos-fat32`, `toyos-fat32-check`, `toyos-blockring`, `bcachefs`. Metal: the T14's stick +and NVMe. Exit: as stage A. + +- host `fsync_failed_commit`: an fsync keeps refusing while the device refuses its flush. +- host `file_mtime_undated`: a file written with no wall clock is stamped as undated. +- host `blockd_serves_partitions`, `blockd_survives_its_death`, `blockd_serves_nothing`: blockd's + protocol, its restart, and its refusals with no controller. +- host `nvme_large_device`, `nvme_wide_sector`: a device past 2 TiB and an 8 KiB namespace. +- host `fsd_two_data`: two DATA partitions are refused by name. +- metal `fsd_restart`, `fsd_end_at_mount`, `fsd_claim_held`: a file server ended under its + clients is restarted, or refused while its claim is held. +- metal `home_overwrite_reads_back`, `apps_and_home_are_one_filesystem`, `layout_fresh_boot`: + `/home`'s bytes and layout on the disk. +- metal `pkg_install_gbae`: `pkg install` lands a package that runs. +- metal `partition_claim`: a partition claimed as a device, and its refusals. +- metal `kernel_log_file`, `log_partition_layout`, `log_partition_identity`: `/log` on the stick + and its layout. +- metal `file_mtime_survives_a_reboot`: a file's mtime survives a reboot. + +## Stage E: USB and xHCI + +Host: `toyos-xhci` and its sim. Metal: the T14's controller and stick. Exit: as stage A. + +- host `xhci_many_devices`, `xhci_second_controller`, `xhci_two_controllers`: devices across one + and two controllers enumerate and deliver. +- host `xhci_msi_only`, `xhci_no_interrupt`: a controller with MSI only, and with no interrupt. +- host `xhci_slot_exhaustion`, `xhci_scan_hands_over_a_free_slot`: slots run out by name and are + handed back. +- host `xhci_deaf_registers`, `xhci_slow_connect`, `xhci_portsc_rw1c`, `xhci_flap`: registers that + never answer, a slow connect, PORTSC's write-one-to-clear, a replug inside the debounce. +- host `xhci_superspeed_ports`: device speeds bind to their ports. +- host `usb_storage_shapes`, `usb_refused_disk_first`, `usb_pool_exhausted`: disk sizes, sector + sizes and counts the driver serves or refuses. +- host `usb_storage_write_error`, `usb_flush_optional`: a refused write and a missing cache flush. +- host `usb_stick_left`: a stick that leaves its port while the port-reset rung holds it, at each + of three points (before the reset's completion is read, after it, and before the rung's TEST + UNIT READY), ends the run as the stick leaving: never `Rung::Offline`, no second break counted, + no Reset Device or Address Device sent to a port the reset or the TEST UNIT READY read empty, + and its slot disabled. Its row arms only `usb-transport-break`, and the T14 cannot pull its own + stick. Exit: a `toyos-xhci` sim test per point, red on each fact alone: when the empty port + climbs the ladder to `Offline`, when it counts a second break, when the sim's port is sent a + Reset Device or Address Device after it read empty, and when the stick's slot is never + disabled. +- metal `usb_reset_records_the_phase_it_cut`: a machine stopped inside a Bulk-Only command at + each of `DataOwed`, `Data` and `StatusOwed` resets itself, and the account the next pass reads + names that phase. Its row arms only `usb-reset-under-load` and reads whichever phase the sweep + cuts. Brings back `usb-wedge-data-owed`, `usb-wedge-in-data` and `usb-wedge-before-status`, + which meet `issues/kernel/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md` + there. Exit: a row per phase, red when `stop::OpenCommand` publishes nothing. +- host `usb_reset_records_the_phase_it_cut`: `usb-reset-under-load` on a disk smaller than + `SWEEP_FLOOR` refuses by name and sweeps nothing; the T14's stick has the room. Exit: a host + test over the sweep's span lifted out of `kernel/src/usb_gate.rs`, red when the floor goes. +- metal `usb_reset_hands_devices_back`, the three resets its row does not reach, each judged on + the account in `loader.log` that `metaldevices::quiesced` reads. Its row judges two orderly + reboots. + - The test runner's job deadline, with `quiesce-late-word` on `tests/jobdeadlinecase`: the stop + took the controller lock before the log volume's flush and completed. Exit: red when + `stop::settle_commands` is reverted. + - The panic console's bound, with `test-late-panic` and `panic-reboot-fast`: no barrier taken, + nothing flushed, and the stop still complete. Waits on + `issues/build/the-metal-loop-cannot-judge-a-boot-that-panics.md`, whose loop refuses that + boot. Exit: as the deadline's. + - The negative control, a controller lock that never comes free (`xhci-lock-wedged`): the + machine hands itself back anyway, and the account says the lock was not free inside its + bound. Exit: red when the barrier's bound is removed, which leaves the machine wedged. +- metal `usb_storage_gate`: the stick is read and written byte for byte. +- metal `late_storage_connect`: a disk that connects after the boot scan is bound, and `/boot` and + `/log` mount off it. Nothing is plugged: `xhci-slow-storage-connect` reports the first root-hub + port empty until the scan has run, and a row whose stick is on another port is red, not vacuous, + because its boot then logs no `usb-storage: 0 device(s)`. + +## Stage F: input and the i8042 + +Host: `toyos-ps2`, `toyos-keymap`. Metal: the T14's controller. Exit: as stage A. + +- host `input_claim_absent`, `keyboard_claim_close_spares_stdin`: a claim with no device, and a + close that leaves stdin armed. +- host `i8042_no_spurious_wake`: drains wake only on an event. +- host `i8042_absent`, `i8042_quarantine`, `i8042_budget_expiry`, `i8042_fadt_denial`, + `i8042_kbd_echo`: the probe's absent, quarantined, expired, denied and echoing controller. +- host `swiss_german_layout`, `locale_detect`, `locale_detect_unrecognized`, + `desktop_locale_detect`: layouts and the locale wizard. +- metal `i8042_health`: the T14's controller reports healthy. + +## Stage G: the desktop, graphics and applications + +Host: `toyos-desktop`, the console's grid. Metal: the T14's desktop. Exit: as stage A. + +- host `metal_sim_window_caps`, `metal_sim_ipc_hostile_peer`, `metal_sim_hostile_clipboard`: the + compositor refuses a hostile client. +- host `metal_sim_compositor_stall`, `metal_sim_client_death`: a stalled or dying client never + stops the desktop. +- host `metal_sim_window_drag`: a drag moves a window. +- host `desktop_typing_damage`: typing damages only the cells it changes. +- metal `metal_sim_compositor`: the compositor runs on the T14's GOP scanout. +- metal `doom_frames`: doom renders a demo to a known digest. +- metal `cxx_runtime`: a C++ program links libc++ and runs. +- metal `toolkit_iced`, `toolkit_window_wake`, `toolkit_winit_loop`, `toolkit_winit_pace`: iced + and winit run unmodified on the desktop. + +## Stage H: the network and remote services + +Host: `toyos-net-tcp`, `toyos-dns`, `toyos-mdns`, `toyos-swap`, `toyos-inspect`, +`toyos-logstream`. Metal: the cable. Exit: as stage C. + +- host `lan_mdns_answer`, `lan_no_lease`: netd answers for its name, and announces with no lease. +- host `netd_connection_caps`, `netd_listener_forgery`, `netd_hostile_peer`: netd refuses forged + flags, hostile peers and excess connections. +- host `netd_slow_reader`, `netd_refused_pipes`, `netd_held_open`: a slow, refused or silent + client costs only itself. +- host `netd_udp_refused`, `netd_udp_any_address`: UDP datagrams too large are refused by name; + `0.0.0.0` receives. +- host `dns_resolve`, `netd_lookup_let_go`: lookups resolve, and abandoned ones are let go. +- host `inspect_reads_its_owners`: `inspect`'s selectors read every owner. +- host `swap_refusals`, `swap_not_inherited`: a wrong digest, a stranger's key and an undeclared + program are refused a swap. +- host `sshd_key_auth`: sshd refuses a key not authorized. +- host `lan_lease_report`: a link that goes down and comes back after a lease neither gives the + lease up nor starts the client over. The T14 cannot flap its cable, and its row reads only a + lease from the bench's router. Exit: netd's link-up decision (its main loop and + `dhcp::restart`) lifted into a function a netd `#[test]` drives with a lease held, red when the + `!dhcp.leased()` guard goes. +- metal `sshd_exec`, the arms `lan_talk`'s one command does not reach, each a step of that row's + exchange (`src/metaltalk.rs`) red when its arm in `userland/sshd/src/main.rs` is reverted: + - a program that is not there ends 127, `cannot run /system/bin/` on stderr and nothing + on stdout; + - an unquotable line ends 127 before anything runs; `command::split`'s own tests already hold + the refusal it names; + - stdout and stderr stay apart: `cat` of a file and of a missing path; + - the channel's input is the program's stdin; + - an `env` request is answered with a failure, never left unanswered; + - a program whose connection goes is ended: no `spin` left running for the next exec to list. +- metal `https_tls13` (and `https_tls13_e1000e`, the same fetch on the 82574): ureq and rustls + fetch over TLS 1.3 on the I219. +- metal `sshd_files`: sftp moves files byte for byte. +- unrepresentable `netd_seeds_its_stack`: the stack cannot be built without the kernel's seed. + +## Stage I: program lines in `/log` + +Metal: the stick's `/log`. Exit: as stage A. + +- `log_program_line`, `log_program_forgery`, `log_carrier_forgery`: a program's line is filed under + its pipe's name, and forged heads are refused. +- `log_after_a_refused_stop`, `log_resume_meets_its_flush`: lines after a refused stop and a + resumed flush reach `/log`. +- `log_program_line_after_its_records`, `log_program_flood`: a flood loses nothing uncounted and + keeps the owner's lines. + +## Stage J: devices, PCI and DMA isolation + +Metal: the T14's VT-d. Host: `toyos-pci`, `toyos-hda`. Exit: as stage A. + +- metal `iommu_discovery`, `iommu_context_absent`, `iommu_empty_domain`, + `iommu_interrupt_remapping`, `iommu_domain_isolation`: the unit is found, and every DMA and + message outside a domain faults. +- metal `iommu_gpu_scanout_swap`, `iommu_gpu_foreign_backing`, `iommu_hda_foreign_bdl`, + `iommu_sound_foreign_dma`, `userdev_dma_fault`: a device aimed outside its grant faults and the + machine runs on. +- metal `userdev_residue_is_its_own`: a claim reads only its own grant's residue. +- metal `blockd_dma_outside_the_lent`, `blockd_lends_within_its_bound`: a userland driver's DMA + stays inside what it was lent. +- metal `swap_quiets_the_function`, `swap_keeps_what_nothing_reset`, `swap_fault_tells_its_holder`, + `swap_resets_the_function`: a released function is stopped, kept, faulted or reset before its + next holder. +- host `pci_function_is_exclusive`, `bar_placement_is_proven`, `pci_claim_caps_truncated`: one + holder per function, BARs moved only once the machine says so, a truncated capability list. +- host `virtio_net_no_msix`: a claim on a function neither MSI-X nor MSI can be armed on is + refused as `Refusal::NoInterrupt`, by name, before any BAR moves, and the other functions keep + their vectors. It was `NoInterrupt`'s one reader + (`issues/kernel/a-claims-own-refusals-are-read-by-nothing.md`). Exit: a host test over + `bring_up`'s arming, red when either arm answers anything else. +- host `swap_refused_device_fails`, `swap_moved_device_fails`: a function whose window was lost or + moved fails the swap by name. +- host `hda_two_live_refused`: two live HDA links are refused by name. +- host `virtio_used_ring`: a used-ring element is refused for a head outside the table, a head + with no chain and a length past its chain (`Virtqueue::parse_used`, pure once it leaves the + kernel crate). +- metal `query_pci_agreement`: the kernel's PCI enumeration equals the T14's Ubuntu `lspci` of the + same machine, function for function: an oracle the kernel did not write. + +## Stage K: the shared boot's own judges + +Metal. Exit: each judge reads on the T14 what its guest judge read. + +- metal `disk_backtrace`, `fault_gates`, `debug_trap`, `dlopen_dedup`, `abuse_elf_loader`, + `exit_wait_storm`: the record beside the exit code — the backtrace's names, the fault's + attribution, the loader's refusal reason, the wait accounting. + +## Stage L: the harness's own guard + +Host. Exit: a host test fails when the harness's death leaves its QEMU running. + +- host `guest_dies_with_its_harness`: a `SIGKILL`ed harness takes its QEMU with it. diff --git a/issues/build/the-metal-loop-cannot-judge-a-boot-that-panics.md b/issues/build/the-metal-loop-cannot-judge-a-boot-that-panics.md index c90ef0f23f8..7725cdf2364 100644 --- a/issues/build/the-metal-loop-cannot-judge-a-boot-that-panics.md +++ b/issues/build/the-metal-loop-cannot-judge-a-boot-that-panics.md @@ -6,9 +6,8 @@ opened: 2026-09-07 # No boot that panics can be judged on the T14 -Three of this project's kernel-performed resets are reachable under QEMU and -judged there by `usb_reset_hands_devices_back`: a job list's `reboot`, the test -runner's job deadline, and the panic console's bound. Only the first two reach +Three of this project's kernel-performed resets are reachable under QEMU: a job +list's `reboot`, the test runner's job deadline, and the panic console's bound. Only the first two reach the T14, and the reason is the loop rather than the kernel. **Two walls, and a boot has to clear both.** @@ -28,14 +27,6 @@ the T14, and the reason is the loop rather than the kernel. unconditionally, so the runner hands the machine back at about one second and the probe never comes due. -## What it costs - -The panic path's register stop (`kernel/src/drivers/xhci/stop.rs`) is the one -arm of the ruling "no reset this kernel performs leaves a USB device -mid-command" that only QEMU has answered. QEMU cannot wedge a stick, so what is -unproven on hardware is exactly the case the ruling is about: the machine's own -controllers, its own stick, and a reset with no shutdown in front of it. - ## What would clear it A per-boot predicate in place of `bootlog::verdict` as the loop's judge — the diff --git a/issues/design-debt/a-lease-kept-across-a-link-flap-is-not-verified-until-its-renewal.md b/issues/design-debt/a-lease-kept-across-a-link-flap-is-not-verified-until-its-renewal.md index 99cb77595a1..8ec4bfb9b6f 100644 --- a/issues/design-debt/a-lease-kept-across-a-link-flap-is-not-verified-until-its-renewal.md +++ b/issues/design-debt/a-lease-kept-across-a-link-flap-is-not-verified-until-its-renewal.md @@ -20,13 +20,6 @@ alternative the client offers, a restart, which gives the address up before it asks again and so takes a machine whose cable only flapped off its network for a whole exchange. -## Evidence - -`lan_lease_report` (`tests/common/lan.rs`) takes QEMU's link away after the -lease and gives it back, and passes only if the report records no second -`leased` line and no `lost` line after the flap: the old lease is kept and no -server was asked about it. - ## Owner The successor of the I219 PHY branch (PR #453) on the LAN track, "The LAN @@ -37,4 +30,4 @@ reaches a router, and is not yet production grade" (stage 4, the stack). The DHCP client verifies a kept lease when the link comes back: a renew-now request, or RFC 2131 §3.2's INIT-REBOOT (a DHCPREQUEST for the address it holds), with the lease kept while the answer is outstanding and given up on a -DHCPNAK. `lan_lease_report` then sees the request after the flap. +DHCPNAK. diff --git a/issues/hardware/a-connect-between-two-accepts-is-reset.md b/issues/hardware/a-connect-between-two-accepts-is-reset.md index 201423b44fb..6104a9eae85 100644 --- a/issues/hardware/a-connect-between-two-accepts-is-reset.md +++ b/issues/hardware/a-connect-between-two-accepts-is-reset.md @@ -27,8 +27,7 @@ the client hides this and does not fix it. **`lan_swap` is deleted**, as a red test is: `bb68c186c` took it out, its QEMU and T14 rows both, with `lan_swap_hold`, the T14 row's judge and the metal harness's swapping boots, which that row was the one user of, and -`29733dba3` then deleted the swap file and `--hand-back` only they used. `git -revert 29733dba3 bb68c186c` brings them back. +`29733dba3` then deleted the swap file and `--hand-back` only they used. **Exit**: a listener that queues a connect arriving between two accepts, and `lan_swap` restored and green. diff --git a/issues/hardware/i8042-mouse-ends-four-packets-short-with-a-clean-exit.md b/issues/hardware/i8042-mouse-ends-four-packets-short-with-a-clean-exit.md index 52fb3e0d0ad..4522d6477b7 100644 --- a/issues/hardware/i8042-mouse-ends-four-packets-short-with-a-clean-exit.md +++ b/issues/hardware/i8042-mouse-ends-four-packets-short-with-a-clean-exit.md @@ -64,5 +64,4 @@ deleted. The i8042/input path, held by the orchestrator. -**Its test is deleted**: `57ac19ada` took `i8042_mouse` out, and -`git revert 57ac19ada` brings it back. +**Its test is deleted**: `57ac19ada` took `i8042_mouse` out. diff --git a/issues/hardware/metal-device-probe-usbread-answered-iofailed-once-beside-other-guests.md b/issues/hardware/metal-device-probe-usbread-answered-iofailed-once-beside-other-guests.md index 5c244ee0a09..c4809769c2a 100644 --- a/issues/hardware/metal-device-probe-usbread-answered-iofailed-once-beside-other-guests.md +++ b/issues/hardware/metal-device-probe-usbread-answered-iofailed-once-beside-other-guests.md @@ -33,5 +33,4 @@ sighting, and whether `usb-storage` logged a transport break before it. **Its test is deleted**, as a flaky test is: `24aa31815` took `metal_device_probe` out, its QEMU and T14 rows both, with the T14 judge and the device inventory in `src/metaldevices.rs` only that judge read — among -them the T14's assertion that blockd drives no NVMe there. `git revert -24aa31815` brings them back. +them the T14's assertion that blockd drives no NVMe there. diff --git a/issues/hardware/pulling-the-boot-stick-freezes-the-t14.md b/issues/hardware/pulling-the-boot-stick-freezes-the-t14.md index e7456cedc96..c770338fc11 100644 --- a/issues/hardware/pulling-the-boot-stick-freezes-the-t14.md +++ b/issues/hardware/pulling-the-boot-stick-freezes-the-t14.md @@ -86,4 +86,4 @@ stood all three would have read `heartbeats stopped at T` — a time and never a class. With `ran=` they read as a time *and* one of two classes, which is what makes a fourth flash worth more than the third was. -`usb_boot_stick_pulled` is deleted, so no gate covers the pull; `issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md` records the commit that restores it. +`usb_boot_stick_pulled` is deleted, so no gate covers the pull. diff --git a/issues/kernel/a-claims-own-refusals-are-read-by-nothing.md b/issues/kernel/a-claims-own-refusals-are-read-by-nothing.md index ee4bdb5dd86..20360ec10e5 100644 --- a/issues/kernel/a-claims-own-refusals-are-read-by-nothing.md +++ b/issues/kernel/a-claims-own-refusals-are-read-by-nothing.md @@ -6,13 +6,11 @@ opened: 2026-09-14 # A claim's own refusals are read by nothing -The refusals `kernel/src/pcidev/mod.rs` raises that are read back are -`ClaimError::Owned` by `pci_function_is_exclusive`, `Refusal::Untranslated` by -`iommu_virtio_platform`'s no-unit arm, `Refusal::NoInterrupt` by -`virtio_net_no_msix`, `Refusal::CapsTruncated` by `pci_claim_caps_truncated`, -the domain by `userdev_dma_fault`, and `SYS_DEVICE_REG_READ`'s bound by netd's -own `config_space_is_bounded`. These are reached by no test: +Refusals of `kernel/src/pcidev/mod.rs` that no test reaches: +- `ClaimError::Owned`, `Refusal::NoInterrupt`, `Refusal::CapsTruncated` and + the domain's fault, whose guest readers the guest suite's cut moved to stage J + of `issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`; - `ClaimError::Ambiguous`, a config naming a device this machine has two of; - `ClaimError::KernelDriven`, a claim on a function one of this kernel's own drivers bound; diff --git a/issues/kernel/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md b/issues/kernel/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md index 55791c6567f..69e9c53ed4d 100644 --- a/issues/kernel/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md +++ b/issues/kernel/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md @@ -35,10 +35,10 @@ Two things are true and neither is decided here: ## Where it bites -Any actuator that stops a CPU inside a driver — today the `usb-wedge-*` arms, -which are QEMU registrations and reach no flashed image. It does not change -their verdicts, but it adds a panic and a page of dropped -records to every one of them. +Any actuator that stops a CPU inside a driver: the `usb-wedge-*` arms, which +went with `usb_reset_records_the_phase_it_cut`'s QEMU registration and come back +as T14 rows in stage E of +`issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`. ## Exit condition diff --git a/issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md b/issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md index 43773ba9b72..570f67e7822 100644 --- a/issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md +++ b/issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md @@ -74,5 +74,5 @@ record before it. That change has never run: the test's first run back is also that change's. **`4f2bea143` holds #588's version of the test**: `git show 4f2bea143:tests/common/usb.rs`. -`usb_stick_left` arms `usb-transport-break`, `usb-reset-moves`, -`usb-reset-moves-after` and `usb-reset-moves-configured`, so they stay. +`usb_stick_left`'s T14 row arms `usb-transport-break`, so it stays; `usb-reset-moves`, +`usb-reset-moves-after` and `usb-reset-moves-configured` went with its QEMU arm in #660. diff --git a/issues/kernel/a-log-rings-owner-is-named-only-when-logd-reads-its-registration.md b/issues/kernel/a-log-rings-owner-is-named-only-when-logd-reads-its-registration.md index 9e68bdfd728..ce649d217d4 100644 --- a/issues/kernel/a-log-rings-owner-is-named-only-when-logd-reads-its-registration.md +++ b/issues/kernel/a-log-rings-owner-is-named-only-when-logd-reads-its-registration.md @@ -53,6 +53,5 @@ line never reached `/log`. That change has never run: the test's first run back is also that change's. **Its test is deleted**: `603b6ee54` took `log_ring_keeps_the_owners_slots` -out, and `git revert 603b6ee54` brings it back as it stood before #536; -`git show 84471bc58:tests/logkeepcase/system.toml` holds #536's adaptation of its -config. +out; `git show 84471bc58:tests/logkeepcase/system.toml` holds #536's adaptation +of its config. diff --git a/issues/kernel/a-shared-boot-stopped-answering-and-no-capture-says-why.md b/issues/kernel/a-shared-boot-stopped-answering-and-no-capture-says-why.md index 3ac8ab4ed33..a4b19b06224 100644 --- a/issues/kernel/a-shared-boot-stopped-answering-and-no-capture-says-why.md +++ b/issues/kernel/a-shared-boot-stopped-answering-and-no-capture-says-why.md @@ -77,5 +77,4 @@ was green on the five nightlies 36400924827, 36496779560, 36550208853, 36600425263 and 36696295750. **`sched_check_build` is deleted**, as a flaky test is: `4d3e2b164` took it -out with `sched-check` from the suite's kernel builds, and `git revert -4d3e2b164` brings both back. +out with `sched-check` from the suite's kernel builds. diff --git a/issues/kernel/a-spawn-of-echo-was-refused-with-an-error-nothing-names.md b/issues/kernel/a-spawn-of-echo-was-refused-with-an-error-nothing-names.md index c2cb50ba4ba..1adbf1af288 100644 --- a/issues/kernel/a-spawn-of-echo-was-refused-with-an-error-nothing-names.md +++ b/issues/kernel/a-spawn-of-echo-was-refused-with-an-error-nothing-names.md @@ -47,6 +47,4 @@ re-run was green. **Its test is deleted**, as a flaky test is: `3b8102cf5` took `log_nested_emit` out with `log-nested-emit`, the nest vector on both -architectures and test-runner's `log-gate` builtin, and `git revert -3b8102cf5` brings them back. The spawn this file is about is -`log_gate.rs`'s record-making child, which `log-storm` still runs. +architectures and test-runner's `log-gate` builtin. diff --git a/issues/kernel/an-ap-its-host-has-not-scheduled-for-100-ms-is-booted-without.md b/issues/kernel/an-ap-its-host-has-not-scheduled-for-100-ms-is-booted-without.md new file mode 100644 index 00000000000..a7643a60aa8 --- /dev/null +++ b/issues/kernel/an-ap-its-host-has-not-scheduled-for-100-ms-is-booted-without.md @@ -0,0 +1,30 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# An AP its host has not scheduled for 100 ms is booted without + +`virt_smp` red in `636r2-virt.log` (`wt/toyos-rulesbatch` `8f142ba9e`, userland only, TCG, +`VirtEl2`, a loaded host, "liveness ceilings paid at 1.93x"): + +``` +[kernel 0.236 cpu0] SMP: cpu6 mpidr=0x6 online +[kernel 0.368 cpu0] SMP: cpu7 mpidr=0x7 did not echo within 100ms (the machine boots with the CPUs that came up before the first that did not); the rest stay off +[kernel 0.372 cpu0] SMP: 7 of 8 MADT CPUs online +``` + +`time::AP_START` gives an AP 100 ms between `CPU_ON` (or the SIPIs) and its echo, and its expiry +boots the machine without that CPU for good. On metal an AP that is alive answers in +microseconds; under any hypervisor a vCPU its host has not scheduled answers late, and 100 ms of +steal reads as a dead CPU. Linux waits 5 s for the same echo on arm64 +(`arch/arm64/kernel/smp.c`, `__cpu_up`: `wait_for_completion_timeout(&cpu_running, +msecs_to_jiffies(5000))`) and 10 s in its generic bring-up (`kernel/cpu.c`, +`cpuhp_wait_for_sync_state`). + +Owner: the orchestrator. + +**Exit**: the bound is one on a dead CPU — the span this kernel already holds a live CPU to +(`time::DEAF_CPU`) — and `virt_smp` passes in a whole-suite run at a host load average above +the dev host's core count. diff --git a/issues/kernel/desktop-window-child-freeze.md b/issues/kernel/desktop-window-child-freeze.md index 8f6f2ffcae3..40727892834 100644 --- a/issues/kernel/desktop-window-child-freeze.md +++ b/issues/kernel/desktop-window-child-freeze.md @@ -177,4 +177,4 @@ CPU-selection half of this family (`CpuHandle::answering`, orchestrator. **Its test is deleted**: `cd685b10a` and `b20d3fd40` took `desktop_window_child` -out, and `git revert b20d3fd40 cd685b10a` brings it back. +out. diff --git a/issues/kernel/every-wait-in-this-kernel-is-a-spin.md b/issues/kernel/every-wait-in-this-kernel-is-a-spin.md index 8997c30ca9e..9be6894a017 100644 --- a/issues/kernel/every-wait-in-this-kernel-is-a-spin.md +++ b/issues/kernel/every-wait-in-this-kernel-is-a-spin.md @@ -541,4 +541,4 @@ Six entries under `issues/design-debt/` recorded that the deleted document's own citations had rotted — five against the tree, one against a log plan deleted before it. All six closed with it. -`usb_boot_stick_pulled` is deleted; `issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md` records the commit that restores it. +`usb_boot_stick_pulled` is deleted. diff --git a/issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md b/issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md index ca2c4195d25..62618d13e81 100644 --- a/issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md +++ b/issues/kernel/io-uring-enter-trips-the-one-queue-invariant.md @@ -104,4 +104,4 @@ guest-side key generator rather than a host-side flood, and then accounting for whichever wait left the flag set. **`usb_boot_stick_pulled` is deleted**, as a red test is: `02366d741` took it -out, and `git revert 02366d741` brings it back. +out. diff --git a/issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md b/issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md index b2c3c206b1e..97806cade81 100644 --- a/issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md +++ b/issues/kernel/nothing-reaches-the-msi-arm-of-a-claimed-function.md @@ -7,11 +7,7 @@ opened: 2026-09-08 # Nothing reaches the MSI arm of a claimed function `pcidev::bring_up` arms a claimed function on MSI where it publishes no MSI-X, -and no test arms one. The *order* is guarded — `https_tls13_e1000e` -holds a claimed `8086:10d3`, which publishes both mechanisms, and refuses an -`msi address=` line for it — but the MSI arm itself is reached by nothing. `virtio_net_no_msix` calls -`PciDevice::enable_msi` from `bring_up` and reads false back; nothing reaches a -true, and so nothing reaches: +and no test arms one, so nothing reaches: - `PciDevice::disable_msi` from either hand-back site (`bring_up`'s `place_bars` failure and `tear_down`), or `Armed::Msi`'s teardown, which turns the @@ -27,11 +23,3 @@ Owned by the network track's stage-2 I219 worker. Exit condition: the first `userdev` interrupt counted against a claim on `00:1f.6` on the bench, which needs the 32-bit BAR window before it, plus netd exiting from that claim, which runs `tear_down`'s MSI arm. - -A guest arm is the alternative, and it needs no new boot config and no holder: -`kernel/src/drivers/pci.rs`'s `StagedCaps` stages a device shape on the function -an existing config already hands to a claim, and `pci_claim_caps_truncated` -reads a refusal off `tests/e1000case`. An MSI arm that *succeeds* is the same -hook with the list ending at its terminator rather than at a link the spec -forbids; what it costs from there is a registered name and its CI price, plus -whatever netd driving that card on MSI turns out to need. diff --git a/issues/kernel/short-sleep-livelock-stalls-on-ci-with-one-sleeper-never-returning.md b/issues/kernel/short-sleep-livelock-stalls-on-ci-with-one-sleeper-never-returning.md index 2ccec9b94f8..a7b31d20da1 100644 --- a/issues/kernel/short-sleep-livelock-stalls-on-ci-with-one-sleeper-never-returning.md +++ b/issues/kernel/short-sleep-livelock-stalls-on-ci-with-one-sleeper-never-returning.md @@ -30,7 +30,7 @@ instrument, and the owner is the sleep path in `kernel/src/sched` that the test's write-up (`tests/toyos.rs`, `short_sleep_livelock`) names. **Exit condition.** The fifth sleeper's stall is fixed in the sleep path, and -`short_sleep_livelock` green on CI's KVM `guest` shards. +`short_sleep_livelock` green. Owner: the sleep path, `kernel/src/sched`; held by the orchestrator. **Its test is deleted**: `1962baa5d` took `short_sleep_livelock` out, QEMU and diff --git a/issues/kernel/so-cache-refusals-saw-the-kernel-refuse-nothing-once.md b/issues/kernel/so-cache-refusals-saw-the-kernel-refuse-nothing-once.md index 16c73122407..f814b8b35ce 100644 --- a/issues/kernel/so-cache-refusals-saw-the-kernel-refuse-nothing-once.md +++ b/issues/kernel/so-cache-refusals-saw-the-kernel-refuse-nothing-once.md @@ -17,4 +17,4 @@ Owed: a mechanism. Nobody has one. **Exit condition.** The cause of the missing refusal is fixed, shown against `so_cache_refusals` and the `so-cache-tiny` budget it arms, as both stand at -`1808fb8d`, restored and green on CI's KVM `guest` shards. Owner: orchestrator. +`1808fb8d`, restored and green. Owner: orchestrator. diff --git a/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md b/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md new file mode 100644 index 00000000000..a5c68b063cd --- /dev/null +++ b/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md @@ -0,0 +1,31 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# `SYS_DEBUG` actions and two loader words that nothing calls + +The guest suite's first cut +(`issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`) deleted the only +callers of nine `SYS_DEBUG` actions and the loader's reading of two boot words, and left their +names in `toyos-abi/src`, because a deletion there is an ABI change and that pull request was not +one. Each kernel arm still answers its action. `git grep -w -- tests userland toyos src` +finds no caller of: + +- `debug_action::PANIC` (0), `NULL_READ` (1), `HEAP_OVER_CEILING` (6) and `IDLE_GUARD_READ` + (9), which `test_panic_child` took by number for stage B's `syscall_panic_halts`, + `syscall_fault_halts` and `heap_over_ceiling_halts` and stage C's `idle_stack_guard`; +- `HEAP_AT_CEILING` (5), `HEAP_AT_CEILING_PAGE_ALIGNED` (7) and `LOWER_SYSINFO_BOUND` (19), + `heap_ceiling`'s, for stage C's `heap_ceiling_bounds`; +- `LOCK_ACROSS_SWITCH` (2), `test_panic_child`'s, and `LOG_PATTERNED` (21), test-runner's + `log-gate` and `log-storm`. Their stage C items are a type (`lock_across_switch_halts`) and + a host test (`log_conservation_smp2`), and neither calls `SYS_DEBUG`, so no stage brings a + caller back; +- `boot::WRITE_NO_LAYOUT_PARAM` and `boot::WITHHOLD_ROOT_PARAM`, for stage A's + `kernel_args_layout_refused` and `root_withheld_refused`. + +Owner: the orchestrator. + +**Exit**: for each name, `git grep -w` finds a caller again, its stage's row, or an ABI change +retires the name with its kernel arm and never reuses its number. diff --git a/issues/kernel/syscall-window-nmi-shortfalls-on-a-contended-host.md b/issues/kernel/syscall-window-nmi-shortfalls-on-a-contended-host.md index 868ea579531..6de9aae15cd 100644 --- a/issues/kernel/syscall-window-nmi-shortfalls-on-a-contended-host.md +++ b/issues/kernel/syscall-window-nmi-shortfalls-on-a-contended-host.md @@ -48,6 +48,4 @@ two read. `syscall-window-nmi` actuator, `nmi_gate`'s aiming and counting, the syscall count on every dispatch, and the Ring 3 spinner. The nested arm stayed, as `nested_nmi_is_loud`, with one NMI sent from the idle loop, and `866532c62` -deleted the doc it had carried over. `git revert 866532c62 2a4893921 539977050 -4600f6754` brings all three back, with that doc and the two other lines -`866532c62` deleted. +deleted the doc it had carried over. diff --git a/kernel-loom/tests/i8042_tally.rs b/kernel-loom/tests/i8042_tally.rs index f99a1dde458..4e307be243c 100644 --- a/kernel-loom/tests/i8042_tally.rs +++ b/kernel-loom/tests/i8042_tally.rs @@ -6,15 +6,9 @@ //! port-drain burst, subtracted by a reader on another CPU, and a reader landing //! between the two writes blamed an interrupt that had carried nothing. //! -//! **That torn read is not what produced the rate the write-up records.** -//! `i8042_undecoded_bytes` at about one full suite in three under load came from -//! the same handler counting on the way *in*, ahead of any byte reaching the -//! ring, which needs no subtraction at all — no reader could have been inside -//! the bring-up ISR's window: the reporting CPU was an AP, and `i8042::init` -//! runs on the BSP before `smp::boot_aps`. -//! One word closes both. The distinction is written here because blaming a -//! proved race for an observed line, without checking that a reader could have -//! been there, is the mistake this file exists downstream of. +//! The same handler counting on the way *in*, ahead of any byte reaching the +//! ring, needs no subtraction at all to blame an interrupt that carried +//! nothing. One word closes both. //! //! **A rate is why this is a model and not a test.** Either window is a handful //! of instructions on one CPU; no guest boot can be made to land in one on diff --git a/kernel/CLAUDE.md b/kernel/CLAUDE.md index eb0254cff8a..50669ba5826 100644 --- a/kernel/CLAUDE.md +++ b/kernel/CLAUDE.md @@ -19,6 +19,6 @@ The module header at the site owns its subsystem — read it before changing a m - **A console is per holder, minted at spawn** — the object *is* the line buffer. - **`ops::close` cancels a poll only for a source its object really ends** — `Watch::cancel_polls` answers every ring's poll on that watch; `ops::close_ends_polls` is where a new object kind answers. - **A page shared with userland is never reached through a Rust reference** — the words a protocol shares are `&AtomicU32` one at a time, everything else is a volatile copy of the whole value, and a page is laid out *before* it is mapped (`SharedMemObject::phys_before_mapping`). The kernel, `toyos-abi` and the SDK each hold one end of this rule. -- **A device a process drives reaches only the memory its grants map** — the domain is the gate, not the descriptor; a device address outside a grant is a `DMA FAULT` record and never a crash, and `userdev_dma_fault` is the registration. +- **A device a process drives reaches only the memory its grants map** — the domain is the gate, not the descriptor; a device address outside a grant is a `DMA FAULT` record and never a crash. - **A bounds-checked accessor whose refusal panics inline is not inlined** — put the refusal in a `#[cold] #[inline(never)]` helper; and measure the emitted assembly before choosing a runtime check over a zero-sized witness. - **Pressing Ctrl+Alt+D destroys the evidence it reports on** — capture `info registers -a` over QMP *first*; `kernel/src/sched/dump.rs` explains the report. diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index 5beca3f086b..12a6c3c57d1 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -108,7 +108,7 @@ boot-actuators = [] # than assumed, because that claim is what makes the split below sound. # # **Separate from `boot-actuators` on purpose.** A diagnostic image the owner -# flashes wants `heartbeat` and `diag-tick`; it must not also gain a debug +# flashes wants `diag-tick`; it must not also gain a debug # syscall, which is what folding the two into one name would give it. test-actuators = [] # The debugger's spin gate, and the one thing `--debug` compiles in. Not a boot @@ -310,8 +310,7 @@ df-witness-mutate = ["df-witness"] # carrying this inherits a set direction flag from whatever it interrupted # exactly as every kernel before 2026-08-21 did. # -# It replaces the *behaviour* and not a verdict, which is `fpu-save-nothing`'s -# argument one file over: everything else about the entry — the pushes, the FP +# It replaces the *behaviour* and not a verdict: everything else about the entry — the pushes, the FP # bracket, the preempt count, the exit-to-user check — is the shipped one, and # what is absent is one instruction. A control that flipped an assertion would # make its own gate vacuous. @@ -338,37 +337,6 @@ entry-df-unclean = [] # own cost into the arm it is being compared against. pass-spin = [] heap-lockspin = ["pass-spin"] -# Take the FP body out of `arch::entry`'s bracket and the declared state out of -# the loader's trampolines, leaving a kernel that preserves the *rest* of the -# user machine state and nothing else — which is the kernel this tree had before -# every Ring 3 transition that can reach another task saved and restored the -# full x87/SSE state. -# -# The negative control for `fpu_isolation`, and the only way to have one: the -# defect it stages is a CPU register file surviving a task switch, and QEMU has -# no device, machine property or `-cpu` flag that changes what an entry saves. -# -# It replaces the *behaviour* and not a verdict. The reservation, the alignment, -# the rsp bookkeeping and every assertion are the shipped ones; what is absent -# is the `fxsave64`, the `fxrstor64` and the trampoline's load. A feature that -# flipped an assertion would make its own gate vacuous. -# -# **The one actuator that kept its own kernel build, and here is why**: these -# are `naked_asm!` bodies on every ring transition, so a -# boot parameter would have to be a branch there — on the path the gate is about, -# in a binary the shipping one no longer resembles. The gate would then certify -# a bracket nothing ships. -fpu-save-nothing = [] -# The kernel this tree had before `arch::control_regs` took `FSGSBASE` out of -# `CR4`: the bit back in `CR4_REQUIRED`, so `WRGSBASE`/`RDGSBASE` are legal at -# CPL 3 again and a Ring 3 thread can aim the `GS.base` every kernel entry -# dereferences. The negative control for `gsbase_locked`, and its own kernel -# build for `fpu-save-nothing`'s reason — the defect is one bit of the `CR4` -# declaration every CPU is asserted against, which a boot parameter cannot reach -# without becoming a branch on the path the doctrine forbids. It replaces the -# machine and not a verdict: the MSR-based FS base, the entry paths and -# `self_check` are the shipped kernel's, and only that one bit differs. -user-writable-gsbase = [] # The one profile every guest binary is built with. # Optimised, because an unoptimised guest mismeasures everything under TCG; diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index f17b9760f3f..ea0305d1cd3 100644 --- a/kernel/src/actuator.rs +++ b/kernel/src/actuator.rs @@ -39,15 +39,6 @@ actuators! { /// control on `crate::deadline`: nothing else in this kernel ends it. wedge_before_reset = "wedge-before-reset"; - /// The loader hands the kernel no ROOT image, which `rootfs::mount` has to - /// refuse by name; read by the loader as [`toyos_abi::boot::WITHHOLD_ROOT_PARAM`]. - loader_withholds_root = "loader-withholds-root"; - - /// The loader writes 0 as the `KernelArgs` layout word, which `kernel_main` - /// has to refuse by name; read by the loader as - /// [`toyos_abi::boot::WRITE_NO_LAYOUT_PARAM`]. - loader_writes_no_layout = "loader-writes-no-layout"; - /// Panic between arming the on-screen console and `mm::init`. test_early_panic = "test-early-panic"; @@ -57,146 +48,31 @@ actuators! { /// x86-64 loads its IDT later, and panics by name instead. test_early_fault = "test-early-fault"; - /// Panic inside `percpu::init_bsp`, one statement after it loads the IDT: - /// the earliest point a panic is reportable at all, and the window the T14 - /// stops in. What it judges is that the reset register is decoded by then. - test_panic_after_idt = "test-panic-after-idt"; - - /// Halt between a record's commit and its repaint, so the panel holds the - /// record before it and `screen_early_panel` reads a paint it can attribute. - test_early_halt = "test-early-halt"; - /// Have the first syscall null SS, force a switch, and report whether it reloaded — the /// AMD `SYSRET` SS-attributes workaround's only guest-observable proof. sysret_ss_probe = "sysret-ss-probe"; - /// Log every i8042 drain: bytes seen, events queued, whether the queue woke. - i8042_trace = "i8042-trace"; - - /// Hold a scheduler pass between reading `irq_ring` and the byte ring so an interrupt lands between them. - i8042_edge_race = "i8042-edge-race"; - - /// Make the ISR's output-buffer check trip its 16-byte bound and run the quarantine path. - i8042_fault = "i8042-fault"; - - /// Zero the i8042 probe's init budget so its expiry paths run. - i8042_budget_expired = "i8042-budget-expired"; - - /// Hand the i8042 probe a FADT denying a controller that is present. - i8042_fadt_denial = "i8042-fadt-denial"; - - /// Answer the i8042 probe's scancode-set query with ECHO's own `0xEE`. - i8042_kbd_echo = "i8042-kbd-echo"; - - /// Shorten the i8042 post-verdict counter report from 10s to 500ms. - i8042_fast_health = "i8042-fast-health"; - /// Script the input core directly at end of boot. test_input_merge = "test-input-merge"; - /// Clamp the xHCI driver to one device block. - xhci_one_slot = "xhci-one-slot"; - /// Run the xHCI extended-capability walk over eight malformed lists at init. xhci_xecp_selftest = "xhci-xecp-selftest"; - /// Read and write every USB disk carrying the gate's stamp in block 0; the stamp, not the parameter, picks the disk, since the boot stick shares the bus. - usb_storage_gate = "usb-storage-gate"; - - /// Hold the thread named `toyos_quiesce::LAST_THREAD` inside `SYS_NANOSLEEP`, and the shutdown until it is held there, until the stop waits on it alone: its park is then the stop's last transition. - quiesce_last_park = "quiesce-last-park"; - /// Establish three nested `scheduler::Operation`s and report what each observed and restored; it stages nothing, touching no device. sched_operation_nesting = "sched-operation-nesting"; - /// Answer SYNCHRONIZE CACHE with ILLEGAL REQUEST / INVALID COMMAND OPERATION CODE. - usb_flush_unimplemented = "usb-flush-unimplemented"; - - /// The same, with a HARDWARE ERROR in place of ILLEGAL REQUEST. - usb_flush_fails = "usb-flush-fails"; - /// Abandon the boot's first WRITE(10) data phase without waiting for it. usb_transport_break = "usb-transport-break"; - /// Hold the port rung's first reset, once, until the port reads empty. See - /// `xhci::msc::reset_moves`; judged by `usb_stick_left`. - usb_reset_moves = "usb-reset-moves"; - - /// The same hold, once the reset's completion has been read with the - /// device on the port: a device that leaves under a USB2 port's reset. - /// See `xhci::msc::reset_moves`; judged by `usb_stick_left`. - usb_reset_moves_after = "usb-reset-moves-after"; - - /// The same hold, once the port rung has configured the device again and - /// before its TEST UNIT READY: a device that leaves after every step of - /// the rung was answered. See `xhci::msc::reset_moves`; judged by - /// `usb_stick_left`. - usb_reset_moves_configured = "usb-reset-moves-configured"; - - /// Refuse the machine's first stop, `SYS_SHUTDOWN` or `SYS_REBOOT`, before - /// anything is torn down, as a machine with no way to stop refuses it: the - /// path on which init tells `logd` the machine runs on. Judged by - /// `log_after_a_refused_stop`. - power_refused_once = "power-refused-once"; - - /// Hold a thread spawn whose argument carries `loader::rebase_window`'s - /// mark between its TLS block being given an address and the block's - /// pointers being rebased to it: where the process can already reach the - /// block, until a sibling has stored into its DTV; where it cannot, it - /// says so. Judged by `tls_rebase_window`. - tls_rebase_window = "tls-rebase-window"; - - /// Have the first disk the boot scan binds answer nothing for longer than - /// the scan's whole silence bound and then be refused, as T14 run 103's - /// stick was: the refusal's Disable Slot is submitted into the scan's one - /// operation slot after the scan has stopped listening, and the next port - /// to connect enumerates on top of it. See - /// `xhci::msc::bind_spends_the_scan`; judged by - /// `xhci_scan_hands_over_a_free_slot`. - usb_bind_spends_the_scan = "usb-bind-spends-the-scan"; - - /// Stop every CPU inside one WRITE(10) at the shutdown syscall, with the - /// device holding the CBW and nothing queued for its data phase, so the - /// bound that ends the machine ends a device inside a Bulk-Only command. - /// See `usb_gate::wedge_inside_a_write`; judged by - /// `usb_reset_records_the_phase_it_cut`. - usb_wedge_data_owed = "usb-wedge-data-owed"; - - /// The same, stopped one step later: the data phase's TRB is on the ring - /// and its doorbell has not been rung. - usb_wedge_in_data = "usb-wedge-in-data"; - - /// The same, stopped after the data phase completed and before anything has - /// asked for the CSW. - usb_wedge_before_status = "usb-wedge-before-status"; - /// Sweep the boot stick from the shutdown syscall so the reset lands on a /// controller that is moving bytes rather than on a bus idle since the /// wedge. See `usb_gate::sweep_under_load`; judged by /// `usb_reset_records_the_phase_it_cut`. usb_reset_under_load = "usb-reset-under-load"; - /// Report the preempt depth and backtrace at the deepest point of a disk transfer; it stages nothing, only measures. - io_depth_probe = "io-depth-probe"; - - /// Raise an unheld claim slot's vector inside a post of its own watch, - /// inside a completion into a ring polling it, and inside that ring's own - /// watch, while the CPU holds preemption off, and count whether the - /// handler posted it there. - handler_post = "handler-post"; - - /// Starve the four xHCI bring-up register waits in `init_one`. - xhci_deaf_controller = "xhci-deaf-controller"; - - /// Starve the port-reset wait in `init_device`. - xhci_deaf_port = "xhci-deaf-port"; - /// Make one CPU ignore a kick. dump_deaf_cpu = "dump-deaf-cpu"; - /// On one CPU, file Ctrl+Alt+D's request inside each kind of pass that may not serve it and inside a report, and count the Ring 3 returns each is left pending across. - dump_in_blocking_pass = "dump-in-blocking-pass"; - /// Wedge one CPU with interrupts off, spinning on a lock another CPU holds /// and never gives back: the negative control on `crate::hardlockup`, and a /// machine nothing else in this tree ends. Where CPUID states no @@ -208,36 +84,12 @@ actuators! { /// Send one NMI from the idle loop, and return from its handler via `iretq` with a second NMI already pending. nmi_nested = "nmi-nested"; - /// Report an empty root hub for the xHCI driver's `SLOW_CONNECT_NS` after a controller powers its ports. - xhci_slow_connect = "xhci-slow-connect"; - - /// Report the first root-hub port empty until the boot scan has run, the rest normal — distinct from hiding the whole bus, since settle waits only for a non-empty settled set. - xhci_slow_storage_connect = "xhci-slow-storage-connect"; - - /// Give PORTSC's PED bit the RW1CS meaning xHCI 1.2 §5.4.8 gives it. - xhci_portsc_rw1c = "xhci-portsc-rw1c"; - /// Run `parse_config` over nine crafted configuration descriptors at init. xhci_descriptor_selftest = "xhci-descriptor-selftest"; - /// Run `Virtqueue::poll_used` over eleven crafted used-ring elements at init. - virtio_used_selftest = "virtio-used-selftest"; - /// Walk the PCI capability list, window check and parse over crafted config-space layouts at init. pci_cap_selftest = "pci-cap-selftest"; - /// End the capability list of the function a claim is bringing up at a link the spec forbids, one link past its MSI capability: a claimed function publishing an MSI-X table no walk may reach. - pcidev_caps_truncated = "pcidev-caps-truncated"; - - /// Put back none of a reset function's BARs but its MSI-X table's, so a function the reset returned to its defaults comes to its next claim no longer decoding the window it was cut: what a reset nobody restored looks like. - pcidev_bar_lost_on_reset = "pcidev-bar-lost-on-reset"; - - /// Put a function a level reset returned to its defaults back with BAR 0 one BAR's size above the window it was cut, inside that window: a register holding a decodable address that is not the cut. - pcidev_bar_moved_on_reset = "pcidev-bar-moved-on-reset"; - - /// Release every claimed function as though it advertised no reset at all, the way the T14's I219 is released: what it had taken in is still aimed at its last holder's grants when the next claim starts it mastering. - pcidev_reset_nothing = "pcidev-reset-nothing"; - /// Raise the local APIC's spurious vector on this CPU once. lapic_spurious_selftest = "lapic-spurious-selftest"; @@ -252,12 +104,6 @@ actuators! { /// a quantum, and take its interrupts with them open. timer_floor = "timer-floor"; - /// Deliver the i8042 vector once at arming with no byte behind it — the arming edge, staged. - i8042_arm_edge = "i8042-arm-edge"; - - /// Blind init's read of the reset handshake, staging virtio devices that never answer; the console — the staged boot's capture channel — is spared. - virtio_reset_stuck = "virtio-reset-stuck"; - /// Withhold `VIRTIO_F_ACCESS_PLATFORM` from every virtio device but the console, staging a function whose addresses the unit never translates. virtio_no_access_platform = "virtio-no-access-platform"; @@ -274,24 +120,6 @@ actuators! { /// machine brought up, with nothing else running, and report the distribution. tlb_shootdown_bench = "tlb-shootdown-bench"; - /// Shrink each process's VA arena from ~1015GB to 256MiB. - test_tiny_va = "test-tiny-va"; - - /// Build a hash container before `hasher::seed()`, so the refusal that stops a seedless container from being silent is executed. - test_hash_before_seed = "test-hash-before-seed"; - - /// Hold the shutdown open for a tenth of a second after the boot's last - /// word, yielding: the window hardware has between `Rebooting.` and the - /// reset and QEMU does not. A boot that writes a record into it is one the - /// stop did not stop. - quiesce_late_word = "quiesce-late-word"; - - /// Make the shutdown's bounded acquisitions of the xHCI controller lock - /// find it busy for their whole bound — the negative control on "no - /// shutdown path may fail to reset". A boot armed with it must still hand - /// the machine back, with its account saying the barrier was refused. - xhci_lock_wedged = "xhci-lock-wedged"; - /// Panic once boot phases are done, with no thread current. test_late_panic = "test-late-panic"; @@ -301,74 +129,12 @@ actuators! { /// report it and end the chain. blackbox_foreign_identity = "blackbox-foreign-identity"; - /// Take a Ring 0 `#UD` once boot phases are done, with no thread current. - test_kernel_fault = "test-kernel-fault"; - - /// Panic inside the crash report before it has said anything; armed alone, a boot that reports no crash never reaches it. - panic_in_report = "panic-in-report"; - - /// Take a `#PF` inside the crash report before it has said anything; armed alone, a boot that reports no crash never reaches it. - fault_in_report = "fault-in-report"; - /// Panic a few seconds after a compositor claims the framebuffer, from an idle CPU. metal_panic_probe = "metal-panic-probe"; /// Cap how long an idle CPU may sleep so the idle loop keeps running. diag_tick = "diag-tick"; - /// Log the monotonic time and which CPUs are alive every 250ms. - heartbeat = "heartbeat"; - - /// Let a handle close cancel every poll on the keyboard's watch in the machine. - keyboard_close_cancels_every_console = "keyboard-close-cancels-every-console"; - - /// Read address zero inside `klogd` on its first instruction. - klogd_fault = "klogd-fault"; - - /// Stop the boot dead in phase 3, interrupts off, before any log drain. - pre_idle_wedge = "pre-idle-wedge"; - - /// Leave the xHCI controller out of the IOMMU's root table. - iommu_context_absent = "iommu-context-absent"; - - /// Give it a present context entry naming an empty second-level table, distinct from an absent context: passthrough would fault identically to the row above. - iommu_empty_domain = "iommu-empty-domain"; - - /// Answer a claimed network function's first DMA grant with an address in another driver's pool, which its own domain does not map. - iommu_userdev_foreign_dma = "iommu-userdev-foreign-dma"; - - /// Point a scanout backing at that same page, which the display's own domain does not map. - iommu_gpu_foreign_backing = "iommu-gpu-foreign-backing"; - - /// Point the HDA stream's buffer descriptor list at that page and start the stream, so the controller fetches descriptors from memory it does not own. - iommu_hda_foreign_bdl = "iommu-hda-foreign-bdl"; - - /// Point a virtio-sound control answer at that page and submit the chain, so the device writes where it may not. - iommu_sound_foreign_dma = "iommu-sound-foreign-dma"; - - /// Cap every device domain at `vtd::table::NARROW_BYTES` of addresses, so a - /// holder that spends addresses runs a domain dry in a short loop. - iommu_domain_narrow = "iommu-domain-narrow"; - - /// Point every device MSI at APIC 1 rather than 0 — the only way to tell the - /// two remapping-entry destination encodings apart, since 0 encodes alike in both. - iommu_dest_apic1 = "iommu-dest-apic1"; - - /// Run the HDA register allow-list over every arm of it at bind time. - hda_allowlist_selftest = "hda-allowlist-selftest"; - - /// Make the wall clock's update flag never clear. - rtc_dead = "rtc-dead"; - - /// Make the RTC registers never settle: no two of four reads agree. - rtc_unstable = "rtc-unstable"; - - /// Make firmware name no century register. - rtc_no_century = "rtc-no-century"; - - /// Make the century register read `0x21`. - rtc_century_next = "rtc-century-next"; - /// Run the leak-rollback controls (device mint) after mount. leak_rollback_selftest = "leak-rollback-selftest"; @@ -378,18 +144,6 @@ actuators! { /// Reopen init by pid once it is spawned, and open every kernel thread's pid, the way `SYS_PROCESS_OPEN` does. process_reopen_selftest = "process-reopen-selftest"; - /// Offer the block layer a second device claiming a registered `DeviceId`, and report what it did with it. - block_duplicate_id = "block-duplicate-id"; - - /// Arm the watchdog at seconds rather than minutes, so a guest reaches the reset. - watchdog_fast = "tco-fast"; - - /// Stop feeding it once boot is done, which is what a wedge looks like to the chipset. - watchdog_starve = "tco-starve"; - - /// Shorten the panicked kernel's own reboot bound from a minute to seconds, so a guest reaches the reset. - panic_reboot_fast = "panic-reboot-fast"; - /// Have Ctrl+Alt+D's report painter go fatal holding the panel's latch: a /// fatal path meeting a painter that will never let go. panel_painter_stalls = "panel-painter-stalls"; @@ -397,9 +151,7 @@ actuators! { #[cfg(feature = "boot-actuators")] const IMPLIES: &[(&str, &[&str])] = &[ - ("i8042-trace", &["i8042-fast-health", "i8042-edge-race"]), ("metal-panic-probe", &["diag-tick"]), - ("heartbeat", &["diag-tick"]), // The staged CPU has to still be deaf when its bound passes, and this boot // would otherwise have handed the machine back at the end of its job list — // so the control that ends a machine no other bound ends is staged over the @@ -532,10 +284,3 @@ const _: () = { i += 1; } }; - -// The loader reads this actuator's word out of the ABI and the table above -// spells it as a literal; the two are one name or the build fails. -#[cfg(feature = "boot-actuators")] -const _: () = assert!(str_eq("loader-withholds-root", toyos_abi::boot::WITHHOLD_ROOT_PARAM)); -#[cfg(feature = "boot-actuators")] -const _: () = assert!(str_eq("loader-writes-no-layout", toyos_abi::boot::WRITE_NO_LAYOUT_PARAM)); diff --git a/kernel/src/arch/aarch64/hw.rs b/kernel/src/arch/aarch64/hw.rs index cb74189d885..eefe2fe72f0 100644 --- a/kernel/src/arch/aarch64/hw.rs +++ b/kernel/src/arch/aarch64/hw.rs @@ -76,8 +76,6 @@ impl Hw for KernelHw { percpu::set_current_pid(incoming.id.map(|id| id.0)); match incoming.id { Some(_) => { - #[cfg(feature = "boot-actuators")] - crate::heartbeat::note_dispatch(); percpu::set_kernel_stack(incoming.kernel_stack_top); incoming.root.activate(); cpu::write_thread_pointer(incoming.thread_pointer); diff --git a/kernel/src/arch/aarch64/trap.rs b/kernel/src/arch/aarch64/trap.rs index 70e41aaef30..397a2137f80 100644 --- a/kernel/src/arch/aarch64/trap.rs +++ b/kernel/src/arch/aarch64/trap.rs @@ -272,9 +272,6 @@ fn user_fatal(frame: &Frame) -> ! { // First: a panic anywhere below reaches the panic handler as DOUBLE // PANIC, which can only report what was captured here. crate::panic::record_fault(class_name(frame.esr), frame.elr, frame.far, frame.esr); - if crate::actuator::panic_in_report() { - panic!("panic-in-report: the crash report panicked before it said anything"); - } let tid = percpu::current_tid().map_or(u32::MAX, |t| t.raw()); alert!( "FAULT pc={:#018x} far={:#018x} esr={:#010x} sp={:#018x} tid={tid}{}", diff --git a/kernel/src/arch/x86_64/control_regs.rs b/kernel/src/arch/x86_64/control_regs.rs index 3ad0eb2ff98..d1c42c00d0d 100644 --- a/kernel/src/arch/x86_64/control_regs.rs +++ b/kernel/src/arch/x86_64/control_regs.rs @@ -54,7 +54,7 @@ pub const CR0: u64 = cr0::PE | cr0::MP | cr0::ET | cr0::NE | cr0::WP | cr0::PG; /// `CR4` bits every CPU must have. `DE` is zero legacy, not need — this kernel /// touches no debug register. `FSGSBASE` is [`CR4_FORBIDDEN`], not here. const CR4_REQUIRED: u64 = - cr4::DE | cr4::PAE | cr4::MCE | cr4::OSFXSR | cr4::OSXMMEXCPT | FSGSBASE_RESTORED; + cr4::DE | cr4::PAE | cr4::MCE | cr4::OSFXSR | cr4::OSXMMEXCPT; /// `CR4` bits this kernel takes when the CPU offers them (checked against CPUID first: an undefined bit is `#GP`). const CR4_OPTIONAL: u64 = cr4::SMEP | cr4::SMAP | cr4::PCIDE | cr4::UMIP; @@ -65,14 +65,8 @@ const CR4_OPTIONAL: u64 = cr4::SMEP | cr4::SMAP | cr4::PCIDE | cr4::UMIP; /// The kernel's FS base uses `IA32_FS_BASE` (`cpu::write_fs_base`) instead. const CR4_FORBIDDEN: u64 = cr4::FSGSBASE; -/// Zero but in the `user-writable-gsbase` control, which restores it. -#[cfg(feature = "user-writable-gsbase")] -const FSGSBASE_RESTORED: u64 = cr4::FSGSBASE; -#[cfg(not(feature = "user-writable-gsbase"))] -const FSGSBASE_RESTORED: u64 = 0; - -// Forbidden in the declaration except where restored, and never optional. -const _: () = assert!(CR4_REQUIRED & CR4_FORBIDDEN == FSGSBASE_RESTORED); +// Forbidden in the declaration, and never optional. +const _: () = assert!(CR4_REQUIRED & CR4_FORBIDDEN == 0); const _: () = assert!(CR4_OPTIONAL & CR4_FORBIDDEN == 0); /// `IA32_EFER` on every CPU: `SCE`, `LME`, `NXE`. `SCE` is declared only diff --git a/kernel/src/arch/x86_64/cpu.rs b/kernel/src/arch/x86_64/cpu.rs index 2353b0f5868..1c489efeffc 100644 --- a/kernel/src/arch/x86_64/cpu.rs +++ b/kernel/src/arch/x86_64/cpu.rs @@ -372,12 +372,6 @@ pub fn frame_pointer() -> u64 { rbp } -/// Raise the architecture's undefined-instruction exception here: `ud2`, whose `#UD` the IDT catches as the kernel's own fault. -pub fn undefined_instruction() { - // SAFETY: ud2 reads and writes nothing and raises #UD, caught by the installed IDT. - unsafe { asm!("ud2", options(nomem, nostack)) }; -} - /// The TSC's frequency in hertz as CPUID *states* it, for the one caller that /// may run before the clock is calibrated — the panic path, which has to bound a wait on a /// machine that never reached the HPET. Nothing calibrates against it and no diff --git a/kernel/src/arch/x86_64/entry.rs b/kernel/src/arch/x86_64/entry.rs index e2187ed1fc4..0065cc5563d 100644 --- a/kernel/src/arch/x86_64/entry.rs +++ b/kernel/src/arch/x86_64/entry.rs @@ -77,7 +77,6 @@ macro_rules! ring3_naked_asm { /// `naked_asm!` for a trampoline into Ring 3 for the first time: supplies /// the declared state's address from `fpu::INITIAL_IMAGE`. -#[cfg(not(feature = "fpu-save-nothing"))] macro_rules! ring3_trampoline_asm { ($($body:tt)*) => { core::arch::naked_asm!( @@ -88,7 +87,6 @@ macro_rules! ring3_trampoline_asm { } /// The state a task that has never been in Ring 3 starts from. -#[cfg(not(feature = "fpu-save-nothing"))] macro_rules! initial_user_state { () => { "fxrstor64 [rip + {fp_initial}]\n" @@ -97,7 +95,6 @@ macro_rules! initial_user_state { /// Park the user machine state on this kernel stack, leaving `rsp` aligned /// for the System V `call` that follows. -#[cfg(not(feature = "fpu-save-nothing"))] macro_rules! save_user_state { () => { concat!( @@ -114,7 +111,6 @@ macro_rules! save_user_state { } /// Put it back, and `rsp` with it. -#[cfg(not(feature = "fpu-save-nothing"))] macro_rules! restore_user_state { () => { concat!( @@ -124,41 +120,6 @@ macro_rules! restore_user_state { }; } -// Negative control (`fpu-save-nothing`): same reservation, alignment and -// `rsp` bookkeeping as above, without moving the state. - -#[cfg(feature = "fpu-save-nothing")] -macro_rules! ring3_trampoline_asm { - ($($body:tt)*) => { core::arch::naked_asm!($($body)*) }; -} - -#[cfg(feature = "fpu-save-nothing")] -macro_rules! initial_user_state { - () => { - "" - }; -} - -#[cfg(feature = "fpu-save-nothing")] -macro_rules! save_user_state { - () => { - concat!( - "mov r11, rsp\n", - "sub rsp, {fp_bytes}\n", - "sub rsp, {fp_align}\n", - "and rsp, -{fp_align}\n", - "mov [rsp + {fp_bytes}], r11\n", - ) - }; -} - -#[cfg(feature = "fpu-save-nothing")] -macro_rules! restore_user_state { - () => { - "mov rsp, [rsp + {fp_bytes}]\n" - }; -} - pub(crate) use {restore_user_state, ring3_naked_asm, save_user_state}; /// Entry point for new processes, reached through `context_switch`'s `ret`. r12 = entry point, r13 = user stack pointer. diff --git a/kernel/src/arch/x86_64/hw.rs b/kernel/src/arch/x86_64/hw.rs index 7f224e72f18..2ee6f21def3 100644 --- a/kernel/src/arch/x86_64/hw.rs +++ b/kernel/src/arch/x86_64/hw.rs @@ -297,11 +297,6 @@ impl Hw for KernelHw { percpu::set_current_pid(incoming.id.map(|id| id.0)); match incoming.id { Some(_) => { - // Here, not in the pass: this is the one place a task (not idle) becomes what a - // CPU runs, which `note_dispatch` below must count for `heartbeat`'s `ran=` to - // be meaningful. - #[cfg(feature = "boot-actuators")] - crate::heartbeat::note_dispatch(); percpu::set_kernel_stack(incoming.kernel_stack_top); incoming.root.activate(); cpu::write_fs_base(incoming.thread_pointer); diff --git a/kernel/src/arch/x86_64/i8042/mod.rs b/kernel/src/arch/x86_64/i8042/mod.rs index 9127d077da8..16b3dcbfe59 100644 --- a/kernel/src/arch/x86_64/i8042/mod.rs +++ b/kernel/src/arch/x86_64/i8042/mod.rs @@ -143,11 +143,7 @@ fn health_period_ns() -> u64 { Duration::from_secs(10), "the PMM dump's own cadence, and one line per 10s of typing", ); - if crate::actuator::i8042_fast_health() { - Duration::from_millis(500).nanos() - } else { - HEALTH.nanos() - } + HEALTH.nanos() } static NEXT_REPORT_NS: AtomicU64 = AtomicU64::new(u64::MAX); static REPORTED_IRQS: AtomicU32 = AtomicU32::new(0); @@ -355,41 +351,6 @@ fn report_counters() { ); } -/// One status-register snapshot, for a machine with nothing else to explain -/// a quiet pin. Side-effect-free (0x64, and an RTE read under the topology's -/// own lock), so it need not run on `IRQ_CPU` and cannot race the ISR. -#[cfg(feature = "boot-actuators")] -pub fn report_line() { - if !ACTIVE.load(Ordering::Relaxed) { - return; - } - log!( - "i8042: line status={:#04x} irqs={} bytes={} kbd {} aux {}", - inb(STATUS), - TALLY.read().irqs(), - RX_BYTES.load(Ordering::Relaxed), - Rte(KEYBOARD_GSI.load(Ordering::Relaxed)), - Rte(AUX_GSI.load(Ordering::Relaxed)), - ); -} - -/// `gsi=1 rte=0x0000000000000024`, or why there is no entry to print. -#[cfg(feature = "boot-actuators")] -struct Rte(u32); - -#[cfg(feature = "boot-actuators")] -impl core::fmt::Display for Rte { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - if self.0 == u32::MAX { - return write!(f, "unrouted"); - } - match ioapic::redirection(Gsi(self.0)) { - Some(entry) => write!(f, "gsi={} rte={:#018x}", self.0, entry), - None => write!(f, "gsi={} rte=busy", self.0), - } - } -} - /// Put the verdict on the panel too, but only when there is no other /// channel: declines once `serial::has_console()`, mirroring /// `panic_flush`'s own test. @@ -460,16 +421,8 @@ fn has_bytes() -> bool { HEAD.load(Ordering::Acquire) != TAIL.load(Ordering::Relaxed) } -/// Under `i8042-fault`, armed after init so the next interrupt looks -/// permanently full — the only way to reach the ISR's bound without a -/// genuinely broken controller. -static FAULT: AtomicBool = AtomicBool::new(false); - #[inline] fn buffer_full(status: u8) -> bool { - if crate::actuator::i8042_fault() && FAULT.load(Ordering::Relaxed) { - return true; - } status & OBF != 0 } @@ -583,7 +536,6 @@ pub fn service() { if AUX_RESET_PENDING.load(Ordering::Relaxed) && is_irq_cpu() { aux_reenable(); } - widen_edge_window(); if has_bytes() { // Asked again with bytes in hand: a record read absent may belong // to an interrupt that arrived just after that read. @@ -598,17 +550,6 @@ pub fn service() { report_counters(); } -/// Under `i8042-edge-race`, widens the window between reading the record and -/// reading the ring so an interrupt can land inside it. -fn widen_edge_window() { - if !crate::actuator::i8042_edge_race() { - return; - } - for _ in 0..200 { - core::hint::spin_loop(); - } -} - /// Decode what the ISR left in the ring and wake whoever it belongs to. /// `recorded` is whether this pass found an `irq_ring` record for the source. fn service_bytes(recorded: bool) { @@ -621,7 +562,7 @@ fn service_bytes(recorded: bool) { } } - let Drained { bytes, keys, motion, aux_reset } = drain(); + let Drained { keys, motion, aux_reset } = drain(); // Wake only when the decode queued something, or a stray wake parks the // next reader until the following real event. @@ -633,7 +574,6 @@ fn service_bytes(recorded: bool) { if woke_ms { crate::mouse::WATCH.post(); } - trace_drain(bytes, keys, motion, woke_kb, woke_ms); if aux_reset { AUX_RESET_PENDING.store(true, Ordering::Relaxed); @@ -641,7 +581,6 @@ fn service_bytes(recorded: bool) { } struct Drained { - bytes: usize, keys: usize, motion: usize, aux_reset: bool, @@ -652,7 +591,7 @@ struct Drained { /// the reverse. fn drain() -> Drained { let mut state = PS2.lock(); - let mut out = Drained { bytes: 0, keys: 0, motion: 0, aux_reset: false }; + let mut out = Drained { keys: 0, motion: 0, aux_reset: false }; let mut lost = false; let dropped = DROPPED.swap(0, Ordering::Relaxed); @@ -670,7 +609,6 @@ fn drain() -> Drained { } while let Some((byte, aux, arrived)) = pop() { - out.bytes += 1; // Whether the run is over and whether it produced anything — a // dropped break or a zero-motion packet counts as "nothing" too. let explained = if aux { @@ -772,22 +710,6 @@ fn quarantine() { ); } -/// `woke_*` are the gates the wakes actually ran under, not a re-derivation, -/// so a test can assert the gate agrees with the event count. -fn trace_drain(bytes: usize, keys: usize, motion: usize, woke_kb: bool, woke_ms: bool) { - if !crate::actuator::i8042_trace() { - return; - } - log!( - "i8042: drain bytes={} keys={} motion={} woke_kb={} woke_ms={}", - bytes, - keys, - motion, - u8::from(woke_kb), - u8::from(woke_ms) - ); -} - // Each read below is done as its section's sole reader: init before the // vector is armed, the aux re-enable on `IRQ_CPU` under `IrqGuard::close`, // and the panic pager with every CPU halted — so no ISR ever races them. @@ -833,11 +755,7 @@ const AUX_RESET: Budget = Budget::of( /// arming write, and that timeout would then present as `DISABLED — cfg … /// did not take`, a controller fault it is not. fn init_budget_ms() -> u64 { - if crate::actuator::i8042_budget_expired() { - 0 - } else { - ms(CONTROLLER) + ms(SELFTEST) + ms(KEYBOARD) + ms(AUX_RESET) - } + ms(CONTROLLER) + ms(SELFTEST) + ms(KEYBOARD) + ms(AUX_RESET) } /// A stage's own deadline, clamped to the probe's; `None` once the probe's @@ -985,7 +903,7 @@ fn query_scancode_set(deadline: u64) -> SetQuery { return SetQuery::Silent; } // A device may ack the command byte and then refuse the argument. - match echo_the_argument(read_data(deadline)) { + match read_data(deadline) { Some(0xFA) => {} Some(other) => return SetQuery::Refused(other), None => return SetQuery::Silent, @@ -996,16 +914,6 @@ fn query_scancode_set(deadline: u64) -> SetQuery { } } -/// Under `i8042-kbd-echo`, answers the argument byte `0xEE` — ECHO's own -/// reply, and the shape a real EC's refusal takes; QEMU always reports its set. -fn echo_the_argument(real: Option) -> Option { - if crate::actuator::i8042_kbd_echo() { - Some(0xEE) - } else { - real - } -} - /// Same as `port_command`, prefixed for port 2. fn aux_command(bytes: &[u8], deadline: u64) -> bool { port_command(bytes, deadline, true) @@ -1046,14 +954,8 @@ fn aux_reenable() { log!("i8042: aux re-enable failed {failures} times — pointer written off, line masked"); } -/// What firmware claims about the 8042 — never what decides. Under -/// `i8042-fadt-denial`, substitutes a real laptop's own FADT (8042 clear) -/// for QEMU's, whose flag and hardware always agree — the only way to test -/// that a denial doesn't stop the probe. +/// What firmware claims about the 8042 — never what decides. fn firmware_claim(rsdp_addr: u64) -> Result<(u8, u16), crate::drivers::acpi::TableError> { - if crate::actuator::i8042_fadt_denial() { - return Ok((6, 0x0011)); - } crate::drivers::acpi::iapc_boot_arch(rsdp_addr) } @@ -1321,12 +1223,6 @@ pub fn init(rsdp_addr: u64) { handler_poll(); crate::arch::cpu::enable_interrupts(); - // Stages this boot's own arming edge: the vector, first, with no byte behind it. - #[cfg(feature = "boot-actuators")] - if crate::actuator::i8042_arm_edge() { - crate::arch::apic::send_self(I8042_VECTOR); - } - log!( "i8042: kbd {} ({}) scanning on, GSI {} -> vec {:#04x} apic {} {}", wire, @@ -1347,10 +1243,6 @@ pub fn init(rsdp_addr: u64) { None => log!("i8042: no pointer on the aux port"), } - if crate::actuator::i8042_fault() { - FAULT.store(true, Ordering::Relaxed); - log!("i8042: fault injection armed"); - } } /// One byte from the controller if it has one; never waits. Only legal once diff --git a/kernel/src/arch/x86_64/idt/exceptions.rs b/kernel/src/arch/x86_64/idt/exceptions.rs index dce2ffc825a..e394f2f401f 100644 --- a/kernel/src/arch/x86_64/idt/exceptions.rs +++ b/kernel/src/arch/x86_64/idt/exceptions.rs @@ -267,20 +267,6 @@ fn crash_report_exception(ctx: &ExceptionContext) { } fn crash_report_panic(info: &core::panic::PanicInfo, rbp: u64) { - // Must run first: if this panics, only DOUBLE PANIC speaks for it — - // everything else comes from the copy `panic::record_panic` took. - if crate::actuator::panic_in_report() { - panic!("panic-in-report: the crash report panicked before it said anything"); - } - #[cfg(feature = "boot-actuators")] - if crate::actuator::fault_in_report() { - // Canonical, high-half, past any physical memory this kernel boots on: - // the read faults instead of hitting the direct map. - const UNMAPPED: u64 = 0xFFFF_8FFF_FFFF_F000; - // SAFETY: none — deliberately unsafe, staged only when the boot - // actuator asked for it, to fault a CPU already `Panic` mid-report. - unsafe { core::ptr::read_volatile(UNMAPPED as *const u64) }; - } alert!("PANIC: {}", info); log!(" Backtrace:"); @@ -480,9 +466,6 @@ fn fatal_exception(ctx: &ExceptionContext) -> ! { ctx.cr2, ctx.frame.error_code, ); - if crate::actuator::panic_in_report() { - panic!("panic-in-report: the crash report panicked before it said anything"); - } let tid_raw = percpu::current_tid().map_or(u32::MAX, |t| t.raw()); if recursive { diff --git a/kernel/src/arch/x86_64/ioapic.rs b/kernel/src/arch/x86_64/ioapic.rs index b765e84cc8f..17f797b42ec 100644 --- a/kernel/src/arch/x86_64/ioapic.rs +++ b/kernel/src/arch/x86_64/ioapic.rs @@ -301,18 +301,6 @@ pub fn route( Ok(()) } -/// The redirection entry for `gsi` exactly as the chip holds it, high word first, or `None` when no unit covers it or the topology is busy. -/// Raw, not decoded: a decode would have to guess which field the caller needs, and this exists to catch an unexpected one. -#[cfg(feature = "boot-actuators")] -pub fn redirection(gsi: Gsi) -> Option { - // try_lock: the caller runs in the idle loop on a possibly-stopped machine. - let topology = TOPOLOGY.try_lock()?; - let (unit, n) = locate(&topology, gsi).ok()?; - let low = unit.read(REG_REDTBL + 2 * n); - let high = unit.read(REG_REDTBL + 2 * n + 1); - Some(u64::from(high) << 32 | u64::from(low)) -} - pub fn set_masked(gsi: Gsi, masked: bool) -> Result<(), RouteError> { let topology = TOPOLOGY.lock(); let (unit, n) = locate(&topology, gsi)?; diff --git a/kernel/src/arch/x86_64/percpu.rs b/kernel/src/arch/x86_64/percpu.rs index eb5655a9313..c19deac2368 100644 --- a/kernel/src/arch/x86_64/percpu.rs +++ b/kernel/src/arch/x86_64/percpu.rs @@ -307,7 +307,7 @@ pub(crate) mod gs { /// that can arrive with `rsp` not a kernel stack (SDM Vol. 3A §6.14.5); `ist[n-1]` is IST*n*. pub(crate) const IST_STACKS: usize = 3; -/// One size for every IST stack, for [`crate::sched::idle_stack::SIZE`]'s reason; must leave room to double the measured high water, which `double_fault_stack` asserts. +/// One size for every IST stack, for [`crate::sched::idle_stack::SIZE`]'s reason; must leave room to double the measured high water. const IST_STACK_SIZE: usize = 16384; /// Filled with [`STACK_FILL`], not unmapped: a fault already on IST1 is a triple fault, so detecting after the fact beats trapping it. @@ -506,13 +506,6 @@ pub fn init_bsp(lapic_id: u32) { // below would stop the machine with the panel holding the record before it. super::idt::init(); - // The first instruction at which a panic is reportable at all, which is why - // it is where this fires: what it judges is that the reset register was - // already decoded, so a panic here can end the machine and not just describe it. - if crate::actuator::test_panic_after_idt() { - panic!("test-panic-after-idt: the IDT is loaded and nothing else is up"); - } - super::fpu::init(0); // Between `fpu::init` and this function's own line: the facts `fpu::init` // established, on a CPU whose extended state QEMU and a Tiger Lake do not diff --git a/kernel/src/arch/x86_64/rtc.rs b/kernel/src/arch/x86_64/rtc.rs index b3c16b0d1b7..a3495ecdf56 100644 --- a/kernel/src/arch/x86_64/rtc.rs +++ b/kernel/src/arch/x86_64/rtc.rs @@ -121,7 +121,7 @@ fn read_registers(century_reg: Option) -> Result { /// Answers the *next* century, so a test can see the register's value reach /// the target year. fn century_read(reg: u8) -> u8 { - if crate::actuator::rtc_century_next() { 0x21 } else { cmos_read(reg) } + cmos_read(reg) } fn wait_for_update() -> Result<(), RtcFault> { @@ -207,16 +207,5 @@ fn port_read(reg: u8) -> u8 { /// The one substitution point for actuator-injected RTC faults; everything /// downstream reads whatever this returns. fn cmos_read(reg: u8) -> u8 { - if crate::actuator::rtc_dead() { - // 0xFF sets `UPDATE_IN_PROGRESS`, so a dead RTC surfaces as `Updating`. - return 0xFF; - } - if crate::actuator::rtc_unstable() && reg == SECONDS { - use core::sync::atomic::{AtomicU8, Ordering::Relaxed}; - static TICK: AtomicU8 = AtomicU8::new(0); - // 0x01..=0x09: always valid BCD, so this stages `Unstable`, not - // `NotADate`. - return TICK.fetch_add(1, Relaxed) % 9 + 1; - } port_read(reg) } diff --git a/kernel/src/arch/x86_64/vtd/domain.rs b/kernel/src/arch/x86_64/vtd/domain.rs index afe5caede28..ab9aaddd951 100644 --- a/kernel/src/arch/x86_64/vtd/domain.rs +++ b/kernel/src/arch/x86_64/vtd/domain.rs @@ -164,11 +164,6 @@ pub fn unmap(id: DomainId, at: Iova, bytes: u64) -> Result<(), IommuError> { } pub fn attach(stream: StreamId, id: DomainId) { - #[cfg(feature = "boot-actuators")] - if super::staged(stream) { - log!("iommu: {stream} keeps the context an actuator staged, over its move to domain {}", id.raw()); - return; - } let mut domains = DOMAINS.lock(); let domain = *domains.at(id); let mut units = UNITS.lock(); diff --git a/kernel/src/arch/x86_64/vtd/mod.rs b/kernel/src/arch/x86_64/vtd/mod.rs index 44c799ad26c..a9e313f5c15 100644 --- a/kernel/src/arch/x86_64/vtd/mod.rs +++ b/kernel/src/arch/x86_64/vtd/mod.rs @@ -468,29 +468,6 @@ fn enable( let root = tables.alloc(); for device in devices { let stream = StreamId::pci(device.bus, device.dev, device.func); - // Unreachable from the host side, so these actuators substitute for - // it; both are answered on the device's first *read*, since a - // first-write access would cache write permission and never fault. - #[cfg(feature = "boot-actuators")] - if crate::actuator::iommu_context_absent() - && device.matches_class(XHCI_CLASS, XHCI_SUBCLASS, Some(XHCI_PROG_IF)) - { - log!("iommu: unit{index} leaves {stream} out of the root table (actuator)"); - STAGED.store(u32::from(stream.requester()), core::sync::atomic::Ordering::Relaxed); - continue; - } - // A present context entry naming an empty domain, distinct from a - // missing entry: passthrough would fault identically either way. - #[cfg(feature = "boot-actuators")] - if crate::actuator::iommu_empty_domain() - && device.matches_class(XHCI_CLASS, XHCI_SUBCLASS, Some(XHCI_PROG_IF)) - { - let empty = tables.alloc(); - log!("iommu: unit{index} gives {stream} a domain with no mappings (actuator)"); - table::bind_identity(&mut tables, root, stream, empty, width); - STAGED.store(u32::from(stream.requester()), core::sync::atomic::Ordering::Relaxed); - continue; - } table::bind_identity(&mut tables, root, stream, domain, width); } @@ -560,26 +537,6 @@ fn enable( ); } -/// The requester id `iommu-context-absent` or `iommu-empty-domain` staged, so -/// its driver's move to a domain of its own leaves the staging in place; -/// `u32::MAX`, which no requester id is, when neither is armed. -#[cfg(feature = "boot-actuators")] -static STAGED: core::sync::atomic::AtomicU32 = core::sync::atomic::AtomicU32::new(u32::MAX); - -/// Whether `stream` is the one an actuator left without a working context. -#[cfg(feature = "boot-actuators")] -pub(super) fn staged(stream: StreamId) -> bool { - STAGED.load(core::sync::atomic::Ordering::Relaxed) == u32::from(stream.requester()) -} - -/// The class the two IOMMU actuators stage on. -#[cfg(feature = "boot-actuators")] -const XHCI_CLASS: u8 = 0x0C; -#[cfg(feature = "boot-actuators")] -const XHCI_SUBCLASS: u8 = 0x03; -#[cfg(feature = "boot-actuators")] -const XHCI_PROG_IF: u8 = 0x30; - /// Slot of the per-width domain cache; exhaustive match so a new `AddressWidth` fails to compile here. fn domain_slot(width: AddressWidth) -> usize { match width { diff --git a/kernel/src/arch/x86_64/vtd/table.rs b/kernel/src/arch/x86_64/vtd/table.rs index 29aafcd58e2..a3cb7812d96 100644 --- a/kernel/src/arch/x86_64/vtd/table.rs +++ b/kernel/src/arch/x86_64/vtd/table.rs @@ -27,9 +27,6 @@ const ADDR_MASK: u64 = 0x000F_FFFF_FFFF_F000; const PRESENT: u64 = 1 << 0; -/// How much room a device domain has under `iommu-domain-narrow`. -pub const NARROW_BYTES: u64 = 128 * 1024 * 1024; - /// The kernel's one domain; not 0, which an all-zero context entry also names — reusing it would blur a fault record and a domain-selective invalidation. pub const KERNEL_DOMAIN: u16 = 1; @@ -239,12 +236,8 @@ impl Domain { /// Where this domain's addresses end: what this unit will translate, not /// what the tables can express — past `MGAW` the hardware faults before the - /// walk it has entries for. `iommu-domain-narrow` brings it down to - /// [`NARROW_BYTES`] above the floor, so running out is a short loop. + /// walk it has entries for. pub fn ceiling(&self) -> u64 { - if crate::actuator::iommu_domain_narrow() { - return self.floor() + NARROW_BYTES; - } 1u64 << self.translatable } diff --git a/kernel/src/arch/x86_64/watchdog.rs b/kernel/src/arch/x86_64/watchdog.rs index 40e162ae0ec..3dec12a4ff0 100644 --- a/kernel/src/arch/x86_64/watchdog.rs +++ b/kernel/src/arch/x86_64/watchdog.rs @@ -13,20 +13,12 @@ use toyos_tco::{ use crate::drivers::pci::PciDevice; use crate::log; -use crate::time::Duration; -const FAST_BOUND: Duration = Duration::from_secs(3); /// Four, so the shipped 9.6 s bound is fed every 2.4 s. What makes a cadence /// that long sound is `kernel/CLAUDE.md`'s rule that no disk wait in this kernel /// can park: a CPU is always on its way back to a scheduler pass. const FEEDS_PER_BOUND: u64 = 4; -/// The fast bound is not a value this kernel can fail to have either. -const FAST_TIMER: u16 = match toyos_tco::timer_for(FAST_BOUND.millis()) { - Some(timer) => timer, - None => panic!("the fast bound reaches no TCO timer"), -}; - /// What the read-back above the arm says. Whole clauses, because a machine /// owner and a test read the same line and neither may have to parse a /// register. @@ -37,9 +29,6 @@ const FAST_TIMER: u16 = match toyos_tco::timer_for(FAST_BOUND.millis()) { const ARMED_ON_ARRIVAL: &str = "the bootloader had already armed the timer"; const UNARMED_ON_ARRIVAL: &str = "nothing had armed the timer"; -/// When `tco-starve` starts starving: boot is long done by here, so a judge measures a reset after starvation and never a race with it. -const STARVE_AFTER: Duration = Duration::from_secs(5); - /// Written by `init` on the BSP before any AP exists, so a relaxed load is the whole of the ordering these need. static PORT: AtomicU16 = AtomicU16::new(0); static NEXT_FEED: AtomicU64 = AtomicU64::new(u64::MAX); @@ -49,7 +38,7 @@ pub fn init(devices: &[PciDevice]) { if !crate::params::watchdog() { return; } - let timer = if crate::actuator::watchdog_fast() { FAST_TIMER } else { TIMER }; + let timer = TIMER; let Some((pci, row)) = devices .iter() @@ -136,9 +125,6 @@ pub fn feed(now: u64) { if port == 0 { return; } - if crate::actuator::watchdog_starve() && now >= STARVE_AFTER.nanos() { - return; - } let next = now + FEED_EVERY_NS.load(Ordering::Relaxed); // A claim, so concurrent CPUs write the port once between them rather than each. if NEXT_FEED.compare_exchange(due, next, Ordering::Relaxed, Ordering::Relaxed).is_err() { diff --git a/kernel/src/block.rs b/kernel/src/block.rs index 36efc841070..72f8fe87e85 100644 --- a/kernel/src/block.rs +++ b/kernel/src/block.rs @@ -479,62 +479,6 @@ impl Partition { } } -/// The duplicate-id control (`block-duplicate-id`): the impostor fills every -/// read with its own mark, so a registry that took it is caught serving that -/// mark for a device it is not. -#[cfg(feature = "boot-actuators")] -pub fn duplicate_id_selftest() { - use alloc::vec; - - const MARK: &[u8] = b"impostor"; - - struct Impostor { - id: DeviceId, - blocks: u64, - } - - impl BlockDevice for Impostor { - fn device_id(&self) -> DeviceId { - self.id - } - fn block_count(&self) -> u64 { - self.blocks - } - fn read_blocks(&mut self, _lba: u64, _count: u32, buf: &mut [u8]) -> BlockResult { - buf.fill(0); - buf[..MARK.len()].copy_from_slice(MARK); - Ok(()) - } - fn write_blocks(&mut self, _lba: u64, _count: u32, _buf: &[u8]) -> BlockResult { - Ok(()) - } - fn flush(&mut self) -> BlockResult { - Ok(()) - } - fn losses(&self) -> u64 { - 0 - } - } - - let before = registered(); - let Some(first) = before.first().cloned() else { - log!("block-duplicate-id: FAIL (this boot registered no block device)"); - return; - }; - let id = first.device_id(); - let refused = register(Box::new(Impostor { id, blocks: first.block_count() })).is_none(); - - let mut buf = vec![0u8; PAGE_SIZE as usize]; - let served = open(id).is_some_and(|h| h.lock().read_blocks(0, 1, &mut buf).is_ok()); - let by_impostor = buf[..MARK.len()] == *MARK; - log!( - "block-duplicate-id: device {id} claimed twice, second registration refused={refused}, \ - devices {} before and {} after, block 0 served={served} by_impostor={by_impostor}", - before.len(), - registered().len() - ); -} - /// Pages the file data cache may hold. pub fn file_cache_pages() -> usize { let (total, _) = crate::mm::pmm::stats(); diff --git a/kernel/src/drivers/acpi.rs b/kernel/src/drivers/acpi.rs index 7ea329f7195..79e52c04852 100644 --- a/kernel/src/drivers/acpi.rs +++ b/kernel/src/drivers/acpi.rs @@ -233,9 +233,6 @@ pub fn iapc_boot_arch(rsdp_addr: u64) -> Result<(u8, u16), TableError> { // `Ok(None)` is "no century register", distinct from `Err`, which the caller must not treat as one. pub fn rtc_century_register(rsdp_addr: u64) -> Result, TableError> { let named = toyos_acpi::rtc_century(direct_phys(), rsdp_addr)?; - // The host can't vary what QEMU's FADT declares, so the actuator override forces "no century register" here. - let named = if crate::actuator::rtc_no_century() { Century::Absent } else { named }; - match named { Century::Absent => { log!("ACPI: the FADT names no RTC century register"); diff --git a/kernel/src/drivers/hda.rs b/kernel/src/drivers/hda.rs index ca6c39b12be..d86471b818d 100644 --- a/kernel/src/drivers/hda.rs +++ b/kernel/src/drivers/hda.rs @@ -456,71 +456,6 @@ pub fn init(devices: &[PciDevice]) { pci.func ); - #[cfg(feature = "boot-actuators")] - if crate::actuator::hda_allowlist_selftest() { - allowlist_selftest(stream_offset); - } - #[cfg(feature = "boot-actuators")] - if crate::actuator::iommu_hda_foreign_bdl() { - run_on_a_foreign_bdl(stream); - } -} - -/// Point the descriptor list at another driver's pool by its *physical* address, which this -/// controller's own domain does not map, and start the stream: the list is fetched at `RUN`. -#[cfg(feature = "boot-actuators")] -fn run_on_a_foreign_bdl(stream: Mmio) { - let foreign = super::xhci::FOREIGN_PROBE.load(Ordering::Relaxed); - assert!(foreign != 0, "hda: this machine staged no foreign pool to aim at"); - stream.write_u32(SD_BDPL, foreign as u32); - stream.write_u32(SD_BDPU, (foreign >> 32) as u32); - stream.write_u8(SD_CTL, SD_CTL_RUN); - log!( - "hda: the stream runs on a descriptor list at {foreign:#x}, inside another driver's \ - pool (actuator)" - ); -} - -/// Every arm of [`write_permit`] and [`read_permit`], run against the bound controller and -/// reported by name. -#[cfg(feature = "boot-actuators")] -fn allowlist_selftest(stream_offset: u64) { - let sd = |field: u64| stream_offset + field; - let cases: &[(&str, u64, RegWidth, u32)] = &[ - ("ICW", IMMEDIATE_COMMAND, RegWidth::U32, 0), - ("SDnFMT", sd(SD_FMT), RegWidth::U16, 0), - ("SDnCTL", sd(SD_CTL), RegWidth::U8, SD_CTL_IOCE as u32), - ("SDnCTL-tag", sd(SD_CTL_TAG), RegWidth::U8, (STREAM_TAG as u32) << 4), - ("SDnBDPL", sd(SD_BDPL), RegWidth::U32, 0), - ("SDnBDPU", sd(SD_BDPU), RegWidth::U32, 0), - ("SDnCBL", sd(SD_CBL), RegWidth::U32, 0), - ("SDnLVI", sd(SD_LVI), RegWidth::U16, 0xFF), - ("SDnSTS", sd(SD_STS), RegWidth::U8, 0), - ("SDnCTL-srst", sd(SD_CTL), RegWidth::U8, SD_CTL_SRST as u32), - // A 32-bit SDnCTL write also reaches SDnSTS, the kernel's own interrupt acknowledgement. - ("SDnCTL-wide", sd(SD_CTL), RegWidth::U32, 0), - ("INTCTL", INTCTL, RegWidth::U32, 0), - ("GCTL", GCTL, RegWidth::U32, 0), - ]; - for &(name, offset, width, value) in cases { - let verdict = match reg_write(offset, width, value) { - Ok(()) => "written", - Err(_) => "refused", - }; - log!("hda: selftest write {name} {verdict}"); - } - for (name, offset, width) in [ - ("ICS", IMMEDIATE_STATUS, RegWidth::U16), - ("IRR", IMMEDIATE_RESPONSE, RegWidth::U32), - ("SDnLPIB", sd(SD_LPIB), RegWidth::U32), - ("STATESTS", STATESTS, RegWidth::U16), - ] { - let verdict = match reg_read(offset, width) { - Ok(_) => "read", - Err(_) => "refused", - }; - log!("hda: selftest read {name} {verdict}"); - } } /// Take one controller out of reset and ask whether anything is on its link; `None` for every way diff --git a/kernel/src/drivers/panic_console/mod.rs b/kernel/src/drivers/panic_console/mod.rs index 5f46719d569..b7fb8373dd0 100644 --- a/kernel/src/drivers/panic_console/mod.rs +++ b/kernel/src/drivers/panic_console/mod.rs @@ -519,7 +519,6 @@ pub fn remap() { /// framebuffer is armed. Freezes the report at the instant of the panic — /// [`live_tail`] re-reads a ring siblings may still be writing to, and a sibling /// logging between panic and paint would push the report off its window. -/// `screen_late_panic` writes such a record and reads the panel for its absence. pub fn capture() { capture_into(false); } diff --git a/kernel/src/drivers/pci.rs b/kernel/src/drivers/pci.rs index 14b18a54104..31b12e1397a 100644 --- a/kernel/src/drivers/pci.rs +++ b/kernel/src/drivers/pci.rs @@ -1,6 +1,4 @@ use alloc::vec::Vec; -#[cfg(feature = "boot-actuators")] -use core::sync::atomic::{AtomicU32, Ordering}; use toyos_pci::{bar, bridge, caps, msi, msix}; @@ -34,34 +32,6 @@ const MSG_DEST: u32 = 0; /// No requester id: a bus/device/function is sixteen bits, so this is none of them. pub(crate) const NO_FUNCTION: u32 = u32::MAX; -/// The one function whose capability list is staged to end early, as a -/// requester id, or [`NO_FUNCTION`]: every other walk in this kernel reads the -/// list the device published. -#[cfg(feature = "boot-actuators")] -static STAGED: AtomicU32 = AtomicU32::new(NO_FUNCTION); - -/// Stages the function a claim is bringing up as one publishing MSI and, past a -/// link the spec forbids, an MSI-X table — for as long as this is held. -#[cfg(feature = "boot-actuators")] -pub(crate) struct StagedCaps; - -#[cfg(feature = "boot-actuators")] -impl StagedCaps { - pub(crate) fn armed_for(pci: &PciDevice) -> Self { - if crate::actuator::pcidev_caps_truncated() { - STAGED.store(u32::from(crate::pcidev::requester(pci)), Ordering::Relaxed); - } - Self - } -} - -#[cfg(feature = "boot-actuators")] -impl Drop for StagedCaps { - fn drop(&mut self) { - STAGED.store(NO_FUNCTION, Ordering::Relaxed); - } -} - /// Why a walk of a function's capability list answered no capability. pub enum NoCapability { /// The walk reached the list's terminator and nothing on it carried the id. @@ -372,7 +342,7 @@ impl PciDevice { /// function has no entry, so the message it would otherwise write is one the /// unit blocks. fn message(&self, vector: u8) -> Option<(u32, u32)> { - let dest = if crate::actuator::iommu_dest_apic1() { 1 } else { MSG_DEST }; + let dest = MSG_DEST; match crate::iommu::remap_msi(self.bus, self.dev, self.func, vector, dest) { // The same CPU the remappable one would name. crate::iommu::Delivery::Direct => match crate::arch::msi_message(dest, vector) { @@ -514,14 +484,6 @@ impl<'a> Iterator for CapabilityIter<'a> { // walk rather than running it off the window or forever. let offset = self.walk.step(self.next)?; self.next = self.device.read_config_u8(offset as u64 + 1); - // A staged function's link past its MSI capability is one byte off - // dword alignment, so the walk ends here and nothing past it is read. - #[cfg(feature = "boot-actuators")] - if STAGED.load(Ordering::Relaxed) == u32::from(crate::pcidev::requester(self.device)) - && self.device.read_config_u8(offset as u64) == msi::CAP_ID - { - self.next = offset | 1; - } Some(Capability { device: self.device, offset: offset as u64 }) } } diff --git a/kernel/src/drivers/usb_storage.rs b/kernel/src/drivers/usb_storage.rs index 980ab593552..ce99fbaf50a 100644 --- a/kernel/src/drivers/usb_storage.rs +++ b/kernel/src/drivers/usb_storage.rs @@ -46,13 +46,6 @@ pub fn untold(index: usize, losses: u64) -> bool { block::open(USB_DEVICE_ID_BASE + index as DeviceId).is_some_and(|disk| disk.untold(losses)) } -/// Whether the controller will still speak to the disk, distinct from a failed -/// transfer — unlike geometry, which outlives recovery giving up on it. -#[cfg(feature = "boot-actuators")] -pub fn healthy(index: usize) -> bool { - xhci::storage_online(index) == Some(true) -} - struct UsbBlockDevice { index: usize, id: DeviceId, diff --git a/kernel/src/drivers/virtio.rs b/kernel/src/drivers/virtio.rs index 5af06658097..381d8983253 100644 --- a/kernel/src/drivers/virtio.rs +++ b/kernel/src/drivers/virtio.rs @@ -165,18 +165,6 @@ const RESET: crate::time::Budget = crate::time::Budget::of( "the device is refused, never waited on", ); -/// The reset handshake's answer; the actuator blinds it to stage a device that -/// never answers, sparing the console — the staged boot's own capture channel. -fn reset_acknowledged(common: &Mmio, pci_dev: &PciDevice) -> bool { - #[cfg(feature = "boot-actuators")] - if crate::actuator::virtio_reset_stuck() && pci_dev.device_id() != 0x1043 { - return false; - } - #[cfg(not(feature = "boot-actuators"))] - let _ = pci_dev; - common.read_u32(COMMON_DEVICE_STATUS) == 0 -} - /// Every driver's accepted set carries [`VIRTIO_F_ACCESS_PLATFORM`]; the actuator /// withholds it to stage a function no unit sees, sparing the console. fn platform_addressing(pci_dev: &PciDevice) -> u64 { @@ -331,32 +319,6 @@ impl DescSlot { pub fn id(&self) -> u16 { self.0 } } -/// Why a used-ring element this driver read is not one it will act on. -/// Refused rather than clamped: there is nothing here to recover from a forged completion. -/// Userland maps virtio-sound's control and event queues writable, so neither device-written field is trustworthy unchecked. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum UsedRefusal { - /// The head descriptor id is not an index into this queue's table. - Head(Refused), - /// The head names a descriptor this queue has published no chain at. - NoChain { id: u16 }, - /// The device claims more bytes written than the chain this head was given. - Written { id: u16, refused: Refused }, -} - -impl core::fmt::Display for UsedRefusal { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Head(refused) => write!(f, "its head descriptor {refused}"), - Self::NoChain { id } => { - write!(f, "a completion for descriptor {id}, where this queue published no chain") - } - Self::Written { id, refused } => { - write!(f, "chain {id} was written {refused}") - } - } - } -} /// Interrupt-context, lock-free consumer of a virtqueue's used ring; an ISR can drain while another CPU submits under a lock. /// Lock-free because it reads only device-written memory and its own `last_used_idx`, never shared driver state. @@ -410,14 +372,6 @@ pub struct Virtqueue<'pool> { used_split: bool, /// Bytes each chain's descriptor was given; the one bound a device-reported `len` is compared against. 0 means no chain. chain_bytes: alloc::vec::Vec, - /// Used-ring elements this queue refused, for the life of the boot. - /// - /// Counted always; the only thing that *reads it out* is - /// [`used_selftest`], in the actuator kernel. The drivers still on this - /// type — console, sound, GPU — answer a refusal where they are rather than - /// by reading a total, and the one that did read it took its driver to - /// userland with it. - refused: u32, } /// Direction of a buffer in a descriptor chain. @@ -455,7 +409,6 @@ impl<'pool> Virtqueue<'pool> { notify_offset: 0, used_split: false, chain_bytes: alloc::vec![0u32; queue_size as usize], - refused: 0, } } @@ -594,7 +547,7 @@ impl<'pool> Virtqueue<'pool> { } /// Non-blocking poll of the used ring: `(DescSlot, written_len)` on completion, `None` if nothing new. - /// A refused element is counted and skipped, never returned, so one forged element cannot hide the ones behind it. + /// A refused element is skipped, never returned, so one forged element cannot hide the ones behind it. /// Never logs: the caller may hold `serial::BackendGuard`, the lock the log backend itself takes. pub fn poll_used(&mut self) -> Option<(DescSlot, u32)> { assert!(!self.used_split, "virtqueue: used ring split off"); @@ -609,60 +562,28 @@ impl<'pool> Virtqueue<'pool> { let id = self.used_ring_id(slot); let len = self.used_ring_len(slot); self.last_used_idx = self.last_used_idx.wrapping_add(1); - match self.parse_used(id, len) { - Ok(elem) => return Some(elem), - Err(_) => { - // Forfeit rather than recovered: losing a token costs throughput, believing a bad one costs memory. - self.refused = self.refused.saturating_add(1); - continue; - } + // Forfeit rather than recovered: losing a token costs throughput, believing a bad one costs memory. + if let Some(elem) = self.parse_used(id, len) { + return Some(elem); } } } - /// What a used-ring element must satisfy, separated from the volatile reads so the self-test can exercise it. - fn parse_used( - &self, - id: Untrusted, - len: Untrusted, - ) -> Result<(DescSlot, u32), UsedRefusal> { + /// What a used-ring element must satisfy: a head inside this queue's table, at a published + /// chain, written no further than that chain. Refused rather than clamped: there is nothing + /// here to recover from a forged completion, and userland maps virtio-sound's control and + /// event queues writable, so neither device-written field is trustworthy unchecked. + fn parse_used(&self, id: Untrusted, len: Untrusted) -> Option<(DescSlot, u32)> { // `chain_bytes` is exactly `size` long: the descriptor table's own bound, not a constant beside it. - let head = id.index(self.chain_bytes.len()).map_err(UsedRefusal::Head)?; - // Exact: `index` proved `head < size`, a `u16`. - let id = head as u16; + let head = id.index(self.chain_bytes.len()).ok()?; let chain = self.chain_bytes[head]; if chain == 0 { - return Err(UsedRefusal::NoChain { id }); + return None; } - let written = len - .at_most(chain as u64) - .map_err(|refused| UsedRefusal::Written { id, refused })?; - // Exact: `at_most` proved it is no more than `chain`, a `u32`. - Ok((DescSlot(id), written as u32)) - } - - /// How many used-ring elements this queue has refused, for [`used_selftest`] - /// alone: every case it stages would pass against a `poll_used` that - /// refused right and counted nothing. - /// - /// Only the actuator kernel has a reader. A shipping kernel's drivers each - /// report their own refusals where they can log — the counter is here, and - /// what is done about it is theirs. - #[cfg(feature = "boot-actuators")] - pub fn refused(&self) -> u32 { - self.refused - } - - /// Write one used-ring element as a device would; the only writer of a used ring in this kernel, for [`used_selftest`] alone. - /// Compiled only into the actuator kernel, so the shipping kernel never gains a way to write its own used ring. - #[cfg(feature = "boot-actuators")] - fn write_used_as_a_device_would(&self, at: u16, id: u32, len: u32) { - let slot = at % self.size; - self.used.write(self.used_elem_at(slot), id); - self.used.write(self.used_elem_at(slot) + 4, len); - // As a device does: the element before the idx. - barrier::dma_wmb(); - self.used.write::(USED_IDX_OFF, at.wrapping_add(1)); + let written = len.at_most(chain as u64).ok()?; + // Exact: `index` proved `head < size`, a `u16`, and `at_most` that `written` is no more + // than `chain`, a `u32`. + Some((DescSlot(head as u16), written as u32)) } /// Submit a descriptor chain and block until the device completes it, returning the recovered `DescSlot`. @@ -737,104 +658,6 @@ fn wait_until(at: u64, mut now: impl FnMut() -> u64, mut look: impl FnMut() - } } -/// [`wait_until`] on a clock that has already passed its bound, as a waiter -/// that was off its CPU for all of it finds it: a completion the next look -/// finds is taken, and one that never comes is `None`. -#[cfg(feature = "boot-actuators")] -pub fn wait_selftest() { - const CASES: usize = 2; - let mut passed = 0usize; - let mut looks = 0u32; - let late = wait_until(0, || u64::MAX, || { - looks += 1; - (looks > LOOKS_PER_CHECK).then_some(()) - }); - if late.is_some() { - passed += 1; - } else { - log!("virtio: wait selftest FAILED on a completion found after the bound: the wait gave up on it"); - } - if wait_until(0, || u64::MAX, || None::<()>).is_none() { - passed += 1; - } else { - log!("virtio: wait selftest FAILED on a device that never answers"); - } - log!("virtio: wait selftest {passed}/{CASES}"); -} - -/// Run [`Virtqueue::poll_used`] over eleven crafted used-ring elements no real device would ever send. -/// Exercises the shipped `poll_used` over a real [`Virtqueue`] and DMA page; only the writer of the ring is not a device. -#[cfg(feature = "boot-actuators")] -pub fn used_selftest() { - use super::DmaPool; - - const SIZE: u16 = 16; - /// The chain the self-test publishes at descriptor 3, in bytes. - const CHAIN: u32 = 256; - /// A descriptor inside the queue that no chain was ever built at. - const UNBUILT: u32 = 5; - const CASES: usize = 11; - - // Not leaked: the pool's pages go back when this returns, and `Dma<'_>`'s borrow keeps the queue from outliving them. - let pool = DmaPool::alloc_in(0x1000, crate::iommu::DeviceSpace::Untranslated); - let dma = pool.view(); - let mut q = Virtqueue::new(dma.subview(0, 0x1000), SIZE); - q.write_chain(3, &[(dma.device_addr(), CHAIN, BufDir::Writable)]); - - // `at` is what the queue's own `last_used_idx` will be when this element is read. - let publish = Virtqueue::write_used_as_a_device_would; - - /// One table row: name, head id, completion length, and what `poll_used` must answer (`None` = must refuse). - type Case = (&'static str, u32, u32, Option<(u16, u32)>); - - /// One element, and what `poll_used` must answer for it. - const TABLE: [Case; 9] = [ - ("a chain the device filled", 3, CHAIN, Some((3, CHAIN))), - ("a chain the device part-filled", 3, 1, Some((3, 1))), - // A readable-only chain: the device wrote nothing into it and says so. - ("a chain the device wrote nothing into", 3, 0, Some((3, 0))), - ("a head past the queue", SIZE as u32, 0, None), - // 0x1_0003 narrows to 3 under `as u16`; a driver that truncated before comparing would accept this. - ("a head whose low 16 bits are in range", 0x1_0003, CHAIN, None), - ("a head of every bit", u32::MAX, 0, None), - ("one byte more than the chain", 3, CHAIN + 1, None), - ("a length of every bit", 3, u32::MAX, None), - ("a completion for a chain never published", UNBUILT, 0, None), - ]; - - let mut passed = 0usize; - let mut at = 0u16; - for (name, id, len, want) in TABLE { - publish(&q, at, id, len); - at = at.wrapping_add(1); - let got = q.poll_used().map(|(slot, len)| (slot.id(), len)); - if got == want { - passed += 1; - } else { - log!("virtio: used-ring selftest FAILED on {name}: got {got:?}, want {want:?}"); - } - } - - // A completion behind a refused element is still delivered: forging one element must not hide the rest. - publish(&q, at, u32::MAX, u32::MAX); - publish(&q, at.wrapping_add(1), 3, CHAIN); - let got = q.poll_used().map(|(slot, len)| (slot.id(), len)); - if got == Some((3, CHAIN)) { - passed += 1; - } else { - log!("virtio: used-ring selftest FAILED on a chain behind a refused element: got {got:?}"); - } - - // The count is checked too: every case above would pass against a `poll_used` that refused right but counted nothing. - let refused = q.refused(); - if refused != 7 { - log!("virtio: used-ring selftest FAILED on the count: refused {refused}, want 7"); - } else { - passed += 1; - } - log!("virtio: used-ring selftest {passed}/{CASES}"); -} - /// Drive the real walk, window check and parse over config space no device produces: a cyclic /// or forbidden link, a BAR/offset/length past the window, a chain missing a required capability. #[cfg(feature = "boot-actuators")] @@ -1027,7 +850,7 @@ impl VirtioDevice { // Order fixed by virtio 1.2 §3.1.1: reset, ACKNOWLEDGE, DRIVER, negotiate features, FEATURES_OK, verify. common.write_u32(COMMON_DEVICE_STATUS, 0); - if !crate::clock::settles(RESET.nanos(), || reset_acknowledged(&common, pci_dev)) { + if !crate::clock::settles(RESET.nanos(), || common.read_u32(COMMON_DEVICE_STATUS) == 0) { pci_dev.disable_bus_master(); return Err(InitRefusal::ResetUnanswered); } diff --git a/kernel/src/drivers/virtio_gpu.rs b/kernel/src/drivers/virtio_gpu.rs index 0fcc39340b2..e58b5a251a8 100644 --- a/kernel/src/drivers/virtio_gpu.rs +++ b/kernel/src/drivers/virtio_gpu.rs @@ -42,10 +42,6 @@ const CURSOR_SIZE: u32 = 64; const CURSOR_RESOURCE_ID: u32 = 1; /// Scanouts count up from here, so no mode change ever reuses the cursor's id. const FIRST_SCANOUT_RESOURCE_ID: u32 = 2; -#[cfg(feature = "boot-actuators")] -const FOREIGN_RESOURCE_ID: u32 = u32::MAX; -#[cfg(feature = "boot-actuators")] -const FOREIGN_COLUMNS: u32 = 32; const REQ_OFFSET: usize = 0x000; const RESP_OFFSET: usize = 0x800; @@ -447,37 +443,6 @@ impl GpuController { Some(FbAlloc { regions, backing: Some(backing) }) } - /// Hand the device a backing in another driver's pool, by its *physical* - /// address, which this display's own domain does not map. The answer is - /// logged, not asserted: the unit's fault halts every CPU from the handler, - /// so which of the halt and the response arrives first is a race. - #[cfg(feature = "boot-actuators")] - fn attach_a_foreign_backing(&mut self) { - let foreign = - super::xhci::FOREIGN_PROBE.load(core::sync::atomic::Ordering::Relaxed); - assert!(foreign != 0, "VirtIO GPU: this machine staged no foreign pool to aim at"); - // Sized to the probe exactly, so one transfer of the whole resource reads every byte of it. - const ROWS: u32 = super::xhci::PROBE_LEN as u32 / (FOREIGN_COLUMNS * 4); - self.create_resource(FOREIGN_RESOURCE_ID, FORMAT_B8G8R8X8_UNORM, FOREIGN_COLUMNS, ROWS); - let resp = self.attach_backing_answering( - FOREIGN_RESOURCE_ID, - foreign, - super::xhci::PROBE_LEN as u32, - ); - log!( - "VirtIO GPU: a backing at {foreign:#x}, inside another driver's pool, was answered \ - {resp:#x} (actuator)" - ); - if resp == RESP_OK_NODATA { - let rect = Rect { x: 0, y: 0, width: FOREIGN_COLUMNS, height: ROWS }; - self.transfer_to_host(FOREIGN_RESOURCE_ID, rect, 0); - log!( - "VirtIO GPU: the device read all {} bytes of it (actuator)", - super::xhci::PROBE_LEN - ); - } - } - fn build_gpu_info(&self) -> GpuInfo { GpuInfo { scanout: self.fb.regions.clone(), @@ -676,11 +641,6 @@ pub fn init(devices: &[PciDevice]) -> Option<(Box, GpuInfo)> { gpu.width = width; gpu.height = height; - #[cfg(feature = "boot-actuators")] - if crate::actuator::iommu_gpu_foreign_backing() { - gpu.attach_a_foreign_backing(); - } - let info = gpu.build_gpu_info(); Some((Box::new(gpu), info)) diff --git a/kernel/src/drivers/virtio_sound.rs b/kernel/src/drivers/virtio_sound.rs index 416d9001b5f..c5cf09e9d47 100644 --- a/kernel/src/drivers/virtio_sound.rs +++ b/kernel/src/drivers/virtio_sound.rs @@ -344,11 +344,6 @@ pub fn init(devices: &[PciDevice]) { device.enable_queue(abi::TX_QUEUE); device.activate(); - #[cfg(feature = "boot-actuators")] - if crate::actuator::iommu_sound_foreign_dma() { - answer_into_a_foreign_page(&mut controlq, &device, shared); - } - // DmaPool allocations are whole 2 MiB pages; ABI offsets are relative to that page. let dma_region = Region { phys: crate::DirectMap::from_phys(shared.host_phys()), @@ -389,36 +384,6 @@ fn queue<'pool>( Virtqueue::from_regions(&VirtqueueRegions::from_separate(desc, avail, used, size), size) } -/// Submit one control command whose answer buffer is in another driver's pool, -/// by its *physical* address, which this function's own domain does not map. -/// The request is the zeroed page's four bytes, a code the device answers with -/// one status word; the descriptors sit past the chain `build_chains` wrote. -#[cfg(feature = "boot-actuators")] -fn answer_into_a_foreign_page( - controlq: &mut Virtqueue<'static>, - device: &VirtioDevice, - shared: Dma<'static>, -) { - const FOREIGN_DESC: usize = 2; - let foreign = super::xhci::FOREIGN_PROBE.load(Ordering::Relaxed); - assert!(foreign != 0, "virtio-sound: this machine staged no foreign pool to aim at"); - let slot = controlq.initial_slots().swap_remove(FOREIGN_DESC); - controlq.submit( - slot, - &[ - (shared.device_addr() + abi::OFF_CTRL_REQ as u64, 4, BufDir::Readable), - (foreign, 8, BufDir::Writable), - ], - device.notify_mmio(), - device.notify_off_multiplier(), - abi::CONTROL_QUEUE, - ); - log!( - "virtio-sound: a control answer aimed at {foreign:#x}, inside another driver's pool \ - (actuator)" - ); -} - /// Builds every chain once; after this no descriptor is ever written again — the /// driver's whole vocabulary becomes an avail-ring index and a doorbell write. fn build_chains( diff --git a/kernel/src/drivers/xhci/device.rs b/kernel/src/drivers/xhci/device.rs index ace0d9cc609..4d44c49e1e6 100644 --- a/kernel/src/drivers/xhci/device.rs +++ b/kernel/src/drivers/xhci/device.rs @@ -232,7 +232,7 @@ pub fn reset_port(ctrl: &mut XhciController, port_idx: u8, kind: Reset) { /// Whether the port has finished the reset it was asked for. pub fn reset_done(ctrl: &XhciController, port_idx: u8) -> bool { - super::port_answers() && ctrl.read_portsc(port_idx).reset_finished() + ctrl.read_portsc(port_idx).reset_finished() } /// One device's enumeration: the state an answer needs, carried because the pass that asked gave up its stack. diff --git a/kernel/src/drivers/xhci/mod.rs b/kernel/src/drivers/xhci/mod.rs index 8f998793ad7..e7b97ef8d96 100644 --- a/kernel/src/drivers/xhci/mod.rs +++ b/kernel/src/drivers/xhci/mod.rs @@ -56,12 +56,7 @@ const USBCMD_HCRST: u32 = 1 << 1; const USBSTS_HCH: u32 = 1 << 0; const USBSTS_CNR: u32 = 1 << 11; -// Raw bits for the two paths that work on a word, not a decoded register: read_portsc's actuator injections and init_one's pre-controller port power. Every decision on these bits goes through `Portsc`, never the raw consts, outside these two paths. -const PORTSC_CCS: u32 = 1 << 0; -const PORTSC_PED: u32 = 1 << 1; -const PORTSC_PR: u32 = 1 << 4; const PORTSC_PP: u32 = 1 << 9; -const PORTSC_SPEED: u32 = 0xF << 10; /// HCCPARAMS1 bit 3: Port Power Control, which decides PORTSC's PP after reset. const HCC_PPC: u32 = 1 << 3; @@ -331,42 +326,12 @@ fn deadline() -> u64 { crate::clock::nanos_since_boot() + USB_TIMEOUT_NS } -/// Let a test starve one of those waits on a controller that otherwise answers perfectly; a kernel feature because QEMU cannot stage a register bit that never settles. -fn controller_answers() -> bool { - !crate::actuator::xhci_deaf_controller() -} - -fn port_answers() -> bool { - !crate::actuator::xhci_deaf_port() -} - /// The boot-time connect settle reads the same interval the per-port machine uses. use portmachine::DEBOUNCE_NS as PORT_DEBOUNCE_NS; -/// How long the slow-connect injection reports an empty root hub after this -/// controller powered its ports. -/// -/// A kernel feature since QEMU cannot stage a port that connects late, and it -/// replaces the register rather than a verdict: the port reads exactly as -/// unpopulated during the window. -use portmachine::SLOW_CONNECT_NS; - -/// Report *one* root-hub port empty while every other port reads normally. -/// -/// The window closes on the boot scan, not the clock: what it stages is an ordering, not a duration. -const SLOW_STORAGE_PORT: u8 = 0; - -/// Whether the boot port scan has run; until it has, [`SLOW_STORAGE_PORT`] reads unpopulated. -pub(super) static BOOT_SCAN_DONE: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); - /// One bit per root-hub port; four words cover every MaxPorts a byte can express. type PortMask = [u64; 4]; -fn port_bit(mask: &PortMask, port_idx: u8) -> bool { - mask[port_idx as usize / 64] & (1 << (port_idx % 64)) != 0 -} - /// When some controller's port state machine must be stepped again, or 0 for none; neither reader may take [`XHCI`]. /// /// A CPU with nothing else to run must not sleep while this is set — nothing else would wake it for deferred port work. @@ -485,20 +450,6 @@ const PAGE: usize = 0x1000; // The pool's fixed head: one of each, since enumeration is serial — see `device::init_device`. #[allow(clippy::erasing_op)] const OFF_DCBAA: usize = 0 * PAGE; // (max_slots + 1) * 8, 2 KiB at most - -/// The half of the DCBAA's page no slot reaches — its at most 256 entries -/// fill the first — zeroed at bring-up and never written: another device's -/// IOMMU control is aimed here, and it must still read zero afterwards. -#[cfg(feature = "boot-actuators")] -pub(crate) const PROBE_OFF: usize = OFF_DCBAA + 0x800; -#[cfg(feature = "boot-actuators")] -pub const PROBE_LEN: usize = 0x800; - -/// Physical, not what this controller is programmed with: the actuator has to -/// hand another device an address that device's own domain does not map. The -/// first controller's. -#[cfg(feature = "boot-actuators")] -pub static FOREIGN_PROBE: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); #[allow(clippy::identity_op)] const OFF_CMD_RING: usize = 1 * PAGE; const OFF_ERST: usize = 2 * PAGE; @@ -540,15 +491,6 @@ const MSC_MAX_BLOCKS: u32 = (MSC_DATA_LEN / 4096) as u32; /// Device blocks to size the pool for before the controller's slot count is consulted; without this floor a scratchpad demand near a 2 MiB boundary can leave zero room for devices. const MIN_DEVICE_BLOCKS: usize = 8; -/// Cap the driver at one device block, so a test can drive the slot-pool-full path; QEMU's `slots=N` cannot stage it since Enable Slot ignores MaxSlotsEn. -fn device_ceiling() -> usize { - if crate::actuator::xhci_one_slot() { - 1 - } else { - usize::MAX - } -} - /// Where each structure sits in the pool, derived from what the controller reported. #[derive(Clone, Copy)] struct Layout { @@ -576,8 +518,7 @@ impl Layout { // DmaPool hands out whole 2 MiB pages; the floor decides how many pages, the slack decides how many device blocks. let pool_size = crate::mm::align_2m(dev_base + MIN_DEVICE_BLOCKS * DEV_STRIDE); let dev_blocks = ((pool_size - dev_base) / DEV_STRIDE) - .min(max_slots as usize) - .min(device_ceiling()); + .min(max_slots as usize); Self { scratch_array, @@ -767,11 +708,6 @@ pub struct XhciController { /// Submitted and left, not spun on: a scheduler pass may not block to [`USB_TIMEOUT_NS`] against a device with nothing to answer. outstanding: Outstanding, - /// Ports this driver has written PED=1 to; a kernel feature since QEMU's PED write is a no-op and cannot be staged otherwise. - /// - /// Replaces the register, not a verdict: the port reads PED clear for every reader until reset (§4.19.1.1.3). - software_disabled: PortMask, - /// What the disk call now inside this controller may still spend, once its transport has broken; closed between calls. after_break: AfterBreak, @@ -802,49 +738,15 @@ impl XhciController { } fn read_portsc_raw(&self, port_idx: u8) -> u32 { - let raw = self.op_base.read_u32(OP_PORT_BASE + port_idx as u64 * PORT_REG_SIZE); - if crate::actuator::xhci_slow_connect() - && crate::clock::nanos_since_boot().saturating_sub(self.powered_at) < SLOW_CONNECT_NS - { - return raw & !(PORTSC_CCS | PORTSC_PED | PORTSC_SPEED); - } - if crate::actuator::xhci_slow_storage_connect() - && port_idx == SLOW_STORAGE_PORT - && !BOOT_SCAN_DONE.load(core::sync::atomic::Ordering::Relaxed) - { - return raw & !(PORTSC_CCS | PORTSC_PED | PORTSC_SPEED); - } - if crate::actuator::xhci_portsc_rw1c() && port_bit(&self.software_disabled, port_idx) { - return raw & !PORTSC_PED; - } - // Also masks PED: QEMU's SuperSpeed port reads Enabled instantly, so without this the actuator stages nothing. - if crate::actuator::xhci_deaf_port() { - return raw & !PORTSC_PED; - } - raw + self.op_base.read_u32(OP_PORT_BASE + port_idx as u64 * PORT_REG_SIZE) } /// Every write of a port register; takes a typed [`toyos_xhci::portsc::Write`], which offers no way to set PED, so disabling a port the driver is enabling is unreachable rather than asserted against. fn write_portsc(&mut self, port_idx: u8, write: toyos_xhci::portsc::Write) { let value = write.raw(); - if crate::actuator::xhci_portsc_rw1c() { - let word = port_idx as usize / 64; - let bit = 1u64 << (port_idx % 64); - if value & PORTSC_PED != 0 { - self.software_disabled[word] |= bit; - } - if value & PORTSC_PR != 0 { - self.software_disabled[word] &= !bit; - } - } self.op_base.write_u32(OP_PORT_BASE + port_idx as u64 * PORT_REG_SIZE, value); } - /// How many ports the driver has written PED=1 to. - fn software_disabled_ports(&self) -> u32 { - self.software_disabled.iter().map(|w| w.count_ones()).sum() - } - /// The port a slot's device is on, or `None` for a slot mid-enumeration — `device::finish` is what gives a port its slot. fn port_of_slot(&self, slot: u8) -> Option { self.ports @@ -895,12 +797,6 @@ impl XhciController { } barrier::dma_rmb(); let event: Trb = self.event_ring.read(at); - // A controller that has not answered yet, which QEMU cannot be: it - // posts a command's completion inside the write to the doorbell. - #[cfg(feature = "boot-actuators")] - if !msc::bind_spends_the_scan::answered() { - return None; - } self.advance_event_ring(); Some(event) } @@ -1673,13 +1569,6 @@ fn lock_settles() -> bool { /// machine nobody can turn off. Not fair — a competitor taking a ticket wins — /// which is why both callers say what they do without it. fn take_within(bound: u64) -> Option>> { - #[cfg(feature = "boot-actuators")] - if crate::actuator::xhci_lock_wedged() { - // The bound is spent, not skipped: what the control is about is that a - // shutdown pays it once and then resets anyway. - crate::clock::settles(bound, || false); - return None; - } let until = crate::clock::tsc_deadline(bound); loop { if let Some(guard) = XHCI.try_lock() { @@ -1824,18 +1713,3 @@ fn on_disk( pub fn storage_geometry(index: usize) -> Option { with_disk(index, |ctrl, at| Some(ctrl.msc[at].disk?.dev.geometry())).flatten() } - -/// Whether the machine's `index`-th disk is still being spoken to; `Some(false)` and not `None` for an unplugged one, since the caller already holds a handle. -#[cfg(feature = "boot-actuators")] -pub fn storage_online(index: usize) -> Option { - (index < storage_count()).then(|| { - with_disk(index, |ctrl, at| ctrl.msc[at].disk.is_some_and(|d| d.dev.online())) - .unwrap_or(false) - }) -} - -/// Stop this machine inside the next WRITE(10), at `at`. See [`msc::mid_write`]. -#[cfg(feature = "boot-actuators")] -pub fn arm_mid_write_wedge(at: toyos_xhci::bot::Phase) { - msc::mid_write::arm(at); -} diff --git a/kernel/src/drivers/xhci/wait/boot.rs b/kernel/src/drivers/xhci/wait/boot.rs index 269b59e7e46..66d252b0a1e 100644 --- a/kernel/src/drivers/xhci/wait/boot.rs +++ b/kernel/src/drivers/xhci/wait/boot.rs @@ -21,7 +21,7 @@ use super::super::{OFF_CMD_RING, OFF_DCBAA, OFF_ERST, OFF_EVT_RING}; use super::super::{OP_CONFIG, OP_CRCR, OP_DCBAAP, OP_PAGESIZE, OP_PORT_BASE, OP_USBCMD, OP_USBSTS}; use super::super::{USBCMD_HCRST, USBCMD_RS, USBSTS_CNR, USBSTS_HCH}; use super::super::{PORTSC_PP, PORT_REG_SIZE, XHCI}; -use super::super::{controller_answers, PORT_DEBOUNCE_NS}; +use super::super::PORT_DEBOUNCE_NS; use super::settles; use toyos_xhci::port::{self, GaveUp, Reset, ResetOutcome}; use toyos_xhci::Protocol; @@ -313,17 +313,17 @@ fn init_one(pci_dev: &PciDevice) -> Option { op_base.write_u32(OP_USBCMD, usbcmd & !USBCMD_RS); } let deadline_ms = USB_TIMEOUT_NS / 1_000_000; - if !settles(|| controller_answers() && op_base.read_u32(OP_USBSTS) & USBSTS_HCH != 0) { + if !settles(|| op_base.read_u32(OP_USBSTS) & USBSTS_HCH != 0) { refuse(format_args!("it never halted, within {deadline_ms} ms of being asked to")); return None; } op_base.write_u32(OP_USBCMD, USBCMD_HCRST); - if !settles(|| controller_answers() && op_base.read_u32(OP_USBCMD) & USBCMD_HCRST == 0) { + if !settles(|| op_base.read_u32(OP_USBCMD) & USBCMD_HCRST == 0) { refuse(format_args!("it held HCRST for {deadline_ms} ms")); return None; } - if !settles(|| controller_answers() && op_base.read_u32(OP_USBSTS) & USBSTS_CNR == 0) { + if !settles(|| op_base.read_u32(OP_USBSTS) & USBSTS_CNR == 0) { refuse(format_args!("it stayed Controller Not Ready for {deadline_ms} ms after its reset")); return None; } @@ -359,13 +359,6 @@ fn init_one(pci_dev: &PciDevice) -> Option { } op_base.write_u64(OP_DCBAAP, dma.device_addr() + OFF_DCBAA as u64); - #[cfg(feature = "boot-actuators")] - let _ = super::super::FOREIGN_PROBE.compare_exchange( - 0, - dma.subview(super::super::PROBE_OFF, super::super::PROBE_LEN).host_phys(), - core::sync::atomic::Ordering::Relaxed, - core::sync::atomic::Ordering::Relaxed, - ); // CRCR bit 0 is RCS; the pointer is 64-byte aligned so `| 1` only sets // that bit (xHCI 1.2 §5.4.5). @@ -387,7 +380,7 @@ fn init_one(pci_dev: &PciDevice) -> Option { op_base.write_u32(OP_USBCMD, 1 | (1 << 2)); } - if !settles(|| controller_answers() && op_base.read_u32(OP_USBSTS) & 1 == 0) { + if !settles(|| op_base.read_u32(OP_USBSTS) & 1 == 0) { refuse(format_args!("it stayed halted for {deadline_ms} ms after R/S")); return None; } @@ -447,7 +440,6 @@ fn init_one(pci_dev: &PciDevice) -> Option { .collect(), ports_dirty: false, outstanding: Outstanding::EMPTY, - software_disabled: [0u64; 4], after_break: toyos_xhci::call::AfterBreak::CLOSED, bulk_began: 0, stopped: None, @@ -542,14 +534,5 @@ pub fn scan_ports(ctrl: &mut XhciController) { // Completions from an earlier port's device can arrive during a later // port's enumeration; without this drain a broken one goes unrecorded. ctrl.settle_outstanding(); - // After acknowledge_port_changes: the connect this raises must be a change - // the port machine sees, not one the scan just cleared. - if crate::actuator::xhci_slow_storage_connect() { - super::super::BOOT_SCAN_DONE.store(true, core::sync::atomic::Ordering::Relaxed); - } - if crate::actuator::xhci_portsc_rw1c() { - log!("xHCI: PED as RW1C, {} port(s) disabled by a driver write", - ctrl.software_disabled_ports()); - } } diff --git a/kernel/src/drivers/xhci/wait/mod.rs b/kernel/src/drivers/xhci/wait/mod.rs index 29800a3bf54..83264a62240 100644 --- a/kernel/src/drivers/xhci/wait/mod.rs +++ b/kernel/src/drivers/xhci/wait/mod.rs @@ -14,26 +14,7 @@ pub mod msc; /// measurement only. #[cfg(feature = "boot-actuators")] mod depth_probe { - use core::sync::atomic::{AtomicU32, Ordering}; - use crate::log; - - static DEEPEST: AtomicU32 = AtomicU32::new(0); - - pub fn report() { - let depth = crate::preempt::count(); - // Logs only a new deepest depth: logging every wait would write to - // the same device the wait is on, a self-sustaining loop. - if depth <= DEEPEST.fetch_max(depth, Ordering::Relaxed) { - return; - } - log!( - "io-depth: a disk transfer is being waited for at preempt depth {depth}, task {:?}", - crate::arch::percpu::current_tid().map(|t| t.raw()) - ); - // `kernel_backtrace` stops at the first unreadable frame. - crate::symbols::kernel_backtrace(crate::arch::cpu::frame_pointer(), 20); - } } use crate::log; @@ -370,10 +351,6 @@ impl XhciController { /// Matched by (slot, dci, trb) rather than the endpoint, since a stalled /// endpoint still completes late transfers this driver stopped waiting for. fn wait_transfer(&mut self, slot: u8, dci: u8, trb: u64) -> Result<(u32, u32), Quiet> { - #[cfg(feature = "boot-actuators")] - if crate::actuator::io_depth_probe() { - depth_probe::report(); - } let on = Await::Transfer { slot, dci, trb }; let (began, deadline) = self.wait_ends(); let port = self.port_of_slot(slot); diff --git a/kernel/src/drivers/xhci/wait/msc.rs b/kernel/src/drivers/xhci/wait/msc.rs index 16bfc7b8ed7..8912ddf5157 100644 --- a/kernel/src/drivers/xhci/wait/msc.rs +++ b/kernel/src/drivers/xhci/wait/msc.rs @@ -115,13 +115,6 @@ pub struct MscDevice { } impl MscDevice { - /// Whether the driver will still speak to this device — distinct from - /// `blocks > 0`, which survives a failure. - #[cfg(feature = "boot-actuators")] - pub fn online(&self) -> bool { - !self.failed - } - /// Whether this device has answered SYNCHRONIZE CACHE with INVALID COMMAND /// OPERATION CODE, which is what makes a flush's `Ok(())` mean "there was /// nothing to make durable" rather than "a cache was emptied". @@ -306,168 +299,15 @@ mod transport_break { } } -/// Stop every CPU inside one WRITE(10), at whichever of its three phases was -/// staged. -/// -/// **The state a boot that hangs during stick I/O leaves the device in**, and -/// the one thing no ordinary boot reaches: a machine wedged here is ended by -/// the boot deadline alone, and what the reset then does to a device holding -/// half a command is what `stop::settle_commands` exists to decide. -/// -/// **Which phase is the whole question, so the caller names one.** The device -/// sees three different things — a CBW with no data coming, a data phase queued -/// and not rung for, and data it has taken with nothing asking for its CSW — -/// and only the machine can say which of them it does not come back from. -/// -/// Staged rather than waited for, because a shutdown reaches its sync with -/// nothing dirty on most boots: the write this is taken inside is one -/// `usb_gate::wedge_inside_a_write` issues for it. -#[cfg(feature = "boot-actuators")] -pub(in crate::drivers::xhci) mod mid_write { - use core::sync::atomic::{AtomicU8, Ordering}; - - use toyos_xhci::bot::Phase; - - /// [`Phase::code`] of the phase to stop at, or `Phase::Closed`'s — which no - /// call site passes — for a boot that staged none. - static AT: AtomicU8 = AtomicU8::new(0); - - /// Called immediately before the write this wedge is taken inside. - pub fn arm(at: Phase) { - AT.store(at.code(), Ordering::Relaxed); - } - - /// Called at each of the three phases, each passing its own. - /// - /// Compare-and-take, not a test and a clear: every command walks all three - /// call sites, so a phase that takes the staging away from the phase it was - /// staged for is a boot that wedges nowhere. - pub fn wedge_if_staged(here: Phase) { - let taken = AT.compare_exchange( - here.code(), - Phase::Closed.code(), - Ordering::Relaxed, - Ordering::Relaxed, - ); - if taken.is_ok() { - crate::deadline::stage_a_wedge() - } - } -} - -/// Stage the boot's first bind to spend the scan's whole silence bound and then -/// refuse, once: T14 run 103's stick, whose first command went unanswered, whose -/// recovery ladder then ran on bounds of its own, and whose refusal at the end -/// of all that submitted a Disable Slot into a scan that had already stopped -/// listening. Staged because no QEMU device stops answering its first command. -/// -/// The spending and the refusal are staged and the ladder is not: what the -/// defect needs is a submit later than the scan's bound, and which rungs ran -/// decides nothing about that. -/// -/// **The held answer is the second half and not decoration.** QEMU posts a -/// Command Completion Event inside the vCPU's write to the doorbell, so the very -/// next read of the event ring already has it and the scan's silence bound is -/// re-armed before it can be spent — on a machine whose controller takes -/// microseconds to answer it is not. [`hold_answers`] is that latency, and -/// without it no QEMU boot reaches the state this stages. -#[cfg(feature = "boot-actuators")] -pub(in crate::drivers::xhci) mod bind_spends_the_scan { - use core::sync::atomic::{AtomicBool, AtomicU64, Ordering}; - - /// Past the scan's bound, which is `USB_TIMEOUT_NS`: the refusal has to - /// land after the scan has stopped re-arming it, and a bind that spent - /// exactly the bound would race it. - pub const SPEND: u64 = super::super::super::USB_TIMEOUT_NS + 200_000_000; - - /// How long the controller's answer to what the refusal submits is held - /// back. It has only to outlast the one loop iteration that follows the - /// submit; the width above that is so a scan that waits can be seen to. - const HOLD: u64 = 50_000_000; - - pub const WHY: &str = "answers nothing for the boot scan's whole bound \ - (usb-bind-spends-the-scan) and is then refused"; - - static UNSPENT: AtomicBool = AtomicBool::new(true); - static HELD_UNTIL: AtomicU64 = AtomicU64::new(0); - - /// Whether this bind is the staged one. - pub fn take() -> bool { - crate::actuator::usb_bind_spends_the_scan() && UNSPENT.swap(false, Ordering::Relaxed) - } - - /// Hold the controller's answers back, from the staged bind's way out — so - /// the window covers what its refusal submits and nothing before it. - pub fn hold_answers() { - HELD_UNTIL.store(crate::clock::nanos_since_boot() + HOLD, Ordering::Relaxed); - } - - /// Whether the event ring may be read yet. Zero is the unarmed state, so an - /// unstaged boot pays one relaxed load per event and no clock read. - pub fn answered() -> bool { - let until = HELD_UNTIL.load(Ordering::Relaxed); - until == 0 || crate::clock::nanos_since_boot() >= until - } -} - /// What the port rung's reset is made for, in its line. const RECOVERING: &str = "recovering"; -/// Hold the port rung, once, until its port reads empty: QEMU cannot move a -/// device off its port on a reset, so the host takes it off. `usb-reset-moves` -/// holds before the reset's completion is read, so the rung reads the port empty; -/// `usb-reset-moves-after` once it has been read with the device on the port, -/// as a USB2 port reads a device that leaves under its reset; -/// `usb-reset-moves-configured` once the rung has configured the device again, -/// so its TEST UNIT READY meets an empty port. -#[cfg(feature = "boot-actuators")] -pub(in crate::drivers::xhci) mod reset_moves { - use core::sync::atomic::{AtomicBool, Ordering}; - - static UNSPENT: AtomicBool = AtomicBool::new(true); - - /// What each hold says, which the host acts on. - pub const HELD: &str = "is held empty for the host to move its device (usb-reset-moves)"; - pub const HELD_AFTER: &str = - "is held, reset with its device on it, for the host to move the device (usb-reset-moves-after)"; - pub const HELD_CONFIGURED: &str = "is held, configured again, for the host to move the device \ - (usb-reset-moves-configured)"; - - /// The cue the host moves the device on, written to the console directly: - /// the record above reaches it only when `klogd` runs, which it may not - /// while this CPU spins in the rung, and a cue that arrives after the - /// rung's bound stages a device that left too late. - const MOVE_NOW: &[u8] = b"usb-reset-moves: move the device now\n"; - - /// Whether the hold `staged` arms is taken here: one hold per boot. - pub fn take(staged: bool) -> bool { - staged && UNSPENT.swap(false, Ordering::Relaxed) - } - - pub fn cue() { - crate::drivers::serial::BackendGuard::lock().write_raw(MOVE_NOW); - } -} - /// The one line a device's refusal produces, wherever it is noticed — one /// function so per-caller wording never obscures what the device said. fn log_refusal(cdb: &Cdb, sense: Sense) { log!("usb-storage: SCSI {:#04x} failed, sense {sense}", cdb.opcode()); } -/// The sense a test actuator makes SYNCHRONIZE CACHE answer with, or `None` -/// on a shipped kernel. ILLEGAL REQUEST/INVALID COMMAND OPERATION CODE must -/// not fail the caller; HARDWARE ERROR/INTERNAL TARGET FAILURE must. -fn flush_sense() -> Option { - if crate::actuator::usb_flush_unimplemented() { - Some(Sense { key: 0x05, asc: 0x20, ascq: 0x00 }) - } else if crate::actuator::usb_flush_fails() { - Some(Sense { key: 0x04, asc: 0x44, ascq: 0x00 }) - } else { - None - } -} - /// A bulk transfer's completion, as the round trip hears it. fn completed(completion: Result<(u32, u32), Quiet>) -> bot::Answer { match completion { @@ -594,9 +434,7 @@ impl XhciController { return Ok(()); } let cdb = Cdb::SYNCHRONIZE_CACHE; - let issued = ctrl.scsi(dev, &cdb, None, until); - let reply = flush_sense().map_or(issued, Reply::Refused); - match scsi::flushed(reply) { + match scsi::flushed(ctrl.scsi(dev, &cdb, None, until)) { Flushed::NoCache => { dev.no_write_cache = true; log!("usb-storage: disk {number} does not implement SYNCHRONIZE CACHE \ @@ -903,10 +741,6 @@ impl XhciController { dev.reset_at = Some(crate::clock::nanos_since_boot()); self.settles_within_call(|| self.read_portsc(port_idx).reset_finished()) }; - #[cfg(feature = "boot-actuators")] - if finished && why == RECOVERING && reset_moves::take(crate::actuator::usb_reset_moves()) { - self.hold_for_the_move(dev, reset_moves::HELD); - } let after = self.read_portsc(port_idx); if finished { self.write_portsc(port_idx, port::enumeration_ack(Some(kind), after)); @@ -937,26 +771,9 @@ impl XhciController { after.link_state(), after.speed(), ); - #[cfg(feature = "boot-actuators")] - if left == AfterReset::Enumerate - && why == RECOVERING - && reset_moves::take(crate::actuator::usb_reset_moves_after()) - { - self.hold_for_the_move(dev, reset_moves::HELD_AFTER); - } left } - /// Say `held`, cue the host, and hold the rung until the device's port - /// reads empty (`reset_moves`). - #[cfg(feature = "boot-actuators")] - fn hold_for_the_move(&self, dev: &MscDevice, held: &str) { - let port_idx = dev.port_idx; - log!("xHCI: {} port {} {held}", self.slot(dev.slot_id), u32::from(port_idx) + 1); - reset_moves::cue(); - let _ = self.settles_within_call(|| !self.read_portsc(port_idx).connected()); - } - /// The ladder's second rung: the port reset, and the enumeration a reset /// owes (xHCI 1.2 §4.19.5), on the slot, the pool block and the disk number /// the device already has — so a mount on it carries on. The steps and @@ -1010,10 +827,6 @@ impl XhciController { return Err(Unverified::Failed); } } - #[cfg(feature = "boot-actuators")] - if reset_moves::take(crate::actuator::usb_reset_moves_configured()) { - self.hold_for_the_move(dev, reset_moves::HELD_CONFIGURED); - } match self.bot(dev, &Cdb::TEST_UNIT_READY, None) { Ok(answer) => { log!("usb-storage: {slot} the port reset took: addressed and configured again, the \ @@ -1105,16 +918,11 @@ impl XhciController { #[cfg(feature = "boot-actuators")] if write { transport_break::arm(); - mid_write::wedge_if_staged(Phase::DataOwed); } completed(self.bulk(dev, pipe == Pipe::In, data_phys, data_len, Phase::Data, &open)) } bot::Act::Status => { open.at(Phase::StatusOwed, &dev.in_ring, &dev.out_ring); - #[cfg(feature = "boot-actuators")] - if data_len > 0 && write { - mid_write::wedge_if_staged(Phase::StatusOwed); - } super::super::zero_dma(dma, dev.block + MSC_CSW, CSW_LEN); let csw_phys = dma.device_addr() + (dev.block + MSC_CSW) as u64; completed(self.bulk(dev, true, csw_phys, CSW_LEN as u32, Phase::Status, &open)) @@ -1173,10 +981,6 @@ impl XhciController { // Before the doorbell, so no transfer is visible to the controller // without a reset being able to see the ring it went on. open.at(phase, &dev.in_ring, &dev.out_ring); - #[cfg(feature = "boot-actuators")] - if phase == Phase::Data && !in_dir { - mid_write::wedge_if_staged(Phase::Data); - } self.bulk_began = crate::clock::nanos_since_boot(); self.ring_doorbell(slot, dci); #[cfg(feature = "boot-actuators")] @@ -1562,13 +1366,6 @@ pub(in crate::drivers::xhci) fn bind( enumerated: Enumerated, described: (UsbId, u8), ) -> Bind { - #[cfg(feature = "boot-actuators")] - if bind_spends_the_scan::take() { - log!("usb-storage: slot {slot_id} {}", bind_spends_the_scan::WHY); - let _ = crate::clock::settles(bind_spends_the_scan::SPEND, || false); - bind_spends_the_scan::hold_answers(); - return Bind::Refused(SlotGoes::Back); - } let MscRings { at, block, in_ring, out_ring, port_idx } = rings; let (usb, serial_index) = described; let mut dev = MscDevice { diff --git a/kernel/src/hasher.rs b/kernel/src/hasher.rs index e47153c9a74..69927a4af33 100644 --- a/kernel/src/hasher.rs +++ b/kernel/src/hasher.rs @@ -118,13 +118,6 @@ impl Hasher for KernelHasher { /// kernel does not have. pub type HashMap = hashbrown::HashMap; -/// Build a container before [`seed`]: the panic is the point. -#[cfg(feature = "boot-actuators")] -pub fn probe_before_seed() { - let mut probe: HashMap = HashMap::default(); - probe.insert(0, 0); -} - /// **What the feature drop does not close**, as code so that closing either /// stops this compiling: a foreign `BuildHasher`, and `hashbrown::HashTable`, /// which needs none. Which hasher a container gets is held by review. diff --git a/kernel/src/heartbeat.rs b/kernel/src/heartbeat.rs deleted file mode 100644 index d20758ad757..00000000000 --- a/kernel/src/heartbeat.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Emits `heartbeat: t=... alive=N/M mask=... ran=... gap=...` every quarter -//! second from the idle loop, with `i8042::report_line` printed beside it. -//! -//! `alive=`/`mask=`: this CPU reached a scheduler pass since the last line. -//! `ran=`: tasks dispatched machine-wide since the last line. -//! `gap=`: time since the previous line. -//! -//! Diagnostic only; the shipping kernel does not carry this module. - -use core::sync::atomic::{AtomicU64, Ordering}; - -use crate::arch::percpu; -use crate::sched::MAX_CPUS; - -const PERIOD_NS: u64 = 250_000_000; - -/// 0 until the first `poll`, which starts the clock without emitting a line. -static LAST_AT: AtomicU64 = AtomicU64::new(0); - -/// Per-CPU last scheduler-pass timestamp; never reset. -static TICKED: [AtomicU64; MAX_CPUS] = [const { AtomicU64::new(0) }; MAX_CPUS]; - -/// Per-CPU dispatch count; monotonic, never reset. -static DISPATCHED: [AtomicU64; MAX_CPUS] = [const { AtomicU64::new(0) }; MAX_CPUS]; - -/// Dispatch total at the previous line, making `ran=` a delta. -static LAST_DISPATCHED: AtomicU64 = AtomicU64::new(0); - -/// Records that this CPU reached a scheduler pass. -pub fn note_pass() { - if !crate::actuator::heartbeat() { - return; - } - let cpu = percpu::cpu_id() as usize; - if cpu < MAX_CPUS { - // Not in the timer ISR: an interrupt taken by a CPU that never reaches a pass must not read as healthy. - TICKED[cpu].store(crate::clock::nanos_since_boot().max(1), Ordering::Relaxed); - } -} - -/// Records that a task is being switched onto this CPU (not the idle context). -pub fn note_dispatch() { - if !crate::actuator::heartbeat() { - return; - } - let cpu = percpu::cpu_id() as usize; - if cpu < MAX_CPUS { - // Load+store, not fetch_add: only this CPU ever writes this slot. - let n = DISPATCHED[cpu].load(Ordering::Relaxed); - DISPATCHED[cpu].store(n + 1, Ordering::Relaxed); - } -} - -/// Emits a heartbeat line if one is due; called from the idle loop. -pub fn poll() { - if !crate::actuator::heartbeat() { - return; - } - let now = crate::clock::nanos_since_boot(); - let last = LAST_AT.load(Ordering::Relaxed); - if last != 0 && now.saturating_sub(last) < PERIOD_NS { - return; - } - // CAS, not store: dedupes the several CPUs that reach this in the same tick. - if LAST_AT - .compare_exchange(last, now, Ordering::AcqRel, Ordering::Relaxed) - .is_err() - { - return; - } - let cpus = (crate::smp::cpu_count() as usize).min(MAX_CPUS); - let dispatched: u64 = (0..cpus).map(|c| DISPATCHED[c].load(Ordering::Relaxed)).sum(); - // Saturating, not `-`: a diagnostic must not be the thing that panics. - let ran = dispatched.saturating_sub(LAST_DISPATCHED.swap(dispatched, Ordering::Relaxed)); - // Still returns after the bookkeeping above: the first call only starts the clock. - if last == 0 { - return; - } - - // Stamps are sampled once and reused below, so the summary and the per-CPU lines agree. - let mut stamps = [0u64; MAX_CPUS]; - let mut mask = 0u64; - let mut alive = 0u32; - for cpu in 0..cpus { - stamps[cpu] = TICKED[cpu].load(Ordering::Relaxed); - // `last` is nonzero here, so this alone excludes a CPU that never reached a pass. - if stamps[cpu] >= last { - mask |= 1 << cpu; - alive += 1; - } - } - let (gs, gms) = split(now - last); - let (ts, tms) = split(now); - log!( - "heartbeat: t={ts}.{tms:03}s alive={alive}/{cpus} mask={mask:#04x} ran={ran} \ - gap={gs}.{gms:03}s" - ); - - // Must not block: report_line uses try_lock and prints `rte=busy` rather than waiting. - crate::arch::keyboard_controller::report_line(); - - for (cpu, &stamp) in stamps.iter().enumerate().take(cpus) { - if mask & (1 << cpu) != 0 { - continue; - } - match stamp { - 0 => log!("heartbeat: cpu{cpu} has never reached a scheduler pass"), - stamp => { - let (s, ms) = split(now.saturating_sub(stamp)); - log!("heartbeat: cpu{cpu} last reached one {s}.{ms:03}s ago"); - } - } - } -} - -/// Splits into whole seconds and milliseconds, matching the log line's timestamp format. -fn split(nanos: u64) -> (u64, u64) { - (nanos / 1_000_000_000, (nanos % 1_000_000_000) / 1_000_000) -} diff --git a/kernel/src/hw.rs b/kernel/src/hw.rs index 041eaeb2fd4..0490865005b 100644 --- a/kernel/src/hw.rs +++ b/kernel/src/hw.rs @@ -98,7 +98,7 @@ impl Machine for KernelHw { } } -/// Longest sleep on a `diag-tick` build; kept under `heartbeat`'s reporting period so a healthy CPU reports on every line. +/// Longest sleep on a `diag-tick` build. #[cfg(feature = "boot-actuators")] const DIAG_TICK_NS: u64 = 100_000_000; diff --git a/kernel/src/inbox/mod.rs b/kernel/src/inbox/mod.rs index 8e6ed014fd1..d2453243fa9 100644 --- a/kernel/src/inbox/mod.rs +++ b/kernel/src/inbox/mod.rs @@ -314,10 +314,6 @@ impl Inbox { /// torn down takes nothing and wakes nobody. fn complete(&self, user_data: u64, result: i32) { let posted = self.completions.with(|c| { - // Inside the section whatever lock it is, so `handler-post` reds - // on one that leaves interrupts open. - #[cfg(feature = "boot-actuators")] - crate::watch::handler_post::raise_if_staged(); c.as_mut().map(|c| c.post_completion(user_data, result, 0)) }); if posted.is_some() { @@ -405,53 +401,6 @@ pub fn create(depth: u32) -> Result<(InboxRef, u64), SyscallError> { Ok((InboxRef(inbox), shm_vaddr)) } -/// `handler-post`'s ring: the kernel's own, mapped into no process and -/// submitted to by nobody, which polls a watch and completes as a submission -/// does. -#[cfg(feature = "boot-actuators")] -pub(crate) struct Staged(Arc); - -#[cfg(feature = "boot-actuators")] -impl Staged { - pub(crate) fn new() -> Self { - let depth = 2 * toyos_sched::watch::handler_post::HOLDS; - let shm = SharedMemObject::create(crate::mm::PAGE_2M).expect("handler-post: a ring's page"); - let page = shm.phys_before_mapping(); - write_ring_page(page, depth, depth * 2); - Self(Arc::new(Inbox { - state: Lock::new(None), - completions: IrqLock::new(Some(Completions { - shm, - page, - completion_size: depth * 2, - completion_tail: 0, - })), - watch: IrqWatch::new(), - })) - } - - /// A poll of this ring on `watch`, which that watch's next post completes. - pub(crate) fn poll(&self, watch: &IrqWatch) { - let poll = Arc::new(Poll { - inbox: self.0.clone(), - user_data: 0, - handle: RawHandle(0), - state: Once::new(), - }); - watch.add_poll(PollEntry { poll, direction: Readiness { readable: true, writable: false } }); - } - - pub(crate) fn complete(&self) { - self.0.complete(0, 0); - } - - /// Run `f` holding this ring's own watch's list lock, as a registration - /// in `submit` holds it. - pub(crate) fn holding_its_watch(&self, f: impl FnOnce()) { - self.0.watch.holding(f); - } -} - /// Processes submissions and waits for completions; called from the syscall handler. pub fn submit( inbox: &Arc, diff --git a/kernel/src/loader/mod.rs b/kernel/src/loader/mod.rs index f29c16ca715..6cc56bf9fa8 100644 --- a/kernel/src/loader/mod.rs +++ b/kernel/src/loader/mod.rs @@ -18,7 +18,6 @@ pub use start::{build_child_handles, PendingHandles, SLOT_PAIR_LEN}; pub(crate) use start::alloc_kernel_stack; pub(crate) use crate::arch::entry::{kernel_start, process_start, thread_start}; pub use tls::{TlsBlock, DTV_INITIAL_CAPACITY, VARIANT as TLS_VARIANT}; -pub(crate) use tls::rebase_window; use alloc::string::String; use alloc::sync::Arc; diff --git a/kernel/src/loader/tls.rs b/kernel/src/loader/tls.rs index 75fb1a07cf6..a4102834b39 100644 --- a/kernel/src/loader/tls.rs +++ b/kernel/src/loader/tls.rs @@ -50,9 +50,6 @@ impl TlsBlock { pub fn publish(self, pt: &PageTables) -> Option<(MappedPages, u64, usize)> { let tp_offset = self.tp_offset; let pages = self.frames.publish(pt, crate::mm::policy::Prot::ReadWrite, |frames, at| { - if crate::actuator::tls_rebase_window() { - rebase_window::hold(frames, at); - } // SAFETY: `frames` is the block `build_combined` wrote, reachable // by no mapping until `publish` maps it after this returns. unsafe { rebase(frames, tp_offset, at) } @@ -162,66 +159,6 @@ unsafe fn rebase(frames: &Unpublished, tp_offset: usize, at: UserAddr) { } } -/// `tls-rebase-window`: a sibling's store staged between a block being given -/// an address and its pointers being rebased to it. Only a spawn whose -/// argument is [`MARK`](rebase_window::MARK) is watched, so the test program -/// chooses the spawns it races. -pub(crate) mod rebase_window { - use core::sync::atomic::{AtomicU64, Ordering}; - - use crate::process::Unpublished; - use crate::time::Duration; - use crate::UserAddr; - - /// The thread argument that asks for a watched spawn. - const MARK: u64 = 0x5eed_c0de_71b0_0001; - /// How long a reachable block waits for a sibling's store before it says - /// the test staged nothing. - const BOUND: Duration = Duration::from_secs(10); - - /// The pid a watched spawn is in flight for, plus one; zero while none is. - static WATCHED: AtomicU64 = AtomicU64::new(0); - - /// `spawn_thread` is about to publish a block for a thread given `arg`. - pub(crate) fn spawning(arg: u64) { - if arg == MARK { - WATCHED.store(crate::process::current_process().0 as u64 + 1, Ordering::SeqCst); - } - } - - pub(super) fn hold(frames: &Unpublished, at: UserAddr) { - // `None` while the kernel spawns init, with no thread running. - let Some(pid) = crate::arch::percpu::current_pid() else { return }; - let pid = pid.0 as u64 + 1; - if WATCHED.compare_exchange(pid, 0, Ordering::SeqCst, Ordering::SeqCst).is_err() { - return; - } - // SAFETY: DTV slot 0 is inside the DTV `build_combined` wrote at the front of `frames`. - let slot = unsafe { frames.ptr().add(super::DTV_HEADER_SIZE) }.cast::(); - // SAFETY: as above; a volatile read, since a user store may land there. - let written = unsafe { slot.read_volatile() }; - // `spawn_thread` publishes into the address space this CPU runs. - if !crate::mm::paging::present_in_current_tables(at.raw()) { - log!("tls-rebase-window: pid {} block at {:#x} is not reachable before its rebase", pid - 1, at.raw()); - return; - } - let deadline = crate::clock::now() + BOUND; - // SAFETY: as above. - while unsafe { slot.read_volatile() } == written { - assert!( - crate::clock::now() < deadline, - "tls-rebase-window: pid {} block at {:#x} was reachable before its rebase and nothing stored into it in {BOUND}", - pid - 1, - at.raw() - ); - // `IF` is clear in a syscall: a sibling's shootdown is answered here. - crate::arch::tlb::poll(); - core::hint::spin_loop(); - } - log!("tls-rebase-window: pid {} block at {:#x} was reachable before its rebase, and a store landed in it", pid - 1, at.raw()); - } -} - /// One combined block for every startup module; `None` when they do not fit, since a missing module would mean relocations resolving against a block that is not there. /// The executable's module goes where its linker resolved its own accesses: next to the thread /// pointer, last in variant II and first in variant I. diff --git a/kernel/src/log/console.rs b/kernel/src/log/console.rs index 0f765497241..c40699f20c7 100644 --- a/kernel/src/log/console.rs +++ b/kernel/src/log/console.rs @@ -43,7 +43,7 @@ pub enum Drain { /// Nothing else runs yet: no thread exists before `klogd`'s spawn, and no CPU takes a scheduler pass this early. Inline, /// `klogd`, woken at the commit of the record it will drain. - /// Only a commit or a queued line wakes it — no idle loop, no timer — and `i8042_no_spurious_wake` depends on that. + /// Only a commit or a queued line wakes it — no idle loop, no timer. Thread, } @@ -407,12 +407,6 @@ impl RecordSink for Raw { } extern "C" fn body(_arg: u64) -> ! { - #[cfg(feature = "boot-actuators")] - if crate::actuator::klogd_fault() { - // SAFETY: unsound by design — a staged Ring 0 null read, only on this actuator's boot. - // Volatile: a plain read could be optimized to unreachable, leaving nothing to fault. - unsafe { core::ptr::read_volatile(core::ptr::null::()) }; - } let parkable = scheduler::Parkable::at_entry(); let handle = crate::sched::driver::current_handle().expect("klogd runs as a task"); diff --git a/kernel/src/log/mod.rs b/kernel/src/log/mod.rs index 5a4533d13a8..8899bd3f735 100644 --- a/kernel/src/log/mod.rs +++ b/kernel/src/log/mod.rs @@ -226,27 +226,10 @@ pub fn emit(severity: Severity, args: core::fmt::Arguments) { } } - // Between the drain and the repaint, so the record this call just put on the - // console is the one the panel does not have. - #[cfg(feature = "boot-actuators")] - if HALT_BEFORE_REPAINT.load(Ordering::Relaxed) { - crate::arch::cpu::halt(); - } - // After the commit, so the record this call made is the one on the panel. crate::drivers::panic_console::early_checkpoint(); } -/// Armed by the `test-early-halt` actuator one record ahead of where it wants -/// the boot to stop. -#[cfg(feature = "boot-actuators")] -static HALT_BEFORE_REPAINT: AtomicBool = AtomicBool::new(false); - -#[cfg(feature = "boot-actuators")] -pub fn halt_before_the_next_repaint() { - HALT_BEFORE_REPAINT.store(true, Ordering::Relaxed); -} - /// A line of ordinary kernel log. #[macro_export] macro_rules! log { diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 097c7b0c1de..9369fc7b1a7 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -59,8 +59,6 @@ mod tmpfs; mod file_backing; mod bcachefs_adapter; mod fs_rename; -#[cfg(feature = "boot-actuators")] -mod heartbeat; mod vfs; mod elf; mod symbols; @@ -89,13 +87,9 @@ mod user_ptr; mod vma; mod syscall; -/// Nested generic forces a demangled symbol wider than the console grid, -/// proving `screen_late_panic`'s renderer really wraps. +/// Nested generic forces a demangled symbol wider than the console grid. #[cfg(feature = "boot-actuators")] mod late_panic { - /// The record the panic path writes after `capture()`, for `screen_late_panic`. - pub const AFTER_CAPTURE: &str = "test-late-panic: after the capture"; - pub struct Nest(core::marker::PhantomData); impl Nest { @@ -162,12 +156,6 @@ fn panic(info: &core::panic::PanicInfo) -> ! { arch::trap::report_panic(info, cpu::frame_pointer()); drivers::panic_console::capture(); - // One record after the snapshot and before the paint: what tells a frozen - // report from a live re-read of a ring siblings are still writing to. - #[cfg(feature = "boot-actuators")] - if actuator::test_late_panic() { - log!("{}", late_panic::AFTER_CAPTURE); - } // SAFETY: IF is clear on this CPU and every other one halts before anything else can write the port. unsafe { drivers::serial::panic_flush(); } @@ -288,21 +276,8 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { params::init(cmdline); deadline::claim(cmdline); actuator::init(cmdline); - // The actuator's other half: a loader that ignored it would boot on unrefused. - if actuator::loader_writes_no_layout() { - panic!( - "boot: {} is armed and the loader wrote this kernel's layout anyway", - toyos_abi::boot::WRITE_NO_LAYOUT_PARAM - ); - } let root_image = rootfs::init(cmdline, &kernel_args, maps); - // Armed here so the next record — the architecture's first — reaches the console and the panel keeps the one before it. - #[cfg(feature = "boot-actuators")] - if actuator::test_early_halt() { - log::halt_before_the_next_repaint(); - } - arch::boot::after_console(&kernel_args, maps); // percpu, the allocator and our own paging aren't up yet, so a fault here only reaches the early-panic branch. @@ -408,10 +383,6 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { // The last point before the first hash container (`mm::init`'s address // space), and not earlier: seeding fails only by panicking, and a panic // before the boot's own log lines reaches no channel at all. - #[cfg(feature = "boot-actuators")] - if actuator::test_hash_before_seed() { - hasher::probe_before_seed(); - } hasher::seed(); mm::init(maps, &reserved); @@ -511,10 +482,6 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { let t_storage = clock::nanos_since_boot(); xhci::init(&pci_devices); - #[cfg(feature = "boot-actuators")] - if actuator::usb_storage_gate() { - usb_gate::run(); - } // After xhci::init: a USB-booted disk doesn't exist until the controller binds it. gpt::probe_usb_disks(); rootfs::hold_source(); @@ -527,11 +494,6 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { if actuator::revoked_backing_selftest() { revoke_selftest::run(); } - // After every driver has registered: the number under test is one a real device holds. - #[cfg(feature = "boot-actuators")] - if actuator::block_duplicate_id() { - block::duplicate_id_selftest(); - } boot_phase!("storage ready", t_storage); @@ -541,13 +503,6 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { // boot's answer for a dead keyboard, and a panel shows the log's tail. arch::boot::platform_devices(kernel_args.rsdp_addr); - // Runs once for the machine: it touches no device, so per-driver repetition would say the same thing four times. - #[cfg(feature = "boot-actuators")] - if actuator::virtio_used_selftest() { - drivers::virtio::used_selftest(); - drivers::virtio::wait_selftest(); - } - #[cfg(feature = "boot-actuators")] arch::boot::interrupt_selftests(); @@ -586,12 +541,6 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { input_merge_test::run(); } - // Under Drain::Inline every record above is already on the wire, so this gate reads the whole boot and then silence. - #[cfg(feature = "boot-actuators")] - if actuator::pre_idle_wedge() { - pre_idle_wedge(); - } - report_log_destination(); let complete_tsc = cpu::counter(); boot_phase!("complete", 0); @@ -604,10 +553,6 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { late_panic::Nest>>>>>>>>>::on_screen_console_check(); } - if actuator::test_kernel_fault() { - cpu::undefined_instruction(); - } - // Last thing before enter_idle_loop: nothing can run before it, and a klogd spawned earlier would idle through phases 5-7 with no drainer. log::console::start(); @@ -630,13 +575,3 @@ pub(crate) unsafe extern "C" fn kernel_main(kernel_args: &KernelArgs) -> ! { crate::scheduler::enter_idle_loop(); } -/// Wedges the machine: interrupts off then spin, with no timer, scheduler, or klogd left to drain anything logged after this. -#[cfg(feature = "boot-actuators")] -fn pre_idle_wedge() -> ! { - log!("pre-idle-wedge: the boot stops here, and this line is the last thing this machine says"); - cpu::disable_interrupts(); - loop { - core::hint::spin_loop(); - } -} - diff --git a/kernel/src/object/ops.rs b/kernel/src/object/ops.rs index d92b4f8abda..ac7edc38b85 100644 --- a/kernel/src/object/ops.rs +++ b/kernel/src/object/ops.rs @@ -315,12 +315,9 @@ pub fn write_watch(object: &KObjectRef) -> Option { fn close_ends_polls(object: &KObjectRef) -> bool { match object { KObjectRef::SysCap(_) => false, - // A keyboard *claim* closing is the stimulus, not a `SysCap`. - KObjectRef::Console(_) => crate::actuator::keyboard_close_cancels_every_console(), + KObjectRef::Console(_) => false, KObjectRef::Device(d) => match d.class() { - device_registry::DeviceType::Keyboard => { - crate::actuator::keyboard_close_cancels_every_console() - } + device_registry::DeviceType::Keyboard => false, device_registry::DeviceType::Mouse | device_registry::DeviceType::PciFunction | device_registry::DeviceType::HdaAudio diff --git a/kernel/src/panic.rs b/kernel/src/panic.rs index 55b778d63d6..dcd72e0cd3e 100644 --- a/kernel/src/panic.rs +++ b/kernel/src/panic.rs @@ -357,9 +357,7 @@ pub fn halt_all_cpus() -> ! { crate::arch::irqchip::stop_other_cpus(); let bound = crate::panic_reboot::arm(true); // Folded into the still-unpainted capture only where the panel is this - // boot's only account of itself: a refresh re-freezes the ring, and - // `screen_late_panic` reads the panel for a record written *after* - // `capture()` to prove the paint comes from the frozen snapshot. A machine + // boot's only account of itself: a refresh re-freezes the ring. A machine // with a console gets the arm line on it. if !serial::has_console() { crate::drivers::panic_console::refresh_capture(); diff --git a/kernel/src/panic_reboot.rs b/kernel/src/panic_reboot.rs index b36141ae16f..4b9cbe9eda4 100644 --- a/kernel/src/panic_reboot.rs +++ b/kernel/src/panic_reboot.rs @@ -27,15 +27,6 @@ const PANIC_BOUND: Budget = Budget::of( "the machine returns itself to firmware instead of holding a panel nobody is reading", ); -/// `tco-fast`'s counterpart for this bound: a judge cannot spend the shipped -/// minute per boot, and its control cannot press a key inside a bound shorter -/// than the round trip that presses it. -#[cfg(feature = "boot-actuators")] -const FAST_BOUND: Budget = Budget::of( - Duration::from_secs(5), - "a guest reaches the reset inside one test, and a control still beats it to the keyboard", -); - /// Whether a reboot is armed on this panic, and when. #[derive(Clone, Copy)] pub enum Bound { @@ -107,9 +98,6 @@ fn deadline(bound: Budget) -> Option<(u64, Source)> { /// and on the console; false is for the reentry guard, whose suspect is the log /// path itself, and there the line goes to the UART raw and the panel carries none. pub fn arm(on_the_record: bool) -> Bound { - #[cfg(feature = "boot-actuators")] - let budget = if crate::actuator::panic_reboot_fast() { FAST_BOUND } else { PANIC_BOUND }; - #[cfg(not(feature = "boot-actuators"))] let budget = PANIC_BOUND; // ASCII only, here and in every line below: the panel's font renders diff --git a/kernel/src/pcidev/mod.rs b/kernel/src/pcidev/mod.rs index 35a4e3afaec..d87b5cd294d 100644 --- a/kernel/src/pcidev/mod.rs +++ b/kernel/src/pcidev/mod.rs @@ -761,12 +761,6 @@ fn bring_up(pci: PciDevice, id: PciId, slot: usize) -> Result { // leave the machine changed by a hand-over that did not happen. let Space { space, lend } = slot_space(slot).map_err(Refusal::Untranslated)?; - // Held across every walk this hand-over makes of the function's own list — - // both readers below and the MSI fallback between them — so the staged - // shape is the device's and not one reader's view of it. - #[cfg(feature = "boot-actuators")] - let _staged = crate::drivers::pci::StagedCaps::armed_for(&pci); - // The table's own BAR, so it can be left where it is and kept out of what // the holder maps. let table_bar = msix_bar(&pci); @@ -936,11 +930,7 @@ impl Kept { /// is half-written. [`bring_up`] turns decode on again. fn restore(&self, pci: &PciDevice) { pci.set_memory_decode(false); - let lost = crate::actuator::pcidev_bar_lost_on_reset().then(|| msix_bar(pci)); for (index, bar) in self.bars.iter().enumerate().take(self.slots as usize) { - if lost.is_some_and(|table| table != Some(index as u8)) { - continue; - } pci.write_config_u32(bar::BASE + index as u64 * 4, *bar); } if let (Some((control, second)), Ok(cap)) = (self.control, pci.capability(express::CAP_ID)) { @@ -987,8 +977,6 @@ enum Declined { /// The function is not in D0, so a round trip from it is not one this /// kernel knows the timing of. NotInD0, - /// `pcidev-reset-nothing` declined it without asking the function. - Staged, } impl core::fmt::Display for Declined { @@ -999,7 +987,6 @@ impl core::fmt::Display for Declined { Self::NoFlr => "no function level reset advertised", Self::NoSoftReset => "No_Soft_Reset set", Self::NotInD0 => "not in D0", - Self::Staged => "declined unasked, as staged", }) } } @@ -1046,10 +1033,6 @@ impl core::fmt::Display for How { /// in its place, then the D3hot round trip, which resets any function that /// does not say `No_Soft_Reset`. fn reset(pci: &PciDevice) -> (How, Option) { - if crate::actuator::pcidev_reset_nothing() { - let staged = Declined::Staged; - return (How::Nothing { express: staged, af: staged, pm: staged }, None); - } let now = crate::clock::nanos_since_boot(); let express = match pci.capability(express::CAP_ID) { Ok(cap) if express::resets(cap.read_u32(express::DEVICE_CAPABILITIES)) => { @@ -1109,30 +1092,6 @@ fn settle_after_reset(pci: &PciDevice) { wait_until(crate::clock::nanos_since_boot() + pm::TRANSITION_NANOS); } kept.restore(pci); - if crate::actuator::pcidev_bar_moved_on_reset() && matches!(resetting, Resetting::Flr { .. }) { - move_inside_its_window(pci); - } -} - -/// [`crate::actuator::pcidev_bar_moved_on_reset`]: BAR 0 one BAR's size above -/// the window it was cut, so the register decodes an address that is not the -/// cut and that nothing else decodes. -fn move_inside_its_window(pci: &PciDevice) { - let (at, span) = { - let who = requester(pci); - let machine = MACHINE.lock(); - let &(_, _, at, span) = machine - .windows - .iter() - .find(|(w, i, _, _)| *w == who && *i == 0) - .expect("pcidev-bar-moved-on-reset: BAR 0 of a reset function was never cut"); - (at, span) - }; - let size = pci.bar_size(0).expect("pcidev-bar-moved-on-reset: BAR 0 does not size"); - assert!(size < span, "pcidev-bar-moved-on-reset: BAR 0 fills its window, so no address inside it is not the cut"); - let low = pci.read_config_u32(bar::BASE); - assert_eq!(u64::from(low & !0xf), at & 0xffff_ffff, "pcidev-bar-moved-on-reset: BAR 0 was not restored to its cut"); - pci.write_config_u32(bar::BASE, low + size as u32); } fn wait_until(at: u64) { @@ -1570,13 +1529,12 @@ pub fn dma_alloc( Err(why) => refused(why), }, }; - let first = bound.grants.is_empty(); let origin = Origin::Allocated { residual }; bound.grants.push(Grant { memory: Arc::clone(&memory), at, bytes: span, origin }); // After the mapping and never before: the first thing this function may // reach has to exist before it may reach anything. bound.start_mastering(); - Ok((memory, foreign_if_armed(first, &bound.pci, at), span)) + Ok((memory, at, span)) }) } @@ -1672,28 +1630,6 @@ pub fn dma_unmap(slot: usize, at: u64) -> Result<(), SyscallError> { Ok(()) } -/// The address a grant answers with, or — for a network function's first -/// grant, with the actuator armed — another driver's pool. -/// -/// The grant is real and mapped; only the address the driver is *told* is one -/// this function's domain does not have, so what the device is pointed at is a -/// wrong descriptor rather than a driver written to misbehave. -fn foreign_if_armed(first: bool, pci: &PciDevice, at: u64) -> u64 { - // A network function's alone: the staging is netd's, and any other claim - // it met would fail beside it and give its slot up. - #[cfg(feature = "boot-actuators")] - if first && crate::actuator::iommu_userdev_foreign_dma() && pci.matches_class(0x02, 0x00, None) { - let foreign = - crate::drivers::xhci::FOREIGN_PROBE.load(core::sync::atomic::Ordering::Relaxed); - if foreign != 0 { - return foreign; - } - } - #[cfg(not(feature = "boot-actuators"))] - let _ = (first, pci); - at -} - /// The window a claim's configuration reads are checked against, for the one /// caller that turns an offset from userland into a [`Register`]. pub fn config_window(offset: u64, width: RegWidth) -> Result { diff --git a/kernel/src/process.rs b/kernel/src/process.rs index 2b5c8d0a2b3..a3433726025 100644 --- a/kernel/src/process.rs +++ b/kernel/src/process.rs @@ -847,9 +847,6 @@ pub fn spawn_thread(entry: u64, stack_ptr: u64, arg: u64, stack_base: u64) -> Op let block = TlsBlock::build(&tls_modules, tls)?; let (tls_alloc, thread_pointer, tp_offset) = { let parent_data = process_data_arc.lock(); - if crate::actuator::tls_rebase_window() { - crate::loader::rebase_window::spawning(arg); - } // VA exhaustion is a resource failure the process caused, not a kernel bug; the block drops on the way out, returning its pages. let published = block.publish(&parent_addr_space)?; drop(parent_data); diff --git a/kernel/src/quiesce.rs b/kernel/src/quiesce.rs index eed377e563e..3d89bb8b229 100644 --- a/kernel/src/quiesce.rs +++ b/kernel/src/quiesce.rs @@ -187,8 +187,6 @@ pub fn stop() -> Record { /// on another CPU finishing its own teardown takes this same lock. fn sweep(caller: ThreadId) -> Sweep { let mut out = Sweep::default(); - #[cfg(feature = "boot-actuators")] - let mut held_running = false; let guard = process::PROCESS_TABLE.lock(); let Some(table) = guard.as_ref() else { return out }; for (_, proc) in table.iter() { @@ -213,133 +211,8 @@ fn sweep(caller: ThreadId) -> Sweep { out.stopped += 1; } else { out.running += 1; - #[cfg(feature = "boot-actuators")] - { - held_running |= last::is_held(ThreadId { pid: pid.raw(), tid: tid.raw() }); - } } } } - #[cfg(feature = "boot-actuators")] - last::note_sweep(out, held_running); out } - -/// `quiesce-last-park`: one thread, named [`toyos_quiesce::LAST_THREAD`], held -/// inside its `SYS_NANOSLEEP` until the stop's latest sweep counts it as the -/// one thread still running, so the park it makes next is the last transition -/// the stop sees. -#[cfg(feature = "boot-actuators")] -pub mod last { - use core::sync::atomic::{ - AtomicBool, AtomicU64, Ordering::AcqRel, Ordering::Acquire, Ordering::Release, - }; - - use toyos_quiesce::{Sweep, ThreadId}; - use toyos_sched::task::WaitClass; - - use crate::watch::{self, Watch}; - use crate::time::{Budget, Deadline, Duration}; - - /// How long either side waits for the other before the boot dies by name: - /// the thread is held before init takes the stop request, so its wait is - /// init's file call, flush and sync, then the stop's own sweeps. - const STAGED: Budget = Budget::of( - Duration::from_nanos( - (toyos_quiesce::FILES_MS + toyos_quiesce::FLUSH_MS + toyos_quiesce::SYNC_MS) * 1_000_000 - + super::PARK.nanos(), - ), - "the boot panics naming the side of the staging that never arrived", - ); - - /// No thread claimed yet: `percpu`'s spelling of idle, which no thread has. - const NOBODY: u64 = u64::MAX; - - /// The one thread this boot holds, `pid` high and `tid` low. - static HELD: AtomicU64 = AtomicU64::new(NOBODY); - /// Whether the latest sweep counted one thread running, and that one [`HELD`]. - static ALONE: AtomicBool = AtomicBool::new(false); - /// What the shutdown's caller parks on until a thread is held. - static ARRIVED: Watch = Watch::new(); - - fn word(thread: ThreadId) -> u64 { - (u64::from(thread.pid) << 32) | u64::from(thread.tid) - } - - pub(super) fn is_held(thread: ThreadId) -> bool { - HELD.load(Acquire) == word(thread) - } - - pub(super) fn note_sweep(swept: Sweep, held_running: bool) { - ALONE.store(swept.running == 1 && held_running, Release); - } - - /// Hold the running thread here if it is the one this boot stages. - pub fn hold() { - if !crate::actuator::quiesce_last_park() { - return; - } - let Some(thread) = the_named_thread() else { return }; - if HELD.compare_exchange(NOBODY, word(thread), AcqRel, Acquire).is_err() { - return; - } - crate::log!( - "quiesce-last-park: {} is held until the stop waits on it alone", - toyos_quiesce::LAST_THREAD, - ); - ARRIVED.post(); - let deadline = Deadline::at(crate::clock::now() + STAGED.duration()); - // Yields and never parks: a park is the transition this hold exists to - // place, and a sweep would stop this thread at the first one. - while !ALONE.load(Acquire) { - assert!( - !deadline.reached(crate::clock::now()), - "quiesce-last-park: the stop never came down to this thread alone in {} ms", - STAGED.nanos() / 1_000_000, - ); - crate::scheduler::yield_now(); - } - crate::log!("quiesce-last-park: the stop counts {} alone", toyos_quiesce::LAST_THREAD); - } - - /// Called by the shutdown before it stops anything: the stop is staged - /// only once the thread it is staged around is inside its syscall. - pub fn await_the_held_thread() { - if !crate::actuator::quiesce_last_park() { - return; - } - crate::log!( - "quiesce-last-park: the stop waits for {} to reach its syscall", - toyos_quiesce::LAST_THREAD, - ); - let deadline = Deadline::at(crate::clock::now() + STAGED.duration()); - let parkable = crate::scheduler::Parkable::at_entry(); - let _ = watch::wait_until( - &parkable, - &ARRIVED, - 0, - WaitClass::Other, - deadline, - || HELD.load(Acquire) != NOBODY, - ); - assert!( - HELD.load(Acquire) != NOBODY, - "quiesce-last-park: no thread named {} reached its syscall in {} ms", - toyos_quiesce::LAST_THREAD, - STAGED.nanos() / 1_000_000, - ); - } - - /// The running thread, if it carries [`toyos_quiesce::LAST_THREAD`]'s name. - fn the_named_thread() -> Option { - let pid = crate::arch::percpu::current_pid()?; - let tid = crate::arch::percpu::current_tid()?; - let guard = crate::process::PROCESS_TABLE.lock(); - guard - .as_ref() - .and_then(|table| table.get(pid)) - .and_then(|proc| proc.threads().get(tid)) - .is_some_and(|thread| thread.name_str() == toyos_quiesce::LAST_THREAD) - .then_some(ThreadId { pid: pid.raw(), tid: tid.raw() }) - } -} diff --git a/kernel/src/rootfs.rs b/kernel/src/rootfs.rs index 72c89824451..eb55f8a27ee 100644 --- a/kernel/src/rootfs.rs +++ b/kernel/src/rootfs.rs @@ -140,14 +140,6 @@ pub fn mount() -> Mounted { ), Handed::Image(image) => image, }; - // The actuator's other half: a loader that ignored it would leave the - // refusal above untested while the test reading it passed. - if crate::actuator::loader_withholds_root() { - panic!( - "boot: {} is armed and the loader handed a ROOT image anyway", - toyos_abi::boot::WITHHOLD_ROOT_PARAM - ); - } let fs = Mounted::<_, ReadOnly>::open(image) .unwrap_or_else(|e| panic!("boot: the ROOT image holds no filesystem this kernel can mount: {e:?}")); if fs.uuid() != named { diff --git a/kernel/src/sched/driver.rs b/kernel/src/sched/driver.rs index 9b58f8a7f50..f6d0ad5f94b 100644 --- a/kernel/src/sched/driver.rs +++ b/kernel/src/sched/driver.rs @@ -658,10 +658,6 @@ fn execute(action: Action) { /// Consume this CPU's `irq_ring` records into wakes, before the mailbox drain, so a wake posted here reaches this pass's pick. fn drain_irqs(entered: super::dump::Entered) { - // First in the function, so the stamp means "this CPU reached a - // pass" and not "this CPU got all the way through one". - #[cfg(feature = "boot-actuators")] - crate::heartbeat::note_pass(); crate::drivers::xhci::poll_if_pending(); crate::arch::keyboard_controller::service(); // Here, not at the keystroke: the keystroke's decoding driver's guard is done by this point. @@ -705,18 +701,11 @@ extern "C" fn idle_loop() -> ! { if crate::drivers::panic_console::probe_due() { panic!("metal-panic-probe: a fatal report over a desktop that owns the screen"); } - #[cfg(feature = "boot-actuators")] - if crate::actuator::handler_post() { - crate::watch::handler_post::run(); - } crate::scheduler::log_health(); crate::scheduler::reap_finished(); // `pass` below covers this too; here as well so a CPU that // halts immediately has still run every hook first. crate::object::drain_zero_handles(); - // A heartbeat is a record like any other; the idle loop touches no filesystem itself. - #[cfg(feature = "boot-actuators")] - crate::heartbeat::poll(); pass(Dispose::None); } } diff --git a/kernel/src/sched/dump.rs b/kernel/src/sched/dump.rs index a8ff114a85f..512cb3ae28e 100644 --- a/kernel/src/sched/dump.rs +++ b/kernel/src/sched/dump.rs @@ -160,10 +160,6 @@ pub fn file_request() { /// Ctrl+Alt+D's request, from `drain_irqs` on every pass. pub fn serve_request(entered: Entered) { - #[cfg(feature = "boot-actuators")] - if crate::actuator::dump_in_blocking_pass() { - staged::at_the_load(entered); - } match entered.under_nothing() { Some(proof) => serve(&proof), None => leave_request(entered), @@ -220,10 +216,6 @@ fn report(_proof: &UnderNothing) { // Two instants, not byte positions: there is no single stream across CPUs. let from = crate::clock::nanos_since_boot(); log!("=== blocked-task dump: {cpus} cpu(s), and this report takes the screen ==="); - #[cfg(feature = "boot-actuators")] - if crate::actuator::dump_in_blocking_pass() { - staged::in_the_report(_proof); - } // Indexed by cpu id: `OWES` is `MAX_CPUS` long regardless of `cpus`. #[allow(clippy::needless_range_loop)] @@ -392,144 +384,6 @@ pub(super) fn deaf_window() { serve(&UnderNothing(())); } -/// `dump-in-blocking-pass`: on one CPU, files a request inside each kind of pass that may not serve it and -/// inside a report, one at a time, and says what each request met. -#[cfg(feature = "boot-actuators")] -pub mod staged { - use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; - - use super::{Entered, UnderNothing, REQUEST}; - use crate::arch::percpu; - - /// This many passes in a row with a task on the CPU: a load that does not idle, so nothing but the pass a - /// leaving pass owes comes to serve what it left. - const BUSY_STREAK: u32 = 32; - - const KERNEL_BLOCKING: usize = 0; - const USER_BLOCKING: usize = 1; - const USER_ABOVE_ZERO: usize = 2; - const DURING_A_REPORT: usize = 3; - const NOTHING: u32 = u32::MAX; - /// [`Entered::Blocking`] in [`ENTERED`]; a depth is itself. - const BLOCKING: u32 = u32::MAX; - - static ARMED: AtomicBool = AtomicBool::new(false); - static REFUSED: AtomicBool = AtomicBool::new(false); - static BUSY: AtomicU32 = AtomicU32::new(0); - static FILED: [AtomicBool; 4] = [const { AtomicBool::new(false) }; 4]; - /// The kind whose request is unaccounted: the gate that makes the stages one at a time. - static STAGED: AtomicU32 = AtomicU32::new(NOTHING); - /// How the pass now deciding was entered, for the report it may run. - static ENTERED: AtomicU32 = AtomicU32::new(NOTHING); - static ACCOUNT_DUE: AtomicBool = AtomicBool::new(false); - static RETURNS: AtomicU32 = AtomicU32::new(0); - - /// `serve_request`'s first statement, in every pass. - pub(super) fn at_the_load(entered: Entered) { - // One CPU: a sibling's pass entered at zero would take a request between its filing and its meeting. - let cpus = super::online_cpus(); - if cpus != 1 { - if !REFUSED.swap(true, Ordering::AcqRel) { - log!("dump-in-blocking-pass: staged on one cpu only, and this machine has {cpus}"); - } - return; - } - account_if_taken(); - ENTERED.store( - match entered { - Entered::Blocking => BLOCKING, - Entered::Pass { depth } => depth, - }, - Ordering::Release, - ); - let busy = match percpu::current_tid() { - Some(_) => BUSY.fetch_add(1, Ordering::AcqRel) + 1 >= BUSY_STREAK, - None => { - BUSY.store(0, Ordering::Release); - false - } - }; - if !ARMED.load(Ordering::Acquire) { - // The release: every CPU has joined, so the count above is the machine's. - if crate::smp::is_ready() && !ARMED.swap(true, Ordering::AcqRel) { - log!("dump-in-blocking-pass: armed"); - } - return; - } - let kernel = crate::sched::kthread::current_is_kernel_thread(); - let kind = match entered { - Entered::Blocking if kernel => KERNEL_BLOCKING, - Entered::Blocking if busy => USER_BLOCKING, - Entered::Pass { depth: 1.. } if busy && !kernel => USER_ABOVE_ZERO, - _ => return, - }; - if FILED[kind].load(Ordering::Acquire) - || REQUEST.pending() - || STAGED - .compare_exchange(NOTHING, kind as u32, Ordering::AcqRel, Ordering::Acquire) - .is_err() - { - return; - } - FILED[kind].store(true, Ordering::Release); - RETURNS.store(0, Ordering::Release); - log!( - "dump-in-blocking-pass: cpu{} files a request in {entered} of a {} thread", - percpu::cpu_id(), - if kernel { "kernel" } else { "user" }, - ); - REQUEST.file(); - // Met twice, with the clear a pass makes on entry between the meetings: what a task woken behind - // this pass does when it blocks again before it reaches Ring 3. - super::leave_request(entered); - crate::preempt::clear_need_resched(); - ACCOUNT_DUE.store(true, Ordering::Release); - } - - /// From a report, once its header is out. - pub(super) fn in_the_report(proof: &UnderNothing) { - if STAGED.load(Ordering::Acquire) == NOTHING { - return; - } - let entered = match ENTERED.load(Ordering::Acquire) { - BLOCKING => Entered::Blocking, - depth => Entered::Pass { depth }, - }; - let cpu = percpu::cpu_id(); - log!("dump-in-blocking-pass: cpu{cpu} reports from {entered}"); - if STAGED.load(Ordering::Acquire) == USER_BLOCKING as u32 - && !FILED[DURING_A_REPORT].swap(true, Ordering::AcqRel) - { - log!("dump-in-blocking-pass: cpu{cpu} files a request during a report"); - REQUEST.file(); - // Met as a sibling's pass entered at zero would meet it while this report runs. - super::serve(proof); - } - } - - /// From the exit to user mode, once it has nothing more to run. - pub fn note_return_to_user() { - if STAGED.load(Ordering::Acquire) != NOTHING && REQUEST.pending() { - RETURNS.fetch_add(1, Ordering::AcqRel); - } - } - - fn account_if_taken() { - if !ACCOUNT_DUE.load(Ordering::Acquire) - || REQUEST.pending() - || !ACCOUNT_DUE.swap(false, Ordering::AcqRel) - { - return; - } - log!( - "dump-in-blocking-pass: cpu{} returned to Ring 3 {} time(s) with its request pending", - percpu::cpu_id(), - RETURNS.load(Ordering::Acquire), - ); - STAGED.store(NOTHING, Ordering::Release); - } -} - /// Where this CPU was, for the NMI probe. Called only from `arch/x86_64/idt/nmi.rs`. /// Stores unconditionally: reading the flag first would race the requester that owns it. pub fn note_nmi(rip: u64) { diff --git a/kernel/src/scheduler.rs b/kernel/src/scheduler.rs index e5b6a594393..7e59c4fe273 100644 --- a/kernel/src/scheduler.rs +++ b/kernel/src/scheduler.rs @@ -404,10 +404,6 @@ pub fn exit_to_user() { leave_user_if_due(); // `do_preempt` owns clearing `need_resched`; this function never clears it itself. if !crate::preempt::need_resched() { - #[cfg(feature = "boot-actuators")] - if crate::actuator::dump_in_blocking_pass() { - crate::sched::dump::staged::note_return_to_user(); - } return; } assert!(!in_schedule_self(), "exit-to-user inside a scheduler pass"); diff --git a/kernel/src/syscall/machine.rs b/kernel/src/syscall/machine.rs index b1d2bda81e9..eb309428068 100644 --- a/kernel/src/syscall/machine.rs +++ b/kernel/src/syscall/machine.rs @@ -57,22 +57,6 @@ fn quiesce(last: &str) -> Result<(), SyscallError> { if crate::actuator::wedge_before_reset() { crate::deadline::stage_a_wedge(); } - // The same shape with a device left inside a Bulk-Only command. Here too, - // so the wedge is a boot that ran its job list. - #[cfg(feature = "boot-actuators")] - { - use toyos_xhci::bot::Phase; - let armed = [ - (crate::actuator::usb_wedge_data_owed(), Phase::DataOwed), - (crate::actuator::usb_wedge_in_data(), Phase::Data), - (crate::actuator::usb_wedge_before_status(), Phase::StatusOwed), - ] - .into_iter() - .find_map(|(on, phase)| on.then_some(phase)); - if let Some(phase) = armed { - crate::usb_gate::wedge_inside_a_write(phase); - } - } // The same machine ended by the same bound, with the bus busy rather than // idle: this one never stops writing, so the reset lands on a controller // that is moving bytes. @@ -84,8 +68,6 @@ fn quiesce(last: &str) -> Result<(), SyscallError> { crate::arch::watchdog::disarm(); // Every userland thread stops here, the log's writer with the rest: // `/system/bin/init` had it flush before it asked for this stop. - #[cfg(feature = "boot-actuators")] - crate::quiesce::last::await_the_held_thread(); let stopped = crate::quiesce::stop(); crate::log::console::drain_for_the_stop(); // The final census: no process runs after this to report another. @@ -103,15 +85,6 @@ fn quiesce(last: &str) -> Result<(), SyscallError> { // emptied and waited for before anything is taken down. crate::drivers::xhci::flush_disks(); log!("{last}"); - // Widens the window every shutdown has here, and nothing else: see the - // actuator's own declaration. - #[cfg(feature = "boot-actuators")] - if crate::actuator::quiesce_late_word() { - let until = crate::clock::nanos_since_boot().saturating_add(100_000_000); - while crate::clock::nanos_since_boot() < until { - crate::scheduler::yield_now(); - } - } // Order is load-bearing: the console drain, the seal, then the caller's // non-returning call. crate::log::console::drain_inline(); @@ -138,25 +111,11 @@ fn quiesce(last: &str) -> Result<(), SyscallError> { Ok(()) } -/// `power-refused-once`: whether this is the stop it refuses. -fn refused_once() -> bool { - static REFUSED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false); - let refuse = crate::actuator::power_refused_once() - && !REFUSED.swap(true, core::sync::atomic::Ordering::Relaxed); - if refuse { - log!("power: refusing this stop, as power-refused-once asks"); - } - refuse -} - /// Powers the machine off; requires a `SysCap` carrying [`Rights::POWER`]. Returns only when refused. pub(super) fn sys_shutdown(syscap: RawHandle) -> u64 { if let Err(e) = demand_syscap(syscap, Rights::POWER) { return e.refuse(); } - if refused_once() { - return SyscallError::NotSupported.to_u64(); - } if let Err(e) = quiesce("Shutting down.") { return e.to_u64(); } @@ -169,9 +128,6 @@ pub(super) fn sys_reboot(syscap: RawHandle) -> u64 { if let Err(e) = demand_syscap(syscap, Rights::POWER) { return e.refuse(); } - if refused_once() { - return SyscallError::NotSupported.to_u64(); - } if !acpi::can_reboot() { log!("reboot: this machine's FADT names no reset register — refused"); return SyscallError::NotSupported.to_u64(); diff --git a/kernel/src/syscall/proc.rs b/kernel/src/syscall/proc.rs index 93fdc338ed5..fada5eddc3b 100644 --- a/kernel/src/syscall/proc.rs +++ b/kernel/src/syscall/proc.rs @@ -180,8 +180,6 @@ pub(super) fn sys_thread_join(tid: u64) -> u64 { } pub(super) fn sys_nanosleep(nanos: u64) -> u64 { - #[cfg(feature = "boot-actuators")] - crate::quiesce::last::hold(); // The ABI's relative span becomes an absolute Deadline here, and only here. let deadline = Deadline::at(crate::clock::now() + Duration::from_nanos(nanos)); // Armed on its own thread with no subject: nothing posts, only the deadline fires it. diff --git a/kernel/src/usb_gate.rs b/kernel/src/usb_gate.rs index 56f48cffbc5..c47beb8c3c4 100644 --- a/kernel/src/usb_gate.rs +++ b/kernel/src/usb_gate.rs @@ -1,288 +1,18 @@ -//! `usb-storage-gate`: in-guest half of the USB mass-storage gate. -//! -//! Verifies blocks the host wrote and writes blocks the host can check, so -//! neither half of the driver certifies itself. +//! `usb-reset-under-load`: the stick written continuously, so a reset lands on +//! a controller that is moving bytes. use alloc::vec; -use crate::block::{BlockDevice, Handle}; +use crate::block::BlockDevice; use crate::drivers::usb_storage; -/// 16 bytes so the block count behind it stays 8-byte aligned. -const MAGIC: &[u8; 16] = b"TOYOS-USB-GATE1\0"; -const AT_BLOCKS: usize = 16; -const AT_NONCE: usize = 24; - const BLOCK: usize = crate::mm::PAGE_SIZE as usize; -/// Blocks the host wrote and the guest must read back unchanged. -const HOST_BLOCKS: [i64; 2] = [1, -1]; -/// Blocks the guest writes and the host must find afterwards. -const GUEST_BLOCKS: [i64; 2] = [2, -2]; -/// Long enough to cross the driver's per-command batch of eight blocks. -const RUN_START: u64 = 4; -const RUN_LEN: u32 = 9; - -/// Mirrored byte-for-byte by the harness; must not change independently. -fn pattern(nonce: u64, block: u64, i: usize) -> u8 { - let n = (nonce >> ((i % 8) * 8)) as u8; - let b = (block ^ (block >> 13) ^ (block >> 27)) as u8; - n ^ b.wrapping_mul(37) ^ (i as u8).wrapping_mul(101) -} - -fn fill(buf: &mut [u8], nonce: u64, block: u64) { - for (i, byte) in buf.iter_mut().enumerate() { - *byte = pattern(nonce, block, i); - } -} - -/// FNV-1a over a whole block, mirrored byte-for-byte by the harness. -/// -/// [`first_bad`] is the guest's own comparator and nothing in the guest can -/// certify it; this is a number the harness recomputes from the image, so a -/// comparator that always agreed would be caught by the disagreement here. -fn digest(buf: &[u8]) -> u64 { - let mut hash: u64 = 0xcbf2_9ce4_8422_2325; - for &byte in buf { - hash ^= byte as u64; - hash = hash.wrapping_mul(0x0000_0100_0000_01b3); - } - hash -} - -/// Where a block does not match, or `None` if it does. -fn first_bad(buf: &[u8], nonce: u64, block: u64) -> Option<(usize, u8, u8)> { - buf.iter().enumerate().find_map(|(i, &got)| { - let want = pattern(nonce, block, i); - (got != want).then_some((i, got, want)) - }) -} - -/// Resolve a possibly-negative block index against the disk's size. -fn at(blocks: u64, index: i64) -> u64 { - if index >= 0 { - index as u64 - } else { - blocks.saturating_sub(index.unsigned_abs()) - } -} - -pub fn run() { - let disks = usb_storage::count(); - log!("usb-gate: {disks} disk(s) on the bus"); - for index in 0..disks { - let Some((disk, _)) = usb_storage::handle(index) else { continue }; - check(index, &disk); - } - log!("usb-gate: sweep complete"); -} - -fn check(index: usize, disk: &Handle) { - let read =|block: u64, count: u32, buf: &mut [u8]| disk.lock().read_blocks(block, count, buf); - let write = |block: u64, count: u32, buf: &[u8]| disk.lock().write_blocks(block, count, buf); - - let blocks = disk.block_count(); - let mut head = vec![0u8; BLOCK]; - if read(0, 1, &mut head).is_err() { - log!("usb-gate: disk {index} would not give up block 0"); - return; - } - if &head[..MAGIC.len()] != MAGIC { - log!("usb-gate: disk {index} carries no stamp, leaving it alone"); - return; - } - // Refuse a stamp written for a different block count: offsets would mean - // something else on this disk. - let stamped = u64::from_le_bytes(head[AT_BLOCKS..AT_BLOCKS + 8].try_into().unwrap()); - if stamped != blocks { - log!("usb-gate: disk {index} is stamped for {stamped} blocks and has {blocks}"); - return; - } - let nonce = u64::from_le_bytes(head[AT_NONCE..AT_NONCE + 8].try_into().unwrap()); - // +2 leaves room for the two blocks addressed from the end. - if blocks < RUN_START + RUN_LEN as u64 + 2 { - log!("usb-gate: disk {index} has only {blocks} blocks, too few to test"); - return; - } - log!("usb-gate: disk {index} designated, blocks={blocks} nonce={nonce:#018x}"); - - let mut reads_ok = true; - let mut buf = vec![0u8; BLOCK]; - for index in HOST_BLOCKS { - let block = at(blocks, index); - buf.fill(0); - if read(block, 1, &mut buf).is_err() { - reads_ok = false; - log!("usb-gate: host block {block} could not be read"); - continue; - } - let hash = digest(&buf); - match first_bad(&buf, nonce, block) { - None => log!("usb-gate: host block {block} verified digest={hash:#018x}"), - Some((i, got, want)) => { - reads_ok = false; - log!( - "usb-gate: host block {block} differs at byte {i}: {got:#04x} not {want:#04x} \ - digest={hash:#018x}" - ); - } - } - } - - // Checks the error channel: a refusal must be distinguishable from data, - // not silently zero-filled. - let past_end = read(blocks, 1, &mut buf).is_err(); - log!("usb-gate: read past the last block refused={past_end}"); - - // No device can be staged slow enough to expire the deadline naturally, so - // an already-passed one is established directly. - // - // This spent-budget read is positioned here rather than at the end - // because every assertion that follows depends on the device being left - // untouched by it. - // - // Goes straight to the driver, not through `BlockDevice`, so this call is - // not captured under a read that was never issued. - // - // budget= separates `BudgetExpired` from `BlockError::Device`. - let spent = { - let _op = crate::scheduler::Operation::begin(crate::time::Deadline::passed()); - // Past the block layer, which is the point; a read answers no writer's flush. - crate::drivers::xhci::storage_read(index, at(blocks, HOST_BLOCKS[0]), 1, &mut buf, &mut 0) - }; - log!("usb-gate: read with a spent budget refused={} budget={}", - spent.is_err(), - spent == Err(crate::block::BlockError::BudgetExpired)); - - // Keyed on the inverted nonce so a driver that returns the wrong block - // cannot pass by returning data of the right kind. - let guest_nonce = !nonce; - let mut writes_ok = true; - // Nonzero only when a write itself reported failure, distinct from a - // readback mismatch. - let mut write_errors = 0usize; - for index in GUEST_BLOCKS { - let block = at(blocks, index); - fill(&mut buf, guest_nonce, block); - if write(block, 1, &buf).is_err() { - writes_ok = false; - write_errors += 1; - log!("usb-gate: block {block} refused the write"); - } - } - - let mut run = vec![0u8; RUN_LEN as usize * BLOCK]; - for i in 0..RUN_LEN as u64 { - let block = RUN_START + i; - let at = i as usize * BLOCK; - fill(&mut run[at..at + BLOCK], guest_nonce, block); - } - if write(RUN_START, RUN_LEN, &run).is_err() { - writes_ok = false; - write_errors += 1; - log!("usb-gate: the {RUN_LEN}-block run refused the write"); - } - if disk.lock().flush().is_err() { - writes_ok = false; - log!("usb-gate: the disk refused to flush"); - } - - let mut back = vec![0u8; RUN_LEN as usize * BLOCK]; - if read(RUN_START, RUN_LEN, &mut back).is_err() { - writes_ok = false; - log!("usb-gate: the {RUN_LEN}-block run could not be read back"); - } - if writes_ok && back != run { - writes_ok = false; - let at = back.iter().zip(&run).position(|(a, b)| a != b).unwrap_or(0); - log!("usb-gate: readback of the {RUN_LEN}-block run differs at byte {at}"); - } - for index in GUEST_BLOCKS { - let block = at(blocks, index); - buf.fill(0); - if read(block, 1, &mut buf).is_err() { - writes_ok = false; - log!("usb-gate: block {block} could not be read back"); - continue; - } - if let Some((i, got, want)) = first_bad(&buf, guest_nonce, block) { - writes_ok = false; - log!("usb-gate: readback of block {block} differs at byte {i}: {got:#04x} not {want:#04x}"); - } - } - - log!( - "usb-gate: disk done reads={} writes={} refusal={past_end} wr_err={write_errors} healthy={}", - if reads_ok { "ok" } else { "bad" }, - if writes_ok { "ok" } else { "bad" }, - usb_storage::healthy(index) - ); -} - /// Blocks per read-and-write-back pair — eight of this driver's largest SCSI /// command, so each pair is several commands and not one. #[cfg(feature = "boot-actuators")] const WEDGE_CHUNK: u32 = 64; -/// Pairs before the wedge, so the wedge is not the first thing this command's -/// device saw. -#[cfg(feature = "boot-actuators")] -const WEDGE_CHUNKS: u32 = 16; - -/// Leave this machine wedged inside one Bulk-Only command, at the phase `at` -/// names, with megabytes of writes behind it. -/// -/// **The stimulus for a state no ordinary boot reaches.** A shutdown reaches -/// its sync with nothing dirty on most boots, so the traffic and the command -/// the wedge is taken inside are both issued here rather than waited for. -/// -/// **Every block is read first and written back byte for byte**, so the medium -/// is what it was however much of a write either reset completes; and at the -/// disk's own end, because a fixed offset is inside a partition this kernel -/// mounts on the smaller disks the same actuator boots on. -#[cfg(feature = "boot-actuators")] -pub fn wedge_inside_a_write(at: toyos_xhci::bot::Phase) { - let Some((disk, _)) = usb_storage::handle(0) else { - log!("usb-wedge: no USB disk on this machine, so there is no command to wedge inside"); - return; - }; - let span = u64::from(WEDGE_CHUNK) * u64::from(WEDGE_CHUNKS); - let Some(first) = disk.block_count().checked_sub(span) else { - log!("usb-wedge: disk 0 holds {} blocks, fewer than the {span} this wedge writes", - disk.block_count()); - return; - }; - let mut buf = vec![0u8; WEDGE_CHUNK as usize * BLOCK]; - // The last pair carries the wedge, so every pair before it is traffic the - // device has already taken. - for chunk in 0..WEDGE_CHUNKS { - let block = first + u64::from(chunk) * u64::from(WEDGE_CHUNK); - if disk.lock().read_blocks(block, WEDGE_CHUNK, &mut buf).is_err() { - log!("usb-wedge: disk 0 would not give up block {block}, so no write is staged from it"); - return; - } - if chunk + 1 == WEDGE_CHUNKS { - log!("{USB_WEDGE_STAGED} {at} phase, after {} KiB rewritten with the bytes just read \ - from it", u64::from(WEDGE_CHUNK) * u64::from(chunk) * BLOCK as u64 / 1024); - crate::drivers::xhci::arm_mid_write_wedge(at); - } - if disk.lock().write_blocks(block, WEDGE_CHUNK, &buf).is_err() { - log!("usb-wedge: disk 0 refused the write at block {block}"); - return; - } - } - log!("{USB_WEDGE_MISSED} — every write completed, so no CPU was stopped inside one and this \ - boot ends itself the ordinary way"); -} - -/// What the wedge says before the write it is taken inside, and what it says if -/// that write ran to completion instead. Judged by the harness, so both are -/// constants (`src/bootlog.rs`). -#[cfg(feature = "boot-actuators")] -pub const USB_WEDGE_STAGED: &str = "usb-wedge: stopping every CPU at the"; -#[cfg(feature = "boot-actuators")] -pub const USB_WEDGE_MISSED: &str = "usb-wedge: the write completed"; - /// The smallest disk this load will sweep: a gibibyte in 4 KiB blocks. /// /// **A refusal and not a smaller sweep.** The sweep takes the last eighth of diff --git a/kernel/src/vma.rs b/kernel/src/vma.rs index f83f2c73193..394c7772c46 100644 --- a/kernel/src/vma.rs +++ b/kernel/src/vma.rs @@ -17,14 +17,11 @@ const GUARD_SIZE: u64 = PAGE_2M; /// The floor at 8 GB. const WINDOW: Window = Window::new(0x0002_0000_0000, ALLOC_CEILING, GUARD_SIZE); -/// The `test-tiny-va` actuator's: 256 MiB under the ceiling, so a process can -/// run out of address space before it runs out of memory. -const TINY_WINDOW: Window = Window::new(ALLOC_CEILING - 256 * 1024 * 1024, ALLOC_CEILING, GUARD_SIZE); /// Where `find_gap` places, and the bound every length from userland is /// refused against before any sum is taken on it. pub fn window() -> Window { - if crate::actuator::test_tiny_va() { TINY_WINDOW } else { WINDOW } + WINDOW } diff --git a/kernel/src/watch.rs b/kernel/src/watch.rs index 268d4e115f8..8697bb067f2 100644 --- a/kernel/src/watch.rs +++ b/kernel/src/watch.rs @@ -65,8 +65,6 @@ impl CellLock for IrqLock { preempt_off(|_| { let irq = crate::arch::IrqGuard::close(); let mut held = self.0.lock(&irq); - #[cfg(feature = "boot-actuators")] - handler_post::raise_if_staged(); f(&mut held) }) } @@ -148,8 +146,6 @@ impl IrqWatch { /// with its CPU's preempt count raised, as `device_irq_entry` holds it, so /// this post's own never reaches zero, and a pass, inside the interrupt. pub fn post_in_place(&self) { - #[cfg(feature = "boot-actuators")] - handler_post::note_post(self); preempt_off(|p| { let env = Poster { cpus: cpus(), kicker: &HW, preempt: p }; self.0.post_in_place(WakeCause::new(WakeReason::Woken), &env); @@ -169,11 +165,6 @@ impl> Waitable { self.0.cancel_rings(); } - /// `handler-post`'s stand where a registration holds the list lock. - #[cfg(feature = "boot-actuators")] - pub(crate) fn holding(&self, f: impl FnOnce()) { - self.0.holding(f); - } } /// A thread's registration on one watch, held across its wait and ended by @@ -286,119 +277,6 @@ pub fn wait_until>( Ok(()) } -/// `handler-post`: the last claim slot's vector, which no claim holds, raised -/// on this CPU while it holds preemption off, posts that slot's watch from the -/// handler before any pass can run. Raised inside a post of the watch itself, -/// the handler's post follows once the outer one lets go; raised inside a -/// completion written into a ring that polls the watch, or inside that ring's -/// own watch's list lock, the handler's post completes that poll once the -/// section lets go. A hold counts the posts of the watch made on its CPU, and -/// no other watch's, and lapses at its budget. One run of [`HOLDS`] holds per -/// arm, on whichever idle loop reaches it first with interrupts open; its -/// verdict is one [`Verdict`] line. -#[cfg(feature = "boot-actuators")] -pub mod handler_post { - use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering::Relaxed}; - - use toyos_sched::watch::handler_post::{Verdict, HOLDS}; - - use crate::pcidev::{MAX_FUNCTIONS, VECTORS}; - use crate::time::{Budget, Deadline, Duration}; - - const SLOT: usize = MAX_FUNCTIONS - 1; - - const WINDOW: Budget = Budget::of( - Duration::from_secs(1), - "the hold is counted as lapsed, and the verdict line says so", - ); - - const NOBODY: u32 = u32::MAX; - static RAN: AtomicBool = AtomicBool::new(false); - /// The CPU whose next interrupts-off section raises the vector inside itself. - static RAISE_INSIDE: AtomicU32 = AtomicU32::new(NOBODY); - static HOLDING: AtomicU32 = AtomicU32::new(NOBODY); - static POSTS: AtomicU64 = AtomicU64::new(0); - - pub fn run() { - // A hold is a CPU that takes interrupts while it holds. - if RAN.load(Relaxed) || !crate::arch::cpu::interrupts_enabled() || RAN.swap(true, Relaxed) { - return; - } - // A held slot's driver would take counts its device never raised. - assert!(crate::pcidev::held_at(SLOT).is_none(), "handler-post: claim slot {SLOT} is held"); - let me = crate::arch::percpu::cpu_id(); - let claim = crate::pcidev::watch(SLOT); - let ring = crate::inbox::Staged::new(); - let verdict = crate::sched::driver::preempt_off(|_| { - HOLDING.store(me, Relaxed); - // The outer post is one of the two a hold waits for. - let in_a_list = holds(2, || { - RAISE_INSIDE.store(me, Relaxed); - claim.post_in_place(); - }); - let in_a_ring = holds(1, || { - ring.poll(claim); - RAISE_INSIDE.store(me, Relaxed); - ring.complete(); - }); - // Where a submitter's registration holds it. - let in_a_rings_watch = holds(1, || { - ring.poll(claim); - ring.holding_its_watch(raise); - }); - HOLDING.store(NOBODY, Relaxed); - Verdict { in_a_list, in_a_ring, in_a_rings_watch } - }); - crate::log!("{verdict}"); - } - - /// [`HOLDS`] holds of `stage`, answering how many saw `owed` posts of the - /// claim's watch before their budget. - fn holds(owed: u64, stage: impl Fn()) -> u32 { - let mut posted = 0; - for _ in 0..HOLDS { - let before = POSTS.load(Relaxed); - stage(); - let deadline = Deadline::at(crate::clock::now() + WINDOW.duration()); - loop { - if POSTS.load(Relaxed) >= before + owed { - posted += 1; - break; - } - if deadline.reached(crate::clock::now()) { - break; - } - core::hint::spin_loop(); - } - } - posted - } - - fn raise() { - crate::arch::irqchip::send_self(VECTORS[SLOT]); - } - - /// From inside an interrupts-off section: the staged CPU's next one raises - /// the vector while it holds. - pub fn raise_if_staged() { - let staged = RAISE_INSIDE.load(Relaxed); - if staged != NOBODY && staged == crate::arch::percpu::cpu_id() { - RAISE_INSIDE.store(NOBODY, Relaxed); - raise(); - } - } - - pub fn note_post(watch: &super::IrqWatch) { - let holding = HOLDING.load(Relaxed); - if holding != NOBODY - && holding == crate::arch::percpu::cpu_id() - && core::ptr::eq(watch, crate::pcidev::watch(SLOT)) - { - POSTS.fetch_add(1, Relaxed); - } - } -} - /// Register, then park until `ready()` holds, for a wait a kill may not end /// and no deadline bounds. #[track_caller] diff --git a/licenses/MIT-gbae.txt b/licenses/MIT-gbae.txt deleted file mode 100644 index 6b0eb98c22f..00000000000 --- a/licenses/MIT-gbae.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025-2026 japabu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/licenses/MIT-iced.txt b/licenses/MIT-iced.txt deleted file mode 100644 index f1148e26eac..00000000000 --- a/licenses/MIT-iced.txt +++ /dev/null @@ -1,18 +0,0 @@ -Copyright 2019 Héctor Ramón, Iced contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/assets.rs b/src/assets.rs index 759ad2719f7..55f3fb77201 100644 --- a/src/assets.rs +++ b/src/assets.rs @@ -246,9 +246,7 @@ fn absentees(dir: &Path, declared: &BTreeSet) -> Vec { /// /// **`assets = [..]` names a directory and sweeps it whole**, so a config that /// builds no reader for a file still shipped it: these two are 19.7 MB of the -/// 20.8 MB `assets/` holds, and `console/`, both desktop cases, -/// `tests/logrotatecase` and `tests/metalcase` each carried both into an image -/// with no doom in it. Named here rather than per config, because which program +/// 20.8 MB `assets/` holds. Named here rather than per config, because which program /// opens a file is a property of the program and not of any one image, and a /// list repeated in five configs is a list that goes stale in four of them. /// The names are ROOT's, which [`collect`] lower-cases. diff --git a/src/bootlog.rs b/src/bootlog.rs index 76321c4fe4f..dcf6947b0ce 100644 --- a/src/bootlog.rs +++ b/src/bootlog.rs @@ -55,23 +55,6 @@ pub const WEDGE_STAGED: &str = "wedge: staged, and only the boot deadline ends t pub const WEDGE_ARRIVED_DEAF: &str = "arrived with interrupts off, through the syscall gate, and takes them again here"; -/// What the USB wedge arms say before the write they stop the machine inside, -/// in `kernel/src/usb_gate.rs`; the phase and the traffic behind it follow on -/// the same line. -/// -/// The witness that the boot the deadline then ended was one holding a device -/// inside a Bulk-Only command, which is the whole of what those controls stage — -/// a wedge taken anywhere else is `WEDGE_STAGED`'s boot with a longer log. -pub const USB_WEDGE_STAGED: &str = "usb-wedge: stopping every CPU at the"; - -/// What the same arms say if every write ran to completion, which means no CPU -/// was stopped inside one. -/// -/// **A control that stages nothing passes for the wrong reason**: without this -/// line the boot would still wedge — at the shutdown, with no device inside -/// anything — and read back exactly like the arm that proves the point. -pub const USB_WEDGE_MISSED: &str = "usb-wedge: the write completed"; - /// What the `usb-reset-under-load` arm says once it is streaming, and the three /// ways it says it is not, in `kernel/src/usb_gate.rs`. /// @@ -94,11 +77,6 @@ pub const USB_LOAD_SWEPT: &str = "usb-load: the sweep reached the end of the dis /// whatever the rest of the machine was doing. pub const LOCKED_UP: &str = "a cpu locked up with interrupts off"; -/// What the `hard-lockup-probe` actuator says before its cpu stops answering, -/// in `kernel/src/hardlockup/probe.rs` — the witness in the sealed record's tail -/// that this machine was ended by the control that was staged on it. -pub const LOCKUP_STAGED: &str = "hard-lockup: staged, and only the lockup detector ends this cpu"; - /// What the kernel seals under its own `DONE` record, in /// `kernel/src/log/mod.rs`'s `seal_tail`: the head of the boot's newest /// records. The next loader pass prints it back under [`PREVIOUS_PANIC`]. @@ -229,12 +207,6 @@ pub fn panel_census(log: &str) -> Option { /// ended, which no program writes ([`is_program_line`]). pub const EXIT: &str = "exit: "; -/// The kernel's record for a process that started, in `kernel/src/process.rs`. -/// -/// Read for where it must *not* be: after the boot's own last word, where it -/// says a process still on a run queue started another one under a shutdown. -pub const SPAWN: &str = "spawn: "; - /// One rendered record's message: what follows the bracket every kernel /// record opens with. `None` for a line that is not a kernel record's first. pub fn message(line: &str) -> Option<&str> { @@ -530,66 +502,10 @@ pub fn verdict(log: &str) -> Result { Ok(boot_ms) } -/// **`Rebooting.` is the last record, and nothing this boot still holds may -/// write one after it.** -/// -/// The runner's deadline kills the job it is watching, which releases the `wait` -/// its own job loop is inside, and that loop can spawn the next job into the -/// window between the boot's last word and the reset. -/// -/// A boot with no such word — a panic — is not asked: it correctly writes none. -/// The window ends at the next loader pass, because everything that pass prints -/// is after the reset by construction. -/// -/// **A spawn record and not every record**, because those are the two different -/// claims. `quiesce` writes after its own last word by construction — an idle -/// CPU's `sched:` report can land there — and nothing is left running to take -/// it anywhere but the console. A *spawn* is a process that was still on a run -/// queue after the stop said it had stopped every one. -/// -/// **The boot's own word, not the next pass's copy of it**: that pass prints -/// the boot's newest records under [`LOG_TAIL`], newest first, so the -/// copy of the last word heads records that were written before it. -pub fn nothing_after_the_last_word(text: &str) -> Result<(), String> { - let lines: Vec<&str> = text.lines().collect(); - let Some(at) = lines - .iter() - .rposition(|line| line.contains(REBOOTING) && !line.contains(LOG_TAIL)) - else { - return Ok(()); - }; - let mut window = - lines[at + 1..].iter().take_while(|line| !line.contains(LOADER_FIRST_LINE)); - match window.find(|line| line.contains(SPAWN)) { - None => Ok(()), - Some(line) => Err(format!( - "a process started after {REBOOTING:?}, which is the boot's own last word and what a \ - metal boot is judged on: {line:?}" - )), - } -} - #[cfg(test)] mod tests { use super::*; - /// A spawn after the boot's own last word is refused; the next pass's - /// newest-first copy of that word, which heads records written before it, - /// opens no window, and the next pass is after the reset. - #[test] - fn a_spawn_after_the_boots_own_last_word_is_refused() { - let word = format!("[kernel 23.340 cpu1] {REBOOTING}\n"); - let spawn = format!("[kernel 23.341 cpu0] {SPAWN}late pid=9\n"); - let loader = format!("{LOADER_FIRST_LINE}\n"); - let tail = format!( - "| {LOG_TAIL}[kernel 23.340 cpu1] {REBOOTING}\n| {LOG_TAIL}[kernel 1.0 cpu0] {SPAWN}init pid=1\n" - ); - assert_eq!(nothing_after_the_last_word(&format!("{word}{loader}{tail}")), Ok(())); - assert!(nothing_after_the_last_word(&format!("{word}{spawn}{loader}{tail}")).is_err()); - assert_eq!(nothing_after_the_last_word(&format!("{word}{loader}{spawn}")), Ok(())); - assert_eq!(nothing_after_the_last_word(&spawn), Ok(())); - } - /// The half-told boot: the kernel got all the way up and the log stops /// there, so the machine either never asked for the reset or `logd` never /// made the log whole before it. @@ -710,17 +626,11 @@ mod tests { ("kernel/src/deadline.rs", format!("\"{DEADLINE_ARMED}{{ms}} ms")), ("kernel/src/deadline.rs", format!("WEDGE_STAGED: &str = \"{WEDGE_STAGED}\"")), ("kernel/src/deadline.rs", format!("\"{WEDGE_ARRIVED_DEAF}\"")), - ("kernel/src/usb_gate.rs", format!("USB_WEDGE_STAGED: &str = \"{USB_WEDGE_STAGED}\"")), - ("kernel/src/usb_gate.rs", format!("USB_WEDGE_MISSED: &str = \"{USB_WEDGE_MISSED}\"")), ("kernel/src/usb_gate.rs", format!("LOAD_RUNNING: &str = \"{USB_LOAD_RUNNING}\"")), ("kernel/src/usb_gate.rs", format!("LOAD_REFUSED: &str = \"{USB_LOAD_REFUSED}\"")), ("kernel/src/usb_gate.rs", format!("LOAD_STOPPED: &str = \"{USB_LOAD_STOPPED}\"")), ("kernel/src/usb_gate.rs", format!("LOAD_SWEPT: &str = \"{USB_LOAD_SWEPT}\"")), ("kernel/src/hardlockup/mod.rs", format!("LOCKED_UP: &str = \"{LOCKED_UP}\"")), - ( - "kernel/src/hardlockup/probe.rs", - format!("PROBE_STAGED: &str = \"{LOCKUP_STAGED}\""), - ), ( "kernel/src/drivers/panic_console/mod.rs", format!("CENSUS: &str = \"{PANEL_CENSUS}\""), diff --git a/src/build.rs b/src/build.rs index 37a33feb876..81493de21c5 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1335,8 +1335,8 @@ fn declared_kernel_features(root: &Path) -> Vec { pub const TEST_KERNEL: &[&str] = &["boot-actuators", "test-actuators"]; /// Kernel builds the ordinary test suite is allowed to make. -pub const TEST_SUITE_KERNEL_BUILDS: [&str; 4] = - ["", "boot-actuators,test-actuators", "fpu-save-nothing", "user-writable-gsbase"]; +pub const TEST_SUITE_KERNEL_BUILDS: [&str; 2] = + ["", "boot-actuators,test-actuators"]; /// The scheduler core's own asserts, compiled in: `toyos-sched/check`. /// @@ -1356,52 +1356,6 @@ pub fn harness_kernel_build_is_declared(features: &str, debug_wait: bool) -> boo } } -/// Every name in which a process of this boot config can speak a console line -/// that is not the program under test's. -/// -/// **Derived, never listed.** The point of reading it out of the config is that -/// a daemon added to `[boot] start` tomorrow is in this set the moment it -/// exists — a hardcoded list would let the next `netd`'s lines start deciding C -/// tests again, which is what task #84 was (`tests/common/console.rs`). -/// `/system/bin/init` itself is added by hand because it is the one speaker that is not a -/// `[programs]` key: it is the parent that starts every one of them, and it -/// speaks before any of them exists (`init: netd: no nic on this machine` is on -/// the console before netd is loaded). -/// -/// The union of the two lists rather than `[boot] start` alone: a program the -/// config declares is a binary this image carries and a name init can be asked -/// to speak in, and the whole value of deriving the set is that it is the -/// config's answer rather than an author's. -/// -/// **A program also speaks in the name of every device it claims, and that is -/// measured rather than supposed.** `userland/soundd/src/virtio.rs` writes -/// `virtio-sound: configured stream 0: 44100Hz 2ch s16le` — the driver layer -/// says which device is talking, not which program — and a plain -/// `tests/testcases` boot puts three such lines on the console before the test -/// runner is ready. `devices` is where those names are declared, so it is where -/// they are read from; `c_capture_ignores_daemon_lines` walks a real boot log -/// and reds on any line this set cannot account for, which is what keeps this -/// derivation honest as the tree grows. -/// -/// `config` is the `system.toml` itself, not its directory. -pub fn console_speakers(config: &Path) -> std::collections::BTreeSet { - let parsed = parse_config(config); - let mut names = std::collections::BTreeSet::new(); - for (program, entry) in parsed.programs { - names.insert(program); - names.extend(entry.devices); - } - names.extend(parsed.boot.start); - names.insert("init".to_string()); - names -} - -/// What `/system/bin/init` starts on the boot `config` describes, in the manifest's -/// order. `config` is the `system.toml` itself, not its directory. -pub fn boot_start(config: &Path) -> Vec { - parse_config(config).boot.start -} - /// The manifest bytes and the symlink table `config` renders to, for a reader /// that judges the finished ROOT against what the config asked for. pub fn manifest_and_symlinks(config: &Path) -> (Vec, Vec<(String, String)>) { @@ -1607,7 +1561,7 @@ fn assert_entry_window_matches_features(features: &str, kernel: &[u8]) { /// Every one of these is a `#[cfg(feature = "check")]` site in `toyos-sched`. /// Two are asserts from `invariants::check_cpu` — invariant T's armed-timer /// bound and the container-versus-state-word agreement. The third is the -/// pass-cost report (`cpu::PassCostReport::PREFIX`), which is a *measurement* +/// pass-cost report, which is a *measurement* /// and not an assert: a pass's elapsed time includes any interval a hypervisor /// took the CPU away, so it is recorded rather than panicked over. Their format /// strings are the only part of the check build with a literal the linker keeps, @@ -1913,8 +1867,7 @@ fn root_image_key(plan: &Plan, image_key: &str, extra_files: &[(String, Vec) /// /// The image itself is never memoized, only the three parts it is made of: /// [`image::create_boot_image`] mints a fresh partition GUID per call and writes -/// it into both the GPT and the ESP, and a boot that did not get its own is a -/// boot `log_partition_identity` is entitled to catch. +/// it into both the GPT and the ESP. pub fn build_test_image( root: &Path, plan: &Plan, @@ -1933,18 +1886,6 @@ pub fn build_test_image( ) } -/// The image `ssh … update` takes, built from a plan as a test image is and -/// signed with this process's key at the plan's version. -pub fn build_update_image(root: &Path, plan: &Plan, quiet: bool, extra_files: &[(String, Vec)]) -> Vec { - let parts = build_test_parts(root, plan, quiet, extra_files); - image::update_image( - &parts.kernel, - &parts.root, - &plan.params.join(","), - image::Signing { key: crate::signing::key(), version: plan.version }, - ) -} - /// The three parts one image is made of, each memoized for this process. pub struct Parts { pub kernel: Arc>, @@ -2068,19 +2009,9 @@ pub fn build_host_judges(root: &Path, quiet: bool) { /// silently repoint every accessor below. type Judge = (&'static str, &'static str); -const HTTPS_SERVER: Judge = ("tests/https-server-host", "https_test_server"); -const HTTPS_FETCH: Judge = ("tests/https-fetch-host", "https_fetch"); const SSH_CLIENT: Judge = ("tests/ssh-client-host", "toyos_ssh"); -const HOST_JUDGES: [Judge; 3] = [HTTPS_SERVER, HTTPS_FETCH, SSH_CLIENT]; - -pub fn https_test_server(root: &Path) -> PathBuf { - host_judge(root, HTTPS_SERVER) -} - -pub fn https_fetch_host(root: &Path) -> PathBuf { - host_judge(root, HTTPS_FETCH) -} +const HOST_JUDGES: [Judge; 1] = [SSH_CLIENT]; /// Copy to `to` the binary the build leaves for userland workspace program /// `name`: the bytes a swap sends a running machine in place of the ones its @@ -2524,7 +2455,7 @@ mod tests { assert!(harness_kernel_build_is_declared(&debug, true)); assert!(!harness_kernel_build_is_declared(&debug, false)); assert!(!harness_kernel_build_is_declared( - "fpu-save-nothing,debug-wait", + "boot-actuators,test-actuators,debug-wait", true )); for suite_build in TEST_SUITE_KERNEL_BUILDS { @@ -2678,11 +2609,10 @@ mod tests { // instruction gone and `DF` back out of the `SYSCALL` mask, so a // build carrying it inherits a set direction flag from whatever // it interrupted. The negative control for that fix, and its own - // build for `fpu-save-nothing`'s reason — the defect is in a - // `naked_asm!` body on every ring transition, where a boot - // parameter would have to be a branch. + // build because the defect is in a `naked_asm!` body on every + // ring transition, where a boot parameter would have to be a + // branch. "entry-df-unclean", - "fpu-save-nothing", // The two band shapes that separate the two readings // `heap-tripwire`'s own result left standing — the bands absorb // a bounded overrun, or they displace every allocation and the @@ -2782,8 +2712,6 @@ mod tests { "switch-witness-mutate-frame", "switch-witness-mutate-rsp", "test-actuators", - // `FSGSBASE` back in `CR4`: `gsbase_locked`'s negative control. - "user-writable-gsbase", // Costs no kernel build at all, for `loom`'s reason: declared // so `cfg` checking knows the name, and turned on only by // `kernel-loom` — to remove the log wake path's two `SeqCst` @@ -3136,42 +3064,17 @@ mod tests { "system.toml", "diag/system.toml", "console/system.toml", - "tests/blockdcase/system.toml", - "tests/desktopcase/system.toml", - "tests/desktopaudiocase/system.toml", - "tests/doommusiccase/system.toml", - "tests/e1000case/system.toml", - "tests/e1000leasecase/system.toml", - "tests/e1000talkcase/system.toml", - "tests/flrswapcase/system.toml", - "tests/fsdclaimcase/system.toml", - "tests/fsdmountcase/system.toml", - "tests/fsdrestartcase/system.toml", - "tests/inspectcase/system.toml", "tests/jobcase/system.toml", - "tests/jobdeadlinecase/system.toml", "tests/lancase/system.toml", "tests/lanicscase/system.toml", "tests/lanleasecase/system.toml", "tests/lantalkcase/system.toml", "tests/latencycase/system.toml", - "tests/layoutcase/system.toml", - "tests/logflushcase/system.toml", - "tests/logrotatecase/system.toml", "tests/logstallcase/system.toml", - "tests/logstreamcase/system.toml", - "tests/logstreame1000case/system.toml", "tests/metalcase/system.toml", "tests/metaldevicecase/system.toml", "tests/netcase/system.toml", - "tests/partclaimcase/system.toml", - "tests/pkgcase/system.toml", - "tests/quiescetwicecase/system.toml", - "tests/sshdcase/system.toml", - "tests/swapcase/system.toml", "tests/testcases/system.toml", - "tests/toolkitcase/system.toml", - "tests/updatecase/system.toml", "tests/virtjobcase/system.toml", "tests/virtpaniccase/system.toml", "tests/virtsmpcase/system.toml", @@ -3301,33 +3204,16 @@ mod tests { assert!(provides_disjoint_from_serves(&bad).is_err()); } - /// The one `devices` entry in the tree that is a deliberate second claim: - /// config, program, device. `pci_function_is_exclusive` boots it and reads - /// the kernel refusing it. - const STAGED_COLLISION: (&str, &str, &str) = - ("tests/netcase/system.toml", "test-runner", "pci:1af4:1041"); - - /// Init mints one claim per device, so a shipping config naming one twice - /// starts a program with a hole where its claim should be. - /// - /// `excused` is one `(program, device)` and never a whole config: every - /// other collision in the config that stages one is still refused. - fn one_claimant_per_device( - cfg: &SystemConfig, - excused: Option<(&str, &str)>, - ) -> Result<(), String> { + /// Init mints one claim per device, so a config naming one twice starts a + /// program with a hole where its claim should be. + fn one_claimant_per_device(cfg: &SystemConfig) -> Result<(), String> { let mut seen: BTreeMap<&str, &str> = BTreeMap::new(); for (name, prog) in &cfg.programs { for d in &prog.devices { - if excused == Some((name.as_str(), d.as_str())) { - continue; - } if let Some(prev) = seen.insert(d, name) { return Err(format!( "device `{d}` is claimed by both `{prev}` and `{name}`; the second \ - claim is refused at boot, and `{}`'s `{}` is the one entry allowed \ - to stage that", - STAGED_COLLISION.0, STAGED_COLLISION.1 + claim is refused at boot" )); } } @@ -3336,25 +3222,18 @@ mod tests { } /// Not the capability boundary — `kernel/src/pcidev`'s slot reservation is, - /// and this compares `system.toml` strings. The excused entry is asserted to - /// still be a collision on the device it names, so the exception cannot rot - /// into a pass and cannot cover a second one added to the same config. + /// and this compares `system.toml` strings. #[test] fn every_device_class_has_at_most_one_claimant() { for cfg in ALL_CONFIGS { - let excused = - (*cfg == STAGED_COLLISION.0).then_some((STAGED_COLLISION.1, STAGED_COLLISION.2)); - one_claimant_per_device(&load(cfg), excused).unwrap_or_else(|e| panic!("{cfg}: {e}")); + one_claimant_per_device(&load(cfg)).unwrap_or_else(|e| panic!("{cfg}: {e}")); } - let staged = one_claimant_per_device(&load(STAGED_COLLISION.0), None) - .expect_err("the excused entry no longer collides with anything"); - assert!(staged.contains(STAGED_COLLISION.2), "{staged}"); let bad: SystemConfig = toml::from_str( "init = []\n[programs.a]\ndevices = [\"framebuffer\"]\n\ [programs.b]\ndevices = [\"framebuffer\"]\n", ) .unwrap(); - assert!(one_claimant_per_device(&bad, None).is_err()); + assert!(one_claimant_per_device(&bad).is_err()); } /// netd's two actuators that only its Intel driver answers, spelled here diff --git a/src/ci.rs b/src/ci.rs index 5323f09f130..c7e6ca72395 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -19,8 +19,7 @@ //! //! **The instrument is declared once.** `.github/qemu-version` is the QEMU //! every guest is measured with — the version has been measured to decide -//! verdicts (`desktop_typing_damage` and `usb_storage_shapes` are red on 8.2.2 -//! and green on 11.0.3, same image, same commit, same accelerator). A guest job +//! verdicts. A guest job //! reds on a disagreement, and on a `/dev/kvm` that is present and does not //! open; `cargo run` only notes one, because a build must not stop for brew. @@ -28,47 +27,36 @@ use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; use std::process::Command; -use crate::arch::Arch; +use crate::arch::{Accel, Arch}; use crate::{flags, release, sdkversion, sync}; -pub const GUEST_ARCH: Arch = Arch::X86_64; - const USAGE: &str = "cargo run -- --ci , where is one of: host every host test: the build system, the harness's own checks, the host workspace, the licences of what ships, clippy, the model controls, userland and the SDK (ci.yml, nightly) toolchain publish this tree's toolchain if nobody has (nightly) - guest / one shard of the guest suite (nightly) - tcg one test on an emulated CPU (nightly) + guest the guest suite (nightly) publish put main's SDK crates on crates.io (publish.yml)"; #[derive(Debug, PartialEq, Eq)] enum Job { Host, Toolchain, - Guest(String), - Tcg, + Guest, Publish, } fn parse(words: &[String]) -> Result { - let shard = |spec: Option<&String>| -> Result { - let spec = spec.ok_or("that job takes a shard, /")?; - crate::testargs::parse_shard(&["--shard".to_string(), spec.clone()])?; - Ok(spec.clone()) - }; let job = match words.first().map(String::as_str) { Some("host") => Job::Host, Some("toolchain") => Job::Toolchain, - Some("guest") => Job::Guest(shard(words.get(1))?), - Some("tcg") => Job::Tcg, + Some("guest") => Job::Guest, Some("publish") => Job::Publish, Some(other) => return Err(format!("no CI job is called {other:?}")), None => return Err("which job?".to_string()), }; - let takes = usize::from(matches!(job, Job::Guest(_))) + 1; - if words.len() > takes { - return Err(format!("{:?} takes nothing after it: {:?}", words[0], &words[takes..])); + if words.len() > 1 { + return Err(format!("{:?} takes nothing after it: {:?}", words[0], &words[1..])); } Ok(job) } @@ -81,8 +69,7 @@ pub fn dispatch(root: &Path, args: &[String]) { let steps = match &job { Job::Host => host(root), Job::Toolchain => vec![step("the toolchain release", || release::ensure_published(root))], - Job::Guest(shard) => guest(root, &suite_args(&["--shard", shard, "--jobs", "1"])), - Job::Tcg => guest(root, &suite_args(&["--jobs", "1", "empty_dir_stat"])), + Job::Guest => guest(root, &suite_args(&["--jobs", "1"])), Job::Publish => vec![step("the SDK crates on crates.io", || publish(root))], }; let failed: Vec<&Step> = steps.iter().filter(|s| s.verdict.is_err()).collect(); @@ -561,7 +548,10 @@ fn guest(root: &Path, suite: &[String]) -> Vec { // inherits this, and nothing here reads the environment concurrently with // the write. std::env::set_var("TMPDIR", tmp.path()); - let mut steps = vec![step("the instrument", || instrument(root, GUEST_ARCH))]; + let mut steps: Vec = Arch::ALL + .iter() + .map(|&arch| step(&format!("the {} instrument", arch.name()), || instrument(root, arch))) + .collect(); if steps.iter().all(|s| s.verdict.is_ok()) { steps.push(step("the toolchain", || release::install(root))); } @@ -605,19 +595,21 @@ fn verdicts(log: &str) -> String { } } -/// The QEMU on `PATH` against `.github/qemu-version`, the firmware it declares, -/// and whether `/dev/kvm` opens where it is present — the three things a guest -/// verdict must be read against. +/// The QEMU on `PATH` that boots `arch` against `.github/qemu-version`, the +/// firmware it declares, and whether `/dev/kvm` opens where it is present and +/// `arch` is the host's — the three things a guest verdict must be read against. fn instrument(root: &Path, arch: Arch) -> Result { let want = declared_qemu_version(root).ok_or(".github/qemu-version declares no version")?; let have = qemu_version(arch)?; let firmware = crate::firmware::of(arch)?; let node = Path::new("/dev/kvm").exists(); - let accelerated = arch.accel().is_hardware(); - let accel = match (node, accelerated) { - (true, true) => "/dev/kvm opens", - (true, false) => "/dev/kvm is present and does not open", - (false, _) => "no /dev/kvm: emulated", + let native = Arch::HOST == Some(arch); + let accel = match (native, arch.accel(), node) { + (false, _, _) => "another architecture's machine: emulated", + (true, Accel::Kvm, _) => "/dev/kvm opens", + (true, Accel::Hvf, _) => "Hypervisor.framework", + (true, Accel::Tcg, true) => "/dev/kvm is present and does not open", + (true, Accel::Tcg, false) => "no /dev/kvm: emulated", }; let cpu = std::fs::read_to_string("/proc/cpuinfo") .ok() @@ -639,7 +631,7 @@ fn instrument(root: &Path, arch: Arch) -> Result { instrument moved" )); } - if node && !accelerated { + if native && node && !arch.accel().is_hardware() { return Err(format!("{line}: every boot would fall back to emulation in silence")); } Ok(line) @@ -805,11 +797,11 @@ mod tests { } #[test] - fn a_job_is_named_and_a_shard_is_a_shard() { + fn a_job_is_named_and_takes_nothing_after_it() { assert_eq!(parse(&words("host")), Ok(Job::Host)); - assert_eq!(parse(&words("guest 3/12")), Ok(Job::Guest("3/12".into()))); - assert!(parse(&words("guest")).is_err()); - assert!(parse(&words("guest 13/12")).is_err()); + assert_eq!(parse(&words("guest")), Ok(Job::Guest)); + assert!(parse(&words("guest 3/12")).is_err()); + assert!(parse(&words("tcg")).is_err()); assert!(parse(&words("host extra")).is_err()); assert!(parse(&words("smoke")).is_err()); assert!(parse(&[]).is_err()); diff --git a/src/clippy.rs b/src/clippy.rs index 29a8f34f5b5..62dcc90ebb1 100644 --- a/src/clippy.rs +++ b/src/clippy.rs @@ -39,13 +39,12 @@ struct Shape { const INSTRUMENTS: &str = "debug-wait,sched-check,sched-tripwire,heap-tripwire,heap-sweep,\ pass-spin,stack-witness,switch-witness,switch-witness-mutate-frame,\ switch-witness-mutate-rsp,df-witness,df-witness-mutate,\ - entry-df-unclean,fpu-save-nothing,user-writable-gsbase"; + entry-df-unclean"; /// [`INSTRUMENTS`] less the direction-flag three, which are x86-64's alone. const AARCH64_INSTRUMENTS: &str = "debug-wait,sched-check,sched-tripwire,heap-tripwire,heap-sweep,\ pass-spin,stack-witness,switch-witness,\ - switch-witness-mutate-frame,switch-witness-mutate-rsp,\ - fpu-save-nothing,user-writable-gsbase"; + switch-witness-mutate-frame,switch-witness-mutate-rsp"; const UNCONTROLLED: &[&str] = &["toyos-pcid/counting-allocator", "toyos-sched/tripwire"]; diff --git a/src/fingerprint.rs b/src/fingerprint.rs deleted file mode 100644 index 3217d9e0747..00000000000 --- a/src/fingerprint.rs +++ /dev/null @@ -1,124 +0,0 @@ -//! What a disk this system was not given is compared against, before and after -//! a boot. -//! -//! **The whole device, not the places a format is expected to write.** A write -//! is a write wherever it lands, and a fingerprint of the two ends is green -//! over every byte between them. -//! -//! One SHA-256 per [`BLOCK`] rather than one over the file, so a difference -//! still says where. The images are ~128 MiB and sparse, so the cost is one -//! sequential read of a file the test just wrote. - -use std::io::Read; -use std::path::Path; - -use sha2::{Digest, Sha256}; - -/// The span one digest covers, and the resolution a difference is reported at. -pub const BLOCK: u64 = 1024 * 1024; - -/// One 32-byte digest per [`BLOCK`], **to the end of the file rather than to a -/// declared length**: a device that grew was written to, and the last block is -/// digested short, so a size change either way moves the fingerprint. -pub fn whole_device(path: &Path) -> Vec { - let mut file = std::fs::File::open(path) - .unwrap_or_else(|e| panic!("open {} to fingerprint: {e}", path.display())); - let mut out = Vec::new(); - let mut buf = vec![0u8; BLOCK as usize]; - loop { - let mut got = 0; - while got < buf.len() { - match file.read(&mut buf[got..]) { - Ok(0) => break, - Ok(n) => got += n, - Err(e) => panic!("read {} to fingerprint: {e}", path.display()), - } - } - if got == 0 { - break; - } - out.extend_from_slice(&Sha256::digest(&buf[..got])); - if got < buf.len() { - break; - } - } - out -} - -/// Where two fingerprints first differ, rendered for a failure message. -pub fn first_difference(before: &[u8], after: &[u8]) -> Option { - if before == after { - return None; - } - let at = before.iter().zip(after).position(|(a, b)| a != b); - Some(match at { - Some(at) => { - let block = at as u64 / 32; - format!( - "the {BLOCK}-byte block at offset {} changed", - block * BLOCK - ) - } - None => format!( - "the device changed length: {} block(s) of digest against {}", - before.len() / 32, - after.len() / 32 - ), - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::io::{Seek, SeekFrom, Write}; - use toyos_tmpdir::TempDir; - - /// A sparse file of `len` bytes, and the directory that holds it. - fn sparse(len: u64) -> (TempDir, std::path::PathBuf) { - let dir = TempDir::new("fingerprint"); - let path = dir.join("device.img"); - let file = std::fs::File::create(&path).expect("create"); - file.set_len(len).expect("size"); - (dir, path) - } - - /// Every block is covered, and the failure says which one. - #[test] - fn a_write_anywhere_changes_the_fingerprint() { - const LEN: u64 = 8 * BLOCK; - let (_dir, path) = sparse(LEN); - let before = whole_device(&path); - assert_eq!( - before.len() as u64, - 32 * LEN / BLOCK, - "one digest per block, and every block of the device" - ); - assert_eq!(first_difference(&before, &whole_device(&path)), None); - - let mut file = std::fs::OpenOptions::new().write(true).open(&path).expect("open"); - file.seek(SeekFrom::Start(LEN / 2)).expect("seek"); - file.write_all(&[1]).expect("write"); - drop(file); - - let diff = first_difference(&before, &whole_device(&path)) - .expect("a byte at the midpoint is a byte the device did not have"); - assert!(diff.contains(&format!("offset {}", LEN / 2)), "{diff}"); - } - - /// A device that came back a different size is a difference and not a - /// panic, whichever way it moved. - #[test] - fn a_device_that_changed_size_is_a_difference_either_way() { - const LEN: u64 = 3 * BLOCK; - let (_dir, path) = sparse(LEN); - let before = whole_device(&path); - - std::fs::File::options().write(true).open(&path).unwrap().set_len(BLOCK).unwrap(); - let shorter = first_difference(&before, &whole_device(&path)).expect("shorter"); - assert!(shorter.contains("changed length"), "{shorter}"); - - std::fs::File::options().write(true).open(&path).unwrap().set_len(4 * BLOCK).unwrap(); - let longer = first_difference(&before, &whole_device(&path)).expect("longer"); - assert!(longer.contains("changed length"), "{longer}"); - } -} diff --git a/src/image.rs b/src/image.rs index b17abed3d9a..a4a3a897a86 100644 --- a/src/image.rs +++ b/src/image.rs @@ -286,32 +286,6 @@ fn cmdline_with_root(root: FsUuid, params: &str) -> String { } } -/// Why a boot may not arm `asked` on the image at `path`, or `None` because -/// that image is armed with exactly that list. -/// -/// **An image carries the actuators it was built with**, in the boot parameter -/// on its marked slot's volume — the file the bootloader reads, holds to the -/// slot's signature and hands the kernel in `KernelArgs`. So what a guest will -/// be armed with is a fact about the image, answerable before anything starts -/// and without asking the guest; a caller that has an image and a list can be -/// told it is holding two different boots. -/// -/// Pure, and every input a parameter, so both directions can be staged without -/// a guest — which is what `an_image_says_what_it_is_armed_with` does. -pub fn param_conflict(path: &Path, asked: &[&str]) -> Option { - let baked = match params_of(path) { - Ok(baked) => baked, - Err(why) => return Some(why), - }; - if baked.iter().map(String::as_str).eq(asked.iter().copied()) { - return None; - } - Some(format!( - "the image {} is armed with {baked:?} and the boot asks for {asked:?}", - path.display() - )) -} - /// The actuator list an image is armed with, read back off the image. /// /// `root=` is not an actuator and is on every image: this answers what a boot @@ -385,101 +359,6 @@ pub fn read_file_on(file: &mut std::fs::File, guid: [u8; 16], name: &str) -> Res Ok(bytes) } -/// Put `update`'s sections into slot `which` of the disk image at `path` and -/// mark it — what `/system/bin/update` does on a machine, **with nothing -/// checked**: a test's way to put a slot in front of the loader that the -/// updater would refuse to write. `signed: false` leaves the slot without its -/// signed header. -pub fn stage_slot(path: &Path, which: toyos_update::slots::Which, update: &[u8], signed: bool) -> Result<(), String> { - use std::io::Write; - let parts = toyos_update::image::Parts::split(update).map_err(|why| format!("the update image: {why}"))?; - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(path) - .map_err(|e| format!("open {}: {e}", path.display()))?; - let (table, copy, table_at) = table_on(&mut file)?; - let slot = table.slot(which).ok_or_else(|| format!("{} carries no slot {}", path.display(), which.letter()))?; - - let (root_at, root_len) = partition_extent(&mut file, slot.root)?; - if parts.root.len() as u64 > root_len { - return Err(format!("ROOT is {} bytes and slot {}'s partition {root_len}", parts.root.len(), which.letter())); - } - file.seek(SeekFrom::Start(root_at)) - .and_then(|_| file.write_all(parts.root)) - .map_err(|e| format!("writing slot {}'s ROOT: {e}", which.letter()))?; - - let (boot_at, boot_len) = partition_extent(&mut file, slot.boot)?; - let mut volume = vec![0u8; boot_len as usize]; - file.seek(SeekFrom::Start(boot_at)) - .and_then(|_| file.read_exact(&mut volume)) - .map_err(|e| format!("reading slot {}'s volume: {e}", which.letter()))?; - { - let time = build_time(); - let mut fs = Fat32::mount(VolumeIo(&mut volume)).map_err(|e| format!("slot {}'s volume: {e}", which.letter()))?; - fs.create_dir_all("toyos", time).map_err(|e| format!("toyos/: {e}"))?; - let mut files: Vec<(&str, &[u8])> = vec![ - (toyos_update::slots::KERNEL_FILE, parts.kernel), - (toyos_update::slots::CMDLINE_FILE, parts.cmdline), - ]; - if signed { - files.push((toyos_update::slots::SIGNED_FILE, &parts.signed[..])); - } - for name in [toyos_update::slots::KERNEL_FILE, toyos_update::slots::CMDLINE_FILE, toyos_update::slots::SIGNED_FILE] { - if fs.exists(name).map_err(|e| format!("{name}: {e}"))? { - fs.remove(name).map_err(|e| format!("removing {name}: {e}"))?; - } - } - for (name, bytes) in files { - let mut f = fs.create(name, time).map_err(|e| format!("creating {name}: {e}"))?; - fs.write(&mut f, 0, bytes).map_err(|e| format!("writing {name}: {e}"))?; - fs.flush_meta(&mut f, time).map_err(|e| format!("recording {name}: {e}"))?; - } - fs.sync().map_err(|e| format!("syncing slot {}'s volume: {e}", which.letter()))?; - } - file.seek(SeekFrom::Start(boot_at)) - .and_then(|_| file.write_all(&volume)) - .map_err(|e| format!("writing slot {}'s volume: {e}", which.letter()))?; - - let mut next = table; - next.marked = which; - let mut marked = slot; - marked.version = parts.header.version; - next.slots[which.index()] = Some(marked); - let (to, block) = toyos_update::slots::next_write((table, copy), next); - file.seek(SeekFrom::Start(table_at + (to * toyos_update::slots::BLOCK) as u64)) - .and_then(|_| file.write_all(&block)) - .and_then(|_| file.sync_all()) - .map_err(|e| format!("writing the slot table: {e}")) -} - -/// Make `edit` of the slot table the disk image at `path` carries, as a writer -/// does — the copy that is not current, one sequence past it — **with nothing -/// checked**: a test's way to put a table in front of init that the updater -/// holding the grant could write. -pub fn restage_table(path: &Path, edit: impl FnOnce(&mut toyos_update::slots::Table)) -> Result<(), String> { - use std::io::Write; - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(path) - .map_err(|e| format!("open {}: {e}", path.display()))?; - let (table, copy, at) = table_on(&mut file)?; - let mut next = table; - edit(&mut next); - let (to, block) = toyos_update::slots::next_write((table, copy), next); - file.seek(SeekFrom::Start(at + (to * toyos_update::slots::BLOCK) as u64)) - .and_then(|_| file.write_all(&block)) - .and_then(|_| file.sync_all()) - .map_err(|e| format!("writing the slot table: {e}")) -} - -/// The unique GUID of the one partition of type `kind` on the disk image -/// `file`, as a GPT entry stores it. -pub fn unique_guid_of(file: &mut std::fs::File, kind: toyos_gpt::Guid) -> Result<[u8; 16], String> { - only_partition(&mut FileSectors(file), kind).map(|part| part.unique_guid().0) -} - /// Why a scan's `out[0]` and `matched` count did not pick out exactly one /// partition, once the table itself was readable. pub enum OnePartitionError { @@ -516,41 +395,6 @@ pub fn only_partition(disk: &mut dyn toyos_gpt::Sectors, kind: toyos_gpt::Guid) }) } -/// Overwrite the file `name` on the FAT partition `guid` of the disk image at -/// `path` with `bytes`, exactly its length, **writing its data clusters and -/// nothing else** — so a guest running on the image that does not write that -/// file sees nothing else of its volume move. -pub fn overwrite_file_on(path: &Path, guid: [u8; 16], name: &str, bytes: &[u8]) -> Result<(), String> { - use std::io::Write; - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(path) - .map_err(|e| format!("open {}: {e}", path.display()))?; - let (start, len) = partition_extent(&mut file, guid)?; - let mut volume = vec![0u8; usize::try_from(len).map_err(|_| format!("a {len}-byte volume"))?]; - file.seek(SeekFrom::Start(start)) - .and_then(|_| file.read_exact(&mut volume)) - .map_err(|e| format!("reading the volume at byte {start}: {e}"))?; - let mut fs = Fat32::mount(VolumeIo(&mut volume)).map_err(|e| format!("the volume does not mount: {e}"))?; - let found = fs.open(name).map_err(|e| format!("the volume has no {name}: {e}"))?; - if found.len() != bytes.len() as u64 { - return Err(format!("{name} is {} bytes, and this writes {} in place", found.len(), bytes.len())); - } - let mut rest = bytes; - for extent in fs.extents(name, usize::MAX).map_err(|e| format!("{name}'s clusters: {e}"))? { - let n = rest.len().min(extent.len as usize); - file.seek(SeekFrom::Start(start + extent.offset)) - .and_then(|_| file.write_all(&rest[..n])) - .map_err(|e| format!("writing {name} at byte {}: {e}", start + extent.offset))?; - rest = &rest[n..]; - } - if !rest.is_empty() { - return Err(format!("{name}'s clusters hold {} bytes fewer than its length", rest.len())); - } - file.sync_all().map_err(|e| format!("syncing {}: {e}", path.display())) -} - /// The slot table on the disk image `file`, which copy is current, and where /// its partition starts. fn table_on(file: &mut std::fs::File) -> Result<(toyos_update::slots::Table, usize, u64), String> { @@ -875,49 +719,6 @@ pub fn designate_data_disk(path: &Path, len: u64) -> (u64, u64) { (start, bytes) } -/// Lay a table on the disk at `path` carrying one TOYOS-DATA partition aligned -/// to a sector rather than a page, so its start lands where the primary table -/// ends and not on a 4096-byte boundary. `over_candidate` refuses that view -/// before anything beneath it is read, and the GPT type still names the -/// partition ours. Answers the byte offset it landed at. -pub fn misaligned_data_disk(path: &Path, len: u64) -> u64 { - let Some(data_bytes) = len.checked_sub(PARTITION_ALIGN as u64).filter(|b| *b > 0) else { - panic!("a {len}-byte disk has no room for a misaligned DATA partition"); - }; - - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(path) - .unwrap_or_else(|e| panic!("open {} to partition it: {e}", path.display())); - let mbr = - gpt::mbr::ProtectiveMBR::with_lb_size(u32::try_from(len / 512 - 1).unwrap_or(0xFF_FF_FF_FF)); - mbr.overwrite_lba0(&mut file).expect("write the protective MBR"); - - let mut gdisk = gpt::GptConfig::default() - .initialized(false) - .writable(true) - .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) - .create_from_device(Box::new(file), None) - .expect("create a GPT on the data disk"); - gdisk - .update_partitions(BTreeMap::::new()) - .expect("initialize the data disk's partition table"); - // One sector, not `PARTITION_ALIGN`: the partition lands at the first - // usable LBA, right after the primary table, which is not a page boundary. - let id = gdisk - .add_partition("ToyOS data", data_bytes, TOYOS_DATA, 0, Some(1)) - .expect("add the data partition"); - let placed = gdisk.partitions().get(&id).expect("the partition was just added"); - let start = placed - .bytes_start(gpt::disk::LogicalBlockSize::Lb512) - .expect("the data partition's start"); - assert_ne!(start % SECTOR as u64, 0, "the partition landed on a page boundary by accident"); - - gdisk.write().expect("write the data disk's GPT"); - start -} - /// Block 0 of a volume: the magic and its block count. fn designation(blocks: u64) -> [u8; SECTOR] { let mut block = [0u8; SECTOR]; @@ -927,16 +728,6 @@ fn designation(blocks: u64) -> [u8; SECTOR] { block } -/// Where the one TOYOS-DATA partition on `path` is, by the parser the kernel -/// selects it with. -pub fn data_partition_of(path: &Path) -> Result<(u64, u64), String> { - let mut file = - std::fs::File::open(path).map_err(|e| format!("open {}: {e}", path.display()))?; - let part = only_partition(&mut FileSectors(&mut file), toyos_gpt::Guid::TOYOS_DATA) - .map_err(|why| format!("{}: {why}", path.display()))?; - Ok((part.first_lba() * u64::from(LBA), part.lba_count().get() * u64::from(LBA))) -} - /// A disk file as logical blocks, for a reader that may not hold the image. pub(crate) struct FileSectors<'a>(pub(crate) &'a mut std::fs::File); @@ -1375,58 +1166,6 @@ mod tests { assert_eq!(parts.signed, &signed); } - /// An image says which actuators a guest booting it would arm, and the - /// answer comes out of the image rather than from whoever built it. - /// - /// **This is what makes a staged boot image answerable.** The actuators are - /// baked in at build time, so a caller supplying its own image cannot arm - /// anything by asking, and a green run with an inert arm is the worst kind - /// of harness defect: every negative control staged through one proves - /// nothing. Both directions, because the reader is the writer's inverse. - #[test] - fn an_image_says_what_it_is_armed_with() { - let dir = toyos_tmpdir::TempDir::new("image-params"); - let root_image = tiny_root(); - let write = |name: &str, params: &str| { - let path = dir.join(name); - std::fs::write(&path, create_boot_image(Arch::X86_64, b"kernel", b"bootloader", &root_image, params, signing(&key()), None)) - .expect("write an image"); - path - }; - - // What every shipping image is: nothing armed at all. - let shipping = write("shipping.img", ""); - // Two, because a reader that handed back the whole file as one name - // would answer every one-actuator question correctly. - let armed = write("armed.img", "usb-flush-fails,fat-boot-reads-fail"); - - for (image, asked) in [ - (&shipping, &[][..]), - (&armed, &["usb-flush-fails", "fat-boot-reads-fail"][..]), - ] { - assert_eq!( - param_conflict(image, asked), - None, - "an image was refused the list it was built with: {asked:?}" - ); - } - - // An actuator armed beside an image built without it names both sides: - // the reader gets the message and nothing else. - for (image, asked, name) in [ - (&shipping, &["usb-flush-fails"][..], "usb-flush-fails"), - (&armed, &[][..], "fat-boot-reads-fail"), - (&armed, &["usb-flush-fails"][..], "fat-boot-reads-fail"), - ] { - let why = param_conflict(image, asked) - .unwrap_or_else(|| panic!("{asked:?} was accepted on {}", image.display())); - assert!( - why.contains(name), - "the refusal does not name {name}, which is the whole of what it is about: {why}" - ); - } - } - /// **One ordering of one set is one image.** The judge below compares /// `root=` against the superblock the same build stamped, so it is blind to /// this: a `root_uuid` returning a constant satisfies it. This is the arm diff --git a/src/lan.rs b/src/lan.rs index 36b8769e5e7..06129ec5212 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -16,20 +16,12 @@ pub const MAC: &str = "netd: MAC "; pub const LEASE: &str = "netd: DHCP: lease "; pub const LINK_UP: &str = "netd: I219: link up at "; pub const READY: &str = "netd: ready, at most "; -pub const NO_LEASE: &str = "netd: DHCP: no lease as "; /// The name this machine asks its network to record for it, and answers for as /// `.local`; held to netd's own `dhcp::HOSTNAME` by /// [`tests::netd_declares_the_name_this_module_spells`]. pub const HOSTNAME: &str = "toyos-t14"; -/// RFC 2132 §3.14: the kind, the length, and the name. -fn host_name_option() -> Vec { - let mut option = vec![12, HOSTNAME.len() as u8]; - option.extend_from_slice(HOSTNAME.as_bytes()); - option -} - /// One lease, as the record carries it. #[derive(Debug, PartialEq, Eq)] pub struct Lease { @@ -186,67 +178,6 @@ pub fn delivered(text: &str) -> Result { Err(why) } -/// **The one place the host-name option can be read.** A server that ignores it -/// answers the same lease either way, so the frames the client sent are the only -/// evidence that it asked at all — and `filter-dump` records both directions, so -/// a frame counts only where it is IPv4 over UDP *leaving* the client's own port. -pub fn asked_under_its_own_name(pcap: &[u8]) -> Result<(), String> { - let option = host_name_option(); - let sent = dhcp_client_frames(pcap)?; - if !sent.iter().any(|frame| frame.windows(option.len()).any(|w| w == option)) { - return Err(format!( - "none of the {} frame(s) this client sent a DHCP server carries the host-name \ - option {option:?}", - sent.len() - )); - } - Ok(()) -} - -/// The transaction ID (RFC 2131 §2, `xid`) of every frame the DHCP client -/// sent, in the order it sent them: what its random source drew. -pub fn dhcp_transaction_ids(pcap: &[u8]) -> Result, String> { - dhcp_client_frames(pcap)? - .iter() - .map(|frame| match frame.get(DHCP_AT + 4..DHCP_AT + 8) { - Some(xid) => Ok(u32::from_be_bytes(xid.try_into().expect("four bytes"))), - None => Err(format!("a {}-byte frame the DHCP client sent ends before its xid", frame.len())), - }) - .collect() -} - -/// Where a DHCP message begins in a frame: behind Ethernet, an IPv4 header -/// carrying no options, and UDP. -const DHCP_AT: usize = 14 + 20 + 8; - -/// Every frame of `pcap` that is IPv4 over UDP *leaving* the DHCP client's own -/// port, carrying anything: `filter-dump` records both directions. -fn dhcp_client_frames(pcap: &[u8]) -> Result, String> { - const LITTLE_ENDIAN_PCAP: [u8; 4] = [0xd4, 0xc3, 0xb2, 0xa1]; - const GLOBAL_HEADER: usize = 24; - const RECORD_HEADER: usize = 16; - if pcap.get(..LITTLE_ENDIAN_PCAP.len()) != Some(&LITTLE_ENDIAN_PCAP[..]) { - return Err("this file does not open with a little-endian pcap header".to_string()); - } - let mut at = GLOBAL_HEADER; - let mut sent = Vec::new(); - while let Some(header) = pcap.get(at..at + RECORD_HEADER) { - let len = u32::from_le_bytes(header[8..12].try_into().expect("four bytes")) as usize; - let frame = pcap.get(at + RECORD_HEADER..at + RECORD_HEADER + len).ok_or_else(|| { - format!("this pcap's record at byte {at} names {len} bytes the file has not") - })?; - at += RECORD_HEADER + len; - if frame.len() > DHCP_AT - && frame[12..14] == [0x08, 0x00] - && frame[23] == 17 - && frame[34..36] == [0, 68] - { - sent.push(frame); - } - } - Ok(sent) -} - #[cfg(test)] mod tests { use super::*; @@ -511,92 +442,4 @@ mod tests { ); } } - - /// One pcap record per frame, with the timestamps a reader here never looks - /// at left zero. - fn pcap(frames: &[Vec]) -> Vec { - let mut out = vec![0xd4, 0xc3, 0xb2, 0xa1]; - out.extend_from_slice(&[0u8; 20]); - for frame in frames { - out.extend_from_slice(&[0u8; 8]); - out.extend_from_slice(&(frame.len() as u32).to_le_bytes()); - out.extend_from_slice(&(frame.len() as u32).to_le_bytes()); - out.extend_from_slice(frame); - } - out - } - - /// One frame: an ethertype, an IPv4 protocol, the two UDP ports, and a - /// payload. - fn frame(ethertype: [u8; 2], protocol: u8, src: u16, dst: u16, payload: &[u8]) -> Vec { - let mut frame = vec![0u8; 14 + 20 + 8]; - frame[12..14].copy_from_slice(ðertype); - frame[23] = protocol; - frame[34..36].copy_from_slice(&src.to_be_bytes()); - frame[36..38].copy_from_slice(&dst.to_be_bytes()); - frame.extend_from_slice(payload); - frame - } - - fn from_client(payload: &[u8]) -> Vec { - frame([0x08, 0x00], 17, 68, 67, payload) - } - - /// **The server's own echo of the option is not the client asking.** A walk - /// keyed on the destination port would count the echo below and report the - /// question as asked when nothing asked it. - #[test] - fn only_the_direction_leaving_the_client_counts() { - let option = host_name_option(); - assert_eq!(asked_under_its_own_name(&pcap(&[from_client(&option)])), Ok(())); - let echoed = frame([0x08, 0x00], 17, 67, 68, &option); - let why = asked_under_its_own_name(&pcap(std::slice::from_ref(&echoed))) - .expect_err("a server's reply is not this client asking"); - assert!(why.contains("none of the 0 frame(s)"), "{why}"); - // The same echo beside a client frame that asked nothing. - let why = asked_under_its_own_name(&pcap(&[echoed, from_client(&[53, 1, 1])])) - .expect_err("the one frame the client sent carried no name"); - assert!(why.contains("none of the 1 frame(s)"), "{why}"); - } - - #[test] - fn a_frame_that_is_not_ipv4_over_udp_carries_no_option_here() { - let option = host_name_option(); - // ARP, and IPv4 carrying TCP: both hold the bytes and neither is a - // DHCP request. - for stray in [ - frame([0x08, 0x06], 17, 68, 67, &option), - frame([0x08, 0x00], 6, 68, 67, &option), - ] { - let why = asked_under_its_own_name(&pcap(&[stray])).expect_err("not a DHCP frame"); - assert!(why.contains("none of the 0 frame(s)"), "{why}"); - } - // A frame with the headers and no payload at all. - let bare = from_client(&[]); - assert!(asked_under_its_own_name(&pcap(&[bare])).is_err()); - } - - /// RFC 2131 §2: `op`, `htype`, `hlen`, `hops`, then the four bytes of - /// `xid`, big-endian, of the frames the client sent and no other. - #[test] - fn the_transaction_id_is_read_off_each_frame_the_client_sent() { - let discover = from_client(&[1, 1, 6, 0, 0xde, 0xad, 0xbe, 0xef, 0, 0]); - let offer = frame([0x08, 0x00], 17, 67, 68, &[2, 1, 6, 0, 1, 2, 3, 4]); - let request = from_client(&[1, 1, 6, 0, 0x01, 0x02, 0x03, 0x04]); - assert_eq!(dhcp_transaction_ids(&pcap(&[discover, offer, request])), Ok(vec![0xdead_beef, 0x0102_0304])); - let why = dhcp_transaction_ids(&pcap(&[from_client(&[1, 1, 6, 0, 0xde])])).expect_err("no whole xid"); - assert!(why.contains("ends before its xid"), "{why}"); - } - - #[test] - fn a_file_that_is_not_a_pcap_and_a_record_past_its_end_are_refused_by_name() { - assert!(asked_under_its_own_name(b"").unwrap_err().contains("little-endian pcap")); - assert!( - asked_under_its_own_name(b"\xa1\xb2\xc3\xd4rest").unwrap_err().contains("pcap header") - ); - let mut truncated = pcap(&[from_client(&host_name_option())]); - truncated.truncate(truncated.len() - 4); - let why = asked_under_its_own_name(&truncated).expect_err("the last record is cut short"); - assert!(why.contains("bytes the file has not"), "{why}"); - } } diff --git a/src/lib.rs b/src/lib.rs index 1bb0c9c3579..06e45ea6a96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,6 @@ pub mod ci; pub mod clang; pub mod clippy; pub mod compiler; -/// What the untouched-disk gate compares a device against, in `tests/`. -pub mod fingerprint; pub mod firmware; #[cfg(test)] pub mod gitfixture; diff --git a/src/licence.rs b/src/licence.rs index 437c9ac1924..c309857aa25 100644 --- a/src/licence.rs +++ b/src/licence.rs @@ -412,12 +412,6 @@ pub const COMMITTED_FILES: &[(&str, &str, &str, Terms)] = &[ "NOTICE", Terms::Font("OFL-1.1"), ), - ( - "tests/fixtures/gbae-v0.2.0-toyos-x86_64.tar.gz", - "99fcd8a7263b5c25cd90cead1baaa7200ef272100fc2226e008a4e8205ba2916", - "NOTICE", - Terms::Spdx("MIT"), - ), ( "toyos-elf/tests/fixtures/toyos-ld-headers.bin", "6243d543a15941133514c1a8a24c79d118060caeae7e985870a67d9fc3021354", @@ -1325,7 +1319,6 @@ mod tests { assert!(names(&parse("MIT AND MPL-2.0").unwrap())); } - /// The kernel's `--kernel-feature` picks any feature it declares, so its /// graph is resolved with all of them; a `[programs]` row's /// `no-default-features` and libc's features reach metadata as the build diff --git a/src/metal.rs b/src/metal.rs index 7da42d01a19..5669ad49cc6 100644 --- a/src/metal.rs +++ b/src/metal.rs @@ -119,9 +119,8 @@ pub enum Refusal { Table(String), /// The table does not hold exactly one partition of a type the loop needs. Partitions { what: &'static str, matched: u32 }, - /// The image is armed with a parameter [`FLASHABLE`] does not clear for - /// this machine, or does not clear at all. - Armed { name: String, why: &'static str }, + /// The image is armed with a parameter [`FLASHABLE`] does not clear. + Armed { name: String }, /// **The image carries no bound on its own boot.** Every metal image is /// built with `boot-deadline=`; one without it is not a metal-staged /// image, and flashing it puts the machine somewhere only a hand gets it out @@ -261,11 +260,11 @@ impl fmt::Display for Refusal { `toyos-fat32-check` reading the volume off the stick, so what it names is \ what the kernel wrote and not what a driver read back" ), - Self::Armed { name, why } => write!( + Self::Armed { name } => write!( f, - "the image is armed with {name:?}, and {why}. Every parameter a flashed image \ - carries needs a row in `toyos_build::metal::FLASHABLE` saying whether this \ - machine survives it" + "the image is armed with {name:?}, and nothing in this tree has ruled on whether \ + the machine survives it. Every parameter a flashed image carries needs a row \ + in `toyos_build::metal::FLASHABLE`" ), Self::PartitionIndex { what, want, got } => write!( f, @@ -721,7 +720,8 @@ struct Flashable { log: Part, } -/// Whether a boot parameter may reach the machine, and why that was decided. +/// Every parameter the T14 survives — the boot ends and the machine is what it +/// was — and nothing else reaches the stick. /// /// **The metal profile flashes test images**, so an actuator is admissible here /// where `build::flashable_params` refuses it for the owner's own flash path. @@ -730,62 +730,53 @@ struct Flashable { /// machine somebody has to open a lid to repair. The internal NVMe is not such /// state — the T14 is a playground, and a disk a broken driver wipes is a disk /// the next install writes again. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Flash { - /// The T14 survives it: the boot ends and the machine is what it was. - Ok, - /// It does not ship in any image, for the reason given. - Never(&'static str), -} - -/// Every parameter this loop has ruled on, and nothing else reaches the stick. /// /// **A deny-list fails open on the next actuator**, so this is the whole /// judgement: an image armed with a name that has no row here is refused by /// that name rather than flashed on the assumption it is harmless. -pub const FLASHABLE: &[(&str, Flash)] = &[ +pub const FLASHABLE: &[&str] = &[ // The kernel's own boot parameters. Both are what a shipped image carries, // and `build::flashable_params` already lets the owner flash them. - ("watchdog", Flash::Ok), - ("early-panel", Flash::Ok), + "watchdog", + "early-panel", // It issues machine-wide TLB shootdowns from the BSP after the roster is // released and before the idle loop, and reports how long each took. It // reaches no device, writes no register outside `CR3`, and leaves nothing // behind: the boot goes on to userland and ends the way an unarmed one does. - ("tlb-shootdown-bench", Flash::Ok), + "tlb-shootdown-bench", // **The in-kernel self-tests.** Each stages inputs the hardware cannot // produce — a crafted PCI capability list, a malformed USB descriptor, a // vector nothing claims — runs a check over them in memory and prints a // count. None reaches a device register, none writes firmware state, and // the boot goes on to userland and ends the way an unarmed one does; what // an armed image leaves behind is a longer log. - ("pci-cap-selftest", Flash::Ok), - ("process-reopen-selftest", Flash::Ok), - ("revoked-backing-selftest", Flash::Ok), - ("leak-rollback-selftest", Flash::Ok), - ("lapic-spurious-selftest", Flash::Ok), - ("unclaimed-vector-selftest", Flash::Ok), - ("xhci-xecp-selftest", Flash::Ok), - ("xhci-descriptor-selftest", Flash::Ok), + "pci-cap-selftest", + "process-reopen-selftest", + "revoked-backing-selftest", + "leak-rollback-selftest", + "lapic-spurious-selftest", + "unclaimed-vector-selftest", + "xhci-xecp-selftest", + "xhci-descriptor-selftest", // Two probes rather than staged inputs, and both are reads: the SS-reload // one runs inside the first syscall's own context switch, and the input-core one merges // events it made up itself. - ("sysret-ss-probe", Flash::Ok), - ("test-input-merge", Flash::Ok), + "sysret-ss-probe", + "test-input-merge", // Three nested `scheduler::Operation`s with known deadlines, in both homes, // each printing what it asked for and what it observed. It establishes and // drops them and reaches nothing else. - ("sched-operation-nesting", Flash::Ok), + "sched-operation-nesting", // It seals this boot's own record under an identity one bit from this // stick's, so the pass that finds it clears it and boots a kernel. The page // is memory the loader allocated and the machine is what it was after. - (FOREIGN_RECORD_ARM, Flash::Ok), + FOREIGN_RECORD_ARM, // **The one arm that deliberately stops this machine.** At the shutdown // syscall, after the job list, every CPU stops taking scheduler passes. // Admissible only because `kernel/src/deadline.rs` is what ends it, which // [`arms_are_admissible`] refuses an image without: it reaches no device // register, writes no firmware state, and the boot after it is ordinary. - (WEDGE_ARM, Flash::Ok), + WEDGE_ARM, // **The other arm that deliberately stops this machine, and it stops one // CPU harder.** It takes a lock of its own, clears `IF` on the last CPU and // never gives either back, which is the state this machine hung in for @@ -794,38 +785,24 @@ pub const FLASHABLE: &[(&str, Flash)] = &[ // the boot deadline is still armed behind that. It reaches no device // register and writes no firmware state; the kernel implies `WEDGE_ARM` // behind it, so the boot cannot end itself before its own bound. - (LOCKUP_ARM, Flash::Ok), + LOCKUP_ARM, // **The arm that stops nothing and never stops writing**, so the reset // lands on a controller that is moving bytes. Admissible for the rows // above's reason and one more: every run is read first and written back // byte for byte in the last eighth of the disk, once and never twice, so // the medium is what it was and no partition a boot mounts is the subject; // and the sweep is refused by name on a disk with no room for it. - (LOAD_ARM, Flash::Ok), + LOAD_ARM, // It withholds transfers to the boot stick so the transport breaks on // purpose. Admissible because it writes nothing the stick did not already // hold, reaches no firmware state, and the worst // it leaves is a stick a replug clears — the defect the arm exists to stage. - ("usb-transport-break", Flash::Ok), + "usb-transport-break", // It deafens one CPU for a window of its own clock and has the blocked-task // dump kick it and probe it with an NMI. It reaches no device register and // writes no firmware state; the CPU rejoins, and the boot goes on to // userland and ends the way an unarmed one does. - ("dump-deaf-cpu", Flash::Ok), - ( - "quiesce-late-word", - Flash::Never( - "it holds the shutdown open after the boot's last word, which is the one window a \ - metal verdict is read across — an image armed with it stages its own red", - ), - ), - ( - "xhci-lock-wedged", - Flash::Never( - "it makes the shutdown skip the disk-cache flush, so the boot's own log may never \ - reach the media it is read off — and a stick is the one channel out of this machine", - ), - ), + "dump-deaf-cpu", ]; /// The arm that stops the machine, named once: [`FLASHABLE`] rules on it and @@ -868,16 +845,13 @@ pub fn clears_its_own_page(armed: &[impl AsRef]) -> bool { armed.iter().any(|name| name.as_ref() == FOREIGN_RECORD_ARM) } -/// [`FLASHABLE`]'s ruling on `name`, or `None` where nobody has made one. -pub fn flash_ruling(name: &str) -> Option { +/// Whether [`FLASHABLE`] clears `name`. +pub fn flashable(name: &str) -> bool { // The two parameters that carry a value are not names: the black-box page's // address, which every image the harness builds has, and the boot // deadline's bound. Neither arms an instrument, and the second is what ends // a boot this loop would otherwise wait 360 s for and then need a hand on. - if crate::build::is_valued_param(name) { - return Some(Flash::Ok); - } - FLASHABLE.iter().find(|(row, _)| *row == name).map(|(_, verdict)| *verdict) + crate::build::is_valued_param(name) || FLASHABLE.contains(&name) } /// The pre-flash gate: what the image is armed with, judged before it is @@ -904,21 +878,10 @@ pub fn judge_arms(armed: &[String]) -> Result<(), Refusal> { if !armed.iter().any(|name| name.starts_with(toyos_tco::DEADLINE_PARAM)) { return Err(Refusal::NoBound { staged_a_wedge: stages_a_wedge(armed) }); } - for name in armed { - match flash_ruling(name) { - Some(Flash::Ok) => {} - Some(Flash::Never(why)) => { - return Err(Refusal::Armed { name: name.clone(), why }) - } - None => { - return Err(Refusal::Armed { - name: name.clone(), - why: "nothing in this tree has ruled on whether the machine survives it", - }) - } - } + match armed.iter().find(|name| !flashable(name)) { + Some(name) => Err(Refusal::Armed { name: name.clone() }), + None => Ok(()), } - Ok(()) } /// Whole sectors, `EFI PART` in the *final* one, and exactly one partition of @@ -2599,7 +2562,7 @@ mod tests { /// **The gate refuses an image with no bound and clears the bound itself.** /// Two rules meeting on one token: `judge_arms` asks for a `boot-deadline=` - /// and then asks `flash_ruling` about every arm including that one, so a + /// and then asks `flashable` about every arm including that one, so a /// bound the ruling table did not clear would make every metal image /// unflashable. It is cleared as one of `build::VALUED_PARAMS`, and this is /// what holds the two together. @@ -2607,7 +2570,7 @@ mod tests { fn the_bound_the_gate_demands_is_a_bound_the_gate_clears() { let bound = alloc_deadline(); assert!(crate::build::is_valued_param(&bound), "{bound}"); - assert_eq!(flash_ruling(&bound), Some(Flash::Ok)); + assert!(flashable(&bound)); // And it is exactly what `tests/common/metal.rs` arms every image with: // the same two constants, so a change to either moves both. assert!(bound.starts_with(toyos_tco::DEADLINE_PARAM)); @@ -2727,13 +2690,10 @@ mod tests { /// refused *by that name* rather than by a count. #[test] fn an_arm_with_no_ruling_never_reaches_the_stick() { - assert_eq!(flash_ruling("watchdog"), Some(Flash::Ok)); - assert_eq!(flash_ruling("blackbox=0x8000000"), Some(Flash::Ok)); - assert_eq!(flash_ruling("nvme-write-selftest"), None); - let refusal = Refusal::Armed { - name: "nvme-write-selftest".to_string(), - why: "nothing in this tree has ruled on whether the machine survives it", - }; + assert!(flashable("watchdog")); + assert!(flashable("blackbox=0x8000000")); + assert!(!flashable("nvme-write-selftest")); + let refusal = Refusal::Armed { name: "nvme-write-selftest".to_string() }; let said = refusal.to_string(); assert!(said.contains("nvme-write-selftest"), "{said}"); assert!(said.contains("FLASHABLE"), "{said}"); @@ -2750,7 +2710,7 @@ mod tests { fn every_arm_that_stops_this_machine_is_cleared_and_judged_as_one() { let bound = alloc_deadline(); for arm in WEDGE_ARMS { - assert_eq!(flash_ruling(arm), Some(Flash::Ok), "{arm} reaches no stick"); + assert!(flashable(arm), "{arm} reaches no stick"); assert_eq!(judge_arms(&[arm.to_string(), bound.clone()]), Ok(()), "{arm}"); assert!(stages_a_wedge(&[arm.to_string()]), "{arm}"); // And with no bound behind it, the sharpest refusal names it as the @@ -2773,9 +2733,9 @@ mod tests { let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); let mut declared = crate::build::declared_actuators(root); declared.extend(crate::build::declared_params(root)); - for (name, _) in FLASHABLE { + for name in FLASHABLE { assert!( - declared.iter().any(|d| d == name), + declared.iter().any(|d| d == *name), "`FLASHABLE` rules on {name:?}, which the kernel declares as neither an \ actuator nor a boot parameter: {declared:?}" ); diff --git a/src/metaldevices.rs b/src/metaldevices.rs index f912f6cdbf8..95a5ac2c538 100644 --- a/src/metaldevices.rs +++ b/src/metaldevices.rs @@ -92,65 +92,10 @@ pub fn quiesced(loader: &str) -> Option { }) } -/// What one job's `exit:` record said. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Exit { - pub code: i64, - pub cpu_ms: u64, -} - -/// The `exit: pid=N code=N cpu=Nms` record for `name`, or `None` where -/// the boot has none — which is a job that never ran, never returned, or was -/// still running when the machine reset. -/// -/// The **last** such record, because a name could in principle run twice and -/// the boot's answer is the one it ended with. The record's whole message and -/// not a substring of a line, so a program's line — which opens with the head -/// `logd` gives it, never a kernel record's bracket — is never one. -pub fn exit_of(log: &str, name: &str) -> Option { - let head = format!("{}{name} pid=", crate::bootlog::EXIT); - log.lines().rev().find_map(|line| { - let rest = crate::bootlog::message(line)?.strip_prefix(&head)?; - let code = field(rest, "code=")?.parse().ok()?; - let cpu = field(rest, "cpu=")?; - let cpu_ms = cpu.strip_suffix("ms")?.parse().ok()?; - Some(Exit { code, cpu_ms }) - }) -} - -/// The word after `key` in `rest`, up to the next space. -fn field<'a>(rest: &'a str, key: &str) -> Option<&'a str> { - rest.split(key).nth(1)?.split_whitespace().next() -} - #[cfg(test)] mod tests { use super::*; - /// A boot log's shape, as `logd` writes one: the wall-clock tag, the - /// boot-relative stamp, the CPU, then the record. - fn line(at: &str, text: &str) -> String { - format!("[2026-09-07 06:45:03 {at} cpu0] {text}\n") - } - - fn a_good_boot() -> String { - [ - line("0.000", "PAT: IA32_PAT=0x0007040100070406, entry 4 = WC"), - line("0.087", "xHCI: found at PCI 00:14.0 8086:a36d"), - line("0.090", "xHCI: max_slots=32 max_ports=16 ctx_size=64 pagesize=0x1"), - line("0.140", "usb-storage: 1 device(s)"), - line("0.150", "i8042: ok selftest=0x55 cfg=0x45->0x44 port1=ok port2=ok"), - "{2026-09-29 08:43:12 1.162 blockd} blockd: no NVMe controller this row names is on \ - this machine; serving no partition\n" - .to_string(), - line("0.319", "GOP: scanout memory type WC (MTRR WB, PAT entry 4)"), - line("0.368", "Boot: complete (368ms)"), - line("1.100", "exit: usbwrite pid=6 code=402000 cpu=140ms"), - line("2.100", "exit: usbread pid=7 code=30500 cpu=90ms"), - ] - .concat() - } - /// `loader.log`'s pass after the reset, with the stop's tail and the /// shutdown's own account under the `|` the loader prefixes a report's /// lines with. @@ -170,26 +115,6 @@ mod tests { .to_string() } - #[test] - fn an_exit_record_is_read_as_its_number() { - let log = a_good_boot(); - assert_eq!(exit_of(&log, "usbwrite"), Some(Exit { code: 402_000, cpu_ms: 140 })); - assert_eq!(exit_of(&log, "never_ran"), None); - // A name that is a prefix of another's is not that other one. - assert_eq!(exit_of(&log, "usb"), None); - // The last of two, because that is the answer the boot ended with. - let twice = format!("{log}{}", line("4.0", "exit: usbread pid=11 code=99 cpu=1ms")); - assert_eq!(exit_of(&twice, "usbread"), Some(Exit { code: 99, cpu_ms: 1 })); - // A program writing the record's words, and a whole record's line, - // after it. - let forged = format!( - "{twice}{{2026-09-08 16:08:23 5.000 evil}} exit: usbread pid=11 code=0 cpu=0ms\n\ - {{2026-09-08 16:08:23 5.100 evil}} {}", - line("5.1", "exit: usbread pid=11 code=0 cpu=0ms"), - ); - assert_eq!(exit_of(&forged, "usbread"), Some(Exit { code: 99, cpu_ms: 1 })); - } - #[test] fn the_shutdowns_own_account_is_read_out_of_the_loaders_file() { assert_eq!( diff --git a/src/metaltalk.rs b/src/metaltalk.rs index ccec7cf8dd3..2482d7e4214 100644 --- a/src/metaltalk.rs +++ b/src/metaltalk.rs @@ -301,23 +301,6 @@ impl Stream { state = self.shared.moved.wait_timeout(state, left).expect("the stream's state").0; } } - - /// Wait until the latest connection ends, or `by` has passed; whether it - /// ended. - pub fn wait_ended(&self, by: Duration) -> bool { - let began = Instant::now(); - let mut state = self.state(); - loop { - if state.end.is_some() { - return true; - } - let left = by.saturating_sub(began.elapsed()); - if left.is_zero() || (state.unopened.is_some() && state.current.is_none()) { - return false; - } - state = self.shared.moved.wait_timeout(state, left).expect("the stream's state").0; - } - } } /// The reader: the first dial by `until`, then each redial it is asked for, @@ -1175,6 +1158,25 @@ mod tests { use super::*; use std::net::Shutdown; + impl Stream { + /// Wait until the latest connection ends, or `by` has passed; whether it + /// ended. + fn wait_ended(&self, by: Duration) -> bool { + let began = Instant::now(); + let mut state = self.state(); + loop { + if state.end.is_some() { + return true; + } + let left = by.saturating_sub(began.elapsed()); + if left.is_zero() || (state.unopened.is_some() && state.current.is_none()) { + return false; + } + state = self.shared.moved.wait_timeout(state, left).expect("the stream's state").0; + } + } + } + fn heard(exec: Result, reboot: Result) -> Heard { let said = Conversation { peer: Ipv4Addr::new(192, 168, 1, 49), diff --git a/src/sourcegate.rs b/src/sourcegate.rs index fea680e8ab9..aab4b89221b 100644 --- a/src/sourcegate.rs +++ b/src/sourcegate.rs @@ -161,7 +161,6 @@ const GUEST_CODE: &[&str] = &[ "toyos-abi/src", "userland", "tests/toyos-rust-tests", - "tests/iced-counter", "tests/testcases", ]; diff --git a/src/testargs.rs b/src/testargs.rs index 32300e76b6a..898767a8433 100644 --- a/src/testargs.rs +++ b/src/testargs.rs @@ -8,97 +8,12 @@ use crate::flags::declare_flags; use std::path::PathBuf; -/// One machine's slice of the suite. -/// -/// A shard is a *host*, never a lane. `--jobs` divides one machine's cores -/// between guests that contend for them; this divides the work between machines -/// that share nothing, which is the only lever CI has and the one the dev host -/// does not have at all. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct Shard { - /// One-based, as it is written on the command line and in a job matrix. - pub index: usize, - pub count: usize, -} - -impl Shard { - /// Drop everything another shard owns out of `pools`, keeping the order of - /// what is left: the items are dealt in turn, so the `n`th of the pools' - /// concatenation, counting from zero, is shard `n % count + 1`'s. - /// - /// **A rule over positions and nothing else**, so every process that builds - /// the same lists takes the same partition of them, and every item lands in - /// exactly one shard, which is the property a verdict depends on. One count - /// runs across the pools, so a later pool's first item goes to the shard - /// after the one the earlier pool's last went to and the counts stay within - /// one of each other. - pub fn keep(self, pools: &mut [&mut Vec]) { - let mut dealt = 0; - for pool in pools.iter_mut() { - pool.retain(|_| { - let mine = dealt % self.count == self.index - 1; - dealt += 1; - mine - }); - } - } -} - -/// `--shard /`, or `None` for the whole suite. -/// -/// `Err` is a refusal to print and exit on, like [`parse`]'s: a shard number -/// outside its range would take no tests and report the run green. -pub fn parse_shard(args: &[String]) -> Result, String> { - let Some(spec) = SUITE.value(args, &SHARD) else { - return Ok(None); - }; - let (index, count) = spec - .split_once('/') - .ok_or_else(|| format!("--shard {spec}: not /, e.g. 2/4"))?; - let index: usize = index - .parse() - .map_err(|_| format!("--shard {spec}: {index:?} is not a shard number"))?; - let count: usize = count - .parse() - .map_err(|_| format!("--shard {spec}: {count:?} is not a shard count"))?; - if !(1..=count).contains(&index) { - return Err(format!( - "--shard {spec}: shards are numbered 1 through {count}, and a run outside \ - that range would take no tests and report itself green" - )); - } - Ok(Some(Shard { index, count })) -} - -/// Refuse a shard that owns nothing after the ordinary suite's filter -/// and task grouping have all been applied. -/// -/// A valid shard number is not enough to establish that the selected suite has -/// at least that many bins. The check therefore belongs after `Shard::keep`, -/// where `total` is the number of verdicts this process can actually produce. -pub fn validate_ordinary_shard( - shard: Option, - filter: Option<&str>, - total: usize, -) -> Result<(), String> { - let Some(shard) = shard else { return Ok(()) }; - if total > 0 { - return Ok(()); - } - Err(format!( - "--shard {}/{} with filter {filter:?} owns no ordinary tests after selection; \ - refusing a false-green shard run", - shard.index, shard.count, - )) -} - declare_flags!(pub SUITE = { pub DEBUG = "--debug", None; pub LIST = "--list", None; pub NOCAPTURE = "--nocapture", None; pub JOBS = "--jobs", Next; pub JOBS_SHORT = "-j", Next; - pub SHARD = "--shard", Next; /// The metal profile: the registrations that run on the T14, batched into /// images and judged off the log the stick came back with. pub METAL = "--metal", None; @@ -106,9 +21,6 @@ declare_flags!(pub SUITE = { /// machine is not touched**: the run builds the images and writes down what /// to run on them, or judges readbacks a driver already left there. pub METAL_READBACK = "--metal-readback", Next; - /// The owner `guest_dies_with_its_harness` kills: the image it names, - /// booted and held until stdin ends. Alone on its line. - pub HOLD = "--hold", Next; }); /// The run's filter and `--metal`'s mode, both decided by [`parse`]: an unknown @@ -136,7 +48,6 @@ pub fn parse(args: &[String]) -> Result, String> { if let Some(refusal) = line.malformed() { return Err(refusal); } - let flags = line.seen.len(); let mut filter: Option<&str> = None; for word in line.positionals { @@ -167,7 +78,7 @@ pub fn parse(args: &[String]) -> Result, String> { ); } if has(&METAL) { - for flag in [&SHARD, &JOBS, &JOBS_SHORT] { + for flag in [&JOBS, &JOBS_SHORT] { if has(flag) { return Err(format!( "{} beside --metal: the metal profile reads no {}, so it would be dropped \ @@ -202,13 +113,6 @@ pub fn parse(args: &[String]) -> Result, String> { ); } } - if has(&HOLD) && (flags != 1 || filter.is_some()) { - return Err( - "--hold boots the image it names and holds it, and reads nothing else on the line; \ - every other word would be dropped in silence" - .to_string(), - ); - } let metal = has(&METAL).then(|| { if has(&LIST) { @@ -254,7 +158,7 @@ mod tests { #[test] fn a_deleted_flag_is_refused_rather_than_becoming_the_filter() { for (flag, value) in - [("--skip", "desktop_window_child"), ("--host-slots", "0"), ("--host-builds", "0")] + [("--skip", "x"), ("--host-slots", "0"), ("--host-builds", "0"), ("--shard", "2/12")] { let refusal = parse_owned(&[flag, value]).unwrap_err(); assert!(refusal.starts_with(&format!("{flag}:")), "{refusal}"); @@ -287,77 +191,6 @@ mod tests { assert!(refusal.contains("\"futex\"") && refusal.contains("\"dlopen\""), "{refusal}"); } - fn shard_of(args: &[&str]) -> Result, String> { - parse_shard(&owned(args)) - } - - #[test] - fn a_shard_is_index_and_count() { - assert_eq!(shard_of(&["--shard", "2/4"]).unwrap(), Some(Shard { index: 2, count: 4 })); - assert_eq!(shard_of(&["--shard=1/1"]).unwrap(), Some(Shard { index: 1, count: 1 })); - assert_eq!(shard_of(&[]).unwrap(), None); - } - - /// The failure with no symptom: a shard nobody owns runs nothing, and a run - /// that ran nothing exits 0. - #[test] - fn a_shard_outside_its_range_is_refused() { - for spec in ["0/4", "5/4", "2/0"] { - let refusal = shard_of(&["--shard", spec]).unwrap_err(); - assert!(refusal.contains("green"), "{spec}: {refusal}"); - } - assert!(shard_of(&["--shard", "half"]).is_err()); - assert!(shard_of(&["--shard", "x/4"]).is_err()); - } - - #[test] - fn an_empty_selected_shard_is_a_named_false_green() { - let shard = Some(Shard { index: 8, count: 12 }); - let refusal = validate_ordinary_shard(shard, Some("one_test"), 0).unwrap_err(); - assert!(refusal.contains("--shard 8/12"), "{refusal}"); - assert!(refusal.contains("filter Some(\"one_test\")"), "{refusal}"); - assert!(refusal.contains("false-green"), "{refusal}"); - - assert!(validate_ordinary_shard(shard, None, 1).is_ok()); - assert!(validate_ordinary_shard(None, Some("nothing"), 0).is_ok()); - } - - /// The property every verdict rests on: the shards are a partition. Not one - /// test may be dropped by all of them, and none may be run by two. - #[test] - fn every_item_lands_in_exactly_one_shard() { - let (first, second): (Vec, Vec) = ((0..97).collect(), (97..105).collect()); - for count in 1..=8 { - let mut seen: Vec = Vec::new(); - let mut sizes = Vec::new(); - for index in 1..=count { - let (mut a, mut b) = (first.clone(), second.clone()); - Shard { index, count }.keep(&mut [&mut a, &mut b]); - sizes.push(a.len() + b.len()); - seen.extend(a.into_iter().chain(b)); - } - seen.sort_unstable(); - assert_eq!(seen, (0..105).collect::>(), "count {count}"); - let (fewest, most) = (sizes.iter().min(), sizes.iter().max()); - assert!(most.zip(fewest).is_some_and(|(m, f)| m - f <= 1), "count {count}: {sizes:?}"); - } - } - - /// **One deal across the pools.** Two pools of `[0, 1, 2]` and `[3, 4]` over - /// two shards: the deal goes on from where the first pool stopped, so the - /// second pool's first item is shard 2's, where a deal restarted per pool - /// would hand shard 1 both pools' first items. - #[test] - fn a_later_pool_is_dealt_on_from_where_the_earlier_stopped() { - let taken = |index| { - let (mut a, mut b) = (vec![0, 1, 2], vec![3, 4]); - Shard { index, count: 2 }.keep(&mut [&mut a, &mut b]); - (a, b) - }; - assert_eq!(taken(1), (vec![0, 2], vec![4])); - assert_eq!(taken(2), (vec![1], vec![3])); - } - /// Every `None` here is a default the run then takes in silence: `--jobs` /// the built-in width. #[test] @@ -408,13 +241,9 @@ mod tests { vec!["process_stats", "--nocapture"], vec!["--list"], vec!["--jobs", "4"], - vec!["--shard", "2/4"], - vec!["--shard", "2/12", "--jobs", "1"], vec!["--debug"], vec!["--metal"], vec!["--metal", "--metal-readback", "target/metal"], - vec!["--hold", "boot.img"], - vec!["--hold=boot.img"], ] { assert!(parse_owned(&argv).is_ok(), "{argv:?}"); } @@ -447,7 +276,7 @@ mod tests { #[test] fn metal_refuses_a_filter_that_is_a_flags_name_or_empty() { - for word in ["list", "metal", "jobs", "shard", "debug", "metal-readback"] { + for word in ["list", "metal", "jobs", "debug", "metal-readback"] { let refusal = metal_owned(&["--metal", word]).expect_err(word); assert!(refusal.contains("without its dashes"), "{word}: {refusal}"); } @@ -463,11 +292,8 @@ mod tests { for argv in [ &["--metal", "-j", "--list"][..], &["--metal", "--jobs", "--list"], - &["--metal", "--shard", "--list"], - &["--list", "--metal", "--shard", "2/4"], &["--list", "--metal", "-j", "4"], &["--list", "--metal", "--jobs", "4"], - &["--metal", "--shard", "2/4"], &["--metal", "-j", "4"], ] { let refusal = metal_owned(argv).expect_err(&format!("{argv:?} was accepted")); @@ -497,17 +323,4 @@ mod tests { let refusal = parse_owned(&["--metal", "--bogus", "--list"]).unwrap_err(); assert!(refusal.contains("--bogus"), "{refusal}"); } - - #[test] - fn hold_is_alone_on_its_line() { - for argv in [ - &["--hold", "boot.img", "boot"][..], - &["--hold", "boot.img", "--list"], - &["-j", "2", "--hold", "boot.img"], - &["--hold"], - ] { - let refusal = parse_owned(argv).unwrap_err(); - assert!(refusal.contains("--hold"), "{argv:?}: {refusal}"); - } - } } diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index 68753b57ba7..d49800353d5 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -7,18 +7,17 @@ The mechanics live where the work is: profiles and shapes in `tests/common/`, re - **The dev host is a laptop that sleeps mid-session, and the suite says so** — a run whose wall clock jumped against the monotonic one reports `INVL` per test and exits 2: re-run. A wild outlier *not* marked that way is a real finding. - **A machine-wide kernel panic reds whichever test was running** — that red's name is the workload, never the cause. `QEMU died before ===READY=== (status 0)` is the same thing said silently: a guest that reset itself is a kernel death, and its evidence is the boot log. - **A machine-wide death during boot is reproduced by boots, not by suites** — parallel `bootable.img` guests, each waited on its completion marker and never on a fixed timer; the baseline is measured in the same session as the arm; a death counts whether or not a marker printed, so run guests with `-action reboot=shutdown -action shutdown=pause` and read a silent one's registers over QMP; a T14 block that gained a CI container mid-run is discarded, never corrected; a defect whose rate is set by interrupts per unit of guest work is measured on the *slowest* instrument — TCG can be the stronger oracle. -- **A C test's capture has other processes' lines removed before comparison**, on the boot config's list of who may speak (`tests/common/console.rs`); a line without a trailing newline is unjoined from the next writer's there too. - **`/system/bin/init` speaks in every program's name before that program runs** — a predicate keyed on a `: ` prefix is satisfied by the wrong speaker; wait for the whole line, in the constant the assertion also reads. - **A guest binary cannot ask what a handle it does not hold does** — the probe ends its caller with exit 139, so it runs in a child, one fault per child. -- **A boot's capture has two pieces** — `boot_log()` ends at the ready marker, `run_test`'s capture begins at `===TEST_START===`, and the lines between land in `TestResult::before`; a test reading a daemon's boot line appends it. -- **Every guest this host boots is TCG** — anything vendor-dependent is gated only by CI's KVM shards, and TCG prices an uncontended atomic read-modify-write unlike hardware. -- **CI's `guest` lane is GitHub-hosted shards, never the T14** — the T14 is the orchestrator's own metal loop (`src/metal.rs`), reached by nothing in `.github/workflows/`. +- **A boot's capture has two pieces** — `boot_log()` ends at the ready marker, `run_test`'s capture begins at `===TEST_START===`. +- **Every guest this host boots is TCG** — anything vendor-dependent is gated only by CI's KVM lane, and TCG prices an uncontended atomic read-modify-write unlike hardware. +- **CI's `guest` lane is GitHub-hosted, never the T14** — the T14 is the orchestrator's own metal loop (`src/metal.rs`), reached by nothing in `.github/workflows/`. - **The dev host's guests boot `-cpu qemu64`, which has no PCID** — every `INVPCID` path is dead locally, so a change gated on a CPUID feature is unverified by a green local suite. - **A liveness ceiling scales by two host facts** — boot-derived host speed *and* the guest's own `vcpus/cores` oversubscription. Widen a *liveness* guard for this, never a correctness bound. - **A wedge verdict needs both the budget spent and the guest gone quiet** — a healthy idle guest can be silent for minutes, and a guest still talking past its budget is slow, not stuck; only a far backstop stands behind a guest that keeps talking. - **A measured bound is asserted against the derivation, never against the measurement** — a bound that has to be widened to pass is a finding. A test asserting a kernel `Budget` never expires asserts a bound the kernel does not promise; the red is only the outcome that is neither the answer nor the declared degradation. - **A crafted-input test asserts the harm before the return value, and never Debug-prints a refused value** — an unrefused one is as large as the input asked for. -- **A stimulus sent through a channel that can silently lose it is verified before its effect is asserted** — QEMU's PS/2 queue drops the seventeenth byte, so typed input paces against the guest's report (`shell_type_once`); a guest's console reaches the host as whole lines only, so a partial line exists on no channel. +- **A stimulus sent through a channel that can silently lose it is verified before its effect is asserted** — QEMU's PS/2 queue drops the seventeenth byte, so typed input paces against the guest's report; a guest's console reaches the host as whole lines only, so a partial line exists on no channel. - **A harness field that can be silently inert is this suite's worst defect class** — where two options can describe the same guest they refuse each other by name, and an image is asked what it is armed with. - **A test whose premise is arranged by a defect passes for the wrong reason** — a staging device's resource has to survive its own enumeration. - **Host suites** are the root `Cargo.toml`'s `[workspace]` members, the SDK and every userland crate `src/userlandhost.rs` finds a test in; `src/ci.rs`'s `host` runs them all. `kernel-loom/` and `toyos-sched/loom` are the memory-ordering checks — x86 TSO hides a missing acquire edge from every guest test. diff --git a/tests/blockdcase/system.toml b/tests/blockdcase/system.toml deleted file mode 100644 index 8ffc8f2b350..00000000000 --- a/tests/blockdcase/system.toml +++ /dev/null @@ -1,69 +0,0 @@ -# The blockd boot: `tests/testcases`'s estate, and `/system/bin/blockd` in the -# image started by nothing. `test_rs_blockd_io` is blockd's supervisor as init -# is a service's: it mints the claim on the machine's second NVMe controller -# from its capability, starts blockd holding it and a port it made, and is the -# only thing that can end or restart it. Nothing drives the first controller, -# so every command in QEMU's NVMe trace is that blockd's. The disks are crafted -# by `tests/common/blockd.rs`. No soundd: `test_rs_blockd_io dma-pool` claims -# virtio-sound itself, to lend the pool its kernel driver keeps. - -[boot] -start = ["logd", "fsd", "test-runner"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does. The kernel keeps the record ring and writes no file at all, so a -# boot config without `logd` is a boot whose `/log` is empty — -# `every_boot_config_runs_logd` is what refuses one. -# It claims no device and serves no port: its whole authority -# is `logread`, which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate. -[programs.logd] -syscap = ["logread"] - -# The test estate's authority. -# `device` because five of the guest binaries claim the keyboard or the mouse -# and no manifest row can name them — they are not `[programs]` keys — and `dup` -# because a claim moves and one boot runs several of them. -# `power` because `run shutdown` is how a dozen host-side gates end their guest -# and read what reached the volume, and test-runner spawns `/system/bin/shutdown` -# directly — it holds no `launcher` connector, so the applet's authority is the -# dup it is endowed here rather than the `toybox` row. -# `roster` because four guest binaries read `SYS_SYSINFO`'s per-thread entries — -# soundd's, for the idle-suspend certification, and their own, which arrive in -# the same machine-wide answer and have no narrower question in the ABI. It is -# also what `endowment_denied` narrows *away* to prove the refusal, so an estate -# without it would make that arm vacuous rather than red. -[programs.test-runner] -syscap = ["device", "dup", "logread", "power", "roster"] - -[programs.toybox] - -[symlinks] -"bin/cat" = "/system/bin/toybox" -"bin/cp" = "/system/bin/toybox" -"bin/echo" = "/system/bin/toybox" -"bin/free" = "/system/bin/toybox" -"bin/grep" = "/system/bin/toybox" -"bin/hexdump" = "/system/bin/toybox" -"bin/ls" = "/system/bin/toybox" -"bin/mkdir" = "/system/bin/toybox" -"bin/mv" = "/system/bin/toybox" -"bin/ps" = "/system/bin/toybox" -"bin/pwd" = "/system/bin/toybox" -"bin/reboot" = "/system/bin/toybox" -"bin/rm" = "/system/bin/toybox" -"bin/shutdown" = "/system/bin/toybox" -# The shipped audio client, and the one the T14 hangs on. The raw-API tone in -# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only -# that one certified a path no user takes. -"bin/tone" = "/system/bin/toybox" - -# The NVMe driver, from userland. No row starts it and it holds nothing here: -# the test that runs it hands it its claim and its port itself. -[programs.blockd] - -# The file servers: the log and the running slot's volume off the stick, and -# DATA in memory, since no block service runs. Started again when one ends, on -# the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] diff --git a/tests/checks.rs b/tests/checks.rs index 2a43cf4eb59..05f9544e77d 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -276,11 +276,9 @@ mod checks { /// [`control_regs`] against machines this host cannot boot, with no guest. /// - /// [`control_regs_negative`] runs the real defective machine and is the link - /// between this verdict and a kernel; what is here is the states no actuator - /// reaches — a CPU that differs from three others, a bit set uniformly on all - /// four, an AP that never printed. Every value is one this tree has printed or - /// one bit away from it. + /// What is here is the states no actuator reaches — a CPU that differs from + /// three others, a bit set uniformly on all four, an AP that never printed. + /// Every value is one this tree has printed or one bit away from it. #[test] fn control_regs_verdict() -> Result<(), String> { const AP_BEFORE: (u64, u64) = (0xe000_0011, 0x0031_0620); @@ -578,97 +576,58 @@ mod checks { Ok(()) } - fn shared_row(name: &str) -> TestDef { - TestDef { - name: name.to_string(), - qemu_name: format!("test_rs_{name}"), - timeout: Duration::from_secs(1), - check: |_| true, - settle: no_settle, - } - } - - /// A run takes every registered test its filter matches, and a shard drops - /// exactly the screen rows whose profile is not of [`toyos_build::ci::GUEST_ARCH`], - /// saying which. + /// A run takes every declared test its filter matches, of either + /// architecture. #[test] - fn a_run_selects_by_filter_and_shard() -> Result<(), String> { - let shared = [shared_row("shared_one")]; - let taken = |filter: Option<&str>, sharded: bool| -> BTreeSet { - let (tests, machine, screen) = select(&shared, filter, sharded); - tests + fn a_run_selects_by_filter() -> Result<(), String> { + let taken = |filter: Option<&str>| -> BTreeSet { + let (machine, screen) = select(filter); + machine .iter() - .map(|t| t.name.clone()) - .chain(machine.iter().map(|(n, _)| n.to_string())) - .chain(screen.iter().map(|(n, _, _)| n.to_string())) + .map(|n| n.to_string()) + .chain(screen.iter().map(|(n, _)| n.to_string())) .collect() }; let names = |of: &[&str]| -> BTreeSet { of.iter().map(|n| n.to_string()).collect() }; - let every: BTreeSet = declared().chain(["shared_one"]).map(String::from).collect(); - let foreign: BTreeSet = SCREEN_TESTS - .iter() - .filter(|(_, _, profile)| profile.arch() != toyos_build::ci::GUEST_ARCH) - .map(|(n, _, _)| String::from(*n)) - .collect(); - if !foreign.contains("virt_el2_drop") { - return Err(format!("the premise: virt_el2_drop is a guest no CI lane boots, and {foreign:?} lacks it")); - } + let every: BTreeSet = declared().map(String::from).collect(); let cases = [ - (None, false, every.clone()), - (None, true, every.difference(&foreign).cloned().collect()), - (Some("virt_el2"), false, names(&["virt_el2_drop"])), - (Some("el2_drop"), false, names(&["virt_el2_drop"])), - (Some("virt_el2"), true, BTreeSet::new()), - (Some("sshd_"), true, names(&["sshd_exec", "sshd_files", "sshd_key_auth"])), - (Some("shared_one"), true, names(&["shared_one"])), + (None, every), + (Some("virt_el2"), names(&["virt_el2_drop"])), + (Some("el2_drop"), names(&["virt_el2_drop"])), + (Some("nested_nmi"), names(&["nested_nmi_is_loud"])), + (Some("no_such_test"), BTreeSet::new()), ]; - for (filter, sharded, want) in cases { - let got = taken(filter, sharded); + for (filter, want) in cases { + let got = taken(filter); if got != want { return Err(format!( - "filter {filter:?}, sharded {sharded}: took {:?} it should not and left out {:?}", + "filter {filter:?}: took {:?} it should not and left out {:?}", got.difference(&want).collect::>(), want.difference(&got).collect::>() )); } } - let named = |filter: Option<&str>| -> Option<(String, BTreeSet)> { - let line = arch_drop_line(&shared, filter)?; - let (_, rows) = line.rsplit_once(": ").expect("the line names its rows after a colon"); - let rows = rows.split(", ").map(String::from).collect(); - Some((line, rows)) - }; - let (line, rows) = - named(None).ok_or("a shard that drops the rows of another architecture said nothing")?; - if rows != foreign || !line.starts_with(&format!("{} test(s)", foreign.len())) { - return Err(format!("a shard dropping {foreign:?} said {line:?}")); - } - let named_el2 = named(Some("el2_drop")).map(|(_, rows)| rows); - if named_el2 != Some(names(&["virt_el2_drop"])) { - return Err(format!("filter el2_drop: a shard said {named_el2:?}")); - } - if let Some((line, _)) = named(Some("sshd_")) { - return Err(format!("a filter matching no foreign row still had a shard say {line:?}")); - } Ok(()) } - /// Two rows under one name are refused, whether both are shared-boot rows - /// or one is a declared registry's. + /// Two rows under one name are refused, whether both are shared-boot names + /// or one is a declared registry's or the metal table's. #[test] fn a_name_registered_twice_is_refused() -> Result<(), String> { - let apart = [shared_row("shared_one"), shared_row("shared_two")]; + let shared = |of: &[&str]| -> Vec { of.iter().map(|n| n.to_string()).collect() }; + let apart = shared(&["shared_one", "shared_two"]); let names = registered(&apart)?; - for name in ["shared_one", "shared_two", "virt_el2_drop"] { + for name in ["shared_one", "shared_two", "virt_el2_drop", "control_regs"] { if !names.contains(name) { return Err(format!("{name} is not among the {} registered names", names.len())); } } for twice in [ - [shared_row("shared_one"), shared_row("shared_one")], - [shared_row("shared_one"), shared_row("virt_el2_drop")], + shared(&["shared_one", "shared_one"]), + shared(&["shared_one", "virt_el2_drop"]), + shared(&["shared_one", "control_regs"]), ] { - let twice_name = &twice[1].name; + let twice_name = &twice[1]; match registered(&twice) { Err(refusal) if refusal.contains(&format!("{twice_name} is registered twice")) => {} other => { @@ -764,8 +723,8 @@ mod checks { /// The judge a metal registration runs. fn metal_judge(name: &str) -> fn(&[&metal::Readback]) -> Result<(), String> { match METAL.iter().find(|(row, _)| *row == name) { - Some((_, metal::Metal::Runs { judge, .. })) => *judge, - _ => panic!("{name} runs no metal judge"), + Some((_, metal::Metal { judge, .. })) => *judge, + None => panic!("{name} runs no metal judge"), } } diff --git a/tests/checks/metal.rs b/tests/checks/metal.rs index fbcfa465905..bcfa00b2c6f 100644 --- a/tests/checks/metal.rs +++ b/tests/checks/metal.rs @@ -157,19 +157,19 @@ fn unqualified(b: &[&Readback]) -> Result<(), String> { } static PASSING: Metal = - Metal::Runs { arms: &[metal::once("passing", "tests/jobcase", &[], &[])], judge: span }; -static FAILING: Metal = Metal::Runs { + Metal { arms: &[metal::once("passing", "tests/jobcase", &[], &[])], judge: span }; +static FAILING: Metal = Metal { arms: &[metal::once("failing", "tests/jobcase", &[], &[])], judge: span_and_fail, }; static REFUSED: Metal = - Metal::Runs { arms: &[metal::once("refused", "tests/jobcase", &[], &[])], judge: span }; + Metal { arms: &[metal::once("refused", "tests/jobcase", &[], &[])], judge: span }; static LATE: Metal = - Metal::Runs { arms: &[metal::once("late", "tests/jobcase", &[], &[])], judge: span }; + Metal { arms: &[metal::once("late", "tests/jobcase", &[], &[])], judge: span }; static ONE: Metal = - Metal::Runs { arms: &[metal::once("one", "tests/jobcase", &[], &[])], judge: unqualified }; + Metal { arms: &[metal::once("one", "tests/jobcase", &[], &[])], judge: unqualified }; static TWO: Metal = - Metal::Runs { arms: &[metal::once("two", "tests/jobcase", &[], &[])], judge: unqualified }; + Metal { arms: &[metal::once("two", "tests/jobcase", &[], &[])], judge: unqualified }; /// **The record is one function of the readbacks.** A boot the loop refused, a /// boot a riding test failed and a boot whose own check failed are each diff --git a/tests/checks/qemu.rs b/tests/checks/qemu.rs index 18f7bb32f36..3c91ab032e6 100644 --- a/tests/checks/qemu.rs +++ b/tests/checks/qemu.rs @@ -32,7 +32,7 @@ pub fn host_scale_self_check() -> Result<(), String> { // Finite in the worst case the suite can reach: eight vCPUs on a single // core is 8x, not unbounded — so a genuine hang still reports in bounded // time. `budget_smp` composes this with `budget`'s own capped host_scale - // (<=8x) and phase width, and on the `--jobs 1` runner width is 1. + // (<=8x) and the run's width, and on the `--jobs 1` runner width is 1. if oversub_ratio(8, 1) != (8, 1) { return Err(format!("the worst suite case must stay finite at 8x, got {:?}", oversub_ratio(8, 1))); } @@ -153,9 +153,7 @@ pub fn ceiling_self_check() -> Result<(), String> { const TIGHT: Duration = Duration::from_secs(153); let bstop = TIGHT.max(GUEST_WEDGED); assert!(TIGHT < bstop, "the case needs a ceiling below the backstop"); - // (a) The flake itself: `launcher_refusals` at `192s "still talking 1s ago"` - // on a loaded smp:2 runner. Past its 153 s budget, but talking — no - // verdict, it runs on. + // (a) Past its 153 s budget, but talking — no verdict, it runs on. if ceiling_verdict(None, Duration::from_secs(192), TIGHT, Duration::from_secs(1), 500).is_some() { return Err(String::from( diff --git a/tests/checks/screen.rs b/tests/checks/screen.rs index 604592ea5b6..2dbcdab8ae7 100644 --- a/tests/checks/screen.rs +++ b/tests/checks/screen.rs @@ -42,58 +42,4 @@ pub fn self_test() { let decoded = Ppm::parse(&ppm).text(); let expected = lines.map(|l| l.trim_end()).join("\n"); assert_eq!(decoded, expected, "screen decoder round-trip failed"); - - console_self_test(); -} - -/// The same round trip for the console's font, and one thing the kernel's -/// cannot have: the two tables must not decode each other. `ConsoleFont::load` -/// has already refused an ambiguous printable-ASCII table by the time this -/// runs. -fn console_self_test() { - let font = ConsoleFont::load(); - let lines = [ - "[kernel 0.099] i8042: ok selftest=0x55 cfg=0x77->0x64 port1=ok port2=ok", - "/> echo hello", - "the quick brown fox JUMPS over 13 lazy dogs {}[]<>|~", - ]; - let cols = lines.iter().map(|l| l.len()).max().unwrap(); - let width = cols * GLYPH_W; - let height = lines.len() * GLYPH_H; - // White on black: `draw_char`'s blend then reduces to the alpha itself, - // which is what makes the decode exact rather than a nearest match. - let mut pixels = vec![[0u8, 0, 0]; width * height]; - for (row, line) in lines.iter().enumerate() { - for (col, ch) in line.chars().enumerate() { - let cell = font.by_cell.iter().find(|(_, c)| **c == ch).expect("a glyph for every char staged").0; - for r in 0..GLYPH_H { - for c in 0..GLYPH_W { - let a = cell[r * GLYPH_W + c]; - pixels[(row * GLYPH_H + r) * width + col * GLYPH_W + c] = [a, a, a]; - } - } - } - } - - let mut ppm = format!("P6\n{width} {height}\n255\n").into_bytes(); - for p in &pixels { - ppm.extend_from_slice(p); - } - let dump = Ppm::parse(&ppm); - let expected = lines.map(|l| l.trim_end()).join("\n"); - assert_eq!( - dump.console_text(&font), - expected, - "console screen decoder round-trip failed" - ); - - // The non-vacuity property the console tests lean on, measured rather than - // argued: a screen the *kernel* painted carries the thresholded form of - // these glyphs, and the two tables are not interchangeable in either - // direction. - assert!( - !dump.text().contains("i8042: ok selftest"), - "the kernel's 1-bit table decodes anti-aliased console glyphs, so a \ - console test could pass on a screen the console never touched" - ); } diff --git a/tests/checks/serial.rs b/tests/checks/serial.rs index 5c2c60e9c65..e333daf7ada 100644 --- a/tests/checks/serial.rs +++ b/tests/checks/serial.rs @@ -93,7 +93,7 @@ pub fn self_check() -> Result<(), String> { // `must_say_after`, against the capture that made it exist: a stranger line // of the right shape before the marker, and the test's own after it. The // first case is the defect and is asserted in both directions — the plain - // scan reads the stranger, which is what `i8042_undecoded_bytes` did. + // scan reads the stranger. const READY: &str = "===I8042_READY==="; let stranger = "[kernel 0.418 cpu1] i8042: 1 interrupts and 0 bytes, nothing decoded — first \ seen at 418ms"; diff --git a/tests/common/blockd.rs b/tests/common/blockd.rs deleted file mode 100644 index 678770aef5c..00000000000 --- a/tests/common/blockd.rs +++ /dev/null @@ -1,732 +0,0 @@ -//! blockd, the NVMe driver in userland, judged off the disk it wrote and off -//! the device's own trace. -//! -//! The guest (`tests/toyos-rust-tests/src/bin/blockd_io.rs`) is blockd's -//! supervisor and client both. What it cannot judge about itself is what -//! reached the medium and what the controller was actually sent, so both are -//! read here after the guest is gone: the image, with the host's own readers — -//! `toyos-fat32-check` (fatgen103's rules) and the `fatfs` crate, neither of -//! which is the code that wrote the volume — and QEMU's trace of every NVMe -//! command, completion and flush, which no driver can print on the device's -//! behalf. -//! -//! The machine has two NVMe controllers: the first (QEMU's own ids), which no -//! driver runs on this boot, and blockd's (Intel's ids) with the partitions -//! below. - -use std::collections::{BTreeMap, BTreeSet}; -use std::io::{Read, Seek, SeekFrom, Write}; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use super::partclaim::{self, Part, Span, ALIGNED, NEIGHBOUR_TYPE, PLAIN_TYPE}; -use super::qemu::{BootOptions, QemuInstance, TestResult}; -use super::serial::Serial; - -/// Mirrored in the guest: blockd's disk. -const TARGET: &str = "9C4E2A71-5B3D-4F18-A6E0-2D7C8B1F3E59"; -const FS: &str = "B2D4F6A8-1C3E-4A57-9B0D-E2F4A6C8E0A1"; -const BENCH: &str = "C3E5A7B9-2D4F-4B68-8C1E-F3A5B7D9F1B2"; -const MISALIGNED: &str = "E5A7C9DB-4F6B-4D8A-8E30-B5C7D9FB13D4"; -const MISSTART: &str = "F6B8DAEC-5A7C-4E9B-9F41-C6D8EA0C24E5"; -const TARGET_BLOCKS: u64 = 2048; -const BENCH_BLOCKS: u64 = 8192; -const FILES: usize = 6; -const FILE_BYTES: usize = 48 * 1024; -const AFTER: &str = "AFTER.BIN"; - -const BLOCK: u64 = 4096; -const MIB: u64 = 1024 * 1024; -/// blockd's namespace's sector, which QEMU's trace counts an LBA in. -const SECTOR: u64 = 512; -/// Mirrored in the guest: a region, and the addresses a device domain has -/// under `iommu-domain-narrow` (`vtd::table::NARROW_BYTES`). -const REGION: u64 = 2 * MIB; -const NARROW: u64 = 128 * MIB; -const CONFIG: &str = "tests/blockdcase"; - -/// Mirrored: block `n` of a region `salt` names. -fn pattern(salt: u8, n: u64) -> Vec { - let mut block = vec![0u8; BLOCK as usize]; - for (i, byte) in block.iter_mut().enumerate() { - *byte = (n as usize).wrapping_mul(131).wrapping_add(i).wrapping_add(salt as usize) as u8; - } - block[..8].copy_from_slice(&n.to_le_bytes()); - block[8] = salt; - block[9..24].copy_from_slice(b"TOYOS-BLOCKDIO\0"); - block -} - -/// Mirrored: file `i`'s bytes. -fn file_bytes(i: usize) -> Vec { - (0..FILE_BYTES).map(|b| (b.wrapping_mul(7) ^ i.wrapping_mul(0x3D)) as u8).collect() -} - -/// Where blockd's partitions landed. -struct Layout { - before: Span, - target: Span, - after: Span, - fs: Span, - bench: Span, -} - -/// blockd's disk: a FAT32 neighbour, the idle ROOT slot, a FAT32 neighbour -/// touching it, the FAT32 volume the crash role writes, the bench partition, -/// and two partitions that are not whole 4 KiB blocks. -fn craft_blockd_disk(path: &Path) -> Result { - const NEIGHBOUR_BYTES: u64 = 64 * MIB; - const FS_BYTES: u64 = 64 * MIB; - let parts: [Part; 7] = [ - ("neighbour before", NEIGHBOUR_BYTES, NEIGHBOUR_TYPE, "21111111-2222-4333-8444-555555555501", ALIGNED), - ("idle ROOT slot", TARGET_BLOCKS * BLOCK, toyos_gpt::Guid::TOYOS_ROOT_TEXT, TARGET, ALIGNED), - ("neighbour after", NEIGHBOUR_BYTES, NEIGHBOUR_TYPE, "21111111-2222-4333-8444-555555555502", ALIGNED), - ("fs", FS_BYTES, PLAIN_TYPE, FS, ALIGNED), - ("bench", BENCH_BLOCKS * BLOCK, PLAIN_TYPE, BENCH, ALIGNED), - ("misaligned", MIB + 512, PLAIN_TYPE, MISALIGNED, ALIGNED), - // At the first LBA past the one above: whole blocks long, and - // beginning 512 bytes into one. - ("misaligned start", MIB, PLAIN_TYPE, MISSTART, 1), - ]; - let total = MIB + parts.iter().map(|p| p.1.next_multiple_of(MIB)).sum::() + 2 * MIB; - let (mut device, spans) = partclaim::table(path, total, &parts)?; - let layout = Layout { before: spans[0], target: spans[1], after: spans[2], fs: spans[3], bench: spans[4] }; - for (label, span) in [("BD-BEFORE", layout.before), ("BD-AFTER", layout.after), ("BD-FS", layout.fs)] { - let volume = partclaim::fat32(span.len as usize, label)?; - device.seek(SeekFrom::Start(span.start)).map_err(|e| format!("seek: {e}"))?; - device.write_all(&volume).map_err(|e| format!("write {label}: {e}"))?; - } - device.flush().map_err(|e| format!("flush the disk: {e}"))?; - Ok(layout) -} - -/// A boot with the actuators `params` armed, and QEMU tracing NVMe to -/// `trace`. -fn boot( - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - name: &str, - params: &'static [&'static str], -) -> Result<(QemuInstance, Layout, PathBuf, PathBuf, Vec), String> { - let config = super::compile::repo_root().join(CONFIG); - let blockd_disk = super::lane::dir().join(format!("{name}-blockd.img")); - let layout = craft_blockd_disk(&blockd_disk)?; - let before = std::fs::read(&blockd_disk).map_err(|e| format!("read the crafted disk: {e}"))?; - let trace = super::lane::dir().join(format!("{name}-nvme.trace")); - let _ = std::fs::remove_file(&trace); - let qemu = QemuInstance::boot_with_options( - &config, - c_bins, - rust_bins, - BootOptions { - userland_nvme: Some(blockd_disk.clone()), - nvme_trace: Some(trace.clone()), - kernel_params: params, - ..Default::default() - }, - ); - partclaim::no_panic("booting", qemu.boot_log())?; - Ok((qemu, layout, blockd_disk, trace, before)) -} - -/// One role of the guest, which must end `PASS ` and exit 0. -fn role(qemu: &mut QemuInstance, role: &str, timeout: Duration) -> Result { - let result = qemu.run_test(&format!("test_rs_blockd_io {role}"), timeout); - if result.exit_code != Some(0) || !result.stdout.contains(&format!("blockd_io: PASS {role}")) { - return Err(format!( - "role {role} exited {:?}:\n{}\nkernel log while it ran:\n{}{}", - result.exit_code, result.stdout, result.before, result.serial - )); - } - Ok(result) -} - -fn said<'a>(result: &'a TestResult, needle: &str) -> Result<&'a str, String> { - result - .stdout - .lines() - .find(|l| l.contains(needle)) - .ok_or_else(|| format!("the guest never said {needle:?}:\n{}", result.stdout)) -} - -/// What QEMU's trace says reached blockd's controller. -struct Traced { - /// Flush commands the device ran. - flushes: usize, - /// Submission queues reads and writes arrived on. - queues: BTreeSet, - /// The most commands outstanding at once on queues 2 and up — nothing but - /// blockd drives a controller on this boot. - peak: usize, -} - -/// A trace line's `key value` field. -fn field(line: &str, key: &str) -> Option { - let mut words = line.split_whitespace(); - while let Some(word) = words.next() { - if word == key { - let value = words.next()?; - return match value.strip_prefix("0x") { - Some(hex) => u64::from_str_radix(hex, 16).ok(), - None => value.parse().ok(), - }; - } - } - None -} - -fn read_trace(trace: &Path) -> Result { - let text = std::fs::read_to_string(trace).map_err(|e| format!("read the NVMe trace: {e}"))?; - let mut flushes = 0; - let mut queues = BTreeSet::new(); - let mut open: BTreeSet<(u64, u64)> = BTreeSet::new(); - let mut peak = 0; - for line in text.lines() { - if line.contains("pci_nvme_flush_ns") { - flushes += 1; - } else if line.contains("pci_nvme_io_cmd") { - let (Some(cid), Some(sqid), Some(opc)) = (field(line, "cid"), field(line, "sqid"), field(line, "opc")) - else { - return Err(format!("a trace line this reader does not know: {line:?}")); - }; - if opc == 1 || opc == 2 { - queues.insert(sqid as u16); - } - if sqid >= 2 { - open.insert((sqid, cid)); - peak = peak.max(open.len()); - } - } else if line.contains("pci_nvme_enqueue_req_completion") { - let (Some(cid), Some(cqid)) = (field(line, "cid"), field(line, "cqid")) else { - return Err(format!("a trace line this reader does not know: {line:?}")); - }; - open.remove(&(cqid, cid)); - } - } - Ok(Traced { flushes, queues, peak }) -} - -/// One thing QEMU's trace says a controller did, in the order it did it. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum Did { - /// `CC.EN` set and the controller ready: a driver brought it up. - Started, - /// A write of `sectors` from sector `lba`. - Wrote { lba: u64, sectors: u64 }, - Flushed, -} - -fn trace_events(trace: &Path) -> Result, String> { - let text = std::fs::read_to_string(trace).map_err(|e| format!("read the NVMe trace: {e}"))?; - let mut did = Vec::new(); - for line in text.lines() { - if line.contains("pci_nvme_mmio_start_success") { - did.push(Did::Started); - } else if line.contains("pci_nvme_flush_ns") { - did.push(Did::Flushed); - } else if line.contains("pci_nvme_write ") { - let (Some(lba), Some(sectors)) = (field(line, "lba"), field(line, "nlb")) else { - return Err(format!("a trace line this reader does not know: {line:?}")); - }; - did.push(Did::Wrote { lba, sectors }); - } - } - Ok(did) -} - -/// The writes to `span` blockd acknowledged and no flush covered when its -/// controller was reset or it was killed, as the device saw them, each written -/// again first thing after — read off QEMU's trace alone. -/// -/// A lifetime is what follows one controller start — -/// blockd's bring-up, or its reset. The one the loss ended is the lifetime -/// whose writes to `span` did not end in a flush and after which another -/// lifetime wrote to it; its last write is the one blockd withheld the answer -/// to, and the ones before it since its last flush are the acknowledged ones. -/// The lifetime after it must write exactly those before any other write to -/// `span`, each after every one of them it overlaps that came before it — -/// which is what reissue means, and what a client that forgot them cannot do -/// by accident. Two writes that do not overlap may be in flight together, so -/// the device's order between them is neither lifetime's to keep. Every Flush -/// in the trace is blockd's: nothing else drives a controller. -fn reissued_after(trace: &Path, span: Span) -> Result, String> { - let mut lives: Vec> = Vec::new(); - for did in trace_events(trace)? { - match did { - Did::Started => lives.push(Vec::new()), - Did::Wrote { lba, .. } if !(span.start..span.end()).contains(&(lba * SECTOR)) => {} - did => match lives.last_mut() { - Some(life) => life.push(did), - None => return Err(format!("the trace has {did:?} before any controller started")), - }, - } - } - let wrote = |life: &[Did]| life.iter().any(|d| matches!(d, Did::Wrote { .. })); - let volume: Vec<&[Did]> = lives.iter().map(Vec::as_slice).filter(|l| wrote(l)).collect(); - let writes = |dids: &[Did]| -> Vec<(u64, u64)> { - dids.iter().filter_map(|d| match d { Did::Wrote { lba, sectors } => Some((*lba, *sectors)), _ => None }).collect() - }; - let tail = |life: &[Did]| -> Vec<(u64, u64)> { - let after = life.iter().rposition(|d| *d == Did::Flushed).map_or(0, |at| at + 1); - writes(&life[after..]) - }; - let died: Vec = (0..volume.len().saturating_sub(1)).filter(|&i| !tail(volume[i]).is_empty()).collect(); - let [died] = died[..] else { - return Err(format!( - "{} blockd lifetimes wrote to {span:?}, and {} of them ended with writes no flush \ - covered and another after them, not one", - volume.len(), - died.len() - )); - }; - let mut unflushed = tail(volume[died]); - let (withheld, _) = unflushed.pop().expect("a tail is not empty"); - // An empty prefix equals anything, so a loss with nothing acknowledged - // before it would pass with no write to reissue. - if unflushed.is_empty() { - return Err(format!( - "blockd died with only the withheld write at sector {withheld} unflushed, so nothing \ - acknowledged was left to reissue" - )); - } - let next: Vec<(u64, u64)> = writes(volume[died + 1]).into_iter().take(unflushed.len()).collect(); - let overlaps = |a: (u64, u64), b: (u64, u64)| a.0 < b.0 + b.1 && b.0 < a.0 + a.1; - // Every write that overlaps one of them, in the order it went out. - let around = |w: (u64, u64), ws: &[(u64, u64)]| ws.iter().copied().filter(|&o| overlaps(o, w)).collect::>(); - let (mut want, mut got) = (unflushed.clone(), next.clone()); - want.sort_unstable(); - got.sort_unstable(); - let same = want == got && unflushed.iter().all(|&w| around(w, &unflushed) == around(w, &next)); - let lbas = |ws: &[(u64, u64)]| ws.iter().map(|w| w.0).collect::>(); - let (unflushed, next) = (lbas(&unflushed), lbas(&next)); - if !same { - return Err(format!( - "blockd died with the writes at sectors {unflushed:?} acknowledged and no flush after them \ - (and {withheld} withheld); the blockd after it wrote {next:?} first" - )); - } - Ok(unflushed) -} - -/// Every byte outside the partitions the guest may write is the byte the host -/// wrote, and both FAT32 neighbours are clean to fatgen103. -fn neighbours_untouched(layout: &Layout, before: &[u8], after: &[u8]) -> Result<(), String> { - if before.len() != after.len() { - return Err(format!("the disk changed size: {} -> {}", before.len(), after.len())); - } - let owned = [layout.target, layout.fs, layout.bench]; - let mut at = 0u64; - while at < before.len() as u64 { - if let Some(span) = owned.iter().find(|s| s.start <= at && at < s.end()) { - at = span.end(); - continue; - } - if before[at as usize] != after[at as usize] { - return Err(format!("byte {at} (block {}) changed outside every partition a session held", at / BLOCK)); - } - at += 1; - } - for (what, span) in [("first", layout.before), ("second", layout.after)] { - let complaints = toyos_fat32_check::check(span.of(after)); - if !complaints.is_empty() { - return Err(format!( - "the {what} neighbour is not the FAT32 it was:\n{}", - toyos_fat32_check::describe(&complaints) - )); - } - } - Ok(()) -} - -/// The partition claims served by blockd, and blockd's rate. -/// -/// - Every refusal by name, the idle ROOT slot's 2048 blocks written whole -/// through a session and read back, and one holder at a time across two -/// processes — the slot a second client is refused while it is held and -/// opened once it is not. -/// - The same bytes through blockd, one request at a time and many, timed. -/// - Off the image: the slot holds every block the guest wrote, and nothing -/// outside the sessions' partitions moved. -/// - Off QEMU's trace, which no driver writes: blockd's Flush reached the -/// device, reads and writes went down several submission queues, and more -/// than one command was outstanding at once. -pub fn blockd_serves_partitions( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (mut qemu, layout, disk, trace, before) = boot(c_bins, rust_bins, "blockd-serves", &[])?; - let claims = role(&mut qemu, "claims", Duration::from_secs(240))?; - for want in [ - "an absent GUID refused with NotFound", - "the zero GUID refused with NotFound", - "a partition not whole blocks long refused with Unusable", - "a partition beginning inside a block refused with Unusable", - "longer than a session, refused with Malformed", - "a second client of the slot refused with Held", - "a second client of the slot refused with Opened", - "blockd: NVMe up:", - ] { - said(&claims, want)?; - } - let cache = said(&claims, "volatile write cache")?.to_string(); - if !cache.contains("present, so a flush issues Flush") { - return Err(format!("blockd's controller reports no volatile write cache: {cache}")); - } - let bench = role(&mut qemu, "bench", Duration::from_secs(600))?; - let numbers = said(&bench, "blockd_io: bench")?.to_string(); - let tail = partclaim::shut_down(qemu); - partclaim::no_panic("on the way down", &tail)?; - let mut log = Serial::named("blockd_serves_partitions", format!("{}{}", claims.serial, bench.serial)); - log.push(&tail); - log.must_be_clean()?; - - let after = std::fs::read(&disk).map_err(|e| format!("read the disk back: {e}"))?; - neighbours_untouched(&layout, &before, &after)?; - let slot = layout.target.of(&after); - for n in 0..TARGET_BLOCKS { - if slot[(n * BLOCK) as usize..((n + 1) * BLOCK) as usize] != pattern(0x5A, n)[..] { - return Err(format!("slot block {n} is not what the guest wrote there")); - } - } - let traced = read_trace(&trace)?; - if traced.flushes == 0 { - return Err("QEMU ran no Flush, and blockd's flushes all said durable".into()); - } - if traced.queues.len() < 2 || traced.peak < 2 { - return Err(format!( - "QEMU saw reads and writes on submission queues {:?} and at most {} of blockd's commands \ - outstanding at once", - traced.queues, traced.peak - )); - } - let _ = std::fs::remove_file(&disk); - let _ = std::fs::remove_file(&trace); - eprintln!( - " [blockd] {numbers}; QEMU traced {} Flush commands, reads and writes on submission queues \ - {:?}, and at most {} of blockd's commands outstanding at once; the idle slot holds all \ - {TARGET_BLOCKS} blocks and nothing outside the sessions' partitions moved", - traced.flushes, traced.queues, traced.peak - ); - Ok(()) -} - -/// blockd's two failures, each survived by its client, and a client that -/// breaks the protocol, survived by blockd. -/// -/// - `hostile-head`: with a write on the device, a client moves its completion -/// ring's head a ring behind blockd's tail; the answer finds no room, blockd -/// ends that session, and serves the next. -/// - `reset`: blockd withholds its second write's answer; the silence ends in -/// a controller reset; the withheld write is answered not done; and the -/// write acknowledged before it, which the reset may have lost, is on the -/// medium after the next flush — blockd says the flush found the loss. -/// - `crash`: a FAT32 volume written through a session, blockd killed the -/// moment it has done a write it never answered, with two acknowledged and -/// not flushed; restarted on the same port, the session reopened, the same -/// mount carried on. Off the image, with the host's own readers: the volume -/// is clean to fatgen103, and every file the guest was told was written — -/// and the one written after the restart — holds its bytes by `fatfs`. -/// QEMU keeps its write cache across a reset and a kill, so the image holds -/// the acknowledged writes whether they were written again or not: the -/// reissue is read off QEMU's trace instead ([`reissued_after`]), for the -/// reset as for the kill, and the guest's own counts are held against it. -pub fn blockd_survives_its_death( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (mut qemu, layout, disk, trace, before) = boot(c_bins, rust_bins, "blockd-death", &[])?; - let hostile = role(&mut qemu, "hostile-head", Duration::from_secs(120))?; - let reset = role(&mut qemu, "reset", Duration::from_secs(240))?; - for want in [ - "blockd: WITHHELD the device's answer to a write", - "resetting the controller", - "blockd: controller reset;", - "a flush found writes of its own the device lost", - "the withheld write was answered Device", - ] { - said(&reset, want)?; - } - let reset_again = said(&reset, "went out again after the reset")?.to_string(); - let crash = role(&mut qemu, "crash", Duration::from_secs(600))?; - let crash_again = said(&crash, "went out again after the restart")?.to_string(); - for want in [ - "blockd: WITHHELD the device's answer to a write", - "blockd killed with the withheld write done on the device", - "was answered Refused", - "blockd restarted and the session reopened", - ] { - said(&crash, want)?; - } - let tail = partclaim::shut_down(qemu); - partclaim::no_panic("on the way down", &tail)?; - let mut log = - Serial::named("blockd_survives_its_death", format!("{}{}{}", hostile.serial, reset.serial, crash.serial)); - log.push(&tail); - log.must_be_clean()?; - - // The device's account first: the image cannot tell a reissue from none. - let (reset_reissued, crash_reissued) = - match (reissued_after(&trace, layout.bench), reissued_after(&trace, layout.fs)) { - (Ok(reset), Ok(crash)) => (reset, crash), - (reset, crash) => { - return Err(format!("QEMU's trace, after the reset: {reset:?}; after the kill: {crash:?}")); - } - }; - for (what, reissued, again) in - [("reset", &reset_reissued, &reset_again), ("restart", &crash_reissued, &crash_again)] - { - if !again.contains(&format!("{} acknowledged writes", reissued.len())) { - return Err(format!( - "QEMU's trace has {} writes blockd acknowledged and wrote again after the {what}, and the \ - guest said {again:?}", - reissued.len() - )); - } - } - - let after = std::fs::read(&disk).map_err(|e| format!("read the disk back: {e}"))?; - neighbours_untouched(&layout, &before, &after)?; - let volume = layout.fs.of(&after); - let complaints = toyos_fat32_check::check(volume); - if !complaints.is_empty() { - return Err(format!( - "the volume blockd died under is not clean:\n{}", - toyos_fat32_check::describe(&complaints) - )); - } - let mut image = volume.to_vec(); - let fs = fatfs::FileSystem::new(std::io::Cursor::new(&mut image), fatfs::FsOptions::new()) - .map_err(|e| format!("the volume does not mount on the host: {e}"))?; - let root = fs.root_dir(); - let mut files: BTreeMap> = - (0..FILES).map(|i| (format!("F{i}.BIN"), file_bytes(i))).collect(); - files.insert(AFTER.to_string(), file_bytes(99)); - for (name, want) in &files { - let mut got = Vec::new(); - root.open_file(name) - .map_err(|e| format!("{name} is not on the volume: {e}"))? - .read_to_end(&mut got) - .map_err(|e| format!("{name}: {e}"))?; - if &got != want { - return Err(format!("{name} is {} bytes of something else off the image", got.len())); - } - } - drop(root); - drop(fs); - let _ = std::fs::remove_file(&disk); - let _ = std::fs::remove_file(&trace); - eprintln!( - " [blockd] a controller reset under a withheld write: answered not done, and the write \ - before it rewritten after the flush found it lost; blockd killed with a write done and \ - unanswered: restarted, the session reopened, the mount carried on, and off the image the \ - volume is clean to fatgen103 and all {} files read back by fatfs; QEMU's trace has the \ - acknowledged writes at sectors {reset_reissued:?} written again first after the reset, and \ - those at {crash_reissued:?} first by the blockd after the kill", - files.len() - ); - Ok(()) -} - -/// A transfer outside what a claim's function was lent is the unit's fault -/// record and nothing else. -/// -/// The guest drives blockd's controller itself (`blockd::nvme`), lending it one -/// region with `SYS_DEVICE_DMA_MAP`, four times, each on a fresh claim: -/// - a read into the lent region lands there, and the function's own register -/// window and a region already lent are refused as regions to lend; -/// - a read aimed at the first address past it is refused at the unit, and -/// the region is untouched; -/// - a read aimed at the region after `SYS_DEVICE_DMA_UNMAP` took it back is -/// refused at the unit, and the region — still the guest's — is untouched; -/// - and the function, released and claimed again, reads into a lent region -/// as the first did. -/// -/// The oracle is the unit: each refusal is one `DMA FAULT` record the kernel -/// wrote from the fault recording registers (VT-d 3.0 §7.2), at the address -/// the guest aimed at, blamed on the claim's slot, with a second-level -/// reason — and nothing on the machine died. -pub fn blockd_dma_outside_the_lent( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (mut qemu, _layout, disk, trace, _before) = boot(c_bins, rust_bins, "blockd-dma", &[])?; - let mut log = String::new(); - let mut aimed = Vec::new(); - for name in ["dma-inside", "dma-outside", "dma-revoked", "dma-after"] { - let result = role(&mut qemu, name, Duration::from_secs(120))?; - if name == "dma-inside" { - said(&result, "a register window, and a region already lent, are refused with InvalidArgument")?; - said(&result, "a spawn from a register window is refused with InvalidArgument, and one from a region reaches its argv")?; - } - if let Some(line) = result.stdout.lines().find(|l| l.contains("aiming the device at ")) { - let at = line - .split("aiming the device at ") - .nth(1) - .and_then(|rest| rest.split([',', ' ']).next()) - .ok_or_else(|| format!("no address in {line:?}"))?; - aimed.push((name, at.to_string())); - } - log.push_str(&result.before); - log.push_str(&result.serial); - } - let tail = partclaim::shut_down(qemu); - partclaim::no_panic("on the way down", &tail)?; - log.push_str(&tail); - let log = Serial::named("blockd_dma_outside_the_lent", log); - log.must_be_clean_apart_from("iommu: DMA FAULT owner=slot", 2)?; - for (name, at) in &aimed { - let want = format!("addr={:#018x}", u64::from_str_radix(at.trim_start_matches("0x"), 16).map_err(|e| format!("{at}: {e}"))?); - let line = log - .text() - .lines() - .find(|l| l.contains("iommu: DMA FAULT owner=slot") && l.contains(&want)) - .ok_or_else(|| format!("{name}: no fault record at {want}:\n{}", log.text()))?; - if !["read-permission", "write-permission", "paging-entry-invalid"].iter().any(|r| line.ends_with(r)) { - return Err(format!("{name}: the record's reason is not a second-level walk's: {line}")); - } - eprintln!(" [blockd] {name}: {}", line.trim()); - } - if aimed.len() != 2 { - return Err(format!("the guest aimed outside the lent region {} times, not 2", aimed.len())); - } - let _ = std::fs::remove_file(&disk); - let _ = std::fs::remove_file(&trace); - eprintln!( - " [blockd] a read into a lent region landed; one aimed past it and one at it after it was \ - taken back were each one fault record at that address and left the region untouched; the \ - function answered again on its next claim" - ); - Ok(()) -} - -/// blockd started holding no claim answers each first frame on the loop and -/// the handshake it serves a controller on: a listing with a payload and an -/// open with no region or a short GUID `Malformed`, a listing empty, an open -/// `NotFound`. The guest's own account; no disk is crafted, since this blockd -/// drives none. -pub fn blockd_serves_nothing( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = super::compile::repo_root().join(CONFIG); - let mut qemu = QemuInstance::boot_with_options(&config, c_bins, rust_bins, BootOptions::default()); - partclaim::no_panic("booting", qemu.boot_log())?; - role(&mut qemu, "nothing", Duration::from_secs(60))?; - eprintln!(" [blockd] with no controller, every first frame answered as the controller's loop answers it"); - Ok(()) -} - -/// What a claim may lend its function, and what it may not. -/// -/// On a boot whose device domains have [`NARROW`] of addresses -/// (`iommu-domain-narrow`), the guest: -/// - lends virtio-sound's pool — ordinary memory, a kernel driver's own — and -/// is refused with `InvalidArgument`; -/// - lends 2 MiB regions beside the claim's own 2 MiB grant until the claim's -/// bound refuses the next with `ResourceExhausted`, at exactly the count the -/// bound leaves room for; and takes the grant's address back as a lent -/// region, which is `NotFound`; then frees leaves 0, 2, 4 and 15 of the -/// window, room the bound allows in no one run, and a 4 MiB region is -/// `ResourceExhausted`; -/// - lends one region and takes it back until ten such domains' worth of -/// addresses went by, and the device then reads into it. -/// -/// Off the log: every domain the kernel made is the narrow one, one of them -/// for the claim, and nothing panicked. -/// -/// Then, on a boot where no release resets the function -/// (`pcidev-reset-nothing`), three claims in turn: the first lends a region -/// and the device reads into it, the second lends and takes it back, and -/// neither the second nor the third lends where the first did. -pub fn blockd_lends_within_its_bound( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (mut qemu, _layout, disk, trace, _before) = - boot(c_bins, rust_bins, "blockd-lend", &["iommu-domain-narrow"])?; - let mut log = String::new(); - let mut lines = Vec::new(); - for (name, want) in [ - ("dma-pool", "is refused with InvalidArgument"), - ("dma-bound", "the next refused with ResourceExhausted"), - ("dma-churn", "the device then read block 0 into it"), - ] { - let result = role(&mut qemu, name, Duration::from_secs(240))?; - lines.push(said(&result, want)?.to_string()); - log.push_str(&result.before); - log.push_str(&result.serial); - } - let bound = said_line(&lines, "regions of")?; - let room = (32 * MIB - REGION) / REGION; - if !bound.contains(&format!("blockd_io: {room} regions of {REGION} bytes")) { - return Err(format!("the claim's bound leaves room for {room} regions, and the guest said {bound:?}")); - } - let churn = said_line(&lines, "lends of a")?; - let rounds: u64 = churn - .split("blockd_io: ") - .nth(1) - .and_then(|rest| rest.split(' ').next()) - .and_then(|n| n.parse().ok()) - .ok_or_else(|| format!("no count in {churn:?}"))?; - if rounds * REGION < 10 * NARROW { - return Err(format!("{rounds} lends of {REGION} bytes are not ten domains of {NARROW}")); - } - let boot_log = qemu.boot_log().to_string(); - let tail = partclaim::shut_down(qemu); - partclaim::no_panic("on the way down", &tail)?; - log.push_str(&tail); - let whole = format!("{boot_log}{log}"); - let mut domains = 0; - let mut claimed = false; - for line in whole.lines().filter(|l| l.contains("iommu: domain") && l.contains(" addresses from ")) { - let range = line.split(" addresses from ").nth(1).unwrap_or_default(); - let mut ends = range.split(" to ").map(|w| u64::from_str_radix(w.trim().trim_start_matches("0x"), 16)); - let (Some(Ok(from)), Some(Ok(to))) = (ends.next(), ends.next()) else { - return Err(format!("unreadable domain line: {line:?}")); - }; - if to - from != NARROW { - return Err(format!("iommu-domain-narrow was armed and a domain has {:#x} of addresses: {line:?}", to - from)); - } - domains += 1; - claimed |= log.contains(line); - } - if domains == 0 || !claimed { - return Err(format!("{domains} narrow domains, and none of them made for the claim")); - } - let log = Serial::named("blockd_lends_within_its_bound", log); - log.must_be_clean()?; - let _ = std::fs::remove_file(&disk); - let _ = std::fs::remove_file(&trace); - eprintln!(" [blockd] {}; {bound}; {churn}", lines[0].trim()); - residue_is_never_lent(c_bins, rust_bins) -} - -/// The second boot of [`blockd_lends_within_its_bound`]. -fn residue_is_never_lent(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let (mut qemu, _layout, disk, trace, _before) = - boot(c_bins, rust_bins, "blockd-residue", &["pcidev-reset-nothing"])?; - let result = role(&mut qemu, "dma-residue", Duration::from_secs(120))?; - let third = said(&result, "claim 3 lent at")?.to_string(); - let mut log = format!("{}{}", result.before, result.serial); - let tail = partclaim::shut_down(qemu); - partclaim::no_panic("on the way down", &tail)?; - log.push_str(&tail); - Serial::named("blockd_lends_within_its_bound, residue", log).must_be_clean()?; - let _ = std::fs::remove_file(&disk); - let _ = std::fs::remove_file(&trace); - eprintln!(" [blockd] {}", third.trim()); - Ok(()) -} - -fn said_line<'a>(lines: &'a [String], needle: &str) -> Result<&'a str, String> { - lines - .iter() - .find(|l| l.contains(needle)) - .map(String::as_str) - .ok_or_else(|| format!("no line says {needle:?}: {lines:?}")) -} diff --git a/tests/common/clang.rs b/tests/common/clang.rs deleted file mode 100644 index 1fd380f04ca..00000000000 --- a/tests/common/clang.rs +++ /dev/null @@ -1,117 +0,0 @@ -//! A C program and a C++ program compiled and linked by the toolchain's clang — -//! the ToyOS driver `ToyOSOrg/llvm-project` carries — judged as the loader sees -//! the file, and then run on ToyOS. - -use std::fs; -use std::process::Command; -use std::time::Duration; - -use super::compile; -use super::qemu::{BootOptions, QemuInstance}; - -/// The program, beside the corpus it is not part of. -const HELLO: &str = "tests/testcases/hello.c"; -/// What it prints, all of which its own arithmetic and libc produce. -const SAYS: &str = "hello from clang, on ToyOS: 6 * 7 = 42"; - -/// What an image the ToyOS driver links must be, as the loader's decoder reads -/// it: a PIE for this machine, its entry loaded, an unwind table header, and no -/// program interpreter. -pub fn judge_elf(elf: &[u8]) -> Result<(), String> { - let header = toyos_elf::FileHeader::parse(elf).map_err(|e| format!("toyos-elf refuses the header: {e:?}"))?; - let machine = match super::qemu::SUITE_ARCH { - toyos_build::arch::Arch::X86_64 => toyos_elf::Machine::X86_64, - toyos_build::arch::Arch::Aarch64 => toyos_elf::Machine::Aarch64, - }; - let layout = toyos_elf::Layout::parse(elf, machine).map_err(|e| format!("the loader's decoder refuses it: {e:?}"))?; - if layout.eh_frame_hdr().is_none() { - return Err("it has no unwind table header, which the driver asks for".to_string()); - } - let table = header.program_headers(elf).map_err(|e| format!("its program headers: {e:?}"))?; - let interp = (0..usize::from(header.phnum)) - .filter_map(|i| toyos_elf::header::ProgramHeader::parse(table, i)) - .any(|p| p.kind == toyos_elf::header::PT_INTERP); - if interp { - return Err("it names a program interpreter, which ToyOS does not have".to_string()); - } - Ok(()) -} - -/// Gate: `hello.c`, compiled and linked by one clang invocation, runs on ToyOS. -pub fn c_hello(rust_bins: &[(String, Vec)]) -> Result<(), String> { - let root = compile::repo_root(); - let c = compile::c_sysroot(); - let out = super::lane::dir().join("hello"); - let built = Command::new(&c.clang) - .args(c.args()) - .arg("-O2") - .arg(root.join(HELLO)) - .arg("-o") - .arg(&out) - .output() - .map_err(|e| format!("run {}: {e}", c.clang.display()))?; - if !built.status.success() { - return Err(format!("clang could not build {HELLO}:\n{}", String::from_utf8_lossy(&built.stderr))); - } - let elf = fs::read(&out).map_err(|e| format!("{}: {e}", out.display()))?; - judge_elf(&elf)?; - - let config = root.join("tests/testcases"); - let c_tests = [("hello".to_string(), elf)]; - let mut qemu = QemuInstance::boot_with_options(&config, &c_tests, rust_bins, BootOptions::default()); - let result = qemu.run_test("test_c_hello", Duration::from_secs(60)); - if let Some(err) = &result.error { - return Err(format!("{err}\n{}", result.stdout)); - } - if result.exit_code != Some(0) { - return Err(format!("hello exited {:?}:\n{}", result.exit_code, result.stdout)); - } - if !result.stdout.lines().any(|l| l.trim_end() == SAYS) { - return Err(format!("hello did not say {SAYS:?}:\n{}", result.stdout)); - } - eprintln!(" [c_hello] {SAYS}"); - Ok(()) -} - -const CXX_RUNTIME: &str = "tests/cxx/runtime.cpp"; -const CXX_RUNTIME_EXPECT: &str = "tests/cxx/runtime.expect"; - -/// Gate: a C++ program — libc++'s containers, strings and streams, exceptions, -/// threads and their destructors — compiled and linked by one clang -/// invocation, prints on ToyOS what [`CXX_RUNTIME_EXPECT`] holds. -pub fn cxx_runtime(rust_bins: &[(String, Vec)]) -> Result<(), String> { - let root = compile::repo_root(); - let c = compile::c_sysroot(); - let out = super::lane::dir().join("cxx-runtime"); - let built = Command::new(&c.clang) - .arg("--driver-mode=g++") - .args(c.args()) - .args(["-std=c++17", "-O2"]) - .arg(root.join(CXX_RUNTIME)) - .arg("-o") - .arg(&out) - .output() - .map_err(|e| format!("run {}: {e}", c.clang.display()))?; - if !built.status.success() { - return Err(format!("clang could not build {CXX_RUNTIME}:\n{}", String::from_utf8_lossy(&built.stderr))); - } - let elf = fs::read(&out).map_err(|e| format!("{}: {e}", out.display()))?; - judge_elf(&elf)?; - let expected = fs::read_to_string(root.join(CXX_RUNTIME_EXPECT)).map_err(|e| format!("{CXX_RUNTIME_EXPECT}: {e}"))?; - - let config = root.join("tests/testcases"); - let c_tests = [("cxx_runtime".to_string(), elf)]; - let mut qemu = QemuInstance::boot_with_options(&config, &c_tests, rust_bins, BootOptions::default()); - let result = qemu.run_test("test_c_cxx_runtime", Duration::from_secs(60)); - if let Some(err) = &result.error { - return Err(format!("{err}\n{}", result.stdout)); - } - if result.exit_code != Some(0) { - return Err(format!("{CXX_RUNTIME} exited {:?}:\n{}", result.exit_code, result.stdout)); - } - if let Some(mismatch) = super::console::c_verdict(&result.stdout, &expected).mismatch { - return Err(mismatch); - } - eprintln!(" [cxx_runtime] {} lines, as expected", expected.lines().count()); - Ok(()) -} diff --git a/tests/common/console.rs b/tests/common/console.rs deleted file mode 100644 index 57489514844..00000000000 --- a/tests/common/console.rs +++ /dev/null @@ -1,535 +0,0 @@ -//! The one thing the harness may conclude from the console's line atomicity: -//! [`verdict`] — a line's *first bytes are its writer's own*, so the C family -//! can tell a daemon's line from the program under test's by reading it, and -//! stop failing on output that is not its own. -//! -//! **L5's guarantee is about flushes, not about newlines.** A program that -//! writes without a trailing newline has its bytes joined to the next writer's -//! line by the *host's* splitter — see [`speaker_at`], which is where that is -//! written down. Both are [`c_capture_ignores_daemon_lines`]'s, and every one -//! of its verdicts carries the control that says it has teeth. - -use std::collections::BTreeSet; -use std::path::{Path, PathBuf}; -use std::sync::OnceLock; -use std::time::Duration; - -use super::qemu::{BootOptions, QemuInstance}; - -/// A liveness guard and never the verdict: it only catches a guest that -/// stopped answering. -const CEILING: Duration = Duration::from_secs(60); - -/// The last of a capture, for a failure message. -fn tail(text: &str) -> String { - let lines: Vec<&str> = text.lines().collect(); - lines[lines.len().saturating_sub(20)..] - .iter() - .map(|l| l.chars().take(100).collect::()) - .collect::>() - .join("\n") -} - -// --- What the C family may conclude from a shared console --- - -/// The `system.toml` the C family boots, every time. -/// -/// [`verdict`] is called from a comparison that has a capture and no guest, so -/// the config is named here rather than passed: the whole registry boots -/// `tests/testcases` and [`c_capture_ignores_daemon_lines`] asserts that the -/// machine it staged the gate on is this one, so the day a second config runs C -/// tests the gate says so instead of the filter quietly deriving its names from -/// the wrong image. -fn config() -> PathBuf { - super::compile::repo_root().join("tests/testcases/system.toml") -} - -/// Every name a process on that boot speaks its console lines in. -/// -/// Derived from the config by `toyos_build::build::console_speakers` and cached -/// once — a list written here would be a list that goes stale the next time a -/// daemon joins `[boot] start`, which is precisely how this defect would come -/// back. -fn speakers() -> &'static BTreeSet { - static SPEAKERS: OnceLock> = OnceLock::new(); - SPEAKERS.get_or_init(|| toyos_build::build::console_speakers(&config())) -} - -/// Whose line this is, when it is not the program under test's. -/// -/// A prefix and nothing cleverer, because after L5 that is exactly what the -/// wire carries: `ConsoleObject` is one line buffer per holder, so the bytes at -/// the front of a console line were written by the process the line belongs to. -/// The shape is `: ` — what every daemon in this tree prints, and what -/// `/system/bin/init` prints when it speaks in one of their names before it has -/// started them. -fn speaker_of<'a>(line: &str, speakers: &'a BTreeSet) -> Option<&'a str> { - let (head, rest) = line.split_once(':')?; - // `soundd: ready` and a bare `soundd:`, and nothing else — `main: x` from a - // C case is a colon in the middle of a word, and `a:b` is not a speaker's - // line whatever `a` is. - if !rest.is_empty() && !rest.starts_with(' ') { - return None; - } - speakers.get(head).map(String::as_str) -} - -/// Where a daemon's whole line begins inside a captured line — which is not -/// always at its front. -/// -/// **The half of this defect that no prefix rule reaches, and it is not a -/// splice.** L5's guarantee is about what the kernel emits: every *flush* is -/// one holder's bytes. It says nothing about where a **newline** is, and the -/// host's line splitter is `BufReader::lines()`. A program that writes without -/// a trailing newline — `71_macro_empty_arg` is `printf("%d", …)` and nothing -/// else — leaves `17` on the wire unterminated, and the next writer's whole -/// line is appended to it by the splitter, not by the kernel. Measured on this -/// tree, 2026-08-15: `17init: started test-runner` in one captured line, with -/// `17` expected. The same shape joins the two halves of a line longer than -/// `MAX_CONSOLE_LINE`, which the kernel does emit in pieces. -/// -/// So a daemon's unit is `: …` up to the newline that ended it, and it -/// can start anywhere in a captured line. Found by walking the colons rather -/// than every offset. -fn speaker_at(line: &str, speakers: &BTreeSet) -> Option { - for (colon, _) in line.match_indices(':') { - for name in speakers { - let Some(start) = colon.checked_sub(name.len()) else { continue }; - if line.is_char_boundary(start) - && &line[start..colon] == name.as_str() - && speaker_of(&line[start..], speakers).is_some() - { - return Some(start); - } - } - } - None -} - -/// What the C family concluded from one capture, and what it took out first. -pub struct Verdict<'a> { - /// Each whole line another process wrote, removed before the comparison — - /// as it stood on the wire, which is from where it started to the newline - /// that ended it, and not necessarily a whole captured line. - /// - /// **Kept and printed either way, never dropped.** The removal is a claim - /// about who wrote a line, and a claim that nobody can see is a capture - /// quietly getting shorter; on a red these are usually the whole - /// explanation. - pub filtered: Vec<&'a str>, - /// `None` is a match. - pub mismatch: Option, -} - -/// Compare a C test's capture against its `.expect`, ignoring lines that are -/// some other process's. -/// -/// **The scope boundary, and it is the whole safety argument.** This is the C -/// family's stdout comparison and nothing else. Every other reader of a -/// daemon's line — `netd_*` waiting on `netd: ready`, the sshd tests reading -/// its host identity, the log gates — reads `TestResult::serial` or a boot log, -/// which this never touches. Those tests *assert on* a daemon's line; this -/// family is the one for which a daemon's line is by construction not the -/// subject, because the subject is a C program's own stdout against a file -/// recorded from it. -/// -/// Which is also why the filter cannot make a broken case pass: a tinycc case's -/// output is decided by its source, so the only way `soundd: …` appears in one -/// is that the source prints it — and then the `.expect` declares it, and the -/// refusal below fires by name rather than the line being silently eaten. 0 of -/// the 153 expectations contain such a substring anywhere, measured, and -/// [`c_capture_ignores_daemon_lines`] re-measures it every run. -pub fn verdict<'a>( - stdout: &'a str, - expected: &str, - speakers: &BTreeSet, -) -> Verdict<'a> { - let mut mine = String::new(); - let mut filtered = Vec::new(); - for line in stdout.lines() { - match speaker_at(line, speakers) { - // **The newline this captured line ended with was the daemon's, so - // it is removed with the rest of that unit and no line break takes - // its place.** That is what puts a program's unterminated `17` back - // beside its own next bytes instead of leaving `17init: started - // test-runner`, and what rejoins the two halves of a line the - // kernel emitted in `MAX_CONSOLE_LINE` pieces. `Some(0)` — a - // daemon's line arriving on its own, the ordinary case — falls out - // of the same arm with an empty head. - Some(at) => { - mine.push_str(&line[..at]); - filtered.push(&line[at..]); - } - None => { - mine.push_str(line); - mine.push('\n'); - } - } - } - - // The one thing this may never do: remove a line the case exists to print. - // Refused by name — a filter that made an exception for such a case would - // be a filter nobody could reason about afterwards. - if let Some(declared) = expected.lines().find(|l| speaker_at(l, speakers).is_some()) { - return Verdict { - filtered, - mismatch: Some(format!( - "the expectation declares {declared:?}, and this comparison attributes that \ - line to another process and removes it from the capture — so the case's own \ - output would be filtered away. Change what the case prints, or take the name \ - out of the boot config. The speakers this boot declares are {:?}", - speakers.iter().collect::>(), - )), - }; - } - - let mismatch = (mine.trim_end() != expected.trim_end()).then(|| { - format!( - "output mismatch\n--- expected ---\n{}\n--- what this program wrote ---\n{}", - expected.trim_end(), - mine.trim_end(), - ) - }); - Verdict { filtered, mismatch } -} - -/// [`verdict`] against the boot config the C family runs on. -pub fn c_verdict<'a>(stdout: &'a str, expected: &str) -> Verdict<'a> { - verdict(stdout, expected, speakers()) -} - -/// The line the gate has a guest write inside a capture window on purpose. -/// -/// `soundd` because that is the daemon the write-up caught doing this, and the -/// text is a sentence no `.expect` in the corpus contains. -const IMPOSTOR: &str = "soundd: capture window gate line"; - -/// The same line with the speaker taken off the front, which is what a C -/// program's own output looks like. The pair is the whole gate: one has to go -/// and the other has to stay, and a filter that got either wrong would pass -/// only one of them. -const MINE: &str = "capture window gate line"; - -/// A daemon's line inside a C test's window no longer decides that test. -/// -/// Deterministic, because nothing here waits for the race: a guest process -/// writes [`IMPOSTOR`] *into* a real capture window on purpose, and the real -/// comparison is then run over the real capture. Four verdicts, and the last -/// two are the controls that stop this from being a gate that would pass on a -/// filter which removed everything or nothing: -/// -/// 1. the impostor lands in the window whole, which is L5's guarantee and this -/// fix's premise — a spliced line would fail the equality, not a `contains`; -/// 2. the comparison ignores it, and names it as ignored; -/// 3. **filter off** (an empty speaker set) and the same capture reds — so the -/// filter is what makes 2 pass and not something else; -/// 4. a line no speaker owns survives, and an expectation that omits it still -/// reds — so the filter removes daemons' lines and not the program's. -pub fn c_capture_ignores_daemon_lines( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // The filter derives its names from one config; this gate stages its - // evidence on a guest. They have to be the same machine or the evidence is - // about a different image than the one the C family runs on. - if test_config.join("system.toml") != config() { - return Err(format!( - "this gate boots {} and `verdict` derives its speakers from {} — the evidence \ - would be about a different image than the one the C family runs on", - test_config.join("system.toml").display(), - config().display(), - )); - } - let speakers = speakers(); - // Non-vacuity: an empty or truncated set filters nothing and every - // assertion below would still be satisfiable by a capture with no daemon - // line in it. - for want in ["init", "logd", "soundd"] { - if !speakers.contains(want) { - return Err(format!( - "the speakers derived from {} are {:?} and do not include `{want}` — either \ - the config stopped starting it or the derivation is reading the wrong file", - config().display(), - speakers.iter().collect::>(), - )); - } - } - - // The scope boundary, asserted against the corpus rather than described. - // Every `.expect` the C family compares against is checked here, so a case - // whose own output would be filtered is caught by this gate rather than by - // whichever suite happened to run it. - let dir = super::compile::testcases_dir(); - let mut declaring: Vec = Vec::new(); - let entries = std::fs::read_dir(&dir).map_err(|e| format!("read {}: {e}", dir.display()))?; - let mut checked = 0usize; - for entry in entries { - let path = entry.map_err(|e| format!("walk {}: {e}", dir.display()))?.path(); - if path.extension().and_then(|e| e.to_str()) != Some("expect") { - continue; - } - checked += 1; - let text = std::fs::read_to_string(&path) - .map_err(|e| format!("read {}: {e}", path.display()))?; - for line in text.lines() { - // `speaker_at` and not `speaker_of`: the removal reaches a daemon - // unit anywhere in a captured line, so the corpus has to be clear of - // one anywhere and not only at the front. - if let Some(at) = speaker_at(line, speakers) { - declaring.push(format!( - "{}: {:?} reads as another process's", - path.file_name().unwrap_or_default().to_string_lossy(), - &line[at..], - )); - } - } - } - if !declaring.is_empty() { - return Err(format!( - "{} expectation(s) contain text this comparison would remove from the capture, so \ - the case could never match its own output:\n{}", - declaring.len(), - declaring.join("\n"), - )); - } - if checked == 0 { - return Err(format!("{} holds no `.expect` file at all", dir.display())); - } - - let mut qemu = QemuInstance::boot(test_config, c_bins, rust_bins); - - // Zero, and the assertion that keeps the derivation honest as the tree - // grows: **every line this boot's userland wrote is one this set can - // account for.** A daemon added tomorrow that speaks in a name the config - // does not declare would otherwise rejoin the defect silently — its line - // would reach a C test's window and decide that test's verdict, at the - // family's own rate, from a name nobody knew to look for. Here it is a red - // on this gate, with the line quoted. - // - // It is also what found `virtio-sound:` — soundd's driver layer speaks in - // the *device's* name, so `[programs]` keys alone were never the set. - let unattributed: Vec<&str> = qemu - .boot_log() - .lines() - .filter(|l| !l.trim().is_empty()) - .filter(|l| !super::qemu::is_kernel_line(l)) - // The runner's own protocol, which is not a console writer's sentence. - .filter(|l| !l.contains(super::qemu::DEFAULT_READY)) - .filter(|l| speaker_at(l, speakers).is_none()) - .collect(); - if !unattributed.is_empty() { - return Err(format!( - "this boot's userland wrote {} line(s) that no name in {:?} accounts for, so a C \ - test whose window one of them lands in would fail on it:\n{}\n\ - Add whatever declares them to the boot config — the set is derived from \ - `[programs]`, their `devices` and `[boot] start`, and never listed in the harness.", - unattributed.len(), - speakers.iter().collect::>(), - unattributed.join("\n"), - )); - } - - // One. A real process writes a daemon-shaped line inside a real window. - let staged = qemu.run_test(&format!("echo {IMPOSTOR}"), CEILING); - if let Some(err) = &staged.error { - return Err(format!("staging the impostor line: {err}\n{}", tail(&staged.stdout))); - } - if !staged.stdout.lines().any(|l| l == IMPOSTOR) { - return Err(format!( - "the guest wrote {IMPOSTOR:?} and the capture has no such line — equality and not \ - `contains`, because a line arriving spliced with another writer's is what makes \ - attribution by prefix unsound in the first place. The capture was:\n{}", - tail(&staged.stdout), - )); - } - - // Two. The comparison the C family makes ignores it, and says it did. - let ignored = c_verdict(&staged.stdout, ""); - if let Some(mismatch) = &ignored.mismatch { - return Err(format!( - "a daemon-shaped line inside the window still decides a C test's verdict, which \ - is the defect this gate exists for:\n{mismatch}" - )); - } - if !ignored.filtered.contains(&IMPOSTOR) { - return Err(format!( - "the comparison passed without naming {IMPOSTOR:?} among the lines it removed — a \ - capture that silently got shorter is not evidence. It named {:?}", - ignored.filtered, - )); - } - - // Three, the negative control: with nothing declared as a speaker, the same - // capture reds. If it did not, step two would prove nothing about the - // filter. - let unfiltered = verdict(&staged.stdout, "", &BTreeSet::new()); - if unfiltered.mismatch.is_none() { - return Err(format!( - "with an empty speaker set the same capture still compares equal to an empty \ - expectation — so the filter is not what made this pass and this gate has no \ - teeth. The capture was:\n{}", - tail(&staged.stdout), - )); - } - - // Four. The other direction: a line no speaker owns is the program's, and - // it both survives the filter and still reds an expectation that omits it. - let ordinary = qemu.run_test(&format!("echo {MINE}"), CEILING); - if let Some(err) = &ordinary.error { - return Err(format!("staging the ordinary line: {err}\n{}", tail(&ordinary.stdout))); - } - let kept = c_verdict(&ordinary.stdout, MINE); - if let Some(mismatch) = &kept.mismatch { - return Err(format!( - "a line no speaker owns did not survive the filter, so this removes the program's \ - own output:\n{mismatch}" - )); - } - let blanket = c_verdict(&ordinary.stdout, ""); - if blanket.mismatch.is_none() { - return Err(format!( - "a capture carrying {MINE:?} compares equal to an *empty* expectation — the filter \ - is removing everything rather than one process's lines" - )); - } - - // Five. The half a whole-line rule cannot reach, staged as the captures the - // wire actually produced rather than as a guess about them. Both of these - // are transcribed from a run of this suite on 2026-08-15, and both are - // *one* captured line: the host splits on newlines, and the newline the - // program never wrote is the reason its bytes and somebody else's share a - // line at all. - let joined: &[(&str, &str, &str)] = &[ - // `71_macro_empty_arg` is `printf("%d", …)` and nothing after it, so - // its `17` reaches the wire with no terminator and init's next whole - // line is appended to it by the splitter. - ("17init: started test-runner\n", "17", "the program's unterminated tail"), - ("aaasoundd: suspended\nbbb\n", "aaabbb", "a line the kernel emitted in pieces"), - // And the ordinary case still has to work the ordinary way. - ("one\nsoundd: suspended\ntwo\n", "one\ntwo", "a daemon's line between two of the program's"), - ]; - for (capture, want, what) in joined { - let got = verdict(capture, want, speakers); - if let Some(mismatch) = &got.mismatch { - return Err(format!( - "{what}: the capture {capture:?} does not read back as {want:?}\n{mismatch}" - )); - } - if got.filtered.is_empty() { - return Err(format!( - "{what}: {capture:?} matched {want:?} while removing nothing, so the two were \ - equal already and this row proves nothing" - )); - } - // The control, per row: without the speakers there is nothing to - // remove and each of these must red. - if verdict(capture, want, &BTreeSet::new()).mismatch.is_none() { - return Err(format!( - "{what}: {capture:?} reads back as {want:?} with an empty speaker set too, so \ - this row is not testing the removal" - )); - } - } - - // Six, end to end: the corpus case whose output has no trailing newline, on - // a real guest. It is `c_bins`' own binary and this boot carries it. - let unterminated = qemu.run_test("test_c_71_macro_empty_arg", CEILING); - if let Some(err) = &unterminated.error { - return Err(format!("running the unterminated case: {err}")); - } - let read_back = c_verdict(&unterminated.stdout, "17"); - if let Some(mismatch) = &read_back.mismatch { - return Err(format!( - "a C program that wrote `17` and no newline did not read back as its own output — \ - this is the capture losing its tail, whichever writer followed it:\n{mismatch}" - )); - } - - eprintln!( - " [console] {} speakers declared by tests/testcases/system.toml, {checked} \ - expectations clear of them, every userland line of the boot attributed; {IMPOSTOR:?} \ - written inside a capture window and ignored, {MINE:?} kept, {} joined captures read \ - back whole; every control red", - speakers.len(), - joined.len(), - ); - Ok(()) -} - -/// A pending poll on stdin is not something the keyboard *claim* closing can -/// cancel. -/// -/// The guest half is `userland/test-runner/src/kbd_close.rs` and it carries all -/// three verdicts; the host owes it one keystroke, the only way to show that -/// what survived the close was a live registration. -/// -/// **`Profile::Metal` because the keystroke has to arrive.** Its i8042 is the -/// only keyboard on the machine — no USB HID, no virtio — which is the shape -/// `swiss_german_layout` already injects through, and the mouse the middle arm -/// claims is the PS/2 one beside it. -/// -/// **One CPU, because the keystroke outlives the probe.** Nothing holds the -/// keyboard once the claim is released, so the key stays queued while the -/// runner goes back to reading its console, which waits on the serial line. A -/// console read that woke on the keyboard's queue instead would spin in the -/// kernel, and on one CPU that spin starves `logd` and the probe's verdict -/// never reaches the console, every boot rather than some. -pub fn keyboard_claim_close_spares_stdin( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - kbd_close_probe(test_config, c_bins, rust_bins, &[]) -} - -/// The gate's body, parameterised on the boot's actuators so its negative -/// control is one argument rather than a second copy of it. -/// -/// `keyboard-close-cancels-every-console` restores what the tree had, and this -/// must red on a boot carrying it. The measurement is in the commit that took -/// the actuator's name. -fn kbd_close_probe( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - params: &'static [&'static str], -) -> Result<(), String> { - /// What the guest prints once both claim arms have run. - const READY: &str = "===KBD_CLOSE_READY==="; - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: super::qemu::Profile::Metal, - qmp: true, - smp: 1, - kernel_params: params, - ..Default::default() - }, - ); - // One tap, injected only after the guest says it is armed. The hook runs - // inside the console read loop, which is the one place "the poll is - // registered" and "the host has not injected yet" are both true. - let result = qemu.run_test_hooked("kbd-close", CEILING, READY, |socket| { - super::qemu::qmp_send_keys(socket, &[("a", true), ("a", false)]); - }); - if let Some(err) = &result.error { - return Err(format!("{err}\nstdout:\n{}", result.stdout)); - } - if result.exit_code != Some(0) || !result.stdout.contains("kbd-close: OK") { - return Err(format!( - "the keyboard-close probe exited {:?}\n{}", - result.exit_code, result.stdout - )); - } - let survived = result - .stdout - .lines() - .find(|l| l.contains("kbd-close: survived=")) - .ok_or_else(|| format!("the guest never said what it saw\n{}", result.stdout))?; - eprintln!(" [console] {}", survived.trim()); - Ok(()) -} diff --git a/tests/common/faults.rs b/tests/common/faults.rs index 5ece48688a6..4b7052fa154 100644 --- a/tests/common/faults.rs +++ b/tests/common/faults.rs @@ -1,496 +1,23 @@ -//! The double fault path, which is the one that has to survive being the -//! thing that reports on itself. -//! -//! #DF is the only vector with an IST, so it is the only stack in the kernel -//! whose overflow is invisible: it is heap memory, it is written while the -//! crash report is being produced, and the corruption lands under whatever -//! the allocator handed out next. A test that only asserted "the report -//! appeared" would have passed throughout -- the report *did* appear, and it -//! scribbled on the heap on its way out. -//! -//! So the assertion is the kernel's own high-water measurement, taken after -//! `panic_flush` (the deepest point) and written straight to the UART rather -//! than through the log ring, which is one of the things an overflow may have -//! corrupted. -use std::io::Write; use std::path::Path; -use std::time::Duration; use super::qemu::{self, BootOptions, QemuInstance}; use super::serial::Serial; -/// The line `ist1_report` writes to the UART. -const MARKER: &str = "[ist1] used "; - -pub fn double_fault_stack( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // Profile::Metal, because there the 16550 *is* the console, so the raw - // write and the ordinary serial stream arrive on the same channel and one - // reader sees both. It is also the T14's shape, which is the machine this - // bug would have poisoned every double-fault investigation on. - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - kernel_features: toyos_build::build::TEST_KERNEL, - ..Default::default() - }, - ); - - writeln!(qemu.stdin_mut(), "run test_rs_test_panic_child 4").expect("write to QEMU stdin"); - qemu.flush_stdin(); - // Until the report, not for twenty seconds: the fatal path halts every CPU - // without exiting QEMU, so a plain drain has nothing left to disconnect it - // and waits out the whole ceiling. The marker is the line every assertion - // below reads, and `ist1_report` writes it last. - let log = qemu.drain_until(Duration::from_secs(20), |line| line.contains(MARKER)); - - // The premise. If the CPU never took a #DF then nothing ran on IST1 and - // every assertion below would be measuring the wrong stack. - if !log.contains("DOUBLE FAULT") { - return Err(format!("no double fault was taken — the trigger did not work\n{log}")); - } - - // **And the harness's own claim about a capture like this one, asked of the - // only real one the suite produces.** `serial::death_report` is what a - // failure verdict now carries, and it is staged against transcribed lines - // everywhere else; a #DF is on the wire here already, so checking it costs - // nothing and is the difference between a recovery gated on a guess about - // the kernel's output and one gated on the output. It is a claim about the - // report and not about IST1, which is why it sits above every assertion - // that is. - let report = super::serial::death_report(&log).ok_or_else(|| { - format!("a capture carrying a real #DF yields no death report at all\n{log}") - })?; - let head = report.lines().next().unwrap_or_default(); - if !head.contains("DOUBLE FAULT") { - return Err(format!("the report starts at {head:?} and not at the death\n{log}")); - } - // The body. The header alone is what the arm that lost this report already - // printed, so the assertion is on the lines under it: the address that - // started the chain, and the backtrace `double_fault_handler` writes after - // the page walk. - for want in ["cr2=", "Kernel backtrace:", MARKER] { - if !report.contains(want) { - return Err(format!("the report drops {want:?}:\n{report}")); - } - } - let Some(line) = log.lines().find(|l| l.contains(MARKER)) else { - return Err(format!( - "the kernel never reported its IST1 usage; the report cannot have run to the \ - end on IST1\n{log}" - )); - }; - - let (used, capacity) = parse(line) - .ok_or_else(|| format!("could not read a usage out of {line:?}"))?; - eprintln!(" [ist1] double fault report used {used} of {capacity} bytes"); - - if line.contains("GUARD CORRUPTED") { - return Err(format!( - "the double fault report overflowed IST1 and wrote into the heap below it: \ - {used} bytes used of {capacity}" - )); - } - if !line.contains("guard intact") { - return Err(format!("unrecognised verdict in {line:?}")); - } - // Not just "it fit": it has to fit with room, or the next line added to - // the crash report silently reintroduces the bug. Half the stack is the - // margin, and it is stated here so that a change which eats it fails - // here rather than on somebody's laptop. - if used * 2 > capacity { - return Err(format!( - "the double fault report used {used} of {capacity} bytes — over half the stack, \ - so the margin for one more report line is gone" - )); - } - Ok(()) -} - -/// The guard page under every per-CPU idle stack. -/// -/// That stack is 16 KiB of ordinary heap, so an overflow off its bottom did -/// not fault — it rewrote whatever the allocator had put underneath, and the -/// damage surfaced somewhere else entirely (a `BTreeMap` node with an -/// out-of-range index, a write to `0x4`). The idle loop ran `log_file::poll` -/// when that was measured — a filesystem write reaching a block device, whose -/// high water was 11,505 bytes of the 16,384 with the USB command path still -/// below the probe. That caller is gone at log architecture L6 and `drain_irqs` -/// still reaches a device from the same stack. -/// -/// Absence is invisible to every log line and every screendump, so the only -/// way to ask whether the page is really gone is to touch it — which nothing -/// in the kernel does, that being the point of a guard page. `SYS_DEBUG` action -/// 9 supplies the one read. -pub fn idle_stack_guard( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - kernel_features: toyos_build::build::TEST_KERNEL, - ..Default::default() - }, - ); - - writeln!(qemu.stdin_mut(), "run test_rs_test_panic_child 9").expect("write to QEMU stdin"); - qemu.flush_stdin(); - // Until the page walk, not for twenty seconds — `double_fault_stack`'s - // shape and for its reason: this fault is fatal, so `halt_all_cpus` stops - // every CPU without QEMU exiting and a plain drain has nothing left to - // disconnect it. `debug_page_walk` is the last thing any assertion below - // reads (PDPTE, then the PDE carrying `PS=`, then this line), and it runs - // early in the crash report, so what follows on the wire — registers, - // backtrace, stack — is diagnostic that nothing here asks for. A boot where - // the guard is *not* there prints no page walk at all, which is the - // `debug syscall returned` arm below: it pays the whole ceiling and then - // reds, which is the right way round. - // - // **The three spaces are load-bearing.** `PDPTE:` one level up contains - // `PTE:` as a substring, so the obvious predicate ends the drain two lines - // early and reds a green machine with `the crash report's page walk does - // not show a split leaf` — measured, on this change's first run. - // `mm::paging::debug_page_walk` writes `PTE: {:#018x}`, which is the - // spelling the assertion below reads too. - let log = qemu.drain_until(Duration::from_secs(20), |line| line.contains("PTE: 0x")); - - // The premise: which address the kernel went for. Without it every - // assertion below could be satisfied by a fault somewhere else. - let addr = log - .lines() - .find_map(|l| l.split("reading the idle stack guard at ").nth(1)) - .map(|rest| rest.split_whitespace().next().unwrap_or("").to_string()) - .ok_or_else(|| { - format!("the kernel never reached the guard read — is `test-actuators` on?\n{log}") - })?; - - // The tell of a guard that is not there: `SYS_DEBUG` returned, so the read - // landed on dlmalloc's bookkeeping for the chunk the idle stack lives in - // and the child walked away. - if log.contains("debug syscall returned") { - return Err(format!( - "the read at {addr} succeeded — the page below the idle stack is still mapped, \ - so an overflow writes into the heap instead of faulting" - )); - } - for want in [ - format!("#PF UNHANDLED: cr2={addr}"), - format!("KERNEL PANIC: read unmapped address at {addr}"), - ] { - if !log.contains(&want) { - return Err(format!("no {want:?}; the kernel said:\n{log}")); - } - } - // The page walk is the ground truth, and it is in the report: a PDE that - // is a page table rather than a 2 MiB leaf, and a PTE of zero under it. - // Without the split the direct map would still show `PS=1` here. - if !log.contains("PS=0") || !log.contains("PTE: 0x0000000000000000") { - return Err(format!( - "the crash report's page walk does not show a split leaf with an empty entry:\n{log}" - )); - } - eprintln!(" [guard] a read at {addr} faulted, one page below the idle stack"); - - // And the machine halts, which is the intended end. An overflow off the - // bottom of the idle stack is a kernel bug, not untrusted input, and - // `fatal_exception` treats a fault on a *kernel* address as fatal by - // policy. The whole change is that it is now reported at all: without the - // guard the same overflow writes into the heap and the machine carries on - // with a `BTreeMap` node the allocator no longer agrees about. - Ok(()) -} - -/// A NIC that cannot raise an interrupt must cost the machine networking and -/// nothing else. -/// -/// The other two virtio functions keep their vectors, which is what makes the -/// verdict mean anything: the console that carries the refusal and the audio -/// device beside it are on the same bus, driven by the same code, and neither -/// notices. -pub fn virtio_net_no_msix() -> Result<(), String> { - let options = BootOptions { - profile: qemu::Profile::VirtioNetNoMsix, - ..Default::default() - }; - // The actuator is a device property and argv is the only place one is - // visible: a NIC that quietly kept its MSI-X table would make every line - // below a re-run of the happy path under a different name. - let argv = qemu::profile_argv(&options); - let devices = |kind: &str| -> Vec<&str> { - argv.windows(2) - .filter(|w| w[0] == "-device" && w[1].starts_with(kind)) - .map(|w| w[1].as_str()) - .collect() - }; - let nics = devices("virtio-net"); - let [nic] = nics[..] else { - return Err(format!("this profile is one NIC; argv has {nics:?}")); - }; - if !nic.contains("vectors=0") { - return Err(format!("{nic} still has its MSI-X table")); - } - for kind in ["virtio-sound", "virtio-serial"] { - let others = devices(kind); - let [other] = others[..] else { - return Err(format!("this profile is one {kind}; argv has {others:?}")); - }; - if other.contains("vectors=") { - return Err(format!( - "{other} is crippled too, so a refusal could not be shown to be per device \ - — and with no console there would be nothing to read it on" - )); - } - } - - // `tests/netcase` rather than the ordinary config, because it is the one - // that runs netd — and netd's own answer is the assertion below that the - // refusal reached userland rather than stopping at a log line. - let config = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/netcase"); - let (log, exited) = netd_answered(QemuInstance::boot_with_options(&config, &[], &[], options)); - - // Refused by name, at a named function, and not by claiming a mode it does - // not have: the xHCI driver's `polled mode` line is the defect this whole - // family exists to keep out of the tree. - refused_claim( - &log, - super::https::VIRTIO.claims, - "neither its MSI-X nor its MSI could be armed", - &[], - )?; - // And it reached userland rather than stopping at a log line. - exited?; - // And the machine is otherwise whole. `must_be_clean` is what makes the - // change from `panic!` an assertion rather than a hope. - log.must_say("virtio-sound: MSI-X vector")?; - log.must_say("Boot: complete")?; - log.must_be_clean()?; - Ok(()) -} - -/// A claimed function whose capability list ends at a link the spec forbids is -/// refused, and never armed on the older mechanism the walk did reach. -/// -/// No device in reach publishes that shape, so the actuator stages it — for -/// this claim's own walks and nothing else. -pub fn claim_caps_truncated() -> Result<(), String> { - // The bench whose claimed function publishes MSI as well: on one that - // publishes neither mechanism the refusal is the one `virtio_net_no_msix` - // already earns, and no table BAR is at stake. - let bench = super::https::E1000E; - let options = BootOptions { - profile: bench.profile, - kernel_params: &["pcidev-caps-truncated"], - ..Default::default() - }; - let config = super::compile::repo_root().join(bench.config); - let (log, exited) = netd_answered(QemuInstance::boot_with_options(&config, &[], &[], options)); - - // Refused by the reason that is true of it: what the list holds past that - // link was never read — not "it has no table". - refused_claim(&log, bench.claims, "its capability list ends at a link the PCI spec forbids", &[])?; - // And it reached userland rather than stopping at a log line. - exited?; - // And the machine is otherwise whole: one claim refused costs networking - // and nothing else. - log.must_say("Boot: complete")?; - log.must_be_clean()?; - Ok(()) -} - -/// The slot QEMU's `-device` order puts the function netd claims on, and the -/// address every judge below is an assertion about. -/// -/// **The address is the harness's own and never the guest's.** A judge that -/// reads the function out of the console and then asserts about *that* asserts -/// about whichever function the kernel happened to name; what the guest printed -/// is asserted equal to this instead, so a constant that names the wrong slot -/// reds and never passes. -pub const CLAIMED_AT: &str = "00:03.0"; - -/// The two lines a hand-over of that function spends. One arm requires them and -/// [`refused_claim`] requires their absence, and both read them here: a kernel -/// that stopped writing either line would otherwise satisfy both. -pub fn bar_moved() -> String { - format!("pcidev: PCI {CLAIMED_AT} BAR") -} - -pub fn msix_armed() -> String { - format!("PCI {CLAIMED_AT}: msix address=") -} - -/// The older mechanism taken where the newer one was published — required -/// absent by [`refused_claim`] and by [`super::iommu::armed_on_msix`], and read -/// here by both for [`msix_armed`]'s reason. -pub fn msi_armed() -> String { - format!("PCI {CLAIMED_AT}: msi address=") -} - -/// Every function named by a line carrying `marker`, in the kernel's own -/// spelling. -/// -/// **A line that carries the marker and no `pcidev: PCI ` prefix is an error, -/// never a dropped line.** A scan closes only the spellings it matches, so a -/// caller asking what a console named on *every* such line would otherwise be -/// answered about the subset this walk could parse — one refusal read and a -/// second one dropped is the case "and no other function" exists for. -pub fn functions_named<'a>(log: &'a Serial, marker: &str) -> Result, String> { - const PREFIX: &str = "pcidev: PCI "; - let mut named = Vec::new(); - for line in log.text().lines().filter(|line| line.contains(marker)) { - named.push( - line.split(PREFIX) - .nth(1) - .and_then(|rest| rest.split_whitespace().next()) - .ok_or_else(|| { - format!("{line:?} says {marker:?} and names no function after {PREFIX:?}") - })?, - ); - } - Ok(named) -} - -/// netd's own answer on a machine it was given no NIC on. -const NETD_EXITS: &str = "netd: no NIC on this machine, exiting"; - -/// Wait for that answer, and hand back the boot console beside it. -/// -/// netd is spawned before the ready marker and speaks after it, so its line is -/// drained for rather than read out of the boot capture. **What is waited for -/// is the whole line and not a prefix naming the program**: init reports the -/// claim it could not make as `init: netd: ...`, and that is already in the -/// boot capture before netd has run at all, so a `"netd: "` predicate is -/// satisfied by the wrong speaker. netd announces itself instead when the claim -/// was *not* refused, so a kernel that handed the function over ends the wait -/// at once rather than being waited out to the stall budget. -/// -/// The verdict is handed back rather than raised, so a caller judges the -/// kernel's own half first: a kernel that handed the function over fails on -/// what it printed about the function, not on what netd did about it. -fn netd_answered(mut qemu: QemuInstance) -> (Serial, Result<(), String>) { - const NETD_RUNS: &str = "netd: ready, at most "; - let mut text = qemu.boot_log().to_string(); - let stalled = qemu::await_guest(&mut qemu, &mut text, "netd's own answer", |c| { - c.contains(NETD_EXITS) || c.contains(NETD_RUNS) - }) - .err(); - let log = Serial::named("boot console", text); - let exited = if log.text().contains(NETD_EXITS) { - Ok(()) - } else { - Err(format!( - "{}{NETD_EXITS:?} never reached the boot console:\n{}", - stalled.map(|why| format!("{why}\n")).unwrap_or_default(), - log.text() - )) - }; - (log, exited) -} - -/// **The claim on [`CLAIMED_AT`] was refused for `why`, and the refusal spent -/// nothing**: no BAR of that function moved, neither of its two message -/// mechanisms is armed, `claims` reached no holder, and init said so in the -/// boot config's own spelling. `beside` is every other function this machine -/// refuses, each judged by its own caller. -/// -/// The three arms that refuse a claim read this one judge, so a kernel that -/// answered a refusal by logging it and handing the function over anyway is red -/// wherever the refusal is reached. `slot_space` put back below `place_bars` -/// reds on the two unspent lines. -pub fn refused_claim(log: &Serial, claims: &str, why: &str, beside: &[&str]) -> Result<(), String> { - let refused = functions_named(log, "NOT HANDED OVER")?; - let others: std::collections::BTreeSet<&str> = refused.iter().copied().filter(|at| *at != CLAIMED_AT).collect(); - if !refused.contains(&CLAIMED_AT) || others != beside.iter().copied().collect() { - return Err(format!( - "the claim this judges is the one on {CLAIMED_AT}, beside {beside:?}; this console \ - refused {refused:?}:\n{}", - log.text() - )); - } - // By the reason true of the path that raised it, on the line that names the - // function: a refusal whose reason belongs to another path is worse than no - // line at all. - log.must_say(&format!("pcidev: PCI {CLAIMED_AT} NOT HANDED OVER — {why}"))?; - log.must_not_say(&format!("[{claims}] handed over"))?; - log.must_not_say(&msix_armed())?; - log.must_not_say(&msi_armed())?; - log.must_not_say(&bar_moved())?; - // All the way out to userland, rather than a kernel that logged a refusal - // and handed netd a NIC anyway. init names what it could not mint in the - // config's own spelling, and **with this refusal's own word**: the machine - // has the function, so "no such device on this machine" would be false. - log.must_say(&format!( - "init: netd: pci:{claims} is on this machine and could not be handed over" - ))?; - Ok(()) -} - -/// A machine with no NVMe controller must boot, and its block service and -/// file servers serve what they have: absence of storage is a configuration, -/// not a failure. -pub fn diskless_boot( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { - profile: qemu::Profile::Diskless, - ..Default::default() - }; - // The teeth, and the only ones: absence is invisible to every console line - // and every screendump, so the argv is where it has to be checked. Only - // the value following `-device`/`-drive` is a device claim — every other - // element, including four filesystem paths, is not one, and a worktree - // checked out under a path containing "nvme" made a plain substring scan - // over the whole argv false-positive on itself. - let argv = qemu::profile_argv(&options); - if argv.windows(2).any(|w| (w[0] == "-device" || w[0] == "-drive") && w[1].contains("nvme")) { - return Err(format!("the diskless profile still has an NVMe device: {argv:?}")); - } - - let qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let log = crate::common::serial::Serial::boot(&qemu); - - // The two absence claims are only claims if the console carried anything, - // and `must_not_say` is what establishes that. The positives below made - // this safe by luck rather than by design -- reorder them and the panic - // scan is a claim about nothing again. - log.must_be_clean()?; - log.must_not_say("no controller found")?; - log.must_say("blockd: no NVMe controller this row names is on this machine; serving no partition")?; - log.must_say("fsd: this machine has no DATA partition;")?; - log.must_say("Boot: complete")?; - Ok(()) -} - /// The line `nested_nmi` writes to the UART, and this boot's ready marker: the /// machine halts on it. const NESTED: &str = "NESTED NMI"; -pub fn nested_nmi_is_loud( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { +pub fn nested_nmi_is_loud(test_config: &Path) -> Result<(), String> { let qemu = QemuInstance::boot_with_options( test_config, - c_bins, - rust_bins, + &[], + &[], BootOptions { kernel_params: &["nmi-nested"], - // `double_fault_stack`'s profile and for its reason: the nested-NMI - // report is a raw write — that handler may not reach the log ring - // at all (`arch::idt::nmi`) — so on any other profile it lands on a - // UART nothing here is reading. + // The profile whose 16550 is the console: the nested-NMI report is + // a raw write — that handler may not reach the log ring at all + // (`arch::idt::nmi`) — so on any other profile it lands on a UART + // nothing here is reading. profile: qemu::Profile::Metal, ready_marker: NESTED, ..Default::default() @@ -501,18 +28,6 @@ pub fn nested_nmi_is_loud( Ok(()) } -/// `[ist1] used N of M bytes, ...` -fn parse(line: &str) -> Option<(usize, usize)> { - let rest = line.split(MARKER).nth(1)?; - let mut words = rest.split_whitespace(); - let used = words.next()?.parse().ok()?; - if words.next()? != "of" { - return None; - } - let capacity = words.next()?.parse().ok()?; - Some((used, capacity)) -} - /// The blocked-task dump's NMI probe: a CPU that ignores a kick is named, and /// then asked where it is with the one interrupt it cannot mask. /// @@ -579,150 +94,3 @@ pub fn dump_nmi_probe_on_metal(kernel: &Serial) -> Result<(), String> { } Ok(()) } - -/// The blocked-task dump asked for where it may not be served, on one CPU: -/// `dump-in-blocking-pass` files one request in a kernel thread's blocking pass, -/// one in a user thread's, one in a pass entered above zero — which a thread -/// exiting from its syscall drives — and one during a report. Each staged pass -/// meets its request twice, with the clear a pass makes on entry between the -/// meetings, as a task woken behind it that blocks again would. -/// -/// One CPU, so no sibling's pass serves what a pass left. The stages arm at the -/// SMP release, so one may fire under the boot's own load; one that has not, -/// `test_rs_dump_stage_load` fires: every task leaves the CPU before a quantum -/// ends and one is always ready, so no tick and no idle loop comes, and the only -/// pass entered at zero is the one the leaving CPU owes itself. The bound is the -/// construction's own and not a duration: `need_resched` is set by every pass -/// that leaves a request, and the Ring 3 exit check runs a pass entered at zero -/// while it is set — so zero returns to Ring 3 with the request pending. -/// -/// Judged per request: it was left and that was said once, every report ran from -/// a pass entered at zero and none began inside another, the request filed during -/// a report got a report of its own, and the job finished clean. -pub fn dump_left_pending_is_owed( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - kernel_params: &["dump-in-blocking-pass"], - smp: 1, - ..Default::default() - }, - ); - // Nothing is staged before this line, which the release prints at the first - // pass after it: in the boot log, or after it on a boot that outran that pass. - const ARMED: &str = "dump-in-blocking-pass: armed"; - let mut armed = qemu.boot_log().to_string(); - if !armed.contains(ARMED) { - armed.push_str(&qemu.drain_until(Duration::from_secs(20), |line| line.contains(ARMED))); - } - if !armed.contains(ARMED) { - return Err(format!("the actuator never armed — is `dump-in-blocking-pass` on?\n{armed}")); - } - let result = qemu.run_test("test_rs_dump_stage_load", Duration::from_secs(60)); - let log = format!("{armed}{}{}{}", result.before, result.serial, result.stdout); - - // A panic's message is the line after the one that says where. - let mut panic = log.lines().skip_while(|line| !line.contains("PANIC")).take(2); - if let Some(line) = panic.next() { - return Err(format!( - "a `PANIC` line is in the log: `{} {}`\n{log}", - unstamped(line), - unstamped(panic.next().unwrap_or("")) - )); - } - // A request is filed only once the one before it is accounted for, so the - // lines from one filing to the next are that request's. - const FILED: &str = "files a request in "; - let lines: Vec<&str> = log.lines().collect(); - let starts: Vec = (0..lines.len()).filter(|&i| lines[i].contains(FILED)).collect(); - // What the filing line says, what the pass that left it says, and how many - // reports follow: the user thread's blocking pass hosts the request filed - // during a report. - const STAGES: [(&str, &str, usize); 3] = [ - ("a blocking pass of a kernel thread", "met the request in a blocking pass", 1), - ("a blocking pass of a user thread", "met the request in a blocking pass", 2), - ("a pass entered at preempt depth", "met the request in a pass entered at preempt depth", 1), - ]; - if starts.len() != STAGES.len() { - return Err(format!("{} request(s) filed in a pass, not {}\n{log}", starts.len(), STAGES.len())); - } - for (filed, left, reports) in STAGES { - let Some(at) = starts.iter().position(|&i| lines[i].contains(&format!("{FILED}{filed}"))) else { - return Err(format!("no request was filed in {filed}\n{log}")); - }; - let end = starts.get(at + 1).copied().unwrap_or(lines.len()); - let own = &lines[starts[at]..end]; - let count = |needle: &str| own.iter().filter(|line| line.contains(needle)).count(); - - // Once per request: a line per meeting is two, and a line never re-armed - // is none for the requests after the first. - if count(left) != 1 || count("met the request in ") != 1 { - return Err(format!( - "the request filed in {filed} was left and that was said {} time(s), not once\n{log}", - count("met the request in ") - )); - } - let mut open = false; - for line in own { - if line.contains("=== blocked-task dump:") { - if open { - return Err(format!("a report began inside another, after {filed}\n{log}")); - } - open = true; - } else if line.contains("=== end of dump ===") { - open = false; - } - } - if count("=== end of dump ===") != reports || count("files a request during a report") != reports - 1 { - return Err(format!( - "{} complete report(s) and {} request(s) filed during one after {filed}, not {reports} and {}\n{log}", - count("=== end of dump ==="), - count("files a request during a report"), - reports - 1, - )); - } - const FROM: &str = " reports from "; - if let Some(line) = own - .iter() - .find(|line| line.contains(FROM) && !line.contains("reports from a pass entered at preempt depth 0")) - { - return Err(format!("a pass that may not serve ran a report: `{}`\n{log}", unstamped(line))); - } - if count(FROM) != reports { - return Err(format!("{} of {reports} report(s) said where they ran after {filed}\n{log}", count(FROM))); - } - const OWED: &str = " time(s) with its request pending"; - if count(OWED) != 1 { - return Err(format!("the request filed in {filed} was accounted for {} time(s)\n{log}", count(OWED))); - } - if let Some(line) = own - .iter() - .find(|line| line.contains(OWED) && !line.contains("returned to Ring 3 0 time(s)")) - { - return Err(format!( - "a cpu that left a request went back to Ring 3 without serving it: `{}`\n{log}", - unstamped(line) - )); - } - } - if result.exit_code != Some(0) { - return Err(format!( - "the job whose passes were staged did not finish clean: exit {:?}\n{log}", - result.exit_code - )); - } - Ok(()) -} - -/// A kernel line without its `[kernel cpuN] ` stamp, which differs on every boot: a -/// quoted line that kept it would make every red of a rerun a different one. -fn unstamped(line: &str) -> &str { - let line = line.trim(); - line.strip_prefix("[kernel ").and_then(|rest| rest.split_once("] ")).map_or(line, |(_, said)| said) -} diff --git a/tests/common/fwvars.rs b/tests/common/fwvars.rs deleted file mode 100644 index 0dc3578b578..00000000000 --- a/tests/common/fwvars.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! The firmware's variable store, as OVMF keeps it in its `VARS` file: a -//! firmware volume holding an authenticated variable store, read and written -//! by the layout EDK2 declares for it (`MdeModulePkg/Include/Guid/ -//! VariableFormat.h`) and not by anything the loader shares. - -use std::path::Path; - -/// `EFI_FIRMWARE_VOLUME_HEADER`: its signature and its header's length. -const FV_SIGNATURE: (usize, &[u8]) = (0x28, b"_FVH"); -const FV_HEADER_LEN_AT: usize = 0x30; -/// `VARIABLE_STORE_HEADER`: signature GUID, size, format, state, reserved. -const STORE_HEADER: usize = 16 + 4 + 1 + 1 + 2 + 4; -/// `AUTHENTICATED_VARIABLE_HEADER`: start id, state, reserved, attributes, -/// monotonic count, time stamp, public key index, name size, data size, -/// vendor GUID. -const HEADER: usize = 2 + 1 + 1 + 4 + 8 + 16 + 4 + 4 + 4 + 16; -const START_ID: u16 = 0x55AA; -const VAR_ADDED: u8 = 0x3F; -/// `VAR_ADDED & VAR_IN_DELETED_TRANSITION`: still the variable until the -/// copy replacing it is added. -const IN_TRANSITION: u8 = 0x3E; - -pub struct Var { - pub name: String, - pub data: Vec, -} - -/// Where the variables begin and where the store ends. -fn store(bytes: &[u8]) -> Result<(usize, usize), String> { - let (at, sig) = FV_SIGNATURE; - if bytes.get(at..at + sig.len()) != Some(sig) { - return Err("the variable file is no firmware volume".into()); - } - let header = u16::from_le_bytes([bytes[FV_HEADER_LEN_AT], bytes[FV_HEADER_LEN_AT + 1]]) as usize; - let size = u32::from_le_bytes(bytes[header + 16..header + 20].try_into().expect("four bytes")) as usize; - Ok((header + STORE_HEADER, header + size)) -} - -/// One variable header in the store. -struct Found { - state: u8, - vendor: [u8; 16], - var: Var, -} - -/// Every variable header in the store, and where the erased space after -/// them begins. -fn walk(bytes: &[u8]) -> Result<(Vec, usize), String> { - let (mut at, end) = store(bytes)?; - let mut out = Vec::new(); - while at + HEADER <= end && u16::from_le_bytes([bytes[at], bytes[at + 1]]) == START_ID { - let word = |off: usize| u32::from_le_bytes(bytes[at + off..at + off + 4].try_into().expect("four bytes")) as usize; - let (name_len, data_len) = (word(36), word(40)); - let vendor: [u8; 16] = bytes[at + 44..at + 60].try_into().expect("sixteen bytes"); - let name_at = at + HEADER; - let units: Vec = bytes[name_at..name_at + name_len] - .chunks(2) - .map(|c| u16::from_le_bytes([c[0], c[1]])) - .take_while(|&u| u != 0) - .collect(); - let data = bytes[name_at + name_len..name_at + name_len + data_len].to_vec(); - out.push(Found { state: bytes[at + 2], vendor, var: Var { name: String::from_utf16_lossy(&units), data } }); - at = (name_at + name_len + data_len).next_multiple_of(4); - } - Ok((out, at)) -} - -/// The live variables under `vendor`, a GUID in the byte order `EFI_GUID` -/// stores. -pub fn live(path: &Path, vendor: &[u8; 16]) -> Result, String> { - let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; - Ok(walk(&bytes)? - .0 - .into_iter() - .filter(|found| found.vendor == *vendor && (found.state == VAR_ADDED || found.state == IN_TRANSITION)) - .map(|found| found.var) - .collect()) -} - -/// Add `name` under `vendor` with `attributes` and `data`, as the firmware -/// would have added it. -pub fn plant(path: &Path, vendor: &[u8; 16], name: &str, attributes: u32, data: &[u8]) -> Result<(), String> { - let mut bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; - let (_, end) = store(&bytes)?; - let (_, at) = walk(&bytes)?; - let mut units: Vec = name.encode_utf16().chain([0]).flat_map(u16::to_le_bytes).collect(); - let mut var = vec![0u8; HEADER]; - var[..2].copy_from_slice(&START_ID.to_le_bytes()); - var[2] = VAR_ADDED; - var[4..8].copy_from_slice(&attributes.to_le_bytes()); - var[36..40].copy_from_slice(&(units.len() as u32).to_le_bytes()); - var[40..44].copy_from_slice(&(data.len() as u32).to_le_bytes()); - var[44..60].copy_from_slice(vendor); - var.append(&mut units); - var.extend_from_slice(data); - if at + var.len() > end || bytes[at..at + var.len()].iter().any(|&b| b != 0xFF) { - return Err(format!("no erased room for {name} at byte {at} of the variable store")); - } - bytes[at..at + var.len()].copy_from_slice(&var); - std::fs::write(path, bytes).map_err(|e| format!("{}: {e}", path.display())) -} diff --git a/tests/common/gpt.rs b/tests/common/gpt.rs deleted file mode 100644 index 5127ee61dcc..00000000000 --- a/tests/common/gpt.rs +++ /dev/null @@ -1,340 +0,0 @@ -//! The boot partition, end to end: firmware, the ABI, and a real block driver. -//! -//! The parser's own reasoning is host-tested inside `toyos-gpt/`, over crafted -//! tables and every hostile field, and none of that needs a guest. What only a -//! guest can answer is whether the *identity* survives the trip — OVMF's -//! device path, the bootloader, `KernelArgs`, the kernel's USB storage driver -//! — and whether the parser finds that identity on a table it did not author. -//! -//! Ground truth is the disk image, read on the host by the `gpt` crate, which -//! is a different implementation from the one under test. The guest's own -//! account of the partition it booted from is exactly what is in question, so -//! it cannot also be the reference. -//! -//! The table on a second USB disk is built to be adversarial in the two ways that -//! matter. Its *first* entry is an ESP by type — so a matcher keying on the -//! type GUID, or taking the first partition, or taking the first ESP, gets a -//! different span than the one asserted. And the second boot moves the -//! matching entry by eight blocks, so a kernel that skips the firmware-versus- -//! table agreement check accepts a partition that is not where firmware said -//! it was. - -use std::collections::BTreeMap; -use std::path::Path; - -use gpt::disk::LogicalBlockSize; -use gpt::partition::Partition; -use gpt::partition_types; - -use super::qemu::{self, BootOptions, QemuInstance}; - -/// What the host knows about the boot image's ESP, before any guest runs. -struct Esp { - guid: String, - first_lba: u64, - last_lba: u64, -} - -impl Esp { - fn blocks(&self) -> u64 { - self.last_lba - self.first_lba + 1 - } -} - -pub fn boot_partition_identity( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let repo = super::compile::repo_root(); - let config = repo.join("tests/metalcase/system.toml"); - let dir = super::lane::dir(); - - // Built here rather than by `boot_with_options`, because the crafted - // table below has to carry this image's partition GUID and the image does - // not exist until it is built. `create_gpt_disk` draws a fresh random GUID - // every time, so there is no second build that would agree with this one. - // - // Through the harness's own door rather than straight into `toyos_build`, - // so this build is in the kernel census like every other: a staged boot - // builds nothing, and a build nothing counted would leave the run reporting - // a kernel it made and did not mention. - let dir_of_config = config.parent().expect("system.toml has a directory"); - let bytes = qemu::build_boot_image(dir_of_config, &[], &[], &[]); - let boot_image = dir.join("gpt-boot.img"); - std::fs::write(&boot_image, &bytes).map_err(|e| format!("write the boot image: {e}"))?; - - let esp = read_esp(&boot_image)?; - eprintln!( - " [gpt] the image the build produced: ESP {} at LBA {}+{}", - esp.guid, - esp.first_lba, - esp.blocks() - ); - - // Positive: the matching entry is third, behind an ESP-typed decoy, and - // sits exactly where firmware says it does. - let agreeing = dir.join("gpt-decoy-agree.img"); - craft_decoy_disk(&agreeing, &esp, 0)?; - let untouched = toyos_build::fingerprint::whole_device(&agreeing); - let log = boot(&config, &boot_image, &agreeing)?; - - let firmware = format!( - "gpt: firmware booted us from partition {} at LBA {}+{}", - esp.guid, - esp.first_lba, - esp.blocks() - ); - if !log.contains(&firmware) { - return Err(format!( - "the kernel did not report the partition the image actually has.\nwanted: {firmware}\n{}", - gpt_lines(&log) - )); - } - - // Entry 2 of 3 is the whole assertion: the decoy at entry 0 is an ESP too, - // so an index or a type would both have answered 0. - let carries = format!( - "carries the boot partition at LBA {}+{} (512-byte blocks), entry 2 of 3", - esp.first_lba, - esp.blocks() - ); - let Some(decoy) = device_saying(&log, &carries) else { - return Err(format!( - "the kernel did not find the boot partition where the table put it.\nwanted: \ - {carries}\n{}", - gpt_lines(&log) - )); - }; - - // And then the arm nothing else reaches. The stick this guest booted from - // is on the bus and carries the same partition — it is the real one, and - // the crafted entry above is a clone of it. Two devices claiming one - // unique partition GUID is the state `Resolution::Ambiguous` exists for, - // and the only safe answer is that this machine has no boot volume at all. - if !log.contains("carries the same partition GUID as device ") { - return Err(format!( - "a second device carrying the boot partition GUID did not make the answer \ - ambiguous.\n{}", - gpt_lines(&log) - )); - } - if !log.contains("this machine now has no boot volume") { - return Err(format!( - "the kernel kept a boot volume two devices were claiming.\n{}", - gpt_lines(&log) - )); - } - - // A boot partition on a disk is not consent to write the disk. - if let Some(diff) = - toyos_build::fingerprint::first_difference(&untouched, &toyos_build::fingerprint::whole_device(&agreeing)) - { - return Err(format!("finding our boot partition on a disk wrote the disk: {diff}")); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not complete:\n{log}")); - } - - // Negative: the same GUID, eight blocks to the left of where firmware saw - // it. Two accounts of one partition that disagree means this is not the - // disk firmware read, and the next thing anyone does with a boot volume is - // write to it. - let disagreeing = dir.join("gpt-decoy-shifted.img"); - craft_decoy_disk(&disagreeing, &esp, 8)?; - let log = boot(&config, &boot_image, &disagreeing)?; - - let refused = format!( - "gpt: device {decoy} puts {} at LBA {}+{} but firmware said {}+{}", - esp.guid, - esp.first_lba + 8, - esp.blocks() - 8, - esp.first_lba, - esp.blocks() - ); - if !log.contains(&refused) { - return Err(format!( - "the kernel accepted a partition that is not where firmware said it was.\nwanted: \ - {refused}\n{}", - gpt_lines(&log) - )); - } - if log.contains(&format!("gpt: device {decoy} carries the boot partition")) { - return Err(format!( - "the kernel claimed a boot volume it had just refused:\n{}", - gpt_lines(&log) - )); - } - // The stick is still the stick. Refusing the decoy must not cost the real - // partition, which is on the USB bus and where firmware said it was — and - // with the decoy refused there is no second claimant, so this boot *does* - // have a boot volume where the agreeing one above does not. - if device_saying(&log, "carries the boot partition at LBA ").is_none_or(|stick| stick == decoy) { - return Err(format!( - "refusing the shifted decoy cost the boot partition on the stick.\n{}", - gpt_lines(&log) - )); - } - if !log.contains("Boot: complete") { - return Err(format!("a refused partition cost the boot:\n{log}")); - } - - let _ = std::fs::remove_file(&boot_image); - let _ = std::fs::remove_file(&agreeing); - let _ = std::fs::remove_file(&disagreeing); - eprintln!( - " [gpt] matched behind an ESP-typed decoy, went ambiguous when a second device claimed \ - it, and refused an eight-block shift" - ); - Ok(()) -} - -/// The device a `gpt: device N …` line carrying `what` names. -fn device_saying(log: &str, what: &str) -> Option { - log.lines() - .filter(|l| l.contains(what)) - .find_map(|l| l.split("gpt: device ").nth(1)?.split(' ').next()?.parse().ok()) -} - -/// A boot off the stick with `decoy` on the bus ahead of it, so the decoy's -/// table is the first the kernel reads. -fn boot(config: &Path, boot_image: &Path, decoy: &Path) -> Result { - let qemu = QemuInstance::boot_with_options( - config.parent().expect("system.toml has a directory"), - &[], - &[], - BootOptions { - profile: qemu::Profile::UsbDiskRefusedFirst, - // Pristine, and this test is why that choice exists: it boots one - // crafted image twice, and the loader counts an image's attempts - // into a file on its own log partition before every handoff — so a - // second launch that saw the first one's writes is a retry and - // boots no kernel at all. - boot_image: Some(qemu::Staged::Pristine(boot_image.to_path_buf())), - usb_images: vec![decoy.to_path_buf()], - ..Default::default() - }, - ); - let log = qemu.boot_log().to_string(); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} during the boot:\n{log}")); - } - } - Ok(log) -} - -/// Every `gpt:` line the guest printed, for a failure message. -fn gpt_lines(log: &str) -> String { - let lines: Vec<&str> = log.lines().filter(|l| l.contains("gpt:")).collect(); - if lines.is_empty() { - return format!("the guest printed no gpt: line at all\n{log}"); - } - format!("what it said:\n{}", lines.join("\n")) -} - -/// The ESP of a boot image, as an implementation that is not ours reads it. -fn read_esp(image: &Path) -> Result { - let disk = gpt::GptConfig::new() - .writable(false) - .logical_block_size(LogicalBlockSize::Lb512) - .open(image) - .map_err(|e| format!("the built image has no readable GPT: {e}"))?; - - let esps: Vec<&Partition> = disk - .partitions() - .values() - .filter(|p| p.part_type_guid == partition_types::EFI) - .collect(); - let [esp] = esps.as_slice() else { - return Err(format!("the built image has {} ESPs, expected one", esps.len())); - }; - Ok(Esp { - guid: esp.part_guid.to_string().to_uppercase(), - first_lba: esp.first_lba, - last_lba: esp.last_lba, - }) -} - -/// A GPT whose third entry is the boot partition and whose first is a decoy -/// ESP, on a sparse disk big enough to hold both. -/// -/// `shift` moves the matching entry that many blocks to the right of where -/// firmware saw it, which is how the agreement check is given something to -/// refuse. Zero is the honest table. -fn craft_decoy_disk(path: &Path, esp: &Esp, shift: u64) -> Result<(), String> { - // Room for the boot partition's span, the two decoys behind it, and the - // backup table. Sparse, so the host pays for the few blocks written. - let lbas = esp.last_lba + 4096; - let file = std::fs::File::create(path).map_err(|e| format!("create the decoy disk: {e}"))?; - file.set_len(lbas * 512).map_err(|e| format!("size the decoy disk: {e}"))?; - drop(file); - - let mbr = gpt::mbr::ProtectiveMBR::with_lb_size(u32::try_from(lbas - 1).unwrap_or(0xFFFF_FFFF)); - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(path) - .map_err(|e| format!("open the decoy disk: {e}"))?; - mbr.overwrite_lba0(&mut file).map_err(|e| format!("write the protective MBR: {e}"))?; - drop(file); - - let mut disk = gpt::GptConfig::new() - .writable(true) - .initialized(false) - .logical_block_size(LogicalBlockSize::Lb512) - .open(path) - .map_err(|e| format!("open the decoy disk as GPT: {e}"))?; - disk.update_partitions(BTreeMap::new()) - .map_err(|e| format!("initialise the decoy table: {e}"))?; - - // Written in key order into array slots 0, 1, 2 — so the ESP-typed decoy - // is what anything selecting by type or by position would land on. - let after = esp.last_lba + 1; - let mut parts = BTreeMap::new(); - parts.insert( - 1, - part(partition_types::EFI, "11111111-2222-3333-4444-555555555555", after, after + 99), - ); - parts.insert( - 2, - part(partition_types::LINUX_FS, "66666666-7777-8888-9999-AAAAAAAAAAAA", after + 100, after + 199), - ); - parts.insert( - 3, - part(partition_types::EFI, &esp.guid, esp.first_lba + shift, esp.last_lba), - ); - disk.update_partitions(parts).map_err(|e| format!("write the decoy table: {e}"))?; - disk.write().map_err(|e| format!("persist the decoy table: {e}"))?; - - // Certify the instrument before trusting a green run: read the disk back - // with the same outside implementation and check it says what was meant. - let back = gpt::GptConfig::new() - .writable(false) - .logical_block_size(LogicalBlockSize::Lb512) - .open(path) - .map_err(|e| format!("the crafted disk does not parse: {e}"))?; - let seen: Vec = back - .partitions() - .values() - .map(|p| p.part_guid.to_string().to_uppercase()) - .collect(); - if seen.len() != 3 || seen[2] != esp.guid { - return Err(format!( - "the crafted disk holds {seen:?}, wanted three entries ending in {}", - esp.guid - )); - } - Ok(()) -} - -fn part(ty: partition_types::Type, guid: &str, first_lba: u64, last_lba: u64) -> Partition { - Partition { - part_type_guid: ty, - part_guid: guid.parse().expect("a literal GUID"), - first_lba, - last_lba, - flags: 0, - name: String::new(), - } -} diff --git a/tests/common/https.rs b/tests/common/https.rs deleted file mode 100644 index 3819d1e024c..00000000000 --- a/tests/common/https.rs +++ /dev/null @@ -1,342 +0,0 @@ -//! The `https_tls13` judge: `tests/https-server-host` on the host, the guest's -//! own `https_fetch` against it, and the same program built by the host's std -//! as the differential arm. -//! -//! The two arms fetch the same body from the same port and their printed lines -//! are compared whole, so a ToyOS socket that truncates, reorders or duplicates -//! is a disagreement rather than a smaller number nobody reads. - -use std::collections::BTreeMap; -use std::io::{BufRead, BufReader}; -use std::path::{Path, PathBuf}; -use std::process::{Child, Command, Stdio}; -use std::time::Duration; - -use super::qemu::{self, BootOptions, QemuInstance}; -use super::{compile, serial}; -use toyos_build::tether::Tether; - -/// Where the host arm sees the servers the guest reaches at -/// [`qemu::GUEST_VIEW_OF_HOST`]. The judge's certificate carries both. -const HOST_VIEW_OF_HOST: &str = "127.0.0.1"; - -/// Where the minted CA lands on ROOT, which mounts at `/system`. -const CA_ON_ROOT: &str = "etc/https-judge-ca.pem"; -const CA_IN_GUEST: &str = "/system/etc/https-judge-ca.pem"; - -/// Every refusal arm: the port role the server prints, and the one line the -/// client must answer with. `ok` is elsewhere — its line carries the digest. -const REFUSALS: &[(&str, &str)] = &[ - ("wrongname", "https_fetch: refused hostname-mismatch"), - ("expired", "https_fetch: refused certificate-expired"), - ("tls12", "https_fetch: refused tls12-refused"), - ("downgrade", "https_fetch: refused downgrade-refused"), - // A valid TLS server whose `302` names a cleartext URL: the peer chooses - // the second hop, so only `https_only` stands between it and plaintext. - ("redirect", "https_fetch: refused plain-http"), -]; - -/// Which machine the judge runs on. -/// -/// **Two of them, and the driver is the difference.** The same body is fetched -/// over the same slirp to the same host server, so a NIC driver that -/// truncates, reorders or duplicates a frame is a disagreement with the host's -/// own std rather than a smaller number nobody reads. -#[derive(Clone, Copy)] -pub struct Bench { - pub profile: qemu::Profile, - /// The boot config whose netd claims this machine's card. - pub config: &'static str, - /// The `-device` this profile must actually carry. Asked of the argv - /// rather than assumed: a harness field that can be silently inert is this - /// suite's worst defect class, and a profile with no NIC would make every - /// refusal below pass for the wrong reason. - pub device: &'static str, - /// Why this card has a BAR no read can settle a candidate against, or - /// `None` for a card with none. The count is asserted either way, so a - /// kernel that stopped refusing such a BAR — and handed its holder a - /// window proved by `0 == 0` — reds here. - pub kept_bar: Option<&'static str>, - /// The function [`Bench::config`]'s netd declares, as its `devices` row - /// spells it. Held to that committed row by - /// [`every_bench_claims_what_its_config_declares`]. - pub claims: &'static str, -} - -/// The record `pcidev` writes for a BAR it settles nothing against: the BAR -/// stays where firmware put it and no holder is given a window onto it. -const KEPT_BAR: &str = "keeps BAR"; - -/// The virtio NIC, which is the card every other network test uses. -pub const VIRTIO: Bench = Bench { - profile: qemu::Profile::Headless, - config: "tests/netcase", - device: "virtio-net", - kept_bar: None, - claims: "1af4:1041", -}; - -/// QEMU's `e1000e` — the 82574L, whose register file is the one the ThinkPad -/// T14's onboard I219 has. The only machine in reach that runs netd's Intel -/// driver at all. -pub const E1000E: Bench = Bench { - profile: qemu::Profile::E1000e, - config: "tests/e1000case", - device: "e1000e", - kept_bar: Some("answers all-zeroes or all-ones where firmware put it"), - claims: "8086:10d3", -}; - -/// Each bench's [`Bench::claims`] is the `devices` row of the boot config it -/// names, read off the committed file rather than restated beside it. -/// -/// A plain function, called from the harness's registration checks, for the -/// reason [`super::devices::the_config_runs_exactly_these_jobs`] gives. -pub fn every_bench_claims_what_its_config_declares() { - for bench in [VIRTIO, E1000E] { - let at = compile::repo_root().join(bench.config).join("system.toml"); - let config = - std::fs::read_to_string(&at).unwrap_or_else(|e| panic!("{}: {e}", at.display())); - let config: toml::Value = - toml::from_str(&config).unwrap_or_else(|e| panic!("parse {}: {e}", at.display())); - // netd's own row and not the file's: `tests/netcase` declares the same - // function twice, once for the daemon and once for the test binary that - // asks the kernel for a second claim on it. - let declared = config - .get("programs") - .and_then(|programs| programs.get("netd")) - .and_then(|netd| netd.get("devices")) - .and_then(toml::Value::as_array) - .unwrap_or_else(|| panic!("{}: [programs.netd] declares no devices", at.display())); - let declared: Vec<&str> = declared - .iter() - .map(|device| { - device - .as_str() - .unwrap_or_else(|| panic!("{}: {device} is not a device name", at.display())) - }) - .collect(); - assert_eq!( - declared, - [format!("pci:{}", bench.claims)], - "{} declares those devices, and the bench names {:?}", - at.display(), - bench.claims - ); - } -} - -/// Answers the boot console, up to netd's ready line: what the claim spent is -/// on it, and only the caller knows whether its bench can red an assertion -/// about that. -pub fn tls13_judge(rust_bins: &[(String, Vec)], bench: Bench) -> Result { - let bins: Vec<(String, Vec)> = rust_bins - .iter() - .filter(|(name, _)| name == "https_fetch") - .cloned() - .collect(); - if bins.is_empty() { - return Err("https_fetch was not built".to_string()); - } - - let server = Server::start()?; - let ca = std::fs::read(&server.ca) - .map_err(|e| format!("read the minted CA {}: {e}", server.ca.display()))?; - - let options = BootOptions { - profile: bench.profile, - extra_root_files: vec![(CA_ON_ROOT.to_string(), ca)], - ..Default::default() - }; - if !qemu::profile_argv(&options).iter().any(|a| a.contains(bench.device)) { - return Err(format!( - "this test needs a {} and the profile carries none", - bench.device - )); - } - let config = compile::repo_root().join(bench.config); - let mut guest = QemuInstance::boot_with_options(&config, &[], &bins, options); - let mut console = guest.boot_log().to_string(); - super::qemu::await_marker( - &mut guest, - &mut console, - "netd: ready, at most ", - "netd to come up", - ) - .map_err(|e| format!("netd never came up, so no fetch below means anything: {e}"))?; - - let log = serial::Serial::named("boot console", console.as_str()); - log.must_be_clean_apart_from(KEPT_BAR, usize::from(bench.kept_bar.is_some()))?; - // And by which refusal: the count alone would pass on a BAR kept back for - // any other reason. - if let Some(why) = bench.kept_bar { - log.must_say(why)?; - } - - let ok_line = format!( - "https_fetch: ok bytes={} sha256={}", - server.body_bytes, server.body_sha - ); - let good = server.port("ok")?; - let mut lines = Vec::new(); - - let guest_ok = fetch_in_guest(&mut guest, qemu::GUEST_VIEW_OF_HOST, good, true)?; - if guest_ok != ok_line { - return Err(format!("the guest fetched {guest_ok:?}, and the server served {ok_line:?}")); - } - lines.push(format!("ok: {guest_ok}")); - - for (role, expected) in REFUSALS { - let port = server.port(role)?; - let got = fetch_in_guest(&mut guest, qemu::GUEST_VIEW_OF_HOST, port, true)?; - if got != *expected { - return Err(format!("the {role} arm answered {got:?}, not {expected:?}")); - } - lines.push(format!("{role}: {got}")); - } - - // The CA is what makes the judge's own roots trusted, so withholding it is - // the unknown-authority arm rather than a separate server. - let unknown = fetch_in_guest(&mut guest, qemu::GUEST_VIEW_OF_HOST, good, false)?; - if unknown != "https_fetch: refused unknown-authority" { - return Err(format!("a fetch with no extra root answered {unknown:?}")); - } - lines.push(format!("unknown-authority: {unknown}")); - - let cleartext = server.port("plain")?; - let plain = run_guest( - &mut guest, - &format!( - "test_rs_https_fetch http://{}:{cleartext}/ --ca {CA_IN_GUEST}", - qemu::GUEST_VIEW_OF_HOST - ), - )?; - if plain != "https_fetch: refused plain-http" { - return Err(format!("a plain http:// fetch answered {plain:?}")); - } - lines.push(format!("plain-http: {plain}")); - - let host_ok = fetch_on_host(&format!( - "https://{HOST_VIEW_OF_HOST}:{good}/" - ), &server.ca)?; - if host_ok != guest_ok { - return Err(format!( - "the differential arms disagree: ToyOS answered {guest_ok:?} and the host's own std \ - answered {host_ok:?} for the same body on the same port" - )); - } - - for line in &lines { - eprintln!(" [https:{}] {line}", bench.device); - } - eprintln!(" [https:{}] host arm agreed byte for byte: {host_ok}", bench.device); - Ok(console) -} - -fn fetch_in_guest( - guest: &mut QemuInstance, - host: &str, - port: u16, - with_ca: bool, -) -> Result { - let ca = if with_ca { format!(" --ca {CA_IN_GUEST}") } else { String::new() }; - run_guest(guest, &format!("test_rs_https_fetch https://{host}:{port}/{ca}")) -} - -fn run_guest(guest: &mut QemuInstance, command: &str) -> Result { - let result = guest.run_test(command, Duration::from_secs(120)); - if let Some(err) = &result.error { - return Err(format!("{command}: {err}\n{}", result.stdout)); - } - if result.exit_code != Some(0) { - return Err(format!( - "{command} exited {:?}:\n{}", - result.exit_code, result.stdout - )); - } - answer(&result.stdout).ok_or_else(|| format!("{command} printed no verdict:\n{}", result.stdout)) -} - -/// The one line the program prints, out of a capture that may carry a daemon's. -fn answer(stdout: &str) -> Option { - stdout - .lines() - .find(|l| l.contains("https_fetch: ")) - .map(|l| l[l.find("https_fetch: ").expect("just matched")..].trim_end().to_string()) -} - -fn fetch_on_host(url: &str, ca: &Path) -> Result { - let out = Command::new(toyos_build::build::https_fetch_host(&compile::repo_root())) - .args([url, "--ca"]) - .arg(ca) - .output() - .map_err(|e| format!("run the host arm: {e}"))?; - let stdout = String::from_utf8_lossy(&out.stdout).to_string(); - answer(&stdout).ok_or_else(|| format!("the host arm printed no verdict:\n{stdout}")) -} - -/// The host servers, killed when this goes out of scope. -struct Server { - child: Child, - /// What ends the servers when this process dies without dropping this. - _tether: Tether, - ca: PathBuf, - body_bytes: usize, - body_sha: String, - ports: BTreeMap, -} - -impl Server { - fn start() -> Result { - let out = super::lane::dir().join("https-judge"); - std::fs::create_dir_all(&out).map_err(|e| format!("create {}: {e}", out.display()))?; - let mut cmd = Command::new(toyos_build::build::https_test_server(&compile::repo_root())); - cmd.arg("--out").arg(&out).stdout(Stdio::piped()); - let (mut child, tether) = toyos_build::tether::spawn(cmd) - .map_err(|e| format!("start the judge's servers: {e}"))?; - - let stdout = child.stdout.take().expect("a piped stdout"); - let mut ca = None; - let mut body_bytes = None; - let mut body_sha = None; - let mut ports = BTreeMap::new(); - // The server binds every listener before it prints `ready`, so nothing - // below races an accept loop that does not exist yet. - for line in BufReader::new(stdout).lines() { - let line = line.map_err(|e| format!("read the judge's contract: {e}"))?; - let mut field = line.split_whitespace(); - match (field.next(), field.next(), field.next()) { - (Some("ca"), Some(path), None) => ca = Some(PathBuf::from(path)), - (Some("body-bytes"), Some(n), None) => body_bytes = n.parse().ok(), - (Some("body-sha256"), Some(hex), None) => body_sha = Some(hex.to_string()), - (Some("port"), Some(role), Some(n)) => { - if let Ok(port) = n.parse() { - ports.insert(role.to_string(), port); - } - } - (Some("ready"), None, None) => break, - _ => return Err(format!("the judge's servers said {line:?}")), - } - } - - let (Some(ca), Some(body_bytes), Some(body_sha)) = (ca, body_bytes, body_sha) else { - let _ = child.kill(); - return Err("the judge's servers never announced a CA and a body".to_string()); - }; - Ok(Server { child, _tether: tether, ca, body_bytes, body_sha, ports }) - } - - fn port(&self, role: &str) -> Result { - self.ports - .get(role) - .copied() - .ok_or_else(|| format!("the judge's servers opened no {role} port")) - } -} - -impl Drop for Server { - fn drop(&mut self) { - let _ = self.child.kill(); - let _ = self.child.wait(); - } -} diff --git a/tests/common/inspect.rs b/tests/common/inspect.rs deleted file mode 100644 index fe8bd3b57ac..00000000000 --- a/tests/common/inspect.rs +++ /dev/null @@ -1,386 +0,0 @@ -//! `/system/bin/inspect` against the four owners it reads, on the one boot that -//! runs them all (`tests/inspectcase`). -//! -//! **Every selector is judged by the exact set of paths it printed**, spelled -//! out here and not recomputed with the reader's own matcher: a `*` that -//! matches too much is a path in the answer this file did not name, and one -//! that matches too little is a named path missing from it. Values are judged -//! where the machine fixes them — QEMU's user network leases `10.0.2.15/24`, -//! nothing plays audio, the boot stick carries one partition the kernel holds -//! and two file servers hold, and the USB stick this file crafts has one -//! partition free and one init grants — and read only for shape elsewhere. - -use std::collections::BTreeMap; -use std::path::Path; -use std::time::Duration; - -use super::qemu::{self, await_marker, BootOptions, QemuInstance, TestResult}; - -/// A liveness guard on one job, never a verdict. -const CEILING: Duration = Duration::from_secs(60); - -pub const CONFIG: &str = "tests/inspectcase"; - -/// The guest binary that holds the negative control. -pub const DENIED: &str = "inspect_denied"; -/// The guest binary that sends `SYS_DEVICE_INVENTORY` its edges. -pub const BOUNDS: &str = "inventory_bounds"; - -/// The crafted disk's partition nobody holds. -const FREE: &str = "9D1E2F30-4A5B-4C6D-8E7F-0A1B2C3D4E5F"; -/// The one init grants test-runner; mirrored in the config. -const GRANTED: &str = "B4C5D6E7-F809-4A1B-8C2D-3E4F5A6B7C8D"; -/// An entry whose first block is after its last, which no inventory lists. -const BACKWARDS: &str = "0D5C4B3A-2918-4F7E-8D6C-5B4A39281706"; - -/// Every path the reader answers for netd on a virtio NIC with a lease. -const NET: &[&str] = &[ - "net.driver", - "net.lease.address", - "net.lease.dns", - "net.lease.held", - "net.lease.router", - "net.lease.server", - "net.link.state", - "net.mac", - "net.piped.live", - "net.piped.max", - "net.sockets.listeners", - "net.sockets.tcp", - "net.sockets.udp", - "net.sockets.untabled", -]; - -pub fn boot(rust_bins: &[(String, Vec)]) -> Result { - let bins: Vec<(String, Vec)> = - rust_bins.iter().filter(|(name, _)| [DENIED, BOUNDS].contains(&name.as_str())).cloned().collect(); - if bins.len() != 2 { - return Err(format!("{DENIED} and {BOUNDS} were not both built")); - } - let stick = super::lane::dir().join("inspect-stick.img"); - let mib = 1024 * 1024; - super::partclaim::craft_stick(&stick, 8 * mib, &[("free", mib, FREE), ("granted", mib, GRANTED)])?; - state_backwards(&stick)?; - let config = Path::new(env!("CARGO_MANIFEST_DIR")).join(CONFIG); - let options = - BootOptions { profile: qemu::Profile::GopUsbDisk, usb_images: vec![stick], ..Default::default() }; - let argv = qemu::profile_argv(&options); - if !argv.iter().any(|a| a.contains("virtio-net")) || !argv.iter().any(|a| a.contains("virtio-sound")) { - return Err("this test needs a virtio NIC and a virtio sound card".to_string()); - } - let mut qemu = QemuInstance::boot_with_options(&config, &[], &bins, options); - let mut console = qemu.boot_log().to_string(); - // netd says it is ready once the lease question is settled, which is when - // `net.lease.*` has an answer to give. - await_marker(&mut qemu, &mut console, "netd: ready, at most ", "netd to come up")?; - await_marker(&mut qemu, &mut console, "compositor: ready", "the compositor to come up")?; - Ok(qemu) -} - -/// Write [`BACKWARDS`] into the first free entry of the table of the disk at -/// `path`, both copies resealed. -fn state_backwards(path: &Path) -> Result<(), String> { - let guid = |text: &str| uuid::Uuid::parse_str(text).map(|u| u.to_bytes_le()).map_err(|e| format!("{text}: {e}")); - let (ty, unique) = (guid(super::partclaim::PLAIN_TYPE)?, guid(BACKWARDS)?); - let mut image = std::fs::read(path).map_err(|e| format!("read {}: {e}", path.display()))?; - let len = image.len(); - super::volumes::rewrite_gpt(&mut image, len, |entries, entry_bytes| { - let free = entries.chunks_mut(entry_bytes).find(|e| e[..16] == [0; 16]).ok_or("the table has no free entry")?; - free[..16].copy_from_slice(&ty); - free[16..32].copy_from_slice(&unique); - free[32..40].copy_from_slice(&500u64.to_le_bytes()); - free[40..48].copy_from_slice(&400u64.to_le_bytes()); - Ok(()) - })?; - std::fs::write(path, image).map_err(|e| format!("write {}: {e}", path.display())) -} - -/// The `path = value` lines of a job's output, and nothing else the console -/// carried while it ran. -fn answer(result: &TestResult) -> BTreeMap { - result - .stdout - .lines() - .filter_map(|line| line.split_once(" = ")) - .filter(|(path, _)| { - path.contains('.') - && path.chars().all(|c| matches!(c, 'a'..='z' | '0'..='9' | '_' | '-' | ':' | '.')) - }) - .map(|(path, value)| (path.to_string(), value.to_string())) - .collect() -} - -/// Run one job and require it exited `code`. -fn job(qemu: &mut QemuInstance, line: &str, code: i32) -> Result { - let result = qemu.run_test(line, CEILING); - if let Some(err) = &result.error { - return Err(format!("`{line}`: {err}\n{}", result.stdout)); - } - if result.exit_code != Some(code) { - return Err(format!("`{line}` exited {:?}, not {code}:\n{}", result.exit_code, result.stdout)); - } - eprintln!(" [inspectcase] $ {line}"); - for (path, value) in answer(&result) { - eprintln!(" [inspectcase] {path} = {value}"); - } - Ok(result) -} - -/// The paths a job printed, against the exact set it must print. -fn exactly(line: &str, got: &BTreeMap, want: &[&str]) -> Result<(), String> { - let got_paths: Vec<&str> = got.keys().map(String::as_str).collect(); - let mut want: Vec<&str> = want.to_vec(); - want.sort(); - if got_paths != want { - return Err(format!("`{line}` printed {got_paths:?}, and the selector names {want:?}")); - } - Ok(()) -} - -fn value<'a>(line: &str, got: &'a BTreeMap, path: &str) -> Result<&'a str, String> { - got.get(path).map(String::as_str).ok_or_else(|| format!("`{line}` printed no {path}")) -} - -fn expect(line: &str, got: &BTreeMap, path: &str, want: &str) -> Result<(), String> { - match value(line, got, path)? { - v if v == want => Ok(()), - v => Err(format!("`{line}`: {path} = {v}, not {want}")), - } -} - -fn number(line: &str, got: &BTreeMap, path: &str) -> Result { - let v = value(line, got, path)?; - v.parse().map_err(|_| format!("`{line}`: {path} = {v} is not a count")) -} - -pub fn reads_its_owners(qemu: &mut QemuInstance) -> Result<(), String> { - let line = "inspect net.*"; - let got = answer(&job(qemu, line, 0)?); - exactly(line, &got, NET)?; - expect(line, &got, "net.driver", "virtio-net")?; - expect(line, &got, "net.link.state", "unreported")?; - expect(line, &got, "net.lease.held", "true")?; - expect(line, &got, "net.lease.address", "10.0.2.15/24")?; - if !value(line, &got, "net.mac")?.starts_with("52:54:00:") { - return Err(format!("`{line}`: net.mac is not QEMU's: {:?}", got["net.mac"])); - } - if number(line, &got, "net.piped.max")? == 0 { - return Err(format!("`{line}`: netd holds no piped connection at all")); - } - - // The pipe the owner asked for: `inspect` into `grep`, through the shell. - let line = "shell -c inspect sound.* | grep periods"; - let got = answer(&job(qemu, line, 0)?); - exactly(line, &got, &["sound.periods.drains", "sound.periods.submitted", "sound.periods.underruns"])?; - // Nothing on this boot has played a period. - expect(line, &got, "sound.periods.submitted", "0")?; - - let line = "inspect sound.*"; - let got = answer(&job(qemu, line, 0)?); - exactly( - line, - &got, - &[ - "sound.buffers", - "sound.channels", - "sound.device", - "sound.period_frames", - "sound.periods.drains", - "sound.periods.submitted", - "sound.periods.underruns", - "sound.rate_hz", - "sound.stream.clients", - "sound.stream.state", - "sound.wakes.late", - ], - )?; - expect(line, &got, "sound.device", "virtio-sound")?; - expect(line, &got, "sound.stream.state", "suspended")?; - expect(line, &got, "sound.stream.clients", "0")?; - - let line = "inspect log.*"; - let got = answer(&job(qemu, line, 0)?); - exactly( - line, - &got, - &["log.records.lost", "log.stream", "log.volume.bytes", "log.volume.part", "log.volume.path", "log.volume.state"], - )?; - expect(line, &got, "log.volume.state", "writing")?; - expect(line, &got, "log.stream", "off")?; - if !value(line, &got, "log.volume.path")?.starts_with("/log/") { - return Err(format!("`{line}`: log.volume.path is not on /log: {:?}", got["log.volume.path"])); - } - if number(line, &got, "log.volume.bytes")? == 0 { - return Err(format!("`{line}`: logd has written nothing this boot")); - } - - let line = "inspect display.*"; - let got = answer(&job(qemu, line, 0)?); - exactly( - line, - &got, - &[ - "display.cursor", - "display.frames.composite_us", - "display.frames.composited", - "display.frames.damage_px", - "display.frames.rects", - "display.height", - "display.width", - "display.windows.max", - "display.windows.open", - ], - )?; - expect(line, &got, "display.windows.open", "0")?; - if number(line, &got, "display.frames.composited")? == 0 { - return Err(format!("`{line}`: the compositor has composited no frame")); - } - - // A `*` in first place reaches every owner and the kernel, and one in last - // place stops at a whole segment: one `state` from each owner that has - // one, and each partition's. - let line = "inspect *.state"; - let got = answer(&job(qemu, line, 0)?); - let (dev, owners): (BTreeMap, BTreeMap) = - got.into_iter().partition(|(path, _)| path.starts_with("dev.")); - exactly(line, &owners, &["log.volume.state", "net.link.state", "sound.stream.state"])?; - if dev.is_empty() { - return Err(format!("`{line}` printed no partition's state")); - } - if let Some(path) = dev.keys().find(|p| !(p.starts_with("dev.disk.") && p.ends_with(".state"))) { - return Err(format!("`{line}` printed {path}, which is no partition's state")); - } - - inventory(qemu)?; - - let result = job(qemu, &format!("test_rs_{BOUNDS}"), 0)?; - for verdict in [ - "inventory bounds: an empty buffer answers ", - " records is refused whole", - "inventory bounds: 1025 records is refused", - "inventory bounds: a count whose length wraps is refused", - ] { - if !result.stdout.contains(verdict) { - return Err(format!("{BOUNDS} did not say {verdict:?}:\n{}", result.stdout)); - } - } - - // Exact, with no `*`, is one path and never a prefix. - let line = "inspect net.link"; - let got = answer(&job(qemu, line, 1)?); - exactly(line, &got, &[])?; - - // A malformed selector is refused by name and asks nobody. - let line = "inspect net*"; - let result = job(qemu, line, 2)?; - if !result.stdout.contains("a `*` is a whole segment") { - return Err(format!("`{line}` was not refused by name:\n{}", result.stdout)); - } - - let result = job(qemu, &format!("test_rs_{DENIED}"), 0)?; - for verdict in [ - "inspect denied: granted read netd, denied was refused by name", - "inventory denied: granted read dev.*, denied was refused by the kernel", - ] { - if !result.stdout.contains(verdict) { - return Err(format!("{DENIED} did not say {verdict:?}:\n{}", result.stdout)); - } - } - Ok(()) -} - -/// The value of every `holder.` under `at`. -fn holders<'a>(got: &'a BTreeMap, at: &str) -> Vec<&'a str> { - let under = format!("{at}.holder."); - got.iter().filter(|(p, _)| p.starts_with(&under)).map(|(_, v)| v.as_str()).collect() -} - -/// The `dev.disk..part` whose unique GUID is `unique`. -fn partition<'a>(line: &str, got: &'a BTreeMap, unique: &str) -> Result<&'a str, String> { - let unique = unique.to_ascii_lowercase(); - let found: Vec<&str> = got - .iter() - .filter(|(p, v)| p.starts_with("dev.disk.") && p.ends_with(".unique") && **v == unique) - .map(|(p, _)| p.trim_end_matches(".unique")) - .collect(); - match found.as_slice() { - [one] => Ok(one), - _ => Err(format!("`{line}`: {} partitions are {unique}, not one", found.len())), - } -} - -/// `inspect dev.*`: the kernel's inventory, judged where QEMU fixes it. The -/// virtio NIC is `1af4:1041` and netd holds it; the virtio sound card and the -/// framebuffer are classes soundd and the compositor hold; the Gop profile's -/// USB keyboard is on the xHCI; the boot stick carries ROOT, which this kernel -/// holds, and the ESP and the log partition, which file servers hold; and of -/// the crafted stick's two, one is free and test-runner holds the other. -fn inventory(qemu: &mut QemuInstance) -> Result<(), String> { - let line = "inspect dev.*"; - let got = answer(&job(qemu, line, 0)?); - if number(line, &got, "dev.cpus")? == 0 { - return Err(format!("`{line}`: the machine has no CPU")); - } - if number(line, &got, "dev.memory.total_bytes")? == 0 { - return Err(format!("`{line}`: the machine has no memory")); - } - let nic: Vec<&str> = got - .iter() - .filter(|(path, value)| path.starts_with("dev.pci.") && path.ends_with(".device") && *value == "1041") - .map(|(path, _)| path.trim_end_matches(".device")) - .collect(); - let [nic] = nic.as_slice() else { - return Err(format!("`{line}`: {} functions are a virtio NIC, not one", nic.len())); - }; - expect(line, &got, &format!("{nic}.vendor"), "1af4")?; - expect(line, &got, &format!("{nic}.driver"), "claimed")?; - for (at, want) in [ - (nic.to_string(), "netd"), - ("dev.class.virtio-sound".to_string(), "soundd"), - ("dev.class.framebuffer".to_string(), "compositor"), - ] { - if holders(&got, &at) != [want] { - return Err(format!("`{line}`: {at} is held by {:?}, not {want}", holders(&got, &at))); - } - } - if !got.iter().any(|(p, v)| p.starts_with("dev.pci.") && p.ends_with(".driver") && v == "kernel") { - return Err(format!("`{line}`: no PCI function is driven by the kernel")); - } - if !got.iter().any(|(p, v)| p.starts_with("dev.usb.") && p.ends_with(".function") && v == "keyboard") { - return Err(format!("`{line}`: no USB keyboard")); - } - if !got.keys().any(|p| p.starts_with("dev.disk.") && p.ends_with(".blocks")) { - return Err(format!("`{line}`: no block device")); - } - if !got.iter().any(|(p, v)| p.starts_with("dev.disk.") && p.ends_with(".state") && v == "kernel") { - return Err(format!("`{line}`: no partition is held by the kernel")); - } - let parts: Vec<&str> = got - .keys() - .filter(|p| p.starts_with("dev.disk.") && p.ends_with(".state")) - .map(|p| p.trim_end_matches(".state")) - .collect(); - let state = |part: &str| got.get(&format!("{part}.state")).map(String::as_str); - let free = partition(line, &got, FREE)?; - expect(line, &got, &format!("{free}.state"), "free")?; - if !holders(&got, free).is_empty() { - return Err(format!("`{line}`: {free} is free and held by {:?}", holders(&got, free))); - } - let granted = partition(line, &got, GRANTED)?; - expect(line, &got, &format!("{granted}.state"), "claimed")?; - if holders(&got, granted) != ["test-runner"] { - return Err(format!("`{line}`: {granted} is held by {:?}, not test-runner", holders(&got, granted))); - } - let by_fsd = parts.iter().filter(|p| state(p) == Some("claimed") && holders(&got, p) == ["fsd"]).count(); - if by_fsd != 2 { - return Err(format!("`{line}`: {by_fsd} partitions are claimed by fsd, not the ESP and the log partition")); - } - if got.values().any(|v| *v == BACKWARDS.to_ascii_lowercase()) { - return Err(format!("`{line}` lists {BACKWARDS}, whose first block is after its last")); - } - let refused = format!("({BACKWARDS}) at LBA 500..=400, whose blocks are no partition on it"); - if !format!("{}{}", qemu.uart_log(), qemu.boot_log()).contains(&refused) { - return Err(format!("the kernel did not say it refused {BACKWARDS}")); - } - Ok(()) -} diff --git a/tests/common/iommu.rs b/tests/common/iommu.rs index a1afa124d1f..43f7618613e 100644 --- a/tests/common/iommu.rs +++ b/tests/common/iommu.rs @@ -1,641 +1,22 @@ -//! Stage I1: what the kernel read off the machine's remapping units. -//! -//! The trap this gate exists to avoid is the one a discovery test falls into -//! by default. A kernel that printed a plausible capability line without -//! reading a register would satisfy any single-machine assertion, and so would -//! a decode reading the wrong bits of the right register. So the assertions -//! are not "the line is there": three machines are booted whose units differ -//! in exactly one advertised capability each, and the gate is that the guest's -//! decode *moves with them*. A constant cannot track a register it never read. -//! -//! Ground truth is split, deliberately. Whether the unit exists at all is -//! invisible to every console line — a kernel that says "no DMAR" on a machine -//! that has one and a harness that forgot the device produce the same log — so -//! presence is checked against the argv, which is the host side of the device. -//! What the unit *says* can only come from the guest, so that half is checked -//! against the console. +//! `iommu_virtio_platform`: whether each virtio function QEMU creates behind +//! its emulated VT-d unit, and none created without one, negotiated +//! `VIRTIO_F_ACCESS_PLATFORM`. use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; -use std::time::Duration; use super::qemu::{self, BootOptions, Profile, QemuInstance}; - -/// Offsets into a unit's register window, Sections 11.4.4.2, 11.4.6 and 11.4.10. -const GSTS_REG: u64 = 0x1C; -const RTADDR_REG: u64 = 0x20; -const IRTA_REG: u64 = 0xB8; - -/// Bits 51:12 of a root, context or second-level entry, Sections 9.1 to 9.8. -const ENTRY_ADDR: u64 = 0x000F_FFFF_FFFF_F000; -/// The one leaf size this kernel writes. -const PAGE_2M: u64 = 2 * 1024 * 1024; use super::serial::Serial; -/// The five machines, and what each one moves. -/// -/// [`Profile::Metal`] is the reference: the configuration every other profile -/// in the suite runs, so a difference below is a difference the profile made -/// and not one the shape did — all five are metal-sim and differ in the unit -/// alone. -const MACHINES: &[Profile] = &[ - Profile::Metal, - Profile::NoIommu, - Profile::IommuNarrow, - Profile::IommuNoIntremap, - Profile::IommuEim, -]; - -pub fn iommu_discovery( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut decoded: BTreeMap<&str, BTreeMap> = BTreeMap::new(); - - for &profile in MACHINES { - let name = profile_name(profile); - let options = BootOptions { profile, qmp: true, ..Default::default() }; - argv_check(profile, &qemu::profile_argv(&options))?; - - let qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let log = Serial::boot(&qemu); - // Discovery runs in the storage phase, long before userland; a machine - // that did not finish booting is a machine whose log says nothing - // about what came after the unit. - log.must_be_clean()?; - log.must_say("Boot: complete")?; - - let Some(unit) = profile.iommu() else { - interrupt_format(&log, name, None, None)?; - // `Absent` is firmware answering the question, and the answer is - // one a user can act on — so the line names the firmware setting - // as well as the hardware. What makes this assertion mean - // something is the pair below it: a kernel that always printed - // this would fail on every other machine here. - log.must_say("iommu: no DMAR table")?; - log.must_say("VT-d is disabled in firmware setup")?; - log.must_not_say("iommu: unit")?; - log.must_not_say("iommu: DMAR haw=")?; - eprintln!(" [iommu] {name}: no DMAR, and no unit described"); - continue; - }; - - // The kernel reports the width one greater than the field holds, so a - // machine declaring 48 bits of host address is the aw-bits the profile - // asked for. Both halves of the table's own header are asserted: - // `INTR_REMAP` is a platform-level flag and `ECAP.IR` below is the - // unit's, and the kernel refuses on them separately. - log.must_say(&format!("iommu: DMAR haw={}", unit.aw_bits))?; - log.must_say(&format!( - "intr_remap={}", - if unit.intremap { 'y' } else { 'n' } - ))?; - - let line = log.must_say("iommu: unit0 @")?; - let fields = unit_fields(line); - let field = |k: &str| -> Result { - fields - .get(k) - .cloned() - .ok_or_else(|| format!("{name}: the unit line has no {k}= field: {line:?}")) - }; - - // The decode, against what the profile asked QEMU for. - expect(&field("aw")?, &unit.aw_bits.to_string(), "aw", name, line)?; - expect(&field("ir")?, if unit.intremap { "y" } else { "n" }, "ir", name, line)?; - expect(&field("eim")?, if unit.eim { "y" } else { "n" }, "eim", name, line)?; - // Not a profile dimension, and asserted because the whole suite rests - // on it: `caching-mode=on` is what makes QEMU's IOTLB a real cache and - // the map-side invalidation load-bearing, and 2 MiB - // leaf entries are what this kernel's one page size requires. - expect(&field("cm")?, "y", "cm", name, line)?; - expect(&field("sps2m")?, "y", "sps2m", name, line)?; - - // Stage I2, on the guest's own word. It is the weakest of the three - // things that certify it and it is here because it costs no boot: the - // suite booting green with the unit on is the second, and the two - // actuator gates below — a device the unit blocks — are the only ones - // that can tell translation from a unit that is merely switched on. - let unit_line = log.must_say("translating gsts=")?; - let tes = unit_fields(unit_line) - .get("tes") - .cloned() - .ok_or_else(|| format!("{name}: no tes= on {unit_line:?}"))?; - expect(&tes, "y", "tes", name, unit_line)?; - - // Every scope naming a PCI function must name one this machine has. - // A decode that read the path bytes at the wrong offset would produce - // requester ids that look like addresses and match no device. - let scopes = scope_check(&log, name)?; - - // The `intremap=off` machine is what makes this mean something: the same - // parser over the same lines has to reach the opposite verdict on it. - interrupt_format(&log, name, Some(qemu.qmp_socket()), unit.intremap.then_some(unit.eim))?; - - eprintln!( - " [iommu] {name}: aw={} ir={} cap={} ecap={} — {scopes} PCI scopes matched", - field("aw")?, - field("ir")?, - field("cap")?, - field("ecap")? - ); - decoded.insert(name, fields); - } - - // The negative control, and the reason this test boots five machines - // instead of one. Each pair below differs in one QEMU knob, so a decode - // that reports the same value for both is a decode that is not reading the - // register the knob moves. - for (a, b, key) in [ - (profile_name(Profile::Metal), profile_name(Profile::IommuNarrow), "aw"), - (profile_name(Profile::Metal), profile_name(Profile::IommuNoIntremap), "ir"), - (profile_name(Profile::Metal), profile_name(Profile::IommuEim), "eim"), - ] { - let (Some(left), Some(right)) = (decoded.get(a), decoded.get(b)) else { - return Err(format!("{a} or {b} produced no unit line to compare")); - }; - let (Some(lv), Some(rv)) = (left.get(key), right.get(key)) else { - return Err(format!("no {key}= on {a} or {b}")); - }; - if lv == rv { - return Err(format!( - "{a} and {b} both report {key}={lv}, but their units advertise different \ - capabilities — the kernel is printing a constant, not decoding a register" - )); - } - // And the raw register the field came out of has to have moved too. A - // decode of the right register reported through the wrong field would - // pass the line above on one of these pairs by accident. - let raw = if key == "aw" { "cap" } else { "ecap" }; - let (Some(lr), Some(rr)) = (left.get(raw), right.get(raw)) else { - return Err(format!("no {raw}= on {a} or {b}")); - }; - if lr == rr { - return Err(format!( - "{a} and {b} report {key}={lv}/{rv} out of the same {raw}={lr} — the value did \ - not come from that register" - )); - } - eprintln!(" [iommu] {a} vs {b}: {key} {lv} != {rv}, out of {raw} {lr} != {rr}"); - } - - destination_encoding(test_config, c_bins, rust_bins) -} - -/// The two ways an entry can name a CPU, told apart. -/// -/// `EIME` puts a 32-bit id at `DST` 63:32 and its absence an 8-bit one at 47:40 -/// (Section 9.9) — the same bits for APIC 0, which is where every interrupt in -/// this kernel goes, so a kernel with the two backwards boots green everywhere. -/// `iommu-dest-apic1` moves the device messages to APIC 1, where they differ. -fn destination_encoding( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut seen = Vec::new(); - for (profile, extended) in [(Profile::Metal, false), (Profile::IommuEim, true)] { - let options = BootOptions { - profile, - qmp: true, - kernel_params: &["iommu-dest-apic1"], - ..Default::default() - }; - let qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let log = Serial::boot(&qemu); - log.must_be_clean()?; - log.must_say("Boot: complete")?; - let name = profile_name(profile); - interrupt_format(&log, name, Some(qemu.qmp_socket()), Some(extended))?; - - // A PCI function's entry: the pins keep the boot CPU either way. - let entries = table_entries(&log, name)?; - let moved = entries - .iter() - .find(|e| e.apic == 1) - .ok_or_else(|| format!("{name}: no entry was moved to APIC 1 by the actuator"))?; - let base = interrupt_table_base(&log, name, qemu.qmp_socket())?; - let (lo, _) = table_word(qemu.qmp_socket(), base, moved.index)?; - seen.push((name, lo >> 32)); - eprintln!(" [iommu] {name}: APIC 1 encodes as DST {:#x}", lo >> 32); - } - let [(a, left), (b, right)] = seen[..] else { - return Err("the destination arm booted the wrong number of machines".to_string()) - }; - if left == right { - return Err(format!( - "{a} and {b} both put APIC 1 at DST {left:#x}, and one has EIME set and the other \ - does not — the encoding is not moving with the mode" - )); - } - Ok(()) -} - -/// The table's address out of `IRTA_REG`, over the monitor. -fn interrupt_table_base(log: &Serial, name: &str, socket: &Path) -> Result { - let window = register_window(socket, log, name)?; - Ok(over_qmp(socket, window + IRTA_REG, 1, 'g')?[0] & !0xFFF) -} - -fn table_word(socket: &Path, base: u64, index: u16) -> Result<(u64, u64), String> { - let words = over_qmp(socket, base + u64::from(index) * 16, 2, 'g')?; - Ok((words[0], words[1])) -} - -/// Every interrupt source in the machine, in the format the hardware holds it. -/// -/// Stage I3, and the trap is a source nobody moved. The specification blocks a -/// compatibility-format message under `IRE` with `CFI` clear, so on real -/// hardware a source left behind is a device that has silently stopped — but -/// QEMU delivers it anyway -/// (`issues/kernel/qemu-passes-compatibility-format-interrupts.md`), so no -/// behavioural test in this suite can see one and this is the only thing that -/// can. So it starts at hardware: `GSTS` and `IRTA_REG` are read out of the -/// unit's register window over the monitor, the table is read at **the address -/// `IRTA_REG` holds** and not the one the kernel printed, and every requester id -/// is checked against this machine's PCI walk and DMAR scope. The kernel's line -/// is checked against all of it — naming a page the register does not hold reds. -/// -/// [`Profile::Headless`] carries the most sources of both kinds — the i8042's -/// two pins, and xHCI, virtio-net and virtio-sound over MSI-X. -pub fn iommu_interrupt_remapping( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { profile: Profile::Headless, qmp: true, ..Default::default() }; - unit_is_first(&qemu::profile_argv(&options), "headless")?; - let qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let log = Serial::boot(&qemu); - log.must_be_clean()?; - log.must_say("Boot: complete")?; - interrupt_format(&log, "headless", Some(qemu.qmp_socket()), Some(false)) -} - -/// `count` words of guest *physical* address space at `base`, over the monitor. -/// `xp` reaches the unit's MMIO window as readily as RAM, which is what lets the -/// checks below start at a register rather than at a number the guest printed. -fn over_qmp(socket: &Path, base: u64, count: usize, width: char) -> Result, String> { - let dump = qemu::QmpMonitor::open(socket).human(&format!("xp/{count}x{width} 0x{base:x}")); - let mut words = Vec::new(); - for token in dump.split_whitespace() { - let Some(hex) = token.strip_prefix("0x") else { continue }; - let hex = hex.trim_end_matches(|c: char| !c.is_ascii_hexdigit()); - words.push( - u64::from_str_radix(hex, 16) - .map_err(|_| format!("unreadable word {token:?} in\n{dump}"))?, - ); - } - if words.len() != count { - return Err(format!( - "the monitor returned {} words for {count} at {base:#x}:\n{dump}", - words.len() - )); - } - Ok(words) -} - -/// One machine's sources, judged against whether its unit remaps at all and, -/// if it does, whether its entries name a 32-bit destination. -fn interrupt_format( - log: &Serial, - name: &str, - socket: Option<&Path>, - mode: Option, -) -> Result<(), String> { - let remapping = mode.is_some(); - let entries = table_entries(log, name)?; - if remapping != !entries.is_empty() { - return Err(format!( - "{name}: the unit remaps interrupts = {remapping}, and the kernel wrote {} table \ - entries. Neither number is allowed to move without the other", - entries.len() - )); - } - - // A machine with no unit prints no unit line, and must be one that does not remap. - let sources = source_formats(log, name)?; - if sources.is_empty() { - return Err(format!( - "{name}: this machine armed no interrupt source at all, so there is nothing here to \ - be in the right format" - )); - } - for source in &sources { - if source.remappable != remapping { - return Err(format!( - "{name}: {} is in {} format and the unit remaps = {remapping}. Under IRE with \ - CFI clear a compatibility-format message is blocked, so this source has stopped", - source.who, - if source.remappable { "remappable" } else { "compatibility" } - )); - } - } - - let Some(line) = log.text().lines().find(|l| l.contains("translating gsts=")).map(str::to_string) - else { - if remapping { - return Err(format!("{name}: no unit is translating, so none can be remapping")); - } - return report(log, name, mode, &sources); - }; - let fields = unit_fields(&line); - let field = |k: &str| -> Result { - fields.get(k).cloned().ok_or_else(|| format!("{name}: no {k}= on {line:?}")) - }; - let socket = socket.ok_or_else(|| format!("{name}: this gate needs BootOptions {{ qmp }}"))?; - let window = register_window(socket, log, name)?; - - // GSTS and IRTA_REG out of that window, and the kernel's line checked - // against them — the only direction that catches a kernel misreporting them. - let gsts = over_qmp(socket, window + GSTS_REG, 1, 'w')?[0] as u32; - let irta = over_qmp(socket, window + IRTA_REG, 1, 'g')?[0]; - let ires = gsts & (1 << 25) != 0; - let cfis = gsts & (1 << 23) != 0; - expect(&field("gsts")?, &format!("{gsts:#010x}"), "gsts", name, &line)?; - expect(&field("ires")?, if ires { "y" } else { "n" }, "ires", name, &line)?; - expect(&field("cfis")?, if cfis { "y" } else { "n" }, "cfis", name, &line)?; - expect(&field("irta")?, &format!("{irta:#x}"), "irta", name, &line)?; - if ires != remapping { - return Err(format!("{name}: GSTS.IRES is {ires} where the unit remaps = {remapping}\n{line}")); - } - // `CFI` is the bit that would let a compatibility message through. It cannot - // fail here — QEMU defines VTD_GCMD_CFI and VTD_GSTS_CFIS and references - // neither — so it states the kernel's intent for whoever reads it on - // hardware; it is not an instrument. - if cfis { - return Err(format!( - "{name}: GSTS.CFIS is set, so the unit passes compatibility-format interrupts through \ - unremapped\n{line}" - )); - } - - let mut memory = Vec::new(); - if let Some(extended) = mode { - // The address the unit walks is IRTA's, never the kernel's `irt=`. - let base = irta & !0xFFF; - if (irta & (1 << 11) != 0) != extended { - return Err(format!( - "{name}: IRTA_REG is {irta:#x}, whose EIME is {} where this unit's ECAP.EIM says \ - {extended}\n{line}", - irta & (1 << 11) != 0 - )); - } - expect(&field("irt")?, &format!("{base:#x}"), "irt", name, &line)?; - let highest = entries.iter().map(|e| e.index).max().unwrap_or(0) as usize; - memory = over_qmp(socket, base, (highest + 1) * 2, 'g')? - .chunks(2) - .map(|pair| (pair[0], pair[1])) - .collect(); - } - - if !remapping { - return report(log, name, mode, &sources); - } - - // Two requester ids that no single source could produce: a PCI function's, - // which the walk printed, and the I/O APIC's, which sits on a pseudo-bus no - // walk reaches and exists only in the DMAR scope. - let functions = enumerated_functions(log); - let apics = scope_sources(log); - if apics.is_empty() { - return Err(format!("{name}: the unit named no I/O APIC scope to take a source id from")); - } - - // The handle a source carries has to reach the entry that verifies *its - // own* requester id. A source pointed at somebody else's entry would be - // refused by the unit for source-id verification, and a gate that only - // asked whether the entry existed would call that correct. - for source in &sources { - let want = match &source.requester { - Requester::Function(bdf) => bdf.clone(), - Requester::Controller(id) => apics.get(id).cloned().ok_or_else(|| { - format!("{name}: {} sits on a chip the unit's scopes never named", source.who) - })?, - }; - let Some(entry) = entries.iter().find(|e| e.index == source.handle) else { - return Err(format!( - "{name}: {} carries handle {}, and the kernel wrote no table entry with that \ - index — the unit would refuse it as out of bounds", - source.who, source.handle - )); - }; - if entry.source != want { - return Err(format!( - "{name}: {} carries handle {}, and irte{} is verified against {} rather than \ - {want} — the unit refuses that message for source-id verification", - source.who, source.handle, entry.index, entry.source - )); - } - } - - let mut from_pci = 0usize; - let mut from_apic = 0usize; - for entry in &entries { - let (lo, hi) = memory[entry.index as usize]; - // Section 9.9: P bit 0, V 23:16, DST 63:32, SID 79:64, SQ 81:80, SVT 83:82. - let (svt, sq, sid) = ((hi >> 18) & 0x3, (hi >> 16) & 0x3, hi & 0xFFFF); - if svt != 1 || sq != 0 { - return Err(format!( - "{name}: irte{} is SVT={svt} SQ={sq} in the memory the unit reads, so a message \ - carrying any other requester id would be remapped through it. Every entry is \ - verified against all sixteen bits of one source id", - entry.index - )); - } - if lo & 1 == 0 { - return Err(format!("{name}: irte{} is not Present in memory", entry.index)); - } - // Not two witnesses: the kernel's read of these bytes against the - // host's, which catches it reporting a table the register does not name. - if format!("{sid:#06x}") != entry.sid { - return Err(format!( - "{name}: irte{} carries SID {sid:#06x} in memory and the kernel reported {}", - entry.index, entry.sid - )); - } - // `entry.apic` is the id the kernel was *given*; `DST` is where it put - // it. Section 9.9 puts a 32-bit id at 63:32 under EIME and an 8-bit one - // at 47:40 without, so the two differ for every id but 0. - let dst = lo >> 32; - let want = if mode == Some(true) { entry.apic } else { entry.apic << 8 }; - if dst != want { - return Err(format!( - "{name}: irte{} has DST {dst:#x} where APIC {:#x} in {} mode encodes as {want:#x}", - entry.index, - entry.apic, - if mode == Some(true) { "extended" } else { "xAPIC" } - )); - } - if apics.values().any(|sid| *sid == entry.source) { - from_apic += 1; - } else if functions.contains(&entry.source) { - from_pci += 1; - } else { - return Err(format!( - "{name}: irte{} is verified against {}, which is neither a function this machine \ - enumerated ({functions:?}) nor an I/O APIC the unit scoped ({apics:?})", - entry.index, entry.source - )); - } - } - if from_pci == 0 || from_apic == 0 { - return Err(format!( - "{name}: {from_pci} entries name a PCI function and {from_apic} name the I/O APIC. \ - Both paths into the unit have to be covered or half of this gate is vacuous" - )); - } - let indices: BTreeSet = entries.iter().map(|e| e.index).collect(); - if indices.len() != entries.len() { - return Err(format!( - "{name}: {} entries over {} distinct indices — two sources share a handle, so one \ - of them is delivered as the other", - entries.len(), - indices.len() - )); - } - - report(log, name, mode, &sources)?; - eprintln!( - " [iommu] {name}: {} entries at the address IRTA_REG holds ({from_pci} pci, \ - {from_apic} ioapic), all SVT=1 SQ=0 Present", - entries.len() - ); - Ok(()) -} - -fn report(log: &Serial, name: &str, mode: Option, sources: &[Source]) -> Result<(), String> { - let _ = log; - match mode { - None => eprintln!( - " [iommu] {name}: no remapping, and all {} source(s) in compatibility format", - sources.len() - ), - Some(extended) => eprintln!( - " [iommu] {name}: IRES=1 CFIS=0 EIME={}, {} source(s) remappable", - u8::from(extended), - sources.len() - ), - } - Ok(()) -} - -/// What the kernel reported reading back out of one entry, all of it cross-checked against memory. -struct Entry { - index: u16, - source: String, - sid: String, - /// The APIC id it was handed, as against the `DST` field it encoded into. - apic: u64, -} - -fn table_entries(log: &Serial, name: &str) -> Result, String> { - let mut entries = Vec::new(); - for line in log.text().lines() { - let Some(rest) = line.split("iommu: irte").nth(1) else { continue }; - let (index, _) = rest - .split_once(' ') - .ok_or_else(|| format!("{name}: unreadable table entry line: {line:?}"))?; - let index: u16 = index - .parse() - .map_err(|_| format!("{name}: {index:?} is not an entry index: {line:?}"))?; - let fields = unit_fields(line); - let field = |k: &str| -> Result { - fields.get(k).cloned().ok_or_else(|| format!("{name}: no {k}= on {line:?}")) - }; - entries.push(Entry { - index, - source: field("source")?, - sid: field("sid")?, - apic: u64::from_str_radix(field("apic")?.trim_start_matches("0x"), 16) - .map_err(|_| format!("{name}: unreadable apic on {line:?}"))?, - }); - } - Ok(entries) -} - -enum Requester { - /// A PCI function, which the walk printed as `bb:dd.f`. - Function(String), - /// An interrupt controller, by MADT id: its requester id exists only in the DMAR. - Controller(String), -} - -struct Source { - who: String, - requester: Requester, - remappable: bool, - handle: u16, -} +/// The function `tests/netcase`'s netd claims, as its `devices` row spells it. +const NETD_CLAIMS: &str = "1af4:1041"; -fn source_formats(log: &Serial, name: &str) -> Result, String> { - let mut sources = Vec::new(); - for line in log.text().lines() { - let fields = unit_fields(line); - if let Some(rest) = line.split("ioapic: gsi ").nth(1) { - let gsi = rest.split(' ').next().unwrap_or_default(); - let (Some(id), Some(rte)) = (fields.get("id"), fields.get("rte")) else { - return Err(format!("{name}: unreadable redirection entry line: {line:?}")); - }; - let rte = u64::from_str_radix(rte.trim_start_matches("0x"), 16) - .map_err(|_| format!("{name}: unreadable rte on {line:?}"))?; - // Figure 5-3: format bit 48, index 63:49, index[15] at bit 11. - sources.push(Source { - who: format!("the pin on GSI {gsi}"), - requester: Requester::Controller(id.clone()), - remappable: rte & (1 << 48) != 0, - handle: ((rte >> 49) & 0x7FFF) as u16 | (((rte >> 11) & 1) as u16) << 15, - }); - } else if line.contains(": msix address=") || line.contains(": msi address=") { - let (Some(address), Some(data)) = (fields.get("address"), fields.get("data")) else { - return Err(format!("{name}: unreadable message line: {line:?}")); - }; - let Some(who) = line - .split("PCI ") - .nth(1) - .and_then(|r| r.split_whitespace().next()) - .map(|bdf| bdf.trim_end_matches(':')) - else { - return Err(format!("{name}: a message line naming no function: {line:?}")); - }; - let address = u32::from_str_radix(address.trim_start_matches("0x"), 16) - .map_err(|_| format!("{name}: unreadable message address on {line:?}"))?; - let data = u32::from_str_radix(data.trim_start_matches("0x"), 16) - .map_err(|_| format!("{name}: unreadable message data on {line:?}"))?; - // Figure 5-4: format bit 4, SHV bit 3, handle 19:5, handle[15] at bit 2. - let remappable = address & (1 << 4) != 0; - let handle = ((address >> 5) & 0x7FFF) as u16 | (((address >> 2) & 1) as u16) << 15; - if remappable && (address & (1 << 3) == 0 || data != 0) { - return Err(format!( - "{name}: {who} writes a remappable message with SHV={} and data={data:#x}; \ - Figure 5-4 sets SHV and programs the data register to 0h, and the index the \ - unit computes is handle plus subhandle", - (address >> 3) & 1 - )); - } - sources.push(Source { - who: format!("the message-signalled interrupt of {who}"), - requester: Requester::Function(who.to_string()), - remappable, - handle, - }); - } - } - Ok(sources) -} +/// fsd's word for DATA's directories served from memory. +const IN_MEMORY: &str = "are in memory and will not survive a reboot"; -/// The requester id the unit's scopes give each interrupt controller, by MADT id. -fn scope_sources(log: &Serial) -> BTreeMap { - let mut named = BTreeMap::new(); - for line in log.text().lines() { - let Some(rest) = line.split("scope ioapic ").nth(1) else { continue }; - let Some(sid) = rest.split(' ').next() else { continue }; - if let Some(id) = unit_fields(line).get("id") { - named.insert(id.clone(), sid.to_string()); - } - } - named +/// The boot config that runs `netd` with a virtio NIC in front of it. +fn netcase() -> std::path::PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/netcase") } /// Whether this machine's virtio functions are behind the unit at all. @@ -649,11 +30,7 @@ fn scope_sources(log: &Serial) -> BTreeMap { /// the guest reports `n` because QEMU never *offers* the bit /// (`hw/virtio/virtio-bus.c:87-94`), not because the driver declined — it offers /// blindly; the independence comes from [`declining_is_not_free`]. -pub fn iommu_virtio_platform( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { +pub fn iommu_virtio_platform(test_config: &Path) -> Result<(), String> { for profile in [Profile::Headless, Profile::HeadlessNoIommu] { let name = if profile.iommu().is_some() { "headless" } else { "headless-no-iommu" }; let behind_unit = profile.iommu().is_some(); @@ -722,8 +99,8 @@ pub fn iommu_virtio_platform( // same machine the arm below requires to be unspent. Without this // pair those `must_not_say`s would pass against a kernel that had // stopped writing either line. - log.must_say(&super::faults::bar_moved())?; - log.must_say(&super::faults::msix_armed())?; + log.must_say(&bar_moved())?; + log.must_say(&msix_armed())?; created.len() } else { no_unit_is_no_claim(&log)?; @@ -779,7 +156,7 @@ pub fn iommu_virtio_platform( if behind_unit { "" } else { "; the NIC's claim refused for want of a domain" } ); } - declining_is_not_free(test_config, c_bins, rust_bins) + declining_is_not_free(test_config) } /// netd's, once its claim answers nothing outside its own function. @@ -811,15 +188,13 @@ const NVME_AT: &str = "00:02.0"; /// there and cannot be used is never answered with memory. fn no_unit_is_no_claim(log: &Serial) -> Result<(), String> { const NO_DOMAIN: &str = "it would have no address space of its own"; - // The same judge the two arms in `faults` read, so a refusal that spent - // something is red wherever it is reached. netd's own exit is the third - // saying, and is not read here. - super::faults::refused_claim(log, super::https::VIRTIO.claims, NO_DOMAIN, &[NVME_AT])?; + // netd's own exit is the third saying, and is not read here. + refused_claim(log, NETD_CLAIMS, NO_DOMAIN, &[NVME_AT])?; log.must_say(&format!("pcidev: PCI {NVME_AT} NOT HANDED OVER — {NO_DOMAIN}"))?; log.must_say("init: blockd: pci:1b36:0010 is on this machine and could not be handed over")?; log.must_say(BLOCKD_REFUSED)?; log.must_say(FSD_WITHOUT_DATA)?; - log.must_not_say(super::storage::IN_MEMORY)?; + log.must_not_say(IN_MEMORY)?; // And this machine handed *nothing* over, which is more than the claim's // own refusal says: with no unit there is no function any process could be // given an address space for. @@ -827,46 +202,17 @@ fn no_unit_is_no_claim(log: &Serial) -> Result<(), String> { Ok(()) } -/// The claimed function was armed on MSI-X, and never on MSI. -/// -/// MSI-X first wherever a function has a table, so an `msi address=` line for -/// the function a claim holds is the older mechanism taken where the newer one -/// was published. `claimed` is the `vendor:device` the boot config declares, and -/// the slot is [`faults::CLAIMED_AT`] — **the address is the harness's own and -/// never the guest's**, so what the hand-over line printed is asserted equal to -/// it rather than used, and the two arming lines have the spelling -/// [`faults::msix_armed`] and [`faults::msi_armed`] give them. -pub fn armed_on_msix(log: &Serial, claimed: &str) -> Result<(), String> { - use super::faults::{self, CLAIMED_AT}; - - let handed = faults::functions_named(log, &format!("[{claimed}] handed over on slot"))?; - if handed != [CLAIMED_AT] { - return Err(format!( - "this is an assertion about the claim on {CLAIMED_AT}; {claimed} was handed over \ - on {handed:?}:\n{}", - log.text() - )); - } - log.must_say(&faults::msix_armed())?; - log.must_not_say(&faults::msi_armed())?; - Ok(()) -} - /// The control that makes the two arms above mean something: a guest that /// declines the feature its host offered gets no device, not a bypassing one. /// `virtio_validate_features` returns `-EFAULT` and `virtio_set_status` returns /// before it stores the status (`hw/virtio/virtio.c:2270-2276` and `:2292-2299` /// at v11.1.1), so `FEATURES_OK` never sticks. The actuator withholds the bit /// from every virtio device but the console, and each of them is refused for it. -fn declining_is_not_free( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { +fn declining_is_not_free(test_config: &Path) -> Result<(), String> { let qemu = QemuInstance::boot_with_options( test_config, - c_bins, - rust_bins, + &[], + &[], BootOptions { profile: Profile::Headless, kernel_params: &["virtio-no-access-platform"], @@ -897,996 +243,128 @@ fn declining_is_not_free( Ok(()) } -/// The needle that says the unit blocked something, and the marker both gates -/// below boot to. A boot that never produces it times out, which is what a -/// unit that is not translating looks like from here. -const FAULT: &str = "iommu: DMA FAULT"; - -/// The fault reasons a *second-level page table walk* decides, as against the -/// ones the root/context walk above it decides. -/// -/// The set rather than one member, because which of them a unit gives for an -/// all-zero entry is an implementation's choice: QEMU 11.0.2 answers -/// `read-permission` — its own line reads `detected sspte permission error -/// (iova=0x1000000, level=0x4, sspte=0x0, write=0)`, so it reached the entry -/// and judged it on its permission bits rather than on a separate present bit. -/// A unit that answered `paging-entry-invalid` instead would be saying the -/// same thing. What the set excludes is the whole of the root and context -/// walk, which is the discrimination the gate needs: those are what a -/// stranded *context* entry produces, and passthrough produces no fault at all. -const SECOND_LEVEL: &[&str] = &["read-permission", "write-permission", "paging-entry-invalid"]; - -/// A function whose context entry the kernel deliberately never wrote must -/// fault on its first transaction, and the fault must name it. +/// The slot QEMU's `-device` order puts the function netd claims on, and the +/// address every judge below is an assertion about. /// -/// This is the exit criterion for I2 and the isolation negative control at the -/// same time, because at this stage they are -/// the same question. Identity mapping means a translated machine and an untranslated -/// one produce the same result for every device that is *in* the tables, so -/// the only way to tell the two apart is a device that is not: with the unit -/// bypassing, or never enabled, or pointed at a context entry naming -/// passthrough, the controller below would go on working and this test would -/// wait for a fault that never comes. -/// -/// [`Profile::Metal`] because it has an xHCI controller the kernel drives -/// from boot, and no virtio device. -/// The distinction matters: QEMU gives a virtio device the bypassing address -/// space unless it is created with `iommu_platform=on`, so a virtio-only -/// machine could not tell a translating unit from an absent one however the -/// tables were written. -pub fn iommu_context_absent( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (log, blocked, ()) = - fault_boot(test_config, c_bins, rust_bins, &["iommu-context-absent", "panic-reboot-fast"], |_, _| { - Ok(()) - })?; - - // Which function the actuator left out is decided in the guest by class - // code; which function that *is* on this machine is read here from the PCI - // walk's own lines. Neither half is told the other's answer, so a fault - // naming some other device — or the actuator skipping a device the walk - // never saw — is a failure rather than a tautology. - let xhci = class_function(&log, "0c03").ok_or_else(|| { - format!("this machine enumerated no xHCI controller to leave out\n{}", log.text()) - })?; - if blocked.stream != xhci { - return Err(format!( - "the unit blocked {} but the controller left out of the root table is {xhci}", - blocked.stream - )); - } - if blocked.reason != "context-entry-not-present" { - return Err(format!( - "the unit blocked {xhci} for {:?}, and a function with no context entry should be \ - blocked for having none", - blocked.reason - )); - } - eprintln!( - " [iommu] {xhci} left out of the root table: blocked at {} on a {} for {}", - blocked.address, blocked.access, blocked.reason - ); - Ok(()) -} +/// **The address is the harness's own and never the guest's.** A judge that +/// reads the function out of the console and then asserts about *that* asserts +/// about whichever function the kernel happened to name; what the guest printed +/// is asserted equal to this instead, so a constant that names the wrong slot +/// reds and never passes. +const CLAIMED_AT: &str = "00:03.0"; -/// A function whose context entry names a domain with nothing in it must fault -/// on its first transaction, and the fault must name the *page table* rather -/// than the entry above it. -/// -/// The half [`iommu_context_absent`] cannot give. A context entry naming -/// **passthrough** would fault identically for a function that has no entry at -/// all — and would then ignore every second-level table this kernel writes, -/// which is the whole of what I4 will build on. Here the entry is present and -/// the domain behind it is empty, so a fault can only come from the unit -/// having walked a table this kernel wrote and found nothing. -/// -/// It fails on a *read* deliberately. QEMU caches a translation with the -/// permissions of whichever access populated it and then lets its memory core -/// drop a later access the cached entry does not allow — silently, with no -/// fault record — so a control built on narrowing a permission hangs the boot -/// instead of faulting. That is measured, not assumed; the first thing a -/// device does here is fetch a descriptor, which -/// misses the cache and is answered by the tables. -pub fn iommu_empty_domain( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // The pool the driver's own `DCBAAP` begins, read out of the controller's - // registers, because that is where its descriptors are: a fault somewhere - // else would be a different machine's bug wearing this one's clothes. - let (_, blocked, (xhci, pool)) = fault_boot( - test_config, - c_bins, - rust_bins, - &["iommu-empty-domain", "panic-reboot-fast"], - |socket, log| { - let xhci = class_function(log, "0c03").ok_or_else(|| { - format!("this machine enumerated no xHCI controller to strand\n{}", log.text()) - })?; - let pool = dcbaa(socket, log, &xhci)?; - Ok((xhci, pool)) - }, - )?; - if blocked.stream != xhci { - return Err(format!( - "the unit blocked {} but the controller given an empty domain is {xhci}", - blocked.stream - )); - } - if !SECOND_LEVEL.contains(&blocked.reason.as_str()) { - return Err(format!( - "the unit blocked {xhci} for {:?}, which is not something a second-level page table \ - walk decides. A present context entry over an empty domain has to be refused by the \ - walk itself — any other reason means the unit stopped before it, and a context entry \ - naming passthrough would not have walked at all", - blocked.reason - )); - } - let at = u64::from_str_radix(blocked.address.trim_start_matches("0x"), 16) - .map_err(|_| format!("unreadable faulting address {:?}", blocked.address))?; - if pool == 0 || !(pool..pool + XHCI_POOL).contains(&at) { - return Err(format!( - "the unit blocked an access to {}, and the driver's descriptors are in the \ - {XHCI_POOL:#x} bytes from its DCBAAP, {pool:#x}", - blocked.address - )); - } - eprintln!( - " [iommu] {xhci} given an empty domain: blocked at {} on a {} for {}", - blocked.address, blocked.access, blocked.reason - ); - Ok(()) +/// The two lines a hand-over of that function spends. One arm requires them and +/// [`refused_claim`] requires their absence, and both read them here: a kernel +/// that stopped writing either line would otherwise satisfy both. +fn bar_moved() -> String { + format!("pcidev: PCI {CLAIMED_AT} BAR") } -/// One device aimed at the physical bytes the xHCI's device context base -/// address array page ends with — a page in another driver's pool, which the -/// aimed device's own domain does not map. Three things then hold at once and -/// no two come from the same place: the unit blocks it and names the device -/// and that address; the address is the one the xHCI's own `DCBAAP` holds, -/// resolved through the tables the unit walks; and the function's bus -/// mastering is gone. A write also leaves bytes to check; a read leaves none. -struct ForeignArm { - profile: Profile, - params: &'static [&'static str], - /// The class `pci::enumerate` printed for the aimed device. - class: &'static str, - access: &'static str, - name: &'static str, - /// How far past the page it was aimed at the block may be: one page where - /// the arm aims a single buffer, a whole 2 MiB block where it aims a grant - /// whose first touched byte is wherever the driver's own layout put it. - blocked_within: u64, - /// Where the aimed device's driver lives; an arm boots a config that runs - /// it, or it aims a device nobody drives. - driver: Driver, +fn msix_armed() -> String { + format!("PCI {CLAIMED_AT}: msix address=") } -#[derive(Clone, Copy, PartialEq, Eq)] -enum Driver { - Kernel, - Netd, +/// The older mechanism taken where the newer one was published — required +/// absent by [`refused_claim`]. +fn msi_armed() -> String { + format!("PCI {CLAIMED_AT}: msi address=") } -impl Driver { - /// The config a boot for this arm uses. - fn config(self, kernel: &Path) -> std::path::PathBuf { - match self { - Self::Kernel => kernel.to_path_buf(), - Self::Netd => netcase(), - } +/// Every function named by a line carrying `marker`, in the kernel's own +/// spelling. +/// +/// **A line that carries the marker and no `pcidev: PCI ` prefix is an error, +/// never a dropped line.** A scan closes only the spellings it matches, so a +/// caller asking what a console named on *every* such line would otherwise be +/// answered about the subset this walk could parse — one refusal read and a +/// second one dropped is the case "and no other function" exists for. +fn functions_named<'a>(log: &'a Serial, marker: &str) -> Result, String> { + const PREFIX: &str = "pcidev: PCI "; + let mut named = Vec::new(); + for line in log.text().lines().filter(|line| line.contains(marker)) { + named.push( + line.split(PREFIX) + .nth(1) + .and_then(|rest| rest.split_whitespace().next()) + .ok_or_else(|| { + format!("{line:?} says {marker:?} and names no function after {PREFIX:?}") + })?, + ); } + Ok(named) } -/// The one shipped boot config that runs `netd`, and so the only one where the -/// NIC's PCI function is claimed and driven at all. -fn netcase() -> std::path::PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/netcase") -} - -/// The claimed NIC's first DMA grant, answered with that page's address. +/// **The claim on [`CLAIMED_AT`] was refused for `why`, and the refusal spent +/// nothing**: no BAR of that function moved, neither of its two message +/// mechanisms is armed, `claims` reached no holder, and init said so in the +/// boot config's own spelling. `beside` is every other function this machine +/// refuses, each judged by its own caller. /// -/// Staged at the grant and not in the driver: what a driver does with an -/// address it was handed is what it does with a correct one, so the descriptor -/// is wrong while netd is unmodified. The access is a **read**, because the -/// grant holds the virtqueues and the device's first touch of it is the -/// descriptor fetch a doorbell alone provokes. -const USERDEV_FOREIGN: ForeignArm = ForeignArm { - profile: Profile::Headless, - params: &["iommu-userdev-foreign-dma"], - class: "0200", - access: "read", - name: "isolation", - blocked_within: PAGE_2M, - driver: Driver::Netd, -}; - -/// Oracle: VT-d Rev. 4.0 Section 9.8, which [`translate`] implements -/// independently, and QEMU's `vtd_iova_to_sspte` -/// (`hw/i386/intel_iommu.c:1146-1210` at v11.1.1). Every moved function's -/// domain is then walked on the three machines that between them carry all -/// seven, and the page sets are required to be pairwise disjoint. -pub fn iommu_domain_isolation( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // No guest binary: every assertion below is read off the boot log and out - // of the unit's own tables over QMP, so the image needs nothing but the - // config's own programs. - let _ = (c_bins, rust_bins); - let qemu = foreign_fault(test_config, &[], &[], &USERDEV_FOREIGN)?; - // Each guest ends before the next boots: QEMU holds an exclusive lock on the NVMe image. - let _ = qemu.shutdown(); - let mut classes: BTreeSet = BTreeSet::new(); - // `netcase`: the seventh domain is the NIC's, made when its claim is - // minted, and only a config that declares the function mints one. - for (profile, name) in - [(Profile::Headless, "headless"), (Profile::Hda, "hda"), (Profile::VirtioGpu, "virtio-gpu")] - { - let clean = QemuInstance::boot_with_options( - &netcase(), - &[], - &[], - BootOptions { profile, qmp: true, ..Default::default() }, - ); - classes.extend(clean_walk(&clean, name)?); - let _ = clean.shutdown(); - } - let want: BTreeSet<&str> = ["0108", "0200", "0380", "0401", "0403", "0780", "0c03"].into(); - let moved: BTreeSet<&str> = classes.iter().map(String::as_str).collect(); - if moved != want { - return Err(format!( - "the functions moved to a domain of their own are of classes {moved:?}, and every \ - driver in this kernel that masters the bus is one of {want:?}" +/// `slot_space` put back below `place_bars` reds on the two unspent lines. +fn refused_claim(log: &Serial, claims: &str, why: &str, beside: &[&str]) -> Result<(), String> { + let refused = functions_named(log, "NOT HANDED OVER")?; + let others: BTreeSet<&str> = refused.iter().copied().filter(|at| *at != CLAIMED_AT).collect(); + if !refused.contains(&CLAIMED_AT) || others != beside.iter().copied().collect() { + return Err(format!( + "the claim this judges is the one on {CLAIMED_AT}, beside {beside:?}; this console \ + refused {refused:?}:\n{}", + log.text() )); } + // By the reason true of the path that raised it, on the line that names the + // function: a refusal whose reason belongs to another path is worse than no + // line at all. + log.must_say(&format!("pcidev: PCI {CLAIMED_AT} NOT HANDED OVER — {why}"))?; + log.must_not_say(&format!("[{claims}] handed over"))?; + log.must_not_say(&msix_armed())?; + log.must_not_say(&msi_armed())?; + log.must_not_say(&bar_moved())?; + // All the way out to userland, rather than a kernel that logged a refusal + // and handed netd a NIC anyway. init names what it could not mint in the + // config's own spelling, and **with this refusal's own word**: the machine + // has the function, so "no such device on this machine" would be false. + log.must_say(&format!( + "init: netd: pci:{claims} is on this machine and could not be handed over" + ))?; Ok(()) } -/// A scanout backing in the xHCI's pool, by its physical address. The device maps a -/// backing when it is attached (`hw/display/virtio-gpu.c:918-931` at v11.1.1: -/// `dma_memory_map` answers NULL for a translation the unit refused, and the -/// command is answered `VIRTIO_GPU_RESP_ERR_UNSPEC` at `:1010-1014`), so the -/// blocked access is the mapping's read. -pub fn iommu_gpu_foreign_backing( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let arm = ForeignArm { - profile: Profile::VirtioGpu, - params: &["iommu-gpu-foreign-backing"], - class: "0380", - access: "read", - name: "gpu", - blocked_within: 0x1000, - driver: Driver::Kernel, - }; - foreign_fault(test_config, c_bins, rust_bins, &arm).map(drop) -} - -/// The HDA stream's buffer descriptor list at that page, and the stream -/// started: the controller fetches the list the moment `RUN` is set -/// (`hw/audio/intel-hda.c:480` at v11.1.1, `pci_dma_rw` per entry; the stream -/// data would follow through `:433`). -pub fn iommu_hda_foreign_bdl( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let arm = ForeignArm { - profile: Profile::Hda, - params: &["iommu-hda-foreign-bdl"], - class: "0403", - access: "read", - name: "hda", - blocked_within: 0x1000, - driver: Driver::Kernel, - }; - foreign_fault(test_config, c_bins, rust_bins, &arm).map(drop) -} - -/// virtio-sound's control-queue answer aimed at that page: the device maps -/// every buffer of a chain when it pops it (`hw/virtio/virtio.c:1641-1648` at -/// v11.1.1), and the answer it would have written there is -/// `hw/audio/virtio-snd.c:723-727`'s. -pub fn iommu_sound_foreign_dma( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let arm = ForeignArm { - profile: Profile::Headless, - params: &["iommu-sound-foreign-dma"], - class: "0401", - access: "write", - name: "sound", - blocked_within: 0x1000, - driver: Driver::Kernel, - }; - foreign_fault(test_config, c_bins, rust_bins, &arm).map(drop) -} -fn foreign_fault( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - arm: &ForeignArm, -) -> Result { - let options = BootOptions { - profile: arm.profile, - qmp: true, - kernel_params: arm.params, - ready_marker: FAULT, - ..Default::default() +fn unit_is_first(argv: &[String], name: &str) -> Result<(), String> { + let devices: Vec<&str> = + argv.windows(2).filter(|w| w[0] == "-device").map(|w| w[1].as_str()).collect(); + let Some(unit) = devices.iter().find(|d| d.starts_with("intel-iommu")) else { + return Ok(()); }; - unit_is_first(&qemu::profile_argv(&options), arm.name)?; - // An arm whose device is driven by a process boots that process's config. - let config = arm.driver.config(test_config); - let qemu = QemuInstance::boot_with_options(&config, c_bins, rust_bins, options); - let log = Serial::boot(&qemu); - let socket = qemu.qmp_socket(); - - let blocked = blocked_on(log.must_say(FAULT)?)?; - let aimed = class_function(&log, arm.class).ok_or_else(|| { - format!("this machine enumerated no class {} function to aim\n{}", arm.class, log.text()) - })?; - let xhci = class_function(&log, "0c03") - .ok_or_else(|| format!("this machine enumerated no xHCI controller\n{}", log.text()))?; - if blocked.stream != aimed { - return Err(format!( - "the unit blocked {} and the device aimed at another driver's pool is {aimed}", - blocked.stream - )); - } - if !SECOND_LEVEL.contains(&blocked.reason.as_str()) { - return Err(format!( - "the unit blocked {aimed} for {:?}, which is not something a second-level page table \ - walk decides — a domain that does not map an address has to refuse it in the walk", - blocked.reason - )); - } - if blocked.access != arm.access { - return Err(format!( - "the unit blocked {aimed} on a {}, and what the actuator staged is a {}", - blocked.access, arm.access - )); - } - - let window = register_window(socket, &log, arm.name)?; - let victim = translate(socket, window, &xhci, dcbaa(socket, &log, &xhci)?)?; - let at = u64::from_str_radix(blocked.address.trim_start_matches("0x"), 16) - .map_err(|_| format!("unreadable faulting address {:?}", blocked.address))?; - // The window and not the page, for the arm that aims a whole grant: the - // first access the device makes into it is at whatever offset the driver's - // own layout put first, and pinning that offset would assert netd's layout - // rather than the unit's refusal. - let aimed_at = victim & !0xFFF; - if !(aimed_at..aimed_at + arm.blocked_within).contains(&at) { - return Err(format!( - "the unit blocked an access to {}, and what the actuator aimed {aimed} at is \ - {:#x}..{:#x} — the page the xHCI's DCBAAP names, through the tables the unit walks. The \ - kernel is not reporting the address the device was aimed at", - blocked.address, - aimed_at, - aimed_at + arm.blocked_within, - )); - } - - let mut bytes = String::new(); - if arm.access == "write" { - let probe = victim + 0x800; - let words = over_qmp(socket, probe, PROBE_WORDS, 'g')?; - if let Some((i, word)) = words.iter().enumerate().find(|(_, w)| **w != 0) { - return Err(format!( - "the unit reported blocking {aimed} at {}, and the {} bytes at {probe:#x} inside \ - the xHCI's pool hold {word:#018x} at word {i} rather than the zero the xHCI driver \ - left. The write landed anyway", - blocked.address, - PROBE_WORDS * 8 - )); - } - bytes = format!(", all {} bytes there are still zero", PROBE_WORDS * 8); - } - // Out of the function's own `COMMAND` rather than off the line the handler printed. - let command = over_qmp(socket, config_space(&log, &aimed)? + PCI_COMMAND, 1, 'w')?[0] as u16; - if command & PCI_BUS_MASTER != 0 { + if devices[0] != *unit { return Err(format!( - "the unit blocked {aimed} and its COMMAND is {command:#06x}, so it still masters the \ - bus and can fault again" + "{name}: the unit is not the first -device ({} is), so every function ahead of it \ + gets QEMU's bypassing address space", + devices[0] )); } - let handled = log.must_say(FAULT)?; - let domain = context_of(socket, window, &aimed)?.0; - for field in [ - "bme=cleared".to_string(), - "first=y".to_string(), - format!("domain={domain}"), - "unitfaults=1".to_string(), - "streamfaults=1".to_string(), - ] { - if !handled.contains(&field) { - return Err(format!("the fault line does not say {field}: {handled:?}")); - } - } - eprintln!( - " [iommu] {aimed} aimed at {}, inside {xhci}'s pool: blocked on a {} for {}{bytes}, and \ - its COMMAND reads {command:#06x} — bus mastering gone", - blocked.address, blocked.access, blocked.reason, - ); - Ok(qemu) + Ok(()) } -/// One clean boot's moved functions, walked and compared; returns their classes. -fn clean_walk(clean: &QemuInstance, name: &str) -> Result, String> { - let log = Serial::boot(clean); - log.must_be_clean()?; - log.must_say("Boot: complete")?; - // The NIC's domain is made when its claim is minted, which is after - // `Boot: complete`: the walk has to be after the line that says so. - log.must_say("init: started netd")?; - let socket = clean.qmp_socket(); - let window = register_window(socket, &log, name)?; - let xhci = class_function(&log, "0c03") - .ok_or_else(|| format!("{name}: this machine enumerated no xHCI controller\n{}", log.text()))?; - let owned = translate(socket, window, &xhci, dcbaa(socket, &log, &xhci)?)?; - domains_are_disjoint(socket, &log, window, owned, &xhci)? - .keys() - .map(|bdf| class_of(&log, bdf)) +/// The `key=value` pairs on a unit line. `@0xfed90000` carries no `=` and is +/// skipped, which is what makes the split total rather than a parse. +fn unit_fields(line: &str) -> BTreeMap { + line.split_whitespace() + .filter_map(|word| word.split_once('=')) + .map(|(k, v)| (k.to_string(), v.to_string())) .collect() } -/// Mirrored in `tests/toyos-rust-tests/src/bin/gpu_scanout_swap.rs`. -const GPU_MODES: [(usize, usize); 3] = [(800, 600), (1024, 768), (640, 480)]; -const SWAPPED: &str = "===GPU_SCANOUT_SWAP_OK==="; - -/// Three mode changes while the guest keeps every retired scanout mapped, then -/// the display's domain walked out of the tables the unit reads: exactly the -/// command pool, the cursor and the live scanout are mapped, the live scanout's -/// device address translates to its pages, and no retired one translates at -/// all. The device address is the attachment's and ends with it; the pages are -/// the holder's and do not. -pub fn iommu_gpu_scanout_swap( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { profile: Profile::VirtioGpu, qmp: true, ..Default::default() }; - unit_is_first(&qemu::profile_argv(&options), "gpu")?; - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let mut log = Serial::boot(&qemu); - let result = qemu.run_test("test_rs_gpu_scanout_swap", Duration::from_secs(30)); - if let Some(err) = &result.error { - return Err(format!("the guest stopped answering: {err}\n{}", result.stdout)); - } - if result.exit_code != Some(0) || !result.stdout.contains(SWAPPED) { - return Err(format!( - "the mode changes did not all go through: exit {:?}\n{}", - result.exit_code, result.stdout - )); - } - log.push(&result.serial); - log.must_not_say(FAULT)?; - log.must_be_clean()?; - let shown = qemu.screendump(); - let last = GPU_MODES[GPU_MODES.len() - 1]; - if (shown.width, shown.height) != last { - return Err(format!( - "QEMU scans out {}x{} after the guest set {}x{}", - shown.width, shown.height, last.0, last.1 - )); - } - - let gpu = class_function(&log, "0380") - .ok_or_else(|| format!("this machine enumerated no display controller\n{}", log.text()))?; - let socket = qemu.qmp_socket(); - let window = register_window(socket, &log, "gpu")?; - let (did, root, levels) = context_of(socket, window, &gpu)?; - let moved = moved_functions(&log)?; - if moved.get(&gpu) != Some(&did) { - return Err(format!( - "the kernel says {gpu} moved to {:?} and its context entry names domain {did}", - moved.get(&gpu) - )); - } - - let scanouts = gpu_buffers(&log, "scanout buffer")?; - let cursors = gpu_buffers(&log, "cursor resource")?; - if scanouts.len() != GPU_MODES.len() + 1 { - return Err(format!( - "{} scanout buffers were allocated for a boot and {} mode changes:\n{}", - scanouts.len(), - GPU_MODES.len(), - log.text() - )); - } - let [cursor] = cursors[..] else { - return Err(format!("{} cursor buffers were allocated", cursors.len())); - }; - let (live, retired) = scanouts.split_last().expect("four scanouts"); - let mut want = BTreeSet::new(); - let mut pools = 0usize; - for (phys, end, _) in mappings_of(&log, did)? { - if retired.iter().any(|(p, _)| *p == phys) { - continue; - } - if phys != live.0 && phys != cursor.0 { - pools += 1; - } - want.extend((phys..end).step_by(PAGE_2M as usize)); - } - if pools != 1 { - return Err(format!( - "{pools} mappings in {gpu}'s domain are neither a scanout nor the cursor, and the \ - command pool is one" - )); - } - let leaves = leaves(socket, root, levels, &gpu)?; - if leaves != want { - return Err(format!( - "{gpu}'s domain {did} maps {leaves:#x?} where its live backings are {want:#x?}" - )); - } - let seen = translate(socket, window, &gpu, live.1)?; - if seen != live.0 { - return Err(format!( - "the live scanout's device address {:#x} translates to {seen:#x} and its pages are \ - at {:#x}", - live.1, live.0 - )); - } - for (phys, device) in retired { - if let Ok(to) = translate(socket, window, &gpu, *device) { - return Err(format!( - "the retired scanout at {phys:#x} was given device address {device:#x}, which \ - still translates to {to:#x} while a holder maps the pages" - )); - } - } - eprintln!( - " [iommu] {gpu}: {} mode changes, {} retired scanout(s) no longer translate, and domain \ - {did} maps exactly {} live 2 MiB page(s) — the command pool, the cursor and the \ - {}x{} scanout", - GPU_MODES.len(), - retired.len(), - leaves.len(), - last.0, - last.1 - ); - Ok(()) -} - -/// `iommu: moves to domain`, by function; a function moved twice is refused. -fn moved_functions(log: &Serial) -> Result, String> { - let mut seen: BTreeMap = BTreeMap::new(); - for line in log.text().lines() { - let Some(rest) = line.split("iommu: ").nth(1) else { continue }; - let Some((bdf, tail)) = rest.split_once(' ') else { continue }; - let Some(id) = tail.strip_prefix("moves to domain") else { continue }; - let id: u64 = id.trim().parse().map_err(|_| format!("unreadable domain on {line:?}"))?; - if seen.insert(bdf.to_string(), id).is_some() { - return Err(format!("{bdf} moved twice: {line:?}")); - } - } - Ok(seen) -} - -/// `iommu: domain maps .. at ` for one domain, in order. -fn mappings_of(log: &Serial, did: u64) -> Result, String> { - let needle = format!("iommu: domain{did} maps "); - let mut found = Vec::new(); - for line in log.text().lines() { - let Some(rest) = line.split(needle.as_str()).nth(1) else { continue }; - let mut words = rest.split_whitespace(); - let (Some(range), Some("at"), Some(at)) = (words.next(), words.next(), words.next()) else { - return Err(format!("unreadable mapping line: {line:?}")); - }; - let Some((phys, end)) = range.split_once("..") else { - return Err(format!("unreadable mapping line: {line:?}")); - }; - found.push((hex(phys, line)?, hex(end, line)?, hex(at, line)?)); - } - Ok(found) -} - -/// `VirtIO GPU: at phys=

device=` lines, as `(phys, device)`. -fn gpu_buffers(log: &Serial, what: &str) -> Result, String> { - let needle = format!("VirtIO GPU: {what} at "); - let mut found = Vec::new(); - for line in log.text().lines().filter(|l| l.contains(needle.as_str())) { - let fields = unit_fields(line); - let field = |k: &str| -> Result { - hex(fields.get(k).ok_or_else(|| format!("no {k}= on {line:?}"))?, line) - }; - found.push((field("phys")?, field("device")?)); - } - Ok(found) -} - -fn hex(word: &str, line: &str) -> Result { - u64::from_str_radix(word.trim_start_matches("0x"), 16) - .map_err(|_| format!("{word:?} is not a hex number on {line:?}")) -} - -/// The class `pci::enumerate` printed for one function. -fn class_of(log: &Serial, bdf: &str) -> Result { - let needle = format!("PCI {bdf} ["); +/// Every function `pci::enumerate` printed. Anchored on the class field that +/// follows the address, so `xHCI: found at PCI 00:02.0` is not one of them. +fn enumerated_functions(log: &Serial) -> BTreeSet { log.text() .lines() - .find_map(|line| line.split(needle.as_str()).nth(1)) - .and_then(|rest| rest.split(']').next()) - .map(str::to_string) - .ok_or_else(|| format!("the PCI walk printed no class for {bdf}")) -} - -/// Every moved function is in a domain of its own, and no two of those domains -/// reach the same physical page. A set comparison and not a spot check: a -/// domain's addresses start at `1 << (width - 2)`, so asking whether one -/// translates an address inside RAM misses every populated top-level index by -/// construction and cannot fail. -fn domains_are_disjoint( - socket: &Path, - log: &Serial, - window: u64, - owned: u64, - owner: &str, -) -> Result, String> { - let seen = moved_functions(log)?; - if seen.len() < 2 { - return Err(format!( - "{} function(s) moved to a domain of their own, so there is no pair here to be \ - disjoint\n{}", - seen.len(), - log.text() - )); - } - - let mut roots: BTreeMap = BTreeMap::new(); - let mut pages: BTreeMap> = BTreeMap::new(); - for (bdf, want) in &seen { - let (did, root, levels) = context_of(socket, window, bdf)?; - if did != *want { - return Err(format!( - "the kernel says {bdf} is in domain {want} and its context entry names domain \ - {did}" - )); - } - if let Some(other) = roots.insert(root, bdf.clone()) { - return Err(format!( - "{bdf} and {other} name the same second-level table at {root:#x}, so they are \ - one address space wearing two domain ids" - )); - } - let mine = leaves(socket, root, levels, bdf)?; - if mine.is_empty() { - return Err(format!("{bdf}'s domain {did} maps nothing at all")); - } - if mine.contains(&(owned & !(PAGE_2M - 1))) != (bdf == owner) { - return Err(format!( - "{bdf}'s domain {did} maps the page at {owned:#x} = {}, and that page is \ - {owner}'s admin completion queue", - mine.contains(&(owned & !(PAGE_2M - 1))) - )); - } - for (other, theirs) in &pages { - if let Some(shared) = mine.intersection(theirs).next() { - return Err(format!( - "{bdf}'s domain and {other}'s both map the physical page at {shared:#x}, so \ - either can reach what the other was given" - )); - } - } - pages.insert(bdf.clone(), mine); - } - eprintln!( - " [iommu] {} function(s) in {} domains over {} distinct second-level tables, mapping {} \ - pairwise-disjoint 2 MiB pages, and only {owner} maps {owned:#x}: {seen:?}", - seen.len(), - seen.values().collect::>().len(), - roots.len(), - pages.values().map(BTreeSet::len).sum::() - ); - Ok(seen) -} - -/// Every physical page one domain's second-level tables reach, Section 9.8's -/// walk, a whole 4 KiB of entries at a time. -fn leaves(socket: &Path, root: u64, levels: u64, bdf: &str) -> Result, String> { - let mut found = BTreeSet::new(); - let mut level = levels; - let mut tables = BTreeSet::from([root]); - while level > 2 { - let mut next = BTreeSet::new(); - for table in &tables { - for entry in over_qmp(socket, *table, ENTRIES, 'g')? { - if entry & 0x3 == 0 { - continue; - } - // A 1 GiB leaf followed as a pointer reads 512 words out of a data page. - if entry & LARGE_PAGE != 0 { - return Err(format!( - "{bdf}: a level-{level} entry {entry:#018x} carries the page-size bit, \ - and this kernel writes only 2 MiB leaves" - )); - } - next.insert(entry & ENTRY_ADDR); - } - } - tables = next; - level -= 1; - } - for table in &tables { - for entry in over_qmp(socket, *table, ENTRIES, 'g')? { - if entry & 0x3 == 0 { - continue; - } - if entry & LARGE_PAGE == 0 { - return Err(format!( - "{bdf}: a page-directory entry {entry:#018x} without the page-size bit, and \ - this kernel writes only 2 MiB leaves" - )); - } - found.insert(entry & ENTRY_ADDR & !(PAGE_2M - 1)); - } - } - Ok(found) -} - -/// Entries in one 4 KiB second-level table, read in a single monitor command. -const ENTRIES: usize = 512; -/// Section 9.8: bit 7 of a page-directory entry, a 2 MiB leaf rather than a pointer. -const LARGE_PAGE: u64 = 1 << 7; - -/// One function's context entry, as `(DID, second-level root, levels)`; Section -/// 9.3 puts `DID` at 87:72, `SLPTPTR` at 51:12 and `AW` at 66:64 as levels minus two. -fn context_of(socket: &Path, window: u64, bdf: &str) -> Result<(u64, u64, u64), String> { - let (bus, dev, func) = parse_bdf(bdf)?; - let root = over_qmp(socket, window + RTADDR_REG, 1, 'g')?[0] & ENTRY_ADDR; - let entry = over_qmp(socket, root + u64::from(bus) * 16, 1, 'g')?[0]; - if entry & 1 == 0 { - return Err(format!("{bdf}: the root entry for bus {bus:#04x} is not present")); - } - let devfn = u64::from(dev) * 8 + u64::from(func); - let context = over_qmp(socket, (entry & ENTRY_ADDR) + devfn * 16, 2, 'g')?; - if context[0] & 1 == 0 { - return Err(format!("{bdf}: its context entry is not present")); - } - Ok(((context[1] >> 8) & 0xFFFF, context[0] & ENTRY_ADDR, (context[1] & 0x7) + 2)) -} - -/// `COMMAND` and its Bus Master Enable bit, PCI 3.0 §6.2.2. -const PCI_COMMAND: u64 = 0x04; -const PCI_BUS_MASTER: u16 = 0x04; - -/// One function's config space in ECAM. -fn config_space(log: &Serial, bdf: &str) -> Result { - let line = log.must_say("ACPI: ECAM base address: ")?; - let ecam = line - .split("ACPI: ECAM base address: 0x") - .nth(1) - .and_then(|hex| u64::from_str_radix(hex.trim(), 16).ok()) - .ok_or_else(|| format!("unreadable ECAM base on {line:?}"))?; - let (bus, dev, func) = parse_bdf(bdf)?; - Ok(ecam + (u64::from(bus) << 20) + (u64::from(dev) << 15) + (u64::from(func) << 12)) -} - -/// The untouched half of the xHCI's DCBAA page: a frame is 1526 -/// bytes at most, so this covers the whole of one landing there. -const PROBE_WORDS: usize = 256; - -/// The xHCI driver's DMA pool, which its DCBAA begins: the `dma 2048 KiB` its -/// bring-up line states. -const XHCI_POOL: u64 = 2 << 20; - -/// `DCBAAP`'s offset in the operational registers, xHCI 1.2 Table 5-18; those -/// begin `CAPLENGTH` bytes into BAR 0, §5.3.1. -const XHCI_DCBAAP: u64 = 0x30; - -/// Where QEMU puts an `intel-iommu` on q35, which every profile here is: a -/// constant on the host side, not a number the guest supplies. -/// -/// `Q35_HOST_BRIDGE_IOMMU_ADDR`, `include/hw/i386/intel_iommu.h:35` at v11.1.1, -/// mapped at `intel_iommu.c:5635`. The DMAR the guest reads is built from that -/// same constant (`acpi-build.c:1687`), so this is one source agreeing with -/// itself and not two: what it catches is a guest reporting something else. -const UNIT_WINDOW: u64 = 0xfed9_0000; - -fn unit_is_first(argv: &[String], name: &str) -> Result<(), String> { - let devices: Vec<&str> = - argv.windows(2).filter(|w| w[0] == "-device").map(|w| w[1].as_str()).collect(); - let Some(unit) = devices.iter().find(|d| d.starts_with("intel-iommu")) else { - return Ok(()); - }; - if devices[0] != *unit { - return Err(format!( - "{name}: the unit is not the first -device ({} is), so every function ahead of it \ - gets QEMU's bypassing address space", - devices[0] - )); - } - Ok(()) -} - -/// The unit's register window, and the one thing on the guest's side of this -/// gate that is checked rather than believed: a kernel that wrote the real -/// `VER`, `GSTS`, `RTADDR` and `IRTA` into a page of RAM and printed *that* -/// address satisfied every readback here. One unit, stated rather than assumed — -/// `must_say` answers with the first match, so a second line is refused. -fn register_window(socket: &Path, log: &Serial, name: &str) -> Result { - let lines: Vec<&str> = - log.text().lines().filter(|l| l.contains("translating gsts=")).collect(); - let [line] = lines[..] else { - return Err(format!( - "{name}: {} unit(s) are translating and this gate reads one window at \ - {UNIT_WINDOW:#x}; a second needs the harness to model it\n{lines:?}", - lines.len() - )); - }; - let printed = line - .split(" @") - .nth(1) - .and_then(|rest| rest.split_whitespace().next()) - .and_then(|hex| u64::from_str_radix(hex.trim_start_matches("0x"), 16).ok()) - .ok_or_else(|| format!("{name}: no register window on {line:?}"))?; - if printed != UNIT_WINDOW { - return Err(format!( - "{name}: the kernel says its unit is at {printed:#x} and QEMU puts one at \ - {UNIT_WINDOW:#x}. Every table this gate walks starts there, so a page of RAM \ - printed here would be a set of forged registers\n{line}" - )); - } - // A unit, not a page somebody left all-ones: `VER` reads a real version, - // which is the same test the kernel makes before programming it. - let version = over_qmp(socket, UNIT_WINDOW, 1, 'w')?[0] as u32; - if version == u32::MAX || (version >> 4) & 0xF == 0 { - return Err(format!( - "{name}: {UNIT_WINDOW:#x} reads VER={version:#010x}, so no unit decodes there" - )); - } - Ok(UNIT_WINDOW) -} - -/// The address the xHCI at `bdf` holds in `DCBAAP`, out of its registers: the -/// first page of its driver's pool. -fn dcbaa(socket: &Path, log: &Serial, bdf: &str) -> Result { - let bar = bar0(socket, log, bdf)?; - let caplength = over_qmp(socket, bar, 1, 'w')?[0] & 0xFF; - Ok(over_qmp(socket, bar + caplength + XHCI_DCBAAP, 1, 'g')?[0] & !0x3F) -} - -/// A function's memory BAR 0, out of ECAM rather than off a console line. -fn bar0(socket: &Path, log: &Serial, bdf: &str) -> Result { - let config = config_space(log, bdf)?; - // A window that decodes at all: an ECAM base the kernel invented would read - // back all ones here, which is no vendor id. - if over_qmp(socket, config, 1, 'w')?[0] as u32 & 0xFFFF == 0xFFFF { - return Err(format!("no PCI function decodes at {config:#x}, so that is not ECAM")); - } - Ok(over_qmp(socket, config + 0x10, 1, 'g')?[0] & !0xF) -} - -fn parse_bdf(bdf: &str) -> Result<(u8, u8, u8), String> { - let (bus, rest) = bdf.split_once(':').ok_or_else(|| format!("not a bdf: {bdf:?}"))?; - let (dev, func) = rest.split_once('.').ok_or_else(|| format!("not a bdf: {bdf:?}"))?; - let refuse = |_| format!("not a bdf: {bdf:?}"); - Ok(( - u8::from_str_radix(bus, 16).map_err(refuse)?, - u8::from_str_radix(dev, 16).map_err(refuse)?, - func.parse().map_err(refuse)?, - )) -} - -/// What the unit itself would translate `at` to for `bdf`, decoded here from -/// Sections 9.1, 9.3 and 9.8 out of the tables it really walks: `RTADDR_REG`, -/// then the root entry for the bus, then the context entry for the function, -/// then the second-level tables the context entry names, at the depth its `AW` -/// field declares. -fn translate(socket: &Path, window: u64, bdf: &str, at: u64) -> Result { - let (bus, dev, func) = parse_bdf(bdf)?; - let root = over_qmp(socket, window + RTADDR_REG, 1, 'g')?[0] & ENTRY_ADDR; - let entry = over_qmp(socket, root + u64::from(bus) * 16, 1, 'g')?[0]; - if entry & 1 == 0 { - return Err(format!("{bdf}: the root entry for bus {bus:#04x} is not present")); - } - let devfn = u64::from(dev) * 8 + u64::from(func); - let context = over_qmp(socket, (entry & ENTRY_ADDR) + devfn * 16, 2, 'g')?; - if context[0] & 1 == 0 { - return Err(format!("{bdf}: its context entry is not present")); - } - // `AW` is levels minus two, Section 9.3. - let mut level = (context[1] & 0x7) + 2; - let mut table = context[0] & ENTRY_ADDR; - while level > 2 { - let index = (at >> (12 + 9 * (level - 1))) & 0x1FF; - let next = over_qmp(socket, table + index * 8, 1, 'g')?[0]; - if next & 0x3 == 0 { - return Err(format!("{bdf}: {at:#x} has no level-{level} entry")); - } - table = next & ENTRY_ADDR; - level -= 1; - } - let leaf = over_qmp(socket, table + ((at >> 21) & 0x1FF) * 8, 1, 'g')?[0]; - if leaf & 0x3 == 0 { - return Err(format!("{bdf}: {at:#x} has no leaf")); - } - Ok((leaf & ENTRY_ADDR & !(PAGE_2M - 1)) | (at & (PAGE_2M - 1))) -} - -/// What the unit reported when it blocked a transaction. -struct Blocked { - stream: String, - address: String, - access: String, - reason: String, -} - -/// Boot a deliberately mis-programmed machine and read the first fault off it. -/// -/// The fault line is the ready marker, so a boot that never produces one fails -/// as a boot timeout — which is exactly what a unit that is not translating -/// would do, and is why neither gate can pass vacuously. `holding` reads the -/// machine over QMP while the fatal path holds its panel, before the -/// `panic-reboot-fast` reset ends QEMU. -fn fault_boot( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - params: &'static [&'static str], - holding: impl FnOnce(&Path, &Serial) -> Result, -) -> Result<(Serial, Blocked, T), String> { - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::Metal, - kernel_params: params, - ready_marker: FAULT, - qmp: true, - ..Default::default() - }, - ); - let mut log = Serial::boot(&qemu); - let held = holding(qemu.qmp_socket(), &log)?; - // Past the fault, because the claim is that the machine stopped there: the - // handler takes the fatal path, and the capture is judged once its reset - // has ended QEMU. - let mut after = String::new(); - qemu::await_reset( - &mut qemu, - &mut after, - "the fault's fatal path to reset the machine", - &["Boot: complete", qemu::DEFAULT_READY], - )?; - log.push(&after); - log.must_not_say("Boot: complete")?; - log.must_not_say(qemu::DEFAULT_READY)?; - - let blocked = blocked_on(log.must_say(FAULT)?)?; - Ok((log, blocked, held)) -} - -/// The fault line's fields; a reason the kernel has no name for is refused. -fn blocked_on(line: &str) -> Result { - let fields = unit_fields(line); - let field = |k: &str| -> Result { - fields.get(k).cloned().ok_or_else(|| format!("the fault line has no {k}=: {line:?}")) - }; - let reason = line - .split_whitespace() - .last() - .ok_or_else(|| format!("the fault line names no reason: {line:?}"))? - .to_string(); - if reason == "unnamed" { - return Err(format!( - "the unit reported a fault reason this kernel has no name for: {line:?}" - )); - } - Ok(Blocked { stream: field("stream")?, address: field("addr")?, access: field("access")?, reason }) + .filter_map(|line| { + let (bdf, tail) = line.split("PCI ").nth(1)?.split_once(' ')?; + tail.starts_with('[').then(|| bdf.to_string()) + }) + .collect() } /// The one function `pci::enumerate` printed with this class, or none. @@ -1909,333 +387,3 @@ fn class_function(log: &Serial, class: &str) -> Option { } found } - -/// The `key=value` pairs on a unit line. `@0xfed90000` carries no `=` and is -/// skipped, which is what makes the split total rather than a parse. -fn unit_fields(line: &str) -> BTreeMap { - line.split_whitespace() - .filter_map(|word| word.split_once('=')) - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect() -} - -fn expect(got: &str, want: &str, key: &str, name: &str, line: &str) -> Result<(), String> { - if got == want { - return Ok(()); - } - Err(format!("{name}: {key}={got}, want {key}={want}\n{line}")) -} - -/// The requester ids the unit's scopes name are exactly the functions this -/// machine enumerated. Returns how many. -/// -/// Set equality rather than "each one exists", and the difference is the whole -/// value of this check. Measured against the raw table on QEMU 11.0.2: the -/// DRHD carries no `INCLUDE_PCI_ALL` flag and instead lists every PCI function -/// as its own scope, so the two sets are the same set. A path read one byte -/// off produces ids that are still *plausible* — `00:1f.3` becomes `00:03.0`, -/// which on this machine is the NVMe controller — and an each-one-exists check -/// stays green on all seven of them. The set catches it, because five of the -/// seven collapse onto `00:00.0` and four real functions go missing. -/// -/// A failure here on a future QEMU that switches to `INCLUDE_PCI_ALL` is a -/// real report and not a false one: which functions a unit's scope names is -/// what stage I2 hands context entries to. -fn scope_check(log: &Serial, name: &str) -> Result { - let mut scoped: Vec = Vec::new(); - for line in log.text().lines() { - let Some(rest) = line.split("iommu: unit0 scope ").nth(1) else { continue }; - let mut words = rest.split_whitespace(); - let (Some(kind), Some(who)) = (words.next(), words.next()) else { - return Err(format!("{name}: unreadable scope line: {line:?}")); - }; - // An I/O APIC sits on a pseudo-bus no PCI walk sees, and a scope whose - // path runs through a bridge reports no requester id at all — neither - // is a name this cross-check can look up. - if kind == "pci-endpoint" || kind == "pci-bridge" { - scoped.push(who.to_string()); - } - } - - let unique: BTreeSet<&String> = scoped.iter().collect(); - if unique.len() != scoped.len() { - return Err(format!( - "{name}: the unit names {} scopes but only {} distinct requester ids. A unit cannot \ - name the same requester twice, so the path bytes are being read at the wrong \ - offset: {scoped:?}", - scoped.len(), - unique.len() - )); - } - - let enumerated = enumerated_functions(log); - let scoped: BTreeSet = scoped.into_iter().collect(); - if scoped != enumerated { - return Err(format!( - "{name}: the unit's scope names {scoped:?} and this machine enumerated \ - {enumerated:?}. On QEMU these are the same set — the DRHD lists every function \ - rather than setting INCLUDE_PCI_ALL." - )); - } - if scoped.is_empty() { - return Err(format!( - "{name}: neither the unit nor the PCI walk named a single function, so this \ - comparison is between two empty sets" - )); - } - Ok(scoped.len()) -} - -/// Every function `pci::enumerate` printed. Anchored on the class field that -/// follows the address, so `xHCI: found at PCI 00:02.0` is not one of them. -fn enumerated_functions(log: &Serial) -> BTreeSet { - log.text() - .lines() - .filter_map(|line| { - let (bdf, tail) = line.split("PCI ").nth(1)?.split_once(' ')?; - tail.starts_with('[').then(|| bdf.to_string()) - }) - .collect() -} - -fn profile_name(profile: Profile) -> &'static str { - match profile { - Profile::Metal => "metal", - Profile::NoIommu => "no-iommu", - Profile::IommuNarrow => "narrow", - Profile::IommuNoIntremap => "no-intremap", - Profile::IommuEim => "eim", - _ => "unexpected", - } -} - -/// Presence, configuration and *position* of the unit in the argv. -/// -/// The last one is the vacuity trap in its harness-side form: QEMU hands a PCI -/// function the bypassing -/// address space when the function is created before the unit exists, so a -/// `-device intel-iommu` emitted after the devices it is meant to decode is a -/// unit that decodes nothing — and every assertion above it would still pass. -fn argv_check(profile: Profile, argv: &[String]) -> Result<(), String> { - let name = profile_name(profile); - let devices: Vec<&str> = argv - .windows(2) - .filter(|w| w[0] == "-device") - .map(|w| w[1].as_str()) - .collect(); - let unit = devices.iter().find(|d| d.starts_with("intel-iommu")); - let machine = argv - .windows(2) - .find(|w| w[0] == "-machine") - .map(|w| w[1].as_str()) - .ok_or_else(|| format!("{name}: no -machine in the argv"))?; - - match profile.iommu() { - None => { - if let Some(d) = unit { - return Err(format!("{name} declares no unit but QEMU is given {d}")); - } - if machine.contains("kernel-irqchip") { - return Err(format!( - "{name} declares no unit but the machine is still split-irqchip: {machine}" - )); - } - } - Some(want) => { - let d = *unit.ok_or_else(|| { - format!("{name} declares a unit and QEMU is given none: {devices:?}") - })?; - for field in [ - format!("aw-bits={}", want.aw_bits), - format!("intremap={}", if want.intremap { "on" } else { "off" }), - format!("eim={}", if want.eim { "on" } else { "off" }), - String::from("caching-mode=on"), - ] { - if !d.contains(&field) { - return Err(format!("{name}: {field} is not in {d}")); - } - } - if !machine.contains("kernel-irqchip=split") { - return Err(format!( - "{name}: interrupt remapping needs the userspace half of the irqchip, and \ - the machine is {machine}" - )); - } - if devices[0] != d { - return Err(format!( - "{name}: the unit is not the first -device ({} is), so every function ahead \ - of it gets QEMU's bypassing address space", - devices[0] - )); - } - } - } - Ok(()) -} - -/// **The machine survives a driver that aimed its device at memory it was not -/// given**, which is the thing moving a driver into userland is for. -/// -/// Every arm above this one is about a stream the *kernel* drives, and for -/// those the response is a halt: nothing can know what a device that reached an -/// address the kernel never gave it has already done, and there is nobody to -/// hand the fault to. A function a process drives has an owner. So the same -/// stimulus has to produce the same record and a machine that is still running, -/// and both halves are asserted here — a kernel that halted would fail the -/// second, and one that ignored the fault would fail the first. -/// -/// The stimulus is `iommu-userdev-foreign-dma`: the kernel answers netd's first -/// DMA grant with an address inside the xHCI's pool, which the NIC's own domain -/// does not map. netd is unmodified and does with that address exactly what it -/// does with a correct one, so what the device is pointed at is a real -/// descriptor holding a wrong address rather than a driver written to misbehave. -pub fn userdev_dma_fault( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let _ = c_bins; - // One guest binary, for the half of this test the fault line cannot say: - // that the machine still schedules, spawns, and answers. `log_origin` says - // one line and exits, and asserts nothing else: a verdict that rides a - // deferred release would red here as a fault it is not. - let bins: Vec<(String, Vec)> = rust_bins - .iter() - .filter(|(name, _)| name == "log_origin") - .cloned() - .collect(); - if bins.is_empty() { - return Err("log_origin was not built".to_string()); - } - let mut qemu = foreign_fault(test_config, &[], &bins, &USERDEV_FOREIGN)?; - let log = Serial::named("boot console", qemu.boot_log().to_string()); - - // The fault was handed to the process that drives the stream, and the line - // says so: `owner=kernel` here would be a machine that halted, or was about - // to. - let handled = log.must_say(FAULT)?; - let slot = slot_of(log.text(), "[1af4:1041]")?; - if !handled.contains(&format!("owner=slot{slot} ")) { - return Err(format!( - "the unit's fault was recorded against {handled:?}, and the function that faulted \ - is one a process drives. A fault the kernel takes as its own is one it halts for" - )); - } - - // netd's answer to the refusal is its own end: `Card::begin_pass` panics - // on the claim's `Io`, and it exits 101. Awaited so that the capture below - // is the machine's after netd, and a claim that stops refusing reds here. - let mut end = String::new(); - qemu::await_guest(&mut qemu, &mut end, "netd's end on its refused claim", |end| { - end.contains("netd: this NIC's claim refused an interrupt read: Io") - && end.lines().any(|l| l.contains("exit: netd pid=") && l.contains(" code=101 ")) - }) - .map_err(|e| format!("{e}\n{end}\n{}", log.text()))?; - - // And the machine is running. This is the assertion the whole stage is - // for: a guest that answers here is one whose scheduler, spawn path and - // IPC all survived a device being refused mid-flight. - let result = qemu.run_test("test_rs_log_origin", Duration::from_secs(60)); - if let Some(err) = &result.error { - return Err(format!( - "the guest stopped answering after the fault: {err}\n{}\n{}", - result.stdout, - log.text() - )); - } - if result.exit_code != Some(0) { - return Err(format!( - "the guest ran after the fault and failed: exit {:?}\n{}", - result.exit_code, result.stdout - )); - } - // `end` is the window from the fault to netd's exit, and nothing else - // judges it — it goes into the check below rather than staying read only - // for the two needles `await_guest` waited on. netd's own panic is - // staged, so its location line, immediately above the message already - // matched above, is the one line this capture may hold; a second panic, - // netd's or anyone else's, has no line here to hide behind. - let message_at = end - .lines() - .position(|l| l.contains("netd: this NIC's claim refused an interrupt read: Io")) - .ok_or_else(|| format!("netd's panic message vanished between the wait and the check:\n{end}"))?; - let mut lines: Vec<&str> = end.lines().collect(); - if message_at == 0 || !lines[message_at - 1].contains("panicked at") { - return Err(format!("netd's panic message arrived without its location line:\n{end}")); - } - lines.remove(message_at - 1); - let end = lines.join("\n"); - - // The staged fault happened **once**: clearing the function's Bus Master - // Enable is what bounds a storm, and a second line would say it did not. - // Every other boot in the estate reds on this line through - // `must_be_clean`; this is the one that staged it. - let mut after = log; - after.push(&end); - after.push(&result.serial); - after.must_be_clean_apart_from("iommu: DMA FAULT owner=slot", 1)?; - eprintln!( - " [iommu] the NIC's driver was refused an address it was handed, and the machine ran on" - ); - Ok(()) -} - -/// **Two claims of a function nothing resets never share a page.** A claim is -/// an ordinary handle and a grant outlives it, so the first holder can close -/// its claim and keep its grant mapped while a second claim of the same -/// function is granted memory at the address the first grant was at. -/// -/// QEMU's 82574 under `pcidev-reset-nothing`, which declines every reset the -/// way the T14's I219 does, on the test estate's boot, where nobody else -/// claims it. `userdev_residue` is both holders and asserts in the guest: the -/// second holder's first grant reads zeros, and the first holder's grant still -/// holds its own word after the second has written its own. The premises are -/// asked of the console: the release reset nothing, and the second claim was -/// handed the range the function was left aimed at. -pub fn userdev_residue_is_its_own( - test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let bins: Vec<(String, Vec)> = - rust_bins.iter().filter(|(name, _)| name == "userdev_residue").cloned().collect(); - if bins.is_empty() { - return Err("userdev_residue was not built".to_string()); - } - let options = BootOptions { - profile: Profile::E1000e, - kernel_params: &["pcidev-reset-nothing"], - ..Default::default() - }; - let mut qemu = QemuInstance::boot_with_options(test_config, &[], &bins, options); - let result = qemu.run_test("test_rs_userdev_residue", Duration::from_secs(60)); - let mut log = Serial::boot(&qemu); - log.push(&result.serial); - if let Some(err) = &result.error { - return Err(format!("userdev_residue did not finish: {err}\n{}\n{}", result.stdout, log.text())); - } - if result.exit_code != Some(0) { - return Err(format!("userdev_residue: exit {:?}\n{}\n{}", result.exit_code, result.stdout, log.text())); - } - let slot = slot_of(log.text(), "[8086:10d3]")?; - let released = log.must_say(&format!("[8086:10d3] released from slot {slot}; reset by"))?; - if !released.contains("reset by nothing") { - return Err(format!("the premise: the 82574 was not released by nothing — {released}")); - } - let kept = log.must_say(&format!("pcidev: slot {slot} holds 1 range(s)"))?.to_string(); - log.must_be_clean()?; - eprintln!(" [iommu] {}; {}", kept.trim_end(), result.stdout.trim_end()); - Ok(()) -} - -/// The `pcidev` slot the function with PCI ids `ids` (`[vvvv:dddd]`) was handed -/// over on: a boot's claims are minted in init's order, and the block service's -/// comes first on a machine with an NVMe controller its row names. -pub(crate) fn slot_of(text: &str, ids: &str) -> Result { - text.lines() - .find_map(|l| { - let rest = l.split(&format!("{ids} handed over on slot ")).nth(1)?; - rest.split(|c: char| !c.is_ascii_digit()).next()?.parse().ok() - }) - .ok_or_else(|| format!("no function {ids} was handed over on any slot")) -} diff --git a/tests/common/irqcensus.rs b/tests/common/irqcensus.rs index ee27653e911..e7a65d4740e 100644 --- a/tests/common/irqcensus.rs +++ b/tests/common/irqcensus.rs @@ -11,8 +11,8 @@ //! landed across every guest a run booted. The second is the instrument the //! `every-interrupt-lands-on-the-boot-cpu` track's later change is measured //! against, so it has to be produced by an ordinary run rather than by -//! `--nocapture`: CI's `guest` shards do not pass that flag, and a number only a -//! developer's terminal can produce is not a baseline. +//! `--nocapture`: a number only a developer's terminal can produce is not a +//! baseline. use std::collections::BTreeMap; use std::sync::Mutex; @@ -125,9 +125,7 @@ struct Guest { /// Interrupts on cpu0 as a fraction of the machine's. boot_cpu_share: f64, /// Interrupts on cpu0, so the run's pooled share is an exact ratio of two - /// integers rather than a mean of per-guest fractions. A run is twelve - /// shards on CI and one process here, so the order statistics below are - /// per-shard and only this pair adds up across them. + /// integers rather than a mean of per-guest fractions. on_boot_cpu: u64, total: u64, /// Per source, summed over every CPU, and the cpu0 part of it. diff --git a/tests/common/lan.rs b/tests/common/lan.rs index 66036a8a136..936f2eab376 100644 --- a/tests/common/lan.rs +++ b/tests/common/lan.rs @@ -6,21 +6,14 @@ //! ring — or the one file netd leaves beside them, the lease probe's //! report. The judge reads netd's lines by that name and no other program's. -use std::net::Ipv4Addr; -use std::path::Path; - use toyos_build::bootlog; use toyos_build::lan::{ - asked_under_its_own_name, lease_in, link_up_ms, Lease, HOSTNAME, LEASE, LINK_UP, MAC, NO_LEASE, + lease_in, link_up_ms, LEASE, LINK_UP, MAC, READY, }; -use toyos_build::metaldevices; use toyos_i219::lease::{self, Event, Verdict}; -use toyos_i219::phy::PhyRefusal; use super::metal; -use super::qemu::{self, BootOptions, QemuInstance}; -use super::serial; pub const CONFIG: &str = "tests/lancase"; pub const BOOT: &str = "lancase"; @@ -59,14 +52,6 @@ pub const TALK_BOOT: &str = "lantalkcase"; const TALK_HOLD: &str = "test_rs_lan_talk_hold"; pub const TALK_JOBS: &[&str] = &[TALK_HOLD]; -/// The same boot in front of QEMU's 82574, and the key its image authorizes. -const TALK_QEMU_CONFIG: &str = "tests/e1000talkcase"; -const TALK_KEY: &str = "lantalk"; - -/// A liveness guard on a rehearsal guest that never opened its stream, never a -/// verdict. -const TALK_CEILING: std::time::Duration = std::time::Duration::from_secs(120); - /// The armed boot's judge: the kernel's own records, tied to the I219's /// hand-over, say whether a message it raised reached a CPU — whatever the PHY /// did about a link. @@ -77,21 +62,6 @@ pub fn provoked_on_metal(back: &metal::Readback) -> Result<(), String> { Ok(()) } -/// The config the QEMU arm boots — the Intel driver in front of the user-mode -/// backend, which is the same driver the T14 arm runs and the only DHCP server -/// this host can put in front of it. -const QEMU_CONFIG: &str = "tests/e1000case"; - -/// The same, with netd's `--exit-with-lease` armed. -const LEASE_QEMU_CONFIG: &str = "tests/e1000leasecase"; - -/// What QEMU's user-mode backend leases, and what it says about the network it -/// leases on. Its own defaults, not this repository's: they are the oracle. -const SLIRP_ADDRESS: Ipv4Addr = Ipv4Addr::new(10, 0, 2, 15); -const SLIRP_PREFIX: u8 = 24; -const SLIRP_ROUTER: Ipv4Addr = Ipv4Addr::new(10, 0, 2, 2); -const SLIRP_DNS: Ipv4Addr = Ipv4Addr::new(10, 0, 2, 3); - /// The card the T14 arm claims, as the kernel and the manifest spell it. const ID: &str = "8086:15fc"; @@ -267,259 +237,6 @@ pub fn leased_on_metal(back: &metal::Readback) -> Result<(), String> { Ok(()) } -/// The netdev QEMU's `e1000e` profile names its backend, which the monitor's -/// `set_link` is addressed to. -const FLAP_NETDEV: &str = "net0"; - -/// netd's own line for a pass that found the link gone, which the link is -/// kept away until: the pass that says it is the one that records the down. -const LINK_DOWN: &str = "netd: I219: link down"; - -/// The report of a boot whose link was taken away after its lease: the link -/// goes down and comes back up after the first lease, and neither a `lost` nor -/// a second `leased` line follows it — the client never started over, which -/// is what gives the address up. Against QEMU's server the second is the one -/// that shows: a restart is answered inside the pass that made it, so the loss -/// between the two never reaches the report. -fn flap_kept_the_lease(text: &str) -> Result<(), String> { - let events: Vec = - text.lines().filter_map(lease::Line::parse).map(|line| line.event).collect(); - let leased = events - .iter() - .position(|event| matches!(event, Event::Leased { .. })) - .ok_or_else(|| format!("the report records no lease:\n{text}"))?; - let after = &events[leased..]; - let down = after - .iter() - .position(|event| *event == Event::Link(toyos_i219::Link::Down)) - .ok_or_else(|| format!("the report never saw the link go down after its lease:\n{text}"))?; - if !after[down..].iter().any(|event| matches!(event, Event::Link(toyos_i219::Link::Up { .. }))) { - return Err(format!("the report never saw the link come back:\n{text}")); - } - if after.contains(&Event::Lost) { - return Err(format!("the lease was given up across the flap:\n{text}")); - } - if after[1..].iter().any(|event| matches!(event, Event::Leased { .. })) { - return Err(format!("the client started over across the flap:\n{text}")); - } - Ok(()) -} - -/// The lease probe, end to end, in front of QEMU's 82574 and its user-mode -/// DHCP server: netd serves its window, the kernel's `exit:` record carries the -/// verdict, and the report read back out of the image by the host's own FAT -/// implementation names the lease that server hands out, field by field, with -/// frames counted both ways by the driver and by the MAC's statistics — -/// which QEMU's model keeps, and not this repository. -/// -/// **The link is taken away and given back once the lease has landed**, from -/// QEMU's own monitor, and the lease has to outlive it: the report says the -/// link went down and came up after the lease, never that the lease was lost, -/// and the verdict is a lease held at the end of the window. -pub fn lan_lease_report( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let case = super::compile::repo_root().join(LEASE_QEMU_CONFIG); - let image_path = super::lane::dir().join("lan-lease-report.img"); - let image = qemu::build_boot_image(&case, &[], &[], &[]); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let (start, len) = super::volumes::log_extent(&image, &image_path)?; - - let options = BootOptions { - profile: qemu::Profile::E1000e, - boot_image: Some(qemu::Staged::Written(image_path.clone())), - qmp: true, - ..Default::default() - }; - if !qemu::profile_argv(&options).iter().any(|a| a.contains("e1000e")) { - return Err("this test needs an Intel NIC and the profile has none".to_string()); - } - let mut guest = QemuInstance::boot_with_options(&case, &[], &[], options); - let mut console = guest.boot_log().to_string(); - // netd says it is ready once the lease is applied, which is when a flap is - // one a bound lease has to survive. - qemu::await_marker(&mut guest, &mut console, READY, "netd to take its lease")?; - { - let mut monitor = qemu::QmpMonitor::open(guest.qmp_socket()); - let mut set_link = |state: &str| { - let said = monitor.human(&format!("set_link {FLAP_NETDEV} {state}")); - match said.trim().is_empty() { - true => Ok(()), - false => Err(format!("QEMU's monitor refused `set_link {state}`: {said}")), - } - }; - let from = console.len(); - set_link("off")?; - qemu::await_marker_new(&mut guest, &mut console, LINK_DOWN, from, "netd to see the link go down")?; - set_link("on")?; - } - // Drained rather than waited on: once the lease lands netd says nothing - // until its window ends, and every wait in this harness reads a quiet guest - // as one that stopped. The window ends inside this drain. - console.push_str( - &guest.drain_serial(std::time::Duration::from_millis(toyos_tco::LEASE_BOUND_MS)), - ); - let exited = format!("{}{NETD} pid=", bootlog::EXIT); - qemu::await_marker(&mut guest, &mut console, &exited, "netd to end its lease probe")?; - drop(guest); - let code = metaldevices::exit_of(&console, NETD) - .and_then(|exit| i32::try_from(exit.code).ok()) - .ok_or_else(|| format!("no readable `{exited}` record:\n{console}"))?; - let log = serial::Serial::named("the lan lease boot", console.as_str()); - log.must_be_clean()?; - if Verdict::from_exit_code(code) != Some(Verdict::Leased) { - return Err(format!( - "netd exited {code} on the 82574, which the table reads as {:?} and not a lease", - Verdict::from_exit_code(code) - )); - } - - let after = std::fs::read(&image_path).map_err(|e| format!("read the image back: {e}"))?; - let volume = after.get(start..start + len).ok_or("the image shrank under the log partition")?; - let text = super::volumes::read_files(volume, &[LEASE_FILE])? - .pop() - .flatten() - .ok_or_else(|| format!("the log volume carries no {LEASE_FILE}"))?; - let text = String::from_utf8(text).map_err(|e| format!("{LEASE_FILE}: {e}"))?; - let complaints = toyos_fat32_check::check(volume); - if !complaints.is_empty() { - return Err(format!( - "the report gave the checker something to say about the log volume:\n{}", - toyos_fat32_check::describe(&complaints) - )); - } - let _ = std::fs::remove_file(&image_path); - - let summary = lease::summary(&text).map_err(|why| format!("{LEASE_FILE}: {why}\n{text}"))?; - if summary.exit != Some(code) { - return Err(format!("netd exited {code} and its report ends {:?}:\n{text}", summary.exit)); - } - let want = Event::Leased { - address: SLIRP_ADDRESS, - prefix: SLIRP_PREFIX, - server: SLIRP_ROUTER, - router: Some(SLIRP_ROUTER), - }; - match summary.lease { - Some((_, got)) if got == want => {} - other => return Err(format!("the report's lease is {other:?} and the backend serves {want:?}:\n{text}")), - } - let counts = summary.counts.ok_or_else(|| format!("the report carries no counts:\n{text}"))?; - if counts.sent == 0 || counts.received == 0 || counts.wire.sent == 0 || counts.wire.received == 0 { - return Err(format!("a lease with {counts:?} is no exchange both counts saw:\n{text}")); - } - if !text.lines().any(|line| line.ends_with(" link up 1000 full")) { - return Err(format!("the report never says the emulated link came up:\n{text}")); - } - flap_kept_the_lease(&text)?; - if !summary.held { - return Err(format!("netd exited leased and its report ends without a lease:\n{text}")); - } - eprintln!(" [lan] netd exited {code}, a lease; its report:"); - for line in text.lines() { - eprintln!(" [lan] {line}"); - } - Ok(()) -} - -/// The QEMU arm: the client, against a DHCP server this repository did not -/// write. -/// -/// Every field of the lease is checked, because a client that dropped the router -/// option or read the mask off the wrong one would otherwise pass where the -/// answers happen to agree; and the readiness line is checked to come *after* -/// the lease, because every other arm waits for it and then connects. -pub fn lan_dhcp_lease( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let case = super::compile::repo_root().join(QEMU_CONFIG); - let dump = wire_dump(); - let options = BootOptions { - profile: qemu::Profile::E1000e, - wire_dump: Some(dump.clone()), - ..Default::default() - }; - if !qemu::profile_argv(&options).iter().any(|a| a.contains("e1000e")) { - return Err("this test needs an Intel NIC and the profile has none".to_string()); - } - let mut guest = QemuInstance::boot_with_options(&case, &[], &[], options); - let mut console = guest.boot_log().to_string(); - qemu::await_marker(&mut guest, &mut console, LEASE, "netd to take an address")?; - // QEMU owns the pcap while it runs, and every refusal below is a return: - // the frames are taken once the machine is gone and the file removed here. - drop(guest); - let frames = std::fs::read(&dump).map_err(|e| format!("{}: {e}", dump.display()))?; - let _ = std::fs::remove_file(&dump); - let log = serial::Serial::named("the lan boot", console.as_str()); - - let lease = lease_in(log.text())?; - let want = Lease { - address: SLIRP_ADDRESS, - prefix: SLIRP_PREFIX, - server: SLIRP_ROUTER, - gateway: SLIRP_ROUTER, - dns: vec![SLIRP_DNS], - ms: lease.ms, - }; - if lease != want { - return Err(format!( - "the client read this lease as {lease:?} and the backend serves {want:?}" - )); - } - // The only thing the I219's §9 bring-up may say on the 82574 this host - // emulates, in the driver crate's own words. - log.must_say(&PhyRefusal::NotThisRegisterMap.to_string())?; - // The order, and not merely the presence of both. - log.must_say_after(LEASE, READY)?; - log.must_say(LINK_UP)?; - log.must_be_clean()?; - asked_under_its_own_name(&frames)?; - eprintln!(" [lan] the client asked under its own name on the wire"); - Ok(()) -} - -/// The client on a wire with nothing at the other end. -/// -/// **The refusal the lease boot cannot reach.** A machine whose network never -/// answers still has to announce itself, or every arm that waits for that line -/// hangs instead of having its connects refused one at a time. -pub fn lan_no_lease( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let case = super::compile::repo_root().join(QEMU_CONFIG); - let options = BootOptions { profile: qemu::Profile::E1000eNoServer, ..Default::default() }; - let mut guest = QemuInstance::boot_with_options(&case, &[], &[], options); - let mut console = guest.boot_log().to_string(); - // Drained until netd's `ready` after its give-up, and not awaited: the - // guest says nothing at all until netd gives up on its own clock, which - // every wait in this harness reads as a machine that stopped. The ceiling - // is the harness's. - let gave_up = format!("{NO_LEASE}{HOSTNAME} in "); - let given_up = std::cell::Cell::new(false); - let served = std::cell::Cell::new(false); - console.push_str(&guest.drain_until(qemu::GUEST_WEDGED, |line| { - given_up.set(given_up.get() || line.contains(&gave_up)); - served.set(given_up.get() && line.contains(READY)); - served.get() - })); - if !served.get() { - return Err(format!("{} waiting for {gave_up:?} and then {READY:?}\n{console}", qemu::STALLED)); - } - let log = serial::Serial::named("the lan boot with no server", console.as_str()); - if let Ok(lease) = lease_in(log.text()) { - return Err(format!("a wire with no server leased {lease:?}")); - } - log.must_say_after(&gave_up, READY)?; - eprintln!(" [lan] no server answered and netd said so, then served anyway"); - Ok(()) -} - /// The T14 talked to over its own cable, judged from the Mac's side: the log /// the machine served, asked for by its name while it booted, is the stick's /// own log from its first line in its own order, the address the name answered @@ -561,165 +278,3 @@ pub fn talked_on_metal(back: &metal::Readback) -> Result<(), String> { } Err(format!("{} finding(s):\n {}", bad.len(), bad.join("\n "))) } - -/// The talking boot rehearsed in front of QEMU's 82574: once the guest serves -/// its log and sshd listens, the host reads the log through a forward onto -/// `logd`'s port from the boot's first line, has the same conversation the -/// metal loop has through slirp's forward to sshd, and `reboot` over it ends -/// the guest. The stream is then compared with the guest's own `/log`, read off -/// the volume behind its back. -/// -/// **What this cannot rehearse is the network**: slirp answers no ICMP from the -/// host and carries no multicast, so the ping and finding the machine by its -/// name are the T14's to judge. -pub fn lan_talk( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - use toyos_build::metaltalk::{self, Ssh}; - - let root = super::compile::repo_root(); - let case = root.join(TALK_QEMU_CONFIG); - let scratch = super::lane::dir().join("lan-talk"); - std::fs::create_dir_all(&scratch).map_err(|e| format!("{}: {e}", scratch.display()))?; - let identity = super::ssh::Identity::mint(TALK_KEY)?; - let bytes = qemu::build_boot_image_carrying( - &case, - &[], - &[], - &[(super::ssh::KEYS_ON_ROOT.to_string(), identity.authorized_line().into_bytes())], - &[], - ); - let image = super::lane::dir().join("lan-talk.img"); - std::fs::write(&image, &bytes).map_err(|e| format!("write {}: {e}", image.display()))?; - let (start, len) = super::volumes::log_extent(&bytes, &image)?; - - // **Its own disk.** sshd mints its identity under `/home`, and the lane's - // shared image would hand that identity to the next boot of the lane. - let data = super::lane::dir().join("lan-talk-data.img"); - toyos_build::build::create_sparse(&data, qemu::NVME_SMALL); - let (ssh_port, log_port) = (qemu::free_host_port(), qemu::free_host_port()); - let options = BootOptions { - profile: qemu::Profile::E1000e, - boot_image: Some(qemu::Staged::Written(image.clone())), - nvme_image: Some(data.clone()), - ssh_port: Some(ssh_port), - log_port: Some(log_port), - ..Default::default() - }; - if !qemu::profile_argv(&options).iter().any(|a| a.contains("e1000e")) { - return Err("[lan] this boot needs an Intel NIC and the profile has none".to_string()); - } - let mut guest = QemuInstance::boot_with_options(&case, &[], &[], options); - let mut console = guest.boot_log().to_string(); - for (marker, doing) in [ - (super::logstream::SERVING, "logd to open its port"), - ("sshd: listening on port 22", "sshd to listen"), - ] { - qemu::await_marker(&mut guest, &mut console, marker, doing)?; - } - let stream = super::logstream::reader(log_port, "lan-talk-stream.txt")?; - let ssh = Ssh::at(&root, identity.private().to_path_buf())?; - let forward = std::net::SocketAddr::from((Ipv4Addr::LOCALHOST, ssh_port)); - let conversation = metaltalk::converse(&stream, &ssh, Some(forward), false, &scratch)?; - // `-no-reboot`: the guest's own reset ends QEMU, and its last word is - // the kernel's. - qemu::await_marker(&mut guest, &mut console, bootlog::REBOOTING, "`reboot` over ssh")?; - drop(guest); - serial::Serial::named("the talking boot", console.as_str()).must_be_clean()?; - stream.wait_ended(TALK_CEILING); - - let heard = metaltalk::Conversation::parse(&conversation.render())? - .ok_or("a rendered conversation names its peer")?; - let said = metaltalk::judge(&heard, &stream.lines()) - .map_err(|bad| format!("{} finding(s):\n {}", bad.len(), bad.join("\n ")))?; - let file = super::volumes::whole_log(&image, start, len)?; - super::logstream::is_prefix_of(&stream.lines(), &file)?; - for line in said { - eprintln!(" [talk] {line}"); - } - eprintln!( - " [talk] the {} served line(s) are /log's own, from its first, in its order ({} in the \ - file)", - stream.lines().len(), - file.len() - ); - let _ = std::fs::remove_file(&image); - let _ = std::fs::remove_file(&data); - Ok(()) -} - -/// A talking boot staged in front of one of QEMU's NICs: its log port -/// forwarded, the key its image authorizes, and where its log partition sits -/// in the image. -pub(super) struct TalkBoot { - pub(super) case: std::path::PathBuf, - pub(super) identity: super::ssh::Identity, - pub(super) image: std::path::PathBuf, - pub(super) scratch: std::path::PathBuf, - pub(super) log_port: u16, - bench: super::logstream::Bench, - actuators: &'static [&'static str], - pub(super) start: usize, - pub(super) len: usize, -} - -/// The talking boot's NIC: QEMU's 82574, the part whose register file the -/// T14's I219 has. -pub(super) const TALK_BENCH: super::logstream::Bench = super::logstream::Bench { - profile: qemu::Profile::E1000e, - config: TALK_QEMU_CONFIG, - device: "e1000e", -}; - -impl TalkBoot { - /// `bench.config`'s boot staged to authorize the lane's talking key, on the - /// test kernel with `actuators` armed. - pub(super) fn stage_armed( - name: &str, - bench: super::logstream::Bench, - actuators: &'static [&'static str], - ) -> Result { - let case = super::compile::repo_root().join(bench.config); - let scratch = super::lane::dir().join(name); - std::fs::create_dir_all(&scratch).map_err(|e| format!("{}: {e}", scratch.display()))?; - let identity = super::ssh::Identity::mint(TALK_KEY)?; - let bytes = qemu::build_boot_image_carrying( - &case, - &[], - &[], - &[(super::ssh::KEYS_ON_ROOT.to_string(), identity.authorized_line().into_bytes())], - actuators, - ); - let image = super::lane::dir().join(format!("{name}.img")); - std::fs::write(&image, &bytes).map_err(|e| format!("write {}: {e}", image.display()))?; - let (start, len) = super::volumes::log_extent(&bytes, &image)?; - let log_port = qemu::free_host_port(); - Ok(Self { case, identity, image, scratch, log_port, bench, actuators, start, len }) - } - - /// The boot's options, the NIC asked of the argv rather than assumed. - pub(super) fn options(&self) -> BootOptions { - let options = BootOptions { - profile: self.bench.profile, - boot_image: Some(qemu::Staged::Written(self.image.clone())), - log_port: Some(self.log_port), - kernel_params: self.actuators, - ..Default::default() - }; - assert!( - qemu::profile_argv(&options).iter().any(|a| a.contains(self.bench.device)), - "[lan] this boot needs {} and the profile has none", - self.bench.device - ); - options - } -} - -/// Where this process writes the frames one boot put on its wire. -fn wire_dump() -> std::path::PathBuf { - let at = super::lane::dir().join("lan.pcap"); - let _ = std::fs::remove_file(&at); - at -} diff --git a/tests/common/logread.rs b/tests/common/logread.rs deleted file mode 100644 index b4a6412f9dc..00000000000 --- a/tests/common/logread.rs +++ /dev/null @@ -1,205 +0,0 @@ -//! `SYS_LOG_READ`, read from inside `test-runner` under a storm. -//! -//! **The verdict is computed in the guest and asserted here.** What the host -//! can see of a conservation law is a line saying it held; what it can check is -//! that the line is there, that the run was not vacuous, and that the numbers -//! the guest printed describe the machine the host booted. So the guest prints -//! its ledger and this file reads it — `log-gate: OK` is the verdict, and every -//! number beside it is evidence a reviewer can weigh. -//! -//! The gate runs *inside* `test-runner` rather than in a binary it spawns: -//! `logread` is a `SysCap` dup and not a namespace entry, so it is not part of -//! what the runner hands its children. - -use std::collections::BTreeMap; -use std::path::Path; -use std::time::Duration; - -use super::qemu::{BootOptions, QemuInstance}; - -/// The in-guest gate's name in the `run ` protocol. It is a `test-runner` -/// builtin rather than a `/system/bin` entry, and the marker protocol is the same -/// either way. -const STORM_GATE: &str = "log-storm"; - -/// The whole run's ceiling: a gate that never finishes is what it reds. -const CEILING: Duration = Duration::from_secs(60); - -/// One boot's storm, as the guest reported it. -struct Report { - stdout: String, - fields: BTreeMap, -} - -impl Report { - fn get(&self, key: &str) -> Result { - self.fields - .get(key) - .copied() - .ok_or_else(|| format!("the guest's report has no `{key}=`:\n{}", self.stdout)) - } -} - -/// A name two of the guest's lines both defined. -/// -/// **Not a merge, because the two lines are different subjects.** The guest -/// prints its ledger over several `log-gate:` lines and this file reads them -/// into one map, so a name appearing twice means the number a test asserts on -/// came from whichever line was printed last — silently, and with the other -/// line still on screen looking like the evidence. -struct Contaminated { - key: String, - first: u64, - second: u64, -} - -/// The conservation law, at one width. -fn conservation( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - smp: u32, -) -> Result<(), String> { - // The test kernel by build rather than by actuator: the producer's - // `SYS_DEBUG` is what needs it, and nothing is armed. - let options = - BootOptions { smp, kernel_features: toyos_build::build::TEST_KERNEL, ..Default::default() }; - let report = storm(test_config, c_bins, rust_bins, STORM_GATE, options)?; - let shards = report.get("shards")?; - if shards != smp as u64 { - return Err(format!( - "--smp {smp} answered {shards} shard(s); the cursor's shard count is the machine's \ - CPU count\n{}", - report.stdout - )); - } - // Non-vacuity, and it is the half a green law cannot supply: a reader that - // took every record after the storm had ended has proved nothing about - // concurrent producers, and one the ring never lapped has proved nothing - // about `lost`. - let concurrent = report.get("concurrent")?; - let dropped = report.get("dropped")?; - let read = report.get("read")?; - let lost = report.get("lost")?; - if concurrent == 0 || read == 0 || lost == 0 { - return Err(format!( - "--smp {smp} read {read} record(s), {concurrent} of them while the storm ran, and \ - lost {lost}\n{}", - report.stdout - )); - } - eprintln!( - " [log] smp={smp}: emitted={} read={read} dropped={dropped} concurrent={concurrent} \ - lost={lost} wakes={}", - report.get("emitted")?, - report.get("wakes")?, - ); - Ok(()) -} - -/// **`--smp 2`**, so the producer thread has a CPU the reader is not on. -pub fn log_conservation_smp2( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - conservation(test_config, c_bins, rust_bins, 2) -} - -/// Boot one machine as `options` says, run `gate` on it and read its verdict off it. -fn storm( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - gate: &str, - options: BootOptions, -) -> Result { - let (smp, params) = (options.smp, options.kernel_params); - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let result = qemu.run_test(gate, CEILING); - if let Some(err) = &result.error { - return Err(format!( - "--smp {smp} {params:?}: {err}\nstdout:\n{}\nserial tail:\n{}", - result.stdout, - tail(&result.serial) - )); - } - match result.exit_code { - Some(0) => {} - Some(code) => { - return Err(format!( - "--smp {smp} {params:?}: the log gate exited {code}\n{}", - result.stdout - )) - } - None => { - return Err(format!("--smp {smp} {params:?}: no exit code\n{}", result.stdout)) - } - } - if !result.stdout.contains("log-gate: OK") { - return Err(format!( - "--smp {smp} {params:?}: the gate exited 0 without saying so\n{}", - result.stdout - )); - } - let fields = fields(&result.stdout).map_err(|c| { - format!( - "--smp {smp} {params:?}: two of the guest's `log-gate:` lines define `{}` ({} and \ - {}), so every number read out of this report is whichever line came last\n{}", - c.key, c.first, c.second, result.stdout - ) - })?; - Ok(Report { fields, stdout: result.stdout }) -} - -/// Every `key=` the guest printed, and the two counts it prints as -/// prose. One parse, so a test asserts on a name rather than on a column. -/// -/// **A name defined twice is refused rather than merged.** The guest's report is -/// several lines about different subjects, and flattening them means a repeated -/// name silently resolves to the last line printed — with the other line still -/// in the failure message, looking like the evidence. Refusing is what makes the -/// flattening safe: it holds exactly while the names really are unique. -fn fields(stdout: &str) -> Result, Contaminated> { - fn put( - out: &mut BTreeMap, - key: &str, - value: u64, - ) -> Result<(), Contaminated> { - match out.insert(key.to_string(), value) { - None => Ok(()), - Some(first) => Err(Contaminated { key: key.to_string(), first, second: value }), - } - } - - let mut out: BTreeMap = BTreeMap::new(); - for line in stdout.lines() { - let Some(rest) = line.split_once("log-gate: ").map(|(_, r)| r) else { continue }; - for word in rest.split_whitespace() { - let Some((key, value)) = word.split_once('=') else { continue }; - if let Ok(n) = value.trim_end_matches(&[',', ';'][..]).parse::() { - put(&mut out, key, n)?; - } - } - // "N record(s) over M read(s) from S shard(s)" — the shape of the line - // rather than a key, because those three are what the sentence is. - let words: Vec<&str> = rest.split_whitespace().collect(); - for pair in words.windows(2) { - let Ok(n) = pair[0].parse::() else { continue }; - match pair[1] { - "record(s)" => put(&mut out, "records", n)?, - "read(s)" => put(&mut out, "reads", n)?, - "shard(s);" | "shard(s)" => put(&mut out, "shards", n)?, - _ => {} - } - } - } - Ok(out) -} - -/// The last of a capture, for a failure message. A storm puts thousands of -/// lines on the console and the interesting end is the recent one. -fn tail(serial: &str) -> String { - let lines: Vec<&str> = serial.lines().collect(); - lines[lines.len().saturating_sub(40)..].join("\n") -} diff --git a/tests/common/logstream.rs b/tests/common/logstream.rs index e55a669d86c..bb6976aee24 100644 --- a/tests/common/logstream.rs +++ b/tests/common/logstream.rs @@ -9,138 +9,6 @@ //! is read off the FAT volume behind the guest's back, so the two readings //! share nothing but the boot that produced them. -use std::io::Write; -use std::net::{Ipv4Addr, SocketAddr}; -use std::path::PathBuf; -use std::time::Duration; - -use toyos_build::metaltalk::{Peer, Stream}; - -use super::qemu::{self, BootOptions, QemuInstance}; -use super::{compile, serial, volumes}; - -/// A liveness guard on a guest that stopped talking, never a verdict. -const CEILING: Duration = Duration::from_secs(90); - -/// What `logd` says once its port is open: the moment a reader can connect. -pub const SERVING: &str = "logd: serving this boot's log on port"; - -/// Which machine the stream is judged on. **Two of them, and the driver is the -/// difference** — the bench's NIC is an Intel I219, and QEMU's `e1000e` is the -/// only machine in reach that runs netd's Intel driver. -#[derive(Clone, Copy)] -pub struct Bench { - pub profile: qemu::Profile, - /// The boot config whose netd claims this machine's card, and whose `logd` - /// row carries the `netd` connector serving needs. - pub config: &'static str, - /// The `-device` this profile must actually carry, asked of the argv rather - /// than assumed. - pub device: &'static str, -} - -pub const VIRTIO: Bench = - Bench { profile: qemu::Profile::Headless, config: "tests/logstreamcase", device: "virtio-net" }; - -pub const E1000E: Bench = - Bench { profile: qemu::Profile::E1000e, config: "tests/logstreame1000case", device: "e1000e" }; - -/// One boot's image and where its log partition sits inside it. -pub struct Staged { - pub image: PathBuf, - pub start: usize, - pub len: usize, -} - -pub fn stage( - config: &str, - name: &str, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result { - stage_armed(config, name, &[], c_bins, rust_bins) -} - -/// [`stage`], its kernel armed with `params`. -pub fn stage_armed( - config: &str, - name: &str, - params: &[&str], - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result { - let config = compile::repo_root().join(config); - let bytes = qemu::build_boot_image(&config, c_bins, rust_bins, params); - let image = super::lane::dir().join(format!("{name}.img")); - std::fs::write(&image, &bytes).map_err(|e| format!("write {}: {e}", image.display()))?; - let (start, len) = volumes::log_extent(&bytes, &image)?; - Ok(Staged { image, start, len }) -} - -/// A boot of `bench` with `logd`'s port forwarded to `port`, up and serving. -fn boot( - bench: Bench, - staged: &Staged, - port: u16, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(QemuInstance, String), String> { - let options = BootOptions { - profile: bench.profile, - boot_image: Some(qemu::Staged::Written(staged.image.clone())), - log_port: Some(port), - ..Default::default() - }; - if !qemu::profile_argv(&options).iter().any(|a| a.contains(bench.device)) { - return Err(format!("this test needs a {} and the profile carries none", bench.device)); - } - let config = compile::repo_root().join(bench.config); - let mut guest = QemuInstance::boot_with_options(&config, c_bins, rust_bins, options); - let mut console = guest.boot_log().to_string(); - qemu::await_marker(&mut guest, &mut console, SERVING, "logd to open its port")?; - serial::Serial::named("boot console", console.as_str()).must_be_clean()?; - Ok((guest, console)) -} - -/// A reader of the forwarded port, connected now. -pub fn reader(port: u16, file: &str) -> Result { - let at = SocketAddr::from((Ipv4Addr::LOCALHOST, port)); - let path = super::lane::dir().join(file); - let stream = Stream::connect(Peer::At(at), &path, false, CEILING)?; - stream - .wait_connected(CEILING) - .ok_or_else(|| stream.unopened().unwrap_or_else(|| "the stream never opened".to_string()))?; - Ok(stream) -} - -/// Shut the guest down, wait for QEMU to exit, and read what it left on its -/// volume. -pub fn shut_down(guest: QemuInstance, console: &mut String, staged: &Staged) -> Result, String> { - shut_down_keeping(guest, console, staged, |_| ()).map(|(file, ())| file) -} - -/// [`shut_down`], with `keep` handed the guest once QEMU has exited and before -/// it is dropped: what QEMU finishes only at its exit — the wav it captured — -/// is whole then, and gone once the guest is dropped. -pub fn shut_down_keeping( - mut guest: QemuInstance, - console: &mut String, - staged: &Staged, - keep: impl FnOnce(&QemuInstance) -> R, -) -> Result<(Vec, R), String> { - writeln!(guest.stdin_mut(), "run shutdown").map_err(|e| format!("write to QEMU stdin: {e}"))?; - guest.flush_stdin(); - console.push_str(&guest.await_exit(Duration::from_secs(20))?); - let kept = keep(&guest); - drop(guest); - for bad in ["PANIC:", "panicked at"] { - if console.contains(bad) { - return Err(format!("{bad:?} on the way down\n{console}")); - } - } - Ok((volumes::whole_log(&staged.image, staged.start, staged.len)?, kept)) -} - /// What a reader received is the file's own first lines, in the file's own /// order, and nothing else. /// @@ -172,67 +40,3 @@ pub fn is_prefix_of(received: &[String], file: &[String]) -> Result<(), String> } Ok(()) } - -/// **A reader that connects late gets the whole boot.** A job runs and ends -/// before anything connects; then a reader connects and must receive the boot -/// from its first line — the job's own line and the kernel's record of its exit -/// among it — and then what the machine writes after, all of it the same lines -/// `/log` holds, in its order. -pub fn stream( - bench: Bench, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let name = format!("logstream-{}", bench.device); - let staged = stage(bench.config, &name, c_bins, rust_bins)?; - let port = qemu::free_host_port(); - let (mut guest, mut console) = boot(bench, &staged, port, c_bins, rust_bins)?; - - // Before any reader exists. - let job = "test_rs_log_origin"; - let before = guest.run_test(job, Duration::from_secs(60)); - if before.exit_code != Some(0) { - return Err(format!("{job} exited {:?}:\n{}", before.exit_code, before.stdout)); - } - let stream = reader(port, &format!("{name}.txt"))?; - let exit = format!("exit: {job} "); - if !stream.wait_for(&exit, CEILING) { - return Err(format!( - "a reader that connected after {job} ended never received its exit record: {} \ - line(s)", - stream.lines().len() - )); - } - // And after: a record written once the reader was already reading. - let later = "test_rs_empty_dir_stat"; - let after = guest.run_test(later, Duration::from_secs(60)); - if after.exit_code != Some(0) { - return Err(format!("{later} exited {:?}:\n{}", after.exit_code, after.stdout)); - } - if !stream.wait_for(&format!("exit: {later} "), CEILING) { - return Err("a record written while the reader read never reached it".to_string()); - } - - let file = shut_down(guest, &mut console, &staged)?; - if !stream.wait_ended(CEILING) { - return Err("the reader's connection had not ended once the guest was down".to_string()); - } - let received = stream.lines(); - is_prefix_of(&received, &file)?; - let whole = received.concat(); - if toyos_build::bootlog::boot_millis(&whole).is_none() { - return Err("the reader was not handed this boot's `Boot: complete`".to_string()); - } - if !toyos_build::bootlog::lines_of(&whole, "test-runner").contains(super::origin::NONCE) { - return Err(format!("the reader was not handed {job}'s own line, said before it connected")); - } - eprintln!( - " [stream] a reader that connected after the first job ended got {} line(s) over {}, \ - from the boot's first, each the line /log holds ({} in the file)", - received.len(), - bench.device, - file.len() - ); - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} diff --git a/tests/common/metal.rs b/tests/common/metal.rs index a203cd9a8b9..5ca51ef4a4b 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -36,9 +36,8 @@ pub struct Arm { /// Named rather than derived from (config, parameters), because sharing is /// not always safe and only the author knows: `mkdir_cap` fills the /// machine-wide directory cap and leaves it there, so `readdir_bound`'s own - /// `create_dir` on that boot is refused with `OutOfMemory` and it panics — - /// which is why each has a boot of its own in QEMU too. A test that must - /// not share names its own; it costs a minute and it says so. + /// `create_dir` on that boot is refused with `OutOfMemory` and it panics. A + /// test that must not share names its own; it costs a minute and it says so. pub boot: &'static str, /// The boot config's directory, relative to the repository root. pub config: &'static str, @@ -158,17 +157,11 @@ fn sized(shared: &[SharedBoot]) -> Vec { out } -/// Whether a registration runs on the T14, and how. -pub enum Metal { - /// It does not, and why — a row rather than a silence, because "no metal - /// declaration" is the answer for the hundred tests nobody has looked at - /// and this is the answer for one somebody has. - QemuOnly(&'static str), - Runs { - arms: &'static [Arm], - /// The readbacks in `arms` order. - judge: fn(&[&Readback]) -> Result<(), String>, - }, +/// How a registration runs on the T14. +pub struct Metal { + pub arms: &'static [Arm], + /// The readbacks in `arms` order. + pub judge: fn(&[&Readback]) -> Result<(), String>, } /// What one boot left on the stick, and what the host clock saw of it. @@ -579,7 +572,7 @@ fn batches( } } for (name, decl) in tests { - let Metal::Runs { arms, .. } = decl else { continue }; + let Metal { arms, .. } = decl; for arm in *arms { let batch = out.entry(arm.boot.to_string()).or_insert_with(|| Batch { config: arm.config, @@ -869,26 +862,12 @@ pub fn run( return Verdict::Red; } }; - let declared: Vec<&str> = tests - .iter() - .filter_map(|(name, decl)| match decl { - Metal::QemuOnly(why) => Some((*name, *why)), - Metal::Runs { .. } => None, - }) - .map(|(name, why)| { - eprintln!("[metal] QEMU-only: {name} — {why}"); - name - }) - .collect(); - let runs: Vec<&(&str, &'static Metal)> = - tests.iter().filter(|(_, d)| matches!(d, Metal::Runs { .. })).collect(); + let runs: Vec<&(&str, &'static Metal)> = tests.iter().collect(); eprintln!( - "[metal] {} registration(s) and {} shared member(s) over {} boot(s); {} declared \ - QEMU-only", + "[metal] {} registration(s) and {} shared member(s) over {} boot(s)", runs.len(), shared.iter().map(|b| b.jobs.len()).sum::(), batches.len(), - declared.len(), ); if runs.is_empty() && shared.iter().all(|b| b.jobs.is_empty()) { eprintln!("[metal] nothing to run"); @@ -1075,7 +1054,7 @@ pub fn judge_readbacks( eprintln!("\n[metal] the tests"); let mut passed = 0usize; for (name, decl) in runs { - let Metal::Runs { arms, judge } = decl else { continue }; + let Metal { arms, judge } = decl; let mut owed: Vec<&Readback> = Vec::new(); let mut missing: Option = None; for arm in *arms { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 703524f867d..d6c4b15489d 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,42 +1,20 @@ pub mod audio; -/// blockd: the NVMe driver in userland, judged off its disk and the device's -/// own trace. -pub mod blockd; -/// The C and C++ toolchain, end to end: a program the toolchain's clang built, -/// judged as the loader reads it and then run. -pub mod clang; pub mod clock; pub mod lane; pub mod compile; -pub mod console; /// The device boot: what `tests/metaldevicecase` measures. pub mod devices; pub mod faults; -pub mod fwvars; -pub mod gpt; -pub mod https; pub mod iommu; -pub mod inspect; pub mod irqcensus; /// The cable: netd's address, and the T14 answering the host on it. pub mod lan; -pub mod logread; pub mod logstream; pub mod metal; -pub mod origin; -pub mod orphan; -pub mod partclaim; -pub mod pkg; pub mod power; pub mod qemu; pub mod screen; -/// The host as a neighbour on a guest's own Ethernet segment. -pub mod segment; pub mod serial; pub mod ssh; -pub mod storage; -pub mod swap; -pub mod update; pub mod usb; pub mod volumes; -pub mod wallclock; diff --git a/tests/common/origin.rs b/tests/common/origin.rs deleted file mode 100644 index 30f6f533baa..00000000000 --- a/tests/common/origin.rs +++ /dev/null @@ -1,653 +0,0 @@ -//! A program's output in the log, under the name of the ring it came out of: -//! in `/log`, on the log `logd` serves, and on the console — and no program's -//! bytes can make a line read as the kernel's or as another program's, and no -//! amount of them is dropped. -//! -//! Every verdict here reads `/log` off the volume behind the guest's back, with -//! the host's own parser of the form (`toyos_logstream::program_line`), the -//! kernel's exit judge (`metaldevices::exit_of`) and the kernel-records filter -//! every judge of the kernel's records reads through (`bootlog::kernel_records`). - -use std::net::{Ipv4Addr, UdpSocket}; -use std::time::{Duration, Instant}; - -use toyos_build::bootlog; -use toyos_build::metaldevices::exit_of; - -use super::logstream::{self, VIRTIO}; -use super::qemu::{self, BootOptions, QemuInstance}; -use super::{compile, segment, serial, volumes}; - -/// What `test_rs_log_origin` says, and the name its line goes in the log under: -/// it runs as `test-runner`'s child, on `test-runner`'s ring. -pub const NONCE: &str = "log origin nonce 7d1f3a"; -const ORIGIN_JOB: &str = "test_rs_log_origin"; -const RUNNER: &str = "test-runner"; - -/// The flooding program, its line count, and its last line's head. -pub const FLOODER: &str = "test_rs_log_flood"; -pub const FLOOD_LINES: usize = 16_384; -const FLOOD_DONE: &str = "flood done lines="; - -/// The forger, and the exit it really has. -const FORGER: &str = "test_rs_log_forger"; -const FORGER_CODE: i64 = 7; -/// The text of the record it stamps `u64::MAX`. -const FORGER_AHEAD: &str = "log forger: stamped at the end of time"; - -/// **A program's line reaches `/log`, the served log and the console, and each -/// says whose it is.** On all three it is the line the program wrote under -/// `test-runner`'s name, because that is the ring it came out of. init's and -/// `logd`'s own lines are in the file under theirs. -pub fn line(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let staged = logstream::stage(VIRTIO.config, "log-program-line", c_bins, rust_bins)?; - let port = qemu::free_host_port(); - let options = BootOptions { - profile: VIRTIO.profile, - boot_image: Some(qemu::Staged::Written(staged.image.clone())), - log_port: Some(port), - ..Default::default() - }; - let config = compile::repo_root().join(VIRTIO.config); - let mut guest = QemuInstance::boot_with_options(&config, c_bins, rust_bins, options); - let mut console = guest.boot_log().to_string(); - qemu::await_marker(&mut guest, &mut console, logstream::SERVING, "logd to open its port")?; - let reader = logstream::reader(port, "log-program-line.txt")?; - - let ran = guest.run_test(ORIGIN_JOB, Duration::from_secs(60)); - if ran.exit_code != Some(0) { - return Err(format!("{ORIGIN_JOB} exited {:?}\n{}", ran.exit_code, ran.stdout)); - } - // The console: the line as written, under the runner's head. - let headed = ran.serial.lines().any(|l| { - toyos_logstream::program_line(l).is_some_and(|said| said.tag == RUNNER && said.text == NONCE) - }); - if !headed { - return Err(format!("the console never carried {NONCE:?} under {RUNNER:?}\n{}", ran.serial)); - } - if !reader.wait_for(NONCE, Duration::from_secs(60)) { - return Err(format!("the served log never carried {NONCE:?}")); - } - let file = logstream::shut_down(guest, &mut console, &staged)?; - serial::Serial::named("the boot", console.as_str()).must_be_clean()?; - if !reader.wait_ended(Duration::from_secs(60)) { - return Err("the reader's connection had not ended once the guest was down".to_string()); - } - - let log = file.concat(); - let under = |name: &str, text: &str| -> Result<(), String> { - match bootlog::lines_of(&log, name).lines().any(|l| l == text) { - true => Ok(()), - false => Err(format!("/log carries no line {text:?} under {name:?}")), - } - }; - under(RUNNER, NONCE)?; - under("init", "init: started logd")?; - if !bootlog::lines_of(&log, "logd").contains(logstream::SERVING) { - return Err(format!("/log carries no {:?} under logd's name", logstream::SERVING)); - } - if bootlog::kernel_records(&log).contains(NONCE) { - return Err(format!("{NONCE:?} is among the kernel's records")); - } - let received = reader.lines(); - logstream::is_prefix_of(&received, &file)?; - let on_stream = received - .iter() - .filter_map(|l| toyos_logstream::program_line(l)) - .any(|said| said.tag == RUNNER && said.text == NONCE); - if !on_stream { - return Err(format!("the served log carries {NONCE:?} under no {RUNNER:?}")); - } - eprintln!( - " [origin] {NONCE:?} under {RUNNER:?} on the console, in /log and on \ - the served log ({} line(s), each /log's own)", - received.len() - ); - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} - -/// Boot `config` on a staged image, run `job` with `timeout`, shut down, and -/// hand back what it said and the whole of its `/log`. -fn one_job( - config: &str, - name: &str, - job: &str, - timeout: Duration, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(qemu::TestResult, String), String> { - one_job_armed(config, name, job, &[], timeout, c_bins, rust_bins) -} - -/// [`one_job`], its kernel armed with `params`. -fn one_job_armed( - config: &str, - name: &str, - job: &str, - params: &'static [&'static str], - timeout: Duration, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(qemu::TestResult, String), String> { - let staged = logstream::stage_armed(config, name, params, c_bins, rust_bins)?; - let options = BootOptions { - boot_image: Some(qemu::Staged::Written(staged.image.clone())), - kernel_params: params, - ..Default::default() - }; - let config = compile::repo_root().join(config); - let mut guest = QemuInstance::boot_with_options(&config, c_bins, rust_bins, options); - let mut console = guest.boot_log().to_string(); - let ran = guest.run_test(job, timeout); - let file = logstream::shut_down(guest, &mut console, &staged)?; - let _ = std::fs::remove_file(&staged.image); - Ok((ran, file.concat())) -} - -/// **No program's bytes make a line another writer's.** `test_rs_log_forger` -/// writes the words of the kernel's `exit:` record claiming it passed, a whole -/// kernel record's line, a carriage return in front of the kernel's -/// `Rebooting.`, and a line under netd's head, and exits 7. Every one of them -/// is in `/log` and on the console — as `test-runner`'s — and every judge -/// reads the truth: the kernel's exit record says 7, no kernel record carries -/// the forged words, no console line opens as the kernel's with them, and -/// netd said nothing (this boot runs no netd). A record it stamps `u64::MAX` -/// is written before the machine stops, not parked until it does. -pub fn forgery(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let (ran, log) = one_job("tests/testcases", "log-program-forgery", FORGER, Duration::from_secs(60), c_bins, rust_bins)?; - if ran.exit_code != Some(FORGER_CODE as i32) { - return Err(format!("{FORGER} exited {:?}\n{}", ran.exit_code, ran.stdout)); - } - let forged = bootlog::lines_of(&log, RUNNER); - let forgeries = [ - "exit: test_rs_log_forger pid=1 code=0 cpu=0ms", - "[2026-09-24 10:00:00 1.000 cpu0] exit: test_rs_log_forger", - "[kernel 1.000 cpu0] exit: test_rs_log_forger", - "netd: DHCP: lease 10.9.9.9/24 forged", - "Rebooting.", - ]; - // Non-vacuity: every forgery reached the file, and the console under the - // runner's head. - for words in forgeries { - if !forged.contains(words) { - return Err(format!("/log carries no {RUNNER} line with {words:?}: nothing was forged\n{log}")); - } - let headed = ran.serial.lines().any(|l| { - toyos_logstream::program_line(l).is_some_and(|said| said.tag == RUNNER && said.text.contains(words)) - }); - if !headed { - return Err(format!( - "the console carries no {RUNNER} line with {words:?}\n{}", - ran.serial - )); - } - } - // **A stamp at the end of time holds nothing back**: its line is written - // in the round that read it, long before the machine stops, and `logd` - // says it read the stamp as the moment it read the record. - let lines: Vec<&str> = log.lines().collect(); - let place = |what: &str, is: &dyn Fn(&str) -> bool| { - lines.iter().position(|l| is(l)).ok_or_else(|| format!("/log carries no {what}\n{log}")) - }; - let ahead = place("line stamped at the end of time", &|l| { - toyos_logstream::program_line(l).is_some_and(|said| said.tag == RUNNER && said.text == FORGER_AHEAD) - })?; - let stopping = place("stop line", &|l| l.contains(toyos_logstream::STOPPING))?; - if ahead > stopping { - return Err(format!( - "{FORGER_AHEAD:?} is after {:?} in /log: logd held it until the machine stopped", - toyos_logstream::STOPPING - )); - } - if !bootlog::lines_of(&log, "logd").contains(&format!("of {RUNNER}'s were stamped ahead of the clock")) { - return Err(format!("logd never said it read a stamp ahead of the clock\n{log}")); - } - // **The console, as nobody's program**: a line that does not open with a - // program's head reads as the kernel's, and no forged word may be in one. - if let Some(line) = ran - .serial - .lines() - .filter(|l| toyos_logstream::program_line(l).is_none()) - .find(|l| l.contains(&format!("{FORGER} pid=1 code=0")) || l.contains("10.9.9.9")) - { - return Err(format!("a program's words opened a console line as the kernel's: {line:?}")); - } - let kernel = bootlog::kernel_records(&log); - for (judge, text) in [("the whole log", log.as_str()), ("its kernel records", kernel.as_str())] { - match exit_of(text, FORGER) { - Some(exit) if exit.code == FORGER_CODE => {} - other => { - return Err(format!( - "the exit judge read {FORGER}'s verdict out of {judge} as {other:?}; it \ - exited {FORGER_CODE}" - )) - } - } - } - let forged_words = |l: &&str| l.contains(&format!("{FORGER} pid=1 code=0")) || l.contains("10.9.9.9"); - if let Some(line) = kernel.lines().find(forged_words) { - return Err(format!("a program's words are among the kernel's records: {line:?}")); - } - if !bootlog::lines_of(&log, "netd").is_empty() { - return Err(format!( - "this boot runs no netd, and /log carries netd lines:\n{}", - bootlog::lines_of(&log, "netd") - )); - } - eprintln!( - " [origin] five forgeries in /log and on the console, each under {RUNNER:?}; the exit \ - judge read {FORGER_CODE} and no kernel record or kernel-shaped console line carries a \ - forged word; its line stamped at the end of time was written before the stop" - ); - Ok(()) -} - -/// **A flood never slows its writer, and every line of it is accounted for.** -/// `test_rs_log_flood` writes megabytes of numbered lines, far more than its ring -/// holds, as fast as it can; a write never waits. Each line is in `/log` — -/// once, in order — or counted by `logd` as one its ring had no room for or -/// one past the program's allowance, and the three add up to every line it -/// wrote: a line lost without a count, or one written twice, is red. -pub fn flood(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let (ran, log) = one_job("tests/testcases", "log-program-flood", FLOODER, Duration::from_secs(300), c_bins, rust_bins)?; - if ran.exit_code != Some(0) { - return Err(format!("{FLOODER} exited {:?}", ran.exit_code)); - } - let said = bootlog::lines_of(&log, RUNNER); - let mut written = 0usize; - let mut last: Option = None; - let mut done = None; - for line in said.lines() { - // First: the last line opens with `flood ` too. - if line.starts_with(FLOOD_DONE) { - done = Some(line.to_string()); - written += 1; - } else if let Some(rest) = line.strip_prefix("flood ") { - let Some(n) = rest.split(' ').next().and_then(|n| n.parse::().ok()) else { - return Err(format!("/log carries a flood line with no number: {line:?}")); - }; - if last.is_some_and(|last| n <= last) || n >= FLOOD_LINES { - return Err(format!( - "/log carries flood line {n} after line {last:?}: a line was repeated or \ - reordered" - )); - } - last = Some(n); - written += 1; - } - } - // `logd`'s own counts of this program's lines it did not write. - let counted = |what: &str| -> usize { - bootlog::lines_of(&log, "logd") - .lines() - .filter_map(|l| l.strip_prefix("logd: ")) - .filter_map(|l| l.split_once(&format!(" record(s) of {RUNNER}'s {what}"))) - .filter_map(|(n, _)| n.parse::().ok()) - .sum() - }; - let refused = counted("found its ring full"); - let suppressed = counted("past its"); - let owed = FLOOD_LINES + 1; - if written + refused + suppressed != owed { - return Err(format!( - "the flood wrote {owed} lines and /log accounts for {}: {written} written, {refused} \ - refused a full ring and {suppressed} past the allowance", - written + refused + suppressed - )); - } - // Non-vacuity: a flood the log took whole says nothing about a count. - if refused + suppressed == 0 { - return Err(format!("all {owed} flood lines reached /log, so nothing here was counted")); - } - let done = done.unwrap_or_else(|| "its last line counted, not written".to_string()); - eprintln!( - " [origin] {owed} flood lines: {written} in /log in order, {refused} refused a full \ - ring, {suppressed} past the allowance; {done}" - ); - Ok(()) -} - -/// The job that asks for a stop the kernel refuses, and the line it says then. -const REFUSED_JOB: &str = "test_rs_log_refused_stop"; -const REFUSED_LINE: &str = "log refused stop: said after the refusal"; - -/// **A refused stop leaves the log written.** init has `logd` flush for a -/// stop, after which `logd` holds the file's lines back; the kernel, armed -/// with `power-refused-once`, refuses the stop and the machine runs on. The -/// job's line after the refusal is in `/log`, after the stop line, with -/// `logd`'s word that the file takes lines again. -pub fn refused_stop(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let (ran, log) = one_job_armed( - "tests/testcases", - "log-refused-stop", - REFUSED_JOB, - &["power-refused-once"], - Duration::from_secs(60), - c_bins, - rust_bins, - )?; - if ran.exit_code != Some(0) { - return Err(format!("{REFUSED_JOB} exited {:?}\n{}", ran.exit_code, ran.stdout)); - } - let lines: Vec<&str> = log.lines().collect(); - let stopping = lines - .iter() - .position(|l| l.contains(toyos_logstream::STOPPING)) - .ok_or_else(|| format!("/log carries no stop line: nothing was stopped\n{log}"))?; - let said = lines - .iter() - .position(|l| toyos_logstream::program_line(l).is_some_and(|s| s.tag == RUNNER && s.text == REFUSED_LINE)) - .ok_or_else(|| format!("/log carries no {REFUSED_LINE:?}: logd held the file back after the refused stop\n{log}"))?; - if said < stopping { - return Err(format!("{REFUSED_LINE:?} is before the stop it follows in /log\n{log}")); - } - if !bootlog::lines_of(&log, "logd").contains("logd: the stop was refused") { - return Err(format!("logd never said the stop was refused\n{log}")); - } - eprintln!(" [origin] a line said after a refused stop is in /log, after the stop line"); - Ok(()) -} - -/// What init says when it stops the machine without `logd`'s answer. -const FLUSH_WAITED_OUT: &str = "init: logd did not answer the flush in"; - -/// **A resume that reaches `logd` with its flush unrun answers that flush.** -/// `tests/logflushcase` holds `logd`'s first flush until init speaks again, so -/// init waits the flush out, the kernel refuses the stop, and the resume is -/// queued behind the flush `logd` has not run. `logd` runs the flush, then the -/// resume, and lives: the job's line after the refusal is in `/log`, and so is -/// init's word that it waited. -pub fn resume_meets_its_flush(rust_bins: &[(String, Vec)]) -> Result<(), String> { - const PARAMS: &[&str] = &["power-refused-once"]; - let config = "tests/logflushcase"; - let bins: Vec<(String, Vec)> = - rust_bins.iter().filter(|(name, _)| name == "log_refused_stop").cloned().collect(); - if bins.len() != 1 { - return Err(format!("the suite built {} copies of log_refused_stop", bins.len())); - } - let staged = logstream::stage_armed(config, "log-flush-held", PARAMS, &[], &bins)?; - let case = compile::repo_root().join(config); - let mut guest = QemuInstance::boot_with_options( - &case, - &[], - &bins, - BootOptions { - qmp: true, - boot_image: Some(qemu::Staged::Written(staged.image.clone())), - kernel_params: PARAMS, - ready_marker: "spawn: /system/bin/test_rs_log_refused_stop ", - ..Default::default() - }, - ); - let mut stop = qemu::QmpShutdown::open(guest.qmp_socket(), guest.budget(Duration::from_secs(120))); - let reason = stop.reason(); - let tail = guest.drain_serial(Duration::from_secs(20)); - drop(guest); - serial::Serial::named("the job list's drain", tail.as_str()).must_be_clean()?; - if reason.as_deref() != Some("guest-reset") { - return Err(format!("the job list did not end the boot ({reason:?})\n{tail}")); - } - let log = volumes::whole_log(&staged.image, staged.start, staged.len)?.concat(); - let _ = std::fs::remove_file(&staged.image); - let ended = format!("{}logd pid=", bootlog::EXIT); - if let Some(line) = format!("{tail}{log}").lines().find(|l| l.contains(&ended)) { - return Err(format!("logd ended before the machine did: {line}\n{tail}")); - } - // Non-vacuity: init waited the flush out, so the resume met it unrun. - if !bootlog::lines_of(&log, "init").contains(FLUSH_WAITED_OUT) { - return Err(format!("init never waited a flush out, so nothing arrived together\n{log}")); - } - let lines: Vec<&str> = log.lines().collect(); - let said = lines - .iter() - .position(|l| toyos_logstream::program_line(l).is_some_and(|s| s.tag == RUNNER && s.text == REFUSED_LINE)) - .ok_or_else(|| format!("/log carries no {REFUSED_LINE:?} after a resume met its flush\n{log}"))?; - let stopping = lines - .iter() - .position(|l| l.contains(toyos_logstream::STOPPING)) - .ok_or_else(|| format!("/log carries no stop line: nothing was stopped\n{log}"))?; - if said < stopping { - return Err(format!("{REFUSED_LINE:?} is before the stop it follows in /log\n{log}")); - } - eprintln!(" [origin] init waited the flush out, logd ran it and then the resume, and the line after is in /log"); - Ok(()) -} - -/// The job, the line it says after its records, and how many records it has -/// the kernel write first. -const HOLD_JOB: &str = "test_rs_log_hold"; -const HOLD_LINE: &str = "log hold: said after 192 records"; -const HOLD_RECORDS: usize = 192; -/// The kernel's record of each of those. -const RETIRED: &str = "syscall 26 is retired"; - -/// **A program's line lands between the records written before and after -/// it.** `test_rs_log_hold` has the kernel write three batches of records, -/// says its line and exits: `logd` reads the program's ring before the -/// kernel's records in every round, so the line is in its hands before the -/// last of them are, and only the stamp each was written with puts it after -/// them all in `/log` — and before the kernel's record of its exit. -pub fn after_records(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let (ran, log) = - one_job("tests/testcases", "log-hold", HOLD_JOB, Duration::from_secs(60), c_bins, rust_bins)?; - if ran.exit_code != Some(0) { - return Err(format!("{HOLD_JOB} exited {:?}\n{}", ran.exit_code, ran.stdout)); - } - let lines: Vec<&str> = log.lines().collect(); - let said = lines - .iter() - .position(|l| toyos_logstream::program_line(l).is_some_and(|s| s.tag == RUNNER && s.text == HOLD_LINE)) - .ok_or_else(|| format!("/log carries no {HOLD_LINE:?} under {RUNNER:?}"))?; - let records: Vec = lines - .iter() - .enumerate() - .filter(|(_, l)| !toyos_logstream::is_program_line(l) && l.contains(RETIRED)) - .map(|(i, _)| i) - .collect(); - if records.len() != HOLD_RECORDS { - return Err(format!("/log carries {} of the job's {HOLD_RECORDS} records", records.len())); - } - let after = records.iter().filter(|&&i| i > said).count(); - if after > 0 { - return Err(format!( - "{after} of the {HOLD_RECORDS} records written before {HOLD_LINE:?} are after it in \ - /log" - )); - } - let exit = format!("{}{} pid=", bootlog::EXIT, bootlog::recorded_name(HOLD_JOB)); - let exited = lines - .iter() - .position(|l| !toyos_logstream::is_program_line(l) && l.contains(&exit)) - .ok_or_else(|| format!("/log carries no {exit:?} record"))?; - if exited < said { - return Err(format!( - "the kernel's record of {HOLD_JOB}'s exit is before the line it said first, in /log" - )); - } - eprintln!( - " [origin] {HOLD_LINE:?} is after every one of its {HOLD_RECORDS} records in /log, and \ - before its exit" - ); - Ok(()) -} - -/// The job that prints init's word accepting a swap of netd, and that word. -const CARRIER_FORGER: &str = "test_rs_log_carrier_forger"; -const CARRIER_FORGED: &str = - "init: swap netd: accepted: /tmp/swap/forged/netd replaces /system/bin/netd (pid 1)"; - -/// **Only init's word to `logd` can turn the network's readers away.** A job -/// prints the very line init says accepting a swap of netd; a reader connecting -/// after it is admitted, and `/log` carries the line under the job's runner and -/// no word from `logd` that it turns readers away. -pub fn carrier_forgery(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let staged = logstream::stage(VIRTIO.config, "log-carrier-forgery", c_bins, rust_bins)?; - let port = qemu::free_host_port(); - let options = BootOptions { - profile: VIRTIO.profile, - boot_image: Some(qemu::Staged::Written(staged.image.clone())), - log_port: Some(port), - ..Default::default() - }; - let config = compile::repo_root().join(VIRTIO.config); - let mut guest = QemuInstance::boot_with_options(&config, c_bins, rust_bins, options); - let mut console = guest.boot_log().to_string(); - qemu::await_marker(&mut guest, &mut console, logstream::SERVING, "logd to open its port")?; - let ran = guest.run_test(CARRIER_FORGER, Duration::from_secs(60)); - if ran.exit_code != Some(0) { - return Err(format!("{CARRIER_FORGER} exited {:?}\n{}", ran.exit_code, ran.stdout)); - } - let reader = logstream::reader(port, "log-carrier-forgery.txt") - .map_err(|e| format!("a reader asking after a program printed init's word was not admitted: {e}"))?; - if !reader.wait_for(CARRIER_FORGED, Duration::from_secs(60)) { - return Err(format!("the served log never carried {CARRIER_FORGED:?}")); - } - let file = logstream::shut_down(guest, &mut console, &staged)?; - if !reader.wait_ended(Duration::from_secs(60)) { - return Err("the reader's connection had not ended once the guest was down".to_string()); - } - let log = file.concat(); - if !bootlog::lines_of(&log, RUNNER).lines().any(|l| l == CARRIER_FORGED) { - return Err(format!("/log carries no {CARRIER_FORGED:?} under {RUNNER:?}: nothing was forged")); - } - if bootlog::lines_of(&log, "logd").contains(toyos_logstream::CARRIER_LEAVING) { - return Err(format!( - "a program's line moved logd to turn readers away: /log carries {:?}", - toyos_logstream::CARRIER_LEAVING - )); - } - logstream::is_prefix_of(&reader.lines(), &file)?; - eprintln!( - " [origin] {RUNNER:?} printed init's word accepting a swap of netd; a reader after it was \ - admitted, and logd turned nobody away" - ); - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} - -/// **netd answers for its name, to its link and to nobody off it.** The host -/// stands on the guest's segment (`segment`) as a neighbour, [`NEIGHBOUR`]. Once -/// the guest holds its lease, the neighbour makes itself known (an ARP request -/// for the guest's address, which the guest answers) and then puts three -/// legacy resolvers' queries (RFC 6762 §6.7) on the wire: -/// -/// 1. for this machine's name, from `127.0.0.1` — a source RFC 1122 -/// §3.2.1.3 says a host MUST NOT send and MUST silently discard; -/// 2. for another name, from the neighbour; -/// 3. for this machine's name, from the neighbour. -/// -/// Only the third is answered: the lease's address, the asker's ID and -/// question, a TTL of ten seconds, addressed to the neighbour. Silence is not -/// waited for — netd answers one socket's queries in the order they arrived, -/// through one socket's queue sent in order, so an answer to either earlier -/// query would be on the wire before the third one's. -/// -/// The frames, the query and the reading of the answer are spelled here, byte -/// by byte from RFC 826, 791, 768 and 1035 §4.1, and not by `toyos_mdns`, -/// which is what wrote the answer. -pub fn mdns(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let options = BootOptions { profile: VIRTIO.profile, segment: true, ..Default::default() }; - let config = compile::repo_root().join(VIRTIO.config); - let mut guest = QemuInstance::boot_with_options(&config, c_bins, rust_bins, options); - let mut console = guest.boot_log().to_string(); - qemu::await_marker(&mut guest, &mut console, "netd: DHCP: lease ", "netd's lease")?; - let mut wire = guest.segment()?; - let deadline = || Instant::now() + Duration::from_secs(10); - - wire.send(&segment::arp_request(NEIGHBOUR_MAC, NEIGHBOUR, GUEST))?; - let until = deadline(); - let guest_mac = loop { - let frame = wire.next(until).map_err(|e| format!("no ARP reply for {GUEST:?} in 10 s: {e}"))?; - if let Some(mac) = segment::arp_reply_for(&frame, GUEST) { - break mac; - } - }; - - // Where slirp delivers anything the guest sends to an address on its - // network that is none of slirp's own: host loopback, at this port. Held - // here so that no other process on the host is handed it. - let stray = UdpSocket::bind((Ipv4Addr::LOCALHOST, 0)).map_err(|e| format!("bind: {e}"))?; - let port = stray.local_addr().map_err(|e| format!("{e}"))?.port(); - let host = toyos_build::lan::HOSTNAME; - let ask = |from: [u8; 4], payload: &[u8]| { - segment::Udp { - dst_mac: guest_mac, - src_mac: NEIGHBOUR_MAC, - src: (from, port), - dst: (GUEST, MDNS_PORT), - payload, - } - .frame() - }; - const LOOPBACK_ID: u16 = 0x7f01; - const OTHER_ID: u16 = 0x0bad; - const OWN_ID: u16 = 0x5eed; - wire.send(&ask([127, 0, 0, 1], &query(LOOPBACK_ID, host)))?; - wire.send(&ask(NEIGHBOUR, &query(OTHER_ID, "some-other-host")))?; - wire.send(&ask(NEIGHBOUR, &query(OWN_ID, host)))?; - - let until = deadline(); - let (answer, to) = loop { - let frame = wire.next(until).map_err(|e| format!("no answer for {host}.local in 10 s: {e}"))?; - let Some(udp) = segment::udp_in(&frame) else { continue }; - if udp.src != (GUEST, MDNS_PORT) || udp.payload.len() < 2 { - continue; - } - match u16::from_be_bytes([udp.payload[0], udp.payload[1]]) { - LOOPBACK_ID => { - return Err(format!( - "a query from 127.0.0.1 was answered, to {:?}: {:02x?}", - udp.dst, udp.payload - )); - } - OTHER_ID => return Err(format!("a query for another name was answered: {:02x?}", udp.payload)), - OWN_ID => break (udp.payload.to_vec(), (udp.dst_mac, udp.dst)), - _ => {} - } - }; - let mut want = query(OWN_ID, host); - // QR and AA, one question, one answer. - want[2..8].copy_from_slice(&[0x84, 0x00, 0, 1, 0, 1]); - want.extend_from_slice(&name(host)); - want.extend_from_slice(&[0, 1, 0, 1, 0, 0, 0, 10, 0, 4]); - want.extend_from_slice(&GUEST); - if answer != want { - return Err(format!("{host}.local was answered {answer:02x?}, and {want:02x?} is owed")); - } - if to != (NEIGHBOUR_MAC, (NEIGHBOUR, port)) { - return Err(format!("{host}.local was answered to {to:02x?}, not to the neighbour that asked")); - } - drop(guest); - serial::Serial::named("the boot", console.as_str()).must_be_clean()?; - eprintln!( - " [mdns] {host}.local answered {GUEST:?} to an on-link neighbour; 127.0.0.1 and another \ - name, nothing" - ); - Ok(()) -} - -/// The address slirp's DHCP gives the first guest on its network, and a -/// neighbour on the same /24 that is none of slirp's own addresses. -const GUEST: [u8; 4] = [10, 0, 2, 15]; -const NEIGHBOUR: [u8; 4] = [10, 0, 2, 7]; -/// A locally administered unicast address (IEEE 802 bit 1 of the first octet). -const NEIGHBOUR_MAC: [u8; 6] = [0x52, 0x54, 0x00, 0x0a, 0x00, 0x07]; -/// RFC 6762 §3: the port every multicast DNS responder listens on. -const MDNS_PORT: u16 = 5353; - -/// `.local` as labels. -fn name(host: &str) -> Vec { - let mut out = vec![host.len() as u8]; - out.extend_from_slice(host.as_bytes()); - out.extend_from_slice(b"\x05local\x00"); - out -} - -/// One question, type A, class IN, from a port that is not 5353. -fn query(id: u16, host: &str) -> Vec { - let mut out = vec![(id >> 8) as u8, id as u8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0]; - out.extend_from_slice(&name(host)); - out.extend_from_slice(&[0, 1, 0, 1]); - out -} diff --git a/tests/common/orphan.rs b/tests/common/orphan.rs deleted file mode 100644 index 643ba749a8c..00000000000 --- a/tests/common/orphan.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! A guest a `SIGKILL`ed harness would leave running: the owner, and the test -//! that kills it and watches its QEMU go. - -use std::io::Read; -use std::path::Path; -use std::process::Command; - -use toyos_build::tether::Owner; -use toyos_build::testargs; -use toyos_tmpdir::TempDir; - -use super::qemu::{self, BootOptions, QemuInstance, Staged}; - -/// What the owner prints its QEMU's pid after. -const HELD: &str = "held: qemu "; - -/// `--hold `: boot `image`, print its QEMU's pid, and hold it until -/// stdin ends. -pub fn hold(test_config: &Path, image: &Path) { - let options = BootOptions { boot_image: Some(Staged::Pristine(image.to_path_buf())), ..Default::default() }; - let guest = QemuInstance::boot_with_options(test_config, &[], &[], options); - println!("{HELD}{}", guest.pid()); - std::io::stdin().read_to_end(&mut Vec::new()).expect("read the owner's stdin"); -} - -/// The harness's `SIGKILL` ends its guest, whose QEMU inherited `SIGHUP` -/// blocked and ignored. -pub fn guest_dies_with_its_harness(test_config: &Path) -> Result<(), String> { - let tmp = TempDir::new("orphan"); - let short = TempDir::short("orphan"); - let image = tmp.join("boot.img"); - std::fs::write(&image, qemu::build_boot_image(test_config, &[], &[], &[])) - .map_err(|e| format!("write {}: {e}", image.display()))?; - let mut owner = Command::new(std::env::current_exe().unwrap()); - owner.arg(testargs::HOLD.name).arg(&image).env("TMPDIR", &tmp); - let mut owner = Owner::spawn(owner)?; - let owner_pid = owner.pid(); - let verdict = (|| { - // The owner builds nothing, so its one wait is its boot, whose own - // ceiling ends it well inside the backstop on any wait on a guest. - let pid: u32 = - owner.said(HELD, qemu::GUEST_WEDGED)?.parse().map_err(|e| format!("the owner's QEMU pid: {e}"))?; - owner.killed(&[pid]).map(|()| pid) - })(); - short.adopt(Path::new(toyos_tmpdir::SHORT_BASE), owner_pid); - let pid = verdict?; - eprintln!(" [orphan] QEMU {pid} gone after its harness's SIGKILL"); - Ok(()) -} diff --git a/tests/common/partclaim.rs b/tests/common/partclaim.rs deleted file mode 100644 index b90d0798945..00000000000 --- a/tests/common/partclaim.rs +++ /dev/null @@ -1,529 +0,0 @@ -//! A GPT partition as a claimable device, judged off the disks. -//! -//! The guest (`tests/toyos-rust-tests/src/bin/partition_claimant.rs`) claims -//! partitions of disks this file crafted and asserts every refusal the ABI -//! promises. What it cannot judge is what its writes did to the disks — that is -//! the claim in question — so the verdict is read here, after the guest has -//! gone, off the images: -//! -//! - every byte of the crafted disk outside the target and DATA is the byte this -//! file wrote: the primary and backup tables, both neighbours, the granted, -//! two misaligned partitions and the twin of the stick's log partition, and -//! the gaps; -//! - both neighbours are FAT32 volumes `toyos-fat32-check` (fatgen103's rules) -//! has nothing to say about — the neighbour after the target begins at the -//! block after its last, so a write one past the end lands in its boot -//! sector; -//! - every block of the target is the pattern the guest wrote there; -//! - the `/home` file fsd wrote between the target's transfers, through its -//! own claim on the same disk, reads back through the host's own bcachefs -//! reader. -//! -//! The partition ranges are UEFI 2.10 §5.3.3's, as the `gpt` crate — not the -//! kernel's parser — laid them out, and each partition's unique GUID is fixed -//! here, where the table and the `system.toml` naming it are both written. - -use std::io::{Seek, SeekFrom, Write}; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use super::qemu::{self, BootOptions, QemuInstance, Staged}; - -/// Mirrored in the guest binary: the idle ROOT slot the guest writes whole. -const TARGET: &str = "7B1D4A3C-2E5F-4C8A-9D6B-0A1F2E3D4C5B"; -/// Mirrored in the guest and in `tests/partclaimcase/system.toml`. -const GRANTED: &str = "A94F0E6D-3B2C-4E1A-8C7D-6E5F4A3B2C1D"; -/// Mirrored: a partition whose length is not whole 4 KiB blocks. -const MISALIGNED: &str = "3E8A1C5F-7D2B-4F60-9A1E-5C4B3D2E1F07"; -/// Mirrored: a partition of whole 4 KiB blocks that begins inside one. -const MISSTART: &str = "5A7C9E1B-3D5F-4B71-8C2E-4F6A8B0C2D35"; -/// Mirrored: DATA, which fsd serves `/home` from. -const DATA: &str = "E3A7C5D9-1B2F-4E6A-8D0C-9F7B5A3E1C24"; - -/// The two FAT32 neighbours' type, and every other test partition's. -pub(super) const NEIGHBOUR_TYPE: &str = "5C3E8F21-9A4B-4D7E-8F10-2B3C4D5E6F70"; -pub(super) const PLAIN_TYPE: &str = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"; - -/// Mirrored: the target's length in blocks. -const TARGET_BLOCKS: u64 = 2048; -/// The granted partition's length in blocks. -const GRANTED_BLOCKS: u64 = 256; -const HOME_FILE: &str = "home/partclaim-interleaved.bin"; -const HOME_CHUNK: usize = 32 * 1024; -const PAST_END: &[u8; 16] = b"TOYOS-PAST-END\0\0"; -/// The claims the guest expects refused: ROOT, the two partitions init minted -/// for file servers, the one init granted test-runner, the log partition two -/// disks carry, one whose length and one whose start is not whole blocks, an -/// absent GUID, the zero GUID, three claims carrying selector words their -/// class does not read, the target a second time, and the target while a -/// child holds it. -const REFUSALS: usize = 14; -const BLOCK: u64 = 4096; -const MIB: u64 = 1024 * 1024; - -/// The config whose one difference from the test estate is the granted row. -const CONFIG: &str = "tests/partclaimcase"; - -/// Mirrored in the guest: what block `n` of the target holds once it is done. -fn pattern(n: u64) -> Vec { - let mut block = vec![0u8; BLOCK as usize]; - for (i, byte) in block.iter_mut().enumerate() { - *byte = (n as usize).wrapping_mul(31).wrapping_add(i) as u8; - } - block[..8].copy_from_slice(&n.to_le_bytes()); - block[8..24].copy_from_slice(b"TOYOS-PARTCLAIM\0"); - block -} - -/// Where one partition landed, in bytes. -#[derive(Clone, Copy, Debug)] -pub(super) struct Span { - pub(super) start: u64, - pub(super) len: u64, -} - -impl Span { - pub(super) fn end(self) -> u64 { - self.start + self.len - } - pub(super) fn of(self, disk: &[u8]) -> &[u8] { - &disk[self.start as usize..self.end() as usize] - } -} - -struct Layout { - before: Span, - target: Span, - after: Span, - misstart: Span, - data: Span, -} - -/// The claims, refusals, idle ROOT slot, releases and neighbours, on a -/// machine booting off its USB stick with the crafted disk beside it on the -/// bus. The crafted disk carries a copy of the stick's log partition's unique -/// GUID, so two disks the kernel drives name one partition: that claim is -/// refused, and no file server is handed the log. -pub fn partition_claim( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = super::compile::repo_root().join(CONFIG); - let boot_image = super::lane::dir().join("partclaim-boot.img"); - std::fs::write(&boot_image, qemu::build_boot_image(&config, c_bins, rust_bins, &[])) - .map_err(|e| format!("write the boot image: {e}"))?; - let [esp, log, root] = boot_stick_guids(&boot_image)?; - let crafted = super::lane::dir().join("partclaim-disk.img"); - let layout = craft_disk(&crafted, &log)?; - - // The premises, checked rather than assumed: a neighbour that did not - // begin where the target ends would let a write past the end land in a - // gap this test does not look at as hard. - if layout.before.end() != layout.target.start || layout.target.end() != layout.after.start { - return Err(format!("the neighbours do not touch the target: {:?}", ( - layout.before, layout.target, layout.after - ))); - } - if layout.misstart.start % BLOCK == 0 || layout.misstart.len % BLOCK != 0 { - return Err(format!("the misaligned start is not one: {:?}", layout.misstart)); - } - if layout.target.len != TARGET_BLOCKS * BLOCK { - return Err(format!("the target is {} bytes, not {TARGET_BLOCKS} blocks", layout.target.len)); - } - let before = std::fs::read(&crafted).map_err(|e| format!("read the crafted disk: {e}"))?; - for (what, span) in [("first", layout.before), ("second", layout.after)] { - let complaints = toyos_fat32_check::check(span.of(&before)); - if !complaints.is_empty() { - return Err(format!( - "the {what} neighbour is not a clean FAT32 before any guest ran:\n{}", - toyos_fat32_check::describe(&complaints) - )); - } - } - - let mut qemu = QemuInstance::boot_with_options( - &config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::UsbDisk, - boot_image: Some(Staged::Pristine(boot_image.clone())), - usb_images: vec![crafted.clone()], - nvme_image: Some(tableless_nvme("partclaim-nvme.img")?), - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - no_panic("booting the claim disks", &boot)?; - // Formatted, on a first boot of the crafted disk: its DATA carries the - // designation, and the readback below is what says it was this disk's. - if !boot.contains("fsd: block 0 designates this partition for ToyOS; formatting it") { - return Err(format!("fsd never formatted DATA off the crafted disk, so nothing shares it:\n{boot}")); - } - // init names what it could not mint; a grant it refused would make the - // guest's endowment about nothing. - let grant = format!("part:{GRANTED}"); - if let Some(line) = boot.lines().find(|l| l.contains("init: test-runner:") && l.contains(&grant)) { - return Err(format!("init did not grant test-runner its partition: {line}\n{boot}")); - } - - // A guest that failed is judged off the disk all the same: what its failure - // did to the neighbours is the half of the verdict it cannot give itself. - let run = format!("test_rs_partition_claimant main {esp} {log} {root}"); - let result = qemu.run_test(&run, Duration::from_secs(180)); - let tail = shut_down(qemu); - let guest = guest_verdict(&result, &tail, REFUSALS).and_then(|kernel| main_kernel_lines(&kernel, &log)); - no_panic("on the way down", &tail)?; - - let after = std::fs::read(&crafted).map_err(|e| format!("read the disk back: {e}"))?; - let neighbours = neighbours_untouched(&layout, &before, &after); - if guest.is_err() || !neighbours.is_empty() { - return Err(format!( - "guest: {}\nneighbours, off the image: {}", - guest.err().unwrap_or_else(|| "every assertion held".to_string()), - if neighbours.is_empty() { "untouched".to_string() } else { neighbours.join("\n") } - )); - } - target_holds_the_pattern(&layout, &after)?; - home_file_reads_back(&crafted)?; - - for path in [&crafted, &boot_image] { - let _ = std::fs::remove_file(path); - } - eprintln!( - " [partclaim] {REFUSALS} claims refused by name; the idle ROOT slot's {TARGET_BLOCKS} \ - blocks written and read back through the claim; released by close and by its holder's \ - death; both FAT32 neighbours untouched byte for byte and clean to fatgen103; /home \ - intact" - ); - Ok(()) -} - -/// What the guest said: exit 0, `refusals` refusals said by name — an exit -/// code alone is also what a binary that asserted nothing leaves — and the -/// kernel's log from the test's start through the shutdown's `tail`, which is -/// returned. The runner's end marker reaches the console through `logd` and -/// the kernel's records through `klogd`, so a record the kernel made before -/// the test ended can arrive after the marker; the shutdown's drain carries it. -fn guest_verdict(result: &qemu::TestResult, tail: &str, refusals: usize) -> Result { - let kernel = format!("{}{}{tail}", result.before, result.serial); - if result.exit_code != Some(0) { - return Err(format!( - "the guest failed:\n{}\nkernel log while it ran:\n{kernel}", - result.stdout - )); - } - let said = result.stdout.lines().filter(|l| l.contains(" refused with ")).count(); - if said != refusals || !result.stdout.contains("partition_claimant: PASS") { - return Err(format!( - "the guest exited 0 having said {said} of its {refusals} refusals:\n{}", - result.stdout - )); - } - Ok(kernel) -} - -/// The kernel's own account of the main run: its hold on ROOT named once, the -/// log partition's twin and both misaligned partitions refused by name, and -/// not one line for a transfer refused past the end — a caller can ask at -/// syscall rate. -fn main_kernel_lines(kernel: &str, twin: &str) -> Result<(), String> { - let held = kernel.matches("is held by the kernel").count(); - if held != 1 { - return Err(format!("the kernel named its own hold {held} times for ROOT alone:\n{kernel}")); - } - for want in [ - format!("partclaim: {twin} is on device "), - format!("partclaim: {MISALIGNED} is at "), - format!("partclaim: {MISSTART} is at "), - ] { - if !kernel.contains(&want) { - return Err(format!("the kernel never said {want:?}:\n{kernel}")); - } - } - if let Some(line) = kernel.lines().find(|l| l.contains("block(s) at") && l.contains("refusing")) { - return Err(format!("a caller's refused transfer wrote the kernel's log: {line:?}")); - } - Ok(()) -} - -pub(super) fn no_panic(when: &str, log: &str) -> Result<(), String> { - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} {when}\n{log}")); - } - } - Ok(()) -} - -/// `run shutdown`, and what the console said on the way down. -pub(super) fn shut_down(mut qemu: QemuInstance) -> String { - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - qemu.drain_serial(Duration::from_secs(30)) -} - -/// The unique GUIDs of the ESP, the log partition and ROOT the image builder -/// drew for this boot image: the partitions the guest must find the kernel -/// holding. -fn boot_stick_guids(image: &Path) -> Result<[String; 3], String> { - let disk = gpt::GptConfig::new() - .writable(false) - .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) - .open(image) - .map_err(|e| format!("the boot image has no readable GPT: {e}"))?; - let one = |kind: &str, what: &str| -> Result { - let found: Vec<_> = disk - .partitions() - .values() - .filter(|p| p.part_type_guid.guid.eq_ignore_ascii_case(kind)) - .collect(); - match found.as_slice() { - [part] => Ok(part.part_guid.to_string().to_uppercase()), - _ => Err(format!("the boot image has {} of {what}, expected one", found.len())), - } - }; - // ROOT's type is read with the kernel's parser: the `gpt` crate answers the - // all-zero GUID for a type its own table does not name. - let bytes = std::fs::read(image).map_err(|e| format!("read the boot image: {e}"))?; - let root = toyos_build::image::only_partition( - &mut super::volumes::ImageSectors { bytes: &bytes }, - toyos_gpt::Guid::TOYOS_ROOT, - ) - .map_err(|why| format!("the boot image's ROOT: {why}"))?; - Ok([ - one(gpt::partition_types::EFI.guid, "ESP")?, - one(gpt::partition_types::BASIC.guid, "log partition")?, - root.unique_guid().to_string(), - ]) -} - -/// Everything that says a byte outside the target and DATA moved: the first -/// such byte, and each neighbour fatgen103's rules have something to say about. -fn neighbours_untouched(layout: &Layout, before: &[u8], after: &[u8]) -> Vec { - let mut found = Vec::new(); - if before.len() != after.len() { - found.push(format!("the disk changed size: {} -> {}", before.len(), after.len())); - return found; - } - let owned = [layout.target, layout.data]; - let mut at = 0u64; - while at < before.len() as u64 { - if let Some(span) = owned.iter().find(|s| s.start <= at && at < s.end()) { - at = span.end(); - continue; - } - let i = at as usize; - if before[i] != after[i] { - let block = at / BLOCK; - let past_end = after[(block * BLOCK) as usize..][..PAST_END.len()] == *PAST_END; - found.push(format!( - "byte {at} (device block {block}) changed outside the claimed partition{}", - if past_end { " — it holds the write the guest made past the end" } else { "" } - )); - break; - } - at += 1; - } - for (what, span) in [("first", layout.before), ("second", layout.after)] { - let complaints = toyos_fat32_check::check(span.of(after)); - if !complaints.is_empty() { - found.push(format!( - "the {what} neighbour is not the FAT32 it was:\n{}", - toyos_fat32_check::describe(&complaints) - )); - } - } - found -} - -/// Every block of the target is the pattern the guest wrote there. -fn target_holds_the_pattern(layout: &Layout, after: &[u8]) -> Result<(), String> { - let target = layout.target.of(after); - for n in 0..TARGET_BLOCKS { - let got = &target[(n * BLOCK) as usize..((n + 1) * BLOCK) as usize]; - if got != pattern(n) { - return Err(format!("target block {n} is not what the guest wrote there")); - } - } - Ok(()) -} - -/// The `/home` file the guest wrote between the target's transfers, through -/// the host's bcachefs reader over a plain seek-and-read of the image. -fn home_file_reads_back(image: &Path) -> Result<(), String> { - let io = super::storage::FileBlocks::open(image)?; - let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(io) - .map_err(|e| format!("DATA does not mount on the host: {e:?}"))?; - let got = fs.read_file(HOME_FILE).map_err(|e| format!("reading {HOME_FILE}: {e:?}"))?; - let runs = TARGET_BLOCKS.div_ceil(32); - let want: Vec = (0..runs) - .filter(|run| run % 8 == 0) - .flat_map(|run| (0..HOME_CHUNK).map(move |i| (run as usize ^ i) as u8)) - .collect(); - if got != want { - return Err(format!( - "{HOME_FILE} is {} bytes off the image against the {} the guest wrote", - got.len(), - want.len() - )); - } - Ok(()) -} - -/// One partition a crafted table carries: its name, length in bytes, type, -/// unique GUID, and the boundary it begins on in 512-byte LBAs. -pub(super) type Part<'a> = (&'static str, u64, &'static str, &'a str, u64); - -/// Where every partition but one begins. -pub(super) const ALIGNED: u64 = MIB / 512; - -/// A disk of `bytes` at `path` holding `parts` in order, each on its own -/// boundary, with the `gpt` crate writing both copies of the table; the disk, -/// and each partition's span in the same order. -pub(super) fn table(path: &Path, bytes: u64, parts: &[Part]) -> Result<(Box, Vec), String> { - let file = std::fs::File::create(path).map_err(|e| format!("create the disk: {e}"))?; - file.set_len(bytes).map_err(|e| format!("size the disk: {e}"))?; - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(path) - .map_err(|e| format!("open the disk: {e}"))?; - let mbr = - gpt::mbr::ProtectiveMBR::with_lb_size(u32::try_from(bytes / 512 - 1).unwrap_or(0xFFFF_FFFF)); - mbr.overwrite_lba0(&mut file).map_err(|e| format!("protective MBR: {e}"))?; - let mut gdisk = gpt::GptConfig::default() - .initialized(false) - .writable(true) - .logical_block_size(gpt::disk::LogicalBlockSize::Lb512) - .create_from_device(Box::new(file), None) - .map_err(|e| format!("create the table: {e}"))?; - gdisk - .update_partitions(std::collections::BTreeMap::new()) - .map_err(|e| format!("initialise the table: {e}"))?; - let mut ids = Vec::new(); - for &(name, len, guid, _, align) in parts { - let ty = gpt::partition_types::Type { guid, os: gpt::partition_types::OperatingSystem::None }; - let id = gdisk.add_partition(name, len, ty, 0, Some(align)); - ids.push(id.map_err(|e| format!("add {name}: {e}"))?); - } - // Each unique GUID fixed where the table is written, so the `part:` row or - // the guest constant that names it names this partition and no other. - let mut fixed = gdisk.partitions().clone(); - for (id, &(name, _, _, unique, _)) in ids.iter().zip(parts) { - let part = fixed.get_mut(id).ok_or_else(|| format!("{name} was just added"))?; - part.part_guid = uuid::Uuid::parse_str(unique).map_err(|e| format!("{unique}: {e}"))?; - } - gdisk.update_partitions(fixed).map_err(|e| format!("fix the unique GUIDs: {e}"))?; - let lb = gpt::disk::LogicalBlockSize::Lb512; - let mut spans = Vec::new(); - for id in &ids { - let part = gdisk.partitions().get(id).ok_or("a partition that was just added")?; - spans.push(Span { - start: part.bytes_start(lb).map_err(|e| format!("start: {e}"))?, - len: part.bytes_len(lb).map_err(|e| format!("length: {e}"))?, - }); - } - let device = gdisk.write().map_err(|e| format!("write the table: {e}"))?; - Ok((device, spans)) -} - -/// The crafted disk: a FAT32 neighbour, the idle ROOT slot, a FAT32 neighbour -/// touching it, the partition init grants, a partition that is not whole -/// blocks, one that begins inside a block, a partition carrying `twin` as its -/// unique GUID, and a DATA fsd formats and serves `/home` from. -fn craft_disk(path: &Path, twin: &str) -> Result { - const FAT_BYTES: u64 = 34 * MIB; - const DATA_BYTES: u64 = 96 * MIB; - let parts: [Part; 8] = [ - ("neighbour before", FAT_BYTES, NEIGHBOUR_TYPE, "11111111-2222-4333-8444-555555555501", ALIGNED), - ("idle ROOT slot", TARGET_BLOCKS * BLOCK, toyos_gpt::Guid::TOYOS_ROOT_TEXT, TARGET, ALIGNED), - ("neighbour after", FAT_BYTES, NEIGHBOUR_TYPE, "11111111-2222-4333-8444-555555555502", ALIGNED), - ("granted", GRANTED_BLOCKS * BLOCK, PLAIN_TYPE, GRANTED, ALIGNED), - ("misaligned", MIB + 512, PLAIN_TYPE, MISALIGNED, ALIGNED), - // Right after the one above, at the first LBA past its odd length: whole - // blocks long, and beginning 512 bytes into one. - ("misaligned start", MIB, PLAIN_TYPE, MISSTART, 1), - ("twin", MIB, PLAIN_TYPE, twin, ALIGNED), - ("ToyOS data", DATA_BYTES, toyos_gpt::Guid::TOYOS_DATA_TEXT, DATA, ALIGNED), - ]; - let total = MIB + parts.iter().map(|p| p.1.next_multiple_of(MIB)).sum::() + 2 * MIB; - let (mut device, spans) = table(path, total, &parts)?; - let layout = Layout { - before: spans[0], - target: spans[1], - after: spans[2], - misstart: spans[5], - data: spans[7], - }; - for (label, span) in [("PC-BEFORE", layout.before), ("PC-AFTER", layout.after)] { - let volume = fat32(span.len as usize, label)?; - device.seek(SeekFrom::Start(span.start)).map_err(|e| format!("seek: {e}"))?; - device.write_all(&volume).map_err(|e| format!("write {label}: {e}"))?; - } - designate(&mut *device, layout.data)?; - device.flush().map_err(|e| format!("flush the disk: {e}"))?; - Ok(layout) -} - -/// An NVMe disk with no partition table, named `name` in the lane: beside a -/// stick carrying DATA, the machine's one DATA partition is the stick's, where -/// the lane's blank NVMe image would carry a second and DATA be refused. -pub(super) fn tableless_nvme(name: &str) -> Result { - let path = super::lane::dir().join(name); - std::fs::File::create(&path) - .and_then(|file| file.set_len(qemu::NVME_SMALL)) - .map_err(|e| format!("make {}: {e}", path.display()))?; - Ok(path) -} - -/// The designation on `data`: fsd formats a DATA only on this consent. -pub(super) fn designate(device: &mut dyn gpt::DiskDevice, data: Span) -> Result<(), String> { - let mut stamp = [0u8; BLOCK as usize]; - stamp[..bcachefs::DESIGNATION_MAGIC.len()].copy_from_slice(&bcachefs::DESIGNATION_MAGIC); - let at = bcachefs::DESIGNATION_BLOCKS_OFFSET; - stamp[at..at + 8].copy_from_slice(&(data.len / BLOCK).to_le_bytes()); - device.seek(SeekFrom::Start(data.start)).map_err(|e| format!("seek: {e}"))?; - device.write_all(&stamp).map_err(|e| format!("stamp DATA: {e}")) -} - -/// A USB stick of `bytes` carrying `parts`, each a name, a length and its -/// unique GUID; their spans. -pub(super) fn craft_stick( - path: &Path, - bytes: u64, - parts: &[(&'static str, u64, &'static str)], -) -> Result, String> { - let parts: Vec = - parts.iter().map(|&(name, len, unique)| (name, len, PLAIN_TYPE, unique, ALIGNED)).collect(); - let (mut device, spans) = table(path, bytes, &parts)?; - device.flush().map_err(|e| format!("flush the stick: {e}"))?; - Ok(spans) -} - -/// A FAT32 volume of `bytes` holding one file, so the check has a directory -/// entry and a cluster chain to judge and not only a boot sector. -pub(super) fn fat32(bytes: usize, label: &str) -> Result, String> { - let mut volume = vec![0u8; bytes]; - let mut name = [b' '; 11]; - name[..label.len()].copy_from_slice(label.as_bytes()); - fatfs::format_volume( - std::io::Cursor::new(&mut volume), - fatfs::FormatVolumeOptions::new().fat_type(fatfs::FatType::Fat32).volume_label(name), - ) - .map_err(|e| format!("format {label}: {e}"))?; - { - let fs = fatfs::FileSystem::new(std::io::Cursor::new(&mut volume), fatfs::FsOptions::new()) - .map_err(|e| format!("mount {label} on the host: {e}"))?; - let mut file = fs.root_dir().create_file("NEIGHBOUR.TXT").map_err(|e| format!("{e}"))?; - file.write_all(label.as_bytes()).map_err(|e| format!("{e}"))?; - file.flush().map_err(|e| format!("{e}"))?; - drop(file); - // Counted before the unmount so FSInfo carries a free count, as every - // writer that has finished with a volume leaves it. - fs.stats().map_err(|e| format!("count {label}'s free clusters: {e}"))?; - fs.unmount().map_err(|e| format!("unmount {label}: {e}"))?; - } - Ok(volume) -} diff --git a/tests/common/pkg.rs b/tests/common/pkg.rs deleted file mode 100644 index f49c9b2697a..00000000000 --- a/tests/common/pkg.rs +++ /dev/null @@ -1,414 +0,0 @@ -//! `pkg install ` from a local archive, and gbae's first run on ToyOS. -//! -//! The subject is the whole package path: the release's own `SHA256SUMS` -//! decides whether an archive is installed at all, `/apps/gbae` is a directory -//! and nothing else, and what a launch out of that directory *holds* comes from -//! the image's `[apps]` row rather than from the caller. `tests/pkgcase` is -//! what makes the last of those checkable — the estate that launches gbae has -//! no `compositor` connector of its own, so a window is proof the row was -//! built. -//! -//! Two checks, neither of them this file's: the release's `SHA256SUMS`, which -//! gbae's own release pipeline wrote and the guest verifies against; and the `tar` crate, -//! which decodes the same archive on the host so the bytes read back off the -//! guest's DATA volume are compared with a decoder `userland/pkg` shares no -//! code with. - -use std::io::{Read, Write}; -use std::path::Path; -use std::time::Duration; - -use super::qemu::{self, BootOptions, QemuInstance}; -use super::storage::{superblock_at, FileBlocks, IN_MEMORY}; - -/// gbae v0.2.0's release archive and the sums file published beside it, both -/// committed under `tests/fixtures` and named in `NOTICE`. -const ASSET: &str = "gbae-v0.2.0-toyos-x86_64.tar.gz"; -const SUMS: &str = "SHA256SUMS"; - -/// The release's own line for [`ASSET`], copied from its `SHA256SUMS` — the -/// digest `NOTICE` records for the committed file, held against it below. -const ASSET_SHA256: &str = "99fcd8a7263b5c25cd90cead1baaa7200ef272100fc2226e008a4e8205ba2916"; -const ASSET_BYTES: usize = 604_872; - -/// Where the archive and its two negative controls sit on ROOT. -const GOOD_DIR: &str = "share/pkg"; -const TAMPERED_DIR: &str = "share/pkg/tampered"; -const NOSUMS_DIR: &str = "share/pkg/nosums"; - -/// What a package's directory holds after this archive is installed. -const INSTALLED: [&str; 4] = ["gbae", "LICENSE", "README.md", "manifest.toml"]; - -pub fn pkg_install_gbae( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (archive, sums) = fixture()?; - let mut tampered = archive.clone(); - // One byte, in the middle of the compressed stream: the digest is what - // must refuse it, and a gzip that also fails to inflate would let the - // wrong refusal pass for the right one. - tampered[ASSET_BYTES / 2] ^= 0x01; - - let bins: Vec<(String, Vec)> = - rust_bins.iter().filter(|(name, _)| name == "pkg_launch_gbae").cloned().collect(); - if bins.is_empty() { - return Err(String::from("the pkg_launch_gbae client was not built")); - } - - // **Its own disk.** The lane's shared image keeps what the last boot left, - // and this test asserts what `/apps` does *not* hold as much as what it - // does. - let image = super::lane::dir().join("pkg-data.img"); - toyos_build::build::create_sparse(&image, qemu::NVME_SMALL); - - let config = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/pkgcase"); - let options = BootOptions { - profile: qemu::Profile::Metal, - nvme_image: Some(image), - extra_root_files: vec![ - (format!("{GOOD_DIR}/{ASSET}"), archive.clone()), - (format!("{GOOD_DIR}/{SUMS}"), sums.clone().into_bytes()), - (format!("{TAMPERED_DIR}/{ASSET}"), tampered), - (format!("{TAMPERED_DIR}/{SUMS}"), sums.into_bytes()), - (format!("{NOSUMS_DIR}/{ASSET}"), archive.clone()), - ], - ..Default::default() - }; - let mut qemu = QemuInstance::boot_with_options(&config, &[], &bins, options); - let boot = qemu.boot_log().to_string(); - if boot.contains(IN_MEMORY) { - return Err(format!( - "/apps and /home fell back to memory, so the readback below would judge no device:\n\ - {boot}" - )); - } - - let mut log = boot; - guest_probes(&mut qemu, &mut log)?; - - let image = qemu.nvme_image().to_path_buf(); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} on the way down\n{tail}")); - } - } - - readback(&image, &archive) -} - -/// Every claim the guest can answer for, in the order that makes each one -/// mean something. -fn guest_probes(qemu: &mut QemuInstance, log: &mut String) -> Result<(), String> { - let good = format!("/system/{GOOD_DIR}/{ASSET}"); - - // The two refusals first, and by name: a tampered archive beside a sums - // file that covers the real one, and a real archive with no sums file - // beside it at all. - refused( - qemu, - log, - &format!("pkg install /system/{TAMPERED_DIR}/{ASSET} --yes"), - &format!("pkg: {ASSET} hashes to"), - )?; - refused( - qemu, - log, - &format!("pkg install /system/{NOSUMS_DIR}/{ASSET} --yes"), - &format!("pkg: cannot read /system/{NOSUMS_DIR}/{SUMS}"), - )?; - - // Nothing is installed, so init has no row to build and the launch is - // refused rather than falling back to the caller's own namespace. - let at = log.len(); - refused(qemu, log, "test_rs_pkg_launch_gbae", "did not start")?; - const WHY: &str = "init: launcher: /apps/gbae/manifest.toml cannot be read"; - if !log[at.min(log.len())..].contains(WHY) { - return Err(format!("init never said {WHY:?}:\n{}", &log[at.min(log.len())..])); - } - - // Consent: `test-runner` closes a child's stdin, so this asks and is - // answered with nothing. - refused(qemu, log, &format!("pkg install {good}"), "pkg: not installing gbae")?; - - let installed = passed(qemu, log, &format!("pkg install {good} --yes"))?; - for said in [ - format!("pkg: verified {ASSET} against {SUMS} ({ASSET_SHA256})"), - String::from("pkg: installed gbae 0.2.0 at /apps/gbae, launching /apps/gbae/gbae"), - ] { - if !installed.contains(&said) { - return Err(format!("no {said:?} line:\n{installed}")); - } - } - - let listed = passed(qemu, log, "pkg list")?; - let row = format!("gbae 0.2.0 /apps/gbae/gbae {ASSET_SHA256}"); - if !listed.contains(&row) { - return Err(format!("`pkg list` does not carry {row:?}:\n{listed}")); - } - - // Removal is deleting the directory, judged by the name coming free: a - // second install of the same archive is refused while `/apps/gbae` exists. - passed(qemu, log, "pkg remove gbae")?; - let empty = passed(qemu, log, "pkg list")?; - if empty.contains("gbae 0.2.0") { - return Err(format!("`pkg remove` left gbae in the listing:\n{empty}")); - } - refused(qemu, log, "test_rs_pkg_launch_gbae", "did not start")?; - passed(qemu, log, &format!("pkg install {good} --yes"))?; - - // And the window. The estate that runs this holds no `compositor` - // connector, so a census of one is the `[apps]` row and can be nothing - // else. - let opened = log.len(); - passed(qemu, log, "test_rs_pkg_launch_gbae")?; - if !window_seen(qemu, log, opened) { - return Err(format!( - "gbae started and the compositor never counted a window:\n{}", - &log[opened.min(log.len())..] - )); - } - eprintln!(" [pkg] gbae opened a window through the /apps row alone"); - - // **Last, and the order is load-bearing**: a block this frees and the next - // file takes reads back off the device holding what it used to hold - // (`issues/filesystem/a-reallocated-extent-on-data-keeps-the-deleted-files-bytes.md`), - // so running it earlier would judge that record instead of this one. - let at = log.len(); - passed(qemu, log, "test_rs_pkg_launch_gbae symlink-row")?; - // The canonical spelling classifies as a package with no manifest; the four - // the kernel would normalize reach no classifier at all. - for said in [ - "init: launcher: /apps/toy/manifest.toml cannot be read", - "init: launcher: \"/apps/./toy/echo\" is not a canonical path", - "init: launcher: \"/apps//toy/echo\" is not a canonical path", - "init: launcher: \"apps/toy/echo\" is not a canonical path", - "init: launcher: \"/tmp/../apps/toy/echo\" is not a canonical path", - ] { - if !log[at.min(log.len())..].contains(said) { - return Err(format!( - "a symlink under /apps was not classified by /apps — init never said {said:?}:\n{}", - &log[at.min(log.len())..] - )); - } - } - - // And the directory it left comes off, because a name `install` refuses to - // write over is a name nothing else could free. - passed(qemu, log, "pkg remove toy")?; - refused(qemu, log, "pkg remove toy", "pkg: toy is not installed — there is no /apps/toy")?; - - // The gate's other half: the shell resolves what its user typed, so a - // dotted path still runs. - let ran = passed(qemu, log, "test_rs_pkg_launch_gbae relative-path")?; - for said in ["./home/toy/reltest/echo ran", "../home/toy/reltest/echo ran"] { - if !ran.contains(said) { - return Err(format!("no {said:?} line:\n{ran}")); - } - } - Ok(()) -} - -/// Wait for a compositor census carrying a window, past `from`. -/// -/// The census is printed every `STATS_INTERVAL`, so this is a wait on the -/// compositor's own clock rather than a span of host wall clock: the ceiling -/// is a liveness guard and the `windows=1` field is the verdict. -fn window_seen(qemu: &mut QemuInstance, log: &mut String, from: usize) -> bool { - let deadline = std::time::Instant::now() + Duration::from_secs(30); - while std::time::Instant::now() < deadline { - log.push_str(&qemu.drain_serial(Duration::from_millis(500))); - if log[from.min(log.len())..] - .lines() - .any(|l| l.contains("compositor: frames=") && l.contains("windows=1")) - { - return true; - } - } - false -} - -/// Run one guest command that must succeed, answering its output. -fn passed(qemu: &mut QemuInstance, log: &mut String, command: &str) -> Result { - let result = qemu.run_test(command, Duration::from_secs(120)); - let output = format!("{}{}", result.stdout, result.serial); - log.push_str(&result.before); - log.push_str(&output); - if result.exit_code != Some(0) { - return Err(format!("`{command}` answered {:?}:\n{output}", result.exit_code)); - } - Ok(output) -} - -/// Run one guest command that must fail, and say so by name. -fn refused( - qemu: &mut QemuInstance, - log: &mut String, - command: &str, - says: &str, -) -> Result<(), String> { - let result = qemu.run_test(command, Duration::from_secs(120)); - let output = format!("{}{}", result.stdout, result.serial); - log.push_str(&result.before); - log.push_str(&output); - if result.exit_code == Some(0) { - return Err(format!("`{command}` was not refused:\n{output}")); - } - if !output.contains(says) { - return Err(format!("`{command}` was refused and never said {says:?}:\n{output}")); - } - Ok(()) -} - -/// What the guest wrote, read off the DATA volume with the guest gone. -/// -/// The partition is found through the image's own table and the volume asked -/// which span it was formatted for, so nothing here takes an address from -/// anything the guest printed. Each file is compared with what the `tar` crate -/// makes of the same archive — a decoder `userland/pkg` shares no line with. -fn readback(image: &Path, archive: &[u8]) -> Result<(), String> { - let (at, bytes) = toyos_build::image::data_partition_of(image)?; - let blocks = bytes / 4096; - let sb = superblock_at(image, at / 4096)?; - if sb.block_count != blocks { - return Err(format!( - "the volume on the image was formatted for {} blocks and the DATA partition is \ - {blocks}", - sb.block_count - )); - } - - let io = FileBlocks::open(image)?; - let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(io) - .map_err(|e| format!("the NVMe image's DATA partition does not mount: {e:?}"))?; - - let mut total = 0usize; - let mut found: Vec = Vec::new(); - for (name, want) in third_party_entries(archive)? { - let on_disk = format!("apps/{name}"); - let got = fs - .read_file(&on_disk) - .map_err(|e| format!("reading {on_disk} off the DATA partition: {e:?}"))?; - if got != want { - // A length that differs and a byte that differs are two findings, - // and this message is the evidence either one rests on. - let Some(first) = got.iter().zip(&want).position(|(a, b)| a != b) else { - return Err(format!( - "{on_disk} is {} bytes on the device against the archive's {}, and agrees on \ - every byte they share", - got.len(), - want.len() - )); - }; - let head = |b: &[u8]| { - b.iter().skip(first).take(16).map(|x| format!("{x:02x}")).collect::>().join("") - }; - return Err(format!( - "{on_disk} is {} bytes on the device against the archive's {}, first differing \ - at {first}: device {} against archive {}", - got.len(), - want.len(), - head(&got), - head(&want), - )); - } - total += want.len(); - found.push(name.rsplit('/').next().unwrap_or(&name).to_string()); - } - - // The manifest is the installer's own and is in no archive, so it is - // checked against the digest the release published rather than against a - // file. - let manifest = fs - .read_file("apps/gbae/manifest.toml") - .map_err(|e| format!("reading apps/gbae/manifest.toml off the DATA partition: {e:?}"))?; - let text = String::from_utf8(manifest).map_err(|e| format!("the manifest is not UTF-8: {e}"))?; - let want = format!( - "name = \"gbae\"\nversion = \"0.2.0\"\ndigest = \"{ASSET_SHA256}\"\n\ - program = \"/apps/gbae/gbae\"\n" - ); - if text != want { - return Err(format!("the manifest on the device is {text:?}, not {want:?}")); - } - found.push(String::from("manifest.toml")); - found.sort(); - let mut expected: Vec<&str> = INSTALLED.to_vec(); - expected.sort_unstable(); - if found != expected { - return Err(format!("apps/gbae carries {found:?} and a package of this archive is \ - {expected:?}")); - } - - eprintln!( - " [pkg] {total} bytes of {ASSET} byte-identical under apps/gbae on the DATA partition \ - at byte {at}, against the `tar` crate's own decoding" - ); - Ok(()) -} - -/// The archive's files, decoded by the `tar` crate rather than by -/// `userland/pkg`. -fn third_party_entries(archive: &[u8]) -> Result)>, String> { - let gz = flate2::read::GzDecoder::new(archive); - let mut tar = tar::Archive::new(gz); - let mut out = Vec::new(); - for entry in tar.entries().map_err(|e| format!("tar: {e}"))? { - let mut entry = entry.map_err(|e| format!("tar entry: {e}"))?; - if !entry.header().entry_type().is_file() { - continue; - } - let path = entry - .path() - .map_err(|e| format!("tar path: {e}"))? - .to_string_lossy() - .into_owned(); - let mut data = Vec::new(); - entry.read_to_end(&mut data).map_err(|e| format!("tar read: {e}"))?; - out.push((path, data)); - } - if out.len() != 3 { - return Err(format!("the archive holds {} files, and gbae v0.2.0 has 3", out.len())); - } - Ok(out) -} - -/// The release asset and its sums file, read out of the tree. -/// -/// **Committed, and fetched by nothing.** The digest is held again here, so a -/// fixture edited in place is a refusal rather than a different subject. -fn fixture() -> Result<(Vec, String), String> { - let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures"); - let at = dir.join(ASSET); - let archive = std::fs::read(&at).map_err(|e| format!("read {}: {e}", at.display()))?; - if archive.len() != ASSET_BYTES || digest(&archive) != ASSET_SHA256 { - return Err(format!( - "{} is {} bytes hashing to {}, and NOTICE records {ASSET_BYTES} bytes hashing to \ - {ASSET_SHA256}", - at.display(), - archive.len(), - digest(&archive) - )); - } - let sums_at = dir.join(SUMS); - let sums = - std::fs::read_to_string(&sums_at).map_err(|e| format!("read {}: {e}", sums_at.display()))?; - // The release's own statement has to cover the archive beside it, or the - // guest below verifies against a line nobody checked. - if !sums.contains(&format!("{ASSET_SHA256} {ASSET}")) { - return Err(format!( - "{} carries no `{ASSET_SHA256} {ASSET}` line:\n{sums}", - sums_at.display() - )); - } - Ok((archive, sums)) -} - -fn digest(bytes: &[u8]) -> String { - use sha2::{Digest, Sha256}; - Sha256::digest(bytes).iter().map(|b| format!("{b:02x}")).collect() -} diff --git a/tests/common/power.rs b/tests/common/power.rs index 09677e61d6c..3dcf5a19632 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -1,247 +1,8 @@ -//! The two ways the machine stops, told apart by QEMU rather than by the guest: -//! a reset, a power-off and a triple fault all end a `-no-reboot` QEMU with -//! status 0, so what is asserted is the cause its `SHUTDOWN` event names, and a -//! reboot implemented as a power-off reds on `guest-shutdown`. -//! -//! Two names here judge the boot *after* a reset instead, and pay for it: -//! `blackbox_panic_chain` and `blackbox_done_chain` set -//! `BootOptions::takes_the_reset`, which gives up the stop reason every other -//! test in this file judges by, because a page crossing a reset cannot be -//! observed from a QEMU that exits on one. - -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::time::Duration; - use toyos_blackbox::{PHYS, State}; use toyos_build::bootlog::{self, REBOOTING}; -use toyos_xhci::bot::Phase; -use super::qemu::{self, BootOptions, QemuInstance}; use super::serial; -const WAIT: Duration = Duration::from_secs(20); - -/// What a guest that never stopped means where something asked it to. -const ASKED_AND_STAYED_UP: &str = - "QEMU never reported stopping: the guest asked for a reboot and stayed up"; - -/// QEMU calls a reset-register write `guest-reset` and ACPI S5 `guest-shutdown`, -/// which the console cannot tell apart. `never` is what a guest that did not -/// stop at all means to the caller, which is not the same thing twice. -fn returned_to_firmware(reason: Option, never: &str, tail: &str) -> Result<(), String> { - match reason.as_deref() { - Some("guest-reset") => Ok(()), - Some(seen) => Err(format!( - "QEMU stopped this guest for {seen:?}, not a guest reset: the machine was not \ - returned to firmware\n{tail}" - )), - None => Err(format!("{never}\n{tail}")), - } -} - -/// The machine returns to firmware when a process holding `POWER` asks it to. -pub fn machine_reboot( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { qmp: true, ..Default::default() }; - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - - let boot = serial::Serial::boot(&qemu); - boot.must_be_clean()?; - // A decode this kernel got wrong, never one it bypassed: a kernel writing - // 0xcf9 without reading the FADT satisfies this and the stop reason both. - boot.must_say("ACPI: reset register SystemIO 0xcf9 <- 0x0f")?; - - let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(WAIT)); - - writeln!(qemu.stdin_mut(), "run reboot").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let reason = stop.reason(); - // Ends when QEMU exits and the reader disconnects, so a guest that came back to firmware pays none of this. - let tail = qemu.drain_serial(WAIT); - - let drain = serial::Serial::named("reboot drain", tail.as_str()); - drain.must_be_clean()?; - drain.must_say(REBOOTING)?; - returned_to_firmware(reason, ASKED_AND_STAYED_UP, &tail)?; - - eprintln!(" [power] QEMU stopped the guest for guest-reset"); - Ok(()) -} - -/// Every [`stopped_boot`] arms it, for the reason `usb_reset_hands_devices_back`'s -/// deadline arm does: QEMU has no window between the boot's last word and the -/// reset and hardware does, so without it the last-word judge is green whether -/// or not anything was stopped. -const LATE_WORD: &str = "quiesce-late-word"; - -/// One boot of `config` whose one job reboots it, with `params` armed, judged -/// on what every boot that ends through the stop owes: a clean console, a -/// return to firmware, nothing under the boot's last word, and a stop that -/// stopped the machine. Answers the whole console and the stop's record. -fn stopped_boot( - config: &str, - job: &str, - params: &'static [&'static str], - rust_bins: &[(String, Vec)], -) -> Result<(String, toyos_quiesce::Record), String> { - if !params.contains(&LATE_WORD) { - return Err(format!( - "a stopped boot armed with {params:?} and not {LATE_WORD:?} has no window under its \ - last word, so the judge of that word would be green over any machine" - )); - } - let config = super::compile::repo_root().join(config); - let case = config.parent().expect("system.toml has a directory"); - let bins: Vec<(String, Vec)> = - rust_bins.iter().filter(|(name, _)| name == job).cloned().collect(); - if bins.len() != 1 { - return Err(format!("the suite built {} copies of {job:?}", bins.len())); - } - let mut qemu = QemuInstance::boot_with_options( - case, - &[], - &bins, - BootOptions { - profile: qemu::Profile::Metal, - qmp: true, - kernel_params: params, - ..Default::default() - }, - ); - serial::Serial::boot(&qemu).must_be_clean()?; - let booted = qemu.boot_log().to_string(); - let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(WAIT)); - let reason = stop.reason(); - let tail = qemu.drain_serial(WAIT); - let whole = format!("{booted}{tail}"); - serial::Serial::named("stopped-boot drain", tail.as_str()).must_be_clean()?; - returned_to_firmware(reason, ASKED_AND_STAYED_UP, &tail)?; - - let lines: Vec<&str> = whole.lines().collect(); - // The word's presence is asserted before what follows it: a boot that never - // wrote it has nothing after it either, and would pass vacuously. - let last_word = lines - .iter() - .position(|line| line.contains(REBOOTING)) - .ok_or_else(|| format!("this boot never wrote {REBOOTING:?}\n{whole}"))?; - // **The defect itself, and the rest are the mechanism**: a record a thread - // put under the boot's own last word. - let after: Vec<&str> = - lines[last_word + 1..].iter().copied().filter(|line| !line.trim().is_empty()).collect(); - if !after.is_empty() { - return Err(format!( - "{} line(s) reached the console after the boot's last word:\n {}", - after.len(), - after.join("\n "), - )); - } - let said = lines - .iter() - .find(|line| line.contains(toyos_quiesce::STOPPED)) - .ok_or_else(|| format!("the kernel wrote no stop record\n{whole}"))?; - // Whether it stopped every thread is not judged here: the stop gives up at - // a budget of the kernel's own clock, so a starved guest reads as the - // defect. Every metal boot is held to it (`metal::Readback::stop_completed`). - let record = toyos_quiesce::Record::parse(said) - .ok_or_else(|| format!("the kernel's stop record did not read back as one:\n {said}"))?; - Ok((whole, record)) -} - -/// **The machine has one shutdown, and the second caller is refused while the -/// first holds it.** init makes the first call; `quiesce-last-park` holds it -/// after it has claimed the stop and before it stops anything, while every -/// other thread still runs. The job reads the kernel's word that the stop -/// waits there and makes the second call — `SYS_SHUTDOWN` against the first's -/// `SYS_REBOOT`, so the claim is judged on both syscalls — and starts the -/// thread the stop waits for only once refused by name. -pub fn quiesce_refuses_a_second_shutdown( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const WAITS: &str = "quiesce-last-park: the stop waits for"; - const SECOND_CALLER: &str = "power: this machine is already stopping"; - let held = format!( - "quiesce-last-park: {} is held until the stop waits on it alone", - toyos_quiesce::LAST_THREAD - ); - let (whole, _record) = stopped_boot( - "tests/quiescetwicecase/system.toml", - "quiesce_twice", - &["quiesce-last-park", LATE_WORD], - rust_bins, - )?; - let lines: Vec<&str> = whole.lines().collect(); - let at = |needle: &str| -> Vec { - lines.iter().enumerate().filter(|(_, l)| l.contains(needle)).map(|(i, _)| i).collect() - }; - - // **The harm, first**: a second caller let in runs a second stop over the - // first, and either way the stop's record is written twice. - let records: Vec = lines - .iter() - .enumerate() - .filter(|(_, l)| toyos_quiesce::Record::parse(l).is_some()) - .map(|(i, _)| i) - .collect(); - if records.len() != 1 { - return Err(format!( - "this boot ran {} shutdowns, not one: the second caller was let in\n{whole}", - records.len() - )); - } - let once = |needle: &str| -> Result { - match at(needle).as_slice() { - [line] => Ok(*line), - other => Err(format!("{needle:?} is on {} console line(s), not one\n{whole}", other.len())), - } - }; - // **The refusal, by name, inside the window**: after the first call said - // it waits, before the thread it waits for was held — which the job starts - // only on reading `AlreadyExists`, so the held line is the refusal having - // reached Ring 3 as that word. - let (waits, second, held, recorded) = (once(WAITS)?, once(SECOND_CALLER)?, once(&held)?, records[0]); - if !(waits < second && second < held && held < recorded) { - return Err(format!( - "the first call's wait, the second call's refusal, the held thread and the stop's \ - record are at console lines {waits}, {second}, {held} and {recorded}: the refusal was \ - not made in the window the first call held\n{whole}" - )); - } - eprintln!( - " [power] the second caller was refused while the first held the stop:\n {}\n {}", - lines[waits], lines[second], - ); - Ok(()) -} - -/// The chipset resets a machine whose kernel stops feeding its watchdog. -/// Starvation begins well after boot, so what is waited for is the reset, with -/// this guest's own scaled ceiling behind it, never an arm-to-ready race. -pub fn watchdog_resets( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, starved()); - let boot = serial::Serial::boot(&qemu); - boot.must_be_clean()?; - boot.must_say(ARMED)?; - - let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(WAIT)); - let reason = stop.reason(); - let tail = qemu.drain_serial(WAIT); - - returned_to_firmware(reason, "the chipset never reset a guest that stopped feeding it", &tail)?; - - eprintln!(" [power] the chipset reset a guest that stopped feeding it"); - Ok(()) -} - /// The kernel's read-back above its own arm, in /// `kernel/src/arch/x86_64/watchdog.rs`: whole clauses, one per branch. const ARMED_ON_ARRIVAL: &str = "so the bootloader had already armed the timer"; @@ -286,44 +47,6 @@ fn decimal_field(line: &str, label: &str) -> Result { rest[..end].parse().map_err(|e| format!("{label:?} in {line:?}: {e}")) } -/// The loader arms the chipset's watchdog before it jumps, so the handoff is -/// inside the bound. -/// -/// What the kernel's read-back must report is the register value the loader -/// wrote, never merely a running timer: `TCO_TMR_HLT` is clear out of reset on -/// q35. -pub fn loader_watchdog_arms( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let armed = BootOptions { - profile: qemu::Profile::Metal, - kernel_params: &[toyos_tco::PARAM], - ..Default::default() - }; - let qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, armed); - let boot = serial::Serial::boot(&qemu); - boot.must_be_clean()?; - // One console carries both writers here; on the T14 the loader's lines are - // in `loader.log` and the kernel's are records, so the predicate takes them - // apart even where they arrive together. - watchdog_armed(&boot, &boot)?; - drop(qemu); - - let idle = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { profile: qemu::Profile::Metal, ..Default::default() }, - ); - let quiet = serial::Serial::boot(&idle); - quiet.must_be_clean()?; - watchdog_quiet(&quiet, &quiet)?; - drop(idle); - Ok(()) -} - /// The armed boot's half: the loader wrote the register block and the kernel /// found the timer already running. /// @@ -398,249 +121,11 @@ pub fn says_nothing_of(channel: &serial::Serial, needle: &str) -> Result<(), Str } } -fn starved() -> BootOptions { - BootOptions { - profile: qemu::Profile::Metal, - qmp: true, - kernel_params: &["watchdog", "tco-fast", "tco-starve"], - ..Default::default() - } -} - -/// The line `arm` logs on q35 at the fast bound, demanded before anything is judged. -/// -/// The tail is what makes it the kernel's: on every guest that passes the -/// parameter the loader prints the same port and a `TCO_TMR=` of its own, which -/// the head of this line cannot be told from. -const ARMED: &str = - "watchdog: 8086:2918 TCO at 0x660 TCO_TMR=2 — this machine resets if no scheduler pass runs \ - for 2400ms"; - -/// The kernel's fast panic bound in seconds — `kernel/src/panic_reboot.rs`'s -/// `FAST_BOUND`, which `panic-reboot-fast` swaps in for the shipped minute. -/// -/// A kernel constant does not cross into the harness, so it is written here and -/// then *read back*: [`panic_armed`] is the whole arm line including this -/// number, demanded before anything is judged. A bound that -/// moved in the kernel and not here reds on that line rather than on a stop -/// reason nobody could attribute. -const PANIC_FAST_SECS: u64 = 5; - -/// The panic path's arm line, which is also this boot's ready marker: the guest -/// has stopped scheduling by the time it is printed, and it is the instant the -/// bound starts running. -const PANIC_ARMED_HEAD: &str = "panic: rebooting in"; - -fn panic_armed() -> String { - format!("{PANIC_ARMED_HEAD} {PANIC_FAST_SECS} s unless a key is pressed, timed by ") -} - -/// A guest whose kernel panicked and armed the bound. `Profile::Metal` for the -/// same reason `screen_pager_keys` needs it: QEMU routes injected keys to one -/// handler per device class, and this is the only GOP profile with an i8042 and -/// no `usb-kbd` to send them to instead. -fn panicked() -> BootOptions { - BootOptions { - profile: qemu::Profile::Metal, - qmp: true, - kernel_params: &["test-late-panic", "panic-reboot-fast"], - ready_marker: PANIC_ARMED_HEAD, - ..Default::default() - } -} - -/// What a panicked guest that never stopped means where the bound should have -/// ended it. -const PANICKED_AND_STAYED_UP: &str = - "QEMU never reported stopping: nobody pressed a key and the panicked guest held its panel \ - anyway"; - -/// A panicked kernel nobody is at returns the machine to firmware itself. -/// -/// The verdict is QEMU's stop reason and the panic path's own line saying the -/// bound ran out; the budget below is the ceiling on that reset, never a bound -/// it is held to. -pub fn panic_reboots( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, panicked()); - let boot = serial::Serial::boot(&qemu); - // Not `must_be_clean`: this boot panics on purpose, and the arm line is - // what says the panic path — not something else — is holding the machine. - let line = boot.must_say(&panic_armed())?.to_string(); - let watch = watch_the_bound(&qemu); - let (budget, _) = resets_inside_the_bound(&mut qemu, watch, PANICKED_AND_STAYED_UP)?; - eprintln!(" [power] the panicked guest reset itself inside {budget:?} of: {}", line.trim()); - Ok(()) -} - -/// QEMU's `SHUTDOWN` event, subscribed to for the fast bound plus what a reset -/// costs. Opened before whatever starts the bound: QMP delivers no event -/// emitted before its client connected. -fn watch_the_bound(qemu: &QemuInstance) -> (qemu::QmpShutdown, Duration) { - let budget = qemu.budget(Duration::from_secs(PANIC_FAST_SECS) + RESET_ALLOWANCE); - (qemu::QmpShutdown::open(qemu.qmp_socket(), budget), budget) -} - -/// QEMU's own `guest-reset` on `watch`, and the serial the guest wrote after -/// its boot log; `never` is what a guest that did not stop means to the caller. -fn resets_inside_the_bound( - qemu: &mut QemuInstance, - (mut stop, budget): (qemu::QmpShutdown, Duration), - never: &str, -) -> Result<(Duration, String), String> { - let reason = stop.reason(); - // A guest that came back to firmware pays none of this: `-no-reboot` exits and the reader disconnects. - let tail = qemu.drain_serial(WAIT); - returned_to_firmware(reason, never, &tail)?; - - let drain = serial::Serial::named("panic reboot drain", tail.as_str()); - drain.must_say(qemu::PANIC_REBOOTING)?; - Ok((budget, tail)) -} - -/// `kernel_params` kills `klogd` on its first instruction, and the machine is -/// what dies. The verdict is QEMU's reset, never the guest's word. -pub fn klogd_death_resets( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - kernel_params: &'static [&'static str], - said: &[&str], -) -> Result<(), String> { - // `panicked()`'s 16550-only guest: the reset's own line goes to the UART raw. - let options = - BootOptions { kernel_params, ready_marker: "kthread: klogd pid=", ..panicked() }; - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let dead = serial::Serial::boot(&qemu); - let watch = watch_the_bound(&qemu); - let never = "QEMU never reported stopping: klogd died and the machine carried on without it"; - died_and_reset(&mut qemu, watch, dead, never, said).map(drop) -} - -/// `SYS_DEBUG` `action` ends the kernel inside its caller's syscall, and the -/// machine is what dies, never only the caller. The verdict is QEMU's reset, as -/// [`klogd_death_resets`]'s is; what the guest said is returned for the caller -/// to read further. -pub fn syscall_death_resets( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - action: u64, - said: &[&str], -) -> Result { - let options = BootOptions { - kernel_params: &["panic-reboot-fast"], - ready_marker: qemu::DEFAULT_READY, - ..panicked() - }; - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let dead = serial::Serial::boot(&qemu); - let watch = watch_the_bound(&qemu); - writeln!(qemu.stdin_mut(), "run test_rs_test_panic_child {action}") - .expect("write to QEMU stdin"); - qemu.flush_stdin(); - let never = "QEMU never reported stopping: the kernel died inside a syscall and the machine \ - carried on without its caller"; - died_and_reset(&mut qemu, watch, dead, never, said) -} - -/// QEMU's reset inside the bound, then what the dead guest said, `said` and the -/// arm line among it. -fn died_and_reset( - qemu: &mut QemuInstance, - watch: (qemu::QmpShutdown, Duration), - mut dead: serial::Serial, - never: &str, - said: &[&str], -) -> Result { - let (budget, tail) = resets_inside_the_bound(qemu, watch, never)?; - dead.push(&tail); - for want in said { - dead.must_say(want)?; - } - dead.must_say(&panic_armed())?; - eprintln!(" [power] {said:?}: QEMU reset the machine inside {budget:?}"); - Ok(dead.text().to_string()) -} - -/// A panic inside `percpu::init_bsp`, one statement after it loads the IDT, -/// finds a reset register already decoded. -/// -/// That is the window the owner's T14 stops in and the earliest point a panic is -/// reportable at all. The FADT's reset register used to be decoded at -/// `acpi::init_power`, hundreds of statements later, so a panic here said it had -/// "decoded no reset register to hand the machine back to firmware with" and -/// held the panel for a hand. -/// -/// **The reset itself is not asserted here, and cannot be on this guest**: the -/// bound is carried in TSC cycles, and before `clock::init` those come from -/// CPUID leaves 15H/16H, which QEMU's model answers with zeros. So this guest -/// reaches the *other* held branch — no clock — and the machine the bound was -/// written for is the judge of the reset. What is asserted is the half QEMU can -/// see, which is the half that was broken: the register is decoded before the -/// panic, and the panic does not name it as missing. -/// `panic_reboots` covers the reset once the calibrated clock exists. -pub fn panic_before_peripherals_reboots( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { - kernel_params: &["test-panic-after-idt", "panic-reboot-fast"], - ready_marker: PANIC_HELD_HEAD, - ..panicked() - }; - let qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let boot = serial::Serial::boot(&qemu); - - // Ordering is the whole assertion: this line is what `init_power` used to - // print long after the panic below. - let decoded = boot.must_say("ACPI: reset register SystemIO")?.to_string(); - boot.must_say("EARLY PANIC: panicked at")?; - let held = boot.must_say(PANIC_HELD_HEAD)?.to_string(); - // The one thing this branch removed. A guest reaching the other held branch - // for the other reason must not be read as this one passing. - boot.must_not_say("decoded no reset register")?; - if !held.contains("states no counter frequency") { - return Err(format!( - "the panel held for a reason this guest was not expected to reach\n{held}" - )); - } - - eprintln!(" [power] a panic inside init_bsp found {}", decoded.trim()); - Ok(()) -} - -/// The head of [`panic_reboot::arm`]'s other line — the machine holds. Kept -/// apart from [`PANIC_ARMED_HEAD`] there and here for the same reason. -/// -/// [`panic_reboot::arm`]: kernel/src/panic_reboot.rs -const PANIC_HELD_HEAD: &str = "panic: holding this panel"; - -/// The ceiling on the reset past the bound: the flush the reset path makes -/// before it writes the register, and the host seeing QEMU's event. Scaled by -/// [`QemuInstance::budget`] at the call site. -const RESET_ALLOWANCE: Duration = Duration::from_secs(20); - /// A line of the first boot's own report, which has to come back out of DRAM on /// the boot after it: the panic's message, so what is recovered is the crash /// and not merely a page that checksummed. const BLACKBOX_WITNESS: &str = "test-late-panic: on-screen console check"; -/// The earliest panic this tree can stage, inside `percpu::init_bsp` one -/// statement after the IDT is loaded — which is before `params::init`, and so -/// before everything the kernel used to learn the page's address from. -/// -/// **It cannot drive the chain and that is not a choice**: the reboot bound is -/// carried in TSC cycles, and before `clock::init` those come off CPUID leaves -/// this guest's CPU answers with zeros, so the panic path holds the panel rather -/// than resetting (`issues/panic-path/the-panic-bounds-cpuid-clock-runs-on-no-guest-this-tree-boots.md`). -/// The seal is read off the page itself instead, which needs no reset at all. -const EARLY_WITNESS: &str = "test-panic-after-idt: the IDT is loaded and nothing else is up"; - /// The first record `serial::init` writes, which is the first thing the kernel /// does after taking the page. const SERIAL_IS_UP: &str = "serial: 16550 loopback read"; @@ -660,676 +145,6 @@ fn armed_and_nothing_else() -> String { format!("{} the page still reads {}", bootlog::PREVIOUS_PANIC, State::Armed.named()) } -/// The two-boot shape both chain judges use: a guest that takes its own reset, -/// so the loader pass after it is observable. -fn chained(params: &'static [&'static str]) -> BootOptions { - BootOptions { - profile: qemu::Profile::Metal, - qmp: true, - kernel_params: params, - takes_the_reset: true, - ready_marker: bootlog::LOADER_LAST_LINE, - ..Default::default() - } -} - -/// An image the host keeps rather than a throwaway one, and where its log -/// volume is: what the guest writes there is what the test reads once the -/// guest is gone. -struct Kept { - image: PathBuf, - start: usize, - len: usize, -} - -impl Kept { - /// Build `case` into such an image. `staged` goes onto its log volume - /// before the boot, for a test whose subject is what the loader does with - /// a file that was already there. - fn build( - case: &Path, - params: &[&str], - name: &str, - staged: &[(String, Vec)], - ) -> Result { - let image = super::lane::dir().join(name); - let mut bytes = qemu::build_boot_image(case, &[], &[], params); - std::fs::write(&image, &bytes).map_err(|e| format!("write the boot image: {e}"))?; - let (start, len) = super::volumes::log_extent(&bytes, &image)?; - if !staged.is_empty() { - super::volumes::stage_files(&mut bytes[start..start + len], staged)?; - std::fs::write(&image, &bytes).map_err(|e| format!("write the boot image: {e}"))?; - } - Ok(Self { image, start, len }) - } - - /// What `boot_with_options` boots instead of building one of its own. - fn boots(&self) -> Option { - Some(qemu::Staged::Written(self.image.clone())) - } - - /// The loader's own file, as the guest left it. - fn loader_log(&self) -> Result { - Ok(super::volumes::loader_log_lines(&self.image, self.start, self.len)?.join("\n")) - } - - /// **Hundreds of megabytes each** (`tests/common/qemu.rs`), so a kept - /// image outlives its test no longer than it has to. - fn remove(self) { - let _ = std::fs::remove_file(&self.image); - } -} - -/// [`chained`] on a [`Kept`] image. -/// -/// **A wedged boot's own records reach no console.** Nothing drains the ring -/// once every CPU has stopped taking scheduler passes, so the only copy of them -/// that crosses the reset is the one the black box carried — and the loader -/// files that tail in `loader.log` rather than scrolling it through the -/// firmware's console a frame at a time. So a judge that wants those records -/// reads the file, which is the channel the T14's judge reads too. -fn chained_on_a_kept_image( - case: &Path, - params: &'static [&'static str], - name: &str, -) -> Result<(BootOptions, Kept), String> { - let kept = Kept::build(case, params, name, &[])?; - let options = BootOptions { boot_image: kept.boots(), ..chained(params) }; - Ok((options, kept)) -} - -/// Resets a chain leaves behind: the kernel's own, and the pass that read the -/// page ending itself rather than returning to the boot manager. -const CHAIN_RESETS: usize = 2; - -/// Both chain judges' second half: the pass after the reset said its piece and -/// then reset the machine itself. -/// -/// **The reset is not decoration.** A UEFI application that returns leaves its -/// `SIGNAL_EXIT_BOOT_SERVICES` callback registered and is then unloaded, and the -/// next operating system's own `ExitBootServices` calls into that freed image — -/// measured on the owner's T14 as Ubuntu freezing in its EFI stub. Asked of QEMU -/// and not of the guest, because a guest that says it is about to reset is not a -/// guest that did. -fn ended_in_a_reset(resets: &mut qemu::QmpResets) -> Result<(), String> { - let seen = resets.seen(CHAIN_RESETS); - if seen < CHAIN_RESETS { - return Err(format!( - "QEMU reported {seen} guest reset(s) and this chain is {CHAIN_RESETS}: the pass \ - that read the page returned to the boot manager instead of resetting, which leaves \ - this image's exit-boot-services callback registered for the next operating system \ - to call into" - )); - } - Ok(()) -} - -/// Every line the guest said after its first boot handed off, up to the second -/// pass's own last line. -fn after_the_reset(qemu: &mut QemuInstance, until: &str) -> serial::Serial { - let until = until.to_string(); - let tail = qemu.drain_until(CHAIN_WAIT, move |line| line.contains(&until)); - serial::Serial::named("boot after the reset", tail.as_str()) -} - -/// What the boot after a reset has to arrive inside: the bound the first boot -/// counts down, plus firmware and a loader. Scaled by `drain_until`, and the -/// predicate is what ends the drain. -const CHAIN_WAIT: Duration = Duration::from_secs(PANIC_FAST_SECS + 60); - -/// The chain closes on a panic: the kernel seals what the panel rendered, the -/// machine resets itself, and the boot after it is this loader again — which -/// reads the page, writes the report, and hands the machine back to firmware -/// rather than starting the same loop over. -/// -/// The A/B is this guest's own two passes. QEMU zeroes a machine's RAM, so the -/// first pass must find no page at all, which is what stops a green run meaning -/// "the loader says that about every boot". -pub fn blackbox_panic_chain( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let params: &[&str] = &["test-late-panic", "panic-reboot-fast"]; - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - chained(params), - ); - // The capture ends at the loader's handoff line, so what it can carry is - // the loader's own account; that the *kernel* took the page is - // `blackbox_unclaimed_page`'s to say and is not restated here. - let first = serial::Serial::boot(&qemu); - // Opened before either reset: events queue on the socket from here. - let mut resets = qemu::QmpResets::open(qemu.qmp_socket(), qemu.budget(CHAIN_WAIT)); - first.must_say(&armed_line())?; - // Nothing was harvested on a machine whose RAM QEMU zeroed, so the pass - // below is reading this boot's page and not a claim about every boot. - if let Some(line) = first.text().lines().find(|l| l.contains(bootlog::PREVIOUS_PANIC)) { - return Err(format!( - "the first pass of a machine with zeroed RAM reported a previous boot ({line:?}), \ - so the pass after the reset would say nothing\n{}", - first.text() - )); - } - - let second = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - second.must_say(bootlog::PREVIOUS_PANIC)?; - // **After the harvest line, and that is the whole of the assertion.** This - // capture begins at the first boot's handoff, so it carries that boot's own - // panic on the console too — and a whole-capture scan for the witness was - // satisfied by it, which let a kernel that sealed nothing pass. Only the - // loader's `| ` lines come after the harvest line. - second.must_say_after(bootlog::PREVIOUS_PANIC, BLACKBOX_WITNESS)?; - // The page read PANIC and not the state the loader itself put there, which - // is what tells a report that crossed the reset from a kernel that vanished. - second.must_not_say(&armed_and_nothing_else())?; - second.must_say(bootlog::CHAIN_ENDS_LINE)?; - // The chain ends rather than going round: a pass that booted a kernel would - // have said so, and this one must not have. - second.must_not_say(bootlog::LOADER_LAST_LINE)?; - ended_in_a_reset(&mut resets)?; - - // The judge of the clear: a page still holding the record is reported again - // by the pass after this one, and that machine reports one crash for ever. - let third = after_the_reset(&mut qemu, bootlog::LOADER_LAST_LINE); - third.must_say(bootlog::LOADER_LAST_LINE)?; - if let Some(line) = third.text().lines().find(|l| l.contains(bootlog::PREVIOUS_PANIC)) { - return Err(format!( - "the pass after the report found a record and reported it again ({line:?}), so the \ - clear did not reach the page and this machine reports one crash for ever\n{}", - third.text() - )); - } - drop(qemu); - - eprintln!( - " [power] the panic crossed the reset, the pass that read it reset in turn, and the \ - pass after that booted a kernel" - ); - Ok(()) -} - -/// The other way a kernel ends, and the control on the name above: a boot that -/// hands the machine back on purpose seals DONE, and the loader pass after it -/// reports a deliberate stop rather than a death — then ends the chain too, so -/// the machine goes back to the firmware's own boot order. -pub fn blackbox_done_chain( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = super::compile::repo_root().join("tests/jobcase/system.toml"); - let case = config.parent().expect("system.toml has a directory"); - let mut qemu = QemuInstance::boot_with_options(case, &[], &[], chained(&[])); - let first = serial::Serial::boot(&qemu); - let mut resets = qemu::QmpResets::open(qemu.qmp_socket(), qemu.budget(CHAIN_WAIT)); - first.must_say(&armed_line())?; - - let second = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - second.must_say(bootlog::HANDED_BACK)?; - done_chain(&second)?; - ended_in_a_reset(&mut resets)?; - drop(qemu); - Ok(()) -} - -/// The boot deadline ends a machine nothing else in this tree can, and the next -/// pass says what it ended. -/// -/// **The negative control is most of the test.** `wedge-before-reset` stops -/// every CPU taking scheduler passes at the shutdown syscall — after the job -/// list has run, with preemption disabled and `IF` set. No watchdog counts that -/// state: the chipset's is fed by any CPU and is disarmed one statement later, -/// the runner's own bound reboots *through* this same syscall, and no panic -/// path is reached because nothing failed. Without `boot-deadline` that boot -/// runs until somebody presses the power button, which is the two T14 hangs -/// this exists for. -/// -/// **The mutation is the whole mechanism, not the arm.** Dropping the -/// `boot-deadline=` parameter leaves every line of the implementation standing -/// and measures only that an unarmed deadline does not fire. What this is a -/// control for is the mechanism reverted onto the base the green arm was -/// measured on: `start` arming no deadline, the `call` gone from the Ring 0 -/// timer entry, and the idle-exit re-arm in `hw::idle_wait` gone with it — so -/// nothing polls, nothing seals and nothing writes the reset register. -pub fn boot_deadline_ends_a_wedge( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = super::compile::repo_root().join("tests/jobcase/system.toml"); - let case = config.parent().expect("system.toml has a directory"); - let (options, kept) = chained_on_a_kept_image( - case, - &["wedge-before-reset", WEDGE_DEADLINE], - "deadlinewedge-boot.img", - )?; - let mut qemu = QemuInstance::boot_with_options(case, &[], &[], options); - let first = serial::Serial::boot(&qemu); - let mut resets = qemu::QmpResets::open(qemu.qmp_socket(), qemu.budget(CHAIN_WAIT)); - first.must_say(&armed_line())?; - - // One capture from the first boot's handoff to the pass that reports it: - // everything this machine drained, and the head of the record read back - // off the page under it. - let second = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - if !second.text().contains(bootlog::CHAIN_ENDS_LINE) { - // One monitor per socket: the reset watcher goes before the question. - drop(resets); - return Err(silent_guest(&qemu, second.text())); - } - // Half of the control: the machine did *not* reach the reset it was one - // statement away from, and `Rebooting.` is quiesce's own last word. - second.must_not_say(bootlog::REBOOTING)?; - second.must_say(bootlog::PREVIOUS_PANIC)?; - // **After the harvest line**, so this is the page and not the wire. - second.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::DEADLINE_EXPIRED)?; - // The head of the record, which `blackbox::tail` owes the console whole: - // why the boot ended, above, and what its panel cost. - second.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::PANEL_CENSUS)?; - // The same seal writes the recovery section, and this boot's transport - // never broke. - second.must_say_after(bootlog::PREVIOUS_PANIC, toyos_blackbox::RECOVERY_NONE)?; - second.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::TAIL_IN_THE_FILE)?; - second.must_not_say(&armed_and_nothing_else())?; - second.must_say(bootlog::CHAIN_ENDS_LINE)?; - second.must_not_say(bootlog::LOADER_LAST_LINE)?; - ended_in_a_reset(&mut resets)?; - drop(qemu); - - // **The other half of the control, off the only channel that carries it.** - // Both records are written after the last drain this machine ever ran, so - // they exist nowhere but the tail the black box carried across the reset: - // the machine reached the wedge, and the CPU that asked for it took - // interrupts again rather than arriving deaf, which is what makes this a - // wedge and not a hard lockup. - let text = kept.loader_log()?; - let filed = serial::Serial::named("the loader's file", text.as_str()); - filed.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::WEDGE_STAGED)?; - filed.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::WEDGE_ARRIVED_DEAF)?; - // The count the console was given in place of the tail is the count of the - // tail: a number derived from anything else would leave a person at the - // machine believing records exist that the file does not carry. - filed_count_matches(second.text(), &text)?; - kept.remove(); - - eprintln!(" [power] a wedge with every CPU stopped ended itself and said so off the page"); - Ok(()) -} - -/// Every record the loader filed instead of printing is counted on the console, -/// exactly. `printed` is the pass's console, `written` is `loader.log`. -fn filed_count_matches(printed: &str, written: &str) -> Result<(), String> { - let said = printed - .lines() - .rev() - .find_map(|line| line.split_once(bootlog::TAIL_IN_THE_FILE)) - .ok_or_else(|| format!("{:?} is on no line of the console", bootlog::TAIL_IN_THE_FILE))? - .1; - let count: usize = said - .split_whitespace() - .next() - .and_then(|word| word.parse().ok()) - .ok_or_else(|| format!("the console's count of filed records is not a number: {said:?}"))?; - // The loader writes each filed record under its own margin; nothing else in - // the file opens a line that way. - let carried = written.lines().filter(|line| line.starts_with("| [")).count(); - if count != carried { - return Err(format!( - "the console says {count} record(s) went to {} and the file carries {carried}", - bootlog::LOADER_LOG, - )); - } - Ok(()) -} - -/// Where every vCPU stood, asked of QEMU, for a guest that stopped speaking -/// before its chain closed. -/// -/// **The guest cannot be asked and the console cannot tell the two apart.** A -/// machine spinning with `IF` clear and a machine halted with no one-shot armed -/// are both silent, and only the first is a state `crate::hardlockup` covers; -/// `info cpus` names the halted CPUs and `info registers -a` carries each -/// vCPU's `RIP` and `RFLAGS`. Without this a hang here is a mute red that says -/// nothing about which of the two it was. -fn silent_guest(qemu: &QemuInstance, tail: &str) -> String { - let mut monitor = qemu::QmpMonitor::open(qemu.qmp_socket()); - let cpus = monitor.human("info cpus"); - let registers = monitor.human("info registers -a"); - // Whether a CPU that is running could ever be interrupted by its own timer: - // `RFLAGS.IF` is only half of it, and a stopped one-shot reads as a zero - // initial count here. - let lapics: String = (0..2).map(|id| monitor.human(&format!("info lapic {id}"))).collect(); - format!( - "the guest went silent and never reached {:?}\nQEMU's own account of its vCPUs:\n\ - {cpus}\n{registers}\n{lapics}\n{tail}", - bootlog::CHAIN_ENDS_LINE, - ) -} - -/// The bound this test arms, as the parameter spells it. -/// -/// **Short, and short on purpose.** `toyos_tco::WEDGE_BOUND_MS` is the bound a -/// T14 boot runs under and is derived from the runner's own; what is under test -/// here is the poll, the seal and the reset, and the bound is the one thing -/// about the mechanism a boot may legitimately differ on. Wide enough that a -/// `jobcase` boot reaches its shutdown syscall under TCG first, which -/// [`bootlog::WEDGE_STAGED`] above is the assertion about. -const WEDGE_DEADLINE: &str = "boot-deadline=15000"; - -/// One CPU that has stopped taking interrupts ends the machine, from its own -/// NMI, and the record names where it was standing. -/// -/// **The state the boot deadline cannot reach.** Nothing polls a deadline on a -/// machine where no CPU takes an interrupt, so a boot can hang with one armed -/// and the deadline never fire. The control stages exactly that — one CPU with -/// `IF` clear, spinning on a ticket -/// lock another CPU holds and never gives back — and no other bound in this tree -/// ends it: the chipset's TCO is fed by any CPU, the runner's job bound needs a -/// scheduler pass, nothing panicked, and the deadline's own poll is still being -/// reached by the CPUs that are healthy, which is what keeps this machine -/// looking alive. -/// -/// **Two records, and which one the page carries is the verdict.** The deadline -/// is armed on this boot too and would end the same machine -/// [`toyos_tco::hard_lockup_bound_ms`] later; a page reading -/// [`bootlog::DEADLINE_EXPIRED`] is this detector failing and the deadline -/// covering for it, so that line is refused as hard as the right one is -/// demanded. The mutation is the whole detector reverted — `start` arming -/// nothing, so no counter is programmed and no sample runs — and the boot then -/// carries no `hard lockup: ms` at all, which is the first thing asserted -/// below. -/// -/// **QEMU's TCG guest has no performance counter**, so the counter's NMI is one -/// thing this cannot judge: the actuator has a second CPU send the victim the -/// NMI the counter would have, and what is under test here is the handler, the -/// decision, the record and the reset. `hardlockup_ends_a_deaf_cpu`'s metal arm -/// is where the counter itself is the sample, and the line this asserts about -/// CPUID is what tells the two runs apart. -pub fn hard_lockup_ends_a_deaf_cpu( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // Both numbers off the one parameter the image is armed with, so the - // kernel's derivation and this test's expectation cannot drift apart. - let deadline_ms = toyos_tco::deadline_in(LOCKUP_DEADLINE) - .and_then(Result::ok) - .ok_or_else(|| format!("{LOCKUP_DEADLINE:?} is not a bound the kernel would read"))?; - let bound_ms = toyos_tco::hard_lockup_bound_ms(deadline_ms); - - let config = super::compile::repo_root().join("tests/jobcase/system.toml"); - let case = config.parent().expect("system.toml has a directory"); - let (options, kept) = chained_on_a_kept_image( - case, - &["hard-lockup-probe", LOCKUP_DEADLINE], - "hardlockup-boot.img", - )?; - let mut qemu = QemuInstance::boot_with_options(case, &[], &[], options); - let first = serial::Serial::boot(&qemu); - let mut resets = qemu::QmpResets::open(qemu.qmp_socket(), qemu.budget(CHAIN_WAIT)); - first.must_say(&armed_line())?; - - // One capture from the first boot's handoff to the pass that reports it: - // everything this machine drained, and the head of the record read back - // off the page under it. - let second = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - // The arm, in the kernel's own words and with the kernel's own arithmetic. - second.must_say(&format!("hard lockup: {bound_ms} ms"))?; - // Half of the control: the machine did not reach a reset of its own accord. - // That it reached the staged lockup at all is asserted off the file below, - // the only channel the cpu that wrote it had left. - second.must_not_say(bootlog::REBOOTING)?; - - second.must_say(bootlog::PREVIOUS_PANIC)?; - // **After the harvest line**, so this is the page and not the wire. - second.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::LOCKED_UP)?; - // The lock the staged cpu was inside, which is the field a machine with - // every CPU deaf has nothing else to say, and the site that took it — the - // control's own witness, carried by the mechanism rather than by a log line - // the sealed page may have no room for. - let stuck = second.must_say_after(bootlog::PREVIOUS_PANIC, "spinning on the lock at 0x")?; - sp_is_a_kernel_stack(stuck)?; - second.must_say_after(bootlog::PREVIOUS_PANIC, "taken at src/hardlockup/probe.rs")?; - // A line for every cpu, so the holder of what the stuck one wanted is in - // the record too. cpu0 is the one this boot is certain of. - second.must_say_after(bootlog::PREVIOUS_PANIC, "cpu0 irqs=")?; - // The discrimination: the deadline was armed and running on this boot, and - // it is not what ended the machine. - second.must_not_say(bootlog::DEADLINE_EXPIRED)?; - second.must_not_say(&armed_and_nothing_else())?; - second.must_say(bootlog::CHAIN_ENDS_LINE)?; - second.must_not_say(bootlog::LOADER_LAST_LINE)?; - ended_in_a_reset(&mut resets)?; - drop(qemu); - - // The other half of the control, off the only channel that carries it: both - // records are written by the cpu this boot staged, after the last drain the - // machine ever ran. The machine reached the staged lockup; and which sample - // source this run proves, the whole difference between it and the metal - // arm: no counter here, so a sibling sends the NMI the counter would have. - let text = kept.loader_log()?; - let filed = serial::Serial::named("the loader's file", text.as_str()); - filed.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::LOCKUP_STAGED)?; - filed.must_say_after(bootlog::PREVIOUS_PANIC, "CPUID states no counter on cpu")?; - filed_count_matches(second.text(), &text)?; - kept.remove(); - - eprintln!( - " [power] one cpu with interrupts off ended the machine {bound_ms} ms in, and the page \ - named where it was" - ); - Ok(()) -} - -/// The bound this control arms, as the parameter spells it. -/// -/// **Wider than [`WEDGE_DEADLINE`] and for the opposite reason.** The staged cpu -/// goes deaf once the machine is up, so its bound starts running seconds after -/// the deadline's does; half of 30 s leaves it reaching its own bound with the -/// whole of the deadline's second half still ahead, which is what makes a page -/// reading [`bootlog::LOCKED_UP`] rather than [`bootlog::DEADLINE_EXPIRED`] a -/// fact about this detector and not a race between two of them. -const LOCKUP_DEADLINE: &str = "boot-deadline=30000"; - -/// A boot that hung is bounded by the stick, and the third boot is free again. -/// -/// **The defect trapped the machine in ToyOS.** `bootnext::point_at_us` aims -/// `BootNext` at the loader before every kernel handoff, so a kernel that hangs -/// and an owner who cuts power get firmware, this loader, the same kernel, the -/// same hang — for ever. The black box cannot break it: a power cut is exactly -/// what empties the black box, so the next pass finds nothing to report and arms -/// a fresh record. The only ways out are the firmware's boot menu and pulling -/// the stick, and neither of those is the loop. -/// -/// Three launches of **one image file**, because what carries the count is the -/// stick and not the memory: -/// -/// 1. killed at the loader's handoff line — the kernel is never given the -/// machine, which is a boot that was handed it and never reported; -/// 2. the same image again. QEMU zeroes a machine's RAM between launches, which -/// is the power cut exactly: the page is empty, the stick says one attempt, -/// and this pass must boot no kernel, say so, and reset; -/// 3. **the same image a third time, which must boot.** One hand per hang and -/// never two: a bound that left the count standing would refuse this image -/// for ever, which is the trap again with a different door. -pub fn hang_bounded_by_the_stick( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = super::compile::repo_root().join("tests/jobcase/system.toml"); - let case = config.parent().expect("system.toml has a directory"); - let launch = |marker: &'static str| BootOptions { - profile: qemu::Profile::Metal, - // Carried, which is the whole shape of this test: the count it is about - // lives in the image file, so all three launches are one file — and the - // harness owns it, so nothing here builds an image of its own to keep. - boot_image: Some(qemu::Staged::Carried("hang-bound")), - takes_the_reset: true, - ready_marker: marker, - ..Default::default() - }; - - // 1. The hang: killed where the loader has counted this attempt and is about - // to hand over, so the kernel gets the machine and reports nothing. - let first = QemuInstance::boot_with_options(case, &[], &[], launch(bootlog::LOADER_LAST_LINE)); - let counted = serial::Serial::boot(&first); - counted.must_say("Boot attempts: this image has had the machine 0 time(s)")?; - drop(first); - - // 2. The bound. Nothing about this machine has changed but its memory, which - // is what a power cut changes. - let second = - QemuInstance::boot_with_options(case, &[], &[], launch(bootlog::CHAIN_ENDS_LINE)); - let bounded = serial::Serial::boot(&second); - bounded.must_say("Boot attempts: this image has had the machine 1 time(s)")?; - bounded.must_say(bootlog::HUNG_WITHOUT_A_RECORD)?; - // It booted no kernel: the handoff line is what a pass that did writes. - says_nothing_of(&bounded, bootlog::LOADER_LAST_LINE)?; - bounded.must_say(bootlog::CHAIN_ENDS_LINE)?; - drop(second); - - // 3. Free again, and this is the half that makes it a bound rather than a - // refusal: the count was cleared when the machine was handed back. - let third = QemuInstance::boot_with_options(case, &[], &[], launch(bootlog::LOADER_LAST_LINE)); - let again = serial::Serial::boot(&third); - again.must_say("Boot attempts: this image has had the machine 0 time(s)")?; - says_nothing_of(&again, bootlog::HUNG_WITHOUT_A_RECORD)?; - again.must_say(bootlog::LOADER_LAST_LINE)?; - drop(third); - - eprintln!(" [power] one hand per hang: the retry booted nothing and the boot after it booted"); - Ok(()) -} -/// The bound this stages inside the panic's own hold, in guest milliseconds. -/// -/// **Between the two, and both are the guest's clock.** `test-late-panic` fires -/// at the end of the boot and `panic-reboot-fast` holds the panel for -/// [`PANIC_FAST_SECS`] after it, so a deadline armed here expires while the -/// panel is up: earlier and it would end the boot before anything panicked, -/// later and the panic's own reset beats it and the arm proves nothing. -const PANIC_OUTLIVES_DEADLINE: &str = "boot-deadline=4000"; - -/// A panic outlives the boot deadline, and the record that crosses the reset is -/// the panic's. -/// -/// **The bound stands down for a report and does not seal over it.** Both are -/// armed on this boot and the deadline's passes while the panel is up, so the -/// page that crosses the reset says which of the two ended the machine. -/// -/// **What this cannot judge, stated rather than implied.** Reverting -/// `deadline::stand_down` alone leaves this green: after `panic::halt_all_cpus` -/// every CPU is halted or spinning with `IF` clear, so nothing reaches the poll -/// and an armed deadline cannot expire whether or not it was disarmed. The -/// window the stand-down closes is the one *before* that — the panicking CPU has -/// not taken `PAINTING` yet and its siblings are still taking timer interrupts — -/// and no actuator in this tree aims a boot at it. What is asserted here is the -/// composed outcome: a panic report crosses the reset with a bound armed and -/// passed, so a panel that ever re-arms a timer, or a stand-down that is -/// dropped along with something that wakes one, is caught here. -/// -/// The witness is the same one `blackbox_panic_chain` reads, so a page carrying -/// it is the panic's own report and not a state the loader put there; -/// [`bootlog::DEADLINE_EXPIRED`] is refused as hard as it is demanded. -pub fn panic_outlives_the_deadline( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let params: &[&str] = &["test-late-panic", "panic-reboot-fast", PANIC_OUTLIVES_DEADLINE]; - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, chained(params)); - let first = serial::Serial::boot(&qemu); - let mut resets = qemu::QmpResets::open(qemu.qmp_socket(), qemu.budget(CHAIN_WAIT)); - first.must_say(&armed_line())?; - - // This capture opens at the first boot's handoff, so it carries that - // kernel's own console as well as the pass that reports it. - let second = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - // The control on the control: this boot really did arm the bound, in the - // kernel's own words, so a green run is not one where nothing was armed. - let armed = second.must_say("boot deadline: 4000 ms")?.to_string(); - second.must_say(bootlog::PREVIOUS_PANIC)?; - // After the harvest line, so this is the sealed page and not the first - // boot's console, which this capture also carries. - second.must_say_after(bootlog::PREVIOUS_PANIC, BLACKBOX_WITNESS)?; - second.must_not_say(&armed_and_nothing_else())?; - // The whole verdict: the deadline was armed, its bound passed while the - // panel was up, and nothing it writes is on the page or on either channel. - says_nothing_of(&second, bootlog::DEADLINE_EXPIRED)?; - second.must_say(bootlog::CHAIN_ENDS_LINE)?; - second.must_not_say(bootlog::LOADER_LAST_LINE)?; - ended_in_a_reset(&mut resets)?; - drop(qemu); - - eprintln!(" [power] the panic report crossed the reset with {} armed", armed.trim()); - Ok(()) -} - -/// A record another image left in this memory is cleared and never reported. -/// -/// **The defect this is the control for cost a T14 run its first boot.** The -/// black-box page is DRAM at a fixed address and nothing between two operating -/// systems clears it: a `DONE` record from a boot two hours and three Ubuntu -/// boots earlier was still there, and the loader — booting a different image off -/// a freshly flashed stick — read it, took itself for that record's reporting -/// pass, wrote `loader.log` and reset. The machine came back in 24 s with an -/// empty kernel log. No stamp could have caught it: the record was written -/// *before* that boot, which is exactly what a real predecessor's is. -/// -/// **One QEMU and three loader passes**, because the page is memory: a second -/// launch is a machine with zeroed RAM and no record to find at all. Two images -/// therefore cannot be booted over one page here, and the actuator stages the -/// same input instead — the shutdown seals under an identity one bit away from -/// this stick's, which is what a foreign record looks like to the pass that -/// finds it. **One bit, because the check is an equality and a plausible -/// near-miss is the input worth staging.** -/// -/// The third pass is what makes the clear an assertion rather than a claim: a -/// record that survived it would be reported to the boot after, for ever. -/// -/// **The second pass also hands the machine back, and that is right.** A record -/// this stick did not write means the last boot of *this* image was handed the -/// machine and reported nothing, which is what `attempt`'s bound is for; the two -/// mechanisms agree here and `hang_bounded_by_the_stick` owns the second. -pub fn blackbox_foreign_record( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = super::compile::repo_root().join("tests/jobcase/system.toml"); - let case = config.parent().expect("system.toml has a directory"); - let mut qemu = - QemuInstance::boot_with_options(case, &[], &[], chained(&["blackbox-foreign-identity"])); - serial::Serial::boot(&qemu).must_say(&armed_line())?; - - // The pass that finds it: it must name it and clear it, and it must never - // report it as this stick's predecessor — which is the whole defect. - let found = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - let said = found.must_say(bootlog::FOREIGN_DONE)?.to_string(); - let cleared = "cleared and this pass boots its kernel"; - if !said.contains(cleared) { - return Err(format!("the pass reported a foreign record without {cleared:?}: {said}")); - } - says_nothing_of(&found, bootlog::PREVIOUS_PANIC)?; - says_nothing_of(&found, "the last boot read")?; - - // The clear stuck: nothing about a record reaches the pass after it, and - // that pass boots a kernel — so neither the page nor the attempt count is - // left holding this machine. - let after = after_the_reset(&mut qemu, bootlog::LOADER_LAST_LINE); - says_nothing_of(&after, "record another image left in this memory")?; - says_nothing_of(&after, bootlog::PREVIOUS_PANIC)?; - says_nothing_of(&after, bootlog::HUNG_WITHOUT_A_RECORD)?; - after.must_say(bootlog::LOADER_LAST_LINE)?; - drop(qemu); - - eprintln!(" [power] {}", said.trim()); - eprintln!(" [power] and the pass after it found nothing, so the clear reached the page"); - Ok(()) -} /// The loader pass after a deliberate reboot: it read DONE, said so, and ended /// the chain rather than booting another kernel. /// @@ -1377,131 +192,6 @@ fn the_tail_is_the_stops(after: &serial::Serial) -> Result<(), String> { Ok(()) } -/// The three phases a Bulk-Only command can be open at, each its own boot, as -/// the parameters that stage it and the phase the account then names. -/// -/// **All three and not one.** The device is left holding something different at -/// each — a CBW with no data coming, a data phase on the ring that was never -/// rung for, and data it has taken with nothing reading its status — and an -/// account that can name one of them is not one that can name the others. -/// -/// **One declaration and no default.** `chained` takes its arms as one static -/// list, so each boot's whole parameter list is here — the arm's name is its -/// first element, and there is no spelling of it anywhere else in the harness -/// for a name to drift away from. -const WEDGE_PHASES: &[(&[&str], Phase)] = &[ - (&["usb-wedge-data-owed", WEDGE_DEADLINE], Phase::DataOwed), - (&["usb-wedge-in-data", WEDGE_DEADLINE], Phase::Data), - (&["usb-wedge-before-status", WEDGE_DEADLINE], Phase::StatusOwed), -]; - -/// A machine stopped inside a Bulk-Only command ends itself, and the reset that -/// ends it says which phase it found the device in — at every phase it can be -/// stopped in. -/// -/// **What an emulator can prove here and what it cannot.** Whether a device -/// survives being cut is the T14's own stick to answer and nothing here can ask -/// it. What this arm judges is that the machine really stopped inside a -/// command and that the reset's account names which one — the only evidence a -/// reset leaves about what it found. -/// [`Profile::Metal`](qemu::Profile::Metal) carries the boot stick on -/// its xHCI, which is why the wedge has a device to be inside at all. -pub fn usb_reset_records_the_phase_it_cut( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // Every phase is run and every finding reported: a mutation that reverts - // `OpenCommand`'s publication breaks all three, and stopping at the first - // would say so about one. - let mut bad = Vec::new(); - for (params, phase) in WEDGE_PHASES { - if let Err(why) = one_wedge_phase(params, *phase) { - bad.push(why); - } - } - if let Err(why) = the_load_refuses_a_disk_with_no_room() { - bad.push(why); - } - if bad.is_empty() { - return Ok(()); - } - Err(format!("{} of {} arm(s) unmet:\n {}", bad.len(), WEDGE_PHASES.len() + 1, bad.join("\n "))) -} - -/// The load arm's QEMU half, and it is the refusal and nothing else. -/// -/// **This machine's disk is the image, and no guest here has the gibibyte the -/// sweep demands.** What a guest can establish is that the arm says so by name -/// and lets the boot end, rather than silently staging nothing and reading back -/// as a wedge that never happened. -fn the_load_refuses_a_disk_with_no_room() -> Result<(), String> { - let config = super::compile::repo_root().join("tests/jobcase/system.toml"); - let case = config.parent().expect("system.toml has a directory"); - let mut qemu = QemuInstance::boot_with_options( - case, - &[], - &[], - chained(&["usb-reset-under-load", WEDGE_DEADLINE]), - ); - let first = serial::Serial::boot(&qemu); - let mut resets = qemu::QmpResets::open(qemu.qmp_socket(), qemu.budget(CHAIN_WAIT)); - first.must_say(&armed_line()).map_err(|why| format!("usb-reset-under-load: {why}"))?; - - let second = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - if !second.text().contains(bootlog::CHAIN_ENDS_LINE) { - drop(resets); - return Err(format!("usb-reset-under-load: {}", silent_guest(&qemu, second.text()))); - } - second - .must_say(bootlog::USB_LOAD_REFUSED) - .map_err(|why| format!("usb-reset-under-load: {why}"))?; - says_nothing_of(&second, bootlog::USB_LOAD_RUNNING) - .map_err(|why| format!("usb-reset-under-load: {why}"))?; - // And the refusal let the boot end, rather than parking a machine that then - // reads back as a wedge nobody staged. - second.must_say(REBOOTING).map_err(|why| format!("usb-reset-under-load: {why}"))?; - ended_in_a_reset(&mut resets).map_err(|why| format!("usb-reset-under-load: {why}"))?; - drop(qemu); - - eprintln!(" [power] usb-reset-under-load: refused by name on a disk with no room, and the \ - boot ended"); - Ok(()) -} - -/// One boot: stop inside a command at this arm's phase, and read what the reset -/// did off the page the pass after it prints. -fn one_wedge_phase(params: &'static [&'static str], phase: Phase) -> Result<(), String> { - let arm = params.first().expect("an arm list opens with its arm"); - let config = super::compile::repo_root().join("tests/jobcase/system.toml"); - let case = config.parent().expect("system.toml has a directory"); - let (options, kept) = chained_on_a_kept_image(case, params, &format!("{arm}-boot.img"))?; - let mut qemu = QemuInstance::boot_with_options(case, &[], &[], options); - let first = serial::Serial::boot(&qemu); - let mut resets = qemu::QmpResets::open(qemu.qmp_socket(), qemu.budget(CHAIN_WAIT)); - first.must_say(&armed_line()).map_err(|why| format!("{arm}: {why}"))?; - - // The account is the page's head, so it is on the console; the wedge's own - // records are written after the last drain the machine ran, so they cross - // the reset only in the black box's tail, which the loader files in - // `loader.log` — the file the T14's judge reads too. - let second = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - if !second.text().contains(bootlog::CHAIN_ENDS_LINE) { - // One monitor per socket: the reset watcher goes before the question. - drop(resets); - return Err(format!("{arm}: {}", silent_guest(&qemu, second.text()))); - } - ended_in_a_reset(&mut resets).map_err(|why| format!("{arm}: {why}"))?; - drop(qemu); - let text = kept.loader_log().map_err(|why| format!("{arm}: {why}"))?; - let filed = serial::Serial::named("the loader's file", text.as_str()); - usb_wedge_chain(&filed, &second, phase).map_err(|why| format!("{arm}: {why}"))?; - kept.remove(); - - eprintln!(" [power] {arm}: stopped in its {phase} phase, and the account named it"); - Ok(()) -} - /// The metal half of the load arm: a T14 boot that never stopped writing, ended /// by the boot deadline with its controller mid-transfer, and the stick still /// there afterwards. @@ -1544,42 +234,6 @@ pub fn usb_load_chain(after: &serial::Serial) -> Result<(), String> { Ok(()) } -/// One wedge boot's two halves: the machine really stopped inside a Bulk-Only -/// command, and the reset that ended it said which phase it found the device in. -/// -/// **The reset does not finish the command and this does not ask it to.** The -/// rings it could write are rebuilt from published numbers a live driver may -/// still be enqueuing on, so what the account owes is the phase and what the -/// controller was doing — which is what -/// `kernel/src/drivers/xhci/stop.rs::settle_commands` writes and what reverting -/// `OpenCommand`'s publication makes absent. -fn usb_wedge_chain( - kernel: &serial::Serial, - after: &serial::Serial, - phase: Phase, -) -> Result<(), String> { - // The control: the machine reached the staged write and stopped inside it. - // Without the second line the boot would wedge anyway — at the shutdown, - // holding nothing — and read back like the arm that proves the point. - kernel.must_say(bootlog::USB_WEDGE_STAGED)?; - says_nothing_of(kernel, bootlog::USB_WEDGE_MISSED)?; - kernel.must_say(bootlog::WEDGE_STAGED)?; - says_nothing_of(kernel, REBOOTING)?; - - after.must_say(bootlog::PREVIOUS_PANIC)?; - after.must_say_after(bootlog::PREVIOUS_PANIC, bootlog::DEADLINE_EXPIRED)?; - after.must_say(bootlog::CHAIN_ENDS_LINE)?; - // The phase the account names is the one this boot was staged for: an - // account that named another would be about a device stopped somewhere - // nobody asked about. - let named = format!("command was open in its {phase} phase"); - let said = after.must_say(&named)?.to_string(); - // And the hardware's own word beside the driver's claim. - after.must_say(toyos_build::metaldevices::QUIESCE_ENDPOINT)?; - eprintln!(" [power] {}", said.trim()); - Ok(()) -} - /// The metal half of [`boot_deadline_ends_a_wedge`]: a T14 boot that wedged on /// purpose ended itself, and the pass after the reset read why off the page. /// @@ -1687,33 +341,6 @@ pub fn reset_register_decoded(kernel: &serial::Serial) -> Result<(), String> { Ok(()) } -/// The kernel side with no page under it: a boot whose loader claimed none says -/// so by name and writes nowhere. -/// -/// **A control on the loader's claim, not on the feature.** Without it a green -/// chain says only that a claimed page works, never that a kernel handed no page -/// declines to write one — and a kernel that wrote anyway would be writing into -/// memory nothing reserved. -pub fn blackbox_unclaimed_page( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { profile: qemu::Profile::Metal, ..Default::default() }, - ); - let boot = serial::Serial::boot(&qemu); - boot.must_be_clean()?; - // One console carries both writers here; on the T14 the loader's two lines - // are in `loader.log` and the kernel's are records on the stick. - blackbox_unclaimed(&boot, &boot)?; - drop(qemu); - Ok(()) -} - /// The loader named a page, the kernel took *that* page, and it took it before /// the console existed. pub fn blackbox_unclaimed( @@ -1739,474 +366,7 @@ pub fn blackbox_unclaimed( Ok(()) } -/// A panic earlier than everything the kernel used to learn the page's address -/// from seals it anyway — read out of the page's own bytes, by QEMU. -/// -/// **What it judges is the seal, not the ordering.** No staged panic can land -/// before `params::init` — arming one requires that line to have been parsed — -/// so the earliest this tree can produce is inside `percpu::init_bsp`, which is -/// after it, and a kernel that took the page late would still seal here. That -/// the page is taken *before* `serial::init` is `blackbox_unclaimed_page`'s to -/// say, off the order of two records. -/// -/// The oracle is `pmemsave`, which is QEMU reading its own guest's physical -/// memory — not the guest reporting on itself, and not a reset the guest cannot -/// perform here anyway. The bytes are then handed to the same `recover` the next -/// boot's loader would call, so what is judged is a page that loader would read. -pub fn blackbox_early_panic_sealed( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - qmp: true, - kernel_params: &["test-panic-after-idt"], - ready_marker: EARLY_WITNESS, - ..Default::default() - }, - ); - let boot = serial::Serial::boot(&qemu); - // The kernel took the page before it panicked, which is the whole claim; - // without this line the seal below could only have come from the loader. - boot.must_say(&kernel_took_it())?; - - // **Polled, because the marker above precedes the seal rather than - // following it.** The console line is the panic's *first* act and the seal - // is one of its last — `render` seals inside the same call that paints — so - // a single read here is a race the guest wins only while the host is quiet, - // and a busy host loses it. Nothing else in this boot can write the page, so - // waiting for `Panic` cannot pass for the wrong reason; a page that stays - // `ARMED` for the whole bound is the defect this test is for. - let (state, text) = sealed_state(&mut qemu, Duration::from_secs(10))?; - if state != State::Panic { - return Err(format!( - "the page reads {} after a panic, so the panic path did not reach it and the next \ - boot would report a kernel that vanished", - state.named() - )); - } - let text = String::from_utf8_lossy(&text); - // The first line, not somewhere in it: what a panel carries after a panic is - // the register dump, the page walk and the backtrace, so a report cut to its - // tail is a report with the crash missing. - let first = text.lines().next().unwrap_or_default(); - if !first.starts_with("PANIC (apic ") || !first.contains(EARLY_WITNESS) { - return Err(format!( - "the report's first line is {first:?} and this crash's message is \ - {EARLY_WITNESS:?}\n{text}" - )); - } - // Under the head, the recovery section: this boot never reached a USB - // controller, so it is the one line that says no transport broke — and - // that the walk it takes over the ring runs this early, on the boot shard - // alone. - let mut under = text.lines().skip(1); - let section = under.next().unwrap_or_default(); - if section != toyos_blackbox::RECOVERY_NONE { - return Err(format!( - "the line under the head is {section:?}, not the recovery section's {:?}\n{text}", - toyos_blackbox::RECOVERY_NONE - )); - } - // And the tail under that opens on a whole record. Only its *first* line is - // the claim: a record renders as several lines — the panic's own message is - // on one of its own — so a continuation below the first is a record being - // shown, not a cut. - if let Some(opened) = under.next() { - if !opened.starts_with('[') { - return Err(format!( - "the tail under the head opens {opened:?} and a record opens with its own \ - timestamp, so it was cut part-way into one\n{text}" - )); - } - } - drop(qemu); - - eprintln!( - " [power] a panic before `params::init` sealed {} bytes under {first:?}", - text.len() - ); - Ok(()) -} - -/// Every exception seals its registers at the entry, and on a healthy boot the -/// page carries the last one — read off the page's own bytes by QEMU. -/// -/// **This is the only judge of the entry seal, and it is a positive one.** A -/// fault whose report never runs is what the seal exists for, and no actuator -/// stages that: every fault this tree can arm reaches `halt_all_cpus`, which -/// paints and seals PANIC over the record. What is left is the ordinary case — -/// a boot takes demand page faults, the entry seals each, and the newest is on -/// the page until something overwrites it. That the record is a real one, with -/// this machine's vector and a canonical `rip`, is what says the entry wrote it. -/// -/// The cache write-back every writer of the page also does cannot be judged -/// here at all: this guest is TCG and has no caches for `CLFLUSH` to act on -/// (`issues/panic-path/the-pages-cache-writeback-runs-on-no-guest-this-tree-boots.md`). -pub fn blackbox_fault_sealed( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { profile: qemu::Profile::Metal, qmp: true, ..Default::default() }, - ); - serial::Serial::boot(&qemu).must_be_clean()?; - - let page = qemu.guest_memory(PHYS, toyos_blackbox::BYTES)?; - let page: &[u8; toyos_blackbox::BYTES] = - page.as_slice().try_into().map_err(|_| "pmemsave returned the wrong length".to_string())?; - let Some((state, _, _, text)) = toyos_blackbox::recover(page) else { - return Err(format!("the page at {PHYS:#x} carries nothing: {:02x?}", &page[..32])) - }; - if state != State::Fault { - return Err(format!( - "the page reads {} on a boot that took exceptions and neither panicked nor stopped, \ - so its exception entry sealed nothing", - state.named() - )); - } - let Some(fault) = toyos_blackbox::Fault::from_bytes(text) else { - return Err(format!("the page is sealed FAULT and its {} bytes are not a record", text.len())) - }; - // The vector this machine's boot ends on, named rather than ranged: a - // record whose vector is whatever happened to be there says nothing about - // whether the entry read the frame or a zeroed one. - if fault.vector != PAGE_FAULT_VECTOR { - return Err(format!( - "the newest sealed fault is vector {} and a boot of this shape ends on \ - {PAGE_FAULT_VECTOR}: {fault:?}", - fault.vector - )); - } - // **Canonical, and asserted rather than claimed.** A `rip` outside both - // halves of the address space is a frame read at the wrong offset, which is - // the one way a fixed-layout record can be wrong while still checksumming. - if !canonical(fault.rip) || fault.cr3 == 0 { - return Err(format!("the sealed record has a non-canonical rip or an empty cr3: {fault:?}")); - } - // The same of the error code: a `#PF` defines bits 0..=5 and bit 15 - // (SDM Vol. 3A §4.7), so anything else in it is not this frame's word. - if fault.error_code & !PAGE_FAULT_ERROR_BITS != 0 { - return Err(format!( - "the sealed error code is {:#x} and a page fault's bits are {PAGE_FAULT_ERROR_BITS:#x}", - fault.error_code - )); - } - drop(qemu); - - eprintln!( - " [power] the exception entry sealed vector {} err={:#x} rip={:#018x} on the page", - fault.vector, fault.error_code, fault.rip - ); - Ok(()) -} - -/// `#PF`, which is the vector a boot of this shape ends its exceptions on: -/// demand paging is what a running machine faults for. -const PAGE_FAULT_VECTOR: u64 = 14; - -/// The bits a `#PF` error code defines: P, W/R, U/S, RSVD, I/D, PK and SGX -/// (SDM Vol. 3A §4.7). Anything else set is not a page fault's word. -const PAGE_FAULT_ERROR_BITS: u64 = 0x803F; - -/// Whether `at` is an address this machine can hold: 48-bit canonical, so -/// either half and nothing between them. -fn canonical(at: u64) -> bool { - !(0x0000_8000_0000_0000..0xFFFF_8000_0000_0000).contains(&at) -} - -/// The same early panic on a machine with no serial port at all: the panel and -/// the page are the only two channels there are, and both must carry it. -/// -/// **The combination nothing else covers.** `blackbox_early_panic_sealed` is -/// this crash with a 16550 to report it on, and `screen_panic_muted` is a muted -/// guest whose panic is late — clock calibrated, `logd` running, the machine -/// released. The owner's laptop is neither: no serial port and a crash before -/// any of that, which is the arm where `halt_all_cpus`' waits have nothing left -/// to wait for and where `!has_console()` sends the panic path down branches no -/// other guest executes. -pub fn blackbox_early_panic_sealed_muted( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { - profile: qemu::Profile::Metal, - qmp: true, - mute: true, - kernel_params: &["test-panic-after-idt"], - ..Default::default() - }; - // The muted profile is this test's whole premise, so it is checked and not assumed. - let argv = qemu::profile_argv(&options); - match argv.iter().position(|a| a == "-serial") { - Some(i) if argv.get(i + 1).is_some_and(|v| v == "none") => {} - _ => return Err(format!("the muted profile still has a 16550: {argv:?}")), - } - - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - // Nothing announces it — there is no console for a marker to arrive on — so - // the screen is polled. The bound covers firmware plus the root read off USB. - let dump = qemu.screendump_until("PANIC:", Duration::from_secs(30)); - let text = dump.text(); - if !text.contains(EARLY_WITNESS) { - return Err(format!( - "the panel of a guest with no serial port does not carry {EARLY_WITNESS:?}, so the \ - fatal text reached neither channel\ndecoded screen:\n{text}" - )); - } - let (state, sealed) = sealed_state(&mut qemu, Duration::from_secs(10))?; - if state != State::Panic { - return Err(format!( - "the page reads {} after a panic on a machine whose only other channel is the \ - panel\ndecoded screen:\n{text}", - state.named() - )); - } - let sealed = String::from_utf8_lossy(&sealed); - if !sealed.contains(EARLY_WITNESS) { - return Err(format!("the page is sealed PANIC without {EARLY_WITNESS:?}\n{sealed}")); - } - drop(qemu); - - eprintln!( - " [power] no serial port and a crash before the clock: the panel carries the report and \ - the page carries {} sealed bytes", - sealed.len() - ); - Ok(()) -} - -/// The black-box page's state once the guest has finished writing it, or what -/// it still read at the deadline. -/// -/// The seal is one of the panic path's last acts and every console or panel -/// marker a test can wait on comes before it, so the page is polled rather than -/// read once. Only the panicking guest writes it, so a poll cannot observe a -/// state some other writer put there. -fn sealed_state(qemu: &mut QemuInstance, within: Duration) -> Result<(State, Vec), String> { - let deadline = std::time::Instant::now() + within; - let mut last = None; - loop { - let page = qemu.guest_memory(PHYS, toyos_blackbox::BYTES)?; - let page: &[u8; toyos_blackbox::BYTES] = page - .as_slice() - .try_into() - .map_err(|_| "pmemsave returned the wrong length".to_string())?; - match toyos_blackbox::recover(page) { - Some((State::Panic, _, _, text)) => return Ok((State::Panic, text.to_vec())), - Some((state, _, _, text)) => last = Some((state, text.to_vec())), - None => {} - } - if std::time::Instant::now() >= deadline { - return match last { - Some(seen) => Ok(seen), - None => Err(format!( - "the page at {PHYS:#x} carried nothing the next boot's loader would read for \ - {within:?} after a panic this kernel rendered" - )), - }; - } - std::thread::sleep(Duration::from_millis(100)); - } -} - -/// The kernel record that says this boot's controller found the boot stick. -/// -/// **The re-enumeration, taken off the boot after the reset.** Under QEMU it is -/// weak on purpose: an emulated stick cannot be wedged, so this arm judges that -/// the account is produced and that the machine still comes up on the same -/// device. -const STICK_ENUMERATED: &str = "usb-storage: 1 device(s)"; - -/// Every way this kernel resets a machine, and the account each one leaves. -/// -/// `acpi::reboot` and `acpi::shutdown` are the two resets this kernel performs, -/// and three different things reach them: a job list's last `reboot`, the test -/// runner's own job deadline, and the panic console's bound. Each arm is a -/// chained boot, so the pass after the reset can be read. -/// One way this kernel reaches a reset, and what its account must then say. -struct ResetPath { - what: &'static str, - config: &'static str, - params: &'static [&'static str], - /// Whether that path reaches the shutdown's disk flush: a panic does not, - /// and a wedged controller lock refuses it, so `0/0` is the right answer - /// for both rather than a miss. - flushes: bool, - /// The clause `kernel/src/drivers/xhci/stop.rs` writes for this path's own - /// answer to "could a transfer still have been in flight". - barrier: &'static str, -} - -/// What every path's account has to say about the command a device was inside -/// before it touched the first register. -/// -/// **On every path and not on one**, since the device a cut costs is the same -/// device whichever bound reached the reset. Reverting `settle_commands` makes -/// the sentence absent and fails all four arms by name. -/// -/// Taken from the one declaration rather than spelled again here. -use toyos_build::metaldevices::QUIESCE_COMMAND as SETTLED_COMMANDS; - const TOOK_THE_LOCK: &str = "the controller lock was held from before the log volume's"; -const NO_BARRIER: &str = "no barrier was taken, so this reset is not the shutdown's"; -const LOCK_REFUSED: &str = "the controller lock was not free inside its bound"; - -const RESET_PATHS: &[ResetPath] = &[ - ResetPath { - what: "the orderly reboot", - config: "tests/metaldevicecase", - params: &[], - flushes: true, - barrier: TOOK_THE_LOCK, - }, - // **Armed, because QEMU has no window and hardware does.** `quiesce` spends - // real time on hardware between the boot's last word and the barrier below - // it, which is the window the carved-out log writer is still putting bytes - // on the volume in. Without the actuator this arm reads its account off a - // gap that does not exist and says nothing. - ResetPath { - what: "the runner's job deadline", - config: "tests/jobdeadlinecase", - params: &["quiesce-late-word"], - flushes: true, - barrier: TOOK_THE_LOCK, - }, - ResetPath { - what: "the panic console's bound", - config: "tests/testcases", - params: &["test-late-panic", "panic-reboot-fast"], - flushes: false, - barrier: NO_BARRIER, - }, - // **The control on every bound in the shutdown path.** A boot can hang - // between the last job's exit and the reset with nothing ending it; this - // stages the one wait this change owns — the barrier — never coming free, - // and requires the machine to hand itself back anyway, with the account - // naming what it did without. - ResetPath { - what: "a controller lock that never comes free", - config: "tests/jobcase", - params: &["xhci-lock-wedged"], - flushes: false, - barrier: LOCK_REFUSED, - }, -]; - -/// **No reset this kernel performs leaves a USB device mid-command.** -/// -/// A boot that writes megabytes to the boot stick and resets it out from under -/// the transfer leaves a device its next host cannot enumerate: through reboots -/// and a sysfs port power cycle, until it is physically replugged. -/// -/// **What this can and cannot judge.** QEMU's emulated stick cannot be wedged, -/// so what is judged here is the *account*: for each of the four reset paths, -/// that the reset reset every connected port, halted and reset every controller -/// and emptied every disk cache before it wrote the reset register — and that -/// the boot after it still finds the stick. `metaldevices::Quiesced::complete` -/// is the same predicate the T14 judge applies to the same line, and the whole -/// stop reverted leaves that line absent: 4 of 4 arms unmet, measured. -pub fn usb_reset_hands_devices_back( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let root = super::compile::repo_root(); - // Every arm is run and every finding reported: a mutation that reverts the - // stop breaks all four, and stopping at the first would say so about one. - let mut bad = Vec::new(); - for path in RESET_PATHS { - if let Err(why) = one_reset_path(&root.join(path.config), path) { - bad.push(why); - } - } - if bad.is_empty() { - return Ok(()); - } - Err(format!("{} of {} reset path(s) unmet:\n {}", bad.len(), RESET_PATHS.len(), bad.join("\n "))) -} - -/// One chained boot: reach the reset, read the account the pass after it prints, -/// and see the machine come back on the same stick. -fn one_reset_path(case: &Path, arm: &ResetPath) -> Result<(), String> { - let (path, flushes) = (arm.what, arm.flushes); - let mut qemu = QemuInstance::boot_with_options(case, &[], &[], chained(arm.params)); - let _ = serial::Serial::boot(&qemu); - let mut resets = qemu::QmpResets::open(qemu.qmp_socket(), qemu.budget(CHAIN_WAIT)); - - let after = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - let head = toyos_build::metaldevices::QUIESCE_HEAD; - let account = toyos_build::metaldevices::quiesced(after.text()).ok_or_else(|| { - let said: Vec<&str> = after.text().lines().filter(|l| l.contains(head)).collect(); - match said.is_empty() { - true => format!( - "{path}: the pass after the reset carries no {head:?} line at all, so nothing \ - stopped this machine's USB before it reset" - ), - false => format!( - "{path}: the pass after the reset carries no readable {head:?} summary — what \ - it did carry is {said:?}" - ), - } - })?; - if !account.complete() { - return Err(format!("{path}: the reset did not hand every device back: {account:?}")); - } - // The flush is the half a panic cannot reach, and a zero there on a path - // that does reach it would be a boot with no USB disk at all — under which - // the port-reset count above would be about nothing. - if flushes && account.disks == 0 { - return Err(format!( - "{path}: this boot emptied no disk cache, so it had no USB disk and the account \ - above is about a machine this ruling is not about: {account:?}" - )); - } - if !flushes && account.disks != 0 { - return Err(format!( - "{path}: a panic never reaches the shutdown's flush, so this account was not \ - written by the path it claims: {account:?}" - )); - } - // Which of the three answers this path has to the one question no register - // can be read for: whether a transfer could still have been in flight. - if !after.text().contains(arm.barrier) { - return Err(format!( - "{path}: the account does not say {:?}, so the barrier this path owes was not the \ - one it took", - arm.barrier - )); - } - // And the settle that has to happen before the first register on every - // path: a stop that cut a command between its CBW and its CSW is what - // leaves a device its next host cannot enumerate. - if !after.text().contains(SETTLED_COMMANDS) { - return Err(format!( - "{path}: the account says nothing about a {SETTLED_COMMANDS}, so this stop reached \ - a controller's registers without settling the command a device was inside" - )); - } - bootlog::nothing_after_the_last_word(after.text()).map_err(|why| format!("{path}: {why}"))?; - ended_in_a_reset(&mut resets).map_err(|why| format!("{path}: {why}"))?; - - // And the machine comes back on the same device. The pass after the chain's - // end boots a kernel again, and that kernel's own controller is what says - // whether the stick answered. - let again = after_the_reset(&mut qemu, STICK_ENUMERATED); - again.must_say(STICK_ENUMERATED).map_err(|why| format!("{path}: {why}"))?; - drop(qemu); - eprintln!(" [power] {path}: {account:?}, and the stick enumerated again"); - Ok(()) -} /// The T14's judge for [`usb_reset_hands_devices_back`]. /// @@ -2215,10 +375,7 @@ fn one_reset_path(case: &Path, arm: &ResetPath) -> Result<(), String> { /// which on this machine is in `loader.log`'s pass after the reset rather than /// in any file the kernel wrote. /// -/// Both arms are orderly reboots, so both owe the barrier. The panic path's -/// account is judged under QEMU only: on this machine a kernel that panics -/// before `logd` runs writes no log file at all, and one that panics after it -/// leaves no `Rebooting.` for the loop's own verdict. +/// Both arms are orderly reboots, so both owe the barrier. pub fn usb_reset_on_metal(arms: &[&super::metal::Readback]) -> Result<(), String> { let mut bad = Vec::new(); for back in arms { diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 1634dedc7ea..93952d7b541 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -4,7 +4,6 @@ use std::path::{Path, PathBuf}; use std::process::{Child, ChildStdin, Command, Stdio}; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; -use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use std::{fs, thread}; @@ -116,24 +115,6 @@ pub fn nvme_conflict(held: &std::collections::BTreeSet, want: &Path) -> }) } -/// Proof that no guest is holding a lane's images. -/// -/// There are two ways to have one and there is no third: a lane that has not -/// booted anything yet ([`LaneFree::no_guest_yet`]), and a guest that has been -/// ended ([`QemuInstance::shutdown`], which takes `self`). A boot that takes -/// this by value therefore *cannot be written* before the guest it replaces is -/// gone — which is the mistake `qemu = boot()` makes, because Rust evaluates -/// the right-hand side first. -#[must_use] -pub struct LaneFree(()); - -impl LaneFree { - /// Before a lane's first boot, where there is no guest to end. - pub fn no_guest_yet() -> Self { - Self(()) - } -} - /// Guests this run has started, how many of them were not the shipping kernel, /// and every distinct kernel build it asked cargo for. /// @@ -171,44 +152,38 @@ pub fn boot_census() -> (u32, u32, Vec) { /// list. /// /// `""` is what an image ships. [`toyos_build::build::TEST_KERNEL`] is every -/// actuator compiled in, armed by boot parameter. `fpu-save-nothing` is the one -/// actuator that could not become a parameter — it takes the `fxsave64` out of -/// `arch::entry`'s `naked_asm!` bracket, which is the path its own gate is -/// about. -/// -/// Interactive debug mode is separate: it builds -/// [`toyos_build::build::DEBUG_KERNEL_BUILD`] and returns before the suite. -pub const DECLARED_KERNEL_BUILDS: [&str; 4] = +/// actuator compiled in, armed by boot parameter. An entry here is a decision +/// to pay a kernel build per suite run forever. Interactive debug mode is +/// separate: it builds [`toyos_build::build::DEBUG_KERNEL_BUILD`] and returns +/// before the suite. +pub const DECLARED_KERNEL_BUILDS: [&str; 2] = toyos_build::build::TEST_SUITE_KERNEL_BUILDS; -/// How many guests the phase now running may have up at once. +/// How many guests the run may have up at once. /// /// The harness's own wall-clock margins are margins on the *host*, and they were /// all derived when one guest had it to itself. Four guests is a different /// machine, so such a margin has to be stated against the regime it runs in -/// rather than widened outright — which is what this multiplies. A serial phase -/// sets it back to 1 and gets the number it always had. +/// rather than widened outright — which is what this multiplies. static WIDTH: AtomicU32 = AtomicU32::new(1); pub fn set_width(width: u32) { - assert!(width >= 1, "a phase runs at least one guest"); + assert!(width >= 1, "a run boots at least one guest"); WIDTH.store(width, Ordering::SeqCst); } -/// A liveness ceiling, stated for one guest and paid out for the phase's. +/// A liveness ceiling, stated for one guest and paid out for the run's width. /// /// Every timeout a test hands [`QemuInstance::run_test`] and its relatives is a /// guard against a wedge, never a verdict: the assertion is what the guest /// *said*, and a test whose pass depended on a deadline expiring would be /// asserting on the host's clock. So the number in the source stays the number -/// its author reasoned about — one guest, this host — and the phase multiplies -/// it, exactly as `wait_for_ready` has multiplied the boot timeout since the -/// parallel phase landed. +/// its author reasoned about — one guest, this host — and the width multiplies +/// it, as `wait_for_ready` multiplies the boot timeout. /// /// The cost of getting this wrong in the generous direction is that a wedge /// takes longer to report. The cost in the other direction is a red run that -/// says a guest hung when it was only sharing a machine, which is the failure -/// mode that put the whole shared block in the serial tail. +/// says a guest hung when it was only sharing a machine. /// /// This corrects for width and for how fast the host is, both host-wide facts. /// It does not correct for a guest being wider than the host — an `smp:8` guest @@ -227,7 +202,7 @@ pub fn budget(one_guest: Duration) -> Duration { /// — `wait_for_ready`'s own comment names the two exceptions, and both read the /// guest's stamps rather than this clock — so it is a measurement of the host /// that costs nothing to take. The *fastest* rather than the mean because a boot -/// taken with three other guests up measures the phase; the minimum over a run +/// taken with three other guests up measures the others; the minimum over a run /// is the closest this can get to the machine with nothing else on it. static FASTEST_BOOT_MS: AtomicU32 = AtomicU32::new(u32::MAX); @@ -342,7 +317,7 @@ fn oversubscription(smp: u32) -> (u32, u32) { /// [`budget`] widened by a guest's own vCPU oversubscription. /// -/// The guest-agnostic [`budget`] scales by phase width and boot-derived host +/// The guest-agnostic [`budget`] scales by the run's width and boot-derived host /// speed; this multiplies in `smp/cores` on top, so a wide-SMP guest that a /// mostly-serial boot said little about is given the extra room the derivation /// above says it needs. `smp <= cores` leaves it exactly [`budget`], which is @@ -412,10 +387,7 @@ impl Liveness { /// /// A test that ran out of time has not found the guest doing the wrong thing; /// it has found nothing at all, and the two readings send an agent to opposite -/// places. `screen_pager_keys` reporting `0 page moves over 30 keystrokes` -/// after 0.3 s was bisected as a kernel regression twice in one day by two -/// agents, and the fact it was hiding is that the whole run had collapsed -/// before the guest could answer once. +/// places. /// /// Still red. A guest that stopped answering may have stopped for a reason this /// tree owns, and a status that is not a failure is a status nobody reads. What @@ -510,10 +482,8 @@ pub struct WaitVerdict(String); impl WaitVerdict { /// The sentence a wait reached, and the capture it reached it on. /// - /// `capture` is the window in the order the guest wrote it — for a test, - /// [`TestResult::before`] and then [`TestResult::serial`], which is where - /// the two halves of one window live — because the first kernel death in it - /// is the one this verdict is about. An empty slice is a claim that there + /// `capture` is the window in the order the guest wrote it, because the + /// first kernel death in it is the one this verdict is about. An empty slice is a claim that there /// was no capture at all, and it is a visible one rather than an omission. pub fn new(sentence: String, capture: &[&str]) -> Self { let Some(report) = capture.iter().find_map(|c| super::serial::death_report(c)) else { @@ -524,12 +494,10 @@ impl WaitVerdict { /// The same, for a test that may never have announced itself. /// - /// **A test whose `===TEST_START` never arrived has an empty - /// [`TestResult::serial`] by construction**, so an arm that formats - /// `serial` prints nothing at all and [`TestResult::before`] is the only - /// record the boot left. [`Self::new`]'s silence on a capture nothing died - /// in holds everywhere else: a started test's window is in `serial`, where - /// its arm already looks. + /// **A test whose `===TEST_START` never arrived has an empty `serial` by + /// construction**, so `before` is the only record the boot left. + /// [`Self::new`]'s silence on a capture nothing died in holds everywhere + /// else. pub fn for_test(sentence: String, before: &str, serial: &str, started: bool) -> Self { let verdict = Self::new(sentence, &[before, serial]); if started || verdict.0.contains(DIED_SAYING) || before.trim().is_empty() { @@ -565,9 +533,7 @@ impl std::fmt::Display for WaitVerdict { /// budgeted wall clock (`budget_smp`-scaled, so it already carries #256's /// `vcpus/cores` oversubscription widening), and until this it ended the wait /// the instant it passed — so a merely-slow guest reported exactly what a wedged -/// one did. `launcher_refusals` was killed at `192s "still talking 1s ago"` on a -/// loaded `smp:2` runner its `vcpus/cores` factor clamps to 1, a guest making -/// steady progress called wedged by a clock. +/// one did. /// /// **`elapsed > ceiling` stays a necessary condition, and that is what keeps /// this safe.** Silence alone is not a wedge on this suite's boots: a healthy @@ -739,55 +705,6 @@ fn words(monitor: &mut QmpMonitor, at: u64) -> Vec { words.split_whitespace().filter_map(|word| u32::from_str_radix(word.strip_prefix("0x")?, 16).ok()).collect() } -/// The fatal path's last line, which `panic_reboot::reboot_now` writes to the -/// 16550 raw just before it resets the machine. -pub const PANIC_REBOOTING: &str = "panic: no key inside the bound, so nobody is here"; - -/// Drain the console into `log` until QEMU exits on the fatal path's reset, or -/// the console says one of `refused`: a line this guest must never write ends -/// the wait at once, and the exit closes a capture that is then whole. -/// -/// **The reset and not a halt**: the CPU that went fatal never halts. It holds -/// its panel under `panic_reboot`'s bound, and the bound's reset is the path's -/// last act, which `-no-reboot` turns into QEMU's exit. So the boot passes -/// `panic-reboot-fast`: its five seconds of silence sit inside [`GUEST_QUIET`], -/// and the shipped minute does not. -pub fn await_reset( - qemu: &mut QemuInstance, - log: &mut String, - doing: &str, - refused: &[&str], -) -> Result<(), String> { - let from = log.len(); - let mut live = guest_liveness(); - loop { - if refused.iter().any(|line| log[from..].contains(line)) { - return Ok(()); - } - match qemu.rx.recv_timeout(Duration::from_millis(200)) { - Ok(line) => { - log.push_str(&line); - log.push('\n'); - } - Err(RecvTimeoutError::Timeout) => {} - Err(RecvTimeoutError::Disconnected) => break, - } - if !live.working(log) { - return Err(format!("{STALLED} waiting for {doing} — {}", live.why())); - } - } - let status = qemu.child.wait().map_err(|e| format!("QEMU could not be waited for: {e}"))?; - // On a machine with a console the line is only in the 16550's own log. - let said = format!("{}{}", &log[from..], qemu.uart_log()); - if !status.success() || !said.contains(PANIC_REBOOTING) { - return Err(format!( - "QEMU exited {status} waiting for {doing}, and not on the fatal path's reset: no \ - {PANIC_REBOOTING:?}\n{said}" - )); - } - Ok(()) -} - /// The hardware shape QEMU presents to the guest. /// /// Not a display setting: each variant is a whole machine. `Headless` is the @@ -805,38 +722,7 @@ pub enum Profile { /// `iommu_platform=on`, and the harness sets that only where a unit exists, /// so the guest's own negotiation comes out the other way here. HeadlessNoIommu, - /// [`Profile::Headless`] with the NIC's MSI-X capability taken away. - /// - /// The one configuration in this suite where a device the kernel has - /// already reset and negotiated features with turns out to have no way of - /// raising an interrupt. Every virtio function QEMU builds and every one - /// that ships has the capability, so nothing else could ask what the - /// driver does without it — and what it used to do was panic the kernel, - /// on a machine whose other devices were all fine. - VirtioNetNoMsix, - /// [`Profile::Headless`] with an Intel `e1000e` in place of the virtio - /// NIC, and everything else — console, sound, disks — unchanged. The only - /// machine in reach on which netd's Intel driver runs at all. - E1000e, - /// [`Profile::E1000e`] with its cable plugged into nothing. - /// - /// The one machine in this suite on which a DHCP client gets no answer: - /// the user-mode backend serves a lease whatever else it is told to - /// restrict, so no profile that has one can ask what a boot does on a - /// network that never replies. - E1000eNoServer, - /// [`Profile::E1000e`] with QEMU's `igb` beside the 82574: a claimable - /// function that performs an Express function level reset, which neither - /// the 82574 nor any virtio function does. - E1000eBesideIgb, Gop, - /// [`Profile::Gop`] with a second USB stick beside the boot stick, whose - /// table the test writes: the bus a stick of somebody else's arrives on. - GopUsbDisk, - /// A virtio-gpu function and no VGA: the owner's own desktop, and the one - /// machine where a mode change can succeed rather than answering - /// `NotSupported` ahead of everything a resize does. - VirtioGpu, /// M1 metal-sim: GOP, NVMe, xHCI with the boot stick on it, i8042 from /// q35, and nothing else -- no virtio device and no USB HID. This is the /// machine shape that gets flashed, so it is the one the input tests run @@ -845,217 +731,6 @@ pub enum Profile { /// ===TEST_START=== protocol like any other. [`BootOptions::mute`] takes /// it away for the one test that certifies the T14's literal shape. Metal, - /// No USB at all — no xHCI, so no boot stick — and no i8042 once the boot - /// passes `i8042: false`: the one bootable shape on which no input source - /// can ever exist. The boot volume rides a second NVMe controller, which - /// works because userland runs off that same disk's ROOT partition. - MetalNoUsb, - /// The machine whose only disk is the internal one, with the boot image on - /// it: no xHCI and so no boot stick, and no second namespace either. Every - /// other profile takes `/boot` and `/log` off USB, so none of them can ask - /// what happens when the boot medium is the device storage already holds. - InternalDisk, - /// metal-sim with the T14's internal xHCI actually populated: the boot - /// stick plus five more devices, two of them keyboards. The laptop's - /// controller carries a camera, Bluetooth and a fingerprint reader - /// alongside whatever is plugged in, and a profile with one USB device - /// cannot see any defect that needs a fourth. - MetalUsb, - /// metal-sim with the T14's actual NVMe capacity instead of a token - /// image. Device *size* is a shape dimension and it was the one nobody - /// had varied: every test disk was small enough that a per-device-block - /// index fit under the object allocator's 2 MiB ceiling, so the first - /// boot on the laptop was the first time anything asked for a - /// device-sized allocation. - MetalDisk, - /// metal-sim with no NVMe controller at all. - /// - /// Device *presence* is the shape dimension underneath size and sector - /// size, and it was the one nobody had varied for storage: every profile - /// gave the guest a disk, so nothing asked what the kernel does without - /// one. The answer was `.expect("NVMe: no controller found")` at 0.08 s. - /// ROOT is on the USB stick here, so a machine really can boot ToyOS with - /// no NVMe -- and a controller hidden behind a firmware setting looks - /// exactly the same. - Diskless, - /// metal-sim with a namespace formatted in 8 KiB logical blocks. - /// - /// Sector size is a shape dimension, and it was one the harness could - /// not express: every profile got QEMU's implicit 512-byte namespace, so - /// nothing asked the driver what it does with a device it cannot address. - /// The answer was `4096 / sector_size == 0` and then a divide by zero, at - /// 0.068 s, before storage is up and before there is a console to report - /// it on. - /// - /// 8192 rather than something absurd because it is real: 8 KiB-format - /// namespaces ship, and this driver's whole stack above the sector layer - /// is written in 4096-byte blocks. The guest is expected to refuse the - /// device by name, so this profile boots no userland at all. - NvmeWideSector, - /// metal-sim with a second USB stick beside the boot stick. - /// - /// The boot stick is on the bus in every profile and is the one device the - /// guest must never write to, so a storage test needs a *second* disk — - /// one the harness stages on the host, stamps as writable, and reads back - /// afterwards. Presence of that disk is the shape dimension; every other - /// profile is its absence. - UsbDisk, - /// [`Profile::UsbDisk`] with the second stick formatted in 4 KiB logical - /// blocks. Sector size is a shape dimension for USB exactly as it is for - /// NVMe, and it is the one that produced a divide-by-zero there. - UsbDisk4k, - /// [`Profile::UsbDisk`] with a 3 TB external disk instead of a stick. - /// - /// Past 2 TiB a 512-byte-sector device has more sectors than a READ(10) - /// command can address, and READ CAPACITY(10) stops being able to report - /// the size at all — so this is the profile where the 16-byte form runs - /// and where the driver has to refuse a device rather than serve the first - /// 2 TiB of it. Sparse, so the host pays for the blocks the guest touches. - UsbDiskHuge, - /// [`Profile::UsbDisk`] with the second stick's backing opened read-only. - /// - /// The only configuration in this suite where a *device* refuses an I/O - /// the driver was right to issue: QEMU answers WRITE(10) on a write- - /// protected LUN with a CHECK CONDITION, which is a CSW status of 1 and - /// the REQUEST SENSE path behind it. Reads on the same disk still work, so - /// one boot shows the error channel carrying a failure and not carrying a - /// success. - UsbDiskReadOnly, - /// [`Profile::UsbDiskHuge`] with the 3 TB disk attached *ahead* of the boot - /// stick, so the controller enumerates the disk the driver refuses first. - /// - /// Order is the whole shape. `bind` configures a device's two bulk - /// endpoints into a pool block and only then asks the disk how big it is, - /// so a disk refused for its size has already pointed the controller's - /// endpoint contexts at that block. Every other USB profile puts the boot - /// stick on port 1, where it binds successfully and the question never - /// arises; here the refusal comes first, and what the *next* disk is given - /// is the assertion. QEMU assigns ports in device-creation order, measured - /// against the kernel's own `port N connected` lines. - UsbDiskRefusedFirst, - /// More USB disks on one controller than its DMA pool has blocks for. - /// - /// `MSC_BLOCKS` is 2 and the boot stick takes one of them, so the second - /// data disk here is the first one past the ceiling. Every other profile - /// declares one disk, which is why nothing could ask what a caller sees when - /// the bound is hit — and the bound is policy, so that answer is the whole - /// question. Both disks are stamped: the one that binds is written, and the - /// one the pool had no room for has to come back byte-identical, which is - /// the claim a log line cannot make. - /// - /// Two and not three, though the pool would refuse either way. - /// `nec-usb-xhci` offers four SuperSpeed ports and QEMU puts the fifth - /// device behind an auto-created hub, which this driver walks past — so a - /// third data disk is not one the guest refuses, it is one the guest never - /// sees, and a count that included it would be measuring QEMU's port - /// allocation. Measured: `class=0x9 vendor=0409 product=55aa` on port 8 at - /// full speed, with `no HID boot interface found, skipping`. - UsbDiskCrowd, - /// Two xHCI controllers, with every device on the *second* one. - /// - /// The T14 Gen 2's literal shape, and the one that had never been staged: - /// Tiger Lake puts a USB4 xHCI in the Thunderbolt block at 00:0d.0 and the - /// PCH's at 00:14.0 — same class, same subclass, same prog_if — and the - /// laptop's own ports hang off the second. Nothing is attached to the - /// first here, exactly as nothing is plugged into the laptop's Thunderbolt - /// ports, so a kernel that stops at the first PCI match sees a machine - /// with no USB at all. The i8042 is off, which is what stops a PS/2 - /// keyboard delivering the keystroke this profile means to route over USB. - MetalXhciSecond, - /// Two xHCI controllers with HID devices on both. - /// - /// One held-set and one button merge for the whole machine is a claim - /// about devices on *different controllers* as much as about two on one - /// bus, and it is a claim nothing could test: with one controller, an - /// xHCI slot id was a machine-wide name for a device. It is not — the - /// device lists here are shaped so both pointers land on the same slot id - /// of their own controller — with a *bound* device, because a refused one - /// gives its slot back the moment it is refused and shifts nothing after - /// it. The hub on the second controller is still there and is still walked - /// past; what balances the boot stick on the first is the second keyboard - /// beside it. - MetalXhciBoth, - /// The HID controller has no MSI-X, and nothing else can drain its ring. - /// - /// The T14's Thunderbolt xHCI has no MSI-X capability — the laptop's own - /// boot log says so — and every controller in this suite had one, so the - /// branch that handles its absence had never executed. It logged "using - /// polled mode" and returned, and there is no polled mode: the driver - /// reads an event ring only when vector 0x21 has fired. This profile is - /// the machine where the driver has to fall through to MSI and where an - /// injected keystroke is the only thing that can prove it did — which - /// takes a machine with no USB storage on it at all, for the reason the - /// shape below states. - MetalXhciMsi, - /// Two controllers, the second with neither MSI-X nor MSI. - /// - /// A function offering neither is not a machine that ships — QEMU is the - /// only place it can be built — but "this driver cannot drive this - /// controller" is a state the code has to be able to reach and say, and - /// nothing else can stage it. The first controller is ordinary and carries - /// the boot stick, so the refusal is visibly *per controller*: the machine - /// boots, and the HID on the crippled one is refused by name rather than - /// enumerated and left mute. - MetalXhciNoIrq, - /// One controller carrying HID alone, the boot volume on its own NVMe: the - /// machine a deafened controller or port costs no filesystem, where the - /// keyboard is what a port that never resets has to fail to bind. - MetalXhciDeaf, - /// Two controllers, and every input device arrives *after* the boot. - /// - /// The T14's shape for the one thing no profile stages: its Thunderbolt - /// xHCI at 00:0d.0 has five ports and has never had a device on them, so - /// the controller a user plugs - /// into is the one that enumerated nothing at boot. Here the second - /// controller is that one and the boot stick is on the first. - /// - /// The boot-time device list is one `usb-tablet`, and every part of that is - /// load-bearing. It is a pointer, so a late-bound one has to compose with a - /// source that already exists rather than being the first. It is - /// *absolute*, so QEMU has no relative handler until a `usb-mouse` is - /// plugged in — which makes an injected `rel` event ground truth that the - /// late device is the one delivering, not the boot-time one. And it is not - /// a keyboard: with `i8042=off` this machine has no keyboard at all until - /// one is hot-plugged, so a keystroke that arrives can only have come - /// through the device that was added after the boot. - MetalHotplug, - /// metal-sim with no IOMMU at all, so firmware publishes no `DMAR`. - /// - /// Presence of the unit is the shape dimension, and it is the one QEMU - /// gives for free that no real machine gives at all: on hardware, "no - /// DMAR" and "VT-d disabled in firmware setup" are the same observation. - /// This is the machine where the kernel has - /// to say which of the two it cannot tell apart. - NoIommu, - /// metal-sim whose unit advertises a 39-bit address width instead of 48. - /// - /// `CAP.SAGAW` is a register the guest decodes into a page-table depth, - /// and a suite with one value of it cannot tell a decode from a constant. - /// Both widths are real: 39-bit units ship, and the IOVA base every domain - /// gets is derived from this number. - IommuNarrow, - /// metal-sim whose unit cannot remap interrupts. - /// - /// Two registers move together — the DMAR's own `INTR_REMAP` flag and the - /// unit's `ECAP.IR` — and the kernel gives them separate - /// refusals, because a platform that declares it cannot remap and a unit - /// that cannot are different facts a user can act on differently. - IommuNoIntremap, - /// metal-sim whose unit advertises Extended Interrupt Mode — the only - /// machine here that does, and so the only boot that writes the guest's - /// 32-bit-destination entry format rather than the 8-bit one. - IommuEim, - /// [`Profile::Headless`] with its virtio sound card replaced by an Intel - /// HDA controller and one codec — the machine soundd drives itself, and - /// the class-0403 function the IOMMU tests aim. - Hda, - /// [`Profile::Hda`] with a second controller that also has a codec. - /// - /// Two live links, which the kernel refuses by name rather than binding - /// the first: choosing between them means walking their codec graphs, and - /// that is the driver's work. The negative control on the whole bind path - /// — a first-match kernel would go green on every other HDA test. - HdaTwoLive, /// QEMU `virt` on AArch64 (GICv3, AAVMF): a GOP from `ramfb`, the boot /// stick on an xHCI, the PL011, and nothing else — no virtio, NIC, NVMe or /// IOMMU. The machine the AArch64 port reaches its console on, and the only @@ -1079,38 +754,8 @@ impl Profile { Self::Virt | Self::VirtEl2 | Self::VirtTcg => Arch::Aarch64, Self::Headless | Self::HeadlessNoIommu - | Self::VirtioNetNoMsix - | Self::E1000e - | Self::E1000eNoServer - | Self::E1000eBesideIgb | Self::Gop - | Self::GopUsbDisk - | Self::VirtioGpu - | Self::Metal - | Self::MetalNoUsb - | Self::InternalDisk - | Self::MetalUsb - | Self::MetalDisk - | Self::Diskless - | Self::NvmeWideSector - | Self::UsbDisk - | Self::UsbDisk4k - | Self::UsbDiskHuge - | Self::UsbDiskReadOnly - | Self::UsbDiskRefusedFirst - | Self::UsbDiskCrowd - | Self::MetalXhciSecond - | Self::MetalXhciBoth - | Self::MetalXhciMsi - | Self::MetalXhciNoIrq - | Self::MetalXhciDeaf - | Self::MetalHotplug - | Self::NoIommu - | Self::IommuNarrow - | Self::IommuNoIntremap - | Self::IommuEim - | Self::Hda - | Self::HdaTwoLive => Arch::X86_64, + | Self::Metal => Arch::X86_64, } } @@ -1157,51 +802,6 @@ pub const IOMMU_DEFAULT: Iommu = Iommu { aw_bits: 48, intremap: true, eim: false /// of them — so the default `p2=4,p3=4` takes **four** devices, two short of the /// crowded set rather than one. const XHCI_DEFAULT: &str = "nec-usb-xhci,id=xhci"; -/// Eight attachable ports, which is `MAX(p2=8, p3=4)`, over twelve port -/// registers: 1-4 the SuperSpeed view, 5-12 the USB2 view. Measured on QEMU -/// 11.0.2 against the kernel's own lines — `max_ports=12`, and the six devices -/// landing on registers 1 and 6-10. The boot stick is a `usb-storage` with a -/// SuperSpeed descriptor, so it takes the SuperSpeed view of the first port and -/// is enumerated *before* every HID; the five devices below are full or high -/// speed and take the USB2 view of ports 2-6. Six of eight used, two spare. -/// -/// `slots=` would have been the natural way to stage slot exhaustion, and it -/// is not: on QEMU 11.0.2 `nec-usb-xhci,slots=N` reads back as N through -/// `qom-get` and HCSPARAMS1 still reports 64, `qemu-xhci` has no such property -/// at all, and Enable Slot ignores the MaxSlotsEn the driver writes to CONFIG. -/// The kernel's own `xhci-one-slot` feature is what drives that path. -const XHCI_WIDE: &str = "nec-usb-xhci,id=xhci,p2=8"; -/// A second controller, for the profiles that stage a machine with two. Only -/// the id differs — the point is precisely that the two are indistinguishable -/// by class, subclass and prog_if, which is why taking the first PCI match -/// looked right for as long as it did. -const XHCI_SECOND: &str = "nec-usb-xhci,id=xhci1"; -/// A controller with no MSI-X table, which leaves `msi=auto` to give it MSI — -/// the shape of the T14's Thunderbolt xHCI and of Intel PCH parts generally. -const XHCI_MSI_ONLY: &str = "nec-usb-xhci,id=xhci1,msix=off"; -/// A controller with no message-signalled interrupts at all, in each of the -/// two bus positions a profile puts one in. Nothing on a PCIe bus is really -/// built this way; it is how the harness reaches the branch where the driver -/// has to refuse a controller instead of driving it blind — and, in the first -/// position, how it takes USB storage off a machine entirely. -const XHCI_NO_IRQ_FIRST: &str = "nec-usb-xhci,id=xhci,msix=off,msi=off"; -const XHCI_NO_IRQ_SECOND: &str = "nec-usb-xhci,id=xhci1,msix=off,msi=off"; - -/// One controller with one codec. `hda-output` because it is a playback-only codec — the driver -/// configures no input path and a duplex codec would only add widgets nothing -/// walks. -const HDA_ONE: &[&str] = &["intel-hda,id=hda0", "hda-output,bus=hda0.0,cad=0,audiodev=hdaaud"]; - -/// Two controllers, each with a codec that answers. -/// -/// The state the kernel refuses: it can tell which links are alive and cannot -/// tell which one a human is wired to, so binding either would be a guess. -const HDA_TWO_LIVE: &[&str] = &[ - "intel-hda,id=hda0", - "hda-output,bus=hda0.0,cad=0,audiodev=hdaaud", - "intel-hda,id=hda1", - "hda-output,bus=hda1.0,cad=0,audiodev=hdaaud", -]; /// Whether a machine has the virtio console and sound block. Which NIC it has /// is [`Nic`]. @@ -1209,13 +809,6 @@ const HDA_TWO_LIVE: &[&str] = &[ enum Virtio { Absent, Present, - /// The block **without virtio-sound**, so the machine's only audio device - /// is the one in `hda`. - /// - /// Not a lesser [`Virtio::Present`]: soundd claims a kernel-driven card - /// before it looks for a controller to drive itself, so a machine carrying - /// both would exercise the virtio path and nothing else. - WithoutSound, } impl Virtio { @@ -1236,26 +829,6 @@ impl Virtio { enum Nic { Absent, Virtio, - /// The virtio NIC with its MSI-X capability removed, virtio-sound's and - /// virtio-serial's left alone — so the console still carries the refusal - /// and audio still works while networking does not. - /// - /// A device that publishes no MSI-X capability is a device, not an absence: - /// the driver reaches it, resets it, negotiates features with it and only - /// then finds it has no way to be told a packet arrived. `vectors=0` is the - /// actuator, and the only one — QEMU builds a virtio-pci function's MSI-X - /// table only for a non-zero vector count, and every emulated and every - /// real virtio function has the capability. - VirtioWithoutMsix, - /// QEMU's `e1000e`, which is the 82574L at `8086:10d3`: the same register - /// file the ThinkPad T14's onboard I219 has. - E1000e, - /// The same card on a hub nothing else is plugged into: a link the guest - /// brings up and puts frames onto, with no host, router or server at the - /// other end. - E1000eNoServer, - /// [`Nic::E1000e`], and an `igb` (`8086:10c9`) on no network at all. - E1000eBesideIgb, } /// Everything a profile decides about the machine, in one table. A new @@ -1271,11 +844,6 @@ struct Shape { /// *size* is a shape dimension exactly as a disk's is, and the tests that /// read pixels were all blind to the remainder until one profile had one. panel: Option<(u32, u32)>, - /// A display adapter of its own, beside `vga`. `None` is firmware's GOP, - /// which cannot change mode once boot services have exited — so there - /// `SYS_GPU_SET_RESOLUTION` answers `NotSupported` and everything past the - /// refusal is unexecuted. - gpu: Option<&'static str>, /// virtio-sound and the console on virtio-serial. virtio: Virtio, nic: Nic, @@ -1283,13 +851,6 @@ struct Shape { /// included. A list because a machine can have more than one and the T14 /// does — its keyboard is on the second. xhci: &'static [&'static str], - /// The bus the boot stick and the second USB disk attach to. Named rather - /// than assumed, because which controller carries the storage is a shape - /// dimension once there is more than one: the index the block layer holds - /// has to name the same disk either way. An actuator that refuses a - /// controller wholesale may not run on a profile whose boot volume rides - /// it: ROOT is read through the block layer, so the refusal costs the mount. - storage_bus: &'static str, /// Every USB device besides the boot stick, each naming its own bus. /// Absence is what makes an i8042 test measure anything: QEMU activates /// one input handler per device class, so with a usb-kbd present every @@ -1300,101 +861,14 @@ struct Shape { /// structure sized per device block is bounded by this number and by /// nothing else. nvme_bytes: u64, - /// The namespace's logical block size. Stated per profile for the same - /// reason `nvme_bytes` is: it is a dimension of the device, the driver - /// turns it into a shift and a divisor, and QEMU's implicit namespace only - /// ever produced one value of it. - nvme_lba_bytes: u32, - /// Every `usb-storage` device besides the boot stick, in the order QEMU - /// creates them. - /// - /// A list and not one device's dimensions. **How many disks are on the bus - /// is a shape dimension in its own right**: the driver's DMA pool holds - /// `MSC_BLOCKS` of them and refuses the rest by name, and every profile - /// that could have asked what happens at that ceiling declared exactly one. - /// The order is the second half of the same field — QEMU hands out - /// root-hub ports in device-creation order, so where the boot stick falls - /// in this list is what decides which disk the controller enumerates first. - usb_disks: &'static [UsbDisk], - /// Every Intel HDA controller on the machine and the codecs behind each, - /// as `-device` arguments in the order QEMU is to create them. Empty is - /// what every profile but [`Profile::Hda`] and [`Profile::HdaTwoLive`] - /// declares, and it is the machine this kernel has always booted: audio - /// through virtio-sound or through nothing at all. - /// - /// Presence of a class-0403 *function* is the shape dimension, and it is - /// separate from whether anything answers on the link behind it — which is - /// H0's question (b), and what the codec - /// arguments in this list decide per controller. - hda: &'static [&'static str], /// The unit that decodes this machine's DMA, or its absence. Stated per /// profile because absence is a shape and because the unit's own /// capabilities are what the kernel reads at boot. iommu: Option, } -/// One `usb-storage` device beside the boot stick. -#[derive(Clone, Copy)] -pub struct UsbDisk { - /// Its size. Stated for the same reason the namespace's is — the driver - /// turns it into an LBA, and whether that LBA fits the command it is sent - /// in is a property of this number. The backing is sparse, so a realistic - /// one is nearly free. - pub bytes: u64, - /// Its logical block size. `usb-storage` takes any power of two from 512 B - /// up, so unlike the boot stick this is something a profile can choose. - pub lba_bytes: u32, - /// Open its backing read-only, so the guest's writes are refused by the - /// device rather than by the driver. Nothing else in this suite can make a - /// real device say no to an I/O the driver was right to issue. - readonly: bool, - /// Attach it *ahead* of the boot stick. Which disk comes first is a shape - /// dimension the moment one of them can be refused: a driver that hands the - /// pool block of a failed bind to the next disk is only observable when the - /// failure is first. - before_boot_stick: bool, - /// The bus it is on, where that is not [`Shape::storage_bus`]: a machine - /// that boots off NVMe has no storage bus. - bus: Option<&'static str>, - /// Its serial number string, where QEMU's default — built from the port it - /// is on — would make the same stick another unit on another port. - pub serial: Option<&'static str>, -} - -impl UsbDisk { - /// The nominal 32 GiB stick this suite's storage tests are staged on, and - /// what a profile carries when it just needs a disk it may write to. - const DATA: Self = Self { - bytes: USB_STICK_BYTES, - lba_bytes: 512, - readonly: false, - before_boot_stick: false, - bus: None, - serial: None, - }; - /// A 3 TB external disk, which this driver has to refuse by name rather - /// than serve the first 2 TiB of. - const HUGE: Self = Self { bytes: USB_HUGE_BYTES, ..Self::DATA }; -} - -/// QEMU's name for the `i`-th data disk's backing, and for the device in front -/// of it. Derived from the position rather than declared, so a profile cannot -/// give two disks one name. -fn usb_drive_id(i: usize) -> String { - format!("usbdisk{i}") -} - -/// The device id, which is what `device_del` names. -pub fn usb_device_id(i: usize) -> String { - format!("usbdev{i}") -} - -/// The boot stick's device id. -/// -/// The data disks have carried one since a test first had to unplug one; the -/// stick the machine booted from had none, so the one device whose removal -/// takes `/boot` and `/log` with it was the one the host could not name — which -/// is the removal the owner's machine dies on. +/// The boot stick's device id: the removal the owner's machine dies on is the +/// one device whose removal takes `/boot` and `/log` with it. pub const BOOT_STICK_ID: &str = "bootstick"; /// The boot stick's serial number string. Stated rather than left to QEMU, @@ -1403,36 +877,10 @@ pub const BOOT_STICK_ID: &str = "bootstick"; /// what a test moving it has to be able to say is not so. pub const BOOT_STICK_SERIAL: &str = "TOYOS0BOOTSTICK1"; -/// What every profile but [`Profile::MetalDisk`] gives the guest. Large -/// enough for a filesystem, small enough that a boot formats it quickly. +/// What every x86-64 profile gives the guest. Large enough for a filesystem, +/// small enough that a boot formats it quickly. pub const NVME_SMALL: u64 = 128 * 1024 * 1024; -/// What every namespace but [`Profile::NvmeWideSector`]'s reports — QEMU's -/// implicit default, and the T14's. -const NVME_LBA_DEFAULT: u32 = 512; - -/// The data stick every USB storage profile but [`Profile::UsbDiskHuge`] -/// carries: a nominal 32 GiB stick, the size of the class of device this -/// project boots from. Chosen rather than measured off one part — but not a -/// token number either, because the last 4 KiB block on it sits at sector -/// 67,108,856, which needs 27 bits of LBA. A 128 MiB scratch image needs 18 -/// and could not tell a truncated LBA field from a correct one. -pub const USB_STICK_BYTES: u64 = 32 * 1024 * 1024 * 1024; - -/// A 3 TB external USB disk: a device that exists, and one this driver cannot -/// address. At 512-byte sectors it has 6,442,450,944 of them, so READ(10)'s -/// 32-bit LBA is a bit short and READ CAPACITY(10) cannot report the size — -/// which is the only configuration in which the 16-byte form runs. -pub const USB_HUGE_BYTES: u64 = 3 * 1024 * 1024 * 1024 * 1024; - -/// The T14 Gen 2's namespace, to the byte: 500,118,192 sectors of 512 B. -/// Taken from the laptop's own boot line rather than rounded from "244 GB", -/// so a test that asserts on the block count is asserting against the machine -/// that gets flashed. -pub const NVME_T14_BYTES: u64 = 500_118_192 * 512; -/// The same device as the kernel counts it: 62,514,774 blocks of 4 KiB. -pub const NVME_T14_BLOCKS: u64 = NVME_T14_BYTES / 4096; - impl Profile { fn shape(self) -> Shape { match self { @@ -1440,100 +888,31 @@ impl Profile { Self::Virt => Shape { vga: "std", panel: None, - gpu: None, virtio: Virtio::Absent, nic: Nic::Absent, xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", usb: &[], nvme_bytes: 0, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], iommu: None, }, Self::Headless => Shape { vga: "none", panel: None, - gpu: None, virtio: Virtio::Present, nic: Nic::Virtio, xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &["usb-kbd,bus=xhci.0"], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::HeadlessNoIommu => Shape { iommu: None, ..Self::Headless.shape() }, - Self::E1000e => Shape { nic: Nic::E1000e, ..Self::Headless.shape() }, - Self::E1000eNoServer => Shape { nic: Nic::E1000eNoServer, ..Self::Headless.shape() }, - Self::E1000eBesideIgb => Shape { nic: Nic::E1000eBesideIgb, ..Self::Headless.shape() }, - Self::VirtioNetNoMsix => Shape { - vga: "none", - panel: None, - gpu: None, - virtio: Virtio::Present, - nic: Nic::VirtioWithoutMsix, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", usb: &["usb-kbd,bus=xhci.0"], nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], iommu: Some(IOMMU_DEFAULT), }, Self::Gop => Shape { vga: "std", panel: None, - gpu: None, virtio: Virtio::Present, nic: Nic::Virtio, xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", usb: &["usb-kbd,bus=xhci.0"], nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::GopUsbDisk => Shape { usb_disks: &[UsbDisk::DATA], ..Self::Gop.shape() }, - Self::VirtioGpu => Shape { - // No VGA at all: firmware then publishes no GOP, and the one - // display the guest has is the one whose mode it can set. - vga: "none", - panel: None, - gpu: Some("virtio-gpu-pci"), - virtio: Virtio::Present, - nic: Nic::Virtio, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &["usb-kbd,bus=xhci.0"], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::Diskless => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - // Zero is the absence, not a zero-length disk: `nvme_args` - // emits no controller, no namespace and no backing file. - nvme_bytes: 0, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], iommu: Some(IOMMU_DEFAULT), }, Self::Metal => Shape { @@ -1543,450 +922,21 @@ impl Profile { // geometry the machine actually has and the one no default // expresses. panel: Some((1920, 1080)), - gpu: None, virtio: Virtio::Absent, nic: Nic::Absent, xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", usb: &[], nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], iommu: Some(IOMMU_DEFAULT), }, - Self::MetalNoUsb => Shape { - vga: "none", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[], - storage_bus: "", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - // Zero `nvme_bytes` beside an empty `xhci` is the absence of a second disk, not an empty one. - Self::InternalDisk => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[], - storage_bus: "", - usb: &[], - nvme_bytes: 0, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - // Two keyboards and two pointers, because the collision this - // stages is between devices of the same HID class; a hub for a - // second non-HID device, since it needs no backing file and the - // driver has to walk past it exactly as it walks past the stick. - Self::MetalUsb => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_WIDE], - storage_bus: "xhci.0", - usb: &[ - "usb-kbd,bus=xhci.0", - "usb-kbd,bus=xhci.0", - "usb-mouse,bus=xhci.0", - "usb-tablet,bus=xhci.0", - "usb-hub,bus=xhci.0", - ], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::MetalDisk => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_T14_BYTES, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::NvmeWideSector => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: 8192, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::UsbDisk => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[UsbDisk::DATA], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::UsbDisk4k => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[UsbDisk { lba_bytes: 4096, ..UsbDisk::DATA }], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::UsbDiskHuge => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[UsbDisk::HUGE], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::UsbDiskRefusedFirst => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[UsbDisk { before_boot_stick: true, ..UsbDisk::HUGE }], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::UsbDiskReadOnly => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[UsbDisk { readonly: true, ..UsbDisk::DATA }], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::UsbDiskCrowd => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[UsbDisk::DATA, UsbDisk::DATA], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - // The first controller carries nothing at all — not even the boot - // stick, which is on the second with the HID. That is the laptop - // exactly: a USB-A port is a PCH port, and the Thunderbolt block's - // controller is empty until something is plugged into it. It also - // means the disk index the block layer holds names a device on a - // controller that is not the first, which nothing else stages. - Self::MetalXhciSecond => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT, XHCI_SECOND], - storage_bus: "xhci1.0", - usb: &["usb-kbd,bus=xhci1.0", "usb-mouse,bus=xhci1.0"], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - // A hub ahead of the second controller's HID, so that controller's - // devices take the same slot ids as the first's: the boot stick is - // SuperSpeed and enumerates ahead of every USB2 device, and the hub - // stands in for it. Both mice therefore land on one slot id, which - // is the collision a slot-derived pointer source turns into a - // single button-merge entry. - Self::MetalXhciBoth => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT, XHCI_SECOND], - storage_bus: "xhci.0", - usb: &[ - "usb-kbd,bus=xhci.0", - "usb-mouse,bus=xhci.0", - "usb-hub,bus=xhci1.0", - "usb-kbd,bus=xhci1.0", - "usb-kbd,bus=xhci1.0", - "usb-mouse,bus=xhci1.0", - ], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - // The machine does no USB storage I/O whatsoever: an empty - // `storage_bus` puts the boot volume on NVMe, and the first - // controller has no interrupt mechanism, so the driver refuses it - // and never polls it. That is load-bearing, not decoration: - // `wait_transfer` drains the *whole* event ring and dispatches - // every HID report in it, so a keyboard on any polled controller - // delivers on the back of somebody else's transfer whether or not - // its own interrupt works. Measured — the first version of this - // profile put storage and HID on one controller and passed with - // MSI deliberately left disabled. - Self::MetalXhciMsi => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_NO_IRQ_FIRST, XHCI_MSI_ONLY], - storage_bus: "", - usb: &["usb-kbd,bus=xhci1.0", "usb-mouse,bus=xhci1.0"], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - // Boot stick on the good controller, HID on the crippled one. A - // keyboard is what makes the absence assertion mean something: - // the driver has a device it would otherwise bind and announce. - Self::MetalXhciNoIrq => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT, XHCI_NO_IRQ_SECOND], - storage_bus: "xhci.0", - usb: &["usb-kbd,bus=xhci1.0", "usb-mouse,bus=xhci1.0"], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::MetalXhciDeaf => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "", - usb: &["usb-kbd,bus=xhci.0"], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - Self::MetalHotplug => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT, XHCI_SECOND], - storage_bus: "xhci.0", - usb: &["usb-tablet,bus=xhci.0"], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - // The three below are metal-sim with one field of the unit moved, - // so what differs between their boot logs and Metal's is the unit - // and nothing else on the machine. - Self::NoIommu => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: None, - }, - Self::IommuNarrow => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(Iommu { aw_bits: 39, ..IOMMU_DEFAULT }), - }, - Self::IommuNoIntremap => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &[], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(Iommu { intremap: false, ..IOMMU_DEFAULT }), - }, - Self::IommuEim => Shape { - iommu: Some(Iommu { eim: true, ..IOMMU_DEFAULT }), - ..Self::Metal.shape() - }, - Self::Hda => Shape { - virtio: Virtio::WithoutSound, - nic: Nic::Virtio, - hda: HDA_ONE, - ..Self::Headless.shape() - }, - Self::HdaTwoLive => Shape { - virtio: Virtio::WithoutSound, - nic: Nic::Virtio, - hda: HDA_TWO_LIVE, - ..Self::Headless.shape() - }, + Self::HeadlessNoIommu => Shape { iommu: None, ..Self::Headless.shape() }, } } - /// The unit this profile puts on the machine, or `None`. A test asserting - /// on what the guest decoded reads the expectation from here rather than - /// restating it, exactly as [`Profile::usb_disk`] does for the data stick. + /// The unit this profile puts on the machine, or `None`. pub fn iommu(self) -> Option { self.shape().iommu } - - /// Every `usb-storage` device this profile puts on the bus besides the - /// boot stick, in creation order. A test asserting on a size or a sector - /// size has to read it from here rather than restate it. - pub fn usb_disks(self) -> &'static [UsbDisk] { - self.shape().usb_disks - } - - /// The first of them, for the tests that stage exactly one. - pub fn usb_disk(self) -> Option<(u64, u32)> { - self.usb_disks().first().map(|d| (d.bytes, d.lba_bytes)) - } - - /// The panel this machine's firmware sets, and therefore the geometry the - /// kernel is handed; `None` where the machine has no VGA adapter at all. - /// A test reading pixels asks the machine here rather than the guest. - pub fn panel(self) -> Option<(u32, u32)> { - let shape = self.shape(); - (shape.vga == "std").then(|| shape.panel.unwrap_or(DEFAULT_PANEL)) - } -} - -/// What QEMU's stdvga advertises with no `xres`/`yres` of its own — measured -/// off a boot, not read off a default. -pub const DEFAULT_PANEL: (u32, u32) = (1280, 800); - -/// The image a boot is handed instead of the one it would build, and what -/// becomes of what the guest writes to it. -/// -/// **A guest writes to its own boot disk, and one of these has to be chosen.** -/// The loader counts this image's attempts into a file on the log partition -/// before every handoff (`bootloader/src/attempt.rs`), so a second launch of one -/// file is a *retry* and boots no kernel at all: `boot_partition_identity` -/// booted one crafted image twice and its second boot never reached a kernel. -/// There is no default, because the author is the only one who knows whether the -/// bytes the guest leaves behind are the verdict or the contamination. -pub enum Staged { - /// **Boot this file under a throwaway overlay; what the guest writes dies - /// with the guest.** The named file is never written, so a test may boot it - /// as many times as it likes and each boot starts where the one before it - /// did. - Pristine(PathBuf), - /// **Boot this file itself, because what the guest wrote to it is what the - /// test reads back.** One boot per file: nothing here clears what the last - /// one left, which is the point. - Written(PathBuf), - /// **One file across several boots of one test, built by the harness under - /// this name in this lane.** For the test whose subject *is* what one image - /// carries from a boot to the next; the first boot naming it builds it with - /// this call's own options and every later one boots what that left. - Carried(&'static str), -} - -impl Staged { - /// The file a test staged, or `None` for one the harness builds itself. - fn authored(&self) -> Option<&Path> { - match self { - Self::Pristine(path) | Self::Written(path) => Some(path), - Self::Carried(_) => None, - } - } } pub struct BootOptions { @@ -1998,166 +948,31 @@ pub struct BootOptions { /// because screen tests boot their own QEMU and several may exist at once. pub qmp: bool, /// Which of [`DECLARED_KERNEL_BUILDS`] this boot wants, and empty for the - /// kernel an image ships. Only a test whose subject *is* a build sets it — - /// `fpu-save-nothing`, and the `SYS_DEBUG` boot; everything else names an - /// actuator in [`BootOptions::kernel_params`] instead. - /// - /// It decides what this call *builds*, so it may not be set beside a - /// [`BootOptions::boot_image`], which is what the guest boots instead — - /// see [`refuse_a_staged_image_this_boot_did_not_ask_for`]. + /// kernel an image ships. Only a test whose subject *is* a build sets it; + /// everything else names an actuator in [`BootOptions::kernel_params`] + /// instead. pub kernel_features: &'static [&'static str], /// The actuators this boot arms, by the names `kernel/src/actuator.rs` /// declares. Non-empty selects the test kernel, which carries all of them. - /// - /// **The arming is in the image, not in this field.** The names are written - /// onto the ESP the build produces, so a boot that also supplies a - /// [`BootOptions::boot_image`] arms whatever *that* image was built with: - /// the two must agree and are refused when they do not. pub kernel_params: &'static [&'static str], - /// Give the machine an i8042 at all. `-machine q35,i8042=off` is the one - /// absence scenario QEMU can stage. - pub i8042: bool, /// Take the 16550 away, leaving the framebuffer as the guest's only /// channel out. Only [`Profile::Metal`] may set it -- the others carry /// their console on it or on virtio-serial. A muted guest has no marker /// to wait for and no `run_test` to drive, so it is observed with /// [`QemuInstance::screendump_while`] and nothing else. pub mute: bool, - /// Let this machine take a guest reset instead of exiting on one. - /// - /// **`-no-reboot` is the default and stays it**: it is what turns a triple - /// fault, a reset-register write and a power-off alike into a QEMU exit - /// whose `SHUTDOWN` reason a test can read, and every power test judges by - /// that reason. This is for the one claim that cannot be made that way — - /// that the boot *after* a reset is this loader again, reading what the boot - /// before it left — and a guest with it set runs until the harness kills it. - pub takes_the_reset: bool, - /// Keep the firmware's variables in this file, writable, instead of the - /// boot's own fresh copy of the template: a copy the test made, so what one - /// boot's loader writes — the anti-rollback floor, `BootNext` — is what the - /// next boot of the same machine reads. `None` is every other boot, whose - /// copy dies with the guest. - pub firmware_vars: Option, /// The console line that means the boot reached the state under test. /// Anything other than [`DEFAULT_READY`] also declares that a panic is the /// expected outcome rather than a boot failure -- the early-panic screen /// test never reaches userland at all. Ignored when [`BootOptions::mute`] /// is set, which leaves no console for a marker to arrive on. pub ready_marker: &'static str, - /// Boot against this disk image instead of the shared scratch one. - /// - /// The shared image is created by `create_sparse`, which designates it -- - /// so every ordinary test boots a disk the kernel is allowed to format, - /// and none of them can observe what it does with one it is not. This is - /// how a test hands the guest somebody else's disk. - pub nvme_image: Option, - /// Boot this disk image instead of the one this call would build, and say - /// what becomes of what the guest writes to it — see [`Staged`]. - /// - /// The built image is written fresh every boot and its GPT gets a fresh - /// random partition GUID with it, so a test that has to know what is on - /// the boot disk *before* the machine starts cannot use it — and asserting - /// on the partition table firmware read is exactly that. Such a test - /// builds the image itself, reads it, and hands it over here. - /// - /// **It replaces the image, so it replaces everything in it**: this call - /// builds nothing when one is set, and every field that would have decided - /// what went into that image has to agree with what is already in this one - /// — [`refuse_a_staged_image_this_boot_did_not_ask_for`]. - pub boot_image: Option, - /// Back the profile's data disks with these files instead of blank ones, - /// in the order the profile declares them. The USB gate stages a file - /// *before* the boot -- the bytes the guest is meant to find are written - /// there -- and reads it afterwards, so it has to name the file rather - /// than discover it. Short lists are allowed: the disks past the end get - /// the blank image their size would have given them anyway. - pub usb_images: Vec, - /// Have QEMU write every packet the first data disk is sent to this file - /// (`usb-storage`'s `pcap=`, usbmon's format): the bus's own record of what - /// a driver put on it, which no line the guest prints can be. Refused by - /// name on a profile with no data disk, where it would record nothing. - pub usb_pcap: Option, - /// Fail with EIO every read of the boot disk that covers this 512-byte - /// sector, through QEMU's `blkdebug` under the boot image's raw format, on - /// whichever bus the profile puts that disk: a disk error at a place the - /// test chose, which no well-formed image can stage. - pub boot_read_error: Option, - /// What the emulated RTC reads when the machine starts, as - /// `YYYY-MM-DDTHH:MM:SS`. - /// - /// The wall clock is a device the host can set, which is what makes the - /// kernel's reading of it checkable from outside the guest: with this - /// given, the name and the timestamp of the file the guest writes are both - /// predictable before the machine exists. `None` leaves QEMU's default, - /// which is the host's own clock in UTC — and leaves the argument off the - /// command line entirely, so every existing profile assertion sees the argv - /// it always saw. - pub rtc_base: Option<&'static str>, /// Files put on ROOT beside the image's own, each named by its /// ROOT-relative path — `share/pkg/x` is `/system/share/pkg/x` in the /// guest. A fixture the guest reads and no program in the image produces; /// the image is memoized on their names and bytes, so two boots staging /// different fixtures do not share one. pub extra_root_files: Vec<(String, Vec)>, - /// Forward this host port to the guest's TCP [`toyos_logstream::PORT`], - /// where `logd` serves the boot's log. - pub log_port: Option, - /// Put the host on the guest's own segment (`super::segment`): frames - /// it writes reach the NIC as if off the cable, and it sees every frame the - /// guest sends, through [`QemuInstance::segment`]. Refused by name on a - /// profile with no NIC. - pub segment: bool, - /// Forward this host port to the guest's TCP 22. **slirp is one-way - /// without it**: nothing on the host can open a connection into the guest - /// unless QEMU is told which port to translate. A profile with no NIC - /// carries no `-netdev` for it to reach. - pub ssh_port: Option, - /// Write every frame this machine's NIC sends or receives to this file, in - /// pcap. **The only way to read what the guest asked for**: a request the - /// server ignores reaches no log on either side. - pub wire_dump: Option, - /// A second NVMe controller, for a driver in userland, backed by this file. - /// - /// QEMU's NVMe under Intel's ids (`use-intel-id`, `8086:5845`), so a claim - /// names it; its MSI-X table in a BAR - /// of its own (`msix-exclusive-bar`), because a claim never maps the BAR - /// holding the table and NVMe keeps its registers in BAR 0; and its - /// namespace's write cache on, so the controller has a volatile cache a - /// flush has to issue Flush for. - pub userland_nvme: Option, - /// Have QEMU record every NVMe command it is sent, every completion it - /// posts, every write with its sectors, every flush it runs and every - /// controller start into this file: the device's own account - /// of what reached it, which no line a driver prints can be. - pub nvme_trace: Option, -} - -/// Where the guest sees the host under QEMU's user-mode networking, and where -/// the host sees the same servers. -pub const GUEST_VIEW_OF_HOST: &str = "10.0.2.2"; - -/// The loopback address the forwarded port is bound on. Loopback and not `*`: -/// a CI runner is on somebody's network and a test guest's sshd is not a -/// service anyone else may reach. -pub const SSH_FORWARD_HOST: &str = "127.0.0.1"; - -/// The `hostfwd` clause [`BootOptions::ssh_port`] adds to the `-netdev` -/// argument, spelled once so the boot and the assertion read the same string. -pub fn ssh_forward_argv(port: u16) -> String { - format!(",hostfwd=tcp:{SSH_FORWARD_HOST}:{port}-:22") -} - -/// A host port nothing is listening on, taken by binding and letting go. The -/// window between the two is unavoidable — QEMU opens its own listener — and a -/// boot that loses that race fails to connect rather than reaching another -/// socket, because the port is on loopback and every connection through it is -/// authenticated. -pub fn free_host_port() -> u16 { - std::net::TcpListener::bind((SSH_FORWARD_HOST, 0)) - .expect("a loopback port for the ssh forward") - .local_addr() - .expect("a bound listener has an address") - .port() } impl BootOptions { @@ -2191,24 +1006,9 @@ impl Default for BootOptions { qmp: false, kernel_features: &[], kernel_params: &[], - i8042: true, mute: false, - takes_the_reset: false, - firmware_vars: None, ready_marker: DEFAULT_READY, - nvme_image: None, - boot_image: None, - usb_images: Vec::new(), - usb_pcap: None, - boot_read_error: None, - rtc_base: None, extra_root_files: Vec::new(), - log_port: None, - segment: false, - ssh_port: None, - wire_dump: None, - userland_nvme: None, - nvme_trace: None, } } } @@ -2218,29 +1018,6 @@ pub struct TestResult { pub name: String, pub exit_code: Option, pub stdout: String, - pub serial: String, - /// Every console line that arrived **before** this test announced itself. - /// - /// **It used to be dropped on the floor, and that is a hole in the capture - /// rather than a tidiness.** A boot's capture is `boot_log()` up to the - /// ready marker and then this function's `stdout`/`serial` from - /// `===TEST_START===` onwards; between those two points the reader thread - /// goes on delivering lines and nothing kept them. The window is not - /// hypothetical and it is not narrow — measured on `wall_clock_file`, - /// 2026-08-15: one run in three carried five real lines in it, including - /// `soundd: null sink idle` and the kernel's `spawn: /system/bin/test-runner` - /// record, so the ready marker fires before the runner is even loaded and - /// every daemon still finishing its startup writes into a hole. - /// - /// That is how a `logd:` line went missing from a `wall_clock_file` capture - /// while the *next* line logd writes was present: the two are either side of - /// a file creation on the log volume, which is milliseconds, and the window - /// closed between them. - /// - /// A caller that reads a daemon's startup out of a boot appends this to its - /// capture. It is separate from `serial` because `serial` means "while this - /// test ran". - pub before: String, /// Why the run did not finish, when it did not. /// /// A [`WaitVerdict`] and not a `String`, so that the sentence and the @@ -2248,288 +1025,32 @@ pub struct TestResult { /// Every arm that formats this gets the report for free, and there are /// fifty-two of them that were never going to be edited one at a time. pub error: Option, - /// Whether the guest ever announced *this* test. - /// - /// The in-guest runner reads one command, prints `===TEST_START ` and - /// spawns; so a test that never started is a guest that never got as far as - /// reading its command, which is a different thing from a test that ran and - /// hung. On a shared boot the two want different answers — the first is - /// about the boot, the second about the test. - pub started: bool, -} - -impl TestResult { - /// The guest is not answering any more: this test's turn came, its whole - /// ceiling passed, and it was never even announced. - pub fn boot_stopped_answering(&self) -> bool { - !self.started && self.error.is_some() - } -} - -/// Every byte the guest's console has produced, the unfinished last line -/// included — **a view, not a queue: reading it takes nothing from anyone.** -/// -/// The line channel is a `Receiver`, so a wait on it consumes: a helper that -/// drained lines looking for its own evidence would take the marker its caller's -/// assertion is waiting for. That is the whole reason this exists, and it is why -/// `shell_type_line` in `tests/toyos.rs` reads the guest's echo of a typed line -/// from here. -/// -/// It also carries what the line channel structurally cannot. A surface owner -/// mirrors the shell's bytes to its own stdout and std buffers that by line, so -/// a prompt — `"{cwd}> "`, no newline — reaches a host reading bytes and no host -/// reading lines. -#[derive(Clone)] -pub struct ConsoleStream(Arc>>); - -impl ConsoleStream { - fn new() -> Self { - Self(Arc::new(Mutex::new(Vec::new()))) - } - - /// How much the guest has said so far: the mark a caller takes before it - /// injects, so that what it reads back afterwards is its own doing. - pub fn mark(&self) -> usize { - self.0.lock().expect("the console stream lock is never held across a panic").len() - } - - /// Everything the guest has said since byte `at`. - /// - /// Lossy, and it has to be: `at` is a byte offset a caller took between two - /// writes and the tail is whatever has arrived since, so both ends can fall - /// inside a multi-byte character that is not finished yet. - pub fn since(&self, at: usize) -> String { - let buf = self.0.lock().expect("the console stream lock is never held across a panic"); - String::from_utf8_lossy(&buf[at.min(buf.len())..]).into_owned() - } } pub struct QemuInstance { child: Child, /// What ends QEMU when this process dies without dropping this. _tether: Tether, - stdin: BufWriter, - rx: Receiver, - console: ConsoleStream, - _reader_thread: thread::JoinHandle, - uart_log: PathBuf, - nvme: NvmeClaim, - sockets: Sockets, - screendump: PathBuf, - /// The image this boot built for itself, which is the only one it may - /// delete: a [`BootOptions::boot_image`] belongs to the test that staged it - /// and is often read back after the guest is gone. - own_boot_image: Option, - /// The variable store this boot copied for itself, on the same terms as - /// `own_boot_image`: a [`BootOptions::firmware_vars`] is the test's. - own_vars: Option, - boot_log: String, - /// Whether this boot armed `i8042-trace`, which is the only channel a - /// windowed shell has for saying it took a burst out of the device. - /// Kept so a caller that paces on it refuses a boot that cannot answer, - /// rather than waiting out a ceiling against a guest that was never asked - /// to speak. - i8042_trace: bool, - /// This guest's vCPU count, kept so its liveness ceilings can be widened by - /// its own oversubscription on a host with fewer cores than vCPUs — see - /// [`oversubscription`] and [`QemuInstance::budget`]. Boot-derived - /// [`host_scale`] cannot see this: a boot is a mostly-serial workload and a - /// wide-SMP guest pays lock-holder preemption a boot never does. - smp: u32, - /// The host port [`BootOptions::ssh_port`] forwarded into this guest, kept - /// so a boot several tests share can tell each of them which port it took. - ssh_port: Option, - /// The test binaries this boot put on ROOT, by the name `run` takes; `None` - /// for a staged image, whose contents its builder chose. - carried: Option>, -} - -/// The test binaries one boot carries onto ROOT, out of the suite's catalogue. -pub struct Carried { - pub c: Vec<(String, Vec)>, - pub rust: Vec<(String, Vec)>, -} - -impl Carried { - /// Each binary's size, by the name [`carrying`] takes. - pub fn sizes(&self) -> std::collections::BTreeMap { - let c = self.c.iter().map(|(name, data)| (format!("test_c_{name}"), data.len())); - let rust = self.rust.iter().map(|(name, data)| { - let key = if name.ends_with(".so") { name.clone() } else { format!("test_rs_{name}") }; - (key, data.len()) - }); - c.chain(rust).collect() - } - - pub fn bytes(&self) -> usize { - self.c.iter().chain(&self.rust).map(|(_, data)| data.len()).sum() - } -} - -/// What a boot that runs `names` (`test_rs_`, `test_c_`) carries. -/// -/// **ROOT is held whole in the guest's memory, so a binary on it costs the -/// guest whether it runs or not.** The closure is over what the named binaries -/// name in turn: a child a binary spawns and a library it links or `dlopen`s -/// appear in its bytes by file name, so every catalogue name found there is -/// carried too. A name the catalogue does not hold panics. -pub fn carrying<'n>( - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - names: impl IntoIterator, -) -> Carried { - let mut catalogue: std::collections::BTreeMap))> = - std::collections::BTreeMap::new(); - for bin in c_bins { - catalogue.insert(format!("test_c_{}", bin.0), (true, bin)); - } - for bin in rust_bins { - let key = - if bin.0.ends_with(".so") { bin.0.clone() } else { format!("test_rs_{}", bin.0) }; - catalogue.insert(key, (false, bin)); - } - let mut todo: Vec = Vec::new(); - for name in names { - assert!( - catalogue.contains_key(name), - "[qemu] a boot names {name:?} and the suite built no such binary" - ); - todo.push(name.to_string()); - } - let mut taken: BTreeSet = BTreeSet::new(); - while let Some(name) = todo.pop() { - if taken.insert(name.clone()) { - todo.extend(named_in(&catalogue[&name].1 .1, &catalogue)); - } - } - let mut carried = Carried { c: Vec::new(), rust: Vec::new() }; - for name in &taken { - let (is_c, bin) = catalogue[name]; - if is_c { carried.c.push(bin.clone()) } else { carried.rust.push(bin.clone()) } - } - carried -} - -/// Every catalogue name that starts somewhere in `bytes`, the longest where -/// two do: string literals sit end to end in `.rodata`, so what follows a name -/// is as often the next literal's first byte as a terminator. -fn named_in(bytes: &[u8], catalogue: &std::collections::BTreeMap) -> Vec { - let word = |b: u8| b.is_ascii_alphanumeric() || b == b'_' || b == b'.'; - let widest = catalogue.keys().map(String::len).max().unwrap_or(0); - let mut found = Vec::new(); - let mut at = 0; - while at < bytes.len() { - let rest = &bytes[at..]; - if !(rest.starts_with(b"test_rs_") || rest.starts_with(b"test_c_") || rest.starts_with(b"lib")) - { - at += 1; - continue; - } - let run = rest.iter().take(widest).position(|&b| !word(b)).unwrap_or(widest.min(rest.len())); - let longest = (1..=run) - .rev() - .filter_map(|end| std::str::from_utf8(&rest[..end]).ok()) - .find(|candidate| catalogue.contains_key(*candidate)); - match longest { - Some(name) => { - at += name.len(); - found.push(name.to_string()); - } - None => at += 1, - } - } - found -} - -/// The bootable disk image a boot with these arguments would use. -/// -/// Public because a test that has to know what is on the boot disk *before* -/// the machine starts — or has to put something there — cannot let -/// `boot_with_options` build it: the image is written fresh every boot and its -/// GPT gets a new random partition GUID with it. Such a test builds the image -/// here, works on it, and hands it back through [`BootOptions::boot_image`]. -pub fn build_boot_image( - test_crate: &Path, - c_tests: &[(String, Vec)], - rust_tests: &[(String, Vec)], - kernel_params: &[&str], -) -> Vec { - build_boot_image_carrying(test_crate, c_tests, rust_tests, &[], kernel_params) -} - -/// [`build_boot_image`] with files put on ROOT beside the image's own, each -/// named by its ROOT-relative path: what [`BootOptions::extra_root_files`] does -/// for an image the boot builds, which a staged image has to carry itself. -pub fn build_boot_image_carrying( - test_crate: &Path, - c_tests: &[(String, Vec)], - rust_tests: &[(String, Vec)], - staged: &[(String, Vec)], - kernel_params: &[&str], -) -> Vec { - // A parameter carrying a value is one the *shipping* kernel answers to, so - // it selects no kernel: an image built with no actuator must be the image a - // flashed stick would be. - let kernel: &[&str] = - if kernel_params.iter().all(|p| toyos_build::build::is_valued_param(p)) { - &[] - } else { - toyos_build::build::TEST_KERNEL - }; - build_boot_image_with(SUITE_ARCH, test_crate, c_tests, rust_tests, staged, kernel, kernel_params, false) -} - -/// Refuse a staged [`BootOptions::boot_image`] that is not the image this -/// boot's other options describe. -/// -/// **A staged image replaces the image this call would have built, so every -/// option that decides what goes *into* an image decides nothing here.** The -/// guest boots the kernel that image ships, armed with the actuators it was -/// built with, and until this refused, a test that set `kernel_params` beside a -/// `boot_image` built without them got an unarmed guest, a pass, and a summary -/// line counting the arm as taken. -/// -/// A green run with an inert arm is the worst kind of harness defect, because -/// every negative control staged through one proves nothing. -/// -/// The image was built by this same process moments earlier and carries its own -/// list on its own ESP, so the question is asked of the image rather than of -/// whoever built it — a name is a name on this side of the wire too, and the -/// guest need not be started to know which kind it is. -fn refuse_a_staged_image_this_boot_did_not_ask_for(image: &Path, options: &BootOptions) { - assert!( - options.kernel_features.is_empty(), - "[qemu] this boot asks for the kernel build {:?} and hands the guest {}; a staged image \ - ships the kernel it was built with and this call builds nothing, so the request would \ - be inert", - options.kernel_features, - image.display(), - ); - assert!( - !options.debug_wait, - "[qemu] this boot asks for the {:?} build and hands the guest {}; a staged image ships \ - the kernel it was built with and this call builds nothing, so the request would be \ - inert", - toyos_build::build::DEBUG_KERNEL_BUILD, - image.display(), - ); - assert!( - options.extra_root_files.is_empty(), - "[qemu] this boot stages {} file(s) onto ROOT and hands the guest {}; a staged image \ - carries the files it was built with and this call builds nothing, so the fixture would \ - never reach the guest", - options.extra_root_files.len(), - image.display(), - ); - let params = options.params(); - let asked: Vec<&str> = params.iter().map(String::as_str).collect(); - if let Some(why) = toyos_build::image::param_conflict(image, &asked) { - panic!( - "[qemu] {why}. `BootOptions::boot_image` replaces the image this call would have \ - built, so `kernel_params` cannot arm a guest booting one: build the staged image \ - with the same list — `qemu::build_boot_image` takes it — or drop the field" - ); - } + stdin: BufWriter, + rx: Receiver, + _reader_thread: thread::JoinHandle, + /// Held for the claim: one live guest per NVMe image. + _nvme: NvmeClaim, + sockets: Sockets, + screendump: PathBuf, + /// The image this boot built for itself. + boot_image: PathBuf, + /// The variable store this boot copied for itself. + vars: PathBuf, + boot_log: String, + /// This guest's vCPU count, kept so its liveness ceilings can be widened by + /// its own oversubscription on a host with fewer cores than vCPUs — see + /// [`oversubscription`] and [`QemuInstance::budget`]. Boot-derived + /// [`host_scale`] cannot see this: a boot is a mostly-serial workload and a + /// wide-SMP guest pays lock-holder preemption a boot never does. + smp: u32, + /// The test binaries this boot put on ROOT, by the name `run` takes. + carried: BTreeSet, } /// Which of [`DECLARED_KERNEL_BUILDS`] this boot wants. @@ -2537,10 +1058,6 @@ fn refuse_a_staged_image_this_boot_did_not_ask_for(image: &Path, options: &BootO /// **A parameter never decides a build.** Every actuator lives in the one test /// kernel, so asking for one selects that kernel and nothing more; the third /// build is asked for by name and by one test. -/// -/// A boot handed a [`BootOptions::boot_image`] builds nothing at all, and this -/// then answers what that image already carries: the two agree or the boot was -/// refused before it got here. fn kernel_of(options: &BootOptions) -> Vec<&'static str> { if options.kernel_params.is_empty() { return options.kernel_features.to_vec(); @@ -2685,25 +1202,12 @@ fn push_user_half(line: &str, stdout: &mut String) { const END_MARKER: &str = "===TEST_END "; impl QemuInstance { - /// Build everything and boot QEMU with test binaries on ROOT. - /// `test_crate` is the path to the test crate (must contain a `system.toml`). - pub fn boot( - test_crate: &Path, - c_tests: &[(String, Vec)], - rust_tests: &[(String, Vec)], - ) -> Self { - Self::boot_with_options(test_crate, c_tests, rust_tests, BootOptions::default()) - } - pub fn boot_with_options( test_crate: &Path, c_tests: &[(String, Vec)], rust_tests: &[(String, Vec)], options: BootOptions, ) -> Self { - if let Some(staged) = options.boot_image.as_ref().and_then(Staged::authored) { - refuse_a_staged_image_this_boot_did_not_ask_for(staged, &options); - } let mut features: Vec<&str> = kernel_of(&options); if options.debug_wait { features.push(toyos_build::build::DEBUG_KERNEL_BUILD); @@ -2720,113 +1224,54 @@ impl QemuInstance { // image file is not a slow test, it is a guest reading bytes another // boot is in the middle of writing — and the lane directory alone would // not settle it, since one test may hold two instances at once. - // - // **A staged image builds nothing.** What this call would have built is - // the image the guest does not boot, and building it anyway cost a - // kernel build the run then reported as one it had made — see - // [`refuse_a_staged_image_this_boot_did_not_ask_for`] for what that - // report was worth. - // - // The second half of each arm is what this guest may delete when it - // goes: a file the test staged is often read back after the guest is - // gone, and a carried one belongs to the boots after this. - let build_here = || { - let params = options.params(); - let params: Vec<&str> = params.iter().map(String::as_str).collect(); - build_boot_image_with( - options.profile.arch(), - test_crate, - c_tests, - rust_tests, - &options.extra_root_files, - &features, - ¶ms, - options.debug_wait, - ) - }; - let carried = match &options.boot_image { - Some(Staged::Written(_) | Staged::Pristine(_)) => None, - Some(Staged::Carried(_)) | None => Some( - c_tests + let boot_image = test_dir.join(format!("boot-{seq}.img")); + let params = options.params(); + let params: Vec<&str> = params.iter().map(String::as_str).collect(); + let image = build_boot_image_with( + options.profile.arch(), + test_crate, + c_tests, + rust_tests, + &options.extra_root_files, + &features, + ¶ms, + options.debug_wait, + ); + fs::write(&boot_image, image).expect("Failed to write test boot image"); + let carried = c_tests + .iter() + .map(|(name, _)| format!("test_c_{name}")) + .chain( + rust_tests .iter() - .map(|(name, _)| format!("test_c_{name}")) - .chain( - rust_tests - .iter() - .filter(|(name, _)| !name.ends_with(".so")) - .map(|(name, _)| format!("test_rs_{name}")), - ) - .collect(), - ), - }; - let (boot_image, own_boot_image) = match &options.boot_image { - // Both boot the file the test staged; what tells them apart is the - // `snapshot=on` `qemu_command` puts on the drive for a `Pristine` - // one, which is where that guest's writes go and die. - Some(Staged::Written(staged) | Staged::Pristine(staged)) => (staged.clone(), None), - Some(Staged::Carried(name)) => { - let path = test_dir.join(format!("carried-{name}.img")); - if !path.exists() { - fs::write(&path, build_here()).expect("Failed to write test boot image"); - } - (path, None) - } - None => { - let path = test_dir.join(format!("boot-{seq}.img")); - fs::write(&path, build_here()).expect("Failed to write test boot image"); - (path.clone(), Some(path)) - } - }; + .filter(|(name, _)| !name.ends_with(".so")) + .map(|(name, _)| format!("test_rs_{name}")), + ) + .collect(); - // **Every boot that names no image gets a blank DATA volume**, so what - // one boot leaves under `/home` — sshd's host identity, a package, a - // cache — is never the premise of whatever test the lane runs next. A - // boot that reads what an earlier one wrote passes that image as - // `nvme_image`. The lane's one file is remade rather than a file per - // boot, so a test can still read the device after its guest is gone. + // **Every boot gets a blank DATA volume**, so what one boot leaves under + // `/home` — sshd's host identity, a package, a cache — is never the + // premise of whatever test the lane runs next. The lane's one file is + // remade rather than a file per boot. // // One live guest per image, claimed here rather than discovered from // QEMU's stderr after the second process has already exited — see // [`NvmeClaim`] — and claimed before the remaking, which truncates. let nvme_bytes = options.profile.shape().nvme_bytes; - let (nvme_image, blank) = match &options.nvme_image { - Some(path) => (path.clone(), false), + let nvme_image = if nvme_bytes == 0 { // A profile with no controller gets no backing file either; the // path is never passed to QEMU. - None if nvme_bytes == 0 => (test_dir.join("no-nvme"), false), - None => (test_dir.join(format!("test-nvme-{nvme_bytes}.img")), true), + test_dir.join("no-nvme") + } else { + test_dir.join(format!("test-nvme-{nvme_bytes}.img")) }; let nvme = if nvme_bytes == 0 { NvmeClaim::unattached(&nvme_image) } else { - NvmeClaim::take(&nvme_image).unwrap_or_else(|why| panic!("[qemu] {why}")) + let claim = NvmeClaim::take(&nvme_image).unwrap_or_else(|why| panic!("[qemu] {why}")); + toyos_build::build::create_sparse(claim.path(), nvme_bytes); + claim }; - if blank { - toyos_build::build::create_sparse(nvme.path(), nvme_bytes); - } - - // Named by size and block size for the same reason the namespace is: - // a stamped image is stamped for one geometry, and handing it to a - // profile that declares another is the mistake the stamp exists to - // catch rather than one to make here. - let usb_images: Vec = options - .profile - .usb_disks() - .iter() - .enumerate() - .map(|(i, disk)| match options.usb_images.get(i) { - Some(path) => path.clone(), - None => { - let path = - test_dir.join(format!("test-usb-{}-{}.img", disk.bytes, disk.lba_bytes)); - if !path.exists() { - let file = fs::File::create(&path).expect("create the USB disk image"); - file.set_len(disk.bytes).expect("size the USB disk image"); - } - path - } - }) - .collect(); let sockets = Sockets::new(&options); let screendump = test_dir.join(format!("screen-{seq}.ppm")); @@ -2837,26 +1282,12 @@ impl QemuInstance { let uart_log = test_dir.join(format!("uart-{seq}.log")); let _ = fs::remove_file(&uart_log); - let (firmware_vars, own_vars) = match &options.firmware_vars { - Some(vars) => (vars.clone(), None), - None => { - let vars = test_dir.join(format!("vars-{seq}.fd")); - toyos_build::firmware::of(options.profile.arch()) - .and_then(|firmware| firmware.fresh_vars(&vars)) - .unwrap_or_else(|why| panic!("[qemu] {why}")); - (vars.clone(), Some(vars)) - } - }; + let vars = test_dir.join(format!("vars-{seq}.fd")); + toyos_build::firmware::of(options.profile.arch()) + .and_then(|firmware| firmware.fresh_vars(&vars)) + .unwrap_or_else(|why| panic!("[qemu] {why}")); - let qemu = qemu_command( - &boot_image, - nvme.path(), - &usb_images, - &uart_log, - &sockets.dir, - &firmware_vars, - &options, - ); + let qemu = qemu_command(&boot_image, nvme.path(), &uart_log, &sockets.dir, &vars, &options); spawn_and_wait_ready( qemu, &options, @@ -2866,8 +1297,8 @@ impl QemuInstance { nvme, sockets, screendump, - own_boot_image, - own_vars, + boot_image, + vars, carried, }, ) @@ -2972,70 +1403,6 @@ impl QemuInstance { } } - /// [`Self::screendump_while`], but a guest still *painting* is still working. - /// - /// The screen-channel form of what [`ceiling_verdict`] does for - /// [`Self::run_test_paced`] on serial: past the budgeted deadline the wait - /// does not give up while the framebuffer keeps *changing*. A console - /// rendering slowly under a loaded `smp:2` runner is making progress, which - /// is the case whose paint "never arrived in the window" while the guest was - /// alive — the budget-scaled deadline undercounts a later moment in the run - /// exactly as the serial ceiling did. Only a screen *frozen* for - /// [`GUEST_QUIET`] past the deadline ends the wait; `done` firing ends it at - /// once, so a passing caller is untouched - /// and a real bug (the paint that should not be there, and stays) still fires - /// its assertion, a frozen-screen `GUEST_QUIET` later. - /// - /// **Only for a config whose screen freezes when idle** — no compositor; - /// `/system/bin/console` repaints on I/O alone. A compositor's cursor blink and its - /// once-a-second taskbar clock never let the screen freeze, so such a caller - /// would wait the whole backstop when its `done` never comes and keeps the - /// plain [`Self::screendump_while`] (which is also why the `screen_blocked_dump` - /// retry loop, whose timeout is a deliberate re-send signal, must not use - /// this). - /// - /// Reuses the one classifier so the two channels cannot drift: `dying` is the - /// serial path's alone, and a halted kernel freezes the screen and is caught - /// by the freeze here. - pub fn screendump_while_rendering( - &mut self, - timeout: Duration, - interval: Duration, - done: impl Fn(&super::screen::Ppm) -> bool, - ) -> super::screen::Ppm { - let ceiling = budget_smp(timeout, self.smp); - let start = Instant::now(); - let mut last_change = start; - let mut prev: Option> = None; - loop { - let dump = self.screendump(); - if done(&dump) { - return dump; - } - let now = Instant::now(); - if prev.as_deref() != Some(dump.pixels.as_slice()) { - last_change = now; - prev = Some(dump.pixels.clone()); - } - if ceiling_verdict( - None, - now.duration_since(start), - ceiling, - now.duration_since(last_change), - 0, - ) - .is_some() - { - return dump; - } - thread::sleep(interval); - } - } - - pub fn pid(&self) -> u32 { - self.child.id() - } - /// Every console line the guest printed before the ready marker. /// /// The kernel's own boot lines sit in the log ring until the scheduler @@ -3048,81 +1415,6 @@ impl QemuInstance { &self.boot_log } - /// The host port this boot forwarded into the guest's TCP 22. Panics - /// rather than returning an option: a `None` here would become a connection - /// refused several layers away from the option that was not set. - pub fn ssh_port(&self) -> u16 { - self.ssh_port.expect("this guest was booted without BootOptions { ssh_port }") - } - - /// Everything the guest put on the 16550 before it switched to the - /// virtio-console — the only record a guest that died early leaves. - pub fn uart_log(&self) -> String { - fs::read_to_string(&self.uart_log).unwrap_or_default() - } - - /// The guest's console byte for byte, unfinished last line included — see - /// [`ConsoleStream`]. - pub fn console_stream(&self) -> &ConsoleStream { - &self.console - } - - /// Whether the kernel will report every i8042 drain on this boot. - pub fn i8042_trace_armed(&self) -> bool { - self.i8042_trace - } - - /// Wait for QEMU to exit within `by`: its console closing is the event, and - /// the process is reaped after it. Answers what the guest said on the way. - /// A file QEMU finishes only at its exit is whole once this answers, and is - /// still there until this instance is dropped. - pub fn await_exit(&mut self, by: Duration) -> Result { - let deadline = Instant::now() + by; - let mut said = String::new(); - loop { - let left = deadline.checked_duration_since(Instant::now()).unwrap_or_default(); - match self.rx.recv_timeout(left) { - Ok(line) => { - said.push_str(&line); - said.push('\n'); - } - Err(RecvTimeoutError::Disconnected) => break, - Err(RecvTimeoutError::Timeout) => { - return Err(format!("QEMU had not exited {} s after it was asked to\n{said}", by.as_secs())) - } - } - } - let status = self.child.wait().map_err(|e| format!("QEMU could not be waited for: {e}"))?; - if !status.success() { - return Err(format!("QEMU exited {status}\n{said}")); - } - Ok(said) - } - - /// The NVMe backing file. It is what the *device* received, so it is the - /// only place a storage assertion can stand outside the guest's own - /// account of itself. - pub fn nvme_image(&self) -> &Path { - self.nvme.path() - } - - /// End this guest and hand back the proof its lane is free. - /// - /// **This is the only way to boot a replacement**, because [`LaneFree`] is - /// the only thing a replacement can be built from and this is the only - /// thing that makes one out of a guest. Taking `self` is the whole of it: - /// `qemu = boot()` launched the new QEMU while the old instance still held - /// the lane's `test-nvme-*.img` open for write, the new one exited 1 on - /// QEMU's own lock, and `wait_for_ready`'s panic escaped the shared block — - /// 129 of one run's 131 reds carried that one sentence on 2026-08-17. - /// Deterministic, not a race in the sense of a window: the old guest is - /// always still alive at that point, so every shared-boot reboot since the - /// mechanism landed on 2026-08-08 died this way. - pub fn shutdown(self) -> LaneFree { - drop(self); - LaneFree(()) - } - pub fn stdin_mut(&mut self) -> &mut BufWriter { &mut self.stdin } @@ -3147,8 +1439,7 @@ impl QemuInstance { /// moment QEMU exits and the reader disconnects, so the ceiling there costs /// nothing. A guest the fatal path has halted does not exit — every CPU is /// stopped and the process stays up — so the drain pays the whole ceiling - /// waiting for a machine that will never speak again. `double_fault_stack` - /// spent twenty seconds of every run that way, which was 80% of it. + /// waiting for a machine that will never speak again. /// /// Here the duration *is* a liveness ceiling — the marker is what ends /// it — so it scales. @@ -3183,24 +1474,6 @@ impl QemuInstance { self.sockets.qmp.as_deref().expect("qmp_socket needs BootOptions { qmp: true }") } - /// Stand on this guest's segment; it needs `BootOptions { segment: true }`. - pub fn segment(&self) -> Result { - self.sockets.segment.as_ref().expect("segment needs BootOptions { segment: true }").open() - } - - /// [`budget`] for a host-side wait on *this* guest, widened by the guest's - /// own vCPU oversubscription. - /// - /// A test that polls the framebuffer or drains serial in its own loop — - /// rather than through [`Self::run_test_paced`] — reaches for a deadline, - /// and a deadline is a claim about the host. The free [`budget`] cannot see - /// how wide this guest is; this can, so an `smp:8` guest's poll loop is - /// given the `smp/cores` extra room a mostly-serial boot never priced. On a - /// host with a core per vCPU it is exactly [`budget`]. - pub fn budget(&self, one_guest: Duration) -> Duration { - budget_smp(one_guest, self.smp) - } - pub fn run_test(&mut self, name: &str, timeout: Duration) -> TestResult { self.run_test_hooked(name, timeout, "", |_| {}) } @@ -3247,22 +1520,20 @@ impl QemuInstance { // `run [args...]`, and the markers carry only the binary name. let want = name.split_whitespace().next().unwrap_or(name); - if let Some(carried) = &self.carried { - let harness = want.starts_with("test_rs_") || want.starts_with("test_c_"); - assert!( - !harness || carried.contains(want), - "[qemu] `run {want}` on a boot whose ROOT does not carry it: a boot carries the \ - test binaries its task names (`CARRIES` in tests/toyos.rs), and this one \ - carries {carried:?}" - ); - } + let harness = want.starts_with("test_rs_") || want.starts_with("test_c_"); + assert!( + !harness || self.carried.contains(want), + "[qemu] `run {want}` on a boot whose ROOT does not carry it: a boot carries the test \ + binaries its caller handed it, and this one carries {:?}", + self.carried + ); let timeout = budget_smp(timeout, self.smp); let start = Instant::now(); let mut stdout = String::new(); let mut serial = String::new(); // Every line seen before this test announced itself. Kept, never - // dropped — `TestResult::before` is the argument. + // dropped. let mut before = String::new(); let mut in_test = false; // **Which of the two things the ceiling caught**: a guest that has said @@ -3296,10 +1567,7 @@ impl QemuInstance { name: name.to_string(), exit_code: None, stdout, - serial, - before, error: Some(error), - started: in_test, }; } @@ -3377,14 +1645,12 @@ impl QemuInstance { name: name.to_string(), exit_code, stdout, - serial, - before, error, - started: in_test, }; } else if !in_test { // **The window between two tests, kept rather than - // dropped.** See [`TestResult::before`]. + // dropped**: a daemon still finishing its startup writes + // into it, and a death report carries it. before.push_str(&line); before.push('\n'); } else if in_test { @@ -3408,10 +1674,7 @@ impl QemuInstance { name: name.to_string(), exit_code: None, stdout, - serial, - before, error: Some(error), - started: in_test, }; } } @@ -3442,7 +1705,7 @@ impl Drop for QemuInstance { let _ = fs::remove_file(&self.screendump); // A per-boot image is hundreds of megabytes and a full run makes ~76 of // them; the shared name used to make that one file. - for own in [&self.own_boot_image, &self.own_vars].into_iter().flatten() { + for own in [&self.boot_image, &self.vars] { let _ = fs::remove_file(own); } // `sockets` goes with the fields, after QEMU is reaped. @@ -3565,146 +1828,6 @@ impl Qmp { } } -/// QEMU's own account of why a guest stopped, off the `SHUTDOWN` event. Held -/// open across the stop: the event is emitted once and QEMU exits behind it, so -/// a connection opened afterwards finds nothing. -pub struct QmpShutdown(Qmp); - -impl QmpShutdown { - /// `budget` bounds the wait and is set here, while the peer is still there - /// to accept it: macOS refuses a `setsockopt` on a socket already closed. - pub fn open(socket: &Path, budget: Duration) -> Self { - let qmp = Qmp::connect(socket); - qmp.stream.set_read_timeout(Some(budget)).expect("qmp: the shutdown-event budget"); - Self(qmp) - } - - /// The `reason` the `SHUTDOWN` event names — `guest-reset`, - /// `guest-shutdown`, `host-signal` — or `None` if the guest never stopped. - pub fn reason(&mut self) -> Option { - use std::io::Read; - let qmp = &mut self.0; - loop { - if let Some(reason) = shutdown_reason(&qmp.pending) { - return Some(reason); - } - let mut buf = [0u8; 4096]; - match qmp.stream.read(&mut buf) { - // Budget spent, or the socket ended: what it had is in `pending`. - Ok(0) | Err(_) => return shutdown_reason(&qmp.pending), - Ok(n) => qmp.pending.extend_from_slice(&buf[..n]), - } - } - } -} - -/// Counts the guest resets QEMU reports, for a machine that takes its own -/// rather than exiting on the first (`BootOptions::takes_the_reset`). -/// -/// **`SHUTDOWN` is not available to such a guest.** `-no-reboot` is what turns a -/// reset into one, and every other power test judges by its reason; a guest that -/// keeps going emits `RESET` instead, and the *count* is what a chain is read -/// by — one is a kernel that reset itself, two is a loader pass that ended the -/// chain by resetting rather than returning to the boot manager. -pub struct QmpResets(Qmp); - -impl QmpResets { - /// `budget` bounds every wait and is set here, while the peer is still there - /// to accept it — as [`QmpShutdown::open`], and for the same reason. - pub fn open(socket: &Path, budget: Duration) -> Self { - let qmp = Qmp::connect(socket); - qmp.stream.set_read_timeout(Some(budget)).expect("qmp: the reset-event budget"); - Self(qmp) - } - - /// How many guest resets have arrived, waiting for up to `want` of them. - /// - /// Events queue on the socket from the moment it is connected, so a caller - /// that opened this before the guest reset reads them here whenever it asks. - pub fn seen(&mut self, want: usize) -> usize { - use std::io::Read; - let qmp = &mut self.0; - loop { - let seen = guest_resets(&qmp.pending); - if seen >= want { - return seen; - } - let mut buf = [0u8; 4096]; - match qmp.stream.read(&mut buf) { - // Budget spent, or the socket ended: what it had is in `pending`. - Ok(0) | Err(_) => return guest_resets(&qmp.pending), - Ok(n) => qmp.pending.extend_from_slice(&buf[..n]), - } - } - } -} - -/// A machine that takes its own resets, held at the next one: the guest's -/// reset pauses it with its memory — the black box — as the reset left it, so -/// a test can change what the next pass reads off the disk after the kernel's -/// last write and before the loader's first read, and then let it go. -pub struct QmpHold(Qmp); - -impl QmpHold { - /// The guest's next reset pauses the machine instead. - pub fn arm(socket: &Path) -> Self { - let mut qmp = Qmp::connect(socket); - qmp.execute("{\"execute\":\"set-action\",\"arguments\":{\"reboot\":\"shutdown\",\"shutdown\":\"pause\"}}"); - Self(qmp) - } - - /// Wait up to `budget` for the machine to stop at its reset. - pub fn held(&mut self, budget: Duration) -> Result<(), String> { - use std::io::Read; - let qmp = &mut self.0; - qmp.stream.set_read_timeout(Some(budget)).map_err(|e| format!("qmp: the hold's budget: {e}"))?; - let began = Instant::now(); - loop { - if qmp.pending.windows(6).any(|w| w == b"\"STOP\"") { - return Ok(()); - } - let mut buf = [0u8; 4096]; - match qmp.stream.read(&mut buf) { - Ok(n) if n > 0 && began.elapsed() < budget => qmp.pending.extend_from_slice(&buf[..n]), - _ => { - return Err(format!( - "the machine did not stop at a reset within {} s: {}", - budget.as_secs(), - String::from_utf8_lossy(&qmp.pending) - )) - } - } - } - } - - /// Take the held reset and run on, taking every later reset as before. - pub fn release(mut self) { - self.0.execute("{\"execute\":\"set-action\",\"arguments\":{\"reboot\":\"reset\",\"shutdown\":\"poweroff\"}}"); - self.0.execute("{\"execute\":\"system_reset\"}"); - self.0.execute("{\"execute\":\"cont\"}"); - } -} - -/// `RESET` events the *guest* caused, scanned rather than parsed like -/// [`shutdown_reason`]. QEMU raises one for its own power-on reset too, which -/// carries `"guest": false` and is not a claim about anything the guest did. -fn guest_resets(bytes: &[u8]) -> usize { - String::from_utf8_lossy(bytes) - .lines() - .filter(|line| line.contains("\"RESET\"") && line.contains("\"guest\": true")) - .count() -} - -/// The `reason` field of a `SHUTDOWN` event in `bytes`, scanned rather than parsed: [`Qmp`] carries no JSON dependency. -fn shutdown_reason(bytes: &[u8]) -> Option { - let text = String::from_utf8_lossy(bytes); - let line = text.lines().find(|l| l.contains("\"SHUTDOWN\""))?; - let (_, after) = line.split_once("\"reason\"")?; - let (_, value) = after.split_once('"')?; - let (value, _) = value.split_once('"')?; - Some(value.to_string()) -} - /// An open QMP connection to QEMU's human monitor, for the questions QMP has /// no command of its own for. pub struct QmpMonitor(Qmp); @@ -3759,154 +1882,6 @@ impl QmpInput { .collect(); self.send(&body); } - - /// Type `text` as one batch of transitions, with no wait anywhere in it. - /// - /// **The caller owns the bound, and there is no version of this that does - /// not need one.** QEMU's PS/2 keyboard queue holds `QEMU_PS2_QUEUE` set-1 - /// bytes and drops what does not fit silently, one byte at a time, so a - /// batch wider than that queue is a hole in the middle of a word whatever - /// the guest is doing. Use [`scancode_bytes`] to measure a batch, and send - /// the next one only once the guest has shown it consumed this one — - /// `console_type_line` and `shell_type_line` in `tests/toyos.rs` are the - /// two patterns, one reading the panel and one reading [`ConsoleStream`]. - /// - /// **There is no wall-clock form of this and there must not be one.** A gap - /// between characters is the same bound bet on the guest being scheduled, - /// and a guest whose vCPU the host has not run for a couple of hundred - /// milliseconds drains none of them — at which point the queue starts - /// dropping, silently and one byte at a time, and the guest receives the - /// line with a hole in it. Both times `screen_console_panic` has ever gone - /// red that is what happened, and neither side of the wire says a word - /// about it. - pub fn type_burst(&mut self, text: &str) { - let mut events: Vec<(&str, bool)> = Vec::new(); - for ch in text.chars() { - let (qcode, shift) = qcode(ch); - if shift { - events.extend([("shift", true), (qcode, true), (qcode, false), ("shift", false)]); - } else { - events.extend([(qcode, true), (qcode, false)]); - } - } - self.keys(&events); - } - - /// One pointer packet: relative motion and/or a button transition. - pub fn mouse(&mut self, dx: i32, dy: i32, button: Option<(&str, bool)>) { - let mut body: Vec = Vec::new(); - if let Some((name, down)) = button { - body.push(format!( - "{{\"type\":\"btn\",\"data\":{{\"down\":{down},\"button\":\"{name}\"}}}}" - )); - } - for (axis, value) in [("x", dx), ("y", dy)] { - if value != 0 { - body.push(format!( - "{{\"type\":\"rel\",\"data\":{{\"axis\":\"{axis}\",\"value\":{value}}}}}" - )); - } - } - self.send(&body); - } -} - -/// What one character costs on the wire, in set-1 bytes. -/// -/// Every qcode [`qcode`] maps is a one-byte make and its break, and none of -/// them is `0xE0`-prefixed; a shifted one carries the modifier's pair around -/// it. This exists because a caller that has to bound what it puts in flight -/// against QEMU's PS/2 queue cannot do it without knowing what a character -/// weighs — an unmapped character panics in `qcode` rather than being counted -/// as anything, which is the same refusal typing one would get. -pub fn scancode_bytes(ch: char) -> usize { - if qcode(ch).1 { 4 } else { 2 } -} - -/// The QEMU qcode for `ch`, and whether Shift is held to produce it. -/// -/// A US layout, because that is what `kernel/src/keyboard.rs` boots with. Only -/// the characters a console test types: an unmapped one panics rather than -/// being dropped, since a command missing a character is a test asserting on -/// output nothing was ever asked to produce. -fn qcode(ch: char) -> (&'static str, bool) { - const LOWER: [&str; 26] = [ - "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", - "s", "t", "u", "v", "w", "x", "y", "z", - ]; - const DIGIT: [&str; 10] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]; - match ch { - 'a'..='z' => (LOWER[ch as usize - 'a' as usize], false), - 'A'..='Z' => (LOWER[ch as usize - 'A' as usize], true), - '0'..='9' => (DIGIT[ch as usize - '0' as usize], false), - ' ' => ("spc", false), - '\n' => ("ret", false), - '-' => ("minus", false), - '_' => ("minus", true), - '.' => ("dot", false), - '/' => ("slash", false), - '&' => ("7", true), - _ => panic!("no qcode for {ch:?}; add it rather than typing something else"), - } -} - -pub fn qmp_send_keys(socket: &Path, events: &[(&str, bool)]) { - QmpInput::open(socket).keys(events); -} - -/// An open QMP connection for attaching and detaching devices while the guest -/// runs — QEMU's own `device_add`/`device_del`, which is what a person -/// plugging something in looks like from the host side. -/// -/// Its own type rather than more methods on [`QmpInput`], and never open at the -/// same time as one: a `-qmp unix:…,server` socket serves one monitor, so a -/// caller that needs both alternates. A type called `QmpInput` with -/// `device_add` on it would also be describing the wrong thing. -pub struct QmpDevices(Qmp); - -impl QmpDevices { - pub fn open(socket: &Path) -> Self { - Self(Qmp::connect(socket)) - } - - /// Attach `driver` on `bus` as `id`, with `extra` naming any further - /// properties. Every value is a bare JSON string, which is what every - /// property these tests set happens to be. - pub fn add(&mut self, driver: &str, bus: &str, id: &str, extra: &[(&str, &str)]) { - let mut args = format!("\"driver\":\"{driver}\",\"bus\":\"{bus}\",\"id\":\"{id}\""); - for (key, value) in extra { - args.push_str(&format!(",\"{key}\":\"{value}\"")); - } - self.0.execute(&format!("{{\"execute\":\"device_add\",\"arguments\":{{{args}}}}}")); - } - - pub fn del(&mut self, id: &str) { - self.0 - .execute(&format!("{{\"execute\":\"device_del\",\"arguments\":{{\"id\":\"{id}\"}}}}")); - } - - /// Hold every frame the guest sends on `netdev` from here on, unseen by - /// it: its link stays up, and nothing it sends reaches anything. QEMU's - /// `filter-buffer` lets its frames go once per `interval` microseconds, - /// which is set past any test's life. - pub fn hold_outbound(&mut self, netdev: &str) { - self.0.execute(&format!( - "{{\"execute\":\"object-add\",\"arguments\":{{\"qom-type\":\"filter-buffer\",\ - \"id\":\"held-{netdev}\",\"netdev\":\"{netdev}\",\"queue\":\"rx\",\ - \"interval\":4000000000}}}}" - )); - } - - /// Give QEMU an image to back a device that is not on the machine yet, so - /// a hot-plugged disk needs nothing in argv. A disk declared at boot is a - /// disk the guest could have enumerated at boot. - pub fn blockdev_add(&mut self, node: &str, image: &Path) { - self.0.execute(&format!( - "{{\"execute\":\"blockdev-add\",\"arguments\":{{\"node-name\":\"{node}\",\ - \"driver\":\"raw\",\"file\":{{\"driver\":\"file\",\"filename\":\"{}\"}}}}}}", - image.display() - )); - } } /// The argv `options` would launch QEMU with, built against placeholder @@ -3915,37 +1890,21 @@ impl QmpDevices { /// unused — so this is what a profile assertion has to read. pub fn profile_argv(options: &BootOptions) -> Vec { let p = Path::new("/nonexistent"); - let usb: Vec = options.profile.usb_disks().iter().map(|_| p.to_path_buf()).collect(); - qemu_command(p, p, &usb, p, p, p, options) + qemu_command(p, p, p, p, p, options) .get_args() .map(|a| a.to_string_lossy().into_owned()) .collect() } -/// The boot stick's backing, as `-drive` keys: the raw image, or the raw image -/// over `blkdebug` failing every read that covers `read_error` with EIO. -fn stick_file(image: &Path, read_error: Option) -> String { - match read_error { - None => format!("format=raw,file={}", image.display()), - Some(sector) => format!( - "driver=raw,file.driver=blkdebug,file.inject-error.0.event=read_aio,\ - file.inject-error.0.sector={sector},file.inject-error.0.errno=5,\ - file.inject-error.0.once=off,file.image.driver=file,file.image.filename={}", - image.display() - ), - } -} - fn qemu_command( boot_image: &Path, nvme_image: &Path, - usb_images: &[PathBuf], uart_log: &Path, socket_dir: &Path, firmware_vars: &Path, options: &BootOptions, ) -> Command { - let (qmp_socket, segment) = socket_names(socket_dir, options); + let qmp_socket = qmp_socket(socket_dir, options); let shape = options.profile.shape(); assert!( !options.mute || !shape.virtio.present(), @@ -3983,26 +1942,18 @@ fn qemu_command( let mut machine = match arch { Arch::X86_64 => arch.machine().to_string(), Arch::Aarch64 => { - // `virt` has no i8042 to take away, and the unit a profile declares - // is VT-d, which it has none of either. - assert!(options.i8042 && shape.iommu.is_none(), "`virt` has neither an i8042 nor VT-d"); + // The unit a profile declares is VT-d, which `virt` has none of. + assert!(shape.iommu.is_none(), "`virt` has no VT-d"); match options.profile { Profile::VirtEl2 => format!("{},gic-version=3,virtualization=on", arch.machine()), _ => format!("{},gic-version=3", arch.machine()), } } }; - if !options.i8042 { - machine.push_str(",i8042=off"); - } if shape.iommu.is_some() { machine.push_str(",kernel-irqchip=split"); } - if let Some(base) = options.rtc_base { - qemu.arg("-rtc").arg(format!("base={base}")); - } - // `virt` puts RAM at 1 GiB and AAVMF allocates from its top, so with 4 GiB // the loader's allocations land past the 4 GiB its boot map reaches and it // refuses the boot: issues/boot-media/the-boot-map-reaches-4-gib-and-firmware-decides-what-lands-in-it.md. @@ -4023,29 +1974,8 @@ fn qemu_command( .arg("-drive") .arg(firmware_vars) .arg("-drive") - .arg(format!( - "if=none,id=stick,{}{}", - stick_file(boot_image, options.boot_read_error), - // **What a `Staged::Pristine` boot is made of.** QEMU keeps this - // drive's writes in a temporary file and drops it when the guest - // exits, so the staged image is never written and the boot after it - // starts where this one did. A copy of the image would do the same - // and costs 180 MB of disk per boot; this costs nothing. - match &options.boot_image { - Some(Staged::Pristine(_)) => ",snapshot=on", - _ => "", - } - )); - assert!( - !shape.xhci.is_empty() || (shape.usb.is_empty() && shape.usb_disks.is_empty()), - "a USB device needs a controller" - ); - // A data stick declared onto no bus is emitted with an empty `bus=`, which - // QEMU puts on whichever controller it likes. - assert!( - shape.usb_disks.iter().all(|disk| !disk.bus.unwrap_or(shape.storage_bus).is_empty()), - "a USB disk needs a bus to be on" - ); + .arg(format!("if=none,id=stick,format=raw,file={}", boot_image.display())); + assert!(!shape.xhci.is_empty() || shape.usb.is_empty(), "a USB device needs a controller"); // Ahead of every other `-device`: QEMU gives a PCI function the bypassing // address space unless the unit exists when the function is created, so a @@ -4068,73 +1998,9 @@ fn qemu_command( qemu.arg("-device").arg(*controller); } - // The data disks' own arguments, emitted either side of the boot stick's - // `-device`. QEMU hands out ports in the order devices are created, so this - // is the only thing that decides which disk the guest enumerates first. - // Each carries a device id as well as a drive id, because a test that - // unplugs one over QMP has to be able to name it. - assert!( - options.usb_pcap.is_none() || !shape.usb_disks.is_empty(), - "usb_pcap records the first data disk's traffic and this profile has no data disk" - ); - let data_sticks: Vec> = shape - .usb_disks - .iter() - .enumerate() - .map(|(i, disk)| { - let pcap = match &options.usb_pcap { - Some(path) if i == 0 => format!(",pcap={}", path.display()), - _ => String::new(), - }; - vec![ - "-drive".to_string(), - format!( - "if=none,id={},format=raw,file={}{}", - usb_drive_id(i), - usb_images[i].display(), - if disk.readonly { ",readonly=on" } else { "" } - ), - "-device".to_string(), - format!( - "usb-storage,bus={1},drive={2},id={3},logical_block_size={0},\ - physical_block_size={0}{pcap}{serial}", - disk.lba_bytes, - disk.bus.unwrap_or(shape.storage_bus), - usb_drive_id(i), - usb_device_id(i), - serial = disk.serial.map(|s| format!(",serial={s}")).unwrap_or_default(), - ), - ] - }) - .collect(); - for (disk, args) in shape.usb_disks.iter().zip(&data_sticks) { - if disk.before_boot_stick { - qemu.args(args); - } - } - - // An empty `storage_bus` declares that storage is not USB here: the boot - // volume rides its own NVMe controller and every xHCI carries HID alone. - if shape.storage_bus.is_empty() { - qemu.arg("-device") - .arg("nvme,serial=bootdisk,id=nvmebootctl,bootindex=0,msix-exclusive-bar=on") - .arg("-device") - .arg("nvme-ns,drive=stick,bus=nvmebootctl,logical_block_size=512,\ - physical_block_size=512"); - } else { - qemu.arg("-device").arg(format!( - "usb-storage,bus={},drive=stick,id={BOOT_STICK_ID},serial={BOOT_STICK_SERIAL},\ - bootindex=0", - shape.storage_bus - )); - } - if let Some(gpu) = shape.gpu { - assert_eq!( - shape.vga, "none", - "a declared adapter beside a `-vga` one gives the guest two displays" - ); - qemu.arg("-device").arg(format!("{gpu}{platform}")); - } + qemu.arg("-device").arg(format!( + "usb-storage,bus=xhci.0,drive=stick,id={BOOT_STICK_ID},serial={BOOT_STICK_SERIAL},bootindex=0" + )); match (arch, shape.vga) { (Arch::X86_64, vga) => { qemu.arg("-vga").arg(vga); @@ -4147,10 +2013,7 @@ fn qemu_command( (Arch::Aarch64, "none") => {} (Arch::Aarch64, other) => panic!("`virt` has no `-vga {other}`"), } - qemu.arg("-display").arg("none"); - if !options.takes_the_reset { - qemu.arg("-no-reboot"); - } + qemu.arg("-display").arg("none").arg("-no-reboot"); if let Some((w, h)) = shape.panel { assert_eq!(arch, Arch::X86_64, "a panel is declared through VGA's EDID, and `virt` has no VGA"); // A panel on a machine with no VGA adapter is a declaration nothing @@ -4179,137 +2042,28 @@ fn qemu_command( // controller alone and this one is nobody's, as the kernel's first-by-class // probe left it. if shape.nvme_bytes != 0 { - let ids = if shape.storage_bus.is_empty() { ",use-intel-id=on" } else { "" }; - qemu.arg("-drive") - .arg(format!( - "if=none,id=nvme0,format=raw,file={}", - nvme_image.display() - )) - .arg("-device") - .arg(format!("nvme,serial=deadbeef,id=nvme0ctl,msix-exclusive-bar=on{ids}")) - .arg("-device") - .arg(format!( - "nvme-ns,drive=nvme0,bus=nvme0ctl,logical_block_size={0},physical_block_size={0}", - shape.nvme_lba_bytes - )); - } - if let Some(image) = &options.userland_nvme { qemu.arg("-drive") - .arg(format!("if=none,id=nvme1,format=raw,file={}", image.display())) + .arg(format!("if=none,id=nvme0,format=raw,file={}", nvme_image.display())) .arg("-device") - .arg("nvme,serial=userland,id=nvme1ctl,use-intel-id=on,msix-exclusive-bar=on") + .arg("nvme,serial=deadbeef,id=nvme0ctl,msix-exclusive-bar=on") .arg("-device") - .arg( - "nvme-ns,drive=nvme1,bus=nvme1ctl,logical_block_size=512,physical_block_size=512,\ - write-cache=on", - ); - } - if let Some(trace) = &options.nvme_trace { - for event in [ - "pci_nvme_io_cmd", - "pci_nvme_enqueue_req_completion", - "pci_nvme_flush_ns", - "pci_nvme_write", - "pci_nvme_mmio_start_success", - ] { - qemu.arg("-trace").arg(event); - } - qemu.arg("-D").arg(trace); - } - - // The mass-storage devices beside the boot stick, and the only ones a test - // may write to: the boot stick is on the same bus and carries the image the - // guest is running from. Their logical block sizes are stated rather than - // left to the default for the same reason the namespace's is. - for (disk, args) in shape.usb_disks.iter().zip(&data_sticks) { - if !disk.before_boot_stick { - qemu.args(args); - } + .arg("nvme-ns,drive=nvme0,bus=nvme0ctl,logical_block_size=512,physical_block_size=512"); } - for dev in shape.usb { qemu.arg("-device").arg(*dev); } - if !shape.hda.is_empty() { - // No guest test plays audio: the device is here as a DMA master and a - // claim, so its audio goes nowhere. - qemu.arg("-audiodev").arg("none,id=hdaaud"); - for dev in shape.hda { - qemu.arg("-device").arg(*dev); - } - } - // The NIC before the virtio block, so a profile that has one and not the // other still creates it after the unit and before everything else. - // `iommu_platform` is virtio's own way of asking to be decoded; an e1000e - // is decoded by the unit whatever it says, so it carries none. - // The one clause that makes slirp two-way, on whichever card this profile - // has. - let forward = [ - options.ssh_port.map(ssh_forward_argv), - options.log_port.map(|port| { - format!(",hostfwd=tcp:{SSH_FORWARD_HOST}:{port}-:{}", toyos_logstream::PORT) - }), - ] - .into_iter() - .flatten() - .collect::(); + // `iommu_platform` is virtio's own way of asking to be decoded. match shape.nic { Nic::Absent => {} Nic::Virtio => { - qemu.arg("-netdev").arg(format!("user,id=net0{forward}")).arg("-device").arg(format!( + qemu.arg("-netdev").arg("user,id=net0").arg("-device").arg(format!( "virtio-net-pci-non-transitional,netdev=net0{platform}" )); } - Nic::VirtioWithoutMsix => { - qemu.arg("-netdev").arg(format!("user,id=net0{forward}")).arg("-device").arg(format!( - "virtio-net-pci-non-transitional,netdev=net0,vectors=0{platform}" - )); - } - Nic::E1000e => { - qemu.arg("-netdev") - .arg(format!("user,id=net0{forward}")) - .arg("-device") - .arg("e1000e,netdev=net0"); - } - Nic::E1000eBesideIgb => { - qemu.arg("-netdev") - .arg(format!("user,id=net0{forward}")) - .arg("-device") - .arg("e1000e,netdev=net0") - .arg("-device") - .arg("igb"); - } - Nic::E1000eNoServer => { - // The hub is not slirp and takes no `hostfwd`, so a boot asking for - // one here is refused rather than booted without a forward. - assert!( - forward.is_empty(), - "this profile's cable is plugged into nothing, so no host port reaches the guest" - ); - qemu.arg("-netdev") - .arg("hubport,id=net0,hubid=0") - .arg("-device") - .arg("e1000e,netdev=net0"); - } - } - if let Some(at) = &options.wire_dump { - assert!( - !matches!(shape.nic, Nic::Absent), - "this profile carries no NIC, so there is no `net0` to dump frames off" - ); - qemu.arg("-object") - .arg(format!("filter-dump,id=wire,netdev=net0,file={}", at.display())); } - if let Some(tap) = &segment { - assert!( - !matches!(shape.nic, Nic::Absent), - "this profile carries no NIC, so there is no `net0` segment to stand on" - ); - qemu.args(tap.argv()); - } - if shape.virtio.present() { if shape.virtio.sound() { // No guest test plays audio: the device is here as a DMA master and @@ -4363,24 +2117,19 @@ fn qemu_command( struct Sockets { dir: TempDir, qmp: Option, - segment: Option, } impl Sockets { fn new(options: &BootOptions) -> Sockets { let dir = TempDir::short("boot"); - let (qmp, segment) = socket_names(&dir, options); - Sockets { dir, qmp, segment } + let qmp = qmp_socket(&dir, options); + Sockets { dir, qmp } } } -/// The QMP and segment sockets `options` asks for, named in `dir`. -fn socket_names( - dir: &Path, - options: &BootOptions, -) -> (Option, Option) { - let qmp = options.qmp.then(|| dir.join("qmp.sock")); - (qmp, options.segment.then(|| super::segment::Tap::in_dir(dir))) +/// The QMP socket `options` asks for, named in `dir`. +fn qmp_socket(dir: &Path, options: &BootOptions) -> Option { + options.qmp.then(|| dir.join("qmp.sock")) } /// Every file one boot owns, so that adding another does not lengthen a @@ -4391,22 +2140,13 @@ struct Files { nvme: NvmeClaim, sockets: Sockets, screendump: PathBuf, - own_boot_image: Option, - own_vars: Option, - carried: Option>, + boot_image: PathBuf, + vars: PathBuf, + carried: BTreeSet, } fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) -> QemuInstance { - let Files { - seq, - uart_log, - nvme, - sockets, - screendump, - own_boot_image, - own_vars, - carried, - } = files; + let Files { seq, uart_log, nvme, sockets, screendump, boot_image, vars, carried } = files; // Inherited: `orphan` reads QEMU's exit as the end of its harness's stderr. qemu.stdin(Stdio::piped()) @@ -4422,8 +2162,6 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) let stdout = child.stdout.take().unwrap(); let (tx, rx) = mpsc::channel::(); - let console = ConsoleStream::new(); - let reader_console = console.clone(); // The virtio port starts at the kernel's first record; a 16550 on stdio has // no other file, so it is read whole. let mut kernel_console = options @@ -4436,9 +2174,7 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) let mut reader = BufReader::new(stdout); let mut full_log = String::new(); // Read bytes and split them, rather than `BufRead::lines`: every - // consumer below still gets whole lines and nothing else, and - // [`ConsoleStream`] gets the tail that is not a line yet, which is - // where a prompt lives. + // consumer below still gets whole lines and nothing else. let mut pending: Vec = Vec::new(); let mut chunk = [0u8; 4096]; loop { @@ -4455,11 +2191,6 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) Some(console) => console.pass(&chunk[..read]), None => std::borrow::Cow::Borrowed(&chunk[..read]), }; - reader_console - .0 - .lock() - .expect("the console stream lock is never held across a panic") - .extend_from_slice(&read); pending.extend_from_slice(&read); while let Some(at) = pending.iter().position(|&b| b == b'\n') { let mut line: Vec = pending.drain(..=at).collect(); @@ -4488,17 +2219,13 @@ fn spawn_and_wait_ready(mut qemu: Command, options: &BootOptions, files: Files) stdin, rx, _reader_thread: reader_thread, - uart_log, - nvme, + _nvme: nvme, sockets, screendump, - own_boot_image, - own_vars, + boot_image, + vars, boot_log, - console, - i8042_trace: options.kernel_params.contains(&"i8042-trace"), smp: options.smp, - ssh_port: options.ssh_port, carried, } } @@ -4543,12 +2270,12 @@ fn wait_for_ready( let no_timeout = options.debug_wait; let ready = options.ready_marker; let panic_aborts = ready == DEFAULT_READY; - // Ten seconds per guest this phase may have up, and never fewer than two + // Ten seconds per guest this run may have up, and never fewer than two // guests' worth — the tree runs 15-25 suites a day across several agents, // so one guest on a quiet host stopped being // the regime some time before this did. Measured on 2026-08-03 with other // agents building: two boots exceeded the flat ten seconds, one of them in a - // phase running a single guest. + // run of a single guest. // // A wedge costs that much longer to report and nothing else. // diff --git a/tests/common/screen.rs b/tests/common/screen.rs index a7ed0f6bced..f8d46326df1 100644 --- a/tests/common/screen.rs +++ b/tests/common/screen.rs @@ -125,36 +125,6 @@ impl Ppm { rows.join("\n") } - /// Every cell row as `/system/bin/console` drew it, right-trimmed, blanks kept. - pub fn console_rows(&self, font: &ConsoleFont) -> Vec { - let mut rows: Vec = Vec::new(); - for cy in 0..self.height / GLYPH_H { - let mut row = String::new(); - for cx in 0..self.width / GLYPH_W { - let mut cell = [0u8; CELL]; - for r in 0..GLYPH_H { - for c in 0..GLYPH_W { - let p = self.pixels[(cy * GLYPH_H + r) * self.width + cx * GLYPH_W + c]; - cell[r * GLYPH_W + c] = p[0].max(p[1]).max(p[2]); - } - } - row.push(font.lookup(&cell)); - } - rows.push(row.trim_end().to_string()); - } - rows - } - - /// [`Ppm::console_rows`] joined, trailing blank rows dropped — the console's - /// counterpart to [`Ppm::text`]. - pub fn console_text(&self, font: &ConsoleFont) -> String { - let mut rows = self.console_rows(font); - while rows.last().is_some_and(|r| r.is_empty()) { - rows.pop(); - } - rows.join("\n") - } - /// The colour of the first foreground pixel in cell row `cy`, or `None` /// for a blank row. /// @@ -186,96 +156,6 @@ impl Ppm { } } -/// Cells of the console's font, in the alpha values it blits. -const CELL: usize = GLYPH_W * GLYPH_H; - -/// The font `/system/bin/console` and `/system/bin/terminal` draw with — 8x16 anti-aliased -/// alpha, not the kernel's 1-bit table. -/// -/// The two decoders exist for the same reason and read the same way: a glyph on -/// screen is a bit-exact function of the table the drawer used, so decoding -/// against *that* table makes a screen assertion an ordinary string assertion. -/// The table is rebuilt here by [`toyos_build::assets::console_font`], the same -/// producer that puts it on ROOT. -/// -/// Exact, not nearest-match, and that is a property of the blend rather than a -/// tolerance: `font::Font::draw_char` computes `(fg*a + bg*(255-a))/255` per -/// channel, so white on black is `a` and black on white — the cursor cell — is -/// its complement. Both are looked up. -/// -/// **It is also the discriminator that keeps the console tests non-vacuous.** -/// A boot checkpoint paints the same kernel log lines from the same ring, in -/// `font8x16.bin`. Those cells are the *thresholded* form of these, so they -/// decode to [`UNKNOWN`] here and these decode to `UNKNOWN` there: "the console -/// rendered the log" and "the console never ran and the kernel's paint is still -/// up" cannot be confused for one another. -pub struct ConsoleFont { - pub(crate) by_cell: HashMap<[u8; CELL], char>, -} - -impl ConsoleFont { - pub fn load() -> ConsoleFont { - let raw = toyos_build::assets::console_font(&super::compile::repo_root()); - let width = u16::from_le_bytes([raw[0], raw[1]]) as usize; - let height = u16::from_le_bytes([raw[2], raw[3]]) as usize; - assert_eq!( - (width, height), - (GLYPH_W, GLYPH_H), - "the console font is not the 8x16 cell this decoder grids for" - ); - let count = u32::from_le_bytes([raw[4], raw[5], raw[6], raw[7]]) as usize; - let alpha = 8 + count * 4; - - let mut by_cell: HashMap<[u8; CELL], char> = HashMap::new(); - let mut ascii_clash: Vec<(char, char)> = Vec::new(); - for i in 0..count { - let cp = u32::from_le_bytes([ - raw[8 + i * 4], - raw[9 + i * 4], - raw[10 + i * 4], - raw[11 + i * 4], - ]); - // C0 and C1 have no glyph and all rasterize blank, which would make - // a space decode as whichever control code sorted first. - if cp < 0x20 || (0x7F..=0x9F).contains(&cp) { - continue; - } - let Some(ch) = char::from_u32(cp) else { continue }; - let mut cell = [0u8; CELL]; - cell.copy_from_slice(&raw[alpha + i * CELL..alpha + (i + 1) * CELL]); - // Lowest codepoint wins, so U+00A0 does not take the blank cell - // away from a space. A clash *inside* printable ASCII would make - // every assertion in the suite ambiguous, so it is refused here - // rather than decoded into whichever codepoint sorted first. - if let Some(&first) = by_cell.get(&cell) { - if (0x20..0x7F).contains(&cp) && (0x20..0x7F).contains(&(first as u32)) { - ascii_clash.push((first, ch)); - } - continue; - } - by_cell.insert(cell, ch); - } - assert!( - ascii_clash.is_empty(), - "the console font rasterizes these printable ASCII pairs identically at \ - 8x16, so a decoded screen cannot say which was drawn: {ascii_clash:?}" - ); - ConsoleFont { by_cell } - } - - fn lookup(&self, cell: &[u8; CELL]) -> char { - if let Some(&ch) = self.by_cell.get(cell) { - return ch; - } - // The cursor cell, drawn with foreground and background swapped. - let mut inverted = [0u8; CELL]; - for (dst, &src) in inverted.iter_mut().zip(cell.iter()) { - *dst = 255 - src; - } - *self.by_cell.get(&inverted).unwrap_or(&UNKNOWN) - } -} - pub struct Font { by_bitmap: HashMap<[u8; GLYPH_H], char>, } diff --git a/tests/common/segment.rs b/tests/common/segment.rs deleted file mode 100644 index 4716b3f9c2e..00000000000 --- a/tests/common/segment.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! The host as a neighbour on the guest's own Ethernet segment. A frame the -//! host writes arrives at the guest's NIC as if off the cable, and every frame -//! the guest sends reaches the host as well as slirp: QEMU's -//! `filter-redirector` puts the host's frames onto `net0` toward the guest, -//! and `filter-mirror` copies the guest's onto a second socket. Both speak -//! QEMU's `net_fill_rstate` framing: a 32-bit big-endian length, then the -//! frame, with no virtio header. -//! -//! What slirp's forward cannot do and this can: a frame's source is whatever -//! the host wrote, so a datagram can come from an on-link neighbour, or carry -//! a source no wire should. - -use std::io::{Read, Write}; -use std::os::unix::net::UnixStream; -use std::path::{Path, PathBuf}; -use std::sync::mpsc::{self, Receiver, RecvTimeoutError}; -use std::time::Instant; - -use toyos_build::icmp::checksum; - -/// The two sockets QEMU serves the segment on, in the socket directory of the -/// [`super::qemu::QemuInstance`] that booted with them. -#[derive(Debug)] -pub struct Tap { - into_guest: PathBuf, - from_guest: PathBuf, -} - -impl Tap { - /// The two sockets' names in a boot's socket directory `dir`. - pub fn in_dir(dir: &Path) -> Self { - Self { into_guest: dir.join("tap-in.sock"), from_guest: dir.join("tap-out.sock") } - } - - /// QEMU's half: two listening sockets, one filter each, both on `net0`. A - /// filter on a netdev sees on its `tx` queue what the netdev sends toward - /// the guest, and on `rx` what the guest sends it. - pub fn argv(&self) -> [String; 8] { - [ - "-chardev".into(), - format!("socket,id=tapin,path={},server=on,wait=off", self.into_guest.display()), - "-object".into(), - "filter-redirector,id=tapinf,netdev=net0,queue=tx,indev=tapin".into(), - "-chardev".into(), - format!("socket,id=tapout,path={},server=on,wait=off", self.from_guest.display()), - "-object".into(), - "filter-mirror,id=tapoutf,netdev=net0,queue=rx,outdev=tapout".into(), - ] - } - - /// Stand on the segment of a guest booted with this tap. QEMU made both - /// sockets before the machine ran, so both connects answer at once; a frame - /// the guest sent before them is not seen, and QEMU says so on its stderr. - pub fn open(&self) -> Result { - let connect = |path: &PathBuf| { - UnixStream::connect(path).map_err(|e| format!("connect to QEMU's {}: {e}", path.display())) - }; - let into = connect(&self.into_guest)?; - let mut from = connect(&self.from_guest)?; - let (tx, frames) = mpsc::channel(); - std::thread::spawn(move || { - let mut len = [0u8; 4]; - while from.read_exact(&mut len).is_ok() { - let mut frame = vec![0u8; u32::from_be_bytes(len) as usize]; - if from.read_exact(&mut frame).is_err() || tx.send(frame).is_err() { - return; - } - } - }); - Ok(Segment { into, frames }) - } -} - -/// A connection onto the segment. -pub struct Segment { - into: UnixStream, - frames: Receiver>, -} - -impl Segment { - /// Put `frame` on the segment, toward the guest. - pub fn send(&mut self, frame: &[u8]) -> Result<(), String> { - let len = u32::try_from(frame.len()).expect("a frame is shorter than 4 GiB").to_be_bytes(); - self.into - .write_all(&len) - .and_then(|()| self.into.write_all(frame)) - .map_err(|e| format!("put a frame on the segment: {e}")) - } - - /// The next frame the guest sends, before `deadline`. - pub fn next(&self, deadline: Instant) -> Result, String> { - let left = deadline.saturating_duration_since(Instant::now()); - self.frames.recv_timeout(left).map_err(|e| match e { - RecvTimeoutError::Timeout => "the guest sent no frame in time".to_string(), - RecvTimeoutError::Disconnected => "QEMU closed the segment".to_string(), - }) - } -} - -const ETHERTYPE_IPV4: u16 = 0x0800; -const ETHERTYPE_ARP: u16 = 0x0806; -const BROADCAST: [u8; 6] = [0xff; 6]; - -/// RFC 826: who has `target`, asked by `mac` at `ip`, broadcast. -pub fn arp_request(mac: [u8; 6], ip: [u8; 4], target: [u8; 4]) -> Vec { - let mut frame = ethernet(BROADCAST, mac, ETHERTYPE_ARP); - // Ethernet, IPv4, 6- and 4-byte addresses, a request. - frame.extend_from_slice(&[0, 1, 0x08, 0x00, 6, 4, 0, 1]); - frame.extend_from_slice(&mac); - frame.extend_from_slice(&ip); - frame.extend_from_slice(&[0; 6]); - frame.extend_from_slice(&target); - // The shortest Ethernet frame, less its FCS. - frame.resize(60, 0); - frame -} - -/// The hardware address of the ARP reply in `frame` saying where `ip` is, if -/// that is what `frame` is. -pub fn arp_reply_for(frame: &[u8], ip: [u8; 4]) -> Option<[u8; 6]> { - let arp = frame.get(14..42)?; - let is_reply = u16_at(frame, 12) == Some(ETHERTYPE_ARP) && arp[6..8] == [0, 2]; - (is_reply && arp[14..18] == ip).then(|| arp[8..14].try_into().expect("six bytes")) -} - -/// One UDP datagram in one IPv4 packet (RFC 791, RFC 768), with both checksums. -pub struct Udp<'a> { - pub dst_mac: [u8; 6], - pub src_mac: [u8; 6], - pub src: ([u8; 4], u16), - pub dst: ([u8; 4], u16), - pub payload: &'a [u8], -} - -impl Udp<'_> { - pub fn frame(&self) -> Vec { - let udp_len = 8 + self.payload.len(); - let mut ip = vec![0x45, 0]; - ip.extend_from_slice(&(20 + udp_len as u16).to_be_bytes()); - // ID, no fragment, TTL 64 (§3.2 ignores it on one link), UDP. - ip.extend_from_slice(&[0, 0, 0x40, 0, 64, 17, 0, 0]); - ip.extend_from_slice(&self.src.0); - ip.extend_from_slice(&self.dst.0); - let sum = checksum(&ip).to_be_bytes(); - ip[10..12].copy_from_slice(&sum); - - let mut udp = Vec::with_capacity(udp_len); - udp.extend_from_slice(&self.src.1.to_be_bytes()); - udp.extend_from_slice(&self.dst.1.to_be_bytes()); - udp.extend_from_slice(&(udp_len as u16).to_be_bytes()); - udp.extend_from_slice(&[0, 0]); - udp.extend_from_slice(self.payload); - let mut pseudo = Vec::with_capacity(12 + udp_len); - pseudo.extend_from_slice(&self.src.0); - pseudo.extend_from_slice(&self.dst.0); - pseudo.extend_from_slice(&[0, 17]); - pseudo.extend_from_slice(&(udp_len as u16).to_be_bytes()); - pseudo.extend_from_slice(&udp); - // RFC 768: a computed zero is sent as all ones. - let sum = match checksum(&pseudo) { - 0 => 0xffff, - sum => sum, - }; - udp[6..8].copy_from_slice(&sum.to_be_bytes()); - - let mut frame = ethernet(self.dst_mac, self.src_mac, ETHERTYPE_IPV4); - frame.extend_from_slice(&ip); - frame.extend_from_slice(&udp); - frame.resize(frame.len().max(60), 0); - frame - } -} - -/// The UDP datagram `frame` carries, if it carries one in an IPv4 packet with -/// no options: its addresses, and its payload. -pub fn udp_in(frame: &[u8]) -> Option> { - if u16_at(frame, 12)? != ETHERTYPE_IPV4 || *frame.get(14)? != 0x45 || *frame.get(23)? != 17 { - return None; - } - let ip_len = u16_at(frame, 16)? as usize; - let udp_len = u16_at(frame, 38)? as usize; - if udp_len < 8 || 20 + udp_len > ip_len { - return None; - } - Some(Udp { - dst_mac: frame[0..6].try_into().ok()?, - src_mac: frame[6..12].try_into().ok()?, - src: (frame[26..30].try_into().ok()?, u16_at(frame, 34)?), - dst: (frame[30..34].try_into().ok()?, u16_at(frame, 36)?), - payload: frame.get(42..34 + udp_len)?, - }) -} - -fn ethernet(dst: [u8; 6], src: [u8; 6], ethertype: u16) -> Vec { - let mut frame = Vec::with_capacity(64); - frame.extend_from_slice(&dst); - frame.extend_from_slice(&src); - frame.extend_from_slice(ðertype.to_be_bytes()); - frame -} - -fn u16_at(bytes: &[u8], at: usize) -> Option { - Some(u16::from_be_bytes([*bytes.get(at)?, *bytes.get(at + 1)?])) -} diff --git a/tests/common/serial.rs b/tests/common/serial.rs index 7c1a5330eec..edc8bcaf92b 100644 --- a/tests/common/serial.rs +++ b/tests/common/serial.rs @@ -40,8 +40,7 @@ pub enum Died { Kernel, /// A process the kernel killed: a Ring 3 fault, reported by name in /// `kernel/src/arch/x86_64/idt/exceptions.rs`. The machine is fine — a test whose - /// whole subject is a process dying (every `faults.rs` - /// probe) produces these deliberately. Before a boot's ready + /// whole subject is a process dying produces these deliberately. Before a boot's ready /// marker it still ends the boot: whatever died was `init` or one of its /// children, and nothing left is going to reach the marker. Faulted, @@ -211,21 +210,12 @@ impl Serial { Self { text: qemu.boot_log().to_string(), source: String::from("boot console") } } - /// For text a test collected itself — a `drain_serial` window, a - /// `TestResult::serial`, the 16550 file of a guest that died early. + /// For text a test collected itself — a `drain_serial` window, the 16550 + /// file of a guest that died early. pub fn named(source: &str, text: impl Into) -> Self { Self { text: text.into(), source: source.to_string() } } - /// Append a later window — `drain_serial`, a test's own serial. Keeps one - /// object to assert against instead of a `format!` of two. - pub fn push(&mut self, more: &str) { - self.text.push_str(more); - if !more.ends_with('\n') { - self.text.push('\n'); - } - } - pub fn text(&self) -> &str { &self.text } @@ -281,13 +271,9 @@ impl Serial { /// A whole-capture scan answers with the earliest line of that shape, /// whoever wrote it and whenever — and for a test that *stages* the event it /// is looking for, the earliest line is the wrong one whenever anything else - /// on the machine can produce the same shape. `i8042_undecoded_bytes` - /// injects an undecodable key once the guest prints `===I8042_READY===` and - /// then read the first `nothing decoded` line in its capture as the answer; - /// the driver's own bring-up can produce one before that marker, and on a - /// laptop a real spurious interrupt can too. + /// on the machine can produce the same shape. /// - /// The marker is what the injection was timed off, so it is the boundary the + /// The marker is what the staging was timed off, so it is the boundary the /// test actually knows — no host clock is involved, and a stranger line /// before it can no longer be read as the test's own. A missing marker is a /// failure rather than a fallback to the whole capture: the anchor going @@ -350,37 +336,6 @@ impl Serial { } Ok(()) } - - /// [`Self::must_be_clean`] for the one test that staged one of - /// [`NEVER_CLEAN`]'s lines on purpose. - /// - /// `allowed` may appear exactly `times` and no other never-clean line may - /// appear at all, so a boot that produced a *second* one — or a different - /// one — still reds. Named rather than a flag, because the whole value of - /// `NEVER_CLEAN` is that a test cannot pass one by without saying so. - pub fn must_be_clean_apart_from(&self, allowed: &str, times: usize) -> Result<(), String> { - for (bad, by_kernel, _) in DEATHS { - if *by_kernel != Died::Kernel { - continue; - } - self.must_not_say(bad)?; - } - for bad in NEVER_CLEAN { - if *bad == allowed { - continue; - } - self.must_not_say(bad)?; - } - let seen = self.text().matches(allowed).count(); - if seen != times { - return Err(format!( - "{allowed:?} appears {seen} time(s) on a {} and this test staged {times}:\n{}", - self.source, - self.text - )); - } - Ok(()) - } } /// Lines a boot survives and still must not print. @@ -394,7 +349,6 @@ const NEVER_CLEAN: &[&str] = &[ // address its own domain does not map. The machine goes on and the claim // refuses every later call, so this is not a death; it is a driver whose // descriptors are wrong, and a netd that did it on every boot would - // otherwise pass everywhere. `userdev_dma_fault` stages exactly one on - // purpose and reads it with `must_say`. + // otherwise pass everywhere. "iommu: DMA FAULT owner=slot", ]; diff --git a/tests/common/ssh.rs b/tests/common/ssh.rs index ac850ac6864..d8776ab0ed4 100644 --- a/tests/common/ssh.rs +++ b/tests/common/ssh.rs @@ -6,55 +6,34 @@ //! complete one. Everything below turns the second into an error and only the //! first into a verdict. -use std::path::{Path, PathBuf}; +use std::path::Path; use std::process::Command; use super::compile; -use super::qemu::SSH_FORWARD_HOST; - -/// Where the guest is, as this host sees it through QEMU's forward. -pub const HOST: &str = SSH_FORWARD_HOST; /// A key pair minted for one test and thrown away with it, as two files in the /// lane's scratch directory. **Nothing in this repository holds a private /// key**: a committed one would be a credential with no owner and no expiry. pub struct Identity { - private: PathBuf, line: String, - fingerprint: String, } impl Identity { - /// The key called `name` in this lane, minted the first time it is asked - /// for and handed back after that: a boot several tests share stages one - /// of these into its image, so a second mint would hand the later members - /// a key the running guest has never heard of. - pub fn mint(name: &str) -> Result { - Self::mint_in(&super::lane::dir().join("ssh").join(name)) - } - /// The key kept in `dir`, minted the first time: a metal image carries its /// public half, so the key lives beside the image and outlives this run. pub fn mint_in(dir: &Path) -> Result { std::fs::create_dir_all(dir).map_err(|e| format!("create {}: {e}", dir.display()))?; let private = dir.join("id_ed25519"); let public = dir.join("id_ed25519.pub"); - let fingerprint = dir.join("fingerprint"); - if !private.exists() || !public.exists() || !fingerprint.exists() { + if !private.exists() || !public.exists() { let said = client(&["keygen", str(&private), str(&public)])?; - let said = said - .trim() - .strip_prefix("ok ") - .ok_or_else(|| format!("the client answered {said:?} to keygen"))?; - std::fs::write(&fingerprint, said) - .map_err(|e| format!("record the minted key's fingerprint: {e}"))?; + if !said.trim().starts_with("ok ") { + return Err(format!("the client answered {said:?} to keygen")); + } } Ok(Identity { line: std::fs::read_to_string(&public) .map_err(|e| format!("read the minted public key: {e}"))?, - fingerprint: std::fs::read_to_string(&fingerprint) - .map_err(|e| format!("read the minted key's fingerprint: {e}"))?, - private, }) } @@ -63,223 +42,6 @@ impl Identity { pub fn authorized_line(&self) -> String { self.line.clone() } - - /// The private half, for a client this module does not run. - pub fn private(&self) -> &Path { - &self.private - } - - /// The fingerprint the guest's daemon prints for this key. - pub fn fingerprint(&self) -> &str { - self.fingerprint.trim() - } -} - -/// What one `exec` came back with. -#[derive(Debug)] -pub struct Exec { - pub stdout: Vec, - pub stderr: Vec, - /// `None` is a channel that closed without an `exit-status` message, which - /// is itself a finding: a harness that cannot learn a program's status has - /// no verdict to report, and one that read a missing status as zero would - /// report a pass. - pub status: Option, -} - -impl Exec { - pub fn stdout_text(&self) -> String { - String::from_utf8_lossy(&self.stdout).into_owned() - } - - pub fn stderr_text(&self) -> String { - String::from_utf8_lossy(&self.stderr).into_owned() - } -} - -/// Run `command` on the guest and collect what it said and how it ended. -pub fn ssh_exec( - host: &str, - port: u16, - identity: &Identity, - command: &str, -) -> Result { - let (out, err, port) = capture(identity, port)?; - let said = client(&["exec", host, &port, str(&identity.private), str(&out), str(&err), command])?; - collected(&said, &out, &err) -} - -/// Run `command` with the file `stdin` on its input and nothing asked -/// before it: `ssh update < image`, as a test asks it. -pub fn ssh_pipe(host: &str, port: u16, identity: &Identity, command: &str, stdin: &Path) -> Result { - let (out, err, port) = capture(identity, port)?; - let said = client(&["pipe", host, &port, str(&identity.private), str(&out), str(&err), str(stdin), command])?; - collected(&said, &out, &err) -} - -/// Ask for `command` and answer the guest's reply to the request, without -/// waiting for the program: `reboot`, whose status no client can collect. -/// A refused request, or a program that came back, is an error by name: the -/// command did not end the machine. -pub fn ssh_fire(host: &str, port: u16, identity: &Identity, command: &str) -> Result { - let said = client(&["fire", host, &port.to_string(), str(&identity.private), command])?; - let said = said.lines().last().unwrap_or("").to_string(); - match said.as_str() { - "accepted" | "closed" | "silent" => Ok(said), - _ => Err(format!("`{command}` over ssh answered {said:?}, so it did not end the machine")), - } -} - -/// Run `command` with `stdin` on its input, after asking the guest to set an -/// environment variable. `Ok`'s second half is what it answered that request. -pub fn ssh_feed( - host: &str, - port: u16, - identity: &Identity, - command: &str, - stdin: &[u8], -) -> Result<(Exec, String), String> { - let (out, err, port) = capture(identity, port)?; - let local = out.with_file_name("stdin"); - std::fs::write(&local, stdin).map_err(|e| format!("stage {}: {e}", local.display()))?; - let said = client(&[ - "feed", - host, - &port, - str(&identity.private), - str(&out), - str(&err), - str(&local), - command, - ])?; - let env = said - .lines() - .find_map(|line| line.strip_prefix("env ")) - .ok_or_else(|| format!("the client said nothing about the env request: {said:?}"))? - .to_string(); - Ok((collected(&said, &out, &err)?, env)) -} - -/// Start `command` on the guest and drop the connection once it is running. -pub fn ssh_abandon( - host: &str, - port: u16, - identity: &Identity, - command: &str, -) -> Result<(), String> { - let port = port.to_string(); - client(&["abandon", host, &port, str(&identity.private), command])?; - Ok(()) -} - -/// Where one exchange's two captured streams go, and the port as the argv -/// wants it. -fn capture(identity: &Identity, port: u16) -> Result<(PathBuf, PathBuf, String), String> { - let dir = identity.private.with_extension(format!("exec-{}", nonce())); - std::fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?; - Ok((dir.join("stdout"), dir.join("stderr"), port.to_string())) -} - -/// The client's last line is the program's status; the captures beside it are -/// the bytes it wrote on each stream. -fn collected(said: &str, out: &Path, err: &Path) -> Result { - let last = said.lines().last().unwrap_or("").trim(); - let status = match last { - "no-exit-status" => None, - line => match line.strip_prefix("exit ") { - Some(code) => { - Some(code.parse().map_err(|_| format!("the client answered {said:?}"))?) - } - None => return Err(format!("the client answered {said:?}")), - }, - }; - Ok(Exec { - stdout: std::fs::read(out).map_err(|e| format!("read the captured stdout: {e}"))?, - stderr: std::fs::read(err).map_err(|e| format!("read the captured stderr: {e}"))?, - status, - }) -} - -/// Put `bytes` on the guest at `remote`. -pub fn ssh_put( - host: &str, - port: u16, - identity: &Identity, - remote: &str, - bytes: &[u8], -) -> Result<(), String> { - let local = identity.private.with_extension(format!("put-{}", nonce())); - std::fs::write(&local, bytes).map_err(|e| format!("stage {}: {e}", local.display()))?; - let port = port.to_string(); - client(&["put", host, &port, str(&identity.private), str(&local), remote])?; - Ok(()) -} - -/// Read the guest's `remote` onto the host. -pub fn ssh_get( - host: &str, - port: u16, - identity: &Identity, - remote: &str, -) -> Result, String> { - let local = identity.private.with_extension(format!("get-{}", nonce())); - let _ = std::fs::remove_file(&local); - let port = port.to_string(); - client(&["get", host, &port, str(&identity.private), remote, str(&local)])?; - std::fs::read(&local).map_err(|e| format!("read what the client fetched: {e}")) -} - -/// The guest's listing of `remote`, one ` ` per entry, sorted. -pub fn ssh_list( - host: &str, - port: u16, - identity: &Identity, - remote: &str, -) -> Result, String> { - let port = port.to_string(); - let said = client(&["list", host, &port, str(&identity.private), remote])?; - Ok(said - .lines() - .filter_map(|line| line.strip_prefix("entry ").map(str::to_string)) - .collect()) -} - -/// What offering an unauthorized key came back with. -pub struct Refusal { - /// Whether the guest answered the offer with `USERAUTH_PK_OK` — asking a - /// stranger to sign, rather than refusing at the probe. - pub asked_to_sign: bool, - /// The comma-separated methods the guest *still* offers after the refusal: - /// the answer to "what could a client guess at instead", and the only - /// place the daemon's `MethodSet` is visible from outside it. - pub methods: String, -} - -/// Offer this key and expect the guest to turn it away. An error is the -/// finding: either the connection did not happen at all — which says nothing -/// about authentication — or the guest let in a key no file names. -pub fn ssh_refused(host: &str, port: u16, identity: &Identity) -> Result { - let port = port.to_string(); - let said = client(&["auth", host, &port, str(&identity.private)])?; - let asked_to_sign = match said.lines().find_map(|l| l.strip_prefix("signed ")) { - Some("yes") => true, - Some("no") => false, - other => return Err(format!("the client said {other:?} about signing")), - }; - let last = said.lines().last().unwrap_or("").trim(); - let methods = match last { - "authenticated" => { - return Err(format!( - "{host}:{port} authenticated a key no authorized_keys file on it names" - )); - } - "asked to sign" => String::new(), - line => match line.strip_prefix("refused offering ") { - Some(methods) => methods.to_string(), - None => return Err(format!("the client answered {line:?}")), - }, - }; - Ok(Refusal { asked_to_sign, methods }) } /// Run the client and hand back what it said, or the reason it could not say @@ -305,333 +67,9 @@ fn str(path: &Path) -> &str { path.to_str().expect("the lane's scratch paths are utf-8") } -/// A per-call suffix, so two calls of one test do not read each other's capture. -fn nonce() -> u64 { - use std::sync::atomic::{AtomicU64, Ordering}; - static NEXT: AtomicU64 = AtomicU64::new(0); - NEXT.fetch_add(1, Ordering::Relaxed) -} - // --- The gate: one boot of `tests/sshdcase`, three judges on it --- -/// The name the boot's staged key is minted under. One name, so every judge on -/// this boot offers the key its image authorizes. -pub const KEY: &str = "sshdcase"; - -/// A second key, minted and staged nowhere. The whole of the negative arm: a -/// well-formed offer from a key no file names. -pub const STRANGER_KEY: &str = "sshdcase-stranger"; - /// Where the image's `authorized_keys` file lands, ROOT-relative — the guest /// reads it at `/system/etc/ssh_authorized_keys`, which `userland/sshd`'s /// `AUTHORIZED_KEYS` is the other half of. pub const KEYS_ON_ROOT: &str = "etc/ssh_authorized_keys"; -const KEYS_IN_GUEST: &str = "/system/etc/ssh_authorized_keys"; - -/// The guest test binary run over `exec`. Self-contained — `/tmp` and syscalls, -/// no capability its spawner has to hand it — and it cleans up after itself so -/// the boot's later judges see the `/tmp` they would have seen. -const GUEST_TEST: &str = "test_rs_empty_dir_stat"; - -/// A path nothing on the guest has, for the arm that reads a program's stderr. -const MISSING: &str = "/tmp/no_such_file_for_the_stderr_arm"; - -/// `tests/sshdcase` with a key in its image and a forward into its port 22. -pub fn boot(rust_bins: &[(String, Vec)]) -> super::qemu::QemuInstance { - boot_case("tests/sshdcase", rust_bins).0 -} - -/// `case` with a key in its image and a forward into its port 22, and its -/// console up to sshd's listening line. -/// -/// **The key is staged rather than installed**: `/home` on this machine may be -/// a tmpfs, so a key that had to be put there after the boot is a key nobody -/// could put there. -/// -/// Panics rather than failing a test on any of the three things below: a -/// profile with no NIC, an argv with no forward, and a daemon that never opened -/// its port are each a machine the gate cannot run on at all, which is the same -/// class as a guest that never printed its ready marker. -pub fn boot_case( - case: &str, - rust_bins: &[(String, Vec)], -) -> (super::qemu::QemuInstance, String) { - let identity = Identity::mint(KEY).unwrap_or_else(|why| panic!("[sshd] {why}")); - let options = super::qemu::BootOptions { - profile: super::qemu::Profile::Headless, - extra_root_files: vec![( - KEYS_ON_ROOT.to_string(), - identity.authorized_line().into_bytes(), - )], - ssh_port: Some(super::qemu::free_host_port()), - ..Default::default() - }; - // Asked of the argv this boot is about to use, not assumed: without a NIC - // the daemon leaves at its bind, and without the forward nothing on this - // host can open a connection into the guest — either way every judge below - // would fail for a reason that has nothing to do with sshd. - let argv = super::qemu::profile_argv(&options); - let forward = super::qemu::ssh_forward_argv(options.ssh_port.expect("just set")); - assert!( - argv.iter().any(|a| a.contains("virtio-net")), - "[sshd] this gate needs a NIC and the profile carries none" - ); - assert!( - argv.iter().any(|a| a.contains(&forward)), - "[sshd] the argv carries no {forward}, so nothing on this host can reach the guest" - ); - - let config = compile::repo_root().join(case); - let mut guest = - super::qemu::QemuInstance::boot_with_options(&config, &[], rust_bins, options); - let mut console = guest.boot_log().to_string(); - if let Err(why) = super::qemu::await_marker( - &mut guest, - &mut console, - "sshd: listening on port 22", - "sshd to open its port", - ) { - panic!("[sshd] never listened, so no exchange below would mean anything: {why}\n{console}"); - } - (guest, console) -} - -/// What `exec` is for: run this program, and tell me how it ended. -pub fn exec_gate(guest: &mut super::qemu::QemuInstance) -> Result<(), String> { - let identity = Identity::mint(KEY)?; - let port = guest.ssh_port(); - - // 1. A program that runs, its output byte-exact and its status zero. - let echo = ssh_exec(HOST, port, &identity, "echo hello from toyos")?; - if echo.stdout != b"hello from toyos\n" { - return Err(format!("`echo` answered {:?}", echo.stdout_text())); - } - if !echo.stderr.is_empty() { - return Err(format!("`echo` wrote {:?} to the channel's stderr", echo.stderr_text())); - } - if echo.status != Some(0) { - return Err(format!("`echo` ended {:?}", echo.status)); - } - - // 2. A program that is not there. The refusal is named on stderr and - // carried in the status; what it must never be is a hang or a silent - // zero, which is the whole reason a harness can trust arm 3. - let missing = ssh_exec(HOST, port, &identity, "no_such_program_on_this_machine")?; - if missing.status != Some(127) { - return Err(format!( - "a missing program ended {:?}, not 127:\n{}", - missing.status, - missing.stderr_text() - )); - } - if !missing.stderr_text().contains("cannot run /system/bin/no_such_program_on_this_machine") { - return Err(format!("the refusal names nothing: {:?}", missing.stderr_text())); - } - if !missing.stdout.is_empty() { - return Err(format!("a refused exec wrote {:?} to stdout", missing.stdout_text())); - } - - // 3. A line the daemon will not read as a command at all, refused before - // anything is spawned. - let unquoted = ssh_exec(HOST, port, &identity, "echo 'unterminated")?; - if unquoted.status != Some(127) || !unquoted.stderr_text().contains("unterminated ' quote") { - return Err(format!( - "an unquotable line ended {:?} saying {:?}", - unquoted.status, - unquoted.stderr_text() - )); - } - - // 4. A real guest test binary, run over the cable and judged by its exit - // status. - let gate = ssh_exec(HOST, port, &identity, GUEST_TEST)?; - if gate.status != Some(0) { - return Err(format!( - "{GUEST_TEST} ended {:?} over ssh:\n{}\n{}", - gate.status, - gate.stdout_text(), - gate.stderr_text() - )); - } - if !gate.stdout_text().contains("empty dir stat:") { - return Err(format!("{GUEST_TEST} printed {:?}", gate.stdout_text())); - } - - // 5. **The two streams are two streams.** A program that writes to both: - // stdout carries the file, stderr the diagnostic, and neither carries - // the other's bytes. Merging stderr into stdout — which is what this - // daemon used to do — is seen here and nowhere else. - let both = ssh_exec(HOST, port, &identity, &format!("cat {KEYS_IN_GUEST} {MISSING}"))?; - if both.stdout != identity.authorized_line().into_bytes() { - return Err(format!("stdout carried {:?}", both.stdout_text())); - } - if !both.stderr_text().contains(&format!("{MISSING}: file not found")) { - return Err(format!("stderr carried {:?}", both.stderr_text())); - } - if both.status != Some(1) { - return Err(format!("a program that wrote to both ended {:?}", both.status)); - } - - // 6. A program's input is the channel's data, and an `env` request is - // answered rather than left for a client to wait on. - let (fed, env) = ssh_feed(HOST, port, &identity, "cat", b"the input arrives\n")?; - if fed.stdout != b"the input arrives\n" || fed.status != Some(0) { - return Err(format!("`cat` of the channel's input said {:?}", fed.stdout_text())); - } - if env != "refused" { - return Err(format!("the guest answered an env request {env:?}")); - } - - // 7. A program that never exits, on a connection that goes away. Nothing - // is left running on the machine, and the daemon names what it ended. - let mut console = String::new(); - ssh_abandon(HOST, port, &identity, "spin")?; - super::qemu::await_marker( - guest, - &mut console, - "the connection is gone; ended /system/bin/spin", - "sshd to end a program whose connection went", - ) - .map_err(|e| format!("a program outlived the connection that started it: {e}\n{console}"))?; - - eprintln!( - " [sshd] echo, a missing program (127), an unquotable line (127), {GUEST_TEST} (0), \ - the two streams apart, the channel's input read, and a spin ended with its connection" - ); - Ok(()) -} - -/// A file in and a file out, judged by its bytes on this host. -pub fn files_gate(guest: &mut super::qemu::QemuInstance) -> Result<(), String> { - let identity = Identity::mint(KEY)?; - let port = guest.ssh_port(); - - // 1. A file this host already knows every byte of, read off the guest. - // **It never travelled over SFTP** — the build wrote it into the image — - // so a read path that quietly reordered or padded is a disagreement - // here rather than a round trip agreeing with itself. - let staged = ssh_get(HOST, port, &identity, KEYS_IN_GUEST)?; - if staged != identity.authorized_line().into_bytes() { - return Err(format!( - "{KEYS_IN_GUEST} came back as {} bytes and the build wrote {}", - staged.len(), - identity.authorized_line().len() - )); - } - - // 2. Out and back, byte for byte, on a small file with every byte value in - // it — a transfer that is text-safe and nothing else passes this. - let small: Vec = (0..=255u8).cycle().take(1000).collect(); - ssh_put(HOST, port, &identity, "/tmp/ssh_small", &small)?; - let back = ssh_get(HOST, port, &identity, "/tmp/ssh_small")?; - if back != small { - return Err(format!( - "a 1,000-byte file came back as {} bytes, first difference at {:?}", - back.len(), - back.iter().zip(&small).position(|(a, b)| a != b) - )); - } - - // 3. The guest's own stat of what it was given, so the size is two - // readings and not one. - let listing = ssh_list(HOST, port, &identity, "/tmp")?; - if !listing.iter().any(|entry| entry == "ssh_small 1000") { - return Err(format!("the guest lists /tmp as {listing:?}")); - } - - // 4. A megabyte each way: several SFTP requests, several channel windows, - // and a guest that has to keep its place across all of them. - let big = pseudorandom(1 << 20); - ssh_put(HOST, port, &identity, "/tmp/ssh_big", &big)?; - let back = ssh_get(HOST, port, &identity, "/tmp/ssh_big")?; - if back != big { - return Err(format!( - "a 1 MiB file came back as {} bytes, first difference at {:?}", - back.len(), - back.iter().zip(&big).position(|(a, b)| a != b) - )); - } - - eprintln!( - " [sshd] the staged file read back byte-exact, and 1,000 B and 1 MiB moved both ways" - ); - Ok(()) -} - -/// Who the machine lets in, in both directions. -pub fn key_auth_gate(guest: &mut super::qemu::QemuInstance) -> Result<(), String> { - let identity = Identity::mint(KEY)?; - let stranger = Identity::mint(STRANGER_KEY)?; - let port = guest.ssh_port(); - let mut console = String::new(); - - // The negative arm. A second connection, a well-formed offer, and a key no - // file on the machine names. - // - // **It is refused at the probe.** A public-key exchange is an offer with no - // signature and then, only under `USERAUTH_PK_OK`, a signature; a machine - // that answers `PK_OK` to a stranger has told it the key would be taken and - // asked it to prove it holds it. The client here cannot sign, so being - // asked at all is the finding. - // - // What the machine still offers after the refusal is the other half: the - // daemon narrows russh's `MethodSet` to public keys alone, and one that - // offered `password` or `keyboard-interactive` here would be offering a - // credential to guess at. This is the only place that narrowing is visible - // from outside the daemon. - let refusal = ssh_refused(HOST, port, &stranger)?; - if refusal.asked_to_sign { - return Err("the machine asked a key no file names to sign, so it answered PK_OK to a \ - stranger's offer instead of refusing it" - .to_string()); - } - if refusal.methods != "publickey" { - return Err(format!( - "after refusing a key the machine still offers {:?}, not publickey alone", - refusal.methods - )); - } - super::qemu::await_marker( - guest, - &mut console, - &format!("{} is authorized by no file, and was not asked to sign", stranger.fingerprint()), - "sshd to name the key it refused at the offer", - ) - .map_err(|e| format!("sshd refused a key without saying which: {e}\n{console}"))?; - - // And the positive one, said the same way: the key the image authorizes is - // named on the console as the one that got in. Without this arm a daemon - // that refused *everything* would pass the arm above. - let ok = ssh_exec(HOST, port, &identity, "echo in")?; - if ok.status != Some(0) || ok.stdout != b"in\n" { - return Err(format!("the authorized key got {:?} / {:?}", ok.status, ok.stdout_text())); - } - super::qemu::await_marker( - guest, - &mut console, - &format!("root authenticated with {}", identity.fingerprint()), - "sshd to name the key it accepted", - ) - .map_err(|e| format!("sshd accepted a key without saying which: {e}\n{console}"))?; - - eprintln!( - " [sshd] {} accepted and {} refused at the offer without being asked to sign, each \ - named on the console, and {} the only method left to try", - identity.fingerprint(), - stranger.fingerprint(), - refusal.methods - ); - Ok(()) -} - -/// A megabyte no compressor shortens and no run-length check passes by -/// accident. A 64-bit LCG, so the host and nothing else decides the bytes. -fn pseudorandom(len: usize) -> Vec { - let mut state: u64 = 0x2545_F491_4F6C_DD1D; - (0..len) - .map(|_| { - state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); - (state >> 33) as u8 - }) - .collect() -} diff --git a/tests/common/storage.rs b/tests/common/storage.rs deleted file mode 100644 index 17d4cf416bf..00000000000 --- a/tests/common/storage.rs +++ /dev/null @@ -1,1156 +0,0 @@ -//! The interlock that keeps ToyOS off a disk it was not given. -//! -//! The claim under test is not "formatting works" -- `nvme_large_device` has -//! that -- but its negative: **a device the kernel was not given comes back -//! byte-for-byte unchanged.** That is asserted against the backing file, on -//! the host, because the guest's account of what it did to a disk is exactly -//! the thing in question. The stimulus is a disk that holds something, mounts -//! as nothing, and belongs to someone -- which a kernel reading "mount returned -//! None" as permission to format would take. - -use std::io::Write; -use std::path::Path; -use std::time::Duration; - -use toyos_build::fingerprint::{first_difference, whole_device}; - -use super::qemu::{self, BootOptions, QemuInstance}; - -/// fsd's word for a DATA partition that holds neither a volume of ours nor a -/// designation stamp: nothing is written to it. -const FOREIGN: &str = "fsd: no volume of ours and no designation stamp"; -/// fsd's word for a volume of ours that mounted. -const MOUNTED: &str = "fsd: mounted the DATA volume"; -/// fsd's word for a volume of ours that did not, followed by the reason. -const UNMOUNTABLE: &str = "fsd: the DATA volume is ours and does not mount ("; -/// fsd's word for DATA's directories served from memory. -pub(super) const IN_MEMORY: &str = "are in memory and will not survive a reboot"; - -/// Whether fsd said it serves DATA's directories as absent: every name under -/// them refused, and never a volume in memory under the paths an owner's data -/// lives at. -fn data_absent(log: &str) -> Result<(), String> { - if log.lines().any(|l| l.contains("fsd: Data serving") && l.contains(" — absent: ")) { - return Ok(()); - } - Err(format!("fsd never said it serves DATA's directories absent\n{log}")) -} - -/// Boot the guest against a disk that belongs to somebody else, and prove it -/// comes back untouched. -/// -/// Lives here so the registration hunk in `toyos.rs` stays one line: every -/// agent edits that file. -pub fn foreign_disk_untouched( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const BYTES: u64 = 128 * 1024 * 1024; - // The same directory `boot_with_options` uses, named here because this - // image has to exist before the boot that must not touch it. - let dir = super::lane::dir(); - let image = dir.join("foreign-disk.img"); - let (data_at, _) = foreign_disk_image(&image, BYTES); - let before = whole_device(&image); - - // The premise, checked before the boot rather than assumed: if this volume - // somehow already parsed as a ToyOS volume, the kernel would mount it and - // the assertion below would pass for the wrong reason. - if front(&image, data_at, 4) == *b"BCFS" { - return Err("the foreign volume starts with a bcachefs superblock".to_string()); - } - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - nvme_image: Some(image.clone()), - ..Default::default() - }, - ); - - // The boot log, not a post-ready drain: every line this test cares about - // is printed in the storage phase, long before the ready marker. - let log = qemu.boot_log().to_string(); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?}: refusing a disk must not be fatal\n{log}")); - } - } - // The refusal is stated, not inferred. A file server that never reached - // the partition would also leave the image untouched. - if !log.contains(FOREIGN) { - return Err(format!("fsd never said {FOREIGN:?} — did it reach the partition?\n{log}")); - } - // And the machine still came up, because a refusal that costs the boot is - // a refusal nobody will leave switched on. - if !log.contains("Boot: complete") { - return Err(format!("the boot did not complete on a disk it refused\n{log}")); - } - // Independent of anything that reaches the platter, and deliberately so: - // the byte comparison below can only see writes that were flushed, and a - // format that is still sitting in the page cache has already destroyed the - // disk as far as the next sync is concerned. - if log.contains("formatting it") { - return Err(format!("fsd decided to format a disk it was not given\n{log}")); - } - - // Shut down rather than kill: the shutdown's sync of every file server is - // what moves a format from fsd's cache to the device, so a killed QEMU - // fingerprints an image a formatting server would also have left untouched. - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} during shutdown\n{tail}")); - } - } - drop(qemu); - - let after = whole_device(&image); - if let Some(diff) = first_difference(&before, &after) { - return Err(format!("the kernel wrote to a disk it was not given: {diff}")); - } - let _ = std::fs::remove_file(&image); - Ok(()) -} - -/// The volume is genuine and the disk is not: block 0 here carries the magic, -/// the version and the CRC this crate wrote, and every other stimulus in this -/// file is refused before any of that is read. What it does not carry is this -/// device's block count, and a read-write mount writes on sight. -pub fn volume_from_another_disk( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const VOLUME_BLOCKS: u64 = 4096; - const DEVICE_BYTES: u64 = 128 * 1024 * 1024; - let dir = super::lane::dir(); - let image = dir.join("copied-volume.img"); - - let mut fs = bcachefs::Formatted::format(bcachefs::VecBlockIO::new(VOLUME_BLOCKS)) - .map_err(|e| format!("format a volume on the host: {e:?}"))?; - fs.create("stranger.txt", b"a file that was already here", 1) - .map_err(|e| format!("put a file on the host volume: {e:?}"))?; - let volume = fs - .into_io() - .map_err(|e| format!("sync the host volume: {e:?}"))? - .into_vec(); - - // The premise, checked before the boot: the guest's refusal below is about - // the device's size and not about an image nothing could have mounted. - bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(bcachefs::VecBlockIO::from_vec( - volume.clone(), - )) - .map_err(|e| format!("the volume this test wrote does not mount on its own device: {e:?}"))?; - - // On a device the volume was not formatted for, inside a partition it was - // not formatted for: the copy lands over the designation stamp, so the - // kernel finds a real superblock naming a block count that is not this - // partition's. - let file = std::fs::File::create(&image).map_err(|e| format!("create the image: {e}"))?; - file.set_len(DEVICE_BYTES).map_err(|e| format!("grow the device under the volume: {e}"))?; - let (at, _) = toyos_build::image::designate_data_disk(&image, DEVICE_BYTES); - { - use std::io::{Seek, SeekFrom, Write}; - let mut file = std::fs::OpenOptions::new() - .write(true) - .open(&image) - .map_err(|e| format!("open the image: {e}"))?; - file.seek(SeekFrom::Start(at)).map_err(|e| format!("seek: {e}"))?; - file.write_all(&volume).map_err(|e| format!("write the copied volume: {e}"))?; - } - let before = whole_device(&image); - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - nvme_image: Some(image.clone()), - ..Default::default() - }, - ); - let log = qemu.boot_log().to_string(); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?}: refusing a copied volume must not be fatal\n{log}")); - } - } - if log.contains(MOUNTED) { - return Err(format!( - "fsd mounted a volume that did not come from this disk: it said {MOUNTED:?}\n{log}" - )); - } - // A superblock of ours that does not describe this device is a volume of - // ours that did not mount, not another's disk. - let refused = format!("{UNMOUNTABLE}BadSuperblock"); - if !log.contains(&refused) { - return Err(format!("fsd never said {refused:?} — did it reach the partition?\n{log}")); - } - if log.contains("formatting it") { - return Err(format!("fsd decided to format a disk it was not given\n{log}")); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not complete on a volume it refused\n{log}")); - } - - // Down through the shutdown's sync of every file server, the only thing - // that moves a write out of fsd's cache and onto the device. - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} during shutdown\n{tail}")); - } - } - drop(qemu); - - let after = whole_device(&image); - if let Some(diff) = first_difference(&before, &after) { - return Err(format!("the kernel wrote to a volume it refused: {diff}")); - } - let _ = std::fs::remove_file(&image); - Ok(()) -} - -/// A DATA volume of ours whose superblock broke, both copies of it: the boot -/// goes on with `/apps` and `/home` absent and the reason logged by name, and -/// never on a tmpfs, which would take the owner's writes into RAM under the -/// paths their data lives at. Absent rather than a refused boot because a -/// corrupt disk is input, and input never takes the kernel down. The oracle for -/// "nothing wrote to it" is the image, compared byte for byte after shutdown. -pub fn broken_data_volume_is_absent( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const DEVICE_BYTES: u64 = 128 * 1024 * 1024; - /// Inside the bytes the superblock's CRC covers, and past every field. - const FLIPPED: usize = 200; - let dir = super::lane::dir(); - let image = dir.join("broken-data-volume.img"); - - let file = std::fs::File::create(&image).map_err(|e| format!("create the image: {e}"))?; - file.set_len(DEVICE_BYTES).map_err(|e| format!("size the image: {e}"))?; - let (at, bytes) = toyos_build::image::designate_data_disk(&image, DEVICE_BYTES); - let blocks = bytes / 4096; - let mut fs = bcachefs::Formatted::format(bcachefs::VecBlockIO::new(blocks)) - .map_err(|e| format!("format the partition's volume on the host: {e:?}"))?; - fs.create("home/kept.txt", b"the owner's file", 1) - .map_err(|e| format!("put a file on the host volume: {e:?}"))?; - let mut volume = fs.into_io().map_err(|e| format!("sync the host volume: {e:?}"))?.into_vec(); - - // The premise, both halves: the volume mounts as written, so the refusal - // below is the flipped bytes' and not the partition's size. - let open = |raw: &[u8]| { - bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(bcachefs::VecBlockIO::from_vec(raw.to_vec())) - .err() - .map(|e| format!("{e:?}")) - }; - if let Some(e) = open(&volume) { - return Err(format!("the volume this test wrote does not mount before it is broken: {e}")); - } - let backup = (blocks as usize - 1) * 4096; - volume[FLIPPED] ^= 0xFF; - volume[backup + FLIPPED] ^= 0xFF; - match open(&volume) { - Some(e) if e.starts_with("ChecksumMismatch") => {} - other => return Err(format!("both superblocks flipped, and the host says {other:?}")), - } - { - use std::io::{Seek, SeekFrom}; - let mut file = std::fs::OpenOptions::new() - .write(true) - .open(&image) - .map_err(|e| format!("open the image: {e}"))?; - file.seek(SeekFrom::Start(at)).map_err(|e| format!("seek: {e}"))?; - file.write_all(&volume).map_err(|e| format!("write the broken volume: {e}"))?; - } - let before = whole_device(&image); - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - nvme_image: Some(image.clone()), - ..Default::default() - }, - ); - let log = qemu.boot_log().to_string(); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?}: a broken volume must not be fatal\n{log}")); - } - } - for said in [&format!("{UNMOUNTABLE}ChecksumMismatch"), "Boot: complete"] { - if !log.contains(said) { - return Err(format!("the boot never said {said:?}\n{log}")); - } - } - data_absent(&log)?; - for unsaid in [IN_MEMORY, MOUNTED, "formatting it", FOREIGN] { - if log.contains(unsaid) { - return Err(format!("fsd said {unsaid:?} of a volume of ours that broke\n{log}")); - } - } - - // The kernel's own log line and the unchanged image are not a guest's - // account of what it sees: this asks one, in the same boot, before - // anything is asleep to answer for /home the way a stray tmpfs mount or a - // `home` still forced true would. - let result = qemu.run_test("test_rs_home_absent", Duration::from_secs(20)); - if result.exit_code != Some(0) { - return Err(format!( - "home_absent guest failed — /apps, /config, /home, /state or /home/toy answered a write, \ - a chdir or a listing that an absent DATA volume must refuse:\n{}\nkernel log while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} during shutdown\n{tail}")); - } - } - drop(qemu); - - let after = whole_device(&image); - if let Some(diff) = first_difference(&before, &after) { - return Err(format!("the kernel wrote to a volume it could not mount: {diff}")); - } - let _ = std::fs::remove_file(&image); - eprintln!(" [storage] a broken DATA volume left /apps and /home absent, and the image unchanged"); - Ok(()) -} - -/// A TOYOS-DATA partition the GPT type names ours, but whose start blockd -/// refuses to serve: the owner's ruling is that this is -/// `Absent`, the same as a volume of ours that did not mount, and never -/// `Volatile` — a tmpfs is for a machine that carries no data volume at all, -/// not for one whose candidate is ours and unreadable by geometry. -pub fn data_candidate_with_bad_geometry_is_absent( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const DEVICE_BYTES: u64 = 128 * 1024 * 1024; - let dir = super::lane::dir(); - let image = dir.join("misaligned-data.img"); - - let file = std::fs::File::create(&image).map_err(|e| format!("create the image: {e}"))?; - file.set_len(DEVICE_BYTES).map_err(|e| format!("size the image: {e}"))?; - toyos_build::image::misaligned_data_disk(&image, DEVICE_BYTES); - let before = whole_device(&image); - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - nvme_image: Some(image.clone()), - ..Default::default() - }, - ); - let log = qemu.boot_log().to_string(); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?}: a misaligned candidate must not be fatal\n{log}")); - } - } - for said in ["is not served: LBA", "not whole", "Boot: complete"] { - if !log.contains(said) { - return Err(format!("the boot never said {said:?}\n{log}")); - } - } - data_absent(&log)?; - for unsaid in [IN_MEMORY, MOUNTED, "formatting it"] { - if log.contains(unsaid) { - return Err(format!("fsd said {unsaid:?} of a partition its own GPT type names ours\n{log}")); - } - } - - let result = qemu.run_test("test_rs_home_absent", Duration::from_secs(20)); - if result.exit_code != Some(0) { - return Err(format!( - "home_absent guest failed on a partition blockd refused:\n{}\nkernel log while it \ - ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} during shutdown\n{tail}")); - } - } - drop(qemu); - - let after = whole_device(&image); - if let Some(diff) = first_difference(&before, &after) { - return Err(format!("the kernel wrote to a candidate it could not open a view over: {diff}")); - } - let _ = std::fs::remove_file(&image); - eprintln!( - " [storage] a TOYOS-DATA candidate with bad geometry left /apps and /home absent, and \ - the image unchanged" - ); - Ok(()) -} - -/// A disk carrying a TOYOS-DATA partition that is somebody else's, and where -/// it landed. -/// -/// **The partition is ToyOS-typed on purpose**: a disk with no such partition -/// is refused before block 0 is read and could not exercise the probe at all. -/// Here the kernel finds the candidate, opens the view, reads block 0, and has -/// to refuse it there — the volume holding neither a bcachefs superblock nor a -/// designation stamp is the only property that matters. -pub fn foreign_disk_image(path: &Path, len: u64) -> (u64, u64) { - use std::io::{Seek, SeekFrom, Write}; - - let file = std::fs::File::create(path).expect("create foreign image"); - file.set_len(len).expect("size foreign image"); - let (at, bytes) = toyos_build::image::designate_data_disk(path, len); - - // Over the stamp the writer left: consent is what this disk must not carry. - let mut volume = [0u8; 4096]; - volume[3..11].copy_from_slice(b"NTFS "); - volume[510] = 0x55; - volume[511] = 0xAA; - - let mut file = std::fs::OpenOptions::new().write(true).open(path).expect("open foreign image"); - file.seek(SeekFrom::Start(at)).expect("seek"); - file.write_all(&volume).expect("write the foreign volume's first block"); - (at, bytes) -} - -/// The `n` bytes at `at`, for a premise that is about one block of the image -/// rather than about all of it. -fn front(path: &Path, at: u64, n: usize) -> Vec { - use std::io::{Read, Seek, SeekFrom}; - - let mut head = vec![0u8; n]; - let mut file = std::fs::File::open(path).expect("open image"); - file.seek(SeekFrom::Start(at)).expect("seek into the image"); - file.read_exact(&mut head).expect("read the front of the volume"); - head -} - -/// A same-length overwrite on `/home` read back through the name it rebound. -/// -/// The oracle is outside the guest and outside the kernel: with the machine -/// gone the file is read off the NVMe image by this crate's own build of the -/// `bcachefs` reader over a plain seek-and-read device, and its length held -/// against the length the guest printed for its own read of the same name. The -/// recorded defect is exactly those two disagreeing. -pub fn home_overwrite_reads_back( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Mirrored in `tests/toyos-rust-tests/src/bin/home_overwrite_zero.rs`. - const PINNED: &str = "home/overwrite-pinned.bin"; - const LEN: usize = 1_902_104; - fn payload(seed: u8) -> Vec { - (0..LEN).map(|i| (i.wrapping_mul(131) ^ seed as usize) as u8).collect() - } - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { profile: qemu::Profile::MetalDisk, ..Default::default() }, - ); - let boot = qemu.boot_log().to_string(); - if boot.contains(IN_MEMORY) { - return Err(format!( - "/apps and /home fell back to memory, so nothing below touches the NVMe path:\n{boot}" - )); - } - - let result = qemu.run_test("test_rs_home_overwrite_zero", Duration::from_secs(240)); - let log = format!("{boot}\n{}{}{}", result.before, result.stdout, result.serial); - let said = log.lines().find(|l| l.contains("HOME-OVERWRITE")).map(str::trim).map(String::from); - let guest_len: Option = said - .as_deref() - .and_then(|l| l.split_whitespace().rev().nth(1)) - .and_then(|n| n.parse().ok()); - - let image = qemu.nvme_image().to_path_buf(); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} on the way down\n{tail}")); - } - } - - let io = FileBlocks::open(&image)?; - let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(io) - .map_err(|e| format!("the NVMe image does not mount on the host: {e:?}"))?; - let got = fs - .read_file(PINNED) - .map_err(|e| format!("reading {PINNED} off the image: {e:?}"))?; - - // Before the exit code: that disagreement is the defect's sentence, and an exit code does not say it. - let Some(guest_len) = guest_len else { - return Err(format!( - "the guest printed no HOME-OVERWRITE line, and the device holds {} bytes at \ - {PINNED}:\n{}{}{}", - got.len(), - result.before, - result.stdout, - result.serial - )); - }; - if guest_len != got.len() { - return Err(format!( - "the guest read {guest_len} bytes back from /{PINNED} and the device holds {} — \ - the overwrite reached the device and the name did not answer for it\n{}", - got.len(), - said.unwrap_or_default() - )); - } - if got != payload(0x22) { - let at = got.iter().zip(payload(0x22)).position(|(a, b)| *a != b); - return Err(format!( - "{PINNED} on the device is {} bytes, first differing at {at:?}", - got.len() - )); - } - if result.exit_code != Some(0) { - return Err(format!( - "home_overwrite_zero guest failed:\n{}\nkernel log while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - - eprintln!( - " [overwrite] the guest's {guest_len} bytes and the device's {} agree at /{PINNED}, off \ - the NVMe image via the host's own bcachefs reader", - got.len() - ); - Ok(()) -} - -/// A file server killed with a write done and unanswered loses nothing a -/// client was told was flushed, and its clients go on; ended past init's -/// budget, its directories answer `Gone`. Judged off the device. -/// -/// `tests/fsdrestartcase` arms every file server to end under a write to -/// `/home/fsd_end` (`--end-on`) and at the first read of an installed -/// package's manifest and binary (`--end-at-read`), and `test_rs_fs_restart` -/// ends DATA's four times, the first two under init's own resolution of a -/// launch and its read of the image: the guest asserts what a client sees, -/// init's and fsd's own lines say who ended and who started again, and with -/// the machine down the DATA partition is read by this crate's own build of -/// the `bcachefs` reader over a plain seek-and-read of the image — nothing the -/// guest executed. The flushed file holds its bytes there. -/// -/// `test_rs_fs_client_bound` runs first on the same boot, which nothing else -/// needs DATA on while it holds every client slot. -pub fn fsd_restart( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Mirrored in `tests/toyos-rust-tests/src/bin/fs_restart.rs`, without the - /// mount point. - const KEPT: &str = "home/fs_restart/kept"; - const ACROSS: &str = "home/fs_restart/across"; - const KEPT_LEN: usize = 64 * 1024 + 13; - const ACROSS_BYTES: &[u8] = b"renamed over the file a handle held across the end"; - let kept: Vec = (0..KEPT_LEN).map(|i| (i.wrapping_mul(37) ^ 0xC3) as u8).collect(); - - let config = super::compile::repo_root().join("tests/fsdrestartcase"); - let mut qemu = QemuInstance::boot_with_options( - &config, - c_bins, - rust_bins, - BootOptions { profile: qemu::Profile::Metal, ..Default::default() }, - ); - let boot = qemu.boot_log().to_string(); - if !boot.contains(MOUNTED) && !boot.contains("formatting it") { - return Err(format!("fsd served DATA from no partition, so nothing here reaches a device:\n{boot}")); - } - let bound = qemu.run_test("test_rs_fs_client_bound", Duration::from_secs(60)); - if bound.exit_code != Some(0) || !bound.stdout.contains("fs_client_bound: PASS") { - return Err(format!("fs_client_bound guest failed:\n{}\nconsole:\n{}{}", bound.stdout, bound.before, bound.serial)); - } - let result = qemu.run_test("test_rs_fs_restart", Duration::from_secs(120)); - let image = qemu.nvme_image().to_path_buf(); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - // The console once: `stdout` is the same lines again, unprefixed. - let log = format!("{boot}\n{}{}{}{}{tail}", bound.before, bound.serial, result.before, result.serial); - if result.exit_code != Some(0) || !result.stdout.contains("fs_restart: PASS") { - return Err(format!("fs_restart guest failed:\n{}\nconsole:\n{log}", result.stdout)); - } - let console = super::serial::Serial::named("fsd_restart", log.as_str()); - let ended = log.matches("fsd: --end-on: ending with a write done and unanswered").count(); - if ended != 2 { - return Err(format!("fsd said it ended under a write {ended} times, not the guest's 2:\n{log}")); - } - for read in ["apps/fs_restart/manifest.toml", "apps/fs_restart/fs_restart"] { - let said = format!("fsd: --end-at-read: ending before the first read of {read} is answered"); - let at_read = log.matches(&said).count(); - if at_read != 1 { - return Err(format!("fsd said {said:?} {at_read} times, not the launch's 1:\n{log}")); - } - } - let restarted = log.lines().filter(|l| l.contains("init: fsd data (pid ") && l.contains("ended; started again")).count(); - if restarted != 3 { - return Err(format!("init started DATA's server again {restarted} times, not 3:\n{log}")); - } - console.must_say("init: fsd data ended 4 times in 10 s; its ports are closed")?; - console.must_be_clean()?; - - let io = FileBlocks::open(&image)?; - let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(io) - .map_err(|e| format!("the DATA partition does not mount on the host: {e:?}"))?; - for (name, want) in [(KEPT, &kept[..]), (ACROSS, ACROSS_BYTES)] { - let got = fs.read_file(name).map_err(|e| format!("reading {name} off the image: {e:?}"))?; - if got != want { - let at = got.iter().zip(want).position(|(a, b)| a != b); - return Err(format!( - "{name} on the device is {} bytes, first differing at {at:?}: a write the guest \ - was told was flushed is not what the device holds", - got.len() - )); - } - } - eprintln!( - " [fsd] DATA's server ended four times, the first two under init's resolution and image \ - read of a launch that was answered; started again three, every handle held across an \ - end answered Gone, new opens answered, the fourth closed /home to Gone; {KEPT} and \ - {ACROSS} read back off the image by the host's own bcachefs reader" - ); - Ok(()) -} - -/// DATA's first file server ending before it accepts a connection that init's -/// own file worker is waiting on costs init nothing: it starts the server -/// again, the waiting call goes on to it, and the boot reaches the ready -/// marker with the session home made. `tests/fsdmountcase` arms the end -/// (`--end-at-mount data`); the home is judged off the image by the host's own -/// bcachefs reader once the machine is down. -pub fn fsd_end_at_mount( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const ENDED: &str = "fsd: --end-at-mount: ending with a connection waiting and unaccepted"; - let config = super::compile::repo_root().join("tests/fsdmountcase"); - let mut qemu = QemuInstance::boot_with_options( - &config, - c_bins, - rust_bins, - BootOptions { profile: qemu::Profile::Metal, ..Default::default() }, - ); - let boot = qemu.boot_log().to_string(); - let image = qemu.nvme_image().to_path_buf(); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - let log = format!("{boot}\n{tail}"); - let ended = log.matches(ENDED).count(); - if ended != 1 { - return Err(format!("fsd said {ENDED:?} {ended} times, not once:\n{log}")); - } - let restarted = log.lines().filter(|l| l.contains("init: fsd data (pid ") && l.contains("ended; started again")).count(); - if restarted != 1 { - return Err(format!("init started DATA's server again {restarted} times, not once:\n{log}")); - } - let console = super::serial::Serial::named("fsd_end_at_mount", log.as_str()); - console.must_not_say("session home")?; - console.must_be_clean()?; - - let home = toyos_manifest::session_home(); - let home = home.trim_start_matches('/'); - let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(FileBlocks::open(&image)?) - .map_err(|e| format!("the DATA partition does not mount on the host: {e:?}"))?; - if !fs.is_dir(home).map_err(|e| format!("asking the image for {home}: {e:?}"))? { - return Err(format!("{home} is not on the DATA volume: init made no session home\n{log}")); - } - eprintln!(" [fsd] DATA's first server ended under init's waiting call; init started it again and {home} is on the image"); - Ok(()) -} - -/// A partition claim held elsewhere refuses its file server's restart by name, -/// and the role's paths are then Gone: no server answers them. -/// -/// DATA is on a USB stick the kernel drives, so its server holds the -/// partition's claim, and the NVMe disk carries no table, so the stick's is -/// the machine's one DATA. `tests/fsdclaimcase` arms `--let-go-at-read`, and -/// `test_rs_fs_claim_held` takes the claim the server let go, ends the server, -/// and holds the claim until init has answered the role's restart. -pub fn fsd_claim_held( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Mirrored in `tests/toyos-rust-tests/src/bin/fs_claim_held.rs`. - const DATA: &str = "7E2B4C6D-8F1A-4B3C-9D5E-6F7A8B9C0D1E"; - const MIB: u64 = 1024 * 1024; - const LET_GO: &str = "fsd: --let-go-at-read: home/fsd_let_go: the partition is let go"; - const ENDED: &str = "fsd: --let-go-at-read: ending at its client's next request"; - const REFUSED: &str = "init: fsd data ended and would not start again ("; - const HELD: &str = "is already claimed); its ports are closed"; - - let stick = super::lane::dir().join("fsd-claim-held.img"); - let data = ("ToyOS data", 96 * MIB, toyos_gpt::Guid::TOYOS_DATA_TEXT, DATA, super::partclaim::ALIGNED); - let (mut device, spans) = super::partclaim::table(&stick, 100 * MIB, &[data])?; - super::partclaim::designate(&mut *device, spans[0])?; - device.flush().map_err(|e| format!("flush the stick: {e}"))?; - drop(device); - - let config = super::compile::repo_root().join("tests/fsdclaimcase"); - let mut qemu = QemuInstance::boot_with_options( - &config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::UsbDisk, - usb_images: vec![stick.clone()], - nvme_image: Some(super::partclaim::tableless_nvme("fsd-claim-held-nvme.img")?), - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - // The premise: DATA's first server holds the stick's partition. - if !boot.contains("fsd: block 0 designates this partition for ToyOS; formatting it") { - return Err(format!("fsd never formatted DATA off the stick, so no server held its claim:\n{boot}")); - } - let result = qemu.run_test("test_rs_fs_claim_held", Duration::from_secs(60)); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - let log = format!("{boot}\n{}{}{tail}", result.before, result.serial); - if result.exit_code != Some(0) || !result.stdout.contains("fs_claim_held: PASS") { - return Err(format!("fs_claim_held guest failed:\n{}\nconsole:\n{log}", result.stdout)); - } - let console = super::serial::Serial::named("fsd_claim_held", log.as_str()); - console.must_say(LET_GO)?; - console.must_say(ENDED)?; - let Some(refused) = log.lines().find(|l| l.contains(REFUSED) && l.contains(HELD)) else { - return Err(format!("init never said DATA's restart was refused for the held claim:\n{log}")); - }; - console.must_be_clean()?; - let _ = std::fs::remove_file(&stick); - eprintln!(" [fsd] {}", refused.trim()); - Ok(()) -} - -/// Two DATA partitions, one on a stick the kernel drives and one on the NVMe -/// disk blockd serves, are refused by name and never guessed between: DATA is -/// absent, nothing stands in from memory, and neither is formatted — the -/// stick is held byte for byte against what it carried before the boot. -pub fn fsd_two_data( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const DATA: &str = "5A1C0E2B-3D4F-4A6B-8C9D-0E1F2A3B4C5D"; - const MIB: u64 = 1024 * 1024; - const REFUSED: &str = "fsd: this machine has 2 DATA partitions, 1 on the kernel's disks and 1 the block \ - service serves, and a volume is one; DATA is absent this boot"; - - let stick = super::lane::dir().join("fsd-two-data.img"); - let data = ("ToyOS data", 96 * MIB, toyos_gpt::Guid::TOYOS_DATA_TEXT, DATA, super::partclaim::ALIGNED); - let (mut device, spans) = super::partclaim::table(&stick, 100 * MIB, &[data])?; - super::partclaim::designate(&mut *device, spans[0])?; - device.flush().map_err(|e| format!("flush the stick: {e}"))?; - drop(device); - let before = whole_device(&stick); - - // The lane's blank NVMe image is the second: a designated DATA partition. - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { profile: qemu::Profile::UsbDisk, usb_images: vec![stick.clone()], ..Default::default() }, - ); - let boot = qemu.boot_log().to_string(); - // Shut down rather than kill: a format sitting in a server's cache reaches - // the device at the stop's sync, and the stick is judged after it. - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - let log = format!("{boot}\n{tail}"); - let console = super::serial::Serial::named("fsd_two_data", log.as_str()); - console.must_say(REFUSED)?; - data_absent(&log)?; - console.must_not_say(IN_MEMORY)?; - console.must_not_say("formatting it")?; - console.must_be_clean()?; - if let Some(diff) = first_difference(&before, &whole_device(&stick)) { - return Err(format!("a DATA partition of two was written: {diff}\n{log}")); - } - let _ = std::fs::remove_file(&stick); - eprintln!(" [fsd] two DATA partitions, one per source, refused by name; the stick untouched"); - Ok(()) -} - -/// `/apps` and `/home` are two paths into one filesystem, judged off the device. -/// -/// The guest writes one file under each and shuts down; the host then finds -/// both in **one** bcachefs volume on the NVMe image, through this crate's own -/// build of the reader over a plain seek-and-read device. A second filesystem -/// behind the second path could not answer for both names out of one mount. -pub fn apps_and_home_are_one_filesystem( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Mirrored in `tests/toyos-rust-tests/src/bin/hierarchy_paths.rs`, without - /// the mount point: the volume carries `/home/x` as `home/x`. - const IN_HOME: &str = "home/hierarchy-home.bin"; - const IN_APPS: &str = "apps/hierarchy-apps.bin"; - const LEN: usize = 2 * 4096 + 61; - fn payload(seed: u8) -> Vec { - (0..LEN).map(|i| (i.wrapping_mul(53) ^ seed as usize) as u8).collect() - } - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { profile: qemu::Profile::MetalDisk, ..Default::default() }, - ); - let boot = qemu.boot_log().to_string(); - if boot.contains(IN_MEMORY) { - return Err(format!( - "/apps and /home fell back to memory, so the readback below would judge no device:\n\ - {boot}" - )); - } - - let result = qemu.run_test("test_rs_hierarchy_paths", Duration::from_secs(60)); - if result.exit_code != Some(0) { - return Err(format!( - "hierarchy_paths guest failed:\n{}\nkernel log while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - - let image = qemu.nvme_image().to_path_buf(); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} on the way down\n{tail}")); - } - } - - // Which volume this is, taken from the volume and not from the reader: - // `Formatted::format` leaves the UUID zero on one nothing named, so a UUID - // here would be a constant every image satisfies. The block count the - // superblock records is not — it says the guest formatted this partition - // and no other span of the device. - let (at, bytes) = toyos_build::image::data_partition_of(&image)?; - let blocks = bytes / 4096; - let sb = superblock_at(&image, at / 4096)?; - if sb.block_count != blocks { - return Err(format!( - "the volume on the image was formatted for {} blocks and the DATA partition is \ - {blocks}", - sb.block_count - )); - } - - let io = FileBlocks::open(&image)?; - let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(io) - .map_err(|e| format!("the NVMe image's DATA partition does not mount: {e:?}"))?; - for (name, seed) in [(IN_HOME, 0xA5u8), (IN_APPS, 0x5A)] { - let got = fs - .read_file(name) - .map_err(|e| format!("reading {name} off the DATA partition: {e:?}"))?; - if got != payload(seed) { - let at = got.iter().zip(payload(seed)).position(|(a, b)| *a != b); - return Err(format!( - "{name} on the device is {} bytes, first differing at {at:?}", - got.len() - )); - } - } - - eprintln!( - " [hierarchy] {IN_HOME} and {IN_APPS}, {LEN} bytes each, both in the one filesystem the \ - DATA partition at byte {at} carries, formatted for its own {blocks} blocks" - ); - Ok(()) -} - -/// `/boot` and `/log` off the same NVMe device the machine booted from, both -/// served by fsd through blockd, which refuses ROOT to every session. -/// -/// The oracle is outside the guest and outside fsd's FAT32: `logd`'s -/// file is read off the image by `fatfs` and the volume judged against -/// fatgen103 by `toyos-fat32-check`, with the guest already halted. -pub fn internal_disk_boot( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = || BootOptions { - profile: qemu::Profile::InternalDisk, - boot_image: None, - ..Default::default() - }; - - // The argv is the only place a device's *absence* is visible. - let argv = qemu::profile_argv(&options()); - for banned in ["usb-storage", "nec-usb-xhci", "usb-kbd", "usb-mouse", "usb-tablet"] { - if let Some(a) = argv.iter().find(|a| a.contains(banned)) { - return Err(format!("{a:?} on the machine whose point is having no USB disk")); - } - } - let controllers: Vec<&String> = - argv.iter().filter(|a| a.starts_with("nvme,serial=")).collect(); - if controllers != ["nvme,serial=bootdisk,id=nvmebootctl,bootindex=0,msix-exclusive-bar=on"] { - return Err(format!( - "the machine's NVMe controllers are {controllers:?} — this profile's whole shape is \ - one controller, carrying the boot image" - )); - } - - // Built here, not by `boot_with_options`: the log partition is read back off this exact file. - let dir = super::lane::dir(); - let image = dir.join("internal-disk-boot.img"); - let bytes = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - std::fs::write(&image, &bytes).map_err(|e| format!("write the boot image: {e}"))?; - let (log_start, log_len) = super::volumes::log_extent(&bytes, &image)?; - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { boot_image: Some(qemu::Staged::Written(image.clone())), ..options() }, - ); - let boot = qemu.boot_log().to_string(); - for bad in ["PANIC:", "panicked at"] { - if boot.contains(bad) { - return Err(format!("{bad:?} booting off the internal disk\n{boot}")); - } - } - - // This machine has no USB, so a volume fsd serves came through blockd. - for said in [super::volumes::BOOT_SERVED, super::volumes::LOG_SERVED] { - if !boot.contains(said) { - return Err(format!( - "the boot never said {said:?} — a machine booting off its internal disk got \ - no /boot and no /log\n{boot}" - )); - } - } - if !boot.contains("this machine runs from; refusing every session to it") { - return Err(format!("blockd never said it refuses ROOT, which the machine runs from\n{boot}")); - } - - // Down, not killed: the file logd wrote reaches the device on the way out. - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} on the way down\n{tail}")); - } - } - drop(qemu); - - let after = std::fs::read(&image).map_err(|e| format!("read the boot image back: {e}"))?; - let volume = &after[log_start..log_start + log_len]; - let complaints = toyos_fat32_check::check(volume); - if !complaints.is_empty() { - return Err(format!( - "the log volume the internal-disk boot left behind is not a FAT32 fatgen103 \ - recognises:\n{}", - toyos_fat32_check::describe(&complaints) - )); - } - let (name, on_device) = super::volumes::newest_log(&image, log_start, log_len)?; - if on_device.is_empty() { - return Err(format!("/log/{name} on the internal disk is empty")); - } - let text = String::from_utf8_lossy(&on_device); - if !text.contains("Boot: complete") { - return Err(format!( - "/log/{name} is {} bytes off the device and carries no boot record — logd mounted \ - nothing worth writing to\nit ends: {:?}", - on_device.len(), - text.lines().rev().take(3).collect::>().join(" | ") - )); - } - - let _ = std::fs::remove_file(&image); - eprintln!( - " [internal-disk] /boot and /log both off the boot NVMe through blockd, and /log/{name} came back \ - {} bytes through fatfs on a volume fatgen103 has nothing to say about", - on_device.len() - ); - Ok(()) -} - -/// The impostor the actuator offers fills every read with its own mark, so a -/// registry that took it is caught serving that mark for a device it is not. -pub fn block_duplicate_id( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["block-duplicate-id"]; - - let qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - kernel_params: PARAMS, - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - for bad in ["PANIC:", "panicked at"] { - if boot.contains(bad) { - return Err(format!("{bad:?}: refusing a duplicate id must not be fatal\n{boot}")); - } - } - - let verdict = boot - .lines() - .find(|l| l.contains("block-duplicate-id: ")) - .ok_or_else(|| format!("the kernel never staged the duplicate registration:\n{boot}"))? - .trim() - .to_string(); - - // `by_impostor` catches a table whose insert displaces; the count below - // catches one that appends. Both naive registries, both silent. - for want in [ - "refused=true", - "block 0 served=true", - "by_impostor=false", - ] { - if !verdict.contains(want) { - return Err(format!( - "a second device claiming a registered number was not refused — {want:?} is \ - missing from: {verdict}" - )); - } - } - let counts: Vec<&str> = verdict - .split("devices ") - .nth(1) - .unwrap_or_default() - .split(", block 0") - .next() - .unwrap_or_default() - .split(" before and ") - .collect(); - match counts.as_slice() { - [before, after] if after.trim_end_matches(" after") == *before => {} - _ => { - return Err(format!( - "the device table changed size across a refused registration: {verdict}" - )) - } - } - if !boot.contains("Boot: complete") { - return Err(format!("the boot did not complete\n{boot}")); - } - - eprintln!(" [block] {verdict}"); - Ok(()) -} - -/// The bcachefs superblock in `image` at device block `block`. -pub fn superblock_at(image: &Path, block: u64) -> Result { - use std::io::{Read, Seek, SeekFrom}; - let mut f = std::fs::File::open(image).map_err(|e| format!("open {}: {e}", image.display()))?; - f.seek(SeekFrom::Start(block * 4096)).map_err(|e| format!("seek: {e}"))?; - let mut buf = bcachefs::BlockBuf::zeroed(); - f.read_exact(buf.as_bytes_mut()).map_err(|e| format!("read: {e}"))?; - bcachefs::Superblock::parse(&buf).map_err(|e| format!("{e:?}")) -} - -/// A disk image's DATA partition as a bcachefs block device: plain -/// seek-and-read, no cache and no kernel code. The partition is located through -/// the table by `toyos-gpt`, never at an offset this side computed. -pub struct FileBlocks { - file: std::cell::RefCell, - first: u64, - blocks: u64, -} - -impl FileBlocks { - pub fn open(path: &Path) -> Result { - let (at, bytes) = toyos_build::image::data_partition_of(path)?; - let file = std::fs::File::open(path) - .map_err(|e| format!("open {}: {e}", path.display()))?; - Ok(Self { - file: std::cell::RefCell::new(file), - first: at / 4096, - blocks: bytes / 4096, - }) - } -} - -/// A host file's I/O failure was attempted and failed; nothing here budgets. -struct HostIoFailed; -impl bcachefs::TransferError for HostIoFailed { - fn refused_before_attempt(&self) -> bool { - false - } -} - -impl bcachefs::BlockIO for FileBlocks { - fn read_block( - &self, - block: bcachefs::BlockNum, - buf: &mut bcachefs::BlockBuf, - ) -> Result<(), bcachefs::DeviceError> { - use std::io::{Read, Seek, SeekFrom}; - let mut file = self.file.borrow_mut(); - file.seek(SeekFrom::Start((self.first + block.raw()) * 4096)) - .map_err(|_| bcachefs::DeviceError::classify(&HostIoFailed))?; - file.read_exact(buf.as_bytes_mut()).map_err(|_| bcachefs::DeviceError::classify(&HostIoFailed)) - } - - fn write_block( - &self, - _block: bcachefs::BlockNum, - _buf: &bcachefs::BlockBuf, - ) -> Result<(), bcachefs::DeviceError> { - Err(bcachefs::DeviceError::classify(&HostIoFailed)) - } - - fn block_count(&self) -> u64 { - self.blocks - } -} diff --git a/tests/common/swap.rs b/tests/common/swap.rs deleted file mode 100644 index f5e664f2723..00000000000 --- a/tests/common/swap.rs +++ /dev/null @@ -1,668 +0,0 @@ -//! A running service's binary replaced with no reboot, rehearsed in QEMU: netd -//! swapped for its own rebuild while `logd` streams to this host and -//! `/system/bin/swap`, run over ssh, carries the ask — and the three ways a swap -//! must leave the old service running. -//! -//! **The machine's own `/log` is the oracle**, read off the image behind the -//! guest's back once `reboot` over ssh has ended the boot: one `Boot: -//! complete` in it is the claim that nothing rebooted, the kernel's `spawn:` -//! record names the binary it loaded, and the stream's lines must be the -//! file's own in its order. - -use std::net::{Ipv4Addr, SocketAddr}; -use std::path::Path; -use std::time::Duration; - -use toyos_build::bootlog; -use toyos_build::metalswap::{self, Ask, Expect, Swapped}; -use toyos_build::metaltalk::Ssh; -use toyos_swap::Word; - -use super::lan::TalkBoot; -use super::logstream::Bench; -use super::qemu::{self, BootOptions, QemuInstance}; -use super::serial; - -/// The rehearsal on virtio-net: the brief's own machine, and the one whose -/// driver comes up the fastest. -pub const VIRTIO: Bench = - Bench { profile: qemu::Profile::Headless, config: "tests/swapcase", device: "virtio-net" }; - -/// A liveness guard on a guest that stopped talking, never a verdict. -const CEILING: Duration = Duration::from_secs(120); - -/// A booted talking guest with its ssh forward, the client to reach it, and -/// the log it serves, read from the moment `logd` opened its port. -struct Rig { - staged: TalkBoot, - stream: toyos_build::metaltalk::Stream, - guest: QemuInstance, - console: String, - ssh: Ssh, - forward: SocketAddr, -} - -impl Rig { - /// `binary` sent as netd's replacement, and the machine's word on it, let - /// go at once. - fn swap_netd(&self, binary: &Path, digest: &toyos_swap::Digest) -> Result { - self.ssh.swap(self.forward, "netd", binary, digest).and_then(|answered| answered.go()).map(|a| a.said.clone()) - } - - fn boot(name: &str, bench: Bench) -> Result { - Self::boot_armed(name, bench, &[]) - } - - /// [`Rig::boot`] on the test kernel, with `actuators` armed. - fn boot_armed(name: &str, bench: Bench, actuators: &'static [&'static str]) -> Result { - let staged = TalkBoot::stage_armed(name, bench, actuators)?; - let ssh_port = qemu::free_host_port(); - let options = BootOptions { ssh_port: Some(ssh_port), ..staged.options() }; - let mut guest = QemuInstance::boot_with_options(&staged.case, &[], &[], options); - let mut console = guest.boot_log().to_string(); - // `logd` serves its port before netd leases, and sshd binds only after. - for (marker, doing) in - [(super::logstream::SERVING, "logd to open its port"), ("sshd: listening on port 22", "sshd to listen")] - { - qemu::await_marker(&mut guest, &mut console, marker, doing)?; - } - let stream = super::logstream::reader(staged.log_port, &format!("{name}-stream.txt"))?; - let ssh = Ssh::at(&super::compile::repo_root(), staged.identity.private().to_path_buf())?; - let forward = SocketAddr::from((Ipv4Addr::LOCALHOST, ssh_port)); - Ok(Self { staged, stream, guest, console, ssh, forward }) - } - - fn swap(&self, service: &str, binary: &Path, named: Option) -> Result { - let swapped = metalswap::swap( - &self.stream, - &self.ssh, - Some(self.forward), - &Ask { service, binary, named }, - CEILING, - &self.staged.scratch, - )?; - for (word, detail) in &swapped.words { - eprintln!(" [swap] init: {}: {detail}", word.as_str()); - } - Ok(swapped) - } - - /// `metalswap::judge`'s verdict, and the rig back for what follows it. - fn judged(self, swapped: &Swapped, expect: Expect) -> Result { - match metalswap::judge(swapped, expect) { - Ok(said) => { - said.iter().for_each(|line| eprintln!(" [swap] {line}")); - Ok(self) - } - Err(bad) => Err(self.fail(format!("{} finding(s):\n {}", bad.len(), bad.join("\n ")))), - } - } - - /// `why`, the guest's whole console, and what `logd` and init wrote about - /// the stream and the swap into the `/log` the guest leaves when it is - /// stopped here. The console is in the red itself: the run's scratch goes - /// with the run, red or green. - fn fail(mut self, why: String) -> String { - self.console.push_str(&self.guest.drain_serial(Duration::from_secs(2))); - drop(self.guest); - let kept = format!("the guest's console:\n{}", self.console); - let said = match super::volumes::whole_log(&self.staged.image, self.staged.start, self.staged.len) { - Ok(file) => file - .into_iter() - .filter(|l| l.contains("logd: ") || l.contains("init: swap ") || l.contains("pcidev: ")) - .collect::>() - .concat(), - Err(e) => format!("/log could not be read: {e}\n"), - }; - format!("{why}\n {kept}\n /log's own lines about the stream and the swap:\n{said}") - } - - /// End the boot so `/log` is whole — `reboot` over ssh, asked as a program - /// whose connection is held until the machine goes, so sshd never ends it - /// for a client that left — and answer the file. - fn finish(mut self) -> Result<(Vec, Vec, TalkBoot), String> { - let asked = self.ssh.exec(self.forward, toyos_build::metaltalk::REBOOT, &self.staged.scratch); - eprintln!(" [swap] `reboot` {:?}", asked.map(|exec| exec.status)); - if let Err(why) = - qemu::await_marker(&mut self.guest, &mut self.console, bootlog::REBOOTING, "`reboot` over ssh") - { - return Err(self.fail(why)); - } - drop(self.guest); - serial::Serial::named("the swapping boot", self.console.as_str()).must_be_clean()?; - let file = super::volumes::whole_log(&self.staged.image, self.staged.start, self.staged.len)?; - let streamed = self.stream.lines(); - super::logstream::is_prefix_of(&streamed, &file)?; - let boots = file.iter().filter(|l| l.contains(bootlog::COMPLETE)).count(); - if boots != 1 { - return Err(format!("/log holds {boots} `Boot: complete` record(s), where one boot owes one")); - } - Ok((file, streamed, self.staged)) - } -} - -/// A copy of the build's own `name` binary in `dir`, which is what a swap -/// rehearsal sends as that service's rebuild. -fn rebuilt(name: &str, dir: &Path) -> Result { - let to = dir.join(format!("{name}.rebuilt")); - toyos_build::build::copy_guest_program(&super::compile::repo_root(), super::qemu::SUITE_ARCH, name, &to)?; - Ok(to) -} - -/// **Asks that must change nothing**: the right binary under the wrong digest, -/// the right binary under the right digest from a key the image does not -/// authorize, and half the binary — under its whole length, and with none. -/// Each leaves netd as it was — no `stopping` word, the machine answering over -/// the same netd — and `/log` shows netd spawned once. -pub fn swap_refusals( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let rig = Rig::boot("swap-refusals", VIRTIO)?; - let binary = rebuilt("netd", &rig.staged.scratch)?; - - let mut wrong = toyos_swap::digest(&std::fs::read(&binary).map_err(|e| e.to_string())?); - wrong[0] ^= 1; - let swapped = match rig.swap("netd", &binary, Some(wrong)) { - Ok(swapped) => swapped, - Err(why) => return Err(rig.fail(why)), - }; - let rig = rig.judged(&swapped, Expect::Refused)?; - if !swapped.answer.as_deref().unwrap_or("").contains("hashes to") { - return Err(format!("the wrong digest was refused as {:?}, not for its hash", swapped.answer)); - } - - let stranger = super::ssh::Identity::mint(super::ssh::STRANGER_KEY)?; - let outsider = Ssh::at(&super::compile::repo_root(), stranger.private().to_path_buf())?; - let digest = toyos_swap::digest(&std::fs::read(&binary).map_err(|e| e.to_string())?); - match outsider.swap(rig.forward, "netd", &binary, &digest) { - Err(why) if why.contains("refused this key") => { - eprintln!(" [swap] a key the image does not authorize: {why}") - } - other => return Err(format!("a stranger's swap was answered {other:?}")), - } - - // **An input that ends early is no binary**: half of netd under its whole - // length and digest, and half of netd with no length at all — the form a - // cut connection could not be told apart in, which is gone. Each is - // refused before init hears of it. - let whole = std::fs::read(&binary).map_err(|e| e.to_string())?; - let cut = rig.staged.scratch.join("netd.cut"); - std::fs::write(&cut, &whole[..whole.len() / 2]).map_err(|e| e.to_string())?; - let hex: String = toyos_swap::digest(&whole).iter().map(|b| format!("{b:02x}")).collect(); - let port = rig.forward.port(); - for (command, owed) in [ - (format!("swap netd {hex} {}", whole.len()), format!("the input ended before the {} bytes it promised", whole.len())), - ("swap netd".to_string(), "usage: swap ".to_string()), - ] { - let exec = super::ssh::ssh_pipe(super::ssh::HOST, port, &rig.staged.identity, &command, &cut)?; - if exec.status != Some(1) || !exec.stdout_text().starts_with("unasked ") || !exec.stdout_text().contains(&owed) { - let said = exec.stdout_text(); - return Err(rig.fail(format!("`{command}` with half of netd ended {:?} saying {said:?}, where {owed:?} is owed", exec.status))); - } - eprintln!(" [swap] `{command}` with half of netd: {owed}"); - } - let (file, streamed, staged) = rig.finish()?; - let stopped: Vec<&String> = - streamed.iter().chain(&file).filter(|l| toyos_swap::heard(l, "netd").is_some_and(|(w, _)| w == Word::Stopping)).collect(); - if !stopped.is_empty() { - return Err(format!("init stopped netd for a refused swap: {stopped:?}")); - } - let spawns = file.iter().filter(|l| l.contains("spawn: ") && l.contains("/netd")).count(); - if spawns != 1 { - return Err(format!("/log records {spawns} spawn(s) of netd where the boot's own is the only one owed")); - } - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} - -/// The replacement a DMA control swaps in: it stops the 82574 the way netd does -/// before its first grant, and does nothing else. -const IDLE: &str = "swap_claim_idle"; - -/// Its line once the part is mastering, which opens the window. -const HOLDING: &str = "swap_claim_idle: holding the NIC mastering"; - -/// The replacement the residue control swaps in: it masters the 82574 with its -/// receive unit as netd left it. -const RUNNING: &str = "swap_claim_running"; - -/// Its line once the part is mastering. -const RUNNING_HOLDING: &str = "swap_claim_running: holding the NIC mastering"; - -/// The actuator that releases every function as though nothing could reset it. -const RESET_NOTHING: &[&str] = &["pcidev-reset-nothing"]; - -/// Connects to the forward that slirp's listener completed inside the window, -/// each one a SYN slirp sends the guest's address: the window closes on the -/// last of them. -const KNOCKS: usize = 25; - -/// netd swapped for `replacement` (the test binary `name`), and from the moment -/// it says `holding` — its part mastering — [`KNOCKS`] SYNs sent through slirp -/// at the guest's address. Answers how many slirp completed; `Err` is a why the -/// caller fails the rig with. -fn swap_and_knock(rig: &mut Rig, rust_bins: &[(String, Vec)], name: &str, holding: &str) -> Result { - use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; - use std::sync::Arc; - - let replacement = test_binary(rust_bins, name)?; - let binary = rig.staged.scratch.join(name); - std::fs::write(&binary, replacement).map_err(|e| format!("{}: {e}", binary.display()))?; - let answer = rig.swap_netd(&binary, &toyos_swap::digest(replacement)); - eprintln!(" [swap] the swap was answered {answer:?}"); - init_accepted(&answer)?; - qemu::await_marker(&mut rig.guest, &mut rig.console, holding, "the replacement holding the part mastering")?; - // From the holding line on, so every frame lands while the replacement - // holds the part. - let (stop, taken) = (Arc::new(AtomicBool::new(false)), Arc::new(AtomicUsize::new(0))); - let knocking = { - let (stop, taken, at) = (Arc::clone(&stop), Arc::clone(&taken), rig.forward); - std::thread::spawn(move || { - while !stop.load(Ordering::SeqCst) { - if std::net::TcpStream::connect_timeout(&at, Duration::from_millis(200)).is_ok() { - taken.fetch_add(1, Ordering::SeqCst); - } - std::thread::sleep(Duration::from_millis(20)); - } - }) - }; - let knocked = qemu::await_guest(&mut rig.guest, &mut rig.console, "the host's frames at the part", |_| { - taken.load(Ordering::SeqCst) >= KNOCKS - }); - stop.store(true, Ordering::SeqCst); - let _ = knocking.join(); - knocked?; - Ok(taken.load(Ordering::SeqCst)) -} - -/// **The part keeps running across a release, and the next holder stops it -/// before its first grant.** netd on QEMU's 82574 is swapped for a program that -/// takes the function, reports the receive and transmit enables it inherited, -/// runs `toyos_i219::quiesce` — netd's own first act — and then masters with -/// one grant, and holds it until it is killed. This host then sends the guest -/// [`KNOCKS`] SYNs through slirp. The verdict is the kernel's console saying -/// the unit saw no DMA fault. -/// -/// **The kernel's reset is not this test's subject**: the 82574 advertises -/// the D3hot round trip and QEMU does not reset it on one — measured, the -/// inherited `RCTL` still has receive enabled. So a holder that skipped the -/// quiesce faults here (the negative control); [`swap_resets_the_function`] is -/// the reset's. -pub fn swap_quiets_the_function( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut rig = Rig::boot("swap-quiet", super::lan::TALK_BENCH)?; - let taken = match swap_and_knock(&mut rig, rust_bins, IDLE, HOLDING) { - Ok(knocked) => knocked, - Err(why) => return Err(rig.fail(why)), - }; - let text = rig.console.clone(); - let console = serial::Serial::named("the quieting boot", text.as_str()); - let slot = super::iommu::slot_of(&text, "[8086:10d3]")?; - let released = console.must_say(&format!("released from slot {slot}; reset by"))?.to_string(); - let inherited = console.must_say("swap_claim_idle: inherited")?.to_string(); - if let Err(why) = console.must_be_clean() { - return Err(rig.fail(format!("{why}\n the release said: {}", released.trim_end()))); - } - eprintln!( - " [swap] {}; {}; the next holder stopped it, mastered it through {taken} SYNs, and \ - the unit saw no fault", - released.trim_end(), - inherited.trim_end(), - ); - drop(rig.guest); - let _ = std::fs::remove_file(&rig.staged.image); - Ok(()) -} - -/// **A function nothing resets reaches, at its next claim, only memory that -/// claim holds.** The T14's I219 advertises no reset, and after netd's -/// replacement had stopped it, its first grant let out a frame the part had -/// already taken in — written into the previous netd's buffers. Here netd on -/// QEMU's 82574 is released under `pcidev-reset-nothing`, which declines every -/// reset the way the I219's capabilities do, and swapped for a program that -/// masters the part with one grant of netd's size and its receive unit left on; -/// this host then sends it [`KNOCKS`] SYNs. -/// -/// The premises are asked of the console: the release says it reset nothing, -/// the replacement inherited a receive unit that is on, and its claim was -/// handed the range netd's grant was at. The verdict is the unit seeing no DMA -/// fault: every frame the part writes to netd's old descriptors lands in the -/// replacement's own grant, placed there. Without the residue nothing is -/// placed at those addresses, and the first frame faults and ends the -/// replacement's claim. -pub fn swap_keeps_what_nothing_reset( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut rig = Rig::boot_armed("swap-residue", super::lan::TALK_BENCH, RESET_NOTHING)?; - let taken = match swap_and_knock(&mut rig, rust_bins, RUNNING, RUNNING_HOLDING) { - Ok(knocked) => knocked, - Err(why) => return Err(rig.fail(why)), - }; - let text = rig.console.clone(); - let judged = (|| { - let console = serial::Serial::named("the residue boot", text.as_str()); - let slot = super::iommu::slot_of(&text, "[8086:10d3]")?; - let released = console.must_say(&format!("[8086:10d3] released from slot {slot}; reset by"))?; - if !released.contains("reset by nothing") { - return Err(format!("the premise: the 82574 was not released by nothing — {released}")); - } - let inherited = console.must_say("swap_claim_running: inherited RCTL 0x")?; - let rctl = inherited - .split("RCTL 0x") - .nth(1) - .and_then(|rest| rest.get(..8)) - .and_then(|hex| u32::from_str_radix(hex, 16).ok()) - .ok_or_else(|| format!("the replacement's line carries no RCTL: {inherited:?}"))?; - if rctl & toyos_i219::regs::rctl::EN == 0 { - return Err(format!("the premise: the part's receive unit was off when it was claimed — {inherited}")); - } - console.must_be_clean()?; - let taken_over = console.must_say(&format!("pcidev: slot {slot} holds 1 range(s)"))?; - eprintln!( - " [swap] {}; {}; {}; mastered through {taken} SYNs, and the unit saw no fault", - released.trim_end(), - inherited.trim_end(), - taken_over.trim_end(), - ); - Ok(()) - })(); - if let Err(why) = judged { - return Err(rig.fail(why)); - } - drop(rig.guest); - let _ = std::fs::remove_file(&rig.staged.image); - Ok(()) -} - -/// The replacement the refusal control swaps in: it aims the 82574's receive -/// ring outside its grant and waits on its claim. -const ASTRAY: &str = "swap_claim_astray"; - -/// Its line once the part is mastering. -const ASTRAY_HOLDING: &str = "swap_claim_astray: holding the NIC mastering"; - -/// Its line when the claim refused the read, and when it refused nothing. -const ASTRAY_TOLD: &str = "swap_claim_astray: its claim refused the interrupt read: Io"; -const ASTRAY_UNTOLD: &str = "swap_claim_astray: its claim refused nothing"; - -/// A fault the unit took on a function a process drives. -const HOLDER_FAULT: &str = "iommu: DMA FAULT owner=slot"; - -/// **A holder whose function the unit refused is told, rather than reading its -/// dead device as a quiet one.** On T14 run 132 the replacement netd's claim -/// faulted, bus mastering was cleared, and netd went on reading "no interrupt" -/// on every pass: it served nothing, said `ready`, and init put it in service. -/// -/// netd on QEMU's 82574 is swapped for a program that aims the part's receive -/// ring outside its one grant, masks every interrupt, and waits on its claim; -/// this host's SYNs make the part fetch a descriptor there, and the unit -/// refuses it. Nothing but that fault can wake the program. The verdict is its -/// own line: the claim refused the interrupt read with `Io`. Without the -/// refusal and the wake it earns, the program waits, and the harness ceiling reds -/// the boot. -pub fn swap_fault_tells_its_holder( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut rig = Rig::boot("swap-astray", super::lan::TALK_BENCH)?; - let taken = match swap_and_knock(&mut rig, rust_bins, ASTRAY, ASTRAY_HOLDING) { - Ok(knocked) => knocked, - Err(why) => return Err(rig.fail(why)), - }; - let ended = qemu::await_guest(&mut rig.guest, &mut rig.console, "the replacement's word on its claim", |c| { - c.contains(ASTRAY_TOLD) || c.contains(ASTRAY_UNTOLD) - }); - if let Err(why) = ended { - return Err(rig.fail(why)); - } - let text = rig.console.clone(); - let judged = (|| { - let console = serial::Serial::named("the astray boot", text.as_str()); - let fault = console.must_say(HOLDER_FAULT)?; - let slot = super::iommu::slot_of(&text, "[8086:10d3]")?; - if !fault.contains(&format!("owner=slot{slot} ")) || !fault.contains("access=read") { - return Err(format!("the premise: the unit refused no descriptor fetch of the claim's part — {fault}")); - } - let told = console.must_say(ASTRAY_TOLD)?; - let faults = text.matches(HOLDER_FAULT).count(); - console.must_be_clean_apart_from(HOLDER_FAULT, faults)?; - eprintln!( - " [swap] {}; {}; after {taken} SYNs", - fault.trim_end(), - told.trim_end(), - ); - Ok(()) - })(); - if let Err(why) = judged { - return Err(rig.fail(why)); - } - drop(rig.guest); - let _ = std::fs::remove_file(&rig.staged.image); - Ok(()) -} - -/// `name` among the build's test binaries. -fn test_binary<'a>(rust_bins: &'a [(String, Vec)], name: &str) -> Result<&'a [u8], String> { - rust_bins - .iter() - .find(|(bin, _)| bin == name) - .map(|(_, bytes)| bytes.as_slice()) - .ok_or_else(|| format!("no `{name}` among the test binaries")) -} - -/// Nothing after a swap that never reached init is init's to say, so a test -/// waiting on init's words first holds the answer to init's `accepted`. -fn init_accepted(answer: &Result) -> Result<(), String> { - match answer { - Ok(said) if said.starts_with("accepted ") => Ok(()), - other => Err(format!("the swap was answered {other:?}, where init's `accepted` is owed")), - } -} - -/// The machine with QEMU's `igb` beside the 82574, netd holding both. -const IGB_BENCH: Bench = - Bench { profile: qemu::Profile::E1000eBesideIgb, config: "tests/flrswapcase", device: "igb" }; - -/// The replacement that reads the `igb` through its claim's window. -const FLR_PROBE: &str = "swap_flr_probe"; - -/// **A function reset on release decodes where its next holder maps it.** netd -/// holds QEMU's `igb`, which resets by an Express function level reset — every -/// BAR back to 0 (PCIe §6.6.2). Swapping netd releases it, and the replacement -/// claims it and reads dword 0 through the window its claim maps: the dword the -/// kernel settled that window against when it first placed it, so all-zeroes -/// or all-ones there is a window the function does not decode. -/// -/// The premise is asked of the kernel's own release record, so a function that -/// stopped resetting cannot pass this vacuously. -pub fn swap_resets_the_function( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut rig = Rig::boot("swap-reset", IGB_BENCH)?; - let probe = test_binary(rust_bins, FLR_PROBE)?; - let binary = rig.staged.scratch.join(FLR_PROBE); - std::fs::write(&binary, probe).map_err(|e| format!("{}: {e}", binary.display()))?; - let answer = rig.swap_netd(&binary, &toyos_swap::digest(probe)); - eprintln!(" [swap] the swap was answered {answer:?}"); - if let Err(why) = init_accepted(&answer) { - return Err(rig.fail(why)); - } - // The probe's read, or a line that says there will be none. - let failed = toyos_swap::said("netd", Word::Failed, ""); - let held = qemu::await_guest(&mut rig.guest, &mut rig.console, "the replacement reading the igb", |c| { - c.contains("swap_flr_probe: igb BAR") - || c.contains("swap_flr_probe: started holding no igb") - || c.contains(&failed) - }); - if let Err(why) = held { - return Err(rig.fail(why)); - } - let text = rig.console.clone(); - let judged = (|| { - let console = serial::Serial::named("the resetting boot", text.as_str()); - let released = console.must_say("[8086:10c9] released from slot")?; - if !released.contains("reset by a function level reset (Express)") { - return Err(format!("the premise: the igb was not released by an Express FLR — {released}")); - } - let read = console.must_say("swap_flr_probe: igb BAR")?; - let dword = read - .rsplit("answers 0x") - .next() - .and_then(|hex| u32::from_str_radix(hex.trim(), 16).ok()) - .ok_or_else(|| format!("the probe's line carries no dword: {read:?}"))?; - if dword == 0 || dword == u32::MAX { - return Err(format!("the reset igb does not decode where its claim maps it: {read}")); - } - console.must_be_clean()?; - eprintln!(" [swap] {}; {}", released.trim_end(), read.trim_end()); - Ok(()) - })(); - if let Err(why) = judged { - return Err(rig.fail(why)); - } - drop(rig.guest); - let _ = std::fs::remove_file(&rig.staged.image); - Ok(()) -} - -/// The actuator that puts back none of a reset function's windows but its -/// MSI-X table's. -const BAR_LOST: &[&str] = &["pcidev-bar-lost-on-reset"]; - -/// The actuator that puts a reset function's BAR 0 back one BAR's size above -/// the window it was cut, inside it. -const BAR_MOVED: &[&str] = &["pcidev-bar-moved-on-reset"]; - -/// **A replacement refused a device the process it replaces held fails the -/// swap.** netd holds the 82574 and QEMU's `igb`; the `igb` resets on release, -/// and the actuator leaves its register window where the reset put it, so the -/// kernel refuses the next claim of it by name. netd's own rebuild is sent: -/// init must answer `failed` naming the `igb` rather than start a netd without -/// it, find the binary it replaced refused the same device, and close the -/// service — never `in service` over a netd running on the 82574 alone. -pub fn swap_refused_device_fails( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - refused_device_fails("swap-refused-device", BAR_LOST) -} - -/// [`swap_refused_device_fails`] with the `igb`'s BAR 0 holding a decodable -/// address that is not its cut: the kernel reads the register's address, not -/// only whether it holds one. -pub fn swap_moved_device_fails( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - refused_device_fails("swap-moved-device", BAR_MOVED) -} - -/// The `igb`'s next claim refused under `actuators`, and init's swap failing -/// on it and closing netd for it: the `gone` names the `igb`, so a claim init -/// kept from the refused start and could not mint again ends it otherwise. -fn refused_device_fails(name: &str, actuators: &'static [&'static str]) -> Result<(), String> { - let mut rig = Rig::boot_armed(name, IGB_BENCH, actuators)?; - let binary = rebuilt("netd", &rig.staged.scratch)?; - let digest = toyos_swap::digest(&std::fs::read(&binary).map_err(|e| e.to_string())?); - let answer = rig.swap_netd(&binary, &digest); - eprintln!(" [swap] the swap was answered {answer:?}"); - if let Err(why) = init_accepted(&answer) { - return Err(rig.fail(why)); - } - // The service carrying the stream is the one swapped, so init's words are - // read off the console: its last one on this swap, whichever it is. - let [gone, in_service, restored, started, failed] = - [Word::Gone, Word::InService, Word::Restored, Word::Started, Word::Failed] - .map(|w| toyos_swap::said("netd", w, "")); - let ended = qemu::await_guest(&mut rig.guest, &mut rig.console, "init's last word on the swap", |c| { - c.contains(&gone) || c.contains(&in_service) || c.contains(&restored) - }); - if let Err(why) = ended { - return Err(rig.fail(why)); - } - let text = rig.console.clone(); - let judged = (|| { - let console = serial::Serial::named("the refusing boot", text.as_str()); - let released = console.must_say("[8086:10c9] released from slot")?; - if !released.contains("reset by a function level reset (Express)") { - return Err(format!("the premise: the igb was not released by an Express FLR — {released}")); - } - for word in [&in_service, &started, &restored] { - if let Some(line) = text.lines().find(|l| l.contains(word.as_str())) { - return Err(format!( - "init started a netd though the igb the one it replaced held no longer holds \ - the window it was cut for: {line}" - )); - } - } - let refused = console.must_say("no longer holds the window it was cut for")?; - if !refused.contains("NOT HANDED OVER") { - return Err(format!("the kernel's refusal is not a refused hand-over: {refused}")); - } - let said = console.must_say(&failed)?; - if !said.contains("pci:8086:10c9") || !said.contains("the process it replaces held it") { - return Err(format!("init's `failed` does not name the device it could not give: {said}")); - } - let closed = console.must_say(&gone)?; - if !closed.contains("pci:8086:10c9") { - return Err(format!("init's `gone` does not name the igb the binary it replaced was refused: {closed}")); - } - console.must_be_clean()?; - eprintln!(" [swap] {}; {}; {}", refused.trim_end(), said.trim_end(), closed.trim_end()); - Ok(()) - })(); - if let Err(why) = judged { - return Err(rig.fail(why)); - } - drop(rig.guest); - let _ = std::fs::remove_file(&rig.staged.image); - Ok(()) -} - -/// The program [`swap_not_inherited`] runs undeclared. -const PROBE: &str = "swap_probe"; - -/// **The swap port is not inherited by what sshd runs.** A program the manifest -/// does not declare is uploaded over sftp and run over ssh, so std spawns it -/// holding a duplicate of sshd's namespace; it asks that namespace for `netd` -/// — the premise that it inherited one at all — and for the swap port, and -/// sends init a frame that is no swap request if it gets one. Its exit is the -/// verdict: 0 is the port out of reach, 1 is init reached. -pub fn swap_not_inherited( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let rig = Rig::boot("swap-not-inherited", VIRTIO)?; - let probe = test_binary(rust_bins, PROBE)?; - let remote = format!("/tmp/{PROBE}"); - let (host, port) = (super::ssh::HOST, rig.forward.port()); - if let Err(why) = super::ssh::ssh_put(host, port, &rig.staged.identity, &remote, probe) { - return Err(rig.fail(why)); - } - let command = format!("{remote} {} {} {}", toyos_swap::PORT, toyos_swap::LABEL, toyos_swap::MSG_SWAP); - let ran = match super::ssh::ssh_exec(host, port, &rig.staged.identity, &command) { - Ok(ran) => ran, - Err(why) => return Err(rig.fail(why)), - }; - let said = format!("{}{}", ran.stdout_text(), ran.stderr_text()); - if ran.status != Some(0) || !said.contains("is not in the namespace it inherited") { - return Err(rig.fail(format!("{PROBE} ended {:?} saying {said:?}", ran.status))); - } - eprintln!(" [swap] {}", said.trim_end()); - let (_, _, staged) = rig.finish()?; - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} diff --git a/tests/common/update.rs b/tests/common/update.rs deleted file mode 100644 index b4ff7f4a6b4..00000000000 --- a/tests/common/update.rs +++ /dev/null @@ -1,642 +0,0 @@ -//! The machine updates itself, rehearsed in QEMU: an image goes over ssh into -//! `update`'s standard input, is written to the slot the machine is not -//! running, and boots after a reboot; a slot the loader must refuse, and one -//! whose kernel dies, each leave the machine on the other slot. -//! -//! **The oracles are the loader's and the kernel's own lines**, read off the -//! 16550 the loader speaks on and the console the kernel does: which slot the table marked, each refusal by its name, the slot -//! the kernel says it came from, and the kernel's length the loader loaded — -//! against the sections this host built and signed, which it holds. A dead -//! boot's report is read back out of `/log` too, which is where the owner -//! finds it on a machine with no console. -//! -//! One machine per test, and it keeps its firmware variables in a copy of -//! its own (`BootOptions::firmware_vars`), because the anti-rollback floor a -//! proven boot raises is what the next boot of the same machine is held to. -//! What the running system can write and the loader must not trust — the -//! slots' record, the slot table — the host writes into the image between or -//! beneath boots, and the variable store it reads and plants in by EDK2's own -//! layout ([`fwvars`]). - -use std::path::{Path, PathBuf}; -use std::time::Instant; - -use toyos_build::bootlog; -use toyos_build::build::{self, Plan}; -use toyos_build::image::{self, SecondSlot, Signing}; -use toyos_build::signing::{self, Key}; -use toyos_update::floor::{self as floors, Scope}; -use toyos_update::record::{Booted, Record}; -use toyos_update::slots::Which; - -use super::fwvars; -use super::qemu::{self, BootOptions, QemuInstance, Staged, DEFAULT_READY}; -use super::ssh::{self, Identity, HOST}; - -/// The boot config every image here is built from. -const CONFIG: &str = "tests/updatecase"; - -/// The versions the three images carry: the machine's own, the update, and an -/// image older than the floor the first proves. -const BASE: u64 = 100; -const NEXT: u64 = 200; -const OLDER: u64 = 50; - -/// The kernel record naming the slot a boot came from (`kernel/src/params.rs`). -const SLOT_RECORD: &str = "boot: slot"; - -/// What the loader says of a slot whose every byte its signature vouched for. -const VERIFIED: &str = "kernel, cmdline and ROOT are the bytes the signed header names"; - -/// A version no image here carries, which a forged record names. -const FORGED: u64 = 1 << 63; - -/// The loader's refusal of a stored floor it did not write -/// (`bootloader/src/floor.rs`), which boots nothing. -const FLOOR_REFUSED: &str = "it is refused rather than read as no floor"; - -/// The loader's slots' record, on the log partition beside `loader.log`. -const RECORD_FILE: &str = "attempts"; - -/// One machine: its disk, its firmware variables, the key the host logs in -/// with, and where the host reaches its sshd. -struct Rig { - scratch: PathBuf, - image: PathBuf, - vars: PathBuf, - identity: Identity, - port: u16, - /// The base image's parts, for what the loader is held to. - base_kernel: usize, -} - -/// The plan for this config's image: `features` is the kernel build, `params` -/// the actuators its slot arms, `version` its signed header's. -fn plan(features: &[&str], params: &[&str], version: u64, second: Option) -> Plan { - let mut plan = Plan::new(toyos_build::arch::Arch::X86_64, &super::compile::repo_root().join(CONFIG).join("system.toml"), features, params); - plan.version = version; - plan.second = second; - plan -} - -/// What every image here carries on ROOT beside the config's own: the key the -/// host logs in with. -fn staged(identity: &Identity) -> Vec<(String, Vec)> { - vec![(ssh::KEYS_ON_ROOT.to_string(), identity.authorized_line().into_bytes())] -} - -impl Rig { - /// The machine's disk: slot A holding this config's image at [`BASE`] on - /// the shipping kernel, marked, and an empty slot B with room for the same - /// ROOT again. - fn stage(name: &str) -> Result { - let scratch = super::lane::dir().join(name); - std::fs::create_dir_all(&scratch).map_err(|e| format!("{}: {e}", scratch.display()))?; - let identity = Identity::mint(&format!("{name}-key"))?; - let root = super::compile::repo_root(); - let files = staged(&identity); - let base = plan(&[], &[], BASE, None); - let parts = build::build_test_parts(&root, &base, true, &files); - let room = SecondSlot { root_bytes: 2 * parts.root.len() as u64 }; - let disk = image::create_boot_image( - toyos_build::arch::Arch::X86_64, - &parts.kernel, - &parts.bootloader, - &parts.root, - "", - Signing { key: signing::key(), version: BASE }, - Some(room), - ); - let image = scratch.join("machine.img"); - std::fs::write(&image, disk).map_err(|e| format!("write {}: {e}", image.display()))?; - let vars = scratch.join("vars.fd"); - toyos_build::firmware::of(toyos_build::arch::Arch::X86_64)?.fresh_vars(&vars)?; - Ok(Self { scratch, image, vars, identity, port: qemu::free_host_port(), base_kernel: parts.kernel.len() }) - } - - /// An update for this machine, written to a file: `features` and `params` - /// as [`plan`] takes them, signed by `key` at `version`. - fn update(&self, name: &str, features: &[&str], params: &[&str], version: u64, key: &Key) -> Result<(PathBuf, usize), String> { - let root = super::compile::repo_root(); - let parts = build::build_test_parts(&root, &plan(features, params, version, None), true, &staged(&self.identity)); - let bytes = image::update_image(&parts.kernel, &parts.root, ¶ms.join(","), Signing { key, version }); - let path = self.scratch.join(format!("{name}.update")); - std::fs::write(&path, bytes).map_err(|e| format!("write {}: {e}", path.display()))?; - Ok((path, parts.kernel.len())) - } - - /// Boot the machine once, taking every reset it makes, to its ready - /// marker: the guest and its console so far. - fn boot(&self) -> Result<(QemuInstance, String), String> { - let options = BootOptions { - profile: qemu::Profile::Headless, - boot_image: Some(Staged::Written(self.image.clone())), - ssh_port: Some(self.port), - takes_the_reset: true, - firmware_vars: Some(self.vars.clone()), - qmp: true, - ..Default::default() - }; - let config = super::compile::repo_root().join(CONFIG); - let mut guest = QemuInstance::boot_with_options(&config, &[], &[], options); - let mut console = guest.boot_log().to_string(); - qemu::await_marker(&mut guest, &mut console, "sshd: listening on port 22", "sshd to open its port")?; - Ok((guest, console)) - } - - /// Boot the machine on the T14's shape, whose 16550 is its console, to - /// the loader's line `marker`: for a boot that never reaches a kernel, or - /// is cut at the loader's handoff. - fn launch(&self, marker: &'static str) -> QemuInstance { - let options = BootOptions { - profile: qemu::Profile::Metal, - boot_image: Some(Staged::Written(self.image.clone())), - takes_the_reset: true, - firmware_vars: Some(self.vars.clone()), - ready_marker: marker, - ..Default::default() - }; - QemuInstance::boot_with_options(&super::compile::repo_root().join(CONFIG), &[], &[], options) - } - - /// The log partition's unique GUID, which the slots' record carries. - fn log_guid(&self) -> Result<[u8; 16], String> { - let mut file = std::fs::File::open(&self.image).map_err(|e| format!("{}: {e}", self.image.display()))?; - image::unique_guid_of(&mut file, toyos_gpt::Guid::MICROSOFT_BASIC) - } - - /// The slots' record the loader last wrote. - fn record(&self) -> Result { - let guid = self.log_guid()?; - let mut file = std::fs::File::open(&self.image).map_err(|e| format!("{}: {e}", self.image.display()))?; - let bytes = image::read_file_on(&mut file, guid, RECORD_FILE)?; - Record::decode(&bytes, &guid).map_err(|why| format!("the slots' record {why}")) - } - - /// `reboot` over ssh, with `edit` made to the slots' record while the - /// machine is held at the reset its kernel makes — **the record as the - /// running system could have left it**, written after that kernel's last - /// write (its cache writes back whole blocks, the record's among them) and - /// before the loader's first read — then the console until `marker`: where - /// on the console and on the 16550 the boots after the reboot begin. - fn reboot_forging( - &self, - guest: &mut QemuInstance, - console: &mut String, - edit: impl FnOnce(&mut Record) -> Result<(), String>, - marker: &str, - ) -> Result<(usize, usize), String> { - let (from, uart) = (console.len(), guest.uart_log().len()); - let mut hold = qemu::QmpHold::arm(guest.qmp_socket()); - let asked = ssh::ssh_fire(HOST, self.port, &self.identity, "reboot")?; - eprintln!(" [update] `reboot` answered {asked:?}"); - hold.held(qemu::GUEST_WEDGED)?; - let guid = self.log_guid()?; - let mut record = self.record()?; - edit(&mut record)?; - image::overwrite_file_on(&self.image, guid, RECORD_FILE, &record.encode(&guid))?; - if self.record()? != record { - return Err("the forged record did not read back".into()); - } - hold.release(); - await_machine(guest, console, &format!("{marker:?} after the forged reboot"), |c| c[from.min(c.len())..].contains(marker))?; - Ok((from, uart)) - } - - /// The name of the floor this machine's loader keeps. - fn floor_name(&self) -> Result { - let key = signing::key(); - Ok(floors::name(key.floor_scope(), &key.public(), &self.log_guid()?).as_str().to_string()) - } - - /// `update < file` over ssh: its status and what it said. - fn install(&self, file: &Path) -> Result<(Option, String), String> { - let exec = ssh::ssh_pipe(HOST, self.port, &self.identity, "update", file)?; - let said = format!("{}{}", exec.stdout_text(), exec.stderr_text()); - eprintln!(" [update] `update < {}` ended {:?}: {}", file.display(), exec.status, said.trim()); - Ok((exec.status, said)) - } - - /// `reboot` over ssh, and the console until `marker`: where on the console - /// and on the loader's 16550 the boots after the reboot begin. - fn reboot_until(&self, guest: &mut QemuInstance, console: &mut String, marker: &str) -> Result<(usize, usize), String> { - let (from, uart) = (console.len(), guest.uart_log().len()); - let asked = ssh::ssh_fire(HOST, self.port, &self.identity, "reboot")?; - eprintln!(" [update] `reboot` answered {asked:?}"); - await_machine(guest, console, &format!("{marker:?} after the reboot"), |c| c[from.min(c.len())..].contains(marker)) - .map_err(|why| { - let all = guest.uart_log(); - format!("{why}\nthe 16550 since the reboot:\n{}", &all[uart.min(all.len())..]) - })?; - Ok((from, uart)) - } -} - -/// Wait until `done` holds of the console, while the machine is talking on -/// either of its channels. -/// -/// **Not the harness's own wait**: that one hears the console alone, and -/// between a kernel's reset and the next kernel's first line the machine -/// talks only on the 16550 — the loader's passes, one of which hashes ROOT — -/// so a machine working through two of them reads as one gone quiet. Its -/// bounds are the harness's, [`qemu::GUEST_QUIET`] of silence on both and -/// [`qemu::GUEST_WEDGED`] in all. -fn await_machine(guest: &mut QemuInstance, console: &mut String, doing: &str, done: impl Fn(&str) -> bool) -> Result<(), String> { - let began = Instant::now(); - let (mut heard, mut grew) = (0usize, Instant::now()); - loop { - if done(console) { - return Ok(()); - } - let more = guest.drain_serial(std::time::Duration::from_millis(200)); - console.push_str(&more); - let now = console.len() + guest.uart_log().len(); - if now != heard { - (heard, grew) = (now, Instant::now()); - } - if grew.elapsed() >= qemu::GUEST_QUIET { - return Err(format!( - "{} waiting for {doing}: the console and the 16550 both went quiet for {} s", - qemu::STALLED, - qemu::GUEST_QUIET.as_secs() - )); - } - if began.elapsed() >= qemu::GUEST_WEDGED { - return Err(format!("{} waiting for {doing}: it never stopped talking and never got there", qemu::STALLED)); - } - } -} - -/// `what` is in `console` from `from` on, or the finding that it is not. -fn owed(console: &str, from: usize, what: &str) -> Result<(), String> { - if console[from.min(console.len())..].contains(what) { - return Ok(()); - } - Err(format!("{what:?} is not on the console after byte {from}:\n{}", &console[from.min(console.len())..])) -} - -/// `what` is among the loader's lines from byte `from` of its 16550 on. -fn loader_said(guest: &QemuInstance, from: usize, what: &str) -> Result<(), String> { - let uart = guest.uart_log(); - let since = &uart[from.min(uart.len())..]; - if since.contains(what) { - return Ok(()); - } - let loader: Vec<&str> = since.lines().filter(|l| !qemu::is_kernel_line(l)).collect(); - Err(format!("the loader never said {what:?}; it said:\n{}", loader.join("\n"))) -} - -/// **The exit**: a kernel change reaches the running machine as `ssh … update -/// < image`, is written to the idle slot, and is the kernel the next boot -/// runs; the boot that proved the old image raised the floor, and the one -/// that proves the new image raises it past the old one. -pub fn update_boots_the_new_kernel(_: &Path, _: &[(String, Vec)], _: &[(String, Vec)]) -> Result<(), String> { - let rig = Rig::stage("update-boots")?; - // The actuator kernel, and no actuator armed: a different kernel binary - // that boots the same machine. - let (next, next_kernel) = rig.update("next", build::TEST_KERNEL, &[], NEXT, signing::key())?; - if next_kernel == rig.base_kernel { - return Err(format!("the update's kernel is {next_kernel} bytes, the base's too: no change to carry")); - } - let (mut guest, mut console) = rig.boot()?; - owed(&console, 0, &format!("{SLOT_RECORD} A, the one the slot table marks"))?; - - let (status, said) = rig.install(&next)?; - if status != Some(0) || !said.contains(&format!("update: installed version {NEXT} in slot B")) { - return Err(format!("`update` ended {status:?} saying {said:?}")); - } - let (from, uart) = rig.reboot_until(&mut guest, &mut console, &format!("{SLOT_RECORD} B, the one the slot table marks"))?; - await_machine(&mut guest, &mut console, "the new slot's ready marker", |c| c[from..].contains(DEFAULT_READY))?; - loader_said(&guest, uart, &format!("Anti-rollback floor: {BASE}, raised from 0 by the boot that proved it"))?; - loader_said(&guest, uart, &format!("Slot B: {VERIFIED}"))?; - loader_said(&guest, uart, &format!("Kernel: {next_kernel} bytes"))?; - eprintln!( - " [update] {} bytes installed; slot B's kernel ({next_kernel} bytes, the base's {}) at its \ - ready marker", - std::fs::metadata(&next).map(|m| m.len()).unwrap_or(0), - rig.base_kernel, - ); - - // **A record the running system forged proves nothing**: slot B's own - // entry, a digest that is not its header's and a version no image - // carries. The pass that reads the clean reboot verifies B's header, - // finds another digest, and leaves the floor at the base's version. - let forge = |record: &mut Record| { - let booted = record.booted.filter(|b| b.slot == Which::B).ok_or("the loader wrote down no boot of slot B")?; - let mut digest = booted.digest; - digest[0] ^= 1; - record.booted = Some(Booted { version: FORGED, digest, ..booted }); - Ok(()) - }; - let (from, uart) = - rig.reboot_forging(&mut guest, &mut console, forge, &format!("{SLOT_RECORD} B, the one the slot table marks"))?; - await_machine(&mut guest, &mut console, "slot B's ready marker again", |c| c[from..].contains(DEFAULT_READY))?; - loader_said(&guest, uart, "Anti-rollback floor: not raised, because the proven image is not verified: slot B's signed header is")?; - loader_said(&guest, uart, &format!("{} (image scope) holds {BASE}", rig.floor_name()?))?; - let since = guest.uart_log()[uart..].to_string(); - for raised in [format!("Anti-rollback floor: {NEXT}"), format!("Anti-rollback floor: {FORGED}")] { - if since.contains(&raised) { - return Err(format!("a forged record raised the floor: the loader said {raised:?}")); - } - } - eprintln!(" [update] a record naming slot B under another digest and version {FORGED} raised nothing"); - - // **What anti-rollback is for**: the plain reboot proves slot B's own - // image, the floor rises to the update's version, and the slot the machine - // updated from is below it. - let (from, uart) = rig.reboot_until(&mut guest, &mut console, &format!("{SLOT_RECORD} B, the one the slot table marks"))?; - await_machine(&mut guest, &mut console, "slot B's ready marker a third time", |c| c[from..].contains(DEFAULT_READY))?; - loader_said(&guest, uart, &format!("Anti-rollback floor: {NEXT}, raised from {BASE} by the boot that proved it"))?; - drop(guest); - image::restage_table(&rig.image, |t| t.marked = Which::A)?; - let (guest, console) = rig.boot()?; - loader_said(&guest, 0, &format!("Slot A: REFUSED, its version {BASE} is below {NEXT}, the highest a boot has proven"))?; - owed(&console, 0, &format!("{SLOT_RECORD} B, because the marked slot A was refused: version"))?; - eprintln!(" [update] the boot of slot B raised the floor to {NEXT}, and slot A at {BASE} is refused under it"); - drop(guest); - let _ = std::fs::remove_dir_all(&rig.scratch); - Ok(()) -} - -/// **Every slot the loader must refuse is refused by name, and the other boots** -/// — a flipped byte, no signed header, another key's signature, a version -/// under the floor — and `update` itself refuses what it can see: another -/// key and an image older than what runs, before it writes anything, and a -/// kernel or ROOT that is not the bytes the header names, or bytes past the -/// last section, before it moves the mark. And the floor the reboot raises is -/// the version the loader verified, whatever the record on the disk says. -pub fn update_refusals_boot_the_other_slot(_: &Path, _: &[(String, Vec)], _: &[(String, Vec)]) -> Result<(), String> { - let rig = Rig::stage("update-refusals")?; - let stranger = Key::mint(); - let (next, _) = rig.update("next", &[], &[], NEXT, signing::key())?; - let (foreign, _) = rig.update("foreign", &[], &[], NEXT, &stranger)?; - let (older, _) = rig.update("older", &[], &[], OLDER, signing::key())?; - let bytes = |path: &Path| std::fs::read(path).map_err(|e| format!("{}: {e}", path.display())); - let whole = bytes(&next)?; - let header = toyos_update::image::Header::parse(&whole).map_err(|why| why.to_string())?; - let root_at = toyos_update::image::SIGNED_BYTES + (header.kernel().len + header.cmdline().len) as usize; - let bent = |name: &str, bend: &dyn Fn(&mut Vec)| -> Result { - let mut bytes = whole.clone(); - bend(&mut bytes); - let path = rig.scratch.join(format!("{name}.update")); - std::fs::write(&path, bytes).map_err(|e| format!("write {}: {e}", path.display()))?; - Ok(path) - }; - let kernel_flipped = bent("kernel-flipped", &|b| b[toyos_update::image::SIGNED_BYTES + 100] ^= 0x01)?; - let root_flipped = bent("root-flipped", &|b| b[root_at + 100] ^= 0x01)?; - let appended = bent("appended", &|b| b.push(0))?; - - // The machine's first boot: `update` refuses each, and a reboot proves the - // base image, which raises the floor to its version. - let (mut guest, mut console) = rig.boot()?; - let older_word = format!("its version {OLDER} is older than {BASE}"); - for (file, word) in [ - (&foreign, "the signature is not this machine's key's"), - (&older, older_word.as_str()), - (&kernel_flipped, "the kernel is not the bytes its signed header names"), - (&root_flipped, "ROOT is not the bytes its signed header names"), - (&appended, "the input carries more bytes than its signed header names"), - ] { - let (status, said) = rig.install(file)?; - if status != Some(1) || !said.contains(word) { - return Err(format!("`update < {}` ended {status:?} saying {said:?}, where {word:?} is owed", file.display())); - } - } - // **What the running system writes, the loader does not believe**: the - // record names slot A and its own digest, and a version no image carries. - let forge = |record: &mut Record| { - let booted = record.booted.filter(|b| b.slot == Which::A).ok_or("the loader wrote down no boot of slot A")?; - record.booted = Some(Booted { version: FORGED, ..booted }); - Ok(()) - }; - let (from, uart) = rig.reboot_forging(&mut guest, &mut console, forge, DEFAULT_READY)?; - loader_said(&guest, uart, &format!("Anti-rollback floor: {BASE}, raised from 0 by the boot that proved it"))?; - if guest.uart_log()[uart..].contains(&FORGED.to_string()) { - return Err(format!("the loader said the forged version {FORGED}")); - } - owed(&console, from, &format!("{SLOT_RECORD} A, the one the slot table marks"))?; - drop(guest); - - let mut flipped = bytes(&next)?; - // A byte of the kernel, past the signed header: the signature still - // verifies and the hash does not. - flipped[toyos_update::image::SIGNED_BYTES + 100] ^= 0x01; - let cases: [(&str, Vec, bool, &str); 4] = [ - ("a flipped byte", flipped, true, "hash"), - ("no signed header", bytes(&next)?, false, "unsigned"), - ("another key's signature", bytes(&foreign)?, true, "signature"), - ("a version under the floor", bytes(&older)?, true, "version"), - ]; - for (what, update, signed, word) in cases { - image::stage_slot(&rig.image, Which::B, &update, signed)?; - let (guest, console) = rig.boot()?; - loader_said(&guest, 0, "Slot B: REFUSED")?; - owed(&console, 0, &format!("{SLOT_RECORD} A, because the marked slot B was refused: {word}"))?; - loader_said(&guest, 0, &format!("Slot A: {VERIFIED}"))?; - eprintln!(" [update] slot B with {what}: refused as {word:?}, and slot A booted"); - drop(guest); - } - let _ = std::fs::remove_dir_all(&rig.scratch); - Ok(()) -} - -/// **A boot that dies falls back on its own**: an update whose kernel panics -/// is installed and marked, the reboot boots it, it panics, the loader reads -/// the panic and marks that image dead, and the pass after boots slot A — -/// whose kernel says why, and whose `/log` carries the saying. -pub fn update_falls_back_from_a_dying_kernel(_: &Path, _: &[(String, Vec)], _: &[(String, Vec)]) -> Result<(), String> { - let rig = Rig::stage("update-dies")?; - // A panic once the boot is complete, and the panicked kernel's own reset - // bound shortened so it hands the machine back inside the test: the - // reset is what brings the loader round to read the panic. - let (dying, _) = rig.update("dying", build::TEST_KERNEL, &["test-late-panic", "panic-reboot-fast"], NEXT, signing::key())?; - let (mut guest, mut console) = rig.boot()?; - let (status, said) = rig.install(&dying)?; - if status != Some(0) || !said.contains(&format!("update: installed version {NEXT} in slot B")) { - return Err(format!("`update` ended {status:?} saying {said:?}")); - } - let fell_back = format!("{SLOT_RECORD} A, because the marked slot B was refused: died"); - // Until slot A says it fell back, or slot B has booted a second time: a - // loader that does not fall back boots the dead slot again, and that is the - // answer, not a wait for one that never comes. - let again = format!("{SLOT_RECORD} B, "); - let (from, uart) = (console.len(), guest.uart_log().len()); - ssh::ssh_fire(HOST, rig.port, &rig.identity, "reboot")?; - await_machine(&mut guest, &mut console, "slot A to fall back, or slot B to boot again", |c| { - let since = &c[from.min(c.len())..]; - since.contains(&fell_back) || since.matches(&again).count() >= 2 - })?; - let booted_b = console[from..].matches(&again).count(); - if !console[from..].contains(&fell_back) { - return Err(format!("slot B booted {booted_b} times after the update and slot A never did")); - } - await_machine(&mut guest, &mut console, "slot A's ready marker", |c| c[from..].contains(DEFAULT_READY))?; - loader_said(&guest, uart, "Previous boot's panic:")?; - loader_said(&guest, uart, "died on its last boot, so no pass boots it again until an update replaces it")?; - - // The owner's channel on a machine with no console: the fallback boot's - // own `/log`, whole once that boot has ended itself. - let at = console.len(); - ssh::ssh_fire(HOST, rig.port, &rig.identity, "reboot")?; - qemu::await_marker_new(&mut guest, &mut console, bootlog::REBOOTING, at, "the fallback boot to end")?; - drop(guest); - let bytes = std::fs::read(&rig.image).map_err(|e| format!("{}: {e}", rig.image.display()))?; - let (start, len) = super::volumes::log_extent(&bytes, &rig.image)?; - let log = super::volumes::whole_log(&rig.image, start, len)?; - if !log.iter().any(|line| line.contains(&fell_back)) { - return Err(format!("/log never says {fell_back:?}; it holds {} lines", log.len())); - } - eprintln!(" [update] slot B's kernel panicked, slot A booted on its own, and /log says {fell_back:?}"); - let _ = std::fs::remove_dir_all(&rig.scratch); - Ok(()) -} - -/// Each of `whats` is among `log`'s lines, or the finding that one is not. -fn said(log: &str, whats: &[&str]) -> Result<(), String> { - for what in whats { - if !log.contains(what) { - return Err(format!("{what:?} is not in what the machine said:\n{log}")); - } - } - Ok(()) -} - -/// **A hang of an image no boot has proven is a death**: slot B is handed the -/// machine and cut at the loader's handoff, which is a hang or a power cut -/// as far as any pass can tell; the retry boots nothing and marks B's image -/// dead, since no floor stands at or above its version; and the pass after -/// boots slot A. -pub fn update_hang_kills_an_unproven_image(_: &Path, _: &[(String, Vec)], _: &[(String, Vec)]) -> Result<(), String> { - let rig = Rig::stage("update-hang")?; - let (next, _) = rig.update("next", &[], &[], NEXT, signing::key())?; - let update = std::fs::read(&next).map_err(|e| format!("{}: {e}", next.display()))?; - image::stage_slot(&rig.image, Which::B, &update, true)?; - - let handed = rig.launch(bootlog::LOADER_LAST_LINE); - said(handed.boot_log(), &[&format!("Slot B: {VERIFIED}")])?; - drop(handed); - let retry = rig.launch(bootlog::CHAIN_ENDS_LINE); - said(retry.boot_log(), &[bootlog::HUNG_WITHOUT_A_RECORD, "died on its last boot, so no pass boots it again"])?; - drop(retry); - let after = rig.launch(bootlog::LOADER_LAST_LINE); - said(after.boot_log(), &["Slot B: REFUSED, its image died on its last boot", &format!("Slot A: {VERIFIED}")])?; - drop(after); - eprintln!(" [update] slot B cut at its handoff was a death: the retry marked it, and slot A booted"); - let _ = std::fs::remove_dir_all(&rig.scratch); - Ok(()) -} - -/// **The record a pass writes before it has chosen names no booted image**: a -/// pass whose every slot is refused panics after that first write, and the -/// record it leaves must not still credit the image the last pass booted — -/// whose clean end the next pass would take as that image's proof. -pub fn update_refused_pass_credits_no_image(_: &Path, _: &[(String, Vec)], _: &[(String, Vec)]) -> Result<(), String> { - let rig = Rig::stage("update-uncredited")?; - let handed = rig.launch(bootlog::LOADER_LAST_LINE); - said(handed.boot_log(), &[&format!("Slot A: {VERIFIED}")])?; - drop(handed); - if rig.record()?.booted.map(|b| b.slot) != Some(Which::A) { - return Err(format!("the pass that booted slot A wrote down {:?}", rig.record()?.booted)); - } - // A byte of slot A's kernel, and slot B holds no image: every slot is - // refused, so the pass panics after its first write and before its second. - let mut file = std::fs::File::open(&rig.image).map_err(|e| format!("{}: {e}", rig.image.display()))?; - let a = image::slot_table_of(&mut file)?.slot(Which::A).ok_or("no slot A")?; - let mut kernel = image::read_file_on(&mut file, a.boot, toyos_update::slots::KERNEL_FILE)?; - drop(file); - kernel[100] ^= 0x01; - image::overwrite_file_on(&rig.image, a.boot, toyos_update::slots::KERNEL_FILE, &kernel)?; - let refused = rig.launch("Slots: no slot verifies"); - said(refused.boot_log(), &["Slot A: REFUSED, its kernel is not the bytes its signed header names", "Slot B: REFUSED"])?; - drop(refused); - if let Some(booted) = rig.record()?.booted { - return Err(format!("a pass that booted nothing left a record naming slot {}'s image", booted.slot.letter())); - } - eprintln!(" [update] a pass that refused every slot left a record naming no booted image"); - let _ = std::fs::remove_dir_all(&rig.scratch); - Ok(()) -} - -/// **init claims nothing the slot table names but an idle slot's partition on -/// the running disk**: a table naming the ESP, the log partition, or either -/// of the running slot's partitions as the idle slot's is refused by name, -/// and `update` holds nothing. -pub fn update_grant_refuses_a_stray_partition(_: &Path, _: &[(String, Vec)], _: &[(String, Vec)]) -> Result<(), String> { - use toyos_update::slots::Slot; - let rig = Rig::stage("update-grant")?; - let (next, _) = rig.update("next", &[], &[], NEXT, signing::key())?; - let mut file = std::fs::File::open(&rig.image).map_err(|e| format!("{}: {e}", rig.image.display()))?; - let esp = image::unique_guid_of(&mut file, toyos_gpt::Guid::EFI_SYSTEM)?; - let table = image::slot_table_of(&mut file)?; - drop(file); - let (a, b) = (table.slot(Which::A).ok_or("no slot A")?, table.slot(Which::B).ok_or("no slot B")?); - let log = rig.log_guid()?; - let not_a_slot = "the idle slot's volume is not of the type a slot's partition of that kind carries"; - let cases = [ - ("the ESP", esp, b.root, not_a_slot), - ("the log partition", log, b.root, not_a_slot), - ("the running slot's volume", a.boot, b.root, "the idle slot's volume is one of the running slot's"), - ("the running slot's ROOT", b.boot, a.root, "the idle slot's ROOT is one of the running slot's"), - ]; - for (what, boot, root, why) in cases { - image::restage_table(&rig.image, |t| t.slots[Which::B.index()] = Some(Slot { boot, root, version: 0 }))?; - let (mut guest, mut console) = rig.boot()?; - let (status, said) = rig.install(&next)?; - if status != Some(1) || !said.contains("this process holds no `slots:table`") { - return Err(format!("with slot B naming {what}, `update` ended {status:?} saying {said:?}")); - } - let refused = format!("init: update: no slot to grant: {why}"); - await_machine(&mut guest, &mut console, &format!("init to refuse {what}"), |c| c.contains(&refused))?; - eprintln!(" [update] slot B naming {what}: init granted nothing, and `update` held nothing"); - drop(guest); - } - let _ = std::fs::remove_dir_all(&rig.scratch); - Ok(()) -} - -/// **A floor is its key's and its image's**: the owner's floor and another -/// image's, both at the highest version there is, hold this image to nothing -/// — the other image's is deleted, the owner's never — and the clean reboot -/// raises this image's own. And this image's own floor, stored in a shape its -/// loader never writes, is refused and boots nothing. -pub fn update_floor_is_the_images_own(_: &Path, _: &[(String, Vec)], _: &[(String, Vec)]) -> Result<(), String> { - let rig = Rig::stage("update-floor")?; - let key = signing::key(); - let own = rig.floor_name()?; - let owner = floors::name(Scope::Machine, &key.public(), &[0; 16]).as_str().to_string(); - let other = floors::name(Scope::Image, &key.public(), &[0x55; 16]).as_str().to_string(); - let fresh = || toyos_build::firmware::of(toyos_build::arch::Arch::X86_64)?.fresh_vars(&rig.vars); - - fresh()?; - fwvars::plant(&rig.vars, &FLOOR_VENDOR, &owner, floors::ATTRIBUTES, &u64::MAX.to_le_bytes())?; - fwvars::plant(&rig.vars, &FLOOR_VENDOR, &other, floors::ATTRIBUTES, &u64::MAX.to_le_bytes())?; - let (mut guest, mut console) = rig.boot()?; - owed(&console, 0, &format!("{SLOT_RECORD} A, the one the slot table marks"))?; - loader_said(&guest, 0, &format!("Anti-rollback floor: {other} is no floor this image loader keeps; deleted"))?; - loader_said(&guest, 0, &format!("Anti-rollback floor: {own} (image scope) holds 0"))?; - let (_, uart) = rig.reboot_until(&mut guest, &mut console, DEFAULT_READY)?; - loader_said(&guest, uart, &format!("Anti-rollback floor: {BASE}, raised from 0 by the boot that proved it"))?; - drop(guest); - - let stored = fwvars::live(&rig.vars, &FLOOR_VENDOR)?; - let value = |name: &str| stored.iter().filter(|v| v.name == name).map(|v| v.data.clone()).collect::>(); - let want = [(&owner, vec![u64::MAX.to_le_bytes().to_vec()]), (&own, vec![BASE.to_le_bytes().to_vec()]), (&other, vec![])]; - for (name, holds) in want { - if value(name) != holds { - return Err(format!("the variable store holds {name} as {:?}, where {holds:?} is owed", value(name))); - } - } - eprintln!(" [update] the owner's floor and another image's held this one to nothing; the other's went, the owner's stayed"); - - fresh()?; - fwvars::plant(&rig.vars, &FLOOR_VENDOR, &own, floors::ATTRIBUTES, &[1; 9])?; - let refused = rig.launch(FLOOR_REFUSED); - said(refused.boot_log(), &[&format!("Anti-rollback floor: {own}: it holds 9 bytes where this loader writes 8")])?; - drop(refused); - eprintln!(" [update] this image's own floor in nine bytes was refused, and nothing booted"); let _ = std::fs::remove_dir_all(&rig.scratch); - Ok(()) -} - -/// The floor's vendor, `33BE3D4A-30E6-49F5-8050-F169D93A20FB`, in the byte -/// order `EFI_GUID` stores. -const FLOOR_VENDOR: [u8; 16] = [0x4a, 0x3d, 0xbe, 0x33, 0xe6, 0x30, 0xf5, 0x49, 0x80, 0x50, 0xf1, 0x69, 0xd9, 0x3a, 0x20, 0xfb]; diff --git a/tests/common/usb.rs b/tests/common/usb.rs index 38fd3bbc4b2..f062b6d03cd 100644 --- a/tests/common/usb.rs +++ b/tests/common/usb.rs @@ -1,1326 +1,8 @@ -//! The USB mass-storage gate. -//! -//! Ground truth is the backing file on the *host*: the harness writes bytes -//! into the image -//! before the boot and the guest has to find them, and the guest writes bytes -//! the harness finds afterwards. Neither half of the driver certifies the -//! other, which a read-back-what-you-wrote test would have let it do. -//! -//! Lives here rather than in `toyos.rs` so the registration hunk in that shared -//! file stays two lines: every agent edits it, and a wide diff there is how -//! work gets swept into somebody else's commit. +//! The USB transport break on the T14's boot stick, judged off the stick's own +//! log: `usb_stick_left`'s metal row. -use std::io::{Read, Seek, SeekFrom, Write}; -use std::path::{Path, PathBuf}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - -use super::qemu::{self, BootOptions, Profile, QemuInstance}; use super::serial; -/// Every constant below is mirrored in `kernel/src/usb_gate.rs`. They are two -/// halves of one wire format; a change to either without the other shows up as -/// "carries no stamp", not as a silent pass. -const MAGIC: &[u8; 16] = b"TOYOS-USB-GATE1\0"; -const AT_BLOCKS: usize = 16; -const AT_NONCE: usize = 24; -const BLOCK: u64 = 4096; -const HOST_BLOCKS: [i64; 2] = [1, -1]; -const GUEST_BLOCKS: [i64; 2] = [2, -2]; -const RUN_START: u64 = 4; -const RUN_LEN: u64 = 9; - -/// The one actuator these boots need. A raw block device has no path to -/// userland, so the kernel is the only in-guest actor that can drive one — the -/// same reason `xhci-one-slot` exists. What decides *which* disk gets written -/// is the stamp in block 0 and not this flag, which is why the unstamped boot -/// below is a real assertion and not a tautology. -const GATE: &[&str] = &["usb-storage-gate"]; - -fn pattern(nonce: u64, block: u64, i: usize) -> u8 { - let n = (nonce >> ((i % 8) * 8)) as u8; - let b = (block ^ (block >> 13) ^ (block >> 27)) as u8; - n ^ b.wrapping_mul(37) ^ (i as u8).wrapping_mul(101) -} - -/// FNV-1a, mirrored byte-for-byte from `kernel/src/usb_gate.rs`: the guest's -/// comparator says a block matched, and this says which bytes it read. -fn digest(buf: &[u8]) -> u64 { - let mut hash: u64 = 0xcbf2_9ce4_8422_2325; - for &byte in buf { - hash ^= byte as u64; - hash = hash.wrapping_mul(0x0000_0100_0000_01b3); - } - hash -} - -fn block_of(blocks: u64, index: i64) -> u64 { - if index >= 0 { - index as u64 - } else { - blocks.saturating_sub(index.unsigned_abs()) - } -} - -fn test_dir() -> PathBuf { - super::lane::dir() -} - -fn sparse(path: &Path, bytes: u64) -> std::fs::File { - let file = std::fs::File::create(path).expect("create the USB image"); - file.set_len(bytes).expect("size the USB image"); - std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(path) - .expect("reopen the USB image") -} - -fn write_block(file: &mut std::fs::File, block: u64, data: &[u8]) { - file.seek(SeekFrom::Start(block * BLOCK)).expect("seek"); - file.write_all(data).expect("write"); -} - -fn read_block(file: &mut std::fs::File, block: u64) -> Vec { - let mut buf = vec![0u8; BLOCK as usize]; - file.seek(SeekFrom::Start(block * BLOCK)).expect("seek"); - file.read_exact(&mut buf).expect("read"); - buf -} - -/// Stage an image the guest is allowed to write: the stamp, then the blocks -/// the guest has to read back byte-for-byte. Returns the nonce. -fn stage(path: &Path, bytes: u64) -> u64 { - let blocks = bytes / BLOCK; - let nonce = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock") - .as_nanos() as u64 - | 1; - let mut file = sparse(path, bytes); - - let mut head = vec![0u8; BLOCK as usize]; - head[..MAGIC.len()].copy_from_slice(MAGIC); - head[AT_BLOCKS..AT_BLOCKS + 8].copy_from_slice(&blocks.to_le_bytes()); - head[AT_NONCE..AT_NONCE + 8].copy_from_slice(&nonce.to_le_bytes()); - write_block(&mut file, 0, &head); - - for index in HOST_BLOCKS { - let block = block_of(blocks, index); - let data: Vec = (0..BLOCK as usize).map(|i| pattern(nonce, block, i)).collect(); - write_block(&mut file, block, &data); - } - file.sync_all().expect("sync the staged image"); - nonce -} - -/// Every claim the host can make about what the guest did to the disk. -/// -/// **Every block, on every boot.** -fn verify(path: &Path, bytes: u64, nonce: u64) -> Result<(), String> { - let blocks = bytes / BLOCK; - let guest_nonce = !nonce; - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(path) - .expect("open the USB image to verify"); - - // What the guest wrote, at the LBAs it was told to write them. - for index in GUEST_BLOCKS { - let block = block_of(blocks, index); - let got = read_block(&mut file, block); - if let Some(at) = (0..BLOCK as usize).find(|&i| got[i] != pattern(guest_nonce, block, i)) { - return Err(format!( - "block {block} in the image is {:#04x} at byte {at}, not the {:#04x} the guest \ - was told to write", - got[at], - pattern(guest_nonce, block, at) - )); - } - } - for i in 0..RUN_LEN { - let block = RUN_START + i; - let got = read_block(&mut file, block); - if let Some(at) = (0..BLOCK as usize).find(|&j| got[j] != pattern(guest_nonce, block, j)) { - return Err(format!( - "block {block} of the {RUN_LEN}-block run is {:#04x} at byte {at}, not {:#04x}", - got[at], - pattern(guest_nonce, block, at) - )); - } - } - - // And what it did not write. A driver whose LBA arithmetic is off by a - // block passes every assertion above only if it is off by zero, but one - // that writes a whole batch where it meant to write one block passes them - // all — so the blocks on either side of the run have to still be nothing. - if !read_block(&mut file, 0).starts_with(MAGIC) { - return Err("the guest overwrote the stamp in block 0".to_string()); - } - for index in HOST_BLOCKS { - let block = block_of(blocks, index); - let got = read_block(&mut file, block); - if let Some(at) = (0..BLOCK as usize).find(|&i| got[i] != pattern(nonce, block, i)) { - return Err(format!( - "the guest wrote over the host's block {block} at byte {at}: {:#04x}", - got[at] - )); - } - } - for block in [3, RUN_START + RUN_LEN, blocks - 3] { - if read_block(&mut file, block).iter().any(|&b| b != 0) { - return Err(format!("block {block} was written and should not have been")); - } - } - Ok(()) -} - -/// The first 64 KiB and the last 16 KiB — everything the gate would touch on a -/// disk it decided it owned. -fn fingerprint(path: &Path, bytes: u64) -> Vec { - let mut file = std::fs::File::open(path).expect("open the USB image to fingerprint"); - let mut out = vec![0u8; 64 * 1024]; - file.read_exact(&mut out).expect("read the head"); - file.seek(SeekFrom::Start(bytes - 16 * 1024)).expect("seek the tail"); - let mut tail = vec![0u8; 16 * 1024]; - file.read_exact(&mut tail).expect("read the tail"); - out.extend_from_slice(&tail); - out -} - -/// Boot, shut the guest down cleanly, and return everything it said. -/// -/// The shutdown is not politeness: it is what makes the host's view of the -/// backing file the device's view of it, and `foreign_disk_untouched` records -/// what killing QEMU instead did to the equivalent NVMe assertion. -fn boot_and_shutdown( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - options: BootOptions, -) -> Result { - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let mut log = qemu.boot_log().to_string(); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - log.push_str(&qemu.drain_serial(Duration::from_secs(20))); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} during the USB gate boot\n{log}")); - } - } - Ok(log) -} - -/// What every gate boot must be able to say about itself before any assertion -/// about bytes means anything. -fn gate_ran(log: &str, disks: usize) -> Result<(), String> { - let want = format!("usb-gate: {disks} disk(s) on the bus"); - if !log.contains(&want) { - return Err(format!("the guest never printed {want:?}; did the gate run?\n{log}")); - } - if !log.contains("usb-gate: sweep complete") { - return Err(format!("the gate did not finish its sweep\n{log}")); - } - // The boot stick is on this bus in every profile and is the disk the guest - // is running from. It carries no stamp, so it must have been read once and - // left alone -- and the gate must say so, because "it did not write it" is - // not observable from an image the harness rewrites every boot. - if !log.contains("carries no stamp, leaving it alone") { - return Err(format!("the gate did not walk past the boot stick\n{log}")); - } - Ok(()) -} - -/// Read what the host wrote, write what the host will read, on a 512-byte -/// sector stick — plus the two negatives that make it mean something: a disk -/// the guest was not given comes back byte-identical, and a machine with one -/// USB disk reports one. -pub fn usb_storage_gate( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (bytes, lba) = Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = test_dir().join("usb-gate-512.img"); - let nonce = stage(&image, bytes); - - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk, - kernel_params: GATE, - usb_images: vec![image.clone()], - ..Default::default() - }, - )?; - gate_ran(&log, 2)?; - check_geometry(&log, bytes, lba)?; - if !log.contains("usb-gate: disk done reads=ok writes=ok refusal=true wr_err=0 healthy=true") { - return Err(format!("the guest did not report a clean pass\n{log}")); - } - // The caller's own device-time budget, spent before the operation started. - // Distinct from `refusal=true`, which is a *device* that cannot serve the - // read: this one is the driver declining to issue a command the caller has - // run out of time for, and the clean pass asserted above is what says the - // disk was left exactly as it was by it. `kernel/src/block.rs`'s - // `OPERATION` carries the number and why a device that answers needs one. - if !log.contains("usb-gate: read with a spent budget refused=true budget=true") { - return Err(format!( - "the driver issued a command past the caller's budget, or reported one it \ - refused as a fact about the disk\n{log}" - )); - } - // What the guest read, and not that it approved of it: `first_bad` is one - // in-guest comparator, and this is the same bytes hashed off the image. - let mut staged = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(&image) - .expect("open the USB image to digest"); - for index in HOST_BLOCKS { - let block = block_of(bytes / BLOCK, index); - let want = digest(&read_block(&mut staged, block)); - let line = format!("usb-gate: host block {block} verified digest={want:#018x}"); - if !log.contains(&line) { - return Err(format!( - "the guest did not report {line:?}; what it read is not what the image holds, \ - whatever its own comparator said\n{log}" - )); - } - } - drop(staged); - - verify(&image, bytes, nonce)?; - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - let _ = std::fs::remove_file(&image); - - // The interlock, on a disk the harness owns end to end: no stamp, no - // writes. This is `foreign_disk_untouched`'s claim for the bus the machine - // boots from, and it is what keeps the gate feature from being a licence - // to write whatever disk happens to be plugged in. - let foreign = test_dir().join("usb-gate-foreign.img"); - drop(sparse(&foreign, bytes)); - let before = fingerprint(&foreign, bytes); - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk, - kernel_params: GATE, - usb_images: vec![foreign.clone()], - ..Default::default() - }, - )?; - gate_ran(&log, 2)?; - // ` designated, blocks=` and not `usb-gate: disk designated`, which the - // kernel has never printed — the disk index sits between the two words, so - // the assertion could not fire whatever the guest did. - if log.contains(" designated, blocks=") { - return Err(format!("the gate claimed an unstamped disk\n{log}")); - } - if fingerprint(&foreign, bytes) != before { - return Err("the guest wrote to a USB disk it was not given".to_string()); - } - let _ = std::fs::remove_file(&foreign); - - // The stamp's *geometry* guard, which nothing staged before this: a stamp - // written for another block count makes every offset in it name another block. - let blocks = bytes / BLOCK; - let claimed = blocks + 1; - let mis_stamped = test_dir().join("usb-gate-misstamped.img"); - stage(&mis_stamped, bytes); - { - let mut file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .open(&mis_stamped) - .expect("open the mis-stamped image"); - let mut head = read_block(&mut file, 0); - head[AT_BLOCKS..AT_BLOCKS + 8].copy_from_slice(&claimed.to_le_bytes()); - write_block(&mut file, 0, &head); - file.sync_all().expect("sync the mis-stamped image"); - } - let before = fingerprint(&mis_stamped, bytes); - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk, - kernel_params: GATE, - usb_images: vec![mis_stamped.clone()], - ..Default::default() - }, - )?; - gate_ran(&log, 2)?; - let refusal = format!("is stamped for {claimed} blocks and has {blocks}"); - if !log.contains(&refusal) { - return Err(format!("the gate did not refuse a stamp for {claimed} blocks\n{log}")); - } - if log.contains(" designated, blocks=") { - return Err(format!("the gate claimed a disk whose stamp is for another one\n{log}")); - } - if fingerprint(&mis_stamped, bytes) != before { - return Err("the guest wrote to a disk whose stamp is for another geometry".to_string()); - } - let _ = std::fs::remove_file(&mis_stamped); - - // And absence. The claim is about the bus, so it is checked against argv: - // no console line can tell "the driver bound one disk" from "only one disk - // was ever attached". - let options = BootOptions { - profile: Profile::Metal, - kernel_params: GATE, - ..Default::default() - }; - let argv = qemu::profile_argv(&options); - let sticks = argv - .windows(2) - .filter(|w| w[0] == "-device" && w[1].starts_with("usb-storage")) - .count(); - if sticks != 1 { - return Err(format!("metal-sim has {sticks} usb-storage devices, want just the boot stick")); - } - let log = boot_and_shutdown(test_config, c_bins, rust_bins, options)?; - gate_ran(&log, 1)?; - if !log.contains("usb-storage: 1 device(s)") { - return Err(format!("the driver did not bind exactly the boot stick\n{log}")); - } - - eprintln!(" [usb] {bytes} B / {lba} B sectors: host bytes read and their digests \ - recomputed host-side, guest bytes verified host-side; unstamped and \ - mis-stamped disks untouched; one disk on metal-sim"); - Ok(()) -} - -/// More disks on one controller than its DMA pool has blocks for. -/// -/// `MSC_BLOCKS` is 2 and the boot stick takes one, so the second data disk on -/// this bus is the first one past the ceiling. The bound is policy, which makes -/// what the caller sees when it is hit the whole question: the disk has to be -/// refused **by name** and left alone, never served out of somebody else's -/// block. -/// -/// Ground truth is host-side and it is what a log line cannot say: both staged -/// disks are stamped and writable as far as the guest is concerned, and the one -/// the pool had no room for comes back byte-for-byte as the harness left it. -pub fn usb_pool_exhausted( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let disks = Profile::UsbDiskCrowd.usb_disks(); - if disks.len() != 2 { - return Err(format!( - "this gate needs two data disks beside the boot stick, the profile declares {}", - disks.len() - )); - } - let bytes = disks[0].bytes; - - // Both stamped, so what decides which one is written is the pool and not - // the harness: the disk the driver refuses is whichever the controller - // enumerated second, and it is the one the gate never designates. - let bound = test_dir().join("usb-crowd-bound.img"); - let refused = test_dir().join("usb-crowd-refused.img"); - let nonce = stage(&bound, bytes); - stage(&refused, bytes); - let refused_before = fingerprint(&refused, bytes); - - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDiskCrowd, - kernel_params: GATE, - usb_images: vec![bound.clone(), refused.clone()], - ..Default::default() - }, - )?; - - // Two blocks, two disks, and the third refused by name with the pool's size - // in the line. The pool runs out inside `bind`, so this refusal is the - // driver's own and not a device's. - if !log.contains("usb-storage: 2 device(s)") { - return Err(format!("the driver did not bind exactly the pool's two blocks\n{log}")); - } - let over = log.matches("this driver serves 2").count(); - if over != 1 { - return Err(format!( - "{over} disk(s) were refused for want of a pool block, want the one past the \ - ceiling\n{log}" - )); - } - gate_ran(&log, 2)?; - - // The disk that bound was written, so the ceiling did not cost the machine - // the disk it does have room for. - verify(&bound, bytes, nonce)?; - - // **And the disk it had no room for was not touched.** A driver that served - // the refused disk out of somebody else's block would write these bytes - // under that disk's number, and every line in the log would still read - // correctly. - if fingerprint(&refused, bytes) != refused_before { - return Err("a disk the pool had no block for was written to".to_string()); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish past a crowded bus\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - for path in [&bound, &refused] { - let _ = std::fs::remove_file(path); - } - - eprintln!( - " [usb] three disks on a bus whose pool holds two: two bound and the staged one written, \ - {over} refused by name, and the stamped disk past the ceiling byte-identical host-side" - ); - Ok(()) -} - -/// The two device shapes that are not a 512-byte-sector stick: a 4 KiB-sector -/// one, which the whole stack above the sector layer has to divide by, and one -/// too large for the command this driver addresses it with. -pub fn usb_storage_shapes( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (bytes, lba) = Profile::UsbDisk4k.usb_disk().expect("UsbDisk4k declares a disk"); - if lba != 4096 { - return Err(format!("UsbDisk4k is a {lba}-byte-sector profile; it is the wrong one")); - } - let image = test_dir().join("usb-gate-4k.img"); - let nonce = stage(&image, bytes); - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk4k, - kernel_params: GATE, - usb_images: vec![image.clone()], - ..Default::default() - }, - )?; - gate_ran(&log, 2)?; - check_geometry(&log, bytes, lba)?; - if !log.contains("usb-gate: disk done reads=ok writes=ok refusal=true wr_err=0 healthy=true") { - return Err(format!("the 4 KiB-sector disk did not pass\n{log}")); - } - verify(&image, bytes, nonce)?; - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - let _ = std::fs::remove_file(&image); - - // A 3 TB disk has more sectors than READ(10) can address. The driver has - // to say so and bind nothing: serving its first 2 TiB would be a silent - // truncation of the device, and it is the only configuration in which - // READ CAPACITY(16) runs at all. - let (huge, _) = Profile::UsbDiskHuge.usb_disk().expect("UsbDiskHuge declares a disk"); - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDiskHuge, - kernel_params: GATE, - ..Default::default() - }, - )?; - let sectors = huge / 512; - let refusal = format!("has {sectors} sectors; this driver issues READ(10)"); - if !log.contains(&refusal) { - return Err(format!("the driver did not refuse the 3 TB disk by name ({refusal:?})\n{log}")); - } - // Refused, not dropped on the floor: the boot stick beside it still binds. - if !log.contains("usb-storage: 1 device(s)") { - return Err(format!("refusing the big disk cost the boot stick too\n{log}")); - } - gate_ran(&log, 1)?; - - eprintln!(" [usb] 4096 B sectors verified host-side; a {huge} B disk refused by name"); - Ok(()) -} - -/// The error channel, against a device that really refuses. -/// -/// Every other assertion in this file is about bytes, and bytes only prove the -/// path that works. `BlockDevice` returned `()` until recently, so a driver -/// could fail a transfer and the caller could not tell -- and the page cache -/// then labelled a slot with a block number whose read had not happened and -/// served the previous tenant's bytes under it. What makes this a real gate -/// rather than a mock is that nothing here injects anything: QEMU answers -/// WRITE(10) on a write-protected LUN with a CHECK CONDITION, which reaches -/// the driver as a CSW status of 1 and takes the REQUEST SENSE path that no -/// other test in this suite touches. -pub fn usb_storage_write_error( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (bytes, _) = Profile::UsbDiskReadOnly.usb_disk().expect("the profile declares a disk"); - let image = test_dir().join("usb-gate-ro.img"); - let nonce = stage(&image, bytes); - let before = fingerprint(&image, bytes); - - let options = BootOptions { - profile: Profile::UsbDiskReadOnly, - kernel_params: GATE, - usb_images: vec![image.clone()], - ..Default::default() - }; - // The claim is about how QEMU opened the file, and argv is the only place - // it is visible: a console line cannot tell a refused write from a write - // the guest never issued. - let argv = qemu::profile_argv(&options); - if !argv.iter().any(|a| a.contains("id=usbdisk") && a.contains("readonly=on")) { - return Err(format!("the data stick is not read-only in argv: {argv:?}")); - } - - let log = boot_and_shutdown(test_config, c_bins, rust_bins, options)?; - gate_ran(&log, 2)?; - - // Reads work, writes do not, and the guest could tell them apart. Before - // the trait carried a result this line read `writes=ok` on exactly this - // machine, because a refused write was indistinguishable from a completed - // one. - // Three write calls, three refusals *reported through the trait*. Not - // `writes=bad`, which this profile makes true anyway: the readback of a - // write that never landed differs whether or not the driver said so, and - // an assertion on it stayed green with `write_blocks` hard-wired to - // `Ok(())`. `wr_err` is zero in that build and three in this one. - if !log.contains("usb-gate: disk done reads=ok writes=bad refusal=true wr_err=3") { - return Err(format!( - "the guest did not see the device refuse its writes\n{log}" - )); - } - // The refusal came from the device, not from the driver's own bound: the - // sense data is what SCSI status 1 carries and nothing else in the driver - // produces this line. - if !log.contains("usb-storage: SCSI 0x2a failed, sense") { - return Err(format!("no WRITE(10) refusal with sense data in the log\n{log}")); - } - // And the reads on the same disk still verified, which is what stops - // "writes=bad" from being true because the whole device fell over. - if !log.contains("usb-gate: host block 1 verified") { - return Err(format!("reads failed too; this proves nothing about writes\n{log}")); - } - if fingerprint(&image, bytes) != before { - return Err("a write the device refused reached the backing file".to_string()); - } - let _ = nonce; - let _ = std::fs::remove_file(&image); - - eprintln!(" [usb] write-protected LUN: CSW status 1 seen, refusal reached the caller, \ - reads on the same disk unaffected"); - Ok(()) -} - -/// The geometry the guest derived, against what the profile handed it. This is -/// where a driver that believed the wrong sector size shows up: at 4 KiB -/// sectors and at 512 the block count is the same number, and it is the -/// *sector* size in the line that says which one it read. -fn check_geometry(log: &str, bytes: u64, lba: u32) -> Result<(), String> { - let blocks = bytes / BLOCK; - let want = format!("blocks of {lba} B"); - if !log.contains(&want) { - return Err(format!("the driver did not report {want:?}\n{log}")); - } - let want = format!("designated, blocks={blocks} "); - if !log.contains(&want) { - return Err(format!("the guest did not see {blocks} blocks ({want:?})\n{log}")); - } - // One stamped disk and one unstamped one, whichever order the controller - // enumerated them in. Asserting the index instead would be asserting - // QEMU's port assignment, which is not what this test is about. - if log.matches("carries no stamp, leaving it alone").count() != 1 { - return Err(format!("want exactly one unstamped disk, the boot stick\n{log}")); - } - Ok(()) -} - -/// The two answers a device can give to an *optional* SCSI command, and the -/// loop that reading them as one answer produced. -/// -/// SYNCHRONIZE CACHE (0x35) is optional in SBC and a great many USB flash -/// drives answer ILLEGAL REQUEST / INVALID COMMAND OPERATION CODE. `msc_flush` -/// read that as a failed flush; `FatFs::sync` logged the failure and returned -/// `()`; the line it logged was new pending content in the shard `/system/bin/logd` was -/// draining, and `Sink::flush` still said `Ok`, so the sink's disable path -/// never ran. Every idle pass was then a file write, a FAT write and another -/// SYNCHRONIZE CACHE on the stick the machine booted from, forever — and -/// `MAX_LOG_BYTES` rotates the boot log off the stick while it happens. -/// -/// Two boots, because the two halves of the fix are separately observable and -/// each is invisible to the other's boot: -/// -/// - `usb-flush-unimplemented` — the refusal is an answer, and the log has to -/// keep reaching the device exactly as on an ordinary boot. Fixing -/// `sync_mount` alone cannot produce that: the returned error disables the -/// sink and the file stops before `Boot: complete`. -/// - `usb-flush-fails` — the same command really failing. The sink has to -/// notice once and stop. Fixing `msc_flush` alone cannot produce that: the -/// error is swallowed and the loop is the one above. -/// -/// Neither boot can be green because the actuator was not armed: each asserts a -/// line that only the injected answer produces. -pub fn usb_flush_optional( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - optional_flush_keeps_the_log(test_config, c_bins, rust_bins)?; - failed_flush_stops_once(test_config, c_bins, rust_bins) -} - -/// Boot with a stick that has no write cache. Nothing about the log changes. -fn optional_flush_keeps_the_log( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["usb-flush-unimplemented"]; - const REPORTED: &str = "usb-storage: disk 0 does not implement SYNCHRONIZE CACHE"; - - let image_path = test_dir().join("usb-flush-optional.img"); - let image = qemu::build_boot_image(test_config, c_bins, rust_bins, PARAMS); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let (start, len) = super::volumes::log_extent(&image, &image_path)?; - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::Metal, - boot_image: Some(qemu::Staged::Written(image_path.clone())), - kernel_params: PARAMS, - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - - // Mid-run and polled, exactly as `kernel_log_file` does it: the claim is that - // the sink is still running, and the only place that is visible is the - // device while the machine is up. The ceiling is the harness's: a stick - // with no write cache that cost the machine its log is a hang here. - let give_up = std::time::Instant::now() + qemu.budget(qemu::GUEST_WEDGED); - loop { - let on_device = String::from_utf8_lossy( - &super::volumes::newest_log(&image_path, start, len)?.1, - ) - .into_owned(); - if on_device.contains("Boot: complete") { - break; - } - if std::time::Instant::now() >= give_up { - return Err(format!( - "{} waiting for `Boot: complete` in the log on a stick with no write cache: {} \ - bytes there", - qemu::STALLED, - on_device.len() - )); - } - std::thread::sleep(Duration::from_millis(50)); - } - - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let log = format!("{boot}{}", qemu.drain_serial(Duration::from_secs(20))); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} on a stick with no write cache\n{log}")); - } - } - - // The injection reached the driver, and the driver said so once. Once is - // half the assertion: a line per flush is itself the loop, because this - // log's own bytes are what the next flush writes. - let said = log.matches(REPORTED).count(); - if said != 1 { - return Err(format!( - "the guest printed {REPORTED:?} {said} times, wanted exactly one\n{log}" - )); - } - for wrong in ["usb-storage: cache flush failed", "usb-storage: SCSI 0x35 failed"] { - if log.contains(wrong) { - return Err(format!( - "an optional command a device does not have was reported as a failure ({wrong:?})\ - \n{log}" - )); - } - } - if log.contains("logd: /log has not answered") { - return Err(format!("logd gave up on a stick that is working\n{log}")); - } - let after = super::volumes::newest_log(&image_path, start, len)?.1; - let after = String::from_utf8_lossy(&after).into_owned(); - // `/log` ends at init's stop line: the kernel's own last word comes after - // the stop of every thread, `logd` among them, and is on the console alone. - if toyos_build::bootlog::stopping_line(&after).is_none() { - return Err(format!( - "init's stop line never reached the file, so the log did not survive to the \ - shutdown: {} bytes", - after.len() - )); - } - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [usb] SYNCHRONIZE CACHE refused as unimplemented: reported once, {} bytes of kernel \ - log still on the stick", - after.len() - ); - Ok(()) -} - -/// Ask test-runner to run `name`, a binary no image carries, and wait for its -/// answer. The spawn's refusal is a kernel record -/// (`spawn: /system/bin/: not found`), which is the probe's load; any other -/// answer ends the wait red, naming it. -fn absent_probe( - qemu: &mut QemuInstance, - console: &mut String, - name: &str, - after: &str, -) -> Result<(), String> { - let asked = console.len(); - writeln!(qemu.stdin_mut(), "run {name}").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let answer = format!("===TEST_END {name} "); - let refused = format!("===TEST_END {name} error=entity not found==="); - qemu::await_guest(qemu, console, &format!("test-runner's answer to {name} {after}"), |c| { - c[asked..].contains(&answer) - })?; - match console[asked..].lines().find(|line| line.contains(&answer)) { - Some(line) if line.contains(&refused) => Ok(()), - line => Err(format!( - "test-runner answered {name} {after} with {line:?}, and a name no image carries is \ - answered {refused:?}" - )), - } -} - -/// Boot with a stick whose flush genuinely fails. The writer says so once and -/// stops, rather than writing the device that just refused it. -/// -/// **Re-pointed at `/system/bin/logd` at L6, and the policy it observes changed shape -/// with the writer.** The kernel sink disabled itself on the *first* error, -/// because the alternative from an idle loop was an error every pass. logd's -/// give-up is a *duration* — `LOG_WRITE_BUDGET`, five seconds — because a -/// userland writer can -/// afford to tell a stick that is busy apart from one that is gone, and a -/// device that answers slowly under load is not a device to abandon. -/// -/// The probes below are what make "and stops" a claim rather than an absence: -/// each names a binary that is not there, so each commits a kernel record, so -/// each is something logd would write if it had not given up. Twelve of them -/// after the give-up and the failing-flush count still has to hold. -fn failed_flush_stops_once( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["usb-flush-fails"]; - /// Probes after the boot, each of which spawns a name that is not there and - /// so commits a kernel record logd would write if it were still writing. - const PROBES: usize = 12; - /// A per-failure line, and the thing that has to stay bounded. Before the - /// fix it is emitted by every pass of the idle loop for the life of the - /// boot. After it: one by the write that gives up. - /// - /// **This is the number that caught the retry**, and it is worth saying what - /// it caught. A logd that retried inside `LOG_WRITE_BUDGET` measured - /// **1,737** failing flushes here, because the driver logs each failure, the - /// failure is a kernel record, and the record is something logd then tries - /// to write. The loop is in the coupling and not in either half. - const BOUND: usize = 4; - /// logd's word as it gives up, naming the sync as the call that refused it. - const GAVE_UP: &str = "logd: /log has not answered (the sync"; - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::Metal, - kernel_params: PARAMS, - ..Default::default() - }, - ); - let mut boot = qemu.boot_log().to_string(); - // The give-up first, then the probes after it, each *driven* and awaited: - // each names a binary that is not there, which commits a kernel record, - // which is what gives logd something to fail to write. - qemu::await_marker(&mut qemu, &mut boot, GAVE_UP, "logd to give up on the sync")?; - for i in 0..PROBES { - absent_probe(&mut qemu, &mut boot, &format!("flush-probe-{i}"), "after the give-up")?; - } - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let log = format!("{boot}{}", qemu.drain_serial(Duration::from_secs(20))); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} on a stick that cannot flush\n{log}")); - } - } - - if !log.contains("usb-storage: SCSI 0x35 failed, sense 0x04/0x44/0x00") { - return Err(format!("the injected flush failure never reached the driver\n{log}")); - } - // By step and not by code alone: logd names which of the two calls refused - // it, and the one this test stages is the sync rather than the append ahead - // of it. - let gave_up = log.matches(GAVE_UP).count(); - if gave_up != 1 { - return Err(format!( - "logd gave up {gave_up} times, wanted exactly one — a failed `SYS_FSYNC` has to \ - reach it as an error, and once it has given up it must not start again\n{log}" - )); - } - let failures = log.matches("usb-storage: cache flush failed").count(); - if failures > BOUND { - return Err(format!( - "the guest issued {failures} failing flushes, over the bound of {BOUND}: a failed \ - sync is still producing the log line that asks for the next one\n{log}" - )); - } - eprintln!( - " [usb] a flush the device refuses: {failures} failing flushes with {PROBES} probes \ - after it, logd stopped once and never started again" - ); - Ok(()) -} - -/// A controller and a port that stop answering, which on the machine this is -/// for is a silent hang and nothing else. -/// -/// The 2 s deadline covered `wait_command` and `wait_transfer` and nothing -/// around them: the port-reset spin in `init_device` and four register spins in -/// `init_one` — halt, HCRST, CNR and R/S — were bare `spin_loop`s. On a T14 -/// that is `Boot: peripherals ready` painted on the panel forever, which is -/// also what a dead port, a dead controller and every other wedge look like. -/// -/// Both boots assert the same shape: the thing that did not answer is named, -/// and the machine gets to the shell anyway. `arm_interrupt` already refuses a -/// controller by name; these waits bypassed that machinery entirely. -pub fn xhci_deaf_registers( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::MetalXhciDeaf, - kernel_params: &["xhci-deaf-controller"], - ..Default::default() - }, - )?; - if !log.contains("it never halted, within 2000 ms of being asked to") { - return Err(format!("the controller that would not halt was not named\n{log}")); - } - if !log.contains("xHCI: 1 controller(s) present, none of them usable, USB unavailable") { - return Err(format!( - "a refused controller did not reach `init`'s own summary — a machine with no xHC and \ - one whose xHC was refused are different machines\n{log}" - )); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish without its USB controller\n{log}")); - } - - // And the port, which is the wait an ordinary machine can actually reach: - // a device pulled between the port scan and the reset lands here. - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::MetalXhciDeaf, - kernel_params: &["xhci-deaf-port"], - ..Default::default() - }, - )?; - let skipped = log.matches("never finished its reset").count(); - if skipped == 0 { - return Err(format!("no port was named as having failed its reset\n{log}")); - } - // The controller itself came up, which is what makes this a *port* refusal - // and not the previous boot again. - if !log.contains("xHCI: controller started") { - return Err(format!("the controller did not start; this is not the port path\n{log}")); - } - if !log.contains("xHCI: 1 controller(s), 0 HID device(s)") { - return Err(format!("a port that never reset still bound its device\n{log}")); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish past a port that would not reset\n{log}")); - } - eprintln!( - " [usb] a controller that will not halt is refused by name; {skipped} port(s) that will \ - not reset are skipped; both machines reach `Boot: complete`" - ); - Ok(()) -} - -/// A root hub that has not finished detecting its devices when the driver first -/// looks — which is every root hub that is made of copper. -/// -/// HCRST puts the ports back to the state they have with nothing attached, so a -/// device firmware had already enumerated has to be detected again, and -/// detection takes milliseconds: power settling, a USB2 pull-up being debounced, -/// a USB3 link training. The T14 logged `controller started` and -/// `no HID devices` in the same millisecond, on both controllers, while running -/// off a stick plugged into one of them. -/// -/// **The actuator is a boot parameter, and the reason is timing rather than -/// expressiveness.** QEMU *can* stage a late attach: `usb-bot` and `usb-uas` -/// are the two devices whose QOM `attached` property is settable, so -/// `qom-set /machine/peripheral/ attached false|true` detaches and -/// reattaches at runtime and does generate a Port Status Change Event -/// (`xhci_attach` → `xhci_port_update` → `xhci_port_notify`, QEMU 11.0.2 -/// `hw/usb/hcd-xhci.c`). What it cannot do is *aim*: the port scan happens -/// ~0.1 s into a boot and the driver's detection window is bounded, so a -/// host-wall-clock QMP write would have to land inside a window the guest -/// opens. That makes the outcome a race rather than an assertion. -/// `xhci-slow-connect` replaces the *register* instead — during the window the -/// port reads CCS, PED and speed exactly as an unpopulated one does — so what -/// appears afterwards is QEMU's own device with its own descriptors and its own -/// bytes, and the host-side verification below is the same one the ordinary -/// gate runs. -pub fn xhci_slow_connect( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["usb-storage-gate", "xhci-slow-connect"]; - let (bytes, lba) = Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = test_dir().join("usb-slow-connect.img"); - let nonce = stage(&image, bytes); - - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk, - kernel_params: PARAMS, - usb_images: vec![image.clone()], - ..Default::default() - }, - )?; - - // And it found everything, and the bytes are the host's. - if !log.contains("usb-storage: 2 device(s)") { - return Err(format!("the driver did not bind both sticks after the wait\n{log}")); - } - gate_ran(&log, 2)?; - check_geometry(&log, bytes, lba)?; - if !log.contains("usb-gate: disk done reads=ok writes=ok refusal=true wr_err=0 healthy=true") { - return Err(format!("the guest did not report a clean pass\n{log}")); - } - verify(&image, bytes, nonce)?; - if toyos_build::bootlog::boot_millis(&log).is_none() { - return Err(format!("the boot did not finish\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - let _ = std::fs::remove_file(&image); - - eprintln!(" [usb] both sticks bound after the held-empty window, host bytes verified host-side"); - Ok(()) -} - -/// A controller on which PORTSC's write-1-to-clear bits mean what the spec says -/// they mean — which QEMU's does not, and which is why every test in this suite -/// was green while five devices on the T14 all reported "not enabled after -/// reset". -/// -/// PED is bit 1 and it is RW1CS: "A port may be disabled by software writing a -/// '1' to this flag" (xHCI 1.2 §5.4.8 Table 5-27), and §4.19.1.1.6 takes the -/// port from Enabled to Disabled when that write lands. §4.19.5 leaves PED and -/// PRC both set after a successful reset, so a read-modify-write that cleared -/// PRC by handing back everything else it read disabled the port it had just -/// enabled — on every port, on every controller, on any machine whose PORTSC is -/// made of silicon. -/// -/// **The actuator is a boot parameter because nothing on the host side can -/// reach it.** QEMU's `xhci_port_write` clears only -/// `CSC|PEC|WRC|OCC|PRC|PLC|CEC` on a written '1', and PED is in neither that -/// set nor its read/write set, so writing PED=1 there does nothing at all -/// (`hw/usb/hcd-xhci.c`). No device or machine property changes that, and no -/// sequence of register writes reaches a PED=0/CCS=1 port either — clearing PP -/// is the closest and leaves PP=0, a different register state and a different -/// diagnosis. `xhci-portsc-rw1c` replaces the *register*: after the driver -/// writes PED=1 that port reads PED clear for every reader, and only a reset -/// clears it, because a reset is what takes a real port out of Disabled -/// (§4.19.1.1.3). -/// -/// The count line is what stops this from passing because nothing was armed. -/// Only the emulation prints it, and it has to say zero — so "the injection is -/// live" and "the driver never wrote PED" are separate assertions, and the -/// per-port ones below are the register's own consequence rather than a verdict. -pub fn xhci_portsc_rw1c( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // Six devices rather than one, because the T14's failure was every port at - // once: a machine with a single stick cannot tell "one port survived" from - // "ports survive". The hub is a device the driver walks past, and the boot - // stick attaches at SuperSpeed, so both protocols' reset paths run here. - let options = BootOptions { - profile: Profile::MetalUsb, - kernel_params: &["xhci-portsc-rw1c"], - ..Default::default() - }; - let argv = qemu::profile_argv(&options); - let usb = crate::usb_argv(&argv); - if usb.len() < 4 { - return Err(format!("this gate needs a crowded bus, argv has {usb:?}")); - } - - let qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let log = qemu.boot_log().to_string(); - - // The emulation ran and saw nothing. Without the first half a boot with the - // feature accidentally off passes everything below it. - const ACCOUNTED: &str = "xHCI: PED as RW1C, "; - let Some(verdict) = log.lines().find(|l| l.contains(ACCOUNTED)) else { - return Err(format!("the PED emulation never reported; was it compiled in?\n{log}")); - }; - if !verdict.contains("0 port(s) disabled by a driver write") { - return Err(format!("the driver wrote PED=1 to a port: {verdict:?}\n{log}")); - } - - // And the register's own consequence: every port that connected came out of - // its reset enabled. This is the pair of counts the T14 printed as 5 and 0. - let mut connected = 0usize; - let mut enabled = 0usize; - let mut refused: Vec<&str> = Vec::new(); - for line in log.lines() { - let Some(rest) = line.split("xHCI: port ").nth(1) else { continue }; - if rest.contains("connected") { - connected += 1; - } - if rest.contains("enabled, speed=") { - enabled += 1; - } - if rest.contains("not enabled") || rest.contains("never finished its reset") { - refused.push(line); - } - } - if !refused.is_empty() { - return Err(format!("{} port(s) refused: {refused:?}\n{log}", refused.len())); - } - if connected != usb.len() { - return Err(format!( - "{connected} port(s) reported a device, {} on the bus:\n{log}", - usb.len() - )); - } - if enabled != connected { - return Err(format!( - "{connected} port(s) connected and {enabled} reached the Enabled state:\n{log}" - )); - } - - // Enabled is not enumerated. A port can read PED=1 and still produce - // nothing, so the devices behind these ports have to come out the far end. - let slots = crate::parse_xhci_slots(&log); - if slots.len() != usb.len() { - return Err(format!( - "{} slots enabled for {} devices ({slots:?}):\n{log}", - slots.len(), - usb.len() - )); - } - let binds = crate::parse_xhci_binds(&log); - let keyboards = binds.iter().filter(|b| b.kind == "keyboard").count(); - if keyboards != 2 { - return Err(format!("{keyboards} keyboards bound, want 2: {binds:?}\n{log}")); - } - let disks = log.matches("usb-storage: disk ").count(); - if disks != 1 { - return Err(format!("{disks} disks bound, want the boot stick:\n{log}")); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - - eprintln!( - " [xhci] PED honoured as RW1C: {connected}/{connected} ports connected reached Enabled, \ - 0 disabled by a driver write, {} slots, {keyboards} keyboards, {disks} disk", - slots.len() - ); - Ok(()) -} - -/// What the boot scan says of a device on a link something before this kernel -/// trained, before it asks the device anything. -const INHERITED: &str = - "link already trained before this kernel ran; warm resetting it before its device is asked \ - anything"; - -/// Where `reset_moves` holds the port rung for the host to unplug the stick. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum Held { - /// `usb-reset-moves`, before the reset's completion is read: the reset - /// reads the port empty. - BeforeItsCompletion, - /// `usb-reset-moves-after`, once the completion has been read with the - /// stick on the port, as a USB2 port reads a device that leaves under its - /// reset: the rung's next step fails on the empty port. - AfterItsCompletion, - /// `usb-reset-moves-configured`, once the rung has configured the stick - /// again: its TEST UNIT READY breaks on the empty port. - BeforeItsTestUnitReady, -} - -/// The gate's data stick, owed a WRITE's data by the staged break, leaves its -/// port inside the port rung the break entered and is not plugged back: the -/// rung ends as the stick leaving. No rung takes it offline, no second break is -/// counted, and its port's teardown gives the slot back. -/// -/// **QEMU cannot take a device off its port on a reset**, so `reset_moves` -/// holds the rung once, at the place [`Held`] names, until the port reads -/// empty, and the host unplugs the stick on the cue the hold writes. -pub fn usb_stick_left( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - for held in [Held::BeforeItsCompletion, Held::AfterItsCompletion, Held::BeforeItsTestUnitReady] { - a_stick_that_left_under_its_rung(test_config, c_bins, rust_bins, held)?; - } - Ok(()) -} - -fn a_stick_that_left_under_its_rung( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - held: Held, -) -> Result<(), String> { - const MOVE_NOW: &str = "usb-reset-moves: move the device now"; - let (params, hold, ended): (&'static [&'static str], &str, &str) = match held { - Held::BeforeItsCompletion => ( - &["usb-storage-gate", "usb-transport-break", "usb-reset-moves"], - "is held empty for the host to move its device (usb-reset-moves)", - "the port reset was not answered", - ), - Held::AfterItsCompletion => ( - &["usb-storage-gate", "usb-transport-break", "usb-reset-moves-after"], - "is held, reset with its device on it, for the host to move the device \ - (usb-reset-moves-after)", - "the port reset was not answered", - ), - Held::BeforeItsTestUnitReady => ( - &["usb-storage-gate", "usb-transport-break", "usb-reset-moves-configured"], - "is held, configured again, for the host to move the device \ - (usb-reset-moves-configured)", - "transport broke on the port reset's TEST UNIT READY: the port disconnected during \ - the command phase", - ), - }; - let (bytes, _) = Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = test_dir().join(format!("usb-stick-left-{held:?}.img")); - stage(&image, bytes); - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk, - qmp: true, - kernel_params: params, - usb_images: vec![image.clone()], - // The hold is inside the boot's USB gate, before any ready marker. - ready_marker: toyos_build::bootlog::LOADER_LAST_LINE, - ..Default::default() - }, - ); - let mut log = qemu.boot_log().to_string(); - // On the cue the staging writes to the console itself: the record above it - // waits for `klogd`, which may not run while the rung holds its CPU. - log.push_str(&qemu.drain_until(Duration::from_secs(60), |l| l.contains(MOVE_NOW))); - if !log.contains(MOVE_NOW) { - return Err(format!("{held:?}: the port rung was never held for the host\n{log}")); - } - let mut devices = qemu::QmpDevices::open(qemu.qmp_socket()); - devices.del(&qemu::usb_device_id(0)); - drop(devices); - // Whichever way the rung ended, a slot goes back after the hold: the - // teardown's, or the last rung's. - qemu::await_guest(&mut qemu, &mut log, "the boot to complete and a slot to go back", |c| { - c.contains("Boot: complete") - && c.split_once(hold).is_some_and(|(_, after)| { - after.lines().any(|l| l.contains("xHCI: slot ") && l.ends_with(" disabled")) - }) - }) - .map_err(|why| format!("{held:?}: {why}\n{log}"))?; - drop(qemu); - let _ = std::fs::remove_file(&image); - - let kernel = serial::Serial::named(&format!("{held:?} boot console"), log.as_str()); - let staged = kernel.must_say( - "transport broke on SCSI 0x2a: a staged break skipped the data phase wait; break 1 of ", - )?; - let under_test = broke_on(staged)?; - let entered = kernel.must_say_after( - staged, - &format!("usb-storage: {under_test} is owed the data of the command that broke"), - )?; - let held_there = kernel.must_say_after(entered, hold)?; - let port = held_there - .split_once(&format!("xHCI: {under_test} port ")) - .and_then(|(_, rest)| rest.split_once(' ')) - .map(|(port, _)| port) - .ok_or_else(|| format!("{held:?}: {held_there:?} holds no port of {under_test}\n{log}"))?; - let left = kernel.must_say_after( - held_there, - &format!( - "usb-storage: {under_test} {ended}, and port {port} no longer holds the device (PORTSC " - ), - )?; - // Nothing is sent to the empty port once the reset is read: the reset - // that read it so ends the rung, and so does the TEST UNIT READY that - // met it. - if held != Held::AfterItsCompletion { - let (_, from_the_hold) = log.split_once(held_there).expect("the line came from this text"); - let (between, _) = from_the_hold.split_once(left).expect("the leave follows the hold"); - for sent in ["Reset Device failed", "Address Device (after the port reset)"] { - if let Some(line) = between.lines().find(|l| l.contains(sent)) { - return Err(format!("{held:?}: {line:?} between the hold and the leave\n{log}")); - } - } - } - let slot = under_test.rsplit(' ').next().expect("a slot id ends the name"); - let gone_back = kernel.must_say_after(left, &format!("xHCI: slot {slot} disabled"))?; - kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?; - if let Some(line) = log - .lines() - .find(|l| l.contains(&format!("usb-storage: {under_test} ")) && l.contains(" break 2 of ")) - { - return Err(format!("{held:?}: {line:?}: the stick leaving was counted as a break\n{log}")); - } - kernel.must_be_clean()?; - eprintln!(" [usb] {held:?}: {left}"); - eprintln!(" [usb] {held:?}: {gone_back}"); - Ok(()) -} - /// The staged break on a real stick: the transfer abandoned on the boot stick's /// first WRITE(10) is recovered, the write completes, the disk keeps its /// number, and the boot goes on to the deliberate reboot that ends its chain. @@ -1385,605 +67,3 @@ fn broke_on(line: &str) -> Result<&str, String> { }) } -/// A SuperSpeed port is not reset into existence, and the driver knows which -/// ports are which because it read the controller's own description of itself. -/// -/// The Supported Protocol capability (§7.2) was never parsed, so every port -/// register looked alike and every one got the USB2 treatment: write PR, wait -/// for PRC. A USB3 link trains itself and reaches Enabled with nothing done to -/// it (§4.19.1.2), so that write is a *hot reset of a working link* — and a -/// link that cannot take one lands Inactive, which only a warm reset this -/// driver did not have would have left. On the T14 that is a USB-A socket that -/// mounts nothing, two boots out of two, while the same stick through a Type-C -/// adapter mounts every time: the adapter lands it on the connector's USB2 -/// pins, and a USB2 port is the one shape the old driver knew. -/// -/// **What this gate can and cannot say.** QEMU's xHC publishes real Supported -/// Protocol capabilities, so the decode and the branch are certified here -/// against a controller's own bytes. It has no link training and no Inactive -/// state, so the warm-reset recovery is unreachable and is certified by the -/// host model instead (`toyos-xhci/sim/tests/superspeed.rs`). This says: the -/// driver read the split correctly, hot resets no trained link, and warm resets -/// the one the firmware trained before enumerating its device. It says nothing -/// about what happens when a link falls over. -pub fn xhci_superspeed_ports( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { profile: Profile::MetalUsb, ..Default::default() }; - let devices = crate::usb_argv(&qemu::profile_argv(&options)).len(); - let log = boot_and_shutdown(test_config, c_bins, rust_bins, options)?; - - // The controller described itself and the driver read it. `nec-usb-xhci` - // with `p2=8` is four SuperSpeed registers and eight USB2 ones, and the - // driver has to name that split without being told it. - const SPLIT: &str = "xHCI: 8 USB2 and 4 USB3 port register(s) of 12 named, \ - 0 capability(ies) refused"; - if !log.contains(SPLIT) { - let got = log.lines().find(|l| l.contains("port register(s) of")); - return Err(format!( - "the driver did not read the controller's protocol split; got {got:?}\n{log}" - )); - } - - // The boot stick is the only SuperSpeed device this profile attaches, so it - // takes a USB3 register and every HID takes a USB2 one. Exactly one port - // must therefore have been found on a trained link — and, the firmware - // having trained it, warm reset before its device is asked anything. - let trained: Vec<&str> = log.lines().filter(|l| l.contains("link already trained")).collect(); - if trained.len() != 1 { - return Err(format!( - "{} port(s) came up on an already-trained link, want the SuperSpeed stick alone: \ - {trained:?}\n{log}", - trained.len() - )); - } - if !trained[0].contains(INHERITED) { - return Err(format!("{:?} does not read {INHERITED:?}\n{log}", trained[0])); - } - - // And every device still reached Enabled. - let enabled = log.matches("enabled, speed=").count(); - if enabled != devices { - return Err(format!( - "{enabled} port(s) reached Enabled, {devices} devices on the bus\n{log}" - )); - } - for wrong in ["never finished its reset", "would not train", "failed its hot reset", "did not take a hot reset"] { - if let Some(line) = log.lines().find(|l| l.contains(wrong)) { - return Err(format!("{line:?} on a bus where every link is healthy\n{log}")); - } - } - // Every `mmio:` line is the kernel reading its own page table back; the - // xHCI BAR in particular must have come up uncacheable. - let mmio: Vec<&str> = log.lines().filter(|l| l.contains("mmio: ")).collect(); - if mmio.is_empty() { - return Err(format!("no mmio: line — the kernel never said what its windows select\n{log}")); - } - for line in &mmio { - if !line.contains("PAT Uncacheable") && !line.contains("PAT WriteCombining") { - return Err(format!("{line:?} defers a register window to firmware\n{log}")); - } - } - if !mmio.iter().any(|l| l.contains("+0x10000 PAT Uncacheable")) { - return Err(format!( - "no xHCI register window came up uncacheable: {mmio:?}\n{log}" - )); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - - eprintln!( - " [xhci] the controller's own capability names 8 USB2 and 4 USB3 registers; the \ - SuperSpeed stick is enumerated on a link that was already trained, and {enabled} \ - port(s) reached Enabled" - ); - Ok(()) -} - -/// A device pulled and pushed back before the driver has looked at the port -/// twice — which is what a person replugging a mouse does. -/// -/// **`PORTSC.CCS` is a level and `PORTSC.CSC` is the edge, and only the edge can -/// report a gap.** The driver debounces a disconnect for 100 ms before acting on -/// it, so a device back in the port inside that window reads connected again at -/// the next look, matching what the driver already believed. Comparing CCS -/// against that belief therefore sees nothing at all: the old slot stays bound -/// to a device that is gone, the new one is never enumerated, and the port is -/// dead until something else disturbs it. xHCI 1.2 §5.4.8 sets CSC on a -/// '0'→'1' *or* a '1'→'0' transition, so a connected port with CSC set is the -/// only evidence that the connection was broken in between. -/// -/// The T14 showed the other half of the same race: the transfers outstanding -/// when the mouse was pulled completed with a transaction error, and that code -/// is indistinguishable from a bad cable's. The driver spent a failure out of -/// the budget, ran Reset Endpoint and a CLEAR_FEATURE(HALT) control transfer -/// against a device the owner was holding, and then printed advice to unplug it. -/// Four times, once per ordinary unplug. -/// -/// The actuator is QEMU's own `device_del`/`device_add` with no wait between -/// them, which lands both edges inside one debounce. No actuator: the -/// window is 100 ms wide and two QMP commands on a unix socket cross it easily. -pub fn xhci_flap( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Enough cycles that a driver leaking one slot per cycle is unmistakable, - /// and few enough that the guest's input window covers them. - const CYCLES: usize = 4; - const DX: i32 = 40; - const DY: i32 = -30; - /// **The device has to come back to the port it left.** Without this QEMU - /// hands each `device_add` the next free root-hub port, so a del/add pair is - /// a clean disconnect on one port and a clean connect on another — two - /// ordinary events, and never the state under test. Measured: the first - /// shape of this gate walked ports 5, 6, 7, 8 and staged nothing. - const PORT: &str = "1"; - const COLLAPSED: &str = "was unplugged and plugged back in between two looks"; - - let options = BootOptions { - profile: Profile::MetalHotplug, - qmp: true, - i8042: false, - ..Default::default() - }; - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let boot = qemu.boot_log().to_string(); - let Some((scale_x, scale_y)) = crate::parse_rel_scale(&boot) else { - return Err(format!("the kernel never said what pointer scale it used:\n{boot}")); - }; - let bound = |line: &str| !crate::parse_pointer_sources(line).is_empty(); - - // Each move waits for the guest to print the one before it. - let (mut ready, mut binds, mut mev) = (false, 0usize, 0usize); - let mut input: Option = None; - let result = qemu.run_test_paced("test_rs_input_events", Duration::from_secs(60), |socket, line| { - let qmp = || socket.expect("xhci_flap needs BootOptions { qmp: true }"); - if line.contains("===INPUT_READY===") { - ready = true; - qemu::QmpDevices::open(qmp()).add("usb-mouse", "xhci1.0", "flap0", &[("port", PORT)]); - return; - } - if ready && bound(line) { - binds += 1; - if binds <= CYCLES { - let cycle = binds - 1; - let mut devices = qemu::QmpDevices::open(qmp()); - // No wait between the two: both edges have to land inside one - // 100 ms debounce, which is the whole point. A fresh id each - // cycle because `device_del` releases the old one - // asynchronously and a reused one races with that. - devices.del(&format!("flap{cycle}")); - devices.add("usb-mouse", "xhci1.0", &format!("flap{}", cycle + 1), &[("port", PORT)]); - } else if binds == CYCLES + 1 { - // The pointer that is in the port now has to work. Off the - // origin first: the accumulated position clamps at 0. - input.insert(qemu::QmpInput::open(qmp())).mouse(100, 100, None); - } - return; - } - let Some(input) = input.as_mut() else { return }; - if line.contains("mev buttons=") { - mev += 1; - match mev { - 1 => input.mouse(DX, DY, None), - 2 => crate::input_events_end(input), - _ => {} - } - } - }); - if let Some(err) = &result.error { - return Err(format!("{err}\n{}\n{}", result.serial, result.stdout)); - } - let log = &result.serial; - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} while the port was flapped\n{log}")); - } - } - - // **Every cycle's device bound before the next cycle's edges went in**, so - // a cycle that never bound is named by the last thing its port did. - if binds != CYCLES + 1 { - let last = log.lines().rfind(|l| l.contains("xHCI: port ") || bound(l)); - let why = match last { - _ if binds > CYCLES + 1 => "more binds than plugs", - Some(line) if line.contains(COLLAPSED) => { - "a collapsed replug was torn down and its port never looked at again" - } - _ => "the device in the port never bound", - }; - return Err(format!( - "{why}: {binds} bind(s) for {} plugs before the guest's input window closed; the \ - port's last line was {last:?}\n{log}", - CYCLES + 1 - )); - } - - // The race was actually staged. Without this the gate would pass on a run - // where every replug happened to be seen as two distinct states, which is - // the easy case and not the one under test. - let collapsed = log.matches(COLLAPSED).count(); - if collapsed == 0 { - return Err(format!( - "no replug collapsed inside a debounce, so this run never staged the race.\n{log}" - )); - } - - // **Bounded slots.** Each cycle's device must give its slot back before the - // next takes one. A driver that enumerates on top of the old slot marches - // through fresh ids — 6, 7, 8, 9 on the T14 — and leaves every one enabled. - let enabled = slot_ids(log, "enabled"); - let disabled = slot_ids(log, "disabled"); - let live: Vec = { - let mut left = disabled.clone(); - enabled.iter().copied().filter(|id| !take_one(&mut left, *id)).collect() - }; - if live.len() != 1 { - return Err(format!( - "{} slot(s) enabled and never disabled ({live:?}) after {CYCLES} replugs; exactly \ - the one in the port now should be live\nenabled {enabled:?}\ndisabled \ - {disabled:?}\n{log}", - live.len() - )); - } - let mut distinct = enabled.clone(); - distinct.sort_unstable(); - distinct.dedup(); - if distinct.len() > 2 { - return Err(format!( - "the driver used {} distinct slot ids across {CYCLES} replugs ({distinct:?}) — a slot \ - is not being reaped before the next enumeration takes one\n{log}", - distinct.len() - )); - } - - // **Sources reclaimed.** One pointer is in the port at a time, so every - // bind must print the same button-table entry. A leak marches 2, 3, 4, 5. - let sources: Vec = crate::parse_pointer_sources(log).iter().map(|(_, s)| *s).collect(); - if sources.iter().any(|s| *s != sources[0]) { - return Err(format!( - "pointer sources were {sources:?} — a replugged pointer took a fresh button-table \ - entry, so the one its predecessor held was never given back\n{log}" - )); - } - - // **No recovery against a device that is not there.** Every one of these is - // the driver treating an unplug as a broken cable: a failure out of the - // budget, a control transfer that spends the deadline failing, and advice - // to unplug something already in the owner's hand. - for wrong in [ - "is being let go", - "could not be restarted", - "endpoint 3 is Halted, recovering", - ] { - if let Some(line) = log.lines().find(|l| l.contains(wrong)) { - return Err(format!( - "the driver ran recovery against a device that had been unplugged: {line:?}\n{log}" - )); - } - } - // And the line that says it declined to, which is the positive half: the - // errors really did arrive and really were attributed to the disconnect. - let superseded = log.matches("as its port went away; leaving it to the disconnect").count(); - - // The device in the port now delivers. This is what stops every assertion - // above from passing on a driver that reaped everything and enumerated - // nothing. - let pointer = crate::parse_mouse_events(&result.stdout); - let want = (DX * scale_x, DY * scale_y); - let deltas: Vec<(i32, i32)> = pointer - .windows(2) - .map(|w| (w[1].x as i32 - w[0].x as i32, w[1].y as i32 - w[0].y as i32)) - .collect(); - if !deltas.contains(&want) { - return Err(format!( - "no pointer event moved by {want:?} after {CYCLES} replugs; deltas seen: {deltas:?} — \ - the port is bound to a device that is no longer in it\n{}", - result.stdout - )); - } - - eprintln!( - " [xhci] {CYCLES} replugs collapsed inside the debounce ({collapsed} seen as such): \ - {} slot(s) enabled and all but one reaped, one button-table entry reused throughout, \ - {superseded} transfer error(s) attributed to the disconnect instead of recovery, and \ - the pointer in the port still delivers", - enabled.len() - ); - Ok(()) -} - -/// Every slot id in an `xHCI: slot 3 enabled …` or `… disabled` line, in order. -fn slot_ids(log: &str, verb: &str) -> Vec { - log.lines() - .filter_map(|line| { - let rest = line.split("xHCI: slot ").nth(1)?; - let (id, tail) = rest.split_once(' ')?; - tail.starts_with(verb).then(|| id.parse().ok())? - }) - .collect() -} - -/// Remove one occurrence of `id`, and say whether there was one. -fn take_one(pool: &mut Vec, id: u8) -> bool { - match pool.iter().position(|x| *x == id) { - Some(at) => { - pool.remove(at); - true - } - None => false, - } -} - -/// A disk the driver refuses, on the port the controller enumerates *first*. -/// -/// `bind` claims a 64 KiB DMA pool block, issues Configure Endpoint — which -/// puts the device's two bulk endpoints into the Running state with their -/// transfer rings inside that block — and only then asks the disk how big it -/// is. A disk refused at that last step never joins `ctrl.storage`, so a block -/// keyed on `ctrl.storage.len()` was handed straight to the next disk, while -/// the first device's slot was still enabled, its endpoint contexts still named -/// that memory, and any transfer `wait_transfer` had abandoned on its 2 s -/// deadline was still outstanding on a Running endpoint. The late completion -/// lands in the next disk's `MSC_SCRATCH` — where READ CAPACITY's block size -/// and last LBA arrive. -/// -/// Every other USB profile puts the boot stick on port 1, where it binds and -/// the reuse cannot happen; that is why a full gate boot never reached this. -/// The actuator is not a boot parameter: QEMU can already stage a disk this -/// driver refuses (3 TB, more sectors than READ(10) addresses) and it assigns -/// ports in device-creation order, so attaching it ahead of the boot stick is -/// the whole injection. Nothing about the driver is modified to run this. -/// -/// The assertion is the *block offset in the log line*, because that is the -/// only place the reuse is visible from outside: both boots bind one disk, -/// both print `1 device(s)`, and both reach the shell. -/// -/// **The second half of the same finding is what happens when that disk is -/// pulled.** Keeping the block is right for as long as the device is on the -/// bus; `teardown_port` gave one back only for entries in the disk list, and a -/// refused disk is not in it. `MSC_BLOCKS` is 2, so one unsupported stick -/// plugged and pulled beside the boot stick left the pool with nothing for any -/// later disk, for the life of the boot. The actuator for that half is QEMU's -/// own `device_del`, and the verdict is that the disk plugged in afterwards -/// binds — at the block the refused one had. -pub fn usb_refused_disk_first( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// The disk that arrives after the refused one is pulled: 48 GiB, sparse, - /// and a size no other device in this suite reports. - const REPLACEMENT_BYTES: u64 = 48 * 1024 * 1024 * 1024; - - let (huge, _) = Profile::UsbDiskRefusedFirst.usb_disk().expect("the profile declares a disk"); - - // The claim is about which device QEMU creates first, and argv is the only - // place it is visible — a console line cannot distinguish "the refused disk - // was enumerated first" from "the driver happened to bind them in that - // order". - let options = BootOptions { - profile: Profile::UsbDiskRefusedFirst, - kernel_params: GATE, - qmp: true, - ..Default::default() - }; - let argv = qemu::profile_argv(&options); - let sticks: Vec<&String> = argv - .iter() - .filter(|a| a.starts_with("usb-storage,")) - .collect(); - match sticks.as_slice() { - [first, second] if first.contains("drive=usbdisk") && second.contains("drive=stick") => {} - other => { - return Err(format!( - "want the data disk created before the boot stick, got {other:?}" - )); - } - } - - let replacement = test_dir().join("usb-refused-replacement.img"); - drop(sparse(&replacement, REPLACEMENT_BYTES)); - - let mut qemu = QemuInstance::boot_with_options(test_config, c_bins, rust_bins, options); - let boot = qemu.boot_log().to_string(); - let mut log = boot.clone(); - - // Pull the disk the driver refused, and put a disk it can use where it was. - // Nothing else on this machine can free that pool block, so the bind below - // is the whole assertion. - let pulled = log.len(); - let mut devices = qemu::QmpDevices::open(qemu.qmp_socket()); - devices.del(&qemu::usb_device_id(0)); - drop(devices); - qemu::await_guest(&mut qemu, &mut log, "the refused disk's port to disconnect", |c| { - c[pulled..].contains(" disconnected") - })?; - let plugged = log.len(); - let mut devices = qemu::QmpDevices::open(qemu.qmp_socket()); - devices.blockdev_add("replacement", &replacement); - devices.add("usb-storage", "xhci.0", "replacement0", &[("drive", "replacement")]); - drop(devices); - qemu::await_guest(&mut qemu, &mut log, "the replacement disk to bind or be refused", |c| { - c[plugged..].contains("usb-storage: disk 1 ready on slot") || c[plugged..].contains("this driver serves 2") - })?; - - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - log.push_str(&qemu.drain_serial(Duration::from_secs(20))); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} during the USB gate boot\n{log}")); - } - } - - // The refusal happened, and it happened on slot 1 — the first device the - // controller enumerated. Without this the test would pass on a boot where - // the ordering silently went back to stick-first. - let sectors = huge / 512; - let refusal = format!( - "usb-storage: slot 1 has {sectors} sectors; this driver issues READ(10)" - ); - if !log.contains(&refusal) { - return Err(format!( - "the first disk enumerated was not the one the driver refuses ({refusal:?})\n{log}" - )); - } - - // And the boot stick behind it got the *second* pool block. `MSC_STRIDE` is - // 0x10000 and `msc_base` is where block 0 starts, so `+0x10000` is the - // block the refused disk's endpoint contexts still name and `+0x20000` is - // the next one. This is the whole finding: before the fix the line below - // reads `+0x10000`. - if !boot.contains("msc_block +0x20000") { - let got = boot - .lines() - .find(|l| l.contains("msc_block +")) - .unwrap_or(""); - return Err(format!( - "the disk after the refused one was given the refused one's pool block: {got:?}" - )); - } - if boot.matches("msc_block +").count() != 1 { - return Err(format!("want exactly one disk bound at boot\n{log}")); - } - - // Refused, not fatal: the stick still binds, still carries /boot, and the - // machine still comes up. A fix that leaked the whole pool would fail here. - if !boot.contains("usb-storage: 1 device(s)") { - return Err(format!("the boot stick did not bind behind the refused disk\n{log}")); - } - gate_ran(&boot, 1)?; - - // **Then the block came back.** The refused disk was unplugged, so its slot - // is disabled and the memory its endpoint contexts named is nobody's — and - // the disk plugged in afterwards binds, at that block. Before the fix this - // pool is out for the life of the boot: the line is the refusal below - // instead, and `MSC_BLOCKS` is 2, so it takes exactly one unsupported stick - // to cost a machine every disk it is given from then on. - if !log.contains("usb-storage: disk 1 ready on slot") { - return Err(format!( - "the disk plugged in after the refused one was pulled never bound; the pool block a \ - refused device holds is not given back when it leaves\n{log}" - )); - } - if !log.contains("msc_block +0x10000") { - let blocks: Vec<&str> = log.lines().filter(|l| l.contains("msc_block +")).collect(); - return Err(format!( - "the replacement disk did not take the refused disk's block: {blocks:?}\n{log}" - )); - } - if log.contains("this driver serves 2") { - return Err(format!( - "the pool refused a disk on a machine with two blocks and one disk on it\n{log}" - )); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - let _ = std::fs::remove_file(&replacement); - - eprintln!( - " [usb] a {huge} B disk refused on slot 1, enumerated first: the boot stick behind it \ - binds at msc_block +0x20000, not the refused disk's block — and once the refused disk \ - is unplugged a {REPLACEMENT_BYTES} B one binds at +0x10000, which is that block back" - ); - Ok(()) -} - -/// **The boot scan hands the next port a free operation slot, whatever its own -/// bound says.** -/// -/// T14 runs 103 and 104 panicked in `toyos_xhci::job::Outstanding::submit` — "a -/// second operation was submitted over an outstanding one" — with nothing wrong -/// but a stick whose first `READ CAPACITY(10)` went unanswered. The scan's -/// blocking bind spent the whole of the scan's silence bound inside -/// `advance_outstanding`; the refusal at the end of it submitted a Disable Slot -/// into the controller's one slot; and the scan, whose bound had expired two -/// seconds before that submit, returned with the slot still occupied. The next -/// port to connect then reached `device::begin`, which submits its Enable Slot -/// unasked — and the kernel died of what a device did. -/// -/// `usb-bind-spends-the-scan` stages that one thing: the boot's first bind -/// spends longer than the bound and is then refused. Everything else is -/// `UsbDiskRefusedFirst`'s own doing — QEMU hands out ports in device-creation -/// order, so a data disk created before the boot stick *is* "the port that -/// enumerates first", and the boot stick behind it is "another device arriving". -/// -/// The assertion is that the scan waited: it never says it heard nothing, and -/// the disk behind the refused one binds. With the fix reverted this boot -/// panics at the line above rather than failing an assertion. -pub fn xhci_scan_hands_over_a_free_slot( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // No gate: what this boot has to show is which ports were enumerated, and - // a sweep of the bytes on them would only make it slower. - const PARAMS: &[&str] = &["usb-bind-spends-the-scan"]; - /// What `msc::bind_spends_the_scan::WHY` prints. Two halves of one wire - /// format, as everything else in this file is: a rename shows up here as a - /// failed assertion and not as a test that quietly stopped staging anything. - const STAGED: &str = "answers nothing for the boot scan's whole bound \ - (usb-bind-spends-the-scan) and is then refused"; - - let options = BootOptions { - profile: Profile::UsbDiskRefusedFirst, - kernel_params: PARAMS, - ..Default::default() - }; - // The ordering is the injection, and argv is the only place it is visible. - let argv = qemu::profile_argv(&options); - let sticks: Vec<&String> = argv.iter().filter(|a| a.starts_with("usb-storage,")).collect(); - match sticks.as_slice() { - [first, second] if first.contains("drive=usbdisk") && second.contains("drive=stick") => {} - other => { - return Err(format!("want the data disk created before the boot stick, got {other:?}")) - } - } - - let log = boot_and_shutdown(test_config, c_bins, rust_bins, options)?; - - if !log.contains(STAGED) { - return Err(format!("the bind that spends the scan's bound never ran\n{log}")); - } - // The finding. The scan may only leave while its slot is free, so a bound - // that expired inside the bind is not a reason to leave at all: the Disable - // Slot the refusal submitted is waited for, on its own deadline. - if log.contains("the boot scan heard nothing") { - return Err(format!( - "the scan gave up with the refusal's Disable Slot still outstanding; the next port's \ - Enable Slot goes into that slot\n{log}" - )); - } - // It waited for the answer and then acted on it: the slot the refusal asked - // for goes back. A scan that abandoned the operation would leave this line - // out and the slot enabled for the life of the boot. - if !log.contains("xHCI: slot 1 disabled") { - return Err(format!("the refused disk's slot never came back\n{log}")); - } - // And the port behind the refused disk was enumerated rather than skipped: - // the boot stick is on it, so nothing else here would run if it were not. - if !log.contains("usb-storage: 1 device(s)") { - return Err(format!("the boot stick did not bind behind the refused disk\n{log}")); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - - eprintln!( - " [usb] a bind that spent the boot scan's whole bound and was then refused: the scan \ - waited out the Disable Slot it submitted, the boot stick behind it enumerated into a \ - free slot, and the machine came up" - ); - Ok(()) -} - diff --git a/tests/common/volumes.rs b/tests/common/volumes.rs index 023c97742a3..6e6dd5de689 100644 --- a/tests/common/volumes.rs +++ b/tests/common/volumes.rs @@ -1,119 +1,6 @@ -//! The boot stick's two partitions, served and written from inside ToyOS. -//! -//! The ESP holds what firmware and the bootloader read. The log partition -//! beside it holds the kernel's own log and exists for one reason: it is typed -//! so that a desktop OS mounts it on plug-in, which an EFI-typed partition is -//! not. Both are FAT32 and neither is found by being FAT32 — the loader names -//! both by unique GUID, fsd serves each off the partition that name finds, -//! and `log_partition_identity` is the gate that says so by moving the name -//! and watching `/log` go absent. -//! -//! Ground truth is the disk image the *device* received, read on the host by -//! implementations that are not fsd's: the `fatfs` crate and -//! `toyos-fat32-check`. The guest's account of a write it made is exactly what -//! is in question, so it cannot also be the evidence. -//! -//! **Where this stops.** `log_partition_layout` pins the image: type GUID, -//! attribute bits, labels, alignment, and that our own GPT parser finds the -//! partition the ESP names. It does not assert that any operating system -//! *mounts* it. Whether macOS attaches a Basic Data partition is -//! `diskarbitrationd`'s policy, not our contract — it moves between macOS -//! versions and host settings, it would put a volume on the owner's desktop -//! every test run, and it would race concurrent runs. That end of the contract -//! was verified once by hand, on 2026-08-02, and is re-verified when a stick is -//! flashed. -//! -//! The image is built and modified before the boot rather than after, because -//! the host-writes-guest-reads direction has no other staging point: a file the -//! guest itself created and read back would pass with the read path broken. - -use std::io::{Cursor, Read, Write}; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use toyos_build::bootlog; -use toyos_fat32_check::{check, describe}; +use std::io::{Cursor, Read}; use fatfs::FsOptions; -use gpt::disk::LogicalBlockSize; -use gpt::partition_types; - -use super::qemu::{self, BootOptions, QemuInstance}; -use super::serial; - -/// fsd's word for `/log` served off the log partition. -pub(crate) const LOG_SERVED: &str = "fsd: Log serving /log — FAT32,"; -/// fsd's word for `/boot` served off the running slot's volume. -pub(crate) const BOOT_SERVED: &str = "fsd: Boot serving /boot — FAT32 read-only,"; -/// fsd's word for a `/log` it has no volume behind. -pub(crate) const LOG_ABSENT: &str = "fsd: Log serving /log — absent:"; - -fn test_dir() -> PathBuf { - super::lane::dir() -} - -/// Where a partition sits inside a GPT disk image, in bytes, and the unique -/// GUID the table gives it. -/// -/// Selected by *type*, which is right in exactly one place and this is it: the -/// host has no handoff to be given, and it is the thing asking whether the -/// image builder produced the layout it claims. Exactly one partition of each -/// type, or this fails. -/// -/// The GUID is drawn fresh by `create_boot_image` for every image, so it is a -/// per-run nonce that the host knows before the machine starts and that only -/// this boot's kernel can have logged. `kernel_log_file` uses it to tell this -/// boot's log from a file left behind by anything else. -struct Extent { - start: usize, - len: usize, - guid: String, -} - -fn extent( - image: &[u8], - path: &Path, - kind: partition_types::Type, - what: &str, -) -> Result { - let disk = gpt::GptConfig::new() - .writable(false) - .logical_block_size(LogicalBlockSize::Lb512) - .open(path) - .map_err(|e| format!("the built image has no readable GPT: {e}"))?; - let found: Vec<_> = - disk.partitions().values().filter(|p| p.part_type_guid == kind).collect(); - let [part] = found.as_slice() else { - return Err(format!("the built image has {} of {what}, expected one", found.len())); - }; - let start = part.first_lba as usize * 512; - let len = (part.last_lba - part.first_lba + 1) as usize * 512; - if start + len > image.len() { - return Err(format!( - "the {what} runs to {} in an image of {}", - start + len, - image.len() - )); - } - Ok(Extent { start, len, guid: part.part_guid.to_string().to_uppercase() }) -} - -/// The ESP's byte range: what firmware and the bootloader read. -pub fn esp_extent(image: &[u8], path: &Path) -> Result<(usize, usize), String> { - let e = extent(image, path, partition_types::EFI, "ESP")?; - Ok((e.start, e.len)) -} - -/// The log partition's byte range: where `/log/kernel.log` lands. -pub fn log_extent(image: &[u8], path: &Path) -> Result<(usize, usize), String> { - let e = extent(image, path, partition_types::BASIC, "log partition")?; - Ok((e.start, e.len)) -} - -/// The unique partition GUID of a boot image's ESP, as the kernel prints it. -fn esp_guid(image: &[u8], path: &Path) -> Result { - Ok(extent(image, path, partition_types::EFI, "ESP")?.guid) -} /// Read several files out of a FAT volume in one mount. `None` is a file that /// is not there, which is an assertion in its own right here. @@ -138,1507 +25,3 @@ pub fn read_files(volume: &[u8], paths: &[&str]) -> Result>>, } Ok(out) } - -/// One file that must be there. -fn need(got: Option>, path: &str) -> Result, String> { - got.ok_or_else(|| format!("{path} is not on the volume")) -} - -/// One directory entry, as the host's own FAT implementation reads it. -#[derive(Debug, Clone)] -pub struct Entry { - pub name: String, - pub len: u64, - /// The entry's modification time in seconds from the Unix epoch, read out - /// of the directory entry rather than from anything the guest said about - /// it. FAT stores local time by specification, so this is in whatever zone - /// the machine that wrote it keeps. - pub modified: i64, -} - -/// Every file in the root of a FAT volume, sorted by name. -/// -/// The ground truth for what a guest put on a volume and what it took off one: -/// the guest's own account of its directory is exactly what is in question when -/// the claim is about retention. -pub fn root_entries(volume: &[u8]) -> Result, String> { - let fs = fatfs::FileSystem::new(Cursor::new(volume.to_vec()), FsOptions::new()) - .map_err(|e| format!("the volume does not mount on the host: {e}"))?; - let mut entries = Vec::new(); - for entry in fs.root_dir().iter() { - let entry = entry.map_err(|e| format!("reading the root directory: {e}"))?; - if entry.is_dir() { - continue; - } - let t = entry.modified(); - entries.push(Entry { - name: entry.file_name(), - len: entry.len(), - modified: unix_secs( - t.date.year as i64, - t.date.month as i64, - t.date.day as i64, - t.time.hour as i64, - t.time.min as i64, - t.time.sec as i64, - ), - }); - } - entries.sort_by(|a, b| a.name.cmp(&b.name)); - Ok(entries) -} - -/// Write files into the root of a FAT volume in place, before the machine that -/// will read them exists. -/// -/// The host-writes-guest-reads direction, which has no other staging point: a -/// file the guest created itself would prove nothing about a guest that deletes -/// the wrong one. -pub fn stage_files(volume: &mut [u8], files: &[(String, Vec)]) -> Result<(), String> { - let fs = fatfs::FileSystem::new(Cursor::new(volume), FsOptions::new()) - .map_err(|e| format!("the volume does not mount on the host: {e}"))?; - let root = fs.root_dir(); - for (name, bytes) in files { - let mut file = - root.create_file(name).map_err(|e| format!("creating {name} on the volume: {e}"))?; - file.write_all(bytes).map_err(|e| format!("writing {name}: {e}"))?; - } - Ok(()) -} - -/// Seconds from the Unix epoch, for comparing a directory entry against the -/// instant the host set the guest's clock to. Hinnant's algorithm. -fn unix_secs(year: i64, month: i64, day: i64, hour: i64, min: i64, sec: i64) -> i64 { - let y = if month <= 2 { year - 1 } else { year }; - let era = y.div_euclid(400); - let yoe = y - era * 400; - let mp = if month > 2 { month - 3 } else { month + 9 }; - let doy = (153 * mp + 2) / 5 + day - 1; - let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - let days = era * 146_097 + doe - 719_468; - days * 86_400 + hour * 3_600 + min * 60 + sec -} - -/// Everything the guest said about identifying and mounting its volumes. -/// -/// Wider than the mount's own lines on purpose. A mount that does not happen is -/// usually not the mount's fault: the two recorded instances were `gpt::probe` -/// reporting an entry-array CRC mismatch, which is a *read* off the stick -/// coming back wrong, and a failure message showing only the mount line said -/// nothing about that. -fn volume_lines(log: &str) -> String { - let lines: Vec<&str> = log - .lines() - .filter(|l| l.contains("fsd:") || l.contains("blockd:") || l.contains("logd:") || l.contains("gpt:") - || l.contains("shutdown") || l.contains("Shutting down") || l.contains("Syncing") - || l.contains("usb-storage:") || l.contains("partclaim:")) - .collect(); - if lines.is_empty() { - return format!("the guest said nothing about its volumes at all\n{log}"); - } - format!("what it said:\n{}", lines.join("\n")) -} - -/// The kernel's own log, written to the log partition of the stick it booted -/// from — **by `/system/bin/logd` since L6, and this gate is what says the hand-over -/// kept its promise**. -/// -/// The claim under test is *continuity*: not that a log file exists at the end, -/// but that the tail of what the kernel said is on the device while the machine -/// is still running — because the failure it is for is a machine that stops -/// without panicking, on a laptop with no serial port, where nothing else is -/// left. So the file is read **mid-run**, before any shutdown. -/// -/// **What it is evidence for changed with the writer.** It used to prove the -/// idle loop's sink; it now proves a userland process holding `logread` reads a -/// cursor, renders, writes, `fsync`s and keeps up — the whole of the log's -/// userland writer, observed from outside the machine. -/// The positive log-content assertion is this, and without it the headline -/// idle-loop I/O number is unfalsifiable: the cheapest way to make an -/// idle-loop I/O measurement look good is for the log to stop being written. -/// -/// Three things could make this green without logd working, and each has an -/// assertion aimed at it: -/// -/// - **A file left over from something else.** The log must carry this image's -/// own unique ESP GUID, which `create_boot_image` draws fresh per build and -/// no earlier run can have. -/// - **A single write when the file was opened.** logd creates its file early, -/// so a logd that then did nothing would still produce one. `Boot: complete` -/// is logged after that, so requiring it requires a write after the open. -/// - **The shutdown path standing in for the continuous one.** The mid-run read -/// happens before `run shutdown` and must already have `Boot: complete`; the -/// post-shutdown read must additionally have init's word that the machine -/// stops, which reaches the file only through the flush init has `logd` make -/// before it asks the kernel. -/// -/// A second boot, from `tests/logrotatecase`, drives the bound: rotation is -/// what stops the file filling the owner's stick, and at the shipped mebibyte -/// no test would ever reach it. -pub fn kernel_log_file( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let image_path = test_dir().join("kernel-log-boot.img"); - let image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let (start, len) = log_extent(&image, &image_path)?; - let guid = esp_guid(&image, &image_path)?; - // Born clean, and asserted so rather than assumed: `create_log_volume` - // formats an empty volume and records its free-cluster count, so unlike the - // ESP there is nothing here for the guest's own complaints to hide behind. - let complaints_before = check(&image[start..start + len]); - if !complaints_before.is_empty() { - return Err(format!( - "the log partition was not born clean, so this gate cannot tell a complaint the \ - guest caused from one it inherited:\n{}", - describe(&complaints_before) - )); - } - - // The line the kernel logs when firmware hands it the partition GUID. The - // host knows it before the machine starts; the guest can only have it from - // this boot. - let nonce = format!("gpt: firmware booted us from partition {guid} "); - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - boot_image: Some(qemu::Staged::Written(image_path.clone())), - ..Default::default() - }, - ); - let mut boot = qemu.boot_log().to_string(); - serial::Serial::named("boot console", boot.as_str()).must_be_clean()?; - // Waited for past the ready marker, on the guest's own liveness: logd - // names the file once the stick is up, and a stick slower than the runner - // puts its line after the marker. - let opened = "logd: this boot's kernel log is"; - qemu::await_marker(&mut qemu, &mut boot, opened, "logd naming this boot's file") - .map_err(|e| format!("logd never opened a file: {e}\n{}", volume_lines(&boot)))?; - - // Mid-run, with the guest still up and nothing shut down. Whatever is here - // was put there by `/system/bin/logd` while the machine was running. - // - // Polled until logd has written through `Boot: complete`, with the - // harness's ceiling and no deadline of this test's own: when logd writes - // is its own business, and one that never does is a hang. - let give_up = std::time::Instant::now() + qemu.budget(qemu::GUEST_WEDGED); - let mut running; - let mut running_text; - let mut running_name; - loop { - (running_name, running) = newest_log(&image_path, start, len)?; - running_text = String::from_utf8_lossy(&running).into_owned(); - if running_text.contains("Boot: complete") { - break; - } - if std::time::Instant::now() >= give_up { - return Err(format!( - "{} waiting for logd to write `Boot: complete` to the device: {} bytes there, \ - starting {:?}", - qemu::STALLED, - running.len(), - running_text.chars().take(120).collect::() - )); - } - std::thread::sleep(Duration::from_millis(50)); - } - if !running_text.contains(&nonce) { - return Err(format!( - "the log on the device does not carry this boot's partition GUID ({nonce:?}); it is \ - {} bytes and starts {:?}", - running.len(), - running_text.chars().take(120).collect::() - )); - } - if running_text.contains("Shutting down.") { - return Err("the guest shut down before the mid-run read".to_string()); - } - eprintln!( - " [log] {running_name}: {} bytes on the device, with the machine still running and \ - through `Boot: complete`", - running.len(), - ); - - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} on the way down\n{tail}")); - } - } - - let after = std::fs::read(&image_path).map_err(|e| format!("read the image back: {e}"))?; - let (final_name, final_log) = newest_log(&image_path, start, len)?; - if final_name != running_name { - return Err(format!( - "the shutdown moved this boot's log from {running_name} to {final_name}, which at the \ - shipped bound means it wrote a megabyte on the way down" - )); - } - let final_text = String::from_utf8_lossy(&final_log).into_owned(); - // **The stop is init's to sequence, and the file ends where init had - // `logd` make it whole**: init says it before it asks `logd`, and `logd` - // answers only once that line is durable. What the kernel says after — - // the stop's record and `Shutting down.` — is on the console and in the - // black box, and never waited for by anyone. - const FLUSHED: &str = toyos_logstream::STOPPING; - if !final_text.contains(FLUSHED) { - return Err(format!( - "the shutdown's flush never reached the file: {} bytes, ending {:?}", - final_log.len(), - final_text.lines().rev().take(3).collect::>().join(" | ") - )); - } - if !tail.contains("Shutting down.") { - return Err(format!("the console never carried the shutdown's last word\n{tail}")); - } - if final_log.len() <= running.len() { - return Err(format!( - "the file is {} bytes after the shutdown and was {} before it", - final_log.len(), - running.len() - )); - } - - let complaints_after = check(&after[start..start + len]); - if !complaints_after.is_empty() { - return Err(format!( - "writing the log gave the checker something to say about a volume it had nothing to \ - say about:\n{}", - describe(&complaints_after) - )); - } - eprintln!( - " [log] {final_name}: {} bytes after the shutdown, carrying init's flush; the checker \ - still silent", - final_log.len() - ); - let _ = std::fs::remove_file(&image_path); - - rotation(test_config, c_bins, rust_bins) -} - -/// The newest of the kernel's log files on the volume, with its name. -/// -/// `logd` names one file per boot for the wall clock and continues a long boot -/// in `_0002` and up, both of which sort after everything older — so the last -/// name is this boot's most recent file. Read off the device, like -/// everything else here. -pub fn newest_log(image_path: &Path, start: usize, len: usize) -> Result<(String, Vec), String> { - let image = std::fs::read(image_path).map_err(|e| format!("read the image: {e}"))?; - if start + len > image.len() { - return Err(format!("the image shrank to {} bytes", image.len())); - } - let volume = &image[start..start + len]; - let logs = log_names(volume)?; - let newest = logs.last().ok_or("the log volume holds no .log file at all")?; - let mut found = read_files(volume, &[newest.as_str()])?; - Ok((newest.clone(), need(found.pop().flatten(), newest)?)) -} - -/// The whole of this boot's log, oldest line first, across every file it was -/// written to. -/// -/// A boot long enough to rotate writes `.log`, then `_0002.log` and -/// up, and the names are chosen to sort in the order they were written — so the -/// boot's log is their concatenation. A reading that took only the newest would -/// call a rotation a hole. -pub fn whole_log(image_path: &Path, start: usize, len: usize) -> Result, String> { - let image = std::fs::read(image_path).map_err(|e| format!("read the image: {e}"))?; - if start + len > image.len() { - return Err(format!("the image shrank to {} bytes", image.len())); - } - let volume = &image[start..start + len]; - let mut names = log_names(volume)?; - names.sort(); - if names.is_empty() { - return Err("the log volume holds no .log file at all".to_string()); - } - let asked: Vec<&str> = names.iter().map(String::as_str).collect(); - let mut lines = Vec::new(); - for (name, found) in names.iter().zip(read_files(volume, &asked)?) { - let bytes = need(found, name)?; - lines.extend(String::from_utf8_lossy(&bytes).lines().map(|l| format!("{l}\n"))); - } - Ok(lines) -} - -/// The loader's own file on the volume, line by line. -pub fn loader_log_lines( - image_path: &Path, - start: usize, - len: usize, -) -> Result, String> { - let image = std::fs::read(image_path).map_err(|e| format!("read the image: {e}"))?; - let volume = - image.get(start..start + len).ok_or("the image shrank under the log partition")?; - let mut found = read_files(volume, &[bootlog::LOADER_LOG])?; - let bytes = need(found.pop().flatten(), bootlog::LOADER_LOG)?; - let text = String::from_utf8(bytes) - .map_err(|e| format!("{} is not UTF-8: {e}", bootlog::LOADER_LOG))?; - Ok(text.lines().map(str::to_string).collect()) -} - -/// Every file `logd` wrote, in the order their names sort. -fn log_names(volume: &[u8]) -> Result, String> { - Ok(root_entries(volume)? - .into_iter() - .filter(|e| bootlog::is_logd_file(&e.name)) - .map(|e| e.name) - .collect()) -} - -/// The bound, from `tests/logrotatecase`: `/system/bin/logd` rotating at 256 bytes -/// rather than a mebibyte, which one boot's own log crosses many times over, so -/// both the continuation path and the retention path run on the shipped code. -/// -/// **A config and no longer a kernel parameter.** The bound moved into a -/// userland program at L6, and the way a userland program is given a number is -/// its manifest row — so the arming is an image this repository builds rather -/// than a word on the kernel's command line. -fn rotation( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/logrotatecase"); - let image_path = test_dir().join("kernel-log-rotate.img"); - let image = qemu::build_boot_image(&config, c_bins, rust_bins, &[]); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let (start, len) = log_extent(&image, &image_path)?; - - let mut qemu = QemuInstance::boot_with_options( - &config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - boot_image: Some(qemu::Staged::Written(image_path.clone())), - ..Default::default() - }, - ); - let mut log = qemu.boot_log().to_string(); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - log.push_str(&qemu.drain_serial(Duration::from_secs(20))); - drop(qemu); - - // At least twice, not at least once. One continuation proves only that the - // bound is noticed; the second is the one that runs with an earlier part of - // the same boot already on the volume, which is what the name has to stay - // clear of. - let continuations = log.matches("and this boot continues in").count(); - if continuations < 2 { - return Err(format!( - "the log continued into a new file {continuations} times, wanted at least two:\n{}", - volume_lines(&log) - )); - } - - let image = std::fs::read(&image_path).map_err(|e| format!("read the image back: {e}"))?; - let entries = root_entries(&image[start..start + len])?; - let logs: Vec<&Entry> = entries.iter().filter(|e| bootlog::is_logd_file(&e.name)).collect(); - // A part is a flush batch that crossed the bound rather than 256 bytes of - // log — the sink drains everything pending before it looks at the size — - // so a metal-sim boot makes a handful, measured at four. That is under the - // retention bound, which is why this only requires the count to stay inside - // it. - if logs.len() < 2 || logs.len() > super::wallclock::MAX_LOG_FILES { - return Err(format!( - "the volume holds {} log files, wanted 2..={}: {}", - logs.len(), - super::wallclock::MAX_LOG_FILES, - logs.iter().map(|e| e.name.as_str()).collect::>().join(", ") - )); - } - // Every part but the newest is one that *filled*, which is the only reason - // a newer one exists. A part under the bound means something else started a - // file. - for entry in &logs[..logs.len() - 1] { - if entry.len < 256 { - return Err(format!( - "{} is {} bytes and is not the newest part, so it did not fill before the next \ - one started", - entry.name, entry.len - )); - } - } - // **The claim is that the shutdown's flush reached the volume**, so the - // search is every part of this boot and not a guess at which one it landed - // in: at a 256-byte bound a round is a part, and whatever the machine says - // while init waits on `logd` pushes the line a part back. The image is built - // fresh for this arm, so every `.log` here is this boot's. - const FLUSHED: &str = toyos_logstream::STOPPING; - let names: Vec<&str> = logs.iter().map(|e| e.name.as_str()).collect(); - let tail_at = read_files(&image[start..start + len], &names)? - .into_iter() - .enumerate() - .find(|(_, bytes)| { - bytes.as_ref().is_some_and(|b| String::from_utf8_lossy(b).contains(FLUSHED)) - }) - .map(|(i, _)| i); - let Some(tail_at) = tail_at else { - let newest = read_files(&image[start..start + len], &names[names.len() - 1..])? - .pop() - .flatten() - .unwrap_or_default(); - let newest = String::from_utf8_lossy(&newest).into_owned(); - return Err(format!( - "the shutdown's flush is in none of the {} parts on the volume ({}).\nthe newest part \ - ends:\n{}\nwhat the guest said:\n{}", - logs.len(), - names.join(", "), - newest.lines().rev().take(4).collect::>().join("\n"), - volume_lines(&log) - )); - }; - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [log] continued {continuations} times at the 256-byte bound, leaving {} parts at the \ - {}-file bound, newest {}; the shutdown's flush is in part {} of {}", - logs.len(), - super::wallclock::MAX_LOG_FILES, - logs.last().map_or("none", |e| e.name.as_str()), - tail_at + 1, - logs.len() - ); - Ok(()) -} - -/// F5's negative control: under `usb-flush-fails` a second `fsync` must refuse -/// like the first, because the mount's device commit is still owed — the -/// pre-generation kernel answered the second call with success and issued -/// nothing. The guest asserts both refusals; the host half proves the staging -/// fired at the shipped site (the driver's own sense line, at least once). -pub fn fsync_failed_commit( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["usb-flush-fails"]; - /// `msc.rs::log_refusal` for SYNCHRONIZE CACHE(10) with the staged sense. - const REFUSED: &str = "usb-storage: SCSI 0x35 failed, sense 0x04/0x44/0x00"; - - let image_path = test_dir().join("fsync-failed-commit.img"); - let image = qemu::build_boot_image(test_config, c_bins, rust_bins, PARAMS); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - boot_image: Some(qemu::Staged::Written(image_path.clone())), - kernel_params: PARAMS, - ..Default::default() - }, - ); - let mut log = qemu.boot_log().to_string(); - if !log.contains(LOG_SERVED) { - return Err(format!( - "the log partition did not mount, so nothing below asks the device to flush:\n{}", - volume_lines(&log) - )); - } - - let result = qemu.run_test("test_rs_fsync_flush_failed", Duration::from_secs(30)); - log.push_str(&result.before); - log.push_str(&result.stdout); - log.push_str(&result.serial); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} under a failing device flush\n{log}")); - } - } - if result.exit_code != Some(0) { - return Err(format!( - "fsync_flush_failed guest failed — an fsync answered success over a device that \ - refused its cache flush:\n{}\nkernel log while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - let refusals = log.matches(REFUSED).count(); - if refusals == 0 { - return Err(format!( - "no {REFUSED:?} line — the staged flush failure never reached the driver, so the \ - guest's two refusals prove nothing\n{log}" - )); - } - - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [f5] {refusals} refused device flush(es); both of the guest's fsyncs were refused" - ); - Ok(()) -} - - - -/// The boot disk arrives *after* the port scan, and both mounts still happen. -/// -/// The machine the T14 was on the boot it lost `/boot` and `/log`, and the one -/// `xhci_slow_connect` cannot be: that gate hides the whole bus, which is the -/// case `xhci::EMPTY_BUS_NS` already keeps looking through. Here the bus is -/// populated and only the disk is late — five HID devices settle, -/// `await_connect_settle` ends on *them* because its own condition is a connect -/// set that has held still and is non-empty, and `scan_ports` runs with no disk -/// on it. Everything downstream then behaves exactly as it did on the laptop: -/// the machine boots, userland comes up, and there is no `/log` to write to. -/// -/// Three things have to hold together, and the first is what stops the other two -/// being vacuous: -/// -/// - the boot scan really did finish with **no** disk (`usb-storage: 0 -/// device(s)`) while really having found the HIDs, so the interleaving under -/// test is the one that happened rather than an ordinary boot; -/// - both volumes mount anyway; -/// - and `kernel.log` is on the device afterwards carrying *this* boot's -/// partition GUID, read off the image on the host rather than out of the -/// guest's own account of itself. -pub fn late_storage_connect( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["xhci-slow-storage-connect"]; - let image_path = test_dir().join("late-connect-boot.img"); - let image = qemu::build_boot_image(test_config, c_bins, rust_bins, PARAMS); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let (start, len) = log_extent(&image, &image_path)?; - let guid = esp_guid(&image, &image_path)?; - let nonce = format!("gpt: firmware booted us from partition {guid} "); - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - // The one profile with the boot stick on port register 1 *and* other - // USB devices behind it. A profile with an empty bus would settle on - // `EMPTY_BUS_NS` and never reach this interleaving. - profile: qemu::Profile::MetalUsb, - boot_image: Some(qemu::Staged::Written(image_path.clone())), - kernel_params: PARAMS, - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - serial::Serial::named("boot console", boot.as_str()).must_be_clean()?; - - if !boot.contains("usb-storage: 0 device(s)") { - return Err(format!( - "the boot scan bound a disk, so the port was not held empty and this gate is \ - measuring an ordinary boot\n{}", - volume_lines(&boot) - )); - } - // The other half of non-vacuity: a bus that is empty as well as diskless is - // the machine `xhci_slow_connect` already covers, and it takes a different - // path out of the settle. - if boot.contains("xHCI: 1 controller(s), 0 HID device(s)") { - return Err(format!( - "the whole bus read empty, not just the disk's port — this is \ - xhci_slow_connect's machine and the settle leaves it by the other door\n{}", - volume_lines(&boot) - )); - } - - // logd opens its file once its file server answers, which need not be - // before the runner's ready line: read on until it says so. - const LOG_OPENED: &str = "logd: this boot's kernel log is"; - let mut boot = boot; - if !boot.contains(LOG_OPENED) { - boot.push_str(&qemu.drain_until(Duration::from_secs(20), |l| l.contains(LOG_OPENED))); - } - for want in [BOOT_SERVED, LOG_SERVED, LOG_OPENED] { - if !boot.contains(want) { - return Err(format!( - "the disk arrived after the port scan and {want:?} never happened — the probe \ - stopped looking while the machine still had no boot volume\n{}", - volume_lines(&boot) - )); - } - } - - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} on the way down\n{tail}")); - } - } - - // Ground truth is the device, not the line the guest printed about it. - let log = newest_log(&image_path, start, len)?.1; - let text = String::from_utf8_lossy(&log).into_owned(); - if !text.contains(&nonce) { - return Err(format!( - "the log on the device does not carry this boot's partition GUID ({nonce:?}); it is \ - {} bytes", - log.len() - )); - } - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [log] the disk was invisible to the port scan and both volumes mounted anyway; \ - {} bytes of kernel.log on the device", - log.len() - ); - Ok(()) -} - -/// One file read out of a partition inside a disk image on the host. -pub fn log_on_device( - image_path: &Path, - start: usize, - len: usize, - name: &str, -) -> Result, String> { - let image = std::fs::read(image_path).map_err(|e| format!("read the image: {e}"))?; - if start + len > image.len() { - return Err(format!("the image shrank to {} bytes", image.len())); - } - let mut found = read_files(&image[start..start + len], &[name])?; - need(found.pop().flatten(), name) -} - -/// The image side of the whole exercise, with nothing mounted and nothing -/// booted: the log partition is what a desktop OS will pick up on plug-in. -/// -/// Every claim here is about bytes this build produced, which is the boundary -/// the suite tests to. What another operating system then *does* with those -/// bytes is that OS's policy and is deliberately not asserted anywhere — see -/// this module's header. -/// -/// The type GUID is written out in full rather than compared against -/// `partition_types::BASIC`, because the image builder used that same constant -/// and a comparison against it would agree with any value it held. -pub fn log_partition_layout( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Microsoft Basic Data. Every desktop OS treats a partition of this type - /// as one of its own to mount; an EFI-typed one macOS will not touch, which - /// is why the log moved off the ESP. - const BASIC_DATA: &str = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7"; - const ESP_TYPE: &str = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"; - /// TOYOS-ROOT, the value the kernel selects its candidates on. - const ROOT_TYPE: &str = "B350BC93-BB6A-4C5E-9589-A5C3CFD555FD"; - - let image_path = test_dir().join("log-layout.img"); - let image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - - let disk = gpt::GptConfig::new() - .writable(false) - .logical_block_size(LogicalBlockSize::Lb512) - .open(&image_path) - .map_err(|e| format!("the built image has no readable GPT: {e}"))?; - let table: Vec<_> = disk.partitions().values().collect(); - // In entry order: the log is third, where the metal loop reads it, and a - // test image carries one slot. - let [esp, slots, log, volume, root] = table.as_slice() else { - return Err(format!( - "the built image has {} partitions, wanted five: the ESP, the slot table, the log, and \ - slot A's volume and ROOT", - table.len() - )); - }; - - let types = [ - (esp.part_type_guid.guid.to_uppercase(), ESP_TYPE, "ESP"), - (log.part_type_guid.guid.to_uppercase(), BASIC_DATA, "log partition"), - ]; - for (got, want, what) in types { - if got != want { - return Err(format!("the {what} is typed {got}, wanted {want}")); - } - } - // ROOT's, the slot table's and the slot volume's types are read with the - // kernel's parser: the `gpt` crate answers the all-zero GUID for a type its - // own table does not name. - for (kind, text, what, entry) in [ - (toyos_gpt::Guid::TOYOS_ROOT, ROOT_TYPE, "ROOT", root), - (toyos_gpt::Guid::TOYOS_SLOTS, toyos_gpt::Guid::TOYOS_SLOTS_TEXT, "the slot table", slots), - (toyos_gpt::Guid::TOYOS_BOOT, toyos_gpt::Guid::TOYOS_BOOT_TEXT, "slot A's volume", volume), - ] { - let found = toyos_build::image::only_partition(&mut ImageSectors { bytes: &image }, kind) - .map_err(|why| format!("the kernel's own GPT parser, on the partitions typed {text}: {why}"))?; - if found.first_lba() != entry.first_lba || found.last_lba() != entry.last_lba { - return Err(format!( - "the kernel's parser puts {what} at LBA {}..{} and the table says {}..{}", - found.first_lba(), - found.last_lba(), - entry.first_lba, - entry.last_lba - )); - } - } - - // The attribute field, spelled out. Bit 0 marks a partition the firmware - // requires and bit 62 marks one hidden from mounting, and either would - // undo the type: an installer that set them would leave a partition that - // parses correctly and never appears. - if log.flags != 0 { - return Err(format!( - "the log partition carries attributes {:#018x}; bit 0 (required) is {}, bit 62 \ - (hidden) is {} — both stop a host mounting it and neither is ever wanted here", - log.flags, - log.flags & 1, - (log.flags >> 62) & 1 - )); - } - - let log_guid = log.part_guid; - let guids = [esp.part_guid, slots.part_guid, log_guid, volume.part_guid, root.part_guid]; - if guids.iter().any(uuid::Uuid::is_nil) { - return Err("a partition was given the all-zero GUID, which GPT reads as unused".to_string()); - } - for (i, one) in guids.iter().enumerate() { - if guids[i + 1..].contains(one) { - return Err(format!("two partitions carry the unique GUID {one}")); - } - } - - // The alignment `create_gpt_disk` asserts, checked again from the table: - // the kernel mounts several over one 4 KiB block device and caches device - // blocks per volume, so a block belonging to two would be held twice and go - // stale on the other's write. - let extent = |p: &gpt::partition::Partition| (p.first_lba * 512, (p.last_lba + 1) * 512); - let placed = [ - ("ESP", extent(esp)), - ("slot table", extent(slots)), - ("log partition", extent(log)), - ("slot A's volume", extent(volume)), - ("root partition", extent(root)), - ]; - for (what, (start, end)) in placed { - if start % 4096 != 0 || end % 4096 != 0 { - return Err(format!( - "the {what} spans bytes {start}..{end}, which is not whole 4 KiB device blocks" - )); - } - } - for (before, after) in placed.iter().zip(&placed[1..]) { - if before.1 .1 > after.1 .0 { - return Err(format!( - "the {} runs to {} and the {} starts at {}", - before.0, before.1 .1, after.0, after.1 .0 - )); - } - } - - // What the bootloader will read, and the kernel will be given. The file and - // the entry are the same sixteen bytes in the same order or the handoff is - // pointing at nothing. - let (start, len) = esp_extent(&image, &image_path)?; - let named = log_on_device(&image_path, start, len, "toyos/log.guid")?; - let named: [u8; 16] = named - .as_slice() - .try_into() - .map_err(|_| format!("toyos/log.guid is {} bytes, wanted 16", named.len()))?; - if named != log_guid.to_bytes_le() { - return Err(format!( - "toyos/log.guid holds {named:02x?}, and the log partition's entry holds {:02x?}", - log_guid.to_bytes_le() - )); - } - - // And the parser that will actually do this on the machine agrees, run - // here over the same bytes: `toyos_gpt::locate` is the kernel's, and it is - // given the GUID exactly as the file carries it. - let located = toyos_gpt::locate(&mut ImageSectors { bytes: &image }, toyos_gpt::Guid(named)) - .map_err(|e| format!("the kernel's own GPT parser cannot find the log partition: {e:?}"))?; - let found = located.partition(); - if found.first_lba() != log.first_lba || found.last_lba() != log.last_lba { - return Err(format!( - "the kernel's parser puts the log partition at LBA {}..{} and the table says {}..{}", - found.first_lba(), - found.last_lba(), - log.first_lba, - log.last_lba - )); - } - - // Both places a FAT label lives. The boot-sector field is what a mount - // reads without walking the root directory; the `VOLUME_ID` entry is what a - // tool that walks it reads. Written by one call, checked as two, because a - // volume with one of them is a volume called `NO NAME` somewhere. - let (log_start, log_len) = log_extent(&image, &image_path)?; - for (what, at, size, label) in [ - ("ESP", start, len, "TOYOS-BOOT"), - ("log partition", log_start, log_len, "TOYOS-LOG"), - ] { - let fs = fatfs::FileSystem::new(Cursor::new(image[at..at + size].to_vec()), FsOptions::new()) - .map_err(|e| format!("the built {what} does not mount on the host: {e}"))?; - if fs.volume_label() != label { - return Err(format!( - "the {what}'s boot sector calls it {:?}, wanted {label:?}", - fs.volume_label() - )); - } - let root = fs - .read_volume_label_from_root_dir() - .map_err(|e| format!("reading the {what}'s root-directory label: {e}"))?; - if root.as_deref() != Some(label) { - return Err(format!( - "the {what}'s root directory carries the label {root:?}, wanted {label:?}" - )); - } - } - // Born clean. The ESP is not and cannot be until `fatfs` is forked; this - // volume has no subdirectory for a fatfs defect to arise in, and its - // free-cluster count is recorded at format time. - let complaints = check(&image[log_start..log_start + log_len]); - if !complaints.is_empty() { - return Err(format!("the log partition is not born clean:\n{}", describe(&complaints))); - } - - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [log] {BASIC_DATA} with attributes 0, labelled TOYOS-LOG in both places, 4 KiB-aligned \ - and disjoint from the ESP, format-clean, and named by toyos/log.guid" - ); - Ok(()) -} - -/// A disk image in 512-byte LBAs, for the kernel's own GPT parser. -pub struct ImageSectors<'a> { - pub bytes: &'a [u8], -} - -impl toyos_gpt::Sectors for ImageSectors<'_> { - fn lba_bytes(&self) -> u32 { - 512 - } - - fn lba_count(&self) -> u64 { - (self.bytes.len() / 512) as u64 - } - - fn lba_count_granularity(&self) -> core::num::NonZeroU64 { - core::num::NonZeroU64::MIN - } - - fn read_lba(&mut self, lba: u64, out: &mut [u8]) -> bool { - let at = lba as usize * 512; - match self.bytes.get(at..at + out.len()) { - Some(src) => { - out.copy_from_slice(src); - true - } - None => false, - } - } -} - -/// A GUID no table this build produces can contain: `create_boot_image` draws -/// v4 UUIDs, whose version nibble is 4 and whose variant bits are `10`. Written -/// in GPT entry byte order, which is the order everything from the file to the -/// comparison uses. -const FORGED: [u8; 16] = [ - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, -]; -/// As `Guid`'s Display prints it: three little-endian fields then raw bytes. -const FORGED_TEXT: &str = "33221100-5544-7766-8899-AABBCCDDEEFF"; - -/// An image on disk and the `(offset, len)` of its ESP and its log partition, -/// in that order. -pub type ImageWithExtents = (PathBuf, (usize, usize), (usize, usize)); - -/// A stick as the build made it, with one file changed: the sixteen bytes of -/// `\toyos\log.guid` now name a partition no machine has. -/// -/// Everything about the log partition stays as it was — still second in the -/// table, still typed Microsoft Basic Data, still the only other FAT32 on the -/// stick, still exactly where it was — so a kernel that found the volume by -/// type, by format or by position would mount it anyway and every gate built on -/// this would go green on the defect it exists for. -/// -/// Returns the image's path and its two partition extents. -pub fn image_with_unnamed_log_partition( - name: &str, - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result { - let image_path = test_dir().join(name); - let mut image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let esp = esp_extent(&image, &image_path)?; - let log = log_extent(&image, &image_path)?; - - { - let volume = &mut image[esp.0..esp.0 + esp.1]; - let fs = fatfs::FileSystem::new(Cursor::new(&mut *volume), FsOptions::new()) - .map_err(|e| format!("the built ESP does not mount on the host: {e}"))?; - let dir = fs - .root_dir() - .open_dir("toyos") - .map_err(|e| format!("the built ESP has no toyos directory: {e}"))?; - let mut file = dir - .create_file("log.guid") - .map_err(|e| format!("opening log.guid on the ESP: {e}"))?; - file.truncate().map_err(|e| format!("truncating log.guid: {e}"))?; - file.write_all(&FORGED).map_err(|e| format!("writing log.guid: {e}"))?; - } - std::fs::write(&image_path, &image).map_err(|e| format!("rewrite the boot image: {e}"))?; - Ok((image_path, esp, log)) -} - -/// What the kernel says on the panel when this boot leaves nothing to read -/// afterwards. It is an `alert!`, so the panel paints the row red off the -/// record's `Level` — nothing in the text says so — and `screen_log_absent` is -/// the gate that it does. -pub const NO_LOG_ALERT: &str = "log: no /log"; - -/// The log partition is named, never discovered — proved by moving the name. -/// -/// The refusal has three halves and each is separately checkable: -/// -/// - it is **named**: the `gpt:` line says which GUID it could not find, which -/// is what a person holding the stick needs; -/// - it costs **nothing else**: `/boot` still mounts and the boot still -/// completes, because a missing diagnostic is not worth a machine; -/// - and it is not a **fallback**: the log partition is read back on the host -/// afterwards and must still be empty. Falling back to the ESP would also -/// leave it empty, so `logd` must not have opened a file either. -pub fn log_partition_identity( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (image_path, _, (log_start, log_len)) = image_with_unnamed_log_partition( - "log-identity-boot.img", - test_config, - c_bins, - rust_bins, - )?; - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - boot_image: Some(qemu::Staged::Written(image_path.clone())), - ..Default::default() - }, - ); - let mut log = qemu.boot_log().to_string(); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - log.push_str(&qemu.drain_serial(Duration::from_secs(20))); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} on a stick whose log partition is not named\n{log}")); - } - } - - // The bootloader read the forged file and handed it on unconverted. A - // mixed-endian slip anywhere on that path shows up here as a different - // GUID rather than as a mysteriously absent partition. - let named = format!("gpt: the boot volume names {FORGED_TEXT} as the log partition"); - if !log.contains(&named) { - return Err(format!( - "the handoff did not carry the bytes the ESP holds.\nwanted: {named}\n{}", - volume_lines(&log) - )); - } - let refused = format!("{LOG_ABSENT} the Log partition {FORGED_TEXT} is on no disk this server reaches"); - if !log.contains(&refused) { - return Err(format!( - "fsd did not serve /log absent for the named partition.\nwanted: {refused}\n{}", - volume_lines(&log) - )); - } - if log.contains("logd: this boot's kernel log is") { - return Err(format!( - "logd opened a file with no log partition — a fallback is exactly what this must not \ - do:\n{}", - volume_lines(&log) - )); - } - if !log.contains("logd: no /log on this machine") { - return Err(format!( - "logd said nothing about a machine with no /log, so its no-/log path is missing:\n{}", - volume_lines(&log) - )); - } - - // And nothing else was lost. The stick is a working stick with one file - // changed on it. - if !log.contains(BOOT_SERVED) { - return Err(format!("a missing log partition cost the machine /boot:\n{}", volume_lines(&log))); - } - if !log.contains("Boot: complete") { - return Err(format!("a missing log partition cost the boot:\n{log}")); - } - - // Ground truth: the partition itself. It is still there, still FAT32, and - // the kernel wrote nothing to it. - let after = std::fs::read(&image_path).map_err(|e| format!("read the image back: {e}"))?; - let volume = &after[log_start..log_start + log_len]; - // Any log at all, rather than two names: the kernel picks this boot's from - // the wall clock, so what has to be absent is the whole family. - let found = log_names(volume)?; - if !found.is_empty() { - return Err(format!( - "the kernel wrote to a partition it had just refused to identify: {}", - found.join(", ") - )); - } - let complaints = check(volume); - if !complaints.is_empty() { - return Err(format!("the untouched log partition is not clean:\n{}", describe(&complaints))); - } - - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [log] the name moved and the mount went with it: refused {FORGED_TEXT} by name, /boot \ - and the boot unaffected, nothing written to the partition" - ); - Ok(()) -} - -/// Where ROOT is on `image`, found by the parser the kernel uses. -fn root_extent(image: &[u8]) -> Result<(usize, usize), String> { - let root = toyos_build::image::only_partition(&mut ImageSectors { bytes: image }, toyos_gpt::Guid::TOYOS_ROOT) - .map_err(|why| format!("this image's ROOT: {why}"))?; - Ok((root.first_lba() as usize * 512, root.lba_count().get() as usize * 512)) -} - -/// A second USB disk carrying a copy of `image`, `mutate`d after the copy. -/// -/// A copy rather than a constructed table: the twin's ROOT is then a filesystem -/// this kernel really can mount and really does name the same thing, which is -/// what the duplicate case needs and what no hand-written GPT would give. -fn root_twin( - path: &Path, - image: &[u8], - bytes: u64, - mutate: impl FnOnce(&mut Vec) -> Result<(), String>, -) -> Result<(), String> { - let mut copy = image.to_vec(); - mutate(&mut copy)?; - let file = std::fs::File::create(path).map_err(|e| format!("create the twin disk: {e}"))?; - file.set_len(bytes).map_err(|e| format!("size the twin disk: {e}"))?; - let mut file = std::fs::OpenOptions::new() - .write(true) - .open(path) - .map_err(|e| format!("open the twin disk: {e}"))?; - file.write_all(©).map_err(|e| format!("write the twin disk: {e}"))?; - Ok(()) -} - -/// **A ROOT whose bytes are not the ones its slot's signed header names is -/// refused by name, and never handed to the kernel.** Disk contents crossed a -/// trust boundary: the boot disk's own ROOT has both superblocks — block 0 and -/// the backup at the volume's last block — inverted, so the image's one slot -/// is bad and the machine has nothing else to boot. -pub fn root_candidate_malformed( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - let (at, len) = root_extent(&image)?; - for block in [at, at + len - 4096] { - for byte in &mut image[block..block + 4096] { - *byte = !*byte; - } - } - let path = test_dir().join("root-malformed.img"); - std::fs::write(&path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let log = boot_expecting_root_refusal(test_config, c_bins, rust_bins, &path)?; - let _ = std::fs::remove_file(&path); - - let verdict = root_refusal(&log)?; - if !verdict.contains("its root is not the bytes its signed header names") { - return Err(format!("the loader did not refuse a ROOT its signature does not cover: {verdict}")); - } - eprintln!(" [root] {verdict}"); - Ok(()) -} - -/// **A boot parameter naming a ROOT its slot does not carry is a boot the -/// loader refuses before the kernel reads it**: the parameter is one of the -/// slot's signed sections. One hex digit of `root=` on the slot's volume is -/// flipped, which is a byte-for-byte edit inside a file of the same length — -/// so the FAT volume is untouched and only the name changes. -pub fn root_named_but_absent( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - let path = test_dir().join("root-absent.img"); - std::fs::write(&path, &image).map_err(|e| format!("write the boot image: {e}"))?; - // `root=` alone appears several times on the slot's volume, because - // `kernel.elf` carries the token as a literal. The search is for the whole - // argument, whose sixteen bytes are read out of ROOT's own superblock. - let (root_at, _) = root_extent(&image)?; - let named: String = - image[root_at + 106..root_at + 122].iter().map(|b| format!("{b:02x}")).collect(); - let want = format!("root={named}"); - let (slot_at, slot_len) = { - let mut file = std::fs::File::open(&path).map_err(|e| format!("{}: {e}", path.display()))?; - let table = toyos_build::image::slot_table_of(&mut file)?; - let slot = table.slot(table.marked).ok_or("the table marks no slot it carries")?; - toyos_build::image::partition_extent(&mut file, slot.boot)? - }; - let (slot_at, slot_len) = (slot_at as usize, slot_len as usize); - let volume = &image[slot_at..slot_at + slot_len]; - let hits: Vec = (0..volume.len().saturating_sub(want.len())) - .filter(|&i| &volume[i..i + want.len()] == want.as_bytes()) - .collect(); - let [at] = hits[..] else { - return Err(format!("the slot's volume carries {} {want:?} tokens, wanted one", hits.len())); - }; - // The last digit, so the flip cannot collide with the first: two names that - // differ in one place are still two names. - let digit = slot_at + at + want.len() - 1; - let was = image[digit]; - image[digit] = if was == b'0' { b'1' } else { b'0' }; - - std::fs::write(&path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let log = boot_expecting_root_refusal(test_config, c_bins, rust_bins, &path)?; - let _ = std::fs::remove_file(&path); - - let verdict = root_refusal(&log)?; - if !verdict.contains("its cmdline is not the bytes its signed header names") { - return Err(format!("the loader did not refuse a parameter its signature does not cover: {verdict}")); - } - eprintln!(" [root] {verdict}"); - Ok(()) -} - -/// **A second disk answering to the same name is not the boot's business.** A -/// second stick carries an untouched copy of the boot image, so the machine -/// has two slot tables and two ROOTs whose superblocks carry one UUID. The -/// loader reads the slot table on the disk it was loaded from and on no other: -/// it reads one ROOT, and the kernel mounts that one from memory. -pub fn root_named_twice( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let (bytes, _) = qemu::Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - let twin = test_dir().join("root-twice-twin.img"); - root_twin(&twin, &image, bytes, |_| Ok(()))?; - - let qemu = qemu::QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::UsbDisk, - usb_images: vec![twin.clone()], - ..Default::default() - }, - ); - let log = qemu.boot_log().to_string(); - drop(qemu); - let _ = std::fs::remove_file(&twin); - - let named: Vec<&str> = log - .lines() - .filter(|l| l.contains(READ_AT)) - .map(str::trim) - .collect(); - if named.len() != 1 { - return Err(format!( - "the loader read {} ROOTs and the boot disk's slot names one:\n{}", - named.len(), - volume_lines(&log) - )); - } - let mounted = log - .lines() - .find(|l| l.contains("root: mounted read-only from memory at")) - .ok_or_else(|| format!("ROOT did not mount from memory:\n{}", volume_lines(&log)))? - .trim() - .to_string(); - eprintln!(" [root] {}", named[0]); - eprintln!(" [root] {mounted}"); - Ok(()) -} - -/// **A chunk of ROOT the disk will not read refuses the boot, naming that -/// chunk.** The boot disk fails with EIO every read covering the sector -/// seven past ROOT's middle, so the chunk that fails is not the first. The -/// loader reads ROOT in chunks, so the refusal names a chunk that holds the -/// sector and starts where the bytes read before it end. -pub fn root_chunk_refused( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - let (at, len) = root_extent(&image)?; - let first = (at / 512) as u64; - let bad = first + (len / 512 / 2) as u64 + 7; - let path = test_dir().join("root-chunk-refused.img"); - std::fs::write(&path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let qemu = qemu::QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::InternalDisk, - boot_image: Some(qemu::Staged::Written(path.clone())), - boot_read_error: Some(bad), - ready_marker: CHUNK_REFUSED, - ..Default::default() - }, - ); - let log = format!("{}{}", qemu.boot_log(), qemu.uart_log()); - drop(qemu); - let _ = std::fs::remove_file(&path); - - let verdict = log - .lines() - .find(|l| l.contains(CHUNK_REFUSED)) - .map(str::trim) - .ok_or_else(|| format!("the loader did not refuse the unreadable chunk:\n{}", volume_lines(&log)))?; - let number = |after: &str| -> Result { - let rest = verdict.split(after).nth(1).ok_or_else(|| format!("{verdict:?} has no {after:?}"))?; - rest.split(|c: char| !c.is_ascii_digit()) - .next() - .and_then(|n| n.parse().ok()) - .ok_or_else(|| format!("{verdict:?} has no number after {after:?}")) - }; - let blocks = number("the read of ")?; - let lba = number(" blocks at LBA ")?; - let read = number(", after ")?; - if !(lba <= bad && bad < lba + blocks) { - return Err(format!("the refusal names LBA {lba}+{blocks}, which does not hold the bad sector {bad}: {verdict}")); - } - if (lba - first) * 512 != read || read == 0 || blocks * 512 >= len as u64 { - return Err(format!( - "ROOT at LBA {first}+{} was not read in chunks up to the bad one: {verdict}", - len / 512 - )); - } - if !verdict.contains("DEVICE_ERROR") { - return Err(format!("the refusal does not carry the firmware's status: {verdict}")); - } - eprintln!(" [root] bad sector {bad}: {verdict}"); - Ok(()) -} - -/// **A ROOT its own table refuses is a boot the loader refuses, naming why.** -/// The partition before ROOT on the boot disk has its last LBA moved eight -/// blocks inside ROOT's start, both copies of the table rewritten and their -/// checksums recomputed, so the table is well-formed and ROOT overlaps its -/// neighbour: `toyos_gpt::locate` refuses it before a byte of the slot is -/// read, and a loader that read it without asking would hand the kernel -/// blocks another partition also claims. -pub fn root_candidate_overlaps( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let mut image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - let (at, len) = root_extent(&image)?; - let root_first = (at / GPT_LBA) as u64; - let _ = len; - let size = image.len(); - rewrite_gpt(&mut image, size, |entries, entry_bytes| { - let before = entries - .chunks(entry_bytes) - .enumerate() - .filter(|(_, entry)| entry[..16] != [0; 16] && entry_lba(entry, 40) < root_first) - .max_by_key(|(_, entry)| entry_lba(entry, 40)) - .map(|(index, _)| index) - .ok_or("no partition comes before ROOT on the boot disk")?; - entries[before * entry_bytes + 40..][..8].copy_from_slice(&(root_first + 7).to_le_bytes()); - Ok(()) - })?; - let path = test_dir().join("root-overlaps.img"); - std::fs::write(&path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let log = boot_expecting_root_refusal(test_config, c_bins, rust_bins, &path)?; - let _ = std::fs::remove_file(&path); - - let verdict = log - .lines() - .find(|l| l.contains("Slot A: partition ")) - .map(str::trim) - .ok_or_else(|| format!("the loader did not refuse an overlapping ROOT:\n{}", volume_lines(&log)))?; - if !verdict.contains("PartitionOverlap") { - return Err(format!("the refusal does not name the overlap: {verdict}")); - } - eprintln!(" [root] {verdict}"); - Ok(()) -} - -/// **A second filesystem answering to ROOT's name on the boot disk is not a -/// candidate at all**: the slot table names ROOT by its partition's unique -/// GUID, and the loader reads that partition and no other. The boot disk grows -/// by a second TOYOS-ROOT partition holding a byte-for-byte copy of ROOT under -/// its own unique GUID, so both carry the name `root=` gives, and the boot -/// reads the one the table names. -pub fn root_named_twice_on_the_boot_disk( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const MIB: usize = 1 << 20; - let mut image = qemu::build_boot_image(test_config, c_bins, rust_bins, &[]); - let (at, len) = root_extent(&image)?; - let root = image[at..at + len].to_vec(); - let twin_at = image.len().div_ceil(MIB) * MIB; - rewrite_gpt(&mut image, twin_at + len + MIB, |entries, entry_bytes| { - let root_entry = entries - .chunks(entry_bytes) - .find(|entry| entry_lba(entry, 32) == (at / GPT_LBA) as u64 && entry[..16] != [0; 16]) - .ok_or("no entry holds ROOT")? - .to_vec(); - let free = entries - .chunks_mut(entry_bytes) - .find(|entry| entry[..16] == [0; 16]) - .ok_or("the table has no free entry")?; - free.copy_from_slice(&root_entry); - free[16] ^= 0xff; - free[32..40].copy_from_slice(&((twin_at / GPT_LBA) as u64).to_le_bytes()); - free[40..48].copy_from_slice(&(((twin_at + len) / GPT_LBA - 1) as u64).to_le_bytes()); - Ok(()) - })?; - image[twin_at..twin_at + len].copy_from_slice(&root); - let path = test_dir().join("root-twice-one-disk.img"); - std::fs::write(&path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let qemu = qemu::QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { boot_image: Some(qemu::Staged::Written(path.clone())), ..Default::default() }, - ); - let log = qemu.uart_log(); - drop(qemu); - let _ = std::fs::remove_file(&path); - - let read: Vec<&str> = log.lines().filter(|l| l.contains(READ_AT)).map(str::trim).collect(); - let from_the_table = format!("from LBA {}+", at / GPT_LBA); - match read[..] { - [one] if one.contains(&from_the_table) => eprintln!(" [root] {one}"), - _ => return Err(format!("the loader read {read:?}, where one read {from_the_table} is owed")), - } - Ok(()) -} - -/// The logical block every table `build_boot_image` writes is laid out in. -const GPT_LBA: usize = 512; - -/// The LBA at byte `at` of a partition entry: 32 is its first, 40 its last. -fn entry_lba(entry: &[u8], at: usize) -> u64 { - u64::from_le_bytes(entry[at..at + 8].try_into().expect("eight bytes")) -} - -/// Rewrite `image`'s GPT with its entry array `edit`ed, the disk resized to -/// `len` bytes, and everything UEFI 2.11 §5.3 derives from those recomputed: -/// the backup array and header moved to the new end, the primary's pointers to -/// them and its last usable LBA, both arrays' and both headers' CRCs, and the -/// protective MBR's size. `edit` gets the array and one entry's length. -pub(super) fn rewrite_gpt( - image: &mut Vec, - len: usize, - edit: impl FnOnce(&mut [u8], usize) -> Result<(), String>, -) -> Result<(), String> { - let word = |bytes: &[u8], at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().expect("four bytes")); - let mut primary = image[GPT_LBA..2 * GPT_LBA].to_vec(); - if &primary[..8] != b"EFI PART" { - return Err("the boot image has no GPT header at LBA 1".to_string()); - } - let header_bytes = word(&primary, 12) as usize; - let old_backup = entry_lba(&primary, 32) as usize; - let array_at = entry_lba(&primary, 72) as usize * GPT_LBA; - let entry_bytes = word(&primary, 84) as usize; - let array_bytes = word(&primary, 80) as usize * entry_bytes; - let old_backup_array = entry_lba(&image[old_backup * GPT_LBA..], 72) as usize; - - let mut array = image[array_at..array_at + array_bytes].to_vec(); - edit(&mut array, entry_bytes)?; - image[old_backup_array * GPT_LBA..(old_backup + 1) * GPT_LBA].fill(0); - image.resize(len, 0); - - let last = len / GPT_LBA - 1; - let backup_array = last - array_bytes.div_ceil(GPT_LBA); - let seal = |header: &mut Vec| { - header[16..20].fill(0); - let crc = toyos_gpt::crc32(&header[..header_bytes]); - header[16..20].copy_from_slice(&crc.to_le_bytes()); - }; - primary[32..40].copy_from_slice(&(last as u64).to_le_bytes()); - primary[48..56].copy_from_slice(&(backup_array as u64 - 1).to_le_bytes()); - primary[88..92].copy_from_slice(&toyos_gpt::crc32(&array).to_le_bytes()); - seal(&mut primary); - let mut backup = primary.clone(); - backup[24..32].copy_from_slice(&(last as u64).to_le_bytes()); - backup[32..40].copy_from_slice(&1u64.to_le_bytes()); - backup[72..80].copy_from_slice(&(backup_array as u64).to_le_bytes()); - seal(&mut backup); - - image[GPT_LBA..2 * GPT_LBA].copy_from_slice(&primary); - image[array_at..array_at + array_bytes].copy_from_slice(&array); - image[backup_array * GPT_LBA..][..array_bytes].copy_from_slice(&array); - image[last * GPT_LBA..][..GPT_LBA].copy_from_slice(&backup); - let mbr_size = u32::try_from(last).unwrap_or(u32::MAX); - image[446 + 12..446 + 16].copy_from_slice(&mbr_size.to_le_bytes()); - Ok(()) -} - -/// The loader's refusal of the image's one slot: the line a boot whose ROOT -/// is refused says, and so the marker the boot is waited on. -const ROOT_REFUSED: &str = "Slot A: REFUSED, "; - -/// The loader's line for the one ROOT it read into memory. -const READ_AT: &str = "ROOT: read into memory at"; - -/// The loader's line for a chunk of the slot's ROOT the disk would not read. -const CHUNK_REFUSED: &str = "Slot A: ROOT: the read of "; - -/// Boot an image whose ROOT the loader is expected to refuse, and hand back the -/// log, which ends at the refusal. -fn boot_expecting_root_refusal( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - image: &Path, -) -> Result { - let qemu = qemu::QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - boot_image: Some(qemu::Staged::Written(image.to_path_buf())), - ready_marker: ROOT_REFUSED, - ..Default::default() - }, - ); - // Both channels: the loader and an early kernel speak on the 16550, and the - // harness stops at the marker on whichever carried it. - Ok(format!("{}{}", qemu.boot_log(), qemu.uart_log())) -} - -/// The loader's refusal line, or what the boot said instead. -fn root_refusal(log: &str) -> Result { - log.lines() - .find(|l| l.contains(ROOT_REFUSED)) - .map(|l| l.trim().to_string()) - .ok_or_else(|| format!("the loader did not refuse this ROOT set:\n{}", volume_lines(log))) -} diff --git a/tests/common/wallclock.rs b/tests/common/wallclock.rs deleted file mode 100644 index 4ef66c03271..00000000000 --- a/tests/common/wallclock.rs +++ /dev/null @@ -1,596 +0,0 @@ -//! What the machine thinks the time is, and what it does when it cannot tell. -//! -//! The wall clock is one of the few devices the *host* can set, which is what -//! makes this checkable from outside the guest at all: `-rtc base=` puts a -//! known instant in the emulated CMOS before the machine starts, so the name -//! and the timestamp of the file the guest writes are both known before there -//! is a guest. The volume's verdicts are read off the disk image the device -//! received; the clock *syscalls* reach no disk, so what the guest printed for -//! those is judged against that same staged instant and nothing it derived. -//! -//! # What only an actuator can stage -//! -//! Four states of the clock, and the host can produce none of them: QEMU has no -//! switch that removes or wedges the mc146818, its RTC always presents the -//! guest a coherent register set, the FADT a guest reads is generated by QEMU -//! and always names the century register at 0x32, and `-rtc base=` sets every -//! digit of the date **except** the century. That last one is measured rather -//! than assumed: a guest booted with `base=2101-06-05` reads century 20 and -//! year 01 out of its own registers and correctly reports 2001, because QEMU -//! maintains the clock registers and leaves CMOS 0x32 at whatever firmware last -//! wrote there. So a staged year cannot distinguish a kernel that reads the -//! century register from one that assumes 2000 — [`no_century`] and -//! [`century_from_the_register`] are the two halves that can, and each is a -//! `#[cfg(feature)]` changing what the *hardware* answers, leaving the decoder -//! and everything downstream of it shipped code. - -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use super::fwvars; -use super::qemu::{self, BootOptions, QemuInstance}; -use super::serial; -use super::volumes::{self, Entry}; - -/// What the host sets the emulated RTC to, and the same instant in seconds. -/// -/// A date this century, so the machine is self-consistent: QEMU sets the clock -/// registers and leaves the century register alone at the 20 firmware wrote, and -/// those two agree only for the 2000s. Well inside FAT's range, whose last -/// representable day is 2107-12-31, because the file this boot writes has to -/// carry the instant as its own timestamp and a clamp would hide a wrong one. -const RTC_BASE: &str = "2033-03-07T09:14:25"; -const RTC_BASE_SECS: i64 = 1_993_799_665; -/// What a file named for that instant begins with. The date and not the time, -/// because the seconds move on while the machine boots and [`after_the_base`] -/// is what bounds that — the timestamp inside the entry is where this is -/// checked to the second. -const RTC_BASE_DATE: &str = "2033-03-07-"; - -/// Whether `secs` past [`RTC_BASE`] is a time this guest's clock can have read: -/// not before the instant the host staged, and not after the RTC — which runs -/// from that instant at the host's own pace from the moment QEMU starts — had -/// got to by the time the boot was read back, `lived` after the launch. Both -/// ends are causality and neither is a margin: a slower host only widens the -/// second, and a kernel that got the century or a zone wrong is out by years or -/// hours. -fn after_the_base(secs: i64, lived: Duration) -> bool { - (0..=lived.as_secs_f64().ceil() as i64).contains(&secs) -} - -/// What [`boot_and_read`] makes the guest print into the window between the -/// ready marker and the first test it runs. -/// -/// Distinctive enough that nothing else on a console could be it, and short -/// enough to be one `write`. -const WINDOW_MARKER: &str = "between-tests-window-is-captured"; - -/// How many logs `/system/bin/logd`'s `MAX_LOG_FILES` keeps. Mirrored rather than shared, -/// so moving the kernel's bound without looking at this fails here rather than -/// quietly weakening the gate. -pub const MAX_LOG_FILES: usize = 16; - -/// The log files this module's kernel wrote or was given, oldest first. -fn logs(entries: &[Entry]) -> Vec<&Entry> { - entries.iter().filter(|e| toyos_build::bootlog::is_logd_file(&e.name)).collect() -} - -fn names(entries: &[&Entry]) -> String { - entries.iter().map(|e| e.name.as_str()).collect::>().join(", ") -} - -/// What `wall_clock_now` printed for `SYS_CLOCK_EPOCH`. -fn probed_epoch(log: &str) -> Option { - let line = log.lines().find(|l| l.contains("wall-clock: epoch="))?; - let rest = line.split("epoch=").nth(1)?; - rest.split_whitespace().next()?.parse().ok() -} - -fn clock_lines(log: &str) -> String { - let lines: Vec<&str> = log - .lines() - .filter(|l| { - l.contains("clock:") || l.contains("logd:") || l.contains("RTC") || l.contains("rtc") - }) - .collect(); - if lines.is_empty() { - return format!("the guest said nothing about its clock at all\n{log}"); - } - format!("what it said:\n{}", lines.join("\n")) -} - -/// A boot with the wall clock staged, returning the log volume afterwards. -/// -/// Metal-sim, because that is the machine shape that gets flashed and the one -/// whose whole reason for having a log on a stick is that it has no serial -/// port. The guest is shut down before the volume is read: the claims here are -/// about the *name* and the timestamp of a file, both of which are decided when -/// the sink installs, so unlike `kernel_log_file` there is nothing to catch -/// mid-run. -fn boot_and_read( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - image_name: &str, - params: &'static [&'static str], - stage: &[(String, Vec)], - firmware_vars: Option, -) -> Result<(Vec, String, Duration), String> { - let image_path = super::lane::dir().join(image_name); - let mut image = qemu::build_boot_image(test_config, c_bins, rust_bins, params); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let (start, len) = volumes::log_extent(&image, &image_path)?; - - if !stage.is_empty() { - volumes::stage_files(&mut image[start..start + len], stage)?; - std::fs::write(&image_path, &image).map_err(|e| format!("rewrite the boot image: {e}"))?; - } - - // Before the launch, so the RTC the guest reads has run no longer than this. - let launched = std::time::Instant::now(); - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::Metal, - boot_image: Some(qemu::Staged::Written(image_path.clone())), - kernel_params: params, - rtc_base: Some(RTC_BASE), - firmware_vars, - ..Default::default() - }, - ); - let mut log = qemu.boot_log().to_string(); - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - // **A line printed into the window between the ready marker and the first - // test, staged so that losing it is a red rather than a flake.** - // - // `logd` writes its retention line, creates this boot's file — a device - // write, milliseconds — and then writes its identity line; the window used - // to close between the two and the first line was dropped by the harness - // with nothing saying so. `run echo` is the smallest thing that lands in - // that window on purpose: the guest's runner reads commands in order, so - // this whole exchange strictly precedes the probe's `===TEST_START===` and - // every line of it arrives while the next `run_test` is waiting for its own - // marker. `TestResult::before` is what keeps it. - writeln!(qemu.stdin_mut(), "run echo {WINDOW_MARKER}") - .map_err(|e| format!("stage the between-tests window: {e}"))?; - qemu.flush_stdin(); - let probe = qemu.run_test("test_rs_wall_clock_now", Duration::from_secs(30)); - log.push_str(&probe.before); - log.push_str(&probe.stdout); - if !log.contains(WINDOW_MARKER) { - return Err(format!( - "the guest printed {WINDOW_MARKER} between the ready marker and the probe's start and \ - this capture does not carry it — the window between two tests is being dropped, which \ - is how a daemon's startup line goes missing from a boot nothing else is wrong \ - with\nbefore:\n{}\nstdout:\n{}", - probe.before, probe.stdout - )); - } - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - log.push_str(&qemu.drain_serial(Duration::from_secs(20))); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if log.contains(bad) { - return Err(format!("{bad:?} on the way down\n{log}")); - } - } - - let after = std::fs::read(&image_path).map_err(|e| format!("read the image back: {e}"))?; - if start + len > after.len() { - return Err(format!("the image shrank to {} bytes", after.len())); - } - let entries = volumes::root_entries(&after[start..start + len])?; - let _ = std::fs::remove_file(&image_path); - Ok((entries, log, launched.elapsed())) -} - -/// `PcatRealTimeClockRuntimeDxe`'s `FILE_GUID`, `378D7B65-8DA9-4773-B6E4-A47826A833E1`, -/// in the byte order `EFI_GUID` stores: the vendor of the `RTC` variable its -/// `PcRtcInit` reads `EFI_TIME::TimeZone` out of (edk2 -/// `PcAtChipsetPkg/PcatRealTimeClockRuntimeDxe/PcRtc.c`). -const PC_RTC_VENDOR: [u8; 16] = - [0x65, 0x7b, 0x8d, 0x37, 0xa9, 0x8d, 0x73, 0x47, 0xb6, 0xe4, 0xa4, 0x78, 0x26, 0xa8, 0x33, 0xe1]; -/// `EFI_VARIABLE_NON_VOLATILE | BOOTSERVICE_ACCESS | RUNTIME_ACCESS`, what -/// `PcRtcSetTime` stores the zone with. -const PC_RTC_ATTRIBUTES: u32 = 0x7; -/// UTC+2 in `EFI_TIME::TimeZone`, whose relation is `Localtime = UTC - TimeZone`. -const FIRMWARE_ZONE_MINUTES: i16 = -120; - -/// How far `h:m:s` is past the staged instant's own time of day; the base is -/// far enough from midnight that no boot crosses one. -fn past_the_base(h: &str, m: &str, s: &str) -> Option { - let [h, m, s] = [h, m, s].map(|field| field.parse::().ok()); - Some(h? * 3_600 + m? * 60 + s? - RTC_BASE_SECS.rem_euclid(86_400)) -} - -/// What `wall_clock_now` printed for `SYS_CLOCK_REALTIME`, past the base. -fn probed_realtime(log: &str) -> Option { - let line = log.lines().find(|l| l.contains("wall-clock: epoch="))?; - let hms = line.split("realtime=").nth(1)?.split_whitespace().next()?; - let [h, m, s] = hms.split(':').collect::>()[..] else { return None }; - past_the_base(h, m, s) -} - -pub fn rtc_is_utc( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let vars = super::lane::dir().join("wall-clock-utc-vars.fd"); - toyos_build::firmware::of(qemu::Profile::Metal.arch())?.fresh_vars(&vars)?; - let zone = u32::from(FIRMWARE_ZONE_MINUTES as u16).to_le_bytes(); - fwvars::plant(&vars, &PC_RTC_VENDOR, "RTC", PC_RTC_ATTRIBUTES, &zone)?; - let booted = - boot_and_read(test_config, c_bins, rust_bins, "wall-clock-utc.img", &[], &[], Some(vars.clone())); - let _ = std::fs::remove_file(&vars); - let (entries, log, lived) = booted?; - let logs = logs(&entries); - - let [only] = logs.as_slice() else { - return Err(format!("the volume holds {} logs, wanted one: {}", logs.len(), names(&logs))); - }; - let named = only.name.strip_prefix(RTC_BASE_DATE).and_then(|hms| hms.strip_suffix(".log")).and_then(|hms| { - let (h, ms) = hms.split_at_checked(2)?; - let (m, s) = ms.split_at_checked(2)?; - past_the_base(h, m, s) - }); - if !named.is_some_and(|drift| after_the_base(drift, lived)) { - return Err(format!( - "the log is {} and the host staged {RTC_BASE}\n{}", - only.name, - clock_lines(&log) - )); - } - let stamp_drift = only.modified - RTC_BASE_SECS; - if !after_the_base(stamp_drift, lived) { - return Err(format!( - "this boot's FAT timestamp is {stamp_drift}s from the staged instant\n{}", - clock_lines(&log) - )); - } - - let Some(epoch) = probed_epoch(&log) else { - return Err(format!( - "the guest never printed what `SYS_CLOCK_EPOCH` answered\n{}", - clock_lines(&log) - )); - }; - let drift = epoch - RTC_BASE_SECS; - if !after_the_base(drift, lived) { - return Err(format!( - "`SYS_CLOCK_EPOCH` answered {epoch}, {drift}s from the staged instant\n{}", - clock_lines(&log) - )); - } - let Some(realtime_drift) = probed_realtime(&log) else { - return Err(format!( - "the guest never printed what `SYS_CLOCK_REALTIME` answered\n{}", - clock_lines(&log) - )); - }; - if !after_the_base(realtime_drift, lived) { - return Err(format!( - "`SYS_CLOCK_REALTIME` answered a time of day {realtime_drift}s from the staged \ - instant's\n{}", - clock_lines(&log) - )); - } - eprintln!( - " [clock] with firmware naming {FIRMWARE_ZONE_MINUTES} minutes, {}, its FAT stamp, epoch \ - {epoch} and the time of day sit on the staged instant", - only.name - ); - Ok(()) -} - -/// A machine whose clock will not answer still boots, still logs, and says so -/// in the name of the file it writes. -pub fn undated( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - image_name: &str, - params: &'static [&'static str], - because: &str, -) -> Result<(), String> { - let (entries, log, _) = - boot_and_read(test_config, c_bins, rust_bins, image_name, params, &[], None)?; - let logs = logs(&entries); - - // The refusal, by name and with its reason. A kernel that silently took - // some other number for the time would produce a dated file and no line. - if !log.contains("clock: this machine will not say what time it is") || !log.contains(because) { - return Err(format!( - "with {params:?} the kernel never refused the clock for {because:?}\n{}", - clock_lines(&log) - )); - } - let [only] = logs.as_slice() else { - return Err(format!("the volume holds {} logs, wanted one: {}", logs.len(), names(&logs))); - }; - if only.name != "unknown-00.log" { - return Err(format!( - "with {params:?} this boot's log is {}, which claims a time the machine never gave \ - it\n{}", - only.name, - clock_lines(&log) - )); - } - // The boot has to have *finished* — a clock that refuses must not cost the - // machine anything else — and the file has to carry it. - if !log.contains("Boot: complete") { - return Err(format!("with {params:?} the boot never completed\n{log}")); - } - // Userland is told the same thing the kernel knows. A syscall answering - // 1970 here is the defect this whole shape exists to make impossible: the - // caller cannot tell it from a machine that really is at the epoch. - if !log.contains("wall-clock: no epoch") { - return Err(format!( - "with {params:?} the clock syscalls did not refuse a process the way the kernel \ - refused itself\n{}", - clock_lines(&log) - )); - } - if only.len == 0 { - return Err(format!("with {params:?} {} is on the volume and empty", only.name)); - } - eprintln!( - " [clock] {params:?}: refused by name, {} carries {} bytes, boot complete", - only.name, only.len - ); - Ok(()) -} - -/// A FADT that names no century register: the machine still has a clock, and -/// the year comes from two digits and the stated assumption. -/// -/// The old code could not express this state at all — it read CMOS 0x32 -/// whatever the FADT said and took anything non-zero as a century — so the -/// assertion is that the *table's* answer is what decides, and that answering -/// "none" costs the machine its century rather than its clock. -pub fn no_century( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // Both, and the second is what gives this teeth. With the register left as - // the machine has it, honouring the FADT's "none" and ignoring it produce - // the same year — 2000 plus two digits, and a register holding 20 — so a - // kernel that read a hardcoded 0x32 would pass. Staging the register at the - // *next* century separates them: honouring the table gives 2033 and - // ignoring it gives 2133. - const PARAMS: &[&str] = &["rtc-no-century", "rtc-century-next"]; - let (entries, log, _) = - boot_and_read(test_config, c_bins, rust_bins, "wall-clock-no-century.img", PARAMS, &[], None)?; - let logs = logs(&entries); - - if !log.contains("ACPI: the FADT names no RTC century register") { - return Err(format!( - "the kernel never said the FADT named no century register\n{}", - clock_lines(&log) - )); - } - let [only] = logs.as_slice() else { - return Err(format!("the volume holds {} logs, wanted one: {}", logs.len(), names(&logs))); - }; - if !only.name.starts_with(RTC_BASE_DATE) { - return Err(format!( - "with no century register this boot's log is {}, and two digits plus 2000 is \ - {RTC_BASE_DATE} — a name in 2133 means the register was read anyway\n{}", - only.name, - clock_lines(&log) - )); - } - eprintln!( - " [clock] no century register: {}, from two digits and 2000, with the register itself \ - staged a century away", - only.name - ); - Ok(()) -} - -/// The century register's *contents* are what widen the year. -/// -/// The one thing `-rtc base=` cannot stage, so the register answers 0x21 and -/// nothing else changes: same clock registers, same FADT, same decoder. A -/// kernel that ignored the register — or read a fixed 2000 — puts this boot in -/// 2033 like every other one here, and the file name is where that shows. -pub fn century_from_the_register( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["rtc-century-next"]; - let (entries, log, _) = - boot_and_read(test_config, c_bins, rust_bins, "wall-clock-century.img", PARAMS, &[], None)?; - let logs = logs(&entries); - - let [only] = logs.as_slice() else { - return Err(format!("the volume holds {} logs, wanted one: {}", logs.len(), names(&logs))); - }; - // 2133 and not 2033: the same two year digits under the next century. The - // day and time are the host's, so only the century moved. - if !only.name.starts_with("2133-03-07-") { - return Err(format!( - "with the century register answering 0x21 this boot's log is {}, and the year the \ - registers describe is 2133-03-07\n{}", - only.name, - clock_lines(&log) - )); - } - eprintln!(" [clock] century register 0x21: {}, a century past the staged clock", only.name); - Ok(()) -} - -/// Where [`file_mtime_survives_a_reboot`] writes, on DATA: the one volume a -/// file outlives its boot on. -const MTIME_PATH: &str = "/home/file-mtime.bin"; - -/// The second boot's RTC, a day past [`RTC_BASE`]: a stamp taken again at the -/// mount or the open would carry this day, so an unchanged one was carried. -const RTC_NEXT_DAY: &str = "2033-03-08T09:14:25"; - -/// What `file_mtime` printed for [`MTIME_PATH`], in nanoseconds. -fn printed_mtime(result: &qemu::TestResult) -> Result { - let head = format!("file-mtime: {MTIME_PATH} mtime="); - result - .stdout - .lines() - .find_map(|l| l.trim().strip_prefix(head.as_str())) - .and_then(|n| n.parse().ok()) - .ok_or_else(|| { - format!( - "`{}` printed no {head:?} line (exit {:?})\n{}{}{}", - result.name, result.exit_code, result.before, result.stdout, result.serial - ) - }) -} - -/// One boot of the image `data` carries DATA on, with the RTC at `rtc_base`, -/// running `file_mtime MTIME_PATH`, then shut down. -fn mtime_boot( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - data: &Path, - rtc_base: &'static str, - mode: &str, -) -> Result<(u64, Duration), String> { - // Before the launch, so the RTC the guest reads has run no longer than this. - let launched = std::time::Instant::now(); - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - nvme_image: Some(data.to_path_buf()), - rtc_base: Some(rtc_base), - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - if boot.contains(super::storage::IN_MEMORY) { - return Err(format!("/home fell back to memory, so no file of it outlives the boot:\n{boot}")); - } - let result = qemu.run_test(&format!("test_rs_file_mtime {mode} {MTIME_PATH}"), Duration::from_secs(60)); - let printed = printed_mtime(&result); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} on the way down\n{tail}")); - } - } - if result.exit_code != Some(0) { - return Err(format!( - "`file_mtime {mode}` failed:\n{}\nkernel log while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - printed.map(|n| (n, launched.elapsed())) -} - -/// A file's mtime is the wall clock at its write, and a reboot carries it -/// unchanged. -/// -/// The oracle is the instant the host staged with `-rtc base=`: the guest's -/// stamp for a file on DATA lies within [`after_the_base`] of it, the -/// DATA volume read off the image by the host's own `bcachefs` reader holds -/// that same stamp, and a second boot with the clock a day on reads it back -/// unchanged. A stamp since boot is decades short of the instant. -pub fn file_mtime_survives_a_reboot( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const NANOS_PER_SEC: u64 = 1_000_000_000; - - let data = super::lane::dir().join("file-mtime-data.img"); - toyos_build::build::create_sparse(&data, qemu::NVME_SMALL); - - let (written, lived) = mtime_boot(test_config, c_bins, rust_bins, &data, RTC_BASE, "write")?; - let drift = (written / NANOS_PER_SEC) as i64 - RTC_BASE_SECS; - if !after_the_base(drift, lived) { - return Err(format!( - "{MTIME_PATH} is stamped {written} ns, {drift} s from the {RTC_BASE} the host set the \ - RTC to" - )); - } - - let io = super::storage::FileBlocks::open(&data)?; - let fs = bcachefs::Mounted::<_, bcachefs::ReadOnly>::open(io) - .map_err(|e| format!("the DATA volume does not mount on the host: {e:?}"))?; - let on_device = fs - .file_mtime(MTIME_PATH.trim_start_matches('/')) - .map_err(|e| format!("reading {MTIME_PATH}'s mtime off the image: {e:?}"))?; - drop(fs); - if on_device != Some(written) { - return Err(format!( - "the guest read {written} ns for {MTIME_PATH} and the device holds {on_device:?}" - )); - } - - let (read, _) = mtime_boot(test_config, c_bins, rust_bins, &data, RTC_NEXT_DAY, "read")?; - if read != written { - return Err(format!( - "{MTIME_PATH} was stamped {written} ns and reads {read} ns after a reboot with the RTC \ - at {RTC_NEXT_DAY}" - )); - } - let _ = std::fs::remove_file(&data); - eprintln!( - " [clock] {MTIME_PATH} stamped {drift} s past the staged RTC, the same {written} ns on \ - the device and after a reboot a day on" - ); - Ok(()) -} - -/// On a machine whose RTC never answered, a file's mtime on `/tmp` or on fsd's -/// `/home` is undated — 0, which std reports as an error — and never 1970 plus -/// the boot's uptime. -pub fn file_mtime_undated( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const SAID: [&str; 2] = - ["file-mtime: /tmp/file-mtime-undated is undated", "file-mtime: /home/file-mtime-undated is undated"]; - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { kernel_params: &["rtc-dead"], ..Default::default() }, - ); - let boot = qemu.boot_log().to_string(); - if !boot.contains("clock: this machine will not say what time it is") { - return Err(format!( - "with rtc-dead armed the kernel never refused the clock\n{}", - clock_lines(&boot) - )); - } - let result = qemu.run_test("test_rs_file_mtime undated", Duration::from_secs(60)); - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let tail = qemu.drain_serial(Duration::from_secs(20)); - drop(qemu); - for bad in ["PANIC:", "panicked at"] { - if tail.contains(bad) { - return Err(format!("{bad:?} on the way down\n{tail}")); - } - } - if result.exit_code != Some(0) || !SAID.iter().all(|said| result.stdout.contains(said)) { - return Err(format!( - "`file_mtime undated` exited {:?}:\n{}\nkernel log while it ran:\n{}{}", - result.exit_code, result.stdout, result.before, result.serial - )); - } - eprintln!(" [clock] rtc-dead: a file written in /tmp or /home is undated"); - Ok(()) -} diff --git a/tests/cxx/runtime.cpp b/tests/cxx/runtime.cpp deleted file mode 100644 index 52177afc575..00000000000 --- a/tests/cxx/runtime.cpp +++ /dev/null @@ -1,374 +0,0 @@ -// What `cxx_runtime` compiles with the toolchain's clang and runs on ToyOS: -// libc++'s containers, strings and streams, exceptions through frames with -// destructors, threads with their thread_local and static destructors, and -// libc's threads, keys, mutexes, condition variables and exit handlers. -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace { - -std::atomic thread_locals_destroyed{0}; -int exit_handlers_ran = 0; - -struct Farewell { - ~Farewell() { - std::printf("static destructor ran after %d exit handlers and %d thread_local destructors\n", - exit_handlers_ran, thread_locals_destroyed.load()); - } -} farewell; - -struct PerThread { - int id = 0; - ~PerThread() { thread_locals_destroyed.fetch_add(1); } -}; - -thread_local PerThread per_thread; - -struct Custom : std::runtime_error { - using std::runtime_error::runtime_error; -}; - -int thrower(int depth) { - std::vector held{"destroyed", "while", "unwinding"}; - if (depth == 0) - throw Custom("thrown from depth 0"); - return thrower(depth - 1) + static_cast(held.size()); -} - -std::mutex hand_m; -std::condition_variable hand_cv; -bool handed = false; -bool released = false; -pthread_t handed_over; - -void* handed_main(void*) { - std::unique_lock lock(hand_m); - handed_over = pthread_self(); - handed = true; - hand_cv.notify_all(); - hand_cv.wait(lock, [] { return released; }); - return reinterpret_cast(42); -} - -void* joiner_main(void* out) { - pthread_t handle; - { - std::unique_lock lock(hand_m); - hand_cv.wait(lock, [] { return handed; }); - handle = handed_over; - } - void* got = nullptr; - int rc = pthread_join(handle, &got); - *static_cast(out) = rc == 0 ? reinterpret_cast(got) : -rc; - return nullptr; -} - -void* exits_with_42(void*) { - pthread_exit(reinterpret_cast(42)); -} - -void* returns_null(void*) { - return nullptr; -} - -std::mutex detached_m; -std::condition_variable detached_cv; -int detached_ran = 0; - -void* detached_main(void*) { - std::lock_guard lock(detached_m); - detached_ran++; - detached_cv.notify_one(); - return nullptr; -} - -template -struct Counted { - ~Counted() { exit_handlers_ran++; } -}; - -template -void register_one() { - static Counted counted; -} - -template -void register_all(std::integer_sequence) { - (register_one(), ...); -} - -const char* errno_name(int e) { - switch (e) { - case 0: - return "0"; - case EPERM: - return "EPERM"; - case EINVAL: - return "EINVAL"; - case EDEADLK: - return "EDEADLK"; - case EAGAIN: - return "EAGAIN"; - case ETIMEDOUT: - return "ETIMEDOUT"; - default: - return "another error"; - } -} - -} // namespace - -int main() { - std::vector v(10); - std::iota(v.begin(), v.end(), 1); - int sum = std::accumulate(v.begin(), v.end(), 0); - std::string s = "hello"; - s += ", ToyOS"; - std::cout << "vector sum " << sum << ", back " << v.back() << ", string " << s << " (" << s.size() << ")\n"; - - try { - thrower(5); - } catch (const std::runtime_error& e) { - std::cout << "caught " << e.what() << "\n"; - } - try { - try { - throw 42; - } catch (int n) { - std::cout << "caught int " << n << ", rethrowing\n"; - throw; - } - } catch (int n) { - std::cout << "caught rethrown int " << n << "\n"; - } - try { - (void)std::vector().at(3); - } catch (const std::out_of_range&) { - std::cout << "caught out_of_range from at\n"; - } - try { - (void)std::stoi("not a number"); - } catch (const std::invalid_argument&) { - std::cout << "caught invalid_argument from stoi\n"; - } - try { - (void)std::stoi("99999999999"); - } catch (const std::out_of_range&) { - std::cout << "caught out_of_range from stoi\n"; - } - - std::vector partial(4); - std::vector workers; - for (int t = 0; t < 4; t++) { - workers.emplace_back([t, &partial] { - per_thread.id = t + 1; - long acc = 0; - for (long i = t; i < 1000; i += 4) - acc += i; - partial[t] = acc; - }); - } - for (auto& w : workers) - w.join(); - std::cout << "threads summed " << std::accumulate(partial.begin(), partial.end(), 0L) << "\n"; - std::cout << "thread_local destructors ran " << thread_locals_destroyed.load() << "\n"; - - std::mutex m; - std::condition_variable cv; - int stage = 0; - std::thread ping([&] { - std::unique_lock lock(m); - cv.wait(lock, [&] { return stage == 1; }); - stage = 2; - cv.notify_one(); - }); - { - std::lock_guard lock(m); - stage = 1; - } - cv.notify_one(); - { - std::unique_lock lock(m); - cv.wait(lock, [&] { return stage == 2; }); - } - ping.join(); - std::cout << "condition variable handshake done\n"; - - std::exception_ptr carried; - std::thread failing([&] { - try { - throw std::logic_error("from a thread"); - } catch (...) { - carried = std::current_exception(); - } - }); - failing.join(); - try { - std::rethrow_exception(carried); - } catch (const std::logic_error& e) { - std::cout << "rethrew " << e.what() << "\n"; - } - - pthread_key_t key; - pthread_key_create(&key, nullptr); - pthread_setspecific(key, &key); - void* seen = &seen; - std::thread::id other; - std::thread reader([&] { - seen = pthread_getspecific(key); - other = std::this_thread::get_id(); - }); - reader.join(); - std::cout << "a key set in main reads " << (seen == nullptr ? "null" : "set") << " in another thread and " - << (pthread_getspecific(key) == &key ? "set" : "lost") << " in main; thread ids " - << (other != std::this_thread::get_id() ? "differ" : "match") << "\n"; - - intptr_t joined = 0; - pthread_t joiner, handed_thread; - pthread_create(&joiner, nullptr, joiner_main, &joined); - pthread_create(&handed_thread, nullptr, handed_main, nullptr); - { - std::lock_guard lock(hand_m); - released = true; - } - hand_cv.notify_all(); - pthread_join(joiner, nullptr); - std::cout << "a thread joined by a third on its own pthread_self, before or after its creator returned, gave " << joined << "\n"; - - pthread_t exiting; - void* exited = nullptr; - pthread_create(&exiting, nullptr, exits_with_42, nullptr); - pthread_join(exiting, &exited); - std::cout << "pthread_exit handed its join " << reinterpret_cast(exited) << "\n"; - - // 64 stacks of 64 MiB each way, more than the guest's memory - // (`tests/common/qemu.rs`'s `-m`): each is freed, or a create runs out. - constexpr int stacks = 64; - pthread_attr_t big; - pthread_attr_init(&big); - pthread_attr_setstacksize(&big, size_t{64} << 20); - int joined_stacks = 0; - int detached_stacks = 0; - int refused = 0; - while (joined_stacks < stacks && refused == 0) { - pthread_t t; - refused = pthread_create(&t, &big, returns_null, nullptr); - if (refused == 0) { - pthread_join(t, nullptr); - joined_stacks++; - } - } - pthread_attr_setdetachstate(&big, PTHREAD_CREATE_DETACHED); - while (detached_stacks < stacks && refused == 0) { - pthread_t t; - refused = pthread_create(&t, &big, detached_main, nullptr); - if (refused == 0) - detached_stacks++; - } - { - std::unique_lock lock(detached_m); - detached_cv.wait(lock, [&] { return detached_ran == detached_stacks; }); - } - std::cout << "threads with 64 MiB stacks: " << joined_stacks << " joined, " << detached_stacks - << " detached, the last create answering " << errno_name(refused) << "\n"; - - std::recursive_mutex recursive; - recursive.lock(); - bool again = recursive.try_lock(); - if (again) - recursive.unlock(); - recursive.unlock(); - pthread_mutexattr_t checking; - pthread_mutexattr_init(&checking); - pthread_mutexattr_settype(&checking, PTHREAD_MUTEX_ERRORCHECK); - pthread_mutex_t checked; - pthread_mutex_init(&checked, &checking); - pthread_mutex_lock(&checked); - int relock = pthread_mutex_lock(&checked); - pthread_mutex_unlock(&checked); - std::cout << "a recursive mutex takes a second lock: " << (again ? "yes" : "no") - << "; an error-checking one answers " << errno_name(relock) << "\n"; - - char small[4]; - int whole = std::snprintf(small, sizeof small, "%d", 123456); - std::cout << "snprintf into 4 bytes answers " << whole << " and holds " << small << "\n"; - int printed = std::printf("%04100d\n", 42); - std::cout << "printf printed " << printed << " bytes\n"; - pthread_attr_t huge; - pthread_attr_init(&huge); - int no_stack = pthread_attr_setstacksize(&huge, SIZE_MAX); - std::cout << "a stack of SIZE_MAX bytes: " << errno_name(no_stack) << "; getentropy of nothing: " - << getentropy(nullptr, 0) << "\n"; - - std::ostringstream out; - out << std::stod("2.5") * 4 << ' ' << std::to_string(-17) << ' ' << std::stoull("18446744073709551615"); - std::cout << "stream " << out.str() << "\n"; - std::wstring wide = L"wide " + std::to_wstring(123); - std::cout << "wide length " << wide.size() << ", last " << static_cast(wide.back()) << "\n"; - std::map counts; - for (const char* word : {"a", "b", "a"}) - counts[word]++; - std::cout << "map a=" << counts["a"] << " b=" << counts["b"] << "\n"; - - { - std::mutex tm; - std::condition_variable tcv; - std::unique_lock lock(tm); - bool woke = tcv.wait_for(lock, std::chrono::milliseconds(10), [] { return false; }); - bool ready = false; - std::thread notifier([&] { - std::lock_guard g(tm); - ready = true; - tcv.notify_one(); - }); - auto notified = std::cv_status::no_timeout; - while (!ready && notified == std::cv_status::no_timeout) - notified = tcv.wait_until(lock, std::chrono::steady_clock::now() + std::chrono::seconds(30)); - lock.unlock(); - notifier.join(); - std::cout << "a wait nobody ends " << (woke ? "was woken" : "timed out") << ", a notified one answered " - << (notified == std::cv_status::no_timeout ? "no_timeout" : "timeout") << "\n"; - } - - std::cout << "a condition wait on a mutex it does not hold answers"; - for (int type : {PTHREAD_MUTEX_ERRORCHECK, PTHREAD_MUTEX_RECURSIVE}) { - pthread_mutexattr_t attr; - pthread_mutexattr_init(&attr); - pthread_mutexattr_settype(&attr, type); - pthread_mutex_t unheld; - pthread_mutex_init(&unheld, &attr); - pthread_cond_t cond; - pthread_cond_init(&cond, nullptr); - timespec at; - clock_gettime(CLOCK_REALTIME, &at); - at.tv_sec += 1; - int timed = pthread_cond_timedwait(&cond, &unheld, &at); - int plain = pthread_cond_wait(&cond, &unheld); - std::cout << (type == PTHREAD_MUTEX_ERRORCHECK ? " error-checking " : ", recursive ") << errno_name(timed) - << " and " << errno_name(plain); - } - std::cout << "\n"; - - register_all(std::make_integer_sequence{}); - per_thread.id = 0; - std::cout << "done\n"; - return 0; -} diff --git a/tests/cxx/runtime.expect b/tests/cxx/runtime.expect deleted file mode 100644 index f4421ae4fcb..00000000000 --- a/tests/cxx/runtime.expect +++ /dev/null @@ -1,27 +0,0 @@ -vector sum 55, back 10, string hello, ToyOS (12) -caught thrown from depth 0 -caught int 42, rethrowing -caught rethrown int 42 -caught out_of_range from at -caught invalid_argument from stoi -caught out_of_range from stoi -threads summed 499500 -thread_local destructors ran 4 -condition variable handshake done -rethrew from a thread -a key set in main reads null in another thread and set in main; thread ids differ -a thread joined by a third on its own pthread_self, before or after its creator returned, gave 42 -pthread_exit handed its join 42 -threads with 64 MiB stacks: 64 joined, 64 detached, the last create answering 0 -a recursive mutex takes a second lock: yes; an error-checking one answers EDEADLK -snprintf into 4 bytes answers 6 and holds 123 -00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000042 -printf printed 4101 bytes -a stack of SIZE_MAX bytes: EINVAL; getentropy of nothing: 0 -stream 10 -17 18446744073709551615 -wide length 8, last 3 -map a=2 b=1 -a wait nobody ends timed out, a notified one answered no_timeout -a condition wait on a mutex it does not hold answers error-checking EPERM and EPERM, recursive EPERM and EPERM -done -static destructor ran after 40 exit handlers and 5 thread_local destructors diff --git a/tests/desktopaudiocase/system.toml b/tests/desktopaudiocase/system.toml deleted file mode 100644 index 162b830f1c7..00000000000 --- a/tests/desktopaudiocase/system.toml +++ /dev/null @@ -1,66 +0,0 @@ -# A desktop with a shell and soundd: the T14's shape when #172 wedged it. -# -# `tests/desktopcase` has no daemon behind the shell and `tests/testcases` has -# no desktop in front of it, so the machine where a *shell-spawned* audio client -# meets a null sink existed in neither. That machine is this one: the client's -# stdio are pipes to a terminal, the terminal is a compositor surface, and -# soundd has no device. - -assets = ["assets"] - -[boot] -start = ["logd", "blockd", "fsd", "compositor", "soundd", "terminal"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does. The kernel keeps the record ring and writes no file at all, so a -# boot config without `logd` is a boot whose `/log` is empty — -# `every_boot_config_runs_logd` is what refuses one. -# It claims no device and serves no port: its row's authority is `logread`, -# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands -# it every program's output beside that. -[programs.logd] -service = true -syscap = ["logread"] - -[programs.compositor] -service = true -serves = ["compositor"] -receives = ["soundd", "launcher"] -devices = ["framebuffer", "keyboard", "mouse"] - -[programs.soundd] -service = true -serves = ["soundd"] -devices = ["hda-audio", "virtio-sound"] -syscap = ["rt"] - -[programs.terminal] -provides = ["surface"] -receives = ["compositor", "launcher"] - -[programs.shell] -receives = ["surface", "launcher"] - -[programs.toybox] -receives = ["compositor", "soundd", "surface"] - -[symlinks] -"bin/echo" = "/system/bin/toybox" -"bin/tone" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/desktopcase/system.toml b/tests/desktopcase/system.toml deleted file mode 100644 index 9df2efbe4c4..00000000000 --- a/tests/desktopcase/system.toml +++ /dev/null @@ -1,66 +0,0 @@ -# A desktop with a shell in it: the surface tree at its deepest. -# -# `tests/metalcase` is the desktop the compositor's own tests run on and has -# no shell; this one exists because a key typed here travels the whole tree — -# i8042, kernel, compositor, the terminal's window, the terminal's translator, -# the shell's stdin — and `desktop_locale_detect` is about the branch of that -# path where a child asks the terminal for the transitions instead. -# -# The terminal is in `init` rather than launched with the compositor's Ctrl+N, -# so it is the only window and therefore the focused one from the first frame. - -assets = ["assets"] - -# The terminal receiving `compositor` is what makes the boot race unrepresentable -# here first: the port exists before either process runs. No soundd or filepicker -# in this config, so nothing receives them. -[boot] -start = ["logd", "blockd", "fsd", "compositor", "terminal"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does. The kernel keeps the record ring and writes no file at all, so a -# boot config without `logd` is a boot whose `/log` is empty — -# `every_boot_config_runs_logd` is what refuses one. -# It claims no device and serves no port: its row's authority is `logread`, -# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands -# it every program's output beside that. -[programs.logd] -service = true -syscap = ["logread"] - -[programs.compositor] -service = true -serves = ["compositor"] -receives = ["launcher"] -devices = ["framebuffer", "keyboard", "mouse"] - -[programs.terminal] -provides = ["surface"] -receives = ["compositor", "launcher"] - -[programs.shell] -receives = ["surface", "launcher"] - -[programs.toybox] -receives = ["compositor", "surface"] - -[symlinks] -"bin/echo" = "/system/bin/toybox" -"bin/locale" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/doommusiccase/system.toml b/tests/doommusiccase/system.toml deleted file mode 100644 index 36ce12d4e08..00000000000 --- a/tests/doommusiccase/system.toml +++ /dev/null @@ -1,51 +0,0 @@ -# doom, soundd and the assets doom's music is made of — and the WAD whose demo -# `doom_frames` replays, hashing every tic's frame. -# -# No compositor: `/system/bin/doom --frame-check` never opens a window. - -assets = ["assets"] - -[boot] -start = ["logd", "blockd", "fsd", "soundd", "test-runner"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does. The kernel keeps the record ring and writes no file at all, so a -# boot config without `logd` is a boot whose `/log` is empty — -# `every_boot_config_runs_logd` is what refuses one. -# It claims no device and serves no port: its row's authority is `logread`, -# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands -# it every program's output beside that. -[programs.logd] -service = true -syscap = ["logread"] - -[programs.soundd] -service = true -serves = ["soundd"] -devices = ["hda-audio", "virtio-sound"] -syscap = ["rt"] - -# test-runner passes its namespace to doom. -[programs.test-runner] -receives = ["soundd"] -syscap = ["logread"] - -[programs.doom] -receives = ["soundd"] - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/e1000case/system.toml b/tests/e1000case/system.toml deleted file mode 100644 index 855ff03d201..00000000000 --- a/tests/e1000case/system.toml +++ /dev/null @@ -1,49 +0,0 @@ -# The one boot that runs netd in front of an Intel NIC. -# -# QEMU's `e1000e` is the 82574L at `8086:10d3`, whose register file is the one -# the ThinkPad T14's onboard I219 at `8086:15fc` has, so this config is what -# stands between `toyos-i219`'s host tests and the laptop. -# -# It is a second directory rather than a second `devices` row on -# `tests/netcase`, because a program that names a card this machine does not -# have costs an `init:` refusal line on every boot of the config that does. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] -# What lets `logd` serve this boot's log on the network, to whoever connects: -# without it the log is served on this machine only. -receives = ["netd"] - -# netd holds the NIC's PCI function and drives it: the descriptor rings, the -# register window and the interrupt are its own, and the kernel keeps only the -# claim. Named by vendor and device rather than by slot, so one row finds the -# card wherever firmware put it. -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:8086:10d3"] - -[programs.test-runner] -receives = ["netd"] -syscap = ["logread"] - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/e1000leasecase/system.toml b/tests/e1000leasecase/system.toml deleted file mode 100644 index 85bbdd86bbf..00000000000 --- a/tests/e1000leasecase/system.toml +++ /dev/null @@ -1,37 +0,0 @@ -# `tests/e1000case` with netd's lease probe armed: netd serves the 82574 QEMU -# models for its window, leaves its report on the log volume, and ends with the -# lease's verdict as its exit code. Nothing here receives `netd`, because the -# netd a client would connect to ends inside the boot. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:8086:10d3"] -args = ["--exit-with-lease"] - -[programs.test-runner] -syscap = ["logread"] - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/e1000talkcase/system.toml b/tests/e1000talkcase/system.toml deleted file mode 100644 index a7bf773b28a..00000000000 --- a/tests/e1000talkcase/system.toml +++ /dev/null @@ -1,57 +0,0 @@ -# `tests/lantalkcase` in front of QEMU's 82574L, the part whose register file -# the T14's I219 has: the rehearsal of that boot on the one machine in reach -# that runs netd's Intel driver. The host reaches it through slirp's forward -# and its listener on the host's loopback; everything else is that boot's. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "sshd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] -# What lets `logd` serve this boot's log on the network, to whoever connects: -# without it the log is served on this machine only. -receives = ["netd"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:8086:10d3"] - -[programs.sshd] -service = true -receives = ["netd", "launcher"] - -# The host replaces netd on this boot without rebooting it. -[programs.swap] -receives = ["swap"] - -[programs.test-runner] -syscap = ["logread"] - -# What `exec` is asked for over the cable. `power` because `reboot` is this -# binary under another name and it is the host's way of handing the machine -# back; `echo` is the command whose answer the host compares. -[programs.toybox] -receives = ["power"] - -[symlinks] -"bin/echo" = "/system/bin/toybox" -"bin/reboot" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/fixtures/SHA256SUMS b/tests/fixtures/SHA256SUMS deleted file mode 100644 index 8f3a800b48a..00000000000 --- a/tests/fixtures/SHA256SUMS +++ /dev/null @@ -1,4 +0,0 @@ -40c73eb97c69c3002bb3b56d8d4cff620b84cef2760bf80ac40eb97565a0fa01 gbae-v0.2.0-linux-x86_64.tar.gz -191216f00c4c8d3cdcd58dd708c9a938ae4956b39eca62615fe95d11facf17d5 gbae-v0.2.0-macos-universal.tar.gz -99fcd8a7263b5c25cd90cead1baaa7200ef272100fc2226e008a4e8205ba2916 gbae-v0.2.0-toyos-x86_64.tar.gz -98bf5cf0036ddd20089a359957d8eadcd153d6e23d329b2c9b9c1bb62a2a9b3d gbae-v0.2.0-windows-x86_64.zip diff --git a/tests/fixtures/gbae-v0.2.0-toyos-x86_64.tar.gz b/tests/fixtures/gbae-v0.2.0-toyos-x86_64.tar.gz deleted file mode 100644 index 53d914348b3..00000000000 Binary files a/tests/fixtures/gbae-v0.2.0-toyos-x86_64.tar.gz and /dev/null differ diff --git a/tests/flrswapcase/system.toml b/tests/flrswapcase/system.toml deleted file mode 100644 index 2999f85ba80..00000000000 --- a/tests/flrswapcase/system.toml +++ /dev/null @@ -1,55 +0,0 @@ -# `tests/e1000talkcase` with QEMU's `igb` beside the 82574: a function that -# resets on release by an Express function level reset, held by netd so a swap -# of netd releases it and the replacement claims it again. The 82574 carries -# the swap's ssh; the igb is only held. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "sshd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] -# The record stream's authority: the address on the parameter line is -# information, and this row is the whole of what can act on it. -receives = ["netd"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:8086:10d3", "pci:8086:10c9"] - -[programs.sshd] -service = true -receives = ["netd", "launcher"] - -[programs.swap] -receives = ["swap"] - -[programs.test-runner] -syscap = ["logread"] - -# `reboot` is this binary under another name and the host's way of ending the -# boot. -[programs.toybox] -receives = ["power"] - -[symlinks] -"bin/echo" = "/system/bin/toybox" -"bin/reboot" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/fsdclaimcase/system.toml b/tests/fsdclaimcase/system.toml deleted file mode 100644 index 80edd6de4df..00000000000 --- a/tests/fsdclaimcase/system.toml +++ /dev/null @@ -1,36 +0,0 @@ -# The boot `fsd_claim_held` judges: DATA is on a USB stick the kernel drives, -# so its file server holds the partition's claim; `--let-go-at-read` lets it -# go at the guest's first read of `home/fsd_let_go`, the guest takes the -# claim, and the server's end then finds it held when init starts the role -# again. - -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -# `device` because the guest mints DATA's claim, and `power` because -# `run shutdown` asks init through the `power` connector. -[programs.test-runner] -receives = ["power"] -syscap = ["device", "dup", "logread", "power"] - -[programs.toybox] -receives = ["power"] - -[symlinks] -"bin/shutdown" = "/system/bin/toybox" - -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] -args = ["--let-go-at-read", "home/fsd_let_go"] diff --git a/tests/fsdmountcase/system.toml b/tests/fsdmountcase/system.toml deleted file mode 100644 index 58ae0a5fac2..00000000000 --- a/tests/fsdmountcase/system.toml +++ /dev/null @@ -1,34 +0,0 @@ -# The boot `fsd_end_at_mount` judges: DATA's first file server ends once its -# volume is mounted and a connection waits on it, before it accepts one — the -# connection init's own file worker makes for the session home. - -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -# `power` because `run shutdown` asks init through the `power` connector, and -# the host reads the DATA partition back once the machine is down. -[programs.test-runner] -receives = ["power"] -syscap = ["dup", "logread", "power"] - -[programs.toybox] -receives = ["power"] - -[symlinks] -"bin/shutdown" = "/system/bin/toybox" - -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] -args = ["--end-at-mount", "data"] diff --git a/tests/fsdrestartcase/system.toml b/tests/fsdrestartcase/system.toml deleted file mode 100644 index e8352c71c39..00000000000 --- a/tests/fsdrestartcase/system.toml +++ /dev/null @@ -1,51 +0,0 @@ -# The boot `fsd_restart` judges: the test estate's shape, with every file -# server armed to end the moment it has taken a write through a file opened to -# append at `/home/fsd_end` and before it answers it, and at the first read of -# an installed package's manifest and of its binary this boot — -# `test_rs_fs_restart` ends DATA's four times, the first two under a launch -# init resolves and reads the image of, and init restarts it three. - -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -# `power` because `run shutdown` asks init through the `power` connector, and -# the host reads the DATA partition back once the machine is down; `launcher` -# for the launch of the `/apps` package the test ends DATA's server under. -[programs.test-runner] -receives = ["power", "launcher"] -syscap = ["dup", "logread", "power"] - -# `power` for `run shutdown`, which the test-runner launches through init. -[programs.toybox] -receives = ["power"] - -[symlinks] -"bin/shutdown" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. `--end-on` and `--end-at-read` are the test's -# actuators: paths on the DATA volume, which neither other role's volume -# carries. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] -args = [ - "--end-on", "home/fsd_end", - "--end-at-read", "apps/fs_restart/manifest.toml", - "--end-at-read", "apps/fs_restart/fs_restart", -] diff --git a/tests/https-fetch-host/Cargo.lock b/tests/https-fetch-host/Cargo.lock deleted file mode 100644 index ab0111f3f95..00000000000 --- a/tests/https-fetch-host/Cargo.lock +++ /dev/null @@ -1,1120 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "cc" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "digest", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2", - "subtle", - "zeroize", -] - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest", - "ff", - "generic-array", - "group", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core", - "subtle", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "https-fetch-host" -version = "0.1.0" -dependencies = [ - "rustls", - "rustls-pki-types", - "rustls-rustcrypto", - "sha2", - "ureq", - "webpki-roots", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-integer" -version = "0.1.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa", - "elliptic-curve", - "primeorder", - "sha2", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der", - "pkcs8", - "spki", -] - -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "pkcs5", - "spki", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom", - "libc", - "untrusted", - "windows-sys", -] - -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8", - "rand_core", - "sha2", - "signature", - "spki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" -dependencies = [ - "log", - "once_cell", - "rustls-pki-types", - "rustls-webpki 0.103.15", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-rustcrypto" -version = "0.0.2-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12052947763ab8515f753315357599e9b0b4dab3b8ba15f30f725fe6d025557" -dependencies = [ - "aead", - "aes-gcm", - "chacha20poly1305", - "crypto-common", - "der", - "digest", - "ecdsa", - "ed25519-dalek", - "hmac", - "p256", - "p384", - "paste", - "pkcs8", - "rand_core", - "rsa", - "rustls", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "sec1", - "sha2", - "signature", - "x25519-dalek", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.5", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core", -] - -[[package]] -name = "smallvec" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" - -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" -dependencies = [ - "base64", - "log", - "percent-encoding", - "rustls", - "rustls-pki-types", - "ureq-proto", - "utf8-zero", - "webpki-roots", -] - -[[package]] -name = "ureq-proto" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" -dependencies = [ - "base64", - "http", - "httparse", - "log", -] - -[[package]] -name = "utf8-zero" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "webpki-roots" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core", - "zeroize", -] - -[[package]] -name = "zerocopy" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] diff --git a/tests/https-fetch-host/Cargo.toml b/tests/https-fetch-host/Cargo.toml deleted file mode 100644 index 4c2a56da3c3..00000000000 --- a/tests/https-fetch-host/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -# The differential arm of `https_tls13`: the guest program's own source, built -# by the host's std instead of the ToyOS fork's. Nothing here may differ from -# `tests/toyos-rust-tests`'s dependency lines but the target the compiler is -# pointed at — that is the whole of what the oracle compares. -[package] -name = "https-fetch-host" -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" - -[[bin]] -name = "https_fetch" -path = "../toyos-rust-tests/src/bin/https_fetch.rs" - -[dependencies] -ureq = { version = "3", default-features = false, features = ["rustls-no-provider", "rustls-webpki-roots"] } -rustls = { version = "0.23", default-features = false, features = ["std", "logging"] } -rustls-rustcrypto = "0.0.2-alpha" -rustls-pki-types = "1" -webpki-roots = "1" -sha2 = { version = "0.10", default-features = false } - -[lints.rust] -warnings = "deny" diff --git a/tests/https-server-host/Cargo.lock b/tests/https-server-host/Cargo.lock deleted file mode 100644 index b27c1e5e628..00000000000 --- a/tests/https-server-host/Cargo.lock +++ /dev/null @@ -1,1444 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common 0.1.7", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base16ct" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" -dependencies = [ - "hybrid-array", -] - -[[package]] -name = "cc" -version = "1.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures 0.2.17", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common 0.1.7", - "inout", - "zeroize", -] - -[[package]] -name = "cmov" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const-oid" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" - -[[package]] -name = "cpubits" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "cpufeatures" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" -dependencies = [ - "cpubits", - "ctutils", - "getrandom 0.4.3", - "hybrid-array", - "num-traits", - "rand_core 0.10.1", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "crypto-common" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" -dependencies = [ - "getrandom 0.4.3", - "hybrid-array", - "rand_core 0.10.1", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "ctutils" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", - "subtle", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "curve25519-dalek-derive", - "digest 0.10.7", - "fiat-crypto", - "rustc_version", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid 0.9.6", - "pem-rfc7468 0.7.0", - "zeroize", -] - -[[package]] -name = "der" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" -dependencies = [ - "const-oid 0.10.2", - "der_derive", - "flagset", - "pem-rfc7468 1.0.0", - "zeroize", -] - -[[package]] -name = "der_derive" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59600e2c2d636fde9b65e99cc6445ac770c63d3628195ff39932b8d6d7409903" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "const-oid 0.9.6", - "crypto-common 0.1.7", - "subtle", -] - -[[package]] -name = "digest" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" -dependencies = [ - "block-buffer 0.12.1", - "const-oid 0.10.2", - "crypto-common 0.2.2", - "ctutils", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der 0.7.10", - "digest 0.10.7", - "elliptic-curve 0.13.8", - "rfc6979 0.4.0", - "signature 2.2.0", - "spki 0.7.3", -] - -[[package]] -name = "ecdsa" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" -dependencies = [ - "der 0.8.1", - "digest 0.11.3", - "elliptic-curve 0.14.1", - "rfc6979 0.6.0", - "signature 3.0.0", - "spki 0.8.0", - "zeroize", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8 0.10.2", - "signature 2.2.0", -] - -[[package]] -name = "ed25519-dalek" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" -dependencies = [ - "curve25519-dalek", - "ed25519", - "serde", - "sha2 0.10.9", - "subtle", - "zeroize", -] - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct 0.2.0", - "crypto-bigint 0.5.5", - "digest 0.10.7", - "ff 0.13.1", - "generic-array", - "group 0.13.0", - "hkdf", - "pem-rfc7468 0.7.0", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sec1 0.7.3", - "subtle", - "zeroize", -] - -[[package]] -name = "elliptic-curve" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" -dependencies = [ - "base16ct 1.0.0", - "crypto-bigint 0.7.5", - "crypto-common 0.2.2", - "digest 0.11.3", - "ff 0.14.0", - "group 0.14.0", - "hybrid-array", - "pem-rfc7468 1.0.0", - "pkcs8 0.11.0", - "rand_core 0.10.1", - "sec1 0.8.1", - "subtle", - "zeroize", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "ff" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" -dependencies = [ - "rand_core 0.10.1", - "subtle", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" - -[[package]] -name = "flagset" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "rand_core 0.10.1", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff 0.13.1", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "group" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" -dependencies = [ - "ff 0.14.0", - "rand_core 0.10.1", - "subtle", -] - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac 0.12.1", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.3", -] - -[[package]] -name = "https-server-host" -version = "0.1.0" -dependencies = [ - "der 0.8.1", - "getrandom 0.4.3", - "p256 0.14.0", - "rand_core 0.10.1", - "rustls", - "rustls-pki-types", - "rustls-rustcrypto", - "sha2 0.11.0", - "x509-cert", -] - -[[package]] -name = "hybrid-array" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" -dependencies = [ - "subtle", - "typenum", - "zeroize", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" - -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", -] - -[[package]] -name = "num-integer" -version = "0.1.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "p256" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", - "sha2 0.10.9", -] - -[[package]] -name = "p256" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" -dependencies = [ - "ecdsa 0.17.0", - "elliptic-curve 0.14.1", - "primefield", - "primeorder 0.14.0", - "sha2 0.11.0", -] - -[[package]] -name = "p384" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" -dependencies = [ - "ecdsa 0.16.9", - "elliptic-curve 0.13.8", - "primeorder 0.13.6", - "sha2 0.10.9", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "pem-rfc7468" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" -dependencies = [ - "base64ct", -] - -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", -] - -[[package]] -name = "pkcs5" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "pkcs5", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" -dependencies = [ - "der 0.8.1", - "spki 0.8.0", -] - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "primefield" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" -dependencies = [ - "crypto-bigint 0.7.5", - "crypto-common 0.2.2", - "ff 0.14.0", - "rand_core 0.10.1", - "subtle", - "zeroize", -] - -[[package]] -name = "primeorder" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" -dependencies = [ - "elliptic-curve 0.13.8", -] - -[[package]] -name = "primeorder" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" -dependencies = [ - "elliptic-curve 0.14.1", - "once_cell", - "primefield", - "serdect", - "wnaf", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" -dependencies = [ - "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac 0.12.1", - "subtle", -] - -[[package]] -name = "rfc6979" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" -dependencies = [ - "crypto-bigint 0.7.5", - "hmac 0.13.0", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.17", - "libc", - "untrusted", - "windows-sys", -] - -[[package]] -name = "rsa" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" -dependencies = [ - "const-oid 0.9.6", - "digest 0.10.7", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "sha2 0.10.9", - "signature 2.2.0", - "spki 0.7.3", - "subtle", - "zeroize", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustls" -version = "0.23.43" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" -dependencies = [ - "log", - "once_cell", - "rustls-pki-types", - "rustls-webpki 0.103.15", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-rustcrypto" -version = "0.0.2-alpha" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12052947763ab8515f753315357599e9b0b4dab3b8ba15f30f725fe6d025557" -dependencies = [ - "aead", - "aes-gcm", - "chacha20poly1305", - "crypto-common 0.1.7", - "der 0.7.10", - "digest 0.10.7", - "ecdsa 0.16.9", - "ed25519-dalek", - "hmac 0.12.1", - "p256 0.13.2", - "p384", - "paste", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "rsa", - "rustls", - "rustls-pki-types", - "rustls-webpki 0.102.8", - "sec1 0.7.3", - "sha2 0.10.9", - "signature 2.2.0", - "x25519-dalek", -] - -[[package]] -name = "rustls-webpki" -version = "0.102.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct 0.2.0", - "der 0.7.10", - "generic-array", - "pkcs8 0.10.2", - "subtle", - "zeroize", -] - -[[package]] -name = "sec1" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" -dependencies = [ - "base16ct 1.0.0", - "ctutils", - "der 0.8.1", - "hybrid-array", - "subtle", - "zeroize", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.5", -] - -[[package]] -name = "serdect" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" -dependencies = [ - "base16ct 1.0.0", - "serde", -] - -[[package]] -name = "sha1" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - -[[package]] -name = "sha2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "digest 0.11.3", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "signature" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" -dependencies = [ - "digest 0.11.3", - "rand_core 0.10.1", -] - -[[package]] -name = "smallvec" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" - -[[package]] -name = "spin" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - -[[package]] -name = "spki" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" -dependencies = [ - "base64ct", - "der 0.8.1", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tls_codec" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" -dependencies = [ - "tls_codec_derive", - "zeroize", -] - -[[package]] -name = "tls_codec_derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "typenum" -version = "1.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common 0.1.7", - "subtle", -] - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "wnaf" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "795ca18b3fdb5e62bf982199278341ddcf7ebf7d32e25e212ad05d496e95f6fa" -dependencies = [ - "ff 0.14.0", - "group 0.14.0", - "hybrid-array", - "primefield", -] - -[[package]] -name = "x25519-dalek" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" -dependencies = [ - "curve25519-dalek", - "rand_core 0.6.4", - "zeroize", -] - -[[package]] -name = "x509-cert" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "105ef4642d9cb137ef83d623d0e4bf08b8adf69e9918ca904a174adb6d3d038b" -dependencies = [ - "const-oid 0.10.2", - "der 0.8.1", - "sha1", - "signature 3.0.0", - "spki 0.8.0", - "tls_codec", -] - -[[package]] -name = "zerocopy" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zeroize" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] diff --git a/tests/https-server-host/Cargo.toml b/tests/https-server-host/Cargo.toml deleted file mode 100644 index dd9ca53e85a..00000000000 --- a/tests/https-server-host/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -# The host end of `https_tls13`: it mints a CA and four servers at run time and -# never commits a key. It is a crate of its own rather than a harness module -# because the harness is a target of the root package, and rustls, x509-cert -# and p256 have no business in the build system's resolution. -[package] -name = "https-server-host" -version = "0.1.0" -edition = "2021" -license = "MIT OR Apache-2.0" - -[[bin]] -name = "https_test_server" -path = "src/main.rs" - -[dependencies] -rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "logging"] } -rustls-rustcrypto = "0.0.2-alpha" -rustls-pki-types = "1" -x509-cert = { version = "0.3", features = ["builder", "pem"] } -p256 = { version = "0.14", features = ["ecdsa", "pkcs8"] } -der = { version = "0.8", features = ["pem"] } -rand_core = "0.10" -getrandom = { version = "0.4", features = ["sys_rng"] } -sha2 = "0.11" - -[lints.rust] -warnings = "deny" - -[profile.release] -opt-level = 2 diff --git a/tests/https-server-host/src/main.rs b/tests/https-server-host/src/main.rs deleted file mode 100644 index 0b10a7d1769..00000000000 --- a/tests/https-server-host/src/main.rs +++ /dev/null @@ -1,381 +0,0 @@ -//! The host end of the `https_tls13` judge: a CA and the servers a client -//! should refuse it for, all minted at start-up so no key is ever committed. -//! -//! stdout is the harness's contract: `ca `, `body-bytes `, -//! `body-sha256 `, one `port ` per listener, then `ready`. - -use std::io::{Read, Write}; -use std::net::{TcpListener, TcpStream}; -use std::sync::Arc; -use std::time::Duration; - -use der::asn1::{Ia5String, OctetString}; -use der::{Encode, EncodePem}; -use p256::ecdsa::{DerSignature, SigningKey}; -use p256::elliptic_curve::Generate; -use p256::pkcs8::EncodePrivateKey; -use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; -use rustls::{ServerConfig, ServerConnection, StreamOwned}; -use sha2::{Digest, Sha256}; -use x509_cert::builder::profile::cabf::tls::{CertificateType, Subscriber}; -use x509_cert::builder::profile::cabf::Root; -use x509_cert::builder::{Builder, CertificateBuilder}; -use x509_cert::ext::pkix::name::{GeneralName, GeneralNames}; -use x509_cert::ext::pkix::SubjectAltName; -use x509_cert::name::Name; -use x509_cert::serial_number::SerialNumber; -use x509_cert::spki::SubjectPublicKeyInfoOwned; -use x509_cert::time::{Time, Validity}; -use x509_cert::Certificate; - -/// The guest reaches the host at QEMU user-mode networking's gateway address; -/// the host arm of the same fetch reaches it on loopback. One certificate -/// carries both so the two arms differ by their std and by nothing else. -const GUEST_VIEW_OF_HOST: [u8; 4] = [10, 0, 2, 2]; -const HOST_LOOPBACK: [u8; 4] = [127, 0, 0, 1]; - -/// Slirp translates the guest's 10.0.2.2 to the host's loopback, so binding -/// there serves both arms without putting an ephemeral TLS port on the LAN. -const HOST_BIND: &str = "127.0.0.1"; - -/// The name the mismatch control's certificate carries instead. `.invalid` is -/// reserved by RFC 2606, so it can never become somebody's real host. -const WRONG_NAME: &str = "not-this-server.invalid"; - -fn main() { - let mut out_dir = None; - let mut body_bytes = 320_000usize; - let args: Vec = std::env::args().collect(); - let mut i = 1; - while i < args.len() { - match args[i].as_str() { - "--out" => { - i += 1; - out_dir = args.get(i).cloned(); - } - "--body-bytes" => { - i += 1; - body_bytes = args[i].parse().expect("--body-bytes takes a number"); - } - other => panic!("unknown argument {other}"), - } - i += 1; - } - let out_dir = out_dir.expect("--out is required"); - - let ca = Authority::mint(); - let ca_pem = ca.cert.to_pem(der::pem::LineEnding::LF).expect("CA to PEM"); - let ca_path = format!("{out_dir}/ca.pem"); - std::fs::write(&ca_path, ca_pem).expect("write the CA"); - - let body = Arc::new(filler(body_bytes)); - let digest = Sha256::digest(&body[..]); - let mut sha = String::with_capacity(64); - for byte in digest { - use std::fmt::Write as _; - let _ = write!(sha, "{byte:02x}"); - } - - let valid = ca.leaf(&addresses(), Age::Valid); - let expired = ca.leaf(&addresses(), Age::Expired); - let wrong = ca.leaf( - &[GeneralName::DnsName( - Ia5String::new(WRONG_NAME).expect("an IA5 name"), - )], - Age::Valid, - ); - - println!("ca {ca_path}"); - println!("body-bytes {}", body.len()); - println!("body-sha256 {sha}"); - serve("ok", &valid, Version::Tls13, Arc::clone(&body)); - serve("wrongname", &wrong, Version::Tls13, Arc::clone(&body)); - serve("expired", &expired, Version::Tls13, Arc::clone(&body)); - serve("tls12", &valid, Version::Tls12, Arc::clone(&body)); - let plain = cleartext(Arc::clone(&body)); - serve_redirect("redirect", &valid, plain); - downgrade(); - println!("ready"); - std::io::stdout().flush().expect("flush the contract"); - - loop { - std::thread::sleep(Duration::from_secs(3600)); - } -} - -fn addresses() -> Vec { - vec![ - GeneralName::IpAddress(OctetString::new(GUEST_VIEW_OF_HOST).expect("four bytes")), - GeneralName::IpAddress(OctetString::new(HOST_LOOPBACK).expect("four bytes")), - ] -} - -/// Deterministic, so the two arms hash the same bytes; large, so the record -/// layer fragments it. -fn filler(len: usize) -> Vec { - let mut out = Vec::with_capacity(len); - let mut state = 0x2545_f491_4f6c_dd1du64; - while out.len() < len { - state ^= state << 13; - state ^= state >> 7; - state ^= state << 17; - out.extend_from_slice(&state.to_le_bytes()); - } - out.truncate(len); - out -} - -enum Age { - Valid, - Expired, -} - -#[derive(Clone, Copy)] -enum Version { - Tls12, - Tls13, -} - -struct Leaf { - chain: Vec>, - key: PrivateKeyDer<'static>, -} - -impl Clone for Leaf { - fn clone(&self) -> Self { - Leaf { - chain: self.chain.clone(), - key: self.key.clone_key(), - } - } -} - -struct Authority { - cert: Certificate, - key: SigningKey, - name: Name, -} - -impl Authority { - fn mint() -> Self { - let key = SigningKey::generate_from_rng(&mut rand_core::UnwrapErr(getrandom::SysRng)); - let name: Name = "CN=ToyOS https judge root,O=ToyOS https judge,C=XX".parse().expect("a root name"); - let spki = SubjectPublicKeyInfoOwned::from_key(key.verifying_key()).expect("the CA's SPKI"); - let profile = Root::new(false, name.clone()).expect("the root profile"); - let cert = CertificateBuilder::new( - profile, - SerialNumber::from(1u32), - window(Age::Valid), - spki, - ) - .expect("a CA builder") - .build::<_, DerSignature>(&key) - .expect("a signed CA"); - Authority { cert, key, name } - } - - fn leaf(&self, names: &[GeneralName], age: Age) -> Leaf { - let key = SigningKey::generate_from_rng(&mut rand_core::UnwrapErr(getrandom::SysRng)); - let subject: Name = "CN=ToyOS https judge leaf,C=XX".parse().expect("a leaf name"); - let spki = SubjectPublicKeyInfoOwned::from_key(key.verifying_key()).expect("the leaf SPKI"); - let general: GeneralNames = names.to_vec(); - let profile = Subscriber { - certificate_type: CertificateType::domain_validated(subject, general.clone()) - .expect("a domain-validated subscriber"), - issuer: self.name.clone(), - client_auth: false, - }; - let mut builder = - CertificateBuilder::new(profile, SerialNumber::from(2u32), window(age), spki) - .expect("a leaf builder"); - // The CABF subscriber profile leaves subjectAltName to its caller. - builder - .add_extension(&SubjectAltName(general)) - .expect("the subject alternative names"); - let cert = builder - .build::<_, DerSignature>(&self.key) - .expect("a signed leaf"); - let der = cert.to_der().expect("the leaf in DER"); - let ca_der = self.cert.to_der().expect("the CA in DER"); - Leaf { - chain: vec![CertificateDer::from(der), CertificateDer::from(ca_der)], - key: PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from( - key.to_pkcs8_der().expect("the leaf key").as_bytes().to_vec(), - )), - } - } -} - -fn window(age: Age) -> Validity { - let now = std::time::SystemTime::now(); - let day = Duration::from_secs(86_400); - let (from, to) = match age { - Age::Valid => (now - day, now + day), - Age::Expired => (now - day * 30, now - day * 29), - }; - Validity::new( - Time::try_from(from).expect("a not-before"), - Time::try_from(to).expect("a not-after"), - ) -} - -/// The cleartext half of the redirect arm: a peer a `302` can name. -fn cleartext(body: Arc>) -> u16 { - let listener = TcpListener::bind((HOST_BIND, 0)).expect("a listening port"); - let port = listener.local_addr().expect("the bound address").port(); - println!("port plain {port}"); - std::thread::spawn(move || { - for stream in listener.incoming() { - let Ok(stream) = stream else { continue }; - let body = Arc::clone(&body); - std::thread::spawn(move || answer(stream, None, &Answer::Body(body))); - } - }); - port -} - -fn serve_redirect(role: &str, leaf: &Leaf, plain: u16) { - listen(role, leaf, Version::Tls13, Answer::Redirect(plain)); -} - -fn serve(role: &str, leaf: &Leaf, version: Version, body: Arc>) { - listen(role, leaf, version, Answer::Body(body)); -} - -#[derive(Clone)] -enum Answer { - Body(Arc>), - Redirect(u16), -} - -impl Answer { - fn head(&self) -> String { - match self { - Answer::Body(body) => format!( - "HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", - body.len() - ), - // The guest reads this `Location`, so it names the guest's view of - // the host: loopback there would name the guest itself. - Answer::Redirect(port) => { - let [a, b, c, d] = GUEST_VIEW_OF_HOST; - format!( - "HTTP/1.1 302 Found\r\nLocation: http://{a}.{b}.{c}.{d}:{port}/\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" - ) - } - } - } -} - -/// Bind one TLS server. The port is printed after the bind, so nothing races. -fn listen(role: &str, leaf: &Leaf, version: Version, answer_with: Answer) { - let provider = Arc::new(rustls_rustcrypto::provider()); - let versions: &[&rustls::SupportedProtocolVersion] = match version { - Version::Tls12 => &[&rustls::version::TLS12], - Version::Tls13 => &[&rustls::version::TLS13], - }; - let leaf = leaf.clone(); - let config = ServerConfig::builder_with_provider(provider) - .with_protocol_versions(versions) - .expect("the server's versions") - .with_no_client_auth() - .with_single_cert(leaf.chain, leaf.key) - .expect("the server's certificate"); - let config = Arc::new(config); - - let listener = TcpListener::bind((HOST_BIND, 0)).expect("a listening port"); - let port = listener.local_addr().expect("the bound address").port(); - println!("port {role} {port}"); - std::thread::spawn(move || { - for stream in listener.incoming() { - let Ok(stream) = stream else { continue }; - let config = Arc::clone(&config); - let with = answer_with.clone(); - std::thread::spawn(move || answer(stream, Some(config), &with)); - } - }); -} - -/// The downgrade control, and the only server here that is not rustls: it -/// answers any ClientHello with a TLS 1.2 ServerHello, so the refusal has to -/// come from the client's own version pin rather than from an honest peer -/// declining. rustls names that one `ServerTlsVersionIsDisabledByOurConfig`. -fn downgrade() { - let listener = TcpListener::bind((HOST_BIND, 0)).expect("a listening port"); - println!("port downgrade {}", listener.local_addr().expect("bound").port()); - std::thread::spawn(move || { - for stream in listener.incoming() { - let Ok(mut stream) = stream else { continue }; - std::thread::spawn(move || { - let mut hello = [0u8; 2048]; - let Ok(n) = stream.read(&mut hello) else { return }; - // ClientHello: 5-byte record header, 4-byte handshake header, - // 2-byte legacy version, 32-byte random, then the session id - // this ServerHello has to echo back. - let id_len_at = 43; - if n < id_len_at + 1 { - return; - } - let id_len = hello[id_len_at] as usize; - if n < id_len_at + 1 + id_len { - return; - } - let session_id = &hello[id_len_at + 1..id_len_at + 1 + id_len]; - - let mut sh = vec![0x03, 0x03]; - sh.extend_from_slice(&[0x5au8; 32]); - sh.push(id_len as u8); - sh.extend_from_slice(session_id); - sh.extend_from_slice(&[0xc0, 0x2b, 0x00, 0x00, 0x00]); - let mut handshake = vec![0x02]; - handshake.extend_from_slice(&(sh.len() as u32).to_be_bytes()[1..]); - handshake.extend_from_slice(&sh); - let mut record = vec![0x16, 0x03, 0x03]; - record.extend_from_slice(&(handshake.len() as u16).to_be_bytes()); - record.extend_from_slice(&handshake); - let _ = stream.write_all(&record); - let _ = stream.flush(); - }); - } - }); -} - -/// One request, one response — over TLS with a config, in the clear without. -fn answer(stream: TcpStream, config: Option>, with: &Answer) { - match config { - Some(config) => { - let Ok(conn) = ServerConnection::new(config) else { - return; - }; - let mut tls = StreamOwned::new(conn, stream); - let _ = exchange(&mut tls, with); - let _ = tls.sock.shutdown(std::net::Shutdown::Write); - } - None => { - let mut plain = stream; - let _ = exchange(&mut plain, with); - let _ = plain.shutdown(std::net::Shutdown::Write); - } - } -} - -/// Read one request head and write the answer. The head is read a byte at a -/// time and bounded: the peer is untrusted and may never send the blank line. -fn exchange(io: &mut (impl Read + Write), with: &Answer) -> std::io::Result<()> { - let mut request = Vec::new(); - let mut byte = [0u8; 1]; - while !request.ends_with(b"\r\n\r\n") { - if io.read(&mut byte)? == 0 { - return Err(std::io::ErrorKind::UnexpectedEof.into()); - } - request.push(byte[0]); - if request.len() > 8192 { - return Err(std::io::ErrorKind::InvalidData.into()); - } - } - io.write_all(with.head().as_bytes())?; - if let Answer::Body(body) = with { - io.write_all(body)?; - } - io.flush() -} diff --git a/tests/iced-counter/Cargo.lock b/tests/iced-counter/Cargo.lock deleted file mode 100644 index 3c8ef1a1859..00000000000 --- a/tests/iced-counter/Cargo.lock +++ /dev/null @@ -1,4182 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ab_glyph" -version = "0.2.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "android-activity" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" -dependencies = [ - "android-properties", - "bitflags 2.13.2", - "cc", - "jni", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-sys", - "num_enum", - "thiserror 2.0.21", -] - -[[package]] -name = "android-build" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fc9904ad2ad097c3c1cfe2eacaaf0fc24710936fa9ed941cb310b7c6ed2ab7" -dependencies = [ - "windows-sys 0.52.0", -] - -[[package]] -name = "android-properties" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" - -[[package]] -name = "android_system_properties" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.104" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" - -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - -[[package]] -name = "ash" -version = "0.38.0+1.3.281" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" -dependencies = [ - "libloading", -] - -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.5", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix 1.1.5", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "async-signal" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.5", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" - -[[package]] -name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - -[[package]] -name = "block2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" -dependencies = [ - "objc2 0.6.4", -] - -[[package]] -name = "blocking" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytemuck" -version = "1.25.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "calloop" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" -dependencies = [ - "bitflags 2.13.2", - "log", - "polling", - "rustix 0.38.44", - "slab", - "thiserror 1.0.69", -] - -[[package]] -name = "calloop" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" -dependencies = [ - "bitflags 2.13.2", - "polling", - "rustix 1.1.5", - "slab", - "tracing", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" -dependencies = [ - "calloop 0.13.0", - "rustix 0.38.44", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "calloop-wayland-source" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" -dependencies = [ - "calloop 0.14.4", - "rustix 1.1.5", - "wayland-backend", - "wayland-client", -] - -[[package]] -name = "cc" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" - -[[package]] -name = "cfg_aliases" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" - -[[package]] -name = "clipboard-win" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" -dependencies = [ - "error-code", -] - -[[package]] -name = "clipboard_macos" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f" -dependencies = [ - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "clipboard_wayland" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "003f886bc4e2987729d10c1db3424e7f80809f3fc22dbc16c685738887cb37b8" -dependencies = [ - "smithay-clipboard", -] - -[[package]] -name = "clipboard_x11" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" -dependencies = [ - "thiserror 2.0.21", - "x11rb", -] - -[[package]] -name = "codespan-reporting" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "combine" -version = "4.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" -dependencies = [ - "bytes", - "memchr", -] - -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core-graphics" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "core-graphics-types 0.1.3", - "foreign-types", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "libc", -] - -[[package]] -name = "core-graphics-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" -dependencies = [ - "bitflags 2.13.2", - "core-foundation 0.10.1", - "libc", -] - -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "cosmic-text" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "173852283a9a57a3cbe365d86e74dc428a09c50421477d5ad6fe9d9509e37737" -dependencies = [ - "bitflags 2.13.2", - "fontdb", - "harfrust", - "linebender_resource_handle", - "log", - "rangemap", - "rustc-hash 1.1.0", - "self_cell", - "skrifa 0.37.0", - "smol_str", - "swash", - "sys-locale", - "unicode-bidi", - "unicode-linebreak", - "unicode-script", - "unicode-segmentation", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "cryoglyph" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc795bdbccdbd461736fb163930a009da6597b226d6f6fce33e7a8eb6ec519" -dependencies = [ - "cosmic-text", - "etagere", - "lru", - "rustc-hash 2.1.3", - "wgpu", -] - -[[package]] -name = "ctor" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83cf0d42651b16c6dfe68685716d18480d18a9c39c62d76e8cf3eb6ed5d8bcbf" -dependencies = [ - "dtor", -] - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - -[[package]] -name = "dispatch2" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" -dependencies = [ - "bitflags 2.13.2", - "objc2 0.6.4", -] - -[[package]] -name = "dlib" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" -dependencies = [ - "libloading", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "downcast-rs" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" - -[[package]] -name = "dpi" -version = "0.1.1" -source = "git+https://github.com/ToyOSOrg/winit?branch=toyos-0.30.13#f58e1f3bca784d97056fe47475c2dca8c6386816" - -[[package]] -name = "dtor" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edf234dd1594d6dd434a8fb8cada51ddbbc593e40e4a01556a0b31c62da2775b" - -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "error-code" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" - -[[package]] -name = "etagere" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc89bf99e5dc15954a60f707c1e09d7540e5cd9af85fa75caa0b510bc08c5342" -dependencies = [ - "euclid", - "svg_fmt", -] - -[[package]] -name = "euclid" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" -dependencies = [ - "num-traits", -] - -[[package]] -name = "event-listener" -version = "5.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" -dependencies = [ - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - -[[package]] -name = "fastrand" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" - -[[package]] -name = "find-msvc-tools" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "font-types" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "font-types" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8eb065f3251655b3c90e22e5e363f310fc5332fb3402e37bbc94752283248f6" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "fontconfig-parser" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" -dependencies = [ - "roxmltree", -] - -[[package]] -name = "fontdb" -version = "0.23.0" -source = "git+https://github.com/ToyOSOrg/fontdb?branch=toyos-0.23.0#2e445cf5f7ba158259b29aea1ad169efeee38ac4" -dependencies = [ - "fontconfig-parser", - "log", - "memmap2", - "slotmap", - "tinyvec", - "ttf-parser", -] - -[[package]] -name = "foreign-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" -dependencies = [ - "foreign-types-macros", - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "foreign-types-shared" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" - -[[package]] -name = "futures" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" - -[[package]] -name = "futures-executor" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" - -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "futures-sink" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" - -[[package]] -name = "futures-task" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" - -[[package]] -name = "futures-util" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.5", - "windows-link", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "git+https://github.com/ToyOSOrg/getrandom?branch=toyos-0.3-sdk-0.12#67c17e0a7cd6d29e8ce231073746970cbd7dade7" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "toyos-abi", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "git+https://github.com/ToyOSOrg/getrandom?branch=toyos-0.4#f0286c8b3f0cd6d9fa9124ee837fa404e66313a7" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "toyos-abi", - "wasip2", - "wasip3", -] - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glam" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "151665d9be52f9bb40fc7966565d39666f2d1e69233571b71b87791c7e0528b3" - -[[package]] -name = "glow" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" -dependencies = [ - "js-sys", - "slotmap", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "glutin_wgl_sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gpu-alloc" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" -dependencies = [ - "bitflags 2.13.2", - "gpu-alloc-types", -] - -[[package]] -name = "gpu-alloc-types" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" -dependencies = [ - "bitflags 2.13.2", -] - -[[package]] -name = "gpu-allocator" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c151a2a5ef800297b4e79efa4f4bec035c5f51d5ae587287c9b952bdf734cacd" -dependencies = [ - "log", - "presser", - "thiserror 1.0.69", - "windows 0.58.0", -] - -[[package]] -name = "gpu-descriptor" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" -dependencies = [ - "bitflags 2.13.2", - "gpu-descriptor-types", - "hashbrown 0.15.5", -] - -[[package]] -name = "gpu-descriptor-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" -dependencies = [ - "bitflags 2.13.2", -] - -[[package]] -name = "guillotiere" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62d5865c036cb1393e23c50693df631d3f5d7bcca4c04fe4cc0fd592e74a782" -dependencies = [ - "euclid", - "svg_fmt", -] - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "num-traits", - "zerocopy", -] - -[[package]] -name = "harfrust" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" -dependencies = [ - "bitflags 2.13.2", - "bytemuck", - "core_maths", - "read-fonts 0.35.0", - "smallvec", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hexf-parse" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" - -[[package]] -name = "iced" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000e01026c93ba643f8357a3db3ada0e6555265a377f6f9291c472f6dd701fb3" -dependencies = [ - "iced_core", - "iced_debug", - "iced_futures", - "iced_renderer", - "iced_runtime", - "iced_widget", - "iced_winit", - "thiserror 2.0.21", -] - -[[package]] -name = "iced-counter" -version = "0.1.0" -dependencies = [ - "iced", -] - -[[package]] -name = "iced_core" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ab1937d699403e7e69252ae743a902bcee9f4ab2052cc4c9a46fcf34729d85" -dependencies = [ - "bitflags 2.13.2", - "bytes", - "glam", - "lilt", - "log", - "num-traits", - "rustc-hash 2.1.3", - "smol_str", - "thiserror 2.0.21", - "web-time", -] - -[[package]] -name = "iced_debug" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25035ab0215a620e53f4103e36fc4e59a1fb2817e4bfc38a30ad27b4202ea0be" -dependencies = [ - "iced_core", - "iced_futures", - "log", -] - -[[package]] -name = "iced_futures" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c0c85ccad42dfbec7293c36c018af0ea0dbcc52d137a4a9a0b0f6822a3fdf0a" -dependencies = [ - "futures", - "iced_core", - "log", - "rustc-hash 2.1.3", - "wasm-bindgen-futures", - "wasmtimer", -] - -[[package]] -name = "iced_graphics" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234ca1c2cec4155055f68fa5fad1b5242c496ac8238d80a259bca382fb44a102" -dependencies = [ - "bitflags 2.13.2", - "bytemuck", - "cosmic-text", - "half", - "iced_core", - "iced_futures", - "log", - "raw-window-handle", - "rustc-hash 2.1.3", - "thiserror 2.0.21", - "unicode-segmentation", -] - -[[package]] -name = "iced_program" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dfafec2947cda688d8eb00dac337ba11aa60f9ef6335aed343e189d26e4a673" -dependencies = [ - "iced_graphics", - "iced_runtime", -] - -[[package]] -name = "iced_renderer" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250cc0802408e8c077986ec56c7d07c65f423ee658a4b9fd795a1f2aae5dac05" -dependencies = [ - "iced_graphics", - "iced_tiny_skia", - "iced_wgpu", - "log", - "thiserror 2.0.21", -] - -[[package]] -name = "iced_runtime" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1889b819ce4c06674183242e336c8d49465665441396914dc07cc86f44fa8d4" -dependencies = [ - "bytes", - "iced_core", - "iced_futures", - "raw-window-handle", - "thiserror 2.0.21", -] - -[[package]] -name = "iced_tiny_skia" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c267596d742714b1853cc10c3983a367762816fc4836bd3b79f76ce76787d6f8" -dependencies = [ - "bytemuck", - "cosmic-text", - "iced_debug", - "iced_graphics", - "kurbo", - "log", - "rustc-hash 2.1.3", - "softbuffer", - "tiny-skia", -] - -[[package]] -name = "iced_wgpu" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff144a999b0ca0f8a10257934500060240825c42e950ec0ebee9c8ae30561c13" -dependencies = [ - "bitflags 2.13.2", - "bytemuck", - "cryoglyph", - "futures", - "glam", - "guillotiere", - "iced_debug", - "iced_graphics", - "log", - "rustc-hash 2.1.3", - "thiserror 2.0.21", - "wgpu", -] - -[[package]] -name = "iced_widget" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1596afa0d3109c2618e8bc12bae6c11d3064df8f95c42dfce570397dbe957ab" -dependencies = [ - "iced_renderer", - "log", - "num-traits", - "rustc-hash 2.1.3", - "thiserror 2.0.21", - "unicode-segmentation", -] - -[[package]] -name = "iced_winit" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7589888c8e951899cc688247a69933bb7a0511f0b4b2e122ac3fcd5dacb37f72" -dependencies = [ - "iced_debug", - "iced_program", - "log", - "mundy", - "rustc-hash 2.1.3", - "thiserror 2.0.21", - "tracing", - "wasm-bindgen-futures", - "web-sys", - "window_clipboard", - "winit", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jni" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" -dependencies = [ - "cfg-if", - "combine", - "jni-macros", - "jni-sys 0.4.1", - "log", - "simd_cesu8", - "thiserror 2.0.21", - "walkdir", - "windows-link", -] - -[[package]] -name = "jni-macros" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" -dependencies = [ - "proc-macro2", - "quote", - "rustc_version", - "simd_cesu8", - "syn 2.0.119", -] - -[[package]] -name = "jni-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" -dependencies = [ - "jni-sys 0.4.1", -] - -[[package]] -name = "jni-sys" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" -dependencies = [ - "jni-sys-macros", -] - -[[package]] -name = "jni-sys-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" -dependencies = [ - "quote", - "syn 2.0.119", -] - -[[package]] -name = "jobserver" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" -dependencies = [ - "getrandom 0.4.2", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883d941dae510fb2d978fc3fe018c71c9e2892fd38854de3e8b92c2e5ad9cc5" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "khronos-egl" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" -dependencies = [ - "libc", - "libloading", - "pkg-config", -] - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "kurbo" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1618d4ebd923e97d67e7cd363d80aef35fe961005cbbbb3d2dad8bdd1bc63440" -dependencies = [ - "arrayvec", - "smallvec", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "libloading" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" -dependencies = [ - "cfg-if", - "windows-link", -] - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ff90caf6077a803a240f62fdbe88645a890bbca49ef8174c3cb0404362171d" -dependencies = [ - "bitflags 2.13.2", - "libc", - "plain", - "redox_syscall 0.9.4", -] - -[[package]] -name = "lilt" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "337d4c256f7d9f2dbd633891d48ace853efa5b1554122d9220b172ad9c03c3a9" -dependencies = [ - "web-time", -] - -[[package]] -name = "linebender_resource_handle" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" - -[[package]] -name = "lru" -version = "0.16.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "memmap2" -version = "0.9.11" -source = "git+https://github.com/ToyOSOrg/memmap2-rs?branch=toyos-0.9.11#ad0d890512c14f3f42236b0ef032f310c40084e0" -dependencies = [ - "libc", -] - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "metal" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" -dependencies = [ - "bitflags 2.13.2", - "block", - "core-graphics-types 0.2.0", - "foreign-types", - "log", - "objc", - "paste", -] - -[[package]] -name = "mundy" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f32eb0db40f2df2bcfb05c93b8f73938d4c26ce9ac8881f1df0c8d3296921a73" -dependencies = [ - "android-build", - "async-io", - "cfg-if", - "dispatch", - "futures-channel", - "futures-lite", - "jni", - "ndk-context", - "objc2 0.6.4", - "objc2-app-kit 0.3.2", - "objc2-foundation 0.3.2", - "pin-project-lite", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.62.2", - "zbus", -] - -[[package]] -name = "naga" -version = "27.0.4" -source = "git+https://github.com/ToyOSOrg/wgpu?branch=toyos-27.0.4#e8a918055f88fb3a0d88d7b459732f1840082f90" -dependencies = [ - "arrayvec", - "bit-set", - "bitflags 2.13.2", - "cfg-if", - "cfg_aliases", - "codespan-reporting", - "half", - "hashbrown 0.16.1", - "hexf-parse", - "indexmap", - "libm", - "log", - "num-traits", - "once_cell", - "rustc-hash 1.1.0", - "spirv", - "thiserror 2.0.21", - "unicode-ident", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.13.2", - "jni-sys 0.3.1", - "log", - "ndk-sys", - "num_enum", - "raw-window-handle", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys 0.3.1", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "objc" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", -] - -[[package]] -name = "objc2" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" -dependencies = [ - "objc2-encode", -] - -[[package]] -name = "objc2-app-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" -dependencies = [ - "bitflags 2.13.2", - "block2 0.5.1", - "libc", - "objc2 0.5.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", -] - -[[package]] -name = "objc2-app-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" -dependencies = [ - "bitflags 2.13.2", - "block2 0.6.2", - "libc", - "objc2 0.6.4", - "objc2-cloud-kit 0.3.2", - "objc2-core-data 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image 0.3.2", - "objc2-core-text", - "objc2-core-video", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" -dependencies = [ - "bitflags 2.13.2", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.13.2", - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-contacts" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" -dependencies = [ - "bitflags 2.13.2", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.13.2", - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.13.2", - "dispatch2", - "objc2 0.6.4", -] - -[[package]] -name = "objc2-core-graphics" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" -dependencies = [ - "bitflags 2.13.2", - "dispatch2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-io-surface", -] - -[[package]] -name = "objc2-core-image" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2 0.6.4", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-core-location" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-contacts", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.13.2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.13.2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - -[[package]] -name = "objc2-encode" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" - -[[package]] -name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.13.2", - "block2 0.5.1", - "dispatch", - "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" -dependencies = [ - "bitflags 2.13.2", - "block2 0.6.2", - "libc", - "objc2 0.6.4", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-io-surface" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" -dependencies = [ - "bitflags 2.13.2", - "objc2 0.6.4", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-link-presentation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.13.2", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.13.2", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" -dependencies = [ - "bitflags 2.13.2", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-foundation 0.3.2", -] - -[[package]] -name = "objc2-symbols" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" -dependencies = [ - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-ui-kit" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" -dependencies = [ - "bitflags 2.13.2", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-cloud-kit 0.2.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", - "objc2-core-location", - "objc2-foundation 0.2.2", - "objc2-link-presentation", - "objc2-quartz-core 0.2.2", - "objc2-symbols", - "objc2-uniform-type-identifiers", - "objc2-user-notifications", -] - -[[package]] -name = "objc2-uniform-type-identifiers" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" -dependencies = [ - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-user-notifications" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" -dependencies = [ - "bitflags 2.13.2", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-core-location", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "orbclient" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" -dependencies = [ - "libc", - "libredox", -] - -[[package]] -name = "ordered-float" -version = "5.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "owned_ttf_parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" -dependencies = [ - "ttf-parser", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "piper" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - -[[package]] -name = "pkg-config" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "polling" -version = "3.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 1.1.5", - "windows-sys 0.61.2", -] - -[[package]] -name = "portable-atomic" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" - -[[package]] -name = "portable-atomic-util" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "presser" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.119", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "profiling" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" - -[[package]] -name = "quick-xml" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "range-alloc" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" - -[[package]] -name = "rangemap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" - -[[package]] -name = "raw-window-handle" -version = "0.6.2" -source = "git+https://github.com/ToyOSOrg/raw-window-handle?branch=toyos-0.6.2#2a5d102bdbd5dc987b653a7ffff5bedbbbfc53b3" - -[[package]] -name = "read-fonts" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" -dependencies = [ - "bytemuck", - "core_maths", - "font-types 0.10.1", -] - -[[package]] -name = "read-fonts" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046a7d674daf459825b32f5062056d6882db0d2f5a479fbd76ccfc870ac18709" -dependencies = [ - "bytemuck", - "font-types 0.12.5", - "once_cell", -] - -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.13.2", -] - -[[package]] -name = "redox_syscall" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" -dependencies = [ - "bitflags 2.13.2", -] - -[[package]] -name = "renderdoc-sys" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" - -[[package]] -name = "roxmltree" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" - -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.13.2", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" -dependencies = [ - "bitflags 2.13.2", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sctk-adwaita" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" -dependencies = [ - "ab_glyph", - "log", - "memmap2", - "smithay-client-toolkit 0.19.2", - "tiny-skia", -] - -[[package]] -name = "self_cell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "shlex" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd_cesu8" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" -dependencies = [ - "rustc_version", - "simdutf8", -] - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "skrifa" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" -dependencies = [ - "bytemuck", - "read-fonts 0.35.0", -] - -[[package]] -name = "skrifa" -version = "0.44.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819ab7d62b1d3e72d9d9dea5650bac30424f9111364bb94928dbf5ecad1baa68" -dependencies = [ - "bytemuck", - "read-fonts 0.41.0", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "slotmap" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" -dependencies = [ - "version_check", -] - -[[package]] -name = "smallvec" -version = "1.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" - -[[package]] -name = "smithay-client-toolkit" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" -dependencies = [ - "bitflags 2.13.2", - "calloop 0.13.0", - "calloop-wayland-source 0.3.0", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 0.38.44", - "thiserror 1.0.69", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-client-toolkit" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" -dependencies = [ - "bitflags 2.13.2", - "calloop 0.14.4", - "calloop-wayland-source 0.4.1", - "cursor-icon", - "libc", - "log", - "memmap2", - "rustix 1.1.5", - "thiserror 2.0.21", - "wayland-backend", - "wayland-client", - "wayland-csd-frame", - "wayland-cursor", - "wayland-protocols", - "wayland-protocols-experimental", - "wayland-protocols-misc", - "wayland-protocols-wlr", - "wayland-scanner", - "xkeysym", -] - -[[package]] -name = "smithay-clipboard" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" -dependencies = [ - "libc", - "smithay-client-toolkit 0.20.0", - "wayland-backend", -] - -[[package]] -name = "smol_str" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -dependencies = [ - "serde", -] - -[[package]] -name = "softbuffer" -version = "0.4.8" -source = "git+https://github.com/ToyOSOrg/softbuffer?branch=toyos-0.4.8#b36854df3cb4e589c124e9ecfa492b4e5b331c05" -dependencies = [ - "as-raw-xcb-connection", - "bytemuck", - "fastrand", - "js-sys", - "memmap2", - "ndk", - "objc2 0.6.4", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", - "raw-window-handle", - "redox_syscall 0.5.18", - "rustix 1.1.5", - "tiny-xlib", - "toyos-window", - "tracing", - "wasm-bindgen", - "wayland-backend", - "wayland-client", - "wayland-sys", - "web-sys", - "windows-sys 0.61.2", - "x11rb", -] - -[[package]] -name = "spirv" -version = "0.3.0+sdk-1.3.268.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" -dependencies = [ - "bitflags 2.13.2", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" - -[[package]] -name = "svg_fmt" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" - -[[package]] -name = "swash" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2499c2d826531388872b2268718aed907a39bd785ab0dcfe57fab26283f92e" -dependencies = [ - "skrifa 0.44.0", - "yazi", - "zeno", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sys-locale" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" -dependencies = [ - "libc", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix 1.1.5", - "windows-sys 0.61.2", -] - -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" -dependencies = [ - "thiserror-impl 2.0.21", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.6", -] - -[[package]] -name = "tiny-skia" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" -dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "tiny-skia-path", -] - -[[package]] -name = "tiny-skia-path" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" -dependencies = [ - "arrayref", - "bytemuck", - "strict-num", -] - -[[package]] -name = "tiny-xlib" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" -dependencies = [ - "as-raw-xcb-connection", - "ctor", - "libloading", - "pkg-config", - "tracing", -] - -[[package]] -name = "tinyvec" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "pin-project-lite", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.15+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" -dependencies = [ - "indexmap", - "toml_datetime", - "toml_parser", - "winnow", -] - -[[package]] -name = "toml_parser" -version = "1.1.3+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" -dependencies = [ - "winnow", -] - -[[package]] -name = "toyos" -version = "0.18.0" -dependencies = [ - "toyos-abi", -] - -[[package]] -name = "toyos-abi" -version = "0.16.0" - -[[package]] -name = "toyos-font" -version = "0.2.0" - -[[package]] -name = "toyos-keymap" -version = "0.1.0" - -[[package]] -name = "toyos-window" -version = "0.20.0" -dependencies = [ - "toyos", - "toyos-abi", - "toyos-font", - "toyos-keymap", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", -] - -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -dependencies = [ - "core_maths", -] - -[[package]] -name = "uds_windows" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" -dependencies = [ - "memoffset", - "tempfile", - "windows-sys 0.61.2", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - -[[package]] -name = "unicode-ident" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954" - -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - -[[package]] -name = "unicode-script" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "uuid" -version = "1.26.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef6dac1e96601b4fb3acccccff2139741fcb757cb9a36089bf5be91cfb285ce" -dependencies = [ - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb54f33acc68fd454578d9820b0bde1a1a3d17aa17bb7b6595806d02886d409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbab34de2d982e9b48e18d216d04c4a6f641066ff19ffb699980f591ee3610e" -dependencies = [ - "js-sys", - "tokio", - "wasm-bindgen", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e29d0c35b16e224a7eeb5cd2d25e3e1968fbd65604117b44d3b789d00ee8535" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f501a8bc3719dba86ef8ae4728879c08001bea749eb1333ac5b91e040e2a6b7" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 3.0.6", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.129" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23f0c9c52aa7cd7d77769a4cfe2a9adb1b331f489a41d912ce14513d5ab995c6" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.2", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "wasmtimer" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c598d6b99ea013e35844697fc4670d08339d5cda15588f193c6beedd12f644b" -dependencies = [ - "futures", - "js-sys", - "parking_lot", - "pin-utils", - "slab", - "wasm-bindgen", -] - -[[package]] -name = "wayland-backend" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" -dependencies = [ - "cc", - "downcast-rs", - "rustix 1.1.5", - "scoped-tls", - "smallvec", - "wayland-sys", -] - -[[package]] -name = "wayland-client" -version = "0.31.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" -dependencies = [ - "bitflags 2.13.2", - "rustix 1.1.5", - "wayland-backend", - "wayland-scanner", -] - -[[package]] -name = "wayland-csd-frame" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" -dependencies = [ - "bitflags 2.13.2", - "cursor-icon", - "wayland-backend", -] - -[[package]] -name = "wayland-cursor" -version = "0.31.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" -dependencies = [ - "rustix 1.1.5", - "wayland-client", - "xcursor", -] - -[[package]] -name = "wayland-protocols" -version = "0.32.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" -dependencies = [ - "bitflags 2.13.2", - "wayland-backend", - "wayland-client", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-experimental" -version = "20250721.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" -dependencies = [ - "bitflags 2.13.2", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-misc" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" -dependencies = [ - "bitflags 2.13.2", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-plasma" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" -dependencies = [ - "bitflags 2.13.2", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-protocols-wlr" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" -dependencies = [ - "bitflags 2.13.2", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-scanner", -] - -[[package]] -name = "wayland-scanner" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" -dependencies = [ - "proc-macro2", - "quick-xml", - "quote", -] - -[[package]] -name = "wayland-sys" -version = "0.31.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" -dependencies = [ - "dlib", - "log", - "once_cell", - "pkg-config", -] - -[[package]] -name = "web-sys" -version = "0.3.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88261b9deccee56594c11a3460c462c41f58d148598fe70ad77070126a68aba4" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "wgpu" -version = "27.0.4" -source = "git+https://github.com/ToyOSOrg/wgpu?branch=toyos-27.0.4#e8a918055f88fb3a0d88d7b459732f1840082f90" -dependencies = [ - "arrayvec", - "bitflags 2.13.2", - "cfg-if", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "js-sys", - "log", - "naga", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "smallvec", - "static_assertions", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "wgpu-core", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core" -version = "27.0.4" -source = "git+https://github.com/ToyOSOrg/wgpu?branch=toyos-27.0.4#e8a918055f88fb3a0d88d7b459732f1840082f90" -dependencies = [ - "arrayvec", - "bit-set", - "bit-vec", - "bitflags 2.13.2", - "bytemuck", - "cfg_aliases", - "document-features", - "hashbrown 0.16.1", - "indexmap", - "log", - "naga", - "once_cell", - "parking_lot", - "portable-atomic", - "profiling", - "raw-window-handle", - "rustc-hash 1.1.0", - "smallvec", - "thiserror 2.0.21", - "wgpu-core-deps-apple", - "wgpu-core-deps-emscripten", - "wgpu-core-deps-windows-linux-android", - "wgpu-hal", - "wgpu-types", -] - -[[package]] -name = "wgpu-core-deps-apple" -version = "27.0.4" -source = "git+https://github.com/ToyOSOrg/wgpu?branch=toyos-27.0.4#e8a918055f88fb3a0d88d7b459732f1840082f90" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-emscripten" -version = "27.0.4" -source = "git+https://github.com/ToyOSOrg/wgpu?branch=toyos-27.0.4#e8a918055f88fb3a0d88d7b459732f1840082f90" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-core-deps-windows-linux-android" -version = "27.0.4" -source = "git+https://github.com/ToyOSOrg/wgpu?branch=toyos-27.0.4#e8a918055f88fb3a0d88d7b459732f1840082f90" -dependencies = [ - "wgpu-hal", -] - -[[package]] -name = "wgpu-hal" -version = "27.0.4" -source = "git+https://github.com/ToyOSOrg/wgpu?branch=toyos-27.0.4#e8a918055f88fb3a0d88d7b459732f1840082f90" -dependencies = [ - "android_system_properties", - "arrayvec", - "ash", - "bit-set", - "bitflags 2.13.2", - "block", - "bytemuck", - "cfg-if", - "cfg_aliases", - "core-graphics-types 0.2.0", - "glow", - "glutin_wgl_sys", - "gpu-alloc", - "gpu-allocator", - "gpu-descriptor", - "hashbrown 0.16.1", - "js-sys", - "khronos-egl", - "libc", - "libloading", - "log", - "metal", - "naga", - "ndk-sys", - "objc", - "once_cell", - "ordered-float", - "parking_lot", - "portable-atomic", - "portable-atomic-util", - "profiling", - "range-alloc", - "raw-window-handle", - "renderdoc-sys", - "smallvec", - "thiserror 2.0.21", - "wasm-bindgen", - "web-sys", - "wgpu-types", - "windows 0.58.0", - "windows-core 0.58.0", -] - -[[package]] -name = "wgpu-types" -version = "27.0.4" -source = "git+https://github.com/ToyOSOrg/wgpu?branch=toyos-27.0.4#e8a918055f88fb3a0d88d7b459732f1840082f90" -dependencies = [ - "bitflags 2.13.2", - "bytemuck", - "js-sys", - "log", - "thiserror 2.0.21", - "web-sys", -] - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "window_clipboard" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5654226305eaf2dde8853fb482861d28e5dcecbbd40cb88e8393d94bb80d733" -dependencies = [ - "clipboard-win", - "clipboard_macos", - "clipboard_wayland", - "clipboard_x11", - "raw-window-handle", - "thiserror 2.0.21", -] - -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets", -] - -[[package]] -name = "windows" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core 0.62.2", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" -dependencies = [ - "windows-core 0.62.2", -] - -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core 0.62.2", - "windows-link", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" -dependencies = [ - "windows-core 0.62.2", - "windows-link", -] - -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows-threading" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winit" -version = "0.30.13" -source = "git+https://github.com/ToyOSOrg/winit?branch=toyos-0.30.13#f58e1f3bca784d97056fe47475c2dca8c6386816" -dependencies = [ - "ahash", - "android-activity", - "atomic-waker", - "bitflags 2.13.2", - "block2 0.5.1", - "bytemuck", - "calloop 0.13.0", - "cfg_aliases", - "concurrent-queue", - "core-foundation 0.9.4", - "core-graphics", - "cursor-icon", - "dpi", - "js-sys", - "libc", - "memmap2", - "ndk", - "objc2 0.5.2", - "objc2-app-kit 0.2.2", - "objc2-foundation 0.2.2", - "objc2-ui-kit", - "orbclient", - "percent-encoding", - "pin-project", - "raw-window-handle", - "redox_syscall 0.4.1", - "rustix 0.38.44", - "sctk-adwaita", - "smithay-client-toolkit 0.19.2", - "smol_str", - "toyos-window", - "tracing", - "unicode-segmentation", - "wasm-bindgen", - "wasm-bindgen-futures", - "wayland-backend", - "wayland-client", - "wayland-protocols", - "wayland-protocols-plasma", - "web-sys", - "web-time", - "windows-sys 0.52.0", - "x11-dl", - "x11rb", - "xkbcommon-dl", -] - -[[package]] -name = "winnow" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn 2.0.119", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.119", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.2", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.5", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - -[[package]] -name = "xcursor" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" - -[[package]] -name = "xkbcommon-dl" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" -dependencies = [ - "bitflags 2.13.2", - "dlib", - "log", - "once_cell", - "xkeysym", -] - -[[package]] -name = "xkeysym" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" - -[[package]] -name = "xml-rs" -version = "0.8.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" - -[[package]] -name = "yazi" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" - -[[package]] -name = "zbus" -version = "5.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.5", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 3.0.6", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" -dependencies = [ - "serde", - "winnow", - "zvariant", -] - -[[package]] -name = "zcheapstr" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" -dependencies = [ - "serde", -] - -[[package]] -name = "zeno" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" - -[[package]] -name = "zerocopy" -version = "0.8.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" - -[[package]] -name = "zvariant" -version = "5.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow", - "zcheapstr", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 3.0.6", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 3.0.6", - "winnow", -] diff --git a/tests/iced-counter/Cargo.toml b/tests/iced-counter/Cargo.toml deleted file mode 100644 index c4c7e18ef4d..00000000000 --- a/tests/iced-counter/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "iced-counter" -version = "0.1.0" -edition = "2024" -license = "MIT" -publish = false - -# iced's own `counter` example, its `src/main.rs` carried byte for byte from -# iced 0.14.0 as `src/bin/iced-counter.rs` (NOTICE names it), where the -# harness's build collects a binary. Default features: wgpu first, which finds no -# adapter here and falls back to tiny-skia. A package of its own and not a -# userland crate: `toolkit_iced` builds it and carries it into the toolkit -# desktop, so no image and no userland resolve pays for iced's tree, and -# upstream's `it_counts`, a test of iced's own simulator, is never built: -# `iced_test` is not a dependency. -[dependencies] -iced = "0.14" - -[workspace] - -# The forks iced's graph reaches on ToyOS, and the tree's own -# SDK crates. -[patch.crates-io] -toyos-abi = { path = "../../toyos-abi" } -toyos = { path = "../../toyos" } -toyos-window = { path = "../../userland/toyos-window" } -getrandom_03 = { git = "https://github.com/ToyOSOrg/getrandom", branch = "toyos-0.3-sdk-0.12", package = "getrandom" } -getrandom_04 = { git = "https://github.com/ToyOSOrg/getrandom", branch = "toyos-0.4", package = "getrandom" } -raw-window-handle = { git = "https://github.com/ToyOSOrg/raw-window-handle", branch = "toyos-0.6.2" } -softbuffer = { git = "https://github.com/ToyOSOrg/softbuffer", branch = "toyos-0.4.8" } -winit = { git = "https://github.com/ToyOSOrg/winit", branch = "toyos-0.30.13" } -memmap2 = { git = "https://github.com/ToyOSOrg/memmap2-rs", branch = "toyos-0.9.11" } -fontdb = { git = "https://github.com/ToyOSOrg/fontdb", branch = "toyos-0.23.0" } -wgpu = { git = "https://github.com/ToyOSOrg/wgpu", branch = "toyos-27.0.4" } - -# The guest profile every ToyOS program is built with. -[profile.toyos] -inherits = "dev" -opt-level = 2 -debug = true -debug-assertions = true -overflow-checks = true diff --git a/tests/iced-counter/src/bin/iced-counter.rs b/tests/iced-counter/src/bin/iced-counter.rs deleted file mode 100644 index 5027afd7e59..00000000000 --- a/tests/iced-counter/src/bin/iced-counter.rs +++ /dev/null @@ -1,67 +0,0 @@ -use iced::Center; -use iced::widget::{Column, button, column, text}; - -pub fn main() -> iced::Result { - iced::run(Counter::update, Counter::view) -} - -#[derive(Default)] -struct Counter { - value: i64, -} - -#[derive(Debug, Clone, Copy)] -enum Message { - Increment, - Decrement, -} - -impl Counter { - fn update(&mut self, message: Message) { - match message { - Message::Increment => { - self.value += 1; - } - Message::Decrement => { - self.value -= 1; - } - } - } - - fn view(&self) -> Column<'_, Message> { - column![ - button("Increment").on_press(Message::Increment), - text(self.value).size(50), - button("Decrement").on_press(Message::Decrement) - ] - .padding(20) - .align_x(Center) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use iced_test::{Error, simulator}; - - #[test] - fn it_counts() -> Result<(), Error> { - let mut counter = Counter { value: 0 }; - let mut ui = simulator(counter.view()); - - let _ = ui.click("Increment")?; - let _ = ui.click("Increment")?; - let _ = ui.click("Decrement")?; - - for message in ui.into_messages() { - counter.update(message); - } - - assert_eq!(counter.value, 1); - - let mut ui = simulator(counter.view()); - assert!(ui.find("1").is_ok(), "Counter should display 1!"); - - Ok(()) - } -} diff --git a/tests/inspectcase/system.toml b/tests/inspectcase/system.toml deleted file mode 100644 index 81de59943ef..00000000000 --- a/tests/inspectcase/system.toml +++ /dev/null @@ -1,78 +0,0 @@ -# The one boot that runs all four owners `inspect` reads: logd, the compositor, -# soundd and netd, on a machine with a framebuffer, a virtio NIC and a virtio -# sound card (`Profile::GopUsbDisk`), whose USB stick `tests/common/inspect.rs` crafts -# with one partition nobody holds and one init grants test-runner. -# -# test-runner holds no `launcher`, so every binary it runs is spawned directly -# and inherits its namespace — which is how `run inspect ...` reaches the -# owners, and how `test_rs_inspect_denied` can hand its own children a narrower -# one. - -assets = ["assets"] - -[boot] -start = ["logd", "blockd", "fsd", "compositor", "soundd", "netd", "test-runner"] - -# `every_boot_config_runs_logd` refuses a config without it. `log` is the port -# it answers `inspect` on. -[programs.logd] -service = true -syscap = ["logread"] -serves = ["log"] - -[programs.compositor] -service = true -serves = ["compositor"] -devices = ["framebuffer", "keyboard", "mouse"] - -[programs.soundd] -service = true -serves = ["soundd"] -devices = ["hda-audio", "virtio-sound"] -syscap = ["rt"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:1af4:1041"] - -# Every owner's connector, and `inventory` for `dev.*`: the runner's namespace -# is what its jobs inherit, and `dup` is what hands each job a duplicate of the -# capability, which `test_rs_inspect_denied` narrows to prove the refusal. -# The partition is the claimed one on the disk `tests/common/inspect.rs` -# crafts, where the GUID is mirrored; its neighbour there is the free one. -[programs.test-runner] -receives = ["netd", "soundd", "log", "compositor"] -syscap = ["inventory", "dup"] -devices = ["part:B4C5D6E7-F809-4A1B-8C2D-3E4F5A6B7C8D"] - -# The shipped reader's row. Declared so the image carries it; the runner spawns -# it directly, so what it holds here is the runner's namespace. -[programs.inspect] -receives = ["netd", "soundd", "log", "compositor"] -syscap = ["inventory"] - -# `shell -c` is how a job line becomes a pipe. -[programs.shell] - -[programs.toybox] - -[symlinks] -"bin/grep" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/jobdeadlinecase/system.toml b/tests/jobdeadlinecase/system.toml deleted file mode 100644 index c2eb878b258..00000000000 --- a/tests/jobdeadlinecase/system.toml +++ /dev/null @@ -1,44 +0,0 @@ -# The job list that never finishes, so nothing but the runner's own deadline -# ends this boot. -# -# `--bound-ms` is the one argument that is not a job. The T14 leaves the bound at -# `toyos_tco::JOB_BOUND_MS`; this shortens it for the suite's ceiling. -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -[programs.test-runner] -receives = ["power"] -# **`echo` is the job the deadline must never reach.** The deadline kills the -# job it was watching so the driver can end what it had in flight, and that kill -# releases the loop `spin` was blocking — so without `userland/test-runner`'s -# stand-down the loop starts this one, into a shutdown that has already written -# the boot's last word. -args = ["--bound-ms=3000", "spin", "echo"] - -[programs.toybox] - -[symlinks] -"bin/reboot" = "/system/bin/toybox" -"bin/spin" = "/system/bin/toybox" -"bin/echo" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/layoutcase/system.toml b/tests/layoutcase/system.toml deleted file mode 100644 index e74af13c381..00000000000 --- a/tests/layoutcase/system.toml +++ /dev/null @@ -1,55 +0,0 @@ -# Where a fresh boot puts things, asked over the cable: `layout_fresh_boot`. -# -# `tests/sshdcase` plus a declared shell and the `locale` applet. The shell is -# declared so that its `HOME` is init's row answer on the launcher path; the -# judge (`test_rs_layout_paths`) is declared nowhere, so its `HOME` is init's -# answer on the direct path, spawned by a service whose own `HOME` is -# `/state/sshd`. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "sshd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:1af4:1041"] - -[programs.sshd] -service = true -receives = ["netd", "launcher"] - -[programs.test-runner] -receives = ["netd"] -syscap = ["logread"] - -# A session program with no authority: what is asked of it is where its -# `HOME` is and where it keeps its history. -[programs.shell] - -# `echo` for the shell's `-c`, `locale` for the machine's keyboard layout. -[programs.toybox] - -[symlinks] -"bin/echo" = "/system/bin/toybox" -"bin/locale" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/logflushcase/system.toml b/tests/logflushcase/system.toml deleted file mode 100644 index ebbe0664aa7..00000000000 --- a/tests/logflushcase/system.toml +++ /dev/null @@ -1,39 +0,0 @@ -# A job list whose one job asks for a stop the kernel refuses -# (`power-refused-once`), while `logd` holds that stop's flush until init -# speaks again (`--hold-flush`): init waits the flush out, the stop is refused, -# and init's resume reaches `logd` with the flush unrun. The last job stops the -# machine for real. `log_resume_meets_its_flush` reads the verdict off `/log`. -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -args = ["--hold-flush"] -syscap = ["logread"] - -# `power` because both jobs ask init to stop the machine. -[programs.test-runner] -receives = ["power"] -args = ["test_rs_log_refused_stop", "reboot"] - -[programs.toybox] - -[symlinks] -"bin/reboot" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/logrotatecase/system.toml b/tests/logrotatecase/system.toml deleted file mode 100644 index b357f4ba57d..00000000000 --- a/tests/logrotatecase/system.toml +++ /dev/null @@ -1,91 +0,0 @@ -# The rotation boot: `tests/metalcase`'s machine shape with `/system/bin/logd` writing -# 256-byte files instead of mebibyte ones. -# -# **A config and not a boot parameter.** `log-rotate-fast` was a kernel actuator -# armed from the command line, because the rotation bound lived in the kernel's -# own file sink. It lives in `/system/bin/logd` now, and the way a program gets an -# argument in this system is its manifest row — so the arming moves from a -# cmdline the kernel parses into a config the image is built from, which is a -# capability-shaped answer rather than a global one. -# -# What it buys is unchanged: filling a mebibyte by -# logging would take a boot far longer than a test should wait, and the code -# the small bound drives is the shipped code. `kernel_log_file`'s second arm -# reads the continuations and the retention sweep off the volume. -# -# It is metalcase's program set and not a smaller one on purpose: a config -# that dropped the daemons would be measuring a different machine. - -assets = ["assets"] - -[boot] -start = ["logd", "blockd", "fsd", "compositor", "soundd", "netd", "sshd", "test-runner"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does. The kernel keeps the record ring and writes no file at all, so a -# boot config without `logd` is a boot whose `/log` is empty — -# `every_boot_config_runs_logd` is what refuses one. -# It claims no device and serves no port: its row's authority is `logread`, -# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands -# it every program's output beside that. -[programs.logd] -service = true -args = ["--rotate-fast"] -syscap = ["logread"] - -[programs.compositor] -service = true -serves = ["compositor"] -receives = ["soundd", "launcher"] -devices = ["framebuffer", "keyboard", "mouse"] - -[programs.soundd] -service = true -serves = ["soundd"] -devices = ["hda-audio", "virtio-sound"] -syscap = ["rt"] - -# netd holds the NIC's PCI function and drives it: the virtqueues, the register -# window and the interrupt are its own, and the kernel keeps only the claim. -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:1af4:1041"] - -[programs.sshd] -service = true -receives = ["netd", "launcher"] - -# The union its guest binaries need on this machine shape. -# `power` is the connector `run shutdown` below asks init through. -[programs.test-runner] -receives = ["compositor", "soundd", "netd", "power"] -syscap = ["logread"] - -# `run shutdown` is what closes `kernel_log_file`'s second arm, and it spawns -# `/system/bin/shutdown`. metalcase carries no toybox and therefore no such name, so -# this config adds the one applet the gate uses — a boot that could not shut -# down would run the rotation path and then fail on a tail that was never -# written, which is what it did the first time this config existed. -[programs.toybox] -receives = ["soundd"] - -[symlinks] -"bin/shutdown" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/logstreamcase/system.toml b/tests/logstreamcase/system.toml deleted file mode 100644 index e0ad0c73ecc..00000000000 --- a/tests/logstreamcase/system.toml +++ /dev/null @@ -1,45 +0,0 @@ -# The boot the served log is judged on (`tests/common/logstream.rs`'s -# `VIRTIO`): netd in front of a virtio NIC, `logd` serving the log through it, -# and a test-runner that can `run shutdown`, so every verdict is read off a -# volume the guest closed itself. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "test-runner"] - -# `receives = ["netd"]` is what lets `logd` serve this boot's log on the -# network. -[programs.logd] -service = true -syscap = ["logread"] -receives = ["netd"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:1af4:1041"] - -# `power` is what `run shutdown` asks init through. -[programs.test-runner] -receives = ["netd", "power"] - -[programs.toybox] - -[symlinks] -"bin/shutdown" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/logstreame1000case/system.toml b/tests/logstreame1000case/system.toml deleted file mode 100644 index 03a622eaa0b..00000000000 --- a/tests/logstreame1000case/system.toml +++ /dev/null @@ -1,45 +0,0 @@ -# The boot the served log is judged on (`tests/common/logstream.rs`'s -# `E1000E`): netd in front of QEMU's 82574L, whose register file the T14's -# I219 has, `logd` serving the log through it, and a test-runner that can -# `run shutdown`, so every verdict is read off a volume the guest closed itself. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "test-runner"] - -# `receives = ["netd"]` is what lets `logd` serve this boot's log on the -# network. -[programs.logd] -service = true -syscap = ["logread"] -receives = ["netd"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:8086:10d3"] - -# `power` is what `run shutdown` asks init through. -[programs.test-runner] -receives = ["netd", "power"] - -[programs.toybox] - -[symlinks] -"bin/shutdown" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/metalcase/system.toml b/tests/metalcase/system.toml index f46c4655cc5..b9539ccf646 100644 --- a/tests/metalcase/system.toml +++ b/tests/metalcase/system.toml @@ -1,8 +1,7 @@ # The metal-sim boot: what `Profile::Metal` puts in front of userland. # # soundd, netd and sshd are here for exactly the reason they have no device: -# their graceful exit is half of what this config certifies, and with a console -# `metal_sim_compositor` reads their exit lines rather than assuming them. +# their graceful exit is half of what this config certifies. # sshd is the one whose absence *is* the device absence — it has no device of # its own and reaches the machine's shape only through netd — and it was the # one this config left out, so nothing anywhere ran the branch where it finds diff --git a/tests/netcase/system.toml b/tests/netcase/system.toml index 1681580fe54..c7b85353392 100644 --- a/tests/netcase/system.toml +++ b/tests/netcase/system.toml @@ -1,11 +1,11 @@ -# The one boot that runs netd with a virtio NIC in front of it. +# The one boot that runs netd with a virtio NIC in front of it, with blockd's +# NVMe controller beside it: `iommu_virtio_platform`'s machine. # # netd's `main` opens the NIC before anything else and returns on `NotFound`, # so a config with no NIC never reaches a line of the daemon proper. # # test-runner is here for the same reason it is in metalcase: it makes the boot -# announce itself and lets an in-guest binary be driven over the -# ===TEST_START=== protocol. +# announce itself. [boot] start = ["logd", "blockd", "fsd", "netd", "test-runner"] @@ -16,9 +16,6 @@ start = ["logd", "blockd", "fsd", "netd", "test-runner"] [programs.logd] service = true syscap = ["logread"] -# What lets `logd` serve this boot's log on the network, to whoever connects: -# without it the log is served on this machine only. -receives = ["netd"] # netd holds the NIC's PCI function and drives it: the virtqueues, the register # window and the interrupt are its own, and the kernel keeps only the claim. @@ -29,32 +26,7 @@ service = true serves = ["netd"] devices = ["pci:1af4:1041"] -# This is the only config whose test binaries take the launcher path to -# `Command::spawn` at all: everywhere else test-runner holds no connector and -# every spawn is direct. [programs.test-runner] -receives = ["netd", "launcher"] -syscap = ["logread"] -# The second claimant on netd's function, and the only one in the tree: what -# `pci_function_is_exclusive` reads is the kernel answering `Owned` to it while -# netd keeps driving the card. `src/build.rs` names this exact entry — config, -# program, device — as the one exception to -# `every_device_class_has_at_most_one_claimant`. -devices = ["pci:1af4:1041"] - -# A declared program that serves nothing and provides nothing, so a refused -# launch of it takes no acceptor with it and a granted one is a clean round -# trip. -[programs.toybox] - -# What `spawn_cwd` asks for the owner's `cd` and a launch from it: the shell -# reaches the launcher through its own row, as it does on the desktop. -[programs.shell] -receives = ["launcher"] - -# What `dns_resolve` runs: a name's addresses, asked of netd's resolver. -[programs.host] -receives = ["netd"] # The block service: the NVMe controller this machine's DATA is on, driven # from userland through its claim. Started again when it ends; the claim goes diff --git a/tests/partclaimcase/system.toml b/tests/partclaimcase/system.toml deleted file mode 100644 index 8cbf0f9a053..00000000000 --- a/tests/partclaimcase/system.toml +++ /dev/null @@ -1,85 +0,0 @@ -# The partition-claim boot: `tests/testcases`'s estate, and one `devices` row -# that grants test-runner a partition by its unique GUID — the grant init makes -# from this file, which `partition_claim` proves by finding that partition -# already held. -# The disk carrying it is crafted by `tests/common/partclaim.rs`; the GUID is -# mirrored there and in the guest binary. - - -[boot] -start = ["logd", "blockd", "fsd", "soundd", "test-runner"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does. The kernel keeps the record ring and writes no file at all, so a -# boot config without `logd` is a boot whose `/log` is empty — -# `every_boot_config_runs_logd` is what refuses one. -# It claims no device and serves no port: its whole authority -# is `logread`, which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate. -[programs.logd] -service = true -syscap = ["logread"] - -[programs.soundd] -service = true -serves = ["soundd"] -devices = ["hda-audio", "virtio-sound"] -syscap = ["rt"] - -# The test estate's authority. -# `device` because five of the guest binaries claim the keyboard or the mouse -# and no manifest row can name them — they are not `[programs]` keys — and `dup` -# because a claim moves and one boot runs several of them. -# `power` because `endowment_denied` narrows it *away* to prove the two power -# syscalls refuse a capability without it, which a capability that never -# carried it would make vacuous. `run shutdown` does not use it: the applet asks -# init through the `power` connector, and init has `logd` make the log whole -# before it stops the machine. -# `roster` because four guest binaries read `SYS_SYSINFO`'s per-thread entries — -# soundd's, for the idle-suspend certification, and their own, which arrive in -# the same machine-wide answer and have no narrower question in the ABI. It is -# also what `endowment_denied` narrows *away* to prove the refusal, so an estate -# without it would make that arm vacuous rather than red. -[programs.test-runner] -receives = ["soundd", "power"] -syscap = ["device", "dup", "logread", "power", "roster"] -devices = ["part:A94F0E6D-3B2C-4E1A-8C7D-6E5F4A3B2C1D"] - -[programs.toybox] -receives = ["soundd"] - -[symlinks] -"bin/cat" = "/system/bin/toybox" -"bin/cp" = "/system/bin/toybox" -"bin/echo" = "/system/bin/toybox" -"bin/free" = "/system/bin/toybox" -"bin/grep" = "/system/bin/toybox" -"bin/hexdump" = "/system/bin/toybox" -"bin/ls" = "/system/bin/toybox" -"bin/mkdir" = "/system/bin/toybox" -"bin/mv" = "/system/bin/toybox" -"bin/ps" = "/system/bin/toybox" -"bin/pwd" = "/system/bin/toybox" -"bin/reboot" = "/system/bin/toybox" -"bin/rm" = "/system/bin/toybox" -"bin/shutdown" = "/system/bin/toybox" -# The shipped audio client, and the one the T14 hangs on. The raw-API tone in -# `toyos-rust-tests` drains the same sink perfectly, so a suite that ran only -# that one certified a path no user takes. -"bin/tone" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/pkgcase/system.toml b/tests/pkgcase/system.toml deleted file mode 100644 index 1dd833f7432..00000000000 --- a/tests/pkgcase/system.toml +++ /dev/null @@ -1,80 +0,0 @@ -# The package machine: a desktop, a sound daemon, the installer, and a test -# estate that holds a `launcher` connector and no `compositor` one. -# -# **That absence is the judge.** A guest binary here inherits test-runner's -# namespace, which reaches no desktop, so an installed package that opens a -# window can only have got there through the `[apps]` row init builds for it. -# On `tests/metalcase`, where test-runner receives `compositor` itself, the same -# launch would draw a window by inheritance and prove nothing. - -assets = ["assets"] - -[boot] -start = ["logd", "blockd", "fsd", "compositor", "soundd", "test-runner"] - -# What every program launched out of `/apps` holds. gbae needs both: it opens -# its window through the compositor and its cpal stream through soundd. -[apps] -receives = ["compositor", "soundd"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does. The kernel keeps the record ring and writes no file at all, so a -# boot config without `logd` is a boot whose `/log` is empty — -# `every_boot_config_runs_logd` is what refuses one. -[programs.logd] -service = true -syscap = ["logread"] - -[programs.compositor] -service = true -serves = ["compositor"] -receives = ["soundd", "launcher"] -devices = ["framebuffer", "keyboard", "mouse"] - -[programs.soundd] -service = true -serves = ["soundd"] -devices = ["hda-audio", "virtio-sound"] -syscap = ["rt"] - -# The installer holds nothing: it writes under `/apps` because `/apps` is -# writable, and a row that gave it anything more would make that a lie. -[programs.pkg] - -# `launcher` and nothing else. `power` is `/system/bin/shutdown`'s, which is -# this binary under another name — the host ends the guest with `run shutdown` -# and then reads `/apps` off the volume. -[programs.toybox] -receives = ["power"] - -# The half that knows a cwd: `relative-path` drives this binary with `-c` and -# asks it to run a dotted path the launcher refuses raw. -[programs.shell] -receives = ["launcher"] - -# `launcher` and no `compositor`, which is this config's whole point. -# `logread` is the estate's everywhere, and it is not dup-able — so test-runner -# endows a child nothing, and a child's spawn goes through the launcher. -[programs.test-runner] -receives = ["launcher"] -syscap = ["logread"] - -[symlinks] -"bin/shutdown" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/quiescetwicecase/system.toml b/tests/quiescetwicecase/system.toml deleted file mode 100644 index 0edffbdcf3d..00000000000 --- a/tests/quiescetwicecase/system.toml +++ /dev/null @@ -1,33 +0,0 @@ -# The boot `quiesce_refuses_a_second_shutdown` judges. -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -# `power` twice: the connector is how the job has init make the first call, -# and the right is the job's own second call. `logread` because the job reads -# the kernel's word that the first call waits, and `dup` because the runner -# passes the job a duplicate of what it holds. -[programs.test-runner] -receives = ["power"] -args = ["test_rs_quiesce_twice"] -syscap = ["dup", "logread", "power"] - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/sshdcase/system.toml b/tests/sshdcase/system.toml deleted file mode 100644 index b9fd26c84be..00000000000 --- a/tests/sshdcase/system.toml +++ /dev/null @@ -1,70 +0,0 @@ -# The one boot that runs sshd with a network under it. -# -# sshd binds before it reads anything of its own, so on every other config it -# leaves at the bind: metal-sim has no NIC by design, and no other test image -# builds it at all. Between them nothing in the suite ever reached a line of -# the daemon past that bind — not the host key it mints, not the file it -# authenticates against. -# -# Its own config rather than a fourth program in `tests/netcase`, because -# netcase's test measures how many connections netd will hold and sshd's bind -# would spend one of them. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "sshd", "test-runner"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does. The kernel keeps the record ring and writes no file at all, so a -# boot config without `logd` is a boot whose `/log` is empty — -# `every_boot_config_runs_logd` is what refuses one. -# It claims no device and serves no port: its row's authority is `logread`, -# which is `Rights::LOG | Rights::WAIT` on a `SysCap` duplicate, and init hands -# it every program's output beside that. -[programs.logd] -service = true -syscap = ["logread"] - -# netd holds the NIC's PCI function and drives it: the virtqueues, the register -# window and the interrupt are its own, and the kernel keeps only the claim. -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:1af4:1041"] - -[programs.sshd] -service = true -receives = ["netd", "launcher"] - -[programs.test-runner] -receives = ["netd"] -syscap = ["logread"] - -# What `exec` is asked for over the cable. A program with no authority at all: -# it serves nothing, receives nothing and claims no device, so what an `exec` -# arm proves is that the daemon ran the named binary and reported how it ended. -[programs.toybox] - -# So a bare name resolves the way it does on a real machine: `echo` for a -# program that says something, `cat` for one that writes to both its streams -# and one that reads its input, `spin` for one that never ends. -[symlinks] -"bin/echo" = "/system/bin/toybox" -"bin/cat" = "/system/bin/toybox" -"bin/spin" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/swapcase/system.toml b/tests/swapcase/system.toml deleted file mode 100644 index bc8283b2a0e..00000000000 --- a/tests/swapcase/system.toml +++ /dev/null @@ -1,62 +0,0 @@ -# A running service's binary replaced with no reboot, rehearsed on QEMU's -# virtio-net: `logd` streams every record to the host's listener, sshd runs -# `swap` for an authenticated login and it hands the binary to init, and netd is the -# service swapped — the one whose restart the stream and the ssh connection -# asking for it both have to outlive. -# -# `tests/e1000talkcase` is the same exchange in front of the T14's register -# file, and `tests/lantalkcase` the T14 itself. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "sshd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] -# The record stream's authority: the address on the parameter line is -# information, and this row is the whole of what can act on it. -receives = ["netd"] - -[programs.netd] -service = true -serves = ["netd"] -devices = ["pci:1af4:1041"] - -[programs.sshd] -service = true -receives = ["netd", "launcher"] - -# `swap` is the authority to replace a service's binary, and no other program -# may hold it: the host runs it over ssh. -[programs.swap] -receives = ["swap"] - -[programs.test-runner] -syscap = ["logread"] - -# What `exec` is asked for over the cable. `power` because `reboot` is this -# binary under another name and it is the host's way of ending the boot with -# its log whole; `echo` is the command whose answer the host compares. -[programs.toybox] -receives = ["power"] - -[symlinks] -"bin/echo" = "/system/bin/toybox" -"bin/reboot" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/testcases/hello.c b/tests/testcases/hello.c deleted file mode 100644 index 86cbacadaa1..00000000000 --- a/tests/testcases/hello.c +++ /dev/null @@ -1,15 +0,0 @@ -/* What `c_hello` compiles with the toolchain's clang and runs on ToyOS. */ -#include -#include -#include - -int main(void) { - const char *who = "ToyOS"; - char *copy = malloc(strlen(who) + 1); - if (copy == NULL) - return 1; - strcpy(copy, who); - printf("hello from clang, on %s: %d * %d = %d\n", copy, 6, 7, 6 * 7); - free(copy); - return 0; -} diff --git a/tests/toolkitcase/system.toml b/tests/toolkitcase/system.toml deleted file mode 100644 index 7f7649f8be1..00000000000 --- a/tests/toolkitcase/system.toml +++ /dev/null @@ -1,50 +0,0 @@ -# A desktop with a shell, for the clients the `toolkit_` tests carry and -# launch from it: an unmodified iced app, and the winit and waiter probes. - -assets = ["assets"] - -[boot] -start = ["logd", "blockd", "fsd", "compositor", "terminal"] - -# **Every image that carries a `TOYOS-LOG` partition runs this**, and every -# image does — `every_boot_config_runs_logd` is what refuses one without. -[programs.logd] -syscap = ["logread"] - -[programs.compositor] -service = true -serves = ["compositor"] -receives = ["launcher"] -devices = ["framebuffer", "keyboard", "mouse"] - -[programs.terminal] -provides = ["surface"] -receives = ["compositor", "launcher"] - -[programs.shell] -receives = ["compositor", "surface", "launcher"] - -# `stats` spawns the app and reports its CPU and peak memory once it leaves. -[programs.toybox] -receives = ["compositor", "surface"] - -[symlinks] -"bin/echo" = "/system/bin/toybox" -"bin/stats" = "/system/bin/toybox" - -# The block service: the NVMe controller this machine's DATA is on, driven -# from userland through its claim. Started again when it ends; the claim goes -# back with the process and is minted again for the next. -[programs.blockd] -service = true -restart = true -serves = ["block"] -devices = ["pci:1b36:0010"] - -# The file servers: DATA, the log and the running slot's volume, one process -# each, serving every program the directories of its role. Started again when -# one ends, on the same ports. -[programs.fsd] -restart = true -roles = ["data", "log", "boot"] -receives = ["block"] diff --git a/tests/toyos-rust-tests/src/bin/blockd_io.rs b/tests/toyos-rust-tests/src/bin/blockd_io.rs deleted file mode 100644 index d0f53a4ae1d..00000000000 --- a/tests/toyos-rust-tests/src/bin/blockd_io.rs +++ /dev/null @@ -1,1040 +0,0 @@ -//! blockd, driven from its client's side and supervised from this process. -//! -//! This binary holds the machine's second NVMe controller's claim the way init -//! holds a service's: it mints the claim, starts `/system/bin/blockd` holding -//! it and a port's acceptor, keeps a duplicate of the acceptor so the port -//! outlives any one blockd, and is the only thing that can end or restart it. -//! The disk is crafted, and the verdict on what reached it read back, by -//! `tests/common/blockd.rs` on the host. -//! -//! Roles, by the first argument: -//! - `claims` — the partition refusals by name, the idle ROOT slot written -//! whole through a session and read back, and one holder at a time across -//! two processes; -//! - `holder ` — the second process: opens the slot and says what it -//! was answered; -//! - `bench` — the same bytes through blockd, one request at a time and many; -//! - `hostile-head` — a client that, with a write on the device, moves its -//! completion ring's head a ring behind blockd's tail: the session is -//! ended, and blockd serves the next one; -//! - `reset` — blockd started withholding its second answer: the silence ends -//! in a controller reset, the withheld write is answered not done, and the -//! write acknowledged before it is on the medium after the next flush; -//! - `crash` — a FAT32 volume written through a session, blockd killed with a -//! write on the wire, restarted, and the same volume carried on; -//! - `dma-inside`, `dma-outside`, `dma-revoked`, `dma-after` — the controller -//! aimed by this process at a lent region, past it, and at one taken back; -//! - `dma-pool`, `dma-bound`, `dma-churn` — what a claim may lend: a kernel -//! driver's pool refused, regions lent until the claim's bound refuses the -//! next and a region no run of the window fits, and one region lent and taken -//! back until ten domains' worth of addresses went by; -//! - `dma-residue` — on a boot where no release resets the function, three -//! claims in turn, and none lends where the first one did. -//! - `nothing` — blockd started holding no claim: each first frame, malformed -//! and well-formed, answered as `serve` answers it. - -use std::io::{BufRead, BufReader, Write}; -use std::os::toyos::process::{ChildExt, CommandExt}; -use std::process::{Child, Command, Stdio}; -use std::sync::atomic::Ordering; -use std::sync::mpsc::{self, Receiver}; - -use blockd::nvme::{Controller, Owner}; -use blockd::region::Region; -use blockd::{Error, Outcome, Session, Unsent}; -use toyos::endow::Endowments; -use toyos::poller::{Poller, READABLE}; -use toyos::namespace::{self, Namespace}; -use toyos::port::{self, Acceptor, Connector}; -use toyos::shm::SharedMemory; -use toyos::syscap::SysCap; -use toyos::AsHandle; -use toyos_abi::part::PartGuid; -use toyos_abi::syscall::{self, DeviceType, PciId, SpawnArgs, SyscallError, DEV_PREFIX, SERVE_PREFIX, SYSCAP_LABEL}; -use toyos_blockring::layout::{ARENA, CQ_HEAD, CQ_TAIL, DEPTH, SQ_BASE, SQ_TAIL}; -use toyos_blockring::wire::{self, Refusal}; -use toyos_blockring::{Op, Request, BLOCK_BYTES, MAX_REQUEST_BLOCKS, PORT}; - -const SELF: &str = "/system/bin/test_rs_blockd_io"; - -/// Mirrored in `tests/common/blockd.rs`: the controller blockd drives, QEMU's -/// NVMe under Intel's ids so a claim names it apart from the kernel's. -const BLOCKD: PciId = PciId { vendor: 0x8086, device: 0x5845 }; -/// Mirrored: blockd's disk. -const TARGET: &str = "9C4E2A71-5B3D-4F18-A6E0-2D7C8B1F3E59"; -const FS: &str = "B2D4F6A8-1C3E-4A57-9B0D-E2F4A6C8E0A1"; -const BENCH: &str = "C3E5A7B9-2D4F-4B68-8C1E-F3A5B7D9F1B2"; -const MISALIGNED: &str = "E5A7C9DB-4F6B-4D8A-8E30-B5C7D9FB13D4"; -const MISSTART: &str = "F6B8DAEC-5A7C-4E9B-9F41-C6D8EA0C24E5"; -const ABSENT: &str = "0A1B2C3D-4E5F-4A6B-8C7D-9E0F1A2B3C4D"; -/// Mirrored: the idle slot's length in blocks, and what each block holds. -const TARGET_BLOCKS: u64 = 2048; -/// Mirrored: what the bench moves each way, each side. -const BENCH_BLOCKS: u64 = 8192; -/// Mirrored: the files the crash role writes before it kills blockd, and the -/// bytes each holds. -const FILES: usize = 6; -const FILE_BYTES: usize = 48 * 1024; -/// Mirrored: the file written after the restart, on the same mount. -const AFTER: &str = "/AFTER.BIN"; - -/// Mirrored in `tests/common/blockd.rs`: the most a claim may hold across its -/// grants and what it lends (`pcidev::MAX_GRANT_TOTAL`), one region, and the -/// addresses a device domain has under `iommu-domain-narrow` -/// (`vtd::table::NARROW_BYTES`). -const GRANT_TOTAL: u64 = 32 * 1024 * 1024; -const REGION: usize = 2 * 1024 * 1024; -const NARROW: u64 = 128 * 1024 * 1024; - -fn guid(text: &str) -> [u8; 16] { - PartGuid::parse(text).unwrap_or_else(|| panic!("{text} is no GUID")).0 -} - -/// Mirrored: block `n` of a region `salt` names. -fn pattern(salt: u8, n: u64) -> Vec { - let mut block = vec![0u8; BLOCK_BYTES]; - for (i, byte) in block.iter_mut().enumerate() { - *byte = (n as usize).wrapping_mul(131).wrapping_add(i).wrapping_add(salt as usize) as u8; - } - block[..8].copy_from_slice(&n.to_le_bytes()); - block[8] = salt; - block[9..24].copy_from_slice(b"TOYOS-BLOCKDIO\0"); - block -} - -fn fail(what: String) -> ! { - println!("blockd_io: FAIL {what}"); - let _ = std::io::stdout().flush(); - std::process::exit(1) -} - -/// blockd, held by this process: its claim minted here, from `syscap` when -/// there is one, the port's acceptor kept here, so a blockd can end and -/// another take its place on the same name. What blockd says goes to this -/// process's stdout, a line at a time. -struct Blockd { - syscap: Option, - acceptor: Acceptor, - connector: Connector, - child: Option, - /// Every line the running blockd says. - said: Option>, -} - -impl Blockd { - fn start(args: &[&str]) -> Self { - Self::with(Some(capability()), args) - } - - fn with(syscap: Option, args: &[&str]) -> Self { - let (acceptor, connector) = port::create().unwrap_or_else(|e| fail(format!("no port: {e:?}"))); - let mut blockd = Self { syscap, acceptor, connector, child: None, said: None }; - blockd.spawn(args, false); - blockd - } - - fn names(&self) -> Namespace { - namespace::build().add(PORT, &self.connector).finish().unwrap_or_else(|e| fail(format!("no namespace: {e:?}"))) - } - - /// Mint the claim — waiting out the one the last blockd held — and start - /// a blockd holding it and a duplicate of the acceptor. With - /// `kill_on_withheld`, blockd is killed the moment it says it withheld a - /// write's answer: the write is done on the device, and its session never - /// hears. - fn spawn(&mut self, args: &[&str], kill_on_withheld: bool) { - let mut command = Command::new("/system/bin/blockd"); - if let Some(syscap) = &self.syscap { - let claim: toyos::Device = claim_when_free(syscap); - command.endow(&format!("{DEV_PREFIX}pci:8086:5845"), claim.into_raw().0); - } - let acceptor = toyos_abi::syscall::dup(self.acceptor.as_handle()) - .unwrap_or_else(|e| fail(format!("the acceptor would not duplicate: {e:?}"))); - command.args(args); - command.stdout(Stdio::piped()); - command.endow(&format!("{SERVE_PREFIX}{PORT}"), acceptor.0); - let mut child = command.spawn().unwrap_or_else(|e| fail(format!("blockd did not start: {e}"))); - let out = child.stdout.take().expect("piped"); - let (says, said) = mpsc::channel(); - let mut kill = kill_on_withheld.then(|| { - toyos_abi::syscall::dup(toyos_abi::RawHandle(child.as_raw_handle())) - .unwrap_or_else(|e| fail(format!("blockd's handle would not duplicate: {e:?}"))) - }); - std::thread::spawn(move || { - for line in BufReader::new(out).lines().map_while(Result::ok) { - println!("{line}"); - if line.contains("WITHHELD") { - if let Some(handle) = kill.take() { - let _ = toyos_abi::syscall::process_kill(handle); - println!("blockd_io: blockd killed with the withheld write done on the device"); - } - } - let _ = says.send(line); - } - }); - self.child = Some(child); - self.said = Some(said); - } - - /// Wait, with no deadline, for the running blockd to say a line holding - /// `needle`. - fn says(&self, needle: &str) { - let said = self.said.as_ref().expect("spawned"); - loop { - match said.recv() { - Ok(line) if line.contains(needle) => return, - Ok(_) => {} - Err(_) => fail(format!("blockd ended before it said {needle:?}")), - } - } - } - - /// End the running blockd, if one is, and wait for it to be gone. - fn kill(&mut self) { - if let Some(mut child) = self.child.take() { - let _ = child.kill(); - let _ = child.wait(); - } - } -} - -impl Drop for Blockd { - fn drop(&mut self) { - self.kill(); - } -} - -/// This process's capability, which test-runner endowed. -fn capability() -> SysCap { - Endowments::get().take(SYSCAP_LABEL).unwrap_or_else(|| fail("started with no system capability".into())) -} - -fn open(names: Namespace, text: &str) -> Session { - Session::open(names, PORT, guid(text)).unwrap_or_else(|e| fail(format!("{text} did not open: {e:?}"))) -} - -/// `blocks` blocks of `salt`'s pattern from block 0, cut into requests of the -/// largest size one may be: built before anything is timed. -fn chunks(blocks: u64, salt: u8) -> Vec> { - let per = MAX_REQUEST_BLOCKS as u64; - (0..blocks.div_ceil(per)) - .map(|c| (c * per..(c * per + per).min(blocks)).flat_map(|b| pattern(salt, b)).collect()) - .collect() -} - -/// Write `chunks` from block 0, `in_flight` requests at a time; flush. Answers -/// how many flushes there were: an acknowledged write holds its arena blocks -/// until a flush covers it, so a full arena is where one is asked. -fn write_all(s: &mut Session, chunks: &[Vec], in_flight: usize) -> u32 { - let mut next = 0usize; - let mut lba = 0u64; - let mut outstanding = 0usize; - let mut full = false; - let mut flushes = 0u32; - while next < chunks.len() || outstanding > 0 { - while next < chunks.len() && outstanding < in_flight && !full { - match s.submit_write(lba, &chunks[next]) { - Ok(_) => { - lba += (chunks[next].len() / BLOCK_BYTES) as u64; - next += 1; - outstanding += 1; - } - Err(Unsent::ArenaFull) => full = true, - Err(Unsent::Ended) => fail("blockd ended under a write".into()), - } - } - if outstanding > 0 { - let waited = s.wait(true); - if waited.ended { - fail("blockd ended under a write".into()); - } - for answer in waited.answers { - if answer.outcome != Outcome::Done { - fail(format!("a write was answered {:?}", answer.outcome)); - } - outstanding -= 1; - } - } - if full && outstanding == 0 { - flushed(s); - flushes += 1; - full = false; - } - } - flushed(s); - flushes + 1 -} - -fn flushed(s: &mut Session) { - match s.flush() { - Ok(Outcome::Durable) => {} - other => fail(format!("a flush was answered {other:?}")), - } -} - -/// Read `blocks` from block 0, `in_flight` requests at a time; the blocks, in -/// order. -fn read_all(s: &mut Session, blocks: u64, in_flight: usize) -> Vec { - let per = MAX_REQUEST_BLOCKS as u64; - let mut out = vec![0u8; (blocks * BLOCK_BYTES as u64) as usize]; - let mut next = 0u64; - let mut asked: std::collections::BTreeMap = std::collections::BTreeMap::new(); - while next < blocks || !asked.is_empty() { - while next < blocks && asked.len() < in_flight { - let n = per.min(blocks - next); - match s.submit_read(next, n as u32) { - Ok(ticket) => { - asked.insert(ticket, next); - next += n; - } - Err(_) => break, - } - } - let waited = s.wait(true); - if waited.ended { - fail("blockd ended under a read".into()); - } - for answer in waited.answers { - let first = asked.remove(&answer.ticket).expect("a ticket this asked for"); - let data = match (answer.outcome, answer.data) { - (Outcome::Done, Some(data)) => data, - (outcome, _) => fail(format!("a read at {first} was answered {outcome:?}")), - }; - let at = (first * BLOCK_BYTES as u64) as usize; - out[at..at + data.len()].copy_from_slice(&data); - } - } - out -} - -/// `read` holds `chunks`, block for block. -fn holds(read: &[u8], chunks: &[Vec], what: &str) { - let mut at = 0usize; - for (c, chunk) in chunks.iter().enumerate() { - if read[at..at + chunk.len()] != chunk[..] { - fail(format!("{what}: request {c}'s blocks read back are not what was written")); - } - at += chunk.len(); - } -} - -fn claims() { - let blockd = Blockd::start(&[]); - for (what, text, want) in [ - ("an absent GUID", ABSENT, Refusal::NotFound), - ("the zero GUID", "00000000-0000-0000-0000-000000000000", Refusal::NotFound), - ("a partition not whole blocks long", MISALIGNED, Refusal::Unusable), - ("a partition beginning inside a block", MISSTART, Refusal::Unusable), - ] { - match Session::open(blockd.names(), PORT, guid(text)) { - Err(Error::Refused(got)) if got == want => { - println!("blockd_io: {what} refused with {got:?}") - } - Err(e) => fail(format!("{what} was answered {e:?}, not {want:?}")), - Ok(_) => fail(format!("{what} opened")), - } - } - oversized(&blockd); - let mut slot = open(blockd.names(), TARGET); - if slot.blocks() != TARGET_BLOCKS { - fail(format!("the slot is {} blocks, not {TARGET_BLOCKS}", slot.blocks())); - } - holder(&blockd, "Held"); - let written = chunks(TARGET_BLOCKS, 0x5A); - write_all(&mut slot, &written, 8); - holds(&read_all(&mut slot, TARGET_BLOCKS, 8), &written, "the idle slot"); - println!( - "blockd_io: the idle slot's {TARGET_BLOCKS} blocks written and read back; at most {} \ - requests on the wire", - slot.peak_on_the_wire() - ); - drop(slot); - holder(&blockd, "Opened"); - println!("blockd_io: PASS claims"); -} - -/// An open sending a region longer than a session is refused, and costs the -/// claim nothing: the slot opens after it. -fn oversized(blockd: &Blockd) { - let conn = blockd.names().open(PORT).unwrap_or_else(|e| fail(format!("the port: {e:?}"))); - let region = SharedMemory::create(2 * REGION).unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - let shared = region.share().unwrap_or_else(|e| fail(format!("a second handle: {e:?}"))); - conn.send_bytes_with_handles(&[shared], wire::MSG_OPEN, &guid(TARGET)) - .unwrap_or_else(|e| fail(format!("the open: {e:?}"))); - let header = conn.recv_header().unwrap_or_else(|e| fail(format!("the answer: {e:?}"))); - let mut payload = [0u8; 64]; - let len = conn.recv_bytes(&header, &mut payload).unwrap_or_else(|e| fail(format!("the answer: {e:?}"))); - match (header.msg_type, Refusal::decode(&payload[..len])) { - (wire::MSG_REFUSED, Some(Refusal::Malformed)) => {} - (msg_type, refusal) => fail(format!("a {}-byte region was answered {msg_type} {refusal:?}", 2 * REGION)), - } - println!("blockd_io: a region of {} bytes, longer than a session, refused with Malformed", 2 * REGION); -} - -/// Another process opens the slot through the same port, and must be answered -/// `expect`. -fn holder(blockd: &Blockd, expect: &str) { - let names = blockd.names(); - let mut command = Command::new(SELF); - command.args(["holder", expect]); - command.endow("block-ns", names.into_raw().0); - let status = command - .spawn() - .and_then(|mut c| c.wait()) - .unwrap_or_else(|e| fail(format!("the second client did not run: {e}"))); - if status.code() != Some(0) { - fail(format!("the second client, expecting {expect}, exited {status:?}")); - } -} - -fn holder_role(expect: &str) { - let names: Namespace = Endowments::get().take("block-ns").unwrap_or_else(|| fail("no namespace".into())); - let got = match Session::open(names, PORT, guid(TARGET)) { - Ok(_) => "Opened".to_string(), - Err(Error::Refused(r)) => format!("{r:?}"), - Err(e) => format!("{e:?}"), - }; - if got != expect { - fail(format!("a second client was answered {got}, not {expect}")); - } - println!("blockd_io: a second client of the slot refused with {got}, as expected"); -} - -/// The same bytes through blockd, one request at a time and then as many as -/// the arena holds. -fn bench() { - let blockd = Blockd::start(&[]); - let mut s = open(blockd.names(), BENCH); - let mut runs = Vec::new(); - for (salt, in_flight) in [(0x3D, 1usize), (0x3C, 15)] { - let written = chunks(BENCH_BLOCKS, salt); - let flushes = write_all(&mut s, &written, in_flight); - let read = read_all(&mut s, BENCH_BLOCKS, in_flight); - holds(&read, &written, "blockd's bench partition"); - runs.push(format!("{in_flight} in flight with {flushes} Flushes")); - } - println!( - "blockd_io: bench {} MiB each way through blockd {}; at most {} requests on the wire", - BENCH_BLOCKS * BLOCK_BYTES as u64 / (1024 * 1024), - runs.join("; "), - s.peak_on_the_wire() - ); - println!("blockd_io: PASS bench"); -} - -fn reset() { - let blockd = Blockd::start(&["--silence-write", "2"]); - let mut s = open(blockd.names(), BENCH); - let first = pattern(0x71, 0); - match s.write(0, &first) { - Ok(Outcome::Done) => {} - other => fail(format!("the first write was answered {other:?}")), - } - match s.write(1, &pattern(0x71, 1)) { - Ok(Outcome::Device) => {} - other => fail(format!("the withheld write was answered {other:?}, not Device")), - } - println!("blockd_io: the withheld write was answered Device"); - // The reset may have dropped the device's cache: the write acknowledged - // before it goes out again, inside this flush. - flushed(&mut s); - println!( - "blockd_io: {} acknowledged writes no flush had covered went out again after the reset", - s.reissued() - ); - match s.read(0, 1) { - Ok((Outcome::Done, Some(data))) if data == first => {} - other => fail(format!("block 0 read back after the reset: {:?}", other.map(|(o, _)| o))), - } - println!("blockd_io: PASS reset"); -} - -/// A client whose write is on the device moves its completion ring's head a -/// ring's depth behind the tail blockd published, so the answer finds no room: -/// blockd ends that session, and serves the next. -fn hostile_head() { - let blockd = Blockd::start(&["--silence-write", "1"]); - let region = Region::create().unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - let conn = blockd.names().open(PORT).unwrap_or_else(|e| fail(format!("the port: {e:?}"))); - let shared = region.share().unwrap_or_else(|e| fail(format!("a second handle: {e:?}"))); - conn.send_bytes_with_handles(&[shared], wire::MSG_OPEN, &guid(TARGET)) - .unwrap_or_else(|e| fail(format!("the open: {e:?}"))); - let header = conn.recv_header().unwrap_or_else(|e| fail(format!("the answer: {e:?}"))); - let mut payload = [0u8; 64]; - conn.recv_bytes(&header, &mut payload).unwrap_or_else(|e| fail(format!("the answer: {e:?}"))); - if header.msg_type != wire::MSG_OPENED { - fail(format!("the slot's open was answered {}", header.msg_type)); - } - // A write of the slot's block 0 from arena block 0 under tag 1, as the - // words a client puts on the request ring, published and rung. - let words = region.words(); - let run = ARENA.run(0, 1).unwrap_or_else(|| fail("arena block 0 is no run".into())); - let write = Request { op: Op::Write { run, lba: 0 }, tag: 1 }; - for (at, word) in write.encode().into_iter().enumerate() { - words[SQ_BASE + at].store(word, Ordering::Relaxed); - } - words[SQ_TAIL].store(1, Ordering::Release); - conn.write_nonblock(&[1]).unwrap_or_else(|e| fail(format!("the doorbell: {e:?}"))); - blockd.says("WITHHELD"); - let tail = words[CQ_TAIL].load(Ordering::Acquire); - words[CQ_HEAD].store(tail.wrapping_sub(DEPTH), Ordering::Release); - conn.write_nonblock(&[1]).unwrap_or_else(|e| fail(format!("the doorbell: {e:?}"))); - println!("blockd_io: with a write on the device, the client moved its completion head {DEPTH} behind the tail"); - blockd.says("closed after"); - println!("blockd_io: blockd ended the session and runs on"); - let mut next = open(blockd.names(), TARGET); - let block = pattern(0x6B, 0); - match next.write(0, &block) { - Ok(Outcome::Done) => {} - other => fail(format!("the next session's write was answered {other:?}")), - } - flushed(&mut next); - match next.read(0, 1) { - Ok((Outcome::Done, Some(data))) if data == block => {} - other => fail(format!("the next session read back {:?}", other.map(|(o, _)| o))), - } - println!("blockd_io: the next session wrote, flushed and read back the slot's block 0"); - println!("blockd_io: PASS hostile-head"); -} - -/// The FAT32 volume's device: a session, with blockd's supervisor beside it. -struct Volume { - blockd: Blockd, - session: Session, - /// What a write that was not done was answered, the last time one was. - refused: Option, -} - -impl Volume { - fn read_block(&mut self, lba: u64) -> Result, toyos_fat32::IoError> { - match self.session.read(lba, 1) { - Ok((Outcome::Done, Some(data))) => Ok(data), - _ => Err(toyos_fat32::IoError::Device), - } - } - - fn write_block(&mut self, lba: u64, data: &[u8]) -> Result<(), toyos_fat32::IoError> { - match self.session.write(lba, data) { - Ok(Outcome::Done) => Ok(()), - Ok(outcome) => { - self.refused = Some(outcome); - Err(toyos_fat32::IoError::Device) - } - Err(_) => Err(toyos_fat32::IoError::Device), - } - } - - /// The session has ended: wait for it to say so, start a blockd in the - /// last one's place, and reopen. - fn restart(&mut self, args: &[&str], kill_on_withheld: bool) { - while !self.session.wait(true).ended {} - self.blockd.kill(); - self.blockd.spawn(args, kill_on_withheld); - self.session.reconnect().unwrap_or_else(|e| fail(format!("the session did not reopen: {e:?}"))); - } -} - -impl toyos_fat32::BlockAccess for Volume { - fn capacity(&self) -> u64 { - self.session.blocks() * BLOCK_BYTES as u64 - } - - fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), toyos_fat32::IoError> { - let mut done = 0usize; - while done < buf.len() { - let at = offset + done as u64; - let lba = at / BLOCK_BYTES as u64; - let within = (at % BLOCK_BYTES as u64) as usize; - let n = (BLOCK_BYTES - within).min(buf.len() - done); - let block = self.read_block(lba)?; - buf[done..done + n].copy_from_slice(&block[within..within + n]); - done += n; - } - Ok(()) - } - - fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<(), toyos_fat32::IoError> { - let mut done = 0usize; - while done < buf.len() { - let at = offset + done as u64; - let lba = at / BLOCK_BYTES as u64; - let within = (at % BLOCK_BYTES as u64) as usize; - let n = (BLOCK_BYTES - within).min(buf.len() - done); - let mut block = if n == BLOCK_BYTES { vec![0u8; BLOCK_BYTES] } else { self.read_block(lba)? }; - block[within..within + n].copy_from_slice(&buf[done..done + n]); - self.write_block(lba, &block)?; - done += n; - } - Ok(()) - } - - fn flush(&mut self) -> Result<(), toyos_fat32::IoError> { - match self.session.flush() { - Ok(Outcome::Durable) => Ok(()), - _ => Err(toyos_fat32::IoError::Device), - } - } -} - -/// Mirrored: file `i`'s bytes. -fn file_bytes(i: usize) -> Vec { - (0..FILE_BYTES).map(|b| (b.wrapping_mul(7) ^ i.wrapping_mul(0x3D)) as u8).collect() -} - -fn file_name(i: usize) -> String { - format!("/F{i}.BIN") -} - -fn write_file(fs: &mut toyos_fat32::Fat32, name: &str, bytes: &[u8]) -> Result<(), toyos_fat32::Error> { - let mut f = fs.create(name, toyos_fat32::FatTime::EPOCH)?; - fs.write(&mut f, 0, bytes)?; - fs.flush_meta(&mut f, toyos_fat32::FatTime::EPOCH)?; - fs.sync() -} - -fn read_file(fs: &mut toyos_fat32::Fat32, name: &str) -> Result, toyos_fat32::Error> { - let mut f = fs.open(name)?; - let mut buf = vec![0u8; f.len() as usize]; - let n = fs.read(&mut f, 0, &mut buf)?; - buf.truncate(n); - Ok(buf) -} - -fn crash() { - let blockd = Blockd::start(&[]); - let session = open(blockd.names(), FS); - let volume = Volume { blockd, session, refused: None }; - let mut fs = toyos_fat32::Fat32::mount(volume).unwrap_or_else(|e| fail(format!("FS did not mount: {e:?}"))); - for i in 0..FILES { - write_file(&mut fs, &file_name(i), &file_bytes(i)) - .unwrap_or_else(|e| fail(format!("{} was not written: {e:?}", file_name(i)))); - } - println!("blockd_io: {FILES} files written and flushed"); - - // A blockd that will do the third write it is asked for and never say so, - // and is killed the moment it has: two writes acknowledged and not yet - // flushed, one done on the device and refused, when it dies. - let v = fs.device(); - v.blockd.kill(); - v.restart(&["--silence-write", "3"], true); - let doomed = file_name(FILES); - match write_file(&mut fs, &doomed, &file_bytes(FILES)) { - Err(e) => println!("blockd_io: {doomed} refused when blockd died under it: {e:?}"), - Ok(()) => fail(format!("{doomed} was written with blockd killed under it")), - } - match fs.device().refused { - Some(Outcome::Refused) => { - println!("blockd_io: the write the device did and blockd died before answering was answered Refused") - } - other => fail(format!("the write on the wire when blockd died was answered {other:?}")), - } - - // A new blockd on the same port, the same session reopened over the same - // region: the two writes the old one acknowledged and no flush covered go - // out again first. - fs.device().restart(&[], false); - println!("blockd_io: blockd restarted and the session reopened"); - - // The same mount carries on: its first mutating call re-drives the repair - // the refused write left, so the volume is whole again before anything new - // lands on it. - write_file(&mut fs, AFTER, &file_bytes(99)).unwrap_or_else(|e| fail(format!("{AFTER} after the restart: {e:?}"))); - println!( - "blockd_io: {} acknowledged writes no flush had covered went out again after the restart", - fs.device().session.reissued() - ); - for i in 0..FILES { - let got = read_file(&mut fs, &file_name(i)).unwrap_or_else(|e| fail(format!("{}: {e:?}", file_name(i)))); - if got != file_bytes(i) { - fail(format!("{} does not read back after the restart", file_name(i))); - } - } - // And a mount that saw nothing of the crash reads the same. - let volume = fs.into_device(); - let mut fresh = toyos_fat32::Fat32::mount(volume).unwrap_or_else(|e| fail(format!("remount: {e:?}"))); - for (name, want) in (0..FILES).map(|i| (file_name(i), file_bytes(i))).chain([(AFTER.to_string(), file_bytes(99))]) { - let got = read_file(&mut fresh, &name).unwrap_or_else(|e| fail(format!("{name} on a fresh mount: {e:?}"))); - if got != want { - fail(format!("{name} does not read back on a fresh mount")); - } - } - println!("blockd_io: every acknowledged file, and one written after the restart, reads back on a fresh mount"); - println!("blockd_io: PASS crash"); -} - -/// A controller this process drives itself, and a region to lend it filled -/// with a sentinel. -fn aim() -> (Controller, SharedMemory) { - let dev: toyos::PciDev = capability().claim_pci(BLOCKD).unwrap_or_else(|e| fail(format!("claim: {e:?}"))); - let ctrl = Controller::open(dev, None).unwrap_or_else(|e| fail(format!("the controller: {e}"))); - let mut region = SharedMemory::create(2 * 1024 * 1024).unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - region.as_mut_slice().fill(0xA5); - (ctrl, region) -} - -/// A spawn from `image`'s first `len` bytes, with an argv no process can read. -fn spawn_unreadable_argv(image: toyos::RawHandle, len: u64) -> Result { - // SAFETY: argv names the null page, which the kernel refuses to read, and - // every other pointer is null with a zero length. - unsafe { - syscall::spawn(&SpawnArgs { - argv_ptr: 8, - argv_len: 8, - slot_map_ptr: 0, - slot_map_count: 0, - env_ptr: 0, - env_len: 0, - endow_ptr: 0, - endow_count: 0, - labels_ptr: 0, - labels_len: 0, - cwd_ptr: 0, - cwd_len: 0, - image: image.0 as u64, - image_len: len, - }) - } -} - -/// Device block 0, the disk's protective MBR, ends 0x55 0xAA. -fn is_block_zero(bytes: &[u8]) -> bool { - bytes[510] == 0x55 && bytes[511] == 0xAA -} - -fn dma(role: &str) { - let (mut ctrl, region) = aim(); - let mapping = ctrl.claim().dma_map(region.as_handle()).unwrap_or_else(|e| fail(format!("dma_map: {e:?}"))); - println!("blockd_io: region lent at device address {:#x}, {} bytes", mapping.device_addr, mapping.bytes); - match role { - "dma-inside" | "dma-after" => { - match transfer(&mut ctrl, 0, mapping.device_addr) { - Ok(true) => {} - other => fail(format!("a read into the lent region was answered {other:?}")), - } - if !is_block_zero(region.as_slice()) { - fail("the lent region does not hold device block 0".into()); - } - println!("blockd_io: the device read block 0 into the lent region"); - // Only memory the kernel allocated is lent, and once: the - // function's own register window lent to it would aim the device - // at a device, and a region lent twice is two grants of one page. - let bar = ctrl.claim().map_bar(0, 4096).unwrap_or_else(|e| fail(format!("the BAR: {e:?}"))); - match ctrl.claim().dma_map(bar.as_handle()) { - Err(SyscallError::InvalidArgument) => {} - other => fail(format!("lending the register window was answered {other:?}")), - } - match ctrl.claim().dma_map(region.as_handle()) { - Err(SyscallError::InvalidArgument) => {} - other => fail(format!("lending the region a second time was answered {other:?}")), - } - println!("blockd_io: a register window, and a region already lent, are refused with InvalidArgument"); - // Nor is a register window a program: the spawn refuses it before - // it reads anything else, where a region's is taken and the spawn - // goes on to refuse the argv. - match spawn_unreadable_argv(bar.as_handle(), 4096) { - Err(SyscallError::InvalidArgument) => {} - other => fail(format!("a spawn from the register window was answered {other:?}")), - } - match spawn_unreadable_argv(region.as_handle(), 4096) { - Err(SyscallError::BadAddress) => {} - other => fail(format!("a spawn from the region with an unreadable argv was answered {other:?}")), - } - println!("blockd_io: a spawn from a register window is refused with InvalidArgument, and one from a region reaches its argv"); - } - "dma-outside" => { - let past = mapping.device_addr + mapping.bytes; - println!("blockd_io: aiming the device at {past:#x}, the first address past the lent region"); - refused(&mut ctrl, ®ion, past, "past the lent region"); - } - "dma-revoked" => { - ctrl.claim().dma_unmap(mapping.device_addr).unwrap_or_else(|e| fail(format!("dma_unmap: {e:?}"))); - println!( - "blockd_io: aiming the device at {:#x}, where the region was lent until it was taken back", - mapping.device_addr - ); - refused(&mut ctrl, ®ion, mapping.device_addr, "at the region taken back"); - } - _ => unreachable!(), - } - println!("blockd_io: PASS {role}"); -} - -/// A read aimed at `at`, which the function's domain does not map: the unit -/// refuses it, the claim answers the refusal from then on, and `region` — -/// still this process's — holds its sentinel. -/// -/// **What the device answers is not the verdict**: QEMU's NVMe completes the -/// command with success when the unit drops its data, and the completion can -/// land before the fault takes the function off the bus. The verdict is the -/// unit's, read three ways: the claim's refusal here, the region untouched -/// here, and the fault record the host reads at `at`. -fn refused(ctrl: &mut Controller, region: &SharedMemory, at: u64, what: &str) { - let answered = transfer(ctrl, 0, at); - println!("blockd_io: the device answered a read aimed {what} with {answered:?}"); - await_refusal(ctrl); - if !region.as_slice().iter().all(|b| *b == 0xA5) { - fail(format!("a read aimed {what} changed the lent region")); - } - println!( - "blockd_io: the unit refused a read aimed {what}, the claim answers the refusal, and the \ - region is untouched" - ); -} - -/// One read of device block `block` into device address `at`, waited for with -/// no deadline on the claim's interrupt, with nothing else on the device: -/// whether the device did it, or the claim's refusal once the unit refused the -/// function an access — which is how a read aimed outside the function's -/// domain ends. -fn transfer(ctrl: &mut Controller, block: u64, at: u64) -> Result { - if ctrl.busy() != 0 { - fail("a waited read beside other commands".into()); - } - ctrl.submit_io(false, block, 1, at, Owner::Driver); - let poller = Poller::new(1); - let mut done = Vec::new(); - loop { - ctrl.reap(&mut done); - if let Some(d) = done.pop() { - return Ok(d.ok); - } - poller.watch(ctrl.claim(), READABLE, 0); - poller.wait(1, u64::MAX, |_| {}); - ctrl.take_interrupt()?; - } -} - -/// Wait, with no deadline, for the claim to answer with the unit's refusal — -/// what every call on a claim answers once its function was refused an access. -/// A unit that never refuses leaves this waiting, and the harness ceiling reds -/// it. -fn await_refusal(ctrl: &Controller) { - let poller = Poller::new(1); - while ctrl.take_interrupt() != Err(SyscallError::Io) { - poller.watch(ctrl.claim(), READABLE, 0); - poller.wait(1, u64::MAX, |_| {}); - } -} - -/// A kernel driver's own pool is not the holder's to lend, though it is -/// ordinary memory the holder may map: virtio-sound's, claimed here since no -/// soundd runs on this boot. -fn dma_pool() { - let syscap = capability(); - let sound: toyos::VirtioSoundDev = syscap - .claim(DeviceType::VirtioSound) - .unwrap_or_else(|e| fail(format!("virtio-sound's claim: {e:?}"))); - let info = sound.info().unwrap_or_else(|e| fail(format!("virtio-sound's description: {e:?}"))); - let dev: toyos::PciDev = syscap.claim_pci(BLOCKD).unwrap_or_else(|e| fail(format!("claim: {e:?}"))); - match dev.dma_map(info.dma) { - Err(SyscallError::InvalidArgument) => {} - other => fail(format!("lending virtio-sound's pool was answered {other:?}")), - } - println!("blockd_io: virtio-sound's pool, a kernel driver's own, is refused with InvalidArgument"); - println!("blockd_io: PASS dma-pool"); -} - -/// Regions lent beside the claim's own grant until the claim's bound refuses -/// the next, and exactly as many as the bound leaves room for. -fn dma_bound() { - let dev: toyos::PciDev = capability().claim_pci(BLOCKD).unwrap_or_else(|e| fail(format!("claim: {e:?}"))); - let grant = dev.dma_alloc(REGION as u64).unwrap_or_else(|e| fail(format!("the claim's grant: {e:?}"))); - match dev.dma_unmap(grant.device_addr) { - Err(SyscallError::NotFound) => {} - other => fail(format!("taking the claim's own grant back as a lent region was answered {other:?}")), - } - println!("blockd_io: the claim's own grant is not taken back as a lent region: NotFound"); - let room = ((GRANT_TOTAL - REGION as u64) / REGION as u64) as usize; - let mut lent = Vec::new(); - let refused = loop { - let region = SharedMemory::create(REGION).unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - match dev.dma_map(region.as_handle()) { - Ok(mapping) if mapping.bytes == REGION as u64 => lent.push((region, mapping)), - Ok(mapping) => fail(format!("a {REGION}-byte region was lent as {} bytes", mapping.bytes)), - Err(why) => break why, - } - if lent.len() > room + 1 { - fail(format!("{} regions lent past a bound that has room for {room}", lent.len())); - } - }; - if refused != SyscallError::ResourceExhausted || lent.len() != room { - fail(format!("{} regions lent and the next answered {refused:?}, not {room} and ResourceExhausted", lent.len())); - } - // One taken back is room for one more, and no more than one. - let (_, first) = lent.remove(0); - dev.dma_unmap(first.device_addr).unwrap_or_else(|e| fail(format!("dma_unmap: {e:?}"))); - for (n, want) in [(1, true), (2, false)] { - let region = SharedMemory::create(REGION).unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - match (dev.dma_map(region.as_handle()), want) { - (Ok(mapping), true) => lent.push((region, mapping)), - (Err(SyscallError::ResourceExhausted), false) => {} - (other, _) => fail(format!("lend {n} after one was taken back was answered {other:?}")), - } - } - println!( - "blockd_io: {room} regions of {REGION} bytes lent beside the claim's own grant, the next refused \ - with ResourceExhausted, and one taken back made room for one more" - ); - // Room the bound allows and the window has in no one run: leaves 0, 2, 4 - // and 15 free is 8 MiB, and no two of them touch. - let leaf = REGION as u64; - let window = lent.iter().map(|(_, mapping)| mapping.device_addr).min().expect("regions were lent"); - if lent.iter().any(|(_, mapping)| mapping.device_addr == window + 15 * leaf) { - fail(format!("the window's last leaf, {:#x}, was lent though the bound had no room for it", window + 15 * leaf)); - } - for n in [0, 2, 4] { - let at = window + n * leaf; - let index = lent - .iter() - .position(|(_, mapping)| mapping.device_addr == at) - .unwrap_or_else(|| fail(format!("no region was lent at the window's leaf {n}, {at:#x}"))); - let (_, mapping) = lent.remove(index); - dev.dma_unmap(mapping.device_addr).unwrap_or_else(|e| fail(format!("dma_unmap of leaf {n}: {e:?}"))); - } - let wide = SharedMemory::create(2 * REGION).unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - match dev.dma_map(wide.as_handle()) { - Err(SyscallError::ResourceExhausted) => {} - other => fail(format!( - "a {}-byte region, with leaves 0, 2, 4 and 15 of the window free, was answered {other:?}", - 2 * REGION - )), - } - println!( - "blockd_io: with leaves 0, 2, 4 and 15 of the window free, a {}-byte region is refused with \ - ResourceExhausted", - 2 * REGION - ); - println!("blockd_io: PASS dma-bound"); -} - -/// One region lent and taken back until ten times the domain's addresses went -/// by: none of it spends an address, and the device still reads into it. -fn dma_churn() { - let (mut ctrl, region) = aim(); - let rounds = (10 * NARROW).div_ceil(REGION as u64); - let mut addresses = std::collections::BTreeSet::new(); - for round in 0..rounds { - let mapping = ctrl - .claim() - .dma_map(region.as_handle()) - .unwrap_or_else(|e| fail(format!("lend {round} of {rounds} was answered {e:?}"))); - addresses.insert(mapping.device_addr); - ctrl.claim() - .dma_unmap(mapping.device_addr) - .unwrap_or_else(|e| fail(format!("taking lend {round} back was answered {e:?}"))); - } - if addresses.len() as u64 > GRANT_TOTAL / REGION as u64 { - fail(format!("{rounds} lends of one region were placed at {} device addresses", addresses.len())); - } - let mapping = ctrl.claim().dma_map(region.as_handle()).unwrap_or_else(|e| fail(format!("dma_map: {e:?}"))); - match transfer(&mut ctrl, 0, mapping.device_addr) { - Ok(true) if is_block_zero(region.as_slice()) => {} - other => fail(format!("a read into the region after the churn was answered {other:?}")), - } - println!( - "blockd_io: {rounds} lends of a {REGION}-byte region, each taken back, {} MiB in all, at {} \ - device address(es); the device then read block 0 into it", - rounds * REGION as u64 / (1024 * 1024), - addresses.len() - ); - println!("blockd_io: PASS dma-churn"); -} - -/// The controller's claim once the last holder's release has run, waited for with -/// no deadline: a process's end is published before the release its handles -/// queued has run (`issues/kernel/deferred-release-outlives-its-syscall.md`). -fn claim_when_free(syscap: &SysCap) -> T { - loop { - match syscap.claim_pci(BLOCKD) { - // A pace, so the CPU this runs on can reach the idle loop that - // drains the release. - Err(SyscallError::AlreadyExists) => std::thread::sleep(std::time::Duration::from_millis(1)), - Ok(claim) => return claim, - Err(e) => fail(format!("the controller's claim was refused: {e:?}")), - } - } -} - -/// On a boot where no release resets the function, the first claim's lent -/// address stays where the function may be aimed: the second claim lends -/// elsewhere and ends holding nothing, and the third still lends elsewhere. -fn dma_residue() { - let syscap = capability(); - let first = { - let dev: toyos::PciDev = claim_when_free(&syscap); - let mut ctrl = Controller::open(dev, None).unwrap_or_else(|e| fail(format!("the controller: {e}"))); - let mut region = SharedMemory::create(REGION).unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - region.as_mut_slice().fill(0xA5); - let mapping = ctrl.claim().dma_map(region.as_handle()).unwrap_or_else(|e| fail(format!("dma_map: {e:?}"))); - match transfer(&mut ctrl, 0, mapping.device_addr) { - Ok(true) if is_block_zero(region.as_slice()) => {} - other => fail(format!("claim 1's read into its lent region was answered {other:?}")), - } - mapping.device_addr - }; - println!("blockd_io: claim 1 lent a region at {first:#x}, the device read into it, and the claim ended holding it"); - for n in [2, 3] { - let dev: toyos::PciDev = claim_when_free(&syscap); - let region = SharedMemory::create(REGION).unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - let mapping = dev.dma_map(region.as_handle()).unwrap_or_else(|e| fail(format!("claim {n}'s dma_map: {e:?}"))); - if mapping.device_addr == first { - fail(format!("claim {n} lent a region at {first:#x}, where the unreset function was left aimed")); - } - dev.dma_unmap(mapping.device_addr).unwrap_or_else(|e| fail(format!("claim {n}'s dma_unmap: {e:?}"))); - println!("blockd_io: claim {n} lent at {:#x}, not {first:#x}, and ended holding nothing", mapping.device_addr); - } - println!("blockd_io: PASS dma-residue"); -} - -/// blockd started holding no controller answers a connection's first frame as -/// it answers every other: the malformed refused as such, a listing empty and -/// an open `NotFound`. -fn nothing() { - let blockd = Blockd::with(None, &[]); - let names = blockd.names(); - let region = || { - let region = SharedMemory::create(REGION).unwrap_or_else(|e| fail(format!("a region: {e:?}"))); - vec![region.share().unwrap_or_else(|e| fail(format!("a second handle: {e:?}")))] - }; - let absent = guid(ABSENT); - let malformed = Some(Refusal::Malformed); - for (what, msg_type, payload, handles, answered, refusal) in [ - ("a listing that carries a payload", wire::MSG_LIST, &[0u8; 4][..], vec![], wire::MSG_REFUSED, malformed), - ("an open with no region", wire::MSG_OPEN, &absent[..], vec![], wire::MSG_REFUSED, malformed), - ("an open whose GUID is short", wire::MSG_OPEN, &absent[..8], region(), wire::MSG_REFUSED, malformed), - ("a listing", wire::MSG_LIST, &[][..], vec![], wire::MSG_LISTED, None), - ("an open", wire::MSG_OPEN, &absent[..], region(), wire::MSG_REFUSED, Some(Refusal::NotFound)), - ] { - let conn = names.open(PORT).unwrap_or_else(|e| fail(format!("the port: {e:?}"))); - conn.send_bytes_with_handles(&handles, msg_type, payload).unwrap_or_else(|e| fail(format!("{what}: {e:?}"))); - let header = conn.recv_header().unwrap_or_else(|e| fail(format!("{what}'s answer: {e:?}"))); - let mut answer = [0u8; 64]; - let len = conn.recv_bytes(&header, &mut answer).unwrap_or_else(|e| fail(format!("{what}'s answer: {e:?}"))); - let got = Refusal::decode(&answer[..len]); - if header.msg_type != answered || got != refusal || (refusal.is_none() && len != 0) { - fail(format!("{what} was answered {} {got:?} in {len} bytes, not {answered} {refusal:?}", header.msg_type)); - } - println!("blockd_io: with no controller, {what} was answered {answered} {refusal:?}"); - } - drop(blockd); - println!("blockd_io: PASS nothing"); -} - -fn main() { - let args: Vec = std::env::args().collect(); - match args.get(1).map(String::as_str) { - Some("nothing") => nothing(), - Some("claims") => claims(), - Some("holder") => holder_role(args.get(2).map_or("", String::as_str)), - Some("bench") => bench(), - Some("hostile-head") => hostile_head(), - Some("reset") => reset(), - Some("crash") => crash(), - Some(role @ ("dma-inside" | "dma-outside" | "dma-revoked" | "dma-after")) => dma(role), - Some("dma-pool") => dma_pool(), - Some("dma-bound") => dma_bound(), - Some("dma-churn") => dma_churn(), - Some("dma-residue") => dma_residue(), - other => fail(format!("no role {other:?}")), - } -} diff --git a/tests/toyos-rust-tests/src/bin/ccheck.rs b/tests/toyos-rust-tests/src/bin/ccheck.rs index 5871d8ee957..469c95f790b 100644 --- a/tests/toyos-rust-tests/src/bin/ccheck.rs +++ b/tests/toyos-rust-tests/src/bin/ccheck.rs @@ -12,12 +12,6 @@ //! run under the case's own name and a host reading the stick can say which of //! a hundred and nineteen failed. `argv[0]` is what this reads to know which //! one it is. -//! -//! It is also a *better* comparison than the host's. The host reads a console -//! every process on the machine shares, and has to take the other writers' -//! lines out before comparing (`common::console::c_verdict`); this reads one -//! pipe that only the case can write to, so there is nothing to filter and no -//! line that can be attributed wrongly. use std::io::Read; use std::process::{Command, Stdio}; @@ -92,13 +86,9 @@ fn run() -> i32 { eprintln!("ccheck: {case}: the case exited {:?}", status.code()); return code::CASE_FAILED; } - // **The host's rule, and it is one line there too.** - // `tests/common/console.rs`'s `verdict` compares `mine.trim_end()` against - // `expected.trim_end()`, so a case that ends its output with a newline and - // an expectation that does not are the same answer — six of the corpus's - // cases are exactly that pair, in one direction or the other. Spelled here - // because a guest binary cannot link the harness, and held to the host's - // by `the_two_comparisons_use_one_rule`. + // A case that ends its output with a newline and an expectation that does + // not are the same answer — six of the corpus's cases are exactly that + // pair, in one direction or the other. let got = trim_end(&got); let expected = trim_end(&expected); if got != expected { diff --git a/tests/toyos-rust-tests/src/bin/compositor_client_death.rs b/tests/toyos-rust-tests/src/bin/compositor_client_death.rs deleted file mode 100644 index 018b96be915..00000000000 --- a/tests/toyos-rust-tests/src/bin/compositor_client_death.rs +++ /dev/null @@ -1,256 +0,0 @@ -//! The desktop must survive a client that dies, and one that asks for -//! something the kernel will refuse on its behalf. -//! -//! The owner's machine lost its whole desktop to this: doom aborted, and three -//! seconds later the compositor granted a resized window's buffer to it — -//! `grant_shared` answered `InvalidArgument` for a pid the process table no -//! longer had, `SharedMemory::grant` was infallible over that, and every other -//! window went with it. `exit: compositor code=101`. There is no grant left to -//! be infallible over: a buffer travels as a handle and a client that has gone -//! is a refused send. -//! -//! Six cases. The first is that one; the next four are the same shape found -//! by reading for it — places where a message from any client reached a -//! syscall or a buffer whose refusal the compositor was not prepared to hear. -//! -//! The fifth is the other side of the same event, and it is the client's: -//! **a window whose connection has gone must let its owner leave.** Nothing -//! else here is about the client's own fate, and that is why it belongs beside -//! them rather than in a test of its own — a window ending has two halves, and -//! each one used to take a process with it. -//! -//! Each case leaves its damage standing and then asks the compositor a -//! question it answers from its dispatch — the host asserts the other half, -//! that the desktop is still painting and that every client dropped on the way -//! was named with its pid. - -use std::io::{BufRead, BufReader}; -use std::os::toyos::process::CommandExt; -use std::process::{exit, Command, Stdio}; - -use toyos::endow; -use toyos::AsHandle; -use toyos::{ipc, Connection}; -use toyos_abi::syscall; -use toyos_abi::RawHandle; -use window::Window; - -const SELF_PATH: &str = "/system/bin/test_rs_compositor_client_death"; - -/// The compositor connection, in the process that finishes the request its -/// creator did not live to send. -const RELAY_SOCKET: RawHandle = RawHandle(3); -/// The other end of the root's pipe, which closes when the creator has been -/// reaped. Nothing is ever read off it but the hang-up. -const RELAY_GO: RawHandle = RawHandle(4); - -/// `timeout_nanos` for a wait with no clock (`syscall::inbox_submit`). -const FOREVER: u64 = u64::MAX; - -fn main() { - match std::env::args().nth(1).as_deref() { - Some("connect") => connect_and_go(), - Some("finish") => finish(), - Some(other) => panic!("unknown role {other:?}"), - None => run(), - } -} - -fn run() { - // **A creator that is gone before its window is asked for, with no race in - // it.** `accept` names the process that called `connect`, and a connection - // outlives that process — so the pid the compositor grants to here is one - // the kernel has already forgotten. - // - // Racing a dying creator against the compositor's own dispatch is what - // this used to do, and under a loaded host the compositor won all eight - // heats and the run proved nothing. Instead the request is *completed by a - // third process*: the creator hands its socket to a grandchild and exits, - // this process reaps it — which is what takes the pid out of the process - // table — and only then closes the pipe that releases the grandchild to - // send the frame. Every step waits on the one before it. - let mut creator = Command::new(SELF_PATH) - .arg("connect") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .unwrap_or_else(|e| fail(&format!("[a reaped creator] spawn failed: {e}"))); - let go = creator.stdin.take().expect("the creator's stdin"); - let mut said = String::new(); - BufReader::new(creator.stdout.take().expect("the creator's stdout")) - .read_line(&mut said) - .unwrap_or_else(|e| fail(&format!("[a reaped creator] it never connected: {e}"))); - if !said.starts_with("connected") { - fail(&format!("[a reaped creator] the creator said {said:?}")); - } - creator.wait().expect("reap the creator"); - // The reap is what makes the pid unknown; this is what tells the grandchild - // the reap has happened. - drop(go); - probe("a creator reaped before its window"); - - // A window is a connection promoted by its first frame, so a second - // `MSG_CREATE_WINDOW` on one arrives with nothing to promote. The - // compositor read that as its own bug. - waiting("a second create on a live window", "its window"); - let doubled = Window::create(64, 64).expect("a window to send a second create on"); - write_handle(doubled.handle(), &create_frame(), "a second create"); - probe("a second create on a live window"); - - // A commit with no copy begun: there is no region for it to name, so this - // must cost the client its connection and nothing else. - let commit = endow::service("compositor").expect("a connection to commit on"); - ipc::signal(commit.as_handle(), window::MSG_COPY_COMMIT).expect("send the commit"); - probe("a commit with no copy begun"); - - // A copy no region is made for. The length decides how large a region the - // compositor makes, so it is the compositor's to bound rather than the - // client's to choose. - let begin = endow::service("compositor").expect("a connection to begin on"); - begin - .send(window::MSG_COPY_BEGIN, &window::ClipboardShmMsg { len: u32::MAX }) - .expect("send the begin"); - probe("a copy longer than any clipboard"); - - // An inline clipboard one byte past what any client may inline. The - // compositor keeps that one byte, so the frame is refusable here instead of - // being stored as the prefix `ipc::FrameRx` would otherwise hand it. - let over = window::MAX_INLINE_PAYLOAD + 1; - let mut frame = vec![b'x'; 8 + over]; - frame[..4].copy_from_slice(&window::MSG_CLIPBOARD_SET.to_ne_bytes()); - frame[4..8].copy_from_slice(&(over as u32).to_ne_bytes()); - let conn = endow::service("compositor").expect("a connection to over-fill"); - write_handle(conn.as_handle(), &frame, "an over-long inline clipboard"); - probe("an over-long inline clipboard"); - - // The other side of a window ending: the client has to be able to leave. - // `MSG_DESTROY_WINDOW` makes the compositor drop the connection, after - // which the handle is permanently read-ready at EOF — so a `poll_event` that - // did not latch answered `Close` for as long as anybody kept asking, and a - // client draining until `None` never got out. Two calls decide it, and - // the second waits for nothing: a latched window answers `None` at once, - // and an unlatched one reads the end of the stream at once. - let what = "a window closed from the inside"; - waiting(what, "its window"); - let mut ending = Window::create(64, 64).expect("a window to close from the inside"); - ipc::signal(ending.handle(), window::MSG_DESTROY_WINDOW) - .expect("ask the compositor to destroy this window"); - waiting(what, "the window's Close"); - loop { - match ending.poll_event(FOREVER) { - Some(window::Event::Close) => break, - // A frame the compositor had already sent can arrive first. It is - // not what this case is about, and skipping it is not a weakening: - // what follows still has to be close and then nothing. - Some(_) => {} - None => fail(&format!("[{what}] the connection went and the window never said so")), - } - } - waiting(what, "the latched poll answering None"); - if ending.poll_event(FOREVER).is_some() { - fail(&format!( - "[{what}] the poll after Close answered again — a client that drains until None \ - cannot leave" - )); - } - probe(what); - - println!("compositor client death: 6 deaths survived, compositor still serving"); -} - -/// The creator: connect, hand the connection to a process that will outlive -/// this one, and go. -/// -/// Nothing is sent here. The compositor's record of who this connection -/// belongs to is made at `connect`, and that is the only thing this role has -/// to establish before dying. -fn connect_and_go() { - let conn = endow::service("compositor").expect("the compositor is not serving"); - // The kernel clones the handle into the child's table - // (`loader::build_child_handles`), so the socket — and the pipes under it — - // outlive this process. - Command::new(SELF_PATH) - .arg("finish") - .inherit_handle(RELAY_SOCKET.0, conn.as_handle().0) - .inherit_handle(RELAY_GO.0, 0) - .spawn() - .expect("spawn the process that finishes the request"); - println!("connected"); -} - -/// The grandchild: send the request its creator never sent, once that creator -/// has been reaped. -fn finish() { - let mut byte = [0u8; 1]; - // The hang-up is the signal and the only signal: the root closes its end - // after `wait` returns, and `wait` returning is the pid leaving the - // process table. - while let Ok(1) = syscall::read(RELAY_GO, &mut byte) {} - write_handle(RELAY_SOCKET, &create_frame(), "finish"); - - // **The answer is the non-vacuity witness, and it changed sides.** The - // compositor used to say "the process behind it has exited" here, because - // it granted the buffer to the pid `accept` reported and the kernel had - // forgotten that pid. There is no pid and no grant: the buffer is a handle - // sent over this connection, which is alive because this process holds it. - // So the request is *served*, and the line that proves the compositor met - // it is the answer rather than a refusal. - let header = ipc::recv_header(RELAY_SOCKET).expect("the compositor answered"); - let what = if header.msg_type == window::MSG_WINDOW_CREATED { "a window" } else { "nothing" }; - // Stderr, because stdout is the pipe the root read one line off and let go - // of: this process outlives the reader of its own stdout, and stderr is the - // console both it and the compositor already share. - eprintln!("a reaped creator's connection still got {what}"); -} - -/// A whole `MSG_CREATE_WINDOW` for a 64x64 window, header and payload. -fn create_frame() -> Vec { - let payload_len = core::mem::size_of::(); - let mut frame = vec![0u8; 8 + payload_len]; - frame[..4].copy_from_slice(&window::MSG_CREATE_WINDOW.to_ne_bytes()); - frame[4..8].copy_from_slice(&(payload_len as u32).to_ne_bytes()); - frame[8..12].copy_from_slice(&64u32.to_ne_bytes()); - frame[12..16].copy_from_slice(&64u32.to_ne_bytes()); - frame -} - -/// Every write here fits in the pipe it goes into, so a blocking `write` can -/// only be the compositor's problem, never this binary's. -fn write_handle(handle: toyos_abi::RawHandle, bytes: &[u8], what: &str) { - let mut offset = 0; - while offset < bytes.len() { - match syscall::write(handle, &bytes[offset..]) { - Ok(n) => offset += n, - Err(e) => fail(&format!("[{what}] write failed after {offset} bytes: {e:?}")), - } - } -} - -/// Ask the compositor something it always answers from its dispatch, so an -/// answer proves the event loop reached the end of a pass rather than merely -/// that the process still exists. -fn probe(what: &str) { - let conn: Connection = endow::service("compositor") - .unwrap_or_else(|e| fail(&format!("[{what}] the compositor is not serving: {e:?}"))); - if let Err(e) = ipc::signal(conn.as_handle(), window::MSG_GET_RESOLUTION) { - fail(&format!("[{what}] could not ask the compositor for its resolution: {e:?}")); - } - waiting(what, "the compositor's answer to a probe"); - let header = conn - .recv_header() - .unwrap_or_else(|e| fail(&format!("[{what}] the probe went unanswered: {e:?}"))); - if header.msg_type != window::MSG_RESOLUTION_CHANGED { - fail(&format!("[{what}] the probe was answered with message type {}", header.msg_type)); - } -} - -/// Said before each wait on the compositor: the line a missing event leaves -/// last. -fn waiting(what: &str, awaited: &str) { - println!("compositor client death: [{what}] waiting for {awaited}"); -} - -fn fail(msg: &str) -> ! { - eprintln!("compositor client death: {msg}"); - exit(1); -} diff --git a/tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs b/tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs deleted file mode 100644 index b9c39034512..00000000000 --- a/tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs +++ /dev/null @@ -1,275 +0,0 @@ -//! Needs a live compositor and a host that types GUI+V, which the shared boot -//! does not have — it is in `RUST_SKIP` and `metal_sim_hostile_clipboard` runs -//! it on the metal-sim profile. -//! -//! 1. **A wrong-typed handle.** A pipe end rides the retired region message. -//! The kernel ends whoever maps a pipe as shared memory, so the compositor -//! must refuse the client without ever receiving the handle — and the pipe's -//! writer, queued on the refused connection, must go back to the kernel -//! unused. -//! 2. **A copy that is not UTF-8.** The client commits a region of `0xFF`, and -//! a paste has to be the clipboard from before it. -//! 3. **A region rewritten after its commit.** Once the compositor has closed -//! the connection the region is the client's own again, and a paste has to -//! be what was committed, not what the region holds now. -//! 4. **A copy never committed.** The client holds its region and says nothing; -//! the compositor has to drop it by name. -//! 5. **A second begin.** A connection holding a region may send its commit and -//! nothing else, so a second `MSG_COPY_BEGIN` on it is refused rather than -//! answered with another region. -//! 6. **A begin with bytes past its length.** Refused rather than answered. -//! 7. **A commit with a payload.** Refused, so a paste has to be the clipboard -//! from before it and not the region's text. -//! 8. **A commit on a window.** A commit names a region held, so a window -//! sending one loses its connection. -//! -//! Each case ends with a probe the compositor answers from its dispatch. The -//! host asserts what this side cannot see: no handle fault and no compositor -//! exit in the kernel's records, and the refusals named. - -use std::sync::atomic::Ordering; - -use toyos::endow; -use toyos::ipc; -use toyos::shm::SharedMemory; -use toyos::{AsHandle, Connection}; -use toyos_abi::syscall::{self, SyscallError}; -use toyos_abi::RawHandle; -use window::{Event, Window}; - -// The wire as this client speaks it, spelled here rather than imported: the -// negative control builds this binary against a `window` that predates all -// four. -const RETIRED_CLIPBOARD_SET_SHM: u32 = 10; -const COPY_BEGIN: u32 = 13; -const COPY_COMMIT: u32 = 14; -const COPY_REGION: u32 = 13; -/// The longest copy the compositor makes a region for. -const COPY_LEN: usize = 2 * 1024 * 1024; - -/// The line the host answers with GUI+V, once. -const PASTE_MARKER: &str = "===HOSTILE_CLIPBOARD_PASTE==="; - -const BEFORE: &str = "hostile clipboard: the text before"; -const AFTER: &str = "hostile clipboard: the text after"; - -fn main() { - // First, so it has the focus: GUI+V pastes into the focused window. - waiting("the paste target", "its window"); - let mut target = Window::create_with_title(160, 120, "paste") - .unwrap_or_else(|e| fail("the paste target", &format!("no window: {e}"))); - target.present(); - waiting("the clipboard to start from", "the compositor taking it"); - window::clipboard_set(BEFORE) - .unwrap_or_else(|e| fail("the clipboard to start from", &e.to_string())); - probe("the clipboard to start from"); - - wrong_typed_handle(); - probe("a wrong-typed handle"); - - let what = "a copy that is not UTF-8"; - commit_filled(what, 0xFF); - probe(what); - let first = paste(&mut target, what); - if first != BEFORE.as_bytes() { - let len = first.len(); - fail(what, &format!("the paste was {len} bytes, not the clipboard from before")); - } - - let what = "a region rewritten after its commit"; - let region = commit_filled(what, b'C'); - // Text too, so a read of the region at the paste is pasted rather than - // refused. - fill(®ion, b'D'); - probe(what); - let second = paste(&mut target, what); - if second.len() != COPY_LEN || second.iter().any(|&b| b != b'C') { - fail(what, &describe(&second, b'C')); - } - - let what = "a copy never committed"; - let (conn, _region) = begin_copy(what); - await_hangup(conn.as_handle(), what, "the compositor giving up on the commit"); - probe(what); - - let what = "a second begin on a copy"; - let (conn, _region) = begin_copy(what); - conn.send(COPY_BEGIN, &window::ClipboardShmMsg { len: COPY_LEN as u32 }) - .unwrap_or_else(|e| fail(what, &format!("could not begin again: {e:?}"))); - await_hangup(conn.as_handle(), what, "the compositor's refusal"); - probe(what); - - let what = "a begin with bytes past its length"; - let conn = connect(what); - let mut begin = (COPY_LEN as u32).to_ne_bytes().to_vec(); - begin.extend_from_slice(&[0; 4]); - conn.send_bytes(COPY_BEGIN, &begin) - .unwrap_or_else(|e| fail(what, &format!("could not begin: {e:?}"))); - await_hangup(conn.as_handle(), what, "the compositor's refusal"); - probe(what); - - let what = "a commit with a payload"; - set_inline(what, AFTER); - let (conn, region) = begin_copy(what); - fill(®ion, b'E'); - conn.send_bytes(COPY_COMMIT, &[0; 4]) - .unwrap_or_else(|e| fail(what, &format!("no commit: {e:?}"))); - await_hangup(conn.as_handle(), what, "the compositor's refusal"); - probe(what); - let third = paste(&mut target, what); - if third != AFTER.as_bytes() { - let len = third.len(); - fail(what, &format!("the paste was {len} bytes, not the clipboard from before")); - } - - // Last: the new window takes the focus the pastes went to. - let what = "a commit on a window"; - waiting(what, "its window"); - let committing = Window::create_with_title(64, 64, "commit") - .unwrap_or_else(|e| fail(what, &format!("no window: {e}"))); - ipc::signal(committing.handle(), COPY_COMMIT) - .unwrap_or_else(|e| fail(what, &format!("no commit: {e:?}"))); - // A compositor still serving the window answers this probe, which the - // hangup wait reds on. One that dropped the window before the probe went - // out refuses its send with `Gone`, which is that hangup; the wait still - // reads whatever it answered before it did. - match ipc::signal(committing.handle(), window::MSG_GET_RESOLUTION) { - Ok(()) | Err(ipc::IpcError::Syscall(SyscallError::Gone)) => {} - Err(e) => fail(what, &format!("no probe: {e:?}")), - } - await_hangup(committing.handle(), what, "the compositor closing the window"); - probe(what); - - println!("hostile clipboard: every case survived, compositor still serving"); -} - -/// Put `text` on the clipboard inline, and wait for the compositor to be done -/// with it. -fn set_inline(what: &str, text: &str) { - let conn = connect(what); - conn.send_bytes(window::MSG_CLIPBOARD_SET, text.as_bytes()) - .unwrap_or_else(|e| fail(what, &format!("could not set the clipboard: {e:?}"))); - await_hangup(conn.as_handle(), what, "the compositor closing the clipboard"); -} - -/// A pipe end where the retired message carried a region. -fn wrong_typed_handle() { - let what = "a wrong-typed handle"; - let ends = syscall::pipe().unwrap_or_else(|e| fail(what, &format!("no pipe: {e:?}"))); - let conn = connect(what); - conn.send_with_handles( - &[ends.write], - RETIRED_CLIPBOARD_SET_SHM, - &window::ClipboardShmMsg { len: 64 }, - ) - .unwrap_or_else(|e| fail(what, &format!("could not send: {e:?}"))); - await_hangup(conn.as_handle(), what, "the compositor's refusal"); - // The send moved the only writer, so the reader hangs up exactly when the - // queue holding it is gone — and not while anything holds it. - await_hangup(ends.read, what, "the writer's return to the kernel"); - syscall::close(ends.read); -} - -/// Commit a whole copy of `byte`, and wait for the compositor to be done with -/// it. -fn commit_filled(what: &str, byte: u8) -> SharedMemory { - let (conn, region) = begin_copy(what); - fill(®ion, byte); - conn.signal(COPY_COMMIT).unwrap_or_else(|e| fail(what, &format!("no commit: {e:?}"))); - await_hangup(conn.as_handle(), what, "the compositor closing the copy"); - region -} - -/// A connection holding the region the compositor made for a whole copy. -fn begin_copy(what: &str) -> (Connection, SharedMemory) { - let conn = connect(what); - conn.send(COPY_BEGIN, &window::ClipboardShmMsg { len: COPY_LEN as u32 }) - .unwrap_or_else(|e| fail(what, &format!("could not begin: {e:?}"))); - waiting(what, "the compositor's region"); - let header = conn.recv_header().unwrap_or_else(|e| fail(what, &format!("no answer: {e:?}"))); - if header.msg_type != COPY_REGION || header.len() != 0 { - fail( - what, - &format!( - "the compositor answered with message type {} and {} bytes", - header.msg_type, - header.len() - ), - ); - } - let [region] = - conn.recv_handles_exact::<1>().unwrap_or_else(|| fail(what, "the answer had no region")); - let region = SharedMemory::adopt(region, COPY_LEN) - .unwrap_or_else(|e| fail(what, &format!("the region would not map: {e:?}"))); - (conn, region) -} - -fn fill(region: &SharedMemory, byte: u8) { - for b in region.as_atomic() { - b.store(byte, Ordering::Relaxed); - } -} - -/// Ask the host for GUI+V and return what the target is pasted. -fn paste(target: &mut Window, what: &str) -> Vec { - waiting(what, "the paste"); - println!("{PASTE_MARKER}"); - loop { - match target.recv_event() { - Event::ClipboardPaste(text) => return text, - Event::Close => fail(what, "the paste target's window was closed"), - _ => {} - } - } -} - -/// A paste, summarised — never printed whole. -fn describe(text: &[u8], fill: u8) -> String { - let stray = text.iter().position(|&b| b != fill); - format!( - "the paste was {} bytes, UTF-8: {}, first byte that is not {:?} at {stray:?}", - text.len(), - std::str::from_utf8(text).is_ok(), - fill as char - ) -} - -fn connect(what: &str) -> Connection { - endow::service("compositor") - .unwrap_or_else(|e| fail(what, &format!("the compositor is not serving: {e:?}"))) -} - -/// Said before every blocking wait: the line a missing event leaves last. -fn waiting(what: &str, awaited: &str) { - println!("hostile clipboard: [{what}] waiting for {awaited}"); -} - -/// Wait for the peer of `handle` to hang up, failing on anything it sends. -fn await_hangup(handle: RawHandle, what: &str, awaited: &str) { - waiting(what, awaited); - let mut byte = [0u8; 1]; - match syscall::read(handle, &mut byte) { - Ok(0) => {} - Ok(_) => fail(what, &format!("the peer answered where {awaited} was due")), - Err(e) => fail(what, &format!("waiting for {awaited}: {e:?}")), - } -} - -/// Ask the compositor something it always answers. -fn probe(what: &str) { - let conn = connect(what); - conn.signal(window::MSG_GET_RESOLUTION) - .unwrap_or_else(|e| fail(what, &format!("could not ask for the resolution: {e:?}"))); - waiting(what, "the compositor's answer to a probe"); - let header = conn - .recv_header() - .unwrap_or_else(|e| fail(what, &format!("the probe went unanswered: {e:?}"))); - if header.msg_type != window::MSG_RESOLUTION_CHANGED { - fail(what, &format!("the probe was answered with message type {}", header.msg_type)); - } -} - -fn fail(what: &str, msg: &str) -> ! { - eprintln!("hostile clipboard: [{what}] {msg}"); - std::process::exit(1); -} diff --git a/tests/toyos-rust-tests/src/bin/compositor_stall.rs b/tests/toyos-rust-tests/src/bin/compositor_stall.rs deleted file mode 100644 index 3c4232e2aa3..00000000000 --- a/tests/toyos-rust-tests/src/bin/compositor_stall.rs +++ /dev/null @@ -1,210 +0,0 @@ -//! The desktop must survive a client that stops talking, stops listening, or -//! never stops. -//! -//! Every one of these cases used to park the compositor's whole event loop in -//! a kernel wait with no deadline — no redraws, no input, nothing — because -//! the compositor read and wrote its clients with blocking calls. The one -//! written up in `issues/isolation/` is the second case here: a client -//! that connects and sends four bytes, met by `ipc::recv_header` on a freshly -//! accepted connection. -//! -//! Each case sets its stall up and leaves it standing, then asks the -//! compositor a question. No wait here has a deadline: a compositor frozen on a -//! client never answers, and the harness ceiling reds it. The host side asserts -//! the other half — that the desktop is still *painting*, and that every client -//! dropped along the way was named in the log. - -use std::process::exit; -use std::thread; -use std::sync::atomic::{AtomicBool, Ordering}; - -use toyos::endow; -use toyos::AsHandle; -use toyos::{ipc, Connection}; -use toyos_abi::syscall::{self, SyscallError}; -use window::Window; - -/// Between two looks at a connection the compositor is expected to drop. A -/// pace and never a verdict. -const POLL_NS: u64 = 10_000_000; - -/// A message type no protocol here defines: the compositor's dispatch ignores -/// it, so a stream of them is pure event-loop load with nothing to draw. That -/// is what makes it a starvation case rather than a redraw case. -const UNKNOWN_MSG: u32 = 0x7FFF_0001; - -/// One `MSG_GET_RESOLUTION` costs the client 8 bytes and the compositor 16, so -/// filling a client's 2,097,088-byte receive ring from the far side takes -/// 131,068 answers. This is that with margin, and the requests themselves are -/// half the bytes and fit in the client's own ring — nothing here can block -/// the *client* instead, which would prove the wrong thing. -const REQUESTS: usize = 140_000; - -fn main() { - // Held to the end of the run: a dropped `Connection` closes the handle, and - // a closed handle is a peer that hung up rather than one that went quiet. - let mut held: Vec = Vec::new(); - - held.push(connect("connected and silent")); - probe("connected and silent"); - - let conn = connect("half a header"); - write_raw(&conn, &[0u8; 4], "half a header"); - held.push(conn); - probe("half a header"); - - let conn = connect("header without payload"); - let payload_len = std::mem::size_of::() as u32; - write_raw(&conn, &header(window::MSG_CREATE_WINDOW, payload_len), "header without payload"); - held.push(conn); - probe("header without payload"); - - // The three above are handshakes that never complete. Nothing the client - // does ends them; the compositor's own deadline does, and each one's close - // is what is waited for. - for conn in &held { - await_hang_up(conn.as_handle()); - } - probe("after the handshake deadline"); - - // A window that stops in the middle of a message it already declared. The - // stall is on an established connection rather than a fresh one, which is - // the sibling of the accept-path defect and had the same cure. - let stuck = Window::create(64, 64).expect("a window to stall mid-message with"); - write_handle(stuck.handle(), &header(window::MSG_CLIPBOARD_SET, 116), "window mid-message"); - write_handle(stuck.handle(), &[b'x'; 8], "window mid-message"); - probe("window stopped mid-message"); - - // A window that asks faster than it reads. The compositor's answer has to - // be a refusal, because the alternative is waiting for a client to read - // its mail. - let deaf = Window::create(64, 64).expect("a window to stop reading with"); - let mut requests = Vec::with_capacity(REQUESTS * 8); - for _ in 0..REQUESTS { - requests.extend_from_slice(&header(window::MSG_GET_RESOLUTION, 0)); - } - write_handle(deaf.handle(), &requests, "window that will not read"); - await_hang_up(deaf.handle()); - probe("window that will not read"); - - // A window with something to send on every pass. Nothing here is - // unanswerable — the loop simply never runs out of work, and a drain that - // ends only when nothing is ready never reaches the screen. So a second - // window presents while it streams, and the stream runs until that present - // is composited: a drain loop the stream starves never gets to `redraw`, and - // the frame never comes. - let noisy = Window::create(64, 64).expect("a window to stream from"); - let handle = noisy.handle(); - let mut watcher = Window::create(64, 64).expect("a window to composite under the stream"); - let (streaming, framed) = (AtomicBool::new(false), AtomicBool::new(false)); - let presenter = thread::current(); - thread::scope(|s| { - s.spawn(|| { - let frame = header(UNKNOWN_MSG, 0); - while !framed.load(Ordering::Acquire) { - // Fill the ring, not merely feed it. The compositor takes one - // frame per client per pass, so a client that keeps up with only - // that lets the drain run dry and the screen get painted — which - // is the thing this case is supposed to prevent. - // - // Never a torn frame: both ends move this ring in multiples of - // eight bytes and its capacity is one too, so a write of a header - // either fits whole or finds no room at all. - while matches!(syscall::write_nonblock(handle, &frame), Ok(8)) {} - streaming.store(true, Ordering::Release); - presenter.unpark(); - syscall::nanosleep(1_000_000); - } - }); - // Presented once the ring is full, so the frame is composited under - // the stream and not ahead of it. Parked until then, never spinning: a - // thread yielding beside the writer held it below the compositor's - // drain rate, and the ring never filled. - while !streaming.load(Ordering::Acquire) { - thread::park(); - } - println!("compositor stall: the ring is full; a second window presents under it"); - watcher.present(); - loop { - match watcher.recv_event() { - window::Event::Frame => break, - window::Event::Close => fail( - "[window that never stops sending] the window presented under the stream \ - was closed", - ), - _ => {} - } - } - framed.store(true, Ordering::Release); - }); - probe("window that never stops sending"); - - println!("compositor stall: 6 stalls survived, compositor still serving"); -} - -fn header(msg_type: u32, len: u32) -> [u8; 8] { - let mut frame = [0u8; 8]; - frame[..4].copy_from_slice(&msg_type.to_ne_bytes()); - frame[4..].copy_from_slice(&len.to_ne_bytes()); - frame -} - -fn connect(what: &str) -> Connection { - endow::service("compositor") - .unwrap_or_else(|e| fail(&format!("[{what}] the compositor is not serving: {e:?}"))) -} - -fn write_raw(conn: &Connection, bytes: &[u8], what: &str) { - write_handle(conn.as_handle(), bytes, what); -} - -/// Every write here fits in the pipe it goes into, so a blocking `write` can -/// only be the compositor's problem, never this binary's. -fn write_handle(handle: toyos_abi::RawHandle, bytes: &[u8], what: &str) { - let mut offset = 0; - while offset < bytes.len() { - match syscall::write(handle, &bytes[offset..]) { - Ok(n) => offset += n, - Err(e) => fail(&format!("[{what}] write failed after {offset} bytes: {e:?}")), - } - } -} - -/// Wait, with no deadline, until nothing holds the other end of `handle`. -/// -/// **Without draining a byte**, which is the whole difficulty: this client's -/// receive ring has to stay full for the compositor to reach the end of it, -/// so the answer cannot be read from the ring. An empty `write_nonblock` -/// writes nothing and still asks the one question that matters — is anything -/// still holding the read end — so the refusal is observed rather than slept -/// through. A compositor parked in `write` instead has its handle open and -/// answers `Ok` here forever. -fn await_hang_up(handle: toyos_abi::RawHandle) { - while syscall::write_nonblock(handle, &[]) != Err(SyscallError::Gone) { - syscall::nanosleep(POLL_NS); - } -} - -/// Ask the compositor something it always answers from its dispatch, so a reply -/// proves the event loop reached the end of a pass. No deadline: a compositor -/// parked on a client never answers, and the harness ceiling reds it. -fn probe(what: &str) { - let conn = connect(what); - if let Err(e) = ipc::signal(conn.as_handle(), window::MSG_GET_RESOLUTION) { - fail(&format!("[{what}] could not ask the compositor for its resolution: {e:?}")); - } - let mut buf = [0u8; 16]; - let mut got = 0; - while got < buf.len() { - match syscall::read(conn.as_handle(), &mut buf[got..]) { - Ok(0) => fail(&format!("[{what}] the compositor closed the probe unanswered")), - Ok(n) => got += n, - Err(e) => fail(&format!("[{what}] the probe could not be read: {e:?}")), - } - } -} - -fn fail(msg: &str) -> ! { - eprintln!("compositor stall: {msg}"); - exit(1); -} diff --git a/tests/toyos-rust-tests/src/bin/doom_frames.rs b/tests/toyos-rust-tests/src/bin/doom_frames.rs deleted file mode 100644 index 78937cea65f..00000000000 --- a/tests/toyos-rust-tests/src/bin/doom_frames.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Runs doom's frame check and reports whether the process lived. -//! -//! The hashing is `frames` in `userland/doom`: it plays `demo1` as a timedemo -//! and folds each tic's frame into one number, which doom prints itself. This -//! side starts it and answers whether it exited or died; the verdict on the -//! number is the host's. - -use std::process::Command; - -fn main() { - let status = Command::new("/system/bin/doom") - .arg("--frame-check") - .status() - .expect("spawn /system/bin/doom --frame-check"); - assert!(status.success(), "doom's frame check did not finish: {status:?}"); - println!("doom drew its frames"); -} diff --git a/tests/toyos-rust-tests/src/bin/dump_stage_load.rs b/tests/toyos-rust-tests/src/bin/dump_stage_load.rs deleted file mode 100644 index 872c08cfcc5..00000000000 --- a/tests/toyos-rust-tests/src/bin/dump_stage_load.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! The load `dump-in-blocking-pass` files Ctrl+Alt+D inside: a victim, not a test. -//! -//! Two loads at once, one for each pass a job reaches on demand that may not -//! serve the request: a pipe ping-pong with a child parks in blocking passes, and -//! a spawner's threads exit from a syscall while it waits for each. Every such -//! pass leaves a task it has just woken behind it and none of these tasks keeps -//! the CPU for a quantum, so on one CPU no idle loop and no tick comes: a request -//! left pending there is served by nothing but the pass that the one who left it -//! owes. -//! -//! Nothing here asserts: the counts are the kernel's, and -//! `tests/common/faults.rs` holds the verdict. - -use std::io::{Read, Write}; -use std::process::{Command, Stdio}; -use std::thread; - -/// Many times the pass the actuator stages at, so both requests are filed and -/// reported with the loads still running. -const ROUND_TRIPS: u32 = 1024; -const EXITS: u32 = 256; - -fn echo() -> ! { - let mut stdin = std::io::stdin(); - let mut stdout = std::io::stdout(); - let mut byte = [0u8; 1]; - loop { - match stdin.read(&mut byte) { - Ok(0) | Err(_) => std::process::exit(0), - Ok(_) => {} - } - if stdout.write_all(&byte).is_err() || stdout.flush().is_err() { - std::process::exit(0); - } - } -} - -fn main() { - if std::env::args().nth(1).as_deref() == Some("echo") { - echo(); - } - - let exe = std::env::current_exe().expect("current_exe"); - let mut child = Command::new(&exe) - .arg("echo") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("spawn the echo half"); - let mut to_child = child.stdin.take().expect("piped stdin"); - let mut from_child = child.stdout.take().expect("piped stdout"); - - let spawner = thread::spawn(|| { - for _ in 0..EXITS { - thread::spawn(|| {}).join().expect("join an exiting thread"); - } - }); - - let mut byte = [0u8; 1]; - for _ in 0..ROUND_TRIPS { - to_child.write_all(&[0x5a]).expect("write to the echo half"); - to_child.flush().expect("flush to the echo half"); - from_child.read_exact(&mut byte).expect("read from the echo half"); - } - - drop(to_child); - child.wait().expect("wait for the echo half"); - spawner.join().expect("join the spawner"); - println!("dump-stage-load: {ROUND_TRIPS} round trips, {EXITS} exits"); -} diff --git a/tests/toyos-rust-tests/src/bin/fpu_isolation.rs b/tests/toyos-rust-tests/src/bin/fpu_isolation.rs deleted file mode 100644 index bf3f3f1180e..00000000000 --- a/tests/toyos-rust-tests/src/bin/fpu_isolation.rs +++ /dev/null @@ -1,507 +0,0 @@ -//! What a transition out of Ring 3 preserves. -//! -//! Three arms, all positive assertions, and each one fails on the tree that -//! came before the bracket in `kernel/src/arch/x86_64/entry.rs`: -//! -//! 1. **Leak.** One process pins a distinctive FP state and exits without -//! restoring it; the next asserts the *declared* state at its own entry. -//! 2. **Fault.** `fault_gate_child mf` dies with an unmasked x87 exception -//! pending; the next process executes `FLDCW` — a waiting instruction — and -//! must survive. That is the defect CI proved by one token: `std_unwind`'s -//! victim was the `FLDCW` inside the unwinder's `restore_context`, and every -//! ToyOS binary executes one on every panic. -//! 3. **Preservation.** One process pins a state, forces many transitions of -//! each kind — syscall, demand page fault, timer preemption — against an -//! FP-heavy sibling, and asserts bit-identity. -//! -//! **It is run at `smp=1`, and that is the stronger choice rather than the -//! weaker one.** The defect is a CPU register file carrying over between tasks, -//! so an arm only means anything when the two tasks share a CPU. With more CPUs -//! that is a coin flip, which is why CI's observation of it was intermittent. - -use std::process::Command; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; - -use core::mem::offset_of; - -use toyos_abi::syscall::{ - mmap, nanosleep, thread_join, thread_spawn, MmapFlags, MmapProt, SYS_EXIT, SYS_GETPID, - SYS_THREAD_EXIT, -}; - -/// The FXSAVE64 image, at the alignment the instruction requires. -#[repr(C, align(16))] -struct FpImage([u8; 512]); - -// Field offsets, SDM Vol. 1 Table 10-2. -const OFF_FCW: usize = 0; -const OFF_FSW: usize = 2; -const OFF_FTW: usize = 4; -const OFF_MXCSR: usize = 24; -const OFF_ST0: usize = 32; -const OFF_XMM0: usize = 160; -const END_XMM: usize = 416; - -/// Every x87 exception masked, extended precision, round to nearest. -const FCW_DECLARED: u16 = 0x037F; -/// Every SSE exception masked, round to nearest, no flush-to-zero. -const MXCSR_DECLARED: u32 = 0x1F80; - -/// Masked exactly like the declared words — nothing here can raise anything — -/// and different from them in every field that has more than one value: -/// round-toward-zero for both, single precision for x87. -const FCW_PINNED: u16 = 0x0C7F; -const MXCSR_PINNED: u32 = 0x7F80; - -/// Everything the pinning assembly reads or writes, in one object, so the -/// blocks below need one base register rather than eight. -/// -/// `r15` is that register, named explicitly at every site rather than left to -/// the allocator: `clobber_abi("sysv64")` does not stop LLVM putting an -/// `in(reg)` input in a clobbered register, and it put this one in `rax`, which -/// the `syscall` loop then destroyed — the base register read back as a -/// truncated 32-bit value and the next load segfaulted. -#[repr(C, align(16))] -struct Arena { - cw: u16, - _pad: [u8; 2], - mx: u32, - /// The page-fault arm's first untouched page. - region: u64, - /// Sixteen distinctive XMM values, one per register. - xmm: [u64; 32], - before: FpImage, - after: FpImage, -} - -static mut ARENA: Arena = Arena { - cw: FCW_PINNED, - _pad: [0; 2], - mx: MXCSR_PINNED, - region: 0, - xmm: { - let mut v = [0u64; 32]; - let mut i = 0; - while i < 32 { - v[i] = 0xF9A3_0000_0000_0000 | (i as u64 + 1); - i += 1; - } - v - }, - before: FpImage([0; 512]), - after: FpImage([0; 512]), -}; - -/// What this process's own state was at the first instruction of `main`. -static mut ENTRY_IMAGE: FpImage = FpImage([0; 512]); - -const PAGE_2M: usize = 2 * 1024 * 1024; -/// First touches the preservation arm makes of [`DEMAND`], 2 MiB apart, so each -/// is a page of its own and each is a `#PF`. -const FAULT_TOUCHES: u64 = 2; - -/// The page-fault workload. -/// -/// Not `mmap`: `sys_mmap` allocates and maps its whole region up front, so the -/// first touch of a fresh mapping faults nothing. Not `.bss` either: that is the -/// loader's `Anonymous` tail of a `PT_LOAD`, and the path measured here is the -/// file-backed one. What is used is a *writable -/// file-backed* page — non-zero so it lands in `.data`, and named by nothing -/// else so no relocation has touched it — which faults on first write and takes -/// the allocate-and-copy path. Four megabytes of test image is what two faults -/// cost, and that is the whole reason there are two rather than twenty. -static mut DEMAND: [u8; PAGE_2M * 2] = [1; PAGE_2M * 2]; - -/// Where the raw thread probe writes what it found at its very first -/// instruction. A static, because there is nothing between the trampoline's -/// `iretq` and the `fxsave64` and there must not be. -static mut THREAD_IMAGE: FpImage = FpImage([0; 512]); - -/// The pin sequence: a distinctive value in every register this kernel permits -/// to exist. Shared by the two arms that need one so they cannot drift. -macro_rules! pin_state { - () => { - concat!( - "fninit\n", - "fldcw [r15]\n", - "fld1\nfldl2t\nfldl2e\nfldpi\nfldlg2\nfldln2\nfld1\nfldpi\n", - "ldmxcsr [r15 + {mx}]\n", - "movdqu xmm0, [r15 + {x} + 0*16]\n", - "movdqu xmm1, [r15 + {x} + 1*16]\n", - "movdqu xmm2, [r15 + {x} + 2*16]\n", - "movdqu xmm3, [r15 + {x} + 3*16]\n", - "movdqu xmm4, [r15 + {x} + 4*16]\n", - "movdqu xmm5, [r15 + {x} + 5*16]\n", - "movdqu xmm6, [r15 + {x} + 6*16]\n", - "movdqu xmm7, [r15 + {x} + 7*16]\n", - "movdqu xmm8, [r15 + {x} + 8*16]\n", - "movdqu xmm9, [r15 + {x} + 9*16]\n", - "movdqu xmm10, [r15 + {x} + 10*16]\n", - "movdqu xmm11, [r15 + {x} + 11*16]\n", - "movdqu xmm12, [r15 + {x} + 12*16]\n", - "movdqu xmm13, [r15 + {x} + 13*16]\n", - "movdqu xmm14, [r15 + {x} + 14*16]\n", - "movdqu xmm15, [r15 + {x} + 15*16]\n", - ) - }; -} - -fn fxsave(dst: *mut FpImage) { - unsafe { - core::arch::asm!("fxsave64 [{}]", in(reg) dst, options(nostack)); - } -} - -fn fcw(img: &FpImage) -> u16 { - u16::from_le_bytes([img.0[OFF_FCW], img.0[OFF_FCW + 1]]) -} - -fn fsw(img: &FpImage) -> u16 { - u16::from_le_bytes([img.0[OFF_FSW], img.0[OFF_FSW + 1]]) -} - -fn mxcsr(img: &FpImage) -> u32 { - u32::from_le_bytes([ - img.0[OFF_MXCSR], - img.0[OFF_MXCSR + 1], - img.0[OFF_MXCSR + 2], - img.0[OFF_MXCSR + 3], - ]) -} - -/// The registers themselves: the x87 file and XMM0-15, contiguous in the image. -fn registers(img: &FpImage) -> &[u8] { - &img.0[OFF_ST0..END_XMM] -} - -fn main() { - // Before anything else this process does: arm 1's observation. Its parent - // spawned a `pin` immediately beforehand, and the assertion is that none of - // what that left is here. - fxsave(&raw mut ENTRY_IMAGE); - - match std::env::args().nth(1).as_deref() { - Some("pin") => pin_and_exit(), - Some("check") => check_entry_state(), - Some("fldcw") => fldcw_survivor(), - Some(other) => panic!("unknown mode {other}"), - None => driver(), - } -} - -/// Every arm runs and every verdict is collected, rather than the first failure -/// ending the run: the negative control's whole job is to show that each arm -/// has teeth, and a run that stops at the first one proves it about one. -fn driver() { - let mut failures: Vec = Vec::new(); - for round in 0..3 { - failures.extend(leak_arm(round).err()); - failures.extend(fault_arm(round).err()); - } - failures.extend(preservation_arm().err()); - for f in &failures { - println!(" FAILED: {f}"); - } - assert!(failures.is_empty(), "{} arm(s) did not preserve the state", failures.len()); - println!("every transition out of Ring 3 preserved the whole user machine state"); -} - -fn require(ok: bool, why: impl FnOnce() -> String) -> Result<(), String> { - if ok { Ok(()) } else { Err(why()) } -} - -fn spawn_mode(mode: &str) -> std::process::ExitStatus { - Command::new("/system/bin/test_rs_fpu_isolation") - .arg(mode) - .status() - .unwrap_or_else(|e| panic!("failed to spawn {mode}: {e}")) -} - -/// Arm 1. `pin` leaves a state behind; `check` must not find it. -fn leak_arm(round: u32) -> Result<(), String> { - let pinned = spawn_mode("pin"); - require(pinned.success(), || { - format!("round {round}: the pin child exited {:?}", pinned.code()) - })?; - let status = spawn_mode("check"); - require(status.success(), || { - format!( - "leak, round {round}: a process started with the previous one's FP registers \ - (exit {:?})", - status.code(), - ) - }) -} - -/// Arm 2. A process dies with an unmasked x87 exception pending; the next one -/// executes a waiting instruction and must live. -fn fault_arm(round: u32) -> Result<(), String> { - // The child's own verdict is this arm's precondition rather than a bonus - // check: a child that survives its `fwait` reaches the `fninit` two - // instructions later, which masks everything again and leaves the next - // process nothing to be protected from. Unasserted, this arm passes - // vacuously on exactly the machine it is for. - let child = Command::new("/system/bin/test_rs_fault_gate_child") - .arg("mf") - .status() - .unwrap_or_else(|e| panic!("failed to spawn fault_gate_child mf: {e}")); - require(!child.success(), || { - format!( - "fault, round {round}: the #MF child lived, so it left the FPU masked and clean \ - and the arm below asserts nothing (exit {:?})", - child.code(), - ) - })?; - let status = spawn_mode("fldcw"); - require(status.success(), || { - format!( - "fault, round {round}: FLDCW took an exception the process never caused — the \ - previous process's pending x87 exception was still on the CPU (exit {:?})", - status.code(), - ) - }) -} - -/// Load the distinctive state and leave Ring 3 in the same instruction stream, -/// so nothing between here and the syscall can disturb it. -fn pin_and_exit() -> ! { - unsafe { - core::arch::asm!( - pin_state!(), - "mov rdi, {exit}", - "xor esi, esi", - "syscall", - in("r15") &raw const ARENA, - mx = const offset_of!(Arena, mx), - x = const offset_of!(Arena, xmm), - exit = const SYS_EXIT, - options(noreturn), - ); - } -} - -/// Arm 1's assertion, in two halves. -fn check_entry_state() { - let entry = unsafe { &*(&raw const ENTRY_IMAGE) }; - assert_eq!( - fcw(entry), - FCW_DECLARED, - "this process started with the previous one's x87 control word", - ); - assert_eq!( - mxcsr(entry), - MXCSR_DECLARED, - "this process started with the previous one's MXCSR", - ); - assert_eq!(entry.0[OFF_FTW], 0, "this process started with a non-empty x87 stack"); - assert!( - entry.0[OFF_ST0..OFF_XMM0].iter().all(|&b| b == 0), - "this process started with the previous one's x87 registers", - ); - - // The XMM half cannot be asserted here: std's startup has already run and it - // uses XMM. A raw thread can be asked, because between the loader's - // trampoline and its first instruction there is nothing but an `iretq`. - thread_entry_state(); - println!(" entry state is the declared one, in the process and in a fresh thread"); -} - -/// A thread whose first instruction records the whole state, so the declared -/// state can be asserted in full — XMM included. -#[unsafe(naked)] -extern "C" fn thread_probe() { - core::arch::naked_asm!( - "fxsave64 [rdi]", - "mov rdi, {exit}", - "xor esi, esi", - "syscall", - "ud2", - exit = const SYS_THREAD_EXIT, - ); -} - -fn thread_entry_state() { - const STACK: usize = 2 * 1024 * 1024; - let stack = unsafe { - mmap( - core::ptr::null_mut(), - STACK, - MmapProt::READ | MmapProt::WRITE, - MmapFlags::ANONYMOUS | MmapFlags::PRIVATE, - ) - }; - assert!(!stack.is_null(), "no stack for the thread probe"); - let entry: extern "C" fn() = thread_probe; - let tid = unsafe { - thread_spawn( - entry as *const () as u64, - stack as u64 + STACK as u64, - (&raw mut THREAD_IMAGE) as u64, - stack as u64, - ) - }; - assert!(tid < 1_000_000, "thread_spawn refused: {tid:#x}"); - assert_eq!(thread_join(tid), 0, "thread_join failed"); - - let img = unsafe { &*(&raw const THREAD_IMAGE) }; - assert_eq!(fcw(img), FCW_DECLARED, "a fresh thread inherited an x87 control word"); - assert_eq!(fsw(img), 0, "a fresh thread inherited an x87 status word"); - assert_eq!(img.0[OFF_FTW], 0, "a fresh thread inherited a non-empty x87 stack"); - assert_eq!(mxcsr(img), MXCSR_DECLARED, "a fresh thread inherited an MXCSR"); - assert!( - registers(img).iter().all(|&b| b == 0), - "a fresh thread inherited the previous tenant's x87 or XMM registers", - ); -} - -/// Arm 2's victim: the waiting instruction the unwinder executes on every -/// panic, in a process that has never touched the FPU. -fn fldcw_survivor() { - let cw = FCW_DECLARED; - unsafe { - core::arch::asm!("fldcw [{cw}]", "fwait", cw = in(reg) &cw, options(nostack)); - } - println!(" FLDCW survived"); -} - -/// Arm 3. Everything from the pin to the capture is one instruction stream, so -/// nothing the compiler emits between them can touch the state under test. -/// -/// **An unbracketed transition only corrupts if it switches**, and that is what -/// the sibling is for. Kernel code is soft-float, so a `#PF` that allocates a -/// page and returns disturbs nothing however unbracketed it is; what does the -/// damage is another task running Ring 3 code in between. A sibling that sleeps -/// in a short loop wakes several times inside a single 2 MiB fault — the fault -/// path runs with interrupts on and the measured cost of one is hundreds of -/// microseconds — so `need_resched` is set when `common_entry` reaches its exit -/// and the switch happens there rather than by luck. -fn preservation_arm() -> Result<(), String> { - const SYSCALLS: u64 = 20_000; - const SPIN: u64 = 2_000_000; - /// Short against the fault's own cost, so several land inside one. - const SIBLING_NAP_NS: u64 = 100_000; - - unsafe { ARENA.region = (&raw mut DEMAND) as u64 }; - - let stop = Arc::new(AtomicBool::new(false)); - let sibling = { - let stop = Arc::clone(&stop); - std::thread::spawn(move || { - while !stop.load(Ordering::Relaxed) { - fp_noise(); - nanosleep(SIBLING_NAP_NS); - } - }) - }; - - unsafe { - core::arch::asm!( - pin_state!(), - "fxsave64 [r15 + {before}]", - - // r13 is the counter and r14 the cursor: `clobber_abi` requires an - // output to name its register, and these two have to outlive the - // `syscall` that clobbers every caller-saved one. - "mov r13, {nsys}", - "2:", - "mov rdi, {getpid}", - "syscall", - "dec r13", - "jnz 2b", - - // One write per 2 MiB page of a writable file-backed region, so - // every iteration is exactly one #PF through `common_entry`. - "mov r13, {npages}", - "mov r14, [r15 + {region}]", - "3:", - "mov byte ptr [r14], 1", - "add r14, {step}", - "dec r13", - "jnz 3b", - - // Long enough for the timer to preempt, several times over. - "mov r13, {spin}", - "4:", - "dec r13", - "jnz 4b", - - "fxsave64 [r15 + {after}]", - // Leave the x87 stack as Rust expects to find it. - "fninit", - in("r15") &raw mut ARENA, - mx = const offset_of!(Arena, mx), - x = const offset_of!(Arena, xmm), - region = const offset_of!(Arena, region), - before = const offset_of!(Arena, before), - after = const offset_of!(Arena, after), - step = const PAGE_2M, - npages = const FAULT_TOUCHES, - nsys = const SYSCALLS, - spin = const SPIN, - getpid = const SYS_GETPID, - out("r13") _, - out("r14") _, - clobber_abi("sysv64"), - ); - } - - stop.store(true, Ordering::Relaxed); - sibling.join().expect("the sibling thread died"); - - let before = unsafe { &*(&raw const ARENA.before) }; - let after = unsafe { &*(&raw const ARENA.after) }; - let what = format!( - "{SYSCALLS} syscalls, {FAULT_TOUCHES} page faults and a preemption spin" - ); - require(fcw(after) == fcw(before), || { - format!( - "preservation: the x87 control word did not survive {what} — {:#06x} became {:#06x}", - fcw(before), - fcw(after), - ) - })?; - require(fsw(after) == fsw(before), || { - format!("preservation: the x87 status word did not survive {what}") - })?; - require(after.0[OFF_FTW] == before.0[OFF_FTW], || { - format!("preservation: the x87 tag word did not survive {what}") - })?; - require(mxcsr(after) == mxcsr(before), || { - format!( - "preservation: MXCSR did not survive {what} — {:#010x} became {:#010x}", - mxcsr(before), - mxcsr(after), - ) - })?; - let differing = - registers(before).iter().zip(registers(after)).filter(|(a, b)| a != b).count(); - require(differing == 0, || { - format!( - "preservation: {differing} of {} register bytes changed across {what}", - registers(before).len(), - ) - })?; - println!(" the whole state survived {what}"); - Ok(()) -} - -/// What the sibling does: dirty every kind of FP register there is. -fn fp_noise() { - unsafe { - core::arch::asm!( - "fninit", - "fldpi", - "fldl2t", - "fldln2", - "movdqu xmm0, [r15 + {x} + 0*16]", - "movdqu xmm3, [r15 + {x} + 1*16]", - "movdqu xmm7, [r15 + {x} + 2*16]", - "movdqu xmm11, [r15 + {x} + 3*16]", - "movdqu xmm15, [r15 + {x} + 4*16]", - in("r15") &raw const ARENA, - x = const offset_of!(Arena, xmm), - clobber_abi("sysv64"), - ); - } -} diff --git a/tests/toyos-rust-tests/src/bin/fs_claim_held.rs b/tests/toyos-rust-tests/src/bin/fs_claim_held.rs deleted file mode 100644 index 1638c15ed73..00000000000 --- a/tests/toyos-rust-tests/src/bin/fs_claim_held.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! DATA's partition claim held across its file server's restart. -//! -//! `tests/fsdclaimcase` arms DATA's server with `--let-go-at-read`: the first -//! read-only open of [`LET_GO`] lets its partition go and is refused, and this -//! client's next request ends the server. This binary takes the claim in -//! between, so init's restart of the role finds it held, and holds it until -//! init has answered: a new open waits in the role's port until init either -//! closes it, which answers Gone, or starts a server that answers it. The host -//! judges init's and fsd's lines (`tests/common/storage.rs`'s `fsd_claim_held`). - -use std::fs::File; -use std::io::ErrorKind; - -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos::PartitionDev; -use toyos_abi::part::PartGuid; -use toyos_abi::syscall::SYSCAP_LABEL; - -/// Mirrored in `tests/common/storage.rs`: the DATA partition on the crafted -/// stick. -const DATA: &str = "7E2B4C6D-8F1A-4B3C-9D5E-6F7A8B9C0D1E"; -/// Mirrored in `tests/fsdclaimcase/system.toml`. -const LET_GO: &str = "/home/fsd_let_go"; -/// A name on DATA nothing makes: asked only to reach its server. -const AFTER: &str = "/home/fsd_claim_held"; - -fn main() { - let cap: SysCap = - Endowments::get().take(SYSCAP_LABEL).expect("the test estate is endowed a device-minting capability"); - let data = PartGuid::parse(DATA).expect("DATA is a GUID"); - - match File::open(LET_GO) { - Err(e) => println!("fs_claim_held: the server let its partition go, and the open was refused ({e})"), - Ok(_) => panic!("the open of {LET_GO} was answered: the server did not let its partition go"), - } - let held = cap - .claim_partition::(data) - .unwrap_or_else(|e| panic!("DATA's claim, which its server let go, was refused: {e:?}")); - println!("fs_claim_held: holding DATA's claim"); - - match File::open(AFTER) { - Err(e) => println!("fs_claim_held: the request the server ends under was refused ({e})"), - Ok(_) => panic!("the server answered the request it was armed to end under"), - } - match File::open(AFTER) { - Err(e) if e.kind() == ErrorKind::StaleNetworkFileHandle => { - println!("fs_claim_held: init did not start DATA's server again, and {AFTER} answers Gone ({e})") - } - Err(e) => panic!("{AFTER} was refused {e} ({:?}), not Gone", e.kind()), - Ok(_) => panic!("{AFTER} was answered while this process held DATA's claim: a server runs without it"), - } - drop(held); - println!("fs_claim_held: PASS"); -} diff --git a/tests/toyos-rust-tests/src/bin/fs_client_bound.rs b/tests/toyos-rust-tests/src/bin/fs_client_bound.rs deleted file mode 100644 index 06b7ade1bb0..00000000000 --- a/tests/toyos-rust-tests/src/bin/fs_client_bound.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! A file server serves a bounded number of clients, and a client past the -//! bound is answered `ResourceExhausted` at its hello — refused by name, never -//! left waiting in the port's queue. -//! -//! Raw connections to `fs:/home`, each lending the same window, which costs -//! one region however many there are. Every other program on this boot holds -//! connections of its own, so the refusal comes at or before this process's -//! `MAX_SERVED + 1`th. - -use toyos::fs::{Reply, Request, HELLO, REPLY, WINDOW_BYTES}; -use toyos::ipc::Connection; -use toyos::shm::SharedMemory; -use toyos_abi::syscall::SyscallError; - -/// Mirrored from `userland/fsd/src/main.rs`. -const MAX_SERVED: usize = 128; - -fn answer(conn: &Connection) -> Reply { - let header = conn.recv_header().expect("a reply to the hello"); - assert_eq!(header.msg_type, REPLY, "a reply frame"); - conn.recv_payload(&header).expect("a reply's words") -} - -fn main() { - let names = toyos::endow::namespace().expect("this program was endowed a namespace"); - let window = SharedMemory::create(WINDOW_BYTES).expect("a window"); - let mut held = Vec::new(); - for n in 1..=MAX_SERVED + 1 { - let conn = names.open("fs:/home").expect("this program holds fs:/home"); - conn.send_with_handles(&[window.share().expect("the window, shared")], HELLO, &Request::new()) - .expect("hello"); - let reply = answer(&conn); - if reply.status == 0 { - held.push(conn); - continue; - } - assert_eq!( - reply.status, - SyscallError::ResourceExhausted.to_u64(), - "connection {n} was refused status {}, not ResourceExhausted", - reply.status - ); - println!("fs_client_bound: connection {n} of this process refused ResourceExhausted at its hello"); - println!("fs_client_bound: PASS"); - return; - } - panic!("{} connections of this process were all served, past the bound of {MAX_SERVED}", MAX_SERVED + 1); -} diff --git a/tests/toyos-rust-tests/src/bin/fs_restart.rs b/tests/toyos-rust-tests/src/bin/fs_restart.rs deleted file mode 100644 index be78c607489..00000000000 --- a/tests/toyos-rust-tests/src/bin/fs_restart.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! A file server that ends is survived and not hidden. -//! -//! Booted by `common::storage::fsd_restart` on `tests/fsdrestartcase`, whose -//! file servers end the moment they take a write through a file opened to -//! append at `/home/fsd_end` and before they answer it, and at the first read -//! of an installed package's manifest and of its binary this boot: -//! -//! - a launch of an installed package is answered though DATA's server ends -//! under init's read of its manifest and again under the read of its image: -//! each call is init's file worker's, whose retry connects again and waits in -//! the port's queue while init's loop starts the server again — a call from -//! the loop would wait for ever on a server only the loop could start; -//! - the write the server ended under is answered as the server's end, never -//! as done; -//! - a handle held across an end is answered `Gone`; -//! - a handle held across an end, on a file another was renamed over, is -//! answered `Gone` and writes nothing into the file now at its path; -//! - DATA's server ended four times inside init's window is not started a -//! fourth: `/home` then answers `Gone` to a new open and to a held handle. -//! -//! What is on the device is the host's to judge, off the image. - -use std::fs::{self, File, OpenOptions}; -use std::io::{ErrorKind, Read, Write}; -use std::process::Command; - -/// Mirrored in `tests/common/storage.rs`. -const KEPT: &str = "/home/fs_restart/kept"; -const ACROSS: &str = "/home/fs_restart/across"; -const REPLACEMENT: &str = "/home/fs_restart/replacement"; -const KEPT_LEN: usize = 64 * 1024 + 13; -const BEFORE: &[u8] = b"written and flushed before the server ended; "; -const REPLACING: &[u8] = b"renamed over the file a handle held across the end"; - -/// `--end-on`'s path, in `tests/fsdrestartcase/system.toml`. -const END: &str = "/home/fsd_end"; - -/// The package, whose manifest and binary are `--end-at-read`'s paths. -const PACKAGE: &str = "fs_restart"; -const MANIFEST: &str = "/apps/fs_restart/manifest.toml"; -const PROGRAM: &str = "/apps/fs_restart/fs_restart"; -const SELF: &str = "/system/bin/test_rs_fs_restart"; -/// What tells this binary it is the package's copy, launched. -const LAUNCHED: &str = "launched"; - -/// Mirrored: what `KEPT` holds. -fn kept() -> Vec { - (0..KEPT_LEN).map(|i| (i.wrapping_mul(37) ^ 0xC3) as u8).collect() -} - -/// End DATA's server: a write it takes and never answers. -fn end_the_server(n: u32) { - let mut f = OpenOptions::new() - .append(true) - .create(true) - .open(END) - .unwrap_or_else(|e| panic!("end {n}: open {END} to append: {e}")); - match f.write(b"the write the server ends under") { - Err(e) if e.kind() == ErrorKind::StaleNetworkFileHandle => { - println!("fs_restart: end {n}: the write was answered as the server's end ({e})"); - } - Err(e) => panic!("end {n}: the write was refused with {e} ({:?}), not the server's end", e.kind()), - Ok(n) => panic!("end {n}: the write the server ended under was answered done, {n} bytes"), - } -} - -/// This binary installed as a package, by writes alone — no read of either -/// file before init's — and durable, since an end loses what no sync covered. -fn install() { - fs::create_dir_all(format!("/apps/{PACKAGE}")).expect("make the package's directory"); - fs::copy(SELF, PROGRAM).unwrap_or_else(|e| panic!("copy {SELF} to {PROGRAM}: {e}")); - OpenOptions::new() - .write(true) - .open(PROGRAM) - .and_then(|f| f.sync_all()) - .unwrap_or_else(|e| panic!("sync {PROGRAM}: {e}")); - let manifest = format!( - "name = \"{PACKAGE}\"\nversion = \"1\"\ndigest = \"{}\"\nprogram = \"{PROGRAM}\"\n", - "0".repeat(64) - ); - let mut f = File::create(MANIFEST).unwrap_or_else(|e| panic!("create {MANIFEST}: {e}")); - f.write_all(manifest.as_bytes()).unwrap_or_else(|e| panic!("write {MANIFEST}: {e}")); - f.sync_all().unwrap_or_else(|e| panic!("sync {MANIFEST}: {e}")); -} - -fn main() { - if std::env::args().nth(1).as_deref() == Some(LAUNCHED) { - println!("fs_restart: running from {PROGRAM}"); - return; - } - - // Ends 1 and 2: init's reads of the manifest and of the image. - install(); - let status = - Command::new(PROGRAM).arg(LAUNCHED).status().unwrap_or_else(|e| panic!("launch {PROGRAM}: {e}")); - assert!(status.success(), "{PROGRAM}, launched across two ends, exited {status}"); - println!("fs_restart: ends 1 and 2: the launch of {PROGRAM} was answered and it ran"); - - fs::create_dir_all("/home/fs_restart").expect("make /home/fs_restart"); - let mut f = File::create(KEPT).expect("create the kept file"); - f.write_all(&kept()).expect("write the kept file"); - f.sync_all().expect("the kept file is durable"); - drop(f); - let mut held = File::open(KEPT).expect("hold the kept file"); - let mut across = File::create(ACROSS).expect("create the file written across the end"); - across.write_all(BEFORE).expect("write before the end"); - across.sync_all().expect("durable before the end"); - // Another file renamed over the one `across` holds, durably. - let mut replacement = File::create(REPLACEMENT).expect("create the replacement"); - replacement.write_all(REPLACING).expect("write the replacement"); - replacement.sync_all().expect("the replacement is durable"); - drop(replacement); - fs::rename(REPLACEMENT, ACROSS).expect("rename the replacement over the held file"); - File::open(ACROSS).and_then(|f| f.sync_all()).expect("the rename is durable"); - - end_the_server(3); - - match held.read(&mut [0u8; 16]) { - Err(e) if e.kind() == ErrorKind::StaleNetworkFileHandle => {} - other => panic!("a handle held across the end read {other:?}, not Gone"), - } - match across.write_all(b"written into whatever is at the path now") { - Err(e) if e.kind() == ErrorKind::StaleNetworkFileHandle => { - println!("fs_restart: a handle on a file renamed over is answered Gone ({e})"); - } - Err(e) => panic!("a handle on a file renamed over was refused {e} ({:?}), not Gone", e.kind()), - Ok(()) => panic!("a handle on a file renamed over wrote into the file now at its path"), - } - drop((held, across)); - let mut back = Vec::new(); - File::open(KEPT).and_then(|mut f| f.read_to_end(&mut back)).expect("a new open reads the kept file"); - assert!(back == kept(), "a new open read {} bytes, not the kept file", back.len()); - let mut whole = Vec::new(); - File::open(ACROSS).and_then(|mut f| f.read_to_end(&mut whole)).expect("read the file at the held path"); - assert_eq!(whole, REPLACING, "the file at the path a handle held across the end"); - println!("fs_restart: the server came back; held handles answered Gone, a new open read the flushed file"); - - let mut held = File::open(KEPT).expect("hold the kept file for the last end"); - - end_the_server(4); - - match File::open(KEPT) { - Err(e) if e.kind() == ErrorKind::StaleNetworkFileHandle => { - println!("fs_restart: after four ends a new open is answered Gone ({e})"); - } - Err(e) => panic!("after four ends a new open was refused {e} ({:?}), not Gone", e.kind()), - Ok(_) => panic!("after four ends a new open of {KEPT} was answered"), - } - match held.read(&mut [0u8; 16]) { - Err(e) if e.kind() == ErrorKind::StaleNetworkFileHandle => { - println!("fs_restart: and a held handle is answered Gone ({e})"); - } - other => panic!("after four ends a held handle read {other:?}, not Gone"), - } - println!("fs_restart: PASS"); -} diff --git a/tests/toyos-rust-tests/src/bin/fsync_flush_failed.rs b/tests/toyos-rust-tests/src/bin/fsync_flush_failed.rs deleted file mode 100644 index 848b148ad70..00000000000 --- a/tests/toyos-rust-tests/src/bin/fsync_flush_failed.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! Two fsyncs under a device that refuses SYNCHRONIZE CACHE (`usb-flush-fails`): -//! both must refuse. The second returning success is the F5 lie — the failed -//! device commit forgotten. `tests/common/volumes.rs::fsync_failed_commit` boots this. - -use std::fs::File; -use std::io::Write; - -const PATH: &str = "/log/f5-flush-failed.bin"; - -fn main() { - let mut f = File::create(PATH).expect("create on /log"); - f.write_all(&[0xC3u8; 2 * 4096 + 33]).expect("write"); - - let first = f.sync_all(); - println!("first fsync: {first:?}"); - assert!(first.is_err(), "fsync reported success while the device refused its cache flush"); - - let second = f.sync_all(); - println!("second fsync: {second:?}"); - assert!( - second.is_err(), - "the second fsync reported success without reaching the device — the failed commit \ - was forgotten" - ); - println!("both fsyncs refused: the failed device commit stays owed"); -} diff --git a/tests/toyos-rust-tests/src/bin/gpu_scanout_swap.rs b/tests/toyos-rust-tests/src/bin/gpu_scanout_swap.rs deleted file mode 100644 index a78b13a49c3..00000000000 --- a/tests/toyos-rust-tests/src/bin/gpu_scanout_swap.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! Three mode changes in a row while this process keeps every scanout it was -//! ever handed mapped and stamped. The pages a swap retires live as long as a -//! holder maps them; what the device may still reach is the host's question, -//! answered by `iommu_gpu_scanout_swap` over the tables the unit walks. - -use toyos::device::FramebufferDev; -use toyos::endow::Endowments; -use toyos::shm::SharedMemory; -use toyos::syscap::SysCap; -use toyos_abi::syscall::{DeviceType, SYSCAP_LABEL}; - -/// Mirrored in `tests/common/iommu.rs`; none is the mode a virtio-gpu boots at. -const MODES: [(u32, u32); 3] = [(800, 600), (1024, 768), (640, 480)]; -const STAMP: [u8; 8] = *b"scanout!"; - -fn main() { - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability"); - let fb: FramebufferDev = - cap.claim(DeviceType::Framebuffer).expect("this machine has a display"); - let mut info = fb.info().expect("a claim describes the display it claimed"); - println!("gpu: claimed {}x{} stride={}", info.width, info.height, info.stride); - - let mut held: Vec = Vec::new(); - for &(width, height) in &MODES { - assert_ne!((info.width, info.height), (width, height), "already in the mode asked for"); - let bytes = (info.stride * info.height * 4) as usize; - let mut front = SharedMemory::adopt(info.scanout[0], bytes) - .expect("the scanout buffer the description just handed over"); - front.as_mut_slice()[..STAMP.len()].copy_from_slice(&STAMP); - held.push(front); - - info = fb.set_resolution(width, height).expect("a virtio-gpu can change mode"); - assert_eq!((info.width, info.height), (width, height), "the call answered another mode"); - fb.present(0, 0, 0, 0).expect("present the new scanout"); - - for (n, buffer) in held.iter().enumerate() { - assert_eq!( - &buffer.as_slice()[..STAMP.len()], - &STAMP, - "scanout {n} is not this process's any more after the change to {width}x{height}", - ); - } - println!( - "gpu: {width}x{height} set, {} retired scanout(s) still mapped and stamped", - held.len() - ); - } - println!("===GPU_SCANOUT_SWAP_OK==="); -} diff --git a/tests/toyos-rust-tests/src/bin/gpu_set_resolution.rs b/tests/toyos-rust-tests/src/bin/gpu_set_resolution.rs deleted file mode 100644 index 9fa872fd3be..00000000000 --- a/tests/toyos-rust-tests/src/bin/gpu_set_resolution.rs +++ /dev/null @@ -1,82 +0,0 @@ -//! A mode change that *succeeds*, which needs a display GOP is not: the new -//! framebuffer, the old one's release, the fresh scanout objects and -//! `device::set_framebuffer_info`'s update all live past the `NotSupported` -//! every other machine here answers with. The second claim is the point — what -//! the call returned is the driver talking about itself, and what a fresh -//! claim is told comes out of the registry the mode change had to update. - -use std::time::Duration; - -use toyos::device::FramebufferDev; -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos_abi::syscall::{DeviceType, SyscallError, SYSCAP_LABEL}; - -/// Not the 1280x800 a virtio-gpu boots at, so the driver's "already this size" -/// early return cannot answer for the call. -const WANT: (u32, u32) = (800, 600); - -fn main() { - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability"); - - let fb: FramebufferDev = - cap.claim(DeviceType::Framebuffer).expect("this machine has a display"); - let before = fb.info().expect("a claim describes the display it claimed"); - println!("gpu: claimed {}x{} stride={}", before.width, before.height, before.stride); - assert_ne!( - (before.width, before.height), - WANT, - "the machine already boots at the mode this asks for, so the call proves nothing", - ); - - let after = fb.set_resolution(WANT.0, WANT.1).expect("a virtio-gpu can change mode"); - assert_eq!((after.width, after.height), WANT, "the call answered another mode"); - assert!(after.stride >= WANT.0, "stride {} is under the width", after.stride); - assert_ne!( - after.scanout, before.scanout, - "the answer names the old scanout objects, so nothing was reallocated", - ); - // The new buffer reaches the device, which is what the host then reads. - fb.present(0, 0, 0, 0).expect("present the new scanout"); - - // Released, so the next description comes out of the registry. - drop(fb); - let again: FramebufferDev = reclaim(&cap); - let told = again.info().expect("the second claim describes the display"); - assert_eq!( - (told.width, told.height, told.stride), - (after.width, after.height, after.stride), - "the call said {}x{} stride={} and a second claim is told {}x{} stride={}", - after.width, - after.height, - after.stride, - told.width, - told.height, - told.stride, - ); - - println!( - "gpu: set {}x{} stride={}, and a second claim is told the same", - told.width, told.height, told.stride - ); - println!("===GPU_RESOLUTION_OK==="); -} - -/// The claim again, once the release the last close *queued* has run. -/// `issues/kernel/deferred-release-outlives-its-syscall.md` is the kernel half: -/// `AlreadyExists` here is that tracked defect, not another holder, and a claim -/// never released is a hang the harness ceiling reds. -fn reclaim(cap: &SysCap) -> FramebufferDev { - loop { - match cap.claim(DeviceType::Framebuffer) { - Ok(fb) => return fb, - Err(SyscallError::AlreadyExists) => std::thread::sleep(RECLAIM_STEP), - Err(e) => panic!("the second claim was refused {e:?}"), - } - } -} - -/// A pace and never a verdict. -const RECLAIM_STEP: Duration = Duration::from_millis(10); diff --git a/tests/toyos-rust-tests/src/bin/gsbase_locked.rs b/tests/toyos-rust-tests/src/bin/gsbase_locked.rs deleted file mode 100644 index 5e7c0e731da..00000000000 --- a/tests/toyos-rust-tests/src/bin/gsbase_locked.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! The GS-base primitive is `#UD` at Ring 3 here. Its probe is a child, so the -//! #UD kills the child and this parent plus `echo` outlive a Ring 3 -> 0 write. - -use std::process::Command; - -fn main() { - let status = Command::new("/system/bin/test_rs_gsbase_probe").status().expect("spawn gsbase_probe"); - if status.success() { - println!("FAIL the gsbase primitive is present at ring 3 (exit {:?})", status.code()); - std::process::exit(1); - } - let out = Command::new("/system/bin/echo").arg("still alive").output().expect("spawn echo"); - assert_eq!( - String::from_utf8_lossy(&out.stdout).trim(), - "still alive", - "the machine survived the probe but can no longer start a process", - ); - println!("PASS rdgsbase/wrgsbase are #UD at ring 3; per-CPU state intact"); -} diff --git a/tests/toyos-rust-tests/src/bin/gsbase_probe.rs b/tests/toyos-rust-tests/src/bin/gsbase_probe.rs deleted file mode 100644 index a3bb04ad8b3..00000000000 --- a/tests/toyos-rust-tests/src/bin/gsbase_probe.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! The GS-base primitive from Ring 3: `#UD` where the kernel took -//! `CR4.FSGSBASE` away, a leaked per-CPU pointer where it did not. - -fn main() { - let base: u64; - unsafe { - core::arch::asm!("rdgsbase {b}", "wrgsbase {b}", b = out(reg) base, options(nomem, nostack)); - } - println!("gsbase-primitive-present base={base:#018x}"); -} diff --git a/tests/toyos-rust-tests/src/bin/heap_ceiling.rs b/tests/toyos-rust-tests/src/bin/heap_ceiling.rs deleted file mode 100644 index b35b6a44fc9..00000000000 --- a/tests/toyos-rust-tests/src/bin/heap_ceiling.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! The kernel heap's ceiling. -//! -//! `KernelPageSource` hands dlmalloc one 2 MiB page and can hand it no more, -//! so `mm::MAX_HEAP_ALLOC` is the largest single allocation the kernel heap -//! can serve. Asking for more is a kernel bug and halts the machine, which -//! `heap_over_ceiling_halts` asserts on a boot of its own. - -// `SYS_DEBUG` actions a `test-actuators` kernel provides. The first two take -// one kernel heap allocation each and release it again — at -// `mm::MAX_HEAP_ALLOC`, and at `MAX_HEAP_ALLOC` with 4096-byte alignment; the -// last lowers `SYS_SYSINFO`'s thread bound to the machine's live threads. -use toyos_abi::syscall::debug_action::{ - HEAP_AT_CEILING, HEAP_AT_CEILING_PAGE_ALIGNED, LOWER_SYSINFO_BOUND, -}; - -/// `SyscallError::ResourceExhausted`, as `SyscallError::to_u64` encodes it. -const RESOURCE_EXHAUSTED: u64 = u64::MAX - 7; - -fn main() { - at_ceiling_is_servable(); - aligned_at_ceiling_is_refused_not_fatal(); - sysinfo_refuses_rather_than_allocating_past_the_ceiling(); - println!("all heap ceiling tests passed"); -} - -/// A syscall whose allocation is derived from something userland grows. -/// -/// `SYS_SYSINFO` collects one 24-byte entry per live thread so it can sort -/// them, and the caller's buffer bounds what is *written*, not what is built. -/// Nothing caps the thread count, so ~87,000 threads made an ordinary syscall -/// ask the heap for more than `MAX_HEAP_ALLOC` and trip the assert three -/// functions above — from any process, with no privilege. -/// -/// [`LOWER_SYSINFO_BOUND`] puts the machine's live threads in -/// `MAX_SYSINFO_THREADS`'s place, because 65,536 threads is 8 GiB of kernel -/// stacks and no guest can make them. The count, the comparison and the -/// refusal are the shipped ones. -/// -/// **Armed here rather than compiled in, and the arming is itself an -/// assertion**: the bound is the shipped 65,536 until this call, so a kernel -/// that answered it and did nothing would fail at the loop below rather than -/// pass. As a `#[cfg]` the 16 rode into every kernel the suite booted, and -/// `SYS_SYSINFO` answered against it in every guest. -fn sysinfo_refuses_rather_than_allocating_past_the_ceiling() { - use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::Arc; - - let live = sysinfo_live().expect("sysinfo already refuses with no threads of ours"); - let rc = toyos_abi::syscall::debug(LOWER_SYSINFO_BOUND); - assert_eq!(rc, 0, "SYS_DEBUG {LOWER_SYSINFO_BOUND} did not lower the bound (rc={rc:#x})"); - - let stop = Arc::new(AtomicBool::new(false)); - let mut parked = Vec::new(); - let mut refused_at = None; - // Past the bound with room, and far short of anything that would matter - // to a guest with one CPU. - for i in 0..64 { - let flag = Arc::clone(&stop); - parked.push(std::thread::spawn(move || { - while !flag.load(Ordering::Relaxed) { - std::thread::sleep(std::time::Duration::from_millis(5)); - } - })); - if sysinfo_live().is_none() { - refused_at = Some(i + 1); - break; - } - } - - let at = refused_at.unwrap_or_else(|| { - stop.store(true, Ordering::Relaxed); - panic!("64 extra threads and sysinfo never refused — its collection is unbounded") - }); - - stop.store(true, Ordering::Relaxed); - for t in parked { - t.join().expect("join a parked thread"); - } - // A bound, not a one-way door: with the threads gone it answers again. - assert!(sysinfo_live().is_some(), "sysinfo stayed refused after the threads exited"); - println!( - " PASS: sysinfo refused past its bound at {at} extra threads over {live} live before arming, and recovered" - ); -} - -/// The live threads `SYS_SYSINFO`'s header counts, or `None` when it refused. -/// The ABI wrapper reports an error as `0`, and the header is the smallest -/// buffer it accepts. -fn sysinfo_live() -> Option { - let mut buf = [0u8; toyos::system::SYSINFO_HEADER_SIZE]; - (toyos::system::sysinfo(&mut buf) == buf.len()) - .then(|| toyos_abi::syscall::SysinfoHeader::decode(&buf).entries) -} - -/// The documented ceiling is a size the heap actually serves. -/// -/// This process makes the call itself, so a kernel that asserts here, or an -/// allocation that comes back null, kills this test. `MAX_HEAP_ALLOC` is -/// `PAGE_2M - 4096` and the 4 KiB is headroom for dlmalloc's own chunk and -/// segment bookkeeping — arithmetic that was reasoned about and never run. -/// -/// It is also the negative side of `heap_over_ceiling_halts`: an assert that -/// simply refused every large allocation would satisfy that one and fail this. -fn at_ceiling_is_servable() { - let rc = toyos_abi::syscall::debug(HEAP_AT_CEILING); - assert_eq!( - rc, 0, - "an allocation at MAX_HEAP_ALLOC was refused (rc={rc:#x}) — the documented \ - ceiling is above the real one" - ); - println!(" PASS: MAX_HEAP_ALLOC is servable"); -} - -/// The same size, page-aligned, is more than the page source can back — and -/// that is an error return, not a dead machine. -/// -/// `memalign` pads by the alignment before it asks for backing, so this request -/// satisfies `MAX_HEAP_ALLOC` and still reaches the page source -/// asking for 2,162,688 bytes. -fn aligned_at_ceiling_is_refused_not_fatal() { - let rc = toyos_abi::syscall::debug(HEAP_AT_CEILING_PAGE_ALIGNED); - assert_eq!( - rc, RESOURCE_EXHAUSTED, - "a page-aligned allocation at MAX_HEAP_ALLOC returned {rc:#x}; expected the \ - page source to refuse it" - ); - println!(" PASS: an allocation the page source cannot back is refused, not fatal"); -} diff --git a/tests/toyos-rust-tests/src/bin/hierarchy_paths.rs b/tests/toyos-rust-tests/src/bin/hierarchy_paths.rs index b3446ceade3..abbfb87fb49 100644 --- a/tests/toyos-rust-tests/src/bin/hierarchy_paths.rs +++ b/tests/toyos-rust-tests/src/bin/hierarchy_paths.rs @@ -5,9 +5,6 @@ //! because some volume happens to carry a directory by that name; `/` is no //! filesystem either, so every syscall that would change what is at it is //! refused the way a read-only mount refuses one, and the machine survives it. -//! -//! The two files this writes are the other half of -//! `apps_and_home_are_one_filesystem` in `tests/common/storage.rs`. use std::fs; use std::io::Write; @@ -19,8 +16,6 @@ use toyos_abi::syscall::{self, OpenFlags, SyscallError}; const ROOT_ENTRIES: [&str; 9] = ["apps", "boot", "config", "home", "log", "media", "state", "system", "tmp"]; -/// Mirrored in `tests/common/storage.rs`, whose reader sees them without the -/// mount point, inside the one volume. const IN_HOME: &str = "/home/hierarchy-home.bin"; const IN_APPS: &str = "/apps/hierarchy-apps.bin"; const LEN: usize = 2 * 4096 + 61; diff --git a/tests/toyos-rust-tests/src/bin/home_absent.rs b/tests/toyos-rust-tests/src/bin/home_absent.rs deleted file mode 100644 index 3d3071f4e71..00000000000 --- a/tests/toyos-rust-tests/src/bin/home_absent.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! On a boot where the DATA volume is ours and did not mount, `/apps`, -//! `/config`, `/home` and `/state` must never come back as a place to write: -//! the kernel's own log line and the byte-identical image (asserted on the -//! host, in `tests/common/storage.rs`) do not observe that from inside the -//! guest — this does. Driven by `broken_data_volume_is_absent` and -//! `data_candidate_with_bad_geometry_is_absent` alone: every other boot mounts -//! the four, on the DATA volume or on a tmpfs, and every check below would -//! fail on it. - -use std::io::ErrorKind; - -fn main() { - let mut wrong = Vec::new(); - - for dir in ["/apps", "/config", "/home", "/state"] { - match std::fs::write(format!("{dir}/x"), b"should never land") { - Err(e) if e.kind() == ErrorKind::PermissionDenied => {} - other => wrong.push(format!("writing {dir}/x: {other:?}, want PermissionDenied")), - } - } - match std::env::set_current_dir("/home/toy") { - Err(e) if e.kind() == ErrorKind::NotFound => {} - other => wrong.push(format!("chdir /home/toy: {other:?}, want NotFound")), - } - match std::fs::read_dir("/home") { - Ok(entries) => { - let names: Vec<_> = entries.map(|e| e.expect("a readdir entry").file_name()).collect(); - if !names.is_empty() { - wrong.push(format!("/home lists {names:?}, want empty")); - } - } - Err(e) => wrong.push(format!("listing /home: {e:?}, want Ok(empty)")), - } - - assert!(wrong.is_empty(), "an absent DATA volume was not absent:\n{}", wrong.join("\n")); - println!( - "home-absent: /apps, /config, /home and /state refused every write, and /home/toy every \ - chdir and listing" - ); -} diff --git a/tests/toyos-rust-tests/src/bin/home_overwrite_zero.rs b/tests/toyos-rust-tests/src/bin/home_overwrite_zero.rs deleted file mode 100644 index cd4682d4c14..00000000000 --- a/tests/toyos-rust-tests/src/bin/home_overwrite_zero.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! A same-length overwrite of a `/home` file, read back through the name the -//! overwrite rebound. -//! -//! `File::create` unlinks what answered to the path and creates a new file -//! under the same name; a handle still holding the unlinked file keeps it -//! alive, so its teardown runs after the new file has taken the name. The -//! re-read must give back the bytes just written, not the empty entry the -//! create left on the device. -//! -//! Host half: `home_overwrite_reads_back` in `tests/common/storage.rs`. - -use std::fs::{self, File}; -use std::io::{Read, Write}; - -/// Mirrored in `tests/common/storage.rs`, whose reader sees these names without -/// the mount point: `/home` is a directory of DATA. -const PINNED: &str = "/home/overwrite-pinned.bin"; -const LOOPED: &str = "/home/overwrite-looped.bin"; -const LEN: usize = 1_902_104; - -fn payload(seed: u8) -> Vec { - (0..LEN).map(|i| (i.wrapping_mul(131) ^ seed as usize) as u8).collect() -} - -fn main() { - let first = payload(0x11); - let second = payload(0x22); - recorded_shape(&first, &second); - // The volume and every write so far on the device, so the pinned file's - // writes are the ones only the stop's sync carries there. - File::open(LOOPED) - .and_then(|f| f.sync_all()) - .unwrap_or_else(|e| panic!("fsync {LOOPED}: {e}")); - pinned_overwrite(&first, &second); - println!("all home overwrite tests passed"); -} - -/// The recorded shape, with no handle held across anything. -fn recorded_shape(first: &[u8], second: &[u8]) { - fs::write(LOOPED, first).unwrap_or_else(|e| panic!("write {LOOPED}: {e}")); - let a = fs::read(LOOPED).unwrap_or_else(|e| panic!("read {LOOPED}: {e}")).len(); - fs::write(LOOPED, second).unwrap_or_else(|e| panic!("overwrite {LOOPED}: {e}")); - let b = fs::read(LOOPED).unwrap_or_else(|e| panic!("re-read {LOOPED}: {e}")).len(); - println!(" recorded shape: read back {a} then {b}"); - assert_eq!((a, b), (LEN, LEN), "the recorded shape"); -} - -/// The same overwrite with the displaced file's teardown made to land after the -/// new file holds the name: a reader is held across the `File::create`. -fn pinned_overwrite(first: &[u8], second: &[u8]) { - fs::write(PINNED, first).unwrap_or_else(|e| panic!("write {PINNED}: {e}")); - let held = File::open(PINNED).unwrap_or_else(|e| panic!("open {PINNED}: {e}")); - let mut writer = File::create(PINNED).unwrap_or_else(|e| panic!("create {PINNED}: {e}")); - writer.write_all(second).unwrap_or_else(|e| panic!("overwrite {PINNED}: {e}")); - drop(held); - - // One reading and no window: the device the host reads after the shutdown's - // drain is the time-free judge, and this length is what it is held against. - let mut lowest = fs::metadata(PINNED).unwrap_or_else(|e| panic!("stat {PINNED}: {e}")).len(); - let mut got = Vec::new(); - File::open(PINNED) - .unwrap_or_else(|e| panic!("re-open {PINNED}: {e}")) - .read_to_end(&mut got) - .unwrap_or_else(|e| panic!("re-read {PINNED}: {e}")); - lowest = lowest.min(got.len() as u64); - // Before any verdict: the host holds this count against the device, and needs it on the failing arm. - println!("HOME-OVERWRITE {PINNED} read back {lowest} bytes"); - - drop(writer); - - assert_eq!(lowest, LEN as u64, "the same-length overwrite read back short"); - assert!(got == second, "{PINNED} read back bytes that are not the overwrite's"); - println!(" PASS {PINNED} answered {LEN} bytes at its stat and its read"); -} diff --git a/tests/toyos-rust-tests/src/bin/https_fetch.rs b/tests/toyos-rust-tests/src/bin/https_fetch.rs deleted file mode 100644 index e2e3cb5de3b..00000000000 --- a/tests/toyos-rust-tests/src/bin/https_fetch.rs +++ /dev/null @@ -1,286 +0,0 @@ -//! Fetches one URL with `ureq` over rustls and prints the body's length and SHA-256. -//! -//! The crates are crates.io's, unpatched: this binary exists so that what a -//! Linux program writes is what ToyOS runs. Every outcome is one line — -//! `ok bytes= sha256=` or `refused `, so a verification failure -//! never reaches the caller as a truncated or empty body. -//! -//! A constraint on scheme, version or authority is set on the client handed to -//! the library, never checked on the argument: the peer chooses every hop after -//! the first. So `https_only` refuses a cleartext hop wherever a redirect puts -//! one, and TLS 1.3 is the only version offered — ureq's connector hardcodes -//! `ALL_VERSIONS`, so it is set on a `ClientConfig` of ours through -//! `Agent::with_parts`, its documented extension point rather than a patch. - -use std::io::{Read, Write}; -use std::sync::Arc; - -use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned}; -use rustls_pki_types::{CertificateDer, ServerName}; -use sha2::{Digest, Sha256}; -use ureq::config::Config; -use ureq::unversioned::resolver::DefaultResolver; -use ureq::unversioned::transport::{ - Buffers, ConnectionDetails, Connector, Either, LazyBuffers, NextTimeout, TcpConnector, - Transport, TransportAdapter, -}; -use ureq::{Agent, Error}; - -/// A body larger than this is refused rather than buffered: the caller asked -/// for a hash of what it fetched, and an unbounded read answers a hostile -/// server with the guest's whole heap. -const MAX_BODY: u64 = 16 * 1024 * 1024; - -fn main() { - let args: Vec = std::env::args().collect(); - let mut url = None; - let mut ca_path = None; - let mut i = 1; - while i < args.len() { - match args[i].as_str() { - "--ca" => { - i += 1; - ca_path = args.get(i).cloned(); - } - other => url = Some(other.to_string()), - } - i += 1; - } - let Some(url) = url else { - println!("https_fetch: usage: https_fetch [--ca ]"); - std::process::exit(2); - }; - - match fetch(&url, ca_path.as_deref()) { - Ok((len, hash)) => println!("https_fetch: ok bytes={len} sha256={hash}"), - Err(reason) => { - println!("https_fetch: refused {reason}"); - if reason.starts_with("unclassified") { - std::process::exit(1); - } - } - } -} - -fn fetch(url: &str, ca_path: Option<&str>) -> Result<(usize, String), String> { - let roots = roots(ca_path)?; - let agent = agent(roots)?; - - let response = agent.get(url).call().map_err(|e| refusal(&e))?; - let mut reader = response.into_body().into_reader().take(MAX_BODY + 1); - let mut body = Vec::new(); - reader - .read_to_end(&mut body) - .map_err(|e| format!("unclassified: read body: {e}"))?; - if body.len() as u64 > MAX_BODY { - return Err("body-too-large".to_string()); - } - - let digest = Sha256::digest(&body); - let mut hex = String::with_capacity(64); - for byte in digest { - use std::fmt::Write as _; - let _ = write!(hex, "{byte:02x}"); - } - Ok((body.len(), hex)) -} - -/// Mozilla's roots as any program gets them, plus the caller's extra CA when it -/// named one. The extra root is added, never substituted for verification. -fn roots(ca_path: Option<&str>) -> Result { - let mut store = RootCertStore { - roots: webpki_roots::TLS_SERVER_ROOTS.to_vec(), - }; - let Some(path) = ca_path else { - return Ok(store); - }; - let pem = std::fs::read(path).map_err(|e| format!("unclassified: read {path}: {e}"))?; - let certs = pem_certificates(&pem); - if certs.is_empty() { - return Err(format!("unclassified: {path} holds no certificate")); - } - let (added, ignored) = store.add_parsable_certificates(certs); - if added == 0 { - return Err(format!("unclassified: {path}: {ignored} unparsable certificates")); - } - Ok(store) -} - -/// PEM decoding by hand, because the ToyOS side of this program is meant to be -/// exactly the dependency set the brief names and nothing else. -fn pem_certificates(pem: &[u8]) -> Vec> { - const BEGIN: &str = "-----BEGIN CERTIFICATE-----"; - const END: &str = "-----END CERTIFICATE-----"; - let text = String::from_utf8_lossy(pem); - let mut out = Vec::new(); - let mut rest = text.as_ref(); - while let Some(start) = rest.find(BEGIN) { - let after = &rest[start + BEGIN.len()..]; - let Some(end) = after.find(END) else { break }; - let base64: String = after[..end].chars().filter(|c| !c.is_whitespace()).collect(); - if let Some(der) = base64_decode(&base64) { - out.push(CertificateDer::from(der)); - } - rest = &after[end + END.len()..]; - } - out -} - -fn base64_decode(text: &str) -> Option> { - let value = |c: u8| -> Option { - Some(match c { - b'A'..=b'Z' => u32::from(c - b'A'), - b'a'..=b'z' => u32::from(c - b'a') + 26, - b'0'..=b'9' => u32::from(c - b'0') + 52, - b'+' => 62, - b'/' => 63, - _ => return None, - }) - }; - let body = text.trim_end_matches('='); - let mut out = Vec::with_capacity(body.len() * 3 / 4); - let mut acc = 0u32; - let mut bits = 0u32; - for c in body.bytes() { - acc = (acc << 6) | value(c)?; - bits += 6; - if bits >= 8 { - bits -= 8; - out.push((acc >> bits) as u8); - } - } - Some(out) -} - -fn agent(roots: RootCertStore) -> Result { - let provider = Arc::new(rustls_rustcrypto::provider()); - let config = ClientConfig::builder_with_provider(provider) - .with_protocol_versions(&[&rustls::version::TLS13]) - .map_err(|e| format!("unclassified: rustls versions: {e}"))? - .with_root_certificates(roots) - .with_no_client_auth(); - let connector = () - .chain(TcpConnector::default()) - .chain(Tls13Connector { config: Arc::new(config) }); - Ok(Agent::with_parts( - Config::builder().https_only(true).build(), - connector, - DefaultResolver::default(), - )) -} - -#[derive(Debug)] -struct Tls13Connector { - config: Arc, -} - -impl Connector for Tls13Connector { - type Out = Either; - - fn connect( - &self, - details: &ConnectionDetails, - chained: Option, - ) -> Result, Error> { - let transport = chained.ok_or(Error::ConnectionFailed)?; - if !details.needs_tls() || transport.is_tls() { - return Ok(Some(Either::A(transport))); - } - let host = details - .uri - .authority() - .ok_or_else(|| Error::BadUri("no authority".to_string()))? - .host(); - let name: ServerName<'static> = ServerName::try_from(host) - .map_err(|_| Error::Tls("not a server name"))? - .to_owned(); - let conn = ClientConnection::new(self.config.clone(), name)?; - let buffers = LazyBuffers::new( - details.config.input_buffer_size(), - details.config.output_buffer_size(), - ); - Ok(Some(Either::B(Tls13Transport { - buffers, - stream: StreamOwned { - conn, - sock: TransportAdapter::new(transport.boxed()), - }, - }))) - } -} - -struct Tls13Transport { - buffers: LazyBuffers, - stream: StreamOwned, -} - -impl std::fmt::Debug for Tls13Transport { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("Tls13Transport") - } -} - -impl Transport for Tls13Transport { - fn buffers(&mut self) -> &mut dyn Buffers { - &mut self.buffers - } - - fn transmit_output(&mut self, amount: usize, timeout: NextTimeout) -> Result<(), Error> { - self.stream.get_mut().set_timeout(timeout); - let output = &self.buffers.output()[..amount]; - self.stream.write_all(output)?; - Ok(()) - } - - fn await_input(&mut self, timeout: NextTimeout) -> Result { - self.stream.get_mut().set_timeout(timeout); - let input = self.buffers.input_append_buf(); - let amount = self.stream.read(input)?; - self.buffers.input_appended(amount); - Ok(amount > 0) - } - - fn is_open(&mut self) -> bool { - self.stream.get_mut().get_mut().is_open() - } - - fn is_tls(&self) -> bool { - true - } -} - -/// rustls reaches the caller as an `io::Error` carrying the real one, so the -/// name comes from the downcast and never from the message text. -fn refusal(err: &Error) -> String { - if let Error::RequireHttpsOnly(_) = err { - return "plain-http".to_string(); - } - if let Error::Io(io) = err { - if let Some(tls) = io.get_ref().and_then(|e| e.downcast_ref::()) { - return tls_refusal(tls); - } - } - format!("unclassified: {err}") -} - -fn tls_refusal(err: &rustls::Error) -> String { - use rustls::CertificateError as C; - use rustls::Error as E; - use rustls::PeerIncompatible as P; - match err { - E::InvalidCertificate(C::UnknownIssuer) => "unknown-authority".to_string(), - E::InvalidCertificate(C::Expired | C::ExpiredContext { .. }) => { - "certificate-expired".to_string() - } - E::InvalidCertificate(C::NotValidForName | C::NotValidForNameContext { .. }) => { - "hostname-mismatch".to_string() - } - E::PeerIncompatible( - P::Tls12NotOfferedOrEnabled - | P::ServerTlsVersionIsDisabledByOurConfig - | P::ServerDoesNotSupportTls12Or13, - ) => "downgrade-refused".to_string(), - E::AlertReceived(rustls::AlertDescription::ProtocolVersion) => "tls12-refused".to_string(), - other => format!("unclassified: tls: {other}"), - } -} diff --git a/tests/toyos-rust-tests/src/bin/i8042_keyboard.rs b/tests/toyos-rust-tests/src/bin/i8042_keyboard.rs deleted file mode 100644 index ded30681e31..00000000000 --- a/tests/toyos-rust-tests/src/bin/i8042_keyboard.rs +++ /dev/null @@ -1,74 +0,0 @@ -//! Claims the keyboard and prints what arrives, one line per event. -//! -//! Driven by host tests that boot a guest with no USB HID at all and -//! inject through QMP once the ready line appears. Not a standalone test: on -//! its own it would time out with nothing to report, which is why it is in -//! RUST_SKIP. -//! -//! It holds a [`Translator`] because the kernel no longer does: the claim carries -//! a HID usage and a modifier mask, and what those type is a layout, which is -//! userland's. This is the same type and the same call `/system/bin/console` and -//! every window client make, so `tr=` below is what a real surface would put -//! on a real shell's stdin. - -use std::time::Duration; -use toyos::device::Keyboard; -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos_abi::syscall::{DeviceType, SYSCAP_LABEL}; -use toyos_abi::input::RawKeyEvent; - -const EVENT_SIZE: usize = std::mem::size_of::(); - -/// The host's end-of-run marker: the HID usage for the End key. None of this -/// binary's callers' own injections presses it, so its release is -/// unambiguous — the same shape as `input_events.rs`'s right-button release. -/// No deadline: a lost sentinel is a hang the host's ceiling reds. -const SENTINEL: u8 = 0x4D; - -fn main() { - let keyboard: Keyboard = - capability().claim(DeviceType::Keyboard).expect("i8042_keyboard: no keyboard device"); - let mut translator = window::configured_translator(); - println!("===I8042_READY==="); - - let mut buf = [0u8; 512]; - let mut seen = 0; - let mut ended = false; - while !ended { - let n = keyboard.read_nonblock(&mut buf).unwrap_or(0); - if n == 0 { - std::thread::sleep(Duration::from_millis(5)); - continue; - } - for chunk in buf[..n].chunks_exact(EVENT_SIZE) { - let key = window::KeyEvent { keycode: chunk[0], modifiers: chunk[1] }; - let translated = if key.pressed() { - translator.press(key.keycode, key.mods()) - } else { - window::Emit::EMPTY - }; - println!( - "kev usage=0x{:02x} mods=0x{:02x} tr={:?}", - key.keycode, - key.modifiers, - translated.as_str() - ); - seen += 1; - if key.keycode == SENTINEL && !key.pressed() { - ended = true; - } - } - } - println!("kev done seen={seen}"); -} - -/// The device-minting capability the test estate is endowed. A claim is -/// `/system/bin/init`'s to mint everywhere else; here test-runner passes a `DEVICE` -/// duplicate down, so a boot can run several binaries that each need an input -/// device. -fn capability() -> SysCap { - Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability") -} diff --git a/tests/toyos-rust-tests/src/bin/input_absent.rs b/tests/toyos-rust-tests/src/bin/input_absent.rs deleted file mode 100644 index 0527816ead5..00000000000 --- a/tests/toyos-rust-tests/src/bin/input_absent.rs +++ /dev/null @@ -1,26 +0,0 @@ -//! A Keyboard or Mouse claim must refuse `NotFound` on a machine with no -//! i8042 and no USB controller. Driven by `input_claim_absent` alone: on -//! every other machine both claims succeed, which is why it is in RUST_SKIP. - -use toyos::device::{Keyboard, Mouse}; -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos_abi::syscall::{DeviceType, SyscallError, SYSCAP_LABEL}; - -fn main() { - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability"); - - match cap.claim::(DeviceType::Keyboard) { - Err(SyscallError::NotFound) => println!("keyboard: refused NotFound"), - Err(e) => panic!("keyboard claim: {e:?}, want NotFound"), - Ok(_) => panic!("a keyboard claim succeeded on a machine with no input source"), - } - match cap.claim::(DeviceType::Mouse) { - Err(SyscallError::NotFound) => println!("mouse: refused NotFound"), - Err(e) => panic!("mouse claim: {e:?}, want NotFound"), - Ok(_) => panic!("a mouse claim succeeded on a machine with no input source"), - } - println!("===INPUT_ABSENT_OK==="); -} diff --git a/tests/toyos-rust-tests/src/bin/input_events.rs b/tests/toyos-rust-tests/src/bin/input_events.rs deleted file mode 100644 index bb08a902672..00000000000 --- a/tests/toyos-rust-tests/src/bin/input_events.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! Claims both input devices and prints every event either one produces. -//! -//! Driven by `xhci_second_controller`, which injects -//! through QMP one step at a time and waits for these lines between steps — so -//! the host never has more in flight than the guest has taken. Not a standalone -//! test: on its own it would report nothing, which is why it is in RUST_SKIP. - -use std::time::Duration; -use toyos::device::{Keyboard, Mouse}; -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos_abi::syscall::{DeviceType, SYSCAP_LABEL}; -use toyos_abi::input::RawKeyEvent; - -const KEY_SIZE: usize = std::mem::size_of::(); -const MOUSE_SIZE: usize = 6; - -/// The host's end-of-run marker, and the only right button in its sequence. -/// PS/2 bit 1 is right and so is HID boot-mouse bit 1. -const RIGHT: u8 = 0x02; - -fn main() { - let cap = capability(); - let keyboard: Keyboard = - cap.claim(DeviceType::Keyboard).expect("input_events: no keyboard device"); - let mouse: Mouse = cap.claim(DeviceType::Mouse).expect("input_events: no mouse device"); - let mut translator = window::configured_translator(); - println!("===INPUT_READY==="); - - // No deadline: the host's sequence ends on the release of the right - // button, which nothing else in it produces, and a path that delivers - // nothing is a hang the host's ceiling reds. - let mut buf = [0u8; 1024]; - let (mut keys, mut pointer) = (0, 0); - let mut right_down = false; - let mut ended = false; - while !ended { - let mut idle = true; - - let n = keyboard.read_nonblock(&mut buf).unwrap_or(0); - for chunk in buf[..n].chunks_exact(KEY_SIZE) { - let key = window::KeyEvent { keycode: chunk[0], modifiers: chunk[1] }; - let translated = if key.pressed() { - translator.press(key.keycode, key.mods()) - } else { - window::Emit::EMPTY - }; - println!( - "kev usage=0x{:02x} mods=0x{:02x} tr={:?}", - key.keycode, - key.modifiers, - translated.as_str() - ); - keys += 1; - idle = false; - } - - let n = mouse.read_nonblock(&mut buf).unwrap_or(0); - for chunk in buf[..n].chunks_exact(MOUSE_SIZE) { - println!( - "mev buttons=0x{:02x} x={} y={}", - chunk[0], - u16::from_le_bytes([chunk[2], chunk[3]]), - u16::from_le_bytes([chunk[4], chunk[5]]), - ); - pointer += 1; - idle = false; - // The release ends the run, not the press: the host reads the - // button state after the last click, and a marker that swallowed - // its own release would leave one held. - if chunk[0] & RIGHT != 0 { - right_down = true; - } else if right_down { - ended = true; - } - } - - if idle { - std::thread::sleep(Duration::from_millis(5)); - } - } - println!("input done keys={keys} pointer={pointer}"); -} - -/// The device-minting capability the test estate is endowed. A claim is -/// `/system/bin/init`'s to mint everywhere else; here test-runner passes a `DEVICE` -/// duplicate down, so a boot can run several binaries that each need an input -/// device. -fn capability() -> SysCap { - Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability") -} diff --git a/tests/toyos-rust-tests/src/bin/inspect_denied.rs b/tests/toyos-rust-tests/src/bin/inspect_denied.rs deleted file mode 100644 index 69b27639b13..00000000000 --- a/tests/toyos-rust-tests/src/bin/inspect_denied.rs +++ /dev/null @@ -1,102 +0,0 @@ -//! A reader without an owner's connector cannot inspect that owner. -//! -//! Two arms, one binary, one boot and one running netd, and the only thing that -//! differs between them is whether the namespace handed to `/system/bin/inspect` -//! carries `netd`. The granted arm is what gives the denied one teeth: a reader -//! that could not reach netd for any other reason — netd down, the protocol -//! broken, the reader missing — fails the first arm instead of passing the -//! second. -//! -//! The denied child keeps every other owner's connector, so what it is refused -//! is exactly the one name it lacks and not a namespace that reaches nothing. -//! -//! The kernel's inventory is the same pair on a capability: `dev.*` read with -//! a duplicate carrying `Rights::INVENTORY` and with one narrowed to lack it. - -use std::os::toyos::process::CommandExt; -use std::process::{Command, Output, Stdio}; - -use toyos::endow::{Endowments, SYSCAP_LABEL}; -use toyos::syscap::SysCap; -use toyos::{endow, namespace}; -use toyos_abi::handle::Rights; -use toyos_abi::syscall::SVC_LABEL; - -const READER: &str = "/system/bin/inspect"; - -/// `inspect net.*`, holding the named connectors out of this process's own and -/// nothing else. -fn inspect_holding(names: &[&str]) -> Output { - let base = endow::namespace().expect("test-runner hands its namespace down"); - let narrowed = namespace::build().keep(base, names).finish().expect("a narrower namespace"); - Command::new(READER) - .arg("net.*") - .endow(SVC_LABEL, narrowed.into_raw().0) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("spawn /system/bin/inspect") - .wait_with_output() - .expect("wait for /system/bin/inspect") -} - -fn main() { - let granted = inspect_holding(&["netd"]); - let out = String::from_utf8_lossy(&granted.stdout); - let err = String::from_utf8_lossy(&granted.stderr); - assert_eq!(granted.status.code(), Some(0), "granted: stdout {out:?} stderr {err:?}"); - assert!(out.lines().any(|l| l.starts_with("net.mac = ")), "granted: {out:?}"); - assert!(out.lines().all(|l| l.starts_with("net.")), "granted answered past net.*: {out:?}"); - - let denied = inspect_holding(&["soundd", "log", "compositor"]); - let out = String::from_utf8_lossy(&denied.stdout); - let err = String::from_utf8_lossy(&denied.stderr); - assert_eq!(denied.status.code(), Some(2), "denied: stdout {out:?} stderr {err:?}"); - assert!(out.is_empty(), "denied read netd anyway: {out:?}"); - assert!( - err.contains("this program holds no `netd` connector"), - "denied was refused for another reason: {err:?}" - ); - println!("inspect denied: granted read netd, denied was refused by name"); - - the_inventory_is_a_right(); -} - -/// `inspect dev.*` with a capability carrying `INVENTORY` and with one that -/// does not: the kernel's own refusal is the only difference, and the reader -/// has to name it. -fn the_inventory_is_a_right() { - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("test-runner endows every binary it spawns a system capability"); - let dev = |rights: Rights| -> Output { - let narrowed = cap.narrowed(rights).expect("a narrower capability"); - Command::new(READER) - .arg("dev.*") - .endow(SYSCAP_LABEL, narrowed.into_raw().0) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("spawn /system/bin/inspect") - .wait_with_output() - .expect("wait for /system/bin/inspect") - }; - - let granted = dev(Rights::TRANSFER.union(Rights::INVENTORY)); - let out = String::from_utf8_lossy(&granted.stdout); - let err = String::from_utf8_lossy(&granted.stderr); - assert_eq!(granted.status.code(), Some(0), "granted: stdout {out:?} stderr {err:?}"); - assert!(out.lines().any(|l| l.starts_with("dev.cpus = ")), "granted: {out:?}"); - assert!(out.lines().all(|l| l.starts_with("dev.")), "granted answered past dev.*: {out:?}"); - - let denied = dev(Rights::TRANSFER); - let out = String::from_utf8_lossy(&denied.stdout); - let err = String::from_utf8_lossy(&denied.stderr); - assert_eq!(denied.status.code(), Some(2), "denied: stdout {out:?} stderr {err:?}"); - assert!(out.is_empty(), "denied read the inventory anyway: {out:?}"); - assert!( - err.contains("does not carry `inventory`"), - "denied was refused for another reason: {err:?}" - ); - println!("inventory denied: granted read dev.*, denied was refused by the kernel"); -} diff --git a/tests/toyos-rust-tests/src/bin/inventory_bounds.rs b/tests/toyos-rust-tests/src/bin/inventory_bounds.rs deleted file mode 100644 index ab23e6d470d..00000000000 --- a/tests/toyos-rust-tests/src/bin/inventory_bounds.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! `SYS_DEVICE_INVENTORY`'s two refusals, each at its edge. -//! -//! A buffer one record short is refused whole and nothing is written into it; -//! a declared count past the bound is refused before it becomes a window, and -//! so is one whose length in bytes wraps. The count the empty buffer answers is -//! the premise of all three, so it is asserted first. - -use toyos::endow::{Endowments, SYSCAP_LABEL}; -use toyos::syscap::SysCap; -use toyos::AsHandle; -use toyos_abi::inventory::{RawRecord, RECORD_BYTES}; -use toyos_abi::syscall::{SyscallError, SYS_DEVICE_INVENTORY}; - -/// The kernel's bound on a declared count. -const MAX_RECORDS: usize = 1024; - -/// `SYS_DEVICE_INVENTORY` with a count no slice can spell. -fn raw(cap: u64, buf: u64, count: u64) -> u64 { - let ret: u64; - // SAFETY: the kernel writes at most `count` records at `buf`, and every - // call here either points `buf` at that many or expects a refusal before - // anything is written. - unsafe { - core::arch::asm!( - "syscall", - in("rdi") SYS_DEVICE_INVENTORY, - in("rsi") cap, - in("rdx") buf, - in("r8") count, - in("r9") 0u64, - lateout("rax") ret, - out("rcx") _, - out("r11") _, - ); - } - ret -} - -fn main() { - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("test-runner endows every binary it spawns a system capability"); - - let count = cap.inventory(&mut []).expect("an empty buffer asks how many"); - assert!(count > 1, "the machine has {count} records, too few to be one short of"); - assert!(count <= MAX_RECORDS, "the machine has {count} records, past the bound"); - println!("inventory bounds: an empty buffer answers {count}"); - - let mut short = vec![RawRecord::EMPTY; count - 1]; - assert_eq!(cap.inventory(&mut short), Err(SyscallError::ResourceExhausted)); - assert!(short.iter().all(|r| *r == RawRecord::EMPTY), "a refused call wrote a record"); - println!("inventory bounds: {} records is refused whole", count - 1); - - let mut whole = vec![RawRecord::EMPTY; count]; - assert_eq!(cap.inventory(&mut whole), Ok(count), "the premise: the machine did not change"); - - let handle = u64::from(cap.as_handle().0); - let mut past = vec![RawRecord::EMPTY; MAX_RECORDS + 1]; - let answer = raw(handle, past.as_mut_ptr() as u64, past.len() as u64); - assert_eq!(SyscallError::from_u64(answer), Some(SyscallError::InvalidArgument), "{answer:#x}"); - assert!(past.iter().all(|r| *r == RawRecord::EMPTY)); - println!("inventory bounds: {} records is refused", MAX_RECORDS + 1); - - // Times the record width, this is 2^64 + 64: one record's worth once it - // wraps, and a buffer that really is one record long. - let wraps = (1u64 << 58) + 1; - assert_eq!(wraps.wrapping_mul(RECORD_BYTES as u64), RECORD_BYTES as u64); - let mut one = [RawRecord::EMPTY]; - let answer = raw(handle, one.as_mut_ptr() as u64, wraps); - assert_eq!(SyscallError::from_u64(answer), Some(SyscallError::InvalidArgument), "{answer:#x}"); - assert_eq!(one[0], RawRecord::EMPTY); - println!("inventory bounds: a count whose length wraps is refused"); -} diff --git a/tests/toyos-rust-tests/src/bin/ipc_hostile_peer.rs b/tests/toyos-rust-tests/src/bin/ipc_hostile_peer.rs deleted file mode 100644 index cc0d9753234..00000000000 --- a/tests/toyos-rust-tests/src/bin/ipc_hostile_peer.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! A daemon must survive a peer that lies about its frames. -//! -//! `ipc::recv_payload` used to `assert!(header.len >= size_of::())` on a -//! number the peer chose, and `header.len` had no upper bound at all, so one -//! 8-byte message from any client was either a compositor panic or a -//! compositor parked in `read_exact` waiting for bytes nobody would send. -//! Nothing in the SDK bounded a frame: `MAX_` matched exactly one constant -//! across both SDK crates. -//! -//! Each case opens its own connection, writes a header the compositor cannot -//! act on, and then requires two things: -//! -//! - the compositor **closed that connection** — proof it read the frame and -//! ruled on it, rather than the frame never arriving. Without this the test -//! passes on a compositor that ignores its listener entirely. -//! - the compositor **still serves a real window** afterwards, from a fresh -//! connection. -//! -//! The order matters: the short-header case is first because it is the one -//! that used to panic outright, so a red run names the defect rather than -//! timing out on the parked case behind it. - -use std::process::exit; - -use toyos::endow; -use toyos::AsHandle; -use toyos::Connection; -use toyos_abi::syscall; -use window::Window; - -/// `CreateWindowRequest` is 40 bytes, so every length below it is a payload -/// the compositor asked for and did not get. -const CASES: &[(&str, u32, u32)] = &[ - // A payload shorter than the type the message type names. - ("short header", window::MSG_CREATE_WINDOW, 0), - // A length no frame can have. The old code walked it 128 bytes at a time. - ("oversized header", window::MSG_CREATE_WINDOW, u32::MAX), - // Neither field means anything: an unknown type with a hostile length. - ("garbage frame", 0xDEAD_BEEF, 0x7FFF_FFFF), -]; - -/// The compositor's answer is a close, which arrives on its own schedule. -/// 100 x 10 ms is two orders of magnitude over a loop iteration and still -/// fails in a second rather than hanging the boot. -const EOF_POLLS: u32 = 100; -const EOF_POLL_NS: u64 = 10_000_000; - -fn main() { - for (name, msg_type, len) in CASES { - let conn = endow::service("compositor") - .unwrap_or_else(|e| panic!("[{name}] the compositor is not serving: {e:?}")); - - let mut frame = [0u8; 8]; - frame[..4].copy_from_slice(&msg_type.to_ne_bytes()); - frame[4..].copy_from_slice(&len.to_ne_bytes()); - let written = syscall::write(conn.as_handle(), &frame) - .unwrap_or_else(|e| panic!("[{name}] could not write the frame: {e:?}")); - assert_eq!(written, frame.len(), "[{name}] partial frame write"); - - if !closed_by_peer(&conn) { - eprintln!("[{name}] the compositor neither closed the connection nor refused it"); - exit(1); - } - - // A window from a fresh connection: the compositor is not merely - // alive as a process, it is still serving the protocol. - let w = Window::create(64, 64) - .unwrap_or_else(|e| panic!("[{name}] the compositor stopped serving windows: {e}")); - drop(w); - } - - println!("ipc hostile peer: {} malformed frames refused, compositor alive", CASES.len()); -} - -/// Did the peer hang up? `read_nonblock` returning 0 is EOF; `WouldBlock` is -/// "not yet". A blocking read would turn a compositor that panicked into a -/// hung boot instead of a named failure. -fn closed_by_peer(conn: &Connection) -> bool { - let mut buf = [0u8; 8]; - for _ in 0..EOF_POLLS { - match conn.read_nonblock(&mut buf) { - Ok(0) => return true, - // Anything the compositor sends back is still an answer, and it - // means the connection is alive — which is not what was asked. - Ok(_) => return false, - Err(syscall::SyscallError::WouldBlock) => syscall::nanosleep(EOF_POLL_NS), - // The connection itself is gone, which is the same hang-up seen from the - // other end of the same race. - Err(_) => return true, - } - } - false -} diff --git a/tests/toyos-rust-tests/src/bin/layout_paths.rs b/tests/toyos-rust-tests/src/bin/layout_paths.rs deleted file mode 100644 index 7c6779976b0..00000000000 --- a/tests/toyos-rust-tests/src/bin/layout_paths.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Where a fresh boot puts things: the session user's home, each service's own -//! `/state`, the machine's keyboard layout in `/config`, the shell's history in -//! its own folder, and no dotfile anywhere ToyOS's own programs write. -//! -//! Driven by `layout_fresh_boot` alone, over ssh on `tests/layoutcase`, after -//! `locale ` and an interactive shell that ran ``. No row -//! declares this binary, so sshd, a service whose own `HOME` is `/state/sshd`, -//! spawns it directly, and the `HOME` it reads is the one init answered for it. - -use std::fs; -use std::io::ErrorKind; -use std::path::Path; - -/// init's `HOME_FOLDERS`: the session home's listing, exactly. -const HOME_FOLDERS: [&str; 8] = - ["Apps", "Desktop", "Documents", "Downloads", "Fonts", "Music", "Pictures", "Videos"]; - -/// The services `tests/layoutcase` runs: `/state`'s listing, exactly. -const SERVICES: [&str; 3] = ["logd", "netd", "sshd"]; - -/// Every volume a ToyOS program writes to. -const WRITTEN: [&str; 6] = ["/apps", "/config", "/home", "/log", "/state", "/tmp"]; - -/// Asked by literal path, so a constant that moved is a red here. -const LAYOUT_FILE: &str = "/config/keyboard-layout"; -const HISTORY_FILE: &str = "/home/toy/Apps/shell/State/history"; - -/// The directory names `dir` lists, sorted. -fn listed_dirs(dir: &str) -> Result, String> { - let mut names = Vec::new(); - for entry in fs::read_dir(dir).map_err(|e| format!("read_dir {dir}: {e}"))? { - let entry = entry.map_err(|e| format!("an entry of {dir}: {e}"))?; - if entry.file_type().map_err(|e| format!("{dir}: {e}"))?.is_dir() { - names.push(entry.file_name().to_string_lossy().into_owned()); - } - } - names.sort(); - Ok(names) -} - -fn main() { - let args: Vec = std::env::args().collect(); - let [_, layout, typed] = args.as_slice() else { - panic!("usage: layout_paths , got {args:?}"); - }; - let mut wrong = Vec::new(); - - let home = std::env::var("HOME"); - if home.as_deref() != Ok("/home/toy") { - wrong.push(format!("HOME is {home:?}, want /home/toy")); - } - let std_home = std::env::home_dir(); - if std_home.as_deref() != Some(Path::new("/home/toy")) { - wrong.push(format!("std::env::home_dir() is {std_home:?}, want /home/toy")); - } - - match fs::metadata("/home/root") { - Err(e) if e.kind() == ErrorKind::NotFound => {} - other => wrong.push(format!("/home/root: {other:?}, want NotFound")), - } - for (dir, want) in [("/home/toy", &HOME_FOLDERS[..]), ("/state", &SERVICES[..])] { - match listed_dirs(dir) { - Ok(names) if names == want => {} - other => wrong.push(format!("{dir} lists {other:?}, want exactly {want:?}")), - } - } - match fs::metadata("/state/sshd/host_ed25519") { - Ok(meta) if meta.is_file() && meta.len() > 0 => {} - other => wrong.push(format!("/state/sshd/host_ed25519: {other:?}, want a file")), - } - match fs::read_to_string(LAYOUT_FILE) { - Ok(text) if text.trim() == layout => {} - other => wrong.push(format!("{LAYOUT_FILE}: {other:?}, want {layout:?}")), - } - match fs::read_to_string(HISTORY_FILE) { - Ok(text) if text.lines().any(|line| line == typed) => {} - other => wrong.push(format!("{HISTORY_FILE}: {other:?}, want a line {typed:?}")), - } - - let mut dotted = Vec::new(); - let mut walked = 0usize; - let mut pending: Vec = WRITTEN.iter().map(|d| d.to_string()).collect(); - while let Some(dir) = pending.pop() { - for entry in fs::read_dir(&dir).unwrap_or_else(|e| panic!("read_dir {dir}: {e}")) { - let entry = entry.expect("dir entry"); - let name = entry.file_name().to_string_lossy().into_owned(); - let path = format!("{dir}/{name}"); - walked += 1; - if name.starts_with('.') { - dotted.push(path.clone()); - } - if entry.file_type().expect("file type").is_dir() { - pending.push(path); - } - } - } - if !dotted.is_empty() { - wrong.push(format!("a dotfile on a fresh boot: {dotted:?}")); - } - - assert!(wrong.is_empty(), "the layout is not as ruled:\n{}", wrong.join("\n")); - println!( - "layout: HOME=/home/toy, /home/toy lists {HOME_FOLDERS:?}, /state lists {SERVICES:?} \ - with sshd's key, {LAYOUT_FILE} and {HISTORY_FILE} as written, and none of {walked} \ - entries under {WRITTEN:?} is a dotfile" - ); -} diff --git a/tests/toyos-rust-tests/src/bin/locale_gate.rs b/tests/toyos-rust-tests/src/bin/locale_gate.rs deleted file mode 100644 index f76e22fdde8..00000000000 --- a/tests/toyos-rust-tests/src/bin/locale_gate.rs +++ /dev/null @@ -1,250 +0,0 @@ -//! The in-guest half of the layout and wizard gates, as a surface. -//! -//! This program is a **surface owner**, built out of exactly the pieces -//! `/system/bin/terminal` and `/system/bin/console` are: it holds the keyboard claim and one -//! `Translator`, makes a port of its own and serves `toyos::surface::Host` on -//! it, and puts that port's connector in the namespace of the child it spawns. -//! What it does not have is a screen — so -//! every assertion the host makes reads a console line instead of a pixel, -//! which is why the layout and wizard gates run here and not against -//! `/system/bin/console`. -//! -//! One binary rather than two: each is ~1.8 MiB of statically linked std, and -//! ROOT goes into a partition sized from its contents. Modes are -//! `run test_rs_locale_gate `, which the test runner has always -//! supported. -//! -//! In RUST_SKIP: every mode waits to be typed at through QMP, so on its own -//! nothing ever answers it. -//! -//! - `layout` — run the real `locale swiss-german`, which writes the config -//! and tells this surface it moved, then print what every key types. Driven -//! by `swiss_german_layout`. -//! - `detect` — run `locale detect` and relay its conversation while the -//! wizard holds this surface's keys. **The keyboard is claimed here**, which -//! is the shape the compositor and `/system/bin/console` put it in and the shape -//! that used to make the wizard refuse. Driven by `locale_detect` and -//! `locale_detect_unrecognized`. - -use std::io::{BufRead, BufReader, Read}; -use std::process::{Child, Command, Stdio}; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; -use std::time::Duration; -use std::os::toyos::process::CommandExt; -use toyos::device::Keyboard; -use toyos::endow::Endowments; -use toyos::namespace; -use toyos::poller::{Poller, READABLE}; -use toyos::port::{self, Connector}; -use toyos::surface::{self, Delivery, Host, Notice}; -use toyos::syscap::SysCap; -use toyos_abi::syscall::{DeviceType, SVC_LABEL, SYSCAP_LABEL}; -use toyos_abi::input::RawKeyEvent; -use window::Translator; - -const EVENT_SIZE: usize = std::mem::size_of::(); - -/// The host's end-of-run marker for `layout`: the HID usage for the End key. -/// The same sentinel and the same reason as `i8042_keyboard.rs` — nothing -/// `swiss_german_layout` injects presses End, so its release is unambiguous. -const SENTINEL: u8 = 0x4D; - -const TOKEN_KEYBOARD: u64 = 1; -const TOKEN_LISTEN: u64 = 2; -const TOKEN_CLIENT: u64 = 3; - -fn main() { - // One port, this instance's, exactly as a terminal makes one. The - // connector goes into the namespace of the `locale` it spawns and nowhere - // else, so the wizard reaches *this* surface and no other. - let (acceptor, connector) = - port::create().expect("locale_gate: the kernel refused a port of its own"); - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability"); - let keyboard: Keyboard = - cap.claim(DeviceType::Keyboard).expect("locale_gate: no keyboard device"); - let surface = - Surface { host: Host::serve(acceptor), keyboard, translator: window::configured_translator() }; - - match std::env::args().nth(1).as_deref() { - Some("layout") => layout(surface, &connector), - Some("detect") => detect(surface, &connector), - other => panic!("locale_gate: unknown mode {other:?}"), - } -} - -/// Everything a surface owner is, minus the screen. -struct Surface { - host: Host, - keyboard: Keyboard, - translator: Translator, -} - -impl Surface { - /// Read the keyboard and hand every transition on: to the client holding - /// the grab if there is one, and otherwise to `typed`, which is where a - /// terminal would write the shell's stdin. - fn drain_keyboard(&mut self, mut typed: impl FnMut(&window::KeyEvent, &str)) { - let mut buf = [0u8; 512]; - let n = self.keyboard.read_nonblock(&mut buf).unwrap_or(0); - for chunk in buf[..n].chunks_exact(EVENT_SIZE) { - let event = RawKeyEvent { keycode: chunk[0], modifiers: chunk[1] }; - if self.host.deliver(event) == Delivery::Sent { - continue; - } - let key = window::KeyEvent::from(event); - let text = if key.pressed() { - self.translator.press(key.keycode, key.mods()) - } else { - window::Emit::EMPTY - }; - typed(&key, text.as_str()); - } - } - - /// The notices a surface owner acts on, with this one's logging. - fn drain_notices(&mut self) { - while let Some(notice) = self.host.poll() { - match notice { - Notice::LayoutChanged => { - window::load_layout(&mut self.translator); - self.host.notify_layout(); - println!("surface: layout is now {}", self.translator.layout()); - } - Notice::Grabbed { client } => println!("surface: client {client} has the keys"), - Notice::Released { client } => { - println!("surface: client {client} gave the keys back") - } - Notice::Dropped { client, why } => { - println!("surface: dropped client {client} — {why}") - } - } - } - } -} - -fn spawn_locale(args: &[&str], surface: &Connector) -> Child { - // The whole of what the wizard is given: one connector, to this surface, - // and the directory its layout is written to. - let names = toyos::endow::namespace().expect("locale_gate: this program was endowed a namespace"); - let child_ns = namespace::build() - .keep(names, &["fs:/config"]) - .add(surface::SERVICE, surface) - .finish() - .expect("locale_gate: the kernel refused a namespace for the wizard"); - Command::new("/system/bin/toybox") - .arg("locale") - .args(args) - .endow(SVC_LABEL, child_ns.into_raw().0) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .expect("locale_gate: cannot run /system/bin/toybox") -} - -fn layout(mut surface: Surface, connector: &Connector) { - let out = spawn_locale(&["swiss-german"], connector) - .wait_with_output() - .expect("locale_gate: locale never exited"); - for line in String::from_utf8_lossy(&out.stdout).lines() { - println!("locale: {line}"); - } - for line in String::from_utf8_lossy(&out.stderr).lines() { - println!("locale-err: {line}"); - } - - // The child connected, said the config moved, and exited. Its frame is in - // the pipe whether or not it is still running, so this is the same accept - // and the same drain a terminal does inside its event loop. - surface.host.accept(); - surface.drain_notices(); - println!("===SWISS_READY==="); - - // No deadline: the host's sequence ends on [`SENTINEL`]'s release, and a - // run that lost it is a hang the host's ceiling reds. - let mut seen = 0; - let mut ended = false; - while !ended { - surface.drain_keyboard(|key, text| { - println!("kev usage=0x{:02x} mods=0x{:02x} tr={:?}", key.keycode, key.modifiers, text); - seen += 1; - if key.keycode == SENTINEL && !key.pressed() { - ended = true; - } - }); - std::thread::sleep(Duration::from_millis(5)); - } - println!("kev done seen={seen}"); -} - -fn detect(mut surface: Surface, connector: &Connector) { - let mut child = spawn_locale(&["detect"], connector); - let stdout = child.stdout.take().expect("locale_gate: no stdout pipe"); - - // The relay is a thread doing blocking reads, and the surface runs here. - // - // Not two halves of one poll loop: the wizard's whole conversation is a - // few hundred bytes and then a hang-up, so the interesting event is the - // *end* of its output — and whether a pipe whose writer has gone reads - // ready is a property of the kernel this test is not about. A blocking - // read answers it directly. - let wizard_done = Arc::new(AtomicBool::new(false)); - let relay_done = wizard_done.clone(); - std::thread::spawn(move || { - let mut reader = BufReader::new(stdout); - // Bytes, not chars: the wizard's legends are `§` and the like, and a - // byte pushed into a `String` as a `char` turns two UTF-8 bytes into - // two Latin-1 ones — which reads as a mangled prompt on the host and - // is a defect in this relay rather than in anything under test. - let mut line: Vec = Vec::new(); - while reader.read_until(b'\n', &mut line).unwrap_or(0) > 0 { - while line.last() == Some(&b'\n') || line.last() == Some(&b'\r') { - line.pop(); - } - println!("detect: {}", String::from_utf8_lossy(&line)); - line.clear(); - } - relay_done.store(true, Ordering::Relaxed); - }); - - let poller = Poller::new(1 + Host::POLL_HANDLES); - // No deadline: a wizard that never ends is a hang the host's ceiling reds. - while !wizard_done.load(Ordering::Relaxed) { - poller.watch(&surface.keyboard, READABLE, TOKEN_KEYBOARD); - poller.watch_raw(surface.host.acceptor_handle(), READABLE, TOKEN_LISTEN); - for client in surface.host.client_handles() { - poller.watch_raw(client, READABLE, TOKEN_CLIENT); - } - - let mut ready = [false; 4]; - // A pace, and never a verdict: `wizard_done` is a flag and not a handle, - // so it is looked at again at least this often. - poller.wait(1, 50_000_000, |token| { - if (token as usize) < ready.len() { - ready[token as usize] = true; - } - }); - - if ready[TOKEN_LISTEN as usize] { - surface.host.accept(); - } - surface.drain_notices(); - - // Keys are drained on every pass, ready or not: the wizard's grab - // arrives between two of them, and a transition read before the grab - // was granted would be translated into nothing anyone is reading. - surface.drain_keyboard(|_, _| {}); - } - println!("===DETECT_DRAINED==="); - - let mut stderr = child.stderr.take().expect("locale_gate: no stderr pipe"); - child.wait().expect("locale_gate: the wizard never exited"); - let mut err = Vec::new(); - stderr.read_to_end(&mut err).ok(); - for line in String::from_utf8_lossy(&err).lines() { - println!("detect-err: {line}"); - } - println!("===DETECT_DONE==="); -} diff --git a/tests/toyos-rust-tests/src/bin/log_carrier_forger.rs b/tests/toyos-rust-tests/src/bin/log_carrier_forger.rs deleted file mode 100644 index ff57c18daaa..00000000000 --- a/tests/toyos-rust-tests/src/bin/log_carrier_forger.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! A program that prints init's own word accepting a swap of netd, and ends. -//! Only init's pipe may move `logd` to turn readers away; this program's line -//! is its own, and changes nothing. `log_carrier_forgery` runs it. - -fn main() { - println!("init: swap netd: accepted: /tmp/swap/forged/netd replaces /system/bin/netd (pid 1)"); -} diff --git a/tests/toyos-rust-tests/src/bin/log_flood.rs b/tests/toyos-rust-tests/src/bin/log_flood.rs deleted file mode 100644 index 1c6186e6e6c..00000000000 --- a/tests/toyos-rust-tests/src/bin/log_flood.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! A program that writes its output far faster than the log can take it: no -//! write waits, and every line reaches `/log` in order or is counted by `logd`. -//! -//! One `write` per whole line, numbered, so the host can count them in the -//! file. The total is many times what one ring holds, so a `logd` that fell -//! behind has lines refused rather than this program waiting. The verdict is -//! the host's; `log_program_flood` runs it. - -use std::io::Write; - -/// Lines written, many times the records one log ring holds, each [`WIDTH`] -/// bytes: what fills a ring is its slots, and a narrow line keeps the stop's -/// flush of a full one inside init's bound. -const LINES: usize = 16_384; -const WIDTH: usize = 64; - -fn main() { - let mut out = std::io::stdout().lock(); - for i in 0..LINES { - let head = format!("flood {i:06} "); - let line = format!("{head}{}\n", "x".repeat(WIDTH - head.len() - 1)); - out.write_all(line.as_bytes()).expect("a write to the log never fails"); - out.flush().expect("a write to the log never fails"); - } - let _ = writeln!( - out, - "flood done lines={LINES} bytes={}", - LINES * WIDTH, - ); -} diff --git a/tests/toyos-rust-tests/src/bin/log_forger.rs b/tests/toyos-rust-tests/src/bin/log_forger.rs deleted file mode 100644 index 82bcb35565d..00000000000 --- a/tests/toyos-rust-tests/src/bin/log_forger.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! A program that writes the words of lines that are not its own: the kernel's -//! `exit:` record claiming this job passed, a whole kernel record's line as the -//! file and as the console spell it, another program's head, and a record -//! stamped at the end of time straight into its ring. Its real exit is -//! [`CODE`], which is the verdict a judge of the log must read; -//! `log_program_forgery` runs it. - -use toyos::log::region::Body; -use toyos::log::ring::Pushed; -use toyos::log::stdio::{sink, Sink, Stream}; - -/// This job's real exit code. -const CODE: i32 = 7; -/// The text of the record stamped `u64::MAX`. -const AHEAD: &[u8] = b"log forger: stamped at the end of time"; - -fn main() { - println!("exit: test_rs_log_forger pid=1 code=0 cpu=0ms"); - println!("[2026-09-24 10:00:00 1.000 cpu0] exit: test_rs_log_forger pid=1 code=0 cpu=0ms"); - println!("[kernel 1.000 cpu0] exit: test_rs_log_forger pid=1 code=0 cpu=0ms"); - println!("{{2026-09-24 10:00:00 1.000 netd}} netd: DHCP: lease 10.9.9.9/24 forged"); - println!("\r[2026-09-24 10:00:00 1.000 cpu0] Rebooting."); - let Sink::Ring(ring) = sink(Stream::Err) else { panic!("log forger: stderr is not a log ring") }; - let mut body = Body::EMPTY; - body.at_ns = u64::MAX; - body.pid = std::process::id(); - body.text[..AHEAD.len()].copy_from_slice(AHEAD); - body.len = AHEAD.len() as u16; - assert!(matches!(ring.push(&body), Pushed::Written), "log forger: its ring refused the record"); - std::process::exit(CODE); -} diff --git a/tests/toyos-rust-tests/src/bin/log_hold.rs b/tests/toyos-rust-tests/src/bin/log_hold.rs deleted file mode 100644 index e0ee228e63e..00000000000 --- a/tests/toyos-rust-tests/src/bin/log_hold.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! A program that has the kernel write three batches of records and then says -//! one line, which must land in `/log` after them all: `logd` reads a -//! program's ring before the kernel's records, so only the stamps order them. -//! `log_program_line_after_its_records` runs it. - -/// Three times what `logd` asks of the ring at once (`BATCH`, 64). -const RECORDS: usize = 192; - -/// A retired syscall's number: each call is refused and is one kernel record -/// naming it (`kernel/src/syscall/dispatch.rs`'s `retired_syscall`). -const RETIRED: u64 = 26; - -fn main() { - for _ in 0..RECORDS { - let ret: u64; - // SAFETY: a register-only `syscall` whose number the kernel refuses - // without reading any argument; nothing in this process is touched. - unsafe { - core::arch::asm!( - "syscall", - in("rdi") RETIRED, - lateout("rax") ret, - out("rcx") _, - out("r11") _, - ); - } - assert_ne!(ret, 0, "syscall {RETIRED} answered as if it were live"); - } - println!("log hold: said after {RECORDS} records"); -} diff --git a/tests/toyos-rust-tests/src/bin/log_origin.rs b/tests/toyos-rust-tests/src/bin/log_origin.rs deleted file mode 100644 index 2734ec2f3f6..00000000000 --- a/tests/toyos-rust-tests/src/bin/log_origin.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! A program that says one line and ends, for where that line goes: `/log`, -//! the log `logd` serves, and the console — each under the name of the -//! program whose pipe it came out of. `log_program_line` runs it. - -fn main() { - println!("log origin nonce 7d1f3a"); -} diff --git a/tests/toyos-rust-tests/src/bin/log_refused_stop.rs b/tests/toyos-rust-tests/src/bin/log_refused_stop.rs deleted file mode 100644 index 934fbf0bee3..00000000000 --- a/tests/toyos-rust-tests/src/bin/log_refused_stop.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Asks init to stop the machine on a kernel armed to refuse the first stop -//! (`power-refused-once`), and says a line once refused. Its verdict is -//! whether that line is in `/log`; `log_after_a_refused_stop` judges it. - -use toyos::power::{self, Refused, Stop}; -use toyos_abi::syscall::SyscallError; - -fn main() { - let refused = power::stop(Stop::Reboot); - assert_eq!(refused, Refused::Kernel(SyscallError::NotSupported), "the armed refusal did not answer"); - println!("log refused stop: said after the refusal"); -} diff --git a/tests/toyos-rust-tests/src/bin/netd_caps.rs b/tests/toyos-rust-tests/src/bin/netd_caps.rs deleted file mode 100644 index c53d0d4c78b..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_caps.rs +++ /dev/null @@ -1,93 +0,0 @@ -//! netd's piped-connection cap, from the client side. -//! -//! Needs netd with a NIC in front of it and the harness's host server behind -//! it, which only `tests/netcase` provides — it is in `RUST_SKIP` and -//! `netd_connection_caps` runs it there. -//! -//! Every connect goes to the host server and is answered before the next is -//! asked, and every one it grants is held open: the cap counts established -//! connections, so the first `ResourceExhausted` is the boundary, and no clock -//! decides where it falls. Where the boundary falls is measured here and -//! compared with the cap netd announced by the host. - -#[path = "../netd_stream.rs"] -mod netd_stream; - -use netd_stream::{HOST, NO_DEADLINE}; -use toyos::net::{ - MsgType, NetError, NetdConn, TcpConnectPipedRequest, TcpConnectResponse, DATA_FROM_CLIENT, - DATA_HANDLES, DATA_TO_CLIENT, -}; -use toyos::Pipe; -use toyos_abi::syscall; - -/// How far past the boundary to keep asking. Small: the point is to cross the -/// boundary, and every request costs netd an IPC connection. -const MARGIN: usize = 4; - -/// One netd event-loop pass, which is what a connect the kernel's queue -/// refused waits for before it asks again. A pace and never a verdict. -const PASS_NANOS: u64 = 1_000_000; - -fn main() { - let port: u16 = std::env::args() - .nth(1) - .and_then(|p| p.parse().ok()) - .expect("usage: netd_caps "); - let request = TcpConnectPipedRequest { addr: HOST, port, _pad: 0, timeout_ms: NO_DEADLINE }; - - let mut held: Vec<[Pipe; DATA_HANDLES]> = Vec::new(); - let granted = loop { - match connect(&request) { - Ok(kept) => held.push(kept), - Err(NetError::ResourceExhausted) => break held.len(), - Err(e) => panic!("connect {}: {e:?}, not a capacity refusal", held.len()), - } - }; - // Both sides of the boundary, because "a refusal happened" is also true of - // a netd that refused everything, and of one that refused at random. - for past in 1..=MARGIN { - assert_eq!( - connect(&request).err(), - Some(NetError::ResourceExhausted), - "connect {} past the boundary at {granted} was not a capacity refusal", - granted + past, - ); - } - assert!(granted >= 2, "only {granted} connects were accepted; netd is refusing, not bounding"); - println!("netd caps: {granted} connections accepted then refused"); - drop(held); -} - -/// One connect, answered before this returns. What a granted one answers is -/// this side's two ends of its data path: while they are held netd holds the -/// connection, which is exactly where the cap is counting it. -fn connect(request: &TcpConnectPipedRequest) -> Result<[Pipe; DATA_HANDLES], NetError> { - let (to_client_read, to_client_write) = toyos::pipe_pair().expect("the pipe netd writes into"); - let (from_client_read, from_client_write) = - toyos::pipe_pair().expect("the pipe netd reads from"); - let mut handles = [toyos_abi::HANDLE_INVALID; DATA_HANDLES]; - handles[DATA_TO_CLIENT] = to_client_write.into_raw(); - handles[DATA_FROM_CLIENT] = from_client_read.into_raw(); - netd() - .request_with_handles(&handles, MsgType::TcpConnectPiped, request) - .unwrap_or_else(|e| panic!("netd would not take a connect: {e:?}")) - .response::()?; - Ok([to_client_read, from_client_write]) -} - -/// A connection to netd, asking again with no bound while the kernel's queue -/// of connections netd has not accepted yet is full. -/// -/// That refusal is backpressure from the kernel, retryable against the same -/// peer; netd's own cap is the `ResourceExhausted` in a *response*, which is -/// what this file is about and what it must not be confused with. -fn netd() -> NetdConn { - loop { - match NetdConn::connect() { - Ok(conn) => return conn, - Err(NetError::ResourceExhausted) => syscall::nanosleep(PASS_NANOS), - Err(e) => panic!("could not reach netd: {e:?}"), - } - } -} diff --git a/tests/toyos-rust-tests/src/bin/netd_gone_mid_bind.rs b/tests/toyos-rust-tests/src/bin/netd_gone_mid_bind.rs index cac369bf392..ffbd09705f7 100644 --- a/tests/toyos-rust-tests/src/bin/netd_gone_mid_bind.rs +++ b/tests/toyos-rust-tests/src/bin/netd_gone_mid_bind.rs @@ -7,10 +7,7 @@ //! machine that *has* a NIC and cannot bind must be loud. On a NIC-less machine //! netd prints its line and exits, and whether sshd took the quiet arm or put a //! tokio backtrace across the boot depended on which side of netd's teardown -//! its bind landed. Four recorded sightings, on the dev host and on CI alike, -//! and the victim was `boot_partition_identity` every time — a test that -//! refuses any boot whose console carries `panicked at`, so its own subject -//! was untouched and the red named the workload rather than the cause. +//! its bind landed. //! //! **The race is not staged here. The sequence is.** What made the defect hard //! to see is that it is a handful of instructions wide in a real boot; what diff --git a/tests/toyos-rust-tests/src/bin/netd_held_open.rs b/tests/toyos-rust-tests/src/bin/netd_held_open.rs deleted file mode 100644 index d6d851b6de1..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_held_open.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Bytes netd holds back in a socket past a full receive pipe move when the -//! reader makes room, and on nothing else. -//! -//! The host sends a pipe's capacity and 32 KiB more, which the pipe and the -//! 64 KiB socket buffer hold between them, and then holds the connection open -//! with no FIN: its bytes are all acknowledged and the window never closes, so -//! the peer has nothing to send and nothing to probe. Once the ring is full -//! this program makes room a [`STEP`] at a time, and after each waits for the -//! next `STEP` of what the socket held to reach the ring. The pipe's room is -//! the only event each step makes: a netd that does not watch for it is moved -//! at most by a timer it already had pending, which is spent by the first step -//! it rescues. -//! -//! argv[1] is the port of the harness's host server on `HOST`. -//! `netd_held_open: ok bytes=` is the only success line. - -#[path = "../netd_stream.rs"] -mod netd_stream; - -use netd_stream::{ask, await_ring_full, await_ring_holds, read_pattern_from, ring_capacity, Ask, HOST, NO_DEADLINE}; - -/// Bytes the host sends past the ring's capacity: less than the socket's -/// 64 KiB buffer, so the window never closes on the peer. -const PAST_THE_RING: u64 = 32 * 1024; - -/// Room made at a time, and the bytes each step waits to see arrive. -const STEP: u64 = 1024; - -fn main() { - let port: u16 = std::env::args() - .nth(1) - .and_then(|p| p.parse().ok()) - .expect("usage: netd_held_open "); - let capacity = ring_capacity(); - let total = capacity + PAST_THE_RING; - println!("netd_held_open: ring capacity {capacity}, expecting {total} bytes and no FIN"); - - let conn = toyos::net::tcp_connect(HOST, port, NO_DEADLINE).expect("connect to the host server"); - ask(&conn.tx, Ask::Held(total)); - await_ring_full(&conn.rx, capacity); - println!("netd_held_open: the ring is full at {capacity} bytes unread; making room a step at a time"); - - let what = format!("netd_held_open (ring capacity {capacity}, the peer silent)"); - let mut at = 0; - while at < PAST_THE_RING { - at = read_pattern_from(&conn.rx, at, at + STEP, &what); - await_ring_holds(&conn.rx, capacity, capacity + at - 16); - } - let at = read_pattern_from(&conn.rx, at, total, &what); - assert_eq!(at, total, "the stream ended after {at} of {total} bytes, every one of them right"); - println!("netd_held_open: ok bytes={at}, the last {PAST_THE_RING} moved a {STEP}-byte step at a time"); -} diff --git a/tests/toyos-rust-tests/src/bin/netd_hostile_peer.rs b/tests/toyos-rust-tests/src/bin/netd_hostile_peer.rs deleted file mode 100644 index d8bde50319b..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_hostile_peer.rs +++ /dev/null @@ -1,185 +0,0 @@ -//! The network stack must survive a client that stops talking. -//! -//! netd used to `accept` and then call `ipc::recv_header` on the fresh connection, and -//! `read_exact` behind it is a *blocking* read — so one client that connected -//! and wrote four bytes stopped the network stack for everyone until it -//! disconnected. Its dispatch read every payload off the connection too, so a whole -//! header followed by silence did the same on a connection that had already -//! said what it wanted. This is the compositor's closed defect, line for line, -//! in the last daemon that still had it. -//! -//! Needs netd with a NIC in front of it, which only `tests/netcase` provides — -//! it is in `RUST_SKIP` and `netd_hostile_peer` runs it there. -//! -//! **No wait here has a deadline.** A netd parked on a client never answers, and -//! the harness ceiling reds the hang. - -use std::process::exit; - -use toyos::endow; -use toyos::AsHandle; -use toyos::ipc::{self, RxStep}; -use toyos::poller::{Poller, READABLE}; -use toyos::net::{MsgType, RespType}; -use toyos::Connection; -use toyos_abi::syscall; - -/// A literal address, which netd parses out of the request and answers without -/// a packet — so this asks whether the daemon is *serving*, on a machine whose -/// NIC has nobody on the other end of it. -const LITERAL: &[u8] = b"192.0.2.7"; -/// What netd sends back for [`LITERAL`]: one address, four bytes, the octets. -const LITERAL_REPLY: [u8; 6] = [1, 4, 192, 0, 2, 7]; - -/// A frame netd cannot act on, and what it must do about it. -struct Case { - name: &'static str, - /// Bytes written on a fresh connection. A prefix of a frame on purpose in - /// the first three: that is what a blocking read parks on. - bytes: Vec, - /// Whether netd must have ruled on this connection — answered it or closed - /// it — by the time it is asked. A partial frame is *not* a ruling: netd is - /// entitled to hold it until its handshake deadline, and that it does so - /// without stopping is the whole point. - ruled: bool, -} - -fn header(msg_type: u32, len: u32) -> Vec { - let mut frame = Vec::with_capacity(8); - frame.extend_from_slice(&msg_type.to_ne_bytes()); - frame.extend_from_slice(&len.to_ne_bytes()); - frame -} - -/// `TcpBindPipedRequest` is 16 bytes, so a frame declaring fewer is a payload -/// netd asked for and did not get. -fn cases() -> Vec { - let bind = MsgType::TcpBindPiped as u32; - vec![ - // The three that used to park netd. First, so a red run names the - // stall rather than timing out on a later case behind it. - Case { name: "connected and silent", bytes: Vec::new(), ruled: false }, - Case { name: "half a header", bytes: vec![0u8; 4], ruled: false }, - Case { name: "header, then silence", bytes: header(bind, 16), ruled: false }, - // Whole frames netd can locate and must rule on. - Case { name: "short payload", bytes: header(bind, 0), ruled: true }, - Case { name: "oversized header", bytes: header(bind, u32::MAX), ruled: true }, - Case { name: "garbage frame", bytes: header(0xDEAD_BEEF, 0x7FFF_FFFF), ruled: true }, - ] -} - -fn main() { - let cases = cases(); - for case in &cases { - let conn = endow::service("netd") - .unwrap_or_else(|e| panic!("[{}] netd is not serving: {e:?}", case.name)); - if !case.bytes.is_empty() { - let written = syscall::write(conn.as_handle(), &case.bytes) - .unwrap_or_else(|e| panic!("[{}] could not write the frame: {e:?}", case.name)); - assert_eq!(written, case.bytes.len(), "[{}] partial frame write", case.name); - } - - // Asked while the hostile connection is still open, which is the whole - // question: a netd parked on it answers nobody. - if let Err(e) = still_serving() { - eprintln!("[{}] {e}", case.name); - exit(1); - } - - if case.ruled { - await_ruling(&conn); - } - drop(conn); - } - - // A connection that never says anything must not be netd's to hold forever: - // its handshake deadline is netd's own, and the close is what is waited for. - let silent = endow::service("netd").expect("netd is not serving"); - await_close(&silent); - drop(silent); - if let Err(e) = still_serving() { - eprintln!("[after the silent connection] {e}"); - exit(1); - } - - println!( - "netd hostile peer: {} malformed frames refused, silent one dropped, netd alive", - cases.len(), - ); -} - -/// Ask netd something it answers from the request itself, without blocking. -/// -/// [`ipc::FrameRx`] is the SDK's non-blocking framing — the same type netd now -/// reads its clients with — waited on with no deadline. -fn still_serving() -> Result<(), String> { - let conn = endow::service("netd").map_err(|e| format!("netd refused a connection: {e:?}"))?; - conn.try_send_bytes(MsgType::DnsLookup as u32, LITERAL) - .map_err(|e| format!("netd would not take a request: {e:?}"))?; - - let mut rx = ipc::FrameRx::<16>::new(); - let poller = Poller::new(1); - loop { - match rx.pump(&conn) { - RxStep::Idle => { - poller.watch(&conn, READABLE, 0); - poller.wait(1, u64::MAX, |_| {}); - } - RxStep::Eof => return Err("netd closed a request without answering it".to_string()), - RxStep::Malformed => return Err("netd sent a frame the SDK cannot read".to_string()), - RxStep::Frame { msg_type, payload_len } => { - if msg_type != RespType::Result as u32 { - return Err(format!("netd answered with message type {msg_type}")); - } - let payload = rx.payload(payload_len); - if payload != LITERAL_REPLY.as_slice() { - return Err(format!("netd answered a literal address with {payload:?}")); - } - return Ok(()); - } - } - } -} - -/// Wait, with no deadline, until netd has either answered this connection or -/// closed it. -/// -/// Both are rulings. An answer is the better one — a client learns that its -/// frame was refused — and a close is what a frame with no locatable next -/// message boundary gets, and what a connection that never finished its -/// request gets at netd's handshake deadline. -fn await_ruling(conn: &Connection) { - let mut buf = [0u8; 8]; - let poller = Poller::new(1); - loop { - match conn.read_nonblock(&mut buf) { - Err(syscall::SyscallError::WouldBlock) => { - poller.watch(conn, READABLE, 0); - poller.wait(1, u64::MAX, |_| {}); - } - // Bytes, EOF, or the connection itself gone: each is netd's word. - _ => return, - } - } -} - -/// Wait, with no deadline, for netd to close `conn`, which never asked anything: -/// bytes on it are an answer to nothing. -fn await_close(conn: &Connection) { - let mut buf = [0u8; 8]; - let poller = Poller::new(1); - loop { - match conn.read_nonblock(&mut buf) { - Err(syscall::SyscallError::WouldBlock) => { - poller.watch(conn, READABLE, 0); - poller.wait(1, u64::MAX, |_| {}); - } - Ok(n) if n > 0 => { - eprintln!("netd answered a connection that never asked anything with {n} bytes"); - exit(1); - } - // EOF, or the connection itself gone. - _ => return, - } - } -} diff --git a/tests/toyos-rust-tests/src/bin/netd_listener_forgery.rs b/tests/toyos-rust-tests/src/bin/netd_listener_forgery.rs deleted file mode 100644 index 664367f01b1..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_listener_forgery.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! netd must not tear a piped listener down on a flag its client forged. The -//! client keeps the notify pipe's reader; netd used to abort the listener when -//! `RingHeader::is_reader_closed()` read set — a bit the client can forge in the -//! writable ring page. netd now probes the kernel (a zero-byte `write_nonblock`, -//! refused only when the reader is really gone). Here the flag is forged with -//! the reader alive: the listener must survive, observed as a kernel fact — -//! netd drops its notify writer when it aborts, so the client's `read_nonblock` -//! sees EOF, and `WouldBlock` while the listener lives. Runs on `tests/netcase`. - -use std::sync::atomic::{AtomicU32, Ordering}; - -use toyos::net::NetdConn; -use toyos::AsHandle; -use toyos_abi::ring::RING_READER_CLOSED; -use toyos_abi::syscall::{self, SyscallError}; - -const PORT: u16 = 8080; -/// netd runs `cleanup_dead_listeners` on every wake, and with no traffic it -/// wakes on a new IPC connection — so poke it, then read the notify verdict. -const POKES: usize = 12; -const POKE_PAUSE_NANOS: u64 = 20_000_000; // 20 ms - -/// A plain store into the mapped ring header, as the client would forge it. -fn forge(page: *mut u8, bit: u32) { - let flags = unsafe { &*(page as *const AtomicU32) }; - flags.fetch_or(bit, Ordering::Release); -} - -/// One netd wake: a bare connection dropped at once, so its next pass runs cleanup. -fn poke() { - if let Ok(conn) = NetdConn::connect() { - drop(conn); - } -} - -fn main() { - let bound = toyos::net::tcp_bind([0, 0, 0, 0], PORT).expect("bind a piped listener"); - - // Forge "my reader is gone" with the reader still open. - let page = bound.notify.pipe_map().expect("map the notify pipe") as *mut u8; - forge(page, RING_READER_CLOSED); - - for _ in 0..POKES { - poke(); - syscall::nanosleep(POKE_PAUSE_NANOS); - } - - // A believed flag drops netd's notify writer (EOF here); a survivor keeps it. - let mut buf = [0u8; 4]; - match syscall::read_nonblock(bound.notify.as_handle(), &mut buf) { - Err(SyscallError::WouldBlock) => { - println!("netd listener forgery: listener survived a forged reader-closed flag"); - } - Ok(0) => panic!( - "netd tore the listener down on a flag the client forged — its notify writer is \ - gone while the client's reader was never closed" - ), - other => panic!("unexpected read of the notify pipe: {other:?}"), - } -} diff --git a/tests/toyos-rust-tests/src/bin/netd_lookup_let_go.rs b/tests/toyos-rust-tests/src/bin/netd_lookup_let_go.rs deleted file mode 100644 index 3eaf4b52edd..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_lookup_let_go.rs +++ /dev/null @@ -1,105 +0,0 @@ -//! A lookup whose client has left is let go at once, by netd's own loop, on a -//! network whose resolver never answers. -//! -//! The harness holds every frame this machine sends once it has its lease, so -//! no query reaches the one resolver the lease names. A lookup started here -//! holds its slot until its whole schedule has run out, [`toyos_dns::ROUNDS`] -//! waits of [`toyos_dns::WAIT_MS`], unless netd lets it go. -//! -//! **Hung up.** [`CAP`] lookups are started and held, and one more is refused -//! as exhausted, which is what shows all of them in flight. All of them hang -//! up, and the next lookup is not refused: it is asked, and ends timed out -//! when its schedule does, on netd's own wakes. -//! -//! **Spoke again.** A connection carries one request, so a client that says -//! more while its lookup is in flight is dropped: its connection closes with -//! no answer, where the schedule would have answered it timed out. -//! -//! `netd_lookup_let_go: ok` is the only success line. - -use toyos::net::{dns_lookup, MsgType, NetError, NetdConn, PendingResponse}; -use toyos::poller::{Poller, READABLE}; -use toyos::Connection; -use toyos_abi::syscall::SyscallError; - -/// netd's `resolve::MAX_LOOKUPS`, one declaration in `toyos_dns`. -const CAP: usize = toyos_dns::MAX_LOOKUPS; - -/// Any name: nothing on this network answers one. -const NAME: &str = "unanswered.example"; - -fn main() { - hung_up(); - spoke_again(); - println!("netd_lookup_let_go: ok"); -} - -fn hung_up() { - let held: Vec = (0..CAP).map(|_| ask()).collect(); - assert_eq!( - lookup(), - Err(NetError::ResourceExhausted), - "lookup {} was not refused as exhausted, so the {CAP} before it are not all in flight", - CAP + 1 - ); - println!("netd_lookup_let_go: {CAP} lookups in flight, and the next refused"); - drop(held); - let answer = lookup(); - assert_ne!( - answer, - Err(NetError::ResourceExhausted), - "{CAP} lookups hung up and the next was refused as exhausted: netd did not let them go" - ); - assert_eq!(answer, Err(NetError::TimedOut), "a lookup nothing answers"); - println!("netd_lookup_let_go: {CAP} hung up, and the next was asked and timed out"); -} - -fn spoke_again() { - let chatty = toyos::endow::service("netd").expect("a connection to netd"); - chatty.send_bytes(MsgType::DnsLookup as u32, NAME.as_bytes()).expect("netd takes a lookup"); - let held: Vec = (1..CAP).map(|_| ask()).collect(); - assert_eq!( - lookup(), - Err(NetError::ResourceExhausted), - "lookup {} was not refused as exhausted, so the {CAP} before it are not all in flight", - CAP + 1 - ); - chatty.send_bytes(MsgType::DnsLookup as u32, NAME.as_bytes()).expect("netd's end is still open"); - let answered = closed_or_answered(&chatty); - assert_eq!(answered, 0, "a client that spoke again while its lookup ran was answered"); - println!("netd_lookup_let_go: a client that spoke again was dropped, unanswered"); - drop(held); -} - -/// A lookup of [`NAME`], asked and left waiting. -fn ask() -> PendingResponse { - NetdConn::connect() - .expect("a connection to netd") - .request_bytes(MsgType::DnsLookup, NAME.as_bytes()) - .expect("netd takes a lookup") -} - -/// A lookup of [`NAME`] to its end, with no deadline: a lookup netd never ends -/// is a hang the harness ceiling reds. -fn lookup() -> Result { - dns_lookup(NAME, &mut [[0; 4]; 4]) -} - -/// Bytes netd wrote on `conn` before it closed, once it has closed, with no -/// deadline. -fn closed_or_answered(conn: &Connection) -> usize { - let poller = Poller::new(1); - let mut got = 0; - let mut buf = [0u8; 64]; - loop { - match conn.read_nonblock(&mut buf) { - Ok(0) => return got, - Ok(n) => got += n, - Err(SyscallError::WouldBlock) => { - poller.watch(conn, READABLE, 0); - poller.wait(1, u64::MAX, |_| {}); - } - Err(e) => panic!("reading netd's end of a lookup's connection: {e:?}"), - } - } -} diff --git a/tests/toyos-rust-tests/src/bin/netd_refused_pipes.rs b/tests/toyos-rust-tests/src/bin/netd_refused_pipes.rs deleted file mode 100644 index d501a0a3b31..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_refused_pipes.rs +++ /dev/null @@ -1,215 +0,0 @@ -//! A client's handle that refuses netd costs that client its connection, and -//! never netd. -//! -//! A client moves netd the far ends of its data pipes and its listener's -//! notify pipe, and nothing about the handles it moves is checked: whatever -//! they are, netd writes and reads them. Each case below hands netd one it -//! cannot use, or takes one away under it, and is followed by an ordinary -//! connection that must round-trip — the proof that netd is still there to -//! serve it: -//! -//! 1. `to_client` is a pipe's **read** end. netd's writes are refused. -//! 2. `from_client` is a pipe's **write** end. netd's reads are refused. -//! 3. A listener's notify handle is a pipe's read end. -//! 4. The client drops its receive end unread while its ring is full and the -//! host is still sending, so netd's next write finds no reader. -//! 5. `to_client` is a file seeked to exactly the kernel's size limit. A -//! zero-byte write there is taken, so netd's liveness probe passes, and -//! every write of a byte is refused: only the write of the peer's bytes -//! meets the refusal. -//! 6. A listener's notify handle is that same kind of file, and the host -//! connects to the listener, so the refusal meets the wake netd owes. -//! -//! netd letting go of a handle is observed as an event, never waited out: a -//! pipe this program keeps full regains room once every reader is gone, and a -//! listener netd closes turns the host's connection into it away. -//! -//! argv[1] is the port of the harness's host server on `HOST`, and the harness -//! forwards a host port to this guest's `FORWARDED_PORT`. -//! `netd_refused_pipes: ok` is the only success line. - -#[path = "../netd_stream.rs"] -mod netd_stream; - -use netd_stream::{ - ask, ask_bytes, await_ring_full, await_until, keep_full_until_released, read_pattern, ring_capacity, Ask, - FORWARDED_PORT, HOST, NO_DEADLINE, -}; -use toyos::net::{ - MsgType, NetError, NetdConn, TcpBindPipedRequest, TcpBindResponse, TcpConnectPipedRequest, - TcpConnectResponse, TcpSocketId, -}; -use toyos::poller::READABLE; -use toyos::Pipe; -use toyos_abi::syscall::{self, OpenFlags, SeekFrom, SyscallError}; -use toyos_abi::RawHandle; - -/// Bytes a round trip asks for: more than one pipe write and one TCP segment, -/// far less than a ring. -const ROUND_TRIP: u64 = 256 * 1024; - -/// Bytes the host sends past the ring's capacity in case 4, so the socket still -/// holds some when the receive end goes. -const PAST_THE_RING: u64 = 1024 * 1024; - -/// The kernel's largest file: 2^32 pages of 4 KiB. Asserted where it is used, -/// by a seek one byte past it being refused. -const FILE_LIMIT: u64 = (u32::MAX as u64 + 1) * 4096; - -/// The file cases 5 and 6 hand netd, one at a time. -const FILE_PATH: &str = "/tmp/netd_refused_pipes"; - -fn main() { - let port: u16 = std::env::args() - .nth(1) - .and_then(|p| p.parse().ok()) - .expect("usage: netd_refused_pipes "); - let capacity = ring_capacity(); - - let cases: [(&str, &dyn Fn()); 6] = [ - ("a receive end netd cannot write", &|| receive_end_is_a_read_end(port)), - ("a send end netd cannot read", &|| send_end_is_a_write_end(port)), - ("a notify end netd cannot write", &|| notify_end_is_a_read_end()), - ("a receive end dropped while netd held bytes for it", &|| { - receive_end_dropped_while_held(port, capacity) - }), - ("a receive end at its size limit", &|| receive_end_is_a_full_file(port)), - ("a notify end at its size limit, owed a wake", &|| notify_end_is_a_full_file(port)), - ]; - for (case, run) in cases { - run(); - round_trip(port, &format!("after {case}")); - println!("netd_refused_pipes: {case}, and a round trip after it"); - } - println!("netd_refused_pipes: ok"); -} - -/// Ask netd for a connection to the host, handing it `to_client` and -/// `from_client` as they are. -fn connect_with(port: u16, to_client: RawHandle, from_client: Pipe) { - let resp: TcpConnectResponse = NetdConn::connect() - .expect("netd is serving") - .request_with_handles( - &[to_client, from_client.into_raw()], - MsgType::TcpConnectPiped, - &TcpConnectPipedRequest { addr: HOST, port, _pad: 0, timeout_ms: NO_DEADLINE }, - ) - .expect("netd takes the request") - .response() - .expect("netd connects to the host"); - println!("netd_refused_pipes: connected, socket {}", resp.socket_id); -} - -/// Ask netd for a listener on `port`, handing it `notify` as it is. -fn bind_with(port: u16, notify: RawHandle) -> TcpSocketId { - let resp: TcpBindResponse = NetdConn::connect() - .expect("netd is serving") - .request_with_handles( - &[notify], - MsgType::TcpBindPiped, - &TcpBindPipedRequest { addr: [0; 4], port, _pad: 0 }, - ) - .expect("netd takes the request") - .response() - .expect("netd binds"); - println!("netd_refused_pipes: bound, socket {} on port {}", resp.socket_id, resp.bound_port); - TcpSocketId(resp.socket_id) -} - -/// A file netd can write zero bytes to and not one more: seeked to exactly -/// [`FILE_LIMIT`], each half of that checked on this program's own handle. -fn file_at_its_limit() -> RawHandle { - let file = syscall::open(FILE_PATH.as_bytes(), OpenFlags::WRITE | OpenFlags::CREATE | OpenFlags::TRUNCATE) - .expect("create a file in /tmp"); - assert_eq!( - syscall::seek(file, SeekFrom::Start(FILE_LIMIT + 1)), - Err(SyscallError::InvalidArgument), - "a seek one byte past the size limit", - ); - assert_eq!(syscall::seek(file, SeekFrom::Start(FILE_LIMIT)), Ok(FILE_LIMIT), "a seek to the size limit"); - assert_eq!(syscall::write_nonblock(file, &[]), Ok(0), "a zero-byte write at the size limit"); - assert_eq!( - syscall::write_nonblock(file, &[0]), - Err(SyscallError::InvalidArgument), - "a one-byte write at the size limit", - ); - file -} - -fn receive_end_is_a_read_end(port: u16) { - let (read_end, watch) = toyos::pipe_pair().expect("a pipe"); - let (from_client, tx) = toyos::pipe_pair().expect("a pipe"); - connect_with(port, read_end.into_raw(), from_client); - // Something for netd to try to deliver, unless netd has already refused - // the receive end on its own and closed this pipe's far end with it. - let asked = ask_bytes(Ask::Stream(64)); - match tx.write(&asked) { - Ok(n) => assert_eq!(n, asked.len(), "telling the host what to send"), - Err(e) => assert_eq!(e, SyscallError::Gone, "telling the host what to send"), - } - keep_full_until_released(&watch, "a read end handed over as the receive pipe"); -} - -fn send_end_is_a_write_end(port: u16) { - let (rx, to_client) = toyos::pipe_pair().expect("a pipe"); - let (_kept, write_end) = toyos::pipe_pair().expect("a pipe"); - connect_with(port, to_client.into_raw(), write_end); - // netd ends the connection by closing the receive pipe's write end, which - // this program reads as EOF. - let what = "a write end handed over as the send pipe"; - let mut buf = [0u8; 64]; - let got = await_until(&rx, READABLE, || match rx.read_nonblock(&mut buf) { - Err(SyscallError::WouldBlock) => None, - other => Some(other), - }); - assert_eq!(got, Ok(0), "{what}: bytes or a refusal, not EOF"); -} - -fn notify_end_is_a_read_end() { - let (read_end, watch) = toyos::pipe_pair().expect("a pipe"); - bind_with(0, read_end.into_raw()); - keep_full_until_released(&watch, "a read end handed over as the notify pipe"); -} - -fn receive_end_dropped_while_held(port: u16, capacity: u64) { - let conn = toyos::net::tcp_connect(HOST, port, NO_DEADLINE).expect("connect to the host server"); - ask(&conn.tx, Ask::Stream(capacity + PAST_THE_RING)); - await_ring_full(&conn.rx, capacity); - drop(conn.rx); - println!("netd_refused_pipes: dropped a full receive end with the host still sending"); -} - -fn receive_end_is_a_full_file(port: u16) { - let (from_client, tx) = toyos::pipe_pair().expect("a pipe"); - connect_with(port, file_at_its_limit(), from_client); - ask(&tx, Ask::Stream(64)); - // netd ends the connection by closing the send pipe's read end. - keep_full_until_released(&tx, "a file at its size limit handed over as the receive pipe"); - std::fs::remove_file(FILE_PATH).expect("remove the file"); -} - -fn notify_end_is_a_full_file(port: u16) { - let listener = bind_with(FORWARDED_PORT, file_at_its_limit()); - // The host dials the listener and writes into the connection until it is - // refused, which a listener netd has closed does at the host's next - // segment; it then ends this connection. - let dial = toyos::net::tcp_connect(HOST, port, NO_DEADLINE).expect("connect to the host server"); - ask(&dial.tx, Ask::Dial); - let what = "a file at its size limit handed over as the notify pipe, and the host dialling in"; - assert_eq!(read_pattern(&dial.rx, what), 0, "{what}: the host sent bytes, not its FIN"); - assert_eq!( - toyos::net::tcp_accept(listener).err(), - Some(NetError::NotConnected), - "{what}: the listener is still there to accept from", - ); - std::fs::remove_file(FILE_PATH).expect("remove the file"); -} - -fn round_trip(port: u16, what: &str) { - let conn = toyos::net::tcp_connect(HOST, port, NO_DEADLINE) - .unwrap_or_else(|e| panic!("{what}: netd did not connect: {e:?}")); - ask(&conn.tx, Ask::Stream(ROUND_TRIP)); - let got = read_pattern(&conn.rx, what); - assert_eq!(got, ROUND_TRIP, "{what}: the stream ended after {got} of {ROUND_TRIP} bytes"); - println!("netd_refused_pipes: round trip {what}: {got} bytes"); -} diff --git a/tests/toyos-rust-tests/src/bin/netd_slow_reader.rs b/tests/toyos-rust-tests/src/bin/netd_slow_reader.rs deleted file mode 100644 index 03a56121688..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_slow_reader.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! A TCP receiver that falls a whole pipe behind still gets every byte, once. -//! -//! netd moves a connection's received bytes out of its TCP socket into the -//! client's receive pipe. A pipe is a ring of fixed capacity, so a client that -//! stops reading fills it, and from then on the only lawful home for the -//! peer's further bytes is the socket's own buffer, whose window then closes. -//! -//! This program is that client: it connects to the harness's host server -//! (argv[1] is its port on `HOST`), which sends `stream_byte`s and closes, and -//! reads **nothing** until the ring holds a whole capacity — seen through its -//! own `SYS_PIPE_MAP` window of the receive pipe. Only then does it read the -//! stream to its end and compare each byte with the pattern. -//! -//! The capacity is measured, never assumed: a fresh pipe of this process's own -//! is written until the kernel refuses a byte. -//! -//! `netd_slow_reader: ok bytes=` is the only success line; a stream that -//! differs names its first differing offset and exits non-zero. - -#[path = "../netd_stream.rs"] -mod netd_stream; - -use netd_stream::{ask, await_ring_full, read_pattern, ring_capacity, Ask, HOST, NO_DEADLINE}; - -/// Bytes the host sends past the ring's capacity. Anything over the socket's -/// own buffer makes a pipe that drops bytes when full drop some. -const PAST_THE_RING: u64 = 1024 * 1024; - -fn main() { - let port: u16 = std::env::args() - .nth(1) - .and_then(|p| p.parse().ok()) - .expect("usage: netd_slow_reader "); - let capacity = ring_capacity(); - let total = capacity + PAST_THE_RING; - println!("netd_slow_reader: ring capacity {capacity}, expecting {total} bytes"); - - let conn = toyos::net::tcp_connect(HOST, port, NO_DEADLINE).expect("connect to the host server"); - // The host learns how much to send from here, before it sends anything, - // so the two ends cannot disagree about the length being judged. - ask(&conn.tx, Ask::Stream(total)); - await_ring_full(&conn.rx, capacity); - println!("netd_slow_reader: the ring is full at {capacity} bytes unread; reading"); - - let what = format!("netd_slow_reader (ring capacity {capacity})"); - let at = read_pattern(&conn.rx, &what); - assert_eq!(at, total, "the stream ended after {at} of {total} bytes, every one of them right"); - println!("netd_slow_reader: ok bytes={at}"); -} diff --git a/tests/toyos-rust-tests/src/bin/netd_udp_any_address.rs b/tests/toyos-rust-tests/src/bin/netd_udp_any_address.rs deleted file mode 100644 index a87fde0647e..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_udp_any_address.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! A UDP socket bound to the unspecified address receives the unicast reply to -//! what it sent, which is how every client that is not a server binds one: a -//! resolver's socket among them. -//! -//! Through `std::net::UdpSocket`, the path a Rust program takes. The reply is -//! the harness's UDP echo on `HOST` sending the datagram back to the address -//! it came from, which is this machine's leased address and not `0.0.0.0`. -//! -//! argv[1] is the port of the harness's host server, which this program does -//! not use; argv[2] is the port of the harness's UDP echo. -//! `netd_udp_any_address: ok` is the only success line. - -#[path = "../netd_stream.rs"] -mod netd_stream; - -use std::net::{Ipv4Addr, SocketAddr, UdpSocket}; - -use netd_stream::HOST; - -fn main() { - let echo: u16 = std::env::args() - .nth(2) - .and_then(|p| p.parse().ok()) - .expect("usage: netd_udp_any_address "); - - let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, 0)).expect("bind the unspecified address"); - let to = SocketAddr::from((HOST, echo)); - let datagram: Vec = (0..200u8).collect(); - assert_eq!(socket.send_to(&datagram, to).expect("send to the echo"), datagram.len()); - - // No deadline: a reply that never comes is a hang the harness ceiling reds. - let mut buf = [0u8; 512]; - let (n, from) = socket.recv_from(&mut buf).expect("the receive"); - let got = &buf[..n]; - assert_eq!(from, to, "the reply came from somewhere other than the echo"); - assert_eq!(got, &datagram[..], "the echo's reply is not the datagram sent"); - println!("netd_udp_any_address: ok"); -} diff --git a/tests/toyos-rust-tests/src/bin/netd_udp_refused.rs b/tests/toyos-rust-tests/src/bin/netd_udp_refused.rs deleted file mode 100644 index e65750478fe..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_udp_refused.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! A UDP datagram the client's receive pipe will not take whole ends that -//! socket by name, and no other. -//! -//! netd answers a receive with the datagram's length once the bytes are in the -//! client's pipe, and a pipe write takes what room there is: a pipe that took -//! part of one would splice the next datagram onto it. So a partial write -//! ends the socket, and the client asking for the datagram is refused. -//! -//! The full socket is this program's own making: it keeps a second handle to -//! the write end it hands netd, fills the pipe through it, and reads back -//! [`ROOM`] bytes, so netd's write of a [`DATAGRAM`]-byte datagram takes -//! exactly `ROOM`. A second, ordinary socket must then still get its datagram. -//! -//! **The socket that ended is gone whole**: netd's stack holds as many sockets -//! no table entry names as before it was bound (`net.sockets.untabled`, read -//! through netd's own `inspect`), its port binds again, and the pipe netd -//! wrote into reads end-of-file once this program has let go of its own write -//! end, because netd has let go of the one it was handed. The count is what -//! sees a socket left in the stack: closing one already frees its port. -//! -//! **A held port is not handed out twice**: binding the ordinary socket's port -//! by number is refused as in use, and a port-0 bind passes over a port bound -//! by number where its next pick would have been. smoltcp hands a datagram to -//! the first socket that takes it, so a second socket on a port receives -//! nothing, the resolver's among them. -//! -//! argv[1] is the port of the harness's host server, which this program does -//! not use; argv[2] is the port of the harness's UDP echo on `HOST`. -//! `netd_udp_refused: ok` is the only success line. - -#[path = "../netd_stream.rs"] -mod netd_stream; - -use netd_stream::{fill, HOST}; -use toyos::ipc::{FrameRx, RxStep}; -use toyos::net::{ - udp_bind, udp_recv_from, udp_send_to, MsgType, NetError, NetdConn, UdpBindRequest, - UdpBindResponse, UdpRecvResponse, UdpSocketId, -}; -use toyos::poller::{Poller, READABLE}; -use toyos::{AsHandle, Pipe}; -use toyos_abi::syscall::{self, SyscallError}; -use toyos_inspect::{Value, MAX_SNAPSHOT_BYTES, MSG_INSPECT, MSG_SNAPSHOT}; - -/// Both sockets bind every address, as an ordinary client's does. -const ANY: [u8; 4] = [0, 0, 0, 0]; - -/// Bytes of room left in the full socket's pipe. -const ROOM: usize = 100; - -/// Bytes in each datagram: more than [`ROOM`], less than one Ethernet frame. -const DATAGRAM: usize = 1000; - -fn main() { - let echo: u16 = std::env::args() - .nth(2) - .and_then(|p| p.parse().ok()) - .expect("usage: netd_udp_refused "); - - let healthy = udp_bind(ANY, 0).expect("bind an ordinary socket"); - assert_eq!( - udp_bind(ANY, healthy.bound_port).err(), - Some(NetError::AddrInUse), - "port {} was bound a second time", - healthy.bound_port - ); - // Where netd's next port-0 pick would be, unless another program took a - // port since, which leaves this check passing without having tested. - let next = if healthy.bound_port == u16::MAX { 49152 } else { healthy.bound_port + 1 }; - let _by_number = udp_bind(ANY, next).unwrap_or_else(|e| panic!("binding port {next} by number: {e:?}")); - let picked = udp_bind(ANY, 0).expect("a port-0 bind"); - assert_ne!(picked.bound_port, next, "a port-0 bind was handed port {next}, which another socket holds"); - println!("netd_udp_refused: port {} is refused a second socket, and a port-0 bind passed over {next}", healthy.bound_port); - - let (rx, kept) = toyos::pipe_pair().expect("a receive pipe"); - let handed = syscall::dup(kept.as_handle()).expect("a second handle to the receive pipe's write end"); - let capacity = fill(&kept); - // netd's handle is the pipe's only writer from here on. - drop(kept); - let mut room = [0u8; ROOM]; - assert_eq!(rx.read_nonblock(&mut room), Ok(ROOM), "making room in the full pipe"); - let (from_client, tx) = toyos::pipe_pair().expect("a send pipe"); - let before = untabled(); - let full: UdpBindResponse = NetdConn::connect() - .expect("netd is serving") - .request_with_handles( - &[handed, from_client.into_raw()], - MsgType::UdpBind, - &UdpBindRequest { addr: ANY, port: 0, _pad: 0 }, - ) - .expect("netd takes the request") - .response() - .expect("netd binds"); - let full_id = UdpSocketId(full.socket_id); - assert_eq!(untabled(), before, "the socket bound is not in netd's table"); - println!("netd_udp_refused: socket {} has {ROOM} bytes of room in a {capacity}-byte pipe", full.socket_id); - - send(full_id, &tx, echo, 0xA5); - match recv(full_id) { - Err(NetError::ConnectionReset) => {} - Ok(r) => panic!("a {DATAGRAM}-byte datagram into {ROOM} bytes of room was answered {} bytes", r.len), - Err(e) => panic!("a {DATAGRAM}-byte datagram into {ROOM} bytes of room was refused {e:?}, not by a reset"), - } - assert_eq!( - recv(full_id).err(), - Some(NetError::NotConnected), - "the socket that could not take a datagram whole is still there", - ); - assert_eq!(untabled(), before, "netd's stack still holds the ended socket"); - println!("netd_udp_refused: the socket whose pipe would not take a datagram whole is gone"); - - let again = udp_bind(ANY, full.bound_port) - .unwrap_or_else(|e| panic!("port {} of the ended socket would not bind again: {e:?}", full.bound_port)); - assert_eq!(again.bound_port, full.bound_port); - let mut drained = 0usize; - let mut chunk = vec![0u8; 65536]; - loop { - match rx.read_nonblock(&mut chunk) { - Ok(0) => break, - Ok(n) => drained += n, - Err(SyscallError::WouldBlock) => { - panic!("netd still holds the ended socket's receive pipe, {drained} bytes read out of it") - } - Err(e) => panic!("reading the ended socket's receive pipe: {e:?}"), - } - } - assert_eq!(drained, capacity as usize, "the pipe held its fill and the {ROOM} bytes netd wrote"); - println!("netd_udp_refused: its port binds again and its pipe has no writer left"); - - send(healthy.socket_id, &healthy.tx, echo, 0x5A); - let answer = recv(healthy.socket_id).expect("the ordinary socket's datagram"); - assert_eq!(answer.len as usize, DATAGRAM, "the ordinary socket's datagram length"); - let mut got = vec![0u8; DATAGRAM]; - let n = healthy.rx.read_nonblock(&mut got).expect("the answered datagram is in the pipe"); - assert_eq!(n, DATAGRAM, "the ordinary socket's pipe holds the datagram it was answered"); - assert!(got.iter().all(|&b| b == 0x5A), "the ordinary socket's datagram came back changed"); - println!("netd_udp_refused: ok"); -} - -/// Send one [`DATAGRAM`] of `byte` from `socket` to the host's echo. -fn send(socket: UdpSocketId, tx: &Pipe, echo: u16, byte: u8) { - let datagram = [byte; DATAGRAM]; - assert_eq!(tx.write(&datagram), Ok(DATAGRAM), "writing the datagram for netd"); - let sent = udp_send_to(socket, HOST, echo, DATAGRAM as u16).expect("netd sends the datagram"); - assert_eq!(sent as usize, DATAGRAM, "netd sent part of the datagram"); -} - -/// The sockets netd's stack holds that no table entry names, as its own -/// `inspect` answers, waited for with no deadline. Every program's sockets are in the -/// table and the resolver's are left out, so only netd's own and one that -/// outlived its entry move it. -fn untabled() -> u64 { - let conn = toyos::endow::service("netd").expect("a connection to netd"); - conn.signal(MSG_INSPECT).expect("netd takes an inspect request"); - let poller = Poller::new(1); - let mut rx: Box> = Box::new(FrameRx::new()); - loop { - match rx.pump(&conn) { - RxStep::Frame { msg_type: MSG_SNAPSHOT, payload_len } => { - let snap = toyos_inspect::decode(rx.payload(payload_len), toyos_inspect::NET) - .unwrap_or_else(|why| panic!("netd's snapshot: {why}")); - return match snap.get("net.sockets.untabled") { - Some(Value::U64(n)) => *n, - other => panic!("netd's snapshot has net.sockets.untabled as {other:?}"), - }; - } - RxStep::Idle => {} - other => panic!("netd answered inspect with {other:?}, not a snapshot"), - } - poller.watch(&conn, READABLE, 0); - poller.wait(1, u64::MAX, |_| {}); - } -} - -/// Ask netd for `socket`'s next datagram, with no deadline: an answer that -/// never comes is a hang the harness ceiling reds. -fn recv(socket: UdpSocketId) -> Result { - udp_recv_from(socket, DATAGRAM as u32) -} diff --git a/tests/toyos-rust-tests/src/bin/panic_halts_first.rs b/tests/toyos-rust-tests/src/bin/panic_halts_first.rs index b97eb5403c6..b9816f69283 100644 --- a/tests/toyos-rust-tests/src/bin/panic_halts_first.rs +++ b/tests/toyos-rust-tests/src/bin/panic_halts_first.rs @@ -1,8 +1,8 @@ //! Threads that make kernel records as fast as they can while this one has //! the kernel go fatal (`SYS_DEBUG` action 3). A sibling still running after //! the fatal path stopped the other CPUs is a record past the fatal path's own -//! line after the stop; `panic_halts_the_others_first` reads the console for -//! one. +//! line after the stop; `virt_fatal_halts_the_others_first` reads the console +//! for one. use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; diff --git a/tests/toyos-rust-tests/src/bin/partition_claimant.rs b/tests/toyos-rust-tests/src/bin/partition_claimant.rs deleted file mode 100644 index d6d17b7f344..00000000000 --- a/tests/toyos-rust-tests/src/bin/partition_claimant.rs +++ /dev/null @@ -1,322 +0,0 @@ -//! A GPT partition on a disk the kernel drives, claimed as a device: its one -//! holder reads and writes its blocks and nobody else's, nothing the kernel -//! or a file server holds can be claimed, and a claim's fsync answers for its -//! own writes. -//! -//! The disks are crafted and judged by `tests/common/partclaim.rs` on the -//! host: this binary's account of what it wrote is exactly what is in -//! question, so the verdict on the neighbours and on the target's bytes is read -//! off the images after the guest is gone. What this binary asserts is every -//! refusal, each with the word the ABI promises for it. -//! -//! Roles, by the first argument: -//! - `main ` — every refusal, the idle ROOT slot written -//! whole and read back, and both releases; the three GUIDs are the boot -//! stick's, which the host drew and this binary cannot know; -//! - `holder` — claims the target, says so, and waits to be killed; -//! - `endowed` — finds the claim its parent moved to it, by the label init -//! endows a `part:` row under. - -use std::io::{BufRead, BufReader, Write}; -use std::os::toyos::process::CommandExt; -use std::process::{Command, Stdio}; -use std::time::Duration; - -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos::{AsHandle, PartitionDev}; -use toyos_abi::part::{Block, PartGuid, BLOCK_BYTES, MAX_BLOCKS_PER_CALL}; -use toyos_abi::syscall::{DeviceType, SyscallError, DEV_PREFIX, SYSCAP_LABEL, SYS_DEVICE_CLAIM}; - -const SELF_PATH: &str = "/system/bin/test_rs_partition_claimant"; - -/// Mirrored in `tests/common/partclaim.rs`: the idle ROOT slot this binary -/// writes whole — a TOYOS-ROOT partition the boot probed and did not mount. -const TARGET: &str = "7B1D4A3C-2E5F-4C8A-9D6B-0A1F2E3D4C5B"; -/// Mirrored, and in `tests/partclaimcase/system.toml`: the partition init -/// grants test-runner. -const GRANTED: &str = "A94F0E6D-3B2C-4E1A-8C7D-6E5F4A3B2C1D"; -/// Mirrored: a partition whose length is not whole 4 KiB blocks. -const MISALIGNED: &str = "3E8A1C5F-7D2B-4F60-9A1E-5C4B3D2E1F07"; -/// Mirrored: a partition of whole 4 KiB blocks that begins inside one. -const MISSTART: &str = "5A7C9E1B-3D5F-4B71-8C2E-4F6A8B0C2D35"; -/// Mirrored: DATA, which fsd serves `/home` from through its claim. -const DATA: &str = "E3A7C5D9-1B2F-4E6A-8D0C-9F7B5A3E1C24"; - -/// Mirrored: the target's length in blocks. -const TARGET_BLOCKS: u64 = 2048; -/// Mirrored: the `/home` file written between the target's transfers, so -/// fsd's writes to the same disk are interleaved with the claim's. -const HOME_FILE: &str = "/home/partclaim-interleaved.bin"; -const HOME_CHUNK: usize = 32 * 1024; - -/// Mirrored: what block `n` of the target holds once this binary is done. -fn pattern(n: u64) -> Block { - let mut block = [0u8; BLOCK_BYTES]; - for (i, byte) in block.iter_mut().enumerate() { - *byte = (n as usize).wrapping_mul(31).wrapping_add(i) as u8; - } - block[..8].copy_from_slice(&n.to_le_bytes()); - block[8..24].copy_from_slice(b"TOYOS-PARTCLAIM\0"); - block -} - -/// Mirrored: what the refused writes past the end carry, so a block of the -/// neighbour holding it says which write reached it. -const PAST_END: &[u8; 16] = b"TOYOS-PAST-END\0\0"; - -fn guid(text: &str) -> PartGuid { - PartGuid::parse(text).unwrap_or_else(|| panic!("{text} is not a GUID")) -} - -fn claim(cap: &SysCap, name: PartGuid) -> Result { - cap.claim_partition::(name) -} - -fn said(what: &str, got: SyscallError) { - println!("partition_claimant: {what} refused with {got:?}"); -} - -fn refused(cap: &SysCap, what: &str, name: PartGuid, want: SyscallError) { - match claim(cap, name) { - Err(got) if got == want => said(what, got), - Err(got) => panic!("{what}: expected {want:?}, got {got:?}"), - Ok(_) => panic!("{what}: expected {want:?}, and the claim was minted"), - } -} - -fn main() { - let cap: SysCap = Endowments::get() - .take(SYSCAP_LABEL) - .expect("the test estate is endowed a device-minting capability"); - let args: Vec = std::env::args().skip(1).collect(); - match args.first().map(String::as_str) { - Some("main") => test(&cap, &args[1..]), - Some("holder") => holder(&cap), - Some("endowed") => endowed(), - other => panic!("unknown role {other:?}"), - } -} - -fn test(cap: &SysCap, boot_stick: &[String]) { - let [esp, log, root] = boot_stick else { - panic!("main takes the boot stick's ESP, log and ROOT GUIDs, got {boot_stick:?}"); - }; - refused(cap, "ROOT, which the kernel holds,", guid(root), SyscallError::PermissionDenied); - // init minted these for the file servers of their roles. - for (what, name) in [("the ESP", esp.as_str()), ("DATA", DATA)] { - refused(cap, &format!("{what}, which a file server holds,"), guid(name), SyscallError::AlreadyExists); - } - - // init minted this one for test-runner from the manifest's `part:` row. - refused(cap, "the partition init granted test-runner", guid(GRANTED), SyscallError::AlreadyExists); - - // The crafted disk carries a copy of the log partition's unique GUID. - refused(cap, "the log partition, whose unique GUID two disks carry,", guid(log), SyscallError::InvalidArgument); - refused( - cap, - "a partition that is not whole 4 KiB blocks", - guid(MISALIGNED), - SyscallError::NotSupported, - ); - refused( - cap, - "a partition that begins inside a 4 KiB block", - guid(MISSTART), - SyscallError::NotSupported, - ); - refused( - cap, - "a GUID no partition carries", - guid("00000000-0000-0000-0000-000000000001"), - SyscallError::NotFound, - ); - refused( - cap, - "the all-zero GUID, which GPT means as an unused entry,", - PartGuid([0; 16]), - SyscallError::NotFound, - ); - unread_selector_words(cap); - - let target = claim(cap, guid(TARGET)).expect("the idle ROOT slot is claimable"); - let info = target.describe().expect("a partition claim describes itself"); - assert_eq!(info.blocks, TARGET_BLOCKS, "the claim's length is the partition's"); - assert_eq!(info.unique(), guid(TARGET), "the claim is the partition it was named by"); - println!("partition_claimant: claimed {} blocks", info.blocks); - refused(cap, "the target, a second time,", guid(TARGET), SyscallError::AlreadyExists); - - past_the_end(&target, info.blocks); - - // The whole partition, first block to last, with fsd's writes to `/home` - // — the same disk — between the runs. - let mut home = std::fs::File::create(HOME_FILE).expect("create the /home file"); - let runs = info.blocks.div_ceil(MAX_BLOCKS_PER_CALL as u64); - for run in 0..runs { - let first = run * MAX_BLOCKS_PER_CALL as u64; - let count = (info.blocks - first).min(MAX_BLOCKS_PER_CALL as u64); - let blocks: Vec = (first..first + count).map(pattern).collect(); - target.write(first, &blocks).unwrap_or_else(|e| panic!("write at {first}: {e:?}")); - if run % 8 == 0 { - let piece: Vec = (0..HOME_CHUNK).map(|i| (run as usize ^ i) as u8).collect(); - home.write_all(&piece).expect("append to the /home file"); - home.sync_all().expect("the /home file is durable"); - } - } - drop(home); - target.sync().expect("the claim's writes are durable"); - - let mut back = vec![[0u8; BLOCK_BYTES]; MAX_BLOCKS_PER_CALL]; - for run in 0..runs { - let first = run * MAX_BLOCKS_PER_CALL as u64; - let count = (info.blocks - first).min(MAX_BLOCKS_PER_CALL as u64) as usize; - target.read(first, &mut back[..count]).unwrap_or_else(|e| panic!("read at {first}: {e:?}")); - for (i, block) in back[..count].iter().enumerate() { - let n = first + i as u64; - assert!(*block == pattern(n), "block {n} did not read back as written"); - } - } - println!("partition_claimant: wrote and read back {} blocks", info.blocks); - - // Letting the last handle go is what releases the partition. - drop(target); - drop(reclaimed(cap, guid(TARGET), "its holder closed it")); - - // A holder that dies never closes anything: teardown is what gives the - // partition back. - let mut holder = child(cap, "holder").stdout(Stdio::piped()).spawn().expect("spawn holder"); - let mut out = BufReader::new(holder.stdout.take().expect("holder stdout")); - let mut line = String::new(); - out.read_line(&mut line).expect("the holder's ready line"); - assert_eq!(line.trim(), "held", "the holder did not claim the target: {line:?}"); - refused(cap, "the target, while another process holds it,", guid(TARGET), SyscallError::AlreadyExists); - holder.kill().expect("kill the holder"); - holder.wait().expect("reap the holder"); - drop(reclaimed(cap, guid(TARGET), "its holder was killed")); - - // A claim moved to a child under the label init writes for a `part:` row — - // `dev:` and the row's own spelling — is the one `endow::partition` finds. - let moved = cap - .claim_partition::(guid(TARGET)) - .expect("the target is claimable to move"); - let status = child(cap, "endowed") - .endow(&format!("{DEV_PREFIX}part:{TARGET}"), moved.into_raw().0) - .status() - .expect("run the endowed child"); - assert!(status.success(), "the endowed child did not find its claim: {status:?}"); - drop(reclaimed(cap, guid(TARGET), "the child it was moved to exited")); - - println!("partition_claimant: PASS"); -} - -/// `name` claimed again once `how`. The release is deferred to a drain -/// another CPU may be running when `close` or the kill returns -/// (issues/kernel/deferred-release-outlives-its-syscall.md), so the claim is -/// asked again for a bounded second rather than once. -fn reclaimed(cap: &SysCap, name: PartGuid, how: &str) -> PartitionDev { - (0..1000) - .find_map(|_| match claim(cap, name) { - Err(SyscallError::AlreadyExists) => { - std::thread::sleep(Duration::from_millis(1)); - None - } - other => Some(other), - }) - .unwrap_or_else(|| panic!("{name:?} was still held a second after {how}")) - .unwrap_or_else(|e| panic!("{name:?} is not claimable again once {how}: {e:?}")) -} - -/// This binary's children mint their own claims, so each is endowed a -/// duplicate of the capability. -fn child(cap: &SysCap, role: &str) -> Command { - let mut command = Command::new(SELF_PATH); - let dup = cap.duplicate().expect("duplicate the capability for a child"); - command.arg(role).endow(SYSCAP_LABEL, dup.into_raw().0); - command -} - -fn holder(cap: &SysCap) { - let target = claim(cap, guid(TARGET)).expect("the holder claims the target"); - println!("held"); - std::io::stdout().flush().expect("flush the ready line"); - loop { - std::thread::sleep(Duration::from_secs(60)); - let _ = ⌖ - } -} - -/// The claim the parent moved here, found by `endow::partition` under the -/// label a `part:` row is endowed with. -fn endowed() { - let target: PartitionDev = - toyos::endow::partition(guid(TARGET)).expect("the moved claim is in the endowment table"); - let info = target.describe().expect("the moved claim describes itself"); - assert_eq!(info.unique(), guid(TARGET), "the endowment is the partition its label names"); - let mut first = [[0u8; BLOCK_BYTES]]; - target.read(0, &mut first).expect("read through the moved claim"); - assert!(first[0] == pattern(0), "the moved claim reads what the parent wrote"); -} - -/// `SYS_DEVICE_CLAIM` with every selector word given, which no typed wrapper -/// can spell: a word the class does not read is refused, never dropped. -fn unread_selector_words(cap: &SysCap) { - fn raw(a1: u64, a2: u64, a3: u64, a4: u64) -> u64 { - let ret: u64; - // SAFETY: a register-only `syscall`; no argument is a pointer. - unsafe { - core::arch::asm!( - "syscall", - in("rdi") SYS_DEVICE_CLAIM, - in("rsi") a1, - in("rdx") a2, - in("r8") a3, - in("r9") a4, - lateout("rax") ret, - out("rcx") _, - out("r11") _, - ); - } - ret - } - let handle = cap.as_handle().0 as u64; - for (what, class, words) in [ - ("a mouse claim carrying a first selector word", DeviceType::Mouse, [1, 0]), - ("a mouse claim carrying a second selector word", DeviceType::Mouse, [0, 1]), - ("a PCI claim carrying a second selector word", DeviceType::PciFunction, [0x1af4_1041, 1]), - ] { - match SyscallError::from_u64(raw(handle, class as u64, words[0], words[1])) { - Some(SyscallError::InvalidArgument) => said(what, SyscallError::InvalidArgument), - other => panic!("{what}: expected InvalidArgument, got {other:?}"), - } - } -} - -/// Every transfer that does not end inside the partition is refused before it -/// reaches the device, reads included: a read past the end is somebody else's -/// data. -fn past_the_end(target: &PartitionDev, blocks: u64) { - let mut marked = [0u8; BLOCK_BYTES]; - marked[..PAST_END.len()].copy_from_slice(PAST_END); - let one = [marked]; - let two = [marked, marked]; - let long = vec![marked; MAX_BLOCKS_PER_CALL + 1]; - - let cases: [(&str, Result<(), SyscallError>); 5] = [ - ("one block at the partition's length", target.write(blocks, &one)), - ("two blocks from its last", target.write(blocks - 1, &two)), - ("a first block that overflows", target.write(u64::MAX, &one)), - ("more blocks than one call carries", target.write(0, &long)), - ("no blocks at all", target.write(0, &[])), - ]; - for (what, got) in cases { - assert_eq!(got, Err(SyscallError::InvalidArgument), "a write of {what}"); - } - let mut into = [[0u8; BLOCK_BYTES]]; - assert_eq!( - target.read(blocks, &mut into), - Err(SyscallError::InvalidArgument), - "a read of one block at the partition's length" - ); - println!("partition_claimant: every transfer past the end refused"); -} - diff --git a/tests/toyos-rust-tests/src/bin/pkg_launch_gbae.rs b/tests/toyos-rust-tests/src/bin/pkg_launch_gbae.rs deleted file mode 100644 index ac154f706a9..00000000000 --- a/tests/toyos-rust-tests/src/bin/pkg_launch_gbae.rs +++ /dev/null @@ -1,118 +0,0 @@ -//! Launch an installed package, holding nothing that could start it otherwise. -//! -//! This binary is no `[programs]` key, so it holds what `test-runner` holds, -//! and `tests/pkgcase/system.toml` gives that estate a `launcher` connector and -//! no `compositor` one. A window that appears after this came from the `[apps]` -//! row init built out of `/apps/gbae/manifest.toml`, because inheritance -//! carries nothing that would draw one. -//! -//! It does not wait: gbae runs until the machine goes down, and the compositor -//! census on the host's side of the serial says the window exists. - -use std::process::Command; - -const PROGRAM: &str = "/apps/gbae/gbae"; - -const PLANTED_DIR: &str = "/apps/toy"; -const PLANTED: &str = "/apps/toy/echo"; -const DECLARED: &str = "/system/bin/toybox"; - -fn main() -> std::process::ExitCode { - match std::env::args().nth(1).as_deref() { - Some("symlink-row") => return symlink_row(), - Some("relative-path") => return relative_path(), - _ => {} - } - // The refusal arm is a first-class outcome and not a panic: the same - // binary runs before the package is installed and after it is removed, - // where init answering "no" is the assertion. - match Command::new(PROGRAM).spawn() { - Ok(child) => { - println!("pkg-launch: started {PROGRAM} as pid {}", child.id()); - std::process::ExitCode::SUCCESS - } - Err(e) => { - println!("pkg-launch: {PROGRAM} did not start: {e}"); - std::process::ExitCode::FAILURE - } - } -} - -/// The first is what `package_of` classifies; the other four it answers `None` -/// for while `sys_readlink` lands them all on the same file. -const SPELLINGS: [&str; 5] = [ - "/apps/toy/echo", - "/apps/./toy/echo", - "/apps//toy/echo", - "apps/toy/echo", - "/tmp/../apps/toy/echo", -]; - -/// `toybox`'s row carries `syscap = ["power"]` here and this estate holds none, -/// so a launch resolving through the link is a process handed a capability -/// nothing gave it. Exit 0 is every spelling refused. -fn symlink_row() -> std::process::ExitCode { - std::fs::create_dir_all(PLANTED_DIR).expect("/apps is writable"); - std::os::toyos::fs::symlink(DECLARED, PLANTED).expect("a symlink under /apps is allowed"); - for spelling in SPELLINGS { - let target = std::fs::read_link(spelling).expect("every spelling reaches the link"); - assert_eq!(target.to_str(), Some(DECLARED), "{spelling} does not reach the planted link"); - } - - let mut refused = 0; - for spelling in SPELLINGS { - match Command::new(spelling).spawn() { - Ok(child) => println!( - "pkg-symlink: {spelling} started as pid {} — a link under /apps reached \ - {DECLARED}'s row", - child.id() - ), - Err(e) => { - println!("pkg-symlink: {spelling} refused: {e}"); - refused += 1; - } - } - } - if refused == SPELLINGS.len() { - std::process::ExitCode::SUCCESS - } else { - println!("pkg-symlink: {refused} of {} spellings refused", SPELLINGS.len()); - std::process::ExitCode::FAILURE - } -} - -/// The real shell binary through `shell -c`, rather than a second copy of what -/// it does. `-c` roots the cwd at `/`, so the dotted forms are rooted there. -fn relative_path() -> std::process::ExitCode { - const DIR: &str = "/home/toy/reltest"; - const NONCE: &str = "relpath-ran-9c41"; - std::fs::create_dir_all(DIR).expect("/home is writable"); - let link = format!("{DIR}/echo"); - let _ = std::fs::remove_file(&link); - std::os::toyos::fs::symlink(DECLARED, &link).expect("a symlink under /home is allowed"); - - let mut ran = 0; - for typed in ["./home/toy/reltest/echo", "../home/toy/reltest/echo"] { - let out = Command::new("/system/bin/shell") - .arg("-c") - .arg(format!("{typed} {NONCE}")) - .output(); - match out { - Ok(out) => { - let said = String::from_utf8_lossy(&out.stdout).into_owned(); - if said.contains(NONCE) { - println!("pkg-relpath: {typed} ran and said {NONCE}"); - ran += 1; - } else { - println!("pkg-relpath: {typed} said {said:?}, not {NONCE}"); - } - } - Err(e) => println!("pkg-relpath: the shell did not start: {e}"), - } - } - if ran == 2 { - std::process::ExitCode::SUCCESS - } else { - std::process::ExitCode::FAILURE - } -} diff --git a/tests/toyos-rust-tests/src/bin/quiesce_twice.rs b/tests/toyos-rust-tests/src/bin/quiesce_twice.rs deleted file mode 100644 index 3d0b1f2a278..00000000000 --- a/tests/toyos-rust-tests/src/bin/quiesce_twice.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Two callers of the stop, and the one that is refused. -//! -//! init makes the first call, asked through its `power` port. -//! `quiesce-last-park` holds that call once it has -//! claimed the stop and before it stops anything, until a thread named -//! [`LAST_THREAD`] parks: the window in which every other thread still runs. -//! This process reads the kernel's log until the kernel says it waits there, -//! makes the second call itself, and only on being refused by name starts the -//! thread the stop waits for — so a stop that completes is that refusal -//! having reached Ring 3 as a word. -//! -//! Nothing here asserts: `common::power::quiesce_refuses_a_second_shutdown` is -//! the judge, and its doc is the scenario. - -use std::time::Duration; - -use toyos::endow::{Endowments, SYSCAP_LABEL}; -use toyos::log::{LogTail, Record, MAX_LOG_SHARDS}; -use toyos::poller::{Poller, READABLE}; -use toyos::power::Stop; -use toyos::syscap::SysCap; -use toyos_abi::syscall::SyscallError; -use toyos_quiesce::LAST_THREAD; - -/// What the kernel says once the first call has claimed the stop and waits -/// for the held thread. -const WAITS: &str = "quiesce-last-park: the stop waits for"; - -/// Records per read; above the shard count, which the call refuses. -const BATCH: usize = 4 * MAX_LOG_SHARDS as usize; - -/// The poll's one token. -const LOG_TOKEN: u64 = 1; - -fn main() { - let Some(cap) = Endowments::get().take::(SYSCAP_LABEL) else { - eprintln!("quiesce_twice: this program was endowed no system capability"); - std::process::exit(1); - }; - // The first call, from init. Comes back only refused. - std::thread::spawn(|| { - let refused = toyos::power::stop(Stop::Reboot); - eprintln!("quiesce_twice: init did not stop the machine ({refused:?})"); - std::process::exit(1); - }); - - // Parked on the log's readiness between reads. The readiness is an edge, - // so each watch is armed before the read it guards, and one is - // outstanding at a time. - let mut tail = LogTail::new(); - let mut buf = [Record::EMPTY; BATCH]; - let poller = Poller::new(1); - poller.watch(&cap, READABLE, LOG_TOKEN); - // A completion drained while arming is the watch spent: waiting on it - // afterwards parks on nothing. - let mut spent = false; - poller.wait(0, 0, |_| spent = true); - loop { - let batch = tail.read(&cap, &mut buf).unwrap_or_else(|e| { - eprintln!("quiesce_twice: the log would not read ({e:?})"); - std::process::exit(1); - }); - if batch.iter().any(|record| record.message().contains(WAITS)) { - break; - } - if !batch.is_empty() { - continue; - } - // No deadline: a first call that never waits is a hang the harness - // ceiling reds. - if !spent { - poller.wait(1, u64::MAX, |_| {}); - } - poller.watch(&cap, READABLE, LOG_TOKEN); - spent = false; - poller.wait(0, 0, |_| spent = true); - } - - // The other power syscall, so the one boot judges the claim on both. - let refused = cap.shutdown(); - if refused != SyscallError::AlreadyExists { - eprintln!("quiesce_twice: the second call was answered {refused:?}, not AlreadyExists"); - std::process::exit(1); - } - std::thread::Builder::new() - .name(LAST_THREAD.into()) - .spawn(asleep_until_stopped) - .expect("spawn the thread the stop waits for"); - // No deadline: a machine that never stops this process is a hang the - // harness ceiling reds. - asleep_until_stopped() -} - -/// Asleep until the machine stops, which is the only thing that ends it. A -/// sleep and not a park because `nanosleep` is the syscall `quiesce-last-park` -/// holds the named thread in; its span is never reached. -fn asleep_until_stopped() -> ! { - loop { - std::thread::sleep(Duration::MAX); - } -} diff --git a/tests/toyos-rust-tests/src/bin/spawn_cwd.rs b/tests/toyos-rust-tests/src/bin/spawn_cwd.rs deleted file mode 100644 index eac826b3a5a..00000000000 --- a/tests/toyos-rust-tests/src/bin/spawn_cwd.rs +++ /dev/null @@ -1,247 +0,0 @@ -//! A child starts in the directory its spawn names, on both roads to the kernel. -//! -//! `SpawnArgs` carries the child's working directory, and the kernel starts the -//! child there or refuses the spawn by name — it never substitutes the -//! caller's. A directory on a file server the kernel cannot judge, so std judges -//! it before either road asks. std's direct spawn states `Command::current_dir` when it is set -//! and this process's own directory when it is not, and init's launcher states -//! the one its client sent. -//! -//! On `tests/netcase`, because the two roads are told apart by configuration -//! there: its test-runner holds a `launcher` connector and declares -//! `/system/bin/toybox` and `/system/bin/shell`, so a spawn of either goes -//! through init, while this binary is declared nowhere and spawns directly. -//! -//! Exit 0 is every arm answering the directory it asked for, and every refusal -//! arriving under its own name. - -use std::process::{Command, Output}; - -use toyos_abi::syscall::{self, SpawnArgs, SyscallError}; - -const SELF: &str = "/system/bin/test_rs_spawn_cwd"; -const TOYBOX: &str = "/system/bin/toybox"; -const SHELL: &str = "/system/bin/shell"; - -/// Two directories, so no arm can pass by answering the one before it. -const NAMED: &str = "/tmp/spawn-cwd/named"; -const OWN: &str = "/tmp/spawn-cwd/own"; -const ABSENT: &str = "/tmp/spawn-cwd/absent"; -/// A file where a directory is asked for, on the volume `SELF` is not. -const FILE: &str = "/tmp/spawn-cwd/file"; -/// One file more than `MAX_LIST_ENTRIES`: a cwd judged by listing its subtree -/// refuses every spawn from here. -const BIG: &str = "/tmp/spawn-cwd/big"; -const BIG_FILES: usize = 16_385; -/// A directory and a file on each writable mount that is not a tmpfs, so each -/// filesystem's own `is_dir` is asked: `/log` is FAT32 and `/home` is the DATA -/// volume's bcachefs. The `/home` directory is never `mkdir`ed, for the reason -/// `BIG` is not. -const LOG_DIR: &str = "/log/spawn-cwd/dir"; -const LOG_FILE: &str = "/log/spawn-cwd/file"; -const HOME_DIR: &str = "/home/spawn-cwd/dir"; -const HOME_FILE: &str = "/home/spawn-cwd/file"; -const SPAWNS: u32 = 8; - -fn main() { - let args: Vec = std::env::args().collect(); - match args.get(1).map(String::as_str) { - Some("pwd") => { - println!("{}", std::env::current_dir().expect("a process has a cwd").display()); - return; - } - // The raw arm gives it no stdio, so it answers by exit code. - Some("is") => { - let here = std::env::current_dir().expect("a process has a cwd"); - let there = args.get(2).map(String::as_str); - std::process::exit(if here.to_str() == there { 0 } else { 3 }); - } - _ => {} - } - std::fs::create_dir_all(NAMED).expect("/tmp is writable"); - std::fs::create_dir_all(OWN).expect("/tmp is writable"); - std::fs::write(FILE, b"not a directory").expect("/tmp is writable"); - std::fs::create_dir_all(LOG_DIR).expect("/log is writable"); - std::fs::write(LOG_FILE, b"not a directory").expect("/log is writable"); - std::fs::write(format!("{HOME_DIR}/marker"), b"a name beneath").expect("/home is writable"); - std::fs::write(HOME_FILE, b"not a directory").expect("/home is writable"); - let _ = std::fs::remove_dir(ABSENT); - - // First, so a kernel that grants any of them is seen refusing none of them. - refusals(); - - // The owner's session: the shell's `cd`, then a program it launches. - said( - "shell cd, launched", - Command::new(SHELL).arg("-c").arg(format!("cd {NAMED} && {TOYBOX} pwd")).output(), - NAMED, - ); - said( - "shell -c from current_dir, launched", - Command::new(SHELL).arg("-c").arg(format!("{TOYBOX} pwd")).current_dir(NAMED).output(), - NAMED, - ); - said( - "current_dir, launched", - Command::new(TOYBOX).arg("pwd").current_dir(NAMED).output(), - NAMED, - ); - said( - "current_dir, direct", - Command::new(SELF).arg("pwd").current_dir(NAMED).output(), - NAMED, - ); - - // No `current_dir`: the child starts where its parent is, because std says - // so on both roads — the kernel has no default to fall back on. - std::env::set_current_dir(OWN).expect("chdir into a directory this made"); - said("own cwd, launched", Command::new(TOYBOX).arg("pwd").output(), OWN); - said("own cwd, direct", Command::new(SELF).arg("pwd").output(), OWN); - said( - "relative current_dir, direct", - Command::new(SELF).arg("pwd").current_dir("../named").output(), - NAMED, - ); - std::env::set_current_dir("/").expect("chdir to /"); - - a_cwd_is_judged_in_its_depth(); - for file in [LOG_FILE, HOME_FILE, format!("{HOME_DIR}/marker").as_str()] { - std::fs::remove_file(file).expect("remove a file this made"); - } - std::fs::remove_dir(LOG_DIR).expect("remove a directory this made"); - std::fs::remove_dir("/log/spawn-cwd").expect("remove a directory this made"); - println!("spawn-cwd: every child started where its spawn said"); -} - -/// `output` must have started and printed `want` as its whole answer. -fn said(arm: &str, output: std::io::Result, want: &str) { - let output = output.unwrap_or_else(|e| panic!("{arm}: the child did not start: {e}")); - let got = String::from_utf8_lossy(&output.stdout); - assert!(output.status.success(), "{arm}: exited {:?}, said {got:?}", output.status); - assert_eq!(got.trim_end(), want, "{arm}: the child's cwd"); - println!("spawn-cwd: {arm}: {want}"); -} - -/// The kernel's answer to one raw spawn of this binary into `cwd`, and the -/// child's exit: 0 is in `cwd`, 3 is somewhere else. -fn spawn_in(cwd: &str) -> Result { - let argv = format!("{SELF}\0is\0{cwd}\0"); - let args = SpawnArgs { - argv_ptr: argv.as_ptr() as u64, - argv_len: argv.len() as u64, - slot_map_ptr: 0, - slot_map_count: 0, - env_ptr: 0, - env_len: 0, - endow_ptr: 0, - endow_count: 0, - labels_ptr: 0, - labels_len: 0, - cwd_ptr: cwd.as_ptr() as u64, - cwd_len: cwd.len() as u64, - image: 0, - image_len: 0, - }; - // SAFETY: every pointer names a live local for the length beside it. - let child = unsafe { syscall::spawn(&args) }?; - let code = syscall::process_wait(child).expect("wait for a child this spawned"); - syscall::close(child); - Ok(code) -} - -/// Every refusal is asked before any is asserted, so one that is granted does -/// not hide the rest. -fn refusals() { - let mut wrong = Vec::new(); - // The same arguments succeed with a directory that exists, so each refusal - // below is the directory and nothing else. - for dir in [NAMED, LOG_DIR, HOME_DIR] { - let got = spawn_in(dir); - if got != Ok(0) { - wrong.push(format!("a spawn into {dir}: {got:?}, not started in it")); - } - } - for (cwd, want) in [ - (ABSENT, SyscallError::NotFound), - (FILE, SyscallError::NotFound), - (SELF, SyscallError::NotFound), - ("tmp/spawn-cwd/named", SyscallError::InvalidArgument), - ("", SyscallError::InvalidArgument), - ] { - let got = spawn_in(cwd); - println!("spawn-cwd: a spawn into {cwd:?}: {got:?}"); - if got != Err(want) { - wrong.push(format!("a spawn into {cwd:?}: {got:?}, not {want:?}")); - } - } - // `SYS_CHDIR` is the same judge, so it refuses the same files. - for file in [FILE, LOG_FILE, HOME_FILE, SELF] { - if std::env::set_current_dir(file).is_ok() { - wrong.push(format!("chdir into the file {file} succeeded")); - std::env::set_current_dir("/").expect("chdir to /"); - } - } - - // A file server's path the kernel cannot judge, and starts a raw spawn in: - // std judges it before it asks, on either road. - for file in [LOG_FILE, HOME_FILE] { - match Command::new(SELF).arg("pwd").current_dir(file).spawn() { - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => wrong.push(format!("std's word for {file}: {:?}, not NotFound", e.kind())), - Ok(mut child) => { - let _ = child.wait(); - wrong.push(format!("std spawned into the file {file}")); - } - } - if let Ok(mut child) = Command::new(TOYBOX).arg("pwd").current_dir(file).spawn() { - let _ = child.wait(); - wrong.push(format!("the launcher started a child in the file {file}")); - } - } - - match Command::new(SELF).arg("pwd").current_dir(ABSENT).spawn() { - Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - Err(e) => wrong.push(format!("std's word for {ABSENT}: {:?}, not NotFound", e.kind())), - Ok(mut child) => { - let _ = child.wait(); - wrong.push(format!("std spawned into {ABSENT}")); - } - } - // init hears the kernel's refusal and answers its client with one. - if let Ok(mut child) = Command::new(TOYBOX).arg("pwd").current_dir(ABSENT).spawn() { - let _ = child.wait(); - wrong.push(format!("the launcher started a child in {ABSENT}")); - } - assert!(wrong.is_empty(), "refusals that were not:\n{}", wrong.join("\n")); - println!("spawn-cwd: every refusal arrived under its own name"); -} - -/// A cwd with more beneath it than one listing may hold is still a cwd. -fn a_cwd_is_judged_in_its_depth() { - // Never made by `mkdir`: a directory the VFS carries is answered from its own - // set, and this one has to be judged by the filesystem its files are on. - std::fs::File::create(format!("{BIG}/0")).expect("/tmp is writable"); - spawns_from("/"); - spawns_from(NAMED); - // Entered while small, grown after: the process that `cd`s into a build - // directory is not asked again when the build fills it. - std::env::set_current_dir(BIG).expect("chdir into a directory this made"); - for i in 1..BIG_FILES { - std::fs::File::create(format!("{BIG}/{i}")).expect("/tmp is writable"); - } - said("own cwd over a large subtree, direct", Command::new(SELF).arg("pwd").output(), BIG); - spawns_from(BIG); - std::env::set_current_dir("/").expect("chdir to /"); - println!("spawn-cwd: {SPAWNS} spawns each from /, {NAMED} and {BIG} ({BIG_FILES} files)"); - // Removed by name: a listing of this directory is the very thing it outgrew. - for i in 0..BIG_FILES { - std::fs::remove_file(format!("{BIG}/{i}")).expect("remove a file this made"); - } -} - -/// [`SPAWNS`] direct spawns-and-waits from `cwd`, each answered. -fn spawns_from(cwd: &str) { - for _ in 0..SPAWNS { - assert_eq!(spawn_in(cwd), Ok(0), "a spawn into {cwd}"); - } -} diff --git a/tests/toyos-rust-tests/src/bin/swap_claim_astray.rs b/tests/toyos-rust-tests/src/bin/swap_claim_astray.rs deleted file mode 100644 index 9e43afd0b51..00000000000 --- a/tests/toyos-rust-tests/src/bin/swap_claim_astray.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Take the 82574 a swapped-out netd drove, aim its receive ring outside the -//! one grant this claim holds, and wait on the claim: the replacement the -//! refusal control puts in netd's place. -//! -//! **The first frame the host sends makes the part fetch a descriptor from an -//! address its domain does not map**, so the unit refuses it and the claim -//! faults. The part's interrupts stay masked, so nothing but that fault can -//! wake this program; what it then reads from the claim is the verdict. -//! -//! It exits 1 on the refusal it waits for, with no deadline, and 2 when a wait -//! on the claim ended with nothing ready: a refusal read after an unwoken wait -//! is one nobody was woken for. - -use toyos::poller::{Poller, READABLE}; -use toyos_abi::syscall::{PciId, SyscallError}; -use toyos_i219::{regs, Registers}; - -/// The 82574, QEMU's `e1000e`: netd's Intel driver's other part. -const E82574: PciId = PciId { vendor: 0x8086, device: 0x10d3 }; - -/// Where the ring is aimed, past the grant: further than a claim may ever be -/// granted in total, so nothing this claim holds is there. -const ASTRAY: u64 = 64 * 1024 * 1024; - -/// The register window, as volatile 32-bit accesses. -struct Bar(*mut u8); - -impl Registers for Bar { - fn bytes(&self) -> usize { - regs::REGISTER_BYTES - } - fn read(&self, reg: usize) -> u32 { - // SAFETY: `reg` is a register offset inside the mapped BAR, which is - // at least `REGISTER_BYTES` long and lives as long as `main`'s mapping. - unsafe { (self.0.add(reg) as *const u32).read_volatile() } - } - fn write(&self, reg: usize, value: u32) { - // SAFETY: as `read`. - unsafe { (self.0.add(reg) as *mut u32).write_volatile(value) } - } -} - -fn main() { - let dev: toyos::PciDev = - toyos::endow::pci_function(E82574).expect("swap_claim_astray: started holding no 82574"); - let info = dev.describe().expect("swap_claim_astray: the claim's description"); - let (bar, bytes) = info - .bar_bytes - .iter() - .enumerate() - .find(|(_, bytes)| **bytes >= regs::REGISTER_BYTES as u64) - .map(|(index, bytes)| (index as u32, *bytes)) - .expect("swap_claim_astray: no register window"); - let mapped = dev.map_bar(bar, bytes).expect("swap_claim_astray: the BAR"); - let regs = Bar(mapped.as_ptr()); - toyos_i219::quiesce(®s); - let grant = dev.dma_alloc(toyos_i219::GRANT_BYTES).expect("swap_claim_astray: a grant"); - let ring = grant.device_addr + ASTRAY; - regs.write(regs::RDBAL, ring as u32); - regs.write(regs::RDBAH, (ring >> 32) as u32); - regs.write(regs::RDLEN, 4096); - regs.write(regs::RDH, 0); - regs.write(regs::RDT, 255); - regs.write( - regs::RCTL, - regs::rctl::EN | regs::rctl::BAM | regs::rctl::BSIZE_2048 | regs::rctl::SECRC, - ); - println!("swap_claim_astray: holding the NIC mastering, its receive ring at {ring:#x}, outside its grant"); - - let poller = Poller::new(1); - loop { - match dev.irq() { - Ok(_) | Err(SyscallError::WouldBlock) => {} - Err(refused) => { - println!("swap_claim_astray: its claim refused the interrupt read: {refused:?}"); - std::process::exit(1); - } - } - poller.watch(&dev, READABLE, 0); - let mut woken = false; - poller.wait(1, u64::MAX, |_| woken = true); - if !woken { - println!("swap_claim_astray: its claim refused nothing: a wait with no deadline ended unwoken"); - std::process::exit(2); - } - } -} diff --git a/tests/toyos-rust-tests/src/bin/swap_claim_idle.rs b/tests/toyos-rust-tests/src/bin/swap_claim_idle.rs deleted file mode 100644 index 74ac19aa158..00000000000 --- a/tests/toyos-rust-tests/src/bin/swap_claim_idle.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Take the 82574 a swapped-out netd drove, stop it the way netd does before -//! its first grant, start it mastering, and touch nothing else: the replacement -//! a swap's DMA control puts in netd's place. -//! -//! **What it inherited is said before anything is changed**, so a reader can -//! tell whether the kernel's release reset the part (its receive unit off) or -//! handed it over still running. The control is whether the quiesce alone keeps -//! the part from writing into the previous holder's descriptors once the grant -//! starts it mastering. -//! -//! **It holds the part mastering until it is killed**: the host ends the -//! window on the frames it sent, and the boot ends under it. - -use toyos_abi::syscall::PciId; -use toyos_i219::{regs, Registers}; - -/// The 82574, QEMU's `e1000e`: netd's Intel driver's other part. -const E82574: PciId = PciId { vendor: 0x8086, device: 0x10d3 }; - -/// The register window, as volatile 32-bit accesses. -struct Bar(*mut u8); - -impl Registers for Bar { - fn bytes(&self) -> usize { - regs::REGISTER_BYTES - } - fn read(&self, reg: usize) -> u32 { - // SAFETY: `reg` is a register offset inside the mapped BAR, which is - // at least `REGISTER_BYTES` long and lives as long as `main`'s mapping. - unsafe { (self.0.add(reg) as *const u32).read_volatile() } - } - fn write(&self, reg: usize, value: u32) { - // SAFETY: as `read`. - unsafe { (self.0.add(reg) as *mut u32).write_volatile(value) } - } -} - -fn main() { - let dev: toyos::PciDev = - toyos::endow::pci_function(E82574).expect("swap_claim_idle: started holding no 82574"); - let info = dev.describe().expect("swap_claim_idle: the claim's description"); - let (bar, bytes) = info - .bar_bytes - .iter() - .enumerate() - .find(|(_, bytes)| **bytes >= regs::REGISTER_BYTES as u64) - .map(|(index, bytes)| (index as u32, *bytes)) - .expect("swap_claim_idle: no register window"); - let mapped = dev.map_bar(bar, bytes).expect("swap_claim_idle: the BAR"); - let regs = Bar(mapped.as_ptr()); - println!( - "swap_claim_idle: inherited RCTL {:#010x} TCTL {:#010x}", - regs.read(regs::RCTL), - regs.read(regs::TCTL) - ); - toyos_i219::quiesce(®s); - let _grant = dev.dma_alloc(2 * 1024 * 1024).expect("swap_claim_idle: a grant"); - println!("swap_claim_idle: holding the NIC mastering, its receive and transmit stopped"); - loop { - std::thread::park(); - } -} diff --git a/tests/toyos-rust-tests/src/bin/swap_claim_running.rs b/tests/toyos-rust-tests/src/bin/swap_claim_running.rs deleted file mode 100644 index 1150927120a..00000000000 --- a/tests/toyos-rust-tests/src/bin/swap_claim_running.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Take the 82574 a swapped-out netd drove exactly as it was left — its -//! receive unit still on and aimed at netd's rings — and start it mastering -//! with one grant of netd's own size: the replacement the residue control puts -//! in netd's place. -//! -//! **It does not stop the part.** On the T14 the I219 wrote into its previous -//! holder's buffers after netd's replacement had stopped it, because no -//! register write retracts a frame the function had already taken in; QEMU's -//! part holds no such frame, so a receive unit left on is how this machine -//! stages the same write. -//! -//! **It holds the part mastering until it is killed**: the host ends the -//! window on the frames it sent, and the boot ends under it. - -use toyos_abi::syscall::PciId; -use toyos_i219::{regs, Registers}; - -/// The 82574, QEMU's `e1000e`: netd's Intel driver's other part. -const E82574: PciId = PciId { vendor: 0x8086, device: 0x10d3 }; - -/// The register window, as volatile 32-bit accesses. -struct Bar(*mut u8); - -impl Registers for Bar { - fn bytes(&self) -> usize { - regs::REGISTER_BYTES - } - fn read(&self, reg: usize) -> u32 { - // SAFETY: `reg` is a register offset inside the mapped BAR, which is - // at least `REGISTER_BYTES` long and lives as long as `main`'s mapping. - unsafe { (self.0.add(reg) as *const u32).read_volatile() } - } - fn write(&self, reg: usize, value: u32) { - // SAFETY: as `read`. - unsafe { (self.0.add(reg) as *mut u32).write_volatile(value) } - } -} - -fn main() { - let dev: toyos::PciDev = - toyos::endow::pci_function(E82574).expect("swap_claim_running: started holding no 82574"); - let info = dev.describe().expect("swap_claim_running: the claim's description"); - let (bar, bytes) = info - .bar_bytes - .iter() - .enumerate() - .find(|(_, bytes)| **bytes >= regs::REGISTER_BYTES as u64) - .map(|(index, bytes)| (index as u32, *bytes)) - .expect("swap_claim_running: no register window"); - let mapped = dev.map_bar(bar, bytes).expect("swap_claim_running: the BAR"); - let regs = Bar(mapped.as_ptr()); - println!( - "swap_claim_running: inherited RCTL {:#010x} TCTL {:#010x}", - regs.read(regs::RCTL), - regs.read(regs::TCTL) - ); - let _grant = dev.dma_alloc(toyos_i219::GRANT_BYTES).expect("swap_claim_running: a grant"); - println!("swap_claim_running: holding the NIC mastering, its receive unit as netd left it"); - loop { - std::thread::park(); - } -} diff --git a/tests/toyos-rust-tests/src/bin/swap_flr_probe.rs b/tests/toyos-rust-tests/src/bin/swap_flr_probe.rs deleted file mode 100644 index 6ee8a81ee23..00000000000 --- a/tests/toyos-rust-tests/src/bin/swap_flr_probe.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! The replacement a swap's reset control puts in netd's place: it takes the -//! `igb` the netd it replaces held — released by an Express function level -//! reset — and says what the function answers through the register window its -//! claim maps. -//! -//! Dword 0 is the one the kernel settled the window against when it placed -//! it, so an answer of all-zeroes or all-ones there is a window the function -//! no longer decodes. -//! -//! **It holds the claim until it is killed**: the host's verdict is the line -//! it prints, and the boot ends under it. - -use toyos_abi::syscall::PciId; - -/// QEMU's `igb`, the 82576. -const IGB: PciId = PciId { vendor: 0x8086, device: 0x10c9 }; - -fn main() { - let Some(dev) = toyos::endow::pci_function::(IGB) else { - println!("swap_flr_probe: started holding no igb"); - return; - }; - let info = dev.describe().expect("swap_flr_probe: the claim's description"); - let (bar, bytes) = info - .bar_bytes - .iter() - .enumerate() - .find(|(_, bytes)| **bytes != 0) - .map(|(index, bytes)| (index as u32, *bytes)) - .expect("swap_flr_probe: a claim with no window"); - let mapped = dev.map_bar(bar, bytes).expect("swap_flr_probe: the BAR"); - // SAFETY: the mapping is at least one dword long and lives until the - // process is killed. - let dword = unsafe { (mapped.as_ptr() as *const u32).read_volatile() }; - println!("swap_flr_probe: igb BAR {bar} dword 0 answers {dword:#010x}"); - loop { - std::thread::park(); - } -} diff --git a/tests/toyos-rust-tests/src/bin/swap_probe.rs b/tests/toyos-rust-tests/src/bin/swap_probe.rs deleted file mode 100644 index cc15fff4834..00000000000 --- a/tests/toyos-rust-tests/src/bin/swap_probe.rs +++ /dev/null @@ -1,49 +0,0 @@ -//! What a program sshd runs undeclared holds of the swap port: uploaded and run -//! over ssh, so the manifest declares nothing for it and std spawns it with a -//! duplicate of sshd's namespace. -//! -//! Argv is the port's name, its endowment label and the swap message type, -//! from `toyos_swap` on the host. `netd` is asked for first, so a spawn that -//! inherited nothing at all is not read as the port withheld. Exit 0 is the -//! port out of reach; 1 is the port reached, and the line says what init -//! answered a frame that is no swap request. - -use toyos::endow::{self, EndowError, Endowments}; - -fn main() { - let args: Vec = std::env::args().collect(); - let [_, port, label, msg] = args.as_slice() else { - println!("swap_probe: asked with {args:?}, not