diff --git a/Justfile b/Justfile index af599ef8b..dda6897bb 100644 --- a/Justfile +++ b/Justfile @@ -248,6 +248,7 @@ test-rust-gdb-debugging target=default-target features="": # rust test for crashdump test-rust-crashdump target=default-target features="": {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} {{ if features =="" {'--features crashdump'} else { "--features crashdump," + features } }} -- test_crashdump + {{ cargo-cmd }} test --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example crashdump {{ if features =="" {'--features crashdump'} else { "--features crashdump," + features } }} # rust test for tracing test-rust-tracing target=default-target features="": @@ -353,6 +354,7 @@ run-rust-examples target=default-target features="": run-rust-examples-linux target=default-target features="": (run-rust-examples target features) {{ cargo-cmd }} run --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example tracing {{ if features =="" {''} else { "--features " + features } }} {{ cargo-cmd }} run --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example tracing {{ if features =="" {"--features function_call_metrics" } else {"--features function_call_metrics," + features} }} + {{ cargo-cmd }} run --profile={{ if target == "debug" { "dev" } else { target } }} {{ target-triple-flag }} --example crashdump {{ if features =="" {'--features crashdump'} else { "--features crashdump," + features } }} ######################### diff --git a/docs/how-to-debug-a-hyperlight-guest.md b/docs/how-to-debug-a-hyperlight-guest.md index f4b45e996..4640b46bb 100644 --- a/docs/how-to-debug-a-hyperlight-guest.md +++ b/docs/how-to-debug-a-hyperlight-guest.md @@ -264,6 +264,48 @@ The crashdump should be available `/tmp` or in the crash dump directory (see `HY After the core dump has been created, to inspect the state of the guest, load the core dump file using `gdb` or `lldb`. **NOTE: This feature has been tested with version `15.0` of `gdb` and version `17` of `lldb`, earlier versions may not work, it is recommended to use these versions or later.** +#### Using gdb + +Load the core dump alongside the guest binary that was running when the crash occurred: + +```bash +gdb -c +``` + +For example: + +```bash +gdb src/tests/rust_guests/bin/debug/simpleguest -c /tmp/hl_dumps/hl_core_20260225_T165358.517.elf +``` + +Common commands for inspecting the dump: + +```gdb +# View all general-purpose registers (rip, rsp, rflags, etc.) +(gdb) info registers + +# Disassemble around the crash site +(gdb) x/10i $rip + +# View the stack +(gdb) x/16xg $rsp + +# Backtrace (requires debug info in guest binary) +(gdb) bt + +# List all memory regions in the dump (snapshot, scratch, mapped regions) +(gdb) info files + +# Read memory at a specific address +(gdb) x/s
# null-terminated string +(gdb) x/32xb
# 32 bytes in hex +``` + +See the `crashdump` example (`cargo run --example crashdump --features crashdump`) +for a runnable demonstration of both automatic and on-demand crash dumps. + +#### Using VSCode + To do this in vscode, the following configuration can be used to add debug configurations: ```vscode diff --git a/src/hyperlight_guest_bin/src/arch/amd64/dispatch.rs b/src/hyperlight_guest_bin/src/arch/amd64/dispatch.rs index 972ecb715..bb80b30ed 100644 --- a/src/hyperlight_guest_bin/src/arch/amd64/dispatch.rs +++ b/src/hyperlight_guest_bin/src/arch/amd64/dispatch.rs @@ -58,6 +58,8 @@ unsafe extern "C" { core::arch::global_asm!(" .global dispatch_function dispatch_function: + .cfi_startproc + .cfi_undefined rip jnz flush_done mov rdi, cr4 xor rdi, 0x80 @@ -67,4 +69,5 @@ core::arch::global_asm!(" flush_done: call {internal_dispatch_function}\n hlt\n + .cfi_endproc ", internal_dispatch_function = sym crate::guest_function::call::internal_dispatch_function); diff --git a/src/hyperlight_guest_bin/src/arch/amd64/init.rs b/src/hyperlight_guest_bin/src/arch/amd64/init.rs index bc44d8e96..09f3c4775 100644 --- a/src/hyperlight_guest_bin/src/arch/amd64/init.rs +++ b/src/hyperlight_guest_bin/src/arch/amd64/init.rs @@ -157,10 +157,22 @@ unsafe extern "C" { ) -> !; } +// Mark this as the bottom-most frame for debuggers: +// - .cfi_undefined rip: tells DWARF unwinders there is no return +// address to recover (i.e. no caller frame). +// - xor ebp, ebp: sets the frame pointer to zero so frame-pointer- +// based unwinders recognise this as the end of the chain. +// See System V AMD64 ABI: https://gitlab.com/x86-psABIs/x86-64-ABI +// §3.4.1 (Initial Stack and Register State) +// §6.3 Unwinding Through Assembler Code core::arch::global_asm!(" .global pivot_stack\n pivot_stack:\n + .cfi_startproc\n + .cfi_undefined rip\n mov rsp, r8\n + xor ebp, ebp\n call {generic_init}\n hlt\n + .cfi_endproc\n ", generic_init = sym crate::generic_init); diff --git a/src/hyperlight_host/examples/crashdump/main.rs b/src/hyperlight_host/examples/crashdump/main.rs new file mode 100644 index 000000000..f4575ab5b --- /dev/null +++ b/src/hyperlight_host/examples/crashdump/main.rs @@ -0,0 +1,596 @@ +/* +Copyright 2025 The Hyperlight Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +//! # Crash Dump Example +//! +//! This example demonstrates Hyperlight's crash dump feature, which generates +//! ELF core dump files containing vCPU state (general-purpose registers, +//! segment registers, XSAVE state) and guest memory (snapshot, scratch, +//! and any dynamically mapped regions). These can be loaded into `gdb` +//! for post-mortem debugging. +//! +//! The crash dump feature must be enabled via the `crashdump` Cargo feature: +//! +//! ```bash +//! cargo run --example crashdump --features crashdump +//! ``` +//! +//! ## What this example shows +//! +//! 1. **Automatic crash dump** — When a guest triggers a VM-level fault +//! that bypasses the guest's exception handler (e.g., writing to a +//! region the hypervisor mapped as read-only), Hyperlight automatically +//! writes an ELF core dump file. +//! +//! 2. **On-demand crash dump** — When the guest's IDT catches the fault +//! (e.g., undefined instruction) and reports it back as a `GuestAborted` +//! error, the automatic crash dump is not triggered. You can call +//! [`MultiUseSandbox::generate_crashdump`] explicitly to capture the +//! VM state. +//! +//! 3. **Disabling crash dumps per sandbox** — You can opt out of crash dump +//! generation for individual sandboxes via +//! [`SandboxConfiguration::set_guest_core_dump`]. +//! +//! 4. **On-demand crash dump from a debugger** — The `generate_crashdump()` +//! method is available for use from gdb while the guest is mid-execution. +//! +//! ## How crashes are reported +//! +//! The Hyperlight guest runtime includes an exception handler that catches most +//! hardware faults (page faults, undefined instructions, etc.) and reports them +//! back to the host as `GuestAborted` errors with diagnostic information. +//! +//! Automatic core dumps are triggered for unhandled VM-level exits that bypass +//! the guest exception handler. For most debugging workflows, the on-demand +//! `generate_crashdump()` method (called from gdb) is the recommended way to +//! capture VM state. +//! +//! ## Controlling the output directory +//! +//! Set the `HYPERLIGHT_CORE_DUMP_DIR` environment variable to specify a custom +//! output directory. If unset, core dump files are written to the system's +//! temporary directory: +//! +//! ```bash +//! HYPERLIGHT_CORE_DUMP_DIR=/tmp/hl_dumps cargo run --example crashdump --features crashdump +//! ``` +//! +//! Core dump files are named `hl_core_.elf`. + +#![allow(clippy::disallowed_macros)] + +#[cfg(all(crashdump, target_os = "linux"))] +use std::io::Write; + +#[cfg(all(crashdump, target_os = "linux"))] +use hyperlight_host::HyperlightError; +use hyperlight_host::sandbox::SandboxConfiguration; +use hyperlight_host::{GuestBinary, MultiUseSandbox, UninitializedSandbox}; + +fn main() -> hyperlight_host::Result<()> { + // Only enable logging if the user explicitly sets RUST_LOG; keep + // the example output clean by default. + if std::env::var_os("RUST_LOG").is_some() { + env_logger::init(); + } + + let guest_path = + hyperlight_testing::simple_guest_as_string().expect("Cannot find simpleguest binary"); + + println!("=== Hyperlight Crash Dump Example ===\n"); + + // ----------------------------------------------------------------------- + // Part 1: Guest-caused crash dump (VM-level fault bypasses guest handler) + // ----------------------------------------------------------------------- + println!("--- Part 1: Automatic crash dump (memory access violation) ---\n"); + + guest_crash_auto_dump(&guest_path)?; + + // ----------------------------------------------------------------------- + // Part 2: On-demand crash dump (guest-caught exception) + // ----------------------------------------------------------------------- + println!("\n--- Part 2: On-demand crash dump (guest-caught exception) ---\n"); + + guest_crash_with_on_demand_dump(&guest_path)?; + + // ----------------------------------------------------------------------- + // Part 3: Guest crash with crash dump feature disabled per-sandbox + // ----------------------------------------------------------------------- + println!("\n--- Part 3: Guest crash with crash dump disabled per sandbox ---\n"); + + guest_crash_with_dump_disabled(&guest_path)?; + + // ----------------------------------------------------------------------- + // Part 4: On-demand crash dump (from gdb) + // ----------------------------------------------------------------------- + println!("\n--- Part 4: On-demand crash dump API ---"); + + print_on_demand_info(); + + println!("\n=== Done ==="); + Ok(()) +} + +/// Demonstrates an **automatic** crash dump triggered by a VM-level fault +/// that bypasses the guest exception handler entirely. +/// +/// The guest has an IDT (Interrupt Descriptor Table) that catches most CPU +/// exceptions (page faults, undefined instructions, etc.) and reports them +/// back to the host as `GuestAborted` errors and doesn't create a crashdump. +/// +/// These hypervisor-level exits produce `MemoryAccessViolation` errors, +/// and Hyperlight automatically writes a crash dump for them. +/// +/// This function: +/// 1. Maps a file into the guest as read-only (via `map_file_cow`) +/// 2. Calls `WriteMappedBuffer` which tries to write to that region +/// 3. The hypervisor rejects the write → `MemoryAccessViolation` +/// 4. The crash dump is written automatically (no explicit call needed) +#[cfg(all(crashdump, target_os = "linux"))] +fn guest_crash_auto_dump(guest_path: &str) -> hyperlight_host::Result<()> { + let cfg = SandboxConfiguration::default(); + + let uninitialized_sandbox = + UninitializedSandbox::new(GuestBinary::FilePath(guest_path.to_string()), Some(cfg))?; + + let mut sandbox: MultiUseSandbox = uninitialized_sandbox.evolve()?; + + // Map a file as read-only into the guest at a known address. + let mapping_file = create_mapping_file(); + let guest_base: u64 = 0x200000000; + let len = sandbox.map_file_cow(mapping_file.as_path(), guest_base)?; + println!("Mapped {len} bytes at guest address {guest_base:#x} (read-only)."); + + // Call WriteMappedBuffer — the guest maps the address in its page tables + // as writable, but the hypervisor's mapping is read-only. + // The write triggers an MMIO exit that the guest exception handler + // never sees. + println!("Calling guest function 'WriteMappedBuffer' on read-only region..."); + let result = sandbox.call::("WriteMappedBuffer", (guest_base, len)); + + match result { + Ok(_) => panic!("Unexpected success."), + Err(HyperlightError::MemoryAccessViolation(addr, ..)) => { + println!("Guest crashed with a memory access violation at {addr:#x}."); + } + Err(e) => panic!("Unexpected error: {e}"), + } + + Ok(()) +} + +/// Fallback when crashdump feature or Linux is not available. +#[cfg(not(all(crashdump, target_os = "linux")))] +fn guest_crash_auto_dump(_guest_path: &str) -> hyperlight_host::Result<()> { + println!( + "This part requires the `crashdump` feature and Linux.\n\ + Re-run with: cargo run --example crashdump --features crashdump" + ); + Ok(()) +} + +/// Create a temporary file with known content to map into the guest. +/// +/// Creates a page-aligned (4 KiB) file containing a marker string. +#[cfg(all(crashdump, target_os = "linux"))] +fn create_mapping_file() -> std::path::PathBuf { + let path = std::env::temp_dir().join("hyperlight_crashdump_example.bin"); + let mut f = std::fs::File::create(&path).expect("create mapping file"); + let mut content = vec![0u8; 4096]; + let marker = b"HYPERLIGHT_CRASHDUMP_EXAMPLE"; + content[..marker.len()].copy_from_slice(marker); + f.write_all(&content).expect("write mapping file"); + path +} + +/// Demonstrates an **on-demand** crash dump for a guest-caught exception. +/// +/// When the guest triggers a CPU exception that its IDT handles (e.g., an +/// undefined instruction via `ud2`), the guest exception handler catches +/// it and sends a `GuestAborted` error back to the host via an I/O port. +/// +/// Because the error is reported through the I/O path (not a VM-level +/// fault), the automatic crash dump code in the VM run loop is not reached. +/// To get a crash dump in this case, call `generate_crashdump()` explicitly. +fn guest_crash_with_on_demand_dump(guest_path: &str) -> hyperlight_host::Result<()> { + let cfg = SandboxConfiguration::default(); + + let uninitialized_sandbox = + UninitializedSandbox::new(GuestBinary::FilePath(guest_path.to_string()), Some(cfg))?; + + let mut sandbox: MultiUseSandbox = uninitialized_sandbox.evolve()?; + + // This call triggers a ud2 instruction in the guest. The guest's IDT + // catches the #UD exception and reports it back to the host as a + // GuestAborted error via I/O. This does NOT trigger an automatic crash + // dump — we must call generate_crashdump() explicitly. + println!("Calling guest function 'TriggerException'..."); + let result = sandbox.call::<()>("TriggerException", ()); + + match result { + Ok(_) => panic!("Unexpected success."), + Err(_) => { + println!("Guest crashed (undefined instruction)."); + + #[cfg(crashdump)] + sandbox.generate_crashdump()?; + + #[cfg(not(crashdump))] + println!("Re-run with: cargo run --example crashdump --features crashdump"); + } + } + + Ok(()) +} + +/// Shows how to disable crash dump generation for a specific sandbox. +/// +/// This repeats the same memory-access-violation scenario from Part 1, +/// but with crash dumps disabled. The VM-level fault still occurs, but +/// no core dump file is written. +/// +/// This is useful when you know certain sandboxes will intentionally crash +/// (e.g., during fuzzing or testing) and you don't want the overhead of +/// writing core dump files. +#[cfg(all(crashdump, target_os = "linux"))] +fn guest_crash_with_dump_disabled(guest_path: &str) -> hyperlight_host::Result<()> { + let mut cfg = SandboxConfiguration::default(); + cfg.set_guest_core_dump(false); + println!("Core dump disabled for this sandbox."); + + let uninitialized_sandbox = + UninitializedSandbox::new(GuestBinary::FilePath(guest_path.to_string()), Some(cfg))?; + + let mut sandbox: MultiUseSandbox = uninitialized_sandbox.evolve()?; + + let mapping_file = create_mapping_file(); + let guest_base: u64 = 0x200000000; + let len = sandbox.map_file_cow(mapping_file.as_path(), guest_base)?; + + println!("Calling guest function 'WriteMappedBuffer' on read-only region..."); + let result = sandbox.call::("WriteMappedBuffer", (guest_base, len)); + + match result { + Ok(_) => panic!("Unexpected success."), + Err(HyperlightError::MemoryAccessViolation(addr, ..)) => { + println!( + "Guest crashed with a memory access violation at {addr:#x}. No core dump generated." + ); + } + Err(e) => panic!("Unexpected error: {e}"), + } + + Ok(()) +} + +/// Fallback when crashdump feature or Linux is not available. +#[cfg(not(all(crashdump, target_os = "linux")))] +fn guest_crash_with_dump_disabled(_guest_path: &str) -> hyperlight_host::Result<()> { + println!( + "This part requires the `crashdump` feature and Linux.\n\ + Re-run with: cargo run --example crashdump --features crashdump" + ); + Ok(()) +} + +/// Prints information about the on-demand crash dump API. +/// +/// The [`MultiUseSandbox::generate_crashdump`] method captures the current +/// vCPU state and writes it to an ELF core dump file. This is primarily +/// useful when attached to a running process via gdb — for example, when a +/// guest function hangs or takes too long to complete. +/// +/// ## gdb workflow +/// +/// ```text +/// # Attach to the running process +/// sudo gdb -p +/// +/// # Find the thread running the guest +/// (gdb) info threads +/// (gdb) thread +/// +/// # Navigate to the frame with the sandbox variable +/// (gdb) backtrace +/// (gdb) frame +/// +/// # Generate the core dump +/// (gdb) call sandbox.generate_crashdump() +/// ``` +/// +/// The core dump file will be written to `HYPERLIGHT_CORE_DUMP_DIR` (or the +/// system temp directory) as `hl_core_.elf`. +fn print_on_demand_info() { + #[cfg(crashdump)] + println!( + "\nUse MultiUseSandbox::generate_crashdump() from gdb to capture\n\ + VM state mid-execution. See docs/how-to-debug-a-hyperlight-guest.md." + ); +} + +// --------------------------------------------------------------------------- +// GDB-based crash dump validation tests +// +// These tests follow the same pattern used by the `guest-debugging` example: +// generate a core dump, then load it in GDB (batch mode) and verify the +// output contains the expected register values and mapped memory content. +// +// Requires: +// - The `crashdump` cargo feature +// - Linux (mmap-based file mapping is used) +// - `rust-gdb` available on PATH +// --------------------------------------------------------------------------- +#[cfg(crashdump)] +#[cfg(target_os = "linux")] +#[cfg(test)] +mod tests { + use std::fs; + use std::io::Write; + use std::path::{Path, PathBuf}; + use std::process::Command; + + use hyperlight_host::sandbox::SandboxConfiguration; + use hyperlight_host::{GuestBinary, MultiUseSandbox, UninitializedSandbox}; + use serial_test::serial; + + #[cfg(not(windows))] + const GDB_COMMAND: &str = "rust-gdb"; + + /// Guest base address where we map the test data file. + /// This address sits outside the normal sandbox memory layout. + const MAP_GUEST_BASE: u64 = 0x200000000; + + /// Sentinel string written into the mapped region so we can verify + /// GDB can read it back from the core dump. + const TEST_SENTINEL: &[u8] = b"HYPERLIGHT_CRASHDUMP_TEST"; + + // -- helpers ------------------------------------------------------------ + + /// Returns `true` if `rust-gdb` (or `gdb` on Windows) is available. + fn gdb_is_available() -> bool { + Command::new(GDB_COMMAND) + .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .map_or(false, |s| s.success()) + } + + /// Create a page-aligned temp file in `dir` containing [`TEST_SENTINEL`] + /// padded to one page (4 KiB). + fn create_test_data_file(dir: &Path) -> PathBuf { + let path = dir.join("test_mapping.bin"); + let mut f = fs::File::create(&path).expect("create test data file"); + let mut content = vec![0u8; 4096]; + content[..TEST_SENTINEL.len()].copy_from_slice(TEST_SENTINEL); + f.write_all(&content).expect("write test data"); + path + } + + /// Build a sandbox, map a file with known content, trigger a crash and + /// return the path to the generated ELF core dump. + /// + /// `dump_dir` controls where the core dump is written. + fn generate_crashdump_with_content(dump_dir: &Path) -> PathBuf { + let data_file = create_test_data_file(dump_dir); + + // Create sandbox with default config (crashdump enabled) + let guest_path = + hyperlight_testing::simple_guest_as_string().expect("Cannot find simpleguest binary"); + let cfg = SandboxConfiguration::default(); + let u_sbox = + UninitializedSandbox::new(GuestBinary::FilePath(guest_path), Some(cfg)).unwrap(); + let mut sbox: MultiUseSandbox = u_sbox.evolve().unwrap(); + + // Map an additional test file into the guest at a known address. + // The core dump already includes snapshot and scratch regions + // automatically. This mapping lets us verify that GDB can read + // a specific sentinel string from a known address. + let len = sbox + .map_file_cow(&data_file, MAP_GUEST_BASE) + .expect("map_file_cow"); + + // Read the mapped region back through the guest and verify it + // contains the sentinel we wrote. + // The also maps the file in so the guest can see it and we will be able to read it as well + // in the crashdump since we are only dumping the GVA + let result: Vec = sbox + .call("ReadMappedBuffer", (MAP_GUEST_BASE, len as u64, true)) + .expect("ReadMappedBuffer should succeed"); + let sentinel_str = + std::str::from_utf8(TEST_SENTINEL).expect("TEST_SENTINEL is valid UTF-8"); + assert!( + result.starts_with(TEST_SENTINEL), + "Guest should read back the sentinel string \"{sentinel_str}\" from mapped memory.\n\ + Got: {:?}", + &result[..TEST_SENTINEL.len().min(result.len())] + ); + + // Trigger a crash — TriggerException causes a GuestAborted error via + // the guest exception handler's IO-based reporting mechanism. + let result = sbox.call::<()>("TriggerException", ()); + assert!(result.is_err(), "TriggerException should return an error"); + + // Use the on-demand crash dump API to capture the VM state. + // The automatic crash dump path in the VM run loop is bypassed for + // IO-based errors (GuestAborted), so we call generate_crashdump() + // explicitly — this is the recommended workflow for post-mortem + // debugging anyway. + sbox.generate_crashdump_to_dir(dump_dir.to_string_lossy()) + .expect("generate_crashdump should succeed"); + + // Find the generated hl_core_*.elf file + let mut elf_files: Vec = fs::read_dir(dump_dir) + .unwrap() + .filter_map(|e| e.ok()) + .map(|e| e.path()) + .filter(|p| { + p.file_name() + .and_then(|n| n.to_str()) + .map_or(false, |n| n.starts_with("hl_core_") && n.ends_with(".elf")) + }) + .collect(); + + assert!( + !elf_files.is_empty(), + "No core dump file (hl_core_*.elf) found in {}", + dump_dir.display() + ); + + // Return the newest one (lexicographic sort by timestamp works) + elf_files.sort(); + elf_files.pop().unwrap() + } + + /// Write GDB batch commands to `cmd_path`, run GDB, and return the + /// content of the logging output file. + fn run_gdb_batch(cmd_path: &Path, out_path: &Path, cmds: &str) -> String { + fs::write(cmd_path, cmds).expect("write gdb command file"); + + let output = Command::new(GDB_COMMAND) + .arg("-nx") // skip .gdbinit + .arg("--nw") + .arg("--batch") + .arg("-x") + .arg(cmd_path) + .output() + .expect("Failed to spawn rust-gdb — is it installed?"); + + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + + fs::read_to_string(out_path).unwrap_or_else(|_| { + panic!("GDB did not produce an output file.\nstdout:\n{stdout}\nstderr:\n{stderr}"); + }) + } + + // -- tests -------------------------------------------------------------- + + /// Verify that GDB can load the crash dump and display vCPU registers. + #[test] + #[serial] + fn test_crashdump_gdb_registers() { + if !gdb_is_available() { + eprintln!("Skipping test: {GDB_COMMAND} not found on PATH"); + return; + } + + let dump_dir = tempfile::tempdir().expect("create temp dir"); + let core_path = generate_crashdump_with_content(dump_dir.path()); + let guest_path = hyperlight_testing::simple_guest_as_string().expect("simpleguest binary"); + + let cmd_file = dump_dir.path().join("gdb_reg_cmds.txt"); + let out_file = dump_dir.path().join("gdb_reg_output.txt"); + + let cmds = format!( + "\ +set pagination off +set logging file {out} +set logging enabled on +file {binary} +core-file {core} +echo === REGISTERS ===\\n +info registers +echo === DONE ===\\n +set logging enabled off +quit +", + out = out_file.display(), + binary = guest_path, + core = core_path.display(), + ); + + let gdb_output = run_gdb_batch(&cmd_file, &out_file, &cmds); + println!("GDB register output:\n{gdb_output}"); + + assert!( + gdb_output.contains("=== REGISTERS ==="), + "GDB should have printed the REGISTERS marker.\nOutput:\n{gdb_output}" + ); + assert!( + gdb_output.contains("rip") && gdb_output.contains("rsp"), + "GDB should show rip and rsp register values.\nOutput:\n{gdb_output}" + ); + assert!( + gdb_output.contains("=== DONE ==="), + "GDB should have completed successfully.\nOutput:\n{gdb_output}" + ); + } + + /// Verify that GDB can read the mapped memory region from the core dump + /// and that it contains the sentinel string we wrote before the crash. + #[test] + #[serial] + fn test_crashdump_gdb_memory() { + let dump_dir = tempfile::tempdir().expect("create temp dir"); + let core_path = generate_crashdump_with_content(dump_dir.path()); + let guest_path = hyperlight_testing::simple_guest_as_string().expect("simpleguest binary"); + + let cmd_file = dump_dir.path().join("gdb_mem_cmds.txt"); + let out_file = dump_dir.path().join("gdb_mem_output.txt"); + + let cmds = format!( + "\ +set pagination off +set logging file {out} +set logging enabled on +file {binary} +core-file {core} +echo === MEMORY ===\\n +x/s {addr:#x} +echo === BACKTRACE ===\\n +bt +echo === DONE ===\\n +set logging enabled off +quit +", + out = out_file.display(), + binary = guest_path, + core = core_path.display(), + addr = MAP_GUEST_BASE, + ); + + let gdb_output = run_gdb_batch(&cmd_file, &out_file, &cmds); + println!("GDB memory output:\n{gdb_output}"); + + let sentinel_str = + std::str::from_utf8(TEST_SENTINEL).expect("TEST_SENTINEL is valid UTF-8"); + + assert!( + gdb_output.contains("=== MEMORY ==="), + "GDB should have printed the MEMORY marker.\nOutput:\n{gdb_output}" + ); + assert!( + gdb_output.contains(sentinel_str), + "GDB should read back the sentinel string \"{sentinel_str}\" from mapped memory.\n\ + Output:\n{gdb_output}" + ); + assert!( + gdb_output.contains("=== BACKTRACE ==="), + "GDB should have printed the BACKTRACE marker.\nOutput:\n{gdb_output}" + ); + assert!( + gdb_output.contains("0x0000000000000000 in ?? ()"), + "GDB backtrace should unwind to the null return address at the \ + bottom of the guest stack.\nOutput:\n{gdb_output}" + ); + assert!( + gdb_output.contains("=== DONE ==="), + "GDB should have completed successfully.\nOutput:\n{gdb_output}" + ); + } +} diff --git a/src/hyperlight_host/src/hypervisor/crashdump.rs b/src/hyperlight_host/src/hypervisor/crashdump.rs index e42a62619..44ba0891b 100644 --- a/src/hyperlight_host/src/hypervisor/crashdump.rs +++ b/src/hyperlight_host/src/hypervisor/crashdump.rs @@ -24,7 +24,9 @@ use elfcore::{ }; use crate::hypervisor::hyperlight_vm::HyperlightVm; -use crate::mem::memory_region::{MemoryRegion, MemoryRegionFlags}; +use crate::mem::memory_region::{CrashDumpRegion, MemoryRegionFlags}; +use crate::mem::mgr::SandboxMemoryManager; +use crate::mem::shared_mem::HostSharedMemory; use crate::{Result, new_error}; /// This constant is used to identify the XSAVE state in the core dump @@ -44,7 +46,7 @@ const CORE_DUMP_PAGE_SIZE: usize = 0x1000; /// This structure contains the information needed to create a core dump #[derive(Debug)] pub(crate) struct CrashDumpContext { - regions: Vec, + regions: Vec, regs: [u64; 27], xsave: Vec, entry: u64, @@ -54,7 +56,7 @@ pub(crate) struct CrashDumpContext { impl CrashDumpContext { pub(crate) fn new( - regions: Vec, + regions: Vec, regs: [u64; 27], xsave: Vec, entry: u64, @@ -90,7 +92,7 @@ impl GuestView { .map(|r| VaRegion { begin: r.guest_region.start as u64, end: r.guest_region.end as u64, - offset: <_ as Into>::into(r.host_region.start) as u64, + offset: r.host_region.start as u64, protection: VaProtection { is_private: false, read: r.flags.contains(MemoryRegionFlags::READ), @@ -202,7 +204,7 @@ impl ProcessInfoSource for GuestView { /// This structure serves as a custom memory reader for `elfcore`'s /// [`CoreDumpBuilder`] struct GuestMemReader { - regions: Vec, + regions: Vec, } impl GuestMemReader { @@ -225,7 +227,7 @@ impl ReadProcessMemory for GuestMemReader { let offset = base - r.guest_region.start; let region_slice = unsafe { std::slice::from_raw_parts( - <_ as Into>::into(r.host_region.start) as *const u8, + r.host_region.start as *const u8, r.guest_region.len(), ) }; @@ -254,22 +256,30 @@ impl ReadProcessMemory for GuestMemReader { /// /// This function generates an ELF core dump file capturing the hypervisor's state, /// which can be used for debugging when crashes occur. -/// The location of the core dump file is determined by the `HYPERLIGHT_CORE_DUMP_DIR` -/// environment variable. If not set, it defaults to the system's temporary directory. +/// +/// If `override_dir` is `Some`, the core dump is placed there. Otherwise, the +/// location is determined by the `HYPERLIGHT_CORE_DUMP_DIR` environment variable. +/// If neither is set, it defaults to the system's temporary directory. /// /// # Arguments /// * `hv`: Reference to the hypervisor implementation +/// * `mem_mgr`: Mutable reference to the sandbox memory manager +/// * `override_dir`: Optional directory path that takes priority over the environment variable /// /// # Returns /// * `Result<()>`: Success or error -pub(crate) fn generate_crashdump(hv: &HyperlightVm) -> Result<()> { +pub(crate) fn generate_crashdump( + hv: &HyperlightVm, + mem_mgr: &mut SandboxMemoryManager, + override_dir: Option, +) -> Result<()> { // Get crash context from hypervisor let ctx = hv - .crashdump_context() + .crashdump_context(mem_mgr) .map_err(|e| new_error!("Failed to get crashdump context: {:?}", e))?; - // Get env variable for core dump directory - let core_dump_dir = std::env::var("HYPERLIGHT_CORE_DUMP_DIR").ok(); + // Prefer the explicit override, then the env var, then the system temp dir + let core_dump_dir = override_dir.or_else(|| std::env::var("HYPERLIGHT_CORE_DUMP_DIR").ok()); // Compute file path on the filesystem let file_path = core_dump_file_path(core_dump_dir); @@ -463,20 +473,10 @@ mod test { #[test] fn test_crashdump_dummy_core_dump() { let dummy_vec = vec![0; 0x1000]; - use crate::mem::memory_region::{HostGuestMemoryRegion, MemoryRegionKind}; - #[cfg(target_os = "windows")] - let host_base = crate::mem::memory_region::HostRegionBase { - from_handle: windows::Win32::Foundation::INVALID_HANDLE_VALUE.into(), - handle_base: 0, - handle_size: -1isize as usize, - offset: dummy_vec.as_ptr() as usize, - }; - #[cfg(not(target_os = "windows"))] - let host_base = dummy_vec.as_ptr() as usize; - let host_end = ::add(host_base, dummy_vec.len()); - let regions = vec![MemoryRegion { + let ptr = dummy_vec.as_ptr() as usize; + let regions = vec![CrashDumpRegion { guest_region: 0x1000..0x2000, - host_region: host_base..host_end, + host_region: ptr..ptr + dummy_vec.len(), flags: MemoryRegionFlags::READ | MemoryRegionFlags::WRITE, region_type: crate::mem::memory_region::MemoryRegionType::Code, }]; diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs index 80298a79e..d3a210926 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -330,6 +330,17 @@ pub enum AccessPageTableError { AccessRegs(#[from] RegisterError), } +#[cfg(crashdump)] +#[derive(Debug, thiserror::Error)] +pub enum CrashDumpError { + #[error("Failed to generate crashdump because of a register error: {0}")] + GetRegs(#[from] RegisterError), + #[error("Failed to get root PT during crashdump generation: {0}")] + GetRootPt(#[from] AccessPageTableError), + #[error("Failed to get guest memory mapping during crashdump generation: {0}")] + AccessPageTable(Box), +} + /// Errors that can occur during HyperlightVm creation #[derive(Debug, thiserror::Error)] pub enum CreateHyperlightVmError { @@ -678,12 +689,21 @@ impl HyperlightVm { Ok(()) } - /// Get the current base page table physical address - pub(crate) fn get_root_pt(&mut self) -> Result { - let sregs = self.vm.sregs()?; - - // Mask off the flags bits - Ok(sregs.cr3 & !0xfff_u64) + /// Get the current base page table physical address. + /// + /// With `init-paging`, reads CR3 from the vCPU special registers. + /// Without `init-paging`, returns 0 (identity-mapped, no page tables). + pub(crate) fn get_root_pt(&self) -> Result { + #[cfg(feature = "init-paging")] + { + let sregs = self.vm.sregs()?; + // Mask off the flags bits + Ok(sregs.cr3 & !0xfff_u64) + } + #[cfg(not(feature = "init-paging"))] + { + Ok(0) + } } /// Get the special registers that need to be stored in a snapshot. @@ -956,7 +976,7 @@ impl HyperlightVm { Err(e) => { #[cfg(crashdump)] if self.rt_cfg.guest_core_dump { - crashdump::generate_crashdump(self) + crashdump::generate_crashdump(self, mem_mgr, None) .map_err(|e| RunVmError::CrashdumpGeneration(Box::new(e)))?; } @@ -1190,7 +1210,8 @@ impl HyperlightVm { #[cfg(crashdump)] pub(crate) fn crashdump_context( &self, - ) -> std::result::Result, RegisterError> { + mem_mgr: &mut SandboxMemoryManager, + ) -> std::result::Result, CrashDumpError> { if self.rt_cfg.guest_core_dump { let mut regs = [0; 27]; @@ -1234,14 +1255,24 @@ impl HyperlightVm { .and_then(|name| name.to_os_string().into_string().ok()) }); - let initialise = match self.entrypoint { - NextAction::Initialise(initialise) => initialise, - _ => 0, - }; + // Use the stored entry point address from the runtime config. + // This is the original entry point (load_addr + ELF entry offset) + // which GDB needs for AT_ENTRY to compute the PIE load offset. + // We cannot use self.entrypoint here because it transitions from + // Initialise(addr) to Call(dispatch_addr) after guest init. + let initialise = self.rt_cfg.entry_point.unwrap_or_else(|| { + tracing::warn!( + "entry_point was never set in SandboxRuntimeConfig; AT_ENTRY will be 0" + ); + 0 + }); + let mmap_regions: Vec = self.get_mapped_regions().cloned().collect(); + let root_pt = self.get_root_pt()?; + + let regions = mem_mgr + .get_guest_memory_regions(root_pt, &mmap_regions) + .map_err(|e| CrashDumpError::AccessPageTable(Box::new(e)))?; - // Include dynamically mapped regions - // TODO: include the snapshot and scratch regions - let regions: Vec = self.get_mapped_regions().cloned().collect(); Ok(Some(crashdump::CrashDumpContext::new( regions, regs, @@ -2197,7 +2228,7 @@ mod tests { &config, stack_top_gva, #[cfg(any(crashdump, gdb))] - &rt_cfg, + rt_cfg, crate::mem::exe::LoadInfo::dummy(), ) .unwrap(); diff --git a/src/hyperlight_host/src/hypervisor/mod.rs b/src/hyperlight_host/src/hypervisor/mod.rs index ddc3da3ee..a582ddb09 100644 --- a/src/hyperlight_host/src/hypervisor/mod.rs +++ b/src/hyperlight_host/src/hypervisor/mod.rs @@ -494,7 +494,7 @@ pub(crate) mod tests { &config, exn_stack_top_gva, #[cfg(any(crashdump, gdb))] - &rt_cfg, + rt_cfg, sandbox.load_info, )?; diff --git a/src/hyperlight_host/src/mem/memory_region.rs b/src/hyperlight_host/src/mem/memory_region.rs index a2b37e982..9cfa44028 100644 --- a/src/hyperlight_host/src/mem/memory_region.rs +++ b/src/hyperlight_host/src/mem/memory_region.rs @@ -248,6 +248,50 @@ pub(crate) struct MemoryRegion_ { pub(crate) type MemoryRegion = MemoryRegion_; +/// A [`MemoryRegionKind`] for crash dump regions that always uses raw +/// `usize` host addresses. The crash dump path only reads host memory +/// through raw pointers, so it never needs the file-mapping metadata +/// stored in [`HostRegionBase`] on Windows. +#[cfg(crashdump)] +#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] +pub(crate) struct CrashDumpMemoryRegion; + +#[cfg(crashdump)] +impl MemoryRegionKind for CrashDumpMemoryRegion { + type HostBaseType = usize; + + fn add(base: Self::HostBaseType, size: usize) -> Self::HostBaseType { + base + size + } +} + +/// A memory region used exclusively by the crash dump path. +/// +/// Host addresses are always raw `usize` pointers, avoiding the need +/// to construct platform-specific wrappers like [`HostRegionBase`]. +#[cfg(crashdump)] +pub(crate) type CrashDumpRegion = MemoryRegion_; + +#[cfg(crashdump)] +impl HostGuestMemoryRegion { + /// Extract the raw `usize` host address from the platform-specific + /// host base type. + /// + /// On Linux this is identity (`HostBaseType` = `usize`). + /// On Windows it computes `handle_base + offset` via the existing + /// `From for usize` impl. + pub(crate) fn to_addr(val: ::HostBaseType) -> usize { + #[cfg(not(target_os = "windows"))] + { + val + } + #[cfg(target_os = "windows")] + { + val.into() + } + } +} + #[cfg_attr(not(feature = "init-paging"), allow(unused))] pub(crate) struct MemoryRegionVecBuilder { guest_base_phys_addr: usize, diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 6c9433dab..c03dea8a0 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -20,15 +20,112 @@ use hyperlight_common::flatbuffer_wrappers::function_call::{ use hyperlight_common::flatbuffer_wrappers::function_types::FunctionCallResult; use hyperlight_common::flatbuffer_wrappers::guest_log_data::GuestLogData; use hyperlight_common::vmem::{self, PAGE_TABLE_SIZE, PageTableEntry, PhysAddr}; +#[cfg(all(feature = "crashdump", feature = "init-paging"))] +use hyperlight_common::vmem::{BasicMapping, MappingKind}; use tracing::{Span, instrument}; use super::layout::SandboxMemoryLayout; -use super::memory_region::MemoryRegion; use super::shared_mem::{ExclusiveSharedMemory, GuestSharedMemory, HostSharedMemory, SharedMemory}; use crate::hypervisor::regs::CommonSpecialRegisters; +use crate::mem::memory_region::MemoryRegion; +#[cfg(crashdump)] +use crate::mem::memory_region::{ + CrashDumpRegion, HostGuestMemoryRegion, MemoryRegionFlags, MemoryRegionType, +}; use crate::sandbox::snapshot::{NextAction, Snapshot}; use crate::{Result, new_error}; +#[cfg(all(feature = "crashdump", feature = "init-paging"))] +fn mapping_kind_to_flags(kind: &MappingKind) -> (MemoryRegionFlags, MemoryRegionType) { + match kind { + MappingKind::Basic(BasicMapping { + readable, + writable, + executable, + }) => { + let mut flags = MemoryRegionFlags::empty(); + if *readable { + flags |= MemoryRegionFlags::READ; + } + if *writable { + flags |= MemoryRegionFlags::WRITE; + } + if *executable { + flags |= MemoryRegionFlags::EXECUTE; + } + (flags, MemoryRegionType::Snapshot) + } + MappingKind::Cow(cow) => { + let mut flags = MemoryRegionFlags::empty(); + if cow.readable { + flags |= MemoryRegionFlags::READ; + } + if cow.executable { + flags |= MemoryRegionFlags::EXECUTE; + } + (flags, MemoryRegionType::Scratch) + } + } +} + +/// Try to extend the last region in `regions` if the new page is contiguous +/// in both guest and host address space and has the same flags. +/// +/// Returns `true` if the region was coalesced, `false` if a new region is needed. +#[cfg(all(feature = "crashdump", feature = "init-paging"))] +fn try_coalesce_region( + regions: &mut [CrashDumpRegion], + virt_base: usize, + virt_end: usize, + host_base: usize, + flags: MemoryRegionFlags, +) -> bool { + if let Some(last) = regions.last_mut() + && last.guest_region.end == virt_base + && last.host_region.end == host_base + && last.flags == flags + { + last.guest_region.end = virt_end; + last.host_region.end = host_base + (virt_end - virt_base); + return true; + } + false +} + +/// Check dynamic mmap regions for a GPA that wasn't found in snapshot/scratch, +/// and push matching regions. +#[cfg(all(feature = "crashdump", feature = "init-paging"))] +fn resolve_from_mmap_regions( + regions: &mut Vec, + mapping: &hyperlight_common::vmem::Mapping, + virt_base: usize, + virt_end: usize, + mmap_regions: &[MemoryRegion], +) { + let phys_start = mapping.phys_base as usize; + let phys_end = (mapping.phys_base + mapping.len) as usize; + + for rgn in mmap_regions { + if phys_start >= rgn.guest_region.start && phys_end <= rgn.guest_region.end { + let offset = phys_start - rgn.guest_region.start; + let host_base = HostGuestMemoryRegion::to_addr(rgn.host_region.start) + offset; + let host_end = host_base + mapping.len as usize; + let flags = rgn.flags; + + if try_coalesce_region(regions, virt_base, virt_end, host_base, flags) { + continue; + } + + regions.push(CrashDumpRegion { + guest_region: virt_base..virt_end, + host_region: host_base..host_end, + flags, + region_type: rgn.region_type, + }); + } + } +} + /// A struct that is responsible for laying out and managing the memory /// for a given `Sandbox`. #[derive(Clone)] @@ -412,6 +509,132 @@ impl SandboxMemoryManager { Ok(()) } + /// Build the list of guest memory regions for a crash dump. + /// + /// With `init-paging` enabled, walks the guest page tables to discover + /// GVA→GPA mappings and translates them to host-backed regions. + #[cfg(all(feature = "crashdump", feature = "init-paging"))] + pub(crate) fn get_guest_memory_regions( + &mut self, + root_pt: u64, + mmap_regions: &[MemoryRegion], + ) -> Result> { + use crate::sandbox::snapshot::{SharedMemoryPageTableBuffer, access_gpa}; + + let scratch_size = self.scratch_mem.mem_size(); + let len = hyperlight_common::layout::MAX_GVA; + + let regions = self.shared_mem.with_exclusivity(|snapshot| { + self.scratch_mem.with_exclusivity(|scratch| { + let pt_buf = + SharedMemoryPageTableBuffer::new(snapshot, scratch, scratch_size, root_pt); + + let mappings: Vec<_> = + unsafe { hyperlight_common::vmem::virt_to_phys(&pt_buf, 0, len as u64) } + .collect(); + + if mappings.is_empty() { + return Err(new_error!( + "No page table mappings found (len {len})", + )); + } + + let mut regions: Vec = Vec::new(); + for mapping in &mappings { + let virt_base = mapping.virt_base as usize; + let virt_end = (mapping.virt_base + mapping.len) as usize; + + if let Some((mem, offset)) = + access_gpa(snapshot, scratch, scratch_size, mapping.phys_base) + { + let (flags, region_type) = mapping_kind_to_flags(&mapping.kind); + + if offset >= mem.mem_size() { + tracing::error!( + "Mapping for GPA {:#x} offset {:#x} out of bounds (size {:#x}), skipping", + mapping.phys_base, offset, mem.mem_size(), + ); + continue; + } + + let host_base = mem.base_addr() + offset; + let host_len = + (mapping.len as usize).min(mem.mem_size().saturating_sub(offset)); + + if try_coalesce_region(&mut regions, virt_base, virt_end, host_base, flags) + { + continue; + } + + regions.push(CrashDumpRegion { + guest_region: virt_base..virt_end, + host_region: host_base..host_base + host_len, + flags, + region_type, + }); + } else { + // GPA not in snapshot/scratch — check dynamic mmap regions + resolve_from_mmap_regions( + &mut regions, mapping, virt_base, virt_end, mmap_regions, + ); + } + } + + Ok(regions) + }) + })???; + + Ok(regions) + } + + /// Build the list of guest memory regions for a crash dump (non-paging). + /// + /// Without paging, GVA == GPA (identity mapped), so we return the + /// snapshot and scratch regions directly at their known addresses + /// alongside any dynamic mmap regions. + #[cfg(all(feature = "crashdump", not(feature = "init-paging")))] + pub(crate) fn get_guest_memory_regions( + &mut self, + _root_pt: u64, + mmap_regions: &[MemoryRegion], + ) -> Result> { + let snapshot_base = SandboxMemoryLayout::BASE_ADDRESS; + let snapshot_size = self.shared_mem.mem_size(); + let snapshot_host = self.shared_mem.base_addr(); + + let scratch_size = self.scratch_mem.mem_size(); + let scratch_gva = hyperlight_common::layout::scratch_base_gva(scratch_size) as usize; + let scratch_host = self.scratch_mem.base_addr(); + + let mut regions = vec![ + CrashDumpRegion { + guest_region: snapshot_base..snapshot_base + snapshot_size, + host_region: snapshot_host..snapshot_host + snapshot_size, + flags: MemoryRegionFlags::READ | MemoryRegionFlags::EXECUTE, + region_type: MemoryRegionType::Snapshot, + }, + CrashDumpRegion { + guest_region: scratch_gva..scratch_gva + scratch_size, + host_region: scratch_host..scratch_host + scratch_size, + flags: MemoryRegionFlags::READ + | MemoryRegionFlags::WRITE + | MemoryRegionFlags::EXECUTE, + region_type: MemoryRegionType::Scratch, + }, + ]; + for rgn in mmap_regions { + regions.push(CrashDumpRegion { + guest_region: rgn.guest_region.clone(), + host_region: HostGuestMemoryRegion::to_addr(rgn.host_region.start) + ..HostGuestMemoryRegion::to_addr(rgn.host_region.end), + flags: rgn.flags, + region_type: rgn.region_type, + }); + } + + Ok(regions) + } + /// Read guest memory at a Guest Virtual Address (GVA) by walking the /// page tables to translate GVA → GPA, then reading from the correct /// backing memory (shared_mem or scratch_mem). diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 4704f42e2..34a4a3313 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -767,8 +767,24 @@ impl MultiUseSandbox { /// #[cfg(crashdump)] #[instrument(err(Debug), skip_all, parent = Span::current())] - pub fn generate_crashdump(&self) -> Result<()> { - crate::hypervisor::crashdump::generate_crashdump(&self.vm) + pub fn generate_crashdump(&mut self) -> Result<()> { + crate::hypervisor::crashdump::generate_crashdump(&self.vm, &mut self.mem_mgr, None) + } + + /// Generate a crash dump of the current state of the VM, writing to `dir`. + /// + /// Like [`generate_crashdump`](Self::generate_crashdump), but the core dump + /// file is placed in `dir` instead of consulting the `HYPERLIGHT_CORE_DUMP_DIR` + /// environment variable. This avoids the need for callers to use + /// `unsafe { std::env::set_var(...) }`. + #[cfg(crashdump)] + #[instrument(err(Debug), skip_all, parent = Span::current())] + pub fn generate_crashdump_to_dir(&mut self, dir: impl Into) -> Result<()> { + crate::hypervisor::crashdump::generate_crashdump( + &self.vm, + &mut self.mem_mgr, + Some(dir.into()), + ) } /// Returns whether the sandbox is currently poisoned. diff --git a/src/hyperlight_host/src/sandbox/snapshot.rs b/src/hyperlight_host/src/sandbox/snapshot.rs index bdf823423..f9a48c871 100644 --- a/src/hyperlight_host/src/sandbox/snapshot.rs +++ b/src/hyperlight_host/src/sandbox/snapshot.rs @@ -181,9 +181,11 @@ pub(crate) fn access_gpa<'a>( gpa: u64, ) -> Option<(&'a ExclusiveSharedMemory, usize)> { let scratch_base = scratch_base_gpa(scratch_size); - if gpa >= scratch_base { + if gpa >= scratch_base && gpa < scratch_base + scratch_size as u64 { Some((scratch, (gpa - scratch_base) as usize)) - } else if gpa >= SandboxMemoryLayout::BASE_ADDRESS as u64 { + } else if gpa >= SandboxMemoryLayout::BASE_ADDRESS as u64 + && gpa < SandboxMemoryLayout::BASE_ADDRESS as u64 + snap.mem_size() as u64 + { Some((snap, gpa as usize - SandboxMemoryLayout::BASE_ADDRESS)) } else { None diff --git a/src/hyperlight_host/src/sandbox/uninitialized.rs b/src/hyperlight_host/src/sandbox/uninitialized.rs index 5545bafab..2167dca43 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized.rs @@ -44,6 +44,14 @@ pub(crate) struct SandboxRuntimeConfig { pub(crate) debug_info: Option, #[cfg(crashdump)] pub(crate) guest_core_dump: bool, + /// The original entry point address of the loaded guest binary + /// (load_addr + ELF entry offset). Used for AT_ENTRY in core dumps + /// so GDB can compute the correct load offset for PIE binaries. + /// + /// `None` until resolved from the snapshot's `NextAction::Initialise` + /// in `set_up_hypervisor_partition`. + #[cfg(crashdump)] + pub(crate) entry_point: Option, } /// A preliminary sandbox that represents allocated memory and registered host functions, @@ -196,6 +204,10 @@ impl UninitializedSandbox { debug_info, #[cfg(crashdump)] guest_core_dump, + // entry_point is set later in set_up_hypervisor_partition + // once the entrypoint is resolved from the snapshot + #[cfg(crashdump)] + entry_point: None, } }; diff --git a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs index f042c3f57..d1450cab1 100644 --- a/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs +++ b/src/hyperlight_host/src/sandbox/uninitialized_evolve.rs @@ -43,7 +43,7 @@ pub(super) fn evolve_impl_multi_use(u_sbox: UninitializedSandbox) -> Result, #[cfg_attr(target_os = "windows", allow(unused_variables))] config: &SandboxConfiguration, stack_top_gva: u64, - #[cfg(any(crashdump, gdb))] rt_cfg: &SandboxRuntimeConfig, + #[cfg(any(crashdump, gdb))] rt_cfg: SandboxRuntimeConfig, _load_info: LoadInfo, ) -> Result { // Create gdb thread if gdb is enabled and the configuration is provided @@ -119,6 +119,19 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg(feature = "mem_profile")] let trace_info = MemTraceInfo::new(_load_info.info)?; + // Store the original entry point address in the runtime config for core dumps. + // This is needed because `entrypoint` transitions from `Initialise(addr)` to + // `Call(dispatch_addr)` after guest initialisation, losing the original value + // that GDB needs to compute the PIE binary's load offset. + #[cfg(crashdump)] + let rt_cfg = { + let mut rt_cfg = rt_cfg; + if let crate::sandbox::snapshot::NextAction::Initialise(addr) = mgr.entrypoint { + rt_cfg.entry_point = Some(addr); + } + rt_cfg + }; + Ok(HyperlightVm::new( mgr.shared_mem, mgr.scratch_mem, @@ -129,7 +142,7 @@ pub(crate) fn set_up_hypervisor_partition( #[cfg(gdb)] gdb_conn, #[cfg(crashdump)] - rt_cfg.clone(), + rt_cfg, #[cfg(feature = "mem_profile")] trace_info, )