From 42a0caa2efd3901adee9f34ce1d238cf28497bbe Mon Sep 17 00:00:00 2001 From: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:25:50 -0800 Subject: [PATCH] feat: enable hyperlight-host cross-compilation for aarch64 Add conditional compilation gates and aarch64 stub modules so that cargo build --target aarch64-unknown-linux-gnu -p hyperlight-host succeeds. No aarch64 implementation is added. All aarch64 code paths are stubs that panic at runtime. Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com> --- docs/aarch64-architecture-plan.md | 258 ++++++ .../src/arch/aarch64/layout.rs | 25 + .../src/arch/aarch64/vmem.rs | 52 ++ src/hyperlight_common/src/layout.rs | 1 + src/hyperlight_common/src/vmem.rs | 1 + src/hyperlight_host/Cargo.toml | 4 +- src/hyperlight_host/build.rs | 4 +- .../src/hypervisor/hyperlight_vm/aarch64.rs | 99 +++ .../src/hypervisor/hyperlight_vm/mod.rs | 730 +++++++++++++++++ .../x86_64.rs} | 764 +----------------- src/hyperlight_host/src/hypervisor/regs.rs | 16 +- .../src/hypervisor/regs/aarch64/mod.rs | 37 + .../regs/{ => x86_64}/debug_regs.rs | 0 .../src/hypervisor/regs/{ => x86_64}/fpu.rs | 0 .../src/hypervisor/regs/x86_64/mod.rs | 28 + .../regs/{ => x86_64}/special_regs.rs | 0 .../regs/{ => x86_64}/standard_regs.rs | 0 .../hypervisor/virtual_machine/kvm/aarch64.rs | 40 + .../src/hypervisor/virtual_machine/kvm/mod.rs | 25 + .../virtual_machine/{kvm.rs => kvm/x86_64.rs} | 0 .../src/hypervisor/virtual_machine/mod.rs | 11 +- .../virtual_machine/mshv/aarch64.rs | 38 + .../hypervisor/virtual_machine/mshv/mod.rs | 25 + .../{mshv.rs => mshv/x86_64.rs} | 0 src/hyperlight_host/src/mem/memory_region.rs | 15 +- 25 files changed, 1413 insertions(+), 760 deletions(-) create mode 100644 docs/aarch64-architecture-plan.md create mode 100644 src/hyperlight_common/src/arch/aarch64/layout.rs create mode 100644 src/hyperlight_common/src/arch/aarch64/vmem.rs create mode 100644 src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs create mode 100644 src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs rename src/hyperlight_host/src/hypervisor/{hyperlight_vm.rs => hyperlight_vm/x86_64.rs} (76%) create mode 100644 src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs rename src/hyperlight_host/src/hypervisor/regs/{ => x86_64}/debug_regs.rs (100%) rename src/hyperlight_host/src/hypervisor/regs/{ => x86_64}/fpu.rs (100%) create mode 100644 src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs rename src/hyperlight_host/src/hypervisor/regs/{ => x86_64}/special_regs.rs (100%) rename src/hyperlight_host/src/hypervisor/regs/{ => x86_64}/standard_regs.rs (100%) create mode 100644 src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs create mode 100644 src/hyperlight_host/src/hypervisor/virtual_machine/kvm/mod.rs rename src/hyperlight_host/src/hypervisor/virtual_machine/{kvm.rs => kvm/x86_64.rs} (100%) create mode 100644 src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs create mode 100644 src/hyperlight_host/src/hypervisor/virtual_machine/mshv/mod.rs rename src/hyperlight_host/src/hypervisor/virtual_machine/{mshv.rs => mshv/x86_64.rs} (100%) diff --git a/docs/aarch64-architecture-plan.md b/docs/aarch64-architecture-plan.md new file mode 100644 index 000000000..95b1123e1 --- /dev/null +++ b/docs/aarch64-architecture-plan.md @@ -0,0 +1,258 @@ +# AArch64 Architecture Plan + +This document outlines the plan for adding aarch64 cross-compilation support to Hyperlight. + +## Goal + +Make the entire workspace compile successfully for `aarch64-unknown-linux-gnu` while maintaining +identical behavior on x86_64. No aarch64 implementation is added — only stubs and conditional +compilation gates so the build succeeds. This is the **first PR** toward full aarch64 support. + +## Current State: Cross-Compilation Errors + +Running `cargo build --target aarch64-unknown-linux-gnu` currently fails. The first errors come from +`hyperlight-common`, which blocks all downstream crates. Fixing each crate in dependency order +will reveal errors in subsequent crates. + +### Error inventory by crate (dependency order) + +#### 1. `hyperlight-common` +| File | Error | Cause | +|------|-------|-------| +| `src/layout.rs:19` | `file not found for module arch` | `cfg_attr` has paths for `x86_64` and `x86` but not `aarch64` | +| `src/vmem.rs:19` | `file not found for module arch` | Same — no `aarch64` path mapping | + +The `arch/aarch64/` directory exists but is **empty**. + +#### 2. `hyperlight-host` (blocked by #1) +| File | Error | Cause | +|------|-------|-------| +| `src/hypervisor/regs/standard_regs.rs` | `CommonRegisters` has x86 fields (rax, rbx, rip...) | No arch gate, no aarch64 equivalent | +| `src/hypervisor/regs/special_regs.rs` | `CommonSpecialRegisters` has x86 fields (cr0, efer, gdt...) | Same | +| `src/hypervisor/regs/fpu.rs` | `CommonFpu` has x86 fields (fpr, xmm, mxcsr) | Same | +| `src/hypervisor/regs/debug_regs.rs` | `CommonDebugRegs` has x86 fields (dr0-dr7) | Same | +| `src/hypervisor/hyperlight_vm.rs` | Uses x86 register names (`rip`, `rsp`, `rdi`, `rsi`, `rdx`, `rcx`, `rflags`, `rax`, `cr3`) throughout; uses `std::arch::x86_64::__cpuid_count` | 3000-line file deeply coupled to x86 | +| `src/hypervisor/virtual_machine/kvm.rs` | CPUID manipulation, `kvm_regs`/`kvm_sregs`/`kvm_fpu` types | KVM x86 API types | +| `src/hypervisor/virtual_machine/mshv.rs` | `hv_message_type_HVMSG_X64_*`, `HV_X64_REGISTER_*` | MSHV x86 API types | +| `src/hypervisor/virtual_machine/mod.rs:115` | `compile_error!` if no hypervisor type available | Will fire if kvm/mshv features disabled | +| `src/hypervisor/gdb/arch.rs` | x86 exception IDs, INT3 opcode, DR6 flags | No arch gate | +| `src/hypervisor/gdb/x86_64_target.rs` | `GdbTargetArch = X86_64_SSE` | No arch gate | +| `src/hypervisor/crashdump.rs` | `NT_X86_XSTATE`, x86-64 register layout | No arch gate | +| `src/mem/mgr.rs:751` | `init-paging` code gated on `target_arch = "x86_64"` | Already gated — OK | +| `src/mem/elf.rs` | ELF relocations | Already has both x86_64 and aarch64 paths — OK | + +## Patterns Researched from the Rust Ecosystem + +The following crates were studied for how they cleanly separate architecture-specific code: + +### Crates Researched + +| Crate | Pattern Used | Notes | +|-------|-------------|-------| +| **`kvm-bindings`** (rust-vmm) | Flat arch modules + wildcard re-export | `mod x86_64; pub use self::x86_64::*;` / `mod arm64; pub use self::arm64::*;` gated by `cfg(target_arch)`. Simple, no traits. | +| **Cloud Hypervisor** (`hypervisor` crate) | Backend-owns-intersection + `cfg` on trait methods | `arch/x86/` and `arch/aarch64/` for types. Trait methods gated: `#[cfg(target_arch = "x86_64")] fn get_sregs()`. Backend dirs contain `kvm/x86_64/`, `kvm/aarch64/`. | +| **Firecracker** (`vmm` crate) | Arch-owns-everything + re-export at `arch/mod.rs` | `arch/x86_64/` and `arch/aarch64/` each contain full `kvm/`, `vcpu/`, `vm/` modules. `arch/mod.rs` re-exports with `pub use x86_64::*`. Clean — callers never write `x86_64::`. | +| **`libc`** (rust-lang) | Deep platform tree | `unix/linux/arch/` hierarchy. Only the matching arch module compiles. | +| **`stdarch`** / `core::arch` (rust-lang) | Separate arch crate modules | `x86_64/`, `aarch64/` directories. Each arch is self-contained. | +| **`rustix`** (bytecodealliance) | Backend abstraction layer | `backend/` directory with `linux_raw/`, `libc/` backends, each internally split by arch. | + +### Recommended Pattern for Hyperlight + +Hyperlight should use a combination of **Firecracker's re-export pattern** and **Cloud Hypervisor's backend structure**: + +1. **`cfg_attr` path switching** for arch-specific module files (already used in `hyperlight-common` and `hyperlight-guest`). Extend to aarch64. +2. **`cfg(target_arch)` module gating with wildcard re-exports** for register types in `hyperlight-host` (like `kvm-bindings`). +3. **`cfg(target_arch)` on entire impl blocks and functions** rather than on individual struct fields — avoids `#[cfg]` spaghetti inside struct definitions. +4. **Stub modules for unimplemented architectures** — compile but `unimplemented!()` at runtime. + +Key principle: **each arch-specific module should be self-contained**. Callers should never write `x86_64::CommonRegisters` — they write `CommonRegisters` and the right type is selected at compile time. + +## Scope: Phase 1 — Compile for aarch64 (No Behavior Change) + +The goal is: `cargo build --target aarch64-unknown-linux-gnu -p hyperlight-host` succeeds with **zero aarch64 implementation**. All aarch64 paths are stubs. All x86_64 behavior is identical. + +Guest crates (`hyperlight-guest`, `hyperlight-guest-bin`, `hyperlight-guest-capi`, test guests) are **out of scope** for this PR — they will be addressed in a separate guest-support PR. + +### Strategy + +For `hyperlight-common` (shared dependency) and `hyperlight-host`, add the minimum conditional compilation needed: + +1. **Add `cfg_attr` / `cfg` gates** — never delete x86 code, only wrap it +2. **Create stub files** — aarch64 modules that export the same symbols but with placeholder types or `todo!()` bodies +3. **Avoid trait-level abstraction changes** — the `VirtualMachine` trait keeps its current shape; the register types it uses become arch-specific via re-exports + +### Changes by Crate + +#### 1. `hyperlight-common` + +**Files to modify:** +- `src/layout.rs` — add `#[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/layout.rs")]` +- `src/vmem.rs` — add `#[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/vmem.rs")]` + +**Files to create:** +- `src/arch/aarch64/layout.rs` — stub with same public API as `arch/amd64/layout.rs` (constants can differ, functions can `todo!()`) +- `src/arch/aarch64/vmem.rs` — stub with same public API as `arch/amd64/vmem.rs` + +#### 2. `hyperlight-host` + +This is the largest change. The approach: **gate entire modules and files** rather than inserting `cfg` attributes inside functions. + +**Register types (`src/hypervisor/regs/`):** + +The current `CommonRegisters`, `CommonSpecialRegisters`, `CommonFpu`, `CommonDebugRegs` types are x86-only. Two options: + +- **Option A (minimal, recommended for Phase 1):** Gate the entire `regs/` module and all four files behind `#[cfg(target_arch = "x86_64")]`, then create parallel aarch64 stub files that define the same type names with aarch64-appropriate (or placeholder) fields. Use `cfg_attr` path switching or conditional `mod` + `pub use` in `regs.rs`. + +- **Option B (premature):** Introduce a `Registers` trait. This is too much refactoring for Phase 1. + +Recommended: **Option A using `kvm-bindings` style**: + +```rust +// regs.rs (new version) +#[cfg(target_arch = "x86_64")] +mod x86_64; +#[cfg(target_arch = "x86_64")] +pub(crate) use x86_64::*; + +#[cfg(target_arch = "aarch64")] +mod aarch64; +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; +``` + +Where `x86_64/` contains the current four files renamed, and `aarch64/` contains stubs exporting the same names (`CommonRegisters`, `CommonSpecialRegisters`, `CommonFpu`, `CommonDebugRegs`) but with aarch64-appropriate fields (can be empty structs initially or placeholder fields). + +**`hyperlight_vm.rs` (3010 lines):** + +This file is deeply x86-specific. Most of it uses x86 register field names directly. For Phase 1, the cleanest approach is to gate the entire `HyperlightVM` implementation behind `#[cfg(target_arch = "x86_64")]` and provide a stub aarch64 version that compiles but is not functional. + +Approach: +- Wrap the entire `impl HyperlightVM` block (or the struct + impl together) with `#[cfg(target_arch = "x86_64")]` +- Create a minimal `hyperlight_vm_aarch64.rs` stub that defines the same struct and implements the same interface with `todo!()` bodies +- Use `cfg_attr` path switching or conditional includes + +**Virtual machine backends (`src/hypervisor/virtual_machine/`):** +- `kvm.rs` — gate behind `#[cfg(target_arch = "x86_64")]` (KVM x86 CPUID code, x86 register types). Create `kvm_aarch64.rs` stub or split `kvm.rs` into `kvm/mod.rs` + `kvm/x86_64.rs` + `kvm/aarch64.rs` +- `mshv.rs` — gate behind `#[cfg(target_arch = "x86_64")]` (MSHV x64 types). Stub for aarch64. +- `mod.rs` — remove the `compile_error!` for no hypervisor, or adjust it to be aware that aarch64+linux with kvm feature is valid + +**GDB (`src/hypervisor/gdb/`):** +- Gate `arch.rs` and `x86_64_target.rs` behind `#[cfg(target_arch = "x86_64")]` +- The `gdb` module is already behind `#[cfg(gdb)]` so this is low priority + +**Crashdump (`src/hypervisor/crashdump.rs`):** +- Gate behind `#[cfg(target_arch = "x86_64")]` (already behind `#[cfg(crashdump)]`) + +**Memory management (`src/mem/`):** +- `mgr.rs:751` — already gated on `target_arch = "x86_64"` — OK +- `elf.rs` — already has both x86_64 and aarch64 paths — OK + +### File Change Summary + +| Action | Path | Description | +|--------|------|-------------| +| **modify** | `hyperlight-common/src/layout.rs` | Add aarch64 cfg_attr | +| **modify** | `hyperlight-common/src/vmem.rs` | Add aarch64 cfg_attr | +| **create** | `hyperlight-common/src/arch/aarch64/layout.rs` | Stub | +| **create** | `hyperlight-common/src/arch/aarch64/vmem.rs` | Stub | +| **modify** | `hyperlight-host/src/hypervisor/regs.rs` | Restructure with cfg(target_arch) re-exports | +| **move** | `hyperlight-host/src/hypervisor/regs/*.rs` | → `regs/x86_64/` subdirectory | +| **create** | `hyperlight-host/src/hypervisor/regs/aarch64/*.rs` | Stub register types | +| **modify** | `hyperlight-host/src/hypervisor/hyperlight_vm.rs` | Gate x86 code, add aarch64 stub | +| **modify** | `hyperlight-host/src/hypervisor/virtual_machine/kvm.rs` | Gate x86-specific parts | +| **modify** | `hyperlight-host/src/hypervisor/virtual_machine/mshv.rs` | Gate x86-specific parts | +| **modify** | `hyperlight-host/src/hypervisor/virtual_machine/mod.rs` | Adjust compile_error for aarch64 | +| **modify** | `hyperlight-host/src/hypervisor/gdb/arch.rs` | Gate behind target_arch = "x86_64" | +| **modify** | `hyperlight-host/src/hypervisor/gdb/x86_64_target.rs` | Gate behind target_arch = "x86_64" | +| **modify** | `hyperlight-host/src/hypervisor/crashdump.rs` | Gate behind target_arch = "x86_64" | + +### Verification Criteria + +1. `cargo build` on x86_64 Linux — must produce identical binary (no behavior change) +2. `cargo build --target aarch64-unknown-linux-gnu -p hyperlight-host` — must succeed (cross-compile) +3. `cargo test` on x86_64 Linux — all existing tests pass +4. `cargo clippy --target aarch64-unknown-linux-gnu -p hyperlight-host` — no warnings +5. `just test-like-ci` — passes + +### Principles + +- **No new crates or dependencies** added +- **No trait redesign** — the `VirtualMachine` trait keeps its current shape +- **No new abstractions** — just conditional compilation (`cfg`) and stub modules +- **Minimal diff** — prefer wrapping existing code with `cfg` over moving/rewriting it +- **Stubs use `todo!()`** not `unimplemented!()` — clearer intent that implementation is planned + +## Future Work (Separate PRs) + +These are **out of scope** for the first PR but documented for planning: + +### Phase 2: Guest crate aarch64 cross-compilation + +Known errors to fix: + +| Crate | File | Error | Cause | +|-------|------|-------|-------| +| `hyperlight-guest` | `src/layout.rs:17-18` | Missing `aarch64` cfg_attr path | Same pattern as hyperlight-common | +| `hyperlight-guest` | `src/prim_alloc.rs:17-18` | Missing `aarch64` cfg_attr path | Same pattern | +| `hyperlight-guest` | `src/exit.rs` | `asm!("out dx, eax")` with no arch gate | x86 I/O port assembly used unconditionally | +| `hyperlight-guest` | `src/lib.rs:18` | `compile_error!` for trace_guest on non-x86_64 | Only blocks trace_guest feature, not main build | +| `hyperlight-guest-bin` | `src/lib.rs:36-37` | `cfg_attr` only maps `x86_64` → `arch/amd64/mod.rs` | No aarch64 path for the `arch` module | +| `hyperlight-guest-bin` | `src/paging.rs` | x86 `asm!` (mov cr3, mov cr0) with no arch gate | Inline assembly is purely x86 | +| `hyperlight-guest-bin` | `src/lib.rs` `mem_profile` | `asm!("out dx, al")` with no arch gate | x86 assembly in allocator tracing | +| `hyperlight-guest-tracing` | — | Already gated | `cfg(target_arch = "x86_64")` in Cargo.toml — OK | +| test guests | `simpleguest/src/main.rs` | x86 asm: `hlt`, `int3`, `ud2`, etc. | No arch gate on test code | +| test guests | `dummyguest/src/main.rs` | x86 asm: `hlt`, `mov al, [0x8000]` | No arch gate on test code | + +Changes needed: +- `hyperlight-guest`: add `cfg_attr` for aarch64 paths in `layout.rs`, `prim_alloc.rs`; gate x86 asm in `exit.rs` +- `hyperlight-guest-bin`: add aarch64 arch module stub; gate x86 asm in `paging.rs` and `lib.rs` +- Test guests: gate x86-specific test code or exclude from aarch64 builds +- Create stub files: `hyperlight-guest/src/arch/aarch64/{layout,prim_alloc}.rs`, `hyperlight-guest-bin/src/arch/aarch64/mod.rs` + +### Phase 3: Implement aarch64 register types +- Replace stub register types with real AArch64 register structs +- Implement conversions to/from KVM aarch64 register types + +### Phase 4: Restructure backends +- Split `kvm.rs` → `kvm/mod.rs` + `kvm/x86_64.rs` + `kvm/aarch64.rs` +- Same for `mshv.rs` +- Implement `VirtualMachine` trait for KVM on aarch64 + +### Phase 5: Implement guest-side aarch64 +- Real `exit.rs` using MMIO or HVC for guest→host communication +- Real `paging.rs` with AArch64 page table manipulation +- Exception handling, context save/restore for AArch64 + +### Phase 6: Add HVF backend (macOS) +- New `hvf/` backend module for Apple Hypervisor.framework + +## The Problem: Two Dimensions + +Hyperlight needs to support multiple architectures AND multiple hypervisor backends: + +| Backend | x86_64 | aarch64 | +|---------|--------|---------| +| KVM | ✓ | ✓ | +| MSHV | ✓ | ✓ | +| WHP | ✓ | ✓ (Windows on ARM) | +| HVF | ✗ | ✓ (macOS only) | + +Note: HVF is the only single-architecture backend — Apple dropped x86 HVF when transitioning to Apple Silicon. + +Code falls into three categories: +1. **Pure architecture** — page tables, registers, memory layout (same across backends) +2. **Pure hypervisor** — API calls, VM creation, memory mapping (similar across arches) +3. **Intersection** — vCPU setup using a specific backend's API for a specific arch + +## Reference Projects + +These projects solve the same arch × backend problem and were studied for this plan: + +| Project | Pattern | How it separates arch code | +|---------|---------|---------------------------| +| **kvm-bindings** (rust-vmm) | Flat arch module + wildcard re-export | `mod x86_64; pub use self::x86_64::*;` gated by `cfg(target_arch)` — callers see one flat namespace | +| **Cloud Hypervisor** | Backend-owns-intersection + cfg on trait methods | `arch/x86/` and `arch/aarch64/` for types; `kvm/x86_64/` and `kvm/aarch64/` for backend code; trait methods gated with `#[cfg(target_arch)]` | +| **Firecracker** | Arch-owns-everything + re-export | `arch/x86_64/` and `arch/aarch64/` each contain full `kvm/`, `vcpu/`, `vm/` trees; `arch/mod.rs` re-exports the right one | +| **libc** (rust-lang) | Deep platform tree | `unix/linux/arch/` hierarchy, only matching arch compiles | +| **stdarch** / `core::arch` (rust-lang) | Separate arch module trees | `x86_64/`, `aarch64/` directories, each self-contained | +| **rustix** (bytecodealliance) | Backend abstraction layer | `backend/` dir with `linux_raw/`, `libc/`, each internally split by arch | diff --git a/src/hyperlight_common/src/arch/aarch64/layout.rs b/src/hyperlight_common/src/arch/aarch64/layout.rs new file mode 100644 index 000000000..20f17026c --- /dev/null +++ b/src/hyperlight_common/src/arch/aarch64/layout.rs @@ -0,0 +1,25 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ + +// TODO(aarch64): change these, they are only provided in order to compile +pub const MAX_GVA: usize = 0xffff_ffff_ffff_efff; +pub const SNAPSHOT_PT_GVA_MIN: usize = 0xffff_8000_0000_0000; +pub const SNAPSHOT_PT_GVA_MAX: usize = 0xffff_80ff_ffff_ffff; +pub const MAX_GPA: usize = 0x0000_000f_ffff_ffff; + +pub fn min_scratch_size(_input_data_size: usize, _output_data_size: usize) -> usize { + unimplemented!("min_scratch_size") +} diff --git a/src/hyperlight_common/src/arch/aarch64/vmem.rs b/src/hyperlight_common/src/arch/aarch64/vmem.rs new file mode 100644 index 000000000..3803251d2 --- /dev/null +++ b/src/hyperlight_common/src/arch/aarch64/vmem.rs @@ -0,0 +1,52 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + */ + +// TODO(aarch64): implement real page table operations + +use crate::vmem::{Mapping, TableOps, TableReadOps, Void}; + +pub const PAGE_SIZE: usize = 4096; +pub const PAGE_TABLE_SIZE: usize = 4096; +pub type PageTableEntry = u64; +pub type VirtAddr = u64; +pub type PhysAddr = u64; + +/// # Safety +/// See `TableOps` documentation. +#[allow(clippy::missing_safety_doc)] +pub unsafe fn map(_op: &Op, _mapping: Mapping) { + unimplemented!("map") +} + +/// # Safety +/// See `TableReadOps` documentation. +#[allow(clippy::missing_safety_doc)] +pub unsafe fn virt_to_phys<'a, Op: TableReadOps + 'a>( + _op: impl core::convert::AsRef + Copy + 'a, + _address: u64, + _len: u64, +) -> impl Iterator + 'a { + unimplemented!("virt_to_phys"); + #[allow(unreachable_code)] + core::iter::empty() +} + +pub trait TableMovability {} +impl> TableMovability + for crate::vmem::MayMoveTable +{ +} +impl TableMovability for crate::vmem::MayNotMoveTable {} diff --git a/src/hyperlight_common/src/layout.rs b/src/hyperlight_common/src/layout.rs index 215a80d87..963315e25 100644 --- a/src/hyperlight_common/src/layout.rs +++ b/src/hyperlight_common/src/layout.rs @@ -16,6 +16,7 @@ limitations under the License. #[cfg_attr(target_arch = "x86_64", path = "arch/amd64/layout.rs")] #[cfg_attr(target_arch = "x86", path = "arch/i686/layout.rs")] +#[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/layout.rs")] mod arch; pub use arch::{MAX_GPA, MAX_GVA, SNAPSHOT_PT_GVA_MAX, SNAPSHOT_PT_GVA_MIN}; diff --git a/src/hyperlight_common/src/vmem.rs b/src/hyperlight_common/src/vmem.rs index be67658a3..c72a6b9af 100644 --- a/src/hyperlight_common/src/vmem.rs +++ b/src/hyperlight_common/src/vmem.rs @@ -16,6 +16,7 @@ limitations under the License. #[cfg_attr(target_arch = "x86_64", path = "arch/amd64/vmem.rs")] #[cfg_attr(target_arch = "x86", path = "arch/i686/vmem.rs")] +#[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/vmem.rs")] mod arch; /// This is always the page size that the /guest/ is being compiled diff --git a/src/hyperlight_host/Cargo.toml b/src/hyperlight_host/Cargo.toml index 9279d1d10..a176e0e79 100644 --- a/src/hyperlight_host/Cargo.toml +++ b/src/hyperlight_host/Cargo.toml @@ -49,7 +49,7 @@ chrono = { version = "0.4", optional = true } anyhow = "1.0" metrics = "0.24.3" serde_json = "1.0" -elfcore = "2.0" +elfcore = { version = "2.0", optional = true } uuid = { version = "1.22.0", features = ["v4"] } [target.'cfg(windows)'.dependencies] @@ -128,7 +128,7 @@ executable_heap = [] # This feature enables printing of debug information to stdout in debug builds print_debug = [] # Dumps the VM state to a file on unexpected errors or crashes. The path of the file will be printed on stdout and logged. -crashdump = ["dep:chrono"] +crashdump = ["dep:chrono", "dep:elfcore"] trace_guest = ["dep:opentelemetry", "dep:tracing-opentelemetry", "dep:hyperlight-guest-tracing", "hyperlight-common/trace_guest"] mem_profile = [ "trace_guest", "dep:framehop", "dep:fallible-iterator", "hyperlight-common/mem_profile" ] kvm = ["dep:kvm-bindings", "dep:kvm-ioctls"] diff --git a/src/hyperlight_host/build.rs b/src/hyperlight_host/build.rs index 953bfda29..d599bedc1 100644 --- a/src/hyperlight_host/build.rs +++ b/src/hyperlight_host/build.rs @@ -99,10 +99,10 @@ fn main() -> Result<()> { // Essentially the kvm and mshv3 features are ignored on windows as long as you use #[cfg(kvm)] and not #[cfg(feature = "kvm")]. // You should never use #[cfg(feature = "kvm")] or #[cfg(feature = "mshv3")] in the codebase. cfg_aliases::cfg_aliases! { - gdb: { all(feature = "gdb", debug_assertions) }, + gdb: { all(feature = "gdb", debug_assertions, target_arch = "x86_64") }, kvm: { all(feature = "kvm", target_os = "linux") }, mshv3: { all(feature = "mshv3", target_os = "linux") }, - crashdump: { all(feature = "crashdump") }, + crashdump: { all(feature = "crashdump", target_arch = "x86_64") }, // print_debug feature is aliased with debug_assertions to make it only available in debug-builds. print_debug: { all(feature = "print_debug", debug_assertions) }, } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs new file mode 100644 index 000000000..c4288ae60 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -0,0 +1,99 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// TODO(aarch64): implement arch-specific HyperlightVm methods + +use std::sync::Arc; + +use super::{ + AccessPageTableError, CreateHyperlightVmError, DispatchGuestCallError, HyperlightVm, + InitializeError, +}; +#[cfg(gdb)] +use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse}; +use crate::hypervisor::regs::CommonSpecialRegisters; +use crate::hypervisor::virtual_machine::RegisterError; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; +use crate::sandbox::SandboxConfiguration; +use crate::sandbox::host_funcs::FunctionRegistry; +use crate::sandbox::snapshot::NextAction; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; +#[cfg(crashdump)] +use crate::sandbox::uninitialized::SandboxRuntimeConfig; + +impl HyperlightVm { + #[allow(clippy::too_many_arguments)] + pub(crate) fn new( + _snapshot_mem: GuestSharedMemory, + _scratch_mem: GuestSharedMemory, + _pml4_addr: u64, + _entrypoint: NextAction, + _rsp_gva: u64, + _config: &SandboxConfiguration, + #[cfg(gdb)] _gdb_conn: Option>, + #[cfg(crashdump)] _rt_cfg: SandboxRuntimeConfig, + #[cfg(feature = "mem_profile")] _trace_info: MemTraceInfo, + ) -> std::result::Result { + unimplemented!("new") + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn initialise( + &mut self, + _peb_addr: crate::mem::ptr::RawPtr, + _seed: u64, + _page_size: u32, + _mem_mgr: &mut SandboxMemoryManager, + _host_funcs: &Arc>, + _guest_max_log_level: Option, + #[cfg(gdb)] _dbg_mem_access_fn: Arc< + std::sync::Mutex>, + >, + ) -> Result<(), InitializeError> { + unimplemented!("initialise") + } + + pub(crate) fn dispatch_call_from_host( + &mut self, + _mem_mgr: &mut SandboxMemoryManager, + _host_funcs: &Arc>, + #[cfg(gdb)] _dbg_mem_access_fn: Arc< + std::sync::Mutex>, + >, + ) -> Result<(), DispatchGuestCallError> { + unimplemented!("dispatch_call_from_host") + } + + pub(crate) fn get_root_pt(&self) -> Result { + unimplemented!("get_root_pt") + } + + pub(crate) fn get_snapshot_sregs( + &mut self, + ) -> Result { + unimplemented!("get_snapshot_sregs") + } + + pub(crate) fn reset_vcpu( + &mut self, + _cr3: u64, + _sregs: &CommonSpecialRegisters, + ) -> std::result::Result<(), RegisterError> { + unimplemented!("reset_vcpu") + } +} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs new file mode 100644 index 000000000..8d8538ea2 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -0,0 +1,730 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#[cfg(target_arch = "x86_64")] +mod x86_64; + +#[cfg(target_arch = "aarch64")] +mod aarch64; +// Shared imports used by the struct definition and shared methods. +#[cfg(gdb)] +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; + +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; +use hyperlight_common::log_level::GuestLogFilter; +use tracing_core::LevelFilter; + +use crate::HyperlightError; +#[cfg(gdb)] +use crate::hypervisor::gdb::DebuggableVm; +#[cfg(gdb)] +use crate::hypervisor::gdb::arch::VcpuStopReasonError; +#[cfg(gdb)] +use crate::hypervisor::gdb::{ + DebugCommChannel, DebugError, DebugMsg, DebugResponse, GdbTargetError, VcpuStopReason, +}; +#[cfg(gdb)] +use crate::hypervisor::hyperlight_vm::x86_64::debug::ProcessDebugRequestError; +#[cfg(not(gdb))] +use crate::hypervisor::virtual_machine::VirtualMachine; +use crate::hypervisor::virtual_machine::{ + MapMemoryError, RegisterError, RunVcpuError, UnmapMemoryError, VmError, VmExit, +}; +use crate::hypervisor::{InterruptHandle, InterruptHandleImpl}; +use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory, SharedMemory}; +use crate::metrics::{METRIC_ERRONEOUS_VCPU_KICKS, METRIC_GUEST_CANCELLATION}; +use crate::sandbox::host_funcs::FunctionRegistry; +use crate::sandbox::outb::{HandleOutbError, handle_outb}; +use crate::sandbox::snapshot::NextAction; +#[cfg(feature = "mem_profile")] +use crate::sandbox::trace::MemTraceInfo; +#[cfg(crashdump)] +use crate::sandbox::uninitialized::SandboxRuntimeConfig; + +// ── Shared helper functions (architecture-independent) ── + +/// Get the logging level filter to pass to the guest entrypoint +/// +/// The guest entrypoint uses this to determine the maximum log level to enable for the guest. +/// The `RUST_LOG` environment variable is expected to be in the format of comma-separated +/// key-value pairs, where the key is a log target (e.g., "hyperlight_guest_bin") and the value is +/// a log level (e.g., "debug"). +/// +/// NOTE: This prioritizes the log level for the targets containing "hyperlight_guest" string, then +/// "hyperlight_host", and then general log level. If none of these targets are found, it +/// defaults to "error". +fn get_max_log_level_filter(rust_log: String) -> LevelFilter { + let level_str = rust_log + .split(',') + .find_map(|part| { + let mut kv = part.splitn(2, '='); + match (kv.next(), kv.next()) { + (Some(k), Some(v)) if k.trim().contains("hyperlight_guest") => Some(v.trim()), + _ => None, + } + }) + .or_else(|| { + rust_log.split(',').find_map(|part| { + let mut kv = part.splitn(2, '='); + match (kv.next(), kv.next()) { + (Some(k), Some(v)) if k.trim().contains("hyperlight_host") => Some(v.trim()), + _ => None, + } + }) + }) + .or_else(|| { + rust_log.split(',').find_map(|part| { + if part.contains("=") { + None + } else { + Some(part.trim()) + } + }) + }) + .unwrap_or(""); + + tracing::info!("Determined guest log level: {}", level_str); + LevelFilter::from_str(level_str).unwrap_or(LevelFilter::ERROR) +} + +/// Converts a given [`Option`] to a `u64` value to be passed to the guest entrypoint +pub(super) fn get_guest_log_filter(guest_max_log_level: Option) -> u64 { + let guest_log_level_filter = match guest_max_log_level { + Some(level) => level, + None => get_max_log_level_filter(std::env::var("RUST_LOG").unwrap_or_default()), + }; + GuestLogFilter::from(guest_log_level_filter).into() +} + +// ── Error types (architecture-independent) ── + +/// DispatchGuestCall error +#[derive(Debug, thiserror::Error)] +pub enum DispatchGuestCallError { + #[error("Failed to run vm: {0}")] + Run(#[from] RunVmError), + #[error("Failed to setup registers: {0}")] + SetupRegs(RegisterError), + #[error("VM was uninitialized")] + Uninitialized, +} + +impl DispatchGuestCallError { + /// Returns true if this error should poison the sandbox + pub(crate) fn is_poison_error(&self) -> bool { + match self { + DispatchGuestCallError::Run(_) => true, + DispatchGuestCallError::SetupRegs(_) | DispatchGuestCallError::Uninitialized => false, + } + } + + /// Converts a `DispatchGuestCallError` to a `HyperlightError`. Used for backwards compatibility. + /// Also determines if the sandbox should be poisoned. + /// + /// Returns a tuple of (error, should_poison) where should_poison indicates whether + /// the sandbox should be marked as poisoned due to incomplete guest execution. + pub(crate) fn promote(self) -> (HyperlightError, bool) { + let should_poison = self.is_poison_error(); + let promoted_error = match self { + DispatchGuestCallError::Run(RunVmError::ExecutionCancelledByHost) => { + HyperlightError::ExecutionCanceledByHost() + } + + DispatchGuestCallError::Run(RunVmError::HandleIo(HandleIoError::Outb( + HandleOutbError::GuestAborted { code, message }, + ))) => HyperlightError::GuestAborted(code, message), + + DispatchGuestCallError::Run(RunVmError::MemoryAccessViolation { + addr, + access_type, + region_flags, + }) => HyperlightError::MemoryAccessViolation(addr, access_type, region_flags), + + // Leave others as is + other => HyperlightVmError::DispatchGuestCall(other).into(), + }; + (promoted_error, should_poison) + } +} + +/// Initialize error +#[derive(Debug, thiserror::Error)] +pub enum InitializeError { + #[error("Failed to convert pointer: {0}")] + ConvertPointer(String), + #[error("Failed to run vm: {0}")] + Run(#[from] RunVmError), + #[error("Failed to setup registers: {0}")] + SetupRegs(#[from] RegisterError), + #[error("Guest initialised stack pointer to architecturally invalid value: {0}")] + InvalidStackPointer(u64), +} + +/// Errors that can occur during VM execution in the run loop +#[derive(Debug, thiserror::Error)] +pub enum RunVmError { + #[cfg(crashdump)] + #[error("Crashdump generation error: {0}")] + CrashdumpGeneration(Box), + #[cfg(gdb)] + #[error("Debug handler error: {0}")] + DebugHandler(#[from] HandleDebugError), + #[error("Execution was cancelled by the host")] + ExecutionCancelledByHost, + #[error("Failed to access page: {0}")] + PageTableAccess(AccessPageTableError), + #[cfg(feature = "trace_guest")] + #[error("Failed to get registers: {0}")] + GetRegs(RegisterError), + #[error("IO handling error: {0}")] + HandleIo(#[from] HandleIoError), + #[error( + "Memory access violation at address {addr:#x}: {access_type} access, but memory is marked as {region_flags}" + )] + MemoryAccessViolation { + addr: u64, + access_type: MemoryRegionFlags, + region_flags: MemoryRegionFlags, + }, + #[error("MMIO READ access to unmapped address {0:#x}")] + MmioReadUnmapped(u64), + #[error("MMIO WRITE access to unmapped address {0:#x}")] + MmioWriteUnmapped(u64), + #[error("vCPU run failed: {0}")] + RunVcpu(#[from] RunVcpuError), + #[error("Unexpected VM exit: {0}")] + UnexpectedVmExit(String), + #[cfg(gdb)] + #[error("vCPU stop reason error: {0}")] + VcpuStopReason(#[from] VcpuStopReasonError), +} + +/// Errors that can occur during IO (outb) handling +#[derive(Debug, thiserror::Error)] +pub enum HandleIoError { + #[cfg(feature = "mem_profile")] + #[error("Failed to get registers: {0}")] + GetRegs(RegisterError), + #[error("No data was given in IO interrupt")] + NoData, + #[error("{0}")] + Outb(#[from] HandleOutbError), +} + +/// Errors that can occur when mapping a memory region +#[derive(Debug, thiserror::Error)] +pub enum MapRegionError { + #[error("VM map memory error: {0}")] + MapMemory(#[from] MapMemoryError), + #[error("Region is not page-aligned (page size: {0:#x})")] + NotPageAligned(usize), +} + +/// Errors that can occur when unmapping a memory region +#[derive(Debug, thiserror::Error)] +pub enum UnmapRegionError { + #[error("Region not found in mapped regions")] + RegionNotFound, + #[error("VM unmap memory error: {0}")] + UnmapMemory(#[from] UnmapMemoryError), +} + +/// Errors that can occur when updating the scratch mapping +#[derive(Debug, thiserror::Error)] +pub enum UpdateRegionError { + #[error("VM map memory error: {0}")] + MapMemory(#[from] MapMemoryError), + #[error("VM unmap memory error: {0}")] + UnmapMemory(#[from] UnmapMemoryError), +} + +/// Errors that can occur when accessing the root page table state +#[derive(Debug, thiserror::Error)] +pub enum AccessPageTableError { + #[error("Failed to get/set registers: {0}")] + AccessRegs(#[from] RegisterError), +} + +#[cfg(crashdump)] +#[derive(Debug, thiserror::Error)] +pub enum CrashDumpError { + #[error("Failed to generate crashdump because of a register error: {0}")] + GetRegs(#[from] RegisterError), + #[error("Failed to get root PT during crashdump generation: {0}")] + GetRootPt(#[from] AccessPageTableError), + #[error("Failed to get guest memory mapping during crashdump generation: {0}")] + AccessPageTable(Box), +} + +/// Errors that can occur during HyperlightVm creation +#[derive(Debug, thiserror::Error)] +pub enum CreateHyperlightVmError { + #[cfg(gdb)] + #[error("Failed to add hardware breakpoint: {0}")] + AddHwBreakpoint(DebugError), + #[error("No hypervisor was found")] + NoHypervisorFound, + #[cfg(gdb)] + #[error("Failed to send debug message: {0}")] + SendDbgMsg(#[from] SendDbgMsgError), + #[error("VM operation error: {0}")] + Vm(#[from] VmError), + #[error("Set scratch error: {0}")] + UpdateRegion(#[from] UpdateRegionError), +} + +/// Errors that can occur during debug exit handling +#[cfg(gdb)] +#[derive(Debug, thiserror::Error)] +pub enum HandleDebugError { + #[error("Debug is not enabled")] + DebugNotEnabled, + #[error("Error processing debug request: {0}")] + ProcessRequest(#[from] ProcessDebugRequestError), + #[error("Failed to receive message from GDB thread: {0}")] + ReceiveMessage(#[from] RecvDbgMsgError), + #[error("Failed to send message to GDB thread: {0}")] + SendMessage(#[from] SendDbgMsgError), +} + +/// Errors that can occur when sending a debug message +#[cfg(gdb)] +#[derive(Debug, thiserror::Error)] +pub enum SendDbgMsgError { + #[error("Debug is not enabled")] + DebugNotEnabled, + #[error("Failed to send message: {0}")] + SendFailed(#[from] GdbTargetError), +} + +/// Errors that can occur when receiving a debug message +#[cfg(gdb)] +#[derive(Debug, thiserror::Error)] +pub enum RecvDbgMsgError { + #[error("Debug is not enabled")] + DebugNotEnabled, + #[error("Failed to receive message: {0}")] + RecvFailed(#[from] GdbTargetError), +} + +/// Unified error type for all HyperlightVm operations +#[derive(Debug, thiserror::Error)] +pub enum HyperlightVmError { + #[error("Create VM error: {0}")] + Create(#[from] CreateHyperlightVmError), + #[error("Dispatch guest call error: {0}")] + DispatchGuestCall(#[from] DispatchGuestCallError), + #[error("Initialize error: {0}")] + Initialize(#[from] InitializeError), + #[error("Map region error: {0}")] + MapRegion(#[from] MapRegionError), + #[error("Restore VM (vcpu) error: {0}")] + Restore(#[from] RegisterError), + #[error("Unmap region error: {0}")] + UnmapRegion(#[from] UnmapRegionError), + #[error("Update region error: {0}")] + UpdateRegion(#[from] UpdateRegionError), + #[error("Access page table error: {0}")] + AccessPageTable(#[from] AccessPageTableError), +} + +// ── Struct definition (shared across architectures) ── + +/// Represents a Hyperlight Virtual Machine instance. +/// +/// This struct manages the lifecycle of the VM, including: +/// - The underlying hypervisor implementation (e.g., KVM, MSHV, WHP). +/// - Memory management, including initial sandbox regions and dynamic mappings. +/// - The vCPU execution loop and handling of VM exits (I/O, MMIO, interrupts). +pub(crate) struct HyperlightVm { + #[cfg(gdb)] + pub(super) vm: Box, + #[cfg(not(gdb))] + pub(super) vm: Box, + pub(super) page_size: usize, + pub(super) entrypoint: NextAction, + pub(super) rsp_gva: u64, + pub(super) interrupt_handle: Arc, + + pub(super) next_slot: u32, + pub(super) freed_slots: Vec, + + pub(super) snapshot_slot: u32, + pub(super) snapshot_memory: Option, + pub(super) scratch_slot: u32, + pub(super) scratch_memory: Option, + + pub(super) mmap_regions: Vec<(u32, MemoryRegion)>, + + pub(super) pending_tlb_flush: bool, + + #[cfg(gdb)] + pub(super) gdb_conn: Option>, + #[cfg(gdb)] + pub(super) sw_breakpoints: HashMap, + #[cfg(feature = "mem_profile")] + pub(super) trace_info: MemTraceInfo, + #[cfg(crashdump)] + pub(super) rt_cfg: SandboxRuntimeConfig, +} + +// ── Shared methods (architecture-independent) ── + +impl HyperlightVm { + /// Map a region of host memory into the sandbox. + /// + /// Safety: The caller must ensure that the region points to valid memory and + /// that the memory is valid for the duration of Self's lifetime. + /// Depending on the host platform, there are likely alignment + /// requirements of at least one page for base and len. + pub(crate) unsafe fn map_region( + &mut self, + region: &MemoryRegion, + ) -> std::result::Result<(), MapRegionError> { + if [ + region.guest_region.start, + region.guest_region.end, + #[allow(clippy::useless_conversion)] + region.host_region.start.into(), + #[allow(clippy::useless_conversion)] + region.host_region.end.into(), + ] + .iter() + .any(|x| x % self.page_size != 0) + { + return Err(MapRegionError::NotPageAligned(self.page_size)); + } + + let slot = if let Some(freed_slot) = self.freed_slots.pop() { + freed_slot + } else { + let slot = self.next_slot; + self.next_slot += 1; + slot + }; + + // Safety: slots are unique. It's up to caller to ensure that the region is valid + unsafe { self.vm.map_memory((slot, region))? }; + self.mmap_regions.push((slot, region.clone())); + Ok(()) + } + + /// Unmap a memory region from the sandbox + pub(crate) fn unmap_region( + &mut self, + region: &MemoryRegion, + ) -> std::result::Result<(), UnmapRegionError> { + let pos = self + .mmap_regions + .iter() + .position(|(_, r)| r == region) + .ok_or(UnmapRegionError::RegionNotFound)?; + + let (slot, _) = self.mmap_regions.remove(pos); + self.freed_slots.push(slot); + self.vm.unmap_memory((slot, region))?; + Ok(()) + } + + /// Get the currently mapped dynamic memory regions (not including initial sandbox region) + pub(crate) fn get_mapped_regions(&self) -> impl Iterator + Clone { + self.mmap_regions.iter().map(|(_, region)| region) + } + + /// Update the snapshot mapping to point to a new GuestSharedMemory + pub(crate) fn update_snapshot_mapping( + &mut self, + snapshot: GuestSharedMemory, + ) -> Result<(), UpdateRegionError> { + let guest_base = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64; + let rgn = snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); + + if let Some(old_snapshot) = self.snapshot_memory.replace(snapshot) { + let old_rgn = old_snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); + self.vm.unmap_memory((self.snapshot_slot, &old_rgn))?; + } + unsafe { self.vm.map_memory((self.snapshot_slot, &rgn))? }; + + Ok(()) + } + + /// Update the scratch mapping to point to a new GuestSharedMemory + pub(crate) fn update_scratch_mapping( + &mut self, + scratch: GuestSharedMemory, + ) -> Result<(), UpdateRegionError> { + let guest_base = hyperlight_common::layout::scratch_base_gpa(scratch.mem_size()); + let rgn = scratch.mapping_at(guest_base, MemoryRegionType::Scratch); + + if let Some(old_scratch) = self.scratch_memory.replace(scratch) { + let old_base = hyperlight_common::layout::scratch_base_gpa(old_scratch.mem_size()); + let old_rgn = old_scratch.mapping_at(old_base, MemoryRegionType::Scratch); + self.vm.unmap_memory((self.scratch_slot, &old_rgn))?; + } + unsafe { self.vm.map_memory((self.scratch_slot, &rgn))? }; + + Ok(()) + } + + /// Get the current stack top virtual address + pub(crate) fn get_stack_top(&mut self) -> u64 { + self.rsp_gva + } + + /// Set the current stack top virtual address + pub(crate) fn set_stack_top(&mut self, gva: u64) { + self.rsp_gva = gva; + } + + /// Get the current entrypoint action + pub(crate) fn get_entrypoint(&self) -> NextAction { + self.entrypoint + } + + /// Set the current entrypoint action + pub(crate) fn set_entrypoint(&mut self, entrypoint: NextAction) { + self.entrypoint = entrypoint + } + + pub(crate) fn interrupt_handle(&self) -> Arc { + self.interrupt_handle.clone() + } + + pub(crate) fn clear_cancel(&self) { + self.interrupt_handle.clear_cancel(); + } + + /// The main vCPU execution loop. Runs the vCPU until halt, cancellation, or error. + pub(super) fn run( + &mut self, + mem_mgr: &mut SandboxMemoryManager, + host_funcs: &Arc>, + #[cfg(gdb)] dbg_mem_access_fn: Arc>>, + ) -> std::result::Result<(), RunVmError> { + #[cfg(feature = "trace_guest")] + let mut tc = crate::sandbox::trace::TraceContext::new(); + + let result = loop { + #[cfg(any(kvm, mshv3))] + self.interrupt_handle.set_tid(); + self.interrupt_handle.set_running(); + + let exit_reason = if self.interrupt_handle.is_cancelled() + || self.interrupt_handle.is_debug_interrupted() + { + Ok(VmExit::Cancelled()) + } else { + let result = self.vm.run_vcpu( + #[cfg(feature = "trace_guest")] + &mut tc, + ); + + #[cfg(feature = "trace_guest")] + { + tc.end_host_trace(); + let regs = self.vm.regs().map_err(RunVmError::GetRegs)?; + + if tc.has_trace_data(®s) { + let root_pt = self.get_root_pt().map_err(RunVmError::PageTableAccess)?; + + tc.handle_trace(®s, mem_mgr, root_pt) + .unwrap_or_else(|e| { + tracing::error!("Cannot handle trace data: {}", e); + }); + } + } + result + }; + + self.interrupt_handle.clear_running(); + + let cancel_requested = self.interrupt_handle.is_cancelled(); + let debug_interrupted = self.interrupt_handle.is_debug_interrupted(); + + match exit_reason { + #[cfg(gdb)] + Ok(VmExit::Debug { dr6, exception }) => { + let initialise = match self.entrypoint { + NextAction::Initialise(initialise) => initialise, + _ => 0, + }; + let stop_reason = crate::hypervisor::gdb::arch::vcpu_stop_reason( + self.vm.as_mut(), + dr6, + initialise, + exception, + )?; + if let Err(e) = self.handle_debug(dbg_mem_access_fn.clone(), stop_reason) { + break Err(e.into()); + } + } + + Ok(VmExit::Halt()) => { + break Ok(()); + } + Ok(VmExit::IoOut(port, data)) => { + self.handle_io(mem_mgr, host_funcs, port, data)?; + } + Ok(VmExit::MmioRead(addr)) => { + let all_regions = self.get_mapped_regions(); + match get_memory_access_violation( + addr as usize, + MemoryRegionFlags::WRITE, + all_regions, + ) { + Some(MemoryAccess::AccessViolation(region_flags)) => { + break Err(RunVmError::MemoryAccessViolation { + addr, + access_type: MemoryRegionFlags::READ, + region_flags, + }); + } + None => { + break Err(RunVmError::MmioReadUnmapped(addr)); + } + } + } + Ok(VmExit::MmioWrite(addr)) => { + let all_regions = self.get_mapped_regions(); + match get_memory_access_violation( + addr as usize, + MemoryRegionFlags::WRITE, + all_regions, + ) { + Some(MemoryAccess::AccessViolation(region_flags)) => { + break Err(RunVmError::MemoryAccessViolation { + addr, + access_type: MemoryRegionFlags::WRITE, + region_flags, + }); + } + None => { + break Err(RunVmError::MmioWriteUnmapped(addr)); + } + } + } + Ok(VmExit::Cancelled()) => { + if !cancel_requested && !debug_interrupted { + metrics::counter!(METRIC_ERRONEOUS_VCPU_KICKS).increment(1); + continue; + } + + #[cfg(gdb)] + { + self.interrupt_handle.clear_debug_interrupt(); + if let Err(e) = + self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Interrupt) + { + break Err(e.into()); + } + } + + metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); + break Err(RunVmError::ExecutionCancelledByHost); + } + Ok(VmExit::Unknown(reason)) => { + break Err(RunVmError::UnexpectedVmExit(reason)); + } + Ok(VmExit::Retry()) => continue, + Err(e) => { + break Err(RunVmError::RunVcpu(e)); + } + } + }; + + match result { + Ok(_) => Ok(()), + Err(RunVmError::ExecutionCancelledByHost) => Err(RunVmError::ExecutionCancelledByHost), + Err(e) => { + #[cfg(crashdump)] + if self.rt_cfg.guest_core_dump { + crate::hypervisor::crashdump::generate_crashdump(self, mem_mgr, None) + .map_err(|e| RunVmError::CrashdumpGeneration(Box::new(e)))?; + } + + #[cfg(gdb)] + if self.gdb_conn.is_some() { + self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Crash)? + } + Err(e) + } + } + } + + /// Handle an IO exit + fn handle_io( + &mut self, + mem_mgr: &mut SandboxMemoryManager, + host_funcs: &Arc>, + port: u16, + data: Vec, + ) -> std::result::Result<(), HandleIoError> { + if data.is_empty() { + return Err(HandleIoError::NoData); + } + + #[allow(clippy::get_first)] + let val = u32::from_le_bytes([ + data.get(0).copied().unwrap_or(0), + data.get(1).copied().unwrap_or(0), + data.get(2).copied().unwrap_or(0), + data.get(3).copied().unwrap_or(0), + ]); + + #[cfg(feature = "mem_profile")] + { + let regs = self.vm.regs().map_err(HandleIoError::GetRegs)?; + handle_outb(mem_mgr, host_funcs, port, val, ®s, &mut self.trace_info)?; + } + + #[cfg(not(feature = "mem_profile"))] + { + handle_outb(mem_mgr, host_funcs, port, val)?; + } + + Ok(()) + } +} + +impl Drop for HyperlightVm { + fn drop(&mut self) { + self.interrupt_handle.set_dropped(); + } +} + +/// The vCPU tried to access the given addr +enum MemoryAccess { + /// The accessed region has the given flags + AccessViolation(MemoryRegionFlags), +} + +/// Determines if a known memory access violation occurred at the given address with the given action type. +fn get_memory_access_violation<'a>( + gpa: usize, + tried: MemoryRegionFlags, + mut mem_regions: impl Iterator, +) -> Option { + let region = mem_regions.find(|region| region.guest_region.contains(&gpa))?; + if !region.flags.contains(tried) { + return Some(MemoryAccess::AccessViolation(region.flags)); + } + None +} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs similarity index 76% rename from src/hyperlight_host/src/hypervisor/hyperlight_vm.rs rename to src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index d3a210926..7dd4a3125 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -18,7 +18,6 @@ limitations under the License. use std::collections::HashMap; #[cfg(crashdump)] use std::path::Path; -use std::str::FromStr; #[cfg(any(kvm, mshv3))] use std::sync::atomic::AtomicBool; use std::sync::atomic::AtomicU8; @@ -26,29 +25,24 @@ use std::sync::atomic::AtomicU8; use std::sync::atomic::AtomicU64; use std::sync::{Arc, Mutex}; -use hyperlight_common::log_level::GuestLogFilter; use tracing::{Span, instrument}; use tracing_core::LevelFilter; -#[cfg(gdb)] -use super::gdb::arch::VcpuStopReasonError; -#[cfg(gdb)] -use super::gdb::{ - DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm, GdbTargetError, VcpuStopReason, arch, -}; -use super::regs::{CommonFpu, CommonRegisters}; -#[cfg(target_os = "windows")] -use super::{PartitionState, WindowsInterruptHandle}; -use crate::HyperlightError; +use super::*; +use crate::hypervisor::InterruptHandleImpl; #[cfg(any(kvm, mshv3))] use crate::hypervisor::LinuxInterruptHandle; #[cfg(crashdump)] use crate::hypervisor::crashdump; #[cfg(gdb)] -use crate::hypervisor::gdb::{DebugError, DebugMemoryAccessError}; +use crate::hypervisor::gdb::{ + DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm, VcpuStopReason, +}; #[cfg(gdb)] -use crate::hypervisor::hyperlight_vm::debug::ProcessDebugRequestError; -use crate::hypervisor::regs::{CommonDebugRegs, CommonSpecialRegisters}; +use crate::hypervisor::gdb::{DebugError, DebugMemoryAccessError}; +use crate::hypervisor::regs::{ + CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, +}; #[cfg(not(gdb))] use crate::hypervisor::virtual_machine::VirtualMachine; #[cfg(kvm)] @@ -58,361 +52,23 @@ use crate::hypervisor::virtual_machine::mshv::MshvVm; #[cfg(target_os = "windows")] use crate::hypervisor::virtual_machine::whp::WhpVm; use crate::hypervisor::virtual_machine::{ - HypervisorType, MapMemoryError, RegisterError, RunVcpuError, UnmapMemoryError, VmError, VmExit, - get_available_hypervisor, + HypervisorType, RegisterError, VmError, get_available_hypervisor, }; -use crate::hypervisor::{InterruptHandle, InterruptHandleImpl}; -use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +#[cfg(target_os = "windows")] +use crate::hypervisor::{PartitionState, WindowsInterruptHandle}; +#[cfg(crashdump)] +use crate::mem::memory_region::MemoryRegion; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::ptr::RawPtr; -use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory, SharedMemory}; -use crate::metrics::{METRIC_ERRONEOUS_VCPU_KICKS, METRIC_GUEST_CANCELLATION}; +use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; use crate::sandbox::SandboxConfiguration; use crate::sandbox::host_funcs::FunctionRegistry; -use crate::sandbox::outb::{HandleOutbError, handle_outb}; use crate::sandbox::snapshot::NextAction; #[cfg(feature = "mem_profile")] use crate::sandbox::trace::MemTraceInfo; #[cfg(crashdump)] use crate::sandbox::uninitialized::SandboxRuntimeConfig; -/// Get the logging level filter to pass to the guest entrypoint -/// -/// The guest entrypoint uses this to determine the maximum log level to enable for the guest. -/// The `RUST_LOG` environment variable is expected to be in the format of comma-separated -/// key-value pairs, where the key is a log target (e.g., "hyperlight_guest_bin") and the value is -/// a log level (e.g., "debug"). -/// -/// NOTE: This prioritizes the log level for the targets containing "hyperlight_guest" string, then -/// "hyperlight_host", and then general log level. If none of these targets are found, it -/// defaults to "error". -fn get_max_log_level_filter(rust_log: String) -> LevelFilter { - // This is done as the guest will produce logs based on the log level returned here - // producing those logs is expensive and we don't want to do it if the host is not - // going to process them - let level_str = rust_log - .split(',') - // Prioritize targets containing "hyperlight_guest" - .find_map(|part| { - let mut kv = part.splitn(2, '='); - match (kv.next(), kv.next()) { - (Some(k), Some(v)) if k.trim().contains("hyperlight_guest") => Some(v.trim()), - _ => None, - } - }) - // Then check for "hyperlight_host" - .or_else(|| { - rust_log.split(',').find_map(|part| { - let mut kv = part.splitn(2, '='); - match (kv.next(), kv.next()) { - (Some(k), Some(v)) if k.trim().contains("hyperlight_host") => Some(v.trim()), - _ => None, - } - }) - }) - // Finally, check for general log level - .or_else(|| { - rust_log.split(',').find_map(|part| { - if part.contains("=") { - None - } else { - Some(part.trim()) - } - }) - }) - .unwrap_or(""); - - tracing::info!("Determined guest log level: {}", level_str); - - // If no value is found, default to Error - LevelFilter::from_str(level_str).unwrap_or(LevelFilter::ERROR) -} - -/// Converts a given [`Option`] to a `u64` value to be passed to the guest entrypoint -/// If the provided filter is `None`, it uses the `RUST_LOG` environment variable to determine the -/// maximum log level filter for the guest and converts it to a `u64` value. -fn get_guest_log_filter(guest_max_log_level: Option) -> u64 { - let guest_log_level_filter = match guest_max_log_level { - Some(level) => level, - None => get_max_log_level_filter(std::env::var("RUST_LOG").unwrap_or_default()), - }; - GuestLogFilter::from(guest_log_level_filter).into() -} - -/// Represents a Hyperlight Virtual Machine instance. -/// -/// This struct manages the lifecycle of the VM, including: -/// - The underlying hypervisor implementation (e.g., KVM, MSHV, WHP). -/// - Memory management, including initial sandbox regions and dynamic mappings. -/// - The vCPU execution loop and handling of VM exits (I/O, MMIO, interrupts). -pub(crate) struct HyperlightVm { - #[cfg(gdb)] - vm: Box, - #[cfg(not(gdb))] - vm: Box, - page_size: usize, - entrypoint: NextAction, // only present if this vm has not yet been initialised - rsp_gva: u64, - interrupt_handle: Arc, - - next_slot: u32, // Monotonically increasing slot number - freed_slots: Vec, // Reusable slots from unmapped regions - - snapshot_slot: u32, - // The current snapshot region, used to keep it alive as long as - // it is used & when unmapping - snapshot_memory: Option, - scratch_slot: u32, // The slot number used for the scratch region - // The current scratch region, used to keep it alive as long as it - // is used & when unmapping - scratch_memory: Option, - - mmap_regions: Vec<(u32, MemoryRegion)>, // Later mapped regions (slot number, region) - - pending_tlb_flush: bool, - - #[cfg(gdb)] - gdb_conn: Option>, - #[cfg(gdb)] - sw_breakpoints: HashMap, // addr -> original instruction - #[cfg(feature = "mem_profile")] - trace_info: MemTraceInfo, - #[cfg(crashdump)] - rt_cfg: SandboxRuntimeConfig, -} - -/// DispatchGuestCall error -#[derive(Debug, thiserror::Error)] -pub enum DispatchGuestCallError { - #[error("Failed to run vm: {0}")] - Run(#[from] RunVmError), - #[error("Failed to setup registers: {0}")] - SetupRegs(RegisterError), - #[error("VM was uninitialized")] - Uninitialized, -} - -impl DispatchGuestCallError { - /// Returns true if this error should poison the sandbox - pub(crate) fn is_poison_error(&self) -> bool { - match self { - // These errors poison the sandbox because they can leave it in an inconsistent state - // by returning before the guest can unwind properly - DispatchGuestCallError::Run(_) => true, - DispatchGuestCallError::SetupRegs(_) | DispatchGuestCallError::Uninitialized => false, - } - } - - /// Converts a `DispatchGuestCallError` to a `HyperlightError`. Used for backwards compatibility. - /// Also determines if the sandbox should be poisoned. - /// - /// Returns a tuple of (error, should_poison) where should_poison indicates whether - /// the sandbox should be marked as poisoned due to incomplete guest execution. - pub(crate) fn promote(self) -> (HyperlightError, bool) { - let should_poison = self.is_poison_error(); - let promoted_error = match self { - DispatchGuestCallError::Run(RunVmError::ExecutionCancelledByHost) => { - HyperlightError::ExecutionCanceledByHost() - } - - DispatchGuestCallError::Run(RunVmError::HandleIo(HandleIoError::Outb( - HandleOutbError::GuestAborted { code, message }, - ))) => HyperlightError::GuestAborted(code, message), - - DispatchGuestCallError::Run(RunVmError::MemoryAccessViolation { - addr, - access_type, - region_flags, - }) => HyperlightError::MemoryAccessViolation(addr, access_type, region_flags), - - // Leave others as is - other => HyperlightVmError::DispatchGuestCall(other).into(), - }; - (promoted_error, should_poison) - } -} - -/// Initialize error -#[derive(Debug, thiserror::Error)] -pub enum InitializeError { - #[error("Failed to convert pointer: {0}")] - ConvertPointer(String), - #[error("Failed to run vm: {0}")] - Run(#[from] RunVmError), - #[error("Failed to setup registers: {0}")] - SetupRegs(#[from] RegisterError), - #[error("Guest initialised stack pointer to architecturally invalid value: {0}")] - InvalidStackPointer(u64), -} - -/// Errors that can occur during VM execution in the run loop -#[derive(Debug, thiserror::Error)] -pub enum RunVmError { - #[cfg(crashdump)] - #[error("Crashdump generation error: {0}")] - CrashdumpGeneration(Box), - #[cfg(gdb)] - #[error("Debug handler error: {0}")] - DebugHandler(#[from] HandleDebugError), - #[error("Execution was cancelled by the host")] - ExecutionCancelledByHost, - #[error("Failed to access page: {0}")] - PageTableAccess(AccessPageTableError), - #[cfg(feature = "trace_guest")] - #[error("Failed to get registers: {0}")] - GetRegs(RegisterError), - #[error("IO handling error: {0}")] - HandleIo(#[from] HandleIoError), - #[error( - "Memory access violation at address {addr:#x}: {access_type} access, but memory is marked as {region_flags}" - )] - MemoryAccessViolation { - addr: u64, - access_type: MemoryRegionFlags, - region_flags: MemoryRegionFlags, - }, - #[error("MMIO READ access to unmapped address {0:#x}")] - MmioReadUnmapped(u64), - #[error("MMIO WRITE access to unmapped address {0:#x}")] - MmioWriteUnmapped(u64), - #[error("vCPU run failed: {0}")] - RunVcpu(#[from] RunVcpuError), - #[error("Unexpected VM exit: {0}")] - UnexpectedVmExit(String), - #[cfg(gdb)] - #[error("vCPU stop reason error: {0}")] - VcpuStopReason(#[from] VcpuStopReasonError), -} - -/// Errors that can occur during IO (outb) handling -#[derive(Debug, thiserror::Error)] -pub enum HandleIoError { - #[cfg(feature = "mem_profile")] - #[error("Failed to get registers: {0}")] - GetRegs(RegisterError), - #[error("No data was given in IO interrupt")] - NoData, - #[error("{0}")] - Outb(#[from] HandleOutbError), -} - -/// Errors that can occur when mapping a memory region -#[derive(Debug, thiserror::Error)] -pub enum MapRegionError { - #[error("VM map memory error: {0}")] - MapMemory(#[from] MapMemoryError), - #[error("Region is not page-aligned (page size: {0:#x})")] - NotPageAligned(usize), -} - -/// Errors that can occur when unmapping a memory region -#[derive(Debug, thiserror::Error)] -pub enum UnmapRegionError { - #[error("Region not found in mapped regions")] - RegionNotFound, - #[error("VM unmap memory error: {0}")] - UnmapMemory(#[from] UnmapMemoryError), -} - -/// Errors that can occur when updating the scratch mapping -#[derive(Debug, thiserror::Error)] -pub enum UpdateRegionError { - #[error("VM map memory error: {0}")] - MapMemory(#[from] MapMemoryError), - #[error("VM unmap memory error: {0}")] - UnmapMemory(#[from] UnmapMemoryError), -} - -/// Errors that can occur when accessing the root page table state -#[derive(Debug, thiserror::Error)] -pub enum AccessPageTableError { - #[error("Failed to get/set registers: {0}")] - AccessRegs(#[from] RegisterError), -} - -#[cfg(crashdump)] -#[derive(Debug, thiserror::Error)] -pub enum CrashDumpError { - #[error("Failed to generate crashdump because of a register error: {0}")] - GetRegs(#[from] RegisterError), - #[error("Failed to get root PT during crashdump generation: {0}")] - GetRootPt(#[from] AccessPageTableError), - #[error("Failed to get guest memory mapping during crashdump generation: {0}")] - AccessPageTable(Box), -} - -/// Errors that can occur during HyperlightVm creation -#[derive(Debug, thiserror::Error)] -pub enum CreateHyperlightVmError { - #[cfg(gdb)] - #[error("Failed to add hardware breakpoint: {0}")] - AddHwBreakpoint(DebugError), - #[error("No hypervisor was found")] - NoHypervisorFound, - #[cfg(gdb)] - #[error("Failed to send debug message: {0}")] - SendDbgMsg(#[from] SendDbgMsgError), - #[error("VM operation error: {0}")] - Vm(#[from] VmError), - #[error("Set scratch error: {0}")] - UpdateRegion(#[from] UpdateRegionError), -} - -/// Errors that can occur during debug exit handling -#[cfg(gdb)] -#[derive(Debug, thiserror::Error)] -pub enum HandleDebugError { - #[error("Debug is not enabled")] - DebugNotEnabled, - #[error("Error processing debug request: {0}")] - ProcessRequest(#[from] ProcessDebugRequestError), - #[error("Failed to receive message from GDB thread: {0}")] - ReceiveMessage(#[from] RecvDbgMsgError), - #[error("Failed to send message to GDB thread: {0}")] - SendMessage(#[from] SendDbgMsgError), -} - -/// Errors that can occur when sending a debug message -#[cfg(gdb)] -#[derive(Debug, thiserror::Error)] -pub enum SendDbgMsgError { - #[error("Debug is not enabled")] - DebugNotEnabled, - #[error("Failed to send message: {0}")] - SendFailed(#[from] GdbTargetError), -} - -/// Errors that can occur when receiving a debug message -#[cfg(gdb)] -#[derive(Debug, thiserror::Error)] -pub enum RecvDbgMsgError { - #[error("Debug is not enabled")] - DebugNotEnabled, - #[error("Failed to receive message: {0}")] - RecvFailed(#[from] GdbTargetError), -} - -/// Unified error type for all HyperlightVm operations -#[derive(Debug, thiserror::Error)] -pub enum HyperlightVmError { - #[error("Create VM error: {0}")] - Create(#[from] CreateHyperlightVmError), - #[error("Dispatch guest call error: {0}")] - DispatchGuestCall(#[from] DispatchGuestCallError), - #[error("Initialize error: {0}")] - Initialize(#[from] InitializeError), - #[error("Map region error: {0}")] - MapRegion(#[from] MapRegionError), - #[error("Restore VM (vcpu) error: {0}")] - Restore(#[from] RegisterError), - #[error("Unmap region error: {0}")] - UnmapRegion(#[from] UnmapRegionError), - #[error("Update region error: {0}")] - UpdateRegion(#[from] UpdateRegionError), - #[error("Access page table error: {0}")] - AccessPageTable(#[from] AccessPageTableError), -} - impl HyperlightVm { /// Create a new HyperlightVm instance (will not run vm until calling `initialise`) #[instrument(err(Debug), skip_all, parent = Span::current(), level = "Trace")] @@ -593,102 +249,6 @@ impl HyperlightVm { Ok(()) } - /// Map a region of host memory into the sandbox. - /// - /// Safety: The caller must ensure that the region points to valid memory and - /// that the memory is valid for the duration of Self's lifetime. - /// Depending on the host platform, there are likely alignment - /// requirements of at least one page for base and len. - pub(crate) unsafe fn map_region( - &mut self, - region: &MemoryRegion, - ) -> std::result::Result<(), MapRegionError> { - if [ - region.guest_region.start, - region.guest_region.end, - #[allow(clippy::useless_conversion)] - region.host_region.start.into(), - #[allow(clippy::useless_conversion)] - region.host_region.end.into(), - ] - .iter() - .any(|x| x % self.page_size != 0) - { - return Err(MapRegionError::NotPageAligned(self.page_size)); - } - - // Try to reuse a freed slot first, otherwise use next_slot - let slot = if let Some(freed_slot) = self.freed_slots.pop() { - freed_slot - } else { - let slot = self.next_slot; - self.next_slot += 1; - slot - }; - - // Safety: slots are unique. It's up to caller to ensure that the region is valid - unsafe { self.vm.map_memory((slot, region))? }; - self.mmap_regions.push((slot, region.clone())); - Ok(()) - } - - /// Unmap a memory region from the sandbox - pub(crate) fn unmap_region( - &mut self, - region: &MemoryRegion, - ) -> std::result::Result<(), UnmapRegionError> { - let pos = self - .mmap_regions - .iter() - .position(|(_, r)| r == region) - .ok_or(UnmapRegionError::RegionNotFound)?; - - let (slot, _) = self.mmap_regions.remove(pos); - self.freed_slots.push(slot); - self.vm.unmap_memory((slot, region))?; - Ok(()) - } - - /// Get the currently mapped dynamic memory regions (not including initial sandbox region) - pub(crate) fn get_mapped_regions(&self) -> impl Iterator { - self.mmap_regions.iter().map(|(_, region)| region) - } - - /// Update the snapshot mapping to point to a new GuestSharedMemory - pub(crate) fn update_snapshot_mapping( - &mut self, - snapshot: GuestSharedMemory, - ) -> Result<(), UpdateRegionError> { - let guest_base = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64; - let rgn = snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); - - if let Some(old_snapshot) = self.snapshot_memory.replace(snapshot) { - let old_rgn = old_snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); - self.vm.unmap_memory((self.snapshot_slot, &old_rgn))?; - } - unsafe { self.vm.map_memory((self.snapshot_slot, &rgn))? }; - - Ok(()) - } - - /// Update the scratch mapping to point to a new GuestSharedMemory - pub(crate) fn update_scratch_mapping( - &mut self, - scratch: GuestSharedMemory, - ) -> Result<(), UpdateRegionError> { - let guest_base = hyperlight_common::layout::scratch_base_gpa(scratch.mem_size()); - let rgn = scratch.mapping_at(guest_base, MemoryRegionType::Scratch); - - if let Some(old_scratch) = self.scratch_memory.replace(scratch) { - let old_base = hyperlight_common::layout::scratch_base_gpa(old_scratch.mem_size()); - let old_rgn = old_scratch.mapping_at(old_base, MemoryRegionType::Scratch); - self.vm.unmap_memory((self.scratch_slot, &old_rgn))?; - } - unsafe { self.vm.map_memory((self.scratch_slot, &rgn))? }; - - Ok(()) - } - /// Get the current base page table physical address. /// /// With `init-paging`, reads CR3 from the vCPU special registers. @@ -713,26 +273,6 @@ impl HyperlightVm { Ok(self.vm.sregs()?) } - /// Get the current stack top virtual address - pub(crate) fn get_stack_top(&mut self) -> u64 { - self.rsp_gva - } - - /// Set the current stack top virtual address - pub(crate) fn set_stack_top(&mut self, gva: u64) { - self.rsp_gva = gva; - } - - /// Get the current entrypoint action - pub(crate) fn get_entrypoint(&self) -> NextAction { - self.entrypoint - } - - /// Set the current entrypoint action - pub(crate) fn set_entrypoint(&mut self, entrypoint: NextAction) { - self.entrypoint = entrypoint - } - /// Dispatch a call from the host to the guest using the given pointer /// to the dispatch function _in the guest's address space_. /// @@ -789,242 +329,6 @@ impl HyperlightVm { .map_err(DispatchGuestCallError::Run) } - pub(crate) fn interrupt_handle(&self) -> Arc { - self.interrupt_handle.clone() - } - - pub(crate) fn clear_cancel(&self) { - self.interrupt_handle.clear_cancel(); - } - - fn run( - &mut self, - mem_mgr: &mut SandboxMemoryManager, - host_funcs: &Arc>, - #[cfg(gdb)] dbg_mem_access_fn: Arc>>, - ) -> std::result::Result<(), RunVmError> { - // Keeps the trace context and open spans - #[cfg(feature = "trace_guest")] - let mut tc = crate::sandbox::trace::TraceContext::new(); - - let result = loop { - // ===== KILL() TIMING POINT 2: Before set_tid() ===== - // If kill() is called and ran to completion BEFORE this line executes: - // - CANCEL_BIT will be set and we will return an early VmExit::Cancelled() - // without sending any signals/WHV api calls - #[cfg(any(kvm, mshv3))] - self.interrupt_handle.set_tid(); - self.interrupt_handle.set_running(); - // NOTE: `set_running()`` must be called before checking `is_cancelled()` - // otherwise we risk missing a call to `kill()` because the vcpu would not be marked as running yet so signals won't be sent - - let exit_reason = if self.interrupt_handle.is_cancelled() - || self.interrupt_handle.is_debug_interrupted() - { - Ok(VmExit::Cancelled()) - } else { - // ==== KILL() TIMING POINT 3: Before calling run() ==== - // If kill() is called and ran to completion BEFORE this line executes: - // - Will still do a VM entry, but signals will be sent until VM exits - let result = self.vm.run_vcpu( - #[cfg(feature = "trace_guest")] - &mut tc, - ); - - // End current host trace by closing the current span that captures traces - // happening when a guest exits and re-enters. - #[cfg(feature = "trace_guest")] - { - tc.end_host_trace(); - // Handle the guest trace data if any - let regs = self.vm.regs().map_err(RunVmError::GetRegs)?; - - // Only parse the trace if it has reported - if tc.has_trace_data(®s) { - let root_pt = self.get_root_pt().map_err(RunVmError::PageTableAccess)?; - - // If something goes wrong with parsing the trace data, we log the error and - // continue execution instead of returning an error since this is not critical - // to correct execution of the guest - tc.handle_trace(®s, mem_mgr, root_pt) - .unwrap_or_else(|e| { - tracing::error!("Cannot handle trace data: {}", e); - }); - } - } - result - }; - - // ===== KILL() TIMING POINT 4: Before clear_running() ===== - // If kill() is called and ran to completion BEFORE this line executes: - // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. - // - Signals will be sent until `clear_running()` is called, which is ok - self.interrupt_handle.clear_running(); - - // ===== KILL() TIMING POINT 5: Before capturing cancel_requested ===== - // If kill() is called and ran to completion BEFORE this line executes: - // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. - // - Signals will not be sent - let cancel_requested = self.interrupt_handle.is_cancelled(); - let debug_interrupted = self.interrupt_handle.is_debug_interrupted(); - - // ===== KILL() TIMING POINT 6: Before checking exit_reason ===== - // If kill() is called and ran to completion BEFORE this line executes: - // - CANCEL_BIT will be set. Cancellation is deferred to the next iteration. - // - Signals will not be sent - match exit_reason { - #[cfg(gdb)] - Ok(VmExit::Debug { dr6, exception }) => { - let initialise = match self.entrypoint { - NextAction::Initialise(initialise) => initialise, - _ => 0, - }; - // Handle debug event (breakpoints) - let stop_reason = - arch::vcpu_stop_reason(self.vm.as_mut(), dr6, initialise, exception)?; - if let Err(e) = self.handle_debug(dbg_mem_access_fn.clone(), stop_reason) { - break Err(e.into()); - } - } - - Ok(VmExit::Halt()) => { - break Ok(()); - } - Ok(VmExit::IoOut(port, data)) => { - self.handle_io(mem_mgr, host_funcs, port, data)?; - } - Ok(VmExit::MmioRead(addr)) => { - let all_regions = self.get_mapped_regions(); - match get_memory_access_violation( - addr as usize, - MemoryRegionFlags::WRITE, - all_regions, - ) { - Some(MemoryAccess::AccessViolation(region_flags)) => { - break Err(RunVmError::MemoryAccessViolation { - addr, - access_type: MemoryRegionFlags::READ, - region_flags, - }); - } - None => { - break Err(RunVmError::MmioReadUnmapped(addr)); - } - } - } - Ok(VmExit::MmioWrite(addr)) => { - let all_regions = self.get_mapped_regions(); - match get_memory_access_violation( - addr as usize, - MemoryRegionFlags::WRITE, - all_regions, - ) { - Some(MemoryAccess::AccessViolation(region_flags)) => { - break Err(RunVmError::MemoryAccessViolation { - addr, - access_type: MemoryRegionFlags::WRITE, - region_flags, - }); - } - None => { - break Err(RunVmError::MmioWriteUnmapped(addr)); - } - } - } - Ok(VmExit::Cancelled()) => { - // If cancellation was not requested for this specific guest function call, - // the vcpu was interrupted by a stale cancellation. This can occur when: - // - Linux: A signal from a previous call arrives late - // - Windows: WHvCancelRunVirtualProcessor called right after vcpu exits but RUNNING_BIT is still true - if !cancel_requested && !debug_interrupted { - // Track that an erroneous vCPU kick occurred - metrics::counter!(METRIC_ERRONEOUS_VCPU_KICKS).increment(1); - // treat this the same as a VmExit::Retry, the cancel was not meant for this call - continue; - } - - // If the vcpu was interrupted by a debugger, we need to handle it - #[cfg(gdb)] - { - self.interrupt_handle.clear_debug_interrupt(); - if let Err(e) = - self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Interrupt) - { - break Err(e.into()); - } - } - - metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); - break Err(RunVmError::ExecutionCancelledByHost); - } - Ok(VmExit::Unknown(reason)) => { - break Err(RunVmError::UnexpectedVmExit(reason)); - } - Ok(VmExit::Retry()) => continue, - Err(e) => { - break Err(RunVmError::RunVcpu(e)); - } - } - }; - - match result { - Ok(_) => Ok(()), - Err(RunVmError::ExecutionCancelledByHost) => { - // no need to crashdump this - Err(RunVmError::ExecutionCancelledByHost) - } - Err(e) => { - #[cfg(crashdump)] - if self.rt_cfg.guest_core_dump { - crashdump::generate_crashdump(self, mem_mgr, None) - .map_err(|e| RunVmError::CrashdumpGeneration(Box::new(e)))?; - } - - // If GDB is enabled, we handle the debug memory access - // Disregard return value as we want to return the error - #[cfg(gdb)] - if self.gdb_conn.is_some() { - self.handle_debug(dbg_mem_access_fn.clone(), VcpuStopReason::Crash)? - } - Err(e) - } - } - } - - /// Handle an IO exit - fn handle_io( - &mut self, - mem_mgr: &mut SandboxMemoryManager, - host_funcs: &Arc>, - port: u16, - data: Vec, - ) -> std::result::Result<(), HandleIoError> { - if data.is_empty() { - return Err(HandleIoError::NoData); - } - - #[allow(clippy::get_first)] - let val = u32::from_le_bytes([ - data.get(0).copied().unwrap_or(0), - data.get(1).copied().unwrap_or(0), - data.get(2).copied().unwrap_or(0), - data.get(3).copied().unwrap_or(0), - ]); - - #[cfg(feature = "mem_profile")] - { - let regs = self.vm.regs().map_err(HandleIoError::GetRegs)?; - handle_outb(mem_mgr, host_funcs, port, val, ®s, &mut self.trace_info)?; - } - - #[cfg(not(feature = "mem_profile"))] - { - handle_outb(mem_mgr, host_funcs, port, val)?; - } - - Ok(()) - } - /// Resets the following vCPU state: /// - General purpose registers /// - Debug registers @@ -1066,13 +370,14 @@ impl HyperlightVm { // Handle a debug exit #[cfg(gdb)] - fn handle_debug( + pub(super) fn handle_debug( &mut self, dbg_mem_access_fn: Arc>>, stop_reason: VcpuStopReason, ) -> std::result::Result<(), HandleDebugError> { + use debug::ProcessDebugRequestError; + use crate::hypervisor::gdb::DebugMemoryAccess; - use crate::hypervisor::hyperlight_vm::debug::ProcessDebugRequestError; if self.gdb_conn.is_none() { return Err(HandleDebugError::DebugNotEnabled); @@ -1211,7 +516,8 @@ impl HyperlightVm { pub(crate) fn crashdump_context( &self, mem_mgr: &mut SandboxMemoryManager, - ) -> std::result::Result, CrashDumpError> { + ) -> std::result::Result, CrashDumpError> + { if self.rt_cfg.guest_core_dump { let mut regs = [0; 27]; @@ -1287,36 +593,8 @@ impl HyperlightVm { } } -impl Drop for HyperlightVm { - fn drop(&mut self) { - self.interrupt_handle.set_dropped(); - } -} - -/// The vCPU tried to access the given addr -enum MemoryAccess { - /// The accessed region has the given flags - AccessViolation(MemoryRegionFlags), -} - -/// Determines if a known memory access violation occurred at the given address with the given action type. -/// Returns Some(reason) if violation reason could be determined, or None if violation occurred but in unmapped region. -fn get_memory_access_violation<'a>( - gpa: usize, - tried: MemoryRegionFlags, - mut mem_regions: impl Iterator, -) -> Option { - let region = mem_regions.find(|region| region.guest_region.contains(&gpa))?; - if !region.flags.contains(tried) { - return Some(MemoryAccess::AccessViolation(region.flags)); - } - // gpa is in `region`, and region allows the tried access, but we got here anyway. - // Treat as a generic access violation for now, unsure if this is reachable. - None -} - #[cfg(gdb)] -mod debug { +pub(super) mod debug { use hyperlight_common::mem::PAGE_SIZE; use super::HyperlightVm; diff --git a/src/hyperlight_host/src/hypervisor/regs.rs b/src/hyperlight_host/src/hypervisor/regs.rs index 5d940ba69..828bdf558 100644 --- a/src/hyperlight_host/src/hypervisor/regs.rs +++ b/src/hyperlight_host/src/hypervisor/regs.rs @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -mod debug_regs; -mod fpu; -mod special_regs; -mod standard_regs; +#[cfg(target_arch = "x86_64")] +mod x86_64; +#[cfg(target_arch = "x86_64")] +pub(crate) use x86_64::*; +#[cfg(target_arch = "aarch64")] +mod aarch64; #[cfg(target_os = "windows")] use std::collections::HashSet; -pub(crate) use debug_regs::*; -pub(crate) use fpu::*; -pub(crate) use special_regs::*; -pub(crate) use standard_regs::*; +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; #[cfg(target_os = "windows")] #[derive(Debug, PartialEq)] diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs new file mode 100644 index 000000000..8f91c634d --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs @@ -0,0 +1,37 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// TODO(aarch64): implement real register definitions + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonRegisters { + _placeholder: u64, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonSpecialRegisters { + _placeholder: u64, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonFpu { + _placeholder: u64, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonDebugRegs { + _placeholder: u64, +} diff --git a/src/hyperlight_host/src/hypervisor/regs/debug_regs.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/debug_regs.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/regs/debug_regs.rs rename to src/hyperlight_host/src/hypervisor/regs/x86_64/debug_regs.rs diff --git a/src/hyperlight_host/src/hypervisor/regs/fpu.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/fpu.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/regs/fpu.rs rename to src/hyperlight_host/src/hypervisor/regs/x86_64/fpu.rs diff --git a/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs new file mode 100644 index 000000000..88724d95a --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/x86_64/mod.rs @@ -0,0 +1,28 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +mod debug_regs; +mod fpu; +mod special_regs; +mod standard_regs; + +pub(crate) use debug_regs::*; +pub(crate) use fpu::*; +pub(crate) use special_regs::*; +pub(crate) use standard_regs::*; + +#[cfg(target_os = "windows")] +pub(crate) use super::FromWhpRegisterError; diff --git a/src/hyperlight_host/src/hypervisor/regs/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/regs/special_regs.rs rename to src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs diff --git a/src/hyperlight_host/src/hypervisor/regs/standard_regs.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/standard_regs.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/regs/standard_regs.rs rename to src/hyperlight_host/src/hypervisor/regs/x86_64/standard_regs.rs diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs new file mode 100644 index 000000000..39ecb775d --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs @@ -0,0 +1,40 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// TODO(aarch64): implement KVM backend + +use tracing::{Span, instrument}; + +use crate::hypervisor::virtual_machine::CreateVmError; + +/// Return `true` if the KVM API is available +#[instrument(skip_all, parent = Span::current(), level = "Trace")] +pub(crate) fn is_hypervisor_present() -> bool { + // TODO(aarch64): implement KVM detection + false +} + +/// A KVM implementation of a single-vcpu VM +#[derive(Debug)] +pub(crate) struct KvmVm { + _placeholder: (), +} + +impl KvmVm { + pub(crate) fn new() -> std::result::Result { + unimplemented!("KvmVm::new") + } +} diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/mod.rs new file mode 100644 index 000000000..b2adf372c --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/mod.rs @@ -0,0 +1,25 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#[cfg(target_arch = "x86_64")] +mod x86_64; +#[cfg(target_arch = "x86_64")] +pub(crate) use x86_64::*; + +#[cfg(target_arch = "aarch64")] +mod aarch64; +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs rename to src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index 82e05c104..1bbff237d 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -111,8 +111,8 @@ pub(crate) const XSAVE_MIN_SIZE: usize = 576; #[cfg(all(any(kvm, mshv3), test, feature = "init-paging"))] pub(crate) const XSAVE_BUFFER_SIZE: usize = 4096; -// Compiler error if no hypervisor type is available -#[cfg(not(any(kvm, mshv3, target_os = "windows")))] +// Compiler error if no hypervisor type is available (not applicable on aarch64 yet) +#[cfg(not(any(kvm, mshv3, target_os = "windows", target_arch = "aarch64")))] compile_error!( "No hypervisor type is available for the current platform. Please enable either the `kvm` or `mshv3` cargo feature." ); @@ -121,7 +121,12 @@ compile_error!( pub(crate) enum VmExit { /// The vCPU has exited due to a debug event (usually breakpoint) #[cfg(gdb)] - Debug { dr6: u64, exception: u32 }, + Debug { + #[cfg(target_arch = "x86_64")] + dr6: u64, + #[cfg(target_arch = "x86_64")] + exception: u32, + }, /// The vCPU has halted Halt(), /// The vCPU has issued a write to the given port with the given value diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs new file mode 100644 index 000000000..79d816b15 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs @@ -0,0 +1,38 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +use tracing::{Span, instrument}; + +use crate::hypervisor::virtual_machine::CreateVmError; + +/// Return `true` if the MSHV API is available +#[instrument(skip_all, parent = Span::current(), level = "Trace")] +pub(crate) fn is_hypervisor_present() -> bool { + // TODO(aarch64): implement MSHV detection + false +} + +/// An MSHV implementation of a single-vcpu VM +#[derive(Debug)] +pub(crate) struct MshvVm { + _placeholder: (), +} + +impl MshvVm { + pub(crate) fn new() -> std::result::Result { + unimplemented!("MshvVm::new") + } +} diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/mod.rs new file mode 100644 index 000000000..b2adf372c --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/mod.rs @@ -0,0 +1,25 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#[cfg(target_arch = "x86_64")] +mod x86_64; +#[cfg(target_arch = "x86_64")] +pub(crate) use x86_64::*; + +#[cfg(target_arch = "aarch64")] +mod aarch64; +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs similarity index 100% rename from src/hyperlight_host/src/hypervisor/virtual_machine/mshv.rs rename to src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index 9cfa44028..46766437e 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -26,7 +26,9 @@ use kvm_bindings::{KVM_MEM_READONLY, kvm_userspace_memory_region}; use mshv_bindings::{ MSHV_SET_MEM_BIT_EXECUTABLE, MSHV_SET_MEM_BIT_UNMAP, MSHV_SET_MEM_BIT_WRITABLE, }; -#[cfg(mshv3)] +#[cfg(all(mshv3, target_arch = "aarch64"))] +use mshv_bindings::{hv_arm64_memory_intercept_message, mshv_user_mem_region}; +#[cfg(all(mshv3, target_arch = "x86_64"))] use mshv_bindings::{hv_x64_memory_intercept_message, mshv_user_mem_region}; #[cfg(target_os = "windows")] use windows::Win32::System::Hypervisor::{self, WHV_MEMORY_ACCESS_TYPE}; @@ -95,7 +97,7 @@ impl TryFrom for MemoryRegionFlags { } } -#[cfg(mshv3)] +#[cfg(all(mshv3, target_arch = "x86_64"))] impl TryFrom for MemoryRegionFlags { type Error = crate::HyperlightError; @@ -112,6 +114,15 @@ impl TryFrom for MemoryRegionFlags { } } +#[cfg(all(mshv3, target_arch = "aarch64"))] +impl TryFrom for MemoryRegionFlags { + type Error = crate::HyperlightError; + + fn try_from(_msg: hv_arm64_memory_intercept_message) -> crate::Result { + unimplemented!("try_from") + } +} + // only used for debugging #[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] /// The type of memory region