Skip to content

Commit

Permalink
Merge pull request #22 from FuzzingLabs/feat/cairo-fuzzer-inputs
Browse files Browse the repository at this point in the history
Input file generator for the Cairo Fuzzer
  • Loading branch information
Rog3rSm1th authored Aug 29, 2024
2 parents c7de2e1 + 9842b57 commit 7fb790e
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 9 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --verbose
run: cargo build --release --verbose
- name: Run tests
run: cargo test --verbose
# We run the unit tests of the sierra-analyzer-lib
working-directory: ./lib
26 changes: 23 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
[workspace]
resolver = "2"
members = [
"sierra-decompiler",
]

[package]
edition = '2021'
name = "sierra-analyzer"
default-run = "sierra-decompiler"

[[bin]]
name = "sierra-decompiler"
path = "bin/sierra-decompiler/src/main.rs"

[[bin]]
name = "test-generator"
path = "bin/test-generator/src/main.rs"

[profile.dev]
opt-level = 0
Expand All @@ -25,3 +35,13 @@ panic = 'unwind'
incremental = false
codegen-units = 1
rpath = false

[dependencies]
clap = { version = "4.0.0-rc.1", features = [ "derive" ] }
serde = "1.0.209"
serde_json = "1.0.116"
tokio = "1.37.0"
cairo-lang-sierra = "~2.7.1"
cairo-lang-starknet-classes = "~2.7.1"
sierra-analyzer-lib = { path = "./lib" }

8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ Sierra Analyzer is a security toolkit designed for analyzing Sierra files. It in

```
.
├── doc # Documentation files
├── examples # Sierra & Contrat class samples files
├── lib # sierra-analyzer library
├── sierra-decompiler # Sierra decompiler tool (based on sierra-analyzer library)
├── doc # Documentation files
├── examples # Sierra & Contrat class samples files
├── lib # sierra-analyzer library
├── bin # Binaries directory containing Sierra decompiler tool (based on sierra-analyzer library) & Tests generator
└── README.md
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ cairo-lang-sierra = "~2.7.1"
cairo-lang-starknet-classes = "~2.7.1"
clap = { version = "4.0.0-rc.1", features = [ "derive" ] }
serde_json = "1.0.116"
sierra-analyzer-lib = { path = "../lib" }
sierra-analyzer-lib = { path = "../../lib" }
tokio = "1.37.0"
File renamed without changes.
11 changes: 11 additions & 0 deletions bin/test-generator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "test-generator"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
serde = "1.0.209"
serde_json = "1.0.127"
sierra-analyzer-lib = { path = "../../lib" }
18 changes: 18 additions & 0 deletions bin/test-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## Input file generator for the Cairo Fuzzer

List the functions for which test case generation is possible :

```bash
cargo run --bin test-generator ./examples/sierra/symbolic_execution_test.sierra

