Skip to main content

calimero_runtime/
lib.rs

1use std::panic::{catch_unwind, AssertUnwindSafe};
2
3use calimero_node_primitives::client::NodeClient;
4use calimero_primitives::context::ContextId;
5use calimero_primitives::identity::PublicKey;
6use tracing::{debug, error, info};
7use wasmer::{DeserializeError, Instance, SerializeError, Store};
8
9// Profiling feature: Only compile these imports when profiling feature is enabled
10#[cfg(feature = "profiling")]
11use wasmer::sys::{CompilerConfig, Cranelift};
12
13mod constants;
14mod constraint;
15pub mod errors;
16pub mod logic;
17mod memory;
18mod panic_payload;
19pub mod store;
20
21pub use constraint::Constraint;
22use errors::{FunctionCallError, HostError, Location, PanicContext, VMRuntimeError};
23use logic::{ContextHost, Outcome, VMContext, VMLimits, VMLogic, VMLogicError};
24use memory::WasmerTunables;
25use store::Storage;
26
27pub type RuntimeResult<T, E = VMRuntimeError> = Result<T, E>;
28
29/// Validates a method name for WASM execution.
30///
31/// Valid method names must:
32/// - Not be empty
33/// - Not exceed the maximum length limit
34/// - Contain only valid characters (ASCII alphanumeric, underscore)
35///
36/// # Arguments
37///
38/// * `method` - The method name to validate
39/// * `max_length` - The maximum allowed length for the method name
40///
41/// # Returns
42///
43/// * `Ok(())` if the method name is valid
44/// * `Err(FunctionCallError)` if the method name is invalid
45fn validate_method_name(method: &str, max_length: u64) -> Result<(), FunctionCallError> {
46    // Check for empty method name
47    if method.is_empty() {
48        return Err(FunctionCallError::MethodResolutionError(
49            errors::MethodResolutionError::EmptyMethodName,
50        ));
51    }
52
53    // Check length limit
54    let method_len = method.len();
55    if method_len as u64 > max_length {
56        return Err(FunctionCallError::MethodResolutionError(
57            errors::MethodResolutionError::MethodNameTooLong {
58                name: method.to_owned(),
59                length: method_len,
60                max: max_length,
61            },
62        ));
63    }
64
65    // Validate characters: only allow ASCII alphanumeric and underscore
66    // This covers typical WASM export names and Rust/JS function naming conventions
67    for (position, character) in method.chars().enumerate() {
68        if !character.is_ascii_alphanumeric() && character != '_' {
69            return Err(FunctionCallError::MethodResolutionError(
70                errors::MethodResolutionError::InvalidMethodNameCharacter {
71                    name: method.to_owned(),
72                    character,
73                    position,
74                },
75            ));
76        }
77    }
78
79    Ok(())
80}
81
82#[derive(Clone, Debug)]
83pub struct Engine {
84    limits: VMLimits,
85    engine: wasmer::Engine,
86}
87
88impl Default for Engine {
89    fn default() -> Self {
90        let limits = VMLimits::default();
91
92        let engine = Self::create_engine();
93
94        Self::new(engine, limits)
95    }
96}
97
98impl Engine {
99    #[must_use]
100    pub fn new(mut engine: wasmer::Engine, limits: VMLimits) -> Self {
101        // Set tunables if this is a sys engine (native engine)
102        if engine.is_sys() {
103            use wasmer::sys::NativeEngineExt;
104            engine.set_tunables(WasmerTunables::new(&limits));
105        }
106
107        Self { limits, engine }
108    }
109
110    /// Create an engine, using Cranelift compiler for profiling builds with PerfMap support
111    fn create_engine() -> wasmer::Engine {
112        #[cfg(feature = "profiling")]
113        {
114            if std::env::var("ENABLE_WASMER_PROFILING")
115                .map(|v| v == "true")
116                .unwrap_or(false)
117            {
118                info!("Enabling Wasmer PerfMap profiling for WASM stack traces");
119                // Create Cranelift config and enable PerfMap file generation
120                let mut config = Cranelift::default();
121                config.enable_perfmap();
122                return wasmer::Engine::from(config);
123            }
124        }
125
126        // Default engine (no profiling)
127        wasmer::Engine::default()
128    }
129
130    #[must_use]
131    pub fn headless() -> Self {
132        let limits = VMLimits::default();
133
134        // Headless engines lack a compiler, so Wasmer skips perf.map generation.
135        // For profiling, use a full engine to enable WASM symbol resolution.
136        #[cfg(feature = "profiling")]
137        {
138            if std::env::var("ENABLE_WASMER_PROFILING")
139                .map(|v| v == "true")
140                .unwrap_or(false)
141            {
142                debug!("Using profiling-enabled engine for precompiled module (required for perf.map generation)");
143                let engine = Self::create_engine();
144                return Self::new(engine, limits);
145            }
146        }
147
148        use wasmer::sys::NativeEngineExt;
149        let engine = wasmer::Engine::headless();
150
151        Self::new(engine, limits)
152    }
153
154    pub fn compile(&self, bytes: &[u8]) -> Result<Module, FunctionCallError> {
155        // Check module size before compilation to prevent memory exhaustion attacks
156        // Note: `as u64` is safe because usize <= u64 on all supported platforms (32-bit and 64-bit)
157        let size = bytes.len() as u64;
158        if size > self.limits.max_module_size {
159            tracing::warn!(
160                size,
161                max = self.limits.max_module_size,
162                "WASM module size limit exceeded"
163            );
164            return Err(FunctionCallError::ModuleSizeLimitExceeded {
165                size,
166                max: self.limits.max_module_size,
167            });
168        }
169
170        // todo! apply a prepare step
171        // todo! - parse the wasm blob, validate and apply transformations
172        // todo!   - validations:
173        // todo!     - there is no memory import
174        // todo!     - there is no _start function
175        // todo!   - transformations:
176        // todo!     - remove memory export
177        // todo!     - remove memory section
178        // todo! cache the compiled module in storage for later
179
180        let module = wasmer::Module::new(&self.engine, bytes)?;
181
182        Ok(Module {
183            limits: self.limits,
184            engine: self.engine.clone(),
185            module,
186        })
187    }
188
189    /// # Safety
190    ///
191    /// This function deserializes a precompiled WASM module. The caller must ensure
192    /// the bytes come from a trusted source (e.g., previously compiled by this engine).
193    ///
194    /// # Security Note
195    ///
196    /// No size limit check is performed here. This is an accepted security trade-off because:
197    /// 1. Precompiled modules have already been validated during their original compilation
198    /// 2. The serialized format may differ significantly in size from the original WASM binary
199    /// 3. The `unsafe` marker already requires callers to ensure the bytes are from a trusted source
200    ///
201    /// **Audit requirement**: All call sites using this method should be reviewed to ensure
202    /// precompiled bytes originate from trusted sources only (e.g., the node's own compilation cache).
203    ///
204    /// If precompiled bytes could come from an untrusted source, callers should implement
205    /// their own size validation before calling this method.
206    pub unsafe fn from_precompiled(&self, bytes: &[u8]) -> Result<Module, DeserializeError> {
207        let module = wasmer::Module::deserialize(&self.engine, bytes)?;
208
209        Ok(Module {
210            limits: self.limits,
211            engine: self.engine.clone(),
212            module,
213        })
214    }
215}
216
217#[derive(Debug)]
218pub struct Module {
219    limits: VMLimits,
220    engine: wasmer::Engine,
221    module: wasmer::Module,
222}
223
224impl Module {
225    pub fn to_bytes(&self) -> Result<Box<[u8]>, SerializeError> {
226        let bytes = self.module.serialize()?;
227
228        Ok(Vec::into_boxed_slice(bytes.into()))
229    }
230
231    pub fn run<'a>(
232        &'a self,
233        context: ContextId,
234        executor: PublicKey,
235        method: &str,
236        input: &'a [u8],
237        storage: &'a mut dyn Storage,
238        private_storage: Option<&'a mut dyn Storage>,
239        node_client: Option<NodeClient>,
240        context_host: Option<Box<dyn ContextHost>>,
241    ) -> RuntimeResult<Outcome> {
242        let context_id = context;
243        info!(%context_id, method, "Running WASM method");
244        debug!(%context_id, method, input_len = input.len(), "WASM execution input");
245
246        let context = VMContext::new(input.into(), *context_id, *executor);
247
248        let mut logic = VMLogic::new(
249            storage,
250            private_storage,
251            context,
252            &self.limits,
253            node_client,
254            context_host,
255        );
256
257        let mut store = Store::new(self.engine.clone());
258
259        let imports = logic.imports(&mut store);
260
261        // Wrap WASM execution in catch_unwind to prevent panics from crashing the node.
262        // This catches any unhandled panics during instance creation, memory access,
263        // or function execution and converts them to proper error responses.
264        let execution_result = catch_unwind(AssertUnwindSafe(|| {
265            Self::execute_wasm(
266                &mut store,
267                &self.module,
268                &imports,
269                &mut logic,
270                method,
271                &context_id,
272                self.limits.max_method_name_length,
273            )
274        }));
275
276        // Determine the error to pass to finish() based on execution result
277        let err = match execution_result {
278            Ok(Ok(err)) => err,
279            Ok(Err(e)) => return Err(e),
280            Err(panic_payload) => {
281                // Extract panic message from the payload
282                let message = panic_payload::panic_payload_to_string(
283                    panic_payload.as_ref(),
284                    "<unknown panic>",
285                );
286                error!(
287                    %context_id,
288                    method,
289                    panic_message = %message,
290                    "WASM execution panicked"
291                );
292                Some(FunctionCallError::HostError(HostError::Panic {
293                    context: PanicContext::Guest,
294                    message,
295                    location: Location::Unknown,
296                }))
297            }
298        };
299
300        let outcome = logic.finish(err);
301        if outcome.returns.is_ok() {
302            info!(%context_id, method, "WASM method execution completed");
303            debug!(
304                %context_id,
305                method,
306                has_return = outcome.returns.as_ref().is_ok_and(Option::is_some),
307                logs_count = outcome.logs.len(),
308                events_count = outcome.events.len(),
309                "WASM execution outcome"
310            );
311            // Print WASM logs for debugging
312            for (i, log) in outcome.logs.iter().enumerate() {
313                info!(%context_id, method, log_index = i, log_content = %log, "WASM_LOG");
314            }
315        }
316
317        Ok(outcome)
318    }
319
320    /// Execute the WASM function within a catch_unwind boundary.
321    /// This method is separated to allow catch_unwind to capture any panics.
322    /// Returns `Ok(Some(error))` if execution failed with an error,
323    /// `Ok(None)` if execution succeeded, or `Err` for critical runtime errors.
324    fn execute_wasm(
325        store: &mut Store,
326        module: &wasmer::Module,
327        imports: &wasmer::Imports,
328        logic: &mut VMLogic<'_>,
329        method: &str,
330        context_id: &ContextId,
331        max_method_name_length: u64,
332    ) -> RuntimeResult<Option<FunctionCallError>> {
333        // Validate method name before attempting to look it up
334        if let Err(err) = validate_method_name(method, max_method_name_length) {
335            error!(%context_id, method, error=?err, "Invalid method name");
336            return Ok(Some(err));
337        }
338        let instance = match Instance::new(store, module, imports) {
339            Ok(instance) => instance,
340            Err(err) => {
341                error!(%context_id, method, error=?err, "Failed to instantiate WASM module");
342                return Ok(Some(err.into()));
343            }
344        };
345
346        // Get memory from the WASM instance and attach it to VMLogic.
347        // Note: memory.clone() is cheap - it just increments an Arc reference count,
348        // not copying actual memory contents. VMLogic::finish() handles cleanup.
349        let memory = match instance.exports.get_memory("memory") {
350            Ok(memory) => memory.clone(),
351            // todo! test memory returns MethodNotFound
352            Err(err) => {
353                error!(%context_id, method, error=?err, "Failed to get WASM memory");
354                return Ok(Some(err.into()));
355            }
356        };
357
358        // Attach memory to VMLogic, which will clean it up in finish()
359        let _ = logic.with_memory(memory);
360
361        // Call the auto-generated registration hook if it exists.
362        // This enables automatic CRDT merge during sync.
363        // Note: This is optional and failures are non-fatal (especially for JS apps).
364        if let Ok(register_fn) = instance
365            .exports
366            .get_typed_function::<(), ()>(store, "__calimero_register_merge")
367        {
368            match register_fn.call(store) {
369                Ok(()) => {
370                    debug!(%context_id, "Successfully registered CRDT merge function");
371                }
372                Err(err) => {
373                    // Log but don't fail - registration is optional (backward compat)
374                    // JS apps may not have this function properly initialized yet.
375                    debug!(
376                        %context_id,
377                        error=?err,
378                        "Failed to call merge registration hook (non-fatal, continuing)"
379                    );
380                }
381            }
382        }
383
384        let function = match instance.exports.get_function(method) {
385            Ok(function) => function,
386            Err(err) => {
387                error!(%context_id, method, error=?err, "Method not found in WASM module");
388                return Ok(Some(err.into()));
389            }
390        };
391
392        let signature = function.ty(store);
393
394        if !(signature.params().is_empty() && signature.results().is_empty()) {
395            error!(%context_id, method, "Invalid method signature");
396            return Ok(Some(FunctionCallError::MethodResolutionError(
397                errors::MethodResolutionError::InvalidSignature {
398                    name: method.to_owned(),
399                },
400            )));
401        }
402
403        if let Err(err) = function.call(store, &[]) {
404            let traces = err
405                .trace()
406                .iter()
407                .map(|frame| {
408                    let module = frame.module_name();
409                    let func = frame.function_name().unwrap_or("<unknown-func>");
410                    let offset = frame.func_offset();
411                    let offset = if offset == 0 {
412                        String::new()
413                    } else {
414                        format!("@0x{offset:x}")
415                    };
416                    format!("{module}::{func}{offset}")
417                })
418                .collect::<Vec<_>>();
419            let trace_joined = if traces.is_empty() {
420                None
421            } else {
422                Some(traces.join(" -> "))
423            };
424
425            let message = err.message();
426            let message_str = if message.is_empty() {
427                "<no error message>"
428            } else {
429                message.as_str()
430            };
431
432            error!(
433                %context_id,
434                method,
435                error_debug = ?err,
436                error_message = %message_str,
437                wasm_trace = trace_joined.as_deref(),
438                "WASM method execution failed"
439            );
440
441            return match err.downcast::<VMLogicError>() {
442                Ok(err) => Ok(Some(err.try_into()?)),
443                Err(err) => Ok(Some(err.into())),
444            };
445        }
446
447        Ok(None)
448    }
449}
450
451#[cfg(test)]
452mod integration_tests_package_usage {
453    use {eyre as _, owo_colors as _, rand as _, wat as _};
454}
455
456/// Integration tests for WASM execution (panic handling, size limits, compilation)
457#[cfg(test)]
458mod wasm_integration_tests {
459    use super::*;
460    use crate::store::InMemoryStorage;
461
462    /// Test that a simple WASM module runs successfully (baseline test)
463    #[test]
464    fn test_wasm_execution_success() {
465        // A minimal WASM module with a function that does nothing
466        let wat = r#"
467            (module
468                (memory (export "memory") 1)
469                (func (export "test_func"))
470            )
471        "#;
472        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
473
474        let engine = Engine::default();
475        let module = engine.compile(&wasm).expect("Failed to compile module");
476
477        let mut storage = InMemoryStorage::default();
478        let outcome = module
479            .run(
480                [0; 32].into(),
481                [0; 32].into(),
482                "test_func",
483                &[],
484                &mut storage,
485                None, // No private storage for tests
486                None,
487                None,
488            )
489            .expect("Failed to run module");
490
491        assert!(
492            outcome.returns.is_ok(),
493            "Expected successful execution, got: {:?}",
494            outcome.returns
495        );
496    }
497
498    /// Test that calling a non-existent method returns MethodNotFound error
499    #[test]
500    fn test_wasm_method_not_found() {
501        let wat = r#"
502            (module
503                (memory (export "memory") 1)
504                (func (export "existing_func"))
505            )
506        "#;
507        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
508
509        let engine = Engine::default();
510        let module = engine.compile(&wasm).expect("Failed to compile module");
511
512        let mut storage = InMemoryStorage::default();
513        let outcome = module
514            .run(
515                [0; 32].into(),
516                [0; 32].into(),
517                "non_existent_func",
518                &[],
519                &mut storage,
520                None, // No private storage for tests
521                None,
522                None,
523            )
524            .expect("Failed to run module");
525
526        match &outcome.returns {
527            Err(FunctionCallError::MethodResolutionError(
528                errors::MethodResolutionError::MethodNotFound { name },
529            )) => {
530                assert_eq!(name, "non_existent_func");
531            }
532            other => panic!("Expected MethodNotFound error, got: {other:?}"),
533        }
534    }
535
536    /// Test that empty method name returns EmptyMethodName error
537    #[test]
538    fn test_wasm_empty_method_name() {
539        let wat = r#"
540            (module
541                (memory (export "memory") 1)
542                (func (export "test_func"))
543            )
544        "#;
545        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
546
547        let engine = Engine::default();
548        let module = engine.compile(&wasm).expect("Failed to compile module");
549
550        let mut storage = InMemoryStorage::default();
551        let outcome = module
552            .run(
553                [0; 32].into(),
554                [0; 32].into(),
555                "",
556                &[],
557                &mut storage,
558                None, // No private storage for tests
559                None,
560                None,
561            )
562            .expect("Failed to run module");
563
564        match &outcome.returns {
565            Err(FunctionCallError::MethodResolutionError(
566                errors::MethodResolutionError::EmptyMethodName,
567            )) => {
568                // Expected - empty method name was rejected
569            }
570            other => panic!("Expected EmptyMethodName error, got: {other:?}"),
571        }
572    }
573
574    /// Test that method name exceeding max length returns MethodNameTooLong error
575    #[test]
576    fn test_wasm_method_name_too_long() {
577        let wat = r#"
578            (module
579                (memory (export "memory") 1)
580                (func (export "test_func"))
581            )
582        "#;
583        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
584
585        let engine = Engine::default();
586        let module = engine.compile(&wasm).expect("Failed to compile module");
587
588        // Create a method name that exceeds the default max length (256 bytes)
589        let long_method_name = "a".repeat(300);
590
591        let mut storage = InMemoryStorage::default();
592        let outcome = module
593            .run(
594                [0; 32].into(),
595                [0; 32].into(),
596                &long_method_name,
597                &[],
598                &mut storage,
599                None, // No private storage for tests
600                None,
601                None,
602            )
603            .expect("Failed to run module");
604
605        match &outcome.returns {
606            Err(FunctionCallError::MethodResolutionError(
607                errors::MethodResolutionError::MethodNameTooLong { length, max, .. },
608            )) => {
609                assert_eq!(*length, 300);
610                assert_eq!(*max, 256);
611            }
612            other => panic!("Expected MethodNameTooLong error, got: {other:?}"),
613        }
614    }
615
616    /// Test that method name with invalid characters returns InvalidMethodNameCharacter error
617    #[test]
618    fn test_wasm_method_name_invalid_characters() {
619        let wat = r#"
620            (module
621                (memory (export "memory") 1)
622                (func (export "test_func"))
623            )
624        "#;
625        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
626
627        let engine = Engine::default();
628        let module = engine.compile(&wasm).expect("Failed to compile module");
629
630        // Test various invalid characters
631        let invalid_names = [
632            ("test func", ' ', 4),   // space
633            ("test\nfunc", '\n', 4), // newline
634            ("test-func", '-', 4),   // hyphen
635            ("test.func", '.', 4),   // dot
636            ("test/func", '/', 4),   // slash
637        ];
638
639        for (method_name, expected_char, expected_pos) in invalid_names {
640            let mut storage = InMemoryStorage::default();
641            let outcome = module
642                .run(
643                    [0; 32].into(),
644                    [0; 32].into(),
645                    method_name,
646                    &[],
647                    &mut storage,
648                    None, // No private storage for tests
649                    None,
650                    None,
651                )
652                .expect("Failed to run module");
653
654            match &outcome.returns {
655                Err(FunctionCallError::MethodResolutionError(
656                    errors::MethodResolutionError::InvalidMethodNameCharacter {
657                        character,
658                        position,
659                        ..
660                    },
661                )) => {
662                    assert_eq!(
663                        *character, expected_char,
664                        "Wrong invalid character for method name: {method_name}"
665                    );
666                    assert_eq!(
667                        *position, expected_pos,
668                        "Wrong position for method name: {method_name}"
669                    );
670                }
671                other => panic!(
672                    "Expected InvalidMethodNameCharacter error for '{method_name}', got: {other:?}"
673                ),
674            }
675        }
676    }
677
678    /// Test that valid method names with various allowed characters work
679    #[test]
680    fn test_wasm_method_name_valid_characters() {
681        // Note: This test verifies that validation passes for valid names.
682        // The actual method lookup may fail with MethodNotFound since these
683        // methods don't exist in the module, but the validation should pass.
684        let wat = r#"
685            (module
686                (memory (export "memory") 1)
687                (func (export "test_func"))
688            )
689        "#;
690        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
691
692        let engine = Engine::default();
693        let module = engine.compile(&wasm).expect("Failed to compile module");
694
695        // Test valid method names (these should pass validation but may not exist in module)
696        let valid_names = [
697            "simple",
698            "with_underscore",
699            "__double_underscore",
700            "_leading_underscore",
701            "trailing_underscore_",
702            "CamelCase",
703            "mixedCase123",
704            "numbers123",
705            "ALLCAPS",
706            "a", // single character
707        ];
708
709        for method_name in valid_names {
710            let mut storage = InMemoryStorage::default();
711            let outcome = module
712                .run(
713                    [0; 32].into(),
714                    [0; 32].into(),
715                    method_name,
716                    &[],
717                    &mut storage,
718                    None, // No private storage for tests
719                    None,
720                    None,
721                )
722                .expect("Failed to run module");
723
724            // Should get MethodNotFound (not a validation error) since the method doesn't exist
725            match &outcome.returns {
726                Err(FunctionCallError::MethodResolutionError(
727                    errors::MethodResolutionError::MethodNotFound { name },
728                )) => {
729                    assert_eq!(
730                        name, method_name,
731                        "Method name should pass validation: {method_name}"
732                    );
733                }
734                // test_func should succeed since it exists
735                Ok(_) if method_name == "test_func" => {}
736                other => panic!(
737                    "Expected MethodNotFound error for valid name '{method_name}', got: {other:?}"
738                ),
739            }
740        }
741    }
742
743    /// Test that test_func (valid name) still works correctly
744    #[test]
745    fn test_wasm_valid_method_name_execution() {
746        let wat = r#"
747            (module
748                (memory (export "memory") 1)
749                (func (export "valid_method_name"))
750            )
751        "#;
752        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
753
754        let engine = Engine::default();
755        let module = engine.compile(&wasm).expect("Failed to compile module");
756
757        let mut storage = InMemoryStorage::default();
758        let outcome = module
759            .run(
760                [0; 32].into(),
761                [0; 32].into(),
762                "valid_method_name",
763                &[],
764                &mut storage,
765                None, // No private storage for tests
766                None,
767                None,
768            )
769            .expect("Failed to run module");
770
771        assert!(
772            outcome.returns.is_ok(),
773            "Expected successful execution with valid method name, got: {:?}",
774            outcome.returns
775        );
776    }
777
778    /// Test that unreachable instruction causes a WasmTrap error (not a crash)
779    #[test]
780    fn test_wasm_unreachable_trap() {
781        // A WASM module with a function that executes unreachable instruction
782        let wat = r#"
783            (module
784                (memory (export "memory") 1)
785                (func (export "trap_func")
786                    unreachable
787                )
788            )
789        "#;
790        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
791
792        let engine = Engine::default();
793        let module = engine.compile(&wasm).expect("Failed to compile module");
794
795        let mut storage = InMemoryStorage::default();
796        let outcome = module
797            .run(
798                [0; 32].into(),
799                [0; 32].into(),
800                "trap_func",
801                &[],
802                &mut storage,
803                None, // No private storage for tests
804                None,
805                None,
806            )
807            .expect("Failed to run module");
808
809        // The unreachable instruction should cause a WasmTrap::Unreachable error
810        match &outcome.returns {
811            Err(FunctionCallError::WasmTrap(errors::WasmTrap::Unreachable)) => {
812                // Expected - the trap was properly caught and converted to an error
813            }
814            other => panic!("Expected WasmTrap::Unreachable error, got: {other:?}"),
815        }
816    }
817
818    /// Test that a WASM module calling the panic host function returns a Panic error
819    #[test]
820    fn test_wasm_panic_host_function() {
821        // A WASM module that calls the panic host function
822        // The panic function expects a pointer to a location struct
823        let wat = r#"
824            (module
825                (import "env" "panic" (func $panic (param i64)))
826                (memory (export "memory") 1)
827                (data (i32.const 0) "test.rs")
828                (func (export "panic_func")
829                    ;; Store the location struct at offset 100:
830                    ;; - ptr to filename (8 bytes): points to 0
831                    ;; - len of filename (8 bytes): 7
832                    ;; - line (4 bytes): 42
833                    ;; - column (4 bytes): 10
834
835                    ;; ptr to filename = 0
836                    (i64.store (i32.const 100) (i64.const 0))
837                    ;; len of filename = 7
838                    (i64.store (i32.const 108) (i64.const 7))
839                    ;; line = 42
840                    (i32.store (i32.const 116) (i32.const 42))
841                    ;; column = 10
842                    (i32.store (i32.const 120) (i32.const 10))
843
844                    ;; Call panic with pointer to location struct
845                    (call $panic (i64.const 100))
846                )
847            )
848        "#;
849        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
850
851        let engine = Engine::default();
852        let module = engine.compile(&wasm).expect("Failed to compile module");
853
854        let mut storage = InMemoryStorage::default();
855        let outcome = module
856            .run(
857                [0; 32].into(),
858                [0; 32].into(),
859                "panic_func",
860                &[],
861                &mut storage,
862                None, // No private storage for tests
863                None,
864                None,
865            )
866            .expect("Failed to run module");
867
868        // The panic should be caught and converted to a HostError::Panic
869        match &outcome.returns {
870            Err(FunctionCallError::HostError(HostError::Panic {
871                context, location, ..
872            })) => {
873                assert!(
874                    matches!(context, PanicContext::Guest),
875                    "Expected Guest panic context"
876                );
877                // Verify location information was captured
878                match location {
879                    Location::At { file, line, column } => {
880                        assert_eq!(file, "test.rs");
881                        assert_eq!(*line, 42);
882                        assert_eq!(*column, 10);
883                    }
884                    Location::Unknown => panic!("Expected location to be known"),
885                }
886            }
887            other => panic!("Expected HostError::Panic, got: {other:?}"),
888        }
889    }
890
891    /// Test that memory out of bounds causes a WasmTrap error (not a crash)
892    #[test]
893    fn test_wasm_memory_out_of_bounds() {
894        // A WASM module that tries to access memory out of bounds
895        let wat = r#"
896            (module
897                (memory (export "memory") 1)
898                (func (export "oob_func")
899                    ;; Try to load from way beyond the memory limit (1 page = 65536 bytes)
900                    (drop (i32.load (i32.const 1000000)))
901                )
902            )
903        "#;
904        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
905
906        let engine = Engine::default();
907        let module = engine.compile(&wasm).expect("Failed to compile module");
908
909        let mut storage = InMemoryStorage::default();
910        let outcome = module
911            .run(
912                [0; 32].into(),
913                [0; 32].into(),
914                "oob_func",
915                &[],
916                &mut storage,
917                None, // No private storage for tests
918                None,
919                None,
920            )
921            .expect("Failed to run module");
922
923        // Memory out of bounds should cause a WasmTrap error
924        match &outcome.returns {
925            Err(FunctionCallError::WasmTrap(errors::WasmTrap::MemoryOutOfBounds)) => {
926                // Expected - the trap was properly caught and converted to an error
927            }
928            other => panic!("Expected WasmTrap::MemoryOutOfBounds error, got: {other:?}"),
929        }
930    }
931
932    /// Test that stack overflow causes a WasmTrap error (not a crash)
933    #[test]
934    fn test_wasm_stack_overflow() {
935        // A WASM module with infinite recursion to cause stack overflow
936        let wat = r#"
937            (module
938                (memory (export "memory") 1)
939                (func $recurse
940                    (call $recurse)
941                )
942                (func (export "overflow_func")
943                    (call $recurse)
944                )
945            )
946        "#;
947        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
948
949        let engine = Engine::default();
950        let module = engine.compile(&wasm).expect("Failed to compile module");
951
952        let mut storage = InMemoryStorage::default();
953        let outcome = module
954            .run(
955                [0; 32].into(),
956                [0; 32].into(),
957                "overflow_func",
958                &[],
959                &mut storage,
960                None, // No private storage for tests
961                None,
962                None,
963            )
964            .expect("Failed to run module");
965
966        // Stack overflow should cause a WasmTrap error
967        match &outcome.returns {
968            Err(FunctionCallError::WasmTrap(errors::WasmTrap::StackOverflow)) => {
969                // Expected - the trap was properly caught and converted to an error
970            }
971            other => panic!("Expected WasmTrap::StackOverflow error, got: {other:?}"),
972        }
973    }
974
975    /// Test that module size limit is enforced during compilation
976    #[test]
977    fn test_wasm_module_size_limit() {
978        use crate::logic::VMLimits;
979
980        // A minimal WASM module
981        let wat = r#"
982            (module
983                (memory (export "memory") 1)
984                (func (export "test_func"))
985            )
986        "#;
987        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
988
989        // Create an engine with a very small module size limit (smaller than our module)
990        let mut limits = VMLimits::default();
991        limits.max_module_size = 10; // 10 bytes - way too small for any valid module
992
993        let engine = Engine::new(wasmer::Engine::default(), limits);
994
995        // Attempt to compile should fail due to size limit
996        let result = engine.compile(&wasm);
997
998        match result {
999            Err(FunctionCallError::ModuleSizeLimitExceeded { size, max }) => {
1000                assert_eq!(max, 10);
1001                assert!(size > 10, "Module size should be greater than the limit");
1002            }
1003            Ok(_) => panic!("Expected ModuleSizeLimitExceeded error, but compilation succeeded"),
1004            Err(other) => panic!("Expected ModuleSizeLimitExceeded error, got: {other:?}"),
1005        }
1006    }
1007
1008    /// Test that modules within size limit compile successfully
1009    #[test]
1010    fn test_wasm_module_within_size_limit() {
1011        use crate::logic::VMLimits;
1012
1013        // A minimal WASM module
1014        let wat = r#"
1015            (module
1016                (memory (export "memory") 1)
1017                (func (export "test_func"))
1018            )
1019        "#;
1020        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1021
1022        // Create an engine with a large enough module size limit
1023        let mut limits = VMLimits::default();
1024        limits.max_module_size = 1024 * 1024; // 1 MiB - plenty of room
1025
1026        let engine = Engine::new(wasmer::Engine::default(), limits);
1027
1028        // Compilation should succeed
1029        let result = engine.compile(&wasm);
1030        assert!(
1031            result.is_ok(),
1032            "Expected successful compilation, got: {result:?}"
1033        );
1034    }
1035
1036    /// Test that modules exactly at the size limit compile successfully (boundary condition)
1037    #[test]
1038    fn test_wasm_module_at_exact_size_limit() {
1039        use crate::logic::VMLimits;
1040
1041        // A minimal WASM module
1042        let wat = r#"
1043            (module
1044                (memory (export "memory") 1)
1045                (func (export "test_func"))
1046            )
1047        "#;
1048        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1049
1050        // Create an engine with module size limit exactly equal to the WASM size
1051        let mut limits = VMLimits::default();
1052        limits.max_module_size = wasm.len() as u64; // Exact size limit
1053
1054        let engine = Engine::new(wasmer::Engine::default(), limits);
1055
1056        // Compilation should succeed because check is `size > limit`, not `size >= limit`
1057        let result = engine.compile(&wasm);
1058        assert!(
1059            result.is_ok(),
1060            "Expected successful compilation at exact size limit, got: {result:?}"
1061        );
1062    }
1063
1064    /// Test that compilation errors are properly wrapped after passing size check
1065    #[test]
1066    fn test_wasm_compilation_error_propagation() {
1067        // Invalid WASM bytes that pass size check but fail compilation
1068        // This is not valid WASM but is large enough to pass typical size limits
1069        let invalid_wasm = b"not valid wasm bytes at all - this should fail compilation";
1070
1071        let engine = Engine::default();
1072
1073        // Attempt to compile should fail with CompilationError (not size limit)
1074        let result = engine.compile(invalid_wasm);
1075
1076        match result {
1077            Err(FunctionCallError::CompilationError { .. }) => {
1078                // Expected - wasmer compilation error is properly wrapped
1079            }
1080            Err(FunctionCallError::ModuleSizeLimitExceeded { .. }) => {
1081                panic!("Should not hit size limit for small invalid module")
1082            }
1083            Ok(_) => panic!("Expected compilation error for invalid WASM"),
1084            Err(other) => panic!("Expected CompilationError, got: {other:?}"),
1085        }
1086    }
1087
1088    /// Test edge case where max_module_size is set to 0
1089    #[test]
1090    fn test_wasm_module_size_limit_zero() {
1091        use crate::logic::VMLimits;
1092
1093        // A minimal WASM module (non-empty)
1094        let wat = r#"
1095            (module
1096                (memory (export "memory") 1)
1097                (func (export "test_func"))
1098            )
1099        "#;
1100        let wasm = wat::parse_str(wat).expect("Failed to parse WAT");
1101
1102        // Create an engine with max_module_size = 0
1103        let mut limits = VMLimits::default();
1104        limits.max_module_size = 0;
1105
1106        let engine = Engine::new(wasmer::Engine::default(), limits);
1107
1108        // Any non-empty module should be rejected
1109        let result = engine.compile(&wasm);
1110
1111        match result {
1112            Err(FunctionCallError::ModuleSizeLimitExceeded { size, max }) => {
1113                assert_eq!(max, 0);
1114                assert!(size > 0, "Module size should be greater than 0");
1115            }
1116            Ok(_) => panic!("Expected ModuleSizeLimitExceeded error with max_module_size=0"),
1117            Err(other) => panic!("Expected ModuleSizeLimitExceeded error, got: {other:?}"),
1118        }
1119
1120        // Empty byte slice should pass size check (0 > 0 is false) but fail compilation
1121        let empty_bytes: &[u8] = &[];
1122        let empty_result = engine.compile(empty_bytes);
1123
1124        match empty_result {
1125            Err(FunctionCallError::CompilationError { .. }) => {
1126                // Expected - empty bytes pass size check but fail compilation
1127            }
1128            Err(FunctionCallError::ModuleSizeLimitExceeded { .. }) => {
1129                panic!("Empty module should pass size check (0 is not > 0)")
1130            }
1131            Ok(_) => panic!("Empty bytes should not compile successfully"),
1132            Err(other) => panic!("Expected CompilationError for empty bytes, got: {other:?}"),
1133        }
1134    }
1135}