Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions crates/engine/src/wasm/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ impl<T> WasmEnv<T> {
self.in_template_invocation = false;
}

/// Closes the window for a call into template code the engine drives from inside an invocation,
/// returning its previous state. Pair with [`Self::restore_template_invocation`] so the window
/// is put back as it was rather than opened.
pub(super) fn suspend_template_invocation(&mut self) -> bool {
let was_open = self.in_template_invocation;
self.in_template_invocation = false;
was_open
}

pub(super) fn restore_template_invocation(&mut self, was_open: bool) {
self.in_template_invocation = was_open;
}

pub(super) fn is_in_template_invocation(&self) -> bool {
self.in_template_invocation
}
Expand Down Expand Up @@ -160,15 +173,6 @@ impl<T> WasmEnv<T> {
self.last_panic = Some(message);
}

pub(super) fn alloc<S: AsStoreMut>(&self, store: &mut S, len: u32) -> Result<WasmPtr<u8>, WasmExecutionError> {
let ptr = self.mem_alloc_func()?.call(store, len)?;
if ptr.is_null() {
return Err(WasmExecutionError::MemoryAllocationFailed);
}

Ok(ptr)
}

/// Hands out the template's `tari_alloc` as an owned handle, for callers that must let go of
/// their borrow of this environment before calling it. `tari_alloc` is template code, and
/// template code can call `tari_engine`, which takes its own `&mut` to this environment.
Expand Down
231 changes: 123 additions & 108 deletions crates/engine/src/wasm/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,7 @@ use tari_template_lib::{
},
types::{LogLevel, engine_args::SignatureInvokeArg},
};
use wasmer::{
AsStoreMut,
AsStoreRef,
Function,
FunctionEnv,
FunctionEnvMut,
Instance,
Store,
StoreMut,
WasmPtr,
imports,
};
use wasmer::{AsStoreMut, AsStoreRef, Function, FunctionEnv, FunctionEnvMut, Instance, Store, WasmPtr, imports};
use wasmer_middlewares::metering::{MeteringPoints, get_remaining_points, set_remaining_points};

