mod abstractdata;
pub use abstractdata::*;
mod allocation;
mod coverage;
use coverage::*;
mod default_hook;
use default_hook::pitchfork_default_hook;
pub mod hooks;
pub mod hook_helpers;
pub mod secret;
mod logging;
mod progress;
mod main_func;
pub use main_func::main_func;
use colored::*;
use haybale::{layout, symex_function, backend::Backend, ExecutionManager, State, ReturnValue};
use haybale::{Error, Result};
pub use haybale::{Config, Project};
use haybale::function_hooks::IsCall;
use lazy_static::lazy_static;
use log::{debug, info, warn};
use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Clone, Debug)]
pub enum ConstantTimeResultForPath {
IsConstantTime,
NotConstantTime {
violation_message: String,
},
OtherError {
error: Error,
full_message: String,
},
}
pub struct ConstantTimeResultForFunction<'a> {
pub funcname: &'a str,
mangled_funcname: &'a str,
pub path_results: Vec<ConstantTimeResultForPath>,
pub block_coverage: HashMap<String, BlockCoverage>,
pub error_filename: Option<String>,
}
impl<'a> ConstantTimeResultForFunction<'a> {
pub fn first_ct_violation(&self) -> Option<&str> {
self.path_results.iter().find_map(|path_result| match path_result {
ConstantTimeResultForPath::IsConstantTime => None,
ConstantTimeResultForPath::NotConstantTime { violation_message } => Some(violation_message as &str),
ConstantTimeResultForPath::OtherError { .. } => None,
})
}
pub fn first_error_or_violation(&self) -> Option<&ConstantTimeResultForPath> {
self.path_results.iter().find(|path_result| match path_result {
ConstantTimeResultForPath::IsConstantTime => false,
ConstantTimeResultForPath::NotConstantTime { .. } => true,
ConstantTimeResultForPath::OtherError { .. } => true,
})
}
pub fn path_statistics(&self) -> PathStatistics {
let mut path_stats = PathStatistics::new();
for result in &self.path_results {
path_stats.add_path_result(result);
}
path_stats
}
}
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct PathStatistics {
pub num_ct_paths: usize,
pub num_ct_violations: usize,
pub num_unsats: usize,
pub num_loop_bound_exceeded: usize,
pub num_null_ptr_deref: usize,
pub num_function_not_found: usize,
pub num_solver_errors: usize,
pub num_unsupported_instruction: usize,
pub num_malformed_instruction: usize,
pub num_unreachable_instruction: usize,
pub num_failed_resolve_fptr: usize,
pub num_hook_retval_mismatch: usize,
pub num_other_errors: usize,
}
impl PathStatistics {
pub(crate) fn new() -> Self {
Self {
num_ct_paths: 0,
num_ct_violations: 0,
num_unsats: 0,
num_loop_bound_exceeded: 0,
num_null_ptr_deref: 0,
num_function_not_found: 0,
num_solver_errors: 0,
num_unsupported_instruction: 0,
num_malformed_instruction: 0,
num_unreachable_instruction: 0,
num_failed_resolve_fptr: 0,
num_hook_retval_mismatch: 0,
num_other_errors: 0,
}
}
pub(crate) fn add_path_result(&mut self, path_result: &ConstantTimeResultForPath) {
match path_result {
ConstantTimeResultForPath::IsConstantTime => self.num_ct_paths += 1,
ConstantTimeResultForPath::NotConstantTime { .. } => self.num_ct_violations += 1,
ConstantTimeResultForPath::OtherError { error: Error::Unsat, .. } => self.num_unsats += 1,
ConstantTimeResultForPath::OtherError { error: Error::LoopBoundExceeded(_), .. } => self.num_loop_bound_exceeded += 1,
ConstantTimeResultForPath::OtherError { error: Error::NullPointerDereference, .. } => self.num_null_ptr_deref += 1,
ConstantTimeResultForPath::OtherError { error: Error::FunctionNotFound(_), .. } => self.num_function_not_found += 1,
ConstantTimeResultForPath::OtherError { error: Error::SolverError(_), .. } => self.num_solver_errors += 1,
ConstantTimeResultForPath::OtherError { error: Error::UnsupportedInstruction(_), .. } => self.num_unsupported_instruction += 1,
ConstantTimeResultForPath::OtherError { error: Error::MalformedInstruction(_), .. } => self.num_malformed_instruction += 1,
ConstantTimeResultForPath::OtherError { error: Error::UnreachableInstruction, .. } => self.num_unreachable_instruction += 1,
ConstantTimeResultForPath::OtherError { error: Error::FailedToResolveFunctionPointer(_), .. } => self.num_failed_resolve_fptr += 1,
ConstantTimeResultForPath::OtherError { error: Error::HookReturnValueMismatch(_), .. } => self.num_hook_retval_mismatch += 1,
ConstantTimeResultForPath::OtherError { error: Error::OtherError(_), .. } => self.num_other_errors += 1,
}
}
}
impl fmt::Display for PathStatistics {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "verified paths: {}",
if self.num_ct_paths > 0 {
self.num_ct_paths.to_string().green()
} else {
self.num_ct_paths.to_string().normal()
}
)?;
writeln!(f, "constant-time violations found: {}",
if self.num_ct_violations > 0 {
self.num_ct_violations.to_string().red()
} else {
self.num_ct_violations.to_string().normal()
}
)?;
if self.num_null_ptr_deref > 0 {
writeln!(f, "null-pointer dereferences found: {}",
self.num_null_ptr_deref.to_string().red()
)?;
}
if self.num_function_not_found > 0 {
writeln!(f, "function-not-found errors: {}",
self.num_function_not_found.to_string().red()
)?;
}
if self.num_unsupported_instruction > 0 {
writeln!(f, "unsupported-instruction errors: {}",
self.num_unsupported_instruction.to_string().red()
)?;
}
if self.num_malformed_instruction > 0 {
writeln!(f, "malformed-instruction errors: {}",
self.num_malformed_instruction.to_string().red()
)?;
}
if self.num_unsats > 0 {
writeln!(f, "unsat errors: {}",
self.num_unsats.to_string().red()
)?;
}
if self.num_loop_bound_exceeded > 0 {
writeln!(f, "paths exceeding the loop bound: {}",
self.num_loop_bound_exceeded.to_string().red()
)?;
}
if self.num_unreachable_instruction > 0 {
writeln!(f, "unreachable-instruction errors: {}",
self.num_unreachable_instruction.to_string().red()
)?;
}
if self.num_failed_resolve_fptr > 0 {
writeln!(f, "failed-function-pointer-resolution errors: {}",
self.num_failed_resolve_fptr.to_string().red()
)?;
}
if self.num_hook_retval_mismatch > 0 {
writeln!(f, "hook-retval-mismatch errors: {}",
self.num_hook_retval_mismatch.to_string().red()
)?;
}
if self.num_solver_errors > 0 {
writeln!(f, "solver errors, including timeouts: {}",
self.num_solver_errors.to_string().red()
)?;
}
if self.num_other_errors > 0 {
writeln!(f, "other errors: {}",
self.num_other_errors.to_string().red()
)?;
}
Ok(())
}
}
impl<'a> fmt::Display for ConstantTimeResultForFunction<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "\nResults for {}:\n", self.funcname)?;
if self.path_results.is_empty() {
writeln!(f, "No valid paths were found and no errors or violations were encountered")?;
return Ok(());
}
let path_stats = self.path_statistics();
path_stats.fmt(f)?;
writeln!(f)?;
let is_ct = self.path_results.len() == path_stats.num_ct_paths;
let show_coverage_stats = is_ct || match std::env::var("PITCHFORK_COVERAGE_STATS") {
Ok(val) if val == "1" => true,
_ => false,
};
if show_coverage_stats {
writeln!(f, "Coverage stats:\n")?;
let toplevel_coverage = self.block_coverage.get(self.mangled_funcname).unwrap();
writeln!(f, " Block coverage of toplevel function ({}): {:.1}%", self.funcname, 100.0 * toplevel_coverage.percentage)?;
if toplevel_coverage.percentage < 1.0 {
writeln!(f, " Missed blocks in toplevel function: {:?}", toplevel_coverage.missed_blocks.iter())?;
}
writeln!(f)?;
for (fname, coverage) in &self.block_coverage {
if fname != self.mangled_funcname {
writeln!(f, " Block coverage of {}: {:.1}%", fname, 100.0 * coverage.percentage)?;
}
}
} else {
writeln!(f, "(for detailed block-coverage stats, rerun with PITCHFORK_COVERAGE_STATS=1 environment variable.)")?;
}
writeln!(f)?;
if path_stats.num_ct_violations > 0 {
match self.first_ct_violation() {
None => panic!("we counted a ct violation, but now can't find one"),
Some(violation_message) => {
writeln!(f, "{} {}", self.funcname, "is not constant-time".red())?;
if let Some(filename) = &self.error_filename {
writeln!(f, "All errors have been logged to {}", filename)?;
writeln!(f, " and the first constant-time violation is described below:\n\n{}", violation_message)?;
} else {
writeln!(f, "First constant-time violation encountered:\n\n{}", violation_message)?;
}
},
}
} else if !is_ct {
match self.first_error_or_violation() {
None => panic!("we counted a non-ct path, but now can't find one"),
Some(ConstantTimeResultForPath::IsConstantTime) => panic!("first_error_or_violation shouldn't return an IsConstantTime"),
Some(ConstantTimeResultForPath::NotConstantTime { .. }) => panic!("we counted no ct violations, but now somehow found one"),
Some(ConstantTimeResultForPath::OtherError { full_message, .. }) => {
if let Some(filename) = &self.error_filename {
writeln!(f, "All errors have been logged to {}", filename)?;
writeln!(f, " and the first error encountered is described below:\n\n{}", full_message)?;
} else {
writeln!(f, "First error encountered:\n\n{}", full_message)?;
}
},
}
} else {
writeln!(f, "{} {}", self.funcname, "is constant-time".green())?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct PitchforkConfig {
pub keep_going: bool,
pub dump_errors: bool,
pub progress_updates: bool,
pub debug_logging: bool,
}
impl Default for PitchforkConfig {
fn default() -> Self {
Self {
keep_going: false,
dump_errors: true,
progress_updates: true,
debug_logging: false,
}
}
}
pub fn check_for_ct_violation_in_inputs<'p>(
funcname: &'p str,
project: &'p Project,
config: Config<'p, secret::Backend>,
pitchfork_config: &PitchforkConfig,
) -> ConstantTimeResultForFunction<'p> {
lazy_static! {
static ref BLANK_STRUCT_DESCRIPTIONS: StructDescriptions = StructDescriptions::new();
}
let (func, _) = project.get_func_by_name(funcname).expect("Failed to find function");
let args = func.parameters.iter().map(|p| AbstractData::sec_integer(layout::size(&p.ty))).collect();
check_for_ct_violation(funcname, project, Some(args), &BLANK_STRUCT_DESCRIPTIONS, config, pitchfork_config)
}
pub fn check_for_ct_violation<'p>(
funcname: &'p str,
project: &'p Project,
args: Option<Vec<AbstractData>>,
sd: &StructDescriptions,
mut config: Config<'p, secret::Backend>,
pitchfork_config: &PitchforkConfig,
) -> ConstantTimeResultForFunction<'p> {
if !config.function_hooks.is_hooked("hook_uninitialized_function_pointer") {
config.function_hooks.add("hook_uninitialized_function_pointer", &hook_uninitialized_function_pointer);
}
if !config.function_hooks.has_default_hook() {
config.function_hooks.add_default_hook(&pitchfork_default_hook);
}
let (log_filename, error_filename) = {
use chrono::prelude::Local;
let time = Local::now().format("%Y-%m-%d_%H:%M:%S").to_string();
let dir = format!("logs/{}", funcname);
let log_filename = if pitchfork_config.progress_updates {
std::fs::create_dir_all(&dir).unwrap();
Some(format!("{}/log_{}.log", dir, time))
} else {
None
};
let error_filename = if pitchfork_config.keep_going && pitchfork_config.dump_errors {
std::fs::create_dir_all(&dir).unwrap();
Some(format!("{}/errors_{}.log", dir, time))
} else {
None
};
(log_filename, error_filename)
};
let mut progress_updater: Box<dyn ProgressUpdater<secret::Backend>> = if pitchfork_config.progress_updates {
Box::new(initialize_progress_updater(log_filename.as_ref().unwrap(), funcname, &mut config, pitchfork_config.debug_logging))
} else {
Box::new(NullProgressUpdater { })
};
let sd_names: HashSet<_> = sd.iter().map(|(name, _)| name).collect();
let proj_names: HashSet<_> = project.all_named_struct_types().map(|(name, _, _)| name).collect();
for name in sd_names.difference(&proj_names) {
panic!("Struct name {:?} appears in StructDescriptions but not found in the Project", name);
}
info!("Checking function {:?} for ct violations", funcname);
let mut em: ExecutionManager<secret::Backend> = symex_function(funcname, project, config);
info!("Allocating memory for function parameters");
let params = em.state().cur_loc.func.parameters.iter();
match args {
Some(args) => {
assert_eq!(params.len(), args.len(), "Function {:?} has {} parameters, but we received only {} argument `AbstractData`s", funcname, params.len(), args.len());
allocation::allocate_args(project, em.mut_state(), sd, params.zip(args.into_iter())).unwrap();
},
None => {
allocation::allocate_args(project, em.mut_state(), sd, params.zip(std::iter::repeat(AbstractData::default()))).unwrap();
},
}
debug!("Done allocating memory for function parameters");
let mut blocks_seen = BlocksSeen::new();
let mangled_funcname = {
let (func, _) = project.get_func_by_name(funcname).unwrap();
&func.name
};
let mut path_results = Vec::new();
let mut error_file = error_filename.as_ref().map(|filename| {
use std::fs::File;
use std::path::Path;
File::create(&Path::new(filename))
.unwrap_or_else(|e| panic!("Failed to open file {} to dump errors: {}", filename, e))
});
loop {
match em.next() {
Some(Ok(_)) => {
info!("Finished a path with no errors or violations");
blocks_seen.update_with_current_path(&em);
let path_result = ConstantTimeResultForPath::IsConstantTime;
progress_updater.update_path_result(&path_result);
path_results.push(path_result);
},
Some(Err(error)) => {
blocks_seen.update_with_current_path(&em);
let mut full_message = em.state().full_error_message_with_context(error.clone());
if full_message.contains("debug-level logging messages") {
full_message.push_str("note: To enable debug-level logging messages when `progress_updates` is\n");
full_message.push_str(" enabled in `PitchforkConfig`, use the `debug_logging` setting\n");
}
if let Some(ref mut file) = error_file {
use std::io::Write;
write!(file, "==================\n\n{}\n\n", full_message)
.unwrap_or_else(|e| warn!("Failed to write an error message to file: {}", e));
}
let path_result = if full_message.contains("Constant-time violation:") {
info!("Found a constant-time violation on this path");
ConstantTimeResultForPath::NotConstantTime { violation_message: full_message }
} else {
info!("Encountered an error (other than a constant-time violation) on this path: {}", error);
ConstantTimeResultForPath::OtherError { error, full_message }
};
progress_updater.update_path_result(&path_result);
path_results.push(path_result);
if !pitchfork_config.keep_going {
break;
}
},
None => break,
}
}
let block_coverage = blocks_seen.full_coverage_stats();
info!("Block coverage of toplevel function ({:?}): {:.1}%", funcname, 100.0 * block_coverage.get(mangled_funcname).unwrap().percentage);
progress_updater.finalize();
ConstantTimeResultForFunction {
funcname,
mangled_funcname,
path_results,
block_coverage,
error_filename,
}
}
fn hook_uninitialized_function_pointer(
proj: &Project,
state: &mut State<secret::Backend>,
call: &dyn IsCall,
) -> Result<ReturnValue<secret::BV>> {
info!("Function pointer is uninitialized; trying Pitchfork default hook");
default_hook::pitchfork_default_hook(proj, state, call)
}
trait ProgressUpdater<B: Backend> {
fn update_progress(&self, state: &State<B>) -> Result<()>;
fn update_path_result(&self, path_result: &ConstantTimeResultForPath);
fn process_log_message(&self, record: &log::Record) -> std::result::Result<(), Box<dyn std::error::Error + Sync + Send>>;
fn finalize(&mut self);
}
struct NullProgressUpdater { }
impl<B: Backend> ProgressUpdater<B> for NullProgressUpdater {
fn update_progress(&self, _state: &State<B>) -> Result<()> { Ok(()) }
fn update_path_result(&self, _path_result: &ConstantTimeResultForPath) { }
fn process_log_message(&self, _record: &log::Record) -> std::result::Result<(), Box<dyn std::error::Error + Sync + Send>> { Ok(()) }
fn finalize(&mut self) { }
}
#[cfg(feature = "progress-updates")]
fn initialize_progress_updater<B: Backend>(log_filename: &str, funcname: &str, config: &mut Config<B>, debug_logging: bool) -> progress::ProgressUpdater {
progress::initialize_progress_updater(log_filename, funcname, config, debug_logging)
}
#[cfg(not(feature = "progress-updates"))]
fn initialize_progress_updater<B: Backend>(_log_filename: &str, _funcname: &str, _config: &mut Config<B>, _debug_logging: bool) -> NullProgressUpdater {
NullProgressUpdater { }
}