Skip to content

Commit

Permalink
feat(symbolic-execution): implement & utilize run_with_timeout (#257)
Browse files Browse the repository at this point in the history
* fix(symbolic-exec): break out of infinite `JUMP` loops where stack grows infinitely

* chore(symbolic-exec): consolidate `JUMP`/`JUMPI` logic

* feat(threading): implement `run_with_timeout`

* feat(cfg): use `run_with_timeout` for symbolic execution

* feat(decompile): use `run_with_timeout` for symbolic execution

* feat(snapshot): use `run_with_timeout` for symbolic execution
  • Loading branch information
Jon-Becker authored Dec 30, 2023
1 parent 5d33533 commit d3d66ae
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 11 deletions.
2 changes: 1 addition & 1 deletion common/src/ether/evm/ext/exec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
};
use std::collections::HashMap;

#[derive(Clone, Debug)]
#[derive(Clone, Debug, Default)]
pub struct VMTrace {
pub instruction: u128,
pub gas_used: u128,
Expand Down
64 changes: 64 additions & 0 deletions common/src/utils/threading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,28 @@ pub fn task_pool<
results
}

/// Takes a function and some arguments, and runs the function in a separate thread. If the function
/// doesnt finish within the given timeout, the thread is killed, and the function returns None.
pub fn run_with_timeout<T, F>(f: F, timeout: std::time::Duration) -> Option<T>
where
T: Send + 'static,
F: FnOnce() -> T + Send + 'static, {
let (tx, rx) = unbounded();
let handle = thread::spawn(move || {
let result = f();
tx.send(result).unwrap();
});

let result = rx.recv_timeout(timeout);
if result.is_err() {
handle.thread().unpark();
return None
}

handle.join().unwrap();
Some(result.unwrap())
}

#[cfg(test)]
mod tests {
use crate::utils::threading::*;
Expand Down Expand Up @@ -109,4 +131,46 @@ mod tests {
let results = task_pool(items, num_threads, f);
assert!(results.is_empty());
}

#[test]
fn test_run_with_timeout() {
// Test case with a function that finishes within the timeout
let timeout = std::time::Duration::from_secs(1);
let f = || 1;
let result = run_with_timeout(f, timeout);
assert_eq!(result, Some(1));

// Test case with a function that doesnt finish within the timeout
let timeout = std::time::Duration::from_millis(1);
let f = || std::thread::sleep(std::time::Duration::from_secs(1));
let result = run_with_timeout(f, timeout);
assert_eq!(result, None);
}

#[test]
fn test_run_with_timeout_with_panic() {
// Test case with a function that panics
let timeout = std::time::Duration::from_secs(1);
let f = || panic!("test");
let result = run_with_timeout(f, timeout);
assert_eq!(result, None);
}

#[test]
fn test_run_with_timeout_with_args() {
// Test case with a function that takes arguments
let timeout = std::time::Duration::from_secs(1);
let f = |x: i32| x * 2;
let result = run_with_timeout(move || f(2), timeout);
assert_eq!(result, Some(4));
}

#[test]
fn test_run_with_timeout_infinite_loop() {
// Test case with a function that runs an infinite loop
let timeout = std::time::Duration::from_secs(1);
let f = || loop {};
let result = run_with_timeout(f, timeout);
assert_eq!(result, None);
}
}
17 changes: 15 additions & 2 deletions core/src/cfg/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use heimdall_common::{
bytecode::get_bytecode_from_target, compiler::detect_compiler,
selectors::find_function_selectors,
},
utils::threading::run_with_timeout,
};
use indicatif::ProgressBar;
use std::time::Duration;
Expand Down Expand Up @@ -56,6 +57,10 @@ pub struct CFGArgs {
/// The name for the output file
#[clap(long, short, default_value = "", hide_default_value = true)]
pub name: String,

/// Timeout for symbolic execution
#[clap(long, short, default_value = "10000", hide_default_value = true)]
pub timeout: u64,
}

