#![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<()> {
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");
println!("--- Part 1: Automatic crash dump (memory access violation) ---\n");
guest_crash_auto_dump(&guest_path)?;
println!("\n--- Part 2: On-demand crash dump (guest-caught exception) ---\n");
guest_crash_with_on_demand_dump(&guest_path)?;
println!("\n--- Part 3: Guest crash with crash dump disabled per sandbox ---\n");
guest_crash_with_dump_disabled(&guest_path)?;
println!("\n--- Part 4: On-demand crash dump API ---");
print_on_demand_info();
println!("\n=== Done ===");
Ok(())
}
#[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()?;
let mapping_file = create_mapping_file();
let guest_base: u64 = 0x200000000;
let len = sandbox.map_file_cow(mapping_file.as_path(), guest_base, None)?;
println!("Mapped {len} bytes at guest address {guest_base:#x} (read-only).");
println!("Calling guest function 'WriteMappedBuffer' on read-only region...");
let result = sandbox.call::<bool>("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(())
}
#[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(())
}
#[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
}
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()?;
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(())
}
#[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, None)?;
println!("Calling guest function 'WriteMappedBuffer' on read-only region...");
let result = sandbox.call::<bool>("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(())
}
#[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(())
}
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."
);
}
#[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";
const MAP_GUEST_BASE: u64 = 0x200000000;
const TEST_SENTINEL: &[u8] = b"HYPERLIGHT_CRASHDUMP_TEST";
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())
}
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
}
fn generate_crashdump_with_content(dump_dir: &Path) -> PathBuf {
let data_file = create_test_data_file(dump_dir);
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();
let len = sbox
.map_file_cow(&data_file, MAP_GUEST_BASE, None)
.expect("map_file_cow");
let result: Vec<u8> = 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())]
);
let result = sbox.call::<()>("TriggerException", ());
assert!(result.is_err(), "TriggerException should return an error");
sbox.generate_crashdump_to_dir(dump_dir.to_string_lossy())
.expect("generate_crashdump should succeed");
let mut elf_files: Vec<PathBuf> = 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()
);
elf_files.sort();
elf_files.pop().unwrap()
}
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") .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}");
})
}
#[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}"
);
}
#[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}"
);
}
}