pr4.7
This commit is contained in:
parent
a7a40bd9c1
commit
b9723cfc40
241
crates/console/prometeu-vm/tests/verifier_golden.rs
Normal file
241
crates/console/prometeu-vm/tests/verifier_golden.rs
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
//! Verifier Golden Test Suite
|
||||||
|
//!
|
||||||
|
//! This suite exercises a stable set of valid and invalid bytecode samples
|
||||||
|
//! and asserts either successful verification or specific `VerifierError`
|
||||||
|
//! kinds. All tests are deterministic and self-contained.
|
||||||
|
|
||||||
|
use prometeu_bytecode::FunctionMeta;
|
||||||
|
use prometeu_bytecode::isa::core::CoreOpCode as OpCode;
|
||||||
|
use prometeu_vm::verifier::{Verifier, VerifierError};
|
||||||
|
|
||||||
|
// --- Small helpers ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn enc_op(op: OpCode) -> [u8; 2] {
|
||||||
|
// Opcodes are encoded as u16 LE; CoreOpCode fits in u8 so we write [lo, hi=0x00]
|
||||||
|
[(op as u8), 0x00]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn func(meta: FunctionMeta) -> Vec<FunctionMeta> {
|
||||||
|
vec![meta]
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Valid Programs --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_valid_empty_function() {
|
||||||
|
// Empty function is allowed by the verifier.
|
||||||
|
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() {
|
||||||
|
// push 1; push 2; add; 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_eq!(res[0], 2); // max stack reaches 2 after two pushes
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_valid_jump_to_end() {
|
||||||
|
// Single-instr function that jumps to exactly end (allowed terminator path)
|
||||||
|
let mut code = Vec::new();
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::Jmp));
|
||||||
|
code.extend_from_slice(&6u32.to_le_bytes());
|
||||||
|
|
||||||
|
let functions = func(FunctionMeta { code_offset: 0, code_len: 6, ..Default::default() });
|
||||||
|
let res = Verifier::verify(&code, &functions).unwrap();
|
||||||
|
assert_eq!(res[0], 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Invalid Programs (one per error category) -----------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_unknown_opcode() {
|
||||||
|
// Write u16 opcode that does not map to any CoreOpCode
|
||||||
|
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_opcode() {
|
||||||
|
let code = vec![OpCode::PushI32 as u8]; // only 1 byte of a u16 opcode
|
||||||
|
let functions = func(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 golden_err_truncated_immediate() {
|
||||||
|
// PushI32 requires 4-byte immediate, provide only 1
|
||||||
|
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 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_jump_to_mid_instruction() {
|
||||||
|
// [PushI32 4 bytes] then jmp to offset 1 (middle of first instruction)
|
||||||
|
let mut code = Vec::new();
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::PushI32));
|
||||||
|
code.extend_from_slice(&42u32.to_le_bytes()); // at pc 0..6
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::Jmp)); // at pc 6..8
|
||||||
|
code.extend_from_slice(&1u32.to_le_bytes()); // target 1
|
||||||
|
|
||||||
|
let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, ..Default::default() });
|
||||||
|
let res = Verifier::verify(&code, &functions);
|
||||||
|
// The verifier reports error at the jmp site pc (func_start + pc==6) with target=1
|
||||||
|
assert_eq!(res, Err(VerifierError::JumpToMidInstruction { pc: 6, target: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_stack_underflow() {
|
||||||
|
// Add requires 2 inputs; stack is empty
|
||||||
|
let code = vec![OpCode::Add as u8, 0x00];
|
||||||
|
let functions = func(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 golden_err_stack_overflow() {
|
||||||
|
// Two pushes would raise max stack to 2, but limit is set to 1
|
||||||
|
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::Ret));
|
||||||
|
|
||||||
|
let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 1, max_stack_slots: 1, ..Default::default() });
|
||||||
|
let res = Verifier::verify(&code, &functions);
|
||||||
|
// Overflow happens when second push increases height from 1 -> 2 over limit=1; pc is start of that push
|
||||||
|
// First push consumes 6 bytes; second push opcode begins at pc=6
|
||||||
|
assert_eq!(res, Err(VerifierError::StackOverflow { pc: 6, height: 2, limit: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_stack_mismatch_join() {
|
||||||
|
// Branch that merges with different heights at join point
|
||||||
|
// 0: PushBool true (3)
|
||||||
|
// 3: JmpIfTrue 15 (6)
|
||||||
|
// 9: Jmp 27 (6)
|
||||||
|
// 15: PushI32 1 (6)
|
||||||
|
// 21: Jmp 27 (6)
|
||||||
|
// 27: Nop (2)
|
||||||
|
let mut code = Vec::new();
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::PushBool));
|
||||||
|
code.push(1);
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::JmpIfTrue));
|
||||||
|
code.extend_from_slice(&15u32.to_le_bytes());
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::Jmp));
|
||||||
|
code.extend_from_slice(&27u32.to_le_bytes());
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::PushI32));
|
||||||
|
code.extend_from_slice(&1u32.to_le_bytes());
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::Jmp));
|
||||||
|
code.extend_from_slice(&27u32.to_le_bytes());
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::Nop));
|
||||||
|
|
||||||
|
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::StackMismatchJoin { pc: 21, target: 27, height_in: 1, height_target: 0 })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_bad_ret_stack_height() {
|
||||||
|
// Push one value but function declares 0 returns
|
||||||
|
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::Ret));
|
||||||
|
|
||||||
|
let functions = func(FunctionMeta { code_offset: 0, code_len: code.len() as u32, return_slots: 0, ..Default::default() });
|
||||||
|
let res = Verifier::verify(&code, &functions);
|
||||||
|
assert_eq!(res, Err(VerifierError::BadRetStackHeight { pc: 6, height: 1, expected: 0 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_function_out_of_bounds() {
|
||||||
|
// Function start is beyond the available code length
|
||||||
|
let code = vec![enc_op(OpCode::Nop)[0], enc_op(OpCode::Nop)[1]]; // len = 2
|
||||||
|
let functions = func(FunctionMeta { code_offset: 10, code_len: 0, ..Default::default() });
|
||||||
|
let res = Verifier::verify(&code, &functions);
|
||||||
|
assert!(matches!(res, Err(VerifierError::FunctionOutOfBounds { func_idx: 0, start: 10, end, code_len: 2 })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_trailing_bytes() {
|
||||||
|
// With current decoder loop, a stray byte at the end is reported as TruncatedOpcode
|
||||||
|
let mut code = Vec::new();
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::Nop)); // 2 bytes
|
||||||
|
code.push(0xCC); // stray byte
|
||||||
|
let functions = func(FunctionMeta { code_offset: 0, code_len: 3, ..Default::default() });
|
||||||
|
let res = Verifier::verify(&code, &functions);
|
||||||
|
assert_eq!(res, Err(VerifierError::TruncatedOpcode { pc: 2 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_unterminated_path() {
|
||||||
|
// Code that falls off end without terminator (no ret/jmp to end/trap/halt)
|
||||||
|
let code = vec![enc_op(OpCode::Nop)[0], enc_op(OpCode::Nop)[1]]; // single NOP, then fallthrough
|
||||||
|
let functions = func(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 golden_err_invalid_syscall_id() {
|
||||||
|
// Syscall expects valid `prometeu_hal::syscalls::Syscall` id; use an unknown value
|
||||||
|
let mut code = Vec::new();
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::Syscall));
|
||||||
|
code.extend_from_slice(&0xDEADBEEFu32.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::InvalidSyscallId { pc: 0, id: 0xDEADBEEF }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn golden_err_invalid_func_id() {
|
||||||
|
// Encode a Call with invalid function id (beyond functions.len())
|
||||||
|
// We use two functions meta entries; first is caller, second is callee placeholder.
|
||||||
|
// The call uses id=5 which is out-of-range → InvalidFuncId.
|
||||||
|
let mut code = Vec::new();
|
||||||
|
code.extend_from_slice(&enc_op(OpCode::Call));
|
||||||
|
code.extend_from_slice(&5u32.to_le_bytes());
|
||||||
|
|
||||||
|
// Caller function only. No actual callee at index 5.
|
||||||
|
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::InvalidFuncId { pc: 0, id: 5 }));
|
||||||
|
}
|
||||||
@ -1,51 +1,3 @@
|
|||||||
# PR-4.7 — Verifier Golden Test Suite
|
|
||||||
|
|
||||||
### Briefing
|
|
||||||
|
|
||||||
We need a stable suite of valid and invalid bytecode samples to ensure verifier correctness over time.
|
|
||||||
|
|
||||||
### Target
|
|
||||||
|
|
||||||
* Introduce golden tests for the verifier.
|
|
||||||
|
|
||||||
### Work items
|
|
||||||
|
|
||||||
* Add a set of small bytecode samples:
|
|
||||||
|
|
||||||
* Valid programs.
|
|
||||||
* Invalid programs for each verifier error.
|
|
||||||
* Implement golden tests asserting:
|
|
||||||
|
|
||||||
* Successful verification.
|
|
||||||
* Specific error kinds for invalid programs.
|
|
||||||
|
|
||||||
### Acceptance checklist
|
|
||||||
|
|
||||||
* [ ] Golden tests exist for all verifier error categories.
|
|
||||||
* [ ] Tests are deterministic.
|
|
||||||
* [ ] `cargo test` passes.
|
|
||||||
|
|
||||||
### Tests
|
|
||||||
|
|
||||||
* New golden tests only.
|
|
||||||
|
|
||||||
### Junie instructions
|
|
||||||
|
|
||||||
**You MAY:**
|
|
||||||
|
|
||||||
* Add deterministic golden tests.
|
|
||||||
|
|
||||||
**You MUST NOT:**
|
|
||||||
|
|
||||||
* Modify verifier logic to fit tests without understanding the cause.
|
|
||||||
* Introduce random or timing-based tests.
|
|
||||||
|
|
||||||
**If unclear:**
|
|
||||||
|
|
||||||
* Ask before changing test expectations.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# PR-5.1 — Define Canonical Syscall Metadata Table
|
# PR-5.1 — Define Canonical Syscall Metadata Table
|
||||||
|
|
||||||
### Briefing
|
### Briefing
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user