impl CFGArgsBuilder {
Expand All @@ -68,6 +73,7 @@ impl CFGArgsBuilder {
color_edges: Some(false),
output: Some(String::new()),
name: Some(String::new()),
timeout: Some(10000),
}
}
}
Expand Down Expand Up @@ -194,7 +200,14 @@ pub async fn cfg(args: CFGArgs) -> Result<Graph<String, String>, Box<dyn std::er
);

// get a map of possible jump destinations
let (map, jumpdest_count) = &evm.symbolic_exec();
let (map, jumpdest_count) =
match run_with_timeout(move || evm.symbolic_exec(), Duration::from_millis(args.timeout)) {
Some(map) => map,
None => {
logger.error("symbolic execution timed out.");
return Err("symbolic execution timed out.".into())
}
};

// add jumpdests to the trace
trace.add_info(
Expand All @@ -204,7 +217,7 @@ pub async fn cfg(args: CFGArgs) -> Result<Graph<String, String>, Box<dyn std::er
);

debug_max!("building control flow graph from symbolic execution trace");
build_cfg(map, &mut contract_cfg, None, false);
build_cfg(&map, &mut contract_cfg, None, false);

progress.finish_and_clear();
logger.info("symbolic execution completed.");
Expand Down
39 changes: 34 additions & 5 deletions core/src/decompile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ pub mod precompile;
pub mod resolve;
pub mod util;
use heimdall_common::{
debug_max, ether::bytecode::get_bytecode_from_target, utils::strings::get_shortned_target,
debug_max,
ether::{bytecode::get_bytecode_from_target, evm::ext::exec::VMTrace},
utils::{strings::get_shortned_target, threading::run_with_timeout},
};

use crate::{
Expand Down Expand Up @@ -80,6 +82,10 @@ pub struct DecompilerArgs {
/// The name for the output file
#[clap(long, short, default_value = "", hide_default_value = true)]
pub name: String,

/// The timeout for each function's symbolic execution in milliseconds.
#[clap(long, short, default_value = "10000", hide_default_value = true)]
pub timeout: u64,
}

impl DecompilerArgsBuilder {
Expand All @@ -94,6 +100,7 @@ impl DecompilerArgsBuilder {
include_yul: Some(false),
output: Some(String::new()),
name: Some(String::new()),
timeout: Some(10000),
}
}
}
Expand Down Expand Up @@ -252,8 +259,22 @@ pub async fn decompile(
);

// get a map of possible jump destinations
let (map, jumpdest_count) =
&evm.clone().symbolic_exec_selector(&selector, function_entry_point);
let mut evm_clone = evm.clone();
let selector_clone = selector.clone();
let (map, jumpdest_count) = match run_with_timeout(
move || evm_clone.symbolic_exec_selector(&selector_clone, function_entry_point),
Duration::from_millis(args.timeout),
) {
Some(map) => map,
None => {
trace.add_error(
func_analysis_trace,
line!(),
&format!("symbolic execution timed out!"),
);
(VMTrace::default(), 0)
}
};

