use crate::Error;
use crate::wasm_op::WasmOp;
pub fn check_no_underflow(wasm_ops: &[WasmOp]) -> crate::Result<()> {
let mut depth: i64 = 0;
for (idx, op) in wasm_ops.iter().enumerate() {
match stack_effect_or_bail(op) {
StackEffect::Modeled { pops, pushes } => {
if depth < pops as i64 {
return Err(Error::validation(format!(
"wasm value-stack underflow at op {idx} ({op:?}): \
would pop {pops} from depth {depth}"
)));
}
depth -= pops as i64;
depth += pushes as i64;
}
StackEffect::Bail => return Ok(()),
}
}
Ok(())
}
enum StackEffect {
Modeled { pops: u32, pushes: u32 },
Bail,
}
fn modeled(pops: u32, pushes: u32) -> StackEffect {
StackEffect::Modeled { pops, pushes }
}
#[allow(clippy::too_many_lines)]
fn stack_effect_or_bail(op: &WasmOp) -> StackEffect {
use WasmOp::*;
match op {
I32Const(_) | I64Const(_) | F32Const(_) | F64Const(_) | V128Const(_) | LocalGet(_)
| GlobalGet(_) | MemorySize(_) => modeled(0, 1),
I32Add | I32Sub | I32Mul | I32DivS | I32DivU | I32RemS | I32RemU | I32And | I32Or
| I32Xor | I32Shl | I32ShrS | I32ShrU | I32Rotl | I32Rotr | I32Eq | I32Ne | I32LtS
| I32LtU | I32LeS | I32LeU | I32GtS | I32GtU | I32GeS | I32GeU => modeled(2, 1),
I32Clz | I32Ctz | I32Popcnt | I32Eqz | I32Extend8S | I32Extend16S | I32WrapI64 => {
modeled(1, 1)
}
I64Add | I64Sub | I64Mul | I64DivS | I64DivU | I64RemS | I64RemU | I64And | I64Or
| I64Xor | I64Shl | I64ShrS | I64ShrU | I64Rotl | I64Rotr | I64Eq | I64Ne | I64LtS
| I64LtU | I64LeS | I64LeU | I64GtS | I64GtU | I64GeS | I64GeU => modeled(2, 1),
I64Clz | I64Ctz | I64Popcnt | I64Eqz | I64Extend8S | I64Extend16S | I64Extend32S
| I64ExtendI32S | I64ExtendI32U => modeled(1, 1),
F32Add | F32Sub | F32Mul | F32Div | F32Eq | F32Ne | F32Lt | F32Le | F32Gt | F32Ge
| F32Min | F32Max | F32Copysign => modeled(2, 1),
F32Abs | F32Neg | F32Ceil | F32Floor | F32Trunc | F32Nearest | F32Sqrt => modeled(1, 1),
F64Add | F64Sub | F64Mul | F64Div | F64Eq | F64Ne | F64Lt | F64Le | F64Gt | F64Ge
| F64Min | F64Max | F64Copysign => modeled(2, 1),
F64Abs | F64Neg | F64Ceil | F64Floor | F64Trunc | F64Nearest | F64Sqrt => modeled(1, 1),
F32ConvertI32S | F32ConvertI32U | F32ConvertI64S | F32ConvertI64U | F32DemoteF64
| F32ReinterpretI32 | I32ReinterpretF32 | I32TruncF32S | I32TruncF32U | F64ConvertI32S
| F64ConvertI32U | F64ConvertI64S | F64ConvertI64U | F64PromoteF32 | F64ReinterpretI64
| I64ReinterpretF64 | I64TruncF64S | I64TruncF64U | I32TruncF64S | I32TruncF64U => {
modeled(1, 1)
}
LocalSet(_) | GlobalSet(_) | Drop => modeled(1, 0),
LocalTee(_) => modeled(1, 1),
I32Load { .. }
| I32Load8S { .. }
| I32Load8U { .. }
| I32Load16S { .. }
| I32Load16U { .. }
| I64Load { .. }
| I64Load8S { .. }
| I64Load8U { .. }
| I64Load16S { .. }
| I64Load16U { .. }
| I64Load32S { .. }
| I64Load32U { .. }
| F32Load { .. }
| F64Load { .. } => modeled(1, 1),
I32Store { .. }
| I32Store8 { .. }
| I32Store16 { .. }
| I64Store { .. }
| I64Store8 { .. }
| I64Store16 { .. }
| I64Store32 { .. }
| F32Store { .. }
| F64Store { .. } => modeled(2, 0),
MemoryGrow(_) => modeled(1, 1),
Select => modeled(3, 1),
Nop => modeled(0, 0),
Unreachable => modeled(0, 0),
Return | Br(_) | BrTable { .. } => modeled(0, 0),
BrIf(_) => modeled(1, 0),
Block | Loop | If | Else | End => modeled(0, 0),
Call(_) => StackEffect::Bail,
_ => StackEffect::Bail,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn binary_op_at_empty_stack_is_underflow() {
let err = check_no_underflow(&[WasmOp::I32DivS]).unwrap_err();
assert!(matches!(err, Error::ValidationError(_)), "got: {err:?}");
let msg = format!("{err}");
assert!(msg.contains("underflow"));
assert!(msg.contains("I32DivS"));
}
#[test]
fn well_formed_add_passes() {
let ops = vec![WasmOp::I32Const(1), WasmOp::I32Const(2), WasmOp::I32Add];
assert!(check_no_underflow(&ops).is_ok());
}
#[test]
fn unary_op_at_empty_stack_is_underflow() {
let err = check_no_underflow(&[WasmOp::I32Eqz]).unwrap_err();
assert!(matches!(err, Error::ValidationError(_)));
}
#[test]
fn drop_at_empty_stack_is_underflow() {
let err = check_no_underflow(&[WasmOp::Drop]).unwrap_err();
assert!(matches!(err, Error::ValidationError(_)));
}
#[test]
fn store_at_empty_stack_is_underflow() {
let err = check_no_underflow(&[WasmOp::I32Store {
offset: 0,
align: 2,
}])
.unwrap_err();
assert!(matches!(err, Error::ValidationError(_)));
}
#[test]
fn select_needs_three_operands() {
let ops = vec![WasmOp::I32Const(1), WasmOp::I32Const(2), WasmOp::Select];
let err = check_no_underflow(&ops).unwrap_err();
assert!(matches!(err, Error::ValidationError(_)));
}
#[test]
fn select_with_three_operands_passes() {
let ops = vec![
WasmOp::I32Const(1),
WasmOp::I32Const(2),
WasmOp::I32Const(0),
WasmOp::Select,
];
assert!(check_no_underflow(&ops).is_ok());
}
#[test]
fn call_bails_conservatively() {
let ops = vec![WasmOp::Call(0), WasmOp::I32Add];
assert!(check_no_underflow(&ops).is_ok());
}
#[test]
fn return_then_binary_op_at_depth_zero_is_underflow() {
let ops = vec![WasmOp::Return, WasmOp::I64Eqz];
let err = check_no_underflow(&ops).unwrap_err();
assert!(matches!(err, Error::ValidationError(_)));
}
#[test]
fn br_then_binary_op_at_depth_zero_is_underflow() {
let ops = vec![WasmOp::Br(0), WasmOp::I32Add];
let err = check_no_underflow(&ops).unwrap_err();
assert!(matches!(err, Error::ValidationError(_)));
}
#[test]
fn br_if_pops_condition() {
let ops = vec![WasmOp::BrIf(0)];
let err = check_no_underflow(&ops).unwrap_err();
assert!(matches!(err, Error::ValidationError(_)));
}
#[test]
fn br_if_with_condition_then_op_is_ok() {
let ops = vec![
WasmOp::I32Const(1),
WasmOp::BrIf(0),
WasmOp::I32Const(0),
WasmOp::I32Eqz,
];
assert!(check_no_underflow(&ops).is_ok());
}
#[test]
fn unreachable_then_binary_op_at_depth_zero_is_underflow() {
let ops = vec![WasmOp::Unreachable, WasmOp::I32GeS];
let err = check_no_underflow(&ops).unwrap_err();
assert!(matches!(err, Error::ValidationError(_)));
}
#[test]
fn unreachable_then_consts_then_binary_op_is_ok() {
let ops = vec![
WasmOp::Unreachable,
WasmOp::I32Const(1),
WasmOp::I32Const(2),
WasmOp::I32GeS,
];
assert!(check_no_underflow(&ops).is_ok());
}
#[test]
fn const_then_unary_then_binary() {
let ops = vec![
WasmOp::I32Const(0),
WasmOp::I32Eqz,
WasmOp::I32Const(1),
WasmOp::I32Const(2),
WasmOp::I32Add,
];
assert!(check_no_underflow(&ops).is_ok());
}
#[test]
fn empty_input_is_ok() {
assert!(check_no_underflow(&[]).is_ok());
}
}