diff --git a/core/executor/src/error.rs b/core/executor/src/error.rs index a81fc1b148287..195ef1820c2d2 100644 --- a/core/executor/src/error.rs +++ b/core/executor/src/error.rs @@ -38,8 +38,8 @@ pub enum Error { #[display(fmt="Method not found: '{}'", _0)] MethodNotFound(String), /// Code is invalid (expected single byte) - #[display(fmt="Invalid Code: {:?}", _0)] - InvalidCode(Vec), + #[display(fmt="Invalid Code")] + InvalidCode, /// Could not get runtime version. #[display(fmt="On-chain runtime does not specify version")] VersionInvalid, diff --git a/core/executor/src/lib.rs b/core/executor/src/lib.rs index fa7cc71eea6d6..a6147a320a7ec 100644 --- a/core/executor/src/lib.rs +++ b/core/executor/src/lib.rs @@ -35,11 +35,13 @@ mod wasm_executor; mod native_executor; mod sandbox; mod allocator; +mod wasm_runtimes_cache; pub mod error; pub use wasmi; pub use wasm_executor::WasmExecutor; pub use native_executor::{with_native_environment, NativeExecutor, NativeExecutionDispatch}; +pub use wasm_runtimes_cache::RuntimesCache; pub use state_machine::Externalities; pub use runtime_version::{RuntimeVersion, NativeVersion}; pub use parity_codec::Codec; diff --git a/core/executor/src/native_executor.rs b/core/executor/src/native_executor.rs index e4a65c811bf6a..506cd46afddf8 100644 --- a/core/executor/src/native_executor.rs +++ b/core/executor/src/native_executor.rs @@ -14,86 +14,20 @@ // You should have received a copy of the GNU General Public License // along with Substrate. If not, see . -use std::{borrow::BorrowMut, result, cell::{RefMut, RefCell}}; +use std::{result, cell::RefCell, panic::UnwindSafe}; use crate::error::{Error, Result}; use state_machine::{CodeExecutor, Externalities}; use crate::wasm_executor::WasmExecutor; -use wasmi::{Module as WasmModule, ModuleRef as WasmModuleInstanceRef}; use runtime_version::{NativeVersion, RuntimeVersion}; -use std::{collections::HashMap, panic::UnwindSafe}; use parity_codec::{Decode, Encode}; use crate::RuntimeInfo; use primitives::{Blake2Hasher, NativeOrEncoded}; -use primitives::storage::well_known_keys; use log::trace; -/// Default num of pages for the heap -const DEFAULT_HEAP_PAGES: u64 = 1024; - -// For the internal Runtime Cache: -// Is it compatible enough to run this natively or do we need to fall back on the WasmModule - -enum RuntimePreproc { - InvalidCode, - ValidCode(WasmModuleInstanceRef, Option), -} - -type CacheType = HashMap<[u8; 32], RuntimePreproc>; +use crate::RuntimesCache; thread_local! { - static RUNTIMES_CACHE: RefCell = RefCell::new(HashMap::new()); -} - -/// fetch a runtime version from the cache or if there is no cached version yet, create -/// the runtime version entry for `code`, determines whether `Compatibility::IsCompatible` -/// can be used by comparing returned RuntimeVersion to `ref_version` -fn fetch_cached_runtime_version<'a, E: Externalities>( - wasm_executor: &WasmExecutor, - cache: &'a mut RefMut, - ext: &mut E, - default_heap_pages: Option, -) -> Result<(&'a WasmModuleInstanceRef, &'a Option)> { - let code_hash = match ext.original_storage_hash(well_known_keys::CODE) { - Some(code_hash) => code_hash, - None => return Err(Error::InvalidCode(vec![])), - }; - - let maybe_runtime_preproc = cache.borrow_mut().entry(code_hash.into()) - .or_insert_with(|| { - let code = match ext.original_storage(well_known_keys::CODE) { - Some(code) => code, - None => return RuntimePreproc::InvalidCode, - }; - let heap_pages = ext.storage(well_known_keys::HEAP_PAGES) - .and_then(|pages| u64::decode(&mut &pages[..])) - .or(default_heap_pages) - .unwrap_or(DEFAULT_HEAP_PAGES); - match WasmModule::from_buffer(code) - .map_err(|_| Error::InvalidCode(vec![])) - .and_then(|module| wasm_executor.prepare_module(ext, heap_pages as usize, &module)) - { - Ok(module) => { - let version = wasm_executor.call_in_wasm_module(ext, &module, "Core_version", &[]) - .ok() - .and_then(|v| RuntimeVersion::decode(&mut v.as_slice())); - RuntimePreproc::ValidCode(module, version) - } - Err(e) => { - trace!(target: "executor", "Invalid code presented to executor ({:?})", e); - RuntimePreproc::InvalidCode - } - } - }); - - match maybe_runtime_preproc { - RuntimePreproc::InvalidCode => { - let code = ext.original_storage(well_known_keys::CODE).unwrap_or(vec![]); - Err(Error::InvalidCode(code)) - }, - RuntimePreproc::ValidCode(m, v) => { - Ok((m, v)) - } - } + static RUNTIMES_CACHE: RefCell = RefCell::new(RuntimesCache::new()); } fn safe_call(f: F) -> Result @@ -140,18 +74,18 @@ pub struct NativeExecutor { fallback: WasmExecutor, /// Native runtime version info. native_version: NativeVersion, - /// The default number of 64KB pages to allocate for Wasm execution. - default_heap_pages: Option, + /// The number of 64KB pages to allocate for Wasm execution. + initial_heap_pages: Option, } impl NativeExecutor { /// Create new instance. - pub fn new(default_heap_pages: Option) -> Self { + pub fn new(initial_heap_pages: Option) -> Self { NativeExecutor { _dummy: Default::default(), fallback: WasmExecutor::new(), native_version: D::native_version(), - default_heap_pages, + initial_heap_pages: initial_heap_pages, } } } @@ -162,7 +96,7 @@ impl Clone for NativeExecutor { _dummy: Default::default(), fallback: self.fallback.clone(), native_version: D::native_version(), - default_heap_pages: self.default_heap_pages, + initial_heap_pages: self.initial_heap_pages, } } } @@ -176,10 +110,11 @@ impl RuntimeInfo for NativeExecutor { &self, ext: &mut E, ) -> Option { - RUNTIMES_CACHE.with(|c| - fetch_cached_runtime_version(&self.fallback, &mut c.borrow_mut(), ext, self.default_heap_pages) + RUNTIMES_CACHE.with(|cache| { + let cache = &mut cache.borrow_mut(); + cache.fetch_runtime(&self.fallback, ext, self.initial_heap_pages) .ok()?.1.clone() - ) + }) } } @@ -198,13 +133,14 @@ impl CodeExecutor for NativeExecutor, - ) -> (Result>, bool) { - RUNTIMES_CACHE.with(|c| { - let mut c = c.borrow_mut(); - let (module, onchain_version) = match fetch_cached_runtime_version( - &self.fallback, &mut c, ext, self.default_heap_pages) { - Ok((module, onchain_version)) => (module, onchain_version), - Err(e) => return (Err(e), false), + ) -> (Result>, bool){ + RUNTIMES_CACHE.with(|cache| { + let cache = &mut cache.borrow_mut(); + let (module, onchain_version) = match cache.fetch_runtime( + &self.fallback, ext, self.initial_heap_pages, + ) { + Ok((module, onchain_version)) => (module, onchain_version), + Err(e) => return (Err(e), false), }; match ( use_native, @@ -224,7 +160,7 @@ impl CodeExecutor for NativeExecutor CodeExecutor for NativeExecutor { ( self.fallback - .call_in_wasm_module(ext, module, method, data) + .call_in_wasm_module(ext, &module, method, data) .map(NativeOrEncoded::Encoded), false ) diff --git a/core/executor/src/wasm_executor.rs b/core/executor/src/wasm_executor.rs index cbb47195de87b..b98dd885d454f 100644 --- a/core/executor/src/wasm_executor.rs +++ b/core/executor/src/wasm_executor.rs @@ -1200,7 +1200,7 @@ impl WasmExecutor { data: &[u8], ) -> Result> { let module = ::wasmi::Module::from_buffer(code)?; - let module = self.prepare_module(ext, heap_pages, &module)?; + let module = self.instantiate_module(ext, heap_pages, &module)?; self.call_in_wasm_module(ext, &module, method, data) } @@ -1222,7 +1222,7 @@ impl WasmExecutor { filter_result: FR, ) -> Result { let module = wasmi::Module::from_buffer(code)?; - let module = self.prepare_module(ext, heap_pages, &module)?; + let module = self.instantiate_module(ext, heap_pages, &module)?; self.call_in_wasm_module_with_custom_signature( ext, &module, @@ -1290,8 +1290,6 @@ impl WasmExecutor { .export_by_name("__indirect_function_table") .and_then(|e| e.as_table().cloned()); - let low = memory.lowest_used(); - let used_mem = memory.used_size(); let mut fec = FunctionExecutor::new(memory.clone(), table, ext)?; let parameters = create_parameters(&mut |data: &[u8]| { let offset = fec.heap.allocate(data.len() as u32)?; @@ -1315,18 +1313,11 @@ impl WasmExecutor { }, }; - // cleanup module instance for next use - let new_low = memory.lowest_used(); - if new_low < low { - memory.zero(new_low as usize, (low - new_low) as usize)?; - memory.reset_lowest_used(low); - } - memory.with_direct_access_mut(|buf| buf.resize(used_mem.0, 0)); result } /// Prepare module instance - pub fn prepare_module>( + pub fn instantiate_module>( &self, ext: &mut E, heap_pages: usize, diff --git a/core/executor/src/wasm_runtimes_cache.rs b/core/executor/src/wasm_runtimes_cache.rs new file mode 100644 index 0000000000000..03b1e2fa87f87 --- /dev/null +++ b/core/executor/src/wasm_runtimes_cache.rs @@ -0,0 +1,265 @@ +// Copyright 2017-2019 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see . + +//! Implements a cache for pre-created Wasm runtime module instances. + +use crate::error::{Error, Result}; +use crate::wasm_executor::WasmExecutor; +use log::trace; +use parity_codec::Decode; +use primitives::storage::well_known_keys; +use primitives::Blake2Hasher; +use runtime_version::RuntimeVersion; +use state_machine::Externalities; +use std::{collections::HashMap, ops::Deref}; +use wasmi::{Module as WasmModule, ModuleRef as WasmModuleInstanceRef, RuntimeValue}; + +/// A runtime along with its version and initial state. +#[derive(Clone)] +enum RuntimePreproc { + InvalidCode, + ValidCode { + /// A wasm module instance. + instance: WasmModuleInstanceRef, + /// Runtime version according to `Core_version`. + /// + /// Can be `None` if the runtime doesn't expose this function. + version: Option, + /// The snapshot of the instance's state taken just after the instantiation. + state_snapshot: StateSnapshot, + }, +} + +/// A state snapshot of an instance taken just after instantiation. +/// +/// It is used for restoring the state of the module after execution. +#[derive(Clone)] +struct StateSnapshot { + memory_contents: Vec, + /// The list of all global variables of the module in their sequential order. + global_mut_values: Vec, +} + +impl StateSnapshot { + fn take(module: &WasmModuleInstanceRef) -> Option { + // TODO: deref? + let module_instance = module.deref(); + + // TODO: Write in trace if the `memory` export is not found. + let mem = module_instance.export_by_name("memory")?; + let memory_contents = match mem { + wasmi::ExternVal::Memory(memory_ref) => { + // The returned used size is a heuristic which returns one more + // than the highest memory address that had been written to. + // TODO: Analyze this + let used_size = memory_ref.used_size().0; + + memory_ref.get(0, used_size) + .expect("extracting data will always succeed since requested range is always valid; qed") + } + _ => { + trace!(target: "runtimes_cache", "the `memory` export is not of memory kind"); + return None; + } + }; + let global_mut_values = module_instance + .globals() + .iter() + .filter(|g| g.is_mutable()) + .map(|g| g.get()) + .collect(); + + Some(Self { + memory_contents, + global_mut_values, + }) + } + + /// Reset the runtime instance to the initial version by restoring + /// the preserved memory and globals. + fn apply(&self, module: &WasmModuleInstanceRef) { + // Restore the memory contents. + // TODO: deref? + let instance: &wasmi::ModuleInstance = module.deref(); + let mem = instance + .export_by_name("memory") + .expect("export identifier 'memory' is hardcoded and will always exist; qed"); + match mem { + wasmi::ExternVal::Memory(memory_ref) => { + let mem = memory_ref; + mem.set(0, &self.memory_contents) + .expect("only putting data back in which was already in; qed"); + } + _ => unreachable!("memory export always exists wasm module; qed"), + } + + // Restore the values of mutable globals. + for (global_ref, global_val) in instance + .globals() + .iter() + .filter(|g| g.is_mutable()) + .zip(self.global_mut_values.iter()) + { + // TODO: Can we guarantee that the instance is the same? + global_ref.set(*global_val).expect( + "the instance should be the same as used for preserving; + we iterate the same way it as we do it for preserving values; + the types should be the same; + all the values are mutable; + qed + ", + ); + } + } +} + +/// Default num of pages for the heap +const DEFAULT_HEAP_PAGES: u64 = 1024; + +/// When an instance is requested for the first time it is added to this +/// cache. Furthermore its initial memory is preserved here. Follow-up +/// requests to fetch a runtime return this one instance with the memory +/// reset to the initial memory. So, one runtime instance is reused for +/// every fetch request. +pub struct RuntimesCache { + // TODO: Consider LRU. + /// A cache of runtime instances along with metadata, ready to be reused. + /// + /// Instances are keyed by the hash of their code. + instances: HashMap<[u8; 32], RuntimePreproc>, +} + +impl RuntimesCache { + /// Creates a new instance of a runtimes cache. + pub fn new() -> RuntimesCache { + RuntimesCache { + instances: HashMap::new(), + } + } + + /// Fetches an instance of the runtime. + /// + /// On first use we create a new runtime instance, save it to the cache + /// and persist its initial memory. + /// + /// Each subsequent request will return this instance, with its memory restored + /// to the persisted initial memory. Thus, we reuse one single runtime instance + /// for every `fetch_runtime` invocation. + /// + /// # Parameters + /// + /// `wasm_executor`- Rust wasm executor. Executes the provided code in a + /// sandboxed Wasm runtime. + /// + /// `ext` - Externalities to use for the runtime. This is used for setting + /// up an initial runtime instance. The parameter is only needed for calling + /// into the Wasm module to find out the `Core_version`. + /// + /// `initial_heap_pages` - Number of 64KB pages to allocate for Wasm execution. + /// Defaults to `DEFAULT_HEAP_PAGES` if `None` is provided. + /// + /// `maybe_requested_version` - If `Some(RuntimeVersion)` is provided the + /// cached instance will be checked for compatibility. In case of incompatibility + /// the instance will be reset and a new one will be created synchronously. + /// + /// # Return value + /// + /// If no error occurred a tuple `(wasmi::ModuleRef, Option)` is + /// returned. `RuntimeVersion` is contained if the call to `Core_version` returned + /// a version. + /// + /// In case of failure one of two errors can be returned: + /// + /// `Err::InvalidCode` is returned for runtime code issues. + /// + /// `Error::InvalidMemoryReference` is returned if no memory export with the + /// identifier `memory` can be found in the runtime. + pub fn fetch_runtime>( + &mut self, + wasm_executor: &WasmExecutor, + ext: &mut E, + initial_heap_pages: Option, + ) -> Result<(WasmModuleInstanceRef, Option)> { + let code_hash = match ext.original_storage_hash(well_known_keys::CODE) { + Some(code_hash) => code_hash, + None => return Err(Error::InvalidCode), + }; + + let runtime_preproc = self.instances.entry(code_hash.into()).or_insert_with(|| { + trace!(target: "runtimes_cache", "no instance found in cache, creating now."); + Self::create_wasm_instance(wasm_executor, ext, initial_heap_pages) + }); + + match *runtime_preproc { + RuntimePreproc::InvalidCode => Err(Error::InvalidCode), + RuntimePreproc::ValidCode { + ref instance, + ref version, + ref state_snapshot, + .. + } => { + state_snapshot.apply(&instance); + Ok((instance.clone(), version.clone())) + } + } + } + + fn create_wasm_instance>( + wasm_executor: &WasmExecutor, + ext: &mut E, + initial_heap_pages: Option, + ) -> RuntimePreproc { + let code = match ext.original_storage(well_known_keys::CODE) { + Some(code) => code, + None => return RuntimePreproc::InvalidCode, + }; + + let heap_pages = ext + .storage(well_known_keys::HEAP_PAGES) + .and_then(|pages| u64::decode(&mut &pages[..])) + .or(initial_heap_pages) + .unwrap_or(DEFAULT_HEAP_PAGES); + + match WasmModule::from_buffer(code) + .map_err(|_| Error::InvalidCode) + .and_then(|instance| { + wasm_executor.instantiate_module(ext, heap_pages as usize, &instance) + }) + { + Ok(instance) => { + // Take state snapshot before executing anything. + let state_snapshot = match StateSnapshot::take(&instance) { + Some(snapshot) => snapshot, + None => return RuntimePreproc::InvalidCode, + }; + + let version = wasm_executor + .call_in_wasm_module(ext, &instance, "Core_version", &[]) + .ok() + .and_then(|v| RuntimeVersion::decode(&mut v.as_slice())); + RuntimePreproc::ValidCode { + instance, + version, + state_snapshot, + } + } + Err(e) => { + trace!(target: "runtimes_cache", "Invalid code presented to executor ({:?})", e); + RuntimePreproc::InvalidCode + } + } + } +} diff --git a/core/sr-api-macros/tests/runtime_calls.rs b/core/sr-api-macros/tests/runtime_calls.rs index 6fa155437b833..c370a180ca66c 100644 --- a/core/sr-api-macros/tests/runtime_calls.rs +++ b/core/sr-api-macros/tests/runtime_calls.rs @@ -16,6 +16,7 @@ use test_client::{ prelude::*, + DefaultTestClientBuilderExt, TestClientBuilder, runtime::{TestAPI, DecodeFails, Transfer, Header}, }; use runtime_primitives::{ @@ -191,3 +192,38 @@ fn record_proof_works() { &block.encode(), ).expect("Executes block while using the proof backend"); } + +#[test] +fn returns_mutable_static() { + let client = TestClientBuilder::new().set_execution_strategy(ExecutionStrategy::AlwaysWasm).build(); + let runtime_api = client.runtime_api(); + let block_id = BlockId::Number(client.info().chain.best_number); + + let ret = runtime_api.returns_mutable_static(&block_id).unwrap(); + assert_eq!(ret, 33); + + // We expect that every invocation will need to return the initial + // value plus one. If the value increases more than that then it is + // a sign that the wasm runtime preserves the memory content. + let ret = runtime_api.returns_mutable_static(&block_id).unwrap(); + assert_eq!(ret, 33); +} + +// If we didn't restore the wasm instance properly, on a trap the stack pointer would not be +// returned to its initial value and thus the stack space is going to be leaked. +// +// See https://github.com/paritytech/substrate/issues/2967 for details +#[test] +fn restoration_of_globals() { + let client = TestClientBuilder::new().set_execution_strategy(ExecutionStrategy::AlwaysWasm).build(); + let runtime_api = client.runtime_api(); + let block_id = BlockId::Number(client.info().chain.best_number); + + // On the first invocation we allocate approx. 75% of stack and then trap. + let ret = runtime_api.allocates_stack_alot(&block_id, true); + assert!(ret.is_err()); + + // On the second invocation we allocate yet another 75% of stack + let ret = runtime_api.allocates_stack_alot(&block_id, false); + assert!(ret.is_ok()); +} diff --git a/core/test-runtime/src/lib.rs b/core/test-runtime/src/lib.rs index 0916cad93d02f..bdb2253c4cb29 100644 --- a/core/test-runtime/src/lib.rs +++ b/core/test-runtime/src/lib.rs @@ -248,6 +248,8 @@ cfg_if! { fn use_trie() -> u64; fn benchmark_indirect_call() -> u64; fn benchmark_direct_call() -> u64; + fn returns_mutable_static() -> u64; + fn allocates_stack_alot(trap: bool); /// Returns the initialized block number. fn get_block_number() -> u64; /// Takes and returns the initialized block number. @@ -279,6 +281,8 @@ cfg_if! { fn use_trie() -> u64; fn benchmark_indirect_call() -> u64; fn benchmark_direct_call() -> u64; + fn returns_mutable_static() -> u64; + fn allocates_stack_alot(trap: bool); /// Returns the initialized block number. fn get_block_number() -> u64; /// Takes and returns the initialized block number. @@ -341,6 +345,11 @@ fn code_using_trie() -> u64 { iter_pairs.len() as u64 } +#[cfg(not(feature = "std"))] +/// Mutable static variables should be always observed to have +/// the initialized value at the start of a runtime call. +static mut MUTABLE_STATIC: u64 = 32; + cfg_if! { if #[cfg(feature = "std")] { impl_runtime_apis! { @@ -446,6 +455,14 @@ cfg_if! { (0..1000).fold(0, |p, i| p + benchmark_add_one(i)) } + fn returns_mutable_static() -> u64 { + unimplemented!("is not expected to be invoked from non-wasm builds"); + } + + fn allocates_stack_alot(_trap: bool) { + unimplemented!("is not expected to be invoked from non-wasm builds"); + } + fn get_block_number() -> u64 { system::get_block_number().expect("Block number is initialized") } @@ -591,6 +608,39 @@ cfg_if! { (0..10000).fold(0, |p, i| p + benchmark_add_one(i)) } + fn returns_mutable_static() -> u64 { + unsafe { + MUTABLE_STATIC += 1; + MUTABLE_STATIC + } + } + + fn allocates_stack_alot(trap: bool) { + // Allocate a stack frame that is approx. 75% of the stack (assuming it is 1MB). + // This will just decrease (stacks in wasm32-u-u grow downwards) the stack + // pointer. This won't trap on the current compilers. + let mut data = [0u8; 1024 * 768]; + + // Then make sure we actually write something to it. + // + // If: + // 1. the stack area is placed at the beginning of the linear memory space, and + // 2. the stack pointer points to out-of-bounds area, and + // 3. a write is performed around the current stack pointer. + // + // then a trap should happen. + // + for (i, v) in data.iter_mut().enumerate() { + *v = i as u8; // deliberate truncation + } + + if trap { + // There is a small chance of this to be pulled up in theory. In practice + // the probability of that is rather low. + panic!() + } + } + fn get_block_number() -> u64 { system::get_block_number().expect("Block number is initialized") } diff --git a/core/test-runtime/wasm/build.sh b/core/test-runtime/wasm/build.sh index 059e475c71e78..4a104b09f73ed 100755 --- a/core/test-runtime/wasm/build.sh +++ b/core/test-runtime/wasm/build.sh @@ -6,7 +6,11 @@ if cargo --version | grep -q "nightly"; then else CARGO_CMD="cargo +nightly" fi -CARGO_INCREMENTAL=0 RUSTFLAGS="-C link-arg=--export-table" $CARGO_CMD build --target=wasm32-unknown-unknown --release "$@" + +# Note that we set the stack-size to 1MB explicitly even though it is set +# to this value by default. This is because some of our tests (`restoration_of_globals`) +# depend on the stack-size. +CARGO_INCREMENTAL=0 RUSTFLAGS="-C link-arg=--export-table -C link-arg=-zstack-size=1048576" $CARGO_CMD build --target=wasm32-unknown-unknown --release "$@" for i in substrate_test_runtime do wasm-gc "target/wasm32-unknown-unknown/release/$i.wasm" "target/wasm32-unknown-unknown/release/$i.compact.wasm" diff --git a/node/executor/src/lib.rs b/node/executor/src/lib.rs index d266265b1e82b..77ef73564d280 100644 --- a/node/executor/src/lib.rs +++ b/node/executor/src/lib.rs @@ -22,6 +22,7 @@ #[cfg(feature = "benchmarks")] extern crate test; pub use substrate_executor::NativeExecutor; +pub use substrate_executor::RuntimesCache; use substrate_executor::native_executor_instance; // Declare an instance of the native executor named `Executor`. Include the wasm binary as the