Skip to main content

byteflow/bytecode/
verify.rs

1use super::chunk::Chunk;
2use super::opcode::Opcode;
3use std::fmt;
4
5/// Why a [`Chunk`] failed verification.
6///
7/// The verifier's job is to make every fact the interpreter's hot loop
8/// relies on (jump targets in range, constant/function indices in range)
9/// true *before* a single instruction runs, so `byteflow-vm` never has to
10/// re-check them per-step. Skipping this on trusted, compiler-generated
11/// bytecode is fine; it is mandatory before loading anything that crossed a
12/// trust boundary (a plugin, a network-fetched module, see design notes
13/// §24 capability/sandboxing).
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum VerifyError {
16    UnknownOpcode { at: usize, byte: u8 },
17    ConstOutOfRange { at: usize, index: u32, len: usize },
18    FunctionOutOfRange { at: usize, index: u32, len: usize },
19    JumpOutOfRange { at: usize, target: i64, len: usize },
20    EmptyFunctionTable,
21    EntryOutOfRange { function: usize, entry: u32, len: usize },
22}
23
24impl fmt::Display for VerifyError {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            VerifyError::UnknownOpcode { at, byte } => {
28                write!(f, "unknown opcode 0x{byte:02X} at instruction {at}")
29            }
30            VerifyError::ConstOutOfRange { at, index, len } => write!(
31                f,
32                "instruction {at} references constant {index}, pool has {len} entries"
33            ),
34            VerifyError::FunctionOutOfRange { at, index, len } => write!(
35                f,
36                "instruction {at} references function {index}, table has {len} entries"
37            ),
38            VerifyError::JumpOutOfRange { at, target, len } => write!(
39                f,
40                "instruction {at} jumps to {target}, out of code bounds (len={len})"
41            ),
42            VerifyError::EmptyFunctionTable => write!(f, "chunk has no entry function"),
43            VerifyError::EntryOutOfRange { function, entry, len } => write!(
44                f,
45                "function {function} entry point {entry} is out of code bounds (len={len})"
46            ),
47        }
48    }
49}
50
51impl std::error::Error for VerifyError {}
52
53/// Verify structural invariants of `chunk`. See [`VerifyError`] for what is
54/// checked. This does **not** perform full dataflow/register-liveness
55/// verification (unlike, say, the JVM verifier) — v0 trades that off
56/// against implementation complexity, and instead the VM bounds-checks
57/// register indices at runtime (cheap: it's an array index against a fixed
58/// small register file, not worth statically proving away yet).
59///
60/// `Opcode::CallNative` targets are deliberately **not** range-checked
61/// here: native functions live in a `byteflow_vm::NativeTable` supplied by
62/// the embedder at `Vm` construction time, entirely outside this crate's
63/// (and this `Chunk`'s) knowledge. An out-of-range `CallNative` is instead
64/// caught at runtime as `Fault::BadNative`.
65pub fn verify(chunk: &Chunk) -> Result<(), VerifyError> {
66    if chunk.functions.is_empty() {
67        return Err(VerifyError::EmptyFunctionTable);
68    }
69
70    let len = chunk.code.len();
71
72    for def in &chunk.functions {
73        if def.entry as usize >= len {
74            return Err(VerifyError::EntryOutOfRange {
75                function: chunk.functions.iter().position(|f| f.name == def.name).unwrap_or(0),
76                entry: def.entry,
77                len,
78            });
79        }
80    }
81
82    for (at, instr) in chunk.code.iter().enumerate() {
83        match instr.op {
84            Opcode::LoadConst => {
85                let idx = instr.imm as u32;
86                if idx as usize >= chunk.constants.len() {
87                    return Err(VerifyError::ConstOutOfRange {
88                        at,
89                        index: idx,
90                        len: chunk.constants.len(),
91                    });
92                }
93            }
94            Opcode::Spawn | Opcode::Call => {
95                let idx = instr.imm as u32;
96                if idx as usize >= chunk.functions.len() {
97                    return Err(VerifyError::FunctionOutOfRange {
98                        at,
99                        index: idx,
100                        len: chunk.functions.len(),
101                    });
102                }
103            }
104            // `CallNative` targets a `byteflow_vm::NativeTable` supplied at
105            // runtime, outside this chunk — see this function's doc
106            // comment. Not checked here; checked as `Fault::BadNative` at
107            // call time instead.
108            Opcode::CallNative => {}
109            Opcode::Jump | Opcode::Branch => {
110                let target = at as i64 + 1 + instr.imm as i64;
111                if target < 0 || target as usize > len {
112                    // == len is allowed: jumping to "one past the end" is a
113                    // valid way to fall off the end of a function body.
114                    return Err(VerifyError::JumpOutOfRange { at, target, len });
115                }
116            }
117            _ => {}
118        }
119    }
120
121    Ok(())
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use crate::bytecode::builder::ChunkBuilder;
128    use crate::bytecode::opcode::Opcode;
129
130
131    #[test]
132    fn rejects_empty_function_table() {
133        let chunk = Chunk::default();
134        assert_eq!(verify(&chunk), Err(VerifyError::EmptyFunctionTable));
135    }
136
137    #[test]
138    fn accepts_well_formed_chunk() {
139        let mut b = ChunkBuilder::new("test");
140        b.begin_function("main", 0, 2);
141        let k = b.const_(crate::bytecode::value::Value::Int(41));
142        b.emit_load_const(0, k);
143        b.emit_load_imm(1, 1);
144        b.emit_binop(Opcode::Add, 0, 0, 1);
145        b.emit_return(0);
146        let chunk = b.finish();
147        assert!(verify(&chunk).is_ok());
148    }
149
150    #[test]
151    fn rejects_out_of_range_jump() {
152        use crate::bytecode::instruction::Instruction;
153        let mut chunk = Chunk::default();
154        chunk.functions.push(crate::bytecode::chunk::FunctionDef {
155            name: "main".into(),
156            entry: 0,
157            arity: 0,
158            num_registers: 1,
159        });
160        chunk.code.push(Instruction::only_imm(Opcode::Jump, 999));
161        assert!(matches!(verify(&chunk), Err(VerifyError::JumpOutOfRange { .. })));
162    }
163}