diff --git a/Cargo.lock b/Cargo.lock index 0337bf42b23..fbbef7eae61 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3628,6 +3628,7 @@ dependencies = [ "wasmer-runtime-core-near", "wasmer-runtime-near", "wasmer-types", + "wasmer-vm", "wasmtime", ] diff --git a/core/primitives/src/errors.rs b/core/primitives/src/errors.rs index 67d36a925eb..985c57fdb84 100644 --- a/core/primitives/src/errors.rs +++ b/core/primitives/src/errors.rs @@ -7,7 +7,7 @@ use std::fmt::{Debug, Display}; use crate::hash::CryptoHash; use near_rpc_error_macro::RpcError; -use near_vm_errors::{FunctionCallError, VMLogicError}; +use near_vm_errors::{CompilationError, FunctionCallErrorSer, MethodResolveError, VMLogicError}; /// Error returned in the ExecutionOutcome in case of failure #[derive( @@ -332,6 +332,46 @@ pub struct ActionError { pub kind: ActionErrorKind, } +#[derive(Debug, Clone, PartialEq, Eq, RpcError)] +pub enum ContractCallError { + MethodResolveError(MethodResolveError), + CompilationError(CompilationError), + ExecutionError { msg: String }, +} + +impl From for FunctionCallErrorSer { + fn from(e: ContractCallError) -> Self { + match e { + ContractCallError::CompilationError(e) => FunctionCallErrorSer::CompilationError(e), + ContractCallError::MethodResolveError(e) => FunctionCallErrorSer::MethodResolveError(e), + ContractCallError::ExecutionError { msg } => FunctionCallErrorSer::ExecutionError(msg), + } + } +} + +impl From for ContractCallError { + fn from(e: FunctionCallErrorSer) -> Self { + match e { + FunctionCallErrorSer::CompilationError(e) => ContractCallError::CompilationError(e), + FunctionCallErrorSer::MethodResolveError(e) => ContractCallError::MethodResolveError(e), + FunctionCallErrorSer::ExecutionError(msg) => ContractCallError::ExecutionError { msg }, + FunctionCallErrorSer::LinkError { msg } => ContractCallError::ExecutionError { msg }, + FunctionCallErrorSer::WasmUnknownError => { + ContractCallError::ExecutionError { msg: "unknown error".to_string() } + } + FunctionCallErrorSer::EvmError(e) => { + ContractCallError::ExecutionError { msg: format!("EVM: {:?}", e) } + } + FunctionCallErrorSer::WasmTrap(e) => { + ContractCallError::ExecutionError { msg: format!("WASM: {:?}", e) } + } + FunctionCallErrorSer::HostError(e) => { + ContractCallError::ExecutionError { msg: format!("Host: {:?}", e) } + } + } + } +} + #[derive( BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq, Eq, Deserialize, Serialize, RpcError, )] @@ -384,8 +424,8 @@ pub enum ActionErrorKind { #[serde(with = "u128_dec_format")] minimum_stake: Balance, }, - /// An error occurred during a `FunctionCall` Action. - FunctionCallError(FunctionCallError), + /// An error occurred during a `FunctionCall` Action, parameter is debug message. + FunctionCallError(FunctionCallErrorSer), /// Error occurs when a new `ActionReceipt` created by the `FunctionCall` action fails /// receipt validation. NewReceiptValidationError(ReceiptValidationError), @@ -689,7 +729,7 @@ impl Display for ActionErrorKind { ActionErrorKind::DeleteAccountStaking { account_id } => { write!(f, "Account {:?} is staking and can not be deleted", account_id) } - ActionErrorKind::FunctionCallError(s) => write!(f, "{}", s), + ActionErrorKind::FunctionCallError(s) => write!(f, "{:?}", s), ActionErrorKind::NewReceiptValidationError(e) => { write!(f, "An new action receipt created during a FunctionCall is not valid: {}", e) } diff --git a/runtime/near-vm-errors/src/lib.rs b/runtime/near-vm-errors/src/lib.rs index fc5d30c47e2..9311eb01030 100644 --- a/runtime/near-vm-errors/src/lib.rs +++ b/runtime/near-vm-errors/src/lib.rs @@ -1,11 +1,11 @@ use std::fmt::{self, Error, Formatter}; use borsh::{BorshDeserialize, BorshSerialize}; -use serde::{Deserialize, Serialize}; - use near_rpc_error_macro::RpcError; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize)] +// TODO: remove serialization derives, once fix compilation caching. +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub enum VMError { FunctionCallError(FunctionCallError), /// Serialized external error from External trait implementation. @@ -17,10 +17,34 @@ pub enum VMError { CacheError(CacheError), } -#[derive( - Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError, -)] +// TODO(4217): remove serialization derives, once fix compilation caching. +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] pub enum FunctionCallError { + /// Wasm compilation error + CompilationError(CompilationError), + /// Wasm binary env link error + LinkError { + msg: String, + }, + /// Import/export resolve error + MethodResolveError(MethodResolveError), + /// A trap happened during execution of a binary + WasmTrap(WasmTrap), + WasmUnknownError { + debug_message: String, + }, + HostError(HostError), + EvmError(EvmError), + /// Non-deterministic error. + Nondeterministic(String), +} + +/// Serializable version of `FunctionCallError`. Must never reorder/remove elements, can only +/// add new variants at the end (but do that very carefully). This type must be never used +/// directly, and must be converted to `ContractCallError` instead using `into()` converter. +/// It describes stable serialization format, and only used by serialization logic. +#[derive(Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Serialize, Deserialize)] +pub enum FunctionCallErrorSer { /// Wasm compilation error CompilationError(CompilationError), /// Wasm binary env link error @@ -34,13 +58,9 @@ pub enum FunctionCallError { WasmUnknownError, HostError(HostError), EvmError(EvmError), - /// An error message when wasmer 1.0 returns a wasmer::RuntimeError - WasmerRuntimeError(String), - /// A trap in Wasmer 1.0, not same as WasmTrap above, String is a machine readable form like "stk_ovf" - /// String is used instead of wasmer internal enum is because of BorshSerializable. - /// It can be convert back by wasmer_vm::TrapCode::from_str - Wasmer1Trap(String), + ExecutionError(String), } + #[derive( Debug, Clone, PartialEq, Eq, BorshDeserialize, BorshSerialize, Deserialize, Serialize, RpcError, )] @@ -67,8 +87,8 @@ pub enum WasmTrap { IllegalArithmetic, /// Misaligned atomic access trap. MisalignedAtomicAccess, - /// Breakpoint trap. - BreakpointTrap, + /// Indirect call to null. + IndirectCallToNull, /// Stack overflow. StackOverflow, /// Generic trap. @@ -363,12 +383,13 @@ impl fmt::Display for FunctionCallError { FunctionCallError::HostError(e) => e.fmt(f), FunctionCallError::LinkError { msg } => write!(f, "{}", msg), FunctionCallError::WasmTrap(trap) => write!(f, "WebAssembly trap: {}", trap), - FunctionCallError::WasmUnknownError => { - write!(f, "Unknown error during Wasm contract execution") + FunctionCallError::WasmUnknownError { debug_message } => { + write!(f, "Unknown error during Wasm contract execution: {}", debug_message) } FunctionCallError::EvmError(e) => write!(f, "EVM: {:?}", e), - FunctionCallError::WasmerRuntimeError(e) => write!(f, "Wasmer Runtime: {}", e), - FunctionCallError::Wasmer1Trap(e) => write!(f, "Wasmer 1.0 trap: {}", e), + FunctionCallError::Nondeterministic(msg) => { + write!(f, "Nondeterministic error during contract execution: {}", msg) + } } } } @@ -387,8 +408,8 @@ impl fmt::Display for WasmTrap { } WasmTrap::MisalignedAtomicAccess => write!(f, "Misaligned atomic access trap."), WasmTrap::GenericTrap => write!(f, "Generic trap."), - WasmTrap::BreakpointTrap => write!(f, "Breakpoint trap."), WasmTrap::StackOverflow => write!(f, "Stack overflow."), + WasmTrap::IndirectCallToNull => write!(f, "Indirect call to null."), } } } diff --git a/runtime/near-vm-runner/Cargo.toml b/runtime/near-vm-runner/Cargo.toml index cd9bd3e6213..c40f2368547 100644 --- a/runtime/near-vm-runner/Cargo.toml +++ b/runtime/near-vm-runner/Cargo.toml @@ -22,6 +22,7 @@ wasmer-types = { version = "1.0.2", optional = true } wasmer-compiler-singlepass = { version = "1.0.2", optional = true } wasmer-compiler-cranelift = { version = "1.0.2", optional = true } wasmer-engine-native = { version = "1.0.2", optional = true } +wasmer-vm = { version = "1.0.2" } pwasm-utils = "0.12" parity-wasm = "0.41" wasmtime = { version = "0.25.0", default-features = false, optional = true } diff --git a/runtime/near-vm-runner/src/tests/error_cases.rs b/runtime/near-vm-runner/src/tests/error_cases.rs index c29f5144c8a..adaeadd9cd1 100644 --- a/runtime/near-vm-runner/src/tests/error_cases.rs +++ b/runtime/near-vm-runner/src/tests/error_cases.rs @@ -1,5 +1,6 @@ use near_vm_errors::{ CompilationError, FunctionCallError, HostError, MethodResolveError, PrepareError, VMError, + WasmTrap, }; use near_vm_logic::{ReturnData, VMKind, VMOutcome}; @@ -130,25 +131,22 @@ fn trap_contract() -> Vec { #[test] fn test_trap_contract() { - // See the comment is test_stack_overflow. - with_vm_variants(|vm_kind: VMKind| match vm_kind { - VMKind::Wasmtime => return, - VMKind::Wasmer0 => assert_eq!( - make_simple_contract_call_vm(&trap_contract(), "hello", vm_kind), - ( - Some(vm_outcome_with_gas(47105334)), - Some(VMError::FunctionCallError(FunctionCallError::WasmUnknownError)) - ) - ), - VMKind::Wasmer1 => assert_eq!( + with_vm_variants(|vm_kind: VMKind| { + match vm_kind { + VMKind::Wasmer0 | VMKind::Wasmer1 => {} + // All contracts leading to hardware traps can not run concurrently on Wasmtime and Wasmer, + // Restore, once get rid of Wasmer 0.x. + VMKind::Wasmtime => return, + } + assert_eq!( make_simple_contract_call_vm(&trap_contract(), "hello", vm_kind), ( Some(vm_outcome_with_gas(47105334)), - Some(VMError::FunctionCallError(FunctionCallError::Wasmer1Trap( - "unreachable".to_string() + Some(VMError::FunctionCallError(FunctionCallError::WasmTrap( + WasmTrap::Unreachable ))) ) - ), + ) }) } @@ -168,25 +166,149 @@ fn trap_initializer() -> Vec { #[test] fn test_trap_initializer() { // See the comment is test_stack_overflow. - with_vm_variants(|vm_kind: VMKind| match vm_kind { - VMKind::Wasmtime => return, - VMKind::Wasmer0 => assert_eq!( + with_vm_variants(|vm_kind: VMKind| { + match vm_kind { + VMKind::Wasmer0 | VMKind::Wasmer1 => {} + // All contracts leading to hardware traps can not run concurrently on Wasmtime and Wasmer, + // Check if can restore, once get rid of Wasmer 0.x. + VMKind::Wasmtime => return, + } + assert_eq!( make_simple_contract_call_vm(&trap_initializer(), "hello", vm_kind), ( Some(vm_outcome_with_gas(47755584)), - Some(VMError::FunctionCallError(FunctionCallError::WasmUnknownError)) + Some(VMError::FunctionCallError(FunctionCallError::WasmTrap( + WasmTrap::Unreachable + ))) ) - ), - VMKind::Wasmer1 => assert_eq!( - make_simple_contract_call_vm(&trap_initializer(), "hello", vm_kind), + ); + }); +} + +fn div_by_zero_contract() -> Vec { + wabt::wat2wasm( + r#" + (module + (type (;0;) (func)) + (func (;0;) (type 0) + i32.const 1 + i32.const 0 + i32.div_s + return + ) + (export "hello" (func 0)) + )"#, + ) + .unwrap() +} + +#[test] +fn test_div_by_zero_contract() { + with_vm_variants(|vm_kind: VMKind| { + match vm_kind { + VMKind::Wasmer0 | VMKind::Wasmer1 => {} + // All contracts leading to hardware traps can not run concurrently on Wasmtime and Wasmer, + // Check if can restore, once get rid of Wasmer 0.x. + VMKind::Wasmtime => return, + } + assert_eq!( + make_simple_contract_call_vm(&div_by_zero_contract(), "hello", vm_kind), ( - Some(vm_outcome_with_gas(47755584)), - Some(VMError::FunctionCallError(FunctionCallError::Wasmer1Trap( - "unreachable".to_string() + Some(vm_outcome_with_gas(59758197)), + Some(VMError::FunctionCallError(FunctionCallError::WasmTrap( + WasmTrap::IllegalArithmetic ))) ) - ), - }); + ) + }) +} + +fn indirect_call_to_null_contract() -> Vec { + wabt::wat2wasm( + r#" + (module + (type (;0;) (func)) + (table (;0;) 2 funcref) + (func (;0;) (type 0) + i32.const 1 + call_indirect (type 0) + return + ) + (export "hello" (func 0)) + )"#, + ) + .unwrap() +} + +#[test] +fn test_indirect_call_to_null_contract() { + with_vm_variants(|vm_kind: VMKind| { + match vm_kind { + VMKind::Wasmer1 => {} + // Wasmer 0.x cannot distinguish indirect calls to null and calls with incorrect signature. + VMKind::Wasmer0 => return, + // All contracts leading to hardware traps can not run concurrently on Wasmtime and Wasmer, + // Check if can restore, once get rid of Wasmer 0.x. + VMKind::Wasmtime => return, + } + assert_eq!( + make_simple_contract_call_vm(&indirect_call_to_null_contract(), "hello", vm_kind), + ( + Some(vm_outcome_with_gas(57202326)), + Some(VMError::FunctionCallError(FunctionCallError::WasmTrap( + WasmTrap::IndirectCallToNull + ))) + ) + ) + }) +} + +fn indirect_call_to_wrong_signature_contract() -> Vec { + wabt::wat2wasm( + r#" + (module + (type (;0;) (func)) + (type (;1;) (func (result i32))) + (func (;0;) (type 0) + i32.const 1 + call_indirect (type 1) + return + ) + (func (;1;) (type 1) + i32.const 0 + return + ) + (table (;0;) 3 3 funcref) + (elem (;0;) (i32.const 1) 0 1) + (export "hello" (func 0)) + )"#, + ) + .unwrap() +} + +#[test] +fn test_indirect_call_to_wrong_signature_contract() { + with_vm_variants(|vm_kind: VMKind| { + match vm_kind { + VMKind::Wasmer0 | VMKind::Wasmer1 => {} + // All contracts leading to hardware traps can not run concurrently on Wasmtime and Wasmer, + // Check if can restore, once get rid of Wasmer 0.x. + VMKind::Wasmtime => return, + } + assert_eq!( + make_simple_contract_call_vm( + &indirect_call_to_wrong_signature_contract(), + "hello", + vm_kind + ), + ( + Some(vm_outcome_with_gas(61970826)), + Some(VMError::FunctionCallError(FunctionCallError::WasmTrap( + WasmTrap::IncorrectCallIndirectSignature + ))) + ) + ) + }) } fn wrong_signature_contract() -> Vec { @@ -288,23 +410,16 @@ fn test_stack_overflow() { // We only test trapping tests on Wasmer, as of version 0.17, when tests executed in parallel, // Wasmer signal handlers may catch signals thrown from the Wasmtime, and produce fake failing tests. match vm_kind { - VMKind::Wasmer0 => assert_eq!( - make_simple_contract_call_vm(&stack_overflow(), "hello", vm_kind), - ( - Some(vm_outcome_with_gas(63226248177)), - Some(VMError::FunctionCallError(FunctionCallError::WasmUnknownError)) - ) - ), - VMKind::Wasmer1 => assert_eq!( + VMKind::Wasmer0 | VMKind::Wasmer1 => assert_eq!( make_simple_contract_call_vm(&stack_overflow(), "hello", vm_kind), ( Some(vm_outcome_with_gas(63226248177)), - Some(VMError::FunctionCallError(FunctionCallError::Wasmer1Trap( - "unreachable".to_string() + Some(VMError::FunctionCallError(FunctionCallError::WasmTrap( + WasmTrap::Unreachable ))) ) ), - _ => {} + VMKind::Wasmtime => {} } }); } @@ -410,7 +525,7 @@ fn test_bad_import_3() { let msg = match vm_kind { VMKind::Wasmer0 => "link error: Incorrect import type, namespace: env, name: input, expected type: global, found type: function", VMKind::Wasmtime => "\"incompatible import type for `env::input` specified\\ndesired signature was: Global(GlobalType { content: I32, mutability: Const })\\nsignatures available:\\n\\n * Func(FuncType { sig: WasmFuncType { params: [I64], returns: [] } })\\n\"", - VMKind::Wasmer1 => "Error while importing \"env\".\"input\": incompatible import type. Expected Global(GlobalType { ty: I32, mutability: Const }) but received Function(FunctionType { params: [I64], results: [] })" + VMKind::Wasmer1 => "Error while importing \"env\".\"input\": incompatible import type. Expected Global(GlobalType { ty: I32, mutability: Const }) but received Function(FunctionType { params: [I64], results: [] })", }.to_string(); assert_eq!( make_simple_contract_call_vm(&bad_import_global("env"), "hello", vm_kind), @@ -553,8 +668,8 @@ fn test_external_call_error() { fn test_contract_error_caching() { with_vm_variants(|vm_kind: VMKind| { match vm_kind { + VMKind::Wasmer0 | VMKind::Wasmer1 => {} VMKind::Wasmtime => return, - _ => {} } let mut cache = MockCompiledContractCache::default(); let code = [42; 1000]; diff --git a/runtime/near-vm-runner/src/tests/rs_contract.rs b/runtime/near-vm-runner/src/tests/rs_contract.rs index edf9351b9c5..a479248fe72 100644 --- a/runtime/near-vm-runner/src/tests/rs_contract.rs +++ b/runtime/near-vm-runner/src/tests/rs_contract.rs @@ -2,7 +2,7 @@ use near_primitives::contract::ContractCode; use near_primitives::profile::ProfileData; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::types::Balance; -use near_vm_errors::{FunctionCallError, VMError}; +use near_vm_errors::{FunctionCallError, VMError, WasmTrap}; use near_vm_logic::mocks::mock_external::MockedExternal; use near_vm_logic::{types::ReturnData, VMConfig, VMKind, VMOutcome}; use std::mem::size_of; @@ -285,12 +285,10 @@ pub fn test_out_of_memory() { assert_eq!( result.1, match vm_kind { - VMKind::Wasmer0 => - Some(VMError::FunctionCallError(FunctionCallError::WasmUnknownError)), - VMKind::Wasmer1 => Some(VMError::FunctionCallError( - FunctionCallError::Wasmer1Trap("unreachable".to_string()) + VMKind::Wasmer0 | VMKind::Wasmer1 => Some(VMError::FunctionCallError( + FunctionCallError::WasmTrap(WasmTrap::Unreachable) )), - _ => unreachable!(), + VMKind::Wasmtime => unreachable!(), } ); }) diff --git a/runtime/near-vm-runner/src/wasmer1_runner.rs b/runtime/near-vm-runner/src/wasmer1_runner.rs index ca0e5bcddfc..96f5a1062a7 100644 --- a/runtime/near-vm-runner/src/wasmer1_runner.rs +++ b/runtime/near-vm-runner/src/wasmer1_runner.rs @@ -4,14 +4,16 @@ use near_primitives::contract::ContractCode; use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::{profile::ProfileData, types::CompiledContractCache}; use near_vm_errors::{ - CompilationError, FunctionCallError, MethodResolveError, PrepareError, VMError, + CompilationError, FunctionCallError, MethodResolveError, PrepareError, VMError, WasmTrap, }; use near_vm_logic::types::{PromiseResult, ProtocolVersion}; use near_vm_logic::{External, MemoryLike, VMConfig, VMContext, VMLogic, VMLogicError, VMOutcome}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use wasmer::{Bytes, ImportObject, Instance, Memory, MemoryType, Module, Pages, Store, JIT}; + use wasmer_compiler_singlepass::Singlepass; +use wasmer_vm::TrapCode; pub struct Wasmer1Memory(Memory); @@ -93,20 +95,54 @@ impl IntoVMError for wasmer::RuntimeError { // so we cannot clone self let error_msg = self.message(); let trap_code = self.clone().to_trap(); - match self.downcast::() { - Ok(e) => (&e).into(), - _ => { - if let Some(trap_code) = trap_code { - // A trap - VMError::FunctionCallError(FunctionCallError::Wasmer1Trap( - trap_code.to_string(), - )) - } else { - // A general error - VMError::FunctionCallError(FunctionCallError::WasmerRuntimeError(error_msg)) - } - } + if let Ok(e) = self.downcast::() { + return (&e).into(); } + // If we panic here - it means we encountered an issue in Wasmer. + let trap_code = trap_code.unwrap_or_else(|| panic!("Unknown error: {}", error_msg)); + let error = match trap_code { + TrapCode::StackOverflow => FunctionCallError::WasmTrap(WasmTrap::StackOverflow), + TrapCode::HeapSetterOutOfBounds => { + FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds) + } + TrapCode::HeapAccessOutOfBounds => { + FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds) + } + TrapCode::HeapMisaligned => { + FunctionCallError::WasmTrap(WasmTrap::MisalignedAtomicAccess) + } + TrapCode::TableSetterOutOfBounds => { + FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds) + } + 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) + } + TrapCode::Interrupt => { + FunctionCallError::Nondeterministic("Wasmer interrupt".to_string()) + } + TrapCode::VMOutOfMemory => { + FunctionCallError::Nondeterministic("Wasmer out of memory".to_string()) + } + }; + VMError::FunctionCallError(error) } } diff --git a/runtime/near-vm-runner/src/wasmer_runner.rs b/runtime/near-vm-runner/src/wasmer_runner.rs index f562c1fc894..32026e71924 100644 --- a/runtime/near-vm-runner/src/wasmer_runner.rs +++ b/runtime/near-vm-runner/src/wasmer_runner.rs @@ -6,8 +6,7 @@ use near_primitives::runtime::fees::RuntimeFeesConfig; use near_primitives::{ config::VMConfig, profile::ProfileData, types::CompiledContractCache, version::ProtocolVersion, }; -use near_vm_errors::FunctionCallError::{WasmTrap, WasmUnknownError}; -use near_vm_errors::{CompilationError, FunctionCallError, MethodResolveError, VMError}; +use near_vm_errors::{CompilationError, FunctionCallError, MethodResolveError, VMError, WasmTrap}; use near_vm_logic::types::PromiseResult; use near_vm_logic::{External, VMContext, VMLogic, VMLogicError, VMOutcome}; use wasmer_runtime::{ImportObject, Module}; @@ -91,7 +90,6 @@ impl IntoVMError for wasmer_runtime::error::ResolveError { impl IntoVMError for wasmer_runtime::error::RuntimeError { fn into_vm_error(self) -> VMError { - use near_vm_errors::WasmTrap::BreakpointTrap; use wasmer_runtime::error::InvokeError; use wasmer_runtime::error::RuntimeError; match &self { @@ -102,7 +100,9 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError { // invoke returns false and doesn't fill error info what Singlepass BE doesn't. // Failed unwinder may happen in the case of deep recursion/stack overflow. // Also can be thrown on unreachable instruction, which is quite unfortunate. - InvokeError::FailedWithNoError => VMError::FunctionCallError(WasmUnknownError), + InvokeError::FailedWithNoError => VMError::FunctionCallError( + FunctionCallError::Nondeterministic("FailedWithNoError".to_string()), + ), // Indicates that a trap occurred that is not known to Wasmer. // As of 0.17.0, thrown only from Cranelift BE. InvokeError::UnknownTrap { address, signal } => { @@ -113,9 +113,27 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError { ); } // A trap that Wasmer knows about occurred. - // As of 0.17.1, can be thrown on C signals caught, for example OOM. - InvokeError::TrapCode { code: _, srcloc: _ } => { - VMError::FunctionCallError(WasmUnknownError) + InvokeError::TrapCode { code, srcloc: _ } => { + VMError::FunctionCallError(match code { + wasmer_runtime::ExceptionCode::Unreachable => { + FunctionCallError::WasmTrap(WasmTrap::Unreachable) + } + wasmer_runtime::ExceptionCode::IncorrectCallIndirectSignature => { + FunctionCallError::WasmTrap(WasmTrap::IncorrectCallIndirectSignature) + } + wasmer_runtime::ExceptionCode::MemoryOutOfBounds => { + FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds) + } + wasmer_runtime::ExceptionCode::CallIndirectOOB => { + FunctionCallError::WasmTrap(WasmTrap::CallIndirectOOB) + } + wasmer_runtime::ExceptionCode::IllegalArithmetic => { + FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic) + } + wasmer_runtime::ExceptionCode::MisalignedAtomicAccess => { + FunctionCallError::WasmTrap(WasmTrap::MisalignedAtomicAccess) + } + }) } // A trap occurred that Wasmer knows about but it had a trap code that // we weren't expecting or that we do not handle. @@ -135,7 +153,7 @@ impl IntoVMError for wasmer_runtime::error::RuntimeError { // upon the middleware or backend being used. // As of 0.17.0, thrown only from Singlepass BE and wraps RuntimeError // instance. - InvokeError::Breakpoint(_) => VMError::FunctionCallError(WasmTrap(BreakpointTrap)), + InvokeError::Breakpoint(_) => unreachable!("Wasmer breakpoint"), }, // A metering triggered error value. // As of 0.17.0, thrown only from Singlepass BE, and as we do not rely diff --git a/runtime/near-vm-runner/src/wasmtime_runner.rs b/runtime/near-vm-runner/src/wasmtime_runner.rs index 349a87ee4c0..29d8c579fcf 100644 --- a/runtime/near-vm-runner/src/wasmtime_runner.rs +++ b/runtime/near-vm-runner/src/wasmtime_runner.rs @@ -11,15 +11,15 @@ pub mod wasmtime_runner { config::VMConfig, profile::ProfileData, types::CompiledContractCache, version::ProtocolVersion, }; - use near_vm_errors::FunctionCallError::{LinkError, WasmUnknownError}; - use near_vm_errors::{FunctionCallError, MethodResolveError, VMError, VMLogicError}; + use near_vm_errors::FunctionCallError::LinkError; + use near_vm_errors::{FunctionCallError, MethodResolveError, VMError, VMLogicError, WasmTrap}; use near_vm_logic::{ types::PromiseResult, External, MemoryLike, VMContext, VMLogic, VMOutcome, }; use std::ffi::c_void; use std::str; use wasmtime::ExternType::Func; - use wasmtime::{Config, Engine, Limits, Linker, Memory, MemoryType, Module, Store}; + use wasmtime::{Config, Engine, Limits, Linker, Memory, MemoryType, Module, Store, TrapCode}; pub struct WasmtimeMemory(Memory); @@ -88,7 +88,41 @@ pub mod wasmtime_runner { None => panic!("Error is not properly set"), } } else { - VMError::FunctionCallError(WasmUnknownError) + match trap.trap_code() { + Some(TrapCode::StackOverflow) => { + VMError::FunctionCallError(FunctionCallError::WasmTrap(WasmTrap::StackOverflow)) + } + Some(TrapCode::MemoryOutOfBounds) => VMError::FunctionCallError( + FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds), + ), + Some(TrapCode::TableOutOfBounds) => VMError::FunctionCallError( + FunctionCallError::WasmTrap(WasmTrap::MemoryOutOfBounds), + ), + Some(TrapCode::IndirectCallToNull) => VMError::FunctionCallError( + FunctionCallError::WasmTrap(WasmTrap::IndirectCallToNull), + ), + Some(TrapCode::BadSignature) => VMError::FunctionCallError( + FunctionCallError::WasmTrap(WasmTrap::IncorrectCallIndirectSignature), + ), + Some(TrapCode::IntegerOverflow) => VMError::FunctionCallError( + FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic), + ), + Some(TrapCode::IntegerDivisionByZero) => VMError::FunctionCallError( + FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic), + ), + Some(TrapCode::BadConversionToInteger) => VMError::FunctionCallError( + FunctionCallError::WasmTrap(WasmTrap::IllegalArithmetic), + ), + Some(TrapCode::UnreachableCodeReached) => { + VMError::FunctionCallError(FunctionCallError::WasmTrap(WasmTrap::Unreachable)) + } + Some(TrapCode::Interrupt) => VMError::FunctionCallError( + FunctionCallError::Nondeterministic("interrupt".to_string()), + ), + _ => VMError::FunctionCallError(FunctionCallError::WasmUnknownError { + debug_message: "unknown trap".to_string(), + }), + } } } @@ -104,11 +138,7 @@ pub mod wasmtime_runner { impl IntoVMError for wasmtime::Trap { fn into_vm_error(self) -> VMError { - if self.i32_exit_status() == Some(239) { - trap_to_error(&self) - } else { - VMError::FunctionCallError(WasmUnknownError) - } + trap_to_error(&self) } } diff --git a/runtime/runtime/src/actions.rs b/runtime/runtime/src/actions.rs index 47a88696be7..2547622e6cc 100644 --- a/runtime/runtime/src/actions.rs +++ b/runtime/runtime/src/actions.rs @@ -4,7 +4,9 @@ use near_crypto::PublicKey; use near_primitives::account::{AccessKey, AccessKeyPermission, Account}; use near_primitives::checked_feature; use near_primitives::contract::ContractCode; -use near_primitives::errors::{ActionError, ActionErrorKind, ExternalError, RuntimeError}; +use near_primitives::errors::{ + ActionError, ActionErrorKind, ContractCallError, ExternalError, RuntimeError, +}; use near_primitives::hash::CryptoHash; use near_primitives::receipt::{ActionReceipt, Receipt}; use near_primitives::runtime::config::AccountCreationConfig; @@ -192,10 +194,45 @@ pub(crate) fn action_function_call( false, ); let execution_succeeded = match err { - Some(VMError::FunctionCallError(err)) => { - result.result = Err(ActionErrorKind::FunctionCallError(err).into()); - false - } + Some(VMError::FunctionCallError(err)) => match err { + FunctionCallError::Nondeterministic(msg) => { + panic!("Contract runner returned non-deterministic error '{}', aborting", msg) + } + FunctionCallError::WasmUnknownError { debug_message } => { + panic!("Wasmer returned unknown message: {}", debug_message) + } + FunctionCallError::CompilationError(err) => { + result.result = Err(ActionErrorKind::FunctionCallError( + ContractCallError::CompilationError(err).into(), + ) + .into()); + false + } + FunctionCallError::LinkError { msg } => { + result.result = Err(ActionErrorKind::FunctionCallError( + ContractCallError::ExecutionError { msg: format!("Link Error: {}", msg) } + .into(), + ) + .into()); + false + } + FunctionCallError::MethodResolveError(err) => { + result.result = Err(ActionErrorKind::FunctionCallError( + ContractCallError::MethodResolveError(err).into(), + ) + .into()); + false + } + FunctionCallError::WasmTrap(_) + | FunctionCallError::HostError(_) + | FunctionCallError::EvmError(_) => { + result.result = Err(ActionErrorKind::FunctionCallError( + ContractCallError::ExecutionError { msg: err.to_string() }.into(), + ) + .into()); + false + } + }, Some(VMError::ExternalError(serialized_error)) => { let err: ExternalError = borsh::BorshDeserialize::try_from_slice(&serialized_error) .expect("External error deserialization shouldn't fail"); diff --git a/runtime/runtime/tests/test_evil_contracts.rs b/runtime/runtime/tests/test_evil_contracts.rs index 6494525c612..cbfa033bbc5 100644 --- a/runtime/runtime/tests/test_evil_contracts.rs +++ b/runtime/runtime/tests/test_evil_contracts.rs @@ -1,7 +1,6 @@ -use near_primitives::errors::{ActionError, ActionErrorKind}; +use near_primitives::errors::{ActionError, ActionErrorKind, ContractCallError}; use near_primitives::serialize::to_base64; use near_primitives::views::FinalExecutionStatus; -use near_vm_errors::{FunctionCallError, HostError}; use std::mem::size_of; use testlib::node::{Node, RuntimeNode}; @@ -137,9 +136,12 @@ fn test_evil_abort() { FinalExecutionStatus::Failure( ActionError { index: Some(0), - kind: ActionErrorKind::FunctionCallError(FunctionCallError::HostError( - HostError::BadUTF16 - )) + kind: ActionErrorKind::FunctionCallError( + ContractCallError::ExecutionError { + msg: "String encoding is bad UTF-16 sequence.".to_string() + } + .into() + ) } .into() ), diff --git a/test-utils/testlib/src/standard_evm_cases.rs b/test-utils/testlib/src/standard_evm_cases.rs index 4ff6b1f2d8e..344f43d2e78 100644 --- a/test-utils/testlib/src/standard_evm_cases.rs +++ b/test-utils/testlib/src/standard_evm_cases.rs @@ -8,9 +8,8 @@ use near_evm_runner::types::WithdrawArgs; use near_evm_runner::utils::{ address_from_arr, encode_call_function_args, encode_view_call_function_args, u256_to_arr, }; -use near_primitives::errors::{ActionError, ActionErrorKind}; +use near_primitives::errors::{ActionError, ActionErrorKind, ContractCallError}; use near_primitives::views::FinalExecutionStatus; -use near_vm_errors::{EvmError, FunctionCallError}; use_contract!(cryptozombies, "../../runtime/near-evm-runner/tests/build/ZombieOwnership.abi"); use_contract!(precompiles, "../../runtime/near-evm-runner/tests/build/StandardPrecompiles.abi"); @@ -81,9 +80,9 @@ pub fn test_evm_infinite_loop_gas_limit(node: impl Node) { FinalExecutionStatus::Failure( ActionError { index: Some(0), - kind: ActionErrorKind::FunctionCallError(FunctionCallError::EvmError( - EvmError::OutOfGas - )) + kind: ActionErrorKind::FunctionCallError( + ContractCallError::ExecutionError { msg: "EVM: OutOfGas".to_string() }.into() + ) } .into() ) @@ -106,9 +105,9 @@ pub fn test_evm_fibonacci_gas_limit(node: impl Node) { FinalExecutionStatus::Failure( ActionError { index: Some(0), - kind: ActionErrorKind::FunctionCallError(FunctionCallError::EvmError( - EvmError::OutOfGas - )) + kind: ActionErrorKind::FunctionCallError( + ContractCallError::ExecutionError { msg: "EVM: OutOfGas".to_string() }.into() + ) } .into() ) diff --git a/test-utils/testlib/src/standard_test_cases.rs b/test-utils/testlib/src/standard_test_cases.rs index eef765309cb..07648daa2ba 100644 --- a/test-utils/testlib/src/standard_test_cases.rs +++ b/test-utils/testlib/src/standard_test_cases.rs @@ -5,20 +5,21 @@ use near_crypto::{InMemorySigner, KeyType}; use near_jsonrpc_primitives::errors::ServerError; use near_primitives::account::{AccessKey, AccessKeyPermission, FunctionCallPermission}; use near_primitives::errors::{ - ActionError, ActionErrorKind, InvalidAccessKeyError, InvalidTxError, TxExecutionError, + ActionError, ActionErrorKind, ContractCallError, InvalidAccessKeyError, InvalidTxError, + TxExecutionError, }; use near_primitives::hash::hash; use near_primitives::serialize::to_base64; use near_primitives::types::Balance; use near_primitives::views::{AccessKeyView, FinalExecutionStatus}; use near_primitives::views::{AccountView, FinalExecutionOutcomeView}; -use near_vm_errors::{FunctionCallError, HostError, MethodResolveError}; use neard::config::{NEAR_BASE, TESTING_INIT_BALANCE, TESTING_INIT_STAKE}; use crate::fees_utils::FeeHelper; use crate::node::Node; use crate::runtime_utils::{alice_account, bob_account, eve_dot_alice_account}; use crate::user::User; +use near_vm_errors::MethodResolveError; /// The amount to send with function call. const FUNCTION_CALL_AMOUNT: Balance = TESTING_INIT_BALANCE / 10; @@ -81,9 +82,12 @@ pub fn test_smart_contract_panic(node: impl Node) { FinalExecutionStatus::Failure( ActionError { index: Some(0), - kind: ActionErrorKind::FunctionCallError(FunctionCallError::HostError( - HostError::GuestPanic { panic_msg: "WAT?".to_string() } - )) + kind: ActionErrorKind::FunctionCallError( + ContractCallError::ExecutionError { + msg: "Smart contract panicked: WAT?".to_string() + } + .into() + ) } .into() ) @@ -119,9 +123,10 @@ pub fn test_smart_contract_bad_method_name(node: impl Node) { FinalExecutionStatus::Failure( ActionError { index: Some(0), - kind: ActionErrorKind::FunctionCallError(FunctionCallError::MethodResolveError( - MethodResolveError::MethodNotFound - )) + kind: ActionErrorKind::FunctionCallError( + ContractCallError::MethodResolveError(MethodResolveError::MethodNotFound) + .into() + ) } .into() ) @@ -143,9 +148,10 @@ pub fn test_smart_contract_empty_method_name_with_no_tokens(node: impl Node) { FinalExecutionStatus::Failure( ActionError { index: Some(0), - kind: ActionErrorKind::FunctionCallError(FunctionCallError::MethodResolveError( - MethodResolveError::MethodEmptyName - )) + kind: ActionErrorKind::FunctionCallError( + ContractCallError::MethodResolveError(MethodResolveError::MethodEmptyName) + .into() + ) } .into() ) @@ -167,9 +173,10 @@ pub fn test_smart_contract_empty_method_name_with_tokens(node: impl Node) { FinalExecutionStatus::Failure( ActionError { index: Some(0), - kind: ActionErrorKind::FunctionCallError(FunctionCallError::MethodResolveError( - MethodResolveError::MethodEmptyName - )) + kind: ActionErrorKind::FunctionCallError( + ContractCallError::MethodResolveError(MethodResolveError::MethodEmptyName) + .into() + ) } .into() )