wasm-executor: Support growing the memory (#12520)

* As always, start with something :P

* Add support for max_heap_pages

* Add support for wasmtime

* Make it compile

* Fix compilation

* Copy wrongly merged code

* Fix compilation

* Some fixes

* Fix

* Get stuff working

* More work

* More fixes

* ...

* More

* FIXEs

* Switch wasmi to use `RuntimeBlob` like wasmtime

* Removed unused stuff

* Cleanup

* More cleanups

* Introduce `CallContext`

* Fixes

* More fixes

* Add builder for creating the `WasmExecutor`

* Adds some docs

* FMT

* First round of feedback.

* Review feedback round 2

* More fixes

* Fix try-runtime

* Update client/executor/wasmtime/src/instance_wrapper.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/executor/common/src/wasm_runtime.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/executor/common/src/runtime_blob/runtime_blob.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/executor/common/src/wasm_runtime.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/allocator/src/freeing_bump.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Update client/allocator/src/freeing_bump.rs

Co-authored-by: Koute <koute@users.noreply.github.com>

* Feedback round 3

* FMT

* Review comments

---------

Co-authored-by: Koute <koute@users.noreply.github.com>
This commit is contained in:
Bastian Köcher
2023-02-24 12:43:01 +01:00
committed by GitHub
parent c848d40775
commit 941288c6d0
37 changed files with 1092 additions and 667 deletions
+2 -1
View File
@@ -17,7 +17,7 @@ targets = ["x86_64-unknown-linux-gnu"]
lru = "0.8.1"
parking_lot = "0.12.1"
tracing = "0.1.29"
wasmi = "0.13"
wasmi = "0.13.2"
codec = { package = "parity-scale-codec", version = "3.2.2" }
sc-executor-common = { version = "0.10.0-dev", path = "common" }
@@ -43,6 +43,7 @@ sp-state-machine = { version = "0.13.0", path = "../../primitives/state-machine"
sp-runtime = { version = "7.0.0", path = "../../primitives/runtime" }
sp-maybe-compressed-blob = { version = "4.1.0-dev", path = "../../primitives/maybe-compressed-blob" }
sc-tracing = { version = "4.0.0-dev", path = "../tracing" }
sp-tracing = { version = "6.0.0", path = "../../primitives/tracing" }
tracing-subscriber = "0.2.19"
paste = "1.0"
regex = "1.6.0"
+4 -5
View File
@@ -21,7 +21,7 @@ use codec::Encode;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{WasmInstance, WasmModule},
wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule},
};
use sc_executor_wasmtime::InstantiationStrategy;
use sc_runtime_test::wasm_binary_unwrap as test_runtime;
@@ -51,13 +51,13 @@ fn initialize(
) -> Arc<dyn WasmModule> {
let blob = RuntimeBlob::uncompress_if_needed(runtime).unwrap();
let host_functions = sp_io::SubstrateHostFunctions::host_functions();
let heap_pages = 2048;
let extra_pages = 2048;
let allow_missing_func_imports = true;
match method {
Method::Interpreted => sc_executor_wasmi::create_runtime(
blob,
heap_pages,
HeapAllocStrategy::Static { extra_pages },
host_functions,
allow_missing_func_imports,
)
@@ -67,12 +67,11 @@ fn initialize(
allow_missing_func_imports,
cache_path: None,
semantics: sc_executor_wasmtime::Semantics {
extra_heap_pages: heap_pages,
heap_alloc_strategy: HeapAllocStrategy::Static { extra_pages },
instantiation_strategy,
deterministic_stack_limit: None,
canonicalize_nans: false,
parallel_compilation: true,
max_memory_size: None,
},
};
+1 -1
View File
@@ -16,7 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
thiserror = "1.0.30"
wasm-instrument = "0.3"
wasmi = "0.13"
wasmi = "0.13.2"
sc-allocator = { version = "4.1.0-dev", path = "../../allocator" }
sp-maybe-compressed-blob = { version = "4.1.0-dev", path = "../../../primitives/maybe-compressed-blob" }
sp-wasm-interface = { version = "7.0.0", path = "../../../primitives/wasm-interface" }
@@ -16,7 +16,7 @@
// 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;
use crate::{error::WasmError, wasm_runtime::HeapAllocStrategy};
use wasm_instrument::{
export_mutable_globals,
parity_wasm::elements::{
@@ -157,18 +157,13 @@ impl RuntimeBlob {
Ok(())
}
/// Increases the number of memory pages requested by the WASM blob by
/// the given amount of `extra_heap_pages`.
/// 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 modifies the initial size of the memory; the maximum is unmodified
/// unless it's smaller than the initial size, in which case it will be increased
/// so that it's at least as big as the initial size.
pub fn add_extra_heap_pages_to_memory_section(
pub fn setup_memory_according_to_heap_alloc_strategy(
&mut self,
extra_heap_pages: u32,
heap_alloc_strategy: HeapAllocStrategy,
) -> Result<(), WasmError> {
let memory_section = self
.raw_module
@@ -179,8 +174,17 @@ impl RuntimeBlob {
return Err(WasmError::Other("memory section is empty".into()))
}
for memory_ty in memory_section.entries_mut() {
let min = memory_ty.limits().initial().saturating_add(extra_heap_pages);
let max = memory_ty.limits().maximum().map(|max| std::cmp::max(min, max));
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(())
+1 -5
View File
@@ -26,11 +26,7 @@ use std::ops::Range;
/// 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)?;
if end <= max {
Some(offset..end)
} else {
None
}
(end <= max).then(|| offset..end)
}
/// Provides safe memory access interface using an external buffer
@@ -119,3 +119,29 @@ pub trait WasmInstance: Send {
None
}
}
/// 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>,
},
}
@@ -328,6 +328,15 @@ sp_core::wasm_export_functions! {
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");
}
@@ -21,25 +21,60 @@
use super::mk_test_runtime;
use crate::WasmExecutionMethod;
use codec::Encode as _;
use sc_executor_common::wasm_runtime::HeapAllocStrategy;
mod smaps;
use self::smaps::Smaps;
#[test]
fn memory_consumption_interpreted() {
let _ = sp_tracing::try_init_simple();
if std::env::var("RUN_TEST").is_ok() {
memory_consumption(WasmExecutionMethod::Interpreted);
} else {
// We need to run the test in isolation, to not getting interfered by the other tests.
let executable = std::env::current_exe().unwrap();
let output = std::process::Command::new(executable)
.env("RUN_TEST", "1")
.args(&["--nocapture", "memory_consumption_interpreted"])
.output()
.unwrap();
assert!(output.status.success());
}
}
#[test]
fn memory_consumption_compiled() {
let _ = sp_tracing::try_init_simple();
if std::env::var("RUN_TEST").is_ok() {
memory_consumption(WasmExecutionMethod::Compiled {
instantiation_strategy:
sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse,
});
} else {
// We need to run the test in isolation, to not getting interfered by the other tests.
let executable = std::env::current_exe().unwrap();
let status = std::process::Command::new(executable)
.env("RUN_TEST", "1")
.args(&["--nocapture", "memory_consumption_compiled"])
.status()
.unwrap();
assert!(status.success());
}
}
fn memory_consumption(wasm_method: WasmExecutionMethod) {
// This aims to see if linear memory stays backed by the physical memory after a runtime call.
//
// For that we make a series of runtime calls, probing the RSS for the VMA matching the linear
// memory. After the call we expect RSS to be equal to 0.
let runtime = mk_test_runtime(
WasmExecutionMethod::Compiled {
instantiation_strategy:
sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse,
},
1024,
);
let runtime = mk_test_runtime(wasm_method, HeapAllocStrategy::Static { extra_pages: 1024 });
let mut instance = runtime.new_instance().unwrap();
let heap_base = instance
@@ -24,7 +24,7 @@ use codec::{Decode, Encode};
use sc_executor_common::{
error::{Error, WasmError},
runtime_blob::RuntimeBlob,
wasm_runtime::WasmModule,
wasm_runtime::{HeapAllocStrategy, WasmModule},
};
use sc_runtime_test::wasm_binary_unwrap;
use sp_core::{
@@ -52,11 +52,13 @@ macro_rules! test_wasm_execution {
paste::item! {
#[test]
fn [<$method_name _interpreted>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Interpreted);
}
#[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
});
@@ -64,6 +66,7 @@ macro_rules! test_wasm_execution {
#[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
});
@@ -71,6 +74,7 @@ macro_rules! test_wasm_execution {
#[test]
fn [<$method_name _compiled_pooling_cow>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite
});
@@ -78,6 +82,7 @@ macro_rules! test_wasm_execution {
#[test]
fn [<$method_name _compiled_pooling_vanilla>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::Pooling
});
@@ -85,6 +90,7 @@ macro_rules! test_wasm_execution {
#[test]
fn [<$method_name _compiled_legacy_instance_reuse>]() {
let _ = sp_tracing::try_init_simple();
$method_name(WasmExecutionMethod::Compiled {
instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse
});
@@ -474,7 +480,10 @@ fn should_trap_when_heap_exhausted(wasm_method: WasmExecutionMethod) {
}
}
fn mk_test_runtime(wasm_method: WasmExecutionMethod, pages: u64) -> Arc<dyn WasmModule> {
fn mk_test_runtime(
wasm_method: WasmExecutionMethod,
pages: HeapAllocStrategy,
) -> Arc<dyn WasmModule> {
let blob = RuntimeBlob::uncompress_if_needed(wasm_binary_unwrap())
.expect("failed to create a runtime blob out of test runtime");
@@ -490,7 +499,8 @@ fn mk_test_runtime(wasm_method: WasmExecutionMethod, pages: u64) -> Arc<dyn Wasm
test_wasm_execution!(returns_mutable_static);
fn returns_mutable_static(wasm_method: WasmExecutionMethod) {
let runtime = mk_test_runtime(wasm_method, 1024);
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();
@@ -505,7 +515,8 @@ fn returns_mutable_static(wasm_method: WasmExecutionMethod) {
test_wasm_execution!(returns_mutable_static_bss);
fn returns_mutable_static_bss(wasm_method: WasmExecutionMethod) {
let runtime = mk_test_runtime(wasm_method, 1024);
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();
@@ -530,9 +541,12 @@ fn restoration_of_globals(wasm_method: WasmExecutionMethod) {
//
// 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: u64 = 32;
const REQUIRED_MEMORY_PAGES: u32 = 32;
let runtime = mk_test_runtime(wasm_method, REQUIRED_MEMORY_PAGES);
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.
@@ -546,7 +560,7 @@ fn restoration_of_globals(wasm_method: WasmExecutionMethod) {
test_wasm_execution!(interpreted_only heap_is_reset_between_calls);
fn heap_is_reset_between_calls(wasm_method: WasmExecutionMethod) {
let runtime = mk_test_runtime(wasm_method, 1024);
let runtime = mk_test_runtime(wasm_method, HeapAllocStrategy::Static { extra_pages: 1024 });
let mut instance = runtime.new_instance().unwrap();
let heap_base = instance
@@ -651,6 +665,15 @@ fn wasm_tracing_should_work(wasm_method: WasmExecutionMethod) {
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*
@@ -713,7 +736,7 @@ fn memory_is_cleared_between_invocations(wasm_method: WasmExecutionMethod) {
let runtime = crate::wasm_runtime::create_wasm_runtime_with_code::<HostFunctions>(
wasm_method,
1024,
HeapAllocStrategy::Dynamic { maximum_pages: Some(1024) },
RuntimeBlob::uncompress_if_needed(&binary[..]).unwrap(),
true,
None,
+211 -39
View File
@@ -32,14 +32,15 @@ use std::{
use codec::Encode;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{AllocationStats, WasmInstance, WasmModule},
wasm_runtime::{AllocationStats, HeapAllocStrategy, WasmInstance, WasmModule},
};
use sp_core::traits::{CodeExecutor, Externalities, RuntimeCode};
use sp_core::traits::{CallContext, CodeExecutor, Externalities, RuntimeCode};
use sp_version::{GetNativeVersion, NativeVersion, RuntimeVersion};
use sp_wasm_interface::{ExtendedHostFunctions, HostFunctions};
/// Default num of pages for the heap
const DEFAULT_HEAP_PAGES: u64 = 2048;
/// Default heap allocation strategy.
const DEFAULT_HEAP_ALLOC_STRATEGY: HeapAllocStrategy =
HeapAllocStrategy::Static { extra_pages: 2048 };
/// Set up the externalities and safe calling environment to execute runtime calls.
///
@@ -79,13 +80,136 @@ pub trait NativeExecutionDispatch: Send + Sync {
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> {
_phantom: PhantomData<H>,
method: WasmExecutionMethod,
onchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
offchain_heap_alloc_strategy: Option<HeapAllocStrategy>,
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(method: WasmExecutionMethod) -> Self {
Self {
_phantom: PhantomData,
method,
onchain_heap_alloc_strategy: None,
offchain_heap_alloc_strategy: None,
max_runtime_instances: 2,
runtime_cache_size: 4,
allow_missing_host_functions: false,
cache_path: None,
}
}
/// 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 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,
),
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> {
/// Method used to execute fallback Wasm code.
method: WasmExecutionMethod,
/// The number of 64KB pages to allocate for Wasm execution.
default_heap_pages: u64,
/// 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,
/// WASM runtime cache.
cache: Arc<RuntimeCache>,
/// The path to a directory which the executor can leverage for a file cache, e.g. put there
@@ -100,7 +224,8 @@ impl<H> Clone for WasmExecutor<H> {
fn clone(&self) -> Self {
Self {
method: self.method,
default_heap_pages: self.default_heap_pages,
default_onchain_heap_alloc_strategy: self.default_onchain_heap_alloc_strategy,
default_offchain_heap_alloc_strategy: self.default_offchain_heap_alloc_strategy,
cache: self.cache.clone(),
cache_path: self.cache_path.clone(),
allow_missing_host_functions: self.allow_missing_host_functions,
@@ -119,8 +244,10 @@ where
///
/// `method` - Method used to execute Wasm code.
///
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution.
/// Defaults to `DEFAULT_HEAP_PAGES` if `None` is provided.
/// `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.
///
@@ -138,7 +265,12 @@ where
) -> Self {
WasmExecutor {
method,
default_heap_pages: default_heap_pages.unwrap_or(DEFAULT_HEAP_PAGES),
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 _ }),
),
cache: Arc::new(RuntimeCache::new(
max_runtime_instances,
cache_path.clone(),
@@ -150,6 +282,11 @@ where
}
}
/// Instantiate a builder for creating an instance of `Self`.
pub fn builder(method: WasmExecutionMethod) -> WasmExecutorBuilder<H> {
WasmExecutorBuilder::new(method)
}
/// Ignore missing function imports if set true.
pub fn allow_missing_host_functions(&mut self, allow_missing_host_functions: bool) {
self.allow_missing_host_functions = allow_missing_host_functions
@@ -172,6 +309,7 @@ where
&self,
runtime_code: &RuntimeCode,
ext: &mut dyn Externalities,
heap_alloc_strategy: HeapAllocStrategy,
f: F,
) -> Result<R>
where
@@ -186,7 +324,7 @@ where
runtime_code,
ext,
self.method,
self.default_heap_pages,
heap_alloc_strategy,
self.allow_missing_host_functions,
|module, instance, version, ext| {
let module = AssertUnwindSafe(module);
@@ -259,7 +397,7 @@ where
) -> std::result::Result<Vec<u8>, Error> {
let module = crate::wasm_runtime::create_wasm_runtime_with_code::<H>(
self.method,
self.default_heap_pages,
self.default_onchain_heap_alloc_strategy,
runtime_blob,
allow_missing_host_functions,
self.cache_path.as_deref(),
@@ -334,6 +472,7 @@ where
method: &str,
data: &[u8],
_use_native: bool,
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
tracing::trace!(
target: "executor",
@@ -341,10 +480,25 @@ where
"Executing function",
);
let result =
self.with_instance(runtime_code, ext, |_, mut instance, _onchain_version, mut ext| {
let on_chain_heap_alloc_strategy = 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, _onchain_version, mut ext| {
with_externalities_safe(&mut **ext, move || instance.call_export(method, data))
});
},
);
(result, false)
}
}
@@ -358,20 +512,25 @@ where
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
self.with_instance(runtime_code, ext, |_module, _instance, version, _ext| {
Ok(version.cloned().ok_or_else(|| Error::ApiError("Unknown version".into())))
})
let on_chain_heap_pages = 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.
pub struct NativeElseWasmExecutor<D>
where
D: NativeExecutionDispatch,
{
/// Dummy field to avoid the compiler complaining about us not using `D`.
_dummy: PhantomData<D>,
pub struct NativeElseWasmExecutor<D: NativeExecutionDispatch> {
/// Native runtime version info.
native_version: NativeVersion,
/// Fallback wasm executor.
@@ -386,8 +545,10 @@ impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
///
/// `fallback_method` - Method used to execute fallback Wasm code.
///
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution.
/// Defaults to `DEFAULT_HEAP_PAGES` if `None` is provided.
/// `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.
///
@@ -406,11 +567,16 @@ impl<D: NativeExecutionDispatch> NativeElseWasmExecutor<D> {
runtime_cache_size,
);
NativeElseWasmExecutor {
_dummy: Default::default(),
native_version: D::native_version(),
wasm,
}
NativeElseWasmExecutor { native_version: D::native_version(), wasm }
}
/// 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 }
}
/// Ignore missing function imports if set true.
@@ -425,9 +591,7 @@ impl<D: NativeExecutionDispatch> RuntimeVersionOf for NativeElseWasmExecutor<D>
ext: &mut dyn Externalities,
runtime_code: &RuntimeCode,
) -> Result<RuntimeVersion> {
self.wasm.with_instance(runtime_code, ext, |_module, _instance, version, _ext| {
Ok(version.cloned().ok_or_else(|| Error::ApiError("Unknown version".into())))
})
self.wasm.runtime_version(ext, runtime_code)
}
}
@@ -447,6 +611,7 @@ impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecut
method: &str,
data: &[u8],
use_native: bool,
context: CallContext,
) -> (Result<Vec<u8>>, bool) {
tracing::trace!(
target: "executor",
@@ -454,10 +619,21 @@ impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecut
"Executing function",
);
let on_chain_heap_alloc_strategy = 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, onchain_version, mut ext| {
let onchain_version =
onchain_version.ok_or_else(|| Error::ApiError("Unknown version".into()))?;
@@ -496,11 +672,7 @@ impl<D: NativeExecutionDispatch + 'static> CodeExecutor for NativeElseWasmExecut
impl<D: NativeExecutionDispatch> Clone for NativeElseWasmExecutor<D> {
fn clone(&self) -> Self {
NativeElseWasmExecutor {
_dummy: Default::default(),
native_version: D::native_version(),
wasm: self.wasm.clone(),
}
NativeElseWasmExecutor { native_version: D::native_version(), wasm: self.wasm.clone() }
}
}
+15 -15
View File
@@ -27,7 +27,7 @@ use lru::LruCache;
use parking_lot::Mutex;
use sc_executor_common::{
runtime_blob::RuntimeBlob,
wasm_runtime::{WasmInstance, WasmModule},
wasm_runtime::{HeapAllocStrategy, WasmInstance, WasmModule},
};
use sp_core::traits::{Externalities, FetchRuntimeCode, RuntimeCode};
use sp_version::RuntimeVersion;
@@ -64,8 +64,8 @@ struct VersionedRuntimeId {
code_hash: Vec<u8>,
/// Wasm runtime type.
wasm_method: WasmExecutionMethod,
/// The number of WebAssembly heap pages this instance was created with.
heap_pages: u64,
/// The heap allocation strategy this runtime was created with.
heap_alloc_strategy: HeapAllocStrategy,
}
/// A Wasm runtime object along with its cached runtime version.
@@ -197,10 +197,12 @@ impl RuntimeCache {
///
/// `runtime_code` - The runtime wasm code used setup the runtime.
///
/// `default_heap_pages` - Number of 64KB pages to allocate for Wasm execution.
/// `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.
@@ -219,7 +221,7 @@ impl RuntimeCache {
runtime_code: &'c RuntimeCode<'c>,
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
default_heap_pages: u64,
heap_alloc_strategy: HeapAllocStrategy,
allow_missing_func_imports: bool,
f: F,
) -> Result<Result<R, Error>, Error>
@@ -233,10 +235,9 @@ impl RuntimeCache {
) -> Result<R, Error>,
{
let code_hash = &runtime_code.hash;
let heap_pages = runtime_code.heap_pages.unwrap_or(default_heap_pages);
let versioned_runtime_id =
VersionedRuntimeId { code_hash: code_hash.clone(), heap_pages, wasm_method };
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)
@@ -251,7 +252,7 @@ impl RuntimeCache {
&code,
ext,
wasm_method,
heap_pages,
heap_alloc_strategy,
allow_missing_func_imports,
self.max_runtime_instances,
self.cache_path.as_deref(),
@@ -289,7 +290,7 @@ impl RuntimeCache {
/// Create a wasm runtime with the given `code`.
pub fn create_wasm_runtime_with_code<H>(
wasm_method: WasmExecutionMethod,
heap_pages: u64,
heap_alloc_strategy: HeapAllocStrategy,
blob: RuntimeBlob,
allow_missing_func_imports: bool,
cache_path: Option<&Path>,
@@ -307,7 +308,7 @@ where
sc_executor_wasmi::create_runtime(
blob,
heap_pages,
heap_alloc_strategy,
H::host_functions(),
allow_missing_func_imports,
)
@@ -320,12 +321,11 @@ where
allow_missing_func_imports,
cache_path: cache_path.map(ToOwned::to_owned),
semantics: sc_executor_wasmtime::Semantics {
extra_heap_pages: heap_pages,
heap_alloc_strategy,
instantiation_strategy,
deterministic_stack_limit: None,
canonicalize_nans: false,
parallel_compilation: true,
max_memory_size: None,
},
},
)
@@ -393,7 +393,7 @@ fn create_versioned_wasm_runtime<H>(
code: &[u8],
ext: &mut dyn Externalities,
wasm_method: WasmExecutionMethod,
heap_pages: u64,
heap_alloc_strategy: HeapAllocStrategy,
allow_missing_func_imports: bool,
max_instances: usize,
cache_path: Option<&Path>,
@@ -408,11 +408,11 @@ where
// 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: Option<_> = read_embedded_version(&blob)?;
let mut version = read_embedded_version(&blob)?;
let runtime = create_wasm_runtime_with_code::<H>(
wasm_method,
heap_pages,
heap_alloc_strategy,
blob,
allow_missing_func_imports,
cache_path,
+1 -1
View File
@@ -15,7 +15,7 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
log = "0.4.17"
wasmi = "0.13"
wasmi = { version = "0.13.2", features = [ "virtual_memory" ] }
sc-allocator = { version = "4.1.0-dev", path = "../../allocator" }
sc-executor-common = { version = "0.10.0-dev", path = "../common" }
sp-runtime-interface = { version = "7.0.0", path = "../../../primitives/runtime-interface" }
+100 -105
View File
@@ -20,23 +20,58 @@
use std::{cell::RefCell, str, sync::Arc};
use log::{debug, error, trace};
use log::{error, trace};
use wasmi::{
memory_units::Pages,
FuncInstance, ImportsBuilder, MemoryInstance, MemoryRef, Module, ModuleInstance, ModuleRef,
FuncInstance, ImportsBuilder, MemoryRef, Module, ModuleInstance, ModuleRef,
RuntimeValue::{self, I32, I64},
TableRef,
};
use sc_allocator::AllocationStats;
use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
use sc_executor_common::{
error::{Error, MessageWithBacktrace, WasmError},
runtime_blob::{DataSegmentsSnapshot, RuntimeBlob},
wasm_runtime::{InvokeMethod, WasmInstance, WasmModule},
wasm_runtime::{HeapAllocStrategy, InvokeMethod, WasmInstance, WasmModule},
};
use sp_runtime_interface::unpack_ptr_and_len;
use sp_wasm_interface::{Function, FunctionContext, Pointer, Result as WResult, WordSize};
/// Wrapper around [`MemorRef`] that implements [`sc_allocator::Memory`].
struct MemoryWrapper<'a>(&'a MemoryRef);
impl sc_allocator::Memory for MemoryWrapper<'_> {
fn with_access_mut<R>(&mut self, run: impl FnOnce(&mut [u8]) -> R) -> R {
self.0.with_direct_access_mut(run)
}
fn with_access<R>(&self, run: impl FnOnce(&[u8]) -> R) -> R {
self.0.with_direct_access(run)
}
fn pages(&self) -> u32 {
self.0.current_size().0 as _
}
fn max_pages(&self) -> Option<u32> {
self.0.maximum().map(|p| p.0 as _)
}
fn grow(&mut self, additional: u32) -> Result<(), ()> {
self.0
.grow(Pages(additional as _))
.map_err(|e| {
log::error!(
target: "wasm-executor",
"Failed to grow memory by {} pages: {}",
additional,
e,
)
})
.map(drop)
}
}
struct FunctionExecutor {
heap: RefCell<sc_allocator::FreeingBumpHeapAllocator>,
memory: MemoryRef,
@@ -55,7 +90,7 @@ impl FunctionExecutor {
missing_functions: Arc<Vec<String>>,
) -> Result<Self, Error> {
Ok(FunctionExecutor {
heap: RefCell::new(sc_allocator::FreeingBumpHeapAllocator::new(heap_base)),
heap: RefCell::new(FreeingBumpHeapAllocator::new(heap_base)),
memory: m,
host_functions,
allow_missing_func_imports,
@@ -75,15 +110,17 @@ impl FunctionContext for FunctionExecutor {
}
fn allocate_memory(&mut self, size: WordSize) -> WResult<Pointer<u8>> {
let heap = &mut self.heap.borrow_mut();
self.memory
.with_direct_access_mut(|mem| heap.allocate(mem, size).map_err(|e| e.to_string()))
self.heap
.borrow_mut()
.allocate(&mut MemoryWrapper(&self.memory), size)
.map_err(|e| e.to_string())
}
fn deallocate_memory(&mut self, ptr: Pointer<u8>) -> WResult<()> {
let heap = &mut self.heap.borrow_mut();
self.memory
.with_direct_access_mut(|mem| heap.deallocate(mem, ptr).map_err(|e| e.to_string()))
self.heap
.borrow_mut()
.deallocate(&mut MemoryWrapper(&self.memory), ptr)
.map_err(|e| e.to_string())
}
fn register_panic_error_message(&mut self, message: &str) {
@@ -101,26 +138,17 @@ struct Resolver<'a> {
allow_missing_func_imports: bool,
/// All the names of functions for that we did not provide a host function.
missing_functions: RefCell<Vec<String>>,
/// Will be used as initial and maximum size of the imported memory.
heap_pages: usize,
/// By default, runtimes should import memory and this is `Some(_)` after
/// resolving. However, to be backwards compatible, we also support memory
/// exported by the WASM blob (this will be `None` after resolving).
import_memory: RefCell<Option<MemoryRef>>,
}
impl<'a> Resolver<'a> {
fn new(
host_functions: &'a [&'static dyn Function],
allow_missing_func_imports: bool,
heap_pages: usize,
) -> Resolver<'a> {
Resolver {
host_functions,
allow_missing_func_imports,
missing_functions: RefCell::new(Vec::new()),
heap_pages,
import_memory: Default::default(),
}
}
}
@@ -148,7 +176,11 @@ impl<'a> wasmi::ModuleImportResolver for Resolver<'a> {
}
if self.allow_missing_func_imports {
trace!(target: "wasm-executor", "Could not find function `{}`, a stub will be provided instead.", name);
trace!(
target: "wasm-executor",
"Could not find function `{}`, a stub will be provided instead.",
name,
);
let id = self.missing_functions.borrow().len() + self.host_functions.len();
self.missing_functions.borrow_mut().push(name.to_string());
@@ -160,44 +192,12 @@ impl<'a> wasmi::ModuleImportResolver for Resolver<'a> {
fn resolve_memory(
&self,
field_name: &str,
memory_type: &wasmi::MemoryDescriptor,
_: &str,
_: &wasmi::MemoryDescriptor,
) -> Result<MemoryRef, wasmi::Error> {
if field_name == "memory" {
match &mut *self.import_memory.borrow_mut() {
Some(_) =>
Err(wasmi::Error::Instantiation("Memory can not be imported twice!".into())),
memory_ref @ None => {
if memory_type
.maximum()
.map(|m| m.saturating_sub(memory_type.initial()))
.map(|m| self.heap_pages > m as usize)
.unwrap_or(false)
{
Err(wasmi::Error::Instantiation(format!(
"Heap pages ({}) is greater than imported memory maximum ({}).",
self.heap_pages,
memory_type
.maximum()
.map(|m| m.saturating_sub(memory_type.initial()))
.expect("Maximum is set, checked above; qed"),
)))
} else {
let memory = MemoryInstance::alloc(
Pages(memory_type.initial() as usize + self.heap_pages),
Some(Pages(memory_type.initial() as usize + self.heap_pages)),
)?;
*memory_ref = Some(memory.clone());
Ok(memory)
}
},
}
} else {
Err(wasmi::Error::Instantiation(format!(
"Unknown memory reference with name: {}",
field_name
)))
}
Err(wasmi::Error::Instantiation(
"Internal error, wasmi expects that the wasm blob exports memory.".into(),
))
}
}
@@ -358,12 +358,11 @@ fn call_in_wasm_module(
/// Prepare module instance
fn instantiate_module(
heap_pages: usize,
module: &Module,
host_functions: &[&'static dyn Function],
allow_missing_func_imports: bool,
) -> Result<(ModuleRef, Vec<String>, MemoryRef), Error> {
let resolver = Resolver::new(host_functions, allow_missing_func_imports, heap_pages);
let resolver = Resolver::new(host_functions, allow_missing_func_imports);
// start module instantiation. Don't run 'start' function yet.
let intermediate_instance =
ModuleInstance::new(module, &ImportsBuilder::new().with_resolver("env", &resolver))?;
@@ -371,22 +370,10 @@ fn instantiate_module(
// Verify that the module has the heap base global variable.
let _ = get_heap_base(intermediate_instance.not_started_instance())?;
// Get the memory reference. Runtimes should import memory, but to be backwards
// compatible we also support exported memory.
let memory = match resolver.import_memory.into_inner() {
Some(memory) => memory,
None => {
debug!(
target: "wasm-executor",
"WASM blob does not imports memory, falling back to exported memory",
);
let memory = get_mem_instance(intermediate_instance.not_started_instance())?;
memory.grow(Pages(heap_pages)).map_err(|_| Error::Runtime)?;
memory
},
};
// The `module` should export the memory with the correct properties (min, max).
//
// This is ensured by modifying the `RuntimeBlob` before initializing the `Module`.
let memory = get_mem_instance(intermediate_instance.not_started_instance())?;
if intermediate_instance.has_start() {
// Runtime is not allowed to have the `start` function.
@@ -451,8 +438,6 @@ pub struct WasmiRuntime {
/// Enable stub generation for functions that are not available in `host_functions`.
/// These stubs will error when the wasm blob tries to call them.
allow_missing_func_imports: bool,
/// Numer of heap pages this runtime uses.
heap_pages: u64,
global_vals_snapshot: GlobalValsSnapshot,
data_segments_snapshot: DataSegmentsSnapshot,
@@ -461,13 +446,9 @@ pub struct WasmiRuntime {
impl WasmModule for WasmiRuntime {
fn new_instance(&self) -> Result<Box<dyn WasmInstance>, Error> {
// Instantiate this module.
let (instance, missing_functions, memory) = instantiate_module(
self.heap_pages as usize,
&self.module,
&self.host_functions,
self.allow_missing_func_imports,
)
.map_err(|e| WasmError::Instantiation(e.to_string()))?;
let (instance, missing_functions, memory) =
instantiate_module(&self.module, &self.host_functions, self.allow_missing_func_imports)
.map_err(|e| WasmError::Instantiation(e.to_string()))?;
Ok(Box::new(WasmiInstance {
instance,
@@ -477,6 +458,7 @@ impl WasmModule for WasmiRuntime {
host_functions: self.host_functions.clone(),
allow_missing_func_imports: self.allow_missing_func_imports,
missing_functions: Arc::new(missing_functions),
memory_zeroed: true,
}))
}
}
@@ -484,25 +466,26 @@ impl WasmModule for WasmiRuntime {
/// Create a new `WasmiRuntime` given the code. This function loads the module and
/// stores it in the instance.
pub fn create_runtime(
blob: RuntimeBlob,
heap_pages: u64,
mut blob: RuntimeBlob,
heap_alloc_strategy: HeapAllocStrategy,
host_functions: Vec<&'static dyn Function>,
allow_missing_func_imports: bool,
) -> Result<WasmiRuntime, WasmError> {
let data_segments_snapshot =
DataSegmentsSnapshot::take(&blob).map_err(|e| WasmError::Other(e.to_string()))?;
// Make sure we only have exported memory to simplify the code of the wasmi executor.
blob.convert_memory_import_into_export()?;
// Ensure that the memory uses the correct heap pages.
blob.setup_memory_according_to_heap_alloc_strategy(heap_alloc_strategy)?;
let module =
Module::from_parity_wasm_module(blob.into_inner()).map_err(|_| WasmError::InvalidModule)?;
let global_vals_snapshot = {
let (instance, _, _) = instantiate_module(
heap_pages as usize,
&module,
&host_functions,
allow_missing_func_imports,
)
.map_err(|e| WasmError::Instantiation(e.to_string()))?;
let (instance, _, _) =
instantiate_module(&module, &host_functions, allow_missing_func_imports)
.map_err(|e| WasmError::Instantiation(e.to_string()))?;
GlobalValsSnapshot::take(&instance)
};
@@ -512,7 +495,6 @@ pub fn create_runtime(
global_vals_snapshot,
host_functions: Arc::new(host_functions),
allow_missing_func_imports,
heap_pages,
})
}
@@ -522,6 +504,8 @@ pub struct WasmiInstance {
instance: ModuleRef,
/// The memory instance of used by the wasm module.
memory: MemoryRef,
/// Is the memory zeroed?
memory_zeroed: bool,
/// The snapshot of global variable values just after instantiation.
global_vals_snapshot: GlobalValsSnapshot,
/// The snapshot of data segments.
@@ -549,14 +533,16 @@ impl WasmiInstance {
// We reuse a single wasm instance for multiple calls and a previous call (if any)
// altered the state. Therefore, we need to restore the instance to original state.
// First, zero initialize the linear memory.
self.memory.erase().map_err(|e| {
// Snapshot restoration failed. This is pretty unexpected since this can happen
// if some invariant is broken or if the system is under extreme memory pressure
// (so erasing fails).
error!(target: "wasm-executor", "snapshot restoration failed: {}", e);
WasmError::ErasingFailed(e.to_string())
})?;
if !self.memory_zeroed {
// First, zero initialize the linear memory.
self.memory.erase().map_err(|e| {
// Snapshot restoration failed. This is pretty unexpected since this can happen
// if some invariant is broken or if the system is under extreme memory pressure
// (so erasing fails).
error!(target: "wasm-executor", "snapshot restoration failed: {}", e);
WasmError::ErasingFailed(e.to_string())
})?;
}
// Second, reapply data segments into the linear memory.
self.data_segments_snapshot
@@ -565,7 +551,7 @@ impl WasmiInstance {
// Third, restore the global variables to their initial values.
self.global_vals_snapshot.apply(&self.instance)?;
call_in_wasm_module(
let res = call_in_wasm_module(
&self.instance,
&self.memory,
method,
@@ -574,7 +560,12 @@ impl WasmiInstance {
self.allow_missing_func_imports,
self.missing_functions.clone(),
allocation_stats,
)
);
// If we couldn't unmap it, erase the memory.
self.memory_zeroed = self.memory.erase().is_ok();
res
}
}
@@ -601,4 +592,8 @@ impl WasmInstance for WasmiInstance {
None => Ok(None),
}
}
fn linear_memory_base_ptr(&self) -> Option<*const u8> {
Some(self.memory.direct_access().as_ref().as_ptr())
}
}
@@ -13,9 +13,9 @@ readme = "README.md"
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
log = "0.4.17"
cfg-if = "1.0"
libc = "0.2.121"
log = "0.4.17"
# When bumping wasmtime do not forget to also bump rustix
# to exactly the same version as used by wasmtime!
+37 -14
View File
@@ -24,20 +24,25 @@ use wasmtime::Caller;
use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator};
use sp_wasm_interface::{Pointer, WordSize};
use crate::{runtime::StoreData, util};
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 {
allocator: FreeingBumpHeapAllocator,
/// The allocator instance to keep track of allocated memory.
///
/// This is stored as an `Option` as we need to temporarly 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, panic_message: None }
HostState { allocator: Some(allocator), panic_message: None }
}
/// Takes the error message out of the host state, leaving a `None` in its place.
@@ -46,7 +51,9 @@ impl HostState {
}
pub(crate) fn allocation_stats(&self) -> AllocationStats {
self.allocator.stats()
self.allocator.as_ref()
.expect("Allocator is always set and only unavailable when doing an allocation/deallocation; qed")
.stats()
}
}
@@ -81,22 +88,38 @@ impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> {
fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result<Pointer<u8>> {
let memory = self.caller.data().memory();
let (memory, data) = memory.data_and_store_mut(&mut self.caller);
data.host_state_mut()
.expect("host state is not empty when calling a function in wasm; qed")
let mut allocator = self
.host_state_mut()
.allocator
.allocate(memory, size)
.map_err(|e| e.to_string())
.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 (memory, data) = memory.data_and_store_mut(&mut self.caller);
data.host_state_mut()
.expect("host state is not empty when calling a function in wasm; qed")
let mut allocator = self
.host_state_mut()
.allocator
.deallocate(memory, ptr)
.map_err(|e| e.to_string())
.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) {
@@ -26,8 +26,7 @@ use sc_executor_common::{
};
use sp_wasm_interface::{Pointer, Value, WordSize};
use wasmtime::{
AsContext, AsContextMut, Engine, Extern, Func, Global, Instance, InstancePre, Memory, Table,
Val,
AsContext, AsContextMut, Engine, Extern, Instance, InstancePre, Memory, Table, Val,
};
/// Invoked entrypoint format.
@@ -113,66 +112,58 @@ impl 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<R>(&self, run: impl FnOnce(&[u8]) -> R) -> R {
run(self.0.data(&self.1))
}
fn with_access_mut<R>(&mut self, run: impl FnOnce(&mut [u8]) -> R) -> R {
run(self.0.data_mut(&mut 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,
/// The memory instance of the `instance`.
///
/// It is important to make sure that we don't make any copies of this to make it easier to
/// proof
memory: Memory,
store: Store,
}
fn extern_memory(extern_: &Extern) -> Option<&Memory> {
match extern_ {
Extern::Memory(mem) => Some(mem),
_ => None,
}
}
fn extern_global(extern_: &Extern) -> Option<&Global> {
match extern_ {
Extern::Global(glob) => Some(glob),
_ => None,
}
}
fn extern_table(extern_: &Extern) -> Option<&Table> {
match extern_ {
Extern::Table(table) => Some(table),
_ => None,
}
}
fn extern_func(extern_: &Extern) -> Option<&Func> {
match extern_ {
Extern::Func(func) => Some(func),
_ => None,
}
}
pub(crate) fn create_store(engine: &wasmtime::Engine, max_memory_size: Option<usize>) -> Store {
let limits = if let Some(max_memory_size) = max_memory_size {
wasmtime::StoreLimitsBuilder::new().memory_size(max_memory_size).build()
} else {
Default::default()
};
let mut store =
Store::new(engine, StoreData { limits, host_state: None, memory: None, table: None });
if max_memory_size.is_some() {
store.limiter(|s| &mut s.limits);
}
store
}
impl InstanceWrapper {
pub(crate) fn new(
engine: &Engine,
instance_pre: &InstancePre<StoreData>,
max_memory_size: Option<usize>,
) -> Result<Self> {
let mut store = create_store(engine, max_memory_size);
pub(crate) fn new(engine: &Engine, instance_pre: &InstancePre<StoreData>) -> Result<Self> {
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: {:#}",
@@ -201,9 +192,10 @@ impl InstanceWrapper {
self.instance.get_export(&mut self.store, method).ok_or_else(|| {
Error::from(format!("Exported method {} is not found", method))
})?;
let func = extern_func(&export)
let func = export
.into_func()
.ok_or_else(|| Error::from(format!("Export {} is not a function", method)))?;
EntryPoint::direct(*func, &self.store).map_err(|_| {
EntryPoint::direct(func, &self.store).map_err(|_| {
Error::from(format!("Exported function '{}' has invalid signature.", method))
})?
},
@@ -259,7 +251,8 @@ impl InstanceWrapper {
.get_export(&mut self.store, "__heap_base")
.ok_or_else(|| Error::from("__heap_base is not found"))?;
let heap_base_global = extern_global(&heap_base_export)
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
@@ -277,7 +270,7 @@ impl InstanceWrapper {
None => return Ok(None),
};
let global = extern_global(&global).ok_or_else(|| format!("`{}` is not a global", name))?;
let global = global.into_global().ok_or_else(|| format!("`{}` is not a global", name))?;
match global.get(&mut self.store) {
Val::I32(val) => Ok(Some(Value::I32(val))),
@@ -300,7 +293,8 @@ fn get_linear_memory(instance: &Instance, ctx: impl AsContextMut) -> Result<Memo
.get_export(ctx, "memory")
.ok_or_else(|| Error::from("memory is not exported under `memory` name"))?;
let memory = *extern_memory(&memory_export)
let memory = memory_export
.into_memory()
.ok_or_else(|| Error::from("the `memory` export should have memory type"))?;
Ok(memory)
@@ -311,8 +305,8 @@ fn get_table(instance: &Instance, ctx: &mut Store) -> Option<Table> {
instance
.get_export(ctx, "__indirect_function_table")
.as_ref()
.and_then(extern_table)
.cloned()
.and_then(Extern::into_table)
}
/// Functions related to memory.
@@ -403,7 +397,7 @@ fn decommit_works() {
let module = wasmtime::Module::new(&engine, code).unwrap();
let linker = wasmtime::Linker::new(&engine);
let instance_pre = linker.instantiate_pre(&module).unwrap();
let mut wrapper = InstanceWrapper::new(&engine, &instance_pre, None).unwrap();
let mut wrapper = InstanceWrapper::new(&engine, &instance_pre).unwrap();
unsafe { *wrapper.memory.data_ptr(&wrapper.store) = 42 };
assert_eq!(unsafe { *wrapper.memory.data_ptr(&wrapper.store) }, 42);
wrapper.decommit();
@@ -20,7 +20,7 @@
use crate::{
host::HostState,
instance_wrapper::{EntryPoint, InstanceWrapper},
instance_wrapper::{EntryPoint, InstanceWrapper, MemoryWrapper},
util::{self, replace_strategy_if_broken},
};
@@ -31,7 +31,7 @@ use sc_executor_common::{
self, DataSegmentsSnapshot, ExposedMutableGlobalsSet, GlobalsSnapshot, RuntimeBlob,
},
util::checked_range,
wasm_runtime::{InvokeMethod, WasmInstance, WasmModule},
wasm_runtime::{HeapAllocStrategy, InvokeMethod, WasmInstance, WasmModule},
};
use sp_runtime_interface::unpack_ptr_and_len;
use sp_wasm_interface::{HostFunctions, Pointer, Value, WordSize};
@@ -42,12 +42,10 @@ use std::{
Arc,
},
};
use wasmtime::{AsContext, Engine, Memory, StoreLimits, Table};
use wasmtime::{AsContext, Engine, Memory, Table};
#[derive(Default)]
pub(crate) struct StoreData {
/// The limits we apply to the store. We need to store it here to return a reference to this
/// object when we have the limits enabled.
pub(crate) limits: StoreLimits,
/// 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.
@@ -83,12 +81,11 @@ enum Strategy {
struct InstanceCreator {
engine: wasmtime::Engine,
instance_pre: Arc<wasmtime::InstancePre<StoreData>>,
max_memory_size: Option<usize>,
}
impl InstanceCreator {
fn instantiate(&mut self) -> Result<InstanceWrapper> {
InstanceWrapper::new(&self.engine, &self.instance_pre, self.max_memory_size)
InstanceWrapper::new(&self.engine, &self.instance_pre)
}
}
@@ -128,18 +125,13 @@ pub struct WasmtimeRuntime {
engine: wasmtime::Engine,
instance_pre: Arc<wasmtime::InstancePre<StoreData>>,
instantiation_strategy: InternalInstantiationStrategy,
config: Config,
}
impl WasmModule for WasmtimeRuntime {
fn new_instance(&self) -> Result<Box<dyn WasmInstance>> {
let strategy = match self.instantiation_strategy {
InternalInstantiationStrategy::LegacyInstanceReuse(ref snapshot_data) => {
let mut instance_wrapper = InstanceWrapper::new(
&self.engine,
&self.instance_pre,
self.config.semantics.max_memory_size,
)?;
let mut instance_wrapper = InstanceWrapper::new(&self.engine, &self.instance_pre)?;
let heap_base = instance_wrapper.extract_heap_base()?;
// This function panics if the instance was created from a runtime blob different
@@ -161,7 +153,6 @@ impl WasmModule for WasmtimeRuntime {
InternalInstantiationStrategy::Builtin => Strategy::RecreateInstance(InstanceCreator {
engine: self.engine.clone(),
instance_pre: self.instance_pre.clone(),
max_memory_size: self.config.semantics.max_memory_size,
}),
};
@@ -350,25 +341,22 @@ fn common_config(semantics: &Semantics) -> std::result::Result<wasmtime::Config,
InstantiationStrategy::LegacyInstanceReuse => (false, false),
};
const WASM_PAGE_SIZE: u64 = 65536;
config.memory_init_cow(use_cow);
config.memory_guaranteed_dense_image_size(
semantics.max_memory_size.map(|max| max as u64).unwrap_or(u64::MAX),
);
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 WASM_PAGE_SIZE: u64 = 65536;
const MAX_WASM_PAGES: u64 = 0x10000;
let memory_pages = if let Some(max_memory_size) = semantics.max_memory_size {
let max_memory_size = max_memory_size as u64;
let mut pages = max_memory_size / WASM_PAGE_SIZE;
if max_memory_size % WASM_PAGE_SIZE != 0 {
pages += 1;
}
std::cmp::min(MAX_WASM_PAGES, pages)
} else {
MAX_WASM_PAGES
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();
@@ -514,25 +502,8 @@ pub struct Semantics {
/// Configures wasmtime to use multiple threads for compiling.
pub parallel_compilation: bool,
/// The number of extra WASM pages which will be allocated
/// on top of what is requested by the WASM blob itself.
pub extra_heap_pages: u64,
/// The total amount of memory in bytes an instance can request.
///
/// If specified, the runtime will be able to allocate only that much of wasm memory.
/// This is the total number and therefore the [`Semantics::extra_heap_pages`] is accounted
/// for.
///
/// That means that the initial number of pages of a linear memory plus the
/// [`Semantics::extra_heap_pages`] multiplied by the wasm page size (64KiB) should be less
/// than or equal to `max_memory_size`, otherwise the instance won't be created.
///
/// Moreover, `memory.grow` will fail (return -1) if the sum of sizes of currently mounted
/// and additional pages exceeds `max_memory_size`.
///
/// The default is `None`.
pub max_memory_size: Option<usize>,
/// The heap allocation strategy to use.
pub heap_alloc_strategy: HeapAllocStrategy,
}
#[derive(Clone)]
@@ -689,12 +660,7 @@ where
.instantiate_pre(&module)
.map_err(|e| WasmError::Other(format!("cannot preinstantiate module: {:#}", e)))?;
Ok(WasmtimeRuntime {
engine,
instance_pre: Arc::new(instance_pre),
instantiation_strategy,
config,
})
Ok(WasmtimeRuntime { engine, instance_pre: Arc::new(instance_pre), instantiation_strategy })
}
fn prepare_blob_for_compilation(
@@ -717,12 +683,7 @@ fn prepare_blob_for_compilation(
// 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.add_extra_heap_pages_to_memory_section(
semantics
.extra_heap_pages
.try_into()
.map_err(|e| WasmError::Other(format!("invalid `extra_heap_pages`: {}", e)))?,
)?;
blob.setup_memory_according_to_heap_alloc_strategy(semantics.heap_alloc_strategy)?;
Ok(blob)
}
@@ -783,9 +744,8 @@ fn inject_input_data(
) -> Result<(Pointer<u8>, WordSize)> {
let mut ctx = instance.store_mut();
let memory = ctx.data().memory();
let memory = memory.data_mut(&mut ctx);
let data_len = data.len() as WordSize;
let data_ptr = allocator.allocate(memory, data_len)?;
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))
}
+96 -158
View File
@@ -17,7 +17,11 @@
// 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::WasmModule};
use sc_executor_common::{
error::Error,
runtime_blob::RuntimeBlob,
wasm_runtime::{HeapAllocStrategy, WasmModule},
};
use sc_runtime_test::wasm_binary_unwrap;
use crate::InstantiationStrategy;
@@ -77,8 +81,7 @@ struct RuntimeBuilder {
instantiation_strategy: InstantiationStrategy,
canonicalize_nans: bool,
deterministic_stack: bool,
extra_heap_pages: u64,
max_memory_size: Option<usize>,
heap_pages: HeapAllocStrategy,
precompile_runtime: bool,
tmpdir: Option<tempfile::TempDir>,
}
@@ -90,8 +93,7 @@ impl RuntimeBuilder {
instantiation_strategy,
canonicalize_nans: false,
deterministic_stack: false,
extra_heap_pages: 1024,
max_memory_size: None,
heap_pages: HeapAllocStrategy::Static { extra_pages: 1024 },
precompile_runtime: false,
tmpdir: None,
}
@@ -117,8 +119,8 @@ impl RuntimeBuilder {
self
}
fn max_memory_size(mut self, max_memory_size: Option<usize>) -> Self {
self.max_memory_size = max_memory_size;
fn heap_alloc_strategy(mut self, heap_pages: HeapAllocStrategy) -> Self {
self.heap_pages = heap_pages;
self
}
@@ -152,8 +154,7 @@ impl RuntimeBuilder {
},
canonicalize_nans: self.canonicalize_nans,
parallel_compilation: true,
extra_heap_pages: self.extra_heap_pages,
max_memory_size: self.max_memory_size,
heap_alloc_strategy: self.heap_pages,
},
};
@@ -227,7 +228,7 @@ fn deep_call_stack_wat(depth: usize) -> String {
// 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 = 65455;
const CALL_DEPTH_UPPER_LIMIT: usize = 65503;
const CALL_DEPTH_UPPER_LIMIT: usize = 65509;
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) {
@@ -344,29 +345,25 @@ fn test_max_memory_pages(
import_memory: bool,
precompile_runtime: bool,
) {
fn try_instantiate(
max_memory_size: Option<usize>,
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)
.max_memory_size(max_memory_size)
.heap_alloc_strategy(heap_alloc_strategy)
.precompile_runtime(precompile_runtime);
let runtime = builder.build();
let mut instance = runtime.new_instance()?;
let mut instance = runtime.new_instance().unwrap();
let _ = instance.call_export("main", &[])?;
Ok(())
}
fn memory(initial: u32, maximum: Option<u32>, import: bool) -> String {
let memory = if let Some(maximum) = maximum {
format!("(memory $0 {} {})", initial, maximum)
} else {
format!("(memory $0 {})", initial)
};
fn memory(initial: u32, maximum: u32, import: bool) -> String {
let memory = format!("(memory $0 {} {})", initial, maximum);
if import {
format!("(import \"env\" \"memory\" {})", memory)
@@ -375,152 +372,90 @@ fn test_max_memory_pages(
}
}
const WASM_PAGE_SIZE: usize = 65536;
let assert_grow_ok = |alloc_strategy: HeapAllocStrategy, initial_pages: u32, max_pages: u32| {
eprintln!("assert_grow_ok({alloc_strategy:?}, {initial_pages}, {max_pages})");
// check the old behavior if preserved. That is, if no limit is set we allow 4 GiB of memory.
try_instantiate(
None,
format!(
r#"
(module
{}
(global (export "__heap_base") i32 (i32.const 0))
(func (export "main")
(param i32 i32) (result i64)
(i64.const 0)
)
)
"#,
/*
We want to allocate the maximum number of pages supported in wasm for this test.
However, due to a bug in wasmtime (I think wasmi is also affected) it is only possible
to allocate 65536 - 1 pages.
call(
alloc_strategy,
format!(
r#"
(module
{}
(global (export "__heap_base") i32 (i32.const 0))
(func (export "main")
(param i32 i32) (result i64)
Then, during creation of the Substrate Runtime instance, 1024 (heap_pages) pages are
mounted.
Thus 65535 = 64511 + 1024
*/
memory(64511, None, import_memory)
),
instantiation_strategy,
precompile_runtime,
)
.unwrap();
// max is not specified, therefore it's implied to be 65536 pages (4 GiB).
//
// max_memory_size = (1 (initial) + 1024 (heap_pages)) * WASM_PAGE_SIZE
try_instantiate(
Some((1 + 1024) * WASM_PAGE_SIZE),
format!(
r#"
(module
{}
(global (export "__heap_base") i32 (i32.const 0))
(func (export "main")
(param i32 i32) (result i64)
(i64.const 0)
)
)
"#,
// 1 initial, max is not specified.
memory(1, None, import_memory)
),
instantiation_strategy,
precompile_runtime,
)
.unwrap();
// max is specified explicitly to 2048 pages.
try_instantiate(
Some((1 + 1024) * WASM_PAGE_SIZE),
format!(
r#"
(module
{}
(global (export "__heap_base") i32 (i32.const 0))
(func (export "main")
(param i32 i32) (result i64)
(i64.const 0)
)
)
"#,
// Max is 2048.
memory(1, Some(2048), import_memory)
),
instantiation_strategy,
precompile_runtime,
)
.unwrap();
// memory grow should work as long as it doesn't exceed 1025 pages in total.
try_instantiate(
Some((0 + 1024 + 25) * WASM_PAGE_SIZE),
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 25)
;; assert(memory.grow returns != -1)
(if
(i32.eq
(memory.grow
(i32.const 1)
)
(i32.const -1)
)
(unreachable)
)
(i32.const -1)
(i64.const 0)
)
(unreachable)
)
(i64.const 0)
)
)
"#,
// Zero starting pages.
memory(0, None, import_memory)
),
instantiation_strategy,
precompile_runtime,
)
.unwrap();
memory(initial_pages, max_pages, import_memory)
),
instantiation_strategy,
precompile_runtime,
)
.unwrap()
};
// We start with 1025 pages and try to grow at least one.
try_instantiate(
Some((1 + 1024) * WASM_PAGE_SIZE),
format!(
r#"
(module
{}
(global (export "__heap_base") i32 (i32.const 0))
(func (export "main")
(param i32 i32) (result i64)
let assert_grow_fail =
|alloc_strategy: HeapAllocStrategy, initial_pages: u32, max_pages: u32| {
eprintln!("assert_grow_fail({alloc_strategy:?}, {initial_pages}, {max_pages})");
;; assert(memory.grow returns == -1)
(if
(i32.ne
(memory.grow
(i32.const 1)
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)
)
(unreachable)
)
(i64.const 0)
)
(i32.const -1)
)
(unreachable)
)
(i64.const 0)
)
"#,
memory(initial_pages, max_pages, import_memory)
),
instantiation_strategy,
precompile_runtime,
)
"#,
// Initial=1, meaning after heap pages mount the total will be already 1025.
memory(1, None, import_memory)
),
instantiation_strategy,
precompile_runtime,
)
.unwrap();
.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)
@@ -538,8 +473,7 @@ fn test_instances_without_reuse_are_not_leaked() {
deterministic_stack_limit: None,
canonicalize_nans: false,
parallel_compilation: true,
extra_heap_pages: 2048,
max_memory_size: None,
heap_alloc_strategy: HeapAllocStrategy::Static { extra_pages: 2048 },
},
},
)
@@ -583,6 +517,10 @@ fn test_rustix_version_matches_with_wasmtime() {
.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);
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,
);
}
}