1use super::chunk::Chunk;
2use super::opcode::Opcode;
3use super::value::Value;
4use std::fmt;
5
6#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum TrustLevel {
13 Trusted,
14 Untrusted,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct VerifyConfig {
20 pub trust: TrustLevel,
21}
22
23impl Default for VerifyConfig {
24 fn default() -> Self {
25 Self {
26 trust: TrustLevel::Untrusted,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum ConstantKind {
34 Capability,
35 ProcessId,
36 Message,
37}
38
39impl fmt::Display for ConstantKind {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 ConstantKind::Capability => f.write_str("capability"),
43 ConstantKind::ProcessId => f.write_str("pid"),
44 ConstantKind::Message => f.write_str("message"),
45 }
46 }
47}
48
49fn validate_constant(value: &Value, trust: TrustLevel, index: usize) -> Result<(), VerifyError> {
50 if trust == TrustLevel::Trusted {
51 return Ok(());
52 }
53 match value {
54 Value::Cap(_) => Err(VerifyError::ForbiddenConstant {
55 index,
56 kind: ConstantKind::Capability,
57 }),
58 Value::Pid(_) => Err(VerifyError::ForbiddenConstant {
59 index,
60 kind: ConstantKind::ProcessId,
61 }),
62 Value::Message(_) => Err(VerifyError::ForbiddenConstant {
63 index,
64 kind: ConstantKind::Message,
65 }),
66 _ => Ok(()),
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum VerifyError {
81 UnknownOpcode { at: usize, byte: u8 },
82 ConstOutOfRange { at: usize, index: u32, len: usize },
83 FunctionOutOfRange { at: usize, index: u32, len: usize },
84 JumpOutOfRange { at: usize, target: i64, len: usize },
85 EmptyFunctionTable,
86 EntryOutOfRange { function: usize, entry: u32, len: usize },
87 ArityExceedsRegisters {
94 function: usize,
95 arity: u8,
96 num_registers: u8,
97 },
98 ForbiddenConstant { index: usize, kind: ConstantKind },
100}
101
102impl fmt::Display for VerifyError {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 match self {
105 VerifyError::UnknownOpcode { at, byte } => {
106 write!(f, "unknown opcode 0x{byte:02X} at instruction {at}")
107 }
108 VerifyError::ConstOutOfRange { at, index, len } => write!(
109 f,
110 "instruction {at} references constant {index}, pool has {len} entries"
111 ),
112 VerifyError::FunctionOutOfRange { at, index, len } => write!(
113 f,
114 "instruction {at} references function {index}, table has {len} entries"
115 ),
116 VerifyError::JumpOutOfRange { at, target, len } => write!(
117 f,
118 "instruction {at} jumps to {target}, out of code bounds (len={len})"
119 ),
120 VerifyError::EmptyFunctionTable => write!(f, "chunk has no entry function"),
121 VerifyError::EntryOutOfRange { function, entry, len } => write!(
122 f,
123 "function {function} entry point {entry} is out of code bounds (len={len})"
124 ),
125 VerifyError::ArityExceedsRegisters { function, arity, num_registers } => write!(
126 f,
127 "function {function} declares arity {arity} but only {num_registers} registers"
128 ),
129 VerifyError::ForbiddenConstant { index, kind } => write!(
130 f,
131 "untrusted constant[{index}] must not embed {kind}"
132 ),
133 }
134 }
135}
136
137impl std::error::Error for VerifyError {}
138
139pub fn verify(chunk: &Chunk) -> Result<(), VerifyError> {
158 verify_with(chunk, VerifyConfig::default())
159}
160
161pub fn verify_with(chunk: &Chunk, config: VerifyConfig) -> Result<(), VerifyError> {
163 if chunk.functions.is_empty() {
164 return Err(VerifyError::EmptyFunctionTable);
165 }
166
167 for (index, value) in chunk.constants.iter().enumerate() {
168 validate_constant(value, config.trust, index)?;
169 }
170
171 let len = chunk.code.len();
172
173 for (function, def) in chunk.functions.iter().enumerate() {
177 if def.entry as usize >= len {
178 return Err(VerifyError::EntryOutOfRange {
179 function,
180 entry: def.entry,
181 len,
182 });
183 }
184 if def.arity > def.num_registers {
185 return Err(VerifyError::ArityExceedsRegisters {
186 function,
187 arity: def.arity,
188 num_registers: def.num_registers,
189 });
190 }
191 }
192
193 for (at, instr) in chunk.code.iter().enumerate() {
194 match instr.op {
195 Opcode::LoadConst => {
196 let idx = instr.imm as u32;
197 if idx as usize >= chunk.constants.len() {
198 return Err(VerifyError::ConstOutOfRange {
199 at,
200 index: idx,
201 len: chunk.constants.len(),
202 });
203 }
204 }
205 Opcode::Spawn | Opcode::Call => {
206 let idx = instr.imm as u32;
207 if idx as usize >= chunk.functions.len() {
208 return Err(VerifyError::FunctionOutOfRange {
209 at,
210 index: idx,
211 len: chunk.functions.len(),
212 });
213 }
214 }
215 Opcode::CallNative => {}
220 Opcode::Jump | Opcode::Branch => {
221 let target = at as i64 + 1 + instr.imm as i64;
222 if target < 0 || target as usize > len {
223 return Err(VerifyError::JumpOutOfRange { at, target, len });
226 }
227 }
228 _ => {}
229 }
230 }
231
232 Ok(())
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::bytecode::builder::ChunkBuilder;
239 use crate::bytecode::opcode::Opcode;
240
241
242 #[test]
243 fn rejects_empty_function_table() {
244 let chunk = Chunk::default();
245 assert_eq!(verify(&chunk), Err(VerifyError::EmptyFunctionTable));
246 }
247
248 #[test]
249 fn accepts_well_formed_chunk() {
250 let mut b = ChunkBuilder::new("test");
251 b.begin_function("main", 0, 2);
252 let k = b.const_(crate::bytecode::value::Value::Int(41));
253 b.emit_load_const(0, k);
254 b.emit_load_imm(1, 1);
255 b.emit_binop(Opcode::Add, 0, 0, 1);
256 b.emit_return(0);
257 let chunk = b.finish();
258 assert!(verify(&chunk).is_ok());
259 }
260
261 #[test]
265 fn rejects_arity_larger_than_the_register_file() {
266 let mut b = ChunkBuilder::new("test");
267 b.begin_function("main", 3, 1);
268 b.emit_return(0);
269 let chunk = b.finish();
270 assert_eq!(
271 verify(&chunk),
272 Err(VerifyError::ArityExceedsRegisters {
273 function: 0,
274 arity: 3,
275 num_registers: 1,
276 })
277 );
278 }
279
280 #[test]
281 fn accepts_arity_equal_to_the_register_file() {
282 let mut b = ChunkBuilder::new("test");
283 b.begin_function("main", 2, 2);
284 b.emit_return(0);
285 let chunk = b.finish();
286 assert!(verify(&chunk).is_ok());
287 }
288
289 #[test]
290 fn rejects_out_of_range_jump() {
291 use crate::bytecode::instruction::Instruction;
292 let mut chunk = Chunk::default();
293 chunk.functions.push(crate::bytecode::chunk::FunctionDef {
294 name: "main".into(),
295 entry: 0,
296 arity: 0,
297 num_registers: 1,
298 });
299 chunk.code.push(Instruction::only_imm(Opcode::Jump, 999));
300 assert!(matches!(verify(&chunk), Err(VerifyError::JumpOutOfRange { .. })));
301 }
302
303 #[test]
304 fn untrusted_rejects_cap_pid_message_constants() {
305 use crate::bytecode::cap::CapId;
306 use crate::bytecode::value::Message;
307 for (value, kind) in [
308 (crate::bytecode::value::Value::Cap(CapId::from_raw(1)), ConstantKind::Capability),
309 (crate::bytecode::value::Value::Pid(7), ConstantKind::ProcessId),
310 (
311 crate::bytecode::value::Value::Message(Message::new(1, 2, 3, 4u64)),
312 ConstantKind::Message,
313 ),
314 ] {
315 let mut b = ChunkBuilder::new("forge");
316 b.begin_function("main", 0, 1);
317 let k = b.const_(value);
318 b.emit_load_const(0, k);
319 b.emit_return(0);
320 let chunk = b.finish();
321 assert_eq!(
322 verify(&chunk),
323 Err(VerifyError::ForbiddenConstant { index: 0, kind })
324 );
325 assert!(verify_with(
326 &chunk,
327 VerifyConfig {
328 trust: TrustLevel::Trusted
329 }
330 )
331 .is_ok());
332 }
333 }
334}