From 93e0ffa5b941cc564d2816c0ec2561cf315e96a3 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Wed, 11 Nov 2026 11:52:13 -0800 Subject: [PATCH] feat(aarch64): implement the Linux MSHV backend MSHV provides partition creation, memory mappings, MMIO dispatch, batched register access and replacement-VM reset on ARM64. Native CI uses tagged hierarchical-virtualization runners and bounded scheduling for the partition limit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- .config/nextest.toml | 7 + .github/workflows/ValidatePullRequest.yml | 8 + .github/workflows/dep_build_test.yml | 47 +- CHANGELOG.md | 1 + Justfile | 10 +- README.md | 2 +- .../src/hypervisor/hyperlight_vm/aarch64.rs | 5 +- .../virtual_machine/mshv/aarch64.rs | 477 +++++++++++++++++- src/hyperlight_host/src/mem/memory_region.rs | 12 +- .../src/sandbox/snapshot/file/config.rs | 7 +- src/hyperlight_host/tests/integration_test.rs | 15 +- .../tests/sandbox_host_tests.rs | 23 +- typos.toml | 4 + 13 files changed, 571 insertions(+), 47 deletions(-) create mode 100644 .config/nextest.toml diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 000000000..38ad68b40 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,7 @@ +[profile.arm64-mshv] +test-threads = 4 + +# Internally parallel tests run alone. VM replacement can double their partition count. +[[profile.arm64-mshv.overrides]] +filter = 'test(/thread|parallel|::create_[0-9]+_/) | (binary(integration_test) & test(/^interrupt_/))' +threads-required = "num-test-threads" diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 235fe4779..9ee4cc683 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -193,6 +193,14 @@ jobs: cpu_vendor: arm arch: arm64 config: release + - hypervisor: mshv3 + cpu_vendor: arm + arch: arm64 + config: debug + - hypervisor: mshv3 + cpu_vendor: arm + arch: arm64 + config: release uses: ./.github/workflows/dep_build_test.yml secrets: inherit with: diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index a745a9c78..3df6f79de 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -33,11 +33,8 @@ on: default: "" full_aarch64: description: >- - When "true", also run the single-driver tests on aarch64. PRs leave - this "false" so the aarch64 runner builds and runs the default Rust - tests; the daily schedule (DailyArm64.yml) sets it "true". This - input only affects aarch64: it does not change X64, where those tests - already run on Linux and are skipped on Windows regardless. + When "true", also run the single-driver tests on Linux aarch64. + ARM64 MSHV always runs its single-driver suite once. required: false type: string default: "false" @@ -62,7 +59,7 @@ defaults: jobs: build-and-test: if: ${{ inputs.docs_only == 'false' }} - timeout-minutes: ${{ fromJSON(vars.BUILD_TEST_JOB_TIMEOUT || '45') }} + timeout-minutes: ${{ fromJSON(vars.BUILD_TEST_JOB_TIMEOUT || (inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3' && '90' || '45')) }} runs-on: ${{ fromJson(inputs.arch == 'X64' && format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}", "JobId=build-and-test-{3}-{4}-{5}-{6}"]', inputs.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux', @@ -78,6 +75,12 @@ jobs: github.run_id, github.run_number, github.run_attempt) + || inputs.hypervisor == 'mshv3' + && format('["self-hosted", "Linux", "ARM64", "1ES.Pool=hld-azlinux3-mshv-arm", "JobId=build-and-test-{0}-{1}-{2}-{3}"]', + inputs.config, + github.run_id, + github.run_number, + github.run_attempt) || format('["self-hosted", "{0}", "arm64", "{1}"]', inputs.hypervisor == 'hvf' && 'macos' || 'Linux', inputs.hypervisor)) }} @@ -158,16 +161,25 @@ jobs: - name: Build run: just build ${{ inputs.config }} - - name: Run Rust tests + - name: Limit ARM64 MSHV test concurrency + if: inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3' run: | - # with default features - just test ${{ inputs.config }} + echo "RUST_TEST_THREADS=1" >> "$GITHUB_ENV" + echo "NEXTEST_PROFILE=arm64-mshv" >> "$GITHUB_ENV" + + - name: Install ARM64 MSHV test runner + if: inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3' + uses: taiki-e/install-action@682ce6c3c696a4cd5168a05f98e51aa02b877941 # v2.89.1 + with: + tool: cargo-nextest@0.9.151 + + - name: Run Rust tests + # ARM64 MSHV selects its driver here to avoid a duplicate full run. + run: just test ${{ inputs.config }} ${{ inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3' && 'mshv3 "nextest run"' || '' }} - name: Run Rust tests with single driver - # Linux-only. Skipped on aarch64 PRs to conserve the limited runners; - # the daily schedule (full_aarch64 == 'true') restores it. On X64 Linux - # it always runs. - if: runner.os == 'Linux' && (inputs.arch == 'X64' || inputs.full_aarch64 == 'true') + # ARM64 MSHV runs this suite in the preceding step. + if: runner.os == 'Linux' && (inputs.arch == 'X64' || (inputs.full_aarch64 == 'true' && inputs.hypervisor != 'mshv3')) run: | # with only one driver enabled (kvm/mshv3 features are unix-only, no-op on Windows) just test ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv3' && 'mshv3' || 'kvm' }} @@ -234,14 +246,15 @@ jobs: if: inputs.arch == 'X64' run: just test-csharp-bindings ${{ inputs.config }} + # Snapshot goldens are not published for ARM64 WHP or MSHV. - name: Install oras - if: runner.os != 'Windows' || inputs.arch != 'arm64' + if: inputs.arch != 'arm64' || (runner.os != 'Windows' && inputs.hypervisor != 'mshv3') uses: oras-project/setup-oras@5e2f2482b30b0cb3e14e67f89a80c88c659a7c63 # v2.0.3 with: version: 1.3.1 - name: Log in to GHCR - if: runner.os != 'Windows' || inputs.arch != 'arm64' + if: inputs.arch != 'arm64' || (runner.os != 'Windows' && inputs.hypervisor != 'mshv3') env: GHCR_USER: ${{ github.actor }} GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -250,13 +263,13 @@ jobs: echo "${GHCR_TOKEN}" | oras login ghcr.io -u "${GHCR_USER}" --password-stdin - name: Snapshot goldens (pull and verify) - if: ${{ inputs.should_regen_goldens != 'true' && (runner.os != 'Windows' || inputs.arch != 'arm64') }} + if: ${{ inputs.should_regen_goldens != 'true' && (inputs.arch != 'arm64' || (runner.os != 'Windows' && inputs.hypervisor != 'mshv3')) }} run: | just snapshot-goldens-pull ${{ inputs.config }} just snapshot-goldens-verify ${{ inputs.config }} - name: Snapshot goldens (regenerate and verify) - if: ${{ inputs.should_regen_goldens == 'true' && (runner.os != 'Windows' || inputs.arch != 'arm64') }} + if: ${{ inputs.should_regen_goldens == 'true' && (inputs.arch != 'arm64' || (runner.os != 'Windows' && inputs.hypervisor != 'mshv3')) }} run: | # regenerate goldens from current branch just snapshot-goldens-regenerate ${{ inputs.config }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 95fecc54f..1f254b3c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added * Add `Sandbox::recreate()`, which rebuilds a sandbox's VM and memory from a snapshot. It recovers unrecoverable sandboxes and keeps configuration, host functions, and interrupt handles by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1751 +* ARM64 Linux guests can run on MSHV. ### Fixed * Fix restore on WHP not rewinding the guest TSC on hosts with `TSC_ADJUST` support. diff --git a/Justfile b/Justfile index 528fc4df2..1916792f2 100644 --- a/Justfile +++ b/Justfile @@ -247,7 +247,7 @@ like-ci config=default-target hypervisor="kvm": just check-license-headers # runs all tests -test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration target features) (test-doc target features) (test-tools target) +test target=default-target features="" runner="test": (test-unit target features runner) (test-isolated target features) (test-integration target features runner) (test-doc target features) (test-tools target) # Cargo launches test binaries through the runner in this variable, for example # CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER. @@ -269,8 +269,8 @@ test-tools target=default-target: {{ cargo-cmd }} test -p hyperlight-ci --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} # runs unit tests -test-unit target=default-target features="": - {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --lib +test-unit target=default-target features="" runner="test": + {{ cargo-cmd }} {{ runner }} {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} {{ if runner == "test" { "--profile=" } else { "--cargo-profile=" } }}{{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --lib # runs loom concurrency tests test-loom: @@ -296,7 +296,7 @@ test-isolated target=default-target features="" : {{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact # runs integration tests -test-integration target=default-target features="": +test-integration target=default-target features="" runner="test": @# run execute_on_heap test with feature "executable_heap" on (runs with off during normal tests) {{ cargo-cmd }} test {{ if features =="" {"--features executable_heap"} else if features=="no-default-features" {"--no-default-features --features executable_heap"} else {"--no-default-features -F executable_heap," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap @@ -308,7 +308,7 @@ test-integration target=default-target features="": @# the glob but no-ops without a mode token, so it needs no golden @# cache here. It verifies in its own step (see the snapshot-goldens @# recipes). - {{ cargo-cmd }} test -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*' + {{ cargo-cmd }} {{ runner }} -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} {{ if runner == "test" { "--profile=" } else { "--cargo-profile=" } }}{{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*' # tests compilation with no default features on different platforms test-compilation-no-default-features target=default-target: diff --git a/README.md b/README.md index 5f93525aa..dba907c35 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Or skip setup entirely with a codespace: | x86-64 | [KVM] | ✅ Yes | ✅ Yes | ✅ Yes | | x86-64 | [MSHV] | ✅ Yes | ✅ Yes | ✅ Yes | | AArch64 | [KVM] | ✅ Yes | ❌ No | ❌ No | -| AArch64 | [MSHV] | ❌ No | ❌ No | ❌ No | +| AArch64 | [MSHV] | ✅ Yes | ❌ No | ❌ No | ### Windows diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 9f527987e..f9a38c4ec 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -61,7 +61,10 @@ impl HyperlightVm { #[cfg(kvm)] Some(HypervisorType::Kvm) => Box::new(KvmVm::new().map_err(VmError::CreateVm)?), #[cfg(mshv3)] - Some(HypervisorType::Mshv) => return Err(CreateHyperlightVmError::NoHypervisorFound), + Some(HypervisorType::Mshv) => { + use crate::hypervisor::virtual_machine::mshv::MshvVm; + Box::new(MshvVm::new().map_err(VmError::CreateVm)?) + } #[cfg(hvf)] Some(HypervisorType::Hvf) => { Box::new(HvfVm::new(interrupt_handle.clone()).map_err(VmError::CreateVm)?) diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs index bb05f73b0..68dd62b41 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs @@ -1,27 +1,486 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2025 The Hyperlight Authors. +use std::sync::LazyLock; + +use hyperlight_common::outb::VmAction; +use mshv_bindings::{ + hv_message_type, hv_message_type_HVMSG_GPA_INTERCEPT, hv_message_type_HVMSG_UNMAPPED_GPA, + hv_partition_property_code_HV_PARTITION_PROPERTY_GIC_LPI_INT_ID_BITS, + hv_partition_property_code_HV_PARTITION_PROPERTY_GIC_PPI_OVERFLOW_INTERRUPT_FROM_CNTV, + hv_partition_property_code_HV_PARTITION_PROPERTY_GIC_PPI_PERFORMANCE_MONITORS_INTERRUPT, + hv_partition_property_code_HV_PARTITION_PROPERTY_GICD_BASE_ADDRESS, + hv_partition_property_code_HV_PARTITION_PROPERTY_GITS_TRANSLATER_BASE_ADDRESS, + hv_register_assoc, hv_register_name_HV_ARM64_REGISTER_CPACR_EL1, + hv_register_name_HV_ARM64_REGISTER_FP, hv_register_name_HV_ARM64_REGISTER_FPCR, + hv_register_name_HV_ARM64_REGISTER_FPSR, hv_register_name_HV_ARM64_REGISTER_GICR_BASE_GPA, + hv_register_name_HV_ARM64_REGISTER_LR, hv_register_name_HV_ARM64_REGISTER_MAIR_EL1, + hv_register_name_HV_ARM64_REGISTER_PC, hv_register_name_HV_ARM64_REGISTER_PSTATE, + hv_register_name_HV_ARM64_REGISTER_Q0, hv_register_name_HV_ARM64_REGISTER_SCTLR_EL1, + hv_register_name_HV_ARM64_REGISTER_SP_EL0, hv_register_name_HV_ARM64_REGISTER_SP_EL1, + hv_register_name_HV_ARM64_REGISTER_TCR_EL1, hv_register_name_HV_ARM64_REGISTER_TTBR0_EL1, + hv_register_name_HV_ARM64_REGISTER_VBAR_EL1, hv_register_name_HV_ARM64_REGISTER_X0, + hv_register_value, hv_u128, mshv_create_partition_v2, mshv_user_mem_region, +}; +use mshv_ioctls::{Mshv, VcpuFd, VmFd}; use tracing::{Span, instrument}; -use crate::hypervisor::virtual_machine::CreateVmError; +use crate::hypervisor::regs::{ + CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, +}; +use crate::hypervisor::virtual_machine::{ + CreateVmError, MapMemoryError, RegisterError, ResetVcpuError, RunVcpuError, UnmapMemoryError, + VirtualMachine, VmExit, +}; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; + +static MSHV: LazyLock> = + LazyLock::new(|| Mshv::new().map_err(|e| CreateVmError::HypervisorNotAvailable(e.into()))); + +fn mmio_write_info(syndrome: u64) -> Option<(u32, usize)> { + const ISV: u64 = 1 << 24; + const WNR: u64 = 1 << 6; + + if syndrome & (ISV | WNR) != ISV | WNR { + return None; + } + + let source_register = ((syndrome >> 16) & 0x1f) as u32; + let access_size = 1usize << ((syndrome >> 22) & 0x3); + Some((source_register, access_size)) +} -/// Return `true` if the MSHV API is available +/// Determine whether the MSHV hypervisor API is available on aarch64. #[instrument(skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn is_hypervisor_present() -> bool { - // TODO(aarch64): implement MSHV detection - false + match Mshv::new() { + Ok(_) => true, + Err(_) => { + tracing::info!("MSHV is not available on this system"); + false + } + } } -/// An MSHV implementation of a single-vcpu VM +/// An MSHV implementation of a single-vcpu VM for aarch64. #[derive(Debug)] pub(crate) struct MshvVm { - _placeholder: (), + vm_fd: VmFd, + vcpu_fd: VcpuFd, + mapped_regions: Vec, } -#[allow(unused)] impl MshvVm { - #[allow(unused)] + /// Create a new MSHV VM instance for aarch64. + #[instrument(skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn new() -> std::result::Result { - unimplemented!("MshvVm::new") + let mshv = MSHV.as_ref().map_err(|e| e.clone())?; + + let pr: mshv_create_partition_v2 = Default::default(); + let vm_fd = mshv + .create_vm_with_args(&pr) + .map_err(|e| CreateVmError::CreateVmFd(e.into()))?; + + for (property, value) in [ + ( + hv_partition_property_code_HV_PARTITION_PROPERTY_GICD_BASE_ADDRESS, + 0xffff0000, + ), + ( + hv_partition_property_code_HV_PARTITION_PROPERTY_GITS_TRANSLATER_BASE_ADDRESS, + 0xeff68000, + ), + ( + hv_partition_property_code_HV_PARTITION_PROPERTY_GIC_LPI_INT_ID_BITS, + 1, + ), + ( + hv_partition_property_code_HV_PARTITION_PROPERTY_GIC_PPI_OVERFLOW_INTERRUPT_FROM_CNTV, + 0x1b, + ), + ( + hv_partition_property_code_HV_PARTITION_PROPERTY_GIC_PPI_PERFORMANCE_MONITORS_INTERRUPT, + 0x17, + ), + ] { + vm_fd + .set_partition_property(property, value) + .map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?; + } + + vm_fd + .initialize() + .map_err(|e| CreateVmError::InitializeVm(e.into()))?; + + let vcpu_fd = vm_fd + .create_vcpu(0) + .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; + vcpu_fd + .set_reg(&[hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_GICR_BASE_GPA, + value: hv_register_value { reg64: 0xeffee000 }, + ..Default::default() + }]) + .map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?; + + Ok(Self { + vm_fd, + vcpu_fd, + mapped_regions: Vec::new(), + }) + } + + /// Helper to get a single 64-bit register by name. + fn get_reg64(&self, name: u32) -> Result { + let mut reg = [hv_register_assoc { + name, + ..Default::default() + }]; + self.vcpu_fd + .get_reg(&mut reg) + .map_err(|e| RegisterError::GetRegs(e.into()))?; + Ok(unsafe { reg[0].value.reg64 }) + } +} + +impl VirtualMachine for MshvVm { + unsafe fn map_memory( + &mut self, + (_slot, region): (u32, &MemoryRegion), + ) -> std::result::Result<(), MapMemoryError> { + let mshv_region: mshv_user_mem_region = region.into(); + self.vm_fd + .map_user_memory(mshv_region) + .map_err(|e| MapMemoryError::Hypervisor(e.into()))?; + self.mapped_regions.push(mshv_region); + Ok(()) + } + + fn unmap_memory( + &mut self, + (_slot, region): (u32, &MemoryRegion), + ) -> std::result::Result<(), UnmapMemoryError> { + let mshv_region: mshv_user_mem_region = region.into(); + self.vm_fd + .unmap_user_memory(mshv_region) + .map_err(|e| UnmapMemoryError::Hypervisor(e.into()))?; + self.mapped_regions.retain(|mapped| { + mapped.guest_pfn != mshv_region.guest_pfn + || mapped.size != mshv_region.size + || mapped.userspace_addr != mshv_region.userspace_addr + }); + Ok(()) + } + + fn run_vcpu( + &mut self, + #[cfg(feature = "trace_guest")] _tc: &mut crate::sandbox::trace::TraceContext, + ) -> std::result::Result { + const UNMAPPED_GPA_MESSAGE: hv_message_type = hv_message_type_HVMSG_UNMAPPED_GPA; + const INVALID_GPA_ACCESS_MESSAGE: hv_message_type = hv_message_type_HVMSG_GPA_INTERCEPT; + + match self.vcpu_fd.run() { + Ok(m) => { + let msg_type = m.header.message_type; + match msg_type { + UNMAPPED_GPA_MESSAGE | INVALID_GPA_ACCESS_MESSAGE => { + let mem_msg = m + .to_memory_info() + .map_err(|_| RunVcpuError::DecodeIOMessage(msg_type))?; + let gpa = mem_msg.guest_physical_address; + let io_page_gpa = const { hyperlight_common::layout::io_page().unwrap().0 }; + let is_write = mem_msg.header.intercept_access_type == 1; + + if is_write + && gpa >= io_page_gpa + && (gpa - io_page_gpa) < hyperlight_common::vmem::PAGE_SIZE as u64 + { + let off = (gpa - io_page_gpa) as usize; + let port = off / core::mem::size_of::(); + let pc = mem_msg.header.pc; + let instruction_length = mem_msg.header.instruction_length as u64; + self.vcpu_fd + .set_reg(&[hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_PC, + value: hv_register_value { + reg64: pc.wrapping_add(instruction_length), + }, + ..Default::default() + }]) + .map_err(|e| RunVcpuError::IncrementRip(e.into()))?; + + if port == VmAction::Halt as usize { + return Ok(VmExit::Halt()); + } + + let (source_register, access_size) = mmio_write_info(mem_msg.syndrome) + .ok_or(RunVcpuError::ParseGpaAccessInfo)?; + let value = if source_register == 31 { + 0 + } else { + self.get_reg64( + hv_register_name_HV_ARM64_REGISTER_X0 + source_register, + ) + .map_err(|e| match e { + RegisterError::GetRegs(error) => RunVcpuError::Unknown(error), + _ => unreachable!("get_reg64 returned a non-get error"), + })? + }; + Ok(VmExit::IoOut( + port as u16, + value.to_le_bytes()[..access_size].to_vec(), + )) + } else { + match MemoryRegionFlags::try_from(mem_msg) + .map_err(|_| RunVcpuError::ParseGpaAccessInfo)? + { + MemoryRegionFlags::READ => Ok(VmExit::MmioRead(gpa)), + MemoryRegionFlags::WRITE => Ok(VmExit::MmioWrite(gpa)), + _ => Ok(VmExit::Unknown("Unknown MMIO access".to_string())), + } + } + } + other => Ok(VmExit::Unknown(format!( + "Unknown MSHV VCPU exit: {:?}", + other + ))), + } + } + Err(e) => match e.errno() { + libc::EINTR => Ok(VmExit::Cancelled()), + libc::EAGAIN => Ok(VmExit::Retry()), + _ => Err(RunVcpuError::Unknown(e.into())), + }, + } + } + + fn regs(&self) -> std::result::Result { + let mshv_regs = self + .vcpu_fd + .get_regs() + .map_err(|e| RegisterError::GetRegs(e.into()))?; + Ok(CommonRegisters { + x: mshv_regs.regs, + sp: self.get_reg64(hv_register_name_HV_ARM64_REGISTER_SP_EL0)?, + pc: mshv_regs.pc, + pstate: mshv_regs.pstate, + }) + } + + fn set_regs(&mut self, regs: &CommonRegisters) -> std::result::Result<(), RegisterError> { + let mut mshv_regs = Vec::with_capacity(34); + for (index, value) in regs.x[..29].iter().enumerate() { + mshv_regs.push(hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_X0 + index as u32, + value: hv_register_value { reg64: *value }, + ..Default::default() + }); + } + mshv_regs.extend([ + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_FP, + value: hv_register_value { reg64: regs.x[29] }, + ..Default::default() + }, + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_LR, + value: hv_register_value { reg64: regs.x[30] }, + ..Default::default() + }, + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_SP_EL0, + value: hv_register_value { reg64: regs.sp }, + ..Default::default() + }, + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_PC, + value: hv_register_value { reg64: regs.pc }, + ..Default::default() + }, + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_PSTATE, + value: hv_register_value { reg64: regs.pstate }, + ..Default::default() + }, + ]); + self.vcpu_fd + .set_reg(&mshv_regs) + .map_err(|e| RegisterError::SetRegs(e.into())) + } + + fn fpu(&self) -> std::result::Result { + let mut regs: Vec<_> = (0..32) + .map(|index| hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_Q0 + index, + ..Default::default() + }) + .collect(); + regs.extend([ + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_FPSR, + ..Default::default() + }, + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_FPCR, + ..Default::default() + }, + ]); + self.vcpu_fd + .get_reg(&mut regs) + .map_err(|e| RegisterError::GetFpu(e.into()))?; + + let mut v: [u128; 32] = [0; 32]; + for (value, reg) in v.iter_mut().zip(®s[..32]) { + let reg128 = unsafe { reg.value.reg128 }; + *value = (reg128.high_part as u128) << 64 | reg128.low_part as u128; + } + let fpsr = unsafe { regs[32].value.reg64 } as u32; + let fpcr = unsafe { regs[33].value.reg64 } as u32; + Ok(CommonFpu { v, fpsr, fpcr }) + } + + fn set_fpu(&mut self, fpu: &CommonFpu) -> std::result::Result<(), RegisterError> { + let mut regs: Vec<_> = fpu + .v + .iter() + .enumerate() + .map(|(index, value)| hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_Q0 + index as u32, + value: hv_register_value { + reg128: hv_u128 { + low_part: *value as u64, + high_part: (*value >> 64) as u64, + }, + }, + ..Default::default() + }) + .collect(); + regs.extend([ + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_FPSR, + value: hv_register_value { + reg64: fpu.fpsr as u64, + }, + ..Default::default() + }, + hv_register_assoc { + name: hv_register_name_HV_ARM64_REGISTER_FPCR, + value: hv_register_value { + reg64: fpu.fpcr as u64, + }, + ..Default::default() + }, + ]); + self.vcpu_fd + .set_reg(®s) + .map_err(|e| RegisterError::SetFpu(e.into())) + } + + fn sregs(&self) -> std::result::Result { + let mut regs = [ + hv_register_name_HV_ARM64_REGISTER_TTBR0_EL1, + hv_register_name_HV_ARM64_REGISTER_TCR_EL1, + hv_register_name_HV_ARM64_REGISTER_MAIR_EL1, + hv_register_name_HV_ARM64_REGISTER_SCTLR_EL1, + hv_register_name_HV_ARM64_REGISTER_CPACR_EL1, + hv_register_name_HV_ARM64_REGISTER_VBAR_EL1, + hv_register_name_HV_ARM64_REGISTER_SP_EL1, + ] + .map(|name| hv_register_assoc { + name, + ..Default::default() + }); + self.vcpu_fd + .get_reg(&mut regs) + .map_err(|e| RegisterError::GetRegs(e.into()))?; + + // SAFETY: Each requested register contains a 64-bit value. + Ok(unsafe { + CommonSpecialRegisters { + ttbr0_el1: regs[0].value.reg64, + tcr_el1: regs[1].value.reg64, + mair_el1: regs[2].value.reg64, + sctlr_el1: regs[3].value.reg64, + cpacr_el1: regs[4].value.reg64, + vbar_el1: regs[5].value.reg64, + sp_el1: regs[6].value.reg64, + } + }) + } + + fn set_sregs( + &mut self, + sregs: &CommonSpecialRegisters, + ) -> std::result::Result<(), RegisterError> { + let regs = [ + ( + hv_register_name_HV_ARM64_REGISTER_TTBR0_EL1, + sregs.ttbr0_el1, + ), + (hv_register_name_HV_ARM64_REGISTER_TCR_EL1, sregs.tcr_el1), + (hv_register_name_HV_ARM64_REGISTER_MAIR_EL1, sregs.mair_el1), + ( + hv_register_name_HV_ARM64_REGISTER_SCTLR_EL1, + sregs.sctlr_el1, + ), + ( + hv_register_name_HV_ARM64_REGISTER_CPACR_EL1, + sregs.cpacr_el1, + ), + (hv_register_name_HV_ARM64_REGISTER_VBAR_EL1, sregs.vbar_el1), + (hv_register_name_HV_ARM64_REGISTER_SP_EL1, sregs.sp_el1), + ] + .map(|(name, value)| hv_register_assoc { + name, + value: hv_register_value { reg64: value }, + ..Default::default() + }); + self.vcpu_fd + .set_reg(®s) + .map_err(|e| RegisterError::SetRegs(e.into())) + } + + fn debug_regs(&self) -> std::result::Result { + // Debug register support on aarch64 MSHV is not yet implemented + Ok(CommonDebugRegs::default()) + } + + fn set_debug_regs(&self, _drs: &CommonDebugRegs) -> std::result::Result<(), RegisterError> { + // Debug register support on aarch64 MSHV is not yet implemented + Ok(()) + } + + fn can_reset_vcpu(&self) -> bool { + true + } + + fn reset_vcpu(&mut self) -> Result<(), ResetVcpuError> { + let mut replacement = Self::new().map_err(|e| ResetVcpuError::Unknown(e.to_string()))?; + for region in &self.mapped_regions { + replacement + .vm_fd + .map_user_memory(*region) + .map_err(|e| ResetVcpuError::Hypervisor(e.into()))?; + } + replacement.mapped_regions.clone_from(&self.mapped_regions); + *self = replacement; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::mmio_write_info; + + #[test] + fn decodes_mmio_write_syndrome() { + assert_eq!(mmio_write_info(0x93c8_8047), Some((8, 8))); + } + + #[test] + fn rejects_invalid_mmio_write_syndrome() { + assert_eq!(mmio_write_info(0), None); + assert_eq!(mmio_write_info(1 << 24), None); + assert_eq!(mmio_write_info(1 << 6), None); } } diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index 75636a6c7..8c9e8cc3e 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -103,8 +103,16 @@ impl TryFrom for MemoryRegionFlags { impl TryFrom for MemoryRegionFlags { type Error = crate::HyperlightError; - fn try_from(_msg: hv_arm64_memory_intercept_message) -> crate::Result { - unimplemented!("try_from") + fn try_from(msg: hv_arm64_memory_intercept_message) -> crate::Result { + let access_type = msg.header.intercept_access_type; + match access_type { + 0 => Ok(MemoryRegionFlags::READ), + 1 => Ok(MemoryRegionFlags::WRITE), + 2 => Ok(MemoryRegionFlags::EXECUTE), + _ => Err(crate::HyperlightError::Error( + "unknown memory access type".to_string(), + )), + } } } diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 1dbade579..6c2aae84b 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -787,8 +787,11 @@ mod tests { "unrecognized x86_64 CPU vendor: {v:?}" ); #[cfg(all(target_arch = "aarch64", target_os = "linux"))] - // MIDR_EL1 implementer byte for Apple silicon. - assert_eq!(v, "0x61", "unexpected aarch64 CPU implementer"); + // MIDR_EL1 implementer bytes for Arm and Apple silicon. + assert!( + matches!(v, "0x41" | "0x61"), + "unexpected aarch64 CPU implementer: {v:?}" + ); #[cfg(all(target_arch = "aarch64", target_os = "macos"))] assert_eq!(v, "0x61", "unexpected aarch64 CPU implementer"); #[cfg(all(target_arch = "aarch64", target_os = "windows"))] diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index eca999e32..f88fd4081 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -1064,8 +1064,9 @@ fn interrupt_random_kill_stress_test() { use hyperlight_host::sandbox::snapshot::Snapshot; use tracing::{error, trace}; + // MSHV restore holds both old and replacement partitions under a 64-partition host limit. #[cfg(target_arch = "aarch64")] - const POOL_SIZE: usize = 64; + const POOL_SIZE: usize = if cfg!(mshv3) { 16 } else { 64 }; #[cfg(not(target_arch = "aarch64"))] const POOL_SIZE: usize = 100; const NUM_THREADS: usize = 100; @@ -1515,7 +1516,12 @@ fn interrupt_infinite_loop_stress_test() { use std::sync::{Arc, Barrier}; use std::thread; - const NUM_THREADS: usize = 50; + // MSHV restore needs a second partition per worker. WHP resets in place. + const NUM_THREADS: usize = if cfg!(all(target_arch = "aarch64", mshv3)) { + 16 + } else { + 50 + }; const ITERATIONS_PER_THREAD: usize = 500; let mut handles = vec![]; @@ -1600,10 +1606,9 @@ fn interrupt_infinite_moving_loop_stress_test() { use std::sync::Arc; use std::thread; - // ARM64 WHP partitions include GICv3 state, limiting practical partition - // concurrency. Each thread creates two partitions. + // Each worker holds two partitions. Keep headroom under MSHV's 64-partition host limit. #[cfg(target_arch = "aarch64")] - const NUM_THREADS: usize = 32; + const NUM_THREADS: usize = if cfg!(mshv3) { 8 } else { 32 }; #[cfg(not(target_arch = "aarch64"))] const NUM_THREADS: usize = 200; diff --git a/src/hyperlight_host/tests/sandbox_host_tests.rs b/src/hyperlight_host/tests/sandbox_host_tests.rs index bd7dc77de..6ade560dd 100644 --- a/src/hyperlight_host/tests/sandbox_host_tests.rs +++ b/src/hyperlight_host/tests/sandbox_host_tests.rs @@ -494,10 +494,19 @@ fn simple_test() { #[test] fn simple_test_parallel() { - let handles: Vec<_> = (0..50) - .map(|_| { - std::thread::spawn(|| { - simple_test_helper(); + const NUM_RUNS: usize = 50; + const NUM_THREADS: usize = if cfg!(all(target_arch = "aarch64", mshv3)) { + 24 + } else { + NUM_RUNS + }; + + let handles: Vec<_> = (0..NUM_THREADS) + .map(|worker| { + std::thread::spawn(move || { + for _ in (worker..NUM_RUNS).step_by(NUM_THREADS) { + simple_test_helper(); + } }) }) .collect(); @@ -907,7 +916,11 @@ fn callback_test_parallel() { #[cfg(all(target_arch = "aarch64", target_os = "windows"))] const THREADS: usize = 64; #[cfg(not(all(target_arch = "aarch64", target_os = "windows")))] - const THREADS: usize = 100; + const THREADS: usize = if cfg!(all(target_arch = "aarch64", mshv3)) { + 16 + } else { + 100 + }; let handles: Vec<_> = (0..THREADS) .map(|_| { diff --git a/typos.toml b/typos.toml index 8155b53e5..1c6cac99c 100644 --- a/typos.toml +++ b/typos.toml @@ -19,3 +19,7 @@ readables="readables" writables="writables" # itme is an acronym used in HVF support for "Into TiMoutable Error" itme="itme" + +[default.extend-identifiers] +# This spelling is part of the mshv-bindings API. +hv_partition_property_code_HV_PARTITION_PROPERTY_GITS_TRANSLATER_BASE_ADDRESS="hv_partition_property_code_HV_PARTITION_PROPERTY_GITS_TRANSLATER_BASE_ADDRESS"