From f5bf24f3acadca2892afd58cc3ce5fdeb932d492 Mon Sep 17 00:00:00 2001 From: Christoph Otter Date: Mon, 2 Sep 2024 15:28:52 +0200 Subject: [PATCH] Fix --- Cargo.lock | 1 + contracts/burner/Cargo.lock | 1 + contracts/crypto-verify/Cargo.lock | 1 + contracts/cyberpunk/Cargo.lock | 1 + contracts/empty/Cargo.lock | 1 + contracts/floaty/Cargo.lock | 1 + contracts/hackatom/Cargo.lock | 1 + contracts/ibc-callbacks/Cargo.lock | 1 + contracts/ibc-reflect-send/Cargo.lock | 1 + contracts/ibc-reflect/Cargo.lock | 1 + contracts/queue/Cargo.lock | 1 + contracts/reflect/Cargo.lock | 1 + contracts/staking/Cargo.lock | 1 + contracts/virus/Cargo.lock | 1 + packages/vm/Cargo.toml | 1 + packages/vm/src/instance.rs | 8 +- packages/vm/src/modules/file_system_cache.rs | 14 +- packages/vm/src/wasm_backend/engine.rs | 17 +- packages/vm/src/wasm_backend/metering.rs | 263 +++++++++++++++++++ packages/vm/src/wasm_backend/mod.rs | 1 + 20 files changed, 298 insertions(+), 20 deletions(-) create mode 100644 packages/vm/src/wasm_backend/metering.rs diff --git a/Cargo.lock b/Cargo.lock index 0aa633b9c1..5aee726c19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -688,6 +688,7 @@ dependencies = [ "wasm-encoder 0.205.0", "wasmer", "wasmer-middlewares", + "wasmer-types", "wat", ] diff --git a/contracts/burner/Cargo.lock b/contracts/burner/Cargo.lock index f67e8bebb5..6b99764900 100644 --- a/contracts/burner/Cargo.lock +++ b/contracts/burner/Cargo.lock @@ -449,6 +449,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/crypto-verify/Cargo.lock b/contracts/crypto-verify/Cargo.lock index 50c7e4d348..ca21e331c2 100644 --- a/contracts/crypto-verify/Cargo.lock +++ b/contracts/crypto-verify/Cargo.lock @@ -444,6 +444,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/cyberpunk/Cargo.lock b/contracts/cyberpunk/Cargo.lock index a79e93d697..d831858166 100644 --- a/contracts/cyberpunk/Cargo.lock +++ b/contracts/cyberpunk/Cargo.lock @@ -461,6 +461,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/empty/Cargo.lock b/contracts/empty/Cargo.lock index 5c95b66cee..28a0cce739 100644 --- a/contracts/empty/Cargo.lock +++ b/contracts/empty/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/floaty/Cargo.lock b/contracts/floaty/Cargo.lock index 497437c686..0ee048e1f4 100644 --- a/contracts/floaty/Cargo.lock +++ b/contracts/floaty/Cargo.lock @@ -450,6 +450,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/hackatom/Cargo.lock b/contracts/hackatom/Cargo.lock index f3860cdd4c..3ab01a3c54 100644 --- a/contracts/hackatom/Cargo.lock +++ b/contracts/hackatom/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/ibc-callbacks/Cargo.lock b/contracts/ibc-callbacks/Cargo.lock index 7b61420bea..8dc8d85302 100644 --- a/contracts/ibc-callbacks/Cargo.lock +++ b/contracts/ibc-callbacks/Cargo.lock @@ -450,6 +450,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/ibc-reflect-send/Cargo.lock b/contracts/ibc-reflect-send/Cargo.lock index 45da080a9d..4eb973cdf8 100644 --- a/contracts/ibc-reflect-send/Cargo.lock +++ b/contracts/ibc-reflect-send/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/ibc-reflect/Cargo.lock b/contracts/ibc-reflect/Cargo.lock index bd7ff40bae..84a9fae9f5 100644 --- a/contracts/ibc-reflect/Cargo.lock +++ b/contracts/ibc-reflect/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/queue/Cargo.lock b/contracts/queue/Cargo.lock index 7c80c06e8b..1e78514dc8 100644 --- a/contracts/queue/Cargo.lock +++ b/contracts/queue/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/reflect/Cargo.lock b/contracts/reflect/Cargo.lock index c6233732be..503eeffb92 100644 --- a/contracts/reflect/Cargo.lock +++ b/contracts/reflect/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/staking/Cargo.lock b/contracts/staking/Cargo.lock index e8fe84c3f6..de46fc4191 100644 --- a/contracts/staking/Cargo.lock +++ b/contracts/staking/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/contracts/virus/Cargo.lock b/contracts/virus/Cargo.lock index 8f8497dc37..6c9e9e1a73 100644 --- a/contracts/virus/Cargo.lock +++ b/contracts/virus/Cargo.lock @@ -438,6 +438,7 @@ dependencies = [ "tracing", "wasmer", "wasmer-middlewares", + "wasmer-types", ] [[package]] diff --git a/packages/vm/Cargo.toml b/packages/vm/Cargo.toml index acfdc1dfaa..45ec434b63 100644 --- a/packages/vm/Cargo.toml +++ b/packages/vm/Cargo.toml @@ -58,6 +58,7 @@ sha2 = "0.10.3" thiserror = "1.0.26" wasmer = { version = "=4.3.3", default-features = false, features = ["singlepass"] } wasmer-middlewares = "=4.3.3" +wasmer-types = "=4.3.3" strum = { version = "0.26.2", default-features = false, features = ["derive"] } # For heap profiling. Only used in the "heap_profiling" example. This has to be a non-dev dependency # because cargo currently does not support optional dev-dependencies. diff --git a/packages/vm/src/instance.rs b/packages/vm/src/instance.rs index 7a52013b58..04b2260db7 100644 --- a/packages/vm/src/instance.rs +++ b/packages/vm/src/instance.rs @@ -914,7 +914,7 @@ mod tests { let report2 = instance.create_gas_report(); assert_eq!(report2.used_externally, 251); - assert_eq!(report2.used_internally, 16995280); + assert_eq!(report2.used_internally, 21059380); assert_eq!(report2.limit, LIMIT); assert_eq!( report2.remaining, @@ -1105,7 +1105,7 @@ mod tests { .unwrap(); let init_used = orig_gas - instance.get_gas_left(); - assert_eq!(init_used, 16995531); + assert_eq!(init_used, 21059631); } #[test] @@ -1130,7 +1130,7 @@ mod tests { .unwrap(); let execute_used = gas_before_execute - instance.get_gas_left(); - assert_eq!(execute_used, 19589666); + assert_eq!(execute_used, 25160611); } #[test] @@ -1173,6 +1173,6 @@ mod tests { ); let query_used = gas_before_query - instance.get_gas_left(); - assert_eq!(query_used, 11942871); + assert_eq!(query_used, 15106521); } } diff --git a/packages/vm/src/modules/file_system_cache.rs b/packages/vm/src/modules/file_system_cache.rs index f0c240f19a..76c1c5df82 100644 --- a/packages/vm/src/modules/file_system_cache.rs +++ b/packages/vm/src/modules/file_system_cache.rs @@ -63,7 +63,13 @@ use super::CachedModule; /// Module compatibility between Wasmer versions is not guaranteed. /// - **v10**:
/// New version because of Metering middleware change. -const MODULE_SERIALIZATION_VERSION: &str = "v10"; +/// - **v11**:
+/// Reserved for 1.5.x branch. +/// - **v12**:
+/// Reserved for 2.0.x branch. +/// - **v13**:
+/// New version because of Metering middleware change. +const MODULE_SERIALIZATION_VERSION: &str = "v13"; /// Representation of a directory that contains compiled Wasm artifacts. pub struct FileSystemCache { @@ -315,7 +321,7 @@ mod tests { cache.store(&checksum, &module).unwrap(); let mut globber = glob::glob(&format!( - "{}/v10-wasmer7/**/{}.module", + "{}/v13-wasmer7/**/{}.module", tmp_dir.path().to_string_lossy(), checksum )) @@ -398,9 +404,9 @@ mod tests { assert_eq!( p.as_os_str(), if cfg!(windows) { - "modules\\v10-wasmer17\\x86_64-nintendo-fuchsia-gnu-coff-01E9F9FE" + "modules\\v13-wasmer17\\x86_64-nintendo-fuchsia-gnu-coff-01E9F9FE" } else { - "modules/v10-wasmer17/x86_64-nintendo-fuchsia-gnu-coff-01E9F9FE" + "modules/v13-wasmer17/x86_64-nintendo-fuchsia-gnu-coff-01E9F9FE" } ); } diff --git a/packages/vm/src/wasm_backend/engine.rs b/packages/vm/src/wasm_backend/engine.rs index a6bdb62e09..1e481573fb 100644 --- a/packages/vm/src/wasm_backend/engine.rs +++ b/packages/vm/src/wasm_backend/engine.rs @@ -3,12 +3,12 @@ use wasmer::NativeEngineExt; use wasmer::{ sys::BaseTunables, wasmparser::Operator, CompilerConfig, Engine, Pages, Target, WASM_PAGE_SIZE, }; -use wasmer_middlewares::Metering; use crate::size::Size; use super::gatekeeper::Gatekeeper; use super::limiting_tunables::LimitingTunables; +use super::metering::{is_accounting, Metering}; /// WebAssembly linear memory objects have sizes measured in pages. Each page /// is 65536 (2^16) bytes. In WebAssembly version 1, a linear memory can have at @@ -25,17 +25,10 @@ fn cost(operator: &Operator) -> u64 { // precise enough to derive insights from it. const GAS_PER_OPERATION: u64 = 115; - match operator { - Operator::Loop { .. } - | Operator::End - | Operator::Else - | Operator::Br { .. } - | Operator::BrTable { .. } - | Operator::BrIf { .. } - | Operator::Call { .. } - | Operator::CallIndirect { .. } - | Operator::Return => GAS_PER_OPERATION * 14, - _ => GAS_PER_OPERATION, + if is_accounting(operator) { + GAS_PER_OPERATION * 14 + } else { + GAS_PER_OPERATION } } diff --git a/packages/vm/src/wasm_backend/metering.rs b/packages/vm/src/wasm_backend/metering.rs new file mode 100644 index 0000000000..e69f42fdbc --- /dev/null +++ b/packages/vm/src/wasm_backend/metering.rs @@ -0,0 +1,263 @@ +use std::fmt; +use std::sync::{Arc, Mutex}; +use wasmer::wasmparser::{BlockType as WpTypeOrFuncType, Operator}; +use wasmer::{ + ExportIndex, FunctionMiddleware, GlobalInit, GlobalType, LocalFunctionIndex, MiddlewareError, + MiddlewareReaderState, ModuleMiddleware, Mutability, Type, +}; +use wasmer_types::{GlobalIndex, ModuleInfo}; + +#[derive(Clone)] +struct MeteringGlobalIndexes(GlobalIndex, GlobalIndex); + +impl MeteringGlobalIndexes { + /// The global index in the current module for remaining points. + fn remaining_points(&self) -> GlobalIndex { + self.0 + } + + /// The global index in the current module for a boolean indicating whether points are exhausted + /// or not. + /// This boolean is represented as a i32 global: + /// * 0: there are remaining points + /// * 1: points have been exhausted + fn points_exhausted(&self) -> GlobalIndex { + self.1 + } +} + +impl fmt::Debug for MeteringGlobalIndexes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MeteringGlobalIndexes") + .field("remaining_points", &self.remaining_points()) + .field("points_exhausted", &self.points_exhausted()) + .finish() + } +} + +/// The module-level metering middleware. +/// +/// # Panic +/// +/// An instance of `Metering` should _not_ be shared among different +/// modules, since it tracks module-specific information like the +/// global index to store metering state. Attempts to use a `Metering` +/// instance from multiple modules will result in a panic. +/// +/// # Example +/// +/// ```rust +/// use std::sync::Arc; +/// use wasmer::{wasmparser::Operator, CompilerConfig}; +/// use wasmer_middlewares::Metering; +/// +/// fn create_metering_middleware(compiler_config: &mut dyn CompilerConfig) { +/// // Let's define a dummy cost function, +/// // which counts 1 for all operators. +/// let cost_function = |_operator: &Operator| -> u64 { 1 }; +/// +/// // Let's define the initial limit. +/// let initial_limit = 10; +/// +/// // Let's creating the metering middleware. +/// let metering = Arc::new(Metering::new( +/// initial_limit, +/// cost_function +/// )); +/// +/// // Finally, let's push the middleware. +/// compiler_config.push_middleware(metering); +/// } +/// ``` +pub struct Metering u64 + Send + Sync> { + /// Initial limit of points. + initial_limit: u64, + + /// Function that maps each operator to a cost in "points". + cost_function: Arc, + + /// The global indexes for metering points. + global_indexes: Mutex>, +} + +/// The function-level metering middleware. +pub struct FunctionMetering u64 + Send + Sync> { + /// Function that maps each operator to a cost in "points". + cost_function: Arc, + + /// The global indexes for metering points. + global_indexes: MeteringGlobalIndexes, + + /// Accumulated cost of the current basic block. + accumulated_cost: u64, +} + +impl u64 + Send + Sync> Metering { + /// Creates a `Metering` middleware. + pub fn new(initial_limit: u64, cost_function: F) -> Self { + Self { + initial_limit, + cost_function: Arc::new(cost_function), + global_indexes: Mutex::new(None), + } + } +} + +/// Returns `true` if and only if the given operator is an accounting operator. +/// Accounting operators do additional work to track the metering points. +pub fn is_accounting(operator: &Operator) -> bool { + matches!( + operator, + Operator::Loop { .. } // loop headers are branch targets + | Operator::End // block ends are branch targets + | Operator::If { .. } // branch source, "if" can branch to else branch + | Operator::Else // "else" is the "end" of an if branch + | Operator::Br { .. } // branch source + | Operator::BrTable { .. } // branch source + | Operator::BrIf { .. } // branch source + | Operator::Call { .. } // function call - branch source + | Operator::CallIndirect { .. } // function call - branch source + | Operator::Return // end of function - branch source + // exceptions proposal + | Operator::Throw { .. } // branch source + | Operator::ThrowRef // branch source + | Operator::Rethrow { .. } // branch source + | Operator::Delegate { .. } // branch source + | Operator::Catch { .. } // branch target + // tail_call proposal + | Operator::ReturnCall { .. } // branch source + | Operator::ReturnCallIndirect { .. } // branch source + // gc proposal + | Operator::BrOnCast { .. } // branch source + | Operator::BrOnCastFail { .. } // branch source + // function_references proposal + | Operator::CallRef { .. } // branch source + | Operator::ReturnCallRef { .. } // branch source + | Operator::BrOnNull { .. } // branch source + | Operator::BrOnNonNull { .. } // branch source + ) +} + +impl u64 + Send + Sync> fmt::Debug for Metering { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Metering") + .field("initial_limit", &self.initial_limit) + .field("cost_function", &"") + .field("global_indexes", &self.global_indexes) + .finish() + } +} + +impl u64 + Send + Sync + 'static> ModuleMiddleware for Metering { + /// Generates a `FunctionMiddleware` for a given function. + fn generate_function_middleware(&self, _: LocalFunctionIndex) -> Box { + Box::new(FunctionMetering { + cost_function: self.cost_function.clone(), + global_indexes: self.global_indexes.lock().unwrap().clone().unwrap(), + accumulated_cost: 0, + }) + } + + /// Transforms a `ModuleInfo` struct in-place. This is called before application on functions begins. + fn transform_module_info(&self, module_info: &mut ModuleInfo) { + let mut global_indexes = self.global_indexes.lock().unwrap(); + + if global_indexes.is_some() { + panic!("Metering::transform_module_info: Attempting to use a `Metering` middleware from multiple modules."); + } + + // Append a global for remaining points and initialize it. + let remaining_points_global_index = module_info + .globals + .push(GlobalType::new(Type::I64, Mutability::Var)); + + module_info + .global_initializers + .push(GlobalInit::I64Const(self.initial_limit as i64)); + + module_info.exports.insert( + "wasmer_metering_remaining_points".to_string(), + ExportIndex::Global(remaining_points_global_index), + ); + + // Append a global for the exhausted points boolean and initialize it. + let points_exhausted_global_index = module_info + .globals + .push(GlobalType::new(Type::I32, Mutability::Var)); + + module_info + .global_initializers + .push(GlobalInit::I32Const(0)); + + module_info.exports.insert( + "wasmer_metering_points_exhausted".to_string(), + ExportIndex::Global(points_exhausted_global_index), + ); + + *global_indexes = Some(MeteringGlobalIndexes( + remaining_points_global_index, + points_exhausted_global_index, + )) + } +} + +impl u64 + Send + Sync> fmt::Debug for FunctionMetering { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FunctionMetering") + .field("cost_function", &"") + .field("global_indexes", &self.global_indexes) + .finish() + } +} + +impl u64 + Send + Sync> FunctionMiddleware for FunctionMetering { + fn feed<'a>( + &mut self, + operator: Operator<'a>, + state: &mut MiddlewareReaderState<'a>, + ) -> Result<(), MiddlewareError> { + // Get the cost of the current operator, and add it to the accumulator. + // This needs to be done before the metering logic, to prevent operators like `Call` from escaping metering in some + // corner cases. + self.accumulated_cost += (self.cost_function)(&operator); + + // Possible sources and targets of a branch. Finalize the cost of the previous basic block and perform necessary checks. + if is_accounting(&operator) && self.accumulated_cost > 0 { + state.extend(&[ + // if unsigned(globals[remaining_points_index]) < unsigned(self.accumulated_cost) { throw(); } + Operator::GlobalGet { + global_index: self.global_indexes.remaining_points().as_u32(), + }, + Operator::I64Const { + value: self.accumulated_cost as i64, + }, + Operator::I64LtU, + Operator::If { + blockty: WpTypeOrFuncType::Empty, + }, + Operator::I32Const { value: 1 }, + Operator::GlobalSet { + global_index: self.global_indexes.points_exhausted().as_u32(), + }, + Operator::Unreachable, + Operator::End, + // globals[remaining_points_index] -= self.accumulated_cost; + Operator::GlobalGet { + global_index: self.global_indexes.remaining_points().as_u32(), + }, + Operator::I64Const { + value: self.accumulated_cost as i64, + }, + Operator::I64Sub, + Operator::GlobalSet { + global_index: self.global_indexes.remaining_points().as_u32(), + }, + ]); + + self.accumulated_cost = 0; + } + state.push_operator(operator); + + Ok(()) + } +} diff --git a/packages/vm/src/wasm_backend/mod.rs b/packages/vm/src/wasm_backend/mod.rs index a7c8bf0570..1c330dc120 100644 --- a/packages/vm/src/wasm_backend/mod.rs +++ b/packages/vm/src/wasm_backend/mod.rs @@ -2,6 +2,7 @@ mod compile; mod engine; mod gatekeeper; mod limiting_tunables; +mod metering; #[cfg(test)] pub use engine::make_compiler_config;