feat: initialize Kurdistan SDK - independent fork of Polkadot SDK

This commit is contained in:
2025-12-13 15:44:15 +03:00
commit e4778b4576
6838 changed files with 1847450 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
[package]
name = "sc-executor"
version = "0.32.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 = "A crate that provides means of executing/dispatching calls into the runtime."
documentation = "https://docs.rs/sc-executor"
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[[bench]]
name = "bench"
harness = false
[dependencies]
parking_lot = { workspace = true, default-features = true }
schnellru = { workspace = true }
tracing = { workspace = true, default-features = true }
codec = { workspace = true, default-features = true }
sc-executor-common = { workspace = true, default-features = true }
sc-executor-polkavm = { workspace = true, default-features = true }
sc-executor-wasmtime = { workspace = true, default-features = true }
sp-api = { workspace = true, default-features = true }
sp-core = { workspace = true, default-features = true }
sp-externalities = { workspace = true, default-features = true }
sp-io = { workspace = true, default-features = true }
sp-panic-handler = { workspace = true, default-features = true }
sp-runtime-interface = { workspace = true, default-features = true }
sp-trie = { workspace = true, default-features = true }
sp-version = { workspace = true, default-features = true }
sp-wasm-interface = { workspace = true, default-features = true }
[dev-dependencies]
array-bytes = { workspace = true, default-features = true }
assert_matches = { workspace = true }
criterion = { workspace = true, default-features = true }
num_cpus = { workspace = true }
paste = { workspace = true, default-features = true }
sc-runtime-test = { workspace = true }
sc-tracing = { workspace = true, default-features = true }
sp-crypto-hashing = { workspace = true, default-features = true }
sp-maybe-compressed-blob = { workspace = true, default-features = true }
sp-runtime = { workspace = true, default-features = true }
sp-state-machine = { workspace = true, default-features = true }
sp-tracing = { workspace = true, default-features = true }
substrate-test-runtime = { workspace = true }
tempfile = { workspace = true }
tracing-subscriber = { workspace = true }
wat = { workspace = true }
[features]
default = ["std"]
# This crate does not have `no_std` support, we just require this for tests
std = [
"sc-runtime-test/std",
"sp-api/std",
"sp-core/std",
"sp-externalities/std",
"sp-io/std",
"sp-runtime-interface/std",
"sp-runtime/std",
"sp-state-machine/std",
"sp-tracing/std",
"sp-trie/std",
"sp-version/std",
"sp-wasm-interface/std",
"substrate-test-runtime/std",
]
wasm-extern-trace = []
runtime-benchmarks = [
"sc-executor-wasmtime/runtime-benchmarks",
"sc-runtime-test/runtime-benchmarks",
"sc-tracing/runtime-benchmarks",
"sp-api/runtime-benchmarks",
"sp-io/runtime-benchmarks",
"sp-runtime-interface/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"sp-state-machine/runtime-benchmarks",
"sp-trie/runtime-benchmarks",
"sp-version/runtime-benchmarks",
"substrate-test-runtime/runtime-benchmarks",
]
+13
View File
@@ -0,0 +1,13 @@
A crate that provides means of executing/dispatching calls into the runtime.
There are a few responsibilities of this crate at the moment:
- It provides an implementation of a common entrypoint for calling into the runtime, both
wasm and compiled.
- It defines the environment for the wasm execution, namely the host functions that are to be
provided into the wasm runtime module.
- It also provides the required infrastructure for executing the current wasm runtime (specified
by the current value of `:code` in the provided externalities), i.e. interfacing with
wasm engine used, instance cache.
License: GPL-3.0-or-later WITH Classpath-exception-2.0
+253
View File
@@ -0,0 +1,253 @@
// This file is part of Substrate.
// 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 criterion::{criterion_group, criterion_main, Criterion};
use codec::Encode;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{WasmInstance, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY},
};
use sc_executor_wasmtime::InstantiationStrategy;
use sc_runtime_test::wasm_binary_unwrap as test_runtime;
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
#[derive(Clone)]
enum Method {
Compiled { instantiation_strategy: InstantiationStrategy, precompile: bool },
}
// This is just a bog-standard Kusama runtime with an extra
// `test_empty_return` and `test_dirty_plenty_memory` functions
// copy-pasted from the test runtime.
fn kusama_runtime() -> &'static [u8] {
include_bytes!("kusama_runtime.wasm")
}
fn initialize(
_tmpdir: &mut Option<tempfile::TempDir>,
runtime: &[u8],
method: Method,
) -> Box<dyn WasmModule> {
let blob = RuntimeBlob::uncompress_if_needed(runtime).unwrap();
let allow_missing_func_imports = true;
match method {
Method::Compiled { instantiation_strategy, precompile } => {
let config = sc_executor_wasmtime::Config {
allow_missing_func_imports,
cache_path: None,
semantics: sc_executor_wasmtime::Semantics {
heap_alloc_strategy: DEFAULT_HEAP_ALLOC_STRATEGY,
instantiation_strategy,
deterministic_stack_limit: None,
canonicalize_nans: false,
parallel_compilation: true,
wasm_multi_value: false,
wasm_bulk_memory: false,
wasm_reference_types: false,
wasm_simd: false,
},
};
if precompile {
let precompiled_blob =
sc_executor_wasmtime::prepare_runtime_artifact(blob, &config.semantics)
.unwrap();
// Create a fresh temporary directory to make absolutely sure
// we'll use the right module.
*_tmpdir = Some(tempfile::tempdir().unwrap());
let tmpdir = _tmpdir.as_ref().unwrap();
let path = tmpdir.path().join("module.bin");
std::fs::write(&path, &precompiled_blob).unwrap();
unsafe {
sc_executor_wasmtime::create_runtime_from_artifact::<
sp_io::SubstrateHostFunctions,
>(&path, config)
}
} else {
sc_executor_wasmtime::create_runtime::<sp_io::SubstrateHostFunctions>(blob, config)
}
.map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) })
},
}
.unwrap()
}
fn run_benchmark(
c: &mut Criterion,
benchmark_name: &str,
thread_count: usize,
runtime: &dyn WasmModule,
testcase: impl Fn(&mut Box<dyn WasmInstance>) + Copy + Send + 'static,
) {
c.bench_function(benchmark_name, |b| {
// Here we deliberately start a bunch of extra threads which will just
// keep on independently instantiating the runtime over and over again.
//
// We don't really have to measure how much time those take since the
// work done is essentially the same on each thread, and what we're
// interested in here is only how those extra threads affect the execution
// on the current thread.
//
// In an ideal case assuming we have enough CPU cores those extra threads
// shouldn't affect the main thread's runtime at all, however in practice
// they're not completely independent. There might be per-process
// locks in the kernel which are briefly held during instantiation, etc.,
// and how much those affect the execution here is what we want to measure.
let is_benchmark_running = Arc::new(AtomicBool::new(true));
let threads_running = Arc::new(AtomicUsize::new(0));
let aux_threads: Vec<_> = (0..thread_count - 1)
.map(|_| {
let mut instance = runtime.new_instance().unwrap();
let is_benchmark_running = is_benchmark_running.clone();
let threads_running = threads_running.clone();
std::thread::spawn(move || {
threads_running.fetch_add(1, Ordering::SeqCst);
while is_benchmark_running.load(Ordering::Relaxed) {
testcase(&mut instance);
}
})
})
.collect();
while threads_running.load(Ordering::SeqCst) != (thread_count - 1) {
std::thread::yield_now();
}
let mut instance = runtime.new_instance().unwrap();
b.iter(|| testcase(&mut instance));
is_benchmark_running.store(false, Ordering::SeqCst);
for thread in aux_threads {
thread.join().unwrap();
}
});
}
fn bench_call_instance(c: &mut Criterion) {
sp_tracing::try_init_simple();
let strategies = [
(
"recreate_instance_vanilla",
Method::Compiled {
instantiation_strategy: InstantiationStrategy::RecreateInstance,
precompile: false,
},
),
(
"recreate_instance_cow_fresh",
Method::Compiled {
instantiation_strategy: InstantiationStrategy::RecreateInstanceCopyOnWrite,
precompile: false,
},
),
(
"recreate_instance_cow_precompiled",
Method::Compiled {
instantiation_strategy: InstantiationStrategy::RecreateInstanceCopyOnWrite,
precompile: true,
},
),
(
"pooling_vanilla_fresh",
Method::Compiled {
instantiation_strategy: InstantiationStrategy::Pooling,
precompile: false,
},
),
(
"pooling_vanilla_precompiled",
Method::Compiled {
instantiation_strategy: InstantiationStrategy::Pooling,
precompile: true,
},
),
(
"pooling_cow_fresh",
Method::Compiled {
instantiation_strategy: InstantiationStrategy::PoolingCopyOnWrite,
precompile: false,
},
),
(
"pooling_cow_precompiled",
Method::Compiled {
instantiation_strategy: InstantiationStrategy::PoolingCopyOnWrite,
precompile: true,
},
),
];
let runtimes = [("kusama_runtime", kusama_runtime()), ("test_runtime", test_runtime())];
let thread_counts = [1, 2, 4, 8, 16];
fn test_call_empty_function(instance: &mut Box<dyn WasmInstance>) {
instance.call_export("test_empty_return", &[0]).unwrap();
}
fn test_dirty_1mb_of_memory(instance: &mut Box<dyn WasmInstance>) {
instance.call_export("test_dirty_plenty_memory", &(0, 16).encode()).unwrap();
}
let testcases = [
("call_empty_function", test_call_empty_function as fn(&mut Box<dyn WasmInstance>)),
("dirty_1mb_of_memory", test_dirty_1mb_of_memory),
];
let num_cpus = num_cpus::get_physical();
let mut tmpdir = None;
for (strategy_name, strategy) in strategies {
for (runtime_name, runtime) in runtimes {
let runtime = initialize(&mut tmpdir, runtime, strategy.clone());
for (testcase_name, testcase) in testcases {
for thread_count in thread_counts {
if thread_count > num_cpus {
// If there are not enough cores available the benchmark is pointless.
continue;
}
let benchmark_name = format!(
"{}_from_{}_with_{}_on_{}_threads",
testcase_name, runtime_name, strategy_name, thread_count
);
run_benchmark(c, &benchmark_name, thread_count, &*runtime, testcase);
}
}
}
}
}
criterion_group! {
name = benches;
config = Criterion::default();
targets = bench_call_instance
}
criterion_main!(benches);
@@ -0,0 +1,28 @@
[package]
name = "sc-executor-common"
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 = "A set of common definitions that are needed for defining execution engines."
documentation = "https://docs.rs/sc-executor-common/"
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
polkavm = { workspace = true }
sc-allocator = { workspace = true, default-features = true }
sp-maybe-compressed-blob = { workspace = true, default-features = true }
sp-wasm-interface = { workspace = true, default-features = true }
thiserror = { workspace = true }
wasm-instrument = { workspace = true, default-features = true }
[features]
default = []
@@ -0,0 +1,3 @@
A set of common definitions that are needed for defining execution engines.
License: GPL-3.0-or-later WITH Classpath-exception-2.0
@@ -0,0 +1,204 @@
// This file is part of Substrate.
// 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/>.
//! Rust executor possible errors.
/// Result type alias.
pub type Result<T> = std::result::Result<T, Error>;
/// Error type.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum Error {
#[error("Error calling api function: {0}")]
ApiError(Box<dyn std::error::Error + Send + Sync>),
#[error("Method not found: '{0}'")]
MethodNotFound(String),
#[error("On-chain runtime does not specify version")]
VersionInvalid,
#[error("Externalities error")]
Externalities,
#[error("Invalid index provided")]
InvalidIndex,
#[error("Invalid type returned (should be u64)")]
InvalidReturn,
#[error("Runtime panicked: {0}")]
RuntimePanicked(String),
#[error("Invalid memory reference")]
InvalidMemoryReference,
#[error("The runtime doesn't provide a global named `__heap_base` of type `i32`")]
HeapBaseNotFoundOrInvalid,
#[error("The runtime must not have the `start` function defined")]
RuntimeHasStartFn,
#[error("Other: {0}")]
Other(String),
#[error(transparent)]
Allocator(#[from] sc_allocator::Error),
#[error("Host function {0} execution failed with: {1}")]
FunctionExecution(String, String),
#[error("No table exported by wasm blob")]
NoTable,
#[error("No table entry with index {0} in wasm blob exported table")]
NoTableEntryWithIndex(u32),
#[error("Table element with index {0} is not a function in wasm blob exported table")]
TableElementIsNotAFunction(u32),
#[error("Table entry with index {0} in wasm blob is null")]
FunctionRefIsNull(u32),
#[error(transparent)]
RuntimeConstruction(#[from] WasmError),
#[error("Shared memory is not supported")]
SharedMemUnsupported,
#[error("Imported globals are not supported yet")]
ImportedGlobalsUnsupported,
#[error("initializer expression can have only up to 2 expressions in wasm 1.0")]
InitializerHasTooManyExpressions,
#[error("Invalid initializer expression provided {0}")]
InvalidInitializerExpression(String),
#[error("Execution aborted due to panic: {0}")]
AbortedDueToPanic(MessageWithBacktrace),
#[error("Execution aborted due to trap: {0}")]
AbortedDueToTrap(MessageWithBacktrace),
#[error("Output exceeds bounds of wasm memory")]
OutputExceedsBounds,
}
impl From<&'static str> for Error {
fn from(err: &'static str) -> Error {
Error::Other(err.into())
}
}
impl From<String> for Error {
fn from(err: String) -> Error {
Error::Other(err)
}
}
/// Type for errors occurring during Wasm runtime construction.
#[derive(Debug, thiserror::Error)]
#[allow(missing_docs)]
pub enum WasmError {
#[error("Code could not be read from the state.")]
CodeNotFound,
#[error("Failure to reinitialize runtime instance from snapshot.")]
ApplySnapshotFailed,
/// Failure to erase the wasm memory.
///
/// Depending on the implementation might mean failure of allocating memory.
#[error("Failure to erase the wasm memory: {0}")]
ErasingFailed(String),
#[error("Wasm code failed validation.")]
InvalidModule,
#[error("Wasm code could not be deserialized.")]
CantDeserializeWasm,
#[error("The module does not export a linear memory named `memory`.")]
InvalidMemory,
#[error("The number of heap pages requested is disallowed by the module.")]
InvalidHeapPages,
/// Instantiation error.
#[error("{0}")]
Instantiation(String),
/// Other error happened.
#[error("Other error happened while constructing the runtime: {0}")]
Other(String),
}
impl From<polkavm::program::ProgramParseError> for WasmError {
fn from(error: polkavm::program::ProgramParseError) -> Self {
WasmError::Other(error.to_string())
}
}
impl From<polkavm::Error> for WasmError {
fn from(error: polkavm::Error) -> Self {
WasmError::Other(error.to_string())
}
}
impl From<polkavm::Error> for Error {
fn from(error: polkavm::Error) -> Self {
Error::Other(error.to_string())
}
}
/// An error message with an attached backtrace.
#[derive(Debug)]
pub struct MessageWithBacktrace {
/// The error message.
pub message: String,
/// The backtrace associated with the error message.
pub backtrace: Option<Backtrace>,
}
impl std::fmt::Display for MessageWithBacktrace {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.write_str(&self.message)?;
if let Some(ref backtrace) = self.backtrace {
fmt.write_str("\nWASM backtrace:\n")?;
backtrace.backtrace_string.fmt(fmt)?;
}
Ok(())
}
}
/// A WASM backtrace.
#[derive(Debug)]
pub struct Backtrace {
/// The string containing the backtrace.
pub backtrace_string: String,
}
impl std::fmt::Display for Backtrace {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.write_str(&self.backtrace_string)
}
}
@@ -0,0 +1,31 @@
// This file is part of Substrate.
// 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/>.
//! A set of common definitions that are needed for defining execution engines.
#![warn(missing_docs)]
#![deny(unused_crate_dependencies)]
pub mod error;
pub mod runtime_blob;
pub mod util;
pub mod wasm_runtime;
pub(crate) fn is_polkavm_enabled() -> bool {
std::env::var_os("SUBSTRATE_ENABLE_POLKAVM").map_or(false, |value| value == "1")
}
@@ -0,0 +1,51 @@
// This file is part of Substrate.
// 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 allows for inspection and instrumentation, i.e. modifying the module to alter it's
//! structure or behavior, of a wasm module.
//!
//! ## Instrumentation
//!
//! In ideal world, there would be no instrumentation. However, in the real world the execution
//! engines we use are somewhat limited in their APIs or abilities.
//!
//! To give you some examples:
//!
//! We need to reset the globals because when we
//! execute the Substrate Runtime, we do not drop and create the instance anew, instead
//! we restore some selected parts of the state.
//!
//! - stack depth metering can be performed via instrumentation or deferred to the engine and say be
//! added directly in machine code. Implementing this in machine code is rather cumbersome so
//! instrumentation looks like a good solution.
//!
//! Stack depth metering is needed to make a wasm blob
//! execution deterministic, which in turn is needed by the Teyrchain Validation Function in
//! Pezkuwi.
//!
//! ## Inspection
//!
//! Inspection of a wasm module may be needed to extract some useful information, such as to extract
//! data segment snapshot, which is helpful for quickly restoring the initial state of instances.
//! Inspection can be also useful to prove that a wasm module possesses some properties, such as,
//! is free of any floating point operations, which is a useful step towards making instances
//! produced from such a module deterministic.
mod runtime_blob;
pub use runtime_blob::RuntimeBlob;
@@ -0,0 +1,234 @@
// This file is part of Substrate.
// 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::{error::WasmError, wasm_runtime::HeapAllocStrategy};
use polkavm::ArcBytes;
use wasm_instrument::parity_wasm::elements::{
deserialize_buffer, serialize, ExportEntry, External, Internal, MemorySection, MemoryType,
Module, Section,
};
/// A program blob containing a Substrate runtime.
#[derive(Clone)]
pub struct RuntimeBlob(BlobKind);
#[derive(Clone)]
enum BlobKind {
WebAssembly(Module),
PolkaVM((polkavm::ProgramBlob, ArcBytes)),
}
impl RuntimeBlob {
/// Create `RuntimeBlob` from the given WASM or PolkaVM compressed program blob.
///
/// See [`sp_maybe_compressed_blob`] for details about decompression.
pub fn uncompress_if_needed(wasm_code: &[u8]) -> Result<Self, WasmError> {
use sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT;
let wasm_code = sp_maybe_compressed_blob::decompress(wasm_code, CODE_BLOB_BOMB_LIMIT)
.map_err(|e| WasmError::Other(format!("Decompression error: {:?}", e)))?;
Self::new(&wasm_code)
}
/// Create `RuntimeBlob` from the given WASM or PolkaVM program blob.
///
/// Returns `Err` if the blob cannot be deserialized.
///
/// Will only accept a PolkaVM program if the `SUBSTRATE_ENABLE_POLKAVM` environment
/// variable is set to `1`.
pub fn new(raw_blob: &[u8]) -> Result<Self, WasmError> {
if raw_blob.starts_with(b"PVM\0") {
if crate::is_polkavm_enabled() {
let raw = ArcBytes::from(raw_blob);
let blob = polkavm::ProgramBlob::parse(raw.clone())?;
return Ok(Self(BlobKind::PolkaVM((blob, raw))));
} else {
return Err(WasmError::Other("expected a WASM runtime blob, found a PolkaVM runtime blob; set the 'SUBSTRATE_ENABLE_POLKAVM' environment variable to enable the experimental PolkaVM-based executor".to_string()));
}
}
let raw_module: Module = deserialize_buffer(raw_blob)
.map_err(|e| WasmError::Other(format!("cannot deserialize module: {:?}", e)))?;
Ok(Self(BlobKind::WebAssembly(raw_module)))
}
/// Run a pass that instrument this module so as to introduce a deterministic stack height
/// limit.
///
/// It will introduce a global mutable counter. The instrumentation will increase the counter
/// according to the "cost" of the callee. If the cost exceeds the `stack_depth_limit` constant,
/// the instrumentation will trap. The counter will be decreased as soon as the the callee
/// returns.
///
/// The stack cost of a function is computed based on how much locals there are and the maximum
/// depth of the wasm operand stack.
///
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
pub fn inject_stack_depth_metering(self, stack_depth_limit: u32) -> Result<Self, WasmError> {
let injected_module =
wasm_instrument::inject_stack_limiter(self.into_webassembly_blob()?, stack_depth_limit)
.map_err(|e| {
WasmError::Other(format!("cannot inject the stack limiter: {:?}", e))
})?;
Ok(Self(BlobKind::WebAssembly(injected_module)))
}
/// Converts a WASM memory import into a memory section and exports it.
///
/// Does nothing if there's no memory import.
///
/// May return an error in case the WASM module is invalid.
///
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
pub fn convert_memory_import_into_export(&mut self) -> Result<(), WasmError> {
let raw_module = self.as_webassembly_blob_mut()?;
let import_section = match raw_module.import_section_mut() {
Some(import_section) => import_section,
None => return Ok(()),
};
let import_entries = import_section.entries_mut();
for index in 0..import_entries.len() {
let entry = &import_entries[index];
let memory_ty = match entry.external() {
External::Memory(memory_ty) => *memory_ty,
_ => continue,
};
let memory_name = entry.field().to_owned();
import_entries.remove(index);
raw_module
.insert_section(Section::Memory(MemorySection::with_entries(vec![memory_ty])))
.map_err(|error| {
WasmError::Other(format!(
"can't convert a memory import into an export: failed to insert a new memory section: {}",
error
))
})?;
if raw_module.export_section_mut().is_none() {
// A module without an export section is somewhat unrealistic, but let's do this
// just in case to cover all of our bases.
raw_module
.insert_section(Section::Export(Default::default()))
.expect("an export section can be always inserted if it doesn't exist; qed");
}
raw_module
.export_section_mut()
.expect("export section already existed or we just added it above, so it always exists; qed")
.entries_mut()
.push(ExportEntry::new(memory_name, Internal::Memory(0)));
break;
}
Ok(())
}
/// Modifies the blob's memory section according to the given `heap_alloc_strategy`.
///
/// Will return an error in case there is no memory section present,
/// or if the memory section is empty.
///
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
pub fn setup_memory_according_to_heap_alloc_strategy(
&mut self,
heap_alloc_strategy: HeapAllocStrategy,
) -> Result<(), WasmError> {
let raw_module = self.as_webassembly_blob_mut()?;
let memory_section = raw_module
.memory_section_mut()
.ok_or_else(|| WasmError::Other("no memory section found".into()))?;
if memory_section.entries().is_empty() {
return Err(WasmError::Other("memory section is empty".into()));
}
for memory_ty in memory_section.entries_mut() {
let initial = memory_ty.limits().initial();
let (min, max) = match heap_alloc_strategy {
HeapAllocStrategy::Dynamic { maximum_pages } => {
// Ensure `initial <= maximum_pages`
(maximum_pages.map(|m| m.min(initial)).unwrap_or(initial), maximum_pages)
},
HeapAllocStrategy::Static { extra_pages } => {
let pages = initial.saturating_add(extra_pages);
(pages, Some(pages))
},
};
*memory_ty = MemoryType::new(min, max);
}
Ok(())
}
/// Scans the wasm blob for the first section with the name that matches the given. Returns the
/// contents of the custom section if found or `None` otherwise.
///
/// Only valid for WASM programs; will return an error if the blob is a PolkaVM program.
pub fn custom_section_contents(&self, section_name: &str) -> Option<&[u8]> {
self.as_webassembly_blob()
.ok()?
.custom_sections()
.find(|cs| cs.name() == section_name)
.map(|cs| cs.payload())
}
/// Consumes this runtime blob and serializes it.
pub fn serialize(self) -> Vec<u8> {
match self.0 {
BlobKind::WebAssembly(raw_module) =>
serialize(raw_module).expect("serializing into a vec should succeed; qed"),
BlobKind::PolkaVM(ref blob) => blob.1.to_vec(),
}
}
fn as_webassembly_blob(&self) -> Result<&Module, WasmError> {
match self.0 {
BlobKind::WebAssembly(ref raw_module) => Ok(raw_module),
BlobKind::PolkaVM(..) => Err(WasmError::Other(
"expected a WebAssembly program; found a PolkaVM program blob".into(),
)),
}
}
fn as_webassembly_blob_mut(&mut self) -> Result<&mut Module, WasmError> {
match self.0 {
BlobKind::WebAssembly(ref mut raw_module) => Ok(raw_module),
BlobKind::PolkaVM(..) => Err(WasmError::Other(
"expected a WebAssembly program; found a PolkaVM program blob".into(),
)),
}
}
fn into_webassembly_blob(self) -> Result<Module, WasmError> {
match self.0 {
BlobKind::WebAssembly(raw_module) => Ok(raw_module),
BlobKind::PolkaVM(..) => Err(WasmError::Other(
"expected a WebAssembly program; found a PolkaVM program blob".into(),
)),
}
}
/// Gets a reference to the inner PolkaVM program blob, if this is a PolkaVM program.
pub fn as_polkavm_blob(&self) -> Option<&polkavm::ProgramBlob> {
match self.0 {
BlobKind::WebAssembly(..) => None,
BlobKind::PolkaVM((ref blob, _)) => Some(blob),
}
}
}
@@ -0,0 +1,48 @@
// This file is part of Substrate.
// 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/>.
//! Utilities used by all backends
use crate::error::Result;
use sp_wasm_interface::Pointer;
use std::ops::Range;
/// Construct a range from an offset to a data length after the offset.
/// Returns None if the end of the range would exceed some maximum offset.
pub fn checked_range(offset: usize, len: usize, max: usize) -> Option<Range<usize>> {
let end = offset.checked_add(len)?;
(end <= max).then(|| offset..end)
}
/// Provides safe memory access interface using an external buffer
pub trait MemoryTransfer {
/// Read data from a slice of memory into a newly allocated buffer.
///
/// Returns an error if the read would go out of the memory bounds.
fn read(&self, source_addr: Pointer<u8>, size: usize) -> Result<Vec<u8>>;
/// Read data from a slice of memory into a destination buffer.
///
/// Returns an error if the read would go out of the memory bounds.
fn read_into(&self, source_addr: Pointer<u8>, destination: &mut [u8]) -> Result<()>;
/// Write data to a slice of memory.
///
/// Returns an error if the write would go out of the memory bounds.
fn write_from(&self, dest_addr: Pointer<u8>, source: &[u8]) -> Result<()>;
}
@@ -0,0 +1,98 @@
// This file is part of Substrate.
// 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/>.
//! Definitions for a wasm runtime.
use crate::error::Error;
pub use sc_allocator::AllocationStats;
/// Default heap allocation strategy.
pub const DEFAULT_HEAP_ALLOC_STRATEGY: HeapAllocStrategy =
HeapAllocStrategy::Static { extra_pages: DEFAULT_HEAP_ALLOC_PAGES };
/// Default heap allocation pages.
pub const DEFAULT_HEAP_ALLOC_PAGES: u32 = 2048;
/// A trait that defines an abstract WASM runtime module.
///
/// This can be implemented by an execution engine.
pub trait WasmModule: Sync + Send {
/// Create a new instance.
fn new_instance(&self) -> Result<Box<dyn WasmInstance>, Error>;
}
/// A trait that defines an abstract wasm module instance.
///
/// This can be implemented by an execution engine.
pub trait WasmInstance: Send {
/// Call a method on this WASM instance.
///
/// Before execution, instance is reset.
///
/// Returns the encoded result on success.
fn call(&mut self, method: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
self.call_with_allocation_stats(method, data).0
}
/// Call a method on this WASM instance.
///
/// Before execution, instance is reset.
///
/// Returns the encoded result on success.
fn call_with_allocation_stats(
&mut self,
method: &str,
data: &[u8],
) -> (Result<Vec<u8>, Error>, Option<AllocationStats>);
/// Call an exported method on this WASM instance.
///
/// Before execution, instance is reset.
///
/// Returns the encoded result on success.
fn call_export(&mut self, method: &str, data: &[u8]) -> Result<Vec<u8>, Error> {
self.call(method.into(), data)
}
}
/// Defines the heap pages allocation strategy the wasm runtime should use.
///
/// A heap page is defined as 64KiB of memory.
#[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)]
pub enum HeapAllocStrategy {
/// Allocate a static number of heap pages.
///
/// The total number of allocated heap pages is the initial number of heap pages requested by
/// the wasm file plus the `extra_pages`.
Static {
/// The number of pages that will be added on top of the initial heap pages requested by
/// the wasm file.
extra_pages: u32,
},
/// Allocate the initial heap pages as requested by the wasm file and then allow it to grow
/// dynamically.
Dynamic {
/// The absolute maximum size of the linear memory (in pages).
///
/// When `Some(_)` the linear memory will be allowed to grow up to this limit.
/// When `None` the linear memory will be allowed to grow up to the maximum limit supported
/// by WASM (4GB).
maximum_pages: Option<u32>,
},
}
@@ -0,0 +1,23 @@
[package]
name = "sc-executor-polkavm"
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 = "PolkaVM executor for Substrate"
readme = "README.md"
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
log = { workspace = true }
polkavm = { workspace = true }
sc-executor-common = { workspace = true, default-features = true }
sp-wasm-interface = { workspace = true, default-features = true }
@@ -0,0 +1 @@
License: GPL-3.0-or-later WITH Classpath-exception-2.0
@@ -0,0 +1,294 @@
// This file is part of Substrate.
// 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 polkavm::{CallError, Caller, Reg};
use sc_executor_common::{
error::{Error, WasmError},
wasm_runtime::{AllocationStats, WasmInstance, WasmModule},
};
use sp_wasm_interface::{
Function, FunctionContext, HostFunctions, Pointer, Value, ValueType, WordSize,
};
#[repr(transparent)]
pub struct InstancePre(polkavm::InstancePre<(), String>);
#[repr(transparent)]
pub struct Instance(polkavm::Instance<(), String>);
impl WasmModule for InstancePre {
fn new_instance(&self) -> Result<Box<dyn WasmInstance>, Error> {
Ok(Box::new(Instance(self.0.instantiate()?)))
}
}
impl WasmInstance for Instance {
fn call_with_allocation_stats(
&mut self,
name: &str,
raw_data: &[u8],
) -> (Result<Vec<u8>, Error>, Option<AllocationStats>) {
let pc = match self.0.module().exports().find(|e| e.symbol() == name) {
Some(export) => export.program_counter(),
None =>
return (
Err(format!("cannot call into the runtime: export not found: '{name}'").into()),
None,
),
};
let Ok(raw_data_length) = u32::try_from(raw_data.len()) else {
return (
Err(format!("cannot call runtime method '{name}': input payload is too big").into()),
None,
);
};
// TODO: This will leak guest memory; find a better solution.
// Make sure that the memory is cleared...
if let Err(err) = self.0.reset_memory() {
return (
Err(format!(
"call into the runtime method '{name}' failed: reset memory failed: {err}"
)
.into()),
None,
);
}
// ... and allocate space for the input payload.
if let Err(err) = self.0.sbrk(raw_data_length) {
return (
Err(format!(
"call into the runtime method '{name}' failed: reset memory failed: {err}"
)
.into()),
None,
);
}
// Grab the address of where the guest's heap starts; that's where we've just allocated
// the memory for the input payload.
let data_pointer = self.0.module().memory_map().heap_base();
if let Err(err) = self.0.write_memory(data_pointer, raw_data) {
return (Err(format!("call into the runtime method '{name}': failed to write the input payload into guest memory: {err}").into()), None);
}
match self.0.call_typed(&mut (), pc, (data_pointer, raw_data_length)) {
Ok(()) => {},
Err(CallError::Trap) =>
return (
Err(format!("call into the runtime method '{name}' failed: trap").into()),
None,
),
Err(CallError::Error(err)) =>
return (
Err(format!("call into the runtime method '{name}' failed: {err}").into()),
None,
),
Err(CallError::User(err)) =>
return (
Err(format!("call into the runtime method '{name}' failed: {err}").into()),
None,
),
Err(CallError::NotEnoughGas) => unreachable!("gas metering is never enabled"),
Err(CallError::Step) => unreachable!("stepping is never enabled"),
};
let result_pointer = self.0.reg(Reg::A0);
let result_length = self.0.reg(Reg::A1);
let output = match self.0.read_memory(result_pointer as u32, result_length as u32) {
Ok(output) => output,
Err(error) => {
return (Err(format!("call into the runtime method '{name}' failed: failed to read the return payload: {error}").into()), None)
},
};
(Ok(output), None)
}
}
struct Context<'r, 'a>(&'r mut polkavm::Caller<'a, ()>);
impl<'r, 'a> FunctionContext for Context<'r, 'a> {
fn read_memory_into(
&self,
address: Pointer<u8>,
dest: &mut [u8],
) -> sp_wasm_interface::Result<()> {
self.0
.instance
.read_memory_into(u32::from(address), dest)
.map_err(|error| error.to_string())
.map(|_| ())
}
fn write_memory(&mut self, address: Pointer<u8>, data: &[u8]) -> sp_wasm_interface::Result<()> {
self.0
.instance
.write_memory(u32::from(address), data)
.map_err(|error| error.to_string())
}
fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result<Pointer<u8>> {
let pointer = match self.0.instance.sbrk(0) {
Ok(pointer) => pointer.expect("fetching the current heap pointer never fails"),
Err(err) => return Err(format!("sbrk failed: {err}")),
};
// TODO: This will leak guest memory; find a better solution.
match self.0.instance.sbrk(size) {
Ok(Some(_)) => (),
Ok(None) => return Err(String::from("allocation error")),
Err(err) => return Err(format!("sbrk failed: {err}")),
}
Ok(Pointer::new(pointer))
}
fn deallocate_memory(&mut self, _ptr: Pointer<u8>) -> sp_wasm_interface::Result<()> {
// This is only used by the allocator host function, which is unused under PolkaVM.
unimplemented!("'deallocate_memory' is never used when running under PolkaVM");
}
fn register_panic_error_message(&mut self, _message: &str) {
unimplemented!("'register_panic_error_message' is never used when running under PolkaVM");
}
}
fn call_host_function(caller: &mut Caller<()>, function: &dyn Function) -> Result<(), String> {
let mut args = [Value::I64(0); Reg::ARG_REGS.len()];
let mut nth_reg = 0;
for (nth_arg, kind) in function.signature().args.iter().enumerate() {
match kind {
ValueType::I32 => {
args[nth_arg] = Value::I32(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as i32);
nth_reg += 1;
},
ValueType::F32 => {
args[nth_arg] = Value::F32(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as u32);
nth_reg += 1;
},
ValueType::I64 =>
if caller.instance.is_64_bit() {
args[nth_arg] = Value::I64(caller.instance.reg(Reg::ARG_REGS[nth_reg]) as i64);
nth_reg += 1;
} else {
let value_lo = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
nth_reg += 1;
let value_hi = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
nth_reg += 1;
args[nth_arg] =
Value::I64((u64::from(value_lo) | (u64::from(value_hi) << 32)) as i64);
},
ValueType::F64 =>
if caller.instance.is_64_bit() {
args[nth_arg] = Value::F64(caller.instance.reg(Reg::ARG_REGS[nth_reg]));
nth_reg += 1;
} else {
let value_lo = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
nth_reg += 1;
let value_hi = caller.instance.reg(Reg::ARG_REGS[nth_reg]);
nth_reg += 1;
args[nth_arg] = Value::F64(u64::from(value_lo) | (u64::from(value_hi) << 32));
},
}
}
log::trace!(
"Calling host function: '{}', args = {:?}",
function.name(),
&args[..function.signature().args.len()]
);
let value = match function
.execute(&mut Context(caller), &mut args.into_iter().take(function.signature().args.len()))
{
Ok(value) => value,
Err(error) => {
let name = function.name();
return Err(format!("call into the host function '{name}' failed: {error}"));
},
};
if let Some(value) = value {
match value {
Value::I32(value) => {
caller.instance.set_reg(Reg::A0, value as u64);
},
Value::F32(value) => {
caller.instance.set_reg(Reg::A0, value as u64);
},
Value::I64(value) =>
if caller.instance.is_64_bit() {
caller.instance.set_reg(Reg::A0, value as u64);
} else {
caller.instance.set_reg(Reg::A0, value as u64);
caller.instance.set_reg(Reg::A1, (value >> 32) as u64);
},
Value::F64(value) =>
if caller.instance.is_64_bit() {
caller.instance.set_reg(Reg::A0, value as u64);
} else {
caller.instance.set_reg(Reg::A0, value as u64);
caller.instance.set_reg(Reg::A1, (value >> 32) as u64);
},
}
}
Ok(())
}
pub fn create_runtime<H>(blob: &polkavm::ProgramBlob) -> Result<Box<dyn WasmModule>, WasmError>
where
H: HostFunctions,
{
static ENGINE: std::sync::OnceLock<Result<polkavm::Engine, polkavm::Error>> =
std::sync::OnceLock::new();
let engine = ENGINE.get_or_init(|| {
let config = polkavm::Config::from_env()?;
polkavm::Engine::new(&config)
});
let engine = match engine {
Ok(ref engine) => engine,
Err(ref error) => {
return Err(WasmError::Other(error.to_string()));
},
};
let module =
polkavm::Module::from_blob(&engine, &polkavm::ModuleConfig::default(), blob.clone())?;
let mut linker = polkavm::Linker::new();
for function in H::host_functions() {
linker.define_untyped(function.name(), |mut caller: Caller<()>| {
call_host_function(&mut caller, function)
})?;
}
let instance_pre = linker.instantiate_pre(&module)?;
Ok(Box::new(InstancePre(instance_pre)))
}
@@ -0,0 +1,41 @@
[package]
name = "sc-runtime-test"
version = "2.0.0"
authors.workspace = true
edition.workspace = true
build = "build.rs"
license = "GPL-3.0-or-later WITH Classpath-exception-2.0"
publish = false
homepage.workspace = true
repository.workspace = true
[lints]
workspace = true
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
sp-core = { workspace = true }
sp-io = { features = ["improved_panic_error_reporting"], workspace = true }
sp-runtime = { workspace = true }
sp-runtime-interface = { workspace = true }
[build-dependencies]
substrate-wasm-builder = { optional = true, workspace = true, default-features = true }
[features]
default = ["std"]
std = [
"sp-core/std",
"sp-io/std",
"sp-runtime-interface/std",
"sp-runtime/std",
"substrate-wasm-builder",
]
runtime-benchmarks = [
"sp-io/runtime-benchmarks",
"sp-runtime-interface/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"substrate-wasm-builder?/runtime-benchmarks",
]
@@ -0,0 +1,43 @@
// This file is part of Substrate.
// 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/>.
fn main() {
// regular build
#[cfg(feature = "std")]
{
substrate_wasm_builder::WasmBuilder::new()
.with_current_project()
.export_heap_base()
.import_memory()
.disable_runtime_version_section_check()
.build();
}
// and building with tracing activated
#[cfg(feature = "std")]
{
substrate_wasm_builder::WasmBuilder::new()
.with_current_project()
.export_heap_base()
.import_memory()
.set_file_name("wasm_binary_with_tracing.rs")
.append_to_rust_flags(r#"--cfg feature="with-tracing""#)
.disable_runtime_version_section_check()
.build();
}
}
@@ -0,0 +1,419 @@
// This file is part of Substrate.
// 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/>.
#![cfg_attr(not(feature = "std"), no_std)]
// Make the WASM binary available.
#[cfg(feature = "std")]
include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs"));
/// Wasm binary unwrapped. If built with `SKIP_WASM_BUILD`, the function panics.
#[cfg(feature = "std")]
pub fn wasm_binary_unwrap() -> &'static [u8] {
WASM_BINARY.expect(
"Development wasm binary is not available. Testing is only supported with the flag \
disabled.",
)
}
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{vec, vec::Vec};
#[cfg(not(feature = "std"))]
use sp_core::{ed25519, sr25519};
#[cfg(not(feature = "std"))]
use sp_io::{
crypto::{ed25519_verify, sr25519_verify},
hashing::{blake2_128, blake2_256, sha2_256, twox_128, twox_256},
storage, wasm_tracing,
};
#[cfg(not(feature = "std"))]
use sp_runtime::{
print,
traits::{BlakeTwo256, Hash},
};
extern "C" {
#[allow(dead_code)]
fn missing_external();
#[allow(dead_code)]
fn yet_another_missing_external();
}
#[cfg(not(feature = "std"))]
/// The size of a WASM page in bytes.
const WASM_PAGE_SIZE: usize = 65536;
#[cfg(not(feature = "std"))]
/// Mutable static variables should be always observed to have
/// the initialized value at the start of a runtime call.
static mut MUTABLE_STATIC: u64 = 32;
#[cfg(not(feature = "std"))]
/// This is similar to `MUTABLE_STATIC`. The tests need `MUTABLE_STATIC` for testing that
/// non-null initialization data is properly restored during instance reusing.
///
/// `MUTABLE_STATIC_BSS` on the other hand focuses on the zeroed data. This is important since there
/// may be differences in handling zeroed and non-zeroed data.
static mut MUTABLE_STATIC_BSS: u64 = 0;
sp_core::wasm_export_functions! {
fn test_calling_missing_external() {
unsafe { missing_external() }
}
fn test_calling_yet_another_missing_external() {
unsafe { yet_another_missing_external() }
}
fn test_data_in(input: Vec<u8>) -> Vec<u8> {
print("set_storage");
storage::set(b"input", &input);
print("storage");
let foo = storage::get(b"foo").unwrap();
print("set_storage");
storage::set(b"baz", &foo);
print("finished!");
b"all ok!".to_vec()
}
fn test_clear_prefix(input: Vec<u8>) -> Vec<u8> {
storage::clear_prefix(&input, None);
b"all ok!".to_vec()
}
fn test_empty_return() {}
fn test_dirty_plenty_memory(heap_base: u32, heap_pages: u32) {
// This piece of code will dirty multiple pages of memory. The number of pages is given by
// the `heap_pages`. It's unit is a wasm page (64KiB). The first page to be cleared
// is a wasm page that that follows the one that holds the `heap_base` address.
//
// This function dirties the **host** pages. I.e. we dirty 4KiB at a time and it will take
// 16 writes to process a single wasm page.
let heap_ptr = heap_base as usize;
// Find the next wasm page boundary.
let heap_ptr = round_up_to(heap_ptr, WASM_PAGE_SIZE);
// Make it an actual pointer
let heap_ptr = heap_ptr as *mut u8;
// Traverse the host pages and make each one dirty
let host_pages = heap_pages as usize * 16;
for i in 0..host_pages {
unsafe {
// technically this is an UB, but there is no way Rust can find this out.
heap_ptr.add(i * 4096).write(0);
}
}
fn round_up_to(n: usize, divisor: usize) -> usize {
(n + divisor - 1) / divisor
}
}
fn test_allocate_vec(size: u32) -> Vec<u8> {
Vec::with_capacity(size as usize)
}
fn test_fp_f32add(a: [u8; 4], b: [u8; 4]) -> [u8; 4] {
let a = f32::from_le_bytes(a);
let b = f32::from_le_bytes(b);
f32::to_le_bytes(a + b)
}
fn test_panic() { panic!("test panic") }
fn test_conditional_panic(input: Vec<u8>) -> Vec<u8> {
if input.len() > 0 {
panic!("test panic")
}
input
}
fn test_blake2_256(input: Vec<u8>) -> Vec<u8> {
blake2_256(&input).to_vec()
}
fn test_blake2_128(input: Vec<u8>) -> Vec<u8> {
blake2_128(&input).to_vec()
}
fn test_sha2_256(input: Vec<u8>) -> Vec<u8> {
sha2_256(&input).to_vec()
}
fn test_twox_256(input: Vec<u8>) -> Vec<u8> {
twox_256(&input).to_vec()
}
fn test_twox_128(input: Vec<u8>) -> Vec<u8> {
twox_128(&input).to_vec()
}
fn test_ed25519_verify(input: Vec<u8>) -> bool {
let mut pubkey = [0; 32];
let mut sig = [0; 64];
pubkey.copy_from_slice(&input[0..32]);
sig.copy_from_slice(&input[32..96]);
let msg = b"all ok!";
ed25519_verify(&ed25519::Signature::from(sig), &msg[..], &ed25519::Public::from(pubkey))
}
fn test_sr25519_verify(input: Vec<u8>) -> bool {
let mut pubkey = [0; 32];
let mut sig = [0; 64];
pubkey.copy_from_slice(&input[0..32]);
sig.copy_from_slice(&input[32..96]);
let msg = b"all ok!";
sr25519_verify(&sr25519::Signature::from(sig), &msg[..], &sr25519::Public::from(pubkey))
}
fn test_ordered_trie_root() -> Vec<u8> {
BlakeTwo256::ordered_trie_root(
vec![
b"zero"[..].into(),
b"one"[..].into(),
b"two"[..].into(),
],
sp_core::storage::StateVersion::V1,
).as_ref().to_vec()
}
fn test_offchain_index_set() {
sp_io::offchain_index::set(b"k", b"v");
}
fn test_offchain_local_storage() -> bool {
let kind = sp_core::offchain::StorageKind::PERSISTENT;
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), None);
sp_io::offchain::local_storage_set(kind, b"test", b"asd");
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), Some(b"asd".to_vec()));
let res = sp_io::offchain::local_storage_compare_and_set(
kind,
b"test",
Some(b"asd".to_vec()),
b"",
);
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), Some(b"".to_vec()));
res
}
fn test_offchain_local_storage_with_none() {
let kind = sp_core::offchain::StorageKind::PERSISTENT;
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), None);
let res = sp_io::offchain::local_storage_compare_and_set(kind, b"test", None, b"value");
assert_eq!(res, true);
assert_eq!(sp_io::offchain::local_storage_get(kind, b"test"), Some(b"value".to_vec()));
}
fn test_offchain_http() -> bool {
use sp_core::offchain::HttpRequestStatus;
let run = || -> Option<()> {
let id = sp_io::offchain::http_request_start(
"POST",
"http://localhost:12345",
&[],
).ok()?;
sp_io::offchain::http_request_add_header(id, "X-Auth", "test").ok()?;
sp_io::offchain::http_request_write_body(id, &[1, 2, 3, 4], None).ok()?;
sp_io::offchain::http_request_write_body(id, &[], None).ok()?;
let status = sp_io::offchain::http_response_wait(&[id], None);
assert!(status == vec![HttpRequestStatus::Finished(200)], "Expected Finished(200) status.");
let headers = sp_io::offchain::http_response_headers(id);
assert_eq!(headers, vec![(b"X-Auth".to_vec(), b"hello".to_vec())]);
let mut buffer = vec![0; 64];
let read = sp_io::offchain::http_response_read_body(id, &mut buffer, None).ok()?;
assert_eq!(read, 3);
assert_eq!(&buffer[0..read as usize], &[1, 2, 3]);
let read = sp_io::offchain::http_response_read_body(id, &mut buffer, None).ok()?;
assert_eq!(read, 0);
Some(())
};
run().is_some()
}
fn test_enter_span() -> u64 {
wasm_tracing::enter_span(Default::default())
}
fn test_exit_span(span_id: u64) {
wasm_tracing::exit(span_id)
}
fn test_nested_spans() {
sp_io::init_tracing();
let span_id = wasm_tracing::enter_span(Default::default());
{
sp_io::init_tracing();
let span_id = wasm_tracing::enter_span(Default::default());
wasm_tracing::exit(span_id);
}
wasm_tracing::exit(span_id);
}
fn returns_mutable_static() -> u64 {
unsafe {
MUTABLE_STATIC += 1;
MUTABLE_STATIC
}
}
fn returns_mutable_static_bss() -> u64 {
unsafe {
MUTABLE_STATIC_BSS += 1;
MUTABLE_STATIC_BSS
}
}
fn allocates_huge_stack_array(trap: bool) -> Vec<u8> {
// Allocate a stack frame that is approx. 75% of the stack (assuming it is 1MB).
// This will just decrease (stacks in wasm32-u-u grow downwards) the stack
// pointer. This won't trap on the current compilers.
let mut data = [0u8; 1024 * 768];
// Then make sure we actually write something to it.
//
// If:
// 1. the stack area is placed at the beginning of the linear memory space, and
// 2. the stack pointer points to out-of-bounds area, and
// 3. a write is performed around the current stack pointer.
//
// then a trap should happen.
//
for (i, v) in data.iter_mut().enumerate() {
*v = i as u8; // deliberate truncation
}
if trap {
// There is a small chance of this to be pulled up in theory. In practice
// the probability of that is rather low.
panic!()
}
data.to_vec()
}
// Check that the heap at `heap_base + offset` don't contains the test message.
// After the check succeeds the test message is written into the heap.
//
// It is expected that the given pointer is not allocated.
fn check_and_set_in_heap(heap_base: u32, offset: u32) {
let test_message = b"Hello invalid heap memory";
let ptr = (heap_base + offset) as *mut u8;
let message_slice = unsafe { alloc::slice::from_raw_parts_mut(ptr, test_message.len()) };
assert_ne!(test_message, message_slice);
message_slice.copy_from_slice(test_message);
}
fn test_return_i8() -> i8 {
-66
}
fn test_take_i8(value: i8) {
assert_eq!(value, -66);
}
fn allocate_two_gigabyte() -> u32 {
let mut data = Vec::new();
for _ in 0..205 {
data.push(Vec::<u8>::with_capacity(10 * 1024 * 1024));
}
data.iter().map(|d| d.capacity() as u32).sum()
}
fn test_abort_on_panic() {
sp_io::panic_handler::abort_on_panic("test_abort_on_panic called");
}
fn test_unreachable_intrinsic() {
core::arch::wasm32::unreachable()
}
fn test_return_value() -> u64 {
// Mainly a test that the macro is working when we have a return statement here.
return 1234;
}
}
// Tests that check output validity. We explicitly return the ptr and len, so we avoid using the
// `wasm_export_functions` macro.
mod output_validity {
#[cfg(not(feature = "std"))]
use super::WASM_PAGE_SIZE;
#[cfg(not(feature = "std"))]
use sp_runtime_interface::pack_ptr_and_len;
// Returns a huge len. It should result in an error, and not an allocation.
#[no_mangle]
#[cfg(not(feature = "std"))]
pub extern "C" fn test_return_huge_len(_params: *const u8, _len: usize) -> u64 {
pack_ptr_and_len(0, u32::MAX)
}
// Returns an offset right before the edge of the wasm memory boundary. It should succeed.
#[no_mangle]
#[cfg(not(feature = "std"))]
pub extern "C" fn test_return_max_memory_offset(_params: *const u8, _len: usize) -> u64 {
let output_ptr = (core::arch::wasm32::memory_size(0) * WASM_PAGE_SIZE) as u32 - 1;
let ptr = output_ptr as *mut u8;
unsafe {
ptr.write(u8::MAX);
}
pack_ptr_and_len(output_ptr, 1)
}
// Returns an offset right after the edge of the wasm memory boundary. It should fail.
#[no_mangle]
#[cfg(not(feature = "std"))]
pub extern "C" fn test_return_max_memory_offset_plus_one(
_params: *const u8,
_len: usize,
) -> u64 {
pack_ptr_and_len((core::arch::wasm32::memory_size(0) * WASM_PAGE_SIZE) as u32, 1)
}
// Returns an output that overflows the u32 range. It should result in an error.
#[no_mangle]
#[cfg(not(feature = "std"))]
pub extern "C" fn test_return_overflow(_params: *const u8, _len: usize) -> u64 {
pack_ptr_and_len(u32::MAX, 1)
}
}
+809
View File
@@ -0,0 +1,809 @@
// This file is part of Substrate.
// 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::{
error::{Error, Result},
wasm_runtime::{RuntimeCache, WasmExecutionMethod},
RuntimeVersionOf,
};
use std::{
marker::PhantomData,
panic::{AssertUnwindSafe, UnwindSafe},
path::PathBuf,
sync::Arc,
};
use codec::Encode;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{
AllocationStats, HeapAllocStrategy, WasmInstance, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY,
},
};
use sp_core::traits::{CallContext, CodeExecutor, Externalities, RuntimeCode};
use sp_version::{GetNativeVersion, NativeVersion, RuntimeVersion};
use sp_wasm_interface::{ExtendedHostFunctions, HostFunctions};
/// Set up the externalities and safe calling environment to execute runtime calls.
///
/// If the inner closure panics, it will be caught and return an error.
pub fn with_externalities_safe<F, U>(ext: &mut dyn Externalities, f: F) -> Result<U>
where
F: UnwindSafe + FnOnce() -> U,
{
sp_externalities::set_and_run_with_externalities(ext, move || {
// Substrate uses custom panic hook that terminates process on panic. Disable
// termination for the native call.
let _guard = sp_panic_handler::AbortGuard::force_unwind();
std::panic::catch_unwind(f).map_err(|e| {
if let Some(err) = e.downcast_ref::<String>() {
Error::RuntimePanicked(err.clone())
} else if let Some(err) = e.downcast_ref::<&'static str>() {
Error::RuntimePanicked(err.to_string())
} else {
Error::RuntimePanicked("Unknown panic".into())
}
})
})
}
/// Delegate for dispatching a CodeExecutor call.
///
/// By dispatching we mean that we execute a runtime function specified by it's name.
pub trait NativeExecutionDispatch: Send + Sync {
/// Host functions for custom runtime interfaces that should be callable from within the runtime
/// besides the default Substrate runtime interfaces.
type ExtendHostFunctions: HostFunctions;
/// Dispatch a method in the runtime.
fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>>;
/// Provide native runtime version.
fn native_version() -> NativeVersion;
}
fn unwrap_heap_pages(pages: Option<HeapAllocStrategy>) -> HeapAllocStrategy {
pages.unwrap_or_else(|| DEFAULT_HEAP_ALLOC_STRATEGY)
}
/// Builder for creating a [`WasmExecutor`] instance.
pub struct WasmExecutorBuilder<H = sp_io::SubstrateHostFunctions> {
_phantom: PhantomData<H>,
method: WasmExecutionMethod,
onchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
offchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
ignore_onchain_heap_pages: bool,
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
allow_missing_host_functions: bool,
runtime_cache_size: u8,
}
impl<H> WasmExecutorBuilder<H> {
/// Create a new instance of `Self`
///
/// - `method`: The wasm execution method that should be used by the executor.
pub fn new() -> Self {
Self {
_phantom: PhantomData,
method: WasmExecutionMethod::default(),
onchain_heap_alloc_strategy: None,
offchain_heap_alloc_strategy: None,
ignore_onchain_heap_pages: false,
max_runtime_instances: 2,
runtime_cache_size: 4,
allow_missing_host_functions: false,
cache_path: None,
}
}
/// Create the wasm executor with execution method that should be used by the executor.
pub fn with_execution_method(mut self, method: WasmExecutionMethod) -> Self {
self.method = method;
self
}
/// Create the wasm executor with the given number of `heap_alloc_strategy` for onchain runtime
/// calls.
pub fn with_onchain_heap_alloc_strategy(
mut self,
heap_alloc_strategy: HeapAllocStrategy,
) -> Self {
self.onchain_heap_alloc_strategy = Some(heap_alloc_strategy);
self
}
/// Create the wasm executor with the given number of `heap_alloc_strategy` for offchain runtime
/// calls.
pub fn with_offchain_heap_alloc_strategy(
mut self,
heap_alloc_strategy: HeapAllocStrategy,
) -> Self {
self.offchain_heap_alloc_strategy = Some(heap_alloc_strategy);
self
}
/// Create the wasm executor and follow/ignore onchain heap pages value.
///
/// By default this the onchain heap pages value is followed.
pub fn with_ignore_onchain_heap_pages(mut self, ignore_onchain_heap_pages: bool) -> Self {
self.ignore_onchain_heap_pages = ignore_onchain_heap_pages;
self
}
/// Create the wasm executor with the given maximum number of `instances`.
///
/// The number of `instances` defines how many different instances of a runtime the cache is
/// storing.
///
/// By default the maximum number of `instances` is `2`.
pub fn with_max_runtime_instances(mut self, instances: usize) -> Self {
self.max_runtime_instances = instances;
self
}
/// Create the wasm executor with the given `cache_path`.
///
/// The `cache_path` is A path to a directory where the executor can place its files for
/// purposes of caching. This may be important in cases when there are many different modules
/// with the compiled execution method is used.
///
/// By default there is no `cache_path` given.
pub fn with_cache_path(mut self, cache_path: impl Into<PathBuf>) -> Self {
self.cache_path = Some(cache_path.into());
self
}
/// Create the wasm executor and allow/forbid missing host functions.
///
/// If missing host functions are forbidden, the instantiation of a wasm blob will fail
/// for imported host functions that the executor is not aware of. If they are allowed,
/// a stub is generated that will return an error when being called while executing the wasm.
///
/// By default missing host functions are forbidden.
pub fn with_allow_missing_host_functions(mut self, allow: bool) -> Self {
self.allow_missing_host_functions = allow;
self
}
/// Create the wasm executor with the given `runtime_cache_size`.
///
/// Defines the number of different runtimes/instantiated wasm blobs the cache stores.
/// Runtimes/wasm blobs are differentiated based on the hash and the number of heap pages.
///
/// By default this value is set to `4`.
pub fn with_runtime_cache_size(mut self, runtime_cache_size: u8) -> Self {
self.runtime_cache_size = runtime_cache_size;
self
}
/// Build the configured [`WasmExecutor`].
pub fn build(self) -> WasmExecutor<H> {
WasmExecutor {
method: self.method,
default_offchain_heap_alloc_strategy: unwrap_heap_pages(
self.offchain_heap_alloc_strategy,
),
default_onchain_heap_alloc_strategy: unwrap_heap_pages(
self.onchain_heap_alloc_strategy,
),
ignore_onchain_heap_pages: self.ignore_onchain_heap_pages,
cache: Arc::new(RuntimeCache::new(
self.max_runtime_instances,
self.cache_path.clone(),
self.runtime_cache_size,
)),
cache_path: self.cache_path,
allow_missing_host_functions: self.allow_missing_host_functions,
phantom: PhantomData,
}
}
}
/// An abstraction over Wasm code executor. Supports selecting execution backend and
/// manages runtime cache.
pub struct WasmExecutor<H = sp_io::SubstrateHostFunctions> {
/// Method used to execute fallback Wasm code.
method: WasmExecutionMethod,
/// The heap allocation strategy for onchain Wasm calls.
default_onchain_heap_alloc_strategy: HeapAllocStrategy,
/// The heap allocation strategy for offchain Wasm calls.
default_offchain_heap_alloc_strategy: HeapAllocStrategy,
/// Ignore onchain heap pages value.
ignore_onchain_heap_pages: bool,
/// WASM runtime cache.
cache: Arc<RuntimeCache>,
/// The path to a directory which the executor can leverage for a file cache, e.g. put there
/// compiled artifacts.
cache_path: Option<PathBuf>,
/// Ignore missing function imports.
allow_missing_host_functions: bool,
phantom: PhantomData<H>,
}
impl<H> Clone for WasmExecutor<H> {
fn clone(&self) -> Self {
Self {
method: self.method,
default_onchain_heap_alloc_strategy: self.default_onchain_heap_alloc_strategy,
default_offchain_heap_alloc_strategy: self.default_offchain_heap_alloc_strategy,
ignore_onchain_heap_pages: self.ignore_onchain_heap_pages,
cache: self.cache.clone(),
cache_path: self.cache_path.clone(),
allow_missing_host_functions: self.allow_missing_host_functions,
phantom: self.phantom,
}
}
}
impl Default for WasmExecutor<sp_io::SubstrateHostFunctions> {
fn default() -> Self {
WasmExecutorBuilder::new().build()
}
}
impl<H> WasmExecutor<H> {
/// Create new instance.
///
/// # Parameters
///
/// `method` - Method used to execute Wasm code.
///
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution. Internally this
/// will be mapped as [`HeapAllocStrategy::Static`] where `default_heap_pages` represent the
/// static number of heap pages to allocate. Defaults to `DEFAULT_HEAP_ALLOC_STRATEGY` if `None`
/// is provided.
///
/// `max_runtime_instances` - The number of runtime instances to keep in memory ready for reuse.
///
/// `cache_path` - A path to a directory where the executor can place its files for purposes of
/// caching. This may be important in cases when there are many different modules with the
/// compiled execution method is used.
///
/// `runtime_cache_size` - The capacity of runtime cache.
#[deprecated(note = "use `Self::builder` method instead of it")]
pub fn new(
method: WasmExecutionMethod,
default_heap_pages: Option<u64>,
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
runtime_cache_size: u8,
) -> Self {
WasmExecutor {
method,
default_onchain_heap_alloc_strategy: unwrap_heap_pages(
default_heap_pages.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ }),
),
default_offchain_heap_alloc_strategy: unwrap_heap_pages(
default_heap_pages.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ }),
),
ignore_onchain_heap_pages: false,
cache: Arc::new(RuntimeCache::new(
max_runtime_instances,
cache_path.clone(),
runtime_cache_size,
)),
cache_path,
allow_missing_host_functions: false,
phantom: PhantomData,
}
}
/// Instantiate a builder for creating an instance of `Self`.
pub fn builder() -> WasmExecutorBuilder<H> {
WasmExecutorBuilder::new()
}
/// Ignore missing function imports if set true.
#[deprecated(note = "use `Self::builder` method instead of it")]
pub fn allow_missing_host_functions(&mut self, allow_missing_host_functions: bool) {
self.allow_missing_host_functions = allow_missing_host_functions
}
}
impl<H> WasmExecutor<H>
where
H: HostFunctions,
{
/// Execute the given closure `f` with the latest runtime (based on `runtime_code`).
///
/// The closure `f` is expected to return `Err(_)` when there happened a `panic!` in native code
/// while executing the runtime in Wasm. If a `panic!` occurred, the runtime is invalidated to
/// prevent any poisoned state. Native runtime execution does not need to report back
/// any `panic!`.
///
/// # Safety
///
/// `runtime` and `ext` are given as `AssertUnwindSafe` to the closure. As described above, the
/// runtime is invalidated on any `panic!` to prevent a poisoned state. `ext` is already
/// implicitly handled as unwind safe, as we store it in a global variable while executing the
/// native runtime.
pub fn with_instance<R, F>(
&self,
runtime_code: &RuntimeCode,
ext: &mut dyn Externalities,
heap_alloc_strategy: HeapAllocStrategy,
f: F,
) -> Result<R>
where
F: FnOnce(
AssertUnwindSafe<&dyn WasmModule>,
AssertUnwindSafe<&mut dyn WasmInstance>,
Option<&RuntimeVersion>,
AssertUnwindSafe<&mut dyn Externalities>,
) -> Result<Result<R>>,
{
match self.cache.with_instance::<H, _, _>(
runtime_code,
ext,
self.method,
heap_alloc_strategy,
self.allow_missing_host_functions,
|module, instance, version, ext| {
let module = AssertUnwindSafe(module);
let instance = AssertUnwindSafe(instance);
let ext = AssertUnwindSafe(ext);
f(module, instance, version, ext)
},
)? {
Ok(r) => r,
Err(e) => Err(e),
}
}
/// Perform a call into the given runtime.
///
/// The runtime is passed as a [`RuntimeBlob`]. The runtime will be instantiated with the
/// parameters this `WasmExecutor` was initialized with.
///
/// In case of problems with during creation of the runtime or instantiation, a `Err` is
/// returned. that describes the message.
#[doc(hidden)] // We use this function for tests across multiple crates.
pub fn uncached_call(
&self,
runtime_blob: RuntimeBlob,
ext: &mut dyn Externalities,
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
) -> std::result::Result<Vec<u8>, Error> {
self.uncached_call_impl(
runtime_blob,
ext,
allow_missing_host_functions,
export_name,
call_data,
&mut None,
)
}
/// Same as `uncached_call`, except it also returns allocation statistics.
#[doc(hidden)] // We use this function in tests.
pub fn uncached_call_with_allocation_stats(
&self,
runtime_blob: RuntimeBlob,
ext: &mut dyn Externalities,
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
) -> (std::result::Result<Vec<u8>, Error>, Option<AllocationStats>) {
let mut allocation_stats = None;
let result = self.uncached_call_impl(
runtime_blob,
ext,
allow_missing_host_functions,
export_name,
call_data,
&mut allocation_stats,
);
(result, allocation_stats)
}
fn uncached_call_impl(
&self,
runtime_blob: RuntimeBlob,
ext: &mut dyn Externalities,
allow_missing_host_functions: bool,
export_name: &str,
call_data: &[u8],
allocation_stats_out: &mut Option<AllocationStats>,
) -> std::result::Result<Vec<u8>, Error> {
let module = crate::wasm_runtime::create_wasm_runtime_with_code::<H>(
self.method,
self.default_onchain_heap_alloc_strategy,
runtime_blob,
allow_missing_host_functions,
self.cache_path.as_deref(),
)
.map_err(|e| format!("Failed to create module: {}", e))?;
let instance =
module.new_instance().map_err(|e| format!("Failed to create instance: {}", e))?;
let mut instance = AssertUnwindSafe(instance);
let mut ext = AssertUnwindSafe(ext);
let mut allocation_stats_out = AssertUnwindSafe(allocation_stats_out);
with_externalities_safe(&mut **ext, move || {
let (result, allocation_stats) =
instance.call_with_allocation_stats(export_name.into(), call_data);
**allocation_stats_out = allocation_stats;
result
})
.and_then(|r| r)
}
}
impl<H> sp_core::traits::ReadRuntimeVersion for WasmExecutor<H>
where
H: HostFunctions,
{
fn read_runtime_version(
&self,
wasm_code: &[u8],
ext: &mut dyn Externalities,
) -> std::result::Result<Vec<u8>, String> {
let runtime_blob = RuntimeBlob::uncompress_if_needed(wasm_code)
.map_err(|e| format!("Failed to create runtime blob: {:?}", e))?;
if let Some(version) = crate::wasm_runtime::read_embedded_version(&runtime_blob)
.map_err(|e| format!("Failed to read the static section: {:?}", e))
.map(|v| v.map(|v| v.encode()))?
{
return Ok(version);
}
// If the blob didn't have embedded runtime version section, we fallback to the legacy
// way of fetching the version: i.e. instantiating the given instance and calling
// `Core_version` on it.
self.uncached_call(
runtime_blob,
ext,
// If a runtime upgrade introduces new host functions that are not provided by
// the node, we should not fail at instantiation. Otherwise nodes that are
// updated could run this successfully and it could lead to a storage root
// mismatch when importing this block.
true,
"Core_version",
&[],
)
.map_err(|e| e.to_string())
}
}
impl<H> CodeExecutor for WasmExecutor<H>
where
H: HostFunctions,
{
type Error = Error;
fn call(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
method: &str,
data: &[u8],
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
tracing::trace!(
target: "executor",
%method,
"Executing function",
);
let on_chain_heap_alloc_strategy = if self.ignore_onchain_heap_pages {
self.default_onchain_heap_alloc_strategy
} else {
runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.default_onchain_heap_alloc_strategy)
};
let heap_alloc_strategy = match context {
CallContext::Offchain => self.default_offchain_heap_alloc_strategy,
CallContext::Onchain => on_chain_heap_alloc_strategy,
};
let result = self.with_instance(
runtime_code,
ext,
heap_alloc_strategy,
|_, mut instance, _on_chain_version, mut ext| {
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
},
);
(result, false)
}
}
impl<H> RuntimeVersionOf for WasmExecutor<H>
where
H: HostFunctions,
{
fn runtime_version(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
let on_chain_heap_pages = if self.ignore_onchain_heap_pages {
self.default_onchain_heap_alloc_strategy
} else {
runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.default_onchain_heap_alloc_strategy)
};
self.with_instance(
runtime_code,
ext,
on_chain_heap_pages,
|_module, _instance, version, _ext| {
Ok(version.cloned().ok_or_else(|| Error::ApiError("Unknown version".into())))
},
)
}
}
/// A generic `CodeExecutor` implementation that uses a delegate to determine wasm code equivalence
/// and dispatch to native code when possible, falling back on `WasmExecutor` when not.
#[deprecated(
note = "Native execution will be deprecated, please replace with `WasmExecutor`. Will be removed at end of 2024."
)]
pub struct NativeElseWasmExecutor<D: NativeExecutionDispatch> {
/// Native runtime version info.
native_version: NativeVersion,
/// Fallback wasm executor.
wasm:
WasmExecutor<ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>>,
use_native: bool,
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
///
/// Create new instance.
///
/// # Parameters
///
/// `fallback_method` - Method used to execute fallback Wasm code.
///
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution. Internally this
/// will be mapped as [`HeapAllocStrategy::Static`] where `default_heap_pages` represent the
/// static number of heap pages to allocate. Defaults to `DEFAULT_HEAP_ALLOC_STRATEGY` if `None`
/// is provided.
///
/// `max_runtime_instances` - The number of runtime instances to keep in memory ready for reuse.
///
/// `runtime_cache_size` - The capacity of runtime cache.
#[deprecated(note = "use `Self::new_with_wasm_executor` method instead of it")]
pub fn new(
fallback_method: WasmExecutionMethod,
default_heap_pages: Option<u64>,
max_runtime_instances: usize,
runtime_cache_size: u8,
) -> Self {
let heap_pages = default_heap_pages.map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| {
HeapAllocStrategy::Static { extra_pages: h as _ }
});
let wasm = WasmExecutor::builder()
.with_execution_method(fallback_method)
.with_onchain_heap_alloc_strategy(heap_pages)
.with_offchain_heap_alloc_strategy(heap_pages)
.with_max_runtime_instances(max_runtime_instances)
.with_runtime_cache_size(runtime_cache_size)
.build();
NativeElseWasmExecutor { native_version: D::native_version(), wasm, use_native: true }
}
/// Create a new instance using the given [`WasmExecutor`].
pub fn new_with_wasm_executor(
executor: WasmExecutor<
ExtendedHostFunctions<sp_io::SubstrateHostFunctions, D::ExtendHostFunctions>,
>,
) -> Self {
Self { native_version: D::native_version(), wasm: executor, use_native: true }
}
/// Disable to use native runtime when possible just behave like `WasmExecutor`.
///
/// Default to enabled.
pub fn disable_use_native(&mut self) {
self.use_native = false;
}
/// Ignore missing function imports if set true.
#[deprecated(note = "use `Self::new_with_wasm_executor` method instead of it")]
pub fn allow_missing_host_functions(&mut self, allow_missing_host_functions: bool) {
self.wasm.allow_missing_host_functions = allow_missing_host_functions
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> RuntimeVersionOf for NativeElseWasmExecutor<D> {
fn runtime_version(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
self.wasm.runtime_version(ext, runtime_code)
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> GetNativeVersion for NativeElseWasmExecutor<D> {
fn native_version(&self) -> &NativeVersion {
&self.native_version
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecutor<D> {
type Error = Error;
fn call(
&self,
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
method: &str,
data: &[u8],
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
let use_native = self.use_native;
tracing::trace!(
target: "executor",
function = %method,
"Executing function",
);
let on_chain_heap_alloc_strategy = if self.wasm.ignore_onchain_heap_pages {
self.wasm.default_onchain_heap_alloc_strategy
} else {
runtime_code
.heap_pages
.map(|h| HeapAllocStrategy::Static { extra_pages: h as _ })
.unwrap_or_else(|| self.wasm.default_onchain_heap_alloc_strategy)
};
let heap_alloc_strategy = match context {
CallContext::Offchain => self.wasm.default_offchain_heap_alloc_strategy,
CallContext::Onchain => on_chain_heap_alloc_strategy,
};
let mut used_native = false;
let result = self.wasm.with_instance(
runtime_code,
ext,
heap_alloc_strategy,
|_, mut instance, on_chain_version, mut ext| {
let on_chain_version =
on_chain_version.ok_or_else(|| Error::ApiError("Unknown version".into()))?;
let can_call_with =
on_chain_version.can_call_with(&self.native_version.runtime_version);
if use_native && can_call_with {
tracing::trace!(
target: "executor",
native = %self.native_version.runtime_version,
chain = %on_chain_version,
"Request for native execution succeeded",
);
used_native = true;
Ok(with_externalities_safe(&mut **ext, move || D::dispatch(method, data))?
.ok_or_else(|| Error::MethodNotFound(method.to_owned())))
} else {
if !can_call_with {
tracing::trace!(
target: "executor",
native = %self.native_version.runtime_version,
chain = %on_chain_version,
"Request for native execution failed",
);
}
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
}
},
);
(result, used_native)
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> Clone for NativeElseWasmExecutor<D> {
fn clone(&self) -> Self {
NativeElseWasmExecutor {
native_version: D::native_version(),
wasm: self.wasm.clone(),
use_native: self.use_native,
}
}
}
#[allow(deprecated)]
impl<D: NativeExecutionDispatch> sp_core::traits::ReadRuntimeVersion for NativeElseWasmExecutor<D> {
fn read_runtime_version(
&self,
wasm_code: &[u8],
ext: &mut dyn Externalities,
) -> std::result::Result<Vec<u8>, String> {
self.wasm.read_runtime_version(wasm_code, ext)
}
}
#[cfg(test)]
mod tests {
use super::*;
use sp_runtime_interface::{pass_by::PassFatPointerAndRead, runtime_interface};
#[runtime_interface]
trait MyInterface {
fn say_hello_world(data: PassFatPointerAndRead<&str>) {
println!("Hello world from: {}", data);
}
}
pub struct MyExecutorDispatch;
impl NativeExecutionDispatch for MyExecutorDispatch {
type ExtendHostFunctions = (my_interface::HostFunctions, my_interface::HostFunctions);
fn dispatch(method: &str, data: &[u8]) -> Option<Vec<u8>> {
substrate_test_runtime::api::dispatch(method, data)
}
fn native_version() -> NativeVersion {
substrate_test_runtime::native_version()
}
}
#[test]
#[allow(deprecated)]
fn native_executor_registers_custom_interface() {
let executor = NativeElseWasmExecutor::<MyExecutorDispatch>::new_with_wasm_executor(
WasmExecutor::builder().build(),
);
fn extract_host_functions<H>(
_: &WasmExecutor<H>,
) -> Vec<&'static dyn sp_wasm_interface::Function>
where
H: HostFunctions,
{
H::host_functions()
}
my_interface::HostFunctions::host_functions().iter().for_each(|function| {
assert_eq!(
extract_host_functions(&executor.wasm).iter().filter(|f| f == &function).count(),
2
);
});
my_interface::say_hello_world("hey");
}
}
@@ -0,0 +1,801 @@
// This file is part of Substrate.
// 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 assert_matches::assert_matches;
use codec::{Decode, Encode};
use sc_executor_common::{
error::Error,
runtime_blob::RuntimeBlob,
wasm_runtime::{HeapAllocStrategy, WasmModule},
};
use sc_runtime_test::wasm_binary_unwrap;
use sp_core::{
ed25519, map,
offchain::{testing, OffchainDbExt, OffchainWorkerExt},
sr25519,
traits::Externalities,
Pair,
};
use sp_crypto_hashing::{blake2_128, blake2_256, sha2_256, twox_128, twox_256};
use sp_runtime::traits::BlakeTwo256;
use sp_state_machine::TestExternalities as CoreTestExternalities;
use sp_trie::{LayoutV1 as Layout, TrieConfiguration};
use std::sync::Arc;
use tracing_subscriber::layer::SubscriberExt;
use crate::WasmExecutionMethod;
pub type TestExternalities = CoreTestExternalities<BlakeTwo256>;
type HostFunctions = sp_io::SubstrateHostFunctions;
/// Simple macro that runs a given method as test with the available wasm execution methods.
#[macro_export]
macro_rules! test_wasm_execution {
($method_name:ident) => {
paste::item! {
#[test]
fn [<$method_name _compiled_recreate_instance_cow>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstanceCopyOnWrite
});
}
#[test]
fn [<$method_name _compiled_recreate_instance_vanilla>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstance
});
}
#[test]
fn [<$method_name _compiled_pooling_cow>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite
});
}
#[test]
fn [<$method_name _compiled_pooling_vanilla>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::Pooling
});
}
}
};
}
fn call_in_wasm<E: Externalities>(
function: &str,
call_data: &[u8],
execution_method: WasmExecutionMethod,
ext: &mut E,
) -> Result<Vec<u8>, Error> {
let executor = crate::WasmExecutor::<HostFunctions>::builder()
.with_execution_method(execution_method)
.build();
executor.uncached_call(
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
ext,
true,
function,
call_data,
)
}
test_wasm_execution!(returning_should_work);
fn returning_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
let output = call_in_wasm("test_empty_return", &[], wasm_method, &mut ext).unwrap();
assert_eq!(output, vec![0u8; 0]);
}
test_wasm_execution!(call_not_existing_function);
fn call_not_existing_function(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
match call_in_wasm("test_calling_missing_external", &[], wasm_method, &mut ext).unwrap_err() {
Error::AbortedDueToTrap(error) => {
let expected = match wasm_method {
WasmExecutionMethod::Compiled { .. } =>
"call to a missing function env:missing_external",
};
assert_eq!(error.message, expected);
},
error => panic!("unexpected error: {:?}", error),
}
}
test_wasm_execution!(call_yet_another_not_existing_function);
fn call_yet_another_not_existing_function(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
match call_in_wasm("test_calling_yet_another_missing_external", &[], wasm_method, &mut ext)
.unwrap_err()
{
Error::AbortedDueToTrap(error) => {
let expected = match wasm_method {
WasmExecutionMethod::Compiled { .. } =>
"call to a missing function env:yet_another_missing_external",
};
assert_eq!(error.message, expected);
},
error => panic!("unexpected error: {:?}", error),
}
}
test_wasm_execution!(panicking_should_work);
fn panicking_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
let output = call_in_wasm("test_panic", &[], wasm_method, &mut ext);
assert!(output.is_err());
let output = call_in_wasm("test_conditional_panic", &[0], wasm_method, &mut ext);
assert_eq!(Decode::decode(&mut &output.unwrap()[..]), Ok(Vec::<u8>::new()));
let output = call_in_wasm("test_conditional_panic", &vec![2].encode(), wasm_method, &mut ext);
assert!(output.is_err());
}
test_wasm_execution!(storage_should_work);
fn storage_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
// Test value must be bigger than 32 bytes
// to test the trie versioning.
let value = vec![7u8; 60];
{
let mut ext = ext.ext();
ext.set_storage(b"foo".to_vec(), b"bar".to_vec());
let output = call_in_wasm("test_data_in", &value.encode(), wasm_method, &mut ext).unwrap();
assert_eq!(output, b"all ok!".to_vec().encode());
}
let mut expected = TestExternalities::new(sp_core::storage::Storage {
top: map![
b"input".to_vec() => value,
b"foo".to_vec() => b"bar".to_vec(),
b"baz".to_vec() => b"bar".to_vec()
],
children_default: map![],
});
assert!(ext.eq(&mut expected));
}
test_wasm_execution!(clear_prefix_should_work);
fn clear_prefix_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
{
let mut ext = ext.ext();
ext.set_storage(b"aaa".to_vec(), b"1".to_vec());
ext.set_storage(b"aab".to_vec(), b"2".to_vec());
ext.set_storage(b"aba".to_vec(), b"3".to_vec());
ext.set_storage(b"abb".to_vec(), b"4".to_vec());
ext.set_storage(b"bbb".to_vec(), b"5".to_vec());
// This will clear all entries which prefix is "ab".
let output =
call_in_wasm("test_clear_prefix", &b"ab".to_vec().encode(), wasm_method, &mut ext)
.unwrap();
assert_eq!(output, b"all ok!".to_vec().encode());
}
let mut expected = TestExternalities::new(sp_core::storage::Storage {
top: map![
b"aaa".to_vec() => b"1".to_vec(),
b"aab".to_vec() => b"2".to_vec(),
b"bbb".to_vec() => b"5".to_vec()
],
children_default: map![],
});
assert!(expected.eq(&mut ext));
}
test_wasm_execution!(blake2_256_should_work);
fn blake2_256_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_blake2_256", &[0], wasm_method, &mut ext,).unwrap(),
blake2_256(b"").to_vec().encode(),
);
assert_eq!(
call_in_wasm("test_blake2_256", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
.unwrap(),
blake2_256(b"Hello world!").to_vec().encode(),
);
}
test_wasm_execution!(blake2_128_should_work);
fn blake2_128_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_blake2_128", &[0], wasm_method, &mut ext,).unwrap(),
blake2_128(b"").to_vec().encode(),
);
assert_eq!(
call_in_wasm("test_blake2_128", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
.unwrap(),
blake2_128(b"Hello world!").to_vec().encode(),
);
}
test_wasm_execution!(sha2_256_should_work);
fn sha2_256_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_sha2_256", &[0], wasm_method, &mut ext,).unwrap(),
sha2_256(b"").to_vec().encode(),
);
assert_eq!(
call_in_wasm("test_sha2_256", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
.unwrap(),
sha2_256(b"Hello world!").to_vec().encode(),
);
}
test_wasm_execution!(twox_256_should_work);
fn twox_256_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_twox_256", &[0], wasm_method, &mut ext,).unwrap(),
twox_256(b"").to_vec().encode()
);
assert_eq!(
call_in_wasm("test_twox_256", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
.unwrap(),
twox_256(b"Hello world!").to_vec().encode()
);
}
test_wasm_execution!(twox_128_should_work);
fn twox_128_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_twox_128", &[0], wasm_method, &mut ext,).unwrap(),
twox_128(b"").to_vec().encode()
);
assert_eq!(
call_in_wasm("test_twox_128", &b"Hello world!".to_vec().encode(), wasm_method, &mut ext,)
.unwrap(),
twox_128(b"Hello world!").to_vec().encode()
);
}
test_wasm_execution!(ed25519_verify_should_work);
fn ed25519_verify_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
let key = ed25519::Pair::from_seed(&blake2_256(b"test"));
let sig = key.sign(b"all ok!");
let mut calldata = vec![];
calldata.extend_from_slice(key.public().as_ref());
calldata.extend_from_slice(sig.as_ref());
assert_eq!(
call_in_wasm("test_ed25519_verify", &calldata.encode(), wasm_method, &mut ext,).unwrap(),
true.encode(),
);
let other_sig = key.sign(b"all is not ok!");
let mut calldata = vec![];
calldata.extend_from_slice(key.public().as_ref());
calldata.extend_from_slice(other_sig.as_ref());
assert_eq!(
call_in_wasm("test_ed25519_verify", &calldata.encode(), wasm_method, &mut ext,).unwrap(),
false.encode(),
);
}
test_wasm_execution!(sr25519_verify_should_work);
fn sr25519_verify_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
let key = sr25519::Pair::from_seed(&blake2_256(b"test"));
let sig = key.sign(b"all ok!");
let mut calldata = vec![];
calldata.extend_from_slice(key.public().as_ref());
calldata.extend_from_slice(sig.as_ref());
assert_eq!(
call_in_wasm("test_sr25519_verify", &calldata.encode(), wasm_method, &mut ext,).unwrap(),
true.encode(),
);
let other_sig = key.sign(b"all is not ok!");
let mut calldata = vec![];
calldata.extend_from_slice(key.public().as_ref());
calldata.extend_from_slice(other_sig.as_ref());
assert_eq!(
call_in_wasm("test_sr25519_verify", &calldata.encode(), wasm_method, &mut ext,).unwrap(),
false.encode(),
);
}
test_wasm_execution!(ordered_trie_root_should_work);
fn ordered_trie_root_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let trie_input = vec![b"zero".to_vec(), b"one".to_vec(), b"two".to_vec()];
assert_eq!(
call_in_wasm("test_ordered_trie_root", &[0], wasm_method, &mut ext.ext(),).unwrap(),
Layout::<BlakeTwo256>::ordered_trie_root(trie_input.iter()).as_bytes().encode(),
);
}
test_wasm_execution!(offchain_index);
fn offchain_index(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let (offchain, _state) = testing::TestOffchainExt::new();
ext.register_extension(OffchainWorkerExt::new(offchain));
call_in_wasm("test_offchain_index_set", &[0], wasm_method, &mut ext.ext()).unwrap();
use sp_core::offchain::OffchainOverlayedChange;
let data = ext
.overlayed_changes()
.clone()
.offchain_drain_committed()
.find(|(k, _v)| k == &(sp_core::offchain::STORAGE_PREFIX.to_vec(), b"k".to_vec()));
assert_eq!(data.map(|data| data.1), Some(OffchainOverlayedChange::SetValue(b"v".to_vec())));
}
test_wasm_execution!(offchain_local_storage_should_work);
fn offchain_local_storage_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let (offchain, state) = testing::TestOffchainExt::new();
ext.register_extension(OffchainDbExt::new(offchain.clone()));
ext.register_extension(OffchainWorkerExt::new(offchain));
assert_eq!(
call_in_wasm("test_offchain_local_storage", &[0], wasm_method, &mut ext.ext(),).unwrap(),
true.encode(),
);
assert_eq!(state.read().persistent_storage.get(b"test"), Some(vec![]));
}
test_wasm_execution!(offchain_http_should_work);
fn offchain_http_should_work(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let (offchain, state) = testing::TestOffchainExt::new();
ext.register_extension(OffchainWorkerExt::new(offchain));
state.write().expect_request(testing::PendingRequest {
method: "POST".into(),
uri: "http://localhost:12345".into(),
body: vec![1, 2, 3, 4],
headers: vec![("X-Auth".to_owned(), "test".to_owned())],
sent: true,
response: Some(vec![1, 2, 3]),
response_headers: vec![("X-Auth".to_owned(), "hello".to_owned())],
..Default::default()
});
assert_eq!(
call_in_wasm("test_offchain_http", &[0], wasm_method, &mut ext.ext(),).unwrap(),
true.encode(),
);
}
test_wasm_execution!(should_trap_when_heap_exhausted);
fn should_trap_when_heap_exhausted(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let executor = crate::WasmExecutor::<HostFunctions>::builder()
.with_execution_method(wasm_method)
// `17` is the initial number of pages compiled into the binary.
.with_onchain_heap_alloc_strategy(HeapAllocStrategy::Static { extra_pages: 17 })
.build();
let err = executor
.uncached_call(
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
&mut ext.ext(),
true,
"test_allocate_vec",
&16777216_u32.encode(),
)
.unwrap_err();
match err {
Error::AbortedDueToTrap(error)
if matches!(wasm_method, WasmExecutionMethod::Compiled { .. }) =>
{
assert_eq!(
error.message,
r#"host code panicked while being called by the runtime: Failed to allocate memory: "Allocator ran out of space""#
);
},
error => panic!("unexpected error: {:?}", error),
}
}
fn mk_test_runtime(
wasm_method: WasmExecutionMethod,
pages: HeapAllocStrategy,
) -> Box<dyn WasmModule> {
let blob = RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap())
.expect("failed to create a runtime blob out of test runtime");
crate::wasm_runtime::create_wasm_runtime_with_code::<HostFunctions>(
wasm_method,
pages,
blob,
true,
None,
)
.expect("failed to instantiate wasm runtime")
}
test_wasm_execution!(returns_mutable_static);
fn returns_mutable_static(wasm_method: WasmExecutionMethod) {
let runtime =
mk_test_runtime(wasm_method, HeapAllocStrategy::Dynamic { maximum_pages: Some(1024) });
let mut instance = runtime.new_instance().unwrap();
let res = instance.call_export("returns_mutable_static", &[0]).unwrap();
assert_eq!(33, u64::decode(&mut &res[..]).unwrap());
// We expect that every invocation will need to return the initial
// value plus one. If the value increases more than that then it is
// a sign that the wasm runtime preserves the memory content.
let res = instance.call_export("returns_mutable_static", &[0]).unwrap();
assert_eq!(33, u64::decode(&mut &res[..]).unwrap());
}
test_wasm_execution!(returns_mutable_static_bss);
fn returns_mutable_static_bss(wasm_method: WasmExecutionMethod) {
let runtime =
mk_test_runtime(wasm_method, HeapAllocStrategy::Dynamic { maximum_pages: Some(1024) });
let mut instance = runtime.new_instance().unwrap();
let res = instance.call_export("returns_mutable_static_bss", &[0]).unwrap();
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
// We expect that every invocation will need to return the initial
// value plus one. If the value increases more than that then it is
// a sign that the wasm runtime preserves the memory content.
let res = instance.call_export("returns_mutable_static_bss", &[0]).unwrap();
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
}
// If we didn't restore the wasm instance properly, on a trap the stack pointer would not be
// returned to its initial value and thus the stack space is going to be leaked.
//
// See https://github.com/paritytech/substrate/issues/2967 for details
test_wasm_execution!(restoration_of_globals);
fn restoration_of_globals(wasm_method: WasmExecutionMethod) {
// Allocate 32 pages (of 65536 bytes) which gives the runtime 2048KB of heap to operate on
// (plus some additional space unused from the initial pages requested by the wasm runtime
// module).
//
// The fixture performs 2 allocations of 768KB and this theoretically gives 1536KB, however, due
// to our allocator algorithm there are inefficiencies.
const REQUIRED_MEMORY_PAGES: u32 = 32;
let runtime = mk_test_runtime(
wasm_method,
HeapAllocStrategy::Static { extra_pages: REQUIRED_MEMORY_PAGES },
);
let mut instance = runtime.new_instance().unwrap();
// On the first invocation we allocate approx. 768KB (75%) of stack and then trap.
let res = instance.call_export("allocates_huge_stack_array", &true.encode());
assert!(res.is_err());
// On the second invocation we allocate yet another 768KB (75%) of stack
let res = instance.call_export("allocates_huge_stack_array", &false.encode());
assert!(res.is_ok());
}
test_wasm_execution!(parallel_execution);
fn parallel_execution(wasm_method: WasmExecutionMethod) {
let executor = Arc::new(
crate::WasmExecutor::<HostFunctions>::builder()
.with_execution_method(wasm_method)
.build(),
);
let threads: Vec<_> = (0..8)
.map(|_| {
let executor = executor.clone();
std::thread::spawn(move || {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
executor
.uncached_call(
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
&mut ext,
true,
"test_twox_128",
&[0],
)
.unwrap(),
array_bytes::hex2bytes_unchecked("99e9d85137db46ef4bbea33613baafd5").encode()
);
})
})
.collect();
for t in threads.into_iter() {
t.join().unwrap();
}
}
test_wasm_execution!(wasm_tracing_should_work);
fn wasm_tracing_should_work(wasm_method: WasmExecutionMethod) {
use sc_tracing::{SpanDatum, TraceEvent};
use std::sync::Mutex;
struct TestTraceHandler(Arc<Mutex<Vec<SpanDatum>>>);
impl sc_tracing::TraceHandler for TestTraceHandler {
fn handle_span(&self, sd: &SpanDatum) {
self.0.lock().unwrap().push(sd.clone());
}
fn handle_event(&self, _event: &TraceEvent) {}
}
let traces = Arc::new(Mutex::new(Vec::new()));
let handler = TestTraceHandler(traces.clone());
// Create subscriber with wasm_tracing disabled
let test_subscriber = tracing_subscriber::fmt()
.finish()
.with(sc_tracing::ProfilingLayer::new_with_handler(Box::new(handler), "default"));
let _guard = tracing::subscriber::set_default(test_subscriber);
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
let span_id =
call_in_wasm("test_enter_span", Default::default(), wasm_method, &mut ext).unwrap();
let span_id = u64::decode(&mut &span_id[..]).unwrap();
assert!(span_id > 0);
call_in_wasm("test_exit_span", &span_id.encode(), wasm_method, &mut ext).unwrap();
// Check there is only the single trace
let len = traces.lock().unwrap().len();
assert_eq!(len, 1);
let span_datum = traces.lock().unwrap().pop().unwrap();
let values = span_datum.values;
assert_eq!(span_datum.target, "default");
assert_eq!(span_datum.name, "");
assert_eq!(values.bool_values.get("wasm").unwrap(), &true);
call_in_wasm("test_nested_spans", Default::default(), wasm_method, &mut ext).unwrap();
let len = traces.lock().unwrap().len();
assert_eq!(len, 2);
}
test_wasm_execution!(allocate_two_gigabyte);
fn allocate_two_gigabyte(wasm_method: WasmExecutionMethod) {
let runtime = mk_test_runtime(wasm_method, HeapAllocStrategy::Dynamic { maximum_pages: None });
let mut instance = runtime.new_instance().unwrap();
let res = instance.call_export("allocate_two_gigabyte", &[0]).unwrap();
assert_eq!(10 * 1024 * 1024 * 205, u32::decode(&mut &res[..]).unwrap());
}
test_wasm_execution!(memory_is_cleared_between_invocations);
fn memory_is_cleared_between_invocations(wasm_method: WasmExecutionMethod) {
// This is based on the code generated by compiling a runtime *without*
// the `-C link-arg=--import-memory` using the following code and then
// disassembling the resulting blob with `wasm-dis`:
//
// ```
// #[no_mangle]
// #[cfg(not(feature = "std"))]
// pub fn returns_no_bss_mutable_static(_: *mut u8, _: usize) -> u64 {
// static mut COUNTER: usize = 0;
// let output = unsafe {
// COUNTER += 1;
// COUNTER as u64
// };
// sp_core::to_substrate_wasm_fn_return_value(&output)
// }
// ```
//
// This results in the BSS section to *not* be emitted, hence the executor has no way
// of knowing about the `static` variable's existence, so this test will fail if the linear
// memory is not properly cleared between invocations.
let binary = wat::parse_str(r#"
(module
(type $i32_=>_i32 (func (param i32) (result i32)))
(type $i32_i32_=>_i64 (func (param i32 i32) (result i64)))
(import "env" "ext_allocator_malloc_version_1" (func $ext_allocator_malloc_version_1 (param i32) (result i32)))
(global $__stack_pointer (mut i32) (i32.const 1048576))
(global $global$1 i32 (i32.const 1048580))
(global $global$2 i32 (i32.const 1048592))
(memory $0 17)
(export "memory" (memory $0))
(export "returns_no_bss_mutable_static" (func $returns_no_bss_mutable_static))
(export "__data_end" (global $global$1))
(export "__heap_base" (global $global$2))
(func $returns_no_bss_mutable_static (param $0 i32) (param $1 i32) (result i64)
(local $2 i32)
(local $3 i32)
(i32.store offset=1048576
(i32.const 0)
(local.tee $2
(i32.add
(i32.load offset=1048576 (i32.const 0))
(i32.const 1)
)
)
)
(i64.store
(local.tee $3
(call $ext_allocator_malloc_version_1 (i32.const 8))
)
(i64.extend_i32_u (local.get $2))
)
(i64.or
(i64.extend_i32_u (local.get $3))
(i64.const 34359738368)
)
)
)"#).unwrap();
let runtime = crate::wasm_runtime::create_wasm_runtime_with_code::<HostFunctions>(
wasm_method,
HeapAllocStrategy::Dynamic { maximum_pages: Some(1024) },
RuntimeBlob::uncompress_if_needed(&binary[..]).unwrap(),
true,
None,
)
.unwrap();
let mut instance = runtime.new_instance().unwrap();
let res = instance.call_export("returns_no_bss_mutable_static", &[0]).unwrap();
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
let res = instance.call_export("returns_no_bss_mutable_static", &[0]).unwrap();
assert_eq!(1, u64::decode(&mut &res[..]).unwrap());
}
test_wasm_execution!(return_i8);
fn return_i8(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_return_i8", &[], wasm_method, &mut ext).unwrap(),
(-66_i8).encode()
);
}
test_wasm_execution!(take_i8);
fn take_i8(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
call_in_wasm("test_take_i8", &(-66_i8).encode(), wasm_method, &mut ext).unwrap();
}
test_wasm_execution!(abort_on_panic);
fn abort_on_panic(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
match call_in_wasm("test_abort_on_panic", &[], wasm_method, &mut ext).unwrap_err() {
Error::AbortedDueToPanic(error) => assert_eq!(error.message, "test_abort_on_panic called"),
error => panic!("unexpected error: {:?}", error),
}
}
test_wasm_execution!(unreachable_intrinsic);
fn unreachable_intrinsic(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
match call_in_wasm("test_unreachable_intrinsic", &[], wasm_method, &mut ext).unwrap_err() {
Error::AbortedDueToTrap(error) => {
let expected = match wasm_method {
WasmExecutionMethod::Compiled { .. } =>
"wasm trap: wasm `unreachable` instruction executed",
};
assert_eq!(error.message, expected);
},
error => panic!("unexpected error: {:?}", error),
}
}
test_wasm_execution!(return_value);
fn return_value(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_return_value", &[], wasm_method, &mut ext).unwrap(),
(1234u64).encode()
);
}
test_wasm_execution!(return_huge_len);
fn return_huge_len(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
match call_in_wasm("test_return_huge_len", &[], wasm_method, &mut ext).unwrap_err() {
Error::OutputExceedsBounds => {
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
},
error => panic!("unexpected error: {:?}", error),
}
}
test_wasm_execution!(return_max_memory_offset);
fn return_max_memory_offset(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
assert_eq!(
call_in_wasm("test_return_max_memory_offset", &[], wasm_method, &mut ext).unwrap(),
(u8::MAX).encode()
);
}
test_wasm_execution!(return_max_memory_offset_plus_one);
fn return_max_memory_offset_plus_one(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
match call_in_wasm("test_return_max_memory_offset_plus_one", &[], wasm_method, &mut ext)
.unwrap_err()
{
Error::OutputExceedsBounds => {
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
},
error => panic!("unexpected error: {:?}", error),
}
}
test_wasm_execution!(return_overflow);
fn return_overflow(wasm_method: WasmExecutionMethod) {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
match call_in_wasm("test_return_overflow", &[], wasm_method, &mut ext).unwrap_err() {
Error::OutputExceedsBounds => {
assert_matches!(wasm_method, WasmExecutionMethod::Compiled { .. });
},
error => panic!("unexpected error: {:?}", error),
}
}
+91
View File
@@ -0,0 +1,91 @@
// This file is part of Substrate.
// 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/>.
//! A crate that provides means of executing/dispatching calls into the runtime.
//!
//! There are a few responsibilities of this crate at the moment:
//!
//! - It provides an implementation of a common entrypoint for calling into the runtime, both
//! wasm and compiled.
//! - It defines the environment for the wasm execution, namely the host functions that are to be
//! provided into the wasm runtime module.
//! - It also provides the required infrastructure for executing the current wasm runtime (specified
//! by the current value of `:code` in the provided externalities), i.e. interfacing with
//! wasm engine used, instance cache.
#![warn(missing_docs)]
#[macro_use]
mod executor;
#[cfg(test)]
mod integration_tests;
mod wasm_runtime;
pub use codec::Codec;
#[allow(deprecated)]
pub use executor::NativeElseWasmExecutor;
pub use executor::{with_externalities_safe, NativeExecutionDispatch, WasmExecutor};
#[doc(hidden)]
pub use sp_core::traits::Externalities;
pub use sp_version::{NativeVersion, RuntimeVersion};
#[doc(hidden)]
pub use sp_wasm_interface;
pub use sp_wasm_interface::HostFunctions;
pub use wasm_runtime::{read_embedded_version, WasmExecutionMethod};
pub use sc_executor_common::{
error,
wasm_runtime::{HeapAllocStrategy, DEFAULT_HEAP_ALLOC_PAGES, DEFAULT_HEAP_ALLOC_STRATEGY},
};
pub use sc_executor_wasmtime::InstantiationStrategy as WasmtimeInstantiationStrategy;
/// Extracts the runtime version of a given runtime code.
pub trait RuntimeVersionOf {
/// Extract [`RuntimeVersion`] of the given `runtime_code`.
fn runtime_version(
&self,
ext: &mut dyn Externalities,
runtime_code: &sp_core::traits::RuntimeCode,
) -> error::Result<RuntimeVersion>;
}
#[cfg(test)]
mod tests {
use super::*;
use sc_executor_common::runtime_blob::RuntimeBlob;
use sc_runtime_test::wasm_binary_unwrap;
use sp_io::TestExternalities;
#[test]
fn call_in_interpreted_wasm_works() {
let mut ext = TestExternalities::default();
let mut ext = ext.ext();
let executor = WasmExecutor::<sp_io::SubstrateHostFunctions>::builder().build();
let res = executor
.uncached_call(
RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap()).unwrap(),
&mut ext,
true,
"test_empty_return",
&[],
)
.unwrap();
assert_eq!(res, vec![0u8; 0]);
}
}
@@ -0,0 +1,564 @@
// This file is part of Substrate.
// 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/>.
//! Traits and accessor functions for calling into the Substrate Wasm runtime.
//!
//! The primary means of accessing the runtimes is through a cache which saves the reusable
//! components of the runtime that are expensive to initialize.
use crate::error::{Error, WasmError};
use codec::Decode;
use parking_lot::Mutex;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule},
};
use schnellru::{ByLength, LruMap};
use sp_core::traits::{Externalities, FetchRuntimeCode, RuntimeCode};
use sp_version::RuntimeVersion;
use sp_wasm_interface::HostFunctions;
use std::{
panic::AssertUnwindSafe,
path::{Path, PathBuf},
sync::Arc,
};
/// Specification of different methods of executing the runtime Wasm code.
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
pub enum WasmExecutionMethod {
/// Uses the Wasmtime compiled runtime.
Compiled {
/// The instantiation strategy to use.
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy,
},
}
impl Default for WasmExecutionMethod {
fn default() -> Self {
Self::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
struct VersionedRuntimeId {
/// Runtime code hash.
code_hash: Vec<u8>,
/// Wasm runtime type.
wasm_method: WasmExecutionMethod,
/// The heap allocation strategy this runtime was created with.
heap_alloc_strategy: HeapAllocStrategy,
}
/// A Wasm runtime object along with its cached runtime version.
struct VersionedRuntime {
/// Shared runtime that can spawn instances.
module: Box<dyn WasmModule>,
/// Runtime version according to `Core_version` if any.
version: Option<RuntimeVersion>,
// TODO: Remove this once the legacy instance reuse instantiation strategy
// for `wasmtime` is gone, as this only makes sense with that particular strategy.
/// Cached instance pool.
instances: Vec<Mutex<Option<Box<dyn WasmInstance>>>>,
}
impl VersionedRuntime {
/// Run the given closure `f` with an instance of this runtime.
fn with_instance<R, F>(&self, ext: &mut dyn Externalities, f: F) -> Result<R, Error>
where
F: FnOnce(
&dyn WasmModule,
&mut dyn WasmInstance,
Option<&RuntimeVersion>,
&mut dyn Externalities,
) -> Result<R, Error>,
{
// Find a free instance
let instance = self
.instances
.iter()
.enumerate()
.find_map(|(index, i)| i.try_lock().map(|i| (index, i)));
match instance {
Some((index, mut locked)) => {
let (mut instance, new_inst) = locked
.take()
.map(|r| Ok((r, false)))
.unwrap_or_else(|| self.module.new_instance().map(|i| (i, true)))?;
let result = f(&*self.module, &mut *instance, self.version.as_ref(), ext);
if let Err(e) = &result {
if new_inst {
tracing::warn!(
target: "wasm-runtime",
error = %e,
"Fresh runtime instance failed",
)
} else {
tracing::warn!(
target: "wasm-runtime",
error = %e,
"Evicting failed runtime instance",
);
}
} else {
*locked = Some(instance);
if new_inst {
tracing::debug!(
target: "wasm-runtime",
"Allocated WASM instance {}/{}",
index + 1,
self.instances.len(),
);
}
}
result
},
None => {
tracing::warn!(target: "wasm-runtime", "Ran out of free WASM instances");
// Allocate a new instance
let mut instance = self.module.new_instance()?;
f(&*self.module, &mut *instance, self.version.as_ref(), ext)
},
}
}
}
/// Cache for the runtimes.
///
/// When an instance is requested for the first time it is added to this cache. Metadata is kept
/// with the instance so that it can be efficiently reinitialized.
///
/// When using the Wasmi interpreter execution method, the metadata includes the initial memory and
/// values of mutable globals. Follow-up requests to fetch a runtime return this one instance with
/// the memory reset to the initial memory. So, one runtime instance is reused for every fetch
/// request.
///
/// The size of cache is configurable via the cli option `--runtime-cache-size`.
pub struct RuntimeCache {
/// A cache of runtimes along with metadata.
///
/// Runtimes sorted by recent usage. The most recently used is at the front.
runtimes: Mutex<LruMap<VersionedRuntimeId, Arc<VersionedRuntime>>>,
/// The size of the instances cache for each runtime.
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
}
impl RuntimeCache {
/// Creates a new instance of a runtimes cache.
///
/// `max_runtime_instances` specifies the number of instances per runtime preserved in an
/// in-memory cache.
///
/// `cache_path` allows to specify an optional directory where the executor can store files
/// for caching.
///
/// `runtime_cache_size` specifies the number of different runtimes versions preserved in an
/// in-memory cache, must always be at least 1.
pub fn new(
max_runtime_instances: usize,
cache_path: Option<PathBuf>,
runtime_cache_size: u8,
) -> RuntimeCache {
let cap = ByLength::new(runtime_cache_size.max(1) as u32);
RuntimeCache { runtimes: Mutex::new(LruMap::new(cap)), max_runtime_instances, cache_path }
}
/// Prepares a WASM module instance and executes given function for it.
///
/// This uses internal cache to find available instance or create a new one.
/// # Parameters
///
/// `runtime_code` - The runtime wasm code used setup the runtime.
///
/// `ext` - The externalities to access the state.
///
/// `wasm_method` - Type of WASM backend to use.
///
/// `heap_alloc_strategy` - The heap allocation strategy to use.
///
/// `allow_missing_func_imports` - Ignore missing function imports.
///
/// `f` - Function to execute.
///
/// `H` - A compile-time list of host functions to expose to the runtime.
///
/// # Returns result of `f` wrapped in an additional result.
/// In case of failure one of two errors can be returned:
///
/// `Err::RuntimeConstruction` is returned for runtime construction issues.
///
/// `Error::InvalidMemoryReference` is returned if no memory export with the
/// identifier `memory` can be found in the runtime.
pub fn with_instance<'c, H, R, F>(
&self,
runtime_code: &'c RuntimeCode<'c>,
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
heap_alloc_strategy: HeapAllocStrategy,
allow_missing_func_imports: bool,
f: F,
) -> Result<Result<R, Error>, Error>
where
H: HostFunctions,
F: FnOnce(
&dyn WasmModule,
&mut dyn WasmInstance,
Option<&RuntimeVersion>,
&mut dyn Externalities,
) -> Result<R, Error>,
{
let code_hash = &runtime_code.hash;
let versioned_runtime_id =
VersionedRuntimeId { code_hash: code_hash.clone(), heap_alloc_strategy, wasm_method };
let mut runtimes = self.runtimes.lock(); // this must be released prior to calling f
let versioned_runtime = if let Some(versioned_runtime) = runtimes.get(&versioned_runtime_id)
{
versioned_runtime.clone()
} else {
let code = runtime_code.fetch_runtime_code().ok_or(WasmError::CodeNotFound)?;
let time = std::time::Instant::now();
let result = create_versioned_wasm_runtime::<H>(
&code,
ext,
wasm_method,
heap_alloc_strategy,
allow_missing_func_imports,
self.max_runtime_instances,
self.cache_path.as_deref(),
);
match result {
Ok(ref result) => {
tracing::debug!(
target: "wasm-runtime",
"Prepared new runtime version {:?} in {} ms.",
result.version,
time.elapsed().as_millis(),
);
},
Err(ref err) => {
tracing::warn!(target: "wasm-runtime", error = ?err, "Cannot create a runtime");
},
}
let versioned_runtime = Arc::new(result?);
// Save new versioned wasm runtime in cache
runtimes.insert(versioned_runtime_id, versioned_runtime.clone());
versioned_runtime
};
// Lock must be released prior to calling f
drop(runtimes);
Ok(versioned_runtime.with_instance(ext, f))
}
}
/// Create a wasm runtime with the given `code`.
pub fn create_wasm_runtime_with_code<H>(
wasm_method: WasmExecutionMethod,
heap_alloc_strategy: HeapAllocStrategy,
blob: RuntimeBlob,
allow_missing_func_imports: bool,
cache_path: Option<&Path>,
) -> Result<Box<dyn WasmModule>, WasmError>
where
H: HostFunctions,
{
if let Some(blob) = blob.as_polkavm_blob() {
return sc_executor_polkavm::create_runtime::<H>(blob);
}
match wasm_method {
WasmExecutionMethod::Compiled { instantiation_strategy } =>
sc_executor_wasmtime::create_runtime::<H>(
blob,
sc_executor_wasmtime::Config {
allow_missing_func_imports,
cache_path: cache_path.map(ToOwned::to_owned),
semantics: sc_executor_wasmtime::Semantics {
heap_alloc_strategy,
instantiation_strategy,
deterministic_stack_limit: None,
canonicalize_nans: false,
parallel_compilation: true,
wasm_multi_value: false,
wasm_bulk_memory: false,
wasm_reference_types: false,
wasm_simd: false,
},
},
)
.map(|runtime| -> Box<dyn WasmModule> { Box::new(runtime) }),
}
}
fn decode_version(mut version: &[u8]) -> Result<RuntimeVersion, WasmError> {
Decode::decode(&mut version).map_err(|_| {
WasmError::Instantiation(
"failed to decode \"Core_version\" result using old runtime version".into(),
)
})
}
fn decode_runtime_apis(apis: &[u8]) -> Result<Vec<([u8; 8], u32)>, WasmError> {
use sp_api::RUNTIME_API_INFO_SIZE;
apis.chunks(RUNTIME_API_INFO_SIZE)
.map(|chunk| {
// `chunk` can be less than `RUNTIME_API_INFO_SIZE` if the total length of `apis`
// doesn't completely divide by `RUNTIME_API_INFO_SIZE`.
<[u8; RUNTIME_API_INFO_SIZE]>::try_from(chunk)
.map(sp_api::deserialize_runtime_api_info)
.map_err(|_| WasmError::Other("a clipped runtime api info declaration".to_owned()))
})
.collect::<Result<Vec<_>, WasmError>>()
}
/// Take the runtime blob and scan it for the custom wasm sections containing the version
/// information and construct the `RuntimeVersion` from them.
///
/// If there are no such sections, it returns `None`. If there is an error during decoding those
/// sections, `Err` will be returned.
pub fn read_embedded_version(blob: &RuntimeBlob) -> Result<Option<RuntimeVersion>, WasmError> {
if let Some(mut version_section) = blob.custom_section_contents("runtime_version") {
let apis = blob
.custom_section_contents("runtime_apis")
.map(decode_runtime_apis)
.transpose()?
.map(Into::into);
let core_version = apis.as_ref().and_then(sp_version::core_version_from_apis);
// We do not use `RuntimeVersion::decode` here because that `decode_version` relies on
// presence of a special API in the `apis` field to treat the input as a non-legacy version.
// However the structure found in the `runtime_version` always contain an empty `apis`
// field. Therefore the version read will be mistakenly treated as an legacy one.
let mut decoded_version = sp_version::RuntimeVersion::decode_with_version_hint(
&mut version_section,
core_version,
)
.map_err(|_| WasmError::Instantiation("failed to decode version section".into()))?;
if let Some(apis) = apis {
decoded_version.apis = apis;
}
Ok(Some(decoded_version))
} else {
Ok(None)
}
}
fn create_versioned_wasm_runtime<H>(
code: &[u8],
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
heap_alloc_strategy: HeapAllocStrategy,
allow_missing_func_imports: bool,
max_instances: usize,
cache_path: Option<&Path>,
) -> Result<VersionedRuntime, WasmError>
where
H: HostFunctions,
{
// The incoming code may be actually compressed. We decompress it here and then work with
// the uncompressed code from now on.
let blob = sc_executor_common::runtime_blob::RuntimeBlob::uncompress_if_needed(code)?;
// Use the runtime blob to scan if there is any metadata embedded into the wasm binary
// pertaining to runtime version. We do it before consuming the runtime blob for creating the
// runtime.
let mut version = read_embedded_version(&blob)?;
let runtime = create_wasm_runtime_with_code::<H>(
wasm_method,
heap_alloc_strategy,
blob,
allow_missing_func_imports,
cache_path,
)?;
// If the runtime blob doesn't embed the runtime version then use the legacy version query
// mechanism: call the runtime.
if version.is_none() {
// Call to determine runtime version.
let version_result = {
// `ext` is already implicitly handled as unwind safe, as we store it in a global
// variable.
let mut ext = AssertUnwindSafe(ext);
// The following unwind safety assertion is OK because if the method call panics, the
// runtime will be dropped.
let runtime = AssertUnwindSafe(runtime.as_ref());
crate::executor::with_externalities_safe(&mut **ext, move || {
runtime.new_instance()?.call("Core_version".into(), &[])
})
.map_err(|_| WasmError::Instantiation("panic in call to get runtime version".into()))?
};
if let Ok(version_buf) = version_result {
version = Some(decode_version(&version_buf)?)
}
}
let mut instances = Vec::with_capacity(max_instances);
instances.resize_with(max_instances, || Mutex::new(None));
Ok(VersionedRuntime { module: runtime, version, instances })
}
#[cfg(test)]
mod tests {
extern crate alloc;
use super::*;
use alloc::borrow::Cow;
use codec::Encode;
use sp_api::{Core, RuntimeApiInfo};
use sp_version::{create_apis_vec, RuntimeVersion};
use sp_wasm_interface::HostFunctions;
use substrate_test_runtime::Block;
#[derive(Encode)]
pub struct OldRuntimeVersion {
pub spec_name: Cow<'static, str>,
pub impl_name: Cow<'static, str>,
pub authoring_version: u32,
pub spec_version: u32,
pub impl_version: u32,
pub apis: sp_version::ApisVec,
}
#[test]
fn host_functions_are_equal() {
let host_functions = sp_io::SubstrateHostFunctions::host_functions();
let equal = &host_functions[..] == &host_functions[..];
assert!(equal, "Host functions are not equal");
}
#[test]
fn old_runtime_version_decodes() {
let old_runtime_version = OldRuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 1)]),
};
let version = decode_version(&old_runtime_version.encode()).unwrap();
assert_eq!(1, version.transaction_version);
assert_eq!(0, version.system_version);
}
#[test]
fn old_runtime_version_decodes_fails_with_version_3() {
let old_runtime_version = OldRuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
};
decode_version(&old_runtime_version.encode()).unwrap_err();
}
#[test]
fn new_runtime_version_decodes() {
let old_runtime_version = RuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 3)]),
transaction_version: 3,
system_version: 4,
};
let version = decode_version(&old_runtime_version.encode()).unwrap();
assert_eq!(3, version.transaction_version);
assert_eq!(0, version.system_version);
let old_runtime_version = RuntimeVersion {
spec_name: "test".into(),
impl_name: "test".into(),
authoring_version: 1,
spec_version: 1,
impl_version: 1,
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
transaction_version: 3,
system_version: 4,
};
let version = decode_version(&old_runtime_version.encode()).unwrap();
assert_eq!(3, version.transaction_version);
assert_eq!(4, version.system_version);
}
#[test]
fn embed_runtime_version_works() {
let wasm = sp_maybe_compressed_blob::decompress(
substrate_test_runtime::wasm_binary_unwrap(),
sp_maybe_compressed_blob::CODE_BLOB_BOMB_LIMIT,
)
.expect("Decompressing works");
let runtime_version = RuntimeVersion {
spec_name: "test_replace".into(),
impl_name: "test_replace".into(),
authoring_version: 100,
spec_version: 100,
impl_version: 100,
apis: create_apis_vec!([(<dyn Core::<Block>>::ID, 4)]),
transaction_version: 100,
system_version: 1,
};
let embedded = sp_version::embed::embed_runtime_version(&wasm, runtime_version.clone())
.expect("Embedding works");
let blob = RuntimeBlob::new(&embedded).expect("Embedded blob is valid");
let read_version = read_embedded_version(&blob)
.ok()
.flatten()
.expect("Reading embedded version works");
assert_eq!(runtime_version, read_version);
}
}
@@ -0,0 +1,72 @@
[package]
name = "sc-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 }
sc-allocator = { workspace = true, default-features = true }
sc-executor-common = { workspace = true, default-features = true }
sp-runtime-interface = { workspace = true, default-features = true }
sp-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 }
sc-runtime-test = { workspace = true }
sp-io = { workspace = true, default-features = true }
tempfile = { workspace = true }
wat = { workspace = true }
[features]
runtime-benchmarks = [
"sc-runtime-test/runtime-benchmarks",
"sp-io/runtime-benchmarks",
"sp-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 Substrate.
// 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 Substrate.
// 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 sc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
use sp_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> sp_wasm_interface::FunctionContext for HostContext<'a> {
fn read_memory_into(
&self,
address: Pointer<u8>,
dest: &mut [u8],
) -> sp_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]) -> sp_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) -> sp_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>) -> sp_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 Substrate.
// 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 sc_executor_common::error::WasmError;
use sp_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> sp_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 Substrate.
// 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 substrate
//! runtime module.
use std::sync::Arc;
use crate::runtime::{InstanceCounter, ReleaseInstanceHandle, Store, StoreData};
use sc_executor_common::error::{Backtrace, Error, MessageWithBacktrace, Result, WasmError};
use sp_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 [`sc_allocator::Memory`].
pub(crate) struct MemoryWrapper<'a, C>(pub &'a wasmtime::Memory, pub &'a mut C);
impl<C: AsContextMut> sc_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 Substrate-runtime.
///
/// This struct is a handy wrapper around a wasmtime `Instance` that provides substrate 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 substrate 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 Substrate.
// 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 sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{HeapAllocStrategy, WasmModule},
};
@@ -0,0 +1,726 @@
// This file is part of Substrate.
// 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 sc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
use sc_executor_common::{
error::{Error, Result, WasmError},
runtime_blob::RuntimeBlob,
util::checked_range,
wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule},
};
use sp_runtime_interface::unpack_ptr_and_len;
use sp_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 Substrate 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 Substrate.
// 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 sc_executor_common::{
error::Error,
runtime_blob::RuntimeBlob,
wasm_runtime::{HeapAllocStrategy, WasmModule, DEFAULT_HEAP_ALLOC_STRATEGY},
};
use sc_runtime_test::wasm_binary_unwrap;
use crate::InstantiationStrategy;
type HostFunctions = sp_io::SubstrateHostFunctions;
#[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 Substrate.
// 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 sc_executor_common::{
error::{Error, Result},
util::checked_range,
};
use sp_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) {}