Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
9 changes: 0 additions & 9 deletions crates/engine/src/wasm/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,15 +160,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
227 changes: 120 additions & 107 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 @@ -385,8 +359,7 @@ impl WasmProcess {
}

pub fn handle<T, U, E>(
mut store: StoreMut,
env_mut: &mut WasmEnv<Runtime>,
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()?;

env.data_mut().exit_template_invocation();
let result = alloc_fn.call(&mut *env, len);
env.data_mut().enter_template_invocation();

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,15 @@ impl Invokable<Store> for WasmProcess {
.interface_mut()
.record_wasm_execution(points_consumed.saturating_sub(already_synced))?;

// A refusal or engine error recorded during the invocation fails the call on both paths.
// `tari_engine_entrypoint` can only answer a refused or 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.
take_refused_engine_call(self.env_mut(store))?;
if let Some(err) = self.env_mut(store).take_last_engine_error() {
return Err(WasmExecutionError::RuntimeError(err));
}

match res {
Ok(return_ptr) => {
// Read response from memory
Expand All @@ -523,9 +539,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