From 1b1c8af4feb82863705511711ca61359e08d1c05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Fri, 7 Aug 2026 17:23:05 +0800 Subject: [PATCH 1/4] refactor(axvm): layer RISC-V SBI IPI routing --- .claude/skills/arch-platform-porting/SKILL.md | 1 + book/design/axvm-riscv-sbi-ipi.md | 108 ++++++ .../configs/vms/qemu/riscv64/linux-smp1.toml | 6 +- .../vms/qemu/riscv64/linux-smp3-ipi.toml | 26 ++ .../build-riscv64gc-unknown-none-elf.toml | 2 +- virtualization/axvm/src/arch/aarch64/mod.rs | 9 +- virtualization/axvm/src/arch/mod.rs | 222 ------------- virtualization/axvm/src/arch/riscv64/ipi.rs | 93 ++++++ virtualization/axvm/src/arch/riscv64/mod.rs | 103 ++---- .../axvm/src/architecture/cpu_up.rs | 19 +- virtualization/axvm/src/architecture/mod.rs | 2 + virtualization/axvm/src/architecture/ops.rs | 27 -- virtualization/axvm/src/irq/sender.rs | 18 +- virtualization/axvm/src/vm/mod.rs | 11 +- .../axvm/tests/arch_boundary_contract.rs | 62 ++++ virtualization/riscv_vcpu/src/legacy_ipi.rs | 56 ---- virtualization/riscv_vcpu/src/lib.rs | 5 +- virtualization/riscv_vcpu/src/sbi_ipi.rs | 196 +++-------- virtualization/riscv_vcpu/src/types.rs | 68 +++- virtualization/riscv_vcpu/src/vcpu.rs | 308 ++++++------------ 20 files changed, 556 insertions(+), 786 deletions(-) create mode 100644 book/design/axvm-riscv-sbi-ipi.md create mode 100644 os/axvisor/configs/vms/qemu/riscv64/linux-smp3-ipi.toml create mode 100644 virtualization/axvm/src/arch/riscv64/ipi.rs create mode 100644 virtualization/axvm/tests/arch_boundary_contract.rs delete mode 100644 virtualization/riscv_vcpu/src/legacy_ipi.rs diff --git a/.claude/skills/arch-platform-porting/SKILL.md b/.claude/skills/arch-platform-porting/SKILL.md index 9e61429daa..9524e8d6db 100644 --- a/.claude/skills/arch-platform-porting/SKILL.md +++ b/.claude/skills/arch-platform-porting/SKILL.md @@ -88,6 +88,7 @@ Current Axvisor LoongArch QEMU bring-up uses the dynamic UEFI platform path. The - **LoongArch QEMU IRQ contract**: the dynamic LoongArch path targets QEMU `virt`/LS7A-style firmware routing through CPU-local timer/IPI lines, EIOINTC, and PCH-PIC. `somehal::begin_irq(raw)` receives the CPU interrupt line from `ESTAT.IS`, not an ACPI GSI or PCI vector; only the timer line, IPI line, and EIOINTC cascade line may enter runtime dispatch. EIOINTC owns claim/complete of external vectors, while PCH-PIC owns PCH input state, ACPI trigger/polarity configuration, mask state, and route memory through its `rdif_intc::Intc` lock. Do not infer PCH-PIC input by subtracting `PCI_INTX_VECTOR_BASE`, do not treat ACPI `route.vector = PCI_INTX_VECTOR_BASE + gsi` as the EIOINTC hardware vector, and do not dispatch unknown CPU-local interrupt lines as PCH-PIC IRQs. - **LoongArch LS2K LIOINTC contract**: follow the AArch64 GIC distributor/CPU-interface ownership split. The `rdif_intc` controller owns route and W1 enable/disable registers, while a separately published shutdown-lifetime CPU interface owns only the domain, ISR mapping, parent lines, and atomic enabled snapshot needed by hard IRQ claim/complete. Publish the CPU interface before enabling the parent cascade. Hard IRQ must not call `rdrive::get_list`, take a task-owned controller lock, allocate, or block. Publish enable after the controller's hardware write, and hide a disabled input from claim before disabling it in hardware. - **RISC-V QEMU IRQ contract**: the dynamic RISC-V path targets QEMU `virt` firmware routing through CPU-local supervisor timer/software/external interrupt causes and one PLIC domain. `somehal::begin_irq(raw)` receives `scause.bits()`, not a PLIC source number; only S-timer, S-soft, and S-ext are runtime CPU-local causes. PLIC source IDs are controller-local `HwIrq`s and may only be produced by FDT translation or by claiming the PLIC after an S-ext trap. Do not dispatch a bare source number as a trap, do not treat PLIC source 0 as valid, and route PLIC enable through the registered `rdif_intc::Intc` controller instead of bypassing the rdrive lock. +- **RISC-V guest SBI IPI contract**: keep SBI decoding, hart-mask representation, completion ABI, and saved/live HVIP state in `riscv_vcpu`; keep guest hart-to-vCPU topology resolution in AxVM's RISC-V layer; and publish VSSIP through the architecture-neutral `VmInterruptSender` path. Validate the complete target set before publishing any interrupt. Current and remote vCPUs must use the same queued delivery path, and guest VSSIP must remain distinct from host S-soft runtime IPI and PLIC routing. Keep this contract synchronized with `book/design/axvm-riscv-sbi-ipi.md`. - **Runtime console selection**: Dynamic platforms expose the firmware-selected hardware console through `somehal::console_device_id()` and `ax_hal::console::device_id()`. The value is `Result` derived from bootargs `console=`, ACPI SPCR, or FDT `stdout-path`; static platforms return `Err(NotSpecified)`. Linux-style `ttyS` and `ttyAMA` select the Nth ordinary FDT serial node, while Rockchip `ttyFIQ` is a distinct `ConsoleSpec::RockchipFiq(N)` and must resolve against the Nth enabled `rockchip,fiq-debugger` node rather than aliasing an ordinary UART index. Numeric `tty`, bare `tty`, and `ttynull` are virtual selections and must not bind a hardware device. OS code such as Starry should match `Ok(id)` against probed serial devices, use `ttyS0` as the Linux-style hardware-console fallback only for `Err(NotSpecified)`, and leave `/dev/console` unbound (`ENODEV`) for non-hardware console selections, unmatched selected hardware devices, or when no serial console TTY exists. Keep the console-spec parser and FDT node-to-`DeviceId` mapping together in `somehal`; do not reparse FDT or bootargs in the tty layer. - **Runtime console ownership**: once Starry or another OS runtime binds the firmware-selected UART to an interrupt-driven tty/serial driver, claim both runtime output routing and the low-level platform output path through the serial-runtime ownership operation; do not leave those as separate caller-side transitions. Axvisor must match the exact firmware-selected `DeviceId`, lease its unique RX subscription, start the runtime, and roll both ownership steps back on failure. SBI or another console without a hardware serial runtime may keep an explicitly documented polling transport. The hardware console must have one runtime register owner; otherwise kernel log output and tty output can interleave at the UART register level and corrupt test markers or user input/output. - **Axvisor guest platform identity**: every Axvisor guest has a mandatory virtual UART with stable ID `console0`. Resolve it in the order machine fallback, valid host FDT/ACPI console snapshot, then user request with the same ID. FDT snapshots preserve node path, phandle, address span, register model/shift/access width, interrupt parent/specifier, clock providers, and stdout identity; ACPI snapshots preserve SPCR model, address space, range, IRQ, clock, baud, and namespace without retaining parser references or AML bytes. Guest TOML may replace `console0` model/options or add another serial ID, but must never provide numeric address, IRQ, controller, MSI/LPI, or `enabled = false`. A compatible model/transport keeps host fixed bindings and identity; an incompatible replacement becomes an automatically allocated virtual device. Keep exactly one host-console backend owner. Place vGIC distributor/redistributor at host GIC windows while retaining firmware identity. Firmware may describe padded, mutually overlapping GICv2 ranges; retain bases but normalize trapped apertures to the architectural 4 KiB Distributor and 8 KiB CPU-interface spans before registration. Corresponding physical UART and GIC ranges remain host-owned and excluded from passthrough. The application console mux remains the sole host-console input reader, and `SerialBackendFactory` creates a fresh generation on graph rebuild. A host-derived UART INTID remains a virtual controller input, not physical IRQ passthrough. diff --git a/book/design/axvm-riscv-sbi-ipi.md b/book/design/axvm-riscv-sbi-ipi.md new file mode 100644 index 0000000000..8ea8aa9f14 --- /dev/null +++ b/book/design/axvm-riscv-sbi-ipi.md @@ -0,0 +1,108 @@ +# AxVM RISC-V SBI IPI 分层设计 + +## 背景与目标 + +RISC-V guest SMP 启动和核间调度依赖 SBI IPI。SBI IPI 的调用约定、hart mask +以及 VSSIP 状态是 RISC-V vCPU 协议的一部分;guest hart 拓扑属于 VM;中断队列、 +目标 vCPU 唤醒和宿主 IPI 则属于 AxVM 的通用运行时。如果把这些职责放进公共 +`ArchOps`,公共层就会出现 RISC-V `cfg`、协议字段和路由分支,后续架构能力也会继续 +向同一接口堆叠。 + +本设计只实现 RISC-V guest SBI IPI,不改变宿主 IPI、vPLIC、其他 guest 架构、 +StarryOS syscall 或启动流程。成功标准是三核 Linux guest 能发现 SBI IPI extension、 +启动至少三个 CPU,并观察到非零 IPI 计数。 + +规范依据: + +- [SBI IPI Extension](https://github.com/riscv-non-isa/riscv-sbi-doc/blob/master/src/ext-ipi.adoc) +- [SBI Legacy Extensions](https://github.com/riscv-non-isa/riscv-sbi-doc/blob/master/src/ext-legacy.adoc) + +## 方案选择 + +| 方案 | 结论 | 原因 | +| --- | --- | --- | +| 在 `ArchOps` 增加 IPI 方法,并在公共实现中加入 RISC-V `cfg` | 不采用 | 把 guest SBI 协议泄漏到公共架构接口,每增加一个可选架构能力都会扩大公共 trait | +| 当前 vCPU 直接写 HVIP,远端 vCPU 走发送队列 | 不采用 | 同一事件有两套顺序、唤醒和 drain 语义,难以证明当前核与远端核行为一致 | +| vCPU 协议层产出类型化请求,RISC-V AxVM 层解析拓扑,通用 sender 发布 | 采用 | 各层只拥有自己的事实源,并复用现有中断发布与唤醒路径 | + +不另建 SBI IPI crate。当前协议规模不足以支撑新的发布单元,而 `riscv_vcpu` 已经拥有 +SBI ECALL 与 vCPU 中断状态。 + +## 所有权边界 + +### `riscv_vcpu` + +`riscv_vcpu` 拥有以下 RISC-V 协议事实: + +- `RiscvIpiRequest` 中的 `hart_mask`、`hart_mask_base` 和 + `RiscvIpiAbi::{Legacy, SbiV02}`; +- SBI v0.2 IPI extension、legacy `SEND_IPI`/`CLEAR_IPI` 的解码; +- legacy/SBI v0.2 返回寄存器的差异,以及 ECALL PC 只推进一次的约束; +- 保存的 HVIP 和当前绑定硬件 CSR 中 VSSIP、VSTIP、VSEIP 的一致性。 + +请求字段私有,只提供只读访问器。AxVM 不能自行拼装协议请求,也不需要知道返回寄存器。 +`CLEAR_IPI` 只清除当前 vCPU 的 VSSIP,不改变 timer、external 或其他 vCPU 状态。 + +### AxVM RISC-V 层 + +`virtualization/axvm/src/arch/riscv64/ipi.rs` 拥有 SBI hart 到 AxVM vCPU 的解释: + +- `hart_mask_base == usize::MAX` 表示所有已配置 guest hart; +- 零 mask 是成功的空操作; +- 普通 mask 的每个置位通过 `base + bit` 得到 guest hart ID; +- guest hart ID 通过 crate-private `VmArchCpuIdResolver::vcpu_id_for_arch_cpu_id` 查询映射为 + vCPU ID。 + +该 capability trait 在正常的 `architecture::cpu_up` 模块中为 `AxVM` 实现,CPU-up 与 IPI +共用同一查询,配置三元组不再由调用方分别拆解。RISC-V 路由实现保持在私有架构模块中, +不向公共 `arch/mod.rs` 或 `architecture/ops.rs` 暴露 hart-mask helper。 + +### AxVM 通用中断运行时 + +`VmInterruptSender`、`PendingVcpuInterrupt` 和目标运行时拥有队列、唤醒与宿主 IPI。 +VSSIP 以 level-triggered `VirtualInterruptId(1)` 发布。当前 vCPU 和远端 vCPU 都先进入 +同一队列,再由目标侧 drain 调用 vCPU 中断注入;RISC-V 层不直接写当前 vCPU 的 HVIP。 + +## 数据流 + +1. guest 执行 legacy `SEND_IPI` 或 SBI v0.2 `send_ipi` ECALL。 +2. `riscv_vcpu` 解码参数,推进一次 PC,返回 `RiscvVmExit::SendIpi(request)`;此时不预写成功。 +3. AxVM RISC-V 层先解析并验证完整目标集合。 +4. 验证成功后,为每个目标通过 `VmInterruptSender` 发布 level-triggered VSSIP。 +5. 目标运行时入队、唤醒并 drain,`riscv_vcpu` 更新保存的 HVIP;若该 vCPU 当前绑定, + 同时更新硬件 HVIP CSR。 +6. AxVM 把整体投递结果交给原 vCPU 的 `complete_ipi`,由协议层写回对应 ABI 的返回值。 + +## 错误与原子性语义 + +- 普通 mask 的 `base + bit` 溢出,或任一 guest hart 未配置,返回 + `SBI_ERR_INVALID_PARAM`。 +- 目标解析在发布前完成,因此参数错误不会产生部分投递。 +- 目标解析成功后的入队或唤醒失败返回 `SBI_ERR_FAILED`。此前已发布的软件中断不可可靠 + 回滚,调用方不能把运行时失败理解为没有任何目标观察到中断。 +- legacy mask 在本次 RV64、最多 64 个 guest hart 的边界内只读取一个 XLEN word; + guest 指针短读或不可读返回失败。 +- 零 mask 不投递中断并返回成功。 + +## 锁与并发边界 + +目标集合解析发生在 vCPU 退出处理的 task context,可以构造临时 `Vec`。解析期间不持有 +目标 vCPU 的运行时锁,也不发布事件。发布阶段沿用 `VmInterruptSender`:运行时注册表查找、 +目标队列更新和唤醒按现有窄临界区顺序完成,不在广域锁内调用目标 vCPU 后端。 + +HVIP 的保存副本由 `riscv_vcpu` 独占;只有当前绑定到硬件的 vCPU 才同步写 CSR。这样迁移、 +解绑和重新绑定仍以保存状态为事实源,不要求 AxVM 路由层持有 vCPU 内部锁。 + +## 验证与回滚 + +回归使用独立的 `linux-smp3-ipi.toml`,不改变 `linux-smp1.toml` 的单核语义。该配置为三核 +新内核提供 128 MiB guest RAM,避免 64 MiB 配置在初始化驱动前只剩约 15 MiB 可用内存而 +失去确定性。QEMU 用例同时检查 `nproc >= 3`、SBI IPI extension 日志和 +`/proc/interrupts` 非零 IPI 计数,最终输出唯一标记 `guest smp ipi pass!`。 + +源码边界契约测试禁止在 `architecture/ops.rs` 加入架构 `cfg`,也禁止公共 `arch/mod.rs` +出现 hart-mask、IPI 路由 helper 或测试专用 RISC-V 编译分支。该测试应在旧 PR #1681 实现 +上失败,在本设计上通过。 + +若需要回滚,应整体移除 RISC-V IPI 请求处理、独立 QEMU 用例和本设计文档;不得保留公共层 +架构分支或恢复当前 vCPU 直接注入的特殊路径。 diff --git a/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml b/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml index 59e93a8820..fceaf2816a 100644 --- a/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml +++ b/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml @@ -8,15 +8,13 @@ name = "linux-qemu" # Virtualization type. guest_type = "passthrough" # The number of virtual CPUs. -cpu_num = 3 +cpu_num = 1 # Guest vm physical cpu sets. -phys_cpu_ids = [0, 1, 2] -phys_cpu_sets = [1, 2, 4] +phys_cpu_ids = [0] # # Vm kernel configs # [kernel] -cmdline = "earlycon=sbi console=ttyS0,115200 init=/bin/sh root=/dev/vda rw" # The entry point of the kernel image. entry_point = 0x9020_0000 # The location of image: "memory" | "fs". diff --git a/os/axvisor/configs/vms/qemu/riscv64/linux-smp3-ipi.toml b/os/axvisor/configs/vms/qemu/riscv64/linux-smp3-ipi.toml new file mode 100644 index 0000000000..99dc86ee62 --- /dev/null +++ b/os/axvisor/configs/vms/qemu/riscv64/linux-smp3-ipi.toml @@ -0,0 +1,26 @@ +# VM base information. +[base] +id = 1 +name = "linux-qemu-smp3-ipi" +guest_type = "passthrough" +cpu_num = 3 +phys_cpu_ids = [0, 1, 2] +phys_cpu_sets = [1, 2, 4] + +# VM kernel configuration. +[kernel] +cmdline = "earlycon=sbi console=ttyS0,115200 init=/bin/sh root=/dev/vda rw" +entry_point = 0x9020_0000 +image_location = "fs" +kernel_path = "/guest/linux/linux-qemu" +kernel_load_addr = 0x9020_0000 +dtb_load_addr = 0x9300_0000 + +# System RAM, identity-mapped for DMA-capable passthrough. +memory_regions = [ + [0x9000_0000, 0x0800_0000, 0x7, 1], +] + +[devices] +passthrough = [] +disabled = [] diff --git a/test-suit/axvisor/normal/qemu-riscv-ipi/build-riscv64gc-unknown-none-elf.toml b/test-suit/axvisor/normal/qemu-riscv-ipi/build-riscv64gc-unknown-none-elf.toml index 9192054037..53cd21f0fb 100644 --- a/test-suit/axvisor/normal/qemu-riscv-ipi/build-riscv64gc-unknown-none-elf.toml +++ b/test-suit/axvisor/normal/qemu-riscv-ipi/build-riscv64gc-unknown-none-elf.toml @@ -5,5 +5,5 @@ features = [ ] log = "Info" target = "riscv64gc-unknown-none-elf" -vm_configs = ["os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml"] +vm_configs = ["os/axvisor/configs/vms/qemu/riscv64/linux-smp3-ipi.toml"] max_cpu_num = 4 diff --git a/virtualization/axvm/src/arch/aarch64/mod.rs b/virtualization/axvm/src/arch/aarch64/mod.rs index a077be6d3a..4390668d97 100644 --- a/virtualization/axvm/src/arch/aarch64/mod.rs +++ b/virtualization/axvm/src/arch/aarch64/mod.rs @@ -12,11 +12,13 @@ use ax_memory_addr::VirtAddr; use axvm_types::{VmBackendError as BackendError, VmBackendResult as BackendResult, *}; use super::*; -use crate::{AxVmResult, ax_err}; +use crate::{ + AxVmResult, + architecture::cpu_up::{self, CpuUpExit, CpuUpOps}, + ax_err, +}; mod capabilities; -#[path = "../../architecture/cpu_up.rs"] -mod cpu_up; pub(crate) mod fdt; mod firmware_plan; mod gic; @@ -34,7 +36,6 @@ pub(crate) use vm_plan::Aarch64VmPlan; mod vtimer; pub use capabilities::{host_fdt_bootarg, host_phys_to_virt}; -use cpu_up::{CpuUpExit, CpuUpOps}; pub use images::ImageLoader; use sysreg::{SysRegReadExit, SysRegWriteExit}; use vgic::Aarch64VgicRuntimeKey; diff --git a/virtualization/axvm/src/arch/mod.rs b/virtualization/axvm/src/arch/mod.rs index 223daa95d2..bef4120a33 100644 --- a/virtualization/axvm/src/arch/mod.rs +++ b/virtualization/axvm/src/arch/mod.rs @@ -136,225 +136,3 @@ pub(crate) fn default_boot_firmware_load_gpa( ) -> Option { CurrentArch::default_boot_firmware_load_gpa(config) } - -#[cfg(any(target_arch = "riscv64", test))] -pub(crate) fn riscv_hart_mask_targets( - hart_mask: usize, - hart_mask_base: usize, - vcpu_mappings: impl IntoIterator, usize)>, -) -> crate::CpuMask<64> { - let mut targets = crate::CpuMask::new(); - - for (vcpu_id, _, phys_id) in vcpu_mappings { - // CpuMask<64> cannot represent a local vCPU ID >= 64. - if vcpu_id >= 64 { - continue; - } - - // SBI uses ULONG_MAX as the all-harts selector. - if hart_mask_base == usize::MAX { - targets.set(vcpu_id, true); - continue; - } - - // A hart below the requested base is not selected. - let Some(bit) = phys_id.checked_sub(hart_mask_base) else { - continue; - }; - - // Ignore mask bits that cannot exist on this host. - if bit >= usize::BITS as usize { - continue; - } - - if ((hart_mask >> bit) & 1) != 0 { - targets.set(vcpu_id, true); - } - } - - targets -} - -/// Delivers a computed IPI target mask to the current and remote vCPUs. -/// -/// This helper is shared by the production RISC-V SEND_IPI path and tests, -/// so tests cover the same split between local HVIP injection and remote queueing. -#[cfg(any(target_arch = "riscv64", test))] -pub(crate) fn deliver_riscv_ipi_targets( - targets: crate::CpuMask<64>, - current_vcpu_id: usize, - vector: usize, - mut inject_current: impl FnMut(usize) -> Result<(), E>, - mut inject_remote: impl FnMut(crate::CpuMask<64>, usize) -> Result<(), E>, -) -> Result<(), E> { - if current_vcpu_id < 64 && targets.get(current_vcpu_id) { - inject_current(vector)?; - } - - let mut remote_targets = targets; - if current_vcpu_id < 64 { - remote_targets.set(current_vcpu_id, false); - } - - if !remote_targets.is_empty() { - inject_remote(remote_targets, vector)?; - } - - Ok(()) -} - -#[cfg(test)] -mod riscv_hart_mask_tests { - use super::*; - - #[test] - fn legacy_hart_mask_routes_sparse_guest_hart_to_local_vcpu() { - let mappings = [ - (0usize, None, 4usize), - (1usize, None, 9usize), - (2usize, None, 5usize), - ]; - - let targets = riscv_hart_mask_targets(1usize << 5, 0, mappings); - - assert!(targets.get(2)); - assert!(!targets.get(0)); - assert!(!targets.get(1)); - } - - #[test] - fn standard_hart_mask_uses_non_zero_base_before_mapping_to_local_vcpu() { - let mappings = [ - (0usize, None, 4usize), - (1usize, None, 9usize), - (2usize, None, 5usize), - ]; - - let targets = riscv_hart_mask_targets(1usize << 1, 4, mappings); - - assert!(targets.get(2)); - assert!(!targets.get(0)); - assert!(!targets.get(1)); - } - - #[test] - fn standard_hart_mask_base_max_targets_all_vcpus() { - let mappings = [ - (0usize, None, 4usize), - (1usize, None, 9usize), - (2usize, None, 5usize), - ]; - - let targets = riscv_hart_mask_targets(0, usize::MAX, mappings); - - assert!(targets.get(0)); - assert!(targets.get(1)); - assert!(targets.get(2)); - } -} - -#[cfg(test)] -mod standard_hart_mask_mapping_tests { - use super::*; - - #[test] - fn standard_hart_mask_base_maps_guest_hart_to_local_vcpu() { - // local vCPU 0/1/2 correspond to guest hart IDs 4/5/9. - let mappings = std::vec![ - (0usize, None, 4usize), - (1usize, None, 5usize), - (2usize, None, 9usize), - ]; - - // base=4, bit 1 selects guest hart 5 only. - let targets = riscv_hart_mask_targets(1usize << 1, 4, mappings); - - assert!(!targets.get(0)); - assert!(targets.get(1)); - assert!(!targets.get(2)); - } -} - -#[cfg(test)] -mod riscv_ipi_delivery_boundary_tests { - use super::*; - - #[test] - fn out_of_range_vcpu_id_is_ignored() { - let mappings = [(0usize, None, 5usize), (64usize, None, 5usize)]; - - let targets = riscv_hart_mask_targets(1usize << 5, 0, mappings); - - assert!(targets.get(0)); - assert!(!targets.get(1)); - } - - #[test] - fn hart_below_base_is_ignored() { - let mappings = [(0usize, None, 3usize), (1usize, None, 5usize)]; - - let targets = riscv_hart_mask_targets(1usize << 1, 4, mappings); - - assert!(!targets.get(0)); - assert!(targets.get(1)); - } - - #[test] - fn send_ipi_routes_only_selected_remote_vcpu() { - let mut targets = crate::CpuMask::<64>::new(); - targets.set(2, true); - - let mut current_count = 0usize; - let mut remote_mask = crate::CpuMask::<64>::new(); - - deliver_riscv_ipi_targets( - targets, - 0, - 1, - |_| -> Result<(), ()> { - current_count += 1; - Ok(()) - }, - |mask, vector| -> Result<(), ()> { - assert_eq!(vector, 1); - remote_mask = mask; - Ok(()) - }, - ) - .unwrap(); - - assert_eq!(current_count, 0); - assert!(remote_mask.get(2)); - assert!(!remote_mask.get(0)); - assert!(!remote_mask.get(1)); - } - - #[test] - fn send_ipi_injects_current_vcpu_only_when_selected() { - let mut targets = crate::CpuMask::<64>::new(); - targets.set(0, true); - targets.set(2, true); - - let mut current_count = 0usize; - let mut remote_mask = crate::CpuMask::<64>::new(); - - deliver_riscv_ipi_targets( - targets, - 0, - 1, - |_| -> Result<(), ()> { - current_count += 1; - Ok(()) - }, - |mask, _| -> Result<(), ()> { - remote_mask = mask; - Ok(()) - }, - ) - .unwrap(); - - assert_eq!(current_count, 1); - assert!(remote_mask.get(2)); - assert!(!remote_mask.get(0)); - } -} diff --git a/virtualization/axvm/src/arch/riscv64/ipi.rs b/virtualization/axvm/src/arch/riscv64/ipi.rs new file mode 100644 index 0000000000..82a843c5db --- /dev/null +++ b/virtualization/axvm/src/arch/riscv64/ipi.rs @@ -0,0 +1,93 @@ +//! RISC-V guest IPI routing through the VM runtime interrupt channel. + +use std::vec::Vec; + +use riscv_vcpu::{RiscvIpiCompletion, RiscvIpiRequest}; + +use super::{AxvmRiscvVcpu, RiscvDeferredRunWork}; +use crate::{ + AxVMRef, AxVmResult, InterruptTriggerMode, + architecture::{BoundVcpuExit, cpu_up::VmArchCpuIdResolver}, + irq::{ + model::{PendingVcpuInterrupt, VirtualInterruptId}, + sender::VmInterruptSender, + }, + vm::AxVCpuRef, +}; + +const SUPERVISOR_SOFTWARE_INTERRUPT_ID: VirtualInterruptId = VirtualInterruptId(1); + +pub(super) fn handle( + vm: &AxVMRef, + vcpu: &AxVCpuRef, + request: RiscvIpiRequest, +) -> AxVmResult> { + let completion = match resolve_targets(vm, request) { + Ok(targets) => deliver(vm, &targets), + Err(error) => { + warn!( + "VM[{}] VCpu[{}] rejected SBI IPI request {:?}: {:?}", + vm.id(), + vcpu.id(), + request, + error + ); + RiscvIpiCompletion::InvalidParameter + } + }; + vcpu.get_arch_vcpu().complete_ipi(request, completion); + Ok(BoundVcpuExit::Continue) +} + +fn deliver(vm: &AxVMRef, targets: &[usize]) -> RiscvIpiCompletion { + let sender = VmInterruptSender::new(vm); + let interrupt = PendingVcpuInterrupt { + id: SUPERVISOR_SOFTWARE_INTERRUPT_ID, + trigger: InterruptTriggerMode::LevelTriggered, + }; + + for &target_vcpu_id in targets { + if let Err(error) = sender.send(target_vcpu_id, interrupt) { + warn!( + "VM[{}] failed to deliver SBI IPI to VCpu[{}]: {:?}", + vm.id(), + target_vcpu_id, + error + ); + return RiscvIpiCompletion::Failed; + } + } + RiscvIpiCompletion::Success +} + +fn resolve_targets(vm: &AxVMRef, request: RiscvIpiRequest) -> Result, IpiTargetError> { + if request.hart_mask_base() == usize::MAX { + return Ok(vm.vcpu_list().iter().map(|vcpu| vcpu.id()).collect()); + } + + let mut targets = Vec::new(); + for bit in 0..usize::BITS { + if request.hart_mask() & (1usize << bit) == 0 { + continue; + } + let hart_id = request + .hart_mask_base() + .checked_add(bit as usize) + .ok_or(IpiTargetError::HartIdOverflow)?; + let target_vcpu_id = vm + .vcpu_id_for_arch_cpu_id(hart_id) + .ok_or(IpiTargetError::UnavailableHart(hart_id))?; + if targets.contains(&target_vcpu_id) { + return Err(IpiTargetError::DuplicateVcpu(target_vcpu_id)); + } + targets.push(target_vcpu_id); + } + Ok(targets) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum IpiTargetError { + HartIdOverflow, + UnavailableHart(usize), + DuplicateVcpu(usize), +} diff --git a/virtualization/axvm/src/arch/riscv64/mod.rs b/virtualization/axvm/src/arch/riscv64/mod.rs index 527b7134af..733099226a 100644 --- a/virtualization/axvm/src/arch/riscv64/mod.rs +++ b/virtualization/axvm/src/arch/riscv64/mod.rs @@ -5,19 +5,24 @@ use axvm_types::{VmBackendError as BackendError, VmBackendResult as BackendResul use riscv_vcpu::{GprIndex as RiscvGprIndex, *}; use super::*; -use crate::{AxVmResult, StopReason, architecture::ops::*, host::*}; +use crate::{ + AxVmResult, StopReason, + architecture::{ + cpu_up::{self, CpuUpExit, CpuUpOps}, + ops::*, + }, + host::*, +}; mod capabilities; -#[path = "../../architecture/cpu_up.rs"] -mod cpu_up; pub(crate) mod fdt; mod images; +mod ipi; mod irq; mod npt; mod resource_pools; mod vm; pub use capabilities::{host_fdt_bootarg, host_phys_to_virt}; -use cpu_up::{CpuUpExit, CpuUpOps}; pub use images::ImageLoader; pub(crate) use vm::RiscvVmPlan; @@ -40,35 +45,6 @@ impl ArchOps for Riscv64Arch { type DeferredRunWork = RiscvDeferredRunWork; type NestedPageTable = npt::NestedPageTable; - fn ipi_targets( - vm: &crate::AxVMRef, - current_vcpu_id: usize, - target_cpu: u64, - target_cpu_aux: u64, - send_to_all: bool, - send_to_self: bool, - ) -> crate::CpuMask<64> { - let mut targets = crate::CpuMask::new(); - - if send_to_all { - for vcpu in vm.vcpu_list() { - if vcpu.id() != current_vcpu_id { - targets.set(vcpu.id(), true); - } - } - } else if send_to_self { - targets.set(current_vcpu_id, true); - } else { - targets = super::riscv_hart_mask_targets( - target_cpu as usize, - target_cpu_aux as usize, - vm.get_vcpu_affinities_pcpu_ids(), - ); - } - - targets - } - fn set_vcpu_on_args(vcpu: &crate::vm::AxVCpuRef, vcpu_id: usize, arg: usize) { vcpu.set_gpr(RiscvGprIndex::A0 as usize, vcpu_id); vcpu.set_gpr(RiscvGprIndex::A1 as usize, arg); @@ -116,6 +92,18 @@ impl ArchOps for Riscv64Arch { crate::check_timer_events(); } + fn inject_vcpu_interrupt( + vcpu: &crate::vm::AxVCpuRef, + interrupt: crate::irq::model::PendingVcpuInterrupt, + ) -> AxVmResult { + const SCAUSE_INTERRUPT_BIT: usize = 1 << (usize::BITS - 1); + + // VirtualInterruptId carries the RISC-V cause number. The backend + // consumes a complete scause value, including its interrupt bit. + let vector = SCAUSE_INTERRUPT_BIT | interrupt.id.0 as usize; + vcpu.inject_interrupt_with_trigger(vector, interrupt.trigger) + } + fn handle_vcpu_exit_bound( vm: &crate::AxVMRef, vcpu: &crate::vm::AxVCpuRef, @@ -165,6 +153,7 @@ impl ArchOps for Riscv64Arch { }, )) } + RiscvVmExit::SendIpi(request) => ipi::handle(vm, vcpu, request), RiscvVmExit::CpuUp { target_cpu, entry_point, @@ -178,50 +167,6 @@ impl ArchOps for Riscv64Arch { arg, }, ), - RiscvVmExit::SendIPI { - target_cpu, - target_cpu_aux, - send_to_all, - send_to_self, - vector, - } => { - let targets = ::ipi_targets( - vm, - vcpu.id(), - target_cpu, - target_cpu_aux, - send_to_all, - send_to_self, - ); - - if targets.is_empty() { - warn!( - "VM[{}] SendIPI has no target: target_cpu={target_cpu:#x}", - vm.id() - ); - return Ok(BoundVcpuExit::Complete(VcpuRunAction { - waits_for_event: false, - stop_reason: None, - resets_vm: false, - exits_vcpu: false, - })); - } - - super::deliver_riscv_ipi_targets( - targets, - vcpu.id(), - vector as _, - |vector| crate::inject_current_vcpu_interrupt(vector), - |remote_targets, vector| vm.inject_interrupt_to_vcpu(remote_targets, vector), - )?; - - Ok(BoundVcpuExit::Complete(VcpuRunAction { - waits_for_event: false, - stop_reason: None, - resets_vm: false, - exits_vcpu: false, - })) - } RiscvVmExit::CpuDown { state } => { warn!( "VM[{}] run VCpu[{}] CpuDown state {state:#x}", @@ -412,6 +357,10 @@ impl AxvmRiscvVcpu { riscv_result(self.0.sync_bound_vseip(asserted)) .map_err(|error| crate::AxVmError::vcpu("synchronize RISC-V VSEIP", error)) } + + fn complete_ipi(&mut self, request: RiscvIpiRequest, completion: RiscvIpiCompletion) { + self.0.complete_ipi(request, completion); + } } impl VmArchVcpuOps for AxvmRiscvVcpu { diff --git a/virtualization/axvm/src/architecture/cpu_up.rs b/virtualization/axvm/src/architecture/cpu_up.rs index 867cfbfd6e..2f00855a03 100644 --- a/virtualization/axvm/src/architecture/cpu_up.rs +++ b/virtualization/axvm/src/architecture/cpu_up.rs @@ -14,15 +14,28 @@ pub(crate) struct CpuUpExit { pub(crate) arg: u64, } +/// Resolves architecture-visible CPU IDs through the VM-owned topology. +pub(crate) trait VmArchCpuIdResolver { + fn vcpu_id_for_arch_cpu_id(&self, arch_cpu_id: usize) -> Option; +} + +impl VmArchCpuIdResolver for crate::AxVM { + fn vcpu_id_for_arch_cpu_id(&self, arch_cpu_id: usize) -> Option { + self.get_vcpu_affinities_pcpu_ids().into_iter().find_map( + |(vcpu_id, _, configured_cpu_id)| (configured_cpu_id == arch_cpu_id).then_some(vcpu_id), + ) + } +} + pub(crate) trait CpuUpOps: ArchOps { fn set_cpu_up_success(vcpu: &crate::vm::AxVCpuRef) { vcpu.set_gpr(0, 0); } fn target_vcpu_id(vm: &crate::AxVMRef, target_cpu: u64) -> Option { - vm.get_vcpu_affinities_pcpu_ids() - .iter() - .find_map(|(vcpu_id, _, phys_id)| (*phys_id == target_cpu as usize).then_some(*vcpu_id)) + usize::try_from(target_cpu) + .ok() + .and_then(|arch_cpu_id| vm.vcpu_id_for_arch_cpu_id(arch_cpu_id)) } } diff --git a/virtualization/axvm/src/architecture/mod.rs b/virtualization/axvm/src/architecture/mod.rs index b6865665b9..7af130ba42 100644 --- a/virtualization/axvm/src/architecture/mod.rs +++ b/virtualization/axvm/src/architecture/mod.rs @@ -1,6 +1,8 @@ //! Architecture-neutral contracts shared by target implementations. pub(crate) mod capabilities; +#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] +pub(crate) mod cpu_up; mod exit; pub(crate) mod ops; mod types; diff --git a/virtualization/axvm/src/architecture/ops.rs b/virtualization/axvm/src/architecture/ops.rs index 53c7de271a..51451adc1b 100644 --- a/virtualization/axvm/src/architecture/ops.rs +++ b/virtualization/axvm/src/architecture/ops.rs @@ -10,33 +10,6 @@ use super::{BoundVcpuExit, VcpuRunAction}; use crate::{AxVmResult, ax_err, irq::model::PendingVcpuInterrupt}; pub(crate) trait ArchOps { - #[cfg(target_arch = "riscv64")] - fn ipi_targets( - vm: &crate::AxVMRef, - current_vcpu_id: usize, - target_cpu: u64, - target_cpu_aux: u64, - send_to_all: bool, - send_to_self: bool, - ) -> crate::CpuMask<64> { - let mut targets = crate::CpuMask::new(); - - if send_to_all { - for vcpu in vm.vcpu_list() { - if vcpu.id() != current_vcpu_id { - targets.set(vcpu.id(), true); - } - } - } else if send_to_self { - targets.set(current_vcpu_id, true); - } else { - let _ = target_cpu_aux; - targets.set(target_cpu as usize, true); - } - - targets - } - type VCpu: VmArchVcpuOps; type PerCpu: VmArchPerCpuOps; type DeferredRunWork; diff --git a/virtualization/axvm/src/irq/sender.rs b/virtualization/axvm/src/irq/sender.rs index 643b3dde8d..ccb419f185 100644 --- a/virtualization/axvm/src/irq/sender.rs +++ b/virtualization/axvm/src/irq/sender.rs @@ -27,9 +27,9 @@ use crate::{AxVM, AxVmResult, ax_err_type, irq::model::PendingVcpuInterrupt}; /// reference. Every [`send`](Self::send) call looks up the current runtime /// through the VM, so a VM stop/start/reset cycle cannot leave the sender /// pointing at a stale dispatcher. -#[expect( - dead_code, - reason = "architecture routers create senders in later modules" +#[cfg_attr( + not(target_arch = "riscv64"), + expect(dead_code, reason = "currently consumed by the RISC-V IPI router") )] #[derive(Clone)] pub struct VmInterruptSender { @@ -38,9 +38,9 @@ pub struct VmInterruptSender { impl VmInterruptSender { /// Constructs a sender from an `AxVMRef` (`Arc`). - #[expect( - dead_code, - reason = "architecture routers create senders in later modules" + #[cfg_attr( + not(target_arch = "riscv64"), + expect(dead_code, reason = "currently consumed by the RISC-V IPI router") )] pub fn new(vm: &Arc) -> Self { Self { @@ -60,9 +60,9 @@ impl VmInterruptSender { /// missing runtime return `BadState`. /// 3. `runtime.dispatch_vcpu_interrupt(vcpu_id, interrupt)` — /// unregistered vCPU task returns `NotFound`. - #[expect( - dead_code, - reason = "architecture interrupt routers call send in later modules" + #[cfg_attr( + not(target_arch = "riscv64"), + expect(dead_code, reason = "currently consumed by the RISC-V IPI router") )] pub fn send(&self, vcpu_id: usize, interrupt: PendingVcpuInterrupt) -> AxVmResult { self.target.send_with( diff --git a/virtualization/axvm/src/vm/mod.rs b/virtualization/axvm/src/vm/mod.rs index 576247b1a2..0b3843b353 100644 --- a/virtualization/axvm/src/vm/mod.rs +++ b/virtualization/axvm/src/vm/mod.rs @@ -326,11 +326,8 @@ impl VmRuntimeHandle { /// The dispatcher releases its queue lock before this method notifies /// waiters or invokes the host IPI boundary. #[cfg_attr( - not(test), - expect( - dead_code, - reason = "architecture interrupt routers dispatch in later modules" - ) + not(target_arch = "riscv64"), + expect(dead_code, reason = "currently consumed by the RISC-V IPI router") )] pub(crate) fn dispatch_vcpu_interrupt( &self, @@ -882,6 +879,10 @@ impl AxVM { f(runtime) } + #[cfg_attr( + not(target_arch = "riscv64"), + expect(dead_code, reason = "currently consumed by the RISC-V IPI router") + )] pub(crate) fn current_interrupt_runtime(&self) -> AxVmResult> { let machine = self.machine.lock(); Ok(machine.interrupt_runtime()?.clone()) diff --git a/virtualization/axvm/tests/arch_boundary_contract.rs b/virtualization/axvm/tests/arch_boundary_contract.rs new file mode 100644 index 0000000000..b96edbed7a --- /dev/null +++ b/virtualization/axvm/tests/arch_boundary_contract.rs @@ -0,0 +1,62 @@ +use std::{env, fs, path::PathBuf}; + +fn source_path(relative: &str) -> PathBuf { + env::var_os("AXVM_SOURCE_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR"))) + .join(relative) +} + +fn read_source(relative: &str) -> String { + let path = source_path(relative); + fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) +} + +fn assert_omits(source: &str, path: &str, forbidden: &[&str]) { + for token in forbidden { + assert!( + !source.contains(token), + "{path} must not own architecture-specific IPI protocol token {token:?}" + ); + } +} + +#[test] +fn riscv_ipi_protocol_stays_out_of_common_architecture_files() { + let architecture_ops = read_source("src/architecture/ops.rs"); + assert_omits( + &architecture_ops, + "src/architecture/ops.rs", + &[ + "target_arch", + "hart_mask", + "ipi_targets", + "SendIpi", + "SendIPI", + ], + ); + + let arch_dispatch = read_source("src/arch/mod.rs"); + assert_omits( + &arch_dispatch, + "src/arch/mod.rs", + &[ + "hart_mask", + "ipi_targets", + "deliver_riscv_ipi", + "SendIpi", + "SendIPI", + "#[cfg(any(target_arch = \"riscv64\", test))]", + ], + ); + + for arch_module in ["src/arch/aarch64/mod.rs", "src/arch/riscv64/mod.rs"] { + let source = read_source(arch_module); + assert_omits( + &source, + arch_module, + &["#[path = \"../../architecture/cpu_up.rs\"]"], + ); + } +} diff --git a/virtualization/riscv_vcpu/src/legacy_ipi.rs b/virtualization/riscv_vcpu/src/legacy_ipi.rs deleted file mode 100644 index f3d50c29e2..0000000000 --- a/virtualization/riscv_vcpu/src/legacy_ipi.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::{RiscvVmExit, consts::traps::irq::S_SOFT}; - -pub(crate) fn decode_legacy_send_ipi_exit( - hart_mask_ptr: usize, - copy_from_guest_va: impl FnMut(usize, &mut [u8]) -> usize, -) -> Result { - let hart_mask = crate::sbi_ipi::read_hart_mask(hart_mask_ptr, copy_from_guest_va)?; - - Ok(RiscvVmExit::SendIPI { - target_cpu: hart_mask as u64, - target_cpu_aux: 0, - send_to_all: false, - send_to_self: false, - vector: S_SOFT as u64, - }) -} - -#[cfg(test)] -mod legacy_ipi_tests { - use super::*; - - #[test] - fn decode_legacy_send_ipi_reads_guest_mask_pointer() { - let guest_mask_addr = 0x4000usize; - let guest_hart_mask = 1usize << 5; - let mask_bytes = guest_hart_mask.to_ne_bytes(); - let mut reads = 0usize; - - let exit = decode_legacy_send_ipi_exit(guest_mask_addr, |guest_va, bytes| { - reads += 1; - assert_eq!(guest_va, guest_mask_addr); - bytes.copy_from_slice(&mask_bytes); - bytes.len() - }) - .unwrap(); - - assert_eq!(reads, 1); - - match exit { - RiscvVmExit::SendIPI { - target_cpu, - target_cpu_aux, - send_to_all, - send_to_self, - vector, - } => { - assert_eq!(target_cpu, guest_hart_mask as u64); - assert_eq!(target_cpu_aux, 0); - assert!(!send_to_all); - assert!(!send_to_self); - assert_eq!(vector, S_SOFT as u64); - } - _ => panic!("legacy SEND_IPI must return SendIPI"), - } - } -} diff --git a/virtualization/riscv_vcpu/src/lib.rs b/virtualization/riscv_vcpu/src/lib.rs index 4e125aedf6..86ea657db1 100644 --- a/virtualization/riscv_vcpu/src/lib.rs +++ b/virtualization/riscv_vcpu/src/lib.rs @@ -26,7 +26,6 @@ mod consts; mod detect; mod guest_mem; pub mod host; -mod legacy_ipi; mod percpu; mod registers; mod regs; @@ -44,8 +43,8 @@ pub use detect::{detect_h_extension as has_hardware_support, max_guest_page_tabl pub use regs::GprIndex; pub use types::{ RiscvAccessFlags, RiscvAccessWidth, RiscvGuestPhysAddr, RiscvGuestVirtAddr, RiscvHostPhysAddr, - RiscvHostVirtAddr, RiscvNestedPagingConfig, RiscvVcpuError, RiscvVcpuId, RiscvVcpuResult, - RiscvVmExit, RiscvVmId, + RiscvHostVirtAddr, RiscvIpiAbi, RiscvIpiCompletion, RiscvIpiRequest, RiscvNestedPagingConfig, + RiscvVcpuError, RiscvVcpuId, RiscvVcpuResult, RiscvVmExit, RiscvVmId, }; pub use self::{ diff --git a/virtualization/riscv_vcpu/src/sbi_ipi.rs b/virtualization/riscv_vcpu/src/sbi_ipi.rs index 8aea66fc03..87a9489605 100644 --- a/virtualization/riscv_vcpu/src/sbi_ipi.rs +++ b/virtualization/riscv_vcpu/src/sbi_ipi.rs @@ -1,37 +1,38 @@ -extern crate alloc; +//! SBI IPI decoding owned by the RISC-V vCPU boundary. -#[cfg(test)] -use alloc::vec::Vec; +use rustsbi::Ipi; +use sbi_spec::binary::{HartMask, SbiRet}; -pub const VSSIP_HVIP_BIT: usize = 2; -#[cfg(test)] -pub const VSTIP_HVIP_BIT: usize = 6; -#[cfg(test)] -pub const VSEIP_HVIP_BIT: usize = 10; +use crate::types::{RiscvIpiAbi, RiscvIpiRequest}; -pub const SUPERVISOR_SOFT_CAUSE: usize = 1; -pub const SUPERVISOR_TIMER_CAUSE: usize = 5; -pub const SUPERVISOR_EXTERNAL_CAUSE: usize = 9; +/// IPI provider used to advertise the SBI extension through BASE probing. +/// +/// Actual delivery is deferred to AxVM because only the VMM owns the guest +/// hart topology and target-vCPU runtime queues. +#[derive(Clone, Copy, Default)] +pub(crate) struct VirtualSbiIpi; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HartMaskReadError { +impl Ipi for VirtualSbiIpi { + fn send_ipi(&self, _hart_mask: HartMask) -> SbiRet { + SbiRet::not_supported() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum HartMaskReadError { ShortRead { expected: usize, copied: usize }, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SupervisorInterruptAction { - ConsumeHostSoft, - InjectGuestTimer, - InjectGuestExternal, +pub(crate) fn decode_standard_request(hart_mask: usize, hart_mask_base: usize) -> RiscvIpiRequest { + RiscvIpiRequest::new(hart_mask, hart_mask_base, RiscvIpiAbi::SbiV02) } -pub fn read_hart_mask( - guest_va: usize, +pub(crate) fn decode_legacy_request( + hart_mask_ptr: usize, mut copy_from_guest_va: impl FnMut(usize, &mut [u8]) -> usize, -) -> Result { +) -> Result { let mut mask_bytes = [0u8; core::mem::size_of::()]; - let copied = copy_from_guest_va(guest_va, &mut mask_bytes); - + let copied = copy_from_guest_va(hart_mask_ptr, &mut mask_bytes); if copied != mask_bytes.len() { return Err(HartMaskReadError::ShortRead { expected: mask_bytes.len(), @@ -39,28 +40,11 @@ pub fn read_hart_mask( }); } - Ok(usize::from_ne_bytes(mask_bytes)) -} - -#[cfg(test)] -pub fn select_targets(hart_mask: usize, vcpu_ids: impl IntoIterator) -> Vec { - vcpu_ids - .into_iter() - .filter(|&vcpu_id| vcpu_id < usize::BITS as usize && ((hart_mask >> vcpu_id) & 1) != 0) - .collect() -} - -pub fn clear_virtual_soft_pending(hvip: usize) -> usize { - hvip & !(1usize << VSSIP_HVIP_BIT) -} - -pub fn classify_supervisor_interrupt(cause: usize) -> Option { - match cause { - SUPERVISOR_SOFT_CAUSE => Some(SupervisorInterruptAction::ConsumeHostSoft), - SUPERVISOR_TIMER_CAUSE => Some(SupervisorInterruptAction::InjectGuestTimer), - SUPERVISOR_EXTERNAL_CAUSE => Some(SupervisorInterruptAction::InjectGuestExternal), - _ => None, - } + Ok(RiscvIpiRequest::new( + usize::from_ne_bytes(mask_bytes), + 0, + RiscvIpiAbi::Legacy, + )) } #[cfg(test)] @@ -68,127 +52,37 @@ mod tests { use super::*; #[test] - fn zero_mask_is_empty_and_not_broadcast() { - assert_eq!(select_targets(0, [0, 1, 2, 3]), Vec::::new()); - } - - #[test] - fn selected_mask_routes_only_selected_harts() { - assert_eq!(select_targets(0b1010, [0, 1, 2, 3]), alloc::vec![1, 3]); - } + fn standard_request_preserves_mask_and_base() { + let request = decode_standard_request(0b1010, 4); - #[test] - fn unreadable_guest_mask_is_rejected() { - let err = read_hart_mask(0x1000, |_guest_va, _bytes| 0).unwrap_err(); - assert_eq!( - err, - HartMaskReadError::ShortRead { - expected: core::mem::size_of::(), - copied: 0, - } - ); + assert_eq!(request.hart_mask(), 0b1010); + assert_eq!(request.hart_mask_base(), 4); + assert_eq!(request.abi(), RiscvIpiAbi::SbiV02); } -} - -#[cfg(test)] -mod contract_tests { - use super::*; #[test] - fn guest_memory_word_routes_only_masked_harts() { - let mask = 0b1010usize; - let mask_bytes = mask.to_ne_bytes(); - - let hart_mask = read_hart_mask(0x4000, |guest_va, bytes| { + fn legacy_request_reads_one_rv64_mask_word() { + let expected = 0b101usize; + let request = decode_legacy_request(0x4000, |guest_va, bytes| { assert_eq!(guest_va, 0x4000); - bytes.copy_from_slice(&mask_bytes); + bytes.copy_from_slice(&expected.to_ne_bytes()); bytes.len() }) .unwrap(); - assert_eq!(select_targets(hart_mask, [0, 1, 2, 3]), alloc::vec![1, 3]); - } - - #[test] - fn mask_bit_one_selects_only_vcpu_one() { - assert_eq!(select_targets(1 << 1, [0, 1, 2, 3]), alloc::vec![1]); - } - - #[test] - fn mask_bits_zero_and_two_select_only_vcpus_zero_and_two() { - assert_eq!( - select_targets((1 << 0) | (1 << 2), [0, 1, 2, 3]), - alloc::vec![0, 2] - ); + assert_eq!(request.hart_mask(), expected); + assert_eq!(request.hart_mask_base(), 0); + assert_eq!(request.abi(), RiscvIpiAbi::Legacy); } #[test] - fn sparse_guest_hart_mask_selects_hart_id_not_local_index() { + fn legacy_request_rejects_short_guest_reads() { assert_eq!( - select_targets(1usize << 5, [4usize, 5usize, 9usize]), - alloc::vec![5] - ); - } - - #[test] - fn clear_ipi_updates_only_current_hart_hvip_snapshot() { - let current_hvip = (1usize << VSSIP_HVIP_BIT) | (1usize << VSTIP_HVIP_BIT); - let remote_hvip = current_hvip; - - let current_after = clear_virtual_soft_pending(current_hvip); - - assert_eq!(current_after & (1usize << VSSIP_HVIP_BIT), 0); - assert_ne!(remote_hvip & (1usize << VSSIP_HVIP_BIT), 0); - } - - #[test] - fn invalid_guest_mask_pointer_rejects_before_routing() { - let err = read_hart_mask(0, |_guest_va, _bytes| 0).unwrap_err(); - - assert_eq!( - err, - HartMaskReadError::ShortRead { + decode_legacy_request(0x4000, |_guest_va, _bytes| 0), + Err(HartMaskReadError::ShortRead { expected: core::mem::size_of::(), copied: 0, - } - ); - } - - #[test] - fn clear_ipi_clears_only_vssip() { - let hvip = 1usize << VSSIP_HVIP_BIT; - assert_eq!(clear_virtual_soft_pending(hvip), 0); - } - - #[test] - fn clear_ipi_preserves_timer_and_external_pending() { - let hvip = - (1usize << VSSIP_HVIP_BIT) | (1usize << VSTIP_HVIP_BIT) | (1usize << VSEIP_HVIP_BIT); - - let cleared = clear_virtual_soft_pending(hvip); - - assert_eq!(cleared & (1usize << VSSIP_HVIP_BIT), 0); - assert_ne!(cleared & (1usize << VSTIP_HVIP_BIT), 0); - assert_ne!(cleared & (1usize << VSEIP_HVIP_BIT), 0); - } - - #[test] - fn host_supervisor_soft_is_consumed_by_host() { - assert_eq!( - classify_supervisor_interrupt(SUPERVISOR_SOFT_CAUSE), - Some(SupervisorInterruptAction::ConsumeHostSoft) - ); - } - - #[test] - fn timer_and_external_are_guest_interrupts() { - assert_eq!( - classify_supervisor_interrupt(SUPERVISOR_TIMER_CAUSE), - Some(SupervisorInterruptAction::InjectGuestTimer) - ); - assert_eq!( - classify_supervisor_interrupt(SUPERVISOR_EXTERNAL_CAUSE), - Some(SupervisorInterruptAction::InjectGuestExternal) + }) ); } } diff --git a/virtualization/riscv_vcpu/src/types.rs b/virtualization/riscv_vcpu/src/types.rs index d36d4534bc..d45e3d9dda 100644 --- a/virtualization/riscv_vcpu/src/types.rs +++ b/virtualization/riscv_vcpu/src/types.rs @@ -174,6 +174,59 @@ pub struct RiscvNestedPagingConfig { pub mode: usize, } +/// SBI calling convention used by an IPI request. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RiscvIpiAbi { + /// Legacy SBI `SEND_IPI` extension. + Legacy, + /// SBI v0.2 or newer IPI extension. + SbiV02, +} + +/// Result of routing an SBI IPI request through the VMM. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum RiscvIpiCompletion { + /// Every selected hart accepted the virtual software interrupt. + Success, + /// At least one selected hart was invalid or unavailable to the guest. + InvalidParameter, + /// Delivery failed after the request had been validated. + Failed, +} + +/// Decoded SBI IPI request forwarded to the VMM. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RiscvIpiRequest { + hart_mask: usize, + hart_mask_base: usize, + abi: RiscvIpiAbi, +} + +impl RiscvIpiRequest { + pub(crate) const fn new(hart_mask: usize, hart_mask_base: usize, abi: RiscvIpiAbi) -> Self { + Self { + hart_mask, + hart_mask_base, + abi, + } + } + + /// Returns the SBI hart mask bits. + pub const fn hart_mask(self) -> usize { + self.hart_mask + } + + /// Returns the SBI hart mask base. + pub const fn hart_mask_base(self) -> usize { + self.hart_mask_base + } + + /// Returns the SBI calling convention that produced this request. + pub const fn abi(self) -> RiscvIpiAbi { + self.abi + } +} + impl RiscvNestedPagingConfig { /// Creates a nested paging configuration. pub const fn new(root_paddr: usize, levels: usize, gpa_bits: usize, mode: usize) -> Self { @@ -230,6 +283,8 @@ pub enum RiscvVmExit { /// Host interrupt vector. vector: u64, }, + /// Guest requested supervisor software interrupts for other harts. + SendIpi(RiscvIpiRequest), /// Guest requested another CPU to start. CpuUp { /// Target vCPU or hart ID. @@ -244,19 +299,6 @@ pub enum RiscvVmExit { /// Guest CPU state value. state: u64, }, - /// Guest requested an IPI. - SendIPI { - /// Target hart mask for legacy SBI IPI. - target_cpu: u64, - /// Auxiliary target selector, unused by RISC-V legacy SBI. - target_cpu_aux: u64, - /// Whether to broadcast to all vCPUs except the sender. - send_to_all: bool, - /// Whether to target the current vCPU. - send_to_self: bool, - /// IPI vector. - vector: u64, - }, /// Guest halted. Halt, /// Guest requested system shutdown. diff --git a/virtualization/riscv_vcpu/src/vcpu.rs b/virtualization/riscv_vcpu/src/vcpu.rs index 9d942f5b52..957117ae57 100644 --- a/virtualization/riscv_vcpu/src/vcpu.rs +++ b/virtualization/riscv_vcpu/src/vcpu.rs @@ -32,8 +32,8 @@ use riscv_h::register::{ vstval, vstvec::{self, Vstvec}, }; -use rustsbi::{Forward, Ipi, RustSBI, SbiRet}; -use sbi_spec::{binary::HartMask, hsm, legacy, pmu, rfnc, srst}; +use rustsbi::{Forward, RustSBI, SbiRet}; +use sbi_spec::{hsm, legacy, pmu, rfnc, spi, srst}; use crate::{ EID_HVC, RiscvVcpuCreateConfig, @@ -45,19 +45,19 @@ use crate::{ sbi_console::*, trap::Exception, types::{ - RiscvAccessFlags, RiscvAccessWidth, RiscvGuestPhysAddr, RiscvGuestVirtAddr, - RiscvNestedPagingConfig, RiscvVcpuError, RiscvVcpuResult, RiscvVmExit, + RiscvAccessFlags, RiscvAccessWidth, RiscvGuestPhysAddr, RiscvGuestVirtAddr, RiscvIpiAbi, + RiscvIpiCompletion, RiscvIpiRequest, RiscvNestedPagingConfig, RiscvVcpuError, + RiscvVcpuResult, RiscvVmExit, }, vpmu::VirtualPmu, }; + unsafe extern "C" { fn _run_guest(state: *mut VmCpuRegisters); } const TINST_PSEUDO_STORE: u32 = 0x3020; const TINST_PSEUDO_LOAD: u32 = 0x3000; -const EID_IPI: usize = 0x0073_5049; -const FID_SEND_IPI: usize = 0; const EID_TIME: usize = 0x5449_4D45; const FID_SET_TIMER: usize = 0; #[cfg(feature = "sstc")] @@ -84,22 +84,12 @@ pub type RiscvVCpu = RiscvVcpu; /// Backward-compatible upper-case vCPU alias. pub type RISCVVCpu = RiscvVcpu; -#[derive(Clone, Copy, Default)] -struct VirtualSbiIpi; - -impl Ipi for VirtualSbiIpi { - fn send_ipi(&self, _hart_mask: HartMask) -> SbiRet { - // Real SEND_IPI is handled by the production EID_IPI path below. - SbiRet::not_supported() - } -} - #[derive(RustSBI)] struct RISCVVCpuSbi { #[rustsbi(pmu)] pmu: VirtualPmu, #[rustsbi(ipi)] - ipi: VirtualSbiIpi, + ipi: crate::sbi_ipi::VirtualSbiIpi, #[rustsbi(console, fence, reset, info, hsm, timer)] forward: Forward, } @@ -122,7 +112,7 @@ impl Default for RISCVVCpuSbi { fn default() -> Self { Self { pmu: VirtualPmu::default(), - ipi: VirtualSbiIpi, + ipi: crate::sbi_ipi::VirtualSbiIpi, forward: Forward, } } @@ -139,23 +129,6 @@ impl Default for RiscvVcpu { } } -fn decode_legacy_send_ipi_exit( - hart_mask_ptr: usize, - copy_from_guest_va: impl FnMut(usize, &mut [u8]) -> usize, -) -> Result { - crate::legacy_ipi::decode_legacy_send_ipi_exit(hart_mask_ptr, copy_from_guest_va) -} - -fn decode_standard_send_ipi_exit(hart_mask: usize, hart_mask_base: usize) -> RiscvVmExit { - RiscvVmExit::SendIPI { - target_cpu: hart_mask as u64, - target_cpu_aux: hart_mask_base as u64, - send_to_all: false, - send_to_self: false, - vector: S_SOFT as u64, - } -} - impl RiscvVcpu { /// Creates a new RISC-V vCPU. pub fn new( @@ -370,30 +343,7 @@ impl RiscvVcpu { /// Injects a virtual interrupt into the guest. pub fn inject_interrupt(&mut self, vector: usize) -> RiscvVcpuResult { - match vector { - S_SOFT => { - self.regs.virtual_hs_csrs.hvip |= 1 << 2; - unsafe { - hvip::set_vssip(); - } - Ok(()) - } - S_TIMER => { - self.regs.virtual_hs_csrs.hvip |= 1 << 6; - unsafe { - hvip::set_vstip(); - } - Ok(()) - } - vector if is_supervisor_external(vector) => { - self.regs.virtual_hs_csrs.hvip |= 1 << 10; - unsafe { - hvip::set_vseip(); - } - Ok(()) - } - _ => Err(RiscvVcpuError::Unsupported), - } + self.set_virtual_interrupt_pending(vector, true) } /// Synchronizes controller-derived VSEIP state for the bound vCPU. @@ -422,6 +372,46 @@ impl RiscvVcpu { pub fn set_return_value(&mut self, val: usize) { self.set_gpr_from_gpr_index(GprIndex::A0, val); } + + /// Completes a previously returned SBI IPI request. + pub fn complete_ipi(&mut self, request: RiscvIpiRequest, completion: RiscvIpiCompletion) { + let result = match completion { + RiscvIpiCompletion::Success => SbiRet::success(0), + RiscvIpiCompletion::InvalidParameter => SbiRet::invalid_param(), + RiscvIpiCompletion::Failed => SbiRet::failed(), + }; + match request.abi() { + RiscvIpiAbi::Legacy => { + self.set_gpr_from_gpr_index(GprIndex::A0, result.error); + } + RiscvIpiAbi::SbiV02 => self.set_sbi_result(result), + } + } + + fn set_virtual_interrupt_pending(&mut self, vector: usize, pending: bool) -> RiscvVcpuResult { + let mut saved = hvip::Hvip::from_bits(self.regs.virtual_hs_csrs.hvip); + match vector { + S_SOFT => saved.set_vssip(pending), + S_TIMER => saved.set_vstip(pending), + vector if is_supervisor_external(vector) => saved.set_vseip(pending), + _ => return Err(RiscvVcpuError::Unsupported), + } + self.regs.virtual_hs_csrs.hvip = saved.bits(); + + if self.bound { + unsafe { + match (vector, pending) { + (S_SOFT, true) => hvip::set_vssip(), + (S_SOFT, false) => hvip::clear_vssip(), + (S_TIMER, true) => hvip::set_vstip(), + (S_TIMER, false) => hvip::clear_vstip(), + (_, true) => hvip::set_vseip(), + (_, false) => hvip::clear_vseip(), + } + } + } + Ok(()) + } } impl RiscvVcpu { @@ -429,8 +419,7 @@ impl RiscvVcpu { /// last `unbind()` so the next `bind()` does not overwrite them with stale /// saved state. pub fn latch_hvip_from_hw(&mut self) { - let hw_hvip = hvip::read().bits(); - self.regs.virtual_hs_csrs.hvip |= hw_hvip; + self.regs.virtual_hs_csrs.hvip |= hvip::read().bits(); } /// Attempts to decode the current guest-page-fault trap as an MMIO access. @@ -449,21 +438,22 @@ impl RiscvVcpu { impl RiscvVcpu { #[inline] - fn program_guest_timer(&mut self, deadline: usize) { + fn program_guest_timer(&mut self, deadline: usize) -> RiscvVcpuResult { #[cfg(feature = "sstc")] { self.regs.vs_csrs.vstimecmp = deadline; } sbi_rt::set_timer(deadline as u64); + self.set_virtual_interrupt_pending(S_TIMER, false)?; unsafe { // The guest has consumed the current VS timer event and programmed // a new deadline, so clear the injected VS timer pending bit and // re-arm HS timer delivery for the next expiration. - hvip::clear_vstip(); #[cfg(feature = "sstc")] vstimecmp::write(deadline); sie::set_stimer(); } + Ok(()) } /// Gets one of the vCPU's general purpose registers. @@ -597,7 +587,7 @@ impl RiscvVcpu { legacy::LEGACY_SET_TIMER => { // info!("set timer: {}", param[0]); self.sbi.pmu.record_set_timer(); - self.program_guest_timer(param[0]); + self.program_guest_timer(param[0])?; self.set_gpr_from_gpr_index(GprIndex::A0, 0); } @@ -618,14 +608,8 @@ impl RiscvVcpu { }); } legacy::LEGACY_CLEAR_IPI => { - self.regs.virtual_hs_csrs.hvip = - crate::sbi_ipi::clear_virtual_soft_pending( - self.regs.virtual_hs_csrs.hvip, - ); - unsafe { - hvip::clear_vssip(); - } - self.set_gpr_from_gpr_index(GprIndex::A0, 0); + self.set_virtual_interrupt_pending(S_SOFT, false)?; + self.set_gpr_from_gpr_index(GprIndex::A0, RET_SUCCESS); } legacy::LEGACY_SHUTDOWN => { // sbi_call_legacy_0(LEGACY_SHUTDOWN) @@ -638,11 +622,12 @@ impl RiscvVcpu { ); } }, - EID_IPI => match function_id { - FID_SEND_IPI => { - let send_ipi = decode_standard_send_ipi_exit(param[0], param[1]); - self.sbi_return(RET_SUCCESS, 0); - return Ok(send_ipi); + spi::EID_SPI => match function_id { + spi::SEND_IPI => { + let request = + crate::sbi_ipi::decode_standard_request(param[0], param[1]); + self.advance_pc(4); + return Ok(RiscvVmExit::SendIpi(request)); } _ => { self.sbi_return(RET_ERR_NOT_SUPPORTED, 0); @@ -652,7 +637,7 @@ impl RiscvVcpu { EID_TIME => match function_id { FID_SET_TIMER => { self.sbi.pmu.record_set_timer(); - self.program_guest_timer(param[0]); + self.program_guest_timer(param[0])?; self.sbi_return(RET_SUCCESS, 0); return Ok(RiscvVmExit::Nothing); } @@ -835,33 +820,22 @@ impl RiscvVcpu { Ok(RiscvVmExit::Nothing) } Trap::Exception(Exception::VirtualInstruction) => self.handle_virtual_instruction(), - Trap::Interrupt(Interrupt::SupervisorSoft) => { - debug_assert_eq!( - crate::sbi_ipi::classify_supervisor_interrupt( - crate::sbi_ipi::SUPERVISOR_SOFT_CAUSE, - ), - Some(crate::sbi_ipi::SupervisorInterruptAction::ConsumeHostSoft), - ); + Trap::Interrupt(Interrupt::SupervisorTimer) => { + // Forward the elapsed timer to VS and stop taking the same HS + // timer interrupt repeatedly until software programs a new one. + self.inject_interrupt(S_TIMER)?; + unsafe { sie::clear_stimer() }; - // Host SSIP must be consumed by the host IRQ path. + Ok(RiscvVmExit::Nothing) + } + Trap::Interrupt(Interrupt::SupervisorSoft) => { + // Host IPIs and scheduler wakeups use SSIP. Route them through + // the host IRQ path so it can acknowledge SSIP before the vCPU + // resumes instead of treating the interrupt as a guest trap. Ok(RiscvVmExit::ExternalInterrupt { vector: S_SOFT as _, }) } - Trap::Interrupt(Interrupt::SupervisorTimer) => { - debug_assert_eq!( - crate::sbi_ipi::classify_supervisor_interrupt( - crate::sbi_ipi::SUPERVISOR_TIMER_CAUSE, - ), - Some(crate::sbi_ipi::SupervisorInterruptAction::InjectGuestTimer), - ); - - unsafe { - sie::clear_stimer(); - } - self.inject_interrupt(S_TIMER)?; - Ok(RiscvVmExit::Nothing) - } Trap::Interrupt(Interrupt::SupervisorExternal) => { // 9 == Interrupt::SupervisorExternal // @@ -897,31 +871,38 @@ impl RiscvVcpu { } } + #[inline] + fn sbi_return(&mut self, a0: usize, a1: usize) { + self.set_sbi_result(SbiRet { + error: a0, + value: a1, + }); + self.advance_pc(4); + } + + #[inline] + fn set_sbi_result(&mut self, result: SbiRet) { + self.set_gpr_from_gpr_index(GprIndex::A0, result.error); + self.set_gpr_from_gpr_index(GprIndex::A1, result.value); + } + fn handle_legacy_send_ipi( &mut self, hart_mask_ptr: usize, copy_from_guest_va: impl FnMut(usize, &mut [u8]) -> usize, ) -> RiscvVcpuResult { - let exit = match decode_legacy_send_ipi_exit(hart_mask_ptr, copy_from_guest_va) { - Ok(exit) => exit, - Err(err) => { - warn!("failed to read legacy SBI IPI hart mask at {hart_mask_ptr:#x}: {err:?}"); - self.set_gpr_from_gpr_index(GprIndex::A0, RET_ERR_FAILED); - self.advance_pc(4); + let request = match crate::sbi_ipi::decode_legacy_request(hart_mask_ptr, copy_from_guest_va) + { + Ok(request) => request, + Err(error) => { + warn!("failed to read legacy SBI IPI hart mask at {hart_mask_ptr:#x}: {error:?}"); + self.sbi_return(RET_ERR_FAILED, 0); return Ok(RiscvVmExit::Nothing); } }; - self.set_gpr_from_gpr_index(GprIndex::A0, RET_SUCCESS); - self.advance_pc(4); - Ok(exit) - } - - #[inline] - fn sbi_return(&mut self, a0: usize, a1: usize) { - self.set_gpr_from_gpr_index(GprIndex::A0, a0); - self.set_gpr_from_gpr_index(GprIndex::A1, a1); self.advance_pc(4); + Ok(RiscvVmExit::SendIpi(request)) } #[cfg(feature = "sstc")] @@ -1003,7 +984,7 @@ impl RiscvVcpu { // We currently emulate that CSR access rather than exposing direct // hardware STCE, so this path must also program the underlying HS // timer instead of only updating saved VS state. - self.program_guest_timer(new_value); + self.program_guest_timer(new_value)?; } self.advance_pc(4); @@ -1245,98 +1226,3 @@ fn sbi_call_legacy_1(eid: usize, arg0: usize) -> usize { } error } - -#[cfg(test)] -mod legacy_ipi_tests { - use super::*; - - struct TestHost; - - impl RiscvHostOps for TestHost { - fn virt_to_phys(vaddr: RiscvHostVirtAddr) -> RiscvHostPhysAddr { - RiscvHostPhysAddr::from_usize(vaddr.as_usize()) - } - } - - #[test] - fn legacy_send_ipi_handler_reads_guest_mask_and_returns_real_exit() { - let mut vcpu = RiscvVcpu::::default(); - let guest_mask_addr = 0x4000usize; - let guest_hart_mask = 1usize << 5; - let mask_bytes = guest_hart_mask.to_ne_bytes(); - let mut reads = 0; - - let exit = vcpu - .handle_legacy_send_ipi(guest_mask_addr, |guest_va, bytes| { - reads += 1; - assert_eq!(guest_va, guest_mask_addr); - bytes.copy_from_slice(&mask_bytes); - bytes.len() - }) - .unwrap(); - - assert_eq!(reads, 1); - assert_eq!(vcpu.get_gpr(GprIndex::A0), RET_SUCCESS); - - match exit { - RiscvVmExit::SendIPI { - target_cpu, - target_cpu_aux, - send_to_all, - send_to_self, - vector, - } => { - assert_eq!(target_cpu, guest_hart_mask as u64); - assert_eq!(target_cpu_aux, 0); - assert!(!send_to_all); - assert!(!send_to_self); - assert_eq!(vector, S_SOFT as u64); - } - _ => panic!("legacy SEND_IPI must return SendIPI"), - } - } -} - -#[cfg(test)] -mod standard_ipi_tests { - use super::*; - - #[test] - fn standard_send_ipi_uses_mask_value_and_non_zero_base() { - let exit = decode_standard_send_ipi_exit(1usize << 1, 4); - - match exit { - RiscvVmExit::SendIPI { - target_cpu, - target_cpu_aux, - send_to_all, - send_to_self, - vector, - } => { - assert_eq!(target_cpu, (1usize << 1) as u64); - assert_eq!(target_cpu_aux, 4); - assert!(!send_to_all); - assert!(!send_to_self); - assert_eq!(vector, S_SOFT as u64); - } - _ => panic!("standard SBI IPI must return SendIPI"), - } - } - - #[test] - fn standard_send_ipi_preserves_all_harts_base() { - let exit = decode_standard_send_ipi_exit(0, usize::MAX); - - match exit { - RiscvVmExit::SendIPI { - target_cpu, - target_cpu_aux, - .. - } => { - assert_eq!(target_cpu, 0); - assert_eq!(target_cpu_aux, usize::MAX as u64); - } - _ => panic!("standard SBI IPI must return SendIPI"), - } - } -} From d48f41e7b3348100d89727d1715829bbf69c876b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Mon, 10 Aug 2026 09:31:29 +0800 Subject: [PATCH 2/4] test(axvm): cover SBI IPI routing failures --- .github/workflows/ci.yml | 9 +- book/design/axvm-riscv-sbi-ipi.md | 19 ++ virtualization/axvm/src/arch/riscv64/ipi.rs | 303 ++++++++++++++++++-- virtualization/riscv_vcpu/src/vcpu.rs | 60 ++++ 4 files changed, 359 insertions(+), 32 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b99ce37df9..d2801774c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -488,7 +488,14 @@ jobs: use_container: false runs_on: '["self-hosted","linux","qcs"]' self_hosted_owner: rcore-os - command: cargo xtask axvisor test qemu --arch riscv64 --test-case smoke + command: | + CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_LINKER=rust-lld \ + CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_RUNNER=qemu-riscv64-static \ + RUSTFLAGS='-C target-feature=+crt-static' \ + cargo test -p riscv_vcpu -p axvm \ + --target riscv64gc-unknown-linux-musl \ + --features axvm/host-test --no-default-features --lib ipi + cargo xtask axvisor test qemu --arch riscv64 --test-case smoke cache_key: "" container_image: base limit_to_owner: "" diff --git a/book/design/axvm-riscv-sbi-ipi.md b/book/design/axvm-riscv-sbi-ipi.md index 8ea8aa9f14..d0a4add3b6 100644 --- a/book/design/axvm-riscv-sbi-ipi.md +++ b/book/design/axvm-riscv-sbi-ipi.md @@ -100,6 +100,25 @@ HVIP 的保存副本由 `riscv_vcpu` 独占;只有当前绑定到硬件的 vCP 失去确定性。QEMU 用例同时检查 `nproc >= 3`、SBI IPI extension 日志和 `/proc/interrupts` 非零 IPI 计数,最终输出唯一标记 `guest smp ipi pass!`。 +最低层行为回归直接编译 RISC-V 生产模块,并通过 RISC-V musl test binary 在 +`qemu-riscv64-static` 中执行。`riscv_vcpu` 用例验证 legacy 与 SBI v0.2 completion +对 A0/A1 的不同写回;AxVM RISC-V router 用例验证广播、零 mask、目标顺序、hart ID +溢出、未映射 hart、重复 vCPU 映射,以及运行时投递失败。参数错误用例同时断言完整 +目标集合校验完成前没有发布任何中断;运行时失败用例断言已经发布的前缀不会被伪装成 +可回滚。对应命令为: + +```bash +CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_LINKER=rust-lld \ + CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_RUNNER=qemu-riscv64-static \ + RUSTFLAGS='-C target-feature=+crt-static' \ + cargo test -p riscv_vcpu -p axvm \ + --target riscv64gc-unknown-linux-musl \ + --features axvm/host-test --no-default-features --lib ipi +``` + +该目标相关行为测试保持在 RISC-V 私有模块和 vCPU 协议模块内部,不通过 `#[path]` +复制生产文件,也不要求公共 `arch/mod.rs` 为 host test 编译 RISC-V 实现。 + 源码边界契约测试禁止在 `architecture/ops.rs` 加入架构 `cfg`,也禁止公共 `arch/mod.rs` 出现 hart-mask、IPI 路由 helper 或测试专用 RISC-V 编译分支。该测试应在旧 PR #1681 实现 上失败,在本设计上通过。 diff --git a/virtualization/axvm/src/arch/riscv64/ipi.rs b/virtualization/axvm/src/arch/riscv64/ipi.rs index 82a843c5db..2c15031236 100644 --- a/virtualization/axvm/src/arch/riscv64/ipi.rs +++ b/virtualization/axvm/src/arch/riscv64/ipi.rs @@ -22,61 +22,91 @@ pub(super) fn handle( vcpu: &AxVCpuRef, request: RiscvIpiRequest, ) -> AxVmResult> { - let completion = match resolve_targets(vm, request) { - Ok(targets) => deliver(vm, &targets), + let sender = VmInterruptSender::new(vm); + let completion = match route_hart_mask( + request.hart_mask(), + request.hart_mask_base(), + || vm.vcpu_list().iter().map(|vcpu| vcpu.id()).collect(), + |hart_id| vm.vcpu_id_for_arch_cpu_id(hart_id), + |target_vcpu_id, interrupt| sender.send(target_vcpu_id, interrupt), + ) { + Ok(()) => RiscvIpiCompletion::Success, Err(error) => { - warn!( - "VM[{}] VCpu[{}] rejected SBI IPI request {:?}: {:?}", - vm.id(), - vcpu.id(), - request, - error - ); - RiscvIpiCompletion::InvalidParameter + match &error { + IpiRouteError::InvalidTarget(source) => warn!( + "VM[{}] VCpu[{}] rejected SBI IPI request {:?}: {:?}", + vm.id(), + vcpu.id(), + request, + source + ), + IpiRouteError::Delivery { + target_vcpu_id, + source, + } => warn!( + "VM[{}] failed to deliver SBI IPI to VCpu[{}]: {:?}", + vm.id(), + target_vcpu_id, + source + ), + } + error.completion() } }; vcpu.get_arch_vcpu().complete_ipi(request, completion); Ok(BoundVcpuExit::Continue) } -fn deliver(vm: &AxVMRef, targets: &[usize]) -> RiscvIpiCompletion { - let sender = VmInterruptSender::new(vm); +fn route_hart_mask( + hart_mask: usize, + hart_mask_base: usize, + all_vcpu_ids: impl FnOnce() -> Vec, + resolve_vcpu_id: impl FnMut(usize) -> Option, + publish: impl FnMut(usize, PendingVcpuInterrupt) -> Result<(), E>, +) -> Result<(), IpiRouteError> { + let targets = resolve_targets(hart_mask, hart_mask_base, all_vcpu_ids, resolve_vcpu_id) + .map_err(IpiRouteError::InvalidTarget)?; + deliver(&targets, publish) +} + +fn deliver( + targets: &[usize], + mut publish: impl FnMut(usize, PendingVcpuInterrupt) -> Result<(), E>, +) -> Result<(), IpiRouteError> { let interrupt = PendingVcpuInterrupt { id: SUPERVISOR_SOFTWARE_INTERRUPT_ID, trigger: InterruptTriggerMode::LevelTriggered, }; for &target_vcpu_id in targets { - if let Err(error) = sender.send(target_vcpu_id, interrupt) { - warn!( - "VM[{}] failed to deliver SBI IPI to VCpu[{}]: {:?}", - vm.id(), - target_vcpu_id, - error - ); - return RiscvIpiCompletion::Failed; - } + publish(target_vcpu_id, interrupt).map_err(|source| IpiRouteError::Delivery { + target_vcpu_id, + source, + })?; } - RiscvIpiCompletion::Success + Ok(()) } -fn resolve_targets(vm: &AxVMRef, request: RiscvIpiRequest) -> Result, IpiTargetError> { - if request.hart_mask_base() == usize::MAX { - return Ok(vm.vcpu_list().iter().map(|vcpu| vcpu.id()).collect()); +fn resolve_targets( + hart_mask: usize, + hart_mask_base: usize, + all_vcpu_ids: impl FnOnce() -> Vec, + mut resolve_vcpu_id: impl FnMut(usize) -> Option, +) -> Result, IpiTargetError> { + if hart_mask_base == usize::MAX { + return Ok(all_vcpu_ids()); } let mut targets = Vec::new(); for bit in 0..usize::BITS { - if request.hart_mask() & (1usize << bit) == 0 { + if hart_mask & (1usize << bit) == 0 { continue; } - let hart_id = request - .hart_mask_base() + let hart_id = hart_mask_base .checked_add(bit as usize) .ok_or(IpiTargetError::HartIdOverflow)?; - let target_vcpu_id = vm - .vcpu_id_for_arch_cpu_id(hart_id) - .ok_or(IpiTargetError::UnavailableHart(hart_id))?; + let target_vcpu_id = + resolve_vcpu_id(hart_id).ok_or(IpiTargetError::UnavailableHart(hart_id))?; if targets.contains(&target_vcpu_id) { return Err(IpiTargetError::DuplicateVcpu(target_vcpu_id)); } @@ -85,9 +115,220 @@ fn resolve_targets(vm: &AxVMRef, request: RiscvIpiRequest) -> Result, Ok(targets) } +#[derive(Debug)] +enum IpiRouteError { + InvalidTarget(IpiTargetError), + Delivery { target_vcpu_id: usize, source: E }, +} + +impl IpiRouteError { + const fn completion(&self) -> RiscvIpiCompletion { + match self { + Self::InvalidTarget(_) => RiscvIpiCompletion::InvalidParameter, + Self::Delivery { .. } => RiscvIpiCompletion::Failed, + } + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum IpiTargetError { HartIdOverflow, UnavailableHart(usize), DuplicateVcpu(usize), } + +#[cfg(all(test, feature = "host-test"))] +mod tests { + use ax_plat::irq::{IrqError, RiscvHvIrqIf}; + + use super::*; + + /// Keeps target userspace tests independent from a live dynamic platform. + struct TestRiscvHvIrqIf; + + #[ax_plat::impl_plat_interface] + impl RiscvHvIrqIf for TestRiscvHvIrqIf { + fn activate_guest_plic_source(_source: u32, _target_cpu: usize) -> Result<(), IrqError> { + Err(IrqError::Unsupported) + } + + fn deactivate_guest_plic_source(_source: u32) -> Result<(), IrqError> { + Err(IrqError::Unsupported) + } + + fn complete_guest_plic_source(_source: u32) -> bool { + false + } + } + + #[test] + fn selected_harts_publish_level_vssip_in_mask_order() { + let mut published = Vec::new(); + + route_hart_mask( + 0b101, + 4, + Vec::new, + |hart_id| match hart_id { + 4 => Some(2), + 6 => Some(0), + _ => None, + }, + |target_vcpu_id, interrupt| { + published.push((target_vcpu_id, interrupt)); + Ok::<_, ()>(()) + }, + ) + .unwrap(); + + let expected_interrupt = PendingVcpuInterrupt { + id: SUPERVISOR_SOFTWARE_INTERRUPT_ID, + trigger: InterruptTriggerMode::LevelTriggered, + }; + assert_eq!( + published, + [(2, expected_interrupt), (0, expected_interrupt)] + ); + } + + #[test] + fn broadcast_publishes_to_every_available_vcpu() { + let mut published = Vec::new(); + + route_hart_mask( + 0, + usize::MAX, + || std::vec![2, 0, 1], + |_| panic!("broadcast must not resolve individual hart IDs"), + |target_vcpu_id, _interrupt| { + published.push(target_vcpu_id); + Ok::<_, ()>(()) + }, + ) + .unwrap(); + + assert_eq!(published, [2, 0, 1]); + } + + #[test] + fn empty_mask_succeeds_without_resolution_or_publication() { + let mut published = Vec::new(); + + route_hart_mask( + 0, + 0, + || panic!("ordinary empty mask must not enumerate all vCPUs"), + |_| panic!("empty mask must not resolve a hart ID"), + |target_vcpu_id, _interrupt| { + published.push(target_vcpu_id); + Ok::<_, ()>(()) + }, + ) + .unwrap(); + + assert!(published.is_empty()); + } + + #[test] + fn unavailable_hart_rejects_the_whole_request_before_publication() { + let mut published = Vec::new(); + + let error = route_hart_mask( + 0b11, + 4, + Vec::new, + |hart_id| (hart_id == 4).then_some(2), + |target_vcpu_id, _interrupt| { + published.push(target_vcpu_id); + Ok::<_, ()>(()) + }, + ) + .unwrap_err(); + + assert!(matches!( + error, + IpiRouteError::InvalidTarget(IpiTargetError::UnavailableHart(5)) + )); + assert_eq!(error.completion(), RiscvIpiCompletion::InvalidParameter); + assert!(published.is_empty()); + } + + #[test] + fn overflowing_hart_id_rejects_the_whole_request_before_publication() { + let mut published = Vec::new(); + + let error = route_hart_mask( + 1 << 2, + usize::MAX - 1, + Vec::new, + |_| Some(0), + |target_vcpu_id, _interrupt| { + published.push(target_vcpu_id); + Ok::<_, ()>(()) + }, + ) + .unwrap_err(); + + assert!(matches!( + error, + IpiRouteError::InvalidTarget(IpiTargetError::HartIdOverflow) + )); + assert_eq!(error.completion(), RiscvIpiCompletion::InvalidParameter); + assert!(published.is_empty()); + } + + #[test] + fn duplicate_vcpu_mapping_rejects_the_whole_request_before_publication() { + let mut published = Vec::new(); + + let error = route_hart_mask( + 0b11, + 4, + Vec::new, + |_| Some(2), + |target_vcpu_id, _interrupt| { + published.push(target_vcpu_id); + Ok::<_, ()>(()) + }, + ) + .unwrap_err(); + + assert!(matches!( + error, + IpiRouteError::InvalidTarget(IpiTargetError::DuplicateVcpu(2)) + )); + assert_eq!(error.completion(), RiscvIpiCompletion::InvalidParameter); + assert!(published.is_empty()); + } + + #[test] + fn delivery_failure_reports_failed_and_keeps_the_published_prefix() { + let mut published = Vec::new(); + + let error = route_hart_mask( + 0b111, + 0, + Vec::new, + |hart_id| Some(hart_id + 4), + |target_vcpu_id, _interrupt| { + published.push(target_vcpu_id); + if target_vcpu_id == 5 { + Err("queue closed") + } else { + Ok(()) + } + }, + ) + .unwrap_err(); + + assert!(matches!( + error, + IpiRouteError::Delivery { + target_vcpu_id: 5, + source: "queue closed", + } + )); + assert_eq!(error.completion(), RiscvIpiCompletion::Failed); + assert_eq!(published, [4, 5]); + } +} diff --git a/virtualization/riscv_vcpu/src/vcpu.rs b/virtualization/riscv_vcpu/src/vcpu.rs index 957117ae57..8cc6d4705b 100644 --- a/virtualization/riscv_vcpu/src/vcpu.rs +++ b/virtualization/riscv_vcpu/src/vcpu.rs @@ -1226,3 +1226,63 @@ fn sbi_call_legacy_1(eid: usize, arg0: usize) -> usize { } error } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{RiscvHostPhysAddr, RiscvHostVirtAddr}; + + struct TestHost; + + impl RiscvHostOps for TestHost { + fn virt_to_phys(_vaddr: RiscvHostVirtAddr) -> RiscvHostPhysAddr { + RiscvHostPhysAddr::from_usize(0) + } + } + + #[test] + fn legacy_ipi_completion_updates_only_a0() { + let mut vcpu = RiscvVcpu::::default(); + let request = RiscvIpiRequest::new(1, 0, RiscvIpiAbi::Legacy); + + for (completion, expected) in [ + (RiscvIpiCompletion::Success, SbiRet::success(0)), + ( + RiscvIpiCompletion::InvalidParameter, + SbiRet::invalid_param(), + ), + (RiscvIpiCompletion::Failed, SbiRet::failed()), + ] { + let preserved_a1 = 0xfeed_face; + vcpu.set_gpr_from_gpr_index(GprIndex::A1, preserved_a1); + + vcpu.complete_ipi(request, completion); + + assert_eq!(vcpu.get_gpr(GprIndex::A0), expected.error); + assert_eq!(vcpu.get_gpr(GprIndex::A1), preserved_a1); + } + } + + #[test] + fn sbi_v02_ipi_completion_updates_a0_and_a1() { + let mut vcpu = RiscvVcpu::::default(); + let request = RiscvIpiRequest::new(1, 0, RiscvIpiAbi::SbiV02); + + for (completion, expected) in [ + (RiscvIpiCompletion::Success, SbiRet::success(0)), + ( + RiscvIpiCompletion::InvalidParameter, + SbiRet::invalid_param(), + ), + (RiscvIpiCompletion::Failed, SbiRet::failed()), + ] { + vcpu.set_gpr_from_gpr_index(GprIndex::A0, usize::MAX); + vcpu.set_gpr_from_gpr_index(GprIndex::A1, usize::MAX); + + vcpu.complete_ipi(request, completion); + + assert_eq!(vcpu.get_gpr(GprIndex::A0), expected.error); + assert_eq!(vcpu.get_gpr(GprIndex::A1), expected.value); + } + } +} From 65e7a3534fbe5f0f836b6847fd375d439361fd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Mon, 10 Aug 2026 09:48:57 +0800 Subject: [PATCH 3/4] feat(axbuild): add qemu-user cross-test command --- .github/workflows/ci.yml | 9 +- book/design/axvm-riscv-sbi-ipi.md | 20 ++- scripts/axbuild/src/context/arch.rs | 7 +- scripts/axbuild/src/lib.rs | 35 +++++ scripts/axbuild/src/support/process.rs | 20 +++ scripts/axbuild/src/test/build/mod.rs | 12 +- scripts/axbuild/src/test/build/rust.rs | 11 +- scripts/axbuild/src/test/build/tests.rs | 2 + scripts/axbuild/src/test/build/wrappers.rs | 20 --- scripts/axbuild/src/test/cross.rs | 167 +++++++++++++++++++++ scripts/axbuild/src/test/mod.rs | 1 + 11 files changed, 251 insertions(+), 53 deletions(-) create mode 100644 scripts/axbuild/src/test/cross.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2801774c4..cdd2924784 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -489,12 +489,9 @@ jobs: runs_on: '["self-hosted","linux","qcs"]' self_hosted_owner: rcore-os command: | - CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_LINKER=rust-lld \ - CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_RUNNER=qemu-riscv64-static \ - RUSTFLAGS='-C target-feature=+crt-static' \ - cargo test -p riscv_vcpu -p axvm \ - --target riscv64gc-unknown-linux-musl \ - --features axvm/host-test --no-default-features --lib ipi + cargo xtask cross-test --arch riscv64 \ + --package riscv_vcpu --package axvm \ + --features axvm/host-test --no-default-features --lib ipi cargo xtask axvisor test qemu --arch riscv64 --test-case smoke cache_key: "" container_image: base diff --git a/book/design/axvm-riscv-sbi-ipi.md b/book/design/axvm-riscv-sbi-ipi.md index d0a4add3b6..0979559461 100644 --- a/book/design/axvm-riscv-sbi-ipi.md +++ b/book/design/axvm-riscv-sbi-ipi.md @@ -101,19 +101,17 @@ HVIP 的保存副本由 `riscv_vcpu` 独占;只有当前绑定到硬件的 vCP `/proc/interrupts` 非零 IPI 计数,最终输出唯一标记 `guest smp ipi pass!`。 最低层行为回归直接编译 RISC-V 生产模块,并通过 RISC-V musl test binary 在 -`qemu-riscv64-static` 中执行。`riscv_vcpu` 用例验证 legacy 与 SBI v0.2 completion -对 A0/A1 的不同写回;AxVM RISC-V router 用例验证广播、零 mask、目标顺序、hart ID -溢出、未映射 hart、重复 vCPU 映射,以及运行时投递失败。参数错误用例同时断言完整 -目标集合校验完成前没有发布任何中断;运行时失败用例断言已经发布的前缀不会被伪装成 -可回滚。对应命令为: +qemu-user 中执行。跨架构 crate test 的 musl target、静态链接、linker 和 qemu-user +runner 统一由 axbuild 的 `cross-test` 命令解析,不把工具链策略展开到 CI workflow。 +`riscv_vcpu` 用例验证 legacy 与 SBI v0.2 completion 对 A0/A1 的不同写回;AxVM +RISC-V router 用例验证广播、零 mask、目标顺序、hart ID 溢出、未映射 hart、重复 +vCPU 映射,以及运行时投递失败。参数错误用例同时断言完整目标集合校验完成前没有发布 +任何中断;运行时失败用例断言已经发布的前缀不会被伪装成可回滚。对应命令为: ```bash -CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_LINKER=rust-lld \ - CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_RUNNER=qemu-riscv64-static \ - RUSTFLAGS='-C target-feature=+crt-static' \ - cargo test -p riscv_vcpu -p axvm \ - --target riscv64gc-unknown-linux-musl \ - --features axvm/host-test --no-default-features --lib ipi +cargo xtask cross-test --arch riscv64 \ + --package riscv_vcpu --package axvm \ + --features axvm/host-test --no-default-features --lib ipi ``` 该目标相关行为测试保持在 RISC-V 私有模块和 vCPU 协议模块内部,不通过 `#[path]` diff --git a/scripts/axbuild/src/context/arch.rs b/scripts/axbuild/src/context/arch.rs index d486b64240..79bbf4e4ad 100644 --- a/scripts/axbuild/src/context/arch.rs +++ b/scripts/axbuild/src/context/arch.rs @@ -8,6 +8,7 @@ use super::{ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct CrossCompileSpec { pub(crate) llvm_target: &'static str, + pub(crate) rust_musl_target: &'static str, pub(crate) cmake_system_processor: &'static str, pub(crate) guest_tool_dir: &'static str, pub(crate) gnu_tool_prefix: &'static str, @@ -29,6 +30,7 @@ const ARCH_SPECS: &[ArchSpec] = &[ default_rootfs_image: "rootfs-aarch64-alpine.img", cross_compile: CrossCompileSpec { llvm_target: "aarch64-linux-musl", + rust_musl_target: "aarch64-unknown-linux-musl", cmake_system_processor: "aarch64", guest_tool_dir: "usr/aarch64-alpine-linux-musl/bin", gnu_tool_prefix: "aarch64-linux-musl", @@ -41,6 +43,7 @@ const ARCH_SPECS: &[ArchSpec] = &[ default_rootfs_image: "rootfs-x86_64-alpine.img", cross_compile: CrossCompileSpec { llvm_target: "x86_64-linux-musl", + rust_musl_target: "x86_64-unknown-linux-musl", cmake_system_processor: "x86_64", guest_tool_dir: "usr/x86_64-alpine-linux-musl/bin", gnu_tool_prefix: "x86_64-linux-musl", @@ -53,6 +56,7 @@ const ARCH_SPECS: &[ArchSpec] = &[ default_rootfs_image: "rootfs-riscv64-alpine.img", cross_compile: CrossCompileSpec { llvm_target: "riscv64-linux-musl", + rust_musl_target: "riscv64gc-unknown-linux-musl", cmake_system_processor: "riscv64", guest_tool_dir: "usr/riscv64-alpine-linux-musl/bin", gnu_tool_prefix: "riscv64-linux-musl", @@ -65,6 +69,7 @@ const ARCH_SPECS: &[ArchSpec] = &[ default_rootfs_image: "rootfs-loongarch64-alpine.img", cross_compile: CrossCompileSpec { llvm_target: "loongarch64-linux-musl", + rust_musl_target: "loongarch64-unknown-linux-musl", cmake_system_processor: "loongarch64", guest_tool_dir: "usr/loongarch64-alpine-linux-musl/bin", gnu_tool_prefix: "loongarch64-linux-musl", @@ -119,7 +124,7 @@ pub(crate) fn cross_compile_spec_for_arch_checked(arch: &str) -> anyhow::Result< .map(|spec| spec.cross_compile) .ok_or_else(|| { anyhow!( - "C-based QEMU test cases are only supported on {SUPPORTED_ARCH_VALUES}, but got \ + "cross-compiled tests are only supported on {SUPPORTED_ARCH_VALUES}, but got \ `{arch}`" ) }) diff --git a/scripts/axbuild/src/lib.rs b/scripts/axbuild/src/lib.rs index 41ad0f7095..ab878d90bb 100644 --- a/scripts/axbuild/src/lib.rs +++ b/scripts/axbuild/src/lib.rs @@ -58,6 +58,8 @@ enum Commands { }, /// Run std tests for the configured workspace package whitelist Test, + /// Run statically linked workspace crate tests through qemu-user + CrossTest(test::cross::CrossTestArgs), /// Run kernel axtest targets through QEMU or a remote board Ktest(ktest::ArgsKtest), /// Run clippy for workspace packages @@ -123,6 +125,7 @@ async fn run_root_cli(cli: Cli) -> anyhow::Result<()> { match cli.command { Commands::AgentReviewBench { command } => agent_review_bench::execute(command).await, Commands::Test => test::std::run_std_test_command(), + Commands::CrossTest(args) => test::cross::run(args), Commands::Ktest(args) => ktest::run(args).await, Commands::Clippy(args) => clippy::run_workspace_clippy_command(&args), Commands::SyncLint(args) => sync_lint::run_sync_lint_command(&args), @@ -343,4 +346,36 @@ mod tests { _ => panic!("expected ktest command"), } } + + #[test] + fn command_parses_cross_test() { + let cli = TestCli::try_parse_from([ + "xtask", + "cross-test", + "--arch", + "riscv64", + "--package", + "riscv_vcpu", + "--package", + "axvm", + "--features", + "axvm/host-test", + "--no-default-features", + "--lib", + "ipi", + ]) + .unwrap(); + + match cli.command { + Commands::CrossTest(args) => { + assert_eq!(args.arch, "riscv64"); + assert_eq!(args.packages, ["riscv_vcpu", "axvm"]); + assert_eq!(args.features, ["axvm/host-test"]); + assert!(args.no_default_features); + assert!(args.lib); + assert_eq!(args.name_filter.as_deref(), Some("ipi")); + } + _ => panic!("expected cross-test command"), + } + } } diff --git a/scripts/axbuild/src/support/process.rs b/scripts/axbuild/src/support/process.rs index 5ab0a33c5e..dc7ffe1b7c 100644 --- a/scripts/axbuild/src/support/process.rs +++ b/scripts/axbuild/src/support/process.rs @@ -36,6 +36,26 @@ pub(crate) fn run_cargo_status_with_env( Ok(status.success()) } +pub(crate) fn find_host_binary_candidates(candidates: &[&str]) -> Result { + candidates + .iter() + .find_map(|candidate| find_optional_host_binary(candidate)) + .ok_or_else(|| { + anyhow::anyhow!( + "required host binary was not found in PATH; tried: {}", + candidates.join(", ") + ) + }) +} + +fn find_optional_host_binary(name: &str) -> Option { + std::env::var_os("PATH").and_then(|path_var| { + std::env::split_paths(&path_var) + .map(|dir| dir.join(name)) + .find(|candidate| candidate.is_file()) + }) +} + impl ProcessExt for Command { fn exec(&mut self) -> Result<()> { print_command(self)?; diff --git a/scripts/axbuild/src/test/build/mod.rs b/scripts/axbuild/src/test/build/mod.rs index 878566b5e8..5deabf375e 100644 --- a/scripts/axbuild/src/test/build/mod.rs +++ b/scripts/axbuild/src/test/build/mod.rs @@ -14,14 +14,17 @@ use std::{ time::Duration, }; -use anyhow::{Context, bail, ensure}; +use anyhow::{Context, ensure}; use super::{ case as case_assets, case::{CaseAssetConfig, TestQemuCase, TestQemuSubcase, TestQemuSubcaseKind}, timing, }; -use crate::{context::CrossCompileSpec, support::process::ProcessExt}; +use crate::{ + context::CrossCompileSpec, + support::process::{ProcessExt, find_host_binary_candidates}, +}; const CASE_C_DIR_NAME: &str = "c"; const CASE_PREBUILD_SCRIPT_NAME: &str = "prebuild.sh"; @@ -76,9 +79,8 @@ pub(crate) use rust::{ }; use toolchain::{cross_compile_spec, write_cmake_toolchain_file, write_cross_bin_wrappers}; use wrappers::{ - apply_case_script_envs, case_script_envs, ensure_guest_tool_exists, - find_host_binary_candidates, guest_library_path, qemu_user_binary_names, - write_guest_command_wrappers, write_guest_exec_wrapper, + apply_case_script_envs, case_script_envs, ensure_guest_tool_exists, guest_library_path, + qemu_user_binary_names, write_guest_command_wrappers, write_guest_exec_wrapper, }; #[cfg(test)] diff --git a/scripts/axbuild/src/test/build/rust.rs b/scripts/axbuild/src/test/build/rust.rs index da779e5bb3..5f886ff2ad 100644 --- a/scripts/axbuild/src/test/build/rust.rs +++ b/scripts/axbuild/src/test/build/rust.rs @@ -6,16 +6,7 @@ pub(crate) fn case_rust_source_dir(case: &TestQemuCase) -> PathBuf { /// Maps a StarryOS arch name to the corresponding Rust musl target triple. pub(super) fn rust_musl_target(arch: &str) -> anyhow::Result<&'static str> { - match arch { - "aarch64" => Ok("aarch64-unknown-linux-musl"), - "riscv64" => Ok("riscv64gc-unknown-linux-musl"), - "x86_64" => Ok("x86_64-unknown-linux-musl"), - "loongarch64" => Ok("loongarch64-unknown-linux-musl"), - _ => bail!( - "Rust-based QEMU test cases are only supported on aarch64, riscv64, x86_64, and \ - loongarch64, but got `{arch}`" - ), - } + Ok(cross_compile_spec(arch)?.rust_musl_target) } fn rust_case_rustflags(arch: &str) -> &'static str { diff --git a/scripts/axbuild/src/test/build/tests.rs b/scripts/axbuild/src/test/build/tests.rs index fd63f64005..3cf02f35e0 100644 --- a/scripts/axbuild/src/test/build/tests.rs +++ b/scripts/axbuild/src/test/build/tests.rs @@ -454,6 +454,7 @@ fn cross_compile_spec_maps_supported_arches() { cross_compile_spec("aarch64").unwrap(), CrossCompileSpec { llvm_target: "aarch64-linux-musl", + rust_musl_target: "aarch64-unknown-linux-musl", cmake_system_processor: "aarch64", guest_tool_dir: "usr/aarch64-alpine-linux-musl/bin", gnu_tool_prefix: "aarch64-linux-musl", @@ -464,6 +465,7 @@ fn cross_compile_spec_maps_supported_arches() { cross_compile_spec("loongarch64").unwrap(), CrossCompileSpec { llvm_target: "loongarch64-linux-musl", + rust_musl_target: "loongarch64-unknown-linux-musl", cmake_system_processor: "loongarch64", guest_tool_dir: "usr/loongarch64-alpine-linux-musl/bin", gnu_tool_prefix: "loongarch64-linux-musl", diff --git a/scripts/axbuild/src/test/build/wrappers.rs b/scripts/axbuild/src/test/build/wrappers.rs index ac17c3b614..d2a5c3c86d 100644 --- a/scripts/axbuild/src/test/build/wrappers.rs +++ b/scripts/axbuild/src/test/build/wrappers.rs @@ -198,26 +198,6 @@ pub(super) fn write_wrapper_script(path: &Path, body: &str) -> anyhow::Result<() Ok(()) } -pub(super) fn find_host_binary_candidates(candidates: &[&str]) -> anyhow::Result { - candidates - .iter() - .find_map(|candidate| find_optional_host_binary(candidate)) - .ok_or_else(|| { - anyhow::anyhow!( - "required host binary was not found in PATH; tried: {}", - candidates.join(", ") - ) - }) -} - -pub(super) fn find_optional_host_binary(name: &str) -> Option { - std::env::var_os("PATH").and_then(|path_var| { - std::env::split_paths(&path_var) - .map(|dir| dir.join(name)) - .find(|candidate| candidate.is_file()) - }) -} - pub(super) fn shell_single_quote(path: impl AsRef) -> String { let value = path.as_ref().display().to_string().replace('\'', "'\\''"); format!("'{value}'") diff --git a/scripts/axbuild/src/test/cross.rs b/scripts/axbuild/src/test/cross.rs new file mode 100644 index 0000000000..63ed0af011 --- /dev/null +++ b/scripts/axbuild/src/test/cross.rs @@ -0,0 +1,167 @@ +use std::{ffi::OsString, path::Path, process::Command}; + +use anyhow::Context; +use clap::Args; + +use crate::{ + context::cross_compile_spec_for_arch_checked, + support::process::{ProcessExt, find_host_binary_candidates}, +}; + +const STATIC_RUSTFLAGS: &str = "-C target-feature=+crt-static"; + +#[derive(Args, Clone, Debug, Eq, PartialEq)] +pub(crate) struct CrossTestArgs { + /// Target architecture used to select the Rust musl target and qemu-user runner + #[arg(long)] + pub(crate) arch: String, + + /// Workspace package to test; may be repeated + #[arg(short = 'p', long = "package", required = true)] + pub(crate) packages: Vec, + + /// Cargo features, separated by commas + #[arg(long, value_delimiter = ',')] + pub(crate) features: Vec, + + /// Disable package default features + #[arg(long)] + pub(crate) no_default_features: bool, + + /// Test only the package library + #[arg(long)] + pub(crate) lib: bool, + + /// Optional cargo test name filter + pub(crate) name_filter: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +struct CrossTestPlan { + cargo_args: Vec, + envs: Vec<(String, OsString)>, +} + +impl CrossTestPlan { + fn new(args: &CrossTestArgs, rust_target: &str, linker: &Path, qemu_runner: &Path) -> Self { + let mut cargo_args = vec!["test".to_string()]; + for package in &args.packages { + cargo_args.extend(["--package".to_string(), package.clone()]); + } + cargo_args.extend(["--target".to_string(), rust_target.to_string()]); + if !args.features.is_empty() { + cargo_args.extend(["--features".to_string(), args.features.join(",")]); + } + if args.no_default_features { + cargo_args.push("--no-default-features".to_string()); + } + if args.lib { + cargo_args.push("--lib".to_string()); + } + if let Some(name_filter) = &args.name_filter { + cargo_args.push(name_filter.clone()); + } + + let target_env = rust_target.to_uppercase().replace('-', "_"); + let envs = vec![ + ( + format!("CARGO_TARGET_{target_env}_LINKER"), + linker.as_os_str().to_os_string(), + ), + ( + format!("CARGO_TARGET_{target_env}_RUNNER"), + qemu_runner.as_os_str().to_os_string(), + ), + ("RUSTFLAGS".to_string(), OsString::from(STATIC_RUSTFLAGS)), + ]; + + Self { cargo_args, envs } + } +} + +pub(crate) fn run(args: CrossTestArgs) -> anyhow::Result<()> { + let spec = cross_compile_spec_for_arch_checked(&args.arch)?; + let qemu_runner = find_host_binary_candidates(spec.qemu_user_binaries)?; + let linker = find_host_binary_candidates(&["rust-lld"])?; + + install_rust_target(spec.rust_musl_target)?; + let plan = CrossTestPlan::new(&args, spec.rust_musl_target, &linker, &qemu_runner); + + let mut command = Command::new("cargo"); + command.args(&plan.cargo_args).envs( + plan.envs + .iter() + .map(|(key, value)| (key.as_str(), value.as_os_str())), + ); + command.exec().with_context(|| { + format!( + "failed to run workspace crate tests for `{}` through {}", + args.arch, + qemu_runner.display() + ) + }) +} + +fn install_rust_target(target: &str) -> anyhow::Result<()> { + let mut command = Command::new("rustup"); + command.args(["target", "add", target]); + command + .exec() + .with_context(|| format!("failed to install Rust target `{target}` via rustup")) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn riscv64_plan_owns_cross_execution_details() { + let args = CrossTestArgs { + arch: "riscv64".to_string(), + packages: vec!["riscv_vcpu".to_string(), "axvm".to_string()], + features: vec!["axvm/host-test".to_string()], + no_default_features: true, + lib: true, + name_filter: Some("ipi".to_string()), + }; + + let plan = CrossTestPlan::new( + &args, + "riscv64gc-unknown-linux-musl", + Path::new("/toolchain/bin/rust-lld"), + Path::new("/usr/bin/qemu-riscv64-static"), + ); + + assert_eq!( + plan.cargo_args, + [ + "test", + "--package", + "riscv_vcpu", + "--package", + "axvm", + "--target", + "riscv64gc-unknown-linux-musl", + "--features", + "axvm/host-test", + "--no-default-features", + "--lib", + "ipi", + ] + ); + assert_eq!( + plan.envs, + [ + ( + "CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_LINKER".to_string(), + OsString::from("/toolchain/bin/rust-lld"), + ), + ( + "CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_RUNNER".to_string(), + OsString::from("/usr/bin/qemu-riscv64-static"), + ), + ("RUSTFLAGS".to_string(), OsString::from(STATIC_RUSTFLAGS)), + ] + ); + } +} diff --git a/scripts/axbuild/src/test/mod.rs b/scripts/axbuild/src/test/mod.rs index a9351db713..0bfa46bb4e 100644 --- a/scripts/axbuild/src/test/mod.rs +++ b/scripts/axbuild/src/test/mod.rs @@ -1,6 +1,7 @@ pub(crate) mod board; pub(crate) mod build; pub(crate) mod case; +pub(crate) mod cross; pub(crate) mod host_http; pub(crate) mod qemu; pub(crate) mod std; From 793668c32fcf069d357d439dcfeaf3196f65b6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=91=A8=E7=9D=BF?= Date: Mon, 10 Aug 2026 09:57:49 +0800 Subject: [PATCH 4/4] test(axvisor): remove redundant RISC-V single-core config --- book/design/axvm-riscv-sbi-ipi.md | 6 +- .../configs/vms/qemu/riscv64/linux-smp1.dts | 132 ------------------ .../configs/vms/qemu/riscv64/linux-smp1.toml | 45 ------ 3 files changed, 3 insertions(+), 180 deletions(-) delete mode 100644 os/axvisor/configs/vms/qemu/riscv64/linux-smp1.dts delete mode 100644 os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml diff --git a/book/design/axvm-riscv-sbi-ipi.md b/book/design/axvm-riscv-sbi-ipi.md index 0979559461..64b2dd9d18 100644 --- a/book/design/axvm-riscv-sbi-ipi.md +++ b/book/design/axvm-riscv-sbi-ipi.md @@ -95,9 +95,9 @@ HVIP 的保存副本由 `riscv_vcpu` 独占;只有当前绑定到硬件的 vCP ## 验证与回滚 -回归使用独立的 `linux-smp3-ipi.toml`,不改变 `linux-smp1.toml` 的单核语义。该配置为三核 -新内核提供 128 MiB guest RAM,避免 64 MiB 配置在初始化驱动前只剩约 15 MiB 可用内存而 -失去确定性。QEMU 用例同时检查 `nproc >= 3`、SBI IPI extension 日志和 +回归只保留 `linux-smp3-ipi.toml`,删除不再提供额外覆盖的 RISC-V QEMU 单核配置。三核 +配置为新内核提供 128 MiB guest RAM,避免 64 MiB 配置在初始化驱动前只剩约 15 MiB +可用内存而失去确定性。QEMU 用例同时检查 `nproc >= 3`、SBI IPI extension 日志和 `/proc/interrupts` 非零 IPI 计数,最终输出唯一标记 `guest smp ipi pass!`。 最低层行为回归直接编译 RISC-V 生产模块,并通过 RISC-V musl test binary 在 diff --git a/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.dts b/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.dts deleted file mode 100644 index 0c65bab80e..0000000000 --- a/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.dts +++ /dev/null @@ -1,132 +0,0 @@ -/dts-v1/; - -/ { - #address-cells = <0x02>; - #size-cells = <0x02>; - compatible = "riscv-virtio"; - model = "riscv-virtio,qemu"; - - memory@90000000 { - device_type = "memory"; - reg = <0x00 0x90000000 0x00 0x40000000>; - }; - - cpus { - #address-cells = <0x01>; - #size-cells = <0x00>; - timebase-frequency = <0x989680>; - - cpu@0 { - phandle = <0x07>; - device_type = "cpu"; - reg = <0x00>; - status = "okay"; - compatible = "riscv"; - riscv,cbop-block-size = <0x40>; - riscv,cboz-block-size = <0x40>; - riscv,cbom-block-size = <0x40>; - riscv,isa-extensions = "i\0m\0a\0f\0d\0c"; - riscv,isa-base = "rv64i"; - riscv,isa = "rv64imafdc"; - mmu-type = "riscv,sv39"; - - interrupt-controller { - #interrupt-cells = <0x01>; - interrupt-controller; - compatible = "riscv,cpu-intc"; - phandle = <0x08>; - }; - }; - }; - - aliases { - serial0 = "/soc/serial@10000000"; - }; - - chosen { - bootargs = "earlycon=sbi console=ttyS0,115200 init=/bin/sh root=/dev/vda rw"; - stdout-path = "/soc/serial@10000000"; - }; - - soc { - #address-cells = <0x02>; - #size-cells = <0x02>; - compatible = "simple-bus"; - ranges; - - serial@10000000 { - interrupts = <0x0a>; - interrupt-parent = <0x09>; - clock-frequency = "\08@"; - reg = <0x00 0x10000000 0x00 0x100>; - compatible = "ns16550a"; - }; - - plic@c000000 { - phandle = <0x09>; - riscv,ndev = <0x5f>; - reg = <0x00 0xc000000 0x00 0x600000>; - interrupts-extended = <0x08 0x0b 0x08 0x09>; - interrupt-controller; - compatible = "sifive,plic-1.0.0\0riscv,plic0"; - #address-cells = <0x00>; - #interrupt-cells = <0x01>; - }; - - virtio_mmio@10008000 { - interrupts = <0x08>; - interrupt-parent = <0x09>; - reg = <0x00 0x10008000 0x00 0x1000>; - compatible = "virtio,mmio"; - }; - - virtio_mmio@10007000 { - interrupts = <0x07>; - interrupt-parent = <0x09>; - reg = <0x00 0x10007000 0x00 0x1000>; - compatible = "virtio,mmio"; - }; - - virtio_mmio@10006000 { - interrupts = <0x06>; - interrupt-parent = <0x09>; - reg = <0x00 0x10006000 0x00 0x1000>; - compatible = "virtio,mmio"; - }; - - virtio_mmio@10005000 { - interrupts = <0x05>; - interrupt-parent = <0x09>; - reg = <0x00 0x10005000 0x00 0x1000>; - compatible = "virtio,mmio"; - }; - - virtio_mmio@10004000 { - interrupts = <0x04>; - interrupt-parent = <0x09>; - reg = <0x00 0x10004000 0x00 0x1000>; - compatible = "virtio,mmio"; - }; - - virtio_mmio@10003000 { - interrupts = <0x03>; - interrupt-parent = <0x09>; - reg = <0x00 0x10003000 0x00 0x1000>; - compatible = "virtio,mmio"; - }; - - virtio_mmio@10002000 { - interrupts = <0x02>; - interrupt-parent = <0x09>; - reg = <0x00 0x10002000 0x00 0x1000>; - compatible = "virtio,mmio"; - }; - - virtio_mmio@10001000 { - interrupts = <0x01>; - interrupt-parent = <0x09>; - reg = <0x00 0x10001000 0x00 0x1000>; - compatible = "virtio,mmio"; - }; - }; -}; diff --git a/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml b/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml deleted file mode 100644 index fceaf2816a..0000000000 --- a/os/axvisor/configs/vms/qemu/riscv64/linux-smp1.toml +++ /dev/null @@ -1,45 +0,0 @@ -# Vm base info configs -# -[base] -# Guest vm id. -id = 1 -# Guest vm name. -name = "linux-qemu" -# Virtualization type. -guest_type = "passthrough" -# The number of virtual CPUs. -cpu_num = 1 -# Guest vm physical cpu sets. -phys_cpu_ids = [0] -# -# Vm kernel configs -# -[kernel] -# The entry point of the kernel image. -entry_point = 0x9020_0000 -# The location of image: "memory" | "fs". -# Load from file system. -image_location = "fs" -# The file path of the kernel image. -kernel_path = "/guest/linux/linux-qemu" -# The load address of the kernel image. -kernel_load_addr = 0x9020_0000 -# The file path of the device tree blob (DTB). -# dtb_path = "/path/tmp/configs/linux-riscv64-qemu-smp1.dtb" -# The load address of the device tree blob (DTB). -dtb_load_addr = 0x9300_0000 - -# Memory regions with format (`base_paddr`, `size`, `flags`, `map_type`). -# For `map_type`, 0 means `MAP_ALLOC`, 1 means `MAP_IDENTICAL`, 2 means `MAP_RESERVED`. -memory_regions = [ - [0x9000_0000, 0x0400_0000, 0x7, 1], # System RAM 64M MAP_IDENTICAL for DMA-capable passthrough -] - -# -# Device specifications -# - -# Physical-device selection. Virtual platform devices are machine-owned. -[devices] -passthrough = [] -disabled = []