Skip to content

Commit

Permalink
Merge pull request #20 from FuzzingLabs/dev/antonin
Browse files Browse the repository at this point in the history
Improve unit tests generation & symbolic execution documentation
  • Loading branch information
Rog3rSm1th authored Aug 12, 2024
2 parents 34592a5 + 848fad0 commit 107d2fe
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 22 deletions.
42 changes: 37 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ Sierra-Analyzer is a security toolkit for analyzing Sierra files.
3) [Analyze a remote contract](#analyze-a-remote-contract)
4) [Print the contract's Control-Flow Graph](#print-the-contracts-control-flow-graph)
5) [Print the contract's Callgraph](#print-the-contracts-callgraph)
6) [Run the detectors](#print-the-contracts-callgraph)
7) [Use it as a library](#print-the-contracts-callgraph)
6) [Run the detectors](#run-the-detectors)
7) [Use the symbolic execution to generate unit tests](#use-the-symbolic-execution-to-generate-unit-tests)
8) [Improve the decompiler output using LLMs](#print-the-contracts-callgraph)
9) [Use it as a library](#print-the-contracts-callgraph)


#### Project structure

Expand Down Expand Up @@ -96,16 +98,46 @@ cargo run -- -f ./examples/sierra/fib_array.sierra -d
<img src="/doc/images/detectors-output.png" height="130px"/>
</p>

#### Use it as a library
#### Use the symbolic execution to generate unit tests

It is also possible to use the `sierra-analyzer-lib` library to decompile serialised or unserialised Sierra files.
##### 1) Using the Tests generator detector

Symbolic execution can be used to generate unit tests for the functions that take `felt252` arguments as input.

For example the file [symbolic_execution_test.sierra](https://github.com/FuzzingLabs/sierra-analyzer/blob/master/examples/sierra/symbolic_execution_test.sierra) contains a main function that takes four `felt252` arguments *v0*, *v1*, *v2* and *v3*. The function includes four conditions that check if `v0 == 102`, `v1 == 117`, `v2 == 122` and `v3 == 122` which correspond to the ASCII values for the letters *f*, *u*, *z*, and *z*, respectively.

When running the detectors we can generate test cases for each path in the function with the **Tests generator detector**:


```
cargo run -- -f ./examples/sierra/fib_array.sierra -d
[...]
[Informational] Tests generator
- symbolic::symbolic::symbolic_execution_test :
- v0: 102, v1: 0, v2: 0, v3: 0
- v0: 103, v1: 0, v2: 0, v3: 0
- v0: 102, v1: 117, v2: 0, v3: 0
- v0: 0, v1: 118, v2: 0, v3: 0
- v0: 102, v1: 117, v2: 122, v3: 0
- v0: 0, v1: 0, v2: 123, v3: 0
- v0: 102, v1: 117, v2: 122, v3: 122
- v0: 0, v1: 0, v2: 0, v3: 123
```

##### 2) Using the library

Examples can be found [here](/lib/examples/).
The tests generator can also be used [with the library](https://github.com/FuzzingLabs/sierra-analyzer/blob/master/lib/examples/tests_generator.rs).

#### Improve the decompiler output using LLMs

[Here](/doc/llm-decompilation.md) is a tutorial on how to improve the decompiler output using LLMs.

#### Use it as a library

It is also possible to use the `sierra-analyzer-lib` library to decompile serialised or unserialised Sierra files.

#### Features

- [x] Decompiler
Expand Down
9 changes: 8 additions & 1 deletion lib/src/detectors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ pub mod detector;
pub mod functions_detector;
pub mod statistics_detector;
pub mod strings_detector;
pub mod tests_generator_detector;

use crate::detectors::detector::Detector;
use crate::detectors::functions_detector::FunctionsDetector;
use crate::detectors::statistics_detector::StatisticsDetector;
use crate::detectors::strings_detector::StringsDetector;
use crate::detectors::tests_generator_detector::TestsGeneratorDetector;

/// Macro to create a vector of detectors
macro_rules! create_detectors {
Expand All @@ -21,5 +23,10 @@ macro_rules! create_detectors {

/// Returns a vector of all the instantiated detectors
pub fn get_detectors() -> Vec<Box<dyn Detector>> {
create_detectors!(FunctionsDetector, StringsDetector, StatisticsDetector)
create_detectors!(
FunctionsDetector,
StringsDetector,
StatisticsDetector,
TestsGeneratorDetector
)
}
76 changes: 76 additions & 0 deletions lib/src/detectors/tests_generator_detector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use crate::decompiler::decompiler::Decompiler;
use crate::detectors::detector::{Detector, DetectorType};
use crate::sym_exec::sym_exec::generate_test_cases_for_function;

#[derive(Debug)]
pub struct TestsGeneratorDetector;

impl TestsGeneratorDetector {
/// Creates a new `TestsGeneratorDetector` instance
pub fn new() -> Self {
Self
}
}

impl Detector for TestsGeneratorDetector {
/// Returns the id of the detector
#[inline]
fn id(&self) -> &'static str {
"tests"
}

/// Returns the name of the detector
#[inline]
fn name(&self) -> &'static str {
"Tests generator"
}

/// Returns the description of the detector
#[inline]
fn description(&self) -> &'static str {
"Returns the tests cases for the functions."
}

/// Returns the type of the detector
#[inline]
fn detector_type(&self) -> DetectorType {
DetectorType::INFORMATIONAL
}

/// Returns the generated unit tests for the function if they exist
fn detect(&mut self, decompiler: &mut Decompiler) -> String {
let mut result = String::new();

for function in &mut decompiler.functions {
// Determine the function name
let function_name = if let Some(prototype) = &function.prototype {
// Remove the "func " prefix and then split at the first parenthese
let stripped_prototype = &prototype[5..];
if let Some(first_space_index) = stripped_prototype.find('(') {
Some(stripped_prototype[..first_space_index].trim().to_string())
} else {
None
}
} else {
None
};

// If a function name was found, proceed with the mutable borrow
if let Some(function_name) = function_name {

// Add the test cases to the result
let test_cases = generate_test_cases_for_function(
function,
decompiler.declared_libfuncs_names.clone(),
);

if !test_cases.is_empty() {
result += &format!("{} : \n", function_name);
result += &format!("{}\n", test_cases);
}
}
}

result
}
}
77 changes: 61 additions & 16 deletions lib/src/sym_exec/sym_exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,18 @@ pub fn generate_test_cases_for_function(
let cfg = Config::new();
let context = Context::new(&cfg);

// Create a solver
let solver = Solver::new(&context);

// Create Z3 variables for each felt252 argument
let z3_variables: Vec<Int> = felt252_arguments
.iter()
.map(|(arg_name, _)| Int::new_const(&context, &**arg_name))
.collect();

// Create a solver
let mut symbolic_execution = SymbolicExecution::new(&context);

let mut zero_constraints = Vec::new();
let mut other_constraints = Vec::new();

// Convert Sierra statements to z3 constraints
for basic_block in path {
for statement in &basic_block.statements {
Expand All @@ -63,16 +66,30 @@ pub fn generate_test_cases_for_function(
&context,
declared_libfuncs_names.clone(),
) {
solver.assert(&constraint);
symbolic_execution.add_constraint(&constraint);

// Identify if it's a zero check and store the variable for non-zero testing
if let GenStatement::Invocation(invocation) = &statement.statement {
let libfunc_id_str = parse_element_name_with_fallback!(
invocation.libfunc_id,
declared_libfuncs_names
);

if IS_ZERO_REGEX.is_match(&libfunc_id_str) {
let operand_name = format!("v{}", invocation.args[0].id.to_string());
let operand = Int::new_const(&context, operand_name.clone());
zero_constraints.push((operand, constraint));
} else {
// Store other constraints for reuse
other_constraints.push(constraint);
}
}
}
}
}

// Check if the constraints are satisfiable
match solver.check() {
SatResult::Sat => {
// If solvable, add the argument names and values to the result
if let Some(model) = solver.get_model() {
// Check if the constraints are satisfiable (value == 0)
if symbolic_execution.check() == SatResult::Sat {
if let Some(model) = symbolic_execution.solver.get_model() {
let values: Vec<String> = felt252_arguments
.iter()
.zip(z3_variables.iter())
Expand All @@ -84,16 +101,44 @@ pub fn generate_test_cases_for_function(
)
})
.collect();
let values_str = format!("{:?}\n", values);
let values_str = format!("{}", values.join(", "));
if unique_results.insert(values_str.clone()) {
result.push_str(&values_str);
result.push_str(&format!("{}\n", values_str));
}
}
}
SatResult::Unsat | SatResult::Unknown => {
let non_solvable_str = "non solvable\n".to_string();
if unique_results.insert(non_solvable_str.clone()) {
result.push_str(&non_solvable_str);

// Now generate test cases where the value is not equal to 0
for (operand, _) in &zero_constraints {
// Create a fresh solver for the non-zero case
let non_zero_solver = Solver::new(&context);

// Re-apply all other constraints except the zero-equality one
for constraint in &other_constraints {
non_zero_solver.assert(constraint);
}

// Add a constraint to force the operand to be not equal to 0
non_zero_solver.assert(&operand._eq(&Int::from_i64(&context, 0)).not());

if non_zero_solver.check() == SatResult::Sat {
if let Some(model) = non_zero_solver.get_model() {
let values: Vec<String> = felt252_arguments
.iter()
.zip(z3_variables.iter())
.map(|((arg_name, _), var)| {
format!(
"{}: {}",
arg_name,
model.eval(var, true).unwrap().to_string()
)
})
.collect();
let values_str = format!("{}", values.join(", "));
if unique_results.insert(values_str.clone()) {
result.push_str(&format!("{}\n", values_str));
}
}
}
}
}
Expand Down

0 comments on commit 107d2fe

Please sign in to comment.