feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
[package]
|
||||
name = "pezsc-executor-wasmtime"
|
||||
version = "0.29.0"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Defines a `WasmRuntime` that uses the Wasmtime JIT to execute."
|
||||
readme = "README.md"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
||||
[dependencies]
|
||||
log = { workspace = true, default-features = true }
|
||||
parking_lot = { workspace = true, default-features = true }
|
||||
|
||||
# When bumping wasmtime do not forget to also bump rustix
|
||||
# to exactly the same version as used by wasmtime!
|
||||
anyhow = { workspace = true }
|
||||
pezsc-allocator = { workspace = true, default-features = true }
|
||||
pezsc-executor-common = { workspace = true, default-features = true }
|
||||
pezsp-runtime-interface = { workspace = true, default-features = true }
|
||||
pezsp-wasm-interface = { features = [
|
||||
"wasmtime",
|
||||
], workspace = true, default-features = true }
|
||||
wasmtime = { features = [
|
||||
"addr2line",
|
||||
"cache",
|
||||
"cranelift",
|
||||
"demangle",
|
||||
"gc",
|
||||
"gc-null",
|
||||
"parallel-compilation",
|
||||
"pooling-allocator",
|
||||
"profiling",
|
||||
"threads",
|
||||
], workspace = true }
|
||||
|
||||
# Here we include the rustix crate in the exactly same semver-compatible version as used by
|
||||
# wasmtime and enable its 'use-libc' flag.
|
||||
#
|
||||
# By default rustix directly calls the appropriate syscalls completely bypassing libc;
|
||||
# this doesn't have any actual benefits for us besides making it harder to debug memory
|
||||
# problems (since then `mmap` etc. cannot be easily hooked into).
|
||||
rustix = { features = [
|
||||
"fs",
|
||||
"mm",
|
||||
"param",
|
||||
"std",
|
||||
"use-libc",
|
||||
], workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
cargo_metadata = { workspace = true }
|
||||
codec = { workspace = true, default-features = true }
|
||||
paste = { workspace = true, default-features = true }
|
||||
pezsc-runtime-test = { workspace = true }
|
||||
pezsp-io = { workspace = true, default-features = true }
|
||||
tempfile = { workspace = true }
|
||||
wat = { workspace = true }
|
||||
|
||||
[features]
|
||||
runtime-benchmarks = [
|
||||
"pezsc-runtime-test/runtime-benchmarks",
|
||||
"pezsp-io/runtime-benchmarks",
|
||||
"pezsp-runtime-interface/runtime-benchmarks",
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
License: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
@@ -0,0 +1,25 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use std::env;
|
||||
|
||||
fn main() {
|
||||
if let Ok(profile) = env::var("PROFILE") {
|
||||
println!("cargo:rustc-cfg=build_profile=\"{}\"", profile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! This module defines `HostState` and `HostContext` structs which provide logic and state
|
||||
//! required for execution of host.
|
||||
|
||||
use wasmtime::Caller;
|
||||
|
||||
use pezsc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
|
||||
use pezsp_wasm_interface::{Pointer, WordSize};
|
||||
|
||||
use crate::{instance_wrapper::MemoryWrapper, runtime::StoreData, util};
|
||||
|
||||
/// The state required to construct a HostContext context. The context only lasts for one host
|
||||
/// call, whereas the state is maintained for the duration of a Wasm runtime call, which may make
|
||||
/// many different host calls that must share state.
|
||||
pub struct HostState {
|
||||
/// The allocator instance to keep track of allocated memory.
|
||||
///
|
||||
/// This is stored as an `Option` as we need to temporarily set this to `None` when we are
|
||||
/// allocating/deallocating memory. The problem being that we can only mutable access `caller`
|
||||
/// once.
|
||||
allocator: Option<FreeingBumpHeapAllocator>,
|
||||
panic_message: Option<String>,
|
||||
}
|
||||
|
||||
impl HostState {
|
||||
/// Constructs a new `HostState`.
|
||||
pub fn new(allocator: FreeingBumpHeapAllocator) -> Self {
|
||||
HostState { allocator: Some(allocator), panic_message: None }
|
||||
}
|
||||
|
||||
/// Takes the error message out of the host state, leaving a `None` in its place.
|
||||
pub fn take_panic_message(&mut self) -> Option<String> {
|
||||
self.panic_message.take()
|
||||
}
|
||||
|
||||
pub(crate) fn allocation_stats(&self) -> AllocationStats {
|
||||
self.allocator.as_ref()
|
||||
.expect("Allocator is always set and only unavailable when doing an allocation/deallocation; qed")
|
||||
.stats()
|
||||
}
|
||||
}
|
||||
|
||||
/// A `HostContext` implements `FunctionContext` for making host calls from a Wasmtime
|
||||
/// runtime. The `HostContext` exists only for the lifetime of the call and borrows state from
|
||||
/// a longer-living `HostState`.
|
||||
pub(crate) struct HostContext<'a> {
|
||||
pub(crate) caller: Caller<'a, StoreData>,
|
||||
}
|
||||
|
||||
impl<'a> HostContext<'a> {
|
||||
fn host_state_mut(&mut self) -> &mut HostState {
|
||||
self.caller
|
||||
.data_mut()
|
||||
.host_state_mut()
|
||||
.expect("host state is not empty when calling a function in wasm; qed")
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> pezsp_wasm_interface::FunctionContext for HostContext<'a> {
|
||||
fn read_memory_into(
|
||||
&self,
|
||||
address: Pointer<u8>,
|
||||
dest: &mut [u8],
|
||||
) -> pezsp_wasm_interface::Result<()> {
|
||||
util::read_memory_into(&self.caller, address, dest).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn write_memory(&mut self, address: Pointer<u8>, data: &[u8]) -> pezsp_wasm_interface::Result<()> {
|
||||
util::write_memory_from(&mut self.caller, address, data).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn allocate_memory(&mut self, size: WordSize) -> pezsp_wasm_interface::Result<Pointer<u8>> {
|
||||
let memory = self.caller.data().memory();
|
||||
let mut allocator = self
|
||||
.host_state_mut()
|
||||
.allocator
|
||||
.take()
|
||||
.expect("allocator is not empty when calling a function in wasm; qed");
|
||||
|
||||
// We can not return on error early, as we need to store back allocator.
|
||||
let res = allocator
|
||||
.allocate(&mut MemoryWrapper(&memory, &mut self.caller), size)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
self.host_state_mut().allocator = Some(allocator);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn deallocate_memory(&mut self, ptr: Pointer<u8>) -> pezsp_wasm_interface::Result<()> {
|
||||
let memory = self.caller.data().memory();
|
||||
let mut allocator = self
|
||||
.host_state_mut()
|
||||
.allocator
|
||||
.take()
|
||||
.expect("allocator is not empty when calling a function in wasm; qed");
|
||||
|
||||
// We can not return on error early, as we need to store back allocator.
|
||||
let res = allocator
|
||||
.deallocate(&mut MemoryWrapper(&memory, &mut self.caller), ptr)
|
||||
.map_err(|e| e.to_string());
|
||||
|
||||
self.host_state_mut().allocator = Some(allocator);
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
fn register_panic_error_message(&mut self, message: &str) {
|
||||
self.host_state_mut().panic_message = Some(message.to_owned());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{host::HostContext, runtime::StoreData};
|
||||
use pezsc_executor_common::error::WasmError;
|
||||
use pezsp_wasm_interface::{FunctionContext, HostFunctions};
|
||||
use std::collections::HashMap;
|
||||
use wasmtime::{ExternType, FuncType, ImportType, Linker, Module};
|
||||
|
||||
/// Goes over all imports of a module and prepares the given linker for instantiation of the module.
|
||||
/// Returns an error if there are imports that cannot be satisfied.
|
||||
pub(crate) fn prepare_imports<H>(
|
||||
linker: &mut Linker<StoreData>,
|
||||
module: &Module,
|
||||
allow_missing_func_imports: bool,
|
||||
) -> Result<(), WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
let mut pending_func_imports = HashMap::new();
|
||||
for import_ty in module.imports() {
|
||||
let name = import_ty.name();
|
||||
|
||||
if import_ty.module() != "env" {
|
||||
return Err(WasmError::Other(format!(
|
||||
"host doesn't provide any imports from non-env module: {}:{}",
|
||||
import_ty.module(),
|
||||
name,
|
||||
)));
|
||||
}
|
||||
|
||||
match import_ty.ty() {
|
||||
ExternType::Func(func_ty) => {
|
||||
pending_func_imports.insert(name.to_owned(), (import_ty, func_ty));
|
||||
},
|
||||
_ =>
|
||||
return Err(WasmError::Other(format!(
|
||||
"host doesn't provide any non function imports: {}:{}",
|
||||
import_ty.module(),
|
||||
name,
|
||||
))),
|
||||
};
|
||||
}
|
||||
|
||||
let mut registry = Registry { linker, pending_func_imports };
|
||||
H::register_static(&mut registry)?;
|
||||
|
||||
if !registry.pending_func_imports.is_empty() {
|
||||
if allow_missing_func_imports {
|
||||
for (name, (import_ty, func_ty)) in registry.pending_func_imports {
|
||||
let error = format!("call to a missing function {}:{}", import_ty.module(), name);
|
||||
log::debug!("Missing import: '{}' {:?}", name, func_ty);
|
||||
linker
|
||||
.func_new("env", &name, func_ty.clone(), move |_, _, _| {
|
||||
Err(anyhow::Error::msg(error.clone()))
|
||||
})
|
||||
.expect("adding a missing import stub can only fail when the item already exists, and it is missing here; qed");
|
||||
}
|
||||
} else {
|
||||
let mut names = Vec::new();
|
||||
for (name, (import_ty, _)) in registry.pending_func_imports {
|
||||
names.push(format!("'{}:{}'", import_ty.module(), name));
|
||||
}
|
||||
let names = names.join(", ");
|
||||
return Err(WasmError::Other(format!(
|
||||
"runtime requires function imports which are not present on the host: {}",
|
||||
names
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct Registry<'a, 'b> {
|
||||
linker: &'a mut Linker<StoreData>,
|
||||
pending_func_imports: HashMap<String, (ImportType<'b>, FuncType)>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> pezsp_wasm_interface::HostFunctionRegistry for Registry<'a, 'b> {
|
||||
type State = StoreData;
|
||||
type Error = WasmError;
|
||||
type FunctionContext = HostContext<'a>;
|
||||
|
||||
fn with_function_context<R>(
|
||||
caller: wasmtime::Caller<Self::State>,
|
||||
callback: impl FnOnce(&mut dyn FunctionContext) -> R,
|
||||
) -> R {
|
||||
callback(&mut HostContext { caller })
|
||||
}
|
||||
|
||||
fn register_static<Params, Results>(
|
||||
&mut self,
|
||||
fn_name: &str,
|
||||
func: impl wasmtime::IntoFunc<Self::State, Params, Results>,
|
||||
) -> Result<(), Self::Error> {
|
||||
if self.pending_func_imports.remove(fn_name).is_some() {
|
||||
self.linker.func_wrap("env", fn_name, func).map_err(|error| {
|
||||
WasmError::Other(format!(
|
||||
"failed to register host function '{}' with the WASM linker: {:#}",
|
||||
fn_name, error
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Defines data and logic needed for interaction with an WebAssembly instance of a bizinikiwi
|
||||
//! runtime module.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::runtime::{InstanceCounter, ReleaseInstanceHandle, Store, StoreData};
|
||||
use pezsc_executor_common::error::{Backtrace, Error, MessageWithBacktrace, Result, WasmError};
|
||||
use pezsp_wasm_interface::{Pointer, WordSize};
|
||||
use wasmtime::{AsContext, AsContextMut, Engine, Instance, InstancePre, Memory};
|
||||
|
||||
/// Wasm blob entry point.
|
||||
pub struct EntryPoint(wasmtime::TypedFunc<(u32, u32), u64>);
|
||||
|
||||
impl EntryPoint {
|
||||
/// Call this entry point.
|
||||
pub(crate) fn call(
|
||||
&self,
|
||||
store: &mut Store,
|
||||
data_ptr: Pointer<u8>,
|
||||
data_len: WordSize,
|
||||
) -> Result<u64> {
|
||||
let data_ptr = u32::from(data_ptr);
|
||||
let data_len = u32::from(data_len);
|
||||
|
||||
self.0.call(&mut *store, (data_ptr, data_len)).map_err(|trap| {
|
||||
let host_state = store
|
||||
.data_mut()
|
||||
.host_state
|
||||
.as_mut()
|
||||
.expect("host state cannot be empty while a function is being called; qed");
|
||||
|
||||
let backtrace = trap.downcast_ref::<wasmtime::WasmBacktrace>().map(|backtrace| {
|
||||
// The logic to print out a backtrace is somewhat complicated,
|
||||
// so let's get wasmtime to print it out for us.
|
||||
Backtrace { backtrace_string: backtrace.to_string() }
|
||||
});
|
||||
|
||||
if let Some(message) = host_state.take_panic_message() {
|
||||
Error::AbortedDueToPanic(MessageWithBacktrace { message, backtrace })
|
||||
} else {
|
||||
let message = trap.root_cause().to_string();
|
||||
Error::AbortedDueToTrap(MessageWithBacktrace { message, backtrace })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn direct(
|
||||
func: wasmtime::Func,
|
||||
ctx: impl AsContext,
|
||||
) -> std::result::Result<Self, &'static str> {
|
||||
let entrypoint = func
|
||||
.typed::<(u32, u32), u64>(ctx)
|
||||
.map_err(|_| "Invalid signature for direct entry point")?;
|
||||
Ok(Self(entrypoint))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around [`Memory`] that implements [`pezsc_allocator::Memory`].
|
||||
pub(crate) struct MemoryWrapper<'a, C>(pub &'a wasmtime::Memory, pub &'a mut C);
|
||||
|
||||
impl<C: AsContextMut> pezsc_allocator::Memory for MemoryWrapper<'_, C> {
|
||||
fn with_access_mut<R>(&mut self, run: impl FnOnce(&mut [u8]) -> R) -> R {
|
||||
run(self.0.data_mut(&mut self.1))
|
||||
}
|
||||
|
||||
fn with_access<R>(&self, run: impl FnOnce(&[u8]) -> R) -> R {
|
||||
run(self.0.data(&self.1))
|
||||
}
|
||||
|
||||
fn grow(&mut self, additional: u32) -> std::result::Result<(), ()> {
|
||||
self.0
|
||||
.grow(&mut self.1, additional as u64)
|
||||
.map_err(|e| {
|
||||
log::error!(
|
||||
target: "wasm-executor",
|
||||
"Failed to grow memory by {} pages: {}",
|
||||
additional,
|
||||
e,
|
||||
)
|
||||
})
|
||||
.map(drop)
|
||||
}
|
||||
|
||||
fn pages(&self) -> u32 {
|
||||
self.0.size(&self.1) as u32
|
||||
}
|
||||
|
||||
fn max_pages(&self) -> Option<u32> {
|
||||
self.0.ty(&self.1).maximum().map(|p| p as _)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap the given WebAssembly Instance of a wasm module with Bizinikiwi-runtime.
|
||||
///
|
||||
/// This struct is a handy wrapper around a wasmtime `Instance` that provides bizinikiwi specific
|
||||
/// routines.
|
||||
pub struct InstanceWrapper {
|
||||
instance: Instance,
|
||||
store: Store,
|
||||
// NOTE: We want to decrement the instance counter *after* the store has been dropped
|
||||
// to avoid a potential race condition, so this field must always be kept
|
||||
// as the last field in the struct!
|
||||
_release_instance_handle: ReleaseInstanceHandle,
|
||||
}
|
||||
|
||||
impl InstanceWrapper {
|
||||
pub(crate) fn new(
|
||||
engine: &Engine,
|
||||
instance_pre: &InstancePre<StoreData>,
|
||||
instance_counter: Arc<InstanceCounter>,
|
||||
) -> Result<Self> {
|
||||
let _release_instance_handle = instance_counter.acquire_instance();
|
||||
let mut store = Store::new(engine, Default::default());
|
||||
let instance = instance_pre.instantiate(&mut store).map_err(|error| {
|
||||
WasmError::Other(format!(
|
||||
"failed to instantiate a new WASM module instance: {:#}",
|
||||
error,
|
||||
))
|
||||
})?;
|
||||
|
||||
let memory = get_linear_memory(&instance, &mut store)?;
|
||||
|
||||
store.data_mut().memory = Some(memory);
|
||||
|
||||
Ok(InstanceWrapper { instance, store, _release_instance_handle })
|
||||
}
|
||||
|
||||
/// Resolves a bizinikiwi entrypoint by the given name.
|
||||
///
|
||||
/// An entrypoint must have a signature `(i32, i32) -> i64`, otherwise this function will return
|
||||
/// an error.
|
||||
pub fn resolve_entrypoint(&mut self, method: &str) -> Result<EntryPoint> {
|
||||
// Resolve the requested method and verify that it has a proper signature.
|
||||
let export = self
|
||||
.instance
|
||||
.get_export(&mut self.store, method)
|
||||
.ok_or_else(|| Error::from(format!("Exported method {} is not found", method)))?;
|
||||
let func = export
|
||||
.into_func()
|
||||
.ok_or_else(|| Error::from(format!("Export {} is not a function", method)))?;
|
||||
EntryPoint::direct(func, &self.store).map_err(|_| {
|
||||
Error::from(format!("Exported function '{}' has invalid signature.", method))
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads `__heap_base: i32` global variable and returns it.
|
||||
///
|
||||
/// If it doesn't exist, not a global or of not i32 type returns an error.
|
||||
pub fn extract_heap_base(&mut self) -> Result<u32> {
|
||||
let heap_base_export = self
|
||||
.instance
|
||||
.get_export(&mut self.store, "__heap_base")
|
||||
.ok_or_else(|| Error::from("__heap_base is not found"))?;
|
||||
|
||||
let heap_base_global = heap_base_export
|
||||
.into_global()
|
||||
.ok_or_else(|| Error::from("__heap_base is not a global"))?;
|
||||
|
||||
let heap_base = heap_base_global
|
||||
.get(&mut self.store)
|
||||
.i32()
|
||||
.ok_or_else(|| Error::from("__heap_base is not a i32"))?;
|
||||
|
||||
Ok(heap_base as u32)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract linear memory instance from the given instance.
|
||||
fn get_linear_memory(instance: &Instance, ctx: impl AsContextMut) -> Result<Memory> {
|
||||
let memory_export = instance
|
||||
.get_export(ctx, "memory")
|
||||
.ok_or_else(|| Error::from("memory is not exported under `memory` name"))?;
|
||||
|
||||
let memory = memory_export
|
||||
.into_memory()
|
||||
.ok_or_else(|| Error::from("the `memory` export should have memory type"))?;
|
||||
|
||||
Ok(memory)
|
||||
}
|
||||
|
||||
/// Functions related to memory.
|
||||
impl InstanceWrapper {
|
||||
pub(crate) fn store(&self) -> &Store {
|
||||
&self.store
|
||||
}
|
||||
|
||||
pub(crate) fn store_mut(&mut self) -> &mut Store {
|
||||
&mut self.store
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Defines a `WasmRuntime` that uses the Wasmtime JIT to execute.
|
||||
//!
|
||||
//! You can choose a profiling strategy at runtime with
|
||||
//! environment variable `WASMTIME_PROFILING_STRATEGY`:
|
||||
//!
|
||||
//! | `WASMTIME_PROFILING_STRATEGY` | Effect |
|
||||
//! |-------------|-------------------------|
|
||||
//! | undefined | No profiling |
|
||||
//! | `"jitdump"` | jitdump profiling |
|
||||
//! | `"perfmap"` | perfmap profiling |
|
||||
//! | other value | No profiling (warning) |
|
||||
|
||||
mod host;
|
||||
mod imports;
|
||||
mod instance_wrapper;
|
||||
mod runtime;
|
||||
mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use runtime::{
|
||||
create_runtime, create_runtime_from_artifact, create_runtime_from_artifact_bytes,
|
||||
prepare_runtime_artifact, Config, DeterministicStackLimit, InstantiationStrategy, Semantics,
|
||||
WasmtimeRuntime,
|
||||
};
|
||||
pub use pezsc_executor_common::{
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmModule},
|
||||
};
|
||||
@@ -0,0 +1,726 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Defines the compiled Wasm runtime that uses Wasmtime internally.
|
||||
|
||||
use crate::{
|
||||
host::HostState,
|
||||
instance_wrapper::{EntryPoint, InstanceWrapper, MemoryWrapper},
|
||||
util::{self, replace_strategy_if_broken},
|
||||
};
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use pezsc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
|
||||
use pezsc_executor_common::{
|
||||
error::{Error, Result, WasmError},
|
||||
runtime_blob::RuntimeBlob,
|
||||
util::checked_range,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule},
|
||||
};
|
||||
use pezsp_runtime_interface::unpack_ptr_and_len;
|
||||
use pezsp_wasm_interface::{HostFunctions, Pointer, WordSize};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
use wasmtime::{AsContext, Cache, CacheConfig, Engine, Memory};
|
||||
|
||||
const MAX_INSTANCE_COUNT: u32 = 64;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct StoreData {
|
||||
/// This will only be set when we call into the runtime.
|
||||
pub(crate) host_state: Option<HostState>,
|
||||
/// This will be always set once the store is initialized.
|
||||
pub(crate) memory: Option<Memory>,
|
||||
}
|
||||
|
||||
impl StoreData {
|
||||
/// Returns a mutable reference to the host state.
|
||||
pub fn host_state_mut(&mut self) -> Option<&mut HostState> {
|
||||
self.host_state.as_mut()
|
||||
}
|
||||
|
||||
/// Returns the host memory.
|
||||
pub fn memory(&self) -> Memory {
|
||||
self.memory.expect("memory is always set; qed")
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type Store = wasmtime::Store<StoreData>;
|
||||
|
||||
enum Strategy {
|
||||
RecreateInstance(InstanceCreator),
|
||||
}
|
||||
|
||||
struct InstanceCreator {
|
||||
engine: Engine,
|
||||
instance_pre: Arc<wasmtime::InstancePre<StoreData>>,
|
||||
instance_counter: Arc<InstanceCounter>,
|
||||
}
|
||||
|
||||
impl InstanceCreator {
|
||||
fn instantiate(&mut self) -> Result<InstanceWrapper> {
|
||||
InstanceWrapper::new(&self.engine, &self.instance_pre, self.instance_counter.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle for releasing an instance acquired by [`InstanceCounter::acquire_instance`].
|
||||
pub(crate) struct ReleaseInstanceHandle {
|
||||
counter: Arc<InstanceCounter>,
|
||||
}
|
||||
|
||||
impl Drop for ReleaseInstanceHandle {
|
||||
fn drop(&mut self) {
|
||||
{
|
||||
let mut counter = self.counter.counter.lock();
|
||||
*counter = counter.saturating_sub(1);
|
||||
}
|
||||
|
||||
self.counter.wait_for_instance.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
/// Keeps track on the number of parallel instances.
|
||||
///
|
||||
/// The runtime cache keeps track on the number of parallel instances. The maximum number in the
|
||||
/// cache is less than what we have configured as [`MAX_INSTANCE_COUNT`] for wasmtime. However, the
|
||||
/// cache will create on demand instances if required. This instance counter will ensure that we are
|
||||
/// blocking when we are trying to create too many instances.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct InstanceCounter {
|
||||
counter: Mutex<u32>,
|
||||
wait_for_instance: parking_lot::Condvar,
|
||||
}
|
||||
|
||||
impl InstanceCounter {
|
||||
/// Acquire an instance.
|
||||
///
|
||||
/// Blocks if there is no free instance available.
|
||||
///
|
||||
/// The returned [`ReleaseInstanceHandle`] should be dropped when the instance isn't used
|
||||
/// anymore.
|
||||
pub fn acquire_instance(self: Arc<Self>) -> ReleaseInstanceHandle {
|
||||
let mut counter = self.counter.lock();
|
||||
|
||||
while *counter >= MAX_INSTANCE_COUNT {
|
||||
self.wait_for_instance.wait(&mut counter);
|
||||
}
|
||||
*counter += 1;
|
||||
|
||||
ReleaseInstanceHandle { counter: self.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
/// A `WasmModule` implementation using wasmtime to compile the runtime module to machine code
|
||||
/// and execute the compiled code.
|
||||
pub struct WasmtimeRuntime {
|
||||
engine: Engine,
|
||||
instance_pre: Arc<wasmtime::InstancePre<StoreData>>,
|
||||
instantiation_strategy: InternalInstantiationStrategy,
|
||||
instance_counter: Arc<InstanceCounter>,
|
||||
}
|
||||
|
||||
impl WasmModule for WasmtimeRuntime {
|
||||
fn new_instance(&self) -> Result<Box<dyn WasmInstance>> {
|
||||
let strategy = match self.instantiation_strategy {
|
||||
InternalInstantiationStrategy::Builtin => Strategy::RecreateInstance(InstanceCreator {
|
||||
engine: self.engine.clone(),
|
||||
instance_pre: self.instance_pre.clone(),
|
||||
instance_counter: self.instance_counter.clone(),
|
||||
}),
|
||||
};
|
||||
|
||||
Ok(Box::new(WasmtimeInstance { strategy }))
|
||||
}
|
||||
}
|
||||
|
||||
/// A `WasmInstance` implementation that reuses compiled module and spawns instances
|
||||
/// to execute the compiled code.
|
||||
pub struct WasmtimeInstance {
|
||||
strategy: Strategy,
|
||||
}
|
||||
|
||||
impl WasmtimeInstance {
|
||||
fn call_impl(
|
||||
&mut self,
|
||||
method: &str,
|
||||
data: &[u8],
|
||||
allocation_stats: &mut Option<AllocationStats>,
|
||||
) -> Result<Vec<u8>> {
|
||||
match &mut self.strategy {
|
||||
Strategy::RecreateInstance(ref mut instance_creator) => {
|
||||
let mut instance_wrapper = instance_creator.instantiate()?;
|
||||
let heap_base = instance_wrapper.extract_heap_base()?;
|
||||
let entrypoint = instance_wrapper.resolve_entrypoint(method)?;
|
||||
let allocator = FreeingBumpHeapAllocator::new(heap_base);
|
||||
|
||||
perform_call(data, &mut instance_wrapper, entrypoint, allocator, allocation_stats)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WasmInstance for WasmtimeInstance {
|
||||
fn call_with_allocation_stats(
|
||||
&mut self,
|
||||
method: &str,
|
||||
data: &[u8],
|
||||
) -> (Result<Vec<u8>>, Option<AllocationStats>) {
|
||||
let mut allocation_stats = None;
|
||||
let result = self.call_impl(method, data, &mut allocation_stats);
|
||||
(result, allocation_stats)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare a directory structure and a config file to enable wasmtime caching.
|
||||
///
|
||||
/// In case of an error the caching will not be enabled.
|
||||
fn setup_wasmtime_caching(
|
||||
cache_path: &Path,
|
||||
config: &mut wasmtime::Config,
|
||||
) -> std::result::Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
let wasmtime_cache_root = cache_path.join("wasmtime");
|
||||
fs::create_dir_all(&wasmtime_cache_root)
|
||||
.map_err(|err| format!("cannot create the dirs to cache: {}", err))?;
|
||||
|
||||
let mut cache_config = CacheConfig::new();
|
||||
cache_config.with_directory(cache_path);
|
||||
|
||||
let cache =
|
||||
Cache::new(cache_config).map_err(|err| format!("failed to initiate Cache: {err:?}"))?;
|
||||
|
||||
config.cache(Some(cache));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn common_config(semantics: &Semantics) -> std::result::Result<wasmtime::Config, WasmError> {
|
||||
let mut config = wasmtime::Config::new();
|
||||
config.cranelift_opt_level(wasmtime::OptLevel::SpeedAndSize);
|
||||
config.cranelift_nan_canonicalization(semantics.canonicalize_nans);
|
||||
|
||||
let profiler = match std::env::var_os("WASMTIME_PROFILING_STRATEGY") {
|
||||
Some(os_string) if os_string == "jitdump" => wasmtime::ProfilingStrategy::JitDump,
|
||||
Some(os_string) if os_string == "perfmap" => wasmtime::ProfilingStrategy::PerfMap,
|
||||
None => wasmtime::ProfilingStrategy::None,
|
||||
Some(_) => {
|
||||
// Remember if we have already logged a warning due to an unknown profiling strategy.
|
||||
static UNKNOWN_PROFILING_STRATEGY: AtomicBool = AtomicBool::new(false);
|
||||
// Make sure that the warning will not be relogged regularly.
|
||||
if !UNKNOWN_PROFILING_STRATEGY.swap(true, Ordering::Relaxed) {
|
||||
log::warn!("WASMTIME_PROFILING_STRATEGY is set to unknown value, ignored.");
|
||||
}
|
||||
wasmtime::ProfilingStrategy::None
|
||||
},
|
||||
};
|
||||
config.profiler(profiler);
|
||||
|
||||
let native_stack_max = match semantics.deterministic_stack_limit {
|
||||
Some(DeterministicStackLimit { native_stack_max, .. }) => native_stack_max,
|
||||
|
||||
// In `wasmtime` 0.35 the default stack size limit was changed from 1MB to 512KB.
|
||||
//
|
||||
// This broke at least one teyrchain which depended on the original 1MB limit,
|
||||
// so here we restore it to what it was originally.
|
||||
None => 1024 * 1024,
|
||||
};
|
||||
|
||||
config.max_wasm_stack(native_stack_max as usize);
|
||||
|
||||
config.parallel_compilation(semantics.parallel_compilation);
|
||||
|
||||
// Be clear and specific about the extensions we support. If an update brings new features
|
||||
// they should be introduced here as well.
|
||||
config.wasm_reference_types(semantics.wasm_reference_types);
|
||||
config.wasm_simd(semantics.wasm_simd);
|
||||
config.wasm_relaxed_simd(semantics.wasm_simd);
|
||||
config.wasm_bulk_memory(semantics.wasm_bulk_memory);
|
||||
config.wasm_multi_value(semantics.wasm_multi_value);
|
||||
config.wasm_multi_memory(false);
|
||||
config.wasm_threads(false);
|
||||
config.wasm_memory64(false);
|
||||
config.wasm_tail_call(false);
|
||||
config.wasm_extended_const(false);
|
||||
|
||||
let (use_pooling, use_cow) = match semantics.instantiation_strategy {
|
||||
InstantiationStrategy::PoolingCopyOnWrite => (true, true),
|
||||
InstantiationStrategy::Pooling => (true, false),
|
||||
InstantiationStrategy::RecreateInstanceCopyOnWrite => (false, true),
|
||||
InstantiationStrategy::RecreateInstance => (false, false),
|
||||
};
|
||||
|
||||
const WASM_PAGE_SIZE: u64 = 65536;
|
||||
|
||||
config.memory_init_cow(use_cow);
|
||||
config.memory_guaranteed_dense_image_size(match semantics.heap_alloc_strategy {
|
||||
HeapAllocStrategy::Dynamic { maximum_pages } =>
|
||||
maximum_pages.map(|p| p as u64 * WASM_PAGE_SIZE).unwrap_or(u64::MAX),
|
||||
HeapAllocStrategy::Static { .. } => u64::MAX,
|
||||
});
|
||||
|
||||
if use_pooling {
|
||||
const MAX_WASM_PAGES: u64 = 0x10000;
|
||||
|
||||
let memory_pages = match semantics.heap_alloc_strategy {
|
||||
HeapAllocStrategy::Dynamic { maximum_pages } =>
|
||||
maximum_pages.map(|p| p as u64).unwrap_or(MAX_WASM_PAGES),
|
||||
HeapAllocStrategy::Static { .. } => MAX_WASM_PAGES,
|
||||
};
|
||||
|
||||
let mut pooling_config = wasmtime::PoolingAllocationConfig::default();
|
||||
pooling_config
|
||||
.max_unused_warm_slots(4)
|
||||
// Pooling needs a bunch of hard limits to be set; if we go over
|
||||
// any of these then the instantiation will fail.
|
||||
//
|
||||
// Current minimum values for kusama (as of 2022-04-14):
|
||||
// size: 32384
|
||||
// table_elements: 1249
|
||||
// memory_pages: 2070
|
||||
.max_core_instance_size(512 * 1024)
|
||||
.table_elements(8192)
|
||||
.max_memory_size(memory_pages as usize * WASM_PAGE_SIZE as usize)
|
||||
.total_tables(MAX_INSTANCE_COUNT)
|
||||
.total_memories(MAX_INSTANCE_COUNT)
|
||||
// This determines how many instances of the module can be
|
||||
// instantiated in parallel from the same `Module`.
|
||||
.total_core_instances(MAX_INSTANCE_COUNT);
|
||||
|
||||
config.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling(pooling_config));
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Knobs for deterministic stack height limiting.
|
||||
///
|
||||
/// The WebAssembly standard defines a call/value stack but it doesn't say anything about its
|
||||
/// size except that it has to be finite. The implementations are free to choose their own notion
|
||||
/// of limit: some may count the number of calls or values, others would rely on the host machine
|
||||
/// stack and trap on reaching a guard page.
|
||||
///
|
||||
/// This obviously is a source of non-determinism during execution. This feature can be used
|
||||
/// to instrument the code so that it will count the depth of execution in some deterministic
|
||||
/// way (the machine stack limit should be so high that the deterministic limit always triggers
|
||||
/// first).
|
||||
///
|
||||
/// The deterministic stack height limiting feature allows to instrument the code so that it will
|
||||
/// count the number of items that may be on the stack. This counting will only act as an rough
|
||||
/// estimate of the actual stack limit in wasmtime. This is because wasmtime measures it's stack
|
||||
/// usage in bytes.
|
||||
///
|
||||
/// The actual number of bytes consumed by a function is not trivial to compute without going
|
||||
/// through full compilation. Therefore, it's expected that `native_stack_max` is greatly
|
||||
/// overestimated and thus never reached in practice. The stack overflow check introduced by the
|
||||
/// instrumentation and that relies on the logical item count should be reached first.
|
||||
///
|
||||
/// See [here][stack_height] for more details of the instrumentation
|
||||
///
|
||||
/// [stack_height]: https://github.com/paritytech/wasm-instrument/blob/master/src/stack_limiter/mod.rs
|
||||
#[derive(Clone)]
|
||||
pub struct DeterministicStackLimit {
|
||||
/// A number of logical "values" that can be pushed on the wasm stack. A trap will be triggered
|
||||
/// if exceeded.
|
||||
///
|
||||
/// A logical value is a local, an argument or a value pushed on operand stack.
|
||||
pub logical_max: u32,
|
||||
/// The maximum number of bytes for stack used by wasmtime JITed code.
|
||||
///
|
||||
/// It's not specified how much bytes will be consumed by a stack frame for a given wasm
|
||||
/// function after translation into machine code. It is also not quite trivial.
|
||||
///
|
||||
/// Therefore, this number should be chosen conservatively. It must be so large so that it can
|
||||
/// fit the [`logical_max`](Self::logical_max) logical values on the stack, according to the
|
||||
/// current instrumentation algorithm.
|
||||
///
|
||||
/// This value cannot be 0.
|
||||
pub native_stack_max: u32,
|
||||
}
|
||||
|
||||
/// The instantiation strategy to use for the WASM executor.
|
||||
///
|
||||
/// All of the CoW strategies (with `CopyOnWrite` suffix) are only supported when either:
|
||||
/// a) we're running on Linux,
|
||||
/// b) we're running on an Unix-like system and we're precompiling
|
||||
/// our module beforehand and instantiating from a file.
|
||||
///
|
||||
/// If the CoW variant of a strategy is unsupported the executor will
|
||||
/// fall back to the non-CoW equivalent.
|
||||
#[non_exhaustive]
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum InstantiationStrategy {
|
||||
/// Pool the instances to avoid initializing everything from scratch
|
||||
/// on each instantiation. Use copy-on-write memory when possible.
|
||||
///
|
||||
/// This is the fastest instantiation strategy.
|
||||
PoolingCopyOnWrite,
|
||||
|
||||
/// Recreate the instance from scratch on every instantiation.
|
||||
/// Use copy-on-write memory when possible.
|
||||
RecreateInstanceCopyOnWrite,
|
||||
|
||||
/// Pool the instances to avoid initializing everything from scratch
|
||||
/// on each instantiation.
|
||||
Pooling,
|
||||
|
||||
/// Recreate the instance from scratch on every instantiation. Very slow.
|
||||
RecreateInstance,
|
||||
}
|
||||
|
||||
enum InternalInstantiationStrategy {
|
||||
Builtin,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Semantics {
|
||||
/// The instantiation strategy to use.
|
||||
pub instantiation_strategy: InstantiationStrategy,
|
||||
|
||||
/// Specifying `Some` will enable deterministic stack height. That is, all executor
|
||||
/// invocations will reach stack overflow at the exactly same point across different wasmtime
|
||||
/// versions and architectures.
|
||||
///
|
||||
/// This is achieved by a combination of running an instrumentation pass on input code and
|
||||
/// configuring wasmtime accordingly.
|
||||
///
|
||||
/// Since this feature depends on instrumentation, it can be set only if runtime is
|
||||
/// instantiated using the runtime blob, e.g. using [`create_runtime`].
|
||||
// I.e. if [`CodeSupplyMode::Verbatim`] is used.
|
||||
pub deterministic_stack_limit: Option<DeterministicStackLimit>,
|
||||
|
||||
/// Controls whether wasmtime should compile floating point in a way that doesn't allow for
|
||||
/// non-determinism.
|
||||
///
|
||||
/// By default, the wasm spec allows some local non-determinism wrt. certain floating point
|
||||
/// operations. Specifically, those operations that are not defined to operate on bits (e.g.
|
||||
/// fneg) can produce NaN values. The exact bit pattern for those is not specified and may
|
||||
/// depend on the particular machine that executes wasmtime generated JITed machine code. That
|
||||
/// is a source of non-deterministic values.
|
||||
///
|
||||
/// The classical runtime environment for Bizinikiwi allowed it and punted this on the runtime
|
||||
/// developers. For PVFs, we want to ensure that execution is deterministic though. Therefore,
|
||||
/// for PVF execution this flag is meant to be turned on.
|
||||
pub canonicalize_nans: bool,
|
||||
|
||||
/// Configures wasmtime to use multiple threads for compiling.
|
||||
pub parallel_compilation: bool,
|
||||
|
||||
/// The heap allocation strategy to use.
|
||||
pub heap_alloc_strategy: HeapAllocStrategy,
|
||||
|
||||
/// Enables WASM Multi-Value proposal
|
||||
pub wasm_multi_value: bool,
|
||||
|
||||
/// Enables WASM Bulk Memory Operations proposal
|
||||
pub wasm_bulk_memory: bool,
|
||||
|
||||
/// Enables WASM Reference Types proposal
|
||||
pub wasm_reference_types: bool,
|
||||
|
||||
/// Enables WASM Fixed-Width SIMD proposal
|
||||
pub wasm_simd: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
/// The WebAssembly standard requires all imports of an instantiated module to be resolved,
|
||||
/// otherwise, the instantiation fails. If this option is set to `true`, then this behavior is
|
||||
/// overridden and imports that are requested by the module and not provided by the host
|
||||
/// functions will be resolved using stubs. These stubs will trap upon a call.
|
||||
pub allow_missing_func_imports: bool,
|
||||
|
||||
/// A directory in which wasmtime can store its compiled artifacts cache.
|
||||
pub cache_path: Option<PathBuf>,
|
||||
|
||||
/// Tuning of various semantics of the wasmtime executor.
|
||||
pub semantics: Semantics,
|
||||
}
|
||||
|
||||
enum CodeSupplyMode<'a> {
|
||||
/// The runtime is instantiated using the given runtime blob.
|
||||
Fresh(RuntimeBlob),
|
||||
|
||||
/// The runtime is instantiated using a precompiled module at the given path.
|
||||
///
|
||||
/// This assumes that the code is already prepared for execution and the same `Config` was
|
||||
/// used.
|
||||
///
|
||||
/// We use a `Path` here instead of simply passing a byte slice to allow `wasmtime` to
|
||||
/// map the runtime's linear memory on supported platforms in a copy-on-write fashion.
|
||||
Precompiled(&'a Path),
|
||||
|
||||
/// The runtime is instantiated using a precompiled module with the given bytes.
|
||||
///
|
||||
/// This assumes that the code is already prepared for execution and the same `Config` was
|
||||
/// used.
|
||||
PrecompiledBytes(&'a [u8]),
|
||||
}
|
||||
|
||||
/// Create a new `WasmtimeRuntime` given the code. This function performs translation from Wasm to
|
||||
/// machine code, which can be computationally heavy.
|
||||
///
|
||||
/// The `H` generic parameter is used to statically pass a set of host functions which are exposed
|
||||
/// to the runtime.
|
||||
pub fn create_runtime<H>(
|
||||
blob: RuntimeBlob,
|
||||
config: Config,
|
||||
) -> std::result::Result<WasmtimeRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
// SAFETY: this is safe because it doesn't use `CodeSupplyMode::Precompiled`.
|
||||
unsafe { do_create_runtime::<H>(CodeSupplyMode::Fresh(blob), config) }
|
||||
}
|
||||
|
||||
/// The same as [`create_runtime`] but takes a path to a precompiled artifact,
|
||||
/// which makes this function considerably faster than [`create_runtime`].
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the compiled artifact passed here was:
|
||||
/// 1) produced by [`prepare_runtime_artifact`],
|
||||
/// 2) written to the disk as a file,
|
||||
/// 3) was not modified,
|
||||
/// 4) will not be modified while any runtime using this artifact is alive, or is being
|
||||
/// instantiated.
|
||||
///
|
||||
/// Failure to adhere to these requirements might lead to crashes and arbitrary code execution.
|
||||
///
|
||||
/// It is ok though if the compiled artifact was created by code of another version or with
|
||||
/// different configuration flags. In such case the caller will receive an `Err` deterministically.
|
||||
pub unsafe fn create_runtime_from_artifact<H>(
|
||||
compiled_artifact_path: &Path,
|
||||
config: Config,
|
||||
) -> std::result::Result<WasmtimeRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
do_create_runtime::<H>(CodeSupplyMode::Precompiled(compiled_artifact_path), config)
|
||||
}
|
||||
|
||||
/// The same as [`create_runtime`] but takes the bytes of a precompiled artifact,
|
||||
/// which makes this function considerably faster than [`create_runtime`],
|
||||
/// but slower than the more optimized [`create_runtime_from_artifact`].
|
||||
/// This is especially slow on non-Linux Unix systems. Useful in very niche cases.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// The caller must ensure that the compiled artifact passed here was:
|
||||
/// 1) produced by [`prepare_runtime_artifact`],
|
||||
/// 2) was not modified,
|
||||
///
|
||||
/// Failure to adhere to these requirements might lead to crashes and arbitrary code execution.
|
||||
///
|
||||
/// It is ok though if the compiled artifact was created by code of another version or with
|
||||
/// different configuration flags. In such case the caller will receive an `Err` deterministically.
|
||||
pub unsafe fn create_runtime_from_artifact_bytes<H>(
|
||||
compiled_artifact_bytes: &[u8],
|
||||
config: Config,
|
||||
) -> std::result::Result<WasmtimeRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
do_create_runtime::<H>(CodeSupplyMode::PrecompiledBytes(compiled_artifact_bytes), config)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// This is only unsafe if called with [`CodeSupplyMode::Artifact`]. See
|
||||
/// [`create_runtime_from_artifact`] to get more details.
|
||||
unsafe fn do_create_runtime<H>(
|
||||
code_supply_mode: CodeSupplyMode<'_>,
|
||||
mut config: Config,
|
||||
) -> std::result::Result<WasmtimeRuntime, WasmError>
|
||||
where
|
||||
H: HostFunctions,
|
||||
{
|
||||
replace_strategy_if_broken(&mut config.semantics.instantiation_strategy);
|
||||
|
||||
let mut wasmtime_config = common_config(&config.semantics)?;
|
||||
if let Some(ref cache_path) = config.cache_path {
|
||||
if let Err(reason) = setup_wasmtime_caching(cache_path, &mut wasmtime_config) {
|
||||
log::warn!(
|
||||
"failed to setup wasmtime cache. Performance may degrade significantly: {}.",
|
||||
reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let engine = Engine::new(&wasmtime_config)
|
||||
.map_err(|e| WasmError::Other(format!("cannot create the wasmtime engine: {:#}", e)))?;
|
||||
|
||||
let (module, instantiation_strategy) = match code_supply_mode {
|
||||
CodeSupplyMode::Fresh(blob) => {
|
||||
let blob = prepare_blob_for_compilation(blob, &config.semantics)?;
|
||||
let serialized_blob = blob.clone().serialize();
|
||||
|
||||
let module = wasmtime::Module::new(&engine, &serialized_blob)
|
||||
.map_err(|e| WasmError::Other(format!("cannot create module: {:#}", e)))?;
|
||||
|
||||
match config.semantics.instantiation_strategy {
|
||||
InstantiationStrategy::Pooling |
|
||||
InstantiationStrategy::PoolingCopyOnWrite |
|
||||
InstantiationStrategy::RecreateInstance |
|
||||
InstantiationStrategy::RecreateInstanceCopyOnWrite =>
|
||||
(module, InternalInstantiationStrategy::Builtin),
|
||||
}
|
||||
},
|
||||
CodeSupplyMode::Precompiled(compiled_artifact_path) => {
|
||||
// SAFETY: The unsafety of `deserialize_file` is covered by this function. The
|
||||
// responsibilities to maintain the invariants are passed to the caller.
|
||||
//
|
||||
// See [`create_runtime_from_artifact`] for more details.
|
||||
let module = wasmtime::Module::deserialize_file(&engine, compiled_artifact_path)
|
||||
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:#}", e)))?;
|
||||
|
||||
(module, InternalInstantiationStrategy::Builtin)
|
||||
},
|
||||
CodeSupplyMode::PrecompiledBytes(compiled_artifact_bytes) => {
|
||||
// SAFETY: The unsafety of `deserialize` is covered by this function. The
|
||||
// responsibilities to maintain the invariants are passed to the caller.
|
||||
//
|
||||
// See [`create_runtime_from_artifact_bytes`] for more details.
|
||||
let module = wasmtime::Module::deserialize(&engine, compiled_artifact_bytes)
|
||||
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:#}", e)))?;
|
||||
|
||||
(module, InternalInstantiationStrategy::Builtin)
|
||||
},
|
||||
};
|
||||
|
||||
let mut linker = wasmtime::Linker::new(&engine);
|
||||
crate::imports::prepare_imports::<H>(&mut linker, &module, config.allow_missing_func_imports)?;
|
||||
|
||||
let instance_pre = linker
|
||||
.instantiate_pre(&module)
|
||||
.map_err(|e| WasmError::Other(format!("cannot preinstantiate module: {:#}", e)))?;
|
||||
|
||||
Ok(WasmtimeRuntime {
|
||||
engine,
|
||||
instance_pre: Arc::new(instance_pre),
|
||||
instantiation_strategy,
|
||||
instance_counter: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn prepare_blob_for_compilation(
|
||||
mut blob: RuntimeBlob,
|
||||
semantics: &Semantics,
|
||||
) -> std::result::Result<RuntimeBlob, WasmError> {
|
||||
if let Some(DeterministicStackLimit { logical_max, .. }) = semantics.deterministic_stack_limit {
|
||||
blob = blob.inject_stack_depth_metering(logical_max)?;
|
||||
}
|
||||
|
||||
// We don't actually need the memory to be imported so we can just convert any memory
|
||||
// import into an export with impunity. This simplifies our code since `wasmtime` will
|
||||
// now automatically take care of creating the memory for us, and it is also necessary
|
||||
// to enable `wasmtime`'s instance pooling. (Imported memories are ineligible for pooling.)
|
||||
blob.convert_memory_import_into_export()?;
|
||||
blob.setup_memory_according_to_heap_alloc_strategy(semantics.heap_alloc_strategy)?;
|
||||
|
||||
Ok(blob)
|
||||
}
|
||||
|
||||
/// Takes a [`RuntimeBlob`] and precompiles it returning the serialized result of compilation. It
|
||||
/// can then be used for calling [`create_runtime`] avoiding long compilation times.
|
||||
pub fn prepare_runtime_artifact(
|
||||
blob: RuntimeBlob,
|
||||
semantics: &Semantics,
|
||||
) -> std::result::Result<Vec<u8>, WasmError> {
|
||||
let mut semantics = semantics.clone();
|
||||
replace_strategy_if_broken(&mut semantics.instantiation_strategy);
|
||||
|
||||
let blob = prepare_blob_for_compilation(blob, &semantics)?;
|
||||
|
||||
let engine = Engine::new(&common_config(&semantics)?)
|
||||
.map_err(|e| WasmError::Other(format!("cannot create the engine: {:#}", e)))?;
|
||||
|
||||
engine
|
||||
.precompile_module(&blob.serialize())
|
||||
.map_err(|e| WasmError::Other(format!("cannot precompile module: {:#}", e)))
|
||||
}
|
||||
|
||||
fn perform_call(
|
||||
data: &[u8],
|
||||
instance_wrapper: &mut InstanceWrapper,
|
||||
entrypoint: EntryPoint,
|
||||
mut allocator: FreeingBumpHeapAllocator,
|
||||
allocation_stats: &mut Option<AllocationStats>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let (data_ptr, data_len) = inject_input_data(instance_wrapper, &mut allocator, data)?;
|
||||
|
||||
let host_state = HostState::new(allocator);
|
||||
|
||||
// Set the host state before calling into wasm.
|
||||
instance_wrapper.store_mut().data_mut().host_state = Some(host_state);
|
||||
|
||||
let ret = entrypoint
|
||||
.call(instance_wrapper.store_mut(), data_ptr, data_len)
|
||||
.map(unpack_ptr_and_len);
|
||||
|
||||
// Reset the host state
|
||||
let host_state = instance_wrapper.store_mut().data_mut().host_state.take().expect(
|
||||
"the host state is always set before calling into WASM so it can't be None here; qed",
|
||||
);
|
||||
*allocation_stats = Some(host_state.allocation_stats());
|
||||
|
||||
let (output_ptr, output_len) = ret?;
|
||||
let output = extract_output_data(instance_wrapper, output_ptr, output_len)?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn inject_input_data(
|
||||
instance: &mut InstanceWrapper,
|
||||
allocator: &mut FreeingBumpHeapAllocator,
|
||||
data: &[u8],
|
||||
) -> Result<(Pointer<u8>, WordSize)> {
|
||||
let mut ctx = instance.store_mut();
|
||||
let memory = ctx.data().memory();
|
||||
let data_len = data.len() as WordSize;
|
||||
let data_ptr = allocator.allocate(&mut MemoryWrapper(&memory, &mut ctx), data_len)?;
|
||||
util::write_memory_from(instance.store_mut(), data_ptr, data)?;
|
||||
Ok((data_ptr, data_len))
|
||||
}
|
||||
|
||||
fn extract_output_data(
|
||||
instance: &InstanceWrapper,
|
||||
output_ptr: u32,
|
||||
output_len: u32,
|
||||
) -> Result<Vec<u8>> {
|
||||
let ctx = instance.store();
|
||||
|
||||
// Do a length check before allocating. The returned output should not be bigger than the
|
||||
// available WASM memory. Otherwise, a malicious teyrchain can trigger a large allocation,
|
||||
// potentially causing memory exhaustion.
|
||||
//
|
||||
// Get the size of the WASM memory in bytes.
|
||||
let memory_size = ctx.as_context().data().memory().data_size(ctx);
|
||||
if checked_range(output_ptr as usize, output_len as usize, memory_size).is_none() {
|
||||
Err(Error::OutputExceedsBounds)?
|
||||
}
|
||||
let mut output = vec![0; output_len as usize];
|
||||
|
||||
util::read_memory_into(ctx, Pointer::new(output_ptr), &mut output)?;
|
||||
Ok(output)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,522 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use codec::{Decode as _, Encode as _};
|
||||
use pezsc_executor_common::{
|
||||
error::Error,
|
||||
runtime_blob::RuntimeBlob,
|
||||
wasm_runtime::{HeapAllocStrategy, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY},
|
||||
};
|
||||
use pezsc_runtime_test::wasm_binary_unwrap;
|
||||
|
||||
use crate::InstantiationStrategy;
|
||||
|
||||
type HostFunctions = pezsp_io::BizinikiwiHostFunctions;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! test_wasm_execution {
|
||||
($method_name:ident) => {
|
||||
paste::item! {
|
||||
#[test]
|
||||
fn [<$method_name _recreate_instance_cow>]() {
|
||||
$method_name(
|
||||
InstantiationStrategy::RecreateInstanceCopyOnWrite
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _recreate_instance_vanilla>]() {
|
||||
$method_name(
|
||||
InstantiationStrategy::RecreateInstance
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _pooling_cow>]() {
|
||||
$method_name(
|
||||
InstantiationStrategy::PoolingCopyOnWrite
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn [<$method_name _pooling_vanilla>]() {
|
||||
$method_name(
|
||||
InstantiationStrategy::Pooling
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct RuntimeBuilder {
|
||||
code: Option<String>,
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
canonicalize_nans: bool,
|
||||
deterministic_stack: bool,
|
||||
heap_pages: HeapAllocStrategy,
|
||||
precompile_runtime: bool,
|
||||
tmpdir: Option<tempfile::TempDir>,
|
||||
}
|
||||
|
||||
impl RuntimeBuilder {
|
||||
fn new(instantiation_strategy: InstantiationStrategy) -> Self {
|
||||
Self {
|
||||
code: None,
|
||||
instantiation_strategy,
|
||||
canonicalize_nans: false,
|
||||
deterministic_stack: false,
|
||||
heap_pages: DEFAULT_HEAP_ALLOC_STRATEGY,
|
||||
precompile_runtime: false,
|
||||
tmpdir: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn use_wat(mut self, code: String) -> Self {
|
||||
self.code = Some(code);
|
||||
self
|
||||
}
|
||||
|
||||
fn canonicalize_nans(mut self, canonicalize_nans: bool) -> Self {
|
||||
self.canonicalize_nans = canonicalize_nans;
|
||||
self
|
||||
}
|
||||
|
||||
fn deterministic_stack(mut self, deterministic_stack: bool) -> Self {
|
||||
self.deterministic_stack = deterministic_stack;
|
||||
self
|
||||
}
|
||||
|
||||
fn precompile_runtime(mut self, precompile_runtime: bool) -> Self {
|
||||
self.precompile_runtime = precompile_runtime;
|
||||
self
|
||||
}
|
||||
|
||||
fn heap_alloc_strategy(mut self, heap_pages: HeapAllocStrategy) -> Self {
|
||||
self.heap_pages = heap_pages;
|
||||
self
|
||||
}
|
||||
|
||||
fn build(&mut self) -> impl WasmModule + '_ {
|
||||
let blob = {
|
||||
let wasm: Vec<u8>;
|
||||
|
||||
let wasm = match self.code {
|
||||
None => wasm_binary_unwrap(),
|
||||
Some(ref wat) => {
|
||||
wasm = wat::parse_str(wat).expect("wat parsing failed");
|
||||
&wasm
|
||||
},
|
||||
};
|
||||
|
||||
RuntimeBlob::uncompress_if_needed(&wasm)
|
||||
.expect("failed to create a runtime blob out of test runtime")
|
||||
};
|
||||
|
||||
let config = crate::Config {
|
||||
allow_missing_func_imports: true,
|
||||
cache_path: None,
|
||||
semantics: crate::Semantics {
|
||||
instantiation_strategy: self.instantiation_strategy,
|
||||
deterministic_stack_limit: match self.deterministic_stack {
|
||||
true => Some(crate::DeterministicStackLimit {
|
||||
logical_max: 65536,
|
||||
native_stack_max: 256 * 1024 * 1024,
|
||||
}),
|
||||
false => None,
|
||||
},
|
||||
canonicalize_nans: self.canonicalize_nans,
|
||||
parallel_compilation: true,
|
||||
heap_alloc_strategy: self.heap_pages,
|
||||
wasm_multi_value: false,
|
||||
wasm_bulk_memory: false,
|
||||
wasm_reference_types: false,
|
||||
wasm_simd: false,
|
||||
},
|
||||
};
|
||||
|
||||
if self.precompile_runtime {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("runtime.bin");
|
||||
|
||||
// Delay the removal of the temporary directory until we're dropped.
|
||||
self.tmpdir = Some(dir);
|
||||
|
||||
let artifact = crate::prepare_runtime_artifact(blob, &config.semantics).unwrap();
|
||||
std::fs::write(&path, artifact).unwrap();
|
||||
unsafe { crate::create_runtime_from_artifact::<HostFunctions>(&path, config) }
|
||||
} else {
|
||||
crate::create_runtime::<HostFunctions>(blob, config)
|
||||
}
|
||||
.expect("cannot create runtime")
|
||||
}
|
||||
}
|
||||
|
||||
fn deep_call_stack_wat(depth: usize) -> String {
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
(memory $0 32)
|
||||
(export "memory" (memory $0))
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "overflow") call 0)
|
||||
|
||||
(func $overflow (param $0 i32)
|
||||
(block $label$1
|
||||
(br_if $label$1
|
||||
(i32.ge_u
|
||||
(local.get $0)
|
||||
(i32.const {depth})
|
||||
)
|
||||
)
|
||||
(call $overflow
|
||||
(i32.add
|
||||
(local.get $0)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
(call $overflow (i32.const 0))
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
// These two tests ensure that the `wasmtime`'s stack size limit and the amount of
|
||||
// stack space used by a single stack frame doesn't suddenly change without us noticing.
|
||||
//
|
||||
// If they do (e.g. because we've pulled in a new version of `wasmtime`) we want to know
|
||||
// that it did, regardless of how small the change was.
|
||||
//
|
||||
// If these tests starting failing it doesn't necessarily mean that something is broken;
|
||||
// what it means is that one (or multiple) of the following has to be done:
|
||||
// a) the tests may need to be updated for the new call depth,
|
||||
// b) the stack limit may need to be changed to maintain backwards compatibility,
|
||||
// c) the root cause of the new call depth limit determined, and potentially fixed,
|
||||
// d) the new call depth limit may need to be validated to ensure it doesn't prevent any
|
||||
// existing chain from syncing (if it was effectively decreased)
|
||||
|
||||
// We need two limits here since depending on whether the code is compiled in debug
|
||||
// or in release mode the maximum call depth is slightly different.
|
||||
const CALL_DEPTH_LOWER_LIMIT: usize = 65451;
|
||||
const CALL_DEPTH_UPPER_LIMIT: usize = 65506;
|
||||
|
||||
test_wasm_execution!(test_consume_under_1mb_of_stack_does_not_trap);
|
||||
fn test_consume_under_1mb_of_stack_does_not_trap(instantiation_strategy: InstantiationStrategy) {
|
||||
let wat = deep_call_stack_wat(CALL_DEPTH_LOWER_LIMIT);
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy).use_wat(wat);
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance().expect("failed to instantiate a runtime");
|
||||
instance.call_export("main", &[]).unwrap();
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_consume_over_1mb_of_stack_does_trap);
|
||||
fn test_consume_over_1mb_of_stack_does_trap(instantiation_strategy: InstantiationStrategy) {
|
||||
let wat = deep_call_stack_wat(CALL_DEPTH_UPPER_LIMIT + 1);
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy).use_wat(wat);
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance().expect("failed to instantiate a runtime");
|
||||
match instance.call_export("main", &[]).unwrap_err() {
|
||||
Error::AbortedDueToTrap(error) => {
|
||||
let expected = "wasm trap: call stack exhausted";
|
||||
assert_eq!(error.message, expected);
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_nan_canonicalization);
|
||||
fn test_nan_canonicalization(instantiation_strategy: InstantiationStrategy) {
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy).canonicalize_nans(true);
|
||||
let runtime = builder.build();
|
||||
|
||||
let mut instance = runtime.new_instance().expect("failed to instantiate a runtime");
|
||||
|
||||
/// A NaN with canonical payload bits.
|
||||
const CANONICAL_NAN_BITS: u32 = 0x7fc00000;
|
||||
/// A NaN value with an arbitrary payload.
|
||||
const ARBITRARY_NAN_BITS: u32 = 0x7f812345;
|
||||
|
||||
// This test works like this: we essentially do
|
||||
//
|
||||
// a + b
|
||||
//
|
||||
// where
|
||||
//
|
||||
// * a is a nan with arbitrary bits in its payload
|
||||
// * b is 1.
|
||||
//
|
||||
// according to the wasm spec, if one of the inputs to the operation is a non-canonical NaN
|
||||
// then the value be a NaN with non-deterministic payload bits.
|
||||
//
|
||||
// However, with the `canonicalize_nans` option turned on above, we expect that the output will
|
||||
// be a canonical NaN.
|
||||
//
|
||||
// We extrapolate the results of this tests so that we assume that all intermediate computations
|
||||
// that involve floats are sanitized and cannot produce a non-deterministic NaN.
|
||||
|
||||
let params = (u32::to_le_bytes(ARBITRARY_NAN_BITS), u32::to_le_bytes(1)).encode();
|
||||
let res = {
|
||||
let raw_result = instance.call_export("test_fp_f32add", ¶ms).unwrap();
|
||||
u32::from_le_bytes(<[u8; 4]>::decode(&mut &raw_result[..]).unwrap())
|
||||
};
|
||||
assert_eq!(res, CANONICAL_NAN_BITS);
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_stack_depth_reaching);
|
||||
fn test_stack_depth_reaching(instantiation_strategy: InstantiationStrategy) {
|
||||
const TEST_GUARD_PAGE_SKIP: &str = include_str!("test-guard-page-skip.wat");
|
||||
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy)
|
||||
.use_wat(TEST_GUARD_PAGE_SKIP.to_string())
|
||||
.deterministic_stack(true);
|
||||
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance().expect("failed to instantiate a runtime");
|
||||
|
||||
match instance.call_export("test-many-locals", &[]).unwrap_err() {
|
||||
Error::AbortedDueToTrap(error) => {
|
||||
let expected = "wasm trap: wasm `unreachable` instruction executed";
|
||||
assert_eq!(error.message, expected);
|
||||
},
|
||||
error => panic!("unexpected error: {:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_max_memory_pages_imported_memory_without_precompilation);
|
||||
fn test_max_memory_pages_imported_memory_without_precompilation(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
) {
|
||||
test_max_memory_pages(instantiation_strategy, true, false);
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_max_memory_pages_exported_memory_without_precompilation);
|
||||
fn test_max_memory_pages_exported_memory_without_precompilation(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
) {
|
||||
test_max_memory_pages(instantiation_strategy, false, false);
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_max_memory_pages_imported_memory_with_precompilation);
|
||||
fn test_max_memory_pages_imported_memory_with_precompilation(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
) {
|
||||
test_max_memory_pages(instantiation_strategy, true, true);
|
||||
}
|
||||
|
||||
test_wasm_execution!(test_max_memory_pages_exported_memory_with_precompilation);
|
||||
fn test_max_memory_pages_exported_memory_with_precompilation(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
) {
|
||||
test_max_memory_pages(instantiation_strategy, false, true);
|
||||
}
|
||||
|
||||
fn test_max_memory_pages(
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
import_memory: bool,
|
||||
precompile_runtime: bool,
|
||||
) {
|
||||
fn call(
|
||||
heap_alloc_strategy: HeapAllocStrategy,
|
||||
wat: String,
|
||||
instantiation_strategy: InstantiationStrategy,
|
||||
precompile_runtime: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut builder = RuntimeBuilder::new(instantiation_strategy)
|
||||
.use_wat(wat)
|
||||
.heap_alloc_strategy(heap_alloc_strategy)
|
||||
.precompile_runtime(precompile_runtime);
|
||||
|
||||
let runtime = builder.build();
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
instance.call_export("main", &[])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn memory(initial: u32, maximum: u32, import: bool) -> String {
|
||||
let memory = format!("(memory $0 {} {})", initial, maximum);
|
||||
|
||||
if import {
|
||||
format!("(import \"env\" \"memory\" {})", memory)
|
||||
} else {
|
||||
format!("{}\n(export \"memory\" (memory $0))", memory)
|
||||
}
|
||||
}
|
||||
|
||||
let assert_grow_ok = |alloc_strategy: HeapAllocStrategy, initial_pages: u32, max_pages: u32| {
|
||||
eprintln!("assert_grow_ok({alloc_strategy:?}, {initial_pages}, {max_pages})");
|
||||
|
||||
call(
|
||||
alloc_strategy,
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
|
||||
;; assert(memory.grow returns != -1)
|
||||
(if
|
||||
(i32.eq
|
||||
(memory.grow
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.const -1)
|
||||
)
|
||||
(then
|
||||
(unreachable)
|
||||
)
|
||||
)
|
||||
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
memory(initial_pages, max_pages, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let assert_grow_fail =
|
||||
|alloc_strategy: HeapAllocStrategy, initial_pages: u32, max_pages: u32| {
|
||||
eprintln!("assert_grow_fail({alloc_strategy:?}, {initial_pages}, {max_pages})");
|
||||
|
||||
call(
|
||||
alloc_strategy,
|
||||
format!(
|
||||
r#"
|
||||
(module
|
||||
{}
|
||||
(global (export "__heap_base") i32 (i32.const 0))
|
||||
(func (export "main")
|
||||
(param i32 i32) (result i64)
|
||||
|
||||
;; assert(memory.grow returns == -1)
|
||||
(if
|
||||
(i32.ne
|
||||
(memory.grow
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.const -1)
|
||||
)
|
||||
(then
|
||||
(unreachable)
|
||||
)
|
||||
)
|
||||
|
||||
(i64.const 0)
|
||||
)
|
||||
)
|
||||
"#,
|
||||
memory(initial_pages, max_pages, import_memory)
|
||||
),
|
||||
instantiation_strategy,
|
||||
precompile_runtime,
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 1, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 9, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Dynamic { maximum_pages: Some(10) }, 10, 10);
|
||||
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 1, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 9, 10);
|
||||
assert_grow_ok(HeapAllocStrategy::Dynamic { maximum_pages: None }, 10, 10);
|
||||
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 1, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 9, 10);
|
||||
assert_grow_fail(HeapAllocStrategy::Static { extra_pages: 10 }, 10, 10);
|
||||
}
|
||||
|
||||
// This test takes quite a while to execute in a debug build (over 6 minutes on a TR 3970x)
|
||||
// so it's ignored by default unless it was compiled with `--release`.
|
||||
#[cfg_attr(build_profile = "debug", ignore)]
|
||||
#[test]
|
||||
fn test_instances_without_reuse_are_not_leaked() {
|
||||
let runtime = crate::create_runtime::<HostFunctions>(
|
||||
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
|
||||
crate::Config {
|
||||
allow_missing_func_imports: true,
|
||||
cache_path: None,
|
||||
semantics: crate::Semantics {
|
||||
instantiation_strategy: InstantiationStrategy::RecreateInstance,
|
||||
deterministic_stack_limit: None,
|
||||
canonicalize_nans: false,
|
||||
parallel_compilation: true,
|
||||
heap_alloc_strategy: DEFAULT_HEAP_ALLOC_STRATEGY,
|
||||
wasm_multi_value: false,
|
||||
wasm_bulk_memory: false,
|
||||
wasm_reference_types: false,
|
||||
wasm_simd: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// As long as the `wasmtime`'s `Store` lives the instances spawned through it
|
||||
// will live indefinitely. Currently it has a maximum limit of 10k instances,
|
||||
// so let's spawn 10k + 1 of them to make sure our code doesn't keep the `Store`
|
||||
// alive longer than it is necessary. (And since we disabled instance reuse
|
||||
// a new instance will be spawned on each call.)
|
||||
let mut instance = runtime.new_instance().unwrap();
|
||||
for _ in 0..10001 {
|
||||
instance.call_export("test_empty_return", &[0]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rustix_version_matches_with_wasmtime() {
|
||||
let metadata = cargo_metadata::MetadataCommand::new().exec().unwrap();
|
||||
|
||||
let wasmtime_rustix = metadata
|
||||
.packages
|
||||
.iter()
|
||||
.find(|pkg| pkg.name == "wasmtime")
|
||||
.unwrap()
|
||||
.dependencies
|
||||
.iter()
|
||||
.find(|dep| dep.name == "rustix")
|
||||
.unwrap();
|
||||
let our_rustix = metadata
|
||||
.packages
|
||||
.iter()
|
||||
.find(|pkg| pkg.name == "sc-executor-wasmtime")
|
||||
.unwrap()
|
||||
.dependencies
|
||||
.iter()
|
||||
.find(|dep| dep.name == "rustix")
|
||||
.unwrap();
|
||||
|
||||
if wasmtime_rustix.req != our_rustix.req {
|
||||
panic!(
|
||||
"our version of rustix ({0}) doesn't match wasmtime's ({1}); \
|
||||
bump the version in `sc-executor-wasmtime`'s `Cargo.toml' to '{1}' and try again",
|
||||
our_rustix.req, wasmtime_rustix.req,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
|
||||
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::{runtime::StoreData, InstantiationStrategy};
|
||||
use pezsc_executor_common::{
|
||||
error::{Error, Result},
|
||||
util::checked_range,
|
||||
};
|
||||
use pezsp_wasm_interface::Pointer;
|
||||
use wasmtime::{AsContext, AsContextMut};
|
||||
|
||||
/// Read data from the instance memory into a slice.
|
||||
///
|
||||
/// Returns an error if the read would go out of the memory bounds.
|
||||
pub(crate) fn read_memory_into(
|
||||
ctx: impl AsContext<Data = StoreData>,
|
||||
address: Pointer<u8>,
|
||||
dest: &mut [u8],
|
||||
) -> Result<()> {
|
||||
let memory = ctx.as_context().data().memory().data(&ctx);
|
||||
|
||||
let range = checked_range(address.into(), dest.len(), memory.len())
|
||||
.ok_or_else(|| Error::Other("memory read is out of bounds".into()))?;
|
||||
dest.copy_from_slice(&memory[range]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write data to the instance memory from a slice.
|
||||
///
|
||||
/// Returns an error if the write would go out of the memory bounds.
|
||||
pub(crate) fn write_memory_from(
|
||||
mut ctx: impl AsContextMut<Data = StoreData>,
|
||||
address: Pointer<u8>,
|
||||
data: &[u8],
|
||||
) -> Result<()> {
|
||||
let memory = ctx.as_context().data().memory();
|
||||
let memory = memory.data_mut(&mut ctx);
|
||||
|
||||
let range = checked_range(address.into(), data.len(), memory.len())
|
||||
.ok_or_else(|| Error::Other("memory write is out of bounds".into()))?;
|
||||
memory[range].copy_from_slice(data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks whether the `madvise(MADV_DONTNEED)` works as expected.
|
||||
///
|
||||
/// In certain environments (e.g. when running under the QEMU user-mode emulator)
|
||||
/// this syscall is broken.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn is_madvise_working() -> std::result::Result<bool, String> {
|
||||
let page_size = rustix::param::page_size();
|
||||
|
||||
unsafe {
|
||||
// Allocate two memory pages.
|
||||
let pointer = rustix::mm::mmap_anonymous(
|
||||
std::ptr::null_mut(),
|
||||
2 * page_size,
|
||||
rustix::mm::ProtFlags::READ | rustix::mm::ProtFlags::WRITE,
|
||||
rustix::mm::MapFlags::PRIVATE,
|
||||
)
|
||||
.map_err(|error| format!("mmap failed: {}", error))?;
|
||||
|
||||
// Dirty them both.
|
||||
std::ptr::write_volatile(pointer.cast::<u8>(), b'A');
|
||||
std::ptr::write_volatile(pointer.cast::<u8>().add(page_size), b'B');
|
||||
|
||||
// Clear the first page.
|
||||
let result_madvise =
|
||||
rustix::mm::madvise(pointer, page_size, rustix::mm::Advice::LinuxDontNeed)
|
||||
.map_err(|error| format!("madvise failed: {}", error));
|
||||
|
||||
// Fetch the values.
|
||||
let value_1 = std::ptr::read_volatile(pointer.cast::<u8>());
|
||||
let value_2 = std::ptr::read_volatile(pointer.cast::<u8>().add(page_size));
|
||||
|
||||
let result_munmap = rustix::mm::munmap(pointer, 2 * page_size)
|
||||
.map_err(|error| format!("munmap failed: {}", error));
|
||||
|
||||
result_madvise?;
|
||||
result_munmap?;
|
||||
|
||||
// Verify that the first page was cleared, while the second one was not.
|
||||
Ok(value_1 == 0 && value_2 == b'B')
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn test_is_madvise_working_check_does_not_fail() {
|
||||
assert!(is_madvise_working().is_ok());
|
||||
}
|
||||
|
||||
/// Checks whether a given instantiation strategy can be safely used, and replaces
|
||||
/// it with a slower (but sound) alternative if it isn't.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn replace_strategy_if_broken(strategy: &mut InstantiationStrategy) {
|
||||
let replacement_strategy = match *strategy {
|
||||
// These strategies don't need working `madvise`.
|
||||
InstantiationStrategy::Pooling | InstantiationStrategy::RecreateInstance => return,
|
||||
|
||||
// These strategies require a working `madvise` to be sound.
|
||||
InstantiationStrategy::PoolingCopyOnWrite => InstantiationStrategy::Pooling,
|
||||
InstantiationStrategy::RecreateInstanceCopyOnWrite =>
|
||||
InstantiationStrategy::RecreateInstance,
|
||||
};
|
||||
|
||||
use std::sync::OnceLock;
|
||||
static IS_OK: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
let is_ok = IS_OK.get_or_init(|| {
|
||||
let is_ok = match is_madvise_working() {
|
||||
Ok(is_ok) => is_ok,
|
||||
Err(error) => {
|
||||
// This should never happen.
|
||||
log::warn!("Failed to check whether `madvise(MADV_DONTNEED)` works: {}", error);
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !is_ok {
|
||||
log::warn!("You're running on a system with a broken `madvise(MADV_DONTNEED)` implementation. This will result in lower performance.");
|
||||
}
|
||||
|
||||
is_ok
|
||||
});
|
||||
|
||||
if !is_ok {
|
||||
*strategy = replacement_strategy;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub(crate) fn replace_strategy_if_broken(_: &mut InstantiationStrategy) {}
|
||||
Reference in New Issue
Block a user