Skip to content

Commit 985ad50

Browse files
committed
Test crashdump symbol resolution for snapshot sandboxes
Signed-off-by: Ludvig Liljenberg <4257730+ludfjig@users.noreply.github.com>
1 parent d0c0980 commit 985ad50

1 file changed

Lines changed: 175 additions & 1 deletion

File tree

  • src/hyperlight_host/examples/crashdump

src/hyperlight_host/examples/crashdump/main.rs

Lines changed: 175 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ mod tests {
342342
use std::process::Command;
343343

344344
use hyperlight_host::sandbox::SandboxConfiguration;
345-
use hyperlight_host::{GuestBinary, MultiUseSandbox, UninitializedSandbox};
345+
use hyperlight_host::{GuestBinary, HostFunctions, MultiUseSandbox, UninitializedSandbox};
346346
use serial_test::serial;
347347

348348
#[cfg(not(windows))]
@@ -454,6 +454,144 @@ mod tests {
454454
elf_files.pop().unwrap()
455455
}
456456

457+
/// Snapshot an initialized sandbox, build a fresh sandbox from that
458+
/// snapshot, trigger a crash on it, and return the path to the generated
459+
/// ELF core dump. Used to check that crash dumps from snapshot-created
460+
/// sandboxes resolve symbols the same way as directly-evolved ones.
461+
fn generate_crashdump_from_snapshot(dump_dir: &Path) -> PathBuf {
462+
let guest_path =
463+
hyperlight_testing::simple_guest_as_string().expect("Cannot find simpleguest binary");
464+
let mut cfg = SandboxConfiguration::default();
465+
cfg.set_guest_core_dump(true);
466+
let u_sbox =
467+
UninitializedSandbox::new(GuestBinary::FilePath(guest_path), Some(cfg)).unwrap();
468+
let mut sbox: MultiUseSandbox = u_sbox.evolve().unwrap();
469+
470+
let snapshot = sbox.snapshot().expect("snapshot");
471+
472+
let mut cfg2 = SandboxConfiguration::default();
473+
cfg2.set_guest_core_dump(true);
474+
let mut sbox2 =
475+
MultiUseSandbox::from_snapshot(snapshot, HostFunctions::default(), Some(cfg2)).unwrap();
476+
477+
let result = sbox2.call::<()>("TriggerException", ());
478+
assert!(result.is_err(), "TriggerException should return an error");
479+
480+
sbox2
481+
.generate_crashdump_to_dir(dump_dir.to_string_lossy())
482+
.expect("generate_crashdump should succeed");
483+
484+
let mut elf_files: Vec<PathBuf> = fs::read_dir(dump_dir)
485+
.unwrap()
486+
.filter_map(|e| e.ok())
487+
.map(|e| e.path())
488+
.filter(|p| {
489+
p.file_name()
490+
.and_then(|n| n.to_str())
491+
.map_or(false, |n| n.starts_with("hl_core_") && n.ends_with(".elf"))
492+
})
493+
.collect();
494+
495+
assert!(
496+
!elf_files.is_empty(),
497+
"No core dump file (hl_core_*.elf) found in {}",
498+
dump_dir.display()
499+
);
500+
501+
elf_files.sort();
502+
elf_files.pop().unwrap()
503+
}
504+
505+
/// Load `core_path` in GDB against the guest binary and assert that
506+
/// `info symbol $pc` resolves to the guest function that raised the abort.
507+
/// Resolving to the correct symbol only works when the core's `AT_ENTRY`
508+
/// conveys the right PIE load bias. A wrong bias still resolves `$pc` to
509+
/// *some* symbol, just the wrong one, so the expected name is asserted.
510+
///
511+
/// As an independent check on the underlying mechanism, this also reads
512+
/// the auxiliary vector (`info auxv`) and asserts the `AT_ENTRY` entry is
513+
/// present and non-zero. GDB derives the PIE load bias from `AT_ENTRY`, so
514+
/// a zero (or missing) value is the exact defect a broken entry point
515+
/// produces, independent of GDB's symbol-relocation heuristics.
516+
fn assert_gdb_resolves_pc_symbol(dump_dir: &Path, core_path: &Path) {
517+
// The crash path is deterministic: TriggerException raises a CPU
518+
// exception, and the guest handler reports it via this function, so
519+
// `$pc` sits here when the dump is taken.
520+
const EXPECTED_SYMBOL: &str = "hyperlight_guest::exit::write_abort";
521+
522+
let guest_path = hyperlight_testing::simple_guest_as_string().expect("simpleguest binary");
523+
524+
let cmd_file = dump_dir.join("gdb_sym_cmds.txt");
525+
let out_file = dump_dir.join("gdb_sym_output.txt");
526+
527+
let cmds = format!(
528+
"\
529+
set pagination off
530+
set logging file {out}
531+
set logging enabled on
532+
file {binary}
533+
core-file {core}
534+
echo === SYMBOL ===\\n
535+
info symbol $pc
536+
echo === AUXV ===\\n
537+
info auxv
538+
echo === DONE ===\\n
539+
set logging enabled off
540+
quit
541+
",
542+
out = out_file.display(),
543+
binary = guest_path,
544+
core = core_path.display(),
545+
);
546+
547+
let gdb_output = run_gdb_batch(&cmd_file, &out_file, &cmds);
548+
println!("GDB symbol output:\n{gdb_output}");
549+
550+
assert!(
551+
gdb_output.contains("=== SYMBOL ==="),
552+
"GDB should have printed the SYMBOL marker.\nOutput:\n{gdb_output}"
553+
);
554+
assert!(
555+
!gdb_output.contains("No symbol matches $pc"),
556+
"GDB failed to resolve $pc to a symbol — this indicates the core's \
557+
AT_ENTRY does not convey the correct PIE load bias.\nOutput:\n{gdb_output}"
558+
);
559+
assert!(
560+
gdb_output.contains(EXPECTED_SYMBOL),
561+
"GDB resolved $pc to the wrong symbol — the core's AT_ENTRY conveys \
562+
an incorrect PIE load bias. Expected `{EXPECTED_SYMBOL}`.\nOutput:\n{gdb_output}"
563+
);
564+
565+
// Independent mechanism check: AT_ENTRY must be present and non-zero.
566+
// `info auxv` prints one entry per line, e.g.
567+
// `9 AT_ENTRY Entry point of program 0x200000da0`
568+
// The value is the last whitespace-separated token on the line.
569+
let at_entry = gdb_output
570+
.lines()
571+
.find(|l| l.contains("AT_ENTRY"))
572+
.unwrap_or_else(|| panic!("info auxv did not report AT_ENTRY.\nOutput:\n{gdb_output}"));
573+
let value_tok = at_entry
574+
.split_whitespace()
575+
.next_back()
576+
.expect("AT_ENTRY line has a value token");
577+
let value = value_tok
578+
.strip_prefix("0x")
579+
.and_then(|h| u64::from_str_radix(h, 16).ok())
580+
.unwrap_or_else(|| {
581+
panic!("could not parse AT_ENTRY value {value_tok:?}.\nOutput:\n{gdb_output}")
582+
});
583+
assert!(
584+
value != 0,
585+
"AT_ENTRY is zero — the core does not convey the guest's entry \
586+
point, so GDB cannot compute the PIE load bias.\nOutput:\n{gdb_output}"
587+
);
588+
589+
assert!(
590+
gdb_output.contains("=== DONE ==="),
591+
"GDB should have completed successfully.\nOutput:\n{gdb_output}"
592+
);
593+
}
594+
457595
/// Write GDB batch commands to `cmd_path`, run GDB, and return the
458596
/// content of the logging output file.
459597
fn run_gdb_batch(cmd_path: &Path, out_path: &Path, cmds: &str) -> String {
@@ -591,4 +729,40 @@ quit
591729
"GDB should have completed successfully.\nOutput:\n{gdb_output}"
592730
);
593731
}
732+
733+
/// Verify that GDB can resolve a guest symbol from the crash dump.
734+
///
735+
/// Symbol resolution for the PIE guest binary only works if the core's
736+
/// `AT_ENTRY` auxv entry conveys the correct load bias: GDB computes the
737+
/// bias as `AT_ENTRY - e_entry` and applies it to the binary's symbols.
738+
/// If `AT_ENTRY` is wrong (e.g. zero), GDB cannot match `$pc` to any
739+
/// symbol, so this test guards that the entry point is reported correctly.
740+
#[test]
741+
#[serial]
742+
fn test_crashdump_gdb_symbols() {
743+
if !gdb_is_available() {
744+
eprintln!("Skipping test: {GDB_COMMAND} not found on PATH");
745+
return;
746+
}
747+
748+
let dump_dir = tempfile::tempdir().expect("create temp dir");
749+
let core_path = generate_crashdump_with_content(dump_dir.path());
750+
assert_gdb_resolves_pc_symbol(dump_dir.path(), &core_path);
751+
}
752+
753+
/// Same symbol-resolution guarantee as [`test_crashdump_gdb_symbols`], but
754+
/// for a sandbox created from a snapshot. The crash dump must convey the
755+
/// same `AT_ENTRY` so symbols resolve identically.
756+
#[test]
757+
#[serial]
758+
fn test_crashdump_gdb_symbols_from_snapshot() {
759+
if !gdb_is_available() {
760+
eprintln!("Skipping test: {GDB_COMMAND} not found on PATH");
761+
return;
762+
}
763+
764+
let dump_dir = tempfile::tempdir().expect("create temp dir");
765+
let core_path = generate_crashdump_from_snapshot(dump_dir.path());
766+
assert_gdb_resolves_pc_symbol(dump_dir.path(), &core_path);
767+
}
594768
}

0 commit comments

Comments
 (0)