use std::marker::PhantomData;
use std::ptr;
use std::sync::{Arc, Mutex, OnceLock};
use applevisor_sys::*;
use crate::error::*;
#[cfg(feature = "macos-15-0")]
use crate::gic::*;
use crate::hv_unsafe_call;
use crate::memory::*;
use crate::vcpu::*;
#[cfg(feature = "macos-26-0")]
pub type IpaGranule = hv_ipa_granule_t;
#[derive(Debug)]
#[cfg(feature = "macos-13-0")]
pub struct VirtualMachineConfig(hv_vm_config_t);
#[cfg(feature = "macos-13-0")]
impl Default for VirtualMachineConfig {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "macos-13-0")]
impl Drop for VirtualMachineConfig {
fn drop(&mut self) {
unsafe { os_release(self.0) }
}
}
#[cfg(feature = "macos-13-0")]
impl VirtualMachineConfig {
pub fn new() -> Self {
Self(unsafe { hv_vm_config_create() })
}
pub fn get_max_ipa_size() -> Result<u32> {
let mut ipa_bit_length = 0;
hv_unsafe_call!(hv_vm_config_get_max_ipa_size(&mut ipa_bit_length))?;
Ok(ipa_bit_length)
}
pub fn get_default_ipa_size() -> Result<u32> {
let mut ipa_bit_length = 0;
hv_unsafe_call!(hv_vm_config_get_default_ipa_size(&mut ipa_bit_length))?;
Ok(ipa_bit_length)
}
pub fn get_ipa_size(&self) -> Result<u32> {
let mut ipa_bit_length = 0;
hv_unsafe_call!(hv_vm_config_get_ipa_size(self.0, &mut ipa_bit_length))?;
Ok(ipa_bit_length)
}
pub fn set_ipa_size(&mut self, ipa_bit_length: u32) -> Result<()> {
hv_unsafe_call!(hv_vm_config_set_ipa_size(self.0, ipa_bit_length))
}
#[cfg(feature = "macos-15-0")]
pub fn get_el2_supported() -> Result<bool> {
let mut el2_supported = false;
hv_unsafe_call!(hv_vm_config_get_el2_supported(&mut el2_supported))?;
Ok(el2_supported)
}
#[cfg(feature = "macos-15-0")]
pub fn get_el2_enabled(&self) -> Result<bool> {
let mut el2_enabled = false;
hv_unsafe_call!(hv_vm_config_get_el2_enabled(self.0, &mut el2_enabled))?;
Ok(el2_enabled)
}
#[cfg(feature = "macos-15-0")]
pub fn set_el2_enabled(&mut self, el2_enabled: bool) -> Result<()> {
hv_unsafe_call!(hv_vm_config_set_el2_enabled(self.0, el2_enabled))?;
Ok(())
}
#[cfg(feature = "macos-15-2")]
pub fn get_max_svl_bytes() -> Result<usize> {
let mut value = 0;
hv_unsafe_call!(hv_sme_config_get_max_svl_bytes(&mut value))?;
Ok(value)
}
#[cfg(feature = "macos-26-0")]
pub fn get_default_ipa_granule() -> Result<IpaGranule> {
let mut value = IpaGranule::HV_IPA_GRANULE_4KB;
hv_unsafe_call!(hv_vm_config_get_default_ipa_granule(&mut value))?;
Ok(value)
}
#[cfg(feature = "macos-26-0")]
pub fn get_ipa_granule(&self) -> Result<IpaGranule> {
let mut value = IpaGranule::HV_IPA_GRANULE_4KB;
hv_unsafe_call!(hv_vm_config_get_ipa_granule(self.0, &mut value))?;
Ok(value)
}
#[cfg(feature = "macos-26-0")]
pub fn set_ipa_granule(&mut self, granule: IpaGranule) -> Result<()> {
hv_unsafe_call!(hv_vm_config_set_ipa_granule(self.0, granule))?;
Ok(())
}
}
pub struct VirtualMachine;
impl VirtualMachine {
#[allow(clippy::new_ret_no_self)]
pub fn new() -> Result<VirtualMachineInstance<GicDisabled>> {
hv_unsafe_call!(hv_vm_create(ptr::null_mut()))?;
Ok(VirtualMachineInstance::<GicDisabled> {
_guard: Some(Arc::new(())),
_phantom: PhantomData,
})
}
#[cfg(feature = "macos-13-0")]
pub fn with_config(
config: VirtualMachineConfig,
) -> Result<VirtualMachineInstance<GicDisabled>> {
hv_unsafe_call!(hv_vm_create(config.0))?;
Ok(VirtualMachineInstance::<GicDisabled> {
_guard: Some(Arc::new(())),
_phantom: PhantomData,
})
}
#[cfg(feature = "macos-15-0")]
pub fn with_gic(
vm_config: VirtualMachineConfig,
gic_config: GicConfig,
) -> Result<VirtualMachineInstance<GicEnabled>> {
let vm = Self::with_config(vm_config)?;
hv_unsafe_call!(hv_gic_create(gic_config.0))?;
Ok(VirtualMachineInstance::<GicEnabled> {
_guard: vm._guard.clone(),
_phantom: PhantomData,
})
}
}
#[derive(Clone, Debug)]
#[cfg(feature = "macos-15-0")]
pub struct GicEnabled;
#[derive(Clone, Debug)]
pub struct GicDisabled;
#[derive(Clone, Debug)]
pub struct VirtualMachineInstance<Gic> {
pub(crate) _guard: Option<Arc<()>>,
_phantom: PhantomData<Gic>,
}
impl<Gic> std::ops::Drop for VirtualMachineInstance<Gic> {
fn drop(&mut self) {
let guard = self._guard.take().unwrap();
if Arc::into_inner(guard).is_some() {
let _ = hv_unsafe_call!(hv_vm_destroy());
}
}
}
impl<Gic> VirtualMachineInstance<Gic> {
pub fn vcpu_create(&self) -> Result<Vcpu> {
self.vcpu_with_config(VcpuConfig(ptr::null_mut()))
}
pub fn vcpu_with_config(&self, config: VcpuConfig) -> Result<Vcpu> {
let mut vcpu = 0;
let mut exit = ptr::null_mut() as *const hv_vcpu_exit_t;
hv_unsafe_call!(hv_vcpu_create(&mut vcpu, &mut exit, config.0))?;
Ok(Vcpu {
vcpu,
exit,
_guard_self: Arc::new(()),
_guard_vm: Arc::clone(self._guard.as_ref().unwrap()),
_phantom: PhantomData,
})
}
pub fn vcpus_exit(&self, vcpus: &[VcpuHandle]) -> Result<()> {
let mut guards = Vec::with_capacity(vcpus.len());
let mut ids = Vec::with_capacity(vcpus.len());
for vcpu in vcpus {
if let Some(strong) = vcpu.take_ref() {
ids.push(vcpu.id());
guards.push(strong);
}
}
hv_unsafe_call!(hv_vcpus_exit(ids.as_ptr(), ids.len() as u32))?;
Ok(())
}
pub fn memory_create(&self, size: usize) -> Result<Memory> {
let host_alloc = MemAlloc::new(size)?;
Ok(Memory {
host_alloc,
guest_addr: None,
_guard_vm: Arc::clone(self._guard.as_ref().unwrap()),
})
}
}
#[cfg(feature = "macos-15-0")]
impl From<VirtualMachineInstance<GicEnabled>> for VirtualMachineInstance<GicDisabled> {
fn from(value: VirtualMachineInstance<GicEnabled>) -> Self {
VirtualMachineInstance::<GicDisabled> {
_guard: value._guard.clone(),
_phantom: PhantomData,
}
}
}
#[cfg(not(test))]
static _VM_INSTANCE: OnceLock<VirtualMachineStaticInstance> = OnceLock::new();
#[cfg(test)]
static mut _VM_INSTANCE: OnceLock<VirtualMachineStaticInstance> = OnceLock::new();
static _VM_INIT_LOCK: Mutex<()> = Mutex::new(());
macro_rules! vm_static_instance {
() => {{
#[cfg(test)]
unsafe {
let ptr: *const OnceLock<VirtualMachineStaticInstance> = &raw const _VM_INSTANCE;
&*ptr
}
#[cfg(not(test))]
&_VM_INSTANCE
}};
}
#[cfg(test)]
pub(crate) fn vm_static_instance_reset() {
unsafe {
let ptr = &mut *&raw mut _VM_INSTANCE;
std::ptr::drop_in_place(ptr);
std::ptr::write(
ptr as *mut OnceLock<VirtualMachineStaticInstance>,
OnceLock::new(),
);
}
}
#[derive(Debug)]
pub enum VirtualMachineStaticInstance {
NoGic(VirtualMachineInstance<GicDisabled>),
#[cfg(feature = "macos-15-0")]
Gic(VirtualMachineInstance<GicEnabled>),
}
impl VirtualMachineStaticInstance {
pub fn init() -> Result<()> {
if vm_static_instance!().get().is_some() {
return Ok(());
}
let _guard = _VM_INIT_LOCK.lock().unwrap();
if vm_static_instance!().get().is_some() {
return Ok(());
}
let vm = VirtualMachine::new()?;
vm_static_instance!()
.set(VirtualMachineStaticInstance::NoGic(vm))
.unwrap();
Ok(())
}
#[cfg(feature = "macos-13-0")]
pub fn init_with_config(vm_config: VirtualMachineConfig) -> Result<()> {
if vm_static_instance!().get().is_some() {
return Ok(());
}
let _guard = _VM_INIT_LOCK.lock().unwrap();
if vm_static_instance!().get().is_some() {
return Ok(());
}
let vm = VirtualMachine::with_config(vm_config)?;
vm_static_instance!()
.set(VirtualMachineStaticInstance::NoGic(vm))
.unwrap();
Ok(())
}
#[cfg(feature = "macos-15-0")]
pub fn init_with_gic(vm_config: VirtualMachineConfig, gic_config: GicConfig) -> Result<()> {
if vm_static_instance!().get().is_some() {
return Ok(());
}
let _guard = _VM_INIT_LOCK.lock().unwrap();
if vm_static_instance!().get().is_some() {
return Ok(());
}
let vm = VirtualMachine::with_gic(vm_config, gic_config)?;
vm_static_instance!()
.set(VirtualMachineStaticInstance::Gic(vm))
.unwrap();
Ok(())
}
pub fn get() -> Option<VirtualMachineInstance<GicDisabled>> {
match vm_static_instance!().get() {
Some(VirtualMachineStaticInstance::NoGic(vm)) => Some(vm.clone()),
#[cfg(feature = "macos-15-0")]
Some(VirtualMachineStaticInstance::Gic(vm)) => Some(Into::<
VirtualMachineInstance<GicDisabled>,
>::into(vm.clone())),
_ => None,
}
}
#[cfg(feature = "macos-15-0")]
pub fn get_gic() -> Option<VirtualMachineInstance<GicEnabled>> {
match vm_static_instance!().get() {
Some(VirtualMachineStaticInstance::Gic(vm)) => Some(vm.clone()),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use serial_test::*;
use std::sync::{mpsc, Barrier};
use std::thread;
use crate::memory::PAGE_SIZE;
use crate::next_mem_addr;
use super::*;
#[cfg(feature = "macos-13-0")]
#[test]
#[parallel]
fn config_ipa_size() {
let mut config = VirtualMachineConfig::default();
let max_ipa_size = VirtualMachineConfig::get_max_ipa_size();
assert!(max_ipa_size.is_ok());
let max_ipa_size = max_ipa_size.unwrap();
let default_ipa_size = VirtualMachineConfig::get_default_ipa_size();
assert!(default_ipa_size.is_ok());
let default_ipa_size = default_ipa_size.unwrap();
assert_eq!(config.set_ipa_size(0), Ok(()));
assert_eq!(config.get_ipa_size(), Ok(0));
assert_eq!(config.set_ipa_size(default_ipa_size - 1), Ok(()));
assert_eq!(config.get_ipa_size(), Ok(default_ipa_size - 1));
assert_eq!(config.set_ipa_size(default_ipa_size), Ok(()));
assert_eq!(config.get_ipa_size(), Ok(default_ipa_size));
assert_eq!(config.set_ipa_size(max_ipa_size - 1), Ok(()));
assert_eq!(config.get_ipa_size(), Ok(max_ipa_size - 1));
assert_eq!(config.set_ipa_size(max_ipa_size), Ok(()));
assert_eq!(config.get_ipa_size(), Ok(max_ipa_size));
assert_eq!(
config.set_ipa_size(max_ipa_size + 1),
Err(HypervisorError::Unsupported)
);
assert_eq!(
config.set_ipa_size(u32::MAX),
Err(HypervisorError::Unsupported)
);
}
#[cfg(feature = "macos-15-0")]
#[test]
#[parallel]
fn config_el2() {
let mut config = VirtualMachineConfig::default();
let el2_supported = VirtualMachineConfig::get_el2_supported();
assert!(el2_supported.is_ok());
assert_eq!(config.set_el2_enabled(false), Ok(()));
assert_eq!(config.get_el2_enabled(), Ok(false));
assert_eq!(config.set_el2_enabled(true), Ok(()));
assert_eq!(config.get_el2_enabled(), Ok(true));
}
#[cfg(feature = "macos-15-2")]
#[test]
#[parallel]
fn config_max_svl_bytes() {
let max_svl_bytes_status = match VirtualMachineConfig::get_max_svl_bytes() {
Ok(_) | Err(HypervisorError::Unsupported) => Ok(()),
Err(x) => Err(x),
};
assert!(max_svl_bytes_status.is_ok())
}
#[cfg(feature = "macos-26-0")]
#[test]
#[parallel]
fn config_ipa_granule() {
let mut config = VirtualMachineConfig::default();
let granule_size = VirtualMachineConfig::get_default_ipa_granule();
assert!(granule_size.is_ok());
assert_eq!(
config.set_ipa_granule(IpaGranule::HV_IPA_GRANULE_4KB),
Ok(())
);
assert_eq!(config.get_ipa_granule(), Ok(IpaGranule::HV_IPA_GRANULE_4KB));
assert_eq!(
config.set_ipa_granule(IpaGranule::HV_IPA_GRANULE_16KB),
Ok(())
);
assert_eq!(
config.get_ipa_granule(),
Ok(IpaGranule::HV_IPA_GRANULE_16KB)
);
}
#[test]
#[serial]
fn create_a_default_vm() {
vm_static_instance_reset();
let vm = VirtualMachine::new();
assert!(vm.is_ok());
}
#[test]
#[serial]
fn create_a_default_static_vm_instance() {
vm_static_instance_reset();
let ret = VirtualMachineStaticInstance::init();
assert!(ret.is_ok());
let vm = VirtualMachineStaticInstance::get();
assert!(vm.is_some());
}
#[cfg(feature = "macos-13-0")]
#[test]
#[serial]
fn create_a_vm_with_a_custom_config() {
vm_static_instance_reset();
let mut config = VirtualMachineConfig::default();
#[cfg(feature = "macos-15-0")]
{
if VirtualMachineConfig::get_el2_supported().unwrap() {
config.set_el2_enabled(true).unwrap();
}
}
let max_ipa_size = VirtualMachineConfig::get_max_ipa_size().unwrap();
config.set_ipa_size(max_ipa_size).unwrap();
let vm = VirtualMachine::with_config(config);
assert!(vm.is_ok());
}
#[cfg(feature = "macos-13-0")]
#[test]
#[serial]
fn create_a_static_vm_instance_with_a_custom_config() {
vm_static_instance_reset();
let mut config = VirtualMachineConfig::default();
#[cfg(feature = "macos-15-0")]
{
if VirtualMachineConfig::get_el2_supported().unwrap() {
config.set_el2_enabled(true).unwrap();
}
}
let max_ipa_size = VirtualMachineConfig::get_max_ipa_size().unwrap();
config.set_ipa_size(max_ipa_size).unwrap();
let ret = VirtualMachineStaticInstance::init_with_config(config);
assert!(ret.is_ok());
let vm = VirtualMachineStaticInstance::get();
assert!(vm.is_some());
}
#[cfg(feature = "macos-15-0")]
#[test]
#[serial]
fn create_a_vm_with_a_gic() {
vm_static_instance_reset();
{
let vm_config = VirtualMachineConfig::default();
let gic_config = GicConfig::new();
let vm = VirtualMachine::with_gic(vm_config, gic_config);
assert!(matches!(vm, Err(HypervisorError::BadArgument)));
}
{
let vm_config = VirtualMachineConfig::default();
let mut gic_config = GicConfig::new();
assert!(gic_config.set_redistributor_base(0x20000).is_ok());
let vm = VirtualMachine::with_gic(vm_config, gic_config);
assert!(vm.is_ok());
}
}
#[cfg(feature = "macos-15-0")]
#[test]
#[serial]
fn create_a_static_vm_instance_with_a_gic() {
vm_static_instance_reset();
let vm_config = VirtualMachineConfig::default();
let mut gic_config = GicConfig::new();
assert!(gic_config.set_redistributor_base(0x20000).is_ok());
let ret = VirtualMachineStaticInstance::init_with_gic(vm_config, gic_config);
assert!(ret.is_ok());
let vm = VirtualMachineStaticInstance::get();
assert!(vm.is_some());
}
#[test]
#[serial]
fn create_multiple_vm_instances_from_a_single_thread() {
vm_static_instance_reset();
{
let vm1 = VirtualMachine::new();
assert!(vm1.is_ok());
let vm2 = VirtualMachine::new();
assert!(matches!(vm2, Err(HypervisorError::Busy)));
let vm3 = vm1.clone();
drop(vm1);
assert!(vm3.is_ok());
let vm4 = VirtualMachine::new();
assert!(matches!(vm4, Err(HypervisorError::Busy)));
}
let vm5 = VirtualMachine::new();
assert!(vm5.is_ok());
}
#[test]
#[serial]
fn use_one_vm_instance_from_multiple_threads() {
vm_static_instance_reset();
let barrier = Barrier::new(2);
let vm = VirtualMachine::new().unwrap();
thread::scope(|s| {
let vm_thread = vm.clone();
s.spawn(|| {
let _ = vm_thread;
barrier.wait();
});
drop(vm);
let vm2 = VirtualMachine::new();
assert!(matches!(vm2, Err(HypervisorError::Busy)));
barrier.wait();
});
let vm3 = VirtualMachine::new();
assert!(vm3.is_ok());
}
#[test]
#[parallel]
fn create_a_vcpu_from_a_vm_instance() {
let _ = VirtualMachineStaticInstance::init();
let vm = VirtualMachineStaticInstance::get().unwrap();
let vcpu = vm.vcpu_create();
assert!(vcpu.is_ok());
}
#[test]
#[parallel]
fn create_a_vcpu_with_a_custom_config_from_a_vm_instance() {
let _ = VirtualMachineStaticInstance::init();
let vm = VirtualMachineStaticInstance::get().unwrap();
let vcpu_config = VcpuConfig::default();
let vcpu = vm.vcpu_with_config(vcpu_config);
assert!(vcpu.is_ok());
}
#[cfg(feature = "macos-15-0")]
#[test]
#[serial]
fn downgrade_a_gicenabled_vm_instance_to_a_gicdisabled_vm_instance() {
vm_static_instance_reset();
let vm_config = VirtualMachineConfig::default();
let mut gic_config = GicConfig::new();
assert!(gic_config.set_redistributor_base(0x20000).is_ok());
let vm = VirtualMachine::with_gic(vm_config, gic_config);
assert!(vm.is_ok());
let vm = vm.unwrap();
let vm_no_gic = Into::<VirtualMachineInstance<GicDisabled>>::into(vm);
let vcpu = vm_no_gic.vcpu_create();
assert!(vcpu.is_ok());
}
#[test]
#[parallel]
fn exit_running_vcpus() {
let _ = VirtualMachineStaticInstance::init();
let vm = VirtualMachineStaticInstance::get().unwrap();
let thread_count = 4;
let (tx, rx) = mpsc::channel();
thread::scope(|s| {
for _ in 0..thread_count {
let vm_thread = vm.clone();
let tx_thread = tx.clone();
s.spawn(move || {
let vcpu = vm_thread.vcpu_create().unwrap();
let addr = next_mem_addr();
let mut mem = vm_thread.memory_create(PAGE_SIZE).unwrap();
mem.map(addr, MemPerms::ReadWriteExec).unwrap();
mem.write_u32(addr, 0x14000000).unwrap();
vcpu.set_reg(Reg::PC, addr).unwrap();
let handle = vcpu.get_handle();
tx_thread.send(handle).unwrap();
vcpu.run().unwrap();
});
}
let mut handles = vec![];
for _ in 0..thread_count {
handles.push(rx.recv().unwrap());
}
vm.vcpus_exit(&handles).unwrap();
});
}
#[test]
#[serial]
fn making_sure_vm_static_instances_behave_correctly() {
vm_static_instance_reset();
assert!(VirtualMachineStaticInstance::get().is_none());
#[cfg(feature = "macos-15-0")]
assert!(VirtualMachineStaticInstance::get_gic().is_none());
assert_eq!(VirtualMachineStaticInstance::init(), Ok(()));
assert!(VirtualMachineStaticInstance::get().is_some());
#[cfg(feature = "macos-15-0")]
assert!(VirtualMachineStaticInstance::get_gic().is_none());
#[cfg(feature = "macos-15-0")]
{
vm_static_instance_reset();
let vm_config = VirtualMachineConfig::default();
let mut gic_config = GicConfig::new();
gic_config.set_redistributor_base(0x20000).unwrap();
assert_eq!(
VirtualMachineStaticInstance::init_with_gic(vm_config, gic_config),
Ok(())
);
assert!(VirtualMachineStaticInstance::get().is_some());
assert!(VirtualMachineStaticInstance::get_gic().is_some());
}
vm_static_instance_reset();
assert_eq!(VirtualMachineStaticInstance::init(), Ok(()));
assert_eq!(VirtualMachineStaticInstance::init(), Ok(()));
#[cfg(feature = "macos-13-0")]
{
let vm_config = VirtualMachineConfig::default();
assert_eq!(
VirtualMachineStaticInstance::init_with_config(vm_config),
Ok(())
);
}
#[cfg(feature = "macos-15-0")]
{
let vm_config = VirtualMachineConfig::default();
let mut gic_config = GicConfig::new();
gic_config.set_redistributor_base(0x20000).unwrap();
assert_eq!(
VirtualMachineStaticInstance::init_with_gic(vm_config, gic_config),
Ok(())
);
}
}
}