From be72e5df46de2fa5805049e3037d0db2c4c25635 Mon Sep 17 00:00:00 2001 From: Sergei Shulepov Date: Mon, 2 Aug 2021 21:15:07 +0200 Subject: [PATCH] Limit the maximum number of wasm memory pages a runtime can have (#9308) * Limit the maximum number of wasm memory pages a runtime can have * Switch the argument order * fmt --- client/executor/src/wasm_runtime.rs | 1 + client/executor/wasmtime/src/runtime.rs | 29 ++++- client/executor/wasmtime/src/tests.rs | 149 +++++++++++++++++++++++- 3 files changed, 177 insertions(+), 2 deletions(-) diff --git a/client/executor/src/wasm_runtime.rs b/client/executor/src/wasm_runtime.rs index 8674e7239255b..c55af60b70a9f 100644 --- a/client/executor/src/wasm_runtime.rs +++ b/client/executor/src/wasm_runtime.rs @@ -321,6 +321,7 @@ pub fn create_wasm_runtime_with_code( blob, sc_executor_wasmtime::Config { heap_pages: heap_pages as u32, + max_memory_pages: None, allow_missing_func_imports, cache_path: cache_path.map(ToOwned::to_owned), semantics: sc_executor_wasmtime::Semantics { diff --git a/client/executor/wasmtime/src/runtime.rs b/client/executor/wasmtime/src/runtime.rs index b69eac6266bb1..d4a2a28394b52 100644 --- a/client/executor/wasmtime/src/runtime.rs +++ b/client/executor/wasmtime/src/runtime.rs @@ -79,9 +79,22 @@ pub struct WasmtimeRuntime { engine: Engine, } +impl WasmtimeRuntime { + /// Creates the store respecting the set limits. + fn new_store(&self) -> Store { + match self.config.max_memory_pages { + Some(max_memory_pages) => Store::new_with_limits( + &self.engine, + wasmtime::StoreLimitsBuilder::new().memory_pages(max_memory_pages).build(), + ), + None => Store::new(&self.engine), + } + } +} + impl WasmModule for WasmtimeRuntime { fn new_instance(&self) -> Result> { - let store = Store::new(&self.engine); + let store = self.new_store(); // Scan all imports, find the matching host functions, and create stubs that adapt arguments // and results. @@ -353,6 +366,20 @@ pub struct Config { /// The number of wasm pages to be mounted after instantiation. pub heap_pages: u32, + /// The total number of wasm pages an instance can request. + /// + /// If specified, the runtime will be able to allocate only that much of wasm memory pages. This + /// is the total number and therefore the [`heap_pages`] is accounted for. + /// + /// That means that the initial number of pages of a linear memory plus the [`heap_pages`] should + /// be less or equal to `max_memory_pages`, otherwise the instance won't be created. + /// + /// Moreover, `memory.grow` will fail (return -1) if the sum of the number of currently mounted + /// pages and the number of additional pages exceeds `max_memory_pages`. + /// + /// The default is `None`. + pub max_memory_pages: Option, + /// The WebAssembly standard requires all imports of an instantiated module to be resolved, /// othewise, the instantiation fails. If this option is set to `true`, then this behavior is /// overriden and imports that are requested by the module and not provided by the host functions diff --git a/client/executor/wasmtime/src/tests.rs b/client/executor/wasmtime/src/tests.rs index 7933578b80499..366352d7f5c39 100644 --- a/client/executor/wasmtime/src/tests.rs +++ b/client/executor/wasmtime/src/tests.rs @@ -29,6 +29,7 @@ struct RuntimeBuilder { canonicalize_nans: bool, deterministic_stack: bool, heap_pages: u32, + max_memory_pages: Option, } impl RuntimeBuilder { @@ -41,6 +42,7 @@ impl RuntimeBuilder { canonicalize_nans: false, deterministic_stack: false, heap_pages: 1024, + max_memory_pages: None, } } @@ -56,6 +58,10 @@ impl RuntimeBuilder { self.deterministic_stack = deterministic_stack; } + fn max_memory_pages(&mut self, max_memory_pages: Option) { + self.max_memory_pages = max_memory_pages; + } + fn build(self) -> Arc { let blob = { let wasm: Vec; @@ -63,7 +69,7 @@ impl RuntimeBuilder { let wasm = match self.code { None => wasm_binary_unwrap(), Some(wat) => { - wasm = wat::parse_str(wat).unwrap(); + wasm = wat::parse_str(wat).expect("wat parsing failed"); &wasm }, }; @@ -76,6 +82,7 @@ impl RuntimeBuilder { blob, crate::Config { heap_pages: self.heap_pages, + max_memory_pages: self.max_memory_pages, allow_missing_func_imports: true, cache_path: None, semantics: crate::Semantics { @@ -160,3 +167,143 @@ fn test_stack_depth_reaching() { format!("{:?}", err).starts_with("Other(\"Wasm execution trapped: wasm trap: unreachable") ); } + +#[test] +fn test_max_memory_pages() { + fn try_instantiate( + max_memory_pages: Option, + wat: &'static str, + ) -> Result<(), Box> { + let runtime = { + let mut builder = RuntimeBuilder::new_on_demand(); + builder.use_wat(wat); + builder.max_memory_pages(max_memory_pages); + builder.build() + }; + let instance = runtime.new_instance()?; + let _ = instance.call_export("main", &[])?; + Ok(()) + } + + // check the old behavior if preserved. That is, if no limit is set we allow 4 GiB of memory. + try_instantiate( + None, + r#" + (module + ;; 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. + ;; + ;; Then, during creation of the Substrate Runtime instance, 1024 (heap_pages) pages are + ;; mounted. + ;; + ;; Thus 65535 = 64511 + 1024 + (import "env" "memory" (memory 64511)) + + (global (export "__heap_base") i32 (i32.const 0)) + (func (export "main") + (param i32 i32) (result i64) + (i64.const 0) + ) + ) + "#, + ) + .unwrap(); + + // max is not specified, therefore it's implied to be 65536 pages (4 GiB). + // + // max_memory_pages = 1 (initial) + 1024 (heap_pages) + try_instantiate( + Some(1 + 1024), + r#" + (module + + (import "env" "memory" (memory 1)) ;; <- 1 initial, max is not specified + + (global (export "__heap_base") i32 (i32.const 0)) + (func (export "main") + (param i32 i32) (result i64) + (i64.const 0) + ) + ) + "#, + ) + .unwrap(); + + // max is specified explicitly to 2048 pages. + try_instantiate( + Some(1 + 1024), + r#" + (module + + (import "env" "memory" (memory 1 2048)) ;; <- max is 2048 + + (global (export "__heap_base") i32 (i32.const 0)) + (func (export "main") + (param i32 i32) (result i64) + (i64.const 0) + ) + ) + "#, + ) + .unwrap(); + + // memory grow should work as long as it doesn't exceed 1025 pages in total. + try_instantiate( + Some(0 + 1024 + 25), + r#" + (module + (import "env" "memory" (memory 0)) ;; <- zero starting pages. + + (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) + ) + (i32.const -1) + ) + (unreachable) + ) + + (i64.const 0) + ) + ) + "#, + ) + .unwrap(); + + // We start with 1025 pages and try to grow at least one. + try_instantiate( + Some(1 + 1024), + r#" + (module + (import "env" "memory" (memory 1)) ;; <- initial=1, meaning after heap pages mount the + ;; total will be already 1025 + (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) + ) + ) + "#, + ) + .unwrap(); +}