diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 510965459e95..21079302a4d4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -493,6 +493,38 @@ jobs: - run: rustup component add clippy - run: cargo clippy --workspace --all-targets --features p3,component-model-async + # Verify that the workspace compiles cleanly under the MSRV toolchain. + # + # The excludes mirror `ci/run-tests.py` so that crates which require + # nightly toolchains or external dependencies (OCaml, SMT solvers) are + # skipped. + msrv_check: + name: MSRV check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + - uses: ./.github/actions/install-rust + with: + toolchain: msrv + - run: | + # wasi-preview1-component-adapter has mutually-exclusive features + # (command/reactor/proxy) that compile-error under --all-features. + # It's a wasm32-only artifact built separately by + # ci/build-wasi-preview1-component-adapter.sh against + # wasm32-unknown-unknown, not user-facing on the host toolchain, + # so MSRV doesn't apply. + cargo check --workspace --all-targets --all-features --locked \ + --exclude test-programs \ + --exclude wasmtime-wasi-nn \ + --exclude wasmtime-wasi-tls \ + --exclude wasmtime-fuzzing \ + --exclude wasm-spec-interpreter \ + --exclude veri_engine \ + --exclude calculator \ + --exclude wasi-preview1-component-adapter + # Similar to `micro_checks` but where we need to install some more state # (e.g. Android NDK) and we haven't factored support for those things out into # a parallel jobs yet. @@ -933,11 +965,11 @@ jobs: - name: Force-run with MPK enabled, if available if: ${{ contains(matrix.name, 'MPK') }} run: | - if cargo run --example mpk-available; then + if cargo run -q -p wasmtime-internal-core --example mpk-available; then echo "::notice::This CI run will force-enable MPK; this ensures tests conditioned with the \`WASMTIME_TEST_FORCE_MPK\` environment variable will run with MPK-protected memory pool stripes." echo WASMTIME_TEST_FORCE_MPK=1 >> $GITHUB_ENV else - echo "::warning::This CI run will not test MPK; it has been detected as not available on this machine (\`cargo run --example mpk-available\`)." + echo "::warning::This CI run will not test MPK; it has been detected as not available on this machine." fi # Install VTune, see `cli_tests::profile_with_vtune`. @@ -1387,6 +1419,7 @@ jobs: - special_tests - test_wasi_tls - clippy + - msrv_check - monolith_checks - platform_checks - bench diff --git a/ci/build-test-matrix.js b/ci/build-test-matrix.js index 4869be377dcc..bd37e596b625 100644 --- a/ci/build-test-matrix.js +++ b/ci/build-test-matrix.js @@ -13,7 +13,35 @@ const GENERIC_BUCKETS = 3; // Crates which are their own buckets. These are the very slowest to // compile-and-test crates. -const SINGLE_CRATE_BUCKETS = ["wasmtime", "wasmtime-cli", "wasmtime-wasi"]; +// +// An entry may be a string (the crate name, producing one bucket) or an +// object `{ crate, sub: [{ name, args }, ...] }` to split a single crate's +// tests across multiple buckets. Sub-bucketing is reserved for crates whose +// integration tests dominate the wall clock and split cleanly along +// `--test` boundaries; the build of the crate's dependencies and test +// binaries is duplicated across the sub-buckets, but the test-execution +// portion (the longer half on slow targets like QEMU) parallelizes. +const SINGLE_CRATE_BUCKETS = [ + "wasmtime", + { + "crate": "wasmtime-cli", + "sub": [ + // The two largest integration suites; each gets its own bucket. + { "name": "all", "args": "--test all" }, + { "name": "wast", "args": "--test wast" }, + // Everything else in the crate: library, binaries, and the smaller + // integration tests. Cargo doesn't have an "exclude test" flag, so the + // remaining `--test` targets are enumerated explicitly. + { "name": "other", "args": "--lib --bins --test disable_host_trap_handlers --test disas --test rlimited-memory --test wasi" }, + ], + }, + "wasmtime-wasi", +]; + +// Helper: get just the crate name from a SINGLE_CRATE_BUCKETS entry. +function singleBucketCrateName(entry) { + return typeof entry === "string" ? entry : entry.crate; +} const ubuntu = 'ubuntu-24.04'; const windows = 'windows-2025'; @@ -57,22 +85,29 @@ const FAST_MATRIX = [ // * `sde` - if `true`, indicates this test should use Intel SDE for instruction // emulation. SDE will be set up and configured as the test runner. // +// * `crates` - if a string, this config is not sharded across the workspace +// and instead runs against only the named crate. If an array of strings, +// the config produces one job per named crate (each job's name suffixed +// with the crate name) and skips the generic buckets entirely. Used to +// restrict env-var-toggled test variants to only the crates that observe +// that env var (e.g. MPK). +// // * `rust` - the Rust version to install, and if unset this'll be set to // `default` const FULL_MATRIX = [ ...FAST_MATRIX, { - "name": "Test MSRV", - "os": ubuntu, - "filter": "linux-x64", - "isa": "x64", - "rust": "msrv", - }, - { + // MPK is only observed at test time by code that reads + // WASMTIME_TEST_FORCE_MPK (the `wasmtime` runtime crate and the + // root-level integration tests under `wasmtime-cli`). Restricting MPK + // testing to those two crates eliminates four redundant shards + // (3 generic + wasmtime-wasi) that produce identical results to + // `Test Linux x86_64`. "name": "Test MPK", "os": ubuntu, "filter": "linux-x64", - "isa": "x64" + "isa": "x64", + "crates": ["wasmtime", "wasmtime-cli"], }, { "name": "Test ASAN", @@ -219,40 +254,72 @@ async function shard(configs) { // Divide the workspace crates into N disjoint subsets. Crates that are // particularly expensive to compile and test form their own singleton subset. + // A bucket is either a Set of crate names (used as-is for `cargo test + // --workspace --exclude ...`) or an object `{ crate, sub }` describing a + // single-crate bucket that should be further split by `--test` filter. + const singleBucketCrateNames = new Set(SINGLE_CRATE_BUCKETS.map(singleBucketCrateName)); const buckets = Array.from({ length: GENERIC_BUCKETS }, _ => new Set()); let i = 0; for (const crate of members) { - if (SINGLE_CRATE_BUCKETS.indexOf(crate) != -1) continue; + if (singleBucketCrateNames.has(crate)) continue; buckets[i].add(crate); i = (i + 1) % GENERIC_BUCKETS; } - for (crate of SINGLE_CRATE_BUCKETS) { - buckets.push(new Set([crate])); + for (const entry of SINGLE_CRATE_BUCKETS) { + if (typeof entry === "string") { + buckets.push(new Set([entry])); + } else { + // A crate with sub-buckets: push one bucket per `sub` entry, retaining + // the crate name and extra-args for naming and bucket-arg expansion. + for (const sub of entry.sub) { + buckets.push({ crate: entry.crate, name: sub.name, args: sub.args }); + } + } } // For each config, expand it into N configs, one for each disjoint set we // created above. const sharded = []; for (const config of configs) { - // If crates is specified, don't shard, just use the specified crates + // If `crates` is specified, don't shard against the generic buckets. + // A string value produces a single job for that crate; an array value + // produces one job per crate with the crate name appended to `name`. if (config.crates) { - sharded.push(Object.assign( - {}, - config, - { - bucket: members - .map(c => c === config.crates ? `--package ${c}` : `--exclude ${c}`) - .join(" ") - } - )); + const cratesList = Array.isArray(config.crates) ? config.crates : [config.crates]; + const useSuffix = Array.isArray(config.crates); + for (const crate of cratesList) { + sharded.push(Object.assign( + {}, + config, + { + name: useSuffix ? `${config.name} (${crate})` : config.name, + bucket: members + .map(c => c === crate ? `--package ${c}` : `--exclude ${c}`) + .join(" "), + } + )); + } continue; } let nbucket = 1; for (const bucket of buckets) { - let bucket_name = `${nbucket}/${buckets.length}`; - if (bucket.size === 1) - bucket_name = Array.from(bucket)[0]; + let bucket_name; + let bucket_args; + if (bucket instanceof Set) { + bucket_name = bucket.size === 1 + ? Array.from(bucket)[0] + : `${nbucket}/${buckets.length}`; + bucket_args = members + .map(c => bucket.has(c) ? `--package ${c}` : `--exclude ${c}`) + .join(" "); + } else { + // Sub-bucket of a single crate. + bucket_name = `${bucket.crate}-${bucket.name}`; + bucket_args = members + .map(c => c === bucket.crate ? `--package ${c}` : `--exclude ${c}`) + .join(" ") + " " + bucket.args; + } sharded.push(Object.assign( {}, @@ -262,9 +329,7 @@ async function shard(configs) { // We run tests via `cargo test --workspace`, so exclude crates that // aren't in this bucket, rather than naming only the crates that are // in this bucket. - bucket: members - .map(c => bucket.has(c) ? `--package ${c}` : `--exclude ${c}`) - .join(" "), + bucket: bucket_args, } )); nbucket += 1; diff --git a/crates/core/examples/mpk-available.rs b/crates/core/examples/mpk-available.rs new file mode 100644 index 000000000000..934f920691cb --- /dev/null +++ b/crates/core/examples/mpk-available.rs @@ -0,0 +1,17 @@ +//! Exit 0 if Wasmtime's MPK runtime support is available on this host, +//! 1 otherwise. This shares the detection logic with `wasmtime`'s runtime +//! `is_supported()` check via [`wasmtime_internal_core::mpk::is_supported`], +//! so CI's "should we set `WASMTIME_TEST_FORCE_MPK=1`?" decision can never +//! drift from what the runtime itself reports. + +use std::process::exit; + +fn main() { + if wasmtime_internal_core::mpk::is_supported() { + eprintln!("MPK is available"); + exit(0); + } else { + eprintln!("MPK is not available"); + exit(1); + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 0e55cba034bb..46ab22b02b5c 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -21,6 +21,7 @@ extern crate std; pub mod alloc; pub mod error; pub mod math; +pub mod mpk; pub mod non_max; pub mod slab; pub mod undo; diff --git a/crates/core/src/mpk.rs b/crates/core/src/mpk.rs new file mode 100644 index 000000000000..07e572195431 --- /dev/null +++ b/crates/core/src/mpk.rs @@ -0,0 +1,44 @@ +//! Detection of Memory Protection Keys (MPK) support on the host system. +//! +//! This is the single source of truth for whether Wasmtime's MPK +//! implementation can be used on the current target. The runtime in the +//! `wasmtime` crate consults [`is_supported`], and `examples/mpk-available.rs` +//! exposes it as a CLI exit code so CI can decide whether to set +//! `WASMTIME_TEST_FORCE_MPK=1`. + +/// 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() +} + +/// 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. +#[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 + } +} + +#[cfg(not(target_arch = "x86_64"))] +fn cpuid_pku_bit_set() -> 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/wasmtime/src/runtime/vm/mpk/enabled.rs b/crates/wasmtime/src/runtime/vm/mpk/enabled.rs index f4844a68d821..14ddba4d628f 100644 --- a/crates/wasmtime/src/runtime/vm/mpk/enabled.rs +++ b/crates/wasmtime/src/runtime/vm/mpk/enabled.rs @@ -6,7 +6,7 @@ use std::sync::OnceLock; /// Check if the MPK feature is supported. pub fn is_supported() -> bool { - cfg!(target_os = "linux") && cfg!(target_arch = "x86_64") && pkru::has_cpuid_bit_set() + wasmtime_core::mpk::is_supported() } /// Allocate up to `max` protection keys. diff --git a/crates/wasmtime/src/runtime/vm/mpk/pkru.rs b/crates/wasmtime/src/runtime/vm/mpk/pkru.rs index df901eb70225..50f1f69b1d47 100644 --- a/crates/wasmtime/src/runtime/vm/mpk/pkru.rs +++ b/crates/wasmtime/src/runtime/vm/mpk/pkru.rs @@ -55,32 +55,6 @@ pub fn write(pkru: u32) { } } -/// 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. -pub fn has_cpuid_bit_set() -> bool { - #[allow( - unused_unsafe, - reason = "rust is transitioning to `__cpuid` being a safe function" - )] - let result = unsafe { core::arch::x86_64::__cpuid(0x07) }; - is_intel_cpu() && (result.ecx & 0b1000) != 0 -} - -/// Check the `CPUID` vendor string for `GenuineIntel`; see the Intel Software -/// Development Manual, vol 2a, `CPUID` description. -pub fn is_intel_cpu() -> bool { - // To read the CPU vendor string, we pass 0 in EAX and read 12 ASCII bytes - // from EBX, EDX, and ECX (in that order). - #[allow(unused_unsafe, reason = "see above about __cpuid")] - let result = unsafe { core::arch::x86_64::__cpuid(0) }; - // Then we check if the vendor string matches "GenuineIntel". - result.ebx == u32::from_le_bytes(*b"Genu") - && result.edx == u32::from_le_bytes(*b"ineI") - && result.ecx == u32::from_le_bytes(*b"ntel") -} - #[cfg(test)] mod tests { use super::*;