use crate::{
Expand Down Expand Up @@ -244,123 +233,108 @@ impl WasmProcess {
return WasmPtr::null();
}

let (env_mut, mut store) = env.data_and_store_mut();

// Only a template function invocation may call the engine. The engine also enters WASM to
// run `tari_alloc`/`tari_free`, which happens outside any invocation: an engine call made
// from there would mutate state and emit effects that no invocation is metered or charged
// for. `WasmProcess::alloc_checked`/`free_checked` turn the null returned here into the
// recorded refusal, so a template that ignores the null cannot proceed either.
if !env_mut.is_in_template_invocation() {
env_mut.set_refused_engine_call(op);
return WasmPtr::null();
}

// Sync this invocation's in-flight meter consumption onto the transaction total before
// dispatching, so budget and allowance checks made inside the host call (native
// verification pre-charges, nested cross-template call budgets) see it. Without this, a
// call could spend its whole metering allowance and still pass mid-call checks that read
// the stale end-of-invocation total.
if let Some(delta) = env_mut.take_unsynced_in_flight_points(&mut store) &&
let Err(err) = env_mut.state_mut().interface_mut().record_wasm_execution(delta)
{
env_mut.set_last_engine_error(err);
return WasmPtr::null();
let (env_mut, mut store) = env.data_and_store_mut();

// Only a template function invocation may call the engine. The engine also enters WASM
// to run `tari_alloc`/`tari_free` — staging a `CallInfo`, writing an engine call's
// response, releasing a returned value — and that template code runs outside any
// invocation. `WasmProcess::alloc_checked`/`free_checked` and `Self::handle` turn the
// null returned here into the recorded refusal, so a template that ignores the null
// cannot proceed either.
if !env_mut.is_in_template_invocation() {
env_mut.set_refused_engine_call(op);
return WasmPtr::null();
}

// Sync this invocation's in-flight meter consumption onto the transaction total before
// dispatching, so budget and allowance checks made inside the host call (native
// verification pre-charges, nested cross-template call budgets) see it. Without this, a
// call could spend its whole metering allowance and still pass mid-call checks that
// read the stale end-of-invocation total.
if let Some(delta) = env_mut.take_unsynced_in_flight_points(&mut store) &&
let Err(err) = env_mut.state_mut().interface_mut().record_wasm_execution(delta)
{
env_mut.set_last_engine_error(err);
return WasmPtr::null();
}
}

log::debug!(target: LOG_TARGET, "Engine call: {:?}", op);

let result = match op {
EngineOp::EmitLog => Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: EmitLogArg| {
EngineOp::EmitLog => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: EmitLogArg| {
state.interface_mut().emit_log(arg.level, arg.message)
}),
EngineOp::ComponentInvoke => {
Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: ComponentInvokeArg| {
state
.interface_mut()
.component_invoke(arg.component_ref, arg.action, arg.args.into())
})
},
EngineOp::ResourceInvoke => {
Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: ResourceInvokeArg| {
state
.interface_mut()
.resource_invoke(arg.resource_ref, arg.action, arg.args.into())
})
},
EngineOp::VaultInvoke => Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: VaultInvokeArg| {
EngineOp::ComponentInvoke => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: ComponentInvokeArg| {
state
.interface_mut()
.component_invoke(arg.component_ref, arg.action, arg.args.into())
}),
EngineOp::ResourceInvoke => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: ResourceInvokeArg| {
state
.interface_mut()
.resource_invoke(arg.resource_ref, arg.action, arg.args.into())
}),
EngineOp::VaultInvoke => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: VaultInvokeArg| {
state
.interface_mut()
.vault_invoke(arg.vault_ref, arg.action, arg.args.into())
}),
EngineOp::BucketInvoke => Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: BucketInvokeArg| {
EngineOp::BucketInvoke => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: BucketInvokeArg| {
state
.interface_mut()
.bucket_invoke(arg.bucket_ref, arg.action, arg.args.into())
}),
EngineOp::NonFungibleInvoke => {
Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: NonFungibleInvokeArg| {
Self::handle(&mut env, arg_ptr, arg_len, |state, arg: NonFungibleInvokeArg| {
state
.interface_mut()
.non_fungible_invoke(arg.address, arg.action, arg.args.into())
})
},
EngineOp::GenerateUniqueId => Self::handle(store, env_mut, arg_ptr, arg_len, |state, _arg: ()| {
EngineOp::GenerateUniqueId => Self::handle(&mut env, arg_ptr, arg_len, |state, _arg: ()| {
state.interface_mut().generate_uuid()
}),
EngineOp::ConsensusInvoke => {
Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: ConsensusInvokeArg| {
state.interface_mut().consensus_invoke(arg.action)
EngineOp::ConsensusInvoke => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: ConsensusInvokeArg| {
state.interface_mut().consensus_invoke(arg.action)
}),
EngineOp::CallerContextInvoke => {
Self::handle(&mut env, arg_ptr, arg_len, |state, arg: CallerContextInvokeArg| {
state.interface_mut().caller_context_invoke(arg.action, arg.args.into())
})
},
EngineOp::CallerContextInvoke => Self::handle(
store,
env_mut,
arg_ptr,
arg_len,
|state, arg: CallerContextInvokeArg| {
state.interface_mut().caller_context_invoke(arg.action, arg.args.into())
},
),
EngineOp::AddressAllocationInvoke => Self::handle(
store,
env_mut,
arg_ptr,
arg_len,
|state, arg: AddressAllocationInvokeArg| state.interface_mut().allocate_address_invoke(arg),
),
EngineOp::GenerateRandomInvoke => Self::handle(
store,
env_mut,
arg_ptr,
arg_len,
|state, arg: GenerateRandomInvokeArg| state.interface_mut().generate_random_invoke(arg.action),
),
EngineOp::EmitEvent => Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: EmitEventArg| {
EngineOp::AddressAllocationInvoke => {
Self::handle(&mut env, arg_ptr, arg_len, |state, arg: AddressAllocationInvokeArg| {
state.interface_mut().allocate_address_invoke(arg)
})
},
EngineOp::GenerateRandomInvoke => {
Self::handle(&mut env, arg_ptr, arg_len, |state, arg: GenerateRandomInvokeArg| {
state.interface_mut().generate_random_invoke(arg.action)
})
},
EngineOp::EmitEvent => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: EmitEventArg| {
state.interface_mut().emit_event(arg.topic, arg.payload)
}),
EngineOp::CallInvoke => Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: CallInvokeArg| {
EngineOp::CallInvoke => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: CallInvokeArg| {
state.interface_mut().call_invoke(arg.action, arg.args.into())
}),
EngineOp::ProofInvoke => Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: ProofInvokeArg| {
EngineOp::ProofInvoke => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: ProofInvokeArg| {
state
.interface_mut()
.proof_invoke(arg.proof_ref, arg.action, arg.args.into())
}),
EngineOp::BuiltinTemplateInvoke => Self::handle(
store,
env_mut,
arg_ptr,
arg_len,
|state, arg: BuiltinTemplateInvokeArg| state.interface_mut().builtin_template_invoke(arg.action),
),
EngineOp::SignatureInvoke => {
Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: SignatureInvokeArg| {
state.interface_mut().signature_invoke(arg.action, arg.args.into())
EngineOp::BuiltinTemplateInvoke => {
Self::handle(&mut env, arg_ptr, arg_len, |state, arg: BuiltinTemplateInvokeArg| {
state.interface_mut().builtin_template_invoke(arg.action)
})
},
EngineOp::SignatureInvoke => Self::handle(&mut env, arg_ptr, arg_len, |state, arg: SignatureInvokeArg| {
state.interface_mut().signature_invoke(arg.action, arg.args.into())
}),
EngineOp::SpendContextInvoke => {
Self::handle(store, env_mut, arg_ptr, arg_len, |state, arg: SpendContextInvokeArg| {
Self::handle(&mut env, arg_ptr, arg_len, |state, arg: SpendContextInvokeArg| {
state.interface_mut().spend_context_invoke(arg.action)
})
},
Expand All @@ -384,9 +358,8 @@ impl WasmProcess {
})
}

pub fn handle<T, U, E>(
mut store: StoreMut,
env_mut: &mut WasmEnv<Runtime>,
fn handle<T, U, E>(
env: &mut FunctionEnvMut<WasmEnv<Runtime>>,
arg_ptr: WasmPtr<u8>,
arg_len: u32,
f: fn(&mut Runtime, T) -> Result<U, E>,
Expand All @@ -396,24 +369,58 @@ impl WasmProcess {
U: tari_bor::Encode<()> + tari_bor::CborLen<()>,
WasmExecutionError: From<E>,
{
// SAFETY: WasmProcess is not used concurrently and templates are not able to spawn threads
let decoded = unsafe {
env_mut.with_memory_slice(&mut store, arg_ptr, arg_len, |arg| {
decode_exact(arg).map_err(|e| {
log::error!(target: LOG_TARGET, "Failed to decode args for engine call: {}", e);
WasmExecutionError::EngineArgDecodeFailed(e)
let decoded = {
let (env_mut, mut store) = env.data_and_store_mut();
// SAFETY: WasmProcess is not used concurrently and templates are not able to spawn threads
unsafe {
env_mut.with_memory_slice(&mut store, arg_ptr, arg_len, |arg| {
decode_exact(arg).map_err(|e| {
log::error!(target: LOG_TARGET, "Failed to decode args for engine call: {}", e);
WasmExecutionError::EngineArgDecodeFailed(e)
})
})
})
}??;
let resp = f(env_mut.state_mut(), decoded)?;
}??
};
let resp = f(env.data_mut().state_mut(), decoded)?;
let len = encoded_len(&resp)?;
let ptr = env_mut.alloc(&mut store, len as u32)?;
let ptr = Self::alloc_response(env, len)?;

// Encode response directly into the WASM memory. The WASM code is responsible for freeing it.
let (env_mut, mut store) = env.data_and_store_mut();
let mut writer = env_mut.memory_writer(&mut store, ptr)?;
encode_into_writer(&resp, &mut writer)?;
Ok(ptr)
}

/// Allocates room for an engine call's response through the template's own `tari_alloc`.
///
/// Servicing an engine call therefore runs template code, which is closed out of the invocation
/// window for the duration: a `tari_alloc` that calls the engine would otherwise cycle
/// host -> WASM -> host once per response and exhaust the native stack. Nothing bounds that
/// cycle — it is one call frame, so `max_call_depth` does not see it, and the per-call metering
/// ceiling permits far more rounds than the stack survives.
///
/// The environment is left unborrowed across the call, since a refusal is recorded through the
/// engine's own `&mut` to it.
fn alloc_response(
env: &mut FunctionEnvMut<WasmEnv<Runtime>>,
len: usize,
) -> Result<WasmPtr<u8>, WasmExecutionError> {
let len = u32::try_from(len).map_err(|_| WasmExecutionError::MemoryAllocationTooLarge)?;
let alloc_fn = env.data().mem_alloc_func()?;

let was_open = env.data_mut().suspend_template_invocation();
let result = alloc_fn.call(&mut *env, len);
env.data_mut().restore_template_invocation(was_open);

take_refused_engine_call(env.data_mut())?;
let ptr = result?;
if ptr.is_null() {
return Err(WasmExecutionError::MemoryAllocationFailed);
}
Ok(ptr)
}

/// Determine if the version of the template_lib crate in the WASM is valid.
pub fn validate_template_abi_version(template_def: &TemplateDef) -> Result<(), WasmExecutionError> {
let template_abi_ver = template_def.abi_version();
Expand Down Expand Up @@ -498,6 +505,17 @@ impl Invokable<Store> for WasmProcess {
.interface_mut()
.record_wasm_execution(points_consumed.saturating_sub(already_synced))?;

// An engine error recorded during the invocation fails the call on both paths.
// `tari_engine_entrypoint` can only answer a failed call with a null pointer, and a
// template is free to ignore that and return normally, so the trap path alone is not enough
// to catch it.
if let Some(err) = self.env_mut(store).take_last_engine_error() {
return Err(WasmExecutionError::RuntimeError(err));
}
// Every site that closes the window drains its own refusal before returning, so this
// catches only a site that is later added without one.
take_refused_engine_call(self.env_mut(store))?;

match res {
Ok(return_ptr) => {
// Read response from memory
Expand All @@ -523,9 +541,6 @@ impl Invokable<Store> for WasmProcess {
})
},
Err(err) => {
if let Some(err) = self.env_mut(store).take_last_engine_error() {
return Err(WasmExecutionError::RuntimeError(err));
}
if let Some(message) = self.env_mut(store).take_last_panic_message() {
return Err(WasmExecutionError::Panic {
message,
Expand Down
1 change: 1 addition & 0 deletions crates/engine/tests/templates/buggy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ return_empty_abi = []
unexpected_export_function = []
engine_call_in_alloc = []
engine_call_in_free = []
engine_call_in_response_alloc = []
Loading
Loading