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.end_run(false, false, &mut hint_processor).unwrap();
154        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
155        // to the stack at the start of the run.
156        runner.vm.run_context.fp = 0;
157        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
158    }
159
160    #[test]
161    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
162    fn verify_secure_runner_program_access_out_of_bounds() {
163        let program = program!(main = Some(0),);
164        let mut runner = cairo_runner!(program);
165
166        runner.initialize(false).unwrap();
167
168        runner.vm.segments = segments![((0, 0), 100)];
169        runner.vm.segments.segment_used_sizes = Some(vec![1]);
170
171        assert_matches!(
172            verify_secure_runner(&runner, true, None),
173            Err(VirtualMachineError::OutOfBoundsProgramSegmentAccess)
174        );
175    }
176
177    #[test]
178    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
179    fn verify_secure_runner_program_with_program_size() {
180        let program = program!(main = Some(0),);
181        let mut runner = cairo_runner!(program);
182
183        runner.initialize(false).unwrap();
184        // We insert (1, 0) for ret_fp segment.
185        runner.vm.segments = segments![((0, 0), 100), ((1, 0), 0)];
186        runner.vm.segments.segment_used_sizes = Some(vec![1]);
187        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
188        // to the stack at the start of the run.
189        runner.vm.run_context.fp = 0;
190        assert_matches!(verify_secure_runner(&runner, true, Some(1)), Ok(()));
191    }
192
193    #[test]
194    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
195    fn verify_secure_runner_builtin_access_out_of_bounds() {
196        let program = program!(main = Some(0), builtins = vec![BuiltinName::range_check],);
197        let mut runner = cairo_runner!(program);
198
199        runner.initialize(false).unwrap();
200        runner.vm.builtin_runners[0].set_stop_ptr(0);
201        runner.vm.segments.memory = memory![((2, 0), 1)];
202        runner.vm.segments.segment_used_sizes = Some(vec![0, 0, 0, 0]);
203
204        assert_matches!(
205            verify_secure_runner(&runner, true, None),
206            Err(VirtualMachineError::OutOfBoundsBuiltinSegmentAccess)
207        );
208    }
209
210    #[test]
211    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
212    fn verify_secure_runner_builtin_access_correct() {
213        let program = program!(main = Some(0), builtins = vec![BuiltinName::range_check],);
214        let mut runner = cairo_runner!(program);
215
216        runner.initialize(false).unwrap();
217        let mut hint_processor = BuiltinHintProcessor::new_empty();
218        runner.end_run(false, false, &mut hint_processor).unwrap();
219        runner.vm.builtin_runners[0].set_stop_ptr(1);
220        // Adding ((1, 1), (3, 0)) to the memory segment to simulate the ret_fp_segment.
221        runner.vm.segments.memory = memory![((2, 0), 1), ((1, 1), (3, 0))];
222        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
223        // to the stack at the start of the run.
224        runner.vm.run_context.fp = 0;
225        runner.vm.segments.segment_used_sizes = Some(vec![0, 0, 1, 0]);
226
227        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
228    }
229
230    #[test]
231    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
232    fn verify_secure_runner_success() {
233        let program = program!(
234            data = vec![
235                Felt252::ZERO.into(),
236                Felt252::ZERO.into(),
237                Felt252::ZERO.into(),
238                Felt252::ZERO.into(),
239            ],
240            main = Some(0),
241        );
242
243        let mut runner = cairo_runner!(program);
244
245        runner.initialize(false).unwrap();
246        // We insert (1, 0) for ret_fp segment.
247        runner.vm.segments.memory = memory![
248            ((0, 0), (1, 0)),
249            ((0, 1), (2, 1)),
250            ((0, 2), (3, 2)),
251            ((0, 3), (4, 3)),
252            ((1, 0), 0)
253        ];
254        runner.vm.segments.segment_used_sizes = Some(vec![5, 1, 2, 3, 4]);
255        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
256        // to the stack at the start of the run.
257        runner.vm.run_context.fp = 0;
258
259        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
260    }
261
262    #[test]
263    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
264    fn verify_secure_runner_temporary_memory_properly_relocated() {
265        let program = program!(
266            data = vec![
267                Felt252::ZERO.into(),
268                Felt252::ZERO.into(),
269                Felt252::ZERO.into(),
270                Felt252::ZERO.into(),
271            ],
272            main = Some(0),
273        );
274
275        let mut runner = cairo_runner!(program);
276
277        // We insert (1, 0) for ret_fp segment.
278        runner.initialize(false).unwrap();
279        runner.vm.segments.memory = memory![
280            ((0, 1), (1, 0)),
281            ((0, 2), (2, 1)),
282            ((0, 3), (3, 2)),
283            ((-1, 0), (1, 2)),
284            ((1, 0), 0)
285        ];
286        runner.vm.segments.segment_used_sizes = Some(vec![5, 1, 2, 3, 4]);
287        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
288        // to the stack at the start of the run.
289        runner.vm.run_context.fp = 0;
290
291        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
292    }
293
294    #[test]
295    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
296    fn verify_secure_runner_temporary_memory_not_fully_relocated() {
297        let program = program!(
298            data = vec![
299                Felt252::ZERO.into(),
300                Felt252::ZERO.into(),
301                Felt252::ZERO.into(),
302                Felt252::ZERO.into(),
303            ],
304            main = Some(0),
305        );
306
307        let mut runner = cairo_runner!(program);
308
309        runner.initialize(false).unwrap();
310        // We insert (1, 0) for ret_fp segment.
311        runner.vm.segments.memory = memory![
312            ((0, 0), (1, 0)),
313            ((0, 1), (2, 1)),
314            ((0, 2), (-3, 2)),
315            ((0, 3), (4, 3)),
316            ((-1, 0), (1, 2)),
317            ((1, 0), 0)
318        ];
319        runner.vm.segments.segment_used_sizes = Some(vec![5, 1, 2, 3, 4]);
320
321        assert_matches!(
322            verify_secure_runner(&runner, true, None),
323            Err(VirtualMachineError::InvalidMemoryValueTemporaryAddress(
324                bx
325            )) if *bx == relocatable!(-3, 2)
326        );
327    }
328
329    #[test]
330    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
331    fn verify_secure_runner_missing_initial_fp_error() {
332        let program = program!(main = Some(0),);
333        let mut runner = cairo_runner!(program);
334        // init program base to avoid other errors.
335        runner.program_base = Some(runner.vm.add_memory_segment());
336
337        assert_matches!(
338            verify_secure_runner(&runner, true, None),
339            Err(VirtualMachineError::MissingInitialFp)
340        );
341    }
342
343    #[test]
344    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
345    fn verify_secure_runner_ret_fp_address_not_in_memory() {
346        let program = program!(main = Some(0),);
347        let mut runner = cairo_runner!(program);
348        runner.initialize(false).unwrap();
349        // simulate empty memory.
350        runner.vm.segments.memory = crate::vm::vm_memory::memory::Memory::new();
351        assert_matches!(
352            verify_secure_runner(&runner, true, None),
353            Err(VirtualMachineError::MissingReturnFp(..))
354        );
355    }
356
357    #[test]
358    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
359    fn verify_secure_runner_return_fp_not_equal_final_fp_proof_mode() {
360        let program = program!(main = Some(0),);
361        let mut runner = cairo_runner!(program);
362        runner.initialize(false).unwrap();
363
364        // Set the runner mode to ProofModeCanonical, so we expect
365        // the return FP to be equal to final_fp.
366        runner.runner_mode = RunnerMode::ProofModeCanonical;
367
368        assert_matches!(
369            verify_secure_runner(&runner, true, None),
370            Err(VirtualMachineError::MismatchReturnFP(..))
371        );
372    }
373
374    #[test]
375    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
376    fn verify_secure_runner_return_fp_offset_not_equal_final_fp_offset_execution_mode() {
377        let program = program!(main = Some(0),);
378        let mut runner = cairo_runner!(program);
379        runner.initialize(false).unwrap();
380
381        // ExecutionMode only requires offset equality, not the entire relocatable.
382        assert_matches!(
383            verify_secure_runner(&runner, true, None),
384            Err(VirtualMachineError::MismatchReturnFPOffset(..))
385        );
386    }
387
388    #[test]
389    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
390    fn verify_secure_runner_return_fp_felt_not_equal_final_fp_offse() {
391        let program = program!(main = Some(0),);
392        let mut runner = cairo_runner!(program);
393        runner.initialize(false).unwrap();
394        // Insert Felt(0) as the return FP.
395        runner.vm.segments.memory = memory![((1, 0), 0)];
396
397        // ExecutionMode only requires offset equality, not the entire relocatable.
398        assert_matches!(
399            verify_secure_runner(&runner, true, None),
400            Err(VirtualMachineError::MismatchReturnFPFelt(..))
401        );
402    }
403}