use core::ffi::c_void;
use std::fs::File;
use std::io::Write;
use std::mem::size_of;
use std::path::{Path, PathBuf};
use crossbeam_channel::{unbounded, Receiver, Sender};
use hyperlight_common::mem::PAGE_SIZE_USIZE;
use rust_embed::RustEmbed;
use tracing::{error, info, instrument, Span};
use windows::core::{s, PCSTR};
use windows::Win32::Foundation::HANDLE;
use windows::Win32::Security::SECURITY_ATTRIBUTES;
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectA, JobObjectExtendedLimitInformation,
SetInformationJobObject, TerminateJobObject, JOBOBJECT_BASIC_LIMIT_INFORMATION,
JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
use windows::Win32::System::Memory::{
MapViewOfFileNuma2, VirtualProtectEx, PAGE_NOACCESS, PAGE_PROTECTION_FLAGS, PAGE_READWRITE,
};
use windows::Win32::System::SystemServices::NUMA_NO_PREFERRED_NODE;
use windows::Win32::System::Threading::{
CreateProcessA, CREATE_SUSPENDED, PROCESS_INFORMATION, STARTUPINFOA,
};
use super::surrogate_process::SurrogateProcess;
use super::wrappers::{HandleWrapper, PSTRWrapper};
use crate::HyperlightError::WindowsAPIError;
use crate::{log_then_return, new_error, Result};
#[derive(RustEmbed)]
#[folder = "$HYPERLIGHT_SURROGATE_DIR"]
#[include = "hyperlight_surrogate.exe"]
struct Asset;
pub(crate) const SURROGATE_PROCESS_BINARY_NAME: &str = "hyperlight_surrogate.exe";
const NUMBER_OF_SURROGATE_PROCESSES: usize = 512;
pub(crate) struct SurrogateProcessManager {
job_handle: HandleWrapper,
process_receiver: Receiver<HandleWrapper>,
process_sender: Sender<HandleWrapper>,
}
impl SurrogateProcessManager {
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
fn new() -> Result<Self> {
ensure_surrogate_process_exe()?;
let surrogate_process_path =
get_surrogate_process_dir()?.join(SURROGATE_PROCESS_BINARY_NAME);
let (sender, receiver) = unbounded();
let job_handle = create_job_object()?;
let surrogate_process_manager = SurrogateProcessManager {
job_handle,
process_receiver: receiver,
process_sender: sender,
};
surrogate_process_manager
.create_surrogate_processes(&surrogate_process_path, job_handle)?;
Ok(surrogate_process_manager)
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(super) fn get_surrogate_process(
&self,
raw_size: usize,
raw_source_address: *const c_void,
mmap_file_handle: HandleWrapper,
) -> Result<SurrogateProcess> {
let surrogate_process_handle: HANDLE = self.process_receiver.recv()?.into();
let mapping_file_handle: HANDLE = mmap_file_handle.into();
let allocated_address = unsafe {
MapViewOfFileNuma2(
mapping_file_handle,
surrogate_process_handle,
0,
Some(raw_source_address),
raw_size,
0,
PAGE_READWRITE.0,
NUMA_NO_PREFERRED_NODE,
)
};
if allocated_address.Value.is_null() {
log_then_return!(
"MapViewOfFileNuma2 failed for mem address {:?}",
raw_source_address
);
}
if allocated_address.Value as *const c_void != raw_source_address {
log_then_return!(
"Address Mismatch Allocated: {:?} Requested: {:?}",
allocated_address.Value,
raw_source_address
);
}
let mut unused_out_old_prot_flags = PAGE_PROTECTION_FLAGS(0);
let first_guard_page_start = raw_source_address;
if let Err(e) = unsafe {
VirtualProtectEx(
surrogate_process_handle,
first_guard_page_start,
PAGE_SIZE_USIZE,
PAGE_NOACCESS,
&mut unused_out_old_prot_flags,
)
} {
log_then_return!(WindowsAPIError(e.clone()));
}
let last_guard_page_start = unsafe { raw_source_address.add(raw_size - PAGE_SIZE_USIZE) };
if let Err(e) = unsafe {
VirtualProtectEx(
surrogate_process_handle,
last_guard_page_start,
PAGE_SIZE_USIZE,
PAGE_NOACCESS,
&mut unused_out_old_prot_flags,
)
} {
log_then_return!(WindowsAPIError(e.clone()));
}
Ok(SurrogateProcess::new(
allocated_address.Value,
surrogate_process_handle,
))
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(super) fn return_surrogate_process(&self, proc_handle: HandleWrapper) -> Result<()> {
Ok(self.process_sender.clone().send(proc_handle)?)
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
fn create_surrogate_processes(
&self,
surrogate_process_path: &Path,
job_handle: HandleWrapper,
) -> Result<()> {
for _ in 0..NUMBER_OF_SURROGATE_PROCESSES {
let surrogate_process = create_surrogate_process(surrogate_process_path, job_handle)?;
self.process_sender.clone().send(surrogate_process)?;
}
Ok(())
}
}
impl Drop for SurrogateProcessManager {
#[instrument(skip_all, parent = Span::current(), level= "Trace")]
fn drop(&mut self) {
let handle: HANDLE = self.job_handle.into();
if unsafe {
TerminateJobObject(handle, 0)
}
.is_err()
{
error!("surrogate job objects were not all terminated");
}
}
}
lazy_static::lazy_static! {
static ref SURROGATE_PROCESSES_MANAGER: std::result::Result<SurrogateProcessManager, &'static str> =
match SurrogateProcessManager::new() {
Ok(manager) => Ok(manager),
Err(e) => {
error!("Failed to create SurrogateProcessManager: {:?}", e);
Err("Failed to create SurrogateProcessManager")
}
};
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn get_surrogate_process_manager() -> Result<&'static SurrogateProcessManager> {
match &*SURROGATE_PROCESSES_MANAGER {
Ok(manager) => Ok(manager),
Err(e) => {
error!("Failed to get SurrogateProcessManager: {:?}", e);
Err(new_error!("Failed to get SurrogateProcessManager {}", e))
}
}
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
fn create_job_object() -> Result<HandleWrapper> {
let security_attributes: SECURITY_ATTRIBUTES = Default::default();
let job_object = unsafe {
CreateJobObjectA(
Some(&security_attributes),
s!("HyperlightSurrogateJobObject"),
)?
};
let mut job_object_information = JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
BasicLimitInformation: JOBOBJECT_BASIC_LIMIT_INFORMATION {
LimitFlags: JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
..Default::default()
},
..Default::default()
};
let job_object_information_ptr: *mut c_void =
&mut job_object_information as *mut _ as *mut c_void;
if let Err(e) = unsafe {
SetInformationJobObject(
job_object,
JobObjectExtendedLimitInformation,
job_object_information_ptr,
size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
} {
log_then_return!(WindowsAPIError(e.clone()));
}
Ok(job_object.into())
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
fn get_surrogate_process_dir() -> Result<PathBuf> {
let binding = std::env::current_exe()?;
let path = binding
.parent()
.ok_or_else(|| new_error!("could not get parent directory of current executable"))?;
Ok(path.to_path_buf())
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
fn ensure_surrogate_process_exe() -> Result<()> {
let surrogate_process_path = get_surrogate_process_dir()?.join(SURROGATE_PROCESS_BINARY_NAME);
let p = Path::new(&surrogate_process_path);
let exe = Asset::get(SURROGATE_PROCESS_BINARY_NAME)
.ok_or_else(|| new_error!("could not find embedded surrogate binary"))?;
if p.exists() {
let embedded_file_sha = sha256::digest(exe.data.as_ref());
let file_on_disk_sha = sha256::try_digest(&p)?;
if embedded_file_sha != file_on_disk_sha {
println!(
"sha of embedded surrogate '{}' does not match sha of file on disk '{}' - deleting surrogate binary at {}",
embedded_file_sha,
file_on_disk_sha,
&surrogate_process_path.display()
);
std::fs::remove_file(p)?;
}
}
if !p.exists() {
info!(
"{} does not exist, copying to {}",
SURROGATE_PROCESS_BINARY_NAME,
&surrogate_process_path.display()
);
let mut f = File::create(&surrogate_process_path)?;
f.write_all(exe.data.as_ref())?;
}
Ok(())
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
fn create_surrogate_process(
surrogate_process_path: &Path,
job_handle: HandleWrapper,
) -> Result<HandleWrapper> {
let mut process_information: PROCESS_INFORMATION = unsafe { std::mem::zeroed() };
let mut startup_info: STARTUPINFOA = unsafe { std::mem::zeroed() };
let process_attributes: SECURITY_ATTRIBUTES = Default::default();
let thread_attributes: SECURITY_ATTRIBUTES = Default::default();
startup_info.cb = std::mem::size_of::<STARTUPINFOA>() as u32;
let cmd_line = surrogate_process_path.to_str().ok_or(new_error!(
"failed to convert surrogate process path to a string"
))?;
let p_cmd_line = &PSTRWrapper::try_from(cmd_line)?;
if let Err(e) = unsafe {
CreateProcessA(
PCSTR::null(),
Some(p_cmd_line.into()),
Some(&process_attributes),
Some(&thread_attributes),
false,
CREATE_SUSPENDED,
None,
None,
&startup_info,
&mut process_information,
)
} {
log_then_return!(WindowsAPIError(e.clone()));
}
let job_handle: HANDLE = job_handle.into();
let process_handle: HANDLE = process_information.hProcess;
unsafe {
if let Err(e) = AssignProcessToJobObject(job_handle, process_handle) {
log_then_return!(WindowsAPIError(e.clone()));
}
}
Ok(process_handle.into())
}
#[cfg(test)]
mod tests {
use std::ffi::CStr;
use std::thread;
use std::time::{Duration, Instant};
use hyperlight_common::mem::PAGE_SIZE_USIZE;
use rand::{rng, Rng};
use serial_test::serial;
use windows::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE};
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS,
};
use windows::Win32::System::JobObjects::IsProcessInJob;
use windows::Win32::System::Memory::{
CreateFileMappingA, MapViewOfFile, UnmapViewOfFile, FILE_MAP_ALL_ACCESS, PAGE_READWRITE,
SEC_COMMIT,
};
use windows_result::BOOL;
use super::*;
use crate::mem::shared_mem::{ExclusiveSharedMemory, SharedMemory};
#[test]
#[serial]
fn test_surrogate_process_manager() {
let mut threads = Vec::new();
for t in 0..NUMBER_OF_SURROGATE_PROCESSES * 2 {
let thread_handle = thread::spawn(move || -> Result<()> {
let surrogate_process_manager_res = get_surrogate_process_manager();
let mut rng = rng();
let size = PAGE_SIZE_USIZE * 3;
assert!(surrogate_process_manager_res.is_ok());
let surrogate_process_manager = surrogate_process_manager_res.unwrap();
let job_handle = surrogate_process_manager.job_handle;
for p in 0..NUMBER_OF_SURROGATE_PROCESSES {
let dwmaximumsizehigh = 0;
let dwmaximumsizelow = (size & 0xFFFFFFFF) as u32;
let handle = unsafe {
CreateFileMappingA(
INVALID_HANDLE_VALUE, None,
PAGE_READWRITE | SEC_COMMIT,
dwmaximumsizehigh,
dwmaximumsizelow,
PCSTR::null(),
)
.unwrap()
};
let addr = unsafe { MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0) };
let timer = Instant::now();
let surrogate_process = {
let res = surrogate_process_manager.get_surrogate_process(
size,
addr.Value,
HandleWrapper::from(handle),
)?;
let elapsed = timer.elapsed();
if (elapsed.as_millis() as u64) > 150 {
println!("Get Process Time Thread {} Process {}: {:?}", t, p, elapsed);
}
res
};
let mut result: BOOL = Default::default();
let process_handle: HANDLE = surrogate_process.process_handle.into();
let job_handle: HANDLE = job_handle.into();
unsafe {
assert!(
IsProcessInJob(process_handle, Some(job_handle), &mut result).is_ok()
);
assert!(result.as_bool());
}
let n: u64 = rng.random_range(1..16);
thread::sleep(Duration::from_millis(n));
drop(surrogate_process);
let res = unsafe { UnmapViewOfFile(addr) };
assert!(res.is_ok(), "Failed to UnmapViewOfFile: {:?}", res.err());
let res = unsafe { CloseHandle(handle) };
assert!(res.is_ok(), "Failed to CloseHandle: {:?}", res.err());
}
Ok(())
});
threads.push(thread_handle);
}
for thread_handle in threads {
assert!(thread_handle.join().is_ok());
}
assert_number_of_surrogate_processes(NUMBER_OF_SURROGATE_PROCESSES);
}
#[track_caller]
fn assert_number_of_surrogate_processes(expected_count: usize) {
let sleep_count = 10;
loop {
let snapshot_handle = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
assert!(snapshot_handle.is_ok());
let snapshot_handle = snapshot_handle.unwrap();
let mut process_entry = PROCESSENTRY32 {
dwSize: size_of::<PROCESSENTRY32>() as u32,
..Default::default()
};
let mut result = unsafe { Process32First(snapshot_handle, &mut process_entry).is_ok() };
let mut count = 0;
while result {
if let Ok(process_name) =
unsafe { CStr::from_ptr(process_entry.szExeFile.as_ptr()).to_str() }
{
if process_name == SURROGATE_PROCESS_BINARY_NAME {
count += 1;
}
}
unsafe {
result = Process32Next(snapshot_handle, &mut process_entry).is_ok();
}
}
if (expected_count == 0) && (count > 0) && (sleep_count < 30) {
thread::sleep(Duration::from_secs(1));
} else {
assert_eq!(count, expected_count);
break;
}
}
}
#[test]
fn windows_guard_page() {
const SIZE: usize = 4096;
let mgr = get_surrogate_process_manager().unwrap();
let mem = ExclusiveSharedMemory::new(SIZE).unwrap();
let process = mgr
.get_surrogate_process(
mem.raw_mem_size(),
mem.raw_ptr() as *mut c_void,
HandleWrapper::from(mem.get_mmap_file_handle()),
)
.unwrap();
let buffer = vec![0u8; SIZE];
let bytes_read: Option<*mut usize> = None;
let process_handle: HANDLE = process.process_handle.into();
unsafe {
let success = windows::Win32::System::Diagnostics::Debug::ReadProcessMemory(
process_handle,
process.allocated_address,
buffer.as_ptr() as *mut c_void,
SIZE,
bytes_read,
);
assert!(success.is_err());
let success = windows::Win32::System::Diagnostics::Debug::ReadProcessMemory(
process_handle,
process.allocated_address.add(SIZE),
buffer.as_ptr() as *mut c_void,
SIZE,
bytes_read,
);
assert!(success.is_ok());
let success = windows::Win32::System::Diagnostics::Debug::ReadProcessMemory(
process_handle,
process.allocated_address.add(2 * SIZE),
buffer.as_ptr() as *mut c_void,
SIZE,
bytes_read,
);
assert!(success.is_err());
}
}
}