diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index 539338765..383886688 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -129,6 +129,17 @@ jobs: - name: Build run: just build ${{ inputs.config }} + - name: Run ARM64 KVM Gdb tests + if: inputs.arch == 'arm64' && inputs.hypervisor == 'kvm' && inputs.config == 'debug' + env: + RUST_LOG: debug + run: | + if ! command -v gdb > /dev/null 2>&1; then + sudo apt-get update + sudo apt-get install -y --no-install-recommends gdb + fi + just test-rust-gdb-debugging debug + - name: Run Rust tests run: | # with default features diff --git a/CHANGELOG.md b/CHANGELOG.md index c095f6de1..3ff5fa367 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Prerelease] - Unreleased ### Added +* ARM64 Linux/KVM guest debugging supports software and hardware breakpoints, register and memory access, and hardware single-step. * `Sandbox::snapshot_with_options` takes a snapshot with `SnapshotOptions`. `SnapshotOptions::scratch_size` gives the sandbox, and later restores of the snapshot, a different scratch size. A guest can boot with the scratch its initialisation needs and run guest function calls from a snapshot with only the scratch they need, so each restore costs less. It can also give a guest more scratch than it booted with. +* Support register, memory, continue, hardware single-step, interrupt, and + hardware and software breakpoint GDB debugging for Windows ARM64 guests. ### Changed * Core dumps no longer record the guest binary path. The process name and command line are always ``, as they already were for sandboxes created from snapshots by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1975 @@ -40,6 +43,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +* GDB memory reads and writes translate each guest page on hosts with larger pages. +* ARM64 GDB snapshot recovery handles cancellation and crash detach. Snapshot checkpointing rejects software breakpoints before replacing memory. +* WHP ARM64 cache synchronization code is read-only to guests. * Guest libc accepts null buffers for zero-length writes. * Fix calls to guest functions with more than 11 parameters. Registration accepts up to 32, but every call with more than 11 failed. diff --git a/Justfile b/Justfile index fbb2b83ce..3470dec2a 100644 --- a/Justfile +++ b/Justfile @@ -331,7 +331,7 @@ test-no-surrogate target=default-target: # runs tests that exercise gdb debugging test-rust-gdb-debugging target=default-target features="": {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example guest-debugging {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} - {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- hypervisor::gdb:: + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib {{ if features =="" {'--features gdb'} else { "--features gdb," + features } }} -- hypervisor::gdb:: hypervisor::hyperlight_vm::debug::tests:: gdb_recovery_tests:: hypervisor::virtual_machine::whp::aarch64::debug_tests:: # rust test for crashdump test-rust-crashdump target=default-target features="": diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index 2deb9894d..2448f574a 100644 --- a/docs/how-to-debug-a-hyperlight-guest.md +++ b/docs/how-to-debug-a-hyperlight-guest.md @@ -7,15 +7,27 @@ 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. +ARM64 on Windows/WHP and Linux/KVM supports two hardware breakpoints, +four-byte `BRK` software breakpoints, and hardware single-step. KVM reports +debug exceptions through `KVM_EXIT_DEBUG`. Guest exception vectors forward debug +exceptions to WHP through HVC. Windows ARM64 guest binaries must retain the +`hyperlight_debug_vectors` symbol so the host can install the vectors before +the initial debug stop. Watchpoints are not advertised. + +ARM64 snapshots reject installed software breakpoints, including those added +during checkpointing. Restoring a poisoned sandbox discards its software +breakpoint state and restores clean snapshot memory. + ## Expected behavior Below is a list describing some cases of expected behavior from a gdb debug @@ -170,7 +182,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_common/src/arch/aarch64/exn.rs b/src/hyperlight_common/src/arch/aarch64/exn.rs index 648822460..f8343a8a8 100644 --- a/src/hyperlight_common/src/arch/aarch64/exn.rs +++ b/src/hyperlight_common/src/arch/aarch64/exn.rs @@ -3,6 +3,9 @@ use crate::vmem::bits; +/// HVC immediate for forwarding guest exceptions to the WHP debugger. +pub const DEBUG_EXCEPTION_HVC: u16 = 0x485a; + const ESR_EC_UNKNOWN: u64 = 0b000000; // udf generates this const ESR_EC_DATA_ABORT_LOWER_EL: u64 = 0b100100; const ESR_EC_DATA_ABORT_SAME_EL: u64 = 0b100101; diff --git a/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs b/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs index 869c10bb1..7fb0bce3a 100644 --- a/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs +++ b/src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs @@ -104,10 +104,24 @@ context_restore:\n q_off = const offset_of!(ExceptionContext, q), ); +// Only synchronous exceptions have a syndrome for debug forwarding. +// The host resumes debug events at ERET and other synchronous exceptions at the prologue. macro_rules! vbar_entry { ($et:literal, $ef:literal) => { concat!( " + .if {ExceptionType_", + $et, + "} == {ExceptionType_Synchronous}\n + stp x0, x1, [sp, #-16]!\n + mrs x0, mdscr_el1\n + tbz x0, #13, 1f\n + ldp x0, x1, [sp], #16\n + hvc #{debug_hvc}\n + eret\n + 1:\n + ldp x0, x1, [sp], #16\n + .endif\n sub sp, sp, #{ctx_size}\n str x30, [sp, #{x30_off}]\n bl context_save\n @@ -129,6 +143,8 @@ macro_rules! vbar_entry { global_asm!(" .balign 0x800\n .global vbar\n +.global hyperlight_debug_vectors\n +hyperlight_debug_vectors:\n vbar:\n", vbar_entry!("Synchronous", "CurrentSP0"), vbar_entry!("IRQ", "CurrentSP0"), @@ -148,6 +164,7 @@ vbar:\n", vbar_entry!("SError", "LowerAArch32"), ctx_size = const size_of::(), x30_off = const offset_of!(ExceptionContext, x) + 15 * 0x010, + debug_hvc = const hyperlight_common::arch::exn::DEBUG_EXCEPTION_HVC, handler = sym super::handle::handle_exception, ExceptionType_Synchronous = const ExceptionType::Synchronous as u64, ExceptionType_IRQ = const ExceptionType::Irq as u64, diff --git a/src/hyperlight_host/build.rs b/src/hyperlight_host/build.rs index 43c4484e6..6f637cfa9 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", any(target_os = "windows", all(target_os = "linux", feature = "kvm")))) }, + gdb: { all(feature = "gdb", debug_assertions, gdb_platform) }, kvm: { all(feature = "kvm", target_os = "linux") }, mshv3: { all(feature = "mshv3", target_os = "linux") }, 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..7b0ca858e 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,161 @@ 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_hardware_breakpoint_and_step() { + let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-aarch64-hardware"); + let cmd = format!( + "set osabi none + file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest + target remote :8080 + set pagination off + set logging file {out_file_path} + set logging enabled on + set $entry_pc = $pc + stepi + if $pc != $entry_pc + 4 + echo Entry step did not execute one instruction\\n + quit 1 + end + hbreak simpleguest::no_op + continue + set $return_pc = $x30 + stepi + if $pc != $return_pc + echo Hardware RET step missed its destination\\n + quit 1 + end + echo Hardware breakpoint and entry/RET stepping passed\\n + set logging enabled off + detach + quit + " + ); + let checker = |contents: String| { + contents.contains("Hardware breakpoint and entry/RET stepping passed") + }; + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker); + cleanup(&out_file_path, &cmd_file_path); + assert!(result.is_ok(), "{}", result.unwrap_err()); + } + + #[test] + #[cfg(target_arch = "aarch64")] + #[serial] + fn test_gdb_code_writes_and_self_loop() { + let (out_file_path, cmd_file_path, manifest_dir) = + gdb_test_paths("gdb-aarch64-code-writes"); + let cmd = format!( + "set osabi none + file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest + target remote :8080 + set pagination off + set logging file {out_file_path} + set logging enabled on + hbreak simpleguest::no_op + continue + delete 1 + set language c + set $loop_pc = $pc + set $instruction = *(unsigned int *)$pc + set *(unsigned int *)$pc = 0x14000000 + stepi + if $pc != $loop_pc + echo Self-loop step missed its destination\\n + quit 1 + end + set *(unsigned int *)$pc = $instruction + set $return_pc = $x30 + stepi + if $pc != $return_pc + echo Restored RET missed its destination\\n + quit 1 + end + echo Code writes and self-loop stepping passed\\n + set logging enabled off + detach + quit + " + ); + let checker = + |contents: String| contents.contains("Code writes and self-loop stepping passed"); + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker); + cleanup(&out_file_path, &cmd_file_path); + assert!(result.is_ok(), "{}", result.unwrap_err()); + } + + #[test] + #[cfg(target_arch = "aarch64")] + #[serial] + 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..1a5bda738 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/gdb/arch/aarch64.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 The Hyperlight Authors. + +pub(crate) const SW_BP_SIZE: usize = 4; +pub(crate) const SW_BP: [u8; SW_BP_SIZE] = 0xd420_0000u32.to_le_bytes(); + +pub(crate) fn valid_sw_breakpoint_address(addr: u64) -> bool { + addr.is_multiple_of(SW_BP_SIZE as u64) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn software_breakpoint_is_brk() { + assert_eq!(SW_BP, [0, 0, 0x20, 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)); + } +} 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..83466bfac 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; @@ -48,6 +52,11 @@ impl run_blocking::BlockingEventLoop for GdbBlockingEventLoop { VcpuStopReason::DoneStep => BaseStopReason::DoneStep, VcpuStopReason::SwBp => BaseStopReason::SwBreak(()), 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 +67,7 @@ impl run_blocking::BlockingEventLoop for GdbBlockingEventLoop { tid: (), signal: Signal(signals::SIGSEGV as u8), }, + #[cfg(target_arch = "x86_64")] VcpuStopReason::Unknown => { tracing::warn!("Unknown stop reason received"); diff --git a/src/hyperlight_host/src/hypervisor/gdb/mod.rs b/src/hyperlight_host/src/hypervisor/gdb/mod.rs index 29380a78b..2810717b0 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/mod.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/mod.rs @@ -3,7 +3,7 @@ pub(crate) mod arch; mod event_loop; -mod x86_64_target; +mod target; use std::io::{self, ErrorKind}; use std::net::TcpListener; @@ -15,8 +15,8 @@ use event_loop::event_loop_thread; use gdbstub::conn::ConnectionExt; use gdbstub::stub::GdbStub; use gdbstub::target::TargetError; +use target::HyperlightSandboxTarget; use thiserror::Error; -use x86_64_target::HyperlightSandboxTarget; use super::InterruptHandle; use super::regs::CommonRegisters; @@ -79,6 +79,9 @@ pub enum DebugMemoryAccessError { TranslateGuestAddress(u64), #[error("Failed to write to read-only region")] WriteToReadOnly, + #[cfg(all(target_arch = "aarch64", target_os = "windows"))] + #[error("Failed to flush the host instruction cache: {0}")] + FlushInstructionCache(String), } impl<'a> DebugMemoryView<'a> { @@ -96,6 +99,36 @@ impl<'a> DebugMemoryView<'a> { self.mem_mgr.layout.get_guest_code_gva() as u64 } + #[cfg(all(kvm, target_arch = "aarch64"))] + pub(crate) fn translate_gva(&self, gva: u64, root_pt: u64) -> Result { + use crate::sandbox::snapshot::PageTableReader; + + self.mem_mgr + .with_guest_memory(&self.guest_mmap_regions, |memory| { + let reader = PageTableReader::new(memory, root_pt); + // SAFETY: Debug requests run with the vCPU stopped. The reader bounds-checks table accesses. + let mapping = + unsafe { hyperlight_common::vmem::virt_to_phys(&reader, gva, 1) }.next(); + reader.finish()?; + let gpa = mapping + .filter(|mapping| { + matches!( + mapping.kind, + hyperlight_common::vmem::MappingKind::Basic(_) + | hyperlight_common::vmem::MappingKind::Cow(_) + ) + }) + .and_then(|mapping| { + let offset = gva.checked_sub(mapping.virt_base)?; + (offset < mapping.len) + .then(|| mapping.phys_base.checked_add(offset)) + .flatten() + }); + gpa.ok_or_else(|| crate::new_error!("GVA is not mapped: {gva:#x}")) + }) + .map_err(|_| DebugError::TranslateGva(gva)) + } + /// Reads memory from the guest's address space with a maximum length of a PAGE_SIZE /// /// # Arguments @@ -153,16 +186,61 @@ impl<'a> DebugMemoryView<'a> { BackingSource::Dynamic(_) => Err(DebugMemoryAccessError::WriteToReadOnly), } } + + pub(crate) fn flush_host_instruction_cache( + &self, + gpa: u64, + len: usize, + ) -> std::result::Result<(), DebugMemoryAccessError> { + #[cfg(target_arch = "aarch64")] + { + #[cfg(target_os = "windows")] + use windows::Win32::System::Diagnostics::Debug::FlushInstructionCache; + #[cfg(target_os = "windows")] + use windows::Win32::System::Threading::GetCurrentProcess; + + let range = self + .mem_mgr + .with_guest_memory(&self.guest_mmap_regions, |memory| { + memory.host_range(gpa, len) + }) + .map_err(|e| DebugMemoryAccessError::CopyFailed(Box::new(e)))?; + + #[cfg(target_os = "windows")] + // SAFETY: The current process handle is valid and the range belongs to a live sandbox mapping. + unsafe { + FlushInstructionCache(GetCurrentProcess(), Some(range.start as *const _), len) + .map_err(|e| DebugMemoryAccessError::FlushInstructionCache(e.to_string()))?; + } + + #[cfg(target_os = "linux")] + { + unsafe extern "C" { + fn __clear_cache(begin: *mut u8, end: *mut u8); + } + // SAFETY: The range is checked against the live sandbox backing. The vCPU is stopped. + unsafe { __clear_cache(range.start as *mut u8, range.end as *mut u8) }; + } + } + + #[cfg(not(target_arch = "aarch64"))] + 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, HwBp, + #[cfg(target_arch = "aarch64")] + Initial, SwBp, Interrupt, + #[cfg(target_arch = "x86_64")] Unknown, } @@ -218,6 +296,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), } @@ -226,20 +308,38 @@ pub enum DebugError { /// This extends the base VirtualMachine trait with GDB-specific functionality. pub(crate) trait DebuggableVm: VirtualMachine { /// Translates a guest virtual address to a guest physical address - fn translate_gva(&self, gva: u64) -> std::result::Result; + fn translate_gva( + &self, + gva: u64, + memory: &DebugMemoryView<'_>, + ) -> std::result::Result; /// Enable/disable debugging fn set_debug(&mut self, enable: bool) -> std::result::Result<(), DebugError>; /// Enable/disable single stepping - fn set_single_step(&mut self, enable: bool) -> std::result::Result<(), DebugError>; + 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(()) + } } /// Debug communication channel that is used for sending a request type and @@ -307,7 +407,7 @@ pub(crate) fn create_gdb_thread( let mut target = HyperlightSandboxTarget::new(hyp_conn); - // Waits for vCPU to stop at entrypoint breakpoint + // Wait for the vCPU interrupt handle. let msg = target.recv()?; if let DebugResponse::InterruptHandle(handle) = msg { tracing::info!("Received interrupt handle: {:?}", handle); @@ -316,7 +416,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); @@ -450,6 +550,53 @@ mod tests { } } + #[test] + #[cfg(all(kvm, target_arch = "aarch64"))] + fn translate_gva_resolves_page_offsets_and_rejects_missing_tables() -> crate::Result<()> { + use hyperlight_common::vmem::PAGE_SIZE; + + let memory = TestMemory::new()?; + let access = memory.access(); + let root = hyperlight_common::layout::scratch_base_gpa( + memory.mem_mgr.layout.get_scratch_size(), + ); + let mut tables = [0u8; 4 * PAGE_SIZE]; + for level in 0..3 { + let entry = (root + (level as u64 + 1) * PAGE_SIZE as u64) | 3; + tables[level * PAGE_SIZE..level * PAGE_SIZE + 8] + .copy_from_slice(&entry.to_le_bytes()); + } + tables[3 * PAGE_SIZE + 8..3 * PAGE_SIZE + 16] + .copy_from_slice(&(SandboxMemoryLayout::BASE_ADDRESS as u64 | 3).to_le_bytes()); + tables[3 * PAGE_SIZE + 16..3 * PAGE_SIZE + 24] + .copy_from_slice(&((1u64 << 2) | (1 << 8)).to_le_bytes()); + access.write(&tables, root).unwrap(); + let code = PAGE_SIZE as u64; + for offset in [0, 1, PAGE_SIZE as u64 - 1] { + assert_eq!( + access.translate_gva(code + offset, root).unwrap(), + SandboxMemoryLayout::BASE_ADDRESS as u64 + offset + ); + } + assert!(matches!( + access.translate_gva(0, root), + Err(DebugError::TranslateGva(0)) + )); + assert!(matches!( + access.translate_gva(code * 2, root), + Err(DebugError::TranslateGva(_)) + )); + assert!(matches!( + access.translate_gva(code | (1 << 48), root), + Err(DebugError::TranslateGva(_)) + )); + assert!(matches!( + access.translate_gva(code, !0xfff), + Err(DebugError::TranslateGva(_)) + )); + Ok(()) + } + #[test] fn test_mem_access_read_single_byte() -> crate::Result<()> { let mut memory = TestMemory::new()?; diff --git a/src/hyperlight_host/src/hypervisor/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..98be27aac 100644 --- a/src/hyperlight_host/src/hypervisor/gdb/x86_64_target.rs +++ b/src/hyperlight_host/src/hypervisor/gdb/target.rs @@ -16,6 +16,9 @@ use gdbstub::target::ext::breakpoints::{ }; 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 +72,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 +109,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 +200,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 +255,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 +277,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, @@ -476,6 +510,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 +529,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 +555,51 @@ mod tests { expected to fail" ); } + + #[cfg(target_arch = "aarch64")] + #[test] + fn aarch64_register_conversion_round_trip() { + let mut common_regs = CommonRegisters::default(); + for (index, value) in common_regs.x.iter_mut().enumerate() { + *value = 0x1000 + index as u64; + } + common_regs.sp = 0x2000; + common_regs.pc = 0x3000; + common_regs.pstate = 0xa000_03c5; + + let mut common_fpu = CommonFpu::default(); + for (index, value) in common_fpu.v.iter_mut().enumerate() { + *value = 0x4000 + index as u128; + } + common_fpu.fpcr = 0x5000; + common_fpu.fpsr = 0x6000; + + let (gdb_conn, hyp_conn) = DebugCommChannel::unbounded(); + let mut target = HyperlightSandboxTarget::new(hyp_conn); + gdb_conn + .send(DebugResponse::ReadRegisters(Box::new(( + common_regs, + common_fpu, + )))) + .unwrap(); + + let mut gdb_regs = AArch64CoreRegs::default(); + assert!(target.read_registers(&mut gdb_regs).is_ok()); + assert_eq!(gdb_regs.x, common_regs.x); + assert_eq!(gdb_regs.sp, common_regs.sp); + assert_eq!(gdb_regs.pc, common_regs.pc); + assert_eq!(gdb_regs.cpsr, common_regs.pstate as u32); + assert_eq!(gdb_regs.v, common_fpu.v); + assert_eq!(gdb_regs.fpcr, common_fpu.fpcr); + assert_eq!(gdb_regs.fpsr, common_fpu.fpsr); + assert!(matches!(gdb_conn.recv().unwrap(), DebugMsg::ReadRegisters)); + + gdb_conn.send(DebugResponse::WriteRegisters).unwrap(); + assert!(target.write_registers(&gdb_regs).is_ok()); + let DebugMsg::WriteRegisters(written) = gdb_conn.recv().unwrap() else { + panic!("unexpected request"); + }; + assert_eq!(written.0, common_regs); + assert_eq!(written.1, common_fpu); + } } diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs index 9f527987e..f16e9c054 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/aarch64.rs @@ -5,6 +5,8 @@ use std::sync::Arc; +#[cfg(gdb)] +use super::SoftwareBreakpoints; use super::{ AccessPageTableError, CreateHyperlightVmError, DispatchGuestCallError, HyperlightVm, InitializeError, @@ -17,9 +19,11 @@ use crate::hypervisor::LinuxInterruptHandle; #[cfg(target_os = "windows")] use crate::hypervisor::WindowsInterruptHandle; #[cfg(gdb)] -use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse}; +use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm}; use crate::hypervisor::hyperlight_vm::get_guest_log_filter; use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; +#[cfg(not(gdb))] +use crate::hypervisor::virtual_machine::VirtualMachine; #[cfg(hvf)] use crate::hypervisor::virtual_machine::hvf::HvfVm; #[cfg(kvm)] @@ -27,8 +31,7 @@ use crate::hypervisor::virtual_machine::kvm::KvmVm; #[cfg(target_os = "windows")] use crate::hypervisor::virtual_machine::whp::WhpVm; use crate::hypervisor::virtual_machine::{ - HypervisorType, RegisterError, ResetVcpuError, VirtualMachine, VmError, - get_available_hypervisor, + HypervisorType, RegisterError, ResetVcpuError, VmError, get_available_hypervisor, }; use crate::mem::mgr::SandboxMemoryManager; use crate::mem::shared_mem::{GuestSharedMemory, HostSharedMemory}; @@ -39,6 +42,31 @@ use crate::sandbox::snapshot::{NextAction, SnapshotMemoryBacking}; use crate::sandbox::trace::MemTraceInfo; impl HyperlightVm { + #[cfg(gdb)] + pub(crate) fn initialize_debug_vectors( + &mut self, + vectors: Option, + ) -> Result<(), CreateHyperlightVmError> { + if self.gdb_conn.is_none() + || cfg!(kvm) + || !matches!(self.next_action, NextAction::Initialise(_)) + { + return Ok(()); + } + let vectors = vectors.ok_or(VmError::Debug( + crate::hypervisor::gdb::DebugError::Unsupported( + "guest binary lacks ARM64 debug vectors", + ), + ))?; + let mut regs = self.vm.sregs().map_err(VmError::Register)?; + regs.vbar_el1 = vectors; + regs.sp_el1 = hyperlight_common::layout::SCRATCH_TOP_GVA as u64 + - hyperlight_common::layout::SCRATCH_TOP_EXN_STACK_OFFSET + + 1; + self.vm.set_sregs(®s).map_err(VmError::Register)?; + Ok(()) + } + #[allow(clippy::too_many_arguments)] #[cfg_attr(target_os = "macos", allow(unused))] pub(crate) fn new( @@ -49,10 +77,12 @@ impl HyperlightVm { rsp_gva: u64, page_size: usize, #[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration, - #[cfg(gdb)] _gdb_conn: Option>, + #[cfg(gdb)] gdb_conn: Option>, #[cfg(feature = "mem_profile")] _trace_info: MemTraceInfo, ) -> std::result::Result { - // TODO: support gdb on aarch64 + #[cfg(gdb)] + type VmType = Box; + #[cfg(not(gdb))] type VmType = Box; #[cfg(hvf)] let interrupt_handle: Arc = @@ -101,9 +131,26 @@ impl HyperlightVm { vm_can_reset_vcpu, pending_tlb_flush: false, + + #[cfg(gdb)] + gdb_conn, + #[cfg(gdb)] + sw_breakpoints: SoftwareBreakpoints::default(), + #[cfg(gdb)] + pending_software_step: None, + #[cfg(gdb)] + initial_debug_stop_pending: false, }; ret.update_snapshot_mapping(snapshot_mem)?; ret.update_scratch_mapping(scratch_mem)?; + + #[cfg(gdb)] + if ret.gdb_conn.is_some() { + ret.send_dbg_msg(DebugResponse::InterruptHandle(ret.interrupt_handle.clone()))?; + ret.vm.set_debug(true).map_err(VmError::Debug)?; + ret.initial_debug_stop_pending = true; + } + Ok(ret) } @@ -133,6 +180,10 @@ impl HyperlightVm { }; self.vm.set_regs(®s)?; + #[cfg(gdb)] + self.handle_initial_debug_stop(mem_mgr) + .map_err(super::RunVmError::DebugHandler)?; + self.run(mem_mgr, host_funcs) .map_err(InitializeError::Run)?; @@ -170,6 +221,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); @@ -198,6 +252,12 @@ impl HyperlightVm { self.vm_can_reset_vcpu, "No fallback path for vcpu reset on aarch64" ); + #[cfg(gdb)] + if self.gdb_conn.is_some() { + self.vm + .set_single_step(false) + .map_err(|error| ResetVcpuError::Unknown(error.to_string()))?; + } self.vm.reset_vcpu()?; self.apply_sregs(cr3, sregs)?; Ok(()) diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs new file mode 100644 index 000000000..da1cd1114 --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs @@ -0,0 +1,672 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 The Hyperlight Authors. + +use std::collections::HashMap; + +use hyperlight_common::vmem::PAGE_SIZE; + +use super::{ + HandleDebugError, HyperlightVm, RecvDbgMsgError, SendDbgMsgError, VcpuStopReason, VmError, +}; +use crate::hypervisor::gdb::arch::{SW_BP, SW_BP_SIZE, valid_sw_breakpoint_address}; +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, + 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), + #[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() + } + + /// Discard software debug state when poisoned guest memory will be replaced. + #[cfg(target_arch = "aarch64")] + pub(crate) fn discard_software_breakpoints(&mut self) { + self.sw_breakpoints.0.clear(); + self.pending_software_step = None; + } + + pub(super) fn handle_debug( + &mut self, + mem_mgr: &crate::mem::mgr::SandboxMemoryManager, + 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; + #[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}"); + self.vm + .set_debug(false) + .map_err(ProcessDebugRequestError::from)?; + self.discard_software_breakpoints(); + DebugResponse::DisableDebug + } + } + #[cfg(not(target_arch = "aarch64"))] + 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 + } + 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)?; + } else { + self.vm.set_single_step(false)?; + } + } + #[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, mem_access)?; + let read_len = std::cmp::min(data.len(), PAGE_SIZE - (gva as usize & (PAGE_SIZE - 1))); + mem_access.read(&mut data[..read_len], gpa)?; + data = &mut data[read_len..]; + gva += read_len as u64; + } + + 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, mem_access)?; + let write_len = std::cmp::min(data.len(), PAGE_SIZE - (gva as usize & (PAGE_SIZE - 1))); + mem_access + .write(&data[..write_len], gpa) + .map_err(ProcessDebugRequestError::WriteMemory)?; + 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) { + 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}" + ); + } + 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) { + 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}" + ); + } + 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 Some(saved_instruction) = self.sw_breakpoints.get(pc).map(<[u8]>::to_vec) else { + self.vm.set_single_step(true)?; + return Ok(()); + }; + let result = self + .write_addrs(pc, &saved_instruction, mem_access) + .and_then(|()| self.vm.set_single_step(true).map_err(Into::into)); + if let Err(error) = result { + if let Err(rollback_error) = self.write_addrs(pc, &SW_BP, mem_access) { + tracing::error!( + "Failed to roll back breakpoint-step origin {pc:#x}: {rollback_error}" + ); + } + return Err(error); + } + + self.pending_software_step = Some(PendingSoftwareStep { + origin: pc, + report_stop, + }); + Ok(()) + } + + #[cfg(target_arch = "aarch64")] + fn cancel_software_step( + &mut self, + mem_access: &DebugMemoryView<'_>, + ) -> std::result::Result<(), ProcessDebugRequestError> { + self.vm.set_single_step(false)?; + let Some(pending) = self.pending_software_step.clone() else { + return Ok(()); + }; + 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> { + self.write_addrs(pending.origin, &SW_BP, mem_access)?; + Ok(()) + } + + #[cfg(target_arch = "aarch64")] + pub(super) fn cancel_pending_software_step( + &mut self, + mem_mgr: &crate::mem::mgr::SandboxMemoryManager, + ) -> std::result::Result<(), ProcessDebugRequestError> { + if self.gdb_conn.is_none() { + return Ok(()); + } + let mapped_regions = self.get_mapped_regions().cloned().collect(); + let mem_access = DebugMemoryView::new(mem_mgr, mapped_regions); + self.cancel_software_step(&mem_access) + } + + #[cfg(target_arch = "aarch64")] + pub(super) fn finish_software_step( + &mut self, + mem_mgr: &crate::mem::mgr::SandboxMemoryManager, + stop_reason: VcpuStopReason, + ) -> std::result::Result, ProcessDebugRequestError> { + let pending = self + .pending_software_step + .clone() + .expect("software-step completion requires pending state"); + let mapped_regions = self.get_mapped_regions().cloned().collect(); + let mem_access = DebugMemoryView::new(mem_mgr, mapped_regions); + self.restore_software_step(&pending, &mem_access)?; + self.vm.set_single_step(false)?; + self.pending_software_step = None; + + if pending.report_stop || !matches!(stop_reason, VcpuStopReason::DoneStep) { + Ok(Some(stop_reason)) + } 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()) + ); + } + + #[cfg(all(kvm, target_arch = "aarch64"))] + mod memory_access_tests { + use hyperlight_common::vmem::PAGE_SIZE; + + use super::super::*; + use crate::hypervisor::gdb::DebugCommChannel; + use crate::mem::layout::SandboxMemoryLayout; + use crate::mem::mgr::SandboxMemoryManager; + use crate::mem::shared_mem::{ExclusiveSharedMemory, HostSharedMemory}; + use crate::sandbox::SandboxConfiguration; + use crate::sandbox::snapshot::{NextAction, SnapshotMemoryBacking}; + + fn noncontiguous_pages() -> (HyperlightVm, SandboxMemoryManager, u64) { + let mut config = SandboxConfiguration::default(); + config.set_scratch_size(config.get_scratch_size().next_multiple_of(page_size::get())); + let layout = SandboxMemoryLayout::new(config, PAGE_SIZE, 0, None).unwrap(); + let snapshot = crate::sandbox::snapshot::memory::tests::flat_memory( + &vec![0; page_size::get()], + &[0; PAGE_SIZE], + SandboxMemoryLayout::BASE_ADDRESS.next_multiple_of(page_size::get()) as u64, + hyperlight_common::layout::scratch_base_gpa(layout.get_scratch_size()), + ); + let memory = SandboxMemoryManager::new( + layout, + SnapshotMemoryBacking::from_snapshot(std::sync::Arc::new(snapshot)).unwrap(), + ExclusiveSharedMemory::new(layout.get_scratch_size()).unwrap(), + NextAction::None, + ); + let (memory, guest_memory) = memory.build().unwrap(); + let (_debugger, connection) = DebugCommChannel::unbounded(); + let mut vm = HyperlightVm::new( + guest_memory.shared_mem, + guest_memory.scratch_mem, + guest_memory.layout.get_pt_base_gpa(), + guest_memory.next_action, + 0, + page_size::get(), + &config, + Some(connection), + #[cfg(feature = "mem_profile")] + crate::sandbox::trace::MemTraceInfo::new(crate::mem::exe::LoadInfo::dummy().info) + .unwrap(), + ) + .unwrap(); + let root = memory.first_free_scratch_gpa(); + let access = DebugMemoryView::new(&memory, Vec::new()); + let mut tables = [0; 4 * PAGE_SIZE]; + for level in 0..3 { + let entry = (root + (level as u64 + 1) * PAGE_SIZE as u64) | 3; + tables[level * PAGE_SIZE..level * PAGE_SIZE + 8] + .copy_from_slice(&entry.to_le_bytes()); + } + for page in 0..3 { + let entry = (root + (4 + 2 * page) as u64 * PAGE_SIZE as u64) | 3; + let offset = 3 * PAGE_SIZE + (page + 1) * 8; + tables[offset..offset + 8].copy_from_slice(&entry.to_le_bytes()); + access + .write(&vec![page as u8 + 1; PAGE_SIZE], entry & !0xfff) + .unwrap(); + } + for page in [5, 7] { + access + .write(&[0xee; PAGE_SIZE], root + page * PAGE_SIZE as u64) + .unwrap(); + } + access.write(&tables, root).unwrap(); + let registers = vm.get_snapshot_sregs().unwrap(); + vm.apply_sregs(root, ®isters).unwrap(); + (vm, memory, root) + } + + #[test] + fn reads_translate_each_noncontiguous_guest_page() { + let (mut vm, memory, _) = noncontiguous_pages(); + let access = DebugMemoryView::new(&memory, Vec::new()); + let address = (2 * PAGE_SIZE - 3) as u64; + let DebugResponse::ReadAddr(data) = vm + .process_dbg_request(DebugMsg::ReadAddr(address, PAGE_SIZE + 8), &access) + .unwrap() + else { + panic!("missing memory read response"); + }; + assert_eq!(&data[..3], &[1; 3]); + assert!(data[3..3 + PAGE_SIZE].iter().all(|&byte| byte == 2)); + assert_eq!(&data[3 + PAGE_SIZE..], &[3; 5]); + } + + #[test] + fn writes_preserve_gaps_between_noncontiguous_guest_pages() { + let (mut vm, memory, root) = noncontiguous_pages(); + let access = DebugMemoryView::new(&memory, Vec::new()); + let address = (2 * PAGE_SIZE - 3) as u64; + assert!(matches!( + vm.process_dbg_request( + DebugMsg::WriteAddr(address, vec![0xab; PAGE_SIZE + 8]), + &access, + ) + .unwrap(), + DebugResponse::WriteAddr + )); + let mut data = [0; PAGE_SIZE]; + for page in [5, 7] { + access + .read(&mut data, root + page * PAGE_SIZE as u64) + .unwrap(); + assert!(data.iter().all(|&byte| byte == 0xee)); + } + for (page, prefix, suffix) in [(4, PAGE_SIZE - 3, 0), (6, 0, 0), (8, 0, PAGE_SIZE - 5)] + { + access + .read(&mut data, root + page * PAGE_SIZE as u64) + .unwrap(); + let original = ((page - 4) / 2 + 1) as u8; + assert!(data[..prefix].iter().all(|&byte| byte == original)); + assert!( + data[prefix..PAGE_SIZE - suffix] + .iter() + .all(|&byte| byte == 0xab) + ); + assert!( + data[PAGE_SIZE - suffix..] + .iter() + .all(|&byte| byte == original) + ); + } + } + } +} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs index 20297eaff..c02f25adc 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -7,28 +7,29 @@ mod x86_64; #[cfg(target_arch = "aarch64")] mod aarch64; +#[cfg(gdb)] +mod debug; + #[cfg(all(test, not(gdb)))] pub(crate) mod test_support; -#[cfg(gdb)] -use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, Mutex}; use hyperlight_common::log_level::GuestLogFilter; use tracing_core::LevelFilter; +#[cfg(gdb)] +use self::debug::{ProcessDebugRequestError, SoftwareBreakpoints}; use crate::HyperlightError; #[cfg(gdb)] use crate::hypervisor::gdb::DebuggableVm; -#[cfg(gdb)] +#[cfg(all(gdb, target_arch = "x86_64"))] use crate::hypervisor::gdb::arch::VcpuStopReasonError; #[cfg(gdb)] use crate::hypervisor::gdb::{ DebugCommChannel, DebugError, DebugMsg, DebugResponse, GdbTargetError, VcpuStopReason, }; -#[cfg(gdb)] -use crate::hypervisor::hyperlight_vm::x86_64::debug::ProcessDebugRequestError; #[cfg(not(gdb))] use crate::hypervisor::virtual_machine::VirtualMachine; use crate::hypervisor::virtual_machine::{ @@ -206,7 +207,7 @@ pub enum RunVmError { RunVcpu(#[from] RunVcpuError), #[error("Unexpected VM exit: {0}")] UnexpectedVmExit(String), - #[cfg(gdb)] + #[cfg(all(gdb, target_arch = "x86_64"))] #[error("vCPU stop reason error: {0}")] VcpuStopReason(#[from] VcpuStopReasonError), } @@ -257,6 +258,9 @@ pub enum UpdateRegionError { MapMemory(#[from] MapMemoryError), #[error("VM unmap memory error: {0}")] UnmapMemory(#[from] UnmapMemoryError), + #[cfg(all(gdb, target_arch = "aarch64"))] + #[error("Cannot replace snapshot memory while software breakpoints are installed")] + ActiveSoftwareBreakpoints, } /// Errors that can occur when accessing the root page table state @@ -387,14 +391,18 @@ pub(crate) struct HyperlightVm { #[cfg(gdb)] pub(super) gdb_conn: Option>, #[cfg(gdb)] - pub(super) sw_breakpoints: HashMap, // addr -> original instruction + pub(super) sw_breakpoints: SoftwareBreakpoints, + #[cfg(all(gdb, target_arch = "aarch64"))] + pending_software_step: Option, /// One-shot hw breakpoint installed at the entry address when gdb is /// enabled, so the gdb stub gets a `VcpuStopped` to enter its event /// loop on the first vCPU run after construction. Cleared by the /// `VmExit::Debug` arm of `run` the first time a `HwBp` stop fires /// at the entry address. - #[cfg(gdb)] + #[cfg(all(gdb, target_arch = "x86_64"))] pub(super) one_shot_entry_bp: Option, + #[cfg(all(gdb, target_arch = "aarch64"))] + pub(super) initial_debug_stop_pending: bool, #[cfg(feature = "mem_profile")] pub(super) trace_info: MemTraceInfo, #[cfg(crashdump)] @@ -484,6 +492,23 @@ impl HyperlightVm { } } + // Check against the hidden debug cache-sync page + #[cfg(all(gdb, target_arch = "aarch64", target_os = "windows"))] + { + use crate::hypervisor::virtual_machine::whp::DEBUG_CACHE_SYNC_GPA; + + let reserved_start = DEBUG_CACHE_SYNC_GPA as usize; + let reserved_end = reserved_start + self.page_size; + if new_start < reserved_end && new_end > reserved_start { + return Err(MapRegionError::Overlapping { + new_start, + new_end, + existing_start: reserved_start, + existing_end: reserved_end, + }); + } + } + // Try to reuse a freed slot first, otherwise use next_slot let slot = self.allocate_slot(); @@ -524,6 +549,11 @@ impl HyperlightVm { &mut self, snapshot: SnapshotMemoryBacking, ) -> Result<(), UpdateRegionError> { + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.has_sw_breakpoints() { + return Err(UpdateRegionError::ActiveSoftwareBreakpoints); + } + let target_mappings = snapshot.mappings(); for index in (0..self.snapshot_mappings.len()).rev() { @@ -680,7 +710,15 @@ impl HyperlightVm { // - Signals will not be sent match exit_reason { #[cfg(gdb)] - Ok(VmExit::Debug { dr6, exception }) => { + Ok(VmExit::Debug { + #[cfg(target_arch = "x86_64")] + dr6, + #[cfg(target_arch = "x86_64")] + exception, + #[cfg(target_arch = "aarch64")] + reason: stop_reason, + }) => { + #[cfg(target_arch = "x86_64")] // Classify the debug exit. `vcpu_stop_reason` is a // pure classifier and has no side effects on the VM. let stop_reason = crate::hypervisor::gdb::arch::vcpu_stop_reason( @@ -688,6 +726,7 @@ impl HyperlightVm { dr6, exception, )?; + #[cfg(target_arch = "x86_64")] // Remove the one-shot entry breakpoint installed by // `HyperlightVm::new` the first time it fires so it // does not interfere with later user-installed @@ -703,12 +742,25 @@ 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, stop_reason) { + 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()); } } Ok(VmExit::Halt()) => { + #[cfg(all(gdb, target_arch = "aarch64"))] + self.cancel_pending_software_step(mem_mgr) + .map_err(HandleDebugError::from)?; break Ok(()); } Ok(VmExit::IoOut(port, data)) => { @@ -764,13 +816,21 @@ impl HyperlightVm { continue; } + #[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(); if let Err(e) = self.handle_debug(mem_mgr, VcpuStopReason::Interrupt) { break Err(e.into()); } + if !cancel_requested { + continue; + } } metrics::counter!(METRIC_GUEST_CANCELLATION).increment(1); diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs index 9463cea4f..6dbe1eb7a 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/x86_64.rs @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2025 The Hyperlight Authors. -#[cfg(gdb)] -use std::collections::HashMap; use std::sync::{Arc, Mutex}; use tracing::{Span, instrument}; @@ -17,11 +15,7 @@ use crate::hypervisor::WindowsInterruptHandle; #[cfg(crashdump)] use crate::hypervisor::crashdump; #[cfg(gdb)] -use crate::hypervisor::gdb::{ - DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm, VcpuStopReason, -}; -#[cfg(gdb)] -use crate::hypervisor::gdb::{DebugError, DebugMemoryAccessError}; +use crate::hypervisor::gdb::{DebugCommChannel, DebugMsg, DebugResponse, DebuggableVm}; use crate::hypervisor::regs::{ CommonDebugRegs, CommonFpu, CommonRegisters, CommonSpecialRegisters, MsrEntry, MsrResetState, }; @@ -126,7 +120,7 @@ impl HyperlightVm { #[cfg(gdb)] gdb_conn, #[cfg(gdb)] - sw_breakpoints: HashMap::new(), + sw_breakpoints: SoftwareBreakpoints::default(), #[cfg(gdb)] one_shot_entry_bp: None, #[cfg(feature = "mem_profile")] @@ -372,145 +366,6 @@ impl HyperlightVm { Ok(()) } - // Handle a debug exit - #[cfg(gdb)] - pub(super) fn handle_debug( - &mut self, - mem_mgr: &SandboxMemoryManager, - stop_reason: VcpuStopReason, - ) -> std::result::Result<(), HandleDebugError> { - use debug::ProcessDebugRequestError; - - use crate::hypervisor::gdb::DebugMemoryView; - - if self.gdb_conn.is_none() { - return Err(HandleDebugError::DebugNotEnabled); - } - - let mem_access = - DebugMemoryView::new(mem_mgr, self.get_mapped_regions().cloned().collect()); - - match stop_reason { - // If the vCPU stopped because of a crash, we need to handle it differently - // We do not want to allow resuming execution or placing breakpoints - // because the guest has crashed. - // We only allow reading registers and memory - VcpuStopReason::Crash => { - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason))?; - - loop { - tracing::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - // Flag to store if we should deny continue or step requests - let mut deny_continue = false; - // Flag to store if we should detach from the gdb session - let mut detach = false; - - let response = match req { - // Allow the detach request to disable debugging by continuing resuming - // hypervisor crash error reporting - DebugMsg::DisableDebug => { - detach = true; - DebugResponse::DisableDebug - } - // Do not allow continue or step requests - DebugMsg::Continue | DebugMsg::Step => { - deny_continue = true; - DebugResponse::NotAllowed - } - // Do not allow adding/removing breakpoints and writing to memory or registers - DebugMsg::AddHwBreakpoint(_) - | DebugMsg::AddSwBreakpoint(_) - | DebugMsg::RemoveHwBreakpoint(_) - | DebugMsg::RemoveSwBreakpoint(_) - | DebugMsg::WriteAddr(_, _) - | DebugMsg::WriteRegisters(_) => DebugResponse::NotAllowed, - - // For all other requests, we will process them normally - _ => { - let result = self.process_dbg_request(req, &mem_access); - match result { - Ok(response) => response, - // Treat non-fatal errors separately so the guest doesn't fail - Err(ProcessDebugRequestError::ReadMemory( - DebugMemoryAccessError::TranslateGuestAddress(_), - )) - | Err(ProcessDebugRequestError::Debug(DebugError::TranslateGva( - _, - ))) => DebugResponse::ErrorOccurred, - Err(e) => { - tracing::error!("Error processing debug request: {:?}", e); - return Err(HandleDebugError::ProcessRequest(e)); - } - } - } - }; - - // Send the response to the request back to gdb - self.send_dbg_msg(response)?; - - // If we are denying continue or step requests, the debugger assumes the - // execution started so we need to report a stop reason as a crash and let - // it request to read registers/memory to figure out what happened - if deny_continue { - self.send_dbg_msg(DebugResponse::VcpuStopped(VcpuStopReason::Crash))?; - } - - // If we are detaching, we will break the loop and the Hypervisor will continue - // to handle the Crash reason - if detach { - break; - } - } - } - // If the vCPU stopped because of any other reason except a crash, we can handle it - // normally - _ => { - // Send the stop reason to the gdb thread - self.send_dbg_msg(DebugResponse::VcpuStopped(stop_reason))?; - - loop { - tracing::debug!("Debug wait for event to resume vCPU"); - // Wait for a message from gdb - let req = self.recv_dbg_msg()?; - - let result = self.process_dbg_request(req, &mem_access); - - let response = match result { - Ok(response) => response, - // Treat non-fatal errors separately so the guest doesn't fail - Err(ProcessDebugRequestError::ReadMemory( - DebugMemoryAccessError::TranslateGuestAddress(_), - )) - | Err(ProcessDebugRequestError::Debug(DebugError::TranslateGva(_))) => { - DebugResponse::ErrorOccurred - } - Err(e) => { - return Err(HandleDebugError::ProcessRequest(e)); - } - }; - - let cont = matches!( - response, - DebugResponse::Continue | DebugResponse::Step | DebugResponse::DisableDebug - ); - - self.send_dbg_msg(response)?; - - // Check if we should continue execution - // We continue if the response is one of the following: Step, Continue, or DisableDebug - if cont { - break; - } - } - } - } - - Ok(()) - } - #[cfg(crashdump)] pub(crate) fn crashdump_context( &self, @@ -586,270 +441,6 @@ impl HyperlightVm { } } -#[cfg(gdb)] -pub(super) mod debug { - use super::HyperlightVm; - use crate::hypervisor::gdb::arch::{SW_BP, SW_BP_SIZE}; - use crate::hypervisor::gdb::{ - DebugError, DebugMemoryAccessError, DebugMemoryView, DebugMsg, DebugResponse, - }; - use crate::hypervisor::virtual_machine::VmError; - - /// Errors that can occur during GDB debug request processing - #[derive(Debug, thiserror::Error)] - pub enum ProcessDebugRequestError { - #[error("Debug is not enabled")] - DebugNotEnabled, - #[error("VM operation error: {0}")] - Vm(#[from] VmError), - #[error("Debug operation error: {0}")] - Debug(#[from] DebugError), - #[error("Address {0:#x} is not a software breakpoint")] - SwBreakpointNotFound(u64), - #[error("Failed to read memory: {0}")] - ReadMemory(#[from] DebugMemoryAccessError), - #[error("Failed to write memory: {0}")] - WriteMemory(DebugMemoryAccessError), - } - - impl HyperlightVm { - pub(crate) fn process_dbg_request( - &mut self, - req: DebugMsg, - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result { - if self.gdb_conn.is_some() { - match req { - DebugMsg::AddHwBreakpoint(addr) => Ok(DebugResponse::AddHwBreakpoint( - self.vm - .add_hw_breakpoint(addr) - .map_err(|e| { - tracing::error!("Failed to add hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::AddSwBreakpoint(addr) => Ok(DebugResponse::AddSwBreakpoint( - self.add_sw_breakpoint(addr, mem_access) - .map_err(|e| { - tracing::error!("Failed to add sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Continue => { - self.vm.set_single_step(false).map_err(|e| { - tracing::error!("Failed to continue execution: {:?}", e); - - e - })?; - - Ok(DebugResponse::Continue) - } - DebugMsg::DisableDebug => { - self.vm.set_debug(false).map_err(|e| { - tracing::error!("Failed to disable debugging: {:?}", e); - e - })?; - - Ok(DebugResponse::DisableDebug) - } - DebugMsg::GetCodeSectionOffset => Ok(DebugResponse::GetCodeSectionOffset( - mem_access.code_section_offset(), - )), - DebugMsg::ReadAddr(addr, len) => { - let mut data = vec![0u8; len]; - - self.read_addrs(addr, &mut data, mem_access).map_err(|e| { - tracing::error!("Failed to read from address: {:?}", e); - - e - })?; - - Ok(DebugResponse::ReadAddr(data)) - } - DebugMsg::ReadRegisters => { - let regs = self.vm.regs().map_err(VmError::Register)?; - let fpu = self.vm.fpu().map_err(VmError::Register)?; - Ok(DebugResponse::ReadRegisters(Box::new((regs, fpu)))) - } - DebugMsg::RemoveHwBreakpoint(addr) => Ok(DebugResponse::RemoveHwBreakpoint( - self.vm - .remove_hw_breakpoint(addr) - .map_err(|e| { - tracing::error!("Failed to remove hw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::RemoveSwBreakpoint(addr) => Ok(DebugResponse::RemoveSwBreakpoint( - self.remove_sw_breakpoint(addr, mem_access) - .map_err(|e| { - tracing::error!("Failed to remove sw breakpoint: {:?}", e); - - e - }) - .is_ok(), - )), - DebugMsg::Step => { - self.vm.set_single_step(true).map_err(|e| { - tracing::error!("Failed to enable step instruction: {:?}", e); - - e - })?; - - Ok(DebugResponse::Step) - } - DebugMsg::WriteAddr(addr, data) => { - self.write_addrs(addr, &data, mem_access).map_err(|e| { - tracing::error!("Failed to write to address: {:?}", e); - - e - })?; - - Ok(DebugResponse::WriteAddr) - } - DebugMsg::WriteRegisters(boxed_regs) => { - let (regs, fpu) = boxed_regs.as_ref(); - self.vm.set_regs(regs).map_err(VmError::Register)?; - self.vm.set_fpu(fpu).map_err(VmError::Register)?; - - Ok(DebugResponse::WriteRegisters) - } - } - } else { - Err(ProcessDebugRequestError::DebugNotEnabled) - } - } - - pub(crate) fn recv_dbg_msg( - &mut self, - ) -> std::result::Result { - use super::RecvDbgMsgError; - - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or(RecvDbgMsgError::DebugNotEnabled)?; - - Ok(gdb_conn.recv()?) - } - - pub(crate) fn send_dbg_msg( - &mut self, - cmd: DebugResponse, - ) -> std::result::Result<(), super::SendDbgMsgError> { - use super::SendDbgMsgError; - - tracing::debug!("Sending {:?}", cmd); - - let gdb_conn = self - .gdb_conn - .as_mut() - .ok_or(SendDbgMsgError::DebugNotEnabled)?; - - Ok(gdb_conn.send(cmd)?) - } - - fn read_addrs( - &mut self, - mut gva: u64, - mut data: &mut [u8], - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result<(), ProcessDebugRequestError> { - let data_len = data.len(); - tracing::debug!("Read addr: {:X} len: {:X}", gva, data_len); - - while !data.is_empty() { - let gpa = self.vm.translate_gva(gva)?; - - let read_len = std::cmp::min( - data.len(), - page_size::get() - (gpa as usize & (page_size::get() - 1)), - ); - - mem_access.read(&mut data[..read_len], gpa)?; - - data = &mut data[read_len..]; - gva += read_len as u64; - } - - Ok(()) - } - - /// Copies the data from the provided slice to the guest memory address - /// The address is checked to be a valid guest address - fn write_addrs( - &mut self, - mut gva: u64, - mut data: &[u8], - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result<(), ProcessDebugRequestError> { - let data_len = data.len(); - tracing::debug!("Write addr: {:X} len: {:X}", gva, data_len); - - while !data.is_empty() { - let gpa = self.vm.translate_gva(gva)?; - - let write_len = std::cmp::min( - data.len(), - page_size::get() - (gpa as usize & (page_size::get() - 1)), - ); - - // Use the memory access to write to guest memory - mem_access - .write(&data[..write_len], gpa) - .map_err(ProcessDebugRequestError::WriteMemory)?; - - data = &data[write_len..]; - gva += write_len as u64; - } - - Ok(()) - } - - // Must be idempotent! - fn add_sw_breakpoint( - &mut self, - gva: u64, - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result<(), ProcessDebugRequestError> { - // Check if breakpoint already exists - if self.sw_breakpoints.contains_key(&gva) { - return Ok(()); - } - - // Write breakpoint OP code to write to guest memory - let mut save_data = [0; SW_BP_SIZE]; - self.read_addrs(gva, &mut save_data[..], mem_access)?; - self.write_addrs(gva, &SW_BP, mem_access)?; - - // Save guest memory to restore when breakpoint is removed - self.sw_breakpoints.insert(gva, save_data[0]); - - Ok(()) - } - - fn remove_sw_breakpoint( - &mut self, - gva: u64, - mem_access: &DebugMemoryView<'_>, - ) -> std::result::Result<(), ProcessDebugRequestError> { - if let Some(saved_data) = self.sw_breakpoints.remove(&gva) { - // Restore saved data to the guest's memory - self.write_addrs(gva, &[saved_data], mem_access)?; - - Ok(()) - } else { - Err(ProcessDebugRequestError::SwBreakpointNotFound(gva)) - } - } - } -} - #[cfg(test)] #[allow(clippy::needless_range_loop)] mod tests { diff --git a/src/hyperlight_host/src/hypervisor/regs/aarch64/whp_reg.rs b/src/hyperlight_host/src/hypervisor/regs/aarch64/whp_reg.rs index 4a7e01b30..6df723be7 100644 --- a/src/hyperlight_host/src/hypervisor/regs/aarch64/whp_reg.rs +++ b/src/hyperlight_host/src/hypervisor/regs/aarch64/whp_reg.rs @@ -31,6 +31,18 @@ pub(crate) const WHV_ARM64_REGISTER_MAIR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_N pub(crate) const WHV_ARM64_REGISTER_VBAR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0004000C); pub(crate) const WHV_ARM64_REGISTER_FPCR: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040012); pub(crate) const WHV_ARM64_REGISTER_FPSR: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040013); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_SPSR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040014); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_ELR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040015); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_ESR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00040008); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_MDSCR_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x0005004D); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_DBGBCR0_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00050000); +#[cfg(gdb)] +pub(crate) const WHV_ARM64_REGISTER_DBGBVR0_EL1: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00050020); pub(crate) const WHV_ARM64_REGISTER_GICR_BASE_GPA: WHV_REGISTER_NAME = WHV_REGISTER_NAME(0x00063000); diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs index fdd8d4963..f16cb1ffc 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/aarch64.rs @@ -1,8 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2025 The Hyperlight Authors. -// TODO(aarch64): implement KVM backend - use std::sync::LazyLock; use hyperlight_common::outb::VmAction; @@ -12,6 +10,8 @@ use kvm_bindings::{ use kvm_ioctls::{Kvm, VcpuExit, VcpuFd, VmFd}; use tracing::{Span, instrument}; +#[cfg(gdb)] +use crate::hypervisor::gdb::{DebugError, DebugMemoryView, DebuggableVm, VcpuStopReason}; use crate::hypervisor::regs::{CommonFpu, CommonRegisters, CommonSpecialRegisters}; use crate::hypervisor::virtual_machine::{ CreateVmError, HypervisorError, MapMemoryError, RegisterError, ResetVcpuError, RunVcpuError, @@ -21,6 +21,9 @@ use crate::hypervisor::virtual_machine::{ static KVM: LazyLock> = LazyLock::new(|| Kvm::new().map_err(|e| CreateVmError::HypervisorNotAvailable(e.into()))); +#[cfg(gdb)] +const HARDWARE_BREAKPOINTS: usize = 2; + /// Return `true` if the KVM API is available #[instrument(skip_all, parent = Span::current(), level = "Trace")] pub(crate) fn is_hypervisor_present() -> bool { @@ -37,6 +40,8 @@ pub(crate) fn is_hypervisor_present() -> bool { pub(crate) struct KvmVm { vm_fd: VmFd, vcpu_fd: VcpuFd, + #[cfg(gdb)] + debug_state: kvm_bindings::kvm_guest_debug, } impl KvmVm { @@ -76,7 +81,12 @@ impl KvmVm { .create_vcpu(0) .map_err(|e| CreateVmError::CreateVcpuFd(e.into()))?; - let mut to_ret = Self { vm_fd, vcpu_fd }; + let mut to_ret = Self { + vm_fd, + vcpu_fd, + #[cfg(gdb)] + debug_state: kvm_bindings::kvm_guest_debug::default(), + }; to_ret .vcpu_init() .map_err(CreateVmError::SetPartitionProperty)?; @@ -103,6 +113,18 @@ impl KvmVm { self.vcpu_fd.set_kvm_immediate_exit(0u8); ret } + + #[cfg(gdb)] + fn apply_debug_state( + &mut self, + state: kvm_bindings::kvm_guest_debug, + ) -> Result<(), DebugError> { + self.vcpu_fd + .set_guest_debug(&state) + .map_err(|e| RegisterError::SetDebugRegs(e.into()))?; + self.debug_state = state; + Ok(()) + } } impl VirtualMachine for KvmVm { @@ -201,6 +223,21 @@ impl VirtualMachine for KvmVm { break exit; }; match exit { + #[cfg(gdb)] + Ok(VcpuExit::Debug(debug)) => { + let reason = match debug.hsr >> 26 { + 0x30 => VcpuStopReason::HwBp, + 0x32 => VcpuStopReason::DoneStep, + 0x3c => VcpuStopReason::SwBp, + _ => { + return Ok(VmExit::Unknown(format!( + "Unknown KVM debug syndrome: {:#x}", + debug.hsr + ))); + } + }; + Ok(VmExit::Debug { reason }) + } Ok(VcpuExit::MmioWrite(addr, data)) => { let io_page_gpa = const { hyperlight_common::layout::io_page().unwrap().0 }; if addr >= io_page_gpa @@ -260,25 +297,28 @@ impl VirtualMachine for KvmVm { } fn regs(&self) -> std::result::Result { - use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, X}; + use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, SP_EL1, X}; let mut x: [u64; 31] = [0; 31]; for (i, xi) in X.iter().enumerate() { x[i] = xi.get(RegisterError::GetSregs, &self.vcpu_fd)?; } + let pstate = PSTATE.get(RegisterError::GetSregs, &self.vcpu_fd)?; + let sp = if pstate & 1 == 0 { SP } else { SP_EL1 }; Ok(CommonRegisters { x, - sp: SP.get(RegisterError::GetSregs, &self.vcpu_fd)?, + sp: sp.get(RegisterError::GetSregs, &self.vcpu_fd)?, pc: PC.get(RegisterError::GetSregs, &self.vcpu_fd)?, - pstate: PSTATE.get(RegisterError::GetSregs, &self.vcpu_fd)?, + pstate, }) } fn set_regs(&mut self, regs: &CommonRegisters) -> std::result::Result<(), RegisterError> { - use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, X}; + use crate::hypervisor::regs::kvm_reg::{PC, PSTATE, SP, SP_EL1, X}; for (i, xi) in X.iter().enumerate() { xi.set(RegisterError::SetSregs, &self.vcpu_fd, regs.x[i])?; } - SP.set(RegisterError::SetSregs, &self.vcpu_fd, regs.sp)?; + let sp = if regs.pstate & 1 == 0 { SP } else { SP_EL1 }; + sp.set(RegisterError::SetSregs, &self.vcpu_fd, regs.sp)?; PC.set(RegisterError::SetSregs, &self.vcpu_fd, regs.pc)?; PSTATE.set(RegisterError::SetSregs, &self.vcpu_fd, regs.pstate)?; @@ -366,6 +406,143 @@ impl VirtualMachine for KvmVm { .map_err(ResetVcpuError::Unknown) .unwrap_or_else(|e| e) })?; + #[cfg(gdb)] + if self.debug_state.control != 0 { + self.vcpu_fd + .set_guest_debug(&self.debug_state) + .map_err(|e| ResetVcpuError::Hypervisor(e.into()))?; + } Ok(()) } } + +#[cfg(gdb)] +impl DebuggableVm for KvmVm { + fn translate_gva(&self, gva: u64, memory: &DebugMemoryView<'_>) -> Result { + use crate::hypervisor::regs::kvm_reg::TTBR0_EL1; + + let root = TTBR0_EL1.get(RegisterError::GetSregs, &self.vcpu_fd)? & 0x0000_ffff_ffff_f000; + memory.translate_gva(gva, root) + } + + fn set_debug(&mut self, enable: bool) -> Result<(), DebugError> { + use kvm_bindings::{KVM_GUESTDBG_ENABLE, KVM_GUESTDBG_USE_HW, KVM_GUESTDBG_USE_SW_BP}; + + let state = if enable { + let mut state = self.debug_state; + state.control |= KVM_GUESTDBG_ENABLE | KVM_GUESTDBG_USE_HW | KVM_GUESTDBG_USE_SW_BP; + state + } else { + kvm_bindings::kvm_guest_debug::default() + }; + self.apply_debug_state(state) + } + + fn set_single_step(&mut self, enable: bool) -> Result<(), DebugError> { + let mut state = self.debug_state; + if enable { + state.control |= kvm_bindings::KVM_GUESTDBG_SINGLESTEP; + } else { + state.control &= !kvm_bindings::KVM_GUESTDBG_SINGLESTEP; + } + self.apply_debug_state(state) + } + + fn add_hw_breakpoint(&mut self, addr: u64) -> Result<(), DebugError> { + if !addr.is_multiple_of(4) { + return Err(DebugError::Unsupported( + "unaligned ARM64 hardware breakpoint", + )); + } + let mut state = self.debug_state; + let slots = &state.arch.dbg_bcr[..HARDWARE_BREAKPOINTS]; + if slots + .iter() + .enumerate() + .any(|(i, control)| control & 1 != 0 && state.arch.dbg_bvr[i] == addr) + { + return Ok(()); + } + let slot = slots + .iter() + .position(|control| control & 1 == 0) + .ok_or(DebugError::TooManyHwBreakpoints(HARDWARE_BREAKPOINTS))?; + state.arch.dbg_bvr[slot] = addr; + state.arch.dbg_bcr[slot] = (15 << 5) | (3 << 1) | 1; + self.apply_debug_state(state) + } + + fn remove_hw_breakpoint(&mut self, addr: u64) -> Result<(), DebugError> { + let mut state = self.debug_state; + let slot = state.arch.dbg_bcr[..HARDWARE_BREAKPOINTS] + .iter() + .enumerate() + .position(|(i, control)| control & 1 != 0 && state.arch.dbg_bvr[i] == addr) + .ok_or(DebugError::HwBreakpointNotFound(addr))?; + state.arch.dbg_bcr[slot] = 0; + state.arch.dbg_bvr[slot] = 0; + self.apply_debug_state(state) + } +} + +#[cfg(all(test, gdb))] +mod debug_tests { + use super::*; + + #[test] + fn hardware_breakpoints_survive_reset_and_detach_clears_debug_state() { + let mut vm = KvmVm::new().unwrap(); + vm.set_debug(true).unwrap(); + vm.add_hw_breakpoint(0).unwrap(); + vm.add_hw_breakpoint(0).unwrap(); + vm.add_hw_breakpoint(0x1000).unwrap(); + assert!(matches!( + vm.add_hw_breakpoint(0x2000), + Err(DebugError::TooManyHwBreakpoints(2)) + )); + assert!(vm.add_hw_breakpoint(1).is_err()); + vm.remove_hw_breakpoint(0).unwrap(); + assert!(matches!( + vm.remove_hw_breakpoint(0), + Err(DebugError::HwBreakpointNotFound(0)) + )); + vm.add_hw_breakpoint(0x2000).unwrap(); + vm.set_single_step(true).unwrap(); + vm.reset_vcpu().unwrap(); + assert_ne!( + vm.debug_state.control & kvm_bindings::KVM_GUESTDBG_SINGLESTEP, + 0 + ); + assert_eq!(&vm.debug_state.arch.dbg_bvr[..2], &[0x2000, 0x1000]); + vm.set_debug(false).unwrap(); + assert_eq!(vm.debug_state.control, 0); + assert!( + vm.debug_state + .arch + .dbg_bcr + .iter() + .all(|&control| control == 0) + ); + } + + #[test] + fn register_writes_preserve_the_inactive_stack_pointer() { + use crate::hypervisor::regs::kvm_reg::{SP, SP_EL1}; + + let mut vm = KvmVm::new().unwrap(); + for (pstate, sp) in [(0x3c4, 0x1000), (0x3c5, 0x2000)] { + vm.set_regs(&CommonRegisters { + pstate, + sp, + ..Default::default() + }) + .unwrap(); + assert_eq!(vm.regs().unwrap().sp, sp); + } + assert_eq!(SP.get(RegisterError::GetRegs, &vm.vcpu_fd).unwrap(), 0x1000); + assert_eq!( + SP_EL1.get(RegisterError::GetRegs, &vm.vcpu_fd).unwrap(), + 0x2000 + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs index 9bd348647..9c84dc616 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/kvm/x86_64.rs @@ -697,7 +697,11 @@ impl VirtualMachine for KvmVm { #[cfg(gdb)] impl DebuggableVm for KvmVm { - fn translate_gva(&self, gva: u64) -> std::result::Result { + fn translate_gva( + &self, + gva: u64, + _memory: &crate::hypervisor::gdb::DebugMemoryView<'_>, + ) -> std::result::Result { let gpa = self .vcpu_fd .translate_gva(gva) diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mod.rs index e1922a1ec..5f0237029 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(), @@ -155,7 +159,10 @@ pub(crate) enum VmExit { Unknown(String), /// The operation should be retried, for example this can happen on Linux where a call to run the CPU can return EAGAIN #[cfg_attr( - any(target_os = "windows", feature = "hw-interrupts"), + all( + any(target_os = "windows", feature = "hw-interrupts"), + not(all(gdb, target_arch = "aarch64")) + ), expect( dead_code, reason = "Retry() is never constructed on Windows or with hw-interrupts (EAGAIN causes continue instead)" @@ -193,6 +200,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/mshv/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs index 1407fceaf..4ce9274b5 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/mshv/x86_64.rs @@ -792,7 +792,11 @@ impl VirtualMachine for MshvVm { #[cfg(gdb)] impl DebuggableVm for MshvVm { - fn translate_gva(&self, gva: u64) -> std::result::Result { + fn translate_gva( + &self, + gva: u64, + _memory: &crate::hypervisor::gdb::DebugMemoryView<'_>, + ) -> std::result::Result { use mshv_bindings::HV_TRANSLATE_GVA_VALIDATE_READ; // Do not use HV_TRANSLATE_GVA_VALIDATE_WRITE, since many diff --git a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs index bfff3ecd7..570d3d00b 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/aarch64.rs @@ -13,6 +13,8 @@ use std::sync::atomic::Ordering; use std::sync::{Condvar, Mutex}; use std::time::{Duration, Instant}; +#[cfg(gdb)] +use hyperlight_common::arch::exn::DEBUG_EXCEPTION_HVC as DEBUG_EXCEPTION_IMMEDIATE; use hyperlight_common::layout::{ WHP_GICD_BASE_GPA, WHP_GICR_BASE_GPA, WHP_GITS_TRANSLATOR_BASE_GPA, }; @@ -22,6 +24,8 @@ use windows::Win32::System::Threading::GetCurrentProcess; use windows_result::HRESULT; use super::release_file_mapping; +#[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 +40,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 +95,31 @@ 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)] +pub(crate) const DEBUG_CACHE_SYNC_GPA: u64 = 0x1000; +#[cfg(gdb)] +const DEBUG_CACHE_SYNC_IMMEDIATE: u16 = 0x4858; +#[cfg(gdb)] +const MDSCR_MDE: u64 = 1 << 15; +#[cfg(gdb)] +const MDSCR_KDE: u64 = 1 << 13; +#[cfg(gdb)] +const PSTATE_SS: u64 = 1 << 21; +#[cfg(gdb)] +const PSTATE_D: u64 = 1 << 9; +// Armv8 guarantees at least two instruction breakpoint registers. +#[cfg(gdb)] +const HARDWARE_BREAKPOINTS: usize = 2; +#[cfg(gdb)] +const DEBUG_CACHE_SYNC_CODE: [u8; 20] = [ + 0x9f, 0x3b, 0x03, 0xd5, // dsb ish + 0x1f, 0x75, 0x08, 0xd5, // ic iallu + 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 +179,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 +223,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 +240,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,8 +275,31 @@ 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::()]; + // ============================================================================ // Surrogate process guard (same as x86_64 version) // ============================================================================ @@ -330,6 +388,16 @@ 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)] + hardware_breakpoints: [Option; HARDWARE_BREAKPOINTS], + #[cfg(gdb)] + debug_exception_pending: bool, + #[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 +440,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 +485,16 @@ impl WhpVm { surrogate_process: None, file_mappings: Vec::new(), _no_surrogate_guard: no_surrogate_guard, + #[cfg(gdb)] + debug_enabled: false, + #[cfg(gdb)] + hardware_breakpoints: [None; HARDWARE_BREAKPOINTS], + #[cfg(gdb)] + debug_exception_pending: false, + #[cfg(gdb)] + _debug_cache_sync_host_memory: None, + #[cfg(gdb)] + _debug_cache_sync_guest_memory: None, }; if !no_surrogate { @@ -417,6 +506,9 @@ impl WhpVm { ); } + #[cfg(gdb)] + vm.initialize_debug_support()?; + Ok(vm) } @@ -455,6 +547,124 @@ impl WhpVm { } Ok(()) } + + #[cfg(gdb)] + fn write_hardware_breakpoint( + &self, + slot: usize, + address: Option, + ) -> Result<(), RegisterError> { + self.set_registers( + &[ + WHV_REGISTER_NAME(WHV_ARM64_REGISTER_DBGBVR0_EL1.0 + slot as i32), + WHV_REGISTER_NAME(WHV_ARM64_REGISTER_DBGBCR0_EL1.0 + slot as i32), + ], + &[ + Align16(WHV_REGISTER_VALUE { + Reg64: address.unwrap_or(0), + }), + Align16(WHV_REGISTER_VALUE { + Reg64: if address.is_some() { + (15 << 5) | (1 << 1) | 1 + } else { + 0 + }, + }), + ], + ) + .map_err(RegisterError::SetRegs) + } + + #[cfg(gdb)] + fn initialize_debug_support(&mut self) -> Result<(), CreateVmError> { + let mut memory = ExclusiveSharedMemory::new(page_size::get()) + .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 mut region = guest_memory.mapping_at(DEBUG_CACHE_SYNC_GPA, MemoryRegionType::Scratch); + region.flags.remove(MemoryRegionFlags::WRITE); + + // SAFETY: Both shared-memory handles are retained for the VM lifetime. + unsafe { self.map_memory((u32::MAX, ®ion)) } + .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 names = [ + WHV_ARM64_REGISTER_PC, + WHV_ARM64_REGISTER_PSTATE, + WHV_ARM64_REGISTER_SCTLR_EL1, + WHV_ARM64_REGISTER_MDSCR_EL1, + ]; + let original_values = self + .get_registers(&names) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; + + let sync_result = (|| { + // SAFETY: These values were read as 64-bit system registers. + let mdscr = unsafe { original_values[3].0.Reg64 }; + self.set_registers( + &names, + &[ + Align16(WHV_REGISTER_VALUE { + Reg64: DEBUG_CACHE_SYNC_GPA, + }), + Align16(WHV_REGISTER_VALUE { Reg64: 0x3c5 }), + Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + }), + Align16(WHV_REGISTER_VALUE { Reg64: mdscr & !1 }), + ], + ) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string()))?; + + 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 = self + .set_registers(&names, &original_values) + .map_err(|e| DebugError::InstructionCacheSync(e.to_string())); + + sync_result?; + restore + } } impl VirtualMachine for WhpVm { @@ -569,6 +779,10 @@ impl VirtualMachine for WhpVm { ) -> Result { use arm64_exit_reasons::*; let mut exit_context = Arm64ExitContext::default(); + #[cfg(gdb)] + { + self.debug_exception_pending = false; + } unsafe { WHvRunVirtualProcessor( @@ -639,6 +853,40 @@ impl VirtualMachine for WhpVm { } } WHV_EXIT_REASON_CANCELLED => Ok(VmExit::Cancelled()), + #[cfg(gdb)] + WHV_EXIT_REASON_HYPERCALL => { + let context = exit_context.hypercall_context(); + if self.debug_enabled && context.immediate == DEBUG_EXCEPTION_IMMEDIATE { + let values = self + .get_registers(&[WHV_ARM64_REGISTER_ESR_EL1]) + .map_err(RunVcpuError::Unknown)?; + // SAFETY: ESR_EL1 is a 64-bit system register. + let ec = unsafe { values[0].0.Reg64 } >> 26; + let reason = match ec { + 0x30 | 0x31 => Some(VcpuStopReason::HwBp), + 0x32 | 0x33 => Some(VcpuStopReason::DoneStep), + 0x3c => Some(VcpuStopReason::SwBp), + _ => None, + }; + // Debug events return through ERET. Other exceptions enter + // the ordinary prologue after the scratch-register restore. + let next_pc = context.header.pc + if reason.is_some() { 4 } else { 12 }; + self.set_registers( + &[WHV_ARM64_REGISTER_PC], + &[Align16(WHV_REGISTER_VALUE { Reg64: next_pc })], + ) + .map_err(RunVcpuError::IncrementRip)?; + if let Some(reason) = reason { + self.debug_exception_pending = true; + return Ok(VmExit::Debug { reason }); + } + return Ok(VmExit::Retry()); + } + Ok(VmExit::Unknown(format!( + "Unsupported ARM64 hypercall immediate {:#x} at PC={:#x}", + context.immediate, context.header.pc + ))) + } WHV_EXIT_REASON_ARM64_RESET => Ok(VmExit::Halt()), WHV_EXIT_REASON_UNRECOVERABLE => { let header = exit_context.intercept_header(); @@ -658,15 +906,21 @@ 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; + #[cfg(gdb)] + if self.debug_exception_pending { + names[31] = WHV_ARM64_REGISTER_ELR_EL1; + names[34] = WHV_ARM64_REGISTER_SPSR_EL1; + } let values = self.get_registers(&names).map_err(RegisterError::GetRegs)?; @@ -675,11 +929,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,10 +957,19 @@ 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 }); + #[cfg(gdb)] + if self.debug_exception_pending { + names[31] = WHV_ARM64_REGISTER_ELR_EL1; + names[33] = WHV_ARM64_REGISTER_SPSR_EL1; + } self.set_registers(&names, &values) .map_err(RegisterError::SetRegs) @@ -851,6 +1121,22 @@ impl VirtualMachine for WhpVm { ) .map_err(|e| ResetVcpuError::Hypervisor(e.into()))?; } + #[cfg(gdb)] + { + self.debug_exception_pending = false; + if self.debug_enabled { + self.set_registers( + &[WHV_ARM64_REGISTER_MDSCR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: MDSCR_MDE | MDSCR_KDE, + })], + ) + .map_err(RegisterError::SetRegs)?; + for (slot, address) in self.hardware_breakpoints.iter().enumerate() { + self.write_hardware_breakpoint(slot, *address)?; + } + } + } Ok(()) } @@ -877,6 +1163,486 @@ impl Drop for WhpVm { } } +#[cfg(gdb)] +impl DebuggableVm for WhpVm { + fn translate_gva( + &self, + gva: u64, + _memory: &crate::hypervisor::gdb::DebugMemoryView<'_>, + ) -> std::result::Result { + let mut gpa = 0; + let mut result = WHV_TRANSLATE_GVA_RESULT::default(); + + 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> { + if !enable { + self.set_single_step(false)?; + for slot in 0..HARDWARE_BREAKPOINTS { + self.write_hardware_breakpoint(slot, None)?; + } + self.hardware_breakpoints.fill(None); + } + let values = self + .get_registers(&[WHV_ARM64_REGISTER_MDSCR_EL1]) + .map_err(RegisterError::GetRegs)?; + // SAFETY: MDSCR_EL1 is a 64-bit system register. + let value = unsafe { values[0].0.Reg64 }; + let value = if enable { + value | MDSCR_MDE | MDSCR_KDE + } else { + value & !(MDSCR_MDE | MDSCR_KDE | 1) + }; + self.set_registers( + &[WHV_ARM64_REGISTER_MDSCR_EL1], + &[Align16(WHV_REGISTER_VALUE { Reg64: value })], + ) + .map_err(RegisterError::SetRegs)?; + self.debug_enabled = enable; + Ok(()) + } + + fn set_single_step(&mut self, enable: bool) -> std::result::Result<(), DebugError> { + let mut regs = self.regs()?; + regs.pstate = if enable { + (regs.pstate | PSTATE_SS) & !PSTATE_D + } else { + regs.pstate & !PSTATE_SS + }; + self.set_regs(®s)?; + let values = self + .get_registers(&[WHV_ARM64_REGISTER_MDSCR_EL1]) + .map_err(RegisterError::GetRegs)?; + // SAFETY: MDSCR_EL1 is a 64-bit system register. + let value = unsafe { values[0].0.Reg64 }; + self.set_registers( + &[WHV_ARM64_REGISTER_MDSCR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: if enable { value | 1 } else { value & !1 }, + })], + ) + .map_err(RegisterError::SetRegs)?; + Ok(()) + } + + fn add_hw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> { + if self.hardware_breakpoints.contains(&Some(addr)) { + return Ok(()); + } + if !addr.is_multiple_of(4) { + return Err(DebugError::Unsupported( + "unaligned ARM64 hardware breakpoint", + )); + } + let slot = self + .hardware_breakpoints + .iter() + .position(Option::is_none) + .ok_or(DebugError::TooManyHwBreakpoints(HARDWARE_BREAKPOINTS))?; + self.write_hardware_breakpoint(slot, Some(addr))?; + self.hardware_breakpoints[slot] = Some(addr); + Ok(()) + } + + fn remove_hw_breakpoint(&mut self, addr: u64) -> std::result::Result<(), DebugError> { + let slot = self + .hardware_breakpoints + .iter() + .position(|&value| value == Some(addr)) + .ok_or(DebugError::HwBreakpointNotFound(addr))?; + self.write_hardware_breakpoint(slot, None)?; + self.hardware_breakpoints[slot] = None; + Ok(()) + } + + fn sync_instruction_cache(&mut self) -> std::result::Result<(), DebugError> { + self.run_debug_cache_sync() + } +} + +#[cfg(all(test, gdb))] +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 parses_arm64_hypercall_context_layout() { + let expected = context(DEBUG_EXCEPTION_IMMEDIATE, 0x1234); + let mut exit = Arm64ExitContext::default(); + // SAFETY: `payload` is larger than `Arm64HypercallContext`. The write is + // unaligned and does not outlive the payload. + 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 guest_cannot_write_cache_sync_trampoline() { + let mut vm = WhpVm::new().unwrap(); + let memory = vm._debug_cache_sync_host_memory.as_ref().unwrap(); + let mut original = [0; 4]; + memory.copy_to_slice(&mut original, 0).unwrap(); + let mut code = Vec::new(); + for instruction in [ + 0xb900_0001u32, // str w1, [x0] + 0xd400_0002 | (0x485b << 5), // hvc #0x485b + ] { + code.extend_from_slice(&instruction.to_le_bytes()); + } + memory.copy_from_slice(&code, 0x20).unwrap(); + vm.sync_instruction_cache().unwrap(); + vm.set_registers( + &[WHV_ARM64_REGISTER_SCTLR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + })], + ) + .unwrap(); + let mut regs = CommonRegisters { + pc: DEBUG_CACHE_SYNC_GPA + 0x20, + pstate: 0x3c5, + ..Default::default() + }; + regs.x[0] = DEBUG_CACHE_SYNC_GPA; + regs.x[1] = u64::from(!u32::from_le_bytes(original)); + vm.set_regs(®s).unwrap(); + let exit = vm.run_vcpu().unwrap(); + assert!( + matches!(&exit, VmExit::Unknown(message) if message.starts_with("Unsupported ARM64 hypercall immediate 0x485b ")) + || matches!(exit, VmExit::MmioWrite(DEBUG_CACHE_SYNC_GPA)) + ); + let mut actual = [0; 4]; + vm._debug_cache_sync_host_memory + .as_ref() + .unwrap() + .copy_to_slice(&mut actual, 0) + .unwrap(); + assert_eq!( + actual, original, + "guest execution overwrote the cache-sync trampoline" + ); + } + + #[test] + #[serial] + fn asynchronous_vectors_ignore_stale_debug_syndromes() { + if !is_hypervisor_present() { + return; + } + + let bytes = std::fs::read(hyperlight_testing::simple_guest_as_pathbuf()).unwrap(); + let elf = goblin::elf::Elf::parse(&bytes).unwrap(); + let address = elf + .syms + .iter() + .find(|symbol| elf.strtab.get_at(symbol.st_name) == Some("hyperlight_debug_vectors")) + .unwrap() + .st_value; + let segment = elf + .program_headers + .iter() + .find(|segment| { + segment.p_type == goblin::elf::program_header::PT_LOAD + && address >= segment.p_vaddr + && address + 0x800 <= segment.p_vaddr + segment.p_filesz + }) + .unwrap(); + let offset = (segment.p_offset + address - segment.p_vaddr) as usize; + let mut vectors = bytes[offset..offset + 0x800].to_vec(); + let mut handler_stops = Vec::new(); + for (index, vector) in vectors.as_chunks_mut::<0x80>().0.iter_mut().enumerate() { + // Stop at the ordinary handler's context-save call. + let call = vector + .as_chunks::<4>() + .0 + .iter() + .position(|instruction| { + u32::from_le_bytes(*instruction) & 0xfc00_0000 == 0x9400_0000 + }) + .unwrap() + * 4; + vector[call..call + 4].copy_from_slice(&(0xd400_0002u32 | (0x485b << 5)).to_le_bytes()); + handler_stops.push(DEBUG_CACHE_SYNC_GPA + 0x800 + (index * 0x80 + call) as u64); + } + + let (_stack_host_memory, stack_guest_memory) = ExclusiveSharedMemory::new(page_size::get()) + .unwrap() + .build(); + let mut stack_region = stack_guest_memory.mapping_at( + DEBUG_CACHE_SYNC_GPA - page_size::get() as u64, + MemoryRegionType::Scratch, + ); + stack_region.flags.remove(MemoryRegionFlags::EXECUTE); + let mut vm = WhpVm::new().unwrap(); + // SAFETY: Both stack handles outlive the VM. + unsafe { vm.map_memory((u32::MAX - 1, &stack_region)) }.unwrap(); + vm._debug_cache_sync_host_memory + .as_ref() + .unwrap() + .copy_from_slice(&vectors, 0x800) + .unwrap(); + vm.sync_instruction_cache().unwrap(); + vm.set_registers( + &[WHV_ARM64_REGISTER_SCTLR_EL1], + &[Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + })], + ) + .unwrap(); + vm.set_debug(true).unwrap(); + + for syndrome in [0x31u64 << 26, 0x33u64 << 26, 0x3cu64 << 26] { + for (index, &handler_pc) in handler_stops.iter().enumerate() { + if index % 4 == 0 { + continue; + } + let mut regs = CommonRegisters { + pc: DEBUG_CACHE_SYNC_GPA + 0x800 + (index * 0x80) as u64, + sp: DEBUG_CACHE_SYNC_GPA, + pstate: 0x3c5, + ..Default::default() + }; + regs.x[0] = 0x1234; + regs.x[1] = 0x5678; + vm.set_regs(®s).unwrap(); + vm.set_registers( + &[WHV_ARM64_REGISTER_ESR_EL1], + &[Align16(WHV_REGISTER_VALUE { Reg64: syndrome })], + ) + .unwrap(); + match vm.run_vcpu().unwrap() { + VmExit::Unknown(message) => assert!( + message.starts_with("Unsupported ARM64 hypercall immediate 0x485b "), + "{message}" + ), + _ => panic!("vector {index} forwarded stale syndrome {syndrome:#x}"), + } + let stopped = vm.regs().unwrap(); + assert_eq!(stopped.pc, handler_pc); + assert_eq!(stopped.x, regs.x); + } + } + } + + #[test] + #[serial] + fn hardware_debug_preserves_context_and_steps_control_flow() { + if !is_hypervisor_present() { + return; + } + + let mut vm = WhpVm::new().unwrap(); + let mut code = vec![0u8; page_size::get()]; + for (offset, instruction) in [ + (0x20, 0xd503_201fu32), + (0x24, 0x1400_0000), // b . + (0x28, 0xd65f_03c0), // ret + (0x800, 0xd409_0b42), // hvc #0x485a + (0x804, 0xd69f_03e0), // eret + ] { + code[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes()); + } + vm._debug_cache_sync_host_memory + .as_ref() + .unwrap() + .copy_from_slice(&code[0x20..], 0x20) + .unwrap(); + vm.sync_instruction_cache().unwrap(); + vm.set_registers( + &[ + WHV_ARM64_REGISTER_SCTLR_EL1, + WHV_ARM64_REGISTER_VBAR_EL1, + WHV_ARM64_REGISTER_SP_EL1, + ], + &[ + Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + }), + Align16(WHV_REGISTER_VALUE { + Reg64: DEBUG_CACHE_SYNC_GPA + 0x800, + }), + Align16(WHV_REGISTER_VALUE { Reg64: 0x9870 }), + ], + ) + .unwrap(); + let mut regs = CommonRegisters { + pc: DEBUG_CACHE_SYNC_GPA + 0x20, + sp: 0x4560, + pstate: 0xc4, + ..Default::default() + }; + regs.x[9] = 0xfeed; + regs.x[30] = DEBUG_CACHE_SYNC_GPA + 0x20; + vm.set_regs(®s).unwrap(); + vm.set_debug(true).unwrap(); + vm.add_hw_breakpoint(regs.pc).unwrap(); + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::HwBp + } + )); + let stopped = vm.regs().unwrap(); + assert_eq!( + (stopped.pc, stopped.sp, stopped.pstate, stopped.x), + (regs.pc, regs.sp, regs.pstate, regs.x) + ); + + let special = vm.sregs().unwrap(); + vm.reset_vcpu().unwrap(); + vm.set_sregs(&special).unwrap(); + vm.set_regs(®s).unwrap(); + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::HwBp + } + )); + vm.sync_instruction_cache().unwrap(); + assert_eq!(vm.regs().unwrap().sp, regs.sp); + let values = vm.get_registers(&[WHV_ARM64_REGISTER_SP_EL1]).unwrap(); + // SAFETY: SP_EL1 is a 64-bit stack-pointer register. + assert_eq!(unsafe { values[0].0.Reg64 }, 0x9870); + vm.remove_hw_breakpoint(regs.pc).unwrap(); + regs.pc += 4; + regs.x[9] = 0xbeef; + vm.set_regs(®s).unwrap(); + vm.set_single_step(true).unwrap(); + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::DoneStep + } + )); + assert_eq!(vm.regs().unwrap().pc, regs.pc); + assert_eq!(vm.regs().unwrap().x[9], 0xbeef); + + regs.pc += 4; + vm.set_regs(®s).unwrap(); + vm.set_single_step(true).unwrap(); + assert!(matches!( + vm.run_vcpu().unwrap(), + VmExit::Debug { + reason: VcpuStopReason::DoneStep + } + )); + assert_eq!(vm.regs().unwrap().pc, regs.x[30]); + vm.set_debug(false).unwrap(); + } + + #[test] + #[serial] + fn patched_software_breakpoint_is_runnable_without_pc_advance() { + 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(); + host_memory + .copy_from_slice( + &(0xd400_0002 | (u32::from(DEBUG_EXCEPTION_IMMEDIATE) << 5)).to_le_bytes(), + 0x800, + ) + .unwrap(); + host_memory + .copy_from_slice(&0xd69f_03e0u32.to_le_bytes(), 0x804) + .unwrap(); + // SAFETY: The current process handle is valid and the range is inside + // the live shared-memory mapping. + unsafe { + FlushInstructionCache( + GetCurrentProcess(), + Some(host_memory.base_ptr().cast::()), + page_size::get(), + ) + .unwrap(); + } + vm.sync_instruction_cache().unwrap(); + + let breakpoint_address = DEBUG_CACHE_SYNC_GPA + BREAKPOINT_OFFSET as u64; + vm.set_debug(true).unwrap(); + vm.set_registers( + &[WHV_ARM64_REGISTER_SCTLR_EL1, WHV_ARM64_REGISTER_VBAR_EL1], + &[ + Align16(WHV_REGISTER_VALUE { + Reg64: crate::hypervisor::regs::SCTLR_EL1_RES1, + }), + Align16(WHV_REGISTER_VALUE { + Reg64: DEBUG_CACHE_SYNC_GPA + 0x800, + }), + ], + ) + .unwrap(); + vm.set_regs(&CommonRegisters { + 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/hypervisor/virtual_machine/whp/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs index 57342a532..2d9636580 100644 --- a/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs +++ b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs @@ -1101,7 +1101,11 @@ impl VirtualMachine for WhpVm { #[cfg(gdb)] impl DebuggableVm for WhpVm { - fn translate_gva(&self, gva: u64) -> std::result::Result { + fn translate_gva( + &self, + gva: u64, + _memory: &crate::hypervisor::gdb::DebugMemoryView<'_>, + ) -> std::result::Result { let mut gpa = 0; let mut result = WHV_TRANSLATE_GVA_RESULT::default(); diff --git a/src/hyperlight_host/src/mem/elf.rs b/src/hyperlight_host/src/mem/elf.rs index f8981883b..11cef5ddd 100644 --- a/src/hyperlight_host/src/mem/elf.rs +++ b/src/hyperlight_host/src/mem/elf.rs @@ -88,6 +88,8 @@ pub(crate) struct ElfInfo { /// The hyperlight version string embedded by `hyperlight-guest-bin`, if /// present. Used to detect version/ABI mismatches between guest and host. guest_bin_version: Option, + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors: Option, } #[cfg(feature = "mem_profile")] @@ -248,6 +250,11 @@ impl ElfInfo { // Look for the hyperlight version note embedded by // hyperlight-guest-bin. let guest_bin_version = Self::read_version_note(&elf, &bytes); + #[cfg(all(gdb, target_arch = "aarch64"))] + let debug_vectors = elf.syms.iter().find_map(|symbol| { + (elf.strtab.get_at(symbol.st_name) == Some("hyperlight_debug_vectors")) + .then_some(symbol.st_value) + }); let phdrs = std::mem::take(&mut elf.program_headers); let entry = elf.entry; @@ -279,6 +286,8 @@ impl ElfInfo { relocs, is_pie, guest_bin_version, + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors, }) } @@ -410,11 +419,23 @@ impl ElfInfo { } } } + #[cfg(all(gdb, target_arch = "aarch64"))] + let debug_vectors = self + .debug_vectors + .map(|address| { + address + .checked_sub(base_va) + .and_then(|offset| load_gva.checked_add(offset)) + .ok_or_else(|| new_error!("ARM64 debug vector address overflows")) + }) + .transpose()?; cfg_if::cfg_if! { if #[cfg(feature = "mem_profile")] { let va_size = self.get_va_size() as u64; let base_svma = self.get_base_va(); Ok(LoadInfo { + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors, info: Arc::new(UnwindInfo { payload: self.payload, load_addr: load_gva, @@ -424,7 +445,10 @@ impl ElfInfo { }) }) } else { - Ok(LoadInfo {}) + Ok(LoadInfo { + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors, + }) } } } diff --git a/src/hyperlight_host/src/mem/exe.rs b/src/hyperlight_host/src/mem/exe.rs index aedb85b1a..7a8746b72 100644 --- a/src/hyperlight_host/src/mem/exe.rs +++ b/src/hyperlight_host/src/mem/exe.rs @@ -48,6 +48,8 @@ impl framehop::ModuleSectionInfo for &DummyUnwindInfo { #[derive(Clone)] pub(crate) struct LoadInfo { + #[cfg(all(gdb, target_arch = "aarch64"))] + pub(crate) debug_vectors: Option, #[cfg(feature = "mem_profile")] pub(crate) info: Arc, } @@ -55,6 +57,8 @@ pub(crate) struct LoadInfo { impl LoadInfo { pub(crate) fn dummy() -> Self { LoadInfo { + #[cfg(all(gdb, target_arch = "aarch64"))] + debug_vectors: None, #[cfg(feature = "mem_profile")] info: Arc::new(DummyUnwindInfo {}), } diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index b1da7c790..14759760d 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -232,8 +232,8 @@ impl<'a> GuestPhysicalMemoryView<'a> { Ok(()) } - #[cfg(crashdump)] - fn host_range(&self, gpa: u64, len: usize) -> Result> { + #[cfg(any(crashdump, all(gdb, target_arch = "aarch64")))] + pub(crate) fn host_range(&self, gpa: u64, len: usize) -> Result> { let backing = self .resolve(gpa, len) .ok_or_else(|| new_error!("GPA range is not backed: {gpa:#x}"))?; diff --git a/src/hyperlight_host/src/sandbox/initialized.rs b/src/hyperlight_host/src/sandbox/initialized.rs index dcc3fa0b5..141c990a2 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -466,6 +466,13 @@ impl Sandbox { #[instrument(err(Debug), skip_all, parent = Span::current())] pub fn snapshot_with_options(&mut self, options: &SnapshotOptions) -> Result> { self.check_ready()?; + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.vm.has_sw_breakpoints() { + return Err(crate::new_error!( + "Cannot snapshot an ARM64 sandbox while software breakpoints are installed" + )); + } + // Unpacked in full so that a new option has to decide whether the // cached plain snapshot still answers it. let SnapshotOptions { scratch_size } = options; @@ -489,6 +496,12 @@ impl Sandbox { fn capture_snapshot(&mut self, scratch_size: usize) -> Result> { if self.transport_dirty { self.checkpoint_transport_for_snapshot()?; + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.vm.has_sw_breakpoints() { + return Err(crate::new_error!( + "Cannot snapshot an ARM64 sandbox while software breakpoints are installed" + )); + } } let mapped_regions_iter = self.vm.get_mapped_regions(); @@ -709,6 +722,13 @@ impl Sandbox { return Err(HyperlightError::UnrecoverableSandbox); } + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.status.is_ready() && self.vm.has_sw_breakpoints() { + return Err(crate::new_error!( + "Cannot restore an ARM64 sandbox while software breakpoints are installed" + )); + } + // 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 @@ -757,6 +777,10 @@ impl Sandbox { } // Errors below leave the sandbox poisoned unless base mapping updates make it unrecoverable. + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.status.is_poisoned() { + self.vm.discard_software_breakpoints(); + } self.status = SandboxStatus::Poisoned; self.snapshot = None; @@ -1406,6 +1430,216 @@ mod tests { SnapshotOptions::default().scratch_size(scratch_size) } + #[cfg(all(gdb, target_arch = "aarch64", target_os = "windows"))] + mod gdb_recovery_tests { + use super::*; + use crate::hypervisor::gdb::{ + DebugCommChannel, DebugMemoryView, DebugMsg, DebugResponse, VcpuStopReason, + }; + use crate::hypervisor::hyperlight_vm::HyperlightVm; + use crate::mem::ptr::RawPtr; + use crate::sandbox::snapshot::NextAction; + + fn sandbox_with_debugger() -> (Sandbox, DebugCommChannel, u64) { + let guest = + UninitializedSandbox::new(GuestBinary::FilePath(simple_guest_as_pathbuf()), None) + .unwrap(); + let vectors = guest.load_info.debug_vectors; + let (mut host, memory) = guest.mgr.build().unwrap(); + let (debugger, connection) = DebugCommChannel::unbounded(); + debugger.send(DebugMsg::Continue).unwrap(); + let mut vm = HyperlightVm::new( + memory.shared_mem, + memory.scratch_mem, + memory.layout.get_pt_base_gpa(), + memory.next_action, + guest.stack_top_gva, + page_size::get(), + &guest.config, + Some(connection), + #[cfg(feature = "mem_profile")] + crate::sandbox::trace::MemTraceInfo::new(guest.load_info.info).unwrap(), + ) + .unwrap(); + vm.initialize_debug_vectors(vectors).unwrap(); + vm.initialise( + RawPtr::from(host.layout.peb_address() as u64), + 0, + &mut host, + &guest.host_funcs, + None, + ) + .unwrap(); + host.finish_snapshot_checkpoint().unwrap(); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::InterruptHandle(_) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::VcpuStopped(VcpuStopReason::Initial) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::Continue + )); + + let bytes = std::fs::read(simple_guest_as_pathbuf()).unwrap(); + let elf = goblin::elf::Elf::parse(&bytes).unwrap(); + let entry = elf + .syms + .iter() + .find(|symbol| elf.strtab.get_at(symbol.st_name) == Some("hyperlight_main")) + .unwrap() + .st_value + + host.layout.get_guest_code_gva() as u64; + ( + Sandbox::from_uninit(guest.host_funcs, host, vm), + debugger, + entry, + ) + } + + fn request(sandbox: &mut Sandbox, message: DebugMsg) -> DebugResponse { + let memory = DebugMemoryView::new( + &sandbox.mem_mgr, + sandbox.vm.get_mapped_regions().cloned().collect(), + ); + sandbox.vm.process_dbg_request(message, &memory).unwrap() + } + + #[test] + fn snapshot_rejects_breakpoints_added_during_checkpoint_without_poisoning() { + let (mut sandbox, debugger, breakpoint) = sandbox_with_debugger(); + let snapshot = sandbox.snapshot().unwrap(); + sandbox.call::<()>("NoOp", ()).unwrap(); + assert!(sandbox.transport_dirty); + let NextAction::Call(dispatch) = sandbox.vm.get_next_action() else { + panic!("guest dispatch is not initialized"); + }; + assert!(matches!( + request(&mut sandbox, DebugMsg::AddHwBreakpoint(dispatch)), + DebugResponse::AddHwBreakpoint(true) + )); + debugger + .send(DebugMsg::RemoveHwBreakpoint(dispatch)) + .unwrap(); + debugger + .send(DebugMsg::AddSwBreakpoint(breakpoint)) + .unwrap(); + debugger.send(DebugMsg::Continue).unwrap(); + + let result = sandbox.snapshot(); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::VcpuStopped(VcpuStopReason::HwBp) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::RemoveHwBreakpoint(true) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::AddSwBreakpoint(true) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::Continue + )); + let error = result + .err() + .expect("snapshot accepted a software breakpoint"); + assert_eq!(sandbox.status(), SandboxStatus::Ready, "{error}"); + assert!(matches!( + request(&mut sandbox, DebugMsg::RemoveSwBreakpoint(breakpoint)), + DebugResponse::RemoveSwBreakpoint(true) + )); + sandbox.restore(snapshot).unwrap(); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + + #[test] + fn cancelled_debug_session_can_restore_with_software_breakpoints() { + let (mut sandbox, _debugger, breakpoint) = sandbox_with_debugger(); + let interrupt = sandbox.interrupt_handle(); + sandbox + .register_host_function("HostNoOp", move || { + interrupt.kill(); + Ok(()) + }) + .unwrap(); + let snapshot = sandbox.snapshot().unwrap(); + assert!(matches!( + request(&mut sandbox, DebugMsg::AddSwBreakpoint(breakpoint)), + DebugResponse::AddSwBreakpoint(true) + )); + assert!(sandbox.restore(snapshot.clone()).is_err()); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + + let error = sandbox.call::<()>("RoundTripHostNoOp", ()).unwrap_err(); + assert!(matches!(error, HyperlightError::ExecutionCanceledByHost())); + assert_eq!(sandbox.status(), SandboxStatus::Poisoned); + sandbox.restore(snapshot).unwrap(); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + + #[test] + fn crash_detach_can_restore_after_breakpoint_mapping_is_lost() { + let (mut sandbox, debugger, _) = sandbox_with_debugger(); + let snapshot = sandbox.snapshot().unwrap(); + // The guard-page backing is outside the guest allocator. + let address = hyperlight_common::layout::scratch_transport_end_gpa(); + sandbox + .call::>("ReadMappedBuffer", (address, 4u64, true)) + .unwrap(); + assert!(matches!( + request(&mut sandbox, DebugMsg::AddSwBreakpoint(address)), + DebugResponse::AddSwBreakpoint(true) + )); + + let root = sandbox.vm.get_root_pt().unwrap(); + let registers = sandbox.vm.get_snapshot_sregs().unwrap(); + let memory = DebugMemoryView::new(&sandbox.mem_mgr, Vec::new()); + let mut table = root; + for shift in [39, 30, 21, 12] { + let entry_address = table + ((address >> shift) & 0x1ff) * 8; + let mut bytes = [0; 8]; + memory.read(&mut bytes, entry_address).unwrap(); + let entry = u64::from_le_bytes(bytes); + assert_ne!(entry & 1, 0); + if shift == 12 || entry & 2 == 0 { + memory.write(&[0; 8], entry_address).unwrap(); + break; + } + table = entry & 0x0000_ffff_ffff_f000; + } + sandbox.vm.apply_sregs(root, ®isters).unwrap(); + debugger.send(DebugMsg::DisableDebug).unwrap(); + + // The guest maps this address, but the hypervisor has no backing for it. + let error = sandbox + .call::>("ReadMappedBuffer", (0x1_0000_0000u64, 1u64, true)) + .unwrap_err(); + let stop = debugger.try_recv().unwrap_or_else(|receive_error| { + panic!("Missing crash stop: {receive_error:?}, guest error: {error:?}") + }); + assert!(matches!( + stop, + DebugResponse::VcpuStopped(VcpuStopReason::Crash) + )); + let response = debugger.try_recv().unwrap(); + assert!( + matches!(response, DebugResponse::DisableDebug), + "{response:?}" + ); + assert_eq!(sandbox.status(), SandboxStatus::Poisoned, "{error:?}"); + sandbox.restore(snapshot).unwrap(); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + } + fn assert_virtq_attached(sbox: &Sandbox) { assert!(sbox.mem_mgr.g2h_consumer.is_some()); assert!(sbox.mem_mgr.h2g_consumer.is_some()); @@ -4643,6 +4877,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()) diff --git a/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs b/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs index 68be2c1bc..757d15740 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs @@ -192,7 +192,7 @@ impl SnapshotMemoryBacking { self.memory.page_table_bytes() } - #[cfg(crashdump)] + #[cfg(any(crashdump, all(gdb, target_arch = "aarch64")))] pub(crate) fn host_range(&self, layer_index: usize) -> Result<(usize, usize)> { #[cfg(not(unshared_snapshot_mem))] let backing = self diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index 8668b8f83..96adc8482 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -128,7 +128,7 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg(feature = "mem_profile")] let trace_info = MemTraceInfo::new(_load_info.info)?; - Ok(HyperlightVm::new( + let vm = HyperlightVm::new( mgr.shared_mem, mgr.scratch_mem, mgr.layout.get_pt_base_gpa(), @@ -141,7 +141,15 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg(feature = "mem_profile")] trace_info, ) - .map_err(HyperlightVmError::Create)?) + .map_err(HyperlightVmError::Create)?; + #[cfg(all(gdb, target_arch = "aarch64"))] + let vm = { + let mut vm = vm; + vm.initialize_debug_vectors(_load_info.debug_vectors) + .map_err(HyperlightVmError::Create)?; + vm + }; + Ok(vm) } #[cfg(test)]