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