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:
2025-12-14 00:04:10 +03:00
parent 286de54384
commit 1c0e57d984
9084 changed files with 997839 additions and 997557 deletions
@@ -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", &params).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) {}