From 460b3a2f9ea06d42ff61f1a476ddd7881a010014 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:19:14 +0100 Subject: [PATCH 01/29] Revert "Remove unused multi-root snapshot plumbing" This reverts commit 68e8d79e79b652d3abf6aa9550acda0f2ad169ce (part of 0a193b3ffd709ce3be7d103df6dd4f232ccdc84d). Although this plumbing was unused for now, there are several possible uses for it coming up. Also, the aarch64 page table code was written with a natively multi-space table walker. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/arch/aarch64/vmem.rs | 23 +++ src/hyperlight_common/src/arch/amd64/vmem.rs | 89 +++++++++ src/hyperlight_common/src/vmem.rs | 89 +++++++++ src/hyperlight_host/src/mem/mgr.rs | 33 +++- .../src/sandbox/initialized_multi_use.rs | 37 +++- src/hyperlight_host/src/sandbox/mod.rs | 2 +- .../src/sandbox/snapshot/mod.rs | 170 ++++++++++++------ 7 files changed, 386 insertions(+), 57 deletions(-) diff --git a/src/hyperlight_common/src/arch/aarch64/vmem.rs b/src/hyperlight_common/src/arch/aarch64/vmem.rs index 2a20691ba..3c83cabc8 100644 --- a/src/hyperlight_common/src/arch/aarch64/vmem.rs +++ b/src/hyperlight_common/src/arch/aarch64/vmem.rs @@ -46,6 +46,29 @@ pub unsafe fn virt_to_phys<'a, Op: TableReadOps + 'a>( core::iter::empty() } +/// Stub — see [`crate::vmem::walk_va_spaces`]. +#[allow(clippy::missing_safety_doc)] +pub unsafe fn walk_va_spaces( + _op: &Op, + _roots: &[Op::TableAddr], + _address: u64, + _len: u64, +) -> ::alloc::vec::Vec<( + crate::vmem::SpaceId, + ::alloc::vec::Vec, +)> { + ::alloc::vec::Vec::new() +} + +/// Stub — see [`crate::vmem::space_aware_map`]. +#[allow(clippy::missing_safety_doc)] +pub unsafe fn space_aware_map( + _op: &Op, + _ref_map: crate::vmem::SpaceReferenceMapping, + _built_roots: &::alloc::collections::BTreeMap, +) { +} + pub trait TableMovability {} impl> TableMovability for crate::vmem::MayMoveTable diff --git a/src/hyperlight_common/src/arch/amd64/vmem.rs b/src/hyperlight_common/src/arch/amd64/vmem.rs index e4c8cc8db..e8947b246 100644 --- a/src/hyperlight_common/src/arch/amd64/vmem.rs +++ b/src/hyperlight_common/src/arch/amd64/vmem.rs @@ -329,6 +329,95 @@ unsafe fn map_page< /// 3. PD (29:21) - allocate PT if needed /// 4. PT (20:12) - write final PTE with physical address and flags /// +/// Multi-space page-table walking on amd64: walks each root +/// independently and emits all leaves as `ThisSpace`. Aliased +/// intermediate-table detection is not implemented here because no +/// current embedder exercises that pattern on amd64. +#[allow(clippy::missing_safety_doc)] +pub unsafe fn walk_va_spaces( + op: &Op, + roots: &[Op::TableAddr], + address: u64, + len: u64, +) -> ::alloc::vec::Vec<( + crate::vmem::SpaceId, + ::alloc::vec::Vec, +)> { + use ::alloc::vec::Vec; + + let mut out: Vec<(crate::vmem::SpaceId, Vec)> = + Vec::with_capacity(roots.len()); + + let addr = address & ((1u64 << VA_BITS) - 1); + let vmin = addr & !(PAGE_SIZE as u64 - 1); + let vmax = core::cmp::min(addr + len, 1u64 << VA_BITS); + + for &root in roots { + #[allow(clippy::unnecessary_cast)] + let root_id: crate::vmem::SpaceId = Op::to_phys(root) as u64; + let mut mappings: Vec = Vec::new(); + + let iter = modify_ptes::<47, 39, Op, _>(MapRequest { + table_base: root, + vmin, + len: vmax.saturating_sub(vmin), + update_parent: UpdateParentNone {}, + }) + .filter_map(|r| unsafe { require_pte_exist(op, r) }) + .flat_map(modify_ptes::<38, 30, Op, _>) + .filter_map(|r| unsafe { require_pte_exist(op, r) }) + .flat_map(modify_ptes::<29, 21, Op, _>) + .filter_map(|r| unsafe { require_pte_exist(op, r) }) + .flat_map(modify_ptes::<20, 12, Op, _>); + + for r in iter { + let Some(pte) = (unsafe { read_pte_if_present(op, r.entry_ptr) }) else { + continue; + }; + let phys_addr = pte & PTE_ADDR_MASK; + let sgn_bit = r.vmin >> (VA_BITS - 1); + let sgn_bits = 0u64.wrapping_sub(sgn_bit) << VA_BITS; + let virt_addr = sgn_bits | r.vmin; + + let executable = (pte & PAGE_NX) == 0; + let avl = pte & PTE_AVL_MASK; + let kind = if avl == PAGE_AVL_COW { + MappingKind::Cow(CowMapping { + readable: true, + executable, + }) + } else { + MappingKind::Basic(BasicMapping { + readable: true, + writable: (pte & PAGE_RW) != 0, + executable, + }) + }; + mappings.push(crate::vmem::SpaceAwareMapping::ThisSpace(Mapping { + phys_base: phys_addr, + virt_base: virt_addr, + len: PAGE_SIZE as u64, + kind, + })); + } + + out.push((root_id, mappings)); + } + + out +} + +/// See [`walk_va_spaces`]: amd64 never emits `AnotherSpace`, so this +/// is unreachable in practice. It silently no-ops (rather than +/// panicking) to keep the architecture-independent re-export usable. +#[allow(clippy::missing_safety_doc)] +pub unsafe fn space_aware_map( + _op: &Op, + _ref_map: crate::vmem::SpaceReferenceMapping, + _built_roots: &::alloc::collections::BTreeMap, +) { +} + #[allow(clippy::missing_safety_doc)] pub unsafe fn map(op: &Op, mapping: Mapping) { modify_ptes::<47, 39, Op, _>(MapRequest { diff --git a/src/hyperlight_common/src/vmem.rs b/src/hyperlight_common/src/vmem.rs index 4eadc3923..47a70a3f1 100644 --- a/src/hyperlight_common/src/vmem.rs +++ b/src/hyperlight_common/src/vmem.rs @@ -426,3 +426,92 @@ pub use arch::map; /// be called concurrently with any other operations that modify the /// page table. pub use arch::virt_to_phys; + +//================================================================================================== +// Multi-space (aliased page-table) walking +//================================================================================================== + +/// Identifier for a virtual address space, used by the multi-space +/// walker to describe which space "owns" a shared intermediate table. +/// Implementations typically use the physical address of the root +/// page table (which is unique per space). +pub type SpaceId = u64; + +/// A reference from one address space to an intermediate page table +/// that lives in a different space. Produced by [`walk_va_spaces`] when +/// the walker encounters an intermediate table (at some `depth` below +/// the root) whose physical address was already seen via an earlier +/// root — i.e. the two spaces alias that sub-tree. +/// +/// Semantics: the level-`depth` block in **our** space that contains +/// VAs starting at `our_va` is aliased to the level-`depth` block in +/// `space` that contains VAs starting at `their_va`. Everything below +/// that sub-tree — PDEs, PTEs, leaf mappings — is shared wholesale. +/// +/// `depth` is counted from the root: +/// - `depth = 1, 2, 3` on amd64: PDPT, PD, or PT respectively. +#[derive(Debug, Clone, Copy)] +pub struct SpaceReferenceMapping { + /// Depth from the root at which the alias starts (1-based). + pub depth: usize, + /// The "owning" space — the first root that visited this + /// intermediate PA during [`walk_va_spaces`]. + pub space: SpaceId, + /// Start VA of the aliased sub-tree in OUR space. + pub our_va: u64, + /// Start VA of the aliased sub-tree in the owning space. Usually + /// equal to `our_va` (kernel mappings at the same VA across + /// processes) but the design permits different VAs. + pub their_va: u64, +} + +/// Either a normal leaf mapping in the current space, or a reference +/// to an intermediate table in another space. The compaction loop in +/// the host snapshotting code treats these two cases differently: +/// +/// - `ThisSpace(m)` is rebuilt like any other leaf mapping: the +/// backing page is compacted into the new snapshot blob, the PTE is +/// written, and intermediate tables are allocated on demand. +/// - `AnotherSpace(r)` is rebuilt by *linking*: the entry in our +/// rebuilt root at depth `r.depth - 1` for `r.our_va` is made to +/// point at whatever table the owning space ended up with at +/// `r.their_va`. See [`space_aware_map`]. +#[derive(Debug)] +pub enum SpaceAwareMapping { + ThisSpace(Mapping), + AnotherSpace(SpaceReferenceMapping), +} + +/// Counterpart of [`walk_va_spaces`]'s `AnotherSpace` entries on the +/// write side: installs a link in `op`'s root PT tree at `ref_map.our_va` +/// that points at whatever intermediate table the owning space ended +/// up with at `ref_map.their_va` (in `built_roots[ref_map.space]`). +/// +/// Callers must ensure that `built_roots` contains populated page +/// tables for any other space referenced by the mapping. +/// +/// # Safety +/// Same invariants as [`map`]: the caller owns the concurrency story +/// around the page tables being written, and must invalidate TLBs +/// afterwards if they were live. +pub use arch::space_aware_map; +/// Walk multiple page-table roots together, emitting either a normal +/// leaf mapping (`ThisSpace`) or a reference to an alias that was +/// already seen via an earlier root (`AnotherSpace`). +/// +/// The caller passes `roots` in their preferred order of primacy. The +/// first root to visit a particular intermediate PA becomes the +/// "owner" of that sub-table — subsequent roots that alias it receive +/// `AnotherSpace` entries referencing the owner. +/// +/// The returned `Vec` is ordered the same way `roots` was passed — so +/// by construction the result is topologically sorted: every +/// `AnotherSpace` reference points to a space that appears earlier in +/// the list. This lets a rebuilder process roots in iteration order +/// without a separate sort pass, and guarantees that the +/// [`space_aware_map`] invariant is met. +/// +/// # Safety +/// Same invariants as [`virt_to_phys`]. Callers must ensure the page +/// tables are not being mutated concurrently. +pub use arch::walk_va_spaces; diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 344583e90..887410b18 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -158,6 +158,11 @@ pub(crate) struct SandboxMemoryManager { pub(crate) struct GuestPageTableBuffer { buffer: std::cell::RefCell>, phys_base: usize, + /// Absolute GPA of the currently-active root table. For + /// multi-root guests, `set_root` switches which root subsequent + /// `vmem::map` / `vmem::space_aware_map` calls target — typically + /// to an address previously returned by `alloc_table`. + root: std::cell::Cell, } impl vmem::TableReadOps for GuestPageTableBuffer { @@ -191,7 +196,7 @@ impl vmem::TableReadOps for GuestPageTableBuffer { } fn root_table(&self) -> u64 { - self.phys_base as u64 + self.root.get() } } @@ -228,13 +233,33 @@ impl core::convert::AsRef for GuestPageTableBuffer { impl GuestPageTableBuffer { /// Create a new buffer with an initial zeroed root table at - /// `phys_base`. + /// `phys_base`. The returned buffer's current root is `phys_base`; + /// additional roots can be obtained by calling `alloc_table`. pub(crate) fn new(phys_base: usize) -> Self { GuestPageTableBuffer { buffer: std::cell::RefCell::new(vec![0u8; PAGE_TABLE_SIZE]), phys_base, + root: std::cell::Cell::new(phys_base as u64), } } + + /// Switch the active root. `addr` must have been obtained either + /// as the initial root GPA (`phys_base`) or via `alloc_table`. + pub(crate) fn set_root(&self, addr: u64) { + self.root.set(addr); + } + + /// GPA of the initial root allocated by `new`. + pub(crate) fn initial_root(&self) -> u64 { + self.phys_base as u64 + } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn size(&self) -> usize { + self.buffer.borrow().len() + } + pub(crate) fn into_bytes(self) -> Box<[u8]> { self.buffer.into_inner().into_boxed_slice() } @@ -272,7 +297,7 @@ where pub(crate) fn snapshot( &mut self, mapped_regions: Vec, - root_pt_gpa: u64, + root_pt_gpas: &[u64], rsp_gva: u64, sregs: CommonSpecialRegisters, entrypoint: NextAction, @@ -285,7 +310,7 @@ where self.layout, crate::mem::exe::LoadInfo::dummy(), mapped_regions, - root_pt_gpa, + root_pt_gpas, rsp_gva, sregs, entrypoint, diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 185f99a56..da130aa6b 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -89,8 +89,24 @@ pub struct MultiUseSandbox { /// If the current state of the sandbox has been captured in a snapshot, /// that snapshot is stored here. pub(crate) snapshot: Option>, + /// Optional callback to discover page table roots from guest memory. + /// Given (snapshot_mem, scratch_mem, cr3), returns a list of root GPAs. + /// If not set, only CR3 is used as the single root. + pt_root_finder: Option, } +/// Callback for discovering page table roots from guest memory. +/// +/// Called during [`MultiUseSandbox::snapshot`] with: +/// - `snapshot_mem` - the sandbox's snapshot (shared) memory as a byte slice +/// - `scratch_mem` - the sandbox's scratch memory as a byte slice +/// - `root_pt_gpa` - the root page table GPA of the currently-executing +/// address space +/// +/// Returns a list of root page table GPAs to walk. If the list is +/// empty, only `root_pt_gpa` is used. +pub type PtRootFinder = Box Vec + Send>; + impl MultiUseSandbox { /// Move an `UninitializedSandbox` into a new `MultiUseSandbox` instance. /// @@ -112,9 +128,17 @@ impl MultiUseSandbox { #[cfg(gdb)] dbg_mem_access_fn, snapshot: None, + pt_root_finder: None, } } + /// Set a callback that discovers page table roots from guest memory. + /// The callback receives (snapshot_mem, scratch_mem, cr3) and returns + /// the list of root GPAs to walk during snapshot creation. + pub fn set_pt_root_finder(&mut self, finder: PtRootFinder) { + self.pt_root_finder = Some(finder); + } + /// Create a `MultiUseSandbox` directly from a [`Snapshot`], /// bypassing [`UninitializedSandbox`](crate::UninitializedSandbox) /// and [`evolve()`](crate::UninitializedSandbox::evolve). @@ -345,6 +369,17 @@ impl MultiUseSandbox { .vm .get_root_pt() .map_err(|e| HyperlightError::HyperlightVmError(e.into()))?; + // Use the callback if set, otherwise just CR3 + let root_pt_gpas = if let Some(finder) = &self.pt_root_finder { + let roots = self.mem_mgr.shared_mem.with_contents(|snap| { + self.mem_mgr + .scratch_mem + .with_contents(|scratch| finder(snap, scratch, cr3)) + })??; + if roots.is_empty() { vec![cr3] } else { roots } + } else { + vec![cr3] + }; let stack_top_gpa = self.vm.get_stack_top(); let sregs = self @@ -359,7 +394,7 @@ impl MultiUseSandbox { let memory_snapshot = self.mem_mgr.snapshot( mapped_regions_vec, - cr3, + &root_pt_gpas, stack_top_gpa, sregs, entrypoint, diff --git a/src/hyperlight_host/src/sandbox/mod.rs b/src/hyperlight_host/src/sandbox/mod.rs index 6d0550be7..066903657 100644 --- a/src/hyperlight_host/src/sandbox/mod.rs +++ b/src/hyperlight_host/src/sandbox/mod.rs @@ -46,7 +46,7 @@ pub use callable::Callable; /// Re-export for `SandboxConfiguration` type pub use config::SandboxConfiguration; /// Re-export for the `MultiUseSandbox` type -pub use initialized_multi_use::MultiUseSandbox; +pub use initialized_multi_use::{MultiUseSandbox, PtRootFinder}; /// Re-export for `GuestBinary` type pub use uninitialized::GuestBinary; /// Re-export for `UninitializedSandbox` type diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index c9ec426b4..2f7aa319e 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -17,13 +17,15 @@ limitations under the License. mod file; mod file_tests; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap}; pub use file::reference::{OciDigest, OciReference, OciTag}; use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails; use hyperlight_common::layout::{scratch_base_gpa, scratch_base_gva}; use hyperlight_common::vmem; -use hyperlight_common::vmem::{BasicMapping, CowMapping, Mapping, MappingKind, PAGE_SIZE}; +use hyperlight_common::vmem::{ + BasicMapping, CowMapping, Mapping, MappingKind, PAGE_SIZE, SpaceAwareMapping, SpaceId, TableOps, +}; use tracing::{Span, instrument}; use crate::Result; @@ -386,7 +388,7 @@ impl Snapshot { mut layout: SandboxMemoryLayout, load_info: LoadInfo, regions: Vec, - root_pt_gpa: u64, + root_pt_gpas: &[u64], stack_top_gva: u64, sregs: CommonSpecialRegisters, entrypoint: NextAction, @@ -397,61 +399,127 @@ impl Snapshot { let scratch_gva = scratch_base_gva(layout.get_scratch_size()); let memory = shared_mem.with_contents(|snap_c| { scratch_mem.with_contents(|scratch_c| { - let op = SharedMemoryPageTableBuffer::new(snap_c, scratch_c, layout, root_pt_gpa); + // Phase 1: walk every PT root together. This detects + // aliased intermediate tables (e.g. Nanvix's kernel- + // half PTs, which multiple process PDs share by + // pointing at the same PT page). The walker emits + // `ThisSpace(leaf)` for private leaves and + // `AnotherSpace(ref)` for sub-trees that were already + // seen via an earlier root. Results are returned in + // `root_pt_gpas` order — which is also the topological + // order of the `AnotherSpace` references — so + // processing in iteration order is safe. + let op = SharedMemoryPageTableBuffer::new( + snap_c, + scratch_c, + layout, + root_pt_gpas.first().copied().unwrap_or(0), + ); let walk = unsafe { - vmem::virt_to_phys(&op, 0, hyperlight_common::layout::MAX_GVA as u64) + vmem::walk_va_spaces( + &op, + root_pt_gpas, + 0, + hyperlight_common::layout::MAX_GVA as u64, + ) }; - // Phase 2: rebuild page tables for the current root, - // compacting leaves into a dense snapshot blob. + // Phase 2: rebuild each space's page tables, compacting + // `ThisSpace` leaves into a dense snapshot blob and + // linking `AnotherSpace` entries to already-built + // spaces' tables. // TODO: Look for opportunities to hugepage map let mut snapshot_memory: Vec = Vec::new(); let pt_buf = GuestPageTableBuffer::new(layout.get_pt_base_gpa() as usize); - for mapping in walk { - // Drop the scratch region and (on amd64) the - // snapshot's own PT self-map; both are re-mapped - // freshly by `map_specials`. - if skip_virt(mapping.virt_base, scratch_gva) { - continue; + // Allocate one root table per space and remember the + // addresses returned by `alloc_table` instead of + // assuming the buffer's physical layout. + let mut root_addrs: Vec = Vec::with_capacity(root_pt_gpas.len()); + root_addrs.push(pt_buf.initial_root()); + for _ in 1..root_pt_gpas.len() { + root_addrs.push(unsafe { pt_buf.alloc_table() }); + } + + let mut built_roots: BTreeMap = BTreeMap::new(); + for (root_idx, (space_id, mappings)) in walk.into_iter().enumerate() { + pt_buf.set_root(root_addrs[root_idx]); + built_roots.insert(space_id, root_addrs[root_idx]); + + for sam in mappings { + match sam { + SpaceAwareMapping::ThisSpace(mapping) => { + // Drop the scratch region and (on + // amd64) the snapshot's own PT + // self-map; both are re-mapped + // freshly by `map_specials`. + if skip_virt(mapping.virt_base, scratch_gva) { + continue; + } + let Some(contents) = (unsafe { + guest_page( + snap_c, + scratch_c, + ®ions, + layout, + mapping.phys_base, + ) + }) else { + continue; + }; + + // Writable pages become CoW in the + // rebuilt snapshot; read-only pages + // stay read-only. + let kind = match mapping.kind { + MappingKind::Cow(cm) => MappingKind::Cow(cm), + MappingKind::Basic(bm) if bm.writable => { + MappingKind::Cow(CowMapping { + readable: bm.readable, + executable: bm.executable, + }) + } + MappingKind::Basic(bm) => MappingKind::Basic(BasicMapping { + readable: bm.readable, + writable: false, + executable: bm.executable, + }), + MappingKind::Unmapped => continue, + }; + let new_gpa = + phys_seen.entry(mapping.phys_base).or_insert_with(|| { + let new_offset = snapshot_memory.len(); + snapshot_memory.extend(contents); + new_offset + SandboxMemoryLayout::BASE_ADDRESS + }); + + let compacted = Mapping { + phys_base: *new_gpa as u64, + virt_base: mapping.virt_base, + len: PAGE_SIZE as u64, + kind, + }; + unsafe { vmem::map(&pt_buf, compacted) }; + } + SpaceAwareMapping::AnotherSpace(ref_map) => { + // Link to the owning space's already- + // rebuilt intermediate table — this + // is what preserves Nanvix's + // kernel-half-shared invariant across + // process PDs after relocation. + unsafe { + vmem::space_aware_map(&pt_buf, ref_map, &built_roots); + } + } + } } - let Some(contents) = (unsafe { - guest_page(snap_c, scratch_c, ®ions, layout, mapping.phys_base) - }) else { - continue; - }; - - // Writable pages become CoW in the rebuilt - // snapshot; read-only pages stay read-only. - let kind = match mapping.kind { - MappingKind::Cow(cm) => MappingKind::Cow(cm), - MappingKind::Basic(bm) if bm.writable => MappingKind::Cow(CowMapping { - readable: bm.readable, - executable: bm.executable, - }), - MappingKind::Basic(bm) => MappingKind::Basic(BasicMapping { - readable: bm.readable, - writable: false, - executable: bm.executable, - }), - MappingKind::Unmapped => continue, - }; - let new_gpa = phys_seen.entry(mapping.phys_base).or_insert_with(|| { - let new_offset = snapshot_memory.len(); - snapshot_memory.extend(contents); - new_offset + SandboxMemoryLayout::BASE_ADDRESS - }); - - let compacted = Mapping { - phys_base: *new_gpa as u64, - virt_base: mapping.virt_base, - len: PAGE_SIZE as u64, - kind, - }; - unsafe { vmem::map(&pt_buf, compacted) }; } - // Phase 3: map the scratch region. - map_specials(&pt_buf, layout.get_scratch_size()); + // Phase 3: Map the scratch region into each root. + for &root_addr in &root_addrs { + pt_buf.set_root(root_addr); + map_specials(&pt_buf, layout.get_scratch_size()); + } + pt_buf.set_root(pt_buf.initial_root()); // Phase 4: finalize PT bytes. let pt_data = pt_buf.into_bytes(); @@ -674,7 +742,7 @@ mod tests { mgr.layout, LoadInfo::dummy(), Vec::new(), - pt_base, + &[pt_base], 0, default_sregs(), super::NextAction::None, @@ -691,7 +759,7 @@ mod tests { mgr.layout, LoadInfo::dummy(), Vec::new(), - pt_base, + &[pt_base], 0, default_sregs(), super::NextAction::None, From e4b834d6f083fc4ffed75b566cac1282ecd02fbe Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 20:18:26 +0100 Subject: [PATCH 02/29] [nix] Make devShells.default work for more than just x86_64-linux Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- flake.nix | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/flake.nix b/flake.nix index 1cedb361d..a2a6cd0b0 100644 --- a/flake.nix +++ b/flake.nix @@ -51,7 +51,7 @@ toolchainVersionAttrs = args; }; })) // { - targetPlatforms = [ "x86_64-linux" ]; + targetPlatforms = [ "aarch64-linux" "x86_64-linux" ]; badTargetPlatforms = [ ]; }; overrideRustPkg = pkg: self.lib.makeOverridable (origArgs: @@ -68,20 +68,21 @@ args = [ "-c" "declare > $out" ]; }); in { - shells.default = gcrootForShell devShells.x86_64-linux.default; + shells.x86_64-linux.default = gcrootForShell devShells.x86_64-linux.default; + shells.aarch64-linux.default = gcrootForShell devShells.aarch64-linux.default; }; - devShells.x86_64-linux.default = - let pkgs = import nixpkgs { - system = "x86_64-linux"; - overlays = [ (import (nixpkgs-mozilla + "/rust-overlay.nix")) overlays.fix-rust ]; - }; - in with pkgs; let + devShells = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (system: { + default = let pkgs = import nixpkgs { + inherit system; + overlays = [ (import (nixpkgs-mozilla + "/rust-overlay.nix")) overlays.fix-rust ]; + }; in with pkgs; let customisedRustChannelOf = args: lib.flip builtins.mapAttrs (rustChannelOf args) (_: pkg: pkg.override { targets = [ "x86_64-unknown-linux-gnu" "x86_64-pc-windows-msvc" "x86_64-unknown-none" "wasm32-wasip1" "wasm32-wasip2" "wasm32-unknown-unknown" + "aarch64-unknown-none" ]; extensions = [ "rust-src" ] ++ (if args.channel == "nightly" then [ "miri-preview" ] else []); }); @@ -224,10 +225,11 @@ src = fetchFromGitHub { owner = "hyperlight-dev"; repo = "cargo-hyperlight"; - tag = "v${version}"; - hash = "sha256-xq4/c69N0wG/I8WOYVloo0J0JqoSIKiWWtECdSKrsxo="; + rev = "28ac7b57e8e7b83f80bd601f1fab334aa3ae6d4a"; + hash = "sha256-a/mvPEDJycrCbmd826SmFdasE8BFtMkCsefCNR5JnkM="; }; - cargoHash = "sha256-muiMVrK1TydQiMitihfo7xYidqUIIQ+Hw3BIeo5rLFw="; + cargoHash = "sha256-wLapaao8qcB/toltV/xjQ7SXXcfh2J19nw6jWljmb2s="; + doCheck = false; }; in (buildRustPackageClang (mkDerivationAttrs: { pname = "hyperlight"; @@ -277,5 +279,6 @@ })).overrideAttrs(oA: { hardeningDisable = [ "all" ]; }); + }); }; } From c1152c4e23b8ffd68f6a6aaffa5f549e0a77f1a9 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Sun, 10 May 2026 22:55:07 +0100 Subject: [PATCH 03/29] Only pass `page_size` to `HyperlightVm` once In 137e9645ea59d0ea2d9d05488521c3ded14afe2a, the signature of `HyperlightVm::new` changed to take `page_size`, which was previously passed in `HyperlightVm::initialise`, so that mapping operations (which are page-size-dependent) could take place before the call to initialise. However, the argument to `HyperlightVm::initialise` was retained, creating the possibility of passing in mis-matched page sizes. This commit removes the second argument, in order to remove that possibility. This commit also, while doing that, fixes compilation of the stubs in hyperlight_vm/aarch64.rs, which was previously broken. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs | 2 +- src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs | 5 +---- src/hyperlight_host/src/hypervisor/mod.rs | 2 -- src/hyperlight_host/src/sandbox/initialized_multi_use.rs | 1 - src/hyperlight_host/src/sandbox/uninitialized_evolve.rs | 1 - 5 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 42dbb7aeb..7fc8d8aea 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -44,6 +44,7 @@ impl HyperlightVm { _root_pt_addr: u64, _entrypoint: NextAction, _rsp_gva: u64, + _page_size: usize, _config: &SandboxConfiguration, #[cfg(gdb)] _gdb_conn: Option>, #[cfg(crashdump)] _rt_cfg: SandboxRuntimeConfig, @@ -57,7 +58,6 @@ impl HyperlightVm { &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, diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 37570808f..9ee7afc48 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -207,7 +207,6 @@ impl HyperlightVm { &mut self, peb_addr: RawPtr, seed: u64, - page_size: u32, mem_mgr: &mut SandboxMemoryManager, host_funcs: &Arc>, guest_max_log_level: Option, @@ -230,7 +229,7 @@ impl HyperlightVm { // function args rdi: peb_addr.into(), rsi: seed, - rdx: page_size.into(), + rdx: self.page_size as u64, rcx: get_guest_log_filter(guest_max_log_level), rflags: 1 << 1, @@ -1526,7 +1525,6 @@ mod tests { let seed = rand::rng().random::(); let peb_addr = RawPtr::from(u64::try_from(peb_address).unwrap()); - let page_size = u32::try_from(page_size::get()).unwrap(); #[cfg(gdb)] let dbg_mem_access_hdl = Arc::new(Mutex::new(hshm.clone())); @@ -1536,7 +1534,6 @@ mod tests { vm.initialise( peb_addr, seed, - page_size, &mut hshm, &host_funcs, None, diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index be1a15c22..b1031f691 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -503,7 +503,6 @@ pub(crate) mod tests { // Set up required parameters for initialise let peb_addr = RawPtr::from(0x1000u64); // Dummy PEB address let seed = 12345u64; // Random seed - let page_size = 4096u32; // Standard page size let host_funcs = Arc::new(Mutex::new(FunctionRegistry::default())); let guest_max_log_level = Some(tracing_core::LevelFilter::ERROR); @@ -514,7 +513,6 @@ pub(crate) mod tests { vm.initialise( peb_addr, seed, - page_size, &mut mem_mgr, &host_funcs, guest_max_log_level, diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index da130aa6b..e6f8e6108 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -283,7 +283,6 @@ impl MultiUseSandbox { vm.initialise( peb_addr, seed, - page_size as u32, &mut hshm, &host_funcs, None, diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index 7fa135360..a38d14409 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -98,7 +98,6 @@ pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Result Date: Sun, 10 May 2026 23:33:34 +0100 Subject: [PATCH 04/29] [aarch64] Fix stub compilation Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 7fc8d8aea..97e0e46a7 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -26,7 +26,7 @@ use super::{ 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::mgr::{SandboxMemoryManager, SnapshotSharedMemory}; use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; use crate::sandbox::SandboxConfiguration; use crate::sandbox::host_funcs::FunctionRegistry; @@ -39,7 +39,7 @@ use crate::sandbox::uninitialized::SandboxRuntimeConfig; impl HyperlightVm { #[allow(clippy::too_many_arguments)] pub(crate) fn new( - _snapshot_mem: GuestSharedMemory, + _snapshot_mem: SnapshotSharedMemory, _scratch_mem: GuestSharedMemory, _root_pt_addr: u64, _entrypoint: NextAction, From dabd5a693878ea2b94bef67c849d3998183c0a61 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Sun, 10 May 2026 23:33:43 +0100 Subject: [PATCH 05/29] [hyperlight_libc] Support building picolibc for aarch64 Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/hyperlight_libc/build.rs | 148 +++++++++++++++++------------ src/hyperlight_libc/build_files.rs | 24 +++++ 2 files changed, 110 insertions(+), 62 deletions(-) diff --git a/src/hyperlight_libc/build.rs b/src/hyperlight_libc/build.rs index d80c66de5..747466034 100644 --- a/src/hyperlight_libc/build.rs +++ b/src/hyperlight_libc/build.rs @@ -23,7 +23,7 @@ use std::{env, fs}; use anyhow::{Context, Result, bail}; use bindgen::Formatter::Prettyplease; use bindgen::RustEdition::Edition2021; -use build_files::{LIBC_FILES, LIBC_FILES_X86, LIBM_FILES, LIBM_FILES_X86}; +use build_files::{LIBC_FILES, LIBC_FILES_X86, LIBC_FILES_AARCH64, LIBM_FILES, LIBM_FILES_X86, LIBM_FILES_AARCH64}; fn copy_includes, Q: AsRef + std::fmt::Debug>( include_dir: P, @@ -52,7 +52,39 @@ fn copy_includes, Q: AsRef + std::fmt::Debug>( Ok(()) } -fn cc_build(picolibc_dir: &PathBuf, target: &str) -> Result { +#[derive(Copy, Clone)] +enum Target { + X86, + AArch64, +} +use Target::*; +impl Target { + fn get() -> Result { + match env::var("CARGO_CFG_TARGET_ARCH").as_deref() { + Err(_) => bail!("cargo TARGET_ARCH not set"), + Ok("x86") | Ok("x86_64") => Ok(X86), + Ok("aarch64") => Ok(AArch64), + Ok(arch) => bail!("Unsupported target architecture: {arch}"), + } + } + fn clang_flag(self) -> &'static str { + // This is a terrible hack, because + // - we need stack clash protection, because we have put the + // stack right smack in the middle of everything in the guest + // - clang refuses to do stack clash protection unless it is + // required by a target ABI (Windows, MacOS) or the target is + // is Linux or FreeBSD (see Clang.cpp RenderSCPOptions + // https://github.com/llvm/llvm-project/blob/1bb52e9/clang/lib/Driver/ToolChains/Clang.cpp#L3724). + // Hopefully a flag to force stack clash protection on generic + // targets will eventually show up. + match self { + X86 => "--target=x86_64-unknown-linux-none", + AArch64 => "--target=aarch64-unknown-linux-none", + } + } +} + +fn cc_build(picolibc_dir: &PathBuf, target: Target) -> Result { let mut build = cc::Build::new(); let compiler = env::var("HYPERLIGHT_GUEST_clang").unwrap_or("clang".to_string()); @@ -89,16 +121,7 @@ fn cc_build(picolibc_dir: &PathBuf, target: &str) -> Result { // Hyperlight's stack management was not designed with a redzone in mind, // so we leave it disabled for now .flag("-mno-red-zone") - // This is a terrible hack, because - // - we need stack clash protection, because we have put the - // stack right smack in the middle of everything in the guest - // - clang refuses to do stack clash protection unless it is - // required by a target ABI (Windows, MacOS) or the target is - // is Linux or FreeBSD (see Clang.cpp RenderSCPOptions - // https://github.com/llvm/llvm-project/blob/1bb52e9/clang/lib/Driver/ToolChains/Clang.cpp#L3724). - // Hopefully a flag to force stack clash protection on generic - // targets will eventually show up. - .flag("--target=x86_64-unknown-linux-none"); + .flag(target.clang_flag()); build .flag_if_supported("-fdirect-access-external-data") @@ -117,12 +140,13 @@ fn cc_build(picolibc_dir: &PathBuf, target: &str) -> Result { .define("_FILE_OFFSET_BITS", "64"); match target { - "x86" | "x86_64" => { + X86 => { build.include(picolibc_dir.join("libm/machine/x86")); build.include(picolibc_dir.join("libc/machine/x86")); } - arch => { - bail!("Unsupported target architecture: {arch}"); + AArch64 => { + build.include(picolibc_dir.join("libc/machine/aarch64")); + build.include(picolibc_dir.join("libm/machine/aarch64")); } } @@ -135,11 +159,11 @@ fn cc_build(picolibc_dir: &PathBuf, target: &str) -> Result { Ok(build) } -fn add_libc(build: &mut cc::Build, picolibc_dir: &Path, target: &str) -> Result<()> { +fn add_libc(build: &mut cc::Build, picolibc_dir: &Path, target: Target) -> Result<()> { let base = LIBC_FILES.iter(); let files = match target { - "x86" | "x86_64" => base.chain(LIBC_FILES_X86.iter()), - arch => bail!("Unsupported target architecture: {arch}"), + X86 => base.chain(LIBC_FILES_X86.iter()), + AArch64 => base.chain(LIBC_FILES_AARCH64.iter()), }; for file in files { @@ -150,13 +174,13 @@ fn add_libc(build: &mut cc::Build, picolibc_dir: &Path, target: &str) -> Result< Ok(()) } -fn add_libm(build: &mut cc::Build, picolibc_dir: &Path, target: &str) -> Result<()> { +fn add_libm(build: &mut cc::Build, picolibc_dir: &Path, target: Target) -> Result<()> { build.include(picolibc_dir.join("libm/common")); let base = LIBM_FILES.iter(); let files = match target { - "x86" | "x86_64" => base.chain(LIBM_FILES_X86.iter()), - arch => bail!("Unsupported target architecture: {arch}"), + X86 => base.chain(LIBM_FILES_X86.iter()), + AArch64 => base.chain(LIBM_FILES_AARCH64.iter()), }; for file in files { @@ -200,7 +224,7 @@ fn generate_bindings(include_dir: &Path, out_dir: &Path) -> Result<()> { .header(include_dir.join("sys/time.h").to_string_lossy()) .clang_arg(format!("-I{}", include_dir.display())) .clang_arg("-nostdlibinc") - .clang_arg("--target=x86_64-unknown-linux-none") + .clang_arg(Target::get()?.clang_flag()) .clang_arg("-fno-stack-protector") .clang_arg("-D_POSIX_MONOTONIC_CLOCK=1") .use_core() @@ -232,7 +256,7 @@ fn cargo_main() -> Result<()> { println!("cargo:rerun-if-env-changed=HYPERLIGHT_GUEST_TOOLCHAIN_ROOT"); let out_dir = env::var("OUT_DIR").expect("cargo OUT_DIR not set"); - let target = env::var("CARGO_CFG_TARGET_ARCH").expect("cargo TARGET_ARCH not set"); + let target = Target::get()?; let include_dir = PathBuf::from(&out_dir).join("include"); fs::create_dir_all(&include_dir) @@ -247,13 +271,13 @@ fn cargo_main() -> Result<()> { init_submodule().with_context(|| "failed to init picolibc submodule")?; } - let mut build = cc_build(&picolibc_dir, &target)?; + let mut build = cc_build(&picolibc_dir, target)?; // include for picolibc configuration: picolibc.h build.include(manifest_dir.join("include")); - add_libc(&mut build, &picolibc_dir, &target)?; - add_libm(&mut build, &picolibc_dir, &target)?; + add_libc(&mut build, &picolibc_dir, target)?; + add_libm(&mut build, &picolibc_dir, target)?; if cfg!(windows) { unsafe { env::set_var("AR_x86_64_unknown_none", "llvm-ar") }; @@ -374,19 +398,7 @@ fn find_next(root_dir: &Path, tool_name: &str) -> PathBuf { panic!("Could not find another implementation of {}", tool_name); } -fn main() -> std::process::ExitCode { - let exe = env::current_exe().expect("expected program name"); - let name = Path::file_name(exe.as_ref()).expect("program name should not be directory"); - let tool: Tool = name.into(); - - if tool == Tool::CargoBuildScript { - if let Err(err) = cargo_main() { - eprintln!("{:#}", err); - return std::process::ExitCode::FAILURE; - } - return std::process::ExitCode::SUCCESS; - } - +fn clang_main(exe: PathBuf) -> Result> { let exe_abs = fs::canonicalize(&exe).expect("program name should be possible to canonicalize"); let root_dir = exe_abs .parent() @@ -396,30 +408,42 @@ fn main() -> std::process::ExitCode { let mut args = env::args(); args.next(); // ignore the exe name let include_dir = >::as_ref(&out_dir).join("include"); + Ok(std::process::Command::new(find_next(root_dir, "clang")) + // terrible hack, see above + .arg(Target::get()?.clang_flag()) + .args([ + // We don't support stack protectors at the moment, but Arch Linux clang + // auto-enables them for -linux platforms, so explicitly disable them. + "-fno-stack-protector", + "-fstack-clash-protection", + "-mstack-probe-size=4096", + "-mno-red-zone", + ]) + .arg("-nostdinc") + .arg("-isystem") + .arg(include_dir) + .args(args) + .status() + .ok() + .and_then(|x| x.code()) + .map(|x| (x as u8).into())) +} + +fn main() -> std::process::ExitCode { + let exe = env::current_exe().expect("expected program name"); + let name = Path::file_name(exe.as_ref()).expect("program name should not be directory"); + let tool: Tool = name.into(); - match tool { - Tool::CargoBuildScript => unreachable!("cargo build script should not be called directly"), - Tool::Clang => { - std::process::Command::new(find_next(root_dir, "clang")) - // terrible hack, see above - .arg("--target=x86_64-unknown-linux-none") - .args([ - // We don't support stack protectors at the moment, but Arch Linux clang - // auto-enables them for -linux platforms, so explicitly disable them. - "-fno-stack-protector", - "-fstack-clash-protection", - "-mstack-probe-size=4096", - "-mno-red-zone", - ]) - .arg("-nostdinc") - .arg("-isystem") - .arg(include_dir) - .args(args) - .status() - .ok() - .and_then(|x| x.code()) - .map(|x| (x as u8).into()) - .unwrap_or(std::process::ExitCode::FAILURE) + let res = match tool { + Tool::CargoBuildScript => cargo_main().map(|_| None), + Tool::Clang => clang_main(exe), + }; + match res { + Err(e) => { + eprintln!("{:#}", e); + std::process::ExitCode::FAILURE } + Ok(None) => std::process::ExitCode::SUCCESS, + Ok(Some(ec)) => ec, } } diff --git a/src/hyperlight_libc/build_files.rs b/src/hyperlight_libc/build_files.rs index 80b2fe1c5..8d32bdcaf 100644 --- a/src/hyperlight_libc/build_files.rs +++ b/src/hyperlight_libc/build_files.rs @@ -659,6 +659,28 @@ pub(crate) const LIBC_FILES_X86: &[&str] = &[ "machine/x86/tls.c", ]; +pub(crate) const LIBC_FILES_AARCH64: &[&str] = &[ + "machine/aarch64/interrupt.c", + "machine/aarch64/interrupt_vector.S", + "machine/aarch64/memchr.S", + "machine/aarch64/memcmp.S", + "machine/aarch64/memmove.S", + "machine/aarch64/memrchr.S", + "machine/aarch64/memset.S", + "machine/aarch64/rawmemchr.S", + "machine/aarch64/setjmp.S", + "machine/aarch64/stpcpy.S", + "machine/aarch64/strchr.S", + "machine/aarch64/strchrnul.S", + "machine/aarch64/strcmp.S", + "machine/aarch64/strcpy.S", + "machine/aarch64/strlen.S", + "machine/aarch64/strncmp.S", + "machine/aarch64/strnlen.S", + "machine/aarch64/strrchr.S", + "machine/aarch64/tls.c", +]; + pub(crate) const LIBM_FILES: &[&str] = &[ "common/copysignl.c", "common/exp10l.c", @@ -942,3 +964,5 @@ pub(crate) const LIBM_FILES: &[&str] = &[ ]; pub(crate) const LIBM_FILES_X86: &[&str] = &["machine/x86/fenv.c"]; + +pub(crate) const LIBM_FILES_AARCH64: &[&str] = &[]; From be6214c45a24d3be0df2c65e4138d9ff4a4ed5aa Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Sun, 10 May 2026 23:42:45 +0100 Subject: [PATCH 06/29] [simpleguest] cfg-gate functions that do not compile on aarch64 In the future, some of these may be reimplemented, but for now, just gate them. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/tests/rust_guests/simpleguest/src/main.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index acc176052..af05b740c 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -45,6 +45,7 @@ use hyperlight_common::log_level::GuestLogFilter; use hyperlight_common::vmem::{BasicMapping, MappingKind}; use hyperlight_guest::error::{HyperlightGuestError, Result}; use hyperlight_guest::exit::{abort_with_code, abort_with_code_and_message}; +#[cfg(target_arch = "x86_64")] use hyperlight_guest_bin::exception::arch::{Context, ExceptionInfo}; use hyperlight_guest_bin::guest_function::definition::{GuestFunc, GuestFunctionDefinition}; use hyperlight_guest_bin::guest_function::register::register_function; @@ -85,6 +86,7 @@ fn echo_double(value: f64) -> f64 { // Test exception handler that validates stack layout and records invocation // It is designed to interact with the trigger_int3 breakpoint exception function below +#[cfg(target_arch = "x86_64")] fn test_exception_handler( exception_number: u64, _exception_info: *mut ExceptionInfo, @@ -133,6 +135,7 @@ fn test_exception_handler( /// Install handler for a specific vector #[guest_function("InstallHandler")] +#[cfg(target_arch = "x86_64")] fn install_handler(vector: i32) { hyperlight_guest_bin::exception::arch::HANDLERS[vector as usize].store( test_exception_handler as *const () as usize as u64, @@ -149,6 +152,7 @@ fn get_exception_handler_call_count() -> i32 { /// Trigger an INT3 breakpoint exception (vector 3) #[guest_function("TriggerInt3")] +#[cfg(target_arch = "x86_64")] fn trigger_int3() -> i32 { // Set up test value in R9 before triggering exception let test_value: u64 = TEST_R9_VALUE; @@ -341,6 +345,7 @@ fn call_malloc(size: i32) -> i32 { } #[guest_function("FillHeapAndCauseException")] +#[cfg(target_arch = "x86_64")] fn fill_heap_and_cause_exception() { let layout: Layout = Layout::new::(); let mut ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }; @@ -484,6 +489,7 @@ fn log_message(message: String, level: i32) { } #[guest_function("TriggerException")] +#[cfg(target_arch = "x86_64")] fn trigger_exception() { // trigger an undefined instruction exception unsafe { core::arch::asm!("ud2") }; @@ -492,6 +498,7 @@ fn trigger_exception() { /// Execute an OUT instruction with an arbitrary port and value. /// This is used to test that invalid OUT ports cause errors. #[guest_function("OutbWithPort")] +#[cfg(target_arch = "x86_64")] fn outb_with_port(port: u32, value: u32) { unsafe { core::arch::asm!( @@ -517,6 +524,7 @@ static TIMER_IRQ_COUNT: AtomicU32 = AtomicU32::new(0); // for the atomic counter update, and sends a non-specific EOI to the master PIC. // // NOTE: global_asm! on x86_64 in Rust defaults to Intel syntax. +#[cfg(target_arch = "x86_64")] core::arch::global_asm!( ".globl _timer_irq_handler", "_timer_irq_handler:", @@ -555,6 +563,7 @@ struct IdtPtr { /// - `max_spin`: maximum busy-wait iterations before giving up /// /// Returns the number of timer interrupts received. +#[cfg(target_arch = "x86_64")] #[guest_function("TestTimerInterrupts")] fn test_timer_interrupts(period_us: i32, max_spin: i32) -> i32 { // Reset counter @@ -704,17 +713,20 @@ fn call_given_paramless_hostfunc_that_returns_i64(hostfuncname: String) -> Resul } #[guest_function("UseSSE2Registers")] +#[cfg(target_arch = "x86_64")] fn use_sse2_registers() { let val: f32 = 1.2f32; unsafe { core::arch::asm!("movss xmm1, DWORD PTR [{0}]", in(reg) &val) }; } #[guest_function("SetDr0")] +#[cfg(target_arch = "x86_64")] fn set_dr0(value: u64) { unsafe { core::arch::asm!("mov dr0, {}", in(reg) value) }; } #[guest_function("GetDr0")] +#[cfg(target_arch = "x86_64")] fn get_dr0() -> u64 { let value: u64; unsafe { core::arch::asm!("mov {}, dr0", out(reg) value) }; @@ -751,6 +763,7 @@ fn read_from_user_memory(num: u64, expected: Vec) -> Result> { Ok(bytes) } +#[cfg(target_arch = "x86_64")] #[guest_function("ReadMappedBuffer")] fn read_mapped_buffer(base: u64, len: u64, do_map: bool) -> Vec { let base = base as usize as *const u8; @@ -777,6 +790,7 @@ fn read_mapped_buffer(base: u64, len: u64, do_map: bool) -> Vec { data.to_vec() } +#[cfg(target_arch = "x86_64")] #[guest_function("CheckMapped")] fn check_mapped_buffer(base: u64) -> bool { hyperlight_guest_bin::paging::virt_to_phys(base) @@ -784,6 +798,7 @@ fn check_mapped_buffer(base: u64) -> bool { .is_some() } +#[cfg(target_arch = "x86_64")] #[guest_function("WriteMappedBuffer")] fn write_mapped_buffer(base: u64, len: u64) -> bool { let base = base as usize as *mut u8; @@ -812,6 +827,7 @@ fn write_mapped_buffer(base: u64, len: u64) -> bool { true } +#[cfg(target_arch = "x86_64")] #[guest_function("ExecMappedBuffer")] fn exec_mapped_buffer(base: u64, len: u64) -> bool { let base = base as usize as *mut u8; @@ -979,6 +995,7 @@ fn fuzz_guest_trace(max_depth: u32, msg: String) -> u32 { fuzz_traced_function(0, max_depth, &msg) } +#[cfg(target_arch = "x86_64")] #[guest_function("CorruptOutputSizePrefix")] fn corrupt_output_size_prefix() -> i32 { unsafe { @@ -1003,6 +1020,7 @@ fn corrupt_output_size_prefix() -> i32 { } } +#[cfg(target_arch = "x86_64")] #[guest_function("CorruptOutputBackPointer")] fn corrupt_output_back_pointer() -> i32 { unsafe { From 2efc3937a158d2f42c27f07700277c99c7fbc172 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Mon, 18 May 2026 11:57:54 +0100 Subject: [PATCH 07/29] Rename MAX_{GPA,GVA} to SCRATCH_TOP_{GPA,GVA} This name is a more accurate reflection of how these constants are used Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/hyperlight_common/src/arch/aarch64/layout.rs | 4 ++-- src/hyperlight_common/src/arch/amd64/layout.rs | 4 ++-- src/hyperlight_common/src/layout.rs | 6 +++--- .../src/arch/amd64/prim_alloc.rs | 3 ++- src/hyperlight_guest/src/layout.rs | 16 ++++++++-------- src/hyperlight_guest_bin/src/arch/amd64/init.rs | 2 +- .../src/hypervisor/hyperlight_vm/x86_64.rs | 4 ++-- src/hyperlight_host/src/hypervisor/mod.rs | 2 +- .../src/hypervisor/virtual_machine/kvm/x86_64.rs | 2 +- src/hyperlight_host/src/mem/mgr.rs | 2 +- .../src/sandbox/snapshot/file/config.rs | 2 +- src/hyperlight_host/src/sandbox/snapshot/mod.rs | 4 ++-- src/hyperlight_libc/build.rs | 4 +++- 13 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/hyperlight_common/src/arch/aarch64/layout.rs b/src/hyperlight_common/src/arch/aarch64/layout.rs index 20f17026c..04d368606 100644 --- a/src/hyperlight_common/src/arch/aarch64/layout.rs +++ b/src/hyperlight_common/src/arch/aarch64/layout.rs @@ -15,10 +15,10 @@ 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 SCRATCH_TOP_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 const SCRATCH_TOP_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/amd64/layout.rs b/src/hyperlight_common/src/arch/amd64/layout.rs index 14a9cd62a..5389ccdab 100644 --- a/src/hyperlight_common/src/arch/amd64/layout.rs +++ b/src/hyperlight_common/src/arch/amd64/layout.rs @@ -21,7 +21,7 @@ limitations under the License. /// We have this the top of the page below the top of memory in order /// to make working with start/end ptrs in a few places more /// convenient (not needing to worry about overflow) -pub const MAX_GVA: usize = 0xffff_ffff_ffff_efff; +pub const SCRATCH_TOP_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; @@ -29,7 +29,7 @@ pub const SNAPSHOT_PT_GVA_MAX: usize = 0xffff_80ff_ffff_ffff; /// supports at least 36 bits. Almost all of them support at least 40 /// bits, so we could consider bumping this in the future if we were /// ever memory-constrained. -pub const MAX_GPA: usize = 0x0000_000f_ffff_ffff; +pub const SCRATCH_TOP_GPA: usize = 0x0000_000f_ffff_ffff; /// On amd64, this is: /// - Two pages for the TSS and IDT diff --git a/src/hyperlight_common/src/layout.rs b/src/hyperlight_common/src/layout.rs index 69ecdb6ef..b12028219 100644 --- a/src/hyperlight_common/src/layout.rs +++ b/src/hyperlight_common/src/layout.rs @@ -18,7 +18,7 @@ limitations under the License. #[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}; +pub use arch::{SCRATCH_TOP_GPA, SCRATCH_TOP_GVA, SNAPSHOT_PT_GVA_MAX, SNAPSHOT_PT_GVA_MIN}; // offsets down from the top of scratch memory for various things pub const SCRATCH_TOP_SIZE_OFFSET: u64 = 0x08; @@ -28,10 +28,10 @@ pub const SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET: u64 = 0x20; pub const SCRATCH_TOP_EXN_STACK_OFFSET: u64 = 0x30; pub fn scratch_base_gpa(size: usize) -> u64 { - (MAX_GPA - size + 1) as u64 + (SCRATCH_TOP_GPA - size + 1) as u64 } pub fn scratch_base_gva(size: usize) -> u64 { - (MAX_GVA - size + 1) as u64 + (SCRATCH_TOP_GVA - size + 1) as u64 } /// Compute the minimum scratch region size needed for a sandbox. diff --git a/src/hyperlight_guest/src/arch/amd64/prim_alloc.rs b/src/hyperlight_guest/src/arch/amd64/prim_alloc.rs index cfaad9a0b..e1d388c64 100644 --- a/src/hyperlight_guest/src/arch/amd64/prim_alloc.rs +++ b/src/hyperlight_guest/src/arch/amd64/prim_alloc.rs @@ -33,7 +33,8 @@ pub unsafe fn alloc_phys_pages(n: u64) -> u64 { } // Set aside two pages at the top of the scratch region for the // exception stack, shared state, etc - let max_avail = hyperlight_common::layout::MAX_GPA - hyperlight_common::vmem::PAGE_SIZE * 2; + let max_avail = + hyperlight_common::layout::SCRATCH_TOP_GPA - hyperlight_common::vmem::PAGE_SIZE * 2; if x.checked_add(nbytes) .is_none_or(|xx| xx >= max_avail as u64) { diff --git a/src/hyperlight_guest/src/layout.rs b/src/hyperlight_guest/src/layout.rs index 5d8b28e2d..6d132ae7c 100644 --- a/src/hyperlight_guest/src/layout.rs +++ b/src/hyperlight_guest/src/layout.rs @@ -20,19 +20,19 @@ mod arch; pub use arch::{MAIN_STACK_LIMIT_GVA, MAIN_STACK_TOP_GVA}; pub fn scratch_size_gva() -> *mut u64 { - use hyperlight_common::layout::{MAX_GVA, SCRATCH_TOP_SIZE_OFFSET}; - (MAX_GVA as u64 - SCRATCH_TOP_SIZE_OFFSET + 1) as *mut u64 + use hyperlight_common::layout::{SCRATCH_TOP_GVA, SCRATCH_TOP_SIZE_OFFSET}; + (SCRATCH_TOP_GVA as u64 - SCRATCH_TOP_SIZE_OFFSET + 1) as *mut u64 } pub fn allocator_gva() -> *mut u64 { - use hyperlight_common::layout::{MAX_GVA, SCRATCH_TOP_ALLOCATOR_OFFSET}; - (MAX_GVA as u64 - SCRATCH_TOP_ALLOCATOR_OFFSET + 1) as *mut u64 + use hyperlight_common::layout::{SCRATCH_TOP_ALLOCATOR_OFFSET, SCRATCH_TOP_GVA}; + (SCRATCH_TOP_GVA as u64 - SCRATCH_TOP_ALLOCATOR_OFFSET + 1) as *mut u64 } pub fn snapshot_pt_gpa_base_gva() -> *mut u64 { - use hyperlight_common::layout::{MAX_GVA, SCRATCH_TOP_SNAPSHOT_PT_GPA_BASE_OFFSET}; - (MAX_GVA as u64 - SCRATCH_TOP_SNAPSHOT_PT_GPA_BASE_OFFSET + 1) as *mut u64 + use hyperlight_common::layout::{SCRATCH_TOP_GVA, SCRATCH_TOP_SNAPSHOT_PT_GPA_BASE_OFFSET}; + (SCRATCH_TOP_GVA as u64 - SCRATCH_TOP_SNAPSHOT_PT_GPA_BASE_OFFSET + 1) as *mut u64 } pub fn snapshot_generation_gva() -> *mut u64 { - use hyperlight_common::layout::{MAX_GVA, SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET}; - (MAX_GVA as u64 - SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET + 1) as *mut u64 + use hyperlight_common::layout::{SCRATCH_TOP_GVA, SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET}; + (SCRATCH_TOP_GVA as u64 - SCRATCH_TOP_SNAPSHOT_GENERATION_OFFSET + 1) as *mut u64 } pub use arch::{scratch_base_gpa, scratch_base_gva}; diff --git a/src/hyperlight_guest_bin/src/arch/amd64/init.rs b/src/hyperlight_guest_bin/src/arch/amd64/init.rs index 073bd3a2f..ef49f1557 100644 --- a/src/hyperlight_guest_bin/src/arch/amd64/init.rs +++ b/src/hyperlight_guest_bin/src/arch/amd64/init.rs @@ -92,7 +92,7 @@ unsafe fn init_tss(pc: *mut ProcCtrl) { let tss_ptr = &raw mut (*pc).tss; // copy byte by byte to avoid alignment issues let ist1_ptr = &raw mut (*tss_ptr).ist1 as *mut [u8; 8]; - let exn_stack = hyperlight_common::layout::MAX_GVA as u64 + let exn_stack = hyperlight_common::layout::SCRATCH_TOP_GVA as u64 - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + 1; ist1_ptr.write_volatile(exn_stack.to_ne_bytes()); diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 9ee7afc48..eba102c6b 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -1509,7 +1509,7 @@ mod tests { let (mut hshm, gshm) = mem_mgr.build().unwrap(); let peb_address = gshm.layout.peb_address(); - let stack_top_gva = hyperlight_common::layout::MAX_GVA as u64 + let stack_top_gva = hyperlight_common::layout::SCRATCH_TOP_GVA as u64 - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + 1; let mut vm = set_up_hypervisor_partition( @@ -2153,7 +2153,7 @@ mod tests { /// Get the stack top GVA, same as the regular codepath. fn stack_top_gva(&self) -> u64 { - hyperlight_common::layout::MAX_GVA as u64 + hyperlight_common::layout::SCRATCH_TOP_GVA as u64 - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + 1 } diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index b1031f691..732f08563 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -487,7 +487,7 @@ pub(crate) mod tests { let sandbox = UninitializedSandbox::new(GuestBinary::FilePath(filename.clone()), Some(config))?; let (mut mem_mgr, gshm) = sandbox.mgr.build().unwrap(); - let exn_stack_top_gva = hyperlight_common::layout::MAX_GVA as u64 + let exn_stack_top_gva = hyperlight_common::layout::SCRATCH_TOP_GVA as u64 - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + 1; let mut vm = set_up_hypervisor_partition( diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs index c12713ecb..3dc8ec87a 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs @@ -175,7 +175,7 @@ impl KvmVm { == CPUID_FUNCTION_PROCESSOR_CAPACITY_PARAMETERS_AND_EXTENDED_FEATURE_IDENTIFICATION { entry.eax &= !0xff; - entry.eax |= hyperlight_common::layout::MAX_GPA.ilog2() + 1; + entry.eax |= hyperlight_common::layout::SCRATCH_TOP_GPA.ilog2() + 1; } } vcpu_fd diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 887410b18..a70b5fba7 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -589,7 +589,7 @@ impl SandboxMemoryManager { ) -> Result> { use crate::sandbox::snapshot::SharedMemoryPageTableBuffer; - let len = hyperlight_common::layout::MAX_GVA; + let len = hyperlight_common::layout::SCRATCH_TOP_GVA; let regions = self.shared_mem.with_contents(|snapshot| { self.scratch_mem.with_contents(|scratch| { diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 4e926a62a..1277c8380 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -568,7 +568,7 @@ impl OciSnapshotConfig { // Bound it to the architecture's addressable GVA range so a // malformed config cannot install an out-of-range stack // pointer. The range is `(0, MAX_GVA]`. - let max_gva = hyperlight_common::layout::MAX_GVA as u64; + let max_gva = hyperlight_common::layout::SCRATCH_TOP_GVA as u64; if self.stack_top_gva == 0 || self.stack_top_gva > max_gva { return Err(crate::new_error!( "snapshot stack_top_gva {:#x} is outside the valid range (0, {:#x}]", diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index 2f7aa319e..8da0c1e48 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -355,7 +355,7 @@ impl Snapshot { layout.set_pt_size(pt_bytes.len())?; memory.extend(&pt_bytes); - let exn_stack_top_gva = hyperlight_common::layout::MAX_GVA as u64 + let exn_stack_top_gva = hyperlight_common::layout::SCRATCH_TOP_GVA as u64 - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + 1; @@ -420,7 +420,7 @@ impl Snapshot { &op, root_pt_gpas, 0, - hyperlight_common::layout::MAX_GVA as u64, + hyperlight_common::layout::SCRATCH_TOP_GVA as u64, ) }; diff --git a/src/hyperlight_libc/build.rs b/src/hyperlight_libc/build.rs index 747466034..e652988cb 100644 --- a/src/hyperlight_libc/build.rs +++ b/src/hyperlight_libc/build.rs @@ -23,7 +23,9 @@ use std::{env, fs}; use anyhow::{Context, Result, bail}; use bindgen::Formatter::Prettyplease; use bindgen::RustEdition::Edition2021; -use build_files::{LIBC_FILES, LIBC_FILES_X86, LIBC_FILES_AARCH64, LIBM_FILES, LIBM_FILES_X86, LIBM_FILES_AARCH64}; +use build_files::{ + LIBC_FILES, LIBC_FILES_AARCH64, LIBC_FILES_X86, LIBM_FILES, LIBM_FILES_AARCH64, LIBM_FILES_X86, +}; fn copy_includes, Q: AsRef + std::fmt::Debug>( include_dir: P, From 09c44f7f30c38dd708702463897a0d511d1aa489 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Sat, 23 May 2026 12:06:29 +0100 Subject: [PATCH 08/29] Justfile: add support for compiling for aarch64 Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- Justfile | 4 +++- c.just | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Justfile b/Justfile index 2bdb842b9..f2edfb2da 100644 --- a/Justfile +++ b/Justfile @@ -27,10 +27,12 @@ export CROSS_CONTAINER_GID := if path_exists("/dev/kvm") == "true" { kvm-gid } e root := justfile_directory() default-target := "debug" +hyperlight-target-arch := env("HYPERLIGHT_TARGET", arch()) +hyperlight-target := if hyperlight-target-arch == "x86_64" { "x86_64-hyperlight-none" } else if hyperlight-target-arch == "aarch64" { "aarch64-hyperlight-none" } else { error("Unsupported architecture: " + arch()) } # All three guest crates share one workspace under src/tests/rust_guests, # so they share one target dir and hyperlight-libc / hyperlight-guest-bin # get compiled once per profile instead of once per crate. -rust_guests_target := "src/tests/rust_guests/target/x86_64-hyperlight-none" +rust_guests_target := "src/tests/rust_guests/target/" + hyperlight-target simpleguest_source := rust_guests_target dummyguest_source := rust_guests_target witguest_source := rust_guests_target diff --git a/c.just b/c.just index 92bcf8f6c..4d5a12a33 100644 --- a/c.just +++ b/c.just @@ -1,8 +1,9 @@ mkdir := if os() == "windows" { "mkdir -f -p" } else { "mkdir -p"} # Elf options +hyperlight-target-c := if hyperlight-target-arch == "x86_64" { "x86_64-unknown-linux-none" } else if hyperlight-target-arch == "aarch64" { "aarch64-unknown-linux-none" } else { error("Unsupported architecture: " + hyperlight-target-arch) } # We don't support stack protectors at the moment, but Arch Linux clang auto-enables them for -linux platforms, so explicitly disable them. -c-compile-options-elf := '-nostdlibinc -H --target=x86_64-unknown-linux-none -fno-stack-protector -fstack-clash-protection -mstack-probe-size=4096 -fPIC' +c-compile-options-elf := "-nostdlibinc -H --target=" + hyperlight-target-c + " -fno-stack-protector -fstack-clash-protection -mstack-probe-size=4096 -fPIC" c-include-flags-elf := "-I " + root / "src/hyperlight_guest_capi/include/" + " -I " + root / "src/hyperlight_libc/third_party/picolibc/libc/include/" + " -I " + root / "src/hyperlight_libc/third_party/picolibc/libc/stdio/" + " -I " + root / "src/hyperlight_libc/include/" c-linker-options-elf := '--entry "entrypoint" --nostdlib -pie --no-dynamic-linker' c-flags-debug-elf := '-O0' @@ -19,7 +20,7 @@ compile-c-guest target=default-target: link-c-guest target=default-target: # elf - cd src/tests/c_guests/c_simpleguest && ld.lld -o out/{{target}}/simpleguest {{c-linker-options-elf}} out/{{target}}/main.o -l hyperlight_guest_capi -L "{{justfile_directory()}}/target/x86_64-hyperlight-none/{{target}}" + cd src/tests/c_guests/c_simpleguest && ld.lld -o out/{{target}}/simpleguest {{c-linker-options-elf}} out/{{target}}/main.o -l hyperlight_guest_capi -L "{{justfile_directory()}}/target/{{hyperlight-target}}/{{target}}" move-c-guests target=default-target: # elf From 84e64737bc846fca0fc09f7f1cedaf9bec5563df Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Sat, 23 May 2026 13:47:44 +0100 Subject: [PATCH 09/29] HyperlightVm: allow hypervisor backends to implement reset_vcpu This is not yet used anywhere, but will be used for aarch64 KVM (where there is no other reasonable method of resetting all MSRs), and will likely be used on x86_64 in the future as well. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/hypervisor/hyperlight_vm/mod.rs | 6 ++++-- .../src/hypervisor/hyperlight_vm/x86_64.rs | 6 ++++-- .../src/hypervisor/virtual_machine/mod.rs | 21 +++++++++++++++++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs index ce6c97a5a..a574880ef 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -43,7 +43,7 @@ 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, + MapMemoryError, RegisterError, ResetVcpuError, RunVcpuError, UnmapMemoryError, VmError, VmExit, }; use crate::hypervisor::{InterruptHandle, InterruptHandleImpl}; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; @@ -353,7 +353,7 @@ pub enum HyperlightVmError { #[error("Map region error: {0}")] MapRegion(#[from] MapRegionError), #[error("Restore VM (vcpu) error: {0}")] - Restore(#[from] RegisterError), + Restore(#[from] ResetVcpuError), #[error("Unmap region error: {0}")] UnmapRegion(#[from] UnmapRegionError), #[error("Update region error: {0}")] @@ -392,6 +392,8 @@ pub(crate) struct HyperlightVm { pub(super) mmap_regions: Vec<(u32, MemoryRegion)>, // Later mapped regions (slot number, region) + #[cfg(target_arch = "aarch64")] + pub(self) vm_can_reset_vcpu: bool, pub(super) pending_tlb_flush: bool, #[cfg(gdb)] diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index eba102c6b..e5f94760e 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -342,7 +342,7 @@ impl HyperlightVm { &mut self, cr3: u64, sregs: &CommonSpecialRegisters, - ) -> std::result::Result<(), RegisterError> { + ) -> std::result::Result<(), ResetVcpuError> { self.vm.set_regs(&CommonRegisters { rflags: 1 << 1, // Reserved bit always set ..Default::default() @@ -350,7 +350,9 @@ impl HyperlightVm { self.vm.set_debug_regs(&CommonDebugRegs::default())?; self.vm.reset_xsave()?; - self.apply_sregs(cr3, sregs) + self.apply_sregs(cr3, sregs)?; + + Ok(()) } /// Apply special registers and mark TLB for flush. diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index 841e941f1..691397bd4 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -246,6 +246,18 @@ pub enum RegisterError { ConversionFailed(String), } +#[derive(Debug, Clone, thiserror::Error)] +pub enum ResetVcpuError { + #[error("Single-operation vcpu reset not supported on this hypervisor")] + NotSupported, + #[error("Hypervisor operation failed: {0}")] + Hypervisor(HypervisorError), + #[error("Register operation failed: {0}")] + Register(#[from] RegisterError), + #[error("Operation failed: {0}")] + Unknown(String), +} + /// Map memory error #[derive(Debug, Clone, thiserror::Error)] pub enum MapMemoryError { @@ -352,6 +364,15 @@ pub(crate) trait VirtualMachine: Debug + Send { #[cfg(test)] fn set_xsave(&self, xsave: &[u32]) -> std::result::Result<(), RegisterError>; + /// Single-operation vCPU reset + #[cfg(target_arch = "aarch64")] + fn can_reset_vcpu(&self) -> bool { + false + } + #[cfg(target_arch = "aarch64")] + fn reset_vcpu(&mut self) -> std::result::Result<(), ResetVcpuError> { + Err(ResetVcpuError::NotSupported) + } /// Get partition handle #[cfg(target_os = "windows")] fn partition_handle(&self) -> windows::Win32::System::Hypervisor::WHV_PARTITION_HANDLE; From 0b22865fce49fbe187750455b9cb0e2a5bdcbcfb Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Sat, 23 May 2026 13:56:28 +0100 Subject: [PATCH 10/29] [hyperlight_guest_bin] Make `init_stack` architecture-independent This was never actually architecture-dependent, since it used the virtual memory APIs from hyperlight_guest. Extract it, so that it can be used by aarch64 init in the near future. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/arch/amd64/init.rs | 24 +----------- src/hyperlight_guest_bin/src/init.rs | 38 +++++++++++++++++++ src/hyperlight_guest_bin/src/lib.rs | 3 ++ 3 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 src/hyperlight_guest_bin/src/init.rs diff --git a/src/hyperlight_guest_bin/src/arch/amd64/init.rs b/src/hyperlight_guest_bin/src/arch/amd64/init.rs index ef49f1557..4dfa7e2f8 100644 --- a/src/hyperlight_guest_bin/src/arch/amd64/init.rs +++ b/src/hyperlight_guest_bin/src/arch/amd64/init.rs @@ -104,28 +104,6 @@ unsafe fn init_tss(pc: *mut ProcCtrl) { } } -/// To initialise the main stack, we just pre-emptively map the first -/// page of it. -unsafe fn init_stack() -> u64 { - use hyperlight_guest::layout::MAIN_STACK_TOP_GVA; - let stack_top_page_base = (MAIN_STACK_TOP_GVA - 1) & !0xfff; - unsafe { - use hyperlight_common::vmem::{BasicMapping, MappingKind, PAGE_SIZE}; - crate::paging::map_region( - hyperlight_guest::prim_alloc::alloc_phys_pages(1), - stack_top_page_base as *mut u8, - PAGE_SIZE as u64, - MappingKind::Basic(BasicMapping { - readable: true, - writable: true, - executable: false, - }), - ); - crate::paging::barrier::first_valid_same_ctx(); - } - MAIN_STACK_TOP_GVA -} - /// Machine-specific initialisation; calls [`crate::generic_init`] /// once stack, CoW, etc have been set up. #[unsafe(no_mangle)] @@ -138,7 +116,7 @@ pub extern "C" fn entrypoint(peb_address: u64, seed: u64, ops: u64, max_log_leve init_gdt(pc); init_tss(pc); init_idt(pc); - let stack_top = init_stack(); + let stack_top = crate::init::init_stack(); // Architecture early init is complete! We pivot now to // executing on the main stack, and jump into generic diff --git a/src/hyperlight_guest_bin/src/init.rs b/src/hyperlight_guest_bin/src/init.rs new file mode 100644 index 000000000..824a51576 --- /dev/null +++ b/src/hyperlight_guest_bin/src/init.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. + */ + +/// To initialise the main stack, we just pre-emptively map the first +/// page of it. We assume the architecture-specific exception handler +/// will allocate pages on fault as necessary +pub(crate) unsafe fn init_stack() -> u64 { + use hyperlight_common::vmem::{BasicMapping, MappingKind, PAGE_SIZE}; + use hyperlight_guest::layout::MAIN_STACK_TOP_GVA; + let stack_top_page_base = (MAIN_STACK_TOP_GVA - 1) & !(PAGE_SIZE as u64 - 1); + unsafe { + crate::paging::map_region( + hyperlight_guest::prim_alloc::alloc_phys_pages(1), + stack_top_page_base as *mut u8, + PAGE_SIZE as u64, + MappingKind::Basic(BasicMapping { + readable: true, + writable: true, + executable: false, + }), + ); + crate::paging::barrier::first_valid_same_ctx(); + } + MAIN_STACK_TOP_GVA +} diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index af1511d1e..10b0d5484 100644 --- a/src/hyperlight_guest_bin/src/lib.rs +++ b/src/hyperlight_guest_bin/src/lib.rs @@ -59,6 +59,9 @@ pub mod paging; #[cfg(feature = "libc")] mod libc_stubs; +/// Shared initialisation code used by multiple architectures +mod init; + /// Re-export the libc bindings from hyperlight-libc when the libc feature is enabled. #[cfg(feature = "libc")] pub use hyperlight_libc as libc; From bcb454a3cd2f11be0f15e3d67bf1316d6835733d Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 14:46:39 +0100 Subject: [PATCH 11/29] Extract UpdateParentTable from amd64 arch-specific to generic vmem code This will be useful on multiple architectures. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/hyperlight_common/src/arch/amd64/vmem.rs | 25 +++--------------- src/hyperlight_common/src/vmem.rs | 27 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/src/hyperlight_common/src/arch/amd64/vmem.rs b/src/hyperlight_common/src/arch/amd64/vmem.rs index e8947b246..f39a93274 100644 --- a/src/hyperlight_common/src/arch/amd64/vmem.rs +++ b/src/hyperlight_common/src/arch/amd64/vmem.rs @@ -27,31 +27,12 @@ limitations under the License. use crate::vmem::{ BasicMapping, CowMapping, MapRequest, MapResponse, Mapping, MappingKind, TableMovabilityBase, - TableOps, TableReadOps, UpdateParent, UpdateParentNone, Void, modify_ptes, + TableOps, TableReadOps, UpdateParent, UpdateParentNone, UpdateParentTable, Void, modify_ptes, write_entry_updating, }; -/// Parent is another page table whose ancestors may also need -/// updating when it relocates. -pub struct UpdateParentTable> { - pub(crate) parent: P, - pub(crate) entry_ptr: Op::TableAddr, -} -impl> Clone for UpdateParentTable { - fn clone(&self) -> Self { - *self - } -} -impl> Copy for UpdateParentTable {} -impl> UpdateParentTable { - pub(crate) fn new(parent: P, entry_ptr: Op::TableAddr) -> Self { - UpdateParentTable { parent, entry_ptr } - } -} - -/// Parent is the root (e.g. CR3). #[derive(Copy, Clone)] -pub struct UpdateParentRoot {} +pub(in crate::vmem) struct UpdateParentRoot {} /// Read a PTE and return it (widened to u64) if the present bit is /// set. The amd64 "present" encoding is a single bit (bit 0); other @@ -162,7 +143,7 @@ fn pte_for_table(table_addr: Op::TableAddr) -> u64 { /// This trait is used to select appropriate implementations of /// [`UpdateParent`] to be used, depending on whether a particular /// implementation needs the ability to move tables. -pub trait TableMovability { +pub(in crate::vmem) trait TableMovability { type RootUpdateParent: UpdateParent; fn root_update_parent() -> Self::RootUpdateParent; } diff --git a/src/hyperlight_common/src/vmem.rs b/src/hyperlight_common/src/vmem.rs index 47a70a3f1..f90fe0c64 100644 --- a/src/hyperlight_common/src/vmem.rs +++ b/src/hyperlight_common/src/vmem.rs @@ -98,6 +98,33 @@ impl UpdateParent for UpdateParentNone { } } +/// A struct implementing [`UpdateParent`] to be used when a table's +/// parent is another table that needs to be updated recursively. +#[allow(unused)] // not all architectures use UpdateParentTable +pub(in crate::vmem) struct UpdateParentTable> { + pub(in crate::vmem) parent: P, + pub(in crate::vmem) entry_ptr: Op::TableAddr, +} +impl> Clone for UpdateParentTable { + fn clone(&self) -> Self { + *self + } +} +impl> Copy for UpdateParentTable {} +impl> UpdateParentTable { + #[allow(unused)] // not all architectures use UpdateParentTable + pub(in crate::vmem) fn new(parent: P, entry_ptr: Op::TableAddr) -> Self { + UpdateParentTable { parent, entry_ptr } + } +} + +/// A struct implementing [`UpdateParent`] to be used when a table's +/// parent is the "root table" (with access to that root pointer +/// provided in an architecture/environment-insensitive manner via +/// `TableOps`) +#[derive(Copy, Clone)] +pub struct UpdateParentRoot {} + /// A helper structure indicating a mapping operation that needs to be /// performed. pub(in crate::vmem) struct MapRequest> { From 8b74b00bfc7738ec55f6cf5b957226976acd0d39 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 14:57:13 +0100 Subject: [PATCH 12/29] aarch64: add memory layout This includes adding the concept of a fixed "IO page" to the memory layout, to be used on architectures where MMIO is convenient. The IO page is above the scratch region at the top of memory, and is mapped in the guest, but not in the host, so that accesses to it will trigger vmexits. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- docs/paging-development-notes.md | 6 +++++ .../src/arch/aarch64/layout.rs | 23 +++++++++++++------ .../src/arch/amd64/layout.rs | 4 ++++ src/hyperlight_common/src/layout.rs | 4 +++- .../src/arch/aarch64/layout.rs | 11 +++++---- .../src/sandbox/snapshot/mod.rs | 16 ++++++++++++- 6 files changed, 50 insertions(+), 14 deletions(-) diff --git a/docs/paging-development-notes.md b/docs/paging-development-notes.md index 573c2f77b..da08f6f24 100644 --- a/docs/paging-development-notes.md +++ b/docs/paging-development-notes.md @@ -172,3 +172,9 @@ below the exception stack within the scratch region. Hyperlight unconditionally uses 48-bit virtual addresses (4-level paging) and enables PAE. The guest is always entered in long mode. + +## aarch64 + +Hyperlight unconditionally uses 48-bit virtual addresses. Hyperlight +presently only uses addresses in the lower (ttbr0) half of the address range. + diff --git a/src/hyperlight_common/src/arch/aarch64/layout.rs b/src/hyperlight_common/src/arch/aarch64/layout.rs index 04d368606..cb32cfe8e 100644 --- a/src/hyperlight_common/src/arch/aarch64/layout.rs +++ b/src/hyperlight_common/src/arch/aarch64/layout.rs @@ -14,12 +14,21 @@ 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 SCRATCH_TOP_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 SCRATCH_TOP_GPA: usize = 0x0000_000f_ffff_ffff; +// TODO: consider using the upper half, like we do on x86; +// this would require enabling ttbr1 +pub const SCRATCH_TOP_GVA: usize = 0x0000_ffff_ffff_dfff; +pub const SNAPSHOT_PT_GVA_MIN: usize = 0x0000_8000_0000_0000; +pub const SNAPSHOT_PT_GVA_MAX: usize = 0x0000_80ff_ffff_ffff; +pub const SCRATCH_TOP_GPA: usize = 0x0000_000f_ffff_efff; -pub fn min_scratch_size(_input_data_size: usize, _output_data_size: usize) -> usize { - unimplemented!("min_scratch_size") +pub const IO_PAGE_GVA: u64 = 0x0000_ffff_ffff_e000; +pub const IO_PAGE_GPA: u64 = 0x0000_000f_ffff_f000; + +pub const fn io_page() -> Option<(crate::vmem::PhysAddr, crate::vmem::VirtAddr)> { + Some((IO_PAGE_GPA, IO_PAGE_GVA)) +} + +pub fn min_scratch_size(input_data_size: usize, output_data_size: usize) -> usize { + (input_data_size + output_data_size).next_multiple_of(crate::vmem::PAGE_SIZE) + + 12 * crate::vmem::PAGE_SIZE } diff --git a/src/hyperlight_common/src/arch/amd64/layout.rs b/src/hyperlight_common/src/arch/amd64/layout.rs index 5389ccdab..4237caf51 100644 --- a/src/hyperlight_common/src/arch/amd64/layout.rs +++ b/src/hyperlight_common/src/arch/amd64/layout.rs @@ -31,6 +31,10 @@ pub const SNAPSHOT_PT_GVA_MAX: usize = 0xffff_80ff_ffff_ffff; /// ever memory-constrained. pub const SCRATCH_TOP_GPA: usize = 0x0000_000f_ffff_ffff; +pub fn io_page() -> Option<(u64, u64)> { + None +} + /// On amd64, this is: /// - Two pages for the TSS and IDT /// - (up to) 4 pages for the PTEs for mapping that (including CoW'ing the root PT) diff --git a/src/hyperlight_common/src/layout.rs b/src/hyperlight_common/src/layout.rs index b12028219..bf25a2e0c 100644 --- a/src/hyperlight_common/src/layout.rs +++ b/src/hyperlight_common/src/layout.rs @@ -18,7 +18,9 @@ limitations under the License. #[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/layout.rs")] mod arch; -pub use arch::{SCRATCH_TOP_GPA, SCRATCH_TOP_GVA, SNAPSHOT_PT_GVA_MAX, SNAPSHOT_PT_GVA_MIN}; +pub use arch::{ + SCRATCH_TOP_GPA, SCRATCH_TOP_GVA, SNAPSHOT_PT_GVA_MAX, SNAPSHOT_PT_GVA_MIN, io_page, +}; // offsets down from the top of scratch memory for various things pub const SCRATCH_TOP_SIZE_OFFSET: u64 = 0x08; diff --git a/src/hyperlight_guest/src/arch/aarch64/layout.rs b/src/hyperlight_guest/src/arch/aarch64/layout.rs index 685447ce7..9902da074 100644 --- a/src/hyperlight_guest/src/arch/aarch64/layout.rs +++ b/src/hyperlight_guest/src/arch/aarch64/layout.rs @@ -15,17 +15,18 @@ limitations under the License. */ // TODO(aarch64): these values are placeholders copied from amd64 -pub const MAIN_STACK_TOP_GVA: u64 = 0xffff_ff00_0000_0000; -pub const MAIN_STACK_LIMIT_GVA: u64 = 0xffff_fe00_0000_0000; +pub const MAIN_STACK_TOP_GVA: u64 = 0x0000_ff00_0000_0000; +pub const MAIN_STACK_LIMIT_GVA: u64 = 0x0000_fe00_0000_0000; pub fn scratch_size() -> u64 { - unimplemented!("aarch64 scratch_size") + let addr = crate::layout::scratch_size_gva(); + unsafe { addr.read_volatile() } } pub fn scratch_base_gpa() -> u64 { - unimplemented!("aarch64 scratch_base_gpa") + hyperlight_common::layout::scratch_base_gpa(scratch_size() as usize) } pub fn scratch_base_gva() -> u64 { - unimplemented!("aarch64 scratch_base_gva") + hyperlight_common::layout::scratch_base_gva(scratch_size() as usize) } diff --git a/src/hyperlight_host/src/sandbox/snapshot/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/mod.rs index 8da0c1e48..c8d56323b 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/mod.rs @@ -21,7 +21,7 @@ use std::collections::{BTreeMap, HashMap}; pub use file::reference::{OciDigest, OciReference, OciTag}; use hyperlight_common::flatbuffer_wrappers::host_function_details::HostFunctionDetails; -use hyperlight_common::layout::{scratch_base_gpa, scratch_base_gva}; +use hyperlight_common::layout::{io_page, scratch_base_gpa, scratch_base_gva}; use hyperlight_common::vmem; use hyperlight_common::vmem::{ BasicMapping, CowMapping, Mapping, MappingKind, PAGE_SIZE, SpaceAwareMapping, SpaceId, TableOps, @@ -249,6 +249,20 @@ unsafe fn guest_page<'a>( } fn map_specials(pt_buf: &GuestPageTableBuffer, scratch_size: usize) { + if let Some((phys_base, virt_base)) = io_page() { + // Map the IO page + let mapping = Mapping { + phys_base, + virt_base, + len: PAGE_SIZE as u64, + kind: MappingKind::Basic(BasicMapping { + readable: true, + writable: true, + executable: false, + }), + }; + unsafe { vmem::map(pt_buf, mapping) }; + } // Map the scratch region let mapping = Mapping { phys_base: scratch_base_gpa(scratch_size), From b4214f454218d2b9888cec28a810b1ae44e68b4a Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 15:45:43 +0100 Subject: [PATCH 13/29] hyperlight_guest_bin: move exposed virtual memory operations to arch-specific module This changes hyperlight_guest_bin::paging to be a reexport of the interface of an architecture-specific implementation, much like hyperlight_common::vmem. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/arch/aarch64/layout.rs | 19 ++ .../src/arch/amd64/paging.rs | 201 ++++++++++++++++++ src/hyperlight_guest_bin/src/lib.rs | 1 - src/hyperlight_guest_bin/src/paging.rs | 179 +--------------- 4 files changed, 227 insertions(+), 173 deletions(-) create mode 100644 src/hyperlight_guest_bin/src/arch/aarch64/layout.rs create mode 100644 src/hyperlight_guest_bin/src/arch/amd64/paging.rs diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/layout.rs b/src/hyperlight_guest_bin/src/arch/aarch64/layout.rs new file mode 100644 index 000000000..27833ec23 --- /dev/null +++ b/src/hyperlight_guest_bin/src/arch/aarch64/layout.rs @@ -0,0 +1,19 @@ +/* +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. + */ + +// The addresses in this file should be coordinated with +// src/hyperlight_common/src/arch/aarc64/layout.rs and +// src/hyperlight_guest/src/arch/aarch64/layout.rs diff --git a/src/hyperlight_guest_bin/src/arch/amd64/paging.rs b/src/hyperlight_guest_bin/src/arch/amd64/paging.rs new file mode 100644 index 000000000..f61d2b658 --- /dev/null +++ b/src/hyperlight_guest_bin/src/arch/amd64/paging.rs @@ -0,0 +1,201 @@ +/* +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 core::arch::asm; + +use hyperlight_common::vmem; +use hyperlight_guest::prim_alloc::alloc_phys_pages; + +// TODO: This is not at all thread-safe atm +// TODO: A lot of code in this file uses inline assembly to load and +// store page table entries. It would be nice to use pointer +// volatile read/writes instead, but unfortunately we have a PTE +// at physical address 0, which is currently identity-mapped at +// virtual address 0, and Rust raw pointer operations can't be +// used to read/write from address 0. + +#[derive(Copy, Clone)] +struct GuestMappingOperations { + scratch_base_gpa: u64, + scratch_base_gva: u64, +} +impl GuestMappingOperations { + fn new() -> Self { + Self { + scratch_base_gpa: hyperlight_guest::layout::scratch_base_gpa(), + scratch_base_gva: hyperlight_guest::layout::scratch_base_gva(), + } + } + fn try_phys_to_virt(&self, addr: u64) -> Option<*mut u8> { + if addr >= self.scratch_base_gpa { + Some((self.scratch_base_gva + (addr - self.scratch_base_gpa)) as *mut u8) + } else { + None + } + } + fn phys_to_virt(&self, addr: u64) -> *mut u8 { + self.try_phys_to_virt(addr) + .expect("phys_to_virt encountered snapshot non-PT page") + } +} +// for virt_to_phys +impl core::convert::AsRef for GuestMappingOperations { + fn as_ref(&self) -> &Self { + self + } +} +impl vmem::TableReadOps for GuestMappingOperations { + type TableAddr = u64; + fn entry_addr(addr: u64, offset: u64) -> u64 { + addr + offset + } + unsafe fn read_entry(&self, addr: u64) -> u64 { + let addr = self.phys_to_virt(addr); + let ret: u64; + unsafe { + asm!("mov {}, qword ptr [{}]", out(reg) ret, in(reg) addr); + } + ret + } + fn to_phys(addr: u64) -> u64 { + addr + } + fn from_phys(addr: u64) -> u64 { + addr + } + fn root_table(&self) -> u64 { + let pml4_base: u64; + unsafe { + asm!("mov {}, cr3", out(reg) pml4_base); + } + pml4_base & !0xfff + } +} + +impl vmem::TableOps for GuestMappingOperations { + // Currently, we don't actually move tables anywhere on amd64 + // because of issues with guest PTs in IPAs that are mapped + // readonly in Stage 2 translation. However, this code all works + // and will re-enabled as soon as there is improved + // architecture/hypervisor support. + type TableMovability = vmem::MayMoveTable; + unsafe fn alloc_table(&self) -> u64 { + let page_addr = unsafe { alloc_phys_pages(1) }; + unsafe { + self.phys_to_virt(page_addr) + .write_bytes(0u8, vmem::PAGE_TABLE_SIZE) + }; + page_addr + } + unsafe fn write_entry(&self, addr: u64, entry: u64) -> Option { + let addr = self.phys_to_virt(addr); + unsafe { + asm!("mov qword ptr [{}], {}", in(reg) addr, in(reg) entry); + } + None + } + unsafe fn update_root(&self, new_root: u64) { + unsafe { + core::arch::asm!("mov cr3, {}", in(reg) ::to_phys(new_root)); + } + } +} + +/// Assumption: all are page-aligned +/// # Safety +/// This function modifies pages backing a virtual memory range which is inherently unsafe w.r.t. +/// the Rust memory model. +/// When using this function note: +/// - No locking is performed before touching page table data structures, +/// as such do not use concurrently with any other page table operations +/// - TLB invalidation is not performed, +/// if previously-unmapped ranges are not being mapped, TLB invalidation may need to be performed afterwards. +pub unsafe fn map_region(phys_base: u64, virt_base: *mut u8, len: u64, kind: vmem::MappingKind) { + unsafe { + vmem::map( + &GuestMappingOperations::new(), + vmem::Mapping { + phys_base, + virt_base: virt_base as u64, + len, + kind, + }, + ); + } +} + +pub fn virt_to_phys(gva: vmem::VirtAddr) -> impl Iterator { + unsafe { vmem::virt_to_phys::<_>(GuestMappingOperations::new(), gva, 1) } +} + +pub fn phys_to_virt(gpa: vmem::PhysAddr) -> Option<*mut u8> { + GuestMappingOperations::new().try_phys_to_virt(gpa) +} + +/// Barriers that other code may need to use when updating page tables +pub mod barrier { + /// Call this function when a virtual address has just been made + /// valid for the first time after the last tlb invalidate that + /// affected it, and it will be used for the first time in the + /// same execution context as has made the modification. + /// + /// On most architectures, TLBs will not cache invalid entries, so + /// this does not need to issue a TLB. However, it does need to + /// ensure coherency between the previous writes and any future + /// uses by a page table walker. + /// + /// # Architecture-specific (amd64) notes + /// + /// The exact details around page walk coherency on amd64 seem a + /// bit fuzzy. The Intel manual notes that a serialising + /// instruction is necessary specifically to synchronise table + /// walks performed during instruction fetch [1], but is + /// relatively quiet about other page walks. The AMD manual notes + /// [2] that "a table entry is allowed to be upgraded (by marking + /// it as present, or by removing its write, execute or supervisor + /// restrictions) without explicitly maintaining TLB coherency", + /// but only states that TLB any upper-level TLB cache entries + /// will be flushed before re-walking to confirm the fault, which + /// does not clearly seem strong enough. + /// + /// In some limited testing, `mfence` typically seems to be + /// enough, but as it is not a serializing instruction on Intel + /// platforms, we assume it may not be quite good enough. `cpuid` + /// is likely to be very slow, since we are definitely running + /// under a hypervisor (and often even nested). Currently, for + /// simplicity's sake, this just copies cr0 to itself, but other + /// options (including the `serialize` instruction where + /// available) could be worth exploring. + /// + /// [1] Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3: System Programming Guide + /// Chapter 5: Paging + /// §5.10: Caching Translation Information + /// §5.10.4: Invalidation of TLBs and Paging-Structure Caches + /// §5.10.4.3: Optional Invalidation + /// [2] AMD64 Architecture Programmer's Manual, Volume 2: System Programming + /// Section 5: Page Translation and Protection + /// §5.5: Translation-Lookaside Buffer + /// §5.5.3: TLB Management + #[inline(always)] + pub fn first_valid_same_ctx() { + unsafe { + core::arch::asm!(" + mov rax, cr0 + mov cr0, rax + ", out("rax") _); + } + } +} diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index 10b0d5484..5df92f647 100644 --- a/src/hyperlight_guest_bin/src/lib.rs +++ b/src/hyperlight_guest_bin/src/lib.rs @@ -51,7 +51,6 @@ pub mod error; pub mod guest_logger; pub mod host_comm; pub mod memory; -#[cfg(target_arch = "x86_64")] pub mod paging; /// Bridge between picolibc's POSIX expectations and the Hyperlight host. diff --git a/src/hyperlight_guest_bin/src/paging.rs b/src/hyperlight_guest_bin/src/paging.rs index f61d2b658..9b559f6c2 100644 --- a/src/hyperlight_guest_bin/src/paging.rs +++ b/src/hyperlight_guest_bin/src/paging.rs @@ -14,137 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -use core::arch::asm; - -use hyperlight_common::vmem; -use hyperlight_guest::prim_alloc::alloc_phys_pages; - -// TODO: This is not at all thread-safe atm -// TODO: A lot of code in this file uses inline assembly to load and -// store page table entries. It would be nice to use pointer -// volatile read/writes instead, but unfortunately we have a PTE -// at physical address 0, which is currently identity-mapped at -// virtual address 0, and Rust raw pointer operations can't be -// used to read/write from address 0. - -#[derive(Copy, Clone)] -struct GuestMappingOperations { - scratch_base_gpa: u64, - scratch_base_gva: u64, -} -impl GuestMappingOperations { - fn new() -> Self { - Self { - scratch_base_gpa: hyperlight_guest::layout::scratch_base_gpa(), - scratch_base_gva: hyperlight_guest::layout::scratch_base_gva(), - } - } - fn try_phys_to_virt(&self, addr: u64) -> Option<*mut u8> { - if addr >= self.scratch_base_gpa { - Some((self.scratch_base_gva + (addr - self.scratch_base_gpa)) as *mut u8) - } else { - None - } - } - fn phys_to_virt(&self, addr: u64) -> *mut u8 { - self.try_phys_to_virt(addr) - .expect("phys_to_virt encountered snapshot non-PT page") - } -} -// for virt_to_phys -impl core::convert::AsRef for GuestMappingOperations { - fn as_ref(&self) -> &Self { - self - } -} -impl vmem::TableReadOps for GuestMappingOperations { - type TableAddr = u64; - fn entry_addr(addr: u64, offset: u64) -> u64 { - addr + offset - } - unsafe fn read_entry(&self, addr: u64) -> u64 { - let addr = self.phys_to_virt(addr); - let ret: u64; - unsafe { - asm!("mov {}, qword ptr [{}]", out(reg) ret, in(reg) addr); - } - ret - } - fn to_phys(addr: u64) -> u64 { - addr - } - fn from_phys(addr: u64) -> u64 { - addr - } - fn root_table(&self) -> u64 { - let pml4_base: u64; - unsafe { - asm!("mov {}, cr3", out(reg) pml4_base); - } - pml4_base & !0xfff - } -} - -impl vmem::TableOps for GuestMappingOperations { - // Currently, we don't actually move tables anywhere on amd64 - // because of issues with guest PTs in IPAs that are mapped - // readonly in Stage 2 translation. However, this code all works - // and will re-enabled as soon as there is improved - // architecture/hypervisor support. - type TableMovability = vmem::MayMoveTable; - unsafe fn alloc_table(&self) -> u64 { - let page_addr = unsafe { alloc_phys_pages(1) }; - unsafe { - self.phys_to_virt(page_addr) - .write_bytes(0u8, vmem::PAGE_TABLE_SIZE) - }; - page_addr - } - unsafe fn write_entry(&self, addr: u64, entry: u64) -> Option { - let addr = self.phys_to_virt(addr); - unsafe { - asm!("mov qword ptr [{}], {}", in(reg) addr, in(reg) entry); - } - None - } - unsafe fn update_root(&self, new_root: u64) { - unsafe { - core::arch::asm!("mov cr3, {}", in(reg) ::to_phys(new_root)); - } - } -} - -/// Assumption: all are page-aligned -/// # Safety -/// This function modifies pages backing a virtual memory range which is inherently unsafe w.r.t. -/// the Rust memory model. -/// When using this function note: -/// - No locking is performed before touching page table data structures, -/// as such do not use concurrently with any other page table operations -/// - TLB invalidation is not performed, -/// if previously-unmapped ranges are not being mapped, TLB invalidation may need to be performed afterwards. -pub unsafe fn map_region(phys_base: u64, virt_base: *mut u8, len: u64, kind: vmem::MappingKind) { - unsafe { - vmem::map( - &GuestMappingOperations::new(), - vmem::Mapping { - phys_base, - virt_base: virt_base as u64, - len, - kind, - }, - ); - } -} - -pub fn virt_to_phys(gva: vmem::VirtAddr) -> impl Iterator { - unsafe { vmem::virt_to_phys::<_>(GuestMappingOperations::new(), gva, 1) } -} - -pub fn phys_to_virt(gpa: vmem::PhysAddr) -> Option<*mut u8> { - GuestMappingOperations::new().try_phys_to_virt(gpa) -} +#[cfg_attr(target_arch = "x86_64", path = "arch/amd64/paging.rs")] +#[cfg_attr(target_arch = "aarch64", path = "arch/aarch64/paging.rs")] +mod arch; +pub use arch::{map_region, phys_to_virt, virt_to_phys}; /// Barriers that other code may need to use when updating page tables pub mod barrier { /// Call this function when a virtual address has just been made @@ -156,46 +30,7 @@ pub mod barrier { /// this does not need to issue a TLB. However, it does need to /// ensure coherency between the previous writes and any future /// uses by a page table walker. - /// - /// # Architecture-specific (amd64) notes - /// - /// The exact details around page walk coherency on amd64 seem a - /// bit fuzzy. The Intel manual notes that a serialising - /// instruction is necessary specifically to synchronise table - /// walks performed during instruction fetch [1], but is - /// relatively quiet about other page walks. The AMD manual notes - /// [2] that "a table entry is allowed to be upgraded (by marking - /// it as present, or by removing its write, execute or supervisor - /// restrictions) without explicitly maintaining TLB coherency", - /// but only states that TLB any upper-level TLB cache entries - /// will be flushed before re-walking to confirm the fault, which - /// does not clearly seem strong enough. - /// - /// In some limited testing, `mfence` typically seems to be - /// enough, but as it is not a serializing instruction on Intel - /// platforms, we assume it may not be quite good enough. `cpuid` - /// is likely to be very slow, since we are definitely running - /// under a hypervisor (and often even nested). Currently, for - /// simplicity's sake, this just copies cr0 to itself, but other - /// options (including the `serialize` instruction where - /// available) could be worth exploring. - /// - /// [1] Intel 64 and IA-32 Architectures Software Developer's Manual, Volume 3: System Programming Guide - /// Chapter 5: Paging - /// §5.10: Caching Translation Information - /// §5.10.4: Invalidation of TLBs and Paging-Structure Caches - /// §5.10.4.3: Optional Invalidation - /// [2] AMD64 Architecture Programmer's Manual, Volume 2: System Programming - /// Section 5: Page Translation and Protection - /// §5.5: Translation-Lookaside Buffer - /// §5.5.3: TLB Management - #[inline(always)] - pub fn first_valid_same_ctx() { - unsafe { - core::arch::asm!(" - mov rax, cr0 - mov cr0, rax - ", out("rax") _); - } - } + pub use arch::first_valid_same_ctx; + + use super::arch::barrier as arch; } From 80f9710a9413819f876bac350b4de7650e15fca1 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 15:53:23 +0100 Subject: [PATCH 14/29] aarch64: add virtual memory operations Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/arch/aarch64/vmem.rs | 521 +++++++++++++++++- src/hyperlight_common/src/vmem.rs | 8 + .../src/arch/aarch64/paging.rs | 177 ++++++ 3 files changed, 679 insertions(+), 27 deletions(-) create mode 100644 src/hyperlight_guest_bin/src/arch/aarch64/paging.rs diff --git a/src/hyperlight_common/src/arch/aarch64/vmem.rs b/src/hyperlight_common/src/arch/aarch64/vmem.rs index 3c83cabc8..64a9ceeed 100644 --- a/src/hyperlight_common/src/arch/aarch64/vmem.rs +++ b/src/hyperlight_common/src/arch/aarch64/vmem.rs @@ -14,9 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -// TODO(aarch64): implement real page table operations +use alloc::collections::BTreeMap; +use alloc::collections::btree_map::Entry; +use alloc::rc::Rc; +use alloc::vec::Vec; +use core::cell::RefCell; -use crate::vmem::{Mapping, TableOps, TableReadOps, Void}; +use crate::vmem::sealed::TableMovabilityBase; +use crate::vmem::{ + BasicMapping, CowMapping, MapRequest, MapResponse, Mapping, MappingKind, SpaceAwareMapping, + SpaceId, SpaceReferenceMapping, TableOps, TableReadOps, UpdateParent, UpdateParentNone, + UpdateParentTable, Void, bits, modify_ptes, write_entry_updating, +}; pub const PAGE_SIZE: usize = 4096; pub const PAGE_TABLE_SIZE: usize = 4096; @@ -26,38 +35,500 @@ pub type PageTableEntry = u64; pub type VirtAddr = u64; pub type PhysAddr = u64; +const VA_BITS: usize = 48; +pub const ATTR_INDEX_NORMAL: u8 = 0; +const SOFTWARE_USE_COW: u8 = 0b1; + +// Utility structures +impl< + Op: TableOps, + P: UpdateParent, +> UpdateParent for UpdateParentTable +{ + type TableMoveInfo = Op::TableAddr; + type ChildType = UpdateParentTable; + fn update_parent(self, op: &Op, new_ptr: Op::TableAddr) { + let pte = desc_for_table::(new_ptr); + unsafe { + write_entry_updating(op, self.parent, self.entry_ptr, pte); + } + } + fn for_child_at_entry(self, entry_ptr: Op::TableAddr) -> Self::ChildType { + Self::ChildType::new(self, entry_ptr) + } +} +#[derive(Copy, Clone)] +pub(in crate::vmem) struct UpdateParentRoot {} +impl> UpdateParent + for UpdateParentRoot +{ + type TableMoveInfo = Op::TableAddr; + type ChildType = UpdateParentTable; + fn update_parent(self, op: &Op, new_ptr: Op::TableAddr) { + unsafe { + op.update_root(new_ptr); + } + } + fn for_child_at_entry(self, entry_ptr: Op::TableAddr) -> Self::ChildType { + Self::ChildType::new(self, entry_ptr) + } +} +/// This trait is used to select appropriate implementations of +/// [`UpdateParent`] to be used, depending on whether a particular +/// implementation needs the ability to move tables. +pub(in crate::vmem) trait TableMovability { + type RootUpdateParent: UpdateParent; + fn root_update_parent() -> Self::RootUpdateParent; +} +impl> TableMovability + for crate::vmem::MayMoveTable +{ + type RootUpdateParent = UpdateParentRoot; + fn root_update_parent() -> Self::RootUpdateParent { + UpdateParentRoot {} + } +} +impl TableMovability for crate::vmem::MayNotMoveTable { + type RootUpdateParent = UpdateParentNone; + fn root_update_parent() -> Self::RootUpdateParent { + UpdateParentNone {} + } +} + +#[allow(clippy::identity_op)] +#[allow(clippy::precedence)] +fn desc_for_table(table_addr: Op::TableAddr) -> u64 { + Op::to_phys(table_addr) | + // Don't set APTable[1:0] - we don't use hierarchical permissions + // Don't set {U,P,}XNTable - we don't use hierarchical permissions + // Don't set set Protected - we don't use FEAT_THE + // We don't need to set AF on a table descriptor to avoid AF + // faults. Since we don't enable FEAT_HAFT, there is no AF on + // table descriptors, only on page descriptors. + 0b11 // table descriptor +} + +// We do not currently use hugepage mappings in the guest, and so we +// do not need to worry about block descriptors at intermediate +// levels. + +#[allow(clippy::identity_op)] +#[allow(clippy::precedence)] +fn desc_for_page( + page_addr: u64, + _readable: bool, + writable: bool, + executable: bool, + software_use: u8, + user_accessible: bool, +) -> u64 { + // todo: make use of the Contiguous bit to reduce tlb pressure + let xn = match (executable, user_accessible) { + (true, true) => 0, + (true, false) => 0b10, + (false, _) => 0b11, + }; + let ap = match (writable, user_accessible) { + (true, true) => 0b01, + (true, false) => 0b00, + (false, true) => 0b11, + (false, false) => 0b10, + }; + page_addr | + ((software_use as u64 & 0xf) << 55) | + (xn << 53) | + // we do not use hardware management of the dirty state + // If we support hugepage block descriptors in the future, we + // will need to support setting the nT bit here when the + // hardware supports FEAT_BBM Level 1 + (0b1 << 11) | // always set nG for now, since multi-space + // support is not properly reflected in the + // mapping API. + (0b1 << 10) | // we don't need AF tracking, so set it always + (0b11 << 8) | // Inner Shareable + (ap << 6) | + ((ATTR_INDEX_NORMAL as u64) << 2) | + 0b11 +} + +#[allow(clippy::identity_op)] +#[allow(clippy::precedence)] +// Produces only page descriptors valid at Level 3; there is presently +// no support for block descriptors valid at earlier levels +unsafe fn map_page< + Op: TableOps, + P: UpdateParent< + Op, + TableMoveInfo = >::TableMoveInfo, + >, +>( + op: &Op, + mapping: &Mapping, + r: MapResponse, +) { + let presumed_base = mapping.phys_base + (r.vmin - mapping.virt_base); + let desc = match &mapping.kind { + MappingKind::Basic(bm) => desc_for_page( + presumed_base, + bm.readable, + bm.writable, + bm.executable, + 0, + false, + ), + MappingKind::Cow(cm) => desc_for_page( + presumed_base, + cm.readable, + false, + cm.executable, + SOFTWARE_USE_COW, + false, + ), + MappingKind::Unmapped => 0, + }; + unsafe { + write_entry_updating(op, r.update_parent, r.entry_ptr, desc); + } +} + +enum FinalLevelDescriptorKind { + Page, +} +enum EarlyLevelDescriptorKind { + Block, + Table, +} +fn final_level_descriptor_kind(desc: u64) -> Option { + if desc & 3 == 3 { + Some(FinalLevelDescriptorKind::Page) + } else { + None + } +} +fn early_level_descriptor_kind(desc: u64) -> Option { + match desc & 0b11 { + 0b01 => Some(EarlyLevelDescriptorKind::Block), + 0b11 => Some(EarlyLevelDescriptorKind::Table), + _ => None, + } +} + +unsafe fn next_level_table_if_present( + op: &Op, + addr: Op::TableAddr, +) -> Option { + let desc: u64 = unsafe { op.read_entry(addr) }; + if let Some(EarlyLevelDescriptorKind::Table) = early_level_descriptor_kind(desc) { + Some(Op::from_phys(bits::<47, 12>(desc) << 12)) + } else { + None + } +} + +/// Page-mapping callback to allocate a next-level page table if necessary. +/// +/// Should only be called on a [`MapResponse`] representing an entry +/// at level < 3, since it allocates a next-level table. +/// # Safety +/// This function modifies page table data structures, and should not be called concurrently +/// with any other operations that modify the page tables. +unsafe fn alloc_table_if_needed< + Op: TableOps, + P: UpdateParent< + Op, + TableMoveInfo = >::TableMoveInfo, + >, +>( + op: &Op, + x: MapResponse, +) -> MapRequest +where + P::ChildType: UpdateParent, +{ + let new_update_parent = x.update_parent.for_child_at_entry(x.entry_ptr); + if let Some(table_base) = unsafe { next_level_table_if_present(op, x.entry_ptr) } { + return MapRequest { + table_base, + vmin: x.vmin, + len: x.len, + update_parent: new_update_parent, + }; + } + // If we eventually support huge pages, we will need to check if + // there was a Block descriptor here and follow the + // break-before-make sequence. + + let page_addr = unsafe { op.alloc_table() }; + + let pte = desc_for_table::(page_addr); + unsafe { + write_entry_updating(op, x.update_parent, x.entry_ptr, pte); + }; + MapRequest { + table_base: page_addr, + vmin: x.vmin, + len: x.len, + update_parent: new_update_parent, + } +} + +unsafe fn require_table_exist>( + op: &Op, + x: MapResponse, +) -> Option> +where + P::ChildType: UpdateParent, +{ + unsafe { + next_level_table_if_present(op, x.entry_ptr).map(|table_base| MapRequest { + table_base, + vmin: x.vmin, + len: x.len, + update_parent: x.update_parent.for_child_at_entry(x.entry_ptr), + }) + } +} + +enum WalkNextLevelResponse> { + WalkNextLevel(MapResponse), + AlreadySeen(SpaceReferenceMapping), +} + +enum WalkNextLevelRequest> { + WalkNextLevel(MapRequest), + AlreadySeen(SpaceReferenceMapping), +} +fn walk_check_request_seen>( + seen_pts: &Option>>>, + space: SpaceId, + depth: usize, + rq: MapRequest, +) -> WalkNextLevelRequest { + let Some(seen_pts) = seen_pts else { + return WalkNextLevelRequest::WalkNextLevel(rq); + }; + match seen_pts.borrow_mut().entry(Op::to_phys(rq.table_base)) { + Entry::Vacant(ve) => { + ve.insert(SpaceReferenceMapping { + depth, + space, + our_va: 0, + their_va: rq.vmin, + }); + WalkNextLevelRequest::WalkNextLevel(rq) + } + Entry::Occupied(oe) => { + let mut sm = *oe.get(); + if sm.depth != depth { + // Sharing a page table at different levels like this + // is not supported in the Hyperlight memory + // model. Ignore the "sharing". + WalkNextLevelRequest::WalkNextLevel(rq) + } else { + sm.our_va = rq.vmin; + WalkNextLevelRequest::AlreadySeen(sm) + } + } + } +} +fn walk_next_level_table>( + op: &Op, + x: WalkNextLevelResponse, + next_depth: usize, + space: SpaceId, + seen_pts: &Option>>>, +) -> Option> +where + P::ChildType: UpdateParent, +{ + let rq = match x { + WalkNextLevelResponse::WalkNextLevel(rq) => rq, + WalkNextLevelResponse::AlreadySeen(sm) => { + return Some(WalkNextLevelRequest::AlreadySeen(sm)); + } + }; + let next_base = unsafe { require_table_exist(op, rq)? }; + Some(walk_check_request_seen( + seen_pts, space, next_depth, next_base, + )) +} + +fn do_walk_next_level< + const HIGH_BIT: u8, + const LOW_BIT: u8, + Op: TableReadOps, + P: UpdateParent, +>( + x: WalkNextLevelRequest, +) -> impl Iterator> { + let (iter_a, iter_b) = match x { + WalkNextLevelRequest::WalkNextLevel(rq) => ( + Some( + modify_ptes::(rq) + .map(|r| WalkNextLevelResponse::WalkNextLevel(r)), + ), + None, + ), + WalkNextLevelRequest::AlreadySeen(sm) => ( + None, + Some(core::iter::once(WalkNextLevelResponse::AlreadySeen(sm))), + ), + }; + iter_a + .into_iter() + .flatten() + .chain(iter_b.into_iter().flatten()) +} + /// # Safety /// See `TableOps` documentation. #[allow(clippy::missing_safety_doc)] -pub unsafe fn map(_op: &Op, _mapping: Mapping) { - unimplemented!("map") +pub unsafe fn map(op: &Op, mapping: Mapping) { + modify_ptes::<47, 39, Op, _>(MapRequest { + table_base: op.root_table(), + vmin: mapping.virt_base, + len: mapping.len, + update_parent: Op::TableMovability::root_update_parent(), + }) + .map(|r| unsafe { alloc_table_if_needed(op, r) }) + .flat_map(modify_ptes::<38, 30, Op, _>) + .map(|r| unsafe { alloc_table_if_needed(op, r) }) + .flat_map(modify_ptes::<29, 21, Op, _>) + .map(|r| unsafe { alloc_table_if_needed(op, r) }) + .flat_map(modify_ptes::<20, 12, Op, _>) + .map(|r| unsafe { map_page(op, &mapping, r) }) + .for_each(drop); } /// # 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, + op: impl core::convert::AsRef + Copy + 'a, + address: u64, + len: u64, ) -> impl Iterator + 'a { - unimplemented!("virt_to_phys"); - #[allow(unreachable_code)] - core::iter::empty() + let roots = core::iter::once(op.as_ref().root_table()); + unsafe { + internal_walk_va_spaces(op, roots, false, address, len) + .flat_map(|(_, mappings)| mappings) + .filter_map(|saw| match saw { + SpaceAwareMapping::ThisSpace(m) => Some(m), + // this is guaranteed to never actually happen, both since + // we only passed one root and since we passed do_dedup = + // false + SpaceAwareMapping::AnotherSpace(_) => None, + }) + } +} + +#[allow(clippy::missing_safety_doc)] +unsafe fn internal_walk_va_spaces<'a, Op: TableReadOps + 'a>( + op: impl core::convert::AsRef + Copy + 'a, + roots: impl Iterator + 'a, + // todo - type magic could unify virt_to_phys and walk_va_spaces + do_dedup: bool, + address: u64, + len: u64, +) -> impl Iterator< + Item = ( + SpaceId, + impl Iterator, + ), +> + 'a { + let addr = address & ((1u64 << VA_BITS) - 1); + let vmin = addr & !((PAGE_SIZE as u64) - 1); + let vmax = core::cmp::min(addr + len, 1u64 << VA_BITS); + let seen_pts: Option>>> = if do_dedup { + Some(Rc::new(RefCell::new(BTreeMap::new()))) + } else { + None + }; + roots.into_iter().map(move |root| { + let root_id = Op::to_phys(root); + let root_req = walk_check_request_seen( + &seen_pts, + root_id, + 0, + MapRequest { + table_base: root, + vmin, + len: vmax.saturating_sub(vmin), + update_parent: UpdateParentNone {}, + }, + ); + let seen_pts_1 = seen_pts.clone(); + let seen_pts_2 = seen_pts.clone(); + let seen_pts_3 = seen_pts.clone(); + let iter = do_walk_next_level::<47, 39, Op, _>(root_req) + .filter_map(move |r| walk_next_level_table(op.as_ref(), r, 1, root_id, &seen_pts_1)) + .flat_map(do_walk_next_level::<38, 30, Op, _>) + .filter_map(move |r| walk_next_level_table(op.as_ref(), r, 2, root_id, &seen_pts_2)) + .flat_map(do_walk_next_level::<29, 21, Op, _>) + .filter_map(move |r| walk_next_level_table(op.as_ref(), r, 3, root_id, &seen_pts_3)) + .flat_map(do_walk_next_level::<20, 12, Op, _>) + .filter_map(move |r| { + let rq = match r { + WalkNextLevelResponse::AlreadySeen(sm) => { + return Some(SpaceAwareMapping::AnotherSpace(sm)); + } + WalkNextLevelResponse::WalkNextLevel(rq) => rq, + }; + let desc = unsafe { op.as_ref().read_entry(rq.entry_ptr) }; + if let Some(FinalLevelDescriptorKind::Page) = final_level_descriptor_kind(desc) { + let phys_addr = bits::<47, 12>(desc) << 12; + // Don't sign-extend to canonicalise, because we + // only uses addresses in the lower half right + // now---VA_BITS does not include the bit that + // selects between the ttbr0 and ttbr1 spaces. + let virt_addr = rq.vmin; + // The division of flags in the mapping structure + // does not perfectly capture the fact that + // user-level data and instruction access + // permissions can be different. For now, we just + // assume that the mapping should be marked as + // executable if it was executable to the kernel + // at all. + let executable = bits::<53, 53>(desc) == 0; + let _user_accessible = bits::<6, 6>(desc) != 0; // AP[1] + let kind = if bits::<58, 55>(desc) == SOFTWARE_USE_COW as u64 { + MappingKind::Cow(CowMapping { + readable: true, + executable, + }) + } else { + MappingKind::Basic(BasicMapping { + readable: true, + writable: bits::<7, 7>(desc) == 0, // AP[2] + executable, + }) + }; + Some(SpaceAwareMapping::ThisSpace(Mapping { + phys_base: phys_addr, + virt_base: virt_addr, + len: PAGE_SIZE as u64, + kind, + })) + } else { + None // do nothing - there is no mapping to record here + } + }); + (root_id, iter) + }) } -/// Stub — see [`crate::vmem::walk_va_spaces`]. #[allow(clippy::missing_safety_doc)] pub unsafe fn walk_va_spaces( - _op: &Op, - _roots: &[Op::TableAddr], - _address: u64, - _len: u64, -) -> ::alloc::vec::Vec<( - crate::vmem::SpaceId, - ::alloc::vec::Vec, -)> { - ::alloc::vec::Vec::new() + op: impl core::convert::AsRef + Copy, + roots: &[Op::TableAddr], + address: u64, + len: u64, +) -> Vec<(SpaceId, Vec)> { + unsafe { + internal_walk_va_spaces(&op, roots.iter().cloned(), true, address, len) + .map(|(id, mappings)| (id, mappings.collect::>())) + .collect::>() + } } /// Stub — see [`crate::vmem::space_aware_map`]. @@ -67,11 +538,7 @@ pub unsafe fn space_aware_map( _ref_map: crate::vmem::SpaceReferenceMapping, _built_roots: &::alloc::collections::BTreeMap, ) { + // in practice, we never construct page tables that would result + // in reaching this right now. todo: implement this properly + debug_assert!(false, "space_aware_map is not yet supported on aarch64"); } - -pub trait TableMovability {} -impl> TableMovability - for crate::vmem::MayMoveTable -{ -} -impl TableMovability for crate::vmem::MayNotMoveTable {} diff --git a/src/hyperlight_common/src/vmem.rs b/src/hyperlight_common/src/vmem.rs index f90fe0c64..272a85680 100644 --- a/src/hyperlight_common/src/vmem.rs +++ b/src/hyperlight_common/src/vmem.rs @@ -25,6 +25,13 @@ pub use arch::{PAGE_PRESENT, PAGE_TABLE_SIZE, PTE_ADDR_MASK, PageTableEntry, Phy pub const PAGE_TABLE_ENTRIES_PER_TABLE: usize = PAGE_TABLE_SIZE / core::mem::size_of::(); +// It would be nice not to have any arch-dependent re-exports here, +// but on arm64 the MAIR indices used need to be synced between the +// descriptor creation code and the register initialisation code to +// make sure that MAIR is set up properly. +#[cfg(target_arch = "aarch64")] +pub use arch::ATTR_INDEX_NORMAL; + // Shared page table iterator infrastructure used by each arch module. /// Utility function to extract an (inclusive on both ends) bit range @@ -325,6 +332,7 @@ mod sealed { use sealed::*; /// A sealed trait used to collect some information about the marker structures [`MayMoveTable`] and [`MayNotMoveTable`] +#[allow(private_bounds)] // this trait is intentionally sealed pub trait TableMovability: TableMovabilityBase + arch::TableMovability>::TableMoveInfo> diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/paging.rs b/src/hyperlight_guest_bin/src/arch/aarch64/paging.rs new file mode 100644 index 000000000..87a8a339c --- /dev/null +++ b/src/hyperlight_guest_bin/src/arch/aarch64/paging.rs @@ -0,0 +1,177 @@ +/* +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 hyperlight_common::vmem; +use hyperlight_guest::prim_alloc::alloc_phys_pages; + +use crate::arch::{mrs, msr}; +// TODO: This is not at all thread-safe atm + +#[derive(Copy, Clone)] +struct GuestMappingOperations { + scratch_base_gpa: u64, + scratch_base_gva: u64, +} +impl GuestMappingOperations { + fn new() -> Self { + Self { + scratch_base_gpa: hyperlight_guest::layout::scratch_base_gpa(), + scratch_base_gva: hyperlight_guest::layout::scratch_base_gva(), + } + } + fn try_phys_to_virt(&self, addr: u64) -> Option<*mut u8> { + if addr >= self.scratch_base_gpa { + Some((self.scratch_base_gva + (addr - self.scratch_base_gpa)) as *mut u8) + } else { + None + } + } + fn phys_to_virt(&self, addr: u64) -> *mut u8 { + self.try_phys_to_virt(addr) + .expect("phys_to_virt encountered snapshot non-PT page") + } +} +// for virt_to_phys +impl core::convert::AsRef for GuestMappingOperations { + fn as_ref(&self) -> &Self { + self + } +} +impl vmem::TableReadOps for GuestMappingOperations { + type TableAddr = u64; + fn entry_addr(addr: u64, offset: u64) -> u64 { + addr + offset + } + unsafe fn read_entry(&self, addr: u64) -> u64 { + let addr = self.phys_to_virt(addr); + unsafe { (addr as *mut u64).read_volatile() } + } + fn to_phys(addr: u64) -> u64 { + addr + } + fn from_phys(addr: u64) -> u64 { + addr + } + fn root_table(&self) -> u64 { + unsafe { mrs!(TTBR0_EL1) & !0xfff } + } +} + +impl vmem::TableOps for GuestMappingOperations { + // Currently, we don't actually move tables anywhere on aarch64 + // because amd64 doesn't, and a few assumptions about that have + // been made. Hopefully, we can re-enable this soon. + type TableMovability = vmem::MayMoveTable; + unsafe fn alloc_table(&self) -> u64 { + let page_addr = unsafe { alloc_phys_pages(1) }; + unsafe { + self.phys_to_virt(page_addr) + .write_bytes(0u8, vmem::PAGE_TABLE_SIZE); + // Make sure that the zero'ing writes are ordered with the + // subsequent write that will actually link this table + // into the hierarchy, so that the table walker can never + // read+cache a stale valid entry. See e.g. litmus test + // ROT.inv+dmbst in [1] + // + // [1] Ben Simner, Alasdair Armstrong, Jean + // Pichon-Pharabod, Christopher Pulte, Richard + // Grisenthwaite, and Peter Sewell. 2022. Relaxed + // virtual memory [extended version]. In: Proceedings + // of the 31st European Symposium on Systems + // Programming, ESOP 2022. + core::arch::asm!("dmb st"); + } + page_addr + } + unsafe fn write_entry(&self, addr: u64, entry: u64) -> Option { + unsafe { + (self.phys_to_virt(addr) as *mut u64).write_volatile(entry); + } + None + } + unsafe fn update_root(&self, new_root: u64) { + unsafe { + msr!(TTBR0_EL1, new_root); + } + } +} + +/// Assumption: all are page-aligned +/// # Safety +/// This function modifies pages backing a virtual memory range which is inherently unsafe w.r.t. +/// the Rust memory model. +/// When using this function note: +/// - No locking is performed before touching page table data structures, +/// as such do not use concurrently with any other page table operations +/// - TLB invalidation is not performed, +/// if previously-unmapped ranges are not being mapped, TLB invalidation may need to be performed afterwards. +pub unsafe fn map_region(phys_base: u64, virt_base: *mut u8, len: u64, kind: vmem::MappingKind) { + unsafe { + vmem::map( + &GuestMappingOperations::new(), + vmem::Mapping { + phys_base, + virt_base: virt_base as u64, + len, + kind, + }, + ); + } +} + +pub fn virt_to_phys(gva: vmem::VirtAddr) -> impl Iterator { + unsafe { vmem::virt_to_phys::<_>(GuestMappingOperations::new(), gva, 1) } +} + +pub fn phys_to_virt(gpa: vmem::PhysAddr) -> Option<*mut u8> { + GuestMappingOperations::new().try_phys_to_virt(gpa) +} + +pub mod barrier { + /// # Architecture-specific (aarch64) notes + /// + /// I_WZCBG from [1]: + /// > When a translation table entry that generates a Translation + /// > fault, Address size fault, or Access flag fault is changed to + /// > one that does not fault, all of the following apply to + /// > software: + /// > - TLB invalidation is not required because an entry that + /// > generates one of the listed faults is never cached in a TLB. + /// > - A Context synchronization event is required to ensure that + /// > the completed change to the translation table entry affects + /// > subsequent instruction fetches. + /// + /// In theory, without FEAT_nTLBPA, there could be some subtlety + /// here if the physical memory location used for the descriptor + /// was previously used after the last TLBI to store a valid + /// descriptor. Hyperlight does not recycle page tables in a way + /// that would cause problems here. + /// + /// [1] Arm Architecture Reference Manual for A-profile architecture + /// Chapter D8: The AArch64 Virtual Memory System Architecture + /// §D8.17 TLB maintenance + #[inline(always)] + pub fn first_valid_same_ctx() { + unsafe { + core::arch::asm!( + " + dsb ish + isb + " + ); + } + } +} From d4a8523d91ef1e7d7168cfb36d737930f8de18bd Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 15:57:11 +0100 Subject: [PATCH 15/29] [hyperlight_host] aarch64: add basic register handling for KVM This does not yet support the debug registers, and the architecture-independent interface will have to be rationalised in the future, since the xsave operations are unlikely to be useful on aarch64. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/hypervisor/regs/aarch64/common_fpu.rs | 22 ++ .../hypervisor/regs/aarch64/common_regs.rs | 23 +++ .../src/hypervisor/regs/aarch64/kvm_reg.rs | 193 ++++++++++++++++++ .../src/hypervisor/regs/aarch64/mod.rs | 21 +- .../hypervisor/regs/aarch64/special_regs.rs | 69 +++++++ .../hypervisor/virtual_machine/kvm/aarch64.rs | 109 ++++++++++ 6 files changed, 425 insertions(+), 12 deletions(-) create mode 100644 src/hyperlight_host/src/hypervisor/regs/aarch64/common_fpu.rs create mode 100644 src/hyperlight_host/src/hypervisor/regs/aarch64/common_regs.rs create mode 100644 src/hyperlight_host/src/hypervisor/regs/aarch64/kvm_reg.rs create mode 100644 src/hyperlight_host/src/hypervisor/regs/aarch64/special_regs.rs diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/common_fpu.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/common_fpu.rs new file mode 100644 index 000000000..b78e3d365 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/common_fpu.rs @@ -0,0 +1,22 @@ +/* +Copyright 2026 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. +*/ + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonFpu { + pub(crate) v: [u128; 32], + pub(crate) fpsr: u32, + pub(crate) fpcr: u32, +} diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/common_regs.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/common_regs.rs new file mode 100644 index 000000000..53941f9ba --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/common_regs.rs @@ -0,0 +1,23 @@ +/* +Copyright 2026 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. +*/ + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonRegisters { + pub(crate) x: [u64; 31], + pub(crate) sp: u64, + pub(crate) pc: u64, + pub(crate) pstate: u64, +} diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/kvm_reg.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/kvm_reg.rs new file mode 100644 index 000000000..774c05b99 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/kvm_reg.rs @@ -0,0 +1,193 @@ +/* +Copyright 2026 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 core::marker::PhantomData; +use core::mem::{self, MaybeUninit}; + +use kvm_bindings::{ + KVM_REG_ARM64, KVM_REG_ARM64_SYSREG, KVM_REG_ARM64_SYSREG_CRM_MASK, + KVM_REG_ARM64_SYSREG_CRM_SHIFT, KVM_REG_ARM64_SYSREG_CRN_MASK, KVM_REG_ARM64_SYSREG_CRN_SHIFT, + KVM_REG_ARM64_SYSREG_OP0_MASK, KVM_REG_ARM64_SYSREG_OP0_SHIFT, KVM_REG_ARM64_SYSREG_OP1_MASK, + KVM_REG_ARM64_SYSREG_OP1_SHIFT, KVM_REG_ARM64_SYSREG_OP2_MASK, KVM_REG_ARM64_SYSREG_OP2_SHIFT, + KVM_REG_SIZE_U32, KVM_REG_SIZE_U64, KVM_REG_SIZE_U128, +}; +use kvm_ioctls::VcpuFd; + +pub(crate) trait KvmRegisterType: Sized { + const KVM_REG_SIZE: u64; + fn get_reg(fd: &VcpuFd, reg: KvmRegister) -> Result; + fn set_reg(fd: &VcpuFd, reg: KvmRegister, val: Self) -> Result<(), kvm_ioctls::Error>; +} +macro_rules! kvm_register_type { + ($t:ident, $k:ident) => { + impl KvmRegisterType for $t { + const KVM_REG_SIZE: u64 = $k; + fn get_reg(fd: &VcpuFd, reg: KvmRegister) -> Result { + get_reg_bytes::<{ core::mem::size_of::() }>(fd, reg.kvm_encoding()) + .map(Self::from_ne_bytes) + } + fn set_reg( + fd: &VcpuFd, + reg: KvmRegister, + val: Self, + ) -> Result<(), kvm_ioctls::Error> { + set_reg_bytes::<{ core::mem::size_of::() }>( + fd, + reg.kvm_encoding(), + val.to_ne_bytes(), + ) + } + } + }; +} +kvm_register_type!(u32, KVM_REG_SIZE_U32); +kvm_register_type!(u64, KVM_REG_SIZE_U64); +kvm_register_type!(u128, KVM_REG_SIZE_U128); + +#[derive(Clone, Copy)] +enum KvmRegister_ { + Sysreg { + op0: u8, + op1: u8, + crn: u8, + crm: u8, + op2: u8, + }, + Core { + offset: u16, + }, +} +#[derive(Clone, Copy)] +pub(crate) struct KvmRegister(KvmRegister_, PhantomData); +impl KvmRegister { + const fn kvm_encoding(&self) -> u64 { + KVM_REG_ARM64 + | match self.0 { + KvmRegister_::Sysreg { + op0, + op1, + crn, + crm, + op2, + } => { + (KVM_REG_ARM64_SYSREG as u64) + | (((op0 as u64) << KVM_REG_ARM64_SYSREG_OP0_SHIFT) + & KVM_REG_ARM64_SYSREG_OP0_MASK as u64) + | (((op1 as u64) << KVM_REG_ARM64_SYSREG_OP1_SHIFT) + & KVM_REG_ARM64_SYSREG_OP1_MASK as u64) + | (((crn as u64) << KVM_REG_ARM64_SYSREG_CRN_SHIFT) + & KVM_REG_ARM64_SYSREG_CRN_MASK as u64) + | (((crm as u64) << KVM_REG_ARM64_SYSREG_CRM_SHIFT) + & KVM_REG_ARM64_SYSREG_CRM_MASK as u64) + | (((op2 as u64) << KVM_REG_ARM64_SYSREG_OP2_SHIFT) + & KVM_REG_ARM64_SYSREG_OP2_MASK as u64) + } + KvmRegister_::Core { offset } => 0x10_0000u64 | offset as u64, + } + | T::KVM_REG_SIZE + } + pub(crate) fn get, E2>( + self, + err: impl Fn(E1) -> E2, + fd: &VcpuFd, + ) -> Result { + T::get_reg(fd, self).map_err(|e| err(e.into())) + } + pub(crate) fn set, E2>( + self, + err: impl Fn(E1) -> E2, + fd: &VcpuFd, + val: T, + ) -> Result<(), E2> { + T::set_reg(fd, self, val).map_err(|e| err(e.into())) + } +} + +macro_rules! decl_sys_reg { + ($name:ident, $op0:expr, $op1:expr, $crn:expr, $crm:expr, $op2:expr, $size:ident) => { + pub const $name: KvmRegister<$size> = KvmRegister( + KvmRegister_::Sysreg { + op0: $op0, + op1: $op1, + crn: $crn, + crm: $crm, + op2: $op2, + }, + PhantomData, + ); + }; +} +decl_sys_reg!(TTBR0_EL1, 0b11, 0b000, 0b0010, 0b0000, 0b000, u64); +decl_sys_reg!(TCR_EL1, 0b11, 0b000, 0b0010, 0b0000, 0b010, u64); +decl_sys_reg!(MAIR_EL1, 0b11, 0b000, 0b1010, 0b0010, 0b000, u64); +decl_sys_reg!(SCTLR_EL1, 0b11, 0b000, 0b0001, 0b0000, 0b000, u64); +decl_sys_reg!(CPACR_EL1, 0b11, 0b000, 0b0001, 0b0000, 0b010, u64); +decl_sys_reg!(VBAR_EL1, 0b11, 0b000, 0b1100, 0b0000, 0b000, u64); + +pub const X: [KvmRegister; 31] = { + let mut r: [MaybeUninit>; 31] = [MaybeUninit::uninit(); 31]; + let mut i: u16 = 0; + while i < 31 { + r[i as usize].write(KvmRegister( + KvmRegister_::Core { offset: i * 2 }, + PhantomData, + )); + i += 1; + } + unsafe { mem::transmute::<_, [KvmRegister; 31]>(r) } +}; +macro_rules! decl_core_reg { + ($name:ident, $offset:expr, $size:ident) => { + pub const $name: KvmRegister<$size> = + KvmRegister(KvmRegister_::Core { offset: $offset }, PhantomData); + }; +} +decl_core_reg!(SP, 0x3E, u64); +decl_core_reg!(PC, 0x40, u64); +decl_core_reg!(PSTATE, 0x42, u64); +decl_core_reg!(SP_EL1, 0x44, u64); +// ignore the other SPSRs that are just for AA32-compat +pub const V: [KvmRegister; 32] = { + let mut r: [MaybeUninit>; 32] = [MaybeUninit::uninit(); 32]; + let mut i: u16 = 0; + while i < 32 { + r[i as usize].write(KvmRegister( + KvmRegister_::Core { + offset: 0x54 + i * 4, + }, + PhantomData, + )); + i += 1; + } + unsafe { mem::transmute::<_, [KvmRegister; 32]>(r) } +}; +decl_core_reg!(FPSR, 0xd4, u32); +decl_core_reg!(FPCR, 0xd5, u32); + +fn get_reg_bytes(fd: &VcpuFd, id: u64) -> Result<[u8; N], kvm_ioctls::Error> { + let mut buf: [u8; N] = [0; N]; + fd.get_one_reg(id, &mut buf)?; + Ok(buf) +} + +pub(crate) fn set_reg_bytes( + fd: &VcpuFd, + id: u64, + bytes: [u8; N], +) -> Result<(), kvm_ioctls::Error> { + fd.set_one_reg(id, &bytes)?; + Ok(()) +} diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs index 8f91c634d..2f7331dbf 100644 --- a/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/mod.rs @@ -16,22 +16,19 @@ limitations under the License. // TODO(aarch64): implement real register definitions -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub(crate) struct CommonRegisters { - _placeholder: u64, -} +mod common_regs; +pub(crate) use common_regs::*; -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub(crate) struct CommonSpecialRegisters { - _placeholder: u64, -} +mod special_regs; +pub(crate) use special_regs::*; -#[derive(Debug, Default, Copy, Clone, PartialEq)] -pub(crate) struct CommonFpu { - _placeholder: u64, -} +mod common_fpu; +pub(crate) use common_fpu::*; #[derive(Debug, Default, Copy, Clone, PartialEq)] pub(crate) struct CommonDebugRegs { _placeholder: u64, } + +#[cfg(kvm)] +pub(crate) mod kvm_reg; diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/special_regs.rs new file mode 100644 index 000000000..b5eb44b53 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/special_regs.rs @@ -0,0 +1,69 @@ +/* +Copyright 2026 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. +*/ + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub(crate) struct CommonSpecialRegisters { + pub(crate) ttbr0_el1: u64, + // todo: handle ttbr1 as well + pub(crate) tcr_el1: u64, + pub(crate) mair_el1: u64, + pub(crate) sctlr_el1: u64, + pub(crate) cpacr_el1: u64, + pub(crate) vbar_el1: u64, + pub(crate) sp_el1: u64, +} + +pub(crate) const TCR_EL1_PS_48: u64 = 0b101u64 << 32; +pub(crate) const TCR_EL1_TG0_4K: u64 = 0b00u64 << 14; +pub(crate) const TCR_EL1_TG1_4K: u64 = 0b10u64 << 30; +#[allow(clippy::identity_op)] +pub(crate) const TCR_EL1_T0SZ_48: u64 = 16u64 << 0; +pub(crate) const TCR_EL1_T1SZ_48: u64 = 16u64 << 16; +pub(crate) const TCR_EL1_IRGN0_WB_AA: u64 = 0b01 << 8; +pub(crate) const TCR_EL1_ORGN0_WB_AA: u64 = 0b01 << 10; +pub(crate) const TCR_EL1_SH0_ISH: u64 = 0b11 << 12; + +pub(crate) const MAIR_NORMAL_OWB_NT_AA: u64 = 0b1111_1111; +pub(crate) const MAIR_ITEM_WIDTH: u8 = 8; + +pub(crate) const SCTLR_EL1_RES1: u64 = 0b11u64 << 28 | 0b11u64 << 22 | 0b1u64 << 20 | 0b1u64 << 11; +pub(crate) const SCTLR_EL1_M: u64 = 0b1u64 << 0; +pub(crate) const SCTLR_EL1_C: u64 = 0b1u64 << 2; +pub(crate) const SCTLR_EL1_I: u64 = 0b1u64 << 12; + +pub(crate) const CPACR_EL1_FPEN_NO_TRAP: u64 = 0b11 << 20; + +impl CommonSpecialRegisters { + pub(crate) fn defaults(root_pt_addr: u64) -> Self { + CommonSpecialRegisters { + ttbr0_el1: root_pt_addr & !0xfff, + tcr_el1: TCR_EL1_PS_48 + | TCR_EL1_TG0_4K + | TCR_EL1_TG1_4K + | TCR_EL1_T0SZ_48 + | TCR_EL1_T1SZ_48 + | TCR_EL1_IRGN0_WB_AA + | TCR_EL1_ORGN0_WB_AA + | TCR_EL1_SH0_ISH, + mair_el1: MAIR_NORMAL_OWB_NT_AA + << (MAIR_ITEM_WIDTH * hyperlight_common::vmem::ATTR_INDEX_NORMAL), + sctlr_el1: SCTLR_EL1_RES1 | SCTLR_EL1_M | SCTLR_EL1_C | SCTLR_EL1_I, + cpacr_el1: CPACR_EL1_FPEN_NO_TRAP, + vbar_el1: 0, + sp_el1: 0, + } + } +} diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs index 39ecb775d..5a78d8bad 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs @@ -38,3 +38,112 @@ impl KvmVm { unimplemented!("KvmVm::new") } } + +impl VirtualMachine for KvmVm { + fn regs(&self) -> std::result::Result { + use crate::hypervisor::regs::kvm_reg::{X, SP, PC, PSTATE}; + let mut x: [u64; 31] = [0; 31]; + for i in 0..31 { + x[i] = X[i].get(RegisterError::GetSregs, &self.vcpu_fd)?; + } + Ok(CommonRegisters { + x, + sp: SP.get(RegisterError::GetSregs, &self.vcpu_fd)?, + pc: PC.get(RegisterError::GetSregs, &self.vcpu_fd)?, + pstate: PSTATE.get(RegisterError::GetSregs, &self.vcpu_fd)?, + }) + } + + fn set_regs(&self, regs: &CommonRegisters) -> std::result::Result<(), RegisterError> { + use crate::hypervisor::regs::kvm_reg::{X, SP, PC, PSTATE}; + for i in 0..31 { + X[i].set(RegisterError::SetSregs, &self.vcpu_fd, regs.x[i])?; + } + SP.set(RegisterError::SetSregs, &self.vcpu_fd, regs.sp)?; + PC.set(RegisterError::SetSregs, &self.vcpu_fd, regs.pc)?; + PSTATE.set(RegisterError::SetSregs, &self.vcpu_fd, regs.pstate)?; + + Ok(()) + } + + fn fpu(&self) -> Result { + use crate::hypervisor::regs::kvm_reg::{V, FPSR, FPCR}; + let mut v: [u128; 32] = [0; 32]; + for i in 0..32 { + v[i] = V[i].get(RegisterError::GetFpu, &self.vcpu_fd)?; + } + Ok(CommonFpu { + v, + fpsr: FPSR.get(RegisterError::GetFpu, &self.vcpu_fd)?, + fpcr: FPCR.get(RegisterError::GetFpu, &self.vcpu_fd)?, + }) + } + + fn set_fpu(&self, fpu: &CommonFpu) -> Result<(), RegisterError> { + use crate::hypervisor::regs::kvm_reg::{V, FPSR, FPCR}; + for i in 0..32 { + V[i].set(RegisterError::SetFpu, &self.vcpu_fd, fpu.v[0])?; + } + FPSR.set(RegisterError::SetFpu, &self.vcpu_fd, fpu.fpsr)?; + FPCR.set(RegisterError::SetFpu, &self.vcpu_fd, fpu.fpcr)?; + Ok(()) + } + + fn sregs(&self) -> Result { + use crate::hypervisor::regs::kvm_reg::{ + TTBR0_EL1, TCR_EL1, MAIR_EL1, SCTLR_EL1, CPACR_EL1, VBAR_EL1, SP_EL1, + }; + Ok(CommonSpecialRegisters { + ttbr0_el1: TTBR0_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)?, + tcr_el1: TCR_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)?, + mair_el1: MAIR_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)?, + sctlr_el1: SCTLR_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)?, + cpacr_el1: CPACR_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)?, + vbar_el1: VBAR_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)?, + sp_el1: SP_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)?, + }) + } + + fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<(), RegisterError> { + use crate::hypervisor::regs::kvm_reg::{ + TTBR0_EL1, TCR_EL1, MAIR_EL1, SCTLR_EL1, CPACR_EL1, VBAR_EL1, SP_EL1, + }; + TTBR0_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.ttbr0_el1)?; + TCR_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.tcr_el1)?; + MAIR_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.mair_el1)?; + SCTLR_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.sctlr_el1)?; + CPACR_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.cpacr_el1)?; + VBAR_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.vbar_el1)?; + SP_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.sp_el1)?; + + Ok(()) + } + + fn debug_regs( + &self, + ) -> std::result::Result { + todo!() + } + + fn set_debug_regs( + &self, + _drs: &crate::hypervisor::regs::CommonDebugRegs, + ) -> std::result::Result<(), RegisterError> { + todo!() + } + + fn xsave(&self) -> std::result::Result, RegisterError> { + unimplemented!("aarch64 does not support XSAVE operations") + } + + fn reset_xsave(&self) -> std::result::Result<(), RegisterError> { + unimplemented!("aarch64 does not support XSAVE operations") + } + + #[cfg(test)] + fn set_xsave(&self, xsave: &[u32]) -> std::result::Result<(), RegisterError> { + unimplemented!("aarch64 does not support XSAVE operations") + } + + } +} From d8df6a4a461908744115053bb3593bbb27e07c59 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 15:59:58 +0100 Subject: [PATCH 16/29] [hyperlight_guest_bin] aarch64: support MMIO exits Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/hyperlight_guest/src/arch/aarch64/exit.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/hyperlight_guest/src/arch/aarch64/exit.rs b/src/hyperlight_guest/src/arch/aarch64/exit.rs index 0ac27570d..e0d8a5389 100644 --- a/src/hyperlight_guest/src/arch/aarch64/exit.rs +++ b/src/hyperlight_guest/src/arch/aarch64/exit.rs @@ -16,7 +16,16 @@ limitations under the License. // TODO(aarch64): implement VM exit mechanism (e.g. hvc instruction) +const IO_PAGE_GVA: u64 = hyperlight_common::layout::io_page().unwrap().1; + /// Trigger a VM exit sending a 32-bit value to the host on the given port. -pub(crate) unsafe fn out32(_port: u16, _val: u32) { - unimplemented!("aarch64 out32") +pub(crate) unsafe fn out32(port: u16, val: u32) { + if port as usize >= (hyperlight_common::vmem::PAGE_SIZE / core::mem::size_of::()) { + panic!("aarch64 mmio: unsupported hypercall number {}", port); + } + unsafe { + (IO_PAGE_GVA as *mut u64) + .wrapping_add(port as usize) + .write_volatile(val as u64); + } } From a46fc7d5852046bda3ffec0569c50de52c25d66e Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 16:01:22 +0100 Subject: [PATCH 17/29] [hyperlight_guest] aarch64: Implement simple physical page allocator This is very similar to the amd64 one, but implemented with different assembly instructions. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/arch/aarch64/prim_alloc.rs | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/hyperlight_guest/src/arch/aarch64/prim_alloc.rs b/src/hyperlight_guest/src/arch/aarch64/prim_alloc.rs index 4a5b5d137..d49e3f936 100644 --- a/src/hyperlight_guest/src/arch/aarch64/prim_alloc.rs +++ b/src/hyperlight_guest/src/arch/aarch64/prim_alloc.rs @@ -14,12 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -// TODO(aarch64): implement real aarch64 page allocator +use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode; +use hyperlight_common::{layout, vmem}; // There are no notable architecture-specific safety considerations // here, and the general conditions are documented in the // architecture-independent re-export in prim_alloc.rs #[allow(clippy::missing_safety_doc)] -pub unsafe fn alloc_phys_pages(_n: u64) -> u64 { - unimplemented!("aarch64 alloc_phys_pages") +pub unsafe fn alloc_phys_pages(n: u64) -> u64 { + let addr = crate::layout::allocator_gva(); + let nbytes = n * vmem::PAGE_SIZE as u64; + let mut prev_base: u64 = 0; + unsafe { + // todo: actually check for FEAT_LSE presence. + core::arch::asm!(" + ldadd {nbytes}, {prev_base}, [{addr}] + ", + addr = in(reg) addr, + nbytes = in(reg) nbytes, + prev_base = out(reg) prev_base, + ); + } + // Set aside two pages at the top of the scratch region for the + // exception stack, shared state, etc + let max_avail = layout::SCRATCH_TOP_GPA - vmem::PAGE_SIZE * 2; + if prev_base + .checked_add(nbytes) + .is_none_or(|xx| xx >= max_avail as u64) + { + unsafe { + crate::exit::abort_with_code_and_message( + &[ErrorCode::MallocFailed as u8], + c"Out of physical memory".as_ptr(), + ) + } + } + prev_base } From ed25f9a61d5f8c5f0e336f87402e05c12d0c5943 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 16:02:42 +0100 Subject: [PATCH 18/29] [hyperlight_guest_bin] aarch64: add basic exception handlers These just implement the bare minimum for stack lazy-allocation and CoW. There is no extension mechanism for other crates (analogous to the raw exception handler table that is presently exported on amd64) yet; we will shortly need to add architecture-independent interfaces to allow other crates (e.g. hyperlight-wasm) to register specially-behaving memory ranges and intercept other errors. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/arch/aarch64/exception/entry.rs | 173 +++++++++++++ .../src/arch/aarch64/exception/handle.rs | 233 ++++++++++++++++++ .../src/arch/aarch64/exception/mod.rs | 19 ++ .../src/arch/aarch64/exception/types.rs | 46 ++++ 4 files changed, 471 insertions(+) create mode 100644 src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs create mode 100644 src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs create mode 100644 src/hyperlight_guest_bin/src/arch/aarch64/exception/mod.rs create mode 100644 src/hyperlight_guest_bin/src/arch/aarch64/exception/types.rs diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs b/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs new file mode 100644 index 000000000..63848de12 --- /dev/null +++ b/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs @@ -0,0 +1,173 @@ +/* +Copyright 2026 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 core::arch::global_asm; +use core::mem::{offset_of, size_of}; + +use super::types::*; + +const _: () = assert!(2 * size_of::() == 0x10); +const _: () = assert!(2 * size_of::() == 0x20); + +// sp should already have been lowered to make room for the context +// save structure +// +// x30 should have been saved already +global_asm!(" +.global context_save\n +context_save:\n + stp x0, x1, [sp, #{x_off}+0x00]\n + stp x2, x3, [sp, #{x_off}+0x10]\n + stp x4, x5, [sp, #{x_off}+0x20]\n + stp x6, x7, [sp, #{x_off}+0x30]\n + stp x8, x9, [sp, #{x_off}+0x40]\n + stp x10, x11, [sp, #{x_off}+0x50]\n + stp x12, x13, [sp, #{x_off}+0x60]\n + stp x14, x15, [sp, #{x_off}+0x70]\n + stp x16, x17, [sp, #{x_off}+0x80]\n + stp x18, x19, [sp, #{x_off}+0x90]\n + stp x20, x21, [sp, #{x_off}+0xa0]\n + stp x22, x23, [sp, #{x_off}+0xb0]\n + stp x24, x25, [sp, #{x_off}+0xc0]\n + stp x26, x27, [sp, #{x_off}+0xd0]\n + stp x28, x29, [sp, #{x_off}+0xe0]\n + mrs x0, fpcr\n + mrs x1, fpsr\n + stp x0, x1, [sp, #{fpcr_off}]\n + stp q0, q1, [sp, #{q_off}+0x000]\n + stp q2, q3, [sp, #{q_off}+0x020]\n + stp q4, q5, [sp, #{q_off}+0x040]\n + stp q6, q7, [sp, #{q_off}+0x060]\n + stp q8, q9, [sp, #{q_off}+0x080]\n + stp q10, q11, [sp, #{q_off}+0x0a0]\n + stp q12, q13, [sp, #{q_off}+0x0c0]\n + stp q14, q15, [sp, #{q_off}+0x0e0]\n + stp q16, q17, [sp, #{q_off}+0x100]\n + stp q18, q19, [sp, #{q_off}+0x120]\n + stp q20, q21, [sp, #{q_off}+0x140]\n + stp q22, q23, [sp, #{q_off}+0x160]\n + stp q24, q25, [sp, #{q_off}+0x180]\n + stp q26, q27, [sp, #{q_off}+0x1a0]\n + stp q28, q29, [sp, #{q_off}+0x1c0]\n + stp q30, q31, [sp, #{q_off}+0x1e0]\n + ret +", + x_off = const offset_of!(ExceptionContext, x), + fpcr_off = const offset_of!(ExceptionContext, fpcr), + q_off = const offset_of!(ExceptionContext, q), +); + +global_asm!(" +.global context_restore\n +context_restore:\n + ldp x0, x1, [sp, #{fpcr_off}]\n + msr fpcr, x0\n + msr fpsr, x1\n + ldp x0, x1, [sp, #{x_off}+0x00]\n + ldp x2, x3, [sp, #{x_off}+0x10]\n + ldp x4, x5, [sp, #{x_off}+0x20]\n + ldp x6, x7, [sp, #{x_off}+0x30]\n + ldp x8, x9, [sp, #{x_off}+0x40]\n + ldp x10, x11, [sp, #{x_off}+0x50]\n + ldp x12, x13, [sp, #{x_off}+0x60]\n + ldp x14, x15, [sp, #{x_off}+0x70]\n + ldp x16, x17, [sp, #{x_off}+0x80]\n + ldp x18, x19, [sp, #{x_off}+0x90]\n + ldp x20, x21, [sp, #{x_off}+0xa0]\n + ldp x22, x23, [sp, #{x_off}+0xb0]\n + ldp x24, x25, [sp, #{x_off}+0xc0]\n + ldp x26, x27, [sp, #{x_off}+0xd0]\n + ldp x28, x29, [sp, #{x_off}+0xe0]\n + ldr x30, [sp, #{x_off}+0xf0]\n + ldp q0, q1, [sp, #{q_off}+0x000]\n + ldp q2, q3, [sp, #{q_off}+0x020]\n + ldp q4, q5, [sp, #{q_off}+0x040]\n + ldp q6, q7, [sp, #{q_off}+0x060]\n + ldp q8, q9, [sp, #{q_off}+0x080]\n + ldp q10, q11, [sp, #{q_off}+0x0a0]\n + ldp q12, q13, [sp, #{q_off}+0x0c0]\n + ldp q14, q15, [sp, #{q_off}+0x0e0]\n + ldp q16, q17, [sp, #{q_off}+0x100]\n + ldp q18, q19, [sp, #{q_off}+0x120]\n + ldp q20, q21, [sp, #{q_off}+0x140]\n + ldp q22, q23, [sp, #{q_off}+0x160]\n + ldp q24, q25, [sp, #{q_off}+0x180]\n + ldp q26, q27, [sp, #{q_off}+0x1a0]\n + ldp q28, q29, [sp, #{q_off}+0x1c0]\n + ldp q30, q31, [sp, #{q_off}+0x1e0]\n + add sp, sp, #{ctx_size}\n + eret\n +", + ctx_size = const size_of::(), + x_off = const offset_of!(ExceptionContext, x), + fpcr_off = const offset_of!(ExceptionContext, fpcr), + q_off = const offset_of!(ExceptionContext, q), +); + +macro_rules! vbar_entry { + ($et:literal, $ef:literal) => { + concat!( + " + sub sp, sp, #{ctx_size}\n + str x30, [sp, #{x30_off}]\n + bl context_save\n + mov x0, {ExceptionType_", + $et, + "}\n + mov x1, {ExceptionFrom_", + $ef, + "}\n + mov x2, sp\n + bl {handler}\n + b context_restore\n + .balign 0x80\n + " + ) + }; +} + +global_asm!(" +.balign 0x800\n +.global vbar\n +vbar:\n", + vbar_entry!("Synchronous", "CurrentSP0"), + vbar_entry!("IRQ", "CurrentSP0"), + vbar_entry!("FIQ", "CurrentSP0"), + vbar_entry!("SError", "CurrentSP0"), + vbar_entry!("Synchronous", "CurrentSPx"), + vbar_entry!("IRQ", "CurrentSPx"), + vbar_entry!("FIQ", "CurrentSPx"), + vbar_entry!("SError", "CurrentSPx"), + vbar_entry!("Synchronous", "LowerAArch64"), + vbar_entry!("IRQ", "LowerAArch64"), + vbar_entry!("FIQ", "LowerAArch64"), + vbar_entry!("SError", "LowerAArch64"), + vbar_entry!("Synchronous", "LowerAArch32"), + vbar_entry!("IRQ", "LowerAArch32"), + vbar_entry!("FIQ", "LowerAArch32"), + vbar_entry!("SError", "LowerAArch32"), + ctx_size = const size_of::(), + x30_off = const offset_of!(ExceptionContext, x) + 15 * 0x010, + handler = sym super::handle::handle_exception, + ExceptionType_Synchronous = const ExceptionType::Synchronous as u64, + ExceptionType_IRQ = const ExceptionType::Irq as u64, + ExceptionType_FIQ = const ExceptionType::Fiq as u64, + ExceptionType_SError = const ExceptionType::SError as u64, + ExceptionFrom_CurrentSP0 = const ExceptionFrom::CurrentSP0 as u64, + ExceptionFrom_CurrentSPx = const ExceptionFrom::CurrentSPx as u64, + ExceptionFrom_LowerAArch64 = const ExceptionFrom::LowerAArch64 as u64, + ExceptionFrom_LowerAArch32 = const ExceptionFrom::LowerAArch32 as u64, +); diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs b/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs new file mode 100644 index 000000000..28054ca13 --- /dev/null +++ b/src/hyperlight_guest_bin/src/arch/aarch64/exception/handle.rs @@ -0,0 +1,233 @@ +/* +Copyright 2026 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 core::fmt::Write; + +use hyperlight_common::vmem::{ + BasicMapping, CowMapping, MappingKind, PAGE_SIZE, PhysAddr, VirtAddr, +}; +use hyperlight_guest::error::ErrorCode; +use hyperlight_guest::exit::write_abort; +use hyperlight_guest::layout::{MAIN_STACK_LIMIT_GVA, MAIN_STACK_TOP_GVA}; + +use super::super::mrs; +use super::types::*; +use crate::HyperlightAbortWriter; + +/// Utility function to extract an (inclusive on both ends) bit range +/// from a quadword. +#[inline(always)] +fn bits(x: u64) -> u64 { + (x & ((1 << (HIGH_BIT + 1)) - 1)) >> LOW_BIT +} + +const ESR_EC_DATA_ABORT_LOWER_EL: u64 = 0b100100; +const ESR_EC_DATA_ABORT_SAME_EL: u64 = 0b100101; + +// some of the data in these is not used presently, but is logically +// part of the code being decoded & should be accounted for +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +enum DataFault { + TranslationFault(i64), + PermissionFault(i64), + Other(u64), +} +fn decode_data_fault(dfsc: u64) -> DataFault { + if bits::<5, 2>(dfsc) == 0b0011 { + DataFault::PermissionFault(bits::<1, 0>(dfsc) as i64) + } else if bits::<5, 2>(dfsc) == 0b0001 { + DataFault::TranslationFault(bits::<1, 0>(dfsc) as i64) + } else if bits::<5, 2>(dfsc) == 0b1010 { + if bits::<1, 0>(dfsc) >= 2 { + DataFault::TranslationFault(bits::<1, 0>(dfsc) as i64 - 4) + } else { + DataFault::Other(dfsc) + } + } else { + DataFault::Other(dfsc) + } +} + +// some of the data in these is not used presently, but is logically +// part of the code being decoded & should be accounted for +#[allow(dead_code)] +#[derive(Debug, Copy, Clone)] +enum Exception { + /// lower el?, faulting address, status code + DataFault(bool, u64, DataFault), + Other(u64), +} +fn decode_syndrome(esr: u64) -> Exception { + let ec = bits::<31, 26>(esr); + match ec { + ESR_EC_DATA_ABORT_LOWER_EL => Exception::DataFault( + true, + unsafe { mrs!(FAR_EL1) }, + decode_data_fault(bits::<5, 0>(esr)), + ), + ESR_EC_DATA_ABORT_SAME_EL => Exception::DataFault( + false, + unsafe { mrs!(FAR_EL1) }, + decode_data_fault(bits::<5, 0>(esr)), + ), + _ => Exception::Other(esr), + } +} + +fn handle_stack_fault(far: u64) { + // TODO: perhaps we should have a sanity check that the + // stack grows only one page at a time, which should be + // ensured by our stack probing discipline? + unsafe { + let new_page = hyperlight_guest::prim_alloc::alloc_phys_pages(1); + crate::paging::map_region( + new_page, + (far & !((PAGE_SIZE - 1) as u64)) as *mut u8, + PAGE_SIZE as u64, + MappingKind::Basic(BasicMapping { + readable: true, + writable: true, + executable: false, + }), + ); + // We don't use crate::barrier::first_valid_same_ctx, because + // we don't (presently) use FEAT_ExS and consequently don't + // need the `isb`. + core::arch::asm!("dsb sy"); + } +} + +fn handle_cow_fault(_orig_phys: PhysAddr, virt: VirtAddr, perms: CowMapping) { + unsafe { + let new_page = hyperlight_guest::prim_alloc::alloc_phys_pages(1); + let target_virt = virt as *mut u8; + let Some(scratch_mapping_access) = crate::paging::phys_to_virt(new_page) else { + write_abort(&[ErrorCode::GuestError as u8, 0xfeu8]); + write_abort("impossible: phys_to_virt failed on alloc_phys_pages return".as_bytes()); + write_abort(&[0xFF]); + // At this point, write_abort with the 0xFF terminator is + // expected to terminate guest execution, so control + // should never reach beyond this call. + unreachable!(); + }; + core::ptr::copy(target_virt, scratch_mapping_access, PAGE_SIZE); + // todo(multithreading): this will definitely require a + // break-before-make sequence + crate::paging::map_region( + new_page, + target_virt, + PAGE_SIZE as u64, + MappingKind::Basic(BasicMapping { + // Inherit R bit from the original mapping (always 1 at the moment) + readable: perms.readable, + // If we got here, the original marking was marked + // CoW, so the copied mapping should always be + // writable + writable: true, + executable: perms.executable, + }), + ); + // This is updating an entry that was already valid, changing + // its OA, so we need to actually invalidate the TLB for it. + core::arch::asm!(" + dsb ish + tlbi vae1is, {} + dsb ish + isb + ", + in(reg) (virt >> 12), + options(readonly, nostack, preserves_flags) + ); + } +} + +#[unsafe(no_mangle)] +pub extern "Rust" fn _debug_print(x: &str) { + hyperlight_guest::exit::debug_print(x); +} + +fn handle_internal_fault(exn: Exception) -> bool { + match exn { + Exception::DataFault(false, far, DataFault::TranslationFault(_)) => { + if (MAIN_STACK_LIMIT_GVA..MAIN_STACK_TOP_GVA).contains(&far) { + handle_stack_fault(far); + true + } else { + false + } + } + Exception::DataFault(false, far, DataFault::PermissionFault(_)) => { + let mut orig_mappings = crate::paging::virt_to_phys(far); + if let Some(mapping) = orig_mappings.next() + && let None = orig_mappings.next() + && let MappingKind::Cow(cm) = mapping.kind + { + handle_cow_fault(mapping.phys_base, mapping.virt_base, cm); + true + } else { + false + } + } + _ => false, + } +} + +pub(super) extern "C" fn handle_exception( + typ: ExceptionType, + from: ExceptionFrom, + _regs: *mut ExceptionContext, +) { + let esr = unsafe { mrs!(ESR_EL1) }; + + if typ == ExceptionType::Synchronous && from == ExceptionFrom::CurrentSP0 { + let exn = decode_syndrome(esr); + if handle_internal_fault(exn) { + return; + } + } + + // Die with some diagnostic information + let elr = unsafe { mrs!(ELR_EL1) }; + let far = unsafe { mrs!(FAR_EL1) }; + let insn_bytes = unsafe { (elr as *const [u8; 8]).read_volatile() }; + // amd64 provides the exception vector as the first byte of the + // abort sequence after the guest error identifier code, but the + // host doesn't use it for anything except printing an error + // message, so it's not really useful to try to find an analogue + // (e.g. we could use ESR_EL1.EC---but it's only used for + // debugging and we'll include the whole syndrome in the message + // anyway). So, use 0xfe which is invalid as an exception on x86, + // to let the host know not to try to print anything extra. + let mut w = HyperlightAbortWriter; + write_abort(&[ErrorCode::GuestError as u8, 0xfeu8]); + let write_res = write!( + w, + "Exception vector: {:?} {:?}\n\ + Faulting Instruction: {:#x}\n\ + Bytes At Faulting Instruction: {:?}\n\ + Faulting Address: {:#x}\n\ + Exception Syndrome: {:#x}", + from, typ, elr, insn_bytes, far, esr + ); + if write_res.is_err() { + write_abort("exception message format failed".as_bytes()); + } + + write_abort(&[0xFF]); + // At this point, write_abort with the 0xFF terminator is expected to terminate guest execution, + // so control should never reach beyond this call. + unreachable!(); +} diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/exception/mod.rs b/src/hyperlight_guest_bin/src/arch/aarch64/exception/mod.rs new file mode 100644 index 000000000..89db2bb7f --- /dev/null +++ b/src/hyperlight_guest_bin/src/arch/aarch64/exception/mod.rs @@ -0,0 +1,19 @@ +/* +Copyright 2026 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. + */ + +pub(super) mod entry; +pub mod handle; +mod types; diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/exception/types.rs b/src/hyperlight_guest_bin/src/arch/aarch64/exception/types.rs new file mode 100644 index 000000000..a5f2ab28e --- /dev/null +++ b/src/hyperlight_guest_bin/src/arch/aarch64/exception/types.rs @@ -0,0 +1,46 @@ +/* +Copyright 2026 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 core::mem::{offset_of, size_of}; + +#[derive(Debug, PartialEq)] +#[repr(u64)] +pub(super) enum ExceptionType { + Synchronous, + Irq, + Fiq, + SError, +} + +#[derive(Debug, PartialEq)] +#[repr(u64)] +pub(super) enum ExceptionFrom { + CurrentSP0, + CurrentSPx, + LowerAArch64, + LowerAArch32, +} + +#[repr(C)] +pub(super) struct ExceptionContext { + pub(super) x: [u64; 31], + pub(super) fpcr: u64, + pub(super) fpsr: u64, + // No need to store main context SP: it's in SP_EL0 + pub(super) q: [u128; 32], +} +const _: () = assert!(size_of::().is_multiple_of(16)); +const _: () = assert!(offset_of!(ExceptionContext, fpsr) == offset_of!(ExceptionContext, fpcr) + 8); From 959e4137663e31f70b210d89ca889dfe44f6b744 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 16:05:27 +0100 Subject: [PATCH 19/29] [hyperlight_guest_bin] aarch64: add initialisation/dispatch assembly stubs Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/arch/aarch64/mod.rs | 112 ++++++++++++++++-- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/mod.rs b/src/hyperlight_guest_bin/src/arch/aarch64/mod.rs index 4af3f518a..7da984d51 100644 --- a/src/hyperlight_guest_bin/src/arch/aarch64/mod.rs +++ b/src/hyperlight_guest_bin/src/arch/aarch64/mod.rs @@ -16,20 +16,110 @@ limitations under the License. // TODO(aarch64): implement aarch64 guest runtime +const IO_PAGE_GVA: u64 = hyperlight_common::layout::io_page().unwrap().1; +const HLT_ADDR: u64 = IO_PAGE_GVA + + (core::mem::size_of::() as u64 * hyperlight_common::outb::VmAction::Halt as u64); + pub mod dispatch { - /// Dispatch function pointer — set during initialisation and called - /// by the host for each guest function invocation. - #[unsafe(no_mangle)] - pub extern "C" fn dispatch_function() { - unimplemented!("aarch64 dispatch_function") + unsafe extern "C" { + /// See comments in amd64/dispatch.rs for why this + /// architecture-dependent stub exists + /// + /// # ABI + /// + /// If a TLB flush is required, the host should start executing + /// one instruction (4 bytes) after the base address of the + /// dispatch function. + pub(crate) unsafe fn dispatch_function(); + } + core::arch::global_asm!(" + .global dispatch_function + dispatch_function: + .cfi_startproc\n + .cfi_undefined x30\n + b 0f\n + tlbi vmalle1\n + dsb ish\n + isb\n + 0:\n + bl {internal_dispatch_function}\n + ldr x1, ={hlt_addr}\n + str x0, [x1]\n + .cfi_endproc\n + ", + internal_dispatch_function = sym crate::guest_function::call::internal_dispatch_function, + hlt_addr = const super::HLT_ADDR, + ); +} + +mod exception; + +macro_rules! msr { + ($sysreg:ident, $expr:expr) => { + core::arch::asm!(concat!("msr ", core::stringify!($sysreg), ", {}"), in(reg) $expr); + } +} +pub(crate) use msr; +macro_rules! mrs { + ($sysreg:ident) => { + { + let x: u64; + core::arch::asm!(concat!("mrs {}, ", core::stringify!($sysreg)), out(reg) x); + x + } } } +pub(crate) use mrs; -/// The entrypoint for the guest binary — called by the hypervisor. -/// -/// On aarch64 this is a stub that will be implemented when the -/// aarch64 hypervisor backend is ready. +unsafe fn init_vbar() { + unsafe { + core::arch::asm!(" + adrp {tmp}, vbar\n + add {tmp}, {tmp}, :lo12:vbar\n + msr VBAR_EL1, {tmp}\n + ", tmp = out(reg) _); + } +} + +/// Machine-specific initialisation; calls [`crate::generic_init`] +/// once VBAR and the main stack have been set up #[unsafe(no_mangle)] -pub extern "C" fn entrypoint() -> ! { - unimplemented!("aarch64 entrypoint") +pub extern "C" fn entrypoint(peb_address: u64, seed: u64, ops: u64, max_log_level: u64) -> ! { + unsafe { + init_vbar(); + let stack_top = crate::init::init_stack(); + pivot_stack(peb_address, seed, ops, max_log_level, stack_top); + } } + +unsafe extern "C" { + unsafe fn pivot_stack( + peb_address: u64, + seed: u64, + ops: u64, + max_log_level: u64, + stack_top: u64, + ) -> !; +} + +core::arch::global_asm!(" + .global pivot_stack\n + pivot_stack:\n + .cfi_startproc\n + .cfi_undefined x30\n + ldr x5, ={exn_stack}\n + msr SPSel, #1\n + mov sp, x5\n + msr SPSel, #0\n + mov sp, x4\n + bl {generic_init}\n + ldr x1, ={hlt_addr}\n + str x0, [x1]\n + .cfi_endproc\n +", + exn_stack = const (hyperlight_common::layout::SCRATCH_TOP_GVA as u64 + - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + + 1), + generic_init = sym crate::generic_init, + hlt_addr = const HLT_ADDR, +); From 859b8c45871a252e3cab09c47cfe33a326da4306 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 16:32:21 +0100 Subject: [PATCH 20/29] [hyperlight_host] aarch64: initial KVM implementation There are a number of features not yet supported (e.g. debugging, trace collection etc); however, this should implement a minimal coherent subset of Hyperlight functionality. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/hypervisor/hyperlight_vm/aarch64.rs | 186 +++++++++-- .../src/hypervisor/hyperlight_vm/mod.rs | 2 - .../hypervisor/virtual_machine/kvm/aarch64.rs | 297 ++++++++++++++++-- .../src/hypervisor/virtual_machine/mod.rs | 11 +- .../src/sandbox/initialized_multi_use.rs | 2 +- 5 files changed, 436 insertions(+), 62 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 97e0e46a7..4419b60f8 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -17,6 +17,7 @@ limitations under the License. // TODO(aarch64): implement arch-specific HyperlightVm methods use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64}; use super::{ AccessPageTableError, CreateHyperlightVmError, DispatchGuestCallError, HyperlightVm, @@ -24,8 +25,16 @@ use super::{ }; #[cfg(gdb)] use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse}; -use crate::hypervisor::regs::CommonSpecialRegisters; -use crate::hypervisor::virtual_machine::RegisterError; +use crate::hypervisor::hyperlight_vm::get_guest_log_filter; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +#[cfg(kvm)] +use crate::hypervisor::virtual_machine::kvm::KvmVm; +#[cfg(kvm)] +use crate::hypervisor::virtual_machine::{HypervisorType, VmError}; +use crate::hypervisor::virtual_machine::{ + RegisterError, ResetVcpuError, VirtualMachine, get_available_hypervisor, +}; +use crate::hypervisor::{InterruptHandleImpl, LinuxInterruptHandle}; use crate::mem::mgr::{SandboxMemoryManager, SnapshotSharedMemory}; use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; use crate::sandbox::SandboxConfiguration; @@ -39,61 +48,186 @@ use crate::sandbox::uninitialized::SandboxRuntimeConfig; impl HyperlightVm { #[allow(clippy::too_many_arguments)] pub(crate) fn new( - _snapshot_mem: SnapshotSharedMemory, - _scratch_mem: GuestSharedMemory, - _root_pt_addr: u64, - _entrypoint: NextAction, - _rsp_gva: u64, - _page_size: usize, - _config: &SandboxConfiguration, + snapshot_mem: SnapshotSharedMemory, + scratch_mem: GuestSharedMemory, + root_pt_addr: u64, + entrypoint: NextAction, + rsp_gva: u64, + page_size: usize, + config: &SandboxConfiguration, #[cfg(gdb)] _gdb_conn: Option>, #[cfg(crashdump)] _rt_cfg: SandboxRuntimeConfig, #[cfg(feature = "mem_profile")] _trace_info: MemTraceInfo, ) -> std::result::Result { - unimplemented!("new") + // TODO: support gdb on aarch64 + type VmType = Box; + let vm: VmType = match get_available_hypervisor() { + #[cfg(kvm)] + Some(HypervisorType::Kvm) => Box::new(KvmVm::new().map_err(VmError::CreateVm)?), + // TODO: mshv support + #[cfg(mshv3)] + Some(HypervisorType::Mshv) => return Err(CreateHyperlightVmError::NoHypervisorFound), + None => return Err(CreateHyperlightVmError::NoHypervisorFound), + }; + vm.set_sregs(&CommonSpecialRegisters::defaults(root_pt_addr)) + .map_err(VmError::Register)?; + let interrupt_handle: Arc = Arc::new(LinuxInterruptHandle { + state: AtomicU8::new(0), + tid: AtomicU64::new(unsafe { libc::pthread_self() as u64 }), + retry_delay: config.get_interrupt_retry_delay(), + sig_rt_min_offset: config.get_interrupt_vcpu_sigrtmin_offset(), + dropped: AtomicBool::new(false), + }); + + let snapshot_slot = 0u32; + let scratch_slot = 1u32; + let vm_can_reset_vcpu = vm.can_reset_vcpu(); + let mut ret = Self { + vm, + entrypoint, + rsp_gva, + interrupt_handle, + page_size, + + next_slot: scratch_slot + 1, + freed_slots: Vec::new(), + + snapshot_slot, + snapshot_memory: None, + scratch_slot, + scratch_memory: None, + + mmap_regions: Vec::new(), + + vm_can_reset_vcpu, + pending_tlb_flush: false, + }; + ret.update_snapshot_mapping(snapshot_mem)?; + ret.update_scratch_mapping(scratch_mem)?; + Ok(ret) } #[allow(clippy::too_many_arguments)] pub(crate) fn initialise( &mut self, - _peb_addr: crate::mem::ptr::RawPtr, - _seed: u64, - _mem_mgr: &mut SandboxMemoryManager, - _host_funcs: &Arc>, - _guest_max_log_level: Option, - #[cfg(gdb)] _dbg_mem_access_fn: Arc< + peb_addr: crate::mem::ptr::RawPtr, + seed: u64, + 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") + let NextAction::Initialise(initialise) = self.entrypoint else { + return Ok(()); + }; + let mut x: [u64; 31] = [0; 31]; + x[0] = peb_addr.into(); + x[1] = seed; + x[2] = self.page_size as u64; + x[3] = get_guest_log_filter(guest_max_log_level); + let regs = CommonRegisters { + pc: initialise, + sp: self.rsp_gva, + x, + // start up with interrupts disabled in EL1t + pstate: 0b11 << 6 | 0b100, + }; + self.vm.set_regs(®s)?; + + self.run( + mem_mgr, + host_funcs, + #[cfg(gdb)] + dbg_mem_access_fn, + ) + .map_err(InitializeError::Run)?; + + let regs = self.vm.regs()?; + if !regs.sp.is_multiple_of(16) { + return Err(InitializeError::InvalidStackPointer(regs.sp)); + } + self.rsp_gva = regs.sp; + self.entrypoint = NextAction::Call(regs.x[0]); + + Ok(()) } pub(crate) fn dispatch_call_from_host( &mut self, - _mem_mgr: &mut SandboxMemoryManager, - _host_funcs: &Arc>, - #[cfg(gdb)] _dbg_mem_access_fn: Arc< + mem_mgr: &mut SandboxMemoryManager, + host_funcs: &Arc>, + #[cfg(gdb)] dbg_mem_access_fn: Arc< std::sync::Mutex>, >, ) -> Result<(), DispatchGuestCallError> { - unimplemented!("dispatch_call_from_host") + let NextAction::Call(dispatch_func_addr) = self.entrypoint else { + return Err(DispatchGuestCallError::Uninitialized); + }; + let mut regs = CommonRegisters { + pc: dispatch_func_addr, + sp: self.rsp_gva, + // start with interrupts disabled in EL1t + pstate: 0b11 << 6 | 0b100, + ..Default::default() + }; + if self.pending_tlb_flush { + regs.pc += 4; + } + self.vm + .set_regs(®s) + .map_err(DispatchGuestCallError::SetupRegs)?; + self.vm + .set_fpu(&CommonFpu::default()) + .map_err(DispatchGuestCallError::SetupRegs)?; + let result = self + .run( + mem_mgr, + host_funcs, + #[cfg(gdb)] + dbg_mem_access_fn, + ) + .map_err(DispatchGuestCallError::Run); + self.pending_tlb_flush = false; + result } pub(crate) fn get_root_pt(&self) -> Result { - unimplemented!("get_root_pt") + let sregs = self.vm.sregs()?; + Ok(sregs.ttbr0_el1 & ((1 << 48) - 2)) } pub(crate) fn get_snapshot_sregs( &mut self, ) -> Result { - unimplemented!("get_snapshot_sregs") + let x = self.vm.sregs()?; + Ok(x) } pub(crate) fn reset_vcpu( &mut self, - _cr3: u64, - _sregs: &CommonSpecialRegisters, + cr3: u64, + sregs: &CommonSpecialRegisters, + ) -> std::result::Result<(), ResetVcpuError> { + debug_assert!( + self.vm_can_reset_vcpu, + "No fallback path for vcpu reset on aarch64" + ); + self.vm.reset_vcpu()?; + self.apply_sregs(cr3, sregs)?; + Ok(()) + } + + pub(crate) fn apply_sregs( + &mut self, + cr3: u64, + sregs: &CommonSpecialRegisters, ) -> std::result::Result<(), RegisterError> { - unimplemented!("reset_vcpu") + let mut sregs = *sregs; + sregs.ttbr0_el1 = cr3 & ((1 << 48) - 2); + self.pending_tlb_flush = true; + + self.vm.set_sregs(&sregs) } } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs index a574880ef..56f7d635d 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -24,8 +24,6 @@ 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; diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs index 5a78d8bad..07c6db953 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs @@ -16,35 +16,267 @@ limitations under the License. // TODO(aarch64): implement KVM backend +use std::sync::LazyLock; + +use hyperlight_common::outb::VmAction; +use kvm_bindings::{ + KVM_CAP_ARM_NISV_TO_USER, KVM_EXIT_ARM_NISV, KVMIO, kvm_enable_cap, kvm_userspace_memory_region, +}; +use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; use tracing::{Span, instrument}; -use crate::hypervisor::virtual_machine::CreateVmError; +use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +use crate::hypervisor::virtual_machine::{ + CreateVmError, HypervisorError, MapMemoryError, RegisterError, ResetVcpuError, RunVcpuError, + UnmapMemoryError, VirtualMachine, VmExit, +}; + +static KVM: LazyLock> = + LazyLock::new(|| Kvm::new().map_err(|e| CreateVmError::HypervisorNotAvailable(e.into()))); /// 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 + if let Ok(kvm) = KVM.as_ref() { + let api_version = kvm.get_api_version(); + api_version == 12 + } else { + false + } } /// A KVM implementation of a single-vcpu VM #[derive(Debug)] pub(crate) struct KvmVm { - _placeholder: (), + vm_fd: VmFd, + vcpu_fd: VcpuFd, } impl KvmVm { + pub(self) fn vcpu_init(&mut self) -> Result<(), HypervisorError> { + let mut kvi = kvm_bindings::kvm_vcpu_init::default(); + self.vm_fd.get_preferred_target(&mut kvi)?; + self.vcpu_fd.vcpu_init(&kvi)?; + Ok(()) + } pub(crate) fn new() -> std::result::Result { - unimplemented!("KvmVm::new") + let hv = KVM.as_ref().map_err(|e| e.clone())?; + let vm_fd = hv + .create_vm_with_type(0) + .map_err(|e| CreateVmError::CreateVmFd(e.into()))?; + if vm_fd.check_extension_raw(KVM_CAP_ARM_NISV_TO_USER as u64) != 0 { + // Available since Linux 5.5. Needed for the workaround + // described below for KVM misbehaviour when a cache + // maintenance operation is applied to a VA that is paged + // out at Stage 2. + // + // When this cap is not available, there is a (small) + // chance that self-modifying code inside the VM will + // cause [`run_vcpu`] to fail, ultimately poisoning the + // sandbox. With this capability, the relevant code will + // instead be retried. + let cap: kvm_enable_cap = kvm_enable_cap { + cap: KVM_CAP_ARM_NISV_TO_USER, + ..Default::default() + }; + unsafe { + vmm_sys_util::ioctl_iow_nr!(KVM_ENABLE_CAP, KVMIO, 0xa3, kvm_enable_cap); + vmm_sys_util::ioctl::ioctl_with_ref(&vm_fd, KVM_ENABLE_CAP(), &cap); + } + } + + let vcpu_fd = vm_fd + .create_vcpu(0) + .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; + + let mut to_ret = Self { vm_fd, vcpu_fd }; + to_ret + .vcpu_init() + .map_err(CreateVmError::SetPartitionProperty)?; + Ok(to_ret) + } + + fn run_immediate_exit(&mut self) -> Result<(), Result> { + self.vcpu_fd.set_kvm_immediate_exit(1u8); + let ret = loop { + let r = self.vcpu_fd.run(); + if let Err(e) = r { + match e.errno() { + libc::EINTR => break Ok(()), + libc::EAGAIN => continue, + _ => break Err(Ok(e.into())), + } + } else { + break Err(Err(format!( + "KVM run for state quiescence exited without EINTR: {:?}", + r + ))); + } + }; + self.vcpu_fd.set_kvm_immediate_exit(0u8); + ret } } impl VirtualMachine for KvmVm { + unsafe fn map_memory( + &mut self, + (slot, region): (u32, &crate::mem::memory_region::MemoryRegion), + ) -> std::result::Result<(), crate::hypervisor::virtual_machine::MapMemoryError> { + let mut kvm_region: kvm_userspace_memory_region = region.into(); + kvm_region.slot = slot; + unsafe { self.vm_fd.set_user_memory_region(kvm_region) } + .map_err(|e| MapMemoryError::Hypervisor(e.into())) + } + + fn unmap_memory( + &mut self, + (slot, region): (u32, &crate::mem::memory_region::MemoryRegion), + ) -> std::result::Result<(), crate::hypervisor::virtual_machine::UnmapMemoryError> { + let mut kvm_region: kvm_userspace_memory_region = region.into(); + kvm_region.slot = slot; + // Setting memory_size to 0 unmaps the slot's region + // From https://docs.kernel.org/virt/kvm/api.html + // > Deleting a slot is done by passing zero for memory_size. + kvm_region.memory_size = 0; + unsafe { self.vm_fd.set_user_memory_region(kvm_region) } + .map_err(|e| UnmapMemoryError::Hypervisor(e.into())) + } + + fn run_vcpu( + &mut self, + #[cfg(feature = "trace_guest")] tc: &mut SandboxTraceContext, + ) -> std::result::Result< + crate::hypervisor::virtual_machine::VmExit, + crate::hypervisor::virtual_machine::RunVcpuError, + > { + let exit = loop { + let mut exit = self.vcpu_fd.run(); + if let Ok(VcpuExit::Unsupported(KVM_EXIT_ARM_NISV)) = exit { + // [`VcpuExit`] borrows the [`Vcpu`] which produced + // it, but that lifetime isn't used in this case. End + // the borrow early by re-constructing the value while + // preserving the possibility for more tests to be + // inserted after this one. + exit = Ok(VcpuExit::Unsupported(KVM_EXIT_ARM_NISV)); + // If a readonly-at-stage-2 page is paged out at stage + // 2, KVM does not correctly handle the page fault due + // to Stage 2 translation that occurs when cache + // maintenance operations must resolve the page + // address in order to execute. KVM incorrectly treats + // the fault as an indication that the guest is making + // an MMIO access the details of which are not + // captured in NISV. + // + // Guest code tries to reduce the chance of this + // happening by making a data access shortly before + // the cache cleaning instructions. However, this is + // possibly racy, since KVM could page out the + // relevant Stage 2 translation in between the data + // access and the cache maintenance operation. In + // order to account for this case, we detect it and + // cooperate with code inside the VM to re-fault-in + // the page and re-try the cache maintenance operation + // in question. + // + // The calling convention for this is: any cache + // maintenance operation should be executed with the + // Zero flag cleared. If it fails for this reason, + // Hyperlight will increment PC to the next + // instruction as usual, but set the Zero flag. The + // guest should detect this and attempt to fault in + // the page and re-try the operation. + use crate::hypervisor::regs::kvm_reg::{PC, PSTATE}; + let pc = PC.get(RunVcpuError::Unknown, &self.vcpu_fd)?; + let pstate = PSTATE.get(RunVcpuError::Unknown, &self.vcpu_fd)?; + + const Z_BIT: u64 = 1 << 30; + // Because we got here from the NISV mmio exit path, + // we know that ESR_EL2.EC codes for a Data Abort, and + // we can assume the relevant encoding of ESR_EL2.ISS + const ESR_EL2_ISS_CM: u64 = 1 << 8; + + let esr_iss = unsafe { + // SAFETY: KVM_EXIT_ARM_NISV implies this is the arm_nisv variant. + self.vcpu_fd.get_kvm_run().__bindgen_anon_1.arm_nisv.esr_iss + }; + if esr_iss & ESR_EL2_ISS_CM != 0 && pstate & Z_BIT == 0 { + // if ESR_EL2.ISS.CM is set, the abort was caused + // by a Cache Maintenance instruction. Assume that + // any Cache Maintenance instruction in the VM is + // part of a Hyperlight-aware sequence and can + // deal with it. + PSTATE.set(RunVcpuError::Unknown, &self.vcpu_fd, pstate | Z_BIT)?; + PC.set(RunVcpuError::Unknown, &self.vcpu_fd, pc + 4)?; + continue; + } + } + break exit; + }; + match exit { + Ok(VcpuExit::MmioWrite(addr, data)) => { + let io_page_gpa = const { hyperlight_common::layout::io_page().unwrap().0 }; + if addr >= io_page_gpa + && let off = (addr - io_page_gpa) as usize + && off < hyperlight_common::vmem::PAGE_SIZE + { + let port = off / core::mem::size_of::(); + if port == VmAction::Halt as usize { + // As per [1]: + // > For KVM_EXIT_IO [...] the corresponding operations are complete + // > (and guest state is consistent) only after userspace has re-entered + // > the kernel with KVM_RUN. The kernel side will first finish + // > incomplete operations and then check for pending signals. + // > + // > The pending state of the operation is not preserved in state which + // > is visible to userspace, thus userspace should ensure that the + // > operation is completed before performing a live + // > migration. Userspace can re-enter the guest with an unmasked signal + // > pending or with the immediate_exit field set to complete pending + // > operations without allowing any further instructions to be + // > executed. + // + // On AArch64, the incomplete operation state includes incrementing the + // program counter past the faulting I/O instruction. Since a halt exit + // is used to logically end a thread of execution, we will likely start + // executing from somewhere else again after, in which case such a + // program counter increment would be undesirable. Therefore, in the hlt + // case, re-enter the kernel with immediate_exit set right away to clear + // that state. + // + // We assume that this pattern is not required in any other case, + // because any error that prevents the guest code from fully unwinding + // its stack and running "to completion" (i.e. to a halt exit) should + // poison the sandbox, and the vcpu reset on sandbox reset needed to + // un-poison it will take care of clearing the necessary state. + self.run_immediate_exit() + .map_err(|e| RunVcpuError::FlushMmioPending(format!("{:?}", e)))?; + Ok(VmExit::Halt()) + } else { + Ok(VmExit::IoOut(port as u16, data.to_vec())) + } + } else { + Ok(VmExit::MmioWrite(addr)) + } + } + Ok(VcpuExit::MmioRead(addr, _)) => Ok(VmExit::MmioRead(addr)), + Err(e) => match e.errno() { + libc::EINTR => Ok(VmExit::Cancelled()), + libc::EAGAIN => Ok(VmExit::Retry()), + _ => Err(RunVcpuError::Unknown(e.into())), + }, + Ok(other) => Ok(VmExit::Unknown(format!( + "Unknown KVM VCPU exit: {:?}", + other + ))), + } + } + fn regs(&self) -> std::result::Result { - use crate::hypervisor::regs::kvm_reg::{X, SP, PC, PSTATE}; + use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, X}; let mut x: [u64; 31] = [0; 31]; - for i in 0..31 { - x[i] = X[i].get(RegisterError::GetSregs, &self.vcpu_fd)?; + for (i, xi) in X.iter().enumerate() { + x[i] = xi.get(RegisterError::GetSregs, &self.vcpu_fd)?; } Ok(CommonRegisters { x, @@ -55,9 +287,9 @@ impl VirtualMachine for KvmVm { } fn set_regs(&self, regs: &CommonRegisters) -> std::result::Result<(), RegisterError> { - use crate::hypervisor::regs::kvm_reg::{X, SP, PC, PSTATE}; - for i in 0..31 { - X[i].set(RegisterError::SetSregs, &self.vcpu_fd, regs.x[i])?; + use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, X}; + for (i, xi) in X.iter().enumerate() { + xi.set(RegisterError::SetSregs, &self.vcpu_fd, regs.x[i])?; } SP.set(RegisterError::SetSregs, &self.vcpu_fd, regs.sp)?; PC.set(RegisterError::SetSregs, &self.vcpu_fd, regs.pc)?; @@ -67,10 +299,10 @@ impl VirtualMachine for KvmVm { } fn fpu(&self) -> Result { - use crate::hypervisor::regs::kvm_reg::{V, FPSR, FPCR}; + use crate::hypervisor::regs::kvm_reg::{FPCR, FPSR, V}; let mut v: [u128; 32] = [0; 32]; - for i in 0..32 { - v[i] = V[i].get(RegisterError::GetFpu, &self.vcpu_fd)?; + for (i, vi) in V.iter().enumerate() { + v[i] = vi.get(RegisterError::GetFpu, &self.vcpu_fd)?; } Ok(CommonFpu { v, @@ -80,9 +312,9 @@ impl VirtualMachine for KvmVm { } fn set_fpu(&self, fpu: &CommonFpu) -> Result<(), RegisterError> { - use crate::hypervisor::regs::kvm_reg::{V, FPSR, FPCR}; - for i in 0..32 { - V[i].set(RegisterError::SetFpu, &self.vcpu_fd, fpu.v[0])?; + use crate::hypervisor::regs::kvm_reg::{FPCR, FPSR, V}; + for (i, vi) in V.iter().enumerate() { + vi.set(RegisterError::SetFpu, &self.vcpu_fd, fpu.v[i])?; } FPSR.set(RegisterError::SetFpu, &self.vcpu_fd, fpu.fpsr)?; FPCR.set(RegisterError::SetFpu, &self.vcpu_fd, fpu.fpcr)?; @@ -91,7 +323,7 @@ impl VirtualMachine for KvmVm { fn sregs(&self) -> Result { use crate::hypervisor::regs::kvm_reg::{ - TTBR0_EL1, TCR_EL1, MAIR_EL1, SCTLR_EL1, CPACR_EL1, VBAR_EL1, SP_EL1, + CPACR_EL1, MAIR_EL1, SCTLR_EL1, SP_EL1, TCR_EL1, TTBR0_EL1, VBAR_EL1, }; Ok(CommonSpecialRegisters { ttbr0_el1: TTBR0_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)?, @@ -106,7 +338,7 @@ impl VirtualMachine for KvmVm { fn set_sregs(&self, sregs: &CommonSpecialRegisters) -> Result<(), RegisterError> { use crate::hypervisor::regs::kvm_reg::{ - TTBR0_EL1, TCR_EL1, MAIR_EL1, SCTLR_EL1, CPACR_EL1, VBAR_EL1, SP_EL1, + CPACR_EL1, MAIR_EL1, SCTLR_EL1, SP_EL1, TCR_EL1, TTBR0_EL1, VBAR_EL1, }; TTBR0_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.ttbr0_el1)?; TCR_EL1.set(RegisterError::SetSregs, &self.vcpu_fd, sregs.tcr_el1)?; @@ -132,18 +364,21 @@ impl VirtualMachine for KvmVm { todo!() } - fn xsave(&self) -> std::result::Result, RegisterError> { - unimplemented!("aarch64 does not support XSAVE operations") + fn can_reset_vcpu(&self) -> bool { + true } - - fn reset_xsave(&self) -> std::result::Result<(), RegisterError> { - unimplemented!("aarch64 does not support XSAVE operations") - } - - #[cfg(test)] - fn set_xsave(&self, xsave: &[u32]) -> std::result::Result<(), RegisterError> { - unimplemented!("aarch64 does not support XSAVE operations") - } - + fn reset_vcpu(&mut self) -> Result<(), ResetVcpuError> { + self.run_immediate_exit().map_err(|e| { + e.map(ResetVcpuError::Hypervisor) + .map_err(ResetVcpuError::Unknown) + .unwrap_or_else(|e| e) + })?; + self.vcpu_init().map_err(ResetVcpuError::Hypervisor)?; + self.run_immediate_exit().map_err(|e| { + e.map(ResetVcpuError::Hypervisor) + .map_err(ResetVcpuError::Unknown) + .unwrap_or_else(|e| e) + })?; + Ok(()) } } diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index 691397bd4..dac344711 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -107,12 +107,12 @@ pub(crate) enum HypervisorType { /// Minimum XSAVE buffer size: 512 bytes legacy region + 64 bytes header. /// Only used by MSHV and WHP which use compacted XSAVE format and need to /// validate buffer size before accessing XCOMP_BV. -#[cfg(any(mshv3, target_os = "windows"))] +#[cfg(all(target_arch = "x86_64", any(mshv3, target_os = "windows")))] pub(crate) const XSAVE_MIN_SIZE: usize = 576; /// Standard XSAVE buffer size (4KB) used by KVM and MSHV. /// WHP queries the required size dynamically. -#[cfg(all(any(kvm, mshv3), test))] +#[cfg(all(any(kvm, mshv3), test, not(target_arch = "aarch64")))] pub(crate) const XSAVE_BUFFER_SIZE: usize = 4096; // Compiler error if no hypervisor type is available (not applicable on aarch64 yet) @@ -202,6 +202,9 @@ pub enum RunVcpuError { IncrementRip(HypervisorError), #[error("Parse GPA access info failed")] ParseGpaAccessInfo, + #[cfg(target_arch = "aarch64")] + #[error("Flush MMIO pending state failed: {0}")] + FlushMmioPending(String), #[error("Unknown error: {0}")] Unknown(HypervisorError), } @@ -353,15 +356,19 @@ pub(crate) trait VirtualMachine: Debug + Send { #[allow(dead_code)] fn debug_regs(&self) -> std::result::Result; /// Set the debug registers of the vCPU + #[allow(dead_code)] fn set_debug_regs(&self, drs: &CommonDebugRegs) -> std::result::Result<(), RegisterError>; /// Get xsave #[allow(dead_code)] + #[cfg(not(target_arch = "aarch64"))] fn xsave(&self) -> std::result::Result, RegisterError>; /// Reset xsave to default state + #[cfg(not(target_arch = "aarch64"))] fn reset_xsave(&self) -> std::result::Result<(), RegisterError>; /// Set xsave - only used for tests #[cfg(test)] + #[cfg(not(target_arch = "aarch64"))] fn set_xsave(&self, xsave: &[u32]) -> std::result::Result<(), RegisterError>; /// Single-operation vCPU reset diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index e6f8e6108..1e6c1254d 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -301,7 +301,7 @@ impl MultiUseSandbox { vm.apply_sregs(hshm.layout.get_pt_base_gpa(), sregs) .map_err(|e| { crate::HyperlightError::HyperlightVmError( - crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e), + crate::hypervisor::hyperlight_vm::HyperlightVmError::Restore(e.into()), ) })?; } From 417ec2c984eb3eda41e26800feb2636a8830cbea Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 26 May 2026 16:33:28 +0100 Subject: [PATCH 21/29] [tests] aarch64: remove amd64-isms from enabled tests Either by disabling them, or by rewriting them to do a similar thing on aarch64. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/sandbox/initialized_multi_use.rs | 16 +- src/hyperlight_host/tests/integration_test.rs | 24 ++- src/tests/rust_guests/dummyguest/src/main.rs | 12 +- src/tests/rust_guests/simpleguest/src/main.rs | 143 +++++++++++++----- 4 files changed, 150 insertions(+), 45 deletions(-) diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 1e6c1254d..76452cf64 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -1394,12 +1394,12 @@ mod tests { match res.unwrap_err() { HyperlightError::GuestAborted(_, msg) => { // msg should indicate we got an invalid opcode exception + #[cfg(target_arch = "x86_64")] assert!(msg.contains("InvalidOpcode")); + #[cfg(target_arch = "aarch64")] + assert!(msg.contains("0x2000000")); } - e => panic!( - "Expected HyperlightError::GuestExecutionError but got {:?}", - e - ), + e => panic!("Expected HyperlightError::GuestAborted but got {:?}", e), } } @@ -1485,7 +1485,10 @@ mod tests { .evolve() .unwrap(); + #[cfg(target_arch = "x86_64")] let expected = &[0x90, 0x90, 0x90, 0xC3]; // NOOP slide to RET + #[cfg(target_arch = "aarch64")] + let expected = &[0x1f, 0x20, 0x03, 0xd5, 0xc0, 0x03, 0x5f, 0xd6]; let map_mem = page_aligned_memory(expected); let guest_base = 0x1_0000_0000; // Arbitrary guest base address @@ -1882,8 +1885,9 @@ mod tests { let dr0_initial: u64 = sandbox.call("GetDr0", ()).unwrap(); assert_eq!(dr0_initial, 0, "DR0 should initially be 0"); - // Dirty DR0 by setting it to a known non-zero value - const DIRTY_VALUE: u64 = 0xDEAD_BEEF_CAFE_BABE; + // Dirty DR0 by setting it to a known non-zero value, avoiding + // bits that are reserved in aarch64 DBGBVR0_EL1 + const DIRTY_VALUE: u64 = 0xFFFF_FEDC_7654_3210; sandbox.call::<()>("SetDr0", DIRTY_VALUE).unwrap(); let dr0_dirty: u64 = sandbox.call("GetDr0", ()).unwrap(); assert_eq!( diff --git a/src/hyperlight_host/tests/integration_test.rs b/src/hyperlight_host/tests/integration_test.rs index 6b5a7f8e3..3b3c6c108 100644 --- a/src/hyperlight_host/tests/integration_test.rs +++ b/src/hyperlight_host/tests/integration_test.rs @@ -711,7 +711,12 @@ fn execute_on_heap() { #[cfg(not(feature = "executable_heap"))] assert!( - result.unwrap_err().to_string().contains("PageFault"), + result.unwrap_err().to_string().contains( + #[cfg(target_arch = "x86_64")] + "PageFault", + #[cfg(target_arch = "aarch64")] + "Exception Syndrome: 0x86", + ), "should get page fault" ); }); @@ -1644,6 +1649,7 @@ fn interrupt_infinite_moving_loop_stress_test() { } #[test] +#[cfg(target_arch = "x86_64")] fn exception_handler_installation_and_validation() { with_rust_sandbox(|mut sandbox| { // Verify handler count starts at 0 @@ -1692,9 +1698,14 @@ fn fill_heap_and_cause_exception() { // Verify the message was properly formatted (proves no-allocation path worked) // Exception vector 6 is #UD (Invalid Opcode from ud2 instruction) + #[cfg(target_arch = "x86_64")] + let vector = "Exception vector: 6"; + #[cfg(target_arch = "aarch64")] + let vector = "Exception vector: CurrentSP0 Synchronous"; assert!( - message.contains("Exception vector: 6"), - "Message should contain 'Exception vector: 6'\nFull error: {:?}", + message.contains(vector), + "Message should contain '{}'\nFull error: {:?}", + vector, err ); assert!( @@ -1702,11 +1713,18 @@ fn fill_heap_and_cause_exception() { "Message should contain 'Faulting Instruction:'\nFull error: {:?}", err ); + #[cfg(target_arch = "x86_64")] assert!( message.contains("Stack Pointer:"), "Message should contain 'Stack Pointer:'\nFull error: {:?}", err ); + #[cfg(target_arch = "aarch64")] + assert!( + message.contains("Exception Syndrome:"), + "Message should contain 'Exception Syndrome:'\nFull error: {:?}", + err + ); } _ => panic!("Expected GuestAborted error, got: {:?}", err), } diff --git a/src/tests/rust_guests/dummyguest/src/main.rs b/src/tests/rust_guests/dummyguest/src/main.rs index 924bb686c..6706b551d 100644 --- a/src/tests/rust_guests/dummyguest/src/main.rs +++ b/src/tests/rust_guests/dummyguest/src/main.rs @@ -33,6 +33,7 @@ fn halt() { // VmAction::Halt = 108; using raw constant to avoid pulling in // anyhow (via hyperlight_common's TryFrom impl) which requires alloc. unsafe { + #[cfg(target_arch = "x86_64")] asm!( "out dx, eax", "cli", @@ -40,18 +41,27 @@ fn halt() { in("dx") 108u16, in("eax") 0u32, ); + #[cfg(target_arch = "aarch64")] + asm!( + "str {val:x}, [{addr}]", + val = in(reg) 0, addr = in(reg) 0xffff_ffff_e000u64 + 108 * 8, + ); } } fn mmio_read() { unsafe { + #[cfg(target_arch = "x86_64")] asm!("mov al, [0x8000]"); + + #[cfg(target_arch = "aarch64")] + asm!("ldr {0:x}, [{1:x}]", out(reg) _, in(reg) 0x8000); } } #[allow(non_snake_case)] #[no_mangle] -pub extern "win64" fn entrypoint(a: i64, b: i64, c: i32) -> i32 { +pub extern "C" fn entrypoint(a: i64, b: i64, c: i32) -> i32 { if a != 0x230000 || b != 1234567890 || c != 4096 { mmio_read(); } diff --git a/src/tests/rust_guests/simpleguest/src/main.rs b/src/tests/rust_guests/simpleguest/src/main.rs index af05b740c..959c43c72 100644 --- a/src/tests/rust_guests/simpleguest/src/main.rs +++ b/src/tests/rust_guests/simpleguest/src/main.rs @@ -32,7 +32,9 @@ use alloc::{format, vec}; use core::alloc::Layout; use core::ffi::c_char; use core::hint::black_box; -use core::sync::atomic::{AtomicU32, AtomicU64, Ordering}; +#[cfg(target_arch = "x86_64")] +use core::sync::atomic::AtomicU32; +use core::sync::atomic::{AtomicU64, Ordering}; use hyperlight_common::flatbuffer_wrappers::function_call::{FunctionCall, FunctionCallType}; use hyperlight_common::flatbuffer_wrappers::function_types::{ @@ -65,8 +67,11 @@ extern crate hyperlight_guest; static mut BIGARRAY: [i32; 1024 * 1024] = [0; 1024 * 1024]; // Exception handler test state static HANDLER_INVOCATION_COUNT: AtomicU64 = AtomicU64::new(0); +#[cfg(target_arch = "x86_64")] const TEST_R9_VALUE: u64 = 0x1234567890ABCDEF; +#[cfg(target_arch = "x86_64")] const TEST_R9_MODIFIED_VALUE: u64 = 0xBADC0FFEE; +#[cfg(target_arch = "x86_64")] const TEST_R10_VALUE: u64 = 0xDEADBEEF; #[guest_function("SetStatic")] @@ -345,7 +350,6 @@ fn call_malloc(size: i32) -> i32 { } #[guest_function("FillHeapAndCauseException")] -#[cfg(target_arch = "x86_64")] fn fill_heap_and_cause_exception() { let layout: Layout = Layout::new::(); let mut ptr = unsafe { alloc::alloc::alloc_zeroed(layout) }; @@ -355,7 +359,7 @@ fn fill_heap_and_cause_exception() { } // trigger an undefined instruction exception - unsafe { core::arch::asm!("ud2") }; + trigger_exception(); } #[guest_function("ExhaustHeap")] @@ -456,7 +460,13 @@ fn test_guest_panic(message: String) { fn execute_on_heap() -> String { unsafe { // NO-OP followed by RET - let heap_memory = Box::new([0x90u8, 0xC3]); + let mut heap_memory = Box::new( + #[cfg(target_arch = "x86_64")] + [0x90u8, 0xC3], + #[cfg(target_arch = "aarch64")] + [0x1f, 0x20, 0x03, 0xd5, 0xc0, 0x03, 0x5f, 0xd6], + ); + dicachesync(heap_memory.as_mut_ptr(), heap_memory.len()); let heap_fn: fn() = core::mem::transmute(Box::into_raw(heap_memory)); heap_fn(); black_box(heap_fn); // avoid optimization when running in release mode @@ -489,17 +499,23 @@ fn log_message(message: String, level: i32) { } #[guest_function("TriggerException")] -#[cfg(target_arch = "x86_64")] fn trigger_exception() { // trigger an undefined instruction exception - unsafe { core::arch::asm!("ud2") }; + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!("ud2") + }; + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!("udf #0") + }; } /// Execute an OUT instruction with an arbitrary port and value. /// This is used to test that invalid OUT ports cause errors. #[guest_function("OutbWithPort")] -#[cfg(target_arch = "x86_64")] fn outb_with_port(port: u32, value: u32) { + #[cfg(target_arch = "x86_64")] unsafe { core::arch::asm!( "out dx, eax", @@ -508,6 +524,12 @@ fn outb_with_port(port: u32, value: u32) { options(preserves_flags, nomem, nostack) ); } + #[cfg(target_arch = "aarch64")] + unsafe { + (hyperlight_common::layout::io_page().unwrap().1 as *mut u64) + .wrapping_add(port as usize) + .write_volatile(value as u64); + } } // ============================================================================= @@ -515,6 +537,7 @@ fn outb_with_port(port: u32, value: u32) { // ============================================================================= /// Counter incremented by the timer interrupt handler. +#[cfg(target_arch = "x86_64")] static TIMER_IRQ_COUNT: AtomicU32 = AtomicU32::new(0); // Timer IRQ handler (vector 0x20 = IRQ0 after PIC remapping). @@ -542,6 +565,7 @@ unsafe extern "C" { } /// IDT pointer structure for SIDT/LIDT instructions. +#[cfg(target_arch = "x86_64")] #[repr(C, packed)] struct IdtPtr { limit: u16, @@ -720,16 +744,28 @@ fn use_sse2_registers() { } #[guest_function("SetDr0")] -#[cfg(target_arch = "x86_64")] fn set_dr0(value: u64) { - unsafe { core::arch::asm!("mov dr0, {}", in(reg) value) }; + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!("mov dr0, {}", in(reg) value) + }; + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!("msr dbgbvr0_el1, {}", in(reg) value) + }; } #[guest_function("GetDr0")] -#[cfg(target_arch = "x86_64")] fn get_dr0() -> u64 { let value: u64; - unsafe { core::arch::asm!("mov {}, dr0", out(reg) value) }; + #[cfg(target_arch = "x86_64")] + unsafe { + core::arch::asm!("mov {}, dr0", out(reg) value) + }; + #[cfg(target_arch = "aarch64")] + unsafe { + core::arch::asm!("mrs {}, dbgbvr0_el1", out(reg) value) + }; value } @@ -763,7 +799,6 @@ fn read_from_user_memory(num: u64, expected: Vec) -> Result> { Ok(bytes) } -#[cfg(target_arch = "x86_64")] #[guest_function("ReadMappedBuffer")] fn read_mapped_buffer(base: u64, len: u64, do_map: bool) -> Vec { let base = base as usize as *const u8; @@ -790,7 +825,6 @@ fn read_mapped_buffer(base: u64, len: u64, do_map: bool) -> Vec { data.to_vec() } -#[cfg(target_arch = "x86_64")] #[guest_function("CheckMapped")] fn check_mapped_buffer(base: u64) -> bool { hyperlight_guest_bin::paging::virt_to_phys(base) @@ -798,7 +832,6 @@ fn check_mapped_buffer(base: u64) -> bool { .is_some() } -#[cfg(target_arch = "x86_64")] #[guest_function("WriteMappedBuffer")] fn write_mapped_buffer(base: u64, len: u64) -> bool { let base = base as usize as *mut u8; @@ -827,7 +860,60 @@ fn write_mapped_buffer(base: u64, len: u64) -> bool { true } -#[cfg(target_arch = "x86_64")] +fn dicachesync(_base: *mut u8, _len: usize) { + #[cfg(target_arch = "aarch64")] + unsafe { + let ctr_el0: u64; + core::arch::asm!("mrs {}, ctr_el0", out(reg) ctr_el0); + let iminline = 4 * (1 << (ctr_el0 & 0xf)); + #[allow(unused)] + let dminline = 4 * (1 << ((ctr_el0 >> 16) & 0xf)); + // See the comment in the `KVM_EXIT_ARM_NISV` case of + // `run_vcpu` in + // src/hyperlight_host/src/hypervisor/virtual_machine/kvm.rs + // for an explanation of why this cache maintenance sequence + // is so complex. + core::arch::asm!(" + ldr xzr, [{addr}] + msr nzcv, xzr + b 2f + + 0: ldr xzr, [{tmp}] + msr nzcv, xzr + b 3f + 1: ldr xzr, [{tmp}] + msr nzcv, xzr + b 4f + + 2: mov {tmp}, {addr} + + 3: dc cvau, {tmp} + b.eq 0b + add {tmp}, {tmp}, {dminline:x} + cmp {tmp}, {max} + b.lt 3b + + dsb ish + + mov {tmp}, {addr} + + 4: ic ivau, {tmp} + b.eq 1b + add {tmp}, {tmp}, {iminline:x} + cmp {tmp}, {max} + b.lt 4b + + dsb ish + isb + ", + iminline = in(reg) iminline, + dminline = in(reg) dminline, + addr = in(reg) _base as usize, + max = in(reg) _base as usize + _len, + tmp = out(reg) _); + } +} + #[guest_function("ExecMappedBuffer")] fn exec_mapped_buffer(base: u64, len: u64) -> bool { let base = base as usize as *mut u8; @@ -851,6 +937,9 @@ fn exec_mapped_buffer(base: u64, len: u64) -> bool { // Should be safe as long as data is something like a NOOP followed by a RET let func: fn() = unsafe { core::mem::transmute(data.as_ptr()) }; + + dicachesync(base, len); + func(); true @@ -995,7 +1084,6 @@ fn fuzz_guest_trace(max_depth: u32, msg: String) -> u32 { fuzz_traced_function(0, max_depth, &msg) } -#[cfg(target_arch = "x86_64")] #[guest_function("CorruptOutputSizePrefix")] fn corrupt_output_size_prefix() -> i32 { unsafe { @@ -1008,19 +1096,11 @@ fn corrupt_output_size_prefix() -> i32 { buf[8..12].copy_from_slice(&0xFFFF_FFFBu32.to_le_bytes()); buf[12..16].copy_from_slice(&[0u8; 4]); buf[16..24].copy_from_slice(&8_u64.to_le_bytes()); - - core::arch::asm!( - "out dx, eax", - "cli", - "hlt", - in("dx") hyperlight_common::outb::VmAction::Halt as u16, - in("eax") 0u32, - options(noreturn), - ); + outb_with_port(hyperlight_common::outb::VmAction::Halt as u32, 0u32); + unreachable!(); } } -#[cfg(target_arch = "x86_64")] #[guest_function("CorruptOutputBackPointer")] fn corrupt_output_back_pointer() -> i32 { unsafe { @@ -1032,15 +1112,8 @@ fn corrupt_output_back_pointer() -> i32 { buf[0..8].copy_from_slice(&24_u64.to_le_bytes()); buf[8..16].copy_from_slice(&[0u8; 8]); buf[16..24].copy_from_slice(&0xDEAD_u64.to_le_bytes()); - - core::arch::asm!( - "out dx, eax", - "cli", - "hlt", - in("dx") hyperlight_common::outb::VmAction::Halt as u16, - in("eax") 0u32, - options(noreturn), - ); + outb_with_port(hyperlight_common::outb::VmAction::Halt as u32, 0u32); + unreachable!(); } } From 7fcd260eb7987a9a3df3738d8f0c2f3c0291f21f Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 9 Jun 2026 03:00:16 +0100 Subject: [PATCH 22/29] aarch64: support running Clippy Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- Justfile | 4 ++-- hack/clippy-package-features.sh | 10 ++++++++-- .../src/hypervisor/virtual_machine/mshv/aarch64.rs | 2 ++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Justfile b/Justfile index f2edfb2da..c599a2cf8 100644 --- a/Justfile +++ b/Justfile @@ -321,7 +321,7 @@ fmt-apply: (ensure-nightly-fmt) cargo +{{nightly-toolchain}} fmt --manifest-path src/hyperlight_guest_capi/Cargo.toml clippy target=default-target: (witguest-wit) - {{ cargo-cmd }} clippy --all-targets --all-features --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -- -D warnings + {{ cargo-cmd }} clippy --all-targets {{ if hyperlight-target-arch == "x86_64" { "--all-features" } else { "" } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -- -D warnings # for use on a linux host-machine when cross-compiling to windows. Uses the windows-gnu which should be sufficient for most purposes clippyw target=default-target: (witguest-wit) @@ -346,7 +346,7 @@ clippy-exhaustive target=default-target: (witguest-wit) ./hack/clippy-package-features.sh hyperlight-testing {{ target }} {{ target-triple }} ./hack/clippy-package-features.sh hyperlight-component-macro {{ target }} {{ target-triple }} ./hack/clippy-package-features.sh hyperlight-component-util {{ target }} {{ target-triple }} - ./hack/clippy-package-features.sh hyperlight-guest-tracing {{ target }} + {{ if hyperlight-target-arch == "x86_64" { "./hack/clippy-package-features.sh hyperlight-guest-tracing " + target } else { "" } }} just clippy-guests {{ target }} # Test a specific package with all feature combinations diff --git a/hack/clippy-package-features.sh b/hack/clippy-package-features.sh index 05c605149..49b6716b8 100755 --- a/hack/clippy-package-features.sh +++ b/hack/clippy-package-features.sh @@ -30,13 +30,19 @@ if [[ "$PACKAGE" == "hyperlight-host" ]]; then REQUIRED_FEATURES=("kvm" "mshv3") elif [[ "$PACKAGE" == "hyperlight-guest-bin" ]]; then REQUIRED_FEATURES=("libc") -else +else REQUIRED_FEATURES=() fi +EXCLUDED_FEATURES=("${REQUIRED_FEATURES[@]}") + +if [[ -n "${TARGET_TRIPLE}" && "$TARGET_TRIPLE" =~ aarch64 ]] || [[ -z "${TARGET_TRIPLE}" && "$(uname -m)" = "aarch64" ]]; then + EXCLUDED_FEATURES+=("crashdump" "trace_guest" "mem_profile" "hw-interrupts" "gdb" "i686-guest" "nanvix-unstable") +fi + # Get all features for the package (excluding default and required features) # Always exclude "default", and exclude any required features using jq -features=$(cargo metadata --format-version 1 --no-deps | jq -r --arg pkg "$PACKAGE" '.packages[] | select(.name == $pkg) | .features | keys[] | select(. != "default" and (IN($ARGS.positional[])|not))' --args "${REQUIRED_FEATURES[@]}" || true) +features=$(cargo metadata --format-version 1 --no-deps | jq -r --arg pkg "$PACKAGE" '.packages[] | select(.name == $pkg) | .features | keys[] | select(. != "default" and (IN($ARGS.positional[])|not))' --args "${EXCLUDED_FEATURES[@]}" || true) # Convert required features array to comma-separated string for cargo if [[ ${#REQUIRED_FEATURES[@]} -gt 0 ]]; then diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs index 79d816b15..3580adad1 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/aarch64.rs @@ -31,7 +31,9 @@ pub(crate) struct MshvVm { _placeholder: (), } +#[allow(unused)] impl MshvVm { + #[allow(unused)] pub(crate) fn new() -> std::result::Result { unimplemented!("MshvVm::new") } From 96f5c910e664c4ca8af7ee7054aae957670054e3 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:43:35 +0100 Subject: [PATCH 23/29] [ci] Set up GitHub workflows for aarch64 KVM Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .github/workflows/ValidatePullRequest.yml | 34 +++++++++++++++++++++-- .github/workflows/dep_benchmarks.yml | 4 +-- .github/workflows/dep_build_guests.yml | 28 ++++++++++++++----- .github/workflows/dep_build_test.yml | 28 +++++++++++-------- .github/workflows/dep_fuzzing.yml | 15 ++++++++-- .github/workflows/dep_run_examples.yml | 23 +++++++-------- 6 files changed, 95 insertions(+), 37 deletions(-) diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index 659ec9acc..a3531da7b 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -60,11 +60,13 @@ jobs: strategy: fail-fast: true matrix: + arch: [X64, arm64] config: [debug, release] uses: ./.github/workflows/dep_build_guests.yml secrets: inherit with: docs_only: ${{ needs.docs-pr.outputs.docs-only }} + arch: ${{ matrix.arch }} config: ${{ matrix.config }} # Code checks (fmt, clippy, MSRV) - runs in parallel with build-guests @@ -92,14 +94,27 @@ jobs: fail-fast: true matrix: hypervisor: ['hyperv-ws2025', mshv3, kvm] - cpu: [amd, intel] + cpu: [amd, intel, apple] + arch: [X64, arm64] config: [debug, release] + exclude: + - cpu: apple + hypervisor: hyperv-ws2025 + - cpu: apple + hypervisor: mshv3 + - cpu: amd + arch: arm64 + - cpu: intel + arch: arm64 + - cpu: apple + arch: X64 uses: ./.github/workflows/dep_build_test.yml secrets: inherit - with: + with: docs_only: ${{ needs.docs-pr.outputs.docs-only }} hypervisor: ${{ matrix.hypervisor }} cpu: ${{ matrix.cpu }} + arch: ${{ matrix.arch }} config: ${{ matrix.config }} # Run examples - needs guest artifacts, runs in parallel with build-test @@ -115,14 +130,27 @@ jobs: fail-fast: true matrix: hypervisor: ['hyperv-ws2025', mshv3, kvm] - cpu: [amd, intel] + cpu: [amd, intel, apple] + arch: [X64, arm64] config: [debug, release] + exclude: + - cpu: apple + hypervisor: hyperv-ws2025 + - cpu: apple + hypervisor: mshv3 + - cpu: amd + arch: arm64 + - cpu: intel + arch: arm64 + - cpu: apple + arch: X64 uses: ./.github/workflows/dep_run_examples.yml secrets: inherit with: docs_only: ${{ needs.docs-pr.outputs.docs-only }} hypervisor: ${{ matrix.hypervisor }} cpu: ${{ matrix.cpu }} + arch: ${{ matrix.arch }} config: ${{ matrix.config }} fuzzing: diff --git a/.github/workflows/dep_benchmarks.yml b/.github/workflows/dep_benchmarks.yml index 167b2e846..690b75779 100644 --- a/.github/workflows/dep_benchmarks.yml +++ b/.github/workflows/dep_benchmarks.yml @@ -103,13 +103,13 @@ jobs: - name: Download Rust guests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: rust-guests-release + name: rust-guests-X64-release path: src/tests/rust_guests/bin/release/ - name: Download C guests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: c-guests-release + name: c-guests-X64-release path: src/tests/c_guests/bin/release/ - name: Build diff --git a/.github/workflows/dep_build_guests.yml b/.github/workflows/dep_build_guests.yml index d36caef90..071dab8a9 100644 --- a/.github/workflows/dep_build_guests.yml +++ b/.github/workflows/dep_build_guests.yml @@ -10,6 +10,10 @@ on: required: false type: string default: "false" + arch: + description: CPU architecture for the build (passed from caller matrix) + required: true + type: string config: description: Build configuration (debug or release) required: true @@ -29,7 +33,17 @@ jobs: build-guests: if: ${{ inputs.docs_only == 'false' }} timeout-minutes: 15 - runs-on: [self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd", "JobId=build-guests-${{ inputs.config }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}"] + runs-on: ${{ fromJson( + format('["self-hosted", "Linux", "{0}", "{1}" {2}]', + inputs.arch, + inputs.arch == 'X64' && '1ES.Pool=hld-kvm-amd' || 'ubuntu-24.04', + inputs.arch == 'X64' && format(', "JobId=build-guests-{0}-{1}-{2}-{3}"', + inputs.config, + github.run_id, + github.run_number, + github.run_attempt) + || '' + )) }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -52,12 +66,12 @@ jobs: with: path: | src/tests/rust_guests/target/sysroot - key: sysroot-linux-${{ inputs.config }}-${{ hashFiles('rust-toolchain.toml') }} + key: sysroot-linux-${{ inputs.arch }}-${{ inputs.config }}-${{ hashFiles('rust-toolchain.toml') }} - name: Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: - shared-key: "guests-${{ inputs.config }}" + shared-key: "guests-${{ inputs.arch }}-${{ inputs.config }}" cache-on-failure: "true" # Only save on main as caches are not shared across branches. # https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache @@ -82,7 +96,7 @@ jobs: - name: Upload Rust guests uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: rust-guests-${{ inputs.config }} + name: rust-guests-${{ inputs.arch }}-${{ inputs.config }} path: | src/tests/rust_guests/bin/${{ inputs.config }}/ retention-days: 1 @@ -91,13 +105,13 @@ jobs: - name: Upload C guests uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: c-guests-${{ inputs.config }} + name: c-guests-${{ inputs.arch }}-${{ inputs.config }} path: src/tests/c_guests/bin/${{ inputs.config }}/ retention-days: 1 if-no-files-found: error - name: Upload interface.wasm - if: inputs.config == 'debug' + if: inputs.config == 'debug' && inputs.arch == 'X64' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: interface-wasm @@ -106,7 +120,7 @@ jobs: if-no-files-found: error - name: Upload twoworlds.wasm - if: inputs.config == 'debug' + if: inputs.config == 'debug' && inputs.arch == 'X64' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: twoworlds-wasm diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index 91ce867aa..9885961ed 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -19,6 +19,10 @@ on: required: true type: string cpu: + description: CPU vendor for the build (passed from caller matrix) + required: true + type: string + arch: description: CPU architecture for the build (passed from caller matrix) required: true type: string @@ -39,14 +43,11 @@ jobs: if: ${{ inputs.docs_only == 'false' }} timeout-minutes: 45 runs-on: ${{ fromJson( - format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}", "JobId=build-and-test-{3}-{4}-{5}-{6}"]', - inputs.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux', - inputs.hypervisor == 'hyperv-ws2025' && 'win2025' || inputs.hypervisor == 'mshv3' && 'azlinux3-mshv' || inputs.hypervisor, - inputs.cpu, - inputs.config, - github.run_id, - github.run_number, - github.run_attempt)) }} + format('["self-hosted", "{0}", "{1}", {2} {3}]', + inputs.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux', + inputs.arch, + inputs.arch == 'X64' && format('"1ES.Pool=hld-{0}-{1}"', inputs.hypervisor == 'hyperv-ws2025' && 'win2025' || inputs.hypervisor == 'mshv3' && 'azlinux3-mshv' || inputs.hypervisor, inputs.cpu) || inputs.hypervisor == 'kvm' && '"kvm", "ubuntu-24.04"' || '', + inputs.arch == 'X64' && format(', "JobId=build-and-test-{0}-{1}-{2}-{3}"', inputs.config, github.run_id, github.run_number, github.run_attempt) || '')) }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -64,7 +65,7 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: - shared-key: "${{ runner.os }}-${{ inputs.hypervisor }}-${{ inputs.config }}" + shared-key: "${{ inputs.arch }}-${{ runner.os }}-${{ inputs.hypervisor }}-${{ inputs.config }}" cache-on-failure: "true" # Only save on main as caches are not shared across branches. # https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache @@ -73,13 +74,13 @@ jobs: - name: Download Rust guests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: rust-guests-${{ inputs.config }} + name: rust-guests-${{ inputs.arch }}-${{ inputs.config }} path: src/tests/rust_guests/bin/${{ inputs.config }}/ - name: Download C guests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: c-guests-${{ inputs.config }} + name: c-guests-${{ inputs.arch }}-${{ inputs.config }} path: src/tests/c_guests/bin/${{ inputs.config }}/ - name: Download interface.wasm @@ -119,22 +120,25 @@ jobs: just test ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv3' && 'mshv3' || 'kvm' }} - name: Run Rust tests with hw-interrupts + if: runner.arch == 'X64' run: | # with hw-interrupts feature enabled (+ explicit driver on Linux) just test ${{ inputs.config }} ${{ runner.os == 'Linux' && (inputs.hypervisor == 'mshv3' && 'mshv3,hw-interrupts' || 'kvm,hw-interrupts') || 'hw-interrupts' }} - name: Run Rust Gdb tests + if: runner.arch == 'X64' env: RUST_LOG: debug run: just test-rust-gdb-debugging ${{ inputs.config }} - name: Run Rust Crashdump tests + if: runner.arch == 'X64' env: RUST_LOG: debug run: just test-rust-crashdump ${{ inputs.config }} - name: Run Rust Tracing tests - if: runner.os == 'Linux' + if: runner.arch == 'X64' && runner.os == 'Linux' env: RUST_LOG: debug run: just test-rust-tracing ${{ inputs.config }} diff --git a/.github/workflows/dep_fuzzing.yml b/.github/workflows/dep_fuzzing.yml index 483cfc779..99b3d351a 100644 --- a/.github/workflows/dep_fuzzing.yml +++ b/.github/workflows/dep_fuzzing.yml @@ -23,10 +23,21 @@ permissions: jobs: fuzz: if: ${{ inputs.docs_only == 'false' }} - runs-on: [ self-hosted, Linux, X64, "1ES.Pool=hld-kvm-amd", "JobId=fuzz-${{ matrix.target }}-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}" ] + runs-on: ${{ fromJson( + format('["self-hosted", "Linux", "{0}", "{1}" {2}]', + matrix.arch, + matrix.arch == 'X64' && '1ES.Pool=hld-kvm-amd' || 'ubuntu-24.04', + matrix.arch == 'X64' && format(', "JobId=fuzz-{0}-{1}-{2}-{3}"', + matrix.target, + github.run_id, + github.run_number, + github.run_attempt) + || '' + )) }} strategy: matrix: target: ${{ fromJson(inputs.targets) }} + arch: [X64, arm64] steps: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -40,7 +51,7 @@ jobs: - name: Download Rust guests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: rust-guests-release + name: rust-guests-${{ matrix.arch }}-release path: src/tests/rust_guests/bin/release/ - name: Install cargo-fuzz diff --git a/.github/workflows/dep_run_examples.yml b/.github/workflows/dep_run_examples.yml index c3e83d5cc..b32cdfa74 100644 --- a/.github/workflows/dep_run_examples.yml +++ b/.github/workflows/dep_run_examples.yml @@ -19,6 +19,10 @@ on: required: true type: string cpu: + description: CPU vendor for the build (passed from caller matrix) + required: true + type: string + arch: description: CPU architecture for the build (passed from caller matrix) required: true type: string @@ -39,14 +43,11 @@ jobs: if: ${{ inputs.docs_only == 'false' }} timeout-minutes: 15 runs-on: ${{ fromJson( - format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}", "JobId=run-examples-{3}-{4}-{5}-{6}"]', - inputs.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux', - inputs.hypervisor == 'hyperv-ws2025' && 'win2025' || inputs.hypervisor == 'mshv3' && 'azlinux3-mshv' || inputs.hypervisor, - inputs.cpu, - inputs.config, - github.run_id, - github.run_number, - github.run_attempt)) }} + format('["self-hosted", "{0}", "{1}", {2} {3}]', + inputs.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux', + inputs.arch, + inputs.arch == 'X64' && format('"1ES.Pool=hld-{0}-{1}"', inputs.hypervisor == 'hyperv-ws2025' && 'win2025' || inputs.hypervisor == 'mshv3' && 'azlinux3-mshv' || inputs.hypervisor, inputs.cpu) || inputs.hypervisor == 'kvm' && '"kvm", "ubuntu-24.04"' || '', + inputs.arch == 'X64' && format(', "JobId=run-examples-{0}-{1}-{2}-{3}"', inputs.config, github.run_id, github.run_number, github.run_attempt) || '')) }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -64,19 +65,19 @@ jobs: - name: Rust cache uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1 with: - shared-key: "${{ runner.os }}-${{ inputs.hypervisor }}-${{ inputs.config }}" + shared-key: "${{ inputs.arch }}-${{ runner.os }}-${{ inputs.hypervisor }}-${{ inputs.config }}" save-if: "false" - name: Download Rust guests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: rust-guests-${{ inputs.config }} + name: rust-guests-${{ inputs.arch }}-${{ inputs.config }} path: src/tests/rust_guests/bin/${{ inputs.config }}/ - name: Download C guests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: c-guests-${{ inputs.config }} + name: c-guests-${{ inputs.arch }}-${{ inputs.config }} path: src/tests/c_guests/bin/${{ inputs.config }}/ - name: Run Rust examples - windows From c38a608f1279e68d6d677ff0123eb3e4ce425416 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:08:24 +0100 Subject: [PATCH 24/29] libc: copy includes/stdio from picolibc This makes the logic in libc build.rs consistent with that in cargo-hyperlight. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- src/hyperlight_libc/build.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hyperlight_libc/build.rs b/src/hyperlight_libc/build.rs index e652988cb..2b2cfb382 100644 --- a/src/hyperlight_libc/build.rs +++ b/src/hyperlight_libc/build.rs @@ -287,6 +287,7 @@ fn cargo_main() -> Result<()> { build.compile("hyperlight_libc"); copy_includes(&include_dir, picolibc_dir.join("libc/include"))?; + copy_includes(&include_dir, picolibc_dir.join("libc/stdio"))?; copy_includes(&include_dir, manifest_dir.join("include"))?; // Generate bindings using bindgen From 8361179f9e4dc6172fd42bade2afd950651c81b6 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:08:33 +0100 Subject: [PATCH 25/29] Update cargo-hyperlight to 0.1.12 This is required to have a version with support for building aarch64 guests. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Justfile b/Justfile index c599a2cf8..f7141ff40 100644 --- a/Justfile +++ b/Justfile @@ -8,7 +8,7 @@ bin-suffix := if os() == "windows" { ".bat" } else { ".sh" } nightly-toolchain := "nightly-2026-02-27" # Pinned cargo-hyperlight version used to build the guest sysroot. Keep this in # lockstep with the version pinned in flake.nix. -cargo-hyperlight-version := "0.1.11" +cargo-hyperlight-version := "0.1.12" ################ ### cross-rs ### From 40eae63b9ae79440b8f7f8707648750a6070e9dd Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:49:43 +0100 Subject: [PATCH 26/29] [snapshot/file] Remove Serde mirror structures for system registers This had resulted in a whole bunch of x86-64-specific code in the snapshot-to-file case. This commit repalces those mirror structs with serde derives in the architecture-specific files that define the system register structure for the machine. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../hypervisor/regs/aarch64/special_regs.rs | 8 +- .../hypervisor/regs/x86_64/special_regs.rs | 15 +- .../src/sandbox/snapshot/file/config.rs | 187 ++++-------------- .../src/sandbox/snapshot/file/mod.rs | 10 +- 4 files changed, 56 insertions(+), 164 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/special_regs.rs index b5eb44b53..0d80977eb 100644 --- a/src/hyperlight_host/src/hypervisor/regs/aarch64/special_regs.rs +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/special_regs.rs @@ -14,8 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -#[derive(Debug, Default, Copy, Clone, PartialEq)] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct CommonSpecialRegisters { + // When serializing for a snapshot, skip TTBRs which will be + // recomputed from the snapshot layout + #[serde(skip)] pub(crate) ttbr0_el1: u64, // todo: handle ttbr1 as well pub(crate) tcr_el1: u64, diff --git a/src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs b/src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs index e0398aa16..e67ab3b49 100644 --- a/src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs +++ b/src/hyperlight_host/src/hypervisor/regs/x86_64/special_regs.rs @@ -21,6 +21,7 @@ use std::collections::HashSet; use kvm_bindings::{kvm_dtable, kvm_segment, kvm_sregs}; #[cfg(mshv3)] use mshv_bindings::{SegmentRegister, SpecialRegisters, TableRegister}; +use serde::{Deserialize, Serialize}; #[cfg(target_os = "windows")] use windows::Win32::System::Hypervisor::*; @@ -47,7 +48,8 @@ mod amd64_consts { } use amd64_consts::*; -#[derive(Debug, Default, Copy, Clone, PartialEq)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct CommonSpecialRegisters { pub cs: CommonSegmentRegister, pub ds: CommonSegmentRegister, @@ -61,6 +63,11 @@ pub(crate) struct CommonSpecialRegisters { pub idt: CommonTableRegister, pub cr0: u64, pub cr2: u64, + // CR3 should not be serialised into a snapshot, because it is + // reconstructed from the snapshot's layout via + // `get_pt_base_gpa()`. Omitting it keeps the config digest stable + // across re-snapshots of the same state. + #[serde(skip)] pub cr3: u64, pub cr4: u64, pub cr8: u64, @@ -373,7 +380,8 @@ impl TryFrom<&[(WHV_REGISTER_NAME, Align16)]> for CommonSpec // --- Segment Register --- -#[derive(Debug, Default, Copy, Clone, PartialEq)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct CommonSegmentRegister { pub base: u64, pub limit: u32, @@ -537,7 +545,8 @@ impl From for WHV_REGISTER_VALUE { // --- Table Register --- -#[derive(Debug, Default, Copy, Clone, PartialEq)] +#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub(crate) struct CommonTableRegister { pub base: u64, pub limit: u16, diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs index 1277c8380..71b65ca17 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/config.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/config.rs @@ -20,7 +20,7 @@ use hyperlight_common::vmem::PAGE_SIZE; use serde::{Deserialize, Serialize}; use super::media_types::SNAPSHOT_ABI_VERSION; -use crate::hypervisor::regs::{CommonSegmentRegister, CommonSpecialRegisters, CommonTableRegister}; +use crate::hypervisor::regs::CommonSpecialRegisters; use crate::mem::layout::SandboxMemoryLayout; // --- Arch and hypervisor identifiers -------------------------------- @@ -129,7 +129,7 @@ pub(super) struct OciSnapshotConfig { pub(super) entrypoint_addr: u64, /// Special registers captured from the paused vCPU, restored /// verbatim when resuming the call. - pub(super) sregs: Sregs, + pub(super) sregs: CommonSpecialRegisters, pub(super) layout: MemoryLayout, /// Total size of the memory blob in bytes (including the guest /// page-table tail, if any). Equal to `self.memory.mem_size()`. @@ -274,150 +274,6 @@ impl From for ReturnType { } } -/// Captured x86_64 special registers for a paused vCPU. Round-trips -/// to/from [`CommonSpecialRegisters`] and is restored verbatim when -/// resuming a `Call` entrypoint. -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub(super) struct Sregs { - cs: SegmentRegister, - ds: SegmentRegister, - es: SegmentRegister, - fs: SegmentRegister, - gs: SegmentRegister, - ss: SegmentRegister, - tr: SegmentRegister, - ldt: SegmentRegister, - gdt: TableRegister, - idt: TableRegister, - cr0: u64, - cr2: u64, - // CR3 is recomputed at load from the reconstructed layout via - // `get_pt_base_gpa()`, so it is omitted to keep the config - // digest stable across re-snapshots of the same guest state. - cr4: u64, - cr8: u64, - efer: u64, - apic_base: u64, - interrupt_bitmap: [u64; 4], -} - -/// Serde mirror of [`CommonSegmentRegister`]. -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct SegmentRegister { - base: u64, - limit: u32, - selector: u16, - type_: u8, - present: u8, - dpl: u8, - db: u8, - s: u8, - l: u8, - g: u8, - avl: u8, - unusable: u8, - padding: u8, -} - -/// Serde mirror of [`CommonTableRegister`]. -#[derive(Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -struct TableRegister { - base: u64, - limit: u16, -} - -// --- Conversions between repr and runtime types --------------------- - -impl From<&CommonSpecialRegisters> for Sregs { - fn from(s: &CommonSpecialRegisters) -> Self { - let seg = |r: &CommonSegmentRegister| SegmentRegister { - base: r.base, - limit: r.limit, - selector: r.selector, - type_: r.type_, - present: r.present, - dpl: r.dpl, - db: r.db, - s: r.s, - l: r.l, - g: r.g, - avl: r.avl, - unusable: r.unusable, - padding: r.padding, - }; - let tab = |r: &CommonTableRegister| TableRegister { - base: r.base, - limit: r.limit, - }; - Self { - cs: seg(&s.cs), - ds: seg(&s.ds), - es: seg(&s.es), - fs: seg(&s.fs), - gs: seg(&s.gs), - ss: seg(&s.ss), - tr: seg(&s.tr), - ldt: seg(&s.ldt), - gdt: tab(&s.gdt), - idt: tab(&s.idt), - cr0: s.cr0, - cr2: s.cr2, - cr4: s.cr4, - cr8: s.cr8, - efer: s.efer, - apic_base: s.apic_base, - interrupt_bitmap: s.interrupt_bitmap, - } - } -} - -impl From for CommonSpecialRegisters { - fn from(r: Sregs) -> Self { - let seg = |s: SegmentRegister| CommonSegmentRegister { - base: s.base, - limit: s.limit, - selector: s.selector, - type_: s.type_, - present: s.present, - dpl: s.dpl, - db: s.db, - s: s.s, - l: s.l, - g: s.g, - avl: s.avl, - unusable: s.unusable, - padding: s.padding, - }; - let tab = |t: TableRegister| CommonTableRegister { - base: t.base, - limit: t.limit, - }; - Self { - cs: seg(r.cs), - ds: seg(r.ds), - es: seg(r.es), - fs: seg(r.fs), - gs: seg(r.gs), - ss: seg(r.ss), - tr: seg(r.tr), - ldt: seg(r.ldt), - gdt: tab(r.gdt), - idt: tab(r.idt), - cr0: r.cr0, - cr2: r.cr2, - cr3: 0, - cr4: r.cr4, - cr8: r.cr8, - efer: r.efer, - apic_base: r.apic_base, - interrupt_bitmap: r.interrupt_bitmap, - } - } -} - impl From<&HostFunctionDefinition> for HostFunction { fn from(d: &HostFunctionDefinition) -> Self { let parameter_types = d @@ -585,10 +441,13 @@ mod tests { use hyperlight_common::flatbuffer_wrappers::function_types::{ParameterType, ReturnType}; use super::*; + #[cfg(target_arch = "x86_64")] + use crate::hypervisor::regs::{CommonSegmentRegister, CommonTableRegister}; /// Build a `CommonSegmentRegister` whose every field holds a /// distinct value, so a transposed field in the `Sregs` /// conversion produces an inequality. + #[cfg(target_arch = "x86_64")] fn distinct_segment(start: u64) -> CommonSegmentRegister { CommonSegmentRegister { base: start, @@ -607,6 +466,7 @@ mod tests { } } + #[cfg(target_arch = "x86_64")] fn distinct_table(start: u64) -> CommonTableRegister { CommonTableRegister { base: start, @@ -617,7 +477,8 @@ mod tests { /// Special registers with a unique value in every field, including /// a nonzero `cr3`. fn distinct_sregs() -> CommonSpecialRegisters { - CommonSpecialRegisters { + #[cfg(target_arch = "x86_64")] + let sr = CommonSpecialRegisters { cs: distinct_segment(10), ds: distinct_segment(30), es: distinct_segment(50), @@ -636,20 +497,38 @@ mod tests { efer: 205, apic_base: 206, interrupt_bitmap: [207, 208, 209, 210], - } + }; + #[cfg(target_arch = "aarch64")] + let sr = CommonSpecialRegisters { + ttbr0_el1: 10, + tcr_el1: 20, + mair_el1: 30, + sctlr_el1: 40, + cpacr_el1: 50, + vbar_el1: 60, + sp_el1: 60, + }; + sr } - /// Round-tripping special registers through the serde mirror - /// preserves every field. `cr3` is the sole exception: it is - /// omitted from the config and recomputed at load, so it returns - /// as zero. + /// Round-tripping special registers through serde preserves every + /// field. `cr3` is the sole exception: it is omitted from the + /// config and recomputed at load, so it returns as zero. #[test] fn sregs_round_trip_preserves_all_fields_except_cr3() { let original = distinct_sregs(); - let restored: CommonSpecialRegisters = Sregs::from(&original).into(); + let serialized = serde_json::to_vec(&original).unwrap(); + let restored: CommonSpecialRegisters = serde_json::from_slice(&serialized).unwrap(); let mut expected = original; - expected.cr3 = 0; + #[cfg(target_arch = "x86_64")] + { + expected.cr3 = 0; + } + #[cfg(target_arch = "aarch64")] + { + expected.ttbr0_el1 = 0; + } assert_eq!(restored, expected); } diff --git a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs index c95c129e6..a725bf378 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file/mod.rs @@ -32,7 +32,7 @@ use oci_spec::image::{ ImageManifestBuilder, MediaType, SCHEMA_VERSION, }; -use self::config::{Arch, HostFunction, Hypervisor, MemoryLayout, OciSnapshotConfig, Sregs}; +use self::config::{Arch, HostFunction, Hypervisor, MemoryLayout, OciSnapshotConfig}; use self::digest::{Digest256, oci_digest, parse_oci_digest, verify_blob_bytes, verify_blob_file}; use self::fsutil::{put_blob, put_blob_if_absent, read_bounded, replace_file_atomic}; use self::media_types::{ @@ -41,7 +41,6 @@ use self::media_types::{ }; use self::reference::{OciDigest, OciReference, OciTag}; use super::{NextAction, Snapshot}; -use crate::hypervisor::regs::CommonSpecialRegisters; use crate::mem::layout::SandboxMemoryLayout; use crate::mem::memory_region::MemoryRegionFlags; use crate::mem::shared_mem::{ReadonlySharedMemory, SharedMemory}; @@ -535,7 +534,7 @@ impl Snapshot { fn build_config(&self) -> crate::Result { let (entrypoint_addr, sregs) = match (self.entrypoint, self.sregs.as_ref()) { - (NextAction::Call(addr), Some(sregs)) => (addr, Sregs::from(sregs)), + (NextAction::Call(addr), Some(sregs)) => (addr, sregs), (NextAction::Call(_), None) => { return Err(crate::new_error!( "snapshot inconsistent: Call entrypoint must have sregs" @@ -568,7 +567,7 @@ impl Snapshot { .ok_or_else(|| crate::new_error!("no hypervisor available to tag snapshot"))?, stack_top_gva: self.stack_top_gva, entrypoint_addr, - sregs, + sregs: *sregs, layout: MemoryLayout { input_data_size: l.input_data_size, output_data_size: l.output_data_size, @@ -831,7 +830,6 @@ impl Snapshot { // 8. Build entrypoint + sregs back from the config. let entrypoint = NextAction::Call(cfg.entrypoint_addr); - let sregs = Some(CommonSpecialRegisters::from(cfg.sregs)); // 9. Reconstitute host_functions metadata. let snapshot_generation = cfg.snapshot_generation; @@ -853,7 +851,7 @@ impl Snapshot { memory, load_info: crate::mem::exe::LoadInfo::dummy(), stack_top_gva: cfg.stack_top_gva, - sregs, + sregs: Some(cfg.sregs), entrypoint, snapshot_generation, host_functions, From 3c8f3d498ba1fb65e58ca5f3265bdae9a8e7aedc Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:51:08 +0100 Subject: [PATCH 27/29] [tests] Remove assumption that loading an aarch64 snapshot should error This is only true on x86_64. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .../src/sandbox/snapshot/file_tests.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs index 9383d1b8b..f849b28d1 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/file_tests.rs @@ -413,7 +413,11 @@ fn arch_mismatch_rejected() { .unwrap(); rewrite_config(&path, |cfg| { - cfg["arch"] = Value::from("aarch64"); + cfg["arch"] = Value::from(if cfg!(target_arch = "x86_64") { + "aarch64" + } else { + "x86_64" + }); }); let err = unwrap_err_snapshot(Snapshot::checked_load( @@ -1759,7 +1763,11 @@ fn load_round_trips() { fn load_still_validates_config_fields() { let (_dir, path) = save_for_mutation(); rewrite_config(&path, |cfg| { - cfg["arch"] = Value::from("aarch64"); + cfg["arch"] = Value::from(if cfg!(target_arch = "x86_64") { + "aarch64" + } else { + "x86_64" + }); }); let err = unwrap_err_snapshot(Snapshot::load(&path, OciTag::new("latest").unwrap())); let msg = format!("{}", err); From bcd6d5dd27b4c9e4773028e8e8bd7b0f088ab198 Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Thu, 25 Jun 2026 15:51:48 +0100 Subject: [PATCH 28/29] [nix] Update flake.nix for the unreleased version of piet used by trace-dump Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- flake.nix | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/flake.nix b/flake.nix index a2a6cd0b0..ae01e57b9 100644 --- a/flake.nix +++ b/flake.nix @@ -112,14 +112,21 @@ rustc = toolchains.stable.rust; }; - manifests = [ - "Cargo.toml" - "src/tests/rust_guests/Cargo.toml" - ]; - manifestDeps = builtins.map (manifest: + manifests = { + "Cargo.toml" = { + outputHashes = { + "piet-0.8.0" = "sha256-yHF0axor+uaGC0RYhw1JmjvFLVTYZkTx1XzDtuN2KIk="; + }; + }; + "src/tests/rust_guests/Cargo.toml" = { + }; + }; + manifestDeps = lib.mapAttrsToList (manifest: importArguments: let lockPath = builtins.replaceStrings [ "toml" ] [ "lock" ] manifest; in let lockFile = ./${lockPath}; in - rust-platform.importCargoLock { inherit lockFile; }) manifests; + rust-platform.importCargoLock ({ + inherit lockFile; + } // importArguments)) manifests; # when building a guest with cargo-hyperlight, or when # building a miri sysroot for the main workspace, we need to # include any crates.io dependencies of the standard library @@ -144,7 +151,7 @@ # like `cargo clippy` and `cargo hyperlight` (see # https://github.com/rust-lang/cargo/issues/11031). materialiseDeps = let - sortedManifests = lib.lists.sort (p: q: p > q) manifests; + sortedManifests = lib.lists.sort (p: q: p > q) (lib.attrNames manifests); matchClause = path: '' */${path}) root="''${manifest%${path}}" ;;''; matchClauses = lib.strings.concatStringsSep "\n" (builtins.map matchClause sortedManifests); From ea98d03dda538bdf94d3b0f9330113488583801a Mon Sep 17 00:00:00 2001 From: Lucy Menon <168595099+syntactically@users.noreply.github.com> Date: Fri, 26 Jun 2026 00:12:55 +0100 Subject: [PATCH 29/29] [ci] Switch dep_fuzzing to have its matrix in the calling workflow Previously, target selection was in the calling workflow, but the architecture selection was in the dep_fuzzing workflow, making it difficult to skip unsupported targets on some architectures. Signed-off-by: Lucy Menon <168595099+syntactically@users.noreply.github.com> --- .github/workflows/ValidatePullRequest.yml | 16 +++++++++++++--- .github/workflows/dep_fuzzing.yml | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ValidatePullRequest.yml b/.github/workflows/ValidatePullRequest.yml index a3531da7b..290cae199 100644 --- a/.github/workflows/ValidatePullRequest.yml +++ b/.github/workflows/ValidatePullRequest.yml @@ -161,12 +161,22 @@ jobs: # and a skipped dependency transitively skips all downstream jobs. # See: https://github.com/actions/runner/issues/2205 if: ${{ !cancelled() && !failure() }} + strategy: + matrix: + target: ['fuzz_host_print', 'fuzz_guest_call', 'fuzz_host_call', 'fuzz_guest_estimate_trace_event', 'fuzz_guest_trace'] + arch: [X64, arm64] + exclude: + - arch: arm64 + target: fuzz_guest_trace + - arch: arm64 + target: fuzz_guest_estimate_trace_event uses: ./.github/workflows/dep_fuzzing.yml + secrets: inherit with: - targets: '["fuzz_host_print", "fuzz_guest_call", "fuzz_host_call", "fuzz_guest_estimate_trace_event", "fuzz_guest_trace"]' # Pass as a JSON array - max_total_time: 300 # 5 minutes in seconds docs_only: ${{ needs.docs-pr.outputs.docs-only }} - secrets: inherit + max_total_time: 300 # 5 minutes in seconds + arch: ${{ matrix.arch }} + target: ${{ matrix.target }} spelling: name: spell check with typos diff --git a/.github/workflows/dep_fuzzing.yml b/.github/workflows/dep_fuzzing.yml index 99b3d351a..b5649b620 100644 --- a/.github/workflows/dep_fuzzing.yml +++ b/.github/workflows/dep_fuzzing.yml @@ -7,10 +7,14 @@ on: description: Maximum total time for the fuzz run in seconds required: true type: number - targets: + target: description: Fuzz targets to run required: true type: string + arch: + description: Architecture on which to fuzz + required: true + type: string docs_only: description: Skip fuzzing if docs only required: false @@ -25,19 +29,15 @@ jobs: if: ${{ inputs.docs_only == 'false' }} runs-on: ${{ fromJson( format('["self-hosted", "Linux", "{0}", "{1}" {2}]', - matrix.arch, - matrix.arch == 'X64' && '1ES.Pool=hld-kvm-amd' || 'ubuntu-24.04', - matrix.arch == 'X64' && format(', "JobId=fuzz-{0}-{1}-{2}-{3}"', - matrix.target, + inputs.arch, + inputs.arch == 'X64' && '1ES.Pool=hld-kvm-amd' || 'ubuntu-24.04', + inputs.arch == 'X64' && format(', "JobId=fuzz-{0}-{1}-{2}-{3}"', + inputs.target, github.run_id, github.run_number, github.run_attempt) || '' )) }} - strategy: - matrix: - target: ${{ fromJson(inputs.targets) }} - arch: [X64, arm64] steps: - name: Checkout code uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -51,14 +51,14 @@ jobs: - name: Download Rust guests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: rust-guests-${{ matrix.arch }}-release + name: rust-guests-${{ inputs.arch }}-release path: src/tests/rust_guests/bin/release/ - name: Install cargo-fuzz run: command -v cargo-fuzz >/dev/null 2>&1 || cargo install cargo-fuzz - name: Run Fuzzing - run: just fuzz-timed ${{ matrix.target }} ${{ inputs.max_total_time }} + run: just fuzz-timed ${{ inputs.target }} ${{ inputs.max_total_time }} working-directory: src/hyperlight_host - name: Upload Crash Artifacts