Integrate Wasmer into Substrate sandbox environment (#5920)

* Add comments and refactor Sandbox module

* Adds some comments

* Add wasmtime instance to the sandbox and delegate calls

* Adds module imports stub

* WIP state holder via *mut

* My take at the problem

* Brings back invoke and instantiate implementation details

* Removes redundant bound

* Code cleanup

* Fixes invoke closure

* Refactors FunctionExecutor to eliminate lifetime

* Wraps `FunctionExecutor::sandbox_store` in `RefCell`

* Renames `FunctionExecutor::heap` to `allocator`

* Wraps `FunctionExecutor::allocator` in `RefCell`

* Refactors FunctionExecutor to `Rc<Inner>` pattern

* Implements scoped TLS for FunctionExecutor

* Fixes wasmi instancing

* Fixes sandbox asserts

* Makes sandbox compile after wasmtime API change

* Uses Vurich/wasmtime for the Lightbeam backend

* Uses wasmtime instead of wasmi for sandbox API results

* Refactors sandbox to use one  of the execution backends at a time

* Fixes wasmtime module instantiation

* TEMP vurich branch stuff

* Adds wasmer impl stub

* Adds get global

* Fixes warnings

* Adds wasmer invoke impl

* Implements host function interface for wasmer

* Fixes wasmer instantiation result

* Adds workaround to remove debug_assert

* Fixes import object generation for wasmer

* Attempt to propagate wasmer::Store through sandbox::Store

* Wraps `sandbox::Store::memories` in `RefCell`

* Moves `sandbox::instantiate` to `sandbox::Store`

* Eliminate `RefCell<memories>`

* Implements `HostState::memory_get/set`, removes accidental `borrow_mut`

* Fixes sandbox memory handling for wasmi

* Fix memory allocation

* Resets Cargo.lock to match master

* Fixes compilation

* Refactors sandbox to use TLS for dispatch_thunk propagation to wasmer

* Pass dispatch thunk to the sandbox as a TLS

* Initialize dispatch thunk holder in `SandboxInstance`

* Comment out Wasmtime/Lightbeam sandbox backend

* Revert wasmtime back to mainstream

* Adds SandboxExecutionMethod enum for cli param

* Cleanup sandbox code

* Allow wasmi to access wasmer memory regions

* More cleanup

* Remove debug logging, replace asserts with runtime errors

* Revert "Adds SandboxExecutionMethod enum for cli param"

This reverts commit dcb2b1d3b54145ab51ad2e3fef0d980ba215b596.

* Fixes warnings

* Fixes indentation and line width

* Fix return types condition

* Puts everything related under the `wasmer-sandbox` feature flag

* Fixes warnings

* Address grumbles

* Split instantiate per backend

* More splits

* Refacmemory allocation

* Nitpicks

* Attempt to wrap wasmer memory in protoco enforcing type

* Revert renaming

* WIP wasm buffer proxy API

* Reimplement util::wasmer::MemoryRef to use buffers instead of memory slices

* Adds WasmiMemoryWrapper and MemoryTransfer trait

* Refactor naming

* Perform all memory transfers using MemoryTransfer

* Adds allocating `read`

* Adds comments

* Removes unused imports

* Removes now unused function

* Pulls Cargo.lock from origin/master

* Fix rustdoc

* Removes unused `TransferError`

* Update Cargo.lock

* Removes unused import

* cargo fmt

* Fix feature dependency graph

* Feature should flow from the top level crate
	* We should not assume a specific workspace structure
* sc-executor-wasmi does not use the feature
* sc-executor-wasmtime should not know about the feature

* Fix doc typo

* Enable wasmer-sandbox by default (for now)

It will be removed before merge. It is so that the benchbot
uses the wasmer sandbox.

* cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_contracts --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/contracts/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Revert "cargo run --quiet --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_contracts --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/contracts/src/weights.rs --template=./.maintain/frame-weight-template.hbs"

This reverts commit d713590ba45387c4204b2ad97c8bd6f6ebabda4e.

* cargo fmt

* Add ci-check to prevent wasmer sandbox build breaking

* Run tests with wasmer-sandbox enabled

* Revert "Run tests with wasmer-sandbox enabled"

This reverts commit cff63156a162f9ffdab23e7cb94a30f44e320f8a.

Co-authored-by: Sergei Shulepov <s.pepyakin@gmail.com>
Co-authored-by: Andrew Jones <ascjones@gmail.com>
Co-authored-by: Alexander Theißen <alex.theissen@me.com>
Co-authored-by: Parity Benchmarking Bot <admin@parity.io>
This commit is contained in:
Dmitry Kashitsyn
2021-08-19 20:04:13 +07:00
committed by GitHub
parent ccfe485b91
commit 61606a0b3b
15 changed files with 1669 additions and 320 deletions
+130 -94
View File
@@ -19,13 +19,14 @@
//! This module defines `HostState` and `HostContext` structs which provide logic and state
//! required for execution of host.
use crate::{instance_wrapper::InstanceWrapper, util};
use crate::instance_wrapper::InstanceWrapper;
use codec::{Decode, Encode};
use log::trace;
use sc_allocator::FreeingBumpHeapAllocator;
use sc_executor_common::{
error::Result,
sandbox::{self, SandboxCapabilities, SupervisorFuncIndex},
sandbox::{self, SandboxCapabilities, SandboxCapabilitiesHolder, SupervisorFuncIndex},
util::MemoryTransfer,
};
use sp_core::sandbox as sandbox_primitives;
use sp_wasm_interface::{FunctionContext, MemoryId, Pointer, Sandbox, WordSize};
@@ -42,7 +43,12 @@ pub struct SupervisorFuncRef(Func);
/// 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.
#[derive(Clone)]
pub struct HostState {
inner: Rc<Inner>,
}
struct Inner {
// We need some interior mutability here since the host state is shared between all host
// function handlers and the wasmtime backend's `impl WasmRuntime`.
//
@@ -61,31 +67,18 @@ impl HostState {
/// Constructs a new `HostState`.
pub fn new(allocator: FreeingBumpHeapAllocator, instance: Rc<InstanceWrapper>) -> Self {
HostState {
sandbox_store: RefCell::new(sandbox::Store::new()),
allocator: RefCell::new(allocator),
instance,
inner: Rc::new(Inner {
sandbox_store: RefCell::new(sandbox::Store::new(
sandbox::SandboxBackend::TryWasmer,
)),
allocator: RefCell::new(allocator),
instance,
}),
}
}
/// Materialize `HostContext` that can be used to invoke a substrate host `dyn Function`.
pub fn materialize<'a>(&'a self) -> HostContext<'a> {
HostContext(self)
}
}
/// A `HostContext` implements `FunctionContext` for making host calls from a Wasmtime
/// runtime. The `HostContext` exists only for the lifetime of the call and borrows state from
/// a longer-living `HostState`.
pub struct HostContext<'a>(&'a HostState);
impl<'a> std::ops::Deref for HostContext<'a> {
type Target = HostState;
fn deref(&self) -> &HostState {
self.0
}
}
impl<'a> SandboxCapabilities for HostContext<'a> {
impl SandboxCapabilities for HostState {
type SupervisorFuncRef = SupervisorFuncRef;
fn invoke(
@@ -125,28 +118,30 @@ impl<'a> SandboxCapabilities for HostContext<'a> {
}
}
impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> {
impl sp_wasm_interface::FunctionContext for HostState {
fn read_memory_into(
&self,
address: Pointer<u8>,
dest: &mut [u8],
) -> sp_wasm_interface::Result<()> {
self.instance.read_memory_into(address, dest).map_err(|e| e.to_string())
self.inner.instance.read_memory_into(address, dest).map_err(|e| e.to_string())
}
fn write_memory(&mut self, address: Pointer<u8>, data: &[u8]) -> sp_wasm_interface::Result<()> {
self.instance.write_memory_from(address, data).map_err(|e| e.to_string())
self.inner.instance.write_memory_from(address, data).map_err(|e| e.to_string())
}
fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result<Pointer<u8>> {
self.instance
.allocate(&mut *self.allocator.borrow_mut(), size)
self.inner
.instance
.allocate(&mut *self.inner.allocator.borrow_mut(), size)
.map_err(|e| e.to_string())
}
fn deallocate_memory(&mut self, ptr: Pointer<u8>) -> sp_wasm_interface::Result<()> {
self.instance
.deallocate(&mut *self.allocator.borrow_mut(), ptr)
self.inner
.instance
.deallocate(&mut *self.inner.allocator.borrow_mut(), ptr)
.map_err(|e| e.to_string())
}
@@ -155,7 +150,7 @@ impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> {
}
}
impl<'a> Sandbox for HostContext<'a> {
impl Sandbox for HostState {
fn memory_get(
&mut self,
memory_id: MemoryId,
@@ -164,27 +159,20 @@ impl<'a> Sandbox for HostContext<'a> {
buf_len: WordSize,
) -> sp_wasm_interface::Result<u32> {
let sandboxed_memory =
self.sandbox_store.borrow().memory(memory_id).map_err(|e| e.to_string())?;
sandboxed_memory.with_direct_access(|sandboxed_memory| {
let len = buf_len as usize;
let src_range = match util::checked_range(offset as usize, len, sandboxed_memory.len())
{
Some(range) => range,
None => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
};
let supervisor_mem_size = self.instance.memory_size() as usize;
let dst_range = match util::checked_range(buf_ptr.into(), len, supervisor_mem_size) {
Some(range) => range,
None => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
};
self.instance
.write_memory_from(
Pointer::new(dst_range.start as u32),
&sandboxed_memory[src_range],
)
.expect("ranges are checked above; write can't fail; qed");
Ok(sandbox_primitives::ERR_OK)
})
self.inner.sandbox_store.borrow().memory(memory_id).map_err(|e| e.to_string())?;
let len = buf_len as usize;
let buffer = match sandboxed_memory.read(Pointer::new(offset as u32), len) {
Err(_) => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
Ok(buffer) => buffer,
};
if let Err(_) = self.inner.instance.write_memory_from(buf_ptr, &buffer) {
return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS)
}
Ok(sandbox_primitives::ERR_OK)
}
fn memory_set(
@@ -195,38 +183,33 @@ impl<'a> Sandbox for HostContext<'a> {
val_len: WordSize,
) -> sp_wasm_interface::Result<u32> {
let sandboxed_memory =
self.sandbox_store.borrow().memory(memory_id).map_err(|e| e.to_string())?;
sandboxed_memory.with_direct_access_mut(|sandboxed_memory| {
let len = val_len as usize;
let supervisor_mem_size = self.instance.memory_size() as usize;
let src_range = match util::checked_range(val_ptr.into(), len, supervisor_mem_size) {
Some(range) => range,
None => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
};
let dst_range = match util::checked_range(offset as usize, len, sandboxed_memory.len())
{
Some(range) => range,
None => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
};
self.instance
.read_memory_into(
Pointer::new(src_range.start as u32),
&mut sandboxed_memory[dst_range],
)
.expect("ranges are checked above; read can't fail; qed");
Ok(sandbox_primitives::ERR_OK)
})
self.inner.sandbox_store.borrow().memory(memory_id).map_err(|e| e.to_string())?;
let len = val_len as usize;
let buffer = match self.inner.instance.read_memory(val_ptr, len) {
Err(_) => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
Ok(buffer) => buffer,
};
if let Err(_) = sandboxed_memory.write_from(Pointer::new(offset as u32), &buffer) {
return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS)
}
Ok(sandbox_primitives::ERR_OK)
}
fn memory_teardown(&mut self, memory_id: MemoryId) -> sp_wasm_interface::Result<()> {
self.sandbox_store
self.inner
.sandbox_store
.borrow_mut()
.memory_teardown(memory_id)
.map_err(|e| e.to_string())
}
fn memory_new(&mut self, initial: u32, maximum: u32) -> sp_wasm_interface::Result<u32> {
self.sandbox_store
self.inner
.sandbox_store
.borrow_mut()
.new_memory(initial, maximum)
.map_err(|e| e.to_string())
@@ -250,9 +233,14 @@ impl<'a> Sandbox for HostContext<'a> {
.map(Into::into)
.collect::<Vec<_>>();
let instance =
self.sandbox_store.borrow().instance(instance_id).map_err(|e| e.to_string())?;
let result = instance.invoke(export_name, &args, self, state);
let instance = self
.inner
.sandbox_store
.borrow()
.instance(instance_id)
.map_err(|e| e.to_string())?;
let result = instance.invoke::<_, CapsHolder, ThunkHolder>(export_name, &args, state);
match result {
Ok(None) => Ok(sandbox_primitives::ERR_OK),
@@ -262,7 +250,7 @@ impl<'a> Sandbox for HostContext<'a> {
if val.len() > return_val_len as usize {
Err("Return value buffer is too small")?;
}
<HostContext as FunctionContext>::write_memory(self, return_val, val)
<HostState as FunctionContext>::write_memory(self, return_val, val)
.map_err(|_| "can't write return value")?;
Ok(sandbox_primitives::ERR_OK)
})
@@ -272,7 +260,8 @@ impl<'a> Sandbox for HostContext<'a> {
}
fn instance_teardown(&mut self, instance_id: u32) -> sp_wasm_interface::Result<()> {
self.sandbox_store
self.inner
.sandbox_store
.borrow_mut()
.instance_teardown(instance_id)
.map_err(|e| e.to_string())
@@ -288,6 +277,7 @@ impl<'a> Sandbox for HostContext<'a> {
// Extract a dispatch thunk from the instance's table by the specified index.
let dispatch_thunk = {
let table_item = self
.inner
.instance
.table()
.as_ref()
@@ -303,20 +293,26 @@ impl<'a> Sandbox for HostContext<'a> {
SupervisorFuncRef(func_ref)
};
let guest_env =
match sandbox::GuestEnvironment::decode(&*self.sandbox_store.borrow(), raw_env_def) {
Ok(guest_env) => guest_env,
Err(_) => return Ok(sandbox_primitives::ERR_MODULE as u32),
};
let guest_env = match sandbox::GuestEnvironment::decode(
&*self.inner.sandbox_store.borrow(),
raw_env_def,
) {
Ok(guest_env) => guest_env,
Err(_) => return Ok(sandbox_primitives::ERR_MODULE as u32),
};
let instance_idx_or_err_code =
match sandbox::instantiate(self, dispatch_thunk, wasm, guest_env, state)
.map(|i| i.register(&mut *self.sandbox_store.borrow_mut()))
{
Ok(instance_idx) => instance_idx,
Err(sandbox::InstantiationError::StartTrapped) => sandbox_primitives::ERR_EXECUTION,
Err(_) => sandbox_primitives::ERR_MODULE,
};
let store = &mut *self.inner.sandbox_store.borrow_mut();
let result = DISPATCH_THUNK.set(&dispatch_thunk, || {
store
.instantiate::<_, CapsHolder, ThunkHolder>(wasm, guest_env, state)
.map(|i| i.register(store))
});
let instance_idx_or_err_code = match result {
Ok(instance_idx) => instance_idx,
Err(sandbox::InstantiationError::StartTrapped) => sandbox_primitives::ERR_EXECUTION,
Err(_) => sandbox_primitives::ERR_MODULE,
};
Ok(instance_idx_or_err_code as u32)
}
@@ -326,10 +322,50 @@ impl<'a> Sandbox for HostContext<'a> {
instance_idx: u32,
name: &str,
) -> sp_wasm_interface::Result<Option<sp_wasm_interface::Value>> {
self.sandbox_store
self.inner
.sandbox_store
.borrow()
.instance(instance_idx)
.map(|i| i.get_global_val(name))
.map_err(|e| e.to_string())
}
}
/// Wasmtime specific implementation of `SandboxCapabilitiesHolder` that provides
/// sandbox with a scoped thread local access to a function executor.
/// This is a way to calm down the borrow checker since host function closures
/// require exclusive access to it.
struct CapsHolder;
impl SandboxCapabilitiesHolder for CapsHolder {
type SupervisorFuncRef = SupervisorFuncRef;
type SC = HostState;
fn with_sandbox_capabilities<R, F: FnOnce(&mut Self::SC) -> R>(f: F) -> R {
crate::state_holder::with_context(|ctx| f(&mut ctx.expect("wasmtime executor is not set")))
}
}
/// Wasmtime specific implementation of `DispatchThunkHolder` that provides
/// sandbox with a scoped thread local access to a dispatch thunk.
/// This is a way to calm down the borrow checker since host function closures
/// require exclusive access to it.
struct ThunkHolder;
scoped_tls::scoped_thread_local!(static DISPATCH_THUNK: SupervisorFuncRef);
impl sandbox::DispatchThunkHolder for ThunkHolder {
type DispatchThunk = SupervisorFuncRef;
fn with_dispatch_thunk<R, F: FnOnce(&mut Self::DispatchThunk) -> R>(f: F) -> R {
assert!(DISPATCH_THUNK.is_set(), "dispatch thunk is not set");
DISPATCH_THUNK.with(|thunk| f(&mut thunk.clone()))
}
fn initialize_thunk<R, F>(s: &Self::DispatchThunk, f: F) -> R
where
F: FnOnce() -> R,
{
DISPATCH_THUNK.set(s, f)
}
}