Skip to content

Commit f82d043

Browse files
committed
[hyperlight_host/trace] Handle the trace records sent by a guest
- When an `Out` instruction is intercepted, the Hypervisor checks for the frame Id, to verify what type of exit it is. Based on this, when a trace record type is received, we copy the array of trace records from the guest's memory, calculate the timestamp based on the cycles returned by the guest's RDTSC and write the record to the trace file. Signed-off-by: Doru Blânzeanu <dblnz@pm.me>
1 parent 1f99f56 commit f82d043

File tree

7 files changed

+178
-12
lines changed

7 files changed

+178
-12
lines changed

src/hyperlight_host/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ tracing = { version = "0.1.41", features = ["log"] }
3939
tracing-log = "0.2.0"
4040
tracing-core = "0.1.34"
4141
hyperlight-common = { workspace = true, default-features = true, features = [ "std" ] }
42+
hyperlight-guest-tracing = { workspace = true, default-features = true, optional = true }
4243
vmm-sys-util = "0.14.0"
4344
crossbeam-channel = "0.5.15"
4445
thiserror = "2.0.12"
@@ -131,7 +132,7 @@ executable_heap = []
131132
print_debug = []
132133
# Dumps the VM state to a file on unexpected errors or crashes. The path of the file will be printed on stdout and logged.
133134
crashdump = ["dep:chrono"]
134-
trace_guest = []
135+
trace_guest = ["hyperlight-common/trace_guest", "dep:hyperlight-guest-tracing"]
135136
# This feature enables unwinding the guest stack from the host, in
136137
# order to produce stack traces for debugging or profiling.
137138
unwind_guest = [ "trace_guest", "dep:framehop", "dep:fallible-iterator", "hyperlight-common/unwind_guest" ]

src/hyperlight_host/src/hypervisor/hyperv_linux.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ use mshv_bindings::{
4848
hv_partition_property_code_HV_PARTITION_PROPERTY_SYNTHETIC_PROC_FEATURES,
4949
hv_partition_synthetic_processor_features,
5050
};
51-
#[cfg(feature = "unwind_guest")]
51+
#[cfg(feature = "trace_guest")]
5252
use mshv_bindings::{
5353
hv_register_name, hv_register_name_HV_X64_REGISTER_RAX, hv_register_name_HV_X64_REGISTER_RBP,
5454
hv_register_name_HV_X64_REGISTER_RCX, hv_register_name_HV_X64_REGISTER_RSP,
@@ -58,7 +58,7 @@ use tracing::{Span, instrument};
5858
#[cfg(crashdump)]
5959
use {super::crashdump, std::path::Path};
6060

61-
#[cfg(feature = "unwind_guest")]
61+
#[cfg(feature = "trace_guest")]
6262
use super::TraceRegister;
6363
use super::fpu::{FP_CONTROL_WORD_DEFAULT, FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT};
6464
#[cfg(gdb)]
@@ -532,7 +532,7 @@ impl Debug for HypervLinuxDriver {
532532
}
533533
}
534534

