diff --git a/.github/workflows/dep_build_test.yml b/.github/workflows/dep_build_test.yml index a745a9c78..743eca092 100644 --- a/.github/workflows/dep_build_test.yml +++ b/.github/workflows/dep_build_test.yml @@ -158,6 +158,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 95fecc54f..bf6fdc2a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added * Add `Sandbox::recreate()`, which rebuilds a sandbox's VM and memory from a snapshot. It recovers unrecoverable sandboxes and keeps configuration, host functions, and interrupt handles by @ludfjig in https://github.com/hyperlight-dev/hyperlight/pull/1751 +* ARM64 Linux/KVM guest debugging supports software and hardware breakpoints, register and memory access, and hardware single-step. ### Fixed * Fix restore on WHP not rewinding the guest TSC on hosts with `TSC_ADJUST` support. +* 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. ## [v0.19.0] - 2026-11-04 diff --git a/Justfile b/Justfile index 528fc4df2..ae42a05de 100644 --- a/Justfile +++ b/Justfile @@ -335,7 +335,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:: # rust test for crashdump test-rust-crashdump target=default-target features="": diff --git a/docs/gdb-commit-layers.md b/docs/gdb-commit-layers.md new file mode 100644 index 000000000..56174181d --- /dev/null +++ b/docs/gdb-commit-layers.md @@ -0,0 +1,54 @@ +# GDB commit layers + +Each layer is one signed commit. Backend layers depend on shared GDB support, +not on another backend's debugger. + +```text +main +|-- A: shared GDB support +| |-- B: WHP ARM64 GDB +| `-- C: KVM ARM64 GDB +`-- D: ARM64 MSHV backend and CI + `-- E: cherry-pick of A + `-- F: MSHV ARM64 GDB +``` + +| Layer | Branch | Parent | Contents | +|---|---|---|---| +| A | `cshung/gdb-shared` | `main` | Architecture adapters, protocol handling, software breakpoints, page-sized memory access, guest debug vectors, snapshot recovery, common tests | +| B | `cshung/whp-aarch64-gdb` | A | WHP register access, HVC exits, debug registers, cache synchronization, platform enablement and tests | +| C | `cshung/kvm-aarch64-gdb` | A | KVM debug exits, debug registers, platform enablement and CI | +| D | `cshung/mshv-aarch64` | `main` | MSHV VM implementation, register access, reset, runner configuration and bounded CI scheduling | +| E | `cshung/mshv-aarch64-gdb-shared` | D | A's shared patch | +| F | `cshung/mshv-aarch64-gdb` | E | MSHV HVC exits, exception context, debug registers, platform enablement and tests | + +A retains the existing x86_64 GDB platform guard. B, C and F enable their ARM64 +backend. D and E provide an MSHV backend without ARM64 GDB enablement. +F rejects KVM debugging until C is integrated. + +## Updating layers + +* Update A for shared behavior. Rebuild B and C on A, cherry-pick A onto D as E, + then rebuild F on E. Validate all three backend tips before publication. +* Update B or C for a backend-only debugger change. The sibling branch is independent. +* Update D for MSHV backend or CI changes. Rebuild E and F on D. +* Update F for an MSHV-only debugger change. +* Keep A and E patch-equivalent. E's parent and commit ID differ from A's. +* Squash each layer to one commit. Keep historical tips in local archive branches + before rewriting published refs. Push with an explicit lease on each old remote tip. + +Run formatting and Clippy on each affected tip. Rebuild guests for guest changes. +Run the common debugger tests, backend tests and real GDB client tests on WHP, +KVM and MSHV for shared updates. Validate both the default Linux driver selection +and the backend-only feature configuration. A history-only rewrite can reuse +runtime results when the corresponding source files are identical. + +## Pull requests + +A has a shared-support PR. B uses #1907, C uses #2016, and D uses #1626. +F has an MSHV GDB PR. E is a cherry-pick, not a second shared-support PR. + +The branches live in `cshung/hyperlight`. Native GitHub stacks require head +branches in the upstream repository. These fork PRs target upstream `main` and +link their prerequisites and backend-only comparisons in their descriptions. +Their full PR diffs include unmerged prerequisites. diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index 2deb9894d..729ea898c 100644 --- a/docs/how-to-debug-a-hyperlight-guest.md +++ b/docs/how-to-debug-a-hyperlight-guest.md @@ -7,15 +7,24 @@ 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 at the entry point - 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 supports up to four hardware breakpoints. +ARM64 Linux/KVM supports two hardware breakpoints, four-byte `BRK` software +breakpoints, and hardware single-step. KVM reports debug exceptions through +`KVM_EXIT_DEBUG`. Watchpoints are not advertised. + +ARM64 snapshots reject installed software breakpoints, including those added +during checkpointing. Restoring a poisoned sandbox discards its software +breakpoint state and restores clean snapshot memory. + ## Expected behavior Below is a list describing some cases of expected behavior from a gdb debug diff --git a/src/hyperlight_common/src/arch/aarch64/exn.rs b/src/hyperlight_common/src/arch/aarch64/exn.rs index 648822460..f7ba7b996 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 Hyper-V 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 3ec2fe181..2f53dca9e 100644 --- a/src/hyperlight_host/build.rs +++ b/src/hyperlight_host/build.rs @@ -151,7 +151,8 @@ fn main() -> Result<()> { // Essentially the kvm and mshv3 features are ignored on windows as long as you use #[cfg(kvm)] and not #[cfg(feature = "kvm")]. // You should never use #[cfg(feature = "kvm")] or #[cfg(feature = "mshv3")] in the codebase. cfg_aliases::cfg_aliases! { - gdb: { all(feature = "gdb", debug_assertions, target_arch = "x86_64") }, + gdb_platform: { any(target_arch = "x86_64", all(target_arch = "aarch64", target_os = "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..cdb08d7e1 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,205 @@ mod tests { } #[test] + #[cfg(target_arch = "aarch64")] + #[serial] + fn test_gdb_bootstrap_step_preserves_live_stack() { + let (out_file_path, cmd_file_path, manifest_dir) = + gdb_test_paths("gdb-aarch64-bootstrap-stack"); + 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 language c + set $entry = $pc + set $instruction = *(unsigned int *)$entry + set *(unsigned int *)$entry = 0xd503201f + set $bootstrap_sp = $sp + set $canary = $sp - 16 + set $saved = *(unsigned long long *)$canary + set *(unsigned long long *)$canary = 0x123456789abcdef0 + set $sp = $sp - 32 + stepi + if *(unsigned long long *)$canary != 0x123456789abcdef0 + echo Bootstrap debug step overwrote the live stack\\n + quit 1 + end + set *(unsigned long long *)$canary = $saved + set $sp = $bootstrap_sp + set *(unsigned int *)$entry = $instruction + set $pc = $entry + echo Bootstrap debug step preserved the live stack\\n + set logging enabled off + detach + quit + " + ); + let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, |contents| { + contents.contains("Bootstrap debug step preserved the live stack") + }); + cleanup(&out_file_path, &cmd_file_path); + assert!(result.is_ok(), "{}", result.unwrap_err()); + } + + #[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..4904472d7 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,37 @@ impl<'a> DebugMemoryView<'a> { self.mem_mgr.layout.get_guest_code_gva() as u64 } + #[cfg(all(any(kvm, mshv3), 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() + }) + .filter(|&gpa| memory.resolve(gpa, 1).is_some()); + 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 +187,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 +297,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 +309,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 +408,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 +417,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 +551,53 @@ mod tests { } } + #[test] + #[cfg(all(any(kvm, mshv3), 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..b1863ca9e 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,36 @@ use crate::sandbox::snapshot::{NextAction, SnapshotMemoryBacking}; use crate::sandbox::trace::MemTraceInfo; impl HyperlightVm { + #[cfg(gdb)] + pub(crate) fn initialize_debug_vectors( + &mut self, + vectors: Option, + ) -> Result<(), CreateHyperlightVmError> { + if self.gdb_conn.is_none() || !matches!(self.next_action, NextAction::Initialise(_)) { + return Ok(()); + } + #[cfg(kvm)] + if matches!(get_available_hypervisor(), Some(HypervisorType::Kvm)) { + 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; + if self.rsp_gva == regs.sp_el1 { + // Reserve the forwarding spill above the live bootstrap stack. + self.rsp_gva -= 16; + } + 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 +82,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 +136,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 +185,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 +226,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 +257,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..c675d65ba --- /dev/null +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/debug.rs @@ -0,0 +1,725 @@ +// 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, + #[cfg(target_arch = "aarch64")] + Err(error) if resume_requested => { + tracing::error!("{error}"); + DebugResponse::NotAllowed + } + Err(ProcessDebugRequestError::ReadMemory( + DebugMemoryAccessError::TranslateGuestAddress(_), + )) + | Err(ProcessDebugRequestError::Debug(DebugError::TranslateGva(_))) => { + DebugResponse::ErrorOccurred + } + 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()); + + let mut written = false; + let result = (|| { + 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)?; + written = true; + mem_access + .flush_host_instruction_cache(gpa, write_len) + .map_err(ProcessDebugRequestError::WriteMemory)?; + data = &data[write_len..]; + gva += write_len as u64; + } + Ok(()) + })(); + if written { + self.vm.sync_instruction_cache()?; + } + result + } + + 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(any(kvm, mshv3), 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) + ); + } + } + + #[test] + fn resume_translation_errors_keep_the_debug_session_stopped() { + let (mut vm, memory, _) = noncontiguous_pages(); + let (debugger, connection) = DebugCommChannel::unbounded(); + vm.gdb_conn = Some(connection); + let mut regs = vm.vm.regs().unwrap(); + regs.pc = 4 * PAGE_SIZE as u64; + vm.vm.set_regs(®s).unwrap(); + vm.sw_breakpoints.insert(regs.pc, vec![0; 4]); + debugger.send(DebugMsg::Continue).unwrap(); + debugger.send(DebugMsg::Step).unwrap(); + regs.pc = PAGE_SIZE as u64; + debugger + .send(DebugMsg::WriteRegisters(Box::new(( + regs, + crate::hypervisor::regs::CommonFpu::default(), + )))) + .unwrap(); + debugger.send(DebugMsg::Continue).unwrap(); + + vm.handle_debug(&memory, crate::hypervisor::gdb::VcpuStopReason::Initial) + .unwrap(); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::VcpuStopped(_) + )); + for _ in 0..2 { + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::NotAllowed + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::VcpuStopped(_) + )); + } + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::WriteRegisters + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::Continue + )); + } + } +} diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs index b01b44200..a34aca9a4 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs @@ -7,28 +7,29 @@ mod x86_64; #[cfg(target_arch = "aarch64")] mod aarch64; +#[cfg(gdb)] +mod debug; + #[cfg(all(test, not(gdb)))] pub(crate) mod test_support; -#[cfg(gdb)] -use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, Mutex}; use hyperlight_common::log_level::GuestLogFilter; use tracing_core::LevelFilter; +#[cfg(gdb)] +use self::debug::{ProcessDebugRequestError, SoftwareBreakpoints}; use crate::HyperlightError; #[cfg(gdb)] use crate::hypervisor::gdb::DebuggableVm; -#[cfg(gdb)] +#[cfg(all(gdb, target_arch = "x86_64"))] use crate::hypervisor::gdb::arch::VcpuStopReasonError; #[cfg(gdb)] use crate::hypervisor::gdb::{ DebugCommChannel, DebugError, DebugMsg, DebugResponse, GdbTargetError, VcpuStopReason, }; -#[cfg(gdb)] -use crate::hypervisor::hyperlight_vm::x86_64::debug::ProcessDebugRequestError; #[cfg(not(gdb))] use crate::hypervisor::virtual_machine::VirtualMachine; use crate::hypervisor::virtual_machine::{ @@ -206,7 +207,7 @@ pub enum RunVmError { RunVcpu(#[from] RunVcpuError), #[error("Unexpected VM exit: {0}")] UnexpectedVmExit(String), - #[cfg(gdb)] + #[cfg(all(gdb, target_arch = "x86_64"))] #[error("vCPU stop reason error: {0}")] VcpuStopReason(#[from] VcpuStopReasonError), } @@ -257,6 +258,9 @@ pub enum UpdateRegionError { MapMemory(#[from] MapMemoryError), #[error("VM unmap memory error: {0}")] UnmapMemory(#[from] UnmapMemoryError), + #[cfg(all(gdb, target_arch = "aarch64"))] + #[error("Cannot replace snapshot memory while software breakpoints are installed")] + ActiveSoftwareBreakpoints, } /// Errors that can occur when accessing the root page table state @@ -387,14 +391,18 @@ pub(crate) struct HyperlightVm { #[cfg(gdb)] pub(super) gdb_conn: Option>, #[cfg(gdb)] - pub(super) sw_breakpoints: HashMap, // addr -> original instruction + pub(super) sw_breakpoints: SoftwareBreakpoints, + #[cfg(all(gdb, target_arch = "aarch64"))] + pending_software_step: Option, /// One-shot hw breakpoint installed at the entry address when gdb is /// enabled, so the gdb stub gets a `VcpuStopped` to enter its event /// loop on the first vCPU run after construction. Cleared by the /// `VmExit::Debug` arm of `run` the first time a `HwBp` stop fires /// at the entry address. - #[cfg(gdb)] + #[cfg(all(gdb, target_arch = "x86_64"))] pub(super) one_shot_entry_bp: Option, + #[cfg(all(gdb, target_arch = "aarch64"))] + pub(super) initial_debug_stop_pending: bool, #[cfg(feature = "mem_profile")] pub(super) trace_info: MemTraceInfo, #[cfg(crashdump)] @@ -524,6 +532,11 @@ impl HyperlightVm { &mut self, snapshot: SnapshotMemoryBacking, ) -> Result<(), UpdateRegionError> { + #[cfg(all(gdb, target_arch = "aarch64"))] + if self.has_sw_breakpoints() { + return Err(UpdateRegionError::ActiveSoftwareBreakpoints); + } + let target_mappings = snapshot.mappings(); for index in (0..self.snapshot_mappings.len()).rev() { @@ -698,7 +711,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( @@ -706,6 +727,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 @@ -721,12 +743,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)) => { @@ -782,13 +817,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 f6037240a..1e27df6b0 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/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 b691742a1..04202f8d2 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 @@ -710,7 +710,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 8f2ebc566..0feba0570 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"))] + #[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/x86_64.rs b/src/hyperlight_host/src/hypervisor/virtual_machine/whp/x86_64.rs index 9c0d27247..84ec6c1fb 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 @@ -1136,7 +1136,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 80b2ee797..33e606577 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 612a388fd..e76f91964 100644 --- a/src/hyperlight_host/src/sandbox/initialized.rs +++ b/src/hyperlight_host/src/sandbox/initialized.rs @@ -481,6 +481,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; @@ -504,6 +511,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(); @@ -724,6 +737,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 @@ -772,6 +792,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; @@ -1498,6 +1522,230 @@ mod tests { SnapshotOptions::default().scratch_size(scratch_size) } + #[cfg(all(gdb, target_arch = "aarch64"))] + 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, guest.config), + 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() + } + + #[cfg(all(target_os = "linux", any(kvm, mshv3)))] + #[test] + fn debugger_rejects_the_unbacked_io_page() { + let (mut sandbox, _debugger, _) = sandbox_with_debugger(); + let address = hyperlight_common::layout::io_page().unwrap().1; + let memory = DebugMemoryView::new(&sandbox.mem_mgr, Vec::new()); + let root = sandbox.vm.get_root_pt().unwrap(); + assert!(matches!( + memory.translate_gva(address, root), + Err(crate::hypervisor::gdb::DebugError::TranslateGva(gva)) if gva == address + )); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + + #[test] + fn snapshot_rejects_breakpoints_added_during_checkpoint_without_poisoning() { + let (mut sandbox, debugger, breakpoint) = sandbox_with_debugger(); + let snapshot = sandbox.snapshot().unwrap(); + sandbox.call::<()>("NoOp", ()).unwrap(); + assert!(sandbox.transport_dirty); + let NextAction::Call(dispatch) = sandbox.vm.get_next_action() else { + panic!("guest dispatch is not initialized"); + }; + assert!(matches!( + request(&mut sandbox, DebugMsg::AddHwBreakpoint(dispatch)), + DebugResponse::AddHwBreakpoint(true) + )); + debugger + .send(DebugMsg::RemoveHwBreakpoint(dispatch)) + .unwrap(); + debugger + .send(DebugMsg::AddSwBreakpoint(breakpoint)) + .unwrap(); + debugger.send(DebugMsg::Continue).unwrap(); + + let result = sandbox.snapshot(); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::VcpuStopped(VcpuStopReason::HwBp) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::RemoveHwBreakpoint(true) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::AddSwBreakpoint(true) + )); + assert!(matches!( + debugger.try_recv().unwrap(), + DebugResponse::Continue + )); + let error = result + .err() + .expect("snapshot accepted a software breakpoint"); + assert_eq!(sandbox.status(), SandboxStatus::Ready, "{error}"); + assert!(matches!( + request(&mut sandbox, DebugMsg::RemoveSwBreakpoint(breakpoint)), + DebugResponse::RemoveSwBreakpoint(true) + )); + sandbox.restore(snapshot).unwrap(); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + + #[test] + fn cancelled_debug_session_can_restore_with_software_breakpoints() { + let (mut sandbox, _debugger, breakpoint) = sandbox_with_debugger(); + let interrupt = sandbox.interrupt_handle(); + sandbox + .register_host_function("HostNoOp", move || { + interrupt.kill(); + Ok(()) + }) + .unwrap(); + let snapshot = sandbox.snapshot().unwrap(); + assert!(matches!( + request(&mut sandbox, DebugMsg::AddSwBreakpoint(breakpoint)), + DebugResponse::AddSwBreakpoint(true) + )); + assert!(sandbox.restore(snapshot.clone()).is_err()); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + + let error = sandbox.call::<()>("RoundTripHostNoOp", ()).unwrap_err(); + assert!(matches!(error, HyperlightError::ExecutionCanceledByHost())); + assert_eq!(sandbox.status(), SandboxStatus::Poisoned); + sandbox.restore(snapshot).unwrap(); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + + #[test] + fn crash_detach_can_restore_after_breakpoint_mapping_is_lost() { + let (mut sandbox, debugger, _) = sandbox_with_debugger(); + let snapshot = sandbox.snapshot().unwrap(); + // The guard-page backing is outside the guest allocator. + let address = hyperlight_common::layout::scratch_transport_end_gpa(); + sandbox + .call::>("ReadMappedBuffer", (address, 4u64, true)) + .unwrap(); + assert!(matches!( + request(&mut sandbox, DebugMsg::AddSwBreakpoint(address)), + DebugResponse::AddSwBreakpoint(true) + )); + + let root = sandbox.vm.get_root_pt().unwrap(); + let registers = sandbox.vm.get_snapshot_sregs().unwrap(); + let memory = DebugMemoryView::new(&sandbox.mem_mgr, Vec::new()); + let mut table = root; + for shift in [39, 30, 21, 12] { + let entry_address = table + ((address >> shift) & 0x1ff) * 8; + let mut bytes = [0; 8]; + memory.read(&mut bytes, entry_address).unwrap(); + let entry = u64::from_le_bytes(bytes); + assert_ne!(entry & 1, 0); + if shift == 12 || entry & 2 == 0 { + memory.write(&[0; 8], entry_address).unwrap(); + break; + } + table = entry & 0x0000_ffff_ffff_f000; + } + sandbox.vm.apply_sregs(root, ®isters).unwrap(); + debugger.send(DebugMsg::DisableDebug).unwrap(); + + // The guest maps this address, but the hypervisor has no backing for it. + let error = sandbox + .call::>("ReadMappedBuffer", (0x1_0000_0000u64, 1u64, true)) + .unwrap_err(); + let stop = debugger.try_recv().unwrap_or_else(|receive_error| { + panic!("Missing crash stop: {receive_error:?}, guest error: {error:?}") + }); + assert!(matches!( + stop, + DebugResponse::VcpuStopped(VcpuStopReason::Crash) + )); + let response = debugger.try_recv().unwrap(); + assert!( + matches!(response, DebugResponse::DisableDebug), + "{response:?}" + ); + assert_eq!(sandbox.status(), SandboxStatus::Poisoned, "{error:?}"); + sandbox.restore(snapshot).unwrap(); + assert_eq!(sandbox.status(), SandboxStatus::Ready); + sandbox.call::<()>("NoOp", ()).unwrap(); + } + } + fn assert_virtq_attached(sbox: &Sandbox) { assert!(sbox.mem_mgr.g2h_consumer.is_some()); assert!(sbox.mem_mgr.h2g_consumer.is_some()); diff --git a/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs b/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs index 68be2c1bc..757d15740 100644 --- a/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs +++ b/src/hyperlight_host/src/sandbox/snapshot/memory/backing.rs @@ -192,7 +192,7 @@ impl SnapshotMemoryBacking { self.memory.page_table_bytes() } - #[cfg(crashdump)] + #[cfg(any(crashdump, all(gdb, target_arch = "aarch64")))] pub(crate) fn host_range(&self, layer_index: usize) -> Result<(usize, usize)> { #[cfg(not(unshared_snapshot_mem))] let backing = self diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index 6387faced..c00929e09 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -104,6 +104,30 @@ pub(crate) fn set_up_hypervisor_partition( page_size: usize, _load_info: LoadInfo, ) -> Result { + #[cfg(all(gdb, target_arch = "aarch64"))] + if config.get_guest_debug_info().is_some() + && matches!(mgr.next_action, NextAction::Initialise(_)) + && _load_info.debug_vectors.is_none() + { + #[cfg(kvm)] + let needs_vectors = !matches!( + crate::hypervisor::virtual_machine::get_available_hypervisor(), + Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm) + ); + #[cfg(not(kvm))] + let needs_vectors = true; + if needs_vectors { + return Err(HyperlightVmError::Create( + crate::hypervisor::virtual_machine::VmError::Debug( + crate::hypervisor::gdb::DebugError::Unsupported( + "guest binary lacks ARM64 debug vectors", + ), + ) + .into(), + ) + .into()); + } + } // Create gdb thread if gdb is enabled and the configuration is provided #[cfg(gdb)] let gdb_conn = if let Some(DebugInfo { port }) = config.get_guest_debug_info() { @@ -128,7 +152,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 +165,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)] @@ -161,4 +193,31 @@ mod tests { initialize_sandbox(u_sbox).unwrap(); } } + + #[cfg(all(gdb, target_arch = "aarch64"))] + #[test] + fn missing_debug_vectors_does_not_reserve_the_debug_port() { + #[cfg(kvm)] + if matches!( + crate::hypervisor::virtual_machine::get_available_hypervisor(), + Some(crate::hypervisor::virtual_machine::HypervisorType::Kvm) + ) { + return; + } + let listener = std::net::TcpListener::bind("localhost:0").unwrap(); + let address = listener.local_addr().unwrap(); + drop(listener); + let mut config = crate::sandbox::SandboxConfiguration::default(); + config.set_guest_debug_info(crate::sandbox::config::DebugInfo { + port: address.port(), + }); + let mut guest = UninitializedSandbox::new( + GuestBinary::FilePath(simple_guest_as_pathbuf()), + Some(config), + ) + .unwrap(); + guest.load_info.debug_vectors = None; + assert!(initialize_sandbox(guest).is_err()); + std::net::TcpListener::bind(address).expect("failed setup retained the GDB listener"); + } }