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::{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::{
VirtualAllocEx, VirtualProtectEx, MEM_COMMIT, MEM_RESERVE, PAGE_NOACCESS,
PAGE_PROTECTION_FLAGS, PAGE_READWRITE,
};
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,
) -> Result<SurrogateProcess> {
let process_handle: HANDLE = self.process_receiver.recv()?.into();
let allocated_address = unsafe {
VirtualAllocEx(
process_handle,
Some(raw_source_address),
raw_size,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
)
};
if allocated_address.is_null() {
log_then_return!(
"VirtualAllocEx failed for mem address {:?}",
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(
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(
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, 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();
unsafe {
TerminateJobObject(handle, 0)
}
.expect("surrogate job objects were not all terminated error:");
}
}
lazy_static::lazy_static! {
static ref SURROGATE_PROCESSES_MANAGER: SurrogateProcessManager =
SurrogateProcessManager::new().unwrap();
}
#[instrument(err(Debug), skip_all, parent = Span::current(), level= "Trace")]
pub(crate) fn get_surrogate_process_manager() -> Result<&'static SurrogateProcessManager> {
Ok(&SURROGATE_PROCESSES_MANAGER)
}
#[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 embeded_file_sha = sha256::digest(exe.data.as_ref());
let file_on_disk_sha = sha256::try_digest(&p)?;
if embeded_file_sha != file_on_disk_sha {
println!(
"sha of embedded surrorate '{}' does not match sha of file on disk '{}' - deleting surrogate binary at {}",
embeded_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(),
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 rand::{thread_rng, Rng};
use serial_test::serial;
use windows::Win32::Foundation::BOOL;
use windows::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS,
};
use windows::Win32::System::JobObjects::IsProcessInJob;
use windows::Win32::System::Memory::{
VirtualAlloc, VirtualFree, MEM_COMMIT, MEM_RELEASE, MEM_RESERVE, PAGE_READWRITE,
};
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 = thread_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 allocated_address = unsafe {
VirtualAlloc(None, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)
};
let timer = Instant::now();
let surrogate_process = {
let res = surrogate_process_manager
.get_surrogate_process(size, allocated_address)?;
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, job_handle, &mut result).is_ok());
assert!(result.as_bool());
}
let n: u64 = rng.gen_range(1..16);
thread::sleep(Duration::from_millis(n));
drop(surrogate_process);
unsafe {
let res = VirtualFree(allocated_address, 0, MEM_RELEASE);
assert!(res.is_ok())
}
}
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)
.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());
}
}
}