cairo_vm/vm/
security.rs

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