diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 16be309c92..74ba813d74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -246,7 +246,7 @@ jobs: env: LITEBOX_DISABLE_NATIVE_FORK: "1" run: | - ./.github/tools/github_actions_run_cargo nextest -p litebox_runner_linux_userland --features litebox_runner_linux_userland/aarch64_copy_based_fork -E 'binary(run) and test(/fork/) and not test(/^deep_fork/)' + ./.github/tools/github_actions_run_cargo nextest -p litebox_runner_linux_userland --features litebox_runner_linux_userland/aarch64_copy_based_fork -E 'binary(run) and test(/fork|hwcaps/) and not test(/^deep_fork/)' # Nextest does not run doctests. - run: | ./.github/tools/github_actions_run_cargo test --doc $AARCH64_CRATES diff --git a/litebox/src/arch/aarch64/mod.rs b/litebox/src/arch/aarch64/mod.rs index 6da6bba7f8..88b5d99b0b 100644 --- a/litebox/src/arch/aarch64/mod.rs +++ b/litebox/src/arch/aarch64/mod.rs @@ -17,3 +17,115 @@ pub const PSR_NZCV_MASK: u64 = 0b1111 << 28; pub const PSR_SSBS_BIT: u64 = 1 << 12; /// Data Independent Timing bit. pub const PSR_DIT_BIT: u64 = 1 << 24; + +bitflags::bitflags! { + /// AArch64 CPU features, named after Arm `FEAT_*` extensions. + /// + /// Bit positions are LiteBox's own and must not be relied on to match any ABI encoding. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct CpuFeatures: u64 { + const FP = 1 << 0; + /// `FEAT_AdvSIMD`. + const ASIMD = 1 << 1; + const AES = 1 << 2; + const PMULL = 1 << 3; + const SHA1 = 1 << 4; + const SHA256 = 1 << 5; + const CRC32 = 1 << 6; + const LSE = 1 << 7; + /// Half-precision scalar and vector arithmetic. + const FP16 = 1 << 8; + const RDM = 1 << 9; + const JSCVT = 1 << 10; + const FCMA = 1 << 11; + const LRCPC = 1 << 12; + /// `DC CVAP`. + const DPB = 1 << 13; + const SHA3 = 1 << 14; + const SM3 = 1 << 15; + const SM4 = 1 << 16; + const DOTPROD = 1 << 17; + const SHA512 = 1 << 18; + const FHM = 1 << 19; + /// Relaxed alignment for single-copy atomic accesses. + const LSE2 = 1 << 20; + const LRCPC2 = 1 << 21; + const FLAGM = 1 << 22; + const SB = 1 << 23; + /// `DC CVADP`. + const DPB2 = 1 << 24; + /// `AXFLAG` and `XAFLAG`. + const FLAGM2 = 1 << 25; + const FRINTTS = 1 << 26; + const I8MM = 1 << 27; + const BF16 = 1 << 28; + const DGH = 1 << 29; + /// `RNDR` and `RNDRRS`. + const RNG = 1 << 30; + const ECV = 1 << 31; + const AFP = 1 << 32; + const RPRES = 1 << 33; + /// `WFET` and `WFIT`. + const WFXT = 1 << 34; + const EBF16 = 1 << 35; + const CSSC = 1 << 36; + const RPRFM = 1 << 37; + const MOPS = 1 << 38; + const HBC = 1 << 39; + /// Adds the `PSTATE.DIT` bit. + const DIT = 1 << 40; + /// Adds the `PSTATE.SSBS` bit. + const SSBS = 1 << 41; + /// Pointer authentication with both address and generic keys. + const PAUTH = 1 << 42; + const SVE = 1 << 43; + const SME = 1 << 44; + const BTI = 1 << 45; + const MTE = 1 << 46; + } +} + +impl CpuFeatures { + /// Features that use only X0-X30, SP, PC, NZCV, V0-V31, FPCR, and FPSR, and need no kernel + /// ABI. A host that reports one has enabled it for EL0, so preserving that state suffices. + pub const BASE_STATE_ONLY: Self = Self::FP + .union(Self::ASIMD) + .union(Self::AES) + .union(Self::PMULL) + .union(Self::SHA1) + .union(Self::SHA256) + .union(Self::CRC32) + .union(Self::LSE) + .union(Self::FP16) + .union(Self::RDM) + .union(Self::JSCVT) + .union(Self::FCMA) + .union(Self::LRCPC) + .union(Self::DPB) + .union(Self::SHA3) + .union(Self::SM3) + .union(Self::SM4) + .union(Self::DOTPROD) + .union(Self::SHA512) + .union(Self::FHM) + .union(Self::LSE2) + .union(Self::LRCPC2) + .union(Self::FLAGM) + .union(Self::SB) + .union(Self::DPB2) + .union(Self::FLAGM2) + .union(Self::FRINTTS) + .union(Self::I8MM) + .union(Self::BF16) + .union(Self::DGH) + .union(Self::RNG) + .union(Self::ECV) + .union(Self::AFP) + .union(Self::RPRES) + .union(Self::WFXT) + .union(Self::EBF16) + .union(Self::CSSC) + .union(Self::RPRFM) + .union(Self::MOPS) + .union(Self::HBC); +} diff --git a/litebox/src/platform/arch.rs b/litebox/src/platform/arch.rs index e708c474a5..48403a5d3e 100644 --- a/litebox/src/platform/arch.rs +++ b/litebox/src/platform/arch.rs @@ -31,6 +31,15 @@ pub trait ArchSpecificProvider { reg: &ArchSpecificRegister, val: usize, ) -> Result<(), ArchSpecificError>; + + /// AArch64 CPU features that guests may use: only those the host supports and whose + /// architectural state (registers, PSTATE bits, and PAC keys) the platform preserves across + /// syscalls, exceptions and signals, interrupts, and native fork. Shims must drop features + /// their own mechanisms do not preserve (e.g., pointer authentication under copy-based fork). + #[cfg(target_arch = "aarch64")] + fn get_guest_cpu_features(&self) -> crate::arch::aarch64::CpuFeatures { + crate::arch::aarch64::CpuFeatures::empty() + } } /// Architecture-specific registers. diff --git a/litebox_common_linux/src/hwcap.rs b/litebox_common_linux/src/hwcap.rs new file mode 100644 index 0000000000..4e5995622f --- /dev/null +++ b/litebox_common_linux/src/hwcap.rs @@ -0,0 +1,193 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +//! Linux AArch64 `AT_HWCAP` and `AT_HWCAP2` encoding of [`CpuFeatures`]. + +use litebox::arch::aarch64::CpuFeatures; + +// Bit positions from Linux uapi `arch/arm64/include/uapi/asm/hwcap.h`. +const HWCAP_FP: u64 = 1 << 0; +const HWCAP_ASIMD: u64 = 1 << 1; +const HWCAP_AES: u64 = 1 << 3; +const HWCAP_PMULL: u64 = 1 << 4; +const HWCAP_SHA1: u64 = 1 << 5; +const HWCAP_SHA2: u64 = 1 << 6; +const HWCAP_CRC32: u64 = 1 << 7; +const HWCAP_ATOMICS: u64 = 1 << 8; +const HWCAP_FPHP: u64 = 1 << 9; +const HWCAP_ASIMDHP: u64 = 1 << 10; +const HWCAP_ASIMDRDM: u64 = 1 << 12; +const HWCAP_JSCVT: u64 = 1 << 13; +const HWCAP_FCMA: u64 = 1 << 14; +const HWCAP_LRCPC: u64 = 1 << 15; +const HWCAP_DCPOP: u64 = 1 << 16; +const HWCAP_SHA3: u64 = 1 << 17; +const HWCAP_SM3: u64 = 1 << 18; +const HWCAP_SM4: u64 = 1 << 19; +const HWCAP_ASIMDDP: u64 = 1 << 20; +const HWCAP_SHA512: u64 = 1 << 21; +const HWCAP_SVE: u64 = 1 << 22; +const HWCAP_ASIMDFHM: u64 = 1 << 23; +const HWCAP_DIT: u64 = 1 << 24; +const HWCAP_USCAT: u64 = 1 << 25; +const HWCAP_ILRCPC: u64 = 1 << 26; +const HWCAP_FLAGM: u64 = 1 << 27; +const HWCAP_SSBS: u64 = 1 << 28; +const HWCAP_SB: u64 = 1 << 29; +const HWCAP_PACA: u64 = 1 << 30; +const HWCAP_PACG: u64 = 1 << 31; + +const HWCAP2_DCPODP: u64 = 1 << 0; +const HWCAP2_FLAGM2: u64 = 1 << 7; +const HWCAP2_FRINT: u64 = 1 << 8; +const HWCAP2_I8MM: u64 = 1 << 13; +const HWCAP2_BF16: u64 = 1 << 14; +const HWCAP2_DGH: u64 = 1 << 15; +const HWCAP2_RNG: u64 = 1 << 16; +const HWCAP2_BTI: u64 = 1 << 17; +const HWCAP2_MTE: u64 = 1 << 18; +const HWCAP2_ECV: u64 = 1 << 19; +const HWCAP2_AFP: u64 = 1 << 20; +const HWCAP2_RPRES: u64 = 1 << 21; +const HWCAP2_SME: u64 = 1 << 23; +const HWCAP2_WFXT: u64 = 1 << 31; +const HWCAP2_EBF16: u64 = 1 << 32; +const HWCAP2_CSSC: u64 = 1 << 34; +const HWCAP2_RPRFM: u64 = 1 << 35; +const HWCAP2_MOPS: u64 = 1 << 43; +const HWCAP2_HBC: u64 = 1 << 44; + +/// A feature with several bits is present only if all of them are. Unmapped: `HWCAP_EVTSTRM` and +/// `HWCAP_CPUID` are kernel services, not CPU features; SVE/SME sub-features and `HWCAP2_MTE3` +/// have no [`CpuFeatures`] flag. +const FEATURE_HWCAPS: &[(CpuFeatures, u64, u64)] = &[ + (CpuFeatures::FP, HWCAP_FP, 0), + (CpuFeatures::ASIMD, HWCAP_ASIMD, 0), + (CpuFeatures::AES, HWCAP_AES, 0), + (CpuFeatures::PMULL, HWCAP_PMULL, 0), + (CpuFeatures::SHA1, HWCAP_SHA1, 0), + (CpuFeatures::SHA256, HWCAP_SHA2, 0), + (CpuFeatures::CRC32, HWCAP_CRC32, 0), + (CpuFeatures::LSE, HWCAP_ATOMICS, 0), + (CpuFeatures::FP16, HWCAP_FPHP | HWCAP_ASIMDHP, 0), + (CpuFeatures::RDM, HWCAP_ASIMDRDM, 0), + (CpuFeatures::JSCVT, HWCAP_JSCVT, 0), + (CpuFeatures::FCMA, HWCAP_FCMA, 0), + (CpuFeatures::LRCPC, HWCAP_LRCPC, 0), + (CpuFeatures::DPB, HWCAP_DCPOP, 0), + (CpuFeatures::SHA3, HWCAP_SHA3, 0), + (CpuFeatures::SM3, HWCAP_SM3, 0), + (CpuFeatures::SM4, HWCAP_SM4, 0), + (CpuFeatures::DOTPROD, HWCAP_ASIMDDP, 0), + (CpuFeatures::SHA512, HWCAP_SHA512, 0), + (CpuFeatures::SVE, HWCAP_SVE, 0), + (CpuFeatures::FHM, HWCAP_ASIMDFHM, 0), + (CpuFeatures::DIT, HWCAP_DIT, 0), + (CpuFeatures::LSE2, HWCAP_USCAT, 0), + (CpuFeatures::LRCPC2, HWCAP_ILRCPC, 0), + (CpuFeatures::FLAGM, HWCAP_FLAGM, 0), + (CpuFeatures::SSBS, HWCAP_SSBS, 0), + (CpuFeatures::SB, HWCAP_SB, 0), + (CpuFeatures::PAUTH, HWCAP_PACA | HWCAP_PACG, 0), + (CpuFeatures::DPB2, 0, HWCAP2_DCPODP), + (CpuFeatures::FLAGM2, 0, HWCAP2_FLAGM2), + (CpuFeatures::FRINTTS, 0, HWCAP2_FRINT), + (CpuFeatures::I8MM, 0, HWCAP2_I8MM), + (CpuFeatures::BF16, 0, HWCAP2_BF16), + (CpuFeatures::DGH, 0, HWCAP2_DGH), + (CpuFeatures::RNG, 0, HWCAP2_RNG), + (CpuFeatures::BTI, 0, HWCAP2_BTI), + (CpuFeatures::MTE, 0, HWCAP2_MTE), + (CpuFeatures::ECV, 0, HWCAP2_ECV), + (CpuFeatures::AFP, 0, HWCAP2_AFP), + (CpuFeatures::RPRES, 0, HWCAP2_RPRES), + (CpuFeatures::SME, 0, HWCAP2_SME), + (CpuFeatures::WFXT, 0, HWCAP2_WFXT), + (CpuFeatures::EBF16, 0, HWCAP2_EBF16), + (CpuFeatures::CSSC, 0, HWCAP2_CSSC), + (CpuFeatures::RPRFM, 0, HWCAP2_RPRFM), + (CpuFeatures::MOPS, 0, HWCAP2_MOPS), + (CpuFeatures::HBC, 0, HWCAP2_HBC), +]; + +/// Linux `AT_HWCAP` and `AT_HWCAP2` auxiliary vector values. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct LinuxHwcaps { + pub hwcap: u64, + pub hwcap2: u64, +} + +impl LinuxHwcaps { + #[must_use] + pub fn from_cpu_features(features: CpuFeatures) -> Self { + let mut hwcaps = Self::default(); + for &(feature, hwcap, hwcap2) in FEATURE_HWCAPS { + if features.contains(feature) { + hwcaps.hwcap |= hwcap; + hwcaps.hwcap2 |= hwcap2; + } + } + hwcaps + } + + /// Drops bits that encode no [`CpuFeatures`] flag. + #[must_use] + pub fn to_cpu_features(self) -> CpuFeatures { + FEATURE_HWCAPS + .iter() + .filter(|&&(_, hwcap, hwcap2)| { + self.hwcap & hwcap == hwcap && self.hwcap2 & hwcap2 == hwcap2 + }) + .map(|&(feature, _, _)| feature) + .collect() + } +} + +#[cfg(test)] +mod tests { + use super::{FEATURE_HWCAPS, HWCAP_FPHP, HWCAP_PACA, LinuxHwcaps}; + use litebox::arch::aarch64::CpuFeatures; + + #[test] + fn feature_hwcaps_table_is_consistent() { + let mut seen_features = CpuFeatures::empty(); + let mut seen = LinuxHwcaps::default(); + for &(feature, hwcap, hwcap2) in FEATURE_HWCAPS { + assert!( + !seen_features.intersects(feature), + "{feature:?} is mapped twice" + ); + assert!(hwcap | hwcap2 != 0, "{feature:?} has no Linux bits"); + assert_eq!(seen.hwcap & hwcap, 0, "{feature:?} reuses an AT_HWCAP bit"); + assert_eq!( + seen.hwcap2 & hwcap2, + 0, + "{feature:?} reuses an AT_HWCAP2 bit" + ); + seen_features |= feature; + seen.hwcap |= hwcap; + seen.hwcap2 |= hwcap2; + } + assert_eq!(seen_features, CpuFeatures::all()); + assert_eq!( + LinuxHwcaps { + hwcap: HWCAP_PACA | HWCAP_FPHP, + hwcap2: 0, + } + .to_cpu_features(), + CpuFeatures::empty() + ); + } + + #[test] + fn feature_hwcaps_match_linux_uapi() { + // Literal bit positions from Linux uapi `hwcap.h`, independent of the constants above. + let features = CpuFeatures::AES | CpuFeatures::FP16 | CpuFeatures::PAUTH | CpuFeatures::HBC; + let hwcaps = LinuxHwcaps { + hwcap: 1 << 3 | 1 << 9 | 1 << 10 | 1 << 30 | 1 << 31, + hwcap2: 1 << 44, + }; + assert_eq!(LinuxHwcaps::from_cpu_features(features), hwcaps); + assert_eq!(hwcaps.to_cpu_features(), features); + } +} diff --git a/litebox_common_linux/src/lib.rs b/litebox_common_linux/src/lib.rs index 5c428cc333..63a77eb13b 100644 --- a/litebox_common_linux/src/lib.rs +++ b/litebox_common_linux/src/lib.rs @@ -20,6 +20,8 @@ use zerocopy::{FromBytes, Immutable, IntoBytes}; use crate::signal::SigSet; pub mod errno; +#[cfg(target_arch = "aarch64")] +pub mod hwcap; pub mod loader; pub mod mm; pub mod program_startup; diff --git a/litebox_platform_linux_userland/src/aarch64.rs b/litebox_platform_linux_userland/src/aarch64.rs index 073c49811a..5c2f1da844 100644 --- a/litebox_platform_linux_userland/src/aarch64.rs +++ b/litebox_platform_linux_userland/src/aarch64.rs @@ -1300,19 +1300,13 @@ switch_to_guest_stage_x16_fixup: sub x4, x4, #{SVC_FRAME_BYTES} mov sp, x4 - // `msr nzcv` writes only PSTATE 31:28. SSBS and DIT are carried in - // `PtRegs::pstate` (see `copy_signal_context`) but are knowingly NOT - // restored here: writing them needs `msr ssbs`/`msr dit`, which are - // ARMv8.0 optional / ARMv8.4 respectively, so unconditionally emitting - // them would raise this crate's hardware floor. A guest that sets SSBS or - // DIT therefore loses them across a syscall: both are hardening hints with - // no architectural effect on results. (The generic `rt_sigreturn` path - // does restore them for free by writing the whole SPSR; see - // `resume_frame::prepare`.) + // Syscall entry saves only NZCV into `PtRegs::pstate`, and this path restores only NZCV, + // so SSBS/DIT are not preserved across syscalls: a resume that rebuilds PSTATE from + // `PtRegs::pstate` (`resume_frame::prepare`) clears them. Saving and restoring them needs + // `mrs`/`msr` of SSBS and DIT (optional ARMv8.0 / ARMv8.4), which would raise this crate's + // hardware floor, so guests are not offered them (see `get_guest_cpu_features`). // - // TODO: restore SSBS/DIT for guests that set them, gated on a runtime - // feature probe (HWCAP_SSBS / HWCAP2_DIT) so the hardware floor stays at - // ARMv8.0. Only worth doing once a guest actually depends on them. + // TODO: save and restore SSBS/DIT on the syscall path, gated on HWCAP_SSBS/HWCAP_DIT. ldr x1, [x0, #264] msr nzcv, x1 ldr x1, [x0, #8] @@ -1449,6 +1443,25 @@ impl litebox::platform::ArchSpecificProvider for LinuxUserland { _ => Err(litebox::platform::ArchSpecificError::RegisterUnsupported), } } + fn get_guest_cpu_features(&self) -> litebox::arch::aarch64::CpuFeatures { + use litebox::arch::aarch64::CpuFeatures; + + // SAFETY: getauxval only reads this process's auxiliary vector. It makes no syscall, so + // it also works under the seccomp filter. + let (hwcap, hwcap2) = unsafe { + ( + libc::getauxval(libc::AT_HWCAP), + libc::getauxval(libc::AT_HWCAP2), + ) + }; + let host = litebox_common_linux::hwcap::LinuxHwcaps { hwcap, hwcap2 }.to_cpu_features(); + // PAUTH is kept: PAC keys are per host process and inherited by native fork. Excluded: + // - DIT and SSBS: the syscall path saves and restores only NZCV (see + // `switch_to_guest_via_outbound_stub`). + // - SVE and SME: only FP/SIMD vector state is saved. + // - BTI and MTE: there is no PROT_BTI/PROT_MTE page-attribute support. + host & (CpuFeatures::BASE_STATE_ONLY | CpuFeatures::PAUTH) + } } impl litebox::platform::GuestVectorStateProvider for LinuxUserland { diff --git a/litebox_platform_macos_userland/src/lib.rs b/litebox_platform_macos_userland/src/lib.rs index 95e8624dbe..d173b896af 100644 --- a/litebox_platform_macos_userland/src/lib.rs +++ b/litebox_platform_macos_userland/src/lib.rs @@ -5,6 +5,7 @@ #![cfg(all(target_os = "macos", target_arch = "aarch64"))] use std::cell::Cell; +use std::ffi::CStr; use std::ops::Range; use std::sync::{ Arc, Condvar, Mutex, OnceLock, Weak, @@ -12,6 +13,7 @@ use std::sync::{ }; use std::time::Duration; +use litebox::arch::aarch64::CpuFeatures; use litebox::platform::page_mgmt::{ AllocationError, DeallocationError, FixedAddressBehavior, MemoryRegionPermissions, PermissionUpdateError, @@ -82,6 +84,7 @@ pub const TASK_ADDR_MAX: usize = 0x7FFF_FE00_0000; /// `MacosUserland4K` (requires `subpage_compat`) for 4 KiB guests. pub struct MacosUserlandWithPageSize { pages: Mutex, + host_cpu_features: CpuFeatures, } /// Page-management backend, chosen once from the guest page size. @@ -184,6 +187,7 @@ impl MacosUserlandWithPageSize { let pages = PageTable::Native(native::Pages::default()); Box::leak(Box::new(Self { pages: Mutex::new(pages), + host_cpu_features: detect_host_cpu_features(), })) } } @@ -1119,6 +1123,75 @@ fn set_guest_x18(value: usize) { write_tls(tls_offset::GUEST_X18, value); } +/// Documented `hw.optional` sysctls for host CPU features. WFXT, ECV, and MOPS stay unmapped +/// because their EL0 usability under XNU is not established. +const HOST_CPU_FEATURE_SYSCTLS: &[(&CStr, CpuFeatures)] = &[ + (c"hw.optional.floatingpoint", CpuFeatures::FP), + // The canonical `hw.optional.arm.*` names exist from macOS 15.4; the legacy names cover + // earlier releases (`hw.optional.AdvSIMD` existed only in 12.0-15.3). Either name suffices. + (c"hw.optional.arm.AdvSIMD", CpuFeatures::ASIMD), + (c"hw.optional.neon", CpuFeatures::ASIMD), + (c"hw.optional.arm.FEAT_CRC32", CpuFeatures::CRC32), + (c"hw.optional.armv8_crc32", CpuFeatures::CRC32), + (c"hw.optional.arm.FEAT_AES", CpuFeatures::AES), + (c"hw.optional.arm.FEAT_PMULL", CpuFeatures::PMULL), + (c"hw.optional.arm.FEAT_SHA1", CpuFeatures::SHA1), + (c"hw.optional.arm.FEAT_SHA256", CpuFeatures::SHA256), + (c"hw.optional.arm.FEAT_LSE", CpuFeatures::LSE), + (c"hw.optional.arm.FEAT_FP16", CpuFeatures::FP16), + (c"hw.optional.arm.FEAT_RDM", CpuFeatures::RDM), + (c"hw.optional.arm.FEAT_JSCVT", CpuFeatures::JSCVT), + (c"hw.optional.arm.FEAT_FCMA", CpuFeatures::FCMA), + (c"hw.optional.arm.FEAT_LRCPC", CpuFeatures::LRCPC), + (c"hw.optional.arm.FEAT_DPB", CpuFeatures::DPB), + (c"hw.optional.arm.FEAT_SHA3", CpuFeatures::SHA3), + (c"hw.optional.arm.FEAT_DotProd", CpuFeatures::DOTPROD), + (c"hw.optional.arm.FEAT_SHA512", CpuFeatures::SHA512), + (c"hw.optional.arm.FEAT_FHM", CpuFeatures::FHM), + (c"hw.optional.arm.FEAT_LSE2", CpuFeatures::LSE2), + (c"hw.optional.arm.FEAT_LRCPC2", CpuFeatures::LRCPC2), + (c"hw.optional.arm.FEAT_FlagM", CpuFeatures::FLAGM), + (c"hw.optional.arm.FEAT_SB", CpuFeatures::SB), + (c"hw.optional.arm.FEAT_DPB2", CpuFeatures::DPB2), + (c"hw.optional.arm.FEAT_FlagM2", CpuFeatures::FLAGM2), + (c"hw.optional.arm.FEAT_FRINTTS", CpuFeatures::FRINTTS), + (c"hw.optional.arm.FEAT_I8MM", CpuFeatures::I8MM), + (c"hw.optional.arm.FEAT_BF16", CpuFeatures::BF16), + (c"hw.optional.arm.FEAT_AFP", CpuFeatures::AFP), + (c"hw.optional.arm.FEAT_RPRES", CpuFeatures::RPRES), + (c"hw.optional.arm.FEAT_EBF16", CpuFeatures::EBF16), + (c"hw.optional.arm.FEAT_CSSC", CpuFeatures::CSSC), + (c"hw.optional.arm.FEAT_HBC", CpuFeatures::HBC), + (c"hw.optional.arm.FEAT_DIT", CpuFeatures::DIT), + (c"hw.optional.arm.FEAT_SSBS", CpuFeatures::SSBS), + (c"hw.optional.arm.FEAT_PAuth", CpuFeatures::PAUTH), + (c"hw.optional.arm.FEAT_BTI", CpuFeatures::BTI), + (c"hw.optional.arm.FEAT_SME", CpuFeatures::SME), +]; + +fn detect_host_cpu_features() -> CpuFeatures { + HOST_CPU_FEATURE_SYSCTLS + .iter() + .filter(|(name, _)| { + let mut value: libc::c_int = 0; + let mut size = size_of_val(&value); + // SAFETY: `name` is NUL-terminated, `value` is writable output storage of `size` + // bytes, and no new value is written. + let result = unsafe { + libc::sysctlbyname( + name.as_ptr(), + (&raw mut value).cast(), + &raw mut size, + std::ptr::null_mut(), + 0, + ) + }; + result == 0 && size == size_of_val(&value) && value != 0 + }) + .map(|&(_, feature)| feature) + .collect() +} + impl litebox::platform::ArchSpecificProvider for MacosUserlandWithPageSize { @@ -1148,6 +1221,16 @@ impl litebox::platform::ArchSpecificProvider _ => Err(ArchSpecificError::RegisterUnsupported), } } + fn get_guest_cpu_features(&self) -> CpuFeatures { + // Excluded: + // - PAUTH: it is not established that XNU enables PAC keys for arm64 (non-arm64e) + // processes. + // - DIT and SSBS: the syscall path saves and restores only NZCV. + // - SME and BTI: they need architectural state or kernel ABI this platform lacks. + // Assumes XNU keeps the whole FPCR across exceptions and sigreturn (its ARM_NEON_STATE64 + // set-state path stores it unmasked), so FP16/AFP/EBF16 FPCR controls need nothing more. + self.host_cpu_features & CpuFeatures::BASE_STATE_ONLY + } } const FORWARDED_SIGNALS: &[(i32, litebox_common_linux::signal::Signal)] = &[ @@ -1444,6 +1527,7 @@ impl litebox::platform::GuestVectorStateProvider set_guest_vector_state(state); } } + impl litebox::platform::SystemInfoProvider for MacosUserlandWithPageSize { @@ -3450,6 +3534,16 @@ mod tests { assert!(result.is_err(), "the panic must propagate to the caller"); } + #[test] + fn detect_host_cpu_features_reports_mandatory_features() { + // Every Apple silicon Mac has these; a miss means a sysctl name is wrong. + let features = detect_host_cpu_features(); + assert!( + features.contains(CpuFeatures::FP | CpuFeatures::ASIMD | CpuFeatures::CRC32), + "{features:?}" + ); + } + #[test] fn pstate_capture_and_restore_preserve_only_user_bits() { use litebox::arch::aarch64::{PSR_DIT_BIT, PSR_NZCV_MASK, PSR_SSBS_BIT}; diff --git a/litebox_runner_linux_on_macos_userland/tests/runner.rs b/litebox_runner_linux_on_macos_userland/tests/runner.rs index bc37b3d36e..ba8ecc953f 100644 --- a/litebox_runner_linux_on_macos_userland/tests/runner.rs +++ b/litebox_runner_linux_on_macos_userland/tests/runner.rs @@ -204,6 +204,58 @@ fn fp_registers_survive_syscalls() { ); } +#[test] +#[cfg_attr( + not(feature = "test-broker"), + ignore = "macOS runner requires broker support" +)] +fn hwcaps_expose_only_supported_host_features() { + let fixture = Fixture::new(); + // Writes the auxiliary vector to stdout. + let code = [ + 0x910003e1, // mov x1, sp + 0xf9400029, // ldr x9, [x1] (argc) + 0x91000929, // add x9, x9, #2 + 0x8b090c21, // add x1, x1, x9, lsl #3 (envp) + 0xf8408429, // ldr x9, [x1], #8 + 0xb5ffffe9, // cbnz x9, -4 + 0xaa0103e2, // mov x2, x1 (auxv) + 0xf8410449, // ldr x9, [x2], #16 + 0xb5ffffe9, // cbnz x9, -4 + 0xd2800020, // mov x0, #1 (stdout) + 0xcb010042, // sub x2, x2, x1 + 0xd2800808, // mov x8, #64 (write) + 0xd4000001, // svc #0 + 0xd2800540, 0xd2800ba8, 0xd4000001, // exit(42) + ]; + 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) + ); + let (words, _) = output.stdout.as_chunks::<8>(); + let words: Vec = words.iter().map(|&word| u64::from_le_bytes(word)).collect(); + let aux = |key: u64| { + words + .chunks(2) + .find(|pair| pair[0] == key) + .map_or_else(|| panic!("missing auxv key {key}"), |pair| pair[1]) + }; + let (hwcap, hwcap2) = (aux(16), aux(26)); + + // HWCAP_FP | HWCAP_ASIMD: every Apple silicon host has them. + assert_eq!(hwcap & 0b11, 0b11, "hwcap={hwcap:#x}"); + // HWCAP_CPUID, HWCAP_DIT, HWCAP_SSBS, HWCAP_PACA, and HWCAP_PACG. + let never = 1 << 11 | 1 << 24 | 1 << 28 | 1 << 30 | 1 << 31; + assert_eq!(hwcap & never, 0, "hwcap={hwcap:#x}"); + // HWCAP2_BTI, HWCAP2_MTE, and HWCAP2_SME. + let never2 = 1 << 17 | 1 << 18 | 1 << 23; + assert_eq!(hwcap2 & never2, 0, "hwcap2={hwcap2:#x}"); +} + #[test] #[cfg_attr( not(feature = "test-broker"), diff --git a/litebox_runner_linux_userland/tests/aarch64_hwcaps.c b/litebox_runner_linux_userland/tests/aarch64_hwcaps.c new file mode 100644 index 0000000000..f47df70391 --- /dev/null +++ b/litebox_runner_linux_userland/tests/aarch64_hwcaps.c @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +// Reports the guest's AT_HWCAP/AT_HWCAP2 and checks that FPCR control bits (RMode, plus those of +// the offered FP16, AFP, and EBF16 features) survive a syscall and a signal whose handler +// clobbers FPCR. + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include + +#if !defined(__aarch64__) +#error "this test checks AArch64 hwcaps and FPCR state" +#endif + +// Linux uapi `arch/arm64/include/uapi/asm/hwcap.h`; older libc headers may lack them. +#ifndef HWCAP_FPHP +#define HWCAP_FPHP (1UL << 9) +#endif +#ifndef HWCAP2_AFP +#define HWCAP2_AFP (1UL << 20) +#endif +#ifndef HWCAP2_EBF16 +#define HWCAP2_EBF16 (1UL << 32) +#endif + +#define FPCR_FIZ_AH_NEP 0x7UL +#define FPCR_EBF (1UL << 13) +#define FPCR_FZ16 (1UL << 19) +// RMode = round toward zero; every FP host supports it. +#define FPCR_RMODE_RZ (3UL << 22) + +static uint64_t read_fpcr(void) { + uint64_t fpcr; + __asm__ volatile("mrs %0, fpcr" : "=r"(fpcr)); + return fpcr; +} + +static void write_fpcr(uint64_t fpcr) { __asm__ volatile("msr fpcr, %0" : : "r"(fpcr)); } + +static volatile sig_atomic_t handler_ran; + +static void clobber_fpcr(int sig) { + (void)sig; + handler_ran = 1; + write_fpcr(0); +} + +int main(void) { + unsigned long hwcap = getauxval(AT_HWCAP); + unsigned long hwcap2 = getauxval(AT_HWCAP2); + printf("hwcaps hwcap=%lu hwcap2=%lu\n", hwcap, hwcap2); + + uint64_t bits = FPCR_RMODE_RZ; + if (hwcap & HWCAP_FPHP) + bits |= FPCR_FZ16; + if (hwcap2 & HWCAP2_AFP) + bits |= FPCR_FIZ_AH_NEP; + if (hwcap2 & HWCAP2_EBF16) + bits |= FPCR_EBF; + + struct sigaction action = {.sa_handler = clobber_fpcr}; + sigemptyset(&action.sa_mask); + if (sigaction(SIGUSR1, &action, NULL) != 0) { + perror("sigaction"); + return 1; + } + + uint64_t original = read_fpcr(); + uint64_t marked = original | bits; + write_fpcr(marked); + syscall(SYS_getppid); + int syscall_ok = read_fpcr() == marked; + write_fpcr(marked); + int raised = raise(SIGUSR1) == 0; + int signal_ok = raised && handler_ran && read_fpcr() == marked; + write_fpcr(original); + + printf("fpcr bits=%#llx syscall=%d signal=%d\n", (unsigned long long)bits, syscall_ok, + signal_ok); + return 0; +} diff --git a/litebox_runner_linux_userland/tests/run.rs b/litebox_runner_linux_userland/tests/run.rs index 6b03c46793..e03f6eb803 100644 --- a/litebox_runner_linux_userland/tests/run.rs +++ b/litebox_runner_linux_userland/tests/run.rs @@ -58,6 +58,7 @@ impl litebox_broker_core::stdio::StdioProvider for CapturingStdioProvider { // Dedicated fixtures build static binaries concurrently; exclude them to avoid // colliding with this sweep's dynamic `_rewriter` outputs. const DEDICATED_C_TESTS: &[&str] = &[ + "aarch64_hwcaps.c", "async_x16.c", "fork_chain_parent.c", "fork_parent.c", @@ -1088,6 +1089,73 @@ fn fork_and_vfork_preserve_aarch64_state() { assert_eq!(numeric_field(signed_line, "code="), 0, "{signed_line}"); } +#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +#[test] +fn aarch64_hwcaps_expose_only_supported_host_features() { + use litebox::arch::aarch64::CpuFeatures; + use litebox_common_linux::hwcap::LinuxHwcaps; + + // Bits from Linux uapi `arch/arm64/include/uapi/asm/hwcap.h` that libc does not define. + const HWCAP2_BTI: u64 = 1 << 17; + const HWCAP2_MTE: u64 = 1 << 18; + const HWCAP2_SME: u64 = 1 << 23; + + let program = common::compile("./tests/aarch64_hwcaps.c", "aarch64_hwcaps", true, false); + let output = String::from_utf8(Runner::new(&program, "aarch64_hwcaps").output()).unwrap(); + let line = |prefix: &str| { + output + .lines() + .find(|line| line.starts_with(prefix)) + .unwrap_or_else(|| panic!("missing {prefix:?} output in {output:?}")) + }; + let hwcaps_line = line("hwcaps "); + let field = |name: &str| -> u64 { + hwcaps_line + .split_whitespace() + .find_map(|field| field.strip_prefix(name)?.parse().ok()) + .unwrap_or_else(|| panic!("missing {name} in {hwcaps_line:?}")) + }; + let (hwcap, hwcap2) = (field("hwcap="), field("hwcap2=")); + // SAFETY: getauxval only reads this process's auxiliary vector. + let (host_hwcap, host_hwcap2) = unsafe { + ( + libc::getauxval(libc::AT_HWCAP), + libc::getauxval(libc::AT_HWCAP2), + ) + }; + + // Mirrors the platform filter intersected with the shim filter, which drops PAUTH under + // copy-based fork. + let mut exposed = CpuFeatures::BASE_STATE_ONLY | CpuFeatures::PAUTH; + if litebox_shim_linux::syscalls::process::COPY_BASED_FORK { + exposed.remove(CpuFeatures::PAUTH); + } + let host = LinuxHwcaps { + hwcap: host_hwcap, + hwcap2: host_hwcap2, + }; + let expected = LinuxHwcaps::from_cpu_features(host.to_cpu_features() & exposed); + assert_eq!( + (hwcap, hwcap2), + (expected.hwcap, expected.hwcap2), + "{hwcaps_line}: host hwcap={host_hwcap:#x} hwcap2={host_hwcap2:#x}" + ); + // These checks are independent of the allow-list and the hwcap mapping. + let required = libc::HWCAP_FP | libc::HWCAP_ASIMD; + assert_eq!(hwcap & required, required, "{hwcaps_line}"); + // HWCAP_CPUID would tell glibc/compiler-rt to trust the host's unfiltered ID-register reads. + let never = libc::HWCAP_CPUID | libc::HWCAP_SVE | libc::HWCAP_DIT | libc::HWCAP_SSBS; + assert_eq!(hwcap & never, 0, "{hwcaps_line}"); + let never2 = HWCAP2_BTI | HWCAP2_MTE | HWCAP2_SME; + assert_eq!(hwcap2 & never2, 0, "{hwcaps_line}"); + + // FPCR controls, including those of the offered FP16, AFP, and EBF16 features, survive + // syscalls and signals. + let fpcr_line = line("fpcr "); + assert_eq!(numeric_field(fpcr_line, "syscall="), 1, "{fpcr_line}"); + assert_eq!(numeric_field(fpcr_line, "signal="), 1, "{fpcr_line}"); +} + #[cfg(target_os = "linux")] #[test] fn forked_and_exec_children_share_unix_sockets() { diff --git a/litebox_shim_linux/src/loader/auxv.rs b/litebox_shim_linux/src/loader/auxv.rs index 6a95941b4a..3ee762a86f 100644 --- a/litebox_shim_linux/src/loader/auxv.rs +++ b/litebox_shim_linux/src/loader/auxv.rs @@ -92,6 +92,34 @@ impl Task { crate::syscalls::signal::max_signal_frame_size(), ); + #[cfg(target_arch = "aarch64")] + { + use litebox::arch::aarch64::CpuFeatures; + use litebox::utils::TruncateExt as _; + + // Excluded: SVE and SME need SVE/ZA signal-frame records and PR_SVE_*/PR_SME_* prctls; + // BTI and MTE need PROT_BTI/PROT_MTE and tagged-address prctls. + // + // PAUTH caveat: PR_PAC_* prctls fail with EINVAL, as on Linux without pointer + // authentication. Key resets and sets must never reach the host: the guest and + // LiteBox share one set of PAC keys. + let mut supported = CpuFeatures::BASE_STATE_ONLY + | CpuFeatures::DIT + | CpuFeatures::SSBS + | CpuFeatures::PAUTH; + // Copy-based fork requires runners to disable the PAC address keys, and its child + // gets a different generic key. + if crate::syscalls::process::COPY_BASED_FORK { + supported.remove(CpuFeatures::PAUTH); + } + let hwcaps = litebox_common_linux::hwcap::LinuxHwcaps::from_cpu_features( + self.global.platform.get_guest_cpu_features() & supported, + ); + // Linux always provides both, even when zero. + aux.insert(AuxKey::AT_HWCAP, hwcaps.hwcap.trunc()); + aux.insert(AuxKey::AT_HWCAP2, hwcaps.hwcap2.trunc()); + } + aux } }