Skip to main content

cairo_vm/vm/
security.rs

1use num_traits::ToPrimitive;
2
3use super::{
4    errors::{runner_errors::RunnerError, vm_errors::VirtualMachineError},
5    runners::cairo_runner::{CairoRunner, RunnerMode},
6};
7use crate::types::relocatable::MaybeRelocatable;
8use crate::Felt252;
9
10/// Verify that the completed run in a runner is safe to be relocated and be
11/// used by other Cairo programs.
12///
13/// Checks include:
14///   - (Only if `verify_builtins` is set to true) All accesses to the builtin segments must be within the range defined by
15///     the builtins themselves.
16///   - There must not be accesses to the program segment outside the program
17///     data range. This check will use the `program_segment_size` instead of the program data length if available.
18///   - All addresses in memory must be real (not temporary)
19///
20/// Note: Each builtin is responsible for checking its own segments' data.
21pub fn verify_secure_runner(
22    runner: &CairoRunner,
23    verify_builtins: bool,
24    program_segment_size: Option<usize>,
25) -> Result<(), VirtualMachineError> {
26    let builtins_segment_info = match verify_builtins {
27        true => runner.get_builtin_segments_info()?,
28        false => Vec::new(),
29    };
30    // Check builtin segment out of bounds.
31    for (index, stop_ptr) in builtins_segment_info {
32        let current_size = runner
33            .vm
34            .segments
35            .memory
36            .data
37            .get(index)
38            .map(|segment| segment.len());
39        // + 1 here accounts for maximum segment offset being segment.len() -1
40        if current_size >= Some(stop_ptr + 1) {
41            return Err(VirtualMachineError::OutOfBoundsBuiltinSegmentAccess);
42        }
43    }
44    // Check out of bounds for program segment.
45    let program_segment_index = runner
46        .program_base
47        .and_then(|rel| rel.segment_index.to_usize())
48        .ok_or(RunnerError::NoProgBase)?;
49    let program_segment_size =
50        program_segment_size.unwrap_or(runner.program.shared_program_data.data.len());
51    let program_length = runner
52        .vm
53        .segments
54        .memory
55        .data
56        .get(program_segment_index)
57        .map(|segment| segment.len());
58    // + 1 here accounts for maximum segment offset being segment.len() -1
59    if program_length >= Some(program_segment_size + 1) {
60        return Err(VirtualMachineError::OutOfBoundsProgramSegmentAccess);
61    }
62    // Check that the addresses in memory are valid
63    // This means that every temporary address has been properly relocated to a real address
64    // Asumption: If temporary memory is empty, this means no temporary memory addresses were generated and all addresses in memory are real
65    if !runner.vm.segments.memory.temp_data.is_empty() {
66        for value in runner.vm.segments.memory.data.iter().flatten() {
67            match value.get_value() {
68                Some(MaybeRelocatable::RelocatableValue(addr)) if addr.segment_index < 0 => {
69                    return Err(VirtualMachineError::InvalidMemoryValueTemporaryAddress(
70                        Box::new(addr),
71                    ))
72                }
73                _ => {}
74            }
75        }
76    }
77    for builtin in runner.vm.builtin_runners.iter() {
78        builtin.run_security_checks(&runner.vm)?;
79    }
80
81    // Validate ret FP.
82    let initial_fp = runner
83        .get_initial_fp()
84        .ok_or(VirtualMachineError::MissingInitialFp)?;
85    let ret_fp_addr = (initial_fp - 2).map_err(VirtualMachineError::Math)?;
86    let ret_fp = runner
87        .vm
88        .get_maybe(&ret_fp_addr)
89        .ok_or(VirtualMachineError::MissingReturnFp(Box::new(ret_fp_addr)))?;
90    let final_fp = runner.vm.get_fp();
91    match ret_fp {
92        MaybeRelocatable::RelocatableValue(value) => {
93            if runner.runner_mode == RunnerMode::ProofModeCanonical && value != final_fp {
94                return Err(VirtualMachineError::MismatchReturnFP(Box::new((
95                    value, final_fp,
96                ))));
97            }
98            if runner.runner_mode == RunnerMode::ExecutionMode && value.offset != final_fp.offset {
99                return Err(VirtualMachineError::MismatchReturnFPOffset(Box::new((
100                    value, final_fp,
101                ))));
102            }
103        }
104        MaybeRelocatable::Int(value) => {
105            if Felt252::from(final_fp.offset) != value {
106                return Err(VirtualMachineError::MismatchReturnFPFelt(Box::new((
107                    value, final_fp,
108                ))));
109            }
110        }
111    }
112    Ok(())
113}
114
115#[cfg(test)]
116mod test {
117    use super::*;
118    use crate::hint_processor::builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor;
119
120    use crate::types::builtin_name::BuiltinName;
121    use crate::types::relocatable::Relocatable;
122
123    use crate::Felt252;
124    use crate::{relocatable, types::program::Program, utils::test_utils::*};
125    use assert_matches::assert_matches;
126
127    #[test]
128    fn verify_secure_runner_without_program_base() {
129        let program = program!();
130
131        let runner = cairo_runner!(program);
132
133        assert_matches!(
134            verify_secure_runner(&runner, true, None),
135            Err(VirtualMachineError::RunnerError(RunnerError::NoProgBase))
136        );
137    }
138
139    #[test]
140    fn verify_secure_runner_empty_memory() {
141        let program = program!(main = Some(0),);
142        let mut runner = cairo_runner!(program);
143        runner.initialize(false).unwrap();
144        // runner.vm.segments.compute_effective_sizes();
145        let mut hint_processor = BuiltinHintProcessor::new_empty();
146        runner
147            .end_run(false, false, &mut hint_processor, false)
148            .unwrap();
149        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
150        // to the stack at the start of the run.
151        runner.vm.run_context.fp = 0;
152        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
153    }
154
155    #[test]
156    fn verify_secure_runner_program_access_out_of_bounds() {
157        let program = program!(main = Some(0),);
158        let mut runner = cairo_runner!(program);
159
160        runner.initialize(false).unwrap();
161
162        runner.vm.segments = segments![((0, 0), 100)];
163        runner.vm.segments.segment_used_sizes = Some(vec![1]);
164
165        assert_matches!(
166            verify_secure_runner(&runner, true, None),
167            Err(VirtualMachineError::OutOfBoundsProgramSegmentAccess)
168        );
169    }
170
171    #[test]
172    fn verify_secure_runner_program_with_program_size() {
173        let program = program!(main = Some(0),);
174        let mut runner = cairo_runner!(program);
175
176        runner.initialize(false).unwrap();
177        // We insert (1, 0) for ret_fp segment.
178        runner.vm.segments = segments![((0, 0), 100), ((1, 0), 0)];
179        runner.vm.segments.segment_used_sizes = Some(vec![1]);
180        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
181        // to the stack at the start of the run.
182        runner.vm.run_context.fp = 0;
183        assert_matches!(verify_secure_runner(&runner, true, Some(1)), Ok(()));
184    }
185
186    #[test]
187    fn verify_secure_runner_builtin_access_out_of_bounds() {
188        let program = program!(main = Some(0), builtins = vec![BuiltinName::range_check],);
189        let mut runner = cairo_runner!(program);
190
191        runner.initialize(false).unwrap();
192        runner.vm.builtin_runners[0].set_stop_ptr(0);
193        runner.vm.segments.memory = memory![((2, 0), 1)];
194        runner.vm.segments.segment_used_sizes = Some(vec![0, 0, 0, 0]);
195
196        assert_matches!(
197            verify_secure_runner(&runner, true, None),
198            Err(VirtualMachineError::OutOfBoundsBuiltinSegmentAccess)
199        );
200    }
201
202    #[test]
203    fn verify_secure_runner_builtin_access_correct() {
204        let program = program!(main = Some(0), builtins = vec![BuiltinName::range_check],);
205        let mut runner = cairo_runner!(program);
206
207        runner.initialize(false).unwrap();
208        let mut hint_processor = BuiltinHintProcessor::new_empty();
209        runner
210            .end_run(false, false, &mut hint_processor, false)
211            .unwrap();
212        runner.vm.builtin_runners[0].set_stop_ptr(1);
213        // Adding ((1, 1), (3, 0)) to the memory segment to simulate the ret_fp_segment.
214        runner.vm.segments.memory = memory![((2, 0), 1), ((1, 1), (3, 0))];
215        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
216        // to the stack at the start of the run.
217        runner.vm.run_context.fp = 0;
218        runner.vm.segments.segment_used_sizes = Some(vec![0, 0, 1, 0]);
219
220        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
221    }
222
223    #[test]
224    fn verify_secure_runner_success() {
225        let program = program!(
226            data = vec![
227                Felt252::ZERO.into(),
228                Felt252::ZERO.into(),
229                Felt252::ZERO.into(),
230                Felt252::ZERO.into(),
231            ],
232            main = Some(0),
233        );
234
235        let mut runner = cairo_runner!(program);
236
237        runner.initialize(false).unwrap();
238        // We insert (1, 0) for ret_fp segment.
239        runner.vm.segments.memory = memory![
240            ((0, 0), (1, 0)),
241            ((0, 1), (2, 1)),
242            ((0, 2), (3, 2)),
243            ((0, 3), (4, 3)),
244            ((1, 0), 0)
245        ];
246        runner.vm.segments.segment_used_sizes = Some(vec![5, 1, 2, 3, 4]);
247        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
248        // to the stack at the start of the run.
249        runner.vm.run_context.fp = 0;
250
251        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
252    }
253
254    #[test]
255    fn verify_secure_runner_temporary_memory_properly_relocated() {
256        let program = program!(
257            data = vec![
258                Felt252::ZERO.into(),
259                Felt252::ZERO.into(),
260                Felt252::ZERO.into(),
261                Felt252::ZERO.into(),
262            ],
263            main = Some(0),
264        );
265
266        let mut runner = cairo_runner!(program);
267
268        // We insert (1, 0) for ret_fp segment.
269        runner.initialize(false).unwrap();
270        runner.vm.segments.memory = memory![
271            ((0, 1), (1, 0)),
272            ((0, 2), (2, 1)),
273            ((0, 3), (3, 2)),
274            ((-1, 0), (1, 2)),
275            ((1, 0), 0)
276        ];
277        runner.vm.segments.segment_used_sizes = Some(vec![5, 1, 2, 3, 4]);
278        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
279        // to the stack at the start of the run.
280        runner.vm.run_context.fp = 0;
281
282        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
283    }
284
285    #[test]
286    fn verify_secure_runner_temporary_memory_not_fully_relocated() {
287        let program = program!(
288            data = vec![
289                Felt252::ZERO.into(),
290                Felt252::ZERO.into(),
291                Felt252::ZERO.into(),
292                Felt252::ZERO.into(),
293            ],
294            main = Some(0),
295        );
296
297        let mut runner = cairo_runner!(program);
298
299        runner.initialize(false).unwrap();
300        // We insert (1, 0) for ret_fp segment.
301        runner.vm.segments.memory = memory![
302            ((0, 0), (1, 0)),
303            ((0, 1), (2, 1)),
304            ((0, 2), (-3, 2)),
305            ((0, 3), (4, 3)),
306            ((-1, 0), (1, 2)),
307            ((1, 0), 0)
308        ];
309        runner.vm.segments.segment_used_sizes = Some(vec![5, 1, 2, 3, 4]);
310
311        assert_matches!(
312            verify_secure_runner(&runner, true, None),
313            Err(VirtualMachineError::InvalidMemoryValueTemporaryAddress(
314                bx
315            )) if *bx == relocatable!(-3, 2)
316        );
317    }
318
319    #[test]
320    fn verify_secure_runner_missing_initial_fp_error() {
321        let program = program!(main = Some(0),);
322        let mut runner = cairo_runner!(program);
323        // init program base to avoid other errors.
324        runner.program_base = Some(runner.vm.add_memory_segment());
325
326        assert_matches!(
327            verify_secure_runner(&runner, true, None),
328            Err(VirtualMachineError::MissingInitialFp)
329        );
330    }
331
332    #[test]
333    fn verify_secure_runner_ret_fp_address_not_in_memory() {
334        let program = program!(main = Some(0),);
335        let mut runner = cairo_runner!(program);
336        runner.initialize(false).unwrap();
337        // simulate empty memory.
338        runner.vm.segments.memory = crate::vm::vm_memory::memory::Memory::new();
339        assert_matches!(
340            verify_secure_runner(&runner, true, None),
341            Err(VirtualMachineError::MissingReturnFp(..))
342        );
343    }
344
345    #[test]
346    fn verify_secure_runner_return_fp_not_equal_final_fp_proof_mode() {
347        let program = program!(main = Some(0),);
348        let mut runner = cairo_runner!(program);
349        runner.initialize(false).unwrap();
350
351        // Set the runner mode to ProofModeCanonical, so we expect
352        // the return FP to be equal to final_fp.
353        runner.runner_mode = RunnerMode::ProofModeCanonical;
354
355        assert_matches!(
356            verify_secure_runner(&runner, true, None),
357            Err(VirtualMachineError::MismatchReturnFP(..))
358        );
359    }
360
361    #[test]
362    fn verify_secure_runner_return_fp_offset_not_equal_final_fp_offset_execution_mode() {
363        let program = program!(main = Some(0),);
364        let mut runner = cairo_runner!(program);
365        runner.initialize(false).unwrap();
366
367        // ExecutionMode only requires offset equality, not the entire relocatable.
368        assert_matches!(
369            verify_secure_runner(&runner, true, None),
370            Err(VirtualMachineError::MismatchReturnFPOffset(..))
371        );
372    }
373
374    #[test]
375    fn verify_secure_runner_return_fp_felt_not_equal_final_fp_offse() {
376        let program = program!(main = Some(0),);
377        let mut runner = cairo_runner!(program);
378        runner.initialize(false).unwrap();
379        // Insert Felt(0) as the return FP.
380        runner.vm.segments.memory = memory![((1, 0), 0)];
381
382        // ExecutionMode only requires offset equality, not the entire relocatable.
383        assert_matches!(
384            verify_secure_runner(&runner, true, None),
385            Err(VirtualMachineError::MismatchReturnFPFelt(..))
386        );
387    }
388}