diff --git a/src/core/reader/types/global.rs b/src/core/reader/types/global.rs index 4bee9fcf..88c12d32 100644 --- a/src/core/reader/types/global.rs +++ b/src/core/reader/types/global.rs @@ -1,11 +1,11 @@ use alloc::vec; -use crate::code::{read_constant_instructions, validate_value_stack}; use crate::core::reader::span::Span; use crate::core::reader::types::{ResultType, ValType}; use crate::core::reader::{WasmReadable, WasmReader}; use crate::execution::assert_validated::UnwrapValidatedExt; -use crate::{unreachable_validated, Error, Result}; +use crate::globals::read_constant_instructions; +use crate::{unreachable_validated, validate_value_stack, Error, Result}; #[derive(Debug, Copy, Clone)] pub struct Global { diff --git a/src/execution/const_interpreter_loop.rs b/src/execution/const_interpreter_loop.rs new file mode 100644 index 00000000..15250ad8 --- /dev/null +++ b/src/execution/const_interpreter_loop.rs @@ -0,0 +1,101 @@ +use crate::{ + assert_validated::UnwrapValidatedExt, core::reader::WasmReader, value_stack::Stack, NumType, + ValType, +}; + +/// Execute a previosly-validated constant expression. These type of expressions are used for initializing global +/// variables. +/// +/// # Arguments +/// - `wasm` - a [WasmReader] whose [program counter](WasmReader::pc) is set at the beginning of the constant +/// expression. Reader will be consumed. +/// - `stack` - a [Stack]. It is preferrable for it to be clean, but that is not required. As long as the executed code +/// is validated, the values on this stack will remain the same except for the addition of the return value of this +/// code sequence. A global's final value can be popped off the top of the stack. +/// - `imported_globals` (TODO) - instances of all imported globals. They are required as local globals can reference +/// imported globals in their initialization. +/// +/// # Safety +/// This function assumes that the expression has been validated. Passing unvalidated code will likely result in a +/// panic, or undefined behaviour. +/// +/// # Note +/// The following instructions are not yet supported: +/// - `ref.null` +/// - `ref.func` +/// - `global.get` +pub(crate) fn run_const( + mut wasm: WasmReader, + stack: &mut Stack, + _imported_globals: (), /*todo!*/ +) { + use crate::core::reader::types::opcode::*; + loop { + let first_instr_byte = wasm.read_u8().unwrap_validated(); + + match first_instr_byte { + END => { + break; + } + I32_CONST => { + let constant = wasm.read_var_i32().unwrap_validated(); + trace!("Constant instruction: i32.const [] -> [{constant}]"); + stack.push_value(constant.into()); + } + I32_ADD => { + let v1: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); + let v2: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); + let res = v1.wrapping_add(v2); + + trace!("Constant instruction: i32.add [{v1} {v2}] -> [{res}]"); + stack.push_value(res.into()); + } + I32_SUB => { + let v2: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); + let v1: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); + let res = v1.wrapping_sub(v2); + + trace!("Constant instruction: i32.sub [{v1} {v2}] -> [{res}]"); + stack.push_value(res.into()); + } + I32_MUL => { + let v1: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); + let v2: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); + let res = v1.wrapping_mul(v2); + + trace!("Constant instruction: i32.mul [{v1} {v2}] -> [{res}]"); + stack.push_value(res.into()); + } + I64_CONST => { + let constant = wasm.read_var_i64().unwrap_validated(); + trace!("Constant instruction: i64.const [] -> [{constant}]"); + stack.push_value(constant.into()); + } + I64_ADD => { + let v1: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); + let v2: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); + let res = v1.wrapping_add(v2); + + trace!("Constant instruction: i64.add [{v1} {v2}] -> [{res}]"); + stack.push_value(res.into()); + } + I64_SUB => { + let v2: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); + let v1: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); + let res = v1.wrapping_sub(v2); + + trace!("Constant instruction: i64.sub [{v1} {v2}] -> [{res}]"); + stack.push_value(res.into()); + } + I64_MUL => { + let v1: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); + let v2: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); + let res = v1.wrapping_mul(v2); + + trace!("Constant instruction: i64.mul [{v1} {v2}] -> [{res}]"); + stack.push_value(res.into()); + } + _ => panic!("¯\\_(ツ)_/¯"), + } + } +} diff --git a/src/execution/interpreter_loop.rs b/src/execution/interpreter_loop.rs index baf6e550..37294c37 100644 --- a/src/execution/interpreter_loop.rs +++ b/src/execution/interpreter_loop.rs @@ -1083,76 +1083,3 @@ pub(super) fn run( } Ok(()) } - -pub fn run_const(mut wasm: WasmReader, stack: &mut Stack, _imported_globals: () /*todo!*/) { - use crate::core::reader::types::opcode::*; - loop { - let first_instr_byte = wasm.read_u8().unwrap_validated(); - - match first_instr_byte { - // Missing: ref.null, ref.func, global.get - END => { - break; - } - I32_CONST => { - let constant = wasm.read_var_i32().unwrap_validated(); - trace!("Constant instruction: i32.const [] -> [{constant}]"); - stack.push_value(constant.into()); - } - I32_ADD => { - let v1: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); - let v2: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); - let res = v1.wrapping_add(v2); - - trace!("Constant instruction: i32.add [{v1} {v2}] -> [{res}]"); - stack.push_value(res.into()); - } - I32_SUB => { - let v2: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); - let v1: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); - let res = v1.wrapping_sub(v2); - - trace!("Constant instruction: i32.sub [{v1} {v2}] -> [{res}]"); - stack.push_value(res.into()); - } - I32_MUL => { - let v1: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); - let v2: i32 = stack.pop_value(ValType::NumType(NumType::I32)).into(); - let res = v1.wrapping_mul(v2); - - trace!("Constant instruction: i32.mul [{v1} {v2}] -> [{res}]"); - stack.push_value(res.into()); - } - I64_CONST => { - let constant = wasm.read_var_i64().unwrap_validated(); - trace!("Constant instruction: i64.const [] -> [{constant}]"); - stack.push_value(constant.into()); - } - I64_ADD => { - let v1: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); - let v2: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); - let res = v1.wrapping_add(v2); - - trace!("Constant instruction: i64.add [{v1} {v2}] -> [{res}]"); - stack.push_value(res.into()); - } - I64_SUB => { - let v2: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); - let v1: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); - let res = v1.wrapping_sub(v2); - - trace!("Constant instruction: i64.sub [{v1} {v2}] -> [{res}]"); - stack.push_value(res.into()); - } - I64_MUL => { - let v1: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); - let v2: i64 = stack.pop_value(ValType::NumType(NumType::I64)).into(); - let res = v1.wrapping_mul(v2); - - trace!("Constant instruction: i64.mul [{v1} {v2}] -> [{res}]"); - stack.push_value(res.into()); - } - _ => panic!("¯\\_(ツ)_/¯"), - } - } -} diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 51217f65..07a2f500 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -1,6 +1,7 @@ use alloc::vec::Vec; -use interpreter_loop::{run, run_const}; +use const_interpreter_loop::run_const; +use interpreter_loop::run; use locals::Locals; use value_stack::Stack; @@ -18,6 +19,7 @@ use crate::{RuntimeError, ValidationInfo}; // TODO pub(crate) mod assert_validated; +mod const_interpreter_loop; pub mod hooks; mod interpreter_loop; pub(crate) mod locals; @@ -240,18 +242,21 @@ where let global_instances: Vec = validation_info .globals .iter() - .map(|global| { - let mut wasm = WasmReader::new(validation_info.wasm); - // The place we are moving the start to should, by all means, be inside the wasm bytecode. - wasm.move_start_to(global.init_expr).unwrap_validated(); + .map({ let mut stack = Stack::new(); - - run_const(wasm, &mut stack, ()); - let value = stack.pop_value(global.ty.ty); - - GlobalInst { - global: *global, - value, + move |global| { + let mut wasm = WasmReader::new(validation_info.wasm); + // The place we are moving the start to should, by all means, be inside the wasm bytecode. + wasm.move_start_to(global.init_expr).unwrap_validated(); + // We shouldn't need to clear the stack. If validation is correct, it will remain empty after execution. + + run_const(wasm, &mut stack, ()); + let value = stack.pop_value(global.ty.ty); + + GlobalInst { + global: *global, + value, + } } }) .collect(); diff --git a/src/validation/code.rs b/src/validation/code.rs index 217d1769..8d8afb31 100644 --- a/src/validation/code.rs +++ b/src/validation/code.rs @@ -4,11 +4,11 @@ use core::iter; use crate::core::indices::{FuncIdx, GlobalIdx, LocalIdx}; use crate::core::reader::section_header::{SectionHeader, SectionTy}; use crate::core::reader::span::Span; -use crate::core::reader::types::global::{Global, GlobalType}; +use crate::core::reader::types::global::Global; use crate::core::reader::types::memarg::MemArg; -use crate::core::reader::types::{FuncType, NumType, ResultType, ValType}; +use crate::core::reader::types::{FuncType, NumType, ValType}; use crate::core::reader::{WasmReadable, WasmReader}; -use crate::{Error, Result}; +use crate::{validate_value_stack, Error, Result}; pub fn validate_code_section( wasm: &mut WasmReader, @@ -356,135 +356,3 @@ fn read_instructions( } } } - -pub fn read_constant_instructions( - wasm: &mut WasmReader, - value_stack: &mut Vec, - _globals_ty: &[GlobalType], -) -> Result { - let start_pc = wasm.pc; - - let assert_pop_value_stack = |value_stack: &mut Vec, expected_ty: ValType| { - value_stack - .pop() - .ok_or(Error::InvalidValueStackType(None)) - .and_then(|ty| { - (ty == expected_ty) - .then_some(()) - .ok_or(Error::InvalidValueStackType(Some(ty))) - }) - }; - - loop { - let Ok(first_instr_byte) = wasm.read_u8() else { - return Err(Error::ExprMissingEnd); - }; - trace!("Read cosntant instruction byte {first_instr_byte:#X?} ({first_instr_byte})"); - - // Valid constant instructions are: - // - Core: https://webassembly.github.io/spec/core/valid/instructions.html#valid-constant - // - Extended Proposal: https://webassembly.github.io/extended-const/core/valid/instructions.html#valid-constant - use crate::core::reader::types::opcode::*; - match first_instr_byte { - // Missing: ref.null, ref.func, global.get - // ------------- - // Global.get only (seems) to work for imported globals - // - // Take the example code: - // ```wat - // (module - // (global (export "g") (mut i32) ( - // i32.add (i32.const 1) (i32.const 2) - // )) - // - // (global (export "h1") i32 ( - // i32.const 1 - // )) - // - // (global (export "h2") i32 ( - // global.get 1 - // )) - // - // (func (export "f") - // i32.const 100 - // global.set 0)) - // ``` - // - // When compiling with wat2wasm, the following error is thrown: - // ``` - // Error: validate failed: - // test.wast:11:24: error: initializer expression can only reference an imported global - // global.get 1 - // ^ - // ``` - // - // When compiling the code with the latest dev build of wasmtime, the following error is thrown: - // ``` - // failed to parse WebAssembly module - // - // Caused by: - // constant expression required: global.get of locally defined global (at offset 0x24) - // ``` - // - // Furthermore, the global must be immutable: - // ```wat - // (module - // (import "env" "g" (global (mut i32))) - // (global (export "h") (mut i32) ( - // i32.add (i32.const 1) (global.get 0) - // )) - // ) - // ``` - // - // When compiling with wat2wasm, the following error is thrown: - // ``` - // Error: validate failed: - // test.wast:4:27: error: initializer expression cannot reference a mutable global - // i32.add (i32.const 1) (global.get 0) - // ``` - END => { - return Ok(Span::new(start_pc, wasm.pc - start_pc + 1)); - } - I32_CONST => { - let _num = wasm.read_var_i32()?; - value_stack.push(ValType::NumType(NumType::I32)); - } - I64_CONST => { - let _num = wasm.read_var_i64()?; - value_stack.push(ValType::NumType(NumType::I64)); - } - I32_ADD | I32_SUB | I32_MUL => { - assert_pop_value_stack(value_stack, ValType::NumType(NumType::I32))?; - assert_pop_value_stack(value_stack, ValType::NumType(NumType::I32))?; - - value_stack.push(ValType::NumType(NumType::I32)); - } - I64_ADD | I64_SUB | I64_MUL => { - assert_pop_value_stack(value_stack, ValType::NumType(NumType::I64))?; - assert_pop_value_stack(value_stack, ValType::NumType(NumType::I64))?; - - value_stack.push(ValType::NumType(NumType::I64)); - } - _ => return Err(Error::InvalidInstr(first_instr_byte)), - } - } -} - -pub fn validate_value_stack(return_ty: ResultType, f: F) -> Result<()> -where - F: FnOnce(&mut Vec) -> Result<()>, -{ - let mut value_stack: Vec = Vec::new(); - - f(&mut value_stack)?; - - // TODO also check here if correct order - if value_stack != return_ty.valtypes { - error!( - "Expected types {:?} on stack, got {:?}", - return_ty.valtypes, value_stack - ); - return Err(Error::EndInvalidValueStack); - } - Ok(()) -} diff --git a/src/validation/globals.rs b/src/validation/globals.rs new file mode 100644 index 00000000..306faabf --- /dev/null +++ b/src/validation/globals.rs @@ -0,0 +1,138 @@ +use alloc::vec::Vec; + +use crate::{ + core::reader::{span::Span, types::global::GlobalType, WasmReader}, + Error, NumType, Result, ValType, +}; + +/// Read and validate constant expressions. +/// +/// This function, alongside [`validate_value_stack()`](crate::validation::validate_value_stack) can be used to validate +/// that a constant expression produces the expected result. The main use case for this is to validate that an +/// initialization expression for a global returns the correct value. +/// +/// Note: to be valid, constant expressions may not leave garbage data on the stack. It may leave only what is expected +/// and nothing more. +/// +/// Valid constant instructions are: +/// - Core: +/// - Extended Proposal: +/// +/// # The Wonders of `global.get` +/// The `global.get` instruction is quite picky by nature. To make a long story short, there are two rules to follow to +/// be able to use this expression. +/// +/// ## 1. The referenced global must be imported +/// Take the example code: +/// ```wat +/// (module +/// (global (export "g") (mut i32) ( +/// i32.add (i32.const 1) (i32.const 2) +/// )) +/// +/// (global (export "h1") i32 ( +/// i32.const 1 +/// )) +/// +/// (global (export "h2") i32 ( +/// global.get 1 +/// )) +/// +/// (func (export "f") +/// i32.const 100 +/// global.set 0)) +/// ``` +/// +/// When compiling with wat2wasm, the following error is thrown: +/// ```wat +/// Error: validate failed: +/// test.wast:11:24: error: initializer expression can only reference an imported global +/// global.get 1 +/// ^ +/// ``` +/// +/// When compiling the code with the latest dev build of wasmtime, the following error is thrown: +/// ```wat +/// failed to parse WebAssembly module +/// +/// Caused by: +/// constant expression required: global.get of locally defined global (at offset 0x24) +/// ``` +/// +/// ## 2. The referenced global must be immutable +/// +///```wat +/// (module +/// (import "env" "g" (global (mut i32))) +/// (global (export "h") (mut i32) ( +/// i32.add (i32.const 1) (global.get 0) +/// )) +/// ) +/// ``` +/// +/// When compiling with wat2wasm, the following error is thrown: +/// ```wat +/// Error: validate failed: +/// test.wast:4:27: error: initializer expression cannot reference a mutable global +/// i32.add (i32.const 1) (global.get 0) +/// ``` +/// +/// # Note +/// The following instructions are not yet supported: +/// - `ref.null` +/// - `ref.func` +/// - `global.get` +pub(crate) fn read_constant_instructions( + wasm: &mut WasmReader, + value_stack: &mut Vec, + _globals_ty: &[GlobalType], +) -> Result { + let start_pc = wasm.pc; + + let assert_pop_value_stack = |value_stack: &mut Vec, expected_ty: ValType| { + value_stack + .pop() + .ok_or(Error::InvalidValueStackType(None)) + .and_then(|ty| { + (ty == expected_ty) + .then_some(()) + .ok_or(Error::InvalidValueStackType(Some(ty))) + }) + }; + + loop { + let Ok(first_instr_byte) = wasm.read_u8() else { + return Err(Error::ExprMissingEnd); + }; + trace!("Read cosntant instruction byte {first_instr_byte:#X?} ({first_instr_byte})"); + + use crate::core::reader::types::opcode::*; + match first_instr_byte { + // Missing: ref.null, ref.func, global.get + END => { + return Ok(Span::new(start_pc, wasm.pc - start_pc + 1)); + } + I32_CONST => { + let _num = wasm.read_var_i32()?; + value_stack.push(ValType::NumType(NumType::I32)); + } + I64_CONST => { + let _num = wasm.read_var_i64()?; + value_stack.push(ValType::NumType(NumType::I64)); + } + I32_ADD | I32_SUB | I32_MUL => { + assert_pop_value_stack(value_stack, ValType::NumType(NumType::I32))?; + assert_pop_value_stack(value_stack, ValType::NumType(NumType::I32))?; + + value_stack.push(ValType::NumType(NumType::I32)); + } + I64_ADD | I64_SUB | I64_MUL => { + assert_pop_value_stack(value_stack, ValType::NumType(NumType::I64))?; + assert_pop_value_stack(value_stack, ValType::NumType(NumType::I64))?; + + value_stack.push(ValType::NumType(NumType::I64)); + } + _ => return Err(Error::InvalidInstr(first_instr_byte)), + } + } +} diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 9d8f08d2..85d1e47e 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -6,11 +6,12 @@ use crate::core::reader::span::Span; use crate::core::reader::types::export::Export; use crate::core::reader::types::global::Global; use crate::core::reader::types::import::Import; -use crate::core::reader::types::{FuncType, MemType, TableType}; +use crate::core::reader::types::{FuncType, MemType, ResultType, TableType}; use crate::core::reader::{WasmReadable, WasmReader}; -use crate::{Error, Result}; +use crate::{Error, Result, ValType}; pub(crate) mod code; +pub(crate) mod globals; /// Information collected from validating a module. /// This can be used to create a [crate::RuntimeInstance]. @@ -192,3 +193,22 @@ fn handle_section Result>( _ => Ok(None), } } + +pub(crate) fn validate_value_stack(return_ty: ResultType, f: F) -> Result<()> +where + F: FnOnce(&mut Vec) -> Result<()>, +{ + let mut value_stack: Vec = Vec::new(); + + f(&mut value_stack)?; + + // TODO also check here if correct order + if value_stack != return_ty.valtypes { + error!( + "Expected types {:?} on stack, got {:?}", + return_ty.valtypes, value_stack + ); + return Err(Error::EndInvalidValueStack); + } + Ok(()) +} diff --git a/tests/globals.rs b/tests/globals.rs index 030e98e7..9b50dedb 100644 --- a/tests/globals.rs +++ b/tests/globals.rs @@ -3,7 +3,7 @@ /// - Setting the global's value and returning its previous value /// - Getting the global's current value #[test_log::test] -fn globals() { +fn valid_global() { use wasm::{validate, RuntimeInstance}; let wat = r#" @@ -37,6 +37,31 @@ fn globals() { assert_eq!(17, instance.invoke_named("get", ()).unwrap()); } +#[test_log::test] +fn global_invalid_value_stack() { + use wasm::validate; + + let wat = r#" + (module + (global $my_global (mut i32) + i32.const 2 + i32.const 2 + i32.const 2 + i32.const 2 + i32.const 2 + i32.const 2 + i32.const 3 + i32.add + ) + ) + "#; + let wasm_bytes = wat::parse_str(wat).unwrap(); + + if validate(&wasm_bytes).is_ok() { + panic!("validation succeeded") + } +} + #[ignore = "not yet implemented"] #[test_log::test] fn imported_globals() {