From 965930a21afceb40fa74798e84614204d95aefce 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/2] feat(aarch64): add GDB debugging for Windows ARM64 WHP ## Summary Adds GDB stub support for guests running on the ARM64 WHP backend. ## Changes ### Debug core * 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`. ### ARM64 WHP * 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. ### Docs * Document ARM64 debugging in `docs/how-to-debug-a-hyperlight-guest.md` and `CHANGELOG.md`. ## Verification 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 489e0a431..4e783a910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). * `ExternalValueSource` implementations for `RecvChain` and `Segments`. * Producer batch completion without notification and segmented payload assembly and extraction without flattening. +* Support register, memory, continue, single-step, interrupt, and software-breakpoint GDB debugging for Windows ARM64 guests. ### Changed * `Sandbox` is the primary initialized sandbox type. `MultiUseSandbox` remains diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index 688b08767..2d868b0e3 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 bd9f15b1d..548b0d3d8 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 79ca6e083..08d1356aa 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; @@ -80,6 +80,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> { @@ -155,16 +158,63 @@ impl<'a> DebugMemoryView<'a> { _ => 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, } @@ -220,6 +270,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), } @@ -234,14 +288,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 @@ -309,7 +386,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); @@ -318,7 +395,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 0c6870f23..01845fc64 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, SnapshotSharedMemory}; use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; @@ -51,11 +54,13 @@ 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(crashdump)] _rt_cfg: SandboxRuntimeConfig, #[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 = @@ -105,9 +110,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) } @@ -137,6 +159,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)?; @@ -174,6 +200,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 9d6157364..468681577 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::{ @@ -208,7 +209,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), } @@ -259,6 +260,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 @@ -388,14 +392,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)] @@ -520,6 +528,11 @@ impl HyperlightVm { &mut self, snapshot: SnapshotSharedMemory, ) -> Result<(), UpdateRegionError> { + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.has_sw_breakpoints() { + return Err(UpdateRegionError::ActiveSoftwareBreakpoints); + } + let guest_base = crate::mem::layout::SandboxMemoryLayout::BASE_ADDRESS as u64; let rgn = snapshot.mapping_at(guest_base, MemoryRegionType::Snapshot); @@ -672,7 +685,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( @@ -680,6 +701,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 @@ -695,6 +717,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()); } @@ -758,11 +790,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 8ca3024fc..ec14afa38 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, }; @@ -130,7 +124,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")] @@ -190,7 +184,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, @@ -276,7 +270,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, @@ -376,145 +370,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, @@ -596,270 +451,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 { @@ -1338,8 +929,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 1b3778162..72c5f6d7b 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -410,6 +410,13 @@ impl Sandbox { pub fn snapshot(&mut self) -> 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" + )); + } + if let Some(snapshot) = &self.snapshot { return Ok(snapshot.clone()); } @@ -595,6 +602,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 39ea4cbb9456b549ec8bd7b3a7d7236cf8d58153 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/2] 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 468681577..55ebcd6c9 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -485,6 +485,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 = if let Some(freed_slot) = self.freed_slots.pop() { freed_slot @@ -788,14 +805,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 ec14afa38..94ad1e60b 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -184,7 +184,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, @@ -270,7 +270,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, @@ -929,8 +929,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 72c5f6d7b..6207def49 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -3696,6 +3696,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())