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
@@ -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,
);
}
}