Skip to content

Commit

Permalink
pulley: Execute a wasm module under miri (#10096)
Browse files Browse the repository at this point in the history
* pulley: Execute a wasm module under miri

This commit adds a test to CI and a script locally to execute which will
run an entire wasm module under Pulley. The goal of this commit is to
add The Test for miri execution of wasm. In general miri is too slow to
run for the full test suite and even for this single test it takes a
very long time to compile the one small module here. To help with this
the module is precompiled on native for Pulley and then deserialized in
miri itself, meaning that we skip miri execution of Cranelift entirely.

The goal of this commit is to eventually expand this test to cover lots
of little and basic operations of wasm which touch VM state. For now
it's just a simple smoke test that doesn't run much but it will be
expanded over time. Making it much larger than now already turns up miri
violations so I wanted to land an initial scaffold first before
expanding later.

Getting this test to pass requires changing the `VmPtr<T>` introduced
in #10043 to use a `NonZeroUsize` internally rather than `NonNull<T>`.
This is because Pulley is only compatible with exposed provenance which
means we need to actually expose the provenance of pointers.

Both Pulley and Wasmtime need to deal with exposed provenance APIs, but
such APIs are not available in Wasmtime's current MSRV of 1.82. These
APIs were instead introduced as stable in Rust 1.84. In lieu of waiting
a few months because I'm impatient I've added a small build script to
both crates to detect the rustc version and see whether provenance APIs
are available. These build script modifications will no longer be
necessary once our MSRV is 1.84+.

prtest:miri

* Rejigger the CI matrix

* Don't hardcode toolchain in script
  • Loading branch information
alexcrichton authored Jan 23, 2025
1 parent 8a96989 commit 887e5c9
Show file tree
Hide file tree
Showing 13 changed files with 237 additions and 25 deletions.
19 changes: 12 additions & 7 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ jobs:
echo run-dwarf=true >> $GITHUB_OUTPUT
elif grep -q 'prtest:platform-checks' commits.log; then
echo platform-checks=true >> $GITHUB_OUTPUT
elif grep -q 'prtest:miri' commits.log; then
echo test-miri=true >> $GITHUB_OUTPUT
fi
if grep -q crates.c-api names.log; then
echo test-capi=true >> $GITHUB_OUTPUT
Expand Down Expand Up @@ -1030,17 +1032,19 @@ jobs:
miri:
strategy:
matrix:
crate:
- "wasmtime --features pulley"
- "wasmtime-cli"
- "wasmtime-environ --all-features"
- "pulley-interpreter --all-features"
include:
- crate: "wasmtime --features pulley"
- crate: "wasmtime-cli"
- crate: "wasmtime-environ --all-features"
- crate: "pulley-interpreter --all-features"
- script: ./ci/miri-provenance-test.sh
needs: determine
if: needs.determine.outputs.test-miri && github.repository == 'bytecodealliance/wasmtime'
name: Miri
runs-on: ubuntu-latest
env:
CARGO_NEXTEST_VERSION: 0.9.67
MIRIFLAGS: -Zmiri-permissive-provenance
steps:
- uses: actions/checkout@v4
with:
Expand All @@ -1057,8 +1061,9 @@ jobs:
- run: cargo install --root ${{ runner.tool_cache }}/cargo-nextest --version ${{ env.CARGO_NEXTEST_VERSION }} cargo-nextest --locked
- run: |
cargo miri nextest run -j4 --no-fail-fast -p ${{ matrix.crate }}
env:
MIRIFLAGS: -Zmiri-strict-provenance
if: ${{ matrix.crate }}
- run: ${{ matrix.script }}
if: ${{ matrix.script }}

# common logic to cancel the entire run if this job fails
- uses: ./.github/actions/cancel-on-failure
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ examples/.cache
*.smt2
cranelift/isle/veri/veri_engine/test_output
crates/explorer/node_modules
tests/all/pulley_provenance_test.cwasm
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ authors = ["The Wasmtime Project Developers"]
edition = "2021"
# Wasmtime's current policy is that this number can be no larger than the
# current stable release of Rust minus 2.
#
# NB: once this is 1.84+ delete `pulley/build.rs` and the similar code in
# `crate/wasmtime/build.rs`
rust-version = "1.82.0"

[workspace.lints.rust]
Expand Down
19 changes: 19 additions & 0 deletions ci/miri-provenance-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

# This is a small script to assist in running the `pulley_provenance_test` test
# located at `tests/all/pulley.rs`. The goal of this script is to use the native
# host to compile the wasm module in question to avoid needing to run Cranelift
# under MIRI. That enables much faster iteration on the test here.

set -ex

cargo run --no-default-features --features compile,pulley,wat,gc-drc,component-model \
compile --target pulley64 ./tests/all/pulley_provenance_test.wat \
-o tests/all/pulley_provenance_test.cwasm \
-O memory-reservation=$((1 << 20)) \
-O memory-guard-size=0 \
-O signals-based-traps=n

MIRIFLAGS="$MIRIFLAGS -Zmiri-disable-isolation -Zmiri-permissive-provenance" \
cargo miri test --test all -- \
--ignored pulley_provenance_test "$@"
28 changes: 28 additions & 0 deletions crates/wasmtime/build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use std::process::Command;
use std::str;

fn main() {
println!("cargo:rerun-if-changed=build.rs");

enable_features_based_on_rustc_version();

// NB: duplicating a workaround in the wasmtime-fiber build script.
println!("cargo:rustc-check-cfg=cfg(asan)");
if cfg_is("sanitize", "address") {
Expand Down Expand Up @@ -92,3 +97,26 @@ fn build_c_helpers() {
build.file("src/runtime/vm/helpers.c");
build.compile("wasmtime-helpers");
}

fn enable_features_based_on_rustc_version() {
// Temporary check to see if the rustc version >= 1.84 in which case
// provenance-related pointer APIs are available. This is temporary because
// in the future the MSRV of this crate will be beyond 1.84 in which case
// this build script can be deleted.
let minor = rustc_minor_version().unwrap_or(0);
if minor >= 84 {
println!("cargo:rustc-cfg=has_provenance_apis");
}
println!("cargo:rustc-check-cfg=cfg(has_provenance_apis)");
}

fn rustc_minor_version() -> Option<u32> {
let rustc = std::env::var("RUSTC").unwrap();
let output = Command::new(rustc).arg("--version").output().ok()?;
let version = str::from_utf8(&output.stdout).ok()?;
let mut pieces = version.split('.');
if pieces.next() != Some("rustc 1") {
return None;
}
pieces.next()?.parse().ok()
}
28 changes: 22 additions & 6 deletions crates/wasmtime/src/runtime/vm/provenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
use crate::vm::SendSyncPtr;
use core::fmt;
use core::marker;
use core::num::NonZeroUsize;
use core::ptr::NonNull;
use core::sync::atomic::{AtomicU64, AtomicUsize};
use wasmtime_environ::VMSharedTypeIndex;
Expand Down Expand Up @@ -84,7 +86,10 @@ use wasmtime_environ::VMSharedTypeIndex;
/// necessary when sharing data structures with compiled code. Prefer to use
/// `NonNull` or `SendSyncPtr` where possible.
#[repr(transparent)]
pub struct VmPtr<T>(SendSyncPtr<T>);
pub struct VmPtr<T> {
ptr: NonZeroUsize,
_marker: marker::PhantomData<SendSyncPtr<T>>,
}

impl<T> VmPtr<T> {
/// View this pointer as a [`SendSyncPtr<T>`].
Expand All @@ -97,17 +102,22 @@ impl<T> VmPtr<T> {
/// Later on this type will be handed back to Wasmtime or read from its
/// location at-rest in which case provenance will be "re-acquired".
pub fn as_send_sync(&self) -> SendSyncPtr<T> {
self.0
SendSyncPtr::from(self.as_non_null())
}

/// Similar to `as_send_sync`, but returns a `NonNull<T>`.
pub fn as_non_null(&self) -> NonNull<T> {
self.0.as_non_null()
#[cfg(has_provenance_apis)]
let ptr = core::ptr::with_exposed_provenance_mut(self.ptr.get());
#[cfg(not(has_provenance_apis))]
let ptr = self.ptr.get() as *mut T;

unsafe { NonNull::new_unchecked(ptr) }
}

/// Similar to `as_send_sync`, but returns a `*mut T`.
pub fn as_ptr(&self) -> *mut T {
self.0.as_ptr()
self.as_non_null().as_ptr()
}
}

Expand All @@ -130,14 +140,20 @@ impl<T> fmt::Debug for VmPtr<T> {
// Constructor from `NonNull<T>`
impl<T> From<NonNull<T>> for VmPtr<T> {
fn from(ptr: NonNull<T>) -> VmPtr<T> {
VmPtr::from(SendSyncPtr::from(ptr))
VmPtr {
#[cfg(has_provenance_apis)]
ptr: unsafe { NonZeroUsize::new_unchecked(ptr.as_ptr().expose_provenance()) },
#[cfg(not(has_provenance_apis))]
ptr: unsafe { NonZeroUsize::new_unchecked(ptr.as_ptr() as usize) },
_marker: marker::PhantomData,
}
}
}

// Constructor from `SendSyncPtr<T>`
impl<T> From<SendSyncPtr<T>> for VmPtr<T> {
fn from(ptr: SendSyncPtr<T>) -> VmPtr<T> {
VmPtr(ptr)
ptr.as_non_null().into()
}
}

Expand Down
28 changes: 23 additions & 5 deletions crates/wasmtime/src/runtime/vm/sys/miri/mmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ use crate::runtime::vm::sys::vm::MemoryImageSource;
use crate::runtime::vm::{HostAlignedByteCount, SendSyncPtr};
use std::alloc::{self, Layout};
use std::fs::File;
use std::io::Read;
use std::ops::Range;
use std::path::Path;
use std::ptr::NonNull;

pub fn open_file_for_mmap(_path: &Path) -> Result<File> {
bail!("not supported on miri");
pub fn open_file_for_mmap(path: &Path) -> Result<File> {
let file = File::open(path)?;
Ok(file)
}

#[derive(Debug)]
Expand All @@ -40,7 +42,10 @@ impl Mmap {
}

pub fn reserve(size: HostAlignedByteCount) -> Result<Self> {
if size.byte_count() > 1 << 32 {
// Miri will abort execution on OOM instead of returning null from
// `alloc::alloc` so detect "definitely too large" requests that the
// test suite does and fail accordingly.
if (size.byte_count() as u64) > 1 << 32 {
bail!("failed to allocate memory");
}
let layout = make_layout(size.byte_count());
Expand All @@ -54,8 +59,21 @@ impl Mmap {
Ok(Mmap { memory })
}

pub fn from_file(_file: &File) -> Result<Self> {
bail!("not supported on miri");
pub fn from_file(mut file: &File) -> Result<Self> {
// Read the file and copy it in to a fresh "mmap" to have allocation for
// an mmap only in one location.
let mut dst = Vec::new();
file.read_to_end(&mut dst)?;
let count = HostAlignedByteCount::new_rounded_up(dst.len())?;
let result = Mmap::new(count)?;
unsafe {
std::ptr::copy_nonoverlapping(
dst.as_ptr(),
result.as_send_sync_ptr().as_ptr(),
dst.len(),
);
}
Ok(result)
}

pub unsafe fn make_accessible(
Expand Down
1 change: 0 additions & 1 deletion pulley/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ workspace = true
arbitrary = { workspace = true, optional = true }
cranelift-bitset = { workspace = true }
log = { workspace = true }
sptr = { workspace = true }
wasmtime-math = { workspace = true, optional = true }
anyhow = { workspace = true, optional = true }

Expand Down
25 changes: 25 additions & 0 deletions pulley/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::process::Command;
use std::str;

fn main() {
// Temporary check to see if the rustc version >= 1.84 in which case
// provenance-related pointer APIs are available. This is temporary because
// in the future the MSRV of this crate will be beyond 1.84 in which case
// this build script can be deleted.
let minor = rustc_minor_version().unwrap_or(0);
if minor >= 84 {
println!("cargo:rustc-cfg=has_provenance_apis");
}
println!("cargo:rustc-check-cfg=cfg(has_provenance_apis)");
}

fn rustc_minor_version() -> Option<u32> {
let rustc = std::env::var("RUSTC").unwrap();
let output = Command::new(rustc).arg("--version").output().ok()?;
let version = str::from_utf8(&output.stdout).ok()?;
let mut pieces = version.split('.');
if pieces.next() != Some("rustc 1") {
return None;
}
pieces.next()?.parse().ok()
}
26 changes: 22 additions & 4 deletions pulley/src/interp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ use core::mem;
use core::ops::ControlFlow;
use core::ops::{Index, IndexMut};
use core::ptr::NonNull;
use sptr::Strict;
use wasmtime_math::WasmFloat;
mod debug;
#[cfg(all(not(pulley_tail_calls), not(pulley_assume_llvm_makes_tail_calls)))]
Expand Down Expand Up @@ -404,7 +403,18 @@ union XRegUnion {
u32: u32,
i64: i64,
u64: u64,
ptr: *mut u8,

// Note that this is intentionally `usize` and not an actual pointer like
// `*mut u8`. The reason for this is that provenance is required in Rust for
// pointers but Cranelift has no pointer type and thus no concept of
// provenance. That means that at-rest it's not known whether the value has
// provenance or not and basically means that Pulley is required to use
// "permissive provenance" in Rust as opposed to strict provenance.
//
// That's more-or-less a long-winded way of saying that storage of a pointer
// in this value is done with `.expose_provenance()` and reading a pointer
// uses `with_exposed_provenance_mut(..)`.
ptr: usize,
}

impl Default for XRegVal {
Expand Down Expand Up @@ -467,7 +477,11 @@ impl XRegVal {

pub fn get_ptr<T>(&self) -> *mut T {
let ptr = unsafe { self.0.ptr };
Strict::map_addr(ptr, |p| usize::from_le(p)).cast()
let ptr = usize::from_le(ptr);
#[cfg(has_provenance_apis)]
return core::ptr::with_exposed_provenance_mut(ptr);
#[cfg(not(has_provenance_apis))]
return ptr as *mut T;
}

pub fn set_i32(&mut self, x: i32) {
Expand All @@ -487,7 +501,11 @@ impl XRegVal {
}

pub fn set_ptr<T>(&mut self, ptr: *mut T) {
self.0.ptr = Strict::map_addr(ptr, |p| p.to_le()).cast();
#[cfg(has_provenance_apis)]
let ptr = ptr.expose_provenance();
#[cfg(not(has_provenance_apis))]
let ptr = ptr as usize;
self.0.ptr = ptr.to_le();
}
}

Expand Down
Loading

0 comments on commit 887e5c9

Please sign in to comment.