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.get_initial_fp().ok_or_else(|| {
85        VirtualMachineError::Other(anyhow::anyhow!(
86            "Failed to retrieve the initial_fp: it is None. \
87                     The initial_fp field should be initialized after running the entry point."
88        ))
89    })?;
90    let ret_fp_addr = (initial_fp - 2).map_err(VirtualMachineError::Math)?;
91    let ret_fp = runner.vm.get_maybe(&ret_fp_addr).ok_or_else(|| {
92        VirtualMachineError::Other(anyhow::anyhow!(
93            "Ret FP address is not in memory: {ret_fp_addr}"
94        ))
95    })?;
96    let final_fp = runner.vm.get_fp();
97    match ret_fp {
98        MaybeRelocatable::RelocatableValue(value) => {
99            if runner.runner_mode == RunnerMode::ProofModeCanonical && value != final_fp {
100                return Err(VirtualMachineError::Other(anyhow::anyhow!(
101                    "Return FP is not equal to final FP: ret_f={ret_fp}, final_fp={final_fp}"
102                )));
103            }
104            if runner.runner_mode == RunnerMode::ExecutionMode && value.offset != final_fp.offset {
105                return Err(VirtualMachineError::Other(anyhow::anyhow!(
106                    "Return FP offset is not equal to final FP offset: ret_f={ret_fp}, final_fp={final_fp}"
107                )));
108            }
109        }
110        MaybeRelocatable::Int(value) => {
111            if Felt252::from(final_fp.offset) != value {
112                return Err(VirtualMachineError::Other(anyhow::anyhow!(
113                    "Return FP felt value is not equal to final FP offset: ret_fp={ret_fp}, final_fp={final_fp}"
114                )));
115            }
116        }
117    }
118    Ok(())
119}
120
121#[cfg(test)]
122mod test {
123    use super::*;
124    use crate::hint_processor::builtin_hint_processor::builtin_hint_processor_definition::BuiltinHintProcessor;
125
126    use crate::types::builtin_name::BuiltinName;
127    use crate::types::relocatable::Relocatable;
128
129    use crate::Felt252;
130    use crate::{relocatable, types::program::Program, utils::test_utils::*};
131    use assert_matches::assert_matches;
132
133    #[cfg(target_arch = "wasm32")]
134    use wasm_bindgen_test::*;
135
136    #[test]
137    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
138    fn verify_secure_runner_without_program_base() {
139        let program = program!();
140
141        let runner = cairo_runner!(program);
142
143        assert_matches!(
144            verify_secure_runner(&runner, true, None),
145            Err(VirtualMachineError::RunnerError(RunnerError::NoProgBase))
146        );
147    }
148
149    #[test]
150    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
151    fn verify_secure_runner_empty_memory() {
152        let program = program!(main = Some(0),);
153        let mut runner = cairo_runner!(program);
154        runner.initialize(false).unwrap();
155        // runner.vm.segments.compute_effective_sizes();
156        let mut hint_processor = BuiltinHintProcessor::new_empty();
157        runner.end_run(false, false, &mut hint_processor).unwrap();
158        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
159        // to the stack at the start of the run.
160        runner.vm.run_context.fp = 0;
161        assert_matches!(verify_secure_runner(&runner, true, None), Ok(()));
162    }
163
164    #[test]
165    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
166    fn verify_secure_runner_program_access_out_of_bounds() {
167        let program = program!(main = Some(0),);
168        let mut runner = cairo_runner!(program);
169
170        runner.initialize(false).unwrap();
171
172        runner.vm.segments = segments![((0, 0), 100)];
173        runner.vm.segments.segment_used_sizes = Some(vec![1]);
174
175        assert_matches!(
176            verify_secure_runner(&runner, true, None),
177            Err(VirtualMachineError::OutOfBoundsProgramSegmentAccess)
178        );
179    }
180
181    #[test]
182    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
183    fn verify_secure_runner_program_with_program_size() {
184        let program = program!(main = Some(0),);
185        let mut runner = cairo_runner!(program);
186
187        runner.initialize(false).unwrap();
188        // We insert (1, 0) for ret_fp segment.
189        runner.vm.segments = segments![((0, 0), 100), ((1, 0), 0)];
190        runner.vm.segments.segment_used_sizes = Some(vec![1]);
191        // At the end of the run, the ret_fp should be the base of the new ret_fp segment we added
192        // to the stack at the start of the run.
193        runner.vm.run_context.fp = 0;
194        assert_matches!(verify_secure_runner(&runner, true, Some(1)), Ok(()));
195    }
196
197    #[test]
198    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
199    fn verify_secure_runner_builtin_access_out_of_bounds() {
200        let program = program!(main = Some(0), builtins = vec![BuiltinName::range_check],);
201        let mut runner = cairo_runner!(program);
202
203        runner.initialize(false).unwrap();
204        runner.vm.builtin_runners[0].set_stop_ptr(0);
205        runner.vm.segments.memory = memory![((2, 0), 1)];
206        runner.vm.segments.segment_used_sizes = Some(vec![0, 0, 0, 0]);
207
208        assert_matches!(
209            verify_secure_runner(&runner, true, None),
210            Err(VirtualMachineError::OutOfBoundsBuiltinSegmentAccess)
211        );
212    }
213
214    #[test]
215    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
216    fn verify_secure_runner_builtin_access_correct() {
217        let program = program!(main = Some(0), builtins = vec![BuiltinName::range_check],);
218        let mut runner = cairo_runner!(program);
219
220        runner.initialize(false).unwrap();
221        let mut hint_processor = BuiltinHintProcessor::new_empty();
222        runner.end_run(false, false, &mut hint_processor).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::Other(ref err)) if err.to_string().contains("Failed to retrieve the initial_fp: it is None")
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::Other(ref err))
358                if err.to_string().contains("Ret FP address is not in memory")
359        );
360    }
361
362    #[test]
363    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
364    fn verify_secure_runner_return_fp_not_equal_final_fp_proof_mode() {
365        let program = program!(main = Some(0),);
366        let mut runner = cairo_runner!(program);
367        runner.initialize(false).unwrap();
368
369        // Set the runner mode to ProofModeCanonical, so we expect
370        // the return FP to be equal to final_fp.
371        runner.runner_mode = RunnerMode::ProofModeCanonical;
372
373        assert_matches!(
374            verify_secure_runner(&runner, true, None),
375            Err(VirtualMachineError::Other(ref err))
376                if err.to_string().contains("Return FP is not equal to final FP")
377        );
378    }
379
380    #[test]
381    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
382    fn verify_secure_runner_return_fp_offset_not_equal_final_fp_offset_execution_mode() {
383        let program = program!(main = Some(0),);
384        let mut runner = cairo_runner!(program);
385        runner.initialize(false).unwrap();
386
387        // ExecutionMode only requires offset equality, not the entire relocatable.
388        assert_matches!(
389            verify_secure_runner(&runner, true, None),
390            Err(VirtualMachineError::Other(ref err))
391                if err.to_string().contains("Return FP offset is not equal to final FP offset")
392        );
393    }
394
395    #[test]
396    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
397    fn verify_secure_runner_return_fp_felt_not_equal_final_fp_offse() {
398        let program = program!(main = Some(0),);
399        let mut runner = cairo_runner!(program);
400        runner.initialize(false).unwrap();
401        // Insert Felt(0) as the return FP.
402        runner.vm.segments.memory = memory![((1, 0), 0)];
403
404        // ExecutionMode only requires offset equality, not the entire relocatable.
405        assert_matches!(
406            verify_secure_runner(&runner, true, None),
407            Err(VirtualMachineError::Other(ref err))
408                if err.to_string().contains("Return FP felt value is not equal to final FP offset")
409        );
410    }
411}