use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use wasmparser::{Operator, ValType};
pub struct IncrementalVerifier {
#[allow(dead_code)]
invariants: InvariantChecker,
stack_analyzer: StackAnalyzer,
}
impl IncrementalVerifier {
pub fn new() -> Result<Self> {
Ok(Self {
invariants: InvariantChecker::new(),
stack_analyzer: StackAnalyzer::new(),
})
}
pub fn verify_module(&self, binary: &[u8]) -> Result<VerificationResult> {
let parser = wasmparser::Parser::new(0);
let mut result = VerificationResult::Safe;
for payload in parser.parse_all(binary) {
let payload = payload?;
if let wasmparser::Payload::CodeSectionEntry(body) = payload {
let func_result = self.verify_function(body)?;
if !func_result.is_safe() {
result = func_result;
break;
}
}
}
Ok(result)
}
fn verify_function(&self, body: wasmparser::FunctionBody) -> Result<VerificationResult> {
let mut stack_types = Vec::new();
let memory_size = 65536;
let reader = body.get_operators_reader()?;
for op in reader {
let operator = op?;
match operator {
Operator::I32Load { memarg } => {
if memarg.offset as usize > memory_size - 4 {
return Ok(VerificationResult::OutOfBounds {
offset: memarg.offset as usize,
size: memory_size,
});
}
if stack_types.pop() != Some(ValType::I32) {
return Ok(VerificationResult::TypeError {
expected: "i32".to_string(),
found: None,
});
}
stack_types.push(ValType::I32);
}
Operator::I32Store { memarg } => {
if memarg.offset as usize > memory_size - 4 {
return Ok(VerificationResult::OutOfBounds {
offset: memarg.offset as usize,
size: memory_size,
});
}
if stack_types.pop() != Some(ValType::I32) {
return Ok(VerificationResult::TypeError {
expected: "i32".to_string(),
found: None,
});
}
if stack_types.pop() != Some(ValType::I32) {
return Ok(VerificationResult::TypeError {
expected: "i32".to_string(),
found: None,
});
}
}
_ => {
self.stack_analyzer
.update_stack(&mut stack_types, &operator)?;
}
}
}
Ok(VerificationResult::Safe)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum VerificationResult {
Safe,
OutOfBounds {
offset: usize,
size: usize,
},
TypeError {
expected: String,
found: Option<String>,
},
StackUnderflow,
StackOverflow,
InvalidIndirectCall,
IntegerOverflow,
}
impl VerificationResult {
#[must_use]
pub fn is_safe(&self) -> bool {
matches!(self, VerificationResult::Safe)
}
}
#[derive(Debug, Clone)]
struct InvariantChecker {
#[allow(dead_code)]
invariants: Vec<SafetyInvariant>,
}
impl InvariantChecker {
fn new() -> Self {
Self {
invariants: vec![
SafetyInvariant::MemoryBounds,
SafetyInvariant::StackBalance,
SafetyInvariant::TypeSafety,
SafetyInvariant::NoIntegerOverflow,
],
}
}
#[allow(dead_code)]
fn check_all(&self, _module: &[u8]) -> Vec<InvariantViolation> {
Vec::new()
}
}
#[derive(Debug, Clone)]
enum SafetyInvariant {
MemoryBounds,
StackBalance,
TypeSafety,
NoIntegerOverflow,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct InvariantViolation {
invariant: SafetyInvariant,
location: usize,
description: String,
}
#[derive(Debug, Clone)]
struct StackAnalyzer {
#[allow(dead_code)]
type_stack: Vec<ValType>,
}
impl StackAnalyzer {
fn new() -> Self {
Self {
type_stack: Vec::new(),
}
}
fn update_stack(&self, stack: &mut Vec<ValType>, op: &Operator) -> Result<()> {
use Operator::{I32Const, I64Const, F32Const, F64Const, I32Add, I32Sub, I32Mul, I32DivS, I32DivU, I64Add, I64Sub, I64Mul, I64DivS, I64DivU, I32Eqz, I32Eq, I32Ne, I32LtS, I32LtU, I32GtS, I32GtU, LocalGet, LocalSet, Drop};
match op {
I32Const { .. } => stack.push(ValType::I32),
I64Const { .. } => stack.push(ValType::I64),
F32Const { .. } => stack.push(ValType::F32),
F64Const { .. } => stack.push(ValType::F64),
I32Add | I32Sub | I32Mul | I32DivS | I32DivU => {
if stack.len() < 2 {
return Err(anyhow!("Stack underflow"));
}
stack.pop();
stack.pop();
stack.push(ValType::I32);
}
I64Add | I64Sub | I64Mul | I64DivS | I64DivU => {
if stack.len() < 2 {
return Err(anyhow!("Stack underflow"));
}
stack.pop();
stack.pop();
stack.push(ValType::I64);
}
I32Eqz => {
if stack.pop() != Some(ValType::I32) {
return Err(anyhow!("Type error: expected i32"));
}
stack.push(ValType::I32);
}
I32Eq | I32Ne | I32LtS | I32LtU | I32GtS | I32GtU => {
if stack.len() < 2 {
return Err(anyhow!("Stack underflow"));
}
if stack.pop() != Some(ValType::I32) || stack.pop() != Some(ValType::I32) {
return Err(anyhow!("Type error: expected i32"));
}
stack.push(ValType::I32);
}
LocalGet { local_index: _ } => {
stack.push(ValType::I32); }
LocalSet { local_index: _ } => {
if stack.is_empty() {
return Err(anyhow!("Stack underflow"));
}
stack.pop();
}
Drop => {
if stack.is_empty() {
return Err(anyhow!("Stack underflow"));
}
stack.pop();
}
_ => {} }
Ok(())
}
}
pub struct DifferentialTester {
#[allow(dead_code)]
test_cases: Vec<TestCase>,
}
impl Default for DifferentialTester {
fn default() -> Self {
Self::new()
}
}
impl DifferentialTester {
#[must_use]
pub fn new() -> Self {
Self {
test_cases: Vec::new(),
}
}
pub fn generate_test_cases(&mut self, _module: &[u8], count: usize) -> Vec<TestCase> {
let mut cases = Vec::new();
for i in 0..count {
cases.push(TestCase {
inputs: vec![i as i32, (i * 2) as i32],
expected_output: None,
});
}
cases
}
#[must_use]
pub fn differential_test(&self, _module: &[u8]) -> DifferentialResult {
DifferentialResult::Consistent
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TestCase {
pub inputs: Vec<i32>,
pub expected_output: Option<Vec<i32>>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DifferentialResult {
Consistent,
Divergence {
test_case: TestCase,
runtime1_result: Vec<i32>,
runtime2_result: Vec<i32>,
},
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}