From 0590389ed52691f5af0c3a945f96b85e54c1cdb1 Mon Sep 17 00:00:00 2001 From: Vadim Smirnov Date: Mon, 19 Dec 2022 14:43:18 +0300 Subject: [PATCH] Revert "Remove sandboxing host function interface (#12852)" This reverts commit 5722ece5efb61cc82307f3e919a60bd4e807f1df. --- Cargo.toml | 1 + bin/node/runtime/Cargo.toml | 7 + client/executor/Cargo.toml | 3 + client/executor/common/Cargo.toml | 7 + client/executor/common/src/error.rs | 3 + client/executor/common/src/lib.rs | 1 + client/executor/common/src/sandbox.rs | 585 ++++++++++++++++++ .../common/src/sandbox/wasmer_backend.rs | 449 ++++++++++++++ .../common/src/sandbox/wasmi_backend.rs | 339 ++++++++++ client/executor/runtime-test/Cargo.toml | 3 + client/executor/runtime-test/src/lib.rs | 159 +++++ client/executor/src/integration_tests/mod.rs | 106 ++++ .../executor/src/integration_tests/sandbox.rs | 339 ++++++++++ client/executor/src/lib.rs | 2 +- client/executor/wasmi/Cargo.toml | 2 + client/executor/wasmi/src/lib.rs | 232 ++++++- client/executor/wasmtime/Cargo.toml | 4 +- client/executor/wasmtime/src/host.rs | 277 ++++++++- client/executor/wasmtime/src/runtime.rs | 10 + client/executor/wasmtime/src/util.rs | 18 + primitives/io/src/lib.rs | 94 +++ primitives/sandbox/Cargo.toml | 40 ++ primitives/sandbox/README.md | 21 + primitives/sandbox/src/embedded_executor.rs | 478 ++++++++++++++ primitives/sandbox/src/env.rs | 120 ++++ primitives/sandbox/src/host_executor.rs | 274 ++++++++ primitives/sandbox/src/lib.rs | 190 ++++++ primitives/wasm-interface/src/lib.rs | 57 ++ scripts/ci/gitlab/pipeline/test.yml | 37 +- 29 files changed, 3850 insertions(+), 8 deletions(-) create mode 100644 client/executor/common/src/sandbox.rs create mode 100644 client/executor/common/src/sandbox/wasmer_backend.rs create mode 100644 client/executor/common/src/sandbox/wasmi_backend.rs create mode 100644 client/executor/src/integration_tests/sandbox.rs create mode 100644 primitives/sandbox/Cargo.toml create mode 100644 primitives/sandbox/README.md create mode 100644 primitives/sandbox/src/embedded_executor.rs create mode 100644 primitives/sandbox/src/env.rs create mode 100644 primitives/sandbox/src/host_executor.rs create mode 100644 primitives/sandbox/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 8f55d8e527ecd..6473d3f313099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -209,6 +209,7 @@ members = [ "primitives/runtime-interface/test", "primitives/runtime-interface/test-wasm", "primitives/runtime-interface/test-wasm-deprecated", + "primitives/sandbox", "primitives/serializer", "primitives/session", "primitives/staking", diff --git a/bin/node/runtime/Cargo.toml b/bin/node/runtime/Cargo.toml index 7037be9d7c54d..ea384f999c46b 100644 --- a/bin/node/runtime/Cargo.toml +++ b/bin/node/runtime/Cargo.toml @@ -40,6 +40,7 @@ sp-session = { version = "4.0.0-dev", default-features = false, path = "../../.. sp-transaction-pool = { version = "4.0.0-dev", default-features = false, path = "../../../primitives/transaction-pool" } sp-version = { version = "5.0.0", default-features = false, path = "../../../primitives/version" } sp-io = { version = "7.0.0", default-features = false, path = "../../../primitives/io" } +sp-sandbox = { version = "0.10.0-dev", default-features = false, path = "../../../primitives/sandbox" } # frame dependencies frame-executive = { version = "4.0.0-dev", default-features = false, path = "../../../frame/executive" } @@ -119,6 +120,7 @@ substrate-wasm-builder = { version = "5.0.0-dev", path = "../../../utils/wasm-bu default = ["std"] with-tracing = ["frame-executive/with-tracing"] std = [ + "sp-sandbox/std", "pallet-whitelist/std", "pallet-offences-benchmarking?/std", "pallet-election-provider-support-benchmarking?/std", @@ -320,3 +322,8 @@ try-runtime = [ "pallet-vesting/try-runtime", "pallet-whitelist/try-runtime", ] +# Force `sp-sandbox` to call into the host resident executor. One still need to make sure +# that `sc-executor` gets the `wasmer-sandbox` feature which happens automatically when +# specified on the command line. +# Don't use that on a production chain. +wasmer-sandbox = ["sp-sandbox/wasmer-sandbox"] diff --git a/client/executor/Cargo.toml b/client/executor/Cargo.toml index 83d4801d1c92b..7cba725a9ea45 100644 --- a/client/executor/Cargo.toml +++ b/client/executor/Cargo.toml @@ -14,6 +14,7 @@ readme = "README.md" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +lazy_static = "1.4.0" lru = "0.8.1" parking_lot = "0.12.1" tracing = "0.1.29" @@ -25,6 +26,7 @@ sc-executor-wasmi = { version = "0.10.0-dev", path = "wasmi" } sc-executor-wasmtime = { version = "0.10.0-dev", path = "wasmtime" } sp-api = { version = "4.0.0-dev", path = "../../primitives/api" } sp-core = { version = "7.0.0", path = "../../primitives/core" } +sp-core-hashing-proc-macro = { version = "5.0.0", path = "../../primitives/core/hashing/proc-macro" } sp-externalities = { version = "0.13.0", path = "../../primitives/externalities" } sp-io = { version = "7.0.0", path = "../../primitives/io" } sp-panic-handler = { version = "5.0.0", path = "../../primitives/panic-handler" } @@ -59,3 +61,4 @@ default = ["std"] # This crate does not have `no_std` support, we just require this for tests std = [] wasm-extern-trace = [] +wasmer-sandbox = ["sc-executor-common/wasmer-sandbox"] diff --git a/client/executor/common/Cargo.toml b/client/executor/common/Cargo.toml index 648e937d371ff..4b83e9fcc9b92 100644 --- a/client/executor/common/Cargo.toml +++ b/client/executor/common/Cargo.toml @@ -14,12 +14,19 @@ readme = "README.md" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0" } +environmental = "1.1.3" thiserror = "1.0.30" wasm-instrument = "0.3" +wasmer = { version = "2.2", features = ["singlepass"], optional = true } wasmi = "0.13" sc-allocator = { version = "4.1.0-dev", path = "../../allocator" } sp-maybe-compressed-blob = { version = "4.1.0-dev", path = "../../../primitives/maybe-compressed-blob" } +sp-sandbox = { version = "0.10.0-dev", path = "../../../primitives/sandbox" } sp-wasm-interface = { version = "7.0.0", path = "../../../primitives/wasm-interface" } [features] default = [] +wasmer-sandbox = [ + "wasmer", +] diff --git a/client/executor/common/src/error.rs b/client/executor/common/src/error.rs index c35a874b7796d..376ac190bd7b7 100644 --- a/client/executor/common/src/error.rs +++ b/client/executor/common/src/error.rs @@ -30,6 +30,9 @@ pub enum Error { #[error(transparent)] Wasmi(#[from] wasmi::Error), + #[error("Sandbox error: {0}")] + Sandbox(String), + #[error("Error calling api function: {0}")] ApiError(Box), diff --git a/client/executor/common/src/lib.rs b/client/executor/common/src/lib.rs index 79bb74b62a41e..b69883afbaac2 100644 --- a/client/executor/common/src/lib.rs +++ b/client/executor/common/src/lib.rs @@ -23,5 +23,6 @@ pub mod error; pub mod runtime_blob; +pub mod sandbox; pub mod util; pub mod wasm_runtime; diff --git a/client/executor/common/src/sandbox.rs b/client/executor/common/src/sandbox.rs new file mode 100644 index 0000000000000..1e925bd5a7835 --- /dev/null +++ b/client/executor/common/src/sandbox.rs @@ -0,0 +1,585 @@ +// This file is part of Substrate. + +// Copyright (C) 2018-2022 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 . + +//! This module implements sandboxing support in the runtime. +//! +//! Sandboxing is backed by wasmi and wasmer, depending on the configuration. + +#[cfg(feature = "wasmer-sandbox")] +mod wasmer_backend; +mod wasmi_backend; + +use std::{collections::HashMap, rc::Rc}; + +use codec::Decode; +use sp_sandbox::env as sandbox_env; +use sp_wasm_interface::{FunctionContext, Pointer, WordSize}; + +use crate::{ + error::{self, Result}, + util, +}; + +#[cfg(feature = "wasmer-sandbox")] +use self::wasmer_backend::{ + get_global as wasmer_get_global, instantiate as wasmer_instantiate, invoke as wasmer_invoke, + new_memory as wasmer_new_memory, Backend as WasmerBackend, + MemoryWrapper as WasmerMemoryWrapper, +}; +use self::wasmi_backend::{ + get_global as wasmi_get_global, instantiate as wasmi_instantiate, invoke as wasmi_invoke, + new_memory as wasmi_new_memory, MemoryWrapper as WasmiMemoryWrapper, +}; + +/// Index of a function inside the supervisor. +/// +/// This is a typically an index in the default table of the supervisor, however +/// the exact meaning of this index is depends on the implementation of dispatch function. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct SupervisorFuncIndex(usize); + +impl From for usize { + fn from(index: SupervisorFuncIndex) -> Self { + index.0 + } +} + +/// Index of a function within guest index space. +/// +/// 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, +} + +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 { + 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, Vec), GuestFuncIndex>, + + /// Maps qualified field name to its memory reference + memories_map: HashMap<(Vec, Vec), Memory>, +} + +impl Imports { + fn func_by_name(&self, module_name: &str, func_name: &str) -> Option { + 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 { + self.memories_map + .get(&(module_name.as_bytes().to_owned(), memory_name.as_bytes().to_owned())) + .cloned() + } +} + +/// The sandbox context used to execute sandboxed functions. +pub trait SandboxContext { + /// Invoke a function in the supervisor environment. + /// + /// 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. + /// + /// # Errors + /// + /// Returns `Err` if the dispatch_thunk function has an incorrect signature or traps during + /// execution. + fn invoke( + &mut self, + invoke_args_ptr: Pointer, + invoke_args_len: WordSize, + state: u32, + func_idx: SupervisorFuncIndex, + ) -> Result; + + /// Returns the supervisor context. + fn supervisor_context(&mut self) -> &mut dyn FunctionContext; +} + +/// Implementation of [`Externals`] that allows execution of guest module with +/// [externals][`Externals`] that might refer functions defined by supervisor. +/// +/// [`Externals`]: ../wasmi/trait.Externals.html +pub struct GuestExternals<'a> { + /// Instance of sandboxed module to be dispatched + sandbox_instance: &'a SandboxInstance, + + /// External state passed to guest environment, see the `instantiate` function + state: u32, +} + +/// 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. +/// +/// All imports of this instance are specified at the creation time and +/// imports are implemented by the supervisor. +/// +/// Hence, in order to invoke an exported function on a sandboxed module instance, +/// it's required to provide supervisor externals: it will be used to execute +/// code in the supervisor context. +/// +/// This is generic over a supervisor function reference type. +/// +/// [`invoke`]: #method.invoke +pub struct SandboxInstance { + backend_instance: BackendInstance, + guest_to_supervisor_mapping: GuestToSupervisorFunctionMapping, +} + +impl SandboxInstance { + /// Invoke an exported function by a name. + /// + /// `supervisor_externals` is required to execute the implementations + /// of the syscalls that published to a sandboxed module instance. + /// + /// The `state` parameter can be used to provide custom data for + /// these syscall implementations. + pub fn invoke( + &self, + export_name: &str, + args: &[sp_wasm_interface::Value], + state: u32, + sandbox_context: &mut dyn SandboxContext, + ) -> std::result::Result, error::Error> { + match &self.backend_instance { + BackendInstance::Wasmi(wasmi_instance) => + wasmi_invoke(self, wasmi_instance, export_name, args, state, sandbox_context), + + #[cfg(feature = "wasmer-sandbox")] + BackendInstance::Wasmer(wasmer_instance) => + wasmer_invoke(wasmer_instance, export_name, args, state, sandbox_context), + } + } + + /// Get the value from a global with the given `name`. + /// + /// Returns `Some(_)` if the global could be found. + pub fn get_global_val(&self, name: &str) -> Option { + match &self.backend_instance { + BackendInstance::Wasmi(wasmi_instance) => wasmi_get_global(wasmi_instance, name), + + #[cfg(feature = "wasmer-sandbox")] + BackendInstance::Wasmer(wasmer_instance) => wasmer_get_global(wasmer_instance, name), + } + } +} + +/// Error occurred during instantiation of a sandboxed module. +pub enum InstantiationError { + /// Something wrong with the environment definition. It either can't + /// be decoded, have a reference to a non-existent or torn down memory instance. + EnvironmentDefinitionCorrupted, + /// Provided module isn't recognized as a valid webassembly binary. + ModuleDecoding, + /// Module is a well-formed webassembly binary but could not be instantiated. This could + /// happen because, e.g. the module imports entries not provided by the environment. + Instantiation, + /// Module is well-formed, instantiated and linked, but while executing the start function + /// a trap was generated. + StartTrapped, + /// The code was compiled with a CPU feature not available on the host. + CpuFeature, +} + +fn decode_environment_definition( + mut raw_env_def: &[u8], + memories: &[Option], +) -> std::result::Result<(Imports, GuestToSupervisorFunctionMapping), InstantiationError> { + let env_def = sandbox_env::EnvironmentDefinition::decode(&mut raw_env_def) + .map_err(|_| InstantiationError::EnvironmentDefinitionCorrupted)?; + + let mut func_map = HashMap::new(); + let mut memories_map = HashMap::new(); + let mut guest_to_supervisor_mapping = GuestToSupervisorFunctionMapping::new(); + + for entry in &env_def.entries { + let module = entry.module_name.clone(); + let field = entry.field_name.clone(); + + match entry.entity { + sandbox_env::ExternEntity::Function(func_idx) => { + let externals_idx = + guest_to_supervisor_mapping.define(SupervisorFuncIndex(func_idx as usize)); + func_map.insert((module, field), externals_idx); + }, + sandbox_env::ExternEntity::Memory(memory_idx) => { + let memory_ref = memories + .get(memory_idx as usize) + .cloned() + .ok_or(InstantiationError::EnvironmentDefinitionCorrupted)? + .ok_or(InstantiationError::EnvironmentDefinitionCorrupted)?; + memories_map.insert((module, field), memory_ref); + }, + } + } + + Ok((Imports { func_map, memories_map }, guest_to_supervisor_mapping)) +} + +/// 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, +} + +impl GuestEnvironment { + /// Decodes an environment definition from the given raw bytes. + /// + /// Returns `Err` if the definition cannot be decoded. + pub fn decode
( + store: &Store
, + raw_env_def: &[u8], + ) -> std::result::Result { + let (imports, guest_to_supervisor_mapping) = + decode_environment_definition(raw_env_def, &store.memories)?; + Ok(Self { imports, guest_to_supervisor_mapping }) + } +} + +/// An unregistered sandboxed instance. +/// +/// To finish off the instantiation the user must call `register`. +#[must_use] +pub struct UnregisteredInstance { + sandbox_instance: Rc, +} + +impl UnregisteredInstance { + /// Finalizes instantiation of this module. + pub fn register
(self, store: &mut Store
, dispatch_thunk: DT) -> u32 { + // At last, register the instance. + store.register_sandbox_instance(self.sandbox_instance, dispatch_thunk) + } +} + +/// 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 { + 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 { + match self { + Memory::Wasmer(memory) => Some(memory.clone()), + Memory::Wasmi(_) => None, + } + } +} + +impl util::MemoryTransfer for Memory { + fn read(&self, source_addr: Pointer, size: usize) -> Result> { + 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, 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, 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), + } + } +} + +/// 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 => + BackendContext::Wasmer(WasmerBackend::new()), + } + } +} + +/// This struct keeps track of all sandboxed components. +/// +/// This is generic over a supervisor function reference type. +pub struct Store
{ + /// Stores the instance and the dispatch thunk associated to per instance. + /// + /// Instances are `Some` until torn down. + instances: Vec, DT)>>, + /// Memories are `Some` until torn down. + memories: Vec>, + backend_context: BackendContext, +} + +impl Store
{ + /// Create a new empty sandbox store. + 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. + /// + /// # Errors + /// + /// 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 { + let memories = &mut self.memories; + let backend_context = &self.backend_context; + + let maximum = match maximum { + sandbox_env::MEM_UNLIMITED => None, + specified_limit => Some(specified_limit), + }; + + let memory = match &backend_context { + BackendContext::Wasmi => wasmi_new_memory(initial, maximum)?, + + #[cfg(feature = "wasmer-sandbox")] + BackendContext::Wasmer(context) => wasmer_new_memory(context, initial, maximum)?, + }; + + let mem_idx = memories.len(); + memories.push(Some(memory)); + + Ok(mem_idx as u32) + } + + /// Returns `SandboxInstance` by `instance_idx`. + /// + /// # Errors + /// + /// Returns `Err` If `instance_idx` isn't a valid index of an instance or + /// instance is already torndown. + pub fn instance(&self, instance_idx: u32) -> Result> { + self.instances + .get(instance_idx as usize) + .ok_or("Trying to access a non-existent instance")? + .as_ref() + .map(|v| v.0.clone()) + .ok_or_else(|| "Trying to access a torndown instance".into()) + } + + /// Returns dispatch thunk by `instance_idx`. + /// + /// # Errors + /// + /// Returns `Err` If `instance_idx` isn't a valid index of an instance or + /// instance is already torndown. + pub fn dispatch_thunk(&self, instance_idx: u32) -> Result
{ + self.instances + .get(instance_idx as usize) + .as_ref() + .ok_or("Trying to access a non-existent instance")? + .as_ref() + .map(|v| v.1.clone()) + .ok_or_else(|| "Trying to access a torndown instance".into()) + } + + /// Returns reference to a memory instance by `memory_idx`. + /// + /// # Errors + /// + /// 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 { + self.memories + .get(memory_idx as usize) + .cloned() + .ok_or("Trying to access a non-existent sandboxed memory")? + .ok_or_else(|| "Trying to access a torndown sandboxed memory".into()) + } + + /// Tear down the memory at the specified index. + /// + /// # Errors + /// + /// Returns `Err` if `memory_idx` isn't a valid index of an memory or + /// if it has been torn down. + pub fn memory_teardown(&mut self, memory_idx: u32) -> Result<()> { + match self.memories.get_mut(memory_idx as usize) { + None => Err("Trying to teardown a non-existent sandboxed memory".into()), + Some(None) => Err("Double teardown of a sandboxed memory".into()), + Some(memory) => { + *memory = None; + Ok(()) + }, + } + } + + /// Tear down the instance at the specified index. + /// + /// # Errors + /// + /// Returns `Err` if `instance_idx` isn't a valid index of an instance or + /// if it has been torn down. + pub fn instance_teardown(&mut self, instance_idx: u32) -> Result<()> { + match self.instances.get_mut(instance_idx as usize) { + None => Err("Trying to teardown a non-existent instance".into()), + Some(None) => Err("Double teardown of an instance".into()), + Some(instance) => { + *instance = None; + Ok(()) + }, + } + } + + /// 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( + &mut self, + wasm: &[u8], + guest_env: GuestEnvironment, + state: u32, + sandbox_context: &mut dyn SandboxContext, + ) -> std::result::Result { + let sandbox_instance = match self.backend_context { + BackendContext::Wasmi => wasmi_instantiate(wasm, guest_env, state, sandbox_context)?, + + #[cfg(feature = "wasmer-sandbox")] + BackendContext::Wasmer(ref context) => + wasmer_instantiate(context, wasm, guest_env, state, sandbox_context)?, + }; + + Ok(UnregisteredInstance { sandbox_instance }) + } +} + +// Private routines +impl
Store
{ + fn register_sandbox_instance( + &mut self, + sandbox_instance: Rc, + dispatch_thunk: DT, + ) -> u32 { + let instance_idx = self.instances.len(); + self.instances.push(Some((sandbox_instance, dispatch_thunk))); + instance_idx as u32 + } +} diff --git a/client/executor/common/src/sandbox/wasmer_backend.rs b/client/executor/common/src/sandbox/wasmer_backend.rs new file mode 100644 index 0000000000000..29926141ed8b8 --- /dev/null +++ b/client/executor/common/src/sandbox/wasmer_backend.rs @@ -0,0 +1,449 @@ +// 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 . + +//! Wasmer specific impls for sandbox + +use std::{cell::RefCell, collections::HashMap, rc::Rc}; + +use wasmer::RuntimeError; + +use codec::{Decode, Encode}; +use sp_sandbox::HostError; +use sp_wasm_interface::{FunctionContext, Pointer, ReturnValue, Value, WordSize}; + +use crate::{ + error::{Error, Result}, + sandbox::{ + BackendInstance, GuestEnvironment, InstantiationError, Memory, SandboxContext, + SandboxInstance, SupervisorFuncIndex, + }, + util::{checked_range, MemoryTransfer}, +}; + +environmental::environmental!(SandboxContextStore: trait SandboxContext); + +/// Wasmer specific context +pub struct Backend { + store: wasmer::Store, +} + +impl Backend { + pub fn new() -> Self { + let compiler = wasmer::Singlepass::default(); + Backend { store: wasmer::Store::new(&wasmer::Universal::new(compiler).engine()) } + } +} + +/// Invoke a function within a sandboxed module +pub fn invoke( + instance: &wasmer::Instance, + export_name: &str, + args: &[Value], + _state: u32, + sandbox_context: &mut dyn SandboxContext, +) -> std::result::Result, Error> { + let function = instance + .exports + .get_function(export_name) + .map_err(|error| Error::Sandbox(error.to_string()))?; + + let args: Vec = args + .iter() + .map(|v| match *v { + Value::I32(val) => wasmer::Val::I32(val), + Value::I64(val) => wasmer::Val::I64(val), + Value::F32(val) => wasmer::Val::F32(f32::from_bits(val)), + Value::F64(val) => wasmer::Val::F64(f64::from_bits(val)), + }) + .collect(); + + let wasmer_result = SandboxContextStore::using(sandbox_context, || { + function.call(&args).map_err(|error| Error::Sandbox(error.to_string())) + })?; + + match wasmer_result.as_ref() { + [] => Ok(None), + + [wasm_value] => { + let wasmer_value = match *wasm_value { + 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)), + _ => + return Err(Error::Sandbox(format!( + "Unsupported return value: {:?}", + wasm_value, + ))), + }; + + Ok(Some(wasmer_value)) + }, + + _ => Err(Error::Sandbox("multiple return types are not supported yet".into())), + } +} + +/// Instantiate a module within a sandbox context +pub fn instantiate( + context: &Backend, + wasm: &[u8], + guest_env: GuestEnvironment, + state: u32, + sandbox_context: &mut dyn SandboxContext, +) -> std::result::Result, InstantiationError> { + let module = wasmer::Module::new(&context.store, wasm) + .map_err(|_| InstantiationError::ModuleDecoding)?; + + type Exports = HashMap; + let mut exports_map = Exports::new(); + + for import in module.imports() { + 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_with(wasmer::Exports::new); + + let memory = guest_env + .imports + .memory_by_name(import.module(), import.name()) + .ok_or(InstantiationError::ModuleDecoding)?; + + let 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. + // + // We take exclusive lock to ensure that we're the only one here, + // since during instantiation phase the memory should only be created + // and not yet accessed. + let wasmer_memory = wasmer_memory_ref + .buffer + .try_borrow_mut() + .map_err(|_| InstantiationError::EnvironmentDefinitionCorrupted)? + .clone(); + + 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 = + dispatch_function(supervisor_func_index, &context.store, func_ty, state); + + let exports = exports_map + .entry(import.module().to_string()) + .or_insert_with(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 = SandboxContextStore::using(sandbox_context, || { + 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, + wasmer::InstantiationError::CpuFeature(_) => InstantiationError::CpuFeature, + }) + })?; + + Ok(Rc::new(SandboxInstance { + backend_instance: BackendInstance::Wasmer(instance), + guest_to_supervisor_mapping: guest_env.guest_to_supervisor_mapping, + })) +} + +fn dispatch_function( + supervisor_func_index: SupervisorFuncIndex, + store: &wasmer::Store, + func_ty: &wasmer::FunctionType, + state: u32, +) -> wasmer::Function { + wasmer::Function::new(store, func_ty, move |params| { + SandboxContextStore::with(|sandbox_context| { + // Serialize arguments into a byte vector. + let invoke_args_data = params + .iter() + .map(|val| match val { + wasmer::Val::I32(val) => Ok(Value::I32(*val)), + wasmer::Val::I64(val) => Ok(Value::I64(*val)), + wasmer::Val::F32(val) => Ok(Value::F32(f32::to_bits(*val))), + wasmer::Val::F64(val) => Ok(Value::F64(f64::to_bits(*val))), + _ => + Err(RuntimeError::new(format!("Unsupported function argument: {:?}", val))), + }) + .collect::, _>>()? + .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 = + sandbox_context.supervisor_context().allocate_memory(invoke_args_len).map_err( + |_| RuntimeError::new("Can't allocate memory in supervisor for the arguments"), + )?; + + let deallocate = |fe: &mut dyn FunctionContext, ptr, fail_msg| { + fe.deallocate_memory(ptr).map_err(|_| RuntimeError::new(fail_msg)) + }; + + if sandbox_context + .supervisor_context() + .write_memory(invoke_args_ptr, &invoke_args_data) + .is_err() + { + deallocate( + sandbox_context.supervisor_context(), + invoke_args_ptr, + "Failed dealloction after failed write of invoke arguments", + )?; + + return Err(RuntimeError::new("Can't write invoke args into memory")) + } + + // Perform the actuall call + let serialized_result = sandbox_context + .invoke(invoke_args_ptr, invoke_args_len, state, supervisor_func_index) + .map_err(|e| RuntimeError::new(e.to_string())); + + deallocate( + sandbox_context.supervisor_context(), + invoke_args_ptr, + "Failed dealloction after invoke", + )?; + + let serialized_result = serialized_result?; + + // 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 = sandbox_context + .supervisor_context() + .read_memory(serialized_result_val_ptr, serialized_result_val_len) + .map_err(|_| { + RuntimeError::new("Can't read the serialized result from dispatch thunk") + }); + + deallocate( + sandbox_context.supervisor_context(), + serialized_result_val_ptr, + "Can't deallocate memory for dispatch thunk's result", + )?; + + let serialized_result_val = serialized_result_val?; + + let deserialized_result = std::result::Result::::decode( + &mut serialized_result_val.as_slice(), + ) + .map_err(|_| RuntimeError::new("Decoding Result failed!"))? + .map_err(|_| RuntimeError::new("Supervisor function returned sandbox::HostError"))?; + + let result = match deserialized_result { + ReturnValue::Value(Value::I32(val)) => vec![wasmer::Val::I32(val)], + ReturnValue::Value(Value::I64(val)) => vec![wasmer::Val::I64(val)], + ReturnValue::Value(Value::F32(val)) => vec![wasmer::Val::F32(f32::from_bits(val))], + ReturnValue::Value(Value::F64(val)) => vec![wasmer::Val::F64(f64::from_bits(val))], + + ReturnValue::Unit => vec![], + }; + + Ok(result) + }) + .expect("SandboxContextStore is set when invoking sandboxed functions; qed") + }) +} + +/// Allocate new memory region +pub fn new_memory( + context: &Backend, + initial: u32, + maximum: Option, +) -> crate::error::Result { + let ty = wasmer::MemoryType::new(initial, maximum, false); + let memory = Memory::Wasmer(MemoryWrapper::new( + wasmer::Memory::new(&context.store, ty).map_err(|_| Error::InvalidMemoryReference)?, + )); + + Ok(memory) +} + +/// 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>, +} + +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( + "maximum memory object size never exceeds pointer size on any architecture; \ + usize by design and definition is enough to store any memory object size \ + possible on current achitecture; thus the conversion can not fail; qed", + ); + + 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: &mut wasmer::Memory) -> &mut [u8] { + let ptr = memory.data_ptr(); + + let len: usize = memory.data_size().try_into().expect( + "maximum memory object size never exceeds pointer size on any architecture; \ + usize by design and definition is enough to store any memory object size \ + possible on current achitecture; thus the conversion can not fail; qed", + ); + + if len == 0 { + &mut [] + } else { + core::slice::from_raw_parts_mut(ptr, len) + } + } +} + +impl MemoryTransfer for MemoryWrapper { + fn read(&self, source_addr: Pointer, size: usize) -> Result> { + let memory = self.buffer.borrow(); + + let data_size: usize = memory.data_size().try_into().expect( + "maximum memory object size never exceeds pointer size on any architecture; \ + usize by design and definition is enough to store any memory object size \ + possible on current achitecture; thus the conversion can not fail; qed", + ); + + 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, 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, source: &[u8]) -> Result<()> { + unsafe { + let memory = &mut 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()))?; + + destination[range].copy_from_slice(source); + Ok(()) + } + } +} + +/// Get global value by name +pub fn get_global(instance: &wasmer::Instance, name: &str) -> Option { + let global = 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) +} diff --git a/client/executor/common/src/sandbox/wasmi_backend.rs b/client/executor/common/src/sandbox/wasmi_backend.rs new file mode 100644 index 0000000000000..2ba133f5f15b1 --- /dev/null +++ b/client/executor/common/src/sandbox/wasmi_backend.rs @@ -0,0 +1,339 @@ +// 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 . + +//! Wasmi specific impls for sandbox + +use std::{fmt, rc::Rc}; + +use codec::{Decode, Encode}; +use sp_sandbox::HostError; +use sp_wasm_interface::{FunctionContext, Pointer, ReturnValue, Value, WordSize}; +use wasmi::{ + memory_units::Pages, ImportResolver, MemoryInstance, Module, ModuleInstance, RuntimeArgs, + RuntimeValue, Trap, +}; + +use crate::{ + error::{self, Error}, + sandbox::{ + BackendInstance, GuestEnvironment, GuestExternals, GuestFuncIndex, Imports, + InstantiationError, Memory, SandboxContext, SandboxInstance, + }, + util::{checked_range, MemoryTransfer}, +}; + +environmental::environmental!(SandboxContextStore: trait SandboxContext); + +#[derive(Debug)] +struct CustomHostError(String); + +impl fmt::Display for CustomHostError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "HostError: {}", self.0) + } +} + +impl wasmi::HostError for CustomHostError {} + +/// Construct trap error from specified message +fn trap(msg: &'static str) -> Trap { + Trap::host(CustomHostError(msg.into())) +} + +impl ImportResolver for Imports { + fn resolve_func( + &self, + module_name: &str, + field_name: &str, + signature: &wasmi::Signature, + ) -> std::result::Result { + 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)) + } + + fn resolve_memory( + &self, + module_name: &str, + field_name: &str, + _memory_type: &wasmi::MemoryDescriptor, + ) -> std::result::Result { + 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. All subsequent memory accesses + // should happen through the wrapper, that enforces the memory access protocol. + let mem = wrapper.0; + + Ok(mem) + } + + fn resolve_global( + &self, + module_name: &str, + field_name: &str, + _global_type: &wasmi::GlobalDescriptor, + ) -> std::result::Result { + Err(wasmi::Error::Instantiation(format!("Export {}:{} not found", module_name, field_name))) + } + + fn resolve_table( + &self, + module_name: &str, + field_name: &str, + _table_type: &wasmi::TableDescriptor, + ) -> std::result::Result { + Err(wasmi::Error::Instantiation(format!("Export {}:{} not found", module_name, field_name))) + } +} + +/// Allocate new memory region +pub fn new_memory(initial: u32, maximum: Option) -> crate::error::Result { + let memory = Memory::Wasmi(MemoryWrapper::new( + MemoryInstance::alloc(Pages(initial as usize), maximum.map(|m| Pages(m as usize))) + .map_err(|error| Error::Sandbox(error.to_string()))?, + )); + + Ok(memory) +} + +/// 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 + fn new(memory: wasmi::MemoryRef) -> Self { + Self(memory) + } +} + +impl MemoryTransfer for MemoryWrapper { + fn read(&self, source_addr: Pointer, size: usize) -> error::Result> { + self.0.with_direct_access(|source| { + let range = checked_range(source_addr.into(), size, source.len()) + .ok_or_else(|| error::Error::Other("memory read is out of bounds".into()))?; + + Ok(Vec::from(&source[range])) + }) + } + + fn read_into(&self, source_addr: Pointer, destination: &mut [u8]) -> error::Result<()> { + self.0.with_direct_access(|source| { + let range = checked_range(source_addr.into(), destination.len(), source.len()) + .ok_or_else(|| error::Error::Other("memory read is out of bounds".into()))?; + + destination.copy_from_slice(&source[range]); + Ok(()) + }) + } + + fn write_from(&self, dest_addr: Pointer, source: &[u8]) -> error::Result<()> { + self.0.with_direct_access_mut(|destination| { + let range = checked_range(dest_addr.into(), source.len(), destination.len()) + .ok_or_else(|| error::Error::Other("memory write is out of bounds".into()))?; + + destination[range].copy_from_slice(source); + Ok(()) + }) + } +} + +impl<'a> wasmi::Externals for GuestExternals<'a> { + fn invoke_index( + &mut self, + index: usize, + args: RuntimeArgs, + ) -> std::result::Result, Trap> { + SandboxContextStore::with(|sandbox_context| { + // 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 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. + let invoke_args_data: Vec = args + .as_ref() + .iter() + .cloned() + .map(sp_wasm_interface::Value::from) + .collect::>() + .encode(); + + let state = self.state; + + // 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 = sandbox_context + .supervisor_context() + .allocate_memory(invoke_args_len) + .map_err(|_| trap("Can't allocate memory in supervisor for the arguments"))?; + + let deallocate = |supervisor_context: &mut dyn FunctionContext, ptr, fail_msg| { + supervisor_context.deallocate_memory(ptr).map_err(|_| trap(fail_msg)) + }; + + if sandbox_context + .supervisor_context() + .write_memory(invoke_args_ptr, &invoke_args_data) + .is_err() + { + deallocate( + sandbox_context.supervisor_context(), + invoke_args_ptr, + "Failed dealloction after failed write of invoke arguments", + )?; + return Err(trap("Can't write invoke args into memory")) + } + + let result = sandbox_context.invoke( + invoke_args_ptr, + invoke_args_len, + state, + func_idx, + ); + + deallocate( + sandbox_context.supervisor_context(), + invoke_args_ptr, + "Can't deallocate memory for dispatch thunk's invoke arguments", + )?; + let result = result?; + + // 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 = 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 = sandbox_context + .supervisor_context() + .read_memory(serialized_result_val_ptr, serialized_result_val_len) + .map_err(|_| trap("Can't read the serialized result from dispatch thunk")); + + deallocate( + sandbox_context.supervisor_context(), + serialized_result_val_ptr, + "Can't deallocate memory for dispatch thunk's result", + ) + .and(serialized_result_val) + .and_then(|serialized_result_val| { + let result_val = std::result::Result::::decode(&mut serialized_result_val.as_slice()) + .map_err(|_| trap("Decoding Result failed!"))?; + + match result_val { + Ok(return_value) => Ok(match return_value { + ReturnValue::Unit => None, + ReturnValue::Value(typed_value) => Some(RuntimeValue::from(typed_value)), + }), + Err(HostError) => Err(trap("Supervisor function returned sandbox::HostError")), + } + }) + }).expect("SandboxContextStore is set when invoking sandboxed functions; qed") + } +} + +fn with_guest_externals(sandbox_instance: &SandboxInstance, state: u32, f: F) -> R +where + F: FnOnce(&mut GuestExternals) -> R, +{ + f(&mut GuestExternals { sandbox_instance, state }) +} + +/// Instantiate a module within a sandbox context +pub fn instantiate( + wasm: &[u8], + guest_env: GuestEnvironment, + state: u32, + sandbox_context: &mut dyn SandboxContext, +) -> std::result::Result, InstantiationError> { + 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 = 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()), + guest_to_supervisor_mapping: guest_env.guest_to_supervisor_mapping, + }); + + with_guest_externals(&sandbox_instance, state, |guest_externals| { + SandboxContextStore::using(sandbox_context, || { + wasmi_instance + .run_start(guest_externals) + .map_err(|_| InstantiationError::StartTrapped) + }) + })?; + + Ok(sandbox_instance) +} + +/// Invoke a function within a sandboxed module +pub fn invoke( + instance: &SandboxInstance, + module: &wasmi::ModuleRef, + export_name: &str, + args: &[Value], + state: u32, + sandbox_context: &mut dyn SandboxContext, +) -> std::result::Result, error::Error> { + with_guest_externals(instance, state, |guest_externals| { + SandboxContextStore::using(sandbox_context, || { + let args = args.iter().cloned().map(Into::into).collect::>(); + + module + .invoke_export(export_name, &args, guest_externals) + .map(|result| result.map(Into::into)) + .map_err(|error| error::Error::Sandbox(error.to_string())) + }) + }) +} + +/// Get global value by name +pub fn get_global(instance: &wasmi::ModuleRef, name: &str) -> Option { + Some(instance.export_by_name(name)?.as_global()?.get().into()) +} diff --git a/client/executor/runtime-test/Cargo.toml b/client/executor/runtime-test/Cargo.toml index e99f3caa9447e..dae1917c83f16 100644 --- a/client/executor/runtime-test/Cargo.toml +++ b/client/executor/runtime-test/Cargo.toml @@ -13,9 +13,11 @@ repository = "https://github.com/paritytech/substrate/" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +paste = "1.0.6" sp-core = { version = "7.0.0", default-features = false, path = "../../../primitives/core" } sp-io = { version = "7.0.0", default-features = false, features = ["improved_panic_error_reporting"], path = "../../../primitives/io" } sp-runtime = { version = "7.0.0", default-features = false, path = "../../../primitives/runtime" } +sp-sandbox = { version = "0.10.0-dev", default-features = false, path = "../../../primitives/sandbox" } sp-std = { version = "5.0.0", default-features = false, path = "../../../primitives/std" } [build-dependencies] @@ -27,6 +29,7 @@ std = [ "sp-core/std", "sp-io/std", "sp-runtime/std", + "sp-sandbox/std", "sp-std/std", "substrate-wasm-builder", ] diff --git a/client/executor/runtime-test/src/lib.rs b/client/executor/runtime-test/src/lib.rs index fc98d1909d00d..0424ad418617b 100644 --- a/client/executor/runtime-test/src/lib.rs +++ b/client/executor/runtime-test/src/lib.rs @@ -29,6 +29,8 @@ use sp_runtime::{ print, traits::{BlakeTwo256, Hash}, }; +#[cfg(not(feature = "std"))] +use sp_sandbox::{SandboxEnvironmentBuilder, SandboxInstance, SandboxMemory, Value}; extern "C" { #[allow(dead_code)] @@ -337,3 +339,160 @@ sp_core::wasm_export_functions! { return 1234; } } + +/// A macro to define a test entrypoint for each available sandbox executor. +macro_rules! wasm_export_sandbox_test_functions { + ( + $( + fn $name:ident( + $( $arg_name:ident: $arg_ty:ty ),* $(,)? + ) $( -> $ret_ty:ty )? where T: SandboxInstance<$state:ty> $(,)? + { $( $fn_impl:tt )* } + )* + ) => { + $( + #[cfg(not(feature = "std"))] + fn $name( $($arg_name: $arg_ty),* ) $( -> $ret_ty )? where T: SandboxInstance<$state> { + $( $fn_impl )* + } + + paste::paste! { + sp_core::wasm_export_functions! { + fn [<$name _host>]( $($arg_name: $arg_ty),* ) $( -> $ret_ty )? { + $name::>( $( $arg_name ),* ) + } + + fn [<$name _embedded>]( $($arg_name: $arg_ty),* ) $( -> $ret_ty )? { + $name::>( $( $arg_name ),* ) + } + } + } + )* + }; +} + +wasm_export_sandbox_test_functions! { + fn test_sandbox(code: Vec) -> bool + where + T: SandboxInstance, + { + execute_sandboxed::(&code, &[]).is_ok() + } + + fn test_sandbox_args(code: Vec) -> bool + where + T: SandboxInstance, + { + execute_sandboxed::(&code, &[Value::I32(0x12345678), Value::I64(0x1234567887654321)]) + .is_ok() + } + + fn test_sandbox_return_val(code: Vec) -> bool + where + T: SandboxInstance, + { + let ok = match execute_sandboxed::(&code, &[Value::I32(0x1336)]) { + Ok(sp_sandbox::ReturnValue::Value(Value::I32(0x1337))) => true, + _ => false, + }; + + ok + } + + fn test_sandbox_instantiate(code: Vec) -> u8 + where + T: SandboxInstance<()>, + { + let env_builder = T::EnvironmentBuilder::new(); + let code = match T::new(&code, &env_builder, &mut ()) { + Ok(_) => 0, + Err(sp_sandbox::Error::Module) => 1, + Err(sp_sandbox::Error::Execution) => 2, + Err(sp_sandbox::Error::OutOfBounds) => 3, + }; + + code + } + + fn test_sandbox_get_global_val(code: Vec) -> i64 + where + T: SandboxInstance<()>, + { + let env_builder = T::EnvironmentBuilder::new(); + let instance = if let Ok(i) = T::new(&code, &env_builder, &mut ()) { + i + } else { + return 20 + }; + + match instance.get_global_val("test_global") { + Some(sp_sandbox::Value::I64(val)) => val, + None => 30, + _ => 40, + } + } +} + +#[cfg(not(feature = "std"))] +struct State { + counter: u32, +} + +#[cfg(not(feature = "std"))] +fn execute_sandboxed( + code: &[u8], + args: &[Value], +) -> Result +where + T: sp_sandbox::SandboxInstance, +{ + fn env_assert( + _e: &mut State, + args: &[Value], + ) -> Result { + if args.len() != 1 { + return Err(sp_sandbox::HostError) + } + let condition = args[0].as_i32().ok_or_else(|| sp_sandbox::HostError)?; + if condition != 0 { + Ok(sp_sandbox::ReturnValue::Unit) + } else { + Err(sp_sandbox::HostError) + } + } + fn env_inc_counter( + e: &mut State, + args: &[Value], + ) -> Result { + if args.len() != 1 { + return Err(sp_sandbox::HostError) + } + let inc_by = args[0].as_i32().ok_or_else(|| sp_sandbox::HostError)?; + e.counter += inc_by as u32; + Ok(sp_sandbox::ReturnValue::Value(Value::I32(e.counter as i32))) + } + + let mut state = State { counter: 0 }; + + let env_builder = { + let mut env_builder = T::EnvironmentBuilder::new(); + env_builder.add_host_func("env", "assert", env_assert); + env_builder.add_host_func("env", "inc_counter", env_inc_counter); + let memory = match T::Memory::new(1, Some(16)) { + Ok(m) => m, + Err(_) => unreachable!( + " + Memory::new() can return Err only if parameters are borked; \ + We passing params here explicitly and they're correct; \ + Memory::new() can't return a Error qed" + ), + }; + env_builder.add_memory("env", "memory", memory); + env_builder + }; + + let mut instance = T::new(code, &env_builder, &mut state)?; + let result = instance.invoke("call", args, &mut state); + + result.map_err(|_| sp_sandbox::HostError) +} diff --git a/client/executor/src/integration_tests/mod.rs b/client/executor/src/integration_tests/mod.rs index 25b999f115363..9b5c4b12fca99 100644 --- a/client/executor/src/integration_tests/mod.rs +++ b/client/executor/src/integration_tests/mod.rs @@ -18,6 +18,7 @@ #[cfg(target_os = "linux")] mod linux; +mod sandbox; use codec::{Decode, Encode}; use sc_executor_common::{error::Error, runtime_blob::RuntimeBlob, wasm_runtime::WasmModule}; @@ -97,6 +98,111 @@ macro_rules! test_wasm_execution { }; } +/// A macro to run a given test for each available WASM execution method *and* for each +/// sandbox execution method. +#[macro_export] +macro_rules! test_wasm_execution_sandbox { + ($method_name:ident) => { + paste::item! { + #[test] + fn [<$method_name _interpreted_host_executor>]() { + $method_name(WasmExecutionMethod::Interpreted, "_host"); + } + + #[test] + fn [<$method_name _interpreted_embedded_executor>]() { + $method_name(WasmExecutionMethod::Interpreted, "_embedded"); + } + + #[test] + fn [<$method_name _compiled_pooling_cow_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite + }, "_host"); + } + + #[test] + fn [<$method_name _compiled_pooling_cow_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::PoolingCopyOnWrite + }, "_embedded"); + } + + #[test] + fn [<$method_name _compiled_pooling_vanilla_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::Pooling + }, "_host"); + } + + #[test] + fn [<$method_name _compiled_pooling_vanilla_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::Pooling + }, "_embedded"); + } + + #[test] + fn [<$method_name _compiled_recreate_instance_cow_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstanceCopyOnWrite + }, "_host"); + } + + #[test] + fn [<$method_name _compiled_recreate_instance_cow_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstanceCopyOnWrite + }, "_embedded"); + } + + #[test] + fn [<$method_name _compiled_recreate_instance_vanilla_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstance + }, "_host"); + } + + #[test] + fn [<$method_name _compiled_recreate_instance_vanilla_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::RecreateInstance + }, "_embedded"); + } + + #[test] + fn [<$method_name _compiled_legacy_instance_reuse_host_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse + }, "_host"); + } + + #[test] + fn [<$method_name _compiled_legacy_instance_reuse_embedded_executor>]() { + $method_name(WasmExecutionMethod::Compiled { + instantiation_strategy: sc_executor_wasmtime::InstantiationStrategy::LegacyInstanceReuse + }, "_embedded"); + } + } + }; + + (interpreted_only $method_name:ident) => { + paste::item! { + #[test] + fn [<$method_name _interpreted_host_executor>]() { + $method_name(WasmExecutionMethod::Interpreted, "_host"); + } + } + + paste::item! { + #[test] + fn [<$method_name _interpreted_embedded_executor>]() { + $method_name(WasmExecutionMethod::Interpreted, "_embedded"); + } + } + }; +} + fn call_in_wasm( function: &str, call_data: &[u8], diff --git a/client/executor/src/integration_tests/sandbox.rs b/client/executor/src/integration_tests/sandbox.rs new file mode 100644 index 0000000000000..643db5097c6ad --- /dev/null +++ b/client/executor/src/integration_tests/sandbox.rs @@ -0,0 +1,339 @@ +// This file is part of Substrate. + +// Copyright (C) 2018-2022 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 . + +use super::{call_in_wasm, TestExternalities}; +use crate::{test_wasm_execution_sandbox, WasmExecutionMethod}; + +use codec::Encode; + +test_wasm_execution_sandbox!(sandbox_should_work); +fn sandbox_should_work(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (import "env" "assert" (func $assert (param i32))) + (import "env" "inc_counter" (func $inc_counter (param i32) (result i32))) + (func (export "call") + (drop + (call $inc_counter (i32.const 5)) + ) + + (call $inc_counter (i32.const 3)) + ;; current counter value is on the stack + + ;; check whether current == 8 + i32.const 8 + i32.eq + + call $assert + ) + ) + "#, + ) + .unwrap() + .encode(); + + assert_eq!( + call_in_wasm(&format!("test_sandbox{}", fn_suffix), &code, wasm_method, &mut ext).unwrap(), + true.encode() + ); +} + +test_wasm_execution_sandbox!(sandbox_trap); +fn sandbox_trap(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (import "env" "assert" (func $assert (param i32))) + (func (export "call") + i32.const 0 + call $assert + ) + ) + "#, + ) + .unwrap(); + + assert_eq!( + call_in_wasm(&format!("test_sandbox{}", fn_suffix), &code, wasm_method, &mut ext).unwrap(), + vec![0] + ); +} + +test_wasm_execution_sandbox!(start_called); +fn start_called(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (import "env" "assert" (func $assert (param i32))) + (import "env" "inc_counter" (func $inc_counter (param i32) (result i32))) + + ;; Start function + (start $start) + (func $start + ;; Increment counter by 1 + (drop + (call $inc_counter (i32.const 1)) + ) + ) + + (func (export "call") + ;; Increment counter by 1. The current value is placed on the stack. + (call $inc_counter (i32.const 1)) + + ;; Counter is incremented twice by 1, once there and once in `start` func. + ;; So check the returned value is equal to 2. + i32.const 2 + i32.eq + call $assert + ) + ) + "#, + ) + .unwrap() + .encode(); + + assert_eq!( + call_in_wasm(&format!("test_sandbox{}", fn_suffix), &code, wasm_method, &mut ext).unwrap(), + true.encode() + ); +} + +test_wasm_execution_sandbox!(invoke_args); +fn invoke_args(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (import "env" "assert" (func $assert (param i32))) + + (func (export "call") (param $x i32) (param $y i64) + ;; assert that $x = 0x12345678 + (call $assert + (i32.eq + (get_local $x) + (i32.const 0x12345678) + ) + ) + + (call $assert + (i64.eq + (get_local $y) + (i64.const 0x1234567887654321) + ) + ) + ) + ) + "#, + ) + .unwrap() + .encode(); + + assert_eq!( + call_in_wasm(&format!("test_sandbox_args{}", fn_suffix), &code, wasm_method, &mut ext,) + .unwrap(), + true.encode(), + ); +} + +test_wasm_execution_sandbox!(return_val); +fn return_val(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (func (export "call") (param $x i32) (result i32) + (i32.add + (get_local $x) + (i32.const 1) + ) + ) + ) + "#, + ) + .unwrap() + .encode(); + + assert_eq!( + call_in_wasm( + &format!("test_sandbox_return_val{}", fn_suffix), + &code, + wasm_method, + &mut ext, + ) + .unwrap(), + true.encode(), + ); +} + +test_wasm_execution_sandbox!(unlinkable_module); +fn unlinkable_module(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (import "env" "non-existent" (func)) + + (func (export "call") + ) + ) + "#, + ) + .unwrap() + .encode(); + + assert_eq!( + call_in_wasm( + &format!("test_sandbox_instantiate{}", fn_suffix), + &code, + wasm_method, + &mut ext, + ) + .unwrap(), + 1u8.encode(), + ); +} + +test_wasm_execution_sandbox!(corrupted_module); +fn corrupted_module(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + // Corrupted wasm file + let code = vec![0u8, 0, 0, 0, 1, 0, 0, 0].encode(); + + assert_eq!( + call_in_wasm( + &format!("test_sandbox_instantiate{}", fn_suffix), + &code, + wasm_method, + &mut ext, + ) + .unwrap(), + 1u8.encode(), + ); +} + +test_wasm_execution_sandbox!(start_fn_ok); +fn start_fn_ok(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (func (export "call") + ) + + (func $start + ) + + (start $start) + ) + "#, + ) + .unwrap() + .encode(); + + assert_eq!( + call_in_wasm( + &format!("test_sandbox_instantiate{}", fn_suffix), + &code, + wasm_method, + &mut ext, + ) + .unwrap(), + 0u8.encode(), + ); +} + +test_wasm_execution_sandbox!(start_fn_traps); +fn start_fn_traps(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (func (export "call") + ) + + (func $start + unreachable + ) + + (start $start) + ) + "#, + ) + .unwrap() + .encode(); + + assert_eq!( + call_in_wasm( + &format!("test_sandbox_instantiate{}", fn_suffix), + &code, + wasm_method, + &mut ext, + ) + .unwrap(), + 2u8.encode(), + ); +} + +test_wasm_execution_sandbox!(get_global_val_works); +fn get_global_val_works(wasm_method: WasmExecutionMethod, fn_suffix: &str) { + let mut ext = TestExternalities::default(); + let mut ext = ext.ext(); + + let code = wat::parse_str( + r#" + (module + (global (export "test_global") i64 (i64.const 500)) + ) + "#, + ) + .unwrap() + .encode(); + + assert_eq!( + call_in_wasm( + &format!("test_sandbox_get_global_val{}", fn_suffix), + &code, + wasm_method, + &mut ext, + ) + .unwrap(), + 500i64.encode(), + ); +} diff --git a/client/executor/src/lib.rs b/client/executor/src/lib.rs index 0670b840949d7..1fb041c358fb1 100644 --- a/client/executor/src/lib.rs +++ b/client/executor/src/lib.rs @@ -49,7 +49,7 @@ pub use sp_wasm_interface; pub use wasm_runtime::{read_embedded_version, WasmExecutionMethod}; pub use wasmi; -pub use sc_executor_common::error; +pub use sc_executor_common::{error, sandbox}; pub use sc_executor_wasmtime::InstantiationStrategy as WasmtimeInstantiationStrategy; /// Extracts the runtime version of a given runtime code. diff --git a/client/executor/wasmi/Cargo.toml b/client/executor/wasmi/Cargo.toml index 4235440023c89..ef01f3784154d 100644 --- a/client/executor/wasmi/Cargo.toml +++ b/client/executor/wasmi/Cargo.toml @@ -14,9 +14,11 @@ readme = "README.md" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0" } log = "0.4.17" wasmi = "0.13" sc-allocator = { version = "4.1.0-dev", path = "../../allocator" } sc-executor-common = { version = "0.10.0-dev", path = "../common" } sp-runtime-interface = { version = "7.0.0", path = "../../../primitives/runtime-interface" } +sp-sandbox = { version = "0.10.0-dev", path = "../../../primitives/sandbox" } sp-wasm-interface = { version = "7.0.0", path = "../../../primitives/wasm-interface" } diff --git a/client/executor/wasmi/src/lib.rs b/client/executor/wasmi/src/lib.rs index 6eb38146946b8..1284cc23e4c96 100644 --- a/client/executor/wasmi/src/lib.rs +++ b/client/executor/wasmi/src/lib.rs @@ -18,7 +18,7 @@ //! This crate provides an implementation of `WasmModule` that is baked by wasmi. -use std::{cell::RefCell, str, sync::Arc}; +use std::{cell::RefCell, rc::Rc, str, sync::Arc}; use log::{debug, error, trace}; use wasmi::{ @@ -28,18 +28,26 @@ use wasmi::{ TableRef, }; +use codec::{Decode, Encode}; use sc_allocator::AllocationStats; use sc_executor_common::{ error::{Error, MessageWithBacktrace, WasmError}, runtime_blob::{DataSegmentsSnapshot, RuntimeBlob}, + sandbox, + util::MemoryTransfer, wasm_runtime::{InvokeMethod, WasmInstance, WasmModule}, }; use sp_runtime_interface::unpack_ptr_and_len; -use sp_wasm_interface::{Function, FunctionContext, Pointer, Result as WResult, WordSize}; +use sp_sandbox::env as sandbox_env; +use sp_wasm_interface::{ + Function, FunctionContext, MemoryId, Pointer, Result as WResult, Sandbox, WordSize, +}; struct FunctionExecutor { + sandbox_store: Rc>>, heap: RefCell, memory: MemoryRef, + table: Option, host_functions: Arc>, allow_missing_func_imports: bool, missing_functions: Arc>, @@ -50,13 +58,18 @@ impl FunctionExecutor { fn new( m: MemoryRef, heap_base: u32, + t: Option, host_functions: Arc>, allow_missing_func_imports: bool, missing_functions: Arc>, ) -> Result { Ok(FunctionExecutor { + sandbox_store: Rc::new(RefCell::new(sandbox::Store::new( + sandbox::SandboxBackend::Wasmi, + ))), heap: RefCell::new(sc_allocator::FreeingBumpHeapAllocator::new(heap_base)), memory: m, + table: t, host_functions, allow_missing_func_imports, missing_functions, @@ -65,6 +78,42 @@ impl FunctionExecutor { } } +struct SandboxContext<'a> { + executor: &'a mut FunctionExecutor, + dispatch_thunk: wasmi::FuncRef, +} + +impl<'a> sandbox::SandboxContext for SandboxContext<'a> { + fn invoke( + &mut self, + invoke_args_ptr: Pointer, + invoke_args_len: WordSize, + state: u32, + func_idx: sandbox::SupervisorFuncIndex, + ) -> Result { + let result = wasmi::FuncInstance::invoke( + &self.dispatch_thunk, + &[ + RuntimeValue::I32(u32::from(invoke_args_ptr) as i32), + RuntimeValue::I32(invoke_args_len as i32), + RuntimeValue::I32(state as i32), + RuntimeValue::I32(usize::from(func_idx) as i32), + ], + self.executor, + ); + + match result { + Ok(Some(RuntimeValue::I64(val))) => Ok(val), + Ok(_) => Err("Supervisor function returned unexpected result!".into()), + Err(err) => Err(Error::Sandbox(err.to_string())), + } + } + + fn supervisor_context(&mut self) -> &mut dyn FunctionContext { + self.executor + } +} + impl FunctionContext for FunctionExecutor { fn read_memory_into(&self, address: Pointer, dest: &mut [u8]) -> WResult<()> { self.memory.get_into(address.into(), dest).map_err(|e| e.to_string()) @@ -86,11 +135,189 @@ impl FunctionContext for FunctionExecutor { .with_direct_access_mut(|mem| heap.deallocate(mem, ptr).map_err(|e| e.to_string())) } + fn sandbox(&mut self) -> &mut dyn Sandbox { + self + } + fn register_panic_error_message(&mut self, message: &str) { self.panic_message = Some(message.to_owned()); } } +impl Sandbox for FunctionExecutor { + fn memory_get( + &mut self, + memory_id: MemoryId, + offset: WordSize, + buf_ptr: Pointer, + buf_len: WordSize, + ) -> WResult { + let sandboxed_memory = + self.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_env::ERR_OUT_OF_BOUNDS), + Ok(buffer) => buffer, + }; + + if self.memory.set(buf_ptr.into(), &buffer).is_err() { + return Ok(sandbox_env::ERR_OUT_OF_BOUNDS) + } + + Ok(sandbox_env::ERR_OK) + } + + fn memory_set( + &mut self, + memory_id: MemoryId, + offset: WordSize, + val_ptr: Pointer, + val_len: WordSize, + ) -> WResult { + let sandboxed_memory = + self.sandbox_store.borrow().memory(memory_id).map_err(|e| e.to_string())?; + + let len = val_len as usize; + + #[allow(deprecated)] + let buffer = match self.memory.get(val_ptr.into(), len) { + Err(_) => return Ok(sandbox_env::ERR_OUT_OF_BOUNDS), + Ok(buffer) => buffer, + }; + + if sandboxed_memory.write_from(Pointer::new(offset as u32), &buffer).is_err() { + return Ok(sandbox_env::ERR_OUT_OF_BOUNDS) + } + + Ok(sandbox_env::ERR_OK) + } + + fn memory_teardown(&mut self, memory_id: MemoryId) -> WResult<()> { + self.sandbox_store + .borrow_mut() + .memory_teardown(memory_id) + .map_err(|e| e.to_string()) + } + + fn memory_new(&mut self, initial: u32, maximum: u32) -> WResult { + self.sandbox_store + .borrow_mut() + .new_memory(initial, maximum) + .map_err(|e| e.to_string()) + } + + fn invoke( + &mut self, + instance_id: u32, + export_name: &str, + mut args: &[u8], + return_val: Pointer, + return_val_len: WordSize, + state: u32, + ) -> WResult { + trace!(target: "sp-sandbox", "invoke, instance_idx={}", instance_id); + + // Deserialize arguments and convert them into wasmi types. + let args = Vec::::decode(&mut args) + .map_err(|_| "Can't decode serialized arguments for the invocation")? + .into_iter() + .collect::>(); + + let instance = + self.sandbox_store.borrow().instance(instance_id).map_err(|e| e.to_string())?; + + let dispatch_thunk = self + .sandbox_store + .borrow() + .dispatch_thunk(instance_id) + .map_err(|e| e.to_string())?; + + match instance.invoke( + export_name, + &args, + state, + &mut SandboxContext { dispatch_thunk, executor: self }, + ) { + Ok(None) => Ok(sandbox_env::ERR_OK), + Ok(Some(val)) => { + // Serialize return value and write it back into the memory. + sp_wasm_interface::ReturnValue::Value(val).using_encoded(|val| { + if val.len() > return_val_len as usize { + return Err("Return value buffer is too small".into()) + } + self.write_memory(return_val, val).map_err(|_| "Return value buffer is OOB")?; + Ok(sandbox_env::ERR_OK) + }) + }, + Err(_) => Ok(sandbox_env::ERR_EXECUTION), + } + } + + fn instance_teardown(&mut self, instance_id: u32) -> WResult<()> { + self.sandbox_store + .borrow_mut() + .instance_teardown(instance_id) + .map_err(|e| e.to_string()) + } + + fn instance_new( + &mut self, + dispatch_thunk_id: u32, + wasm: &[u8], + raw_env_def: &[u8], + state: u32, + ) -> WResult { + // Extract a dispatch thunk from instance's table by the specified index. + let dispatch_thunk = { + let table = self + .table + .as_ref() + .ok_or("Runtime doesn't have a table; sandbox is unavailable")?; + table + .get(dispatch_thunk_id) + .map_err(|_| "dispatch_thunk_idx is out of the table bounds")? + .ok_or("dispatch_thunk_idx points on an empty table entry")? + }; + + let guest_env = + match sandbox::GuestEnvironment::decode(&*self.sandbox_store.borrow(), raw_env_def) { + Ok(guest_env) => guest_env, + Err(_) => return Ok(sandbox_env::ERR_MODULE as u32), + }; + + let store = self.sandbox_store.clone(); + let result = store.borrow_mut().instantiate( + wasm, + guest_env, + state, + &mut SandboxContext { executor: self, dispatch_thunk: dispatch_thunk.clone() }, + ); + + let instance_idx_or_err_code = + match result.map(|i| i.register(&mut store.borrow_mut(), dispatch_thunk)) { + Ok(instance_idx) => instance_idx, + Err(sandbox::InstantiationError::StartTrapped) => sandbox_env::ERR_EXECUTION, + Err(_) => sandbox_env::ERR_MODULE, + }; + + Ok(instance_idx_or_err_code) + } + + fn get_global_val( + &self, + instance_idx: u32, + name: &str, + ) -> WResult> { + self.sandbox_store + .borrow() + .instance(instance_idx) + .map(|i| i.get_global_val(name)) + .map_err(|e| e.to_string()) + } +} + /// Will be used on initialization of a module to resolve function and memory imports. struct Resolver<'a> { /// All the hot functions that we export for the WASM blob. @@ -275,6 +502,7 @@ fn call_in_wasm_module( let mut function_executor = FunctionExecutor::new( memory.clone(), heap_base, + table.clone(), host_functions, allow_missing_func_imports, missing_functions, diff --git a/client/executor/wasmtime/Cargo.toml b/client/executor/wasmtime/Cargo.toml index 7e38929f05a13..b12ca0779e7a2 100644 --- a/client/executor/wasmtime/Cargo.toml +++ b/client/executor/wasmtime/Cargo.toml @@ -14,8 +14,10 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] cfg-if = "1.0" +codec = { package = "parity-scale-codec", version = "3.0.0" } libc = "0.2.121" log = "0.4.17" +parity-wasm = "0.45" # When bumping wasmtime do not forget to also bump rustix # to exactly the same version as used by wasmtime! @@ -30,6 +32,7 @@ wasmtime = { version = "1.0.0", default-features = false, features = [ sc-allocator = { version = "4.1.0-dev", path = "../../allocator" } sc-executor-common = { version = "0.10.0-dev", path = "../common" } sp-runtime-interface = { version = "7.0.0", path = "../../../primitives/runtime-interface" } +sp-sandbox = { version = "0.10.0-dev", path = "../../../primitives/sandbox" } sp-wasm-interface = { version = "7.0.0", path = "../../../primitives/wasm-interface" } # Here we include the rustix crate in the exactly same semver-compatible version as used by @@ -47,4 +50,3 @@ sc-runtime-test = { version = "2.0.0", path = "../runtime-test" } sp-io = { version = "7.0.0", path = "../../../primitives/io" } tempfile = "3.3.0" paste = "1.0" -codec = { package = "parity-scale-codec", version = "3.0.0" } diff --git a/client/executor/wasmtime/src/host.rs b/client/executor/wasmtime/src/host.rs index 0d9eac875170b..768a6e36e2390 100644 --- a/client/executor/wasmtime/src/host.rs +++ b/client/executor/wasmtime/src/host.rs @@ -19,17 +19,33 @@ //! This module defines `HostState` and `HostContext` structs which provide logic and state //! required for execution of host. -use wasmtime::Caller; +use log::trace; +use wasmtime::{Caller, Func, Val}; +use codec::{Decode, Encode}; use sc_allocator::{AllocationStats, FreeingBumpHeapAllocator}; -use sp_wasm_interface::{Pointer, WordSize}; +use sc_executor_common::{ + error::Result, + sandbox::{self, SupervisorFuncIndex}, + util::MemoryTransfer, +}; +use sp_sandbox::env as sandbox_env; +use sp_wasm_interface::{FunctionContext, MemoryId, Pointer, Sandbox, WordSize}; use crate::{runtime::StoreData, util}; +// The sandbox store is inside of a Option>> so that we can temporarily borrow it. +struct SandboxStore(Option>>); + +// There are a bunch of `Rc`s within the sandbox store, however we only manipulate +// those within one thread so this should be safe. +unsafe impl Send for SandboxStore {} + /// 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. pub struct HostState { + sandbox_store: SandboxStore, allocator: FreeingBumpHeapAllocator, panic_message: Option, } @@ -37,7 +53,13 @@ pub struct HostState { impl HostState { /// Constructs a new `HostState`. pub fn new(allocator: FreeingBumpHeapAllocator) -> Self { - HostState { allocator, panic_message: None } + HostState { + sandbox_store: SandboxStore(Some(Box::new(sandbox::Store::new( + sandbox::SandboxBackend::TryWasmer, + )))), + allocator, + panic_message: None, + } } /// Takes the error message out of the host state, leaving a `None` in its place. @@ -58,12 +80,35 @@ pub(crate) struct HostContext<'a> { } impl<'a> HostContext<'a> { + fn host_state(&self) -> &HostState { + self.caller + .data() + .host_state() + .expect("host state is not empty when calling a function in wasm; qed") + } + fn host_state_mut(&mut self) -> &mut HostState { self.caller .data_mut() .host_state_mut() .expect("host state is not empty when calling a function in wasm; qed") } + + fn sandbox_store(&self) -> &sandbox::Store { + self.host_state() + .sandbox_store + .0 + .as_ref() + .expect("sandbox store is only empty when temporarily borrowed") + } + + fn sandbox_store_mut(&mut self) -> &mut sandbox::Store { + self.host_state_mut() + .sandbox_store + .0 + .as_mut() + .expect("sandbox store is only empty when temporarily borrowed") + } } impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> { @@ -99,7 +144,233 @@ impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> { .map_err(|e| e.to_string()) } + fn sandbox(&mut self) -> &mut dyn Sandbox { + self + } + fn register_panic_error_message(&mut self, message: &str) { self.host_state_mut().panic_message = Some(message.to_owned()); } } + +impl<'a> Sandbox for HostContext<'a> { + fn memory_get( + &mut self, + memory_id: MemoryId, + offset: WordSize, + buf_ptr: Pointer, + buf_len: WordSize, + ) -> sp_wasm_interface::Result { + let sandboxed_memory = self.sandbox_store().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_env::ERR_OUT_OF_BOUNDS), + Ok(buffer) => buffer, + }; + + if util::write_memory_from(&mut self.caller, buf_ptr, &buffer).is_err() { + return Ok(sandbox_env::ERR_OUT_OF_BOUNDS) + } + + Ok(sandbox_env::ERR_OK) + } + + fn memory_set( + &mut self, + memory_id: MemoryId, + offset: WordSize, + val_ptr: Pointer, + val_len: WordSize, + ) -> sp_wasm_interface::Result { + let sandboxed_memory = self.sandbox_store().memory(memory_id).map_err(|e| e.to_string())?; + + let len = val_len as usize; + + let buffer = match util::read_memory(&self.caller, val_ptr, len) { + Err(_) => return Ok(sandbox_env::ERR_OUT_OF_BOUNDS), + Ok(buffer) => buffer, + }; + + if sandboxed_memory.write_from(Pointer::new(offset as u32), &buffer).is_err() { + return Ok(sandbox_env::ERR_OUT_OF_BOUNDS) + } + + Ok(sandbox_env::ERR_OK) + } + + fn memory_teardown(&mut self, memory_id: MemoryId) -> sp_wasm_interface::Result<()> { + self.sandbox_store_mut().memory_teardown(memory_id).map_err(|e| e.to_string()) + } + + fn memory_new(&mut self, initial: u32, maximum: u32) -> sp_wasm_interface::Result { + self.sandbox_store_mut().new_memory(initial, maximum).map_err(|e| e.to_string()) + } + + fn invoke( + &mut self, + instance_id: u32, + export_name: &str, + mut args: &[u8], + return_val: Pointer, + return_val_len: u32, + state: u32, + ) -> sp_wasm_interface::Result { + trace!(target: "sp-sandbox", "invoke, instance_idx={}", instance_id); + + // Deserialize arguments and convert them into wasmi types. + let args = Vec::::decode(&mut args) + .map_err(|_| "Can't decode serialized arguments for the invocation")? + .into_iter() + .collect::>(); + + let instance = self.sandbox_store().instance(instance_id).map_err(|e| e.to_string())?; + + let dispatch_thunk = + self.sandbox_store().dispatch_thunk(instance_id).map_err(|e| e.to_string())?; + + let result = instance.invoke( + export_name, + &args, + state, + &mut SandboxContext { host_context: self, dispatch_thunk }, + ); + + match result { + Ok(None) => Ok(sandbox_env::ERR_OK), + Ok(Some(val)) => { + // Serialize return value and write it back into the memory. + sp_wasm_interface::ReturnValue::Value(val.into()).using_encoded(|val| { + if val.len() > return_val_len as usize { + return Err("Return value buffer is too small".into()) + } + ::write_memory(self, return_val, val) + .map_err(|_| "can't write return value")?; + Ok(sandbox_env::ERR_OK) + }) + }, + Err(_) => Ok(sandbox_env::ERR_EXECUTION), + } + } + + fn instance_teardown(&mut self, instance_id: u32) -> sp_wasm_interface::Result<()> { + self.sandbox_store_mut() + .instance_teardown(instance_id) + .map_err(|e| e.to_string()) + } + + fn instance_new( + &mut self, + dispatch_thunk_id: u32, + wasm: &[u8], + raw_env_def: &[u8], + state: u32, + ) -> sp_wasm_interface::Result { + // Extract a dispatch thunk from the instance's table by the specified index. + let dispatch_thunk = { + let table = self + .caller + .data() + .table() + .ok_or("Runtime doesn't have a table; sandbox is unavailable")?; + let table_item = table.get(&mut self.caller, dispatch_thunk_id); + + *table_item + .ok_or("dispatch_thunk_id is out of bounds")? + .funcref() + .ok_or("dispatch_thunk_idx should be a funcref")? + .ok_or("dispatch_thunk_idx should point to actual func")? + }; + + let guest_env = match sandbox::GuestEnvironment::decode(self.sandbox_store(), raw_env_def) { + Ok(guest_env) => guest_env, + Err(_) => return Ok(sandbox_env::ERR_MODULE as u32), + }; + + let mut store = self + .host_state_mut() + .sandbox_store + .0 + .take() + .expect("sandbox store is only empty when borrowed"); + + // Catch any potential panics so that we can properly restore the sandbox store + // which we've destructively borrowed. + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + store.instantiate( + wasm, + guest_env, + state, + &mut SandboxContext { host_context: self, dispatch_thunk }, + ) + })); + + self.host_state_mut().sandbox_store.0 = Some(store); + + let result = match result { + Ok(result) => result, + Err(error) => std::panic::resume_unwind(error), + }; + + let instance_idx_or_err_code = match result { + Ok(instance) => instance.register(self.sandbox_store_mut(), dispatch_thunk), + Err(sandbox::InstantiationError::StartTrapped) => sandbox_env::ERR_EXECUTION, + Err(_) => sandbox_env::ERR_MODULE, + }; + + Ok(instance_idx_or_err_code as u32) + } + + fn get_global_val( + &self, + instance_idx: u32, + name: &str, + ) -> sp_wasm_interface::Result> { + self.sandbox_store() + .instance(instance_idx) + .map(|i| i.get_global_val(name)) + .map_err(|e| e.to_string()) + } +} + +struct SandboxContext<'a, 'b> { + host_context: &'a mut HostContext<'b>, + dispatch_thunk: Func, +} + +impl<'a, 'b> sandbox::SandboxContext for SandboxContext<'a, 'b> { + fn invoke( + &mut self, + invoke_args_ptr: Pointer, + invoke_args_len: WordSize, + state: u32, + func_idx: SupervisorFuncIndex, + ) -> Result { + let mut ret_vals = [Val::null()]; + let result = self.dispatch_thunk.call( + &mut self.host_context.caller, + &[ + Val::I32(u32::from(invoke_args_ptr) as i32), + Val::I32(invoke_args_len as i32), + Val::I32(state as i32), + Val::I32(usize::from(func_idx) as i32), + ], + &mut ret_vals, + ); + + match result { + Ok(()) => + if let Some(ret_val) = ret_vals[0].i64() { + Ok(ret_val) + } else { + Err("Supervisor function returned unexpected result!".into()) + }, + Err(err) => Err(err.to_string().into()), + } + } + + fn supervisor_context(&mut self) -> &mut dyn FunctionContext { + self.host_context + } +} diff --git a/client/executor/wasmtime/src/runtime.rs b/client/executor/wasmtime/src/runtime.rs index b124fd627dc69..5bca899648c34 100644 --- a/client/executor/wasmtime/src/runtime.rs +++ b/client/executor/wasmtime/src/runtime.rs @@ -56,6 +56,11 @@ pub(crate) struct StoreData { } impl StoreData { + /// Returns a reference to the host state. + pub fn host_state(&self) -> Option<&HostState> { + self.host_state.as_ref() + } + /// Returns a mutable reference to the host state. pub fn host_state_mut(&mut self) -> Option<&mut HostState> { self.host_state.as_mut() @@ -65,6 +70,11 @@ impl StoreData { pub fn memory(&self) -> Memory { self.memory.expect("memory is always set; qed") } + + /// Returns the host table. + pub fn table(&self) -> Option { + self.table + } } pub(crate) type Store = wasmtime::Store; diff --git a/client/executor/wasmtime/src/util.rs b/client/executor/wasmtime/src/util.rs index 15f62e475033a..83745e21e86af 100644 --- a/client/executor/wasmtime/src/util.rs +++ b/client/executor/wasmtime/src/util.rs @@ -48,6 +48,24 @@ pub fn into_wasmtime_val(value: Value) -> wasmtime::Val { } } +/// 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. +pub(crate) fn read_memory( + ctx: impl AsContext, + source_addr: Pointer, + size: usize, +) -> Result> { + let range = + checked_range(source_addr.into(), size, ctx.as_context().data().memory().data_size(&ctx)) + .ok_or_else(|| Error::Other("memory read is out of bounds".into()))?; + + let mut buffer = vec![0; range.len()]; + read_memory_into(ctx, source_addr, &mut buffer)?; + + Ok(buffer) +} + /// Read data from the instance memory into a slice. /// /// Returns an error if the read would go out of the memory bounds. diff --git a/primitives/io/src/lib.rs b/primitives/io/src/lib.rs index bb06c00ee2c6f..7f4b111a66202 100644 --- a/primitives/io/src/lib.rs +++ b/primitives/io/src/lib.rs @@ -1611,6 +1611,99 @@ mod tracing_setup { pub use tracing_setup::init_tracing; +/// Wasm-only interface that provides functions for interacting with the sandbox. +#[runtime_interface(wasm_only)] +pub trait Sandbox { + /// Instantiate a new sandbox instance with the given `wasm_code`. + fn instantiate( + &mut self, + dispatch_thunk: u32, + wasm_code: &[u8], + env_def: &[u8], + state_ptr: Pointer, + ) -> u32 { + self.sandbox() + .instance_new(dispatch_thunk, wasm_code, env_def, state_ptr.into()) + .expect("Failed to instantiate a new sandbox") + } + + /// Invoke `function` in the sandbox with `sandbox_idx`. + fn invoke( + &mut self, + instance_idx: u32, + function: &str, + args: &[u8], + return_val_ptr: Pointer, + return_val_len: u32, + state_ptr: Pointer, + ) -> u32 { + self.sandbox() + .invoke(instance_idx, function, args, return_val_ptr, return_val_len, state_ptr.into()) + .expect("Failed to invoke function with sandbox") + } + + /// Create a new memory instance with the given `initial` and `maximum` size. + fn memory_new(&mut self, initial: u32, maximum: u32) -> u32 { + self.sandbox() + .memory_new(initial, maximum) + .expect("Failed to create new memory with sandbox") + } + + /// Get the memory starting at `offset` from the instance with `memory_idx` into the buffer. + fn memory_get( + &mut self, + memory_idx: u32, + offset: u32, + buf_ptr: Pointer, + buf_len: u32, + ) -> u32 { + self.sandbox() + .memory_get(memory_idx, offset, buf_ptr, buf_len) + .expect("Failed to get memory with sandbox") + } + + /// Set the memory in the given `memory_idx` to the given value at `offset`. + fn memory_set( + &mut self, + memory_idx: u32, + offset: u32, + val_ptr: Pointer, + val_len: u32, + ) -> u32 { + self.sandbox() + .memory_set(memory_idx, offset, val_ptr, val_len) + .expect("Failed to set memory with sandbox") + } + + /// Teardown the memory instance with the given `memory_idx`. + fn memory_teardown(&mut self, memory_idx: u32) { + self.sandbox() + .memory_teardown(memory_idx) + .expect("Failed to teardown memory with sandbox") + } + + /// Teardown the sandbox instance with the given `instance_idx`. + fn instance_teardown(&mut self, instance_idx: u32) { + self.sandbox() + .instance_teardown(instance_idx) + .expect("Failed to teardown sandbox instance") + } + + /// Get the value from a global with the given `name`. The sandbox is determined by the given + /// `instance_idx`. + /// + /// Returns `Some(_)` when the requested global variable could be found. + fn get_global_val( + &mut self, + instance_idx: u32, + name: &str, + ) -> Option { + self.sandbox() + .get_global_val(instance_idx, name) + .expect("Failed to get global from sandbox") + } +} + /// Allocator used by Substrate when executing the Wasm runtime. #[cfg(all(target_arch = "wasm32", not(feature = "std")))] struct WasmAllocator; @@ -1686,6 +1779,7 @@ pub type SubstrateHostFunctions = ( allocator::HostFunctions, panic_handler::HostFunctions, logging::HostFunctions, + sandbox::HostFunctions, crate::trie::HostFunctions, offchain_index::HostFunctions, transaction_index::HostFunctions, diff --git a/primitives/sandbox/Cargo.toml b/primitives/sandbox/Cargo.toml new file mode 100644 index 0000000000000..024fe7209393c --- /dev/null +++ b/primitives/sandbox/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "sp-sandbox" +version = "0.10.0-dev" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "This crate provides means to instantiate and execute wasm modules." +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } +log = { version = "0.4", default-features = false } +wasmi = { version = "0.13", default-features = false } +sp-core = { version = "7.0.0", default-features = false, path = "../core" } +sp-io = { version = "7.0.0", default-features = false, path = "../io" } +sp-std = { version = "5.0.0", default-features = false, path = "../std" } +sp-wasm-interface = { version = "7.0.0", default-features = false, path = "../wasm-interface" } + +[dev-dependencies] +assert_matches = "1.3.0" +wat = "1.0" + +[features] +default = ["std"] +std = [ + "codec/std", + "log/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", + "sp-wasm-interface/std", + "wasmi/std", +] +strict = [] +wasmer-sandbox = [] diff --git a/primitives/sandbox/README.md b/primitives/sandbox/README.md new file mode 100644 index 0000000000000..9335b53ae1fb9 --- /dev/null +++ b/primitives/sandbox/README.md @@ -0,0 +1,21 @@ +This crate provides means to instantiate and execute wasm modules. + +It works even when the user of this library executes from +inside the wasm VM. In this case the same VM is used for execution +of both the sandbox owner and the sandboxed module, without compromising security +and without the performance penalty of full wasm emulation inside wasm. + +This is achieved by using bindings to the wasm VM, which are published by the host API. +This API is thin and consists of only a handful functions. It contains functions for instantiating +modules and executing them, but doesn't contain functions for inspecting the module +structure. The user of this library is supposed to read the wasm module. + +When this crate is used in the `std` environment all these functions are implemented by directly +calling the wasm VM. + +Examples of possible use-cases for this library are not limited to the following: + +- implementing smart-contract runtimes that use wasm for contract code +- executing a wasm substrate runtime inside of a wasm parachain + +License: Apache-2.0 \ No newline at end of file diff --git a/primitives/sandbox/src/embedded_executor.rs b/primitives/sandbox/src/embedded_executor.rs new file mode 100644 index 0000000000000..115c3192f3d89 --- /dev/null +++ b/primitives/sandbox/src/embedded_executor.rs @@ -0,0 +1,478 @@ +// This file is part of Substrate. + +// Copyright (C) 2018-2022 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. + +//! An embedded WASM executor utilizing `wasmi`. + +use alloc::string::String; + +use wasmi::{ + memory_units::Pages, Externals, FuncInstance, FuncRef, GlobalDescriptor, GlobalRef, + ImportResolver, MemoryDescriptor, MemoryInstance, MemoryRef, Module, ModuleInstance, ModuleRef, + RuntimeArgs, RuntimeValue, Signature, TableDescriptor, TableRef, Trap, +}; + +use sp_std::{ + borrow::ToOwned, collections::btree_map::BTreeMap, fmt, marker::PhantomData, prelude::*, +}; + +use crate::{Error, HostError, HostFuncType, ReturnValue, Value, TARGET}; + +/// The linear memory used by the sandbox. +#[derive(Clone)] +pub struct Memory { + memref: MemoryRef, +} + +impl super::SandboxMemory for Memory { + fn new(initial: u32, maximum: Option) -> Result { + Ok(Memory { + memref: MemoryInstance::alloc( + Pages(initial as usize), + maximum.map(|m| Pages(m as usize)), + ) + .map_err(|_| Error::Module)?, + }) + } + + fn get(&self, ptr: u32, buf: &mut [u8]) -> Result<(), Error> { + self.memref.get_into(ptr, buf).map_err(|_| Error::OutOfBounds)?; + Ok(()) + } + + fn set(&self, ptr: u32, value: &[u8]) -> Result<(), Error> { + self.memref.set(ptr, value).map_err(|_| Error::OutOfBounds)?; + Ok(()) + } +} + +struct HostFuncIndex(usize); + +struct DefinedHostFunctions { + funcs: Vec>, +} + +impl Clone for DefinedHostFunctions { + fn clone(&self) -> DefinedHostFunctions { + DefinedHostFunctions { funcs: self.funcs.clone() } + } +} + +impl DefinedHostFunctions { + fn new() -> DefinedHostFunctions { + DefinedHostFunctions { funcs: Vec::new() } + } + + fn define(&mut self, f: HostFuncType) -> HostFuncIndex { + let idx = self.funcs.len(); + self.funcs.push(f); + HostFuncIndex(idx) + } +} + +#[derive(Debug)] +struct DummyHostError; + +impl fmt::Display for DummyHostError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "DummyHostError") + } +} + +impl wasmi::HostError for DummyHostError {} + +struct GuestExternals<'a, T: 'a> { + state: &'a mut T, + defined_host_functions: &'a DefinedHostFunctions, +} + +impl<'a, T> Externals for GuestExternals<'a, T> { + fn invoke_index( + &mut self, + index: usize, + args: RuntimeArgs, + ) -> Result, Trap> { + let args = args.as_ref().iter().cloned().map(to_interface).collect::>(); + + let result = (self.defined_host_functions.funcs[index])(self.state, &args); + match result { + Ok(value) => Ok(match value { + ReturnValue::Value(v) => Some(to_wasmi(v)), + ReturnValue::Unit => None, + }), + Err(HostError) => Err(Trap::host(DummyHostError)), + } + } +} + +enum ExternVal { + HostFunc(HostFuncIndex), + Memory(Memory), +} + +/// A builder for the environment of the sandboxed WASM module. +pub struct EnvironmentDefinitionBuilder { + map: BTreeMap<(Vec, Vec), ExternVal>, + defined_host_functions: DefinedHostFunctions, +} + +impl super::SandboxEnvironmentBuilder for EnvironmentDefinitionBuilder { + fn new() -> EnvironmentDefinitionBuilder { + EnvironmentDefinitionBuilder { + map: BTreeMap::new(), + defined_host_functions: DefinedHostFunctions::new(), + } + } + + fn add_host_func(&mut self, module: N1, field: N2, f: HostFuncType) + where + N1: Into>, + N2: Into>, + { + let idx = self.defined_host_functions.define(f); + self.map.insert((module.into(), field.into()), ExternVal::HostFunc(idx)); + } + + fn add_memory(&mut self, module: N1, field: N2, mem: Memory) + where + N1: Into>, + N2: Into>, + { + self.map.insert((module.into(), field.into()), ExternVal::Memory(mem)); + } +} + +impl ImportResolver for EnvironmentDefinitionBuilder { + fn resolve_func( + &self, + module_name: &str, + field_name: &str, + signature: &Signature, + ) -> Result { + let key = (module_name.as_bytes().to_owned(), field_name.as_bytes().to_owned()); + let externval = self.map.get(&key).ok_or_else(|| { + log::debug!(target: TARGET, "Export {}:{} not found", module_name, field_name); + wasmi::Error::Instantiation(String::new()) + })?; + let host_func_idx = match *externval { + ExternVal::HostFunc(ref idx) => idx, + _ => { + log::debug!( + target: TARGET, + "Export {}:{} is not a host func", + module_name, + field_name, + ); + return Err(wasmi::Error::Instantiation(String::new())) + }, + }; + Ok(FuncInstance::alloc_host(signature.clone(), host_func_idx.0)) + } + + fn resolve_global( + &self, + _module_name: &str, + _field_name: &str, + _global_type: &GlobalDescriptor, + ) -> Result { + log::debug!(target: TARGET, "Importing globals is not supported yet"); + Err(wasmi::Error::Instantiation(String::new())) + } + + fn resolve_memory( + &self, + module_name: &str, + field_name: &str, + _memory_type: &MemoryDescriptor, + ) -> Result { + let key = (module_name.as_bytes().to_owned(), field_name.as_bytes().to_owned()); + let externval = self.map.get(&key).ok_or_else(|| { + log::debug!(target: TARGET, "Export {}:{} not found", module_name, field_name); + wasmi::Error::Instantiation(String::new()) + })?; + let memory = match *externval { + ExternVal::Memory(ref m) => m, + _ => { + log::debug!( + target: TARGET, + "Export {}:{} is not a memory", + module_name, + field_name, + ); + return Err(wasmi::Error::Instantiation(String::new())) + }, + }; + Ok(memory.memref.clone()) + } + + fn resolve_table( + &self, + _module_name: &str, + _field_name: &str, + _table_type: &TableDescriptor, + ) -> Result { + log::debug!("Importing tables is not supported yet"); + Err(wasmi::Error::Instantiation(String::new())) + } +} + +/// Sandboxed instance of a WASM module. +pub struct Instance { + instance: ModuleRef, + defined_host_functions: DefinedHostFunctions, + _marker: PhantomData, +} + +impl super::SandboxInstance for Instance { + type Memory = Memory; + type EnvironmentBuilder = EnvironmentDefinitionBuilder; + + fn new( + code: &[u8], + env_def_builder: &EnvironmentDefinitionBuilder, + state: &mut T, + ) -> Result, Error> { + let module = Module::from_buffer(code).map_err(|_| Error::Module)?; + let not_started_instance = + ModuleInstance::new(&module, env_def_builder).map_err(|_| Error::Module)?; + + let defined_host_functions = env_def_builder.defined_host_functions.clone(); + let instance = { + let mut externals = + GuestExternals { state, defined_host_functions: &defined_host_functions }; + let instance = + not_started_instance.run_start(&mut externals).map_err(|_| Error::Execution)?; + instance + }; + + Ok(Instance { instance, defined_host_functions, _marker: PhantomData:: }) + } + + fn invoke(&mut self, name: &str, args: &[Value], state: &mut T) -> Result { + let args = args.iter().cloned().map(to_wasmi).collect::>(); + + let mut externals = + GuestExternals { state, defined_host_functions: &self.defined_host_functions }; + let result = self.instance.invoke_export(name, &args, &mut externals); + + match result { + Ok(None) => Ok(ReturnValue::Unit), + Ok(Some(val)) => Ok(ReturnValue::Value(to_interface(val))), + Err(_err) => Err(Error::Execution), + } + } + + fn get_global_val(&self, name: &str) -> Option { + let global = self.instance.export_by_name(name)?.as_global()?.get(); + + Some(to_interface(global)) + } +} + +/// Convert the substrate value type to the wasmi value type. +fn to_wasmi(value: Value) -> RuntimeValue { + match value { + Value::I32(val) => RuntimeValue::I32(val), + Value::I64(val) => RuntimeValue::I64(val), + Value::F32(val) => RuntimeValue::F32(val.into()), + Value::F64(val) => RuntimeValue::F64(val.into()), + } +} + +/// Convert the wasmi value type to the substrate value type. +fn to_interface(value: RuntimeValue) -> Value { + match value { + RuntimeValue::I32(val) => Value::I32(val), + RuntimeValue::I64(val) => Value::I64(val), + RuntimeValue::F32(val) => Value::F32(val.into()), + RuntimeValue::F64(val) => Value::F64(val.into()), + } +} + +#[cfg(test)] +mod tests { + use super::{EnvironmentDefinitionBuilder, Instance}; + use crate::{Error, HostError, ReturnValue, SandboxEnvironmentBuilder, SandboxInstance, Value}; + use assert_matches::assert_matches; + + fn execute_sandboxed(code: &[u8], args: &[Value]) -> Result { + struct State { + counter: u32, + } + + fn env_assert(_e: &mut State, args: &[Value]) -> Result { + if args.len() != 1 { + return Err(HostError) + } + let condition = args[0].as_i32().ok_or_else(|| HostError)?; + if condition != 0 { + Ok(ReturnValue::Unit) + } else { + Err(HostError) + } + } + fn env_inc_counter(e: &mut State, args: &[Value]) -> Result { + if args.len() != 1 { + return Err(HostError) + } + let inc_by = args[0].as_i32().ok_or_else(|| HostError)?; + e.counter += inc_by as u32; + Ok(ReturnValue::Value(Value::I32(e.counter as i32))) + } + /// Function that takes one argument of any type and returns that value. + fn env_polymorphic_id(_e: &mut State, args: &[Value]) -> Result { + if args.len() != 1 { + return Err(HostError) + } + Ok(ReturnValue::Value(args[0])) + } + + let mut state = State { counter: 0 }; + + let mut env_builder = EnvironmentDefinitionBuilder::new(); + env_builder.add_host_func("env", "assert", env_assert); + env_builder.add_host_func("env", "inc_counter", env_inc_counter); + env_builder.add_host_func("env", "polymorphic_id", env_polymorphic_id); + + let mut instance = Instance::new(code, &env_builder, &mut state)?; + let result = instance.invoke("call", args, &mut state); + + result.map_err(|_| HostError) + } + + #[test] + fn invoke_args() { + let code = wat::parse_str( + r#" + (module + (import "env" "assert" (func $assert (param i32))) + + (func (export "call") (param $x i32) (param $y i64) + ;; assert that $x = 0x12345678 + (call $assert + (i32.eq + (get_local $x) + (i32.const 0x12345678) + ) + ) + + (call $assert + (i64.eq + (get_local $y) + (i64.const 0x1234567887654321) + ) + ) + ) + ) + "#, + ) + .unwrap(); + + let result = + execute_sandboxed(&code, &[Value::I32(0x12345678), Value::I64(0x1234567887654321)]); + assert!(result.is_ok()); + } + + #[test] + fn return_value() { + let code = wat::parse_str( + r#" + (module + (func (export "call") (param $x i32) (result i32) + (i32.add + (get_local $x) + (i32.const 1) + ) + ) + ) + "#, + ) + .unwrap(); + + let return_val = execute_sandboxed(&code, &[Value::I32(0x1336)]).unwrap(); + assert_eq!(return_val, ReturnValue::Value(Value::I32(0x1337))); + } + + #[test] + fn signatures_dont_matter() { + let code = wat::parse_str( + r#" + (module + (import "env" "polymorphic_id" (func $id_i32 (param i32) (result i32))) + (import "env" "polymorphic_id" (func $id_i64 (param i64) (result i64))) + (import "env" "assert" (func $assert (param i32))) + + (func (export "call") + ;; assert that we can actually call the "same" function with different + ;; signatures. + (call $assert + (i32.eq + (call $id_i32 + (i32.const 0x012345678) + ) + (i32.const 0x012345678) + ) + ) + (call $assert + (i64.eq + (call $id_i64 + (i64.const 0x0123456789abcdef) + ) + (i64.const 0x0123456789abcdef) + ) + ) + ) + ) + "#, + ) + .unwrap(); + + let return_val = execute_sandboxed(&code, &[]).unwrap(); + assert_eq!(return_val, ReturnValue::Unit); + } + + #[test] + fn cant_return_unmatching_type() { + fn env_returns_i32(_e: &mut (), _args: &[Value]) -> Result { + Ok(ReturnValue::Value(Value::I32(42))) + } + + let mut env_builder = EnvironmentDefinitionBuilder::new(); + env_builder.add_host_func("env", "returns_i32", env_returns_i32); + + let code = wat::parse_str( + r#" + (module + ;; It's actually returns i32, but imported as if it returned i64 + (import "env" "returns_i32" (func $returns_i32 (result i64))) + + (func (export "call") + (drop + (call $returns_i32) + ) + ) + ) + "#, + ) + .unwrap(); + + // It succeeds since we are able to import functions with types we want. + let mut instance = Instance::new(&code, &env_builder, &mut ()).unwrap(); + + // But this fails since we imported a function that returns i32 as if it returned i64. + assert_matches!(instance.invoke("call", &[], &mut ()), Err(Error::Execution)); + } +} diff --git a/primitives/sandbox/src/env.rs b/primitives/sandbox/src/env.rs new file mode 100644 index 0000000000000..94b1c5e467a9c --- /dev/null +++ b/primitives/sandbox/src/env.rs @@ -0,0 +1,120 @@ +// This file is part of Substrate. + +// Copyright (C) 2018-2022 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. + +//! Definition of a sandbox environment. + +use codec::{Decode, Encode}; + +use sp_core::RuntimeDebug; +use sp_std::vec::Vec; + +/// Error error that can be returned from host function. +#[derive(Encode, Decode, RuntimeDebug)] +pub struct HostError; + +/// Describes an entity to define or import into the environment. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +pub enum ExternEntity { + /// Function that is specified by an index in a default table of + /// a module that creates the sandbox. + #[codec(index = 1)] + Function(u32), + + /// Linear memory that is specified by some identifier returned by sandbox + /// module upon creation new sandboxed memory. + #[codec(index = 2)] + Memory(u32), +} + +/// An entry in a environment definition table. +/// +/// Each entry has a two-level name and description of an entity +/// being defined. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +pub struct Entry { + /// Module name of which corresponding entity being defined. + pub module_name: Vec, + /// Field name in which corresponding entity being defined. + pub field_name: Vec, + /// External entity being defined. + pub entity: ExternEntity, +} + +/// Definition of runtime that could be used by sandboxed code. +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug)] +pub struct EnvironmentDefinition { + /// Vector of all entries in the environment definition. + pub entries: Vec, +} + +/// Constant for specifying no limit when creating a sandboxed +/// memory instance. For FFI purposes. +pub const MEM_UNLIMITED: u32 = -1i32 as u32; + +/// No error happened. +/// +/// For FFI purposes. +pub const ERR_OK: u32 = 0; + +/// Validation or instantiation error occurred when creating new +/// sandboxed module instance. +/// +/// For FFI purposes. +pub const ERR_MODULE: u32 = -1i32 as u32; + +/// Out-of-bounds access attempted with memory or table. +/// +/// For FFI purposes. +pub const ERR_OUT_OF_BOUNDS: u32 = -2i32 as u32; + +/// Execution error occurred (typically trap). +/// +/// For FFI purposes. +pub const ERR_EXECUTION: u32 = -3i32 as u32; + +#[cfg(test)] +mod tests { + use super::*; + use codec::Codec; + use std::fmt; + + fn roundtrip(s: S) { + let encoded = s.encode(); + assert_eq!(S::decode(&mut &encoded[..]).unwrap(), s); + } + + #[test] + fn env_def_roundtrip() { + roundtrip(EnvironmentDefinition { entries: vec![] }); + + roundtrip(EnvironmentDefinition { + entries: vec![Entry { + module_name: b"kernel"[..].into(), + field_name: b"memory"[..].into(), + entity: ExternEntity::Memory(1337), + }], + }); + + roundtrip(EnvironmentDefinition { + entries: vec![Entry { + module_name: b"env"[..].into(), + field_name: b"abort"[..].into(), + entity: ExternEntity::Function(228), + }], + }); + } +} diff --git a/primitives/sandbox/src/host_executor.rs b/primitives/sandbox/src/host_executor.rs new file mode 100644 index 0000000000000..e62c051262ca8 --- /dev/null +++ b/primitives/sandbox/src/host_executor.rs @@ -0,0 +1,274 @@ +// This file is part of Substrate. + +// Copyright (C) 2018-2022 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. + +//! A WASM executor utilizing the sandbox runtime interface of the host. + +use codec::{Decode, Encode}; + +use sp_io::sandbox; +use sp_std::{marker, mem, prelude::*, rc::Rc, slice, vec}; + +use crate::{env, Error, HostFuncType, ReturnValue, Value}; + +mod ffi { + use super::HostFuncType; + use sp_std::mem; + + /// Index into the default table that points to a `HostFuncType`. + pub type HostFuncIndex = usize; + + /// Coerce `HostFuncIndex` to a callable host function pointer. + /// + /// # Safety + /// + /// This function should be only called with a `HostFuncIndex` that was previously registered + /// in the environment definition. Typically this should only + /// be called with an argument received in `dispatch_thunk`. + pub unsafe fn coerce_host_index_to_func(idx: HostFuncIndex) -> HostFuncType { + // We need to ensure that sizes of a callable function pointer and host function index is + // indeed equal. + // We can't use `static_assertions` create because it makes compiler panic, fallback to + // runtime assert. const_assert!(mem::size_of::() == + // mem::size_of::>()); + assert!(mem::size_of::() == mem::size_of::>()); + mem::transmute::>(idx) + } +} + +struct MemoryHandle { + memory_idx: u32, +} + +impl Drop for MemoryHandle { + fn drop(&mut self) { + sandbox::memory_teardown(self.memory_idx); + } +} + +/// The linear memory used by the sandbox. +#[derive(Clone)] +pub struct Memory { + // Handle to memory instance is wrapped to add reference-counting semantics + // to `Memory`. + handle: Rc, +} + +impl super::SandboxMemory for Memory { + fn new(initial: u32, maximum: Option) -> Result { + let maximum = if let Some(maximum) = maximum { maximum } else { env::MEM_UNLIMITED }; + + match sandbox::memory_new(initial, maximum) { + env::ERR_MODULE => Err(Error::Module), + memory_idx => Ok(Memory { handle: Rc::new(MemoryHandle { memory_idx }) }), + } + } + + fn get(&self, offset: u32, buf: &mut [u8]) -> Result<(), Error> { + let result = + sandbox::memory_get(self.handle.memory_idx, offset, buf.as_mut_ptr(), buf.len() as u32); + match result { + env::ERR_OK => Ok(()), + env::ERR_OUT_OF_BOUNDS => Err(Error::OutOfBounds), + _ => unreachable!(), + } + } + + fn set(&self, offset: u32, val: &[u8]) -> Result<(), Error> { + let result = sandbox::memory_set( + self.handle.memory_idx, + offset, + val.as_ptr() as _, + val.len() as u32, + ); + match result { + env::ERR_OK => Ok(()), + env::ERR_OUT_OF_BOUNDS => Err(Error::OutOfBounds), + _ => unreachable!(), + } + } +} + +/// A builder for the environment of the sandboxed WASM module. +pub struct EnvironmentDefinitionBuilder { + env_def: env::EnvironmentDefinition, + retained_memories: Vec, + _marker: marker::PhantomData, +} + +impl EnvironmentDefinitionBuilder { + fn add_entry(&mut self, module: N1, field: N2, extern_entity: env::ExternEntity) + where + N1: Into>, + N2: Into>, + { + let entry = env::Entry { + module_name: module.into(), + field_name: field.into(), + entity: extern_entity, + }; + self.env_def.entries.push(entry); + } +} + +impl super::SandboxEnvironmentBuilder for EnvironmentDefinitionBuilder { + fn new() -> EnvironmentDefinitionBuilder { + EnvironmentDefinitionBuilder { + env_def: env::EnvironmentDefinition { entries: Vec::new() }, + retained_memories: Vec::new(), + _marker: marker::PhantomData::, + } + } + + fn add_host_func(&mut self, module: N1, field: N2, f: HostFuncType) + where + N1: Into>, + N2: Into>, + { + let f = env::ExternEntity::Function(f as u32); + self.add_entry(module, field, f); + } + + fn add_memory(&mut self, module: N1, field: N2, mem: Memory) + where + N1: Into>, + N2: Into>, + { + // We need to retain memory to keep it alive while the EnvironmentDefinitionBuilder alive. + self.retained_memories.push(mem.clone()); + + let mem = env::ExternEntity::Memory(mem.handle.memory_idx as u32); + self.add_entry(module, field, mem); + } +} + +/// Sandboxed instance of a WASM module. +pub struct Instance { + instance_idx: u32, + _retained_memories: Vec, + _marker: marker::PhantomData, +} + +/// The primary responsibility of this thunk is to deserialize arguments and +/// call the original function, specified by the index. +extern "C" fn dispatch_thunk( + serialized_args_ptr: *const u8, + serialized_args_len: usize, + state: usize, + f: ffi::HostFuncIndex, +) -> u64 { + let serialized_args = unsafe { + if serialized_args_len == 0 { + &[] + } else { + slice::from_raw_parts(serialized_args_ptr, serialized_args_len) + } + }; + let args = Vec::::decode(&mut &serialized_args[..]).expect( + "serialized args should be provided by the runtime; + correctly serialized data should be deserializable; + qed", + ); + + unsafe { + // This should be safe since `coerce_host_index_to_func` is called with an argument + // received in an `dispatch_thunk` implementation, so `f` should point + // on a valid host function. + let f = ffi::coerce_host_index_to_func(f); + + // This should be safe since mutable reference to T is passed upon the invocation. + let state = &mut *(state as *mut T); + + // Pass control flow to the designated function. + let result = f(state, &args).encode(); + + // Leak the result vector and return the pointer to return data. + let result_ptr = result.as_ptr() as u64; + let result_len = result.len() as u64; + mem::forget(result); + + (result_ptr << 32) | result_len + } +} + +impl super::SandboxInstance for Instance { + type Memory = Memory; + type EnvironmentBuilder = EnvironmentDefinitionBuilder; + + fn new( + code: &[u8], + env_def_builder: &EnvironmentDefinitionBuilder, + state: &mut T, + ) -> Result, Error> { + let serialized_env_def: Vec = env_def_builder.env_def.encode(); + // It's very important to instantiate thunk with the right type. + let dispatch_thunk = dispatch_thunk::; + let result = sandbox::instantiate( + dispatch_thunk as u32, + code, + &serialized_env_def, + state as *const T as _, + ); + + let instance_idx = match result { + env::ERR_MODULE => return Err(Error::Module), + env::ERR_EXECUTION => return Err(Error::Execution), + instance_idx => instance_idx, + }; + + // We need to retain memories to keep them alive while the Instance is alive. + let retained_memories = env_def_builder.retained_memories.clone(); + Ok(Instance { + instance_idx, + _retained_memories: retained_memories, + _marker: marker::PhantomData::, + }) + } + + fn invoke(&mut self, name: &str, args: &[Value], state: &mut T) -> Result { + let serialized_args = args.to_vec().encode(); + let mut return_val = vec![0u8; ReturnValue::ENCODED_MAX_SIZE]; + + let result = sandbox::invoke( + self.instance_idx, + name, + &serialized_args, + return_val.as_mut_ptr() as _, + return_val.len() as u32, + state as *const T as _, + ); + + match result { + env::ERR_OK => { + let return_val = + ReturnValue::decode(&mut &return_val[..]).map_err(|_| Error::Execution)?; + Ok(return_val) + }, + env::ERR_EXECUTION => Err(Error::Execution), + _ => unreachable!(), + } + } + + fn get_global_val(&self, name: &str) -> Option { + sandbox::get_global_val(self.instance_idx, name) + } +} + +impl Drop for Instance { + fn drop(&mut self) { + sandbox::instance_teardown(self.instance_idx); + } +} diff --git a/primitives/sandbox/src/lib.rs b/primitives/sandbox/src/lib.rs new file mode 100644 index 0000000000000..b6b4a5a97da8c --- /dev/null +++ b/primitives/sandbox/src/lib.rs @@ -0,0 +1,190 @@ +// This file is part of Substrate. + +// Copyright (C) 2018-2022 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. + +//! This crate provides means to instantiate and execute wasm modules. +//! +//! It works even when the user of this library executes from +//! inside the wasm VM. In this case the same VM is used for execution +//! of both the sandbox owner and the sandboxed module, without compromising security +//! and without the performance penalty of full wasm emulation inside wasm. +//! +//! This is achieved by using bindings to the wasm VM, which are published by the host API. +//! This API is thin and consists of only a handful functions. It contains functions for +//! instantiating modules and executing them, but doesn't contain functions for inspecting the +//! module structure. The user of this library is supposed to read the wasm module. +//! +//! When this crate is used in the `std` environment all these functions are implemented by directly +//! calling the wasm VM. +//! +//! Examples of possible use-cases for this library are not limited to the following: +//! +//! - implementing smart-contract runtimes that use wasm for contract code +//! - executing a wasm substrate runtime inside of a wasm parachain + +#![warn(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod embedded_executor; +pub mod env; +#[cfg(not(feature = "std"))] +pub mod host_executor; + +use sp_core::RuntimeDebug; +use sp_std::prelude::*; + +pub use sp_wasm_interface::{ReturnValue, Value}; + +#[cfg(not(all(feature = "wasmer-sandbox", not(feature = "std"))))] +pub use self::embedded_executor as default_executor; +pub use self::env::HostError; +#[cfg(all(feature = "wasmer-sandbox", not(feature = "std")))] +pub use self::host_executor as default_executor; + +/// The target used for logging. +const TARGET: &str = "runtime::sandbox"; + +/// Error that can occur while using this crate. +#[derive(RuntimeDebug)] +pub enum Error { + /// Module is not valid, couldn't be instantiated. + Module, + + /// Access to a memory or table was made with an address or an index which is out of bounds. + /// + /// Note that if wasm module makes an out-of-bounds access then trap will occur. + OutOfBounds, + + /// Failed to invoke the start function or an exported function for some reason. + Execution, +} + +impl From for HostError { + fn from(_e: Error) -> HostError { + HostError + } +} + +/// Function pointer for specifying functions by the +/// supervisor in [`EnvironmentDefinitionBuilder`]. +/// +/// [`EnvironmentDefinitionBuilder`]: struct.EnvironmentDefinitionBuilder.html +pub type HostFuncType = fn(&mut T, &[Value]) -> Result; + +/// Reference to a sandboxed linear memory, that +/// will be used by the guest module. +/// +/// The memory can't be directly accessed by supervisor, but only +/// through designated functions [`get`](SandboxMemory::get) and [`set`](SandboxMemory::set). +pub trait SandboxMemory: Sized + Clone { + /// Construct a new linear memory instance. + /// + /// The memory allocated with initial number of pages specified by `initial`. + /// Minimal possible value for `initial` is 0 and maximum possible is `65536`. + /// (Since maximum addressable memory is 232 = 4GiB = 65536 * 64KiB). + /// + /// It is possible to limit maximum number of pages this memory instance can have by specifying + /// `maximum`. If not specified, this memory instance would be able to allocate up to 4GiB. + /// + /// Allocated memory is always zeroed. + fn new(initial: u32, maximum: Option) -> Result; + + /// Read a memory area at the address `ptr` with the size of the provided slice `buf`. + /// + /// Returns `Err` if the range is out-of-bounds. + fn get(&self, ptr: u32, buf: &mut [u8]) -> Result<(), Error>; + + /// Write a memory area at the address `ptr` with contents of the provided slice `buf`. + /// + /// Returns `Err` if the range is out-of-bounds. + fn set(&self, ptr: u32, value: &[u8]) -> Result<(), Error>; +} + +/// Struct that can be used for defining an environment for a sandboxed module. +/// +/// The sandboxed module can access only the entities which were defined and passed +/// to the module at the instantiation time. +pub trait SandboxEnvironmentBuilder: Sized { + /// Construct a new `EnvironmentDefinitionBuilder`. + fn new() -> Self; + + /// Register a host function in this environment definition. + /// + /// NOTE that there is no constraints on type of this function. An instance + /// can import function passed here with any signature it wants. It can even import + /// the same function (i.e. with same `module` and `field`) several times. It's up to + /// the user code to check or constrain the types of signatures. + fn add_host_func(&mut self, module: N1, field: N2, f: HostFuncType) + where + N1: Into>, + N2: Into>; + + /// Register a memory in this environment definition. + fn add_memory(&mut self, module: N1, field: N2, mem: Memory) + where + N1: Into>, + N2: Into>; +} + +/// Sandboxed instance of a wasm module. +/// +/// This instance can be used for invoking exported functions. +pub trait SandboxInstance: Sized { + /// The memory type used for this sandbox. + type Memory: SandboxMemory; + + /// The environment builder used to construct this sandbox. + type EnvironmentBuilder: SandboxEnvironmentBuilder; + + /// Instantiate a module with the given [`EnvironmentDefinitionBuilder`]. It will + /// run the `start` function (if it is present in the module) with the given `state`. + /// + /// Returns `Err(Error::Module)` if this module can't be instantiated with the given + /// environment. If execution of `start` function generated a trap, then `Err(Error::Execution)` + /// will be returned. + /// + /// [`EnvironmentDefinitionBuilder`]: struct.EnvironmentDefinitionBuilder.html + fn new( + code: &[u8], + env_def_builder: &Self::EnvironmentBuilder, + state: &mut State, + ) -> Result; + + /// Invoke an exported function with the given name. + /// + /// # Errors + /// + /// Returns `Err(Error::Execution)` if: + /// + /// - An export function name isn't a proper utf8 byte sequence, + /// - This module doesn't have an exported function with the given name, + /// - If types of the arguments passed to the function doesn't match function signature then + /// trap occurs (as if the exported function was called via call_indirect), + /// - Trap occurred at the execution time. + fn invoke( + &mut self, + name: &str, + args: &[Value], + state: &mut State, + ) -> Result; + + /// Get the value from a global with the given `name`. + /// + /// Returns `Some(_)` if the global could be found. + fn get_global_val(&self, name: &str) -> Option; +} diff --git a/primitives/wasm-interface/src/lib.rs b/primitives/wasm-interface/src/lib.rs index 1ecff5a0ce91e..173e3241170fc 100644 --- a/primitives/wasm-interface/src/lib.rs +++ b/primitives/wasm-interface/src/lib.rs @@ -303,6 +303,9 @@ pub trait FunctionContext { fn allocate_memory(&mut self, size: WordSize) -> Result>; /// Deallocate a given memory instance. fn deallocate_memory(&mut self, ptr: Pointer) -> Result<()>; + /// Provides access to the sandbox. + fn sandbox(&mut self) -> &mut dyn Sandbox; + /// Registers a panic error message within the executor. /// /// This is meant to be used in situations where the runtime @@ -327,6 +330,60 @@ pub trait FunctionContext { fn register_panic_error_message(&mut self, message: &str); } +/// Sandbox memory identifier. +pub type MemoryId = u32; + +/// Something that provides access to the sandbox. +pub trait Sandbox { + /// Get sandbox memory from the `memory_id` instance at `offset` into the given buffer. + fn memory_get( + &mut self, + memory_id: MemoryId, + offset: WordSize, + buf_ptr: Pointer, + buf_len: WordSize, + ) -> Result; + /// Set sandbox memory from the given value. + fn memory_set( + &mut self, + memory_id: MemoryId, + offset: WordSize, + val_ptr: Pointer, + val_len: WordSize, + ) -> Result; + /// Delete a memory instance. + fn memory_teardown(&mut self, memory_id: MemoryId) -> Result<()>; + /// Create a new memory instance with the given `initial` size and the `maximum` size. + /// The size is given in wasm pages. + fn memory_new(&mut self, initial: u32, maximum: u32) -> Result; + /// Invoke an exported function by a name. + fn invoke( + &mut self, + instance_id: u32, + export_name: &str, + args: &[u8], + return_val: Pointer, + return_val_len: WordSize, + state: u32, + ) -> Result; + /// Delete a sandbox instance. + fn instance_teardown(&mut self, instance_id: u32) -> Result<()>; + /// Create a new sandbox instance. + fn instance_new( + &mut self, + dispatch_thunk_id: u32, + wasm: &[u8], + raw_env_def: &[u8], + state: u32, + ) -> Result; + + /// Get the value from a global with the given `name`. The sandbox is determined by the + /// given `instance_idx` instance. + /// + /// Returns `Some(_)` when the requested global variable could be found. + fn get_global_val(&self, instance_idx: u32, name: &str) -> Result>; +} + if_wasmtime_is_enabled! { /// A trait used to statically register host callbacks with the WASM executor, /// so that they call be called from within the runtime with minimal overhead. diff --git a/scripts/ci/gitlab/pipeline/test.yml b/scripts/ci/gitlab/pipeline/test.yml index a468a7b04caeb..accae462cd99b 100644 --- a/scripts/ci/gitlab/pipeline/test.yml +++ b/scripts/ci/gitlab/pipeline/test.yml @@ -144,7 +144,7 @@ cargo-check-try-runtime: - time cargo check --locked --features try-runtime - rusty-cachier cache upload -test-deterministic-wasm: +cargo-check-wasmer-sandbox: stage: test # this is an artificial job dependency, for pipeline optimization using GitLab's DAGs needs: @@ -153,6 +153,20 @@ test-deterministic-wasm: extends: - .docker-env - .test-refs + script: + - rusty-cachier snapshot create + - time cargo check --locked --features wasmer-sandbox + - rusty-cachier cache upload + +test-deterministic-wasm: + stage: test + # this is an artificial job dependency, for pipeline optimization using GitLab's DAGs + needs: + - job: cargo-check-wasmer-sandbox + artifacts: false + extends: + - .docker-env + - .test-refs variables: WASM_BUILD_NO_COLOR: 1 # this variable gets overriden by "rusty-cachier environment inject", use the value as default @@ -362,6 +376,27 @@ test-full-crypto-feature: - time cargo +nightly build --locked --verbose --no-default-features --features full_crypto - rusty-cachier cache upload +test-wasmer-sandbox: + stage: test + needs: + - job: cargo-check-wasmer-sandbox + artifacts: false + extends: + - .docker-env + - .test-refs-wasmer-sandbox + variables: + RUSTFLAGS: "-Cdebug-assertions=y -Dwarnings" + RUST_BACKTRACE: 1 + WASM_BUILD_NO_COLOR: 1 + WASM_BUILD_RUSTFLAGS: "-Cdebug-assertions=y -Dwarnings" + CI_JOB_NAME: "test-wasmer-sandbox" + parallel: 3 + script: + - rusty-cachier snapshot create + - echo "Node index - ${CI_NODE_INDEX}. Total amount - ${CI_NODE_TOTAL}" + - time cargo nextest run --locked --release --features runtime-benchmarks,wasmer-sandbox,disable-ui-tests --partition count:${CI_NODE_INDEX}/${CI_NODE_TOTAL} + - if [ ${CI_NODE_INDEX} == 1 ]; then rusty-cachier cache upload; fi + check-rustdoc: stage: test variables: