Skip to content

[WIP] Enable guest tracing #695

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
15554a8
[hyperlight_host] Restrict OutBHandler{Caller,Wrapper} to pub(crate)
syntactically Oct 19, 2024
e3f1c34
[hyperlight_host] Plumb a trace file descriptor around
syntactically Oct 19, 2024
a04e125
[hyperlight_host/exe] Allow load() to consume the exe_info
syntactically Dec 5, 2024
21dd437
[hyperlight_host/trace] Add HV interface for reading trace registers
syntactically Oct 19, 2024
d3f8b63
[hyperlight_host/trace] Support collecting guest stacktraces
syntactically Dec 5, 2024
35e044b
allow dead code for unused functions triggered by previous commit
dblnz Jul 3, 2025
6a5dc64
Add trace support for recording memory allocations and frees
syntactically Dec 6, 2024
82e08d2
Add basic utility for dumping logs and memory statistics from traces
syntactically Dec 6, 2024
ce53952
add license header to trace_dump
dblnz Jul 7, 2025
1e376fd
[hyperlight-guest-tracing] Add crates that generate guest tracing rec…
dblnz Jul 8, 2025
ce5f50e
[hyperlight_host/trace] Handle the trace records sent by a guest
dblnz Jul 8, 2025
d325ddc
[hyperlight_guest] Add traces in the guest to track the execution timing
dblnz Jul 8, 2025
b2ff420
Update sample guests to accept features that enable tracing
dblnz Jul 8, 2025
9738dba
Update utility for dumping logs to support the traces
dblnz Jul 8, 2025
9218de5
Add documentation on how to use the tracing functionality
dblnz Jul 8, 2025
8d3558e
Fix clippy warnings for unrelated work
dblnz Jul 8, 2025
66af14f
Fix clippy warning of method missing # Safety section
dblnz Jul 9, 2025
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
1,374 changes: 1,030 additions & 344 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ members = [
"src/hyperlight_guest",
"src/hyperlight_host",
"src/hyperlight_guest_capi",
"src/hyperlight_guest_tracing",
"src/hyperlight_guest_tracing_macro",
"src/hyperlight_testing",
"fuzz",
"src/hyperlight_guest_bin",
"src/hyperlight_component_util",
"src/hyperlight_component_macro",
"src/trace_dump",
]
# Guests have custom linker flags, so we need to exclude them from the workspace
exclude = [
Expand All @@ -39,6 +42,8 @@ hyperlight-host = { path = "src/hyperlight_host", version = "0.7.0", default-fea
hyperlight-guest = { path = "src/hyperlight_guest", version = "0.7.0", default-features = false }
hyperlight-guest-bin = { path = "src/hyperlight_guest_bin", version = "0.7.0", default-features = false }
hyperlight-testing = { path = "src/hyperlight_testing", default-features = false }
hyperlight-guest-tracing = { path = "src/hyperlight_guest_tracing", default-features = false }
hyperlight-guest-tracing-macro = { path = "src/hyperlight_guest_tracing_macro", default-features = false }
hyperlight-component-util = { path = "src/hyperlight_component_util", version = "0.7.0", default-features = false }
hyperlight-component-macro = { path = "src/hyperlight_component_macro", version = "0.7.0", default-features = false }

Expand Down
10 changes: 5 additions & 5 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ witguest-wit:
cargo install --locked wasm-tools
cd src/tests/rust_guests/witguest && wasm-tools component wit guest.wit -w -o interface.wasm

build-rust-guests target=default-target: (witguest-wit)
cd src/tests/rust_guests/callbackguest && cargo build --profile={{ if target == "debug" { "dev" } else { target } }}
cd src/tests/rust_guests/simpleguest && cargo build --profile={{ if target == "debug" { "dev" } else { target } }}
cd src/tests/rust_guests/dummyguest && cargo build --profile={{ if target == "debug" { "dev" } else { target } }}
cd src/tests/rust_guests/witguest && cargo build --profile={{ if target == "debug" { "dev" } else { target } }}
build-rust-guests target=default-target features="": (witguest-wit)
cd src/tests/rust_guests/callbackguest && cargo build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }}
cd src/tests/rust_guests/simpleguest && cargo build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }}
cd src/tests/rust_guests/dummyguest && cargo build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }}
cd src/tests/rust_guests/witguest && cargo build {{ if features =="" {''} else if features=="no-default-features" {"--no-default-features" } else {"--no-default-features -F " + features } }} --profile={{ if target == "debug" { "dev" } else { target } }}

