From 88ea06c89c952a622acbfdc02f298c1bb23f2f04 Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Fri, 23 Oct 2026 18:28:18 +0200 Subject: [PATCH 1/9] Revise MPK support check to allow non-Intel CPUs. The check for `mpk::is_supported` explicitly checked for GenuineIntel, and then for the PKU bit on the cpuid message. This is misguided for two reasons: 1. Zen3 AMD also supports MPKs but wasmtime misses out on using them; 2. the OSPKE bit also has to be checked to actually be correct. Number 1. seems to have been done as an attempt to avoid CI crashes. In #7445, #7446 MPK was disabled and then reenabled for Intel only in PR #7513. I believe that was actually a red herring and those crashes were caused by the second point. PKU only says if the CPU supports MPKs on the hardware level. However, the OS still needs to enable PKE. If it is not enabled, RDPKRU/WRPKRU raise #UD, which kills the process with SIGILL. An example is a VM that runs on a modern CPU as the hardware, but whose hypervisor has PKU disabled. This would actually fault on *any* vendor, Intel included. You can test it today on the old version by running wasmtime in QEMU with nopku passed (`-append "nopku"`), it will crash with the pooling allocator when it tries to set up PKs. In this commit we switch to checking both PKE and OSPKE, removing the vendor check. This is confirmed to work on modern AMD and Intel, and correctly recognises MPK when ran under QEMU: - with `-cpu host` and default kernel settings (supported), - with `-cpu host` but `nopku` in the kernel (unsupported), - with `-cpu host,-pku` (unsupported). We now have a test in tests/mpk.rs to ensure the result of the check agrees with actual capabilities the process has. Additionally, removed a stale test in `pkru::tests::check_read`, it was ignored since forever and the new mpk test supersedes it. --- Cargo.lock | 1 + crates/core/Cargo.toml | 3 + crates/core/src/mpk.rs | 53 ++++++------ crates/core/tests/mpk.rs | 93 ++++++++++++++++++++++ crates/wasmtime/src/runtime/vm/mpk/pkru.rs | 9 --- 5 files changed, 125 insertions(+), 34 deletions(-) create mode 100644 crates/core/tests/mpk.rs diff --git a/Cargo.lock b/Cargo.lock index dc5a3d079735..6bccf33b67b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4929,6 +4929,7 @@ version = "51.0.0-dev" dependencies = [ "anyhow", "hashbrown 0.17.0", + "libc", "libm", "serde", ] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index b8573e2b3a1d..70cbdec673f7 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -15,6 +15,9 @@ hashbrown = { workspace = true, features = ["default-hasher"] } libm = { workspace = true } serde = { workspace = true, optional = true } +[target.'cfg(target_os = "linux")'.dev-dependencies] +libc = { workspace = true } + [lints] workspace = true diff --git a/crates/core/src/mpk.rs b/crates/core/src/mpk.rs index 07e572195431..a62de495f2b0 100644 --- a/crates/core/src/mpk.rs +++ b/crates/core/src/mpk.rs @@ -8,37 +8,40 @@ /// Returns `true` if Wasmtime's MPK support can be used on this host. pub fn is_supported() -> bool { - cfg!(target_os = "linux") && cpuid_pku_bit_set() + cfg!(target_os = "linux") && cpuid_pku_enabled() } -/// Check the `ECX.PKU` flag (bit 3, zero-based) of the `07h` `CPUID` leaf; see -/// the Intel Software Development Manual, vol 3a, section 2.7. This flag is -/// only set on Intel CPUs, so this function also checks the `CPUID` vendor -/// string. +/// Check whether protection keys (pkus) are supported by the CPU and enabled by the OS. +/// +/// This is exposed in the ECX result of cpuid leaf 07h: +/// +/// - bit 3 (`PKU`) is set if the processor supports protection keys; +/// - bit 4 (`OSPKE`) is set if the OS has also set `CR4.PKE`, which is what makes +/// `RDPKRU`/`WRPKRU` usable. Without it these instructions raise `#UD` (`SIGILL`), for example in +/// virtual machines whose hypervisor reports `PKU` but doesn't enable it. +/// +/// This is consistent between Intel and AMD CPUs. #[cfg(target_arch = "x86_64")] -fn cpuid_pku_bit_set() -> bool { - is_intel_cpu() && { - #[allow( - unused_unsafe, - reason = "rust is transitioning to `__cpuid` being a safe function" - )] - let result = unsafe { core::arch::x86_64::__cpuid(0x07) }; - (result.ecx & 0b1000) != 0 +fn cpuid_pku_enabled() -> bool { + use core::arch::x86_64::{__cpuid, __cpuid_count}; + + const PKU: u32 = 1 << 3; + const OSPKE: u32 = 1 << 4; + + #[allow( + unused_unsafe, + reason = "rust is transitioning to `__cpuid` being a safe function" + )] + let max_leaf = unsafe { __cpuid(0) }.eax; + if max_leaf < 0x07 { + return false; } + #[allow(unused_unsafe, reason = "as above")] + let ecx = unsafe { __cpuid_count(0x07, 0) }.ecx; + ecx & (PKU | OSPKE) == PKU | OSPKE } #[cfg(not(target_arch = "x86_64"))] -fn cpuid_pku_bit_set() -> bool { +fn cpuid_pku_enabled() -> bool { false } - -/// Check the `CPUID` vendor string for `GenuineIntel`; see the Intel Software -/// Development Manual, vol 2a, `CPUID` description. -#[cfg(target_arch = "x86_64")] -fn is_intel_cpu() -> bool { - #[allow(unused_unsafe, reason = "see above about __cpuid")] - let result = unsafe { core::arch::x86_64::__cpuid(0) }; - result.ebx == u32::from_le_bytes(*b"Genu") - && result.edx == u32::from_le_bytes(*b"ineI") - && result.ecx == u32::from_le_bytes(*b"ntel") -} diff --git a/crates/core/tests/mpk.rs b/crates/core/tests/mpk.rs new file mode 100644 index 000000000000..2b78440c973c --- /dev/null +++ b/crates/core/tests/mpk.rs @@ -0,0 +1,93 @@ +//! Checks that `mpk::is_supported` agrees with what the hardware and kernel +//! actually allow. +//! +//! We test that `RDPKRU`/`WRPKRU` work if and only if `mpk::is_supported` returns true. +//! The => direction is required to avoid hard to debug SIGILLs if our is_supported logic was wrong. +//! The other direction allows us to detect if we're missing out on using MPKs in places they are +//! actually allowed. +//! +//! This is still linux and x86 specific. +#![cfg(all(target_os = "linux", target_arch = "x86_64", not(miri)))] + +use std::arch::asm; +use std::io; +use std::os::unix::process::ExitStatusExt; +use std::process::{Command, exit}; + +/// Set in the child process to make it run the probe instead of the test. +const PROBE_ENV_VAR: &str = "WASMTIME_TEST_MPK_PROBE"; + +/// Name of the test below, used to run only it in the child process. +const TEST_NAME: &str = "is_supported_matches_hardware"; + +// Exit codes of the probe, besides success. +const EXIT_PKEY_ALLOC_FAILED: i32 = 2; +const EXIT_PKEY_FREE_FAILED: i32 = 3; + +#[test] +fn is_supported_matches_hardware() { + // We are the child that is supposed to perform the probe. + if std::env::var_os(PROBE_ENV_VAR).is_some() { + probe(); + // probe always exits, this is unreachable + } + + let output = Command::new(std::env::current_exe().unwrap()) + .args(["--exact", TEST_NAME, "--nocapture", "--test-threads=1"]) + .env(PROBE_ENV_VAR, "1") + .output() + .expect("failed to spawn the probe process"); + let usable = if output.status.success() { + true + } else if output.status.signal() == Some(libc::SIGILL) { + false + } else { + panic!( + "inconclusive probe for protection key support: {}\n\ + stdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + }; + + assert_eq!( + wasmtime_internal_core::mpk::is_supported(), + usable, + "`mpk::is_supported()` disagrees with whether protection keys can \ + actually be used on this host", + ); +} + +/// Checks `RDPKRU`, `WRPKRU`, and tries to allocate and free a pkey. +/// Exits the process with 0 if everything worked, raises `SIGILL` otherwise. +fn probe() -> ! { + // No crate offers rdpkru or wrpkru as a Rust function (neither rustix nor libc), so we need to + // rawdog asm. + let pkru: u32; + // SAFETY: reading PKRU has no side effects; if protection keys are not + // enabled this raises `SIGILL`, which is what the parent checks for. + unsafe { + asm!("rdpkru", in("ecx") 0u32, out("eax") pkru, out("edx") _, + options(nomem, nostack, preserves_flags)); + } + // SAFETY: writing back the same value doesn't change any access rights. + unsafe { + asm!("wrpkru", in("eax") pkru, in("ecx") 0u32, in("edx") 0u32, + options(nomem, nostack, preserves_flags)); + } + + // SAFETY: `pkey_alloc(flags = 0, access_rights = 0)` only allocates a key. + let key = unsafe { libc::syscall(libc::SYS_pkey_alloc, 0, 0) }; + if key < 0 { + eprintln!("pkey_alloc failed: {}", io::Error::last_os_error()); + exit(EXIT_PKEY_ALLOC_FAILED); + } + // SAFETY: frees the key just allocated, which nothing uses. + let ret = unsafe { libc::syscall(libc::SYS_pkey_free, key as usize, 0) }; + if ret != 0 { + eprintln!("pkey_free failed: {}", io::Error::last_os_error()); + exit(EXIT_PKEY_FREE_FAILED); + } + exit(0) +} diff --git a/crates/wasmtime/src/runtime/vm/mpk/pkru.rs b/crates/wasmtime/src/runtime/vm/mpk/pkru.rs index 50f1f69b1d47..d59c437660ec 100644 --- a/crates/wasmtime/src/runtime/vm/mpk/pkru.rs +++ b/crates/wasmtime/src/runtime/vm/mpk/pkru.rs @@ -60,15 +60,6 @@ mod tests { use super::*; use crate::runtime::vm::mpk::enabled::skip_if_mpk_unavailable; - #[test] - #[ignore = "cannot be run with other tests that munge the PKRU register"] - fn check_read() { - skip_if_mpk_unavailable!(); - assert_eq!(read(), DISABLE_ACCESS ^ 1); - // By default, the Linux kernel only allows a process to access key 0, - // the default kernel key. - } - #[test] fn check_roundtrip() { skip_if_mpk_unavailable!(); From 29bf46e6718088ba17cd127a12b66069bd6c1ed9 Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Sat, 24 Oct 2026 01:17:01 +0200 Subject: [PATCH 2/9] Rework the MPK test to not use harness. --- Cargo.lock | 1 + crates/core/Cargo.toml | 7 ++++ crates/core/tests/mpk.rs | 79 +++++++++++++++++++++++++--------------- 3 files changed, 57 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6bccf33b67b5..e65b6d5bb977 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4931,6 +4931,7 @@ dependencies = [ "hashbrown 0.17.0", "libc", "libm", + "libtest-mimic", "serde", ] diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 70cbdec673f7..c945e00915c7 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -17,10 +17,15 @@ serde = { workspace = true, optional = true } [target.'cfg(target_os = "linux")'.dev-dependencies] libc = { workspace = true } +libtest-mimic = { workspace = true } [lints] workspace = true +[[test]] +name = "mpk" +harness = false + [features] # Enable the use of the `std` crate. std = [] @@ -30,3 +35,5 @@ backtrace = ["std"] # `Error::from_anyhow` constructor. anyhow = ["dep:anyhow"] serde = ["dep:serde"] + + diff --git a/crates/core/tests/mpk.rs b/crates/core/tests/mpk.rs index 2b78440c973c..aafe11145503 100644 --- a/crates/core/tests/mpk.rs +++ b/crates/core/tests/mpk.rs @@ -6,9 +6,10 @@ //! The other direction allows us to detect if we're missing out on using MPKs in places they are //! actually allowed. //! -//! This is still linux and x86 specific. -#![cfg(all(target_os = "linux", target_arch = "x86_64", not(miri)))] +//! On miri we cannot spawn a subprocess at all. +#![cfg(not(miri))] +use libtest_mimic::{Arguments, Failed, Trial}; use std::arch::asm; use std::io; use std::os::unix::process::ExitStatusExt; @@ -17,50 +18,54 @@ use std::process::{Command, exit}; /// Set in the child process to make it run the probe instead of the test. const PROBE_ENV_VAR: &str = "WASMTIME_TEST_MPK_PROBE"; -/// Name of the test below, used to run only it in the child process. -const TEST_NAME: &str = "is_supported_matches_hardware"; - // Exit codes of the probe, besides success. const EXIT_PKEY_ALLOC_FAILED: i32 = 2; const EXIT_PKEY_FREE_FAILED: i32 = 3; -#[test] -fn is_supported_matches_hardware() { +fn main() { // We are the child that is supposed to perform the probe. if std::env::var_os(PROBE_ENV_VAR).is_some() { probe(); // probe always exits, this is unreachable } - - let output = Command::new(std::env::current_exe().unwrap()) - .args(["--exact", TEST_NAME, "--nocapture", "--test-threads=1"]) - .env(PROBE_ENV_VAR, "1") - .output() - .expect("failed to spawn the probe process"); - let usable = if output.status.success() { - true - } else if output.status.signal() == Some(libc::SIGILL) { - false - } else { - panic!( - "inconclusive probe for protection key support: {}\n\ + // We are the test entrypoint. + libtest_mimic::run( + &Arguments::from_args(), + vec![Trial::test("is_supported_matches_hardware_and_os", || { + let output = Command::new(std::env::current_exe().unwrap()) + .env(PROBE_ENV_VAR, "1") + .output() + .expect("failed to spawn the probe process"); + let usable = if output.status.success() { + true + } else if output.status.signal() == Some(libc::SIGILL) { + false + } else { + return Err(Failed::from(format!( + "inconclusive probe for protection key support: {}\n\ stdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - }; + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ))); + }; - assert_eq!( - wasmtime_internal_core::mpk::is_supported(), - usable, - "`mpk::is_supported()` disagrees with whether protection keys can \ + if wasmtime_internal_core::mpk::is_supported() != usable { + Err(Failed::from( + "`mpk::is_supported()` disagrees with whether protection keys can \ actually be used on this host", - ); + )) + } else { + Ok(()) + } + })], + ) + .exit(); } /// Checks `RDPKRU`, `WRPKRU`, and tries to allocate and free a pkey. /// Exits the process with 0 if everything worked, raises `SIGILL` otherwise. +#[cfg(all(unix))] fn probe() -> ! { // No crate offers rdpkru or wrpkru as a Rust function (neither rustix nor libc), so we need to // rawdog asm. @@ -91,3 +96,17 @@ fn probe() -> ! { } exit(0) } + +/// On non-unix platforms the pkey_alloc syscall doesn't exist, so we can't compile the above. +/// However, rdpkru and wrpkru should kill us with SIGILL, since these OSs don't enable PKU at all. +#[cfg(not(unix))] +fn probe() -> ! { + let pkru: u32; + // SAFETY: reading PKRU has no side effects; if protection keys are not + // enabled this raises `SIGILL`, which is what the parent checks for. + unsafe { + asm!("rdpkru", in("ecx") 0u32, out("eax") pkru, out("edx") _, + options(nomem, nostack, preserves_flags)); + } + exit(0) +} From 631dd393797f7071ed17e20c3b18996561581c0b Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Sat, 24 Oct 2026 01:17:20 +0200 Subject: [PATCH 3/9] Add sources for Intel and AMD on MPK support. --- crates/core/src/mpk.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/core/src/mpk.rs b/crates/core/src/mpk.rs index a62de495f2b0..7666afadac1e 100644 --- a/crates/core/src/mpk.rs +++ b/crates/core/src/mpk.rs @@ -21,6 +21,11 @@ pub fn is_supported() -> bool { /// virtual machines whose hypervisor reports `PKU` but doesn't enable it. /// /// This is consistent between Intel and AMD CPUs. +/// Sources: +/// - Intel SDM Volume 1, Chapter 21, Section 21.3, Table 21-20 +/// (PDF at https://cdrdv2.intel.com/v1/dl/getContent/671436). +/// - AMD64 Architecture Programmer's Manual, Volume 3, Appendix E, section E.3.6 +/// (PDF at https://docs.amd.com/api/khub/documents/BqEoC2FadIAgbrUdEzDJIg/content). #[cfg(target_arch = "x86_64")] fn cpuid_pku_enabled() -> bool { use core::arch::x86_64::{__cpuid, __cpuid_count}; From 43907b97eb9d5c548e8c8eb8329c1f4067247f7f Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Sat, 24 Oct 2026 01:34:30 +0200 Subject: [PATCH 4/9] Fix SIGILL check invalid on Windows and Miri build. --- crates/core/Cargo.toml | 4 +++- crates/core/tests/mpk.rs | 39 ++++++++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index c945e00915c7..ff5388f4ce7a 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -15,9 +15,11 @@ hashbrown = { workspace = true, features = ["default-hasher"] } libm = { workspace = true } serde = { workspace = true, optional = true } +[dev-dependencies] +libtest-mimic = { workspace = true } + [target.'cfg(target_os = "linux")'.dev-dependencies] libc = { workspace = true } -libtest-mimic = { workspace = true } [lints] workspace = true diff --git a/crates/core/tests/mpk.rs b/crates/core/tests/mpk.rs index aafe11145503..b349f1f10500 100644 --- a/crates/core/tests/mpk.rs +++ b/crates/core/tests/mpk.rs @@ -7,12 +7,10 @@ //! actually allowed. //! //! On miri we cannot spawn a subprocess at all. -#![cfg(not(miri))] use libtest_mimic::{Arguments, Failed, Trial}; use std::arch::asm; -use std::io; -use std::os::unix::process::ExitStatusExt; +use std::process::ExitStatus; use std::process::{Command, exit}; /// Set in the child process to make it run the probe instead of the test. @@ -22,6 +20,11 @@ const PROBE_ENV_VAR: &str = "WASMTIME_TEST_MPK_PROBE"; const EXIT_PKEY_ALLOC_FAILED: i32 = 2; const EXIT_PKEY_FREE_FAILED: i32 = 3; +// Skip the test on miri, still needs an empty main. +#[cfg(miri)] +fn main() {} + +#[cfg(not(miri))] fn main() { // We are the child that is supposed to perform the probe. if std::env::var_os(PROBE_ENV_VAR).is_some() { @@ -38,7 +41,7 @@ fn main() { .expect("failed to spawn the probe process"); let usable = if output.status.success() { true - } else if output.status.signal() == Some(libc::SIGILL) { + } else if is_illegal_instruction(&output.status) { false } else { return Err(Failed::from(format!( @@ -50,11 +53,12 @@ fn main() { ))); }; - if wasmtime_internal_core::mpk::is_supported() != usable { - Err(Failed::from( + let is_supported = wasmtime_internal_core::mpk::is_supported(); + if is_supported != usable { + Err(Failed::from(format!( "`mpk::is_supported()` disagrees with whether protection keys can \ - actually be used on this host", - )) + actually be used on this host: is_supported() = {is_supported}, usable = {usable}", + ))) } else { Ok(()) } @@ -63,10 +67,23 @@ fn main() { .exit(); } +#[cfg(unix)] +fn is_illegal_instruction(status: &ExitStatus) -> bool { + use std::os::unix::process::ExitStatusExt; + status.signal() == Some(libc::SIGILL) +} + +#[cfg(windows)] +fn is_illegal_instruction(status: &ExitStatus) -> bool { + // STATUS_ILLEGAL_INSTRUCTION + status.code().map(|c| c as u32) == Some(0xC000001D) +} + /// Checks `RDPKRU`, `WRPKRU`, and tries to allocate and free a pkey. /// Exits the process with 0 if everything worked, raises `SIGILL` otherwise. -#[cfg(all(unix))] +#[cfg(target_os = "linux")] fn probe() -> ! { + use std::io; // No crate offers rdpkru or wrpkru as a Rust function (neither rustix nor libc), so we need to // rawdog asm. let pkru: u32; @@ -89,7 +106,7 @@ fn probe() -> ! { exit(EXIT_PKEY_ALLOC_FAILED); } // SAFETY: frees the key just allocated, which nothing uses. - let ret = unsafe { libc::syscall(libc::SYS_pkey_free, key as usize, 0) }; + let ret = unsafe { libc::syscall(libc::SYS_pkey_free, key as usize) }; if ret != 0 { eprintln!("pkey_free failed: {}", io::Error::last_os_error()); exit(EXIT_PKEY_FREE_FAILED); @@ -99,7 +116,7 @@ fn probe() -> ! { /// On non-unix platforms the pkey_alloc syscall doesn't exist, so we can't compile the above. /// However, rdpkru and wrpkru should kill us with SIGILL, since these OSs don't enable PKU at all. -#[cfg(not(unix))] +#[cfg(not(target_os = "linux"))] fn probe() -> ! { let pkru: u32; // SAFETY: reading PKRU has no side effects; if protection keys are not From e673de4edc0a221e25f5e5f9e97606032df0eef8 Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Sat, 24 Oct 2026 17:05:24 +0200 Subject: [PATCH 5/9] Revert the test to only run an actual probe on x86 linux. --- crates/core/Cargo.toml | 2 +- crates/core/tests/mpk.rs | 123 ++++++++++++++++++--------------------- 2 files changed, 59 insertions(+), 66 deletions(-) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index ff5388f4ce7a..b38cfd1558ab 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -18,7 +18,7 @@ serde = { workspace = true, optional = true } [dev-dependencies] libtest-mimic = { workspace = true } -[target.'cfg(target_os = "linux")'.dev-dependencies] +[target.'cfg(unix)'.dev-dependencies] libc = { workspace = true } [lints] diff --git a/crates/core/tests/mpk.rs b/crates/core/tests/mpk.rs index b349f1f10500..edb7172228fd 100644 --- a/crates/core/tests/mpk.rs +++ b/crates/core/tests/mpk.rs @@ -1,30 +1,25 @@ //! Checks that `mpk::is_supported` agrees with what the hardware and kernel //! actually allow. //! -//! We test that `RDPKRU`/`WRPKRU` work if and only if `mpk::is_supported` returns true. -//! The => direction is required to avoid hard to debug SIGILLs if our is_supported logic was wrong. -//! The other direction allows us to detect if we're missing out on using MPKs in places they are -//! actually allowed. +//! We test that `RDPKRU`/`WRPKRU` work only if `mpk::is_supported` returns true. +//! This is required to avoid hard to debug SIGILLs if our is_supported logic was wrong. //! -//! On miri we cannot spawn a subprocess at all. +//! On unsupported platforms we just test that is_supported is false. +//! There are platforms where MPK is supported by hardware and the OS (32-bit x86, FreeBSD, ARM targets), +//! but Wasmtime has no backend for that. use libtest_mimic::{Arguments, Failed, Trial}; -use std::arch::asm; use std::process::ExitStatus; -use std::process::{Command, exit}; +use std::process::{exit, Command}; /// Set in the child process to make it run the probe instead of the test. const PROBE_ENV_VAR: &str = "WASMTIME_TEST_MPK_PROBE"; // Exit codes of the probe, besides success. -const EXIT_PKEY_ALLOC_FAILED: i32 = 2; -const EXIT_PKEY_FREE_FAILED: i32 = 3; +pub const EXIT_PKEY_ALLOC_FAILED: i32 = 2; +pub const EXIT_PKEY_FREE_FAILED: i32 = 3; +pub const EXIT_MPK_NOT_SUPPORTED_BY_WASMTIME: i32 = 4; -// Skip the test on miri, still needs an empty main. -#[cfg(miri)] -fn main() {} - -#[cfg(not(miri))] fn main() { // We are the child that is supposed to perform the probe. if std::env::var_os(PROBE_ENV_VAR).is_some() { @@ -32,57 +27,59 @@ fn main() { // probe always exits, this is unreachable } // We are the test entrypoint. - libtest_mimic::run( - &Arguments::from_args(), - vec![Trial::test("is_supported_matches_hardware_and_os", || { - let output = Command::new(std::env::current_exe().unwrap()) - .env(PROBE_ENV_VAR, "1") - .output() - .expect("failed to spawn the probe process"); - let usable = if output.status.success() { - true - } else if is_illegal_instruction(&output.status) { - false - } else { - return Err(Failed::from(format!( - "inconclusive probe for protection key support: {}\n\ - stdout:\n{}\nstderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ))); - }; + let run_test = || { + let output = Command::new(std::env::current_exe().unwrap()) + .env(PROBE_ENV_VAR, "1") + .output() + .expect("failed to spawn the probe process"); + let usable = if output.status.success() { + true + } else if is_illegal_instruction(&output.status) + || is_not_supported_by_wasmtime(&output.status) + { + false + } else { + return Err(Failed::from(format!( + "inconclusive probe for protection key support: {}\n\ + stdout:\n{}\nstderr:\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ))); + }; - let is_supported = wasmtime_internal_core::mpk::is_supported(); - if is_supported != usable { - Err(Failed::from(format!( - "`mpk::is_supported()` disagrees with whether protection keys can \ - actually be used on this host: is_supported() = {is_supported}, usable = {usable}", - ))) - } else { - Ok(()) - } - })], - ) - .exit(); + let is_supported = wasmtime_internal_core::mpk::is_supported(); + if is_supported != usable { + Err(Failed::from(format!( + "`mpk::is_supported()` disagrees with whether protection keys can \ + actually be used on this host: is_supported() = {is_supported}, usable = {usable}", + ))) + } else { + Ok(()) + } + }; + let trial = Trial::test("is_supported_matches_hardware_and_os", run_test) + // Has to be excluded on miri as we can't even spawn the subcommand there. + .with_ignored_flag(cfg!(miri)); + libtest_mimic::run(&Arguments::from_args(), vec![trial]).exit(); } -#[cfg(unix)] -fn is_illegal_instruction(status: &ExitStatus) -> bool { - use std::os::unix::process::ExitStatusExt; - status.signal() == Some(libc::SIGILL) +fn is_not_supported_by_wasmtime(status: &ExitStatus) -> bool { + status + .code() + .is_some_and(|x| x == EXIT_MPK_NOT_SUPPORTED_BY_WASMTIME) } -#[cfg(windows)] fn is_illegal_instruction(status: &ExitStatus) -> bool { - // STATUS_ILLEGAL_INSTRUCTION - status.code().map(|c| c as u32) == Some(0xC000001D) + use std::os::unix::process::ExitStatusExt; + status.signal() == Some(libc::SIGILL) } /// Checks `RDPKRU`, `WRPKRU`, and tries to allocate and free a pkey. /// Exits the process with 0 if everything worked, raises `SIGILL` otherwise. -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", target_arch = "x86_64"))] fn probe() -> ! { + use std::arch::asm; use std::io; // No crate offers rdpkru or wrpkru as a Rust function (neither rustix nor libc), so we need to // rawdog asm. @@ -114,16 +111,12 @@ fn probe() -> ! { exit(0) } -/// On non-unix platforms the pkey_alloc syscall doesn't exist, so we can't compile the above. -/// However, rdpkru and wrpkru should kill us with SIGILL, since these OSs don't enable PKU at all. -#[cfg(not(target_os = "linux"))] +/// Some platforms like ARM or FreeBSD support MPKs, but with different APIs that wasmtime +/// currently does not hook into. +/// We just return a special error on those. +/// If wasmtime gets MPK support on those platforms, this should be updated to check the +/// capabilities of the relevant API. +#[cfg(not(all(target_os = "linux", target_arch = "x86_64")))] fn probe() -> ! { - let pkru: u32; - // SAFETY: reading PKRU has no side effects; if protection keys are not - // enabled this raises `SIGILL`, which is what the parent checks for. - unsafe { - asm!("rdpkru", in("ecx") 0u32, out("eax") pkru, out("edx") _, - options(nomem, nostack, preserves_flags)); - } - exit(0) + exit(EXIT_MPK_NOT_SUPPORTED_BY_WASMTIME) } From f1b38426a721673b5ed23b93c2c3ba98e9418b93 Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Tue, 27 Oct 2026 16:17:43 +0100 Subject: [PATCH 6/9] Reformat. --- crates/core/tests/mpk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/tests/mpk.rs b/crates/core/tests/mpk.rs index edb7172228fd..20b54bb09e62 100644 --- a/crates/core/tests/mpk.rs +++ b/crates/core/tests/mpk.rs @@ -10,7 +10,7 @@ use libtest_mimic::{Arguments, Failed, Trial}; use std::process::ExitStatus; -use std::process::{exit, Command}; +use std::process::{Command, exit}; /// Set in the child process to make it run the probe instead of the test. const PROBE_ENV_VAR: &str = "WASMTIME_TEST_MPK_PROBE"; From c49de8a50a47b6515ec211f96adb98d90a1378a8 Mon Sep 17 00:00:00 2001 From: Mat Gienieczko Date: Fri, 30 Oct 2026 21:57:25 +0100 Subject: [PATCH 7/9] Only refer to SIGILL on unix. The tests runs regardless of platform so it cannot have an unguarded `is_illegal_instruction`. --- crates/core/tests/mpk.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/core/tests/mpk.rs b/crates/core/tests/mpk.rs index 20b54bb09e62..92649899d87a 100644 --- a/crates/core/tests/mpk.rs +++ b/crates/core/tests/mpk.rs @@ -70,11 +70,17 @@ fn is_not_supported_by_wasmtime(status: &ExitStatus) -> bool { .is_some_and(|x| x == EXIT_MPK_NOT_SUPPORTED_BY_WASMTIME) } +#[cfg(unix)] fn is_illegal_instruction(status: &ExitStatus) -> bool { use std::os::unix::process::ExitStatusExt; status.signal() == Some(libc::SIGILL) } +#[cfg(not(unix))] +fn is_illegal_instruction(status: &ExitStatus) -> bool { + false +} + /// Checks `RDPKRU`, `WRPKRU`, and tries to allocate and free a pkey. /// Exits the process with 0 if everything worked, raises `SIGILL` otherwise. #[cfg(all(target_os = "linux", target_arch = "x86_64"))] From 95f0c6b893fab87db1f1f1bd8679913f3d2e4233 Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Wed, 4 Nov 2026 14:00:59 +0100 Subject: [PATCH 8/9] fix unused warning on tests in non-unix --- crates/core/tests/mpk.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/core/tests/mpk.rs b/crates/core/tests/mpk.rs index 92649899d87a..3ccafdf785e4 100644 --- a/crates/core/tests/mpk.rs +++ b/crates/core/tests/mpk.rs @@ -10,7 +10,7 @@ use libtest_mimic::{Arguments, Failed, Trial}; use std::process::ExitStatus; -use std::process::{Command, exit}; +use std::process::{exit, Command}; /// Set in the child process to make it run the probe instead of the test. const PROBE_ENV_VAR: &str = "WASMTIME_TEST_MPK_PROBE"; @@ -77,7 +77,7 @@ fn is_illegal_instruction(status: &ExitStatus) -> bool { } #[cfg(not(unix))] -fn is_illegal_instruction(status: &ExitStatus) -> bool { +fn is_illegal_instruction(_status: &ExitStatus) -> bool { false } From accc7ec95db6afb900689b906e015e52c24eb999 Mon Sep 17 00:00:00 2001 From: Mateusz Gienieczko Date: Wed, 4 Nov 2026 16:50:43 +0100 Subject: [PATCH 9/9] reformat --- crates/core/tests/mpk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/tests/mpk.rs b/crates/core/tests/mpk.rs index 3ccafdf785e4..a5f54f8e06b3 100644 --- a/crates/core/tests/mpk.rs +++ b/crates/core/tests/mpk.rs @@ -10,7 +10,7 @@ use libtest_mimic::{Arguments, Failed, Trial}; use std::process::ExitStatus; -use std::process::{exit, Command}; +use std::process::{Command, exit}; /// Set in the child process to make it run the probe instead of the test. const PROBE_ENV_VAR: &str = "WASMTIME_TEST_MPK_PROBE";