diff --git a/.circleci/config.yml b/.circleci/config.yml index b1cb18cd7b..04228da1bc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -96,7 +96,7 @@ workflows: matrix: parameters: # Run with MSRV and some modern stable Rust - rust-version: ["1.74.0", "1.78.0"] + rust-version: ["1.74.0", "1.82.0"] - benchmarking: requires: - package_vm @@ -684,7 +684,8 @@ jobs: contract_hackatom: docker: - - image: rust:1.74 + # We compile this contract with the upper bound to detect issues with new Rust versions early + - image: rust:1.82 environment: RUST_BACKTRACE: 1 working_directory: ~/cosmwasm/contracts/hackatom @@ -696,9 +697,9 @@ jobs: command: rustc --version; cargo --version; rustup --version - restore_cache: keys: - - cargocache-v2-contract_hackatom-rust:1.74-{{ checksum "Cargo.lock" }} + - cargocache-v2-contract_hackatom-rust:1.82-{{ checksum "Cargo.lock" }} - check_contract: - min_version: "2.2" + min_version: "2.2.1" - save_cache: paths: - /usr/local/cargo/registry @@ -708,7 +709,7 @@ jobs: - target/wasm32-unknown-unknown/release/.fingerprint - target/wasm32-unknown-unknown/release/build - target/wasm32-unknown-unknown/release/deps - key: cargocache-v2-contract_hackatom-rust:1.74-{{ checksum "Cargo.lock" }} + key: cargocache-v2-contract_hackatom-rust:1.82-{{ checksum "Cargo.lock" }} contract_ibc_callbacks: docker: diff --git a/packages/vm/src/cache.rs b/packages/vm/src/cache.rs index 87c861a50a..9ca6602e8d 100644 --- a/packages/vm/src/cache.rs +++ b/packages/vm/src/cache.rs @@ -776,6 +776,24 @@ mod tests { cache.store_code(&wasm, false, true).unwrap(); } + #[test] + fn func_ref_in_type_fails() { + let wasm = wat::parse_str( + r#"(module + (type $x1 (func (param funcref))) + (export "memory" (memory 0)) + (import "env" "abort" (func $f (type $x1))) + (memory 3) + )"#, + ) + .unwrap(); + + let cache: Cache = + unsafe { Cache::new(make_testing_options()).unwrap() }; + + cache.store_code(&wasm, true, true).unwrap_err(); + } + #[test] fn load_wasm_works() { let cache: Cache = diff --git a/packages/vm/src/errors/vm_error.rs b/packages/vm/src/errors/vm_error.rs index 5a6260418a..d99f605e20 100644 --- a/packages/vm/src/errors/vm_error.rs +++ b/packages/vm/src/errors/vm_error.rs @@ -284,6 +284,19 @@ impl From for VmError { } } +impl From for VmError { + fn from(value: crate::parsed_wasm::ValidatorError) -> Self { + match value { + crate::parsed_wasm::ValidatorError::BinaryReaderError(e) => e.into(), + crate::parsed_wasm::ValidatorError::Custom(msg) => { + VmError::static_validation_err(format!( + "Wasm bytecode could not be deserialized. Deserialization error: \"{msg}\"" + )) + } + } + } +} + impl From for VmError { fn from(original: wasmer::ExportError) -> Self { VmError::resolve_err(format!("Could not get export: {original}")) diff --git a/packages/vm/src/parsed_wasm.rs b/packages/vm/src/parsed_wasm.rs index 11c67210d1..50a2f2a58e 100644 --- a/packages/vm/src/parsed_wasm.rs +++ b/packages/vm/src/parsed_wasm.rs @@ -1,8 +1,8 @@ use std::{fmt, mem, str}; use wasmer::wasmparser::{ - BinaryReaderError, CompositeType, Export, FuncToValidate, FunctionBody, Import, MemoryType, - Parser, Payload, TableType, ValidPayload, Validator, ValidatorResources, WasmFeatures, + CompositeType, Export, FuncToValidate, FuncValidator, FunctionBody, Import, MemoryType, Parser, + Payload, TableType, ValidPayload, Validator, ValidatorResources, VisitOperator, WasmFeatures, }; use crate::{VmError, VmResult}; @@ -31,7 +31,7 @@ impl fmt::Debug for OpaqueDebug { pub enum FunctionValidator<'a> { Pending(OpaqueDebug, FunctionBody<'a>)>>), Success, - Error(BinaryReaderError), + Error(ValidatorError), } impl<'a> FunctionValidator<'a> { @@ -210,7 +210,7 @@ impl<'a> ParsedWasm<'a> { let mut allocations = <_>::default(); for (func, body) in mem::take(funcs) { let mut validator = func.into_validator(allocations); - validator.validate(&body)?; + validate_function(&mut validator, &body)?; allocations = validator.into_allocations(); } Ok(()) @@ -229,6 +229,110 @@ impl<'a> ParsedWasm<'a> { } } +/// Copy of [wasmer::wasmparser::FuncValidator::validate], but with partial support for reference-types. +fn validate_function( + validator: &mut FuncValidator, + body: &FunctionBody<'_>, +) -> Result<(), ValidatorError> { + let mut reader = body.get_binary_reader(); + reader.allow_memarg64(false); + + validator.read_locals(&mut reader)?; + while !reader.eof() { + // We need to wrap the reader in a PartialRefTypeValidator to allow LEB-128 encoded table indices for call_indirect. + // That feature was introduced in the reference-types proposal. + reader.visit_operator(&mut PartialRefTypeValidator::new( + validator.visitor(reader.original_position()), + reader.original_position(), + ))??; + } + Ok(validator.finish(reader.original_position())?) +} + +/// Internal macro to forward all visit operators to the inner validator, except for `call_indirect`. +macro_rules! implement_visit_operator { + ($(@$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident)*) => { + $( + // delegate to sub-invocation of this macro + implement_visit_operator!(visit_one @$proposal $op $({ $($arg: $argty),* })? => $visit); + )* + }; + (visit_one @mvp CallIndirect $($rest:tt)*) => { + fn visit_call_indirect( + &mut self, + type_index: u32, + table_index: u32, + _table_byte: u8, + ) -> Self::Output { + if table_index != 0 { + return Err(ValidatorError::custom(format!( + "reference-types not fully supported: table_index must be zero (at offset 0x{:x})", + self.offset + ))); + } + // ignore table_byte, since we know that table_index is 0. That's all we care about. + Ok(self.inner.visit_call_indirect(type_index, table_index, 0)?) + } + }; + (visit_one @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident) => { + fn $visit(&mut self $($(,$arg: $argty)*)?) -> Self::Output { + Ok(self.inner.$visit($($($arg),*)?)?) + } + }; +} + +/// This is a validator that forwards everything to its inner validator, except for `call_indirect` operations. +/// For those, it will check if the table index is 0 and will ignore the table byte. +/// This is in order to not reject contracts that use the new reference-types proposal layout for `call_indirect`. +/// +/// Before that proposal, the table_byte was always 0 and defined the table index, but now the table index is LEB128 encoded. +/// Note that in newer versions of wasmparser, this check was moved into the [`wasmer::wasmparser::BinaryReader`], +/// so the reader needs to be wrapped instead of the validator. +struct PartialRefTypeValidator<'a, V> { + inner: V, + offset: usize, + _phantom: std::marker::PhantomData<&'a ()>, +} + +impl PartialRefTypeValidator<'_, V> { + fn new(inner: V, offset: usize) -> Self { + Self { + inner, + offset, + _phantom: std::marker::PhantomData, + } + } +} + +impl<'a, V> VisitOperator<'a> for PartialRefTypeValidator<'a, V> +where + V: VisitOperator<'a, Output = wasmer::wasmparser::Result<()>>, +{ + type Output = Result<(), ValidatorError>; + + wasmer::wasmparser::for_each_operator!(implement_visit_operator); +} + +/// Custom error type for [`PartialRefTypeValidator`]. +/// We need a custom type because we cannot construct a [`wasmer::wasmparser::BinaryReaderError`] directly. +#[derive(Debug, Clone)] +pub(crate) enum ValidatorError { + BinaryReaderError(wasmer::wasmparser::BinaryReaderError), + Custom(String), +} + +impl From for ValidatorError { + fn from(err: wasmer::wasmparser::BinaryReaderError) -> Self { + Self::BinaryReaderError(err) + } +} + +impl ValidatorError { + pub fn custom(err: impl Into) -> Self { + Self::Custom(err.into()) + } +} + #[cfg(test)] mod test { use super::ParsedWasm;