1use super::{BytecodeProgram, OpCode, Operand};
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum VerifyError {
48 TrustedOpcodeBadOperand {
51 function_name: String,
52 opcode: OpCode,
53 instruction_offset: usize,
54 },
55 V2MissingFrameDescriptor {
57 function_name: String,
58 opcode: OpCode,
59 instruction_offset: usize,
60 },
61 V2FieldOffsetTooLarge {
63 function_name: String,
64 opcode: OpCode,
65 instruction_offset: usize,
66 offset: u16,
67 },
68 V2MissingFieldOffset {
70 function_name: String,
71 opcode: OpCode,
72 instruction_offset: usize,
73 },
74}
75
76impl std::fmt::Display for VerifyError {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 match self {
79 VerifyError::TrustedOpcodeBadOperand {
80 function_name,
81 opcode,
82 instruction_offset,
83 } => write!(
84 f,
85 "Trusted opcode {:?} at offset {} in function '{}' has the wrong operand shape",
86 opcode, instruction_offset, function_name
87 ),
88 VerifyError::V2MissingFrameDescriptor {
89 function_name,
90 opcode,
91 instruction_offset,
92 } => write!(
93 f,
94 "V2 typed opcode {:?} at offset {} in function '{}' has no FrameDescriptor",
95 opcode, instruction_offset, function_name
96 ),
97 VerifyError::V2FieldOffsetTooLarge {
98 function_name,
99 opcode,
100 instruction_offset,
101 offset,
102 } => write!(
103 f,
104 "V2 field opcode {:?} at offset {} in function '{}': byte offset {} exceeds maximum (4096)",
105 opcode, instruction_offset, function_name, offset
106 ),
107 VerifyError::V2MissingFieldOffset {
108 function_name,
109 opcode,
110 instruction_offset,
111 } => write!(
112 f,
113 "V2 field opcode {:?} at offset {} in function '{}': missing FieldOffset operand",
114 opcode, instruction_offset, function_name
115 ),
116 }
117 }
118}
119
120impl std::error::Error for VerifyError {}
121
122pub fn verify_trusted_opcodes(program: &BytecodeProgram) -> Result<(), Vec<VerifyError>> {
136 let mut errors = Vec::new();
137
138 for func in &program.functions {
139 let start = func.entry_point;
144 let end = program
146 .functions
147 .iter()
148 .filter(|f| f.entry_point > start)
149 .map(|f| f.entry_point)
150 .min()
151 .unwrap_or(program.instructions.len());
152
153 for offset in start..end {
154 let Some(instruction) = program.instructions.get(offset) else {
155 break;
156 };
157 if !instruction.opcode.is_trusted() {
158 continue;
159 }
160
161 let operand_ok = match instruction.opcode {
167 OpCode::LoadLocalTrusted => {
168 matches!(instruction.operand, Some(Operand::Local(_)))
169 }
170 OpCode::JumpIfFalseTrusted => {
171 matches!(instruction.operand, Some(Operand::Offset(_)))
172 }
173 _ => true,
177 };
178 if !operand_ok {
179 errors.push(VerifyError::TrustedOpcodeBadOperand {
180 function_name: func.name.clone(),
181 opcode: instruction.opcode,
182 instruction_offset: offset,
183 });
184 }
185 }
186 }
187
188 if errors.is_empty() {
189 Ok(())
190 } else {
191 Err(errors)
192 }
193}
194
195const MAX_FIELD_OFFSET: u16 = 4096;
198
199fn is_v2_field_op(op: OpCode) -> bool {
201 matches!(
202 op,
203 OpCode::FieldLoadF64
204 | OpCode::FieldLoadI64
205 | OpCode::FieldLoadI32
206 | OpCode::FieldLoadBool
207 | OpCode::FieldLoadPtr
208 | OpCode::FieldStoreF64
209 | OpCode::FieldStoreI64
210 | OpCode::FieldStoreI32
211 )
212}
213
214pub fn verify_v2_typed_opcodes(program: &BytecodeProgram) -> Result<(), Vec<VerifyError>> {
222 let mut errors = Vec::new();
223
224 for func in &program.functions {
225 let start = func.entry_point;
226 let end = program
227 .functions
228 .iter()
229 .filter(|f| f.entry_point > start)
230 .map(|f| f.entry_point)
231 .min()
232 .unwrap_or(program.instructions.len());
233
234 for offset in start..end {
235 let Some(instruction) = program.instructions.get(offset) else {
236 break;
237 };
238 if !instruction.opcode.is_v2_typed() {
239 continue;
240 }
241
242 if func.frame_descriptor.is_none() {
244 errors.push(VerifyError::V2MissingFrameDescriptor {
245 function_name: func.name.clone(),
246 opcode: instruction.opcode,
247 instruction_offset: offset,
248 });
249 continue;
250 }
251
252 if is_v2_field_op(instruction.opcode) {
254 match &instruction.operand {
255 Some(Operand::FieldOffset(off)) => {
256 if *off > MAX_FIELD_OFFSET {
257 errors.push(VerifyError::V2FieldOffsetTooLarge {
258 function_name: func.name.clone(),
259 opcode: instruction.opcode,
260 instruction_offset: offset,
261 offset: *off,
262 });
263 }
264 }
265 _ => {
266 errors.push(VerifyError::V2MissingFieldOffset {
267 function_name: func.name.clone(),
268 opcode: instruction.opcode,
269 instruction_offset: offset,
270 });
271 }
272 }
273 }
274 }
275 }
276
277 if errors.is_empty() {
278 Ok(())
279 } else {
280 Err(errors)
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::bytecode::{Function, Instruction, OpCode};
288 use crate::type_tracking::{FrameDescriptor, NativeKind};
289
290 fn make_program(functions: Vec<Function>, instructions: Vec<Instruction>) -> BytecodeProgram {
291 let mut prog = BytecodeProgram::new();
292 prog.functions = functions;
293 prog.instructions = instructions;
294 prog
295 }
296
297 #[test]
298 fn no_trusted_opcodes_passes() {
299 let func = Function {
300 name: "main".to_string(),
301 arity: 0,
302 param_names: vec![],
303 locals_count: 2,
304 entry_point: 0,
305 body_length: 2,
306 is_closure: false,
307 captures_count: 0,
308 is_async: false,
309 ref_params: vec![],
310 ref_mutates: vec![],
311 mutable_captures: vec![],
312 frame_descriptor: None,
313 osr_entry_points: vec![],
314 mir_data: None,
315 };
316 let instructions = vec![
317 Instruction::simple(OpCode::AddInt),
318 Instruction::simple(OpCode::ReturnValue),
319 ];
320 let prog = make_program(vec![func], instructions);
321 assert!(verify_trusted_opcodes(&prog).is_ok());
322 }
323
324 #[test]
333 fn trusted_opcode_no_frame_descriptor_is_not_a_violation() {
334 use crate::bytecode::Operand;
335 let func = Function {
336 name: "load_trusted".to_string(),
337 arity: 2,
338 param_names: vec!["a".to_string(), "b".to_string()],
339 locals_count: 2,
340 entry_point: 0,
341 body_length: 3,
342 is_closure: false,
343 captures_count: 0,
344 is_async: false,
345 ref_params: vec![],
346 ref_mutates: vec![],
347 mutable_captures: vec![],
348 frame_descriptor: None,
349 osr_entry_points: vec![],
350 mir_data: None,
351 };
352 let instructions = vec![
353 Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Local(0))),
354 Instruction::new(OpCode::JumpIfFalseTrusted, Some(Operand::Offset(1))),
355 Instruction::simple(OpCode::ReturnValue),
356 ];
357 let prog = make_program(vec![func], instructions);
358 assert!(
359 verify_trusted_opcodes(&prog).is_ok(),
360 "trusted opcodes with no FrameDescriptor must pass (WS-10b stale-rule removal)"
361 );
362 }
363
364 #[test]
368 fn trusted_opcode_bad_operand_is_a_violation() {
369 use crate::bytecode::Operand;
370 let func = Function {
371 name: "bad_load".to_string(),
372 arity: 0,
373 param_names: vec![],
374 locals_count: 1,
375 entry_point: 0,
376 body_length: 2,
377 is_closure: false,
378 captures_count: 0,
379 is_async: false,
380 ref_params: vec![],
381 ref_mutates: vec![],
382 mutable_captures: vec![],
383 frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
384 osr_entry_points: vec![],
385 mir_data: None,
386 };
387 let instructions = vec![
389 Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Offset(3))),
390 Instruction::simple(OpCode::ReturnValue),
391 ];
392 let prog = make_program(vec![func], instructions);
393 let errs = verify_trusted_opcodes(&prog).unwrap_err();
394 assert_eq!(errs.len(), 1);
395 assert!(matches!(
396 &errs[0],
397 VerifyError::TrustedOpcodeBadOperand { .. }
398 ));
399 }
400
401 #[test]
402 fn trusted_opcode_with_valid_operand_passes() {
403 use crate::bytecode::Operand;
404 let func = Function {
405 name: "load_trusted".to_string(),
406 arity: 2,
407 param_names: vec!["a".to_string(), "b".to_string()],
408 locals_count: 2,
409 entry_point: 0,
410 body_length: 2,
411 is_closure: false,
412 captures_count: 0,
413 is_async: false,
414 ref_params: vec![],
415 ref_mutates: vec![],
416 mutable_captures: vec![],
417 frame_descriptor: Some(FrameDescriptor::from_slots(vec![
418 NativeKind::Int64,
419 NativeKind::Int64,
420 ])),
421 osr_entry_points: vec![],
422 mir_data: None,
423 };
424 let instructions = vec![
425 Instruction::new(OpCode::LoadLocalTrusted, Some(Operand::Local(0))),
426 Instruction::simple(OpCode::ReturnValue),
427 ];
428 let prog = make_program(vec![func], instructions);
429 assert!(verify_trusted_opcodes(&prog).is_ok());
430 }
431
432 #[test]
433 fn is_trusted_method() {
434 assert!(OpCode::LoadLocalTrusted.is_trusted());
435 assert!(OpCode::JumpIfFalseTrusted.is_trusted());
436 assert!(!OpCode::AddInt.is_trusted());
437 assert!(!OpCode::Halt.is_trusted());
438 }
439
440 #[test]
441 fn trusted_variant_mapping() {
442 assert_eq!(
443 OpCode::LoadLocal.trusted_variant(),
444 Some(OpCode::LoadLocalTrusted)
445 );
446 assert_eq!(
447 OpCode::JumpIfFalse.trusted_variant(),
448 Some(OpCode::JumpIfFalseTrusted)
449 );
450 assert_eq!(OpCode::Halt.trusted_variant(), None);
451 assert_eq!(OpCode::AddInt.trusted_variant(), None);
452 }
453
454 #[test]
457 fn v2_no_typed_opcodes_passes() {
458 let func = Function {
459 name: "main".to_string(),
460 arity: 0,
461 param_names: vec![],
462 locals_count: 2,
463 entry_point: 0,
464 body_length: 2,
465 is_closure: false,
466 captures_count: 0,
467 is_async: false,
468 ref_params: vec![],
469 ref_mutates: vec![],
470 mutable_captures: vec![],
471 frame_descriptor: None,
472 osr_entry_points: vec![],
473 mir_data: None,
474 };
475 let instructions = vec![
476 Instruction::simple(OpCode::PushNull),
477 Instruction::simple(OpCode::ReturnValue),
478 ];
479 let prog = make_program(vec![func], instructions);
480 assert!(verify_v2_typed_opcodes(&prog).is_ok());
481 }
482
483 #[test]
484 fn v2_typed_array_op_missing_frame_descriptor() {
485 let func = Function {
486 name: "array_fn".to_string(),
487 arity: 0,
488 param_names: vec![],
489 locals_count: 1,
490 entry_point: 0,
491 body_length: 2,
492 is_closure: false,
493 captures_count: 0,
494 is_async: false,
495 ref_params: vec![],
496 ref_mutates: vec![],
497 mutable_captures: vec![],
498 frame_descriptor: None,
499 osr_entry_points: vec![],
500 mir_data: None,
501 };
502 let instructions = vec![
503 Instruction::simple(OpCode::TypedArrayGetF64),
504 Instruction::simple(OpCode::ReturnValue),
505 ];
506 let prog = make_program(vec![func], instructions);
507 let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
508 assert_eq!(errs.len(), 1);
509 assert!(matches!(
510 &errs[0],
511 VerifyError::V2MissingFrameDescriptor { .. }
512 ));
513 }
514
515 #[test]
516 fn v2_typed_array_op_with_frame_descriptor_passes() {
517 let func = Function {
518 name: "array_fn".to_string(),
519 arity: 0,
520 param_names: vec![],
521 locals_count: 1,
522 entry_point: 0,
523 body_length: 2,
524 is_closure: false,
525 captures_count: 0,
526 is_async: false,
527 ref_params: vec![],
528 ref_mutates: vec![],
529 mutable_captures: vec![],
530 frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
531 osr_entry_points: vec![],
532 mir_data: None,
533 };
534 let instructions = vec![
535 Instruction::simple(OpCode::TypedArrayGetF64),
536 Instruction::simple(OpCode::ReturnValue),
537 ];
538 let prog = make_program(vec![func], instructions);
539 assert!(verify_v2_typed_opcodes(&prog).is_ok());
540 }
541
542 #[test]
543 fn v2_field_load_valid_offset() {
544 use crate::bytecode::Operand;
545 let func = Function {
546 name: "field_fn".to_string(),
547 arity: 0,
548 param_names: vec![],
549 locals_count: 1,
550 entry_point: 0,
551 body_length: 2,
552 is_closure: false,
553 captures_count: 0,
554 is_async: false,
555 ref_params: vec![],
556 ref_mutates: vec![],
557 mutable_captures: vec![],
558 frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
559 osr_entry_points: vec![],
560 mir_data: None,
561 };
562 let instructions = vec![
563 Instruction::new(OpCode::FieldLoadF64, Some(Operand::FieldOffset(16))),
564 Instruction::simple(OpCode::ReturnValue),
565 ];
566 let prog = make_program(vec![func], instructions);
567 assert!(verify_v2_typed_opcodes(&prog).is_ok());
568 }
569
570 #[test]
571 fn v2_field_load_offset_too_large() {
572 use crate::bytecode::Operand;
573 let func = Function {
574 name: "field_fn".to_string(),
575 arity: 0,
576 param_names: vec![],
577 locals_count: 1,
578 entry_point: 0,
579 body_length: 2,
580 is_closure: false,
581 captures_count: 0,
582 is_async: false,
583 ref_params: vec![],
584 ref_mutates: vec![],
585 mutable_captures: vec![],
586 frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
587 osr_entry_points: vec![],
588 mir_data: None,
589 };
590 let instructions = vec![
591 Instruction::new(OpCode::FieldLoadF64, Some(Operand::FieldOffset(5000))),
592 Instruction::simple(OpCode::ReturnValue),
593 ];
594 let prog = make_program(vec![func], instructions);
595 let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
596 assert_eq!(errs.len(), 1);
597 assert!(matches!(
598 &errs[0],
599 VerifyError::V2FieldOffsetTooLarge { offset: 5000, .. }
600 ));
601 }
602
603 #[test]
604 fn v2_field_load_missing_operand() {
605 let func = Function {
606 name: "field_fn".to_string(),
607 arity: 0,
608 param_names: vec![],
609 locals_count: 1,
610 entry_point: 0,
611 body_length: 2,
612 is_closure: false,
613 captures_count: 0,
614 is_async: false,
615 ref_params: vec![],
616 ref_mutates: vec![],
617 mutable_captures: vec![],
618 frame_descriptor: Some(FrameDescriptor::from_slots(vec![NativeKind::Int64])),
619 osr_entry_points: vec![],
620 mir_data: None,
621 };
622 let instructions = vec![
623 Instruction::simple(OpCode::FieldLoadI64),
624 Instruction::simple(OpCode::ReturnValue),
625 ];
626 let prog = make_program(vec![func], instructions);
627 let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
628 assert_eq!(errs.len(), 1);
629 assert!(matches!(
630 &errs[0],
631 VerifyError::V2MissingFieldOffset { .. }
632 ));
633 }
634
635 #[test]
636 fn v2_i32_arithmetic_missing_frame_descriptor() {
637 let func = Function {
638 name: "i32_fn".to_string(),
639 arity: 0,
640 param_names: vec![],
641 locals_count: 2,
642 entry_point: 0,
643 body_length: 2,
644 is_closure: false,
645 captures_count: 0,
646 is_async: false,
647 ref_params: vec![],
648 ref_mutates: vec![],
649 mutable_captures: vec![],
650 frame_descriptor: None,
651 osr_entry_points: vec![],
652 mir_data: None,
653 };
654 let instructions = vec![
655 Instruction::simple(OpCode::AddI32),
656 Instruction::simple(OpCode::ReturnValue),
657 ];
658 let prog = make_program(vec![func], instructions);
659 let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
660 assert_eq!(errs.len(), 1);
661 assert!(matches!(
662 &errs[0],
663 VerifyError::V2MissingFrameDescriptor { .. }
664 ));
665 }
666
667 #[test]
668 fn v2_is_v2_typed_method() {
669 assert!(OpCode::TypedArrayGetF64.is_v2_typed());
670 assert!(OpCode::FieldLoadF64.is_v2_typed());
671 assert!(OpCode::AddI32.is_v2_typed());
672 assert!(OpCode::NewTypedStruct.is_v2_typed());
673 assert!(!OpCode::Halt.is_v2_typed());
674 assert!(!OpCode::AddInt.is_v2_typed());
675 assert!(!OpCode::LoadLocal.is_v2_typed());
676 }
677
678 #[test]
684 fn v11a_ownership_opcodes_pass_both_verifiers() {
685 use crate::bytecode::Operand;
686 let func = Function {
687 name: "own_fn".to_string(),
688 arity: 0,
689 param_names: vec![],
690 locals_count: 1,
691 entry_point: 0,
692 body_length: 4,
693 is_closure: false,
694 captures_count: 0,
695 is_async: false,
696 ref_params: vec![],
697 ref_mutates: vec![],
698 mutable_captures: vec![],
699 frame_descriptor: None,
700 osr_entry_points: vec![],
701 mir_data: None,
702 };
703 let instructions = vec![
704 Instruction::new(OpCode::MoveLocal, Some(Operand::Local(0))),
705 Instruction::new(OpCode::CloneLocal, Some(Operand::Local(0))),
706 Instruction::new(OpCode::DropLocal, Some(Operand::Local(0))),
707 Instruction::simple(OpCode::ReturnValue),
708 ];
709 let prog = make_program(vec![func], instructions);
710 assert!(
711 verify_trusted_opcodes(&prog).is_ok(),
712 "V1.1A ownership opcodes should pass trusted verification"
713 );
714 assert!(
715 verify_v2_typed_opcodes(&prog).is_ok(),
716 "V1.1A ownership opcodes should pass v2-typed verification"
717 );
718 }
719
720 #[test]
726 fn v12a_promote_to_shared_passes_both_verifiers() {
727 let func = Function {
728 name: "promote_shared_fn".to_string(),
729 arity: 0,
730 param_names: vec![],
731 locals_count: 0,
732 entry_point: 0,
733 body_length: 2,
734 is_closure: false,
735 captures_count: 0,
736 is_async: false,
737 ref_params: vec![],
738 ref_mutates: vec![],
739 mutable_captures: vec![],
740 frame_descriptor: None,
741 osr_entry_points: vec![],
742 mir_data: None,
743 };
744 let instructions = vec![
745 Instruction::simple(OpCode::PromoteToShared),
746 Instruction::simple(OpCode::ReturnValue),
747 ];
748 let prog = make_program(vec![func], instructions);
749 assert!(
750 verify_trusted_opcodes(&prog).is_ok(),
751 "V1.2A PromoteToShared should pass trusted verification"
752 );
753 assert!(
754 verify_v2_typed_opcodes(&prog).is_ok(),
755 "V1.2A PromoteToShared should pass v2-typed verification"
756 );
757 }
758
759 #[test]
767 fn r51a_typed_bitwise_opcodes_pass_both_verifiers() {
768 let func = Function {
769 name: "bitwise_fn".to_string(),
770 arity: 0,
771 param_names: vec![],
772 locals_count: 0,
773 entry_point: 0,
774 body_length: 7,
775 is_closure: false,
776 captures_count: 0,
777 is_async: false,
778 ref_params: vec![],
779 ref_mutates: vec![],
780 mutable_captures: vec![],
781 frame_descriptor: None,
782 osr_entry_points: vec![],
783 mir_data: None,
784 };
785 let instructions = vec![
786 Instruction::simple(OpCode::BitAndInt),
787 Instruction::simple(OpCode::BitOrInt),
788 Instruction::simple(OpCode::BitXorInt),
789 Instruction::simple(OpCode::BitShlInt),
790 Instruction::simple(OpCode::BitShrInt),
791 Instruction::simple(OpCode::BitNotInt),
792 Instruction::simple(OpCode::ReturnValue),
793 ];
794 let prog = make_program(vec![func], instructions);
795 assert!(
796 verify_trusted_opcodes(&prog).is_ok(),
797 "R5.1A typed bitwise opcodes should pass trusted verification"
798 );
799 assert!(
800 verify_v2_typed_opcodes(&prog).is_ok(),
801 "R5.1A typed bitwise opcodes should pass v2-typed verification"
802 );
803 }
804
805 #[test]
806 fn v2_multiple_errors_collected() {
807 let func = Function {
808 name: "multi_err".to_string(),
809 arity: 0,
810 param_names: vec![],
811 locals_count: 2,
812 entry_point: 0,
813 body_length: 3,
814 is_closure: false,
815 captures_count: 0,
816 is_async: false,
817 ref_params: vec![],
818 ref_mutates: vec![],
819 mutable_captures: vec![],
820 frame_descriptor: None,
821 osr_entry_points: vec![],
822 mir_data: None,
823 };
824 let instructions = vec![
825 Instruction::simple(OpCode::AddI32),
826 Instruction::simple(OpCode::TypedArrayGetI64),
827 Instruction::simple(OpCode::ReturnValue),
828 ];
829 let prog = make_program(vec![func], instructions);
830 let errs = verify_v2_typed_opcodes(&prog).unwrap_err();
831 assert_eq!(errs.len(), 2);
832 }
833}