Skip to content

Commit d3c416c

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 <[email protected]>
1 parent 946c565 commit d3c416c

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)]
@@ -554,7 +554,7 @@ impl Debug for HypervLinuxDriver {
554554
}
555555
}
556556

557-
#[cfg(feature = "unwind_guest")]
557+
#[cfg(feature = "trace_guest")]
558558
impl From<TraceRegister> for hv_register_name {
559559
fn from(r: TraceRegister) -> Self {
560560
match r {
@@ -761,6 +761,12 @@ impl Hypervisor for HypervLinuxDriver {
761761
libc::EINTR,
762762
)))
763763
} else {
764+
#[cfg(feature = "trace_guest")]
765+
if self.trace_info.guest_start_epoch.is_none() {
766+
// Set the guest start epoch to the current time, before running the vcpu
767+
crate::debug!("HyperV - Guest Start Epoch set");
768+
self.trace_info.guest_start_epoch = Some(std::time::Instant::now());
769+
}
764770
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
765771
// Then the vcpu will run, but we will keep sending signals to this thread
766772
// to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will
@@ -1114,7 +1120,7 @@ impl Hypervisor for HypervLinuxDriver {
11141120
Ok(())
11151121
}
11161122

1117-
#[cfg(feature = "unwind_guest")]
1123+
#[cfg(feature = "trace_guest")]
11181124
fn read_trace_reg(&self, reg: TraceRegister) -> Result<u64> {
11191125
let mut assoc = [hv_register_assoc {
11201126
name: reg.into(),
@@ -1129,6 +1135,10 @@ impl Hypervisor for HypervLinuxDriver {
11291135
fn trace_info_as_ref(&self) -> &TraceInfo {
11301136
&self.trace_info
11311137
}
1138+
#[cfg(feature = "trace_guest")]
1139+
fn trace_info_as_mut(&mut self) -> &mut TraceInfo {
1140+
&mut self.trace_info
1141+
}
11321142
}
11331143

11341144
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
@@ -44,7 +44,7 @@ use {
4444
std::sync::Mutex,
4545
};
4646

47-
#[cfg(feature = "unwind_guest")]
47+
#[cfg(feature = "trace_guest")]
4848
use super::TraceRegister;
4949
use super::fpu::{FP_TAG_WORD_DEFAULT, MXCSR_DEFAULT};
5050
use super::handlers::{MemAccessHandlerWrapper, OutBHandlerWrapper};
@@ -732,6 +732,12 @@ impl Hypervisor for HypervWindowsDriver {
732732
Reserved: Default::default(),
733733
}
734734
} else {
735+
#[cfg(feature = "trace_guest")]
736+
if self.trace_info.guest_start_epoch.is_none() {
737+
// Set the guest start epoch to the current time, before running the vcpu
738+
crate::debug!("MSHV - Guest Start Epoch set");
739+
self.trace_info.guest_start_epoch = Some(std::time::Instant::now());
740+
}
735741
self.processor.run()?
736742
};
737743
self.interrupt_handle
@@ -1046,7 +1052,7 @@ impl Hypervisor for HypervWindowsDriver {
10461052
Ok(())
10471053
}
10481054

1049-
#[cfg(feature = "unwind_guest")]
1055+
#[cfg(feature = "trace_guest")]
10501056
fn read_trace_reg(&self, reg: TraceRegister) -> Result<u64> {
10511057
let regs = self.processor.get_regs()?;
10521058
match reg {
@@ -1062,6 +1068,10 @@ impl Hypervisor for HypervWindowsDriver {
10621068
fn trace_info_as_ref(&self) -> &TraceInfo {
10631069
&self.trace_info
10641070
}
1071+
#[cfg(feature = "trace_guest")]
1072+
fn trace_info_as_mut(&mut self) -> &mut TraceInfo {
1073+
&mut self.trace_info
1074+
}
10651075
}
10661076

10671077
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)]
@@ -643,6 +643,13 @@ impl Hypervisor for KVMDriver {
643643
{
644644
Err(kvm_ioctls::Error::new(libc::EINTR))
645645
} else {
646+
#[cfg(feature = "trace_guest")]
647+
if self.trace_info.guest_start_epoch.is_none() {
648+
// Set the guest start epoch to the current time, before running the vcpu
649+
crate::debug!("KVM - Guest Start Epoch set");
650+
self.trace_info.guest_start_epoch = Some(std::time::Instant::now());
651+
}
652+
646653
// Note: if a `InterruptHandle::kill()` called while this thread is **here**
647654
// Then the vcpu will run, but we will keep sending signals to this thread
648655
// to interrupt it until `running` is set to false. The `vcpu_fd::run()` call will
@@ -965,7 +972,7 @@ impl Hypervisor for KVMDriver {
965972
Ok(())
966973
}
967974

968-
#[cfg(feature = "unwind_guest")]
975+
#[cfg(feature = "trace_guest")]
969976
fn read_trace_reg(&self, reg: TraceRegister) -> Result<u64> {
970977
let regs = self.vcpu_fd.get_regs()?;
971978
Ok(match reg {
@@ -981,6 +988,10 @@ impl Hypervisor for KVMDriver {
981988
fn trace_info_as_ref(&self) -> &TraceInfo {
982989
&self.trace_info
983990
}
991+
#[cfg(feature = "trace_guest")]
992+
fn trace_info_as_mut(&mut self) -> &mut TraceInfo {
993+
&mut self.trace_info
994+
}
984995
}
985996

986997
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
@@ -105,6 +105,17 @@ pub(crate) struct TraceInfo {
105105
/// early as the creation of the sandbox being traced.
106106
#[allow(dead_code)]
107107
pub epoch: std::time::Instant,
108+
/// The frequency of the timestamp counter.
109+
#[allow(dead_code)]
110+
pub tsc_freq: u64,
111+
/// The epoch at which the guest started, if it has started.
112+
/// This is used to calculate the time spent in the guest relative to the
113+
/// time of the host.
114+
#[allow(dead_code)]
115+
pub guest_start_epoch: Option<std::time::Instant>,
116+
/// The start guest time, in TSC cycles, for the current guest.
117+
#[allow(dead_code)]
118+
pub guest_start_tsc: Option<u64>,
108119
/// The file to which the trace is being written
109120
#[allow(dead_code)]
110121
pub file: Arc<Mutex<std::fs::File>>,
@@ -139,8 +150,13 @@ impl TraceInfo {
139150
let cache = framehop::x86_64::CacheX86_64::new();
140151
(unwinder, Arc::new(Mutex::new(cache)))
141152
};
153+
let tsc_freq = Self::calculate_tsc_freq()?;
154+
142155
let ret = Self {
143156
epoch: std::time::Instant::now(),
157+
tsc_freq,
158+
guest_start_epoch: None,
159+
guest_start_tsc: None,
144160
file: Arc::new(Mutex::new(std::fs::File::create_new(path)?)),
145161
#[cfg(feature = "unwind_guest")]
146162
unwind_module,
@@ -156,6 +172,24 @@ impl TraceInfo {
156172
})?;
157173
Ok(ret)
158174
}
175+
176+
/// Calculate the TSC frequency based on the RDTSC instruction.
177+
fn calculate_tsc_freq() -> crate::Result<u64> {
178+
if !hyperlight_guest_tracing::invariant_tsc::has_invariant_tsc() {
179+
return Err(crate::new_error!(
180+
"Invariant TSC is not supported on this platform"
181+
));
182+
}
183+
let start = hyperlight_guest_tracing::invariant_tsc::read_tsc();
184+
let start_time = std::time::Instant::now();
185+
// Sleep for 1 second to get a good sample
186+
std::thread::sleep(std::time::Duration::from_secs(1));
187+
let end = hyperlight_guest_tracing::invariant_tsc::read_tsc();
188+
let end_time = std::time::Instant::now();
189+
let elapsed = end_time.duration_since(start_time).as_secs_f64();
190+
191+
Ok(((end - start) as f64 / elapsed) as u64)
192+
}
159193
}
160194

161195
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)