diff --git a/src/hyperlight_common/src/arch/amd64/vmem.rs b/src/hyperlight_common/src/arch/amd64/vmem.rs index 1c64e8ca9..ce6d22577 100644 --- a/src/hyperlight_common/src/arch/amd64/vmem.rs +++ b/src/hyperlight_common/src/arch/amd64/vmem.rs @@ -539,7 +539,10 @@ pub unsafe fn virt_to_phys<'a, Op: TableReadOps + 'a>( ) -> impl Iterator + 'a { // Undo sign-extension, and mask off any sub-page bits let vmin = (address & ((1u64 << VA_BITS) - 1)) & !(PAGE_SIZE as u64 - 1); - let vmax = core::cmp::min(vmin + len, 1u64 << VA_BITS); + let addr_canonical = address & ((1u64 << VA_BITS) - 1); + // Calculate the maximum virtual address we need to look at based on the starting + // address and length ensuring we don't go past the end of the address space + let vmax = core::cmp::min(addr_canonical + len, 1u64 << VA_BITS); modify_ptes::<47, 39, Op, _>(MapRequest { table_base: op.as_ref().root_table(), vmin, @@ -920,6 +923,52 @@ mod tests { assert_eq!(mapping.phys_base, 0x1000); } + #[test] + fn test_virt_to_phys_unaligned_virt_and_across_pages_len() { + let ops = MockTableOps::new(); + let mapping = Mapping { + phys_base: 0x1000, + virt_base: 0x1000, + len: 2 * PAGE_SIZE as u64, // 2 page + kind: MappingKind::Basic(BasicMapping { + readable: true, + writable: true, + executable: false, + }), + }; + + unsafe { map(&ops, mapping) }; + + let mappings = unsafe { virt_to_phys(&ops, 0x1F00, 0x300).collect::>() }; + assert_eq!(mappings.len(), 2, "Should return 2 mappings for 2 pages"); + assert_eq!(mappings[0].phys_base, 0x1000); + assert_eq!(mappings[1].phys_base, 0x2000); + } + + #[test] + fn test_virt_to_phys_unaligned_virt_and_multiple_page_len() { + let ops = MockTableOps::new(); + let mapping = Mapping { + phys_base: 0x1000, + virt_base: 0x1000, + len: PAGE_SIZE as u64 * 2 + 0x200, // 2 page + 512 bytes + kind: MappingKind::Basic(BasicMapping { + readable: true, + writable: true, + executable: false, + }), + }; + + unsafe { map(&ops, mapping) }; + + let mappings = + unsafe { virt_to_phys(&ops, 0x1234, PAGE_SIZE as u64 * 2 + 0x10).collect::>() }; + assert_eq!(mappings.len(), 3, "Should return 3 mappings for 3 pages"); + assert_eq!(mappings[0].phys_base, 0x1000); + assert_eq!(mappings[1].phys_base, 0x2000); + assert_eq!(mappings[2].phys_base, 0x3000); + } + #[test] fn test_virt_to_phys_perms() { let test = |kind| { diff --git a/src/hyperlight_common/src/flatbuffer_wrappers/function_call.rs b/src/hyperlight_common/src/flatbuffer_wrappers/function_call.rs index 056ced8e0..e5f67134f 100644 --- a/src/hyperlight_common/src/flatbuffer_wrappers/function_call.rs +++ b/src/hyperlight_common/src/flatbuffer_wrappers/function_call.rs @@ -41,7 +41,7 @@ pub enum FunctionCallType { } /// `Functioncall` represents a call to a function in the guest or host. -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct FunctionCall { /// The function name pub function_name: String, diff --git a/src/hyperlight_common/src/flatbuffer_wrappers/guest_trace_data.rs b/src/hyperlight_common/src/flatbuffer_wrappers/guest_trace_data.rs index b2006bf77..f29bdecc1 100644 --- a/src/hyperlight_common/src/flatbuffer_wrappers/guest_trace_data.rs +++ b/src/hyperlight_common/src/flatbuffer_wrappers/guest_trace_data.rs @@ -40,6 +40,15 @@ use crate::flatbuffers::hyperlight::generated::{ OpenSpanTypeArgs as FbOpenSpanTypeArgs, }; +/// TODO: Change these constant to be configurable at runtime by the guest +/// Maybe use a weak symbol that the guest can override at link time? +/// +/// Pre-calculated capacity for the encoder buffer +/// This is to avoid reallocations in the guest +/// If the next event would exceed this size, the encoder will flush the current buffer to the host +/// before encoding the new event. +pub const MAX_TRACE_DATA_SIZE: usize = 4096; + /// Key-Value pair structure used in tracing spans/events #[derive(Debug, Clone, PartialEq, Eq)] pub struct EventKeyValue { diff --git a/src/hyperlight_common/src/vmem.rs b/src/hyperlight_common/src/vmem.rs index c206e50f1..be67658a3 100644 --- a/src/hyperlight_common/src/vmem.rs +++ b/src/hyperlight_common/src/vmem.rs @@ -222,8 +222,8 @@ pub struct Mapping { /// are being remapped, TLB invalidation may need to be performed /// afterwards. pub use arch::map; -/// This function is not presently used for anything, but is useful -/// for debugging +/// This function is presently used for reading the tracing data, also +/// it is useful for debugging /// /// # Safety /// This function traverses page table data structures, and should not diff --git a/src/hyperlight_guest/src/exit.rs b/src/hyperlight_guest/src/exit.rs index e8d58e24e..1cc68a7dd 100644 --- a/src/hyperlight_guest/src/exit.rs +++ b/src/hyperlight_guest/src/exit.rs @@ -103,9 +103,9 @@ pub(crate) unsafe fn out32(port: u16, val: u32) { asm!("out dx, eax", in("dx") port, in("eax") val, - in("r8") OutBAction::TraceBatch as u64, - in("r9") ptr, - in("r10") len, + in("r12") OutBAction::TraceBatch as u64, + in("r13") ptr, + in("r14") len, options(preserves_flags, nomem, nostack) ) }; diff --git a/src/hyperlight_guest_bin/src/guest_function/call.rs b/src/hyperlight_guest_bin/src/guest_function/call.rs index 3191a108a..f3138a229 100644 --- a/src/hyperlight_guest_bin/src/guest_function/call.rs +++ b/src/hyperlight_guest_bin/src/guest_function/call.rs @@ -75,23 +75,25 @@ pub(crate) fn call_guest_function(function_call: FunctionCall) -> Result pub(crate) fn internal_dispatch_function() { // Read the current TSC to report it to the host with the spans/events // This helps calculating the timestamps relative to the guest call - #[cfg(feature = "trace_guest")] - { + #[cfg(all(feature = "trace_guest", target_arch = "x86_64"))] + let _entered = { let guest_start_tsc = hyperlight_guest_tracing::invariant_tsc::read_tsc(); // Reset the trace state for the new guest function call with the new start TSC // This clears any existing spans/events from previous calls ensuring a clean state hyperlight_guest_tracing::new_call(guest_start_tsc); - } - let handle = unsafe { GUEST_HANDLE }; + tracing::span!(tracing::Level::INFO, "internal_dispatch_function").entered() + }; - #[cfg(debug_assertions)] - log::trace!("internal_dispatch_function"); + let handle = unsafe { GUEST_HANDLE }; let function_call = handle .try_pop_shared_input_data_into::() .expect("Function call deserialization failed"); + #[cfg(debug_assertions)] + tracing::trace!("{:?}", function_call); + let res = call_guest_function(function_call); match res { @@ -111,8 +113,20 @@ pub(crate) fn internal_dispatch_function() { } } - // Ensure that any tracing output during the call is flushed to - // the host, if necessary. + // All this tracing logic shall be done right before the call to `hlt` which is done after this + // function returns #[cfg(all(feature = "trace_guest", target_arch = "x86_64"))] - hyperlight_guest_tracing::flush(); + { + // This span captures the internal dispatch function only, without tracing internals. + // Close the span before flushing to ensure that the `flush` call is not included in the span + // NOTE: This is necessary to avoid closing the span twice. Flush closes all the open + // spans, when preparing to close a guest function call context. + // It is not mandatory, though, but avoids a warning on the host that alerts a spans + // that has not been opened but is being closed. + _entered.exit(); + + // Ensure that any tracing output during the call is flushed to + // the host, if necessary. + hyperlight_guest_tracing::flush(); + } } diff --git a/src/hyperlight_guest_bin/src/lib.rs b/src/hyperlight_guest_bin/src/lib.rs index c026cbe80..5aee17f5c 100644 --- a/src/hyperlight_guest_bin/src/lib.rs +++ b/src/hyperlight_guest_bin/src/lib.rs @@ -226,6 +226,12 @@ pub(crate) extern "C" fn generic_init( hyperlight_guest_tracing::init_guest_tracing(guest_start_tsc); } + // Open a span to partly capture the initialization of the guest. + // This is done here because the tracing subscriber is initialized and the guest is in a + // well-known state + #[cfg(all(feature = "trace_guest", target_arch = "x86_64"))] + let _entered = tracing::span!(tracing::Level::INFO, "generic_init").entered(); + #[cfg(feature = "macros")] for registration in __private::GUEST_FUNCTION_INIT { registration(); @@ -235,10 +241,20 @@ pub(crate) extern "C" fn generic_init( hyperlight_main(); } - // Ensure that any tracing output from the initialisation phase is - // flushed to the host, if necessary. + // All this tracing logic shall be done right before the call to `hlt` which is done after this + // function returns #[cfg(all(feature = "trace_guest", target_arch = "x86_64"))] - hyperlight_guest_tracing::flush(); + { + // NOTE: This is necessary to avoid closing the span twice. Flush closes all the open + // spans, when preparing to close a guest function call context. + // It is not mandatory, though, but avoids a warning on the host that alerts a spans + // that has not been opened but is being closed. + _entered.exit(); + + // Ensure that any tracing output from the initialisation phase is + // flushed to the host, if necessary. + hyperlight_guest_tracing::flush(); + } dispatch_function as usize as u64 } diff --git a/src/hyperlight_guest_tracing/src/state.rs b/src/hyperlight_guest_tracing/src/state.rs index 5536a93a4..1ef7636e4 100644 --- a/src/hyperlight_guest_tracing/src/state.rs +++ b/src/hyperlight_guest_tracing/src/state.rs @@ -20,7 +20,7 @@ use alloc::vec::Vec; use core::sync::atomic::{AtomicU64, Ordering}; use hyperlight_common::flatbuffer_wrappers::guest_trace_data::{ - EventsBatchEncoder, EventsEncoder, GuestEvent, + EventsBatchEncoder, EventsEncoder, GuestEvent, MAX_TRACE_DATA_SIZE, }; use hyperlight_common::outb::OutBAction; use tracing_core::Event; @@ -43,12 +43,6 @@ pub(crate) struct GuestState { stack: Vec, } -/// TODO: Change these constant to be configurable at runtime by the guest -/// Maybe use a weak symbol that the guest can override at link time? -/// -/// Pre-calculated capacity for the encoder buffer -/// This is to avoid reallocations in the guest -const ENCODER_CAPACITY: usize = 4096; /// Start with a stack capacity for active spans const ACTIVE_SPANS_CAPACITY: usize = 64; @@ -60,16 +54,16 @@ fn send_to_host(data: &[u8]) { in("dx") OutBAction::TraceBatch as u16, in("al") 0u8, // Additional magic number to identify the action - in("r8") OutBAction::TraceBatch as u64, - in("r9") data.as_ptr() as u64, - in("r10") data.len() as u64, + in("r12") OutBAction::TraceBatch as u64, + in("r13") data.as_ptr() as u64, + in("r14") data.len() as u64, ); } } impl GuestState { pub(crate) fn new(guest_start_tsc: u64) -> Self { - let mut encoder = EventsBatchEncoder::new(ENCODER_CAPACITY, send_to_host); + let mut encoder = EventsBatchEncoder::new(MAX_TRACE_DATA_SIZE, send_to_host); encoder.encode(&GuestEvent::GuestStart { tsc: guest_start_tsc, }); diff --git a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs index 13943c1e2..cf5f33458 100644 --- a/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs +++ b/src/hyperlight_host/src/hypervisor/hyperlight_vm.rs @@ -191,6 +191,8 @@ pub enum RunVmError { DebugHandler(#[from] HandleDebugError), #[error("Execution was cancelled by the host")] ExecutionCancelledByHost, + #[error("Failed to access page: {0}")] + PageTableAccess(AccessPageTableError), #[cfg(feature = "trace_guest")] #[error("Failed to get registers: {0}")] GetRegs(RegisterError), @@ -754,10 +756,18 @@ impl HyperlightVm { tc.end_host_trace(); // Handle the guest trace data if any let regs = self.vm.regs().map_err(RunVmError::GetRegs)?; - if let Err(e) = tc.handle_trace(®s, mem_mgr) { - // If no trace data is available, we just log a message and continue - // Is this the right thing to do? - log::debug!("Error handling guest trace: {:?}", e); + + // Only parse the trace if it has reported + if tc.has_trace_data(®s) { + let root_pt = self.get_root_pt().map_err(RunVmError::PageTableAccess)?; + + // If something goes wrong with parsing the trace data, we log the error and + // continue execution instead of returning an error since this is not critical + // to correct execution of the guest + tc.handle_trace(®s, mem_mgr, root_pt) + .unwrap_or_else(|e| { + tracing::error!("Cannot handle trace data: {}", e); + }); } } result diff --git a/src/hyperlight_host/src/mem/mgr.rs b/src/hyperlight_host/src/mem/mgr.rs index 8691df380..6c9433dab 100644 --- a/src/hyperlight_host/src/mem/mgr.rs +++ b/src/hyperlight_host/src/mem/mgr.rs @@ -411,6 +411,117 @@ impl SandboxMemoryManager { Ok(()) } + + /// Read guest memory at a Guest Virtual Address (GVA) by walking the + /// page tables to translate GVA → GPA, then reading from the correct + /// backing memory (shared_mem or scratch_mem). + /// + /// This is necessary because with Copy-on-Write (CoW) the guest's + /// virtual pages are backed by physical pages in the scratch + /// region rather than being identity-mapped. + /// + /// # Arguments + /// * `gva` - The Guest Virtual Address to read from + /// * `len` - The number of bytes to read + /// * `root_pt` - The root page table physical address (CR3) + #[cfg(feature = "trace_guest")] + pub(crate) fn read_guest_memory_by_gva( + &mut self, + gva: u64, + len: usize, + root_pt: u64, + ) -> Result> { + use hyperlight_common::vmem::PAGE_SIZE; + + use crate::sandbox::snapshot::{SharedMemoryPageTableBuffer, access_gpa}; + + let scratch_size = self.scratch_mem.mem_size(); + + self.shared_mem.with_exclusivity(|snap| { + self.scratch_mem.with_exclusivity(|scratch| { + let pt_buf = SharedMemoryPageTableBuffer::new(snap, scratch, scratch_size, root_pt); + + // Walk page tables to get all mappings that cover the GVA range + let mappings: Vec<_> = unsafe { + hyperlight_common::vmem::virt_to_phys(&pt_buf, gva, len as u64) + } + .collect(); + + if mappings.is_empty() { + return Err(new_error!( + "No page table mappings found for GVA {:#x} (len {})", + gva, + len, + )); + } + + // Resulting vector of bytes to return + let mut result = Vec::with_capacity(len); + let mut current_gva = gva; + + for mapping in &mappings { + // The page table walker should only return valid mappings + // that cover our current read position. + if mapping.virt_base > current_gva { + return Err(new_error!( + "Page table walker returned mapping with virt_base {:#x} > current read position {:#x}", + mapping.virt_base, + current_gva, + )); + } + + // Calculate the offset within this page where to start copying + let page_offset = (current_gva - mapping.virt_base) as usize; + + let bytes_remaining = len - result.len(); + let available_in_page = PAGE_SIZE - page_offset; + let bytes_to_copy = bytes_remaining.min(available_in_page); + + // Translate the GPA to host memory + let gpa = mapping.phys_base + page_offset as u64; + let (mem, offset) = access_gpa(snap, scratch, scratch_size, gpa) + .ok_or_else(|| { + new_error!( + "Failed to resolve GPA {:#x} to host memory (GVA {:#x})", + gpa, + gva + ) + })?; + + let slice = mem + .as_slice() + .get(offset..offset + bytes_to_copy) + .ok_or_else(|| { + new_error!( + "GPA {:#x} resolved to out-of-bounds host offset {} (need {} bytes)", + gpa, + offset, + bytes_to_copy + ) + })?; + + result.extend_from_slice(slice); + current_gva += bytes_to_copy as u64; + } + + if result.len() != len { + tracing::error!( + "Page table walker returned mappings that don't cover the full requested length: got {}, expected {}", + result.len(), + len, + ); + return Err(new_error!( + "Could not read full GVA range: got {} of {} bytes {:?}", + result.len(), + len, + mappings + )); + } + + Ok(result) + }) + })?? + } } #[cfg(test)] diff --git a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs index 22e5797a6..4704f42e2 100644 --- a/src/hyperlight_host/src/sandbox/initialized_multi_use.rs +++ b/src/hyperlight_host/src/sandbox/initialized_multi_use.rs @@ -1429,4 +1429,98 @@ mod tests { drop(sbox); } } + + /// Helper: create a MultiUseSandbox from the simple guest with default config. + #[cfg(feature = "trace_guest")] + fn sandbox_for_gva_tests() -> MultiUseSandbox { + let path = simple_guest_as_string().unwrap(); + UninitializedSandbox::new(GuestBinary::FilePath(path), None) + .unwrap() + .evolve() + .unwrap() + } + + /// Helper: read memory at `gva` of length `len` from the guest side via + /// `ReadMappedBuffer(gva, len, false)` and from the host side via + /// `read_guest_memory_by_gva`, then assert both views are identical. + #[cfg(feature = "trace_guest")] + fn assert_gva_read_matches(sbox: &mut MultiUseSandbox, gva: u64, len: usize) { + // Guest reads via its own page tables + let expected: Vec = sbox + .call("ReadMappedBuffer", (gva, len as u64, true)) + .unwrap(); + assert_eq!(expected.len(), len); + + // Host reads by walking the same page tables + let root_pt = sbox.vm.get_root_pt().unwrap(); + let actual = sbox + .mem_mgr + .read_guest_memory_by_gva(gva, len, root_pt) + .unwrap(); + + assert_eq!( + actual, expected, + "read_guest_memory_by_gva at GVA {:#x} (len {}) differs from guest ReadMappedBuffer", + gva, len, + ); + } + + /// Test reading a small buffer (< 1 page) from guest memory via GVA. + /// Uses the guest code section which is already identity-mapped. + #[test] + #[cfg(feature = "trace_guest")] + fn read_guest_memory_by_gva_single_page() { + let mut sbox = sandbox_for_gva_tests(); + let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64; + assert_gva_read_matches(&mut sbox, code_gva, 128); + } + + /// Test reading exactly one full page (4096 bytes) from guest memory. + /// Uses the guest code section + #[test] + #[cfg(feature = "trace_guest")] + fn read_guest_memory_by_gva_full_page() { + let mut sbox = sandbox_for_gva_tests(); + let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64; + assert_gva_read_matches(&mut sbox, code_gva, 4096); + } + + /// Test that a read starting at an odd (non-page-aligned) address and + /// spanning two page boundaries returns correct data. + #[test] + #[cfg(feature = "trace_guest")] + fn read_guest_memory_by_gva_unaligned_cross_page() { + let mut sbox = sandbox_for_gva_tests(); + let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64; + // Start 1 byte before the second page boundary and read 4097 bytes + // (spans 2 full page boundaries). + let start = code_gva + 4096 - 1; + println!( + "Testing unaligned cross-page read starting at {:#x} spanning 4097 bytes", + start + ); + assert_gva_read_matches(&mut sbox, start, 4097); + } + + /// Test reading exactly two full pages (8192 bytes) from guest memory. + #[test] + #[cfg(feature = "trace_guest")] + fn read_guest_memory_by_gva_two_full_pages() { + let mut sbox = sandbox_for_gva_tests(); + let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64; + assert_gva_read_matches(&mut sbox, code_gva, 4096 * 2); + } + + /// Test reading a region that spans across a page boundary: starts + /// 100 bytes before the end of the first page and reads 200 bytes + /// into the second page. + #[test] + #[cfg(feature = "trace_guest")] + fn read_guest_memory_by_gva_cross_page_boundary() { + let mut sbox = sandbox_for_gva_tests(); + let code_gva = sbox.mem_mgr.layout.get_guest_code_address() as u64; + // Start 100 bytes before the first page boundary, read across it. + let start = code_gva + 4096 - 100; + assert_gva_read_matches(&mut sbox, start, 200); + } } diff --git a/src/hyperlight_host/src/sandbox/snapshot.rs b/src/hyperlight_host/src/sandbox/snapshot.rs index 53c45ef21..bdf823423 100644 --- a/src/hyperlight_host/src/sandbox/snapshot.rs +++ b/src/hyperlight_host/src/sandbox/snapshot.rs @@ -174,7 +174,7 @@ fn hash(memory: &[u8], regions: &[MemoryRegion]) -> Result<[u8; 32]> { Ok(hasher.finalize().into()) } -fn access_gpa<'a>( +pub(crate) fn access_gpa<'a>( snap: &'a ExclusiveSharedMemory, scratch: &'a ExclusiveSharedMemory, scratch_size: usize, @@ -197,7 +197,7 @@ pub(crate) struct SharedMemoryPageTableBuffer<'a> { root: u64, } impl<'a> SharedMemoryPageTableBuffer<'a> { - fn new( + pub(crate) fn new( snap: &'a ExclusiveSharedMemory, scratch: &'a ExclusiveSharedMemory, scratch_size: usize, diff --git a/src/hyperlight_host/src/sandbox/trace/context.rs b/src/hyperlight_host/src/sandbox/trace/context.rs index c885a1732..ac799062f 100644 --- a/src/hyperlight_host/src/sandbox/trace/context.rs +++ b/src/hyperlight_host/src/sandbox/trace/context.rs @@ -18,7 +18,7 @@ use std::collections::HashMap; use std::time::{Duration, Instant, SystemTime}; use hyperlight_common::flatbuffer_wrappers::guest_trace_data::{ - EventKeyValue, EventsBatchDecoder, EventsDecoder, GuestEvent, + EventKeyValue, EventsBatchDecoder, EventsDecoder, GuestEvent, MAX_TRACE_DATA_SIZE, }; use hyperlight_common::outb::OutBAction; use opentelemetry::global::BoxedSpan; @@ -28,73 +28,56 @@ use tracing::span::{EnteredSpan, Span}; use tracing_opentelemetry::OpenTelemetrySpanExt; use crate::hypervisor::regs::CommonRegisters; -use crate::mem::layout::SandboxMemoryLayout; use crate::mem::mgr::SandboxMemoryManager; -use crate::mem::shared_mem::{HostSharedMemory, SharedMemory}; -use crate::{HyperlightError, Result, new_error}; +use crate::mem::shared_mem::HostSharedMemory; +use crate::{Result, new_error}; /// Type that helps get the data from the guest provided the registers and memory access struct EventsBatch { events: Vec, } -impl - TryFrom<( - &CommonRegisters, - &mut SandboxMemoryManager, - )> for EventsBatch -{ - type Error = HyperlightError; - fn try_from( - (regs, mem_mgr): ( - &CommonRegisters, - &mut SandboxMemoryManager, - ), +impl EventsBatch { + /// Extract a batch of guest trace events from guest memory. + /// + /// The guest passes the trace data pointer as a Guest Virtual Address (GVA) + /// in register r13. With Copy-on-Write enabled, this GVA may not be + /// identity-mapped to its physical address, so we walk the guest page + /// tables to translate GVA → GPA before reading the data. + /// + /// # Arguments + /// * `regs` - The guest registers (r12 = magic, r13 = GVA pointer, r14 = length) + /// * `mem_mgr` - The sandbox memory manager with access to shared and scratch memory + /// * `root_pt` - The root page table physical address (CR3) for GVA translation + fn from_regs( + regs: &CommonRegisters, + mem_mgr: &mut SandboxMemoryManager, + root_pt: u64, ) -> Result { - let magic_no = regs.r8; - let trace_data_ptr = regs.r9 as usize; - let trace_data_len = regs.r10 as usize; + let magic_no = regs.r12; + let trace_data_gva = regs.r13; + let trace_data_len = regs.r14 as usize; + // Validate the magic number to ensure the guest is providing trace data if magic_no != OutBAction::TraceBatch as u64 { return Err(new_error!("A TraceBatch is not present")); } - // Extract the GuestTraceData from guest memory - // This involves: - // 1. Using a mutable reference to the memory manager to get exclusive access to the shared memory. - // This is necessary to ensure that no other part of the code is accessing the memory - // while we are reading from it. - // 2. Getting immutable access to the slice of memory that contains the GuestTraceData - // 3. Parsing the slice into a GuestTraceData structure - // - // Error handling is done at each step to ensure that any issues are properly reported. - // This includes logging errors for easier debugging. - // - // The reason for using `with_exclusivity` is to ensure that we have exclusive access - // and avoid allocating new memory, which needs to be correctly aligned for the - // flatbuffer parsing. - let events = mem_mgr.shared_mem.with_exclusivity(|mem| { - let buf_slice = mem - .as_slice() - // Adjust the pointer to be relative to the base address of the sandbox memory - .get( - trace_data_ptr - SandboxMemoryLayout::BASE_ADDRESS - ..trace_data_ptr - SandboxMemoryLayout::BASE_ADDRESS + trace_data_len, - ) - // Convert the slice to a Result to handle the case where the slice is out of - // bounds and return a proper error message and log the error. - .ok_or_else(|| { - tracing::error!("Failed to get guest trace batch slice from guest memory"); - new_error!("Failed to get guest trace batch slice from guest memory") - })?; + // Validate the length to prevent reading excessive memory + if trace_data_len == 0 || trace_data_len > MAX_TRACE_DATA_SIZE { + return Err(new_error!("Invalid TraceBatch length: {}", trace_data_len)); + } - let events = EventsBatchDecoder {}.decode(buf_slice).map_err(|e| { - tracing::error!("Failed to deserialize guest trace events: {:?}", e); - new_error!("Failed to deserialize guest trace events: {:?}", e) - })?; + // Read the trace data from guest memory by walking the page tables + // to translate the GVA to physical addresses. This is necessary + // because with CoW, guest virtual pages are backed by physical + // pages in the scratch region rather than being identity-mapped. + let buf = mem_mgr.read_guest_memory_by_gva(trace_data_gva, trace_data_len, root_pt)?; - Ok::, HyperlightError>(events) - })??; + let events = EventsBatchDecoder {}.decode(&buf).map_err(|e| { + tracing::error!("Failed to deserialize guest trace events: {:?}", e); + new_error!("Failed to deserialize guest trace events: {:?}", e) + })?; Ok(EventsBatch { events }) } @@ -215,13 +198,19 @@ impl TraceContext { + Duration::from_micros(rel_start_us as u64)) } + /// Check if the registers indicate that there is trace data to be handled. + pub fn has_trace_data(&self, regs: &CommonRegisters) -> bool { + regs.r12 == OutBAction::TraceBatch as u64 + } + pub fn handle_trace( &mut self, regs: &CommonRegisters, mem_mgr: &mut SandboxMemoryManager, + root_pt: u64, ) -> Result<()> { // Get the guest sent info - let trace_batch = EventsBatch::try_from((regs, mem_mgr))?; + let trace_batch = EventsBatch::from_regs(regs, mem_mgr, root_pt)?; self.handle_trace_impl(trace_batch.events) }