use std::sync::{Arc, Mutex};
use hyperlight_common::flatbuffer_wrappers::function_types::{FunctionCallResult, ParameterValue};
use hyperlight_common::flatbuffer_wrappers::guest_error::{ErrorCode, GuestError};
use hyperlight_common::flatbuffer_wrappers::guest_log_data::GuestLogData;
use hyperlight_common::outb::{Exception, OutBAction};
use log::{Level, Record};
use tracing::{Span, instrument};
use tracing_log::format_trace;
use super::host_funcs::FunctionRegistry;
#[cfg(feature = "mem_profile")]
use crate::hypervisor::regs::CommonRegisters;
use crate::mem::mgr::SandboxMemoryManager;
use crate::mem::shared_mem::HostSharedMemory;
#[cfg(feature = "mem_profile")]
use crate::sandbox::trace::MemTraceInfo;
#[derive(Debug, thiserror::Error)]
pub enum HandleOutbError {
#[error("Guest aborted: error code {code}, message: {message}")]
GuestAborted {
code: u8,
message: String,
},
#[error("Invalid outb port: {0}")]
InvalidPort(String),
#[error("Failed to read guest log data: {0}")]
ReadLogData(String),
#[error("Trace formatting error: {0}")]
TraceFormat(String),
#[error("Failed to read host function call: {0}")]
ReadHostFunctionCall(String),
#[error("Failed to acquire lock at {0}:{1} - {2}")]
LockFailed(&'static str, u32, String),
#[error("Failed to write host function response: {0}")]
WriteHostFunctionResponse(String),
#[error("Invalid character for debug print: {0}")]
InvalidDebugPrintChar(u32),
#[cfg(feature = "mem_profile")]
#[error("Memory profiling error: {0}")]
MemProfile(String),
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level="Trace")]
pub(super) fn outb_log(
mgr: &mut SandboxMemoryManager<HostSharedMemory>,
) -> Result<(), HandleOutbError> {
let log_data: GuestLogData = mgr
.read_guest_log_data()
.map_err(|e| HandleOutbError::ReadLogData(e.to_string()))?;
let record_level: Level = (&log_data.level).into();
let should_trace = tracing_core::dispatcher::has_been_set();
let source_file = Some(log_data.source_file.as_str());
let line = Some(log_data.line);
let source = Some(log_data.source.as_str());
if should_trace {
format_trace(
&Record::builder()
.args(format_args!("{}", log_data.message))
.level(record_level)
.target("hyperlight_guest")
.file(source_file)
.line(line)
.module_path(source)
.build(),
)
.map_err(|e| HandleOutbError::TraceFormat(e.to_string()))?;
} else {
log::logger().log(
&Record::builder()
.args(format_args!("{}", log_data.message))
.level(record_level)
.target("hyperlight_guest")
.file(Some(&log_data.source_file))
.line(Some(log_data.line))
.module_path(Some(&log_data.source))
.build(),
);
}
Ok(())
}
const ABORT_TERMINATOR: u8 = 0xFF;
const MAX_ABORT_BUFFER_LEN: usize = 1024;
fn outb_abort(
mem_mgr: &mut SandboxMemoryManager<HostSharedMemory>,
data: u32,
) -> Result<(), HandleOutbError> {
let buffer = mem_mgr.get_abort_buffer_mut();
let bytes = data.to_le_bytes(); let len = bytes[0].min(3);
for &b in &bytes[1..=len as usize] {
if b == ABORT_TERMINATOR {
let guest_error_code = *buffer.first().unwrap_or(&0);
let result = {
let message = if let Some(&maybe_exception_code) = buffer.get(1) {
match Exception::try_from(maybe_exception_code) {
Ok(exception) => {
let extra_msg = String::from_utf8_lossy(&buffer[2..]);
format!("Exception: {:?} | {}", exception, extra_msg)
}
Err(_) => String::from_utf8_lossy(&buffer[1..]).into(),
}
} else {
String::new()
};
Err(HandleOutbError::GuestAborted {
code: guest_error_code,
message,
})
};
buffer.clear();
return result;
}
if buffer.len() >= MAX_ABORT_BUFFER_LEN {
buffer.clear();
return Err(HandleOutbError::GuestAborted {
code: 0,
message: "Guest abort buffer overflowed".into(),
});
}
buffer.push(b);
}
Ok(())
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn handle_outb(
mem_mgr: &mut SandboxMemoryManager<HostSharedMemory>,
host_funcs: &Arc<Mutex<FunctionRegistry>>,
port: u16,
data: u32,
#[cfg(feature = "mem_profile")] regs: &CommonRegisters,
#[cfg(feature = "mem_profile")] trace_info: &mut MemTraceInfo,
) -> Result<(), HandleOutbError> {
match port
.try_into()
.map_err(|e: anyhow::Error| HandleOutbError::InvalidPort(e.to_string()))?
{
OutBAction::Log => outb_log(mem_mgr),
OutBAction::CallFunction => {
let call = mem_mgr
.get_host_function_call()
.map_err(|e| HandleOutbError::ReadHostFunctionCall(e.to_string()))?;
let name = call.function_name.clone();
let args: Vec<ParameterValue> = call.parameters.unwrap_or(vec![]);
let res = host_funcs
.try_lock()
.map_err(|e| HandleOutbError::LockFailed(file!(), line!(), e.to_string()))?
.call_host_function(&name, args)
.map_err(|e| GuestError::new(ErrorCode::HostFunctionError, e.to_string()));
let func_result = FunctionCallResult::new(res);
mem_mgr
.write_response_from_host_function_call(&func_result)
.map_err(|e| HandleOutbError::WriteHostFunctionResponse(e.to_string()))?;
Ok(())
}
OutBAction::Abort => outb_abort(mem_mgr, data),
OutBAction::DebugPrint => {
let ch: char = match char::from_u32(data) {
Some(c) => c,
None => {
return Err(HandleOutbError::InvalidDebugPrintChar(data));
}
};
eprint!("{}", ch);
Ok(())
}
#[cfg(feature = "trace_guest")]
OutBAction::TraceBatch => Ok(()),
#[cfg(feature = "mem_profile")]
OutBAction::TraceMemoryAlloc => trace_info.handle_trace_mem_alloc(regs, mem_mgr),
#[cfg(feature = "mem_profile")]
OutBAction::TraceMemoryFree => trace_info.handle_trace_mem_free(regs, mem_mgr),
}
}
#[cfg(test)]
mod tests {
use hyperlight_common::flatbuffer_wrappers::guest_log_level::LogLevel;
use hyperlight_testing::logger::{LOGGER, Logger};
use hyperlight_testing::simple_guest_as_string;
use log::Level;
use tracing_core::callsite::rebuild_interest_cache;
use super::outb_log;
use crate::GuestBinary;
use crate::mem::mgr::SandboxMemoryManager;
use crate::sandbox::SandboxConfiguration;
use crate::sandbox::outb::GuestLogData;
use crate::testing::log_values::test_value_as_str;
fn new_guest_log_data(level: LogLevel) -> GuestLogData {
GuestLogData::new(
"test log".to_string(),
"test source".to_string(),
level,
"test caller".to_string(),
"test source file".to_string(),
123,
)
}
#[test]
#[ignore]
fn test_log_outb_log() {
Logger::initialize_test_logger();
LOGGER.set_max_level(log::LevelFilter::Off);
let sandbox_cfg = SandboxConfiguration::default();
let new_mgr = || {
let bin = GuestBinary::FilePath(simple_guest_as_string().unwrap());
let snapshot = crate::sandbox::snapshot::Snapshot::from_env(bin, sandbox_cfg).unwrap();
let mgr = SandboxMemoryManager::from_snapshot(&snapshot).unwrap();
let (hmgr, _) = mgr.build().unwrap();
hmgr
};
{
let mut mgr = new_mgr();
assert!(outb_log(&mut mgr).is_err());
}
{
let mut mgr = new_mgr();
let log_msg = new_guest_log_data(LogLevel::Information);
let guest_log_data_buffer: Vec<u8> = log_msg.try_into().unwrap();
let offset = mgr.layout.get_output_data_buffer_scratch_host_offset();
mgr.scratch_mem
.push_buffer(
offset,
sandbox_cfg.get_output_data_size(),
&guest_log_data_buffer,
)
.unwrap();
let res = outb_log(&mut mgr);
assert!(res.is_ok());
assert_eq!(0, LOGGER.num_log_calls());
LOGGER.clear_log_calls();
}
{
LOGGER.set_max_level(log::LevelFilter::Trace);
let mut mgr = new_mgr();
LOGGER.clear_log_calls();
let levels = vec![
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Information,
LogLevel::Warning,
LogLevel::Error,
LogLevel::Critical,
LogLevel::None,
];
for level in levels {
let layout = mgr.layout;
let log_data = new_guest_log_data(level);
let guest_log_data_buffer: Vec<u8> = log_data.clone().try_into().unwrap();
mgr.scratch_mem
.push_buffer(
layout.get_output_data_buffer_scratch_host_offset(),
sandbox_cfg.get_output_data_size(),
guest_log_data_buffer.as_slice(),
)
.unwrap();
outb_log(&mut mgr).unwrap();
LOGGER.test_log_records(|log_calls| {
let expected_level: Level = (&level).into();
assert!(
log_calls
.iter()
.filter(|log_call| {
log_call.level == expected_level
&& log_call.line == Some(log_data.line)
&& log_call.args == log_data.message
&& log_call.module_path == Some(log_data.source.clone())
&& log_call.file == Some(log_data.source_file.clone())
})
.count()
== 1,
"log call did not occur for level {:?}",
level.clone()
);
});
}
}
}
#[ignore]
#[test]
fn test_trace_outb_log() {
Logger::initialize_log_tracer();
rebuild_interest_cache();
let subscriber =
hyperlight_testing::tracing_subscriber::TracingSubscriber::new(tracing::Level::TRACE);
let sandbox_cfg = SandboxConfiguration::default();
tracing::subscriber::with_default(subscriber.clone(), || {
let new_mgr = || {
let bin = GuestBinary::FilePath(simple_guest_as_string().unwrap());
let snapshot =
crate::sandbox::snapshot::Snapshot::from_env(bin, sandbox_cfg).unwrap();
let mgr = SandboxMemoryManager::from_snapshot(&snapshot).unwrap();
let (hmgr, _) = mgr.build().unwrap();
hmgr
};
let levels = vec![
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Information,
LogLevel::Warning,
LogLevel::Error,
LogLevel::Critical,
LogLevel::None,
];
for level in levels {
let mut mgr = new_mgr();
let layout = mgr.layout;
let log_data: GuestLogData = new_guest_log_data(level);
subscriber.clear();
let guest_log_data_buffer: Vec<u8> = log_data.try_into().unwrap();
mgr.scratch_mem
.push_buffer(
layout.get_output_data_buffer_scratch_host_offset(),
sandbox_cfg.get_output_data_size(),
guest_log_data_buffer.as_slice(),
)
.unwrap();
subscriber.clear();
outb_log(&mut mgr).unwrap();
subscriber.test_trace_records(|spans, events| {
let expected_level = match level {
LogLevel::Trace => "TRACE",
LogLevel::Debug => "DEBUG",
LogLevel::Information => "INFO",
LogLevel::Warning => "WARN",
LogLevel::Error => "ERROR",
LogLevel::Critical => "ERROR",
LogLevel::None => "TRACE",
};
assert!(
spans.len() == 21,
"expected 21 spans, found {}",
spans.len()
);
let span_value = spans
.get(&1)
.unwrap()
.as_object()
.unwrap()
.get("span")
.unwrap()
.get("attributes")
.unwrap()
.as_object()
.unwrap()
.get("metadata")
.unwrap()
.as_object()
.unwrap();
test_value_as_str(span_value, "module_path", "hyperlight_host::sandbox::outb");
let expected_file = if cfg!(windows) {
"src\\hyperlight_host\\src\\sandbox\\outb.rs"
} else {
"src/hyperlight_host/src/sandbox/outb.rs"
};
test_value_as_str(span_value, "file", expected_file);
test_value_as_str(span_value, "target", "hyperlight_host::sandbox::outb");
let mut count_matching_events = 0;
for json_value in events {
let event_values = json_value.as_object().unwrap().get("event").unwrap();
let metadata_values_map =
event_values.get("metadata").unwrap().as_object().unwrap();
let event_values_map = event_values.as_object().unwrap();
test_value_as_str(metadata_values_map, "level", expected_level);
test_value_as_str(event_values_map, "log.file", "test source file");
test_value_as_str(event_values_map, "log.module_path", "test source");
test_value_as_str(event_values_map, "log.target", "hyperlight_guest");
count_matching_events += 1;
}
assert!(
count_matching_events == 1,
"trace log call did not occur for level {:?}",
level.clone()
);
});
}
});
}
}