Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[profile.arm64-mshv]
test-threads = 4

# Internally parallel tests run alone. VM replacement can double their partition count.
[[profile.arm64-mshv.overrides]]
filter = 'test(/thread|parallel|::create_[0-9]+_/) | (binary(integration_test) & test(/^interrupt_/))'
threads-required = "num-test-threads"
8 changes: 8 additions & 0 deletions .github/workflows/ValidatePullRequest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ jobs:
cpu_vendor: arm
arch: arm64
config: release
- hypervisor: mshv3
cpu_vendor: arm
arch: arm64
config: debug
- hypervisor: mshv3
cpu_vendor: arm
arch: arm64
config: release
uses: ./.github/workflows/dep_build_test.yml
secrets: inherit
with:
Expand Down
57 changes: 40 additions & 17 deletions .github/workflows/dep_build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,8 @@ on:
default: ""
full_aarch64:
description: >-
When "true", also run the single-driver tests on aarch64. PRs leave
this "false" so the aarch64 runner builds and runs the default Rust
tests; the daily schedule (DailyArm64.yml) sets it "true". This
input only affects aarch64: it does not change X64, where those tests
already run on Linux and are skipped on Windows regardless.
When "true", also run the single-driver tests on Linux aarch64.
ARM64 MSHV always runs its single-driver suite once.
required: false
type: string
default: "false"
Expand All @@ -62,7 +59,7 @@ defaults:
jobs:
build-and-test:
if: ${{ inputs.docs_only == 'false' }}
timeout-minutes: ${{ fromJSON(vars.BUILD_TEST_JOB_TIMEOUT || '45') }}
timeout-minutes: ${{ fromJSON(vars.BUILD_TEST_JOB_TIMEOUT || (inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3' && '90' || '45')) }}
runs-on: ${{ fromJson(inputs.arch == 'X64'
&& format('["self-hosted", "{0}", "X64", "1ES.Pool=hld-{1}-{2}", "JobId=build-and-test-{3}-{4}-{5}-{6}"]',
inputs.hypervisor == 'hyperv-ws2025' && 'Windows' || 'Linux',
Expand All @@ -78,6 +75,12 @@ jobs:
github.run_id,
github.run_number,
github.run_attempt)
|| inputs.hypervisor == 'mshv3'
&& format('["self-hosted", "Linux", "ARM64", "1ES.Pool=hld-azlinux3-mshv-arm", "JobId=build-and-test-{0}-{1}-{2}-{3}"]',
inputs.config,
github.run_id,
github.run_number,
github.run_attempt)
|| format('["self-hosted", "{0}", "arm64", "{1}"]',
inputs.hypervisor == 'hvf' && 'macos' || 'Linux',
inputs.hypervisor)) }}
Expand Down Expand Up @@ -158,16 +161,25 @@ jobs:
- name: Build
run: just build ${{ inputs.config }}

- name: Run Rust tests
- name: Limit ARM64 MSHV test concurrency
if: inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3'
run: |
# with default features
just test ${{ inputs.config }}
echo "RUST_TEST_THREADS=1" >> "$GITHUB_ENV"
echo "NEXTEST_PROFILE=arm64-mshv" >> "$GITHUB_ENV"

- name: Install ARM64 MSHV test runner
if: inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3'
uses: taiki-e/install-action@682ce6c3c696a4cd5168a05f98e51aa02b877941 # v2.89.1
with:
tool: cargo-nextest@0.9.151

- name: Run Rust tests
# ARM64 MSHV selects its driver here to avoid a duplicate full run.
run: just test ${{ inputs.config }} ${{ inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3' && 'mshv3 "nextest run"' || '' }}

- name: Run Rust tests with single driver
# Linux-only. Skipped on aarch64 PRs to conserve the limited runners;
# the daily schedule (full_aarch64 == 'true') restores it. On X64 Linux
# it always runs.
if: runner.os == 'Linux' && (inputs.arch == 'X64' || inputs.full_aarch64 == 'true')
# ARM64 MSHV runs this suite in the preceding step.
if: runner.os == 'Linux' && (inputs.arch == 'X64' || (inputs.full_aarch64 == 'true' && inputs.hypervisor != 'mshv3'))
run: |
# with only one driver enabled (kvm/mshv3 features are unix-only, no-op on Windows)
just test ${{ inputs.config }} ${{ inputs.hypervisor == 'mshv3' && 'mshv3' || 'kvm' }}
Expand Down Expand Up @@ -217,6 +229,16 @@ jobs:
RUST_LOG: debug
run: just test-rust-gdb-debugging ${{ inputs.config }}

- name: Run ARM64 MSHV Gdb tests
if: inputs.arch == 'arm64' && inputs.hypervisor == 'mshv3' && inputs.config == 'debug'
env:
RUST_LOG: debug
run: |
if ! command -v gdb > /dev/null 2>&1; then
sudo tdnf install -y gdb
fi
just test-rust-gdb-debugging debug mshv3

- name: Run Rust Crashdump tests
if: inputs.arch == 'X64'
env:
Expand All @@ -234,14 +256,15 @@ jobs:
if: inputs.arch == 'X64'
run: just test-csharp-bindings ${{ inputs.config }}

# Snapshot goldens are not published for ARM64 WHP or MSHV.
- name: Install oras
if: runner.os != 'Windows' || inputs.arch != 'arm64'
if: inputs.arch != 'arm64' || (runner.os != 'Windows' && inputs.hypervisor != 'mshv3')
uses: oras-project/setup-oras@5e2f2482b30b0cb3e14e67f89a80c88c659a7c63 # v2.0.3
with:
version: 1.3.1

- name: Log in to GHCR
if: runner.os != 'Windows' || inputs.arch != 'arm64'
if: inputs.arch != 'arm64' || (runner.os != 'Windows' && inputs.hypervisor != 'mshv3')
env:
GHCR_USER: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand All @@ -250,13 +273,13 @@ jobs:
echo "${GHCR_TOKEN}" | oras login ghcr.io -u "${GHCR_USER}" --password-stdin

- name: Snapshot goldens (pull and verify)
if: ${{ inputs.should_regen_goldens != 'true' && (runner.os != 'Windows' || inputs.arch != 'arm64') }}
if: ${{ inputs.should_regen_goldens != 'true' && (inputs.arch != 'arm64' || (runner.os != 'Windows' && inputs.hypervisor != 'mshv3')) }}
run: |
just snapshot-goldens-pull ${{ inputs.config }}
just snapshot-goldens-verify ${{ inputs.config }}

- name: Snapshot goldens (regenerate and verify)
if: ${{ inputs.should_regen_goldens == 'true' && (runner.os != 'Windows' || inputs.arch != 'arm64') }}
if: ${{ inputs.should_regen_goldens == 'true' && (inputs.arch != 'arm64' || (runner.os != 'Windows' && inputs.hypervisor != 'mshv3')) }}
run: |
# regenerate goldens from current branch
just snapshot-goldens-regenerate ${{ inputs.config }}
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ 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 guests can run on MSHV.
* ARM64 Linux/MSHV 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

Expand Down
12 changes: 6 additions & 6 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ like-ci config=default-target hypervisor="kvm":
just check-license-headers

# runs all tests
test target=default-target features="": (test-unit target features) (test-isolated target features) (test-integration target features) (test-doc target features) (test-tools target)
test target=default-target features="" runner="test": (test-unit target features runner) (test-isolated target features) (test-integration target features runner) (test-doc target features) (test-tools target)

# Cargo launches test binaries through the runner in this variable, for example
# CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER.
Expand All @@ -269,8 +269,8 @@ test-tools target=default-target:
{{ cargo-cmd }} test -p hyperlight-ci --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }}

# runs unit tests
test-unit target=default-target features="":
{{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --lib
test-unit target=default-target features="" runner="test":
{{ cargo-cmd }} {{ runner }} {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} {{ if runner == "test" { "--profile=" } else { "--cargo-profile=" } }}{{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --lib

# runs loom concurrency tests
test-loom:
Expand All @@ -296,7 +296,7 @@ test-isolated target=default-target features="" :
{{ cargo-cmd }} test {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F function_call_metrics," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} -p hyperlight-host --lib -- metrics::tests::test_metrics_are_emitted --exact

# runs integration tests
test-integration target=default-target features="":
test-integration target=default-target features="" runner="test":
@# run execute_on_heap test with feature "executable_heap" on (runs with off during normal tests)
{{ cargo-cmd }} test {{ if features =="" {"--features executable_heap"} else if features=="no-default-features" {"--no-default-features --features executable_heap"} else {"--no-default-features -F executable_heap," + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test integration_test execute_on_heap

Expand All @@ -308,7 +308,7 @@ test-integration target=default-target features="":
@# the glob but no-ops without a mode token, so it needs no golden
@# cache here. It verifies in its own step (see the snapshot-goldens
@# recipes).
{{ cargo-cmd }} test -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*'
{{ cargo-cmd }} {{ runner }} -p hyperlight-host {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} {{ if runner == "test" { "--profile=" } else { "--cargo-profile=" } }}{{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --test '*'

# tests compilation with no default features on different platforms
test-compilation-no-default-features target=default-target:
Expand All @@ -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:: hypervisor::virtual_machine::mshv::aarch64::

# rust test for crashdump
test-rust-crashdump target=default-target features="":
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Or skip setup entirely with a codespace:
| x86-64 | [KVM] | ✅ Yes | ✅ Yes | ✅ Yes |
| x86-64 | [MSHV] | ✅ Yes | ✅ Yes | ✅ Yes |
| AArch64 | [KVM] | ✅ Yes | ❌ No | ❌ No |
| AArch64 | [MSHV] | ❌ No | ❌ No | ❌ No |
| AArch64 | [MSHV] | ✅ Yes | ❌ No | ❌ No |

### Windows

Expand Down
54 changes: 54 additions & 0 deletions docs/gdb-commit-layers.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 14 additions & 3 deletions docs/how-to-debug-a-hyperlight-guest.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,26 @@ 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/MSHV supports two hardware breakpoints, four-byte `BRK` software
breakpoints, and hardware single-step. Guest exception vectors forward debug
exceptions through HVC. Guest binaries must retain the
`hyperlight_debug_vectors` symbol so the host can install these vectors before
the initial debug stop. Watchpoints are not advertised.

ARM64 snapshots reject installed software breakpoints, including those added
during checkpointing. Restoring a poisoned sandbox discards its software
breakpoint state and restores clean snapshot memory.

## Expected behavior

Below is a list describing some cases of expected behavior from a gdb debug
Expand Down
3 changes: 3 additions & 0 deletions src/hyperlight_common/src/arch/aarch64/exn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions src/hyperlight_guest_bin/src/arch/aarch64/exception/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"),
Expand All @@ -148,6 +164,7 @@ vbar:\n",
vbar_entry!("SError", "LowerAArch32"),
ctx_size = const size_of::<ExceptionContext>(),
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,
Expand Down
3 changes: 2 additions & 1 deletion src/hyperlight_host/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "mshv3")) },
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") },
Expand Down
Loading
Loading