use crate::errors::ContractPrecompilatonResult;
use crate::imports::unc_vm::UncVmImports;
use crate::logic::errors::{
CacheError, CompilationError, FunctionCallError, MethodResolveError, VMRunnerError, WasmTrap,
};
use crate::logic::gas_counter::FastGasCounter;
use crate::logic::types::PromiseResult;
use crate::logic::{
CompiledContract, CompiledContractCache, Config, External, MemSlice, MemoryLike, VMContext,
VMLogic, VMOutcome,
};
use crate::prepare;
use crate::runner::VMResult;
use crate::{get_contract_cache_key, imports, ContractCode};
use memoffset::offset_of;
use std::borrow::Cow;
use std::hash::Hash;
use std::mem::size_of;
use std::sync::{Arc, OnceLock};
use unc_parameters::vm::VMKind;
use unc_parameters::RuntimeFeesConfig;
use unc_vm_compiler_singlepass::Singlepass;
use unc_vm_engine::universal::{
LimitedMemoryPool, Universal, UniversalEngine, UniversalExecutable, UniversalExecutableRef,
};
use unc_vm_types::{FunctionIndex, InstanceConfig, MemoryType, Pages, WASM_PAGE_SIZE};
use unc_vm_vm::{
Artifact, Instantiatable, LinearMemory, LinearTable, Memory, MemoryStyle, TrapCode, VMMemory,
};
#[derive(Clone)]
pub struct UncVmMemory(Arc<LinearMemory>);
impl UncVmMemory {
fn new(
initial_memory_pages: u32,
max_memory_pages: u32,
) -> Result<Self, unc_vm_vm::MemoryError> {
let max_pages = Pages(max_memory_pages);
Ok(UncVmMemory(Arc::new(LinearMemory::new(
&MemoryType::new(Pages(initial_memory_pages), Some(max_pages), false),
&MemoryStyle::Static {
bound: max_pages,
offset_guard_size: unc_vm_types::WASM_PAGE_SIZE as u64,
},
)?)))
}
unsafe fn get_ptr(&self, offset: u64, len: usize) -> Result<*mut u8, ()> {
let offset = usize::try_from(offset).map_err(|_| ())?;
let vmmem = unsafe { self.0.vmmemory().as_ref() };
let remaining = vmmem.current_length.checked_sub(offset).ok_or(())?;
if len <= remaining {
Ok(vmmem.base.add(offset))
} else {
Err(())
}
}
unsafe fn get(&self, offset: u64, len: usize) -> Result<&[u8], ()> {
let ptr = unsafe { self.get_ptr(offset, len)? };
Ok(unsafe { core::slice::from_raw_parts(ptr, len) })
}
unsafe fn get_mut(&mut self, offset: u64, len: usize) -> Result<&mut [u8], ()> {
let ptr = unsafe { self.get_ptr(offset, len)? };
Ok(unsafe { core::slice::from_raw_parts_mut(ptr, len) })
}
pub(crate) fn vm(&self) -> VMMemory {
VMMemory { from: self.0.clone(), instance_ref: None }
}
}
impl MemoryLike for UncVmMemory {
fn fits_memory(&self, slice: MemSlice) -> Result<(), ()> {
unsafe { self.get_ptr(slice.ptr, slice.len()?) }.map(|_| ())
}
fn view_memory(&self, slice: MemSlice) -> Result<Cow<[u8]>, ()> {
unsafe { self.get(slice.ptr, slice.len()?) }.map(Cow::Borrowed)
}
fn read_memory(&self, offset: u64, buffer: &mut [u8]) -> Result<(), ()> {
Ok(buffer.copy_from_slice(unsafe { self.get(offset, buffer.len())? }))
}
fn write_memory(&mut self, offset: u64, buffer: &[u8]) -> Result<(), ()> {
Ok(unsafe { self.get_mut(offset, buffer.len())? }.copy_from_slice(buffer))
}
}
fn get_entrypoint_index(
artifact: &unc_vm_engine::universal::UniversalArtifact,
method_name: &str,
) -> Result<FunctionIndex, FunctionCallError> {
if method_name.is_empty() {
return Err(FunctionCallError::MethodResolveError(MethodResolveError::MethodEmptyName));
}
if let Some(unc_vm_types::ExportIndex::Function(index)) = artifact.export_field(method_name) {
let signature = artifact.function_signature(index).expect("index should produce signature");
let signature =
artifact.engine().lookup_signature(signature).expect("signature store invlidated?");
if signature.params().is_empty() && signature.results().is_empty() {
Ok(index)
} else {
Err(FunctionCallError::MethodResolveError(MethodResolveError::MethodInvalidSignature))
}
} else {
Err(FunctionCallError::MethodResolveError(MethodResolveError::MethodNotFound))
}
}
fn translate_runtime_error(
error: unc_vm_engine::RuntimeError,
logic: &mut VMLogic,
) -> Result<FunctionCallError, VMRunnerError> {
let error = match error.downcast::<crate::logic::VMLogicError>() {
Ok(vm_logic) => {
return vm_logic.try_into();
}
Err(original) => original,
};
let msg = error.message();
let trap_code = error.to_trap().unwrap_or_else(|| {
panic!("runtime error is not a trap: {}", msg);
});
Ok(match trap_code {
TrapCode::GasExceeded => FunctionCallError::HostError(logic.process_gas_limit()),
TrapCode::StackOverflow => FunctionCallError::WasmTrap(WasmTrap::StackOverflow),
TrapCode::HeapAccessOutOfBounds => FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds),
TrapCode::HeapMisaligned => FunctionCallError::WasmTrap(WasmTrap::MisalignedAtomicAccess),
TrapCode::TableAccessOutOfBounds => {
FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds)
}
TrapCode::OutOfBounds => FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds),
TrapCode::IndirectCallToNull => FunctionCallError::WasmTrap(WasmTrap::IndirectCallToNull),
TrapCode::BadSignature => {
FunctionCallError::WasmTrap(WasmTrap::IncorrectCallIndirectSignature)
}
TrapCode::IntegerOverflow => FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic),
TrapCode::IntegerDivisionByZero => FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic),
TrapCode::BadConversionToInteger => {
FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic)
}
TrapCode::UnreachableCodeReached => FunctionCallError::WasmTrap(WasmTrap::Unreachable),
TrapCode::UnalignedAtomic => FunctionCallError::WasmTrap(WasmTrap::MisalignedAtomicAccess),
})
}
#[derive(Hash, PartialEq, Debug)]
#[allow(unused)]
enum UncVmEngine {
Universal = 1,
StaticLib = 2,
DynamicLib = 3,
}
#[derive(Hash, PartialEq, Debug)]
#[allow(unused)]
enum UncVmCompiler {
Singlepass = 1,
Cranelift = 2,
Llvm = 3,
}
#[derive(Hash)]
struct UncVmConfig {
seed: u32,
engine: UncVmEngine,
compiler: UncVmCompiler,
}
impl UncVmConfig {
fn config_hash(self: Self) -> u64 {
crate::utils::stable_hash(&self)
}
}
const VM_CONFIG: UncVmConfig = UncVmConfig {
seed: (2 << 29) | (2 << 6) | 1,
engine: UncVmEngine::Universal,
compiler: UncVmCompiler::Singlepass,
};
pub(crate) fn unc_vm_vm_hash() -> u64 {
VM_CONFIG.config_hash()
}
pub(crate) type VMArtifact = Arc<unc_vm_engine::universal::UniversalArtifact>;
pub(crate) struct UncVM {
pub(crate) config: Config,
pub(crate) engine: UniversalEngine,
}
impl UncVM {
pub(crate) fn new_for_target(config: Config, target: unc_vm_compiler::Target) -> Self {
assert_eq!(VM_CONFIG.compiler, UncVmCompiler::Singlepass);
let mut compiler = Singlepass::new();
compiler.set_9393_fix(!config.disable_9393_fix);
assert_eq!(VM_CONFIG.engine, UncVmEngine::Universal);
static CODE_MEMORY_POOL_CELL: OnceLock<LimitedMemoryPool> = OnceLock::new();
let code_memory_pool = CODE_MEMORY_POOL_CELL
.get_or_init(|| {
LimitedMemoryPool::new(8, 64 * 1024 * 1024).unwrap_or_else(|e| {
panic!("could not pre-allocate resources for the runtime: {e}");
})
})
.clone();
let features =
crate::features::WasmFeatures::from(config.limit_config.contract_prepare_version);
Self {
config,
engine: Universal::new(compiler)
.target(target)
.features(features.into())
.code_memory_pool(code_memory_pool)
.engine(),
}
}
pub(crate) fn new(config: Config) -> Self {
use unc_vm_compiler::{CpuFeature, Target, Triple};
let target_features = if cfg!(feature = "no_cpu_compatibility_checks") {
let mut fs = CpuFeature::set();
fs.insert(CpuFeature::SSE2);
fs.insert(CpuFeature::SSE3);
fs.insert(CpuFeature::SSSE3);
fs.insert(CpuFeature::SSE41);
fs.insert(CpuFeature::SSE42);
fs.insert(CpuFeature::POPCNT);
fs.insert(CpuFeature::AVX);
fs
} else {
CpuFeature::for_host()
};
Self::new_for_target(config, Target::new(Triple::host(), target_features))
}
pub(crate) fn compile_uncached(
&self,
code: &ContractCode,
) -> Result<UniversalExecutable, CompilationError> {
let _span = tracing::debug_span!(target: "vm", "UncVM::compile_uncached").entered();
let prepared_code = prepare::prepare_contract(code.code(), &self.config, VMKind::UncVm)
.map_err(CompilationError::PrepareError)?;
debug_assert!(
matches!(self.engine.validate(&prepared_code), Ok(_)),
"unc_vm failed to validate the prepared code"
);
let executable = self
.engine
.compile_universal(&prepared_code, &self)
.map_err(|err| {
tracing::error!(?err, "unc_vm failed to compile the prepared code (this is defense-in-depth, the error was recovered from but should be reported to pagoda)");
CompilationError::WasmerCompileError { msg: err.to_string() }
})?;
Ok(executable)
}
fn compile_and_cache(
&self,
code: &ContractCode,
cache: Option<&dyn CompiledContractCache>,
) -> Result<Result<UniversalExecutable, CompilationError>, CacheError> {
let executable_or_error = self.compile_uncached(code);
let key = get_contract_cache_key(code, &self.config);
if let Some(cache) = cache {
let record = match &executable_or_error {
Ok(executable) => {
let code = executable
.serialize()
.map_err(|_e| CacheError::SerializationError { hash: key.0 })?;
CompiledContract::Code(code)
}
Err(err) => CompiledContract::CompileModuleError(err.clone()),
};
cache.put(&key, record).map_err(CacheError::WriteError)?;
}
Ok(executable_or_error)
}
fn compile_and_load(
&self,
code: &ContractCode,
cache: Option<&dyn CompiledContractCache>,
) -> VMResult<Result<VMArtifact, CompilationError>> {
let _span = tracing::debug_span!(target: "vm", "UncVM::compile_and_load").entered();
let key = get_contract_cache_key(code, &self.config);
let cache_record = cache
.map(|cache| cache.get(&key))
.transpose()
.map_err(CacheError::ReadError)?
.flatten();
let stored_artifact: Option<VMArtifact> = match cache_record {
None => None,
Some(CompiledContract::CompileModuleError(err)) => return Ok(Err(err)),
Some(CompiledContract::Code(serialized_module)) => {
let _span = tracing::debug_span!(target: "vm", "UncVM::read_from_cache").entered();
unsafe {
let executable = UniversalExecutableRef::deserialize(&serialized_module)
.map_err(|_| CacheError::DeserializationError)?;
let artifact = self
.engine
.load_universal_executable_ref(&executable)
.map(Arc::new)
.map_err(|err| VMRunnerError::LoadingError(err.to_string()))?;
Some(artifact)
}
}
};
Ok(if let Some(it) = stored_artifact {
Ok(it)
} else {
match self.compile_and_cache(code, cache)? {
Ok(executable) => Ok(self
.engine
.load_universal_executable(&executable)
.map(Arc::new)
.map_err(|err| VMRunnerError::LoadingError(err.to_string()))?),
Err(err) => Err(err),
}
})
}
fn run_method(
&self,
artifact: &VMArtifact,
mut import: UncVmImports<'_, '_, '_>,
method_name: &str,
) -> Result<Result<(), FunctionCallError>, VMRunnerError> {
let _span = tracing::debug_span!(target: "vm", "run_method").entered();
assert_eq!(
size_of::<FastGasCounter>(),
size_of::<unc_vm_types::FastGasCounter>() + size_of::<u64>()
);
assert_eq!(
offset_of!(FastGasCounter, burnt_gas),
offset_of!(unc_vm_types::FastGasCounter, burnt_gas)
);
assert_eq!(
offset_of!(FastGasCounter, gas_limit),
offset_of!(unc_vm_types::FastGasCounter, gas_limit)
);
let gas = import.vmlogic.gas_counter_pointer() as *mut unc_vm_types::FastGasCounter;
let entrypoint = match get_entrypoint_index(&*artifact, method_name) {
Ok(index) => index,
Err(abort) => return Ok(Err(abort)),
};
unsafe {
let instance = {
let _span = tracing::debug_span!(target: "vm", "run_method/instantiate").entered();
let maybe_handle = Arc::clone(artifact).instantiate(
&self,
&mut import,
Box::new(()),
InstanceConfig::with_stack_limit(self.config.limit_config.max_stack_height)
.with_counter(gas),
);
let handle = match maybe_handle {
Ok(handle) => handle,
Err(err) => {
use unc_vm_engine::InstantiationError::*;
let abort = match err {
Start(err) => translate_runtime_error(err, import.vmlogic)?,
Link(e) => FunctionCallError::LinkError { msg: e.to_string() },
CpuFeature(e) => panic!(
"host doesn't support the CPU features needed to run contracts: {}",
e
),
};
return Ok(Err(abort));
}
};
match handle.finish_instantiation() {
Ok(handle) => handle,
Err(trap) => {
let abort = translate_runtime_error(
unc_vm_engine::RuntimeError::from_trap(trap),
import.vmlogic,
)?;
return Ok(Err(abort));
}
};
handle
};
if let Some(function) = instance.function_by_index(entrypoint) {
let _span = tracing::debug_span!(target: "vm", "run_method/call").entered();
let signature = artifact
.engine()
.lookup_signature(function.signature)
.expect("extern type should refer to valid signature");
if signature.params().is_empty() && signature.results().is_empty() {
let trampoline =
function.call_trampoline.expect("externs always have a trampoline");
let res = instance.invoke_function(
function.vmctx,
trampoline,
function.address,
[].as_mut_ptr() as *mut _,
);
if let Err(trap) = res {
let abort = translate_runtime_error(
unc_vm_engine::RuntimeError::from_trap(trap),
import.vmlogic,
)?;
return Ok(Err(abort));
}
} else {
panic!("signature should've already been checked by `get_entrypoint_index`")
}
} else {
panic!("signature should've already been checked by `get_entrypoint_index`")
}
{
let _span =
tracing::debug_span!(target: "vm", "run_method/drop_instance").entered();
drop(instance)
}
}
Ok(Ok(()))
}
}
impl unc_vm_vm::Tunables for &UncVM {
fn memory_style(&self, memory: &MemoryType) -> MemoryStyle {
MemoryStyle::Static {
bound: memory.maximum.unwrap_or(Pages(self.config.limit_config.max_memory_pages)),
offset_guard_size: WASM_PAGE_SIZE as u64,
}
}
fn table_style(&self, _table: &unc_vm_types::TableType) -> unc_vm_vm::TableStyle {
unc_vm_vm::TableStyle::CallerChecksSignature
}
fn create_host_memory(
&self,
ty: &MemoryType,
_style: &MemoryStyle,
) -> Result<std::sync::Arc<dyn Memory>, unc_vm_vm::MemoryError> {
Err(unc_vm_vm::MemoryError::CouldNotGrow { current: Pages(0), attempted_delta: ty.minimum })
}
unsafe fn create_vm_memory(
&self,
ty: &MemoryType,
_style: &MemoryStyle,
_vm_definition_location: std::ptr::NonNull<unc_vm_vm::VMMemoryDefinition>,
) -> Result<std::sync::Arc<dyn Memory>, unc_vm_vm::MemoryError> {
Err(unc_vm_vm::MemoryError::CouldNotGrow { current: Pages(0), attempted_delta: ty.minimum })
}
fn create_host_table(
&self,
_ty: &unc_vm_types::TableType,
_style: &unc_vm_vm::TableStyle,
) -> Result<std::sync::Arc<dyn unc_vm_vm::Table>, String> {
panic!("should never be called")
}
unsafe fn create_vm_table(
&self,
ty: &unc_vm_types::TableType,
style: &unc_vm_vm::TableStyle,
vm_definition_location: std::ptr::NonNull<unc_vm_vm::VMTableDefinition>,
) -> Result<std::sync::Arc<dyn unc_vm_vm::Table>, String> {
Ok(Arc::new(LinearTable::from_definition(&ty, &style, vm_definition_location)?))
}
fn stack_init_gas_cost(&self, stack_size: u64) -> u64 {
u64::from(self.config.regular_op_cost).saturating_mul((stack_size + 7) / 8)
}
fn stack_limiter_cfg(&self) -> Box<dyn finite_wasm::max_stack::SizeConfig> {
Box::new(MaxStackCfg)
}
fn gas_cfg(&self) -> Box<dyn finite_wasm::wasmparser::VisitOperator<Output = u64>> {
Box::new(GasCostCfg(u64::from(self.config.regular_op_cost)))
}
}
struct MaxStackCfg;
impl finite_wasm::max_stack::SizeConfig for MaxStackCfg {
fn size_of_value(&self, ty: finite_wasm::wasmparser::ValType) -> u8 {
use finite_wasm::wasmparser::ValType;
match ty {
ValType::I32 => 4,
ValType::I64 => 8,
ValType::F32 => 4,
ValType::F64 => 8,
ValType::V128 => 16,
ValType::Ref(_) => 8,
}
}
fn size_of_function_activation(
&self,
locals: &prefix_sum_vec::PrefixSumVec<finite_wasm::wasmparser::ValType, u32>,
) -> u64 {
let mut res = 64_u64; let mut last_idx_plus_one = 0_u64;
for (idx, local) in locals {
let idx = u64::from(*idx);
res = res.saturating_add(
idx.checked_sub(last_idx_plus_one)
.expect("prefix-sum-vec indices went backwards")
.saturating_add(1)
.saturating_mul(u64::from(self.size_of_value(*local))),
);
last_idx_plus_one = idx.saturating_add(1);
}
res
}
}
struct GasCostCfg(u64);
macro_rules! gas_cost {
($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident)*) => {
$(
fn $visit(&mut self $($(, $arg: $argty)*)?) -> u64 {
gas_cost!(@@$proposal $op self $({ $($arg: $argty),* })? => $visit)
}
)*
};
(@@mvp $_op:ident $_self:ident $({ $($_arg:ident: $_argty:ty),* })? => visit_block) => {
0
};
(@@mvp $_op:ident $_self:ident $({ $($_arg:ident: $_argty:ty),* })? => visit_end) => {
0
};
(@@mvp $_op:ident $_self:ident $({ $($_arg:ident: $_argty:ty),* })? => visit_else) => {
0
};
(@@$_proposal:ident $_op:ident $self:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident) => {
$self.0
};
}
impl<'a> finite_wasm::wasmparser::VisitOperator<'a> for GasCostCfg {
type Output = u64;
finite_wasm::wasmparser::for_each_operator!(gas_cost);
}
impl crate::runner::VM for UncVM {
fn run(
&self,
code: &ContractCode,
method_name: &str,
ext: &mut dyn External,
context: VMContext,
fees_config: &RuntimeFeesConfig,
promise_results: &[PromiseResult],
cache: Option<&dyn CompiledContractCache>,
) -> Result<VMOutcome, VMRunnerError> {
let mut memory = UncVmMemory::new(
self.config.limit_config.initial_memory_pages,
self.config.limit_config.max_memory_pages,
)
.expect("Cannot create memory for a contract call");
let vmmemory = memory.vm();
let mut logic =
VMLogic::new(ext, context, &self.config, fees_config, promise_results, &mut memory);
let result = logic.before_loading_executable(method_name, code.code().len());
if let Err(e) = result {
return Ok(VMOutcome::abort(logic, e));
}
let artifact = self.compile_and_load(code, cache)?;
let artifact = match artifact {
Ok(it) => it,
Err(err) => {
return Ok(VMOutcome::abort(logic, FunctionCallError::CompilationError(err)));
}
};
let result = logic.after_loading_executable(code.code().len());
if let Err(e) = result {
return Ok(VMOutcome::abort(logic, e));
}
let import = imports::unc_vm::build(vmmemory, &mut logic, artifact.engine());
if let Err(e) = get_entrypoint_index(&*artifact, method_name) {
return Ok(VMOutcome::abort_but_nop_outcome_in_old_protocol(logic, e));
}
match self.run_method(&artifact, import, method_name)? {
Ok(()) => Ok(VMOutcome::ok(logic)),
Err(err) => Ok(VMOutcome::abort(logic, err)),
}
}
fn precompile(
&self,
code: &ContractCode,
cache: &dyn CompiledContractCache,
) -> Result<
Result<ContractPrecompilatonResult, CompilationError>,
crate::logic::errors::CacheError,
> {
Ok(self
.compile_and_cache(code, Some(cache))?
.map(|_| ContractPrecompilatonResult::ContractCompiled))
}
}
#[test]
fn test_memory_like() {
crate::logic::test_utils::test_memory_like(|| Box::new(UncVmMemory::new(1, 1).unwrap()));
}