Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Fixed
* Fix restore on WHP not rewinding the guest TSC on hosts with `TSC_ADJUST` support.
* HVF sandbox initialization accepts seeds that match Apple hypercall identifiers.
* Host-written snapshot memory has instruction-cache coherence before guest execution.

## [v0.19.0] - 2026-11-04

Expand Down
59 changes: 57 additions & 2 deletions src/hyperlight_host/src/hypervisor/virtual_machine/hvf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -764,14 +764,15 @@ struct TlbiRegion {
insn_memory: ReadonlySharedMemory,
}
impl TlbiRegion {
/// Prepare the TLB-flush instructions for execution on any vCPU.
fn new() -> Result<Self, SharedMemoryError> {
let mut bytes = vec![0; page_size::get()];
bytes[0..20].copy_from_slice(&[
0x9f, 0x3b, 0x03, 0xd5, // dsb ish
0x1f, 0x87, 0x08, 0xd5, // tlbi vmalle1
0x9f, 0x3b, 0x03, 0xd5, // dsb ish
0xdf, 0x3f, 0x03, 0xd5, // isb sy
0x02, 0x00, 0x00, 0xd4, // hvc #0
0x82, 0x09, 0x09, 0xd4, // hvc #0x484c ('HL'), outside Apple's HVC #0 ABI
]);
Ok(Self {
insn_memory: ReadonlySharedMemory::from_bytes(&bytes, page_size::get())?,
Expand Down Expand Up @@ -922,7 +923,7 @@ impl LoadedMemorySpace {
use bindings::hv_exit_reason_t;
match exit.reason {
hv_exit_reason_t::HV_EXIT_REASON_EXCEPTION
if exit.exception.syndrome.0 == 0x5a000000 => {} // hvc #0 is the success case
if exit.exception.syndrome.0 == 0x5a00484c => {} // hvc #0x484c is the success case
hv_exit_reason_t::HV_EXIT_REASON_CANCELED => {
return Err(TimeoutableError::Timeout);
}
Expand Down Expand Up @@ -1234,3 +1235,57 @@ impl VirtualMachine for HvfVm {
Ok(())
}
}

#[cfg(test)]
mod tests {
use std::time::Instant;

use super::*;
use crate::hypervisor::HvfInterruptHandle;
use crate::sandbox::SandboxConfiguration;

#[test]
fn tlbi_does_not_invoke_apple_hypercalls() {
let config = SandboxConfiguration::default();
let interrupt_handle =
Arc::new(HvfInterruptHandle::new(config.get_interrupt_retry_delay()));
let _vm = HvfVm::new(interrupt_handle).unwrap();
let deadline = Instant::now() + Duration::from_secs(60);

let mut cpu = loop {
match HvfCpu::new() {
Ok((cpu, _)) => break cpu,
Err(TimeoutableError::Timeout) => {
assert!(
Instant::now() < deadline,
"timed out waiting for an HVF vCPU"
);
}
Err(TimeoutableError::Error(error)) => panic!("creating HVF vCPU: {error}"),
}
};

// Apple PAC_NOP and OEM UID hypercalls.
for x0 in [0xc100_00f0, 0x8300_ff01] {
cpu.hv_vcpu_set_reg(bindings::hv_reg_t::HV_REG_X0, x0)
.unwrap();
let space = MemorySpace::new();
let mut loaded = CURRENT_LOADED_MEMORY_SPACE.write();

loop {
match loaded.sync_space(&space, &mut cpu) {
Ok(()) => break,
Err(TimeoutableError::Timeout) => {
assert!(Instant::now() < deadline, "timed out flushing the TLB");
}
Err(TimeoutableError::Error(error)) => panic!("flushing the TLB: {error}"),
}
}

assert_eq!(
cpu.hv_vcpu_get_reg(bindings::hv_reg_t::HV_REG_X0).unwrap(),
x0
);
}
}
}
59 changes: 53 additions & 6 deletions src/hyperlight_host/src/mem/shared_mem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,6 +604,49 @@ pub trait SharedMemory {
}
}

/// Prepare host-written code in a guest-visible prefix for execution.
///
/// - Linux x86-64 relies on hardware instruction/data-cache coherence for KVM and MSHV.
/// - On ARM64, KVM cleans the data cache as needed when installing stage-2 mappings.
/// It invalidates the instruction cache before installing an executable mapping or
/// adding execute permission to an existing mapping.
///
/// See Linux:
/// -[stage-2 page-table code](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/arm64/kvm/hyp/pgtable.c)
/// -[cache helpers](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/arm64/include/asm/kvm_mmu.h).
fn sync_instr_cache(memory: &impl SharedMemory, size: usize) -> Result<()> {
bounds_check!(0usize, size, memory.mem_size());

if size == 0 {
return Ok(());
}

#[cfg(target_os = "macos")]
{
unsafe extern "C" {
fn sys_icache_invalidate(start: *mut c_void, len: usize);
}

// SAFETY: The checked range is within a live host mapping.
unsafe {
sys_icache_invalidate(memory.base_ptr().cast(), size);
}
}

#[cfg(target_os = "windows")]
{
use windows::Win32::System::Diagnostics::Debug::FlushInstructionCache;
use windows::Win32::System::Threading::GetCurrentProcess;

// SAFETY: The checked range belongs to the current process and its mapping is live.
unsafe {
FlushInstructionCache(GetCurrentProcess(), Some(memory.base_ptr().cast()), size)?;
}
}

Ok(())
}

fn mapping_at(
s: &impl SharedMemory,
gpa: u64,
Expand Down Expand Up @@ -968,6 +1011,8 @@ impl ExclusiveSharedMemory {
return Err(SharedMemoryError::SharedMemoryNotExclusive);
}

sync_instr_cache(&self, guest_mapped_size)?;

// This is the only Arc left. To make another one you have to clone this
// one, and it lives in `self`, which this function owns.
Ok(ReadonlySharedMemory {
Expand Down Expand Up @@ -1573,10 +1618,7 @@ impl ReadonlySharedMemory {
let mut anon =
ExclusiveSharedMemory::new(contents.len().next_multiple_of(page_size::get()))?;
anon.copy_from_slice(contents, 0)?;
Ok(ReadonlySharedMemory {
region: anon.region,
guest_mapped_size,
})
anon.freeze(guest_mapped_size)
}

/// The number of bytes that should be mapped into guest PA space.
Expand Down Expand Up @@ -1616,10 +1658,13 @@ impl ReadonlySharedMemory {
}

let region = Self::map_file(file, len)?;
Ok(ReadonlySharedMemory {
let memory = Self {
region,
guest_mapped_size,
})
};
sync_instr_cache(&memory, guest_mapped_size)?;

Ok(memory)
}

/// Linux: reserve `[guard][blob][guard]` as one anonymous
Expand Down Expand Up @@ -1778,6 +1823,8 @@ impl ReadonlySharedMemory {
pub(crate) fn copy_to_writable(&self) -> Result<ExclusiveSharedMemory> {
let mut writable = ExclusiveSharedMemory::new(self.guest_mapped_size)?;
writable.copy_from_slice(&self.as_slice()[..self.guest_mapped_size], 0)?;
sync_instr_cache(&writable, self.guest_mapped_size)?;

Ok(writable)
}

Expand Down
Loading