All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Reviewed-on: #13 Co-authored-by: bQUARKz <bquarkz@gmail.com> Co-committed-by: bQUARKz <bquarkz@gmail.com>
1396 lines
51 KiB
Rust
1396 lines
51 KiB
Rust
use crate::lookup_intrinsic_by_id;
|
|
use prometeu_bytecode::FunctionMeta;
|
|
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
|
|
use prometeu_bytecode::isa::core::CoreOpCodeSpecExt as OpCodeSpecExt;
|
|
use prometeu_bytecode::{DecodeError, decode_next};
|
|
use prometeu_bytecode::{FunctionLayout, compute_function_layouts};
|
|
use prometeu_hal::syscalls::Syscall;
|
|
use std::collections::{HashMap, HashSet, VecDeque};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum VerifierError {
|
|
UnknownOpcode {
|
|
pc: usize,
|
|
opcode: u16,
|
|
},
|
|
TruncatedOpcode {
|
|
pc: usize,
|
|
},
|
|
TruncatedImmediate {
|
|
pc: usize,
|
|
opcode: OpCode,
|
|
need: usize,
|
|
have: usize,
|
|
},
|
|
InvalidJumpTarget {
|
|
pc: usize,
|
|
target: usize,
|
|
},
|
|
JumpToMidInstruction {
|
|
pc: usize,
|
|
target: usize,
|
|
},
|
|
StackUnderflow {
|
|
pc: usize,
|
|
opcode: OpCode,
|
|
},
|
|
StackOverflow {
|
|
pc: usize,
|
|
height: u16,
|
|
limit: u16,
|
|
},
|
|
StackMismatchJoin {
|
|
pc: usize,
|
|
target: usize,
|
|
height_in: u16,
|
|
height_target: u16,
|
|
},
|
|
BadRetStackHeight {
|
|
pc: usize,
|
|
height: u16,
|
|
expected: u16,
|
|
},
|
|
FunctionOutOfBounds {
|
|
func_idx: usize,
|
|
start: usize,
|
|
end: usize,
|
|
code_len: usize,
|
|
},
|
|
InvalidSyscallId {
|
|
pc: usize,
|
|
id: u32,
|
|
},
|
|
InvalidIntrinsicId {
|
|
pc: usize,
|
|
id: u32,
|
|
},
|
|
TrailingBytes {
|
|
func_idx: usize,
|
|
at_pc: usize,
|
|
},
|
|
InvalidFuncId {
|
|
pc: usize,
|
|
id: u32,
|
|
},
|
|
HostcallNotPatched {
|
|
pc: usize,
|
|
sysc_index: u32,
|
|
},
|
|
/// Execution can fall through past the end of the function without a valid terminator
|
|
/// (e.g., RET, JMP to end, HALT/TRAP). Verifier requires every reachable path to end
|
|
/// in a terminator.
|
|
UnterminatedPath {
|
|
func_idx: usize,
|
|
at_pc: usize,
|
|
},
|
|
// --- Closure-specific errors ---
|
|
/// Top of stack is not a closure value on CALL_CLOSURE
|
|
NotAClosureOnCallClosure {
|
|
pc: usize,
|
|
},
|
|
/// CALL_CLOSURE used with a closure whose callee function is not known at verify time
|
|
UnknownClosureCallee {
|
|
pc: usize,
|
|
},
|
|
/// User-provided arg_count for CALL_CLOSURE does not match callee signature
|
|
BadClosureArgCount {
|
|
pc: usize,
|
|
expected: u16,
|
|
got: u16,
|
|
},
|
|
/// YIELD executed in an invalid context (minimal safety rule violation)
|
|
InvalidYieldContext {
|
|
pc: usize,
|
|
height: u16,
|
|
},
|
|
/// SPAWN arg_count does not match callee param_slots
|
|
BadSpawnArgCount {
|
|
pc: usize,
|
|
expected: u16,
|
|
got: u16,
|
|
},
|
|
}
|
|
|
|
pub struct Verifier;
|
|
|
|
impl Verifier {
|
|
pub fn verify(code: &[u8], functions: &[FunctionMeta]) -> Result<Vec<u16>, VerifierError> {
|
|
let mut max_stacks = Vec::with_capacity(functions.len());
|
|
// Precompute function [start, end) ranges once for O(1) lookups
|
|
let layouts = compute_function_layouts(functions, code.len());
|
|
for (i, func) in functions.iter().enumerate() {
|
|
max_stacks.push(Self::verify_function(code, func, i, functions, &layouts)?);
|
|
}
|
|
Ok(max_stacks)
|
|
}
|
|
|
|
fn verify_function(
|
|
code: &[u8],
|
|
func: &FunctionMeta,
|
|
func_idx: usize,
|
|
all_functions: &[FunctionMeta],
|
|
layouts: &[FunctionLayout],
|
|
) -> Result<u16, VerifierError> {
|
|
let func_start = func.code_offset as usize;
|
|
// Use precomputed canonical range end
|
|
let func_end = layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
|
|
|
|
if func_start > code.len() || func_end > code.len() || func_start > func_end {
|
|
return Err(VerifierError::FunctionOutOfBounds {
|
|
func_idx,
|
|
start: func_start,
|
|
end: func_end,
|
|
code_len: code.len(),
|
|
});
|
|
}
|
|
|
|
let func_code = &code[func_start..func_end];
|
|
|
|
// Empty functions (no code bytes) are considered valid in the verifier.
|
|
// They do not consume or produce values on the stack and have no internal flow.
|
|
// Note: if an empty function is called at runtime and return/effects
|
|
// are expected, it is the responsibility of the code generator/linker to prevent this situation.
|
|
if func_code.is_empty() {
|
|
return Ok(0);
|
|
}
|
|
|
|
// First pass: find all valid instruction boundaries
|
|
let mut valid_pc = HashSet::new();
|
|
let mut pc = 0;
|
|
while pc < func_code.len() {
|
|
valid_pc.insert(pc);
|
|
let instr = decode_next(pc, func_code).map_err(|e| match e {
|
|
DecodeError::UnknownOpcode { pc: _, opcode } => {
|
|
VerifierError::UnknownOpcode { pc: func_start + pc, opcode }
|
|
}
|
|
DecodeError::TruncatedOpcode { pc: _ } => {
|
|
VerifierError::TruncatedOpcode { pc: func_start + pc }
|
|
}
|
|
DecodeError::TruncatedImmediate { pc: _, opcode, need, have } => {
|
|
VerifierError::TruncatedImmediate { pc: func_start + pc, opcode, need, have }
|
|
}
|
|
DecodeError::ImmediateSizeMismatch { pc: _, opcode, expected, actual } => {
|
|
VerifierError::TruncatedImmediate {
|
|
pc: func_start + pc,
|
|
opcode,
|
|
need: expected,
|
|
have: actual,
|
|
}
|
|
}
|
|
})?;
|
|
pc = instr.next_pc;
|
|
}
|
|
|
|
if pc != func_code.len() {
|
|
return Err(VerifierError::TrailingBytes { func_idx, at_pc: func_start + pc });
|
|
}
|
|
|
|
// Minimal stack type lattice to validate closure semantics
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum SlotTy {
|
|
/// Type information not known statically (join of different paths or from loads/calls)
|
|
Unknown,
|
|
/// A value that is definitely not a closure (immediates, arithmetic results)
|
|
NonClosure,
|
|
/// A closure value whose target function id is known (from MAKE_CLOSURE)
|
|
Closure(u32),
|
|
}
|
|
|
|
fn join_slot(a: SlotTy, b: SlotTy) -> SlotTy {
|
|
use SlotTy::*;
|
|
match (a, b) {
|
|
(Closure(id1), Closure(id2)) if id1 == id2 => Closure(id1),
|
|
(NonClosure, NonClosure) => NonClosure,
|
|
_ => Unknown,
|
|
}
|
|
}
|
|
|
|
let mut stack_types_in: HashMap<usize, Vec<SlotTy>> = HashMap::new();
|
|
let mut worklist = VecDeque::new();
|
|
let mut max_stack: u16 = 0;
|
|
|
|
// Start from function entry
|
|
stack_types_in.insert(0, Vec::new());
|
|
worklist.push_back(0);
|
|
|
|
while let Some(pc) = worklist.pop_front() {
|
|
let in_types = stack_types_in.get(&pc).cloned().unwrap();
|
|
let in_height = in_types.len() as u16;
|
|
let instr = decode_next(pc, func_code).unwrap(); // Guaranteed to succeed due to first pass
|
|
let spec = instr.opcode.spec();
|
|
|
|
// Resolve dynamic pops/_pushes
|
|
let (pops, _pushes) = match instr.opcode {
|
|
OpCode::PopN => {
|
|
let n = instr.imm_u32().unwrap() as u16;
|
|
(n, 0)
|
|
}
|
|
OpCode::Call => {
|
|
let func_id = instr.imm_u32().unwrap();
|
|
let callee = all_functions.get(func_id as usize).ok_or_else(|| {
|
|
VerifierError::InvalidFuncId { pc: func_start + pc, id: func_id }
|
|
})?;
|
|
(callee.param_slots, callee.return_slots)
|
|
}
|
|
OpCode::MakeClosure => {
|
|
// imm: fn_id (u32), capture_count (u32)
|
|
let (_fn_id, capture_count_u32) = instr.imm_u32x2().unwrap();
|
|
let capture_count = capture_count_u32 as u16;
|
|
(capture_count, 1)
|
|
}
|
|
OpCode::CallClosure => {
|
|
// imm: arg_count (u32). Pops closure_ref + arg_count, _pushes callee returns.
|
|
// We can't determine _pushes here without looking at TOS type; will validate below.
|
|
let arg_count = instr.imm_u32().unwrap() as u16;
|
|
// Temporarily set _pushes=0; we'll compute real _pushes after type checks.
|
|
(arg_count + 1, 0)
|
|
}
|
|
OpCode::Spawn => {
|
|
// imm: fn_id (u32), arg_count (u32)
|
|
let (fn_id, arg_count_u32) = instr.imm_u32x2().unwrap();
|
|
let callee = all_functions.get(fn_id as usize).ok_or_else(|| {
|
|
VerifierError::InvalidFuncId { pc: func_start + pc, id: fn_id }
|
|
})?;
|
|
let arg_count = arg_count_u32 as u16;
|
|
// Enforce exact arity match for safety and determinism
|
|
if callee.param_slots != arg_count {
|
|
return Err(VerifierError::BadSpawnArgCount {
|
|
pc: func_start + pc,
|
|
expected: callee.param_slots,
|
|
got: arg_count,
|
|
});
|
|
}
|
|
(arg_count, 0)
|
|
}
|
|
OpCode::Ret => (func.return_slots, 0),
|
|
OpCode::Hostcall => {
|
|
return Err(VerifierError::HostcallNotPatched {
|
|
pc: func_start + pc,
|
|
sysc_index: instr.imm_u32().unwrap(),
|
|
});
|
|
}
|
|
OpCode::Syscall => {
|
|
let id = instr.imm_u32().unwrap();
|
|
let syscall = Syscall::from_u32(id).ok_or_else(|| {
|
|
VerifierError::InvalidSyscallId { pc: func_start + pc, id }
|
|
})?;
|
|
(syscall.args_count() as u16, syscall.results_count() as u16)
|
|
}
|
|
OpCode::Intrinsic => {
|
|
let id = instr.imm_u32().unwrap();
|
|
let intrinsic = lookup_intrinsic_by_id(id).ok_or_else(|| {
|
|
VerifierError::InvalidIntrinsicId { pc: func_start + pc, id }
|
|
})?;
|
|
(intrinsic.arg_slots() as u16, intrinsic.ret_slots() as u16)
|
|
}
|
|
_ => (spec.pops, spec.pushes),
|
|
};
|
|
|
|
if in_height < pops {
|
|
return Err(VerifierError::StackUnderflow {
|
|
pc: func_start + pc,
|
|
opcode: instr.opcode,
|
|
});
|
|
}
|
|
|
|
// Coroutine safety: forbid YIELD when operand stack is not empty (minimal rule)
|
|
if let OpCode::Yield = instr.opcode
|
|
&& in_height != 0
|
|
{
|
|
return Err(VerifierError::InvalidYieldContext {
|
|
pc: func_start + pc,
|
|
height: in_height,
|
|
});
|
|
}
|
|
|
|
// Compute out types vector with closure-aware rules
|
|
use SlotTy::*;
|
|
let mut out_types = in_types.clone();
|
|
// Pop first
|
|
for _ in 0..pops {
|
|
out_types.pop();
|
|
}
|
|
|
|
// Special handling per opcode that affects types/_pushes
|
|
let mut dynamic_pushes: Option<u16> = None;
|
|
match instr.opcode {
|
|
OpCode::MakeClosure => {
|
|
let (fn_id, _cap) = instr.imm_u32x2().unwrap();
|
|
out_types.push(Closure(fn_id));
|
|
}
|
|
OpCode::CallClosure => {
|
|
// Validate TOS was a known closure and arg_count matches signature
|
|
let tos = in_types.last().copied().unwrap_or(Unknown);
|
|
match tos {
|
|
Closure(fn_id) => {
|
|
let callee = all_functions.get(fn_id as usize).ok_or_else(|| {
|
|
VerifierError::InvalidFuncId { pc: func_start + pc, id: fn_id }
|
|
})?;
|
|
let user_argc = instr.imm_u32().unwrap() as u16;
|
|
if callee.param_slots == 0 {
|
|
// Hidden arg0 must exist in callee signature
|
|
return Err(VerifierError::BadClosureArgCount {
|
|
pc: func_start + pc,
|
|
expected: 0,
|
|
got: user_argc,
|
|
});
|
|
}
|
|
let expected_user = callee.param_slots - 1; // exclude hidden arg0
|
|
if expected_user != user_argc {
|
|
return Err(VerifierError::BadClosureArgCount {
|
|
pc: func_start + pc,
|
|
expected: expected_user,
|
|
got: user_argc,
|
|
});
|
|
}
|
|
// Push callee returns as Unknown types
|
|
for _ in 0..callee.return_slots {
|
|
out_types.push(Unknown);
|
|
}
|
|
dynamic_pushes = Some(callee.return_slots);
|
|
}
|
|
NonClosure => {
|
|
return Err(VerifierError::NotAClosureOnCallClosure {
|
|
pc: func_start + pc,
|
|
});
|
|
}
|
|
Unknown => {
|
|
// We cannot determine return arity; be strict and reject
|
|
return Err(VerifierError::UnknownClosureCallee {
|
|
pc: func_start + pc,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Immediates and known non-closure _pushes
|
|
OpCode::PushConst
|
|
| OpCode::PushI64
|
|
| OpCode::PushF64
|
|
| OpCode::PushBool
|
|
| OpCode::PushI32 => {
|
|
out_types.push(NonClosure);
|
|
}
|
|
// Dup duplicates TOS type
|
|
OpCode::Dup => {
|
|
let tos = in_types.last().copied().unwrap_or(Unknown);
|
|
if matches!(tos, Unknown) && in_height == 0 {
|
|
// Will already have underflowed on pops check for other ops; for Dup, enforce explicitly
|
|
return Err(VerifierError::StackUnderflow {
|
|
pc: func_start + pc,
|
|
opcode: OpCode::Dup,
|
|
});
|
|
}
|
|
out_types.push(tos);
|
|
}
|
|
// Swap swaps top-2 types; ensure enough stack
|
|
OpCode::Swap => {
|
|
if in_types.len() < 2 {
|
|
return Err(VerifierError::StackUnderflow {
|
|
pc: func_start + pc,
|
|
opcode: OpCode::Swap,
|
|
});
|
|
}
|
|
let len = out_types.len();
|
|
out_types.swap(len - 1, len - 2);
|
|
}
|
|
// Arithmetic/logic produce NonClosure but we don't strictly need it; mark Unknown conservatively
|
|
// Calls and syscalls return Unknown
|
|
OpCode::Call => {
|
|
let func_id = instr.imm_u32().unwrap();
|
|
let callee = all_functions.get(func_id as usize).unwrap();
|
|
for _ in 0..callee.return_slots {
|
|
out_types.push(Unknown);
|
|
}
|
|
}
|
|
OpCode::Syscall => {
|
|
let id = instr.imm_u32().unwrap();
|
|
let syscall = Syscall::from_u32(id).unwrap();
|
|
for _ in 0..(syscall.results_count() as u16) {
|
|
out_types.push(Unknown);
|
|
}
|
|
}
|
|
OpCode::Intrinsic => {
|
|
let id = instr.imm_u32().unwrap();
|
|
let intrinsic = lookup_intrinsic_by_id(id).unwrap();
|
|
for _ in 0..(intrinsic.ret_slots() as u16) {
|
|
out_types.push(Unknown);
|
|
}
|
|
}
|
|
_ => {
|
|
// Default: push Unknown for any declared _pushes
|
|
if spec.pushes > 0 {
|
|
for _ in 0..spec.pushes {
|
|
out_types.push(Unknown);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let _pushes_final = dynamic_pushes.unwrap_or(match instr.opcode {
|
|
OpCode::MakeClosure => 1,
|
|
OpCode::CallClosure => {
|
|
// If we reached here, we handled it above and set dynamic_pushes
|
|
0
|
|
}
|
|
OpCode::Syscall => spec.pushes, // already added Unknowns above
|
|
OpCode::Intrinsic => spec.pushes, // already added Unknowns above
|
|
OpCode::Call => spec.pushes, // already added Unknowns above
|
|
_ => spec.pushes,
|
|
});
|
|
|
|
let out_height = (out_types.len()) as u16;
|
|
max_stack = max_stack.max(out_height);
|
|
|
|
// Enforce declared max stack if provided (0 means "no explicit limit")
|
|
if func.max_stack_slots != 0 && out_height > func.max_stack_slots {
|
|
return Err(VerifierError::StackOverflow {
|
|
pc: func_start + pc,
|
|
height: out_height,
|
|
limit: func.max_stack_slots,
|
|
});
|
|
}
|
|
|
|
if instr.opcode == OpCode::Ret && in_height != func.return_slots {
|
|
return Err(VerifierError::BadRetStackHeight {
|
|
pc: func_start + pc,
|
|
height: in_height,
|
|
expected: func.return_slots,
|
|
});
|
|
}
|
|
|
|
// Propagate to successors
|
|
if spec.is_branch {
|
|
// Canonical contract: branch immediate is RELATIVE to function start.
|
|
let target_rel = instr.imm_u32().unwrap() as usize;
|
|
let func_end_abs =
|
|
layouts.get(func_idx).map(|l| l.end).unwrap_or_else(|| code.len());
|
|
let func_len = func_end_abs - func_start;
|
|
|
|
if target_rel > func_len {
|
|
// Mandatory structured diagnostic for InvalidJumpTarget
|
|
let pc_abs = func_start + pc;
|
|
let opcode = instr.opcode;
|
|
let imm_raw = instr.imm_u32().unwrap();
|
|
let func_start_abs = func_start;
|
|
let func_end_abs_log = func_end_abs;
|
|
let target_abs_expected = func_start_abs + target_rel;
|
|
let is_boundary_target_rel = valid_pc.contains(&target_rel);
|
|
eprintln!(
|
|
"[VERIFIER] invalid jump: pc_abs={} opcode={:?} imm_raw={} func=F{} start={} end={} len={} target_rel={} target_abs_expected={} boundary_rel={}",
|
|
pc_abs,
|
|
opcode,
|
|
imm_raw,
|
|
func_idx,
|
|
func_start_abs,
|
|
func_end_abs_log,
|
|
func_len,
|
|
target_rel,
|
|
target_abs_expected,
|
|
is_boundary_target_rel
|
|
);
|
|
return Err(VerifierError::InvalidJumpTarget {
|
|
pc: pc_abs,
|
|
target: target_abs_expected,
|
|
});
|
|
}
|
|
|
|
if target_rel == func_len {
|
|
// jump to the end of function
|
|
if out_height != func.return_slots {
|
|
return Err(VerifierError::BadRetStackHeight {
|
|
pc: func_start + pc,
|
|
height: out_height,
|
|
expected: func.return_slots,
|
|
});
|
|
}
|
|
} else {
|
|
if !valid_pc.contains(&target_rel) {
|
|
return Err(VerifierError::JumpToMidInstruction {
|
|
pc: func_start + pc,
|
|
target: func_start + target_rel,
|
|
});
|
|
}
|
|
|
|
if let Some(existing_types) = stack_types_in.get_mut(&target_rel) {
|
|
let existing_height = existing_types.len() as u16;
|
|
if existing_height != out_height {
|
|
return Err(VerifierError::StackMismatchJoin {
|
|
pc: func_start + pc,
|
|
target: func_start + target_rel,
|
|
height_in: out_height,
|
|
height_target: existing_height,
|
|
});
|
|
} else {
|
|
// Merge types pointwise; if any slot changes, re-enqueue
|
|
let mut changed = false;
|
|
for i in 0..existing_types.len() {
|
|
let joined = join_slot(existing_types[i], out_types[i]);
|
|
if joined != existing_types[i] {
|
|
existing_types[i] = joined;
|
|
changed = true;
|
|
}
|
|
}
|
|
if changed {
|
|
worklist.push_back(target_rel);
|
|
}
|
|
}
|
|
} else {
|
|
stack_types_in.insert(target_rel, out_types.clone());
|
|
worklist.push_back(target_rel);
|
|
}
|
|
}
|
|
}
|
|
|
|
if !spec.is_terminator {
|
|
let next_pc = instr.next_pc;
|
|
let func_len = layouts.get(func_idx).map(|l| l.end - l.start).unwrap_or_else(|| 0);
|
|
if next_pc < func_len {
|
|
if let Some(existing_types) = stack_types_in.get_mut(&next_pc) {
|
|
let existing_height = existing_types.len() as u16;
|
|
if existing_height != out_height {
|
|
return Err(VerifierError::StackMismatchJoin {
|
|
pc: func_start + pc,
|
|
target: func_start + next_pc,
|
|
height_in: out_height,
|
|
height_target: existing_height,
|
|
});
|
|
} else {
|
|
let mut changed = false;
|
|
for i in 0..existing_types.len() {
|
|
let joined = join_slot(existing_types[i], out_types[i]);
|
|
if joined != existing_types[i] {
|
|
existing_types[i] = joined;
|
|
changed = true;
|
|
}
|
|
}
|
|
if changed {
|
|
worklist.push_back(next_pc);
|
|
}
|
|
}
|
|
} else {
|
|
stack_types_in.insert(next_pc, out_types);
|
|
worklist.push_back(next_pc);
|
|
}
|
|
} else if next_pc == func_len {
|
|
// Reaching end of function by falling through (no terminator) is invalid.
|
|
return Err(VerifierError::UnterminatedPath {
|
|
func_idx,
|
|
at_pc: func_start + pc,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(max_stack)
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Moved integration tests: Verifier Golden + Closures
|
|
// These were previously in `crates/console/prometeu-vm/tests/` but importing
|
|
// `crate::verifier` from integration tests would force the verifier to be
|
|
// public. To keep the module private while preserving coverage, we embed them
|
|
// here as unit tests.
|
|
// -----------------------------------------------------------------------------
|
|
#[cfg(test)]
|
|
mod golden_ext {
|
|
use super::{Verifier, VerifierError};
|
|
use prometeu_bytecode::FunctionMeta;
|
|
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
|
|
|
|
fn enc_op(op: OpCode) -> [u8; 2] {
|
|
(op as u16).to_le_bytes()
|
|
}
|
|
|
|
fn func(meta: FunctionMeta) -> Vec<FunctionMeta> {
|
|
vec![meta]
|
|
}
|
|
|
|
// A minimal selection from the golden suite (full file migrated from
|
|
// integration tests). Keeping names to avoid confusion.
|
|
|
|
#[test]
|
|
fn golden_valid_empty_function() {
|
|
let code = vec![];
|
|
let functions = func(FunctionMeta { code_offset: 0, code_len: 0, ..Default::default() });
|
|
let res = Verifier::verify(&code, &functions).unwrap();
|
|
assert_eq!(res[0], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_valid_simple_arith_and_ret() {
|
|
let mut code = Vec::new();
|
|
code.extend_from_slice(&enc_op(OpCode::PushI32));
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.extend_from_slice(&enc_op(OpCode::PushI32));
|
|
code.extend_from_slice(&2u32.to_le_bytes());
|
|
code.extend_from_slice(&enc_op(OpCode::Add));
|
|
code.extend_from_slice(&enc_op(OpCode::Ret));
|
|
|
|
let functions = func(FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
});
|
|
let res = Verifier::verify(&code, &functions).unwrap();
|
|
assert!(res[0] >= 2);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_err_unknown_opcode() {
|
|
let code = vec![0xFF, 0xFF];
|
|
let functions = func(FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() });
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::UnknownOpcode { pc: 0, opcode: 0xFFFF }));
|
|
}
|
|
|
|
#[test]
|
|
fn golden_err_truncated_immediate() {
|
|
let mut code = Vec::new();
|
|
code.extend_from_slice(&enc_op(OpCode::PushI32));
|
|
code.push(0xAA);
|
|
let functions = func(FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
..Default::default()
|
|
});
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(
|
|
res,
|
|
Err(VerifierError::TruncatedImmediate {
|
|
pc: 0,
|
|
opcode: OpCode::PushI32,
|
|
need: 4,
|
|
have: 1
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_err_invalid_jump_target() {
|
|
let mut code = Vec::new();
|
|
code.extend_from_slice(&enc_op(OpCode::Jmp));
|
|
code.extend_from_slice(&100u32.to_le_bytes());
|
|
let functions = func(FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
..Default::default()
|
|
});
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 }));
|
|
}
|
|
|
|
// --- Closures subset ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn closure_call_valid_passes() {
|
|
let mut code = Vec::new();
|
|
// F0 @ 0
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&7u32.to_le_bytes());
|
|
code.push(OpCode::MakeClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes()); // fn id
|
|
code.extend_from_slice(&0u32.to_le_bytes()); // cap count
|
|
code.push(OpCode::CallClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes()); // argc = 1 (excludes hidden)
|
|
code.push(OpCode::PopN as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let f0_len = code.len() as u32;
|
|
|
|
// F1 @ f0_len
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
let f1_len = (code.len() as u32) - f0_len;
|
|
|
|
let functions = vec![
|
|
FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: f0_len,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
},
|
|
FunctionMeta {
|
|
code_offset: f0_len,
|
|
code_len: f1_len,
|
|
param_slots: 2,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
},
|
|
];
|
|
|
|
let res = Verifier::verify(&code, &functions).unwrap();
|
|
assert!(res[0] >= 2);
|
|
}
|
|
|
|
#[test]
|
|
fn call_closure_on_non_closure_fails() {
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&7u32.to_le_bytes());
|
|
code.push(OpCode::CallClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&0u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
}];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert!(matches!(res, Err(VerifierError::NotAClosureOnCallClosure { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn closure_call_wrong_argc_fails() {
|
|
// Same as valid case but argc = 0 while callee expects 1 user arg
|
|
let mut code = Vec::new();
|
|
// F0 @ 0
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&7u32.to_le_bytes());
|
|
code.push(OpCode::MakeClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes()); // fn id
|
|
code.extend_from_slice(&0u32.to_le_bytes()); // cap count
|
|
code.push(OpCode::CallClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&0u32.to_le_bytes()); // argc = 0 (mismatch)
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let f0_len = code.len() as u32;
|
|
|
|
// F1 @ f0_len
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
let f1_len = (code.len() as u32) - f0_len;
|
|
|
|
let functions = vec![
|
|
FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: f0_len,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
},
|
|
FunctionMeta {
|
|
code_offset: f0_len,
|
|
code_len: f1_len,
|
|
param_slots: 2,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
},
|
|
];
|
|
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert!(matches!(res, Err(VerifierError::BadClosureArgCount { .. })));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_closure_calls_verify() {
|
|
// F0: MakeClosure(fn=1,0); CallClosure argc=0; PopN 1; Ret
|
|
// F1: MakeClosure(fn=2,0); CallClosure argc=0; Ret (param=1 hidden, ret=1)
|
|
// F2: PushI32 5; Ret (param=1 hidden, ret=1)
|
|
let mut code = Vec::new();
|
|
// F0 @ 0
|
|
code.push(OpCode::MakeClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes()); // F1
|
|
code.extend_from_slice(&0u32.to_le_bytes()); // cap=0
|
|
code.push(OpCode::CallClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&0u32.to_le_bytes()); // argc=0
|
|
code.push(OpCode::PopN as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
let f0_len = code.len() as u32;
|
|
|
|
// F1 @ f0_len
|
|
code.push(OpCode::MakeClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&2u32.to_le_bytes()); // F2
|
|
code.extend_from_slice(&0u32.to_le_bytes()); // cap=0
|
|
code.push(OpCode::CallClosure as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&0u32.to_le_bytes()); // argc=0
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
let f1_len = (code.len() as u32) - f0_len;
|
|
|
|
// F2 @ f0_len + f1_len
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&5u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
let f2_len = (code.len() as u32) - f0_len - f1_len;
|
|
|
|
let functions = vec![
|
|
FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: f0_len,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
},
|
|
FunctionMeta {
|
|
code_offset: f0_len,
|
|
code_len: f1_len,
|
|
param_slots: 1,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
},
|
|
FunctionMeta {
|
|
code_offset: f0_len + f1_len,
|
|
code_len: f2_len,
|
|
param_slots: 1,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
},
|
|
];
|
|
|
|
let res = Verifier::verify(&code, &functions).unwrap();
|
|
assert!(res[0] >= 1);
|
|
}
|
|
|
|
// --- Coroutines subset ----------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn err_invalid_yield_with_non_empty_stack() {
|
|
// Program:
|
|
// 0..2: PUSH_I32 1 (imm 4 bytes)
|
|
// 6..8: YIELD
|
|
// 8..10: RET
|
|
let mut code = Vec::new();
|
|
code.extend_from_slice(&enc_op(OpCode::PushI32));
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.extend_from_slice(&enc_op(OpCode::Yield));
|
|
code.extend_from_slice(&enc_op(OpCode::Ret));
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
}];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::InvalidYieldContext { pc: 6, height: 1 }));
|
|
}
|
|
|
|
#[test]
|
|
fn err_spawn_arg_mismatch() {
|
|
// Caller at func 0: SPAWN fn_id=1, arg_count=1; RET
|
|
// Callee at func 1: expects 2 param_slots
|
|
let mut code = Vec::new();
|
|
code.extend_from_slice(&enc_op(OpCode::Spawn));
|
|
code.extend_from_slice(&1u32.to_le_bytes()); // fn_id
|
|
code.extend_from_slice(&1u32.to_le_bytes()); // arg_count (mismatch: callee expects 2)
|
|
code.extend_from_slice(&enc_op(OpCode::Ret));
|
|
|
|
let caller = FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
};
|
|
// Callee has no code here; only signature matters
|
|
let callee = FunctionMeta {
|
|
code_offset: code.len() as u32,
|
|
code_len: 0,
|
|
param_slots: 2,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
};
|
|
let functions = vec![caller, callee];
|
|
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::BadSpawnArgCount { pc: 0, expected: 2, got: 1 }));
|
|
}
|
|
|
|
#[test]
|
|
fn err_sleep_truncated_immediate() {
|
|
// Encode SLEEP but provide only 1 of 4 immediate bytes
|
|
let mut code = Vec::new();
|
|
code.extend_from_slice(&enc_op(OpCode::Sleep));
|
|
code.push(0xAB);
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
..Default::default()
|
|
}];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(
|
|
res,
|
|
Err(VerifierError::TruncatedImmediate {
|
|
pc: 0,
|
|
opcode: OpCode::Sleep,
|
|
need: 4,
|
|
have: 1
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_verifier_underflow() {
|
|
// OpCode::Add (2 bytes)
|
|
let code = vec![OpCode::Add as u8, 0x00];
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Add }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_dup_underflow() {
|
|
let code =
|
|
vec![(OpCode::Dup as u16).to_le_bytes()[0], (OpCode::Dup as u16).to_le_bytes()[1]];
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Dup }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_invalid_jmp_target() {
|
|
// Jmp (2 bytes) + 100u32 (4 bytes)
|
|
let mut code = vec![OpCode::Jmp as u8, 0x00];
|
|
code.extend_from_slice(&100u32.to_le_bytes());
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::InvalidJumpTarget { pc: 0, target: 100 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_jmp_to_mid_instr() {
|
|
// PushI32 (2 bytes) + 42u32 (4 bytes)
|
|
// Jmp 1 (middle of PushI32)
|
|
let mut code = vec![OpCode::PushI32 as u8, 0x00];
|
|
code.extend_from_slice(&42u32.to_le_bytes());
|
|
code.push(OpCode::Jmp as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 12, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_jump_to_end_ok() {
|
|
// Single-instruction function where JMP targets exactly func_len (end-exclusive)
|
|
// Encoding: [JMP][u32 imm], with imm == total function length (6 bytes)
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::Jmp as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&6u32.to_le_bytes());
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: 6,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
}];
|
|
|
|
let res = Verifier::verify(&code, &functions).unwrap();
|
|
// No stack usage; max stack remains 0
|
|
assert_eq!(res[0], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_truncation_opcode() {
|
|
let code = vec![OpCode::PushI32 as u8]; // Truncated u16 opcode
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 1, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 0 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_truncation_immediate() {
|
|
let mut code = vec![OpCode::PushI32 as u8, 0x00];
|
|
code.push(0x42); // Only 1 byte of 4-byte immediate
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 3, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(
|
|
res,
|
|
Err(VerifierError::TruncatedImmediate {
|
|
pc: 0,
|
|
opcode: OpCode::PushI32,
|
|
need: 4,
|
|
have: 1
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_stack_mismatch_join() {
|
|
// Let's make it reachable:
|
|
// 0: PushBool true
|
|
// 3: JmpIfTrue 15
|
|
// 9: Jmp 27
|
|
// 15: PushI32 1
|
|
// 21: Jmp 27
|
|
// 27: Nop
|
|
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::PushBool as u8);
|
|
code.push(0x00);
|
|
code.push(1); // 0: PushBool (3 bytes)
|
|
code.push(OpCode::JmpIfTrue as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&15u32.to_le_bytes()); // 3: JmpIfTrue (6 bytes)
|
|
code.push(OpCode::Jmp as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&27u32.to_le_bytes()); // 9: Jmp (6 bytes)
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes()); // 15: PushI32 (6 bytes)
|
|
code.push(OpCode::Jmp as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&27u32.to_le_bytes()); // 21: Jmp (6 bytes)
|
|
code.push(OpCode::Nop as u8);
|
|
code.push(0x00); // 27: Nop (2 bytes)
|
|
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 29, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
// Path 0->3->9->27: height 1-1+0 = 0.
|
|
// Path 0->3->15->21->27: height 1-1+1 = 1.
|
|
// Mismatch at 27: 0 vs 1.
|
|
|
|
assert_eq!(
|
|
res,
|
|
Err(VerifierError::StackMismatchJoin {
|
|
pc: 21,
|
|
target: 27,
|
|
height_in: 1,
|
|
height_target: 0
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_bad_ret_height() {
|
|
// PushI32 1 (6 bytes)
|
|
// Ret (2 bytes)
|
|
let mut code = vec![OpCode::PushI32 as u8, 0x00];
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: 8,
|
|
return_slots: 0, // Expected 0, but got 1
|
|
..Default::default()
|
|
}];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::BadRetStackHeight { pc: 6, height: 1, expected: 0 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_max_stack() {
|
|
// PushI32 1
|
|
// PushI32 2
|
|
// Add
|
|
// Ret
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&2u32.to_le_bytes());
|
|
code.push(OpCode::Add as u8);
|
|
code.push(0x00);
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: 16,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
}];
|
|
let res = Verifier::verify(&code, &functions).unwrap();
|
|
assert_eq!(res[0], 2);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_stack_overflow_limit() {
|
|
// Same program as max_stack, but declare max_stack_slots = 1 → should overflow at second push
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&2u32.to_le_bytes());
|
|
code.push(OpCode::Add as u8);
|
|
code.push(0x00);
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: 16,
|
|
return_slots: 1,
|
|
max_stack_slots: 1,
|
|
..Default::default()
|
|
}];
|
|
|
|
let res = Verifier::verify(&code, &functions);
|
|
// Overflow occurs at pc = 6 (start of second PushI32)
|
|
assert_eq!(res, Err(VerifierError::StackOverflow { pc: 6, height: 2, limit: 1 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_invalid_syscall_id() {
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::Syscall as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes()); // Unknown ID
|
|
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_invalid_intrinsic_id() {
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::Intrinsic as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&0xDEADBEEFu32.to_le_bytes());
|
|
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::InvalidIntrinsicId { pc: 0, id: 0xDEADBEEF }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_accepts_intrinsic_stack_effects() {
|
|
let mut code = Vec::new();
|
|
for value in [1i32, 2, 3, 4] {
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&value.to_le_bytes());
|
|
}
|
|
code.push(OpCode::Intrinsic as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&0x1000u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
}];
|
|
let res = Verifier::verify(&code, &functions).expect("intrinsic program must verify");
|
|
assert!(res[0] >= 4);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_rejects_intrinsic_stack_underflow() {
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1i32.to_le_bytes());
|
|
code.push(OpCode::Intrinsic as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&0x1000u32.to_le_bytes());
|
|
code.push(OpCode::Halt as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: code.len() as u32,
|
|
..Default::default()
|
|
}];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 6, opcode: OpCode::Intrinsic }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_rejects_unpatched_hostcall() {
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::Hostcall as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&3u32.to_le_bytes());
|
|
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::HostcallNotPatched { pc: 0, sysc_index: 3 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_function_without_terminator_is_rejected() {
|
|
// Single NOP with no RET/JMP/TRAP/HALT at the end → fallthrough to end
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::Nop as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta { code_offset: 0, code_len: 2, ..Default::default() }];
|
|
let res = Verifier::verify(&code, &functions);
|
|
assert_eq!(res, Err(VerifierError::UnterminatedPath { func_idx: 0, at_pc: 0 }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_function_with_proper_terminator_passes() {
|
|
// Minimal function that returns immediately
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: 2,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
}];
|
|
let res = Verifier::verify(&code, &functions).unwrap();
|
|
assert_eq!(res[0], 0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_ret_too_few_slots() {
|
|
// Function declares 1 return slot but returns nothing
|
|
// 0: Ret
|
|
let mut code = Vec::new();
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: 2,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
}];
|
|
|
|
let res = Verifier::verify(&code, &functions);
|
|
// With too few return slots at RET, the verifier raises StackUnderflow on RET
|
|
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 0, opcode: OpCode::Ret }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_call_return_mismatch_underflow_in_caller() {
|
|
// Two functions:
|
|
// F0 (idx 0): Call F1; PopN 1; Ret (expects 0 total at return)
|
|
// F1 (idx 1): Ret (returns 0)
|
|
// Caller tries to pop 1 result that callee did not return → underflow at PopN
|
|
|
|
let mut code = Vec::new();
|
|
|
|
// F0 @ 0
|
|
// Call 1
|
|
code.push(OpCode::Call as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
// PopN 1
|
|
code.push(OpCode::PopN as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
// Ret
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let f0_len = code.len() as u32; // 14
|
|
|
|
// F1 @ f0_len
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let functions = vec![
|
|
FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: f0_len,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
},
|
|
FunctionMeta {
|
|
code_offset: f0_len,
|
|
code_len: 2,
|
|
return_slots: 0,
|
|
..Default::default()
|
|
},
|
|
];
|
|
|
|
let res = Verifier::verify(&code, &functions);
|
|
// Underflow at PopN (pc = 6)
|
|
assert_eq!(res, Err(VerifierError::StackUnderflow { pc: 6, opcode: OpCode::PopN }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_verifier_call_return_mismatch_bad_ret_in_caller() {
|
|
// Two functions:
|
|
// F0 (idx 0): Call F1; Ret (declares return_slots = 1)
|
|
// F1 (idx 1): PushI32 1; PushI32 2; Ret (returns 2)
|
|
// Caller ends with 2 values but declares 1 → BadRetStackHeight at caller's Ret
|
|
|
|
let mut code = Vec::new();
|
|
|
|
// F0 @ 0
|
|
// Call 1
|
|
code.push(OpCode::Call as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
// Ret
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let f0_len = code.len() as u32; // 8
|
|
|
|
// F1 @ f0_len
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&1u32.to_le_bytes());
|
|
code.push(OpCode::PushI32 as u8);
|
|
code.push(0x00);
|
|
code.extend_from_slice(&2u32.to_le_bytes());
|
|
code.push(OpCode::Ret as u8);
|
|
code.push(0x00);
|
|
|
|
let f1_len = (code.len() as u32) - f0_len;
|
|
|
|
let functions = vec![
|
|
FunctionMeta {
|
|
code_offset: 0,
|
|
code_len: f0_len,
|
|
return_slots: 1,
|
|
..Default::default()
|
|
},
|
|
FunctionMeta {
|
|
code_offset: f0_len,
|
|
code_len: f1_len,
|
|
return_slots: 2,
|
|
..Default::default()
|
|
},
|
|
];
|
|
|
|
let res = Verifier::verify(&code, &functions);
|
|
// Error at caller's RET (pc = 6)
|
|
assert_eq!(res, Err(VerifierError::BadRetStackHeight { pc: 6, height: 2, expected: 1 }));
|
|
}
|
|
}
|