trace.add_debug(
func_analysis_trace,
Expand All @@ -276,7 +297,7 @@ pub async fn decompile(
if args.include_yul {
debug_max!("analyzing symbolic execution trace '0x{}' with yul analyzer", selector);
analyzed_function = analyze_yul(
map,
&map,
Function {
selector: selector.clone(),
entry_point: function_entry_point,
Expand All @@ -301,7 +322,7 @@ pub async fn decompile(
} else {
debug_max!("analyzing symbolic execution trace '0x{}' with sol analyzer", selector);
analyzed_function = analyze_sol(
map,
&map,
Function {
selector: selector.clone(),
entry_point: function_entry_point,
Expand All @@ -326,6 +347,14 @@ pub async fn decompile(
);
}

// add notice to analyzed_function if jumpdest_count == 0, indicating that
// symbolic execution timed out
if jumpdest_count == 0 {
analyzed_function
.notices
.push("symbolic execution timed out. please report this!".to_string());
}

let argument_count = analyzed_function.arguments.len();

if argument_count != 0 {
Expand Down
25 changes: 22 additions & 3 deletions core/src/snapshot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pub mod menus;
pub mod resolve;
pub mod structures;
pub mod util;
use heimdall_common::debug_max;
use heimdall_common::{debug_max, utils::threading::run_with_timeout};

use std::{
collections::{HashMap, HashSet},
Expand Down Expand Up @@ -76,6 +76,10 @@ pub struct SnapshotArgs {
/// The output directory to write the output to, or 'print' to print to the console.
#[clap(long = "output", short = 'o', default_value = "output", hide_default_value = true)]
pub output: String,

/// The timeout for each function's symbolic execution in milliseconds.
#[clap(long, short, default_value = "10000", hide_default_value = true)]
pub timeout: u64,
}

impl SnapshotArgsBuilder {
Expand All @@ -89,6 +93,7 @@ impl SnapshotArgsBuilder {
no_tui: Some(true),
name: Some(String::new()),
output: Some(String::new()),
timeout: Some(10000),
}
}
}
Expand Down Expand Up @@ -257,8 +262,22 @@ async fn get_snapshots(
);

// get a map of possible jump destinations
let (map, jumpdest_count) =
evm.clone().symbolic_exec_selector(&selector, function_entry_point);
let mut evm_clone = evm.clone();
let selector_clone = selector.clone();
let (map, jumpdest_count) = match run_with_timeout(
move || evm_clone.symbolic_exec_selector(&selector_clone, function_entry_point),
Duration::from_millis(args.timeout),
) {
Some(map) => map,
None => {
trace.add_error(
func_analysis_trace,
line!(),
&format!("symbolic execution timed out, skipping snapshotting."),
);
continue
}
};

trace.add_debug(
func_analysis_trace,
Expand Down
4 changes: 4 additions & 0 deletions core/tests/test_cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod benchmark {
color_edges: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
};
let _ = heimdall_core::cfg::cfg(args).await;
}
Expand All @@ -34,6 +35,7 @@ mod benchmark {
color_edges: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
};
let _ = heimdall_core::cfg::cfg(args).await;
}
Expand All @@ -58,6 +60,7 @@ mod integration_tests {
color_edges: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
})
.await
.unwrap();
Expand All @@ -82,6 +85,7 @@ mod integration_tests {
color_edges: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
})
.await
.unwrap();
Expand Down
10 changes: 10 additions & 0 deletions core/tests/test_decompile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod benchmark {
include_yul: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
};
let _ = heimdall_core::decompile::decompile(args).await;
}
Expand All @@ -38,6 +39,7 @@ mod benchmark {
include_yul: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
};
let _ = heimdall_core::decompile::decompile(args).await;
}
Expand All @@ -58,6 +60,7 @@ mod benchmark {
include_yul: true,
output: String::from(""),
name: String::from(""),
timeout: 10000,
};
let _ = heimdall_core::decompile::decompile(args).await;
}
Expand All @@ -78,6 +81,7 @@ mod benchmark {
include_yul: true,
output: String::from(""),
name: String::from(""),
timeout: 10000,
};
let _ = heimdall_core::decompile::decompile(args).await;
}
Expand All @@ -98,6 +102,7 @@ mod benchmark {
include_yul: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
};
let _ = heimdall_core::decompile::decompile(args).await;
}
Expand All @@ -118,6 +123,7 @@ mod benchmark {
include_yul: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
};
let _ = heimdall_core::decompile::decompile(args).await;
}
Expand All @@ -144,6 +150,7 @@ mod integration_tests {
include_yul: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
})
.await
.unwrap();
Expand Down Expand Up @@ -173,6 +180,7 @@ mod integration_tests {
include_yul: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
})
.await
.unwrap();
Expand Down Expand Up @@ -209,6 +217,7 @@ mod integration_tests {
include_yul: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
})
.await
.unwrap();
Expand Down Expand Up @@ -322,6 +331,7 @@ mod integration_tests {
include_yul: false,
output: String::from(""),
name: String::from(""),
timeout: 10000,
})
.await
.unwrap();
Expand Down
Loading

0 comments on commit d3d66ae

Please sign in to comment.