use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::fmt;
use std::time::{Duration, Instant};
pub struct AddrSinkAnalysis;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ProgramPoint {
pub block_id: usize,
pub stmt_index: usize,
pub kind: ProgramPointKind,
}
impl ProgramPoint {
pub const fn new(block_id: usize, stmt_index: usize, kind: ProgramPointKind) -> Self {
Self {
block_id,
stmt_index,
kind,
}
}
pub const fn block_entry(block_id: usize) -> Self {
Self {
block_id,
stmt_index: 0,
kind: ProgramPointKind::BlockEntry,
}
}
pub const fn block_exit(block_id: usize) -> Self {
Self {
block_id,
stmt_index: usize::MAX,
kind: ProgramPointKind::BlockExit,
}
}
}
impl fmt::Display for ProgramPoint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "B{}.{}.{:?}", self.block_id, self.stmt_index, self.kind)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ProgramPointKind {
BlockEntry,
BeforeStmt,
AfterStmt,
BlockExit,
PostStmt,
PreStmt,
EdgeTransition,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SymExpr {
Symbolic(String),
ConstantInt(i64),
ConstantFloat(u64), Region(SymRegion),
BinaryOp(BinOpKind, Box<SymExpr>, Box<SymExpr>),
UnaryOp(UnOpKind, Box<SymExpr>),
Cast(SymCastKind, Box<SymExpr>),
Unknown,
Undefined,
Conjured(SymConjured),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BinOpKind {
Add,
Sub,
Mul,
Div,
Rem,
Shl,
Shr,
And,
Or,
Xor,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum UnOpKind {
Neg,
Not,
BitNot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SymCastKind {
IntToInt,
IntToFloat,
FloatToInt,
FloatToFloat,
ZeroExt,
SignExt,
BitCast,
PtrToInt,
IntToPtr,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SymRegion {
VarRegion(String),
FieldRegion(Box<SymRegion>, String),
ElementRegion(Box<SymRegion>, Box<SymExpr>),
HeapRegion(usize),
GlobalRegion(String),
StackLocal(String, usize),
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SymConjured {
pub id: usize,
pub description: String,
pub kind: ConjuredKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ConjuredKind {
HeapAlloc,
ReturnValue,
CallArgument,
ConstructedObject,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Constraint {
Range(RangeConstraint),
Nullability(NullabilityConstraint),
Taint(TaintConstraint),
BugOn(BugOnConstraint),
Assumption(SymExpr, bool),
DeadSymbols(Vec<SymExpr>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct RangeConstraint {
pub expr: SymExpr,
pub lo: i64,
pub hi: i64,
pub is_signed: bool,
}
impl RangeConstraint {
pub fn new(expr: SymExpr, lo: i64, hi: i64, is_signed: bool) -> Self {
Self {
expr,
lo,
hi,
is_signed,
}
}
pub fn single(expr: SymExpr, value: i64, is_signed: bool) -> Self {
Self {
expr,
lo: value,
hi: value,
is_signed,
}
}
pub fn is_single_value(&self) -> bool {
self.lo == self.hi
}
pub fn overlaps(&self, other: &RangeConstraint) -> bool {
self.lo <= other.hi && other.lo <= self.hi
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum NullabilityConstraint {
Null(SymExpr),
NonNull(SymExpr),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct TaintConstraint {
pub expr: SymExpr,
pub source: TaintSource,
pub propagation: TaintPropagation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TaintSource {
UserInput,
Network,
FileSystem,
Environment,
Untrusted,
}
impl fmt::Display for TaintSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UserInput => write!(f, "user_input"),
Self::Network => write!(f, "network"),
Self::FileSystem => write!(f, "filesystem"),
Self::Environment => write!(f, "environment"),
Self::Untrusted => write!(f, "untrusted"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TaintPropagation {
Direct,
Transitive,
Sanitized,
Unsanitized,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BugOnConstraint {
pub condition: SymExpr,
pub bug_type: String,
pub message: String,
pub severity: BugSeverity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BugSeverity {
Warning,
Error,
Fatal,
Note,
}
impl std::fmt::Display for BugSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BugSeverity::Warning => write!(f, "warning"),
BugSeverity::Error => write!(f, "error"),
BugSeverity::Fatal => write!(f, "fatal"),
BugSeverity::Note => write!(f, "note"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramState {
pub store: HashMap<SymRegion, SymExpr>,
pub constraints: Vec<Constraint>,
pub environment: HashMap<String, SymRegion>,
pub location: ProgramPoint,
pub call_stack: Vec<CallSite>,
pub state_id: usize,
pub is_bug_state: bool,
pub tags: HashMap<String, String>,
}
impl ProgramState {
pub fn new(location: ProgramPoint, state_id: usize) -> Self {
Self {
store: HashMap::new(),
constraints: Vec::new(),
environment: HashMap::new(),
location,
call_stack: Vec::new(),
state_id,
is_bug_state: false,
tags: HashMap::new(),
}
}
pub fn with_call_stack(mut self, stack: Vec<CallSite>) -> Self {
self.call_stack = stack;
self
}
pub fn bind(&mut self, name: &str, region: SymRegion) {
self.environment.insert(name.to_string(), region);
}
pub fn lookup(&self, name: &str) -> Option<&SymRegion> {
self.environment.get(name)
}
pub fn store_value(&mut self, region: SymRegion, value: SymExpr) {
self.store.insert(region, value);
}
pub fn load_value(&self, region: &SymRegion) -> Option<&SymExpr> {
self.store.get(region)
}
pub fn add_constraint(&mut self, constraint: Constraint) {
self.constraints.push(constraint);
}
pub fn add_tag(&mut self, key: &str, value: &str) {
self.tags.insert(key.to_string(), value.to_string());
}
pub fn get_tag(&self, key: &str) -> Option<&str> {
self.tags.get(key).map(|s| s.as_str())
}
pub fn has_tag(&self, key: &str) -> bool {
self.tags.contains_key(key)
}
}
impl fmt::Display for ProgramState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "State#{} @ {}", self.state_id, self.location)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CallSite {
pub function_name: String,
pub call_point: ProgramPoint,
pub return_state: Option<Box<ProgramState>>,
}
impl CallSite {
pub fn new(function_name: &str, call_point: ProgramPoint) -> Self {
Self {
function_name: function_name.to_string(),
call_point,
return_state: None,
}
}
}
#[derive(Debug, Clone)]
pub struct ExplodedNode {
pub id: usize,
pub state: ProgramState,
pub predecessor: Option<usize>,
pub successors: Vec<usize>,
pub is_sink: bool,
pub generation: usize,
pub checker_tag: Option<String>,
}
impl ExplodedNode {
pub fn new(id: usize, state: ProgramState) -> Self {
Self {
id,
state,
predecessor: None,
successors: Vec::new(),
is_sink: false,
generation: 0,
checker_tag: None,
}
}
pub fn with_predecessor(mut self, pred_id: usize) -> Self {
self.predecessor = Some(pred_id);
self
}
pub fn with_generation(mut self, r#gen: usize) -> Self {
self.generation = r#gen;
self
}
pub fn mark_sink(mut self) -> Self {
self.is_sink = true;
self
}
pub fn set_checker_tag(mut self, tag: &str) -> Self {
self.checker_tag = Some(tag.to_string());
self
}
}
impl fmt::Display for ExplodedNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sink = if self.is_sink { " [SINK]" } else { "" };
write!(
f,
"Node#{} gen={}{} {}",
self.id, self.generation, sink, self.state
)
}
}
#[derive(Debug, Clone)]
pub struct ExplodedGraph {
pub nodes: Vec<ExplodedNode>,
pub root: usize,
pub sink_count: usize,
pub max_nodes: usize,
pub total_generations: usize,
pub worklist: VecDeque<usize>,
pub worklist_strategy: WorklistStrategy,
pub processed: HashSet<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorklistStrategy {
BFS,
DFS,
PriorityQueue,
Hybrid,
}
impl fmt::Display for WorklistStrategy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BFS => write!(f, "BFS"),
Self::DFS => write!(f, "DFS"),
Self::PriorityQueue => write!(f, "PriorityQueue"),
Self::Hybrid => write!(f, "Hybrid"),
}
}
}
impl ExplodedGraph {
pub fn new(root_state: ProgramState, max_nodes: usize) -> Self {
let root = ExplodedNode::new(0, root_state).with_generation(0);
let mut worklist = VecDeque::new();
worklist.push_back(0);
Self {
nodes: vec![root],
root: 0,
sink_count: 0,
max_nodes,
total_generations: 0,
worklist,
worklist_strategy: WorklistStrategy::BFS,
processed: HashSet::new(),
}
}
pub fn with_strategy(mut self, strategy: WorklistStrategy) -> Self {
self.worklist_strategy = strategy;
self
}
pub fn add_node(&mut self, state: ProgramState, predecessor: usize) -> usize {
if self.nodes.len() >= self.max_nodes {
return predecessor; }
let id = self.nodes.len();
let r#gen = self.nodes[predecessor].generation + 1;
let node = ExplodedNode::new(id, state)
.with_predecessor(predecessor)
.with_generation(r#gen);
self.nodes[predecessor].successors.push(id);
self.nodes.push(node);
self.total_generations = self.total_generations.max(r#gen);
id
}
pub fn add_sink(&mut self, state: ProgramState, predecessor: usize) -> usize {
if self.nodes.len() >= self.max_nodes {
return predecessor;
}
let id = self.nodes.len();
let r#gen = self.nodes[predecessor].generation + 1;
let node = ExplodedNode::new(id, state)
.with_predecessor(predecessor)
.with_generation(r#gen)
.mark_sink();
self.nodes[predecessor].successors.push(id);
self.nodes.push(node);
self.sink_count += 1;
id
}
pub fn push_worklist(&mut self, node_id: usize) {
match self.worklist_strategy {
WorklistStrategy::BFS => self.worklist.push_back(node_id),
WorklistStrategy::DFS => self.worklist.push_front(node_id),
WorklistStrategy::PriorityQueue | WorklistStrategy::Hybrid => {
self.worklist.push_back(node_id);
}
}
}
pub fn pop_worklist(&mut self) -> Option<usize> {
self.worklist.pop_front()
}
pub fn is_worklist_empty(&self) -> bool {
self.worklist.is_empty()
}
pub fn mark_processed(&mut self, node_id: usize) {
self.processed.insert(node_id);
}
pub fn is_processed(&self, node_id: usize) -> bool {
self.processed.contains(&node_id)
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn get_node(&self, id: usize) -> Option<&ExplodedNode> {
self.nodes.get(id)
}
pub fn path_to(&self, target: usize) -> Vec<usize> {
let mut path = Vec::new();
let mut current = target;
while current != self.root {
path.push(current);
if let Some(node) = self.nodes.get(current) {
if let Some(pred) = node.predecessor {
current = pred;
} else {
break;
}
} else {
break;
}
}
path.push(self.root);
path.reverse();
path
}
pub fn find_sinks(&self) -> Vec<usize> {
self.nodes
.iter()
.filter(|n| n.is_sink)
.map(|n| n.id)
.collect()
}
pub fn stats(&self) -> GraphStats {
GraphStats {
total_nodes: self.nodes.len(),
sinks: self.sink_count,
max_generation: self.total_generations,
max_degree: self
.nodes
.iter()
.map(|n| n.successors.len())
.max()
.unwrap_or(0),
processed: self.processed.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct GraphStats {
pub total_nodes: usize,
pub sinks: usize,
pub max_generation: usize,
pub max_degree: usize,
pub processed: usize,
}
impl fmt::Display for GraphStats {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"nodes={}, sinks={}, max_gen={}, max_degree={}, processed={}",
self.total_nodes, self.sinks, self.max_generation, self.max_degree, self.processed
)
}
}
#[derive(Debug)]
pub struct SymbolicExecutionEngine {
pub graph: ExplodedGraph,
pub sym_counter: usize,
pub region_counter: usize,
pub state_counter: usize,
pub config: AnalysisConfig,
pub stats: SymExecStats,
pub checkers: Vec<Box<dyn Checker>>,
pub inline_config: InlineConfig,
}
#[derive(Debug, Clone)]
pub struct AnalysisConfig {
pub max_nodes: usize,
pub max_generations: usize,
pub max_call_depth: usize,
pub enable_state_merging: bool,
pub enable_constraint_optimization: bool,
pub enable_inlining: bool,
pub worklist_strategy: WorklistStrategy,
pub analysis_timeout_ms: u64,
}
impl Default for AnalysisConfig {
fn default() -> Self {
Self {
max_nodes: 100_000,
max_generations: 200,
max_call_depth: 10,
enable_state_merging: true,
enable_constraint_optimization: true,
enable_inlining: true,
worklist_strategy: WorklistStrategy::BFS,
analysis_timeout_ms: 30_000,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SymExecStats {
pub nodes_explored: usize,
pub states_merged: usize,
pub constraints_resolved: usize,
pub bugs_found: usize,
pub inlined_calls: usize,
pub timeouts: usize,
pub elapsed: Duration,
}
#[derive(Debug, Clone)]
pub struct InlineConfig {
pub enabled: bool,
pub max_inline_depth: usize,
pub max_inlined_function_size: usize,
pub always_inline: HashSet<String>,
pub never_inline: HashSet<String>,
pub inline_threshold_stmts: usize,
pub inline_constructors: bool,
pub inline_accessors: bool,
pub inline_single_block: bool,
}
impl Default for InlineConfig {
fn default() -> Self {
Self {
enabled: true,
max_inline_depth: 5,
max_inlined_function_size: 200,
always_inline: HashSet::new(),
never_inline: HashSet::new(),
inline_threshold_stmts: 20,
inline_constructors: true,
inline_accessors: true,
inline_single_block: true,
}
}
}
impl InlineConfig {
pub fn should_inline(&self, fn_name: &str, fn_size: usize, call_depth: usize) -> bool {
if !self.enabled {
return false;
}
if self.never_inline.contains(fn_name) {
return false;
}
if call_depth >= self.max_inline_depth {
return false;
}
if self.always_inline.contains(fn_name) {
return true;
}
fn_size <= self.max_inlined_function_size
|| (self.inline_single_block && fn_size <= self.inline_threshold_stmts)
}
}
impl SymbolicExecutionEngine {
pub fn new(config: AnalysisConfig) -> Self {
let root_location = ProgramPoint::block_entry(0);
let root_state = ProgramState::new(root_location, 0);
let graph = ExplodedGraph::new(root_state, config.max_nodes)
.with_strategy(config.worklist_strategy);
Self {
graph,
sym_counter: 0,
region_counter: 0,
state_counter: 1,
config,
stats: SymExecStats::default(),
checkers: Vec::new(),
inline_config: InlineConfig::default(),
}
}
pub fn with_inline_config(mut self, config: InlineConfig) -> Self {
self.inline_config = config;
self
}
pub fn register_checker(&mut self, checker: Box<dyn Checker>) {
self.checkers.push(checker);
}
pub fn fresh_symbol(&mut self, description: &str, kind: ConjuredKind) -> SymExpr {
self.sym_counter += 1;
SymExpr::Conjured(SymConjured {
id: self.sym_counter,
description: description.to_string(),
kind,
})
}
pub fn fresh_region(&mut self, base: &str) -> SymRegion {
self.region_counter += 1;
SymRegion::HeapRegion(self.region_counter)
}
pub fn fresh_state(&mut self, location: ProgramPoint) -> ProgramState {
let id = self.state_counter;
self.state_counter += 1;
ProgramState::new(location, id)
}
pub fn eval_concrete(&self, expr: &SymExpr) -> Option<i64> {
match expr {
SymExpr::ConstantInt(v) => Some(*v),
SymExpr::BinaryOp(op, left, right) => {
let l = self.eval_concrete(left)?;
let r = self.eval_concrete(right)?;
match op {
BinOpKind::Add => Some(l.wrapping_add(r)),
BinOpKind::Sub => Some(l.wrapping_sub(r)),
BinOpKind::Mul => Some(l.wrapping_mul(r)),
BinOpKind::Div if r != 0 => Some(l / r),
BinOpKind::Rem if r != 0 => Some(l % r),
BinOpKind::Shl => Some(l << (r as u32)),
BinOpKind::Shr => Some(l >> (r as u32)),
BinOpKind::And => Some(l & r),
BinOpKind::Or => Some(l | r),
BinOpKind::Xor => Some(l ^ r),
BinOpKind::Eq => Some(if l == r { 1 } else { 0 }),
BinOpKind::Ne => Some(if l != r { 1 } else { 0 }),
BinOpKind::Lt => Some(if l < r { 1 } else { 0 }),
BinOpKind::Le => Some(if l <= r { 1 } else { 0 }),
BinOpKind::Gt => Some(if l > r { 1 } else { 0 }),
BinOpKind::Ge => Some(if l >= r { 1 } else { 0 }),
_ => None,
}
}
SymExpr::UnaryOp(op, operand) => {
let v = self.eval_concrete(operand)?;
match op {
UnOpKind::Neg => Some(-v),
UnOpKind::Not => Some(if v == 0 { 1 } else { 0 }),
UnOpKind::BitNot => Some(!v),
}
}
SymExpr::Cast(kind, operand) => {
let v = self.eval_concrete(operand)?;
match kind {
SymCastKind::ZeroExt => Some(v as u32 as i64),
SymCastKind::SignExt => Some(v as i32 as i64),
_ => Some(v),
}
}
_ => None,
}
}
pub fn can_merge(&self, s1: &ProgramState, s2: &ProgramState) -> bool {
s1.location == s2.location && s1.call_stack == s2.call_stack
}
pub fn merge_states(&self, s1: &ProgramState, s2: &ProgramState) -> Option<ProgramState> {
if !self.can_merge(s1, s2) {
return None;
}
let mut merged = ProgramState::new(s1.location, s1.state_id);
merged.call_stack = s1.call_stack.clone();
for (region, val1) in &s1.store {
if let Some(val2) = s2.store.get(region) {
if val1 == val2 {
merged.store.insert(region.clone(), val1.clone());
}
}
}
for (name, region1) in &s1.environment {
if let Some(region2) = s2.environment.get(name) {
if region1 == region2 {
merged.environment.insert(name.clone(), region1.clone());
}
}
}
let mut seen = HashSet::new();
for c in s1.constraints.iter().chain(s2.constraints.iter()) {
let hash_key = format!("{:?}", c);
if seen.insert(hash_key) {
merged.constraints.push(c.clone());
}
}
Some(merged)
}
pub fn run(&mut self, _cfg: &AnalysisCFG) -> Vec<BugReport> {
let start = Instant::now();
let mut reports = Vec::new();
self.graph.push_worklist(self.graph.root);
while !self.graph.is_worklist_empty() {
if start.elapsed().as_millis() > self.config.analysis_timeout_ms as u128 {
self.stats.timeouts += 1;
break;
}
if self.graph.node_count() >= self.config.max_nodes {
break;
}
let node_id = match self.graph.pop_worklist() {
Some(id) => id,
None => break,
};
if self.graph.is_processed(node_id) {
continue;
}
self.graph.mark_processed(node_id);
self.stats.nodes_explored += 1;
let state = match self.graph.get_node(node_id) {
Some(node) => node.state.clone(),
None => continue,
};
for checker in &self.checkers {
if let Some(bugs) = checker.check(&state, &self.graph) {
let bug_count = bugs.len();
reports.extend(bugs);
self.stats.bugs_found += bug_count;
}
}
let successors = self.explore_successors(&state);
for succ_state in successors {
let succ_id = self.graph.add_node(succ_state, node_id);
self.graph.push_worklist(succ_id);
}
}
self.stats.elapsed = start.elapsed();
reports
}
fn explore_successors(&mut self, _state: &ProgramState) -> Vec<ProgramState> {
let mut successors = Vec::new();
match _state.location.kind {
ProgramPointKind::PostStmt => {
let next_point = ProgramPoint::new(
_state.location.block_id,
_state.location.stmt_index + 1,
ProgramPointKind::PreStmt,
);
successors.push(ProgramState::new(next_point, self.state_counter));
self.state_counter += 1;
}
ProgramPointKind::BlockEntry => {
let point =
ProgramPoint::new(_state.location.block_id, 0, ProgramPointKind::PreStmt);
successors.push(ProgramState::new(point, self.state_counter));
self.state_counter += 1;
}
_ => {}
}
successors
}
pub fn reset(&mut self) {
let root_location = ProgramPoint::block_entry(0);
let root_state = ProgramState::new(root_location, 0);
self.graph = ExplodedGraph::new(root_state, self.config.max_nodes)
.with_strategy(self.config.worklist_strategy);
self.sym_counter = 0;
self.region_counter = 0;
self.state_counter = 1;
self.stats = SymExecStats::default();
}
}
impl Default for SymbolicExecutionEngine {
fn default() -> Self {
Self::new(AnalysisConfig::default())
}
}
#[derive(Debug, Clone)]
pub struct AnalysisCFG {
pub blocks: Vec<AnalysisBlock>,
pub entry: usize,
pub exit: usize,
pub function_name: String,
}
#[derive(Debug, Clone)]
pub struct AnalysisBlock {
pub id: usize,
pub stmts: Vec<AnalysisStmt>,
pub successors: Vec<usize>,
pub predecessors: Vec<usize>,
}
#[derive(Debug, Clone)]
pub enum AnalysisStmt {
Assign(String, SymExpr),
Call(String, String, Vec<SymExpr>),
Return(Option<SymExpr>),
Branch(SymExpr, usize, usize),
Switch(SymExpr, Vec<(i64, usize)>, usize),
DeclRef(String),
MemberExpr(String, String),
BinaryOp(BinOpKind, String, String, String),
UnaryOp(UnOpKind, String, String),
CastExpr(SymCastKind, String, String),
Alloca(String, usize),
Load(String, String),
Store(String, SymExpr),
Gep(String, String, Vec<SymExpr>),
NullCheck(String),
BoundsCheck(SymExpr, SymExpr, SymExpr),
Assert(SymExpr, String),
NoOp,
}
impl AnalysisCFG {
pub fn new(function_name: &str) -> Self {
let entry_block = AnalysisBlock {
id: 0,
stmts: Vec::new(),
successors: Vec::new(),
predecessors: Vec::new(),
};
let exit_block = AnalysisBlock {
id: 1,
stmts: vec![AnalysisStmt::Return(None)],
successors: Vec::new(),
predecessors: vec![0],
};
Self {
blocks: vec![entry_block, exit_block],
entry: 0,
exit: 1,
function_name: function_name.to_string(),
}
}
pub fn add_block(&mut self, mut block: AnalysisBlock) -> usize {
let id = self.blocks.len();
block.id = id;
self.blocks.push(block);
id
}
pub fn add_edge(&mut self, from: usize, to: usize) {
self.blocks[from].successors.push(to);
self.blocks[to].predecessors.push(from);
}
pub fn block_count(&self) -> usize {
self.blocks.len()
}
}
#[derive(Debug, Clone)]
pub struct ConstraintSolver {
pub range_constraints: HashMap<SymExpr, RangeConstraint>,
pub nullability_map: HashMap<SymExpr, NullabilityConstraint>,
pub taint_constraints: Vec<TaintConstraint>,
pub bugon_constraints: Vec<BugOnConstraint>,
pub assumptions: Vec<(SymExpr, bool)>,
pub is_contradictory: bool,
}
impl ConstraintSolver {
pub fn new() -> Self {
Self {
range_constraints: HashMap::new(),
nullability_map: HashMap::new(),
taint_constraints: Vec::new(),
bugon_constraints: Vec::new(),
assumptions: Vec::new(),
is_contradictory: false,
}
}
pub fn add_range(&mut self, expr: SymExpr, lo: i64, hi: i64, is_signed: bool) {
let key = expr.clone();
if let Some(existing) = self.range_constraints.get(&key) {
let new_lo = existing.lo.max(lo);
let new_hi = existing.hi.min(hi);
if new_lo > new_hi {
self.is_contradictory = true;
return;
}
self.range_constraints
.insert(key, RangeConstraint::new(expr, new_lo, new_hi, is_signed));
} else {
self.range_constraints
.insert(key, RangeConstraint::new(expr, lo, hi, is_signed));
}
}
pub fn add_nullability(&mut self, expr: SymExpr, constraint: NullabilityConstraint) {
if let Some(existing) = self.nullability_map.get(&expr) {
if *existing != constraint {
self.is_contradictory = true;
return;
}
}
self.nullability_map.insert(expr, constraint);
}
pub fn add_taint(&mut self, constraint: TaintConstraint) {
self.taint_constraints.push(constraint);
}
pub fn add_bugon(&mut self, constraint: BugOnConstraint) {
self.bugon_constraints.push(constraint);
}
pub fn add_assumption(&mut self, expr: SymExpr, assumed_true: bool) {
self.assumptions.push((expr, assumed_true));
}
pub fn is_feasible(&self) -> bool {
!self.is_contradictory
}
pub fn get_range(&self, expr: &SymExpr) -> Option<&RangeConstraint> {
self.range_constraints.get(expr)
}
pub fn is_null(&self, expr: &SymExpr) -> Option<bool> {
self.nullability_map
.get(expr)
.map(|c| matches!(c, NullabilityConstraint::Null(_)))
}
pub fn is_non_null(&self, expr: &SymExpr) -> Option<bool> {
self.nullability_map
.get(expr)
.map(|c| matches!(c, NullabilityConstraint::NonNull(_)))
}
pub fn is_tainted(&self, expr: &SymExpr) -> bool {
self.taint_constraints.iter().any(|t| &t.expr == expr)
}
pub fn get_taint_source(&self, expr: &SymExpr) -> Option<TaintSource> {
self.taint_constraints
.iter()
.find(|t| &t.expr == expr)
.map(|t| t.source)
}
pub fn check_bugons(&self) -> Vec<BugOnConstraint> {
self.bugon_constraints
.iter()
.filter(|b| {
true
})
.cloned()
.collect()
}
pub fn dump(&self) -> String {
let mut s = String::new();
s.push_str(&format!(
"ConstraintSolver{{feasible={}, ",
self.is_feasible()
));
s.push_str(&format!("ranges={}, ", self.range_constraints.len()));
s.push_str(&format!("nullability={}, ", self.nullability_map.len()));
s.push_str(&format!("taints={}, ", self.taint_constraints.len()));
s.push_str(&format!("bugons={}, ", self.bugon_constraints.len()));
s.push_str(&format!("assumptions={}}}", self.assumptions.len()));
s
}
pub fn merge(&mut self, other: &ConstraintSolver) {
for (expr, rc) in &other.range_constraints {
self.add_range(expr.clone(), rc.lo, rc.hi, rc.is_signed);
}
for (expr, nc) in &other.nullability_map {
self.add_nullability(expr.clone(), nc.clone());
}
self.taint_constraints
.extend(other.taint_constraints.clone());
self.bugon_constraints
.extend(other.bugon_constraints.clone());
}
}
impl Default for ConstraintSolver {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct BugReport {
pub id: usize,
pub bug_type: String,
pub checker_name: String,
pub message: String,
pub severity: BugSeverity,
pub location: ProgramPoint,
pub path: Vec<ProgramPoint>,
pub state_snapshot: Option<ProgramState>,
pub category: BugCategory,
pub fixit_hint: Option<String>,
pub cwe_id: Option<String>,
pub source_location: Option<SourceLocation>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BugCategory {
NullDereference,
UseAfterFree,
DoubleFree,
MemoryLeak,
BufferOverflow,
UninitializedRead,
DeadCode,
LogicError,
DivisionByZero,
ReturnStackAddress,
UndefinedBehavior,
Security,
API,
Taint,
Concurrency,
Custom,
}
impl fmt::Display for BugCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NullDereference => write!(f, "null-dereference"),
Self::UseAfterFree => write!(f, "use-after-free"),
Self::DoubleFree => write!(f, "double-free"),
Self::MemoryLeak => write!(f, "memory-leak"),
Self::BufferOverflow => write!(f, "buffer-overflow"),
Self::UninitializedRead => write!(f, "uninitialized-read"),
Self::DeadCode => write!(f, "dead-code"),
Self::LogicError => write!(f, "logic-error"),
Self::DivisionByZero => write!(f, "division-by-zero"),
Self::ReturnStackAddress => write!(f, "return-stack-address"),
Self::UndefinedBehavior => write!(f, "undefined-behavior"),
Self::Security => write!(f, "security"),
Self::API => write!(f, "api"),
Self::Taint => write!(f, "taint"),
Self::Concurrency => write!(f, "concurrency"),
Self::Custom => write!(f, "custom"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
pub file: String,
pub line: usize,
pub column: usize,
pub end_line: Option<usize>,
pub end_column: Option<usize>,
}
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
impl BugReport {
pub fn new(
id: usize,
bug_type: &str,
message: &str,
severity: BugSeverity,
location: ProgramPoint,
category: BugCategory,
) -> Self {
Self {
id,
bug_type: bug_type.to_string(),
checker_name: String::new(),
message: message.to_string(),
severity,
location,
path: Vec::new(),
state_snapshot: None,
category,
fixit_hint: None,
cwe_id: None,
source_location: None,
}
}
pub fn with_path(mut self, path: Vec<ProgramPoint>) -> Self {
self.path = path;
self
}
pub fn with_checker(mut self, name: &str) -> Self {
self.checker_name = name.to_string();
self
}
pub fn with_fixit(mut self, hint: &str) -> Self {
self.fixit_hint = Some(hint.to_string());
self
}
pub fn with_cwe(mut self, cwe: &str) -> Self {
self.cwe_id = Some(cwe.to_string());
self
}
pub fn with_source(mut self, loc: SourceLocation) -> Self {
self.source_location = Some(loc);
self
}
}
impl fmt::Display for BugReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[{}] {}: {} @ {}",
self.id, self.category, self.message, self.location
)
}
}
pub trait Checker: fmt::Debug + Send + Sync {
fn name(&self) -> &str;
fn category(&self) -> &str;
fn enabled_by_default(&self) -> bool {
true
}
fn check(&self, state: &ProgramState, graph: &ExplodedGraph) -> Option<Vec<BugReport>>;
fn configure(&mut self, _options: &HashMap<String, String>) {}
}
#[derive(Debug, Clone)]
pub struct BoolAssignmentChecker;
#[derive(Debug, Clone)]
pub struct DivZeroChecker;
#[derive(Debug, Clone)]
pub struct NullDereferenceChecker;
#[derive(Debug, Clone)]
pub struct UndefResultChecker;
#[derive(Debug, Clone)]
pub struct VLASizeChecker;
#[derive(Debug, Clone)]
pub struct CallAndMessageChecker;
#[derive(Debug, Clone)]
pub struct StackAddrLeakChecker;
#[derive(Debug, Clone)]
pub struct DynamicTypeChecker;
#[derive(Debug, Clone)]
pub struct IdenticalExprChecker;
#[derive(Debug, Clone)]
pub struct SizeofPointerChecker;
#[derive(Debug, Clone)]
pub struct UnsafeBufferUsageChecker;
#[derive(Debug, Clone)]
pub struct UninitializedObjectChecker;
#[derive(Debug, Clone)]
pub struct ReturnStackAddressChecker;
#[derive(Debug, Clone)]
pub struct ReturnPointerRangeChecker;
#[derive(Debug, Clone)]
pub struct AlphaSecurityArrayBoundChecker;
#[derive(Debug, Clone)]
pub struct AlphaSecurityMallocChecker;
#[derive(Debug, Clone)]
pub struct AlphaSecurityTaintChecker;
#[derive(Debug, Clone)]
pub struct AlphaSecurityRetValChecker;
#[derive(Debug, Clone)]
pub struct AlphaUnixStreamChecker;
#[derive(Debug, Clone)]
pub struct AlphaUnixChrootChecker;
#[derive(Debug, Clone)]
pub struct AlphaUnixPthreadChecker;
#[derive(Debug, Clone)]
pub struct AlphaUnixCstringChecker;
#[derive(Debug, Clone)]
pub struct AlphaUnixSimpleStreamChecker;
#[derive(Debug, Clone)]
pub struct AlphaCPlusPlusDeleteChecker;
#[derive(Debug, Clone)]
pub struct AlphaCPlusPlusIteratorChecker;
#[derive(Debug, Clone)]
pub struct AlphaCPlusPlusSmartPtrChecker;
#[derive(Debug, Clone)]
pub struct AlphaCPlusPlusMoveChecker;
#[derive(Debug, Clone)]
pub struct AlphaCPlusPlusEnumCastChecker;
#[derive(Debug, Clone)]
pub struct OptinPerformanceFasterStringFind;
#[derive(Debug, Clone)]
pub struct OptinPerformancePaddingChecker;
#[derive(Debug, Clone)]
pub struct OptinMPIChecker;
#[derive(Debug, Clone)]
pub struct OptinOpenMPChecker;
#[derive(Debug, Clone)]
pub struct OptinOSXAPI;
#[derive(Debug, Clone)]
pub struct OptinOSXCocoa;
#[derive(Debug, Clone)]
pub struct OptinFuchsiaChecker;
#[derive(Debug, Clone)]
pub struct OptinWebKitChecker;
#[derive(Debug, Clone)]
pub struct OptinAndroidChecker;
#[derive(Debug, Clone)]
pub struct OptinPortabilityEndianChecker;
impl Checker for BoolAssignmentChecker {
fn name(&self) -> &str {
"alpha.core.BoolAssignment"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("bool_assign_warning").map(|msg| {
vec![BugReport::new(
0,
"BoolAssignment",
msg,
BugSeverity::Warning,
state.location,
BugCategory::LogicError,
)
.with_checker(self.name())]
})
}
}
impl Checker for DivZeroChecker {
fn name(&self) -> &str {
"alpha.core.DivideZero"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("div_zero").map(|_| {
vec![BugReport::new(
1,
"DivZero",
"Division by zero detected",
BugSeverity::Error,
state.location,
BugCategory::DivisionByZero,
)
.with_checker(self.name())
.with_cwe("CWE-369")]
})
}
}
impl Checker for NullDereferenceChecker {
fn name(&self) -> &str {
"alpha.core.NullDereference"
}
fn category(&self) -> &str {
"alpha.core"
}
fn enabled_by_default(&self) -> bool {
true
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("null_deref").map(|_| {
vec![BugReport::new(
2,
"NullDereference",
"Dereference of null pointer",
BugSeverity::Error,
state.location,
BugCategory::NullDereference,
)
.with_checker(self.name())
.with_cwe("CWE-476")]
})
}
}
impl Checker for UndefResultChecker {
fn name(&self) -> &str {
"alpha.core.UndefinedBinaryOperatorResult"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("undef_binop").map(|msg| {
vec![BugReport::new(
3,
"UndefBinop",
msg,
BugSeverity::Warning,
state.location,
BugCategory::UndefinedBehavior,
)
.with_checker(self.name())]
})
}
}
impl Checker for VLASizeChecker {
fn name(&self) -> &str {
"alpha.core.VLASize"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("vla_negative").map(|_| {
vec![BugReport::new(
4,
"VLASize",
"VLA size must be positive",
BugSeverity::Error,
state.location,
BugCategory::UndefinedBehavior,
)
.with_checker(self.name())]
})
}
}
impl Checker for CallAndMessageChecker {
fn name(&self) -> &str {
"alpha.core.CallAndMessage"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("call_null_fn_ptr").map(|_| {
vec![BugReport::new(
5,
"CallAndMessage",
"Call through null function pointer",
BugSeverity::Error,
state.location,
BugCategory::NullDereference,
)
.with_checker(self.name())]
})
}
}
impl Checker for StackAddrLeakChecker {
fn name(&self) -> &str {
"alpha.core.StackAddressEscape"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("stack_addr_escape").map(|_| {
vec![BugReport::new(
6,
"StackAddrEscape",
"Stack address stored in global/external variable",
BugSeverity::Warning,
state.location,
BugCategory::ReturnStackAddress,
)
.with_checker(self.name())]
})
}
}
impl Checker for DynamicTypeChecker {
fn name(&self) -> &str {
"alpha.core.DynamicTypeChecker"
}
fn category(&self) -> &str {
"alpha.core"
}
fn enabled_by_default(&self) -> bool {
false
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("dynamic_type_mismatch").map(|msg| {
vec![BugReport::new(
7,
"DynamicType",
msg,
BugSeverity::Warning,
state.location,
BugCategory::UndefinedBehavior,
)
.with_checker(self.name())]
})
}
}
impl Checker for IdenticalExprChecker {
fn name(&self) -> &str {
"alpha.core.IdenticalExpr"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("identical_expr").map(|msg| {
vec![BugReport::new(
8,
"IdenticalExpr",
msg,
BugSeverity::Warning,
state.location,
BugCategory::LogicError,
)
.with_checker(self.name())]
})
}
}
impl Checker for SizeofPointerChecker {
fn name(&self) -> &str {
"alpha.core.SizeofPointer"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("sizeof_pointer").map(|msg| {
vec![BugReport::new(
9,
"SizeofPointer",
msg,
BugSeverity::Warning,
state.location,
BugCategory::LogicError,
)
.with_checker(self.name())]
})
}
}
impl Checker for UnsafeBufferUsageChecker {
fn name(&self) -> &str {
"alpha.core.UnsafeBufferUsage"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("unsafe_buffer").map(|_| {
vec![BugReport::new(
10,
"UnsafeBuffer",
"Unsafe buffer usage detected",
BugSeverity::Warning,
state.location,
BugCategory::BufferOverflow,
)
.with_checker(self.name())]
})
}
}
impl Checker for UninitializedObjectChecker {
fn name(&self) -> &str {
"alpha.core.UninitializedObject"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("uninit_object").map(|msg| {
vec![BugReport::new(
11,
"UninitObject",
msg,
BugSeverity::Warning,
state.location,
BugCategory::UninitializedRead,
)
.with_checker(self.name())]
})
}
}
impl Checker for ReturnStackAddressChecker {
fn name(&self) -> &str {
"alpha.core.ReturnStackAddress"
}
fn category(&self) -> &str {
"alpha.core"
}
fn enabled_by_default(&self) -> bool {
true
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("return_stack_addr").map(|_| {
vec![BugReport::new(
12,
"ReturnStackAddr",
"Return of stack memory address",
BugSeverity::Error,
state.location,
BugCategory::ReturnStackAddress,
)
.with_checker(self.name())]
})
}
}
impl Checker for ReturnPointerRangeChecker {
fn name(&self) -> &str {
"alpha.core.ReturnPointerRange"
}
fn category(&self) -> &str {
"alpha.core"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("return_ptr_range").map(|_| {
vec![BugReport::new(
13,
"ReturnPtrRange",
"Returning pointer to local array range",
BugSeverity::Error,
state.location,
BugCategory::ReturnStackAddress,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaSecurityArrayBoundChecker {
fn name(&self) -> &str {
"alpha.security.ArrayBound"
}
fn category(&self) -> &str {
"alpha.security"
}
fn enabled_by_default(&self) -> bool {
true
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("array_bound").map(|_| {
vec![BugReport::new(
14,
"ArrayBound",
"Array index out of bounds",
BugSeverity::Error,
state.location,
BugCategory::BufferOverflow,
)
.with_checker(self.name())
.with_cwe("CWE-129")]
})
}
}
impl Checker for AlphaSecurityMallocChecker {
fn name(&self) -> &str {
"alpha.security.MallocOverflow"
}
fn category(&self) -> &str {
"alpha.security"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("malloc_overflow").map(|_| {
vec![BugReport::new(
15,
"MallocOverflow",
"Potential malloc overflow",
BugSeverity::Error,
state.location,
BugCategory::Security,
)
.with_checker(self.name())
.with_cwe("CWE-190")]
})
}
}
impl Checker for AlphaSecurityTaintChecker {
fn name(&self) -> &str {
"alpha.security.taint.TaintPropagation"
}
fn category(&self) -> &str {
"alpha.security"
}
fn enabled_by_default(&self) -> bool {
false
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("taint_propagation").map(|msg| {
vec![BugReport::new(
16,
"TaintPropagation",
msg,
BugSeverity::Warning,
state.location,
BugCategory::Taint,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaSecurityRetValChecker {
fn name(&self) -> &str {
"alpha.security.ReturnPtrRange"
}
fn category(&self) -> &str {
"alpha.security"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("ret_val_unchecked").map(|_| {
vec![BugReport::new(
17,
"RetValUnchecked",
"Return value not checked",
BugSeverity::Warning,
state.location,
BugCategory::API,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaUnixStreamChecker {
fn name(&self) -> &str {
"alpha.unix.Stream"
}
fn category(&self) -> &str {
"alpha.unix"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("stream_state").map(|msg| {
vec![BugReport::new(
18,
"StreamState",
msg,
BugSeverity::Warning,
state.location,
BugCategory::API,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaUnixChrootChecker {
fn name(&self) -> &str {
"alpha.unix.Chroot"
}
fn category(&self) -> &str {
"alpha.unix"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("chroot_jailbreak").map(|msg| {
vec![BugReport::new(
19,
"ChrootJailbreak",
msg,
BugSeverity::Warning,
state.location,
BugCategory::Security,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaUnixPthreadChecker {
fn name(&self) -> &str {
"alpha.unix.PthreadLock"
}
fn category(&self) -> &str {
"alpha.unix"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("pthread_lock").map(|msg| {
vec![BugReport::new(
20,
"PthreadLock",
msg,
BugSeverity::Warning,
state.location,
BugCategory::Concurrency,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaUnixCstringChecker {
fn name(&self) -> &str {
"alpha.unix.cstring.BufferOverlap"
}
fn category(&self) -> &str {
"alpha.unix"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("cstring_overlap").map(|_| {
vec![BugReport::new(
21,
"CStringOverlap",
"Buffer arguments may overlap",
BugSeverity::Warning,
state.location,
BugCategory::BufferOverflow,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaUnixSimpleStreamChecker {
fn name(&self) -> &str {
"alpha.unix.SimpleStream"
}
fn category(&self) -> &str {
"alpha.unix"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("simple_stream").map(|msg| {
vec![BugReport::new(
22,
"SimpleStream",
msg,
BugSeverity::Warning,
state.location,
BugCategory::API,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaCPlusPlusDeleteChecker {
fn name(&self) -> &str {
"alpha.cplusplus.DeleteWithNonVirtualDtor"
}
fn category(&self) -> &str {
"alpha.cplusplus"
}
fn enabled_by_default(&self) -> bool {
true
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("delete_non_virtual").map(|msg| {
vec![BugReport::new(
23,
"DeleteNonVirtual",
msg,
BugSeverity::Warning,
state.location,
BugCategory::UndefinedBehavior,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaCPlusPlusIteratorChecker {
fn name(&self) -> &str {
"alpha.cplusplus.IteratorRange"
}
fn category(&self) -> &str {
"alpha.cplusplus"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("iterator_range").map(|msg| {
vec![BugReport::new(
24,
"IteratorRange",
msg,
BugSeverity::Warning,
state.location,
BugCategory::UndefinedBehavior,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaCPlusPlusSmartPtrChecker {
fn name(&self) -> &str {
"alpha.cplusplus.SmartPtr"
}
fn category(&self) -> &str {
"alpha.cplusplus"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("smart_ptr_misuse").map(|msg| {
vec![BugReport::new(
25,
"SmartPtrMisuse",
msg,
BugSeverity::Warning,
state.location,
BugCategory::API,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaCPlusPlusMoveChecker {
fn name(&self) -> &str {
"alpha.cplusplus.Move"
}
fn category(&self) -> &str {
"alpha.cplusplus"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("move_use_after").map(|msg| {
vec![BugReport::new(
26,
"MoveUseAfter",
msg,
BugSeverity::Warning,
state.location,
BugCategory::UseAfterFree,
)
.with_checker(self.name())]
})
}
}
impl Checker for AlphaCPlusPlusEnumCastChecker {
fn name(&self) -> &str {
"alpha.cplusplus.EnumCastOutOfRange"
}
fn category(&self) -> &str {
"alpha.cplusplus"
}
fn enabled_by_default(&self) -> bool {
false
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("enum_cast_oob").map(|msg| {
vec![BugReport::new(
27,
"EnumCastOOB",
msg,
BugSeverity::Warning,
state.location,
BugCategory::UndefinedBehavior,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinPerformanceFasterStringFind {
fn name(&self) -> &str {
"optin.performance.FasterStringFind"
}
fn category(&self) -> &str {
"optin.performance"
}
fn enabled_by_default(&self) -> bool {
false
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("faster_string_find").map(|msg| {
vec![BugReport::new(
28,
"FasterStringFind",
msg,
BugSeverity::Note,
state.location,
BugCategory::Custom,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinPerformancePaddingChecker {
fn name(&self) -> &str {
"optin.performance.Padding"
}
fn category(&self) -> &str {
"optin.performance"
}
fn enabled_by_default(&self) -> bool {
false
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("excessive_padding").map(|msg| {
vec![BugReport::new(
29,
"ExcessivePadding",
msg,
BugSeverity::Note,
state.location,
BugCategory::Custom,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinMPIChecker {
fn name(&self) -> &str {
"optin.mpi.MPI-Checker"
}
fn category(&self) -> &str {
"optin.mpi"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("mpi_mismatch").map(|msg| {
vec![BugReport::new(
30,
"MPIMismatch",
msg,
BugSeverity::Warning,
state.location,
BugCategory::API,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinOpenMPChecker {
fn name(&self) -> &str {
"optin.openmp.OpenMP-Checker"
}
fn category(&self) -> &str {
"optin.openmp"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("openmp_violation").map(|msg| {
vec![BugReport::new(
31,
"OpenMPViolation",
msg,
BugSeverity::Warning,
state.location,
BugCategory::Concurrency,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinOSXAPI {
fn name(&self) -> &str {
"optin.osx.API"
}
fn category(&self) -> &str {
"optin.osx"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("osx_api_misuse").map(|msg| {
vec![BugReport::new(
32,
"OSXAPIMisuse",
msg,
BugSeverity::Warning,
state.location,
BugCategory::API,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinOSXCocoa {
fn name(&self) -> &str {
"optin.osx.cocoa.AtSync"
}
fn category(&self) -> &str {
"optin.osx"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("cocoa_atsync").map(|msg| {
vec![BugReport::new(
33,
"CocoaAtSync",
msg,
BugSeverity::Warning,
state.location,
BugCategory::Concurrency,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinFuchsiaChecker {
fn name(&self) -> &str {
"optin.fuchsia.Fuchsia-RestrictSystemIncludes"
}
fn category(&self) -> &str {
"optin.fuchsia"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("fuchsia_system_include").map(|msg| {
vec![BugReport::new(
34,
"FuchsiaSystemInclude",
msg,
BugSeverity::Warning,
state.location,
BugCategory::Custom,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinWebKitChecker {
fn name(&self) -> &str {
"optin.webkit.WebKit"
}
fn category(&self) -> &str {
"optin.webkit"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("webkit_ref_counting").map(|msg| {
vec![BugReport::new(
35,
"WebKitRefCounting",
msg,
BugSeverity::Warning,
state.location,
BugCategory::API,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinAndroidChecker {
fn name(&self) -> &str {
"optin.android.AndroidChecker"
}
fn category(&self) -> &str {
"optin.android"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("android_binder").map(|msg| {
vec![BugReport::new(
36,
"AndroidBinder",
msg,
BugSeverity::Warning,
state.location,
BugCategory::API,
)
.with_checker(self.name())]
})
}
}
impl Checker for OptinPortabilityEndianChecker {
fn name(&self) -> &str {
"optin.portability.Endian"
}
fn category(&self) -> &str {
"optin.portability"
}
fn check(&self, state: &ProgramState, _graph: &ExplodedGraph) -> Option<Vec<BugReport>> {
state.tags.get("endian_dependent").map(|msg| {
vec![BugReport::new(
37,
"EndianDependent",
msg,
BugSeverity::Warning,
state.location,
BugCategory::Custom,
)
.with_checker(self.name())]
})
}
}
#[derive(Debug, Clone)]
pub struct AnalysisConsumer {
pub reports: Vec<BugReport>,
pub file_path: String,
pub statistics: AnalysisStatistics,
pub output_format: AnalysisOutputFormat,
pub plist_config: Option<PlistConfig>,
pub sarif_config: Option<SarifConfig>,
}
#[derive(Debug, Clone)]
pub struct AnalysisStatistics {
pub total_paths: usize,
pub paths_explored: usize,
pub bugs_found: usize,
pub false_positives: usize,
pub timeouts: usize,
pub checker_stats: HashMap<String, CheckerStats>,
pub graph_stats: Option<GraphStats>,
}
#[derive(Debug, Clone, Default)]
pub struct CheckerStats {
pub checks_run: usize,
pub bugs_found: usize,
pub false_positives: usize,
pub time_us: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnalysisOutputFormat {
Text,
Plist,
Sarif,
Json,
CodeClimate,
}
impl Default for AnalysisOutputFormat {
fn default() -> Self {
Self::Text
}
}
#[derive(Debug, Clone, Default)]
pub struct PlistConfig {
pub output_path: String,
pub include_ast_context: bool,
pub include_source_code: bool,
pub compression: bool,
}
#[derive(Debug, Clone, Default)]
pub struct SarifConfig {
pub output_path: String,
pub sarif_version: String,
pub tool_name: String,
pub tool_version: String,
pub include_artifacts: bool,
pub include_logical_locations: bool,
pub taxonomies: Vec<String>,
}
impl AnalysisConsumer {
pub fn new(file_path: &str) -> Self {
Self {
reports: Vec::new(),
file_path: file_path.to_string(),
statistics: AnalysisStatistics::default(),
output_format: AnalysisOutputFormat::default(),
plist_config: None,
sarif_config: None,
}
}
pub fn with_format(mut self, format: AnalysisOutputFormat) -> Self {
self.output_format = format;
self
}
pub fn with_plist(mut self, config: PlistConfig) -> Self {
self.plist_config = Some(config);
self.output_format = AnalysisOutputFormat::Plist;
self
}
pub fn with_sarif(mut self, config: SarifConfig) -> Self {
self.sarif_config = Some(config);
self.output_format = AnalysisOutputFormat::Sarif;
self
}
pub fn add_report(&mut self, report: BugReport) {
self.reports.push(report);
}
pub fn add_reports(&mut self, reports: Vec<BugReport>) {
self.reports.extend(reports);
}
pub fn get_reports_by_category(&self, category: BugCategory) -> Vec<&BugReport> {
self.reports
.iter()
.filter(|r| r.category == category)
.collect()
}
pub fn get_reports_by_checker(&self, checker: &str) -> Vec<&BugReport> {
self.reports
.iter()
.filter(|r| r.checker_name == checker)
.collect()
}
pub fn get_reports_by_severity(&self, severity: BugSeverity) -> Vec<&BugReport> {
self.reports
.iter()
.filter(|r| r.severity == severity)
.collect()
}
pub fn error_count(&self) -> usize {
self.reports
.iter()
.filter(|r| r.severity == BugSeverity::Error || r.severity == BugSeverity::Fatal)
.count()
}
pub fn warning_count(&self) -> usize {
self.reports
.iter()
.filter(|r| r.severity == BugSeverity::Warning)
.count()
}
pub fn generate_text_report(&self) -> String {
let mut out = String::new();
out.push_str(&format!("=== Analysis Report: {} ===\n", self.file_path));
out.push_str(&format!("Total bugs found: {}\n", self.reports.len()));
out.push_str(&format!(
"Errors: {}, Warnings: {}\n\n",
self.error_count(),
self.warning_count()
));
for report in &self.reports {
out.push_str(&format!(
" {} [{}] {}: {}\n",
report.id, report.severity, report.checker_name, report.message
));
}
out
}
pub fn generate_plist(&self) -> String {
let config = self.plist_config.as_ref();
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n");
xml.push_str("<plist version=\"1.0\">\n<dict>\n");
xml.push_str(&format!(
" <key>files</key>\n <array>\n <string>{}</string>\n </array>\n",
self.file_path
));
xml.push_str(" <key>diagnostics</key>\n <array>\n");
for report in &self.reports {
xml.push_str(" <dict>\n");
xml.push_str(&format!(
" <key>check_name</key><string>{}</string>\n",
report.checker_name
));
xml.push_str(&format!(
" <key>description</key><string>{}</string>\n",
report.message
));
xml.push_str(&format!(
" <key>category</key><string>{}</string>\n",
report.category
));
if let Some(ref loc) = report.source_location {
xml.push_str(" <key>location</key><dict>\n");
xml.push_str(&format!(" <key>file</key><integer>0</integer>\n"));
xml.push_str(&format!(
" <key>line</key><integer>{}</integer>\n",
loc.line
));
xml.push_str(&format!(
" <key>col</key><integer>{}</integer>\n",
loc.column
));
xml.push_str(" </dict>\n");
}
if let Some(ref fixit) = report.fixit_hint {
xml.push_str(&format!(" <key>fixits</key><array><dict><key>replacement</key><string>{}</string></dict></array>\n", fixit));
}
xml.push_str(" </dict>\n");
}
xml.push_str(" </array>\n</dict>\n</plist>\n");
xml
}
pub fn generate_sarif(&self) -> String {
let config = self.sarif_config.as_ref();
let version = config.map(|c| c.sarif_version.as_str()).unwrap_or("2.1.0");
let tool_name = config
.map(|c| c.tool_name.as_str())
.unwrap_or("X86AnalysisDeep");
let tool_ver = config.map(|c| c.tool_version.as_str()).unwrap_or("1.0.0");
let mut json = String::new();
json.push_str("{\n");
json.push_str(&format!(" \"$schema\": \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-{}.json\",\n", version));
json.push_str(&format!(" \"version\": \"{}\",\n", version));
json.push_str(" \"runs\": [\n");
json.push_str(" {\n");
json.push_str(" \"tool\": {\n");
json.push_str(" \"driver\": {\n");
json.push_str(&format!(" \"name\": \"{}\",\n", tool_name));
json.push_str(&format!(" \"version\": \"{}\"\n", tool_ver));
json.push_str(" }\n");
json.push_str(" },\n");
json.push_str(" \"results\": [\n");
for (i, report) in self.reports.iter().enumerate() {
if i > 0 {
json.push_str(",\n");
}
json.push_str(" {\n");
json.push_str(&format!(
" \"ruleId\": \"{}\",\n",
report.checker_name
));
json.push_str(" \"level\": \"");
json.push_str(match report.severity {
BugSeverity::Error | BugSeverity::Fatal => "error",
BugSeverity::Warning => "warning",
BugSeverity::Note => "note",
});
json.push_str("\",\n");
json.push_str(" \"message\": {\n");
json.push_str(&format!(" \"text\": \"{}\"\n", report.message));
json.push_str(" },\n");
if let Some(ref loc) = report.source_location {
json.push_str(" \"locations\": [\n");
json.push_str(" {\n");
json.push_str(" \"physicalLocation\": {\n");
json.push_str(" \"artifactLocation\": {\n");
json.push_str(&format!(" \"uri\": \"{}\"\n", loc.file));
json.push_str(" },\n");
json.push_str(" \"region\": {\n");
json.push_str(&format!(" \"startLine\": {},\n", loc.line));
json.push_str(&format!(
" \"startColumn\": {}\n",
loc.column
));
json.push_str(" }\n");
json.push_str(" }\n");
json.push_str(" }\n");
json.push_str(" ],\n");
}
json.push_str(&format!(" \"rank\": {}\n", report.id));
json.push_str(" }");
}
json.push_str("\n ]\n");
json.push_str(" }\n");
json.push_str(" ]\n");
json.push_str("}\n");
json
}
pub fn generate_json_report(&self) -> String {
let mut out = String::new();
out.push_str("{\n");
out.push_str(&format!(" \"file\": \"{}\",\n", self.file_path));
out.push_str(&format!(" \"total_bugs\": {},\n", self.reports.len()));
out.push_str(&format!(" \"errors\": {},\n", self.error_count()));
out.push_str(&format!(" \"warnings\": {},\n", self.warning_count()));
out.push_str(" \"reports\": [\n");
for (i, report) in self.reports.iter().enumerate() {
if i > 0 {
out.push_str(",\n");
}
out.push_str(" {\n");
out.push_str(&format!(" \"id\": {},\n", report.id));
out.push_str(&format!(
" \"checker\": \"{}\",\n",
report.checker_name
));
out.push_str(&format!(" \"category\": \"{}\",\n", report.category));
out.push_str(&format!(" \"message\": \"{}\",\n", report.message));
out.push_str(&format!(" \"severity\": \"{:?}\"\n", report.severity));
out.push_str(" }");
}
out.push_str("\n ]\n}\n");
out
}
pub fn generate_output(&self) -> String {
match self.output_format {
AnalysisOutputFormat::Text => self.generate_text_report(),
AnalysisOutputFormat::Plist => self.generate_plist(),
AnalysisOutputFormat::Sarif => self.generate_sarif(),
AnalysisOutputFormat::Json => self.generate_json_report(),
AnalysisOutputFormat::CodeClimate => self.generate_json_report(), }
}
pub fn clear(&mut self) {
self.reports.clear();
self.statistics = AnalysisStatistics::default();
}
}
impl Default for AnalysisStatistics {
fn default() -> Self {
Self {
total_paths: 0,
paths_explored: 0,
bugs_found: 0,
false_positives: 0,
timeouts: 0,
checker_stats: HashMap::new(),
graph_stats: None,
}
}
}
#[derive(Debug)]
pub struct X86AnalysisDeep {
pub engine: SymbolicExecutionEngine,
pub consumer: AnalysisConsumer,
pub solver: ConstraintSolver,
pub checker_names: Vec<String>,
pub checker_category_map: HashMap<String, String>,
pub enabled_checkers: HashSet<String>,
pub total_files_analyzed: usize,
pub total_lines_analyzed: usize,
pub is_initialized: bool,
}
impl X86AnalysisDeep {
pub fn new(file_path: &str) -> Self {
let config = AnalysisConfig::default();
let mut engine = SymbolicExecutionEngine::new(config);
engine.register_checker(Box::new(BoolAssignmentChecker));
engine.register_checker(Box::new(DivZeroChecker));
engine.register_checker(Box::new(NullDereferenceChecker));
engine.register_checker(Box::new(UndefResultChecker));
engine.register_checker(Box::new(VLASizeChecker));
engine.register_checker(Box::new(CallAndMessageChecker));
engine.register_checker(Box::new(StackAddrLeakChecker));
engine.register_checker(Box::new(DynamicTypeChecker));
engine.register_checker(Box::new(IdenticalExprChecker));
engine.register_checker(Box::new(SizeofPointerChecker));
engine.register_checker(Box::new(UnsafeBufferUsageChecker));
engine.register_checker(Box::new(UninitializedObjectChecker));
engine.register_checker(Box::new(ReturnStackAddressChecker));
engine.register_checker(Box::new(ReturnPointerRangeChecker));
engine.register_checker(Box::new(AlphaSecurityArrayBoundChecker));
engine.register_checker(Box::new(AlphaSecurityMallocChecker));
engine.register_checker(Box::new(AlphaSecurityTaintChecker));
engine.register_checker(Box::new(AlphaSecurityRetValChecker));
engine.register_checker(Box::new(AlphaUnixStreamChecker));
engine.register_checker(Box::new(AlphaUnixChrootChecker));
engine.register_checker(Box::new(AlphaUnixPthreadChecker));
engine.register_checker(Box::new(AlphaUnixCstringChecker));
engine.register_checker(Box::new(AlphaUnixSimpleStreamChecker));
engine.register_checker(Box::new(AlphaCPlusPlusDeleteChecker));
engine.register_checker(Box::new(AlphaCPlusPlusIteratorChecker));
engine.register_checker(Box::new(AlphaCPlusPlusSmartPtrChecker));
engine.register_checker(Box::new(AlphaCPlusPlusMoveChecker));
engine.register_checker(Box::new(AlphaCPlusPlusEnumCastChecker));
engine.register_checker(Box::new(OptinPerformanceFasterStringFind));
engine.register_checker(Box::new(OptinPerformancePaddingChecker));
engine.register_checker(Box::new(OptinMPIChecker));
engine.register_checker(Box::new(OptinOpenMPChecker));
engine.register_checker(Box::new(OptinOSXAPI));
engine.register_checker(Box::new(OptinOSXCocoa));
engine.register_checker(Box::new(OptinFuchsiaChecker));
engine.register_checker(Box::new(OptinWebKitChecker));
engine.register_checker(Box::new(OptinAndroidChecker));
engine.register_checker(Box::new(OptinPortabilityEndianChecker));
let mut checker_names: Vec<String> = engine
.checkers
.iter()
.map(|c| c.name().to_string())
.collect();
checker_names.sort();
let mut checker_category_map = HashMap::new();
for checker in &engine.checkers {
checker_category_map.insert(checker.name().to_string(), checker.category().to_string());
}
let enabled_checkers: HashSet<String> = engine
.checkers
.iter()
.filter(|c| c.enabled_by_default())
.map(|c| c.name().to_string())
.collect();
Self {
engine,
consumer: AnalysisConsumer::new(file_path),
solver: ConstraintSolver::new(),
checker_names,
checker_category_map,
enabled_checkers,
total_files_analyzed: 0,
total_lines_analyzed: 0,
is_initialized: true,
}
}
pub fn enable_checker(&mut self, name: &str) {
self.enabled_checkers.insert(name.to_string());
}
pub fn disable_checker(&mut self, name: &str) {
self.enabled_checkers.remove(name);
}
pub fn enable_category(&mut self, category: &str) {
for name in &self.checker_names {
if let Some(cat) = self.checker_category_map.get(name) {
if cat == category {
self.enabled_checkers.insert(name.clone());
}
}
}
}
pub fn disable_category(&mut self, category: &str) {
for name in &self.checker_names {
if let Some(cat) = self.checker_category_map.get(name) {
if cat == category {
self.enabled_checkers.remove(name);
}
}
}
}
pub fn is_checker_enabled(&self, name: &str) -> bool {
self.enabled_checkers.contains(name)
}
pub fn enabled_checker_count(&self) -> usize {
self.enabled_checkers.len()
}
pub fn get_checker_names(&self) -> Vec<String> {
self.checker_names.clone()
}
pub fn get_checkers_by_category(&self, category: &str) -> Vec<String> {
self.checker_names
.iter()
.filter(|n| {
self.checker_category_map
.get(*n)
.map(|c| c == category)
.unwrap_or(false)
})
.cloned()
.collect()
}
pub fn analyze(&mut self, cfg: &AnalysisCFG) -> Vec<BugReport> {
let reports = self.engine.run(cfg);
self.consumer.add_reports(reports.clone());
self.total_files_analyzed += 1;
self.total_lines_analyzed += cfg.block_count();
self.consumer.statistics.bugs_found = reports.len();
reports
}
pub fn analyze_with_output(
&mut self,
cfg: &AnalysisCFG,
format: AnalysisOutputFormat,
) -> String {
self.consumer.output_format = format;
self.analyze(cfg);
self.consumer.generate_output()
}
pub fn report(&self) -> String {
self.consumer.generate_output()
}
pub fn plist_report(&self) -> String {
let config = self.consumer.plist_config.clone().unwrap_or_default();
let consumer = AnalysisConsumer {
reports: self.consumer.reports.clone(),
file_path: self.consumer.file_path.clone(),
statistics: self.consumer.statistics.clone(),
output_format: AnalysisOutputFormat::Plist,
plist_config: Some(config),
sarif_config: None,
};
consumer.generate_plist()
}
pub fn sarif_report(&self) -> String {
let config = self.consumer.sarif_config.clone().unwrap_or(SarifConfig {
sarif_version: "2.1.0".to_string(),
tool_name: "X86AnalysisDeep".to_string(),
tool_version: "1.0.0".to_string(),
include_artifacts: true,
include_logical_locations: true,
output_path: String::new(),
taxonomies: vec!["CWE".to_string()],
});
let consumer = AnalysisConsumer {
reports: self.consumer.reports.clone(),
file_path: self.consumer.file_path.clone(),
statistics: self.consumer.statistics.clone(),
output_format: AnalysisOutputFormat::Sarif,
plist_config: None,
sarif_config: Some(config),
};
consumer.generate_sarif()
}
pub fn statistics(&self) -> &AnalysisStatistics {
&self.consumer.statistics
}
pub fn reset(&mut self) {
self.engine.reset();
self.consumer.clear();
self.solver = ConstraintSolver::new();
self.total_files_analyzed = 0;
self.total_lines_analyzed = 0;
}
pub fn dump_state(&self) -> String {
let mut out = String::new();
out.push_str(&format!("=== X86AnalysisDeep State ===\n"));
out.push_str(&format!(
"Checkers: {} total, {} enabled\n",
self.checker_names.len(),
self.enabled_checker_count()
));
out.push_str(&format!("Files analyzed: {}\n", self.total_files_analyzed));
out.push_str(&format!("Lines analyzed: {}\n", self.total_lines_analyzed));
out.push_str(&format!(
"Explored graph: {} nodes, {} sinks, {} generations\n",
self.engine.graph.node_count(),
self.engine.graph.sink_count,
self.engine.graph.total_generations
));
out.push_str(&format!(
"Bugs found: {}\n",
self.consumer.statistics.bugs_found
));
out.push_str(&format!("Solver: {}\n", self.solver.dump()));
out
}
}
impl Default for X86AnalysisDeep {
fn default() -> Self {
Self::new("unknown.c")
}
}
pub fn make_x86_analysis_deep(file_path: &str) -> X86AnalysisDeep {
X86AnalysisDeep::new(file_path)
}
pub fn run_x86_analysis_deep(cfg: &AnalysisCFG) -> Vec<BugReport> {
let mut analysis = X86AnalysisDeep::new("input.c");
analysis.analyze(cfg)
}
pub fn run_x86_analysis_deep_plist(cfg: &AnalysisCFG, output_path: &str) -> String {
let mut analysis = X86AnalysisDeep::new("input.c");
analysis.consumer = analysis.consumer.with_plist(PlistConfig {
output_path: output_path.to_string(),
include_ast_context: false,
include_source_code: true,
compression: false,
});
analysis.analyze(cfg);
analysis.plist_report()
}
pub fn run_x86_analysis_deep_sarif(cfg: &AnalysisCFG, output_path: &str) -> String {
let mut analysis = X86AnalysisDeep::new("input.c");
analysis.consumer = analysis.consumer.with_sarif(SarifConfig {
output_path: output_path.to_string(),
sarif_version: "2.1.0".to_string(),
tool_name: "X86AnalysisDeep".to_string(),
tool_version: "1.0.0".to_string(),
include_artifacts: true,
include_logical_locations: true,
taxonomies: vec!["CWE".to_string()],
});
analysis.analyze(cfg);
analysis.sarif_report()
}
pub fn x86_analysis_checker_names() -> Vec<String> {
let analysis = X86AnalysisDeep::default();
analysis.get_checker_names()
}
pub fn make_test_cfg() -> AnalysisCFG {
let mut cfg = AnalysisCFG::new("test_fn");
let b0 = cfg.add_block(AnalysisBlock {
id: 0,
stmts: vec![
AnalysisStmt::DeclRef("x".to_string()),
AnalysisStmt::Assign("x".to_string(), SymExpr::ConstantInt(0)),
AnalysisStmt::Branch(SymExpr::ConstantInt(1), 2, 3),
],
successors: vec![2, 3],
predecessors: vec![],
});
let b1 = cfg.add_block(AnalysisBlock {
id: 1,
stmts: vec![AnalysisStmt::Return(Some(SymExpr::ConstantInt(0)))],
successors: vec![],
predecessors: vec![3],
});
let b2 = cfg.add_block(AnalysisBlock {
id: 2,
stmts: vec![
AnalysisStmt::Assign("x".to_string(), SymExpr::ConstantInt(42)),
AnalysisStmt::Return(Some(SymExpr::ConstantInt(42))),
],
successors: vec![],
predecessors: vec![0],
});
let b3 = cfg.add_block(AnalysisBlock {
id: 3,
stmts: vec![AnalysisStmt::Return(Some(SymExpr::ConstantInt(0)))],
successors: vec![],
predecessors: vec![0],
});
cfg
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_state() -> ProgramState {
ProgramState::new(ProgramPoint::block_entry(0), 1)
}
fn make_analysis() -> X86AnalysisDeep {
X86AnalysisDeep::new("test.c")
}
#[test]
fn test_program_point_new() {
let pp = ProgramPoint::new(1, 2, ProgramPointKind::PreStmt);
assert_eq!(pp.block_id, 1);
assert_eq!(pp.stmt_index, 2);
}
#[test]
fn test_program_point_display() {
let pp = ProgramPoint::new(0, 0, ProgramPointKind::BlockEntry);
let s = format!("{}", pp);
assert!(s.contains("B0"));
}
#[test]
fn test_sym_expr_constant_int() {
let c = SymExpr::ConstantInt(42);
assert_eq!(c, SymExpr::ConstantInt(42));
}
#[test]
fn test_sym_expr_eq_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(SymExpr::ConstantInt(1));
set.insert(SymExpr::ConstantInt(1));
assert_eq!(set.len(), 1);
}
#[test]
fn test_range_constraint_single() {
let expr = SymExpr::ConstantInt(5);
let rc = RangeConstraint::single(expr.clone(), 5, true);
assert!(rc.is_single_value());
assert_eq!(rc.lo, 5);
assert_eq!(rc.hi, 5);
}
#[test]
fn test_range_constraint_overlaps() {
let expr = SymExpr::ConstantInt(0);
let r1 = RangeConstraint::new(expr.clone(), 0, 10, true);
let r2 = RangeConstraint::new(expr.clone(), 5, 15, true);
assert!(r1.overlaps(&r2));
let r3 = RangeConstraint::new(expr, 11, 20, true);
assert!(!r1.overlaps(&r3));
}
#[test]
fn test_taint_source_display() {
assert_eq!(format!("{}", TaintSource::UserInput), "user_input");
assert_eq!(format!("{}", TaintSource::Network), "network");
assert_eq!(format!("{}", TaintSource::Untrusted), "untrusted");
}
#[test]
fn test_program_state_bind_lookup() {
let mut state = make_test_state();
let region = SymRegion::VarRegion("x".to_string());
state.bind("x", region.clone());
assert_eq!(state.lookup("x"), Some(®ion));
assert_eq!(state.lookup("y"), None);
}
#[test]
fn test_program_state_store_load() {
let mut state = make_test_state();
let region = SymRegion::VarRegion("x".to_string());
let val = SymExpr::ConstantInt(42);
state.store_value(region.clone(), val.clone());
assert_eq!(state.load_value(®ion), Some(&val));
}
#[test]
fn test_program_state_tags() {
let mut state = make_test_state();
state.add_tag("null_deref", "true");
assert!(state.has_tag("null_deref"));
assert_eq!(state.get_tag("null_deref"), Some("true"));
}
#[test]
fn test_exploded_graph_new() {
let root_state = make_test_state();
let graph = ExplodedGraph::new(root_state, 1000);
assert_eq!(graph.node_count(), 1);
assert_eq!(graph.root, 0);
assert_eq!(graph.sink_count, 0);
}
#[test]
fn test_exploded_graph_add_node() {
let root_state = make_test_state();
let mut graph = ExplodedGraph::new(root_state, 1000);
let s = ProgramState::new(ProgramPoint::block_entry(1), 2);
let id = graph.add_node(s, 0);
assert_eq!(id, 1);
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.nodes[0].successors, vec![1]);
assert_eq!(graph.nodes[1].generation, 1);
}
#[test]
fn test_exploded_graph_add_sink() {
let root_state = make_test_state();
let mut graph = ExplodedGraph::new(root_state, 1000);
let s = ProgramState::new(ProgramPoint::block_exit(0), 2);
let id = graph.add_sink(s, 0);
assert_eq!(graph.sink_count, 1);
assert!(graph.nodes[id].is_sink);
}
#[test]
fn test_exploded_graph_path() {
let root_state = make_test_state();
let mut graph = ExplodedGraph::new(root_state, 1000);
let s1 = ProgramState::new(ProgramPoint::new(1, 0, ProgramPointKind::PreStmt), 2);
let id1 = graph.add_node(s1, 0);
let s2 = ProgramState::new(ProgramPoint::new(2, 0, ProgramPointKind::PreStmt), 3);
let id2 = graph.add_node(s2, id1);
let path = graph.path_to(id2);
assert_eq!(path.len(), 3);
assert_eq!(path[0], 0);
assert_eq!(path[2], id2);
}
#[test]
fn test_exploded_graph_find_sinks() {
let root_state = make_test_state();
let mut graph = ExplodedGraph::new(root_state, 1000);
let s = ProgramState::new(ProgramPoint::block_exit(0), 2);
graph.add_sink(s, 0);
let sinks = graph.find_sinks();
assert_eq!(sinks.len(), 1);
}
#[test]
fn test_exploded_graph_stats() {
let root_state = make_test_state();
let mut graph = ExplodedGraph::new(root_state, 1000);
graph.add_node(ProgramState::new(ProgramPoint::block_entry(1), 2), 0);
let stats = graph.stats();
assert_eq!(stats.total_nodes, 2);
assert!(stats.max_degree > 0);
}
#[test]
fn test_exploded_graph_worklist_bfs() {
let root_state = make_test_state();
let mut graph = ExplodedGraph::new(root_state, 1000).with_strategy(WorklistStrategy::BFS);
graph.push_worklist(0);
let popped = graph.pop_worklist();
assert_eq!(popped, Some(0));
}
#[test]
fn test_exploded_graph_worklist_dfs() {
let root_state = make_test_state();
let mut graph = ExplodedGraph::new(root_state, 1000).with_strategy(WorklistStrategy::DFS);
graph.push_worklist(0);
graph.push_worklist(1);
let popped = graph.pop_worklist();
assert_eq!(popped, Some(1)); }
#[test]
fn test_worklist_strategy_display() {
assert_eq!(format!("{}", WorklistStrategy::BFS), "BFS");
assert_eq!(format!("{}", WorklistStrategy::DFS), "DFS");
}
#[test]
fn test_sym_exec_engine_new() {
let engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
assert_eq!(engine.sym_counter, 0);
assert_eq!(engine.graph.node_count(), 1);
}
#[test]
fn test_sym_exec_fresh_symbol() {
let mut engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
let sym = engine.fresh_symbol("alloc", ConjuredKind::HeapAlloc);
match sym {
SymExpr::Conjured(c) => {
assert_eq!(c.id, 1);
assert_eq!(c.description, "alloc");
}
_ => panic!("Expected Conjured symbol"),
}
}
#[test]
fn test_sym_exec_fresh_region() {
let mut engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
let reg = engine.fresh_region("heap");
match reg {
SymRegion::HeapRegion(id) => assert!(id > 0),
_ => panic!("Expected HeapRegion"),
}
}
#[test]
fn test_sym_exec_eval_concrete() {
let engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
let expr = SymExpr::BinaryOp(
BinOpKind::Add,
Box::new(SymExpr::ConstantInt(3)),
Box::new(SymExpr::ConstantInt(4)),
);
assert_eq!(engine.eval_concrete(&expr), Some(7));
}
#[test]
fn test_sym_exec_eval_concrete_nested() {
let engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
let inner = SymExpr::BinaryOp(
BinOpKind::Mul,
Box::new(SymExpr::ConstantInt(2)),
Box::new(SymExpr::ConstantInt(3)),
);
let outer = SymExpr::BinaryOp(
BinOpKind::Add,
Box::new(inner),
Box::new(SymExpr::ConstantInt(10)),
);
assert_eq!(engine.eval_concrete(&outer), Some(16));
}
#[test]
fn test_sym_exec_eval_concrete_div_by_zero() {
let engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
let expr = SymExpr::BinaryOp(
BinOpKind::Div,
Box::new(SymExpr::ConstantInt(10)),
Box::new(SymExpr::ConstantInt(0)),
);
assert_eq!(engine.eval_concrete(&expr), None);
}
#[test]
fn test_sym_exec_can_merge() {
let engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
let s1 = ProgramState::new(ProgramPoint::block_entry(0), 1);
let s2 = ProgramState::new(ProgramPoint::block_entry(0), 2);
assert!(engine.can_merge(&s1, &s2));
let s3 = ProgramState::new(ProgramPoint::block_entry(1), 3);
assert!(!engine.can_merge(&s1, &s3));
}
#[test]
fn test_sym_exec_merge_states() {
let engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
let mut s1 = ProgramState::new(ProgramPoint::block_entry(0), 1);
let mut s2 = ProgramState::new(ProgramPoint::block_entry(0), 2);
let region = SymRegion::VarRegion("x".to_string());
let val = SymExpr::ConstantInt(42);
s1.store_value(region.clone(), val.clone());
s2.store_value(region.clone(), val.clone());
let merged = engine.merge_states(&s1, &s2);
assert!(merged.is_some());
}
#[test]
fn test_sym_exec_reset() {
let mut engine = SymbolicExecutionEngine::new(AnalysisConfig::default());
engine.sym_counter = 100;
engine.state_counter = 50;
engine.reset();
assert_eq!(engine.sym_counter, 0);
assert_eq!(engine.state_counter, 1);
assert_eq!(engine.graph.node_count(), 1);
}
#[test]
fn test_constraint_solver_range() {
let mut solver = ConstraintSolver::new();
let expr = SymExpr::Symbolic("x".to_string());
solver.add_range(expr.clone(), 0, 10, true);
assert!(solver.is_feasible());
let range = solver.get_range(&expr).unwrap();
assert_eq!(range.lo, 0);
assert_eq!(range.hi, 10);
}
#[test]
fn test_constraint_solver_range_contradiction() {
let mut solver = ConstraintSolver::new();
let expr = SymExpr::Symbolic("x".to_string());
solver.add_range(expr.clone(), 0, 5, true);
solver.add_range(expr, 10, 15, true);
assert!(!solver.is_feasible());
}
#[test]
fn test_constraint_solver_nullability() {
let mut solver = ConstraintSolver::new();
let expr = SymExpr::Symbolic("p".to_string());
solver.add_nullability(expr.clone(), NullabilityConstraint::NonNull(expr.clone()));
assert!(solver.is_non_null(&expr).unwrap());
assert!(!solver.is_null(&expr).unwrap());
}
#[test]
fn test_constraint_solver_nullability_contradiction() {
let mut solver = ConstraintSolver::new();
let expr = SymExpr::Symbolic("p".to_string());
solver.add_nullability(expr.clone(), NullabilityConstraint::NonNull(expr.clone()));
solver.add_nullability(
expr,
NullabilityConstraint::Null(SymExpr::Symbolic("p".to_string())),
);
assert!(!solver.is_feasible());
}
#[test]
fn test_constraint_solver_taint() {
let mut solver = ConstraintSolver::new();
let expr = SymExpr::Symbolic("input".to_string());
solver.add_taint(TaintConstraint {
expr: expr.clone(),
source: TaintSource::UserInput,
propagation: TaintPropagation::Direct,
});
assert!(solver.is_tainted(&expr));
}
#[test]
fn test_constraint_solver_bugons() {
let mut solver = ConstraintSolver::new();
let cond = SymExpr::BinaryOp(
BinOpKind::Eq,
Box::new(SymExpr::Symbolic("x".to_string())),
Box::new(SymExpr::ConstantInt(0)),
);
solver.add_bugon(BugOnConstraint {
condition: cond,
bug_type: "DIV_ZERO".to_string(),
message: "Division by zero".to_string(),
severity: BugSeverity::Error,
});
let bugs = solver.check_bugons();
assert!(!bugs.is_empty());
}
#[test]
fn test_constraint_solver_merge() {
let mut s1 = ConstraintSolver::new();
let s2 = ConstraintSolver::new();
let expr = SymExpr::Symbolic("x".to_string());
s1.add_range(expr.clone(), 0, 10, true);
s1.add_nullability(expr.clone(), NullabilityConstraint::NonNull(expr));
assert!(s1.is_feasible());
}
#[test]
fn test_constraint_solver_default() {
let solver = ConstraintSolver::default();
assert!(solver.is_feasible());
}
#[test]
fn test_bug_report_new() {
let loc = ProgramPoint::block_entry(0);
let report = BugReport::new(
0,
"NULL_DEREF",
"Null dereference",
BugSeverity::Error,
loc,
BugCategory::NullDereference,
);
assert_eq!(report.id, 0);
assert_eq!(report.bug_type, "NULL_DEREF");
assert_eq!(report.severity, BugSeverity::Error);
}
#[test]
fn test_bug_report_with_path() {
let loc1 = ProgramPoint::block_entry(0);
let loc2 = ProgramPoint::block_exit(0);
let report = BugReport::new(
1,
"TEST",
"test",
BugSeverity::Warning,
loc1,
BugCategory::LogicError,
)
.with_path(vec![loc1, loc2]);
assert_eq!(report.path.len(), 2);
}
#[test]
fn test_bug_report_with_checker() {
let loc = ProgramPoint::block_entry(0);
let report = BugReport::new(
2,
"TEST",
"test",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
)
.with_checker("alpha.core.Test");
assert_eq!(report.checker_name, "alpha.core.Test");
}
#[test]
fn test_bug_report_with_cwe() {
let loc = ProgramPoint::block_entry(0);
let report = BugReport::new(
3,
"TEST",
"test",
BugSeverity::Warning,
loc,
BugCategory::Security,
)
.with_cwe("CWE-476");
assert_eq!(report.cwe_id, Some("CWE-476".to_string()));
}
#[test]
fn test_bug_report_with_source() {
let loc = ProgramPoint::block_entry(0);
let src = SourceLocation {
file: "test.c".to_string(),
line: 10,
column: 5,
end_line: None,
end_column: None,
};
let report = BugReport::new(
4,
"TEST",
"test",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
)
.with_source(src);
assert!(report.source_location.is_some());
}
#[test]
fn test_bug_report_display() {
let loc = ProgramPoint::block_entry(0);
let report = BugReport::new(
5,
"TEST",
"test message",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
);
let s = format!("{}", report);
assert!(s.contains("test message"));
}
#[test]
fn test_bug_category_display() {
assert_eq!(
format!("{}", BugCategory::NullDereference),
"null-dereference"
);
assert_eq!(
format!("{}", BugCategory::DivisionByZero),
"division-by-zero"
);
assert_eq!(
format!("{}", BugCategory::ReturnStackAddress),
"return-stack-address"
);
}
#[test]
fn test_bool_assignment_checker() {
let checker = BoolAssignmentChecker;
assert_eq!(checker.name(), "alpha.core.BoolAssignment");
assert_eq!(checker.category(), "alpha.core");
assert!(checker.enabled_by_default());
}
#[test]
fn test_div_zero_checker() {
let checker = DivZeroChecker;
assert_eq!(checker.name(), "alpha.core.DivideZero");
assert!(checker.enabled_by_default());
}
#[test]
fn test_null_deref_checker() {
let checker = NullDereferenceChecker;
assert_eq!(checker.name(), "alpha.core.NullDereference");
assert!(checker.enabled_by_default());
}
#[test]
fn test_all_core_checkers_names_match() {
let checkers: Vec<Box<dyn Checker>> = vec![
Box::new(BoolAssignmentChecker),
Box::new(DivZeroChecker),
Box::new(NullDereferenceChecker),
Box::new(UndefResultChecker),
Box::new(VLASizeChecker),
Box::new(CallAndMessageChecker),
Box::new(StackAddrLeakChecker),
Box::new(DynamicTypeChecker),
Box::new(IdenticalExprChecker),
Box::new(SizeofPointerChecker),
Box::new(UnsafeBufferUsageChecker),
Box::new(UninitializedObjectChecker),
Box::new(ReturnStackAddressChecker),
Box::new(ReturnPointerRangeChecker),
];
assert_eq!(checkers.len(), 14);
for c in &checkers {
assert!(!c.name().is_empty());
assert!(!c.category().is_empty());
}
}
#[test]
fn test_all_alpha_security_checkers() {
let names = vec![
AlphaSecurityArrayBoundChecker.name(),
AlphaSecurityMallocChecker.name(),
AlphaSecurityTaintChecker.name(),
AlphaSecurityRetValChecker.name(),
];
assert!(names.iter().all(|n| n.contains("alpha.security")));
}
#[test]
fn test_all_alpha_unix_checkers() {
let names = vec![
AlphaUnixStreamChecker.name(),
AlphaUnixChrootChecker.name(),
AlphaUnixPthreadChecker.name(),
AlphaUnixCstringChecker.name(),
AlphaUnixSimpleStreamChecker.name(),
];
assert_eq!(names.len(), 5);
assert!(names.iter().all(|n| n.contains("alpha.unix")));
}
#[test]
fn test_all_alpha_cplusplus_checkers() {
let names = vec![
AlphaCPlusPlusDeleteChecker.name(),
AlphaCPlusPlusIteratorChecker.name(),
AlphaCPlusPlusSmartPtrChecker.name(),
AlphaCPlusPlusMoveChecker.name(),
AlphaCPlusPlusEnumCastChecker.name(),
];
assert_eq!(names.len(), 5);
assert!(names.iter().all(|n| n.contains("alpha.cplusplus")));
}
#[test]
fn test_all_optin_checkers() {
let names = vec![
OptinPerformanceFasterStringFind.name(),
OptinPerformancePaddingChecker.name(),
OptinMPIChecker.name(),
OptinOpenMPChecker.name(),
OptinOSXAPI.name(),
OptinOSXCocoa.name(),
OptinFuchsiaChecker.name(),
OptinWebKitChecker.name(),
OptinAndroidChecker.name(),
OptinPortabilityEndianChecker.name(),
];
assert_eq!(names.len(), 10);
assert!(names.iter().all(|n| n.contains("optin")));
}
#[test]
fn test_analysis_consumer_new() {
let consumer = AnalysisConsumer::new("test.c");
assert_eq!(consumer.file_path, "test.c");
assert!(consumer.reports.is_empty());
assert_eq!(consumer.output_format, AnalysisOutputFormat::Text);
}
#[test]
fn test_analysis_consumer_add_report() {
let mut consumer = AnalysisConsumer::new("test.c");
let loc = ProgramPoint::block_entry(0);
let report = BugReport::new(
0,
"TEST",
"test",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
);
consumer.add_report(report);
assert_eq!(consumer.reports.len(), 1);
}
#[test]
fn test_analysis_consumer_get_reports_by_category() {
let mut consumer = AnalysisConsumer::new("test.c");
for i in 0..5 {
let loc = ProgramPoint::block_entry(i);
let cat = if i % 2 == 0 {
BugCategory::NullDereference
} else {
BugCategory::LogicError
};
consumer.add_report(BugReport::new(i, "T", "m", BugSeverity::Warning, loc, cat));
}
assert_eq!(
consumer
.get_reports_by_category(BugCategory::NullDereference)
.len(),
3
);
assert_eq!(
consumer
.get_reports_by_category(BugCategory::LogicError)
.len(),
2
);
}
#[test]
fn test_analysis_consumer_error_warning_count() {
let mut consumer = AnalysisConsumer::new("test.c");
let loc = ProgramPoint::block_entry(0);
consumer.add_report(BugReport::new(
0,
"E",
"e",
BugSeverity::Error,
loc,
BugCategory::NullDereference,
));
consumer.add_report(BugReport::new(
1,
"W",
"w",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
));
consumer.add_report(BugReport::new(
2,
"E2",
"e2",
BugSeverity::Fatal,
loc,
BugCategory::Security,
));
assert_eq!(consumer.error_count(), 2);
assert_eq!(consumer.warning_count(), 1);
}
#[test]
fn test_analysis_consumer_text_report() {
let mut consumer = AnalysisConsumer::new("test.c");
let loc = ProgramPoint::block_entry(0);
consumer.add_report(BugReport::new(
0,
"T",
"test msg",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
));
let report = consumer.generate_text_report();
assert!(report.contains("test.c"));
assert!(report.contains("test msg"));
assert!(report.contains("Total bugs found: 1"));
}
#[test]
fn test_analysis_consumer_plist() {
let mut consumer = AnalysisConsumer::new("test.c");
consumer = consumer.with_plist(PlistConfig::default());
let loc = ProgramPoint::block_entry(0);
let src = SourceLocation {
file: "test.c".to_string(),
line: 10,
column: 5,
end_line: None,
end_column: None,
};
consumer.add_report(
BugReport::new(
0,
"T",
"test",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
)
.with_source(src),
);
let plist = consumer.generate_plist();
assert!(plist.contains("<?xml"));
assert!(plist.contains("plist"));
assert!(plist.contains("test.c"));
}
#[test]
fn test_analysis_consumer_sarif() {
let mut consumer = AnalysisConsumer::new("test.c");
consumer = consumer.with_sarif(SarifConfig {
sarif_version: "2.1.0".to_string(),
tool_name: "TestTool".to_string(),
tool_version: "0.1.0".to_string(),
output_path: String::new(),
include_artifacts: true,
include_logical_locations: true,
taxonomies: Vec::new(),
});
let loc = ProgramPoint::block_entry(0);
consumer.add_report(BugReport::new(
0,
"T",
"test",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
));
let sarif = consumer.generate_sarif();
assert!(sarif.contains("\"$schema\""));
assert!(sarif.contains("TestTool"));
assert!(sarif.contains("\"T\""));
}
#[test]
fn test_analysis_consumer_json() {
let mut consumer = AnalysisConsumer::new("test.c");
consumer.output_format = AnalysisOutputFormat::Json;
let loc = ProgramPoint::block_entry(0);
consumer.add_report(BugReport::new(
0,
"T",
"test",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
));
let json = consumer.generate_json_report();
assert!(json.contains("\"total_bugs\":"));
assert!(json.contains("\"reports\""));
}
#[test]
fn test_analysis_consumer_clear() {
let mut consumer = AnalysisConsumer::new("test.c");
let loc = ProgramPoint::block_entry(0);
consumer.add_report(BugReport::new(
0,
"T",
"test",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
));
consumer.clear();
assert!(consumer.reports.is_empty());
}
#[test]
fn test_x86_analysis_deep_new() {
let analysis = make_analysis();
assert!(analysis.is_initialized);
assert!(!analysis.checker_names.is_empty());
assert_eq!(analysis.total_files_analyzed, 0);
}
#[test]
fn test_x86_analysis_deep_checker_count() {
let analysis = make_analysis();
assert_eq!(analysis.checker_names.len(), 38);
}
#[test]
fn test_x86_analysis_deep_enable_disable_checker() {
let mut analysis = make_analysis();
let checker_name = "alpha.core.NullDereference";
assert!(analysis.is_checker_enabled(checker_name));
analysis.disable_checker(checker_name);
assert!(!analysis.is_checker_enabled(checker_name));
analysis.enable_checker(checker_name);
assert!(analysis.is_checker_enabled(checker_name));
}
#[test]
fn test_x86_analysis_deep_enable_disable_category() {
let mut analysis = make_analysis();
let core_count = analysis.get_checkers_by_category("alpha.core").len();
assert!(core_count > 0);
analysis.disable_category("alpha.core");
for name in analysis.get_checkers_by_category("alpha.core") {
assert!(!analysis.is_checker_enabled(&name));
}
}
#[test]
fn test_x86_analysis_deep_get_checker_names() {
let analysis = make_analysis();
let names = analysis.get_checker_names();
assert!(names.contains(&"alpha.core.DivideZero".to_string()));
assert!(names.contains(&"alpha.security.ArrayBound".to_string()));
}
#[test]
fn test_x86_analysis_deep_get_checkers_by_category() {
let analysis = make_analysis();
let security = analysis.get_checkers_by_category("alpha.security");
assert_eq!(security.len(), 4);
let unix = analysis.get_checkers_by_category("alpha.unix");
assert_eq!(unix.len(), 5);
let cpp = analysis.get_checkers_by_category("alpha.cplusplus");
assert_eq!(cpp.len(), 5);
}
#[test]
fn test_x86_analysis_deep_dump_state() {
let analysis = make_analysis();
let dump = analysis.dump_state();
assert!(dump.contains("X86AnalysisDeep State"));
assert!(dump.contains("Checkers:"));
}
#[test]
fn test_x86_analysis_deep_reset() {
let mut analysis = make_analysis();
let cfg = make_test_cfg();
analysis.analyze(&cfg);
assert!(analysis.total_files_analyzed > 0);
analysis.reset();
assert_eq!(analysis.total_files_analyzed, 0);
assert_eq!(analysis.total_lines_analyzed, 0);
}
#[test]
fn test_x86_analysis_deep_analyze() {
let mut analysis = make_analysis();
let cfg = make_test_cfg();
let reports = analysis.analyze(&cfg);
assert_eq!(analysis.total_files_analyzed, 1);
}
#[test]
fn test_x86_analysis_deep_default() {
let analysis = X86AnalysisDeep::default();
assert!(analysis.is_initialized);
}
#[test]
fn test_make_x86_analysis_deep() {
let analysis = make_x86_analysis_deep("myfile.c");
assert_eq!(analysis.consumer.file_path, "myfile.c");
}
#[test]
fn test_run_x86_analysis_deep() {
let cfg = make_test_cfg();
let reports = run_x86_analysis_deep(&cfg);
}
#[test]
fn test_x86_analysis_checker_names() {
let names = x86_analysis_checker_names();
assert!(!names.is_empty());
assert_eq!(names.len(), 38);
}
#[test]
fn test_make_test_cfg() {
let cfg = make_test_cfg();
assert_eq!(cfg.entry, 0);
assert!(cfg.block_count() > 1);
}
#[test]
fn test_analysis_cfg_new() {
let cfg = AnalysisCFG::new("my_func");
assert_eq!(cfg.function_name, "my_func");
assert_eq!(cfg.entry, 0);
assert_eq!(cfg.exit, 1);
assert_eq!(cfg.block_count(), 2);
}
#[test]
fn test_analysis_cfg_add_block() {
let mut cfg = AnalysisCFG::new("test");
let id = cfg.add_block(AnalysisBlock {
id: 0,
stmts: vec![AnalysisStmt::NoOp],
successors: vec![],
predecessors: vec![],
});
assert_eq!(id, 2);
assert_eq!(cfg.block_count(), 3);
}
#[test]
fn test_analysis_cfg_add_edge() {
let mut cfg = AnalysisCFG::new("test");
cfg.add_edge(0, 1);
assert_eq!(cfg.blocks[0].successors, vec![1]);
assert_eq!(cfg.blocks[1].predecessors, vec![0]);
}
#[test]
fn test_inline_config_should_inline() {
let mut config = InlineConfig::default();
assert!(config.should_inline("small_fn", 10, 0));
assert!(!config.should_inline("big_fn", 500, 0));
assert!(!config.should_inline("small_fn", 10, 10)); }
#[test]
fn test_inline_config_never_inline() {
let mut config = InlineConfig::default();
config.never_inline.insert("malloc".to_string());
assert!(!config.should_inline("malloc", 5, 0));
}
#[test]
fn test_inline_config_always_inline() {
let mut config = InlineConfig::default();
config.always_inline.insert("getter".to_string());
assert!(config.should_inline("getter", 500, 0));
}
#[test]
fn test_call_site() {
let cs = CallSite::new("foo", ProgramPoint::block_entry(0));
assert_eq!(cs.function_name, "foo");
assert!(cs.return_state.is_none());
}
#[test]
fn stress_many_nodes_in_graph() {
let root_state = make_test_state();
let mut graph = ExplodedGraph::new(root_state, 10000);
for i in 0..100 {
let state =
ProgramState::new(ProgramPoint::new(i, 0, ProgramPointKind::PreStmt), i + 1);
let prev = if i == 0 { 0 } else { i };
graph.add_node(state, prev);
}
assert_eq!(graph.node_count(), 101);
let path = graph.path_to(100);
assert_eq!(path.len(), 101);
}
#[test]
fn stress_many_constraints() {
let mut solver = ConstraintSolver::new();
for i in 0..100 {
let expr = SymExpr::Symbolic(format!("v{}", i));
solver.add_range(expr, i as i64, (i + 10) as i64, true);
}
assert!(solver.is_feasible());
assert_eq!(solver.range_constraints.len(), 100);
}
#[test]
fn stress_many_checkers() {
let mut analysis = make_analysis();
assert_eq!(analysis.checker_names.len(), 38);
let categories: HashSet<&str> = analysis
.checker_category_map
.values()
.map(|s| s.as_str())
.collect();
assert!(categories.contains("alpha.core"));
assert!(categories.contains("alpha.security"));
assert!(categories.contains("alpha.unix"));
assert!(categories.contains("alpha.cplusplus"));
assert!(categories.contains("optin.performance"));
assert!(categories.contains("optin.mpi"));
assert!(categories.contains("optin.openmp"));
assert!(categories.contains("optin.osx"));
assert!(categories.contains("optin.fuchsia"));
assert!(categories.contains("optin.webkit"));
assert!(categories.contains("optin.android"));
assert!(categories.contains("optin.portability"));
}
#[test]
fn stress_plist_sarif_roundtrip() {
let mut analysis = make_analysis();
let src = SourceLocation {
file: "a.c".to_string(),
line: 1,
column: 1,
end_line: None,
end_column: None,
};
let loc = ProgramPoint::block_entry(0);
for i in 0..20 {
let report = BugReport::new(
i,
"TEST",
"msg",
BugSeverity::Warning,
loc,
BugCategory::LogicError,
)
.with_source(src.clone());
analysis.consumer.add_report(report);
}
let plist = analysis.plist_report();
assert!(plist.contains("plist"));
let sarif = analysis.sarif_report();
assert!(sarif.contains("results"));
}
#[test]
fn smoke_all_checkers_have_correct_prefixes() {
let analysis = make_analysis();
for name in &analysis.checker_names {
let valid = name.starts_with("alpha.") || name.starts_with("optin.");
assert!(
valid,
"Checker {} does not start with alpha.* or optin.*",
name
);
}
}
#[test]
fn smoke_graph_stats_display() {
let stats = GraphStats {
total_nodes: 100,
sinks: 5,
max_generation: 10,
max_degree: 3,
processed: 95,
};
let s = format!("{}", stats);
assert!(s.contains("100"));
assert!(s.contains("5"));
}
#[test]
fn smoke_bug_severity_comparison() {
assert!(BugSeverity::Error > BugSeverity::Warning);
assert!(BugSeverity::Fatal > BugSeverity::Error);
assert!(BugSeverity::Warning > BugSeverity::Note);
}
#[test]
fn smoke_program_state_display() {
let state = ProgramState::new(ProgramPoint::block_entry(42), 7);
let s = format!("{}", state);
assert!(s.contains("State#7"));
}
}