@move-rust-guests target=default-target:
cp {{ callbackguest_source }}/{{ target }}/callbackguest* {{ rust_guests_bin_dir }}/{{ target }}/
Expand Down
69 changes: 69 additions & 0 deletions docs/hyperlight-metrics-logs-and-traces.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,72 @@ NOTE: when running this on windows that this is a linux container, so you will n
```

Once the container or the exe is running, the trace output can be viewed in the jaeger UI at [http://localhost:16686/search](http://localhost:16686/search).

## Guest Tracing, Unwinding, and Memory Profiling

Hyperlight provides advanced observability features for guest code running inside micro virtual machines. You can enable guest-side tracing, stack unwinding, and memory profiling using the `trace_guest`, `unwind_guest`, and `mem_profile` features. This section explains how to build, run, and inspect guest traces.

### Building a Guest with Tracing Support
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section is focused on how to use the development tools to test from a hyperlight developers perspective but it would be nice to think about it from a user of hyper light. For instance, if I am writing my own guest what do I need to do? I can of course look at these samples but having some docs about it would be useful here.


To build a guest with tracing enabled, use the following commands:

```bash
just build-rust-guests debug trace_guest
just move-rust-guests debug
```

This will build the guest binaries with the `trace_guest` feature enabled and move them to the appropriate location for use by the host.

### Running a Hyperlight Example with Guest Tracing

Once the guest is built, you can run a Hyperlight example with guest tracing enabled. For example:

```bash
cargo run --example hello-world --features trace_guest
```

This will execute the `hello-world` example, loading the guest with tracing enabled. During execution, trace data will be collected and written to a file in the `trace` directory.

### Inspecting Guest Trace Files

To inspect the trace file generated by the guest, use the `trace_dump` crate. You will need the path to the guest symbols and the trace file. Run the following command:

```bash
cargo run -p trace_dump <path_to_guest_symbols> <trace_file_path> list_frames
```

Replace `<path_to_guest_symbols>` with the path to the guest binary or symbol file, and `<trace_file_path>` with the path to the trace file in the `trace` directory.

This command will list the stack frames and tracing information captured during guest execution, allowing you to analyze guest behavior, stack traces, and memory usage.

#### Example

```bash
cargo run -p trace_dump ./src/tests/rust_guests/bin/debug/simpleguest ./trace/<UUID>.trace list_frames
```

You can use additional features such as `unwind_guest` and `mem_profile` by enabling them during the build and run steps. Refer to the guest and host documentation for more details on these features.

> **Note:** Make sure to follow the build and run steps in order, and ensure that the guest binaries are up to date before running the host example.

## System Prerequisites for `trace_dump`

To build and use the `trace_dump` crate and related guest tracing features, you must have the following system libraries and development tools installed on your system:

- **glib-2.0** development files
- Fedora/RHEL/CentOS:
```bash
sudo dnf install glib2-devel pkgconf-pkg-config
```
- **cairo** and **cairo-gobject** development files
- Fedora/RHEL/CentOS:
```bash
sudo dnf install cairo-devel cairo-gobject-devel
```
- **pango** development files
- Fedora/RHEL/CentOS:
```bash
sudo dnf install pango-devel
```

These libraries are required by Rust crates such as `glib-sys`, `cairo-sys-rs`, and `pango-sys`, which are dependencies of the tracing and visualization tools. If you encounter errors about missing `.pc` files (e.g., `glib-2.0.pc`, `cairo.pc`, `pango.pc`), ensure the corresponding `-devel` packages are installed.
5 changes: 4 additions & 1 deletion src/hyperlight_common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ spin = "0.10.0"
[features]
default = ["tracing"]
fuzzing = ["dep:arbitrary"]
trace_guest = []
unwind_guest = []
mem_profile = []
Comment on lines +28 to +30
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reasoning for three separate features? From a usability standpoint it seems like a lot of things to keep track of.

From a maintainer standpoint it also feels like a large additional matrix of things to test

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think part of the confusion for me is that these seem like three related (as in use some base functionality) but different features but are wrapped up into a single PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jsturtevant They kind of are, since Doru built this on top of my old memory profiling PR from way back when. The idea with that was that the base tracing feature that lets you generate a file of fine-grained events quickly is useful for other things (and indeed it is being used it here for the performance events), while the full memory profiling build is extraordinarily slow, since it generates a trace event on every allocation/deallocation. Carrying around the registers to unwind the guest stack also seemed like something that would be useful in other contexts but a different amount of overhead & not always needed, e.g. the performance events here don't I believe use it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense that you may want to turn those on/off due to overhead. I think it would make for an easier review if these were split out, but I am fine with it if others are.

Copy link
Member

@syntactically syntactically Jul 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend reviewing commit-by-commit; I put a lot of effort into making sure that each commit itself is small/atomic/reviewable. That means that we can have a PR like this one where we only merge things after they are useful (& consequently we know that the design is usable), but reviewers can understand and review each step along that path in isolation.

std = []

[dev-dependencies]
hyperlight-testing = { workspace = true }

[lib]
bench = false # see https://bheisler.github.io/criterion.rs/book/faq.html#cargo-bench-gives-unrecognized-option-errors-for-valid-command-line-options
doctest = false # reduce noise in test output
doctest = false # reduce noise in test output
20 changes: 20 additions & 0 deletions src/hyperlight_common/src/outb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,23 @@ impl TryFrom<u8> for Exception {
/// - CallFunction: makes a call to a host function,
/// - Abort: aborts the execution of the guest,
/// - DebugPrint: prints a message to the host
/// - TraceRecordStack: records the stack trace of the guest
/// - TraceMemoryAlloc: records memory allocation events
/// - TraceMemoryFree: records memory deallocation events
/// - TraceRecord: records a trace event in the guest
pub enum OutBAction {
Log = 99,
CallFunction = 101,
Abort = 102,
DebugPrint = 103,
#[cfg(feature = "unwind_guest")]
TraceRecordStack = 104,
#[cfg(feature = "mem_profile")]
TraceMemoryAlloc = 105,
#[cfg(feature = "mem_profile")]
TraceMemoryFree = 106,
#[cfg(feature = "trace_guest")]
TraceRecord = 107,
}

impl TryFrom<u16> for OutBAction {
Expand All @@ -105,6 +117,14 @@ impl TryFrom<u16> for OutBAction {
101 => Ok(OutBAction::CallFunction),
102 => Ok(OutBAction::Abort),
103 => Ok(OutBAction::DebugPrint),
#[cfg(feature = "unwind_guest")]
104 => Ok(OutBAction::TraceRecordStack),
#[cfg(feature = "mem_profile")]
105 => Ok(OutBAction::TraceMemoryAlloc),
#[cfg(feature = "mem_profile")]
106 => Ok(OutBAction::TraceMemoryFree),
#[cfg(feature = "trace_guest")]
107 => Ok(OutBAction::TraceRecord),
_ => Err(anyhow::anyhow!("Invalid OutBAction value: {}", val)),
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/hyperlight_guest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,9 @@ Provides only the essential building blocks for interacting with the host enviro
anyhow = { version = "1.0.98", default-features = false }
serde_json = { version = "1.0", default-features = false, features = ["alloc"] }
hyperlight-common = { workspace = true }
hyperlight-guest-tracing = { workspace = true, optional = true }
hyperlight-guest-tracing-macro = { workspace = true}

[features]
default = []
trace_guest = ["dep:hyperlight-guest-tracing"]
10 changes: 10 additions & 0 deletions src/hyperlight_guest/src/exit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ use hyperlight_common::outb::OutBAction;

/// Halt the execution of the guest and returns control to the host.
#[inline(never)]
#[hyperlight_guest_tracing_macro::trace_function]
pub fn halt() {
// Ensure all tracing data is flushed before halting
hyperlight_guest_tracing_macro::flush!();
unsafe { asm!("hlt", options(nostack)) }
}

/// Exits the VM with an Abort OUT action and code 0.
#[unsafe(no_mangle)]
#[hyperlight_guest_tracing_macro::trace_function]
pub extern "C" fn abort() -> ! {
abort_with_code(&[0, 0xFF])
}

/// Exits the VM with an Abort OUT action and a specific code.
#[hyperlight_guest_tracing_macro::trace_function]
pub fn abort_with_code(code: &[u8]) -> ! {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you need to flush on an abort too?

outb(OutBAction::Abort as u16, code);
outb(OutBAction::Abort as u16, &[0xFF]); // send abort terminator (if not included in code)
Expand All @@ -42,6 +47,7 @@ pub fn abort_with_code(code: &[u8]) -> ! {
///
/// # Safety
/// This function is unsafe because it dereferences a raw pointer.
#[hyperlight_guest_tracing_macro::trace_function]
pub unsafe fn abort_with_code_and_message(code: &[u8], message_ptr: *const c_char) -> ! {
unsafe {
// Step 1: Send abort code (typically 1 byte, but `code` allows flexibility)
Expand All @@ -62,7 +68,10 @@ pub unsafe fn abort_with_code_and_message(code: &[u8], message_ptr: *const c_cha
}

/// OUT bytes to the host through multiple exits.
#[hyperlight_guest_tracing_macro::trace_function]
pub(crate) fn outb(port: u16, data: &[u8]) {
// Ensure all tracing data is flushed before sending OUT bytes
hyperlight_guest_tracing_macro::flush!();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the abort section this means this gets called twice, is that expected?

unsafe {
let mut i = 0;
while i < data.len() {
Expand All @@ -79,6 +88,7 @@ pub(crate) fn outb(port: u16, data: &[u8]) {
}

/// OUT function for sending a 32-bit value to the host.
#[hyperlight_guest_tracing_macro::trace_function]
pub(crate) unsafe fn out32(port: u16, val: u32) {
unsafe {
asm!("out dx, eax", in("dx") port, in("eax") val, options(preserves_flags, nomem, nostack));
Expand Down
7 changes: 7 additions & 0 deletions src/hyperlight_guest/src/guest_handle/host_comm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ use crate::exit::out32;

impl GuestHandle {
/// Get user memory region as bytes.
#[hyperlight_guest_tracing_macro::trace_function]
pub fn read_n_bytes_from_user_memory(&self, num: u64) -> Result<Vec<u8>> {
let peb_ptr = self.peb().unwrap();
let user_memory_region_ptr = unsafe { (*peb_ptr).init_data.ptr as *mut u8 };
Expand Down Expand Up @@ -63,6 +64,7 @@ impl GuestHandle {
///
/// When calling `call_host_function<T>`, this function is called
/// internally to get the return value.
#[hyperlight_guest_tracing_macro::trace_function]
pub fn get_host_return_value<T: TryFrom<ReturnValue>>(&self) -> Result<T> {
let return_value = self
.try_pop_shared_input_data_into::<ReturnValue>()
Expand All @@ -83,6 +85,7 @@ impl GuestHandle {
///
/// Note: The function return value must be obtained by calling
/// `get_host_return_value`.
#[hyperlight_guest_tracing_macro::trace_function]
pub fn call_host_function_without_returning_result(
&self,
function_name: &str,
Expand Down Expand Up @@ -114,6 +117,7 @@ impl GuestHandle {
/// sends it to the host, and then retrieves the return value.
///
/// The return value is deserialized into the specified type `T`.
#[hyperlight_guest_tracing_macro::trace_function]
pub fn call_host_function<T: TryFrom<ReturnValue>>(
&self,
function_name: &str,
Expand All @@ -124,6 +128,7 @@ impl GuestHandle {
self.get_host_return_value::<T>()
}

#[hyperlight_guest_tracing_macro::trace_function]
pub fn get_host_function_details(&self) -> HostFunctionDetails {
let peb_ptr = self.peb().unwrap();
let host_function_details_buffer =
Expand All @@ -140,6 +145,7 @@ impl GuestHandle {
}

/// Write an error to the shared output data buffer.
#[hyperlight_guest_tracing_macro::trace_function]
pub fn write_error(&self, error_code: ErrorCode, message: Option<&str>) {
let guest_error: GuestError = GuestError::new(
error_code.clone(),
Expand All @@ -155,6 +161,7 @@ impl GuestHandle {
}

/// Log a message with the specified log level, source, caller, source file, and line number.
#[hyperlight_guest_tracing_macro::trace_function]
pub fn log_message(
&self,
log_level: LogLevel,
Expand Down
6 changes: 6 additions & 0 deletions src/hyperlight_guest/src/guest_handle/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ use crate::error::{HyperlightGuestError, Result};

impl GuestHandle {
/// Pops the top element from the shared input data buffer and returns it as a T
#[hyperlight_guest_tracing_macro::trace_function]
pub fn try_pop_shared_input_data_into<T>(&self) -> Result<T>
where
T: for<'a> TryFrom<&'a [u8]>,
Expand Down Expand Up @@ -68,6 +69,7 @@ impl GuestHandle {
let buffer = &idb[last_element_offset_rel as usize..];

// convert the buffer to T
hyperlight_guest_tracing_macro::trace!("Start converting buffer");
let type_t = match T::try_from(buffer) {
Ok(t) => Ok(t),
Err(_e) => {
Expand All @@ -77,6 +79,7 @@ impl GuestHandle {
));
}
};
hyperlight_guest_tracing_macro::trace!("Finish converting buffer");

// update the stack pointer to point to the element we just popped of since that is now free
idb[..8].copy_from_slice(&last_element_offset_rel.to_le_bytes());
Expand All @@ -88,6 +91,7 @@ impl GuestHandle {
}

/// Pushes the given data onto the shared output data buffer.
#[hyperlight_guest_tracing_macro::trace_function]
pub fn push_shared_output_data(&self, data: Vec<u8>) -> Result<()> {
let peb_ptr = self.peb().unwrap();
let output_stack_size = unsafe { (*peb_ptr).output_stack.size as usize };
Expand Down Expand Up @@ -133,7 +137,9 @@ impl GuestHandle {
}

// write the actual data
hyperlight_guest_tracing_macro::trace!("Start copy of data");
odb[stack_ptr_rel as usize..stack_ptr_rel as usize + data.len()].copy_from_slice(&data);
hyperlight_guest_tracing_macro::trace!("Finish copy of data");

// write the offset to the newly written data, to the top of the stack
let bytes: [u8; 8] = stack_ptr_rel.to_le_bytes();
Expand Down
4 changes: 4 additions & 0 deletions src/hyperlight_guest_bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@ and third-party code used by our C-API needed to build a native hyperlight-guest
default = ["libc", "printf"]
libc = [] # compile musl libc
printf = [] # compile printf
trace_guest = ["hyperlight-common/trace_guest", "dep:hyperlight-guest-tracing", "hyperlight-guest/trace_guest"]
mem_profile = ["hyperlight-common/unwind_guest","hyperlight-common/mem_profile"]

[dependencies]
hyperlight-guest = { workspace = true, default-features = false }
hyperlight-common = { workspace = true, default-features = false }
hyperlight-guest-tracing = { workspace = true, optional = true }
hyperlight-guest-tracing-macro = { workspace = true }
buddy_system_allocator = "0.11.0"
log = { version = "0.4", default-features = false }
spin = "0.10.0"
Expand Down
1 change: 1 addition & 0 deletions src/hyperlight_guest_bin/src/exceptions/gdt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ struct GdtPointer {
}

/// Load the GDT
#[hyperlight_guest_tracing_macro::trace_function]
pub unsafe fn load_gdt() {
unsafe {
let gdt_ptr = GdtPointer {
Expand Down
1 change: 1 addition & 0 deletions src/hyperlight_guest_bin/src/exceptions/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type handler_t = fn(n: u64, info: *mut ExceptionInfo, ctx: *mut Context, pf_addr

/// Exception handler
#[unsafe(no_mangle)]
#[hyperlight_guest_tracing_macro::trace_function]
pub extern "C" fn hl_exception_handler(
stack_pointer: u64,
exception_number: u64,
Expand Down
1 change: 1 addition & 0 deletions src/hyperlight_guest_bin/src/exceptions/idt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ impl IdtEntry {
// Architectures Software Developer's Manual).
pub(crate) static mut IDT: [IdtEntry; 256] = unsafe { core::mem::zeroed() };

#[hyperlight_guest_tracing_macro::trace_function]
pub(crate) fn init_idt() {
set_idt_entry(Exception::DivideByZero as usize, _do_excp0); // Divide by zero
set_idt_entry(Exception::Debug as usize, _do_excp1); // Debug
Expand Down
1 change: 1 addition & 0 deletions src/hyperlight_guest_bin/src/exceptions/idtr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ impl Idtr {
}
}

#[hyperlight_guest_tracing_macro::trace_function]
pub(crate) unsafe fn load_idt() {
unsafe {
init_idt();
Expand Down
Loading
Loading