Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -1387,6 +1419,7 @@ jobs:
- special_tests
- test_wasi_tls
- clippy
- msrv_check
- monolith_checks
- platform_checks
- bench
Expand Down
121 changes: 93 additions & 28 deletions ci/build-test-matrix.js
Original file line number Diff line number Diff line change
Expand Up @@ -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" },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While I realize that Cargo has no way of splitting this up, personally I don't think this is viable to add to CI. If a new test is added to the wasmtime-cli crate it'll never get run on CI because it's not mentioned here, and we basically have no way of knowing that (it's pretty unlikely someone checks CI logs to ensure the test is being run).

I don't really know of a great answer for this otherwise. The only other thing I can think of is to move test suites around to keep the "single crate buckets" from before as still just crates. For example we could create a dedicated crate to just doing the *.wast tests and leave the all test where it is. Or... something like that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can use cargo metadata and parse the JSON output to get all test targets for a crate.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very good point, yes. I added a cargo metadata based attempt to address this over here.

],
},
"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';
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
{},
Expand All @@ -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;
Expand Down
17 changes: 17 additions & 0 deletions crates/core/examples/mpk-available.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
1 change: 1 addition & 0 deletions crates/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
44 changes: 44 additions & 0 deletions crates/core/src/mpk.rs
Original file line number Diff line number Diff line change
@@ -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")
}
2 changes: 1 addition & 1 deletion crates/wasmtime/src/runtime/vm/mpk/enabled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 0 additions & 26 deletions crates/wasmtime/src/runtime/vm/mpk/pkru.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;
Expand Down