synor/crates/synor-compiler/src/validator.rs
Gulshan Yadav 5c643af64c fix: resolve all clippy warnings for CI
Fix all Rust clippy warnings that were causing CI failures when built
with RUSTFLAGS=-Dwarnings. Changes include:

- Replace derivable_impls with derive macros for BlockBody, Network, etc.
- Use div_ceil() instead of manual implementation
- Fix should_implement_trait by renaming from_str to parse
- Add type aliases for type_complexity warnings
- Use or_default(), is_some_and(), is_multiple_of() where appropriate
- Remove needless borrows and redundant closures
- Fix manual_strip with strip_prefix()
- Add allow attributes for intentional patterns (too_many_arguments,
  needless_range_loop in cryptographic code, assertions_on_constants)
- Remove unused imports, mut bindings, and dead code in tests
2026-01-08 05:58:22 +05:30

645 lines
20 KiB
Rust

//! 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<ValidationError>),
}
/// Validation result.
#[derive(Clone, Debug)]
pub struct ValidationResult {
/// Whether validation succeeded.
pub valid: bool,
/// Validation errors.
pub errors: Vec<ValidationError>,
/// Validation warnings.
pub warnings: Vec<String>,
/// Detected exports.
pub exports: Vec<ExportInfo>,
/// Detected imports.
pub imports: Vec<ImportInfo>,
/// Memory information.
pub memory: Option<MemoryInfo>,
/// 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<u64>,
/// 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<String, HashSet<String>>,
/// Required exports.
required_exports: HashSet<String>,
}
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<ValidationResult, ValidationError> {
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<u8> {
// 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"));
}
}