From 520c0d129deddf0ca76997b01c3eef8672be4dee Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 04:55:12 +0200 Subject: [PATCH 01/19] tests: the guest suite keeps what only a guest shows; the harness that served the rest goes The guest suite is cut to 22 tests: the 17 AArch64 `virt_*` rows, five panel tests and `usb_boot_stick_pulled`. Every other guest test is cut: its metal row already runs it on the T14, or its behaviour is recorded in the guest-suite track as a host, metal or type stage. - The shared boot leaves the guest suite. Every Rust test binary and the C corpus already ride `shared_metal` and `c_corpus_metal`; registration now reads the binaries off their source directory, so the guest path builds none of them. `check_not_run` moves with the corpus compile. - `METAL` rows stand on their own: `Metal` is a struct, `METAL_ONLY` and `QemuOnly` go, and the gate refuses a name twice, a row with no boot and a config that does not exist. - Deleted with the tests: 77 helper binaries, 28 case configs, 17 common modules, the `--hold` owner, the staged-image path, 32 machine profiles, 15 boot options, the https judges, `iced-counter`, `tests/cxx`, the gbae fixture and their NOTICE sections, and the src items only they read. - The redlist keeps the five rows whose tests still run (metal); the fourteen issues the deleted rows named are open again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- .github/workflows/nightly.yml | 2 - Cargo.lock | 62 - Cargo.toml | 13 +- NOTICE | 55 - ...s-the-loader-past-the-firmware-watchdog.md | 2 +- ...ts-clean-with-none-of-its-refusals-said.md | 2 +- ...usb-storage-transport-break-during-boot.md | 2 +- ...d-runs-host-tools-outside-rust-and-qemu.md | 2 +- ...nput-path-can-stop-after-a-ps2-overflow.md | 2 +- ...-loses-typed-keystrokes-under-host-load.md | 2 +- ...ds-four-packets-short-with-a-clean-exit.md | 2 +- ...hung-waiting-for-a-wake-that-never-came.md | 2 +- ...takes-when-every-cpu-is-in-a-call-on-it.md | 2 +- ...d-only-when-logd-reads-its-registration.md | 2 +- ...s-outlasts-the-jobs-five-second-spin-up.md | 2 +- ...thread-it-waits-on-may-be-queued-behind.md | 2 +- issues/kernel/desktop-window-child-freeze.md | 2 +- ...ve-up-on-one-thread-beside-the-held-one.md | 2 +- ...ndow-nmi-shortfalls-on-a-contended-host.md | 2 +- licenses/MIT-gbae.txt | 21 - licenses/MIT-iced.txt | 18 - src/bootlog.rs | 76 - src/build.rs | 136 +- src/fingerprint.rs | 124 - src/image.rs | 261 - src/lan.rs | 157 - src/lib.rs | 2 - src/licence.rs | 7 - src/metaldevices.rs | 44 - src/redlist.rs | 57 - src/sourcegate.rs | 1 - src/testargs.rs | 26 - tests/blockdcase/system.toml | 69 - tests/checks.rs | 50 +- tests/checks/metal.rs | 12 +- tests/common/blockd.rs | 732 - tests/common/clang.rs | 117 - tests/common/console.rs | 542 - tests/common/devices.rs | 95 - tests/common/faults.rs | 1209 -- tests/common/fwvars.rs | 101 - tests/common/gpt.rs | 340 - tests/common/https.rs | 342 - tests/common/inspect.rs | 386 - tests/common/iommu.rs | 2226 --- tests/common/lan.rs | 447 +- tests/common/logread.rs | 431 - tests/common/logstream.rs | 370 - tests/common/metal.rs | 38 +- tests/common/mod.rs | 23 - tests/common/origin.rs | 729 - tests/common/orphan.rs | 49 - tests/common/partclaim.rs | 799 - tests/common/pkg.rs | 414 - tests/common/power.rs | 2167 --- tests/common/qemu.rs | 2492 +-- tests/common/screen.rs | 52 - tests/common/segment.rs | 204 - tests/common/serial.rs | 51 +- tests/common/ssh.rs | 572 +- tests/common/storage.rs | 1156 -- tests/common/swap.rs | 764 +- tests/common/toybox.rs | 277 - tests/common/update.rs | 642 - tests/common/usb.rs | 3655 +---- tests/common/volumes.rs | 2646 +-- tests/common/wallclock.rs | 596 - tests/cxx/runtime.cpp | 374 - tests/cxx/runtime.expect | 27 - tests/desktopaudiocase/system.toml | 66 - tests/desktopcase/system.toml | 78 - tests/doommusiccase/system.toml | 51 - tests/e1000case/system.toml | 49 - tests/e1000leasecase/system.toml | 37 - tests/e1000talkcase/system.toml | 57 - tests/fixtures/SHA256SUMS | 4 - .../fixtures/gbae-v0.2.0-toyos-x86_64.tar.gz | Bin 604872 -> 0 bytes tests/flrswapcase/system.toml | 55 - tests/fsdclaimcase/system.toml | 36 - tests/fsdmountcase/system.toml | 34 - tests/fsdrestartcase/system.toml | 51 - tests/https-fetch-host/Cargo.lock | 1120 -- tests/https-fetch-host/Cargo.toml | 24 - tests/https-server-host/Cargo.lock | 1444 -- tests/https-server-host/Cargo.toml | 30 - tests/https-server-host/src/main.rs | 381 - tests/iced-counter/Cargo.lock | 4182 ----- tests/iced-counter/Cargo.toml | 42 - tests/iced-counter/src/bin/iced-counter.rs | 67 - tests/inspectcase/system.toml | 78 - tests/jobdeadlinecase/system.toml | 44 - tests/layoutcase/system.toml | 55 - tests/logflushcase/system.toml | 39 - tests/logkeepcase/system.toml | 42 - tests/logstreamcase/system.toml | 45 - tests/logstreame1000case/system.toml | 45 - tests/netcase/system.toml | 76 - tests/partclaimcase/system.toml | 85 - tests/pkgcase/system.toml | 80 - tests/quiescecase/system.toml | 31 - tests/quiescelastcase/system.toml | 30 - tests/quiescetwicecase/system.toml | 33 - tests/sshdcase/system.toml | 70 - tests/swapcase/system.toml | 62 - tests/test-durations | 387 - tests/toolkitcase/system.toml | 50 - tests/toyos-rust-tests/src/bin/blockd_io.rs | 1040 -- .../src/bin/compositor_client_death.rs | 256 - .../src/bin/compositor_hostile_clipboard.rs | 269 - .../src/bin/compositor_stall.rs | 210 - .../src/bin/copy_out_races_munmap.rs | 98 - tests/toyos-rust-tests/src/bin/doom_frames.rs | 17 - .../src/bin/dump_stage_load.rs | 70 - tests/toyos-rust-tests/src/bin/esp_files.rs | 188 - .../src/bin/fat_backing_revoked.rs | 136 - .../toyos-rust-tests/src/bin/fpu_isolation.rs | 507 - .../toyos-rust-tests/src/bin/fs_claim_held.rs | 55 - .../src/bin/fs_client_bound.rs | 48 - .../src/bin/fs_dirs_durable.rs | 68 - .../src/bin/fs_rename_durable.rs | 41 - tests/toyos-rust-tests/src/bin/fs_restart.rs | 156 - .../src/bin/fsync_flush_failed.rs | 26 - .../src/bin/gpu_scanout_swap.rs | 51 - .../src/bin/gpu_set_resolution.rs | 82 - .../toyos-rust-tests/src/bin/gsbase_locked.rs | 19 - .../toyos-rust-tests/src/bin/gsbase_probe.rs | 10 - .../toyos-rust-tests/src/bin/heap_ceiling.rs | 128 - tests/toyos-rust-tests/src/bin/home_absent.rs | 40 - .../src/bin/home_overwrite_zero.rs | 74 - tests/toyos-rust-tests/src/bin/https_fetch.rs | 286 - .../src/bin/i8042_keyboard.rs | 75 - tests/toyos-rust-tests/src/bin/i8042_mouse.rs | 66 - .../toyos-rust-tests/src/bin/input_absent.rs | 26 - .../toyos-rust-tests/src/bin/input_events.rs | 95 - .../src/bin/inspect_denied.rs | 102 - .../src/bin/inventory_bounds.rs | 73 - .../src/bin/ipc_hostile_peer.rs | 93 - .../src/bin/launcher_refusals.rs | 269 - .../toyos-rust-tests/src/bin/layout_paths.rs | 107 - tests/toyos-rust-tests/src/bin/locale_gate.rs | 250 - .../src/bin/log_carrier_forger.rs | 7 - tests/toyos-rust-tests/src/bin/log_flood.rs | 30 - tests/toyos-rust-tests/src/bin/log_forger.rs | 31 - tests/toyos-rust-tests/src/bin/log_hold.rs | 30 - tests/toyos-rust-tests/src/bin/log_origin.rs | 7 - .../src/bin/log_refused_stop.rs | 12 - tests/toyos-rust-tests/src/bin/netd_caps.rs | 93 - .../src/bin/netd_held_open.rs | 52 - .../src/bin/netd_hostile_peer.rs | 185 - .../src/bin/netd_listener_forgery.rs | 60 - .../src/bin/netd_lookup_let_go.rs | 105 - .../src/bin/netd_refused_accept.rs | 129 - .../src/bin/netd_refused_pipes.rs | 215 - .../src/bin/netd_slow_reader.rs | 49 - .../src/bin/netd_udp_any_address.rs | 38 - .../src/bin/netd_udp_refused.rs | 179 - .../src/bin/nmi_window_spin.rs | 76 - .../src/bin/partition_claimant.rs | 492 - .../src/bin/pkg_launch_gbae.rs | 118 - .../toyos-rust-tests/src/bin/quiesce_last.rs | 45 - .../toyos-rust-tests/src/bin/quiesce_twice.rs | 101 - .../src/bin/quiesce_writers.rs | 96 - .../src/bin/redirty_mid_flush.rs | 121 - .../src/bin/smp_hole_shootdown.rs | 29 - tests/toyos-rust-tests/src/bin/spawn_cwd.rs | 247 - .../src/bin/swap_claim_astray.rs | 87 - .../src/bin/swap_claim_idle.rs | 62 - .../src/bin/swap_claim_running.rs | 62 - tests/toyos-rust-tests/src/bin/swap_crash.rs | 7 - .../src/bin/swap_flr_probe.rs | 39 - tests/toyos-rust-tests/src/bin/swap_probe.rs | 49 - .../src/bin/test_panic_child.rs | 41 - .../src/bin/test_screen_churn.rs | 77 - .../src/bin/test_screen_graffiti.rs | 9 - .../toyos-rust-tests/src/bin/tls_dtv_race.rs | 134 - .../src/bin/userdev_residue.rs | 128 - .../toyos-rust-tests/src/bin/va_exhaustion.rs | 82 - tests/toyos-rust-tests/src/bin/window_caps.rs | 60 - .../toyos-rust-tests/src/bin/window_child.rs | 48 - tests/toyos-rust-tests/src/bin/window_drag.rs | 61 - tests/toyos-rust-tests/src/bin/window_wake.rs | 105 - tests/toyos-rust-tests/src/bin/winit_loop.rs | 331 - tests/toyos-rust-tests/src/bin/winit_pace.rs | 88 - tests/toyos.rs | 13363 +--------------- tests/updatecase/system.toml | 55 - 185 files changed, 615 insertions(+), 57199 deletions(-) delete mode 100644 licenses/MIT-gbae.txt delete mode 100644 licenses/MIT-iced.txt delete mode 100644 src/fingerprint.rs delete mode 100644 tests/blockdcase/system.toml delete mode 100644 tests/common/blockd.rs delete mode 100644 tests/common/clang.rs delete mode 100644 tests/common/console.rs delete mode 100644 tests/common/fwvars.rs delete mode 100644 tests/common/gpt.rs delete mode 100644 tests/common/https.rs delete mode 100644 tests/common/inspect.rs delete mode 100644 tests/common/iommu.rs delete mode 100644 tests/common/logread.rs delete mode 100644 tests/common/origin.rs delete mode 100644 tests/common/orphan.rs delete mode 100644 tests/common/partclaim.rs delete mode 100644 tests/common/pkg.rs delete mode 100644 tests/common/segment.rs delete mode 100644 tests/common/storage.rs delete mode 100644 tests/common/toybox.rs delete mode 100644 tests/common/update.rs delete mode 100644 tests/common/wallclock.rs delete mode 100644 tests/cxx/runtime.cpp delete mode 100644 tests/cxx/runtime.expect delete mode 100644 tests/desktopaudiocase/system.toml delete mode 100644 tests/desktopcase/system.toml delete mode 100644 tests/doommusiccase/system.toml delete mode 100644 tests/e1000case/system.toml delete mode 100644 tests/e1000leasecase/system.toml delete mode 100644 tests/e1000talkcase/system.toml delete mode 100644 tests/fixtures/SHA256SUMS delete mode 100644 tests/fixtures/gbae-v0.2.0-toyos-x86_64.tar.gz delete mode 100644 tests/flrswapcase/system.toml delete mode 100644 tests/fsdclaimcase/system.toml delete mode 100644 tests/fsdmountcase/system.toml delete mode 100644 tests/fsdrestartcase/system.toml delete mode 100644 tests/https-fetch-host/Cargo.lock delete mode 100644 tests/https-fetch-host/Cargo.toml delete mode 100644 tests/https-server-host/Cargo.lock delete mode 100644 tests/https-server-host/Cargo.toml delete mode 100644 tests/https-server-host/src/main.rs delete mode 100644 tests/iced-counter/Cargo.lock delete mode 100644 tests/iced-counter/Cargo.toml delete mode 100644 tests/iced-counter/src/bin/iced-counter.rs delete mode 100644 tests/inspectcase/system.toml delete mode 100644 tests/jobdeadlinecase/system.toml delete mode 100644 tests/layoutcase/system.toml delete mode 100644 tests/logflushcase/system.toml delete mode 100644 tests/logkeepcase/system.toml delete mode 100644 tests/logstreamcase/system.toml delete mode 100644 tests/logstreame1000case/system.toml delete mode 100644 tests/netcase/system.toml delete mode 100644 tests/partclaimcase/system.toml delete mode 100644 tests/pkgcase/system.toml delete mode 100644 tests/quiescecase/system.toml delete mode 100644 tests/quiescelastcase/system.toml delete mode 100644 tests/quiescetwicecase/system.toml delete mode 100644 tests/sshdcase/system.toml delete mode 100644 tests/swapcase/system.toml delete mode 100644 tests/toolkitcase/system.toml delete mode 100644 tests/toyos-rust-tests/src/bin/blockd_io.rs delete mode 100644 tests/toyos-rust-tests/src/bin/compositor_client_death.rs delete mode 100644 tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs delete mode 100644 tests/toyos-rust-tests/src/bin/compositor_stall.rs delete mode 100644 tests/toyos-rust-tests/src/bin/copy_out_races_munmap.rs delete mode 100644 tests/toyos-rust-tests/src/bin/doom_frames.rs delete mode 100644 tests/toyos-rust-tests/src/bin/dump_stage_load.rs delete mode 100644 tests/toyos-rust-tests/src/bin/esp_files.rs delete mode 100644 tests/toyos-rust-tests/src/bin/fat_backing_revoked.rs delete mode 100644 tests/toyos-rust-tests/src/bin/fpu_isolation.rs delete mode 100644 tests/toyos-rust-tests/src/bin/fs_claim_held.rs delete mode 100644 tests/toyos-rust-tests/src/bin/fs_client_bound.rs delete mode 100644 tests/toyos-rust-tests/src/bin/fs_dirs_durable.rs delete mode 100644 tests/toyos-rust-tests/src/bin/fs_rename_durable.rs delete mode 100644 tests/toyos-rust-tests/src/bin/fs_restart.rs delete mode 100644 tests/toyos-rust-tests/src/bin/fsync_flush_failed.rs delete mode 100644 tests/toyos-rust-tests/src/bin/gpu_scanout_swap.rs delete mode 100644 tests/toyos-rust-tests/src/bin/gpu_set_resolution.rs delete mode 100644 tests/toyos-rust-tests/src/bin/gsbase_locked.rs delete mode 100644 tests/toyos-rust-tests/src/bin/gsbase_probe.rs delete mode 100644 tests/toyos-rust-tests/src/bin/heap_ceiling.rs delete mode 100644 tests/toyos-rust-tests/src/bin/home_absent.rs delete mode 100644 tests/toyos-rust-tests/src/bin/home_overwrite_zero.rs delete mode 100644 tests/toyos-rust-tests/src/bin/https_fetch.rs delete mode 100644 tests/toyos-rust-tests/src/bin/i8042_keyboard.rs delete mode 100644 tests/toyos-rust-tests/src/bin/i8042_mouse.rs delete mode 100644 tests/toyos-rust-tests/src/bin/input_absent.rs delete mode 100644 tests/toyos-rust-tests/src/bin/input_events.rs delete mode 100644 tests/toyos-rust-tests/src/bin/inspect_denied.rs delete mode 100644 tests/toyos-rust-tests/src/bin/inventory_bounds.rs delete mode 100644 tests/toyos-rust-tests/src/bin/ipc_hostile_peer.rs delete mode 100644 tests/toyos-rust-tests/src/bin/launcher_refusals.rs delete mode 100644 tests/toyos-rust-tests/src/bin/layout_paths.rs delete mode 100644 tests/toyos-rust-tests/src/bin/locale_gate.rs delete mode 100644 tests/toyos-rust-tests/src/bin/log_carrier_forger.rs delete mode 100644 tests/toyos-rust-tests/src/bin/log_flood.rs delete mode 100644 tests/toyos-rust-tests/src/bin/log_forger.rs delete mode 100644 tests/toyos-rust-tests/src/bin/log_hold.rs delete mode 100644 tests/toyos-rust-tests/src/bin/log_origin.rs delete mode 100644 tests/toyos-rust-tests/src/bin/log_refused_stop.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_caps.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_held_open.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_hostile_peer.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_listener_forgery.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_lookup_let_go.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_refused_accept.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_refused_pipes.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_slow_reader.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_udp_any_address.rs delete mode 100644 tests/toyos-rust-tests/src/bin/netd_udp_refused.rs delete mode 100644 tests/toyos-rust-tests/src/bin/nmi_window_spin.rs delete mode 100644 tests/toyos-rust-tests/src/bin/partition_claimant.rs delete mode 100644 tests/toyos-rust-tests/src/bin/pkg_launch_gbae.rs delete mode 100644 tests/toyos-rust-tests/src/bin/quiesce_last.rs delete mode 100644 tests/toyos-rust-tests/src/bin/quiesce_twice.rs delete mode 100644 tests/toyos-rust-tests/src/bin/quiesce_writers.rs delete mode 100644 tests/toyos-rust-tests/src/bin/redirty_mid_flush.rs delete mode 100644 tests/toyos-rust-tests/src/bin/smp_hole_shootdown.rs delete mode 100644 tests/toyos-rust-tests/src/bin/spawn_cwd.rs delete mode 100644 tests/toyos-rust-tests/src/bin/swap_claim_astray.rs delete mode 100644 tests/toyos-rust-tests/src/bin/swap_claim_idle.rs delete mode 100644 tests/toyos-rust-tests/src/bin/swap_claim_running.rs delete mode 100644 tests/toyos-rust-tests/src/bin/swap_crash.rs delete mode 100644 tests/toyos-rust-tests/src/bin/swap_flr_probe.rs delete mode 100644 tests/toyos-rust-tests/src/bin/swap_probe.rs delete mode 100644 tests/toyos-rust-tests/src/bin/test_panic_child.rs delete mode 100644 tests/toyos-rust-tests/src/bin/test_screen_churn.rs delete mode 100644 tests/toyos-rust-tests/src/bin/test_screen_graffiti.rs delete mode 100644 tests/toyos-rust-tests/src/bin/tls_dtv_race.rs delete mode 100644 tests/toyos-rust-tests/src/bin/userdev_residue.rs delete mode 100644 tests/toyos-rust-tests/src/bin/va_exhaustion.rs delete mode 100644 tests/toyos-rust-tests/src/bin/window_caps.rs delete mode 100644 tests/toyos-rust-tests/src/bin/window_child.rs delete mode 100644 tests/toyos-rust-tests/src/bin/window_drag.rs delete mode 100644 tests/toyos-rust-tests/src/bin/window_wake.rs delete mode 100644 tests/toyos-rust-tests/src/bin/winit_loop.rs delete mode 100644 tests/toyos-rust-tests/src/bin/winit_pace.rs delete mode 100644 tests/updatecase/system.toml diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5b327c87940..49193d4480c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -133,8 +133,6 @@ 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- diff --git a/Cargo.lock b/Cargo.lock index f22b64e4ae1..242c040e5ef 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", @@ -1616,16 +1564,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 57ebd52d1eb..f9d7a2dfed5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -157,11 +157,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] @@ -186,12 +183,6 @@ toyos-sched = { path = "toyos-sched", features = ["check"] } # 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/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 8f6dd54b1eb..8be9aba9561 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 @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-28 --- 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 d529626606a..11fa3bbad02 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 @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: tooling opened: 2026-09-27 --- diff --git a/issues/boot-media/screen-fatal-halt-reds-on-ci-with-a-usb-storage-transport-break-during-boot.md b/issues/boot-media/screen-fatal-halt-reds-on-ci-with-a-usb-storage-transport-break-during-boot.md index 78faa7317de..af01a31f7db 100644 --- a/issues/boot-media/screen-fatal-halt-reds-on-ci-with-a-usb-storage-transport-break-during-boot.md +++ b/issues/boot-media/screen-fatal-halt-reds-on-ci-with-a-usb-storage-transport-break-during-boot.md @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-05 --- 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 9d993d77588..eb48e5c7c0f 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 @@ -20,7 +20,7 @@ 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 | +| `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 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 | | `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 | diff --git a/issues/build/the-console-input-path-can-stop-after-a-ps2-overflow.md b/issues/build/the-console-input-path-can-stop-after-a-ps2-overflow.md index bcb37ef76a9..9e49aeb3b11 100644 --- a/issues/build/the-console-input-path-can-stop-after-a-ps2-overflow.md +++ b/issues/build/the-console-input-path-can-stop-after-a-ps2-overflow.md @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-01 --- diff --git a/issues/build/the-console-loses-typed-keystrokes-under-host-load.md b/issues/build/the-console-loses-typed-keystrokes-under-host-load.md index 43a4f7fc749..60e66f7ba85 100644 --- a/issues/build/the-console-loses-typed-keystrokes-under-host-load.md +++ b/issues/build/the-console-loses-typed-keystrokes-under-host-load.md @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-29 --- 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 edc2bb5832f..1124e4a8c6f 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 @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: tooling opened: 2026-09-27 --- diff --git a/issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md b/issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md index 4e389c509b5..81016792bfa 100644 --- a/issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md +++ b/issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-28 --- 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 0bfb676062c..45d841bc5c5 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 @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-22 --- 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 0f453268cb3..f7864355907 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 @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-27 --- diff --git a/issues/kernel/a-quiesce-writers-first-pass-outlasts-the-jobs-five-second-spin-up.md b/issues/kernel/a-quiesce-writers-first-pass-outlasts-the-jobs-five-second-spin-up.md index 2da298ed258..28cd6d509b6 100644 --- a/issues/kernel/a-quiesce-writers-first-pass-outlasts-the-jobs-five-second-spin-up.md +++ b/issues/kernel/a-quiesce-writers-first-pass-outlasts-the-jobs-five-second-spin-up.md @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-25 --- diff --git a/issues/kernel/copy-meets-a-remap-holds-a-cpu-the-thread-it-waits-on-may-be-queued-behind.md b/issues/kernel/copy-meets-a-remap-holds-a-cpu-the-thread-it-waits-on-may-be-queued-behind.md index bc92f201d18..b33808437f8 100644 --- a/issues/kernel/copy-meets-a-remap-holds-a-cpu-the-thread-it-waits-on-may-be-queued-behind.md +++ b/issues/kernel/copy-meets-a-remap-holds-a-cpu-the-thread-it-waits-on-may-be-queued-behind.md @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-28 --- diff --git a/issues/kernel/desktop-window-child-freeze.md b/issues/kernel/desktop-window-child-freeze.md index e32523131a6..684bf2c5b70 100644 --- a/issues/kernel/desktop-window-child-freeze.md +++ b/issues/kernel/desktop-window-child-freeze.md @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-08-06 task: 156 diff --git a/issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md b/issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md index 323466776ad..93bca8d1144 100644 --- a/issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md +++ b/issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: defect opened: 2026-09-28 --- 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 7c8d7894ff0..9201fd83751 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 @@ -1,5 +1,5 @@ --- -status: expected-red +status: open kind: tooling opened: 2026-08-23 --- 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/bootlog.rs b/src/bootlog.rs index f3665da49ab..51fe583ae52 100644 --- a/src/bootlog.rs +++ b/src/bootlog.rs @@ -94,11 +94,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`]. @@ -133,10 +128,6 @@ pub const LOADER_LOG: &str = "loader.log"; pub const LOADER_FIRST_LINE: &str = "ToyOS Bootloader 1.0"; pub const LOADER_LAST_LINE: &str = "Loader log: the kernel handoff begins, so this file ends here"; -/// The line the loader prints once it has opened `GraphicsOutput`, which the -/// kernel's own `GOP:` line does not begin with. -pub const LOADER_GOP_LINE: &str = "GOP: mode"; - /// The head the loader writes every line about the black-box page under, and /// the line a harvested report goes under. pub const BLACKBOX_HEAD: &str = "Black box:"; @@ -233,12 +224,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> { @@ -534,66 +519,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. @@ -659,7 +588,6 @@ mod tests { ("bootloader/src/loaderlog.rs", format!("\"{CHAIN_ENDS_LINE}\"")), ("bootloader/src/loaderlog.rs", format!("\"{SEPARATOR}\"")), ("bootloader/src/main.rs", format!("\"{HUNG_WITHOUT_A_RECORD}\"")), - ("bootloader/src/loaderlog.rs", format!("\"{LOADER_GOP_LINE}\"")), ("bootloader/src/blackbox.rs", format!("\"{BLACKBOX_HEAD}\"")), ("bootloader/src/blackbox.rs", format!("\"{PREVIOUS_PANIC}\"")), ("bootloader/src/blackbox.rs", format!("\"{TAIL_IN_THE_FILE}\"")), @@ -722,10 +650,6 @@ mod tests { ("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 15d9f5386ea..48fc849a923 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1392,52 +1392,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)>) { @@ -2008,18 +1962,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>, @@ -2143,19 +2085,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 @@ -3125,45 +3057,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/logkeepcase/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/quiescecase/system.toml", - "tests/quiescelastcase/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", @@ -3293,33 +3197,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" )); } } @@ -3328,25 +3215,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/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 ced7fc6a450..39186da209b 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/metaldevices.rs b/src/metaldevices.rs index bea9ec723b4..5c159dac463 100644 --- a/src/metaldevices.rs +++ b/src/metaldevices.rs @@ -248,30 +248,6 @@ pub struct Exit { 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() -} - /// One line of a boot's own report about itself, in the order a reader wants /// them: what the devices said, then what each job measured. #[derive(Debug, Clone, PartialEq, Eq)] @@ -423,26 +399,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/redlist.rs b/src/redlist.rs index c08804b93d1..8b1c1b8a0a0 100644 --- a/src/redlist.rs +++ b/src/redlist.rs @@ -25,74 +25,17 @@ pub struct Disabled { /// Every disabled test. pub const DISABLED: &[Disabled] = &[ - Disabled { - test: "console_locale_detect", - issue: "issues/build/the-console-input-path-can-stop-after-a-ps2-overflow.md", - }, - Disabled { test: "desktop_window_child", issue: "issues/kernel/desktop-window-child-freeze.md" }, Disabled { test: "handle_basic", issue: "issues/kernel/deferred-release-outlives-its-syscall.md" }, Disabled { test: "handle_kill_policy", issue: "issues/kernel/handle-kill-policy-census-grew-one-sharedmem-on-two-nightlies.md", }, Disabled { test: "handle_transfer", issue: "issues/kernel/deferred-release-outlives-its-syscall.md" }, - Disabled { - test: "i8042_mouse", - issue: "issues/hardware/i8042-mouse-ends-four-packets-short-with-a-clean-exit.md", - }, Disabled { test: "kill_while_blocked", issue: "issues/kernel/deferred-release-outlives-its-syscall.md" }, - Disabled { - test: "log_ring_keeps_the_owners_slots", - issue: "issues/kernel/a-log-rings-owner-is-named-only-when-logd-reads-its-registration.md", - }, - Disabled { - test: "netd_refused_accept", - issue: "issues/hardware/netd-refused-accept-hung-waiting-for-a-wake-that-never-came.md", - }, - Disabled { - test: "partition_claim_departure", - issue: "issues/boot-media/partition-claim-departure-exits-clean-with-none-of-its-refusals-said.md", - }, - Disabled { - test: "quiesce_stops_the_machine", - issue: "issues/kernel/a-quiesce-writers-first-pass-outlasts-the-jobs-five-second-spin-up.md", - }, - Disabled { - test: "quiesce_wakes_on_the_last_park", - issue: "issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md", - }, - Disabled { - test: "quiesce_wakes_on_the_last_teardown", - issue: "issues/kernel/quiesce-wakes-on-the-last-park-gave-up-on-one-thread-beside-the-held-one.md", - }, - Disabled { - test: "root_chunk_refused_on_a_usb_stick", - issue: "issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md", - }, - Disabled { - test: "screen_console_scroll", - issue: "issues/build/the-console-loses-typed-keystrokes-under-host-load.md", - }, - Disabled { - test: "screen_fatal_halt", - issue: "issues/boot-media/screen-fatal-halt-reds-on-ci-with-a-usb-storage-transport-break-during-boot.md", - }, Disabled { test: "short_sleep_livelock", issue: "issues/kernel/short-sleep-livelock-stalls-on-ci-with-one-sleeper-never-returning.md", }, - Disabled { - test: "syscall_window_nmi", - issue: "issues/kernel/syscall-window-nmi-shortfalls-on-a-contended-host.md", - }, - Disabled { - test: "usb_transport_break", - issue: "issues/kernel/a-held-disk-waits-for-a-pass-no-cpu-takes-when-every-cpu-is-in-a-call-on-it.md", - }, - Disabled { - test: "user_copy_races_munmap", - issue: "issues/kernel/copy-meets-a-remap-holds-a-cpu-the-thread-it-waits-on-may-be-queued-behind.md", - }, ]; /// The row of `rows` that disables `test`, matched by the whole name. diff --git a/src/sourcegate.rs b/src/sourcegate.rs index 7ec261e4661..32885908963 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 3ed17a0aa8f..ead181fcbd1 100644 --- a/src/testargs.rs +++ b/src/testargs.rs @@ -159,9 +159,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 @@ -189,7 +186,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 { @@ -255,13 +251,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) { @@ -532,8 +521,6 @@ mod tests { 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:?}"); } @@ -616,17 +603,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/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 7012d12945f..7ba48e5a9b0 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -578,35 +578,22 @@ 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 + /// A run takes every declared test its filter matches, and a shard drops /// exactly the screen rows whose profile is not of [`toyos_build::ci::GUEST_ARCH`], /// saying which. #[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 + let (machine, screen) = select(filter, sharded); + machine .iter() - .map(|t| t.name.clone()) - .chain(machine.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 enabled = |n: &&str| redlist::disabled(redlist::DISABLED, n).is_none(); - let every: BTreeSet = - declared().chain(["shared_one"]).filter(enabled).map(String::from).collect(); + let every: BTreeSet = declared().filter(enabled).map(String::from).collect(); let foreign: BTreeSet = SCREEN_TESTS .iter() .filter(|(_, _, profile)| profile.arch() != toyos_build::ci::GUEST_ARCH) @@ -623,8 +610,7 @@ mod checks { (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"])), + (Some("usb_boot_stick"), true, names(&["usb_boot_stick_pulled"])), ]; for (filter, sharded, want) in cases { let got = taken(filter, sharded); @@ -637,7 +623,7 @@ mod checks { } } let named = |filter: Option<&str>| -> Option<(String, BTreeSet)> { - let line = arch_drop_line(&shared, filter)?; + let line = arch_drop_line(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)) @@ -651,28 +637,30 @@ mod checks { 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_")) { + if let Some((line, _)) = named(Some("usb_boot_stick")) { 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 => { @@ -768,8 +756,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/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 f81a2ed935e..00000000000 --- a/tests/common/console.rs +++ /dev/null @@ -1,542 +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: -/// `90_stdio_buffering` prints a 10,000-byte line against that 1024-byte bound -/// and is the only case in the corpus that does. -/// -/// 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, because `90_stdio_buffering`'s ten thousand `x`s hold -/// none and a per-offset search would read them ten thousand times. -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"), - // The other joiner, and the only case in the corpus that reaches it: - // `90_stdio_buffering` prints a line ten times `MAX_CONSOLE_LINE`, so - // the kernel does emit it in pieces and a daemon's line lands between - // two of them. The two halves have to come back as one line. - ("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/devices.rs b/tests/common/devices.rs index 64f91c5fe69..0bd94379424 100644 --- a/tests/common/devices.rs +++ b/tests/common/devices.rs @@ -7,13 +7,9 @@ //! one of `metaldevices::Refused`'s codes, and the shutdown took the devices //! down in order. -use std::path::Path; - use toyos_build::metaldevices; use super::metal; -use super::qemu::{self, BootOptions, QemuInstance}; -use super::serial; /// Every job the config's runner list names that measures something, in that /// order. `reboot` is the list's last job and measures nothing. @@ -24,8 +20,6 @@ pub const JOBS: &[&str] = &["usbwrite", "usbread", "fbcheck", "fbfill", "fbread" pub const CONFIG: &str = "tests/metaldevicecase"; pub const BOOT: &str = "metaldevicecase"; -const WAIT: std::time::Duration = std::time::Duration::from_secs(120); - pub fn on_metal(back: &metal::Readback) -> Result<(), String> { let mut bad = metaldevices::unmet(back.loader().text(), back.log().text()); @@ -65,95 +59,6 @@ fn measurement(job: &str, code: i32) -> Result { .map_err(|_| format!("{job}: exited {code}, which is neither a span nor a named refusal")) } -/// The QEMU arm: the plumbing, on a machine whose spans mean nothing. -/// -/// Every device here is emulated and every one of them is faster or slower than -/// the laptop by an amount nobody has measured, so **no span is judged**. What -/// is judged is that the boot ran its whole job list, that each job answered -/// with a measurement rather than a refusal, that blockd served this guest's -/// NVMe and fsd its DATA, and that the shutdown emptied a cache and halted a -/// controller before it let the reset go — which is a control for the sequence and not for its -/// timing. -pub fn metal_device_probe( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let case = super::compile::repo_root().join(CONFIG); - let mut qemu = QemuInstance::boot_with_options( - &case, - &[], - &[], - BootOptions { profile: qemu::Profile::Metal, qmp: true, ..Default::default() }, - ); - serial::Serial::boot(&qemu).must_be_clean()?; - - let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(WAIT)); - let _ = stop.reason(); - let tail = qemu.drain_serial(WAIT); - let text = format!("{}{tail}", qemu.boot_log()); - let log = serial::Serial::named("the device boot", text.as_str()); - - for job in JOBS { - let head = format!("exit: {job} pid="); - let line = text - .lines() - .rfind(|l| l.contains(&head)) - .ok_or_else(|| format!("no `{head}` record: {job} never ran, or never ended"))?; - let code: i32 = line - .split_once(" code=") - .and_then(|(_, rest)| rest.split_whitespace().next()) - .and_then(|v| v.parse().ok()) - .ok_or_else(|| format!("unreadable exit record: {line:?}"))?; - measurement(job, code)?; - } - - // **The positive control for the safety instrument**, and this machine is - // the only one that can give it. On the T14 the metal judge asserts that - // blockd drives no NVMe there (`metaldevices::NVME_UNDRIVEN`) and serves no - // partition; a judge whose needles could never appear would pass that and - // say nothing. Here blockd's row names this guest's controller, so the - // lines the T14 must never carry have to appear: blockd serves the disk's - // partitions, and fsd mounts or formats the DATA volume on one of them — - // either, because which it finds depends on whether an earlier boot in - // this lane already formatted the disk, and both of them write it. - if text.contains(metaldevices::NVME_UNDRIVEN) || !text.contains("blockd: partition ") { - return Err(format!( - "blockd served no partition of this guest's NVMe, so the needles the T14's judge \ - refuses are ones this machine never showed it can print:\n{text}" - )); - } - const OWNED: &[&str] = &[ - "fsd: mounted the DATA volume", - "fsd: block 0 designates this partition for ToyOS; formatting it", - ]; - if !OWNED.iter().any(|said| text.contains(said)) { - return Err(format!( - "this guest's DATA read as neither of {OWNED:?}, so it owns no volume there and the \ - control would be asserting the wrong thing:\n{text}" - )); - } - // And the shutdown, in the order a device needs it: the cache is emptied - // while the volume is still there, and the boot's own last word is still - // the last thing in the log. - log.must_say("usb-quiesce: disk 0 SYNCHRONIZE CACHE ok")?; - log.must_say(toyos_build::bootlog::REBOOTING)?; - let flushed = text - .find("SYNCHRONIZE CACHE") - .ok_or_else(|| "no cache flush in the shutdown".to_string())?; - let last_word = text - .rfind(toyos_build::bootlog::REBOOTING) - .ok_or_else(|| "no reset word in the log".to_string())?; - if flushed > last_word { - return Err(format!( - "the cache flush is recorded after {:?}, so on a machine whose log is a file it \ - would land after the boot's own last line and no metal verdict could read it", - toyos_build::bootlog::REBOOTING - )); - } - Ok(()) -} - /// The runner's job list is the one this file names, under the symlinks that /// give each job the name the kernel's `exit:` record carries. /// diff --git a/tests/common/faults.rs b/tests/common/faults.rs index e3805d09ae4..47fd773f00e 100644 --- a/tests/common/faults.rs +++ b/tests/common/faults.rs @@ -12,1071 +12,9 @@ //! `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(()) -} - -/// How long the guest spins. The storm arms about 190 ms after the spinner -/// starts — a million syscalls at its measured rate — and this is what covers a -/// slow arming plus the storm itself on a shard with company. -const SPIN_SECS: u32 = 10; - -/// The kernel's own summary line, printed last so a drain that ends on it has -/// every per-CPU line and the symbolized `rip` under it already. -const NMI_REPORT: &str = "syscall-window-nmi: sent="; - -/// The storm's own word, before it sends, that it holds the victim inside the -/// entry: which CPU, the `rsp` it is held at, and where the entry spins. -const HELD: &str = "syscall-window-nmi: held cpu="; - -/// The storm's word that it found nobody to hold, before it sprays. -const HELD_NOBODY: &str = "syscall-window-nmi: held nobody"; - -/// The storm's word that the hold is over: an arrival under this line is a -/// sprayed one. -const RELEASED: &str = "syscall-window-nmi: released cpu="; - -/// A held CPU's word that its entry spent the ask's whole budget and left the -/// window with nobody having released it. -const HOLD_EXPIRED: &str = "syscall-window-nmi: hold expired cpu="; - -/// The storm's word that the victim it released made no syscall before the -/// spray went out. -const NO_SYSCALL_AFTER_RELEASE: &str = "of its release, so the spray's first samples may be"; - -/// What the storm said of the hold it arranged. -struct Hold { - cpu: u64, - /// The user `rsp` the victim was held at. - rsp: u64, - /// Where the entry spins while held. - spin: std::ops::Range, -} - -/// The premise both arms rest on, read off the capture: the CPU the storm held -/// inside the entry's window and the `rsp` it held it at, which has to be the -/// user's or the hold was not the window. -fn held_in_window(capture: &str) -> Result { - let Some(line) = capture.lines().find(|l| l.contains(HELD)) else { - return Err(if capture.contains(HELD_NOBODY) { - format!( - "the storm says it held nobody, so the premise every verdict rests on was not \ - arranged and this run says nothing about vector 2's IST\n{capture}" - ) - } else { - format!("the capture has no `{HELD}` line and no `{HELD_NOBODY}` line\n{capture}") - }); - }; - let cpu = field(line, "cpu=")?; - let part = |name: &str| hex(line, name).ok_or_else(|| format!("no {name}0x… field in {line:?}")); - let (rsp, spin) = (part("rsp=")?, part("spin=")?..part("end=")?); - if rsp >= toyos_userbound::USER_TOP { - return Err(format!( - "cpu{cpu} was held at rsp={rsp:#x}, which is not a user address: a hold on a kernel \ - stack is not the window, and an NMI there finds a stack the CPU may push \ - on\n{capture}" - )); - } - Ok(Hold { cpu, rsp, spin }) -} - -/// Refuses a capture in which the kernel says a hold ended by the entry's own -/// bound: nobody released that CPU, so nothing under the line is the run the -/// storm arranges. -fn hold_expired(capture: &str) -> Result<(), String> { - match capture.lines().find(|l| l.contains(HOLD_EXPIRED)) { - Some(line) => Err(format!( - "the kernel says a hold ended by the entry's own bound and not by the storm's \ - release: `{}`\n{capture}", - line.trim(), - )), - None => Ok(()), - } -} - -/// A held CPU with neither end of its hold in the capture: what is absent, and -/// no reason for it. -fn never_released(hold: &Hold, capture: &str) -> String { - format!( - "cpu{} was held inside the entry and the capture has no `{RELEASED}` line and no \ - `{HOLD_EXPIRED}` line\n{capture}", - hold.cpu, - ) -} - -/// The `name`N field of a key=value report line, by name and not position. -fn field(line: &str, name: &str) -> Result { - line.split_whitespace() - .find_map(|w| w.strip_prefix(name)?.parse::().ok()) - .ok_or_else(|| format!("no {name}N field in {line:?}")) -} - -/// The `field`0x… value on `line`, up to sixteen hex digits. -fn hex(line: &str, field: &str) -> Option { - let rest = line.split(field).nth(1)?; - let digits: String = rest.trim_start_matches("0x").chars().take(16).collect(); - u64::from_str_radix(&digits, 16).ok() -} - -/// An NMI delivered where CPL is 0 and `rsp` is still the user's, and a machine -/// that carries on. -/// -/// **One boot, and the two negative controls are `syscall_window_nmi_controls`'s -/// two.** -/// -/// **The window.** `SYSCALL` switches no stack, so `arch::syscall`'s entry runs -/// three instructions at CPL 0 with the user's `rsp` and its exit one more -/// between `pop rsp` and `sysretq`. A frame the CPU builds there is a supervisor -/// write to a user page: SMAP refuses it, the `#PF` lands on the same stack, and -/// the machine takes a `#DF`. `arch::idt`'s IST2 row is the fix and this is what -/// says the row is load-bearing. -/// -/// **The first arrival is arranged; where the sprayed ones land is the -/// accelerator's answer, and on KVM it is the host's.** Before it sprays, the -/// storm holds the victim inside the entry — `nmi_gate::hold`'s word, which the -/// entry acknowledges and spins on at CPL 0 on the user's stack — and aims one -/// NMI at it there. That arrival is every host's, and is asserted on every -/// host: the `held cpu=… rsp=…` line the storm prints before the send, with an -/// `rsp` in the user half, and a `held` count in its report that the held CPU -/// itself keeps — a window arrival taken while its own word still had both -/// bits — whose frame stands at the held `rsp` with a `rip` inside the entry's -/// spin. The spray's landings are not. -/// -/// Under TCG, QEMU checks for a pending interrupt between translation -/// blocks and `syscall` ends one, so a pending NMI is delivered at -/// `syscall_entry+0`: the dev host reads 36 to 58 arrivals per 3,000, run after -/// run. Under KVM an NMI to a running vCPU is a host kick, a VM exit and an -/// injection at the next VM entry — and **which instruction that entry is -/// depends on where the kick's exit landed**, which is a property of the host -/// and not of the guest. Both extremes are measured on the hosted lane, on the -/// spray alone: -/// -/// - **0 of 6,000** (run 32584121311, two boots, with 2,451 and 438 of the same -/// NMIs arriving in Ring 3, so the aim was right and the injection point was -/// simply somewhere else); -/// - **64 of 64** (run 32587665835 `guest (9)`, `window=64 ring3=0 spun=16`, -/// the exit landing on the `syscall` boundary and the injection on the entry's -/// first instruction every time, so the storm ended at `ENOUGH` after 64 -/// deliveries). -/// -/// So a sprayed in-window count asserted on KVM would be asserting about the -/// host, in either direction: a floor reds the first host and a ceiling reds -/// the second. CI's guest lane is KVM only (`tests/CLAUDE.md`), and the -/// accelerator is read off the argv this boot was built from — the same -/// `-accel kvm` decision `qemu_command` made, not a re-derivation of it: -/// -/// - **under TCG** the derived count is asserted as [`SAME_ORDER`] below, with -/// the held arrival taken out of it first — on a run whose victim was running -/// when sampled (victim-located arrivals at or under its own traversals); a -/// parked victim is the declared degradation at that check, printed and not -/// judged; -/// - **under KVM** the sprayed counts are printed as the instrument's verdict, -/// and what is asserted is what every host witnesses: the held arrival, nine -/// of ten aimed NMIs delivered, at least one of them arriving somewhere only -/// the victim can be — in Ring 3 *or* in the window — no window arrival with -/// a `rip` outside the entry, and no `#DF`. -/// -/// The window itself is gated on both by `syscall_window_nmi_controls`, whose -/// `nmi-without-ist` arm double faults at `syscall_entry` on the held arrival, -/// with `cr2 = rsp - 8` at the `rsp` it was held at. `wake_storm_cost` is the -/// shape this follows: whether an instrument can read the thing is the -/// instrument's verdict, printed, and the derived assertion is made only on a -/// run that can read it. -/// -/// **The derivation, where it applies.** Every iteration of the spinner's loop -/// passes through the window exactly once and through Ring 3 exactly once, so -/// the two counts differ only by how many points an NMI can be delivered at -/// inside each: a few instructions either side under a delivery model uniform -/// over instructions, and under TCG one block boundary in the window against -/// two or three on the user side. Both readings say one traversal each within a -/// small factor, and [`SAME_ORDER`] is the bound: an order of magnitude, which -/// no reading of the delivery model reaches and a classification that has -/// stopped tracking the loop fails at once. The held arrival is not a sample of -/// the spray and is subtracted before the ratio. -/// -/// Measured, dev host, TCG, `-smp 4`, 3,000 NMIs sent: **47 window arrivals -/// against 136 in Ring 3** aimed, **36 against 122** while the storm still -/// sprayed every sibling. -/// -/// The bound is not the teeth on its own. What says the count means the window -/// is every counted arrival's own `rip`: the kernel holds each against -/// `syscall_entry`'s extent and the report's `outside` has to be zero, on both -/// accelerators. Under TCG the first *sprayed* one is also symbolized by the -/// kernel and asserted against `syscall_entry`: `dump_nmi_probe`'s rule, that a -/// probe naming the wrong instruction is worse than one naming none, read off -/// the symbol table rather than off the labels `outside` is judged by. The held -/// arrival cannot say either — it is inside the entry by arrangement. -pub fn syscall_window_nmi( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // One traversal each per iteration, so the two counts are of one order. - const SAME_ORDER: u64 = 10; - - let survived = storm(test_config, c_bins, rust_bins, &["syscall-window-nmi"], SPIN_SECS, |l| { - l.contains(NMI_REPORT) || l.contains(HOLD_EXPIRED) - })?; - if survived.contains("DOUBLE FAULT") { - return Err(format!( - "an NMI in the syscall window still took the machine down — vector 2's IST index \ - is not doing what the table says\n{survived}" - )); - } - hold_expired(&survived)?; - let Some(report) = survived.lines().find(|l| l.contains(NMI_REPORT)) else { - return Err(match held_in_window(&survived) { - Ok(hold) if !survived.contains(RELEASED) => never_released(&hold, &survived), - _ => format!("the storm never reported — is `syscall-window-nmi` on?\n{survived}"), - }); - }; - // The premise before any verdict: the storm's own word that it held the - // victim inside the entry at the user's `rsp`, and the held CPU's own count - // of the arrival it took there. - let hold = held_in_window(&survived)?; - if let Some(line) = survived.lines().find(|l| l.contains(NO_SYSCALL_AFTER_RELEASE)) { - return Err(format!( - "the storm sprayed without waiting out the hold's end, so a window arrival of this \ - run may be the released victim still inside the entry, which vouches for any \ - classifier: `{}`\n{survived}", - line.trim(), - )); - } - let (sent, seen) = (field(report, "sent=")?, field(report, "seen=")?); - let (window, ring3) = (field(report, "window=")?, field(report, "ring3=")?); - let (spun, held) = (field(report, "spun=")?, field(report, "held=")?); - let outside = field(report, "outside=")?; - let Some(released) = survived.lines().find(|l| l.contains(RELEASED)) else { - return Err(never_released(&hold, &survived)); - }; - // Printed and not judged: how much of the entry's budget a hold takes is - // the host's pace. - let (turns, budget) = (field(released, "turns=")?, field(released, "budget=")?); - eprintln!( - " [nmi-window] cpu{} held at rsp={:#x} for {turns} of the entry's {budget} turns; {sent} \ - sent, {seen} taken, {window} in the window with {held} of them the held arrival and \ - {outside} outside the entry, {ring3} in Ring 3, {spun} syscalls made under the storm", - hold.cpu, hold.rsp, - ); - // Every arrival and every accelerator: the kernel holds each `window` - // frame's `rip` against the entry's own extent, which is the only code that - // runs at CPL 0 on a user's `rsp`. - if outside != 0 { - return Err(format!( - "{outside} of {window} window arrivals had a `rip` outside `syscall_entry`: either the \ - classifier counts Ring 0 frames that are not the window, or there is a second place \ - this kernel runs at CPL 0 on a user's `rsp`\n{survived}" - )); - } - if held == 0 { - return Err(format!( - "cpu{} was held inside the entry at rsp={:#x} and took no window arrival while its \ - hold word had both bits — either the NMI aimed at it was not delivered inside the \ - hold, or a Ring 0 frame with a user `rsp` is classified as something else; either \ - way the premise is missing and nothing below is a verdict on the window\n{survived}", - hold.cpu, hold.rsp, - )); - } - if window < held { - return Err(format!( - "the report counts the held arrival in the window and {window} window arrivals in \ - all — one counter did not read the other's decision\n{survived}" - )); - } - // The held arrival's own frame, which the kernel keeps apart from the - // sprayed ones': at the `rsp` the storm read before it sent, and inside the - // spin the entry was held in. - let Some(arrival) = survived.lines().find(|l| l.contains("the held arrival had rip=")) else { - return Err(format!("the report counts a held arrival and names no frame for it\n{survived}")); - }; - let (rip, rsp) = (hex(arrival, "rip="), hex(arrival, "rsp=")); - if rsp != Some(hold.rsp) || !rip.is_some_and(|rip| hold.spin.contains(&rip)) { - return Err(format!( - "the held arrival's frame is rip={rip:#x?} rsp={rsp:#x?}, and cpu{} was held at \ - rsp={:#x} spinning in {:#x?}: the arrival counted as the held one is not the one \ - the hold arranged\n{survived}", - hold.cpu, hold.rsp, hold.spin, - )); - } - - // **A low delivery ratio is the host, not the kernel — unless the victim - // also made no progress.** A victim that an NMI *ended* takes the machine - // down with it (the `_controls` arm shows an IST-less NMI double-faults and - // halts), so a real death never reaches this report at all — the guest dies - // and the harness times it out. What lowers `seen` here instead is a loaded - // host delivering NMIs slower than the sender's own deadline: an APIC latches - // at most one pending NMI, so under load `sent` outruns `seen` with the - // machine perfectly alive (1,854 of 3,000 on a dev host running four other - // suites, run 32637… local, `spun=69071`). So this fires only when few were - // taken *and* the victim completed no syscall under the storm — the stall - // signature, which the aim check below also catches. - if seen * 10 < sent * 9 && spun == 0 { - return Err(format!( - "{sent} NMIs were sent, only {seen} taken, and the victim completed no syscall under \ - the storm — that pair is a CPU that stopped running, which is what an NMI that ends \ - one looks like from here\n{survived}" - )); - } - // **The aim is proved by any of three witnesses, and requiring only the - // first two reds a run where the victim was mid-syscall the whole storm.** A - // Ring 3 frame and a Ring 0 frame with a user `rsp` (the window) are states - // only the running spinner can be in — but so is a Ring 0 frame *inside the - // syscall it is spamming*, which the report counts as neither `window` nor - // `ring3`, and an idle sibling is in a Ring 0 frame too, so that count alone - // cannot tell them apart. `spun` disambiguates it: the victim's own syscalls - // completed under the storm, which only the running spinner produces (an - // idle CPU makes none). So the aim missed only when all three are zero. This - // was `window=0 ring3=0 spun=34 ring0=3000` on a hosted lane (run - // 32637767026, `guest (8)`): every NMI caught the spinner inside `SYS_GETPID` - // and the old `window + ring3 == 0` called a perfect aim a miss. - if window + ring3 == 0 && spun == 0 { - return Err(format!( - "not one of {seen} NMIs arrived with a Ring 3 frame or in the window, and the aimed \ - CPU completed no syscall under the storm — all three are states only the CPU \ - running the spinner produces, so the storm was aimed at a CPU that was not running \ - it and this run measured an idle loop\n{survived}" - )); - } - - if kvm_accelerated() { - // **The instrument's verdict, not the kernel's**, and on KVM the - // instrument's answer is the host's: the injection lands where the - // kick's VM exit did, so one hosted host put none of 3,000 in the window - // and another put 64 of 64 there (this function's header carries both). - // Neither number says anything about the kernel, so neither is asserted. - // - // `spun` is printed and not asserted here for the same reason: a storm - // that stops at its 64th window arrival is over in a few dozen - // deliveries, so the count measures how fast `ENOUGH` arrived — 16 - // syscalls under a 64-NMI storm is the instrument working, not a stall. - // What the victim's liveness rests on is the arrival counts above and - // the delivery ratio, which are the same on every host. - eprintln!( - " [nmi-window] KVM delivered {} of {seen} sprayed NMIs into the window and {ring3} \ - in Ring 3, with {spun} syscalls made under the storm: where this accelerator \ - injects is the host's business, so what this run gates is the held arrival and \ - that the machine took {sent} aimed NMIs with IST2 in place and went on working", - window - held, - ); - return Ok(()); - } - - // **Under TCG the arrivals themselves imply the syscalls**, whichever limit - // ended the storm: a window arrival is an NMI taken *inside* a syscall - // entry, and that syscall then returns from the handler and completes, which - // is what increments this counter. The dev host reads tens of them per - // thousand deliveries, so a zero here is a CPU that stopped running Ring 3 - // code rather than a storm that ended early. It is not asserted on KVM for - // the reason above: there a storm can be over in 64 deliveries, and the - // count then measures how fast the ceiling arrived. - if spun == 0 { - return Err(format!( - "the victim made no syscall at all while {seen} NMIs were delivered to it — it \ - stopped running Ring 3 code under the storm\n{survived}" - )); - } - - // **A starved victim voids the sampling the window verdicts rest on, and - // that is the declared degradation rather than a red.** The derivation - // samples a *running* spinner's loop, and a running victim collects fewer - // victim-located arrivals than traversals of its own (64+155 against 794 - // alone on this host); a victim the host mostly keeps parked collects - // them piled at one point — the recorded red is `window=0 ring3=77` on 18 - // traversals, twelve wide beside a second suite. What such a run still - // gated is everything above: survival, delivery, and an aim only the - // victim can witness. - if window + ring3 > spun { - eprintln!( - " [nmi-window] declared degradation: {} victim-located arrivals against {spun} \ - traversals of the victim's own loop — the arrivals piled onto a parked CPU, so \ - the window-placement verdicts are not rendered on this run; survival, delivery \ - and aim were", - window + ring3, - ); - return Ok(()); - } - - // The spray alone: the held arrival was aimed, not sampled. - let sprayed = window - held; - if sprayed == 0 { - return Err(format!( - "{sent} NMIs were sent and {seen} taken under TCG, and not one sprayed NMI landed \ - in the syscall window — this accelerator delivers at translation-block boundaries \ - and `syscall` ends one, so the spray proved nothing about the stack the CPU pushes \ - on\n{survived}" - )); - } - if sprayed * SAME_ORDER < ring3 { - return Err(format!( - "{sprayed} sprayed window arrivals against {ring3} in Ring 3. Every iteration passes \ - through both exactly once, so they are of one order; a {SAME_ORDER}x shortfall \ - says the arrivals are not being classified where they land\n{survived}" - )); - } - // What makes the count a claim about the window rather than about some - // other Ring 0 frame with a low `rsp`: the kernel symbolizes the first - // sprayed one it saw, and it has to be the entry. - let Some(rest) = survived.split("the first sprayed window arrival was here:\n").nth(1) else { - return Err(format!( - "the report named no rip for the first sprayed window arrival\n{survived}" - )); - }; - let named = rest.lines().next().unwrap_or(""); - if !named.contains("syscall_entry") { - return Err(format!( - "the first sprayed window arrival resolved to `{}`, not to the syscall entry — a \ - Ring 0 frame with a user `rsp` somewhere else is a different finding, and this test \ - is not measuring it\n{survived}", - named.trim(), - )); - } - Ok(()) -} - -/// Whether this host's guests run under KVM, read off the argv a boot is built -/// from. -/// -/// **The decision itself rather than a second reading of it**: `qemu_command` -/// puts `-accel kvm` there when `SUITE_ARCH.accel()` says so, and -/// `profile_argv` is that same builder. A CPUID probe in the guest would be a -/// second place that can be told the wrong answer, and `virtio_net_no_msix` and -/// `diskless_boot` already assert about a boot by reading its argv. -fn kvm_accelerated() -> bool { - qemu::profile_argv(&storm_options(&[])) - .windows(2) - .any(|w| w[0] == "-accel" && w[1] == "kvm") -} - -/// The two negative controls on [`syscall_window_nmi`], which is where the -/// property is asserted and this is where it is shown not to be vacuous. -/// -/// `#MC` has no control here and cannot have one: CR4.MCE is set and nothing in -/// QEMU raises a machine check. Its IST index rides the same table column NMI's -/// does, plus `arch::idt`'s compile-time assertion over that table. -pub fn syscall_window_nmi_controls( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // The first control: the same boot with vector 2's IST index taken off. - // Everything else — the handler, the gate, the storm, the hold, the spinner - // — is the same, so what the `#DF` below measures is the one byte. - // Drained past the header, not to it: `double_fault_handler` prints the - // address that started the chain, then the registers, then the backtrace - // that carries the symbol every assertion below reads, and only then this. - let unfixed = storm( - test_config, - c_bins, - rust_bins, - &["syscall-window-nmi", "nmi-without-ist"], - SPIN_SECS, - |l| l.contains("Scanning kernel stack") || l.contains(HOLD_EXPIRED), - )?; - hold_expired(&unfixed)?; - // The premise before the consequence: a control whose stimulus missed can - // only say what an absent defect says, and this one says which it was. - let hold = held_in_window(&unfixed)?; - let line_of = |what: &str| unfixed.lines().position(|l| l.contains(what)); - let Some(df_at) = line_of("DOUBLE FAULT") else { - if line_of(RELEASED).is_none() { - return Err(never_released(&hold, &unfixed)); - } - return Err(format!( - "cpu{} was held inside the entry at rsp={:#x} with no IST on vector 2 and an NMI \ - aimed at it, and the machine survived — the CPU took that NMI at CPL 0 on a user \ - page without the stack IST2 provides and nothing refused the frame, so on this \ - machine the row is not what stands between the window and a #DF\n{unfixed}", - hold.cpu, hold.rsp, - )); - }; - // The held arrival's `#DF` and not a sprayed one's: the storm says when the - // hold ended, and the death has to be above that line or the victim was - // already out of the hold when it died. - if line_of(RELEASED).is_some_and(|released| released < df_at) { - return Err(format!( - "the storm released cpu{} before the #DF: the NMI aimed at the hold did not take the \ - machine down, and the one that did was sprayed at a CPU nobody held\n{unfixed}", - hold.cpu, - )); - } - let df = unfixed.lines().nth(df_at).unwrap_or_default(); - eprintln!(" [nmi-window] without IST2: {}", df.trim()); - let df_cpu = df - .split("on CPU ") - .nth(1) - .and_then(|rest| rest.split_whitespace().next()?.parse::().ok()); - if df_cpu != Some(hold.cpu) { - return Err(format!( - "the double fault was on CPU {df_cpu:?}, not on cpu{} the storm held — a #DF on any \ - other CPU is a different death\n{unfixed}", - hold.cpu, - )); - } - if !unfixed.contains("syscall_entry") { - return Err(format!( - "the control double faulted somewhere other than the syscall entry\n{unfixed}" - )); - } - // **The exact signature, and the reason this is a control and not a - // coincidence**: the `#DF` stands inside the spin the victim was held in, - // at the `rsp` it was held at, and the address the CPU faulted on is the - // first qword of the frame it was trying to push there, one below it. A #DF - // for any other reason does not put `cr2` there, one on any other stack is - // not the held one, and a sprayed arrival's is at the entry's first - // instruction and not in the spin. - let report: Vec<&str> = unfixed.lines().skip(df_at).collect(); - let cr2 = report.iter().find_map(|l| hex(l, "cr2=")); - let rip = report.iter().find_map(|l| hex(l, "rip=")); - let rsp = report.iter().find_map(|l| hex(l, "rsp=")); - match (cr2, rip, rsp) { - (Some(cr2), Some(rip), Some(rsp)) - if rsp == hold.rsp && cr2 == rsp.wrapping_sub(8) && hold.spin.contains(&rip) => - { - eprintln!( - " [nmi-window] without IST2: the #DF stands at the held rsp={rsp:#x} with \ - rip={rip:#x} inside the spin, and cr2={cr2:#x} is rsp-8, the frame's first qword" - ); - } - (cr2, rip, rsp) => { - return Err(format!( - "the control's #DF reports cr2={cr2:#x?} rip={rip:#x?} rsp={rsp:#x?}, with the \ - victim held at rsp={:#x} spinning in {:#x?}; the fault this stages is the \ - frame's own first qword at the held rsp-8 from inside that spin, so this is a \ - different death\n{unfixed}", - hold.rsp, hold.spin, - )); - } - } - - // The second control: an NMI handler that returns early through `iretq` - // un-masks NMIs while still standing on IST2, which is the one way a second - // NMI can enter on that stack. The check has to fire and say so. - let nested = storm( - test_config, - c_bins, - rust_bins, - &["syscall-window-nmi", "nmi-nested"], - SPIN_SECS, - |l| l.contains("NESTED NMI"), - )?; - let Some(loud) = nested.lines().find(|l| l.contains("NESTED NMI")) else { - return Err(format!( - "a second NMI entered on IST2 and the machine said nothing: the outer handler's \ - frame was overwritten silently, which is the failure this check exists for\n{nested}" - )); - }; - eprintln!(" [nmi-window] nested: {}", loud.trim()); - Ok(()) -} - -/// One storm boot: the spinner in Ring 3, the kernel's NMIs at it, drained until -/// `done` or the ceiling. -/// -/// The ceiling is a ceiling and not the run — every arm here ends either with -/// the kernel's report or with a halted machine, and a halted machine neither -/// exits QEMU nor disconnects the drain. -/// One declaration of the machine every arm boots, so that the argv -/// [`kvm_accelerated`] reads is the argv the boot is built from. -fn storm_options(params: &'static [&'static str]) -> BootOptions { - BootOptions { - kernel_params: params, - // `double_fault_stack`'s profile and for its reason: on Metal the 16550 - // *is* the console, so `serial::panic_raw`'s bytes and the ordinary log - // stream arrive on one channel and one reader sees both. 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, - // Four, so that the scheduler has somewhere to put the spinner that is - // not the CPU whose idle loop does the storming. - smp: 4, - ..Default::default() - } -} - -fn storm( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - params: &'static [&'static str], - secs: u32, - done: impl Fn(&str) -> bool, -) -> Result { - let mut qemu = - QemuInstance::boot_with_options(test_config, c_bins, rust_bins, storm_options(params)); - writeln!(qemu.stdin_mut(), "run test_rs_nmi_window_spin {secs}").expect("write to QEMU stdin"); - qemu.flush_stdin(); - Ok(qemu.drain_until(Duration::from_secs(u64::from(secs) + 20), |line| done(line))) -} - -/// `[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. /// @@ -1143,150 +81,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 deleted file mode 100644 index 66293f205e5..00000000000 --- a/tests/common/iommu.rs +++ /dev/null @@ -1,2226 +0,0 @@ -//! 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. - -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, -} - -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) -} - -/// 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 -} - -/// Whether this machine's virtio functions are behind the unit at all. -/// -/// QEMU keeps a virtio function on `&address_space_memory` — the unit bypassed, -/// whatever the tables say — unless it is created with `iommu_platform=on` -/// (`hw/virtio/virtio-bus.c:86-99`, `hw/virtio/virtio-pci.c:1400-1405` at -/// v11.1.1), and under identity mapping the two are indistinguishable. So the -/// argv says which functions were created behind a unit and the console says -/// which negotiated `VIRTIO_F_ACCESS_PLATFORM`. On [`Profile::HeadlessNoIommu`] -/// 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> { - 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(); - let options = BootOptions { profile, ..Default::default() }; - - let argv = qemu::profile_argv(&options); - let created: Vec<&str> = argv - .windows(2) - .filter(|w| w[0] == "-device") - .map(|w| w[1].as_str()) - .filter(|d| d.starts_with("virtio-") && d.contains("-pci")) - .collect(); - if created.is_empty() { - return Err(format!("{name}: this machine creates no virtio function at all")); - } - // Ahead of everything it decodes, or a function created before it keeps - // the bypassing address space and `iommu_platform=on` changes nothing - // (`hw/virtio/virtio-bus.c:97`). - unit_is_first(&argv, name)?; - for device in &created { - if device.contains("iommu_platform=on") != behind_unit { - return Err(format!( - "{name}: the machine has a unit = {behind_unit} and QEMU is given {device}, \ - where iommu_platform=on is owed = {behind_unit}. A virtio function without \ - it keeps the address space the unit does not decode" - )); - } - } - - // `netcase`: the NIC's driver is a process, and this is the one config - // that runs it. - let qemu = QemuInstance::boot_with_options(&netcase(), &[], &[], options); - let log = Serial::boot(&qemu); - log.must_be_clean()?; - log.must_say("Boot: complete")?; - log.must_say("init: started netd")?; - - // **Whether the NIC's function is handed to a process at all is the - // machine's answer, not a choice.** A process driving a device writes - // addresses into descriptors, so the substrate refuses a claim on a - // function this machine cannot give an address space of its own — and - // on a machine with no unit that is every function. So the arm with a - // unit has three negotiators, one of them across the boundary, and the - // arm without one has two and a refusal. - let expected = if behind_unit { - // And the claim netd was given is bounded to its own function's - // configuration space, which is what makes its capability walk — - // an index by numbers the *device* wrote — safe to run at all. - // netd asks the kernel for a read past the end, one straddling it - // and one misaligned, and refuses to drive a claim that answers any - // of them. - log.must_say( - "netd: this claim answers 4096 bytes of configuration space and refuses every \ - access outside them", - )?; - // The two things a hand-over spends, on the same function and the - // 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())?; - created.len() - } else { - no_unit_is_no_claim(&log)?; - created.len() - 1 - }; - - let mut negotiated = Vec::new(); - for line in log.text().lines() { - let Some(rest) = line.split("VirtIO: PCI ").nth(1) else { continue }; - let Some((who, _)) = rest.split_once(' ') else { - return Err(format!("{name}: unreadable feature line: {line:?}")); - }; - let fields = unit_fields(line); - let Some(accepted) = fields.get("access_platform") else { continue }; - negotiated.push((who.to_string(), accepted == "y")); - } - if negotiated.len() != expected { - return Err(format!( - "{name}: QEMU created {} virtio function(s), {expected} of them for a driver \ - this machine can bring up, and the guest negotiated features with {} — \ - {negotiated:?} against {created:?}", - created.len(), - negotiated.len() - )); - } - let enumerated = enumerated_functions(&log); - for (who, accepted) in &negotiated { - if *accepted != behind_unit { - return Err(format!( - "{name}: {who} negotiated VIRTIO_F_ACCESS_PLATFORM = {accepted} where \ - {behind_unit} is owed — the unit exists = {behind_unit}" - )); - } - if !enumerated.contains(who) { - return Err(format!( - "{name}: {who} negotiated features and the PCI walk enumerated \ - {enumerated:?} — the driver is naming a function this machine does not have" - )); - } - } - let sound = class_function(&log, "0401") - .ok_or_else(|| format!("{name}: this machine enumerated no audio function"))?; - if !negotiated.iter().any(|(who, _)| *who == sound) { - return Err(format!( - "{name}: the audio function {sound} negotiated nothing, so whether it is behind \ - the unit was never asked: {negotiated:?}" - )); - } - eprintln!( - " [iommu] {name}: {} virtio function(s) behind a unit = {behind_unit}, the audio \ - function {sound} among them{}", - negotiated.len(), - if behind_unit { "" } else { "; the NIC's claim refused for want of a domain" } - ); - } - declining_is_not_free(test_config, c_bins, rust_bins) -} - -/// The slot QEMU's `-device` order puts `tests/netcase`'s NVMe controller on, -/// the one its blockd row claims. -const NVME_AT: &str = "00:02.0"; - -/// **A machine with no unit hands no function to a process**, and says so -/// three times over. -/// -/// The ordering ruling this whole stage stands on -/// (`issues/kernel/every-driver-is-still-in-the-kernel.md`) is that moving a -/// driver out without translation costs security: a descriptor holding a -/// physical address is an arbitrary read and write over all of memory. So the -/// kernel refuses the claim by name, init says which device it could not mint, -/// and netd exits rather than driving anything — and the machine finishes -/// booting, which is the half a refusal that panicked would fail. The NVMe -/// controller is refused the same, and DATA with it by name: a disk that is -/// 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: it speaks after the ready marker this - // capture ends at. - super::faults::refused_claim(log, super::https::VIRTIO.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: NOT SERVING — pci:1b36:0010 is on this machine and the kernel refused this service its claim", - )?; - log.must_say( - "fsd: the block service would not list its partitions (Refused(ClaimRefused)); DATA is absent this boot", - )?; - log.must_not_say(super::storage::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. - log.must_not_say("handed over on slot")?; - 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> { - let qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::Headless, - kernel_params: &["virtio-no-access-platform"], - ..Default::default() - }, - ); - let log = Serial::boot(&qemu); - log.must_be_clean()?; - log.must_say("Boot: complete")?; - - let refused: Vec<&str> = log - .text() - .lines() - .filter(|l| l.contains("refused the feature set")) - .collect(); - if refused.is_empty() { - return Err(format!( - "the actuator withheld VIRTIO_F_ACCESS_PLATFORM from every virtio device but the \ - console and none was refused for it. A device the host offered it on and the guest \ - declined has to lose FEATURES_OK, and this machine went on as though the \ - negotiation were free\n{}", - log.text() - )); - } - // The console kept the bit, so this is not simply a machine with no virtio. - log.must_say("access_platform=y")?; - eprintln!(" [iommu] declined: {}", refused.join("\n [iommu] declined: ")); - 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. -/// -/// 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(()) -} - -/// 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(()) -} - -/// 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, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -enum Driver { - Kernel, - Netd, -} - -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(), - } - } -} - -/// 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. -/// -/// 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:?}" - )); - } - 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() - }; - 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 { - return Err(format!( - "the unit blocked {aimed} and its COMMAND is {command:#06x}, so it still masters the \ - bus and can fault again" - )); - } - 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) -} - -/// 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)) - .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} ["); - 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 }) -} - -/// The one function `pci::enumerate` printed with this class, or none. -/// -/// `None` rather than a first match over several: two controllers of one class -/// would make "the one the actuator skipped" ambiguous, and a gate that picked -/// either would be asserting against a guess. -fn class_function(log: &Serial, class: &str) -> Option { - let mut found: Option = None; - for line in log.text().lines() { - let Some((bdf, tail)) = line.split("PCI ").nth(1).and_then(|r| r.split_once(' ')) else { - continue; - }; - if tail.starts_with(&format!("[{class}]")) { - if found.is_some() { - return None; - } - found = Some(bdf.to_string()); - } - } - 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/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 ba10d353240..00000000000 --- a/tests/common/logread.rs +++ /dev/null @@ -1,431 +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 GATE: &str = "log-gate"; - -/// The same gate with its own producer thread storming the log beside it. -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. The nest and storm lines -/// already share `read=` and `dropped=`, and every gate here reads exactly one -/// of the two. -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) -} - -/// The nested-`emit` gate: an interrupt that logs, inside another `emit`, on one CPU. -/// -/// **The one case loom cannot express and the host cannot stage.** The -/// stimulus is a self-IPI sent from inside a record's own body copy, inside -/// `SYS_LOG_READ` with `IF` opened for it — where `emit`'s IF-off bracket is the -/// only thing holding the interrupt off. The handler emits exactly one shard generation of -/// patterned records; the outer record is then dropped by the ring's own -/// drop-oldest policy, which is what makes "the burst laps the shard" a -/// statement with an arithmetic behind it. -/// -/// What is asserted is the conservation ledger over a workload of that shape: every -/// sequence number read or counted lost, every burst record's text regenerated -/// byte for byte from the two numbers it declares, and the burst's own `done` -/// read — so a run in which nothing was injected cannot pass quietly. -/// -/// **`--smp 1`, and that is the test's own claim.** Nesting is a property of -/// one CPU: a second CPU adds records to the merge and takes nothing away from -/// what this asks, while at one the interrupted writer and its interrupting -/// handler are provably the same CPU. -pub fn log_nested_emit( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { smp: 1, kernel_params: &["log-nested-emit"], ..Default::default() }; - let report = storm(test_config, c_bins, rust_bins, GATE, options)?; - let declared = report.get("declared")?; - let read = report.get("read")?; - if read == 0 { - return Err(format!("the burst was declared and none of it read\n{}", report.stdout)); - } - eprintln!( - " [log] nested: burst declared={declared} read={read} dropped={}", - report.get("dropped")? - ); - Ok(()) -} - -/// The reserve bracket at the window it names first: an interrupt that logs, -/// landing between a record's shard-pointer read and its unlocked `xadd`. -/// -/// **The property is that a shard has one order and not two.** `emit` reads the -/// clock and takes its sequence number inside one IF-off bracket, and every -/// reader in the tree rests on the two being the same order — `read.rs`'s -/// `Descent::advance` stops a shard's descent on the first record older than the -/// window it was asked for, which is only sound while a lower sequence number -/// cannot carry a later timestamp. `log-nested-reserve` puts an interrupt that -/// logs into exactly that window: with the bracket the IPI is pending until the -/// guard drops and the handler's whole burst is reserved *after* the record it -/// interrupted, and without it the burst is reserved *before*. -/// -/// **`--smp 8`, and no storm beside it.** Eight shards is where the merge across -/// shards has to keep each shard's own order while interleaving eight of them; -/// a storm on the injected CPU would lap the interrupted record before the -/// reader reached it, which is the one record the verdict is about. -pub fn log_reserve_window( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let options = BootOptions { smp: 8, kernel_params: &["log-nested-reserve"], ..Default::default() }; - let report = storm(test_config, c_bins, rust_bins, GATE, options)?; - let declared = report.get("declared")?; - let read = report.get("read")?; - let dropped = report.get("dropped")?; - let shards = report.get("shards")?; - if shards != 8 { - return Err(format!( - "--smp 8 answered {shards} shard(s); the cursor's shard count is the machine's CPU \ - count\n{}", - report.stdout - )); - } - if read == 0 { - return Err(format!( - "the reservation-window burst was declared and none of it read, so nothing was \ - injected into anything\n{}", - report.stdout - )); - } - // **The derivation, and it is exact rather than a bound.** With the bracket - // the IPI is pending across the whole publication, so the interrupted - // producer's own record takes `S` and the handler's burst takes - // `S+1 ..= S+BURST` after it, with `lognest done` at `S+BURST+1`. `head` is - // then `S+BURST+2` and `oldest_readable` is `head - BURST`, which is `S+2` — - // so the reader can never answer for the outer record or for the burst's - // first, and can answer for every one of the other `BURST-1`. Measured - // `read=511 dropped=1` in eight of eight boots on the dev host, 2026-08-22. - if declared != BURST || read != BURST - 1 || dropped != 1 { - return Err(format!( - "the burst declared {declared} record(s), this reader took {read} and lost \ - {dropped}: one shard generation is {BURST}, and the ring's own drop-oldest policy \ - puts exactly the burst's first record below `oldest_readable` and nothing else\n{}", - report.stdout - )); - } - eprintln!( - " [log] reserve window: burst declared={declared} read={read} dropped={dropped} \ - shards={shards}" - ); - Ok(()) -} - -/// `kernel/src/log/shard.rs`'s `SHARD_RECORDS`, which is how many records -/// `log::nested`'s handler emits: exactly one shard generation. -const BURST: u64 = 512; - -/// The negative control on [`log_reserve_window`], and on `arch::IrqGuard` -/// itself: the same boot with the reserve bracket removed. -/// -/// **The one thing that can make the log's correctness claim fail on purpose.** -/// `log-unbracketed-reserve` leaves the guard constructed and dropped exactly as -/// it is and masks nothing, so the self-IPI is delivered where it was sent — -/// inside the reservation window — and the handler's `SHARD_RECORDS` records -/// take the sequence numbers below the one the interrupted producer goes on to -/// take, while carrying timestamps above all of its. The gate must then refuse -/// the shard, by name, and the assertion here is that refusal and not merely a -/// non-zero exit: a boot that failed for any other reason has not read this -/// actuator. -/// -/// **The failure is derived, not sampled.** The burst is exactly [`BURST`] -/// records reserved back to back on one shard, so the interrupted record's own -/// number is exactly [`BURST`] above the burst's first while its `at_ns` was -/// stamped before any of them; the reader walks a shard in sequence order, so it -/// meets the inversion at that record on its first pass over the shard, on every -/// boot. Measured on the dev host 2026-08-22, eight of eight: the refusal names -/// `seq 517` in six boots, 518 in one and 665 in one — 517 is 5 + 512, cpu7's -/// shard having held four boot records before the injection. -pub fn log_reserve_window_negative( - 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 { - smp: 8, - kernel_params: &["log-nested-reserve", "log-unbracketed-reserve"], - ..Default::default() - }, - ); - let result = qemu.run_test(GATE, CEILING); - if let Some(err) = &result.error { - return Err(format!( - "the unbracketed boot never reported: {err}\nstdout:\n{}\nserial tail:\n{}", - result.stdout, - tail(&result.serial) - )); - } - if result.exit_code == Some(0) || result.stdout.contains("log-gate: OK") { - return Err(format!( - "the bracket was removed and the log gate passed anyway ({:?}), so the guard's `cli` \ - is still measured by nothing\n{}", - result.exit_code, result.stdout - )); - } - let refusal = result - .stdout - .lines() - .find(|l| l.contains(INVERSION)) - .ok_or_else(|| { - format!( - "the unbracketed boot failed for some other reason than the one this control \ - stages — no line said `{INVERSION}`\n{}", - result.stdout - ) - })?; - eprintln!(" [log] unbracketed: {}", refusal.trim()); - Ok(()) -} - -/// The clause `userland/test-runner/src/log_gate.rs` refuses a descending -/// `at_ns` with. Two copies of one sentence, and this file is the one that -/// would notice if the other changed. -const INVERSION: &str = "within a shard the sequence order is the timestamp order"; - -/// A pending poll on the machine's log is not something a handle closing -/// can cancel. -/// -/// **The close-cancels-a-foreign-poll defect, gated.** `object::ops::close` handed every source the -/// closing object named to `io_uring::cancel_by_source`, which cancels across every -/// ring in the machine — right for a pipe whose other end has really gone, and -/// wrong for a stream that outlives every handle. Every `SysCap` maps to -/// `Source::Log`, so any process closing any capability posted `-NotFound` into -/// every pending log poll there was. It was latent while nothing parked on one -/// and live from the moment `/system/bin/logd`'s whole loop is read-then-park. -/// -/// The verdict is the guest's and it has two halves: closing a second handle to -/// the same capability completes nothing, and a record afterwards still -/// completes the poll — so what the close did not take was a live arming and not -/// an absent one. The immediate half is retried against a record committing in -/// the same microseconds, which is distinguishable because an honest completion -/// leaves the cursor owing records. -pub fn log_poll_outlives_a_close( - 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::default()); - let result = qemu.run_test("log-close", CEILING); - if let Some(err) = &result.error { - return Err(format!("{err}\nstdout:\n{}", result.stdout)); - } - if result.exit_code != Some(0) || !result.stdout.contains("log-close: OK") { - return Err(format!( - "the close probe exited {:?}\n{}", - result.exit_code, result.stdout - )); - } - let survived = result - .stdout - .lines() - .find(|l| l.contains("log-close: survived=")) - .ok_or_else(|| format!("the guest never said what it saw\n{}", result.stdout))?; - eprintln!(" [log] {}", survived.trim()); - Ok(()) -} - -/// 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 e991aab264d..bb6976aee24 100644 --- a/tests/common/logstream.rs +++ b/tests/common/logstream.rs @@ -9,141 +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, TcpStream}; -use std::path::PathBuf; -use std::time::{Duration, Instant}; - -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; -/// its console a file where `console_file` says ([`BootOptions::console_file`]). -fn boot( - bench: Bench, - staged: &Staged, - port: u16, - console_file: bool, - 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), - console_file, - ..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. /// @@ -175,238 +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, false, 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(()) -} - -/// `logd`'s readers on the network at once (`serve.rs`'s `MAX_NETWORK_READERS`). -const NETWORK_READERS: usize = 8; - -/// A liveness guard on the flood reaching a reader: five megabytes through a -/// TCG guest's netd, as long as the flood job itself is given. -const FLOOD_CEILING: Duration = Duration::from_secs(300); - -/// What `logd` says as it lets a reader go that took no bytes it was owed. -const LET_GO: &str = "logd: letting "; - -/// **A reader that stops reading costs nobody else anything, and its slot is -/// not kept.** Every network slot `logd` has is taken by a connection that -/// never reads, while a program floods its output past every buffer between -/// them, until `logd` has let each stalled reader go; a reader that connects -/// after that is handed the whole boot — every line the file took, to the -/// kernel's record of each flood's end. -pub fn stalled_reader( - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let bench = VIRTIO; - let staged = stage(bench.config, "logstream-stalled", c_bins, rust_bins)?; - let port = qemu::free_host_port(); - // The flood puts a mebibyte of program lines on the console ahead of the - // runner's end marker, which a stdio console under host load drops. - let (mut guest, mut console) = boot(bench, &staged, port, true, c_bins, rust_bins)?; - - let stalled = (0..NETWORK_READERS) - .map(|_| never_read(port)) - .collect::, _>>() - .map_err(|e| format!("connect the readers that will not read: {e}"))?; - let from = console.len(); - let seen = |console: &str| console[from.min(console.len())..].matches(LET_GO).count(); - // Flooded until every stalled reader is let go, not by an amount: a - // reader is owed only what `logd` took of the flood, which is `logd`'s - // pace and not this test's, so a fixed flood outruns every buffer between - // them only on a host fast enough. When `logd` lets each one go is its own - // clock's business and no verdict here: the ceiling is the harness's, and - // a `logd` that never lets a reader go is a hang it reds. - let deadline = Instant::now() + guest.budget(FLOOD_CEILING); - let mut floods = 0usize; - while seen(&console) < NETWORK_READERS { - let left = deadline.saturating_duration_since(Instant::now()); - if left.is_zero() { - break; - } - let flood = guest.run_test(super::origin::FLOODER, left); - console.push_str(&flood.before); - console.push_str(&flood.serial); - if flood.exit_code != Some(0) { - return Err(format!( - "flood {} exited {:?} with {} of the {NETWORK_READERS} stalled readers let go", - floods + 1, - flood.exit_code, - seen(&console) - )); - } - floods += 1; - } - let let_go = seen(&console); - if let_go < NETWORK_READERS { - let said = match shut_down(guest, &mut console, &staged) { - Ok(file) => file.iter().filter(|l| l.contains("logd: ")).cloned().collect::(), - Err(why) => format!("none read: {}", why.lines().next().unwrap_or("")), - }; - return Err(format!( - "{} flooding until logd let the readers that stopped reading go: {let_go} of \ - {NETWORK_READERS} after {floods} flood(s); /log's logd lines:\n{said}", - qemu::STALLED - )); - } - let second = reader(port, "logstream-stalled-second.txt")?; - // The kernel's word and not the flood's last line, which its ring may - // have had no room for. - let ended = format!("exit: {} pid=", super::origin::FLOODER); - let every_end = |lines: &[String]| (lines.iter().filter(|l| l.contains(&ended)).count() >= floods).then_some(()); - if second.wait_until(FLOOD_CEILING, every_end).is_none() { - return Err(format!( - "a reader that connected after the {floods} flood(s), once every stalled reader was let \ - go, did not receive the kernel's record of each one's end: {} line(s)", - second.lines().len() - )); - } - let file = shut_down(guest, &mut console, &staged)?; - drop(stalled); - if !second.wait_ended(CEILING) { - return Err("the reader's connection had not ended once the guest was down".to_string()); - } - let received = second.lines(); - is_prefix_of(&received, &file)?; - let flooded = received.iter().filter(|l| l.contains("} flood ")).count(); - let let_go = file.iter().filter(|l| l.contains(LET_GO)).count(); - eprintln!( - " [stream] {let_go} reader(s) that never read were let go over {floods} flood(s); a \ - reader after them got {} line(s), {flooded} of them the floods', each the line /log holds", - received.len() - ); - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} - -/// The receive buffer a reader that never reads is given: set before the -/// connect, so this host's autotuning does not grow it, and the window it -/// advertises closes once this much has arrived. -const NARROW_WINDOW: libc::c_int = 16 * 1024; - -/// A connection to this host's `port`, admitted — its first line read — and -/// never read again, with a receive buffer of [`NARROW_WINDOW`]: the peer a -/// zero window makes of it. -fn never_read(port: u16) -> Result { - use std::os::fd::FromRawFd; - let failed = |what: &str| format!("{what}: {}", std::io::Error::last_os_error()); - // SAFETY: a fresh descriptor, owned by the `TcpStream` made of it at once - // so every path below closes it. - let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_STREAM, 0) }; - if fd < 0 { - return Err(failed("socket")); - } - // SAFETY: `fd` is the descriptor just made, and nothing else owns it. - let stream = unsafe { TcpStream::from_raw_fd(fd) }; - let size = NARROW_WINDOW; - // SAFETY: `size` outlives the call, and the length is its own. - let set = unsafe { - libc::setsockopt( - fd, - libc::SOL_SOCKET, - libc::SO_RCVBUF, - (&size as *const libc::c_int).cast(), - std::mem::size_of::() as libc::socklen_t, - ) - }; - if set != 0 { - return Err(failed("SO_RCVBUF")); - } - // SAFETY: all-zero is a valid `sockaddr_in`, whose fields are integers. - let mut addr: libc::sockaddr_in = unsafe { std::mem::zeroed() }; - #[cfg(target_os = "macos")] - { - addr.sin_len = std::mem::size_of::() as u8; - } - addr.sin_family = libc::AF_INET as libc::sa_family_t; - addr.sin_port = port.to_be(); - addr.sin_addr = libc::in_addr { s_addr: u32::from(Ipv4Addr::LOCALHOST).to_be() }; - // SAFETY: `addr` is a whole `sockaddr_in` and the length says so. - let connected = unsafe { - libc::connect( - fd, - (&addr as *const libc::sockaddr_in).cast(), - std::mem::size_of::() as libc::socklen_t, - ) - }; - if connected != 0 { - return Err(failed("connect")); - } - // Admitted once it carries a line: `logd` hands every reader the boot's - // first line at once. One byte at a time, so nothing past it is taken. - use std::io::Read; - let mut stream = stream; - stream.set_read_timeout(Some(CEILING)).map_err(|e| format!("a read bound: {e}"))?; - let mut byte = [0u8; 1]; - loop { - match stream.read(&mut byte) { - Ok(1) if byte[0] == b'\n' => break, - Ok(1) => {} - other => return Err(format!("a reader that will not read was never admitted: {other:?}")), - } - } - stream.set_read_timeout(None).map_err(|e| format!("a read bound: {e}"))?; - Ok(stream) -} diff --git a/tests/common/metal.rs b/tests/common/metal.rs index a633aad5206..8552281cce2 100644 --- a/tests/common/metal.rs +++ b/tests/common/metal.rs @@ -166,17 +166,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. @@ -605,7 +599,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, @@ -925,26 +919,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"); @@ -1156,7 +1136,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 8158bc6d87e..7209b083885 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1,43 +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, and its two judges. 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 toybox; -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 fcc8cd09531..00000000000 --- a/tests/common/origin.rs +++ /dev/null @@ -1,729 +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(()) -} - -/// **A child's flood leaves its parent's slots.** `tests/logkeepcase` has -/// `logd` read nothing of test-runner's ring while its one job floods it, so -/// the ring fills and stays full; test-runner's own end-of-job line comes -/// after that, and only the slots its ring keeps for its owner can take it. -/// The boot ends itself, `logd` reads the ring again at the stop, and `/log` -/// carries that line. -pub fn keeps_the_owners_slots(rust_bins: &[(String, Vec)]) -> Result<(), String> { - const ENDED: &str = "===TEST_END test_rs_log_flood exit=0==="; - let config = "tests/logkeepcase"; - let bins: Vec<(String, Vec)> = - rust_bins.iter().filter(|(name, _)| name == "log_flood").cloned().collect(); - if bins.len() != 1 { - return Err(format!("the suite built {} copies of log_flood", bins.len())); - } - let staged = logstream::stage(config, "log-keep", &[], &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())), - // test-runner's `===READY===` is a line of the ring logd leaves - // unread; the kernel's record of the job's start is not. - ready_marker: "spawn: /system/bin/test_rs_log_flood ", - ..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); - // A stop that went ahead before the flush answered cuts `/log` short - // whatever the slots did, so that is its own verdict and not this one's. - // init's stop line in /log does not say the flush was answered: init says - // it waited one out, on the console or in /log, and neither may carry that. - let unanswered = if tail.contains(FLUSH_WAITED_OUT) - || bootlog::lines_of(&log, "init").contains(FLUSH_WAITED_OUT) - { - Some("init said so") - } else if bootlog::stopping_line(&log).is_none() { - Some("init's stop line never reached /log") - } else { - None - }; - if let Some(why) = unanswered { - return Err(format!( - "the stop went ahead without the flush's answer ({why}), so /log says nothing of the \ - slots\n{tail}" - )); - } - let runner = bootlog::lines_of(&log, RUNNER); - // Non-vacuity: the flood met a full ring, so the slots were contested. - let flooded = runner.lines().filter(|l| l.starts_with("flood ")).count(); - if flooded == 0 || flooded >= FLOOD_LINES { - return Err(format!("{flooded} of {FLOOD_LINES} flood lines reached /log: the ring was never filled by the flood\n{log}")); - } - if !runner.lines().any(|l| l == ENDED) { - return Err(format!( - "/log carries no {ENDED:?}: the child's flood took the slots its parent's line needed \ - ({flooded} flood lines in /log)\n{log}" - )); - } - eprintln!( - " [origin] {flooded} flood lines filled the ring, and test-runner's {ENDED:?} 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 ab143314eab..00000000000 --- a/tests/common/partclaim.rs +++ /dev/null @@ -1,799 +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; -//! - after a departure, the stick holds what the guest wrote again. -//! -//! 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::{Read, 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"; -/// The twin partition's unique GUID where no boot stick's is copied: the -/// crafted disk of the boots that judge no twin. -const TWIN: &str = "6D2F9B41-8C3E-4A57-B1D0-2E4F6A8C0B13"; -/// Mirrored: DATA, which fsd serves `/home` from. -const DATA: &str = "E3A7C5D9-1B2F-4E6A-8D0C-9F7B5A3E1C24"; -/// Mirrored: the partitions of the stick whose device leaves. -const DEPARTING: &str = "1F3E5D7C-9B2A-4C6E-8F01-A3B5C7D9E2F4"; -const STAYING: &str = "2A4C6E80-1B3D-4F57-9E6A-C8D0B2F4A6E1"; -const EARLIER: &str = "4C6E8A02-3D5F-4179-A0B2-D4F6A8C0E2B4"; - -/// 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 -} - -/// Mirrored in the guest: block `n` of a departure partition, `which` being -/// `D` or `S`. -fn departure_block(which: u8, n: u64) -> Vec { - let mut block = vec![which; BLOCK as usize]; - block[..8].copy_from_slice(&n.to_le_bytes()); - block[8..24].copy_from_slice(b"TOYOS-DEPARTURE\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(()) -} - -/// The exits of a claim that gets no answer: a disk that does not answer a -/// read of its table refuses the claim rather than resolving it on the disks -/// that did, a transfer every attempt of which is refused on its budget ends -/// at the deadman with the device's word, and ROOT's source, whose disk did -/// not answer its hold, stays the kernel's once the disk answers. The crafted -/// disk is a second USB stick beside the boot stick. -pub fn partition_claim_gives_up( - _test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = super::compile::repo_root().join(CONFIG); - let crafted = super::lane::dir().join("partclaim-gives-up.img"); - let cases: [(&'static [&'static str], &str, usize, &[&str]); 2] = [ - ( - &["partclaim-table-unanswered"], - "unanswered", - 1, - &[" did not answer a read of LBA 0 while looking for "], - ), - ( - &["fsync-budget-spent", "fsync-deadman-now"], - "deadman", - 0, - &[ - "partclaim: a write still refused after 1 attempt(s)", - "partclaim: a read still refused after 1 attempt(s)", - ], - ), - ]; - for (params, role, refusals, wants) in cases { - craft_disk(&crafted, TWIN)?; - let mut qemu = QemuInstance::boot_with_options( - &config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::UsbDisk, - usb_images: vec![crafted.clone()], - nvme_image: Some(tableless_nvme("partclaim-nvme.img")?), - kernel_params: params, - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - no_panic(role, &boot)?; - let result = - qemu.run_test(&format!("test_rs_partition_claimant {role}"), Duration::from_secs(180)); - let tail = shut_down(qemu); - let kernel = guest_verdict(&result, &tail, refusals).map_err(|e| format!("{role}: {e}"))?; - for want in wants { - if !kernel.contains(want) { - return Err(format!("{role}: the kernel never said {want:?}:\n{kernel}")); - } - } - no_panic(role, &tail)?; - for want in wants { - let line = kernel.lines().find(|l| l.contains(want)).unwrap_or_default(); - eprintln!(" [partclaim] {role}: {}", line.trim()); - } - } - root_withheld(&config, &crafted, c_bins, rust_bins)?; - let _ = std::fs::remove_file(&crafted); - Ok(()) -} - -fn root_withheld( - config: &Path, - crafted: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["partclaim-root-withheld"]; - let image = super::lane::dir().join("partclaim-root-withheld.img"); - std::fs::write(&image, qemu::build_boot_image(config, c_bins, rust_bins, PARAMS)) - .map_err(|e| format!("write the boot image: {e}"))?; - let [_, _, root] = boot_stick_guids(&image)?; - craft_disk(crafted, TWIN)?; - let mut qemu = QemuInstance::boot_with_options( - config, - c_bins, - rust_bins, - BootOptions { - profile: qemu::Profile::UsbDisk, - boot_image: Some(Staged::Pristine(image.clone())), - usb_images: vec![crafted.to_path_buf()], - nvme_image: Some(tableless_nvme("partclaim-nvme.img")?), - kernel_params: PARAMS, - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - no_panic("withheld", &boot)?; - let not_held = format!( - "root: the partition ROOT was read from, {root}, is not held because it is on no disk \ - that answered" - ); - if !boot.contains(¬_held) { - return Err(format!("withheld: the kernel never said {not_held:?}:\n{boot}")); - } - let result = - qemu.run_test(&format!("test_rs_partition_claimant withheld {root}"), Duration::from_secs(180)); - let tail = shut_down(qemu); - let kernel = guest_verdict(&result, &tail, 1).map_err(|e| format!("withheld: {e}"))?; - let want = format!("partclaim: {root} is where ROOT was read from, and the kernel withholds it"); - if !kernel.contains(&want) { - return Err(format!("withheld: the kernel never said {want:?}:\n{kernel}")); - } - no_panic("withheld", &tail)?; - let _ = std::fs::remove_file(&image); - eprintln!(" [partclaim] withheld: {want}"); - Ok(()) -} - -/// Claims on a USB stick whose device leaves owing a flush of one claim's -/// write and is moved to another port by the host, as a reset moved T14 run -/// 79's stick: each fsync answers for its own partition's writes. Three boots, -/// one departure each (`usb-transport-break-owed` breaks the first write that -/// goes out owing a flush): -/// -/// - `departure`: the claim that lost the write writes again after the return, -/// then is closed and claimed again, and that claim's fsync is told; -/// - `silent`: the claim that lost it never writes again and another claim -/// flushes first — `logd`'s `/log` — and a claim whose write a flush made -/// durable before the departure is not told; -/// - `untold`: nobody asks, and the shutdown's flush of the disk says so. -/// -/// The machine boots off NVMe, so the stick's only writer is the guest. -pub fn partition_claim_departure( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const TOLD: &str = "writes a process's claim of partition "; - const FLUSHED: &str = "usb-quiesce: disk 0 SYNCHRONIZE CACHE ok"; - const UNTOLD: &str = "no writer's flush has said so; the disk is not counted flushed"; - let departing = format!( - "{TOLD}{DEPARTING} made before its disk came back owing a flush may not have survived" - ); - for (role, told) in [("departure", 1), ("silent", 1), ("untold", 0)] { - let (kernel, tail, spans) = departed(test_config, c_bins, rust_bins, role, told)?; - let count = kernel.matches(TOLD).count(); - if count != told || kernel.matches(departing.as_str()).count() != told { - return Err(format!( - "{role}: {count} flushes were told of the loss, not {told}, each {DEPARTING}'s:\n{kernel}" - )); - } - // The untold line begins as the flushed one does, so a flushed disk is - // a flushed line without it. - let untold = tail.contains(UNTOLD); - let clean = tail.lines().any(|l| l.contains(FLUSHED) && !l.contains(UNTOLD)); - let said = if told == 0 { UNTOLD } else { FLUSHED }; - if untold == clean || untold != (told == 0) { - return Err(format!("{role}: the shutdown did not say {said:?} alone:\n{tail}")); - } - if role == "departure" { - let [departing, staying, _] = [spans[0], spans[1], spans[2]]; - let stick = super::lane::dir().join("partclaim-departure.img"); - let got = read_span(&stick, Span { start: departing.start, len: 2 * BLOCK })?; - if got != [departure_block(b'D', 0), departure_block(b'D', 1)].concat() { - return Err("the departing partition does not hold the blocks written again".into()); - } - if read_span(&stick, Span { start: staying.start, len: BLOCK })? - != departure_block(b'S', 0) - { - return Err("the staying partition does not hold its block".into()); - } - } - let line = tail.lines().find(|l| l.contains(said)).unwrap_or_default(); - eprintln!(" [partclaim] {role}: {count} told; {}", line.trim()); - } - let _ = std::fs::remove_file(super::lane::dir().join("partclaim-departure.img")); - eprintln!( - " [partclaim] the stick left owing a claim's write and came back on port 3 three times: \ - the claim that wrote it was told once — after a close and a re-claim, and after another \ - claim flushed first — no other claim was, and a loss nobody asked about kept the \ - shutdown from calling the disk flushed" - ); - Ok(()) -} - -/// One departure boot running the guest's `role`, which says `refusals` -/// refusals: the kernel's log from the test's start through the shutdown, -/// what the shutdown said, and the stick's partitions — `DEPARTING`, -/// `STAYING`, `EARLIER`. -fn departed( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], - role: &str, - refusals: usize, -) -> Result<(String, String, Vec), String> { - const MOVE_NOW: &str = "usb-reset-moves: move the device now"; - const PARAMS: &[&str] = &["usb-transport-break-owed", "usb-reset-moves"]; - const CAME_BACK: &str = "usb-storage: disk 0 came back on port 3 slot "; - let profile = qemu::Profile::NvmeBootUsbDisk; - let (bytes, _) = profile.usb_disk().expect("NvmeBootUsbDisk declares a disk"); - let stick = super::lane::dir().join("partclaim-departure.img"); - let parts = [("departing", MIB, DEPARTING), ("staying", MIB, STAYING), ("earlier", MIB, EARLIER)]; - let spans = craft_stick(&stick, bytes, &parts)?; - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile, - qmp: true, - smp: 2, - kernel_params: PARAMS, - usb_images: vec![stick.clone()], - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - no_panic(role, &boot)?; - let moved = stick; - let result = qemu.run_test_hooked( - &format!("test_rs_partition_claimant {role}"), - Duration::from_secs(240), - MOVE_NOW, - move |socket| { - let mut devices = qemu::QmpDevices::open(socket); - devices.del(&qemu::usb_device_id(0)); - devices.blockdev_add_again("moved", &moved); - devices.add( - "usb-storage", - "xhci.0", - "movedstick", - &[("drive", "moved"), ("port", "3"), ("serial", qemu::DATA_STICK_SERIAL)], - ); - }, - ); - let tail = shut_down(qemu); - let kernel = guest_verdict(&result, &tail, refusals).map_err(|e| format!("{role}: {e}"))?; - for want in [MOVE_NOW, CAME_BACK] { - if !kernel.contains(want) { - return Err(format!("{role}: the kernel never said {want:?}:\n{kernel}")); - } - } - no_panic(role, &tail)?; - Ok((kernel, tail, spans)) -} - -/// 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(()) -} - -/// `span`'s bytes of the file at `path`, without reading the rest of a sparse -/// stick. -pub(super) fn read_span(path: &Path, span: Span) -> Result, String> { - let mut file = std::fs::File::open(path).map_err(|e| format!("open {}: {e}", path.display()))?; - file.seek(SeekFrom::Start(span.start)).map_err(|e| format!("seek: {e}"))?; - let mut buf = vec![0u8; span.len as usize]; - file.read_exact(&mut buf).map_err(|e| format!("read {}: {e}", path.display()))?; - Ok(buf) -} - -/// 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 3e2aa24e99f..8f61750c7df 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -9,532 +9,11 @@ //! 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(()) -} - -/// A boot with no host on the console runs its manifest's jobs and ends -/// itself, and the loader's own account of it is on the stick beside `logd`'s. -pub fn metal_job_reboot( - _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"); - - // A file under the loader's name that the last boot could have left: a - // loader that opens without truncating ends in this one's tail. - let stale = (bootlog::LOADER_LOG.to_string(), vec![b'x'; 64 * 1024]); - let kept = Kept::build(case, &[], "jobcase-boot.img", &[stale])?; - let (image_path, start, len) = (kept.image.clone(), kept.start, kept.len); - - let mut qemu = QemuInstance::boot_with_options( - case, - &[], - &[], - BootOptions { - profile: qemu::Profile::Metal, - qmp: true, - boot_image: kept.boots(), - ..Default::default() - }, - ); - serial::Serial::boot(&qemu).must_be_clean()?; - let console = 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 drain = serial::Serial::named("job drain", tail.as_str()); - drain.must_be_clean()?; - drain.must_say("===TEST_START reboot===")?; - // The control for `job_deadline_reboots`: a list that finishes inside the - // bound is ended by its own last job and never by the deadline. - drain.must_not_say(bootlog::JOB_DEADLINE_SAID)?; - drain.must_say(REBOOTING)?; - returned_to_firmware(reason, ASKED_AND_STAYED_UP, &tail)?; - drop(qemu); - - let (name, log) = super::volumes::newest_log(&image_path, start, len)?; - let text = String::from_utf8_lossy(&log); - // The volume is born clean in an image built moments ago, so every record in it is this boot's. - // Judged by `bootlog`, because a T14 run judges the same volume by it. - let boot_ms = bootlog::verdict(&text).map_err(|unfit| { - format!( - "{name}: {unfit}. A machine with no console would have no account of this \ - boot\n{text}" - ) - })?; - let printed = loader_window(&console)?; - let written = super::volumes::loader_log_lines(&image_path, start, len)?; - // Compared byte for byte against a console the firmware rendered: a - // character it has no glyph for is a line the two channels disagree about - // on one machine and not on the next. - if let Some(line) = written.iter().find(|line| !line.is_ascii()) { - return Err(format!("the loader wrote {line:?}, which is not ASCII")); - } - if written != printed { - return Err(format!( - "{} carries {} line(s) and the loader printed {}\n--- on the stick\n{}\n--- on the \ - console\n{}", - bootlog::LOADER_LOG, - written.len(), - printed.len(), - written.join("\n"), - printed.join("\n"), - )); - } - - kept.remove(); - eprintln!( - " [power] {name} carries Boot: complete ({boot_ms}ms) and init's stop; {} carries the \ - loader's {} lines beside it", - bootlog::LOADER_LOG, - written.len() - ); - Ok(()) -} - -/// **`Rebooting.` is the last record, and it is last by construction.** -pub fn quiesce_stops_the_machine( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // The one binary this config's job list names: every other one staged - // beside it is image the boot pays to write and never reads. - const JOB: &str = "quiesce_writers"; - // How many threads that binary puts to work. Spelt here because a guest - // binary cannot be linked from the harness. - const WRITERS: u32 = 6; - // The threads the stop names besides the writers: the job's own main - // thread, parked on init's answer; `test-runner`'s main and deadline - // threads; `logd`'s; `blockd`'s; one per file server, three roles; and - // `init`'s waiter on each of those four services, and its file worker. - // `init`'s main thread asked for the stop and is its caller. - const OTHERS: u32 = 1 + 2 + 1 + 1 + 3 + 4 + 1; - /// Mirrored in `kernel/src/syscall/machine.rs`, which queues it. - const QUEUED: &str = "console: a holder's line, queued once the stop had stopped every holder"; - let (whole, record) = stopped_boot( - "tests/quiescecase/system.toml", - JOB, - &[LATE_WORD, "console-queue-at-the-stop"], - rust_bins, - )?; - // **A holder's line still queued at the stop is the stop's to put on the - // wire**, above the last word: `klogd` is kept off the queue from the - // stop's claim on, so without that drain the line is never written. - let lines: Vec<&str> = whole.lines().collect(); - let queued = lines.iter().position(|l| l.contains(QUEUED)); - let last = lines.iter().position(|l| l.contains(REBOOTING)); - match (queued, last) { - (Some(queued), Some(last)) if queued < last => {} - _ => { - return Err(format!( - "the line queued at the stop is at {queued:?} and the last word at {last:?}: \ - a holder's line the stop left in the queue is lost at the reset\n{whole}" - )); - } - } - if record.in_flight != 0 { - return Err(format!( - "the block layer still had {} operation(s) open on a thread this stop had stopped, so \ - the machine was not stopped before the sync claimed it was:\n {record}", - record.in_flight, - )); - } - if record.begun == 0 { - return Err(format!( - "this boot began no block-device operation on a stoppable thread, so the zero above \ - is a counter that never counted rather than a machine that stopped:\n {record}" - )); - } - // **The workload, counted by the kernel rather than by the guest, and - // counted exactly.** A boot whose writers never ran, or ran fewer than the - // harness is told, or lost one to an I/O error before the reset, has fewer - // threads to stop and would pass every judge above over a machine that was - // not the one described. - if record.sweep.total() != WRITERS + OTHERS { - return Err(format!( - "this boot's stop named {} userland thread(s); {WRITERS} writers plus the {OTHERS} \ - of the job, test-runner, logd, the storage services, init's waiters and its file worker make {}, so this is not the machine the writers \ - were on:\n {record}\n{whole}", - record.sweep.total(), - WRITERS + OTHERS, - )); - } - eprintln!(" [power] the machine stopped before it claimed anything: {record}"); - 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)) -} - -/// `quiesce-last-park` holds a thread inside `SYS_NANOSLEEP` until the stop's -/// latest sweep counts it as the one thread still running, so the park it then -/// makes is the stop's last transition. -pub fn quiesce_wakes_on_the_last_park( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - woken_by_the_held_thread(&["quiesce-last-park", LATE_WORD], None, rust_bins) -} - -/// **A process teardown that is the stop's last transition is waited for.** -/// The same, with `quiesce-last-teardown` holding the last thread out of a -/// process between its leaving and its teardown. -pub fn quiesce_wakes_on_the_last_teardown( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - woken_by_the_held_thread( - &["quiesce-last-teardown", LATE_WORD], - Some("test_rs_quiesce_last"), - rust_bins, - ) -} - -/// One of the `quiesce-last-*` boots: its actuator first, the late word beside it; -/// `torn_down` names the process whose teardown the held thread runs. -fn woken_by_the_held_thread( - armed: &'static [&'static str; 2], - torn_down: Option<&str>, - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let actuator = armed[0]; - let (whole, record) = stopped_boot( - "tests/quiescelastcase/system.toml", - "quiesce_last", - armed, - rust_bins, - )?; - // **The premise, by the kernel's own word**: the thread was held, and - // held before the stop claimed anything. Without it the boot below is - // one whose last transition was anything at all. - let held = format!( - "{actuator}: {} is held until the stop waits on it alone", - toyos_quiesce::LAST_THREAD, - ); - let at = |needle: &str| whole.lines().position(|line| line.contains(needle)); - let stopped_at = whole.lines().position(|line| toyos_quiesce::Record::parse(line).is_some()); - let (Some(held_at), Some(stopped_at)) = (at(&held), stopped_at) else { - return Err(format!("the kernel never held the thread it names ({held:?})\n{whole}")); - }; - if held_at > stopped_at { - return Err(format!("the thread was held after the stop was over\n{whole}")); - } - // **The claim**: a sweep counted the held thread, and it alone, as - // running before the stop wrote its record, so what the held thread did - // next is what the stop waited for. - let alone = format!("{actuator}: the stop counts {} alone", toyos_quiesce::LAST_THREAD); - let stopped_at = at(toyos_quiesce::STOPPED) - .ok_or_else(|| format!("the kernel wrote no stop record\n{whole}"))?; - let Some(alone_at) = at(&alone).filter(|&line| line < stopped_at) else { - return Err(format!( - "no {alone:?} line before the stop's record, so no transition of the held thread \ - was waited for:\n {record}\n{whole}" - )); - }; - if let Some(process) = torn_down { - let exit = format!("exit: {process} pid="); - let mut torn = whole.lines().skip(alone_at).take(stopped_at - alone_at); - if !torn.any(|line| line.contains(&exit) && line.contains(" code=0 ")) { - return Err(format!( - "no `{exit}… code=0` record between {alone:?} and the stop's record, so the \ - stop did not wait for that teardown\n{whole}" - )); - } - } - eprintln!(" [power] {actuator}: the stop waited on the held thread's transition: {record}"); - Ok(()) -} - -/// **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(()) -} - -/// A job list that never finishes ends the boot anyway, on the runner's own -/// deadline: the kernel is alive and its scheduler passes keep feeding the -/// chipset, so no watchdog is what fires here. -pub fn job_deadline_reboots( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let config = super::compile::repo_root().join("tests/jobdeadlinecase/system.toml"); - let case = config.parent().expect("system.toml has a directory"); - - let mut qemu = QemuInstance::boot_with_options( - case, - &[], - &[], - BootOptions { profile: qemu::Profile::Metal, qmp: true, ..Default::default() }, - ); - serial::Serial::boot(&qemu).must_be_clean()?; - - let mut stop = qemu::QmpShutdown::open(qemu.qmp_socket(), qemu.budget(WAIT)); - let reason = stop.reason(); - let tail = qemu.drain_serial(WAIT); - - let drain = serial::Serial::named("deadline drain", tail.as_str()); - drain.must_be_clean()?; - drain.must_say("===TEST_START spin===")?; - // A deadline that fired without naming the job it was inside answers - // nothing to whoever reads the console afterwards. - drain.must_say(&format!("{} spin", bootlog::JOB_DEADLINE_SAID))?; - drain.must_say(REBOOTING)?; - returned_to_firmware(reason, ASKED_AND_STAYED_UP, &tail)?; - - eprintln!(" [power] the job list did not finish and the runner ended the boot itself"); - Ok(()) -} - -/// Everything the loader printed on the console, its first line to its last. -fn loader_window(console: &str) -> Result, String> { - let lines: Vec<&str> = console.lines().collect(); - let at = |line: &str| { - lines - .iter() - .position(|seen| seen.contains(line)) - .ok_or_else(|| format!("the loader never printed {line:?} on the console")) - }; - let (first, last) = (at(bootlog::LOADER_FIRST_LINE)?, at(bootlog::LOADER_LAST_LINE)?); - if last < first { - return Err(format!( - "the console carries {:?} before {:?}, so there is no window between them", - bootlog::LOADER_LAST_LINE, - bootlog::LOADER_FIRST_LINE - )); - } - Ok(lines[first..=last].iter().map(|line| (*line).to_string()).collect()) -} - -/// 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"; @@ -579,44 +58,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. /// @@ -691,249 +132,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"; @@ -953,721 +156,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 the T14 takes for `usb_stick_left`, under QEMU: the break is -/// staged on the stick the machine booted from, and the page the next pass -/// reads carries the transport's recovery whatever the log volume got. -/// -/// **The page and not the file, because on the machine this is for the file is -/// what goes missing.** The first WRITE(10) a boot issues is `logd` creating -/// its file, so the staged break lands inside the one program that would have -/// written the break down. -pub fn transport_break_chain() -> 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-transport-break"])); - 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 on, so it is the kernel's - // console and the pass after the reset both. - let second = after_the_reset(&mut qemu, bootlog::CHAIN_ENDS_LINE); - super::usb::transport_break_on_metal(&second, &second)?; - ended_in_a_reset(&mut resets)?; - drop(qemu); - - // Off the page: the loader's margin, the section's own head, then the - // record as the kernel rendered it. - let on_the_page = format!("| {}[", toyos_blackbox::RECOVERY_OPENS_WITH); - let carried = |said: &str| { - second - .text() - .lines() - .find(|line| line.starts_with(&on_the_page) && line.contains(said)) - .ok_or_else(|| { - format!( - "no line of the page's recovery section says {said:?}\n{}", - second.text() - ) - }) - }; - let broke = carried("transport broke on SCSI 0x2a: a staged break skipped the data phase wait")?; - carried("the port reset took")?; - carried("SCSI 0x2a completed after ")?; - eprintln!(" [power] the boot stick's own break crossed the reset on the page: {}", broke.trim()); - 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. - second.must_say_after(bootlog::PREVIOUS_PANIC, "spinning on the lock at 0x")?; - 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. /// @@ -1715,131 +203,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. @@ -1882,42 +245,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. /// @@ -2014,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( @@ -2066,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`]. /// diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 7feb27c1729..c90961f6d06 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -116,24 +116,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. /// @@ -521,10 +503,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 { @@ -535,12 +515,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() { @@ -750,55 +728,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 @@ -810,44 +739,7 @@ pub fn await_reset( #[derive(Clone, Copy, PartialEq)] pub enum Profile { Headless, - /// [`Profile::Headless`] with no unit at all: the negative control for - /// whether a virtio function is behind one. QEMU offers - /// `VIRTIO_F_ACCESS_PLATFORM` only for a function created with - /// `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 @@ -856,249 +748,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, - /// The boot volume on NVMe, as [`Profile::MetalNoUsb`] has it, and one USB - /// stick on an xHCI beside it with a serial number of its own. - /// - /// The one machine on which a USB disk's only writer is the guest: every - /// other USB profile boots off the stick, so `/log` is on the bus and - /// logd's first batch is the first write `usb-transport-break-owed` can - /// break. Here the guest decides which write its device leaves under, and - /// the stated serial number is what lets the host move it to another port - /// and have it taken back as itself. - NvmeBootUsbDisk, - /// [`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, - /// metal-sim with a device that attaches at **full speed**. - /// - /// Speed is a shape dimension and it was one no profile varied: every USB - /// device in this suite is high or SuperSpeed, and those two are the speeds - /// whose EP0 max packet size is fixed by the specification. Full speed is - /// the one where it is not — 8, 16, 32 or 64, and unknown until the first - /// eight bytes of the device descriptor have been read over the very - /// endpoint being sized. A T14 port answered a USB Transaction Error to a - /// driver that assumed 64 and read 18 bytes in one go, and no test here - /// could have seen it. - /// - /// Two of them, because `bMaxPacketSize0` is the dimension under test and a - /// profile with one value of it cannot tell "the driver read the device's - /// answer" from "the driver's guess happened to match": the tablet answers - /// **8** and the smartcard reader answers **64**, so one boot carries both - /// the correction and its absence. Both are full-speed only — QEMU gives - /// each a `.full` descriptor set and no `.high` one, so `usb_desc_attach` - /// has no faster speed to pick — and neither needs a chardev, drive or - /// audiodev to enumerate. Measured with `info usb` on QEMU 11.0.2: both - /// report 12 Mb/s, and `usb-kbd`, which every other profile uses, reports - /// 480. - MetalFullSpeed, - /// 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 @@ -1121,41 +770,8 @@ impl Profile { match self { 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::NvmeBootUsbDisk - | Self::UsbDiskRefusedFirst - | Self::UsbDiskCrowd - | Self::MetalFullSpeed - | 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, } } @@ -1202,51 +818,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`]. @@ -1254,13 +825,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 { @@ -1281,26 +845,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 @@ -1316,11 +860,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, @@ -1328,13 +867,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 @@ -1345,101 +877,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, @@ -1448,40 +893,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"; -/// The serial number of [`Profile::NvmeBootUsbDisk`]'s stick, for the same -/// reason the boot stick states one. -pub const DATA_STICK_SERIAL: &str = "TOYOS0DATASTICK1"; - -/// 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 { @@ -1489,100 +904,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 { @@ -1592,472 +938,13 @@ 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::NvmeBootUsbDisk => Shape { - xhci: &[XHCI_DEFAULT], - usb_disks: &[UsbDisk { - bus: Some("xhci.0"), - serial: Some(DATA_STICK_SERIAL), - ..UsbDisk::DATA - }], - ..Self::MetalNoUsb.shape() - }, - 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::MetalFullSpeed => Shape { - vga: "std", - panel: None, - gpu: None, - virtio: Virtio::Absent, - nic: Nic::Absent, - xhci: &[XHCI_DEFAULT], - storage_bus: "xhci.0", - usb: &["usb-wacom-tablet,bus=xhci.0", "usb-ccid,bus=xhci.0"], - nvme_bytes: NVME_SMALL, - nvme_lba_bytes: NVME_LBA_DEFAULT, - usb_disks: &[], - hda: &[], - iommu: Some(IOMMU_DEFAULT), - }, - 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() - }, - } - } - - /// 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. - 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, } } } @@ -2087,156 +974,24 @@ pub struct BootOptions { /// [`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, - /// The virtio console's output into a regular file the harness follows, - /// and its input through a FIFO, instead of QEMU's stdio. QEMU's - /// `virtconsole` drops what a full non-blocking stdout refuses, and a - /// regular file refuses no write: for a test whose verdict is a line after - /// megabytes of console (`issues/build/qemu-drops-console-output-the-harness-is-slow-to-read.md`). - pub console_file: bool, - /// 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 { @@ -2270,25 +1025,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, - console_file: false, - segment: false, - ssh_port: None, - wire_dump: None, - userland_nvme: None, - nvme_trace: None, } } } @@ -2298,29 +1037,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 @@ -2328,22 +1044,6 @@ 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 @@ -2367,12 +1067,6 @@ impl ConsoleStream { 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 @@ -2386,233 +1080,29 @@ impl ConsoleStream { 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. Measured 2026-08-22: `usb-flush-fails` armed -/// through `kernel_params` alone on `esp_filesystem` passed with no injected -/// sense anywhere in the log, while the same actuator baked into the image -/// failed the same assertion. -/// -/// 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" - ); - } + /// What ends QEMU when this process dies without dropping this. + _tether: Tether, + stdin: BufWriter>, + rx: Receiver, + console: ConsoleStream, + _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. @@ -2620,10 +1110,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(); @@ -2768,25 +1254,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); @@ -2803,113 +1276,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")); @@ -2919,28 +1333,13 @@ impl QemuInstance { // boot. let uart_log = test_dir.join(format!("uart-{seq}.log")); let _ = fs::remove_file(&uart_log); - let console_file = options.console_file.then(|| ConsoleFile::of(&uart_log).made()); - - 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 qemu = qemu_command( - &boot_image, - nvme.path(), - &usb_images, - &uart_log, - &sockets.dir, - &firmware_vars, - &options, - ); + 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(), &uart_log, &sockets.dir, &vars, &options); spawn_and_wait_ready( qemu, &options, @@ -2950,10 +1349,9 @@ impl QemuInstance { nvme, sockets, screendump, - own_boot_image, - own_vars, + boot_image, + vars, carried, - console_file, }, ) } @@ -3117,10 +1515,6 @@ impl QemuInstance { } } - 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 @@ -3133,81 +1527,12 @@ 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 } @@ -3264,59 +1589,12 @@ impl QemuInstance { } } - /// Wait for `marker` on the console, or the timeout. - /// - /// A console is a stream and this consumes it: every line up to and - /// including the marker is taken from whatever reads next. - pub fn wait_for_console(&mut self, marker: &str, timeout: Duration) -> bool { - let deadline = Instant::now() + budget_smp(timeout, self.smp); - loop { - let Some(left) = deadline.checked_duration_since(Instant::now()) else { - return false; - }; - match self.rx.recv_timeout(left) { - Ok(line) if line.contains(marker) => return true, - Ok(_) => continue, - Err(_) => return false, - } - } - } - - /// Send `command` and wait for `marker` on the console. - /// - /// For a guest that will never report `===TEST_END`, which is any guest - /// the fatal path has run through: every CPU is halted by the time the - /// marker arrives. - pub fn command_until(&mut self, command: &str, marker: &str, timeout: Duration) -> bool { - writeln!(self.stdin, "{command}").expect("Failed to write to QEMU stdin"); - self.stdin.flush().expect("Failed to flush QEMU stdin"); - self.wait_for_console(marker, timeout) - } - /// The QMP socket this instance opened. Injection needs it, and it needs /// `BootOptions { qmp: true }`. pub fn qmp_socket(&self) -> &Path { 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, "", |_| {}) } @@ -3363,22 +1641,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 @@ -3412,10 +1688,7 @@ impl QemuInstance { name: name.to_string(), exit_code: None, stdout, - serial, - before, error: Some(error), - started: in_test, }; } @@ -3493,14 +1766,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 { @@ -3524,10 +1795,7 @@ impl QemuInstance { name: name.to_string(), exit_code: None, stdout, - serial, - before, error: Some(error), - started: in_test, }; } } @@ -3558,7 +1826,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. @@ -3681,146 +1949,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); @@ -3907,37 +2035,6 @@ impl QmpInput { } self.keys(&events); } - - /// `times` relative moves of `dx`, all in one command. - /// - /// QEMU syncs its input once per command and its PS/2 device *accumulates* - /// motion between syncs, so this is one packet carrying the sum however - /// many moves it names — the deterministic form of what a host holding more - /// packets outstanding than that device's queue meets by accident. - pub fn mouse_merged(&mut self, dx: i32, times: usize) { - let body: Vec = (0..times) - .map(|_| format!("{{\"type\":\"rel\",\"data\":{{\"axis\":\"x\",\"value\":{dx}}}}}")) - .collect(); - self.send(&body); - } - - /// 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. @@ -3979,10 +2076,6 @@ fn qcode(ch: char) -> (&'static str, bool) { } } -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. @@ -4014,31 +2107,6 @@ impl QmpDevices { .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}}}}" - )); - } - - /// [`Self::blockdev_add`] for a file a drive may still hold open: the - /// unplugged device's own, which QEMU may not have let go of yet. Taken - /// without the image lock that would refuse it; both read and write the one - /// file, so what the first wrote is what the second reads. - pub fn blockdev_add_again(&mut self, node: &str, image: &Path) { - self.0.execute(&format!( - "{{\"execute\":\"blockdev-add\",\"arguments\":{{\"node-name\":\"{node}\",\ - \"driver\":\"raw\",\"file\":{{\"driver\":\"file\",\"locking\":\"off\",\ - \"filename\":\"{}\"}}}}}}", - image.display() - )); - } - /// 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. @@ -4057,47 +2125,26 @@ 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(); - let console_file = options.console_file.then(|| ConsoleFile::of(uart_log)); assert!( !options.mute || !shape.virtio.present(), "mute removes the only console a virtio profile has" ); - assert!( - console_file.is_none() || shape.virtio.present(), - "console_file is the virtio console's, and this profile has none" - ); let arch = options.profile.arch(); let [firmware_code, firmware_vars] = toyos_build::firmware::of(arch) @@ -4130,26 +2177,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. @@ -4170,29 +2209,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 @@ -4215,73 +2233,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); @@ -4294,10 +2248,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 @@ -4326,137 +2277,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); + .arg("nvme-ns,drive=nvme0,bus=nvme0ctl,logical_block_size=512,physical_block_size=512"); } - - // 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); - } - } - 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 @@ -4473,10 +2315,7 @@ fn qemu_command( .arg("-serial") .arg(format!("file:{}", uart_log.display())) .arg("-chardev") - .arg(match &console_file { - Some(file) => format!("file,id=cs0,path={},input-path={}", file.out.display(), file.input.display()), - None => "stdio,id=cs0,signal=off".to_string(), - }) + .arg("stdio,id=cs0,signal=off") .arg("-device") .arg(format!( "virtio-serial-pci-non-transitional,id=virtio-serial0,max_ports=1{platform}" @@ -4513,24 +2352,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 @@ -4541,116 +2375,27 @@ struct Files { nvme: NvmeClaim, sockets: Sockets, screendump: PathBuf, - own_boot_image: Option, - own_vars: Option, - carried: Option>, - console_file: Option, -} - -/// [`BootOptions::console_file`]'s two paths, beside the boot's UART log: the -/// file QEMU writes the console into, and the FIFO it reads its input from. -struct ConsoleFile { - out: PathBuf, - input: PathBuf, -} - -impl ConsoleFile { - fn of(uart_log: &Path) -> Self { - Self { out: uart_log.with_extension("console"), input: uart_log.with_extension("in") } - } - - /// The two made: the file, so the follower can open it before QEMU does, - /// and the FIFO. - fn made(self) -> Self { - fs::File::create(&self.out).unwrap_or_else(|e| panic!("create {}: {e}", self.out.display())); - let _ = fs::remove_file(&self.input); - let c = std::ffi::CString::new(self.input.as_os_str().as_encoded_bytes()).expect("a path holds no NUL"); - // SAFETY: a NUL-terminated path this call owns. - if unsafe { libc::mkfifo(c.as_ptr(), 0o600) } != 0 { - panic!("mkfifo {}: {}", self.input.display(), std::io::Error::last_os_error()); - } - self - } -} - -/// The console file read as a stream. At its end a read waits for more on the -/// one event QEMU gives: its stdout, which it never writes with a file -/// console, ending when it exits. A regular file has no readiness of its own -/// on either host, so between those the file is asked again every -/// [`Self::PERIOD_MS`]. -struct Followed { - file: fs::File, - exit: std::process::ChildStdout, - gone: bool, -} - -impl Followed { - const PERIOD_MS: i32 = 2; -} - -impl Read for Followed { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - use std::os::fd::AsRawFd; - loop { - let n = self.file.read(buf)?; - if n > 0 || self.gone { - return Ok(n); - } - let mut fd = libc::pollfd { fd: self.exit.as_raw_fd(), events: libc::POLLIN, revents: 0 }; - // SAFETY: one `pollfd` this call owns, for the one entry it holds. - if unsafe { libc::poll(&mut fd, 1, Self::PERIOD_MS) } > 0 { - let mut stray = [0u8; 256]; - // Its end, after which the file is read once more for what QEMU wrote last. - self.gone = self.exit.read(&mut stray)? == 0; - } - } - } + 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, - console_file, - } = 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()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()); - // Read and write, so QEMU's read-only open finds a writer and does not block. - let input = console_file.as_ref().map(|f| { - fs::OpenOptions::new() - .read(true) - .write(true) - .open(&f.input) - .unwrap_or_else(|e| panic!("open {}: {e}", f.input.display())) - }); if VERBOSE.load(Ordering::Relaxed) { eprintln!("[qemu {seq}] Launching QEMU..."); } let (mut child, tether) = toyos_build::tether::spawn(qemu).expect("Failed to launch QEMU"); - let stdin: Box = match input { - Some(fifo) => Box::new(fifo), - None => Box::new(child.stdin.take().unwrap()), - }; + let stdin: Box = Box::new(child.stdin.take().unwrap()); let stdin = BufWriter::new(stdin); - let stdout: Box = match &console_file { - Some(f) => Box::new(Followed { - file: fs::File::open(&f.out).unwrap_or_else(|e| panic!("open {}: {e}", f.out.display())), - exit: child.stdout.take().unwrap(), - gone: false, - }), - None => Box::new(child.stdout.take().unwrap()), - }; + let stdout: Box = Box::new(child.stdout.take().unwrap()); let (tx, rx) = mpsc::channel::(); let console = ConsoleStream::new(); @@ -4719,17 +2464,14 @@ 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, } } diff --git a/tests/common/screen.rs b/tests/common/screen.rs index 4ebfb20c044..a7ed0f6bced 100644 --- a/tests/common/screen.rs +++ b/tests/common/screen.rs @@ -173,53 +173,6 @@ impl Ppm { None } - /// How many rows of text are on the panel, counted without decoding a - /// glyph: no firmware font is committed here, so a row one drew is - /// invisible to [`Ppm::text`]. - /// - /// A band taller than the pitch is a logo or two rows that touch, and - /// neither is one row of text; the pitch is the median distance between - /// band tops, because a stray scanline sets a minimum of one and drags a - /// mean as well. A panel with fewer than two bands has no pitch to take, - /// and is refused rather than counted as none. - pub fn text_row_bands(&self) -> Result { - let mut bands: Vec<(usize, usize)> = Vec::new(); - let mut top = None; - for y in 0..self.height { - let lit = (0..self.width) - .any(|x| self.pixels[y * self.width + x].iter().any(|c| *c >= FG_THRESHOLD)); - match (lit, top) { - (true, None) => top = Some(y), - (false, Some(from)) => { - bands.push((from, y)); - top = None; - } - _ => {} - } - } - if let Some(from) = top { - bands.push((from, self.height)); - } - let mut gaps: Vec = bands.windows(2).map(|pair| pair[1].0 - pair[0].0).collect(); - if gaps.is_empty() { - return Err(format!( - "the panel carries {} band(s) of lit scanlines, too few to take a row pitch from", - bands.len() - )); - } - gaps.sort_unstable(); - let pitch = gaps[gaps.len() / 2]; - let rows = bands.iter().filter(|(from, to)| to - from <= pitch).count(); - if rows == 0 { - return Err(format!( - "every one of the panel's {} band(s) is taller than the {pitch}-pixel pitch, so \ - none of them is a row of text", - bands.len() - )); - } - Ok(rows) - } - /// The fill colour, read from the bottom-right pixel. The renderer paints /// at most `MAX_ROWS` rows and never the last column of a glyph cell, so /// this corner carries the fill and nothing else. @@ -231,11 +184,6 @@ impl Ppm { pub fn row_index(&self, needle: &str) -> Option { self.rows().iter().position(|r| r.contains(needle)) } - - /// Whether every pixel matches `other`. - pub fn identical_to(&self, other: &Ppm) -> bool { - self.width == other.width && self.height == other.height && self.pixels == other.pixels - } } /// Cells of the console's font, in the alpha values it blits. 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 07da8bff12c..4185722027b 100644 --- a/tests/common/serial.rs +++ b/tests/common/serial.rs @@ -25,7 +25,7 @@ //! This is the text channel. The framebuffer is `screen.rs`, deliberately the //! only thing in the suite that reads pixels. -use super::qemu::{is_kernel_line, QemuInstance}; +use super::qemu::is_kernel_line; /// Whose death a console line reports. /// @@ -206,26 +206,12 @@ pub struct Serial { } impl Serial { - /// Everything the guest said on the way to its ready marker. - pub fn boot(qemu: &QemuInstance) -> Self { - 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 } @@ -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. 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 index edc2d8f2858..239d3404b98 100644 --- a/tests/common/swap.rs +++ b/tests/common/swap.rs @@ -10,332 +10,19 @@ //! file's own in its order. What the host heard ([`metalswap::judge`]) is the //! same reading the T14 run gets. -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); +use toyos_build::metalswap::{self, Expect}; /// The swapping boot's one job on the T14: it holds the machine until the /// swap invocation hands it back. const HOLD: &str = "test_rs_lan_swap_hold"; pub const HOLD_JOBS: &[&str] = &[HOLD]; -/// A test binary that panics the instant it starts, and its panic's own line. -const CRASH: &str = "swap_crash"; -const CRASH_PANIC: &str = "panicked at src/bin/swap_crash.rs"; - -/// 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. `staged` is the one - /// program panic the test caused on purpose, which the console must carry - /// exactly once. - fn finish(mut self, staged: Option<&str>) -> 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); - // A program's panic prints the spelling a kernel panic does, so the - // one this test staged is taken out by its own line, and counted. - let mut console = self.console.clone(); - if let Some(needle) = staged { - let seen = console.matches(needle).count(); - if seen != 1 { - return Err(format!("{needle:?} is on the console {seen} time(s), where it was staged once")); - } - console = console.lines().filter(|l| !l.contains(needle)).collect::>().join("\n"); - } - serial::Serial::named("the swapping boot", 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) -} - /// The first line in `file` holding `needle` at or after index `from`. fn after(file: &[String], from: usize, needle: &str) -> Option { file[from.min(file.len())..].iter().position(|l| l.contains(needle)).map(|at| from + at) } -/// netd swapped for its rebuild on `bench`, and the lease coming back through -/// the new process with no reboot between. -fn netd_in_service(name: &str, bench: Bench) -> Result<(), String> { - let rig = Rig::boot(name, bench)?; - let binary = rebuilt("netd", &rig.staged.scratch)?; - let swapped = match rig.swap("netd", &binary, None) { - Ok(swapped) => swapped, - Err(why) => return Err(rig.fail(why)), - }; - let rig = rig.judged(&swapped, Expect::InService)?; - if !swapped.said.iter().any(|l| l.contains(toyos_build::lan::LEASE)) { - return Err(format!( - "the stream carried no lease from netd after the swap; netd said {:?}", - swapped.said - )); - } - let digest = toyos_swap::parse_hex(&swapped.digest).ok_or("the digest the host sent")?; - let installed = toyos_swap::installed_path("netd", &digest); - let (file, _, staged) = rig.finish(None)?; - // The kernel's own record of what it loaded, then init putting it in - // service, then a lease from the network after both. - let spawned = after(&file, 0, &format!("spawn: {installed}")) - .ok_or_else(|| format!("/log has no `spawn: {installed}` record"))?; - let committed = after(&file, spawned, &toyos_swap::said("netd", Word::InService, &installed)) - .ok_or("/log has no `in service` from init after the spawn")?; - let leased = after(&file, spawned, toyos_build::lan::LEASE) - .ok_or("/log has no lease after the new netd was spawned")?; - eprintln!( - " [swap] /log: spawn at line {spawned}, in service at {committed}, lease at {leased}; \ - one boot ({} lines)", - file.len() - ); - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} - -pub fn swap_netd( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - netd_in_service("swap-netd", VIRTIO) -} - -/// The T14's swap rehearsed on its register file: QEMU's 82574 brought up a -/// second time in one boot by a second netd. -pub fn lan_swap( - _test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - netd_in_service("lan-swap", super::lan::TALK_BENCH) -} - -/// **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(None)?; - 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(()) -} - -/// A replacement that panics at once: init says it failed, starts the binary it -/// replaced, and the machine answers ssh through that one. -pub fn swap_crash_rolls_back( - _test_config: &Path, - _c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let rig = Rig::boot("swap-crash", VIRTIO)?; - let (_, crash) = rust_bins - .iter() - .find(|(name, _)| name == CRASH) - .ok_or_else(|| format!("no `{CRASH}` among the test binaries"))?; - let binary = rig.staged.scratch.join(CRASH); - std::fs::write(&binary, crash).map_err(|e| format!("{}: {e}", binary.display()))?; - let swapped = match rig.swap("netd", &binary, None) { - Ok(swapped) => swapped, - Err(why) => return Err(rig.fail(why)), - }; - let rig = rig.judged(&swapped, Expect::Restored)?; - let (file, _, staged) = rig.finish(Some(CRASH_PANIC))?; - let restored = after(&file, 0, &toyos_swap::said("netd", Word::Restored, "/system/bin/netd as pid")) - .ok_or("/log has no `restored` of the image's netd")?; - let pid = file[restored] - .rsplit("as pid ") - .next() - .and_then(|pid| pid.trim().parse::().ok()) - .ok_or_else(|| format!("init's `restored` names no pid: {:?}", file[restored]))?; - // The lease is looked for after the kernel's spawn of the restored process - // and not after init's word on it: init speaks once the spawn returns, and - // a netd that leases first puts its lease above that word. - let spawned = after(&file, 0, &format!("spawn: /system/bin/netd pid={pid} ")) - .ok_or_else(|| format!("/log has no `spawn:` of the restored netd, pid {pid}"))?; - after(&file, spawned, toyos_build::lan::LEASE) - .ok_or("/log has no lease from the restored netd")?; - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} - /// The T14's swap, judged: what the swap invocation heard over the cable, held /// against the stick's own `/log` — which came back over a different path and /// is the oracle for all of it. One `Boot: complete` in the file is the claim @@ -384,452 +71,3 @@ pub fn swapped_on_metal(back: &super::metal::Readback) -> Result<(), String> { Err(format!("{} finding(s):\n {}", bad.len(), bad.join("\n "))) } -/// 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(None)?; - let _ = std::fs::remove_file(&staged.image); - Ok(()) -} diff --git a/tests/common/toybox.rs b/tests/common/toybox.rs deleted file mode 100644 index 8fded239b42..00000000000 --- a/tests/common/toybox.rs +++ /dev/null @@ -1,277 +0,0 @@ -//! `cp` and `mv` against a real volume, judged on the disk image the device -//! received rather than on what the guest said it did. -//! -//! Two questions, one boot, one volume, because the second needs the first to -//! have already spent the space: -//! -//! 1. **A copy is byte-exact on the device.** The source is staged by the host -//! before the machine exists, so a guest that read back its own writes -//! cannot pass — and it is several times `cp`'s flush interval, so the copy -//! reaches the device in a series of flushes rather than one. -//! 2. **A copy that runs out of volume refuses, and leaves nothing.** The -//! volume is filled here to a hair over one copy's worth of free space, so -//! the first `cp` succeeds and the second cannot. What the second must leave -//! behind is *nothing*: no file under the destination's name, no `.part` -//! sibling, and nothing new for the volume checker to say — a half-allocated -//! cluster chain is what a "did the command exit non-zero" check cannot see. -//! -//! The log partition and not the ESP, which is where this started. Measured on -//! a freshly built test image: the ESP has **167 free clusters, 85,504 bytes**. -//! `create_esp_volume` sizes it at `content + 4 MiB`, and at that volume size -//! FAT32 uses 512-byte clusters, so the two FATs describing half a million of -//! them eat the whole four megabytes of slack. Nothing large can be written to -//! `/boot` at runtime; the log partition is the only writable FAT32 volume on -//! the stick with room, and it starts with 33 MiB free. -//! -//! Which means sharing with `/system/bin/logd`. [`LEAVE_FREE`] is set so that after the -//! copy that succeeds there are still about two megabytes for this boot's log — -//! a boot's log is a few tens of kilobytes, and logd stops and says so if a -//! write ever fails, so a squeeze here would be visible rather -//! than silent. - -use std::io::{Cursor, Write}; -use std::path::{Path, PathBuf}; -use std::time::Duration; - -use fatfs::FsOptions; - -use super::qemu::{self, BootOptions, QemuInstance}; -use super::serial; -use toyos_fat32_check::{check, describe}; - -use super::volumes::{log_extent, read_files}; - -/// The file the guest copies. Several times `cp`'s `FLUSH_BYTES` and not a page -/// multiple: the periodic flush fires repeatedly and the tail is partial, which -/// are the two shapes a single-`write_page` copy would get wrong. -const SRC_LEN: usize = 4 * 1024 * 1024 + 137; -const SRC: &str = "cp-src.bin"; -const DST: &str = "cp-dst.bin"; -/// Where the first copy ends up after `mv` renames it, on a FAT32 volume the -/// host can check. -const MOVED: &str = "cp-moved.bin"; -/// The copy that must not fit. -const FULL: &str = "cp-full.bin"; -/// What the host writes to eat the rest of the volume. -const FILLER: &str = "filler.bin"; - -/// Free bytes to leave once the source and the filler are staged. -/// -/// Above one source so the first copy fits, below two so the second cannot — -/// and the half that is left over after the first copy is the room `kernel.log` -/// has for the rest of the boot. The band is asserted rather than assumed: -/// the volume's free space is read back after staging. -const LEAVE_FREE: usize = SRC_LEN + SRC_LEN / 2; - -fn source_bytes() -> Vec { - (0..SRC_LEN).map(|i| (i.wrapping_mul(2_654_435_761) >> 7) as u8).collect() -} - -fn test_dir() -> PathBuf { - super::lane::dir() -} - -/// Free bytes, and the cluster size the volume counts them in. -fn free_space(volume: &[u8]) -> Result<(usize, usize), 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 stats = fs.stats().map_err(|e| format!("counting free clusters: {e}"))?; - let cluster = stats.cluster_size() as usize; - Ok((stats.free_clusters() as usize * cluster, cluster)) -} - -/// Every name in the volume's root, which is where everything here lands. -fn names(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 out = Vec::new(); - for entry in fs.root_dir().iter() { - let entry = entry.map_err(|e| format!("listing the volume root: {e}"))?; - out.push(entry.file_name()); - } - out.sort(); - Ok(out) -} - -fn write_file(volume: &mut [u8], path: &str, bytes: &[u8]) -> Result<(), String> { - let fs = fatfs::FileSystem::new(Cursor::new(volume), FsOptions::new()) - .map_err(|e| format!("the built volume does not mount on the host: {e}"))?; - let mut file = fs - .root_dir() - .create_file(path) - .map_err(|e| format!("creating {path}: {e}"))?; - file.write_all(bytes).map_err(|e| format!("writing {path}: {e}")) -} - -/// Drive one command through the in-guest runner and say what it did. -/// -/// `run_test` sends the whole string after `run `, and the runner splits it -/// into a binary and its arguments — so this is the real `/system/bin/cp`, invoked the -/// way a user invokes it, and the exit code is the one it returned. -/// -/// The serial window is appended to `log` rather than judged here: a `cp` that -/// takes a second produces no kernel line of its own, and `must_be_clean` on a -/// window with none in it is a claim about nothing. The shutdown at the end -/// supplies the liveness for all four windows at once. -fn run( - qemu: &mut QemuInstance, - log: &mut serial::Serial, - command: &str, -) -> Result<(Option, String), String> { - let result = qemu.run_test(command, Duration::from_secs(120)); - log.push(&result.serial); - if let Some(err) = &result.error { - return Err(format!("`{command}` never finished: {err}\nserial:\n{}", result.serial)); - } - Ok((result.exit_code, result.stdout)) -} - -pub fn cp_volume( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let image_path = test_dir().join("toybox-cp-boot.img"); - 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 (start, len) = log_extent(&image, &image_path)?; - - let source = source_bytes(); - write_file(&mut image[start..start + len], SRC, &source)?; - - // The filler is sized from what the volume says it has left, not from a - // number picked here — a constant would go stale the moment the partition - // or its cluster size moved, and would do it silently. - let (free, cluster) = free_space(&image[start..start + len])?; - if free <= LEAVE_FREE { - return Err(format!( - "the log volume has {free} bytes free after staging a {SRC_LEN}-byte source, \ - already at or under the {LEAVE_FREE} this test must leave — there is no room \ - to set the full-volume half up" - )); - } - let filler = (free - LEAVE_FREE) / cluster * cluster; - if filler > 0 { - write_file(&mut image[start..start + len], FILLER, &vec![0x5A; filler])?; - } - - let (left, _) = free_space(&image[start..start + len])?; - if left < SRC_LEN + cluster || left >= 2 * SRC_LEN { - return Err(format!( - "staging left {left} bytes free; one copy needs {SRC_LEN} and two need {}, so \ - this boot would answer only one of the two questions", - 2 * SRC_LEN - )); - } - std::fs::write(&image_path, &image).map_err(|e| format!("rewrite the boot image: {e}"))?; - let complaints_before = check(&image[start..start + len]); - eprintln!(" [toybox] staged {SRC_LEN} bytes and a {filler}-byte filler, {left} bytes free"); - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { boot_image: Some(qemu::Staged::Written(image_path.clone())), ..Default::default() }, - ); - let boot = qemu.boot_log().to_string(); - let boot = serial::Serial::named("boot console", boot.as_str()); - boot.must_be_clean()?; - boot.must_say(super::volumes::LOG_SERVED)?; - let mut log = serial::Serial::named("the three commands and the shutdown", ""); - - // One: the copy that fits. - let (code, _) = run(&mut qemu, &mut log, &format!("cp /log/{SRC} /log/{DST}"))?; - if code != Some(0) { - return Err(format!("cp of a {SRC_LEN}-byte file onto {left} free bytes exited {code:?}")); - } - - // Two: the same copy again, onto what is left, which is not enough. The - // refusal has to name the file — an exit code alone leaves the caller - // guessing which half of the command failed. - let (code, said) = run(&mut qemu, &mut log, &format!("cp /log/{SRC} /log/{FULL}"))?; - if code == Some(0) { - return Err(format!( - "cp claimed to copy {SRC_LEN} bytes onto a volume with under {SRC_LEN} free" - )); - } - if !said.contains("cp:") || !said.contains(FULL) { - return Err(format!("cp exited {code:?} without naming what it refused:\n{said}")); - } - eprintln!(" [toybox] {}", said.lines().find(|l| l.contains("cp:")).unwrap_or("").trim()); - - // Three: a rename on a FAT32 volume, so the host sees the move rather than - // the guest's account of it. - let (code, _) = run(&mut qemu, &mut log, &format!("mv /log/{DST} /log/{MOVED}"))?; - if code != Some(0) { - return Err(format!("mv within the log volume exited {code:?}")); - } - - // The shutdown is not politeness: it is what makes the host's view of the - // backing file the device's view of it. - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - log.push(&qemu.drain_serial(Duration::from_secs(20))); - drop(qemu); - log.must_be_clean()?; - - let after = std::fs::read(&image_path).map_err(|e| format!("read the image back: {e}"))?; - if after.len() != image.len() { - return Err(format!("the image is {} bytes, was {}", after.len(), image.len())); - } - let volume = &after[start..start + len]; - - // Strongest first: a failed allocation that damaged the FAT would still - // pass every byte comparison below. The staging above is `fatfs`'s work - // rather than the guest's, so what is asked is that the boot add nothing — - // and a complaint carries its numbers as fields, so a moved free-cluster - // count is a different complaint of the same kind rather than a string that - // has to have its digits blanked before the two lists can be compared. - let complaints_after = check(volume); - let fresh: Vec<&toyos_fat32_check::Complaint> = - complaints_after.iter().filter(|c| !complaints_before.contains(c)).collect(); - if !fresh.is_empty() { - return Err(format!( - "cp left the log volume breaking the format:\n{}\n\ - before the boot the checker said:\n{}", - fresh.iter().map(|c| c.to_string()).collect::>().join("\n"), - describe(&complaints_before) - )); - } - - let found = names(volume)?; - for absent in [DST, FULL] { - if found.iter().any(|n| n.eq_ignore_ascii_case(absent)) { - return Err(format!("{absent} is on the volume; its root holds {found:?}")); - } - } - // The one a "did it exit non-zero" test cannot see: a refused copy that - // left its working file behind eats the volume for good. - let partials: Vec<&String> = - found.iter().filter(|n| n.to_lowercase().contains("part")).collect(); - if !partials.is_empty() { - return Err(format!("a refused cp left {partials:?} on the volume")); - } - - let mut got = read_files(volume, &[MOVED, SRC])?.into_iter(); - let moved = got - .next() - .flatten() - .ok_or_else(|| format!("{MOVED} is not on the volume; its root holds {found:?}"))?; - if moved.len() != source.len() { - return Err(format!("the copy is {} bytes on the device, source is {SRC_LEN}", moved.len())); - } - if let Some(at) = moved.iter().zip(&source).position(|(a, b)| a != b) { - return Err(format!("the copy differs from the source at byte {at}")); - } - let src_back = got.next().flatten().ok_or_else(|| format!("{SRC} is gone from the volume"))?; - if src_back != source { - return Err("the source changed under a copy that only reads it".to_string()); - } - - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [toybox] {SRC_LEN} bytes copied, moved and verified host-side; the copy that did \ - not fit left nothing" - ); - 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 fbbaaa641f3..7fe31562bf1 100644 --- a/tests/common/usb.rs +++ b/tests/common/usb.rs @@ -10,57 +10,12 @@ //! file stays two lines: every agent edits it, and a wide diff there is how //! work gets swept into somebody else's commit. -use std::io::{Read, Seek, SeekFrom, Write}; +use std::io::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() } @@ -75,819 +30,6 @@ fn sparse(path: &Path, bytes: u64) -> std::fs::File { .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, the one a staged break interrupted included.** -/// `usb_transport_break` used to name that block as nobody's claim, which was -/// the driver's lost write written into the harness as an expectation. -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(()) -} - -/// A data phase the **controller** cut short while the device's own CSW claims -/// it moved everything. -/// -/// One number, counted twice. `bulk` returns the residue the xHC reports — -/// bytes it did not move into the buffer — and `bot` threw it away, so -/// `delivered` came from the CSW's `dCSWDataResidue` alone: the device's own -/// account of its own transfer. The `MSC_DATA` window is never cleared between -/// transfers, so a device that under-delivers a READ(10) and reports a residue -/// of zero handed the caller the *previous* transfer's bytes for the part that -/// never arrived — a different LBA's data, under this LBA's number, with no -/// error anywhere. -/// -/// **The actuator corrupts the transfer, not the -/// verdict.** QEMU derives the CSW residue from the same transfer the xHC -/// completed, so the two accounts are one number there and can never -/// contradict each other; `rerror` fails the whole command instead. The -/// injection puts the tail of the window back to what it held *before* the -/// transfer — the previous read's bytes, read off that window rather than -/// invented — and adds those bytes to the controller's residue. The completion -/// code is the controller's, the CSW is the device's, and what a driver -/// discarding the controller's number is handed is byte-for-byte what a real -/// short data phase would have left. -/// -/// The bytes compared against are the host's: `stage` wrote them before the -/// boot. What the guest reports is which of the two things happened to them — -/// a refusal, or another block's data delivered as this one's. -pub fn usb_short_read( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["usb-storage-gate", "usb-short-read"]; - /// Mirrors `short_read::SHORT_BY`. One wire format with the kernel's, in - /// the same sense the stamp is: a change to either without the other stops - /// the line below matching rather than passing silently. - const SHORT_BY: u64 = 512; - - let (bytes, lba) = Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = test_dir().join("usb-short-read.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() - }, - )?; - gate_ran(&log, 2)?; - check_geometry(&log, bytes, lba)?; - - // The injection landed on the block the harness staged. Without this the - // assertions below are about a boot in which nothing was injected. - let block = block_of(bytes / BLOCK, HOST_BLOCKS[0]); - let staged = format!("usb-gate: short read of block {block} "); - let Some(verdict) = log.lines().find(|l| l.contains(&staged)) else { - return Err(format!("the guest never attempted the short read ({staged:?})\n{log}")); - }; - - // **The finding.** `refused=false` is the defect: the caller was handed - // bytes for a transfer the controller says did not finish, and `matched` - // says whether they were this block's. They are not — the window's tail - // still holds the block read into it before this one. - if !verdict.contains("refused=true") { - return Err(format!( - "{verdict:?} — a data phase the controller cut short reached the caller as data. \ - The bytes past {} of {BLOCK} are the previous read's, from a different LBA\n{log}", - BLOCK - SHORT_BY - )); - } - - // And the driver said so by name, with both numbers in it: a refusal with - // no line is a disk that stops working for no stated reason, which is what - // the machine this is for has no second channel to diagnose. - let named = format!("usb-storage: {} of {BLOCK} B at block {block}", BLOCK - SHORT_BY); - let shorts = log.matches(named.as_str()).count(); - if shorts != 1 { - return Err(format!( - "the driver named {shorts} short transfers ({named:?}); the injection is armed once, \ - so anything else is a transfer this test did not stage\n{log}" - )); - } - - // One refused read and nothing else disturbed: the rest of the sweep still - // passed and everything the guest wrote is where the host expects it. - if !log.contains("usb-gate: disk done reads=ok writes=ok refusal=true wr_err=0 healthy=true") { - return Err(format!("one short read cost the disk the rest of its sweep\n{log}")); - } - verify(&image, bytes, nonce)?; - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish\n{log}")); - } - let _ = std::fs::remove_file(&image); - - eprintln!( - " [usb] a data phase {SHORT_BY} B short with a CSW claiming none: refused by name, and \ - the {BLOCK}-byte window's stale tail never reached the caller" - ); - 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 @@ -915,2015 +57,49 @@ fn absent_probe( } } -/// 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(()) -} - -/// The kernel timestamp on the first line carrying `needle`, in seconds. -/// -/// `[kernel 0.218 cpu0] ...`, and `[kernel 1.042 cpu0 tid=3] ...` — the field -/// is in the same place either way. -fn stamp_of(log: &str, needle: &str) -> Result { - let line = log - .lines() - .find(|l| l.contains(needle)) - .ok_or_else(|| format!("no line carrying {needle:?}"))?; - let rest = line.split_once("[kernel ").ok_or("line has no kernel timestamp")?.1; - let secs = rest.split_once(' ').ok_or("timestamp is not followed by a field")?.0; - secs.parse::().map_err(|e| format!("timestamp {secs:?}: {e}")) -} - -/// 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(()) -} - -/// A bulk transfer that breaks **without halting the endpoint**, which is the -/// shape the recovery path had no answer for. -/// -/// The first metal boot with a working USB stack mounted `/boot` off a stick and -/// then lost it: a WRITE(10) broke, `clear_stall` opened with a Reset Endpoint -/// command, and the controller answered **completion code 19, Context State -/// Error** — twice, once per endpoint. xHCI 1.2 §4.6.8 defines Reset Endpoint -/// only for a Halted endpoint; §4.6.9's Stop Endpoint is the command for a -/// Running one. `reset_recovery` returned false, `dev.failed` was set, nothing -/// in this driver ever clears that flag, and the machine's own boot disk was -/// offline for the rest of the boot with `/boot/toyos/kernel.log` — the only -/// diagnostic channel that machine has — stopped where it stood. -/// -/// **The actuator is a boot parameter, and it replaces no verdict.** QEMU's -/// `usb-storage` answers every CBW, data phase and CSW it is handed; nothing on -/// the host side makes one bulk transfer not complete, and `rerror`/`werror` -/// fail a whole drive rather than leaving a transfer in flight. -/// `usb-transport-break` skips the *wait* on one data phase and nothing else: -/// the TRB is really on the ring, the endpoint is really left Running, and the -/// controller really completes the transfer afterwards. That is the state a -/// transfer which ran out `USB_TIMEOUT_NS` leaves behind, byte for byte, so the -/// recovery under test runs against a real endpoint state rather than a flag. -/// -/// **One injection is one abandoned transfer, and not one broken transfer.** The -/// device may answer that transfer after the driver has stopped listening, so -/// counting breaks is a count of who won a race. What the driver owes either way -/// is what is asserted here: the device, owed a WRITE's data, is sent no class -/// reset and no command block before its port has been reset, it answers under -/// its own tag after that, and the caller's write survives. -/// -/// The assertion that decides it is host-side and is about bytes: **every** block -/// the guest was told to write is byte-correct in the backing file, the broken -/// one included. Before the recovery existed the disk is offline from the break -/// onward, so the nine-block run and the second guest block never leave the guest -/// at all; before the command was re-issued over the transport the recovery gave -/// back, the broken block is missing from the image and `wr_err` is not zero. -pub fn usb_transport_break( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = - &["usb-storage-gate", "usb-transport-break", "usb-port-gone", "usb-serial-short"]; - - let (bytes, lba) = Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = test_dir().join("usb-transport-break.img"); - let nonce = stage(&image, bytes); - let pcap = test_dir().join("usb-transport-break.pcap"); - let _ = std::fs::remove_file(&pcap); - - let log = boot_and_shutdown( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk, - kernel_params: PARAMS, - usb_images: vec![image.clone()], - usb_pcap: Some(pcap.clone()), - ..Default::default() - }, - )?; - gate_ran(&log, 2)?; - check_geometry(&log, bytes, lba)?; - serial_short_is_not_read(&log)?; - - // The injection reached the driver, and the driver said *what* broke. Both - // halves are assertions: without the first this is a boot with nothing - // injected, and without the second the log says only that something went - // wrong. - // - // **And it says which of the three silences it was.** This wait never - // started, so a message asserting the transfer budget would be a false - // number on the one channel the T14 has — a pulled stick and a staged skip - // both reading as a slow device sends a triage to the wrong shelf. - const BROKE: &str = "transport broke on SCSI 0x2a: a staged break skipped the data phase wait"; - let staged: Vec<&str> = log.lines().filter(|l| l.contains(BROKE)).collect(); - if staged.len() != 1 { - return Err(format!( - "the staged break happened {} times, want the one the injection arms per \ - boot; did it run?\n{log}", - staged.len() - )); - } - // **And every count below is about that one device.** The actuator abandons - // the first WRITE(10) of the boot, so the line above names the disk under - // test; this profile carries a second one — the stick the machine booted - // from — whose own transport can break in the same boot for reasons that - // have nothing to do with the injection. `log.matches("transport broke")` - // summed both, and on CI run 31684437719 (job 94397136494) the boot - // stick's clean status-phase recovery at 2.616 s — one break, one retry, - // `SCSI 0x35`, slot 1, 2.3 s after the gate had swept — pushed the total - // from the injected disk's real 2 to 3 and reddened a run in which the - // disk under test never left its budget. - let under_test = broke_on(staged[0])?; - - // And the driver got over it. Two attempts are explained by the fault — the - // fault itself, and the recovery the device's late answer to the abandoned - // transfer can undo — so a third that also breaks is the transport failing - // to come back rather than this test's doing. - // A counted break reads `break N of M running`; the read the gate stages - // with its port gone is a break the driver does not count, judged below. - let mine = format!("usb-storage: {under_test} transport broke"); - let breaks = - log.lines().filter(|l| l.contains(mine.as_str()) && l.ends_with(" running")).count(); - if breaks > 2 { - let all: Vec<&str> = log.lines().filter(|l| l.contains("transport broke")).collect(); - return Err(format!( - "{under_test}'s transport broke {breaks} times off one abandoned transfer, which \ - can undo one recovery and no more; every break this boot: {all:?}\n{log}" - )); - } - // Left counting every device on the machine on purpose, and it is not the - // shape above: a break the driver recovers from belongs to the disk it - // happened on, but a transport that never comes back takes the boot with it - // whichever disk it was. - if let Some(gave_up) = log.lines().find(|l| l.contains("times running; the transport is not")) { - return Err(format!("{gave_up:?}\n{log}")); - } - - // The endpoint state the recovery had to be chosen for, read out of the - // controller's own output device context. `Halted` here would mean the - // injection staged the other shape and everything below proves nothing. - // Scoped to the disk under test for the same reason the count is: another - // device's endpoint being found Running says nothing about this one's. - let recovered = format!("xHCI: {under_test} endpoint"); - let looked = |l: &&str| l.contains(", recovering") || l.contains(", stopping it before its port is reset"); - let states: Vec<&str> = log.lines().filter(looked).collect(); - if !states.iter().any(|l| l.contains(recovered.as_str()) && l.contains("is Running,")) { - return Err(format!( - "no endpoint of {under_test} was found Running after the break, so this is not the \ - non-halt shape: {states:?}\n{log}" - )); - } - - no_command_was_refused(&log)?; - - // **A device owed a WRITE's data is asked nothing on the Bulk-Out until its - // port has been reset.** The break left it holding a command block and - // waiting for bytes, where the class reset's own TEST UNIT READY would be - // 31 of them: the ladder enters at the port reset, says why, and the - // device's answer after it is what lets the write go out again. - let entered = format!( - "usb-storage: {under_test} is owed the data of the command that broke, so nothing can be \ - asked of it on the Bulk-Out: its port is reset with no class reset before it" - ); - let Some((_, climb)) = log.split_once(entered.as_str()) else { - return Err(format!("the driver never said {entered:?}\n{log}")); - }; - let mut rest = climb; - for (needle, also) in [ - ("reset while recovering (warm on a USB3 port)", ": reset, and the port is enabled"), - ("bulk pair added again: endpoint", " is Running, endpoint "), - ("the port reset took: addressed and configured again, the device answered TEST UNIT READY under its own tag", ""), - ("SCSI 0x2a completed after ", " break(s) running"), - ] { - let Some(line) = rest.lines().find(|l| l.contains(needle) && l.contains(also)) else { - return Err(format!("after the break, no line reads {needle:?} and {also:?}, in order\n{log}")); - }; - rest = rest.split_once(line).expect("the line came from this text").1; - } - // On the wire: the abandoned WRITE's command block is followed by no - // Bulk-Only reset and no other command block until the device has been - // configured again, and the first one after that is a TEST UNIT READY. - let requests = control_requests(&pcap)?; - let blocks = command_blocks(&pcap)?; - let rungs = every_port_reset_is_followed_by_a_test_unit_ready(&requests, &blocks)?; - if rungs == 0 { - return Err(format!("the disk was never configured again: {requests:02x?}\n{log}")); - } - let Some(abandoned) = blocks.iter().position(|(_, opcode)| *opcode == 0x2A) else { - return Err(format!("no WRITE(10) reached the wire: {blocks:02x?}\n{log}")); - }; - let (sent_before, _) = blocks[abandoned]; - let Some(configured_again) = requests - .iter() - .enumerate() - .skip(sent_before) - .find(|(i, _)| is_a_rungs_configuration(&requests, *i)) - .map(|(i, _)| i) - else { - return Err(format!("no port reset's SET_CONFIGURATION follows the abandoned WRITE\n{log}")); - }; - if let Some(reset) = requests[sent_before..configured_again].iter().find(|r| r[..2] == [0x21, 0xFF]) { - return Err(format!( - "{reset:02x?}: a Bulk-Only reset was sent at a device owed Data-Out bytes\n{log}" - )); - } - match blocks.get(abandoned + 1) { - Some((before, 0x00)) if *before > configured_again => {} - next => { - return Err(format!( - "the abandoned WRITE's command block was followed by {next:02x?}, want a TEST \ - UNIT READY after request {configured_again}; every block: {blocks:02x?}\n{log}" - )); - } - } - - // Not one write reported a failure, and the disk stayed online. Before the - // recovery existed this line reads `wr_err=3 healthy=false`; before the - // command was re-issued it reads `writes=bad ... wr_err=1`. - if !log.contains("usb-gate: disk done reads=ok writes=ok refusal=true wr_err=0 healthy=true") { - return Err(format!("a write did not survive one broken transfer\n{log}")); - } - - // And the bytes, which is the claim nothing in the guest can make for - // itself: every block the guest was told to write is in the backing file, - // the host's own blocks are unchanged, and the blocks either side of the - // run are still zero. - verify(&image, bytes, nonce)?; - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish after the break\n{log}")); - } - port_gone_is_left_to_the_teardown(&log, under_test)?; - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - for file in [&image, &pcap] { - let _ = std::fs::remove_file(file); - } - - eprintln!( - " [usb] a bulk transfer abandoned mid-flight on {under_test}: the endpoint was found \ - Running and stopped rather than reset, no class reset and no command block reached a \ - device owed data, the port reset brought its transport back in {breaks} break(s) of \ - the {} this boot, and every block the guest was told to write verified host-side", - log.matches("transport broke").count() - ); - - a_read_whose_first_wait_spent_its_budget_goes_out_again(test_config, c_bins, rust_bins)?; - transport_gives_up(test_config, c_bins, rust_bins)?; - abandoned_write_is_taken_offline(test_config, c_bins, rust_bins)?; - a_stick_its_reset_moved_carries_on(Moved::SameStick)?; - a_stick_its_reset_moved_carries_on(Moved::AnotherStick)?; - a_stick_its_reset_moved_carries_on(Moved::SlowStick)?; - a_stick_its_reset_moved_carries_on(Moved::OwedFlush)?; - a_stick_its_reset_moved_carries_on(Moved::FlushedStick)?; - a_stick_its_reset_moved_carries_on(Moved::SilentReturn)?; - super::power::transport_break_chain() -} - -/// 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"; - -/// A READ whose first wait spends its operation's whole budget, then a class -/// reset the device answers out of step, then a port reset that takes: the -/// READ goes out again on what the call has left and returns the host's bytes. -fn a_read_whose_first_wait_spent_its_budget_goes_out_again( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["usb-storage-gate", "usb-first-wait-spent"]; - let (bytes, _) = Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = test_dir().join("usb-first-wait-spent.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() - }, - )?; - gate_ran(&log, 2)?; - let want = "usb-gate: a first wait that spent the operation's budget, a recovery out of step \ - and a port reset: read refused=false matched=true untaken=0 probes_untaken=0 \ - healthy=true"; - if !log.contains(want) { - let got = log.lines().find(|l| l.contains("a first wait that spent")); - return Err(format!("the gate read {got:?}, want {want:?}\n{log}")); - } - let broke = line_with(&log, "transport broke on SCSI 0x28: no answer in the ")?; - let under_test = broke_on(broke)?; - let (_, after) = log.split_once(broke).expect("the line came from this text"); - let mut rest = after; - for needle in [ - format!("usb-storage: {under_test} transport broke on the class reset's TEST UNIT READY"), - format!("usb-storage: {under_test} the port reset took"), - format!("usb-storage: {under_test} SCSI 0x28 completed after 2 break(s) running"), - ] { - let Some(line) = rest.lines().find(|l| l.contains(needle.as_str())) else { - return Err(format!("after the break, no line reads {needle:?}, in order\n{log}")); - }; - rest = rest.split_once(line).expect("the line came from this text").1; - } - if let Some(line) = after.lines().find(|l| l.contains(" not issued")) { - return Err(format!("{line:?}: the command the recovery was for was not sent again\n{log}")); - } - verify(&image, bytes, nonce)?; - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - let _ = std::fs::remove_file(&image); - eprintln!( - " [usb] {under_test}: a READ whose wait spent the operation's budget went out again \ - after the port reset took, and returned the host's bytes" - ); - Ok(()) -} - -/// `usb-serial-short` asks each disk for its serial number string in 8 bytes, -/// fewer than QEMU's string carries: the bytes past what was delivered are the -/// zeroed buffer, and a driver that read them would take a string the device -/// never sent whole for the unit's name. Every disk this boot binds says it -/// could not read one. -fn serial_short_is_not_read(log: &str) -> Result<(), String> { - let said: Vec<&str> = - log.lines().filter(|l| l.contains("usb-storage: slot ") && l.contains(" serial number ")).collect(); - if said.is_empty() { - return Err(format!("no disk said what its serial number was\n{log}")); - } - if let Some(read) = said.iter().find(|l| !l.ends_with(" serial number named and not read")) { - return Err(format!("{read:?}: a serial number that arrived short was read\n{log}")); - } - Ok(()) -} - -/// The CPU a kernel record was written on. -fn cpu_of(line: &str) -> Result { - line.split_once("[kernel ") - .and_then(|(_, rest)| rest.split_whitespace().nth(1)) - .and_then(|cpu| cpu.trim_end_matches(']').strip_prefix("cpu")) - .and_then(|n| n.parse().ok()) - .ok_or_else(|| format!("{line:?} names no CPU")) -} - -/// The one line carrying `needle`, as the log wrote it. -fn line_with<'a>(log: &'a str, needle: &str) -> Result<&'a str, String> { - log.lines().find(|l| l.contains(needle)).ok_or_else(|| format!("no line reads {needle:?}\n{log}")) -} - -/// What the host plugs in on another port once the port rung's reset has -/// emptied the boot stick's. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -enum Moved { - /// The same backing under the same serial number: the stick itself, as a - /// reset can move a stick from the USB2 half of its receptacle to the USB3 - /// half. - SameStick, - /// The same backing under another serial number, which is everything a - /// second unit of the same model shares with the first — INQUIRY, - /// capacity, USB ids — and the negative control: it must not be adopted. - AnotherStick, - /// The stick itself, whose bind `usb-slow-return` stalls past both the - /// write's operation budget and the disk's window: the call held for it - /// ends on its own bound on its own CPU, the bind runs on another, and the - /// disk is still taken back, since the enumeration began inside its window. - SlowStick, - /// The stick itself, broken by `usb-transport-break-owed` on a write that - /// went out while an earlier one was reported complete and not flushed: it - /// is taken back, and the flush of the writer whose write that was fails, - /// since a device that comes back is not known to have kept its cache. - OwedFlush, - /// The stick itself, broken by `usb-transport-break-flushed` on the first - /// write after a flush that succeeded over the one before it: it is taken - /// back owing nothing, since that flush emptied the cache it left with. - FlushedStick, - /// The stick itself, which `usb-return-silent` has come back late in the - /// held call and answer nothing on the operation sent again on it: every - /// wait of it spins to its end. - SilentReturn, -} - -/// The boot stick's first WRITE(10) is abandoned, the ladder resets its port, -/// and the device leaves that port and binds on another. **QEMU cannot move a device on a reset**, so `usb-reset-moves` -/// holds the port rung's reset until the port reads empty and the host makes -/// the move: `device_del` of the stick, then the same backing file plugged in -/// on port 3 with the serial number the move says. -/// -/// The same stick takes its disk number back, the write that broke goes out -/// again on it and completes, and the job the root volume carries runs to its -/// reset with nothing failed on disk 0. Another stick is refused by name, bound -/// as a new disk, and disk 0 is lost when its window ends — as every disk whose -/// device left was before. -/// -/// **The call held for the stick only waits.** It spins with `IF` clear, so -/// the bind is another CPU's, read off the kernel's own stamps. -fn a_stick_its_reset_moved_carries_on(moved: Moved) -> Result<(), String> { - const HELD: &str = "is held empty for the host to move its device (usb-reset-moves)"; - const MOVE_NOW: &str = "usb-reset-moves: move the device now"; - const STALLED: &str = "answers slowly (usb-slow-return): its bind is stalled"; - const OWED: &str = ", and it left owing a flush of writes it had reported complete, so the \ - flush of each writer whose writes they were fails"; - const FLUSH_LOST: &str = - "made before its disk came back owing a flush may not have survived, and its flush says so"; - const STILL_HELD: &str = "is still held when this call may wait no longer"; - const AFTER_A_FLUSH: &str = "breaks next (usb-transport-break-flushed)"; - const SILENT: &str = "answers nothing on the operation sent again on it (usb-return-silent)"; - const LATE: &str = "comes back late (usb-return-silent): its bind is stalled"; - const WENT_OUT_AGAIN: &str = "usb-storage: disk 0 is back, and the operation it was asked went \ - out again on it"; - let params: &'static [&'static str] = match moved { - Moved::SameStick | Moved::AnotherStick => &["usb-transport-break", "usb-reset-moves"], - Moved::SlowStick => &["usb-transport-break", "usb-reset-moves", "usb-slow-return"], - Moved::OwedFlush => &["usb-transport-break-owed", "usb-reset-moves"], - Moved::FlushedStick => &["usb-transport-break-flushed", "usb-reset-moves"], - Moved::SilentReturn => &["usb-transport-break", "usb-reset-moves", "usb-return-silent"], - }; - let case = super::compile::repo_root().join("tests/jobcase"); - let (name, serial) = match moved { - Moved::SameStick => ("usb-reset-moves-same.img", qemu::BOOT_STICK_SERIAL), - Moved::AnotherStick => ("usb-reset-moves-another.img", "TOYOS0OTHERSTICK"), - Moved::SlowStick => ("usb-reset-moves-slow.img", qemu::BOOT_STICK_SERIAL), - Moved::OwedFlush => ("usb-reset-moves-owed.img", qemu::BOOT_STICK_SERIAL), - Moved::FlushedStick => ("usb-reset-moves-flushed.img", qemu::BOOT_STICK_SERIAL), - Moved::SilentReturn => ("usb-reset-moves-silent.img", qemu::BOOT_STICK_SERIAL), - }; - let image = test_dir().join(name); - std::fs::write(&image, qemu::build_boot_image(&case, &[], &[], params)) - .map_err(|e| format!("write {}: {e}", image.display()))?; - // The stick's one writer at the break is `/log`: logd's first batch is - // the first WRITE(10) that goes out owing a flush. - let log_told = { - let mut file = std::fs::File::open(&image).map_err(|e| format!("{}: {e}", image.display()))?; - let guid = toyos_build::image::unique_guid_of(&mut file, toyos_gpt::Guid::MICROSOFT_BASIC)?; - format!( - "writes a process's claim of partition {} made before its disk came back owing a flush", - toyos_gpt::Guid(guid) - ) - }; - let mut qemu = QemuInstance::boot_with_options( - &case, - &[], - &[], - BootOptions { - profile: Profile::Metal, - qmp: true, - kernel_params: params, - // The held call sees the stick back, and the job resets after it, - // only if a CPU in no call on the held disk binds it: these boots - // put two CPUs in calls on it. - smp: if matches!(moved, Moved::SilentReturn | Moved::FlushedStick) { 4 } else { 2 }, - boot_image: Some(qemu::Staged::Written(image.clone())), - 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!("{moved:?}: the port rung's reset was never held for the move\n{log}")); - } - let mut devices = qemu::QmpDevices::open(qemu.qmp_socket()); - devices.del(qemu::BOOT_STICK_ID); - devices.blockdev_add_again("moved", &image); - devices.add("usb-storage", "xhci.0", "movedstick", &[("drive", "moved"), ("port", "3"), ("serial", serial)]); - drop(devices); - // The last line each shape's verdict reads, and the end of the held call, - // which may come on either side of it. - let ends = |c: &str| { - let last = match moved { - Moved::SameStick | Moved::SlowStick | Moved::FlushedStick => { - c.contains(toyos_build::bootlog::REBOOTING) - } - Moved::AnotherStick => c.contains(" did not come back within "), - Moved::OwedFlush => c.contains(FLUSH_LOST), - Moved::SilentReturn => c.contains(&format!("{WENT_OUT_AGAIN}: it failed")), - }; - last && (c.contains(WENT_OUT_AGAIN) || c.contains(STILL_HELD)) - }; - qemu::await_guest(&mut qemu, &mut log, "the moved stick's last line", ends) - .map_err(|why| format!("{moved:?}: {why}\n{log}"))?; - drop(qemu); - let _ = std::fs::remove_file(&image); - - let staged = log - .lines() - .find(|l| l.contains("transport broke on SCSI 0x2a: a staged break skipped the data phase wait")) - .ok_or_else(|| format!("{moved:?}: the staged break never happened\n{log}"))?; - let under_test = broke_on(staged)?; - let (_, after) = log.split_once(staged).expect("the line came from this text"); - let in_order = |needles: &[String]| -> Result<(), String> { - let mut rest = after; - for needle in needles { - let Some((_, tail)) = rest.split_once(needle.as_str()) else { - return Err(format!("{moved:?}: after the break, no line reads {needle:?}, in order\n{log}")); - }; - rest = tail; - } - Ok(()) - }; - let left = [ - format!("usb-storage: {under_test} is owed the data of the command that broke"), - HELD.to_string(), - // Its port read empty inside the rung, or — when the host's move came - // after the rung's bound and the reset verified — disconnected at the - // next look: run 79's shape and run 74's, both a device that left - // under a reset of this driver's. - "usb-storage: disk 0 left port 1 (".to_string(), - " after this driver reset it; it is held ".to_string(), - ]; - let inside_the_rung = log.contains("usb-storage: disk 0 left port 1 (its port read empty)"); - let back = [ - "xHCI: port 3 connected".to_string(), - format!(" serial number \"{serial}\""), - "usb-storage: disk 0 came back on port 3 slot ".to_string(), - "its volume carries on".to_string(), - ]; - // The call its transport broke in: the first line on the break's CPU that - // ends a held call on disk 0 — a call on another CPU may find the disk - // held too, and ends its own. - let staged_cpu = cpu_of(staged)?; - let held_end = after - .lines() - .find(|l| { - (l.contains(WENT_OUT_AGAIN) || l.contains(STILL_HELD)) - && cpu_of(l).is_ok_and(|cpu| cpu == staged_cpu) - }) - .ok_or_else(|| format!("{moved:?}: the call held on cpu{staged_cpu} never ended\n{log}"))?; - // Where it ended, and when. A bind while it held ran on another CPU; one - // after it ended may run on any. - let held_call = |bind: &str| -> Result<(), String> { - let line = held_end; - let ended = stamp_of(line, "[kernel ")?; - let (call_cpu, bind_cpu) = (cpu_of(line)?, cpu_of(line_with(&log, bind)?)?); - let during = stamp_of(&log, bind)? <= ended; - if during && call_cpu == bind_cpu { - return Err(format!( - "{moved:?}: the stick was bound on cpu{bind_cpu} while the call held for it spun \ - there with IF clear\n{log}" - )); - } - eprintln!( - " [usb] {moved:?}: the held call ended on cpu{call_cpu}; the stick was bound on \ - cpu{bind_cpu}, {} it ended", - if during { "before" } else { "after" } - ); - Ok(()) - }; - let came_back = "usb-storage: disk 0 came back on port 3 slot "; - match moved { - Moved::SameStick | Moved::SlowStick | Moved::FlushedStick => { - if moved == Moved::FlushedStick { - let flushed = line_with(&log, AFTER_A_FLUSH)?; - if !log.split_once(staged).is_some_and(|(before, _)| before.contains(flushed)) { - return Err(format!("{moved:?}: the break was not the one after a flush\n{log}")); - } - } - // First, and the debt first of all, so a debt the stick came back - // with is named as one. - for never in [ - OWED, - FLUSH_LOST, - " failed on disk 0", - " did not come back within ", - "disk 1 ready", - " is not disk 0 come back", - ] { - if let Some(line) = log.lines().find(|l| l.contains(never)) { - return Err(format!("{moved:?}: {line:?} of a stick that came back as itself\n{log}")); - } - } - let mut want = left.to_vec(); - want.push(back[0].clone()); - if moved == Moved::SlowStick { - want.push(STALLED.to_string()); - } - want.extend(back[1..].iter().cloned()); - want.push(toyos_build::bootlog::REBOOTING.to_string()); - in_order(&want)?; - // Two shapes, both the ruling's: the held call saw the stick bound - // and sent its write again; or it ended on its bound first — the - // stall, or every CPU inside a call on the held disk, so no CPU - // took the pass that binds — and the write was asked again. - let shape = if moved != Moved::SlowStick && held_end.ends_with(": it completed") { - in_order(&[left[3].clone(), came_back.to_string(), held_end.to_string()])?; - held_call(came_back)?; - "the write that waited went out again on it" - } else if held_end.contains(STILL_HELD) { - in_order(&[left[3].clone(), held_end.to_string()])?; - held_call(if moved == Moved::SlowStick { STALLED } else { came_back })?; - "the call that waited ended on its bound and the write was asked again" - } else { - return Err(format!("{moved:?}: {held_end:?} of a stick that came back as itself\n{log}")); - }; - if !log.contains("Boot: complete") { - return Err(format!("{moved:?}: the boot never completed\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - eprintln!( - " [usb] {under_test}'s port reset moved the boot stick to port 3 ({}); it bound \ - as disk 0 again on its serial number, {shape}, and the job on the root volume it \ - carries ran to its reset{}", - if inside_the_rung { - "inside the rung" - } else { - "after the rung's reset verified" - }, - match moved { - Moved::SlowStick => ", though its bind was stalled past the window", - Moved::FlushedStick => { - ", owing no flush: it broke after a flush that succeeded over its last write" - } - _ => "", - } - ); - } - Moved::OwedFlush => { - let mut want = left.to_vec(); - want.extend(back.iter().cloned()); - want.push(OWED.to_string()); - want.push(log_told); - in_order(&want)?; - for never in [" did not come back within ", "disk 1 ready", " is not disk 0 come back"] { - if let Some(line) = log.lines().find(|l| l.contains(never)) { - return Err(format!("{moved:?}: {line:?}\n{log}")); - } - } - eprintln!( - " [usb] a stick that left owing a flush was taken back as disk 0" - ); - } - Moved::SilentReturn => { - let mut want = left.to_vec(); - want.push(back[0].clone()); - want.push(LATE.to_string()); - want.extend(back[1..].iter().cloned()); - want.extend([ - format!("usb-storage: disk 0 {SILENT}"), - " is offline: both bulk endpoints Stopped=".to_string(), - format!("{WENT_OUT_AGAIN}: it failed"), - ]); - in_order(&want)?; - // The stick came back late enough that the operation sent again on - // a bound of its own would run past the call's: on the call's, - // its waits reach where the last rung begins, and no further. - if !held_end.contains(&format!("{WENT_OUT_AGAIN}: it failed")) { - return Err(format!( - "{moved:?}: {held_end:?}: the call held on cpu{staged_cpu} did not send its \ - operation again on the stick that came back\n{log}" - )); - } - held_call(came_back)?; - eprintln!( - " [usb] a stick that came back and answered nothing on the operation sent again \ - on it went offline on the last rung" - ); - } - Moved::AnotherStick => { - let mut refused = left.to_vec(); - refused.extend([ - "usb-storage: the device on port 3 is not disk 0 come back: its serial number differs" - .to_string(), - "usb-storage: disk 1 ready on slot ".to_string(), - "usb-storage: disk 0 did not come back within 2000 ms of its port reset; it is \ - offline" - .to_string(), - ]); - in_order(&refused)?; - for never in ["usb-storage: disk 0 came back", "usb-storage: disk 0 is back"] { - if let Some(line) = log.lines().find(|l| l.contains(never)) { - return Err(format!("{line:?}: another stick was taken for disk 0\n{log}")); - } - } - eprintln!( - " [usb] a stick with another serial number arrived on port 3 while disk 0 was \ - held: it bound as disk 1, and disk 0 was lost when its window ended" - ); - } - } - Ok(()) -} - -/// The ladder's last rung, with the rung before it spent: -/// `usb-transport-offline` leaves every rung's TEST UNIT READY unsent and -/// unanswered for the whole of that rung's bound. The port reset — every step -/// of it answered — may not say it took, the write is never re-issued, and the -/// last rung still runs whole: both endpoints stopped, the port reset and -/// waited for, the controller told, the slot back. -/// -/// **T14 run 77 is this boot with one budget for all of it**: two waits of the -/// whole timeout left the give-up nothing, so it stopped no endpoint and waited -/// for no reset. The clock is read here so the staging is known to have spent -/// what it claims. -fn abandoned_write_is_taken_offline( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["usb-storage-gate", "usb-transport-break", "usb-transport-offline"]; - let (bytes, _) = Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = test_dir().join("usb-transport-offline.img"); - stage(&image, bytes); - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk, - kernel_params: PARAMS, - usb_images: vec![image.clone()], - ..Default::default() - }, - ); - let mut log = qemu.boot_log().to_string(); - // The slot goes back from the poll, which the boot log may end before. - let deadline = std::time::Instant::now() + Duration::from_secs(20); - while std::time::Instant::now() < deadline - && !log.split_once(" is offline: ").is_some_and(|(_, after)| after.contains(" disabled")) - { - log.push_str(&qemu.drain_serial(Duration::from_millis(250))); - } - 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); - - let staged = log - .lines() - .find(|l| l.contains("transport broke on SCSI 0x2a: a staged break skipped the data phase wait")) - .ok_or_else(|| format!("the staged break never happened\n{log}"))?; - let under_test = broke_on(staged)?; - let spent = "the bound on this part of the call ran out during the status phase"; - let rungs: Vec = [("port reset", 2)] - .iter() - .map(|(rung, next)| { - format!( - "usb-storage: {under_test} transport broke on the {rung}'s TEST UNIT READY: \ - {spent}; break {next} of 3 running" - ) - }) - .collect(); - let mut rest = log.as_str(); - for rung in &rungs { - let Some((_, after)) = rest.split_once(rung.as_str()) else { - return Err(format!("no line reads {rung:?}, in that order\n{log}")); - }; - rest = after; - } - for never in [ - format!("usb-storage: {under_test} Reset Recovery took"), - format!("usb-storage: {under_test} the port reset took"), - format!("usb-storage: {under_test} SCSI 0x2a completed"), - format!("xHCI: {under_test} bulk pair dropped and added"), - ] { - if log.contains(never.as_str()) { - return Err(format!("{never:?} of a device that never answered a rung\n{log}")); - } - } - - // The last rung, after both of those: the reset waited for and seen, the - // controller told, the slot back — and no step of it refused for want of - // anything to spend. - let last_reset = format!("xHCI: {under_test} port "); - let Some(reset) = rest.lines().find(|l| { - l.contains(last_reset.as_str()) && l.contains("reset while taking it offline (warm on a USB3 port)") - }) else { - return Err(format!("the last rung reset no port\n{log}")); - }; - if !reset.ends_with(": reset, and the port is enabled") { - return Err(format!("{reset:?}: the last rung's reset was not seen to complete\n{log}")); - } - let offline = format!( - "usb-storage: {under_test} is offline: both bulk endpoints Stopped=true, port " - ); - let said = rest - .lines() - .find(|l| l.contains(offline.as_str())) - .ok_or_else(|| format!("no {offline:?} line after the rungs: the disk was left online\n{log}"))?; - for did in ["reset=true and nothing sent after it, Reset Device=true", "its slot goes back"] { - if !said.contains(did) { - return Err(format!("{said:?} does not read {did:?}\n{log}")); - } - } - let (_, since_the_break) = log.split_once(staged).expect("the line came from this text"); - for starved in [" not issued: ", "not sent: ", " not issued again: "] { - if let Some(line) = since_the_break.lines().find(|l| l.contains(starved)) { - return Err(format!("{line:?}: a step was refused for want of a bound to run on\n{log}")); - } - } - // Nothing reaches the device after the reset that ends it. - let (_, after_offline) = rest.split_once(said).expect("the line came from this text"); - for sent in [format!("xHCI: {under_test} endpoint"), format!("usb-storage: {under_test} transport broke")] { - if after_offline.contains(sent.as_str()) { - return Err(format!("{sent:?} after {under_test} was taken offline\n{log}")); - } - } - let slot = under_test.rsplit(' ').next().expect("a slot id ends the name"); - if !after_offline.contains(&format!("xHCI: slot {slot} disabled")) { - return Err(format!("slot {slot} never went back after the give-up\n{log}")); - } - - no_command_was_refused(&log)?; - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish after the give-up\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - let _ = std::fs::remove_file(&image); - eprintln!( - " [usb] {under_test}: a port reset that never verified left the last rung whole: {}", - said.split_once("is offline: ").map_or("", |(_, rest)| rest) - ); - Ok(()) -} - -/// `run_command` logs only failures, so each of these lines is the controller -/// refusing a command the driver should not have sent, or a recovery that -/// could not be completed. -fn no_command_was_refused(log: &str) -> Result<(), String> { - for illegal in [ - "Reset Endpoint failed", - "Stop Endpoint failed", - "Set TR Dequeue failed", - "Configure Endpoint (the bulk pair dropped and added) failed", - "Configure Endpoint (the bulk pair added after the port reset) failed", - "Reset Device failed", - "Address Device (after the port reset) failed", - " was not answered; break ", - ] { - if log.contains(illegal) { - return Err(format!( - "{illegal:?}: the recovery did not pick a command the endpoint's state \ - permits\n{log}" - )); - } - } - Ok(()) -} - -/// 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. pub fn transport_break_on_metal( kernel: &serial::Serial, - after: &serial::Serial, -) -> Result<(), String> { - transport_break_recovered(kernel)?; - super::power::done_chain(after) -} - -/// The kernel log's half of [`transport_break_on_metal`]. -/// -/// **The ladder enters at the port reset**: the break leaves the stick owed a -/// WRITE's data, across which no class reset may be asked. The stick decides -/// the rest. It answers the rung's TEST UNIT READY on its port, and the write -/// goes out again there; or it leaves its port under the reset — a SuperSpeed -/// stick enumerated on the USB2 half of its receptacle trains on the USB3 half -/// — is held, comes back as the same device, and the write goes out again on -/// it. **Either way no rung takes it offline.** -pub fn transport_break_recovered(kernel: &serial::Serial) -> Result<(), String> { - 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"), - )?; - kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?; - if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") { - let back = kernel.must_say_after(left, " as the same device (USB ")?; - kernel.must_say_after( - back, - "is back, and the operation it was asked went out again on it: it completed", - )?; - eprintln!(" [usb] {left}"); - eprintln!(" [usb] {back}"); - return Ok(()); - } - let took = kernel.must_say_after(entered, &format!("usb-storage: {under_test} the port reset took"))?; - kernel.must_say_after(took, &format!("usb-storage: {under_test} SCSI 0x2a completed after "))?; - eprintln!(" [usb] {took}"); - Ok(()) -} - -/// A read whose port reads gone is a break the driver does not recover: no -/// command, no class reset and no port reset is aimed at a device that is not -/// on the bus, and the disk is left to the teardown its port owes. -/// -/// Staged, because a real pull lands on a transfer in flight only by timing: -/// the gate's last read ends as `wait_transfer` ends one whose port read -/// disconnected, with nothing queued. -fn port_gone_is_left_to_the_teardown(log: &str, under_test: &str) -> Result<(), String> { - let gone = format!( - "usb-storage: {under_test} transport broke on SCSI 0x28: the port disconnected during \ - the command phase; its port's teardown takes it from here" - ); - let Some((_, after)) = log.split_once(gone.as_str()) else { - return Err(format!("the driver never said {gone:?}; did the staging run?\n{log}")); - }; - for acted in [ - format!("xHCI: {under_test} endpoint"), - format!("usb-storage: {under_test} is offline"), - format!("usb-storage: {under_test} Reset Recovery"), - ] { - if after.contains(acted.as_str()) { - return Err(format!("{acted:?} after the port read gone: the driver acted on it\n{log}")); - } - } - let read = "usb-gate: a read whose port reads gone: refused=true untaken=0 healthy=false"; - if !log.contains(read) { - let got = log.lines().find(|l| l.contains("a read whose port reads gone")); - return Err(format!("the gate read {got:?}, want {read:?}\n{log}")); - } - Ok(()) -} - -/// Every control request the capture's device was sent, as its eight setup -/// bytes, in the order the bus carried them. -/// -/// The file is QEMU's own record (`hw/usb/pcap.c`): a pcap of link type 220, -/// each packet led by usbmon's 64-byte header, little-endian as the host wrote -/// it. A submission (`'S'`) on a control endpoint (transfer type 2) whose -/// `flag_setup` is zero carries its setup packet at byte 40. -fn control_requests(pcap: &Path) -> Result, String> { - let bytes = std::fs::read(pcap).map_err(|e| format!("{}: {e}", pcap.display()))?; - let word = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().expect("four bytes")); - if bytes.len() < 24 || word(0) != 0xA1B2_C3D4 || word(20) != 220 { - return Err(format!("{} is not a usbmon capture ({} B)", pcap.display(), bytes.len())); - } - let mut requests = Vec::new(); - let mut at = 24; - while at + 16 <= bytes.len() { - let captured = word(at + 8) as usize; - let packet = at + 16; - if packet + captured > bytes.len() { - return Err(format!("{}: a packet at {at} runs off the file", pcap.display())); - } - if captured >= 48 && bytes[packet + 8] == b'S' && bytes[packet + 9] == 2 && bytes[packet + 14] == 0 - { - requests.push(bytes[packet + 40..packet + 48].try_into().expect("eight bytes")); - } - at = packet + captured; - } - Ok(requests) -} - -/// The opcode of every command block the capture's device was sent, each with -/// how many control requests the bus had carried before it. -/// -/// A submission on a bulk endpoint (transfer type 3) going out, 31 bytes long -/// and opening with the CBW signature (BOT 1.0 §5.1); the opcode is the first -/// byte of the command block, at byte 15. -fn command_blocks(pcap: &Path) -> Result, String> { - let bytes = std::fs::read(pcap).map_err(|e| format!("{}: {e}", pcap.display()))?; - let word = |at: usize| u32::from_le_bytes(bytes[at..at + 4].try_into().expect("four bytes")); - let mut blocks = Vec::new(); - let mut requests = 0; - let mut at = 24; - while at + 16 <= bytes.len() { - let captured = word(at + 8) as usize; - let packet = at + 16; - if packet + captured > bytes.len() { - return Err(format!("{}: a packet at {at} runs off the file", pcap.display())); - } - if captured >= 64 && bytes[packet + 8] == b'S' { - let data = &bytes[packet + 64..packet + captured]; - match bytes[packet + 9] { - 2 if bytes[packet + 14] == 0 => requests += 1, - 3 if bytes[packet + 10] & 0x80 == 0 && data.len() == 31 && data[..4] == *b"USBC" => { - blocks.push((requests, data[15])); - } - _ => {} - } - } - at = packet + captured; - } - Ok(blocks) -} - -/// The recovery's own question, on the wire: the first command block the disk -/// is sent after every Bulk-Only reset and its two clears is a TEST UNIT READY, -/// so no command with a buffer reaches a device that has not answered one. -fn every_reset_is_followed_by_a_test_unit_ready( - requests: &[[u8; 8]], - blocks: &[(usize, u8)], -) -> Result<(), String> { - for (i, request) in requests.iter().enumerate() { - if request[..2] != [0x21, 0xFF] { - continue; - } - // The reset is request `i`, so its clears are `i + 1` and `i + 2` and - // a block sent after them has `i + 3` requests before it. - match blocks.iter().find(|(before, _)| *before >= i + 3) { - Some((_, 0x00)) => {} - next => { - return Err(format!( - "the Bulk-Only reset at request {i} was followed by the command block \ - {next:02x?}, want a TEST UNIT READY; every block: {blocks:02x?}" - )); - } - } - } - Ok(()) -} - -/// Whether request `i` is a port rung's SET_CONFIGURATION: an enumeration -/// reads the configuration descriptor it chooses the value from after the -/// configuration before it, and a rung reads none — string reads, the -/// firmware's or a bind's, can stand between either and its request. -fn is_a_rungs_configuration(requests: &[[u8; 8]], i: usize) -> bool { - const SET_CONFIGURATION: [u8; 2] = [0x00, 0x09]; - const GET_CONFIGURATION_DESCRIPTOR: [u8; 4] = [0x80, 0x06, 0x00, 0x02]; - requests[i][..2] == SET_CONFIGURATION - && !requests[..i] - .iter() - .rev() - .take_while(|r| r[..2] != SET_CONFIGURATION) - .any(|r| r[..4] == GET_CONFIGURATION_DESCRIPTOR) -} - -/// The port reset's own question, on the wire: the first command block the -/// disk is sent after every SET_CONFIGURATION the ladder's second rung sent it -/// is a TEST UNIT READY. Answers how many there were. -/// -/// The capture opens with the firmware's own enumeration of the disk, and an -/// enumeration's SET_CONFIGURATION is told from a rung's by -/// [`is_a_rungs_configuration`]. -fn every_port_reset_is_followed_by_a_test_unit_ready( - requests: &[[u8; 8]], - blocks: &[(usize, u8)], -) -> Result { - let mut rungs = 0; - for i in 0..requests.len() { - if !is_a_rungs_configuration(requests, i) { - continue; - } - rungs += 1; - match blocks.iter().find(|(before, _)| *before > i) { - Some((_, 0x00)) => {} - next => { - return Err(format!( - "the port reset's SET_CONFIGURATION at request {i} was followed by the \ - command block {next:02x?}, want a TEST UNIT READY; every block: {blocks:02x?}" - )); - } - } - } - Ok(rungs) -} - -/// BOT 1.0 §5.3.4 on the wire: every Bulk-Only Mass Storage Reset the disk was -/// sent is followed, with nothing between, by a ClearFeature(ENDPOINT_HALT) to -/// an IN endpoint and then one to an OUT endpoint. Answers how many. -fn every_reset_is_followed_by_both_clears(requests: &[[u8; 8]]) -> Result { - const CLASS_RESET: [u8; 2] = [0x21, 0xFF]; - let halt_cleared = |r: &[u8; 8]| (r[..4] == [0x02, 0x01, 0x00, 0x00]).then_some(r[4]); - let mut resets = 0; - for (i, request) in requests.iter().enumerate() { - if request[..2] != CLASS_RESET { - continue; - } - resets += 1; - let cleared = [requests.get(i + 1), requests.get(i + 2)].map(|r| r.and_then(halt_cleared)); - match cleared { - [Some(first), Some(second)] if first & 0x80 != 0 && second & 0x80 == 0 => {} - other => { - return Err(format!( - "Bulk-Only reset {resets} was followed by clears of {other:02x?}, want the \ - Bulk-In then the Bulk-Out; every control request the disk was sent: \ - {requests:02x?}" - )); - } - } - } - Ok(resets) -} - -/// The transport's recovery judged on what it does and not on what it plans: -/// runs of breaks it brings a disk back from, a run it gives up on, and the -/// same give-up inside a bind. -/// -/// The gate stages, on a disk that answers every well-formed command: one -/// break short of the budget by a CBW the device refuses, which QEMU's -/// `usb-storage` answers with a STALL of the Bulk-Out — Bulk-In Running, -/// Bulk-Out Halted; the same count by a data phase with no CBW before it, which -/// it answers with a STALL of the Bulk-In — the pair the other way round; then -/// one refused CBW whose class reset's own TEST UNIT READY is refused too, a -/// rung the device answered without being in step after it; then the whole -/// budget. Every run's second break climbs to the port reset, which addresses -/// and configures the disk again under the number it has, and the third takes -/// it offline behind one more reset. The second run only passes if the read -/// that ended the first cleared the count. Then a disk is plugged in whose -/// INQUIRY is refused the whole budget over, so it climbs the same ladder with -/// its port not yet told which slot it holds. -/// -/// **What judges the recovery is outside the kernel where it can be.** The -/// three requests, the SET_CONFIGURATION of every port reset, and the TEST UNIT -/// READY that is the first command block after either, are read off QEMU's own -/// capture of the disk's traffic, so a driver that clears only the pipe its -/// controller halted, or sends a command with a buffer to a device that has -/// not answered one without, reds on the wire whatever it prints. -/// The Drop and Add are a command and reach no wire: their -/// judge is the controller's output context, which calls both endpoints Running -/// with no doorbell rung — a state only an Add leaves. QEMU models no data -/// toggle, no sequence number and no device that is a phase ahead of its host, -/// so that the clears *resynchronise* a device is not judged here at all. -fn transport_gives_up( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - const PARAMS: &[&str] = &["usb-storage-gate", "usb-transport-faults"]; - /// What the driver says of a CBW the device stalled; the code is the - /// controller's own word for it (xHCI 1.2 Table 6-90). - const STALLED_CBW: &str = "command phase completion code 6 (Stall Error)"; - const STALLED_CSW: &str = "status phase completion code 6 (Stall Error)"; - - let (bytes, lba) = Profile::UsbDisk.usb_disk().expect("UsbDisk declares a disk"); - let image = test_dir().join("usb-transport-faults.img"); - let nonce = stage(&image, bytes); - let late = test_dir().join("usb-transport-faults-late.img"); - drop(sparse(&late, 64 * 1024 * 1024)); - let pcap = test_dir().join("usb-transport-faults.pcap"); - let _ = std::fs::remove_file(&pcap); - - let mut qemu = QemuInstance::boot_with_options( - test_config, - c_bins, - rust_bins, - BootOptions { - profile: Profile::UsbDisk, - kernel_params: PARAMS, - usb_images: vec![image.clone()], - usb_pcap: Some(pcap.clone()), - qmp: true, - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - let mut plugged = String::new(); - let wait_for = |qemu: &mut QemuInstance, plugged: &mut String, line: &str| { - let deadline = std::time::Instant::now() + Duration::from_secs(20); - while std::time::Instant::now() < deadline && !plugged.contains(line) { - plugged.push_str(&qemu.drain_serial(Duration::from_millis(250))); - } - }; - // The offline disk's slot goes back from the poll, which the boot log may - // end before: it is waited for, so the pull below is not what gave it back. - let deadline = std::time::Instant::now() + Duration::from_secs(20); - let gone_back = |text: &str| { - text.split_once(" is offline: ").is_some_and(|(_, after)| after.contains(" disabled")) - }; - while std::time::Instant::now() < deadline && !gone_back(&format!("{boot}{plugged}")) { - plugged.push_str(&qemu.drain_serial(Duration::from_millis(250))); - } - // It is pulled before the next disk is plugged, so the plug's port and - // slot are the late disk's alone. - let mut devices = qemu::QmpDevices::open(qemu.qmp_socket()); - devices.del(&qemu::usb_device_id(0)); - drop(devices); - wait_for(&mut qemu, &mut plugged, "it is offline"); - let mut devices = qemu::QmpDevices::open(qemu.qmp_socket()); - devices.blockdev_add("latedisk", &late); - devices.add("usb-storage", "xhci.0", "latedisk0", &[("drive", "latedisk")]); - drop(devices); - // The bind breaks on stalls, which cost no timeout; what is waited for is - // the line that ends it. - wait_for(&mut qemu, &mut plugged, "would not answer INQUIRY"); - // And the poll after it, which is where the refused slot goes back. - let inquiry = plugged.find("would not answer INQUIRY").unwrap_or(0); - qemu::await_guest(&mut qemu, &mut plugged, "the refused slot to go back", |c| { - c[inquiry..].lines().any(|l| l.contains("xHCI: slot ") && l.ends_with(" disabled")) - })?; - writeln!(qemu.stdin_mut(), "run shutdown").expect("write to QEMU stdin"); - qemu.flush_stdin(); - let log = format!("{boot}{plugged}{}", 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}")); - } - } - gate_ran(&boot, 2)?; - check_geometry(&boot, bytes, lba)?; - // The gate leaves its disk offline and still registered, so ROOT's hold - // reads a table that does not answer: it holds ROOT off the boot stick and - // names that disk, rather than refusing the boot over it. - let Some(held) = boot.lines().find(|l| l.contains("root: holding ")) else { - return Err(format!("the boot never held the partition ROOT was read from\n{log}")); - }; - let Some(gate) = boot.lines().find_map(|l| { - l.split_once("usb-gate: disk ")?.1.split_once(" designated")?.0.parse::().ok() - }) else { - return Err(format!("the gate never said which disk it designated\n{log}")); - }; - // A USB disk's `DeviceId` is 16 past its index (`drivers/usb_storage.rs`). - let silent = format!("disks that did not answer: [{}]", 16 + gate); - if !held.ends_with(&silent) { - return Err(format!("{held:?}: ROOT's hold did not name the gate's disk alone, {silent:?}\n{log}")); - } - - // The budget is the kernel's declaration, read off the gate's own line. - let Some(budget) = boot - .lines() - .find_map(|l| l.split_once(" bad CBW signatures in a row of a budget of ")?.1.split(':').next()) - .and_then(|n| n.parse::().ok()) - else { - return Err(format!("the gate never said what the transport's budget is\n{log}")); - }; - let short = budget - 1; - - // The two runs the recovery brings back: the read returned the host's - // bytes, and the disk is healthy after each. - for shape in ["bad CBW signatures", "withheld CBWs"] { - let want = format!( - "usb-gate: {short} {shape} in a row of a budget of {budget}: read refused=false \ - matched=true untaken=0 healthy=true" - ); - if !boot.contains(&want) { - let got = boot.lines().find(|l| l.contains(shape) && l.contains("read refused=")); - return Err(format!("the gate read {got:?}, want {want:?}\n{log}")); - } - } - - // The run whose first recovery the device answered without being in step: - // the read still returned the host's bytes, and both stagings were taken. - let want = "usb-gate: a bad CBW signature and then a recovery out of step: read refused=false \ - matched=true untaken=0 probes_untaken=0 healthy=true"; - if !boot.contains(want) { - let got = boot.lines().find(|l| l.contains("a recovery out of step")); - return Err(format!("the gate read {got:?}, want {want:?}\n{log}")); - } - - // Every staged fault reached the device and the device answered as staged, - // on one disk, each break counted as the driver's running count says: two - // runs that stop one short, the one whose recovery was out of step, and one - // that spends the budget. - let broke: Vec<&str> = boot - .lines() - .filter(|l| l.contains("transport broke on SCSI 0x28") && l.ends_with(" running")) - .collect(); - let want_counts: Vec = - (1..=short).chain(1..=short).chain(1..=1).chain(1..=budget).collect(); - if broke.len() != want_counts.len() { - return Err(format!( - "{} counted break(s), want the {} the gate staged; did it run?\n{log}", - broke.len(), - want_counts.len() - )); - } - let under_test = broke_on(broke[0])?; - for (i, (line, count)) in broke.iter().zip(&want_counts).enumerate() { - if broke_on(line)? != under_test { - return Err(format!("a staged fault landed on another device: {line:?}\n{log}")); - } - let stall = if (short..2 * short).contains(&i) { STALLED_CSW } else { STALLED_CBW }; - let counted = format!("break {count} of {budget} running"); - if !line.contains(stall) || !line.contains(&counted) { - return Err(format!("{line:?} does not read {stall:?} and {counted:?}\n{log}")); - } - } - // The recovery that was out of step is the break between the read's one - // and the read's completion: counted, said in the recovery's own words, and - // followed by a recovery that took. - let out_of_step = format!( - "usb-storage: {under_test} transport broke on the class reset's TEST UNIT READY: \ - {STALLED_CBW}; break 2 of {budget} running" - ); - if boot.matches(out_of_step.as_str()).count() != 1 { - return Err(format!("want {out_of_step:?} once\n{log}")); - } - let came_back = |breaks: usize| { - format!( - "usb-storage: {under_test} SCSI 0x28 completed after {breaks} break(s) running; the \ - transport came back and the count is cleared" - ) - }; - // Twice after `short` breaks, once per shape, and once after the two of the - // run that was out of step — which is the same line where `short` is two. - let mut want_back = vec![(short, 2)]; - match want_back.iter_mut().find(|(breaks, _)| *breaks == 2) { - Some((_, times)) => *times += 1, - None => want_back.push((2, 1)), - } - for (breaks, times) in want_back { - let line = came_back(breaks); - if boot.matches(line.as_str()).count() != times { - return Err(format!("want {line:?} {times} time(s)\n{log}")); - } - } - - // The pair each break left, Bulk-In first as the driver prints them, read - // off the two lines the driver's look at it printed next: both shapes were - // really staged, neither a repeat of the other. - let looked = format!("xHCI: {under_test} endpoint"); - let lines: Vec<&str> = boot.lines().collect(); - for (i, line) in broke.iter().enumerate() { - let at = lines.iter().position(|l| l == line).expect("the line came from this text"); - let pair: Vec<&str> = lines[at + 1..] - .iter() - .filter(|l| l.contains(looked.as_str())) - .take(2) - .filter_map(|l| l.split_once(" is ")?.1.split(',').next()) - .collect(); - let want = - if (short..2 * short).contains(&i) { ["Halted", "Running"] } else { ["Running", "Halted"] }; - if pair != want { - return Err(format!("{line:?} left the bulk pair {pair:?}, want {want:?}\n{log}")); - } - } - // Four runs, and each climbed both rungs once: the two that came back - // after `short` breaks, the one whose class reset was out of step, and the - // one that spent the budget. - let runs = 4; - if short != 2 { - return Err(format!("the ladder has {budget} rungs and this gate counts its climbs for 3")); - } - // The Drop and Add of every class reset and the Add of every port reset, - // in the controller's word: both endpoints Running with no doorbell rung. - for (did, want) in [("dropped and added", runs), ("added again", runs)] { - let made = format!("xHCI: {under_test} bulk pair {did}: endpoint"); - let running = boot - .lines() - .filter(|l| l.contains(made.as_str()) && l.matches(" is Running").count() == 2) - .count(); - if running != want { - return Err(format!("{running} time(s) the pair was {did} and left Running, want {want}\n{log}")); - } - } - // Every rung says it took only after the device's own answer: every class - // reset but the one staged out of step, and every port reset. - for (took, want) in [ - (format!("usb-storage: {under_test} Reset Recovery took: the device answered TEST UNIT READY"), runs - 1), - (format!("usb-storage: {under_test} the port reset took: addressed and configured again, the device answered TEST UNIT READY"), runs), - ] { - if boot.matches(took.as_str()).count() != want { - return Err(format!("want {took:?} {want} times\n{log}")); - } - } - - // The three requests of every class reset and the configuration of every - // port reset, on the wire, each followed by the rung's question. - let requests = control_requests(&pcap)?; - let blocks = command_blocks(&pcap)?; - let resets = every_reset_is_followed_by_both_clears(&requests)?; - every_reset_is_followed_by_a_test_unit_ready(&requests, &blocks)?; - if resets != runs { - return Err(format!( - "the disk was sent {resets} Bulk-Only reset(s), want {runs}: {requests:02x?}\n{log}" - )); - } - let configured = every_port_reset_is_followed_by_a_test_unit_ready(&requests, &blocks)?; - if configured != runs { - return Err(format!( - "the disk was configured again {configured} time(s), want {runs} port resets': \ - {requests:02x?}\n{log}" - )); - } - - // The give-up, what it did to the device, and that nothing reached the - // device after it. - let gave_up = format!( - "usb-storage: {under_test} broke {budget} times running; its port reset did not bring \ - the transport back" - ); - let Some((_, after)) = boot.split_once(gave_up.as_str()) else { - return Err(format!("the driver never said {gave_up:?}\n{log}")); - }; - let offline = format!("usb-storage: {under_test} is offline: "); - let Some(said) = after.lines().find(|l| l.contains(offline.as_str())) else { - return Err(format!("no {offline:?} line after the give-up: the disk was left online\n{log}")); - }; - // The disk under test trains SuperSpeed, so the most its port has is a - // warm reset. - let last_reset = format!("xHCI: {under_test} port "); - if !after.lines().any(|l| { - l.contains(last_reset.as_str()) - && l.contains("reset while taking it offline (warm on a USB3 port)") - && l.ends_with(": reset, and the port is enabled") - }) { - return Err(format!("the last rung's reset was not seen to complete\n{log}")); - } - for did in [ - "both bulk endpoints Stopped=true", - "reset=true and nothing sent after it, Reset Device=true", - "its slot goes back", - ] { - if !said.contains(did) { - return Err(format!("{said:?} does not read {did:?}\n{log}")); - } - } - let (_, after_offline) = after.split_once(said).expect("the line came from this text"); - let asked_again = format!("usb-storage: {under_test} transport broke"); - if after_offline.contains(asked_again.as_str()) || after_offline.contains(looked.as_str()) { - return Err(format!("a command reached {under_test} after it was taken offline\n{log}")); - } - let read = format!( - "usb-gate: {budget} bad CBW signatures in a row of a budget of {budget}: read \ - refused=true untaken=0 the read after it refused=true healthy=false" - ); - if !boot.contains(&read) { - let got = boot.lines().find(|l| l.contains("the read after it")); - return Err(format!("the gate read {got:?}, want {read:?}\n{log}")); - } - if !boot.contains("usb-gate: disk done reads=ok writes=ok refusal=true wr_err=0 healthy=false") { - return Err(format!("the gate's sweep before the faults did not survive them\n{log}")); - } - - // The same budget spent inside a bind. The port is the one the plug named, - // the slot is given back once — by the enumeration that fails — and the - // controller is never asked for a slot it already has back. - let inquiries: Vec<&str> = plugged - .lines() - .filter(|l| l.contains("transport broke on SCSI 0x12") && l.contains(STALLED_CBW)) - .collect(); - if inquiries.len() != budget { - return Err(format!( - "the plugged disk's INQUIRY broke {} time(s), want {budget}\n{log}", - inquiries.len() - )); - } - let binding = broke_on(inquiries[0])?; - let Some(port) = plugged - .lines() - .filter(|l| l.contains(" connected")) - .find_map(|l| l.split_once("xHCI: port ")?.1.split([' ', ',']).next()) - else { - return Err(format!("no port said the plugged disk connected\n{log}")); - }; - let offline = format!( - "usb-storage: {binding} is offline: both bulk endpoints Stopped=true, port {port} \ - reset=true" - ); - if !plugged.contains(&offline) { - let got = plugged.lines().find(|l| l.contains("is offline: ")); - return Err(format!("the bind's give-up read {got:?}, want {offline:?}\n{log}")); - } - // One more than the budget was staged, so the bind's own disarm has one to - // take back and say so. - let staged = format!("bound under {} staged INQUIRY fault(s): untaken=1", budget + 1); - if !plugged.contains(&staged) { - let got = plugged.lines().find(|l| l.contains("staged INQUIRY fault(s)")); - return Err(format!("the bind's staging read {got:?}, want {staged:?}\n{log}")); - } - let Some(gate_port) = said.split_once(", port ").and_then(|(_, rest)| rest.split(' ').next()) - else { - return Err(format!("{said:?} does not name the port it reset\n{log}")); - }; - // Two slots went back in this boot, each once, and the controller hands a - // freed slot id out again, so they are told apart by order. The gate's - // disk gave its own back while it was still plugged in: before its port - // said it had gone, which is the only time an unplug's teardown could have - // done it instead. - let slot_of = |name: &str| name.rsplit(' ').next().expect("a slot id ends the name").to_string(); - let want = [slot_of(under_test), slot_of(binding)].map(|slot| format!("xHCI: slot {slot} disabled")); - let pulled = format!("xHCI: port {gate_port} disconnected"); - match (log.find(want[0].as_str()), log.find(&pulled)) { - (Some(given_back), Some(gone)) if given_back < gone => {} - (given_back, gone) => { - return Err(format!( - "{:?} at {given_back:?} and {pulled:?} at {gone:?}: the offline disk's slot did \ - not go back while it was still plugged in\n{log}", - want[0] - )); - } - } - let disabled: Vec<&str> = log - .lines() - .filter_map(|l| l.find("xHCI: slot ").map(|at| &l[at..])) - .filter(|l| l.ends_with(" disabled")) - .collect(); - let bind_gave_up = log.find(&offline).expect("found above"); - if disabled != want || log.rfind(want[1].as_str()).is_none_or(|at| at < bind_gave_up) { - return Err(format!( - "the slots given back read {disabled:?}, want {want:?} with the last after the \ - bind's give-up\n{log}" - )); - } - if log.contains("Disable Slot failed") { - return Err(format!("a slot was given back twice, or over a running endpoint\n{log}")); - } - no_command_was_refused(&log)?; + after: &serial::Serial, +) -> Result<(), String> { + transport_break_recovered(kernel)?; + super::power::done_chain(after) +} - // Every byte the gate wrote before the give-up is on the image, and the - // machine went on: the boot stick beside the disk is the one it runs from. - verify(&image, bytes, nonce)?; - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish after the give-up\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - for file in [&image, &late, &pcap] { - let _ = std::fs::remove_file(file); +/// The kernel log's half of [`transport_break_on_metal`]. +/// +/// **The ladder enters at the port reset**: the break leaves the stick owed a +/// WRITE's data, across which no class reset may be asked. The stick decides +/// the rest. It answers the rung's TEST UNIT READY on its port, and the write +/// goes out again there; or it leaves its port under the reset — a SuperSpeed +/// stick enumerated on the USB2 half of its receptacle trains on the USB3 half +/// — is held, comes back as the same device, and the write goes out again on +/// it. **Either way no rung takes it offline.** +pub fn transport_break_recovered(kernel: &serial::Serial) -> Result<(), String> { + 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"), + )?; + kernel.must_not_say(&format!("usb-storage: {under_test} is offline"))?; + if let Ok(left) = kernel.must_say_after(entered, " after this driver reset it; it is held ") { + let back = kernel.must_say_after(left, " as the same device (USB ")?; + kernel.must_say_after( + back, + "is back, and the operation it was asked went out again on it: it completed", + )?; + eprintln!(" [usb] {left}"); + eprintln!(" [usb] {back}"); + return Ok(()); } - - eprintln!( - " [usb] {under_test}: {short} refused CBWs and {short} withheld ones each came back by \ - the port reset (the wire carried {resets} Bulk-Only resets, each followed by both \ - clears and a TEST UNIT READY, and {configured} SET_CONFIGURATIONs, each followed by \ - one), one class reset out of step counted as a break, {budget} taken offline behind a \ - last reset with its slot given back; {binding} broke {budget} times inside its bind \ - on port {port} and its slot went back once" - ); + let took = kernel.must_say_after(entered, &format!("usb-storage: {under_test} the port reset took"))?; + kernel.must_say_after(took, &format!("usb-storage: {under_test} SCSI 0x2a completed after "))?; + eprintln!(" [usb] {took}"); Ok(()) } @@ -2945,756 +121,6 @@ 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 that attaches at **full speed**, where EP0's max packet size is a -/// thing only the device knows. -/// -/// Low, High and SuperSpeed each fix it at 8, 64 and 512, and every USB device -/// in this suite was one of those — so a driver that answered 64 for full speed -/// and read all 18 bytes of the device descriptor in one go passed everything -/// here. A T14's port 9 came up at speed 1 and answered -/// `GET_DESCRIPTOR(Config) failed, code=Some(4)` — USB Transaction Error — after -/// the driver had already logged `vendor=0000 product=0000` off a buffer no -/// transfer had filled. -/// -/// Two things are asserted and they are separate. The **sequence**: the driver -/// reads eight bytes, takes `bMaxPacketSize0` from them, and only then reads the -/// rest. The **error channel**: what it prints about a device is what the device -/// sent, so a read that delivered nothing can never be logged as a device whose -/// identifiers are zero. -/// -/// Ground truth is host-side in the sense that matters here — QEMU's descriptor -/// tables are the host's bytes and a guest cannot invent them. `usb-wacom-tablet` -/// is full-speed only: QEMU gives it a `.full` descriptor set and no `.high` one, -/// so `usb_desc_attach` has no faster speed to choose. -pub fn xhci_full_speed_device( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Each device's own `idVendor`, out of QEMU's descriptor tables: PenPartner - /// for the tablet, Gemalto for the reader. The host's bytes, and the thing in - /// a device descriptor a guest cannot have guessed. - const VENDORS: [&str; 2] = ["vendor=056a", "vendor=08e6"]; - /// What the reader answers to the eight-byte prefix, measured on QEMU - /// 11.0.2. The tablet answers 8 and so needs no correction — which is the - /// other half of the claim, because a driver that wrote a constant would - /// produce this line for both devices or for neither. - const CORRECTED: &str = "EP0 packet size 8 -> 64"; - - let options = BootOptions { - profile: Profile::MetalFullSpeed, - ..Default::default() - }; - // The claim is that full-speed devices are on the bus, and argv is where a - // device's presence is visible: no console line distinguishes "the driver - // did not enumerate it" from "it was never attached". - let argv = qemu::profile_argv(&options); - let usb = crate::usb_argv(&argv); - for want in ["usb-wacom-tablet", "usb-ccid"] { - if !usb.iter().any(|d| d.starts_with(want)) { - return Err(format!("this gate needs {want} on the bus, argv has {usb:?}")); - } - } - - let log = boot_and_shutdown(test_config, c_bins, rust_bins, options)?; - - // Speed 1 is Full Speed in PORTSC, and it is the premise of the whole test: - // on a bus of high- and SuperSpeed devices EP0's packet size is fixed by - // the specification and nothing below here has anything to measure. - let full_speed: Vec<&str> = log - .lines() - .filter(|l| l.contains("xHCI: port ") && l.contains("enabled, speed=1")) - .collect(); - if full_speed.len() != 2 { - return Err(format!( - "{} port(s) came up at full speed, want both: {full_speed:?}\n{log}", - full_speed.len() - )); - } - - // **The sequence.** 64 is a number the driver can only have got by reading - // the first eight bytes of the reader's device descriptor and issuing - // Evaluate Context with what it found — the eighth byte is `bMaxPacketSize0` - // and QEMU's `desc_device_ccid` is where this 64 comes from. Exactly one - // such line, because the tablet on the same bus answers 8: a driver that - // wrote a constant for full speed produces this line twice or not at all, - // and the shipped one produced it never. - let corrected: Vec<&str> = log.lines().filter(|l| l.contains("EP0 packet size")).collect(); - match corrected.as_slice() { - [only] if only.contains(CORRECTED) => {} - other => { - return Err(format!( - "want exactly one endpoint resized, to the {CORRECTED:?} the reader asked \ - for; got {other:?}\n{log}" - )); - } - } - - // **The error channel.** What the driver prints about a device is what the - // device sent. Both identities are the host's bytes; an all-zero one is what - // an unfilled buffer looks like, and it is what a T14 port printed off a - // transfer that had delivered no descriptor at all. - for vendor in VENDORS { - if !log.contains(vendor) { - return Err(format!( - "the driver never reported {vendor:?}; a device descriptor that was not \ - delivered must not be logged as one that was\n{log}" - )); - } - } - if log.contains("vendor=0000 product=0000") { - return Err(format!( - "a device was logged with an all-zero identity, which is what an unfilled \ - descriptor buffer looks like\n{log}" - )); - } - for wrong in ["GET_DESCRIPTOR(Device)", "GET_DESCRIPTOR(Config)", "code=Some("] { - if let Some(line) = log.lines().find(|l| l.contains(wrong)) { - return Err(format!("{line:?}\n{log}")); - } - } - - // And one came out the far end: a full-speed HID enumerated, bound, and took - // a button-merge source. `Enabled` is not `enumerated`, and neither is - // `addressed`. The reader is not a HID and is walked past by name. - let binds = crate::parse_xhci_binds(&log); - if binds.len() != 1 || binds[0].kind != "mouse" { - return Err(format!("want exactly the full-speed pointer bound, got {binds:?}\n{log}")); - } - if !log.contains("xHCI: no HID boot interface found") { - return Err(format!("the reader was not walked past\n{log}")); - } - if !log.contains("Boot: complete") { - return Err(format!("the boot did not finish\n{log}")); - } - - // The tablet stalls SET_PROTOCOL: QEMU's `usb-wacom-tablet` reports a boot - // protocol it will not select, and the driver binds it anyway. What it may - // not do is bind it with EP0 still halted, because a halted control - // endpoint runs no TRB — so a device whose interrupt endpoint later needs a - // CLEAR_FEATURE could never be recovered. This is the one bus in the suite - // where a device stalls a request the driver goes on from. - let stalled = log.lines().filter(|l| l.contains("SET_PROTOCOL")).count(); - if stalled != 1 { - return Err(format!( - "{stalled} stalled SET_PROTOCOL(s); this gate needs the tablet's one\n{log}" - )); - } - if !log.contains("runs again after the stall") { - return Err(format!("EP0 was left halted behind the stall\n{log}")); - } - serial::Serial::named("boot console", log.as_str()).must_be_clean()?; - - eprintln!( - " [xhci] two full-speed devices enumerated: one EP0 resized to 64 from the reader's \ - own bMaxPacketSize0 and the tablet's 8 left alone, both identities read off the wire, \ - and the tablet's stalled SET_PROTOCOL left EP0 running" - ); - 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(()) -} - /// The stick the machine booted from, pulled while the desktop is up. /// /// **The instrument for #152, and the reason it exists is that the failure has @@ -3720,12 +146,7 @@ pub fn xhci_scan_hands_over_a_free_slot( /// certifies that this shape of unplug, on this emulated controller, leaves the /// guest drawing and answering. What it *is* good for is red: a red here is the /// first reproduction of the owner's freeze anywhere but his desk. -pub fn usb_boot_stick_pulled( - test_config: &Path, - _c_bins: &[(String, Vec)], - _rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let _ = test_config; +pub fn usb_boot_stick_pulled() -> Result<(), String> { /// Probes sent before the pull, and after it, each once the one before it /// was answered. The drumbeat is the liveness signal as well as the load: /// each one is a userland `println!` into the ring the log sink drains to diff --git a/tests/common/volumes.rs b/tests/common/volumes.rs index db12e842490..22c433da97e 100644 --- a/tests/common/volumes.rs +++ b/tests/common/volumes.rs @@ -29,114 +29,9 @@ //! 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:"; - -/// Mirrored in `tests/toyos-rust-tests/src/bin/esp_files.rs`. Two halves of one -/// fixture; a change to either alone fails loudly rather than passing quietly. -const HOST_NOTE: &str = "host-note.txt"; -const HOST_TEXT: &str = "written by the host before this machine started\n"; -/// On the *log* volume, not the ESP: `/boot` is read-only toward userland, so -/// the guest's own writes go where it is allowed to put them. -const GUEST_NOTE: &str = "guest-note.txt"; -const GUEST_TEXT: &str = "written by ToyOS through the VFS\n"; -const GUEST_BLOB: &str = "guest-blob.bin"; -const BLOB_LEN: usize = 10 * 4096 + 137; - -fn blob() -> Vec { - (0..BLOB_LEN).map(|i| (i.wrapping_mul(97) ^ 0x5A) as u8).collect() -} - -/// The files the build put on the ESP, which the guest must not have touched. -/// `BOOTx64.EFI` is the one firmware reads and `log.guid` the one the -/// bootloader does. Damaging either makes the stick unbootable, so -/// "still byte-identical" is the assertion that matters most here. -const UNTOUCHED: [&str; 2] = ["EFI/BOOT/BOOTx64.EFI", "toyos/log.guid"]; - -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. @@ -162,1687 +57,6 @@ 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 -} - -pub fn esp_filesystem( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - let image_path = test_dir().join("esp-boot.img"); - 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 (start, len) = esp_extent(&image, &image_path)?; - - // The host's half of the fixture, put there before the machine exists. - { - let volume = &mut image[start..start + len]; - 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(HOST_NOTE) - .map_err(|e| format!("creating {HOST_NOTE} on the ESP: {e}"))?; - file.write_all(HOST_TEXT.as_bytes()) - .map_err(|e| format!("writing {HOST_NOTE}: {e}"))?; - } - std::fs::write(&image_path, &image).map_err(|e| format!("rewrite the boot image: {e}"))?; - - // What the build wrote, before the guest ever sees the volume. Read - // through `fatfs` rather than from the artifact files, so a byte the image - // builder mangled is not counted against the kernel. - let before = read_files(&image[start..start + len], &UNTOUCHED)?; - for (name, bytes) in UNTOUCHED.iter().zip(&before) { - if bytes.is_none() { - return Err(format!("the built image has no {name}")); - } - } - // Including the file this test just wrote through `fatfs` above: the - // fixture is part of what has to leave the volume clean, or the gate below - // could only ever be as strong as the untidiest thing on the stick. - let complaints_before = check(&image[start..start + len]); - if !complaints_before.is_empty() { - return Err(format!( - "the boot volume breaks the format before the guest has run:\n{}", - describe(&complaints_before) - )); - } - - // metal-sim, because that is the machine shape that gets flashed and the - // one whose whole reason for having a log on the stick is that it has no - // serial port. - 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 boot = qemu.boot_log().to_string(); - serial::Serial::named("boot console", boot.as_str()).must_be_clean()?; - if !boot.contains(BOOT_SERVED) { - return Err(format!( - "the kernel did not mount the boot partition:\n{}", - volume_lines(&boot) - )); - } - - let result = qemu.run_test("test_rs_esp_files", Duration::from_secs(60)); - if let Some(err) = &result.error { - return Err(format!("the guest stopped answering: {err}\nserial:\n{}", result.serial)); - } - if result.exit_code != Some(0) { - // **The kernel's own lines, not just the guest binary's.** Every failure - // this test can produce on the write path — `write_page`, `set_len`, - // `flush_meta`, the FSInfo write, the device cache flush — reaches - // userland as one `SyscallError::Io`, which `std` flattens to - // `Kind(Other)`; *which* layer refused is in a `log!` line and nowhere - // else (`usb_storage`'s three trait methods, - // `xhci::wait::msc`'s `log_refusal` and its budget line). Reporting - // `stdout` alone left `fsync the blob: Kind(Other)` as the whole of the - // evidence for a real 2026-08-21 sighting, and the next author with no - // more to go on than the one before. - return Err(format!( - "esp_files failed:\n{}\nkernel log while the test ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - serial::Serial::named("test serial", result.serial.as_str()).must_be_clean()?; - for line in result.stdout.lines().filter(|l| l.contains("PASS")) { - eprintln!(" [esp]{}", line.trim_start_matches(" PASS")); - } - - // The shutdown is not politeness: it is what makes the host's view of the - // backing file the device's view of 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); - 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}"))?; - if after.len() != image.len() { - return Err(format!("the image is {} bytes, was {}", after.len(), image.len())); - } - let volume = &after[start..start + len]; - - // The strongest claim first: the volume is still a volume. A driver that - // wrote the right file into a broken FAT would pass every byte comparison - // below and leave a stick that cannot boot. - // Silence, not sameness. While the image builder left complaints of its own - // on every volume it wrote, all this could ask was that the guest add none - // — which would have hidden a complaint the guest produced for its own - // reason inside the ones it did not. - let complaints_after = check(volume); - if !complaints_after.is_empty() { - return Err(format!( - "the guest left the boot volume breaking the format:\n{}", - describe(&complaints_after) - )); - } - eprintln!(" [esp] the volume checker is silent on the boot volume before and after the boot"); - - // Everything the host has to say about the boot volume, in one mount: what - // the guest must not have left behind, and what it must not have touched. - // Nothing on this volume is the guest's to write — that is what the mount - // policy says and what `esp_files` attacked from inside. - let wanted: Vec = [ - format!("toyos/{HOST_NOTE}"), - "toyos/new-file.txt".to_string(), - "toyos/moved.txt".to_string(), - "toyos/link".to_string(), - ] - .into_iter() - .chain(UNTOUCHED.iter().map(|s| s.to_string())) - .collect(); - let refs: Vec<&str> = wanted.iter().map(String::as_str).collect(); - let mut found = read_files(volume, &refs)?.into_iter(); - - // The host's note, unchanged. A refusal that deleted the file first would - // still satisfy the absences below. - let got = need(found.next().flatten(), HOST_NOTE)?; - if got != HOST_TEXT.as_bytes() { - return Err("the guest changed the host's note".to_string()); - } - for absent in ["toyos/new-file.txt", "toyos/moved.txt", "toyos/link"] { - if found.next().flatten().is_some() { - return Err(format!("{absent} reached the boot volume; a refusal wrote to it")); - } - } - - // The assertion this test exists for. A guest test once wrote five bytes - // over the kernel through the VFS; `esp_files` tries that on the loader, and - // this is where the answer comes from — the image the device received, - // not the guest's opinion of what it did. - for (name, want) in UNTOUCHED.iter().zip(&before) { - let got = need(found.next().flatten(), name)?; - if Some(&got) != want.as_ref() { - return Err(format!( - "{name} is {} bytes on the volume and was {} — the boot stick has been damaged", - got.len(), - want.as_ref().map_or(0, Vec::len) - )); - } - } - eprintln!(" [esp] {} build artifacts byte-identical after the guest's attempts", UNTOUCHED.len()); - - // The write direction, on the volume the guest is allowed to have. Same - // adapter and same driver, so the refusals above cost the FAT32 write path - // no coverage. - let (log_start, log_len) = log_extent(&after, &image_path)?; - let log = &after[log_start..log_start + log_len]; - let mut found = read_files(log, &[GUEST_NOTE, GUEST_BLOB, "doomed.txt", "link"])?.into_iter(); - - let got = need(found.next().flatten(), GUEST_NOTE)?; - if got != GUEST_TEXT.as_bytes() { - return Err(format!( - "{GUEST_NOTE} on the log volume is {:?}, not what the guest wrote", - String::from_utf8_lossy(&got) - )); - } - let got = need(found.next().flatten(), GUEST_BLOB)?; - if got.len() != BLOB_LEN { - return Err(format!("{GUEST_BLOB} is {} bytes on the volume, wrote {BLOB_LEN}", got.len())); - } - if let Some(at) = got.iter().zip(blob()).position(|(a, b)| *a != b) { - return Err(format!("{GUEST_BLOB} differs from what the guest wrote at byte {at}")); - } - - // A deleted file, and the symlink FAT32 cannot hold. Both are the half a - // read-back-what-you-wrote test cannot see. - for absent in ["doomed.txt", "link"] { - if found.next().flatten().is_some() { - return Err(format!("{absent} is still on the log volume")); - } - } - - let _ = std::fs::remove_file(&image_path); - eprintln!(" [esp] {BLOB_LEN} bytes and two files verified host-side on the log volume"); - Ok(()) -} - -/// 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: 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"; - if !boot.contains(opened) { - boot.push_str(&qemu.drain_until(Duration::from_secs(10), |line| line.contains(opened))); - } - if !boot.contains(opened) { - return Err(format!("logd never opened a file:\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. The other caller of that config is -/// `usb_boot_stick_pulled`, which wants the same rotation in flight for a -/// different reason. -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(()) -} - -/// A `FatBacking` handed out before an unlink reads nothing after it, and the -/// delete-and-reallocate cycle leaves the volume a volume. -/// -/// `FatFs::delete` frees the file's clusters, and FSInfo's `next_free` is walked -/// down to the lowest one freed — so the next allocation on the volume takes -/// them. Until `FatFs::revoke` existed, a process holding a descriptor across -/// somebody else's `rm` demand-paged whatever went into those clusters next. -/// The guest (`test_rs_fat_backing_revoked`) stages exactly that and asserts the -/// read is **refused**. -/// -/// This is the **independent oracle**, and it answers the two questions the -/// guest cannot ask about itself: -/// -/// - **were the clusters really reissued?** The attacker file is read off the -/// image by the `fatfs` crate and must hold its own bytes end to end, and the -/// victim's name must be gone from the directory. A run in which the volume -/// simply had room elsewhere is not a run in which the refusal proved -/// anything, and only the host's own FAT implementation can say. -/// - **did the cycle break the format?** `toyos-fat32-check` (fatgen103) reads -/// the volume after it, against a partition asserted clean before the boot. -/// A revocation that also corrupted the FAT would pass every assertion the -/// guest can make and leave a stick that does not boot. -pub fn fat_backing_revoked( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Mirrored in `tests/toyos-rust-tests/src/bin/fat_backing_revoked.rs`. - const VICTIM: &str = "fat-revoke-victim.bin"; - const ATTACKER: &str = "fat-revoke-attacker.bin"; - const CONTROL: &str = "fat-revoke-control.bin"; - const LEN: usize = 8 * 4096; - const VICTIM_BYTE: u8 = 0xA7; - const ATTACKER_BYTE: u8 = 0x5C; - - let image_path = test_dir().join("fat-backing-revoked.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)?; - - // Born clean, asserted rather than assumed, so a complaint after the run is - // the guest's and not one it inherited. - 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) - )); - } - - 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 boot = qemu.boot_log().to_string(); - serial::Serial::named("boot console", boot.as_str()).must_be_clean()?; - if !boot.contains(LOG_SERVED) { - return Err(format!( - "the log partition did not mount, so the guest had nowhere to stage the unlink:\n{}", - volume_lines(&boot) - )); - } - - let result = qemu.run_test("test_rs_fat_backing_revoked", Duration::from_secs(60)); - if let Some(err) = &result.error { - return Err(format!("the guest stopped answering: {err}\nserial:\n{}", result.serial)); - } - if result.exit_code != Some(0) { - // The kernel's own lines too: the refusal reaches userland as one `Io`, - // and which layer refused is only in a `log!`. - return Err(format!( - "fat_backing_revoked guest failed:\n{}\nkernel log while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - serial::Serial::named("test serial", result.serial.as_str()).must_be_clean()?; - - 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}"))?; - if after.len() != image.len() { - return Err(format!("the image is {} bytes, was {}", after.len(), image.len())); - } - let volume = &after[start..start + len]; - - // The strongest claim first: the volume is still a volume. A revocation that - // freed the chain wrongly would pass every byte comparison below and leave a - // stick that cannot boot. - let complaints_after = check(volume); - if !complaints_after.is_empty() { - return Err(format!( - "the unlink-and-reallocate cycle left the log volume breaking the format:\n{}", - describe(&complaints_after) - )); - } - - let mut files = read_files(volume, &[VICTIM, ATTACKER, CONTROL])?; - let control = need(files.pop().flatten(), CONTROL)?; - let attacker = need(files.pop().flatten(), ATTACKER)?; - if files.pop().flatten().is_some() { - return Err(format!( - "{VICTIM} is still on the volume after the guest unlinked it — the delete never \ - reached the directory, so nothing about a reissued cluster was staged" - )); - } - - for (name, bytes, want) in - [(ATTACKER, &attacker, ATTACKER_BYTE), (CONTROL, &control, VICTIM_BYTE)] - { - if bytes.len() != LEN { - return Err(format!( - "{name} is {} bytes on the volume; the guest wrote {LEN}", - bytes.len() - )); - } - if let Some(at) = bytes.iter().position(|&b| b != want) { - return Err(format!( - "{name} holds {:#04x} at byte {at} on the volume, not {want:#04x} — the host's \ - own FAT implementation does not see the file the guest wrote", - bytes[at] - )); - } - } - - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [fat] the victim is gone from the volume, the {LEN}-byte file written into its place \ - holds {ATTACKER_BYTE:#04x} end to end on the host's own reader, and the checker is silent" - ); - 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(()) -} - -/// F6's negative control, and its host-side oracle: after the guest's racing -/// rounds the file is read off the image by the host's own FAT implementation -/// — every slot must be on the device, `toyos-fat32-check` silent. The guest -/// reds first on a kernel that clears a mid-flush redirty; this half reds if -/// what the guest read back was the cache's word and not the device's. -pub fn redirty_mid_flush( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // The shipped eviction code under a 64-page budget: without it the file's - // pages stay resident all boot and the read-back never asks the device. - const PARAMS: &[&str] = &["test-small-caches"]; - /// Mirrored in `tests/toyos-rust-tests/src/bin/redirty_mid_flush.rs`. - const TARGET: &str = "redirty.bin"; - const ROUNDS: u64 = 128; - const SLOTS_AT: usize = 64; - - let image_path = test_dir().join("redirty-mid-flush.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 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) - )); - } - - 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 boot = qemu.boot_log().to_string(); - serial::Serial::named("boot console", boot.as_str()).must_be_clean()?; - if !boot.contains(LOG_SERVED) { - return Err(format!( - "the log partition did not mount, so the race had nowhere to run:\n{}", - volume_lines(&boot) - )); - } - - let result = qemu.run_test("test_rs_redirty_mid_flush", Duration::from_secs(120)); - if let Some(err) = &result.error { - return Err(format!("the guest stopped answering: {err}\nserial:\n{}", result.serial)); - } - if result.exit_code != Some(0) { - return Err(format!( - "redirty_mid_flush guest failed — a write racing a flush was lost:\n{}\nkernel log \ - while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - serial::Serial::named("test serial", result.serial.as_str()).must_be_clean()?; - - 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 volume = &after[start..start + len]; - let complaints_after = check(volume); - if !complaints_after.is_empty() { - return Err(format!( - "the racing rounds left the log volume breaking the format:\n{}", - describe(&complaints_after) - )); - } - let got = need(read_files(volume, &[TARGET])?.pop().flatten(), TARGET)?; - for slot in 0..ROUNDS { - let at = SLOTS_AT + slot as usize * 8; - let held = got - .get(at..at + 8) - .map(|b| u64::from_le_bytes(b.try_into().expect("8 bytes"))) - .ok_or_else(|| format!("{TARGET} is {} bytes on the volume, short of slot {slot}", got.len()))?; - if held != slot { - return Err(format!( - "slot {slot} holds {held} on the device — a write the guest confirmed durable \ - is not in the bytes the host reads" - )); - } - } - - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [f6] {ROUNDS} racing rounds survived in-guest; all {ROUNDS} slots on the device by \ - the host's own reader, checker silent" - ); - Ok(()) -} - -/// A rename whose source is absent leaves the destination on the FAT `/log` -/// volume, and `rename(p, p)` leaves the file — the **independent oracle** for -/// the same reason `fat_backing_revoked` is one. The guest -/// (`test_rs_fs_rename_durable`) stages both; after the shutdown drain the two -/// files are read off the image by the `fatfs` crate and must hold their bytes -/// end to end, and `toyos-fat32-check` reads the whole volume against a -/// partition asserted clean before the boot — a rename that freed the -/// destination's clusters first would fail both, and every guest assertion none. -pub fn fs_rename_durable( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Mirrored in `tests/toyos-rust-tests/src/bin/fs_rename_durable.rs`. - const VICTIM: &str = "fstx-rename-victim.bin"; - const SELFED: &str = "fstx-rename-self.bin"; - const LEN: usize = 5 * 4096 + 33; - fn payload() -> Vec { - (0..LEN).map(|i| (i.wrapping_mul(97) ^ 0x5A) as u8).collect() - } - - let image_path = test_dir().join("fs-rename-durable.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)?; - - // Born clean, asserted rather than assumed, so a complaint after the run is - // the guest's and not one it inherited. - 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) - )); - } - - 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 boot = qemu.boot_log().to_string(); - serial::Serial::named("boot console", boot.as_str()).must_be_clean()?; - if !boot.contains(LOG_SERVED) { - return Err(format!( - "the log partition did not mount, so the guest had nowhere to stage the rename:\n{}", - volume_lines(&boot) - )); - } - - let result = qemu.run_test("test_rs_fs_rename_durable", Duration::from_secs(60)); - if let Some(err) = &result.error { - return Err(format!("the guest stopped answering: {err}\nserial:\n{}", result.serial)); - } - if result.exit_code != Some(0) { - // The kernel's own lines too: the refusal reaches userland as one error, - // and which layer refused is only in a `log!`. - return Err(format!( - "fs_rename_durable guest failed:\n{}\nkernel log while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - serial::Serial::named("test serial", result.serial.as_str()).must_be_clean()?; - - 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}"))?; - if after.len() != image.len() { - return Err(format!("the image is {} bytes, was {}", after.len(), image.len())); - } - let volume = &after[start..start + len]; - - // The strongest claim first: the volume is still a volume, so a wrongly - // freed cluster is caught here and not only by the byte comparison below. - let complaints_after = check(volume); - if !complaints_after.is_empty() { - return Err(format!( - "the staged renames left the log volume breaking the format:\n{}", - describe(&complaints_after) - )); - } - - let want = payload(); - let mut files = read_files(volume, &[VICTIM, SELFED])?; - for (name, got) in [(SELFED, files.pop().flatten()), (VICTIM, files.pop().flatten())] { - let got = need(got, name)?; - if got.len() != LEN { - return Err(format!("{name} is {} bytes on the volume; the guest staged {LEN}", got.len())); - } - if let Some(at) = got.iter().zip(&want).position(|(a, b)| a != b) { - return Err(format!( - "{name} differs on the volume from what the guest staged at byte {at} — the \ - host's own FAT reader does not see the file the guest left" - )); - } - } - - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [fat] a rename with an absent source left its {LEN}-byte destination intact and \ - rename(p, p) left its file, both end to end on the host's own reader, and the checker \ - is silent" - ); - Ok(()) -} - - -/// Directories on `/log` are real FAT32 directories — the **independent -/// oracle** for the directory work, in the shape `fs_rename_durable` set: the -/// guest (`test_rs_fs_dirs_durable`) makes one with `mkdir`, empties and -/// removes another the mount grew for a file's path, and after the shutdown -/// drain the image is read back by the `fatfs` crate — the kept directory must -/// be a real, empty directory, the removed one gone — while `toyos-fat32-check` -/// reads the whole volume against a partition asserted clean before the boot, -/// so an `rmdir` that erased the entry and leaked its cluster chain is a lost -/// cluster it names. -pub fn fs_dirs_durable( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - /// Mirrored in `tests/toyos-rust-tests/src/bin/fs_dirs_durable.rs`. - const KEEP: &str = "fsdir-keep"; - const GONE: &str = "fsdir-gone"; - - let image_path = test_dir().join("fs-dirs-durable.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)?; - - // Born clean, asserted rather than assumed, as in `fs_rename_durable`. - 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) - )); - } - - 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 boot = qemu.boot_log().to_string(); - serial::Serial::named("boot console", boot.as_str()).must_be_clean()?; - if !boot.contains(LOG_SERVED) { - return Err(format!( - "the log partition did not mount, so the guest had nowhere to stage directories:\n{}", - volume_lines(&boot) - )); - } - - let result = qemu.run_test("test_rs_fs_dirs_durable", Duration::from_secs(60)); - if let Some(err) = &result.error { - return Err(format!("the guest stopped answering: {err}\nserial:\n{}", result.serial)); - } - if result.exit_code != Some(0) { - return Err(format!( - "fs_dirs_durable guest failed:\n{}\nkernel log while it ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - serial::Serial::named("test serial", result.serial.as_str()).must_be_clean()?; - - 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}"))?; - if after.len() != image.len() { - return Err(format!("the image is {} bytes, was {}", after.len(), image.len())); - } - let volume = &after[start..start + len]; - - let complaints_after = check(volume); - if !complaints_after.is_empty() { - return Err(format!( - "the staged directories left the log volume breaking the format:\n{}", - describe(&complaints_after) - )); - } - - 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 root = fs.root_dir(); - let mut keep_is_dir = false; - for entry in root.iter() { - let entry = entry.map_err(|e| format!("reading the root directory: {e}"))?; - let name = entry.file_name(); - if name == GONE { - return Err(format!("{GONE} is still on the volume after its rmdir")); - } - if name == KEEP { - if !entry.is_dir() { - return Err(format!("{KEEP} is on the volume as a file, not a directory")); - } - keep_is_dir = true; - } - } - if !keep_is_dir { - return Err(format!( - "{KEEP} is not on the volume: the guest's mkdir wrote nothing the host's own \ - FAT reader can see" - )); - } - let inside: Vec = root - .open_dir(KEEP) - .map_err(|e| format!("opening {KEEP} on the host: {e}"))? - .iter() - .filter_map(|e| e.ok().map(|e| e.file_name())) - .filter(|n| n != "." && n != "..") - .collect(); - if !inside.is_empty() { - return Err(format!("{KEEP} holds {inside:?} on the volume; the guest left it empty")); - } - - let _ = std::fs::remove_file(&image_path); - eprintln!( - " [fat] mkdir left a real empty directory the host's own reader lists, rmdir left \ - no trace of the other, and the checker is silent" - ); - 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 other arm of the same table: what the kernel says when both halves are /// there. `screen_diag_boot` is the gate on it. /// @@ -1852,861 +66,3 @@ pub const NO_LOG_ALERT: &str = "log: no /log"; /// named declaration that cites its writer, never from a literal copied at the /// assertion, which is how a hand-copied spelling outlives the kernel's. pub const LOG_ON_CONSOLE_AND_FILE: &str = "log: this boot is on the console and on /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(()) -} - -/// The slow-vs-failed policy, staged end to end: a budget-refused flush is -/// retried and keeps the volume, the deadman declares a volume that never -/// comes durable, and a hung device whose reset escalation fails is a device -/// fact that ends it — the three exits of `object/ops.rs`'s `fsync` loop, each -/// observed from outside the machine. -/// -/// Three boots, because the three verdicts are mutually exclusive states of -/// one volume: -/// -/// 1. **Retry keeps the volume, and a refused attempt discarded nothing.** -/// The guest's own -/// fsync must succeed, logd must never give its volume up, and the blob is -/// then read off the *image* by the host: the safety invariant is that the -/// refused attempt left every un-flushed page dirty, so the retry delivered -/// them, and a kernel that marked them clean on the timeout (the fsyncgate -/// failure mode) has nothing left to deliver and reds the byte comparison. -/// `toyos-fat32-check`'s silence over the volume is the outside judge that -/// the retry also left a consistent filesystem. -/// 2. **The deadman is the third failure evidence.** `fsync-deadman-now` -/// expires it at once, so every claim transfer's first refused attempt is -/// the last: the kernel answers fsd's reads of the log partition -/// `SyscallError::Io`, fsd serves `/log` absent, and logd — which keeps a -/// volume across any number of `WouldBlock`s — has none, on what is a -/// device word, exactly as it would be for an error status. -/// 3. **A failed reset escalation is the second.** `usb-transport-break` -/// abandons the first WRITE(10)'s data phase and `usb-reset-break` makes -/// the recovery ladder meet a device that answers nothing on EP0 — a truly -/// hung device, which QEMU cannot otherwise be. The port reset may not say -/// it took, the disk must go offline, and the boot's log ends on the console -/// — while the machine itself stays up and clean, its userland paged from -/// the ROOT the loader put in memory and never from the stick that broke. -pub fn log_flush_retry( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - // --- Boot 1: slow but live retries, keeps the volume, loses nothing. --- - let image_path = test_dir().join("log-flush-retry.img"); - let mut image = - qemu::build_boot_image(test_config, c_bins, rust_bins, &["fsync-budget-spent"]); - std::fs::write(&image_path, &image).map_err(|e| format!("write the boot image: {e}"))?; - let (start, len) = esp_extent(&image, &image_path)?; - { - // The fixture `esp_files` asserts against, same as `esp_filesystem`. - let volume = &mut image[start..start + len]; - 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(HOST_NOTE) - .map_err(|e| format!("creating {HOST_NOTE} on the ESP: {e}"))?; - file.write_all(HOST_TEXT.as_bytes()).map_err(|e| format!("writing {HOST_NOTE}: {e}"))?; - } - std::fs::write(&image_path, &image).map_err(|e| format!("rewrite 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: &["fsync-budget-spent"], - ..Default::default() - }, - ); - let boot = qemu.boot_log().to_string(); - serial::Serial::named("boot console", boot.as_str()).must_be_clean()?; - - let result = qemu.run_test("test_rs_esp_files", Duration::from_secs(60)); - if let Some(err) = &result.error { - return Err(format!("the guest stopped answering: {err}\nserial:\n{}", result.serial)); - } - if result.exit_code != Some(0) { - return Err(format!( - "esp_files failed under a once-refused flush, so a budget-expired attempt was \ - not retried into a durable one:\n{}\nkernel log while the test ran:\n{}{}", - result.stdout, result.before, result.serial - )); - } - let log = format!("{boot}\n{}{}", result.before, result.serial); - // The staged refusal really ran at the shipped site, and the retry is what - // answered: both halves, or the arm proved nothing. - if !log.contains("not issued") && !log.contains("ran out of its operation budget") { - return Err(format!( - "no budget refusal in the log, so `fsync-budget-spent` staged nothing:\n{}", - volume_lines(&log) - )); - } - // `/log` is flushed through fsd's claim of the log partition, whose retry - // says so by that partition's name. - let log_guid = { - let mut file = - std::fs::File::open(&image_path).map_err(|e| format!("{}: {e}", image_path.display()))?; - toyos_gpt::Guid(toyos_build::image::unique_guid_of(&mut file, toyos_gpt::Guid::MICROSOFT_BASIC)?) - }; - let log_retry = format!("partclaim: a flush of {log_guid} durable on attempt"); - let retried = log - .lines() - .find(|l| l.contains(&log_retry)) - .ok_or_else(|| { - format!( - "no `{log_retry}` line, so the operation-level retry never ran:\n{}", - volume_lines(&log) - ) - })? - .trim() - .to_string(); - if log.contains("on the console only") { - return Err(format!( - "logd gave its volume up under refusals that were only budget words:\n{}", - volume_lines(&log) - )); - } - // The device's view, after a clean shutdown: what the retried flushes left. - 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}")); - } - } - // init has logd flush before it asks for the stop, so a flush of the log - // partition follows the guest's; every record before the stop's own is on - // the wire ahead of it, so a retry of that flush would be here too. - let whole = format!("{log}\n{tail}"); - let stop = whole - .lines() - .position(|l| toyos_quiesce::Record::parse(l).is_some()) - .ok_or_else(|| format!("the shutdown wrote no stop record:\n{tail}"))?; - let retries: Vec<&str> = whole.lines().take(stop).filter(|l| l.contains(&log_retry)).collect(); - if retries.len() != 1 { - return Err(format!( - "{} flush(es) of the log partition retried before the stop, and the refused one is \ - the first alone: every flush is being refused, and each refusal's records are the \ - next flush\n{}", - retries.len(), - retries.join("\n") - )); - } - let after = std::fs::read(&image_path).map_err(|e| format!("read the image back: {e}"))?; - let (log_start, log_len) = log_extent(&after, &image_path)?; - let volume = &after[log_start..log_start + log_len]; - let complaints = check(volume); - if !complaints.is_empty() { - return Err(format!( - "the retried flushes left the log volume breaking the format:\n{}", - describe(&complaints) - )); - } - let got = need(read_files(volume, &[GUEST_BLOB])?.remove(0), GUEST_BLOB)?; - if got.len() != BLOB_LEN { - return Err(format!( - "{GUEST_BLOB} is {} bytes on the volume, wrote {BLOB_LEN} — a refused attempt's \ - pages were dropped instead of kept dirty", - got.len() - )); - } - if let Some(at) = got.iter().zip(blob()).position(|(a, b)| *a != b) { - return Err(format!( - "{GUEST_BLOB} differs from what the guest wrote at byte {at} — a page the refused \ - attempt should have kept dirty never reached the device" - )); - } - let _ = std::fs::remove_file(&image_path); - eprintln!(" [retry] {retried}"); - eprintln!( - " [retry] volume kept, checker silent, {BLOB_LEN} blob bytes byte-identical after a \ - once-refused flush" - ); - - // --- Boot 2: the deadman expires, and that is a declared death. --- - let image_path = test_dir().join("log-flush-deadman.img"); - let image = qemu::build_boot_image( - test_config, - c_bins, - rust_bins, - &["fsync-budget-spent", "fsync-deadman-now"], - ); - 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: &["fsync-budget-spent", "fsync-deadman-now"], - ..Default::default() - }, - ); - let mut log = qemu.boot_log().to_string(); - if !log.contains("on the console only") { - log.push_str(&qemu.drain_until(Duration::from_secs(30), |l| { - l.contains("on the console only") - })); - } - drop(qemu); - serial::Serial::named("deadman boot console", log.as_str()).must_be_clean()?; - // Every claim transfer's first attempt is the last here, fsd's reads of - // the log partition among them, so the death is declared at the mount. - let declared = log - .lines() - .find(|l| l.contains("partclaim: ") && l.contains("still refused after 1 attempt(s)")) - .ok_or_else(|| { - format!("the deadman never declared a claim's transfer failed:\n{}", volume_lines(&log)) - })? - .trim() - .to_string(); - if !log.contains(&format!("{LOG_ABSENT} no FAT32 here: device I/O failed")) { - return Err(format!( - "fsd served /log off a partition the deadman had declared failed:\n{}", - volume_lines(&log) - )); - } - let gave_up = log - .lines() - .find(|l| l.contains("logd:") && l.contains("on the console only")) - .ok_or_else(|| { - format!( - "logd kept a volume the deadman had declared failed:\n{}", - volume_lines(&log) - ) - })? - .trim() - .to_string(); - let _ = std::fs::remove_file(&image_path); - eprintln!(" [deadman] {declared}"); - eprintln!(" [deadman] {gave_up}"); - - // --- Boot 3: a hung device fails its reset, which is a device fact. --- - let image_path = test_dir().join("log-flush-hung.img"); - let image = qemu::build_boot_image( - test_config, - c_bins, - rust_bins, - &["usb-transport-break", "usb-reset-break"], - ); - 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: &["usb-transport-break", "usb-reset-break"], - ..Default::default() - }, - ); - let mut log = qemu.boot_log().to_string(); - if !log.contains("on the console only") { - log.push_str(&qemu.drain_until(Duration::from_secs(30), |l| { - l.contains("on the console only") - })); - } - drop(qemu); - serial::Serial::named("hung boot console", log.as_str()).must_be_clean()?; - for needed in [ - "transport broke on SCSI", - "the port reset was not answered; break 2 of 3 running", - " is offline: ", - ] { - if !log.contains(needed) { - return Err(format!( - "no {needed:?} in the log, so the staged hung device never met its \ - recovery:\n{}", - volume_lines(&log) - )); - } - } - let offline = log - .lines() - .find(|l| l.contains(" is offline: ")) - .map(str::trim) - .unwrap_or_default() - .to_string(); - let gave_up = log - .lines() - .find(|l| l.contains("logd:") && l.contains("on the console only")) - .ok_or_else(|| { - format!( - "logd never reported losing a volume whose device went offline:\n{}", - volume_lines(&log) - ) - })? - .trim() - .to_string(); - let _ = std::fs::remove_file(&image_path); - eprintln!(" [hung] {offline}"); - eprintln!(" [hung] {gave_up}"); - 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> { - root_chunk_refused_on(qemu::Profile::InternalDisk, test_config, c_bins, rust_bins) -} - -/// [`root_chunk_refused`] with the boot image on a USB stick: stock edk2's -/// read of that sector does not return, and its watchdog does not reset the -/// machine -/// (`issues/boot-media/an-unreadable-sector-on-a-usb-boot-stick-hangs-the-loader-past-the-firmware-watchdog.md`). -pub fn root_chunk_refused_on_a_usb_stick( - test_config: &Path, - c_bins: &[(String, Vec)], - rust_bins: &[(String, Vec)], -) -> Result<(), String> { - root_chunk_refused_on(qemu::Profile::Headless, test_config, c_bins, rust_bins) -} - -fn root_chunk_refused_on( - profile: qemu::Profile, - 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, - 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 b75d6c5aef5..00000000000 --- a/tests/desktopcase/system.toml +++ /dev/null @@ -1,78 +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"] - -# `compositor` is here and not in the shipping `system.toml`'s shell row, and -# it is what endows `test_rs_window_child`. A harness-injected binary is no -# `[programs]` key, so init can build it nothing; what it gets is what its -# parent moved into it, and a child spawned directly inherits the spawner's -# namespace. So the authority travels with the spawn, which is how a real shell -# will launch a real program — and giving it to the shell here widens exactly -# this config's shell and no other. -[programs.shell] -receives = ["compositor", "surface", "launcher"] - -[programs.toybox] -receives = ["compositor", "surface"] - -# The winit app the owner closed the window of. `desktop_window_child` -# launches it from the shell; nothing else in this config uses it. -[programs.snake] -receives = ["compositor"] - -[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 53d914348b3be052bb010673cfffb6413ef759e9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 604872 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

^f{F1;1 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

2$ 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

Il|Gih!56nnjOq{H@d!1#dRSb+xT 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 q2`@>ytL3)S7TY~Wuu8?Z4YZhhr+r`x=$G3o$ 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, `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/logkeepcase/system.toml b/tests/logkeepcase/system.toml deleted file mode 100644 index e221fc28dc7..00000000000 --- a/tests/logkeepcase/system.toml +++ /dev/null @@ -1,42 +0,0 @@ -# A job list whose one job floods test-runner's log ring while `logd` reads -# nothing of that ring: the ring fills, and test-runner's own line after the -# job lands in the slots a child may not take. `logd` reads the ring again -# once init says the machine stops. `log_ring_keeps_the_owners_slots` reads -# the verdict off `/log`. -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -args = [ - "--stall=test-runner", - "--stall-until=init: power: the machine stops, and logd makes the log whole first (Reboot)", -] -syscap = ["logread"] - -# `power` because the last job ends the machine, asking init. -[programs.test-runner] -receives = ["power"] -args = ["test_rs_log_flood", "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/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/netcase/system.toml b/tests/netcase/system.toml deleted file mode 100644 index 73b3c655cd2..00000000000 --- a/tests/netcase/system.toml +++ /dev/null @@ -1,76 +0,0 @@ -# The one boot that runs netd with a virtio NIC in front of it. -# -# 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. - -[boot] -start = ["logd", "blockd", "fsd", "netd", "test-runner"] - -# `every_boot_config_runs_logd` is what refuses a boot config without it: the -# kernel keeps the record ring and writes no file, so such a boot's `/log` is -# empty. It claims no device and serves no port. -[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. -# 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:1af4:1041"] - -# `launcher` for `launcher_refusals`, whose subject is what a client can make -# `/system/bin/init` do — so the test estate has to be able to reach it somewhere. -# This is also 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"] - -# What `launcher_refusals` asks init to start. 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 -# 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/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/quiescecase/system.toml b/tests/quiescecase/system.toml deleted file mode 100644 index a3b9cb4ab94..00000000000 --- a/tests/quiescecase/system.toml +++ /dev/null @@ -1,31 +0,0 @@ -# A boot whose one job writes from six threads and then reboots out from under -# them. The job asks for the reset itself, from a thread that is not one of the -# writers, so no `reboot` job follows the list. -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -# `power` because the job ends the machine, asking init. -[programs.test-runner] -receives = ["power"] -args = ["test_rs_quiesce_writers"] - -# 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/quiescelastcase/system.toml b/tests/quiescelastcase/system.toml deleted file mode 100644 index 731f497244a..00000000000 --- a/tests/quiescelastcase/system.toml +++ /dev/null @@ -1,30 +0,0 @@ -# This boot's one job starts the thread and the child the kernel may hold and -# reboots; it asserts nothing itself. -[boot] -start = ["logd", "blockd", "fsd", "test-runner"] - -[programs.logd] -service = true -syscap = ["logread"] - -# `power` because the job ends the machine, asking init. -[programs.test-runner] -receives = ["power"] -args = ["test_rs_quiesce_last"] - -# 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/test-durations b/tests/test-durations index e92b25a72e1..638eddd4bc5 100644 --- a/tests/test-durations +++ b/tests/test-durations @@ -1,392 +1,5 @@ -00_assignment 12 -01_comment 11 -02_printf 12 -04_for 11 -05_array 11 -06_case 11 -07_function 11 -08_while 11 -09_do_while 11 -100_c99array_decls 11 -103_implicit_memmove 18 -105_local_extern 11 -107_stack_safe 11 -109_float_struct_calling 11 -10_pointer 11 -110_average 11 -111_conversion 11 -118_switch 12 -119_random_stuff 11 -11_precedence 11 -121_struct_return 11 -123_vla_bug 20 -129_scopes 11 -12_hashdefine 11 -130_large_argument 11 -131_return_struct_in_reg 11 -132_bound_test 25 -133_old_func 11 -134_double_to_signed 11 -135_func_arg_struct_compare 11 -137_funcall_struct_args 11 -138_offsetof 11 -13_integer_literals 11 -140_switch_hex 11 -141_tok_str 12 -142_pp_sizeof_ptr 11 -143_uint64_split 11 -144_sizeof_init 1 -145_self_ref_struct 11 -146_deref_assign 11 -147_sizeof_deref_array 11 -148_directive_in_args 11 -149_bitfield_write 11 -14_if 11 -150_union_short_store 11 -151_cast_truncate 11 -152_float_const_init 11 -153_sizeof_const_init 11 -154_funcptr_global_init 11 -155_addr_array_elem_init 11 -156_sizeof_array_count 11 -157_sizeof_member 11 -158_vla 11 -159_va_list 11 -15_recursion 11 -160_global_variadic 11 -16_nesting 11 -17_enum 11 -18_include 27 -19_pointer_arithmetic 11 -200_variadic_float 11 -201_packed_layout 11 -20_pointer_comparison 11 -21_char_array 11 -22_floating_point 12 -23_type_coercion 11 -24_math_library 11 -25_quicksort 11 -26_character_constants 11 -27_sizeof 11 -28_strings 11 -29_array_address 11 -30_hanoi 2 -32_led 18 -34_array_assignment 11 -35_sizeof 11 -36_array_initialisers 11 -37_sprintf 1 -38_multiple_array_index 11 -39_typedef 11 -41_hashif 11 -42_function_pointer 11 -43_void_param 11 -44_scoped_declarations 1 -45_empty_for 11 -47_switch_return 11 -48_nested_break 11 -49_bracket_evaluation 11 -50_logical_second_arg 11 -51_static 11 -52_unnamed_enum 11 -54_goto 11 -55_lshift_type 11 -61_integers 13 -64_macro_nesting 11 -67_macro_concat 1 -70_floating_point_literals 12 -71_macro_empty_arg 11 -72_long_long_constant 11 -75_array_in_struct_init 14 -76_dollars_in_identifiers 11 -77_push_pop_macro 11 -78_vla_label 20 -80_flexarray 11 -81_types 11 -82_attribs_position 11 -84_hex_float 11 -86_memory_model 11 -87_dead_code 12 -88_codeopt 11 -89_nocode_wanted 19 -90_static_vs_global 11 -90_stdio_buffering 11 -90_struct_init 13 -91_ptr_longlong_arith32 11 -92_enum_bitfield 11 -93_integer_promotion 12 -97_utf8_string_literal 11 -abuse_connect_flood 2726 -abuse_cwd_growth 14 -abuse_elf_loader 31 -abuse_elf_segments 25 -abuse_gpu_resolution 8 -abuse_handle_table 6 -abuse_inbox 11 -abuse_kernel_addr 734 -abuse_listener_hijack 6 -abuse_page_straddle 14 -abuse_pipe_map 16 -abuse_pipe_owner 12 -abuse_pipe_ring 5 -abuse_shared_grant 16 -abuse_spawn_argv 3 -abuse_thread_name 1 -abuse_tls_alloc 15 -acpi_table_inventory 4601 -allocator_stress 787 -apps_and_home_are_one_filesystem 7263 -bar_placement_is_proven 3297 -blackbox_done_chain 4119 -blackbox_early_panic_sealed 2470 -blackbox_early_panic_sealed_muted 3145 -blackbox_fault_sealed 6258 -blackbox_foreign_record 5605 -blackbox_panic_chain 11407 -panic_outlives_the_deadline 9437 -blackbox_unclaimed_page 6078 -block_duplicate_id 6997 -blocked_dump 3400 -blocking_read_stress 41 -boot_deadline_ends_a_wedge 19295 -boot_partition_identity 5519 -c_capture_ignores_daemon_lines 4578 -connect_before_serve 23 -console_locale_detect 3922 -control_regs 7243 -control_regs_negative 3872 -debug_float 11 -debug_trap 25 -demand_paging_sse 12 -demand_window_race 54 -desktop_locale_detect 4510 -desktop_typing_damage 14338 -desktop_window_child 40312 -device_claim_lifetime 21 -disk_backtrace 85 -diskless_boot 4475 -dlopen_dedup 40 -double_fault_stack 5139 -double_panic_names_the_fault 9120 -driver_wait_refused 17739 -empty_dir_stat 12 -endowment_denied 10 -esp_filesystem 10123 -exit_wait_storm 178 -fat_backing_revoked 8226 -fault_gates 31 -foreign_disk_untouched 4688 -fpu_isolation 14662 -fs_dirs_durable 7893 -fs_large_file 94 -fs_rename_durable 9346 -fs_transactional 85 -fs_truncate_persist 17 -fsync_failed_commit 8386 -futex_wake_counts 1007 -gpu_set_resolution 8610 -gsbase_locked 13091 -handle_basic 19 -handle_kill_policy 515 -handle_lifetime 33 -handle_transfer 66 -hang_bounded_by_the_stick 3717 -hard_lockup_ends_a_deaf_cpu 20277 -hash_seed_precedes_every_map 4976 -hda_two_live_refused 4848 -hierarchy_paths 87 -home_backing_revoked 663 -home_overwrite_reads_back 4835 -https_tls13 5166 -https_tls13_e1000e 6072 -i8042_absent 9221 -i8042_budget_expiry 3135 -i8042_fadt_denial 5736 -i8042_health 9509 -i8042_kbd_echo 4770 -i8042_mouse 4310 -i8042_no_spurious_wake 146 -i8042_quarantine 10068 -i8042_undecoded_bytes 4960 -idle_stack_guard 9601 -inbox_cancel_wakes 543 -input_claim_absent 2565 -input_merge 3326 -internal_disk_boot 4729 -ioapic_topology 3142 -iommu_context_absent 7382 -iommu_discovery 18600 -iommu_domain_isolation 15756 -iommu_empty_domain 5872 -iommu_gpu_foreign_backing 7784 -iommu_gpu_scanout_swap 7673 -iommu_hda_foreign_bdl 7104 -iommu_interrupt_remapping 5609 -iommu_sound_foreign_dma 7537 -iommu_virtio_platform 17259 -irq_census_conservation 5056 -job_deadline_reboots 4806 -kernel_heartbeat 5634 -kernel_log_file 13152 -keyboard_claim_close_spares_stdin 4580 -kill_while_blocked 44 -klogd_hosted 5674 -lan_dhcp_lease 3029 -lan_no_lease 32709 -lapic_spurious_vector 6829 -late_storage_connect 6455 -launcher_refusals 2106 -leak_rollback_selftest 5423 -loader_watchdog_arms 10064 -locale_detect 9959 -locale_detect_unrecognized 160 -log_flush_retry 20526 -log_nested_emit 5008 -log_partition_identity 9516 -log_partition_layout 477 -log_poll_outlives_a_close 4738 -log_reserve_window 7022 -log_reserve_window_negative 6791 -log_stream 24865 -log_stream_e1000e 25219 -log_stream_no_listener 25182 -log_stream_stalled_peer_delivers_whole_records 34591 -log_stream_unreachable 25707 -lseek_past_eof 24 -machine_reboot 4551 -metal_device_probe 5237 -metal_job_reboot 2289 -metal_sim_client_death 4126 -metal_sim_compositor 8986 -metal_sim_compositor_stall 11390 -metal_sim_input 5065 -metal_sim_ipc_hostile_peer 226 -metal_sim_pointer_churn 16658 -metal_sim_scanout_wc 0 -metal_sim_window_caps 148 -metal_sim_window_drag 6256 -mkdir_cap 6878 -mmap_prot 32 -mmap_stress 45 -munmap_reissues_read_window 100 -nested_fault_is_recursive 5936 -netd_connection_caps 6538 -netd_gone_mid_bind 32 -netd_hostile_peer 4509 -netd_listener_forgery 2649 -nvme_home_roundtrip 13 -nvme_large_device 6052 -nvme_wide_sector 3500 -operation_nesting 5075 -panic_before_peripherals_reboots 3227 -panic_reboots 9768 -pci_capability_walk 5775 -pci_claim_caps_truncated 2583 -pci_inventory 4529 -pci_function_is_exclusive 3007 -pipe_flag_forgery 12 -pkg_install_gbae 6688 -pmm_accounting 6932 -poll_wake_pipe 16 -poller_capacity 36 -port_poll_churn 84 -pre_idle_wedge_speaks 3688 -process_lifecycle 225 -process_reopen_selftest 5800 -process_stats 221 -query_modules_size 12 -query_pci_agreement 7182 -quiesce_refuses_a_second_shutdown 4249 -quiesce_stops_the_machine 4434 -read_fault_selftests 4694 -readdir_bound 19823 -redirty_mid_flush 21262 -reentry_names_the_first_panic 5073 -root_candidate_malformed 5126 -root_named_but_absent 6294 -root_named_twice 8079 -sched_check_build 6833 -sched_stress 1292 -screen_blocked_dump 4700 -screen_console_clear 5756 -screen_console_panic 6788 -screen_console_scroll 12310 screen_console_shell 2604 screen_diag_boot 7330 -screen_early_panel 5779 -screen_fatal_halt 4970 screen_fatal_halt_composited 6908 -screen_gop_firmware_mode 12624 -screen_i8042_health 4495 -screen_late_panic 3926 -screen_loader_lines 3991 -screen_log_absent 1823 -screen_paged_scrollback 7384 -screen_pager_keys 16152 screen_panic_muted 4416 -shipped_config_boots 3024 -shm_release_reclaims 29 -short_sleep_livelock 4823 -smp_failed_ap_leaves_no_hole 6672 -smp_roster_and_tsc_trail 4926 -sshd_exec 5110 -sshd_files 535 -sshd_key_auth 1428 -std_alloc 12 -std_fs 17 -std_fs_write 12 -std_io 11 -std_mmap 12 -std_process 29 -std_sync 79 -std_threading 17 -std_tls 14 -std_tls_cranelift 23 -std_tls_dlopen 18 -std_tls_multi_crate 13 -std_unwind 18 -std_unwind_so 20 -swiss_german_layout 5355 -syscall_window_nmi 6825 -syscall_window_nmi_controls 13090 -sysret_ss_reload 6453 -timer_calibration 6722 -toybox_cp_volume 18616 -toybox_file_tools 205 usb_boot_stick_pulled 15650 -usb_flush_optional 18056 -usb_pool_exhausted 5237 -usb_refused_disk_first 7260 -usb_reset_records_the_phase_it_cut 64260 -usb_reset_hands_devices_back 49057 -usb_short_read 8150 -usb_storage_gate 14200 -usb_storage_shapes 10215 -usb_storage_write_error 6092 -usb_transport_break 5408 -userdev_dma_fault 4670 -va_exhaustion 6159 -virtio_net_no_msix 2039 -virtio_used_ring 4189 -volume_from_another_disk 7122 -wall_clock_century_register 9030 -wall_clock_no_century 6987 -wall_clock_now 13 -wall_clock_rtc_dead 8070 -wall_clock_rtc_unstable 7805 -watchdog_resets 9393 -window_refusal 16 -xhci_deaf_registers 21244 -xhci_descriptor_walk 5186 -xhci_flap 8220 -xhci_full_speed_device 8833 -xhci_many_devices 6247 -xhci_msi_only 5757 -xhci_no_interrupt 5114 -xhci_portsc_rw1c 5692 -xhci_second_controller 5825 -xhci_slot_exhaustion 8149 -xhci_slow_connect 5150 -xhci_superspeed_ports 5494 -xhci_two_controllers 6643 -xhci_xecp_walk 4635 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/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 2f8f453dcba..00000000000 --- a/tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs +++ /dev/null @@ -1,269 +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; -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:?}"))); - ipc::signal(committing.handle(), window::MSG_GET_RESOLUTION) - .unwrap_or_else(|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/copy_out_races_munmap.rs b/tests/toyos-rust-tests/src/bin/copy_out_races_munmap.rs deleted file mode 100644 index edbb013fb81..00000000000 --- a/tests/toyos-rust-tests/src/bin/copy_out_races_munmap.rs +++ /dev/null @@ -1,98 +0,0 @@ -//! A typed copy into user memory must never land in a frame a sibling has -//! been handed since the copy translated its destination. -//! -//! The kernel, armed with `copy-meets-a-remap`, holds a copy whose destination -//! carries the mark below between its translation and its store, writes the -//! cue into the destination's second word, and lets the store go once this -//! process has mapped memory again. The main thread waits for the cue, unmaps -//! the destination and maps a fresh region, which the physical allocator -//! serves from the lowest free frame: the one just unmapped, unless the copy -//! holds it. A kernel that holds nothing across the copy stores into that -//! region; one that pins the frame leaves it as the allocator zeroed it. -//! -//! **The memory is the verdict, before the return value.** - -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; -use std::thread; - -use toyos_abi::syscall::{self, MmapFlags, MmapProt, OpenFlags, SYS_FSTAT}; - -/// `kernel/src/user_ptr.rs`'s `remap_race::MARK` and `HELD`. -const MARK: u64 = 0x5eed_c0de_2ace_0001; -const HELD: u64 = 0x5eed_c0de_2ace_0002; - -const SELF_PATH: &str = "/system/bin/test_rs_copy_out_races_munmap"; -const PAGE_2M: usize = 2 * 1024 * 1024; -/// Past `Stat`, which is what `fstat` stores. -const CHECKED: usize = 64; - -/// `fstat` into an address, which the typed wrapper cannot take. -fn fstat_into(handle: u64, addr: u64) -> u64 { - let ret: u64; - unsafe { - core::arch::asm!( - "syscall", - in("rdi") SYS_FSTAT, - in("rsi") handle, - in("rdx") addr, - in("r8") 0u64, - in("r9") 0u64, - lateout("rax") ret, - out("rcx") _, - out("r11") _, - ); - } - ret -} - -fn map_2m() -> *mut u8 { - let p = unsafe { - syscall::mmap( - core::ptr::null_mut(), - PAGE_2M, - MmapProt::READ | MmapProt::WRITE, - MmapFlags::ANONYMOUS | MmapFlags::PRIVATE, - ) - }; - assert!(!p.is_null(), "mmap of a 2 MiB region failed"); - p -} - -fn main() { - let fd = syscall::open(SELF_PATH.as_bytes(), OpenFlags::READ).expect("open self"); - let victim = map_2m(); - let words = victim.cast::(); - unsafe { - words.write_volatile(MARK); - words.add(1).write_volatile(0); - } - - let ret = Arc::new(AtomicU64::new(u64::MAX)); - let copier = { - let ret = Arc::clone(&ret); - let (fd, addr) = (fd.0 as u64, victim as u64); - thread::spawn(move || ret.store(fstat_into(fd, addr), Ordering::SeqCst)) - }; - - // No deadline: a kernel that never holds the marked copy leaves this spinning, - // and the harness ceiling reds it. - while unsafe { words.add(1).read_volatile() } != HELD { - std::hint::spin_loop(); - } - unsafe { syscall::munmap(victim, PAGE_2M) }.expect("munmap the copy's destination"); - let sibling = map_2m(); - - copier.join().expect("the copying thread panicked"); - let got: Vec = (0..CHECKED).map(|i| unsafe { sibling.add(i).read_volatile() }).collect(); - if let Some(at) = got.iter().position(|&b| b != 0) { - panic!( - "a copy held across a sibling's munmap stored into the region mapped after it: byte \ - {at} is {:#x}, and the region was zeroed when it was handed out: {got:x?}", - got[at] - ); - } - assert_eq!(ret.load(Ordering::SeqCst), 0, "the held fstat did not succeed"); - syscall::close(fd); - println!("copy_out_races_munmap: the copy held its frame across the sibling's munmap and mmap"); -} 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/esp_files.rs b/tests/toyos-rust-tests/src/bin/esp_files.rs deleted file mode 100644 index a27549290c5..00000000000 --- a/tests/toyos-rust-tests/src/bin/esp_files.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! The two FAT32 partitions as ordinary mounts, from inside the machine. -//! -//! Every claim here is checked again on the host against the disk image the -//! *device* received — see `tests/common/volumes.rs`. This half exists because -//! the host cannot ask the guest's VFS anything: whether `/boot` reads as a -//! directory tree, whether a file the host wrote arrives byte-for-byte, and -//! whether the things the mount will not do are refused rather than silently -//! accepted, are all questions only a process can put. -//! -//! `/boot` is read-only toward userland and the write direction is exercised -//! on `/log`, which is the same adapter over the same driver. That split is -//! the point rather than an accident of where the files went: `/boot` is what -//! firmware reads the loader off, and it had no permission model at all — -//! `fs::write("/boot/toyos/kernel.elf", "TEETH")` from an ordinary process -//! truncated the kernel image to five bytes, when the kernel lived here. The -//! loader is the one binary left on it and the one no signature covers, so it -//! is the file attacked. The host's byte-for-byte check of the build artifacts -//! is the other half of that; this half is the attack. - -use std::fs; -use std::io::{Read, Write}; -use std::os::toyos::fs::symlink; - - -/// Mirrored in `tests/common/volumes.rs`. Two halves of one fixture; a change -/// to either without the other shows up as a mismatch here, not as a silent -/// pass. -const HOST_NOTE: &str = "/boot/toyos/host-note.txt"; -const HOST_TEXT: &str = "written by the host before this machine started\n"; -const GUEST_NOTE: &str = "/log/guest-note.txt"; -const GUEST_TEXT: &str = "written by ToyOS through the VFS\n"; -const GUEST_BLOB: &str = "/log/guest-blob.bin"; -/// Ten pages and a partial eleventh: more than one `write_page` call, more -/// than one cluster on any FAT32 volume, and a tail that is the case an -/// off-by-one in the size bookkeeping gets wrong. -const BLOB_LEN: usize = 10 * 4096 + 137; - -/// The file whose truncation is the reason `/boot` has a permission model: -/// the loader, which firmware runs and nothing verifies. -const LOADER: &str = "/boot/EFI/BOOT/BOOTx64.EFI"; - -fn blob() -> Vec { - (0..BLOB_LEN).map(|i| (i.wrapping_mul(97) ^ 0x5A) as u8).collect() -} - -fn names(dir: &str) -> Vec { - let mut out: Vec = fs::read_dir(dir) - .unwrap_or_else(|e| panic!("read_dir {dir}: {e}")) - .map(|e| e.expect("dir entry").file_name().to_string_lossy().into_owned()) - .collect(); - out.sort(); - out -} - -/// The first 64 bytes of the loader; a plain WRITE at offset 0 shows in content, not length. -fn loader_prefix() -> [u8; 64] { - let mut buf = [0u8; 64]; - fs::File::open(LOADER) - .and_then(|mut f| f.read_exact(&mut buf)) - .expect("read the loader's prefix"); - buf -} - -fn main() { - // What the host put there before the machine booted. A guest that read - // its own writes back could pass without the read path working at all. - let got = fs::read_to_string(HOST_NOTE).expect("read the host's note off /boot"); - assert_eq!(got, HOST_TEXT, "the host's note did not survive the trip"); - println!(" PASS host note read back through /boot"); - - // The bootloader's own directory, which firmware and the build both put - // there — so a listing that misses it is a listing, not a namespace. - let toyos = names("/boot/toyos"); - for want in ["log.guid", "host-note.txt"] { - assert!(toyos.iter().any(|n| n == want), "/boot/toyos has {toyos:?}, wanted {want}"); - } - let root = names("/boot"); - for want in ["toyos", "EFI"] { - assert!(root.iter().any(|n| n == want), "/boot lists {root:?}, wanted {want}"); - } - println!(" PASS /boot and /boot/toyos list what the image holds"); - - // Two levels down, and a file nothing here wrote: the path the bootloader - // itself was loaded from. - let loader = fs::read("/boot/EFI/BOOT/BOOTx64.EFI").expect("read the bootloader off /boot"); - assert!(loader.len() > 4096, "BOOTx64.EFI is {} bytes", loader.len()); - assert_eq!(&loader[..2], b"MZ", "BOOTx64.EFI does not start with a PE header"); - println!(" PASS BOOTx64.EFI reads back, {} bytes", loader.len()); - - boot_refuses_every_way_of_changing_it(); - log_takes_writes(); -} - -/// Every syscall that can change what is on a volume, aimed at `/boot`. -/// -/// One per syscall rather than one representative, because they are separate -/// entry points and a gate on `open` alone would have said nothing about -/// `unlink` or `rename`. The truncation is first because it is the one that was -/// actually done, by a guest test, to a real image. -fn boot_refuses_every_way_of_changing_it() { - let before = fs::metadata(LOADER).expect("stat the loader").len(); - assert!(before > 4096, "the loader is {before} bytes before we start"); - - let err = fs::write(LOADER, "TEETH").expect_err("truncating the loader was permitted"); - println!(" PASS writing {LOADER} is refused: {err}"); - let after = fs::metadata(LOADER).expect("stat the loader again").len(); - assert_eq!(after, before, "the refused write changed the loader's length"); - - fs::write("/boot/toyos/new-file.txt", "x").expect_err("creating a file on /boot was permitted"); - fs::remove_file(HOST_NOTE).expect_err("deleting a file on /boot was permitted"); - fs::create_dir("/boot/toyos/newdir").expect_err("mkdir on /boot was permitted"); - fs::rename(HOST_NOTE, "/boot/toyos/moved.txt").expect_err("rename on /boot was permitted"); - assert!(symlink("/boot/EFI/BOOT/BOOTx64.EFI", "/boot/toyos/link").is_err(), "symlink on /boot was permitted"); - - // A read-only open still works, and reads. The refusal is of changes, not - // of the mount. - let still = fs::read_to_string(HOST_NOTE).expect("read /boot after the refusals"); - assert_eq!(still, HOST_TEXT, "the host's note changed under the refused operations"); - - let toyos = names("/boot/toyos"); - for absent in ["new-file.txt", "newdir", "moved.txt", "link"] { - assert!(!toyos.iter().any(|n| n == absent), "a refused operation left {absent} behind"); - } - println!(" PASS create, delete, mkdir, rename and symlink are all refused on /boot"); - - // The path checked must be the path opened, and plain WRITE is the hole: - // CREATE/TRUNCATE unlink the link. A link on a writable served directory - // to the loader: an absolute one is handed back and resolved again in this - // process's own table, where `/boot`'s server refuses the write; one that - // climbs out is refused by the server it lies on. - let before = loader_prefix(); - assert_eq!(&before[..2], b"MZ", "the loader is not a PE image before the symlink attack"); - for (link, target) in - [("/home/esp_evil", "/boot/EFI/BOOT/BOOTx64.EFI"), ("/home/esp_evil_up", "../boot/EFI/BOOT/BOOTx64.EFI")] - { - let _ = fs::remove_file(link); - symlink(target, link).unwrap_or_else(|e| panic!("a link on /home is allowed: {link}: {e}")); - let write = || fs::OpenOptions::new().write(true).open(link); - match write() { - Err(e) => println!(" {link} -> {target} refused for writing: {e}"), - Ok(_) => panic!("{link} -> {target} opened {LOADER} for writing"), - } - let reader = fs::File::open(LOADER).expect("read /boot is allowed"); - assert!(write().is_err(), "{link} -> {target} opened {LOADER} for writing while a /boot read handle was held"); - drop(reader); - fs::remove_file(link).unwrap_or_else(|e| panic!("remove {link}: {e}")); - } - assert_eq!(loader_prefix(), before, "a refused symlink write still changed the loader"); - println!(" PASS a link on /home to {LOADER}, absolute or climbing, is refused for writing"); -} - -/// The write direction, on the volume userland is allowed to have. -/// -/// Same adapter and same driver as `/boot`, so nothing about the FAT32 write -/// path goes untested by the refusals above. -fn log_takes_writes() { - fs::write(GUEST_NOTE, GUEST_TEXT).expect("write a note to /log"); - let back = fs::read_to_string(GUEST_NOTE).expect("read the note back"); - assert_eq!(back, GUEST_TEXT, "the note changed between write and read"); - - let data = blob(); - { - let mut f = fs::File::create(GUEST_BLOB).expect("create the blob on /log"); - f.write_all(&data).expect("write the blob"); - f.sync_all().expect("fsync the blob"); - } - let back = fs::read(GUEST_BLOB).expect("read the blob back"); - assert_eq!(back.len(), data.len(), "the blob is {} bytes, wrote {}", back.len(), data.len()); - let bad = back.iter().zip(&data).position(|(a, b)| a != b); - assert!(bad.is_none(), "the blob differs at byte {}", bad.unwrap_or(0)); - println!(" PASS {BLOB_LEN} bytes written and read back on /log"); - - // FAT32 has no symlink, and the contract is that this fails rather than - // leaving a regular file the caller believes is a link. On a mount that - // permits writes, so what is being refused is the format and not the - // policy. - let err = symlink("/log/guest-note.txt", "/log/link"); - assert!(err.is_err(), "creating a symlink on FAT32 reported success"); - assert!(!names("/log").iter().any(|n| n == "link"), "a refused symlink left a file"); - println!(" PASS a symlink on /log is refused, and leaves nothing behind"); - - // Delete has to reach the volume, not just the name cache: the host checks - // afterwards that this file is gone from the image. - fs::write("/log/doomed.txt", "deleted before shutdown\n").expect("write doomed.txt"); - fs::remove_file("/log/doomed.txt").expect("remove doomed.txt"); - assert!(fs::read("/log/doomed.txt").is_err(), "the deleted file still reads"); - println!(" PASS a file created and deleted on /log is gone"); -} diff --git a/tests/toyos-rust-tests/src/bin/fat_backing_revoked.rs b/tests/toyos-rust-tests/src/bin/fat_backing_revoked.rs deleted file mode 100644 index b922e5319cd..00000000000 --- a/tests/toyos-rust-tests/src/bin/fat_backing_revoked.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! A FAT32 file backing must not outlive the file it reads. -//! -//! `/log` hands every open file a `FatBacking` holding the volume byte ranges -//! its data lives in. Unlink the file and `Fat32::remove` puts those clusters -//! back in the FAT — and FSInfo's `next_free` is walked *down* to the lowest one -//! freed, so the very next allocation on the volume is the one that takes them. -//! A backing that still names them reads that file's contents: an information -//! disclosure through `open`, `rm` and a write, with nothing crafted about it -//! and no privilege needed. -//! -//! The same defect `home_backing_revoked` covers for `/home`, on the other -//! filesystem and the other allocator. `FatFs::delete` already dropped the -//! *write* handle, so the destructive half was closed and the read half was not. -//! -//! Staged rather than reasoned about: the victim's clusters are freed and then -//! deliberately handed to a file whose bytes are nothing like the victim's, and -//! the still-open descriptor is read afterwards. The host half -//! (`tests/common/volumes.rs::fat_backing_revoked`) shuts the machine down and -//! reads the volume back with an independent FAT implementation and the -//! fatgen103 checker, so what the delete-and-reallocate cycle left on the stick -//! is judged by something that is not the kernel. - -use std::fs; -use std::io::{Read, Write}; -use std::thread; -use std::time::Duration; - -/// Mirrored in `tests/common/volumes.rs::fat_backing_revoked`. Two halves of one -/// fixture; a change to either alone fails loudly rather than passing quietly. -const VICTIM: &str = "/log/fat-revoke-victim.bin"; -const ATTACKER: &str = "/log/fat-revoke-attacker.bin"; -const CONTROL: &str = "/log/fat-revoke-control.bin"; - -/// Eight pages. More than one so the read crosses pages, and — at the 512-byte -/// clusters a 34 MiB FAT32 volume gets — sixty-four clusters, so each page is -/// several extents and the multi-run half of `FatBacking::read_page` is the one -/// under test. -const LEN: usize = 8 * 4096; - -const VICTIM_BYTE: u8 = 0xA7; -const ATTACKER_BYTE: u8 = 0x5C; - -/// Between two asks of a setup step the kernel refused with `WouldBlock` -/// (`kernel/src/block.rs::OPERATION`). A pace and never a verdict: device -/// patience is not what this test is about, so its setup asks again the way -/// logd's flush policy does. -const SETUP_PAUSE: Duration = Duration::from_millis(200); - -/// One idempotent setup step, asked again on `WouldBlock` with no deadline; -/// anything else panics with the step's message. -fn patient(what: &str, mut op: impl FnMut() -> std::io::Result) -> T { - loop { - match op() { - Ok(v) => return v, - Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => thread::sleep(SETUP_PAUSE), - Err(e) => panic!("{what}: {e}"), - } - } -} - -fn write_file(path: &str, byte: u8) { - { - let mut f = patient(&format!("create {path}"), || fs::File::create(path)); - // Not `patient`: a `write_all` refused partway has advanced the cursor, - // so asking again blind would double bytes; it lands in the cache anyway. - f.write_all(&vec![byte; LEN]).unwrap_or_else(|e| panic!("write {path}: {e}")); - patient(&format!("fsync {path}"), || f.sync_all()); - } // close: the last handle drops here. -} - -fn read_all(f: &mut fs::File) -> std::io::Result> { - let mut got = Vec::new(); - f.read_to_end(&mut got)?; - Ok(got) -} - -fn main() { - // The control. - write_file(CONTROL, VICTIM_BYTE); - let control = read_all(&mut fs::File::open(CONTROL).expect("open the control")) - .expect("read the control"); - assert_eq!(control.len(), LEN, "the control read short"); - assert!( - control.iter().all(|&b| b == VICTIM_BYTE), - "the backing did not serve the control file's own bytes", - ); - - write_file(VICTIM, VICTIM_BYTE); - - // Held open, and deliberately not read. - let mut held = fs::File::open(VICTIM).expect("open the victim"); - - fs::remove_file(VICTIM).expect("unlink the victim"); - - // The victim's clusters are the lowest free ones now, so this takes them. - write_file(ATTACKER, ATTACKER_BYTE); - - // **Refused, not zeroed.** A revoked backing has no bytes to serve and has - // to say so; the byte checks below are kept for the case where the refusal - // does not come, because a backing that still resolves serves either - // {ATTACKER_BYTE:#04x} or {VICTIM_BYTE:#04x} and neither is zero. - let refused = match read_all(&mut held) { - Err(e) => e, - Ok(got) => { - if let Some(at) = got.iter().position(|&b| b == ATTACKER_BYTE) { - panic!( - "byte {at} read through the deleted file's descriptor is \ - {ATTACKER_BYTE:#04x} — the backing served another file's data", - ); - } - if let Some(at) = got.iter().position(|&b| b != 0) { - panic!( - "byte {at} read through the deleted file's descriptor is {:#04x}, not \ - zero — the backing still resolves clusters the FAT has taken back", - got[at], - ); - } - panic!( - "the read through the deleted file's descriptor returned {} bytes and \ - succeeded; a revoked backing has no bytes to serve and has to say so", - got.len(), - ); - } - }; - - // The name is gone as well as the bytes, and a fresh open says so with the - // error a missing file gets rather than the one a revoked backing gets. - assert!(fs::File::open(VICTIM).is_err(), "the unlinked victim still opens by name"); - - // Left on the volume on purpose: the host reads both back off the image - // with its own FAT implementation after the shutdown. - println!( - "a read through a backing whose file was deleted was refused ({refused}) rather than \ - serving any of the next file's {LEN} bytes" - ); -} 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_dirs_durable.rs b/tests/toyos-rust-tests/src/bin/fs_dirs_durable.rs deleted file mode 100644 index 8a7b3ca28ef..00000000000 --- a/tests/toyos-rust-tests/src/bin/fs_dirs_durable.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Directories on the FAT `/log` volume are real: `mkdir` writes one the -//! volume keeps, a directory the mount grew for a file's path is visible and -//! removable once emptied, and every `rmdir` outcome is the real one. -//! `common::volumes::fs_dirs_durable` judges what this leaves off the raw -//! image — a directory the server only pretended to make is one `fatfs` -//! cannot see. - -use std::fs::{self, File}; -use std::io::{ErrorKind, Write}; - -/// Mirrored in `tests/common/volumes.rs`. -const KEEP: &str = "/log/fsdir-keep"; -const GONE: &str = "/log/fsdir-gone"; - -/// How many entries `path` lists, or the kind of its refusal. -fn entries(path: &str) -> Result { - fs::read_dir(path).map(|d| d.count()).map_err(|e| e.kind()) -} - -fn rmdir(path: &str) -> Result<(), ErrorKind> { - fs::remove_dir(path).map_err(|e| e.kind()) -} - -fn main() { - // POSIX mkdir(2): a new directory answers, a repeat is EEXIST. - fs::create_dir(KEEP).expect("mkdir on the FAT volume"); - let err = fs::create_dir(KEEP).expect_err("mkdir of an existing directory must refuse"); - assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists, "mkdir twice reported {err:?}"); - assert_eq!(entries(KEEP), Ok(0), "a fresh empty directory did not list as empty"); - - // A directory the mount created for a file's path, then emptied by that - // file's unlink: it must stay visible and become removable — on-disk - // state `created_dirs` never saw. - let file = format!("{GONE}/f.bin"); - let mut f = File::create(&file).expect("create under an implied directory"); - f.write_all(&[0x5C; 4096 + 33]).expect("write"); - f.sync_all().expect("fsync"); - drop(f); - assert_eq!( - rmdir(GONE), - Err(ErrorKind::InvalidInput), - "rmdir of a non-empty directory must refuse" - ); - assert_eq!( - rmdir(&file), - Err(ErrorKind::InvalidInput), - "rmdir of a file must refuse" - ); - fs::remove_file(&file).expect("unlink the file"); - assert_eq!( - entries(GONE), - Ok(0), - "an emptied on-disk directory disappeared from list" - ); - rmdir(GONE).expect("rmdir of the emptied directory"); - assert_eq!( - rmdir(GONE), - Err(ErrorKind::NotFound), - "rmdir of a removed directory must refuse" - ); - assert_eq!( - entries(GONE), - Err(ErrorKind::NotFound), - "a removed directory still lists" - ); - - println!("staged /log directories for the host oracle"); -} diff --git a/tests/toyos-rust-tests/src/bin/fs_rename_durable.rs b/tests/toyos-rust-tests/src/bin/fs_rename_durable.rs deleted file mode 100644 index fc7675197d6..00000000000 --- a/tests/toyos-rust-tests/src/bin/fs_rename_durable.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Stage, on the FAT `/log` volume, the rename a failed source used to destroy, -//! and leave the destination behind for `common::volumes::fs_rename_durable` to -//! judge off the raw image — a source the kernel is not. - -use std::fs::{self, File}; -use std::io::Write; - -/// Mirrored in `tests/common/volumes.rs`. -const VICTIM: &str = "/log/fstx-rename-victim.bin"; -const SELFED: &str = "/log/fstx-rename-self.bin"; -const ABSENT: &str = "/log/fstx-rename-absent.bin"; -const LEN: usize = 5 * 4096 + 33; - -fn payload() -> Vec { - (0..LEN).map(|i| (i.wrapping_mul(97) ^ 0x5A) as u8).collect() -} - -fn stage(path: &str) { - let mut f = File::create(path).unwrap_or_else(|e| panic!("create {path}: {e}")); - f.write_all(&payload()).expect("write the payload"); - f.sync_all().expect("fsync the payload"); -} - -fn main() { - let bytes = payload(); - let _ = fs::remove_file(ABSENT); - - stage(VICTIM); - let err = fs::rename(ABSENT, VICTIM) - .expect_err("rename of an absent source onto the victim must fail"); - assert_eq!(err.kind(), std::io::ErrorKind::NotFound, "rename(absent, victim) returned {err:?}"); - assert_eq!(fs::read(VICTIM).expect("victim gone after a failed rename"), bytes, - "a failed rename changed the victim's bytes in the kernel's view"); - - stage(SELFED); - fs::rename(SELFED, SELFED).expect("rename(p, p) must succeed"); - assert_eq!(fs::read(SELFED).expect("self file gone after rename(p, p)"), bytes, - "rename(p, p) changed the file in the kernel's view"); - - println!("staged /log rename victims for the host oracle"); -} 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/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 8e0fc3cd31e..00000000000 --- a/tests/toyos-rust-tests/src/bin/i8042_keyboard.rs +++ /dev/null @@ -1,75 +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 -/// and `i8042_mouse.rs`'s own `ended`. 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/i8042_mouse.rs b/tests/toyos-rust-tests/src/bin/i8042_mouse.rs deleted file mode 100644 index 7fedab1456a..00000000000 --- a/tests/toyos-rust-tests/src/bin/i8042_mouse.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Claims the mouse and prints every pointer event that arrives. -//! -//! Driven by the `i8042_mouse` host test, which paces its injection against -//! these lines: a packet goes out for each one printed here, so this is the -//! host's clock as well as its evidence. Not a standalone test — in RUST_SKIP -//! for that reason. - -use std::time::Duration; -use toyos::device::Mouse; -use toyos::endow::Endowments; -use toyos::syscap::SysCap; -use toyos_abi::syscall::{DeviceType, SYSCAP_LABEL}; - -const EVENT_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 mouse: Mouse = - capability().claim(DeviceType::Mouse).expect("i8042_mouse: no mouse device"); - println!("===I8042_MOUSE_READY==="); - - let mut buf = [0u8; 1024]; - let mut seen = 0; - let mut right_down = false; - let mut ended = false; - // No deadline: the run ends on the marker's release, and a machine that - // lost it is a hang the host's ceiling reds. - while !ended { - let n = mouse.read_nonblock(&mut buf).unwrap_or(0); - if n == 0 { - std::thread::sleep(Duration::from_millis(2)); - continue; - } - for chunk in buf[..n].chunks_exact(EVENT_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]]), - ); - seen += 1; - // The release ends the run, not the press: the host's framing - // assertion 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; - } - } - } - println!("mev 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 8822cb51199..00000000000 --- a/tests/toyos-rust-tests/src/bin/input_events.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! Claims both input devices and prints every event either one produces. -//! -//! Driven by `metal_sim_input` and `xhci_second_controller`, which inject -//! through QMP one step at a time and wait for these lines between steps — so -//! the host never has more in flight than the guest has taken. The line formats -//! are the ones `i8042_keyboard` and `i8042_mouse` already print, so the host -//! parses them with the same two functions. 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/launcher_refusals.rs b/tests/toyos-rust-tests/src/bin/launcher_refusals.rs deleted file mode 100644 index cbf2f97973f..00000000000 --- a/tests/toyos-rust-tests/src/bin/launcher_refusals.rs +++ /dev/null @@ -1,269 +0,0 @@ -//! What a client can make `/system/bin/init` do by sending it a bad launch. -//! -//! **init is the one process the machine cannot lose.** It holds the only -//! `SysCap`, every unhanded acceptor and the `launcher` port, and nothing -//! restarts it — so a client that can end it, panic it or grow its handle -//! table without bound takes the machine's ability to start a process with it. -//! Every field of `MSG_LAUNCH` and every handle in its batch is a client's -//! claim about itself, and the launcher connector is held by the compositor, -//! every terminal, every shell and sshd. -//! -//! Three shapes, each of which was reachable before this gate existed: -//! -//! 1. **A frame whose handle count is not the batch's.** The handles are -//! already in init's table when the count is checked, so a refusal that -//! returns without closing them leaks one per attempt — and a client picks -//! how often it attempts. Measured against the kernel's live-object census -//! rather than believed: the batch is a duplicate of a pipe end this -//! process then drops, so the object survives exactly if init kept it. -//! 2. **An extra that is not a connector.** init has no way to ask what a -//! handle it received names, so it hands it to `SYS_NAMESPACE_BUILD` — and -//! a wrong type there used to end the caller, which is init. -//! 3. **A connector the client narrowed `DUP` away from.** init duplicates a -//! provided connector so the namespace and the label can both carry one; a -//! duplicate that is refused used to be an `.expect`. -//! -//! The fourth arm is what stops the other three passing on a dead launcher: an -//! ordinary spawn, which goes through init because this process holds a -//! `launcher` connector and `/system/bin/toybox` is a declared program. It runs last, -//! so it also asserts init survived all three. -//! -//! **A fourth shape, and it is the one init could not survive at all: a client -//! that connects and says nothing.** `serve_launch`'s first statement was a -//! blocking `recv_header` on the fresh connection, so two syscalls from any -//! holder of the connector — the compositor, every terminal, every shell, sshd -//! — parked the machine's only way to start a process for ever, with init alive -//! and looking healthy. - -use std::process::Command; - -use toyos::census::Census; -use toyos::ipc::{Connection, FrameRx, RxStep}; -use toyos::launch::{self, Launch}; -use toyos::poller::{Poller, READABLE}; -use toyos::{namespace, port, AsHandle}; -use toyos_abi::handle::Rights; -use toyos_abi::syscall::{self, SyscallError}; -use toyos_abi::RawHandle; - -/// Rounds per census sample. Large enough that one leaked handle per round is -/// a number no drain lag can hide. -const ROUNDS: usize = 16; - -/// Clients that connect to the launcher and then say nothing, held open across -/// the launch that must still be answered. -/// -/// Well under init's own `MAX_PENDING_LAUNCHES`, because what is under test is -/// that a silent client costs a slot rather than the event loop — not the bound -/// on how many slots there are. -const QUIET_CLIENTS: usize = 8; - -/// A program `tests/netcase` declares that serves nothing and provides -/// nothing, so a refused launch of it takes no acceptor with it. -const DECLARED: &str = "/system/bin/toybox"; - -fn main() { - the_kernel_answers_rather_than_faults(); - a_quiet_client_does_not_wedge_the_launcher(); - not_a_connector(); - a_connector_it_cannot_duplicate(); - a_working_directory_that_is_not_absolute(); - - let before = churn(); - let after = churn(); - let grown: Vec<_> = after.grown_since(&before).collect(); - assert!( - grown.is_empty(), - "{ROUNDS} more refused launches left more live objects behind: {grown:?} — \ - first {before}, then {after}: init is keeping the handles a refusal took", - ); - println!(" census: {} live objects, then {}", before.total(), after.total()); - - the_launcher_still_works(); - println!("a bad launch is refused, and init is still the launcher"); -} - -fn launcher() -> Connection { - toyos::endow::service("launcher").expect("this process was endowed a launcher connector") -} - -/// The launcher's reply, waited for with no deadline: a launcher that never -/// answers is a hang the harness ceiling reds. -fn answer(conn: &Connection) -> Result { - let poller = Poller::new(1); - // Only the reply's type is judged here, so nothing of a payload is kept. - let mut rx = FrameRx::<0>::new(); - loop { - match rx.pump(conn) { - RxStep::Frame { msg_type, .. } => return Ok(msg_type), - RxStep::Eof => return Err("the launcher dropped the connection"), - RxStep::Malformed => return Err("the launcher sent a frame this protocol cannot describe"), - RxStep::Idle => { - poller.watch(conn, READABLE, 0); - poller.wait(1, u64::MAX, |_| {}); - } - } - } -} - -/// Clients that connect and go quiet, and a launch that must be answered anyway. -/// -/// Two silences, because they park a server at different statements: a -/// connection that never writes a byte, and one that writes half a header and -/// stops. The first is what `accept` used to be fused to; the second is what a -/// frame read in one blocking call used to wait out. -fn a_quiet_client_does_not_wedge_the_launcher() { - let quiet: Vec = (0..QUIET_CLIENTS).map(|_| launcher()).collect(); - let half = launcher(); - half.write_nonblock(&[0u8; 4]).expect("half a frame header"); - - // **A launch init answers and does not grant.** What is under test is that - // the event loop reaches a frame at all while other connections are silent; - // a *granted* launch would put a spawned process's output on init's own - // stdio, which is this boot's console, and hand back a `Process` handle for - // the census arm below to account for. - let conn = launcher(); - let mut buf = [0u8; 512]; - let request = Launch { - program: "/system/bin/no-such-program", - argv: b"", - env: b"", - cwd: "/", - extras: &[], - slots: &[], - }; - let len = request.encode(&mut buf).expect("encode a launch"); - conn.send_bytes_with_handles(&[], launch::MSG_LAUNCH, &buf[..len]) - .expect("the launcher took the frame"); - - match answer(&conn) { - Ok(launch::MSG_NOT_DECLARED) => {} - Ok(other) => panic!("the launcher answered {other} for a program nothing declares"), - Err(why) => panic!( - "{QUIET_CLIENTS} clients that said nothing and one that said half a header \ - left the machine unable to start a process: {why}", - ), - } - drop(quiet); - drop(half); - println!(" quiet clients: {QUIET_CLIENTS} silent and one half-spoken, and a launch still ran"); -} - -/// One frame that promises no handles, with two in the batch beside it. -/// -/// init cannot answer this — it does not know which handle was for what — so -/// there is no reply to read. What it must do is close them. -fn a_frame_that_lies() { - let (read, write) = toyos::pipe_pair().expect("a pipe of our own"); - let first = syscall::dup(write.as_handle()).expect("a duplicate to send"); - let second = syscall::dup(write.as_handle()).expect("a second duplicate to send"); - - let mut buf = [0u8; 512]; - let request = - Launch { program: DECLARED, argv: b"", env: b"", cwd: "/", extras: &[], slots: &[] }; - let len = request.encode(&mut buf).expect("encode a launch"); - - let conn = launcher(); - conn.send_bytes_with_handles(&[first, second], launch::MSG_LAUNCH, &buf[..len]) - .expect("the launcher took the frame"); - drop(conn); - // Both ends go, so the pipe's objects are alive after this only if init - // still holds one of the duplicates. - drop(read); - drop(write); -} - -fn churn() -> Census { - for _ in 0..ROUNDS { - a_frame_that_lies(); - } - // **A launch init answers, and deliberately not one it grants.** init is - // single-threaded and serves connections in the order they queued, so an - // answer to a request sent after the sixteen is the proof it has served - // every one of them. A *granted* launch would put a process exit inside - // the sampled window, and an exiting process leaves objects on the - // deferred release queue — the sample would be reading that lag. - assert_eq!(a_launch_it_refuses(), launch::MSG_REFUSED, "the synchronising launch was granted"); - Census::now() -} - -/// A launch init answers and does not grant: an extra naming a pipe where a -/// connector belongs. -fn a_launch_it_refuses() -> u32 { - let (_read, write) = toyos::pipe_pair().expect("a pipe of our own"); - let handle = syscall::dup(write.as_handle()).expect("a duplicate to send"); - refused_with(&[("surface", handle)]) -} - -fn not_a_connector() { - assert_eq!(a_launch_it_refuses(), launch::MSG_REFUSED); - println!(" not a connector: refused, and init is still here"); -} - -/// A real connector, narrowed so init cannot duplicate it. -fn a_connector_it_cannot_duplicate() { - let (_acceptor, connector) = port::create().expect("a port of our own"); - // Everything `SYS_NAMESPACE_BUILD` asks for and nothing `dup` does, so - // init gets past the namespace and fails on the label. - let narrowed = syscall::dup_narrowed(connector.as_handle(), Rights::TRANSFER) - .expect("a connector carrying only TRANSFER"); - assert_eq!(refused_with(&[("surface", narrowed)]), launch::MSG_REFUSED); - println!(" a connector it cannot duplicate: refused, and init is still here"); -} - -/// Send one launch carrying `extras` and answer the message type init replied -/// with. The reply is the liveness proof as much as the verdict. -fn refused_with(extras: &[(&str, RawHandle)]) -> u32 { - answer_to("/", extras) -} - -/// Send one launch from `cwd` carrying `extras`, and answer init's reply. -fn answer_to(cwd: &str, extras: &[(&str, RawHandle)]) -> u32 { - let mut buf = [0u8; 512]; - let request = Launch { program: DECLARED, argv: b"", env: b"", cwd, extras, slots: &[] }; - let (handles, count) = request.handles(); - let len = request.encode(&mut buf).expect("encode a launch"); - - let conn = launcher(); - conn.send_bytes_with_handles(&handles[..count], launch::MSG_LAUNCH, &buf[..len]) - .expect("the launcher took the frame"); - answer(&conn).expect("init answered the launch") -} - -/// A cwd the launch does not state absolutely. std would join it onto init's -/// own, so a grant here is the child started in a directory nobody named. -fn a_working_directory_that_is_not_absolute() { - for cwd in ["", "tmp"] { - assert_eq!(answer_to(cwd, &[]), launch::MSG_REFUSED, "a launch from cwd {cwd:?} was not refused"); - } - println!(" a working directory that is not absolute: refused, and init is still here"); -} - -/// The non-vacuity arm. `/system/bin/toybox` is a `[programs]` key, so a caller -/// holding a `launcher` connector reaches it through init and not through -/// `SYS_SPAWN` — this only passes while init is alive and still launching. -fn the_launcher_still_works() { - let out = Command::new(DECLARED) - .arg("pwd") - .output() - .expect("the launcher started a declared program"); - assert!(out.status.success(), "the launched program exited {:?}", out.status.code()); -} - -/// **The half of it that is the kernel's**, asserted from a process that can -/// afford to die so that init does not have to. -/// -/// `SYS_NAMESPACE_BUILD`'s added connector is the one handle argument in the -/// ABI that routinely crossed a trust boundary — a `provides` name is exactly -/// a connector somebody else made — so a wrong type there answers a word. Every -/// other `WrongType` in the table still ends the caller, and if this one goes -/// back to doing that, this arm never returns and the test reds on exit 139. -fn the_kernel_answers_rather_than_faults() { - let (_read, write) = toyos::pipe_pair().expect("a pipe of our own"); - // SAFETY: it is not a connector, which is the point — the call must answer - // a word rather than end this process. - let pretend = unsafe { port::Connector::from_raw(write.as_handle()) }; - let refused = namespace::build().add("surface", &pretend).finish(); - let _ = pretend.into_raw(); - assert_eq!(refused.err(), Some(SyscallError::InvalidArgument)); -} 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_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_accept.rs b/tests/toyos-rust-tests/src/bin/netd_refused_accept.rs deleted file mode 100644 index 7485fcb3464..00000000000 --- a/tests/toyos-rust-tests/src/bin/netd_refused_accept.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! An accept netd refuses for room still spends its owner's wake, so the -//! connection it left is announced again once room returns, and not before. -//! -//! The host dials this program's listener through the forward. Woken, this -//! program fills netd's connections to the host until one is refused, and -//! asks for the connection; netd refuses it for room. Once a request netd -//! answered after that refusal says room is still gone, no wake may be -//! waiting. One connection closed, the next wake is the verdict. -//! -//! 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_accept: ok` is the only success line. - -#[path = "../netd_stream.rs"] -mod netd_stream; - -use netd_stream::{ask, Ask, FORWARDED_PORT, HOST}; -use toyos::net::{ - MsgType, NetError, NetdConn, TcpAcceptPipedRequest, TcpAcceptPipedResponse, TcpBound, TcpConnectPipedRequest, - TcpConnectResponse, TcpConnection, TcpSocketId, DATA_FROM_CLIENT, DATA_HANDLES, DATA_TO_CLIENT, -}; -use toyos_abi::syscall::SyscallError; - -fn main() { - let port: u16 = std::env::args() - .nth(1) - .and_then(|p| p.parse().ok()) - .expect("usage: netd_refused_accept "); - let listener = toyos::net::tcp_bind([0; 4], FORWARDED_PORT).expect("bind the forwarded port"); - - let dial = toyos::net::tcp_connect(HOST, port, 0).expect("connect to the host server"); - ask(&dial.tx, Ask::Dial); - wake(&listener, "the host's dial"); - let mut held = Vec::new(); - let refused = loop { - match connect(port) { - Ok(conn) => { - ask(&conn.tx, Ask::Held(0)); - held.push(conn); - } - Err(e) => break e, - } - }; - assert_eq!(refused, NetError::ResourceExhausted, "a connect after {} held", held.len()); - assert_eq!( - accept(listener.socket_id), - Err(NetError::ResourceExhausted), - "an accept with every connection taken" - ); - println!("netd_refused_accept: an accept refused for room, {} connections held", held.len()); - assert_eq!( - connect(port).err(), - Some(NetError::ResourceExhausted), - "a connect after an accept refused for room" - ); - let mut byte = [0u8; 1]; - assert_eq!( - listener.notify.read_nonblock(&mut byte), - Err(SyscallError::WouldBlock), - "netd woke its owner for a connection there is no room to take" - ); - end(held.pop().expect("the cap holds at least the connection before the refusal")); - wake(&listener, "the connection an accept refused for room left, once room returned"); - accept(listener.socket_id).unwrap_or_else(|e| panic!("the connection an accept refused for room left: {e:?}")); - end(dial); - held.into_iter().for_each(end); - toyos::net::tcp_close(listener.socket_id).expect("close the listener"); - println!("netd_refused_accept: ok"); -} - -/// Wait for netd's wake on `listener`, and take it. -fn wake(listener: &TcpBound, what: &str) { - println!("netd_refused_accept: waiting for a wake for {what}"); - let mut byte = [0u8; 1]; - match listener.notify.read(&mut byte) { - Ok(1) => {} - Ok(_) => panic!("a wake for {what}: netd closed the listener"), - Err(e) => panic!("a wake for {what}: {e:?}"), - } -} - -/// netd's answer to an accept on `listener`. An accepted connection is closed -/// at once. -fn accept(listener: TcpSocketId) -> Result<(), NetError> { - let (_rx, _tx, handles) = data_path(); - let resp: TcpAcceptPipedResponse = reach_netd() - .request_with_handles(&handles, MsgType::TcpAcceptPiped, &TcpAcceptPipedRequest { socket_id: listener.0 }) - .expect("netd takes the request") - .response()?; - toyos::net::tcp_close(TcpSocketId(resp.socket_id)).expect("close the accepted connection"); - Ok(()) -} - -/// netd's answer to a connect to the host server. -fn connect(port: u16) -> Result { - let (rx, tx, handles) = data_path(); - let resp: TcpConnectResponse = reach_netd() - .request_with_handles( - &handles, - MsgType::TcpConnectPiped, - &TcpConnectPipedRequest { addr: HOST, port, _pad: 0, timeout_ms: 0 }, - ) - .expect("netd takes the request") - .response()?; - Ok(TcpConnection { rx, tx, socket_id: TcpSocketId(resp.socket_id), local_port: resp.local_port }) -} - -/// A connection to netd. **Refused only by the kernel's port queue**, which -/// `toyos::net` spells as netd's own `ResourceExhausted`, so it is no answer -/// of netd's here. -fn reach_netd() -> NetdConn { - NetdConn::connect().unwrap_or_else(|e| panic!("reach netd: {e:?}")) -} - -/// The ends of a duplex data path this side keeps, and the two it hands netd. -fn data_path() -> (toyos::Pipe, toyos::Pipe, [toyos_abi::RawHandle; DATA_HANDLES]) { - let (rx, to_client) = toyos::pipe_pair().expect("the pipe netd writes into"); - let (from_client, tx) = toyos::pipe_pair().expect("the pipe netd reads from"); - let mut handles = [toyos_abi::HANDLE_INVALID; DATA_HANDLES]; - handles[DATA_TO_CLIENT] = to_client.into_raw(); - handles[DATA_FROM_CLIENT] = from_client.into_raw(); - (rx, tx, handles) -} - -fn end(conn: TcpConnection) { - let TcpConnection { rx, tx, socket_id, .. } = conn; - drop((rx, tx)); - toyos::net::tcp_close(socket_id).expect("close a connection"); -} 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/nmi_window_spin.rs b/tests/toyos-rust-tests/src/bin/nmi_window_spin.rs deleted file mode 100644 index 7dd722641c3..00000000000 --- a/tests/toyos-rust-tests/src/bin/nmi_window_spin.rs +++ /dev/null @@ -1,76 +0,0 @@ -//! A Ring 3 loop that is inside `SYSCALL` as often as a program can be. -//! -//! **The victim half of `syscall_window_nmi`.** The kernel's storm sends NMIs -//! from another CPU; where each one lands is decided by this loop's timing, and -//! the window it has to land in is the three instructions of `arch::syscall`'s -//! entry that run at CPL 0 on this stack plus the one between its `pop rsp` and -//! its `sysretq`. Nothing here asserts: the counts are the kernel's, and -//! `tests/common/faults.rs` holds the verdict. -//! -//! **The loop is written as assembly because its instruction count is part of -//! the derivation.** Four instructions per iteration — `mov`, `syscall`, `dec`, -//! `jnz` — so the boundaries at which an NMI can be delivered while this program -//! is in Ring 3 are four per iteration, exactly as many as the window has. The -//! expected number of window arrivals is therefore the number of Ring 3 -//! arrivals, and the gate asserts against that ratio rather than against a -//! measurement. A `for` loop over `getpid()` would put the count at the mercy of -//! whatever the optimiser did that day. -//! -//! `SYS_GETPID` because it is the cheapest thing the kernel answers that takes -//! no argument and touches no state: what should dominate the iteration is the -//! entry and the exit, which is what the window is part of. - -use toyos_abi::clock::nanos_since_boot as clock_nanos; -use toyos_abi::syscall::SYS_GETPID; - -/// Iterations between two clock reads. Large enough that the clock's own -/// read is a rounding error in the mix, small enough to stop promptly. -const CHUNK: u64 = 50_000; - -fn main() { - let secs: u64 = std::env::args() - .nth(1) - .and_then(|a| a.parse().ok()) - .unwrap_or(10); - - println!("nmi-window-spin: spinning on SYS_GETPID for {secs}s"); - - let until = clock_nanos() + secs * 1_000_000_000; - let mut done: u64 = 0; - while clock_nanos() < until { - chunk(); - done += CHUNK; - } - - println!("nmi-window-spin: {done} syscalls"); -} - -/// [`CHUNK`] iterations of exactly four instructions. -fn chunk() { - let mut n = CHUNK; - // SAFETY: the ABI's own `syscall` sequence for `SYS_GETPID` — the number in - // `rdi`, the answer in `rax`, `rcx` and `r11` clobbered by the instruction - // itself — around a counted loop. Every register it writes is declared, the - // three the syscall clobbers by name so the allocator cannot put the counter - // in one, and it touches no memory: `nostack` is true because this kernel's - // entry parks `rsp` in per-CPU data rather than pushing on it. Irreducible - // for the reason the module header gives: the instruction count is the - // derivation, and no Rust loop has a stated one. - unsafe { - core::arch::asm!( - "2:", - "mov edi, {num}", - "syscall", - "dec {n}", - "jnz 2b", - num = const SYS_GETPID, - n = inout(reg) n, - out("rax") _, - out("rcx") _, - out("r11") _, - out("rdi") _, - options(nostack), - ); - } - assert_eq!(n, 0, "the counted loop did not run to zero"); -} 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 8aecf8fd8af..00000000000 --- a/tests/toyos-rust-tests/src/bin/partition_claimant.rs +++ /dev/null @@ -1,492 +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; -//! - `unanswered` — a claim while a disk does not answer a read of its table; -//! - `withheld ` — a claim of ROOT's source, whose disk did not answer -//! ROOT's hold and answers now; -//! - `deadman` — transfers whose every attempt is refused on its budget until -//! the deadman; -//! - `departure`, `silent`, `untold` — claims on one USB stick whose device -//! leaves owing a flush and comes back: whose fsync answers for which writes, -//! across a close, and what is left when nobody asks. - -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 partitions of the stick whose device leaves. -const DEPARTING: &str = "1F3E5D7C-9B2A-4C6E-8F01-A3B5C7D9E2F4"; -const STAYING: &str = "2A4C6E80-1B3D-4F57-9E6A-C8D0B2F4A6E1"; -const EARLIER: &str = "4C6E8A02-3D5F-4179-A0B2-D4F6A8C0E2B4"; - -/// 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"; - -/// Mirrored: what the departure writes, block `n` of partition `which`. -fn departure_block(which: u8, n: u64) -> Block { - let mut block = [which; BLOCK_BYTES]; - block[..8].copy_from_slice(&n.to_le_bytes()); - block[8..24].copy_from_slice(b"TOYOS-DEPARTURE\0"); - block -} - -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(), - Some("unanswered") => unanswered(&cap), - Some("withheld") => withheld(&cap, &args[1..]), - Some("deadman") => deadman(&cap), - Some("departure") => departure(&cap), - Some("silent") => silent(&cap), - Some("untold") => untold(&cap), - 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"); -} - -/// A disk that did not answer a read of its table makes the answer unknown: -/// the claim is refused, not resolved on the disks that did answer. -fn unanswered(cap: &SysCap) { - refused( - cap, - "the target, while its disk does not answer a read of its table,", - guid(TARGET), - SyscallError::NotSupported, - ); - println!("partition_claimant: PASS"); -} - -/// ROOT's source, which the boot withheld when its disk did not answer: the -/// disk answers now and nothing holds the span, and the claim is still the -/// kernel's to refuse. -fn withheld(cap: &SysCap, root: &[String]) { - let [root] = root else { panic!("withheld takes ROOT's GUID, got {root:?}") }; - refused( - cap, - "ROOT, withheld when its disk did not answer the boot's hold,", - guid(root), - SyscallError::PermissionDenied, - ); - println!("partition_claimant: PASS"); -} - -/// Every attempt of a transfer is refused on its budget until the deadman: each -/// ends `Io`, the device's word, and not another ask-again. (NVMe's flush asks -/// the device nothing, so it has no budget to refuse.) -fn deadman(cap: &SysCap) { - let target = claim(cap, guid(TARGET)).expect("the target is claimable"); - let mut one = [[0u8; BLOCK_BYTES]]; - assert_eq!(target.write(0, &one), Err(SyscallError::Io), "a write past the deadman"); - assert_eq!(target.read(0, &mut one), Err(SyscallError::Io), "a read past the deadman"); - println!("partition_claimant: PASS"); -} - -/// Two claims on one USB stick. `STAYING` writes and is flushed; `DEPARTING` -/// writes a block, and its next write is the one `usb-transport-break-owed` -/// breaks, so the stick's device leaves holding that first block unflushed and -/// the host moves it to another port. When it is back, the first flush asked -/// is `STAYING`'s, which wrote nothing that was lost. `DEPARTING` is then -/// closed and claimed again — the updater's write, close, reopen, fsync — and -/// that fsync is the one told: the loss is the partition's, not the handle's. -fn departure(cap: &SysCap) { - let staying = claim(cap, guid(STAYING)).expect("the staying partition is claimable"); - let departing = claim(cap, guid(DEPARTING)).expect("the departing partition is claimable"); - - staying.write(0, &[departure_block(b'S', 0)]).expect("the staying write"); - staying.sync().expect("the staying write, flushed before anything left"); - departing.write(0, &[departure_block(b'D', 0)]).expect("the first departing write"); - println!("partition_claimant: a write is reported and not flushed"); - departing - .write(1, &[departure_block(b'D', 1)]) - .expect("the write the device left under, sent again on it when it came back"); - println!("partition_claimant: the write the device left under completed"); - - assert_eq!( - staying.sync(), - Ok(()), - "a claim that wrote nothing the departure lost was told of the loss" - ); - drop(departing); - let departing = reclaimed(cap, guid(DEPARTING), "the claim that wrote it closed"); - assert_eq!( - departing.sync(), - Err(SyscallError::Io), - "a partition whose write was in the cache of the device that left was told it is \ - durable, once the claim that wrote it had closed" - ); - said("the departing partition's flush, over a write the device lost,", SyscallError::Io); - assert_eq!(departing.sync(), Ok(()), "a loss is told once"); - - departing - .write(0, &[departure_block(b'D', 0), departure_block(b'D', 1)]) - .expect("the lost write, written again"); - departing.sync().expect("the rewrite is durable"); - let mut back = [[0u8; BLOCK_BYTES]; 2]; - departing.read(0, &mut back).expect("read the rewrite back"); - assert!(back[0] == departure_block(b'D', 0) && back[1] == departure_block(b'D', 1)); - println!("partition_claimant: PASS"); -} - -/// `logd`'s `/log` on the boot stick: `DEPARTING` writes a block and never -/// writes again, and another claim's write is the one the device leaves -/// under. `EARLIER` wrote before `STAYING`'s flush made its write durable — -/// `STAYING` flushes having written nothing, since a second write before that -/// flush would be the one the device leaves under. When the device is back, -/// `STAYING` flushes first: it lost nothing, and its flush settles every -/// account. `EARLIER` lost nothing either, because the flush that made its -/// write durable came before the departure; `DEPARTING` is told, though -/// another claim's flush came first. -fn silent(cap: &SysCap) { - let earlier = claim(cap, guid(EARLIER)).expect("the earlier partition is claimable"); - let staying = claim(cap, guid(STAYING)).expect("the staying partition is claimable"); - let departing = claim(cap, guid(DEPARTING)).expect("the departing partition is claimable"); - - earlier.write(0, &[departure_block(b'E', 0)]).expect("the earlier write"); - staying.sync().expect("another claim's flush, which makes it durable, before anything left"); - departing.write(0, &[departure_block(b'D', 0)]).expect("the departing write"); - println!("partition_claimant: a write is reported and not flushed"); - staying - .write(0, &[departure_block(b'S', 0)]) - .expect("the write the device left under, sent again on it when it came back"); - println!("partition_claimant: the write the device left under completed"); - - assert_eq!( - staying.sync(), - Ok(()), - "a claim that wrote nothing the departure lost was told of the loss" - ); - assert_eq!( - earlier.sync(), - Ok(()), - "a claim whose write a flush made durable before the departure was told it was lost" - ); - assert_eq!( - departing.sync(), - Err(SyscallError::Io), - "a claim whose write the device that left had not flushed, and which wrote nothing \ - after, was told it is durable because another claim flushed first" - ); - said("the silent claim's flush, over a write the device lost,", SyscallError::Io); - assert_eq!(departing.sync(), Ok(()), "a loss is told once"); - println!("partition_claimant: PASS"); -} - -/// A loss nobody asks about: `DEPARTING` writes a block, the device leaves -/// under `STAYING`'s next write, and both claims close with no fsync. The -/// shutdown's flush of the disk is what is left to say so, and the host reads -/// that it did. -fn untold(cap: &SysCap) { - let staying = claim(cap, guid(STAYING)).expect("the staying partition is claimable"); - let departing = claim(cap, guid(DEPARTING)).expect("the departing partition is claimable"); - departing.write(0, &[departure_block(b'D', 0)]).expect("the departing write"); - println!("partition_claimant: a write is reported and not flushed"); - staying - .write(0, &[departure_block(b'S', 0)]) - .expect("the write the device left under, sent again on it when it came back"); - println!("partition_claimant: the write the device left under completed"); - drop(departing); - drop(staying); - println!("partition_claimant: PASS"); -} 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_last.rs b/tests/toyos-rust-tests/src/bin/quiesce_last.rs deleted file mode 100644 index 6500834ae55..00000000000 --- a/tests/toyos-rust-tests/src/bin/quiesce_last.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! The threads the kernel's `quiesce-last-*` actuators can hold, and a reboot. -//! -//! They carry [`toyos_quiesce::LAST_THREAD`]'s name. One parks for longer than -//! any stop's budget, and one is the only thread of a child that exits at once. -//! `quiesce-last-park` holds the first inside its `SYS_NANOSLEEP`, and -//! `quiesce-last-teardown` the second between leaving its process and tearing -//! it down. The kernel holds the reset itself until one of them is held, so -//! this program orders nothing. -//! -//! Nothing here asserts: the kernel's hold line and its `stop:` record are -//! read elsewhere. - -use std::process::Command; -use std::time::Duration; - -use toyos::power::Stop; -use toyos_quiesce::LAST_THREAD; - -const SELF_PATH: &str = "/system/bin/test_rs_quiesce_last"; - -/// Longer than any stop's budget: a park the timer ends inside the stop would -/// come back to Ring 3 and be banded there, and that band's post would wake the -/// stop in place of the park's. -const PARKED_FOR: Duration = Duration::from_secs(3_600); - -fn main() { - if std::env::args().nth(1).as_deref() == Some("teardown") { - toyos_abi::syscall::set_thread_name(LAST_THREAD.as_bytes()); - std::process::exit(0); - } - std::thread::Builder::new() - .name(LAST_THREAD.into()) - .spawn(park) - .expect("spawn a thread the kernel may hold"); - let _child = Command::new(SELF_PATH).arg("teardown").spawn().expect("spawn a process the kernel may hold"); - - // Comes back only refused. - let refused = toyos::power::stop(Stop::Reboot); - eprintln!("quiesce_last: the reboot was refused ({refused:?})"); - std::process::exit(1); -} - -fn park() { - std::thread::sleep(PARKED_FOR); -} 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/quiesce_writers.rs b/tests/toyos-rust-tests/src/bin/quiesce_writers.rs deleted file mode 100644 index 67cffc0cdd6..00000000000 --- a/tests/toyos-rust-tests/src/bin/quiesce_writers.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! Writers that never stop, and a reboot underneath them. -//! -//! **The shape the shutdown's two claims are false in.** `quiesce` syncs every -//! filesystem and then says `Rebooting.`; both are claims about a machine, and -//! a machine with threads still writing when they are made is a machine they -//! are not true of. So this boot puts [`WRITERS`] threads into an unbounded -//! write-and-fsync loop, waits for every one of them to say it has finished a -//! pass, and then asks for the reset from a thread that is not one of them. -//! -//! Nothing here asserts: `common::power::quiesce_stops_the_machine` reads the -//! kernel's own `stop:` record and the order of the console around it, which -//! are the two things a guest cannot see about its own death. - -use std::fs::File; -use std::io::Write; -use std::sync::mpsc; - -use toyos::power::Stop; - -/// Threads writing when the reset is asked for. More than one CPU's worth on -/// the harness's guest, so the reset cannot simply find them all descheduled. -const WRITERS: usize = 6; - -/// Bytes per write: over a page, so each one is a real block-layer operation -/// rather than a page-cache touch. -const CHUNK: usize = 8192; - -/// What a writer says every pass. -const WRITING: &str = "quiesce-writer:"; - -fn main() { - // One word per writer that has finished a pass. **The reset is asked for - // over a machine every writer is known to be working on**: a writer still - // being spawned when the last word is written puts no line above it, which - // is a boot that had nothing to stop. - let (in_the_loop, first_passes) = mpsc::channel::(); - for writer in 0..WRITERS { - let in_the_loop = in_the_loop.clone(); - std::thread::Builder::new() - .name(format!("writer{writer}")) - .spawn(move || { - let path = format!("/log/quiesce-{writer}.bin"); - let payload = [b'q'; CHUNK]; - for pass in 0u64.. { - // One per pass, so the rate is the write's and not a spin's. - println!("{WRITING} {writer} {pass}"); - // Reopened each pass: the close is what puts the last - // chunk's pages where only a sync can reach them. - // - // A writer that fails says so and is gone; the harness - // counts the threads the kernel stopped, so one fewer is - // the red, and this line is its reason. - let mut f = match File::create(&path) { - Ok(f) => f, - Err(e) => { - eprintln!("{WRITING} {writer} could not create {path}: {e}"); - return; - } - }; - for _ in 0..8 { - if let Err(e) = f.write_all(&payload) { - eprintln!("{WRITING} {writer} could not write {path}: {e}"); - return; - } - } - if let Err(e) = f.sync_all() { - eprintln!("{WRITING} {writer} could not sync {path}: {e}"); - return; - } - if pass == 0 { - in_the_loop.send(writer).expect("main holds the receiver"); - } - } - }) - .expect("spawn a writer"); - } - // The writers hold the only senders: a writer that failed is a closed - // channel here once it is the last. - drop(in_the_loop); - - // No deadline: a writer that never reaches its loop is a hang the harness - // ceiling reds. - for reached in 0..WRITERS { - if first_passes.recv().is_err() { - eprintln!("quiesce_writers: {reached} of {WRITERS} writers reached their loop"); - std::process::exit(1); - } - } - println!("{WRITERS} writers are running; asking for the reset"); - - // Comes back only refused: on the other path the machine is already at its - // firmware, and every writer above is still mid-loop when it goes. - let refused = toyos::power::stop(Stop::Reboot); - eprintln!("quiesce_writers: the reboot was refused ({refused:?})"); - std::process::exit(1); -} diff --git a/tests/toyos-rust-tests/src/bin/redirty_mid_flush.rs b/tests/toyos-rust-tests/src/bin/redirty_mid_flush.rs deleted file mode 100644 index 4e57237e698..00000000000 --- a/tests/toyos-rust-tests/src/bin/redirty_mid_flush.rs +++ /dev/null @@ -1,121 +0,0 @@ -//! Two processes racing one page of one `/log` file: a write that lands while -//! a flush has that page copied but not yet marked clean must survive to the -//! device (F6). The parent dirties [`PAGES`] pages and fsyncs; the child lands -//! one 8-byte slot write in page 0 after a swept delay, so across [`ROUNDS`] -//! the write falls inside the copy-to-clear window many times. Each round -//! evicts page 0 (`test-small-caches` arms the pressure) and reads it back off -//! the device: every slot written so far must be there, and a cleared -//! mid-flush redirty loses exactly one slot forever. -//! `tests/common/volumes.rs::redirty_mid_flush` boots this and re-judges the -//! final bytes off the image. - -use std::fs::{File, OpenOptions}; -use std::io::{BufRead, BufReader, Read, Seek, SeekFrom, Write}; -use std::process::{Command, Stdio}; -use std::time::{Duration, Instant}; - -const PATH: &str = "/log/redirty.bin"; -const JUNK: &str = "/log/redirty-junk.bin"; -/// Width of each round's dirty set: page 0 is copied first and marked clean -/// last, so its exposure is the other pages' device writes. -const PAGES: usize = 12; -/// Mirrored in `tests/common/volumes.rs::redirty_mid_flush`, with `SLOTS_AT`. -const ROUNDS: u64 = 128; -/// The child's slot array in page 0; one slot per round, never rewritten, so a -/// lost slot stays lost. The parent's own stripe is at [`PARENT_AT`] per page. -const SLOTS_AT: u64 = 64; -const PARENT_AT: u64 = 2048; -const JUNK_PAGES: usize = 72; - -fn main() { - if std::env::args().nth(1).as_deref() == Some("child") { - return child(); - } - parent(); -} - -fn child() { - let mut f = OpenOptions::new().write(true).open(PATH).expect("child open"); - let stdin = std::io::stdin(); - for line in stdin.lock().lines() { - let line = line.expect("child stdin"); - if line == "done" { - return; - } - let mut parts = line.split(' '); - let round: u64 = parts.next().expect("round").parse().expect("round"); - let delay_us: u64 = parts.next().expect("delay").parse().expect("delay"); - let start = Instant::now(); - while start.elapsed() < Duration::from_micros(delay_us) { - std::hint::spin_loop(); - } - f.seek(SeekFrom::Start(SLOTS_AT + round * 8)).expect("seek slot"); - f.write_all(&round.to_le_bytes()).expect("slot write"); - println!("did {round}"); - } -} - -fn parent() { - let mut f = File::create(PATH).expect("create"); - f.write_all(&vec![0u8; PAGES * 4096]).expect("fill"); - f.sync_all().expect("first fsync"); - { - let mut junk = File::create(JUNK).expect("create junk"); - junk.write_all(&vec![0xEEu8; JUNK_PAGES * 4096]).expect("junk write"); - junk.sync_all().expect("junk fsync"); - } - - let mut child = Command::new("/system/bin/test_rs_redirty_mid_flush") - .arg("child") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .expect("spawn child"); - let mut go = child.stdin.take().expect("child stdin"); - let mut ack = BufReader::new(child.stdout.take().expect("child stdout")); - - for round in 0..ROUNDS { - for page in 0..PAGES { - f.seek(SeekFrom::Start(page as u64 * 4096 + PARENT_AT)).expect("seek stripe"); - f.write_all(&round.to_le_bytes()).expect("stripe write"); - } - writeln!(go, "{round} {}", (round * 131) % 5000).expect("send go"); - f.sync_all().expect("racing fsync"); - let mut line = String::new(); - ack.read_line(&mut line).expect("read ack"); - assert_eq!(line.trim(), format!("did {round}"), "child fell out of step"); - f.sync_all().expect("delivering fsync"); - evict(); - check_slots(round); - } - writeln!(go, "done").expect("send done"); - assert!(child.wait().expect("child wait").success(), "child failed"); - println!("{ROUNDS} racing rounds: every mid-flush write survived to the device"); -} - -/// Push page 0 out of the shrunken cache so the next read is the device's word. -fn evict() { - let mut junk = File::open(JUNK).expect("open junk"); - let mut buf = vec![0u8; 4096]; - for _ in 0..2 { - junk.seek(SeekFrom::Start(0)).expect("rewind junk"); - for _ in 0..JUNK_PAGES { - junk.read_exact(&mut buf).expect("junk read"); - } - } -} - -fn check_slots(upto: u64) { - let mut f = File::open(PATH).expect("re-open"); - f.seek(SeekFrom::Start(SLOTS_AT)).expect("seek slots"); - let mut raw = vec![0u8; (upto as usize + 1) * 8]; - f.read_exact(&mut raw).expect("slot read"); - for slot in 0..=upto { - let got = u64::from_le_bytes(raw[slot as usize * 8..][..8].try_into().expect("8 bytes")); - assert_eq!( - got, slot, - "slot {slot} reads {got} after eviction: a write that landed mid-flush was marked \ - clean and never reached the device" - ); - } -} diff --git a/tests/toyos-rust-tests/src/bin/smp_hole_shootdown.rs b/tests/toyos-rust-tests/src/bin/smp_hole_shootdown.rs deleted file mode 100644 index 2cca51988d6..00000000000 --- a/tests/toyos-rust-tests/src/bin/smp_hole_shootdown.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Survives a TLB shootdown on the `smp-skip-ap` boot, where a non-last AP never -//! starts. The unfixed kernel leaves a dead slot in `0..cpu_count()` and the next -//! shootdown waits on a CPU that never existed and panics; the dense machine -//! returns and this marker prints. Each `munmap` frees 2 MiB back and shoots down. - -use toyos_abi::syscall::{self, MmapFlags, MmapProt}; - -const PAGE_2M: usize = 2 * 1024 * 1024; - -/// Enough that a boot which happened to skip one shootdown is not why it survived. -const ROUNDS: usize = 8; - -fn main() { - for _ in 0..ROUNDS { - let p = unsafe { - syscall::mmap( - core::ptr::null_mut(), - PAGE_2M, - MmapProt::READ | MmapProt::WRITE, - MmapFlags::ANONYMOUS | MmapFlags::PRIVATE, - ) - }; - assert!(!p.is_null(), "mmap failed"); - // Cache a translation before freeing, so the free is a real shootdown target. - unsafe { core::ptr::write_volatile(p, 0xA5) }; - unsafe { syscall::munmap(p, PAGE_2M) }.expect("munmap"); - } - println!("smp_hole_shootdown: survived {ROUNDS} shootdowns"); -} 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_crash.rs b/tests/toyos-rust-tests/src/bin/swap_crash.rs deleted file mode 100644 index dfd1fb449a0..00000000000 --- a/tests/toyos-rust-tests/src/bin/swap_crash.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Panic at once. The binary a swap's negative control puts in a running -//! service's place: a replacement that does not start, which init must answer -//! by starting the binary it replaced again. - -fn main() { - panic!("swap_crash: this binary ends the instant it starts"); -} 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

(handle) +} diff --git a/bootloader/src/loaderlog.rs b/bootloader/src/loaderlog.rs index 2d431a1c03c..5e7f1445d6e 100644 --- a/bootloader/src/loaderlog.rs +++ b/bootloader/src/loaderlog.rs @@ -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::exclusive::open::(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::exclusive::open::(bs, handle) { Ok(fs) => fs, Err(e) => return refused(format_args!("the log partition would not open ({e})")), }; diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index b2b607b6714..7633ed818e1 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -42,6 +42,7 @@ mod arch; mod attempt; mod blackbox; mod bootnext; +mod exclusive; 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 = exclusive::open::(bs, handle).ok()?; let device = image.device()?; - let path = bs.open_protocol_exclusive::(device).ok()?; + let path = exclusive::open::(bs, device).ok()?; let is_hard_drive = |node: &&DevicePathNode| { node.full_type() == (DeviceType::MEDIA, DeviceSubType::MEDIA_HARD_DRIVE) @@ -519,7 +520,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 +554,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 = exclusive::open::(bs, bs.image_handle()) .expect("firmware answers LoadedImage for the image it started"); let (base, size) = image.info(); (base as u64, size) @@ -612,8 +612,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 +640,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 +959,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 +976,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/rootimage.rs b/bootloader/src/rootimage.rs index 3609e088bab..359b6a19584 100644 --- a/bootloader/src/rootimage.rs +++ b/bootloader/src/rootimage.rs @@ -92,8 +92,7 @@ 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::exclusive::open::(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) diff --git a/bootloader/src/slot.rs b/bootloader/src/slot.rs index 424bcfe8764..1f21cdd3b6c 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::exclusive::open::(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..63c23c63f38 100644 --- a/clippy.toml +++ b/clippy.toml @@ -4,4 +4,5 @@ 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 `exclusive::open`" }, ] diff --git a/kernel/src/actuator.rs b/kernel/src/actuator.rs index 57b7703102d..9005c5d86a1 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"; @@ -70,9 +61,6 @@ actuators! { /// 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"; - /// 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"; @@ -117,25 +105,19 @@ actuators! { /// a quantum, and take its interrupts with them open. timer_floor = "timer-floor"; + /// Leave every AP holding the CR0/CR4 that INIT left it. + no_ap_control_regs = "no-ap-control-regs"; + /// Skip the startup for the AP that would be cpu2, so a non-last AP never starts. smp_skip_ap = "smp-skip-ap"; + /// Time the same read loop on every CPU, either side of the `mov cr0` that enables caching. + control_regs_bench = "control-regs-bench"; + /// Issue a fixed count of machine-wide TLB shootdowns against every CPU the /// machine brought up, with nothing else running, and report the distribution. tlb_shootdown_bench = "tlb-shootdown-bench"; - /// 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"; @@ -300,10 +282,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/x86_64/boot.rs b/kernel/src/arch/x86_64/boot.rs index 21354011ba8..341a7edd204 100644 --- a/kernel/src/arch/x86_64/boot.rs +++ b/kernel/src/arch/x86_64/boot.rs @@ -53,10 +53,11 @@ pub fn after_console(_args: &KernelArgs, _maps: &[MemoryMapEntry]) { if crate::actuator::test_early_fault() { panic!("test-early-fault: x86-64 has no vectors of its own this early"); } - // `pat::init` restored the `CR0` it found, so a firmware + // After actuator::init, whose table the `control-regs-bench` probe inside + // this call reads. `pat::init` restored the `CR0` it found, so a firmware // `CD` — which would make every mapping uncacheable whatever the PAT // says — ends here. - control_regs::init_cr0(); + control_regs::init_cr0(0); // The read-back `pat::init` owes, on a boot that now has three channels to // carry a refusal. diff --git a/kernel/src/arch/x86_64/control_regs.rs b/kernel/src/arch/x86_64/control_regs.rs index bf8223120d9..d1c42c00d0d 100644 --- a/kernel/src/arch/x86_64/control_regs.rs +++ b/kernel/src/arch/x86_64/control_regs.rs @@ -85,39 +85,45 @@ static CHECKED: AtomicU64 = AtomicU64::new(0); /// restores the `CR0` it finds, so a firmware `CD` survives that write and is /// cleared here: every AP runs this first, and the BSP runs it after, because /// the BSP's `pat::init` has to precede the panel it would report a refusal on. -pub fn init_cr0() { - let live = cpu::read_cr0(); - if live & (cr0::CD | cr0::NW) != 0 { - // SDM Vol. 3A §11.5.3's no-fill sequence: `CD` set, `NW` clear, - // then write-back-invalidate — required when crossing cache states. - // SAFETY: only `CD`/`NW` change in `write_cr0`; `wbinvd` runs inside - // the no-fill window the write just opened (SDM Vol. 3A §11.5.3). - unsafe { - cpu::write_cr0((live | cr0::CD) & !cr0::NW); - cpu::wbinvd(); +pub fn init_cr0(cpu_id: u32) { + let before = bench::sample(); + if !skipped(cpu_id) { + let live = cpu::read_cr0(); + if live & (cr0::CD | cr0::NW) != 0 { + // SDM Vol. 3A §11.5.3's no-fill sequence: `CD` set, `NW` clear, + // then write-back-invalidate — required when crossing cache states. + // SAFETY: only `CD`/`NW` change in `write_cr0`; `wbinvd` runs inside + // the no-fill window the write just opened (SDM Vol. 3A §11.5.3). + unsafe { + cpu::write_cr0((live | cr0::CD) & !cr0::NW); + cpu::wbinvd(); + } } + // SAFETY: `CR0`'s value is this file's declaration, argued in its own + // doc comment. + unsafe { cpu::write_cr0(CR0) }; } - // SAFETY: `CR0`'s value is this file's declaration, argued in its own - // doc comment. - unsafe { cpu::write_cr0(CR0) }; + bench::report(cpu_id, before); } /// Puts this CPU's `CR4` and `EFER` into the declaration and checks all /// three against it. Must run after [`init_cr0`] and before `arch::syscall::init`, which needs `SCE` set. pub fn init(cpu_id: u32) { let declared = declaration(cpu_id); - // SAFETY: `write_cr4` faults only on an undefined bit, on clearing `PAE` - // in long mode, or on `PCIDE` with a nonzero PCID — `declaration` checked - // the first two and both callers use PCID 0; `wrmsr` writes [`EFER`], whose - // bits `declaration` has just confirmed this CPU defines. - unsafe { - cpu::write_cr4(declared); - cpu::wrmsr(efer::MSR, EFER); - } - if declared & cr4::SMAP != 0 { - // Nothing in this kernel sets `RFLAGS.AC`, so this is the only - // `clac` the kernel needs. - cpu::clac(); + if !skipped(cpu_id) { + // SAFETY: `write_cr4` faults only on an undefined bit, on clearing `PAE` + // in long mode, or on `PCIDE` with a nonzero PCID — `declaration` checked + // the first two and both callers use PCID 0; `wrmsr` writes [`EFER`], whose + // bits `declaration` has just confirmed this CPU defines. + unsafe { + cpu::write_cr4(declared); + cpu::wrmsr(efer::MSR, EFER); + } + if declared & cr4::SMAP != 0 { + // Nothing in this kernel sets `RFLAGS.AC`, so this is the only + // `clac` the kernel needs. + cpu::clac(); + } } self_check(cpu_id, declared); } @@ -288,3 +294,59 @@ fn opt(value: u64, bit: u64, name: &'static str) -> &'static str { if value & bit != 0 { name } else { "" } } +/// Cycles the caching probe took. Bare metal only — QEMU models no cache and +/// KVM never holds `CD` — read via `--kernel-param control-regs-bench`. +#[cfg(feature = "boot-actuators")] +mod bench { + use super::cpu; + use crate::log; + + /// Bigger than any L1, inside every L2 this kernel targets. + const LINES: usize = 4096; + const STRIDE: usize = 8; + static PROBE: [u64; LINES * STRIDE] = [0; LINES * STRIDE]; + + pub fn sample() -> u64 { + if !crate::actuator::control_regs_bench() { + return 0; + } + let start = cpu::rdtsc(); + let mut acc = 0u64; + let mut i = 0; + while i < PROBE.len() { + // SAFETY: `i < PROBE.len()` keeps the index in bounds. + acc = acc.wrapping_add(unsafe { core::ptr::read_volatile(&raw const PROBE[i]) }); + i += STRIDE; + } + let end = cpu::rdtsc(); + core::hint::black_box(acc); + end.wrapping_sub(start) + } + + pub fn report(cpu_id: u32, before: u64) { + if !crate::actuator::control_regs_bench() { + return; + } + let cold = sample(); + let warm = sample(); + log!( + "control_regs: cpu{} probe {} lines: pre={} cold={} warm={} cycles", + cpu_id, LINES, before, cold, warm, + ); + } +} + +#[cfg(not(feature = "boot-actuators"))] +mod bench { + pub fn sample() -> u64 { + 0 + } + pub fn report(_cpu_id: u32, _before: u64) {} +} + +/// The negative control: leaves an AP holding what `INIT` left it, since no +/// QEMU flag can stage a divergent control register any other way. +fn skipped(cpu_id: u32) -> bool { + crate::actuator::no_ap_control_regs() && cpu_id != 0 +} + diff --git a/kernel/src/arch/x86_64/i8042/mod.rs b/kernel/src/arch/x86_64/i8042/mod.rs index 97a620c92a4..16b3dcbfe59 100644 --- a/kernel/src/arch/x86_64/i8042/mod.rs +++ b/kernel/src/arch/x86_64/i8042/mod.rs @@ -562,7 +562,7 @@ fn service_bytes(recorded: bool) { } } - let Drained { 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. @@ -581,7 +581,6 @@ fn service_bytes(recorded: bool) { } struct Drained { - bytes: usize, keys: usize, motion: usize, aux_reset: bool, @@ -592,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); @@ -610,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 { diff --git a/kernel/src/arch/x86_64/smp.rs b/kernel/src/arch/x86_64/smp.rs index 438558bfdc7..e16c55118e5 100644 --- a/kernel/src/arch/x86_64/smp.rs +++ b/kernel/src/arch/x86_64/smp.rs @@ -273,7 +273,7 @@ extern "C" fn ap_entry() -> ! { AP_TSC.store(cpu::rdtsc(), Ordering::Release); // Must run before `pat::init`, which restores the CR0 this call sets. - crate::arch::control_regs::init_cr0(); + crate::arch::control_regs::init_cr0(percpu::cpu_id()); // Must run before this CPU touches the framebuffer, which needs write-combining mapped first. crate::arch::pat::init(); diff --git a/kernel/src/drivers/virtio.rs b/kernel/src/drivers/virtio.rs index ef66ddb0718..ef6e7ab4934 100644 --- a/kernel/src/drivers/virtio.rs +++ b/kernel/src/drivers/virtio.rs @@ -307,32 +307,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. @@ -386,14 +360,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. @@ -431,7 +397,6 @@ impl<'pool> Virtqueue<'pool> { notify_offset: 0, used_split: false, chain_bytes: alloc::vec![0u32; queue_size as usize], - refused: 0, } } @@ -570,7 +535,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"); @@ -585,36 +550,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)) + 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`. diff --git a/kernel/src/drivers/xhci/mod.rs b/kernel/src/drivers/xhci/mod.rs index 6628d4cf301..fdfa7ef0396 100644 --- a/kernel/src/drivers/xhci/mod.rs +++ b/kernel/src/drivers/xhci/mod.rs @@ -1583,13 +1583,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() { diff --git a/kernel/src/drivers/xhci/wait/msc.rs b/kernel/src/drivers/xhci/wait/msc.rs index 9c94501602a..c6c4de0dc0e 100644 --- a/kernel/src/drivers/xhci/wait/msc.rs +++ b/kernel/src/drivers/xhci/wait/msc.rs @@ -428,17 +428,6 @@ 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_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 { @@ -565,9 +554,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 \ diff --git a/kernel/src/main.rs b/kernel/src/main.rs index 16a87ad92e9..9369fc7b1a7 100644 --- a/kernel/src/main.rs +++ b/kernel/src/main.rs @@ -276,13 +276,6 @@ 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); arch::boot::after_console(&kernel_args, maps); 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/syscall/machine.rs b/kernel/src/syscall/machine.rs index a9dc5768089..3b3ecfb2598 100644 --- a/kernel/src/syscall/machine.rs +++ b/kernel/src/syscall/machine.rs @@ -86,15 +86,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(); diff --git a/src/metal.rs b/src/metal.rs index 54a0bc8bded..7e013e8b2c1 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,42 +785,28 @@ 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), + "dump-deaf-cpu", // It holds each pipe waiter up to a short budget of its own clock for a post // to land between its condition and its park. It reaches no device and // writes no firmware state, and a hold nothing posts into lapses. - ("watch-window", 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", - ), - ), + "watch-window", ]; /// The arm that stops the machine, named once: [`FLASHABLE`] rules on it and @@ -872,16 +849,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 @@ -908,21 +882,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 @@ -2635,7 +2598,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. @@ -2643,7 +2606,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)); @@ -2768,13 +2731,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}"); @@ -2791,7 +2751,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 @@ -2814,9 +2774,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:?}" ); From 13f71c52e68e476fcd7a120d53bb17a9e729506a Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 07:12:44 +0200 Subject: [PATCH 09/19] test-runner, metaltalk: what only the cut tests called goes - `log-gate`, `log-storm` and `kbd-close` were run only by the deleted `tests/common/logread.rs` and `tests/common/console.rs`; `git grep` over `tests`, `src` and every `*.toml` found no other runner. `log_gate.rs` and `kbd_close.rs` go, and `BUILTINS` keeps `log-close`, which a metal row runs. `log_close.rs`'s doc no longer borrows `log-gate`'s reason. They were `SYS_DEBUG`'s `LOG_PATTERNED`'s one caller; the action is recorded in `issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md`. - `Stream::wait_ended` was called only by `metaltalk`'s own unit tests, so it moves into them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- src/metaltalk.rs | 36 +- userland/test-runner/src/kbd_close.rs | 172 -------- userland/test-runner/src/log_close.rs | 6 +- userland/test-runner/src/log_gate.rs | 584 -------------------------- userland/test-runner/src/main.rs | 14 +- 5 files changed, 23 insertions(+), 789 deletions(-) delete mode 100644 userland/test-runner/src/kbd_close.rs delete mode 100644 userland/test-runner/src/log_gate.rs 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/userland/test-runner/src/kbd_close.rs b/userland/test-runner/src/kbd_close.rs deleted file mode 100644 index 71b3e7e09d0..00000000000 --- a/userland/test-runner/src/kbd_close.rs +++ /dev/null @@ -1,172 +0,0 @@ -//! A pending poll on stdin outlives the keyboard *claim* being closed. -//! -//! **The defect this is aimed at was cancellation by an object that did not own -//! the source.** `io_uring::cancel_by_source` cancels by source across every ring in -//! the machine — right for a pipe, whose other end really has gone — and -//! `object::ops::close` decided whether to call it by asking the *object*: -//! `Device(_)` answered "this ends its sources", on the argument that a claim -//! admits exactly one handle so every ring watching it is the one holder's. -//! That is true of the claim and false of the source. `Source::Keyboard` is -//! named by the `Device(Keyboard)` claim *and* by every `Console` -//! (`object::ops::read_source`), so the claim's holder closing its handle posted -//! `-NotFound` into every pending `POLL_ADD` on stdin in the machine — libc's -//! terminal read is what arms them — for processes that hold no device and were -//! never consulted. -//! -//! It runs inside `test-runner` because a spawned binary's stdin is a pipe: this -//! process's handle 0 is the `Console`, and the claim it closes is minted from the capability the estate holds. Both objects are in -//! one process, which is the smallest machine the collision exists on; the real -//! failure needs two, and neither has to know about the other. -//! -//! Three arms, and the middle one is why it is not enough to assert that the -//! poll survived: -//! -//! 1. the keyboard claim is taken and released, and the stdin poll must still be -//! pending — the arm that reds on the defect; -//! 2. the **mouse** claim is polled and released, and *that* poll must be -//! cancelled — cancellation is exactly what a close there owes. This is the direction a -//! fix overshoots into: a tree that stopped cancelling on close would pass -//! arm 1 and red here; -//! 3. an injected keystroke completes the stdin poll — so what survived arm 1 -//! was a live registration and not an absent one. - -use toyos::poller::{Poller, READABLE}; -use toyos::syscap::SysCap; -use toyos::{Keyboard, Mouse}; -use toyos_abi::syscall::DeviceType; -use toyos_abi::RawHandle; - -/// This process's console. -const STDIN: RawHandle = RawHandle(0); - -const STDIN_TOKEN: u64 = 1; -const MOUSE_TOKEN: u64 = 2; - -/// What the host waits for before it injects. Printed only once both claim arms -/// have run, so a key can never arrive early enough to complete the poll the -/// first arm is asserting is still pending. -const READY: &str = "===KBD_CLOSE_READY==="; - -/// How long arm 3 gives an injected keystroke. -/// -/// A liveness bound and not a verdict: what has to happen is one key transition -/// reaching `keyboard::handle_key`, which posts to this ring's watcher list -/// before the interrupt returns. Seconds is four orders of magnitude above it, -/// and the host injects only after [`READY`]. -const KEY_WAIT_NANOS: u64 = 5_000_000_000; - -/// Completions seen so far, by token. Accumulated across drains because a drain -/// consumes the ring: asking twice about the same completion is asking about -/// nothing. -#[derive(Default)] -struct Seen { - stdin: usize, - mouse: usize, -} - -pub fn run(cap: Option<&SysCap>) -> i32 { - let Some(cap) = cap else { - println!("kbd-close: this program holds no system capability, so it can mint no claim"); - return 1; - }; - match probe(cap) { - Ok(()) => { - println!("kbd-close: OK"); - 0 - } - Err(e) => { - println!("kbd-close: FAILED: {e}"); - 1 - } - } -} - -fn probe(cap: &SysCap) -> Result<(), String> { - let poller = Poller::new(2); - let mut seen = Seen::default(); - - // **Submitted before anything is closed, and that is the whole of what this - // has to get right.** `watch_raw` only queues a submission entry; `wait` - // is what enters the kernel. A probe that closed first would stage nothing - // — the ring is not a watcher of the keyboard yet, so there is nothing for - // a cancellation to reach, and it would pass on a tree with the defect. - poller.watch_raw(STDIN, READABLE, STDIN_TOKEN); - drain(&poller, &mut seen); - if poller.pending() != 0 { - return Err(format!("{} submission(s) never reached the kernel", poller.pending())); - } - if seen.stdin != 0 { - return Err( - "the console was already readable, so nothing here would have been pending; this \ - guest was given input before the gate ran" - .to_string(), - ); - } - - // Arm 2 first, so a tree that has stopped cancelling anything is caught - // before arm 1 congratulates it. The mouse is the device every machine - // shape has. - let mouse: Mouse = cap - .claim(DeviceType::Mouse) - .map_err(|e| format!("the mouse must be claimable and answered {e:?}"))?; - poller.watch(&mouse, READABLE, MOUSE_TOKEN); - drain(&poller, &mut seen); - if seen.mouse != 0 { - return Err("the mouse reported input; this gate needs an idle pointer".to_string()); - } - drop(mouse); - // Synchronous: `ops::close` posts the cancellation before the `close` - // syscall returns, on this thread. - drain(&poller, &mut seen); - if seen.mouse != 1 { - return Err(format!( - "releasing the mouse claim left its own poll pending ({} completions) — a source \ - that no other kind of object names must be cancelled by its holder's close", - seen.mouse, - )); - } - if seen.stdin != 0 { - return Err("closing the mouse claim completed the poll on stdin".to_string()); - } - - // Arm 1. Nothing about the machine's keyboard changes here: the class is - // claimed and released, and the console still names the same source. - let keyboard: Keyboard = cap - .claim(DeviceType::Keyboard) - .map_err(|e| format!("the keyboard must be claimable and answered {e:?}"))?; - drop(keyboard); - drain(&poller, &mut seen); - if seen.stdin != 0 { - return Err( - "releasing the keyboard claim cancelled a poll on stdin — a process that holds no \ - device had its terminal read completed from under it" - .to_string(), - ); - } - - // Arm 3. - println!("{READY}"); - poller.wait(1, KEY_WAIT_NANOS, |token| count(&mut seen, token)); - if seen.stdin == 0 { - return Err( - "the poll outlived the close and then never completed on a keystroke either, so \ - what it outlived may have been its own arming" - .to_string(), - ); - } - println!("kbd-close: survived=1 mouse_cancelled={} stdin_woken={}", seen.mouse, seen.stdin); - Ok(()) -} - -/// Take whatever is in the completion ring right now, without waiting. -fn drain(poller: &Poller, seen: &mut Seen) { - poller.wait(1, 0, |token| count(seen, token)); -} - -fn count(seen: &mut Seen, token: u64) { - match token { - STDIN_TOKEN => seen.stdin += 1, - MOUSE_TOKEN => seen.mouse += 1, - other => panic!("kbd-close: a completion for a token nothing submitted: {other}"), - } -} diff --git a/userland/test-runner/src/log_close.rs b/userland/test-runner/src/log_close.rs index 3bebf8ce14b..246a5e6842c 100644 --- a/userland/test-runner/src/log_close.rs +++ b/userland/test-runner/src/log_close.rs @@ -17,9 +17,9 @@ //! `cancel_by_source` acted on. What it proves is that the *handle* is not what the //! source's lifetime is tied to. //! -//! It runs inside `test-runner` for `log-gate`'s reason — a `SysCap` dup is not -//! a namespace entry, so a spawned binary has none — and it needs `dup` in its -//! manifest row on top of `logread`, which `tests/testcases/system.toml` has. +//! It runs inside `test-runner` because a `SysCap` dup is not a namespace entry, +//! so a spawned binary has none, and it needs `dup` in its manifest row on top +//! of `logread`, which `tests/testcases/system.toml` has. use std::process::Command; diff --git a/userland/test-runner/src/log_gate.rs b/userland/test-runner/src/log_gate.rs deleted file mode 100644 index b8723d90d1e..00000000000 --- a/userland/test-runner/src/log_gate.rs +++ /dev/null @@ -1,584 +0,0 @@ -//! The conservation law, read through `SYS_LOG_READ` from inside `test-runner`. -//! -//! **It runs here rather than in a binary of its own, and that is capability -//! doctrine rather than convenience.** `test-runner` passes its whole -//! *namespace* to every binary it spawns, and `logread` is not a namespace -//! entry — it is a `SysCap` dup, exactly like `realtime`, which the estate does -//! not hand down either. So the gate that reads the machine's log is the one -//! process in a test image that holds the right from its own manifest row. -//! -//! **The verdict is exact, not statistical.** Every sequence number a shard -//! ever issued is either a record this reader took or one the kernel counted as -//! lost; no number is taken twice; and every storm record's text regenerates -//! byte for byte from the two numbers it declares. A torn record fails the -//! text, a lost record that is not counted fails the ledger, and a duplicated -//! one fails it the other way. -//! -//! **The storm is a thread of this process**, calling `SYS_DEBUG`'s -//! `LOG_PATTERNED` once per record and counting each call after it returns. -//! **Every interleave the verdict rests on is an event, not a schedule**: the -//! producer stops after [`HANDOVER`] records until this reader has taken one; -//! this reader then reads nothing until the producer has emitted enough more to -//! lap its cursor on some shard, so `lost` is never zero; and the producer then -//! emits until a read has taken storm records while its counter moved. -//! -//! **Nothing this reader waits for is a record the ring may drop.** The -//! termination condition is the *cursor*: the log has been drained and nothing -//! new has arrived for [`QUIET_READS`] reads, once the producer has returned -//! from its last call. The nesting burst's own `done` is a cross-check where it -//! survived and is never waited on. **The rule this shape exists to keep is -//! general**: a workload whose liveness depends on a record the ring is allowed -//! to drop is the same mistake wherever it appears. - -use std::collections::BTreeMap; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::mpsc::{self, Receiver, SyncSender}; -use std::sync::Arc; -use std::thread::JoinHandle; - -use toyos::log::{LogTail, Record, MAX_LOG_SHARDS}; -use toyos::poller::{Poller, READABLE}; -use toyos::syscap::SysCap; -use toyos_abi::syscall::debug_action::LOG_PATTERNED; - -/// The first sequence number any shard issues — one, so a slot nothing has ever -/// written cannot read as record 0 of every shard on every boot. -/// `kernel/src/log/shard.rs`'s `FIRST_SEQ` is the other half of this constant. -const FIRST_SEQ: u64 = 1; - -/// Records per `SYS_LOG_READ`. Above the shard count, which the call refuses -/// below. -const BATCH: usize = 64; - -/// Empty reads in a row before the log is called quiet. -/// -/// **Eight, each after a bounded park on the readiness source**, because a -/// single empty read can land while a producer is inside its publication -/// bracket: `drain_ordered` stops that shard and says nothing about it, so a -/// ledger closed on the first empty read can be short by what was in flight. -const QUIET_READS: u32 = 8; - -/// How long a park on the log's readiness source waits before giving up on it. -/// -/// It is the gate's pacing as much as its wait: with nothing left to say the -/// kernel posts nothing, and eight of these is the whole tail of the run. -const IDLE_NANOS: u64 = 2_000_000; - -/// How long the deterministic readiness round waits for its own record. -/// -/// Generous, because what it bounds is a scheduler getting round to a child's -/// exit — not the post, which is one function call after the drain. A gate -/// that timed out here would be reporting the host's load and not the kernel's. -const READINESS_WAIT_NANOS: u64 = 2_000_000_000; - -/// The poll's token. One handle is watched, so it identifies the round rather -/// than the source. -const LOG_TOKEN: u64 = 1; - -/// `kernel/src/log/storm.rs`'s `PAYLOAD`. -const PAYLOAD: usize = 96; - -/// The producer id `LOG_PATTERNED`'s records declare. -const STORM_PRODUCER: u64 = 0; - -/// Storm records emitted before the producer waits for this reader to take -/// one: far under a shard's 512, so the ring still holds them when it arrives. -const HANDOVER: u64 = 64; - -/// `kernel/src/log/shard.rs`'s `SHARD_RECORDS`: one more than `shards` times -/// this, emitted between two reads, puts more than a shard's worth into one -/// shard, whichever CPUs the producer ran on. -const SHARD_RECORDS: u64 = 512; - -/// `kernel/src/log/nested.rs`'s `NEST_PRODUCER`: the burst an interrupt handler -/// emits declares itself as this, so it goes through the same per-producer -/// ledger and the same byte-for-byte regeneration as a storm's records. -const NEST_PRODUCER: u64 = u64::MAX; - -/// One producer's ledger. -#[derive(Default)] -struct Producer { - /// The next index expected from this producer, and `None` before its first - /// record. - next: Option, - read: u64, -} - -/// One shard's ledger: the sequence numbers the kernel issued on that CPU. -#[derive(Default, Clone, Copy)] -struct ShardLedger { - first: Option, - next: u64, - read: u64, - /// Sequence numbers this reader never saw, derived from the gaps between - /// the ones it did. - gaps: u64, - last_at_ns: u64, -} - -/// The gate over whatever the boot's actuators write. -pub fn run(cap: Option<&SysCap>) -> i32 { - report(cap, false) -} - -/// The gate with a storm beside it. -pub fn run_storm(cap: Option<&SysCap>) -> i32 { - report(cap, true) -} - -fn report(cap: Option<&SysCap>, storm: bool) -> i32 { - let Some(cap) = cap else { - println!("log-gate: this program holds no system capability, so it holds no `logread`"); - return 1; - }; - match gate(cap, storm) { - Ok(()) => 0, - Err(e) => { - println!("log-gate: FAILED: {e}"); - 1 - } - } -} - -struct Run { - shards: [ShardLedger; MAX_LOG_SHARDS], - producers: BTreeMap, - /// The nesting gate's declared burst, once its `done` has been read. A - /// cross-check and never a requirement: the burst laps its shard, so the - /// ring is allowed to drop it. - nest: Option, - records: u64, - reads: u64, - /// Storm records taken, after the lap, by a read across which the - /// producer's counter moved. - concurrent: u64, - /// Times the log's readiness source completed a poll. - completions: u64, -} - -fn gate(cap: &SysCap, storm: bool) -> Result<(), String> { - let mut tail = LogTail::new(); - let mut buf = [Record::EMPTY; BATCH]; - let mut run = Run { - shards: [ShardLedger::default(); MAX_LOG_SHARDS], - producers: BTreeMap::new(), - nest: None, - records: 0, - reads: 0, - concurrent: 0, - completions: 0, - }; - - // **Armed before the storm starts and kept armed**, which is what makes a - // completion deterministic rather than lucky: the storm's records are - // committed after this poll was registered, and re-arming after every - // harvest means a post landing *during* the storm finds a pending poll - // rather than a gap. - // - // **It used to arm only on an empty read, and that made the assertion - // depend on the shape of the boot.** During a storm no read is empty, so the - // only poll in flight was the one from before the first read; whether it was - // ever completed came down to when `klogd` happened to get a turn. At - // `--smp 4` that measured `wakes=1`, and at `--smp 8` with `/system/bin/logd` also - // reading the cursor it measured **zero** — a red about scheduling rather - // than about the readiness source. `min_complete` 0 with no timeout submits - // and harvests without blocking, so this costs one syscall a round. - let poller = Poller::new(1); - poller.watch(cap, READABLE, LOG_TOKEN); - let mut armed = true; - - let produced = Arc::new(AtomicU64::new(0)); - let stop = Arc::new(AtomicBool::new(false)); - let (handover, taken) = mpsc::sync_channel(1); - let (lap, lapped) = mpsc::sync_channel(1); - let mut handover = storm.then_some(handover); - let mut producer = storm - .then(|| spawn_producer(Arc::clone(&produced), Arc::clone(&stop), taken, lap)); - let mut after_lap = false; - - let mut quiet = 0u32; - loop { - if !armed { - poller.watch(cap, READABLE, LOG_TOKEN); - armed = true; - } - poller.wait(0, 0, |token| { - assert_eq!(token, LOG_TOKEN, "the log poll completed with another token"); - run.completions += 1; - armed = false; - }); - - let before = produced.load(Ordering::Acquire); - let batch = tail - .read(cap, &mut buf) - .map_err(|e| format!("SYS_LOG_READ refused a {BATCH}-record buffer: {e:?}"))?; - let moved = produced.load(Ordering::Acquire) != before; - run.reads += 1; - if batch.is_empty() { - quiet += 1; - } else { - quiet = 0; - run.records += batch.len() as u64; - } - - let storm_before = storm_read(&run); - for record in batch { - account(record, &mut run)?; - } - let took = storm_read(&run) - storm_before; - if after_lap && moved && took > 0 { - run.concurrent += took; - stop.store(true, Ordering::Release); - } - if let Some(handover) = handover.take_if(|_| storm_read(&run) > 0) { - let records = u64::from(tail.shards()) * SHARD_RECORDS + 1; - handover.send(records).map_err(|_| ended(producer.take()))?; - lapped.recv().map_err(|_| ended(producer.take()))?; - after_lap = true; - } - - if producer.as_ref().is_some_and(JoinHandle::is_finished) { - join(producer.take())?; - } - if quiet >= QUIET_READS && producer.is_none() { - break; - } - if batch.is_empty() { - // **Nothing new, so park on the readiness source rather than spin.** - // `SYS_LOG_READ` never blocks by design; this is the other half of - // that design, and the timeout is what bounds a machine that has - // nothing left to say. The poll is already armed by the top of the - // loop, so this parks on it rather than adding a second. - poller.wait(1, IDLE_NANOS, |token| { - assert_eq!(token, LOG_TOKEN, "the log poll completed with another token"); - run.completions += 1; - armed = false; - }); - } - } - let emitted = produced.load(Ordering::Acquire); - - // **The readiness source, observed deterministically rather than raced.** - // If the reads above completed no poll, make one: a child that runs and - // exits commits `process.rs`'s `exit:` line, which is one kernel record - // from userland with no actuator and no privilege behind it. - if run.completions == 0 { - let mut child = std::process::Command::new("/system/bin/echo") - .arg("log-gate") - .spawn() - .map_err(|e| format!("the record-making child would not start: {e}"))?; - let _ = child.wait(); - if !armed { - poller.watch(cap, READABLE, LOG_TOKEN); - } - poller.wait(1, READINESS_WAIT_NANOS, |token| { - assert_eq!(token, LOG_TOKEN, "the log poll completed with another token"); - run.completions += 1; - }); - } - - verdict(&tail, &run, storm, emitted) -} - -/// The storm: one kernel record per call, counted after each call returns. -/// [`HANDOVER`] records, then the lap the reader names once it has taken one, -/// then records until the reader sets `stop`. -fn spawn_producer( - produced: Arc, - stop: Arc, - taken: Receiver, - lapped: SyncSender<()>, -) -> JoinHandle> { - std::thread::spawn(move || { - let emit = || { - let index = produced.load(Ordering::Relaxed); - let answer = toyos_abi::syscall::debug_with(LOG_PATTERNED, index); - if answer != 0 { - return Err(format!( - "SYS_DEBUG LOG_PATTERNED answered {answer:#x} at index {index}" - )); - } - produced.store(index + 1, Ordering::Release); - Ok(()) - }; - for _ in 0..HANDOVER { - emit()?; - } - let lap = taken.recv().map_err(|_| "the reader ended before it took a storm record")?; - for _ in 0..lap { - emit()?; - } - lapped.send(()).map_err(|_| "the reader ended before the storm lapped it")?; - while !stop.load(Ordering::Acquire) { - emit()?; - } - Ok(()) - }) -} - -/// Why the producer's end of a channel closed: it returned, and its join says why. -fn ended(producer: Option>>) -> String { - match join(producer) { - Err(e) => e, - Ok(()) => "the producer returned before the storm lapped this reader".into(), - } -} - -fn join(producer: Option>>) -> Result<(), String> { - match producer.map(JoinHandle::join) { - None | Some(Ok(Ok(()))) => Ok(()), - Some(Ok(Err(e))) => Err(e), - Some(Err(_)) => Err("the producer thread panicked".into()), - } -} - -fn storm_read(run: &Run) -> u64 { - run.producers.get(&STORM_PRODUCER).map_or(0, |p| p.read) -} - -/// Put one record through both ledgers. -fn account(record: &Record, run: &mut Run) -> Result<(), String> { - let cpu = record.cpu as usize; - let ledger = run.shards.get_mut(cpu).ok_or_else(|| { - format!("a record claims cpu{cpu}, past the ABI's {MAX_LOG_SHARDS} shards") - })?; - - match ledger.first { - None => ledger.first = Some(record.seq), - Some(_) => { - if record.seq < ledger.next { - return Err(format!( - "cpu{cpu} answered seq {} after seq {}: a sequence number was read twice, \ - or out of order, within one shard", - record.seq, - ledger.next - 1 - )); - } - ledger.gaps += record.seq - ledger.next; - } - } - if record.at_ns < ledger.last_at_ns { - return Err(format!( - "cpu{cpu} seq {} is stamped {} ns, behind the {} ns of the record before it — within \ - a shard the sequence order is the timestamp order, and `emit` stamps inside the \ - same bracket it reserves in", - record.seq, record.at_ns, ledger.last_at_ns - )); - } - ledger.last_at_ns = record.at_ns; - ledger.next = record.seq + 1; - ledger.read += 1; - - let message = record.message(); - if message.len() != record.len as usize { - return Err(format!( - "cpu{cpu} seq {} declares {} message bytes and decodes to {}", - record.seq, - record.len, - message.len() - )); - } - - if let Some(rest) = message.strip_prefix("lognest done ") { - let emitted = rest - .split_whitespace() - .find_map(|w| w.strip_prefix("emitted=")) - .and_then(|v| v.parse::().ok()) - .ok_or_else(|| format!("`lognest done` is unreadable: {rest}"))?; - if run.nest.replace(emitted).is_some() { - return Err("the nesting gate said `done` twice".into()); - } - return Ok(()); - } - if message.starts_with("lognest ") { - // `start` and `outer`. Both are records like any other and the burst - // laps the shard they are in, so both are *expected* to be dropped — - // which is the ring's declared policy and not a loss of evidence. - return Ok(()); - } - let Some(rest) = message.strip_prefix("logstorm t=") else { - // An ordinary kernel record. It is in the shard ledger above, which is - // where the conservation law is computed; it declares nothing this gate - // could regenerate. - return Ok(()); - }; - - let (thread, index) = parse_record(rest)?; - if thread != STORM_PRODUCER && thread != NEST_PRODUCER { - return Err(format!( - "cpu{cpu} seq {} names producer t={thread}, which no gate runs", - record.seq - )); - } - let expected = storm_message(thread, index); - if message != expected { - return Err(format!( - "cpu{cpu} seq {} is a torn or mixed storm record\n read: {message}\n expected: {expected}", - record.seq - )); - } - let producer = run.producers.entry(thread).or_default(); - if let Some(next) = producer.next { - if index < next { - return Err(format!( - "producer t={thread} answered index {index} after {}: one record's body was \ - published under another record's sequence number", - next - 1 - )); - } - } - producer.next = Some(index + 1); - producer.read += 1; - Ok(()) -} - -/// The line a storm record carries, from the two numbers that identify it. -/// -/// **The kernel builds this and the reader rebuilds it**, so a body half -/// overwritten by another generation fails on the byte that differs rather than -/// on a checksum that might not have covered it. `kernel/src/log/storm.rs` is -/// the other half; a disagreement between the two formulas reds loudly rather -/// than passing quietly. -fn storm_message(thread: u64, index: u64) -> String { - let checksum = (thread.wrapping_mul(0x9E37_79B9_7F4A_7C15) - ^ index.wrapping_mul(0xC2B2_AE3D_27D4_EB4F)) - .rotate_left(17); - let payload: String = (0..PAYLOAD) - .map(|offset| (b'a' + (checksum.wrapping_add(offset as u64) % 26) as u8) as char) - .collect(); - format!("logstorm t={thread} i={index} k={checksum:016x} {payload}") -} - -fn parse_record(rest: &str) -> Result<(u64, u64), String> { - let mut words = rest.split_whitespace(); - let thread = words - .next() - .and_then(|w| w.parse::().ok()) - .ok_or_else(|| format!("a storm record names no thread: {rest}"))?; - let index = words - .next() - .and_then(|w| w.strip_prefix("i=")) - .and_then(|w| w.parse::().ok()) - .ok_or_else(|| format!("a storm record names no index: {rest}"))?; - Ok((thread, index)) -} - -/// The conservation law, and everything the gate prints for a reader of its -/// output. -fn verdict(tail: &LogTail, run: &Run, storm: bool, emitted: u64) -> Result<(), String> { - let seen: Vec = - (0..MAX_LOG_SHARDS).filter(|&i| run.shards[i].first.is_some()).collect(); - if seen.is_empty() { - return Err("no shard answered a single record".into()); - } - if tail.shards() as usize != seen.len() { - return Err(format!( - "the kernel says this machine has {} shard(s) and {} answered a record", - tail.shards(), - seen.len() - )); - } - - // **`records_emitted == records_read + lost`, with the sequence numbers as - // the ledger.** Every number a shard issued is either a record this reader - // took or one it never saw, and the second is what the kernel derives - // `lost` from — out of `head` and `next`, two numbers that have to be right - // anyway, rather than out of a producer-side counter that could drift from - // the ring. - let mut computed = 0u64; - for &i in &seen { - let first = run.shards[i].first.expect("`seen` is the shards with a first record"); - computed += first - FIRST_SEQ + run.shards[i].gaps; - } - let reported = tail.lost(); - if computed != reported { - let per_shard: Vec = seen - .iter() - .map(|&i| { - format!( - "cpu{i}: first={} last={} read={} gaps={}", - run.shards[i].first.unwrap_or(0), - run.shards[i].next.saturating_sub(1), - run.shards[i].read, - run.shards[i].gaps - ) - }) - .collect(); - return Err(format!( - "conservation failed: the sequence numbers say {computed} record(s) were never read \ - and the kernel counted {reported}\n {}", - per_shard.join("\n ") - )); - } - - let read_total = storm_read(run); - if storm { - if read_total == 0 { - return Err("the storm ran and this reader read none of it".into()); - } - let next = run.producers.get(&STORM_PRODUCER).and_then(|p| p.next).unwrap_or(0); - if next > emitted { - return Err(format!( - "the storm answered index {} of {emitted} emitted", - next - 1 - )); - } - // The readiness source, asserted where it is reachable: the poll was - // armed before the storm started, so the records that answer it were - // committed after it was registered. - if run.completions == 0 { - return Err( - "the log's readiness source completed no poll — not across the storm, and not on \ - the record a child's exit commits afterwards either" - .into(), - ); - } - } - - if let Some(burst) = run.producers.get(&NEST_PRODUCER) { - // The burst's own `done` is a cross-check where it survived, and the - // ledger's own floor where it did not. The burst laps its shard by - // construction, so a reader that required that record would be - // requiring one the design says may go. - let declared = match (run.nest, burst.next) { - (Some(declared), _) => declared, - (None, Some(next)) => next, - (None, None) => { - return Err("the nesting burst was seen and named no index".into()) - } - }; - if burst.read == 0 { - return Err("the nesting burst was injected and none of it was read".into()); - } - if burst.next.is_some_and(|next| next > declared) { - return Err(format!( - "the nesting burst answered index {} of a declared {declared}", - burst.next.unwrap_or(0) - 1 - )); - } - println!( - "log-gate: nest declared={declared} read={} dropped={}", - burst.read, - declared - burst.read, - ); - } - - println!( - "log-gate: {} record(s) over {} read(s) from {} shard(s); lost={reported}, and the \ - sequence numbers say the same", - run.records, - run.reads, - seen.len() - ); - if storm { - println!( - "log-gate: storm emitted={emitted} read={read_total} dropped={} \ - concurrent={} wakes={}", - emitted - read_total, - run.concurrent, - run.completions, - ); - } - println!("log-gate: OK"); - Ok(()) -} diff --git a/userland/test-runner/src/main.rs b/userland/test-runner/src/main.rs index 69c328db6cb..52a40b2bf76 100644 --- a/userland/test-runner/src/main.rs +++ b/userland/test-runner/src/main.rs @@ -1,6 +1,4 @@ -mod kbd_close; mod log_close; -mod log_gate; use std::io::{self, BufRead, Write}; use std::os::toyos::process::{ChildExt, CommandExt}; @@ -22,17 +20,7 @@ use toyos::syscap::SysCap; /// capability has nowhere else to run. They answer the same /// `===TEST_START===`/`===TEST_END===` protocol as a binary, so the host cannot /// tell the difference and does not have to. -/// -/// `kbd-close` is here for a second reason as well as that one: its subject is a -/// pending poll on **this process's own stdin**, which is a `Console`. A spawned -/// binary's stdin is a pipe (see the `Stdio::piped()` below), so the object the -/// collision is about does not exist in one. -const BUILTINS: &[(&str, fn(Option<&SysCap>) -> i32)] = &[ - ("log-gate", log_gate::run), - ("log-storm", log_gate::run_storm), - ("log-close", log_close::run), - ("kbd-close", kbd_close::run), -]; +const BUILTINS: &[(&str, fn(Option<&SysCap>) -> i32)] = &[("log-close", log_close::run)]; /// The job the runner is inside, and whether the list got through: written by /// the loop, read by the deadline watching it. From c3c8aa078682b869a68896fcfbc6ee6503131f33 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 07:12:57 +0200 Subject: [PATCH 10/19] ci, harness: one nightly guest job runs the whole suite, with no shards and no durations The nightly dealt the suite's six x86-64 tasks over twelve shards, and `Shard::keep` gives each task its own bin, so shards 7 to 12 owned nothing and `validate_ordinary_shard` exited 1 on each; `tcg` filtered on `process_stats`, which the guest suite no longer selects, and exited 1 on an empty selection. Both were red every night by construction, and the sixteen AArch64 rows ran on no automated lane, because a shard booted only `GUEST_ARCH`. - `cargo run -- --ci guest` takes no argument and runs the whole suite, `--jobs 1`; `tcg` is gone as a job, and the nightly's `tcg` lane runs the same command in its container without `/dev/kvm`. The matrix goes. - Both containers install `qemu-system-arm` and `qemu-efi-aarch64`, and the guest job checks the instrument of every architecture: the AArch64 QEMU is held to `.github/qemu-version` like the x86-64 one, and its firmware found the same way. A guest of another architecture than the host's is emulated by construction, so only the host's own is refused for a `/dev/kvm` that does not open. `qemu-efi-aarch64` is declared beside `ovmf-generic` in `issues/build/the-build-runs-host-tools-outside-rust-and-qemu.md`. - The shard and duration machinery goes: `Shard`, `parse_shard`, `validate_ordinary_shard`, `--shard`, `check_shard_partition`, `shard_pricing`, `load_durations`, `save_durations`, `longest_first`, `tests/test-durations`, and with the arch filter `GUEST_ARCH` and `arch_drop_line`. `--shard` is now refused as a deleted flag. The parallel and serial phases stay while `usb_boot_stick_pulled` is serial. - `--metal --list` printed the registered names since round 1, which moved `--list` ahead of `--metal`; it prints the metal plan again, as on main. - `tests/CLAUDE.md` no longer names shards or `tests/test-durations`. `cargo test --test toyos-build -- --shard 12/12` now refuses the flag before anything boots. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- .github/workflows/nightly.yml | 19 +- ...d-runs-host-tools-outside-rust-and-qemu.md | 4 +- src/ci.rs | 62 ++-- src/testargs.rs | 286 +----------------- tests/CLAUDE.md | 4 +- tests/checks.rs | 56 +--- tests/test-durations | 5 - tests/toyos.rs | 285 +---------------- 8 files changed, 70 insertions(+), 651 deletions(-) delete mode 100644 tests/test-durations diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 49193d4480c..0a44866b6a5 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -80,10 +80,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. @@ -106,7 +102,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 @@ -136,7 +133,7 @@ jobs: 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 +142,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 +162,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/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 eb48e5c7c0f..272235ff02a 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 @@ -21,8 +21,8 @@ arrives and is not one. M4 and M5 are stages of `issues/build/toyos-builds-itsel | `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); `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 | +| 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/src/ci.rs b/src/ci.rs index 08e533522d4..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", "process_stats"])), + 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/testargs.rs b/src/testargs.rs index ead181fcbd1..53efd14ddee 100644 --- a/src/testargs.rs +++ b/src/testargs.rs @@ -7,143 +7,6 @@ use crate::flags::declare_flags; use std::path::PathBuf; -use std::time::Duration; - -/// 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 { - /// The empty accumulator [`keep`](Self::keep) fills, one bin per shard. - /// - /// The only way to make one, so a caller cannot hand `keep` a vector of the - /// wrong width; what it *can* still do is make a second one, which is the - /// defect the doc on `keep` names. - pub fn bins(self) -> Vec { - vec![Duration::ZERO; self.count] - } - - /// Drop everything another shard owns, keeping the order of what is left. - /// - /// Longest-processing-time on the measured duration profile the suite - /// already orders its queue by, because a shard's wall clock is its bin's - /// total and the run's is the fullest bin. `items` is read in the order - /// given, so a list already sorted descending gets LPT's bound and one that - /// is not still gets a complete, deterministic partition — **every item - /// lands in exactly one shard whatever the profile says**, which is the - /// property a verdict depends on and the one the gates below hold. - /// - /// **`load` is the run's one accumulator, not this call's.** A suite that - /// partitions several pools — the parallel tasks and the serial tail — is one - /// machine's wall clock either way, so the second pool has - /// to fill the bins the first left light. Starting each call from - /// [`bins`](Self::bins) makes each partition good and their sum bad, and - /// the imbalances add: measured over run `31377439504`'s twelve shards it - /// was a widest shard of 466.1 s against an even split of 369.1 s, where - /// one accumulator over the same items put the widest bin at 363.9 s. - /// Thread one through the calls, heaviest pool first. - /// - /// Every process partitioning one run must therefore make the same calls in - /// the same order over the same items: the bins each call leaves are the - /// next call's input, so a shard that skipped a pool would price every later - /// one differently and the twelve would stop being a partition. - /// - /// `None` is an item the profile has never seen, and it is priced at the - /// longest that was measured *in its own pool* — the same conservatism - /// `longest_first` expresses by sorting unknowns first, in a form that can - /// be added up. Where *nothing* was measured, every item prices the same and - /// LPT degenerates to round-robin, which is the best a machine with no - /// profile can do and is what every runner's first run gets. - pub fn keep( - self, - items: &mut Vec, - load: &mut [Duration], - cost: impl Fn(&T) -> Option, - ) { - assert_eq!( - load.len(), - self.count, - "a {}-way shard reads {} bins, and a partition over the wrong number of them \ - would not be one", - self.count, - load.len() - ); - let unmeasured = items - .iter() - .filter_map(&cost) - .max() - .unwrap_or(Duration::from_secs(1)); - let mut owner = Vec::with_capacity(items.len()); - for item in items.iter() { - let bin = (0..self.count).min_by_key(|&b| load[b]).expect("count >= 1"); - load[bin] += cost(item).unwrap_or(unmeasured); - owner.push(bin); - } - let mut i = 0; - items.retain(|_| { - let mine = owner[i] == self.index - 1; - i += 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; @@ -151,7 +14,6 @@ declare_flags!(pub SUITE = { 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; @@ -216,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 \ @@ -296,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}"); @@ -329,143 +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 items: Vec = (0..97).map(|i| (i * 37) % 23).collect(); - for count in 1..=8 { - let mut seen: Vec = Vec::new(); - for index in 1..=count { - let shard = Shard { index, count }; - let mut mine = items.clone(); - shard.keep(&mut mine, &mut shard.bins(), |&c| Some(Duration::from_secs(c))); - seen.extend(mine); - } - seen.sort_unstable(); - let mut want = items.clone(); - want.sort_unstable(); - assert_eq!(seen, want, "count {count}"); - } - } - - /// A shard's wall clock is its bin's total, so the split has to be by cost - /// and not by position. Descending input is what the suite hands it. - #[test] - fn the_split_is_by_cost_and_not_by_position() { - let items: Vec = vec![100, 90, 80, 70, 60, 50, 40, 30]; - let totals: Vec = (1..=4) - .map(|index| { - let shard = Shard { index, count: 4 }; - let mut mine = items.clone(); - shard.keep(&mut mine, &mut shard.bins(), |&c| Some(Duration::from_secs(c))); - mine.iter().sum() - }) - .collect(); - assert_eq!(totals, vec![130, 130, 130, 130], "{totals:?}"); - } - - /// **One run is one accumulator.** The suite partitions two pools — the - /// parallel tasks and the serial tail — and a shard runs both, so the second - /// call has to fill the bins the first left light. Two - /// pools of `[3 s, 1 s]` across two shards is the smallest case that tells - /// the two apart: threaded, both shards take 4 s; from a fresh accumulator - /// each time, the heavy item lands on shard 1 twice and the widest bin is - /// 6 s against an even split of 4 s. - #[test] - fn a_second_pool_fills_the_bins_the_first_left_light() { - let cost = |&c: &u64| Some(Duration::from_secs(c)); - let (mut threaded, mut apart) = (Vec::new(), Vec::new()); - let (mut kept, mut kept_apart) = (Vec::new(), Vec::new()); - for index in 1..=2 { - let shard = Shard { index, count: 2 }; - - let (mut first, mut second) = (vec![3u64, 1], vec![3u64, 1]); - let mut load = shard.bins(); - shard.keep(&mut first, &mut load, cost); - shard.keep(&mut second, &mut load, cost); - threaded.push(first.iter().chain(&second).sum::()); - kept.extend(first.iter().chain(&second).copied()); - - // The defect, spelled out with the same function: a second - // accumulator knows nothing about what the first one placed. - let (mut first, mut second) = (vec![3u64, 1], vec![3u64, 1]); - shard.keep(&mut first, &mut shard.bins(), cost); - shard.keep(&mut second, &mut shard.bins(), cost); - apart.push(first.iter().chain(&second).sum::()); - kept_apart.extend(first.iter().chain(&second).copied()); - } - assert_eq!(apart, vec![6, 2], "the defect's own numbers: {apart:?}"); - assert_eq!(threaded, vec![4, 4], "one accumulator splits it evenly: {threaded:?}"); - assert!( - threaded.iter().max() < apart.iter().max(), - "widest bin threaded {threaded:?} against apart {apart:?}" - ); - - // And it is still a partition: threading changes which shard owns an - // item, never how many own it. - for mut got in [kept, kept_apart] { - got.sort_unstable(); - assert_eq!(got, vec![1, 1, 3, 3], "every item exactly once"); - } - } - - /// A test the profile has never seen costs `Duration::MAX` so that it sorts - /// first, and a machine with no recorded profile at all — every runner's - /// first run — has a whole suite of them. Plain addition panicked on the - /// second item, which is what the first sharded CI run found. - #[test] - fn a_suite_with_no_measured_profile_still_splits_evenly() { - let items: Vec = (0..10).collect(); - let mut seen: Vec = Vec::new(); - let mut sizes = Vec::new(); - for index in 1..=3 { - let shard = Shard { index, count: 3 }; - let mut mine = items.clone(); - shard.keep(&mut mine, &mut shard.bins(), |_| None); - sizes.push(mine.len()); - seen.extend(mine); - } - seen.sort_unstable(); - assert_eq!(seen, items); - assert_eq!(sizes, vec![4, 3, 3], "{sizes:?}"); - } - /// Every `None` here is a default the run then takes in silence: `--jobs` /// the built-in width. #[test] @@ -516,8 +241,6 @@ 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"], @@ -553,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}"); } @@ -569,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")); diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index b05f4e49d91..72cc9867d7f 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -10,8 +10,8 @@ The mechanics live where the work is: profiles and shapes in `tests/common/`, re - **`/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; `handle_kill_policy` is the pattern. - **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 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/`; `tests/test-durations` is hosted, and a duration measured on the T14 does not transfer. +- **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. diff --git a/tests/checks.rs b/tests/checks.rs index f7e58959f14..00706d81f9c 100644 --- a/tests/checks.rs +++ b/tests/checks.rs @@ -576,13 +576,12 @@ mod checks { Ok(()) } - /// A run takes every declared 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 enabled declared test its filter matches, of either + /// architecture. #[test] - fn a_run_selects_by_filter_and_shard() -> Result<(), String> { - let taken = |filter: Option<&str>, sharded: bool| -> BTreeSet { - let (machine, screen) = select(filter, sharded); + fn a_run_selects_by_filter() -> Result<(), String> { + let taken = |filter: Option<&str>| -> BTreeSet { + let (machine, screen) = select(filter); machine .iter() .map(|(n, _)| n.to_string()) @@ -592,52 +591,23 @@ mod checks { let names = |of: &[&str]| -> BTreeSet { of.iter().map(|n| n.to_string()).collect() }; let enabled = |n: &&str| redlist::disabled(redlist::DISABLED, n).is_none(); let every: BTreeSet = declared().filter(enabled).map(String::from).collect(); - let foreign: BTreeSet = SCREEN_TESTS - .iter() - .filter(|(_, _, profile)| profile.arch() != toyos_build::ci::GUEST_ARCH) - .map(|(n, _, _)| *n) - .filter(enabled) - .map(String::from) - .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 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("usb_boot_stick"), true, names(&["usb_boot_stick_pulled"])), + (None, every), + (Some("virt_el2"), names(&["virt_el2_drop"])), + (Some("el2_drop"), names(&["virt_el2_drop"])), + (Some("usb_boot_stick"), names(&["usb_boot_stick_pulled"])), + (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(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("usb_boot_stick")) { - return Err(format!("a filter matching no foreign row still had a shard say {line:?}")); - } Ok(()) } diff --git a/tests/test-durations b/tests/test-durations deleted file mode 100644 index 638eddd4bc5..00000000000 --- a/tests/test-durations +++ /dev/null @@ -1,5 +0,0 @@ -screen_console_shell 2604 -screen_diag_boot 7330 -screen_fatal_halt_composited 6908 -screen_panic_muted 4416 -usb_boot_stick_pulled 15650 diff --git a/tests/toyos.rs b/tests/toyos.rs index f5aaf8ac0af..f1bef25cad3 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -13,7 +13,7 @@ use common::qemu::{ }; use common::{audio, compile, devices, faults, lan, metal, power, screen, serial, usb}; use toyos_build::bootlog::{self}; -use toyos_build::testargs::{self, Shard, SUITE}; +use toyos_build::testargs::{self, SUITE}; use toyos_build::redlist; /// Whether a test may run while other guests are up. @@ -49,9 +49,6 @@ enum Sched { /// a measurement and not a division. /// /// **Twelve is the number for one suite on this host.** -/// An earlier table said eight; it was taken while `drain_serial` was still -/// width-scaled and -/// `metal_sim_pointer_churn`'s twenty-four paced drains *were* the phase. const DEFAULT_WIDTH: usize = 12; /// The shared-boot binaries that call `SYS_DEBUG`, and so cannot run on the @@ -2625,14 +2622,7 @@ fn ps2_bursts(line: &str) -> Vec { /// silently, one byte at a time; nothing on either side of the wire is told. A /// host that keeps typing while the guest is not draining therefore hands the /// shell a command with a hole in it, and every assertion below that point is -/// about a question the guest was never asked. Both recorded -/// `screen_console_panic` failures are exactly that and nothing else: the panel -/// carried `/home/root> test_rs_TESTpanic_child 3` on 2026-08-19 (a lost shift -/// break, so four letters came back capitalised, and a lost make) and -/// `/home/root> test_rspanic_child 3` on 2026-08-23 (sixteen bytes gone in one -/// run — one queue's worth, exactly), and in both the shell answered -/// `not found` and the test blamed the panic path for a report nothing had -/// asked for. +/// about a question the guest was never asked. /// /// So the line goes out in bursts no wider than that queue, and the next burst /// waits until the panel shows the shell echoed the last one. An echoed @@ -4266,108 +4256,6 @@ impl Task { } } -/// Where the last run in this worktree left what each test cost it. -/// -/// Under `target/`, so it is per-worktree: on a single dev host repeating runs -/// it is a *hint* about how to order a queue and never an input to a verdict, -/// where a wrong number costs some idle lane time and a missing one costs -/// nothing at all. **A sharded run does not read it** — [`shard_pricing`] -/// says why the same claim does not hold once `target/` is a cache twelve -/// separate processes restore. -fn durations_path() -> std::path::PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("target/test-durations") -} - -/// The profile a checkout that has never run the suite starts from. -/// -/// A machine with no measurement at all prices every test the same, and -/// [`Shard::keep`]'s LPT then degenerates to round-robin — which is what put 191 -/// of 268 tests on one CI shard and cut it off at its job timeout while another -/// finished in sixteen minutes. Every runner is that -/// machine on every push, because a fresh clone has no `target/`. -/// -/// Measured on a runner rather than here, deliberately: it is read by the -/// machines that have nothing else, and the dev host overrides it with its own -/// numbers the first time it runs the suite. Cross-arch TCG on an M4 Pro and -/// KVM on four Azure cores do not agree about which tests are long. -fn committed_durations_path() -> std::path::PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/test-durations") -} - -fn read_durations(path: &Path, out: &mut BTreeMap) { - let Ok(text) = fs::read_to_string(path) else { return }; - for line in text.lines() { - // `

(handle) -} diff --git a/bootloader/src/loaderlog.rs b/bootloader/src/loaderlog.rs index 5e7f1445d6e..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,7 +111,7 @@ pub fn with_volume( ) -> Result { let bs = system_table.boot_services(); let handle = volume_handle(bs, guid)?; - let mut fs = crate::exclusive::open::(bs, 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() @@ -137,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 crate::exclusive::open::(bs, 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})")), }; @@ -215,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 7633ed818e1..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,7 +42,7 @@ mod arch; mod attempt; mod blackbox; mod bootnext; -mod exclusive; +mod protocol; mod floor; mod gcd; mod loaderlog; @@ -176,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 = exclusive::open::(bs, handle).ok()?; + let image = protocol::exclusive::(bs, handle).ok()?; let device = image.device()?; - let path = exclusive::open::(bs, device).ok()?; + let path = protocol::exclusive::(bs, device).ok()?; let is_hard_drive = |node: &&DevicePathNode| { node.full_type() == (DeviceType::MEDIA, DeviceSubType::MEDIA_HARD_DRIVE) @@ -378,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(); @@ -554,7 +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 = exclusive::open::(bs, 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) diff --git a/bootloader/src/protocol.rs b/bootloader/src/protocol.rs new file mode 100644 index 00000000000..ad9625b6bfe --- /dev/null +++ b/bootloader/src/protocol.rs @@ -0,0 +1,41 @@ +//! Every protocol the loader opens, opened here: `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 359b6a19584..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,10 +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 = crate::exclusive::open::(bs, 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 { @@ -115,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(); @@ -125,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>, @@ -157,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 1f21cdd3b6c..fd072b7c61d 100644 --- a/bootloader/src/slot.rs +++ b/bootloader/src/slot.rs @@ -246,7 +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 = crate::exclusive::open::(bs, 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 63c23c63f38..6f96d7356d0 100644 --- a/clippy.toml +++ b/clippy.toml @@ -4,5 +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 `exclusive::open`" }, + { 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`" }, ] From b581b0fa608836bb01b98abc41dbbe5f5d8374b6 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 10:02:09 +0200 Subject: [PATCH 15/19] kernel: FOREIGN_PROBE goes with its readers, and Storm takes main's number `FOREIGN_PROBE`, `PROBE_OFF` and `PROBE_LEN` were stored at xHCI bring-up and loaded nowhere: their four readers (`hda.rs`, `virtio_gpu.rs`, `virtio_sound.rs`, `pcidev/mod.rs` on main) went with the foreign-DMA actuators, and the `let _ =` on the `compare_exchange` kept every lint quiet. Round 1's write-only class; all three and the store are deleted. All of it sat under `boot-actuators`, so the shipping kernel is untouched. `Intid::Storm = 3` pinned a number #639 had already renumbered to 2 when 3b8102cf5 deleted `LogNest`, leaving INTID 2 a hole nothing read and `Storm`, `Hda` and `VirtioSound` disagreeing with main. The pin goes, and `kernel/src/arch/aarch64/irqchip.rs` is main's again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- kernel/src/arch/aarch64/irqchip.rs | 2 +- kernel/src/drivers/xhci/mod.rs | 14 -------------- kernel/src/drivers/xhci/wait/boot.rs | 7 ------- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/kernel/src/arch/aarch64/irqchip.rs b/kernel/src/arch/aarch64/irqchip.rs index 927e36e4932..7a2345f232c 100644 --- a/kernel/src/arch/aarch64/irqchip.rs +++ b/kernel/src/arch/aarch64/irqchip.rs @@ -41,7 +41,7 @@ pub(super) enum Intid { /// Stops a CPU for good: [`stop_other_cpus`]'s. Halt, /// What `irq-storm` floods this CPU with. - Storm = 3, + Storm, Hda, VirtioSound, } diff --git a/kernel/src/drivers/xhci/mod.rs b/kernel/src/drivers/xhci/mod.rs index fdfa7ef0396..e7b97ef8d96 100644 --- a/kernel/src/drivers/xhci/mod.rs +++ b/kernel/src/drivers/xhci/mod.rs @@ -450,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; diff --git a/kernel/src/drivers/xhci/wait/boot.rs b/kernel/src/drivers/xhci/wait/boot.rs index 631536033f7..66d252b0a1e 100644 --- a/kernel/src/drivers/xhci/wait/boot.rs +++ b/kernel/src/drivers/xhci/wait/boot.rs @@ -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). From 0830817758d0629bb56ba6de38e342e222ed9b4b Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 10:02:19 +0200 Subject: [PATCH 16/19] tests: prose about QEMU registrations the cut deleted goes With the cut, no QEMU registration shares a judge with a metal row, so every sentence that said one did is false: - `DEFAULT_WIDTH`'s 246-test timing, taken for a suite of 21 that no longer has its phases; - the "T14's readback and a QEMU boot log are judged by this one predicate" paragraph on fourteen judges only metal rows now call; - the metal rows' comparisons with "the QEMU registration" in `control_regs`, `irq_census_conservation`, `tlb_shootdown_cost` and the self-test cluster, and `TESTCASES_MKDIR`'s and `metal::Arm::boot`'s "a boot of its own in QEMU too"; - `usb_reset_on_metal`'s "the panic path's account is judged under QEMU only": `usb_reset_hands_devices_back`'s QEMU arms are cut, and the panic bound's account is a stage item of `the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`. Deletions only: what each comment says that is still true stays. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- tests/common/metal.rs | 5 ++- tests/common/power.rs | 5 +-- tests/toyos.rs | 74 +++---------------------------------------- 3 files changed, 8 insertions(+), 76 deletions(-) diff --git a/tests/common/metal.rs b/tests/common/metal.rs index 4eb7b104ff5..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, diff --git a/tests/common/power.rs b/tests/common/power.rs index 64439d3156b..3dcf5a19632 100644 --- a/tests/common/power.rs +++ b/tests/common/power.rs @@ -375,10 +375,7 @@ const TOOK_THE_LOCK: &str = "the controller lock was held from before the log vo /// 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/toyos.rs b/tests/toyos.rs index 9d337ec2db7..13a0f210766 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -15,17 +15,7 @@ use common::{audio, compile, devices, faults, lan, metal, power, screen, serial, use toyos_build::bootlog::{self}; use toyos_build::testargs::{self, SUITE}; -/// The width with no `--jobs`, and where it came from. -/// -/// 14 cores and about three host threads a guest divides out to four. The suite -/// says twelve. Alternated in one session on a quiet host, 246 tests, both -/// green: **125.6 s wide eight against 109.1 s wide twelve**, with the parallel -/// phase at 58.3 s against 42.1 s — the same 16 s and the same direction as the -/// pair taken on the tree six commits earlier. A guest here is mostly -/// *waiting* — for a marker, for a debounce, for a device — which is why this is -/// a measurement and not a division. -/// -/// **Twelve is the number for one suite on this host.** +/// The width with no `--jobs`. const DEFAULT_WIDTH: usize = 12; /// The shared-boot binaries that call `SYS_DEBUG`, and so cannot run on the @@ -210,9 +200,7 @@ const METAL: &[(&str, metal::Metal)] = &[ ), ( // The machine's own CPU count, off the SMP bring-up records — a source - // independent of the `control_regs:` lines it is then held to. The QEMU - // registration says four because the harness staged four; here the - // laptop says how many it has. + // independent of the `control_regs:` lines it is then held to. "control_regs", metal::Metal { arms: TESTCASES, @@ -228,9 +216,6 @@ const METAL: &[(&str, metal::Metal)] = &[ metal::Metal { arms: TESTCASES, judge: |b| klogd_hosted(&b[0].kernel()) }, ), ( - // The stimulus a host types at a console in QEMU is this boot's own job - // list on the T14: every job that runs and exits is a process exit, and - // the census is printed at each one. "irq_census_conservation", metal::Metal { arms: TESTCASES, @@ -428,9 +413,7 @@ const METAL: &[(&str, metal::Metal)] = &[ metal::Metal { arms: LATENCYCASE, // The machine's own CPU count, off the bring-up records rather than - // off a number the harness staged: the QEMU registration says eight - // because it asked for eight, and here the laptop says how many it - // has. + // off a number the harness staged. judge: |b| { let (p50, p99) = tlb_shootdown_cost(b[0].kernel().text(), b[0].cpus()?)?; b[0].measured("tlb.latencycase.p50_ns", p50)?; @@ -555,8 +538,7 @@ const METAL: &[(&str, metal::Metal)] = &[ // The cheapest cluster there is: every one of these arms a check that runs // at init, logs its verdict and does nothing else, so they cost one flash // between them. **Nothing had to be promoted into `kernel/src/params.rs`** - // — the metal profile flashes test images (the track's ruling), so an - // actuator is armed the way the QEMU registration arms it and the + // — the metal profile flashes test images (the track's ruling), and the // pre-flash gate is what says the machine survives each one. ( "pci_capability_walk", @@ -630,8 +612,7 @@ const TESTCASES: &[metal::Arm] = &[metal::once( /// **Two boots of one config, because these two cannot share one.** Each fills /// a machine-wide cap and leaves it filled: `mkdir_cap` fills the directory cap, /// and `readdir_bound`'s own `create_dir("/tmp/empty")` is then refused with -/// `OutOfMemory` and it panics — measured on the first staged image, and the -/// reason each has a boot of its own in QEMU too. +/// `OutOfMemory` and it panics — measured on the first staged image. const TESTCASES_MKDIR: &[metal::Arm] = &[metal::once("testcases-mkdir", "tests/testcases", &[], &["test_rs_mkdir_cap"])]; @@ -1975,10 +1956,6 @@ fn metal_sim_argv_check(argv: &[String]) -> Result<(), String> { } /// The scanout's memory type, out of the three records that decide it. -/// -/// Text in, a verdict out: `PAT:`, `GOP: scanout memory type` and `shm: … -/// mapped WriteCombining into pid` are all kernel records, so the T14's -/// readback and a QEMU console are judged by this one predicate. fn scanout_wc(console: &str) -> Result<(), String> { const PAT: &str = "PAT: IA32_PAT="; const SCANOUT: &str = "GOP: scanout memory type "; @@ -2055,11 +2032,6 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { /// set at all**. Silence about a bit is a hole rather than a permission. /// The interrupt census adds up, is monotonic, and every device delivery is /// still cpu0's. -/// -/// Text in, a verdict out. Every line it reads is a kernel record, so the -/// T14's readback and a QEMU capture are judged by this one predicate — and -/// on the T14 the *stimulus* is the boot's own job list rather than two -/// commands typed at a console. fn irq_census(capture: &str) -> Result<(), String> { use common::irqcensus::{Census, DEVICE_SOURCES}; // Every line, in order, so a later census can be compared with an @@ -2210,8 +2182,6 @@ fn irq_census(capture: &str) -> Result<(), String> { Ok(()) } -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn pci_cap_selftest(log: &str) -> Result<(), String> { if let Some(bad) = log.lines().find(|l| l.contains("pci cap selftest FAILED")) { return Err(format!("{bad}\n{log}")); @@ -2248,9 +2218,6 @@ fn pci_cap_selftest(log: &str) -> Result<(), String> { /// The kernel reopens init by pid after the last handle to it has gone, and /// no kernel thread's pid opens. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn process_reopen(log: &str) -> Result<(), String> { for control in ["process-reopen:", "process-open-kthread:"] { let Some(verdict) = log.lines().find(|l| l.contains(control)) else { @@ -2265,9 +2232,6 @@ fn process_reopen(log: &str) -> Result<(), String> { } /// A backing read after deletion is refused on both writable mounts, and a page-cache slot whose fill the device refused is unbound. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn read_fault_probes(log: &str) -> Result<(), String> { let probe = "revoke-selftest: /tmp/revoke_probe"; let Some(verdict) = log.lines().find(|l| l.contains(probe)) else { @@ -2281,9 +2245,6 @@ fn read_fault_probes(log: &str) -> Result<(), String> { } /// An "acquire before a fallible step" control: the count returned to its baseline after a refused call. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn leak_rollback(log: &str) -> Result<(), String> { let probe = "leak-selftest: device-mint"; let Some(verdict) = log.lines().find(|l| l.contains(probe)) else { @@ -2297,9 +2258,6 @@ fn leak_rollback(log: &str) -> Result<(), String> { } /// The spurious vector and an unclaimed one are both gated rather than escalated to #DF. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn lapic_vectors(log: &str) -> Result<(), String> { for kind in ["spurious", "unclaimed"] { if let Some(bad) = @@ -2332,9 +2290,6 @@ fn lapic_vectors(log: &str) -> Result<(), String> { } /// Nine crafted USB configuration descriptors, parsed at init. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn xhci_descriptors(log: &str) -> Result<(), String> { if let Some(bad) = log.lines().find(|l| l.contains("descriptor selftest FAILED")) { return Err(format!("{bad}\n{log}")); @@ -2363,9 +2318,6 @@ fn xhci_descriptors(log: &str) -> Result<(), String> { } /// Eight malformed extended-capability lists refused, and the handoff on every controller. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn xhci_xecp(log: &str) -> Result<(), String> { if let Some(bad) = log.lines().find(|l| l.contains("xecp selftest FAILED")) { return Err(format!("{bad}\n{log}")); @@ -2435,9 +2387,6 @@ const SYSRET_SS_RELOADED: &str = "sysret-ss: reloaded"; const SYSRET_SS_NOT_RELOADED: &str = "sysret-ss: NOT reloaded"; /// The context switch reloads SS from null before a `sysretq` can see it. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn sysret_ss(log: &str) -> Result<(), String> { if log.contains(SYSRET_SS_UNARMED) { return Err(format!("the SS-reload probe could not arm, so it measured nothing:\n{log}")); @@ -2458,9 +2407,6 @@ fn sysret_ss(log: &str) -> Result<(), String> { } /// The input core merged what it was handed. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn input_merge_ok(log: &str) -> Result<(), String> { if !log.contains("input-merge: ok") { return Err(format!("the input core check never reported:\n{log}")); @@ -2469,9 +2415,6 @@ fn input_merge_ok(log: &str) -> Result<(), String> { } /// An inner `scheduler::Operation` may only narrow, and its drop restores what it displaced. -/// -/// Text in, a verdict out: every line it reads is a kernel record, so the -/// T14's readback and a QEMU boot log are judged by this one predicate. fn operation_nesting_log(log: &str) -> Result<(), String> { /// One `key=value` off a gate line, as a number. @@ -2580,9 +2523,6 @@ fn operation_nesting_log(log: &str) -> Result<(), String> { } /// The machine's kernel thread is hosted. -/// -/// Text in, a verdict out: its line is a `log!` record, so the T14's -/// readback and a QEMU boot log are judged by this one predicate. fn klogd_hosted(boot: &serial::Serial) -> Result<(), String> { boot.must_be_clean()?; let line = boot.must_say("kthread: klogd")?; @@ -2592,10 +2532,6 @@ fn klogd_hosted(boot: &serial::Serial) -> Result<(), String> { /// Every I/O APIC this machine has, and whether its redirection table is a /// chip's rather than a floating bus's. -/// -/// Text in, a verdict out: the driver runs in Phase 2 and every line it -/// writes is a kernel record, so the T14's readback and a QEMU boot log are -/// judged by this one predicate. fn ioapic_topology(log: &str) -> Result<(), String> { let units: Vec<&str> = log .lines() From 83bac082dc1826283a3d219df20483c777790999 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 10:09:06 +0200 Subject: [PATCH 17/19] issues: the track names what the seven cuts' rows and copies do not hold Round 2's review found seven cuts recorded as covered or as copies that are neither. They stay cut, under the owner's ruling on the legal cuts, and the track now names every behaviour the review lists, each with its tier and an exit a test can fail: - `usb_stick_left` (stage E, host): the stick leaving under the port rung at three points, never offline, no second break, nothing sent to the empty port, its slot back. Its row arms only `usb-transport-break`. - `usb_reset_records_the_phase_it_cut` (stage E): the three Bulk-Only phases on the T14 with the `usb-wedge-*` arms brought back, and the no-room refusal on the host. Its row reads whichever phase the sweep cuts. - `usb_reset_hands_devices_back` (stage E, metal): the job deadline, the panic bound (waiting on the metal loop's panic issue) and the `xhci-lock-wedged` negative control. Its row judges two orderly reboots. - `lan_lease_report` (stage H, host): the flap, which the T14 cannot produce; its row reads only a lease. - `sshd_exec` (stage H, metal, on `lan_talk`'s exchange): the six arms `lan_talk`'s one command does not reach. - `virtio_net_no_msix` (stage J, host): `Refusal::NoInterrupt`'s one reader, beside `pci_claim_caps_truncated`. - `screen_late_panic` (stage B, host): the report painted from the frozen snapshot, and `check_wrap`. A muted boot refreshes the capture, so `screen_panic_muted` holds neither. The issues that pointed at these tests follow: `a-deliberate-wedge-...` lives with the stage E wedge rows; `the-metal-loop-cannot-judge-...` and `a-lease-kept-across-a-link-flap-...` lose the sentences that had QEMU judging what it no longer judges; `a-claims-own-refusals-...` loses its reader list (four of its five readers are cut) and lists those four as unread; `sys-debug-actions-...` no longer says a host test or a type will call `LOG_PATTERNED` or `LOCK_ACROSS_SWITCH`. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- ...d-shipping-it-cannot-shorten-the-matrix.md | 60 ------------------- ...device-class-answers-for-a-block-device.md | 52 ---------------- ...-runs-only-what-no-cheaper-tier-reaches.md | 58 +++++++++++++++++- ...al-loop-cannot-judge-a-boot-that-panics.md | 13 +--- ...-flap-is-not-verified-until-its-renewal.md | 9 +-- ...claims-own-refusals-are-read-by-nothing.md | 10 ++-- ...ics-every-other-cpu-that-wants-its-lock.md | 8 +-- ...and-two-loader-words-that-nothing-calls.md | 13 ++-- 8 files changed, 75 insertions(+), 148 deletions(-) delete mode 100644 issues/build/building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md delete mode 100644 issues/build/no-device-class-answers-for-a-block-device.md 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/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/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 index 72b4cc5fa09..20a9ea04556 100644 --- 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 @@ -8,7 +8,8 @@ opened: 2026-10-01 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. Stages are independent and run in parallel; a stage's +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. @@ -73,6 +74,12 @@ console's renderer. Exit: as stage A. - 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 @@ -136,6 +143,35 @@ Host: `toyos-xhci` and its sim. Metal: the T14's controller and stick. Exit: as - 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 when an empty port climbs the ladder to + `Offline` or counts as a break. +- 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 @@ -188,6 +224,21 @@ Host: `toyos-net-tcp`, `toyos-dns`, `toyos-mdns`, `toyos-swap`, `toyos-inspect`, - 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. @@ -222,6 +273,11 @@ Metal: the T14's VT-d. Host: `toyos-pci`, `toyos-pcid`, `toyos-hda`. Exit: as st 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 lifted into `toyos-pcid`, 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. 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/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/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 index ca93681085f..a5c68b063cd 100644 --- 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 @@ -13,14 +13,15 @@ names in `toyos-abi/src`, because a deletion there is an ABI change and that pul 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), `LOCK_ACROSS_SWITCH` (2), `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` and `lock_across_switch_halts`; +- `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`; -- `LOG_PATTERNED` (21), test-runner's `log-gate` and `log-storm`, for stage C's - `log_conservation_smp2`; +- `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`. From edd6e2ed5d18ba4ef0fad45a61c9084176cae739 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 10:09:23 +0200 Subject: [PATCH 18/19] issues: the ways back this branch broke, and the guards it cut, go Round 2's REMOVE, and the same class wherever it is measured: - `no-device-class-answers-for-a-block-device.md` (its subject, `driver_wait_refused`, is cut) and `building-the-image-once-and-shipping-it-cannot-shorten-the-matrix.md` (the matrix is deleted) went in 83bac082d, staged before it; nothing cites either, by path or by bare name. - `nothing-reaches-the-msi-arm-of-a-claimed-function.md` loses the `https_tls13_e1000e` guard on the order, `virtio_net_no_msix`'s false `enable_msi`, and the `tests/e1000case` guest arm: all three are cut. - The recorded `git revert` ways back. A way back goes where the review names it, or where `git merge-tree --merge-base= ^` (what `git revert` does, run with nothing in the worktree moving) applies on main's tree and conflicts on this head's, or must modify a file this head deleted: - named: `02366d741` (`usb_boot_stick_pulled`), `958ada05e` (`screen_console_shell`), `87f74892d ad6dc0781` (`log_poll_outlives_a_close`) and `3b8102cf5` (`log_nested_emit`); - the same `3b8102cf5` in `log-reserve-window-negative-...` and `smp-ap-hole-...`, which needs `log_gate.rs` (13f71c52e); - modify/delete at this head: `b20d3fd40 cd685b10a`, `603b6ee54`, `9ebf080e8 fa4c31409`, `9ebf080e8 b5c59cbcc 0e19bf898`, `57ac19ada`, `2dfe1008e`, and `4a60c35c8` and `415d9a102` on the flaky list; - clean on main, conflicting here: `4d3e2b164`, `866532c62 2a4893921 539977050 4600f6754`, `86e606616`, `24aa31815`, `cc291947e 96763794e`, `4a5b228d2`, and `4c191469f`, `c6923cd50` and `0a7fc5f70` on the flaky list. The flaky list loses the clause that made every bullet a way back, and keeps each bullet's commit, which is still the commit that took the test out; `parallel-tests-red-under-other-suites.md:184` and this list are where `screen_console_shell`'s behaviour stays recorded. The two citations that called the io-uring file the record of the commit that restores `usb_boot_stick_pulled` lose that clause. The other recorded ways back conflict on main already or apply cleanly at both, and are left as main wrote them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- ...-hangs-the-loader-past-the-firmware-watchdog.md | 2 +- ...e-exits-clean-with-none-of-its-refusals-said.md | 4 +--- ...-reds-beside-other-guests-and-is-green-alone.md | 3 +-- ...indow-negative-times-out-beside-other-guests.md | 3 +-- .../build/parallel-tests-red-under-other-suites.md | 7 +++---- .../process-stats-exits-101-beside-other-guests.md | 2 +- ...s-console-output-the-harness-is-slow-to-read.md | 4 +--- ...d-log-reserve-window-red-under-a-loaded-host.md | 3 +-- ...io-buffering-saw-one-long-line-arrive-as-two.md | 3 +-- ...se-ends-four-packets-short-with-a-clean-exit.md | 3 +-- ...d-answered-iofailed-once-beside-other-guests.md | 3 +-- .../pulling-the-boot-stick-freezes-the-t14.md | 2 +- ...-named-only-when-logd-reads-its-registration.md | 5 ++--- ...ot-stopped-answering-and-no-capture-says-why.md | 3 +-- ...echo-was-refused-with-an-error-nothing-names.md | 4 +--- issues/kernel/desktop-window-child-freeze.md | 2 +- .../kernel/every-wait-in-this-kernel-is-a-spin.md | 2 +- ...io-uring-enter-trips-the-one-queue-invariant.md | 2 +- ...ng-reaches-the-msi-arm-of-a-claimed-function.md | 14 +------------- ...ll-window-nmi-shortfalls-on-a-contended-host.md | 4 +--- 20 files changed, 23 insertions(+), 52 deletions(-) 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/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/parallel-tests-red-under-other-suites.md b/issues/build/parallel-tests-red-under-other-suites.md index 46640082ec2..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` 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 f30eaca234e..a024d03bcfc 100644 --- a/issues/build/process-stats-exits-101-beside-other-guests.md +++ b/issues/build/process-stats-exits-101-beside-other-guests.md @@ -35,6 +35,6 @@ 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 a69da7ac4ae..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 @@ -67,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/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-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 c4213b33530..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. +architectures and test-runner's `log-gate` builtin. 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/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. From b5ac169153ed28c789a4458f6108e876955919e2 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 10:52:19 +0200 Subject: [PATCH 19/19] Review round 3: protocol.rs's false clause, usb_stick_left's four reds, and five removals protocol.rs's module header began "Every protocol the loader opens, opened here", which was false: `load_file_bytes` calls uefi 0.26's `get_image_file_system`, which opens `LoadedImage`, `DevicePath` and `SimpleFileSystem` EXCLUSIVE inside the crate (`table/boot.rs:1332-1341`), and `uefi_services::init` reaches `uefi::allocator::init`, which opens `LoadedImage` EXCLUSIVE (`allocator.rs:44`). Banning `get_image_file_system` and routing its three opens through `protocol::exclusive` would still leave the allocator's, so the clause is deleted rather than made true. Every open uefi makes for the loader is of a protocol `Exclusive` admits; none is `GraphicsOutput`. The track's `usb_stick_left` exit was red on two of its four facts. It now names a red for each: `Offline`, a second break, a Reset Device or Address Device sent to the empty port, and a slot never disabled. Removed, as the review names them: - the `git revert` way back in `a-loaded-suite-reds-a-volume-checker-on-both-arms.md` and in `a-connect-between-two-accepts-is-reset.md`, which this branch broke; - `toyos-pcid` from stage J, a crate about PCIDs, not PCI claims; - the nightly's guest shards, which this branch deleted, from `tests/common/irqcensus.rs` and three issues' exits. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- bootloader/src/protocol.rs | 14 +++++++------- ...ded-suite-reds-a-volume-checker-on-both-arms.md | 2 +- ...omicity-loses-five-of-a-thousand-lines-on-ci.md | 3 +-- ...suite-runs-only-what-no-cheaper-tier-reaches.md | 10 ++++++---- .../a-connect-between-two-accepts-is-reset.md | 3 +-- ...talls-on-ci-with-one-sleeper-never-returning.md | 2 +- ...-refusals-saw-the-kernel-refuse-nothing-once.md | 2 +- tests/common/irqcensus.rs | 8 +++----- 8 files changed, 21 insertions(+), 23 deletions(-) diff --git a/bootloader/src/protocol.rs b/bootloader/src/protocol.rs index ad9625b6bfe..d8efcaf28a1 100644 --- a/bootloader/src/protocol.rs +++ b/bootloader/src/protocol.rs @@ -1,10 +1,10 @@ -//! Every protocol the loader opens, opened here: `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. +//! `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; 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/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/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 index 20a9ea04556..6d5c436518e 100644 --- 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 @@ -148,8 +148,10 @@ Host: `toyos-xhci` and its sim. Metal: the T14's controller and stick. Exit: as 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 when an empty port climbs the ladder to - `Offline` or counts as a break. + 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 @@ -257,7 +259,7 @@ Metal: the stick's `/log`. Exit: as stage A. ## Stage J: devices, PCI and DMA isolation -Metal: the T14's VT-d. Host: `toyos-pci`, `toyos-pcid`, `toyos-hda`. Exit: as stage A. +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 @@ -277,7 +279,7 @@ Metal: the T14's VT-d. Host: `toyos-pci`, `toyos-pcid`, `toyos-hda`. Exit: as st 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 lifted into `toyos-pcid`, red when either arm answers anything else. + `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. 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/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/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.