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
@@ -24,4 +24,5 @@
pub mod error;
pub mod runtime_blob;
pub mod sandbox;
pub mod util;
pub mod wasm_runtime;
+638 -83
View File
@@ -18,19 +18,25 @@
//! This module implements sandboxing support in the runtime.
//!
//! Sandboxing is baked by wasmi at the moment. In future, however, we would like to add/switch to
//! a compiled execution engine.
//! Sandboxing is backed by wasmi and wasmer, depending on the configuration.
use crate::error::{Error, Result};
use crate::{
error::{Error, Result},
util,
};
use codec::{Decode, Encode};
use sp_core::sandbox as sandbox_primitives;
use sp_wasm_interface::{FunctionContext, Pointer, WordSize};
use std::{collections::HashMap, rc::Rc};
use wasmi::{
memory_units::Pages, Externals, ImportResolver, MemoryInstance, MemoryRef, Module,
ModuleInstance, ModuleRef, RuntimeArgs, RuntimeValue, Trap, TrapKind,
memory_units::Pages, Externals, ImportResolver, MemoryInstance, Module, ModuleInstance,
RuntimeArgs, RuntimeValue, Trap, TrapKind,
};
#[cfg(feature = "wasmer-sandbox")]
use crate::util::wasmer::MemoryWrapper as WasmerMemoryWrapper;
use crate::util::wasmi::MemoryWrapper as WasmiMemoryWrapper;
/// Index of a function inside the supervisor.
///
/// This is a typically an index in the default table of the supervisor, however
@@ -46,34 +52,59 @@ impl From<SupervisorFuncIndex> for usize {
/// Index of a function within guest index space.
///
/// This index is supposed to be used with as index for `Externals`.
/// This index is supposed to be used as index for `Externals`.
#[derive(Copy, Clone, Debug, PartialEq)]
struct GuestFuncIndex(usize);
/// This struct holds a mapping from guest index space to supervisor.
struct GuestToSupervisorFunctionMapping {
/// Position of elements in this vector are interpreted
/// as indices of guest functions and are mapped to
/// corresponding supervisor function indices.
funcs: Vec<SupervisorFuncIndex>,
}
impl GuestToSupervisorFunctionMapping {
/// Create an empty function mapping
fn new() -> GuestToSupervisorFunctionMapping {
GuestToSupervisorFunctionMapping { funcs: Vec::new() }
}
/// Add a new supervisor function to the mapping.
/// Returns a newly assigned guest function index.
fn define(&mut self, supervisor_func: SupervisorFuncIndex) -> GuestFuncIndex {
let idx = self.funcs.len();
self.funcs.push(supervisor_func);
GuestFuncIndex(idx)
}
/// Find supervisor function index by its corresponding guest function index
fn func_by_guest_index(&self, guest_func_idx: GuestFuncIndex) -> Option<SupervisorFuncIndex> {
self.funcs.get(guest_func_idx.0).cloned()
}
}
/// Holds sandbox function and memory imports and performs name resolution
struct Imports {
/// Maps qualified function name to its guest function index
func_map: HashMap<(Vec<u8>, Vec<u8>), GuestFuncIndex>,
memories_map: HashMap<(Vec<u8>, Vec<u8>), MemoryRef>,
/// Maps qualified field name to its memory reference
memories_map: HashMap<(Vec<u8>, Vec<u8>), Memory>,
}
impl Imports {
fn func_by_name(&self, module_name: &str, func_name: &str) -> Option<GuestFuncIndex> {
self.func_map
.get(&(module_name.as_bytes().to_owned(), func_name.as_bytes().to_owned()))
.cloned()
}
fn memory_by_name(&self, module_name: &str, memory_name: &str) -> Option<Memory> {
self.memories_map
.get(&(module_name.as_bytes().to_owned(), memory_name.as_bytes().to_owned()))
.cloned()
}
}
impl ImportResolver for Imports {
@@ -83,10 +114,10 @@ impl ImportResolver for Imports {
field_name: &str,
signature: &::wasmi::Signature,
) -> std::result::Result<wasmi::FuncRef, wasmi::Error> {
let key = (module_name.as_bytes().to_owned(), field_name.as_bytes().to_owned());
let idx = *self.func_map.get(&key).ok_or_else(|| {
let idx = self.func_by_name(module_name, field_name).ok_or_else(|| {
wasmi::Error::Instantiation(format!("Export {}:{} not found", module_name, field_name))
})?;
Ok(wasmi::FuncInstance::alloc_host(signature.clone(), idx.0))
}
@@ -95,18 +126,22 @@ impl ImportResolver for Imports {
module_name: &str,
field_name: &str,
_memory_type: &::wasmi::MemoryDescriptor,
) -> std::result::Result<MemoryRef, wasmi::Error> {
let key = (module_name.as_bytes().to_vec(), field_name.as_bytes().to_vec());
let mem = self
.memories_map
.get(&key)
.ok_or_else(|| {
wasmi::Error::Instantiation(format!(
"Export {}:{} not found",
module_name, field_name
))
})?
.clone();
) -> std::result::Result<wasmi::MemoryRef, wasmi::Error> {
let mem = self.memory_by_name(module_name, field_name).ok_or_else(|| {
wasmi::Error::Instantiation(format!("Export {}:{} not found", module_name, field_name))
})?;
let wrapper = mem.as_wasmi().ok_or_else(|| {
wasmi::Error::Instantiation(format!(
"Unsupported non-wasmi export {}:{}",
module_name, field_name
))
})?;
// Here we use inner memory reference only to resolve
// the imports without accessing the memory contents.
let mem = unsafe { wrapper.clone_inner() };
Ok(mem)
}
@@ -134,6 +169,7 @@ impl ImportResolver for Imports {
/// Note that this functions are only called in the `supervisor` context.
pub trait SandboxCapabilities: FunctionContext {
/// Represents a function reference into the supervisor environment.
/// Provides an abstraction over execution environment.
type SupervisorFuncRef;
/// Invoke a function in the supervisor environment.
@@ -141,9 +177,9 @@ pub trait SandboxCapabilities: FunctionContext {
/// This first invokes the dispatch_thunk function, passing in the function index of the
/// desired function to call and serialized arguments. The thunk calls the desired function
/// with the deserialized arguments, then serializes the result into memory and returns
/// reference. The pointer to and length of the result in linear memory is encoded into an i64,
/// with the upper 32 bits representing the pointer and the lower 32 bits representing the
/// length.
/// reference. The pointer to and length of the result in linear memory is encoded into an
/// `i64`, with the upper 32 bits representing the pointer and the lower 32 bits representing
/// the length.
///
/// # Errors
///
@@ -164,11 +200,17 @@ pub trait SandboxCapabilities: FunctionContext {
///
/// [`Externals`]: ../wasmi/trait.Externals.html
pub struct GuestExternals<'a, FE: SandboxCapabilities + 'a> {
/// Supervisor function environment
supervisor_externals: &'a mut FE,
/// Instance of sandboxed module to be dispatched
sandbox_instance: &'a SandboxInstance<FE::SupervisorFuncRef>,
/// External state passed to guest environment, see the `instantiate` function
state: u32,
}
/// Construct trap error from specified message
fn trap(msg: &'static str) -> Trap {
TrapKind::Host(Box::new(Error::Other(msg.into()))).into()
}
@@ -199,14 +241,15 @@ impl<'a, FE: SandboxCapabilities + 'a> Externals for GuestExternals<'a, FE> {
// Make `index` typesafe again.
let index = GuestFuncIndex(index);
// Convert function index from guest to supervisor space
let func_idx = self.sandbox_instance
.guest_to_supervisor_mapping
.func_by_guest_index(index)
.expect(
"`invoke_index` is called with indexes registered via `FuncInstance::alloc_host`;
`FuncInstance::alloc_host` is called with indexes that was obtained from `guest_to_supervisor_mapping`;
`func_by_guest_index` called with `index` can't return `None`;
qed"
`FuncInstance::alloc_host` is called with indexes that were obtained from `guest_to_supervisor_mapping`;
`func_by_guest_index` called with `index` can't return `None`;
qed"
);
// Serialize arguments into a byte vector.
@@ -220,7 +263,7 @@ impl<'a, FE: SandboxCapabilities + 'a> Externals for GuestExternals<'a, FE> {
let state = self.state;
// Move serialized arguments inside the memory and invoke dispatch thunk and
// Move serialized arguments inside the memory, invoke dispatch thunk and
// then free allocated memory.
let invoke_args_len = invoke_args_data.len() as WordSize;
let invoke_args_ptr = self
@@ -299,6 +342,16 @@ where
f(&mut guest_externals)
}
/// Module instance in terms of selected backend
enum BackendInstance {
/// Wasmi module instance
Wasmi(wasmi::ModuleRef),
/// Wasmer module instance
#[cfg(feature = "wasmer-sandbox")]
Wasmer(wasmer::Instance),
}
/// Sandboxed instance of a wasm module.
///
/// It's primary purpose is to [`invoke`] exported functions on it.
@@ -314,7 +367,7 @@ where
///
/// [`invoke`]: #method.invoke
pub struct SandboxInstance<FR> {
instance: ModuleRef,
backend_instance: BackendInstance,
dispatch_thunk: FR,
guest_to_supervisor_mapping: GuestToSupervisorFunctionMapping,
}
@@ -327,15 +380,78 @@ impl<FR> SandboxInstance<FR> {
///
/// The `state` parameter can be used to provide custom data for
/// these syscall implementations.
pub fn invoke<FE: SandboxCapabilities<SupervisorFuncRef = FR>>(
pub fn invoke<'a, FE, SCH, DTH>(
&self,
// function to call that is exported from the module
export_name: &str,
// arguments passed to the function
args: &[RuntimeValue],
supervisor_externals: &mut FE,
// arbitraty context data of the call
state: u32,
) -> std::result::Result<Option<wasmi::RuntimeValue>, wasmi::Error> {
with_guest_externals(supervisor_externals, self, state, |guest_externals| {
self.instance.invoke_export(export_name, args, guest_externals)
) -> std::result::Result<Option<wasmi::RuntimeValue>, wasmi::Error>
where
FE: SandboxCapabilities<SupervisorFuncRef = FR> + 'a,
SCH: SandboxCapabilitiesHolder<SupervisorFuncRef = FR, SC = FE>,
DTH: DispatchThunkHolder<DispatchThunk = FR>,
{
SCH::with_sandbox_capabilities(|supervisor_externals| {
with_guest_externals(supervisor_externals, self, state, |guest_externals| {
match &self.backend_instance {
BackendInstance::Wasmi(wasmi_instance) => {
let wasmi_result =
wasmi_instance.invoke_export(export_name, args, guest_externals)?;
Ok(wasmi_result)
},
#[cfg(feature = "wasmer-sandbox")]
BackendInstance::Wasmer(wasmer_instance) => {
let function = wasmer_instance
.exports
.get_function(export_name)
.map_err(|error| wasmi::Error::Function(error.to_string()))?;
let args: Vec<wasmer::Val> = args
.iter()
.map(|v| match *v {
RuntimeValue::I32(val) => wasmer::Val::I32(val),
RuntimeValue::I64(val) => wasmer::Val::I64(val),
RuntimeValue::F32(val) => wasmer::Val::F32(val.into()),
RuntimeValue::F64(val) => wasmer::Val::F64(val.into()),
})
.collect();
let wasmer_result =
DTH::initialize_thunk(&self.dispatch_thunk, || function.call(&args))
.map_err(|error| wasmi::Error::Function(error.to_string()))?;
if wasmer_result.len() > 1 {
return Err(wasmi::Error::Function(
"multiple return types are not supported yet".to_owned(),
))
}
let wasmer_result = if let Some(wasmer_value) = wasmer_result.first() {
let wasmer_value = match *wasmer_value {
wasmer::Val::I32(val) => RuntimeValue::I32(val),
wasmer::Val::I64(val) => RuntimeValue::I64(val),
wasmer::Val::F32(val) => RuntimeValue::F32(val.into()),
wasmer::Val::F64(val) => RuntimeValue::F64(val.into()),
_ => unreachable!(),
};
Some(wasmer_value)
} else {
None
};
Ok(wasmer_result)
},
}
})
})
}
@@ -343,9 +459,29 @@ impl<FR> SandboxInstance<FR> {
///
/// Returns `Some(_)` if the global could be found.
pub fn get_global_val(&self, name: &str) -> Option<sp_wasm_interface::Value> {
let global = self.instance.export_by_name(name)?.as_global()?.get();
match &self.backend_instance {
BackendInstance::Wasmi(wasmi_instance) => {
let wasmi_global = wasmi_instance.export_by_name(name)?.as_global()?.get();
Some(global.into())
Some(wasmi_global.into())
},
#[cfg(feature = "wasmer-sandbox")]
BackendInstance::Wasmer(wasmer_instance) => {
use sp_wasm_interface::Value;
let global = wasmer_instance.exports.get_global(name).ok()?;
let wasmtime_value = match global.get() {
wasmer::Val::I32(val) => Value::I32(val),
wasmer::Val::I64(val) => Value::I64(val),
wasmer::Val::F32(val) => Value::F32(f32::to_bits(val)),
wasmer::Val::F64(val) => Value::F64(f64::to_bits(val)),
_ => None?,
};
Some(wasmtime_value)
},
}
}
}
@@ -366,7 +502,7 @@ pub enum InstantiationError {
fn decode_environment_definition(
mut raw_env_def: &[u8],
memories: &[Option<MemoryRef>],
memories: &[Option<Memory>],
) -> std::result::Result<(Imports, GuestToSupervisorFunctionMapping), InstantiationError> {
let env_def = sandbox_primitives::EnvironmentDefinition::decode(&mut raw_env_def)
.map_err(|_| InstantiationError::EnvironmentDefinitionCorrupted)?;
@@ -401,7 +537,10 @@ fn decode_environment_definition(
/// An environment in which the guest module is instantiated.
pub struct GuestEnvironment {
/// Function and memory imports of the guest module
imports: Imports,
/// Supervisor functinons mapped to guest index space
guest_to_supervisor_mapping: GuestToSupervisorFunctionMapping,
}
@@ -436,47 +575,142 @@ impl<FR> UnregisteredInstance<FR> {
}
}
/// Instantiate a guest module and return it's index in the store.
///
/// The guest module's code is specified in `wasm`. Environment that will be available to
/// guest module is specified in `raw_env_def` (serialized version of [`EnvironmentDefinition`]).
/// `dispatch_thunk` is used as function that handle calls from guests.
///
/// # Errors
///
/// Returns `Err` if any of the following conditions happens:
///
/// - `raw_env_def` can't be deserialized as a [`EnvironmentDefinition`].
/// - Module in `wasm` is invalid or couldn't be instantiated.
///
/// [`EnvironmentDefinition`]: ../sandbox/struct.EnvironmentDefinition.html
pub fn instantiate<'a, FE: SandboxCapabilities>(
supervisor_externals: &mut FE,
dispatch_thunk: FE::SupervisorFuncRef,
wasm: &[u8],
host_env: GuestEnvironment,
state: u32,
) -> std::result::Result<UnregisteredInstance<FE::SupervisorFuncRef>, InstantiationError> {
let module = Module::from_buffer(wasm).map_err(|_| InstantiationError::ModuleDecoding)?;
let instance = ModuleInstance::new(&module, &host_env.imports)
.map_err(|_| InstantiationError::Instantiation)?;
/// Helper type to provide sandbox capabilities to the inner context
pub trait SandboxCapabilitiesHolder {
/// Supervisor function reference
type SupervisorFuncRef;
let sandbox_instance = Rc::new(SandboxInstance {
// In general, it's not a very good idea to use `.not_started_instance()` for anything
// but for extracting memory and tables. But in this particular case, we are extracting
// for the purpose of running `start` function which should be ok.
instance: instance.not_started_instance().clone(),
dispatch_thunk,
guest_to_supervisor_mapping: host_env.guest_to_supervisor_mapping,
});
/// Capabilities trait
type SC: SandboxCapabilities<SupervisorFuncRef = Self::SupervisorFuncRef>;
with_guest_externals(supervisor_externals, &sandbox_instance, state, |guest_externals| {
instance
.run_start(guest_externals)
.map_err(|_| InstantiationError::StartTrapped)
})?;
/// Wrapper that provides sandbox capabilities in a limited context
fn with_sandbox_capabilities<R, F: FnOnce(&mut Self::SC) -> R>(f: F) -> R;
}
Ok(UnregisteredInstance { sandbox_instance })
/// Helper type to provide dispatch thunk to the inner context
pub trait DispatchThunkHolder {
/// Dispatch thunk for this particular context
type DispatchThunk;
/// Provide `DispatchThunk` for the runtime method call and execute the given function `f`.
///
/// During the execution of the provided function `dispatch_thunk` will be callable.
fn initialize_thunk<R, F>(s: &Self::DispatchThunk, f: F) -> R
where
F: FnOnce() -> R;
/// Wrapper that provides dispatch thunk in a limited context
fn with_dispatch_thunk<R, F: FnOnce(&mut Self::DispatchThunk) -> R>(f: F) -> R;
}
/// Sandbox backend to use
pub enum SandboxBackend {
/// Wasm interpreter
Wasmi,
/// Wasmer environment
#[cfg(feature = "wasmer-sandbox")]
Wasmer,
/// Use wasmer backend if available. Fall back to wasmi otherwise.
TryWasmer,
}
/// Memory reference in terms of a selected backend
#[derive(Clone, Debug)]
pub enum Memory {
/// Wasmi memory reference
Wasmi(WasmiMemoryWrapper),
/// Wasmer memory refernce
#[cfg(feature = "wasmer-sandbox")]
Wasmer(WasmerMemoryWrapper),
}
impl Memory {
/// View as wasmi memory
pub fn as_wasmi(&self) -> Option<WasmiMemoryWrapper> {
match self {
Memory::Wasmi(memory) => Some(memory.clone()),
#[cfg(feature = "wasmer-sandbox")]
Memory::Wasmer(_) => None,
}
}
/// View as wasmer memory
#[cfg(feature = "wasmer-sandbox")]
pub fn as_wasmer(&self) -> Option<WasmerMemoryWrapper> {
match self {
Memory::Wasmer(memory) => Some(memory.clone()),
Memory::Wasmi(_) => None,
}
}
}
impl util::MemoryTransfer for Memory {
fn read(&self, source_addr: Pointer<u8>, size: usize) -> Result<Vec<u8>> {
match self {
Memory::Wasmi(sandboxed_memory) => sandboxed_memory.read(source_addr, size),
#[cfg(feature = "wasmer-sandbox")]
Memory::Wasmer(sandboxed_memory) => sandboxed_memory.read(source_addr, size),
}
}
fn read_into(&self, source_addr: Pointer<u8>, destination: &mut [u8]) -> Result<()> {
match self {
Memory::Wasmi(sandboxed_memory) => sandboxed_memory.read_into(source_addr, destination),
#[cfg(feature = "wasmer-sandbox")]
Memory::Wasmer(sandboxed_memory) => sandboxed_memory.read_into(source_addr, destination),
}
}
fn write_from(&self, dest_addr: Pointer<u8>, source: &[u8]) -> Result<()> {
match self {
Memory::Wasmi(sandboxed_memory) => sandboxed_memory.write_from(dest_addr, source),
#[cfg(feature = "wasmer-sandbox")]
Memory::Wasmer(sandboxed_memory) => sandboxed_memory.write_from(dest_addr, source),
}
}
}
/// Wasmer specific context
#[cfg(feature = "wasmer-sandbox")]
struct WasmerBackend {
store: wasmer::Store,
}
/// Information specific to a particular execution backend
enum BackendContext {
/// Wasmi specific context
Wasmi,
/// Wasmer specific context
#[cfg(feature = "wasmer-sandbox")]
Wasmer(WasmerBackend),
}
impl BackendContext {
pub fn new(backend: SandboxBackend) -> BackendContext {
match backend {
SandboxBackend::Wasmi => BackendContext::Wasmi,
#[cfg(not(feature = "wasmer-sandbox"))]
SandboxBackend::TryWasmer => BackendContext::Wasmi,
#[cfg(feature = "wasmer-sandbox")]
SandboxBackend::Wasmer | SandboxBackend::TryWasmer => {
let compiler = wasmer_compiler_singlepass::Singlepass::default();
BackendContext::Wasmer(WasmerBackend {
store: wasmer::Store::new(&wasmer::JIT::new(compiler).engine()),
})
},
}
}
}
/// This struct keeps track of all sandboxed components.
@@ -485,13 +719,18 @@ pub fn instantiate<'a, FE: SandboxCapabilities>(
pub struct Store<FR> {
// Memories and instances are `Some` until torn down.
instances: Vec<Option<Rc<SandboxInstance<FR>>>>,
memories: Vec<Option<MemoryRef>>,
memories: Vec<Option<Memory>>,
backend_context: BackendContext,
}
impl<FR> Store<FR> {
/// Create a new empty sandbox store.
pub fn new() -> Self {
Store { instances: Vec::new(), memories: Vec::new() }
pub fn new(backend: SandboxBackend) -> Self {
Store {
instances: Vec::new(),
memories: Vec::new(),
backend_context: BackendContext::new(backend),
}
}
/// Create a new memory instance and return it's index.
@@ -501,15 +740,33 @@ impl<FR> Store<FR> {
/// Returns `Err` if the memory couldn't be created.
/// Typically happens if `initial` is more than `maximum`.
pub fn new_memory(&mut self, initial: u32, maximum: u32) -> Result<u32> {
let memories = &mut self.memories;
let backend_context = &self.backend_context;
let maximum = match maximum {
sandbox_primitives::MEM_UNLIMITED => None,
specified_limit => Some(Pages(specified_limit as usize)),
specified_limit => Some(specified_limit),
};
let mem = MemoryInstance::alloc(Pages(initial as usize), maximum)?;
let memory = match &backend_context {
BackendContext::Wasmi => Memory::Wasmi(WasmiMemoryWrapper::new(MemoryInstance::alloc(
Pages(initial as usize),
maximum.map(|m| Pages(m as usize)),
)?)),
#[cfg(feature = "wasmer-sandbox")]
BackendContext::Wasmer(context) => {
let ty = wasmer::MemoryType::new(initial, maximum, false);
Memory::Wasmer(WasmerMemoryWrapper::new(
wasmer::Memory::new(&context.store, ty)
.map_err(|_| Error::InvalidMemoryReference)?,
))
},
};
let mem_idx = memories.len();
memories.push(Some(memory.clone()));
let mem_idx = self.memories.len();
self.memories.push(Some(mem));
Ok(mem_idx as u32)
}
@@ -533,7 +790,7 @@ impl<FR> Store<FR> {
///
/// Returns `Err` If `memory_idx` isn't a valid index of an memory or
/// if memory has been torn down.
pub fn memory(&self, memory_idx: u32) -> Result<MemoryRef> {
pub fn memory(&self, memory_idx: u32) -> Result<Memory> {
self.memories
.get(memory_idx as usize)
.cloned()
@@ -575,9 +832,307 @@ impl<FR> Store<FR> {
}
}
/// Instantiate a guest module and return it's index in the store.
///
/// The guest module's code is specified in `wasm`. Environment that will be available to
/// guest module is specified in `guest_env`. A dispatch thunk is used as function that
/// handle calls from guests. `state` is an opaque pointer to caller's arbitrary context
/// normally created by `sp_sandbox::Instance` primitive.
///
/// Note: Due to borrowing constraints dispatch thunk is now propagated using DTH
///
/// Returns uninitialized sandboxed module instance or an instantiation error.
pub fn instantiate<'a, FE, SCH, DTH>(
&mut self,
wasm: &[u8],
guest_env: GuestEnvironment,
state: u32,
) -> std::result::Result<UnregisteredInstance<FR>, InstantiationError>
where
FR: Clone + 'static,
FE: SandboxCapabilities<SupervisorFuncRef = FR> + 'a,
SCH: SandboxCapabilitiesHolder<SupervisorFuncRef = FR, SC = FE>,
DTH: DispatchThunkHolder<DispatchThunk = FR>,
{
let backend_context = &self.backend_context;
let sandbox_instance = match backend_context {
BackendContext::Wasmi =>
Self::instantiate_wasmi::<FE, SCH, DTH>(wasm, guest_env, state)?,
#[cfg(feature = "wasmer-sandbox")]
BackendContext::Wasmer(context) =>
Self::instantiate_wasmer::<FE, SCH, DTH>(context, wasm, guest_env, state)?,
};
Ok(UnregisteredInstance { sandbox_instance })
}
}
// Private routines
impl<FR> Store<FR> {
fn register_sandbox_instance(&mut self, sandbox_instance: Rc<SandboxInstance<FR>>) -> u32 {
let instance_idx = self.instances.len();
self.instances.push(Some(sandbox_instance));
instance_idx as u32
}
fn instantiate_wasmi<'a, FE, SCH, DTH>(
wasm: &[u8],
guest_env: GuestEnvironment,
state: u32,
) -> std::result::Result<Rc<SandboxInstance<FR>>, InstantiationError>
where
FR: Clone + 'static,
FE: SandboxCapabilities<SupervisorFuncRef = FR> + 'a,
SCH: SandboxCapabilitiesHolder<SupervisorFuncRef = FR, SC = FE>,
DTH: DispatchThunkHolder<DispatchThunk = FR>,
{
let wasmi_module =
Module::from_buffer(wasm).map_err(|_| InstantiationError::ModuleDecoding)?;
let wasmi_instance = ModuleInstance::new(&wasmi_module, &guest_env.imports)
.map_err(|_| InstantiationError::Instantiation)?;
let sandbox_instance = DTH::with_dispatch_thunk(|dispatch_thunk| {
Rc::new(SandboxInstance {
// In general, it's not a very good idea to use `.not_started_instance()` for
// anything but for extracting memory and tables. But in this particular case, we
// are extracting for the purpose of running `start` function which should be ok.
backend_instance: BackendInstance::Wasmi(
wasmi_instance.not_started_instance().clone(),
),
dispatch_thunk: dispatch_thunk.clone(),
guest_to_supervisor_mapping: guest_env.guest_to_supervisor_mapping,
})
});
SCH::with_sandbox_capabilities(|supervisor_externals| {
with_guest_externals(
supervisor_externals,
&sandbox_instance,
state,
|guest_externals| {
wasmi_instance
.run_start(guest_externals)
.map_err(|_| InstantiationError::StartTrapped)
// Note: no need to run start on wasmtime instance, since it's done
// automatically
},
)
})?;
Ok(sandbox_instance)
}
#[cfg(feature = "wasmer-sandbox")]
fn instantiate_wasmer<'a, FE, SCH, DTH>(
context: &WasmerBackend,
wasm: &[u8],
guest_env: GuestEnvironment,
state: u32,
) -> std::result::Result<Rc<SandboxInstance<FR>>, InstantiationError>
where
FR: Clone + 'static,
FE: SandboxCapabilities<SupervisorFuncRef = FR> + 'a,
SCH: SandboxCapabilitiesHolder<SupervisorFuncRef = FR, SC = FE>,
DTH: DispatchThunkHolder<DispatchThunk = FR>,
{
let module = wasmer::Module::new(&context.store, wasm)
.map_err(|_| InstantiationError::ModuleDecoding)?;
type Exports = HashMap<String, wasmer::Exports>;
let mut exports_map = Exports::new();
for import in module.imports().into_iter() {
match import.ty() {
// Nothing to do here
wasmer::ExternType::Global(_) | wasmer::ExternType::Table(_) => (),
wasmer::ExternType::Memory(_) => {
let exports = exports_map
.entry(import.module().to_string())
.or_insert(wasmer::Exports::new());
let memory = guest_env
.imports
.memory_by_name(import.module(), import.name())
.ok_or(InstantiationError::ModuleDecoding)?;
let mut wasmer_memory_ref = memory.as_wasmer().expect(
"memory is created by wasmer; \
exported by the same module and backend; \
thus the operation can't fail; \
qed",
);
// This is safe since we're only instantiating the module and populating
// the export table, so no memory access can happen at this time.
// All subsequent memory accesses should happen through the wrapper,
// that enforces the memory access protocol.
let wasmer_memory = unsafe { wasmer_memory_ref.clone_inner() };
exports.insert(import.name(), wasmer::Extern::Memory(wasmer_memory));
},
wasmer::ExternType::Function(func_ty) => {
let guest_func_index =
guest_env.imports.func_by_name(import.module(), import.name());
let guest_func_index = if let Some(index) = guest_func_index {
index
} else {
// Missing import (should we abort here?)
continue
};
let supervisor_func_index = guest_env
.guest_to_supervisor_mapping
.func_by_guest_index(guest_func_index)
.ok_or(InstantiationError::ModuleDecoding)?;
let function = Self::wasmer_dispatch_function::<FE, SCH, DTH>(
supervisor_func_index,
&context.store,
func_ty,
state,
);
let exports = exports_map
.entry(import.module().to_string())
.or_insert(wasmer::Exports::new());
exports.insert(import.name(), wasmer::Extern::Function(function));
},
}
}
let mut import_object = wasmer::ImportObject::new();
for (module_name, exports) in exports_map.into_iter() {
import_object.register(module_name, exports);
}
let instance =
wasmer::Instance::new(&module, &import_object).map_err(|error| match error {
wasmer::InstantiationError::Link(_) => InstantiationError::Instantiation,
wasmer::InstantiationError::Start(_) => InstantiationError::StartTrapped,
wasmer::InstantiationError::HostEnvInitialization(_) =>
InstantiationError::EnvironmentDefinitionCorrupted,
})?;
Ok(Rc::new(SandboxInstance {
backend_instance: BackendInstance::Wasmer(instance),
dispatch_thunk: DTH::with_dispatch_thunk(|dispatch_thunk| dispatch_thunk.clone()),
guest_to_supervisor_mapping: guest_env.guest_to_supervisor_mapping,
}))
}
#[cfg(feature = "wasmer-sandbox")]
fn wasmer_dispatch_function<'a, FE, SCH, DTH>(
supervisor_func_index: SupervisorFuncIndex,
store: &wasmer::Store,
func_ty: &wasmer::FunctionType,
state: u32,
) -> wasmer::Function
where
FR: Clone + 'static,
FE: SandboxCapabilities<SupervisorFuncRef = FR> + 'a,
SCH: SandboxCapabilitiesHolder<SupervisorFuncRef = FR, SC = FE>,
DTH: DispatchThunkHolder<DispatchThunk = FR>,
{
wasmer::Function::new(store, func_ty, move |params| {
SCH::with_sandbox_capabilities(|supervisor_externals| {
use sp_wasm_interface::Value;
// Serialize arguments into a byte vector.
let invoke_args_data = params
.iter()
.map(|val| match val {
wasmer::Val::I32(val) => Value::I32(*val),
wasmer::Val::I64(val) => Value::I64(*val),
wasmer::Val::F32(val) => Value::F32(f32::to_bits(*val)),
wasmer::Val::F64(val) => Value::F64(f64::to_bits(*val)),
_ => unimplemented!(),
})
.collect::<Vec<_>>()
.encode();
// Move serialized arguments inside the memory, invoke dispatch thunk and
// then free allocated memory.
let invoke_args_len = invoke_args_data.len() as WordSize;
let invoke_args_ptr =
supervisor_externals.allocate_memory(invoke_args_len).map_err(|_| {
wasmer::RuntimeError::new(
"Can't allocate memory in supervisor for the arguments",
)
})?;
let deallocate = |fe: &mut FE, ptr, fail_msg| {
fe.deallocate_memory(ptr).map_err(|_| wasmer::RuntimeError::new(fail_msg))
};
if supervisor_externals.write_memory(invoke_args_ptr, &invoke_args_data).is_err() {
deallocate(
supervisor_externals,
invoke_args_ptr,
"Failed dealloction after failed write of invoke arguments",
)?;
return Err(wasmer::RuntimeError::new("Can't write invoke args into memory"))
}
// Perform the actuall call
let serialized_result = DTH::with_dispatch_thunk(|dispatch_thunk| {
supervisor_externals.invoke(
&dispatch_thunk,
invoke_args_ptr,
invoke_args_len,
state,
supervisor_func_index,
)
})
.map_err(|e| wasmer::RuntimeError::new(e.to_string()))?;
// dispatch_thunk returns pointer to serialized arguments.
// Unpack pointer and len of the serialized result data.
let (serialized_result_val_ptr, serialized_result_val_len) = {
// Cast to u64 to use zero-extension.
let v = serialized_result as u64;
let ptr = (v as u64 >> 32) as u32;
let len = (v & 0xFFFFFFFF) as u32;
(Pointer::new(ptr), len)
};
let serialized_result_val = supervisor_externals
.read_memory(serialized_result_val_ptr, serialized_result_val_len)
.map_err(|_| {
wasmer::RuntimeError::new(
"Can't read the serialized result from dispatch thunk",
)
});
let deserialized_result = deallocate(
supervisor_externals,
serialized_result_val_ptr,
"Can't deallocate memory for dispatch thunk's result",
)
.and_then(|_| serialized_result_val)
.and_then(|serialized_result_val| {
deserialize_result(&serialized_result_val)
.map_err(|e| wasmer::RuntimeError::new(e.to_string()))
})?;
if let Some(value) = deserialized_result {
Ok(vec![match value {
RuntimeValue::I32(val) => wasmer::Val::I32(val),
RuntimeValue::I64(val) => wasmer::Val::I64(val),
RuntimeValue::F32(val) => wasmer::Val::F32(val.into()),
RuntimeValue::F64(val) => wasmer::Val::F64(val.into()),
}])
} else {
Ok(vec![])
}
})
})
}
}
@@ -0,0 +1,241 @@
// This file is part of Substrate.
// Copyright (C) 2019-2021 Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! Utilities used by all backends
use crate::error::{Error, Result};
use sp_wasm_interface::Pointer;
use std::ops::Range;
/// Construct a range from an offset to a data length after the offset.
/// Returns None if the end of the range would exceed some maximum offset.
pub fn checked_range(offset: usize, len: usize, max: usize) -> Option<Range<usize>> {
let end = offset.checked_add(len)?;
if end <= max {
Some(offset..end)
} else {
None
}
}
/// Provides safe memory access interface using an external buffer
pub trait MemoryTransfer {
/// Read data from a slice of memory into a newly allocated buffer.
///
/// Returns an error if the read would go out of the memory bounds.
fn read(&self, source_addr: Pointer<u8>, size: usize) -> Result<Vec<u8>>;
/// Read data from a slice of memory into a destination buffer.
///
/// Returns an error if the read would go out of the memory bounds.
fn read_into(&self, source_addr: Pointer<u8>, destination: &mut [u8]) -> Result<()>;
/// Write data to a slice of memory.
///
/// Returns an error if the write would go out of the memory bounds.
fn write_from(&self, dest_addr: Pointer<u8>, source: &[u8]) -> Result<()>;
}
/// Safe wrapper over wasmi memory reference
pub mod wasmi {
use super::*;
/// Wasmi provides direct access to its memory using slices.
///
/// This wrapper limits the scope where the slice can be taken to
#[derive(Debug, Clone)]
pub struct MemoryWrapper(::wasmi::MemoryRef);
impl MemoryWrapper {
/// Take ownership of the memory region and return a wrapper object
pub fn new(memory: ::wasmi::MemoryRef) -> Self {
Self(memory)
}
/// Clone the underlying memory object
///
/// # Safety
///
/// The sole purpose of `MemoryRef` is to protect the memory from uncontrolled
/// access. By returning the memory object "as is" we bypass all of the checks.
///
/// Intended to use only during module initialization.
pub unsafe fn clone_inner(&self) -> ::wasmi::MemoryRef {
self.0.clone()
}
}
impl super::MemoryTransfer for MemoryWrapper {
fn read(&self, source_addr: Pointer<u8>, size: usize) -> Result<Vec<u8>> {
self.0.with_direct_access(|source| {
let range = checked_range(source_addr.into(), size, source.len())
.ok_or_else(|| Error::Other("memory read is out of bounds".into()))?;
Ok(Vec::from(&source[range]))
})
}
fn read_into(&self, source_addr: Pointer<u8>, destination: &mut [u8]) -> Result<()> {
self.0.with_direct_access(|source| {
let range = checked_range(source_addr.into(), destination.len(), source.len())
.ok_or_else(|| Error::Other("memory read is out of bounds".into()))?;
destination.copy_from_slice(&source[range]);
Ok(())
})
}
fn write_from(&self, dest_addr: Pointer<u8>, source: &[u8]) -> Result<()> {
self.0.with_direct_access_mut(|destination| {
let range = checked_range(dest_addr.into(), source.len(), destination.len())
.ok_or_else(|| Error::Other("memory write is out of bounds".into()))?;
&mut destination[range].copy_from_slice(source);
Ok(())
})
}
}
}
// Routines specific to Wasmer runtime. Since sandbox can be invoked from both
/// wasmi and wasmtime runtime executors, we need to have a way to deal with sanbox
/// backends right from the start.
#[cfg(feature = "wasmer-sandbox")]
pub mod wasmer {
use super::checked_range;
use crate::error::{Error, Result};
use sp_wasm_interface::Pointer;
use std::{cell::RefCell, convert::TryInto, rc::Rc};
/// In order to enforce memory access protocol to the backend memory
/// we wrap it with `RefCell` and encapsulate all memory operations.
#[derive(Debug, Clone)]
pub struct MemoryWrapper {
buffer: Rc<RefCell<wasmer::Memory>>,
}
impl MemoryWrapper {
/// Take ownership of the memory region and return a wrapper object
pub fn new(memory: wasmer::Memory) -> Self {
Self { buffer: Rc::new(RefCell::new(memory)) }
}
/// Returns linear memory of the wasm instance as a slice.
///
/// # Safety
///
/// Wasmer doesn't provide comprehensive documentation about the exact behavior of the data
/// pointer. If a dynamic style heap is used the base pointer of the heap can change. Since
/// growing, we cannot guarantee the lifetime of the returned slice reference.
unsafe fn memory_as_slice(memory: &wasmer::Memory) -> &[u8] {
let ptr = memory.data_ptr() as *const _;
let len: usize =
memory.data_size().try_into().expect("data size should fit into usize");
if len == 0 {
&[]
} else {
core::slice::from_raw_parts(ptr, len)
}
}
/// Returns linear memory of the wasm instance as a slice.
///
/// # Safety
///
/// See `[memory_as_slice]`. In addition to those requirements, since a mutable reference is
/// returned it must be ensured that only one mutable and no shared references to memory
/// exists at the same time.
unsafe fn memory_as_slice_mut(memory: &wasmer::Memory) -> &mut [u8] {
let ptr = memory.data_ptr();
let len: usize =
memory.data_size().try_into().expect("data size should fit into usize");
if len == 0 {
&mut []
} else {
core::slice::from_raw_parts_mut(ptr, len)
}
}
/// Clone the underlying memory object
///
/// # Safety
///
/// The sole purpose of `MemoryRef` is to protect the memory from uncontrolled
/// access. By returning the memory object "as is" we bypass all of the checks.
///
/// Intended to use only during module initialization.
///
/// # Panics
///
/// Will panic if `MemoryRef` is currently in use.
pub unsafe fn clone_inner(&mut self) -> wasmer::Memory {
// We take exclusive lock to ensure that we're the only one here
self.buffer.borrow_mut().clone()
}
}
impl super::MemoryTransfer for MemoryWrapper {
fn read(&self, source_addr: Pointer<u8>, size: usize) -> Result<Vec<u8>> {
let memory = self.buffer.borrow();
let data_size = memory.data_size().try_into().expect("data size does not fit");
let range = checked_range(source_addr.into(), size, data_size)
.ok_or_else(|| Error::Other("memory read is out of bounds".into()))?;
let mut buffer = vec![0; range.len()];
self.read_into(source_addr, &mut buffer)?;
Ok(buffer)
}
fn read_into(&self, source_addr: Pointer<u8>, destination: &mut [u8]) -> Result<()> {
unsafe {
let memory = self.buffer.borrow();
// This should be safe since we don't grow up memory while caching this reference
// and we give up the reference before returning from this function.
let source = Self::memory_as_slice(&memory);
let range = checked_range(source_addr.into(), destination.len(), source.len())
.ok_or_else(|| Error::Other("memory read is out of bounds".into()))?;
destination.copy_from_slice(&source[range]);
Ok(())
}
}
fn write_from(&self, dest_addr: Pointer<u8>, source: &[u8]) -> Result<()> {
unsafe {
let memory = self.buffer.borrow_mut();
// This should be safe since we don't grow up memory while caching this reference
// and we give up the reference before returning from this function.
let destination = Self::memory_as_slice_mut(&memory);
let range = checked_range(dest_addr.into(), source.len(), destination.len())
.ok_or_else(|| Error::Other("memory write is out of bounds".into()))?;
&mut destination[range].copy_from_slice(source);
Ok(())
}
}
}
}