Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
194 changes: 150 additions & 44 deletions src/hyperlight_host/examples/guest-debugging/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,76 @@ mod tests {
#[cfg(windows)]
const GDB_COMMAND: &str = "gdb";

/// Construct the (out_file_path, cmd_file_path, manifest_dir)
/// triple every gdb test needs.
fn gdb_test_paths(name: &str) -> (String, String, String) {
let out_dir = std::env::var("OUT_DIR").expect("Failed to get out dir");
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.expect("Failed to get manifest dir")
.replace('\\', "/");
let out_file_path = format!("{out_dir}/{name}.output");
let cmd_file_path = format!("{out_dir}/{name}-commands.txt");
(out_file_path, cmd_file_path, manifest_dir)
}

/// Build a gdb script that connects to `port`, sets a single
/// breakpoint at `breakpoint`, prints `echo_msg` when hit, and
/// detaches before quitting.
///
/// The breakpoint commands end with `detach` + `quit` instead of
/// `continue`. The previous "inner continue, outer continue, quit"
/// shape races with the inferior exit. After the breakpoint hits
/// and the inner `continue` resumes the guest, the guest may run
/// to completion and the gdb stub may close the remote before gdb
/// has dispatched the outer `continue`, producing a non-zero exit
/// with `Remote connection closed`. Detaching from the breakpoint
/// commands removes that window. The host process keeps running
/// the guest call to completion on its own after detach.
fn single_breakpoint_script(
manifest_dir: &str,
port: u16,
out_file_path: &str,
breakpoint: &str,
echo_msg: &str,
) -> String {
let cmd = format!(
"file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
target remote :{port}

set pagination off
set logging file {out_file_path}
set logging enabled on

break {breakpoint}
commands
echo \"{echo_msg}\\n\"
backtrace

set logging enabled off
detach
quit
end

continue
"
);
#[cfg(windows)]
let cmd = format!("set osabi none\n{cmd}");
cmd
}

/// Spawn the gdb client to execute the script in `cmd_file_path`.
fn spawn_gdb_client(cmd_file_path: &str) -> std::process::Child {
Command::new(GDB_COMMAND)
.arg("-nx")
.arg("--nw")
.arg("--batch")
.arg("-x")
.arg(cmd_file_path)
.spawn()
.expect("Failed to start gdb")
}

fn write_cmds_file(cmd_file_path: &str, cmd: &str) -> io::Result<()> {
let file = File::create(cmd_file_path)?;
let mut writer = BufWriter::new(file);
Expand Down Expand Up @@ -163,14 +233,7 @@ mod tests {
// wait 3 seconds for the gdb to connect
thread::sleep(Duration::from_secs(3));

let mut gdb = Command::new(GDB_COMMAND)
.arg("-nx") // Don't load any .gdbinit files
.arg("--nw")
.arg("--batch")
.arg("-x")
.arg(cmd_file_path)
.spawn()
.map_err(|e| new_error!("Failed to start gdb process: {}", e))?;
let mut gdb = spawn_gdb_client(cmd_file_path);

// wait 3 seconds for the gdb to connect
thread::sleep(Duration::from_secs(10));
Expand Down Expand Up @@ -245,38 +308,16 @@ mod tests {
#[test]
#[serial]
fn test_gdb_end_to_end() {
let out_dir = std::env::var("OUT_DIR").expect("Failed to get out dir");
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.expect("Failed to get manifest dir")
.replace('\\', "/");
let out_file_path = format!("{out_dir}/gdb.output");
let cmd_file_path = format!("{out_dir}/gdb-commands.txt");

let cmd = format!(
"file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
target remote :8080

set pagination off
set logging file {out_file_path}
set logging enabled on

break hyperlight_main
commands
echo \"Stopped at hyperlight_main breakpoint\\n\"
backtrace

set logging enabled off
detach
quit
end

continue
"
let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb");

let cmd = single_breakpoint_script(
&manifest_dir,
8080,
&out_file_path,
"hyperlight_main",
"Stopped at hyperlight_main breakpoint",
);

#[cfg(windows)]
let cmd = format!("set osabi none\n{}", cmd);

let checker = |contents: String| contents.contains("Stopped at hyperlight_main breakpoint");

let result = run_guest_and_gdb(&cmd_file_path, &out_file_path, &cmd, checker);
Expand All @@ -288,13 +329,8 @@ mod tests {
#[test]
#[serial]
fn test_gdb_sse_check() {
let out_dir = std::env::var("OUT_DIR").expect("Failed to get out dir");
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.expect("Failed to get manifest dir")
.replace('\\', "/");
let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-sse");
println!("manifest dir {manifest_dir}");
let out_file_path = format!("{out_dir}/gdb-sse.output");
let cmd_file_path = format!("{out_dir}/gdb-sse--commands.txt");

let cmd = format!(
"file {manifest_dir}/../tests/rust_guests/bin/debug/simpleguest
Expand Down Expand Up @@ -330,4 +366,74 @@ mod tests {
cleanup(&out_file_path, &cmd_file_path);
assert!(result.is_ok(), "{}", result.unwrap_err());
}

#[test]
#[serial]
fn test_gdb_from_snapshot() {
use hyperlight_host::HostFunctions;

const PORT: u16 = 8081;

let (out_file_path, cmd_file_path, manifest_dir) = gdb_test_paths("gdb-from-snapshot");

// Build a sandbox the normal way and snapshot it in-memory.
let mut producer: MultiUseSandbox = UninitializedSandbox::new(
hyperlight_host::GuestBinary::FilePath(
hyperlight_testing::simple_guest_as_string().unwrap(),
),
None,
)
.unwrap()
.evolve()
.unwrap();
let snap = producer.snapshot().unwrap();

// Order matters. The gdb stub event loop must enter (i.e.
// `VcpuStopped` must be sent on the channel) before the gdb
// client connects, otherwise the wire protocol desyncs. The
// evolve case gets this for free because `evolve()` runs
// `vm.initialise()` which trips the entry breakpoint
// immediately. For a `Call` snapshot `vm.initialise` is a
// no-op, so we trigger the breakpoint by running `sbox.call`
// here before the client is launched below.
let snap_thread = snap.clone();
let sandbox_thread = thread::spawn(move || -> Result<()> {
let mut cfg = SandboxConfiguration::default();
cfg.set_guest_debug_info(DebugInfo { port: PORT });

let mut sbox =
MultiUseSandbox::from_snapshot(snap_thread, HostFunctions::default(), Some(cfg))?;
sbox.call::<i32>(
"PrintOutput",
"Hello from a from_snapshot sandbox\n".to_string(),
)?;
Ok(())
});

// Wait for the sandbox thread to bind the listener, install
// the one-shot breakpoint, and trip it.
thread::sleep(Duration::from_secs(3));

let cmd = single_breakpoint_script(
&manifest_dir,
PORT,
&out_file_path,
"main.rs:simpleguest::print_output",
"Stopped at print_output breakpoint",
);
write_cmds_file(&cmd_file_path, &cmd).expect("Failed to write gdb commands");

let mut gdb = spawn_gdb_client(&cmd_file_path);
let _ = gdb.wait();
let sandbox_result = sandbox_thread
.join()
.expect("from_snapshot sandbox thread panicked");

let checker = |contents: String| contents.contains("Stopped at print_output breakpoint");
let result = check_output(&out_file_path, checker);

cleanup(&out_file_path, &cmd_file_path);
Comment thread
ludfjig marked this conversation as resolved.
sandbox_result.expect("from_snapshot sandbox returned error");
result.expect("gdb output missing expected breakpoint hit");
}
}
26 changes: 23 additions & 3 deletions src/hyperlight_host/src/func/host_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ impl Registerable for UninitializedSandbox {
return_type: Output::TYPE,
};

(*hfs).register_host_function(name.to_string(), entry)
(*hfs).register_host_function(name.to_string(), entry);
Ok(())
}
}

Expand Down Expand Up @@ -92,7 +93,26 @@ impl Registerable for crate::MultiUseSandbox {
return_type: Output::TYPE,
};

(*hfs).register_host_function(name.to_string(), entry)
(*hfs).register_host_function(name.to_string(), entry);
Ok(())
}
}

impl Registerable for crate::HostFunctions {
fn register_host_function<Args: ParameterTuple, Output: SupportedReturnType>(
&mut self,
name: &str,
hf: impl Into<HostFunction<Output, Args>>,
) -> Result<()> {
let entry = FunctionEntry {
function: hf.into().into(),
parameter_types: Args::TYPE,
return_type: Output::TYPE,
};

self.inner_mut()
.register_host_function(name.to_string(), entry);
Ok(())
}
}

Expand Down Expand Up @@ -236,7 +256,7 @@ pub(crate) fn register_host_function<Args: ParameterTuple, Output: SupportedRetu
.host_funcs
.try_lock()
.map_err(|e| new_error!("Error locking at {}:{}: {}", file!(), line!(), e))?
.register_host_function(name.to_string(), entry)?;
.register_host_function(name.to_string(), entry);

Ok(())
}
9 changes: 1 addition & 8 deletions src/hyperlight_host/src/hypervisor/gdb/arch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,8 @@ pub(crate) const DR6_HW_BP_FLAGS_MASK: u64 = 0x0F << DR6_HW_BP_FLAGS_POS;
/// Determine the reason the vCPU stopped
/// This is done by checking the DR6 register and the exception id
pub(crate) fn vcpu_stop_reason(
vm: &mut dyn DebuggableVm,
vm: &dyn DebuggableVm,
dr6: u64,
entrypoint: u64,
exception: u32,
) -> std::result::Result<VcpuStopReason, VcpuStopReasonError> {
let CommonRegisters { rip, .. } = vm.regs()?;
Expand All @@ -81,10 +80,6 @@ pub(crate) fn vcpu_stop_reason(
// Check page 19-4 Vol. 3B of Intel 64 and IA-32
// Architectures Software Developer's Manual
if DR6_HW_BP_FLAGS_MASK & dr6 != 0 {
if rip == entrypoint {
vm.remove_hw_breakpoint(entrypoint)?;
return Ok(VcpuStopReason::EntryPointBp);
}
return Ok(VcpuStopReason::HwBp);
}
}
Expand All @@ -98,12 +93,10 @@ pub(crate) fn vcpu_stop_reason(
r"The vCPU exited because of an unknown reason:
rip: {:?}
dr6: {:?}
entrypoint: {:?}
exception: {:?}
",
rip,
dr6,
entrypoint,
exception,
);

Expand Down
1 change: 0 additions & 1 deletion src/hyperlight_host/src/hypervisor/gdb/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ impl run_blocking::BlockingEventLoop for GdbBlockingEventLoop {
// Resume execution if unknown reason for stop
let stop_response = match stop_reason {
VcpuStopReason::DoneStep => BaseStopReason::DoneStep,
VcpuStopReason::EntryPointBp => BaseStopReason::HwBreak(()),
VcpuStopReason::SwBp => BaseStopReason::SwBreak(()),
VcpuStopReason::HwBp => BaseStopReason::HwBreak(()),
// This is a consequence of the GDB client sending an interrupt signal
Expand Down
4 changes: 0 additions & 4 deletions src/hyperlight_host/src/hypervisor/gdb/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,6 @@ impl DebugMemoryAccess {
pub enum VcpuStopReason {
Crash,
DoneStep,
/// Hardware breakpoint inserted by the hypervisor so the guest can be stopped
/// at the entry point. This is used to avoid the guest from executing
/// the entry point code before the debugger is connected
EntryPointBp,
HwBp,
SwBp,
Interrupt,
Expand Down
31 changes: 24 additions & 7 deletions src/hyperlight_host/src/hypervisor/hyperlight_vm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,12 @@ pub(crate) struct HyperlightVm {
pub(super) gdb_conn: Option<DebugCommChannel<DebugResponse, DebugMsg>>,
#[cfg(gdb)]
pub(super) sw_breakpoints: HashMap<u64, u8>, // addr -> original instruction
/// One-shot hw breakpoint installed at the entry address when gdb is
/// enabled, so the gdb stub gets a `VcpuStopped` to enter its event
/// loop on the first vCPU run after construction. Cleared on first
/// hit by `handle_debug`.
#[cfg(gdb)]
pub(super) one_shot_entry_bp: Option<u64>,
#[cfg(feature = "mem_profile")]
pub(super) trace_info: MemTraceInfo,
#[cfg(crashdump)]
Expand Down Expand Up @@ -598,17 +604,28 @@ impl HyperlightVm {
match exit_reason {
#[cfg(gdb)]
Ok(VmExit::Debug { dr6, exception }) => {
let initialise = match self.entrypoint {
NextAction::Initialise(initialise) => initialise,
_ => 0,
};
// Handle debug event (breakpoints)
// Classify the debug exit. `vcpu_stop_reason` is a
// pure classifier and has no side effects on the VM.
let stop_reason = crate::hypervisor::gdb::arch::vcpu_stop_reason(
self.vm.as_mut(),
self.vm.as_ref(),
dr6,
initialise,
exception,
)?;
// Remove the one-shot entry breakpoint installed by
// `HyperlightVm::new` the first time it fires so it
// does not interfere with later user-installed
// breakpoints at the same address.
if matches!(stop_reason, VcpuStopReason::HwBp)
&& let Some(entry_addr) = self.one_shot_entry_bp
{
let rip = self.vm.regs().map_err(VcpuStopReasonError::GetRegs)?.rip;
if rip == entry_addr {
self.vm
.remove_hw_breakpoint(entry_addr)
.map_err(VcpuStopReasonError::RemoveHwBreakpoint)?;
self.one_shot_entry_bp = None;
}
}
if let Err(e) = self.handle_debug(dbg_mem_access_fn.clone(), stop_reason) {
break Err(e.into());
}
Expand Down
Loading
Loading