Available functions:
- symbolic::symbolic::symbolic_execution_test
```

Generate an inputfile for the cairo fuzzer :

```bash
cargo run --bin test-generator ./examples/sierra/symbolic_execution_test.sierra symbolic::symbolic::symbolic_execution_test > inputfile.json
```

It can now be used as an input file for the function we want to fuzz using the Cairo-fuzzer with the `--inputfile` parameter.
186 changes: 186 additions & 0 deletions bin/test-generator/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
use std::collections::HashSet;
use std::env;
use std::fs;
use std::process;
use std::str::FromStr;

use serde::Serialize;

use sierra_analyzer_lib::sierra_program::SierraProgram;
use sierra_analyzer_lib::sym_exec::sym_exec::generate_test_cases_for_function;

/// Struct representing the fuzzing data
#[derive(Serialize)]
struct FuzzingData {
workspace: String,
path: String,
name: String,
args: Vec<String>,
inputs: Vec<Vec<Value>>,
}

/// Struct representing a value in the fuzzing data
#[derive(Serialize)]
struct Value {
value: ValueData,
}

/// Struct representing the value data
#[derive(Serialize)]
struct ValueData {
val: Vec<i64>,
}

/// Prints the names of all available functions in the decompiler
fn print_function_names(decompiler: &sierra_analyzer_lib::decompiler::decompiler::Decompiler) {
println!("Available functions:");
for function in &decompiler.functions {
if let Some(prototype) = &function.prototype {
let function_name = extract_function_name(prototype);
println!("\t- {}", function_name);
}
}
}

/// Extracts the function name from the prototype string
fn extract_function_name(prototype: &str) -> String {
let stripped_prototype = &prototype[5..];
if let Some(first_space_index) = stripped_prototype.find('(') {
return stripped_prototype[..first_space_index].trim().to_string();
}
String::new()
}

/// Parses the result of generate_test_cases_for_function and returns a vector of vectors of integer inputs
fn get_integers_inputs(test_cases: &str) -> Vec<Vec<i64>> {
let unique_results: HashSet<String> = test_cases.lines().map(|line| line.to_string()).collect();
unique_results
.iter()
.map(|line| parse_line_inputs(line))
.collect()
}

/// Parses a single line of test cases and returns a vector of integer inputs
fn parse_line_inputs(line: &str) -> Vec<i64> {
let parts: Vec<&str> = line.split(", ").collect();
parts
.iter()
.filter_map(|part| {
let key_value: Vec<&str> = part.split(": ").collect();
if key_value.len() == 2 {
if let Ok(value) = i64::from_str(key_value[1]) {
return Some(value);
}
}
None
})
.collect()
}

/// Generates the fuzzing data for a given function
fn generate_fuzzing_data(
function: &mut sierra_analyzer_lib::decompiler::function::Function,
declared_libfuncs_names: Vec<String>,
workspace: &str,
path: &str,
name: &str,
) -> FuzzingData {
let test_cases = generate_test_cases_for_function(function, declared_libfuncs_names);
let integer_inputs = get_integers_inputs(&test_cases);
let arg_count = function.arguments.len();
let args = vec!["felt".to_string(); arg_count];
let inputs = convert_integer_inputs_to_values(integer_inputs);

FuzzingData {
workspace: workspace.to_string(),
path: path.to_string(),
name: name.to_string(),
args,
inputs,
}
}

/// Converts integer inputs to the desired JSON format
fn convert_integer_inputs_to_values(integer_inputs: Vec<Vec<i64>>) -> Vec<Vec<Value>> {
integer_inputs
.iter()
.map(|inputs| {
inputs
.iter()
.map(|&input| Value {
value: ValueData { val: vec![input] },
})
.collect()
})
.collect()
}

/// Main function to handle command-line arguments and generate fuzzing data
fn main() {
let args: Vec<String> = env::args().collect();

if args.len() < 2 {
eprintln!("Error: Please provide a file path as an argument.");
process::exit(1);
}

let file_path = &args[1];

// Read the content of the Sierra program file
let content = match fs::read_to_string(file_path) {
Ok(content) => content,
Err(e) => {
eprintln!("Error reading file {}: {}", file_path, e);
process::exit(1);
}
};

// Initialize a new SierraProgram with the content of the .sierra file
let program = SierraProgram::new(content);

// Disable verbose output for the decompiler
let verbose_output = false;

// Create a decompiler instance for the Sierra program
let mut decompiler = program.decompiler(verbose_output);

// Decompile the Sierra program
let use_color = false;
decompiler.decompile(use_color);

if args.len() == 2 {
// No specific function specified, print all available functions
print_function_names(&decompiler);
} else {
// Specific function specified, generate test cases for that function
let function_name = &args[2];
let mut found = false;

for function in &mut decompiler.functions {
if let Some(prototype) = &function.prototype {
let name = extract_function_name(prototype);
if name == *function_name {
let fuzzing_data = generate_fuzzing_data(
function,
decompiler.declared_libfuncs_names.clone(),
"fuzzer_workspace",
"input_file",
"Fuzz_one",
);

// Serialize the data to JSON and print it
let json_output = serde_json::to_string_pretty(&fuzzing_data).unwrap();
println!("{}", json_output);

found = true;
break;
}
}
}

if !found {
eprintln!("Error: Function '{}' not found.", function_name);
process::exit(1);
}
}
}
23 changes: 23 additions & 0 deletions lib/src/sym_exec/sym_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,29 @@ fn generate_non_zero_test_cases(
}
}

/// Parses the result of generate_test_cases_for_function and returns a vector of vectors of integer inputs
/// TODO : Reverse the logic and generate formatted testcases from the integers vectors
pub fn get_integers_inputs(test_cases: &str) -> Vec<Vec<i64>> {
let mut result = Vec::new();
let unique_results: HashSet<String> = test_cases.lines().map(|line| line.to_string()).collect();

for line in unique_results {
let mut line_inputs = Vec::new();
let parts: Vec<&str> = line.split(", ").collect();
for part in parts {
let key_value: Vec<&str> = part.split(": ").collect();
if key_value.len() == 2 {
if let Ok(value) = i64::from_str(key_value[1]) {
line_inputs.push(value);
}
}
}
result.push(line_inputs);
}

result
}

/// A struct that represents a symbolic execution solver
#[derive(Debug)]
pub struct SymbolicExecution<'a> {
Expand Down

0 comments on commit 7fb790e

Please sign in to comment.