diff --git a/.circleci/config.yml b/.circleci/config.yml index c701dafe39..7fbee3bedb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -218,15 +218,15 @@ jobs: - run: name: Build library for native target (all features) working_directory: ~/project/packages/std - command: cargo build --locked --features iterator,staking,stargate + command: cargo build --locked --features abort,iterator,staking,stargate - run: name: Build library for wasm target (all features) working_directory: ~/project/packages/std - command: cargo wasm --locked --features iterator,staking,stargate + command: cargo wasm --locked --features abort,iterator,staking,stargate - run: name: Run unit tests (all features) working_directory: ~/project/packages/std - command: cargo test --locked --features iterator,staking,stargate + command: cargo test --locked --features abort,iterator,staking,stargate - run: name: Build and run schema generator working_directory: ~/project/packages/std @@ -945,7 +945,7 @@ jobs: - run: name: Clippy linting on std (all feature flags) working_directory: ~/project/packages/std - command: cargo clippy --all-targets --features iterator,staking,stargate -- -D warnings + command: cargo clippy --all-targets --features abort,iterator,staking,stargate -- -D warnings - run: name: Clippy linting on storage (no feature flags) working_directory: ~/project/packages/storage diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fa21556e7..9cfb63fdc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ and this project adheres to ## [Unreleased] +### Added + +- cosmwasm-std: When the new `abort` feature is enabled, cosmwasm-std installs a + panic handler that aborts the contract and passes the panic message to the + host. The `abort` feature can only be used when deploying to chains that + implement the import. For this reason, it's not yet enabled by default. + ([#1299]) +- cosmwasm-vm: A new import `abort` is created to abort contract execution when + requested by the contract. ([#1299]) + +[#1299]: https://github.com/CosmWasm/cosmwasm/pull/1299 + ## [1.0.0-rc.0] - 2022-05-05 ### Fixed diff --git a/contracts/hackatom/Cargo.toml b/contracts/hackatom/Cargo.toml index 9231cad19b..2b91ed1308 100644 --- a/contracts/hackatom/Cargo.toml +++ b/contracts/hackatom/Cargo.toml @@ -31,7 +31,7 @@ cranelift = ["cosmwasm-vm/cranelift"] backtraces = ["cosmwasm-std/backtraces", "cosmwasm-vm/backtraces"] [dependencies] -cosmwasm-std = { path = "../../packages/std", default-features = false } +cosmwasm-std = { path = "../../packages/std", default-features = false, features = ["abort"] } rust-argon2 = "0.8" schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/contracts/hackatom/src/contract.rs b/contracts/hackatom/src/contract.rs index f0dca04246..9bc34efe7e 100644 --- a/contracts/hackatom/src/contract.rs +++ b/contracts/hackatom/src/contract.rs @@ -189,7 +189,22 @@ fn do_allocate_large_memory(pages: u32) -> Result { } fn do_panic() -> Result { + // Uncomment your favourite panic case + + // panicked at 'This page intentionally faulted', src/contract.rs:53:5 panic!("This page intentionally faulted"); + + // panicked at 'oh no (a = 3)', src/contract.rs:56:5 + // let a = 3; + // panic!("oh no (a = {a})"); + + // panicked at 'attempt to subtract with overflow', src/contract.rs:59:13 + // #[allow(arithmetic_overflow)] + // let _ = 5u32 - 8u32; + + // panicked at 'no entry found for key', src/contract.rs:62:13 + // let map = std::collections::HashMap::::new(); + // let _ = map["foo"]; } fn do_user_errors_in_api_calls(api: &dyn Api) -> Result { diff --git a/contracts/hackatom/tests/integration.rs b/contracts/hackatom/tests/integration.rs index 0035900872..06d40d8e40 100644 --- a/contracts/hackatom/tests/integration.rs +++ b/contracts/hackatom/tests/integration.rs @@ -484,7 +484,11 @@ fn execute_panic() { ); match execute_res.unwrap_err() { VmError::RuntimeErr { msg, .. } => { - assert_eq!(msg, "Wasmer runtime error: RuntimeError: unreachable") + assert!( + msg.contains("Aborted: panicked at 'This page intentionally faulted'"), + "Must contain panic message" + ); + assert!(msg.contains("contract.rs:"), "Must contain file and line"); } err => panic!("Unexpected error: {:?}", err), } diff --git a/docs/FEATURES.md b/docs/FEATURES.md new file mode 100644 index 0000000000..e6fcce6410 --- /dev/null +++ b/docs/FEATURES.md @@ -0,0 +1,100 @@ +# Features + +Features are a mechanism to negotiate functionality between a contract and an +environment (i.e. the chain that embeds cosmwasm-vm/[wasmvm]) in a very +primitive way. The contract defines required features. The environment defines +supported features. If the required features are all supported, the contract can +be used. Doing this check when the contract is first stored ensures missing +features are detected early and not when a user tries to execute a certain code +path. + +## Disambiguation + +This document is about app level features in the CosmWasm VM. Features should +not be confused with Cargo's build system features, even when connected. +Features can be implemented in any language that compiles to Wasm. + +## Required features + +The contract defines required features using a marker export function that takes +no arguments and returns no value. The name of the export needs to start with +"requires\_" followed by the name of the feature. Do yourself a favor and keep +the name all lower ASCII alphanumerical. + +An example of such markers in cosmwasm-std are those: + +```rust +#[cfg(feature = "iterator")] +#[no_mangle] +extern "C" fn requires_iterator() -> () {} + +#[cfg(feature = "staking")] +#[no_mangle] +extern "C" fn requires_staking() -> () {} + +#[cfg(feature = "stargate")] +#[no_mangle] +extern "C" fn requires_stargate() -> () {} +``` + +which in Wasm compile to this: + +``` +# ... + (export "requires_staking" (func 181)) + (export "requires_stargate" (func 181)) + (export "requires_iterator" (func 181)) +# ... + (func (;181;) (type 12) + nop) +# ... + (type (;12;) (func)) +``` + +As mentioned above, the Cargo features are independent of the features we talk +about and it is perfectly fine to have a requires\_\* export that is +unconditional in a library or a contract. + +The marker export functions can be executed, but the VM does not require such a +call to succeed. So a contract can use no-op implementation or crashing +implementation. + +## Supported features + +An instance of the main `Cache` has `supported_features` in its `CacheOptions`. +This value is set in the caller, such as +[here](https://github.com/CosmWasm/wasmvm/blob/v1.0.0-rc.0/libwasmvm/src/cache.rs#L75) +and +[here](https://github.com/CosmWasm/wasmvm/blob/v1.0.0-rc.0/libwasmvm/src/cache.rs#L62). +`features_from_csv` takes a comma separated list and returns a set of features. +This features list is set +[in keeper.go](https://github.com/CosmWasm/wasmd/blob/v0.27.0-rc0/x/wasm/keeper/keeper.go#L100) +and +[in app.go](https://github.com/CosmWasm/wasmd/blob/v0.27.0-rc0/app/app.go#L475-L496). + +## Common features + +Here is a list of features created in the past. Since features can be created +between contract and environment, we don't know them all in the VM. + +- `iterator` is for storage backends that allow range queries. Not all types of + databases do that. There are trees that don't allow it and Secret Network does + not support iterators for other technical reasons. +- `stargate` is for messages and queries that came with the Cosmos SDK upgrade + "Stargate". It primarily includes protobuf messages and IBC support. +- `staking` is for chains with the Cosmos SDK staking module. There are Cosmos + chains that don't use this (e.g. Tgrade). + +## What's a good feature? + +A good feature makes sense to be disabled. The examples above explain why the +feature is not present in some environments. + +When functionality is always present in the VM (such as a new import implemented +directly in the VM, see [#1299]), we should not use features. They just create +fragmentation in the CosmWasm ecosystem and increase the barrier for adoption. +Instead the `check_wasm_imports` check is used to validate this when the +contract is stored. + +[wasmvm]: https://github.com/CosmWasm/wasmvm +[#1299]: https://github.com/CosmWasm/cosmwasm/pull/1299 diff --git a/packages/std/Cargo.toml b/packages/std/Cargo.toml index 254d1a3f3b..6092377a57 100644 --- a/packages/std/Cargo.toml +++ b/packages/std/Cargo.toml @@ -13,6 +13,7 @@ features = ["stargate", "staking"] [features] default = ["iterator"] +abort = [] # iterator allows us to iterate over all DB items in a given range # optional as some merkle stores (like tries) don't support this # given Ethereum 1.0, 2.0, Substrate, and other major projects use Tries diff --git a/packages/std/src/exports.rs b/packages/std/src/exports.rs index f1a06a9a31..b04b89c580 100644 --- a/packages/std/src/exports.rs +++ b/packages/std/src/exports.rs @@ -20,6 +20,8 @@ use crate::ibc::{ }; use crate::imports::{ExternalApi, ExternalQuerier, ExternalStorage}; use crate::memory::{alloc, consume_region, release_buffer, Region}; +#[cfg(feature = "abort")] +use crate::panic::install_panic_handler; use crate::query::CustomQuery; use crate::results::{ContractResult, QueryResponse, Reply, Response}; use crate::serde::{from_slice, to_vec}; @@ -93,6 +95,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_instantiate( instantiate_fn, env_ptr as *mut Region, @@ -121,6 +125,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_execute( execute_fn, env_ptr as *mut Region, @@ -148,6 +154,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_migrate(migrate_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -170,6 +178,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_sudo(sudo_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -191,6 +201,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_reply(reply_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -211,6 +223,8 @@ where M: DeserializeOwned, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_query(query_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -232,6 +246,8 @@ where Q: CustomQuery, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_ibc_channel_open(contract_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -255,6 +271,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_ibc_channel_connect(contract_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -278,6 +296,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_ibc_channel_close(contract_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -302,6 +322,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_ibc_packet_receive(contract_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -326,6 +348,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_ibc_packet_ack(contract_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 @@ -351,6 +375,8 @@ where C: CustomMsg, E: ToString, { + #[cfg(feature = "abort")] + install_panic_handler(); let res = _do_ibc_packet_timeout(contract_fn, env_ptr as *mut Region, msg_ptr as *mut Region); let v = to_vec(&res).unwrap(); release_buffer(v) as u32 diff --git a/packages/std/src/imports.rs b/packages/std/src/imports.rs index f5aec89958..df03d1d640 100644 --- a/packages/std/src/imports.rs +++ b/packages/std/src/imports.rs @@ -26,6 +26,9 @@ const HUMAN_ADDRESS_BUFFER_LENGTH: usize = 90; // A complete documentation those functions is available in the VM that provides them: // https://github.com/CosmWasm/cosmwasm/blob/v1.0.0-beta/packages/vm/src/instance.rs#L89-L206 extern "C" { + #[cfg(feature = "abort")] + fn abort(source_ptr: u32); + fn db_read(key: u32) -> u32; fn db_write(key: u32, value: u32); fn db_remove(key: u32); @@ -395,3 +398,11 @@ impl Querier for ExternalQuerier { }) } } + +#[cfg(feature = "abort")] +pub fn handle_panic(message: &str) { + // keep the boxes in scope, so we free it at the end (don't cast to pointers same line as build_region) + let region = build_region(message.as_bytes()); + let region_ptr = region.as_ref() as *const Region as u32; + unsafe { abort(region_ptr) }; +} diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index be52ed8396..8b695fedaa 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -14,6 +14,7 @@ mod import_helpers; #[cfg(feature = "iterator")] mod iterator; mod math; +mod panic; mod query; mod results; mod sections; diff --git a/packages/std/src/panic.rs b/packages/std/src/panic.rs new file mode 100644 index 0000000000..7ca619e31a --- /dev/null +++ b/packages/std/src/panic.rs @@ -0,0 +1,14 @@ +/// Installs a panic handler that aborts the contract execution +/// and sends the panic message and location to the host. +/// +/// This overrides any previous panic handler. See +/// for details. +#[cfg(all(feature = "abort", target_arch = "wasm32"))] +pub fn install_panic_handler() { + use super::imports::handle_panic; + std::panic::set_hook(Box::new(|info| { + // E.g. "panicked at 'oh no (a = 3)', src/contract.rs:51:5" + let full_message = info.to_string(); + handle_panic(&full_message); + })); +} diff --git a/packages/vm/src/cache.rs b/packages/vm/src/cache.rs index da765e0cd2..d3f50777c3 100644 --- a/packages/vm/src/cache.rs +++ b/packages/vm/src/cache.rs @@ -402,9 +402,11 @@ mod tests { } fn make_stargate_testing_options() -> CacheOptions { + let mut feature = default_features(); + feature.insert("stargate".into()); CacheOptions { base_dir: TempDir::new().unwrap().into_path(), - supported_features: features_from_csv("iterator,staking,stargate"), + supported_features: feature, memory_cache_size: TESTING_MEMORY_CACHE_SIZE, instance_memory_limit: TESTING_MEMORY_LIMIT, } diff --git a/packages/vm/src/compatibility.rs b/packages/vm/src/compatibility.rs index e381ff438f..f8c894ac11 100644 --- a/packages/vm/src/compatibility.rs +++ b/packages/vm/src/compatibility.rs @@ -10,6 +10,7 @@ use crate::static_analysis::{deserialize_wasm, ExportInfo}; /// Lists all imports we provide upon instantiating the instance in Instance::from_module() /// This should be updated when new imports are added const SUPPORTED_IMPORTS: &[&str] = &[ + "env.abort", "env.db_read", "env.db_write", "env.db_remove", diff --git a/packages/vm/src/errors/vm_error.rs b/packages/vm/src/errors/vm_error.rs index 7da6b00a85..863a27ebd3 100644 --- a/packages/vm/src/errors/vm_error.rs +++ b/packages/vm/src/errors/vm_error.rs @@ -11,6 +11,12 @@ use crate::backend::BackendError; #[derive(Error, Debug)] #[non_exhaustive] pub enum VmError { + #[error("Aborted: {}", msg)] + Aborted { + msg: String, + #[cfg(feature = "backtraces")] + backtrace: Backtrace, + }, #[error("Error calling into the VM's backend: {}", source)] BackendErr { source: BackendError, @@ -142,6 +148,14 @@ pub enum VmError { } impl VmError { + pub(crate) fn aborted(msg: impl Into) -> Self { + VmError::Aborted { + msg: msg.into(), + #[cfg(feature = "backtraces")] + backtrace: Backtrace::capture(), + } + } + pub(crate) fn backend_err(original: BackendError) -> Self { VmError::BackendErr { source: original, diff --git a/packages/vm/src/imports.rs b/packages/vm/src/imports.rs index c183f2511d..73d5acad74 100644 --- a/packages/vm/src/imports.rs +++ b/packages/vm/src/imports.rs @@ -54,6 +54,9 @@ const MAX_COUNT_ED25519_BATCH: usize = 256; /// Max length for a debug message const MAX_LENGTH_DEBUG: usize = 2 * MI; +/// Max length for an abort message +const MAX_LENGTH_ABORT: usize = 2 * MI; + // Import implementations // // This block of do_* prefixed functions is tailored for Wasmer's @@ -359,6 +362,16 @@ pub fn do_debug( Ok(()) } +/// Aborts the contract and shows the given error message +pub fn do_abort( + env: &Environment, + message_ptr: u32, +) -> VmResult<()> { + let message_data = read_region(&env.memory(), message_ptr, MAX_LENGTH_ABORT)?; + let msg = String::from_utf8_lossy(&message_data); + Err(VmError::aborted(msg)) +} + /// Creates a Region in the contract, writes the given data to it and returns the memory location fn write_to_contract( env: &Environment, diff --git a/packages/vm/src/instance.rs b/packages/vm/src/instance.rs index 8110455a76..c507b055e5 100644 --- a/packages/vm/src/instance.rs +++ b/packages/vm/src/instance.rs @@ -10,7 +10,7 @@ use crate::environment::Environment; use crate::errors::{CommunicationError, VmError, VmResult}; use crate::features::required_features_from_module; use crate::imports::{ - do_addr_canonicalize, do_addr_humanize, do_addr_validate, do_db_read, do_db_remove, + do_abort, do_addr_canonicalize, do_addr_humanize, do_addr_validate, do_db_read, do_db_remove, do_db_write, do_debug, do_ed25519_batch_verify, do_ed25519_verify, do_query_chain, do_secp256k1_recover_pubkey, do_secp256k1_verify, }; @@ -180,6 +180,14 @@ where Function::new_native_with_env(store, env.clone(), do_debug), ); + // Aborts the contract execution with an error message provided by the contract. + // Takes a pointer argument of a memory region that must contain an UTF-8 encoded string. + // Ownership of both input and output pointer is not transferred to the host. + env_imports.insert( + "abort", + Function::new_native_with_env(store, env.clone(), do_abort), + ); + env_imports.insert( "query_chain", Function::new_native_with_env(store, env.clone(), do_query_chain),