diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 541895eef..6d435cd09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,7 @@ jobs: key: custom-out-${{ runner.os }}-${{ github.job }}-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('**/litebox_syscall_rewriter/**/*.rs') }} - run: ./.github/tools/github_actions_run_cargo fmt - run: | - ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features --workspace --exclude litebox_runner_lvbs --exclude litebox_runner_snp --exclude litebox_runner_optee_on_linux_userland + ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features --workspace --exclude litebox_runner_lvbs --exclude litebox_runner_snp --exclude litebox_runner_optee_on_linux_userland --exclude litebox_runner_linux_on_macos_userland ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features -p litebox_runner_optee_on_linux_userland # We exclude `litebox_runner_lvbs` because it requires a custom target and nightly # features. `build_and_test_lvbs` covers it. @@ -76,7 +76,7 @@ jobs: # aren't included in nextest at the moment. See relevant discussion at # https://github.com/nextest-rs/nextest/issues/16 - name: Build documentation (fail on warnings) - run: ./.github/tools/github_actions_run_cargo doc --no-deps --all-features --document-private-items --workspace --exclude litebox_runner_lvbs --exclude litebox_runner_snp + run: ./.github/tools/github_actions_run_cargo doc --no-deps --all-features --document-private-items --workspace --exclude litebox_runner_lvbs --exclude litebox_runner_snp --exclude litebox_runner_linux_on_macos_userland build_and_test_arm64: name: Build and Test (AArch64) @@ -138,6 +138,42 @@ jobs: RUSTDOCFLAGS: -Dwarnings run: ./.github/tools/github_actions_run_cargo doc --no-deps --all-features --document-private-items $AARCH64_CRATES + build_and_test_macos_arm64: + name: Build and Test (macOS AArch64) + runs-on: macos-15 + env: + RUSTFLAGS: -Dwarnings + MACOS_AARCH64_CRATES: >- + -p dev_tests + -p litebox + -p litebox_common_linux + -p litebox_syscall_rewriter + -p litebox_platform_macos_userland + -p litebox_shim_linux + -p litebox_runner_linux_on_macos_userland + steps: + - name: Check out repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Verify AArch64 host + run: test "$(uname -m)" = arm64 + - name: Set up Rust + run: | + rustup toolchain install $(awk -F'"' '/channel/{print $2}' rust-toolchain.toml) --profile minimal --no-self-update --component rustfmt,clippy + - name: Set up Nextest + uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5 + with: + tool: nextest@${{ env.NEXTEST_VERSION }} + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 + - run: ./.github/tools/github_actions_run_cargo fmt + - run: ./.github/tools/github_actions_run_cargo clippy --all-targets --all-features $MACOS_AARCH64_CRATES + - run: ./.github/tools/github_actions_run_cargo build $MACOS_AARCH64_CRATES + - run: ./.github/tools/github_actions_run_cargo nextest $MACOS_AARCH64_CRATES + - run: ./.github/tools/github_actions_run_cargo test --doc $MACOS_AARCH64_CRATES + - name: Build documentation (fail on warnings) + env: + RUSTDOCFLAGS: -Dwarnings + run: ./.github/tools/github_actions_run_cargo doc --no-deps --all-features --document-private-items $MACOS_AARCH64_CRATES + build_and_test_lvbs: name: Build and Test LVBS runs-on: ubuntu-latest @@ -298,6 +334,9 @@ jobs: # - `litebox_platform_linux_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # + # - `litebox_platform_macos_userland` is allowed to have `std` access, + # since it is a purely-userland implementation. + # # - `litebox_platform_windows_userland` is allowed to have `std` access, # since it is a purely-userland implementation. # @@ -327,6 +366,9 @@ jobs: # - `litebox_platform_lvbs` has a custom target (`no_std`), so it does # not work with the current no_std checker. # + # - `litebox_runner_linux_on_macos_userland` is allowed to have `std` + # access since it loads files and runs LiteBox on a hosted platform. + # # - `litebox_runner_linux_on_windows_userland` is allowed to have `std` # access since it needs to actually access the file-system, pull in # relevant files, and then actually trigger LiteBox itself. @@ -386,7 +428,9 @@ jobs: -not -path './litebox_broker_userland/Cargo.toml' \ -not -path './litebox_egress_proxy/Cargo.toml' \ -not -path './litebox_platform_linux_userland/Cargo.toml' \ + -not -path './litebox_platform_macos_userland/Cargo.toml' \ -not -path './litebox_platform_windows_userland/Cargo.toml' \ + -not -path './litebox_runner_linux_on_macos_userland/Cargo.toml' \ -not -path './litebox_runner_linux_on_windows_userland/Cargo.toml' \ -not -path './litebox_runner_windows_on_linux_userland/Cargo.toml' \ -not -path './litebox_platform_lvbs/Cargo.toml' \ diff --git a/Cargo.lock b/Cargo.lock index 047290154..63f601525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1733,6 +1733,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "litebox_platform_macos_userland" +version = "0.1.0" +dependencies = [ + "bitflags", + "libc", + "litebox", + "litebox_common_linux", + "litebox_platform", + "litebox_syscall_rewriter", + "zerocopy", +] + +[[package]] +name = "litebox_runner_linux_on_macos_userland" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "litebox", + "litebox_common_linux", + "litebox_platform_macos_userland", + "litebox_shim_linux", + "litebox_util_log", + "tempfile", + "tracing-subscriber", +] + [[package]] name = "litebox_platform_windows_userland" version = "0.1.0" @@ -1918,6 +1946,7 @@ dependencies = [ "litebox_common_linux", "litebox_platform", "litebox_platform_linux_userland", + "litebox_platform_macos_userland", "litebox_platform_windows_userland", "litebox_syscall_rewriter", "litebox_util_log", diff --git a/Cargo.toml b/Cargo.toml index 6fc783dc0..7b7bd6eac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,9 +20,11 @@ members = [ "litebox_egress_proxy", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", + "litebox_platform_macos_userland", "litebox_platform_windows_userland", "litebox_platform_lvbs", "litebox_runner_linux_userland", + "litebox_runner_linux_on_macos_userland", "litebox_runner_linux_on_windows_userland", "litebox_runner_windows_on_linux_userland", "litebox_runner_windows_userland", @@ -61,6 +63,7 @@ default-members = [ "litebox_egress_proxy", "litebox_platform_linux_kernel", "litebox_platform_linux_userland", + "litebox_platform_macos_userland", "litebox_platform_windows_userland", "litebox_platform_lvbs", "litebox_runner_linux_userland", diff --git a/dev_tests/src/boilerplate.rs b/dev_tests/src/boilerplate.rs index 8239a5ce9..fe2b94f2d 100644 --- a/dev_tests/src/boilerplate.rs +++ b/dev_tests/src/boilerplate.rs @@ -133,6 +133,8 @@ const SKIP_FILES: &[&str] = &[ "LICENSE", "litebox_platform/src/sync/mutex.rs", "litebox_platform/src/sync/rwlock.rs", + "litebox_runner_linux_on_macos_userland/tests/test-bins/hello_world_dyn", + "litebox_runner_linux_on_macos_userland/tests/test-bins/ld-linux-aarch64.so.1", "litebox_runner_linux_on_windows_userland/tests/test-bins/hello_exec_nolibc", "litebox_runner_linux_on_windows_userland/tests/test-bins/hello_thread", "litebox_runner_linux_on_windows_userland/tests/test-bins/hello_thread_static", diff --git a/dev_tests/src/ratchet.rs b/dev_tests/src/ratchet.rs index a3d7c0e0f..3497a0ac0 100644 --- a/dev_tests/src/ratchet.rs +++ b/dev_tests/src/ratchet.rs @@ -14,6 +14,7 @@ fn ratchet_transmutes() -> Result<()> { ("dev_tests/", 2), ("litebox/", 8), ("litebox_platform_linux_userland/", 2), + ("litebox_platform_macos_userland/", 2), ], |file| { Ok(file @@ -41,6 +42,7 @@ fn ratchet_globals() -> Result<()> { ("litebox_platform/", 2), ("litebox_platform_linux_kernel/", 5), ("litebox_platform_linux_userland/", 5), + ("litebox_platform_macos_userland/", 5), ("litebox_platform_lvbs/", 21), ("litebox_platform_windows_userland/", 8), ("litebox_runner_lvbs/", 8), @@ -77,6 +79,7 @@ fn ratchet_maybe_uninit() -> Result<()> { ("litebox/", 1), ("litebox_broker_transport_linux_userland/", 3), ("litebox_platform_linux_userland/", 2), + ("litebox_platform_macos_userland/", 2), ], |file| { Ok(file diff --git a/litebox/src/mm/exception_table.rs b/litebox/src/mm/exception_table.rs index aa7bf081f..0822c4498 100644 --- a/litebox/src/mm/exception_table.rs +++ b/litebox/src/mm/exception_table.rs @@ -34,6 +34,14 @@ macro_rules! ex_table_section { }; } +#[cfg(target_os = "macos")] +macro_rules! ex_table_section { + () => { + // Regular data; retain entries even when the linker cannot see their users. + "__DATA,__ex_table,regular,no_dead_strip" + }; +} + macro_rules! ex_table_entry { ($start:tt, $stop:tt, $recover:tt) => { concat!( @@ -302,14 +310,17 @@ struct ExceptionTableEntry { /// Returns the exception table, found by linker-defined symbols marking the /// start and end of the section. -#[cfg(any(target_os = "linux", target_os = "none"))] +#[cfg(any(target_os = "linux", target_os = "none", target_os = "macos"))] fn exception_table() -> &'static [ExceptionTableEntry] { // SAFETY: the linker automatically defines these symbols when the section // is non-empty. unsafe extern "C" { - #[link_name = "__start_ex_table"] + // \x01 suppresses Mach-O's leading underscore. + #[cfg_attr(target_os = "macos", link_name = "\x01section$start$__DATA$__ex_table")] + #[cfg_attr(not(target_os = "macos"), link_name = "__start_ex_table")] static START_EX_TABLE: [ExceptionTableEntry; 0]; - #[link_name = "__stop_ex_table"] + #[cfg_attr(target_os = "macos", link_name = "\x01section$end$__DATA$__ex_table")] + #[cfg_attr(not(target_os = "macos"), link_name = "__stop_ex_table")] static STOP_EX_TABLE: [ExceptionTableEntry; 0]; } diff --git a/litebox/src/mm/linux.rs b/litebox/src/mm/linux.rs index 37a7965da..0599f7995 100644 --- a/litebox/src/mm/linux.rs +++ b/litebox/src/mm/linux.rs @@ -17,7 +17,12 @@ use crate::platform::page_mgmt::AllocationError; use crate::platform::page_mgmt::FixedAddressBehavior; use crate::platform::page_mgmt::MemoryRegionPermissions; -/// Page size in bytes +/// Host page size in bytes on Apple Silicon macOS. +#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +pub const PAGE_SIZE: usize = 16384; + +/// Page size in bytes. +#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] pub const PAGE_SIZE: usize = 4096; bitflags::bitflags! { @@ -660,6 +665,9 @@ impl + 'static, const ALIGN: usize> Vmem } { Ok(_) => {} Err(AllocationError::OutOfMemory) => return Err(VmemResizeError::OutOfMemory), + Err(AllocationError::PermissionDenied) => { + return Err(VmemResizeError::PermissionDenied); + } Err( AllocationError::AddressInUse | AllocationError::AddressInUseByPlatform @@ -979,6 +987,8 @@ pub(super) enum VmemResizeError { InvalidAddr { range: Range, addr: usize }, #[error("range {0:?} is already (partially) occupied")] RangeOccupied(Range), + #[error("requested page permissions are denied")] + PermissionDenied, #[error("out of memory")] OutOfMemory, } diff --git a/litebox/src/mm/mod.rs b/litebox/src/mm/mod.rs index 401de5b7c..119e6743a 100644 --- a/litebox/src/mm/mod.rs +++ b/litebox/src/mm/mod.rs @@ -442,6 +442,7 @@ where Err(linux::VmemResizeError::NotExist(_)) => Err(RemapError::AlreadyUnallocated), Err(linux::VmemResizeError::InvalidAddr { .. }) => Err(RemapError::AlreadyAllocated), Err(linux::VmemResizeError::OutOfMemory) => Err(RemapError::OutOfMemory), + Err(linux::VmemResizeError::PermissionDenied) => Err(RemapError::PermissionDenied), } } diff --git a/litebox/src/mm/tests.rs b/litebox/src/mm/tests.rs index 31ad722fa..9a0a95203 100644 --- a/litebox/src/mm/tests.rs +++ b/litebox/src/mm/tests.rs @@ -36,6 +36,10 @@ impl crate::platform::PageManagementProvider for DummyVmemBackend { const TASK_ADDR_MAX: usize = 0x7FFF_FFFF_F000; // (1 << 47) - PAGE_SIZE; #[cfg(all(target_arch = "aarch64", target_os = "linux"))] const TASK_ADDR_MAX: usize = 0xFFFF_FFFF_F000; // 48-bit VA space + #[cfg(all(target_arch = "aarch64", target_os = "macos"))] + const TASK_ADDR_MIN: usize = 0x1_0000; // Vmem unit-test bound + #[cfg(all(target_arch = "aarch64", target_os = "macos"))] + const TASK_ADDR_MAX: usize = 0x7FFF_FE00_0000; // MACH_VM_MAX_ADDRESS fn allocate_pages( &self, diff --git a/litebox/src/platform/page_mgmt.rs b/litebox/src/platform/page_mgmt.rs index a321ce7aa..b228faf94 100644 --- a/litebox/src/platform/page_mgmt.rs +++ b/litebox/src/platform/page_mgmt.rs @@ -112,6 +112,7 @@ pub trait PageManagementProvider: RawPointerProvider { ) .map_err(|e| match e { AllocationError::OutOfMemory => RemapError::OutOfMemory, + AllocationError::PermissionDenied => RemapError::PermissionDenied, AllocationError::AddressInUse | AllocationError::AddressInUseByPlatform => { RemapError::AlreadyAllocated } @@ -220,6 +221,8 @@ pub enum AllocationError { AboveMaxAddress, #[error("out of memory")] OutOfMemory, + #[error("requested page permissions are denied")] + PermissionDenied, #[error("provided fixed address range is in use")] AddressInUse, #[error("provided fixed address range is in use by the platform")] @@ -250,6 +253,8 @@ pub enum RemapError { Overlapping, #[error("provided new range is already allocated")] AlreadyAllocated, + #[error("requested page permissions are denied")] + PermissionDenied, #[error("out of memory")] OutOfMemory, } @@ -262,6 +267,12 @@ pub enum PermissionUpdateError { Unaligned, #[error("provided range contains unallocated pages")] Unallocated, + #[error("requested page permissions are denied")] + PermissionDenied, + #[error("out of memory")] + OutOfMemory, + #[error("platform failed to update page permissions")] + PlatformFailure, } /// Possible errors for [`PageManagementProvider::try_allocate_cow_pages`] diff --git a/litebox_common_linux/src/errno/mod.rs b/litebox_common_linux/src/errno/mod.rs index 9d57b82de..bbddd8257 100644 --- a/litebox_common_linux/src/errno/mod.rs +++ b/litebox_common_linux/src/errno/mod.rs @@ -238,6 +238,7 @@ impl From for Errno { litebox::platform::page_mgmt::AllocationError::Unaligned | litebox::platform::page_mgmt::AllocationError::AboveMaxAddress => Errno::EINVAL, litebox::platform::page_mgmt::AllocationError::BelowMinAddress => Errno::EPERM, + litebox::platform::page_mgmt::AllocationError::PermissionDenied => Errno::EACCES, litebox::platform::page_mgmt::AllocationError::OutOfMemory | litebox::platform::page_mgmt::AllocationError::AddressPartiallyInUse | litebox::platform::page_mgmt::AllocationError::AddressInUseByPlatform => { @@ -299,7 +300,8 @@ impl From for Errno { | litebox::platform::page_mgmt::RemapError::Overlapping => Errno::EINVAL, litebox::platform::page_mgmt::RemapError::AlreadyAllocated | litebox::platform::page_mgmt::RemapError::AlreadyUnallocated => Errno::EFAULT, - litebox::platform::page_mgmt::RemapError::OutOfMemory => Errno::ENOMEM, + litebox::platform::page_mgmt::RemapError::OutOfMemory + | litebox::platform::page_mgmt::RemapError::PermissionDenied => Errno::ENOMEM, _ => unimplemented!(), } } @@ -309,7 +311,10 @@ impl From for Errno { fn from(value: litebox::platform::page_mgmt::PermissionUpdateError) -> Self { match value { litebox::platform::page_mgmt::PermissionUpdateError::Unaligned => Errno::EINVAL, - litebox::platform::page_mgmt::PermissionUpdateError::Unallocated => Errno::ENOMEM, + litebox::platform::page_mgmt::PermissionUpdateError::Unallocated + | litebox::platform::page_mgmt::PermissionUpdateError::OutOfMemory => Errno::ENOMEM, + litebox::platform::page_mgmt::PermissionUpdateError::PermissionDenied => Errno::EACCES, + litebox::platform::page_mgmt::PermissionUpdateError::PlatformFailure => Errno::EINVAL, _ => unimplemented!(), } } diff --git a/litebox_common_linux/src/loader.rs b/litebox_common_linux/src/loader.rs index 382a132a9..b562f7998 100644 --- a/litebox_common_linux/src/loader.rs +++ b/litebox_common_linux/src/loader.rs @@ -195,6 +195,12 @@ impl ElfParsedFile { return Err(ElfParseError::UnsupportedType); } + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + if header.e_type != elf::abi::ET_DYN { + // Darwin's reserved low address range cannot be replaced, so only relocatable ELFs are supported. + return Err(ElfParseError::UnsupportedType); + } + // Read the program headers. let phent_size = if cfg!(target_pointer_width = "64") { size_of::() @@ -214,6 +220,35 @@ impl ElfParsedFile { file.read_at(header.e_phoff, &mut phdrs) .map_err(ElfParseError::Io)?; + #[cfg(all(target_os = "macos", target_arch = "aarch64"))] + { + // Reject LOADs that overlap after native-page alignment. + let mut ranges = alloc::vec::Vec::new(); + let table = elf::segment::SegmentTable::new(header.endianness, CLASS, &phdrs); + for ph in table + .iter() + .filter(|ph| ph.p_type == elf::abi::PT_LOAD && ph.p_memsz != 0) + { + let page = PAGE_SIZE as u64; + let end = ph + .p_vaddr + .checked_add(ph.p_memsz) + .and_then(|end| end.checked_next_multiple_of(page)) + .ok_or(ElfParseError::BadFormat)?; + let start = ph.p_vaddr & !(page - 1); + if ph.p_offset % page != ph.p_vaddr % page + || ph.p_flags & (elf::abi::PF_W | elf::abi::PF_X) + == (elf::abi::PF_W | elf::abi::PF_X) + || ranges + .iter() + .any(|r: &core::ops::Range| r.start < end && start < r.end) + { + return Err(ElfParseError::BadFormat); + } + ranges.push(start..end); + } + } + Ok(ElfParsedFile { header, phdrs, @@ -322,7 +357,6 @@ impl ElfParsedFile { return Ok(()); } - // Verify the file offset is page-aligned (as required by the rewriter) if !file_offset.is_multiple_of(PAGE_SIZE as u64) { return Err(ElfParseError::BadTrampoline); } diff --git a/litebox_platform_macos_userland/Cargo.toml b/litebox_platform_macos_userland/Cargo.toml new file mode 100644 index 000000000..11f9247e1 --- /dev/null +++ b/litebox_platform_macos_userland/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "litebox_platform_macos_userland" +version = "0.1.0" +edition = "2024" + +[target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] +bitflags = "2.9" +litebox_platform = { path = "../litebox_platform", version = "0.1.0" } +litebox = { path = "../litebox", version = "0.1.0" } +litebox_common_linux = { path = "../litebox_common_linux", version = "0.1.0", features = ["aarch64_gate_recovery"] } +libc = { version = "0.2.169", default-features = false } +litebox_syscall_rewriter = { path = "../litebox_syscall_rewriter", version = "0.1.0", default-features = false } +zerocopy = { version = "0.8", default-features = false } + +[lints] +workspace = true diff --git a/litebox_platform_macos_userland/src/lib.rs b/litebox_platform_macos_userland/src/lib.rs new file mode 100644 index 000000000..f2c2f96d7 --- /dev/null +++ b/litebox_platform_macos_userland/src/lib.rs @@ -0,0 +1,2739 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! AArch64 macOS userland platform. +#![cfg(all(target_os = "macos", target_arch = "aarch64"))] + +use std::cell::Cell; +use std::ops::Range; +use std::sync::{ + Arc, Mutex, OnceLock, + atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}, +}; +use std::time::Duration; + +use litebox::platform::page_mgmt::{ + AllocationError, DeallocationError, FixedAddressBehavior, MemoryRegionPermissions, + PermissionUpdateError, RemapError, +}; +use litebox::platform::{ + ArchSpecificError, ArchSpecificRegister, RawConstPointer as _, trivial_providers, +}; +use litebox::shim::{ContinueOperation, EnterShim, Exception, ExceptionInfo}; +use litebox::utils::{ReinterpretUnsignedExt as _, TruncateExt as _}; +use litebox_common_linux::gate_recovery::{ + Aarch64GateSignalResult, GateInterruption, GateRuntimeState, canonicalize, +}; +use litebox_common_linux::{GuestVectorState, PtRegs}; +use litebox_platform::sync::{ + ImmediatelyWokenUp, RawMutex as RawMutexTrait, RawMutexProvider, UnblockedOrTimedOut, + WaitWakerProvider, +}; +use litebox_platform::time::{ + Instant as InstantTrait, SystemTime as SystemTimeTrait, TimeProvider, +}; +use litebox_syscall_rewriter::aarch64::{ + SVC_FRAME_BYTES, SVC_FRAME_OFF_RETADDR, SVC_FRAME_OFF_STUB, SVC_FRAME_OFF_X16, + is_patchable_guest_tpidr_offset, is_patchable_guest_x18_offset, +}; +use zerocopy::{FromBytes, IntoBytes}; + +pub use litebox::mm::linux::PAGE_SIZE; +/// The macOS host's Mach-O `__PAGEZERO` reserves the first 4 GiB. +pub const TASK_ADDR_MIN: usize = 0x1_0000_0000; +/// Exclusive upper bound for guest mappings (`MACH_VM_MAX_ADDRESS` on AArch64 macOS). +pub const TASK_ADDR_MAX: usize = 0x7FFF_FE00_0000; + +pub struct MacosUserland { + pages: std::sync::Mutex>, +} + +impl core::fmt::Debug for MacosUserland { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("MacosUserland").finish_non_exhaustive() + } +} + +impl MacosUserland { + /// Initialize the platform. + /// + /// # Panics + /// Panics if the host page size, pthread TSD layout, or signal setup is unsupported. + pub fn new() -> &'static Self { + // SAFETY: this scalar query has no pointer arguments. + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) }; + assert_eq!( + usize::try_from(page_size).ok(), + Some(PAGE_SIZE), + "unsupported macOS page size" + ); + TLS_KEYS + .get_or_init(create_tls_keys) + .as_ref() + .unwrap_or_else(|error| panic!("failed to initialize macOS TLS: {error}")); + initialize_thread_tls().expect("failed to initialize macOS thread TLS"); + register_exception_handlers().expect("failed to install macOS signal handlers"); + Box::leak(Box::new(Self { + pages: std::sync::Mutex::new(std::collections::BTreeSet::new()), + })) + } +} + +impl litebox::platform::Provider for MacosUserland {} + +type UserMutPtr = litebox::platform::common_providers::userspace_pointers::UserMutPtr< + litebox::platform::common_providers::userspace_pointers::NoValidation, + T, +>; +type UserConstPtr = litebox::platform::common_providers::userspace_pointers::UserConstPtr< + litebox::platform::common_providers::userspace_pointers::NoValidation, + T, +>; +impl litebox::platform::RawPointerProvider for MacosUserland { + type RawConstPointer = UserConstPtr; + type RawMutPointer = UserMutPtr; +} + +thread_local! { + static PLATFORM_TLS: Cell<*mut ()> = const { Cell::new(core::ptr::null_mut()) }; +} + +// SAFETY: the pointer is isolated by host TLS and initialized to null. +unsafe impl litebox::platform::ThreadLocalStorageProvider for MacosUserland { + fn get_thread_local_storage() -> *mut () { + PLATFORM_TLS.get() + } + + unsafe fn replace_thread_local_storage(value: *mut ()) -> *mut () { + PLATFORM_TLS.replace(value) + } +} + +impl TimeProvider for MacosUserland { + type Instant = Instant; + type SystemTime = SystemTime; + + fn now(&self) -> Self::Instant { + let mut t = core::mem::MaybeUninit::::uninit(); + // SAFETY: t is writable output storage for clock_gettime. + let result = unsafe { libc::clock_gettime(libc::CLOCK_UPTIME_RAW, t.as_mut_ptr()) }; + assert_eq!(result, 0, "clock_gettime(CLOCK_UPTIME_RAW) failed"); + // SAFETY: successful clock_gettime initialized t. + let t = unsafe { t.assume_init() }; + Instant { + #[expect(clippy::useless_conversion)] + inner: Duration::new( + t.tv_sec.reinterpret_as_unsigned().into(), + t.tv_nsec.reinterpret_as_unsigned().trunc(), + ), + } + } + + fn current_time(&self) -> Self::SystemTime { + let mut t = core::mem::MaybeUninit::::uninit(); + // SAFETY: t is writable output storage for clock_gettime. + let result = unsafe { libc::clock_gettime(libc::CLOCK_REALTIME, t.as_mut_ptr()) }; + assert_eq!(result, 0, "clock_gettime(CLOCK_REALTIME) failed"); + // SAFETY: successful clock_gettime initialized t. + let t = unsafe { t.assume_init() }; + SystemTime { + #[expect(clippy::useless_conversion)] + inner: Duration::new( + t.tv_sec.reinterpret_as_unsigned().into(), + t.tv_nsec.reinterpret_as_unsigned().trunc(), + ), + } + } +} + +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct Instant { + inner: Duration, +} + +impl InstantTrait for Instant { + fn checked_duration_since(&self, earlier: &Self) -> Option { + self.inner.checked_sub(earlier.inner) + } + + fn checked_add(&self, duration: Duration) -> Option { + Some(Self { + inner: self.inner.checked_add(duration)?, + }) + } +} + +pub struct SystemTime { + inner: Duration, +} + +impl SystemTimeTrait for SystemTime { + const UNIX_EPOCH: Self = SystemTime { + inner: Duration::ZERO, + }; + + fn duration_since(&self, earlier: &Self) -> Result { + self.inner + .checked_sub(earlier.inner) + .ok_or_else(|| earlier.inner.checked_sub(self.inner).unwrap()) + } +} + +bitflags::bitflags! { + #[repr(transparent)] + struct OsSyncFlags: u32 { + const NONE = 0; + const SHARED = 1; + } +} + +#[repr(u32)] +enum OsClockId { + MachAbsoluteTime = 32, +} + +unsafe extern "C" { + fn os_sync_wait_on_address( + address: *mut libc::c_void, + value: u64, + size: usize, + flags: OsSyncFlags, + ) -> i32; + fn os_sync_wait_on_address_with_timeout( + address: *mut libc::c_void, + value: u64, + size: usize, + flags: OsSyncFlags, + clock: OsClockId, + timeout_ns: u64, + ) -> i32; + fn os_sync_wake_by_address_any( + address: *mut libc::c_void, + size: usize, + flags: OsSyncFlags, + ) -> i32; + fn os_sync_wake_by_address_all( + address: *mut libc::c_void, + size: usize, + flags: OsSyncFlags, + ) -> i32; +} + +pub struct RawMutex { + inner: AtomicU32, +} + +impl RawMutex { + const fn new() -> Self { + Self { + inner: AtomicU32::new(0), + } + } + + fn address(&self) -> *mut libc::c_void { + std::ptr::from_ref(&self.inner).cast_mut().cast() + } + + fn block_or_maybe_timeout( + &self, + val: u32, + timeout: Option, + ) -> Result { + if self.inner.load(Ordering::Relaxed) != val { + return Err(ImmediatelyWokenUp); + } + if timeout.is_some_and(|timeout| timeout.is_zero()) { + return Ok(UnblockedOrTimedOut::TimedOut); + } + + let result = if let Some(timeout) = timeout { + let timeout_ns = u64::try_from(timeout.as_nanos()).unwrap_or(u64::MAX); + // SAFETY: address points to the aligned AtomicU32 compared by the kernel. + unsafe { + os_sync_wait_on_address_with_timeout( + self.address(), + u64::from(val), + size_of::(), + OsSyncFlags::NONE, + OsClockId::MachAbsoluteTime, + timeout_ns, + ) + } + } else { + // SAFETY: address points to the aligned AtomicU32 compared by the kernel. + unsafe { + os_sync_wait_on_address( + self.address(), + u64::from(val), + size_of::(), + OsSyncFlags::NONE, + ) + } + }; + if result >= 0 { + return Ok(UnblockedOrTimedOut::Unblocked); + } + + // SAFETY: __error returns this thread's live errno slot. + let error = unsafe { *libc::__error() }; + match error { + libc::ETIMEDOUT => Ok(UnblockedOrTimedOut::TimedOut), + // Documented transient failures are equivalent to spurious wakeups. + libc::EINTR | libc::EFAULT | libc::ENOMEM => Ok(UnblockedOrTimedOut::Unblocked), + _ => panic!("unexpected os_sync_wait_on_address errno {error}"), + } + } +} + +impl RawMutexProvider for MacosUserland { + type RawMutex = RawMutex; +} + +impl RawMutexTrait for RawMutex { + const INIT: Self = Self::new(); + + fn underlying_atomic(&self) -> &AtomicU32 { + &self.inner + } + + fn wake_many(&self, n: usize) -> usize { + assert!(n > 0); + if n >= i32::MAX as usize { + // SAFETY: address points to the aligned AtomicU32 used by matching waits. + let result = unsafe { + os_sync_wake_by_address_all(self.address(), size_of::(), OsSyncFlags::NONE) + }; + assert!(result == 0 || unsafe { *libc::__error() } == libc::ENOENT); + return 0; + } + + let mut woken = 0; + for _ in 0..n { + // SAFETY: address points to the aligned AtomicU32 used by matching waits. + let result = unsafe { + os_sync_wake_by_address_any(self.address(), size_of::(), OsSyncFlags::NONE) + }; + if result == 0 { + woken += 1; + } else { + // SAFETY: __error returns this thread's live errno slot. + let error = unsafe { *libc::__error() }; + assert_eq!(error, libc::ENOENT); + break; + } + } + woken + } + + fn block(&self, val: u32) -> Result<(), ImmediatelyWokenUp> { + match self.block_or_maybe_timeout(val, None) { + Ok(UnblockedOrTimedOut::Unblocked) => Ok(()), + Ok(UnblockedOrTimedOut::TimedOut) => unreachable!(), + Err(ImmediatelyWokenUp) => Err(ImmediatelyWokenUp), + } + } + + fn block_or_timeout( + &self, + val: u32, + timeout: Duration, + ) -> Result { + self.block_or_maybe_timeout(val, Some(timeout)) + } +} + +impl litebox::platform::TimerProvider for MacosUserland { + type TimerHandle = trivial_providers::UnsupportedTimerHandle; + type Signal = litebox_common_linux::signal::Signal; +} +// TODO: forward host signals such as SIGINT into the guest. For now, +// application-originated signals retain their previous host disposition and +// take_pending_signals uses the default no-op implementation. +impl litebox::platform::SignalProvider for MacosUserland { + type Signal = litebox_common_linux::signal::Signal; +} +impl litebox::mm::linux::VmemPageFaultHandler for MacosUserland { + unsafe fn handle_page_fault( + &self, + _: usize, + _: litebox::mm::linux::VmFlags, + _: u64, + ) -> Result<(), litebox::mm::linux::PageFaultError> { + unreachable!("XNU handles page faults for macOS userland") + } + fn access_error(_: u64, _: litebox::mm::linux::VmFlags) -> bool { + unreachable!("XNU handles page faults for macOS userland") + } +} + +#[repr(transparent)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct KernReturn(libc::c_int); + +impl KernReturn { + const SUCCESS: Self = Self(0); + const PROTECTION_FAILURE: Self = Self(2); + const RESOURCE_SHORTAGE: Self = Self(6); +} + +impl From for AllocationError { + fn from(result: KernReturn) -> Self { + match result { + KernReturn::PROTECTION_FAILURE => Self::PermissionDenied, + KernReturn::RESOURCE_SHORTAGE => Self::OutOfMemory, + _ => Self::AddressInUseByPlatform, + } + } +} + +bitflags::bitflags! { + #[repr(transparent)] + struct MachVmFlags: i32 { + const FIXED = 0; + const ANYWHERE = 0x0000_0001; + const RANDOM_ADDRESS = 0x0000_0008; + const OVERWRITE = 0x0000_4000; + } +} + +unsafe extern "C" { + fn mach_task_self() -> u32; + fn mach_vm_allocate(task: u32, address: *mut u64, size: u64, flags: MachVmFlags) -> KernReturn; + fn mach_vm_deallocate(task: u32, address: u64, size: u64) -> KernReturn; + fn mach_vm_read_overwrite( + task: u32, + address: u64, + size: u64, + data: u64, + out_size: *mut u64, + ) -> KernReturn; + fn sys_icache_invalidate(start: *mut libc::c_void, size: usize); +} +fn is_page_aligned(range: &Range) -> bool { + range.start < range.end + && range.start.is_multiple_of(PAGE_SIZE) + && range.end.is_multiple_of(PAGE_SIZE) +} +fn prot_flags(permissions: MemoryRegionPermissions) -> i32 { + if permissions.contains(MemoryRegionPermissions::SHARED) { + unimplemented!("shared macOS mappings are not supported") + } + let mut flags = libc::PROT_NONE; + if permissions.contains(MemoryRegionPermissions::READ) { + flags |= libc::PROT_READ; + } + if permissions.contains(MemoryRegionPermissions::WRITE) { + flags |= libc::PROT_WRITE; + } + if permissions.contains(MemoryRegionPermissions::EXEC) { + flags |= libc::PROT_EXEC; + } + flags +} +impl litebox::platform::PageManagementProvider for MacosUserland { + const TASK_ADDR_MIN: usize = TASK_ADDR_MIN; + const TASK_ADDR_MAX: usize = TASK_ADDR_MAX; + fn allocate_pages( + &self, + range: Range, + permissions: MemoryRegionPermissions, + can_grow_down: bool, + populate_pages_immediately: bool, + behavior: FixedAddressBehavior, + ) -> Result, AllocationError> { + // TODO: grow the mapping from the signal path. macOS has no + // MAP_GROWSDOWN equivalent, so the initial stack is currently fixed-size. + let _ = can_grow_down; + // Eager population is an optional performance hint. + let _ = populate_pages_immediately; + if !is_page_aligned(&range) { + return Err(AllocationError::Unaligned); + } + if range.start < TASK_ADDR_MIN { + return Err(AllocationError::BelowMinAddress); + } + if range.end > TASK_ADDR_MAX { + return Err(AllocationError::AboveMaxAddress); + } + if permissions.contains(MemoryRegionPermissions::WRITE | MemoryRegionPermissions::EXEC) { + return Err(AllocationError::PermissionDenied); + } + let mut pages = self.pages.lock().unwrap(); + if behavior == FixedAddressBehavior::Hint { + let mut error = AllocationError::OutOfMemory; + for hint in [range.start, 0] { + // SAFETY: anonymous, page-aligned allocation; without MAP_FIXED the hint cannot replace memory. + let mapped = unsafe { + libc::mmap( + hint as *mut _, + range.len(), + prot_flags(permissions), + libc::MAP_PRIVATE | libc::MAP_ANON, + -1, + 0, + ) + }; + if mapped == libc::MAP_FAILED { + // SAFETY: __error returns the current thread's live errno slot. + error = match unsafe { *libc::__error() } { + libc::EACCES | libc::EPERM => AllocationError::PermissionDenied, + libc::EEXIST => AllocationError::AddressInUse, + _ => AllocationError::OutOfMemory, + }; + continue; + } + let start = mapped as usize; + if start < TASK_ADDR_MIN + || start + .checked_add(range.len()) + .is_none_or(|end| end > TASK_ADDR_MAX) + { + // SAFETY: this is the unused mapping just returned by mmap. + unsafe { + libc::munmap(mapped, range.len()); + } + continue; + } + pages.extend((start..start + range.len()).step_by(PAGE_SIZE)); + return Ok(Self::RawMutPointer::from_usize(start)); + } + return Err(error); + } + if behavior != FixedAddressBehavior::Replace && pages.range(range.clone()).next().is_some() + { + return Err(AllocationError::AddressInUse); + } + let mut reserved = Vec::new(); + for page in range.clone().step_by(PAGE_SIZE) { + if pages.contains(&page) { + continue; + } + let mut address = page as u64; + // SAFETY: address is writable, the size is page-aligned, and the task port is ours. + // VM_FLAGS_FIXED rejects occupied ranges rather than overwriting them. + let result = unsafe { + mach_vm_allocate( + mach_task_self(), + &raw mut address, + PAGE_SIZE as u64, + MachVmFlags::FIXED, + ) + }; + if result != KernReturn::SUCCESS { + for page in reserved { + // SAFETY: these pages were reserved by mach_vm_allocate above. + assert_eq!( + unsafe { + mach_vm_deallocate(mach_task_self(), page as u64, PAGE_SIZE as u64) + }, + KernReturn::SUCCESS + ); + } + return Err(result.into()); + } + reserved.push(page); + } + // SAFETY: every page is guest-owned or newly reserved; the lock prevents mapping changes. + // MAP_FIXED cannot replace Rust allocations in this range. + let mapped = unsafe { + libc::mmap( + range.start as *mut _, + range.len(), + prot_flags(permissions), + libc::MAP_PRIVATE | libc::MAP_ANON | libc::MAP_FIXED, + -1, + 0, + ) + }; + if mapped == libc::MAP_FAILED { + // SAFETY: __error returns the current thread's live errno slot. + let errno = unsafe { *libc::__error() }; + for page in reserved { + // SAFETY: release only this call's unpublished Mach reservations. + assert_eq!( + unsafe { mach_vm_deallocate(mach_task_self(), page as u64, PAGE_SIZE as u64) }, + KernReturn::SUCCESS + ); + } + return Err(match errno { + libc::EACCES | libc::EPERM => AllocationError::PermissionDenied, + libc::EEXIST => AllocationError::AddressInUse, + _ => AllocationError::OutOfMemory, + }); + } + pages.extend(range.clone().step_by(PAGE_SIZE)); + Ok(Self::RawMutPointer::from_usize(range.start)) + } + unsafe fn deallocate_pages(&self, range: Range) -> Result<(), DeallocationError> { + if !is_page_aligned(&range) { + return Err(DeallocationError::Unaligned); + } + let mut pages = self.pages.lock().unwrap(); + // Leave host-owned pages in holes untouched, and make work proportional + // to owned mappings rather than the requested virtual-address span. + let owned = pages.range(range).copied().collect::>(); + for page in owned { + // SAFETY: the registry owns this page and the caller guarantees it is no longer in use. + if unsafe { libc::munmap(page as *mut _, PAGE_SIZE) } != 0 { + return Err(DeallocationError::AlreadyUnallocated); + } + pages.remove(&page); + } + Ok(()) + } + unsafe fn remap_pages( + &self, + old_range: Range, + new_range: Range, + permissions: MemoryRegionPermissions, + ) -> Result, RemapError> { + if !is_page_aligned(&old_range) || !is_page_aligned(&new_range) { + return Err(RemapError::Unaligned); + } + if old_range.start < new_range.end && new_range.start < old_range.end { + return Err(RemapError::Overlapping); + } + assert!( + new_range.len() > old_range.len(), + "remap_pages requires the new range to be larger than the old range" + ); + { + let pages = self.pages.lock().unwrap(); + if old_range + .clone() + .step_by(PAGE_SIZE) + .any(|page| !pages.contains(&page)) + { + return Err(RemapError::AlreadyUnallocated); + } + } + + let mut temporary = + permissions | MemoryRegionPermissions::READ | MemoryRegionPermissions::WRITE; + temporary.remove(MemoryRegionPermissions::EXEC); + let map_error = |error| match error { + AllocationError::Unaligned => RemapError::Unaligned, + AllocationError::PermissionDenied => RemapError::PermissionDenied, + AllocationError::AddressInUse + | AllocationError::AddressPartiallyInUse + | AllocationError::AddressInUseByPlatform => RemapError::AlreadyAllocated, + _ => RemapError::OutOfMemory, + }; + let preferred = self.allocate_pages( + new_range.clone(), + temporary, + false, + true, + FixedAddressBehavior::NoReplace, + ); + let new_ptr = match preferred { + Ok(ptr) => ptr, + Err( + AllocationError::AddressInUse + | AllocationError::AddressPartiallyInUse + | AllocationError::AddressInUseByPlatform, + ) => self + .allocate_pages( + new_range.clone(), + temporary, + false, + true, + FixedAddressBehavior::Hint, + ) + .map_err(map_error)?, + Err(error) => return Err(map_error(error)), + }; + let allocated_range = new_ptr.as_usize()..new_ptr.as_usize() + new_range.len(); + + let source_readable = if permissions.contains(MemoryRegionPermissions::READ) { + Ok(()) + } else { + // SAFETY: the caller permits moving the idle source mapping. + unsafe { + self.update_permissions( + old_range.clone(), + permissions | MemoryRegionPermissions::READ, + ) + } + }; + if let Err(error) = source_readable { + // SAFETY: this call allocated the destination and has not published it. + let _ = unsafe { self.deallocate_pages(allocated_range) }; + return Err(match error { + PermissionUpdateError::PermissionDenied => RemapError::PermissionDenied, + PermissionUpdateError::Unallocated => RemapError::AlreadyUnallocated, + _ => RemapError::OutOfMemory, + }); + } + + // SAFETY: the destination is writable and the source was made readable above. + if unsafe { + litebox::mm::exception_table::memcpy_fallible( + allocated_range.start as *mut u8, + old_range.start as *const u8, + old_range.len(), + ) + } + .is_err() + { + if !permissions.contains(MemoryRegionPermissions::READ) { + // SAFETY: restore the still-owned source mapping before returning. + let _ = unsafe { self.update_permissions(old_range.clone(), permissions) }; + } + // SAFETY: this call allocated the destination and has not published it. + let _ = unsafe { self.deallocate_pages(allocated_range) }; + return Err(RemapError::AlreadyUnallocated); + } + let final_permissions = if temporary == permissions { + Ok(()) + } else { + // SAFETY: the destination is unpublished and exclusively owned by this call. + unsafe { self.update_permissions(allocated_range.clone(), permissions) } + }; + if let Err(error) = final_permissions { + if !permissions.contains(MemoryRegionPermissions::READ) { + // SAFETY: restore the still-owned source mapping before returning. + let _ = unsafe { self.update_permissions(old_range.clone(), permissions) }; + } + // SAFETY: this call allocated the destination and has not published it. + let _ = unsafe { self.deallocate_pages(allocated_range) }; + return Err(match error { + PermissionUpdateError::PermissionDenied => RemapError::PermissionDenied, + _ => RemapError::OutOfMemory, + }); + } + // SAFETY: the copied source is no longer needed and the caller permits moving it. + unsafe { self.deallocate_pages(old_range) }.map_err(|_| RemapError::AlreadyUnallocated)?; + Ok(new_ptr) + } + + unsafe fn update_permissions( + &self, + range: Range, + permissions: MemoryRegionPermissions, + ) -> Result<(), PermissionUpdateError> { + if !is_page_aligned(&range) { + return Err(PermissionUpdateError::Unaligned); + } + if permissions.contains(MemoryRegionPermissions::WRITE | MemoryRegionPermissions::EXEC) { + return Err(PermissionUpdateError::PermissionDenied); + } + let pages = self.pages.lock().unwrap(); + if range + .clone() + .step_by(PAGE_SIZE) + .any(|p| !pages.contains(&p)) + { + return Err(PermissionUpdateError::Unallocated); + } + let executable = permissions.contains(MemoryRegionPermissions::EXEC); + // TODO: avoid paying for a protection transition on every executable update. + let cache_permissions = if executable { + (permissions | MemoryRegionPermissions::READ) & !MemoryRegionPermissions::EXEC + } else { + permissions + }; + // SAFETY: the locked registry covers the aligned range; the caller permits reprotection. + if unsafe { + libc::mprotect( + range.start as *mut _, + range.len(), + prot_flags(cache_permissions), + ) + } != 0 + { + // SAFETY: __error returns the current thread's live errno slot. + return Err(match unsafe { *libc::__error() } { + libc::EACCES | libc::EPERM => PermissionUpdateError::PermissionDenied, + libc::ENOMEM => PermissionUpdateError::OutOfMemory, + _ => PermissionUpdateError::PlatformFailure, + }); + } + if executable { + // SAFETY: mprotect made the entire owned range readable for cache maintenance. + unsafe { sys_icache_invalidate(range.start as *mut _, range.len()) }; + if cache_permissions != permissions + // SAFETY: the same owned range remains mapped; the caller permits the final permissions. + && unsafe { + libc::mprotect(range.start as *mut _, range.len(), prot_flags(permissions)) + } != 0 + { + // SAFETY: __error returns the current thread's live errno slot. + return Err(match unsafe { *libc::__error() } { + libc::EACCES | libc::EPERM => PermissionUpdateError::PermissionDenied, + libc::ENOMEM => PermissionUpdateError::OutOfMemory, + _ => PermissionUpdateError::PlatformFailure, + }); + } + } + Ok(()) + } + fn reserved_pages(&self) -> impl Iterator> { + // TODO: snapshot host mappings with mach_vm_region so Vmem avoids the + // runtime image, dyld cache, heap, and thread stacks during placement. + std::iter::empty() + } +} + +// The macOS pthread TSD ABI addresses key slots relative to TPIDRRO_EL0. +// TODO: use one pthread key pointing to a LiteBox-owned TlsBlock, reducing the +// private ABI dependency to locating that single key slot. +#[repr(C)] +struct TlsBlock { + guest_thread_pointer: usize, + guest_x18: usize, + active: usize, + current_thread: usize, + in_guest: usize, + vector_state: usize, + host_fp_state: usize, + initialized: usize, +} + +mod tls_offset { + use super::TlsBlock; + + pub const GUEST_THREAD_POINTER: usize = core::mem::offset_of!(TlsBlock, guest_thread_pointer); + pub const GUEST_X18: usize = core::mem::offset_of!(TlsBlock, guest_x18); + pub const ACTIVE: usize = core::mem::offset_of!(TlsBlock, active); + pub const CURRENT_THREAD: usize = core::mem::offset_of!(TlsBlock, current_thread); + pub const IN_GUEST: usize = core::mem::offset_of!(TlsBlock, in_guest); + pub const VECTOR_STATE: usize = core::mem::offset_of!(TlsBlock, vector_state); + pub const HOST_FP_STATE: usize = core::mem::offset_of!(TlsBlock, host_fp_state); + pub const INITIALIZED: usize = core::mem::offset_of!(TlsBlock, initialized); +} + +const TLS_SLOT_COUNT: usize = size_of::() / size_of::(); + +#[derive(Debug)] +struct GuestTlsKeys { + slots: [libc::pthread_key_t; TLS_SLOT_COUNT], + interrupt_signal: i32, +} + +static TLS_KEYS: OnceLock> = OnceLock::new(); +static TLS_BLOCK_OFFSET: AtomicUsize = AtomicUsize::new(0); + +impl Drop for GuestTlsKeys { + fn drop(&mut self) { + for key in self.slots { + // SAFETY: GuestTlsKeys owns every successfully allocated key. + unsafe { libc::pthread_key_delete(key) }; + } + } +} + +fn keys() -> &'static GuestTlsKeys { + let Some(Ok(keys)) = TLS_KEYS.get() else { + fatal_signal(b"macOS TLS is not initialized", 0); + }; + keys +} + +unsafe extern "C" fn drop_vector_state(value: *mut libc::c_void) { + if !value.is_null() { + // SAFETY: the vector-state slot contains only pointers created by Box::into_raw below. + unsafe { drop(Box::from_raw(value.cast::())) }; + } +} + +fn validate_tls_layout( + keys: &[libc::pthread_key_t; TLS_SLOT_COUNT], + first: usize, +) -> Result<(), i32> { + for (index, key) in keys.iter().copied().enumerate() { + let vector_sentinel = (index * size_of::() == tls_offset::VECTOR_STATE) + .then(|| Box::into_raw(Box::new(GuestVectorState::default()))); + let sentinel = vector_sentinel.map_or(0x1234usize + index, |pointer| pointer as usize); + // SAFETY: the newly allocated key remains live; pthread treats the value as opaque. + let previous = unsafe { libc::pthread_getspecific(key) }; + // SAFETY: the vector sentinel has the type required by its destructor; other slots + // have no destructor. Signal handlers have not been installed yet. + let error = unsafe { libc::pthread_setspecific(key, sentinel as *const libc::c_void) }; + if error != 0 { + if let Some(pointer) = vector_sentinel { + // SAFETY: pthread did not take ownership after the failed call. + unsafe { drop(Box::from_raw(pointer)) }; + } + return Err(error); + } + + let mut value = [0u8; size_of::()]; + let mut copied = 0; + let address = anchor() + first * size_of::() + index * size_of::(); + // SAFETY: value and copied are writable; Mach validates the source address without faulting. + let valid = unsafe { + mach_vm_read_overwrite( + mach_task_self(), + address as u64, + value.len() as u64, + value.as_mut_ptr() as u64, + &raw mut copied, + ) == KernReturn::SUCCESS + } && copied == value.len() as u64 + && usize::from_ne_bytes(value) == sentinel; + + // SAFETY: restore the opaque value that preceded this temporary probe. + let error = unsafe { libc::pthread_setspecific(key, previous) }; + if error != 0 { + // Keep a vector sentinel allocated if pthread still owns it; leaking is safer + // than letting key deletion retain a dangling destructor argument. + return Err(error); + } + if let Some(pointer) = vector_sentinel { + // SAFETY: restoring the prior value returned ownership to this function. + unsafe { drop(Box::from_raw(pointer)) }; + } + if !valid { + return Err(libc::ENOTSUP); + } + } + Ok(()) +} + +fn create_tls_keys() -> Result { + let mut keys = [0; TLS_SLOT_COUNT]; + for index in 0..TLS_SLOT_COUNT { + let destructor = (index * size_of::() == tls_offset::VECTOR_STATE) + .then_some(drop_vector_state as unsafe extern "C" fn(*mut libc::c_void)); + // SAFETY: the array element is writable; only the vector slot owns its opaque pointer. + let error = unsafe { libc::pthread_key_create(&raw mut keys[index], destructor) }; + if error != 0 { + for key in &keys[..index] { + // SAFETY: these keys were allocated by preceding iterations. + unsafe { libc::pthread_key_delete(*key) }; + } + return Err(error); + } + } + let first: usize = keys[0].trunc(); + if keys.iter().enumerate().any(|(index, key)| { + let key: usize = (*key).trunc(); + key != first + index + }) || !u16::try_from(first * size_of::() + tls_offset::GUEST_THREAD_POINTER) + .is_ok_and(is_patchable_guest_tpidr_offset) + || !u16::try_from(first * size_of::() + tls_offset::GUEST_X18) + .is_ok_and(is_patchable_guest_x18_offset) + { + for key in keys { + // SAFETY: every key was allocated above and has not been published. + unsafe { libc::pthread_key_delete(key) }; + } + return Err(libc::ENOTSUP); + } + if let Err(error) = validate_tls_layout(&keys, first) { + for key in keys { + // SAFETY: every key was allocated above and has not been published. + unsafe { libc::pthread_key_delete(key) }; + } + return Err(error); + } + let mut interrupt_signal = None; + for candidate in [libc::SIGUSR1, libc::SIGUSR2] { + // SAFETY: macOS sigaction contains integer fields; zero is a valid representation. + let mut disposition = unsafe { std::mem::zeroed::() }; + // SAFETY: disposition is writable and null requests a query. + if unsafe { libc::sigaction(candidate, core::ptr::null(), &raw mut disposition) } != 0 { + for key in keys { + // SAFETY: every key was allocated above and has not been published. + unsafe { libc::pthread_key_delete(key) }; + } + // SAFETY: __error returns the current thread's live errno slot. + return Err(unsafe { *libc::__error() }); + } + if disposition.sa_sigaction == libc::SIG_DFL { + interrupt_signal = Some(candidate); + break; + } + } + let Some(interrupt_signal) = interrupt_signal else { + for key in keys { + // SAFETY: every key was allocated above and has not been published. + unsafe { libc::pthread_key_delete(key) }; + } + return Err(libc::EBUSY); + }; + TLS_BLOCK_OFFSET.store(first * size_of::(), Ordering::Relaxed); + Ok(GuestTlsKeys { + slots: keys, + interrupt_signal, + }) +} + +fn anchor() -> usize { + let value: usize; + // SAFETY: TPIDRRO_EL0 is readable at EL0 on macOS; this changes no memory or flags. + unsafe { + core::arch::asm!("mrs {value}, tpidrro_el0", value = out(reg) value, options(nomem, nostack, preserves_flags)); + } + value & !0b111 +} + +fn tls_address(offset: usize) -> *mut usize { + (anchor() + TLS_BLOCK_OFFSET.load(Ordering::Relaxed) + offset) as *mut usize +} + +fn read_tls(offset: usize) -> usize { + // SAFETY: key creation validates the process-wide pthread TSD layout before + // publishing the keys. Each thread has storage for every allocated slot, + // even before initialize_thread_tls populates its nonzero values. + unsafe { tls_address(offset).read_volatile() } +} + +fn write_tls(offset: usize, value: usize) { + // SAFETY: key creation validates the process-wide pthread TSD layout before + // publishing the keys. The address is this thread's slot at that offset. + unsafe { tls_address(offset).write_volatile(value) } +} + +fn initialize_thread_tls() -> std::io::Result<()> { + let keys = keys(); + let initialized_key = keys.slots[tls_offset::INITIALIZED / size_of::()]; + // SAFETY: the process-wide key remains allocated for the process lifetime. + if !unsafe { libc::pthread_getspecific(initialized_key) }.is_null() { + return Ok(()); + } + let vector_slot = tls_address(tls_offset::VECTOR_STATE); + // SAFETY: this thread's vector slot was validated above. + let vector = unsafe { vector_slot.read_volatile() }; + if vector == 0 { + let vector = Box::into_raw(Box::new(GuestVectorState::default())) as usize; + // SAFETY: the key is allocated with drop_vector_state as its destructor. + let error = unsafe { + libc::pthread_setspecific( + keys.slots[tls_offset::VECTOR_STATE / size_of::()], + vector as *const libc::c_void, + ) + }; + if error != 0 { + // SAFETY: pthread did not take ownership after the failed call. + unsafe { drop(Box::from_raw(vector as *mut GuestVectorState)) }; + return Err(std::io::Error::from_raw_os_error(error)); + } + } + // Publish initialization only after every slot and owned allocation is valid. + // SAFETY: initialized_key is allocated and has no destructor. + let error = unsafe { libc::pthread_setspecific(initialized_key, core::ptr::dangling()) }; + if error != 0 { + return Err(std::io::Error::from_raw_os_error(error)); + } + Ok(()) +} + +fn guest_thread_pointer_tp_offset() -> usize { + TLS_BLOCK_OFFSET.load(Ordering::Relaxed) + tls_offset::GUEST_THREAD_POINTER +} +fn get_guest_thread_pointer() -> usize { + read_tls(tls_offset::GUEST_THREAD_POINTER) +} +fn get_guest_x18() -> usize { + read_tls(tls_offset::GUEST_X18) +} +fn set_guest_thread_pointer(value: usize) { + write_tls(tls_offset::GUEST_THREAD_POINTER, value); +} +fn set_guest_x18(value: usize) { + write_tls(tls_offset::GUEST_X18, value); +} + +impl litebox::platform::ArchSpecificProvider for MacosUserland { + fn get_arch_specific_register( + &self, + reg: &ArchSpecificRegister, + ) -> Result { + match reg { + ArchSpecificRegister::TpidrEl0 => Ok(get_guest_thread_pointer()), + _ => Err(ArchSpecificError::RegisterUnsupported), + } + } + fn set_arch_specific_register( + &self, + reg: &ArchSpecificRegister, + value: usize, + ) -> Result<(), ArchSpecificError> { + match reg { + ArchSpecificRegister::TpidrEl0 => { + if litebox_common_linux::arch::is_valid_user_tls_base(value) { + set_guest_thread_pointer(value); + Ok(()) + } else { + Err(ArchSpecificError::RegisterUnpermittedValue) + } + } + _ => Err(ArchSpecificError::RegisterUnsupported), + } + } +} + +fn interrupt_signal() -> i32 { + keys().interrupt_signal +} + +fn host_signals() -> [i32; 5] { + [ + libc::SIGTRAP, + libc::SIGSEGV, + libc::SIGBUS, + libc::SIGILL, + interrupt_signal(), + ] +} +static PREVIOUS: OnceLock<[libc::sigaction; 5]> = OnceLock::new(); +// Private Darwin si_code values absent from libc's public constants. +const SI_USER: i32 = 0x1_0001; +const SI_QUEUE: i32 = 0x1_0002; + +#[derive(Clone, Copy)] +enum GuestExit { + Exception(ExceptionInfo), + Interrupt, +} + +struct ThreadContext<'a> { + shim: &'a dyn EnterShim, + ctx: &'a mut PtRegs, + host_sp: usize, + svc_frame: usize, + outbound_x16: usize, + outbound_pc: usize, + outbound_stub: usize, + interrupted: *const AtomicBool, + thread: ThreadHandle, + exit: GuestExit, +} + +struct ThreadState { + // Cleared before thread exit to prevent pthread ID-reuse races. + identity: Mutex>, + interrupted: AtomicBool, + waker: Mutex>, +} +#[derive(Clone)] +pub struct ThreadHandle(Arc); +impl ThreadHandle { + fn current() -> Self { + let handle = read_tls(tls_offset::CURRENT_THREAD) as *const ThreadHandle; + assert!(!handle.is_null(), "not running a LiteBox thread"); + // SAFETY: CURRENT_THREAD points to this thread's live stack-owned handle. + unsafe { (*handle).clone() } + } + + fn interrupt(&self) { + self.0.interrupted.store(true, Ordering::Release); + { + let identity = self.0.identity.lock().unwrap(); + if let Some(identity) = *identity { + // SAFETY: this lock prevents unregistering/reusing the saved pthread_t during delivery. + unsafe { libc::pthread_kill(identity as libc::pthread_t, interrupt_signal()) }; + } + } + let waker = self.0.waker.lock().unwrap().clone(); + if let Some(waker) = waker { + waker.wake(); + } + } +} + +impl litebox::platform::ThreadProvider for MacosUserland { + type ExecutionContext = litebox_common_linux::PtRegs; + type ThreadSpawnError = std::io::Error; + type ThreadHandle = ThreadHandle; + unsafe fn spawn_thread( + &self, + _ctx: &Self::ExecutionContext, + _init_thread: Box>, + ) -> Result<(), Self::ThreadSpawnError> { + Err(std::io::Error::new( + std::io::ErrorKind::Unsupported, + "guest thread creation is not supported", + )) + } + fn current_thread(&self) -> Self::ThreadHandle { + ThreadHandle::current() + } + fn interrupt_thread(&self, thread: &Self::ThreadHandle) { + thread.interrupt(); + } + + #[cfg(debug_assertions)] + fn run_test_thread(f: impl FnOnce() -> R) -> R { + initialize_thread_tls().expect("unsupported macOS TLS layout"); + assert_eq!(read_tls(tls_offset::CURRENT_THREAD), 0); + let handle = ThreadHandle(Arc::new(ThreadState { + // SAFETY: pthread_self has no preconditions. + identity: Mutex::new(Some(unsafe { libc::pthread_self() } as usize)), + interrupted: AtomicBool::new(false), + waker: Mutex::new(None), + })); + write_tls(tls_offset::CURRENT_THREAD, (&raw const handle) as usize); + let cleanup_handle = handle.clone(); + let _cleanup = litebox::utils::defer(move || { + *cleanup_handle.0.identity.lock().unwrap() = None; + write_tls(tls_offset::CURRENT_THREAD, 0); + }); + f() + } +} + +impl WaitWakerProvider for MacosUserland { + fn update_waker(&self, waker: Option) { + if read_tls(tls_offset::CURRENT_THREAD) != 0 { + *ThreadHandle::current().0.waker.lock().unwrap() = waker; + } + } +} +pub(crate) fn get_guest_vector_state() -> GuestVectorState { + let state = read_tls(tls_offset::VECTOR_STATE) as *const GuestVectorState; + assert!(!state.is_null(), "macOS TLS is not initialized"); + // SAFETY: this thread owns the allocation; volatile matches transition + // assembly and signal-handler accesses hidden from the compiler. + unsafe { state.read_volatile() } +} +pub(crate) fn set_guest_vector_state(state: &GuestVectorState) { + let saved = read_tls(tls_offset::VECTOR_STATE) as *mut GuestVectorState; + assert!(!saved.is_null(), "macOS TLS is not initialized"); + // SAFETY: this thread owns the allocation; volatile matches transition + // assembly and signal-handler accesses hidden from the compiler. + unsafe { saved.write_volatile(state.clone()) }; +} + +impl litebox::platform::GuestVectorStateProvider for MacosUserland { + type GuestVectorState = litebox_common_linux::GuestVectorState; + fn get_guest_vector_state(&self) -> Self::GuestVectorState { + get_guest_vector_state() + } + fn set_guest_vector_state(&self, state: &Self::GuestVectorState) { + set_guest_vector_state(state); + } +} +impl litebox::platform::SystemInfoProvider for MacosUserland { + fn get_syscall_entry_point(&self) -> usize { + syscall_callback as *const () as usize + } + fn guest_thread_pointer_offset(&self) -> Option { + Some(guest_thread_pointer_tp_offset()) + } + fn get_vdso_address(&self) -> Option { + None + } +} + +// The rewriter leaves the fourth word of its 32-byte SVC frame unused. +const MACOS_SVC_FRAME_OFF_SCRATCH: u16 = 24; + +const _: () = assert!( + SVC_FRAME_OFF_X16 == 0 + && SVC_FRAME_OFF_RETADDR == 8 + && SVC_FRAME_OFF_STUB == 16 + && MACOS_SVC_FRAME_OFF_SCRATCH == 24 + && MACOS_SVC_FRAME_OFF_SCRATCH + 8 == SVC_FRAME_BYTES +); + +// SVC gate callback: the macOS signal frame captures the full register state at this PC. +unsafe extern "C" { + fn litebox_macos_syscall_callback_in_guest_cleared(); + fn switch_to_guest_via_sigreturn_start(); + fn switch_to_guest_via_sigreturn_end(); + fn switch_to_guest_via_outbound_stub_start(); + fn switch_to_guest_via_outbound_stub_end(); +} + +#[unsafe(naked)] +unsafe extern "C" fn syscall_callback() { + core::arch::naked_asm!( + ".cfi_startproc", + ".cfi_def_cfa x29, 160", + ".cfi_offset x29, -160", + ".cfi_offset x30, -152", + ".cfi_offset x19, -144", + ".cfi_offset x20, -136", + ".cfi_offset x21, -128", + ".cfi_offset x22, -120", + ".cfi_offset x23, -112", + ".cfi_offset x24, -104", + ".cfi_offset x25, -96", + ".cfi_offset x26, -88", + ".cfi_offset x27, -80", + ".cfi_offset x28, -72", + ".cfi_offset d8, -64", + ".cfi_offset d9, -56", + ".cfi_offset d10, -48", + ".cfi_offset d11, -40", + ".cfi_offset d12, -32", + ".cfi_offset d13, -24", + ".cfi_offset d14, -16", + ".cfi_offset d15, -8", + // Preserve guest x17 in the unused SVC-frame word before using it as scratch. + "str x17, [sp, #{frame_scratch}]", + "mrs x16, tpidrro_el0", + "and x16, x16, #0xfffffffffffffff8", + "adrp x17, {tls_block_offset}@PAGE", + "ldr x17, [x17, {tls_block_offset}@PAGEOFF]", + "add x16, x16, x17", + "ldr x17, [x16, #{active}]", // ThreadContext + "ldr x17, [x17, #{context}]", // PtRegs + "stp x0, x1, [x17, #0]", + "stp x2, x3, [x17, #16]", + "stp x4, x5, [x17, #32]", + "stp x6, x7, [x17, #48]", + "stp x8, x9, [x17, #64]", + "stp x10, x11, [x17, #80]", + "stp x12, x13, [x17, #96]", + "stp x14, x15, [x17, #112]", + "ldr x0, [sp, #{frame_x16}]", + "ldr x1, [sp, #{frame_scratch}]", + "stp x0, x1, [x17, #128]", + // Preserve the gate-frame ABI before leaving the guest stack. + "ldr x2, [x16, #{active}]", // ThreadContext + "mov x3, sp", + "str x3, [x2, #{thread_svc_frame}]", + "str x0, [x2, #{thread_outbound_x16}]", + "ldr x3, [sp, #{frame_retaddr}]", + "str x3, [x2, #{thread_outbound_pc}]", + "ldr x3, [sp, #{frame_stub}]", + "str x3, [x2, #{thread_outbound_stub}]", + "ldr x0, [x16, #{guest_x18}]", + "str x0, [x17, #144]", + "stp x19, x20, [x17, #152]", + "stp x21, x22, [x17, #168]", + "stp x23, x24, [x17, #184]", + "stp x25, x26, [x17, #200]", + "stp x27, x28, [x17, #216]", + "stp x29, x30, [x17, #232]", + "add x0, sp, #{svc_frame}", + "str x0, [x17, #{regs_sp}]", + "ldr x0, [sp, #{frame_retaddr}]", + "str x0, [x17, #{regs_pc}]", + "mrs x0, nzcv", + "mrs x1, ssbs", + "orr x0, x0, x1", + "mrs x1, dit", + "orr x0, x0, x1", + "str x0, [x17, #{regs_pstate}]", + "ldr x0, [x17, #0]", + "str x0, [x17, #{regs_orig_x0}]", + "str w8, [x17, #{regs_syscallno}]", + "mov x0, #-38", + "str x0, [x17, #0]", + // Save guest vector state before entering host Rust code. + "ldr x0, [x16, #{vector_state}]", + "stp q0, q1, [x0, #0]", + "stp q2, q3, [x0, #32]", + "stp q4, q5, [x0, #64]", + "stp q6, q7, [x0, #96]", + "stp q8, q9, [x0, #128]", + "stp q10, q11, [x0, #160]", + "stp q12, q13, [x0, #192]", + "stp q14, q15, [x0, #224]", + "stp q16, q17, [x0, #256]", + "stp q18, q19, [x0, #288]", + "stp q20, q21, [x0, #320]", + "stp q22, q23, [x0, #352]", + "stp q24, q25, [x0, #384]", + "stp q26, q27, [x0, #416]", + "stp q28, q29, [x0, #448]", + "stp q30, q31, [x0, #480]", + "mrs x1, fpsr", + "str w1, [x0, #{vector_fpsr}]", + "mrs x1, fpcr", + "str w1, [x0, #{vector_fpcr}]", + // Restore the host's FP control state before entering Rust. + "ldp w1, w2, [x16, #{host_fp_state}]", + "msr fpsr, x1", + "msr fpcr, x2", + "str xzr, [x16, #{in_guest}]", + "b _litebox_macos_syscall_callback_in_guest_cleared", + ".cfi_endproc", + ".globl _litebox_macos_syscall_callback_in_guest_cleared", + "_litebox_macos_syscall_callback_in_guest_cleared:", + ".cfi_startproc", + ".cfi_def_cfa x29, 160", + ".cfi_offset x29, -160", + ".cfi_offset x30, -152", + ".cfi_offset x19, -144", + ".cfi_offset x20, -136", + ".cfi_offset x21, -128", + ".cfi_offset x22, -120", + ".cfi_offset x23, -112", + ".cfi_offset x24, -104", + ".cfi_offset x25, -96", + ".cfi_offset x26, -88", + ".cfi_offset x27, -80", + ".cfi_offset x28, -72", + ".cfi_offset d8, -64", + ".cfi_offset d9, -56", + ".cfi_offset d10, -48", + ".cfi_offset d11, -40", + ".cfi_offset d12, -32", + ".cfi_offset d13, -24", + ".cfi_offset d14, -16", + ".cfi_offset d15, -8", + // This label is a separate Mach-O atom, so a linker veneer may have + // clobbered x16/x17. Recompute the TLS base before dereferencing it. + "mrs x16, tpidrro_el0", + "and x16, x16, #0xfffffffffffffff8", + "adrp x17, {tls_block_offset}@PAGE", + "ldr x17, [x17, {tls_block_offset}@PAGEOFF]", + "add x16, x16, x17", + "ldr x0, [x16, #{active}]", // ThreadContext + "ldr x1, [x0, #{host_sp}]", + "mov sp, x1", + "add x29, sp, #16", + "bl {syscall_handler}", + "b {finish_thread_arch}", + ".cfi_endproc", + tls_block_offset = sym TLS_BLOCK_OFFSET, + active = const tls_offset::ACTIVE, + in_guest = const tls_offset::IN_GUEST, + guest_x18 = const tls_offset::GUEST_X18, + vector_state = const tls_offset::VECTOR_STATE, + context = const core::mem::offset_of!(ThreadContext, ctx), + host_sp = const core::mem::offset_of!(ThreadContext, host_sp), + thread_svc_frame = const core::mem::offset_of!(ThreadContext, svc_frame), + thread_outbound_x16 = const core::mem::offset_of!(ThreadContext, outbound_x16), + thread_outbound_pc = const core::mem::offset_of!(ThreadContext, outbound_pc), + thread_outbound_stub = const core::mem::offset_of!(ThreadContext, outbound_stub), + regs_sp = const core::mem::offset_of!(PtRegs, sp), + regs_pc = const core::mem::offset_of!(PtRegs, pc), + regs_pstate = const core::mem::offset_of!(PtRegs, pstate), + regs_orig_x0 = const core::mem::offset_of!(PtRegs, orig_x0), + regs_syscallno = const core::mem::offset_of!(PtRegs, syscallno), + vector_fpsr = const core::mem::offset_of!(GuestVectorState, fpsr), + vector_fpcr = const core::mem::offset_of!(GuestVectorState, fpcr), + host_fp_state = const tls_offset::HOST_FP_STATE, + svc_frame = const SVC_FRAME_BYTES, + frame_x16 = const SVC_FRAME_OFF_X16, + frame_retaddr = const SVC_FRAME_OFF_RETADDR, + frame_stub = const SVC_FRAME_OFF_STUB, + frame_scratch = const MACOS_SVC_FRAME_OFF_SCRATCH, + syscall_handler = sym syscall_handler, + finish_thread_arch = sym finish_thread_arch, + ); +} + +// TODO: replace this synthetic signal return with a guest-side restoration +// stub that can resume an arbitrary context without relying on private XNU +// sigreturn-frame details. +#[unsafe(naked)] +unsafe extern "C" fn switch_to_guest_via_sigreturn() -> ! { + core::arch::naked_asm!( + "mrs x16, tpidrro_el0", + "and x16, x16, #0xfffffffffffffff8", + "adrp x17, {tls_block_offset}@PAGE", + "ldr x17, [x17, {tls_block_offset}@PAGEOFF]", + "add x16, x16, x17", + "mrs x17, fpsr", + "mrs x9, fpcr", + "stp w17, w9, [x16, #{host_fp_state}]", + ".globl _switch_to_guest_via_sigreturn_start", + ".alt_entry _switch_to_guest_via_sigreturn_start", + "_switch_to_guest_via_sigreturn_start:", + "mov x17, #1", + "str x17, [x16, #{in_guest}]", + "brk #0", + ".globl _switch_to_guest_via_sigreturn_end", + ".alt_entry _switch_to_guest_via_sigreturn_end", + "_switch_to_guest_via_sigreturn_end:", + // The signal handler redirects the first BRK. Trap again rather than + // falling through if that invariant is ever violated. + "brk #0", + tls_block_offset = sym TLS_BLOCK_OFFSET, + in_guest = const tls_offset::IN_GUEST, + host_fp_state = const tls_offset::HOST_FP_STATE, + ); +} + +#[unsafe(naked)] +unsafe extern "C" fn switch_to_guest_via_outbound_stub(_: &mut ThreadContext) -> ! { + core::arch::naked_asm!( + // x0 is ThreadContext and remains available if an interrupt is pending. + "ldr x16, [x0, #{context}]", + "mrs x17, tpidrro_el0", + "and x17, x17, #0xfffffffffffffff8", + "adrp x1, {tls_block_offset}@PAGE", + "ldr x1, [x1, {tls_block_offset}@PAGEOFF]", + "add x17, x17, x1", + // Save host FP control state before installing the guest's. + "mrs x1, fpsr", + "mrs x2, fpcr", + "stp w1, w2, [x17, #{host_fp_state}]", + ".globl _switch_to_guest_via_outbound_stub_start", + ".alt_entry _switch_to_guest_via_outbound_stub_start", + "_switch_to_guest_via_outbound_stub_start:", + "mov x1, #1", + "str x1, [x17, #{in_guest}]", + "ldr x1, [x0, #{interrupted}]", + "ldarb w1, [x1]", + "cbz w1, 1f", + "b _switch_to_guest_via_outbound_stub_interrupted", + "1:", + "ldr x1, [x16, #144]", + "str x1, [x17, #{guest_x18}]", + "ldr x0, [x17, #{vector_state}]", + "ldp q0, q1, [x0, #0]", + "ldp q2, q3, [x0, #32]", + "ldp q4, q5, [x0, #64]", + "ldp q6, q7, [x0, #96]", + "ldp q8, q9, [x0, #128]", + "ldp q10, q11, [x0, #160]", + "ldp q12, q13, [x0, #192]", + "ldp q14, q15, [x0, #224]", + "ldp q16, q17, [x0, #256]", + "ldp q18, q19, [x0, #288]", + "ldp q20, q21, [x0, #320]", + "ldp q22, q23, [x0, #352]", + "ldp q24, q25, [x0, #384]", + "ldp q26, q27, [x0, #416]", + "ldp q28, q29, [x0, #448]", + "ldp q30, q31, [x0, #480]", + "ldr w1, [x0, #{vector_fpsr}]", + "msr fpsr, x1", + "ldr w1, [x0, #{vector_fpcr}]", + "msr fpcr, x1", + "ldr x0, [x16, #{regs_pstate}]", + "msr nzcv, x0", + "msr ssbs, x0", + "msr dit, x0", + "ldp x0, x1, [x16, #0]", + "ldp x2, x3, [x16, #16]", + "ldp x4, x5, [x16, #32]", + "ldp x6, x7, [x16, #48]", + "ldp x8, x9, [x16, #64]", + "ldp x10, x11, [x16, #80]", + "ldp x12, x13, [x16, #96]", + "ldp x14, x15, [x16, #112]", + "ldr x17, [x16, #136]", + "ldp x19, x20, [x16, #152]", + "ldp x21, x22, [x16, #168]", + "ldp x23, x24, [x16, #184]", + "ldp x25, x26, [x16, #200]", + "ldp x27, x28, [x16, #216]", + "ldp x29, x30, [x16, #232]", + "ldr x16, [x16, #{regs_sp}]", + "sub sp, x16, #{svc_frame}", + "ldr x16, [sp, #{frame_stub}]", + "br x16", + "_switch_to_guest_via_outbound_stub_interrupted:", + "str xzr, [x17, #{in_guest}]", + ".globl _switch_to_guest_via_outbound_stub_end", + ".alt_entry _switch_to_guest_via_outbound_stub_end", + "_switch_to_guest_via_outbound_stub_end:", + "b _litebox_macos_interrupt_callback", + tls_block_offset = sym TLS_BLOCK_OFFSET, + in_guest = const tls_offset::IN_GUEST, + guest_x18 = const tls_offset::GUEST_X18, + vector_state = const tls_offset::VECTOR_STATE, + host_fp_state = const tls_offset::HOST_FP_STATE, + context = const core::mem::offset_of!(ThreadContext, ctx), + interrupted = const core::mem::offset_of!(ThreadContext, interrupted), + regs_sp = const core::mem::offset_of!(PtRegs, sp), + regs_pstate = const core::mem::offset_of!(PtRegs, pstate), + vector_fpsr = const core::mem::offset_of!(GuestVectorState, fpsr), + vector_fpcr = const core::mem::offset_of!(GuestVectorState, fpcr), + svc_frame = const SVC_FRAME_BYTES, + frame_stub = const SVC_FRAME_OFF_STUB, + ); +} + +/// Run a guest thread. +/// +/// # Safety +/// The shim must supply valid mappings and macOS-targeted rewritten guest code. +pub unsafe fn run_thread(shim: T, ctx: &mut PtRegs) +where + T: EnterShim, +{ + run_thread_inner(&shim, ctx); +} + +fn run_thread_inner(shim: &dyn EnterShim, ctx: &mut PtRegs) { + initialize_thread_tls().expect("unsupported macOS TLS layout"); + assert!( + read_tls(tls_offset::ACTIVE) == 0, + "nested guest entry is not supported" + ); + set_guest_thread_pointer(0); + set_guest_x18(0); + let thread = ThreadHandle(Arc::new(ThreadState { + // SAFETY: pthread_self has no preconditions; unregister before thread exit. + identity: Mutex::new(Some(unsafe { libc::pthread_self() } as usize)), + interrupted: AtomicBool::new(false), + waker: Mutex::new(None), + })); + let mut thread_ctx = ThreadContext { + shim, + ctx, + host_sp: 0, + svc_frame: 0, + outbound_x16: 0, + outbound_pc: 0, + outbound_stub: 0, + interrupted: &raw const thread.0.interrupted, + thread, + exit: GuestExit::Interrupt, + }; + write_tls(tls_offset::ACTIVE, (&raw mut thread_ctx) as usize); + write_tls( + tls_offset::CURRENT_THREAD, + (&raw const thread_ctx.thread) as usize, + ); + let thread_handle = thread_ctx.thread.clone(); + let _registration = litebox::utils::defer(move || { + *thread_handle.0.identity.lock().unwrap() = None; + write_tls(tls_offset::ACTIVE, 0); + write_tls(tls_offset::CURRENT_THREAD, 0); + write_tls(tls_offset::IN_GUEST, 0); + }); + // SAFETY: macOS sigset_t is an integer bitmask; zero is valid output storage. + let mut old_mask = unsafe { std::mem::zeroed::() }; + // SAFETY: zero is valid for this integer bitmask. + let mut signals = unsafe { std::mem::zeroed::() }; + // SAFETY: both masks are live stack storage; only this thread's mask is changed. + unsafe { + libc::sigemptyset(&raw mut signals); + for signal in host_signals() { + libc::sigaddset(&raw mut signals, signal); + } + assert_eq!( + libc::pthread_sigmask(libc::SIG_UNBLOCK, &raw const signals, &raw mut old_mask), + 0 + ); + } + let _mask_guard = litebox::utils::defer(|| { + assert_eq!( + // SAFETY: old_mask is this thread's saved mask and remains live. + unsafe { + libc::pthread_sigmask( + libc::SIG_SETMASK, + &raw const old_mask, + core::ptr::null_mut(), + ) + }, + 0, + ); + }); + with_signal_alt_stack(|| { + // SAFETY: thread state, handlers and stack are initialized; the caller supplies valid guest mappings. + unsafe { run_thread_arch(&mut thread_ctx) }; + }); +} + +fn with_signal_alt_stack(f: impl FnOnce() -> R) -> R { + let alt_stack_size = (libc::SIGSTKSZ * 2).next_multiple_of(PAGE_SIZE); + let mapping_size = PAGE_SIZE + alt_stack_size; + // SAFETY: allocate fresh anonymous memory without replacing any existing mapping. + let stack_base = unsafe { + libc::mmap( + core::ptr::null_mut(), + mapping_size, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANONYMOUS, + -1, + 0, + ) + }; + assert_ne!( + stack_base, + libc::MAP_FAILED, + "failed to allocate signal stack" + ); + let _unmap_guard = litebox::utils::defer(|| { + assert_eq!( + // SAFETY: the previous altstack is restored before this owned mapping is freed. + unsafe { libc::munmap(stack_base, mapping_size) }, + 0, + ); + }); + assert_eq!( + // SAFETY: the first page is exclusively owned and outside the usable signal stack. + unsafe { libc::mprotect(stack_base, PAGE_SIZE, libc::PROT_NONE) }, + 0, + ); + let alternate = libc::stack_t { + ss_sp: stack_base.wrapping_byte_add(PAGE_SIZE), + ss_size: alt_stack_size, + ss_flags: 0, + }; + // SAFETY: stack_t consists of a nullable pointer and integers, all zero-valid. + let mut previous = unsafe { std::mem::zeroed::() }; + assert_eq!( + // SAFETY: the writable mapping stays live until the previous altstack is restored. + unsafe { libc::sigaltstack(&raw const alternate, &raw mut previous) }, + 0, + ); + let _restore_guard = litebox::utils::defer(|| { + assert_eq!( + // SAFETY: f and its handlers have returned; the saved descriptor remains live. + unsafe { libc::sigaltstack(&raw const previous, core::ptr::null_mut()) }, + 0, + ); + }); + f() +} + +impl ThreadContext<'_> { + fn call_shim( + &mut self, + f: impl FnOnce(&dyn EnterShim, &mut PtRegs) -> ContinueOperation, + ) { + let mut operation = f(self.shim, self.ctx); + if operation == ContinueOperation::Resume + && self.thread.0.interrupted.swap(false, Ordering::AcqRel) + { + operation = self.shim.interrupt(self.ctx); + } + if operation == ContinueOperation::Resume { + // SAFETY: the shim prepared the guest context; no owned guards cross the switch. + unsafe { switch_to_guest(self) }; + } + } +} + +unsafe fn switch_to_guest(thread_ctx: &mut ThreadContext) -> ! { + if thread_ctx.outbound_stub != 0 + && thread_ctx.ctx.sp == thread_ctx.svc_frame + usize::from(SVC_FRAME_BYTES) + && thread_ctx.ctx.pc == thread_ctx.outbound_pc + && thread_ctx.ctx.regs[16] == thread_ctx.outbound_x16 + { + let frame = [ + thread_ctx.outbound_x16, + thread_ctx.outbound_pc, + thread_ctx.outbound_stub, + ]; + // Restage the frame immediately before use. If the guest stack became + // inaccessible during the shim round trip, use generic sigreturn. + // SAFETY: frame is readable. Faulting guest writes use the installed + // exception-table handler and return failure instead of escaping to XNU. + let frame_staged = unsafe { + litebox::mm::exception_table::memcpy_fallible( + thread_ctx.svc_frame as *mut u8, + frame.as_ptr().cast(), + core::mem::size_of_val(&frame), + ) + .is_ok() + }; + if frame_staged { + // SAFETY: the captured frame still matches the guest context and + // was restaged with an exception-table-protected write. + unsafe { switch_to_guest_via_outbound_stub(thread_ctx) } + } + } + // SAFETY: generic resume obtains an XNU-created signal context to restore every register. + unsafe { switch_to_guest_via_sigreturn() } +} + +extern "C-unwind" fn syscall_handler(thread_ctx: &mut ThreadContext) { + thread_ctx.call_shim(|shim, ctx| shim.syscall(ctx)); +} + +extern "C-unwind" fn direct_interrupt_handler(thread_ctx: &mut ThreadContext) { + thread_ctx + .thread + .0 + .interrupted + .store(false, Ordering::Release); + thread_ctx.call_shim(|shim, ctx| shim.interrupt(ctx)); +} + +extern "C-unwind" fn init_handler(thread_ctx: &mut ThreadContext) { + thread_ctx.call_shim(|shim, ctx| shim.init(ctx)); +} + +extern "C-unwind" fn exit_handler(thread_ctx: &mut ThreadContext) { + // This callback handles only signal-based exceptions and interrupts. Do + // not let their resulting context accidentally reuse an older SVC frame. + thread_ctx.outbound_stub = 0; + let exit = thread_ctx.exit; + if matches!(exit, GuestExit::Interrupt) { + thread_ctx + .thread + .0 + .interrupted + .store(false, Ordering::Release); + } + thread_ctx.call_shim(|shim, ctx| match exit { + GuestExit::Exception(info) => shim.exception(ctx, &info), + GuestExit::Interrupt => shim.interrupt(ctx), + }); +} + +fn restore_host_fp_state() { + let state = read_tls(tls_offset::HOST_FP_STATE); + let status: u32 = state.trunc(); + let control: u32 = (state >> 32).trunc(); + // SAFETY: these are this thread's host control values, saved immediately + // before entering guest execution. + unsafe { + core::arch::asm!( + "msr fpsr, {status}", + "msr fpcr, {control}", + status = in(reg) u64::from(status), + control = in(reg) u64::from(control), + options(nomem, nostack, preserves_flags), + ); + } +} + +fn set_signal_return(mc: &mut libc::__darwin_mcontext64, thread_ctx: &ThreadContext) { + // Retain the function defining the assembly callback, including in platform-only builds. + core::hint::black_box(run_thread_arch as *const ()); + let host_fp_state = read_tls(tls_offset::HOST_FP_STATE); + mc.__ns.__fpsr = host_fp_state.trunc(); + mc.__ns.__fpcr = (host_fp_state >> 32).trunc(); + mc.__ss.__pc = litebox_macos_host_callback as *const () as u64; + mc.__ss.__sp = thread_ctx.host_sp as u64; + mc.__ss.__fp = (thread_ctx.host_sp + 16) as u64; +} + +fn read_guest(address: usize, output: &mut [u8]) -> bool { + // SAFETY: output is writable; callers read guest mappings or pthread-owned ABI storage. + // Faulting source reads use the installed exception-table handler, not Rust references. + unsafe { + litebox::mm::exception_table::memcpy_fallible( + output.as_mut_ptr(), + address as *const u8, + output.len(), + ) + .is_ok() + } +} + +fn copy_signal_context(regs: &mut PtRegs, mc: &libc::__darwin_mcontext64) { + for (dst, src) in regs.regs[..29].iter_mut().zip(&mc.__ss.__x) { + *dst = src.trunc(); + } + regs.regs[18] = get_guest_x18(); + regs.regs[29] = mc.__ss.__fp.trunc(); + regs.regs[30] = mc.__ss.__lr.trunc(); + regs.sp = mc.__ss.__sp.trunc(); + regs.pc = mc.__ss.__pc.trunc(); + regs.pstate = u64::from(mc.__ss.__cpsr) & litebox_common_linux::arch::SAFE_USER_PSTATE; + regs.orig_x0 = regs.regs[0]; + regs.syscallno = litebox_common_linux::arch::NO_SYSCALL; + let state = read_tls(tls_offset::VECTOR_STATE) as *mut GuestVectorState; + if state.is_null() { + fatal_signal(b"guest vector state is not initialized", regs.pc); + } + let captured = GuestVectorState { + registers: mc.__ns.__v, + fpsr: mc.__ns.__fpsr, + fpcr: mc.__ns.__fpcr, + }; + // SAFETY: state is this thread's allocation; volatile matches transition assembly. + unsafe { state.write_volatile(captured) } +} +fn restore_signal_context(regs: &PtRegs, mc: &mut libc::__darwin_mcontext64) { + set_guest_x18(regs.regs[18]); + for (i, value) in regs.regs[..29].iter().enumerate() { + if i != 18 { + mc.__ss.__x[i] = *value as u64; + } + } + mc.__ss.__fp = regs.regs[29] as u64; + mc.__ss.__lr = regs.regs[30] as u64; + mc.__ss.__sp = regs.sp as u64; + mc.__ss.__pc = regs.pc as u64; + mc.__ss.__cpsr = (regs.pstate & litebox_common_linux::arch::SAFE_USER_PSTATE).trunc(); + let state = read_tls(tls_offset::VECTOR_STATE) as *const GuestVectorState; + if state.is_null() { + fatal_signal(b"guest vector state is not initialized", regs.pc); + } + // SAFETY: state is this thread's allocation; volatile matches transition assembly. + let state = unsafe { state.read_volatile() }; + mc.__ns.__v = state.registers; + mc.__ns.__fpsr = state.fpsr; + mc.__ns.__fpcr = state.fpcr; +} + +fn fatal_signal(message: &[u8], pc: usize) -> ! { + const DIGITS: usize = size_of::() * 2; + let mut address = [b'0'; DIGITS + 1]; + for (index, byte) in address[..DIGITS].iter_mut().enumerate() { + *byte = b"0123456789abcdef"[(pc >> ((DIGITS - index - 1) * 4)) & 15]; + } + address[DIGITS] = b'\n'; + // SAFETY: all buffers are live for their lengths; write and _exit are async-signal-safe. + unsafe { + libc::write(libc::STDERR_FILENO, message.as_ptr().cast(), message.len()); + libc::write(libc::STDERR_FILENO, b" pc=0x".as_ptr().cast(), 6); + libc::write(libc::STDERR_FILENO, address.as_ptr().cast(), address.len()); + libc::_exit(128 + libc::SIGABRT); + } +} + +fn resume_or_interrupt(mc: &mut libc::__darwin_mcontext64, thread_ctx: &mut ThreadContext) { + if thread_ctx.thread.0.interrupted.load(Ordering::Acquire) { + thread_ctx.exit = GuestExit::Interrupt; + set_signal_return(mc, thread_ctx); + } else { + restore_signal_context(thread_ctx.ctx, mc); + write_tls(tls_offset::IN_GUEST, 1); + } +} + +pub(crate) fn register_exception_handlers() -> std::io::Result<()> { + static INSTALLED: Mutex = Mutex::new(false); + let mut installed = INSTALLED.lock().unwrap(); + if *installed { + return Ok(()); + } + // SAFETY: macOS sigaction contains integer fields; zero is a valid representation. + let mut previous = unsafe { std::mem::zeroed::<[libc::sigaction; 5]>() }; + for (signal, previous) in host_signals().into_iter().zip(&mut previous) { + // SAFETY: previous is writable and null requests a query without installing a handler. + if unsafe { libc::sigaction(signal, core::ptr::null(), previous) } != 0 { + return Err(std::io::Error::last_os_error()); + } + } + let previous = PREVIOUS.get_or_init(|| previous); + // SAFETY: zero is valid for every field; the handler, flags and mask are filled below. + let mut action = unsafe { std::mem::zeroed::() }; + action.sa_sigaction = exception_signal_handler as *const () as usize; + action.sa_flags = libc::SA_SIGINFO | libc::SA_ONSTACK | libc::SA_NODEFER; + // SAFETY: action.sa_mask is writable storage for these sigset operations. + unsafe { + libc::sigemptyset(&raw mut action.sa_mask); + // Allow nested memory faults for fallible reads, but not shim re-entry. + libc::sigaddset(&raw mut action.sa_mask, interrupt_signal()); + libc::sigaddset(&raw mut action.sa_mask, libc::SIGTRAP); + } + let restore = |count| { + for (signal, previous) in host_signals().into_iter().zip(previous.iter()).take(count) { + // SAFETY: these immutable actions were returned by sigaction for the same signals. + unsafe { + libc::sigaction(signal, previous, core::ptr::null_mut()); + } + } + }; + for (index, signal) in host_signals().into_iter().enumerate() { + // SAFETY: sigaction consists of zero-valid scalar fields. + let mut replaced = unsafe { std::mem::zeroed::() }; + // SAFETY: action is initialized, replaced is writable, and SA_SIGINFO matches the handler. + let result = unsafe { libc::sigaction(signal, &raw const action, &raw mut replaced) }; + if result != 0 { + let error = std::io::Error::last_os_error(); + restore(index); + return Err(error); + } + if signal == interrupt_signal() && replaced.sa_sigaction != libc::SIG_DFL { + // SAFETY: replaced was atomically returned while installing this signal's action. + unsafe { libc::sigaction(signal, &raw const replaced, core::ptr::null_mut()) }; + restore(index); + return Err(std::io::Error::new( + std::io::ErrorKind::AddrInUse, + "interrupt signal already has a host handler", + )); + } + } + *installed = true; + Ok(()) +} + +fn exception_class(esr: u64) -> u8 { + (esr >> 26).trunc() +} + +fn is_synchronous_memory_fault(signal: i32, code: i32, esr: u64) -> bool { + const SEGV_MAPERR: i32 = 1; + const SEGV_ACCERR: i32 = 2; + let abort = matches!( + Exception(exception_class(esr)), + Exception::INSTRUCTION_ABORT_LOWER_EL + | Exception::INSTRUCTION_ABORT_CURRENT_EL + | Exception::DATA_ABORT_LOWER_EL + | Exception::DATA_ABORT_CURRENT_EL + ); + abort + && match signal { + libc::SIGSEGV => matches!(code, SEGV_MAPERR | SEGV_ACCERR), + libc::SIGBUS => matches!(code, libc::BUS_ADRALN | libc::BUS_ADRERR | libc::BUS_OBJERR), + _ => false, + } +} + +fn gate_interruption(signal: i32, code: i32, esr: u64) -> GateInterruption { + if is_synchronous_memory_fault(signal, code, esr) { + GateInterruption::Synchronous + } else if signal == libc::SIGTRAP && esr >> 26 == u64::from(Exception::BRK64.0) { + GateInterruption::Breakpoint + } else { + GateInterruption::Asynchronous + } +} + +unsafe extern "C" fn exception_signal_handler( + signal: i32, + info: *mut libc::siginfo_t, + raw: *mut libc::c_void, +) { + // SAFETY: SA_SIGINFO supplies a live, aligned ucontext for this invocation. + let uc = unsafe { &mut *raw.cast::() }; + // SAFETY: the machine context is live; nested signals receive separate frames. + let mc = unsafe { &mut *uc.uc_mcontext }; + let pc: usize = mc.__ss.__pc.trunc(); + let esr = u64::from(mc.__es.__esr); + // SAFETY: SA_SIGINFO supplies a live siginfo for this invocation. + let code = unsafe { (*info).si_code }; + if is_synchronous_memory_fault(signal, code, esr) + && let Some(fixup) = litebox::mm::exception_table::search_exception_tables(pc) + { + mc.__ss.__pc = fixup as u64; + return; + } + let ptr = read_tls(tls_offset::ACTIVE) as *mut ThreadContext<'static>; + let breakpoint = signal == libc::SIGTRAP && exception_class(esr) == Exception::BRK64.0; + let in_sigreturn_transition = (switch_to_guest_via_sigreturn_start as *const () as usize + ..switch_to_guest_via_sigreturn_end as *const () as usize) + .contains(&pc); + let resuming = breakpoint && in_sigreturn_transition; + let in_guest = read_tls(tls_offset::IN_GUEST) != 0; + let in_syscall_callback_prologue = (syscall_callback as *const () as usize + ..litebox_macos_syscall_callback_in_guest_cleared as *const () as usize) + .contains(&pc); + let in_outbound_transition = (switch_to_guest_via_outbound_stub_start as *const () as usize + ..switch_to_guest_via_outbound_stub_end as *const () as usize) + .contains(&pc); + if signal != interrupt_signal() && matches!(code, SI_USER | SI_QUEUE) { + // Application-originated signals are not a guest signal source. Restore + // host FP control state before invoking arbitrary host signal code; XNU + // restores the interrupted context if that handler returns. + if !ptr.is_null() + && (in_guest + || in_syscall_callback_prologue + || in_outbound_transition + || in_sigreturn_transition) + { + restore_host_fp_state(); + } + // SAFETY: the kernel-provided signal arguments remain live for forwarding. + unsafe { next_signal_handler(signal, info, raw) }; + return; + } + if !ptr.is_null() && in_syscall_callback_prologue { + if signal != interrupt_signal() { + // TODO: preserve transition diagnostics while forwarding this fault + // to the previous host disposition instead of exiting directly. + fatal_signal(b"fault in macOS syscall transition", pc); + } + // ThreadState::interrupt already recorded the request. Let the direct + // callback finish saving a coherent guest context before dispatching it. + return; + } + if !ptr.is_null() && (in_outbound_transition || in_sigreturn_transition) { + if resuming { + restore_host_fp_state(); + write_tls(tls_offset::IN_GUEST, 0); + // SAFETY: ACTIVE remains live while run_thread_arch is suspended. + resume_or_interrupt(mc, unsafe { &mut *ptr }); + return; + } + if signal != interrupt_signal() { + // TODO: preserve transition diagnostics while forwarding this fault + // to the previous host disposition instead of exiting directly. + fatal_signal(b"fault in macOS guest-resume transition", pc); + } + restore_host_fp_state(); + write_tls(tls_offset::IN_GUEST, 0); + // SAFETY: ACTIVE remains live while run_thread_arch is suspended. + let thread_ctx = unsafe { &mut *ptr }; + thread_ctx.exit = GuestExit::Interrupt; + set_signal_return(mc, thread_ctx); + return; + } + if ptr.is_null() || !in_guest { + if signal != interrupt_signal() { + // SAFETY: the kernel-provided signal arguments remain live for forwarding. + unsafe { next_signal_handler(signal, info, raw) }; + } + return; + } + restore_host_fp_state(); + write_tls(tls_offset::IN_GUEST, 0); + // SAFETY: ACTIVE remains live while run_thread_arch is suspended; nested shim access is disabled. + let thread_ctx = unsafe { &mut *ptr }; + + copy_signal_context(thread_ctx.ctx, mc); + match canonicalize( + thread_ctx.ctx, + GateRuntimeState { + guest_thread_pointer_addr: tls_address(tls_offset::GUEST_THREAD_POINTER) as usize, + // Signal exits have an authoritative XNU context and do not + // originate from the direct outbound-stub resume path. + expected_outbound_stub: 0, + expected_outbound_pc: 0, + }, + gate_interruption(signal, code, esr), + litebox_syscall_rewriter::TargetHost::MacOs, + true, + read_guest, + ) { + Aarch64GateSignalResult::NotGate => {} + Aarch64GateSignalResult::Canonicalized(ctx) => *thread_ctx.ctx = ctx, + Aarch64GateSignalResult::ResumeGuest(ctx) => { + *thread_ctx.ctx = ctx; + resume_or_interrupt(mc, thread_ctx); + return; + } + Aarch64GateSignalResult::InvalidRuntimeState => { + fatal_signal(b"invalid AArch64 gate runtime state", pc) + } + // With both expected outbound values zero, canonicalize cannot + // classify a macOS signal context as an interrupted outbound stub. + Aarch64GateSignalResult::PreserveSavedContext => { + fatal_signal(b"unreachable macOS outbound-stub recovery", pc) + } + } + thread_ctx.exit = if signal == interrupt_signal() { + GuestExit::Interrupt + } else { + let exception = match signal { + libc::SIGILL => Exception::INSTRUCTION_ABORT_LOWER_EL, + libc::SIGTRAP => Exception::BRK64, + _ if is_synchronous_memory_fault(signal, code, esr) + && matches!( + Exception(exception_class(esr)), + Exception::INSTRUCTION_ABORT_LOWER_EL | Exception::INSTRUCTION_ABORT_CURRENT_EL + ) => + { + Exception::INSTRUCTION_ABORT_LOWER_EL + } + _ => Exception::DATA_ABORT_LOWER_EL, + }; + GuestExit::Exception(ExceptionInfo { + exception, + fault_address: mc.__es.__far.trunc(), + esr, + kernel_mode: false, + }) + }; + set_signal_return(mc, thread_ctx); +} + +unsafe fn next_signal_handler(signal: i32, info: *mut libc::siginfo_t, raw: *mut libc::c_void) { + let Some(previous) = host_signals() + .iter() + .position(|s| *s == signal) + .and_then(|index| PREVIOUS.get()?.get(index)) + else { + fatal_signal(b"missing host signal disposition", 0); + }; + match previous.sa_sigaction { + // SAFETY: signal came from host_signals(); these scalar APIs restore its default disposition. + libc::SIG_DFL => unsafe { + libc::signal(signal, libc::SIG_DFL); + libc::raise(signal); + }, + libc::SIG_IGN => {} + // SAFETY: sigaction returned a live callback, and the sentinel cases were excluded. + // SA_SIGINFO selects this three-argument C ABI; info and raw remain live for the call. + handler if previous.sa_flags & libc::SA_SIGINFO != 0 => unsafe { + let handler: unsafe extern "C" fn(i32, *mut libc::siginfo_t, *mut libc::c_void) = + std::mem::transmute(handler); + handler(signal, info, raw); + }, + // SAFETY: the saved non-sentinel callback lacks SA_SIGINFO, selecting the one-argument C ABI. + handler => unsafe { + let handler: unsafe extern "C" fn(i32) = std::mem::transmute(handler); + handler(signal); + }, + } +} + +unsafe extern "C" { + fn litebox_macos_host_callback(); +} + +#[unsafe(naked)] +unsafe extern "C-unwind" fn run_thread_arch(_: &mut ThreadContext) { + // SAFETY: the caller supplies live thread state and guest mappings. Both + // entry paths use the same host frame, including during Rust unwinding. + core::arch::naked_asm!( + ".cfi_startproc", + "stp x29, x30, [sp, #-160]!", + ".cfi_def_cfa_offset 160", + ".cfi_offset x29, -160", + ".cfi_offset x30, -152", + "mov x29, sp", + ".cfi_def_cfa x29, 160", + "stp x19, x20, [sp, #16]", + ".cfi_offset x19, -144", + ".cfi_offset x20, -136", + "stp x21, x22, [sp, #32]", + ".cfi_offset x21, -128", + ".cfi_offset x22, -120", + "stp x23, x24, [sp, #48]", + ".cfi_offset x23, -112", + ".cfi_offset x24, -104", + "stp x25, x26, [sp, #64]", + ".cfi_offset x25, -96", + ".cfi_offset x26, -88", + "stp x27, x28, [sp, #80]", + ".cfi_offset x27, -80", + ".cfi_offset x28, -72", + "stp d8, d9, [sp, #96]", + ".cfi_offset d8, -64", + ".cfi_offset d9, -56", + "stp d10, d11, [sp, #112]", + ".cfi_offset d10, -48", + ".cfi_offset d11, -40", + "stp d12, d13, [sp, #128]", + ".cfi_offset d12, -32", + ".cfi_offset d13, -24", + "stp d14, d15, [sp, #144]", + ".cfi_offset d14, -16", + ".cfi_offset d15, -8", + "sub sp, sp, #16", + "str x0, [sp]", + "mov x1, sp", + "str x1, [x0, #{host_sp}]", + "bl {init_handler}", + "b {finish_thread_arch}", + // Mach-O requires a separate FDE for this alternate entry point. + ".cfi_endproc", + ".globl _litebox_macos_host_callback", + "_litebox_macos_host_callback:", + ".cfi_startproc", + ".cfi_def_cfa x29, 160", + ".cfi_offset x29, -160", + ".cfi_offset x30, -152", + ".cfi_offset x19, -144", + ".cfi_offset x20, -136", + ".cfi_offset x21, -128", + ".cfi_offset x22, -120", + ".cfi_offset x23, -112", + ".cfi_offset x24, -104", + ".cfi_offset x25, -96", + ".cfi_offset x26, -88", + ".cfi_offset x27, -80", + ".cfi_offset x28, -72", + ".cfi_offset d8, -64", + ".cfi_offset d9, -56", + ".cfi_offset d10, -48", + ".cfi_offset d11, -40", + ".cfi_offset d12, -32", + ".cfi_offset d13, -24", + ".cfi_offset d14, -16", + ".cfi_offset d15, -8", + "ldr x0, [sp]", + "bl {exit_handler}", + "b {finish_thread_arch}", + ".cfi_endproc", + // Direct entry from the FDE-less outbound stub. x0 still carries + // ThreadContext; reload the canonical host sp and x29 below. + ".globl _litebox_macos_interrupt_callback", + "_litebox_macos_interrupt_callback:", + ".cfi_startproc", + ".cfi_def_cfa x29, 160", + ".cfi_offset x29, -160", + ".cfi_offset x30, -152", + ".cfi_offset x19, -144", + ".cfi_offset x20, -136", + ".cfi_offset x21, -128", + ".cfi_offset x22, -120", + ".cfi_offset x23, -112", + ".cfi_offset x24, -104", + ".cfi_offset x25, -96", + ".cfi_offset x26, -88", + ".cfi_offset x27, -80", + ".cfi_offset x28, -72", + ".cfi_offset d8, -64", + ".cfi_offset d9, -56", + ".cfi_offset d10, -48", + ".cfi_offset d11, -40", + ".cfi_offset d12, -32", + ".cfi_offset d13, -24", + ".cfi_offset d14, -16", + ".cfi_offset d15, -8", + "ldr x1, [x0, #{host_sp}]", + "mov sp, x1", + "add x29, sp, #16", + "bl {direct_interrupt_handler}", + "b {finish_thread_arch}", + ".cfi_endproc", + host_sp = const core::mem::offset_of!(ThreadContext, host_sp), + init_handler = sym init_handler, + exit_handler = sym exit_handler, + direct_interrupt_handler = sym direct_interrupt_handler, + finish_thread_arch = sym finish_thread_arch, + ); +} + +#[unsafe(naked)] +unsafe extern "C" fn finish_thread_arch() { + core::arch::naked_asm!( + ".cfi_startproc", + ".cfi_def_cfa x29, 160", + ".cfi_offset x29, -160", + ".cfi_offset x30, -152", + "add sp, sp, #16", + "ldp x19, x20, [sp, #16]", + "ldp x21, x22, [sp, #32]", + "ldp x23, x24, [sp, #48]", + "ldp x25, x26, [sp, #64]", + "ldp x27, x28, [sp, #80]", + "ldp d8, d9, [sp, #96]", + "ldp d10, d11, [sp, #112]", + "ldp d12, d13, [sp, #128]", + "ldp d14, d15, [sp, #144]", + "ldp x29, x30, [sp], #160", + ".cfi_def_cfa sp, 0", + "ret", + ".cfi_endproc", + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use litebox::platform::{PageManagementProvider as _, RawMutPointer as _}; + const RW: MemoryRegionPermissions = + MemoryRegionPermissions::READ.union(MemoryRegionPermissions::WRITE); + + #[test] + fn transition_ranges_have_stable_sizes() { + let syscall_prologue = syscall_callback as *const () as usize + ..litebox_macos_syscall_callback_in_guest_cleared as *const () as usize; + let sigreturn = switch_to_guest_via_sigreturn_start as *const () as usize + ..switch_to_guest_via_sigreturn_end as *const () as usize; + let outbound = switch_to_guest_via_outbound_stub_start as *const () as usize + ..switch_to_guest_via_outbound_stub_end as *const () as usize; + assert_eq!(syscall_prologue.len(), 76 * size_of::()); + assert_eq!(sigreturn.len(), 3 * size_of::()); + assert_eq!(outbound.len(), 53 * size_of::()); + } + + #[test] + fn ptregs_layout_matches_transition_assembly() { + assert_eq!(core::mem::size_of::(), 288); + assert_eq!(core::mem::align_of::(), 16); + assert_eq!(core::mem::offset_of!(PtRegs, regs) + 16 * 8, 128); + assert_eq!(core::mem::offset_of!(PtRegs, sp), 248); + assert_eq!(core::mem::offset_of!(PtRegs, pc), 256); + assert_eq!(core::mem::offset_of!(PtRegs, pstate), 264); + assert_eq!(core::mem::offset_of!(PtRegs, orig_x0), 272); + assert_eq!(core::mem::offset_of!(PtRegs, syscallno), 280); + } + + #[test] + fn unwind_through_run_thread_arch() { + struct PanickingShim; + + impl EnterShim for PanickingShim { + type ExecutionContext = PtRegs; + + fn init(&self, _ctx: &mut PtRegs) -> ContinueOperation { + panic!("unwind out of init_handler"); + } + + fn syscall(&self, _ctx: &mut PtRegs) -> ContinueOperation { + unreachable!() + } + + fn exception(&self, _ctx: &mut PtRegs, _info: &ExceptionInfo) -> ContinueOperation { + unreachable!() + } + + fn interrupt(&self, _ctx: &mut PtRegs) -> ContinueOperation { + unreachable!() + } + } + + MacosUserland::new(); + let result = std::panic::catch_unwind(|| { + let mut ctx = PtRegs::default(); + // SAFETY: init panics before the context is used for guest entry. + unsafe { run_thread(PanickingShim, &mut ctx) }; + }); + assert!(result.is_err(), "the panic must propagate to the caller"); + } + + #[test] + fn pstate_capture_and_restore_preserve_only_user_bits() { + use litebox_common_linux::arch::{ + PSR_DIT_BIT, PSR_NZCV_MASK, PSR_SSBS_BIT, SAFE_USER_PSTATE, + }; + + MacosUserland::new(); + let vector = get_guest_vector_state(); + let _restore = litebox::utils::defer(|| set_guest_vector_state(&vector)); + // SAFETY: this register-state struct contains only integers and arrays; zero is valid. + let mut mc: libc::__darwin_mcontext64 = unsafe { core::mem::zeroed() }; + let mut regs = PtRegs::default(); + for (index, value) in mc.__ss.__x.iter_mut().enumerate() { + *value = 0x1000 + index as u64; + } + mc.__ss.__fp = 0x2000; + mc.__ss.__lr = 0x3000; + mc.__ss.__sp = 0x4000; + mc.__ss.__pc = 0x5000; + for (index, value) in mc.__ns.__v.iter_mut().enumerate() { + *value = 0x6000 + index as u128; + } + mc.__ns.__fpsr = 0x7000; + mc.__ns.__fpcr = 0x8000; + let input_mc = mc; + for bits in [ + 0, + PSR_NZCV_MASK, + PSR_SSBS_BIT, + PSR_DIT_BIT, + SAFE_USER_PSTATE, + ] { + mc = input_mc; + set_guest_x18(0x1818); + mc.__ss.__cpsr = (bits | !SAFE_USER_PSTATE).trunc(); + copy_signal_context(&mut regs, &mc); + assert_eq!(regs.regs[..18], (0x1000..0x1012).collect::>()); + assert_eq!(regs.regs[18], 0x1818); + assert_eq!(regs.regs[19..29], (0x1013..0x101d).collect::>()); + assert_eq!((regs.regs[29], regs.regs[30]), (0x2000, 0x3000)); + assert_eq!((regs.sp, regs.pc), (0x4000, 0x5000)); + assert_eq!(regs.pstate, bits); + let saved_vector = get_guest_vector_state(); + assert_eq!(saved_vector.registers, mc.__ns.__v); + assert_eq!((saved_vector.fpsr, saved_vector.fpcr), (0x7000, 0x8000)); + + regs.regs = core::array::from_fn(|index| 0x9000 + index); + regs.sp = 0xa000; + regs.pc = 0xb000; + regs.pstate = bits | !SAFE_USER_PSTATE; + let restored_vector = GuestVectorState { + registers: core::array::from_fn(|index| 0xc000 + index as u128), + fpsr: 0xd000, + fpcr: 0xe000, + }; + set_guest_vector_state(&restored_vector); + restore_signal_context(®s, &mut mc); + for (index, value) in mc.__ss.__x.iter().enumerate() { + if index != 18 { + assert_eq!(*value, 0x9000 + index as u64); + } + } + assert_eq!((mc.__ss.__fp, mc.__ss.__lr), (0x901d, 0x901e)); + assert_eq!((mc.__ss.__sp, mc.__ss.__pc), (0xa000, 0xb000)); + assert_eq!(u64::from(mc.__ss.__cpsr), bits); + assert_eq!(mc.__ns.__v, restored_vector.registers); + assert_eq!((mc.__ns.__fpsr, mc.__ns.__fpcr), (0xd000, 0xe000)); + } + } + + #[test] + fn initialization_preserves_tls_across_instances() { + let first = MacosUserland::new(); + let original = ( + get_guest_thread_pointer(), + get_guest_x18(), + read_tls(tls_offset::IN_GUEST), + ); + let _restore = litebox::utils::defer(|| { + set_guest_thread_pointer(original.0); + set_guest_x18(original.1); + write_tls(tls_offset::IN_GUEST, original.2); + }); + set_guest_thread_pointer(0x1234); + set_guest_x18(0x5678); + write_tls(tls_offset::IN_GUEST, 1); + let second = MacosUserland::new(); + assert!(!core::ptr::eq(first, second)); + assert_eq!( + ( + get_guest_thread_pointer(), + get_guest_x18(), + read_tls(tls_offset::IN_GUEST), + ), + (0x1234, 0x5678, 1) + ); + initialize_thread_tls().unwrap(); + assert_eq!( + ( + get_guest_thread_pointer(), + get_guest_x18(), + read_tls(tls_offset::IN_GUEST), + ), + (0x1234, 0x5678, 1) + ); + std::thread::spawn(MacosUserland::new).join().unwrap(); + } + + #[test] + fn executable_protection_after_prot_none() { + let p = MacosUserland::new(); + let ptr = p + .allocate_pages( + TASK_ADDR_MIN..TASK_ADDR_MIN + PAGE_SIZE, + RW, + false, + true, + FixedAddressBehavior::Hint, + ) + .unwrap(); + let range = ptr.as_usize()..ptr.as_usize() + PAGE_SIZE; + let _unmap = litebox::utils::defer(|| { + // SAFETY: the test owns the mapping and its code has returned before cleanup. + unsafe { + p.deallocate_pages(range.clone()).unwrap(); + } + }); + // mov x0, #42; ret + assert_eq!( + ptr.write_slice_at_offset(0, &[0x40, 0x05, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6]), + Some(()) + ); + for permissions in [ + MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC, + MemoryRegionPermissions::EXEC, + ] { + // SAFETY: the test exclusively owns the mapping; its RX code is a C-ABI mov/ret stub. + // The assembly declares the call's register clobbers. + unsafe { + p.update_permissions(range.clone(), MemoryRegionPermissions::empty()) + .unwrap(); + p.update_permissions(range.clone(), permissions).unwrap(); + let value: usize; + core::arch::asm!("blr {entry}", entry = in(reg) range.start, + lateout("x0") value, clobber_abi("C")); + assert_eq!(value, 42); + } + } + } + + #[test] + fn executable_remap_preserves_code_without_wx() { + let p = MacosUserland::new(); + let source = p + .allocate_pages( + TASK_ADDR_MIN..TASK_ADDR_MIN + PAGE_SIZE, + RW, + false, + true, + FixedAddressBehavior::Hint, + ) + .unwrap(); + let source_range = source.as_usize()..source.as_usize() + PAGE_SIZE; + assert_eq!( + source.write_slice_at_offset(0, &[0x40, 0x05, 0x80, 0xd2, 0xc0, 0x03, 0x5f, 0xd6]), + Some(()) + ); + // SAFETY: the test exclusively owns the idle source mapping. + unsafe { + p.update_permissions( + source_range.clone(), + MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC, + ) + .unwrap(); + } + let target = p + .allocate_pages( + TASK_ADDR_MIN..TASK_ADDR_MIN + 2 * PAGE_SIZE, + RW, + false, + true, + FixedAddressBehavior::Hint, + ) + .unwrap(); + let target_range = target.as_usize()..target.as_usize() + 2 * PAGE_SIZE; + // SAFETY: release the probe, then occupy its range as host memory so + // remap_pages must choose a different destination without replacing it. + unsafe { p.deallocate_pages(target_range.clone()).unwrap() }; + // SAFETY: target_range was just released and cannot overlap live Rust allocations. + let host_mapping = unsafe { + libc::mmap( + target_range.start as *mut _, + target_range.len(), + libc::PROT_NONE, + libc::MAP_PRIVATE | libc::MAP_ANON | libc::MAP_FIXED, + -1, + 0, + ) + }; + assert_eq!(host_mapping as usize, target_range.start); + let _host_unmap = litebox::utils::defer(|| { + // SAFETY: this test owns the host mapping and the remap leaves it intact. + assert_eq!(unsafe { libc::munmap(host_mapping, target_range.len()) }, 0); + }); + // SAFETY: both guest ranges are idle, aligned, and non-overlapping. + let remapped = unsafe { + p.remap_pages( + source_range, + target_range.clone(), + MemoryRegionPermissions::READ | MemoryRegionPermissions::EXEC, + ) + .unwrap() + }; + assert_ne!(remapped.as_usize(), target_range.start); + let remapped_range = remapped.as_usize()..remapped.as_usize() + target_range.len(); + let value: usize; + // SAFETY: remap preserved the C-ABI mov/ret stub and installed RX permissions. + unsafe { + core::arch::asm!("blr {entry}", entry = in(reg) remapped.as_usize(), + lateout("x0") value, clobber_abi("C")); + } + assert_eq!(value, 42); + // SAFETY: the remapped code has returned and the test owns its actual range. + unsafe { p.deallocate_pages(remapped_range).unwrap() }; + } + + #[test] + fn permission_denials_are_not_reported_as_missing_pages() { + unsafe extern "C" { + fn mach_vm_protect( + task: u32, + address: u64, + size: u64, + set_maximum: i32, + protection: i32, + ) -> i32; + } + let p = MacosUserland::new(); + let ptr = p + .allocate_pages( + TASK_ADDR_MIN..TASK_ADDR_MIN + PAGE_SIZE, + RW, + false, + true, + FixedAddressBehavior::Hint, + ) + .unwrap(); + let range = ptr.as_usize()..ptr.as_usize() + PAGE_SIZE; + let _unmap = litebox::utils::defer(|| { + // SAFETY: the test owns this mapping and has no active accesses at cleanup. + unsafe { + p.deallocate_pages(range.clone()).unwrap(); + } + }); + assert!(matches!( + p.allocate_pages( + range.clone(), + RW | MemoryRegionPermissions::EXEC, + false, + true, + FixedAddressBehavior::Replace + ), + Err(AllocationError::PermissionDenied) + )); + assert!(matches!( + // SAFETY: the test owns the idle mapping; denied permissions must leave it intact. + unsafe { p.update_permissions(range.clone(), RW | MemoryRegionPermissions::EXEC) }, + Err(PermissionUpdateError::PermissionDenied) + )); + // SAFETY: this aligned range is exclusively test-owned; no live references require write access. + unsafe { + assert_eq!( + mach_vm_protect( + mach_task_self(), + range.start as u64, + PAGE_SIZE as u64, + 1, + libc::PROT_READ + ), + 0 + ); + assert!(matches!( + p.update_permissions(range.clone(), RW), + Err(PermissionUpdateError::PermissionDenied) + )); + } + } + + #[test] + fn native_pages_preserve_neighbors_and_reject_collisions() { + let p = MacosUserland::new(); + let ptr = p + .allocate_pages( + TASK_ADDR_MIN..TASK_ADDR_MIN + 2 * PAGE_SIZE, + RW, + false, + true, + FixedAddressBehavior::Hint, + ) + .unwrap(); + let base = ptr.as_usize(); + assert_eq!(base % PAGE_SIZE, 0); + assert_eq!(ptr.read_at_offset(0), Some(0)); + assert_eq!(ptr.write_at_offset(PAGE_SIZE.cast_signed(), 0x5a), Some(())); + assert!(matches!( + p.allocate_pages( + base..base + PAGE_SIZE, + RW, + false, + true, + FixedAddressBehavior::NoReplace + ), + Err(AllocationError::AddressInUse) + )); + p.allocate_pages( + base..base + PAGE_SIZE, + RW, + false, + true, + FixedAddressBehavior::Replace, + ) + .unwrap(); + assert_eq!(ptr.read_at_offset(PAGE_SIZE.cast_signed()), Some(0x5a)); + // SAFETY: no accesses to the first test-owned page overlap this permission change. + unsafe { + p.update_permissions(base..base + PAGE_SIZE, MemoryRegionPermissions::READ) + .unwrap(); + } + assert_eq!(ptr.write_at_offset(0, 1), None); // Fault-safe exception-table recovery + assert_eq!(ptr.write_at_offset(PAGE_SIZE.cast_signed(), 0x6b), Some(())); + // SAFETY: the first page is idle; subsequent probes use fallible raw accesses. + unsafe { + p.deallocate_pages(base..base + PAGE_SIZE).unwrap(); + } + assert_eq!(ptr.read_at_offset(0), None); + assert_eq!(ptr.read_at_offset(PAGE_SIZE.cast_signed()), Some(0x6b)); + // SAFETY: the remaining test-owned page is no longer accessed. + unsafe { + p.deallocate_pages(base + PAGE_SIZE..base + 2 * PAGE_SIZE) + .unwrap(); + } + } + + #[test] + fn fixed_mappings_never_replace_host_memory() { + let p = MacosUserland::new(); + // SAFETY: request fresh anonymous memory with no fixed-address replacement. + let host = unsafe { + libc::mmap( + core::ptr::null_mut(), + PAGE_SIZE, + libc::PROT_READ | libc::PROT_WRITE, + libc::MAP_PRIVATE | libc::MAP_ANON, + -1, + 0, + ) + }; + assert_ne!(host, libc::MAP_FAILED); + // SAFETY: mmap succeeded with write permission; this test exclusively owns the byte. + unsafe { + host.cast::().write(0x42); + } + for behavior in [ + FixedAddressBehavior::NoReplace, + FixedAddressBehavior::Replace, + ] { + assert!(matches!( + p.allocate_pages( + host as usize..host as usize + PAGE_SIZE, + RW, + false, + true, + behavior + ), + Err(AllocationError::AddressInUseByPlatform) + )); + } + // SAFETY: the range is idle; the platform must leave this unowned mapping intact. + unsafe { + p.deallocate_pages(host as usize..host as usize + PAGE_SIZE) + .unwrap(); + } + // SAFETY: rejected replacements and unowned deallocation leave the initialized byte mapped. + assert_eq!(unsafe { host.cast::().read() }, 0x42); + // SAFETY: this releases the test's still-live mapping after its last access. + unsafe { + libc::munmap(host, PAGE_SIZE); + } + } +} diff --git a/litebox_runner_linux_on_macos_userland/Cargo.toml b/litebox_runner_linux_on_macos_userland/Cargo.toml new file mode 100644 index 000000000..a72588adf --- /dev/null +++ b/litebox_runner_linux_on_macos_userland/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "litebox_runner_linux_on_macos_userland" +version = "0.1.0" +edition = "2024" + +[target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +litebox = { path = "../litebox", version = "0.1.0" } +litebox_common_linux = { path = "../litebox_common_linux", version = "0.1.0" } +litebox_platform_macos_userland = { path = "../litebox_platform_macos_userland", version = "0.1.0" } +litebox_shim_linux = { path = "../litebox_shim_linux", version = "0.1.0", default-features = false, features = ["alarm_fallback"] } +litebox_util_log = { path = "../litebox_util_log", version = "0.1.0", features = ["backend_tracing"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dev-dependencies] +tempfile = "3" + +[lints] +workspace = true diff --git a/litebox_runner_linux_on_macos_userland/src/lib.rs b/litebox_runner_linux_on_macos_userland/src/lib.rs new file mode 100644 index 000000000..9f8426235 --- /dev/null +++ b/litebox_runner_linux_on_macos_userland/src/lib.rs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Run AArch64 Linux PIE programs on an AArch64 macOS host. +#![cfg(all(target_os = "macos", target_arch = "aarch64"))] + +use anyhow::{Context as _, Result, bail}; +use clap::Parser; +use litebox::fs::{ + Mode, UserInfo, + in_mem::{InMem, InitialNode}, +}; +use litebox_platform_macos_userland::MacosUserland as Platform; +use std::ffi::CString; +use std::os::unix::fs::MetadataExt as _; +use std::path::PathBuf; +use std::sync::Arc; + +const DEFAULT_GUEST_UID: u16 = 1000; +const DEFAULT_GUEST_GID: u16 = 1000; + +#[derive(Parser, Debug)] +#[command(about = "Run AArch64 Linux PIE programs on an AArch64 macOS host")] +pub struct CliArgs { + /// Program and its arguments; host path unless --program-from-tar is set. + #[arg(required = true, trailing_var_arg = true, value_hint = clap::ValueHint::CommandWithArguments)] + pub program_and_arguments: Vec, + /// Guest environment variable, KEY=VALUE. + #[arg(long = "env")] + pub environment_variables: Vec, + /// Forward the host environment. + #[arg(long = "forward-env")] + pub forward_environment_variables: bool, + /// Enable unstable runner options. + #[arg(short = 'Z', long = "unstable")] + pub unstable: bool, + /// Uncompressed tar containing the Linux interpreter, libraries and files. + #[arg(long = "initial-files", value_name = "PATH_TO_TAR", value_hint = clap::ValueHint::FilePath, + requires = "unstable", help_heading = "Unstable Options")] + pub initial_files: Option, + /// Resolve the absolute program path within --initial-files. + #[arg(long = "program-from-tar", requires_all = ["unstable", "initial_files"], help_heading = "Unstable Options")] + pub program_from_tar: bool, +} + +/// Load and run a Linux program. +/// +/// # Panics +/// Unsupported guest operations may still panic in the Linux shim. +pub fn run(cli_args: CliArgs) -> Result { + tracing_subscriber::fmt() + .with_timer(tracing_subscriber::fmt::time::uptime()) + .with_level(true) + .with_env_filter( + tracing_subscriber::EnvFilter::builder() + .with_env_var("LITEBOX_LOG") + .from_env_lossy(), + ) + .init(); + + let program = cli_args + .program_and_arguments + .first() + .context("missing program path")?; + let prog = if cli_args.program_from_tar { + if !program.starts_with('/') { + bail!("--program-from-tar requires an absolute guest path"); + } + PathBuf::from(program) + } else { + std::path::absolute(program)? + }; + let prog_path = prog.to_str().context("program path must be UTF-8")?; + + let (ancestor_modes_and_users, prog_data) = if cli_args.program_from_tar { + (Vec::new(), None) + } else { + let modes = prog + .ancestors() + .collect::>() + .into_iter() + .rev() + .skip(1) + .map(|path| { + let metadata = path + .metadata() + .with_context(|| format!("reading metadata for {}", path.display()))?; + Ok(( + Mode::from_bits(metadata.mode()).context("unsupported file mode")?, + metadata.uid(), + )) + }) + .collect::>>()?; + let data = std::fs::read(&prog).with_context(|| format!("reading {}", prog.display()))?; + (modes, Some(data)) + }; + let tar_data = if let Some(tar_file) = &cli_args.initial_files { + if tar_file.extension().and_then(|x| x.to_str()) != Some("tar") { + bail!("Expected a .tar file, found {}", tar_file.display()); + } + std::fs::read(tar_file).with_context(|| format!("reading {}", tar_file.display()))? + } else { + litebox::fs::tar_ro::EMPTY_TAR_FILE.to_vec() + }; + + let platform = Platform::new(); + let shim_builder = litebox_shim_linux::LinuxShimBuilder::new(platform); + let task_params = litebox_common_linux::TaskParams { + pid: 1, + ppid: 0, + uid: u32::from(DEFAULT_GUEST_UID), + euid: u32::from(DEFAULT_GUEST_UID), + gid: u32::from(DEFAULT_GUEST_GID), + egid: u32::from(DEFAULT_GUEST_GID), + }; + let initial_file_system = { + let owner_of = |parent_host_user: u32, host_user: u32| { + if parent_host_user == 0 && host_user == 0 { + UserInfo::ROOT + } else { + UserInfo { + user: DEFAULT_GUEST_UID, + group: DEFAULT_GUEST_GID, + } + } + }; + let mut entries = Vec::new(); + if let Some(prog_data) = prog_data { + let mut prev_user = 0; + for (path, &(mode, user)) in prog + .ancestors() + .skip(1) + .collect::>() + .into_iter() + .rev() + .skip(1) + .zip(&ancestor_modes_and_users) + { + entries.push(( + path.to_str().context("non-UTF-8 ancestor")?.to_owned(), + InitialNode::Directory { + mode, + owner: owner_of(prev_user, user), + }, + )); + prev_user = user; + } + let &(mode, user) = ancestor_modes_and_users + .last() + .context("program path has no ancestors")?; + entries.push(( + prog_path.to_owned(), + InitialNode::File { + mode, + owner: owner_of(prev_user, user), + data: prog_data.into(), + }, + )); + } + let tmp_mode = Mode::RWXU | Mode::RWXG | Mode::RWXO; + if let Some((_, node)) = entries.iter_mut().find(|(path, _)| path == "/tmp") { + let InitialNode::Directory { mode, .. } = node else { + unreachable!() + }; + *mode = tmp_mode; + } else { + entries.push(( + "/tmp".to_owned(), + InitialNode::Directory { + mode: tmp_mode, + owner: UserInfo::ROOT, + }, + )); + } + shim_builder.default_fs(InMem::new_initialized(entries), tar_data.into()) + }; + let initial_file_system = Arc::new(initial_file_system); + let shim = shim_builder.build(); + + let argv = cli_args + .program_and_arguments + .iter() + .map(|value| CString::new(value.as_bytes())) + .collect::, _>>()?; + let mut environment = cli_args.environment_variables; + if cli_args.forward_environment_variables { + environment.extend(std::env::vars().map(|(key, value)| format!("{key}={value}"))); + } + let envp = environment + .iter() + .map(|value| CString::new(value.as_bytes())) + .collect::, _>>()?; + let program = shim + .load_program(initial_file_system, task_params, prog_path, argv, envp) + .context("loading Linux ELF (requires a PIE and 16 KiB-compatible LOAD segments)")?; + // SAFETY: the shim loader supplies valid initial guest code and stack mappings. + unsafe { + litebox_platform_macos_userland::run_thread( + program.entrypoints, + &mut litebox_common_linux::PtRegs::default(), + ); + } + Ok(program.process.wait_for_unix_shell_exit_code()) +} diff --git a/litebox_runner_linux_on_macos_userland/src/main.rs b/litebox_runner_linux_on_macos_userland/src/main.rs new file mode 100644 index 000000000..9e8dd69b2 --- /dev/null +++ b/litebox_runner_linux_on_macos_userland/src/main.rs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use clap::Parser as _; +use litebox_runner_linux_on_macos_userland::CliArgs; + +fn main() -> anyhow::Result<()> { + let exit_code = litebox_runner_linux_on_macos_userland::run(CliArgs::parse())?; + std::process::exit(exit_code) +} diff --git a/litebox_runner_linux_on_macos_userland/tests/loader.rs b/litebox_runner_linux_on_macos_userland/tests/loader.rs new file mode 100644 index 000000000..075f80c65 --- /dev/null +++ b/litebox_runner_linux_on_macos_userland/tests/loader.rs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#![cfg(all(target_os = "macos", target_arch = "aarch64"))] + +use std::{path::Path, process::Command}; + +// Prebuilt AArch64 Linux programs with their dynamic loader and glibc. +fn run_program(name: &str) { + let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/test-bins"); + let directory = tempfile::tempdir().unwrap(); + let root = directory.path().join("root"); + for (source, destination) in [ + (name, format!("bin/{name}")), + ("ld-linux-aarch64.so.1", "lib/ld-linux-aarch64.so.1".into()), + ("libc.so.6", "lib/aarch64-linux-gnu/libc.so.6".into()), + ] { + let path = root.join(destination); + std::fs::create_dir_all(path.parent().unwrap()).unwrap(); + std::fs::copy(fixtures.join(source), &path).unwrap(); + } + let archive = directory.path().join("root.tar"); + let tar = Command::new("tar") + .env("COPYFILE_DISABLE", "1") + .args(["--format=ustar", "-cf"]) + .arg(&archive) + .arg("-C") + .arg(&root) + .args(["bin", "lib"]) + .output() + .unwrap(); + assert!( + tar.status.success(), + "{}", + String::from_utf8_lossy(&tar.stderr) + ); + let runner = std::env::var_os("NEXTEST_BIN_EXE_litebox_runner_linux_on_macos_userland") + .unwrap_or_else(|| env!("CARGO_BIN_EXE_litebox_runner_linux_on_macos_userland").into()); + for from_tar in [false, true] { + let mut command = Command::new(&runner); + command + .args(["-Z", "--initial-files"]) + .arg(&archive) + .args(["--env", "LD_LIBRARY_PATH=/lib/aarch64-linux-gnu"]); + if from_tar { + command + .arg("--program-from-tar") + .arg(format!("/bin/{name}")); + } else { + command.arg(root.join("bin").join(name)); + } + let output = command.output().unwrap(); + // Stdout requires a broker; these tests check successful execution. + assert_eq!( + output.status.code(), + Some(0), + "{name} (from_tar={from_tar}): {}\nstdout: {}\nstderr: {}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } +} + +#[test] +fn test_load_exec_dynamic() { + run_program("hello_world_dyn"); +} diff --git a/litebox_runner_linux_on_macos_userland/tests/runner.rs b/litebox_runner_linux_on_macos_userland/tests/runner.rs new file mode 100644 index 000000000..f1e4a135b --- /dev/null +++ b/litebox_runner_linux_on_macos_userland/tests/runner.rs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#![cfg(all(target_os = "macos", target_arch = "aarch64"))] + +#[path = "runner/gates.rs"] +mod gates; + +use std::{ + path::PathBuf, + process::{Command, Output}, +}; + +struct Fixture(PathBuf); +impl Fixture { + fn new() -> Self { + Self( + tempfile::Builder::new() + .prefix("litebox-macos-") + .tempdir() + .unwrap() + .keep(), + ) + } + fn run(&self, extra: &[&str]) -> Output { + let runner = std::env::var_os("NEXTEST_BIN_EXE_litebox_runner_linux_on_macos_userland") + .unwrap_or_else(|| env!("CARGO_BIN_EXE_litebox_runner_linux_on_macos_userland").into()); + Command::new(runner) + .args(extra) + .arg(self.0.join("program")) + .output() + .unwrap() + } +} +impl Drop for Fixture { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +// Minimal RX ELF for register and fault regressions. +fn elf(code: &[u32]) -> Vec { + let mut bytes = vec![0u8; 0x1000]; + for instruction in code { + bytes.extend(instruction.to_le_bytes()); + } + bytes[..7].copy_from_slice(b"\x7fELF\x02\x01\x01"); + bytes[16..18].copy_from_slice(&3u16.to_le_bytes()); // ET_DYN + bytes[18..20].copy_from_slice(&183u16.to_le_bytes()); // EM_AARCH64 + bytes[20..24].copy_from_slice(&1u32.to_le_bytes()); + bytes[24..32].copy_from_slice(&0x1000u64.to_le_bytes()); + bytes[32..40].copy_from_slice(&64u64.to_le_bytes()); + bytes[52..54].copy_from_slice(&64u16.to_le_bytes()); + bytes[54..56].copy_from_slice(&56u16.to_le_bytes()); + bytes[56..58].copy_from_slice(&1u16.to_le_bytes()); + let size = bytes.len() as u64; + phdr(&mut bytes[64..120], 1, 5, 0, 0, size, size, 0x10000); + // Exclude headers and literals from the code ranges. + let shoff = bytes.len().next_multiple_of(8); + bytes.resize(shoff + 3 * 64, 0); + let names = b"\0.text\0.shstrtab\0"; + let names_offset = bytes.len(); + bytes.extend_from_slice(names); + bytes[40..48].copy_from_slice(&(shoff as u64).to_le_bytes()); + bytes[58..60].copy_from_slice(&64u16.to_le_bytes()); + bytes[60..62].copy_from_slice(&3u16.to_le_bytes()); + bytes[62..64].copy_from_slice(&2u16.to_le_bytes()); + let text = &mut bytes[shoff + 64..shoff + 128]; + text[..4].copy_from_slice(&1u32.to_le_bytes()); // name + text[4..8].copy_from_slice(&1u32.to_le_bytes()); // PROGBITS + for (at, value) in [ + (8, 6u64), + (16, 0x1000), + (24, 0x1000), + (32, (code.len() * 4) as u64), + (48, 4), + ] { + text[at..at + 8].copy_from_slice(&value.to_le_bytes()); + } + let strings = &mut bytes[shoff + 128..shoff + 192]; + strings[..4].copy_from_slice(&7u32.to_le_bytes()); + strings[4..8].copy_from_slice(&3u32.to_le_bytes()); + strings[24..32].copy_from_slice(&(names_offset as u64).to_le_bytes()); + strings[32..40].copy_from_slice(&(names.len() as u64).to_le_bytes()); + bytes +} +#[allow(clippy::too_many_arguments)] +fn phdr( + buf: &mut [u8], + kind: u32, + flags: u32, + offset: u64, + vaddr: u64, + filesz: u64, + memsz: u64, + align: u64, +) { + buf[..4].copy_from_slice(&kind.to_le_bytes()); + buf[4..8].copy_from_slice(&flags.to_le_bytes()); + for (index, value) in [offset, vaddr, 0, filesz, memsz, align] + .into_iter() + .enumerate() + { + buf[8 + index * 8..16 + index * 8].copy_from_slice(&value.to_le_bytes()); + } +} +const EXIT_42: &[u32] = &[0xd2800540, 0xd2800ba8, 0xd4000001]; // x0=42; x8=exit; svc #0 + +#[test] +fn bad_syscall_pointer_returns_efault_without_host_crash() { + let fixture = Fixture::new(); + let code = [ + 0xd2800020, // mov x0, #1 (stdout) + 0xd2800021, // mov x1, #1 (invalid pointer) + 0xd2800022, // mov x2, #1 + 0xd2800808, // mov x8, #64 (write) + 0xd4000001, // svc #0 + 0xb100381f, // cmn x0, #14 (EFAULT) + 0x54000081, // b.ne failure (+16) + 0xd2800540, 0xd2800ba8, 0xd4000001, 0xd2800020, 0xd2800ba8, 0xd4000001, + ]; + std::fs::write(fixture.0.join("program"), elf(&code)).unwrap(); + let output = fixture.run(&[]); + assert_eq!( + output.status.code(), + Some(42), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn guest_memory_fault_terminates_with_linux_status() { + let fixture = Fixture::new(); + let code = [0xd2800000, 0xf9400000]; // mov x0, #0; ldr x0, [x0] + std::fs::write(fixture.0.join("program"), elf(&code)).unwrap(); + let output = fixture.run(&[]); + // macOS SIGBUS maps to Linux SIGSEGV. + assert_eq!( + output.status.code(), + Some(128 + 11), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn guest_instruction_faults_deliver_sigill() { + for (name, code) in [ + ("undefined instruction", vec![0]), + ("instruction fetch", vec![0xd2800000, 0xd61f0000]), // mov x0, #0; br x0 + ] { + let fixture = Fixture::new(); + std::fs::write(fixture.0.join("program"), elf(&code)).unwrap(); + let output = fixture.run(&[]); + assert_eq!( + output.status.code(), + Some(128 + 4), + "{name}: {}", + String::from_utf8_lossy(&output.stderr) + ); + } +} + +#[test] +fn fp_registers_survive_syscalls() { + let fixture = Fixture::new(); + let code = [ + 0xd2824689, // mov x9, #0x1234 + 0x9e670120, // fmov d0, x9 + 0x9e67013f, // fmov d31, x9 + 0xd2801588, 0xd4000001, // getpid + 0x9e66000a, // fmov x10, d0 + 0xeb0a013f, // cmp x9, x10 + 0x540000e1, // b.ne failure (+28) + 0x9e6603ea, // fmov x10, d31 + 0xeb0a013f, // cmp x9, x10 + 0x54000081, // b.ne failure (+16) + 0xd2800540, 0xd2800ba8, 0xd4000001, 0xd2800020, 0xd2800ba8, 0xd4000001, + ]; + std::fs::write(fixture.0.join("program"), elf(&code)).unwrap(); + let output = fixture.run(&[]); + assert_eq!( + output.status.code(), + Some(42), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn rejects_fixed_address_and_incompatible_page_layouts() { + let fixture = Fixture::new(); + let mut binary = elf(EXIT_42); + binary[16..18].copy_from_slice(&2u16.to_le_bytes()); // ET_EXEC + std::fs::write(fixture.0.join("program"), &binary).unwrap(); + let output = fixture.run(&[]); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("Unsupported ELF type")); + + binary[16..18].copy_from_slice(&3u16.to_le_bytes()); + // 4 KiB congruent, but not 16 KiB congruent. + binary[80..88].copy_from_slice(&0x1000u64.to_le_bytes()); + std::fs::write(fixture.0.join("program"), &binary).unwrap(); + let output = fixture.run(&[]); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("Bad ELF format")); + + // Disjoint LOADs sharing a native page. + binary = elf(EXIT_42); + binary[56..58].copy_from_slice(&2u16.to_le_bytes()); + phdr(&mut binary[120..176], 1, 6, 0x2000, 0x2000, 0, 16, 0x1000); + std::fs::write(fixture.0.join("program"), &binary).unwrap(); + let output = fixture.run(&[]); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr).contains("Bad ELF format")); +} + +#[test] +fn preserves_scratch_registers_and_accepts_nonzero_svc_immediates() { + let fixture = Fixture::new(); + let code = [ + 0xd2824690, // mov x16, #0x1234 + 0xd2824691, // mov x17, #0x1234 + 0xd2801588, 0xd41fffe1, // getpid via svc #0xffff + 0xd2824689, // mov x9, #0x1234 + 0xeb09021f, // cmp x16, x9 + 0x540000c1, // b.ne failure (+24) + 0xeb09023f, // cmp x17, x9 + 0x54000081, // b.ne failure (+16) + 0xd2800540, 0xd2800ba8, 0xd4000001, 0xd2800020, 0xd2800ba8, 0xd4000001, + ]; + std::fs::write(fixture.0.join("program"), elf(&code)).unwrap(); + let output = fixture.run(&[]); + assert_eq!( + output.status.code(), + Some(42), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} diff --git a/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs b/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs new file mode 100644 index 000000000..90d0028b8 --- /dev/null +++ b/litebox_runner_linux_on_macos_userland/tests/runner/gates.rs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +use super::*; +// Exercises TLS, syscalls, x18 spills, SP writeback, and ADR/BLR through x18. +const X18: &[u32] = &[ + 0xd2824692, 0xd2822229, 0xd51bd049, 0xd2801588, 0xd4000001, 0xd53bd04a, 0xeb0a013f, 0x54000221, + 0xd282468b, 0xeb0b025f, 0x540001c1, 0xd10043ff, 0xa9002ff2, 0xd2800012, 0xa8c133f2, 0xeb0c025f, + 0x54000101, 0xb40000f2, 0x10000132, 0xd63f0240, 0xf100a81f, 0x54000061, 0xd2800ba8, 0xd4000001, + 0xd2800020, 0xd2800bc8, 0xd4000001, // failure: exit_group(1) + 0xd2800540, 0xd65f03c0, // BLR target: return 42 +]; + +#[test] +fn guest_signal_return_restores_x18_and_vector_state() { + let fixture = Fixture::new(); + // SIGUSR1 handler clobbers x18 and d0; synthetic rt_sigreturn restores them. + let code = [ + 0xd10083ff, 0x10000409, 0xf90003e9, 0xa900ffff, 0xf9000fff, 0xd2800140, 0x910003e1, + 0xd2800002, 0xd2800103, 0xd28010c8, 0xd4000001, 0xb5000260, 0xd2824692, 0xd28acf09, + 0x9e670120, 0xd2801588, 0xd4000001, 0xd2800141, 0xd2801028, 0xd4000001, 0xd2824689, + 0xeb09025f, 0x54000101, 0x9e66000a, 0xd28acf09, 0xeb09015f, 0x54000081, 0xd2800540, + 0xd2800bc8, 0xd4000001, 0xd2800020, 0xd2800bc8, 0xd4000001, 0xd2933332, 0x9e6703e0, + 0xd65f03c0, + ]; + std::fs::write(fixture.0.join("program"), elf(&code)).unwrap(); + let output = fixture.run(&[]); + assert_eq!( + output.status.code(), + Some(42), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn x18_gates_preserve_registers_and_branch_targets() { + let fixture = Fixture::new(); + std::fs::write(fixture.0.join("program"), elf(X18)).unwrap(); + let output = fixture.run(&[]); + assert_eq!( + output.status.code(), + Some(42), + "{}", + String::from_utf8_lossy(&output.stderr) + ); +} diff --git a/litebox_runner_linux_on_macos_userland/tests/test-bins/hello_world_dyn b/litebox_runner_linux_on_macos_userland/tests/test-bins/hello_world_dyn new file mode 100755 index 000000000..60cfad254 Binary files /dev/null and b/litebox_runner_linux_on_macos_userland/tests/test-bins/hello_world_dyn differ diff --git a/litebox_runner_linux_on_macos_userland/tests/test-bins/ld-linux-aarch64.so.1 b/litebox_runner_linux_on_macos_userland/tests/test-bins/ld-linux-aarch64.so.1 new file mode 100755 index 000000000..c4475e9a7 Binary files /dev/null and b/litebox_runner_linux_on_macos_userland/tests/test-bins/ld-linux-aarch64.so.1 differ diff --git a/litebox_runner_linux_on_macos_userland/tests/test-bins/libc.so.6 b/litebox_runner_linux_on_macos_userland/tests/test-bins/libc.so.6 new file mode 100755 index 000000000..e6934241c Binary files /dev/null and b/litebox_runner_linux_on_macos_userland/tests/test-bins/libc.so.6 differ diff --git a/litebox_shim_linux/Cargo.toml b/litebox_shim_linux/Cargo.toml index ca277428f..2c04c2378 100644 --- a/litebox_shim_linux/Cargo.toml +++ b/litebox_shim_linux/Cargo.toml @@ -41,5 +41,8 @@ litebox_platform_linux_userland = { path = "../litebox_platform_linux_userland/" [target.'cfg(target_os = "windows")'.dev-dependencies] litebox_platform_windows_userland = { path = "../litebox_platform_windows_userland/", version = "0.1.0" } +[target.'cfg(all(target_os = "macos", target_arch = "aarch64"))'.dev-dependencies] +litebox_platform_macos_userland = { path = "../litebox_platform_macos_userland", version = "0.1.0" } + [lints] workspace = true diff --git a/litebox_shim_linux/src/lib.rs b/litebox_shim_linux/src/lib.rs index bba98eb4d..3620ff1eb 100644 --- a/litebox_shim_linux/src/lib.rs +++ b/litebox_shim_linux/src/lib.rs @@ -38,10 +38,11 @@ use litebox_platform::time::TimeProvider; #[cfg(target_arch = "aarch64")] const fn aarch64_rewrite_options() -> litebox_syscall_rewriter::RewriteOptions { - litebox_syscall_rewriter::RewriteOptions::new( - litebox_syscall_rewriter::TargetHost::Linux, - cfg!(feature = "aarch64_virtualize_x18"), - ) + #[cfg(target_os = "macos")] + let host = litebox_syscall_rewriter::TargetHost::MacOs; + #[cfg(not(target_os = "macos"))] + let host = litebox_syscall_rewriter::TargetHost::Linux; + litebox_syscall_rewriter::RewriteOptions::new(host, cfg!(feature = "aarch64_virtualize_x18")) } /// On debug builds, logs that the user attempted to use an unsupported feature. diff --git a/litebox_shim_linux/src/loader/elf.rs b/litebox_shim_linux/src/loader/elf.rs index d9fc2e9bf..e0f4c9b49 100644 --- a/litebox_shim_linux/src/loader/elf.rs +++ b/litebox_shim_linux/src/loader/elf.rs @@ -524,6 +524,7 @@ mod tests { } #[test] + #[cfg_attr(target_os = "macos", ignore = "macOS runner supports PIE guests only")] fn et_exec_interpreter_loads_top_down_above_low_heap() { let task = crate::syscalls::tests::init_platform(); write_file(&task, "/main", &minimal_elf(ET_EXEC, Some(INTERP_PATH))); diff --git a/litebox_shim_linux/src/loader/mod.rs b/litebox_shim_linux/src/loader/mod.rs index c54509b3a..8d675b97b 100644 --- a/litebox_shim_linux/src/loader/mod.rs +++ b/litebox_shim_linux/src/loader/mod.rs @@ -11,4 +11,9 @@ pub(crate) const DEFAULT_STACK_SIZE: usize = 8 * 1024 * 1024; // 8 MB /// A default low address is used for the binary (which grows upwards) to avoid /// conflicts with the kernel's memory mappings (which grows downwards). +#[cfg(not(target_os = "macos"))] pub(crate) const DEFAULT_LOW_ADDR: usize = 0x1000_0000; + +/// Above Mach-O __PAGEZERO and the runner's usual image base. +#[cfg(target_os = "macos")] +pub(crate) const DEFAULT_LOW_ADDR: usize = 0x4_0000_0000; diff --git a/litebox_shim_linux/src/syscalls/mm.rs b/litebox_shim_linux/src/syscalls/mm.rs index f07a15cef..4ff83135c 100644 --- a/litebox_shim_linux/src/syscalls/mm.rs +++ b/litebox_shim_linux/src/syscalls/mm.rs @@ -57,7 +57,8 @@ fn finalize_trampoline_gates( // Gates encode a scaled `u16`; the platform API remains pointer-width. let offset = u16::try_from(offset) .map_err(|_| format!("guest thread-pointer offset {offset} is too large for a gate"))?; - if cfg!(feature = "aarch64_virtualize_x18") { + let options = crate::aarch64_rewrite_options(); + if options.virtualizes_x18() { let x18 = offset .checked_add( u16::try_from(litebox_syscall_rewriter::aarch64::GUEST_X18_OFFSET_FROM_GUEST_TP) @@ -68,7 +69,7 @@ fn finalize_trampoline_gates( trampoline, offset, x18, - litebox_syscall_rewriter::TargetHost::Linux, + options.target_host(), ) .map_err(|e| format!("failed to patch guest offsets {offset}/{x18}: {e}")) } else { @@ -816,7 +817,23 @@ impl Task { if base_addr.is_none() && align_down(p_offset, PAGE_SIZE) == align_down(file_offset, PAGE_SIZE) { - base_addr = Some(mapped_addr.wrapping_sub(p_vaddr.trunc())); + #[cfg(all(target_arch = "aarch64", target_os = "macos"))] + { + let Some(base) = + mapped_addr.checked_sub(align_down(p_vaddr.trunc(), PAGE_SIZE)) + else { + litebox_util_log::warn!( + mapped_addr:? = mapped_addr, p_vaddr:? = p_vaddr; + "mapped ELF address is below its page-aligned virtual address" + ); + return; + }; + base_addr = Some(base); + } + #[cfg(not(all(target_arch = "aarch64", target_os = "macos")))] + { + base_addr = Some(mapped_addr.wrapping_sub(p_vaddr.trunc())); + } } } @@ -919,6 +936,8 @@ impl Task { return; }; let offset: usize = offset.trunc(); + #[cfg(all(target_arch = "aarch64", target_os = "macos"))] + let offset = align_up(offset, PAGE_SIZE); base + offset }; @@ -1622,11 +1641,12 @@ impl Task { #[cfg(test)] mod tests { use super::PAGE_SIZE; - use litebox::{ - fs::{Mode, OFlags}, - platform::PageManagementProvider, - }; - use litebox_common_linux::{MRemapFlags, MapFlags, ProtFlags, errno::Errno}; + use litebox::fs::{Mode, OFlags}; + #[cfg(any(target_os = "linux", target_os = "windows"))] + use litebox::platform::PageManagementProvider; + #[cfg(any(target_os = "linux", target_os = "windows"))] + use litebox_common_linux::MRemapFlags; + use litebox_common_linux::{MapFlags, ProtFlags, errno::Errno}; use crate::syscalls::tests::TestPlatform as Platform; use crate::{UserPtrMut, syscalls::tests::init_platform}; @@ -1658,6 +1678,11 @@ mod tests { #[cfg(target_arch = "aarch64")] mod aarch64_trampoline_gates { use litebox::platform::SystemInfoProvider; + use litebox_syscall_rewriter::{ + RewriteOptions, + aarch64::{GateMetadata, classify_copied_gate_slot_for_host}, + patch_code_segment_with_options, + }; struct StubPlatform(Option); @@ -1674,140 +1699,136 @@ mod tests { } fn unpatched_trampoline() -> alloc::vec::Vec { + let options = crate::aarch64_rewrite_options(); let mut code = 0xD53B_D049u32.to_le_bytes(); // MRS X9, TPIDR_EL0 let (tramp, trapped) = - litebox_syscall_rewriter::patch_code_segment(&mut code, 0x1000, 0x400000, 0) - .unwrap(); + patch_code_segment_with_options(&mut code, 0x1000, 0x400000, 0, options).unwrap(); assert!(trapped.is_empty()); - assert_eq!( - litebox_syscall_rewriter::aarch64::find_guest_tpidr_placeholder(&tramp), - Some(16 + 4), - "the fixture must start out unpatched, or these tests prove nothing" - ); - let classified = - litebox_syscall_rewriter::aarch64::classify_gate_pc(&tramp, 0x400000, 0x400010) - .expect("emitted MRS slot must validate"); - assert_eq!(classified.slot_offset(), 16); + let classified = classify_copied_gate_slot_for_host( + &tramp[16..], + 0x400010, + 0x400010, + options.target_host(), + ) + .expect("emitted MRS slot must validate"); assert!(matches!( classified.metadata(), - litebox_syscall_rewriter::aarch64::GateMetadata::MrsTpidr { destination: 9, .. } + GateMetadata::MrsTpidr { destination: 9 } )); tramp } - #[cfg(feature = "aarch64_virtualize_x18")] #[test] - fn x18_gate_is_finalized_with_the_platform_offset() { + fn x18_gate_matches_configured_policy() { + let options = crate::aarch64_rewrite_options(); let mut code = 0xaa00_03f2u32.to_le_bytes(); // mov x18, x0 - let options = litebox_syscall_rewriter::RewriteOptions::new( - litebox_syscall_rewriter::TargetHost::Linux, - true, - ); - let (mut trampoline, trapped) = - litebox_syscall_rewriter::patch_code_segment_with_options( - &mut code, 0x1000, 0x400000, 0, options, - ) - .unwrap(); + let (mut trampoline, trapped) = patch_code_segment_with_options( + &mut code, + 0x1000, + 0x400000, + 0, + RewriteOptions::new(options.target_host(), true), + ) + .unwrap(); assert!(trapped.is_empty()); - - super::super::finalize_trampoline_gates(&StubPlatform(Some(96)), &mut trampoline) - .unwrap(); - - assert!(matches!( - litebox_syscall_rewriter::aarch64::classify_gate_pc( - &trampoline, - 0x400000, - 0x400010, - ) - .unwrap() - .metadata(), - litebox_syscall_rewriter::aarch64::GateMetadata::X18 { .. } - )); + let before = trampoline.clone(); + let result = + super::super::finalize_trampoline_gates(&StubPlatform(Some(96)), &mut trampoline); + if options.virtualizes_x18() { + result.unwrap(); + assert!(matches!( + classify_copied_gate_slot_for_host( + &trampoline[16..], + 0x400010, + 0x400010, + options.target_host(), + ) + .unwrap() + .metadata(), + GateMetadata::X18 { .. } + )); + } else { + assert!(result.is_err()); + assert_eq!(trampoline, before); + } } - /// Runtime gates receive the callback address, not a callback-slot address. #[test] fn the_runtime_paths_argument_shape_produces_installable_gates() { const TRAMPOLINE_BASE: u64 = 0x40_0000; const SYSCALL_ENTRY: u64 = 0xDEAD_0000; - const GUEST_THREAD_POINTER_OFFSET: usize = 96; - + let options = crate::aarch64_rewrite_options(); let mut code = 0xD400_0001u32.to_le_bytes(); // SVC #0 - let (mut stubs, trapped) = litebox_syscall_rewriter::patch_code_segment( + let (mut stubs, trapped) = patch_code_segment_with_options( &mut code, 0x1000, TRAMPOLINE_BASE, SYSCALL_ENTRY, + options, ) - .expect("a gate-aligned base and a real callback must be accepted"); + .unwrap(); assert!(trapped.is_empty()); - assert_eq!( u64::from_le_bytes(stubs[..8].try_into().unwrap()), SYSCALL_ENTRY ); - - super::super::finalize_trampoline_gates( - &StubPlatform(Some(GUEST_THREAD_POINTER_OFFSET)), - &mut stubs, - ) - .expect("runtime gates must be patchable exactly like ahead-of-time ones"); - assert_eq!( - litebox_syscall_rewriter::aarch64::find_guest_tpidr_placeholder(&stubs), - None - ); - litebox_syscall_rewriter::aarch64::classify_gate_pc( - &stubs, - TRAMPOLINE_BASE, - TRAMPOLINE_BASE + 16, - ) - .expect("the installed gate must classify at runtime"); - + super::super::finalize_trampoline_gates(&StubPlatform(Some(96)), &mut stubs).unwrap(); + assert!(matches!( + classify_copied_gate_slot_for_host( + &stubs[16..], + TRAMPOLINE_BASE + 16, + TRAMPOLINE_BASE + 16, + options.target_host(), + ) + .unwrap() + .metadata(), + GateMetadata::Svc + )); let mut code = 0xD400_0001u32.to_le_bytes(); assert!( - litebox_syscall_rewriter::patch_code_segment( + patch_code_segment_with_options( &mut code, 0x1000, TRAMPOLINE_BASE + 8, TRAMPOLINE_BASE, + options, ) - .is_err(), - "a base-relative callback has to be rejected, or this test proves nothing" + .is_err() ); } #[test] - fn a_supplied_offset_is_baked_into_every_gate() { + fn a_supplied_offset_is_accepted() { let mut tramp = unpatched_trampoline(); - super::super::finalize_trampoline_gates(&StubPlatform(Some(96)), &mut tramp) - .expect("a well-formed trampoline and a valid offset must be accepted"); - assert_eq!( - litebox_syscall_rewriter::aarch64::find_guest_tpidr_placeholder(&tramp), - None - ); + super::super::finalize_trampoline_gates(&StubPlatform(Some(96)), &mut tramp).unwrap(); + if crate::aarch64_rewrite_options().target_host() + == litebox_syscall_rewriter::TargetHost::Linux + { + assert_eq!( + litebox_syscall_rewriter::aarch64::find_guest_tpidr_placeholder(&tramp), + None + ); + } } #[test] fn a_platform_with_no_offset_is_refused() { let mut tramp = unpatched_trampoline(); + let before = tramp.clone(); let err = super::super::finalize_trampoline_gates(&StubPlatform(None), &mut tramp) - .expect_err( - "an AArch64 platform that supplies no offset must be fatal for the \ - binary, not a silent skip", - ); + .unwrap_err(); assert!(err.contains("no guest thread-pointer offset"), "{err}"); - assert_eq!( - litebox_syscall_rewriter::aarch64::find_guest_tpidr_placeholder(&tramp), - Some(16 + 4) - ); + assert_eq!(tramp, before); } #[test] fn an_offset_no_gate_can_encode_is_refused() { let mut tramp = unpatched_trampoline(); + let before = tramp.clone(); let err = super::super::finalize_trampoline_gates(&StubPlatform(Some(4)), &mut tramp) - .expect_err("an offset the gates' scaled immediate cannot hold is fatal"); + .unwrap_err(); assert!(err.contains("failed to patch"), "{err}"); + assert_eq!(tramp, before); } } @@ -1862,6 +1883,7 @@ mod tests { } #[test] + #[cfg_attr(target_os = "macos", ignore = "assumes 4 KiB host pages")] fn test_mremap() { let task = init_platform(); @@ -1900,6 +1922,7 @@ mod tests { } #[test] + #[cfg_attr(target_os = "macos", ignore = "assumes 4 KiB host pages")] fn test_mmap_fixed_noreplace() { let task = init_platform(); @@ -2104,6 +2127,7 @@ mod tests { } #[test] + #[cfg_attr(target_os = "macos", ignore = "assumes 4 KiB host pages")] fn test_map_shared_anonymous() { let task = init_platform(); @@ -2139,6 +2163,7 @@ mod tests { } #[test] + #[cfg_attr(target_os = "macos", ignore = "assumes 4 KiB host pages")] fn test_map_shared_anonymous_writable() { let task = init_platform(); @@ -2162,6 +2187,7 @@ mod tests { } #[test] + #[cfg_attr(target_os = "macos", ignore = "assumes 4 KiB host pages")] fn test_map_shared_readonly_file() { let task = init_platform(); diff --git a/litebox_shim_linux/src/syscalls/process.rs b/litebox_shim_linux/src/syscalls/process.rs index dd09c3e37..47c2cd8a4 100644 --- a/litebox_shim_linux/src/syscalls/process.rs +++ b/litebox_shim_linux/src/syscalls/process.rs @@ -1915,6 +1915,10 @@ mod tests { /// After the alarm deadline passes, a blocking operation should be /// interrupted and SIGALRM should be pending. #[test] + #[cfg_attr( + target_os = "macos", + ignore = "host sleep is not interrupted by fallback alarms" + )] fn test_alarm_fires_after_deadline() { use litebox_common_linux::{ClockId, TimerFlags, Timespec}; use litebox_platform::time::{Instant as _, TimeProvider}; @@ -2112,6 +2116,8 @@ mod tests { } #[test] + #[cfg_attr(target_os = "macos", ignore = "requires platform timer support")] + #[cfg_attr(target_os = "macos", allow(unused_variables))] fn test_timer_delivers_correct_signal() { use litebox::platform::{TimerHandle as _, TimerProvider as _}; use litebox_common_linux::signal::Signal; diff --git a/litebox_shim_linux/src/syscalls/tests.rs b/litebox_shim_linux/src/syscalls/tests.rs index e70502fdc..8eb78a917 100644 --- a/litebox_shim_linux/src/syscalls/tests.rs +++ b/litebox_shim_linux/src/syscalls/tests.rs @@ -44,10 +44,12 @@ const TEST_TAR_FILE: &[u8] = include_bytes!("../../../litebox/src/fs/test.tar"); /// The concrete platform used by the shim's unit tests. /// /// This is selected by the build target so the tests can run against whichever -/// userland platform matches the host (Linux or Windows) rather than being +/// userland platform matches the host rather than being /// hard-wired to one. #[cfg(target_os = "linux")] pub(crate) use litebox_platform_linux_userland::LinuxUserland as TestPlatform; +#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +pub(crate) use litebox_platform_macos_userland::MacosUserland as TestPlatform; #[cfg(target_os = "windows")] pub(crate) use litebox_platform_windows_userland::WindowsUserland as TestPlatform;