diff --git a/cairo-native-fuzzer/.gitignore b/cairo-native-fuzzer/.gitignore new file mode 100644 index 0000000..aa223a5 --- /dev/null +++ b/cairo-native-fuzzer/.gitignore @@ -0,0 +1,4 @@ +/target +/cairo2 +/corelib +cairo*.tar \ No newline at end of file diff --git a/cairo-native-fuzzer/Cargo.toml b/cairo-native-fuzzer/Cargo.toml new file mode 100644 index 0000000..dce9317 --- /dev/null +++ b/cairo-native-fuzzer/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cairo-native-fuzzer" +version = "0.1.0" +edition = "2021" + +[dependencies] +cairo-lang-compiler = "2.8.4" +cairo-lang-sierra = "2.8.4" +cairo-lang-starknet = "2.8.4" +cairo-native = { version = "0.2.0-alpha.4", features = ["with-runtime"] } +cairo-native-runtime = { version = "0.2.0-alpha.4", optional = true } +clap = "4.5.20" +rand = "0.8.5" +starknet-types-core = "0.1.7" + +[dependencies.felt] +git = 'https://github.com/FuzzingLabs/cairo-rs' +rev = '48af153240392992f18a09e969bae6518eec9639' +package = 'cairo-felt' + +[features] +default = ["with-runtime"] +with-runtime = ["dep:cairo-native-runtime"] \ No newline at end of file diff --git a/cairo-native-fuzzer/README.md b/cairo-native-fuzzer/README.md new file mode 100644 index 0000000..5f826bc --- /dev/null +++ b/cairo-native-fuzzer/README.md @@ -0,0 +1,15 @@ +## Cairo Native Fuzzer + +Cairo Native Fuzzer is a rewrite of the Cairo Fuzzer based on [Cairo native from Lambdaclass](https://github.com/lambdaclass/cairo_native) developed to enhance fuzzer execution speed. + +### Roadmap + +### Step 1 : Create a basic fuzzer based on Cairo Native : +- [x] Implement the Cairo Native runner +- [x] Implement the fuzzer based on Cairo Native runner +- [x] Import existing mutator from the cairo-fuzzer + +### Step 2 : Integrate existing cairo-fuzzer features into Cairo Native fuzzer : +- [ ] Multithreading +- [ ] Support config files +- [ ] Property testing diff --git a/cairo-native-fuzzer/examples/fuzzinglabs.cairo b/cairo-native-fuzzer/examples/fuzzinglabs.cairo new file mode 100644 index 0000000..de8d46e --- /dev/null +++ b/cairo-native-fuzzer/examples/fuzzinglabs.cairo @@ -0,0 +1,56 @@ +use starknet::{ + Store, SyscallResult, StorageBaseAddress, storage_read_syscall, storage_write_syscall, + storage_address_from_base_and_offset +}; +use integer::{ + U128IntoFelt252, Felt252IntoU256, Felt252TryIntoU64, U256TryIntoFelt252, u256_from_felt252 +}; + + +#[starknet::contract] +mod test_contract { + #[storage] + struct Storage { + bal:u8 + } + #[external(v0)] + fn Fuzz_symbolic_execution( +ref self: ContractState, + f: felt252, + u: felt252, + z: u16, + z2: u32, + i: u64, + n: u128, + g: u128, + l: u128, + a: felt252, + b: felt252, + s: u8, + ) { + if (f == 'f') { + if (u == 'u') { + if (z == 'z') { + if (z2 == 'z') { + if (i == 'i') { + if (n == 'n') { + if (g == 'g') { + if (l == 'l') { + if (a == 'a') { + if (b == 'b') { + if (s == 's') { + assert(1==0 , '!(f & t)'); + } + } + } + } + } + } + } + } + } + } + } + return (); + } +} \ No newline at end of file diff --git a/cairo-native-fuzzer/src/fuzzer/fuzzer.rs b/cairo-native-fuzzer/src/fuzzer/fuzzer.rs new file mode 100644 index 0000000..1843e58 --- /dev/null +++ b/cairo-native-fuzzer/src/fuzzer/fuzzer.rs @@ -0,0 +1,164 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use cairo_lang_compiler::CompilerConfig; +use cairo_lang_sierra::ids::FunctionId; +use cairo_lang_sierra::program::Program; +use cairo_lang_starknet::compile::compile_path; +use starknet_types_core::felt::Felt; + +use crate::mutator::argument_type::map_argument_type; +use crate::mutator::argument_type::ArgumentType; +use crate::mutator::basic_mutator::Mutator; +use crate::runner::runner::CairoNativeRunner; +use crate::utils::get_function_by_id; + +#[allow(dead_code)] +pub struct Fuzzer { + program_path: PathBuf, + entry_point: String, + runner: CairoNativeRunner, + sierra_program: Option>, + params: Vec, + entry_point_id: Option, + mutator: Mutator, + argument_types: Vec, +} + +impl Fuzzer { + pub fn new(program_path: PathBuf, entry_point: String) -> Self { + Self { + program_path, + entry_point, + runner: CairoNativeRunner::new(), + sierra_program: None, + params: Vec::new(), + entry_point_id: None, + mutator: Mutator::new(), + argument_types: Vec::new(), + } + } + + /// Init the fuzzer + /// - Compile Cairo code to Sierra + /// - Find the entry id + /// - Init the runner + pub fn init(&mut self) -> Result<(), String> { + self.convert_and_store_cairo_to_sierra()?; + self.entry_point_id = Some(self.find_entry_point_id()); + self.runner + .init(&self.entry_point_id, &self.sierra_program)?; + self.argument_types = self.get_function_arguments_types(); + self.generate_params(); + Ok(()) + } + + /// Compile the Cairo program to Sierra + fn convert_and_store_cairo_to_sierra(&mut self) -> Result<(), String> { + let contract = compile_path( + &self.program_path, + None, + CompilerConfig { + replace_ids: true, + ..Default::default() + }, + ) + .map_err(|e| format!("Failed to compile Cairo program: {}", e))?; + + let sierra_program = contract + .extract_sierra_program() + .map_err(|e| format!("Failed to extract Sierra program: {}", e))?; + self.sierra_program = Some(Arc::new(sierra_program)); + Ok(()) + } + + /// Find the entry point id + fn find_entry_point_id(&self) -> FunctionId { + let sierra_program = self + .sierra_program + .as_ref() + .expect("Sierra program not available"); + cairo_native::utils::find_function_id(sierra_program, &self.entry_point) + .expect(&format!("Entry point '{}' not found", self.entry_point)) + .clone() + } + + /// Returns a vector of the function parameter types + /// + /// For example, given a function with the prototype: + /// ``` + /// myfunction(a: felt252, b: felt252) -> felt252 + /// ``` + /// This function will return: + /// ``` + /// [Felt, Felt] + /// ``` + pub fn get_function_arguments_types(&self) -> Vec { + let func = match (&self.sierra_program, &self.entry_point_id) { + (Some(program), Some(entry_point_id)) => get_function_by_id(program, entry_point_id), + _ => None, + }; + + if let Some(func) = func { + let argument_types: Vec = func + .signature + .param_types + .iter() + .filter_map(|param_type| { + if let Some(debug_name) = ¶m_type.debug_name { + // Map param_type to an `ArgumentType` + // For now we only handle felt252 + return map_argument_type(debug_name); + } + None + }) + .collect(); + argument_types + } else { + Vec::new() + } + } + + /// Generate params based on the function argument types + pub fn generate_params(&mut self) { + self.params = self + .argument_types + .iter() + .map(|arg_type| match arg_type { + ArgumentType::Felt => Felt::from(0), + // TODO: Add support for other types + }) + .collect(); + } + + /// Mutate a single function parameter + pub fn mutate_param(&mut self, value: Felt) -> Felt { + // Use the Mutator to mutate the felt value + self.mutator.mutate(value) + } + + /// Mutate the parameters using the Mutator + fn mutate_params(&mut self) { + // Iterate through the current params and mutate each one + for i in 0..self.params.len() { + let mutated_value = self.mutate_param(self.params[i]); + self.params[i] = mutated_value; + } + } + + /// Run the fuzzer + /// We just use an infinite loop for now + pub fn fuzz(&mut self) -> Result<(), String> { + self.generate_params(); + loop { + match self.runner.run_program(&self.params) { + Ok(result) => { + println!("Cairo program was compiled and executed successfully."); + println!("{:?}", result); + } + Err(e) => eprintln!("Error during execution: {}", e), + } + self.mutate_params(); + } + } +} diff --git a/cairo-native-fuzzer/src/fuzzer/mod.rs b/cairo-native-fuzzer/src/fuzzer/mod.rs new file mode 100644 index 0000000..bdb5729 --- /dev/null +++ b/cairo-native-fuzzer/src/fuzzer/mod.rs @@ -0,0 +1 @@ +pub mod fuzzer; diff --git a/cairo-native-fuzzer/src/main.rs b/cairo-native-fuzzer/src/main.rs new file mode 100644 index 0000000..f27c271 --- /dev/null +++ b/cairo-native-fuzzer/src/main.rs @@ -0,0 +1,36 @@ +mod fuzzer; +mod mutator; +mod runner; +mod utils; + +use clap::Parser; +use std::path::PathBuf; + +use crate::fuzzer::fuzzer::Fuzzer; + +/// Command-line arguments for the fuzzer +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Path to the Cairo program + #[arg(short, long)] + program_path: PathBuf, + + /// Entry point of the Sierra program + #[arg(short, long)] + entry_point: String, +} + +fn main() { + let args = Args::parse(); + + let mut fuzzer = Fuzzer::new(args.program_path, args.entry_point); + + match fuzzer.init() { + Ok(()) => match fuzzer.fuzz() { + Ok(()) => println!("Fuzzing completed successfully."), + Err(e) => eprintln!("Error during fuzzing: {}", e), + }, + Err(e) => eprintln!("Error during initialization: {}", e), + } +} diff --git a/cairo-native-fuzzer/src/mutator/argument_type.rs b/cairo-native-fuzzer/src/mutator/argument_type.rs new file mode 100644 index 0000000..2af9c5a --- /dev/null +++ b/cairo-native-fuzzer/src/mutator/argument_type.rs @@ -0,0 +1,16 @@ +/// Enum representing the types of arguments that can be passed to a function +#[derive(Debug)] +pub enum ArgumentType { + Felt, + // TODO: Add support for other types +} + +/// Helper function to map argument types based on their debug names +/// This function takes a debug name string and returns the corresponding `ArgumentType` +pub fn map_argument_type(debug_name: &str) -> Option { + match debug_name { + "felt252" => Some(ArgumentType::Felt), + // TODO: Add support for other types + _ => None, + } +} diff --git a/cairo-native-fuzzer/src/mutator/basic_mutator.rs b/cairo-native-fuzzer/src/mutator/basic_mutator.rs new file mode 100644 index 0000000..04e56d5 --- /dev/null +++ b/cairo-native-fuzzer/src/mutator/basic_mutator.rs @@ -0,0 +1,256 @@ +use rand::Rng; +use starknet_types_core::felt::Felt; + +use crate::mutator::magic_values::MAGIC_VALUES; + +/// This mutator only handle felt252 +/// TODO : Handle more types +pub struct Mutator { + rng: rand::rngs::ThreadRng, + max_input_size: usize, +} + +impl Mutator { + pub fn new() -> Self { + Self { + rng: rand::thread_rng(), + max_input_size: 252, + } + } + + pub fn mutate(&mut self, felt: Felt) -> Felt { + // Perform a random mutation + let mutation_type = self.rng.gen_range(0..16); // Increase range to accommodate more strategies + match mutation_type { + 0 => self.add_small_random_value(felt), + 1 => self.subtract_small_random_value(felt), + 2 => self.flip_random_bit(felt), + 3 => self.inc_byte(felt), + 4 => self.dec_byte(felt), + // 5 => self.neg_byte(felt), + 6 => self.add_sub(felt), + 7 => self.swap(felt), + 8 => self.copy(felt), + 9 => self.inter_splice(felt), + 10 => self.magic_overwrite(felt), + 11 => self.magic_insert(felt), + 12 => self.random_overwrite(felt), + 13 => self.random_insert(felt), + 14 => self.byte_repeat_overwrite(felt), + 15 => self.byte_repeat_insert(felt), + // Fallback to the original value if something goes wrong + _ => felt, + } + } + + fn add_small_random_value(&mut self, felt: Felt) -> Felt { + // Random value between 1 and 9 + let small_value = self.rng.gen_range(1..10); + felt + Felt::from(small_value) + } + + fn subtract_small_random_value(&mut self, felt: Felt) -> Felt { + // Random value between 1 and 9 + let small_value = self.rng.gen_range(1..10); + felt - Felt::from(small_value) + } + + fn flip_random_bit(&mut self, felt: Felt) -> Felt { + // Random bit index between 0 and 251 + let bit_index = self.rng.gen_range(0..252); + let mut felt_bytes = felt.to_bytes_be(); + felt_bytes[bit_index / 8] ^= 1 << (bit_index % 8); + Felt::from_bytes_be(&felt_bytes) + } + + fn inc_byte(&mut self, felt: Felt) -> Felt { + felt + Felt::from(1) + } + + fn dec_byte(&mut self, felt: Felt) -> Felt { + felt - Felt::from(1) + } + + // fn neg_byte(&mut self, felt: Felt) -> Felt { + // -felt + // } + + fn add_sub(&mut self, felt: Felt) -> Felt { + // Add or subtract a random amount with a random endianness from a random size `u8` through `u64` + let delta = self.rng.gen_range(-100..100); // Example range + felt + Felt::from(delta) + } + + fn swap(&mut self, felt: Felt) -> Felt { + // Swap two ranges in an input buffer + let mut felt_bytes = felt.to_bytes_be(); + let len = felt_bytes.len(); + let src = self.rng.gen_range(0..len); + let dst = self.rng.gen_range(0..len); + let swap_len = self.rng.gen_range(1..=len.min(len - src.max(dst))); + + for i in 0..swap_len { + felt_bytes.swap(src + i, dst + i); + } + + Felt::from_bytes_be(&felt_bytes) + } + + fn copy(&mut self, felt: Felt) -> Felt { + // Copy bytes from one location in the input and overwrite them at another + let mut felt_bytes = felt.to_bytes_be(); + let len = felt_bytes.len(); + let src = self.rng.gen_range(0..len); + let dst = self.rng.gen_range(0..len); + let copy_len = self.rng.gen_range(1..=len.min(len - src.max(dst))); + + for i in 0..copy_len { + felt_bytes[dst + i] = felt_bytes[src + i]; + } + + Felt::from_bytes_be(&felt_bytes) + } + + fn inter_splice(&mut self, felt: Felt) -> Felt { + // Take one location of the input and splice it into another + let felt_bytes = felt.to_bytes_be(); + let len = felt_bytes.len(); + let src = self.rng.gen_range(0..len); + let dst = self.rng.gen_range(0..len); + let splice_len = self.rng.gen_range(1..=len.min(len - src.max(dst))); + + let mut new_bytes = Vec::new(); + new_bytes.extend_from_slice(&felt_bytes[..dst]); + new_bytes.extend_from_slice(&felt_bytes[src..src + splice_len]); + new_bytes.extend_from_slice(&felt_bytes[dst..]); + + // Ensure the length is exactly 32 bytes + if new_bytes.len() > 32 { + new_bytes.truncate(32); + } else if new_bytes.len() < 32 { + new_bytes.resize(32, 0); + } + + let mut array = [0u8; 32]; + array.copy_from_slice(&new_bytes); + + Felt::from_bytes_be(&array) + } + + fn magic_overwrite(&mut self, felt: Felt) -> Felt { + // Pick a random magic value + let magic_value = &MAGIC_VALUES[self.rng.gen_range(0..MAGIC_VALUES.len())]; + let mut felt_bytes = felt.to_bytes_be(); + + // Overwrite the bytes in the input with the magic value + let len = magic_value.len().min(felt_bytes.len()); + felt_bytes[..len].copy_from_slice(&magic_value[..len]); + + Felt::from_bytes_be(&felt_bytes) + } + + fn magic_insert(&mut self, felt: Felt) -> Felt { + // Pick a random magic value + let magic_value = &MAGIC_VALUES[self.rng.gen_range(0..MAGIC_VALUES.len())]; + let felt_bytes = felt.to_bytes_be(); + + // Insert the magic value at a random offset + let offset = self.rng.gen_range(0..=felt_bytes.len()); + let mut new_bytes = Vec::new(); + new_bytes.extend_from_slice(&felt_bytes[..offset]); + new_bytes.extend_from_slice(magic_value); + new_bytes.extend_from_slice(&felt_bytes[offset..]); + + // Ensure the length is exactly 32 bytes + if new_bytes.len() > 32 { + new_bytes.truncate(32); + } else if new_bytes.len() < 32 { + new_bytes.resize(32, 0); + } + + let mut array = [0u8; 32]; + array.copy_from_slice(&new_bytes); + + Felt::from_bytes_be(&array) + } + + fn random_overwrite(&mut self, felt: Felt) -> Felt { + // Overwrite a random offset of the input with random bytes + let mut felt_bytes = felt.to_bytes_be(); + let offset = self.rng.gen_range(0..felt_bytes.len()); + let amount = self.rng.gen_range(1..=felt_bytes.len() - offset); + + for i in offset..offset + amount { + felt_bytes[i] = self.rng.gen(); + } + + Felt::from_bytes_be(&felt_bytes) + } + + fn random_insert(&mut self, felt: Felt) -> Felt { + // Insert random bytes into a random offset in the input + let felt_bytes = felt.to_bytes_be(); + let offset = self.rng.gen_range(0..=felt_bytes.len()); + let amount = self + .rng + .gen_range(0..=self.max_input_size - felt_bytes.len()); + + let mut new_bytes = Vec::new(); + new_bytes.extend_from_slice(&felt_bytes[..offset]); + new_bytes.extend(std::iter::repeat(self.rng.gen::()).take(amount)); + new_bytes.extend_from_slice(&felt_bytes[offset..]); + + // Ensure the length is exactly 32 bytes + if new_bytes.len() > 32 { + new_bytes.truncate(32); + } else if new_bytes.len() < 32 { + new_bytes.resize(32, 0); + } + + let mut array = [0u8; 32]; + array.copy_from_slice(&new_bytes); + + Felt::from_bytes_be(&array) + } + + fn byte_repeat_overwrite(&mut self, felt: Felt) -> Felt { + // Find a byte and repeat it multiple times by overwriting the data after it + let mut felt_bytes = felt.to_bytes_be(); + let offset = self.rng.gen_range(0..felt_bytes.len()); + let amount = self.rng.gen_range(1..=felt_bytes.len() - offset); + + let val = felt_bytes[offset]; + for i in offset + 1..offset + amount { + felt_bytes[i] = val; + } + + Felt::from_bytes_be(&felt_bytes) + } + + fn byte_repeat_insert(&mut self, felt: Felt) -> Felt { + // Find a byte and repeat it multiple times by splicing a random amount of the byte in + let felt_bytes = felt.to_bytes_be(); + let offset = self.rng.gen_range(0..felt_bytes.len()); + let amount = self + .rng + .gen_range(1..=self.max_input_size - felt_bytes.len()); + + let val = felt_bytes[offset]; + let mut new_bytes = Vec::new(); + new_bytes.extend_from_slice(&felt_bytes[..offset]); + new_bytes.extend(std::iter::repeat(val).take(amount)); + new_bytes.extend_from_slice(&felt_bytes[offset..]); + + // Ensure the length is exactly 32 bytes + if new_bytes.len() > 32 { + new_bytes.truncate(32); + } else if new_bytes.len() < 32 { + new_bytes.resize(32, 0); + } + + let mut array = [0u8; 32]; + array.copy_from_slice(&new_bytes); + + Felt::from_bytes_be(&array) + } +} diff --git a/cairo-native-fuzzer/src/mutator/magic_values.rs b/cairo-native-fuzzer/src/mutator/magic_values.rs new file mode 100644 index 0000000..3acb062 --- /dev/null +++ b/cairo-native-fuzzer/src/mutator/magic_values.rs @@ -0,0 +1,248 @@ +//! A file containing a bunch of magic values, from honggfuzz +/* + * + * Authors: + * Robert Swiecki + * Brandon Falk + * + * Copyright 2010-2018 by Google Inc. All Rights Reserved. + * Copyright 2020 by Brandon Falk + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +/// Magic values of various sizes and endiannesses +pub const MAGIC_VALUES: &[&[u8]] = &[ + b"\x00", + b"\x01", + b"\x02", + b"\x03", + b"\x04", + b"\x05", + b"\x06", + b"\x07", + b"\x08", + b"\x09", + b"\x0a", + b"\x0b", + b"\x0c", + b"\x0d", + b"\x0e", + b"\x0f", + b"\x10", + b"\x20", + b"\x40", + b"\x7e", + b"\x7f", + b"\x80", + b"\x81", + b"\xc0", + b"\xfe", + b"\xff", + b"\x00\x00", + b"\x01\x01", + b"\x80\x80", + b"\xff\xff", + b"\x00\x01", + b"\x00\x02", + b"\x00\x03", + b"\x00\x04", + b"\x00\x05", + b"\x00\x06", + b"\x00\x07", + b"\x00\x08", + b"\x00\x09", + b"\x00\x0a", + b"\x00\x0b", + b"\x00\x0c", + b"\x00\x0d", + b"\x00\x0e", + b"\x00\x0f", + b"\x00\x10", + b"\x00\x20", + b"\x00\x40", + b"\x00\x7e", + b"\x00\x7f", + b"\x00\x80", + b"\x00\x81", + b"\x00\xc0", + b"\x00\xfe", + b"\x00\xff", + b"\x7e\xff", + b"\x7f\xff", + b"\x80\x00", + b"\x80\x01", + b"\xff\xfe", + b"\x00\x00", + b"\x01\x00", + b"\x02\x00", + b"\x03\x00", + b"\x04\x00", + b"\x05\x00", + b"\x06\x00", + b"\x07\x00", + b"\x08\x00", + b"\x09\x00", + b"\x0a\x00", + b"\x0b\x00", + b"\x0c\x00", + b"\x0d\x00", + b"\x0e\x00", + b"\x0f\x00", + b"\x10\x00", + b"\x20\x00", + b"\x40\x00", + b"\x7e\x00", + b"\x7f\x00", + b"\x80\x00", + b"\x81\x00", + b"\xc0\x00", + b"\xfe\x00", + b"\xff\x00", + b"\xff\x7e", + b"\xff\x7f", + b"\x00\x80", + b"\x01\x80", + b"\xfe\xff", + b"\x00\x00\x00\x00", + b"\x01\x01\x01\x01", + b"\x80\x80\x80\x80", + b"\xff\xff\xff\xff", + b"\x00\x00\x00\x01", + b"\x00\x00\x00\x02", + b"\x00\x00\x00\x03", + b"\x00\x00\x00\x04", + b"\x00\x00\x00\x05", + b"\x00\x00\x00\x06", + b"\x00\x00\x00\x07", + b"\x00\x00\x00\x08", + b"\x00\x00\x00\x09", + b"\x00\x00\x00\x0a", + b"\x00\x00\x00\x0b", + b"\x00\x00\x00\x0c", + b"\x00\x00\x00\x0d", + b"\x00\x00\x00\x0e", + b"\x00\x00\x00\x0f", + b"\x00\x00\x00\x10", + b"\x00\x00\x00\x20", + b"\x00\x00\x00\x40", + b"\x00\x00\x00\x7e", + b"\x00\x00\x00\x7f", + b"\x00\x00\x00\x80", + b"\x00\x00\x00\x81", + b"\x00\x00\x00\xc0", + b"\x00\x00\x00\xfe", + b"\x00\x00\x00\xff", + b"\x7e\xff\xff\xff", + b"\x7f\xff\xff\xff", + b"\x80\x00\x00\x00", + b"\x80\x00\x00\x01", + b"\xff\xff\xff\xfe", + b"\x00\x00\x00\x00", + b"\x01\x00\x00\x00", + b"\x02\x00\x00\x00", + b"\x03\x00\x00\x00", + b"\x04\x00\x00\x00", + b"\x05\x00\x00\x00", + b"\x06\x00\x00\x00", + b"\x07\x00\x00\x00", + b"\x08\x00\x00\x00", + b"\x09\x00\x00\x00", + b"\x0a\x00\x00\x00", + b"\x0b\x00\x00\x00", + b"\x0c\x00\x00\x00", + b"\x0d\x00\x00\x00", + b"\x0e\x00\x00\x00", + b"\x0f\x00\x00\x00", + b"\x10\x00\x00\x00", + b"\x20\x00\x00\x00", + b"\x40\x00\x00\x00", + b"\x7e\x00\x00\x00", + b"\x7f\x00\x00\x00", + b"\x80\x00\x00\x00", + b"\x81\x00\x00\x00", + b"\xc0\x00\x00\x00", + b"\xfe\x00\x00\x00", + b"\xff\x00\x00\x00", + b"\xff\xff\xff\x7e", + b"\xff\xff\xff\x7f", + b"\x00\x00\x00\x80", + b"\x01\x00\x00\x80", + b"\xfe\xff\xff\xff", + b"\x00\x00\x00\x00\x00\x00\x00\x00", + b"\x01\x01\x01\x01\x01\x01\x01\x01", + b"\x80\x80\x80\x80\x80\x80\x80\x80", + b"\xff\xff\xff\xff\xff\xff\xff\xff", + b"\x00\x00\x00\x00\x00\x00\x00\x01", + b"\x00\x00\x00\x00\x00\x00\x00\x02", + b"\x00\x00\x00\x00\x00\x00\x00\x03", + b"\x00\x00\x00\x00\x00\x00\x00\x04", + b"\x00\x00\x00\x00\x00\x00\x00\x05", + b"\x00\x00\x00\x00\x00\x00\x00\x06", + b"\x00\x00\x00\x00\x00\x00\x00\x07", + b"\x00\x00\x00\x00\x00\x00\x00\x08", + b"\x00\x00\x00\x00\x00\x00\x00\x09", + b"\x00\x00\x00\x00\x00\x00\x00\x0a", + b"\x00\x00\x00\x00\x00\x00\x00\x0b", + b"\x00\x00\x00\x00\x00\x00\x00\x0c", + b"\x00\x00\x00\x00\x00\x00\x00\x0d", + b"\x00\x00\x00\x00\x00\x00\x00\x0e", + b"\x00\x00\x00\x00\x00\x00\x00\x0f", + b"\x00\x00\x00\x00\x00\x00\x00\x10", + b"\x00\x00\x00\x00\x00\x00\x00\x20", + b"\x00\x00\x00\x00\x00\x00\x00\x40", + b"\x00\x00\x00\x00\x00\x00\x00\x7e", + b"\x00\x00\x00\x00\x00\x00\x00\x7f", + b"\x00\x00\x00\x00\x00\x00\x00\x80", + b"\x00\x00\x00\x00\x00\x00\x00\x81", + b"\x00\x00\x00\x00\x00\x00\x00\xc0", + b"\x00\x00\x00\x00\x00\x00\x00\xfe", + b"\x00\x00\x00\x00\x00\x00\x00\xff", + b"\x7e\xff\xff\xff\xff\xff\xff\xff", + b"\x7f\xff\xff\xff\xff\xff\xff\xff", + b"\x80\x00\x00\x00\x00\x00\x00\x00", + b"\x80\x00\x00\x00\x00\x00\x00\x01", + b"\xff\xff\xff\xff\xff\xff\xff\xfe", + b"\x00\x00\x00\x00\x00\x00\x00\x00", + b"\x01\x00\x00\x00\x00\x00\x00\x00", + b"\x02\x00\x00\x00\x00\x00\x00\x00", + b"\x03\x00\x00\x00\x00\x00\x00\x00", + b"\x04\x00\x00\x00\x00\x00\x00\x00", + b"\x05\x00\x00\x00\x00\x00\x00\x00", + b"\x06\x00\x00\x00\x00\x00\x00\x00", + b"\x07\x00\x00\x00\x00\x00\x00\x00", + b"\x08\x00\x00\x00\x00\x00\x00\x00", + b"\x09\x00\x00\x00\x00\x00\x00\x00", + b"\x0a\x00\x00\x00\x00\x00\x00\x00", + b"\x0b\x00\x00\x00\x00\x00\x00\x00", + b"\x0c\x00\x00\x00\x00\x00\x00\x00", + b"\x0d\x00\x00\x00\x00\x00\x00\x00", + b"\x0e\x00\x00\x00\x00\x00\x00\x00", + b"\x0f\x00\x00\x00\x00\x00\x00\x00", + b"\x10\x00\x00\x00\x00\x00\x00\x00", + b"\x20\x00\x00\x00\x00\x00\x00\x00", + b"\x40\x00\x00\x00\x00\x00\x00\x00", + b"\x7e\x00\x00\x00\x00\x00\x00\x00", + b"\x7f\x00\x00\x00\x00\x00\x00\x00", + b"\x80\x00\x00\x00\x00\x00\x00\x00", + b"\x81\x00\x00\x00\x00\x00\x00\x00", + b"\xc0\x00\x00\x00\x00\x00\x00\x00", + b"\xfe\x00\x00\x00\x00\x00\x00\x00", + b"\xff\x00\x00\x00\x00\x00\x00\x00", + b"\xff\xff\xff\xff\xff\xff\xff\x7e", + b"\xff\xff\xff\xff\xff\xff\xff\x7f", + b"\x00\x00\x00\x00\x00\x00\x00\x80", + b"\x01\x00\x00\x00\x00\x00\x00\x80", + b"\xfe\xff\xff\xff\xff\xff\xff\xff", +]; diff --git a/cairo-native-fuzzer/src/mutator/mod.rs b/cairo-native-fuzzer/src/mutator/mod.rs new file mode 100644 index 0000000..fb993d0 --- /dev/null +++ b/cairo-native-fuzzer/src/mutator/mod.rs @@ -0,0 +1,3 @@ +pub mod argument_type; +pub mod basic_mutator; +pub mod magic_values; diff --git a/cairo-native-fuzzer/src/mutator/mutator.rs b/cairo-native-fuzzer/src/mutator/mutator.rs new file mode 100644 index 0000000..b1b730d --- /dev/null +++ b/cairo-native-fuzzer/src/mutator/mutator.rs @@ -0,0 +1,1018 @@ +//! Basic fuzzer mutation strategies, largely ported from honggfuzz +/* + * + * Authors: + * Robert Swiecki + * Brandon Falk + * + * Copyright 2010-2018 by Google Inc. All Rights Reserved. + * Copyright 2020 by Brandon Falk + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. You may obtain + * a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ +#![allow(dead_code)] +extern crate alloc; + +use super::magic_values; +use alloc::vec::Vec; +use magic_values::MAGIC_VALUES; +/// An empty database that never returns an input, useful for fuzzers without +/// corpuses or input databases. +pub struct EmptyDatabase; + +use starknet_types_core::felt::Felt; + +// Define a trait for converting to a byte array +trait ToBeBytes { + fn to_be_bytes(&self) -> [u8; 32]; +} + +// Implement the ToBeBytes trait for the Felt type +impl ToBeBytes for Felt { + fn to_be_bytes(&self) -> [u8; 32] { + let (_, bytes) = self.to_bigint().to_bytes_be(); + let mut array = [0u8; 32]; + array[32 - bytes.len()..].copy_from_slice(&bytes); + array + } +} + +impl InputDatabase for EmptyDatabase { + fn num_inputs(&self) -> usize { + 0 + } + fn input(&self, _idx: usize) -> Option { + None + } +} + +/// Routines to generically access a corpus/input database for a fuzzer. It's +/// up to the database to implement this trait, allowing generic access to +/// the number of inputs in the database, and accessors for a specific input. +/// +/// The inputs should have zero-indexed IDs such that any input in the range of +/// [0, self.num_inputs()) should be a valid input. +/// +/// If the `idx` does not lead to the same input each run, the determinism of +/// the mutator is unstable and can produce different results across different +/// runs. +pub trait InputDatabase { + /// Get the number of inputs in the database + fn num_inputs(&self) -> usize; + + /// Get an input with a specific zero-index identifier + /// If the `idx` is invalid or otherwise not available, this returns `None` + fn input(&self, idx: usize) -> Option; +} + +/// A basic random number generator based on xorshift64 with 64-bits of state +struct Rng { + /// The RNG's seed and state + seed: u64, + + /// If set, `rand_exp` behaves the same as `rand` + exp_disabled: bool, +} + +impl Rng { + /// Generate a random number + #[inline] + fn next(&mut self) -> u64 { + let val = self.seed; + self.seed ^= self.seed << 13; + self.seed ^= self.seed >> 17; + self.seed ^= self.seed << 43; + val + } + + /// Generates a random number with uniform distribution in the range of + /// [min, max] + #[inline] + fn rand(&mut self, min: usize, max: usize) -> usize { + // Make sure the range is sane + assert!(max >= min, "Bad range specified for rand()"); + + // If there is no range, just return `min` + if min == max { + return min; + } + + // If the range is unbounded, just return a random number + if min == 0 && max == core::usize::MAX { + return self.next() as usize; + } + + // Pick a random number in the range + min + (self.next() as usize % (max - min + 1)) + } + + /// Generates a random number with exponential distribution in the range of + /// [min, max] with a worst case deviation from uniform of 0.5x. Meaning + /// this will always return uniform at least half the time. + #[inline] + fn rand_exp(&mut self, min: usize, max: usize) -> usize { + // If exponential random is disabled, fall back to uniform + if self.exp_disabled { + return self.rand(min, max); + } + + if self.rand(0, 1) == 0 { + // Half the time, provide uniform + self.rand(min, max) + } else { + // Pick an exponentially difficult random number + let x = self.rand(min, max); + self.rand(min, x) + } + } +} + +/// A mutator, a playground for corrupting the public `input` vector when +/// `mutate` is invoked +pub struct Mutator { + /// Input vector to mutate, this is just an entire input files bytes + /// + /// It is strongly recommended that you do `input.clear()` and + /// `input.extend_from_slice()` to update this buffer, to prevent the + /// backing from being deallocated and reallocated. + pub input: Vec, + pub types: Vec, + /// If non-zero length, this contains a list of valid indicies into + /// `input`, indicating which bytes of the input should mutated. This often + /// comes from instrumentation like access tracking or taint tracking to + /// indicate which parts of the input are used. This will prevent us from + /// corrupting parts of the input which have zero effect on the program. + /// + /// It's possible you can have this take any meaning you want, all it does + /// is limit the corruption/splicing locations to the indicies in this + /// vector. Feel free to change this to have different meanings, like + /// indicate indicies which are used in comparison instructions! + /// + /// Since we use `rand_exp` to pick from this, this list should remain + /// sorted for best behavior. If you cannot sort this, you should probably + /// change the behaviors of `rand_offset` to be uniform + /// + /// It is strongly recommended that you do `accessed.clear()` and + /// `accessed.extend_from_slice()` to update this buffer, to prevent the + /// backing from being deallocated and reallocated. + pub accessed: Vec, + + /// Maximum size to allow inputs to expand to + max_input_size: usize, + + /// The random number generator used for mutations + rng: Rng, + + /// The mutations should prefer creating ASCII-printable characters + printable: bool, +} + +/// A byte corruption skeleton which has user-supplied corruption logic which +/// will be used to mutate a byte which is passed to it +/// +/// $corrupt takes a &mut self, `u8` as arguments, and returns a `u8` as the +/// corrupted value. +macro_rules! byte_corruptor { + ($func:ident, $corrupt:expr) => { + /// Corrupt a byte in the input + fn $func(&mut self) { + // Only corrupt a byte if there are bytes present + if !self.input.is_empty() { + // Pick a random byte offset + let offset = self.rand_offset(); + + // Perform the corruption + self.input[offset] = ($corrupt)(self, self.input[offset].clone()).into(); + } + } + }; +} + +impl Mutator { + /// Create a new mutator + pub fn new() -> Self { + Mutator { + input: Vec::new(), + types: Vec::new(), + accessed: Vec::new(), + max_input_size: 1024, + printable: false, + rng: Rng { + seed: 0x12640367f4b7ea35, + exp_disabled: false, + }, + } + } + + pub fn fix_inputs_types(&mut self) { + let mut idx = 0; + self.types.clone().into_iter().for_each(|input_type| { + let bits_to_save; + match input_type.as_str() { + "core::integer::u8" => { + bits_to_save = 8; + } + "core::integer::u16" => { + bits_to_save = 16; + } + "core::integer::u32" => { + bits_to_save = 32; + } + "core::integer::u64" => { + bits_to_save = 64; + } + "core::integer::u128" => { + bits_to_save = 128; + } + "core::integer::u256" => { + bits_to_save = 256; // still need to fix for this + } + "core::Felt" => { + bits_to_save = 252; + } + + _ => { + bits_to_save = 252; // Should be a Todo! but since the runner takes a Felt vector, it's a good idea to send random data + } + } + if bits_to_save != 256 && bits_to_save != 252 { + let mut new_value = self.input[idx].to_be_bytes(); + for i in 0..new_value.len() - (bits_to_save / 8) { + new_value[i] = 0; + } + self.input[idx] = Felt::from_bytes_be(&new_value); + } + idx += 1; + }); + } + /// Set whether or not this mutator should produce only ASCII-printable + /// characters. + /// + /// If non-printable characters are used in part of the corpus or existing + /// input, they may be inherited and still exist in the output of the + /// fuzzer. + pub fn printable(mut self, printable: bool) -> Self { + self.printable = printable; + self + } + + /// Allows enabling and disabling of exponential random in the fuzzer. If + /// disabled, all random selections will be uniform. + pub fn rand_exp(mut self, exponential_random: bool) -> Self { + self.rng.exp_disabled = !exponential_random; + self + } + + /// Sets the seed for the internal RNG + pub fn seed(mut self, seed: u64) -> Self { + self.rng.seed = seed ^ 0x12640367f4b7ea35; + self + } + + pub fn types(mut self, types: Vec) -> Self { + self.types.clone_from(&types); + self + } + /// Sets the maximum input size + pub fn max_input_size(mut self, size: usize) -> Self { + self.max_input_size = size; + self + } + + /// Performs standard mutation of an the input + pub fn mutate(&mut self, mutations: usize, inputs: &T) { + /// List of mutation strategies which do not require an input database + const STRATEGIES: &[fn(&mut Mutator)] = &[ + //Mutator::shrink, + //Mutator::expand, + Mutator::inc_byte, + Mutator::dec_byte, + Mutator::neg_byte, + Mutator::add_sub, + //Mutator::set, + Mutator::swap, + Mutator::copy, + Mutator::inter_splice, + //Mutator::insert_rand, + Mutator::overwrite_rand, + //Mutator::byte_repeat_overwrite, + //Mutator::byte_repeat_insert, + Mutator::magic_overwrite, + //Mutator::magic_insert, + Mutator::random_overwrite, + //Mutator::random_insert, + Mutator::splice_overwrite, + //Mutator::splice_insert, + ]; + + // Save the old state of the exponential random and randomly disable + // the exponential random + let old_exp_state = self.rng.exp_disabled; + if self.rng.rand(0, 1) == 0 { + self.rng.exp_disabled = true; + } + + for _ in 0..mutations { + // Pick a random mutation strategy + let sel = self.rng.rand(0, STRATEGIES.len() - 1); + + // Get the strategy + let strat = STRATEGIES[sel]; + + // Determine if we're doing an overwrite or insert splice strategy, + // as we have to handle these a bit specially due to the use of + // a generic input database. + let splice_overwrite = + core::ptr::eq(strat as *const (), Mutator::splice_overwrite as *const ()); + let splice_insert = + core::ptr::eq(strat as *const (), Mutator::splice_insert as *const ()); + + // Handle special-case mutations which need input database access + if splice_overwrite || splice_insert { + // Get the number of inputs in the database + let dblen = inputs.num_inputs(); + if dblen == 0 { + continue; + } + + // Select a random input + if let Some(inp) = inputs.input(self.rng.rand(0, dblen - 1)) { + // Nothing to splice for an empty input + /* if inp.is_empty() { + continue; + } */ + println!("Inputs selected => {:?}", inp); + // Pick a random offset and length from the input which + // we want to use for splicing + let donor_offset = self.rng.rand_exp(0, inp.to_string().len() - 1); + let donor_length = self.rng.rand_exp(1, inp.to_string().len() - donor_offset); + + if splice_overwrite { + // Cannot overwrite an empty input + if self.input.is_empty() { + continue; + } + + // Find an offset to overwrite in our input + let offset = self.rand_offset(); + let length = core::cmp::min(donor_length, self.input.len() - offset); + + println!("inputs to bytes => {:?}", &inp.to_be_bytes()); + println!( + "inputs slices => {:?}", + &inp.to_be_bytes()[donor_offset..donor_offset + length] + ); + // Overwrite it! + self.overwrite( + offset, + &inp.to_be_bytes()[donor_offset..donor_offset + length], + ); + } else { + // Find an offset to insert at in our input + let offset = self.rand_offset_int(true); + // Insert! + self.insert(offset, inp); + } + } + } else { + // Run the mutation strategy + strat(self); + } + } + + // Restore exponential random state to the old state + self.rng.exp_disabled = old_exp_state; + self.fix_inputs_types(); + } + + /// Pick a random offset in the input to corrupt. Any mutation + /// strategy which needs to pick a random byte should us this, such that + /// a bias can be applied to the offsets and automatically affect all + /// aspects of the fuzzer + /// + /// If `insert` is set to `true`, then the offset being returned is + /// expected to be for insertion. This means that we'll have a chance of + /// returning an offset before or after sections of the input. For example, + /// we may return an index which is `self.input.len()`, which would allow + /// for the user to insert data at the end of the input. + /// + /// If the input is zero length, then this will return a zero. Thus, it + /// is up to the caller to know whether or not the input size being zero + /// matters. For example, flipping a byte at offset 0 on a 0 size input + /// would cause a panic, but inserting at offset 0 may be desired. + fn rand_offset_int(&mut self, plus_one: bool) -> usize { + if !self.accessed.is_empty() { + // If we have an accessed list, use an index from the list of known + // accessed bytes from the input. This prevents us from corrupting + // a byte which cannot possibly influence the binary, as it is + // never read during the fuzz case. + self.accessed[self.rng.rand_exp(0, self.accessed.len() - 1)] + } else if !self.input.is_empty() { + // We have no accessed list, just return a random index + self.rng + .rand_exp(0, self.input.len() - (!plus_one) as usize) + } else { + // Input is entirely empty, just return index 0 such that + // things that insert into the input know that they should + // just insert at 0. + 0 + } + } + + /// Generate a random offset, see `rand_offset_int` for more info + fn rand_offset(&mut self) -> usize { + self.rand_offset_int(false) + } + + /// Dummy function, just used for RNG selection, logic is done in `mutate` + fn splice_overwrite(&mut self) {} + + /// Dummy function, just used for RNG selection, logic is done in `mutate` + fn splice_insert(&mut self) {} + + /// Randomly delete a chunk of the input + fn shrink(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick a random offset to remove data at + let offset = self.rand_offset(); + + // Compute the number of bytes we could remove from this offset + let can_remove = self.input.len() - offset; + + // Compute a maximum number of bytes to remove + let max_remove = if self.rng.rand(0, 15) != 0 { + // 15 in 16 chance of removing at most 16 bytes, this limits the + // amount we remove in the most common case + core::cmp::min(16, can_remove) + } else { + // 1 in 16 chance of removing a random amount of bytes to the end + // of the input + can_remove + }; + + // Pick the amount of bytes to remove + let to_remove = self.rng.rand_exp(1, max_remove); + + // Remove the bytes from the input + let _ = self.input.drain(offset..offset + to_remove); + } + + /// Make space in the input, filling with zeros if `printable` is not + /// set, and if it is set, then fill with spaces. + fn expand(&mut self) { + // Nothing to do if the input size is >= the cap + if self.input.len() >= self.max_input_size { + return; + } + + // Pick a random offset to expand at + let offset = self.rand_offset_int(true); + + // Calculate a maximum expansion size based on our maximum allowed + // input size + let max_expand = self.max_input_size - self.input.len(); + + // Compute a maximum number of expansion bytes + let max_expand = if self.rng.rand(0, 15) != 0 { + // 15 in 16 chance of capping expansion to 16 bytes + core::cmp::min(16, max_expand) + } else { + // 1 in 16 chance of uncapped expansion + max_expand + }; + + // Create what to expand with + let iter = if self.printable { + core::iter::repeat(Felt::from(b' ')).take(self.rng.rand_exp(1, max_expand)) + } else { + core::iter::repeat(Felt::from(b'\0')).take(self.rng.rand_exp(1, max_expand)) + }; + + // Expand at `offset` with `iter` + self.input.splice(offset..offset, iter); + } + + /// Add or subtract a random amount with a random endianness from a random + /// size `u8` through `u64` + fn add_sub(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick an offset to corrupt at + let offset = self.rand_offset(); + + // Get the remaining number of bytes in the input + let remain = self.input.len() - offset; + + // Pick a random size of the add or subtract as a 1, 2, 4, or 8 byte + // signed integer + let intsize = match remain { + 1..=1 => 1, + 2..=3 => 1 << self.rng.rand(0, 1), + 4..=7 => 1 << self.rng.rand(0, 2), + 8..=core::usize::MAX => 1 << self.rng.rand(0, 3), + _ => unreachable!(), + }; + + // Determine the maximum number to add or subtract + let range = match intsize { + 1 => 16, + 2 => 4096, + 4 => 1024 * 1024, + 8 => 256 * 1024 * 1024, + _ => unreachable!(), + }; + + // Convert the range to a random number from [-range, range] + let delta = self.rng.rand(0, range * 2) as i32 - range as i32; + + /// Macro to mutate bytes in the input as a `$ty` + macro_rules! mutate { + ($ty:ty) => {{ + // Interpret the `offset` as a `$ty` + let tmp = self.input[offset].clone(); + + // Apply the delta, interpreting the bytes as a random + // endianness + let tmp = if self.rng.rand(0, 1) == 0 { + (Felt::from(delta) + Felt::from(tmp)) + } else { + //tmp.swap_bytes().wrapping_add(delta as $ty).swap_bytes() + Felt::from(delta) + }; + + // Write the new value out to the input + self.input[offset] += Felt::from(tmp); + }}; + } + + // Apply the delta to the offset + match intsize { + 1 => mutate!(u8), + 2 => mutate!(u16), + 4 => mutate!(u32), + 8 => mutate!(u64), + 16 => mutate!(Felt), + _ => unreachable!(), + }; + } + + /// Randomly replace a sequence of bytes with the same random character + /// repeated a random amount of times + fn set(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick offset to memset at + let offset = self.rand_offset(); + + // Pick random length to remainder of input + let len = self.rng.rand_exp(1, self.input.len() - offset); + + // Pick the value to memset + let chr = if self.printable { + (self.rng.rand(0, 94) + 32) as u8 + } else { + self.rng.rand(0, 255) as u8 + }; + + // Replace the selected bytes at the offset with `chr` + self.input[offset..offset + len] + .iter_mut() + .for_each(|x| *x = Felt::from(chr)); + } + + /// Swap two ranges in an input buffer + fn swap_ranges(vec: &mut [Felt], mut offset1: usize, mut offset2: usize, mut len: usize) { + if offset1 < offset2 && offset1 + len >= offset2 { + // The ranges have the following layout here: + // [o1--------] + // [o2--------] + let tail = offset2 - offset1; + // Copy the tail from offset1 into offset2 + // [o1-][tail1] + // [o2-][tail2] + // This needs to happen in the reverse order so that the later + // values at offset1 are not mangled in the process of copying. + // Same as memmove. + for ii in (tail..len).rev() { + vec[offset2 + ii] = vec[offset1 + ii].clone(); + } + + // After this, the layout is the following: + // [o1-][xxxxx] + // [o2-][tail1] + len = tail; + } else if offset2 < offset1 && offset2 + len >= offset1 { + // The ranges have the following layout here: + // [o1--------] + // [o2--------] + let head = len - (offset1 - offset2); + // Copy the head from offset1 into offset2 + // [head1][o1-] + // [head2][o2-] + for ii in 0..head { + vec[offset2 + ii] = vec[offset1 + ii].clone(); + } + + // After this, the layout is the following: + // [xxxxx][o1-] + // [head1][o2-] + offset1 += head; + offset2 += head; + len -= head; + } + + // At this point, the ranges are non-overlapping + // and the swap can be done in a naive way. + for ii in 0..len { + vec.swap(offset1 + ii, offset2 + ii); + } + } + + /// Swap two difference sequence of bytes in the input to different places + fn swap(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick two random ranges in the input and calculate the remaining + // bytes for them + let src = self.rand_offset(); + let srcrem = self.input.len() - src; + let dst = self.rand_offset(); + let dstrem = self.input.len() - dst; + + // Pick a random length up to the max for both offsets + let len = self.rng.rand_exp(1, core::cmp::min(srcrem, dstrem)); + + // Swap the ranges of bytes + Self::swap_ranges(&mut self.input, src, dst, len); + } + + /// Insert `buf` at `offset` in the input. `buf` will be truncated to + /// ensure the input stays within the maximum input size + fn insert(&mut self, offset: usize, buf: Felt) { + self.input.insert(offset, buf.clone()); + // Make sure we don't expand past the maximum input size + //let len = core::cmp::min(buf.len(), self.max_input_size - self.input.len()); + + // Splice in the `buf` + //self.input + // .splice(offset..offset, buf[..len].iter().copied()); + } + + /// Overwrite the bytes in the input with `buf` at `offset`. If `buf` + /// goes out of bounds of the input the `buf` will be truncated and the + /// copy will stop. + fn overwrite(&mut self, offset: usize, buf: &[u8]) { + // Get the slice that we may overwrite + let target = &mut self.input[offset..].to_vec(); + // Get the length to overwrite + let len = core::cmp::min(buf.len(), target.len()); + let mut i = 0; + while i < len { + target[i] = Felt::from(buf[i]); + i += 1; + } + // Overwrite the bytes + //target.to[..len].copy_from_slice(&buf[..len]); + } + + /// Take the bytes from `source` for `len` bytes in the input, and insert + /// a copy of them at `dest` + fn insert_inplace(&mut self, source: usize, len: usize, dest: usize) { + // Nothing to do + if len == 0 || source == dest { + return; + } + + // Cap the insertion to the max input size + let len = core::cmp::min(len, self.max_input_size - self.input.len()); + + // Create an interator to splice into the input + let rep = core::iter::repeat(Felt::from(b'\0')).take(len); + + // Expand at `dest` with `rep`, making room for the copy + self.input.splice(dest..dest, rep); + + // Determine where the splice occurred + let split_point = dest.saturating_sub(source).min(len); + + for ii in 0..split_point { + self.input[dest + ii] = self.input[source + ii].clone(); + } + + for ii in split_point..len { + self.input[dest + ii] = self.input[source + ii + len].clone(); + } + } + + /// Take the bytes from `source` for `len` bytes in the input, and copy + /// them to `dest` + fn overwrite_inplace(&mut self, source: usize, len: usize, dest: usize) { + // Nothing to do + if len == 0 || source == dest { + return; + } + + if source < dest { + // Copy forwards + for ii in 0..len { + self.input[dest + ii] = self.input[source + ii].clone(); + } + } else { + // Copy backwards + for ii in (0..len).rev() { + self.input[dest + ii] = self.input[source + ii].clone(); + } + } + } + + /// Copy bytes from one location in the input and overwrite them at another + /* fn convert<'a>(arr: &'a [&'a [u8; 32]]) -> &'a [u8] { + let size = arr.iter().map(|a| a.len()).sum(); + let ptr = arr.as_ptr() as *const u8; + + // SAFETY: We have ensured that the pointer and length are valid + unsafe { std::slice::from_raw_parts(ptr, size) } + } */ + + /// location in the input + fn copy(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick a source and destination for a copy + let src = self.rand_offset(); + let srcrem = self.input.len() - src; + let dst = self.rand_offset(); + let dstrem = self.input.len() - dst; + + // Pick a random length up to the max for both offsets + let len = self.rng.rand_exp(1, core::cmp::min(srcrem, dstrem)); + + // Perform a copy inplace in the input + self.overwrite_inplace(src, len, dst); + } + + /// Take one location of the input and splice it into another + fn inter_splice(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick a source and destination for an insertion + let src = self.rand_offset(); + let srcrem = self.input.len() - src; + let dst = self.rand_offset_int(true); + + // Pick a random length + let len = self.rng.rand_exp(1, srcrem); + + // Perform an insertion inplace in the input + self.insert_inplace(src, len, dst); + } + + /// Create 1 or 2 random bytes and insert them into the input + fn insert_rand(&mut self) { + // Pick some random values + let bytes = if self.printable { + [ + Felt::from(self.rng.rand(0, 94) + 32), + Felt::from(self.rng.rand(0, 94) + 32), + ] + } else { + [ + Felt::from(self.rng.rand(0, 255)), + Felt::from(self.rng.rand(0, 255)), + ] + }; + + // Pick a random offset + let offset = self.rand_offset_int(true); + self.insert(offset, bytes[0].clone()); + // Pick a random offset + let offset = self.rand_offset_int(true); + self.insert(offset, bytes[1].clone()); + } + + /// Create 1 or 2 random bytes and overwrite them at a location in the + /// input + fn overwrite_rand(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick some random values + let bytes = if self.printable { + [ + Felt::from(self.rng.rand(0, 94) + 32).to_be_bytes(), + Felt::from(self.rng.rand(0, 94) + 32).to_be_bytes(), + ] + } else { + [ + Felt::from(self.rng.rand(0, 255)).to_be_bytes(), + Felt::from(self.rng.rand(0, 255)).to_be_bytes(), + ] + }; + + // Pick a random offset and length + let offset = self.rand_offset(); + let len = core::cmp::min(self.input.len() - offset, 2); + let len = self.rng.rand(1, len); + + let test = &bytes[..len]; + + let size = test.iter().map(|a| a.len()).sum(); + let ptr = test.as_ptr() as *const u8; + + // SAFETY: We have ensured that the pointer and length are valid + unsafe { + let new_arr = std::slice::from_raw_parts(ptr, size); + // Overwrite the bytes + self.overwrite(offset, new_arr); + }; + } + + /// Find a byte and repeat it multiple times by overwriting the data after + /// it + fn byte_repeat_overwrite(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick a random offset + let offset = self.rand_offset(); + + // Pick an amount to repeat + let amount = self.rng.rand_exp(1, self.input.len() - offset); + + // Get the old value and repeat it + let val = self.input[offset].clone(); + self.input[offset + 1..offset + amount] + .iter_mut() + .for_each(|x| *x = val.clone()); + } + + /// Find a byte and repeat it multiple times by splicing a random amount + /// of the byte in + fn byte_repeat_insert(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick a random offset + let offset = self.rand_offset(); + + // Pick an amount to repeat, subtracting one to account for the byte + // we're copying itself + let amount = self.rng.rand_exp(1, self.input.len() - offset) - 1; + + // Make sure we don't expand past the maximum input size + let amount = core::cmp::min(self.max_input_size - self.input.len(), amount); + + // Get the value we want to repeat + let val = self.input[offset].clone(); + + // Create an interator we want to expand with + let iter = core::iter::repeat(val).take(amount); + + // Expand at `offset` with `iter` + self.input.splice(offset..offset, iter); + } + + /// Write over the input with a random magic value + fn magic_overwrite(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick a random offset + let offset = self.rand_offset(); + + // Pick a random magic value + let magic_value = &MAGIC_VALUES[self.rng.rand(0, MAGIC_VALUES.len() - 1)]; + let mut magic_felt = Vec::new(); + for i in magic_value.iter() { + magic_felt.push(*i); + } + // Overwrite it + + let test = &magic_felt[..]; + + self.overwrite(offset, &test); + } + + /// Inject a magic value into the input + fn magic_insert(&mut self) { + // Pick a random offset + let offset = self.rand_offset_int(true); + + // Pick a random magic value + let magic_value = &MAGIC_VALUES[self.rng.rand(0, MAGIC_VALUES.len() - 1)]; + for i in magic_value.iter() { + self.insert(offset, Felt::from(*i)); + } + // Insert it + } + + /// Overwrite a random offset of the input with random bytes + fn random_overwrite(&mut self) { + // Nothing to do on an empty input + if self.input.is_empty() { + return; + } + + // Pick a random offset + let offset = self.rand_offset(); + + // Pick an amount to overwrite + let amount = self.rng.rand_exp(1, self.input.len() - offset); + + // Overwrite with random data + let rng = &mut self.rng; + if self.printable { + self.input[offset..offset + amount] + .iter_mut() + .for_each(|x| { + *x = Felt::from(rng.rand(0, 94) + 32); + }); + } else { + self.input[offset..offset + amount] + .iter_mut() + .for_each(|x| *x = Felt::from(rng.rand(0, 255))); + } + } + + /// Insert random bytes into a random offset in the input + fn random_insert(&mut self) { + // Pick a random offset + let offset = self.rand_offset_int(true); + + // Pick an amount to insert + let amount = self.rng.rand_exp(0, self.input.len() - offset); + + // Make sure the amount doesn't expand us past the maximum input size + let amount = core::cmp::min(amount, self.max_input_size - self.input.len()); + + // Insert `amount` random bytes + let rng = &mut self.rng; + if self.printable { + self.input.splice( + offset..offset, + (0..amount).map(|_| Felt::from(rng.rand(0, 94) + 32)), + ); + } else { + self.input.splice( + offset..offset, + (0..amount).map(|_| Felt::from(rng.rand(0, 255))), + ); + } + } + + // Corrupt a random bit in the input + /* byte_corruptor!(bit, |obj: &mut Self, x: Felt| -> Felt { + x ^ Felt::from(1u8 << obj.rng.rand(0, 7)) + }); */ + + // Increment a byte in the input + byte_corruptor!(inc_byte, |_: &mut Self, x: Felt| -> Felt { + x + Felt::from(1) + }); + + // Decrement a byte in the input + byte_corruptor!(dec_byte, |_: &mut Self, x: Felt| -> Felt { + x - Felt::from(1) + }); + + // Negate a byte in the input + byte_corruptor!(neg_byte, |_: &mut Self, x: Felt| -> Felt { -x }); +} diff --git a/cairo-native-fuzzer/src/runner/mod.rs b/cairo-native-fuzzer/src/runner/mod.rs new file mode 100644 index 0000000..919f9fb --- /dev/null +++ b/cairo-native-fuzzer/src/runner/mod.rs @@ -0,0 +1,2 @@ +pub mod runner; +pub mod syscall_handler; diff --git a/cairo-native-fuzzer/src/runner/runner.rs b/cairo-native-fuzzer/src/runner/runner.rs new file mode 100644 index 0000000..94f9501 --- /dev/null +++ b/cairo-native-fuzzer/src/runner/runner.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; + +use cairo_lang_sierra::ids::FunctionId; +use cairo_lang_sierra::program::Program; +use cairo_native::execution_result::ContractExecutionResult; +use cairo_native::module::NativeModule; +use cairo_native::{context::NativeContext, executor::JitNativeExecutor}; +use starknet_types_core::felt::Felt; + +use crate::runner::syscall_handler::SyscallHandler; + +/// Compile the sierra program into a MLIR module +fn compile_sierra_program<'a>( + native_context: &'a NativeContext, + sierra_program: &'a Program, +) -> Result, String> { + native_context + .compile(sierra_program, false) + .map_err(|e| e.to_string()) +} + +// Create the Native Executor (with JIT) +fn create_executor<'a>(native_program: NativeModule<'a>) -> JitNativeExecutor<'a> { + JitNativeExecutor::from_native_module(native_program, Default::default()) +} + +/// Cairo Runner that uses Cairo Native +pub struct CairoNativeRunner { + native_context: NativeContext, + sierra_program: Option>, + entry_point_id: Option, +} + +impl CairoNativeRunner { + pub fn new() -> Self { + let native_context = NativeContext::new(); + Self { + native_context, + sierra_program: None, + entry_point_id: None, + } + } + + /// Initialize the runner + /// 1 - Load the sierra_program instance variable + /// 2 - Load the entry point id in an instance variable + pub fn init( + &mut self, + entry_point_id: &Option, + sierra_program: &Option>, + ) -> Result<(), String> { + self.sierra_program = sierra_program.clone(); + self.entry_point_id = entry_point_id.clone(); + + Ok(()) + } + + // Run the program using Cairo Native + #[inline] + pub fn run_program(&mut self, params: &[Felt]) -> Result { + // Compile the Sierra program into a MLIR module + let native_program = compile_sierra_program( + &self.native_context, + self.sierra_program + .as_ref() + .ok_or("Sierra program not available")?, + )?; + + // Instantiate the executor + let native_executor = create_executor(native_program); + + // Execute the program + native_executor + .invoke_contract_dynamic( + self.entry_point_id.as_ref().unwrap(), + params, + Some(u128::MAX), + SyscallHandler, + ) + .map_err(|e| e.to_string()) + } +} diff --git a/cairo-native-fuzzer/src/runner/syscall_handler.rs b/cairo-native-fuzzer/src/runner/syscall_handler.rs new file mode 100644 index 0000000..2731130 --- /dev/null +++ b/cairo-native-fuzzer/src/runner/syscall_handler.rs @@ -0,0 +1,277 @@ +// Source : https://github.com/lambdaclass/cairo_native/blob/9791075918f17b1787084d85063180f6aa67a6ec/examples/erc20.rs + +use cairo_native::starknet::BlockInfo; +use cairo_native::starknet::ExecutionInfo; +use cairo_native::starknet::ExecutionInfoV2; +use cairo_native::starknet::ResourceBounds; +use cairo_native::starknet::Secp256k1Point; +use cairo_native::starknet::Secp256r1Point; +use cairo_native::starknet::StarknetSyscallHandler; +use cairo_native::starknet::SyscallResult; +use cairo_native::starknet::TxInfo; +use cairo_native::starknet::TxV2Info; +use cairo_native::starknet::U256; +use starknet_types_core::felt::Felt; + +#[derive(Debug)] +pub struct SyscallHandler; + +impl StarknetSyscallHandler for SyscallHandler { + fn get_block_hash(&mut self, block_number: u64, _gas: &mut u128) -> SyscallResult { + println!("Called `get_block_hash({block_number})` from MLIR."); + Ok(Felt::from_bytes_be_slice(b"get_block_hash ok")) + } + + fn get_execution_info( + &mut self, + _gas: &mut u128, + ) -> SyscallResult { + println!("Called `get_execution_info()` from MLIR."); + Ok(ExecutionInfo { + block_info: BlockInfo { + block_number: 1234, + block_timestamp: 2345, + sequencer_address: 3456.into(), + }, + tx_info: TxInfo { + version: 4567.into(), + account_contract_address: 5678.into(), + max_fee: 6789, + signature: vec![1248.into(), 2486.into()], + transaction_hash: 9876.into(), + chain_id: 8765.into(), + nonce: 7654.into(), + }, + caller_address: 6543.into(), + contract_address: 5432.into(), + entry_point_selector: 4321.into(), + }) + } + + fn get_execution_info_v2( + &mut self, + _remaining_gas: &mut u128, + ) -> SyscallResult { + println!("Called `get_execution_info_v2()` from MLIR."); + Ok(ExecutionInfoV2 { + block_info: BlockInfo { + block_number: 1234, + block_timestamp: 2345, + sequencer_address: 3456.into(), + }, + tx_info: TxV2Info { + version: 1.into(), + account_contract_address: 1.into(), + max_fee: 0, + signature: vec![1.into()], + transaction_hash: 1.into(), + chain_id: 1.into(), + nonce: 1.into(), + tip: 1, + paymaster_data: vec![1.into()], + nonce_data_availability_mode: 0, + fee_data_availability_mode: 0, + account_deployment_data: vec![1.into()], + resource_bounds: vec![ResourceBounds { + resource: 2.into(), + max_amount: 10, + max_price_per_unit: 20, + }], + }, + caller_address: 6543.into(), + contract_address: 5432.into(), + entry_point_selector: 4321.into(), + }) + } + + fn deploy( + &mut self, + class_hash: Felt, + contract_address_salt: Felt, + calldata: &[Felt], + deploy_from_zero: bool, + _gas: &mut u128, + ) -> SyscallResult<(Felt, Vec)> { + println!("Called `deploy({class_hash}, {contract_address_salt}, {calldata:?}, {deploy_from_zero})` from MLIR."); + Ok(( + class_hash + contract_address_salt, + calldata.iter().map(|x| x + Felt::ONE).collect(), + )) + } + + fn replace_class(&mut self, class_hash: Felt, _gas: &mut u128) -> SyscallResult<()> { + println!("Called `replace_class({class_hash})` from MLIR."); + Ok(()) + } + + fn library_call( + &mut self, + class_hash: Felt, + function_selector: Felt, + calldata: &[Felt], + _gas: &mut u128, + ) -> SyscallResult> { + println!( + "Called `library_call({class_hash}, {function_selector}, {calldata:?})` from MLIR." + ); + Ok(calldata.iter().map(|x| x * Felt::from(3)).collect()) + } + + fn call_contract( + &mut self, + address: Felt, + entry_point_selector: Felt, + calldata: &[Felt], + _gas: &mut u128, + ) -> SyscallResult> { + println!( + "Called `call_contract({address}, {entry_point_selector}, {calldata:?})` from MLIR." + ); + Ok(calldata.iter().map(|x| x * Felt::from(3)).collect()) + } + + fn storage_read( + &mut self, + address_domain: u32, + address: Felt, + _gas: &mut u128, + ) -> SyscallResult { + println!("Called `storage_read({address_domain}, {address})` from MLIR."); + Ok(address * Felt::from(3)) + } + + fn storage_write( + &mut self, + address_domain: u32, + address: Felt, + value: Felt, + _gas: &mut u128, + ) -> SyscallResult<()> { + println!("Called `storage_write({address_domain}, {address}, {value})` from MLIR."); + Ok(()) + } + + fn emit_event(&mut self, keys: &[Felt], data: &[Felt], _gas: &mut u128) -> SyscallResult<()> { + println!("Called `emit_event({keys:?}, {data:?})` from MLIR."); + Ok(()) + } + + fn send_message_to_l1( + &mut self, + to_address: Felt, + payload: &[Felt], + _gas: &mut u128, + ) -> SyscallResult<()> { + println!("Called `send_message_to_l1({to_address}, {payload:?})` from MLIR."); + Ok(()) + } + + fn keccak( + &mut self, + input: &[u64], + _gas: &mut u128, + ) -> SyscallResult { + println!("Called `keccak({input:?})` from MLIR."); + Ok(U256 { + hi: 0, + lo: 1234567890, + }) + } + + fn secp256k1_new( + &mut self, + _x: U256, + _y: U256, + _remaining_gas: &mut u128, + ) -> SyscallResult> { + unimplemented!() + } + + fn secp256k1_add( + &mut self, + _p0: Secp256k1Point, + _p1: Secp256k1Point, + _remaining_gas: &mut u128, + ) -> SyscallResult { + unimplemented!() + } + + fn secp256k1_mul( + &mut self, + _p: Secp256k1Point, + _m: U256, + _remaining_gas: &mut u128, + ) -> SyscallResult { + unimplemented!() + } + + fn secp256k1_get_point_from_x( + &mut self, + _x: U256, + _y_parity: bool, + _remaining_gas: &mut u128, + ) -> SyscallResult> { + unimplemented!() + } + + fn secp256k1_get_xy( + &mut self, + _p: Secp256k1Point, + _remaining_gas: &mut u128, + ) -> SyscallResult<(U256, U256)> { + unimplemented!() + } + + fn secp256r1_new( + &mut self, + _x: U256, + _y: U256, + _remaining_gas: &mut u128, + ) -> SyscallResult> { + unimplemented!() + } + + fn secp256r1_add( + &mut self, + _p0: Secp256r1Point, + _p1: Secp256r1Point, + _remaining_gas: &mut u128, + ) -> SyscallResult { + unimplemented!() + } + + fn secp256r1_mul( + &mut self, + _p: Secp256r1Point, + _m: U256, + _remaining_gas: &mut u128, + ) -> SyscallResult { + unimplemented!() + } + + fn secp256r1_get_point_from_x( + &mut self, + _x: U256, + _y_parity: bool, + _remaining_gas: &mut u128, + ) -> SyscallResult> { + unimplemented!() + } + + fn secp256r1_get_xy( + &mut self, + _p: Secp256r1Point, + _remaining_gas: &mut u128, + ) -> SyscallResult<(U256, U256)> { + unimplemented!() + } + + fn sha256_process_block( + &mut self, + _state: &mut [u32; 8], + _block: &[u32; 16], + _remaining_gas: &mut u128, + ) -> SyscallResult<()> { + unimplemented!() + } +} diff --git a/cairo-native-fuzzer/src/utils.rs b/cairo-native-fuzzer/src/utils.rs new file mode 100644 index 0000000..18e433f --- /dev/null +++ b/cairo-native-fuzzer/src/utils.rs @@ -0,0 +1,12 @@ +use cairo_lang_sierra::ids::FunctionId; +use cairo_lang_sierra::program::GenFunction; +use cairo_lang_sierra::program::Program; +use cairo_lang_sierra::program::StatementIdx; + +/// Find and return the function with the given `FunctionId` in the `Program` +pub fn get_function_by_id<'a>( + program: &'a Program, + function_id: &FunctionId, +) -> Option<&'a GenFunction> { + program.funcs.iter().find(|f| &f.id == function_id) +}