#[cfg(all(crashdump, target_os = "linux"))]
use std::io::Write;
use std::path::Path;
#[cfg(all(crashdump, target_os = "linux"))]
use hyperlight_host::HyperlightError;
use hyperlight_host::SandboxBuilder;
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_pathbuf();
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: &Path) -> hyperlight_host::Result<()> {
let mut sandbox = SandboxBuilder::from_file(guest_path).build()?;
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).");
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: &Path) -> 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: &Path) -> hyperlight_host::Result<()> {
let mut sandbox = SandboxBuilder::from_file(guest_path).build()?;
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: &Path) -> hyperlight_host::Result<()> {
println!("Core dump disabled for this sandbox.");
let mut sandbox = SandboxBuilder::from_file(guest_path)
.guest_core_dump(false)
.build()?;
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::<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: &Path) -> 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::SandboxBuilder;
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_pathbuf();
let mut sbox = SandboxBuilder::from_file(guest_path).build().unwrap();
let len = sbox
.map_file_cow(&data_file, MAP_GUEST_BASE)
.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)
.expect("generate_crashdump should succeed");
let 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_eq!(
elf_files.len(),
1,
"Expected exactly one core dump file (hl_core_*.elf) in {}, found {}",
dump_dir.display(),
elf_files.len()
);
elf_files.into_iter().next().unwrap()
}
fn generate_crashdump_from_snapshot(dump_dir: &Path) -> PathBuf {
let guest_path = hyperlight_testing::simple_guest_as_pathbuf();
let mut sbox = SandboxBuilder::from_file(guest_path)
.guest_core_dump(true)
.build()
.unwrap();
let snapshot = sbox.snapshot().expect("snapshot");
let mut sbox2 = SandboxBuilder::from_snapshot(snapshot)
.guest_core_dump(true)
.build()
.unwrap();
let result = sbox2.call::<()>("TriggerException", ());
assert!(result.is_err(), "TriggerException should return an error");
sbox2
.generate_crashdump_to_dir(dump_dir)
.expect("generate_crashdump should succeed");
let 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_eq!(
elf_files.len(),
1,
"Expected exactly one core dump file (hl_core_*.elf) in {}, found {}",
dump_dir.display(),
elf_files.len()
);
elf_files.into_iter().next().unwrap()
}
fn assert_gdb_resolves_pc_symbol(dump_dir: &Path, core_path: &Path) {
const EXPECTED_SYMBOL: &str = "hyperlight_guest::exit::";
let guest_path = hyperlight_testing::simple_guest_as_pathbuf();
let cmd_file = dump_dir.join("gdb_sym_cmds.txt");
let out_file = dump_dir.join("gdb_sym_output.txt");
let cmds = format!(
"\
set pagination off
set logging file {out}
set logging enabled on
file {binary}
core-file {core}
echo === SYMBOL ===\\n
info symbol $pc
echo === AUXV ===\\n
info auxv
echo === DONE ===\\n
set logging enabled off
quit
",
out = out_file.display(),
binary = guest_path.display(),
core = core_path.display(),
);
let gdb_output = run_gdb_batch(&cmd_file, &out_file, &cmds);
println!("GDB symbol output:\n{gdb_output}");
assert!(
gdb_output.contains("=== SYMBOL ==="),
"GDB should have printed the SYMBOL marker.\nOutput:\n{gdb_output}"
);
assert!(
!gdb_output.contains("No symbol matches $pc"),
"GDB failed to resolve $pc to a symbol — this indicates the core's \
AT_ENTRY does not convey the correct PIE load bias.\nOutput:\n{gdb_output}"
);
assert!(
gdb_output.contains(EXPECTED_SYMBOL),
"GDB resolved $pc to the wrong symbol — the core's AT_ENTRY conveys \
an incorrect PIE load bias. Expected `{EXPECTED_SYMBOL}`.\nOutput:\n{gdb_output}"
);
let at_entry = gdb_output
.lines()
.find(|l| l.contains("AT_ENTRY"))
.unwrap_or_else(|| panic!("info auxv did not report AT_ENTRY.\nOutput:\n{gdb_output}"));
let value_tok = at_entry
.split_whitespace()
.next_back()
.expect("AT_ENTRY line has a value token");
let value = value_tok
.strip_prefix("0x")
.and_then(|h| u64::from_str_radix(h, 16).ok())
.unwrap_or_else(|| {
panic!("could not parse AT_ENTRY value {value_tok:?}.\nOutput:\n{gdb_output}")
});
assert!(
value != 0,
"AT_ENTRY is zero — the core does not convey the guest's entry \
point, so GDB cannot compute the PIE load bias.\nOutput:\n{gdb_output}"
);
assert!(
gdb_output.contains("=== DONE ==="),
"GDB should have completed successfully.\nOutput:\n{gdb_output}"
);
}
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_pathbuf();
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.display(),
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_pathbuf();
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.display(),
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}"
);
}
#[test]
#[serial]
fn test_crashdump_gdb_symbols() {
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());
assert_gdb_resolves_pc_symbol(dump_dir.path(), &core_path);
}
#[test]
#[serial]
fn test_crashdump_gdb_symbols_from_snapshot() {
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_from_snapshot(dump_dir.path());
assert_gdb_resolves_pc_symbol(dump_dir.path(), &core_path);
}
}