From 5fd100c979f58f9201a0c06c1d2a12934890e86d Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:31:23 -0700 Subject: [PATCH 1/7] feat(aarch64): add GDB debugging for Windows ARM64 WHP Adds GDB stub support for guests running on the ARM64 WHP backend. * Split the x86_64 debug code out of `hyperlight_vm/x86_64.rs` into a shared `hyperlight_vm/debug.rs`. * Rename `gdb/x86_64_target.rs` to `gdb/target.rs` and add per-architecture register handling in `gdb/arch/`. * Give `DebuggableVm` default methods so backends without hardware breakpoints or single step report `Unsupported`. * Implement `DebuggableVm` for the aarch64 WHP VM, using the batched register helpers. * Flush the host instruction cache after guest memory writes so software breakpoints take effect. * Document ARM64 debugging in `docs/how-to-debug-a-hyperlight-guest.md` and `CHANGELOG.md`. On Windows ARM64 (WHP): * `just build`, `just clippy`, `just guests`, `just test` and `just fmt-check` pass. --------- Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/how-to-debug-a-hyperlight-guest.md | 15 +- src/hyperlight_host/build.rs | 3 +- .../examples/guest-debugging/main.rs | 74 +- .../src/hypervisor/gdb/arch.rs | 97 +-- .../src/hypervisor/gdb/arch/aarch64.rs | 183 +++++ .../src/hypervisor/gdb/arch/x86_64.rs | 61 ++ .../src/hypervisor/gdb/event_loop.rs | 13 +- src/hyperlight_host/src/hypervisor/gdb/mod.rs | 93 ++- .../gdb/{x86_64_target.rs => target.rs} | 142 +++- .../src/hypervisor/hyperlight_vm/aarch64.rs | 39 +- .../src/hypervisor/hyperlight_vm/debug.rs | 632 ++++++++++++++++++ .../src/hypervisor/hyperlight_vm/mod.rs | 59 +- .../src/hypervisor/hyperlight_vm/x86_64.rs | 421 +----------- .../src/hypervisor/virtual_machine/mod.rs | 7 + .../hypervisor/virtual_machine/whp/aarch64.rs | 411 +++++++++++- .../src/sandbox/initialized.rs | 14 + 17 files changed, 1697 insertions(+), 568 deletions(-) create mode 100644 src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs create mode 100644 src/hyperlight_host/src/hypervisor/gdb/arch/x86_64.rs rename src/hyperlight_host/src/hypervisor/gdb/{x86_64_target.rs => target.rs} (79%) create mode 100644 src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c095f6de1..ecea482dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added * `Sandbox::snapshot_with_options` takes a snapshot with `SnapshotOptions`. `SnapshotOptions::scratch_size` gives the sandbox, and later restores of the snapshot, a different scratch size. A guest can boot with the scratch its initialisation needs and run guest function calls from a snapshot with only the scratch they need, so each restore costs less. It can also give a guest more scratch than it booted with. +* Support register, memory, continue, single-step, interrupt, and software-breakpoint GDB debugging for Windows ARM64 guests. ### Changed * Core dumps no longer record the guest binary path. The process name and command line are always ``, as they already were for sandboxes created from snapshots by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1975 diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index 2deb9894d..c8e5ab235 100644 --- a/docs/how-to-debug-a-hyperlight-guest.md +++ b/docs/how-to-debug-a-hyperlight-guest.md @@ -7,15 +7,22 @@ to start listening for a gdb connection. ## Supported features The Hyperlight `gdb` feature enables guest debugging to: - - stop at an entry point breakpoint which is automatically set by Hyperlight - - add and remove HW breakpoints (maximum 4 set breakpoints at a time) + - stop before the first vCPU run - add and remove SW breakpoints - read and write registers - read and write addresses - - step/continue + - continue + - single-step - get code offset from target - stop when a crash occurs and only allow read access to the guest memory and registers +On x86_64, gdb can also add up to four hardware breakpoints. +Windows ARM64 uses four-byte HVC software breakpoints and software single-step. +ARM64 stepping supports linear instructions, direct and conditional branches, +compare and test branches, and `BR`, `BLR`, and `RET`. Other control-flow and +exception instructions return an explicit error. Hardware breakpoints and +watchpoints are not advertised on Windows ARM64. + ## Expected behavior Below is a list describing some cases of expected behavior from a gdb debug @@ -170,7 +177,7 @@ involved in the gdb debugging of a Hyperlight guest running inside a **KVM** or | │ | create_gdb_thread | | │ | │ |◄─────────────────────────────────────────┌─┐ vcpu stopped ┌─┐ │ | attach │ ┌─┐ │ │◄──────────────────────────────┴─┘ │ - ┌─┐───────────────────────┼────────►│ │ │ │ entrypoint breakpoint | │ + ┌─┐───────────────────────┼────────►│ │ │ │ initial debug stop | │ │ │ attach response │ │ │ │ │ | │ │ │◄──────────────────────┼─────────│ │ │ │ | │ │ │ │ │ │ │ │ | │ diff --git a/src/hyperlight_host/build.rs b/src/hyperlight_host/build.rs index 43c4484e6..98b357caa 100644 --- a/src/hyperlight_host/build.rs +++ b/src/hyperlight_host/build.rs @@ -150,7 +150,8 @@ fn main() -> Result<()> { // Essentially the kvm and mshv3 features are ignored on windows as long as you use #[cfg(kvm)] and not #[cfg(feature = "kvm")]. // You should never use #[cfg(feature = "kvm")] or #[cfg(feature = "mshv3")] in the codebase. cfg_aliases::cfg_aliases! { - gdb: { all(feature = "gdb", debug_assertions, target_arch = "x86_64") }, + gdb_platform: { any(target_arch = "x86_64", all(target_arch = "aarch64", target_os = "windows")) }, + gdb: { all(feature = "gdb", debug_assertions, gdb_platform) }, kvm: { all(feature = "kvm", target_os = "linux") }, mshv3: { all(feature = "mshv3", target_os = "linux") }, hvf: { all(feature = "hvf", target_os = "macos") }, diff --git a/src/hyperlight_host/examples/guest-debugging/main.rs b/src/hyperlight_host/examples/guest-debugging/main.rs index 47a493a9b..631e95ec4 100644 --- a/src/hyperlight_host/examples/guest-debugging/main.rs +++ b/src/hyperlight_host/examples/guest-debugging/main.rs @@ -34,10 +34,12 @@ fn main() -> hyperlight_host::Result<()> { .host_function("Sleep5Secs", sleep_5_secs) .build()?; - // Call guest function + #[cfg(target_arch = "x86_64")] multi_use_sandbox_dbg .call::<()>("UseSSE2Registers", ()) .unwrap(); + #[cfg(target_arch = "aarch64")] + multi_use_sandbox_dbg.call::<()>("NoOp", ()).unwrap(); let message = "Hello, World! I am executing inside of a VM with debugger attached :)\n".to_string(); @@ -291,6 +293,76 @@ mod tests { } #[test] + #[cfg(target_arch = "aarch64")] + #[serial] + fn test_gdb_continue_over_sw_breakpoint() { + let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-aarch64-continue"); + let cmd = format!( + "file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest + target remote :8080 + set pagination off + set logging file {out_file_path} + set logging enabled on + break simpleguest::no_op + commands 1 + continue + end + break simpleguest::print_output + commands 2 + echo Continued over ARM64 software breakpoint\\n + set logging enabled off + detach + quit + end + continue + " + ); + #[cfg(windows)] + let cmd = format!("set osabi none\n{cmd}"); + let checker = + |contents: String| contents.contains("Continued over ARM64 software breakpoint"); + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker); + + cleanup(&out_file_path, &cmd_file_path); + assert!(result.is_ok(), "{}", result.unwrap_err()); + } + + #[test] + #[cfg(target_arch = "aarch64")] + #[serial] + fn test_gdb_single_step() { + let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-aarch64-step"); + let cmd = format!( + "file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest + target remote :8080 + set pagination off + set logging file {out_file_path} + set logging enabled on + break simpleguest::no_op + stepi + echo Stepped over ARM64 linear instruction\\n + continue + stepi + echo Stepped over ARM64 RET\\n + set logging enabled off + detach + quit + " + ); + #[cfg(windows)] + let cmd = format!("set osabi none\n{cmd}"); + let checker = |contents: String| { + contents.contains("Stepped over ARM64 linear instruction") + && contents.contains("Stepped over ARM64 RET") + }; + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker); + + cleanup(&out_file_path, &cmd_file_path); + assert!(result.is_ok(), "{}", result.unwrap_err()); + } + + #[test] + #[cfg(target_arch = "x86_64")] #[serial] fn test_gdb_sse_check() { let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-sse"); diff --git a/src/hyperlight_host/src/hypervisor/gdb/arch.rs b/src/hyperlight_host/src/hypervisor/gdb/arch.rs index b4cfca9ea..e7cea6ca8 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/arch.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/arch.rs @@ -1,91 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2025 The Hyperlight Authors. -//! This file contains architecture specific code for the x86_64 - -use super::{DebugError, DebuggableVm, VcpuStopReason}; -use crate::hypervisor::regs::CommonRegisters; -use crate::hypervisor::virtual_machine::RegisterError; - -/// Errors that can occur when determining the vCPU stop reason -#[derive(Debug, thiserror::Error)] -pub enum VcpuStopReasonError { - #[error("Failed to get registers: {0}")] - GetRegs(#[from] RegisterError), - #[error("Failed to remove hardware breakpoint: {0}")] - RemoveHwBreakpoint(#[from] DebugError), -} - -// Described in Table 6-1. Exceptions and Interrupts at Page 6-13 Vol. 1 -// of Intel 64 and IA-32 Architectures Software Developer's Manual -/// Exception id for #DB -pub(crate) const DB_EX_ID: u32 = 1; -/// Exception id for #BP - triggered by the INT3 instruction -pub(crate) const BP_EX_ID: u32 = 3; - -/// Software Breakpoint size in memory -pub(crate) const SW_BP_SIZE: usize = 1; -/// Software Breakpoint opcode - INT3 -/// Check page 7-28 Vol. 3A of Intel 64 and IA-32 -/// Architectures Software Developer's Manual -pub(crate) const SW_BP_OP: u8 = 0xCC; -/// Software Breakpoint written to memory -pub(crate) const SW_BP: [u8; SW_BP_SIZE] = [SW_BP_OP]; -/// Maximum number of supported hardware breakpoints -pub(crate) const MAX_NO_OF_HW_BP: usize = 4; - -/// Check page 19-4 Vol. 3B of Intel 64 and IA-32 -/// Architectures Software Developer's Manual -/// Bit position of BS flag in DR6 debug register -pub(crate) const DR6_BS_FLAG_POS: usize = 14; -/// Bit mask of BS flag in DR6 debug register -pub(crate) const DR6_BS_FLAG_MASK: u64 = 1 << DR6_BS_FLAG_POS; -/// Bit position of HW breakpoints status in DR6 debug register -pub(crate) const DR6_HW_BP_FLAGS_POS: usize = 0; -/// Bit mask of HW breakpoints status in DR6 debug register -pub(crate) const DR6_HW_BP_FLAGS_MASK: u64 = 0x0F << DR6_HW_BP_FLAGS_POS; - -/// Determine the reason the vCPU stopped -/// This is done by checking the DR6 register and the exception id -pub(crate) fn vcpu_stop_reason( - vm: &dyn DebuggableVm, - dr6: u64, - exception: u32, -) -> std::result::Result { - let CommonRegisters { rip, .. } = vm.regs()?; - if DB_EX_ID == exception { - // If the BS flag in DR6 register is set, it means a single step - // instruction triggered the exit - // Check page 19-4 Vol. 3B of Intel 64 and IA-32 - // Architectures Software Developer's Manual - if dr6 & DR6_BS_FLAG_MASK != 0 { - return Ok(VcpuStopReason::DoneStep); - } - - // If any of the B0-B3 flags in DR6 register is set, it means a - // hardware breakpoint triggered the exit - // Check page 19-4 Vol. 3B of Intel 64 and IA-32 - // Architectures Software Developer's Manual - if DR6_HW_BP_FLAGS_MASK & dr6 != 0 { - return Ok(VcpuStopReason::HwBp); - } - } - - if BP_EX_ID == exception { - return Ok(VcpuStopReason::SwBp); - } - - // Log an error and provide internal debugging info - tracing::error!( - r"The vCPU exited because of an unknown reason: - rip: {:?} - dr6: {:?} - exception: {:?} - ", - rip, - dr6, - exception, - ); - - Ok(VcpuStopReason::Unknown) -} +#[cfg(target_arch = "aarch64")] +mod aarch64; +#[cfg(target_arch = "x86_64")] +mod x86_64; + +#[cfg(target_arch = "aarch64")] +pub(crate) use aarch64::*; +#[cfg(target_arch = "x86_64")] +pub(crate) use x86_64::*; diff --git a/src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs b/src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs new file mode 100644 index 000000000..abab066dd --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs @@ -0,0 +1,183 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 The Hyperlight Authors. + +use crate::hypervisor::regs::CommonRegisters; + +pub(crate) const SW_BP_SIZE: usize = 4; +pub(crate) const SW_BP_IMMEDIATE: u16 = 0x4859; +const HVC_BASE: u32 = 0xd400_0002; +const SW_BP_INSTRUCTION: u32 = HVC_BASE | ((SW_BP_IMMEDIATE as u32) << 5); +pub(crate) const SW_BP: [u8; SW_BP_SIZE] = SW_BP_INSTRUCTION.to_le_bytes(); + +pub(crate) fn valid_sw_breakpoint_address(addr: u64) -> bool { + addr.is_multiple_of(SW_BP_SIZE as u64) +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum SoftwareStepError { + #[error("Software single-step does not support instruction {instruction:#010x} at {pc:#x}")] + UnsupportedInstruction { pc: u64, instruction: u32 }, + #[error("Software single-step target overflow for instruction at {0:#x}")] + TargetOverflow(u64), + #[error("Software single-step target is the current PC {0:#x}")] + SelfLoop(u64), + #[error("Software single-step branch register X{0} is invalid")] + InvalidBranchRegister(u8), + #[error("Software single-step target {0:#x} is not instruction-aligned")] + UnalignedTarget(u64), +} + +fn sign_extend(value: u32, bits: u32) -> i64 { + ((value as i64) << (64 - bits)) >> (64 - bits) +} + +fn relative_target(pc: u64, immediate: u32, bits: u32) -> Result { + let offset = sign_extend(immediate, bits) << 2; + pc.checked_add_signed(offset) + .ok_or(SoftwareStepError::TargetOverflow(pc)) +} + +fn branch_register(instruction: u32, regs: &CommonRegisters) -> Result { + let register = ((instruction >> 5) & 0x1f) as u8; + regs.x + .get(register as usize) + .copied() + .ok_or(SoftwareStepError::InvalidBranchRegister(register)) +} + +pub(crate) fn software_step_targets( + pc: u64, + instruction: u32, + regs: &CommonRegisters, +) -> Result, SoftwareStepError> { + let fallthrough = pc + .checked_add(SW_BP_SIZE as u64) + .ok_or(SoftwareStepError::TargetOverflow(pc))?; + + let mut targets = if instruction & 0x7c00_0000 == 0x1400_0000 { + vec![relative_target(pc, instruction & 0x03ff_ffff, 26)?] + } else if instruction & 0xff00_0000 == 0x5400_0000 || instruction & 0x7e00_0000 == 0x3400_0000 { + vec![ + fallthrough, + relative_target(pc, (instruction >> 5) & 0x7ffff, 19)?, + ] + } else if instruction & 0x7e00_0000 == 0x3600_0000 { + vec![ + fallthrough, + relative_target(pc, (instruction >> 5) & 0x3fff, 14)?, + ] + } else if instruction & 0xffff_fc1f == 0xd61f_0000 + || instruction & 0xffff_fc1f == 0xd63f_0000 + || instruction & 0xffff_fc1f == 0xd65f_0000 + { + vec![branch_register(instruction, regs)?] + } else if instruction & 0xfe00_0000 == 0xd600_0000 || instruction & 0xff00_0000 == 0xd400_0000 { + return Err(SoftwareStepError::UnsupportedInstruction { pc, instruction }); + } else { + vec![fallthrough] + }; + + targets.sort_unstable(); + targets.dedup(); + if targets.contains(&pc) { + return Err(SoftwareStepError::SelfLoop(pc)); + } + if let Some(target) = targets + .iter() + .find(|&&target| !valid_sw_breakpoint_address(target)) + { + return Err(SoftwareStepError::UnalignedTarget(*target)); + } + Ok(targets) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn software_breakpoint_is_reserved_hvc() { + assert_eq!(SW_BP, [0x22, 0x0b, 0x09, 0xd4]); + } + + #[test] + fn software_breakpoints_require_instruction_alignment() { + assert!(valid_sw_breakpoint_address(0x4000)); + assert!(!valid_sw_breakpoint_address(0x4001)); + assert!(!valid_sw_breakpoint_address(0x4002)); + assert!(!valid_sw_breakpoint_address(0x4003)); + } + + #[test] + fn software_step_decodes_linear_instruction() { + assert_eq!( + software_step_targets(0x1000, 0xd503_201f, &CommonRegisters::default()).unwrap(), + vec![0x1004] + ); + } + + #[test] + fn software_step_decodes_direct_branches() { + assert_eq!( + software_step_targets(0x1000, 0x1400_0002, &CommonRegisters::default()).unwrap(), + vec![0x1008] + ); + assert_eq!( + software_step_targets(0x1000, 0x17ff_ffff, &CommonRegisters::default()).unwrap(), + vec![0xffc] + ); + assert_eq!( + software_step_targets(0x1000, 0x9400_0002, &CommonRegisters::default()).unwrap(), + vec![0x1008] + ); + } + + #[test] + fn software_step_decodes_conditional_branches() { + assert_eq!( + software_step_targets(0x1000, 0x5400_0040, &CommonRegisters::default()).unwrap(), + vec![0x1004, 0x1008] + ); + assert_eq!( + software_step_targets(0x1000, 0x5400_0050, &CommonRegisters::default()).unwrap(), + vec![0x1004, 0x1008] + ); + assert_eq!( + software_step_targets(0x1000, 0xb400_0040, &CommonRegisters::default()).unwrap(), + vec![0x1004, 0x1008] + ); + assert_eq!( + software_step_targets(0x1000, 0x3600_0040, &CommonRegisters::default()).unwrap(), + vec![0x1004, 0x1008] + ); + } + + #[test] + fn software_step_decodes_register_branches() { + let mut regs = CommonRegisters::default(); + regs.x[30] = 0x1234; + assert_eq!( + software_step_targets(0x1000, 0xd65f_03c0, ®s).unwrap(), + vec![0x1234] + ); + } + + #[test] + fn software_step_rejects_unsupported_control_flow() { + assert!(matches!( + software_step_targets(0x1000, 0xd400_0002, &CommonRegisters::default()), + Err(SoftwareStepError::UnsupportedInstruction { .. }) + )); + assert_eq!( + software_step_targets(0x1000, 0x1400_0000, &CommonRegisters::default()), + Err(SoftwareStepError::SelfLoop(0x1000)) + ); + + let mut regs = CommonRegisters::default(); + regs.x[30] = 0x1235; + assert_eq!( + software_step_targets(0x1000, 0xd65f_03c0, ®s), + Err(SoftwareStepError::UnalignedTarget(0x1235)) + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/gdb/arch/x86_64.rs b/src/hyperlight_host/src/hypervisor/gdb/arch/x86_64.rs new file mode 100644 index 000000000..d94783029 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/gdb/arch/x86_64.rs @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2025 The Hyperlight Authors. + +use super::super::{DebugError, DebuggableVm, VcpuStopReason}; +use crate::hypervisor::regs::CommonRegisters; +use crate::hypervisor::virtual_machine::RegisterError; + +#[derive(Debug, thiserror::Error)] +pub enum VcpuStopReasonError { + #[error("Failed to get registers: {0}")] + GetRegs(#[from] RegisterError), + #[error("Failed to remove hardware breakpoint: {0}")] + RemoveHwBreakpoint(#[from] DebugError), +} + +pub(crate) const DB_EX_ID: u32 = 1; +pub(crate) const BP_EX_ID: u32 = 3; +pub(crate) const SW_BP_SIZE: usize = 1; +pub(crate) const SW_BP: [u8; SW_BP_SIZE] = [0xCC]; +pub(crate) const MAX_NO_OF_HW_BP: usize = 4; + +const DR6_BS_FLAG_MASK: u64 = 1 << 14; +const DR6_HW_BP_FLAGS_MASK: u64 = 0x0F; + +pub(crate) fn valid_sw_breakpoint_address(_addr: u64) -> bool { + true +} + +pub(crate) fn vcpu_stop_reason( + vm: &dyn DebuggableVm, + dr6: u64, + exception: u32, +) -> std::result::Result { + let CommonRegisters { rip, .. } = vm.regs()?; + if DB_EX_ID == exception { + if dr6 & DR6_BS_FLAG_MASK != 0 { + return Ok(VcpuStopReason::DoneStep); + } + + if DR6_HW_BP_FLAGS_MASK & dr6 != 0 { + return Ok(VcpuStopReason::HwBp); + } + } + + if BP_EX_ID == exception { + return Ok(VcpuStopReason::SwBp); + } + + tracing::error!( + r"The vCPU exited because of an unknown reason: + rip: {:?} + dr6: {:?} + exception: {:?} + ", + rip, + dr6, + exception, + ); + + Ok(VcpuStopReason::Unknown) +} diff --git a/src/hyperlight_host/src/hypervisor/gdb/event_loop.rs b/src/hyperlight_host/src/hypervisor/gdb/event_loop.rs index 766ec008d..6249fe854 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/event_loop.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/event_loop.rs @@ -7,18 +7,22 @@ use gdbstub::stub::{ BaseStopReason, DisconnectReason, GdbStub, SingleThreadStopReason, run_blocking, }; -use super::x86_64_target::HyperlightSandboxTarget; +use super::target::HyperlightSandboxTarget; use super::{DebugResponse, GdbTargetError, VcpuStopReason}; // Signals are defined differently on Windows and Linux, so we use conditional compilation #[cfg(target_os = "linux")] mod signals { + #[cfg(target_arch = "aarch64")] + pub use libc::SIGTRAP; pub use libc::{SIGINT, SIGSEGV}; } #[cfg(windows)] mod signals { pub const SIGINT: i8 = 2; pub const SIGSEGV: i8 = 11; + #[cfg(target_arch = "aarch64")] + pub const SIGTRAP: i8 = 5; } struct GdbBlockingEventLoop; @@ -47,7 +51,13 @@ impl run_blocking::BlockingEventLoop for GdbBlockingEventLoop { let stop_response = match stop_reason { VcpuStopReason::DoneStep => BaseStopReason::DoneStep, VcpuStopReason::SwBp => BaseStopReason::SwBreak(()), + #[cfg(target_arch = "x86_64")] VcpuStopReason::HwBp => BaseStopReason::HwBreak(()), + #[cfg(target_arch = "aarch64")] + VcpuStopReason::Initial => BaseStopReason::SignalWithThread { + tid: (), + signal: Signal(signals::SIGTRAP as u8), + }, // This is a consequence of the GDB client sending an interrupt signal // to the target thread VcpuStopReason::Interrupt => BaseStopReason::SignalWithThread { @@ -58,6 +68,7 @@ impl run_blocking::BlockingEventLoop for GdbBlockingEventLoop { tid: (), signal: Signal(signals::SIGSEGV as u8), }, + #[cfg(target_arch = "x86_64")] VcpuStopReason::Unknown => { tracing::warn!("Unknown stop reason received"); diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 29380a78b..f27e37207 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod arch; mod event_loop; -mod x86_64_target; +mod target; use std::io::{self, ErrorKind}; use std::net::TcpListener; @@ -15,8 +15,8 @@ use event_loop::event_loop_thread; use gdbstub::conn::ConnectionExt; use gdbstub::stub::GdbStub; use gdbstub::target::TargetError; +use target::HyperlightSandboxTarget; use thiserror::Error; -use x86_64_target::HyperlightSandboxTarget; use super::InterruptHandle; use super::regs::CommonRegisters; @@ -79,6 +79,9 @@ pub enum DebugMemoryAccessError { TranslateGuestAddress(u64), #[error("Failed to write to read-only region")] WriteToReadOnly, + #[cfg(all(target_arch = "aarch64", target_os = "windows"))] + #[error("Failed to flush the host instruction cache: {0}")] + FlushInstructionCache(String), } impl<'a> DebugMemoryView<'a> { @@ -153,16 +156,63 @@ impl<'a> DebugMemoryView<'a> { BackingSource::Dynamic(_) => Err(DebugMemoryAccessError::WriteToReadOnly), } } + + pub(crate) fn flush_host_instruction_cache( + &self, + gpa: u64, + len: usize, + ) -> std::result::Result<(), DebugMemoryAccessError> { + #[cfg(all(target_arch = "aarch64", target_os = "windows"))] + { + use std::ffi::c_void; + + use windows::Win32::System::Diagnostics::Debug::FlushInstructionCache; + use windows::Win32::System::Threading::GetCurrentProcess; + + use crate::mem::shared_mem::SharedMemory; + + let resolved = self + .mem_mgr + .layout + .resolve_gpa(gpa, &self.guest_mmap_regions) + .ok_or(DebugMemoryAccessError::TranslateGuestAddress(gpa))?; + let address = match resolved.base { + BaseGpaRegion::Snapshot(()) => self.mem_mgr.shared_mem.base_ptr(), + BaseGpaRegion::Scratch(()) => self.mem_mgr.scratch_mem.base_ptr(), + BaseGpaRegion::Mmap(region) => { + let base: usize = region.host_region.start.into(); + base as *mut u8 + } + } + .wrapping_add(resolved.offset); + + // SAFETY: The current process handle is valid and `address..address + // + len` lies inside a live sandbox memory mapping. + unsafe { + FlushInstructionCache(GetCurrentProcess(), Some(address.cast::()), len) + .map_err(|e| DebugMemoryAccessError::FlushInstructionCache(e.to_string()))?; + } + } + + #[cfg(not(all(target_arch = "aarch64", target_os = "windows")))] + let _ = (gpa, len); + + Ok(()) + } } /// Defines the possible reasons for which a vCPU can be stopped when debugging -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum VcpuStopReason { Crash, DoneStep, + #[cfg(target_arch = "x86_64")] HwBp, + #[cfg(target_arch = "aarch64")] + Initial, SwBp, Interrupt, + #[cfg(target_arch = "x86_64")] Unknown, } @@ -218,6 +268,10 @@ pub enum DebugError { Register(#[from] RegisterError), #[error("Maximum hardware breakpoints ({0}) exceeded")] TooManyHwBreakpoints(usize), + #[error("Debug capability is not supported: {0}")] + Unsupported(&'static str), + #[error("Instruction cache synchronization failed: {0}")] + InstructionCacheSync(String), #[error("Translation of guest virtual address failed: {0}")] TranslateGva(u64), } @@ -232,14 +286,37 @@ pub(crate) trait DebuggableVm: VirtualMachine { fn set_debug(&mut self, enable: bool) -> std::result::Result<(), DebugError>; /// Enable/disable single stepping - fn set_single_step(&mut self, enable: bool) -> std::result::Result<(), DebugError>; + #[cfg(target_arch = "x86_64")] + fn set_single_step(&mut self, enable: bool) -> std::result::Result<(), DebugError> { + if enable { + Err(DebugError::Unsupported("single-step")) + } else { + Ok(()) + } + } /// Add a hardware breakpoint at the given address. /// Must be idempotent. - fn add_hw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError>; + fn add_hw_breakpoint(&mut self, _addr: u64) -> std::result::Result<(), DebugError> { + Err(DebugError::Unsupported("hardware breakpoints")) + } /// Remove a hardware breakpoint at the given address - fn remove_hw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError>; + fn remove_hw_breakpoint(&mut self, _addr: u64) -> std::result::Result<(), DebugError> { + Err(DebugError::Unsupported("hardware breakpoints")) + } + + fn sync_instruction_cache(&mut self) -> std::result::Result<(), DebugError> { + Ok(()) + } + + fn register_sw_breakpoint(&mut self, _addr: u64) -> std::result::Result<(), DebugError> { + Ok(()) + } + + fn unregister_sw_breakpoint(&mut self, _addr: u64) -> std::result::Result<(), DebugError> { + Ok(()) + } } /// Debug communication channel that is used for sending a request type and @@ -307,7 +384,7 @@ pub(crate) fn create_gdb_thread( let mut target = HyperlightSandboxTarget::new(hyp_conn); - // Waits for vCPU to stop at entrypoint breakpoint + // Wait for the vCPU interrupt handle. let msg = target.recv()?; if let DebugResponse::InterruptHandle(handle) = msg { tracing::info!("Received interrupt handle: {:?}", handle); @@ -316,7 +393,7 @@ pub(crate) fn create_gdb_thread( return Err(GdbTargetError::UnexpectedMessage); } - // Waits for vCPU to stop at entrypoint breakpoint + // Wait for the architecture-specific initial stop. let msg = target.recv()?; if let DebugResponse::VcpuStopped(_) = msg { event_loop_thread(debugger, &mut target); diff --git a/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs b/src/hyperlight_host/src/hypervisor/gdb/target.rs similarity index 79% rename from src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs rename to src/hyperlight_host/src/hypervisor/gdb/target.rs index e67401f0d..9295cc911 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/target.rs @@ -11,11 +11,16 @@ use gdbstub::target::ext::base::singlethread::{ SingleThreadBase, SingleThreadResume, SingleThreadResumeOps, SingleThreadSingleStep, SingleThreadSingleStepOps, }; +#[cfg(target_arch = "x86_64")] +use gdbstub::target::ext::breakpoints::HwBreakpointOps; use gdbstub::target::ext::breakpoints::{ - Breakpoints, BreakpointsOps, HwBreakpoint, HwBreakpointOps, SwBreakpoint, SwBreakpointOps, + Breakpoints, BreakpointsOps, HwBreakpoint, SwBreakpoint, SwBreakpointOps, }; use gdbstub::target::ext::section_offsets::{Offsets, SectionOffsets}; use gdbstub::target::{Target, TargetError, TargetResult}; +#[cfg(target_arch = "aarch64")] +use gdbstub_arch::aarch64::AArch64 as GdbTargetArch; +#[cfg(target_arch = "x86_64")] use gdbstub_arch::x86::X86_64_SSE as GdbTargetArch; use super::{DebugCommChannel, DebugMsg, DebugResponse, GdbTargetError}; @@ -69,9 +74,7 @@ impl HyperlightSandboxTarget { match self.send_command(DebugMsg::Continue)? { DebugResponse::Continue => Ok(()), DebugResponse::NotAllowed => { - tracing::error!("Action not allowed at this time, crash might have occurred"); - // This is a consequence of the target crashing or being in an invalid state - // we cannot continue execution, but we can still read registers and memory + tracing::error!("Resume is not allowed in the current target state"); Ok(()) } msg => { @@ -108,7 +111,8 @@ impl HyperlightSandboxTarget { /// Interrupts the vCPU execution pub(crate) fn interrupt_vcpu(&mut self) -> bool { if let Some(handle) = &self.interrupt_handle { - handle.kill_from_debugger() + handle.kill_from_debugger(); + true } else { tracing::warn!("No interrupt handle set, cannot interrupt vCPU"); @@ -198,27 +202,40 @@ impl SingleThreadBase for HyperlightSandboxTarget { match self.send_command(DebugMsg::ReadRegisters)? { DebugResponse::ReadRegisters(boxed_regs) => { let (read_regs, read_fpu) = boxed_regs.as_ref(); - regs.regs[0] = read_regs.rax; - regs.regs[1] = read_regs.rbp; - regs.regs[2] = read_regs.rcx; - regs.regs[3] = read_regs.rdx; - regs.regs[4] = read_regs.rsi; - regs.regs[5] = read_regs.rdi; - regs.regs[6] = read_regs.rbp; - regs.regs[7] = read_regs.rsp; - regs.regs[8] = read_regs.r8; - regs.regs[9] = read_regs.r9; - regs.regs[10] = read_regs.r10; - regs.regs[11] = read_regs.r11; - regs.regs[12] = read_regs.r12; - regs.regs[13] = read_regs.r13; - regs.regs[14] = read_regs.r14; - regs.regs[15] = read_regs.r15; - regs.rip = read_regs.rip; - regs.eflags = read_regs.rflags as u32; - - regs.xmm = read_fpu.xmm.map(u128::from_le_bytes); - regs.mxcsr = read_fpu.mxcsr; + #[cfg(target_arch = "x86_64")] + { + regs.regs[0] = read_regs.rax; + regs.regs[1] = read_regs.rbp; + regs.regs[2] = read_regs.rcx; + regs.regs[3] = read_regs.rdx; + regs.regs[4] = read_regs.rsi; + regs.regs[5] = read_regs.rdi; + regs.regs[6] = read_regs.rbp; + regs.regs[7] = read_regs.rsp; + regs.regs[8] = read_regs.r8; + regs.regs[9] = read_regs.r9; + regs.regs[10] = read_regs.r10; + regs.regs[11] = read_regs.r11; + regs.regs[12] = read_regs.r12; + regs.regs[13] = read_regs.r13; + regs.regs[14] = read_regs.r14; + regs.regs[15] = read_regs.r15; + regs.rip = read_regs.rip; + regs.eflags = read_regs.rflags as u32; + + regs.xmm = read_fpu.xmm.map(u128::from_le_bytes); + regs.mxcsr = read_fpu.mxcsr; + } + #[cfg(target_arch = "aarch64")] + { + regs.x = read_regs.x; + regs.sp = read_regs.sp; + regs.pc = read_regs.pc; + regs.cpsr = read_regs.pstate as u32; + regs.v = read_fpu.v; + regs.fpcr = read_fpu.fpcr; + regs.fpsr = read_fpu.fpsr; + } Ok(()) } @@ -240,6 +257,7 @@ impl SingleThreadBase for HyperlightSandboxTarget { ) -> TargetResult<(), Self> { tracing::debug!("Write regs"); + #[cfg(target_arch = "x86_64")] let common_regs = CommonRegisters { rax: regs.regs[0], rbx: regs.regs[1], @@ -261,17 +279,35 @@ impl SingleThreadBase for HyperlightSandboxTarget { rflags: u64::from(regs.eflags), }; + #[cfg(target_arch = "x86_64")] let mut xmm = [[0u8; 16]; 16]; + #[cfg(target_arch = "x86_64")] for (i, ®) in regs.xmm.iter().enumerate() { xmm[i] = reg.to_le_bytes(); } + #[cfg(target_arch = "x86_64")] let common_fpu = CommonFpu { xmm, mxcsr: regs.mxcsr, ..Default::default() }; + #[cfg(target_arch = "aarch64")] + let common_regs = CommonRegisters { + x: regs.x, + sp: regs.sp, + pc: regs.pc, + pstate: u64::from(regs.cpsr), + }; + + #[cfg(target_arch = "aarch64")] + let common_fpu = CommonFpu { + v: regs.v, + fpcr: regs.fpcr, + fpsr: regs.fpsr, + }; + match self.send_command(DebugMsg::WriteRegisters(Box::new(( common_regs, common_fpu, @@ -321,6 +357,7 @@ impl SectionOffsets for HyperlightSandboxTarget { } impl Breakpoints for HyperlightSandboxTarget { + #[cfg(target_arch = "x86_64")] fn support_hw_breakpoint(&mut self) -> Option> { Some(self) } @@ -476,6 +513,9 @@ impl SingleThreadSingleStep for HyperlightSandboxTarget { #[cfg(test)] mod tests { + #[cfg(target_arch = "aarch64")] + use gdbstub_arch::aarch64::reg::AArch64CoreRegs; + #[cfg(target_arch = "x86_64")] use gdbstub_arch::x86::reg::X86_64CoreRegs; use super::*; @@ -492,6 +532,9 @@ mod tests { let res = gdb_conn.send(msg); assert!(res.is_ok()); + #[cfg(target_arch = "aarch64")] + let mut regs = AArch64CoreRegs::default(); + #[cfg(target_arch = "x86_64")] let mut regs = X86_64CoreRegs::default(); assert!( target.read_registers(&mut regs).is_ok(), @@ -515,4 +558,51 @@ mod tests { expected to fail" ); } + + #[cfg(target_arch = "aarch64")] + #[test] + fn aarch64_register_conversion_round_trip() { + let mut common_regs = CommonRegisters::default(); + for (index, value) in common_regs.x.iter_mut().enumerate() { + *value = 0x1000 + index as u64; + } + common_regs.sp = 0x2000; + common_regs.pc = 0x3000; + common_regs.pstate = 0xa000_03c5; + + let mut common_fpu = CommonFpu::default(); + for (index, value) in common_fpu.v.iter_mut().enumerate() { + *value = 0x4000 + index as u128; + } + common_fpu.fpcr = 0x5000; + common_fpu.fpsr = 0x6000; + + let (gdb_conn, hyp_conn) = DebugCommChannel::unbounded(); + let mut target = HyperlightSandboxTarget::new(hyp_conn); + gdb_conn + .send(DebugResponse::ReadRegisters(Box::new(( + common_regs, + common_fpu, + )))) + .unwrap(); + + let mut gdb_regs = AArch64CoreRegs::default(); + assert!(target.read_registers(&mut gdb_regs).is_ok()); + assert_eq!(gdb_regs.x, common_regs.x); + assert_eq!(gdb_regs.sp, common_regs.sp); + assert_eq!(gdb_regs.pc, common_regs.pc); + assert_eq!(gdb_regs.cpsr, common_regs.pstate as u32); + assert_eq!(gdb_regs.v, common_fpu.v); + assert_eq!(gdb_regs.fpcr, common_fpu.fpcr); + assert_eq!(gdb_regs.fpsr, common_fpu.fpsr); + assert!(matches!(gdb_conn.recv().unwrap(), DebugMsg::ReadRegisters)); + + gdb_conn.send(DebugResponse::WriteRegisters).unwrap(); + assert!(target.write_registers(&gdb_regs).is_ok()); + let DebugMsg::WriteRegisters(written) = gdb_conn.recv().unwrap() else { + panic!("unexpected request"); + }; + assert_eq!(written.0, common_regs); + assert_eq!(written.1, common_fpu); + } } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 9f527987e..b6b9d3f10 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -5,6 +5,8 @@ use std::sync::Arc; +#[cfg(gdb)] +use super::SoftwareBreakpoints; use super::{ AccessPageTableError, CreateHyperlightVmError, DispatchGuestCallError, HyperlightVm, InitializeError, @@ -17,9 +19,11 @@ use crate::hypervisor::LinuxInterruptHandle; #[cfg(target_os = "windows")] use crate::hypervisor::WindowsInterruptHandle; #[cfg(gdb)] -use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse}; +use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm}; use crate::hypervisor::hyperlight_vm::get_guest_log_filter; use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +#[cfg(not(gdb))] +use crate::hypervisor::virtual_machine::VirtualMachine; #[cfg(hvf)] use crate::hypervisor::virtual_machine::hvf::HvfVm; #[cfg(kvm)] @@ -27,8 +31,7 @@ use crate::hypervisor::virtual_machine::kvm::KvmVm; #[cfg(target_os = "windows")] use crate::hypervisor::virtual_machine::whp::WhpVm; use crate::hypervisor::virtual_machine::{ - HypervisorType, RegisterError, ResetVcpuError, VirtualMachine, VmError, - get_available_hypervisor, + HypervisorType, RegisterError, ResetVcpuError, VmError, get_available_hypervisor, }; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; @@ -49,10 +52,12 @@ impl HyperlightVm { rsp_gva: u64, page_size: usize, #[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration, - #[cfg(gdb)] _gdb_conn: Option>, + #[cfg(gdb)] gdb_conn: Option>, #[cfg(feature = "mem_profile")] _trace_info: MemTraceInfo, ) -> std::result::Result { - // TODO: support gdb on aarch64 + #[cfg(gdb)] + type VmType = Box; + #[cfg(not(gdb))] type VmType = Box; #[cfg(hvf)] let interrupt_handle: Arc = @@ -101,9 +106,26 @@ impl HyperlightVm { vm_can_reset_vcpu, pending_tlb_flush: false, + + #[cfg(gdb)] + gdb_conn, + #[cfg(gdb)] + sw_breakpoints: SoftwareBreakpoints::default(), + #[cfg(gdb)] + pending_software_step: None, + #[cfg(gdb)] + initial_debug_stop_pending: false, }; ret.update_snapshot_mapping(snapshot_mem)?; ret.update_scratch_mapping(scratch_mem)?; + + #[cfg(gdb)] + if ret.gdb_conn.is_some() { + ret.send_dbg_msg(DebugResponse::InterruptHandle(ret.interrupt_handle.clone()))?; + ret.vm.set_debug(true).map_err(VmError::Debug)?; + ret.initial_debug_stop_pending = true; + } + Ok(ret) } @@ -133,6 +155,10 @@ impl HyperlightVm { }; self.vm.set_regs(®s)?; + #[cfg(gdb)] + self.handle_initial_debug_stop(mem_mgr) + .map_err(super::RunVmError::DebugHandler)?; + self.run(mem_mgr, host_funcs) .map_err(InitializeError::Run)?; @@ -170,6 +196,9 @@ impl HyperlightVm { self.vm .set_fpu(&CommonFpu::default()) .map_err(DispatchGuestCallError::SetupRegs)?; + #[cfg(gdb)] + self.handle_initial_debug_stop(mem_mgr) + .map_err(super::RunVmError::DebugHandler)?; let result = self .run(mem_mgr, host_funcs) .map_err(DispatchGuestCallError::Run); diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs new file mode 100644 index 000000000..abc2f26c6 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs @@ -0,0 +1,632 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 The Hyperlight Authors. + +use std::collections::HashMap; + +use super::{ + HandleDebugError, HyperlightVm, RecvDbgMsgError, SendDbgMsgError, VcpuStopReason, VmError, +}; +use crate::hypervisor::gdb::arch::{SW_BP, SW_BP_SIZE, valid_sw_breakpoint_address}; +#[cfg(target_arch = "aarch64")] +use crate::hypervisor::gdb::arch::{SoftwareStepError, software_step_targets}; +use crate::hypervisor::gdb::{ + DebugError, DebugMemoryAccessError, DebugMemoryView, DebugMsg, DebugResponse, +}; + +#[derive(Debug, Default)] +pub(crate) struct SoftwareBreakpoints(HashMap>); + +impl SoftwareBreakpoints { + pub(super) fn contains(&self, gva: u64) -> bool { + self.0.contains_key(&gva) + } + + pub(super) fn get(&self, gva: u64) -> Option<&[u8]> { + self.0.get(&gva).map(Vec::as_slice) + } + + pub(super) fn insert(&mut self, gva: u64, instruction: Vec) { + self.0.insert(gva, instruction); + } + + pub(super) fn remove(&mut self, gva: u64) { + self.0.remove(&gva); + } + + fn addresses(&self) -> Vec { + self.0.keys().copied().collect() + } +} + +#[cfg(target_arch = "aarch64")] +#[derive(Clone, Debug)] +pub(super) struct PendingSoftwareStep { + origin: u64, + reinsert_origin: bool, + targets: Vec, + temporary_breakpoints: HashMap>, + report_stop: bool, +} + +#[derive(Debug, thiserror::Error)] +pub enum ProcessDebugRequestError { + #[error("Debug is not enabled")] + DebugNotEnabled, + #[error("VM operation error: {0}")] + Vm(#[from] VmError), + #[error("Debug operation error: {0}")] + Debug(#[from] DebugError), + #[error("Software breakpoint address {address:#x} is not {alignment}-byte aligned")] + UnalignedSwBreakpoint { address: u64, alignment: usize }, + #[error("Address {0:#x} is not a software breakpoint")] + SwBreakpointNotFound(u64), + #[cfg(target_arch = "aarch64")] + #[error("Failed to prepare ARM64 software single-step: {0}")] + SoftwareStep(#[from] SoftwareStepError), + #[error("Failed to read memory: {0}")] + ReadMemory(#[from] DebugMemoryAccessError), + #[error("Failed to write memory: {0}")] + WriteMemory(DebugMemoryAccessError), +} + +impl HyperlightVm { + #[cfg(target_arch = "aarch64")] + pub(crate) fn has_sw_breakpoints(&self) -> bool { + !self.sw_breakpoints.0.is_empty() + } + + pub(super) fn handle_debug( + &mut self, + mem_mgr: &crate::mem::mgr::SandboxMemoryManager, + stop_reason: VcpuStopReason, + ) -> std::result::Result<(), HandleDebugError> { + if self.gdb_conn.is_none() { + return Err(HandleDebugError::DebugNotEnabled); + } + + let mem_access = + DebugMemoryView::new(mem_mgr, self.get_mapped_regions().cloned().collect()); + + match stop_reason { + VcpuStopReason::Crash => { + self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason))?; + + loop { + tracing::debug!("Debug wait for event to resume vCPU"); + let req = self.recv_dbg_msg()?; + let mut deny_continue = false; + let mut detach = false; + + let response = match req { + DebugMsg::DisableDebug => { + detach = true; + DebugResponse::DisableDebug + } + DebugMsg::Continue => { + deny_continue = true; + DebugResponse::NotAllowed + } + DebugMsg::Step => { + deny_continue = true; + DebugResponse::NotAllowed + } + DebugMsg::AddHwBreakpoint(_) + | DebugMsg::AddSwBreakpoint(_) + | DebugMsg::RemoveHwBreakpoint(_) + | DebugMsg::RemoveSwBreakpoint(_) + | DebugMsg::WriteAddr(_, _) + | DebugMsg::WriteRegisters(_) => DebugResponse::NotAllowed, + _ => match self.process_dbg_request(req, &mem_access) { + Ok(response) => response, + Err(ProcessDebugRequestError::ReadMemory( + DebugMemoryAccessError::TranslateGuestAddress(_), + )) + | Err(ProcessDebugRequestError::Debug(DebugError::TranslateGva(_))) => { + DebugResponse::ErrorOccurred + } + #[cfg(target_arch = "aarch64")] + Err(error @ ProcessDebugRequestError::SoftwareStep(_)) => { + tracing::error!("{error}"); + DebugResponse::NotAllowed + } + Err(e) => { + tracing::error!("Error processing debug request: {:?}", e); + return Err(HandleDebugError::ProcessRequest(e)); + } + }, + }; + + self.send_dbg_msg(response)?; + + if deny_continue { + self.send_dbg_msg(DebugResponse::VcpuStopped(VcpuStopReason::Crash))?; + } + + if detach { + break; + } + } + } + _ => { + self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason))?; + + loop { + tracing::debug!("Debug wait for event to resume vCPU"); + let req = self.recv_dbg_msg()?; + let resume_requested = matches!(&req, DebugMsg::Continue); + let resume_requested = resume_requested || matches!(&req, DebugMsg::Step); + let disable_requested = matches!(&req, DebugMsg::DisableDebug); + + let response = match self.process_dbg_request(req, &mem_access) { + Ok(response) => response, + Err(ProcessDebugRequestError::ReadMemory( + DebugMemoryAccessError::TranslateGuestAddress(_), + )) + | Err(ProcessDebugRequestError::Debug(DebugError::TranslateGva(_))) => { + DebugResponse::ErrorOccurred + } + #[cfg(target_arch = "aarch64")] + Err(error) if resume_requested => { + tracing::error!("{error}"); + DebugResponse::NotAllowed + } + Err(error) if disable_requested => { + tracing::error!("{error}"); + DebugResponse::ErrorOccurred + } + Err(e) => return Err(HandleDebugError::ProcessRequest(e)), + }; + + let resume = matches!( + response, + DebugResponse::Continue | DebugResponse::DisableDebug + ); + let resume = resume || matches!(response, DebugResponse::Step); + let denied_resume = + resume_requested && matches!(response, DebugResponse::NotAllowed); + self.send_dbg_msg(response)?; + + if denied_resume { + self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason))?; + } + if resume { + break; + } + } + } + } + + Ok(()) + } + + #[cfg(target_arch = "aarch64")] + pub(super) fn handle_initial_debug_stop( + &mut self, + mem_mgr: &crate::mem::mgr::SandboxMemoryManager, + ) -> std::result::Result<(), HandleDebugError> { + if self.initial_debug_stop_pending { + self.initial_debug_stop_pending = false; + self.handle_debug(mem_mgr, VcpuStopReason::Initial)?; + } + Ok(()) + } + + pub(crate) fn process_dbg_request( + &mut self, + req: DebugMsg, + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result { + if self.gdb_conn.is_none() { + return Err(ProcessDebugRequestError::DebugNotEnabled); + } + + match req { + DebugMsg::AddHwBreakpoint(addr) => Ok(DebugResponse::AddHwBreakpoint( + self.vm + .add_hw_breakpoint(addr) + .inspect_err(|e| tracing::error!("Failed to add hw breakpoint: {:?}", e)) + .is_ok(), + )), + DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( + self.add_sw_breakpoint(addr, mem_access) + .inspect_err(|e| tracing::error!("Failed to add sw breakpoint: {:?}", e)) + .is_ok(), + )), + DebugMsg::Continue => { + #[cfg(target_arch = "aarch64")] + { + let pc = self.vm.regs().map_err(VmError::Register)?.pc; + if self.sw_breakpoints.contains(pc) { + self.begin_software_step(false, mem_access)?; + } + } + #[cfg(target_arch = "x86_64")] + self.vm + .set_single_step(false) + .inspect_err(|e| tracing::error!("Failed to continue execution: {:?}", e))?; + Ok(DebugResponse::Continue) + } + DebugMsg::DisableDebug => { + #[cfg(target_arch = "aarch64")] + self.cancel_software_step(mem_access)?; + for address in self.sw_breakpoints.addresses() { + self.remove_sw_breakpoint(address, mem_access)?; + } + self.vm + .set_debug(false) + .inspect_err(|e| tracing::error!("Failed to disable debugging: {:?}", e))?; + Ok(DebugResponse::DisableDebug) + } + DebugMsg::GetCodeSectionOffset => Ok(DebugResponse::GetCodeSectionOffset( + mem_access.code_section_offset(), + )), + DebugMsg::ReadAddr(addr, len) => { + let mut data = vec![0u8; len]; + self.read_addrs(addr, &mut data, mem_access) + .inspect_err(|e| tracing::error!("Failed to read from address: {:?}", e))?; + Ok(DebugResponse::ReadAddr(data)) + } + DebugMsg::ReadRegisters => { + let regs = self.vm.regs().map_err(VmError::Register)?; + let fpu = self.vm.fpu().map_err(VmError::Register)?; + Ok(DebugResponse::ReadRegisters(Box::new((regs, fpu)))) + } + DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( + self.vm + .remove_hw_breakpoint(addr) + .inspect_err(|e| tracing::error!("Failed to remove hw breakpoint: {:?}", e)) + .is_ok(), + )), + DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( + self.remove_sw_breakpoint(addr, mem_access) + .inspect_err(|e| tracing::error!("Failed to remove sw breakpoint: {:?}", e)) + .is_ok(), + )), + DebugMsg::Step => { + #[cfg(target_arch = "aarch64")] + self.begin_software_step(true, mem_access)?; + #[cfg(target_arch = "x86_64")] + self.vm.set_single_step(true).inspect_err(|e| { + tracing::error!("Failed to enable step instruction: {:?}", e) + })?; + Ok(DebugResponse::Step) + } + DebugMsg::WriteAddr(addr, data) => { + self.write_addrs(addr, &data, mem_access) + .inspect_err(|e| tracing::error!("Failed to write to address: {:?}", e))?; + Ok(DebugResponse::WriteAddr) + } + DebugMsg::WriteRegisters(boxed_regs) => { + let (regs, fpu) = boxed_regs.as_ref(); + self.vm.set_regs(regs).map_err(VmError::Register)?; + self.vm.set_fpu(fpu).map_err(VmError::Register)?; + Ok(DebugResponse::WriteRegisters) + } + } + } + + pub(crate) fn recv_dbg_msg(&mut self) -> std::result::Result { + let gdb_conn = self + .gdb_conn + .as_mut() + .ok_or(RecvDbgMsgError::DebugNotEnabled)?; + Ok(gdb_conn.recv()?) + } + + pub(crate) fn send_dbg_msg( + &mut self, + cmd: DebugResponse, + ) -> std::result::Result<(), SendDbgMsgError> { + tracing::debug!("Sending {:?}", cmd); + let gdb_conn = self + .gdb_conn + .as_mut() + .ok_or(SendDbgMsgError::DebugNotEnabled)?; + Ok(gdb_conn.send(cmd)?) + } + + fn read_addrs( + &mut self, + mut gva: u64, + mut data: &mut [u8], + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result<(), ProcessDebugRequestError> { + tracing::debug!("Read addr: {:X} len: {:X}", gva, data.len()); + + while !data.is_empty() { + let gpa = self.vm.translate_gva(gva)?; + let read_len = std::cmp::min( + data.len(), + page_size::get() - (gpa as usize & (page_size::get() - 1)), + ); + mem_access.read(&mut data[..read_len], gpa)?; + data = &mut data[read_len..]; + gva += read_len as u64; + } + + Ok(()) + } + + fn write_addrs( + &mut self, + mut gva: u64, + mut data: &[u8], + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result<(), ProcessDebugRequestError> { + tracing::debug!("Write addr: {:X} len: {:X}", gva, data.len()); + + while !data.is_empty() { + let gpa = self.vm.translate_gva(gva)?; + let write_len = std::cmp::min( + data.len(), + page_size::get() - (gpa as usize & (page_size::get() - 1)), + ); + mem_access + .write(&data[..write_len], gpa) + .map_err(ProcessDebugRequestError::WriteMemory)?; + mem_access + .flush_host_instruction_cache(gpa, write_len) + .map_err(ProcessDebugRequestError::WriteMemory)?; + data = &data[write_len..]; + gva += write_len as u64; + } + + self.vm.sync_instruction_cache()?; + Ok(()) + } + + fn add_sw_breakpoint( + &mut self, + gva: u64, + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result<(), ProcessDebugRequestError> { + if !valid_sw_breakpoint_address(gva) { + return Err(ProcessDebugRequestError::UnalignedSwBreakpoint { + address: gva, + alignment: SW_BP_SIZE, + }); + } + if self.sw_breakpoints.contains(gva) { + return Ok(()); + } + + let mut saved_instruction = vec![0; SW_BP_SIZE]; + self.read_addrs(gva, &mut saved_instruction, mem_access)?; + if let Err(error) = self + .write_addrs(gva, &SW_BP, mem_access) + .and_then(|()| self.vm.register_sw_breakpoint(gva).map_err(Into::into)) + { + if let Err(rollback_error) = self.write_addrs(gva, &saved_instruction, mem_access) { + tracing::error!( + "Failed to roll back software breakpoint {gva:#x}: {rollback_error}" + ); + } + self.vm.unregister_sw_breakpoint(gva)?; + return Err(error); + } + self.sw_breakpoints.insert(gva, saved_instruction); + Ok(()) + } + + fn remove_sw_breakpoint( + &mut self, + gva: u64, + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result<(), ProcessDebugRequestError> { + let saved_instruction = self + .sw_breakpoints + .get(gva) + .ok_or(ProcessDebugRequestError::SwBreakpointNotFound(gva))? + .to_vec(); + if let Err(error) = self + .write_addrs(gva, &saved_instruction, mem_access) + .and_then(|()| self.vm.unregister_sw_breakpoint(gva).map_err(Into::into)) + { + if let Err(rollback_error) = self.write_addrs(gva, &SW_BP, mem_access) { + tracing::error!( + "Failed to roll back software breakpoint removal at {gva:#x}: {rollback_error}" + ); + } + self.vm.register_sw_breakpoint(gva)?; + return Err(error); + } + self.sw_breakpoints.remove(gva); + Ok(()) + } + + #[cfg(target_arch = "aarch64")] + fn begin_software_step( + &mut self, + report_stop: bool, + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result<(), ProcessDebugRequestError> { + if self.pending_software_step.is_some() { + self.cancel_software_step(mem_access)?; + } + + let regs = self.vm.regs().map_err(VmError::Register)?; + let pc = regs.pc; + let saved_instruction = if let Some(instruction) = self.sw_breakpoints.get(pc) { + instruction.to_vec() + } else { + let mut instruction = vec![0; SW_BP_SIZE]; + self.read_addrs(pc, &mut instruction, mem_access)?; + instruction + }; + let instruction = u32::from_le_bytes( + saved_instruction + .as_slice() + .try_into() + .expect("ARM64 instructions are four bytes"), + ); + let targets = software_step_targets(pc, instruction, ®s)?; + + let mut temporary_breakpoints = HashMap::new(); + for &target in &targets { + if self.sw_breakpoints.contains(target) { + continue; + } + let mut target_instruction = vec![0; SW_BP_SIZE]; + self.read_addrs(target, &mut target_instruction, mem_access)?; + temporary_breakpoints.insert(target, target_instruction); + } + + let mut installed_targets = Vec::new(); + for &target in temporary_breakpoints.keys() { + let result = self + .write_addrs(target, &SW_BP, mem_access) + .and_then(|()| self.vm.register_sw_breakpoint(target).map_err(Into::into)); + if let Err(error) = result { + installed_targets.push(target); + self.rollback_temporary_breakpoints( + &installed_targets, + &temporary_breakpoints, + mem_access, + ); + return Err(error); + } + installed_targets.push(target); + } + + let reinsert_origin = self.sw_breakpoints.contains(pc); + if reinsert_origin { + let result = self + .write_addrs(pc, &saved_instruction, mem_access) + .and_then(|()| self.vm.unregister_sw_breakpoint(pc).map_err(Into::into)); + if let Err(error) = result { + if let Err(rollback_error) = self.write_addrs(pc, &SW_BP, mem_access) { + tracing::error!( + "Failed to roll back software-step origin {pc:#x}: {rollback_error}" + ); + } + if let Err(rollback_error) = self.vm.register_sw_breakpoint(pc) { + tracing::error!( + "Failed to re-register software-step origin {pc:#x}: {rollback_error}" + ); + } + self.rollback_temporary_breakpoints( + &installed_targets, + &temporary_breakpoints, + mem_access, + ); + return Err(error); + } + } + + self.pending_software_step = Some(PendingSoftwareStep { + origin: pc, + reinsert_origin, + targets, + temporary_breakpoints, + report_stop, + }); + Ok(()) + } + + #[cfg(target_arch = "aarch64")] + fn rollback_temporary_breakpoints( + &mut self, + targets: &[u64], + temporary_breakpoints: &HashMap>, + mem_access: &DebugMemoryView<'_>, + ) { + for &target in targets { + if let Some(instruction) = temporary_breakpoints.get(&target) { + if let Err(error) = self.write_addrs(target, instruction, mem_access) { + tracing::error!( + "Failed to roll back temporary breakpoint {target:#x}: {error}" + ); + } + if let Err(error) = self.vm.unregister_sw_breakpoint(target) { + tracing::error!( + "Failed to unregister temporary breakpoint {target:#x}: {error}" + ); + } + } + } + } + + #[cfg(target_arch = "aarch64")] + fn cancel_software_step( + &mut self, + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result<(), ProcessDebugRequestError> { + let Some(pending) = self.pending_software_step.clone() else { + return Ok(()); + }; + self.restore_software_step(&pending, mem_access)?; + self.pending_software_step = None; + Ok(()) + } + + #[cfg(target_arch = "aarch64")] + fn restore_software_step( + &mut self, + pending: &PendingSoftwareStep, + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result<(), ProcessDebugRequestError> { + for (target, instruction) in &pending.temporary_breakpoints { + self.write_addrs(*target, instruction, mem_access)?; + self.vm.unregister_sw_breakpoint(*target)?; + } + if pending.reinsert_origin { + self.write_addrs(pending.origin, &SW_BP, mem_access)?; + self.vm.register_sw_breakpoint(pending.origin)?; + } + Ok(()) + } + + #[cfg(target_arch = "aarch64")] + pub(super) fn cancel_pending_software_step( + &mut self, + mem_mgr: &crate::mem::mgr::SandboxMemoryManager, + ) -> std::result::Result<(), ProcessDebugRequestError> { + let mapped_regions = self.get_mapped_regions().cloned().collect(); + let mem_access = DebugMemoryView::new(mem_mgr, mapped_regions); + self.cancel_software_step(&mem_access) + } + + #[cfg(target_arch = "aarch64")] + pub(super) fn finish_software_step( + &mut self, + mem_mgr: &crate::mem::mgr::SandboxMemoryManager, + ) -> std::result::Result, ProcessDebugRequestError> { + let pending = self + .pending_software_step + .clone() + .expect("software-step completion requires pending state"); + let pc = self.vm.regs().map_err(VmError::Register)?.pc; + if !pending.targets.contains(&pc) { + return Ok(Some(VcpuStopReason::SwBp)); + } + + let mapped_regions = self.get_mapped_regions().cloned().collect(); + let mem_access = DebugMemoryView::new(mem_mgr, mapped_regions); + self.restore_software_step(&pending, &mem_access)?; + self.pending_software_step = None; + + if self.sw_breakpoints.contains(pc) { + Ok(Some(VcpuStopReason::SwBp)) + } else if pending.report_stop { + Ok(Some(VcpuStopReason::DoneStep)) + } else { + Ok(None) + } + } +} + +#[cfg(test)] +mod tests { + use super::SoftwareBreakpoints; + + #[test] + fn software_breakpoint_storage_preserves_full_instruction() { + let mut breakpoints = SoftwareBreakpoints::default(); + breakpoints.insert(0x4000, vec![0x11, 0x22, 0x33, 0x44]); + + assert_eq!( + breakpoints.get(0x4000), + Some([0x11, 0x22, 0x33, 0x44].as_slice()) + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs index 20297eaff..a65bf6add 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -7,28 +7,29 @@ mod x86_64; #[cfg(target_arch = "aarch64")] mod aarch64; +#[cfg(gdb)] +mod debug; + #[cfg(all(test, not(gdb)))] pub(crate) mod test_support; -#[cfg(gdb)] -use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, Mutex}; use hyperlight_common::log_level::GuestLogFilter; use tracing_core::LevelFilter; +#[cfg(gdb)] +use self::debug::{ProcessDebugRequestError, SoftwareBreakpoints}; use crate::HyperlightError; #[cfg(gdb)] use crate::hypervisor::gdb::DebuggableVm; -#[cfg(gdb)] +#[cfg(all(gdb, target_arch = "x86_64"))] use crate::hypervisor::gdb::arch::VcpuStopReasonError; #[cfg(gdb)] use crate::hypervisor::gdb::{ DebugCommChannel, DebugError, DebugMsg, DebugResponse, GdbTargetError, VcpuStopReason, }; -#[cfg(gdb)] -use crate::hypervisor::hyperlight_vm::x86_64::debug::ProcessDebugRequestError; #[cfg(not(gdb))] use crate::hypervisor::virtual_machine::VirtualMachine; use crate::hypervisor::virtual_machine::{ @@ -206,7 +207,7 @@ pub enum RunVmError { RunVcpu(#[from] RunVcpuError), #[error("Unexpected VM exit: {0}")] UnexpectedVmExit(String), - #[cfg(gdb)] + #[cfg(all(gdb, target_arch = "x86_64"))] #[error("vCPU stop reason error: {0}")] VcpuStopReason(#[from] VcpuStopReasonError), } @@ -257,6 +258,9 @@ pub enum UpdateRegionError { MapMemory(#[from] MapMemoryError), #[error("VM unmap memory error: {0}")] UnmapMemory(#[from] UnmapMemoryError), + #[cfg(all(gdb, target_arch = "aarch64"))] + #[error("Cannot replace snapshot memory while software breakpoints are installed")] + ActiveSoftwareBreakpoints, } /// Errors that can occur when accessing the root page table state @@ -387,14 +391,18 @@ pub(crate) struct HyperlightVm { #[cfg(gdb)] pub(super) gdb_conn: Option>, #[cfg(gdb)] - pub(super) sw_breakpoints: HashMap, // addr -> original instruction + pub(super) sw_breakpoints: SoftwareBreakpoints, + #[cfg(all(gdb, target_arch = "aarch64"))] + pending_software_step: Option, /// One-shot hw breakpoint installed at the entry address when gdb is /// enabled, so the gdb stub gets a `VcpuStopped` to enter its event /// loop on the first vCPU run after construction. Cleared by the /// `VmExit::Debug` arm of `run` the first time a `HwBp` stop fires /// at the entry address. - #[cfg(gdb)] + #[cfg(all(gdb, target_arch = "x86_64"))] pub(super) one_shot_entry_bp: Option, + #[cfg(all(gdb, target_arch = "aarch64"))] + pub(super) initial_debug_stop_pending: bool, #[cfg(feature = "mem_profile")] pub(super) trace_info: MemTraceInfo, #[cfg(crashdump)] @@ -524,6 +532,11 @@ impl HyperlightVm { &mut self, snapshot: SnapshotMemoryBacking, ) -> Result<(), UpdateRegionError> { + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.has_sw_breakpoints() { + return Err(UpdateRegionError::ActiveSoftwareBreakpoints); + } + let target_mappings = snapshot.mappings(); for index in (0..self.snapshot_mappings.len()).rev() { @@ -680,7 +693,15 @@ impl HyperlightVm { // - Signals will not be sent match exit_reason { #[cfg(gdb)] - Ok(VmExit::Debug { dr6, exception }) => { + Ok(VmExit::Debug { + #[cfg(target_arch = "x86_64")] + dr6, + #[cfg(target_arch = "x86_64")] + exception, + #[cfg(target_arch = "aarch64")] + reason: stop_reason, + }) => { + #[cfg(target_arch = "x86_64")] // Classify the debug exit. `vcpu_stop_reason` is a // pure classifier and has no side effects on the VM. let stop_reason = crate::hypervisor::gdb::arch::vcpu_stop_reason( @@ -688,6 +709,7 @@ impl HyperlightVm { dr6, exception, )?; + #[cfg(target_arch = "x86_64")] // Remove the one-shot entry breakpoint installed by // `HyperlightVm::new` the first time it fires so it // does not interfere with later user-installed @@ -703,6 +725,16 @@ impl HyperlightVm { self.one_shot_entry_bp = None; } } + #[cfg(target_arch = "aarch64")] + let stop_reason = if self.pending_software_step.is_some() { + match self.finish_software_step(mem_mgr) { + Ok(Some(reason)) => reason, + Ok(None) => continue, + Err(e) => break Err(HandleDebugError::from(e).into()), + } + } else { + stop_reason + }; if let Err(e) = self.handle_debug(mem_mgr, stop_reason) { break Err(e.into()); } @@ -766,11 +798,18 @@ impl HyperlightVm { // If the vcpu was interrupted by a debugger, we need to handle it #[cfg(gdb)] - { + if debug_interrupted { self.interrupt_handle.state().clear_debug_interrupt(); + #[cfg(target_arch = "aarch64")] + if let Err(e) = self.cancel_pending_software_step(mem_mgr) { + break Err(HandleDebugError::from(e).into()); + } if let Err(e) = self.handle_debug(mem_mgr, VcpuStopReason::Interrupt) { break Err(e.into()); } + if !cancel_requested { + continue; + } } metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); 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 9463cea4f..520cf9e55 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2025 The Hyperlight Authors. -#[cfg(gdb)] -use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tracing::{Span, instrument}; @@ -17,11 +15,7 @@ use crate::hypervisor::WindowsInterruptHandle; #[cfg(crashdump)] use crate::hypervisor::crashdump; #[cfg(gdb)] -use crate::hypervisor::gdb::{ - DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm, VcpuStopReason, -}; -#[cfg(gdb)] -use crate::hypervisor::gdb::{DebugError, DebugMemoryAccessError}; +use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm}; use crate::hypervisor::regs::{ CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, MsrEntry, MsrResetState, }; @@ -126,7 +120,7 @@ impl HyperlightVm { #[cfg(gdb)] gdb_conn, #[cfg(gdb)] - sw_breakpoints: HashMap::new(), + sw_breakpoints: SoftwareBreakpoints::default(), #[cfg(gdb)] one_shot_entry_bp: None, #[cfg(feature = "mem_profile")] @@ -186,7 +180,7 @@ impl HyperlightVm { // We usually keep the top of the stack 16-byte // aligned. However, the ABI requirement is that the stack // be aligned _before a call instruction_, which means - // that the stack needs to actually be ≡ 8 mod 16 at the + // that the stack needs to actually be ≡ 8 mod 16 at the // first instruction (since, on x64, a call instruction // automatically pushes a return address). rsp: self.rsp_gva - 8, @@ -272,7 +266,7 @@ impl HyperlightVm { // aligned. Since the usual ABI requirement is that the // stack be aligned _before a call instruction_, one might // expect that the stack pointer here needs to actually be - // ≡ 8 mod 16 at the first instruction (since, on x64, a + // ≡ 8 mod 16 at the first instruction (since, on x64, a // call instruction automatically pushes a return // address). However, the x64 entry stub in // hyperlight_guest::arch::dispatch handles this itself, @@ -372,145 +366,6 @@ impl HyperlightVm { Ok(()) } - // Handle a debug exit - #[cfg(gdb)] - pub(super) fn handle_debug( - &mut self, - mem_mgr: &SandboxMemoryManager, - stop_reason: VcpuStopReason, - ) -> std::result::Result<(), HandleDebugError> { - use debug::ProcessDebugRequestError; - - use crate::hypervisor::gdb::DebugMemoryView; - - if self.gdb_conn.is_none() { - return Err(HandleDebugError::DebugNotEnabled); - } - - let mem_access = - DebugMemoryView::new(mem_mgr, self.get_mapped_regions().cloned().collect()); - - match stop_reason { - // If the vCPU stopped because of a crash, we need to handle it differently - // We do not want to allow resuming execution or placing breakpoints - // because the guest has crashed. - // We only allow reading registers and memory - VcpuStopReason::Crash => { - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason))?; - - loop { - tracing::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - // Flag to store if we should deny continue or step requests - let mut deny_continue = false; - // Flag to store if we should detach from the gdb session - let mut detach = false; - - let response = match req { - // Allow the detach request to disable debugging by continuing resuming - // hypervisor crash error reporting - DebugMsg::DisableDebug => { - detach = true; - DebugResponse::DisableDebug - } - // Do not allow continue or step requests - DebugMsg::Continue | DebugMsg::Step => { - deny_continue = true; - DebugResponse::NotAllowed - } - // Do not allow adding/removing breakpoints and writing to memory or registers - DebugMsg::AddHwBreakpoint(_) - | DebugMsg::AddSwBreakpoint(_) - | DebugMsg::RemoveHwBreakpoint(_) - | DebugMsg::RemoveSwBreakpoint(_) - | DebugMsg::WriteAddr(_, _) - | DebugMsg::WriteRegisters(_) => DebugResponse::NotAllowed, - - // For all other requests, we will process them normally - _ => { - let result = self.process_dbg_request(req, &mem_access); - match result { - Ok(response) => response, - // Treat non-fatal errors separately so the guest doesn't fail - Err(ProcessDebugRequestError::ReadMemory( - DebugMemoryAccessError::TranslateGuestAddress(_), - )) - | Err(ProcessDebugRequestError::Debug(DebugError::TranslateGva( - _, - ))) => DebugResponse::ErrorOccurred, - Err(e) => { - tracing::error!("Error processing debug request: {:?}", e); - return Err(HandleDebugError::ProcessRequest(e)); - } - } - } - }; - - // Send the response to the request back to gdb - self.send_dbg_msg(response)?; - - // If we are denying continue or step requests, the debugger assumes the - // execution started so we need to report a stop reason as a crash and let - // it request to read registers/memory to figure out what happened - if deny_continue { - self.send_dbg_msg(DebugResponse::VcpuStopped(VcpuStopReason::Crash))?; - } - - // If we are detaching, we will break the loop and the Hypervisor will continue - // to handle the Crash reason - if detach { - break; - } - } - } - // If the vCPU stopped because of any other reason except a crash, we can handle it - // normally - _ => { - // Send the stop reason to the gdb thread - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason))?; - - loop { - tracing::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - let result = self.process_dbg_request(req, &mem_access); - - let response = match result { - Ok(response) => response, - // Treat non-fatal errors separately so the guest doesn't fail - Err(ProcessDebugRequestError::ReadMemory( - DebugMemoryAccessError::TranslateGuestAddress(_), - )) - | Err(ProcessDebugRequestError::Debug(DebugError::TranslateGva(_))) => { - DebugResponse::ErrorOccurred - } - Err(e) => { - return Err(HandleDebugError::ProcessRequest(e)); - } - }; - - let cont = matches!( - response, - DebugResponse::Continue | DebugResponse::Step | DebugResponse::DisableDebug - ); - - self.send_dbg_msg(response)?; - - // Check if we should continue execution - // We continue if the response is one of the following: Step, Continue, or DisableDebug - if cont { - break; - } - } - } - } - - Ok(()) - } - #[cfg(crashdump)] pub(crate) fn crashdump_context( &self, @@ -586,270 +441,6 @@ impl HyperlightVm { } } -#[cfg(gdb)] -pub(super) mod debug { - use super::HyperlightVm; - use crate::hypervisor::gdb::arch::{SW_BP, SW_BP_SIZE}; - use crate::hypervisor::gdb::{ - DebugError, DebugMemoryAccessError, DebugMemoryView, DebugMsg, DebugResponse, - }; - use crate::hypervisor::virtual_machine::VmError; - - /// Errors that can occur during GDB debug request processing - #[derive(Debug, thiserror::Error)] - pub enum ProcessDebugRequestError { - #[error("Debug is not enabled")] - DebugNotEnabled, - #[error("VM operation error: {0}")] - Vm(#[from] VmError), - #[error("Debug operation error: {0}")] - Debug(#[from] DebugError), - #[error("Address {0:#x} is not a software breakpoint")] - SwBreakpointNotFound(u64), - #[error("Failed to read memory: {0}")] - ReadMemory(#[from] DebugMemoryAccessError), - #[error("Failed to write memory: {0}")] - WriteMemory(DebugMemoryAccessError), - } - - impl HyperlightVm { - pub(crate) fn process_dbg_request( - &mut self, - req: DebugMsg, - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result { - if self.gdb_conn.is_some() { - match req { - DebugMsg::AddHwBreakpoint(addr) => Ok(DebugResponse::AddHwBreakpoint( - self.vm - .add_hw_breakpoint(addr) - .map_err(|e| { - tracing::error!("Failed to add hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( - self.add_sw_breakpoint(addr, mem_access) - .map_err(|e| { - tracing::error!("Failed to add sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Continue => { - self.vm.set_single_step(false).map_err(|e| { - tracing::error!("Failed to continue execution: {:?}", e); - - e - })?; - - Ok(DebugResponse::Continue) - } - DebugMsg::DisableDebug => { - self.vm.set_debug(false).map_err(|e| { - tracing::error!("Failed to disable debugging: {:?}", e); - e - })?; - - Ok(DebugResponse::DisableDebug) - } - DebugMsg::GetCodeSectionOffset => Ok(DebugResponse::GetCodeSectionOffset( - mem_access.code_section_offset(), - )), - DebugMsg::ReadAddr(addr, len) => { - let mut data = vec![0u8; len]; - - self.read_addrs(addr, &mut data, mem_access).map_err(|e| { - tracing::error!("Failed to read from address: {:?}", e); - - e - })?; - - Ok(DebugResponse::ReadAddr(data)) - } - DebugMsg::ReadRegisters => { - let regs = self.vm.regs().map_err(VmError::Register)?; - let fpu = self.vm.fpu().map_err(VmError::Register)?; - Ok(DebugResponse::ReadRegisters(Box::new((regs, fpu)))) - } - DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( - self.vm - .remove_hw_breakpoint(addr) - .map_err(|e| { - tracing::error!("Failed to remove hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( - self.remove_sw_breakpoint(addr, mem_access) - .map_err(|e| { - tracing::error!("Failed to remove sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Step => { - self.vm.set_single_step(true).map_err(|e| { - tracing::error!("Failed to enable step instruction: {:?}", e); - - e - })?; - - Ok(DebugResponse::Step) - } - DebugMsg::WriteAddr(addr, data) => { - self.write_addrs(addr, &data, mem_access).map_err(|e| { - tracing::error!("Failed to write to address: {:?}", e); - - e - })?; - - Ok(DebugResponse::WriteAddr) - } - DebugMsg::WriteRegisters(boxed_regs) => { - let (regs, fpu) = boxed_regs.as_ref(); - self.vm.set_regs(regs).map_err(VmError::Register)?; - self.vm.set_fpu(fpu).map_err(VmError::Register)?; - - Ok(DebugResponse::WriteRegisters) - } - } - } else { - Err(ProcessDebugRequestError::DebugNotEnabled) - } - } - - pub(crate) fn recv_dbg_msg( - &mut self, - ) -> std::result::Result { - use super::RecvDbgMsgError; - - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or(RecvDbgMsgError::DebugNotEnabled)?; - - Ok(gdb_conn.recv()?) - } - - pub(crate) fn send_dbg_msg( - &mut self, - cmd: DebugResponse, - ) -> std::result::Result<(), super::SendDbgMsgError> { - use super::SendDbgMsgError; - - tracing::debug!("Sending {:?}", cmd); - - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or(SendDbgMsgError::DebugNotEnabled)?; - - Ok(gdb_conn.send(cmd)?) - } - - fn read_addrs( - &mut self, - mut gva: u64, - mut data: &mut [u8], - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result<(), ProcessDebugRequestError> { - let data_len = data.len(); - tracing::debug!("Read addr: {:X} len: {:X}", gva, data_len); - - while !data.is_empty() { - let gpa = self.vm.translate_gva(gva)?; - - let read_len = std::cmp::min( - data.len(), - page_size::get() - (gpa as usize & (page_size::get() - 1)), - ); - - mem_access.read(&mut data[..read_len], gpa)?; - - data = &mut data[read_len..]; - gva += read_len as u64; - } - - Ok(()) - } - - /// Copies the data from the provided slice to the guest memory address - /// The address is checked to be a valid guest address - fn write_addrs( - &mut self, - mut gva: u64, - mut data: &[u8], - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result<(), ProcessDebugRequestError> { - let data_len = data.len(); - tracing::debug!("Write addr: {:X} len: {:X}", gva, data_len); - - while !data.is_empty() { - let gpa = self.vm.translate_gva(gva)?; - - let write_len = std::cmp::min( - data.len(), - page_size::get() - (gpa as usize & (page_size::get() - 1)), - ); - - // Use the memory access to write to guest memory - mem_access - .write(&data[..write_len], gpa) - .map_err(ProcessDebugRequestError::WriteMemory)?; - - data = &data[write_len..]; - gva += write_len as u64; - } - - Ok(()) - } - - // Must be idempotent! - fn add_sw_breakpoint( - &mut self, - gva: u64, - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result<(), ProcessDebugRequestError> { - // Check if breakpoint already exists - if self.sw_breakpoints.contains_key(&gva) { - return Ok(()); - } - - // Write breakpoint OP code to write to guest memory - let mut save_data = [0; SW_BP_SIZE]; - self.read_addrs(gva, &mut save_data[..], mem_access)?; - self.write_addrs(gva, &SW_BP, mem_access)?; - - // Save guest memory to restore when breakpoint is removed - self.sw_breakpoints.insert(gva, save_data[0]); - - Ok(()) - } - - fn remove_sw_breakpoint( - &mut self, - gva: u64, - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result<(), ProcessDebugRequestError> { - if let Some(saved_data) = self.sw_breakpoints.remove(&gva) { - // Restore saved data to the guest's memory - self.write_addrs(gva, &[saved_data], mem_access)?; - - Ok(()) - } else { - Err(ProcessDebugRequestError::SwBreakpointNotFound(gva)) - } - } - } -} - #[cfg(test)] #[allow(clippy::needless_range_loop)] mod tests { @@ -1326,8 +917,8 @@ mod tests { /// Bytes 0-1: FCW, 2-3: FSW, 4: FTW, 5: reserved, 6-7: FOP /// Bytes 8-15: FIP, 16-23: FDP /// Bytes 24-27: MXCSR, 28-31: MXCSR_MASK (preserve - hardware defined) - /// Bytes 32-159: ST0-ST7/MM0-MM7 (8 regs × 16 bytes) - /// Bytes 160-415: XMM0-XMM15 (16 regs × 16 bytes) + /// Bytes 32-159: ST0-ST7/MM0-MM7 (8 regs × 16 bytes) + /// Bytes 160-415: XMM0-XMM15 (16 regs × 16 bytes) /// Bytes 416-511: Reserved fn dirty_xsave_legacy(xsave: &mut [u32], current_xsave: &[u8]) { // FCW (bytes 0-1) + FSW (bytes 2-3) - pack into xsave[0] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index e1922a1ec..9f7a0c86d 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -8,6 +8,8 @@ use tracing::{Span, instrument}; #[cfg(gdb)] use crate::hypervisor::gdb::DebugError; +#[cfg(all(gdb, target_arch = "aarch64"))] +use crate::hypervisor::gdb::VcpuStopReason; use crate::hypervisor::regs::{ CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, }; @@ -140,6 +142,8 @@ pub(crate) enum VmExit { dr6: u64, #[cfg(target_arch = "x86_64")] exception: u32, + #[cfg(target_arch = "aarch64")] + reason: VcpuStopReason, }, /// The vCPU has halted Halt(), @@ -193,6 +197,9 @@ pub enum CreateVmError { HypervisorNotAvailable(HypervisorError), #[error("Initialize VM failed: {0}")] InitializeVm(HypervisorError), + #[cfg(all(gdb, target_arch = "aarch64", target_os = "windows"))] + #[error("Failed to initialize ARM64 debug support: {0}")] + InitializeDebug(String), #[cfg(all(kvm, target_arch = "x86_64"))] #[error("KVM MSR filtering requires KVM_CAP_X86_MSR_FILTER")] MsrFilterNotSupported, diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index bfff3ecd7..60e51ce15 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -8,6 +8,8 @@ //! expose ARM64 WHP structures, we define our own FFI bindings derived from //! the Windows SDK header `WinHvPlatformDefs.h` (10.0.26100.0). +#[cfg(gdb)] +use std::collections::HashSet; use std::os::raw::c_void; use std::sync::atomic::Ordering; use std::sync::{Condvar, Mutex}; @@ -22,6 +24,10 @@ use windows::Win32::System::Threading::GetCurrentProcess; use windows_result::HRESULT; use super::release_file_mapping; +#[cfg(gdb)] +use crate::hypervisor::gdb::arch::SW_BP_IMMEDIATE; +#[cfg(gdb)] +use crate::hypervisor::gdb::{DebugError, DebuggableVm, VcpuStopReason}; use crate::hypervisor::regs::whp_reg::*; use crate::hypervisor::regs::{ CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, @@ -36,6 +42,8 @@ use crate::hypervisor::virtual_machine::{ }; use crate::hypervisor::wrappers::HandleWrapper; use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags, MemoryRegionType}; +#[cfg(gdb)] +use crate::mem::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, HostSharedMemory}; #[cfg(feature = "trace_guest")] use crate::sandbox::trace::TraceContext as SandboxTraceContext; @@ -89,6 +97,20 @@ const ARM64_IC_PARAMETERS: Arm64IcParameters = Arm64IcParameters { }; const PARTITION_WAIT_TIMEOUT: Duration = Duration::from_secs(10); +#[cfg(gdb)] +const EXTENDED_VM_EXIT_HYPERCALL: u64 = 1 << 5; +#[cfg(gdb)] +const DEBUG_CACHE_SYNC_GPA: u64 = 0x1000; +#[cfg(gdb)] +const DEBUG_CACHE_SYNC_IMMEDIATE: u16 = 0x4858; +#[cfg(gdb)] +const DEBUG_CACHE_SYNC_CODE: [u8; 20] = [ + 0x9f, 0x3b, 0x03, 0xd5, // dsb ish + 0x1f, 0x75, 0x08, 0xd5, // ic iallu + 0x9f, 0x3b, 0x03, 0xd5, // dsb ish + 0xdf, 0x3f, 0x03, 0xd5, // isb + 0x02, 0x0b, 0x09, 0xd4, // hvc #0x4858 +]; type WhvResetPartitionFn = unsafe extern "system" fn(WHV_PARTITION_HANDLE) -> HRESULT; @@ -148,6 +170,8 @@ mod arm64_exit_reasons { WHV_RUN_VP_EXIT_REASON(0x80000021u32 as i32); pub const WHV_EXIT_REASON_INVALID_VP_REGISTER: WHV_RUN_VP_EXIT_REASON = WHV_RUN_VP_EXIT_REASON(0x80000020u32 as i32); + pub const WHV_EXIT_REASON_HYPERCALL: WHV_RUN_VP_EXIT_REASON = + WHV_RUN_VP_EXIT_REASON(0x80000050u32 as i32); pub const WHV_EXIT_REASON_ARM64_RESET: WHV_RUN_VP_EXIT_REASON = WHV_RUN_VP_EXIT_REASON(0x8001000cu32 as i32); pub const WHV_EXIT_REASON_CANCELLED: WHV_RUN_VP_EXIT_REASON = @@ -190,7 +214,9 @@ impl Default for Arm64ExitContext { } /// Parsed fields from `WHV_INTERCEPT_MESSAGE_HEADER` (ARM64 version). -struct InterceptHeader { +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +struct Arm64InterceptMessageHeader { #[allow(dead_code)] vp_index: u32, instruction_length: u8, @@ -205,9 +231,9 @@ struct InterceptHeader { impl Arm64ExitContext { /// Parse the intercept message header from the start of the payload. /// This is valid for memory access, unrecoverable, and register intercept exits. - fn intercept_header(&self) -> InterceptHeader { + fn intercept_header(&self) -> Arm64InterceptMessageHeader { let bytes = unsafe { core::slice::from_raw_parts(self.payload.as_ptr() as *const u8, 24) }; - InterceptHeader { + Arm64InterceptMessageHeader { vp_index: u32::from_le_bytes(bytes[0..4].try_into().unwrap()), instruction_length: bytes[4], intercept_access_type: bytes[5], @@ -240,6 +266,55 @@ impl Arm64ExitContext { let bytes = unsafe { core::slice::from_raw_parts(self.payload.as_ptr() as *const u8, 64) }; u64::from_le_bytes(bytes[56..64].try_into().unwrap()) } + + #[cfg(gdb)] + fn hypercall_context(&self) -> Arm64HypercallContext { + // SAFETY: The exit payload is 256 bytes. The hypercall context occupies + // its first 176 bytes and may be read without alignment assumptions. + unsafe { core::ptr::read_unaligned(self.payload.as_ptr().cast()) } + } +} + +#[cfg(gdb)] +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +struct Arm64HypercallContext { + header: Arm64InterceptMessageHeader, + immediate: u16, + reserved1: u16, + reserved2: u32, + x: [u64; 18], +} + +#[cfg(gdb)] +const _: [(); 24] = [(); core::mem::size_of::()]; +#[cfg(gdb)] +const _: [(); 176] = [(); core::mem::size_of::()]; + +#[cfg(gdb)] +#[derive(Debug, PartialEq, Eq)] +enum Arm64HypercallClassification { + SoftwareBreakpoint, + Unknown { immediate: u16, pc: u64 }, +} + +#[cfg(gdb)] +fn classify_hypercall( + context: &Arm64HypercallContext, + debug_enabled: bool, + software_breakpoints: &HashSet, +) -> Arm64HypercallClassification { + if debug_enabled + && context.immediate == SW_BP_IMMEDIATE + && software_breakpoints.contains(&context.header.pc) + { + Arm64HypercallClassification::SoftwareBreakpoint + } else { + Arm64HypercallClassification::Unknown { + immediate: context.immediate, + pc: context.header.pc, + } + } } // ============================================================================ @@ -330,6 +405,14 @@ pub(crate) struct WhpVm { /// Tracks host-side file mappings for cleanup. file_mappings: Vec<(HandleWrapper, *mut c_void)>, _no_surrogate_guard: Option, + #[cfg(gdb)] + debug_enabled: bool, + #[cfg(gdb)] + software_breakpoints: HashSet, + #[cfg(gdb)] + _debug_cache_sync_host_memory: Option, + #[cfg(gdb)] + _debug_cache_sync_guest_memory: Option, } // Safety: same reasoning as x86_64 WhpVm — raw pointers are kernel resource handles, @@ -372,6 +455,17 @@ impl WhpVm { ) .map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?; + #[cfg(gdb)] + WHvSetPartitionProperty( + p, + WHvPartitionPropertyCodeExtendedVmExits, + &WHV_EXTENDED_VM_EXITS { + AsUINT64: EXTENDED_VM_EXIT_HYPERCALL, + } as *const _ as *const _, + std::mem::size_of::() as _, + ) + .map_err(|e| CreateVmError::SetPartitionProperty(e.into()))?; + WHvSetupPartition(p).map_err(|e| CreateVmError::InitializeVm(e.into()))?; WHvCreateVirtualProcessor(p, 0, 0) .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; @@ -406,6 +500,14 @@ impl WhpVm { surrogate_process: None, file_mappings: Vec::new(), _no_surrogate_guard: no_surrogate_guard, + #[cfg(gdb)] + debug_enabled: false, + #[cfg(gdb)] + software_breakpoints: HashSet::new(), + #[cfg(gdb)] + _debug_cache_sync_host_memory: None, + #[cfg(gdb)] + _debug_cache_sync_guest_memory: None, }; if !no_surrogate { @@ -417,6 +519,9 @@ impl WhpVm { ); } + #[cfg(gdb)] + vm.initialize_debug_support()?; + Ok(vm) } @@ -455,6 +560,99 @@ impl WhpVm { } Ok(()) } + + #[cfg(gdb)] + fn initialize_debug_support(&mut self) -> Result<(), CreateVmError> { + let mut memory = ExclusiveSharedMemory::new(page_size::get()) + .map_err(|e| CreateVmError::InitializeDebug(e.to_string()))?; + memory + .copy_from_slice(&DEBUG_CACHE_SYNC_CODE, 0) + .map_err(|e| CreateVmError::InitializeDebug(e.to_string()))?; + let (host_memory, guest_memory) = memory.build(); + let region = guest_memory.mapping_at(DEBUG_CACHE_SYNC_GPA, MemoryRegionType::Scratch); + + // SAFETY: Both shared-memory handles are retained for the VM lifetime. + unsafe { self.map_memory((u32::MAX, ®ion)) } + .map_err(|e| CreateVmError::InitializeDebug(e.to_string()))?; + self._debug_cache_sync_host_memory = Some(host_memory); + self._debug_cache_sync_guest_memory = Some(guest_memory); + Ok(()) + } + + #[cfg(gdb)] + fn run_debug_cache_sync(&mut self) -> std::result::Result<(), DebugError> { + let original_regs = self + .regs() + .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; + let original_sctlr_values = self + .get_registers(&[WHV_ARM64_REGISTER_SCTLR_EL1]) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; + let original_sctlr = unsafe { original_sctlr_values[0].0.Reg64 }; + + let sync_result = (|| { + let mut sync_regs = original_regs; + sync_regs.pc = DEBUG_CACHE_SYNC_GPA; + sync_regs.pstate = 0b11 << 6 | 0b100; + self.set_regs(&sync_regs) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; + self.set_registers( + &[WHV_ARM64_REGISTER_SCTLR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + })], + ) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; + + let mut exit_context = Arm64ExitContext::default(); + // SAFETY: The partition and vCPU are live. `exit_context` has the + // SDK-defined ARM64 exit-context size and remains valid for the call. + unsafe { + WHvRunVirtualProcessor( + self.partition, + 0, + &mut exit_context as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + ) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; + } + + if exit_context.exit_reason != arm64_exit_reasons::WHV_EXIT_REASON_HYPERCALL { + return Err(DebugError::InstructionCacheSync(format!( + "unexpected WHP exit reason {:#x}", + exit_context.exit_reason.0 as u32 + ))); + } + + let hypercall = exit_context.hypercall_context(); + let expected_pc = DEBUG_CACHE_SYNC_GPA + (DEBUG_CACHE_SYNC_CODE.len() - 4) as u64; + if hypercall.immediate != DEBUG_CACHE_SYNC_IMMEDIATE + || hypercall.header.pc != expected_pc + { + return Err(DebugError::InstructionCacheSync(format!( + "unexpected hypercall immediate {:#x} at PC {:#x}", + hypercall.immediate, hypercall.header.pc + ))); + } + + Ok(()) + })(); + + let restore_sctlr = self + .set_registers( + &[WHV_ARM64_REGISTER_SCTLR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: original_sctlr, + })], + ) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string())); + let restore_regs = self + .set_regs(&original_regs) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string())); + + sync_result?; + restore_sctlr?; + restore_regs + } } impl VirtualMachine for WhpVm { @@ -639,6 +837,20 @@ impl VirtualMachine for WhpVm { } } WHV_EXIT_REASON_CANCELLED => Ok(VmExit::Cancelled()), + #[cfg(gdb)] + WHV_EXIT_REASON_HYPERCALL => { + let context = exit_context.hypercall_context(); + match classify_hypercall(&context, self.debug_enabled, &self.software_breakpoints) { + Arm64HypercallClassification::SoftwareBreakpoint => Ok(VmExit::Debug { + reason: VcpuStopReason::SwBp, + }), + Arm64HypercallClassification::Unknown { immediate, pc } => { + Ok(VmExit::Unknown(format!( + "Unsupported ARM64 hypercall immediate {immediate:#x} at PC={pc:#x}" + ))) + } + } + } WHV_EXIT_REASON_ARM64_RESET => Ok(VmExit::Halt()), WHV_EXIT_REASON_UNRECOVERABLE => { let header = exit_context.intercept_header(); @@ -658,15 +870,16 @@ impl VirtualMachine for WhpVm { } fn regs(&self) -> Result { - // Get all 31 GP regs + PC + SP + PSTATE in one batch - const COUNT: usize = 31 + 3; // X0..X30, PC, SP, PSTATE + // Get all 31 GP regs + PC + both stack pointers + PSTATE in one batch. + const COUNT: usize = 31 + 4; let mut names = [WHV_REGISTER_NAME(0); COUNT]; for i in 0..31u32 { names[i as usize] = xreg(i); } names[31] = WHV_ARM64_REGISTER_PC; names[32] = WHV_ARM64_REGISTER_SP_EL0; - names[33] = WHV_ARM64_REGISTER_PSTATE; + names[33] = WHV_ARM64_REGISTER_SP_EL1; + names[34] = WHV_ARM64_REGISTER_PSTATE; let values = self.get_registers(&names).map_err(RegisterError::GetRegs)?; @@ -675,11 +888,18 @@ impl VirtualMachine for WhpVm { x[i] = unsafe { values[i].0.Reg64 }; } + let pstate = unsafe { values[34].0.Reg64 }; + let sp = if pstate & 1 == 0 { + unsafe { values[32].0.Reg64 } + } else { + unsafe { values[33].0.Reg64 } + }; + Ok(CommonRegisters { x, pc: unsafe { values[31].0.Reg64 }, - sp: unsafe { values[32].0.Reg64 }, - pstate: unsafe { values[33].0.Reg64 }, + sp, + pstate, }) } @@ -696,7 +916,11 @@ impl VirtualMachine for WhpVm { } names[31] = WHV_ARM64_REGISTER_PC; values[31] = Align16(WHV_REGISTER_VALUE { Reg64: regs.pc }); - names[32] = WHV_ARM64_REGISTER_SP_EL0; + names[32] = if regs.pstate & 1 == 0 { + WHV_ARM64_REGISTER_SP_EL0 + } else { + WHV_ARM64_REGISTER_SP_EL1 + }; values[32] = Align16(WHV_REGISTER_VALUE { Reg64: regs.sp }); names[33] = WHV_ARM64_REGISTER_PSTATE; values[33] = Align16(WHV_REGISTER_VALUE { Reg64: regs.pstate }); @@ -877,6 +1101,175 @@ impl Drop for WhpVm { } } +#[cfg(gdb)] +impl DebuggableVm for WhpVm { + fn translate_gva(&self, gva: u64) -> std::result::Result { + let mut gpa = 0; + let mut result = WHV_TRANSLATE_GVA_RESULT::default(); + + unsafe { + WHvTranslateGva( + self.partition, + 0, + gva, + WHvTranslateGvaFlagValidateRead, + &mut result, + &mut gpa, + ) + .map_err(|_| DebugError::TranslateGva(gva))?; + } + if result.ResultCode != WHvTranslateGvaResultSuccess { + return Err(DebugError::TranslateGva(gva)); + } + + Ok(gpa) + } + + fn set_debug(&mut self, enable: bool) -> std::result::Result<(), DebugError> { + self.debug_enabled = enable; + Ok(()) + } + + fn sync_instruction_cache(&mut self) -> std::result::Result<(), DebugError> { + self.run_debug_cache_sync() + } + + fn register_sw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> { + self.software_breakpoints.insert(addr); + Ok(()) + } + + fn unregister_sw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> { + self.software_breakpoints.remove(&addr); + Ok(()) + } +} + +#[cfg(all(test, gdb))] +mod debug_tests { + use std::ffi::c_void; + + use serial_test::serial; + use windows::Win32::System::Diagnostics::Debug::FlushInstructionCache; + use windows::Win32::System::Threading::GetCurrentProcess; + + use super::*; + use crate::hypervisor::gdb::arch::SW_BP; + use crate::mem::shared_mem::SharedMemory; + + fn context(immediate: u16, pc: u64) -> Arm64HypercallContext { + Arm64HypercallContext { + immediate, + header: Arm64InterceptMessageHeader { + pc, + instruction_length: 4, + ..Default::default() + }, + ..Default::default() + } + } + + #[test] + fn classifies_only_tracked_debugger_hypercall() { + let breakpoints = HashSet::from([0x4000]); + assert_eq!( + classify_hypercall(&context(SW_BP_IMMEDIATE, 0x4000), true, &breakpoints), + Arm64HypercallClassification::SoftwareBreakpoint + ); + assert!(matches!( + classify_hypercall(&context(SW_BP_IMMEDIATE, 0x4004), true, &breakpoints), + Arm64HypercallClassification::Unknown { .. } + )); + assert!(matches!( + classify_hypercall(&context(0, 0x4000), true, &breakpoints), + Arm64HypercallClassification::Unknown { .. } + )); + assert!(matches!( + classify_hypercall(&context(SW_BP_IMMEDIATE, 0x4000), false, &breakpoints), + Arm64HypercallClassification::Unknown { .. } + )); + } + + #[test] + fn parses_arm64_hypercall_context_layout() { + let expected = context(SW_BP_IMMEDIATE, 0x1234); + let mut exit = Arm64ExitContext::default(); + // SAFETY: `payload` is larger than `Arm64HypercallContext`. The write is + // unaligned and does not outlive the payload. + unsafe { + core::ptr::write_unaligned(exit.payload.as_mut_ptr().cast(), expected); + } + assert_eq!(exit.hypercall_context(), expected); + } + + #[test] + #[serial] + fn cache_sync_trampoline_exits_through_reserved_hypercall() { + if !is_hypervisor_present() { + return; + } + + let mut vm = WhpVm::new().unwrap(); + vm.sync_instruction_cache().unwrap(); + } + + #[test] + #[serial] + fn patched_software_breakpoint_is_runnable_without_pc_advance() { + if !is_hypervisor_present() { + return; + } + + const BREAKPOINT_OFFSET: usize = 0x20; + let mut vm = WhpVm::new().unwrap(); + let host_memory = vm._debug_cache_sync_host_memory.as_ref().unwrap(); + host_memory + .copy_from_slice(&SW_BP, BREAKPOINT_OFFSET) + .unwrap(); + // SAFETY: The current process handle is valid and the range is inside + // the live shared-memory mapping. + unsafe { + FlushInstructionCache( + GetCurrentProcess(), + Some( + host_memory + .base_ptr() + .wrapping_add(BREAKPOINT_OFFSET) + .cast::(), + ), + SW_BP.len(), + ) + .unwrap(); + } + vm.sync_instruction_cache().unwrap(); + + let breakpoint_address = DEBUG_CACHE_SYNC_GPA + BREAKPOINT_OFFSET as u64; + vm.set_debug(true).unwrap(); + vm.register_sw_breakpoint(breakpoint_address).unwrap(); + vm.set_registers( + &[WHV_ARM64_REGISTER_SCTLR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + })], + ) + .unwrap(); + vm.set_regs(&CommonRegisters { + pc: breakpoint_address, + pstate: 0b11 << 6 | 0b100, + ..Default::default() + }) + .unwrap(); + + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::SwBp + } + )); + assert_eq!(vm.regs().unwrap().pc, breakpoint_address); + } +} + // ============================================================================ // Helpers: dynamically load optional WHP APIs // ============================================================================ diff --git a/src/hyperlight_host/src/sandbox/initialized.rs b/src/hyperlight_host/src/sandbox/initialized.rs index dcc3fa0b5..893049690 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -466,6 +466,13 @@ impl Sandbox { #[instrument(err(Debug), skip_all, parent = Span::current())] pub fn snapshot_with_options(&mut self, options: &SnapshotOptions) -> Result> { self.check_ready()?; + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.vm.has_sw_breakpoints() { + return Err(crate::new_error!( + "Cannot snapshot an ARM64 sandbox while software breakpoints are installed" + )); + } + // Unpacked in full so that a new option has to decide whether the // cached plain snapshot still answers it. let SnapshotOptions { scratch_size } = options; @@ -709,6 +716,13 @@ impl Sandbox { return Err(HyperlightError::UnrecoverableSandbox); } + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.vm.has_sw_breakpoints() { + return Err(crate::new_error!( + "Cannot restore an ARM64 sandbox while software breakpoints are installed" + )); + } + // Currently, we do not try to optimise restore to the // most-current snapshot. This is because the most-current // snapshot, while it must have identical virtual memory From 7abed406f72e74025f9c47bf1ca0e2969a6d758f Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:53:37 -0700 Subject: [PATCH 2/7] fix(aarch64): address review feedback on WHP GDB support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes * Run the full disable path when the debugger detaches after a crash, so software breakpoints and pending steps are cleaned up. * Cancel pending software steps for any cancellation source, not only debugger interrupts. * Reject `map_region` requests that overlap the hidden cache sync page at GPA 0x1000, and add a test. * Restore the `≡` and `×` characters in `hyperlight_vm/x86_64.rs` comments. ## Verification On Windows ARM64 (WHP): * `just clippy`, `just fmt-check` and the `hypervisor::` and `map_region` lib tests with the `gdb` feature pass. --------- Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/hypervisor/hyperlight_vm/debug.rs | 10 +++++++ .../src/hypervisor/hyperlight_vm/mod.rs | 28 ++++++++++++++++--- .../src/hypervisor/hyperlight_vm/x86_64.rs | 8 +++--- .../hypervisor/virtual_machine/whp/aarch64.rs | 2 +- .../src/sandbox/initialized.rs | 19 +++++++++++++ 5 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs index abc2f26c6..3ea8053a0 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs @@ -100,6 +100,16 @@ impl HyperlightVm { let response = match req { DebugMsg::DisableDebug => { detach = true; + // Remove software breakpoint state so restore does not reject recovery. + #[cfg(target_arch = "aarch64")] + match self.process_dbg_request(DebugMsg::DisableDebug, &mem_access) { + Ok(response) => response, + Err(error) => { + tracing::error!("Failed to clean up after detach: {error}"); + DebugResponse::ErrorOccurred + } + } + #[cfg(not(target_arch = "aarch64"))] DebugResponse::DisableDebug } DebugMsg::Continue => { diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs index a65bf6add..d82876809 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -492,6 +492,23 @@ impl HyperlightVm { } } + // Check against the hidden debug cache-sync page + #[cfg(all(gdb, target_arch = "aarch64", target_os = "windows"))] + { + use crate::hypervisor::virtual_machine::whp::DEBUG_CACHE_SYNC_GPA; + + let reserved_start = DEBUG_CACHE_SYNC_GPA as usize; + let reserved_end = reserved_start + self.page_size; + if new_start < reserved_end && new_end > reserved_start { + return Err(MapRegionError::Overlapping { + new_start, + new_end, + existing_start: reserved_start, + existing_end: reserved_end, + }); + } + } + // Try to reuse a freed slot first, otherwise use next_slot let slot = self.allocate_slot(); @@ -796,14 +813,17 @@ impl HyperlightVm { continue; } + // Pending ARM64 software steps hold temporary breakpoints, so they + // are cancelled for any interruption source. + #[cfg(all(gdb, target_arch = "aarch64"))] + if let Err(e) = self.cancel_pending_software_step(mem_mgr) { + break Err(HandleDebugError::from(e).into()); + } + // If the vcpu was interrupted by a debugger, we need to handle it #[cfg(gdb)] if debug_interrupted { self.interrupt_handle.state().clear_debug_interrupt(); - #[cfg(target_arch = "aarch64")] - if let Err(e) = self.cancel_pending_software_step(mem_mgr) { - break Err(HandleDebugError::from(e).into()); - } if let Err(e) = self.handle_debug(mem_mgr, VcpuStopReason::Interrupt) { break Err(e.into()); } 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 520cf9e55..6dbe1eb7a 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -180,7 +180,7 @@ impl HyperlightVm { // We usually keep the top of the stack 16-byte // aligned. However, the ABI requirement is that the stack // be aligned _before a call instruction_, which means - // that the stack needs to actually be ≡ 8 mod 16 at the + // that the stack needs to actually be ≡ 8 mod 16 at the // first instruction (since, on x64, a call instruction // automatically pushes a return address). rsp: self.rsp_gva - 8, @@ -266,7 +266,7 @@ impl HyperlightVm { // aligned. Since the usual ABI requirement is that the // stack be aligned _before a call instruction_, one might // expect that the stack pointer here needs to actually be - // ≡ 8 mod 16 at the first instruction (since, on x64, a + // ≡ 8 mod 16 at the first instruction (since, on x64, a // call instruction automatically pushes a return // address). However, the x64 entry stub in // hyperlight_guest::arch::dispatch handles this itself, @@ -917,8 +917,8 @@ mod tests { /// Bytes 0-1: FCW, 2-3: FSW, 4: FTW, 5: reserved, 6-7: FOP /// Bytes 8-15: FIP, 16-23: FDP /// Bytes 24-27: MXCSR, 28-31: MXCSR_MASK (preserve - hardware defined) - /// Bytes 32-159: ST0-ST7/MM0-MM7 (8 regs × 16 bytes) - /// Bytes 160-415: XMM0-XMM15 (16 regs × 16 bytes) + /// Bytes 32-159: ST0-ST7/MM0-MM7 (8 regs × 16 bytes) + /// Bytes 160-415: XMM0-XMM15 (16 regs × 16 bytes) /// Bytes 416-511: Reserved fn dirty_xsave_legacy(xsave: &mut [u32], current_xsave: &[u8]) { // FCW (bytes 0-1) + FSW (bytes 2-3) - pack into xsave[0] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index 60e51ce15..123d04273 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -100,7 +100,7 @@ const PARTITION_WAIT_TIMEOUT: Duration = Duration::from_secs(10); #[cfg(gdb)] const EXTENDED_VM_EXIT_HYPERCALL: u64 = 1 << 5; #[cfg(gdb)] -const DEBUG_CACHE_SYNC_GPA: u64 = 0x1000; +pub(crate) const DEBUG_CACHE_SYNC_GPA: u64 = 0x1000; #[cfg(gdb)] const DEBUG_CACHE_SYNC_IMMEDIATE: u16 = 0x4858; #[cfg(gdb)] diff --git a/src/hyperlight_host/src/sandbox/initialized.rs b/src/hyperlight_host/src/sandbox/initialized.rs index 893049690..37e78dced 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -4657,6 +4657,25 @@ mod tests { ); } + #[cfg(all(gdb, target_arch = "aarch64", target_os = "windows"))] + #[test] + fn map_region_rejects_debug_cache_sync_page() { + use crate::hypervisor::virtual_machine::whp::DEBUG_CACHE_SYNC_GPA; + + let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf()) + .build() + .unwrap(); + + let mem = allocate_guest_memory(); + let region = + region_for_memory(&mem, DEBUG_CACHE_SYNC_GPA as usize, MemoryRegionFlags::READ); + let err = unsafe { sbox.map_region(®ion) }.unwrap_err(); + assert!( + format!("{err:?}").contains("Overlapping"), + "Expected Overlapping error, got: {err:?}" + ); + } + #[test] fn map_region_allows_adjacent_non_overlapping() { let mut sbox = SandboxBuilder::from_file(simple_guest_as_pathbuf()) From 905ca05b157148c38e9297d19a722f6372226519 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Fri, 16 Oct 2026 13:26:17 -0700 Subject: [PATCH 3/7] feat(aarch64): use hardware debugging for WHP guests Guest synchronous exception vectors forward debug stops through HVC. Hardware single-step supports branches, returns, and self-loops. Two hardware breakpoint slots and BRK software breakpoints are available. Interrupted register context and breakpoint configuration survive cache synchronization and vCPU reset. Asynchronous vectors retain their normal exception paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- CHANGELOG.md | 3 +- docs/how-to-debug-a-hyperlight-guest.md | 10 +- src/hyperlight_common/src/arch/aarch64/exn.rs | 3 + .../src/arch/aarch64/exception/entry.rs | 17 + .../examples/guest-debugging/main.rs | 40 ++ .../src/hypervisor/gdb/arch/aarch64.rs | 162 +----- .../src/hypervisor/gdb/event_loop.rs | 1 - src/hyperlight_host/src/hypervisor/gdb/mod.rs | 10 - .../src/hypervisor/gdb/target.rs | 5 +- .../src/hypervisor/hyperlight_vm/aarch64.rs | 22 + .../src/hypervisor/hyperlight_vm/debug.rs | 152 +---- .../src/hypervisor/hyperlight_vm/mod.rs | 7 +- .../src/hypervisor/regs/aarch64/whp_reg.rs | 12 + .../src/hypervisor/virtual_machine/mod.rs | 5 +- .../hypervisor/virtual_machine/whp/aarch64.rs | 522 ++++++++++++++---- src/hyperlight_host/src/mem/elf.rs | 26 +- src/hyperlight_host/src/mem/exe.rs | 4 + .../src/sandbox/uninitialized_evolve.rs | 12 +- 18 files changed, 591 insertions(+), 422 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecea482dd..3f4b7adc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added * `Sandbox::snapshot_with_options` takes a snapshot with `SnapshotOptions`. `SnapshotOptions::scratch_size` gives the sandbox, and later restores of the snapshot, a different scratch size. A guest can boot with the scratch its initialisation needs and run guest function calls from a snapshot with only the scratch they need, so each restore costs less. It can also give a guest more scratch than it booted with. -* Support register, memory, continue, single-step, interrupt, and software-breakpoint GDB debugging for Windows ARM64 guests. +* Support register, memory, continue, hardware single-step, interrupt, and + hardware and software breakpoint GDB debugging for Windows ARM64 guests. ### Changed * Core dumps no longer record the guest binary path. The process name and command line are always ``, as they already were for sandboxes created from snapshots by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1975 diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index c8e5ab235..e1572d42d 100644 --- a/docs/how-to-debug-a-hyperlight-guest.md +++ b/docs/how-to-debug-a-hyperlight-guest.md @@ -17,11 +17,11 @@ The Hyperlight `gdb` feature enables guest debugging to: - stop when a crash occurs and only allow read access to the guest memory and registers On x86_64, gdb can also add up to four hardware breakpoints. -Windows ARM64 uses four-byte HVC software breakpoints and software single-step. -ARM64 stepping supports linear instructions, direct and conditional branches, -compare and test branches, and `BR`, `BLR`, and `RET`. Other control-flow and -exception instructions return an explicit error. Hardware breakpoints and -watchpoints are not advertised on Windows ARM64. +Windows ARM64 supports two hardware breakpoints, four-byte `BRK` software +breakpoints, and hardware single-step. Guest exception vectors forward debug +exceptions to WHP through HVC. ARM64 guest binaries must retain the +`hyperlight_debug_vectors` symbol so the host can install the vectors before +the initial debug stop. Watchpoints are not advertised. ## Expected behavior diff --git a/src/hyperlight_common/src/arch/aarch64/exn.rs b/src/hyperlight_common/src/arch/aarch64/exn.rs index 648822460..f8343a8a8 100644 --- a/src/hyperlight_common/src/arch/aarch64/exn.rs +++ b/src/hyperlight_common/src/arch/aarch64/exn.rs @@ -3,6 +3,9 @@ use crate::vmem::bits; +/// HVC immediate for forwarding guest exceptions to the WHP debugger. +pub const DEBUG_EXCEPTION_HVC: u16 = 0x485a; + const ESR_EC_UNKNOWN: u64 = 0b000000; // udf generates this const ESR_EC_DATA_ABORT_LOWER_EL: u64 = 0b100100; const ESR_EC_DATA_ABORT_SAME_EL: u64 = 0b100101; diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs b/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs index 869c10bb1..7fb0bce3a 100644 --- a/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs +++ b/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs @@ -104,10 +104,24 @@ context_restore:\n q_off = const offset_of!(ExceptionContext, q), ); +// Only synchronous exceptions have a syndrome for debug forwarding. +// The host resumes debug events at ERET and other synchronous exceptions at the prologue. macro_rules! vbar_entry { ($et:literal, $ef:literal) => { concat!( " + .if {ExceptionType_", + $et, + "} == {ExceptionType_Synchronous}\n + stp x0, x1, [sp, #-16]!\n + mrs x0, mdscr_el1\n + tbz x0, #13, 1f\n + ldp x0, x1, [sp], #16\n + hvc #{debug_hvc}\n + eret\n + 1:\n + ldp x0, x1, [sp], #16\n + .endif\n sub sp, sp, #{ctx_size}\n str x30, [sp, #{x30_off}]\n bl context_save\n @@ -129,6 +143,8 @@ macro_rules! vbar_entry { global_asm!(" .balign 0x800\n .global vbar\n +.global hyperlight_debug_vectors\n +hyperlight_debug_vectors:\n vbar:\n", vbar_entry!("Synchronous", "CurrentSP0"), vbar_entry!("IRQ", "CurrentSP0"), @@ -148,6 +164,7 @@ vbar:\n", vbar_entry!("SError", "LowerAArch32"), ctx_size = const size_of::(), x30_off = const offset_of!(ExceptionContext, x) + 15 * 0x010, + debug_hvc = const hyperlight_common::arch::exn::DEBUG_EXCEPTION_HVC, handler = sym super::handle::handle_exception, ExceptionType_Synchronous = const ExceptionType::Synchronous as u64, ExceptionType_IRQ = const ExceptionType::Irq as u64, diff --git a/src/hyperlight_host/examples/guest-debugging/main.rs b/src/hyperlight_host/examples/guest-debugging/main.rs index 631e95ec4..cbcd879ea 100644 --- a/src/hyperlight_host/examples/guest-debugging/main.rs +++ b/src/hyperlight_host/examples/guest-debugging/main.rs @@ -327,6 +327,46 @@ mod tests { assert!(result.is_ok(), "{}", result.unwrap_err()); } + #[test] + #[cfg(target_arch = "aarch64")] + #[serial] + fn test_gdb_hardware_breakpoint_and_step() { + let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-aarch64-hardware"); + let cmd = format!( + "set osabi none + file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest + target remote :8080 + set pagination off + set logging file {out_file_path} + set logging enabled on + set $entry_pc = $pc + stepi + if $pc != $entry_pc + 4 + echo Entry step did not execute one instruction\\n + quit 1 + end + hbreak simpleguest::no_op + continue + set $return_pc = $x30 + stepi + if $pc != $return_pc + echo Hardware RET step missed its destination\\n + quit 1 + end + echo Hardware breakpoint and entry/RET stepping passed\\n + set logging enabled off + detach + quit + " + ); + let checker = |contents: String| { + contents.contains("Hardware breakpoint and entry/RET stepping passed") + }; + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker); + cleanup(&out_file_path, &cmd_file_path); + assert!(result.is_ok(), "{}", result.unwrap_err()); + } + #[test] #[cfg(target_arch = "aarch64")] #[serial] diff --git a/src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs b/src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs index abab066dd..1a5bda738 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs @@ -1,103 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2026 The Hyperlight Authors. -use crate::hypervisor::regs::CommonRegisters; - pub(crate) const SW_BP_SIZE: usize = 4; -pub(crate) const SW_BP_IMMEDIATE: u16 = 0x4859; -const HVC_BASE: u32 = 0xd400_0002; -const SW_BP_INSTRUCTION: u32 = HVC_BASE | ((SW_BP_IMMEDIATE as u32) << 5); -pub(crate) const SW_BP: [u8; SW_BP_SIZE] = SW_BP_INSTRUCTION.to_le_bytes(); +pub(crate) const SW_BP: [u8; SW_BP_SIZE] = 0xd420_0000u32.to_le_bytes(); pub(crate) fn valid_sw_breakpoint_address(addr: u64) -> bool { addr.is_multiple_of(SW_BP_SIZE as u64) } -#[derive(Debug, thiserror::Error, PartialEq, Eq)] -pub enum SoftwareStepError { - #[error("Software single-step does not support instruction {instruction:#010x} at {pc:#x}")] - UnsupportedInstruction { pc: u64, instruction: u32 }, - #[error("Software single-step target overflow for instruction at {0:#x}")] - TargetOverflow(u64), - #[error("Software single-step target is the current PC {0:#x}")] - SelfLoop(u64), - #[error("Software single-step branch register X{0} is invalid")] - InvalidBranchRegister(u8), - #[error("Software single-step target {0:#x} is not instruction-aligned")] - UnalignedTarget(u64), -} - -fn sign_extend(value: u32, bits: u32) -> i64 { - ((value as i64) << (64 - bits)) >> (64 - bits) -} - -fn relative_target(pc: u64, immediate: u32, bits: u32) -> Result { - let offset = sign_extend(immediate, bits) << 2; - pc.checked_add_signed(offset) - .ok_or(SoftwareStepError::TargetOverflow(pc)) -} - -fn branch_register(instruction: u32, regs: &CommonRegisters) -> Result { - let register = ((instruction >> 5) & 0x1f) as u8; - regs.x - .get(register as usize) - .copied() - .ok_or(SoftwareStepError::InvalidBranchRegister(register)) -} - -pub(crate) fn software_step_targets( - pc: u64, - instruction: u32, - regs: &CommonRegisters, -) -> Result, SoftwareStepError> { - let fallthrough = pc - .checked_add(SW_BP_SIZE as u64) - .ok_or(SoftwareStepError::TargetOverflow(pc))?; - - let mut targets = if instruction & 0x7c00_0000 == 0x1400_0000 { - vec![relative_target(pc, instruction & 0x03ff_ffff, 26)?] - } else if instruction & 0xff00_0000 == 0x5400_0000 || instruction & 0x7e00_0000 == 0x3400_0000 { - vec![ - fallthrough, - relative_target(pc, (instruction >> 5) & 0x7ffff, 19)?, - ] - } else if instruction & 0x7e00_0000 == 0x3600_0000 { - vec![ - fallthrough, - relative_target(pc, (instruction >> 5) & 0x3fff, 14)?, - ] - } else if instruction & 0xffff_fc1f == 0xd61f_0000 - || instruction & 0xffff_fc1f == 0xd63f_0000 - || instruction & 0xffff_fc1f == 0xd65f_0000 - { - vec![branch_register(instruction, regs)?] - } else if instruction & 0xfe00_0000 == 0xd600_0000 || instruction & 0xff00_0000 == 0xd400_0000 { - return Err(SoftwareStepError::UnsupportedInstruction { pc, instruction }); - } else { - vec![fallthrough] - }; - - targets.sort_unstable(); - targets.dedup(); - if targets.contains(&pc) { - return Err(SoftwareStepError::SelfLoop(pc)); - } - if let Some(target) = targets - .iter() - .find(|&&target| !valid_sw_breakpoint_address(target)) - { - return Err(SoftwareStepError::UnalignedTarget(*target)); - } - Ok(targets) -} - #[cfg(test)] mod tests { use super::*; #[test] - fn software_breakpoint_is_reserved_hvc() { - assert_eq!(SW_BP, [0x22, 0x0b, 0x09, 0xd4]); + fn software_breakpoint_is_brk() { + assert_eq!(SW_BP, [0, 0, 0x20, 0xd4]); } #[test] @@ -107,77 +24,4 @@ mod tests { assert!(!valid_sw_breakpoint_address(0x4002)); assert!(!valid_sw_breakpoint_address(0x4003)); } - - #[test] - fn software_step_decodes_linear_instruction() { - assert_eq!( - software_step_targets(0x1000, 0xd503_201f, &CommonRegisters::default()).unwrap(), - vec![0x1004] - ); - } - - #[test] - fn software_step_decodes_direct_branches() { - assert_eq!( - software_step_targets(0x1000, 0x1400_0002, &CommonRegisters::default()).unwrap(), - vec![0x1008] - ); - assert_eq!( - software_step_targets(0x1000, 0x17ff_ffff, &CommonRegisters::default()).unwrap(), - vec![0xffc] - ); - assert_eq!( - software_step_targets(0x1000, 0x9400_0002, &CommonRegisters::default()).unwrap(), - vec![0x1008] - ); - } - - #[test] - fn software_step_decodes_conditional_branches() { - assert_eq!( - software_step_targets(0x1000, 0x5400_0040, &CommonRegisters::default()).unwrap(), - vec![0x1004, 0x1008] - ); - assert_eq!( - software_step_targets(0x1000, 0x5400_0050, &CommonRegisters::default()).unwrap(), - vec![0x1004, 0x1008] - ); - assert_eq!( - software_step_targets(0x1000, 0xb400_0040, &CommonRegisters::default()).unwrap(), - vec![0x1004, 0x1008] - ); - assert_eq!( - software_step_targets(0x1000, 0x3600_0040, &CommonRegisters::default()).unwrap(), - vec![0x1004, 0x1008] - ); - } - - #[test] - fn software_step_decodes_register_branches() { - let mut regs = CommonRegisters::default(); - regs.x[30] = 0x1234; - assert_eq!( - software_step_targets(0x1000, 0xd65f_03c0, ®s).unwrap(), - vec![0x1234] - ); - } - - #[test] - fn software_step_rejects_unsupported_control_flow() { - assert!(matches!( - software_step_targets(0x1000, 0xd400_0002, &CommonRegisters::default()), - Err(SoftwareStepError::UnsupportedInstruction { .. }) - )); - assert_eq!( - software_step_targets(0x1000, 0x1400_0000, &CommonRegisters::default()), - Err(SoftwareStepError::SelfLoop(0x1000)) - ); - - let mut regs = CommonRegisters::default(); - regs.x[30] = 0x1235; - assert_eq!( - software_step_targets(0x1000, 0xd65f_03c0, ®s), - Err(SoftwareStepError::UnalignedTarget(0x1235)) - ); - } } diff --git a/src/hyperlight_host/src/hypervisor/gdb/event_loop.rs b/src/hyperlight_host/src/hypervisor/gdb/event_loop.rs index 6249fe854..83466bfac 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/event_loop.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/event_loop.rs @@ -51,7 +51,6 @@ impl run_blocking::BlockingEventLoop for GdbBlockingEventLoop { let stop_response = match stop_reason { VcpuStopReason::DoneStep => BaseStopReason::DoneStep, VcpuStopReason::SwBp => BaseStopReason::SwBreak(()), - #[cfg(target_arch = "x86_64")] VcpuStopReason::HwBp => BaseStopReason::HwBreak(()), #[cfg(target_arch = "aarch64")] VcpuStopReason::Initial => BaseStopReason::SignalWithThread { diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index f27e37207..7e1dc02e9 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -206,7 +206,6 @@ impl<'a> DebugMemoryView<'a> { pub enum VcpuStopReason { Crash, DoneStep, - #[cfg(target_arch = "x86_64")] HwBp, #[cfg(target_arch = "aarch64")] Initial, @@ -286,7 +285,6 @@ pub(crate) trait DebuggableVm: VirtualMachine { fn set_debug(&mut self, enable: bool) -> std::result::Result<(), DebugError>; /// Enable/disable single stepping - #[cfg(target_arch = "x86_64")] fn set_single_step(&mut self, enable: bool) -> std::result::Result<(), DebugError> { if enable { Err(DebugError::Unsupported("single-step")) @@ -309,14 +307,6 @@ pub(crate) trait DebuggableVm: VirtualMachine { fn sync_instruction_cache(&mut self) -> std::result::Result<(), DebugError> { Ok(()) } - - fn register_sw_breakpoint(&mut self, _addr: u64) -> std::result::Result<(), DebugError> { - Ok(()) - } - - fn unregister_sw_breakpoint(&mut self, _addr: u64) -> std::result::Result<(), DebugError> { - Ok(()) - } } /// Debug communication channel that is used for sending a request type and diff --git a/src/hyperlight_host/src/hypervisor/gdb/target.rs b/src/hyperlight_host/src/hypervisor/gdb/target.rs index 9295cc911..98be27aac 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/target.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/target.rs @@ -11,10 +11,8 @@ use gdbstub::target::ext::base::singlethread::{ SingleThreadBase, SingleThreadResume, SingleThreadResumeOps, SingleThreadSingleStep, SingleThreadSingleStepOps, }; -#[cfg(target_arch = "x86_64")] -use gdbstub::target::ext::breakpoints::HwBreakpointOps; use gdbstub::target::ext::breakpoints::{ - Breakpoints, BreakpointsOps, HwBreakpoint, SwBreakpoint, SwBreakpointOps, + Breakpoints, BreakpointsOps, HwBreakpoint, HwBreakpointOps, SwBreakpoint, SwBreakpointOps, }; use gdbstub::target::ext::section_offsets::{Offsets, SectionOffsets}; use gdbstub::target::{Target, TargetError, TargetResult}; @@ -357,7 +355,6 @@ impl SectionOffsets for HyperlightSandboxTarget { } impl Breakpoints for HyperlightSandboxTarget { - #[cfg(target_arch = "x86_64")] fn support_hw_breakpoint(&mut self) -> Option> { Some(self) } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index b6b9d3f10..d3ea63c82 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -42,6 +42,28 @@ use crate::sandbox::snapshot::{NextAction, SnapshotMemoryBacking}; use crate::sandbox::trace::MemTraceInfo; impl HyperlightVm { + #[cfg(gdb)] + pub(crate) fn initialize_debug_vectors( + &mut self, + vectors: Option, + ) -> Result<(), CreateHyperlightVmError> { + if self.gdb_conn.is_none() || !matches!(self.next_action, NextAction::Initialise(_)) { + return Ok(()); + } + let vectors = vectors.ok_or_else(|| { + VmError::Debug(crate::hypervisor::gdb::DebugError::Unsupported( + "guest binary lacks ARM64 debug vectors", + )) + })?; + let mut regs = self.vm.sregs().map_err(VmError::Register)?; + regs.vbar_el1 = vectors; + regs.sp_el1 = hyperlight_common::layout::SCRATCH_TOP_GVA as u64 + - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + + 1; + self.vm.set_sregs(®s).map_err(VmError::Register)?; + Ok(()) + } + #[allow(clippy::too_many_arguments)] #[cfg_attr(target_os = "macos", allow(unused))] pub(crate) fn new( diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs index 3ea8053a0..95ca24d9d 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs @@ -7,8 +7,6 @@ use super::{ HandleDebugError, HyperlightVm, RecvDbgMsgError, SendDbgMsgError, VcpuStopReason, VmError, }; use crate::hypervisor::gdb::arch::{SW_BP, SW_BP_SIZE, valid_sw_breakpoint_address}; -#[cfg(target_arch = "aarch64")] -use crate::hypervisor::gdb::arch::{SoftwareStepError, software_step_targets}; use crate::hypervisor::gdb::{ DebugError, DebugMemoryAccessError, DebugMemoryView, DebugMsg, DebugResponse, }; @@ -42,9 +40,6 @@ impl SoftwareBreakpoints { #[derive(Clone, Debug)] pub(super) struct PendingSoftwareStep { origin: u64, - reinsert_origin: bool, - targets: Vec, - temporary_breakpoints: HashMap>, report_stop: bool, } @@ -60,9 +55,6 @@ pub enum ProcessDebugRequestError { UnalignedSwBreakpoint { address: u64, alignment: usize }, #[error("Address {0:#x} is not a software breakpoint")] SwBreakpointNotFound(u64), - #[cfg(target_arch = "aarch64")] - #[error("Failed to prepare ARM64 software single-step: {0}")] - SoftwareStep(#[from] SoftwareStepError), #[error("Failed to read memory: {0}")] ReadMemory(#[from] DebugMemoryAccessError), #[error("Failed to write memory: {0}")] @@ -134,11 +126,6 @@ impl HyperlightVm { | Err(ProcessDebugRequestError::Debug(DebugError::TranslateGva(_))) => { DebugResponse::ErrorOccurred } - #[cfg(target_arch = "aarch64")] - Err(error @ ProcessDebugRequestError::SoftwareStep(_)) => { - tracing::error!("{error}"); - DebugResponse::NotAllowed - } Err(e) => { tracing::error!("Error processing debug request: {:?}", e); return Err(HandleDebugError::ProcessRequest(e)); @@ -248,6 +235,8 @@ impl HyperlightVm { let pc = self.vm.regs().map_err(VmError::Register)?.pc; if self.sw_breakpoints.contains(pc) { self.begin_software_step(false, mem_access)?; + } else { + self.vm.set_single_step(false)?; } } #[cfg(target_arch = "x86_64")] @@ -402,16 +391,12 @@ impl HyperlightVm { let mut saved_instruction = vec![0; SW_BP_SIZE]; self.read_addrs(gva, &mut saved_instruction, mem_access)?; - if let Err(error) = self - .write_addrs(gva, &SW_BP, mem_access) - .and_then(|()| self.vm.register_sw_breakpoint(gva).map_err(Into::into)) - { + if let Err(error) = self.write_addrs(gva, &SW_BP, mem_access) { if let Err(rollback_error) = self.write_addrs(gva, &saved_instruction, mem_access) { tracing::error!( "Failed to roll back software breakpoint {gva:#x}: {rollback_error}" ); } - self.vm.unregister_sw_breakpoint(gva)?; return Err(error); } self.sw_breakpoints.insert(gva, saved_instruction); @@ -428,16 +413,12 @@ impl HyperlightVm { .get(gva) .ok_or(ProcessDebugRequestError::SwBreakpointNotFound(gva))? .to_vec(); - if let Err(error) = self - .write_addrs(gva, &saved_instruction, mem_access) - .and_then(|()| self.vm.unregister_sw_breakpoint(gva).map_err(Into::into)) - { + if let Err(error) = self.write_addrs(gva, &saved_instruction, mem_access) { if let Err(rollback_error) = self.write_addrs(gva, &SW_BP, mem_access) { tracing::error!( "Failed to roll back software breakpoint removal at {gva:#x}: {rollback_error}" ); } - self.vm.register_sw_breakpoint(gva)?; return Err(error); } self.sw_breakpoints.remove(gva); @@ -456,111 +437,35 @@ impl HyperlightVm { let regs = self.vm.regs().map_err(VmError::Register)?; let pc = regs.pc; - let saved_instruction = if let Some(instruction) = self.sw_breakpoints.get(pc) { - instruction.to_vec() - } else { - let mut instruction = vec![0; SW_BP_SIZE]; - self.read_addrs(pc, &mut instruction, mem_access)?; - instruction + let Some(saved_instruction) = self.sw_breakpoints.get(pc).map(<[u8]>::to_vec) else { + self.vm.set_single_step(true)?; + return Ok(()); }; - let instruction = u32::from_le_bytes( - saved_instruction - .as_slice() - .try_into() - .expect("ARM64 instructions are four bytes"), - ); - let targets = software_step_targets(pc, instruction, ®s)?; - - let mut temporary_breakpoints = HashMap::new(); - for &target in &targets { - if self.sw_breakpoints.contains(target) { - continue; - } - let mut target_instruction = vec![0; SW_BP_SIZE]; - self.read_addrs(target, &mut target_instruction, mem_access)?; - temporary_breakpoints.insert(target, target_instruction); - } - - let mut installed_targets = Vec::new(); - for &target in temporary_breakpoints.keys() { - let result = self - .write_addrs(target, &SW_BP, mem_access) - .and_then(|()| self.vm.register_sw_breakpoint(target).map_err(Into::into)); - if let Err(error) = result { - installed_targets.push(target); - self.rollback_temporary_breakpoints( - &installed_targets, - &temporary_breakpoints, - mem_access, - ); - return Err(error); - } - installed_targets.push(target); - } - - let reinsert_origin = self.sw_breakpoints.contains(pc); - if reinsert_origin { - let result = self - .write_addrs(pc, &saved_instruction, mem_access) - .and_then(|()| self.vm.unregister_sw_breakpoint(pc).map_err(Into::into)); - if let Err(error) = result { - if let Err(rollback_error) = self.write_addrs(pc, &SW_BP, mem_access) { - tracing::error!( - "Failed to roll back software-step origin {pc:#x}: {rollback_error}" - ); - } - if let Err(rollback_error) = self.vm.register_sw_breakpoint(pc) { - tracing::error!( - "Failed to re-register software-step origin {pc:#x}: {rollback_error}" - ); - } - self.rollback_temporary_breakpoints( - &installed_targets, - &temporary_breakpoints, - mem_access, + let result = self + .write_addrs(pc, &saved_instruction, mem_access) + .and_then(|()| self.vm.set_single_step(true).map_err(Into::into)); + if let Err(error) = result { + if let Err(rollback_error) = self.write_addrs(pc, &SW_BP, mem_access) { + tracing::error!( + "Failed to roll back breakpoint-step origin {pc:#x}: {rollback_error}" ); - return Err(error); } + return Err(error); } self.pending_software_step = Some(PendingSoftwareStep { origin: pc, - reinsert_origin, - targets, - temporary_breakpoints, report_stop, }); Ok(()) } - #[cfg(target_arch = "aarch64")] - fn rollback_temporary_breakpoints( - &mut self, - targets: &[u64], - temporary_breakpoints: &HashMap>, - mem_access: &DebugMemoryView<'_>, - ) { - for &target in targets { - if let Some(instruction) = temporary_breakpoints.get(&target) { - if let Err(error) = self.write_addrs(target, instruction, mem_access) { - tracing::error!( - "Failed to roll back temporary breakpoint {target:#x}: {error}" - ); - } - if let Err(error) = self.vm.unregister_sw_breakpoint(target) { - tracing::error!( - "Failed to unregister temporary breakpoint {target:#x}: {error}" - ); - } - } - } - } - #[cfg(target_arch = "aarch64")] fn cancel_software_step( &mut self, mem_access: &DebugMemoryView<'_>, ) -> std::result::Result<(), ProcessDebugRequestError> { + self.vm.set_single_step(false)?; let Some(pending) = self.pending_software_step.clone() else { return Ok(()); }; @@ -575,14 +480,7 @@ impl HyperlightVm { pending: &PendingSoftwareStep, mem_access: &DebugMemoryView<'_>, ) -> std::result::Result<(), ProcessDebugRequestError> { - for (target, instruction) in &pending.temporary_breakpoints { - self.write_addrs(*target, instruction, mem_access)?; - self.vm.unregister_sw_breakpoint(*target)?; - } - if pending.reinsert_origin { - self.write_addrs(pending.origin, &SW_BP, mem_access)?; - self.vm.register_sw_breakpoint(pending.origin)?; - } + self.write_addrs(pending.origin, &SW_BP, mem_access)?; Ok(()) } @@ -591,6 +489,9 @@ impl HyperlightVm { &mut self, mem_mgr: &crate::mem::mgr::SandboxMemoryManager, ) -> std::result::Result<(), ProcessDebugRequestError> { + if self.gdb_conn.is_none() { + return Ok(()); + } let mapped_regions = self.get_mapped_regions().cloned().collect(); let mem_access = DebugMemoryView::new(mem_mgr, mapped_regions); self.cancel_software_step(&mem_access) @@ -600,25 +501,20 @@ impl HyperlightVm { pub(super) fn finish_software_step( &mut self, mem_mgr: &crate::mem::mgr::SandboxMemoryManager, + stop_reason: VcpuStopReason, ) -> std::result::Result, ProcessDebugRequestError> { let pending = self .pending_software_step .clone() .expect("software-step completion requires pending state"); - let pc = self.vm.regs().map_err(VmError::Register)?.pc; - if !pending.targets.contains(&pc) { - return Ok(Some(VcpuStopReason::SwBp)); - } - let mapped_regions = self.get_mapped_regions().cloned().collect(); let mem_access = DebugMemoryView::new(mem_mgr, mapped_regions); self.restore_software_step(&pending, &mem_access)?; + self.vm.set_single_step(false)?; self.pending_software_step = None; - if self.sw_breakpoints.contains(pc) { - Ok(Some(VcpuStopReason::SwBp)) - } else if pending.report_stop { - Ok(Some(VcpuStopReason::DoneStep)) + if pending.report_stop || !matches!(stop_reason, VcpuStopReason::DoneStep) { + Ok(Some(stop_reason)) } else { Ok(None) } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs index d82876809..c02f25adc 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -744,7 +744,7 @@ impl HyperlightVm { } #[cfg(target_arch = "aarch64")] let stop_reason = if self.pending_software_step.is_some() { - match self.finish_software_step(mem_mgr) { + match self.finish_software_step(mem_mgr, stop_reason) { Ok(Some(reason)) => reason, Ok(None) => continue, Err(e) => break Err(HandleDebugError::from(e).into()), @@ -758,6 +758,9 @@ impl HyperlightVm { } Ok(VmExit::Halt()) => { + #[cfg(all(gdb, target_arch = "aarch64"))] + self.cancel_pending_software_step(mem_mgr) + .map_err(HandleDebugError::from)?; break Ok(()); } Ok(VmExit::IoOut(port, data)) => { @@ -813,8 +816,6 @@ impl HyperlightVm { continue; } - // Pending ARM64 software steps hold temporary breakpoints, so they - // are cancelled for any interruption source. #[cfg(all(gdb, target_arch = "aarch64"))] if let Err(e) = self.cancel_pending_software_step(mem_mgr) { break Err(HandleDebugError::from(e).into()); diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/whp_reg.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/whp_reg.rs index 4a7e01b30..6df723be7 100644 --- a/src/hyperlight_host/src/hypervisor/regs/aarch64/whp_reg.rs +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/whp_reg.rs @@ -31,6 +31,18 @@ pub(crate) const WHV_ARM64_REGISTER_MAIR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_N pub(crate) const WHV_ARM64_REGISTER_VBAR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0004000C); pub(crate) const WHV_ARM64_REGISTER_FPCR: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040012); pub(crate) const WHV_ARM64_REGISTER_FPSR: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040013); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_SPSR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040014); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_ELR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040015); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_ESR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040008); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_MDSCR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0005004D); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_DBGBCR0_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00050000); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_DBGBVR0_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00050020); pub(crate) const WHV_ARM64_REGISTER_GICR_BASE_GPA: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00063000); diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index 9f7a0c86d..5f0237029 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs @@ -159,7 +159,10 @@ pub(crate) enum VmExit { Unknown(String), /// The operation should be retried, for example this can happen on Linux where a call to run the CPU can return EAGAIN #[cfg_attr( - any(target_os = "windows", feature = "hw-interrupts"), + all( + any(target_os = "windows", feature = "hw-interrupts"), + not(all(gdb, target_arch = "aarch64")) + ), expect( dead_code, reason = "Retry() is never constructed on Windows or with hw-interrupts (EAGAIN causes continue instead)" diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index 123d04273..e4e4c2261 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -8,13 +8,13 @@ //! expose ARM64 WHP structures, we define our own FFI bindings derived from //! the Windows SDK header `WinHvPlatformDefs.h` (10.0.26100.0). -#[cfg(gdb)] -use std::collections::HashSet; use std::os::raw::c_void; use std::sync::atomic::Ordering; use std::sync::{Condvar, Mutex}; use std::time::{Duration, Instant}; +#[cfg(gdb)] +use hyperlight_common::arch::exn::DEBUG_EXCEPTION_HVC as DEBUG_EXCEPTION_IMMEDIATE; use hyperlight_common::layout::{ WHP_GICD_BASE_GPA, WHP_GICR_BASE_GPA, WHP_GITS_TRANSLATOR_BASE_GPA, }; @@ -25,8 +25,6 @@ use windows_result::HRESULT; use super::release_file_mapping; #[cfg(gdb)] -use crate::hypervisor::gdb::arch::SW_BP_IMMEDIATE; -#[cfg(gdb)] use crate::hypervisor::gdb::{DebugError, DebuggableVm, VcpuStopReason}; use crate::hypervisor::regs::whp_reg::*; use crate::hypervisor::regs::{ @@ -104,6 +102,17 @@ pub(crate) const DEBUG_CACHE_SYNC_GPA: u64 = 0x1000; #[cfg(gdb)] const DEBUG_CACHE_SYNC_IMMEDIATE: u16 = 0x4858; #[cfg(gdb)] +const MDSCR_MDE: u64 = 1 << 15; +#[cfg(gdb)] +const MDSCR_KDE: u64 = 1 << 13; +#[cfg(gdb)] +const PSTATE_SS: u64 = 1 << 21; +#[cfg(gdb)] +const PSTATE_D: u64 = 1 << 9; +// Armv8 guarantees at least two instruction breakpoint registers. +#[cfg(gdb)] +const HARDWARE_BREAKPOINTS: usize = 2; +#[cfg(gdb)] const DEBUG_CACHE_SYNC_CODE: [u8; 20] = [ 0x9f, 0x3b, 0x03, 0xd5, // dsb ish 0x1f, 0x75, 0x08, 0xd5, // ic iallu @@ -291,32 +300,6 @@ const _: [(); 24] = [(); core::mem::size_of::()]; #[cfg(gdb)] const _: [(); 176] = [(); core::mem::size_of::()]; -#[cfg(gdb)] -#[derive(Debug, PartialEq, Eq)] -enum Arm64HypercallClassification { - SoftwareBreakpoint, - Unknown { immediate: u16, pc: u64 }, -} - -#[cfg(gdb)] -fn classify_hypercall( - context: &Arm64HypercallContext, - debug_enabled: bool, - software_breakpoints: &HashSet, -) -> Arm64HypercallClassification { - if debug_enabled - && context.immediate == SW_BP_IMMEDIATE - && software_breakpoints.contains(&context.header.pc) - { - Arm64HypercallClassification::SoftwareBreakpoint - } else { - Arm64HypercallClassification::Unknown { - immediate: context.immediate, - pc: context.header.pc, - } - } -} - // ============================================================================ // Surrogate process guard (same as x86_64 version) // ============================================================================ @@ -408,7 +391,9 @@ pub(crate) struct WhpVm { #[cfg(gdb)] debug_enabled: bool, #[cfg(gdb)] - software_breakpoints: HashSet, + hardware_breakpoints: [Option; HARDWARE_BREAKPOINTS], + #[cfg(gdb)] + debug_exception_pending: bool, #[cfg(gdb)] _debug_cache_sync_host_memory: Option, #[cfg(gdb)] @@ -503,7 +488,9 @@ impl WhpVm { #[cfg(gdb)] debug_enabled: false, #[cfg(gdb)] - software_breakpoints: HashSet::new(), + hardware_breakpoints: [None; HARDWARE_BREAKPOINTS], + #[cfg(gdb)] + debug_exception_pending: false, #[cfg(gdb)] _debug_cache_sync_host_memory: None, #[cfg(gdb)] @@ -561,6 +548,33 @@ impl WhpVm { Ok(()) } + #[cfg(gdb)] + fn write_hardware_breakpoint( + &self, + slot: usize, + address: Option, + ) -> Result<(), RegisterError> { + self.set_registers( + &[ + WHV_REGISTER_NAME(WHV_ARM64_REGISTER_DBGBVR0_EL1.0 + slot as i32), + WHV_REGISTER_NAME(WHV_ARM64_REGISTER_DBGBCR0_EL1.0 + slot as i32), + ], + &[ + Align16(WHV_REGISTER_VALUE { + Reg64: address.unwrap_or(0), + }), + Align16(WHV_REGISTER_VALUE { + Reg64: if address.is_some() { + (15 << 5) | (1 << 1) | 1 + } else { + 0 + }, + }), + ], + ) + .map_err(RegisterError::SetRegs) + } + #[cfg(gdb)] fn initialize_debug_support(&mut self) -> Result<(), CreateVmError> { let mut memory = ExclusiveSharedMemory::new(page_size::get()) @@ -581,25 +595,31 @@ impl WhpVm { #[cfg(gdb)] fn run_debug_cache_sync(&mut self) -> std::result::Result<(), DebugError> { - let original_regs = self - .regs() - .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; - let original_sctlr_values = self - .get_registers(&[WHV_ARM64_REGISTER_SCTLR_EL1]) + let names = [ + WHV_ARM64_REGISTER_PC, + WHV_ARM64_REGISTER_PSTATE, + WHV_ARM64_REGISTER_SCTLR_EL1, + WHV_ARM64_REGISTER_MDSCR_EL1, + ]; + let original_values = self + .get_registers(&names) .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; - let original_sctlr = unsafe { original_sctlr_values[0].0.Reg64 }; let sync_result = (|| { - let mut sync_regs = original_regs; - sync_regs.pc = DEBUG_CACHE_SYNC_GPA; - sync_regs.pstate = 0b11 << 6 | 0b100; - self.set_regs(&sync_regs) - .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; + // SAFETY: These values were read as 64-bit system registers. + let mdscr = unsafe { original_values[3].0.Reg64 }; self.set_registers( - &[WHV_ARM64_REGISTER_SCTLR_EL1], - &[Align16(WHV_REGISTER_VALUE { - Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, - })], + &names, + &[ + Align16(WHV_REGISTER_VALUE { + Reg64: DEBUG_CACHE_SYNC_GPA, + }), + Align16(WHV_REGISTER_VALUE { Reg64: 0x3c5 }), + Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + }), + Align16(WHV_REGISTER_VALUE { Reg64: mdscr & !1 }), + ], ) .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; @@ -637,21 +657,12 @@ impl WhpVm { Ok(()) })(); - let restore_sctlr = self - .set_registers( - &[WHV_ARM64_REGISTER_SCTLR_EL1], - &[Align16(WHV_REGISTER_VALUE { - Reg64: original_sctlr, - })], - ) - .map_err(|e| DebugError::InstructionCacheSync(e.to_string())); - let restore_regs = self - .set_regs(&original_regs) + let restore = self + .set_registers(&names, &original_values) .map_err(|e| DebugError::InstructionCacheSync(e.to_string())); sync_result?; - restore_sctlr?; - restore_regs + restore } } @@ -767,6 +778,10 @@ impl VirtualMachine for WhpVm { ) -> Result { use arm64_exit_reasons::*; let mut exit_context = Arm64ExitContext::default(); + #[cfg(gdb)] + { + self.debug_exception_pending = false; + } unsafe { WHvRunVirtualProcessor( @@ -840,16 +855,36 @@ impl VirtualMachine for WhpVm { #[cfg(gdb)] WHV_EXIT_REASON_HYPERCALL => { let context = exit_context.hypercall_context(); - match classify_hypercall(&context, self.debug_enabled, &self.software_breakpoints) { - Arm64HypercallClassification::SoftwareBreakpoint => Ok(VmExit::Debug { - reason: VcpuStopReason::SwBp, - }), - Arm64HypercallClassification::Unknown { immediate, pc } => { - Ok(VmExit::Unknown(format!( - "Unsupported ARM64 hypercall immediate {immediate:#x} at PC={pc:#x}" - ))) + if self.debug_enabled && context.immediate == DEBUG_EXCEPTION_IMMEDIATE { + let values = self + .get_registers(&[WHV_ARM64_REGISTER_ESR_EL1]) + .map_err(RunVcpuError::Unknown)?; + // SAFETY: ESR_EL1 is a 64-bit system register. + let ec = unsafe { values[0].0.Reg64 } >> 26; + let reason = match ec { + 0x30 | 0x31 => Some(VcpuStopReason::HwBp), + 0x32 | 0x33 => Some(VcpuStopReason::DoneStep), + 0x3c => Some(VcpuStopReason::SwBp), + _ => None, + }; + // Debug events return through ERET. Other exceptions enter + // the ordinary prologue after the scratch-register restore. + let next_pc = context.header.pc + if reason.is_some() { 4 } else { 12 }; + self.set_registers( + &[WHV_ARM64_REGISTER_PC], + &[Align16(WHV_REGISTER_VALUE { Reg64: next_pc })], + ) + .map_err(RunVcpuError::IncrementRip)?; + if let Some(reason) = reason { + self.debug_exception_pending = true; + return Ok(VmExit::Debug { reason }); } + return Ok(VmExit::Retry()); } + Ok(VmExit::Unknown(format!( + "Unsupported ARM64 hypercall immediate {:#x} at PC={:#x}", + context.immediate, context.header.pc + ))) } WHV_EXIT_REASON_ARM64_RESET => Ok(VmExit::Halt()), WHV_EXIT_REASON_UNRECOVERABLE => { @@ -880,6 +915,11 @@ impl VirtualMachine for WhpVm { names[32] = WHV_ARM64_REGISTER_SP_EL0; names[33] = WHV_ARM64_REGISTER_SP_EL1; names[34] = WHV_ARM64_REGISTER_PSTATE; + #[cfg(gdb)] + if self.debug_exception_pending { + names[31] = WHV_ARM64_REGISTER_ELR_EL1; + names[34] = WHV_ARM64_REGISTER_SPSR_EL1; + } let values = self.get_registers(&names).map_err(RegisterError::GetRegs)?; @@ -924,6 +964,11 @@ impl VirtualMachine for WhpVm { values[32] = Align16(WHV_REGISTER_VALUE { Reg64: regs.sp }); names[33] = WHV_ARM64_REGISTER_PSTATE; values[33] = Align16(WHV_REGISTER_VALUE { Reg64: regs.pstate }); + #[cfg(gdb)] + if self.debug_exception_pending { + names[31] = WHV_ARM64_REGISTER_ELR_EL1; + names[33] = WHV_ARM64_REGISTER_SPSR_EL1; + } self.set_registers(&names, &values) .map_err(RegisterError::SetRegs) @@ -1075,6 +1120,22 @@ impl VirtualMachine for WhpVm { ) .map_err(|e| ResetVcpuError::Hypervisor(e.into()))?; } + #[cfg(gdb)] + { + self.debug_exception_pending = false; + if self.debug_enabled { + self.set_registers( + &[WHV_ARM64_REGISTER_MDSCR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: MDSCR_MDE | MDSCR_KDE, + })], + ) + .map_err(RegisterError::SetRegs)?; + for (slot, address) in self.hardware_breakpoints.iter().enumerate() { + self.write_hardware_breakpoint(slot, *address)?; + } + } + } Ok(()) } @@ -1126,23 +1187,88 @@ impl DebuggableVm for WhpVm { } fn set_debug(&mut self, enable: bool) -> std::result::Result<(), DebugError> { + if !enable { + self.set_single_step(false)?; + for slot in 0..HARDWARE_BREAKPOINTS { + self.write_hardware_breakpoint(slot, None)?; + } + self.hardware_breakpoints.fill(None); + } + let values = self + .get_registers(&[WHV_ARM64_REGISTER_MDSCR_EL1]) + .map_err(RegisterError::GetRegs)?; + // SAFETY: MDSCR_EL1 is a 64-bit system register. + let value = unsafe { values[0].0.Reg64 }; + let value = if enable { + value | MDSCR_MDE | MDSCR_KDE + } else { + value & !(MDSCR_MDE | MDSCR_KDE | 1) + }; + self.set_registers( + &[WHV_ARM64_REGISTER_MDSCR_EL1], + &[Align16(WHV_REGISTER_VALUE { Reg64: value })], + ) + .map_err(RegisterError::SetRegs)?; self.debug_enabled = enable; Ok(()) } - fn sync_instruction_cache(&mut self) -> std::result::Result<(), DebugError> { - self.run_debug_cache_sync() + fn set_single_step(&mut self, enable: bool) -> std::result::Result<(), DebugError> { + let mut regs = self.regs()?; + regs.pstate = if enable { + (regs.pstate | PSTATE_SS) & !PSTATE_D + } else { + regs.pstate & !PSTATE_SS + }; + self.set_regs(®s)?; + let values = self + .get_registers(&[WHV_ARM64_REGISTER_MDSCR_EL1]) + .map_err(RegisterError::GetRegs)?; + // SAFETY: MDSCR_EL1 is a 64-bit system register. + let value = unsafe { values[0].0.Reg64 }; + self.set_registers( + &[WHV_ARM64_REGISTER_MDSCR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: if enable { value | 1 } else { value & !1 }, + })], + ) + .map_err(RegisterError::SetRegs)?; + Ok(()) } - fn register_sw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> { - self.software_breakpoints.insert(addr); + fn add_hw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> { + if self.hardware_breakpoints.contains(&Some(addr)) { + return Ok(()); + } + if !addr.is_multiple_of(4) { + return Err(DebugError::Unsupported( + "unaligned ARM64 hardware breakpoint", + )); + } + let slot = self + .hardware_breakpoints + .iter() + .position(Option::is_none) + .ok_or(DebugError::TooManyHwBreakpoints(HARDWARE_BREAKPOINTS))?; + self.write_hardware_breakpoint(slot, Some(addr))?; + self.hardware_breakpoints[slot] = Some(addr); Ok(()) } - fn unregister_sw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> { - self.software_breakpoints.remove(&addr); + fn remove_hw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> { + let slot = self + .hardware_breakpoints + .iter() + .position(|&value| value == Some(addr)) + .ok_or(DebugError::HwBreakpointNotFound(addr))?; + self.write_hardware_breakpoint(slot, None)?; + self.hardware_breakpoints[slot] = None; Ok(()) } + + fn sync_instruction_cache(&mut self) -> std::result::Result<(), DebugError> { + self.run_debug_cache_sync() + } } #[cfg(all(test, gdb))] @@ -1169,30 +1295,9 @@ mod debug_tests { } } - #[test] - fn classifies_only_tracked_debugger_hypercall() { - let breakpoints = HashSet::from([0x4000]); - assert_eq!( - classify_hypercall(&context(SW_BP_IMMEDIATE, 0x4000), true, &breakpoints), - Arm64HypercallClassification::SoftwareBreakpoint - ); - assert!(matches!( - classify_hypercall(&context(SW_BP_IMMEDIATE, 0x4004), true, &breakpoints), - Arm64HypercallClassification::Unknown { .. } - )); - assert!(matches!( - classify_hypercall(&context(0, 0x4000), true, &breakpoints), - Arm64HypercallClassification::Unknown { .. } - )); - assert!(matches!( - classify_hypercall(&context(SW_BP_IMMEDIATE, 0x4000), false, &breakpoints), - Arm64HypercallClassification::Unknown { .. } - )); - } - #[test] fn parses_arm64_hypercall_context_layout() { - let expected = context(SW_BP_IMMEDIATE, 0x1234); + let expected = context(DEBUG_EXCEPTION_IMMEDIATE, 0x1234); let mut exit = Arm64ExitContext::default(); // SAFETY: `payload` is larger than `Arm64HypercallContext`. The write is // unaligned and does not outlive the payload. @@ -1213,6 +1318,201 @@ mod debug_tests { vm.sync_instruction_cache().unwrap(); } + #[test] + #[serial] + fn asynchronous_vectors_ignore_stale_debug_syndromes() { + if !is_hypervisor_present() { + return; + } + + let bytes = std::fs::read(hyperlight_testing::simple_guest_as_pathbuf()).unwrap(); + let elf = goblin::elf::Elf::parse(&bytes).unwrap(); + let address = elf + .syms + .iter() + .find(|symbol| elf.strtab.get_at(symbol.st_name) == Some("hyperlight_debug_vectors")) + .unwrap() + .st_value; + let segment = elf + .program_headers + .iter() + .find(|segment| { + segment.p_type == goblin::elf::program_header::PT_LOAD + && address >= segment.p_vaddr + && address + 0x800 <= segment.p_vaddr + segment.p_filesz + }) + .unwrap(); + let offset = (segment.p_offset + address - segment.p_vaddr) as usize; + let mut vectors = bytes[offset..offset + 0x800].to_vec(); + let mut handler_stops = Vec::new(); + for (index, vector) in vectors.chunks_exact_mut(0x80).enumerate() { + // Stop at the ordinary handler's context-save call. + let call = vector + .chunks_exact(4) + .position(|instruction| { + u32::from_le_bytes(instruction.try_into().unwrap()) & 0xfc00_0000 == 0x9400_0000 + }) + .unwrap() + * 4; + vector[call..call + 4].copy_from_slice(&(0xd400_0002u32 | (0x485b << 5)).to_le_bytes()); + handler_stops.push(DEBUG_CACHE_SYNC_GPA + 0x800 + (index * 0x80 + call) as u64); + } + + let mut vm = WhpVm::new().unwrap(); + vm._debug_cache_sync_host_memory + .as_ref() + .unwrap() + .copy_from_slice(&vectors, 0x800) + .unwrap(); + vm.sync_instruction_cache().unwrap(); + vm.set_registers( + &[WHV_ARM64_REGISTER_SCTLR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + })], + ) + .unwrap(); + vm.set_debug(true).unwrap(); + + for syndrome in [0x31u64 << 26, 0x33u64 << 26, 0x3cu64 << 26] { + for (index, &handler_pc) in handler_stops.iter().enumerate() { + if index % 4 == 0 { + continue; + } + let mut regs = CommonRegisters { + pc: DEBUG_CACHE_SYNC_GPA + 0x800 + (index * 0x80) as u64, + sp: DEBUG_CACHE_SYNC_GPA + 0x800, + pstate: 0x3c5, + ..Default::default() + }; + regs.x[0] = 0x1234; + regs.x[1] = 0x5678; + vm.set_regs(®s).unwrap(); + vm.set_registers( + &[WHV_ARM64_REGISTER_ESR_EL1], + &[Align16(WHV_REGISTER_VALUE { Reg64: syndrome })], + ) + .unwrap(); + match vm.run_vcpu().unwrap() { + VmExit::Unknown(message) => assert!( + message.starts_with("Unsupported ARM64 hypercall immediate 0x485b "), + "{message}" + ), + _ => panic!("vector {index} forwarded stale syndrome {syndrome:#x}"), + } + let stopped = vm.regs().unwrap(); + assert_eq!(stopped.pc, handler_pc); + assert_eq!(stopped.x, regs.x); + } + } + } + + #[test] + #[serial] + fn hardware_debug_preserves_context_and_steps_control_flow() { + if !is_hypervisor_present() { + return; + } + + let mut vm = WhpVm::new().unwrap(); + let mut code = vec![0u8; page_size::get()]; + for (offset, instruction) in [ + (0x20, 0xd503_201fu32), + (0x24, 0x1400_0000), // b . + (0x28, 0xd65f_03c0), // ret + (0x800, 0xd409_0b42), // hvc #0x485a + (0x804, 0xd69f_03e0), // eret + ] { + code[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes()); + } + vm._debug_cache_sync_host_memory + .as_ref() + .unwrap() + .copy_from_slice(&code[0x20..], 0x20) + .unwrap(); + vm.sync_instruction_cache().unwrap(); + vm.set_registers( + &[ + WHV_ARM64_REGISTER_SCTLR_EL1, + WHV_ARM64_REGISTER_VBAR_EL1, + WHV_ARM64_REGISTER_SP_EL1, + ], + &[ + Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + }), + Align16(WHV_REGISTER_VALUE { + Reg64: DEBUG_CACHE_SYNC_GPA + 0x800, + }), + Align16(WHV_REGISTER_VALUE { Reg64: 0x9870 }), + ], + ) + .unwrap(); + let mut regs = CommonRegisters { + pc: DEBUG_CACHE_SYNC_GPA + 0x20, + sp: 0x4560, + pstate: 0xc4, + ..Default::default() + }; + regs.x[9] = 0xfeed; + regs.x[30] = DEBUG_CACHE_SYNC_GPA + 0x20; + vm.set_regs(®s).unwrap(); + vm.set_debug(true).unwrap(); + vm.add_hw_breakpoint(regs.pc).unwrap(); + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::HwBp + } + )); + let stopped = vm.regs().unwrap(); + assert_eq!( + (stopped.pc, stopped.sp, stopped.pstate, stopped.x), + (regs.pc, regs.sp, regs.pstate, regs.x) + ); + + let special = vm.sregs().unwrap(); + vm.reset_vcpu().unwrap(); + vm.set_sregs(&special).unwrap(); + vm.set_regs(®s).unwrap(); + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::HwBp + } + )); + vm.sync_instruction_cache().unwrap(); + assert_eq!(vm.regs().unwrap().sp, regs.sp); + let values = vm.get_registers(&[WHV_ARM64_REGISTER_SP_EL1]).unwrap(); + // SAFETY: SP_EL1 is a 64-bit stack-pointer register. + assert_eq!(unsafe { values[0].0.Reg64 }, 0x9870); + vm.remove_hw_breakpoint(regs.pc).unwrap(); + regs.pc += 4; + regs.x[9] = 0xbeef; + vm.set_regs(®s).unwrap(); + vm.set_single_step(true).unwrap(); + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::DoneStep + } + )); + assert_eq!(vm.regs().unwrap().pc, regs.pc); + assert_eq!(vm.regs().unwrap().x[9], 0xbeef); + + regs.pc += 4; + vm.set_regs(®s).unwrap(); + vm.set_single_step(true).unwrap(); + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::DoneStep + } + )); + assert_eq!(vm.regs().unwrap().pc, regs.x[30]); + vm.set_debug(false).unwrap(); + } + #[test] #[serial] fn patched_software_breakpoint_is_runnable_without_pc_advance() { @@ -1226,18 +1526,22 @@ mod debug_tests { host_memory .copy_from_slice(&SW_BP, BREAKPOINT_OFFSET) .unwrap(); + host_memory + .copy_from_slice( + &(0xd400_0002 | (u32::from(DEBUG_EXCEPTION_IMMEDIATE) << 5)).to_le_bytes(), + 0x800, + ) + .unwrap(); + host_memory + .copy_from_slice(&0xd69f_03e0u32.to_le_bytes(), 0x804) + .unwrap(); // SAFETY: The current process handle is valid and the range is inside // the live shared-memory mapping. unsafe { FlushInstructionCache( GetCurrentProcess(), - Some( - host_memory - .base_ptr() - .wrapping_add(BREAKPOINT_OFFSET) - .cast::(), - ), - SW_BP.len(), + Some(host_memory.base_ptr().cast::()), + page_size::get(), ) .unwrap(); } @@ -1245,12 +1549,16 @@ mod debug_tests { let breakpoint_address = DEBUG_CACHE_SYNC_GPA + BREAKPOINT_OFFSET as u64; vm.set_debug(true).unwrap(); - vm.register_sw_breakpoint(breakpoint_address).unwrap(); vm.set_registers( - &[WHV_ARM64_REGISTER_SCTLR_EL1], - &[Align16(WHV_REGISTER_VALUE { - Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, - })], + &[WHV_ARM64_REGISTER_SCTLR_EL1, WHV_ARM64_REGISTER_VBAR_EL1], + &[ + Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + }), + Align16(WHV_REGISTER_VALUE { + Reg64: DEBUG_CACHE_SYNC_GPA + 0x800, + }), + ], ) .unwrap(); vm.set_regs(&CommonRegisters { diff --git a/src/hyperlight_host/src/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index f8981883b..11cef5ddd 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -88,6 +88,8 @@ pub(crate) struct ElfInfo { /// The hyperlight version string embedded by `hyperlight-guest-bin`, if /// present. Used to detect version/ABI mismatches between guest and host. guest_bin_version: Option, + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors: Option, } #[cfg(feature = "mem_profile")] @@ -248,6 +250,11 @@ impl ElfInfo { // Look for the hyperlight version note embedded by // hyperlight-guest-bin. let guest_bin_version = Self::read_version_note(&elf, &bytes); + #[cfg(all(gdb, target_arch = "aarch64"))] + let debug_vectors = elf.syms.iter().find_map(|symbol| { + (elf.strtab.get_at(symbol.st_name) == Some("hyperlight_debug_vectors")) + .then_some(symbol.st_value) + }); let phdrs = std::mem::take(&mut elf.program_headers); let entry = elf.entry; @@ -279,6 +286,8 @@ impl ElfInfo { relocs, is_pie, guest_bin_version, + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors, }) } @@ -410,11 +419,23 @@ impl ElfInfo { } } } + #[cfg(all(gdb, target_arch = "aarch64"))] + let debug_vectors = self + .debug_vectors + .map(|address| { + address + .checked_sub(base_va) + .and_then(|offset| load_gva.checked_add(offset)) + .ok_or_else(|| new_error!("ARM64 debug vector address overflows")) + }) + .transpose()?; cfg_if::cfg_if! { if #[cfg(feature = "mem_profile")] { let va_size = self.get_va_size() as u64; let base_svma = self.get_base_va(); Ok(LoadInfo { + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors, info: Arc::new(UnwindInfo { payload: self.payload, load_addr: load_gva, @@ -424,7 +445,10 @@ impl ElfInfo { }) }) } else { - Ok(LoadInfo {}) + Ok(LoadInfo { + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors, + }) } } } diff --git a/src/hyperlight_host/src/mem/exe.rs b/src/hyperlight_host/src/mem/exe.rs index aedb85b1a..7a8746b72 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -48,6 +48,8 @@ impl framehop::ModuleSectionInfo for &DummyUnwindInfo { #[derive(Clone)] pub(crate) struct LoadInfo { + #[cfg(all(gdb, target_arch = "aarch64"))] + pub(crate) debug_vectors: Option, #[cfg(feature = "mem_profile")] pub(crate) info: Arc, } @@ -55,6 +57,8 @@ pub(crate) struct LoadInfo { impl LoadInfo { pub(crate) fn dummy() -> Self { LoadInfo { + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors: None, #[cfg(feature = "mem_profile")] info: Arc::new(DummyUnwindInfo {}), } diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index 8668b8f83..96adc8482 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -128,7 +128,7 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg(feature = "mem_profile")] let trace_info = MemTraceInfo::new(_load_info.info)?; - Ok(HyperlightVm::new( + let vm = HyperlightVm::new( mgr.shared_mem, mgr.scratch_mem, mgr.layout.get_pt_base_gpa(), @@ -141,7 +141,15 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg(feature = "mem_profile")] trace_info, ) - .map_err(HyperlightVmError::Create)?) + .map_err(HyperlightVmError::Create)?; + #[cfg(all(gdb, target_arch = "aarch64"))] + let vm = { + let mut vm = vm; + vm.initialize_debug_vectors(_load_info.debug_vectors) + .map_err(HyperlightVmError::Create)?; + vm + }; + Ok(vm) } #[cfg(test)] From 00b72128ef72e851e950cf793e1214aeb1c1e875 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Tue, 3 Nov 2026 11:24:06 -0800 Subject: [PATCH 4/7] fix(aarch64): preserve GDB recovery after guest failures Poisoned sandbox restore discards software breakpoint state. Crash detach disables backend debugging even when breakpoint memory is unavailable. Snapshot checkpoints reject installed software breakpoints before replacing memory. The WHP cache-sync page permits guest reads and execution. Exception-vector tests use a separate writable stack. Cache flushing resolves layered snapshot memory through the guest memory view. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- CHANGELOG.md | 2 + Justfile | 2 +- docs/how-to-debug-a-hyperlight-guest.md | 4 + src/hyperlight_host/src/hypervisor/gdb/mod.rs | 27 +-- .../src/hypervisor/hyperlight_vm/aarch64.rs | 6 + .../src/hypervisor/hyperlight_vm/debug.rs | 14 +- .../hypervisor/virtual_machine/whp/aarch64.rs | 71 +++++- src/hyperlight_host/src/mem/mgr.rs | 4 +- .../src/sandbox/initialized.rs | 222 +++++++++++++++++- .../src/sandbox/snapshot/memory/backing.rs | 2 +- 10 files changed, 322 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f4b7adc6..831998659 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +* ARM64 GDB snapshot recovery handles cancellation and crash detach. Snapshot checkpointing rejects software breakpoints before replacing memory. +* WHP ARM64 cache synchronization code is read-only to guests. * Guest libc accepts null buffers for zero-length writes. * Fix calls to guest functions with more than 11 parameters. Registration accepts up to 32, but every call with more than 11 failed. diff --git a/Justfile b/Justfile index fbb2b83ce..e20a52adb 100644 --- a/Justfile +++ b/Justfile @@ -331,7 +331,7 @@ test-no-surrogate target=default-target: # runs tests that exercise gdb debugging test-rust-gdb-debugging target=default-target features="": {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example guest-debugging {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} - {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- hypervisor::gdb:: + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- hypervisor::gdb:: gdb_recovery_tests:: hypervisor::virtual_machine::whp::aarch64::debug_tests:: # rust test for crashdump test-rust-crashdump target=default-target features="": diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index e1572d42d..bd207af9c 100644 --- a/docs/how-to-debug-a-hyperlight-guest.md +++ b/docs/how-to-debug-a-hyperlight-guest.md @@ -23,6 +23,10 @@ exceptions to WHP through HVC. ARM64 guest binaries must retain the `hyperlight_debug_vectors` symbol so the host can install the vectors before the initial debug stop. Watchpoints are not advertised. +ARM64 snapshots reject installed software breakpoints, including those added +during checkpointing. Restoring a poisoned sandbox discards its software +breakpoint state and restores clean snapshot memory. + ## Expected behavior Below is a list describing some cases of expected behavior from a gdb debug diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 7e1dc02e9..00bed232e 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -164,32 +164,19 @@ impl<'a> DebugMemoryView<'a> { ) -> std::result::Result<(), DebugMemoryAccessError> { #[cfg(all(target_arch = "aarch64", target_os = "windows"))] { - use std::ffi::c_void; - use windows::Win32::System::Diagnostics::Debug::FlushInstructionCache; use windows::Win32::System::Threading::GetCurrentProcess; - use crate::mem::shared_mem::SharedMemory; - - let resolved = self + let range = self .mem_mgr - .layout - .resolve_gpa(gpa, &self.guest_mmap_regions) - .ok_or(DebugMemoryAccessError::TranslateGuestAddress(gpa))?; - let address = match resolved.base { - BaseGpaRegion::Snapshot(()) => self.mem_mgr.shared_mem.base_ptr(), - BaseGpaRegion::Scratch(()) => self.mem_mgr.scratch_mem.base_ptr(), - BaseGpaRegion::Mmap(region) => { - let base: usize = region.host_region.start.into(); - base as *mut u8 - } - } - .wrapping_add(resolved.offset); + .with_guest_memory(&self.guest_mmap_regions, |memory| { + memory.host_range(gpa, len) + }) + .map_err(|e| DebugMemoryAccessError::CopyFailed(Box::new(e)))?; - // SAFETY: The current process handle is valid and `address..address - // + len` lies inside a live sandbox memory mapping. + // SAFETY: The current process handle is valid and the range belongs to a live sandbox mapping. unsafe { - FlushInstructionCache(GetCurrentProcess(), Some(address.cast::()), len) + FlushInstructionCache(GetCurrentProcess(), Some(range.start as *const _), len) .map_err(|e| DebugMemoryAccessError::FlushInstructionCache(e.to_string()))?; } } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index d3ea63c82..9c0fdffec 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -249,6 +249,12 @@ impl HyperlightVm { self.vm_can_reset_vcpu, "No fallback path for vcpu reset on aarch64" ); + #[cfg(gdb)] + if self.gdb_conn.is_some() { + self.vm + .set_single_step(false) + .map_err(|error| ResetVcpuError::Unknown(error.to_string()))?; + } self.vm.reset_vcpu()?; self.apply_sregs(cr3, sregs)?; Ok(()) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs index 95ca24d9d..3ca48680d 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs @@ -67,6 +67,13 @@ impl HyperlightVm { !self.sw_breakpoints.0.is_empty() } + /// Discard software debug state when poisoned guest memory will be replaced. + #[cfg(target_arch = "aarch64")] + pub(crate) fn discard_software_breakpoints(&mut self) { + self.sw_breakpoints.0.clear(); + self.pending_software_step = None; + } + pub(super) fn handle_debug( &mut self, mem_mgr: &crate::mem::mgr::SandboxMemoryManager, @@ -92,13 +99,16 @@ impl HyperlightVm { let response = match req { DebugMsg::DisableDebug => { detach = true; - // Remove software breakpoint state so restore does not reject recovery. #[cfg(target_arch = "aarch64")] match self.process_dbg_request(DebugMsg::DisableDebug, &mem_access) { Ok(response) => response, Err(error) => { tracing::error!("Failed to clean up after detach: {error}"); - DebugResponse::ErrorOccurred + self.vm + .set_debug(false) + .map_err(ProcessDebugRequestError::from)?; + self.discard_software_breakpoints(); + DebugResponse::DisableDebug } } #[cfg(not(target_arch = "aarch64"))] diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index e4e4c2261..0f1ddcb41 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -583,7 +583,8 @@ impl WhpVm { .copy_from_slice(&DEBUG_CACHE_SYNC_CODE, 0) .map_err(|e| CreateVmError::InitializeDebug(e.to_string()))?; let (host_memory, guest_memory) = memory.build(); - let region = guest_memory.mapping_at(DEBUG_CACHE_SYNC_GPA, MemoryRegionType::Scratch); + let mut region = guest_memory.mapping_at(DEBUG_CACHE_SYNC_GPA, MemoryRegionType::Scratch); + region.flags.remove(MemoryRegionFlags::WRITE); // SAFETY: Both shared-memory handles are retained for the VM lifetime. unsafe { self.map_memory((u32::MAX, ®ion)) } @@ -1318,6 +1319,54 @@ mod debug_tests { vm.sync_instruction_cache().unwrap(); } + #[test] + #[serial] + fn guest_cannot_write_cache_sync_trampoline() { + let mut vm = WhpVm::new().unwrap(); + let memory = vm._debug_cache_sync_host_memory.as_ref().unwrap(); + let mut original = [0; 4]; + memory.copy_to_slice(&mut original, 0).unwrap(); + let mut code = Vec::new(); + for instruction in [ + 0xb900_0001u32, // str w1, [x0] + 0xd400_0002 | (0x485b << 5), // hvc #0x485b + ] { + code.extend_from_slice(&instruction.to_le_bytes()); + } + memory.copy_from_slice(&code, 0x20).unwrap(); + vm.sync_instruction_cache().unwrap(); + vm.set_registers( + &[WHV_ARM64_REGISTER_SCTLR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + })], + ) + .unwrap(); + let mut regs = CommonRegisters { + pc: DEBUG_CACHE_SYNC_GPA + 0x20, + pstate: 0x3c5, + ..Default::default() + }; + regs.x[0] = DEBUG_CACHE_SYNC_GPA; + regs.x[1] = u64::from(!u32::from_le_bytes(original)); + vm.set_regs(®s).unwrap(); + let exit = vm.run_vcpu().unwrap(); + assert!( + matches!(&exit, VmExit::Unknown(message) if message.starts_with("Unsupported ARM64 hypercall immediate 0x485b ")) + || matches!(exit, VmExit::MmioWrite(DEBUG_CACHE_SYNC_GPA)) + ); + let mut actual = [0; 4]; + vm._debug_cache_sync_host_memory + .as_ref() + .unwrap() + .copy_to_slice(&mut actual, 0) + .unwrap(); + assert_eq!( + actual, original, + "guest execution overwrote the cache-sync trampoline" + ); + } + #[test] #[serial] fn asynchronous_vectors_ignore_stale_debug_syndromes() { @@ -1345,12 +1394,14 @@ mod debug_tests { let offset = (segment.p_offset + address - segment.p_vaddr) as usize; let mut vectors = bytes[offset..offset + 0x800].to_vec(); let mut handler_stops = Vec::new(); - for (index, vector) in vectors.chunks_exact_mut(0x80).enumerate() { + for (index, vector) in vectors.as_chunks_mut::<0x80>().0.iter_mut().enumerate() { // Stop at the ordinary handler's context-save call. let call = vector - .chunks_exact(4) + .as_chunks::<4>() + .0 + .iter() .position(|instruction| { - u32::from_le_bytes(instruction.try_into().unwrap()) & 0xfc00_0000 == 0x9400_0000 + u32::from_le_bytes(*instruction) & 0xfc00_0000 == 0x9400_0000 }) .unwrap() * 4; @@ -1358,7 +1409,17 @@ mod debug_tests { handler_stops.push(DEBUG_CACHE_SYNC_GPA + 0x800 + (index * 0x80 + call) as u64); } + let (_stack_host_memory, stack_guest_memory) = ExclusiveSharedMemory::new(page_size::get()) + .unwrap() + .build(); + let mut stack_region = stack_guest_memory.mapping_at( + DEBUG_CACHE_SYNC_GPA - page_size::get() as u64, + MemoryRegionType::Scratch, + ); + stack_region.flags.remove(MemoryRegionFlags::EXECUTE); let mut vm = WhpVm::new().unwrap(); + // SAFETY: Both stack handles outlive the VM. + unsafe { vm.map_memory((u32::MAX - 1, &stack_region)) }.unwrap(); vm._debug_cache_sync_host_memory .as_ref() .unwrap() @@ -1381,7 +1442,7 @@ mod debug_tests { } let mut regs = CommonRegisters { pc: DEBUG_CACHE_SYNC_GPA + 0x800 + (index * 0x80) as u64, - sp: DEBUG_CACHE_SYNC_GPA + 0x800, + sp: DEBUG_CACHE_SYNC_GPA, pstate: 0x3c5, ..Default::default() }; diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index b1da7c790..14759760d 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -232,8 +232,8 @@ impl<'a> GuestPhysicalMemoryView<'a> { Ok(()) } - #[cfg(crashdump)] - fn host_range(&self, gpa: u64, len: usize) -> Result> { + #[cfg(any(crashdump, all(gdb, target_arch = "aarch64")))] + pub(crate) fn host_range(&self, gpa: u64, len: usize) -> Result> { let backing = self .resolve(gpa, len) .ok_or_else(|| new_error!("GPA range is not backed: {gpa:#x}"))?; diff --git a/src/hyperlight_host/src/sandbox/initialized.rs b/src/hyperlight_host/src/sandbox/initialized.rs index 37e78dced..141c990a2 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -496,6 +496,12 @@ impl Sandbox { fn capture_snapshot(&mut self, scratch_size: usize) -> Result> { if self.transport_dirty { self.checkpoint_transport_for_snapshot()?; + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.vm.has_sw_breakpoints() { + return Err(crate::new_error!( + "Cannot snapshot an ARM64 sandbox while software breakpoints are installed" + )); + } } let mapped_regions_iter = self.vm.get_mapped_regions(); @@ -717,7 +723,7 @@ impl Sandbox { } #[cfg(all(gdb, target_arch = "aarch64"))] - if self.vm.has_sw_breakpoints() { + if self.status.is_ready() && self.vm.has_sw_breakpoints() { return Err(crate::new_error!( "Cannot restore an ARM64 sandbox while software breakpoints are installed" )); @@ -771,6 +777,10 @@ impl Sandbox { } // Errors below leave the sandbox poisoned unless base mapping updates make it unrecoverable. + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.status.is_poisoned() { + self.vm.discard_software_breakpoints(); + } self.status = SandboxStatus::Poisoned; self.snapshot = None; @@ -1420,6 +1430,216 @@ mod tests { SnapshotOptions::default().scratch_size(scratch_size) } + #[cfg(all(gdb, target_arch = "aarch64", target_os = "windows"))] + mod gdb_recovery_tests { + use super::*; + use crate::hypervisor::gdb::{ + DebugCommChannel, DebugMemoryView, DebugMsg, DebugResponse, VcpuStopReason, + }; + use crate::hypervisor::hyperlight_vm::HyperlightVm; + use crate::mem::ptr::RawPtr; + use crate::sandbox::snapshot::NextAction; + + fn sandbox_with_debugger() -> (Sandbox, DebugCommChannel, u64) { + let guest = + UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None) + .unwrap(); + let vectors = guest.load_info.debug_vectors; + let (mut host, memory) = guest.mgr.build().unwrap(); + let (debugger, connection) = DebugCommChannel::unbounded(); + debugger.send(DebugMsg::Continue).unwrap(); + let mut vm = HyperlightVm::new( + memory.shared_mem, + memory.scratch_mem, + memory.layout.get_pt_base_gpa(), + memory.next_action, + guest.stack_top_gva, + page_size::get(), + &guest.config, + Some(connection), + #[cfg(feature = "mem_profile")] + crate::sandbox::trace::MemTraceInfo::new(guest.load_info.info).unwrap(), + ) + .unwrap(); + vm.initialize_debug_vectors(vectors).unwrap(); + vm.initialise( + RawPtr::from(host.layout.peb_address() as u64), + 0, + &mut host, + &guest.host_funcs, + None, + ) + .unwrap(); + host.finish_snapshot_checkpoint().unwrap(); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::InterruptHandle(_) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::VcpuStopped(VcpuStopReason::Initial) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::Continue + )); + + let bytes = std::fs::read(simple_guest_as_pathbuf()).unwrap(); + let elf = goblin::elf::Elf::parse(&bytes).unwrap(); + let entry = elf + .syms + .iter() + .find(|symbol| elf.strtab.get_at(symbol.st_name) == Some("hyperlight_main")) + .unwrap() + .st_value + + host.layout.get_guest_code_gva() as u64; + ( + Sandbox::from_uninit(guest.host_funcs, host, vm), + debugger, + entry, + ) + } + + fn request(sandbox: &mut Sandbox, message: DebugMsg) -> DebugResponse { + let memory = DebugMemoryView::new( + &sandbox.mem_mgr, + sandbox.vm.get_mapped_regions().cloned().collect(), + ); + sandbox.vm.process_dbg_request(message, &memory).unwrap() + } + + #[test] + fn snapshot_rejects_breakpoints_added_during_checkpoint_without_poisoning() { + let (mut sandbox, debugger, breakpoint) = sandbox_with_debugger(); + let snapshot = sandbox.snapshot().unwrap(); + sandbox.call::<()>("NoOp", ()).unwrap(); + assert!(sandbox.transport_dirty); + let NextAction::Call(dispatch) = sandbox.vm.get_next_action() else { + panic!("guest dispatch is not initialized"); + }; + assert!(matches!( + request(&mut sandbox, DebugMsg::AddHwBreakpoint(dispatch)), + DebugResponse::AddHwBreakpoint(true) + )); + debugger + .send(DebugMsg::RemoveHwBreakpoint(dispatch)) + .unwrap(); + debugger + .send(DebugMsg::AddSwBreakpoint(breakpoint)) + .unwrap(); + debugger.send(DebugMsg::Continue).unwrap(); + + let result = sandbox.snapshot(); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::VcpuStopped(VcpuStopReason::HwBp) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::RemoveHwBreakpoint(true) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::AddSwBreakpoint(true) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::Continue + )); + let error = result + .err() + .expect("snapshot accepted a software breakpoint"); + assert_eq!(sandbox.status(), SandboxStatus::Ready, "{error}"); + assert!(matches!( + request(&mut sandbox, DebugMsg::RemoveSwBreakpoint(breakpoint)), + DebugResponse::RemoveSwBreakpoint(true) + )); + sandbox.restore(snapshot).unwrap(); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + + #[test] + fn cancelled_debug_session_can_restore_with_software_breakpoints() { + let (mut sandbox, _debugger, breakpoint) = sandbox_with_debugger(); + let interrupt = sandbox.interrupt_handle(); + sandbox + .register_host_function("HostNoOp", move || { + interrupt.kill(); + Ok(()) + }) + .unwrap(); + let snapshot = sandbox.snapshot().unwrap(); + assert!(matches!( + request(&mut sandbox, DebugMsg::AddSwBreakpoint(breakpoint)), + DebugResponse::AddSwBreakpoint(true) + )); + assert!(sandbox.restore(snapshot.clone()).is_err()); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + + let error = sandbox.call::<()>("RoundTripHostNoOp", ()).unwrap_err(); + assert!(matches!(error, HyperlightError::ExecutionCanceledByHost())); + assert_eq!(sandbox.status(), SandboxStatus::Poisoned); + sandbox.restore(snapshot).unwrap(); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + + #[test] + fn crash_detach_can_restore_after_breakpoint_mapping_is_lost() { + let (mut sandbox, debugger, _) = sandbox_with_debugger(); + let snapshot = sandbox.snapshot().unwrap(); + // The guard-page backing is outside the guest allocator. + let address = hyperlight_common::layout::scratch_transport_end_gpa(); + sandbox + .call::>("ReadMappedBuffer", (address, 4u64, true)) + .unwrap(); + assert!(matches!( + request(&mut sandbox, DebugMsg::AddSwBreakpoint(address)), + DebugResponse::AddSwBreakpoint(true) + )); + + let root = sandbox.vm.get_root_pt().unwrap(); + let registers = sandbox.vm.get_snapshot_sregs().unwrap(); + let memory = DebugMemoryView::new(&sandbox.mem_mgr, Vec::new()); + let mut table = root; + for shift in [39, 30, 21, 12] { + let entry_address = table + ((address >> shift) & 0x1ff) * 8; + let mut bytes = [0; 8]; + memory.read(&mut bytes, entry_address).unwrap(); + let entry = u64::from_le_bytes(bytes); + assert_ne!(entry & 1, 0); + if shift == 12 || entry & 2 == 0 { + memory.write(&[0; 8], entry_address).unwrap(); + break; + } + table = entry & 0x0000_ffff_ffff_f000; + } + sandbox.vm.apply_sregs(root, ®isters).unwrap(); + debugger.send(DebugMsg::DisableDebug).unwrap(); + + // The guest maps this address, but the hypervisor has no backing for it. + let error = sandbox + .call::>("ReadMappedBuffer", (0x1_0000_0000u64, 1u64, true)) + .unwrap_err(); + let stop = debugger.try_recv().unwrap_or_else(|receive_error| { + panic!("Missing crash stop: {receive_error:?}, guest error: {error:?}") + }); + assert!(matches!( + stop, + DebugResponse::VcpuStopped(VcpuStopReason::Crash) + )); + let response = debugger.try_recv().unwrap(); + assert!( + matches!(response, DebugResponse::DisableDebug), + "{response:?}" + ); + assert_eq!(sandbox.status(), SandboxStatus::Poisoned, "{error:?}"); + sandbox.restore(snapshot).unwrap(); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + } + fn assert_virtq_attached(sbox: &Sandbox) { assert!(sbox.mem_mgr.g2h_consumer.is_some()); assert!(sbox.mem_mgr.h2g_consumer.is_some()); diff --git a/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs b/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs index 68be2c1bc..757d15740 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs @@ -192,7 +192,7 @@ impl SnapshotMemoryBacking { self.memory.page_table_bytes() } - #[cfg(crashdump)] + #[cfg(any(crashdump, all(gdb, target_arch = "aarch64")))] pub(crate) fn host_range(&self, layer_index: usize) -> Result<(usize, usize)> { #[cfg(not(unshared_snapshot_mem))] let backing = self From 4f4aba83f7faf2d873195df361cc5c384839a866 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:23:30 -0800 Subject: [PATCH 5/7] feat(aarch64): add GDB debugging for Linux KVM ARM64 KVM debugging uses KVM_SET_GUEST_DEBUG for hardware single-step and two hardware breakpoints. Software breakpoints use BRK. Guest virtual addresses use the existing page-table reader. Debugger code writes synchronize the host instruction cache. Register access respects the active stack pointer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- CHANGELOG.md | 1 + docs/how-to-debug-a-hyperlight-guest.md | 7 +- src/hyperlight_host/build.rs | 2 +- .../examples/guest-debugging/main.rs | 45 ++++ src/hyperlight_host/src/hypervisor/gdb/mod.rs | 99 ++++++++- .../src/hypervisor/hyperlight_vm/aarch64.rs | 13 +- .../src/hypervisor/hyperlight_vm/debug.rs | 4 +- .../hypervisor/virtual_machine/kvm/aarch64.rs | 193 +++++++++++++++++- .../hypervisor/virtual_machine/kvm/x86_64.rs | 6 +- .../hypervisor/virtual_machine/mshv/x86_64.rs | 6 +- .../hypervisor/virtual_machine/whp/aarch64.rs | 6 +- .../hypervisor/virtual_machine/whp/x86_64.rs | 6 +- 12 files changed, 362 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 831998659..bbc3f6476 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Prerelease] - Unreleased ### Added +* ARM64 Linux/KVM guest debugging supports software and hardware breakpoints, register and memory access, and hardware single-step. * `Sandbox::snapshot_with_options` takes a snapshot with `SnapshotOptions`. `SnapshotOptions::scratch_size` gives the sandbox, and later restores of the snapshot, a different scratch size. A guest can boot with the scratch its initialisation needs and run guest function calls from a snapshot with only the scratch they need, so each restore costs less. It can also give a guest more scratch than it booted with. * Support register, memory, continue, hardware single-step, interrupt, and hardware and software breakpoint GDB debugging for Windows ARM64 guests. diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index bd207af9c..2448f574a 100644 --- a/docs/how-to-debug-a-hyperlight-guest.md +++ b/docs/how-to-debug-a-hyperlight-guest.md @@ -17,9 +17,10 @@ The Hyperlight `gdb` feature enables guest debugging to: - stop when a crash occurs and only allow read access to the guest memory and registers On x86_64, gdb can also add up to four hardware breakpoints. -Windows ARM64 supports two hardware breakpoints, four-byte `BRK` software -breakpoints, and hardware single-step. Guest exception vectors forward debug -exceptions to WHP through HVC. ARM64 guest binaries must retain the +ARM64 on Windows/WHP and Linux/KVM supports two hardware breakpoints, +four-byte `BRK` software breakpoints, and hardware single-step. KVM reports +debug exceptions through `KVM_EXIT_DEBUG`. Guest exception vectors forward debug +exceptions to WHP through HVC. Windows ARM64 guest binaries must retain the `hyperlight_debug_vectors` symbol so the host can install the vectors before the initial debug stop. Watchpoints are not advertised. diff --git a/src/hyperlight_host/build.rs b/src/hyperlight_host/build.rs index 98b357caa..6f637cfa9 100644 --- a/src/hyperlight_host/build.rs +++ b/src/hyperlight_host/build.rs @@ -150,7 +150,7 @@ fn main() -> Result<()> { // Essentially the kvm and mshv3 features are ignored on windows as long as you use #[cfg(kvm)] and not #[cfg(feature = "kvm")]. // You should never use #[cfg(feature = "kvm")] or #[cfg(feature = "mshv3")] in the codebase. cfg_aliases::cfg_aliases! { - gdb_platform: { any(target_arch = "x86_64", all(target_arch = "aarch64", target_os = "windows")) }, + gdb_platform: { any(target_arch = "x86_64", all(target_arch = "aarch64", any(target_os = "windows", all(target_os = "linux", feature = "kvm")))) }, gdb: { all(feature = "gdb", debug_assertions, gdb_platform) }, kvm: { all(feature = "kvm", target_os = "linux") }, mshv3: { all(feature = "mshv3", target_os = "linux") }, diff --git a/src/hyperlight_host/examples/guest-debugging/main.rs b/src/hyperlight_host/examples/guest-debugging/main.rs index cbcd879ea..7b0ca858e 100644 --- a/src/hyperlight_host/examples/guest-debugging/main.rs +++ b/src/hyperlight_host/examples/guest-debugging/main.rs @@ -367,6 +367,51 @@ mod tests { assert!(result.is_ok(), "{}", result.unwrap_err()); } + #[test] + #[cfg(target_arch = "aarch64")] + #[serial] + fn test_gdb_code_writes_and_self_loop() { + let (out_file_path, cmd_file_path, manifest_dir) = + gdb_test_paths("gdb-aarch64-code-writes"); + let cmd = format!( + "set osabi none + file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest + target remote :8080 + set pagination off + set logging file {out_file_path} + set logging enabled on + hbreak simpleguest::no_op + continue + delete 1 + set language c + set $loop_pc = $pc + set $instruction = *(unsigned int *)$pc + set *(unsigned int *)$pc = 0x14000000 + stepi + if $pc != $loop_pc + echo Self-loop step missed its destination\\n + quit 1 + end + set *(unsigned int *)$pc = $instruction + set $return_pc = $x30 + stepi + if $pc != $return_pc + echo Restored RET missed its destination\\n + quit 1 + end + echo Code writes and self-loop stepping passed\\n + set logging enabled off + detach + quit + " + ); + let checker = + |contents: String| contents.contains("Code writes and self-loop stepping passed"); + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker); + cleanup(&out_file_path, &cmd_file_path); + assert!(result.is_ok(), "{}", result.unwrap_err()); + } + #[test] #[cfg(target_arch = "aarch64")] #[serial] diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 00bed232e..2810717b0 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -99,6 +99,36 @@ impl<'a> DebugMemoryView<'a> { self.mem_mgr.layout.get_guest_code_gva() as u64 } + #[cfg(all(kvm, target_arch = "aarch64"))] + pub(crate) fn translate_gva(&self, gva: u64, root_pt: u64) -> Result { + use crate::sandbox::snapshot::PageTableReader; + + self.mem_mgr + .with_guest_memory(&self.guest_mmap_regions, |memory| { + let reader = PageTableReader::new(memory, root_pt); + // SAFETY: Debug requests run with the vCPU stopped. The reader bounds-checks table accesses. + let mapping = + unsafe { hyperlight_common::vmem::virt_to_phys(&reader, gva, 1) }.next(); + reader.finish()?; + let gpa = mapping + .filter(|mapping| { + matches!( + mapping.kind, + hyperlight_common::vmem::MappingKind::Basic(_) + | hyperlight_common::vmem::MappingKind::Cow(_) + ) + }) + .and_then(|mapping| { + let offset = gva.checked_sub(mapping.virt_base)?; + (offset < mapping.len) + .then(|| mapping.phys_base.checked_add(offset)) + .flatten() + }); + gpa.ok_or_else(|| crate::new_error!("GVA is not mapped: {gva:#x}")) + }) + .map_err(|_| DebugError::TranslateGva(gva)) + } + /// Reads memory from the guest's address space with a maximum length of a PAGE_SIZE /// /// # Arguments @@ -162,9 +192,11 @@ impl<'a> DebugMemoryView<'a> { gpa: u64, len: usize, ) -> std::result::Result<(), DebugMemoryAccessError> { - #[cfg(all(target_arch = "aarch64", target_os = "windows"))] + #[cfg(target_arch = "aarch64")] { + #[cfg(target_os = "windows")] use windows::Win32::System::Diagnostics::Debug::FlushInstructionCache; + #[cfg(target_os = "windows")] use windows::Win32::System::Threading::GetCurrentProcess; let range = self @@ -174,14 +206,24 @@ impl<'a> DebugMemoryView<'a> { }) .map_err(|e| DebugMemoryAccessError::CopyFailed(Box::new(e)))?; + #[cfg(target_os = "windows")] // SAFETY: The current process handle is valid and the range belongs to a live sandbox mapping. unsafe { FlushInstructionCache(GetCurrentProcess(), Some(range.start as *const _), len) .map_err(|e| DebugMemoryAccessError::FlushInstructionCache(e.to_string()))?; } + + #[cfg(target_os = "linux")] + { + unsafe extern "C" { + fn __clear_cache(begin: *mut u8, end: *mut u8); + } + // SAFETY: The range is checked against the live sandbox backing. The vCPU is stopped. + unsafe { __clear_cache(range.start as *mut u8, range.end as *mut u8) }; + } } - #[cfg(not(all(target_arch = "aarch64", target_os = "windows")))] + #[cfg(not(target_arch = "aarch64"))] let _ = (gpa, len); Ok(()) @@ -266,7 +308,11 @@ pub enum DebugError { /// This extends the base VirtualMachine trait with GDB-specific functionality. pub(crate) trait DebuggableVm: VirtualMachine { /// Translates a guest virtual address to a guest physical address - fn translate_gva(&self, gva: u64) -> std::result::Result; + fn translate_gva( + &self, + gva: u64, + memory: &DebugMemoryView<'_>, + ) -> std::result::Result; /// Enable/disable debugging fn set_debug(&mut self, enable: bool) -> std::result::Result<(), DebugError>; @@ -504,6 +550,53 @@ mod tests { } } + #[test] + #[cfg(all(kvm, target_arch = "aarch64"))] + fn translate_gva_resolves_page_offsets_and_rejects_missing_tables() -> crate::Result<()> { + use hyperlight_common::vmem::PAGE_SIZE; + + let memory = TestMemory::new()?; + let access = memory.access(); + let root = hyperlight_common::layout::scratch_base_gpa( + memory.mem_mgr.layout.get_scratch_size(), + ); + let mut tables = [0u8; 4 * PAGE_SIZE]; + for level in 0..3 { + let entry = (root + (level as u64 + 1) * PAGE_SIZE as u64) | 3; + tables[level * PAGE_SIZE..level * PAGE_SIZE + 8] + .copy_from_slice(&entry.to_le_bytes()); + } + tables[3 * PAGE_SIZE + 8..3 * PAGE_SIZE + 16] + .copy_from_slice(&(SandboxMemoryLayout::BASE_ADDRESS as u64 | 3).to_le_bytes()); + tables[3 * PAGE_SIZE + 16..3 * PAGE_SIZE + 24] + .copy_from_slice(&((1u64 << 2) | (1 << 8)).to_le_bytes()); + access.write(&tables, root).unwrap(); + let code = PAGE_SIZE as u64; + for offset in [0, 1, PAGE_SIZE as u64 - 1] { + assert_eq!( + access.translate_gva(code + offset, root).unwrap(), + SandboxMemoryLayout::BASE_ADDRESS as u64 + offset + ); + } + assert!(matches!( + access.translate_gva(0, root), + Err(DebugError::TranslateGva(0)) + )); + assert!(matches!( + access.translate_gva(code * 2, root), + Err(DebugError::TranslateGva(_)) + )); + assert!(matches!( + access.translate_gva(code | (1 << 48), root), + Err(DebugError::TranslateGva(_)) + )); + assert!(matches!( + access.translate_gva(code, !0xfff), + Err(DebugError::TranslateGva(_)) + )); + Ok(()) + } + #[test] fn test_mem_access_read_single_byte() -> crate::Result<()> { let mut memory = TestMemory::new()?; diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 9c0fdffec..f16e9c054 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -47,14 +47,17 @@ impl HyperlightVm { &mut self, vectors: Option, ) -> Result<(), CreateHyperlightVmError> { - if self.gdb_conn.is_none() || !matches!(self.next_action, NextAction::Initialise(_)) { + if self.gdb_conn.is_none() + || cfg!(kvm) + || !matches!(self.next_action, NextAction::Initialise(_)) + { return Ok(()); } - let vectors = vectors.ok_or_else(|| { - VmError::Debug(crate::hypervisor::gdb::DebugError::Unsupported( + let vectors = vectors.ok_or(VmError::Debug( + crate::hypervisor::gdb::DebugError::Unsupported( "guest binary lacks ARM64 debug vectors", - )) - })?; + ), + ))?; let mut regs = self.vm.sregs().map_err(VmError::Register)?; regs.vbar_el1 = vectors; regs.sp_el1 = hyperlight_common::layout::SCRATCH_TOP_GVA as u64 diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs index 3ca48680d..294c8a9af 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs @@ -343,7 +343,7 @@ impl HyperlightVm { tracing::debug!("Read addr: {:X} len: {:X}", gva, data.len()); while !data.is_empty() { - let gpa = self.vm.translate_gva(gva)?; + let gpa = self.vm.translate_gva(gva, mem_access)?; let read_len = std::cmp::min( data.len(), page_size::get() - (gpa as usize & (page_size::get() - 1)), @@ -365,7 +365,7 @@ impl HyperlightVm { tracing::debug!("Write addr: {:X} len: {:X}", gva, data.len()); while !data.is_empty() { - let gpa = self.vm.translate_gva(gva)?; + let gpa = self.vm.translate_gva(gva, mem_access)?; let write_len = std::cmp::min( data.len(), page_size::get() - (gpa as usize & (page_size::get() - 1)), 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 fdd8d4963..f16cb1ffc 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2025 The Hyperlight Authors. -// TODO(aarch64): implement KVM backend - use std::sync::LazyLock; use hyperlight_common::outb::VmAction; @@ -12,6 +10,8 @@ use kvm_bindings::{ use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; use tracing::{Span, instrument}; +#[cfg(gdb)] +use crate::hypervisor::gdb::{DebugError, DebugMemoryView, DebuggableVm, VcpuStopReason}; use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; use crate::hypervisor::virtual_machine::{ CreateVmError, HypervisorError, MapMemoryError, RegisterError, ResetVcpuError, RunVcpuError, @@ -21,6 +21,9 @@ use crate::hypervisor::virtual_machine::{ static KVM: LazyLock> = LazyLock::new(|| Kvm::new().map_err(|e| CreateVmError::HypervisorNotAvailable(e.into()))); +#[cfg(gdb)] +const HARDWARE_BREAKPOINTS: usize = 2; + /// Return `true` if the KVM API is available #[instrument(skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn is_hypervisor_present() -> bool { @@ -37,6 +40,8 @@ pub(crate) fn is_hypervisor_present() -> bool { pub(crate) struct KvmVm { vm_fd: VmFd, vcpu_fd: VcpuFd, + #[cfg(gdb)] + debug_state: kvm_bindings::kvm_guest_debug, } impl KvmVm { @@ -76,7 +81,12 @@ impl KvmVm { .create_vcpu(0) .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; - let mut to_ret = Self { vm_fd, vcpu_fd }; + let mut to_ret = Self { + vm_fd, + vcpu_fd, + #[cfg(gdb)] + debug_state: kvm_bindings::kvm_guest_debug::default(), + }; to_ret .vcpu_init() .map_err(CreateVmError::SetPartitionProperty)?; @@ -103,6 +113,18 @@ impl KvmVm { self.vcpu_fd.set_kvm_immediate_exit(0u8); ret } + + #[cfg(gdb)] + fn apply_debug_state( + &mut self, + state: kvm_bindings::kvm_guest_debug, + ) -> Result<(), DebugError> { + self.vcpu_fd + .set_guest_debug(&state) + .map_err(|e| RegisterError::SetDebugRegs(e.into()))?; + self.debug_state = state; + Ok(()) + } } impl VirtualMachine for KvmVm { @@ -201,6 +223,21 @@ impl VirtualMachine for KvmVm { break exit; }; match exit { + #[cfg(gdb)] + Ok(VcpuExit::Debug(debug)) => { + let reason = match debug.hsr >> 26 { + 0x30 => VcpuStopReason::HwBp, + 0x32 => VcpuStopReason::DoneStep, + 0x3c => VcpuStopReason::SwBp, + _ => { + return Ok(VmExit::Unknown(format!( + "Unknown KVM debug syndrome: {:#x}", + debug.hsr + ))); + } + }; + Ok(VmExit::Debug { reason }) + } Ok(VcpuExit::MmioWrite(addr, data)) => { let io_page_gpa = const { hyperlight_common::layout::io_page().unwrap().0 }; if addr >= io_page_gpa @@ -260,25 +297,28 @@ impl VirtualMachine for KvmVm { } fn regs(&self) -> std::result::Result { - use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, X}; + use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, SP_EL1, X}; let mut x: [u64; 31] = [0; 31]; for (i, xi) in X.iter().enumerate() { x[i] = xi.get(RegisterError::GetSregs, &self.vcpu_fd)?; } + let pstate = PSTATE.get(RegisterError::GetSregs, &self.vcpu_fd)?; + let sp = if pstate & 1 == 0 { SP } else { SP_EL1 }; Ok(CommonRegisters { x, - sp: SP.get(RegisterError::GetSregs, &self.vcpu_fd)?, + sp: sp.get(RegisterError::GetSregs, &self.vcpu_fd)?, pc: PC.get(RegisterError::GetSregs, &self.vcpu_fd)?, - pstate: PSTATE.get(RegisterError::GetSregs, &self.vcpu_fd)?, + pstate, }) } fn set_regs(&mut self, regs: &CommonRegisters) -> std::result::Result<(), RegisterError> { - use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, X}; + use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, SP_EL1, 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)?; + let sp = if regs.pstate & 1 == 0 { SP } else { SP_EL1 }; + 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)?; @@ -366,6 +406,143 @@ impl VirtualMachine for KvmVm { .map_err(ResetVcpuError::Unknown) .unwrap_or_else(|e| e) })?; + #[cfg(gdb)] + if self.debug_state.control != 0 { + self.vcpu_fd + .set_guest_debug(&self.debug_state) + .map_err(|e| ResetVcpuError::Hypervisor(e.into()))?; + } Ok(()) } } + +#[cfg(gdb)] +impl DebuggableVm for KvmVm { + fn translate_gva(&self, gva: u64, memory: &DebugMemoryView<'_>) -> Result { + use crate::hypervisor::regs::kvm_reg::TTBR0_EL1; + + let root = TTBR0_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)? & 0x0000_ffff_ffff_f000; + memory.translate_gva(gva, root) + } + + fn set_debug(&mut self, enable: bool) -> Result<(), DebugError> { + use kvm_bindings::{KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_USE_HW, KVM_GUESTDBG_USE_SW_BP}; + + let state = if enable { + let mut state = self.debug_state; + state.control |= KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW | KVM_GUESTDBG_USE_SW_BP; + state + } else { + kvm_bindings::kvm_guest_debug::default() + }; + self.apply_debug_state(state) + } + + fn set_single_step(&mut self, enable: bool) -> Result<(), DebugError> { + let mut state = self.debug_state; + if enable { + state.control |= kvm_bindings::KVM_GUESTDBG_SINGLESTEP; + } else { + state.control &= !kvm_bindings::KVM_GUESTDBG_SINGLESTEP; + } + self.apply_debug_state(state) + } + + fn add_hw_breakpoint(&mut self, addr: u64) -> Result<(), DebugError> { + if !addr.is_multiple_of(4) { + return Err(DebugError::Unsupported( + "unaligned ARM64 hardware breakpoint", + )); + } + let mut state = self.debug_state; + let slots = &state.arch.dbg_bcr[..HARDWARE_BREAKPOINTS]; + if slots + .iter() + .enumerate() + .any(|(i, control)| control & 1 != 0 && state.arch.dbg_bvr[i] == addr) + { + return Ok(()); + } + let slot = slots + .iter() + .position(|control| control & 1 == 0) + .ok_or(DebugError::TooManyHwBreakpoints(HARDWARE_BREAKPOINTS))?; + state.arch.dbg_bvr[slot] = addr; + state.arch.dbg_bcr[slot] = (15 << 5) | (3 << 1) | 1; + self.apply_debug_state(state) + } + + fn remove_hw_breakpoint(&mut self, addr: u64) -> Result<(), DebugError> { + let mut state = self.debug_state; + let slot = state.arch.dbg_bcr[..HARDWARE_BREAKPOINTS] + .iter() + .enumerate() + .position(|(i, control)| control & 1 != 0 && state.arch.dbg_bvr[i] == addr) + .ok_or(DebugError::HwBreakpointNotFound(addr))?; + state.arch.dbg_bcr[slot] = 0; + state.arch.dbg_bvr[slot] = 0; + self.apply_debug_state(state) + } +} + +#[cfg(all(test, gdb))] +mod debug_tests { + use super::*; + + #[test] + fn hardware_breakpoints_survive_reset_and_detach_clears_debug_state() { + let mut vm = KvmVm::new().unwrap(); + vm.set_debug(true).unwrap(); + vm.add_hw_breakpoint(0).unwrap(); + vm.add_hw_breakpoint(0).unwrap(); + vm.add_hw_breakpoint(0x1000).unwrap(); + assert!(matches!( + vm.add_hw_breakpoint(0x2000), + Err(DebugError::TooManyHwBreakpoints(2)) + )); + assert!(vm.add_hw_breakpoint(1).is_err()); + vm.remove_hw_breakpoint(0).unwrap(); + assert!(matches!( + vm.remove_hw_breakpoint(0), + Err(DebugError::HwBreakpointNotFound(0)) + )); + vm.add_hw_breakpoint(0x2000).unwrap(); + vm.set_single_step(true).unwrap(); + vm.reset_vcpu().unwrap(); + assert_ne!( + vm.debug_state.control & kvm_bindings::KVM_GUESTDBG_SINGLESTEP, + 0 + ); + assert_eq!(&vm.debug_state.arch.dbg_bvr[..2], &[0x2000, 0x1000]); + vm.set_debug(false).unwrap(); + assert_eq!(vm.debug_state.control, 0); + assert!( + vm.debug_state + .arch + .dbg_bcr + .iter() + .all(|&control| control == 0) + ); + } + + #[test] + fn register_writes_preserve_the_inactive_stack_pointer() { + use crate::hypervisor::regs::kvm_reg::{SP, SP_EL1}; + + let mut vm = KvmVm::new().unwrap(); + for (pstate, sp) in [(0x3c4, 0x1000), (0x3c5, 0x2000)] { + vm.set_regs(&CommonRegisters { + pstate, + sp, + ..Default::default() + }) + .unwrap(); + assert_eq!(vm.regs().unwrap().sp, sp); + } + assert_eq!(SP.get(RegisterError::GetRegs, &vm.vcpu_fd).unwrap(), 0x1000); + assert_eq!( + SP_EL1.get(RegisterError::GetRegs, &vm.vcpu_fd).unwrap(), + 0x2000 + ); + } +} 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 9bd348647..9c84dc616 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 @@ -697,7 +697,11 @@ impl VirtualMachine for KvmVm { #[cfg(gdb)] impl DebuggableVm for KvmVm { - fn translate_gva(&self, gva: u64) -> std::result::Result { + fn translate_gva( + &self, + gva: u64, + _memory: &crate::hypervisor::gdb::DebugMemoryView<'_>, + ) -> std::result::Result { let gpa = self .vcpu_fd .translate_gva(gva) diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs index 1407fceaf..4ce9274b5 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs @@ -792,7 +792,11 @@ impl VirtualMachine for MshvVm { #[cfg(gdb)] impl DebuggableVm for MshvVm { - fn translate_gva(&self, gva: u64) -> std::result::Result { + fn translate_gva( + &self, + gva: u64, + _memory: &crate::hypervisor::gdb::DebugMemoryView<'_>, + ) -> std::result::Result { use mshv_bindings::HV_TRANSLATE_GVA_VALIDATE_READ; // Do not use HV_TRANSLATE_GVA_VALIDATE_WRITE, since many diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index 0f1ddcb41..570d3d00b 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -1165,7 +1165,11 @@ impl Drop for WhpVm { #[cfg(gdb)] impl DebuggableVm for WhpVm { - fn translate_gva(&self, gva: u64) -> std::result::Result { + fn translate_gva( + &self, + gva: u64, + _memory: &crate::hypervisor::gdb::DebugMemoryView<'_>, + ) -> std::result::Result { let mut gpa = 0; let mut result = WHV_TRANSLATE_GVA_RESULT::default(); diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs index 57342a532..2d9636580 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs @@ -1101,7 +1101,11 @@ impl VirtualMachine for WhpVm { #[cfg(gdb)] impl DebuggableVm for WhpVm { - fn translate_gva(&self, gva: u64) -> std::result::Result { + fn translate_gva( + &self, + gva: u64, + _memory: &crate::hypervisor::gdb::DebugMemoryView<'_>, + ) -> std::result::Result { let mut gpa = 0; let mut result = WHV_TRANSLATE_GVA_RESULT::default(); From 2418379219e492dfe9128fca7c47e31194c8a910 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Mon, 2 Nov 2026 17:07:31 -0800 Subject: [PATCH 6/7] ci(aarch64): run KVM GDB tests ARM64 KVM debug jobs run the GDB suite before the general Rust tests. Install GDB when the runner lacks it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- .github/workflows/dep_build_test.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index 539338765..383886688 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -129,6 +129,17 @@ jobs: - name: Build run: just build ${{ inputs.config }} + - name: Run ARM64 KVM Gdb tests + if: inputs.arch == 'arm64' && inputs.hypervisor == 'kvm' && inputs.config == 'debug' + env: + RUST_LOG: debug + run: | + if ! command -v gdb > /dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y --no-install-recommends gdb + fi + just test-rust-gdb-debugging debug + - name: Run Rust tests run: | # with default features From a26c2d9dc6e6104b098b2db9b42529fa882d9db6 Mon Sep 17 00:00:00 2001 From: cshung <3410332+cshung@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:35:10 -0800 Subject: [PATCH 7/7] fix(gdb): split memory access at guest page boundaries Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: cshung <3410332+cshung@users.noreply.github.com> --- CHANGELOG.md | 1 + Justfile | 2 +- .../src/hypervisor/hyperlight_vm/debug.rs | 140 +++++++++++++++++- 3 files changed, 134 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc3f6476..3ff5fa367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +* GDB memory reads and writes translate each guest page on hosts with larger pages. * ARM64 GDB snapshot recovery handles cancellation and crash detach. Snapshot checkpointing rejects software breakpoints before replacing memory. * WHP ARM64 cache synchronization code is read-only to guests. * Guest libc accepts null buffers for zero-length writes. diff --git a/Justfile b/Justfile index e20a52adb..3470dec2a 100644 --- a/Justfile +++ b/Justfile @@ -331,7 +331,7 @@ test-no-surrogate target=default-target: # runs tests that exercise gdb debugging test-rust-gdb-debugging target=default-target features="": {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example guest-debugging {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} - {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- hypervisor::gdb:: gdb_recovery_tests:: hypervisor::virtual_machine::whp::aarch64::debug_tests:: + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- hypervisor::gdb:: hypervisor::hyperlight_vm::debug::tests:: gdb_recovery_tests:: hypervisor::virtual_machine::whp::aarch64::debug_tests:: # rust test for crashdump test-rust-crashdump target=default-target features="": diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs index 294c8a9af..da1cd1114 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; +use hyperlight_common::vmem::PAGE_SIZE; + use super::{ HandleDebugError, HyperlightVm, RecvDbgMsgError, SendDbgMsgError, VcpuStopReason, VmError, }; @@ -344,10 +346,7 @@ impl HyperlightVm { while !data.is_empty() { let gpa = self.vm.translate_gva(gva, mem_access)?; - let read_len = std::cmp::min( - data.len(), - page_size::get() - (gpa as usize & (page_size::get() - 1)), - ); + let read_len = std::cmp::min(data.len(), PAGE_SIZE - (gva as usize & (PAGE_SIZE - 1))); mem_access.read(&mut data[..read_len], gpa)?; data = &mut data[read_len..]; gva += read_len as u64; @@ -366,10 +365,7 @@ impl HyperlightVm { while !data.is_empty() { let gpa = self.vm.translate_gva(gva, mem_access)?; - let write_len = std::cmp::min( - data.len(), - page_size::get() - (gpa as usize & (page_size::get() - 1)), - ); + let write_len = std::cmp::min(data.len(), PAGE_SIZE - (gva as usize & (PAGE_SIZE - 1))); mem_access .write(&data[..write_len], gpa) .map_err(ProcessDebugRequestError::WriteMemory)?; @@ -545,4 +541,132 @@ mod tests { Some([0x11, 0x22, 0x33, 0x44].as_slice()) ); } + + #[cfg(all(kvm, target_arch = "aarch64"))] + mod memory_access_tests { + use hyperlight_common::vmem::PAGE_SIZE; + + use super::super::*; + use crate::hypervisor::gdb::DebugCommChannel; + use crate::mem::layout::SandboxMemoryLayout; + use crate::mem::mgr::SandboxMemoryManager; + use crate::mem::shared_mem::{ExclusiveSharedMemory, HostSharedMemory}; + use crate::sandbox::SandboxConfiguration; + use crate::sandbox::snapshot::{NextAction, SnapshotMemoryBacking}; + + fn noncontiguous_pages() -> (HyperlightVm, SandboxMemoryManager, u64) { + let mut config = SandboxConfiguration::default(); + config.set_scratch_size(config.get_scratch_size().next_multiple_of(page_size::get())); + let layout = SandboxMemoryLayout::new(config, PAGE_SIZE, 0, None).unwrap(); + let snapshot = crate::sandbox::snapshot::memory::tests::flat_memory( + &vec![0; page_size::get()], + &[0; PAGE_SIZE], + SandboxMemoryLayout::BASE_ADDRESS.next_multiple_of(page_size::get()) as u64, + hyperlight_common::layout::scratch_base_gpa(layout.get_scratch_size()), + ); + let memory = SandboxMemoryManager::new( + layout, + SnapshotMemoryBacking::from_snapshot(std::sync::Arc::new(snapshot)).unwrap(), + ExclusiveSharedMemory::new(layout.get_scratch_size()).unwrap(), + NextAction::None, + ); + let (memory, guest_memory) = memory.build().unwrap(); + let (_debugger, connection) = DebugCommChannel::unbounded(); + let mut vm = HyperlightVm::new( + guest_memory.shared_mem, + guest_memory.scratch_mem, + guest_memory.layout.get_pt_base_gpa(), + guest_memory.next_action, + 0, + page_size::get(), + &config, + Some(connection), + #[cfg(feature = "mem_profile")] + crate::sandbox::trace::MemTraceInfo::new(crate::mem::exe::LoadInfo::dummy().info) + .unwrap(), + ) + .unwrap(); + let root = memory.first_free_scratch_gpa(); + let access = DebugMemoryView::new(&memory, Vec::new()); + let mut tables = [0; 4 * PAGE_SIZE]; + for level in 0..3 { + let entry = (root + (level as u64 + 1) * PAGE_SIZE as u64) | 3; + tables[level * PAGE_SIZE..level * PAGE_SIZE + 8] + .copy_from_slice(&entry.to_le_bytes()); + } + for page in 0..3 { + let entry = (root + (4 + 2 * page) as u64 * PAGE_SIZE as u64) | 3; + let offset = 3 * PAGE_SIZE + (page + 1) * 8; + tables[offset..offset + 8].copy_from_slice(&entry.to_le_bytes()); + access + .write(&vec![page as u8 + 1; PAGE_SIZE], entry & !0xfff) + .unwrap(); + } + for page in [5, 7] { + access + .write(&[0xee; PAGE_SIZE], root + page * PAGE_SIZE as u64) + .unwrap(); + } + access.write(&tables, root).unwrap(); + let registers = vm.get_snapshot_sregs().unwrap(); + vm.apply_sregs(root, ®isters).unwrap(); + (vm, memory, root) + } + + #[test] + fn reads_translate_each_noncontiguous_guest_page() { + let (mut vm, memory, _) = noncontiguous_pages(); + let access = DebugMemoryView::new(&memory, Vec::new()); + let address = (2 * PAGE_SIZE - 3) as u64; + let DebugResponse::ReadAddr(data) = vm + .process_dbg_request(DebugMsg::ReadAddr(address, PAGE_SIZE + 8), &access) + .unwrap() + else { + panic!("missing memory read response"); + }; + assert_eq!(&data[..3], &[1; 3]); + assert!(data[3..3 + PAGE_SIZE].iter().all(|&byte| byte == 2)); + assert_eq!(&data[3 + PAGE_SIZE..], &[3; 5]); + } + + #[test] + fn writes_preserve_gaps_between_noncontiguous_guest_pages() { + let (mut vm, memory, root) = noncontiguous_pages(); + let access = DebugMemoryView::new(&memory, Vec::new()); + let address = (2 * PAGE_SIZE - 3) as u64; + assert!(matches!( + vm.process_dbg_request( + DebugMsg::WriteAddr(address, vec![0xab; PAGE_SIZE + 8]), + &access, + ) + .unwrap(), + DebugResponse::WriteAddr + )); + let mut data = [0; PAGE_SIZE]; + for page in [5, 7] { + access + .read(&mut data, root + page * PAGE_SIZE as u64) + .unwrap(); + assert!(data.iter().all(|&byte| byte == 0xee)); + } + for (page, prefix, suffix) in [(4, PAGE_SIZE - 3, 0), (6, 0, 0), (8, 0, PAGE_SIZE - 5)] + { + access + .read(&mut data, root + page * PAGE_SIZE as u64) + .unwrap(); + let original = ((page - 4) / 2 + 1) as u8; + assert!(data[..prefix].iter().all(|&byte| byte == original)); + assert!( + data[prefix..PAGE_SIZE - suffix] + .iter() + .all(|&byte| byte == 0xab) + ); + assert!( + data[PAGE_SIZE - suffix..] + .iter() + .all(|&byte| byte == original) + ); + } + } + } }