535-
#[cfg(feature = "unwind_guest")]
535+
#[cfg(feature = "trace_guest")]
536536
impl From<TraceRegister> for hv_register_name {
537537
fn from(r: TraceRegister) -> Self {
538538
match r {
@@ -739,6 +739,12 @@ impl Hypervisor for HypervLinuxDriver {
739739
libc::EINTR,
740740
)))
741741
} else {
742+
#[cfg(feature = "trace_guest")]
743+
if self.trace_info.guest_start_epoch.is_none() {
744+
// Set the guest start epoch to the current time, before running the vcpu
745+
crate::debug!("HyperV - Guest Start Epoch set");
746+
self.trace_info.guest_start_epoch = Some(std::time::Instant::now());
747+
}
742748
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
743749
// Then the vcpu will run, but we will keep sending signals to this thread
744750
// to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will
@@ -1092,7 +1098,7 @@ impl Hypervisor for HypervLinuxDriver {
10921098
Ok(())
10931099
}
10941100

1095-
#[cfg(feature = "unwind_guest")]
1101+
#[cfg(feature = "trace_guest")]
10961102
fn read_trace_reg(&self, reg: TraceRegister) -> Result<u64> {
10971103
let mut assoc = [hv_register_assoc {
10981104
name: reg.into(),
@@ -1107,6 +1113,10 @@ impl Hypervisor for HypervLinuxDriver {
11071113
fn trace_info_as_ref(&self) -> &TraceInfo {
11081114
&self.trace_info
11091115
}
1116+
#[cfg(feature = "trace_guest")]
1117+
fn trace_info_as_mut(&mut self) -> &mut TraceInfo {
1118+
&mut self.trace_info
1119+
}
11101120
}
11111121

11121122
impl Drop for HypervLinuxDriver {

src/hyperlight_host/src/hypervisor/hyperv_windows.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ use {
4141
std::sync::Mutex,
4242
};
4343

44-
#[cfg(feature = "unwind_guest")]
44+
#[cfg(feature = "trace_guest")]
4545
use super::TraceRegister;
4646
use super::fpu::{FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT};
4747
use super::handlers::{MemAccessHandlerWrapper, OutBHandlerWrapper};
@@ -723,6 +723,12 @@ impl Hypervisor for HypervWindowsDriver {
723723
Reserved: Default::default(),
724724
}
725725
} else {
726+
#[cfg(feature = "trace_guest")]
727+
if self.trace_info.guest_start_epoch.is_none() {
728+
// Set the guest start epoch to the current time, before running the vcpu
729+
crate::debug!("MSHV - Guest Start Epoch set");
730+
self.trace_info.guest_start_epoch = Some(std::time::Instant::now());
731+
}
726732
self.processor.run()?
727733
};
728734
self.interrupt_handle
@@ -1037,7 +1043,7 @@ impl Hypervisor for HypervWindowsDriver {
10371043
Ok(())
10381044
}
10391045

1040-
#[cfg(feature = "unwind_guest")]
1046+
#[cfg(feature = "trace_guest")]
10411047
fn read_trace_reg(&self, reg: TraceRegister) -> Result<u64> {
10421048
let regs = self.processor.get_regs()?;
10431049
match reg {
@@ -1053,6 +1059,10 @@ impl Hypervisor for HypervWindowsDriver {
10531059
fn trace_info_as_ref(&self) -> &TraceInfo {
10541060
&self.trace_info
10551061
}
1062+
#[cfg(feature = "trace_guest")]
1063+
fn trace_info_as_mut(&mut self) -> &mut TraceInfo {
1064+
&mut self.trace_info
1065+
}
10561066
}
10571067

10581068
impl Drop for HypervWindowsDriver {

src/hyperlight_host/src/hypervisor/kvm.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use tracing::{Span, instrument};
2929
#[cfg(crashdump)]
3030
use {super::crashdump, std::path::Path};
3131

32-
#[cfg(feature = "unwind_guest")]
32+
#[cfg(feature = "trace_guest")]
3333
use super::TraceRegister;
3434
use super::fpu::{FP_CONTROL_WORD_DEFAULT, FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT};
3535
#[cfg(gdb)]
@@ -627,6 +627,13 @@ impl Hypervisor for KVMDriver {
627627
{
628628
Err(kvm_ioctls::Error::new(libc::EINTR))
629629
} else {
630+
#[cfg(feature = "trace_guest")]
631+
if self.trace_info.guest_start_epoch.is_none() {
632+
// Set the guest start epoch to the current time, before running the vcpu
633+
crate::debug!("KVM - Guest Start Epoch set");
634+
self.trace_info.guest_start_epoch = Some(std::time::Instant::now());
635+
}
636+
630637
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
631638
// Then the vcpu will run, but we will keep sending signals to this thread
632639
// to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will
@@ -949,7 +956,7 @@ impl Hypervisor for KVMDriver {
949956
Ok(())
950957
}
951958

952-
#[cfg(feature = "unwind_guest")]
959+
#[cfg(feature = "trace_guest")]
953960
fn read_trace_reg(&self, reg: TraceRegister) -> Result<u64> {
954961
let regs = self.vcpu_fd.get_regs()?;
955962
Ok(match reg {
@@ -965,6 +972,10 @@ impl Hypervisor for KVMDriver {
965972
fn trace_info_as_ref(&self) -> &TraceInfo {
966973
&self.trace_info
967974
}
975+
#[cfg(feature = "trace_guest")]
976+
fn trace_info_as_mut(&mut self) -> &mut TraceInfo {
977+
&mut self.trace_info
978+
}
968979
}
969980

970981
impl Drop for KVMDriver {

src/hyperlight_host/src/hypervisor/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,15 @@ pub(crate) trait Hypervisor: Debug + Sync + Send {
270270
}
271271

272272
/// Read a register for trace/unwind purposes
273-
#[cfg(feature = "unwind_guest")]
273+
#[cfg(feature = "trace_guest")]
274274
fn read_trace_reg(&self, reg: TraceRegister) -> Result<u64>;
275275

276276
/// Get a reference of the trace info for the guest
277277
#[cfg(feature = "trace_guest")]
278278
fn trace_info_as_ref(&self) -> &TraceInfo;
279+
/// Get a mutable reference of the trace info for the guest
280+
#[cfg(feature = "trace_guest")]
281+
fn trace_info_as_mut(&mut self) -> &mut TraceInfo;
279282
}
280283

281284
/// A virtual CPU that can be run until an exit occurs

src/hyperlight_host/src/sandbox/mod.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ pub(crate) struct TraceInfo {
102102
/// early as the creation of the sandbox being traced.
103103
#[allow(dead_code)]
104104
pub epoch: std::time::Instant,
105+
/// The frequency of the timestamp counter.
106+
#[allow(dead_code)]
107+
pub tsc_freq: u64,
108+
/// The epoch at which the guest started, if it has started.
109+
/// This is used to calculate the time spent in the guest relative to the
110+
/// time of the host.
111+
#[allow(dead_code)]
112+
pub guest_start_epoch: Option<std::time::Instant>,
113+
/// The start guest time, in TSC cycles, for the current guest.
114+
#[allow(dead_code)]
115+
pub guest_start_tsc: Option<u64>,
105116
/// The file to which the trace is being written
106117
#[allow(dead_code)]
107118
pub file: Arc<Mutex<std::fs::File>>,
@@ -136,8 +147,13 @@ impl TraceInfo {
136147
let cache = framehop::x86_64::CacheX86_64::new();
137148
(unwinder, Arc::new(Mutex::new(cache)))
138149
};
150+
let tsc_freq = Self::calculate_tsc_freq()?;
151+
139152
let ret = Self {
140153
epoch: std::time::Instant::now(),
154+
tsc_freq,
155+
guest_start_epoch: None,
156+
guest_start_tsc: None,
141157
file: Arc::new(Mutex::new(std::fs::File::create_new(path)?)),
142158
#[cfg(feature = "unwind_guest")]
143159
unwind_module,
@@ -153,6 +169,24 @@ impl TraceInfo {
153169
})?;
154170
Ok(ret)
155171
}
172+
173+
/// Calculate the TSC frequency based on the RDTSC instruction.
174+
fn calculate_tsc_freq() -> crate::Result<u64> {
175+
if !hyperlight_guest_tracing::invariant_tsc::has_invariant_tsc() {
176+
return Err(crate::new_error!(
177+
"Invariant TSC is not supported on this platform"
178+
));
179+
}
180+
let start = hyperlight_guest_tracing::invariant_tsc::read_tsc();
181+
let start_time = std::time::Instant::now();
182+
// Sleep for 1 second to get a good sample
183+
std::thread::sleep(std::time::Duration::from_secs(1));
184+
let end = hyperlight_guest_tracing::invariant_tsc::read_tsc();
185+
let end_time = std::time::Instant::now();
186+
let elapsed = end_time.duration_since(start_time).as_secs_f64();
187+
188+
Ok(((end - start) as f64 / elapsed) as u64)
189+
}
156190
}
157191

158192
pub(crate) trait WrapperGetter {

src/hyperlight_host/src/sandbox/outb.rs

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
1616

17-
#[cfg(feature = "unwind_guest")]
17+
#[cfg(feature = "trace_guest")]
1818
use std::io::Write;
1919
use std::sync::{Arc, Mutex};
2020

@@ -26,14 +26,16 @@ use hyperlight_common::flatbuffer_wrappers::function_types::ParameterValue;
2626
use hyperlight_common::flatbuffer_wrappers::guest_error::ErrorCode;
2727
use hyperlight_common::flatbuffer_wrappers::guest_log_data::GuestLogData;
2828
use hyperlight_common::outb::{Exception, OutBAction};
29+
#[cfg(feature = "trace_guest")]
30+
use hyperlight_guest_tracing::TraceRecord;
2931
use log::{Level, Record};
3032
use tracing::{Span, instrument};
3133
use tracing_log::format_trace;
3234

3335
use super::host_funcs::FunctionRegistry;
3436
use super::mem_mgr::MemMgrWrapper;
3537
use crate::hypervisor::handlers::{OutBHandler, OutBHandlerFunction, OutBHandlerWrapper};
36-
#[cfg(feature = "unwind_guest")]
38+
#[cfg(feature = "trace_guest")]
3739
use crate::mem::layout::SandboxMemoryLayout;
3840
use crate::mem::mgr::SandboxMemoryManager;
3941
use crate::mem::shared_mem::HostSharedMemory;
@@ -211,6 +213,53 @@ pub(super) fn record_trace_frame<F: FnOnce(&mut std::fs::File)>(
211213
Ok(())
212214
}
213215

216+
#[cfg(feature = "trace_guest")]
217+
pub(super) fn record_guest_trace_frame<F: FnOnce(&mut std::fs::File)>(
218+
trace_info: &TraceInfo,
219+
frame_id: u64,
220+
cycles: u64,
221+
write_frame: F,
222+
) -> Result<()> {
223+
let Ok(mut out) = trace_info.file.lock() else {
224+
return Ok(());
225+
};
226+
// frame structure:
227+
// 16 bytes timestamp
228+
229+
// The number of cycles spent in the guest relative to the first received trace record
230+
let cycles_spent = cycles
231+
- trace_info
232+
.guest_start_tsc
233+
.as_ref()
234+
.map_or_else(|| 0, |c| *c);
235+
// Convert cycles to microseconds based on the TSC frequency
236+
let micros = cycles_spent as f64 / trace_info.tsc_freq as f64 * 1_000_000f64;
237+
238+
// Convert to a Duration
239+
let guest_duration = std::time::Duration::from_micros(micros as u64);
240+
241+
// Calculate the time when the guest started execution relative to the host epoch
242+
// Note: This is relative to the time saved when the `TraceInfo` was created (before the
243+
// Hypervisor is created).
244+
let guest_start_time = trace_info
245+
.guest_start_epoch
246+
.as_ref()
247+
.unwrap_or(&trace_info.epoch)
248+
.saturating_duration_since(trace_info.epoch);
249+
250+
// Calculate the timestamp when the actual frame was recorded relative to the host epoch
251+
let timestamp = guest_start_time
252+
.checked_add(guest_duration)
253+
.unwrap_or(guest_duration);
254+
255+
let _ = out.write_all(&timestamp.as_micros().to_ne_bytes());
256+
// 8 bytes frame type id
257+
let _ = out.write_all(&frame_id.to_ne_bytes());
258+
// frame data
259+
write_frame(&mut out);
260+
Ok(())
261+
}
262+
214263
/// Handles OutB operations from the guest.
215264
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
216265
fn handle_outb_impl(
@@ -287,6 +336,54 @@ fn handle_outb_impl(
287336
write_stack(f, &stack);
288337
})
289338
}
339+
#[cfg(feature = "trace_guest")]
340+
OutBAction::TraceRecord => {
341+
let Ok(len) = _hv.read_trace_reg(crate::hypervisor::TraceRegister::RAX) else {
342+
return Ok(());
343+
};
344+
let Ok(ptr) = _hv.read_trace_reg(crate::hypervisor::TraceRegister::RCX) else {
345+
return Ok(());
346+
};
347+
let mut buffer = vec![0u8; len as usize * std::mem::size_of::<TraceRecord>()];
348+
let buffer = &mut buffer[..];
349+
350+
// Read the trace records from the guest memory
351+
// TODO: maybe this can be done without copying?
352+
mem_mgr
353+
.as_ref()
354+
.shared_mem
355+
.copy_to_slice(buffer, ptr as usize - SandboxMemoryLayout::BASE_ADDRESS)
356+
// .read::<u64>((addr - SandboxMemoryLayout::BASE_ADDRESS as u64) as usize)
357+
.map_err(|e| {
358+
new_error!(
359+
"Failed to copy trace records from guest memory to host: {:?}",
360+
e
361+
)
362+
})?;
363+
364+
let traces = unsafe {
365+
std::slice::from_raw_parts(buffer.as_ptr() as *const TraceRecord, len as usize)
366+
};
367+
368+
{
369+
let trace_info = _hv.trace_info_as_mut();
370+
// Store the start guest cycles if not already set
371+
// This is the `entrypoint` of the guest execution
372+
// This should be set only once, at the start of the guest execution
373+
if trace_info.guest_start_tsc.is_none() && !traces.is_empty() {
374+
trace_info.guest_start_tsc = Some(traces[0].cycles);
375+
}
376+
}
377+
378+
for record in traces {
379+
record_guest_trace_frame(_hv.trace_info_as_ref(), 4u64, record.cycles, |f| {
380+
let _ = f.write_all(&record.msg_len.to_ne_bytes());
381+
let _ = f.write_all(&record.msg[..record.msg_len]);
382+
})?
383+
}
384+
385+
Ok(())
386+
}
290387
}
291388
}
292389

0 commit comments

Comments
 (0)