//! VM compatibility validation for Synor smart contracts. //! //! This module validates WASM bytecode against Synor VM requirements: //! //! - Required exports (memory, init, call functions) //! - Memory limits and configuration //! - Import validation (only allowed host functions) //! - Feature detection (SIMD, threads, etc.) //! - Security checks use std::collections::{HashMap, HashSet}; use thiserror::Error; use tracing::{debug, info, warn}; use wasmparser::{Parser, Payload}; use synor_vm::host::wasm_imports; use synor_vm::{MAX_CONTRACT_SIZE, MAX_MEMORY_PAGES}; /// Validation errors. #[derive(Debug, Clone, Error)] pub enum ValidationError { /// WASM parsing error. #[error("Parse error: {0}")] ParseError(String), /// Contract exceeds maximum size. #[error("Contract too large: {size} bytes (max: {max} bytes)")] ContractTooLarge { size: usize, max: usize }, /// Missing required export. #[error("Missing required export: {0}")] MissingExport(String), /// Invalid export type. #[error("Invalid export type for '{name}': expected {expected}, got {actual}")] InvalidExportType { name: String, expected: String, actual: String, }, /// Invalid export signature. #[error("Invalid signature for export '{name}': expected {expected}, got {actual}")] InvalidExportSignature { name: String, expected: String, actual: String, }, /// Invalid import. #[error("Invalid import: {module}::{name} - {reason}")] InvalidImport { module: String, name: String, reason: String, }, /// Memory configuration error. #[error("Memory error: {0}")] MemoryError(String), /// Unsupported WASM feature. #[error("Unsupported WASM feature: {0}")] UnsupportedFeature(String), /// Security violation. #[error("Security violation: {0}")] SecurityViolation(String), /// Multiple errors. #[error("Multiple validation errors")] Multiple(Vec), } /// Validation result. #[derive(Clone, Debug)] pub struct ValidationResult { /// Whether validation succeeded. pub valid: bool, /// Validation errors. pub errors: Vec, /// Validation warnings. pub warnings: Vec, /// Detected exports. pub exports: Vec, /// Detected imports. pub imports: Vec, /// Memory information. pub memory: Option, /// Detected WASM features. pub features: WasmFeatures, } impl ValidationResult { /// Creates a successful validation result. pub fn success() -> Self { ValidationResult { valid: true, errors: Vec::new(), warnings: Vec::new(), exports: Vec::new(), imports: Vec::new(), memory: None, features: WasmFeatures::default(), } } /// Creates a failed validation result. pub fn failure(error: ValidationError) -> Self { ValidationResult { valid: false, errors: vec![error], warnings: Vec::new(), exports: Vec::new(), imports: Vec::new(), memory: None, features: WasmFeatures::default(), } } } /// Export information. #[derive(Clone, Debug)] pub struct ExportInfo { /// Export name. pub name: String, /// Export kind. pub kind: ExportKind, /// Index in the respective section. pub index: u32, } /// Export kind. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ExportKind { Function, Table, Memory, Global, } /// Import information. #[derive(Clone, Debug)] pub struct ImportInfo { /// Module name. pub module: String, /// Import name. pub name: String, /// Import kind. pub kind: ImportKind, } /// Import kind. #[derive(Clone, Debug, PartialEq, Eq)] pub enum ImportKind { Function(u32), // Type index Table, Memory, Global, } /// Memory information. #[derive(Clone, Debug)] pub struct MemoryInfo { /// Minimum pages. pub min_pages: u64, /// Maximum pages (if specified). pub max_pages: Option, /// Whether memory is shared. pub shared: bool, /// Whether memory is 64-bit. pub memory64: bool, } /// Detected WASM features. #[derive(Clone, Debug, Default)] pub struct WasmFeatures { /// Multi-value returns. pub multi_value: bool, /// Reference types. pub reference_types: bool, /// SIMD instructions. pub simd: bool, /// Threads and atomics. pub threads: bool, /// Bulk memory operations. pub bulk_memory: bool, /// Exception handling. pub exceptions: bool, /// Tail calls. pub tail_calls: bool, /// Memory64. pub memory64: bool, /// Mutable globals. pub mutable_globals: bool, /// Sign extension operations. pub sign_extension: bool, /// Saturating float-to-int conversions. pub saturating_float_to_int: bool, } /// WASM validator for Synor VM compatibility. pub struct Validator { /// Maximum contract size. max_contract_size: usize, /// Maximum memory pages. max_memory_pages: u32, /// Allowed import modules. allowed_imports: HashMap>, /// Required exports. required_exports: HashSet, } impl Validator { /// Creates a new validator. pub fn new(max_contract_size: usize, max_memory_pages: u32) -> Self { let mut allowed_imports = HashMap::new(); // Build allowed env imports from synor_vm let mut env_imports = HashSet::new(); env_imports.insert(wasm_imports::STORAGE_READ.to_string()); env_imports.insert(wasm_imports::STORAGE_WRITE.to_string()); env_imports.insert(wasm_imports::STORAGE_DELETE.to_string()); env_imports.insert(wasm_imports::GET_CALLER.to_string()); env_imports.insert(wasm_imports::GET_ADDRESS.to_string()); env_imports.insert(wasm_imports::GET_VALUE.to_string()); env_imports.insert(wasm_imports::GET_CALLDATA_SIZE.to_string()); env_imports.insert(wasm_imports::GET_CALLDATA.to_string()); env_imports.insert(wasm_imports::GET_TIMESTAMP.to_string()); env_imports.insert(wasm_imports::GET_BLOCK_HEIGHT.to_string()); env_imports.insert(wasm_imports::SHA3.to_string()); env_imports.insert(wasm_imports::BLAKE3.to_string()); env_imports.insert(wasm_imports::EMIT_LOG.to_string()); env_imports.insert(wasm_imports::CALL.to_string()); env_imports.insert(wasm_imports::REVERT.to_string()); env_imports.insert(wasm_imports::RETURN.to_string()); allowed_imports.insert("env".to_string(), env_imports); // Required exports let mut required_exports = HashSet::new(); required_exports.insert("memory".to_string()); // __synor_init and __synor_call are optional but recommended Validator { max_contract_size, max_memory_pages, allowed_imports, required_exports, } } /// Creates a validator with default settings. pub fn default_validator() -> Self { Self::new(MAX_CONTRACT_SIZE, MAX_MEMORY_PAGES) } /// Validates WASM bytecode. pub fn validate(&self, wasm: &[u8]) -> Result { info!("Validating contract ({} bytes)", wasm.len()); // Check size if wasm.len() > self.max_contract_size { return Err(ValidationError::ContractTooLarge { size: wasm.len(), max: self.max_contract_size, }); } let mut result = ValidationResult::success(); let mut errors = Vec::new(); // Parse the WASM module let parser = Parser::new(0); let mut imports = Vec::new(); let mut exports = Vec::new(); let mut memories = Vec::new(); let mut has_mutable_globals = false; for payload in parser.parse_all(wasm) { let payload = payload.map_err(|e| ValidationError::ParseError(e.to_string()))?; match payload { Payload::Version { encoding, .. } => { if encoding != wasmparser::Encoding::Module { return Err(ValidationError::UnsupportedFeature( "WASM components not supported".into(), )); } } Payload::ImportSection(reader) => { for import in reader { let import = import.map_err(|e| ValidationError::ParseError(e.to_string()))?; let kind = match import.ty { wasmparser::TypeRef::Func(type_idx) => ImportKind::Function(type_idx), wasmparser::TypeRef::Table(_) => ImportKind::Table, wasmparser::TypeRef::Memory(_) => ImportKind::Memory, wasmparser::TypeRef::Global(_) => ImportKind::Global, wasmparser::TypeRef::Tag(_) => { return Err(ValidationError::UnsupportedFeature( "Exception handling tags".into(), )); } }; imports.push(ImportInfo { module: import.module.to_string(), name: import.name.to_string(), kind, }); } } Payload::MemorySection(reader) => { for memory in reader { let memory = memory.map_err(|e| ValidationError::ParseError(e.to_string()))?; memories.push(MemoryInfo { min_pages: memory.initial, max_pages: memory.maximum, shared: memory.shared, memory64: memory.memory64, }); } } Payload::GlobalSection(reader) => { for global in reader { let global = global.map_err(|e| ValidationError::ParseError(e.to_string()))?; if global.ty.mutable { has_mutable_globals = true; } } } Payload::ExportSection(reader) => { for export in reader { let export = export.map_err(|e| ValidationError::ParseError(e.to_string()))?; let kind = match export.kind { wasmparser::ExternalKind::Func => ExportKind::Function, wasmparser::ExternalKind::Table => ExportKind::Table, wasmparser::ExternalKind::Memory => ExportKind::Memory, wasmparser::ExternalKind::Global => ExportKind::Global, wasmparser::ExternalKind::Tag => { return Err(ValidationError::UnsupportedFeature( "Exception handling tags".into(), )); } }; exports.push(ExportInfo { name: export.name.to_string(), kind, index: export.index, }); } } Payload::End(_) => break, _ => {} } } // Validate imports debug!("Validating {} imports", imports.len()); for import in &imports { if let Err(e) = self.validate_import(import) { errors.push(e); } } result.imports = imports; // Validate exports debug!("Validating {} exports", exports.len()); for required in &self.required_exports { if !exports.iter().any(|e| e.name == *required) { errors.push(ValidationError::MissingExport(required.clone())); } } result.exports = exports.clone(); // Validate entry points (warn if missing, don't error) if !exports.iter().any(|e| e.name == "__synor_init") { result .warnings .push("Missing __synor_init export - contract cannot be initialized".into()); } if !exports.iter().any(|e| e.name == "__synor_call") { result .warnings .push("Missing __synor_call export - contract cannot be called".into()); } // Validate entry point signatures for export in &exports { if (export.name == "__synor_init" || export.name == "__synor_call") && export.kind != ExportKind::Function { errors.push(ValidationError::InvalidExportType { name: export.name.clone(), expected: "function".into(), actual: format!("{:?}", export.kind), }); } } // Validate memory debug!("Validating {} memories", memories.len()); if memories.is_empty() { // Check if memory is imported let has_imported_memory = result .imports .iter() .any(|i| matches!(i.kind, ImportKind::Memory)); if !has_imported_memory { errors.push(ValidationError::MemoryError( "No memory defined or imported".into(), )); } } else if memories.len() > 1 { errors.push(ValidationError::MemoryError( "Multiple memories not supported".into(), )); } else { let mem = &memories[0]; // Check max pages if let Some(max) = mem.max_pages { if max > self.max_memory_pages as u64 { errors.push(ValidationError::MemoryError(format!( "Memory max pages {} exceeds limit {}", max, self.max_memory_pages ))); } } else { result .warnings .push("Memory has no maximum limit - recommended to set max_pages".into()); } // Check for shared memory (threads) if mem.shared { result.features.threads = true; } // Check for memory64 if mem.memory64 { result.features.memory64 = true; } result.memory = Some(mem.clone()); } // Detect features from globals if has_mutable_globals { result.features.mutable_globals = true; } // Set result if errors.is_empty() { result.valid = true; info!("Validation passed"); } else { result.valid = false; result.errors = errors; warn!("Validation failed with {} errors", result.errors.len()); } Ok(result) } /// Validates an import. fn validate_import(&self, import: &ImportInfo) -> Result<(), ValidationError> { // Check if the module is allowed let allowed_names = self.allowed_imports.get(&import.module); match allowed_names { Some(names) => { // Check if the specific import is allowed if !names.contains(&import.name) { return Err(ValidationError::InvalidImport { module: import.module.clone(), name: import.name.clone(), reason: "Unknown import function".into(), }); } } None => { return Err(ValidationError::InvalidImport { module: import.module.clone(), name: import.name.clone(), reason: format!("Unknown import module '{}'", import.module), }); } } Ok(()) } /// Adds an allowed import. pub fn allow_import(&mut self, module: &str, name: &str) { self.allowed_imports .entry(module.to_string()) .or_default() .insert(name.to_string()); } /// Adds a required export. pub fn require_export(&mut self, name: &str) { self.required_exports.insert(name.to_string()); } } #[cfg(test)] mod tests { use super::*; fn minimal_wasm() -> Vec { // Minimal WASM module with memory export vec![ 0x00, 0x61, 0x73, 0x6d, // WASM magic 0x01, 0x00, 0x00, 0x00, // Version 1 // Memory section 0x05, 0x03, 0x01, 0x00, 0x01, // Memory: min 0, max 1 page // Export section 0x07, 0x0a, 0x01, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, // Export "memory" as memory 0 ] } #[test] fn test_validator_creation() { let validator = Validator::default_validator(); assert!(validator.allowed_imports.contains_key("env")); assert!(validator.required_exports.contains("memory")); } #[test] fn test_validate_minimal_wasm() { let validator = Validator::default_validator(); let result = validator.validate(&minimal_wasm()); assert!(result.is_ok()); let result = result.unwrap(); assert!(result.valid); } #[test] fn test_validate_too_large() { let validator = Validator::new(10, MAX_MEMORY_PAGES); let result = validator.validate(&[0u8; 100]); assert!(matches!( result, Err(ValidationError::ContractTooLarge { .. }) )); } #[test] fn test_export_info() { let export = ExportInfo { name: "memory".to_string(), kind: ExportKind::Memory, index: 0, }; assert_eq!(export.kind, ExportKind::Memory); } #[test] fn test_memory_info() { let mem = MemoryInfo { min_pages: 1, max_pages: Some(256), shared: false, memory64: false, }; assert_eq!(mem.min_pages, 1); assert_eq!(mem.max_pages, Some(256)); } #[test] fn test_wasm_features_default() { let features = WasmFeatures::default(); assert!(!features.simd); assert!(!features.threads); assert!(!features.memory64); } #[test] fn test_validation_result_success() { let result = ValidationResult::success(); assert!(result.valid); assert!(result.errors.is_empty()); } #[test] fn test_validation_result_failure() { let result = ValidationResult::failure(ValidationError::MissingExport("test".into())); assert!(!result.valid); assert_eq!(result.errors.len(), 1); } #[test] fn test_allow_import() { let mut validator = Validator::default_validator(); validator.allow_import("custom", "my_function"); assert!(validator .allowed_imports .get("custom") .unwrap() .contains("my_function")); } #[test] fn test_require_export() { let mut validator = Validator::default_validator(); validator.require_export("custom_export"); assert!(validator.required_exports.contains("custom_export")); } }