@@ -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.\n Output:\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.\n Output:\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}`.\n Output:\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.\n Output:\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:?}.\n Output:\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.\n Output:\n {gdb_output}"
587+ ) ;
588+
589+ assert ! (
590+ gdb_output. contains( "=== DONE ===" ) ,
591+ "GDB should have completed successfully.\n Output:\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.\n Output:\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