Improve sandbox internal api (#9709)

* Improve sandbox internal api

This improves the internal sandbox api for the executor implementations.
The main point is to hide the tls in the internal api and not having it
exposed to the outside.

This is especially needed for wasmtime 0.29.0

* Fmt

* Make it nicer
This commit is contained in:
Bastian Köcher
2021-09-08 12:28:23 +02:00
committed by GitHub
parent 8cf6474388
commit b382cc8f9d
6 changed files with 579 additions and 697 deletions
+255 -284
View File
@@ -25,7 +25,7 @@ use log::trace;
use sc_allocator::FreeingBumpHeapAllocator;
use sc_executor_common::{
error::Result,
sandbox::{self, SandboxCapabilities, SandboxCapabilitiesHolder, SupervisorFuncIndex},
sandbox::{self, SupervisorFuncIndex},
util::MemoryTransfer,
};
use sp_core::sandbox as sandbox_primitives;
@@ -33,32 +33,20 @@ use sp_wasm_interface::{FunctionContext, MemoryId, Pointer, Sandbox, WordSize};
use std::{cell::RefCell, rc::Rc};
use wasmtime::{Func, Val};
/// Wrapper type for pointer to a Wasm table entry.
///
/// The wrapper type is used to ensure that the function reference is valid as it must be unsafely
/// dereferenced from within the safe method `<HostContext as SandboxCapabilities>::invoke`.
#[derive(Clone)]
pub struct SupervisorFuncRef(Func);
/// The state required to construct a HostContext context. The context only lasts for one host
/// call, whereas the state is maintained for the duration of a Wasm runtime call, which may make
/// many different host calls that must share state.
#[derive(Clone)]
pub struct HostState {
inner: Rc<Inner>,
}
struct Inner {
// We need some interior mutability here since the host state is shared between all host
// function handlers and the wasmtime backend's `impl WasmRuntime`.
//
// Furthermore, because of recursive calls (e.g. runtime can create and call an sandboxed
// instance which in turn can call the runtime back) we have to be very careful with borrowing
// those.
//
// Basically, most of the interactions should do temporary borrow immediately releasing the
// borrow after performing necessary queries/changes.
sandbox_store: RefCell<sandbox::Store<SupervisorFuncRef>>,
/// We need some interior mutability here since the host state is shared between all host
/// function handlers and the wasmtime backend's `impl WasmRuntime`.
///
/// Furthermore, because of recursive calls (e.g. runtime can create and call an sandboxed
/// instance which in turn can call the runtime back) we have to be very careful with borrowing
/// those.
///
/// Basically, most of the interactions should do temporary borrow immediately releasing the
/// borrow after performing necessary queries/changes.
sandbox_store: Rc<RefCell<sandbox::Store<Func>>>,
allocator: RefCell<FreeingBumpHeapAllocator>,
instance: Rc<InstanceWrapper>,
}
@@ -67,29 +55,260 @@ impl HostState {
/// Constructs a new `HostState`.
pub fn new(allocator: FreeingBumpHeapAllocator, instance: Rc<InstanceWrapper>) -> Self {
HostState {
inner: Rc::new(Inner {
sandbox_store: RefCell::new(sandbox::Store::new(
sandbox::SandboxBackend::TryWasmer,
)),
allocator: RefCell::new(allocator),
instance,
}),
sandbox_store: Rc::new(RefCell::new(sandbox::Store::new(
sandbox::SandboxBackend::TryWasmer,
))),
allocator: RefCell::new(allocator),
instance,
}
}
/// Materialize `HostContext` that can be used to invoke a substrate host `dyn Function`.
pub fn materialize<'a>(&'a self) -> HostContext<'a> {
HostContext(self)
}
}
impl SandboxCapabilities for HostState {
type SupervisorFuncRef = SupervisorFuncRef;
/// A `HostContext` implements `FunctionContext` for making host calls from a Wasmtime
/// runtime. The `HostContext` exists only for the lifetime of the call and borrows state from
/// a longer-living `HostState`.
pub struct HostContext<'a>(&'a HostState);
impl<'a> std::ops::Deref for HostContext<'a> {
type Target = HostState;
fn deref(&self) -> &HostState {
self.0
}
}
impl<'a> sp_wasm_interface::FunctionContext for HostContext<'a> {
fn read_memory_into(
&self,
address: Pointer<u8>,
dest: &mut [u8],
) -> sp_wasm_interface::Result<()> {
self.instance.read_memory_into(address, dest).map_err(|e| e.to_string())
}
fn write_memory(&mut self, address: Pointer<u8>, data: &[u8]) -> sp_wasm_interface::Result<()> {
self.instance.write_memory_from(address, data).map_err(|e| e.to_string())
}
fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result<Pointer<u8>> {
self.instance
.allocate(&mut *self.allocator.borrow_mut(), size)
.map_err(|e| e.to_string())
}
fn deallocate_memory(&mut self, ptr: Pointer<u8>) -> sp_wasm_interface::Result<()> {
self.instance
.deallocate(&mut *self.allocator.borrow_mut(), ptr)
.map_err(|e| e.to_string())
}
fn sandbox(&mut self) -> &mut dyn Sandbox {
self
}
}
impl<'a> Sandbox for HostContext<'a> {
fn memory_get(
&mut self,
memory_id: MemoryId,
offset: WordSize,
buf_ptr: Pointer<u8>,
buf_len: WordSize,
) -> sp_wasm_interface::Result<u32> {
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_primitives::ERR_OUT_OF_BOUNDS),
Ok(buffer) => buffer,
};
if let Err(_) = self.instance.write_memory_from(buf_ptr, &buffer) {
return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS)
}
Ok(sandbox_primitives::ERR_OK)
}
fn memory_set(
&mut self,
memory_id: MemoryId,
offset: WordSize,
val_ptr: Pointer<u8>,
val_len: WordSize,
) -> sp_wasm_interface::Result<u32> {
let sandboxed_memory =
self.sandbox_store.borrow().memory(memory_id).map_err(|e| e.to_string())?;
let len = val_len as usize;
let buffer = match self.instance.read_memory(val_ptr, len) {
Err(_) => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
Ok(buffer) => buffer,
};
if let Err(_) = sandboxed_memory.write_from(Pointer::new(offset as u32), &buffer) {
return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS)
}
Ok(sandbox_primitives::ERR_OK)
}
fn memory_teardown(&mut self, memory_id: MemoryId) -> sp_wasm_interface::Result<()> {
self.sandbox_store
.borrow_mut()
.memory_teardown(memory_id)
.map_err(|e| e.to_string())
}
fn memory_new(&mut self, initial: u32, maximum: u32) -> sp_wasm_interface::Result<u32> {
self.sandbox_store
.borrow_mut()
.new_memory(initial, maximum)
.map_err(|e| e.to_string())
}
fn invoke(
&mut self,
dispatch_thunk: &Self::SupervisorFuncRef,
instance_id: u32,
export_name: &str,
args: &[u8],
return_val: Pointer<u8>,
return_val_len: u32,
state: u32,
) -> sp_wasm_interface::Result<u32> {
trace!(target: "sp-sandbox", "invoke, instance_idx={}", instance_id);
// Deserialize arguments and convert them into wasmi types.
let args = Vec::<sp_wasm_interface::Value>::decode(&mut &args[..])
.map_err(|_| "Can't decode serialized arguments for the invocation")?
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
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())?;
let result = instance.invoke(
export_name,
&args,
state,
&mut SandboxContext { host_context: self, dispatch_thunk },
);
match result {
Ok(None) => Ok(sandbox_primitives::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 {
Err("Return value buffer is too small")?;
}
<HostContext as FunctionContext>::write_memory(self, return_val, val)
.map_err(|_| "can't write return value")?;
Ok(sandbox_primitives::ERR_OK)
})
},
Err(_) => Ok(sandbox_primitives::ERR_EXECUTION),
}
}
fn instance_teardown(&mut self, instance_id: u32) -> sp_wasm_interface::Result<()> {
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,
) -> sp_wasm_interface::Result<u32> {
// Extract a dispatch thunk from the instance's table by the specified index.
let dispatch_thunk = {
let table_item = self
.instance
.table()
.as_ref()
.ok_or_else(|| "Runtime doesn't have a table; sandbox is unavailable")?
.get(dispatch_thunk_id);
table_item
.ok_or_else(|| "dispatch_thunk_id is out of bounds")?
.funcref()
.ok_or_else(|| "dispatch_thunk_idx should be a funcref")?
.ok_or_else(|| "dispatch_thunk_idx should point to actual func")?
.clone()
};
let guest_env =
match sandbox::GuestEnvironment::decode(&*self.sandbox_store.borrow(), raw_env_def) {
Ok(guest_env) => guest_env,
Err(_) => return Ok(sandbox_primitives::ERR_MODULE as u32),
};
let store = self.sandbox_store.clone();
let store = &mut store.borrow_mut();
let result = store
.instantiate(
wasm,
guest_env,
state,
&mut SandboxContext { host_context: self, dispatch_thunk: dispatch_thunk.clone() },
)
.map(|i| i.register(store, dispatch_thunk));
let instance_idx_or_err_code = match result {
Ok(instance_idx) => instance_idx,
Err(sandbox::InstantiationError::StartTrapped) => sandbox_primitives::ERR_EXECUTION,
Err(_) => sandbox_primitives::ERR_MODULE,
};
Ok(instance_idx_or_err_code as u32)
}
fn get_global_val(
&self,
instance_idx: u32,
name: &str,
) -> sp_wasm_interface::Result<Option<sp_wasm_interface::Value>> {
self.sandbox_store
.borrow()
.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<u8>,
invoke_args_len: WordSize,
state: u32,
func_idx: SupervisorFuncIndex,
) -> Result<i64> {
let result = dispatch_thunk.0.call(&[
let result = self.dispatch_thunk.call(&[
Val::I32(u32::from(invoke_args_ptr) as i32),
Val::I32(invoke_args_len as i32),
Val::I32(state as i32),
@@ -116,256 +335,8 @@ impl SandboxCapabilities for HostState {
Err(err) => Err(err.to_string().into()),
}
}
}
impl sp_wasm_interface::FunctionContext for HostState {
fn read_memory_into(
&self,
address: Pointer<u8>,
dest: &mut [u8],
) -> sp_wasm_interface::Result<()> {
self.inner.instance.read_memory_into(address, dest).map_err(|e| e.to_string())
}
fn write_memory(&mut self, address: Pointer<u8>, data: &[u8]) -> sp_wasm_interface::Result<()> {
self.inner.instance.write_memory_from(address, data).map_err(|e| e.to_string())
}
fn allocate_memory(&mut self, size: WordSize) -> sp_wasm_interface::Result<Pointer<u8>> {
self.inner
.instance
.allocate(&mut *self.inner.allocator.borrow_mut(), size)
.map_err(|e| e.to_string())
}
fn deallocate_memory(&mut self, ptr: Pointer<u8>) -> sp_wasm_interface::Result<()> {
self.inner
.instance
.deallocate(&mut *self.inner.allocator.borrow_mut(), ptr)
.map_err(|e| e.to_string())
}
fn sandbox(&mut self) -> &mut dyn Sandbox {
self
}
}
impl Sandbox for HostState {
fn memory_get(
&mut self,
memory_id: MemoryId,
offset: WordSize,
buf_ptr: Pointer<u8>,
buf_len: WordSize,
) -> sp_wasm_interface::Result<u32> {
let sandboxed_memory =
self.inner.sandbox_store.borrow().memory(memory_id).map_err(|e| e.to_string())?;
let len = buf_len as usize;
let buffer = match sandboxed_memory.read(Pointer::new(offset as u32), len) {
Err(_) => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
Ok(buffer) => buffer,
};
if let Err(_) = self.inner.instance.write_memory_from(buf_ptr, &buffer) {
return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS)
}
Ok(sandbox_primitives::ERR_OK)
}
fn memory_set(
&mut self,
memory_id: MemoryId,
offset: WordSize,
val_ptr: Pointer<u8>,
val_len: WordSize,
) -> sp_wasm_interface::Result<u32> {
let sandboxed_memory =
self.inner.sandbox_store.borrow().memory(memory_id).map_err(|e| e.to_string())?;
let len = val_len as usize;
let buffer = match self.inner.instance.read_memory(val_ptr, len) {
Err(_) => return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS),
Ok(buffer) => buffer,
};
if let Err(_) = sandboxed_memory.write_from(Pointer::new(offset as u32), &buffer) {
return Ok(sandbox_primitives::ERR_OUT_OF_BOUNDS)
}
Ok(sandbox_primitives::ERR_OK)
}
fn memory_teardown(&mut self, memory_id: MemoryId) -> sp_wasm_interface::Result<()> {
self.inner
.sandbox_store
.borrow_mut()
.memory_teardown(memory_id)
.map_err(|e| e.to_string())
}
fn memory_new(&mut self, initial: u32, maximum: u32) -> sp_wasm_interface::Result<u32> {
self.inner
.sandbox_store
.borrow_mut()
.new_memory(initial, maximum)
.map_err(|e| e.to_string())
}
fn invoke(
&mut self,
instance_id: u32,
export_name: &str,
args: &[u8],
return_val: Pointer<u8>,
return_val_len: u32,
state: u32,
) -> sp_wasm_interface::Result<u32> {
trace!(target: "sp-sandbox", "invoke, instance_idx={}", instance_id);
// Deserialize arguments and convert them into wasmi types.
let args = Vec::<sp_wasm_interface::Value>::decode(&mut &args[..])
.map_err(|_| "Can't decode serialized arguments for the invocation")?
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
let instance = self
.inner
.sandbox_store
.borrow()
.instance(instance_id)
.map_err(|e| e.to_string())?;
let result = instance.invoke::<_, CapsHolder, ThunkHolder>(export_name, &args, state);
match result {
Ok(None) => Ok(sandbox_primitives::ERR_OK),
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 {
Err("Return value buffer is too small")?;
}
<HostState as FunctionContext>::write_memory(self, return_val, val)
.map_err(|_| "can't write return value")?;
Ok(sandbox_primitives::ERR_OK)
})
},
Err(_) => Ok(sandbox_primitives::ERR_EXECUTION),
}
}
fn instance_teardown(&mut self, instance_id: u32) -> sp_wasm_interface::Result<()> {
self.inner
.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,
) -> sp_wasm_interface::Result<u32> {
// Extract a dispatch thunk from the instance's table by the specified index.
let dispatch_thunk = {
let table_item = self
.inner
.instance
.table()
.as_ref()
.ok_or_else(|| "Runtime doesn't have a table; sandbox is unavailable")?
.get(dispatch_thunk_id);
let func_ref = table_item
.ok_or_else(|| "dispatch_thunk_id is out of bounds")?
.funcref()
.ok_or_else(|| "dispatch_thunk_idx should be a funcref")?
.ok_or_else(|| "dispatch_thunk_idx should point to actual func")?
.clone();
SupervisorFuncRef(func_ref)
};
let guest_env = match sandbox::GuestEnvironment::decode(
&*self.inner.sandbox_store.borrow(),
raw_env_def,
) {
Ok(guest_env) => guest_env,
Err(_) => return Ok(sandbox_primitives::ERR_MODULE as u32),
};
let store = &mut *self.inner.sandbox_store.borrow_mut();
let result = DISPATCH_THUNK.set(&dispatch_thunk, || {
store
.instantiate::<_, CapsHolder, ThunkHolder>(wasm, guest_env, state)
.map(|i| i.register(store))
});
let instance_idx_or_err_code = match result {
Ok(instance_idx) => instance_idx,
Err(sandbox::InstantiationError::StartTrapped) => sandbox_primitives::ERR_EXECUTION,
Err(_) => sandbox_primitives::ERR_MODULE,
};
Ok(instance_idx_or_err_code as u32)
}
fn get_global_val(
&self,
instance_idx: u32,
name: &str,
) -> sp_wasm_interface::Result<Option<sp_wasm_interface::Value>> {
self.inner
.sandbox_store
.borrow()
.instance(instance_idx)
.map(|i| i.get_global_val(name))
.map_err(|e| e.to_string())
}
}
/// Wasmtime specific implementation of `SandboxCapabilitiesHolder` that provides
/// sandbox with a scoped thread local access to a function executor.
/// This is a way to calm down the borrow checker since host function closures
/// require exclusive access to it.
struct CapsHolder;
impl SandboxCapabilitiesHolder for CapsHolder {
type SupervisorFuncRef = SupervisorFuncRef;
type SC = HostState;
fn with_sandbox_capabilities<R, F: FnOnce(&mut Self::SC) -> R>(f: F) -> R {
crate::state_holder::with_context(|ctx| f(&mut ctx.expect("wasmtime executor is not set")))
}
}
/// Wasmtime specific implementation of `DispatchThunkHolder` that provides
/// sandbox with a scoped thread local access to a dispatch thunk.
/// This is a way to calm down the borrow checker since host function closures
/// require exclusive access to it.
struct ThunkHolder;
scoped_tls::scoped_thread_local!(static DISPATCH_THUNK: SupervisorFuncRef);
impl sandbox::DispatchThunkHolder for ThunkHolder {
type DispatchThunk = SupervisorFuncRef;
fn with_dispatch_thunk<R, F: FnOnce(&mut Self::DispatchThunk) -> R>(f: F) -> R {
assert!(DISPATCH_THUNK.is_set(), "dispatch thunk is not set");
DISPATCH_THUNK.with(|thunk| f(&mut thunk.clone()))
}
fn initialize_thunk<R, F>(s: &Self::DispatchThunk, f: F) -> R
where
F: FnOnce() -> R,
{
DISPATCH_THUNK.set(s, f)
fn supervisor_context(&mut self) -> &mut dyn FunctionContext {
self.host_context
}
}
@@ -16,7 +16,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
use crate::host::HostState;
use crate::host::{HostContext, HostState};
scoped_tls::scoped_thread_local!(static HOST_STATE: HostState);
@@ -36,10 +36,10 @@ where
/// context will be `None`.
pub fn with_context<R, F>(f: F) -> R
where
F: FnOnce(Option<HostState>) -> R,
F: FnOnce(Option<HostContext>) -> R,
{
if !HOST_STATE.is_set() {
return f(None)
}
HOST_STATE.with(|state| f(Some(state.clone())))
HOST_STATE.with(|state| f(Some(state.materialize())))
}