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
645 lines
20 KiB
Rust
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"));
|
|
}
|
|
}
|