From f5a8bb09008cfaf12917591bf03b6321d3854830 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 14:42:38 +0000 Subject: [PATCH 1/5] fix: Add timeout configuration for WASM execution Co-authored-by: sandi --- Cargo.lock | 12 ++ crates/runtime/Cargo.toml | 7 +- crates/runtime/src/errors.rs | 2 + crates/runtime/src/lib.rs | 237 +++++++++++++++++++++++++++++++---- crates/runtime/src/logic.rs | 20 +++ 5 files changed, 255 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 727ab8cfd..228e69fd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2335,6 +2335,7 @@ dependencies = [ "ureq", "wasmer", "wasmer-compiler-cranelift", + "wasmer-middlewares", "wasmer-types", "wat", ] @@ -11618,6 +11619,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "wasmer-middlewares" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4fdc1455c09a2b8e3aae9df8e163430d87353a2b467a3c48ceeb3e3bbeeed69" +dependencies = [ + "wasmer", + "wasmer-types", + "wasmer-vm", +] + [[package]] name = "wasmer-types" version = "6.1.0" diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 9c4c8008e..384ea4cc2 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -25,8 +25,9 @@ tokio-stream.workspace = true tracing.workspace = true ureq.workspace = true wasmer.workspace = true +wasmer-compiler-cranelift.workspace = true +wasmer-middlewares = "6.1.0" wasmer-types.workspace = true -wasmer-compiler-cranelift = { workspace = true, optional = true } calimero-primitives.workspace = true calimero-node-primitives.workspace = true @@ -46,4 +47,6 @@ wat.workspace = true [features] host-traces = ["owo-colors"] -profiling = ["wasmer-compiler-cranelift"] +# Enables PerfMap profiling support for WASM stack traces when ENABLE_WASMER_PROFILING=true. +# Note: Cranelift compiler is always included for operation limit metering. +profiling = [] diff --git a/crates/runtime/src/errors.rs b/crates/runtime/src/errors.rs index 70bf0f782..52caccd2f 100644 --- a/crates/runtime/src/errors.rs +++ b/crates/runtime/src/errors.rs @@ -187,6 +187,8 @@ pub enum WasmTrap { Unreachable, #[error("unaligned atomic operation")] UnalignedAtomic, + #[error("execution exceeded timeout")] + ExecutionTimeout, #[error("indeterminate trap")] Indeterminate, } diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index ce01f2e3a..2fb787ce4 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -1,14 +1,14 @@ use std::panic::{catch_unwind, AssertUnwindSafe}; +use std::sync::Arc; use calimero_node_primitives::client::NodeClient; use calimero_primitives::context::ContextId; use calimero_primitives::identity::PublicKey; use tracing::{debug, error, info}; -use wasmer::{DeserializeError, Instance, SerializeError, Store}; - -// Profiling feature: Only compile these imports when profiling feature is enabled -#[cfg(feature = "profiling")] use wasmer::sys::{CompilerConfig, Cranelift}; +use wasmer::{DeserializeError, Instance, SerializeError, Store}; +use wasmer_middlewares::metering::{self, MeteringPoints}; +use wasmer_middlewares::Metering; mod constants; mod constraint; @@ -88,9 +88,7 @@ pub struct Engine { impl Default for Engine { fn default() -> Self { let limits = VMLimits::default(); - - let engine = Self::create_engine(); - + let engine = Self::create_engine_with_metering(&limits); Self::new(engine, limits) } } @@ -107,8 +105,19 @@ impl Engine { Self { limits, engine } } - /// Create an engine, using Cranelift compiler for profiling builds with PerfMap support - fn create_engine() -> wasmer::Engine { + /// Create an engine with custom VMLimits. + /// This is the preferred way to create an Engine when you need custom limits. + #[must_use] + pub fn with_limits(limits: VMLimits) -> Self { + let engine = Self::create_engine_with_metering(&limits); + Self::new(engine, limits) + } + + /// Create an engine with metering middleware for execution limits. + fn create_engine_with_metering(limits: &VMLimits) -> wasmer::Engine { + let mut compiler = Cranelift::default(); + + // Enable profiling if requested #[cfg(feature = "profiling")] { if std::env::var("ENABLE_WASMER_PROFILING") @@ -116,20 +125,35 @@ impl Engine { .unwrap_or(false) { info!("Enabling Wasmer PerfMap profiling for WASM stack traces"); - // Create Cranelift config and enable PerfMap file generation - let mut config = Cranelift::default(); - config.enable_perfmap(); - return wasmer::Engine::from(config); + compiler.enable_perfmap(); } } - // Default engine (no profiling) - wasmer::Engine::default() + // Add metering middleware if operation limit is set + if limits.max_operations > 0 { + // Cost function: each WASM operation costs 1 unit + let metering = Arc::new(Metering::new(limits.max_operations, |_op| 1)); + compiler.push_middleware(metering); + debug!( + max_operations = limits.max_operations, + "Execution limit metering enabled" + ); + } + + wasmer::Engine::from(compiler) } + /// Create a headless engine for running precompiled modules. + /// + /// **Note:** Headless engines cannot enforce operation limits because the metering + /// middleware must be applied at compile time. Modules loaded with a headless engine + /// will run without operation limits regardless of `VMLimits.max_operations`. + /// For operation limit enforcement, use a full engine to compile modules. #[must_use] pub fn headless() -> Self { - let limits = VMLimits::default(); + let mut limits = VMLimits::default(); + // Disable operation limit for headless engines since metering requires compilation + limits.max_operations = 0; // Headless engines lack a compiler, so Wasmer skips perf.map generation. // For profiling, use a full engine to enable WASM symbol resolution. @@ -140,7 +164,7 @@ impl Engine { .unwrap_or(false) { debug!("Using profiling-enabled engine for precompiled module (required for perf.map generation)"); - let engine = Self::create_engine(); + let engine = Self::create_engine_with_metering(&limits); return Self::new(engine, limits); } } @@ -250,6 +274,9 @@ impl Module { let imports = logic.imports(&mut store); + // Get the operation limit for execution + let max_ops = self.limits.max_operations; + // Wrap WASM execution in catch_unwind to prevent panics from crashing the node. // This catches any unhandled panics during instance creation, memory access, // or function execution and converts them to proper error responses. @@ -262,6 +289,7 @@ impl Module { method, &context_id, self.limits.max_method_name_length, + max_ops, ) })); @@ -317,6 +345,7 @@ impl Module { method: &str, context_id: &ContextId, max_method_name_length: u64, + max_ops: u64, ) -> RuntimeResult> { // Validate method name before attempting to look it up if let Err(err) = validate_method_name(method, max_method_name_length) { @@ -346,6 +375,18 @@ impl Module { // Attach memory to VMLogic, which will clean it up in finish() let _ = logic.with_memory(memory); + // Set execution limit before any WASM function calls (including hooks) + // to ensure all execution is protected by the operation limit. + // + // **Important:** The operation budget (`max_ops`) is shared between all WASM + // function calls in this execution, including registration hooks like + // `__calimero_register_merge`. If hooks consume significant operations, + // the remaining budget for the user method will be reduced accordingly. + if max_ops > 0 { + metering::set_remaining_points(store, &instance, max_ops); + debug!(%context_id, method, max_ops, "Execution limit set for WASM method"); + } + // Call the auto-generated registration hook if it exists. // This enables automatic CRDT merge during sync. // Note: This is optional and failures are non-fatal (especially for JS apps). @@ -426,12 +467,39 @@ impl Module { "WASM method execution failed" ); + // Check if execution exceeded operation limit + if max_ops > 0 { + if let MeteringPoints::Exhausted = metering::get_remaining_points(store, &instance) + { + error!(%context_id, method, "WASM execution exceeded operation limit"); + return Ok(Some(FunctionCallError::WasmTrap( + errors::WasmTrap::ExecutionTimeout, + ))); + } + } + return match err.downcast::() { Ok(err) => Ok(Some(err.try_into()?)), Err(err) => Ok(Some(err.into())), }; } + // Log execution stats if metering is enabled + if max_ops > 0 { + if let MeteringPoints::Remaining(remaining) = + metering::get_remaining_points(store, &instance) + { + let consumed = max_ops.saturating_sub(remaining); + debug!( + %context_id, + method, + consumed, + remaining, + "WASM execution completed within operation limit" + ); + } + } + Ok(None) } } @@ -967,7 +1035,7 @@ mod wasm_integration_tests { let mut limits = VMLimits::default(); limits.max_module_size = 10; // 10 bytes - way too small for any valid module - let engine = Engine::new(wasmer::Engine::default(), limits); + let engine = Engine::with_limits(limits); // Attempt to compile should fail due to size limit let result = engine.compile(&wasm); @@ -1000,7 +1068,7 @@ mod wasm_integration_tests { let mut limits = VMLimits::default(); limits.max_module_size = 1024 * 1024; // 1 MiB - plenty of room - let engine = Engine::new(wasmer::Engine::default(), limits); + let engine = Engine::with_limits(limits); // Compilation should succeed let result = engine.compile(&wasm); @@ -1028,7 +1096,7 @@ mod wasm_integration_tests { let mut limits = VMLimits::default(); limits.max_module_size = wasm.len() as u64; // Exact size limit - let engine = Engine::new(wasmer::Engine::default(), limits); + let engine = Engine::with_limits(limits); // Compilation should succeed because check is `size > limit`, not `size >= limit` let result = engine.compile(&wasm); @@ -1080,7 +1148,7 @@ mod wasm_integration_tests { let mut limits = VMLimits::default(); limits.max_module_size = 0; - let engine = Engine::new(wasmer::Engine::default(), limits); + let engine = Engine::with_limits(limits); // Any non-empty module should be rejected let result = engine.compile(&wasm); @@ -1109,4 +1177,131 @@ mod wasm_integration_tests { Err(other) => panic!("Expected CompilationError for empty bytes, got: {other:?}"), } } + + /// Test that execution with custom operation limit works + #[test] + fn test_wasm_with_custom_operation_limit() { + // A minimal WASM module that should complete quickly + let wat = r#" + (module + (memory (export "memory") 1) + (func (export "simple_func")) + ) + "#; + let wasm = wat::parse_str(wat).expect("Failed to parse WAT"); + + // Create engine with custom operation limit + let mut limits = VMLimits::default(); + limits.max_operations = 1_000_000; // 1 million operations + let engine = Engine::with_limits(limits); + let module = engine.compile(&wasm).expect("Failed to compile module"); + + let mut storage = InMemoryStorage::default(); + let outcome = module + .run( + [0; 32].into(), + [0; 32].into(), + "simple_func", + &[], + &mut storage, + None, + None, + ) + .expect("Failed to run module"); + + // Should complete successfully within operation limit + assert!( + outcome.returns.is_ok(), + "Expected successful execution within operation limit, got: {:?}", + outcome.returns + ); + } + + /// Test that an infinite loop triggers ExecutionTimeout error + /// This verifies the operation limit actually prevents infinite loops. + #[test] + fn test_wasm_infinite_loop_operation_limit() { + // A WASM module with an infinite loop + let wat = r#" + (module + (memory (export "memory") 1) + (func (export "infinite_loop") + (local $i i32) + (loop $loop + ;; Increment counter + (local.set $i (i32.add (local.get $i) (i32.const 1))) + ;; Loop forever + (br $loop) + ) + ) + ) + "#; + let wasm = wat::parse_str(wat).expect("Failed to parse WAT"); + + // Create engine with a low operation limit to trigger quickly + let mut limits = VMLimits::default(); + limits.max_operations = 10_000; // 10,000 operations - will be exceeded quickly + let engine = Engine::with_limits(limits); + let module = engine.compile(&wasm).expect("Failed to compile module"); + + let mut storage = InMemoryStorage::default(); + let outcome = module + .run( + [0; 32].into(), + [0; 32].into(), + "infinite_loop", + &[], + &mut storage, + None, + None, + ) + .expect("Failed to run module"); + + // Infinite loop should trigger ExecutionTimeout when operation limit exceeded + match &outcome.returns { + Err(FunctionCallError::WasmTrap(errors::WasmTrap::ExecutionTimeout)) => { + // Expected - operation limit stopped the infinite loop + } + other => panic!("Expected WasmTrap::ExecutionTimeout error, got: {other:?}"), + } + } + + /// Test that execution with operation limit disabled (zero) works + #[test] + fn test_wasm_operation_limit_disabled() { + // A minimal WASM module that should complete quickly + let wat = r#" + (module + (memory (export "memory") 1) + (func (export "simple_func")) + ) + "#; + let wasm = wat::parse_str(wat).expect("Failed to parse WAT"); + + // Create engine with operation limit disabled + let mut limits = VMLimits::default(); + limits.max_operations = 0; // Disable operation limit + let engine = Engine::with_limits(limits); + let module = engine.compile(&wasm).expect("Failed to compile module"); + + let mut storage = InMemoryStorage::default(); + let outcome = module + .run( + [0; 32].into(), + [0; 32].into(), + "simple_func", + &[], + &mut storage, + None, + None, + ) + .expect("Failed to run module"); + + // Should complete successfully without operation limit + assert!( + outcome.returns.is_ok(), + "Expected successful execution with operation limit disabled, got: {:?}", + outcome.returns + ); + } } diff --git a/crates/runtime/src/logic.rs b/crates/runtime/src/logic.rs index 1f6765c73..05858ce82 100644 --- a/crates/runtime/src/logic.rs +++ b/crates/runtime/src/logic.rs @@ -111,6 +111,10 @@ const DEFAULT_MAX_BLOB_CHUNK_SIZE_MIB: u64 = 10; const DEFAULT_MAX_METHOD_NAME_LENGTH: u64 = 256; /// Default maximum WASM module size in MiB (10 MiB). const DEFAULT_MAX_MODULE_SIZE_MIB: u64 = 10; +/// Default maximum operations for WASM execution (300 million). +/// This limits how many WASM operations can be executed before termination. +/// A value of 0 disables the execution limit. +const DEFAULT_MAX_OPERATIONS: u64 = 300_000_000; /// Defines the resource limits for a VM instance. /// @@ -167,6 +171,20 @@ pub struct VMLimits { /// The default of 10 MiB accommodates most applications while preventing memory /// exhaustion. Consider reducing for memory-constrained environments. pub max_module_size: u64, + /// The maximum number of WASM operations allowed per execution. + /// + /// Each WASM instruction counts as one operation (uniform cost). When the limit is reached, + /// execution is terminated with an `ExecutionTimeout` error. + /// This prevents infinite loops and long-running computations from blocking + /// the executor indefinitely. + /// + /// **Note:** The operation budget is shared between all WASM function calls in a single + /// execution, including registration hooks like `__calimero_register_merge`. If hooks + /// consume operations, the remaining budget for the user method is reduced accordingly. + /// + /// A value of 0 disables the execution limit. Use caution when disabling limits for + /// untrusted code, as infinite loops will block execution indefinitely. + pub max_operations: u64, } impl Default for VMLimits { @@ -202,6 +220,7 @@ impl Default for VMLimits { max_blob_chunk_size: DEFAULT_MAX_BLOB_CHUNK_SIZE_MIB * u64::from(ONE_MIB), max_method_name_length: DEFAULT_MAX_METHOD_NAME_LENGTH, max_module_size: DEFAULT_MAX_MODULE_SIZE_MIB * u64::from(ONE_MIB), + max_operations: DEFAULT_MAX_OPERATIONS, } } } @@ -811,6 +830,7 @@ mod tests { assert_eq!(limits.max_blob_handles, 100); assert_eq!(limits.max_blob_chunk_size, 10 << 20); // 10 MiB assert_eq!(limits.max_method_name_length, 256); + assert_eq!(limits.max_operations, 300_000_000); // 300 million operations } /// A smoke test for the successful path of the `finish` method. From bfe0ee613d00099be86675fd69e89b6226e685b4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 3 Feb 2026 18:46:48 +0000 Subject: [PATCH 2/5] fix: Address PR review comments for WASM operation limits - Add wasmer-middlewares to workspace dependencies for version consistency - Add documentation for Engine::with_limits explaining compile-time metering - Add security warning log when creating headless engines - Improve error handling by logging original error before returning ExecutionTimeout - Document that max_operations budget is shared with registration hooks - Expand max_operations field documentation with important usage notes Co-authored-by: sandi --- Cargo.toml | 1 + crates/runtime/Cargo.toml | 2 +- crates/runtime/src/lib.rs | 36 ++++++++++++++++++++++++++++++++---- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e9a74753a..2393067bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -212,6 +212,7 @@ velcro = "0.5.4" wasmer = "6.1.0" wasmer-types = "6.1.0" wasmer-compiler-cranelift = "6.1.0" +wasmer-middlewares = "6.1.0" wat = "1.243.0" web3 = "0.19.0" webbrowser = "1.0.4" diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 384ea4cc2..576f65f8c 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -26,7 +26,7 @@ tracing.workspace = true ureq.workspace = true wasmer.workspace = true wasmer-compiler-cranelift.workspace = true -wasmer-middlewares = "6.1.0" +wasmer-middlewares.workspace = true wasmer-types.workspace = true calimero-primitives.workspace = true diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 2fb787ce4..2439ea1b5 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -107,6 +107,11 @@ impl Engine { /// Create an engine with custom VMLimits. /// This is the preferred way to create an Engine when you need custom limits. + /// + /// **Note:** The operation limit is embedded at compile time via metering middleware. + /// Modules compiled with this engine will use the specified `max_operations` limit. + /// To use different limits for the same WASM code, create a new Engine with the + /// desired limits and recompile the module. #[must_use] pub fn with_limits(limits: VMLimits) -> Self { let engine = Self::create_engine_with_metering(&limits); @@ -145,16 +150,32 @@ impl Engine { /// Create a headless engine for running precompiled modules. /// - /// **Note:** Headless engines cannot enforce operation limits because the metering + /// # Security Warning + /// + /// **Headless engines cannot enforce operation limits** because the metering /// middleware must be applied at compile time. Modules loaded with a headless engine /// will run without operation limits regardless of `VMLimits.max_operations`. - /// For operation limit enforcement, use a full engine to compile modules. + /// + /// This means: + /// - Infinite loops or long-running computations will **not** be terminated + /// - Only use headless engines for trusted, pre-validated WASM modules + /// - For untrusted code, use a full engine with [`Engine::with_limits`] to compile modules + /// + /// If a module was compiled with metering enabled (max_operations > 0) and then loaded + /// via a headless engine, the metering will still be enforced by the embedded middleware. + /// However, errors from metering exhaustion will be reported as `ExecutionTimeout`. #[must_use] pub fn headless() -> Self { let mut limits = VMLimits::default(); // Disable operation limit for headless engines since metering requires compilation limits.max_operations = 0; + // Log security warning when using headless engines + tracing::warn!( + "Creating headless engine without operation limit enforcement. \ + Only use for trusted, pre-validated WASM modules." + ); + // Headless engines lack a compiler, so Wasmer skips perf.map generation. // For profiling, use a full engine to enable WASM symbol resolution. #[cfg(feature = "profiling")] @@ -467,10 +488,17 @@ impl Module { "WASM method execution failed" ); - // Check if execution exceeded operation limit + // Check if execution exceeded operation limit (only when metering was enabled) if max_ops > 0 { if let MeteringPoints::Exhausted = metering::get_remaining_points(store, &instance) { + // Log the original error for debugging before returning timeout + debug!( + %context_id, + method, + original_error = ?err, + "Metering exhausted, returning ExecutionTimeout (original error logged)" + ); error!(%context_id, method, "WASM execution exceeded operation limit"); return Ok(Some(FunctionCallError::WasmTrap( errors::WasmTrap::ExecutionTimeout, @@ -484,7 +512,7 @@ impl Module { }; } - // Log execution stats if metering is enabled + // Log execution stats if metering was enabled for this engine if max_ops > 0 { if let MeteringPoints::Remaining(remaining) = metering::get_remaining_points(store, &instance) From 2dcf714cd1938552530b485e5d131687b26db8be Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 18:24:54 +0000 Subject: [PATCH 3/5] docs: Improve profiling feature documentation in Cargo.toml Co-authored-by: sandi --- crates/runtime/Cargo.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/runtime/Cargo.toml b/crates/runtime/Cargo.toml index 576f65f8c..b6973cef0 100644 --- a/crates/runtime/Cargo.toml +++ b/crates/runtime/Cargo.toml @@ -47,6 +47,8 @@ wat.workspace = true [features] host-traces = ["owo-colors"] -# Enables PerfMap profiling support for WASM stack traces when ENABLE_WASMER_PROFILING=true. -# Note: Cranelift compiler is always included for operation limit metering. +# Enables PerfMap profiling support for WASM stack traces. +# When enabled, set ENABLE_WASMER_PROFILING=true at runtime to generate perf.map files. +# Note: This feature only controls the runtime env var check behavior - it does NOT +# gate the Cranelift compiler dependency, which is always included for operation limit metering. profiling = [] From 91f364692ba9aa6f5891f53fc5d046d085f4a50a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 18:45:22 +0000 Subject: [PATCH 4/5] fix: Address PR review comments for headless engine docs and logging Co-authored-by: sandi --- crates/runtime/src/lib.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs index 2439ea1b5..60c5dad2e 100644 --- a/crates/runtime/src/lib.rs +++ b/crates/runtime/src/lib.rs @@ -161,17 +161,19 @@ impl Engine { /// - Only use headless engines for trusted, pre-validated WASM modules /// - For untrusted code, use a full engine with [`Engine::with_limits`] to compile modules /// - /// If a module was compiled with metering enabled (max_operations > 0) and then loaded - /// via a headless engine, the metering will still be enforced by the embedded middleware. - /// However, errors from metering exhaustion will be reported as `ExecutionTimeout`. + /// **Note on precompiled modules with metering:** If a module was originally compiled + /// with metering enabled (max_operations > 0) and later loaded via a headless engine, + /// the metering middleware embedded in the compiled code will still trap when exhausted. + /// However, these traps will surface as generic runtime errors rather than + /// `ExecutionTimeout`, since headless engines cannot detect metering state. #[must_use] pub fn headless() -> Self { let mut limits = VMLimits::default(); // Disable operation limit for headless engines since metering requires compilation limits.max_operations = 0; - // Log security warning when using headless engines - tracing::warn!( + // Log at debug level - security implications are documented above + debug!( "Creating headless engine without operation limit enforcement. \ Only use for trusted, pre-validated WASM modules." ); @@ -492,12 +494,13 @@ impl Module { if max_ops > 0 { if let MeteringPoints::Exhausted = metering::get_remaining_points(store, &instance) { - // Log the original error for debugging before returning timeout - debug!( + // Log the original error at info level for production debugging + // The error type from metering exhaustion is typically an unreachable trap + info!( %context_id, method, original_error = ?err, - "Metering exhausted, returning ExecutionTimeout (original error logged)" + "Metering exhausted, returning ExecutionTimeout (original trap preserved in logs)" ); error!(%context_id, method, "WASM execution exceeded operation limit"); return Ok(Some(FunctionCallError::WasmTrap( From 4f4bc09259cc6025df023d9409923cca13458a75 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 4 Feb 2026 18:49:24 +0000 Subject: [PATCH 5/5] docs: Add practical guidance for max_operations configuration Co-authored-by: sandi --- crates/runtime/src/logic.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/runtime/src/logic.rs b/crates/runtime/src/logic.rs index 05858ce82..56c7d3142 100644 --- a/crates/runtime/src/logic.rs +++ b/crates/runtime/src/logic.rs @@ -112,8 +112,14 @@ const DEFAULT_MAX_METHOD_NAME_LENGTH: u64 = 256; /// Default maximum WASM module size in MiB (10 MiB). const DEFAULT_MAX_MODULE_SIZE_MIB: u64 = 10; /// Default maximum operations for WASM execution (300 million). +/// /// This limits how many WASM operations can be executed before termination. /// A value of 0 disables the execution limit. +/// +/// **Practical guidance:** On typical hardware, 300M operations corresponds to +/// roughly 10-60 seconds of wall-clock time, depending on the operation mix. +/// Simple arithmetic loops execute faster than memory-intensive operations. +/// Start with the default and adjust based on your workload's profiling data. const DEFAULT_MAX_OPERATIONS: u64 = 300_000_000; /// Defines the resource limits for a VM instance. @@ -173,8 +179,13 @@ pub struct VMLimits { pub max_module_size: u64, /// The maximum number of WASM operations allowed per execution. /// - /// Each WASM instruction counts as one operation (uniform cost). When the limit is reached, - /// execution is terminated with an `ExecutionTimeout` error. + /// Each WASM instruction counts as one operation using a **uniform cost function**. + /// This means all operations (arithmetic, memory access, calls, branches) have equal + /// cost. While this doesn't perfectly model wall-clock time, it provides predictable + /// and deterministic execution limits. For production use, consider adding a safety + /// margin to account for operation mix variability. + /// + /// When the limit is reached, execution is terminated with an `ExecutionTimeout` error. /// This prevents infinite loops and long-running computations from blocking /// the executor indefinitely. /// @@ -182,6 +193,10 @@ pub struct VMLimits { /// execution, including registration hooks like `__calimero_register_merge`. If hooks /// consume operations, the remaining budget for the user method is reduced accordingly. /// + /// **Practical guidance:** The default of 300M operations typically corresponds to + /// 10-60 seconds of execution time depending on workload. Monitor actual execution + /// stats (logged at debug level) to tune this value for your specific use case. + /// /// A value of 0 disables the execution limit. Use caution when disabling limits for /// untrusted code, as infinite loops will block execution indefinitely. pub max_operations: u64,