diff --git a/Cargo.lock b/Cargo.lock index 01a67a8031d0..9101890e4ce3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8709,6 +8709,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde_json", + "sp-allocator-v1", "sp-api", "sp-block-builder", "sp-consensus-aura", @@ -17876,6 +17877,16 @@ dependencies = [ "sha-1 0.9.8", ] +[[package]] +name = "sp-allocator-v1" +version = "7.0.0" +dependencies = [ + "dlmalloc", + "sp-core", + "sp-io", + "sp-std 8.0.0", +] + [[package]] name = "sp-api" version = "4.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 55958b0d83ee..edc2214cfa82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -412,6 +412,7 @@ members = [ "substrate/frame/utility", "substrate/frame/vesting", "substrate/frame/whitelist", + "substrate/primitives/allocator-v1", "substrate/primitives/api", "substrate/primitives/api/proc-macro", "substrate/primitives/api/test", diff --git a/substrate/bin/node-template/runtime/Cargo.toml b/substrate/bin/node-template/runtime/Cargo.toml index 14a64948c0bc..49c1bf053ae9 100644 --- a/substrate/bin/node-template/runtime/Cargo.toml +++ b/substrate/bin/node-template/runtime/Cargo.toml @@ -29,6 +29,7 @@ frame-try-runtime = { path = "../../../frame/try-runtime", default-features = fa pallet-timestamp = { path = "../../../frame/timestamp", default-features = false } pallet-transaction-payment = { path = "../../../frame/transaction-payment", default-features = false } frame-executive = { path = "../../../frame/executive", default-features = false } +sp-allocator-v1 = { default-features = false, path = "../../../primitives/allocator-v1" } sp-api = { path = "../../../primitives/api", default-features = false } sp-block-builder = { path = "../../../primitives/block-builder", default-features = false } sp-consensus-aura = { path = "../../../primitives/consensus/aura", default-features = false, features = ["serde"] } @@ -60,7 +61,10 @@ pallet-template = { path = "../pallets/template", default-features = false } substrate-wasm-builder = { path = "../../../utils/wasm-builder", optional = true } [features] -default = ["std"] +default = [ "std" ] +allocator-v1 = [ + "sp-allocator-v1/allocator-v1", +] std = [ "codec/std", "frame-benchmarking?/std", @@ -79,6 +83,7 @@ std = [ "pallet-transaction-payment-rpc-runtime-api/std", "pallet-transaction-payment/std", "scale-info/std", + "sp-allocator-v1/std", "serde_json/std", "sp-api/std", "sp-block-builder/std", diff --git a/substrate/bin/node-template/runtime/build.rs b/substrate/bin/node-template/runtime/build.rs index c03d618535be..3f1d9bfc455d 100644 --- a/substrate/bin/node-template/runtime/build.rs +++ b/substrate/bin/node-template/runtime/build.rs @@ -5,6 +5,7 @@ fn main() { .with_current_project() .export_heap_base() .import_memory() + .enable_feature("allocator-v1") .build(); } } diff --git a/substrate/bin/node-template/runtime/src/lib.rs b/substrate/bin/node-template/runtime/src/lib.rs index 5f399edda987..6bf91dbcc72c 100644 --- a/substrate/bin/node-template/runtime/src/lib.rs +++ b/substrate/bin/node-template/runtime/src/lib.rs @@ -6,6 +6,8 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +use sp_allocator_v1 as _; + use pallet_grandpa::AuthorityId as GrandpaId; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; diff --git a/substrate/client/executor/common/src/runtime_blob/runtime_blob.rs b/substrate/client/executor/common/src/runtime_blob/runtime_blob.rs index becf9e219b0b..d6b7c4a39742 100644 --- a/substrate/client/executor/common/src/runtime_blob/runtime_blob.rs +++ b/substrate/client/executor/common/src/runtime_blob/runtime_blob.rs @@ -52,6 +52,14 @@ impl RuntimeBlob { Ok(Self { raw_module }) } + /// Return true if wasm contains a export function. + pub fn contain_export_func(&self, func: &str) -> bool { + self.raw_module + .export_section() + .map(|section| section.entries().iter().any(|entry| entry.field() == func)) + .unwrap_or(false) + } + /// The number of globals defined in locally in this module. pub fn declared_globals_count(&self) -> u32 { self.raw_module @@ -160,6 +168,8 @@ impl RuntimeBlob { &mut self, heap_alloc_strategy: HeapAllocStrategy, ) -> Result<(), WasmError> { + let is_allocator_v1 = self.contain_export_func("v1"); + let memory_section = self .raw_module .memory_section_mut() @@ -168,6 +178,7 @@ impl RuntimeBlob { if memory_section.entries().is_empty() { return Err(WasmError::Other("memory section is empty".into())) } + for memory_ty in memory_section.entries_mut() { let initial = memory_ty.limits().initial(); let (min, max) = match heap_alloc_strategy { @@ -177,7 +188,14 @@ impl RuntimeBlob { }, HeapAllocStrategy::Static { extra_pages } => { let pages = initial.saturating_add(extra_pages); - (pages, Some(pages)) + // The runtime-customized allocator may rely on this init memory page, + // so this value cannot be modified at will, otherwise it will cause errors in + // the allocator logic. + if is_allocator_v1 { + (initial, Some(pages)) + } else { + (pages, Some(pages)) + } }, }; *memory_ty = MemoryType::new(min, max); diff --git a/substrate/client/executor/wasmtime/src/host.rs b/substrate/client/executor/wasmtime/src/host.rs index f8c78cbb660e..2379fb7b6cf4 100644 --- a/substrate/client/executor/wasmtime/src/host.rs +++ b/substrate/client/executor/wasmtime/src/host.rs @@ -19,7 +19,7 @@ //! This module defines `HostState` and `HostContext` structs which provide logic and state //! required for execution of host. -use wasmtime::Caller; +use wasmtime::{AsContextMut, Caller, Val}; use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator}; use sp_wasm_interface::{Pointer, WordSize}; @@ -41,8 +41,8 @@ pub struct HostState { impl HostState { /// Constructs a new `HostState`. - pub fn new(allocator: FreeingBumpHeapAllocator) -> Self { - HostState { allocator: Some(allocator), panic_message: None } + pub fn new(allocator: Option) -> Self { + HostState { allocator, panic_message: None } } /// Takes the error message out of the host state, leaving a `None` in its place. @@ -88,38 +88,56 @@ impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> { fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result> { let memory = self.caller.data().memory(); - let mut allocator = self - .host_state_mut() - .allocator - .take() - .expect("allocator is not empty when calling a function in wasm; qed"); - - // We can not return on error early, as we need to store back allocator. - let res = allocator - .allocate(&mut MemoryWrapper(&memory, &mut self.caller), size) - .map_err(|e| e.to_string()); - - self.host_state_mut().allocator = Some(allocator); - - res + if let Some(alloc) = self.caller.data().alloc { + let params = [Val::I32(size as i32)]; + let mut results = [Val::I32(0)]; + + alloc + .call(self.caller.as_context_mut(), ¶ms, &mut results) + .expect("alloc must success; qed"); + let data_ptr = results[0].i32().unwrap(); + let data_ptr = Pointer::new(data_ptr as u32); + + Ok(data_ptr) + } else { + let mut allocator = self + .host_state_mut() + .allocator + .take() + .expect("allocator is not empty when calling a function in wasm; qed"); + + // We can not return on error early, as we need to store back allocator. + let res = allocator + .allocate(&mut MemoryWrapper(&memory, &mut self.caller), size) + .map_err(|e| e.to_string()); + + self.host_state_mut().allocator = Some(allocator); + + res + } } fn deallocate_memory(&mut self, ptr: Pointer) -> sp_wasm_interface::Result<()> { let memory = self.caller.data().memory(); - let mut allocator = self - .host_state_mut() - .allocator - .take() - .expect("allocator is not empty when calling a function in wasm; qed"); - - // We can not return on error early, as we need to store back allocator. - let res = allocator - .deallocate(&mut MemoryWrapper(&memory, &mut self.caller), ptr) - .map_err(|e| e.to_string()); - - self.host_state_mut().allocator = Some(allocator); - - res + if let Some(_alloc) = self.caller.data().alloc { + // TODO: maybe we need to support it. + unreachable!("The `deallocate_memory` never be used in allocator v1"); + } else { + let mut allocator = self + .host_state_mut() + .allocator + .take() + .expect("allocator is not empty when calling a function in wasm; qed"); + + // We can not return on error early, as we need to store back allocator. + let res = allocator + .deallocate(&mut MemoryWrapper(&memory, &mut self.caller), ptr) + .map_err(|e| e.to_string()); + + self.host_state_mut().allocator = Some(allocator); + + res + } } fn register_panic_error_message(&mut self, message: &str) { diff --git a/substrate/client/executor/wasmtime/src/instance_wrapper.rs b/substrate/client/executor/wasmtime/src/instance_wrapper.rs index 8852532adbca..5ba1c2a14110 100644 --- a/substrate/client/executor/wasmtime/src/instance_wrapper.rs +++ b/substrate/client/executor/wasmtime/src/instance_wrapper.rs @@ -28,7 +28,7 @@ use sc_executor_common::{ }; use sp_wasm_interface::{Pointer, Value, WordSize}; use wasmtime::{ - AsContext, AsContextMut, Engine, Extern, Instance, InstancePre, Memory, Table, Val, + AsContext, AsContextMut, Engine, Extern, Func, Instance, InstancePre, Memory, Table, Val, }; /// Invoked entrypoint format. @@ -179,9 +179,11 @@ impl InstanceWrapper { let memory = get_linear_memory(&instance, &mut store)?; let table = get_table(&instance, &mut store); + let alloc = get_export_alloc(&instance, &mut store); store.data_mut().memory = Some(memory); store.data_mut().table = table; + store.data_mut().alloc = alloc; Ok(InstanceWrapper { instance, store, _release_instance_handle }) } @@ -310,6 +312,11 @@ fn get_table(instance: &Instance, ctx: &mut Store) -> Option { .and_then(Extern::into_table) } +/// Extract export `alloc` func from the given instance. +fn get_export_alloc(instance: &Instance, ctx: impl AsContextMut) -> Option { + instance.get_export(ctx, "alloc").and_then(Extern::into_func) +} + /// Functions related to memory. impl InstanceWrapper { pub(crate) fn store(&self) -> &Store { diff --git a/substrate/client/executor/wasmtime/src/runtime.rs b/substrate/client/executor/wasmtime/src/runtime.rs index ac88663f4e79..b6ec674291be 100644 --- a/substrate/client/executor/wasmtime/src/runtime.rs +++ b/substrate/client/executor/wasmtime/src/runtime.rs @@ -41,7 +41,7 @@ use std::{ Arc, }, }; -use wasmtime::{AsContext, Engine, Memory, Table}; +use wasmtime::{AsContext, Engine, Func, Memory, Table, Val}; const MAX_INSTANCE_COUNT: u32 = 64; @@ -53,6 +53,8 @@ pub(crate) struct StoreData { pub(crate) memory: Option, /// This will be set only if the runtime actually contains a table. pub(crate) table: Option
, + /// This will be set only if the runtime actually contains a table. + pub(crate) alloc: Option, } impl StoreData { @@ -171,11 +173,21 @@ impl WasmtimeInstance { match &mut self.strategy { Strategy::RecreateInstance(ref mut instance_creator) => { let mut instance_wrapper = instance_creator.instantiate()?; - let heap_base = instance_wrapper.extract_heap_base()?; let entrypoint = instance_wrapper.resolve_entrypoint(method)?; - let allocator = FreeingBumpHeapAllocator::new(heap_base); - perform_call(data, &mut instance_wrapper, entrypoint, allocator, allocation_stats) + if let Some(alloc) = instance_wrapper.store().data().alloc { + perform_call_v1(data, &mut instance_wrapper, entrypoint, alloc) + } else { + let heap_base = instance_wrapper.extract_heap_base()?; + let allocator = FreeingBumpHeapAllocator::new(heap_base); + perform_call( + data, + &mut instance_wrapper, + entrypoint, + allocator, + allocation_stats, + ) + } }, } } @@ -694,7 +706,7 @@ fn perform_call( ) -> Result> { let (data_ptr, data_len) = inject_input_data(instance_wrapper, &mut allocator, data)?; - let host_state = HostState::new(allocator); + let host_state = HostState::new(Some(allocator)); // Set the host state before calling into wasm. instance_wrapper.store_mut().data_mut().host_state = Some(host_state); @@ -715,6 +727,29 @@ fn perform_call( Ok(output) } +fn perform_call_v1( + data: &[u8], + instance_wrapper: &mut InstanceWrapper, + entrypoint: EntryPoint, + alloc: Func, +) -> Result> { + let (data_ptr, data_len) = inject_input_data_v1(instance_wrapper, alloc, data)?; + + let host_state = HostState::new(None); + + // Set the host state before calling into wasm. + instance_wrapper.store_mut().data_mut().host_state = Some(host_state); + + let ret = entrypoint + .call(instance_wrapper.store_mut(), data_ptr, data_len) + .map(unpack_ptr_and_len); + + let (output_ptr, output_len) = ret?; + let output = extract_output_data(instance_wrapper, output_ptr, output_len)?; + + Ok(output) +} + fn inject_input_data( instance: &mut InstanceWrapper, allocator: &mut FreeingBumpHeapAllocator, @@ -728,6 +763,25 @@ fn inject_input_data( Ok((data_ptr, data_len)) } +fn inject_input_data_v1( + instance: &mut InstanceWrapper, + alloc: Func, + data: &[u8], +) -> Result<(Pointer, WordSize)> { + let data_len = data.len() as WordSize; + let params = [Val::I32(data_len as _)]; + let mut results = [Val::I32(0)]; + + alloc + .call(instance.store_mut(), ¶ms, &mut results) + .expect("alloc must success; qed"); + let data_ptr = results[0].i32().unwrap(); + let data_ptr = Pointer::new(data_ptr as u32); + util::write_memory_from(instance.store_mut(), data_ptr, data)?; + + Ok((data_ptr, data_len)) +} + fn extract_output_data( instance: &InstanceWrapper, output_ptr: u32, diff --git a/substrate/primitives/allocator-v1/Cargo.toml b/substrate/primitives/allocator-v1/Cargo.toml new file mode 100644 index 000000000000..a1122b0d76cc --- /dev/null +++ b/substrate/primitives/allocator-v1/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "sp-allocator-v1" +version = "7.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "I/O for Substrate runtimes" +documentation = "https://docs.rs/sp-allocator-v1" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +dlmalloc = { version = "0.2.4", optional = true, features = ["global"] } + +sp-io = { path = "../io", default-features = false } +sp-std = { path = "../std", default-features = false } +sp-core = { path = "../core", default-features = false } + +[features] +default = ["std"] +std = [ + "sp-io/std", + "sp-std/std", + "sp-core/std", +] + +improved_panic_error_reporting = [] + +allocator-v1 = [ + "dep:dlmalloc", + "sp-io/disable_allocator", + "sp-io/disable_panic_handler", + "sp-io/disable_oom", +] diff --git a/substrate/primitives/allocator-v1/README.md b/substrate/primitives/allocator-v1/README.md new file mode 100644 index 000000000000..2e55f49069f4 --- /dev/null +++ b/substrate/primitives/allocator-v1/README.md @@ -0,0 +1,3 @@ +Spec about runtime allocator + +License: Apache-2.0 diff --git a/substrate/primitives/allocator-v1/src/config.rs b/substrate/primitives/allocator-v1/src/config.rs new file mode 100644 index 000000000000..ec028d647981 --- /dev/null +++ b/substrate/primitives/allocator-v1/src/config.rs @@ -0,0 +1,80 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use core::alloc::GlobalAlloc; +use dlmalloc::GlobalDlmalloc; + +#[global_allocator] +pub static ALLOCATOR: GlobalDlmalloc = GlobalDlmalloc; + +#[no_mangle] +unsafe fn alloc(size: usize) -> *mut u8 { + ALLOCATOR.alloc(core::alloc::Layout::array::(size).unwrap()) +} + +#[no_mangle] +unsafe fn dealloc(ptr: *mut u8, size: usize) { + ALLOCATOR.dealloc(ptr, core::alloc::Layout::array::(size).unwrap()) +} + +#[no_mangle] +unsafe fn realloc(ptr: *mut u8, size: usize, new_size: usize) -> *mut u8 { + ALLOCATOR.realloc(ptr, core::alloc::Layout::array::(size).unwrap(), new_size) +} + +// TODO: maybe it's better to rename this crate to `sp-runtime-abi`. +/// The dummy function represents the version of runtime ABI. +#[no_mangle] +fn v1() { + // nop +} + +/// A default panic handler for WASM environment. +#[cfg(all(not(feature = "disable_panic_handler"), not(feature = "std")))] +#[panic_handler] +#[no_mangle] +pub fn panic(info: &core::panic::PanicInfo) -> ! { + let message = sp_std::alloc::format!("{}", info); + #[cfg(feature = "improved_panic_error_reporting")] + { + sp_io::panic_handler::abort_on_panic(&message); + } + #[cfg(not(feature = "improved_panic_error_reporting"))] + { + sp_io::logging::log(sp_core::LogLevel::Error, "runtime", message.as_bytes()); + core::arch::wasm32::unreachable(); + } +} + +/// A default OOM handler for WASM environment. +#[cfg(all(not(feature = "disable_oom"), enable_alloc_error_handler))] +#[alloc_error_handler] +pub fn oom(_layout: core::alloc::Layout) -> ! { + #[cfg(feature = "improved_panic_error_reporting")] + { + panic_handler::abort_on_panic("Runtime memory exhausted."); + } + #[cfg(not(feature = "improved_panic_error_reporting"))] + { + sp_io::logging::log( + sp_core::LogLevel::Error, + "runtime", + b"Runtime memory exhausted. Aborting", + ); + core::arch::wasm32::unreachable(); + } +} diff --git a/substrate/primitives/allocator-v1/src/lib.rs b/substrate/primitives/allocator-v1/src/lib.rs new file mode 100644 index 000000000000..c835a30d40f1 --- /dev/null +++ b/substrate/primitives/allocator-v1/src/lib.rs @@ -0,0 +1,30 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! # Spec about runtime allocator +//! +//! Export the `alloc`/`dealloc`/`realloc` primitive functions inside the runtime to host. +//! +//! When enable this crate, the following features will be set: +//! - disable_allocator +//! - disable_panic_handler +//! - disable_oom +#![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(enable_alloc_error_handler, feature(alloc_error_handler))] + +#[cfg(all(feature = "allocator-v1", not(feature = "std")))] +pub mod config;