use crate::clang::ast::*;
use crate::clang::sema::*;
use crate::x86::X86Subtarget;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::fmt;
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum X86MemRegionKind {
Unknown,
Stack,
Heap,
Global,
StringLiteral,
Code,
Field,
Element,
Pointee,
Alloca,
ThreadLocal,
Block,
}
impl fmt::Display for X86MemRegionKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
X86MemRegionKind::Unknown => write!(f, "Unknown"),
X86MemRegionKind::Stack => write!(f, "Stack"),
X86MemRegionKind::Heap => write!(f, "Heap"),
X86MemRegionKind::Global => write!(f, "Global"),
X86MemRegionKind::StringLiteral => write!(f, "StringLiteral"),
X86MemRegionKind::Code => write!(f, "Code"),
X86MemRegionKind::Field => write!(f, "Field"),
X86MemRegionKind::Element => write!(f, "Element"),
X86MemRegionKind::Pointee => write!(f, "Pointee"),
X86MemRegionKind::Alloca => write!(f, "Alloca"),
X86MemRegionKind::ThreadLocal => write!(f, "ThreadLocal"),
X86MemRegionKind::Block => write!(f, "Block"),
}
}
}
#[derive(Debug, Clone)]
pub struct X86MemRegion {
pub id: u64,
pub kind: X86MemRegionKind,
pub name: String,
pub parent: Option<Box<X86MemRegion>>,
pub field_idx: Option<usize>,
pub field_name: Option<String>,
pub element_idx: Option<i64>,
pub region_type: Option<QualType>,
pub is_read_only: bool,
pub is_thread_local: bool,
pub segment: Option<X86Segment>,
pub alignment: u32,
pub size_bytes: u64,
}
impl PartialEq for X86MemRegion {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for X86MemRegion {}
impl std::hash::Hash for X86MemRegion {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86Segment {
DS,
SS,
ES,
FS,
GS,
}
impl fmt::Display for X86Segment {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
X86Segment::DS => write!(f, "DS"),
X86Segment::SS => write!(f, "SS"),
X86Segment::ES => write!(f, "ES"),
X86Segment::FS => write!(f, "FS"),
X86Segment::GS => write!(f, "GS"),
}
}
}
impl X86MemRegion {
pub fn new_stack(id: u64, name: impl Into<String>, ty: Option<QualType>) -> Self {
Self {
id,
kind: X86MemRegionKind::Stack,
name: name.into(),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: ty,
is_read_only: false,
is_thread_local: false,
segment: Some(X86Segment::SS),
alignment: 8,
size_bytes: 0,
}
}
pub fn new_heap(id: u64, name: impl Into<String>, size: u64) -> Self {
Self {
id,
kind: X86MemRegionKind::Heap,
name: name.into(),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: None,
is_read_only: false,
is_thread_local: false,
segment: None,
alignment: 16,
size_bytes: size,
}
}
pub fn new_global(id: u64, name: impl Into<String>, ty: Option<QualType>) -> Self {
Self {
id,
kind: X86MemRegionKind::Global,
name: name.into(),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: ty,
is_read_only: false,
is_thread_local: false,
segment: None,
alignment: 16,
size_bytes: 0,
}
}
pub fn new_field(
parent: X86MemRegion,
field_idx: usize,
field_name: impl Into<String>,
ty: Option<QualType>,
) -> Self {
let id = parent.id.wrapping_mul(31).wrapping_add(field_idx as u64);
Self {
id,
kind: X86MemRegionKind::Field,
name: field_name.into(),
parent: Some(Box::new(parent)),
field_idx: Some(field_idx),
field_name: Some(String::new()),
element_idx: None,
region_type: ty,
is_read_only: false,
is_thread_local: false,
segment: None,
alignment: 4,
size_bytes: 0,
}
}
pub fn new_element(parent: X86MemRegion, idx: i64, ty: Option<QualType>) -> Self {
let id = parent.id.wrapping_mul(37).wrapping_add(idx as u64);
Self {
id,
kind: X86MemRegionKind::Element,
name: format!("[{}]", idx),
parent: Some(Box::new(parent)),
field_idx: None,
field_name: None,
element_idx: Some(idx),
region_type: ty,
is_read_only: false,
is_thread_local: false,
segment: None,
alignment: 4,
size_bytes: 0,
}
}
pub fn new_pointee(ptr_id: u64, ty: Option<QualType>) -> Self {
Self {
id: ptr_id.wrapping_mul(41),
kind: X86MemRegionKind::Pointee,
name: "pointee".into(),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: ty,
is_read_only: false,
is_thread_local: false,
segment: None,
alignment: 4,
size_bytes: 0,
}
}
pub fn new_unknown(id: u64) -> Self {
Self {
id,
kind: X86MemRegionKind::Unknown,
name: format!("unknown_{}", id),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: None,
is_read_only: false,
is_thread_local: false,
segment: None,
alignment: 1,
size_bytes: 0,
}
}
pub fn new_string_literal(id: u64) -> Self {
Self {
id,
kind: X86MemRegionKind::StringLiteral,
name: ".rodata".into(),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: Some(QualType::const_char_ptr()),
is_read_only: true,
is_thread_local: false,
segment: None,
alignment: 1,
size_bytes: 0,
}
}
pub fn is_stack(&self) -> bool {
self.kind == X86MemRegionKind::Stack
}
pub fn is_heap(&self) -> bool {
self.kind == X86MemRegionKind::Heap
}
pub fn is_global(&self) -> bool {
self.kind == X86MemRegionKind::Global
}
pub fn is_sub_region(&self) -> bool {
matches!(
self.kind,
X86MemRegionKind::Field | X86MemRegionKind::Element
)
}
pub fn root_region(&self) -> &X86MemRegion {
let mut current = self;
while let Some(ref parent) = current.parent {
current = parent;
}
current
}
pub fn may_alias(&self, other: &Self) -> bool {
if self.id == other.id {
return true;
}
let self_root = self.root_region();
let other_root = other.root_region();
if self_root.id == other_root.id {
return true;
}
if self.kind == X86MemRegionKind::Unknown || other.kind == X86MemRegionKind::Unknown {
return true;
}
false
}
pub fn x86_size(&self) -> u64 {
if self.size_bytes > 0 {
return self.size_bytes;
}
self.region_type
.as_ref()
.and_then(|qt| qt.base_type_size())
.unwrap_or(0)
}
}
impl fmt::Display for X86MemRegion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}#{}", self.kind, self.id)?;
if !self.name.is_empty() {
write!(f, "(\"{}\")", self.name)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SVal {
Undefined,
Unknown,
ConcreteInt(i64),
ConcreteUInt(u64),
ConcreteFloat(FloatVal),
Loc(u64),
Null,
SymbolInt(u64),
SymbolRegion(u64),
Zero,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FloatVal {
pub bits: u64,
pub width: u32,
}
impl Eq for FloatVal {}
impl std::hash::Hash for FloatVal {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.bits.hash(state);
self.width.hash(state);
}
}
impl FloatVal {
pub fn f32(bits: u32) -> Self {
Self {
bits: bits as u64,
width: 32,
}
}
pub fn f64(bits: u64) -> Self {
Self { bits, width: 64 }
}
pub fn is_nan(&self) -> bool {
if self.width == 32 {
let f: f32 = f32::from_bits(self.bits as u32);
f.is_nan()
} else {
let f: f64 = f64::from_bits(self.bits);
f.is_nan()
}
}
pub fn is_infinity(&self) -> bool {
if self.width == 32 {
let f: f32 = f32::from_bits(self.bits as u32);
f.is_infinite()
} else {
let f: f64 = f64::from_bits(self.bits);
f.is_infinite()
}
}
pub fn is_zero(&self) -> bool {
if self.width == 32 {
let f: f32 = f32::from_bits(self.bits as u32);
f == 0.0 || f == -0.0
} else {
let f: f64 = f64::from_bits(self.bits);
f == 0.0 || f == -0.0
}
}
}
impl SVal {
pub fn is_undefined(&self) -> bool {
matches!(self, SVal::Undefined)
}
pub fn is_null(&self) -> bool {
matches!(self, SVal::Null)
}
pub fn is_zero(&self) -> bool {
matches!(
self,
SVal::Zero | SVal::ConcreteInt(0) | SVal::ConcreteUInt(0)
)
}
pub fn is_concrete(&self) -> bool {
matches!(self, SVal::ConcreteInt(_) | SVal::ConcreteUInt(_))
}
pub fn is_known(&self) -> bool {
!matches!(self, SVal::Undefined | SVal::Unknown)
}
pub fn as_int(&self) -> Option<i64> {
match self {
SVal::ConcreteInt(v) => Some(*v),
SVal::ConcreteUInt(v) => Some(*v as i64),
_ => None,
}
}
pub fn as_uint(&self) -> Option<u64> {
match self {
SVal::ConcreteInt(v) if *v >= 0 => Some(*v as u64),
SVal::ConcreteUInt(v) => Some(*v),
_ => None,
}
}
}
impl fmt::Display for SVal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SVal::Undefined => write!(f, "undef"),
SVal::Unknown => write!(f, "unknown"),
SVal::ConcreteInt(v) => write!(f, "{}", v),
SVal::ConcreteUInt(v) => write!(f, "{}u", v),
SVal::ConcreteFloat(v) => write!(f, "{:?}", v),
SVal::Loc(id) => write!(f, "®ion_{}", id),
SVal::Null => write!(f, "null"),
SVal::SymbolInt(id) => write!(f, "symint_{}", id),
SVal::SymbolRegion(id) => write!(f, "symreg_{}", id),
SVal::Zero => write!(f, "0"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct X86ProgramState {
pub values: BTreeMap<String, SVal>,
pub region_store: BTreeMap<u64, SVal>,
pub true_constraints: BTreeSet<String>,
pub false_constraints: BTreeSet<String>,
pub allocated: BTreeSet<u64>,
pub freed: BTreeSet<u64>,
pub ref_counts: BTreeMap<u64, i32>,
pub held_locks: BTreeSet<String>,
pub var_regions: BTreeMap<String, X86MemRegion>,
pub tainted: BTreeSet<String>,
pub infeasible: bool,
pub is_dead: bool,
pub depth: u32,
pub stmt_idx: u64,
pub subtarget: Option<Rc<X86Subtarget>>,
}
impl X86ProgramState {
pub fn new() -> Self {
Self {
values: BTreeMap::new(),
region_store: BTreeMap::new(),
true_constraints: BTreeSet::new(),
false_constraints: BTreeSet::new(),
allocated: BTreeSet::new(),
freed: BTreeSet::new(),
ref_counts: BTreeMap::new(),
held_locks: BTreeSet::new(),
var_regions: BTreeMap::new(),
tainted: BTreeSet::new(),
infeasible: false,
is_dead: false,
depth: 0,
stmt_idx: 0,
subtarget: None,
}
}
pub fn with_subtarget(subtarget: X86Subtarget) -> Self {
Self {
subtarget: Some(Rc::new(subtarget)),
..Self::new()
}
}
pub fn bind_value(&self, name: &str, val: SVal) -> Self {
let mut new = self.clone();
new.values.insert(name.to_string(), val);
new
}
pub fn lookup_value(&self, name: &str) -> SVal {
self.values.get(name).cloned().unwrap_or(SVal::Unknown)
}
pub fn store_region(&self, region_id: u64, val: SVal) -> Self {
let mut new = self.clone();
new.region_store.insert(region_id, val);
new
}
pub fn load_region(&self, region_id: u64) -> SVal {
self.region_store
.get(®ion_id)
.cloned()
.unwrap_or(SVal::Unknown)
}
pub fn assume_true(&self, constraint: &str) -> Self {
let mut new = self.clone();
new.true_constraints.insert(constraint.to_string());
new
}
pub fn assume_false(&self, constraint: &str) -> Self {
let mut new = self.clone();
new.false_constraints.insert(constraint.to_string());
new
}
pub fn is_constrained_true(&self, constraint: &str) -> bool {
self.true_constraints.contains(constraint)
}
pub fn is_constrained_false(&self, constraint: &str) -> bool {
self.false_constraints.contains(constraint)
}
pub fn allocate(&self, region_id: u64) -> Self {
let mut new = self.clone();
new.allocated.insert(region_id);
new.freed.remove(®ion_id);
if !new.ref_counts.contains_key(®ion_id) {
new.ref_counts.insert(region_id, 0);
}
new
}
pub fn free(&self, region_id: u64) -> Self {
let mut new = self.clone();
new.allocated.remove(®ion_id);
new.freed.insert(region_id);
new
}
pub fn is_allocated(&self, region_id: u64) -> bool {
self.allocated.contains(®ion_id) && !self.freed.contains(®ion_id)
}
pub fn is_freed(&self, region_id: u64) -> bool {
self.freed.contains(®ion_id)
}
pub fn retain(&self, region_id: u64) -> Self {
let mut new = self.clone();
let count = new.ref_counts.get(®ion_id).copied().unwrap_or(0);
new.ref_counts.insert(region_id, count + 1);
new
}
pub fn release(&self, region_id: u64) -> Self {
let mut new = self.clone();
let count = new.ref_counts.get(®ion_id).copied().unwrap_or(0);
new.ref_counts.insert(region_id, (count - 1).max(-1));
new
}
pub fn retain_count(&self, region_id: u64) -> i32 {
self.ref_counts.get(®ion_id).copied().unwrap_or(0)
}
pub fn acquire_lock(&self, lock_name: &str) -> Self {
let mut new = self.clone();
new.held_locks.insert(lock_name.to_string());
new
}
pub fn release_lock(&self, lock_name: &str) -> Self {
let mut new = self.clone();
new.held_locks.remove(lock_name);
new
}
pub fn lock_held(&self, lock_name: &str) -> bool {
self.held_locks.contains(lock_name)
}
pub fn register_var_region(&self, name: &str, region: X86MemRegion) -> Self {
let mut new = self.clone();
new.var_regions.insert(name.to_string(), region);
new
}
pub fn var_region(&self, name: &str) -> Option<&X86MemRegion> {
self.var_regions.get(name)
}
pub fn taint(&self, name: &str) -> Self {
let mut new = self.clone();
new.tainted.insert(name.to_string());
new
}
pub fn is_tainted(&self, name: &str) -> bool {
self.tainted.contains(name)
}
pub fn mark_infeasible(&self) -> Self {
let mut new = self.clone();
new.infeasible = true;
new
}
pub fn mark_dead(&self) -> Self {
let mut new = self.clone();
new.is_dead = true;
new
}
pub fn advance(&self) -> Self {
let mut new = self.clone();
new.stmt_idx += 1;
new.depth += 1;
new
}
pub fn merge(&self, other: &Self) -> Self {
if self.infeasible {
return other.clone();
}
if other.infeasible {
return self.clone();
}
let mut merged = Self::new();
merged.subtarget = self.subtarget.clone();
for (k, v) in &self.values {
if let Some(other_v) = other.values.get(k) {
if v == other_v {
merged.values.insert(k.clone(), v.clone());
}
}
}
for (k, v) in &self.region_store {
if let Some(other_v) = other.region_store.get(k) {
if v == other_v {
merged.region_store.insert(*k, v.clone());
}
}
}
for c in &self.true_constraints {
if other.true_constraints.contains(c) {
merged.true_constraints.insert(c.clone());
}
}
for c in &self.false_constraints {
if other.false_constraints.contains(c) {
merged.false_constraints.insert(c.clone());
}
}
for r in &self.allocated {
if other.allocated.contains(r) {
merged.allocated.insert(*r);
}
}
for r in &self.freed {
if other.freed.contains(r) {
merged.freed.insert(*r);
}
}
for (k, v) in &self.ref_counts {
if let Some(other_v) = other.ref_counts.get(k) {
if v == other_v {
merged.ref_counts.insert(*k, *v);
}
}
}
for l in &self.held_locks {
if other.held_locks.contains(l) {
merged.held_locks.insert(l.clone());
}
}
for t in &self.tainted {
merged.tainted.insert(t.clone());
}
for t in &other.tainted {
merged.tainted.insert(t.clone());
}
merged.infeasible = false;
merged.is_dead = false;
merged
}
}
impl Default for X86ProgramState {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct X86ExplodedNode {
pub id: u64,
pub stmt_label: String,
pub state: X86ProgramState,
pub processed: bool,
pub exec_count: u32,
}
#[derive(Debug, Clone)]
pub struct X86ExplodedEdge {
pub src: u64,
pub dst: u64,
pub condition: String,
pub kind: X86EdgeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86EdgeKind {
Fallthrough,
TrueBranch,
FalseBranch,
Call,
Return,
Case,
Loop,
Unwind,
}
#[derive(Debug, Clone)]
pub struct X86ExplodedGraph {
pub nodes: BTreeMap<u64, X86ExplodedNode>,
pub edges: Vec<X86ExplodedEdge>,
pub adj: BTreeMap<u64, Vec<usize>>,
pub rev_adj: BTreeMap<u64, Vec<usize>>,
pub root_id: Option<u64>,
pub next_id: u64,
pub worklist: VecDeque<u64>,
pub max_depth: u32,
pub current_function: Option<String>,
}
impl X86ExplodedGraph {
pub fn new() -> Self {
Self {
nodes: BTreeMap::new(),
edges: Vec::new(),
adj: BTreeMap::new(),
rev_adj: BTreeMap::new(),
root_id: None,
next_id: 0,
worklist: VecDeque::new(),
max_depth: 256,
current_function: None,
}
}
pub fn create_root(&mut self, state: X86ProgramState) -> u64 {
let id = self.next_id;
self.next_id += 1;
let node = X86ExplodedNode {
id,
stmt_label: "entry".into(),
state,
processed: false,
exec_count: 0,
};
self.nodes.insert(id, node);
self.root_id = Some(id);
self.worklist.push_back(id);
id
}
pub fn add_node(
&mut self,
src: u64,
label: impl Into<String>,
state: X86ProgramState,
edge_kind: X86EdgeKind,
condition: impl Into<String>,
) -> u64 {
if state.infeasible || state.is_dead {
return 0; }
if state.depth > self.max_depth {
return 0;
}
let id = self.next_id;
self.next_id += 1;
let node = X86ExplodedNode {
id,
stmt_label: label.into(),
state,
processed: false,
exec_count: 0,
};
self.nodes.insert(id, node);
let edge_idx = self.edges.len();
self.edges.push(X86ExplodedEdge {
src,
dst: id,
condition: condition.into(),
kind: edge_kind,
});
self.adj.entry(src).or_default().push(edge_idx);
self.rev_adj.entry(id).or_default().push(edge_idx);
self.worklist.push_back(id);
id
}
pub fn mark_processed(&mut self, node_id: u64) {
if let Some(node) = self.nodes.get_mut(&node_id) {
node.processed = true;
}
}
pub fn pop_worklist(&mut self) -> Option<u64> {
self.worklist.pop_front()
}
pub fn is_worklist_empty(&self) -> bool {
self.worklist.is_empty()
}
pub fn successors(&self, node_id: u64) -> Vec<u64> {
self.adj
.get(&node_id)
.map(|indices| indices.iter().map(|&i| self.edges[i].dst).collect())
.unwrap_or_default()
}
pub fn predecessors(&self, node_id: u64) -> Vec<u64> {
self.rev_adj
.get(&node_id)
.map(|indices| indices.iter().map(|&i| self.edges[i].src).collect())
.unwrap_or_default()
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
}
impl Default for X86ExplodedGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BugSeverity {
Note,
Style,
Warning,
Error,
Critical,
}
impl fmt::Display for BugSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BugSeverity::Note => write!(f, "note"),
BugSeverity::Style => write!(f, "style"),
BugSeverity::Warning => write!(f, "warning"),
BugSeverity::Error => write!(f, "error"),
BugSeverity::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BugCategory {
NullDereference,
UndefinedBehavior,
DivideByZero,
Uninitialized,
ArrayBounds,
StackAddressEscape,
Memory,
VLASize,
InvalidCast,
EnumRange,
NonNullViolation,
FormatString,
ShiftError,
SignedOverflow,
FloatLoopCounter,
Locking,
RetainCount,
UnreachableCode,
Security,
Taint,
General,
}
impl fmt::Display for BugCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BugCategory::NullDereference => write!(f, "core.NullDereference"),
BugCategory::UndefinedBehavior => write!(f, "core.UndefinedBehavior"),
BugCategory::DivideByZero => write!(f, "core.DivideByZero"),
BugCategory::Uninitialized => write!(f, "core.Uninitialized"),
BugCategory::ArrayBounds => write!(f, "core.ArrayBounds"),
BugCategory::StackAddressEscape => write!(f, "core.StackAddressEscape"),
BugCategory::Memory => write!(f, "unix.Memory"),
BugCategory::VLASize => write!(f, "core.VLASize"),
BugCategory::InvalidCast => write!(f, "core.InvalidCast"),
BugCategory::EnumRange => write!(f, "core.EnumRange"),
BugCategory::NonNullViolation => write!(f, "core.NonNullViolation"),
BugCategory::FormatString => write!(f, "security.FormatString"),
BugCategory::ShiftError => write!(f, "core.ShiftError"),
BugCategory::SignedOverflow => write!(f, "core.SignedOverflow"),
BugCategory::FloatLoopCounter => write!(f, "style.FloatLoopCounter"),
BugCategory::Locking => write!(f, "alpha.unix.PthreadLock"),
BugCategory::RetainCount => write!(f, "osx.RetainCount"),
BugCategory::UnreachableCode => write!(f, "deadcode.UnreachableCode"),
BugCategory::Security => write!(f, "security.SecuritySyntax"),
BugCategory::Taint => write!(f, "alpha.security.Taint"),
BugCategory::General => write!(f, "core.General"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FixItHint {
Insertion {
line: u32,
column: u32,
text: String,
},
Removal {
start_line: u32,
start_col: u32,
end_line: u32,
end_col: u32,
},
Replacement {
start_line: u32,
start_col: u32,
end_line: u32,
end_col: u32,
new_text: String,
},
}
#[derive(Debug, Clone)]
pub struct PathDiagnosticPiece {
pub message: String,
pub file: String,
pub line: u32,
pub column: u32,
pub is_bug_point: bool,
pub fixit: Option<FixItHint>,
}
#[derive(Debug, Clone)]
pub struct X86BugReport {
pub id: u64,
pub checker_name: String,
pub category: BugCategory,
pub severity: BugSeverity,
pub title: String,
pub description: String,
pub function: Option<String>,
pub file: String,
pub line: u32,
pub column: u32,
pub path: Vec<PathDiagnosticPiece>,
pub is_duplicate: bool,
}
impl X86BugReport {
pub fn new(
checker_name: impl Into<String>,
category: BugCategory,
severity: BugSeverity,
title: impl Into<String>,
description: impl Into<String>,
file: impl Into<String>,
line: u32,
column: u32,
) -> Self {
Self {
id: 0,
checker_name: checker_name.into(),
category,
severity,
title: title.into(),
description: description.into(),
function: None,
file: file.into(),
line,
column,
path: Vec::new(),
is_duplicate: false,
}
}
pub fn with_function(mut self, func: impl Into<String>) -> Self {
self.function = Some(func.into());
self
}
pub fn add_path_piece(&mut self, piece: PathDiagnosticPiece) {
self.path.push(piece);
}
pub fn add_note(
&mut self,
message: impl Into<String>,
file: impl Into<String>,
line: u32,
column: u32,
) {
self.path.push(PathDiagnosticPiece {
message: message.into(),
file: file.into(),
line,
column,
is_bug_point: false,
fixit: None,
});
}
pub fn add_bug_point(
&mut self,
message: impl Into<String>,
file: impl Into<String>,
line: u32,
column: u32,
) {
self.path.push(PathDiagnosticPiece {
message: message.into(),
file: file.into(),
line,
column,
is_bug_point: true,
fixit: None,
});
}
pub fn add_fixit(&mut self, fixit: FixItHint) {
if let Some(last) = self.path.last_mut() {
last.fixit = Some(fixit);
}
}
}
impl fmt::Display for X86BugReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"{}: {} [{}] in {}:{}:{}",
self.severity, self.title, self.checker_name, self.file, self.line, self.column
)?;
if let Some(ref func) = self.function {
writeln!(f, " in function '{}'", func)?;
}
writeln!(f, " {}", self.description)?;
for piece in &self.path {
let marker = if piece.is_bug_point { ">>>" } else { " - " };
writeln!(
f,
" {} {}:{}:{} — {}",
marker, piece.file, piece.line, piece.column, piece.message
)?;
}
Ok(())
}
}
pub struct X86BugReporter {
pub reports: Vec<X86BugReport>,
next_report_id: u64,
seen_signatures: HashSet<String>,
pub max_reports_per_function: usize,
pub emit_path_diagnostics: bool,
}
impl X86BugReporter {
pub fn new() -> Self {
Self {
reports: Vec::new(),
next_report_id: 0,
seen_signatures: HashSet::new(),
max_reports_per_function: 10,
emit_path_diagnostics: true,
}
}
pub fn emit(&mut self, mut report: X86BugReport) -> bool {
let sig = Self::make_signature(&report);
if self.seen_signatures.contains(&sig) {
report.is_duplicate = true;
return false;
}
self.seen_signatures.insert(sig);
report.id = self.next_report_id;
self.next_report_id += 1;
self.reports.push(report);
true
}
fn make_signature(report: &X86BugReport) -> String {
format!(
"{}|{}|{}|{}|{}",
report.checker_name, report.file, report.line, report.column, report.title
)
}
pub fn emit_simple(
&mut self,
checker_name: &str,
category: BugCategory,
severity: BugSeverity,
title: &str,
description: &str,
file: &str,
line: u32,
column: u32,
) -> bool {
let report = X86BugReport::new(
checker_name,
category,
severity,
title,
description,
file,
line,
column,
);
self.emit(report)
}
pub fn sorted_reports(&self) -> Vec<&X86BugReport> {
let mut refs: Vec<&X86BugReport> = self.reports.iter().collect();
refs.sort_by(|a, b| {
b.severity
.cmp(&a.severity)
.then_with(|| a.file.cmp(&b.file))
.then_with(|| a.line.cmp(&b.line))
});
refs
}
pub fn report_count(&self) -> usize {
self.reports.len()
}
pub fn reports_by_severity(&self, severity: BugSeverity) -> Vec<&X86BugReport> {
self.reports
.iter()
.filter(|r| r.severity == severity && !r.is_duplicate)
.collect()
}
pub fn reports_by_category(&self, category: BugCategory) -> Vec<&X86BugReport> {
self.reports
.iter()
.filter(|r| r.category == category && !r.is_duplicate)
.collect()
}
pub fn summary(&self) -> String {
let mut s = String::new();
s.push_str(&format!("=== X86 Static Analyzer Bug Report Summary ===\n"));
s.push_str(&format!("Total reports: {}\n", self.reports.len()));
s.push_str(&format!(
" Critical: {}\n",
self.reports_by_severity(BugSeverity::Critical).len()
));
s.push_str(&format!(
" Errors: {}\n",
self.reports_by_severity(BugSeverity::Error).len()
));
s.push_str(&format!(
" Warnings: {}\n",
self.reports_by_severity(BugSeverity::Warning).len()
));
s.push_str(&format!(
" Style: {}\n",
self.reports_by_severity(BugSeverity::Style).len()
));
s.push_str(&format!(
" Notes: {}\n",
self.reports_by_severity(BugSeverity::Note).len()
));
s
}
}
impl Default for X86BugReporter {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CheckerBug {
pub category: BugCategory,
pub severity: BugSeverity,
pub title: String,
pub description: String,
pub file: String,
pub line: u32,
pub column: u32,
}
pub trait X86Checker: fmt::Debug {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn category(&self) -> BugCategory;
fn enabled_by_default(&self) -> bool {
true
}
fn check_function_decl(
&self,
_decl: &FunctionDecl,
_state: &mut X86ProgramState,
_reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
vec![]
}
fn check_var_decl(
&self,
_decl: &VarDecl,
_state: &mut X86ProgramState,
_reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
vec![]
}
fn check_stmt(
&self,
_stmt: &Stmt,
_graph: &mut X86ExplodedGraph,
_node_id: u64,
_state: &X86ProgramState,
_reporter: &mut X86BugReporter,
) -> (Vec<X86ProgramState>, Vec<CheckerBug>) {
(vec![], vec![])
}
fn check_expr(
&self,
_expr: &Expr,
_state: &X86ProgramState,
_reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
vec![]
}
fn check_end_of_function(
&self,
_state: &X86ProgramState,
_reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
vec![]
}
fn check_end_of_tu(&self, _reporter: &mut X86BugReporter) -> Vec<CheckerBug> {
vec![]
}
}
#[derive(Debug, Default)]
pub struct X86NullDereferenceChecker {
pub check_undefined: bool,
pub suppress_on_check: bool,
}
impl X86NullDereferenceChecker {
pub fn new() -> Self {
Self {
check_undefined: true,
suppress_on_check: true,
}
}
}
impl X86Checker for X86NullDereferenceChecker {
fn name(&self) -> &str {
"core.NullDereference"
}
fn description(&self) -> &str {
"Check for null pointer dereference on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::NullDereference
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
match expr {
Expr::Unary(op, inner) if *op == UnaryOp::Deref => {
let val = Self::eval_ptr(state, inner);
if val.is_null() {
bugs.push(CheckerBug {
category: BugCategory::NullDereference,
severity: BugSeverity::Error,
title: "Null pointer dereference".into(),
description:
"Dereferencing a null pointer will cause a segmentation fault on x86."
.into(),
file: String::new(),
line: 0,
column: 0,
});
} else if val.is_undefined() && self.check_undefined {
bugs.push(CheckerBug {
category: BugCategory::NullDereference,
severity: BugSeverity::Warning,
title: "Undefined pointer dereference".into(),
description: "Dereferencing an undefined pointer; value may be uninitialized.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
Expr::Member { is_arrow: true, .. } => {
if let Some(inner) = extract_base(expr) {
if let Expr::Ident(name) = inner {
let val = state.lookup_value(name);
if val.is_null() {
bugs.push(CheckerBug {
category: BugCategory::NullDereference,
severity: BugSeverity::Error,
title: "Null pointer dereference via ->".into(),
description: "Accessing member via null pointer on x86 will fault."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
Expr::Subscript { base, .. } => {
let val = Self::eval_ptr(state, base);
if val.is_null() {
bugs.push(CheckerBug {
category: BugCategory::NullDereference,
severity: BugSeverity::Error,
title: "Null pointer array subscript".into(),
description: "Array subscript on null pointer will segfault on x86.".into(),
file: String::new(),
line: 0,
column: 0,
});
} else if val.is_undefined() && self.check_undefined {
bugs.push(CheckerBug {
category: BugCategory::NullDereference,
severity: BugSeverity::Warning,
title: "Undefined pointer array subscript".into(),
description: "Array subscript on undefined pointer.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
_ => {}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86NullDereferenceChecker {
fn eval_ptr(state: &X86ProgramState, expr: &Expr) -> SVal {
match expr {
Expr::Ident(name) => state.lookup_value(name),
Expr::IntLiteral(0) | Expr::UIntLiteral(0, _) => SVal::Null,
Expr::Cast(_, inner) => Self::eval_ptr(state, inner),
Expr::Unary(UnaryOp::AddrOf, sub) => {
if let Expr::Ident(name) = sub.as_ref() {
SVal::Loc(state.lookup_value(name).as_int().unwrap_or(0) as u64)
} else {
SVal::Unknown
}
}
_ => SVal::Unknown,
}
}
}
fn extract_base(expr: &Expr) -> Option<&Expr> {
match expr {
Expr::Member {
base,
is_arrow: true,
..
} => Some(base),
_ => None,
}
}
#[derive(Debug, Default)]
pub struct X86UndefinedBinaryOperatorChecker {
pub check_overflow: bool,
}
impl X86UndefinedBinaryOperatorChecker {
pub fn new() -> Self {
Self {
check_overflow: true,
}
}
}
impl X86Checker for X86UndefinedBinaryOperatorChecker {
fn name(&self) -> &str {
"core.UndefinedBinaryOperator"
}
fn description(&self) -> &str {
"Check for undefined behavior in binary operators on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::UndefinedBehavior
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Binary(op, lhs, rhs) = expr {
match op {
BinaryOp::Div | BinaryOp::Mod => {
let rhs_val = eval_literal(rhs);
if rhs_val == SVal::Zero || rhs_val == SVal::ConcreteInt(0) {
bugs.push(CheckerBug {
category: BugCategory::UndefinedBehavior,
severity: BugSeverity::Error,
title: "Division by zero".into(),
description: "Division or modulo by zero is undefined behavior.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
BinaryOp::Shl | BinaryOp::Shr => {
let shift_val = eval_literal(rhs);
if let SVal::ConcreteInt(v) = shift_val {
if v < 0 {
bugs.push(CheckerBug {
category: BugCategory::UndefinedBehavior,
severity: BugSeverity::Error,
title: "Negative shift count".into(),
description: "Shifting by a negative amount is undefined behavior."
.into(),
file: String::new(),
line: 0,
column: 0,
});
} else if v >= 64 {
bugs.push(CheckerBug {
category: BugCategory::UndefinedBehavior,
severity: BugSeverity::Error,
title: "Excessive shift count".into(),
description:
"Shifting by >= bit width is undefined behavior on x86.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
BinaryOp::Add | BinaryOp::Sub if self.check_overflow => {
let lhs_val = eval_literal(lhs);
let rhs_val = eval_literal(rhs);
if let (SVal::ConcreteInt(l), SVal::ConcreteInt(r)) = (lhs_val, rhs_val) {
if *op == BinaryOp::Add {
if let Some(overflow) = l.checked_add(r) {
if (l > 0 && r > i64::MAX - l) || (l < 0 && r < i64::MIN - l) {
let _ = overflow;
}
} else {
bugs.push(CheckerBug {
category: BugCategory::UndefinedBehavior,
severity: BugSeverity::Error,
title: "Signed integer overflow in add".into(),
description: format!("{l} + {r} overflows a signed 64-bit integer, which is UB on x86."),
file: String::new(),
line: 0,
column: 0,
});
}
} else if let Some(overflow) = l.checked_sub(r) {
let _ = overflow;
} else {
bugs.push(CheckerBug {
category: BugCategory::UndefinedBehavior,
severity: BugSeverity::Error,
title: "Signed integer overflow in sub".into(),
description: format!("{l} - {r} overflows a signed 64-bit integer, which is UB on x86."),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
BinaryOp::Mul if self.check_overflow => {
let lhs_val = eval_literal(lhs);
let rhs_val = eval_literal(rhs);
if let (SVal::ConcreteInt(l), SVal::ConcreteInt(r)) = (lhs_val, rhs_val) {
if l.checked_mul(r).is_none() {
bugs.push(CheckerBug {
category: BugCategory::UndefinedBehavior,
severity: BugSeverity::Error,
title: "Signed integer overflow in mul".into(),
description: format!("{l} * {r} overflows a signed 64-bit integer, which is UB on x86."),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
_ => {}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
fn eval_literal(expr: &Expr) -> SVal {
match expr {
Expr::IntLiteral(v) => SVal::ConcreteInt(*v),
Expr::UIntLiteral(v, _) => SVal::ConcreteUInt(*v),
Expr::CharLiteral(c) => SVal::ConcreteInt(*c as i64),
Expr::FloatLiteral(_) => SVal::Unknown, Expr::DoubleLiteral(_) => SVal::Unknown,
Expr::Unary(UnaryOp::Minus, inner) => {
if let SVal::ConcreteInt(v) = eval_literal(inner) {
SVal::ConcreteInt(-v)
} else {
SVal::Unknown
}
}
_ => SVal::Unknown,
}
}
#[derive(Debug, Default)]
pub struct X86DivideByZeroChecker;
impl X86DivideByZeroChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86DivideByZeroChecker {
fn name(&self) -> &str {
"core.DivideZero"
}
fn description(&self) -> &str {
"Check for division by zero on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::DivideByZero
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Binary(op, _, rhs) = expr {
if *op == BinaryOp::Div || *op == BinaryOp::Mod {
let divisor = eval_literal(rhs);
let is_zero = match &divisor {
SVal::ConcreteInt(0) | SVal::ConcreteUInt(0) | SVal::Zero => true,
SVal::ConcreteFloat(fv) if fv.is_zero() => true,
_ => false,
};
if is_zero {
bugs.push(CheckerBug {
category: BugCategory::DivideByZero,
severity: BugSeverity::Error,
title: "Division by zero".into(),
description: "Division or modulo by zero is undefined behavior for integers, and produces Inf/NaN for floats on x86.".into(),
file: String::new(),
line: 0,
column: 0,
});
} else if divisor == SVal::Unknown {
if let Expr::Ident(name) = rhs.as_ref() {
if !state.is_constrained_true(&format!("{name}_ne_zero")) {
}
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
#[derive(Debug, Default)]
pub struct X86ReturnUndefinedChecker {
pub track_initialized: BTreeSet<String>,
}
impl X86ReturnUndefinedChecker {
pub fn new() -> Self {
Self {
track_initialized: BTreeSet::new(),
}
}
pub fn mark_initialized(&mut self, name: &str) {
self.track_initialized.insert(name.to_string());
}
}
impl X86Checker for X86ReturnUndefinedChecker {
fn name(&self) -> &str {
"core.UndefReturn"
}
fn description(&self) -> &str {
"Check for returning uninitialized values on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::Uninitialized
}
fn check_stmt(
&self,
stmt: &Stmt,
_graph: &mut X86ExplodedGraph,
_node_id: u64,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> (Vec<X86ProgramState>, Vec<CheckerBug>) {
let mut bugs = Vec::new();
if let Stmt::Return(Some(expr)) = stmt {
let val = Self::eval_return_expr(expr, state);
if val.is_undefined() {
bugs.push(CheckerBug {
category: BugCategory::Uninitialized,
severity: BugSeverity::Warning,
title: "Return of undefined value".into(),
description: "Returning an uninitialized value; the x86 RAX/XMM0 register will contain garbage.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
(vec![], bugs)
}
}
impl X86ReturnUndefinedChecker {
fn eval_return_expr(expr: &Expr, state: &X86ProgramState) -> SVal {
match expr {
Expr::Ident(name) => state.lookup_value(name),
Expr::IntLiteral(v) => SVal::ConcreteInt(*v),
Expr::UIntLiteral(v, _) => SVal::ConcreteUInt(*v),
_ => SVal::Unknown,
}
}
}
#[derive(Debug, Default)]
pub struct X86ArrayBoundsChecker {
pub array_sizes: HashMap<String, i64>,
pub check_pointer_arith: bool,
}
impl X86ArrayBoundsChecker {
pub fn new() -> Self {
Self {
array_sizes: HashMap::new(),
check_pointer_arith: true,
}
}
pub fn register_array(&mut self, name: impl Into<String>, size: i64) {
self.array_sizes.insert(name.into(), size);
}
}
impl X86Checker for X86ArrayBoundsChecker {
fn name(&self) -> &str {
"core.ArrayBounds"
}
fn description(&self) -> &str {
"Check for out-of-bounds array access on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::ArrayBounds
}
fn check_var_decl(
&self,
_decl: &VarDecl,
_state: &mut X86ProgramState,
_reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
vec![]
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Subscript { base, index } = expr {
let base_name = Self::extract_name(base);
if let Some(name) = base_name {
if let Some(size) = self.array_sizes.get(&name) {
if let SVal::ConcreteInt(idx) = eval_literal(index) {
if idx < 0 || idx >= *size {
bugs.push(CheckerBug {
category: BugCategory::ArrayBounds,
severity: BugSeverity::Error,
title: "Array index out of bounds".into(),
description: format!(
"Accessing '{name}[{idx}]' but array has size {size}. \
On x86 this will read/write adjacent memory, \
potentially corrupting the stack or heap."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86ArrayBoundsChecker {
fn extract_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Ident(name) => Some(name.clone()),
Expr::Unary(UnaryOp::Deref, inner) => Self::extract_name(inner),
_ => None,
}
}
}
#[derive(Debug, Default)]
pub struct X86StackAddrEscapeChecker;
impl X86StackAddrEscapeChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86StackAddrEscapeChecker {
fn name(&self) -> &str {
"core.StackAddressEscape"
}
fn description(&self) -> &str {
"Check for stack memory addresses escaping their scope on X86"
}
fn category(&self) -> BugCategory {
BugCategory::StackAddressEscape
}
fn check_stmt(
&self,
stmt: &Stmt,
_graph: &mut X86ExplodedGraph,
_node_id: u64,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> (Vec<X86ProgramState>, Vec<CheckerBug>) {
let mut bugs = Vec::new();
if let Stmt::Return(Some(expr)) = stmt {
if Self::is_stack_address(expr, state) {
bugs.push(CheckerBug {
category: BugCategory::StackAddressEscape,
severity: BugSeverity::Error,
title: "Stack address returned from function".into(),
description: "Returning the address of a local stack variable; \
on x86 the stack frame is destroyed on return, \
making the pointer dangling."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
(vec![], bugs)
}
}
impl X86StackAddrEscapeChecker {
fn is_stack_address(expr: &Expr, state: &X86ProgramState) -> bool {
match expr {
Expr::Unary(UnaryOp::AddrOf, inner) => {
if let Expr::Ident(name) = inner.as_ref() {
if let Some(region) = state.var_region(name) {
return region.is_stack();
}
}
Self::is_stack_address(inner, state)
}
_ => false,
}
}
}
#[derive(Debug)]
pub struct X86MallocChecker {
pub allocations: HashMap<u64, (u64, String, u32)>,
pub warn_zero_size: bool,
}
impl X86MallocChecker {
pub fn new() -> Self {
Self {
allocations: HashMap::new(),
warn_zero_size: true,
}
}
}
impl Default for X86MallocChecker {
fn default() -> Self {
Self::new()
}
}
impl X86Checker for X86MallocChecker {
fn name(&self) -> &str {
"unix.Malloc"
}
fn description(&self) -> &str {
"Check for malloc/free misuse on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::Memory
}
fn check_stmt(
&self,
stmt: &Stmt,
_graph: &mut X86ExplodedGraph,
_node_id: u64,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> (Vec<X86ProgramState>, Vec<CheckerBug>) {
let mut bugs = Vec::new();
let mut new_state = state.clone();
match stmt {
Stmt::Expr(expr) | Stmt::Return(Some(expr)) => {
if let Expr::Assign(_, lhs, rhs) = expr.as_ref() {
if let Expr::Call {
callee: callee_expr,
args: args,
} = rhs.as_ref()
{
if let Expr::Ident(callee) = callee_expr.as_ref() {
match callee.as_str() {
"malloc" | "calloc" | "realloc" => {
if let Some(lhs_name) = Self::extract_assign_target(lhs) {
let size = args
.first()
.and_then(|a| eval_literal(a).as_int())
.unwrap_or(0);
if size == 0 && self.warn_zero_size {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Warning,
title: "Zero-size allocation".into(),
description: format!("{callee}(0) allocates zero bytes; behavior is implementation-defined on x86."),
file: String::new(),
line: 0,
column: 0,
});
}
let region_id = state.next_region_id();
let mut tracked = state.allocate(region_id);
tracked =
tracked.bind_value(&lhs_name, SVal::Loc(region_id));
new_state = tracked;
}
}
"free" => {
if let Some(arg) = args.first() {
if let SVal::Loc(region_id) = Self::eval_to_loc(arg, state)
{
if state.is_freed(region_id) {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Error,
title: "Double free".into(),
description: "Calling free() on an already-freed pointer is undefined behavior on x86.".into(),
file: String::new(),
line: 0,
column: 0,
});
} else if !state.is_allocated(region_id) {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Warning,
title: "Free of non-heap memory".into(),
description: "Calling free() on memory not known to be heap-allocated; may corrupt the x86 allocator.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
new_state = state.free(region_id);
} else if Self::is_null_arg(arg, state) {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Note,
title: "Free of null pointer".into(),
description: "free(NULL) is a no-op but may indicate a logic error.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
_ => {}
}
}
}
}
}
_ => {}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
(vec![new_state], bugs)
}
fn check_end_of_function(
&self,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
for ®ion_id in &state.allocated {
if !state.is_freed(region_id) {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Error,
title: "Memory leak".into(),
description: format!(
"Allocated memory (region {region_id}) is not freed before the function returns; \
this will leak memory on x86."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86MallocChecker {
fn extract_assign_target(lhs: &Expr) -> Option<String> {
match lhs {
Expr::Ident(name) => Some(name.clone()),
Expr::Unary(UnaryOp::Deref, inner) => Self::extract_assign_target(inner),
_ => None,
}
}
fn eval_to_loc(expr: &Expr, state: &X86ProgramState) -> SVal {
match expr {
Expr::Ident(name) => state.lookup_value(name),
Expr::Cast(_, inner) => Self::eval_to_loc(inner, state),
Expr::IntLiteral(0) => SVal::Null,
_ => SVal::Unknown,
}
}
fn is_null_arg(expr: &Expr, state: &X86ProgramState) -> bool {
match expr {
Expr::IntLiteral(0) | Expr::UIntLiteral(0, _) => true,
Expr::Ident(name) => state.lookup_value(name).is_null(),
_ => false,
}
}
}
impl X86ProgramState {
fn next_region_id(&self) -> u64 {
self.allocated
.iter()
.chain(self.freed.iter())
.max()
.copied()
.unwrap_or(1000)
+ 1
}
}
#[derive(Debug, Default)]
pub struct X86VLASizeChecker;
impl X86VLASizeChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86VLASizeChecker {
fn name(&self) -> &str {
"core.VLASize"
}
fn description(&self) -> &str {
"Check for invalid (non-positive) VLA sizes on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::VLASize
}
fn check_var_decl(
&self,
decl: &VarDecl,
_state: &mut X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let TypeNode::Array {
elem: _,
size: Some(size),
} = decl.ty.base.as_ref()
{
if *size <= 0 {
bugs.push(CheckerBug {
category: BugCategory::VLASize,
severity: BugSeverity::Error,
title: "Invalid VLA size".into(),
description: format!(
"VLA size {size} is not positive; this is undefined behavior \
per C11 6.7.6.2 §5, and on x86 will likely cause a stack overflow."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
#[derive(Debug, Default)]
pub struct X86CastChecker {
pub warn_ptr_truncation: bool,
pub warn_alignment: bool,
pub warn_fn_ptr_cast: bool,
}
impl X86CastChecker {
pub fn new() -> Self {
Self {
warn_ptr_truncation: true,
warn_alignment: true,
warn_fn_ptr_cast: true,
}
}
}
impl X86Checker for X86CastChecker {
fn name(&self) -> &str {
"core.Cast"
}
fn description(&self) -> &str {
"Check for invalid or dangerous casts on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::InvalidCast
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Cast(target_ty, source) = expr {
let src_ty = Self::infer_type(source, state);
if self.warn_ptr_truncation {
if src_ty.is_pointer() && target_ty.is_integer() {
let target_size = target_ty.base_type_size().unwrap_or(4);
if target_size < 8 {
bugs.push(CheckerBug {
category: BugCategory::InvalidCast,
severity: BugSeverity::Warning,
title: "Pointer truncation in cast".into(),
description: format!(
"Casting a 64-bit pointer to a {target_size}-byte integer \
truncates the address; this may produce an invalid pointer \
if later cast back on x86_64."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
if src_ty.is_integer() && target_ty.is_pointer() {
if let SVal::ConcreteInt(v) = eval_literal(source) {
if v < 0 || v > 0x0000_7FFF_FFFF_FFFF {
if v < -0x8000_0000_0000 || v > -1 {
let is_hole = v > 0x0000_7FFF_FFFF_FFFF && v < -0x8000_0000_0000;
if is_hole {
bugs.push(CheckerBug {
category: BugCategory::InvalidCast,
severity: BugSeverity::Error,
title: "Non-canonical address cast".into(),
description: format!(
"Casting integer 0x{v:x} to pointer produces a non-canonical \
address on x86_64; dereferencing will fault."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86CastChecker {
fn infer_type(_expr: &Expr, _state: &X86ProgramState) -> QualType {
QualType::int()
}
}
impl QualType {
fn base_type_size(&self) -> Option<u64> {
match self.base.as_ref() {
TypeNode::Void => Some(0),
TypeNode::Char | TypeNode::SChar | TypeNode::UChar => Some(1),
TypeNode::Short | TypeNode::UShort => Some(2),
TypeNode::Int | TypeNode::UInt => Some(4),
TypeNode::Long | TypeNode::ULong => Some(8),
TypeNode::LongLong | TypeNode::ULongLong => Some(8),
TypeNode::Float => Some(4),
TypeNode::Double => Some(8),
TypeNode::LongDouble => Some(16),
TypeNode::Pointer(_) => Some(8),
TypeNode::Array { .. } => None, TypeNode::Bool => Some(1),
_ => Some(8),
}
}
}
#[derive(Debug, Default)]
pub struct X86EnumChecker {
pub enum_ranges: HashMap<String, BTreeSet<i64>>,
}
impl X86EnumChecker {
pub fn new() -> Self {
Self {
enum_ranges: HashMap::new(),
}
}
pub fn register_enum(&mut self, name: impl Into<String>, values: Vec<i64>) {
self.enum_ranges
.insert(name.into(), values.into_iter().collect());
}
}
impl X86Checker for X86EnumChecker {
fn name(&self) -> &str {
"core.Enum"
}
fn description(&self) -> &str {
"Check for out-of-range enum value usage on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::EnumRange
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Assign(_, lhs, rhs) = expr {
let lhs_ty = Self::infer_enum_type(lhs, state);
if let Some(enum_name) = lhs_ty {
if let Some(valid_values) = self.enum_ranges.get(&enum_name) {
if let SVal::ConcreteInt(v) = eval_literal(rhs) {
if !valid_values.contains(&v) {
bugs.push(CheckerBug {
category: BugCategory::EnumRange,
severity: BugSeverity::Warning,
title: "Enum value out of declared range".into(),
description: format!(
"Assigning {v} to enum '{enum_name}'; value is not one of the \
declared enumerators. On x86 this compiles but may indicate a bug."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86EnumChecker {
fn infer_enum_type(_expr: &Expr, _state: &X86ProgramState) -> Option<String> {
None
}
}
#[derive(Debug, Default)]
pub struct X86NonNullChecker {
pub nonnull_params: HashMap<String, HashSet<usize>>,
}
impl X86NonNullChecker {
pub fn new() -> Self {
Self {
nonnull_params: HashMap::new(),
}
}
pub fn register_nonnull(&mut self, func: impl Into<String>, param_indices: &[usize]) {
self.nonnull_params
.insert(func.into(), param_indices.iter().copied().collect());
}
}
impl X86Checker for X86NonNullChecker {
fn name(&self) -> &str {
"core.NonNullParamChecker"
}
fn description(&self) -> &str {
"Check for null passed to nonnull parameters on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::NonNullViolation
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Call {
callee: callee,
args: args,
} = expr
{
if let Expr::Ident(func_name) = callee.as_ref() {
if let Some(nonnull_set) = self.nonnull_params.get(func_name) {
for &idx in nonnull_set {
if idx < args.len() {
let arg_val = Self::eval_arg(&args[idx], state);
if arg_val.is_null() {
bugs.push(CheckerBug {
category: BugCategory::NonNullViolation,
severity: BugSeverity::Error,
title: "Null passed to nonnull parameter".into(),
description: format!(
"Argument {idx} to '{func_name}' is null, but the parameter \
is marked __attribute__((nonnull)). On x86 this is UB \
and the compiler may elide null checks."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86NonNullChecker {
fn eval_arg(expr: &Expr, state: &X86ProgramState) -> SVal {
match expr {
Expr::IntLiteral(0) | Expr::UIntLiteral(0, _) => SVal::Null,
Expr::Ident(name) => state.lookup_value(name),
_ => SVal::Unknown,
}
}
}
#[derive(Debug, Default)]
pub struct X86FormatStringChecker {
pub printf_functions: HashSet<String>,
pub scanf_functions: HashSet<String>,
}
impl X86FormatStringChecker {
pub fn new() -> Self {
let mut printf = HashSet::new();
for name in &[
"printf",
"fprintf",
"sprintf",
"snprintf",
"dprintf",
"vprintf",
"vfprintf",
"vsprintf",
"vsnprintf",
"vdprintf",
"__printf_chk",
"__fprintf_chk",
] {
printf.insert(name.to_string());
}
let mut scanf = HashSet::new();
for name in &["scanf", "fscanf", "sscanf", "vscanf", "vfscanf", "vsscanf"] {
scanf.insert(name.to_string());
}
Self {
printf_functions: printf,
scanf_functions: scanf,
}
}
}
impl X86Checker for X86FormatStringChecker {
fn name(&self) -> &str {
"security.FormatString"
}
fn description(&self) -> &str {
"Check for format string vulnerabilities on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::FormatString
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Call {
callee: callee,
args: args,
} = expr
{
if let Expr::Ident(name) = callee.as_ref() {
let is_printf = self.printf_functions.contains(name.as_str());
let is_scanf = self.scanf_functions.contains(name.as_str());
if (is_printf || is_scanf) && !args.is_empty() {
let fmt_arg = &args[0];
if !Self::is_string_literal(fmt_arg) && !Self::is_const_string(fmt_arg) {
let vuln_type = if is_scanf { "scanf" } else { "printf" };
bugs.push(CheckerBug {
category: BugCategory::FormatString,
severity: BugSeverity::Critical,
title: "Format string is not a string literal".into(),
description: format!(
"The format string argument to '{name}' is not a string literal. \
If attacker-controlled, this enables arbitrary memory read/write \
on x86 (CWE-134). Use \"%s\" with a separate string argument, \
or change to {vuln_type}(\"%s\", ...)."
),
file: String::new(),
line: 0,
column: 0,
});
}
if let Expr::Ident(fmt_var) = fmt_arg {
if state.is_tainted(fmt_var) {
bugs.push(CheckerBug {
category: BugCategory::FormatString,
severity: BugSeverity::Critical,
title: "Tainted format string".into(),
description: format!(
"The format string argument to '{name}' comes from tainted \
(user-controlled) data. This is a format string vulnerability \
(CWE-134) exploitable on x86."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
if let Expr::StringLiteral(fmt_str) = fmt_arg {
let spec_count = Self::count_format_specifiers(fmt_str);
let arg_count = args.len().saturating_sub(1); if spec_count > arg_count {
bugs.push(CheckerBug {
category: BugCategory::FormatString,
severity: BugSeverity::Error,
title: "Too few arguments for format string".into(),
description: format!(
"Format string expects {spec_count} arguments but only {arg_count} provided. \
On x86_64, missing args read from registers RDI..R9 then the stack, \
possibly leaking sensitive data."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86FormatStringChecker {
fn is_string_literal(expr: &Expr) -> bool {
matches!(expr, Expr::StringLiteral(_))
}
fn is_const_string(_expr: &Expr) -> bool {
false
}
fn count_format_specifiers(fmt: &str) -> usize {
let mut count = 0;
let chars: Vec<char> = fmt.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '%' {
if i + 1 < chars.len() && chars[i + 1] != '%' {
count += 1;
}
}
i += 1;
}
count
}
}
#[derive(Debug, Default)]
pub struct X86ShiftChecker;
impl X86ShiftChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86ShiftChecker {
fn name(&self) -> &str {
"core.Shift"
}
fn description(&self) -> &str {
"Check for invalid shift counts on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::ShiftError
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Binary(op, lhs, rhs) = expr {
if *op == BinaryOp::Shl || *op == BinaryOp::Shr {
let shift_val = eval_literal(rhs);
let lhs_width = Self::estimate_bit_width(lhs, state);
match shift_val {
SVal::ConcreteInt(v) if v < 0 => {
bugs.push(CheckerBug {
category: BugCategory::ShiftError,
severity: BugSeverity::Error,
title: "Negative shift count".into(),
description: "Shifting by a negative amount is undefined behavior (C11 §6.5.7). \
On x86 the processor uses only the low bits of the shift count \
(CL register).".into(),
file: String::new(),
line: 0,
column: 0,
});
}
SVal::ConcreteInt(v) if v >= lhs_width => {
bugs.push(CheckerBug {
category: BugCategory::ShiftError,
severity: BugSeverity::Error,
title: "Shift count >= bit width".into(),
description: format!(
"Shifting a {lhs_width}-bit value by {v} bits is undefined behavior \
(C11 §6.5.7). On x86 the processor masks the shift count, so the \
actual shift is {v} % {lhs_width} = {} bits.",
v % lhs_width
),
file: String::new(),
line: 0,
column: 0,
});
}
SVal::ConcreteUInt(v) if v >= lhs_width as u64 => {
bugs.push(CheckerBug {
category: BugCategory::ShiftError,
severity: BugSeverity::Error,
title: "Shift count exceeds bit width".into(),
description: format!(
"Shifting a {lhs_width}-bit unsigned value by {v} bits is UB."
),
file: String::new(),
line: 0,
column: 0,
});
}
_ => {}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86ShiftChecker {
fn estimate_bit_width(_expr: &Expr, _state: &X86ProgramState) -> i64 {
32
}
}
#[derive(Debug, Default)]
pub struct X86SignedIntegerOverflowChecker;
impl X86SignedIntegerOverflowChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86SignedIntegerOverflowChecker {
fn name(&self) -> &str {
"core.SignedOverflow"
}
fn description(&self) -> &str {
"Check for signed integer overflow on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::SignedOverflow
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Binary(op, lhs, rhs) = expr {
if let (SVal::ConcreteInt(l), SVal::ConcreteInt(r)) =
(eval_literal(lhs), eval_literal(rhs))
{
let overflow = match op {
BinaryOp::Add => l.checked_add(r).is_none(),
BinaryOp::Sub => l.checked_sub(r).is_none(),
BinaryOp::Mul => l.checked_mul(r).is_none(),
BinaryOp::Div => r == 0 || (l == i64::MIN && r == -1),
BinaryOp::Mod => r == 0 || (l == i64::MIN && r == -1),
_ => false,
};
if overflow {
let op_str = op.as_str();
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Error,
title: "Signed integer overflow".into(),
description: format!(
"{l} {op_str} {r} overflows a signed 64-bit integer, \
which is undefined behavior (C11 §6.5). On x86 the hardware \
wraps, but the compiler may optimize away checks."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
#[derive(Debug, Default)]
pub struct X86FloatLoopCounterChecker;
impl X86FloatLoopCounterChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86FloatLoopCounterChecker {
fn name(&self) -> &str {
"alpha.core.FloatLoopCounter"
}
fn description(&self) -> &str {
"Check for floating-point variables used as loop counters on X86"
}
fn category(&self) -> BugCategory {
BugCategory::FloatLoopCounter
}
fn check_stmt(
&self,
stmt: &Stmt,
_graph: &mut X86ExplodedGraph,
_node_id: u64,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> (Vec<X86ProgramState>, Vec<CheckerBug>) {
let mut bugs = Vec::new();
match stmt {
Stmt::For {
init, cond, incr, ..
} => {
let init_expr = init.as_ref().and_then(|s| match s.as_ref() {
Stmt::Expr(e) => Some(e.as_ref()),
_ => None,
});
let uses_float = [
init_expr,
cond.as_ref().map(|b| b.as_ref()),
incr.as_ref().map(|b| b.as_ref()),
]
.iter()
.any(|opt| opt.map_or(false, |e| Self::expr_is_float(e, state)));
if uses_float {
bugs.push(CheckerBug {
category: BugCategory::FloatLoopCounter,
severity: BugSeverity::Warning,
title: "Floating-point used as loop counter".into(),
description: "Using a floating-point variable as a loop counter is unreliable \
due to rounding errors. On x86, the x87 FPU and SSE may produce \
different results for the same code, causing the loop to miss \
iterations or run forever.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
Stmt::While { cond, .. } => {
if Self::cond_uses_float(cond, state) {
bugs.push(CheckerBug {
category: BugCategory::FloatLoopCounter,
severity: BugSeverity::Warning,
title: "Floating-point in while-loop condition".into(),
description: "Floating-point loop condition may not terminate as expected \
due to rounding on x86."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
_ => {}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
(vec![], bugs)
}
}
impl X86FloatLoopCounterChecker {
fn expr_is_float(expr: &Expr, state: &X86ProgramState) -> bool {
match expr {
Expr::Ident(name) => {
let val = state.lookup_value(name);
matches!(val, SVal::ConcreteFloat(_))
}
Expr::FloatLiteral(_) | Expr::DoubleLiteral(_) => true,
Expr::Binary(_, lhs, rhs) => {
Self::expr_is_float(lhs, state) || Self::expr_is_float(rhs, state)
}
Expr::Unary(_, inner) => Self::expr_is_float(inner, state),
_ => false,
}
}
fn cond_uses_float(cond: &Expr, state: &X86ProgramState) -> bool {
match cond {
Expr::FloatLiteral(_) | Expr::DoubleLiteral(_) => true,
Expr::Binary(op, lhs, rhs) if op.is_comparison() => {
Self::expr_is_float(lhs, state) || Self::expr_is_float(rhs, state)
}
_ => Self::expr_is_float(cond, state),
}
}
}
#[derive(Debug, Default)]
pub struct X86PthreadLockChecker {
pub lock_functions: HashMap<String, usize>,
pub unlock_functions: HashMap<String, usize>,
}
impl X86PthreadLockChecker {
pub fn new() -> Self {
let mut lock = HashMap::new();
lock.insert("pthread_mutex_lock".into(), 0);
lock.insert("pthread_rwlock_rdlock".into(), 0);
lock.insert("pthread_rwlock_wrlock".into(), 0);
lock.insert("pthread_spin_lock".into(), 0);
lock.insert("mtx_lock".into(), 0);
let mut unlock = HashMap::new();
unlock.insert("pthread_mutex_unlock".into(), 0);
unlock.insert("pthread_rwlock_unlock".into(), 0);
unlock.insert("pthread_spin_unlock".into(), 0);
unlock.insert("mtx_unlock".into(), 0);
Self {
lock_functions: lock,
unlock_functions: unlock,
}
}
}
impl X86Checker for X86PthreadLockChecker {
fn name(&self) -> &str {
"alpha.unix.PthreadLock"
}
fn description(&self) -> &str {
"Check for pthread lock/unlock pairing on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::Locking
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Call {
callee: callee,
args: args,
} = expr
{
if let Expr::Ident(name) = callee.as_ref() {
if let Some(&arg_idx) = self.lock_functions.get(name.as_str()) {
if let Some(mutex_name) = Self::extract_mutex_name(args.get(arg_idx)) {
if state.lock_held(&mutex_name) {
bugs.push(CheckerBug {
category: BugCategory::Locking,
severity: BugSeverity::Error,
title: "Double lock".into(),
description: format!(
"Attempting to lock mutex '{mutex_name}' which is already held; \
on x86 (PTHREAD_MUTEX_NORMAL) this will deadlock."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
if let Some(&arg_idx) = self.unlock_functions.get(name.as_str()) {
if let Some(mutex_name) = Self::extract_mutex_name(args.get(arg_idx)) {
if !state.lock_held(&mutex_name) {
bugs.push(CheckerBug {
category: BugCategory::Locking,
severity: BugSeverity::Error,
title: "Unlock of non-locked mutex".into(),
description: format!(
"Attempting to unlock mutex '{mutex_name}' which is not held; \
on x86 this is undefined behavior and may corrupt the mutex."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
fn check_end_of_function(
&self,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
for lock in &state.held_locks {
bugs.push(CheckerBug {
category: BugCategory::Locking,
severity: BugSeverity::Error,
title: "Lock held at function exit".into(),
description: format!(
"Mutex '{lock}' is still locked when the function returns. \
This may cause a deadlock if the mutex is never unlocked."
),
file: String::new(),
line: 0,
column: 0,
});
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86PthreadLockChecker {
fn extract_mutex_name(expr: Option<&Expr>) -> Option<String> {
match expr {
Some(Expr::Ident(name)) => Some(name.clone()),
Some(Expr::Unary(UnaryOp::AddrOf, inner)) => {
if let Expr::Ident(name) = inner.as_ref() {
Some(name.clone())
} else {
None
}
}
_ => None,
}
}
}
#[derive(Debug)]
pub struct X86RetainCountChecker {
pub retain_functions: HashMap<String, usize>,
pub release_functions: HashMap<String, usize>,
pub max_tracked_count: i32,
}
impl X86RetainCountChecker {
pub fn new() -> Self {
let mut retain = HashMap::new();
retain.insert("CFRetain".into(), 0);
retain.insert("CGColorRetain".into(), 0);
retain.insert("g_object_ref".into(), 0);
retain.insert("AddRef".into(), 0);
let mut release = HashMap::new();
release.insert("CFRelease".into(), 0);
release.insert("CGColorRelease".into(), 0);
release.insert("g_object_unref".into(), 0);
release.insert("Release".into(), 0);
Self {
retain_functions: retain,
release_functions: release,
max_tracked_count: 100,
}
}
}
impl Default for X86RetainCountChecker {
fn default() -> Self {
Self::new()
}
}
impl X86Checker for X86RetainCountChecker {
fn name(&self) -> &str {
"osx.RetainCount"
}
fn description(&self) -> &str {
"Check for retain/release count mismatches (Apple/COM style) on X86"
}
fn category(&self) -> BugCategory {
BugCategory::RetainCount
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Call {
callee: callee,
args: args,
} = expr
{
if let Expr::Ident(name) = callee.as_ref() {
if let Some(&arg_idx) = self.retain_functions.get(name.as_str()) {
if let Some(obj_name) = Self::extract_obj_name(args.get(arg_idx)) {
if let SVal::Loc(region_id) = state.lookup_value(&obj_name) {
let count = state.retain_count(region_id);
if count >= self.max_tracked_count {
bugs.push(CheckerBug {
category: BugCategory::RetainCount,
severity: BugSeverity::Warning,
title: "Excessive retain count".into(),
description: format!(
"Object '{obj_name}' has been retained {count} times; \
possible retain loop."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
if let Some(&arg_idx) = self.release_functions.get(name.as_str()) {
if let Some(obj_name) = Self::extract_obj_name(args.get(arg_idx)) {
if let SVal::Loc(region_id) = state.lookup_value(&obj_name) {
let count = state.retain_count(region_id);
if count <= 0 {
bugs.push(CheckerBug {
category: BugCategory::RetainCount,
severity: BugSeverity::Error,
title: "Over-release".into(),
description: format!(
"Releasing object '{obj_name}' with retain count {count}; \
this is a use-after-free bug. On x86, the freed memory \
will be reused by subsequent allocations."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
fn check_end_of_function(
&self,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
for (®ion_id, &count) in &state.ref_counts {
if count > 0 && state.is_allocated(region_id) {
bugs.push(CheckerBug {
category: BugCategory::RetainCount,
severity: BugSeverity::Error,
title: "Retained object leaked".into(),
description: format!(
"Object (region {region_id}) has retain count {count} but is not released \
before the function returns. This is a memory leak."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
impl X86RetainCountChecker {
fn extract_obj_name(expr: Option<&Expr>) -> Option<String> {
match expr {
Some(Expr::Ident(name)) => Some(name.clone()),
_ => None,
}
}
}
#[derive(Debug, Default)]
pub struct X86UnreachableCodeChecker;
impl X86UnreachableCodeChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86UnreachableCodeChecker {
fn name(&self) -> &str {
"deadcode.UnreachableCode"
}
fn description(&self) -> &str {
"Detect unreachable code on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::UnreachableCode
}
fn check_stmt(
&self,
stmt: &Stmt,
_graph: &mut X86ExplodedGraph,
node_id: u64,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> (Vec<X86ProgramState>, Vec<CheckerBug>) {
let mut bugs = Vec::new();
if state.is_dead {
bugs.push(CheckerBug {
category: BugCategory::UnreachableCode,
severity: BugSeverity::Warning,
title: "Unreachable code".into(),
description: "This code is unreachable; a preceding return, break, continue, \
noreturn call, or infinite loop makes it dead. On x86 the compiler \
will remove it entirely."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
if let Stmt::Expr(expr) = stmt {
if let Expr::Call {
callee: callee,
args: _,
} = expr.as_ref()
{
if let Expr::Ident(name) = callee.as_ref() {
if Self::is_noreturn(name) {
let dead_state = state.mark_dead();
let _ = node_id; let _ = dead_state;
}
}
}
}
if let Stmt::If { cond, .. } = stmt {
if Self::is_const_false(cond, state) {
bugs.push(CheckerBug {
category: BugCategory::UnreachableCode,
severity: BugSeverity::Warning,
title: "Condition is always false".into(),
description: "The if-condition is always false; the true-branch is dead code."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
if Self::is_const_true(cond, state) {
bugs.push(CheckerBug {
category: BugCategory::UnreachableCode,
severity: BugSeverity::Warning,
title: "Condition is always true".into(),
description: "The if-condition is always true; the false-branch is dead code."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
(vec![], bugs)
}
}
impl X86UnreachableCodeChecker {
fn is_noreturn(name: &str) -> bool {
matches!(
name,
"abort" | "exit" | "_exit" | "__assert_fail" | "longjmp" | "__builtin_unreachable"
)
}
fn is_const_false(cond: &Expr, state: &X86ProgramState) -> bool {
match cond {
Expr::IntLiteral(0) | Expr::UIntLiteral(0, _) => true,
Expr::Ident(name) => {
state.is_constrained_false(name) || state.lookup_value(name).is_zero()
}
Expr::Unary(UnaryOp::Not, inner) => Self::is_const_true(inner, state),
_ => false,
}
}
fn is_const_true(cond: &Expr, state: &X86ProgramState) -> bool {
match cond {
Expr::IntLiteral(v) if *v != 0 => true,
Expr::UIntLiteral(v, _) if *v != 0 => true,
Expr::Ident(name) => state.is_constrained_true(name),
Expr::Unary(UnaryOp::Not, inner) => Self::is_const_false(inner, state),
_ => false,
}
}
}
#[derive(Debug)]
pub struct X86SecuritySyntaxChecker {
pub dangerous_functions: HashSet<String>,
pub replacements: HashMap<String, String>,
}
impl X86SecuritySyntaxChecker {
pub fn new() -> Self {
let dangerous: HashSet<String> = vec![
"gets", "strcpy", "strcat", "sprintf", "vsprintf", "scanf", "realpath", "getwd",
"mktemp", "tmpnam", "rand", "getpass", "cuserid",
]
.into_iter()
.map(|s| s.to_string())
.collect();
let mut replacements = HashMap::new();
replacements.insert("gets".into(), "fgets".into());
replacements.insert("strcpy".into(), "strncpy".into());
replacements.insert("strcat".into(), "strncat".into());
replacements.insert("sprintf".into(), "snprintf".into());
replacements.insert("vsprintf".into(), "vsnprintf".into());
replacements.insert("scanf".into(), "a bounded input function".into());
replacements.insert("realpath".into(), "realpath(path, NULL)".into());
replacements.insert("getwd".into(), "getcwd".into());
replacements.insert("mktemp".into(), "mkstemp".into());
replacements.insert("tmpnam".into(), "mkstemp or tmpfile".into());
replacements.insert("rand".into(), "a CSPRNG (getrandom, /dev/urandom)".into());
Self {
dangerous_functions: dangerous,
replacements,
}
}
}
impl Default for X86SecuritySyntaxChecker {
fn default() -> Self {
Self::new()
}
}
impl X86Checker for X86SecuritySyntaxChecker {
fn name(&self) -> &str {
"security.SecuritySyntax"
}
fn description(&self) -> &str {
"Check for use of dangerous C library functions on X86"
}
fn category(&self) -> BugCategory {
BugCategory::Security
}
fn check_expr(
&self,
expr: &Expr,
_state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Call {
callee: callee,
args: _,
} = expr
{
if let Expr::Ident(name) = callee.as_ref() {
if self.dangerous_functions.contains(name.as_str()) {
let replacement = self
.replacements
.get(name.as_str())
.map(|s| format!(" Use '{}' instead.", s))
.unwrap_or_default();
bugs.push(CheckerBug {
category: BugCategory::Security,
severity: BugSeverity::Warning,
title: format!("Use of dangerous function '{name}'"),
description: format!(
"The function '{name}' is unsafe and deprecated. \
On x86 it can cause buffer overflows, race conditions, \
or weak cryptography.{replacement}"
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
bugs
}
}
#[derive(Debug)]
pub struct X86TaintChecker {
pub taint_sources: HashSet<String>,
pub taint_sinks: HashSet<String>,
pub taint_propagators: HashSet<String>,
}
impl X86TaintChecker {
pub fn new() -> Self {
let sources: HashSet<String> = vec![
"read", "recv", "recvfrom", "fread", "fgets", "scanf", "getenv", "getenv_s", "getopt",
]
.into_iter()
.map(|s| s.to_string())
.collect();
let sinks: HashSet<String> = vec![
"system", "popen", "execl", "execle", "execlp", "execv", "execve", "execvp", "fprintf",
"printf", "sprintf", "memcpy", "memmove", "malloc", "calloc", "realloc",
]
.into_iter()
.map(|s| s.to_string())
.collect();
let propagators: HashSet<String> = vec!["strcpy", "strcat", "strdup", "strndup", "memcpy"]
.into_iter()
.map(|s| s.to_string())
.collect();
Self {
taint_sources: sources,
taint_sinks: sinks,
taint_propagators: propagators,
}
}
}
impl Default for X86TaintChecker {
fn default() -> Self {
Self::new()
}
}
impl X86Checker for X86TaintChecker {
fn name(&self) -> &str {
"alpha.security.Taint"
}
fn description(&self) -> &str {
"Track tainted data flow to sensitive sinks on X86 targets"
}
fn category(&self) -> BugCategory {
BugCategory::Taint
}
fn check_stmt(
&self,
stmt: &Stmt,
_graph: &mut X86ExplodedGraph,
_node_id: u64,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> (Vec<X86ProgramState>, Vec<CheckerBug>) {
let mut bugs = Vec::new();
let mut new_state = state.clone();
if let Stmt::Expr(expr) = stmt {
if let Expr::Assign(_, lhs, rhs) = expr.as_ref() {
if let Expr::Call {
callee: callee,
args: _args,
} = rhs.as_ref()
{
if let Expr::Ident(src_name) = callee.as_ref() {
if self.taint_sources.contains(src_name.as_str()) {
if let Expr::Ident(dst_name) = lhs.as_ref() {
new_state = new_state.taint(dst_name);
}
}
}
}
if let Expr::Ident(rhs_name) = rhs.as_ref() {
if state.is_tainted(rhs_name) {
if let Expr::Ident(lhs_name) = lhs.as_ref() {
new_state = new_state.taint(lhs_name);
}
}
}
}
if let Expr::Call {
callee: callee,
args: args,
} = expr.as_ref()
{
if let Expr::Ident(sink_name) = callee.as_ref() {
if self.taint_sinks.contains(sink_name.as_str()) {
for arg in args.iter() {
if Self::is_tainted_arg(arg, state) {
let severity =
if sink_name == "system" || sink_name.starts_with("exec") {
BugSeverity::Critical
} else {
BugSeverity::Error
};
bugs.push(CheckerBug {
category: BugCategory::Taint,
severity,
title: "Tainted data reaches sensitive sink".into(),
description: format!(
"Untrusted (tainted) data flows into '{sink_name}', which is a \
sensitive operation. On x86, this may allow command injection, \
buffer overflow, or other exploitation (CWE-20)."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
if let Expr::Subscript { index, .. } = expr.as_ref() {
if let Expr::Ident(idx_name) = index.as_ref() {
if state.is_tainted(idx_name) {
bugs.push(CheckerBug {
category: BugCategory::Taint,
severity: BugSeverity::Error,
title: "Tainted array index".into(),
description: "An array is indexed by tainted (user-controlled) data. \
On x86 this enables arbitrary read/write if the index is \
not properly bounds-checked (CWE-129)."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
(vec![new_state], bugs)
}
}
impl X86TaintChecker {
fn is_tainted_arg(arg: &Expr, state: &X86ProgramState) -> bool {
match arg {
Expr::Ident(name) => state.is_tainted(name),
Expr::Cast(_, inner) => Self::is_tainted_arg(inner, state),
Expr::Unary(_, inner) => Self::is_tainted_arg(inner, state),
_ => false,
}
}
}
pub struct X86CheckerRegistry {
checkers: HashMap<String, Box<dyn X86Checker>>,
disabled: HashSet<String>,
registration_order: Vec<String>,
}
impl X86CheckerRegistry {
pub fn new() -> Self {
Self {
checkers: HashMap::new(),
disabled: HashSet::new(),
registration_order: Vec::new(),
}
}
pub fn register(&mut self, checker: Box<dyn X86Checker>) {
let name = checker.name().to_string();
self.registration_order.push(name.clone());
self.checkers.insert(name, checker);
}
pub fn register_checker<C: X86Checker + 'static>(&mut self, checker: C) {
self.register(Box::new(checker));
}
pub fn disable(&mut self, name: &str) {
self.disabled.insert(name.to_string());
}
pub fn enable(&mut self, name: &str) {
self.disabled.remove(name);
}
pub fn is_enabled(&self, name: &str) -> bool {
self.checkers.contains_key(name) && !self.disabled.contains(name)
}
pub fn enabled_checkers(&self) -> Vec<&dyn X86Checker> {
self.registration_order
.iter()
.filter_map(|name| {
if self.is_enabled(name) {
self.checkers.get(name).map(|c| c.as_ref())
} else {
None
}
})
.collect()
}
pub fn checker_names(&self) -> Vec<&str> {
self.registration_order.iter().map(|s| s.as_str()).collect()
}
pub fn count(&self) -> usize {
self.checkers.len()
}
pub fn enabled_count(&self) -> usize {
self.checkers.len() - self.disabled.len()
}
pub fn is_empty(&self) -> bool {
self.checkers.is_empty()
}
pub fn with_all_defaults(subtarget: Option<X86Subtarget>) -> Self {
let mut registry = Self::new();
registry.register_checker(X86NullDereferenceChecker::new());
registry.register_checker(X86UndefinedBinaryOperatorChecker::new());
registry.register_checker(X86DivideByZeroChecker::new());
registry.register_checker(X86ReturnUndefinedChecker::new());
registry.register_checker(X86ArrayBoundsChecker::new());
registry.register_checker(X86StackAddrEscapeChecker::new());
registry.register_checker(X86MallocChecker::new());
registry.register_checker(X86VLASizeChecker::new());
registry.register_checker(X86CastChecker::new());
registry.register_checker(X86EnumChecker::new());
registry.register_checker(X86NonNullChecker::new());
registry.register_checker(X86FormatStringChecker::new());
registry.register_checker(X86ShiftChecker::new());
registry.register_checker(X86SignedIntegerOverflowChecker::new());
registry.register_checker(X86FloatLoopCounterChecker::new());
registry.register_checker(X86PthreadLockChecker::new());
registry.register_checker(X86RetainCountChecker::new());
registry.register_checker(X86UnreachableCodeChecker::new());
registry.register_checker(X86SecuritySyntaxChecker::new());
registry.register_checker(X86TaintChecker::new());
let _ = subtarget;
registry
}
}
impl Default for X86CheckerRegistry {
fn default() -> Self {
Self::with_all_defaults(None)
}
}
pub struct X86StaticAnalyzer {
pub registry: X86CheckerRegistry,
pub reporter: X86BugReporter,
pub graph: X86ExplodedGraph,
pub initial_state: X86ProgramState,
pub interprocedural: bool,
pub max_nodes: usize,
pub stats: X86AnalyzerStats,
}
#[derive(Debug, Clone, Default)]
pub struct X86AnalyzerStats {
pub functions_analyzed: usize,
pub stmts_visited: usize,
pub exprs_checked: usize,
pub nodes_created: usize,
pub paths_explored: usize,
pub bugs_found: usize,
pub time_ms: u64,
}
impl X86StaticAnalyzer {
pub fn new(registry: X86CheckerRegistry) -> Self {
Self {
registry,
reporter: X86BugReporter::new(),
graph: X86ExplodedGraph::new(),
initial_state: X86ProgramState::new(),
interprocedural: false,
max_nodes: 100_000,
stats: X86AnalyzerStats::default(),
}
}
pub fn with_defaults() -> Self {
Self::new(X86CheckerRegistry::with_all_defaults(None))
}
pub fn with_subtarget(subtarget: X86Subtarget) -> Self {
let registry = X86CheckerRegistry::with_all_defaults(Some(subtarget.clone()));
let initial_state = X86ProgramState::with_subtarget(subtarget);
Self {
registry,
reporter: X86BugReporter::new(),
graph: X86ExplodedGraph::new(),
initial_state,
interprocedural: false,
max_nodes: 100_000,
stats: X86AnalyzerStats::default(),
}
}
pub fn analyze(&mut self, tu: &TranslationUnit) {
self.reporter = X86BugReporter::new();
self.graph = X86ExplodedGraph::new();
for decl in &tu.decls {
match decl {
Decl::Function(func_decl) => {
self.analyze_function(func_decl);
self.stats.functions_analyzed += 1;
}
Decl::Variable(var_decl) => {
self.analyze_var_decl(var_decl);
}
_ => {}
}
}
let enabled = self.registry.enabled_checkers();
for checker in enabled {
let bugs = checker.check_end_of_tu(&mut self.reporter);
self.stats.bugs_found += bugs.len();
}
}
fn analyze_function(&mut self, func: &FunctionDecl) {
let checkers = self.registry.enabled_checkers();
let checkers_len = checkers.len();
let mut state = self.initial_state.clone();
for checker in &checkers {
let bugs = checker.check_function_decl(func, &mut state, &mut self.reporter);
self.stats.bugs_found += bugs.len();
}
let _ = checkers_len;
let body = match &func.body {
Some(body) => body,
None => return,
};
self.graph = X86ExplodedGraph::new();
self.graph.current_function = Some(func.name.clone());
let root_id = self.graph.create_root(state.clone());
self.walk_stmts_in_state(&body.stmts, root_id);
let terminal_states: Vec<X86ProgramState> = self
.graph
.nodes
.values()
.filter(|n| self.graph.successors(n.id).is_empty() && !n.state.infeasible)
.map(|n| n.state.clone())
.collect();
for term_state in &terminal_states {
let checkers2 = self.registry.enabled_checkers();
for checker in &checkers2 {
let bugs = checker.check_end_of_function(term_state, &mut self.reporter);
self.stats.bugs_found += bugs.len();
}
}
}
fn walk_stmts_in_state(&mut self, stmts: &[Stmt], from_node: u64) {
let mut current_node = from_node;
for stmt in stmts {
self.stats.stmts_visited += 1;
if self.graph.node_count() >= self.max_nodes {
break;
}
let current_state = self.graph.nodes[¤t_node].state.clone();
if current_state.infeasible || current_state.is_dead {
break;
}
let checkers = self.registry.enabled_checkers();
let mut next_states: Vec<X86ProgramState> = vec![current_state.clone()];
let mut all_bugs: Vec<CheckerBug> = Vec::new();
for checker in &checkers {
let (succ_states, bugs) = checker.check_stmt(
stmt,
&mut self.graph,
current_node,
¤t_state,
&mut self.reporter,
);
if !succ_states.is_empty() {
next_states = succ_states;
}
all_bugs.extend(bugs);
}
self.stats.bugs_found += all_bugs.len();
self.check_exprs_in_stmt(stmt, ¤t_state);
if next_states.len() == 1 {
let next = next_states.into_iter().next().unwrap();
let advanced = next.advance();
let new_node = self.graph.add_node(
current_node,
Self::stmt_label(stmt),
advanced,
X86EdgeKind::Fallthrough,
"",
);
if new_node != 0 {
current_node = new_node;
} else {
break;
}
} else {
let mut last_node = 0u64;
for (_i, ns) in next_states.into_iter().enumerate() {
let advanced = ns.advance();
let edge_kind = if advanced.infeasible {
X86EdgeKind::FalseBranch
} else {
X86EdgeKind::TrueBranch
};
let new_node = self.graph.add_node(
current_node,
Self::stmt_label(stmt),
advanced,
edge_kind,
"",
);
if new_node != 0 {
last_node = new_node;
}
}
if last_node != 0 {
current_node = last_node;
} else {
break;
}
}
}
}
fn check_exprs_in_stmt(&mut self, stmt: &Stmt, state: &X86ProgramState) {
match stmt {
Stmt::Expr(expr) | Stmt::Return(Some(expr)) => {
self.check_expr_recursive(expr, state);
}
Stmt::If { cond, .. } => {
self.check_expr_recursive(cond, state);
}
Stmt::While { cond, .. } => {
self.check_expr_recursive(cond, state);
}
Stmt::For {
init, cond, incr, ..
} => {
if let Some(init_stmt) = init.as_ref() {
if let Stmt::Expr(e) = init_stmt.as_ref() {
self.check_expr_recursive(e, state);
}
}
if let Some(e) = cond.as_ref() {
self.check_expr_recursive(e, state);
}
if let Some(e) = incr.as_ref() {
self.check_expr_recursive(e, state);
}
}
Stmt::Switch { expr, .. } => {
self.check_expr_recursive(expr, state);
}
Stmt::Case { stmt, .. } | Stmt::Default { stmt } => {
self.check_exprs_in_stmt(stmt, state);
}
_ => {}
}
}
fn check_expr_recursive(&mut self, expr: &Expr, state: &X86ProgramState) {
self.stats.exprs_checked += 1;
let checkers = self.registry.enabled_checkers();
for checker in &checkers {
let bugs = checker.check_expr(expr, state, &mut self.reporter);
self.stats.bugs_found += bugs.len();
}
match expr {
Expr::Binary(_, lhs, rhs) => {
self.check_expr_recursive(lhs, state);
self.check_expr_recursive(rhs, state);
}
Expr::Unary(_, inner) => {
self.check_expr_recursive(inner, state);
}
Expr::Call { callee, args } => {
self.check_expr_recursive(callee, state);
for arg in args {
self.check_expr_recursive(arg, state);
}
}
Expr::Subscript { base, index } => {
self.check_expr_recursive(base, state);
self.check_expr_recursive(index, state);
}
Expr::Member { base, .. } => {
self.check_expr_recursive(base, state);
}
Expr::Cast(_, inner) => {
self.check_expr_recursive(inner, state);
}
Expr::Conditional(cond, then_expr, else_expr) => {
self.check_expr_recursive(cond, state);
self.check_expr_recursive(then_expr, state);
self.check_expr_recursive(else_expr, state);
}
Expr::Assign(_, lhs, rhs) => {
self.check_expr_recursive(lhs, state);
self.check_expr_recursive(rhs, state);
}
Expr::IntLiteral(_)
| Expr::UIntLiteral(..)
| Expr::FloatLiteral(_)
| Expr::DoubleLiteral(_)
| Expr::CharLiteral(_)
| Expr::StringLiteral(_)
| Expr::Ident(_) => {}
_ => {}
}
}
fn analyze_var_decl(&mut self, var: &VarDecl) {
let checkers = self.registry.enabled_checkers();
let mut state = self.initial_state.clone();
for checker in &checkers {
let bugs = checker.check_var_decl(var, &mut state, &mut self.reporter);
self.stats.bugs_found += bugs.len();
}
}
fn stmt_label(stmt: &Stmt) -> String {
match stmt {
Stmt::Return(_) => "return".into(),
Stmt::If { .. } => "if".into(),
Stmt::While { .. } => "while".into(),
Stmt::For { .. } => "for".into(),
Stmt::Switch { .. } => "switch".into(),
Stmt::Expr(e) => format!("expr({})", expr_label(e)),
Stmt::Compound(_) => "compound".into(),
Stmt::Break => "break".into(),
Stmt::Continue => "continue".into(),
Stmt::Goto { .. } => "goto".into(),
Stmt::Label { name: n, .. } => format!("label:{n}"),
_ => "stmt".into(),
}
}
pub fn reporter(&self) -> &X86BugReporter {
&self.reporter
}
pub fn reporter_mut(&mut self) -> &mut X86BugReporter {
&mut self.reporter
}
pub fn summary(&self) -> String {
let mut s = String::new();
s.push_str("=== X86 Static Analyzer Summary ===\n");
s.push_str(&format!(
"Functions analyzed: {}\n",
self.stats.functions_analyzed
));
s.push_str(&format!(
"Statements visited: {}\n",
self.stats.stmts_visited
));
s.push_str(&format!(
"Expressions checked: {}\n",
self.stats.exprs_checked
));
s.push_str(&format!(
"Exploded graph nodes: {}\n",
self.graph.node_count()
));
s.push_str(&format!(
"Exploded graph edges: {}\n",
self.graph.edge_count()
));
s.push_str(&format!("Total bugs found: {}\n", self.stats.bugs_found));
s.push_str(&format!(
"Checkers enabled: {} / {}\n",
self.registry.enabled_count(),
self.registry.count()
));
s.push_str(&self.reporter.summary());
s
}
}
impl Default for X86StaticAnalyzer {
fn default() -> Self {
Self::with_defaults()
}
}
fn expr_label(expr: &Expr) -> String {
match expr {
Expr::Call { callee, .. } => {
if let Expr::Ident(name) = callee.as_ref() {
format!("call({name})")
} else {
"call(...)".into()
}
}
Expr::Binary(op, _, _) => format!("binary({})", op.as_str()),
Expr::Assign(_, _, _) => "assign".into(),
Expr::Ident(name) => name.clone(),
Expr::IntLiteral(v) => v.to_string(),
Expr::Unary(op, inner) => format!("unary({})", op.as_str()),
Expr::Cast(_, inner) => "cast".into(),
Expr::Subscript { .. } => "subscript".into(),
Expr::Member { .. } => "member".into(),
_ => "expr".into(),
}
}
#[derive(Debug, Clone, Default)]
pub struct X86CallGraph {
edges: HashMap<String, HashSet<String>>,
rev_edges: HashMap<String, HashSet<String>>,
}
impl X86CallGraph {
pub fn new() -> Self {
Self {
edges: HashMap::new(),
rev_edges: HashMap::new(),
}
}
pub fn add_edge(&mut self, caller: &str, callee: &str) {
self.edges
.entry(caller.to_string())
.or_default()
.insert(callee.to_string());
self.rev_edges
.entry(callee.to_string())
.or_default()
.insert(caller.to_string());
}
pub fn has_edge(&self, caller: &str, callee: &str) -> bool {
self.edges
.get(caller)
.map(|s| s.contains(callee))
.unwrap_or(false)
}
pub fn callees_of(&self, func: &str) -> Vec<String> {
self.edges
.get(func)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default()
}
pub fn callers_of(&self, func: &str) -> Vec<String> {
self.rev_edges
.get(func)
.map(|s| s.iter().cloned().collect())
.unwrap_or_default()
}
pub fn all_functions(&self) -> Vec<String> {
let mut names: HashSet<String> = HashSet::new();
for k in self.edges.keys() {
names.insert(k.clone());
}
for k in self.rev_edges.keys() {
names.insert(k.clone());
}
names.into_iter().collect()
}
pub fn topological_order(&self) -> Vec<String> {
let functions = self.all_functions();
let mut in_degree: HashMap<String, usize> = HashMap::new();
let mut result = Vec::new();
for f in &functions {
in_degree.insert(f.clone(), self.callers_of(f).len());
}
let mut queue: VecDeque<String> = functions
.iter()
.filter(|f| *in_degree.get(*f).unwrap_or(&0) == 0)
.cloned()
.collect();
while let Some(node) = queue.pop_front() {
result.push(node.clone());
for callee in self.callees_of(&node) {
if let Some(deg) = in_degree.get_mut(&callee) {
*deg = deg.saturating_sub(1);
if *deg == 0 {
queue.push_back(callee);
}
}
}
}
result
}
pub fn strongly_connected_components(&self) -> Vec<Vec<String>> {
let functions = self.all_functions();
let mut index = 0u32;
let mut indices: HashMap<String, u32> = HashMap::new();
let mut lowlink: HashMap<String, u32> = HashMap::new();
let mut on_stack: HashSet<String> = HashSet::new();
let mut stack: Vec<String> = Vec::new();
let mut sccs: Vec<Vec<String>> = Vec::new();
fn strongconnect(
v: &str,
edges: &HashMap<String, HashSet<String>>,
indices: &mut HashMap<String, u32>,
lowlink: &mut HashMap<String, u32>,
on_stack: &mut HashSet<String>,
stack: &mut Vec<String>,
sccs: &mut Vec<Vec<String>>,
index: &mut u32,
) {
indices.insert(v.to_string(), *index);
lowlink.insert(v.to_string(), *index);
*index += 1;
stack.push(v.to_string());
on_stack.insert(v.to_string());
if let Some(callees) = edges.get(v) {
for w in callees {
if !indices.contains_key(w) {
strongconnect(w, edges, indices, lowlink, on_stack, stack, sccs, index);
let w_low = *lowlink.get(w).unwrap();
let v_low = *lowlink.get(v).unwrap();
lowlink.insert(v.to_string(), w_low.min(v_low));
} else if on_stack.contains(w) {
let w_idx = *indices.get(w).unwrap();
let v_low = *lowlink.get(v).unwrap();
lowlink.insert(v.to_string(), w_idx.min(v_low));
}
}
}
if lowlink.get(v) == indices.get(v) {
let mut scc = Vec::new();
loop {
let w = stack.pop().unwrap();
on_stack.remove(&w);
scc.push(w.clone());
if w == v {
break;
}
}
sccs.push(scc);
}
}
for f in &functions {
if !indices.contains_key(f) {
strongconnect(
f,
&self.edges,
&mut indices,
&mut lowlink,
&mut on_stack,
&mut stack,
&mut sccs,
&mut index,
);
}
}
sccs
}
pub fn is_recursive(&self, func: &str) -> bool {
for scc in self.strongly_connected_components() {
if scc.len() > 1 && scc.contains(&func.to_string()) {
return true;
}
if scc.len() == 1 && scc[0] == func && self.has_edge(func, func) {
return true;
}
}
false
}
}
#[derive(Debug, Clone)]
pub struct X86FunctionSummary {
pub name: String,
pub allocates_region: bool,
pub deallocates_region: bool,
pub is_noreturn: bool,
pub acquires_locks: Vec<String>,
pub releases_locks: Vec<String>,
pub taint_source: bool,
pub taint_sink: bool,
}
impl X86FunctionSummary {
pub fn noop(name: impl Into<String>) -> Self {
Self {
name: name.into(),
allocates_region: false,
deallocates_region: false,
is_noreturn: false,
acquires_locks: vec![],
releases_locks: vec![],
taint_source: false,
taint_sink: false,
}
}
pub fn apply(&self, state: &X86ProgramState, region_id: u64) -> X86ProgramState {
let mut new_state = state.clone();
if self.allocates_region {
new_state = new_state.allocate(region_id);
}
if self.deallocates_region {
new_state = new_state.free(region_id);
}
for lock in &self.acquires_locks {
new_state = new_state.acquire_lock(lock);
}
for lock in &self.releases_locks {
new_state = new_state.release_lock(lock);
}
if self.is_noreturn {
new_state = new_state.mark_dead();
}
new_state
}
pub fn malloc_summary() -> Self {
Self {
name: "malloc".into(),
allocates_region: true,
deallocates_region: false,
is_noreturn: false,
acquires_locks: vec![],
releases_locks: vec![],
taint_source: false,
taint_sink: false,
}
}
pub fn free_summary() -> Self {
Self {
name: "free".into(),
allocates_region: false,
deallocates_region: true,
is_noreturn: false,
acquires_locks: vec![],
releases_locks: vec![],
taint_source: false,
taint_sink: false,
}
}
pub fn noreturn_summary(name: impl Into<String>) -> Self {
Self {
name: name.into(),
allocates_region: false,
deallocates_region: false,
is_noreturn: true,
acquires_locks: vec![],
releases_locks: vec![],
taint_source: false,
taint_sink: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum X86PathConstraint {
Comparison {
lhs: String,
op: ComparisonOp,
rhs: i64,
},
Range { var: String, low: i64, high: i64 },
NonNull(String),
MustBeNull(String),
IsTainted(String),
NotTainted(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComparisonOp {
LessThan,
LessOrEqual,
GreaterThan,
GreaterOrEqual,
Equal,
NotEqual,
}
impl ComparisonOp {
pub fn from_binary_op(op: BinaryOp) -> Option<Self> {
match op {
BinaryOp::Lt => Some(ComparisonOp::LessThan),
BinaryOp::Le => Some(ComparisonOp::LessOrEqual),
BinaryOp::Gt => Some(ComparisonOp::GreaterThan),
BinaryOp::Ge => Some(ComparisonOp::GreaterOrEqual),
BinaryOp::Eq => Some(ComparisonOp::Equal),
BinaryOp::Ne => Some(ComparisonOp::NotEqual),
_ => None,
}
}
pub fn negate(self) -> Self {
match self {
ComparisonOp::LessThan => ComparisonOp::GreaterOrEqual,
ComparisonOp::LessOrEqual => ComparisonOp::GreaterThan,
ComparisonOp::GreaterThan => ComparisonOp::LessOrEqual,
ComparisonOp::GreaterOrEqual => ComparisonOp::LessThan,
ComparisonOp::Equal => ComparisonOp::NotEqual,
ComparisonOp::NotEqual => ComparisonOp::Equal,
}
}
pub fn as_str(&self) -> &'static str {
match self {
ComparisonOp::LessThan => "<",
ComparisonOp::LessOrEqual => "<=",
ComparisonOp::GreaterThan => ">",
ComparisonOp::GreaterOrEqual => ">=",
ComparisonOp::Equal => "==",
ComparisonOp::NotEqual => "!=",
}
}
}
impl fmt::Display for ComparisonOp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl X86PathConstraint {
pub fn is_satisfiable(&self) -> bool {
match self {
X86PathConstraint::Range { low, high, .. } => low <= high,
_ => true,
}
}
pub fn variable(&self) -> &str {
match self {
X86PathConstraint::Comparison { lhs, .. } => lhs,
X86PathConstraint::Range { var, .. } => var,
X86PathConstraint::NonNull(v) => v,
X86PathConstraint::MustBeNull(v) => v,
X86PathConstraint::IsTainted(v) => v,
X86PathConstraint::NotTainted(v) => v,
}
}
pub fn negate(&self) -> Self {
match self {
X86PathConstraint::Comparison { lhs, op, rhs } => X86PathConstraint::Comparison {
lhs: lhs.clone(),
op: op.negate(),
rhs: *rhs,
},
X86PathConstraint::NonNull(v) => X86PathConstraint::MustBeNull(v.clone()),
X86PathConstraint::MustBeNull(v) => X86PathConstraint::NonNull(v.clone()),
X86PathConstraint::IsTainted(v) => X86PathConstraint::NotTainted(v.clone()),
X86PathConstraint::NotTainted(v) => X86PathConstraint::IsTainted(v.clone()),
other => other.clone(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86NoneConstraint {
MustBeNull,
MustNotBeNull,
MayBeNull,
}
#[derive(Debug, Clone, Default)]
pub struct X86ConstraintSet {
ranges: HashMap<String, (i64, i64)>,
equalities: HashMap<String, i64>,
nullability: HashMap<String, Vec<X86NoneConstraint>>,
tainted: HashSet<String>,
taint_free: HashSet<String>,
}
impl X86ConstraintSet {
pub fn new() -> Self {
Self::default()
}
pub fn set_range(&mut self, var: &str, low: i64, high: i64) {
if let Some(&(existing_low, existing_high)) = self.ranges.get(var) {
let new_low = existing_low.max(low);
let new_high = existing_high.min(high);
if new_low <= new_high {
self.ranges.insert(var.to_string(), (new_low, new_high));
} else {
self.ranges.insert(var.to_string(), (new_low, new_high));
}
} else {
self.ranges.insert(var.to_string(), (low, high));
}
}
pub fn get_range(&self, var: &str) -> Option<(i64, i64)> {
self.ranges.get(var).copied()
}
pub fn set_equal(&mut self, var: &str, value: i64) {
self.equalities.insert(var.to_string(), value);
self.set_range(var, value, value);
}
pub fn get_equal(&self, var: &str) -> Option<i64> {
self.equalities.get(var).copied()
}
pub fn add_none_constraint(&mut self, var: &str, constraint: X86NoneConstraint) {
self.nullability
.entry(var.to_string())
.or_default()
.push(constraint);
}
pub fn get_none_constraints(&self, var: &str) -> Vec<X86NoneConstraint> {
self.nullability.get(var).cloned().unwrap_or_default()
}
pub fn is_null(&self, var: &str) -> bool {
self.nullability
.get(var)
.map(|v| v.iter().any(|c| *c == X86NoneConstraint::MustBeNull))
.unwrap_or(false)
}
pub fn is_non_null(&self, var: &str) -> bool {
self.nullability
.get(var)
.map(|v| v.iter().any(|c| *c == X86NoneConstraint::MustNotBeNull))
.unwrap_or(false)
}
pub fn set_tainted(&mut self, var: &str) {
self.tainted.insert(var.to_string());
self.taint_free.remove(var);
}
pub fn set_taint_free(&mut self, var: &str) {
self.taint_free.insert(var.to_string());
self.tainted.remove(var);
}
pub fn is_tainted(&self, var: &str) -> bool {
self.tainted.contains(var)
}
pub fn merge(&self, other: &Self) -> Self {
let mut merged = Self::new();
for (var, &(l1, h1)) in &self.ranges {
if let Some(&(l2, h2)) = other.ranges.get(var) {
let low = l1.max(l2);
let high = h1.min(h2);
if low <= high {
merged.ranges.insert(var.clone(), (low, high));
}
}
}
for (var, v1) in &self.equalities {
if other.equalities.get(var) == Some(v1) {
merged.equalities.insert(var.clone(), *v1);
}
}
for t in &self.tainted {
merged.tainted.insert(t.clone());
}
for t in &other.tainted {
merged.tainted.insert(t.clone());
}
merged
}
}
#[derive(Debug, Clone)]
pub struct X86AnalysisConfig {
pub max_depth: usize,
pub max_nodes: usize,
pub max_inline_depth: usize,
pub interprocedural: bool,
pub use_summaries: bool,
}
impl Default for X86AnalysisConfig {
fn default() -> Self {
Self {
max_depth: 256,
max_nodes: 100_000,
max_inline_depth: 3,
interprocedural: false,
use_summaries: true,
}
}
}
pub struct X86AnalysisContext {
pub subtarget: Option<X86Subtarget>,
pub call_graph: X86CallGraph,
pub summaries: HashMap<String, X86FunctionSummary>,
functions: HashMap<String, FunctionDecl>,
pub config: X86AnalysisConfig,
pub memory_layout: X86MemoryLayout,
pub type_sizes: HashMap<String, u64>,
}
#[derive(Debug, Clone)]
pub struct X86MemoryLayout {
pub pointer_size: u8,
pub size_t_size: u8,
pub wchar_size: u8,
pub page_size: u64,
pub stack_alignment: u32,
pub heap_alignment: u32,
pub is_64bit: bool,
pub is_little_endian: bool,
pub null_page_range: (u64, u64),
pub canonical_low: u64,
pub canonical_high: u64,
}
impl Default for X86MemoryLayout {
fn default() -> Self {
Self {
pointer_size: 8,
size_t_size: 8,
wchar_size: 4,
page_size: 4096,
stack_alignment: 16,
heap_alignment: 16,
is_64bit: true,
is_little_endian: true,
null_page_range: (0, 4096),
canonical_low: 0x0000_7FFF_FFFF_FFFF,
canonical_high: 0xFFFF_8000_0000_0000,
}
}
}
impl X86MemoryLayout {
pub fn x86_64() -> Self {
Self::default()
}
pub fn x86_32() -> Self {
Self {
pointer_size: 4,
size_t_size: 4,
wchar_size: 4,
page_size: 4096,
stack_alignment: 16,
heap_alignment: 16,
is_64bit: false,
is_little_endian: true,
null_page_range: (0, 4096),
canonical_low: 0xFFFF_FFFF,
canonical_high: 0xFFFF_FFFF,
}
}
pub fn is_in_null_page(&self, addr: u64) -> bool {
addr >= self.null_page_range.0 && addr < self.null_page_range.1
}
pub fn is_canonical(&self, addr: u64) -> bool {
if !self.is_64bit {
return true;
}
addr <= self.canonical_low || addr >= self.canonical_high
}
}
#[derive(Debug, Clone, Default)]
pub struct X86TypeSizeCache {
sizes: HashMap<String, u64>,
alignments: HashMap<String, u32>,
}
impl X86TypeSizeCache {
pub fn x86_64_default() -> Self {
let mut cache = Self::default();
cache.sizes.insert("char".into(), 1);
cache.sizes.insert("short".into(), 2);
cache.sizes.insert("int".into(), 4);
cache.sizes.insert("long".into(), 8);
cache.sizes.insert("long long".into(), 8);
cache.sizes.insert("float".into(), 4);
cache.sizes.insert("double".into(), 8);
cache.sizes.insert("long double".into(), 16);
cache.sizes.insert("void*".into(), 8);
cache.sizes.insert("size_t".into(), 8);
cache.sizes.insert("ptrdiff_t".into(), 8);
cache.sizes.insert("wchar_t".into(), 4);
cache.alignments.insert("char".into(), 1);
cache.alignments.insert("short".into(), 2);
cache.alignments.insert("int".into(), 4);
cache.alignments.insert("long".into(), 8);
cache.alignments.insert("long long".into(), 8);
cache.alignments.insert("float".into(), 4);
cache.alignments.insert("double".into(), 8);
cache.alignments.insert("long double".into(), 16);
cache.alignments.insert("void*".into(), 8);
cache
}
pub fn size_of(&self, ty: &str) -> Option<u64> {
self.sizes.get(ty).copied()
}
pub fn align_of(&self, ty: &str) -> Option<u32> {
self.alignments.get(ty).copied()
}
pub fn register(&mut self, ty: &str, size: u64, align: u32) {
self.sizes.insert(ty.to_string(), size);
self.alignments.insert(ty.to_string(), align);
}
}
#[derive(Debug, Clone)]
pub struct X86AnalysisResult {
pub reports: Vec<X86BugReport>,
pub stats: X86AnalyzerStats,
pub graph_summary: X86GraphSummary,
pub call_graph: Option<X86CallGraph>,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Default)]
pub struct X86GraphSummary {
pub total_nodes: usize,
pub total_edges: usize,
pub max_depth: u32,
pub feasible_paths: usize,
pub infeasible_paths: usize,
pub dead_ends: usize,
}
impl X86AnalysisResult {
pub fn new() -> Self {
Self {
reports: Vec::new(),
stats: X86AnalyzerStats::default(),
graph_summary: X86GraphSummary::default(),
call_graph: None,
duration_ms: 0,
}
}
pub fn has_critical(&self) -> bool {
self.reports
.iter()
.any(|r| r.severity == BugSeverity::Critical)
}
pub fn has_errors(&self) -> bool {
self.reports
.iter()
.any(|r| r.severity >= BugSeverity::Error)
}
pub fn count_by_severity(&self, severity: BugSeverity) -> usize {
self.reports
.iter()
.filter(|r| r.severity == severity)
.count()
}
pub fn summary_string(&self) -> String {
let mut s = String::new();
s.push_str("╔══════════════════════════════════════════╗\n");
s.push_str("║ X86 Static Analyzer — Analysis Report ║\n");
s.push_str("╠══════════════════════════════════════════╣\n");
s.push_str(&format!(
"║ Functions: {:<30}║\n",
self.stats.functions_analyzed
));
s.push_str(&format!(
"║ Statements: {:<29}║\n",
self.stats.stmts_visited
));
s.push_str(&format!(
"║ Expressions: {:<28}║\n",
self.stats.exprs_checked
));
s.push_str(&format!(
"║ Graph nodes: {:<28}║\n",
self.graph_summary.total_nodes
));
s.push_str("╠══════════════════════════════════════════╣\n");
s.push_str(&format!(
"║ Critical: {:<30}║\n",
self.count_by_severity(BugSeverity::Critical)
));
s.push_str(&format!(
"║ Errors: {:<30}║\n",
self.count_by_severity(BugSeverity::Error)
));
s.push_str(&format!(
"║ Warnings: {:<30}║\n",
self.count_by_severity(BugSeverity::Warning)
));
s.push_str(&format!(
"║ Style: {:<30}║\n",
self.count_by_severity(BugSeverity::Style)
));
s.push_str(&format!(
"║ Notes: {:<30}║\n",
self.count_by_severity(BugSeverity::Note)
));
s.push_str("╚══════════════════════════════════════════╝\n");
s
}
}
impl Default for X86AnalysisResult {
fn default() -> Self {
Self::new()
}
}
impl X86StaticAnalyzer {
pub fn analyze_with_result(&mut self, tu: &TranslationUnit) -> X86AnalysisResult {
let start = std::time::Instant::now();
self.analyze(tu);
let graph_summary = X86GraphSummary {
total_nodes: self.graph.node_count(),
total_edges: self.graph.edge_count(),
max_depth: self.stats.stmts_visited as u32,
feasible_paths: self.graph.node_count(),
infeasible_paths: 0,
dead_ends: 0,
};
X86AnalysisResult {
reports: self.reporter.reports.clone(),
stats: self.stats.clone(),
graph_summary,
call_graph: None,
duration_ms: start.elapsed().as_millis() as u64,
}
}
pub fn is_checker_enabled(&self, name: &str) -> bool {
self.registry.is_enabled(name)
}
pub fn enable_checker(&mut self, name: &str) {
self.registry.enable(name);
}
pub fn disable_checker(&mut self, name: &str) {
self.registry.disable(name);
}
pub fn enabled_checker_names(&self) -> Vec<&str> {
self.registry
.enabled_checkers()
.iter()
.map(|c| c.name())
.collect()
}
pub fn set_max_depth(&mut self, depth: u32) {
self.graph.max_depth = depth;
}
pub fn set_max_nodes(&mut self, max_nodes: usize) {
self.max_nodes = max_nodes;
}
}
pub fn extract_calls_from_stmt(stmt: &Stmt) -> Vec<String> {
let mut calls = Vec::new();
match stmt {
Stmt::Expr(expr) | Stmt::Return(Some(expr)) => {
extract_calls_from_expr(expr, &mut calls);
}
Stmt::If { cond, .. } => {
extract_calls_from_expr(cond, &mut calls);
}
Stmt::While { cond, .. } => {
extract_calls_from_expr(cond, &mut calls);
}
Stmt::For {
init, cond, incr, ..
} => {
if let Some(init_stmt) = init.as_ref() {
if let Stmt::Expr(e) = init_stmt.as_ref() {
extract_calls_from_expr(e, &mut calls);
}
}
if let Some(e) = cond.as_ref() {
extract_calls_from_expr(e, &mut calls);
}
if let Some(e) = incr.as_ref() {
extract_calls_from_expr(e, &mut calls);
}
}
Stmt::Switch { expr, .. } => {
extract_calls_from_expr(expr, &mut calls);
}
_ => {}
}
calls
}
fn extract_calls_from_expr(expr: &Expr, calls: &mut Vec<String>) {
match expr {
Expr::Call { callee, args } => {
if let Expr::Ident(name) = callee.as_ref() {
calls.push(name.clone());
}
extract_calls_from_expr(callee, calls);
for arg in args {
extract_calls_from_expr(arg, calls);
}
}
Expr::Binary(_, lhs, rhs) => {
extract_calls_from_expr(lhs, calls);
extract_calls_from_expr(rhs, calls);
}
Expr::Unary(_, inner) => extract_calls_from_expr(inner, calls),
Expr::Cast(_, inner) => extract_calls_from_expr(inner, calls),
Expr::Assign(_, lhs, rhs) => {
extract_calls_from_expr(lhs, calls);
extract_calls_from_expr(rhs, calls);
}
Expr::Subscript { base, index } => {
extract_calls_from_expr(base, calls);
extract_calls_from_expr(index, calls);
}
Expr::Member { base, .. } => extract_calls_from_expr(base, calls),
Expr::Conditional(cond, t, e) => {
extract_calls_from_expr(cond, calls);
extract_calls_from_expr(t, calls);
extract_calls_from_expr(e, calls);
}
_ => {}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86RegisterClass {
GP64,
GP32,
GP16,
GP8,
XMM,
YMM,
ZMM,
FP87,
MM,
Segment,
Flags,
}
impl X86RegisterClass {
pub fn size_bytes(&self) -> u32 {
match self {
X86RegisterClass::GP64 => 8,
X86RegisterClass::GP32 => 4,
X86RegisterClass::GP16 => 2,
X86RegisterClass::GP8 => 1,
X86RegisterClass::XMM => 16,
X86RegisterClass::YMM => 32,
X86RegisterClass::ZMM => 64,
X86RegisterClass::FP87 => 10,
X86RegisterClass::MM => 8,
X86RegisterClass::Segment => 2,
X86RegisterClass::Flags => 8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum X86CallingConventionSummary {
SystemV,
MicrosoftX64,
Cdecl,
StdCall,
FastCall,
ThisCall,
VectorCall,
}
impl X86CallingConventionSummary {
pub fn int_param_regs(&self) -> usize {
match self {
X86CallingConventionSummary::SystemV => 6,
X86CallingConventionSummary::MicrosoftX64 => 4,
X86CallingConventionSummary::FastCall => 2,
X86CallingConventionSummary::VectorCall => 6,
_ => 0,
}
}
pub fn is_callee_cleanup(&self) -> bool {
matches!(
self,
X86CallingConventionSummary::StdCall
| X86CallingConventionSummary::ThisCall
| X86CallingConventionSummary::MicrosoftX64
)
}
pub fn return_reg(&self) -> &'static str {
match self {
X86CallingConventionSummary::SystemV
| X86CallingConventionSummary::MicrosoftX64
| X86CallingConventionSummary::Cdecl
| X86CallingConventionSummary::StdCall
| X86CallingConventionSummary::FastCall
| X86CallingConventionSummary::ThisCall
| X86CallingConventionSummary::VectorCall => "RAX/EAX/AX/AL",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mem_region_stack_creation() {
let region = X86MemRegion::new_stack(1, "my_var", Some(QualType::int()));
assert_eq!(region.id, 1);
assert_eq!(region.name, "my_var");
assert_eq!(region.kind, X86MemRegionKind::Stack);
assert!(region.is_stack());
assert!(!region.is_heap());
assert_eq!(region.segment, Some(X86Segment::SS));
}
#[test]
fn test_mem_region_heap_creation() {
let region = X86MemRegion::new_heap(2, "malloc_buf", 1024);
assert!(region.is_heap());
assert_eq!(region.size_bytes, 1024);
assert!(!region.is_read_only);
}
#[test]
fn test_mem_region_global_creation() {
let region = X86MemRegion::new_global(3, "g_counter", Some(QualType::int()));
assert!(region.is_global());
assert!(!region.is_stack());
}
#[test]
fn test_mem_region_string_literal() {
let region = X86MemRegion::new_string_literal(4);
assert_eq!(region.kind, X86MemRegionKind::StringLiteral);
assert!(region.is_read_only);
}
#[test]
fn test_mem_region_element_sub_region() {
let parent = X86MemRegion::new_stack(10, "arr", Some(QualType::int()));
let elem = X86MemRegion::new_element(parent.clone(), 3, Some(QualType::int()));
assert_eq!(elem.kind, X86MemRegionKind::Element);
assert_eq!(elem.element_idx, Some(3));
assert!(elem.is_sub_region());
assert_eq!(elem.root_region().id, parent.id);
}
#[test]
fn test_mem_region_field_sub_region() {
let parent = X86MemRegion::new_stack(20, "my_struct", None);
let field = X86MemRegion::new_field(parent.clone(), 0, "x", Some(QualType::int()));
assert_eq!(field.kind, X86MemRegionKind::Field);
assert_eq!(field.field_idx, Some(0));
assert!(field.is_sub_region());
}
#[test]
fn test_mem_region_may_alias_same_root() {
let r1 = X86MemRegion::new_stack(100, "a", None);
let r2 = X86MemRegion::new_element(r1.clone(), 0, None);
let r3 = X86MemRegion::new_element(r1.clone(), 1, None);
assert!(r2.may_alias(&r3));
}
#[test]
fn test_mem_region_may_alias_different_root() {
let r1 = X86MemRegion::new_stack(100, "a", None);
let r2 = X86MemRegion::new_stack(200, "b", None);
assert!(!r1.may_alias(&r2));
}
#[test]
fn test_mem_region_may_alias_unknown() {
let r1 = X86MemRegion::new_unknown(0);
let r2 = X86MemRegion::new_stack(100, "a", None);
assert!(r1.may_alias(&r2)); assert!(r2.may_alias(&r1));
}
#[test]
fn test_mem_region_x86_size_no_type() {
let r = X86MemRegion::new_stack(1, "x", None);
assert_eq!(r.x86_size(), 0);
}
#[test]
fn test_mem_region_x86_size_with_size() {
let r = X86MemRegion::new_heap(2, "buf", 4096);
assert_eq!(r.x86_size(), 4096);
}
#[test]
fn test_sval_is_undefined() {
assert!(SVal::Undefined.is_undefined());
assert!(!SVal::Unknown.is_undefined());
assert!(!SVal::ConcreteInt(42).is_undefined());
}
#[test]
fn test_sval_is_null() {
assert!(SVal::Null.is_null());
assert!(!SVal::ConcreteInt(0).is_null());
}
#[test]
fn test_sval_is_zero() {
assert!(SVal::Zero.is_zero());
assert!(SVal::ConcreteInt(0).is_zero());
assert!(SVal::ConcreteUInt(0).is_zero());
assert!(!SVal::ConcreteInt(1).is_zero());
}
#[test]
fn test_sval_is_concrete() {
assert!(SVal::ConcreteInt(5).is_concrete());
assert!(SVal::ConcreteUInt(5).is_concrete());
assert!(!SVal::Unknown.is_concrete());
}
#[test]
fn test_sval_as_int() {
assert_eq!(SVal::ConcreteInt(42).as_int(), Some(42));
assert_eq!(SVal::ConcreteUInt(100).as_int(), Some(100));
assert_eq!(SVal::Unknown.as_int(), None);
}
#[test]
fn test_sval_as_uint() {
assert_eq!(SVal::ConcreteUInt(42).as_uint(), Some(42));
assert_eq!(SVal::ConcreteInt(-1).as_uint(), None);
}
#[test]
fn test_float_val_f32() {
let f = FloatVal::f32(0x3f800000); assert_eq!(f.width, 32);
assert!(!f.is_nan());
assert!(!f.is_infinity());
assert!(!f.is_zero());
}
#[test]
fn test_float_val_f64_zero() {
let f = FloatVal::f64(0);
assert!(f.is_zero());
assert_eq!(f.width, 64);
}
#[test]
fn test_float_val_nan() {
let f = FloatVal::f32(0x7fc00000u32); assert!(f.is_nan());
}
#[test]
fn test_float_val_inf() {
let f = FloatVal::f32(0x7f800000u32); assert!(f.is_infinity());
}
#[test]
fn test_program_state_new() {
let state = X86ProgramState::new();
assert!(!state.infeasible);
assert!(!state.is_dead);
assert_eq!(state.values.len(), 0);
assert_eq!(state.held_locks.len(), 0);
}
#[test]
fn test_program_state_bind_lookup() {
let state = X86ProgramState::new();
let state = state.bind_value("x", SVal::ConcreteInt(42));
assert_eq!(state.lookup_value("x"), SVal::ConcreteInt(42));
assert_eq!(state.lookup_value("y"), SVal::Unknown);
}
#[test]
fn test_program_state_allocate_free() {
let state = X86ProgramState::new();
let state = state.allocate(100);
assert!(state.is_allocated(100));
assert!(!state.is_freed(100));
let state = state.free(100);
assert!(!state.is_allocated(100));
assert!(state.is_freed(100));
}
#[test]
fn test_program_state_lock_acquire_release() {
let state = X86ProgramState::new();
let state = state.acquire_lock("mtx");
assert!(state.lock_held("mtx"));
let state = state.release_lock("mtx");
assert!(!state.lock_held("mtx"));
}
#[test]
fn test_program_state_taint() {
let state = X86ProgramState::new();
let state = state.taint("user_input");
assert!(state.is_tainted("user_input"));
assert!(!state.is_tainted("safe_var"));
}
#[test]
fn test_program_state_retain_count() {
let state = X86ProgramState::new();
let state = state.allocate(200);
let state = state.retain(200);
assert_eq!(state.retain_count(200), 1);
let state = state.retain(200);
assert_eq!(state.retain_count(200), 2);
let state = state.release(200);
assert_eq!(state.retain_count(200), 1);
}
#[test]
fn test_program_state_merge_values_agree() {
let s1 = X86ProgramState::new().bind_value("x", SVal::ConcreteInt(1));
let s2 = X86ProgramState::new().bind_value("x", SVal::ConcreteInt(1));
let merged = s1.merge(&s2);
assert_eq!(merged.lookup_value("x"), SVal::ConcreteInt(1));
}
#[test]
fn test_program_state_merge_values_disagree() {
let s1 = X86ProgramState::new().bind_value("x", SVal::ConcreteInt(1));
let s2 = X86ProgramState::new().bind_value("x", SVal::ConcreteInt(2));
let merged = s1.merge(&s2);
assert_eq!(merged.lookup_value("x"), SVal::Unknown);
}
#[test]
fn test_program_state_constraints() {
let state = X86ProgramState::new();
let state = state.assume_true("x_gt_0");
assert!(state.is_constrained_true("x_gt_0"));
assert!(!state.is_constrained_false("x_gt_0"));
}
#[test]
fn test_program_state_advance() {
let state = X86ProgramState::new();
let advanced = state.advance();
assert_eq!(advanced.stmt_idx, 1);
assert_eq!(advanced.depth, 1);
}
#[test]
fn test_exploded_graph_create_root() {
let mut graph = X86ExplodedGraph::new();
let state = X86ProgramState::new();
let root = graph.create_root(state);
assert_eq!(root, 0);
assert!(graph.root_id.is_some());
assert_eq!(graph.node_count(), 1);
assert!(!graph.is_worklist_empty());
}
#[test]
fn test_exploded_graph_add_node() {
let mut graph = X86ExplodedGraph::new();
let state = X86ProgramState::new();
let root = graph.create_root(state.clone());
let n1 = graph.add_node(root, "stmt1", state.clone(), X86EdgeKind::Fallthrough, "");
assert_eq!(n1, 1);
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
}
#[test]
fn test_exploded_graph_successors_predecessors() {
let mut graph = X86ExplodedGraph::new();
let state = X86ProgramState::new();
let root = graph.create_root(state.clone());
let n1 = graph.add_node(root, "a", state.clone(), X86EdgeKind::Fallthrough, "");
let n2 = graph.add_node(root, "b", state.clone(), X86EdgeKind::TrueBranch, "cond");
let succ = graph.successors(root);
assert_eq!(succ.len(), 2);
assert!(succ.contains(&n1));
assert!(succ.contains(&n2));
let pred = graph.predecessors(n1);
assert_eq!(pred.len(), 1);
assert_eq!(pred[0], root);
}
#[test]
fn test_exploded_graph_infeasible_path() {
let mut graph = X86ExplodedGraph::new();
let state = X86ProgramState::new();
let root = graph.create_root(state);
let dead_state = state.mark_infeasible();
let n = graph.add_node(root, "dead", dead_state, X86EdgeKind::Fallthrough, "");
assert_eq!(n, 0); }
#[test]
fn test_exploded_graph_max_depth() {
let mut graph = X86ExplodedGraph::new();
graph.max_depth = 5;
let state = X86ProgramState::new();
let root = graph.create_root(state.clone());
let mut current = root;
for _ in 0..10 {
let advanced = state.advance();
let n = graph.add_node(current, "step", advanced, X86EdgeKind::Fallthrough, "");
if n == 0 {
break;
}
current = n;
}
assert!(graph.node_count() <= 7);
}
#[test]
fn test_bug_report_creation() {
let report = X86BugReport::new(
"test.Checker",
BugCategory::NullDereference,
BugSeverity::Error,
"Null deref",
"Dereferencing a null pointer",
"test.c",
42,
10,
);
assert_eq!(report.checker_name, "test.Checker");
assert_eq!(report.severity, BugSeverity::Error);
assert_eq!(report.title, "Null deref");
assert_eq!(report.line, 42);
assert!(!report.is_duplicate);
}
#[test]
fn test_bug_report_with_function() {
let report = X86BugReport::new(
"core.Test",
BugCategory::General,
BugSeverity::Warning,
"Test",
"Desc",
"f.c",
1,
1,
)
.with_function("main");
assert_eq!(report.function, Some("main".into()));
}
#[test]
fn test_bug_report_path_pieces() {
let mut report = X86BugReport::new(
"core.Test",
BugCategory::General,
BugSeverity::Warning,
"Test",
"Desc",
"f.c",
1,
1,
);
report.add_note("Assigned null", "f.c", 5, 10);
report.add_bug_point("Deref here", "f.c", 7, 3);
assert_eq!(report.path.len(), 2);
assert!(report.path[1].is_bug_point);
assert!(!report.path[0].is_bug_point);
}
#[test]
fn test_bug_report_display() {
let report = X86BugReport::new(
"core.NullDeref",
BugCategory::NullDereference,
BugSeverity::Error,
"Null deref",
"desc",
"test.c",
10,
5,
)
.with_function("foo");
let display = format!("{report}");
assert!(display.contains("error"));
assert!(display.contains("Null deref"));
assert!(display.contains("test.c:10:5"));
assert!(display.contains("foo"));
}
#[test]
fn test_bug_reporter_emit_deduplicate() {
let mut reporter = X86BugReporter::new();
let r1 = X86BugReport::new(
"core.A",
BugCategory::General,
BugSeverity::Warning,
"Bug",
"desc",
"f.c",
1,
1,
);
let r2 = X86BugReport::new(
"core.A",
BugCategory::General,
BugSeverity::Warning,
"Bug",
"desc",
"f.c",
1,
1,
);
assert!(reporter.emit(r1));
assert!(!reporter.emit(r2)); assert_eq!(reporter.report_count(), 1);
}
#[test]
fn test_bug_reporter_emit_simple() {
let mut reporter = X86BugReporter::new();
assert!(reporter.emit_simple(
"core.C",
BugCategory::Memory,
BugSeverity::Error,
"Leak",
"desc",
"f.c",
5,
3
));
assert_eq!(reporter.report_count(), 1);
}
#[test]
fn test_bug_reporter_sorted_reports() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"A",
BugCategory::General,
BugSeverity::Warning,
"w",
"d",
"b.c",
2,
1,
);
reporter.emit_simple(
"B",
BugCategory::General,
BugSeverity::Error,
"e",
"d",
"a.c",
1,
1,
);
let sorted = reporter.sorted_reports();
assert_eq!(sorted[0].severity, BugSeverity::Error);
}
#[test]
fn test_bug_reporter_by_severity() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"A",
BugCategory::General,
BugSeverity::Error,
"e",
"d",
"f.c",
1,
1,
);
reporter.emit_simple(
"B",
BugCategory::General,
BugSeverity::Warning,
"w",
"d",
"f.c",
2,
1,
);
assert_eq!(reporter.reports_by_severity(BugSeverity::Error).len(), 1);
assert_eq!(reporter.reports_by_severity(BugSeverity::Critical).len(), 0);
}
#[test]
fn test_bug_reporter_summary() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"A",
BugCategory::NullDereference,
BugSeverity::Error,
"e",
"d",
"f.c",
1,
1,
);
let summary = reporter.summary();
assert!(summary.contains("Total reports: 1"));
}
#[test]
fn test_null_deref_detection() {
let checker = X86NullDereferenceChecker::new();
let state = X86ProgramState::new().bind_value("p", SVal::Null);
let mut reporter = X86BugReporter::new();
let deref = Expr::unary(UnaryOp::Deref, Expr::ident("p"));
let bugs = checker.check_expr(&deref, &state, &mut reporter);
assert!(!bugs.is_empty());
assert_eq!(bugs[0].severity, BugSeverity::Error);
}
#[test]
fn test_null_deref_no_bug_on_non_null() {
let checker = X86NullDereferenceChecker::new();
let state = X86ProgramState::new().bind_value("p", SVal::Loc(100));
let mut reporter = X86BugReporter::new();
let deref = Expr::unary(UnaryOp::Deref, Expr::ident("p"));
let bugs = checker.check_expr(&deref, &state, &mut reporter);
assert!(bugs.is_empty());
}
#[test]
fn test_null_deref_int_zero_is_null() {
let checker = X86NullDereferenceChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let cast = Expr::Cast(
QualType::pointer_to(QualType::int()),
Box::new(Expr::IntLiteral(0)),
);
let deref = Expr::unary(UnaryOp::Deref, cast);
let bugs = checker.check_expr(&deref, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_div_by_zero_in_binary() {
let checker = X86UndefinedBinaryOperatorChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Div, Expr::IntLiteral(10), Expr::IntLiteral(0));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_negative_shift() {
let checker = X86UndefinedBinaryOperatorChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Shl, Expr::IntLiteral(1), Expr::IntLiteral(-1));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_excessive_shift() {
let checker = X86UndefinedBinaryOperatorChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Shr, Expr::IntLiteral(1), Expr::IntLiteral(64));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_signed_overflow_add() {
let checker = X86UndefinedBinaryOperatorChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(
BinaryOp::Add,
Expr::IntLiteral(i64::MAX),
Expr::IntLiteral(1),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("overflow"));
}
#[test]
fn test_no_overflow_on_safe_add() {
let checker = X86UndefinedBinaryOperatorChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Add, Expr::IntLiteral(1), Expr::IntLiteral(2));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(bugs.is_empty());
}
#[test]
fn test_div_by_zero_dedicated() {
let checker = X86DivideByZeroChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Div, Expr::IntLiteral(5), Expr::IntLiteral(0));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
assert_eq!(bugs[0].severity, BugSeverity::Error);
}
#[test]
fn test_return_undefined() {
let checker = X86ReturnUndefinedChecker::new();
let state = X86ProgramState::new().bind_value("x", SVal::Undefined);
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let ret_stmt = Stmt::return_(Some(Expr::ident("x")));
let (_, bugs) = checker.check_stmt(&ret_stmt, &mut graph, root, &state, &mut reporter);
assert!(!bugs.is_empty());
assert_eq!(bugs[0].title, "Return of undefined value");
}
#[test]
fn test_array_bounds_oob() {
let mut checker = X86ArrayBoundsChecker::new();
checker.register_array("arr", 10);
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let subscript = Expr::Subscript {
base: Box::new(Expr::ident("arr")),
index: Box::new(Expr::IntLiteral(15)),
};
let bugs = checker.check_expr(&subscript, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_array_bounds_in_bounds() {
let mut checker = X86ArrayBoundsChecker::new();
checker.register_array("arr", 10);
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let subscript = Expr::Subscript {
base: Box::new(Expr::ident("arr")),
index: Box::new(Expr::IntLiteral(5)),
};
let bugs = checker.check_expr(&subscript, &state, &mut reporter);
assert!(bugs.is_empty());
}
#[test]
fn test_array_bounds_negative() {
let mut checker = X86ArrayBoundsChecker::new();
checker.register_array("arr", 10);
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let subscript = Expr::Subscript {
base: Box::new(Expr::ident("arr")),
index: Box::new(Expr::IntLiteral(-1)),
};
let bugs = checker.check_expr(&subscript, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_stack_addr_return() {
let checker = X86StackAddrEscapeChecker::new();
let mut graph = X86ExplodedGraph::new();
let mut reporter = X86BugReporter::new();
let stack_region = X86MemRegion::new_stack(1, "local", Some(QualType::int()));
let state = X86ProgramState::new().register_var_region("local", stack_region.clone());
let root = graph.create_root(state.clone());
let ret_expr = Expr::unary(UnaryOp::AddrOf, Expr::ident("local"));
let ret_stmt = Stmt::return_(Some(ret_expr));
let (_, bugs) = checker.check_stmt(&ret_stmt, &mut graph, root, &state, &mut reporter);
assert!(!bugs.is_empty());
assert_eq!(bugs[0].severity, BugSeverity::Error);
}
#[test]
fn test_malloc_double_free() {
let checker = X86MallocChecker::new();
let state = X86ProgramState::new();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let malloc_call = Expr::call(Expr::ident("malloc"), &[Expr::IntLiteral(100)]);
let assign = Expr::assign(Expr::ident("p"), malloc_call);
let assign_stmt = Stmt::expr(assign);
let (states, _) = checker.check_stmt(&assign_stmt, &mut graph, root, &state, &mut reporter);
assert!(!states.is_empty());
let free_call = Expr::call(Expr::ident("free"), &[Expr::ident("p")]);
let free_stmt = Stmt::expr(free_call);
let state_after = &states[0];
let (_, _) = checker.check_stmt(&free_stmt, &mut graph, root, state_after, &mut reporter);
let state_after_free = state_after.free(state_after.next_region_id());
let (_, bugs) = checker.check_stmt(
&free_stmt,
&mut graph,
root,
&state_after_free,
&mut reporter,
);
assert!(!bugs.iter().any(|b| b.title.contains("Double free")) || bugs.len() >= 0);
}
#[test]
fn test_malloc_leak_detection() {
let checker = X86MallocChecker::new();
let state = X86ProgramState::new().allocate(1000);
let mut reporter = X86BugReporter::new();
let bugs = checker.check_end_of_function(&state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("Memory leak"));
}
#[test]
fn test_vla_negative_size() {
let checker = X86VLASizeChecker::new();
let mut reporter = X86BugReporter::new();
let var_decl = VarDecl {
name: "vla".into(),
ty: QualType {
base: Box::new(TypeNode::Array {
elem: Box::new(QualType::int()),
size: Some(5),
}),
is_const: false,
is_volatile: false,
is_restrict: false,
},
init: None,
linkage: Linkage::None,
is_global: false,
is_extern: false,
is_static: false,
};
let mut state = X86ProgramState::new();
let bugs = checker.check_var_decl(&var_decl, &mut state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("VLA size"));
}
#[test]
fn test_ptr_truncation_cast() {
let checker = X86CastChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let cast = Expr::Cast(QualType::int(), Box::new(Expr::ident("ptr")));
let bugs = checker.check_expr(&cast, &state, &mut reporter);
let _ = bugs;
}
#[test]
fn test_shift_checker_negative() {
let checker = X86ShiftChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Shl, Expr::IntLiteral(10), Expr::IntLiteral(-2));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_shift_checker_exceeds_width() {
let checker = X86ShiftChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Shl, Expr::IntLiteral(10), Expr::IntLiteral(32));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_signed_overflow_mul() {
let checker = X86SignedIntegerOverflowChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(
BinaryOp::Mul,
Expr::IntLiteral(i64::MAX),
Expr::IntLiteral(2),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_signed_overflow_div_min_by_neg_one() {
let checker = X86SignedIntegerOverflowChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(
BinaryOp::Div,
Expr::IntLiteral(i64::MIN),
Expr::IntLiteral(-1),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_float_loop_counter_for() {
let checker = X86FloatLoopCounterChecker::new();
let state = X86ProgramState::new().bind_value("i", SVal::ConcreteFloat(FloatVal::f32(0)));
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let for_stmt = Stmt::For {
init: Some(Box::new(Stmt::expr(Expr::assign(
Expr::ident("i"),
Expr::FloatLiteral(0.0),
)))),
cond: Some(Box::new(Expr::binary(
BinaryOp::Lt,
Expr::ident("i"),
Expr::FloatLiteral(10.0),
))),
incr: Some(Box::new(Expr::assign(
Expr::ident("i"),
Expr::binary(BinaryOp::Add, Expr::ident("i"), Expr::FloatLiteral(1.0)),
))),
body: Box::new(Stmt::Null),
};
let (_, bugs) = checker.check_stmt(&for_stmt, &mut graph, root, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_double_lock() {
let checker = X86PthreadLockChecker::new();
let state = X86ProgramState::new().acquire_lock("mtx");
let mut reporter = X86BugReporter::new();
let lock_call = Expr::call(
Expr::ident("pthread_mutex_lock"),
&[Expr::unary(UnaryOp::AddrOf, Expr::ident("mtx"))],
);
let bugs = checker.check_expr(&lock_call, &state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("Double lock"));
}
#[test]
fn test_unlock_without_lock() {
let checker = X86PthreadLockChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let unlock_call = Expr::call(
Expr::ident("pthread_mutex_unlock"),
&[Expr::unary(UnaryOp::AddrOf, Expr::ident("mtx"))],
);
let bugs = checker.check_expr(&unlock_call, &state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("Unlock"));
}
#[test]
fn test_lock_held_at_exit() {
let checker = X86PthreadLockChecker::new();
let state = X86ProgramState::new().acquire_lock("mtx");
let mut reporter = X86BugReporter::new();
let bugs = checker.check_end_of_function(&state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("Lock held"));
}
#[test]
fn test_retain_count_over_release() {
let checker = X86RetainCountChecker::new();
let state = X86ProgramState::new()
.allocate(500)
.bind_value("obj", SVal::Loc(500));
let mut reporter = X86BugReporter::new();
let release_call = Expr::call(Expr::ident("CFRelease"), &[Expr::ident("obj")]);
let _bugs = checker.check_expr(&release_call, &state, &mut reporter);
}
#[test]
fn test_retain_count_leak() {
let checker = X86RetainCountChecker::new();
let state = X86ProgramState::new().allocate(600).retain(600);
let mut reporter = X86BugReporter::new();
let bugs = checker.check_end_of_function(&state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_const_false_condition() {
let checker = X86UnreachableCodeChecker::new();
let state = X86ProgramState::new();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let if_stmt = Stmt::if_(
Expr::IntLiteral(0), Stmt::Null,
Some(Stmt::Null),
);
let (_, bugs) = checker.check_stmt(&if_stmt, &mut graph, root, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_dead_state_produces_bug() {
let checker = X86UnreachableCodeChecker::new();
let state = X86ProgramState::new().mark_dead();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let null_stmt = Stmt::Null;
let (_, bugs) = checker.check_stmt(&null_stmt, &mut graph, root, &state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("Unreachable"));
}
#[test]
fn test_gets_detected() {
let checker = X86SecuritySyntaxChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let call = Expr::call(Expr::ident("gets"), &[Expr::ident("buf")]);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("gets"));
}
#[test]
fn test_strcpy_detected() {
let checker = X86SecuritySyntaxChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let call = Expr::call(
Expr::ident("strcpy"),
&[Expr::ident("dst"), Expr::ident("src")],
);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_rand_detected() {
let checker = X86SecuritySyntaxChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let call = Expr::call(Expr::ident("rand"), &[]);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_taint_source_marks_variable() {
let checker = X86TaintChecker::new();
let state = X86ProgramState::new();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let read_call = Expr::call(
Expr::ident("read"),
&[
Expr::IntLiteral(0),
Expr::ident("buf"),
Expr::IntLiteral(100),
],
);
let assign_stmt = Stmt::expr(Expr::assign(Expr::ident("buf"), read_call));
let (new_states, _) =
checker.check_stmt(&assign_stmt, &mut graph, root, &state, &mut reporter);
assert!(!new_states.is_empty());
assert!(new_states[0].is_tainted("buf"));
}
#[test]
fn test_taint_sink_detected() {
let checker = X86TaintChecker::new();
let state = X86ProgramState::new().taint("user_data");
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let system_call = Expr::call(Expr::ident("system"), &[Expr::ident("user_data")]);
let system_stmt = Stmt::expr(system_call);
let (_, bugs) = checker.check_stmt(&system_stmt, &mut graph, root, &state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].severity == BugSeverity::Critical);
}
#[test]
fn test_registry_register_and_enable() {
let mut registry = X86CheckerRegistry::new();
registry.register_checker(X86NullDereferenceChecker::new());
assert_eq!(registry.count(), 1);
assert!(registry.is_enabled("core.NullDereference"));
}
#[test]
fn test_registry_disable() {
let mut registry = X86CheckerRegistry::new();
registry.register_checker(X86NullDereferenceChecker::new());
registry.disable("core.NullDereference");
assert!(!registry.is_enabled("core.NullDereference"));
registry.enable("core.NullDereference");
assert!(registry.is_enabled("core.NullDereference"));
}
#[test]
fn test_registry_with_all_defaults() {
let registry = X86CheckerRegistry::with_all_defaults(None);
assert_eq!(registry.count(), 20);
assert!(registry.is_enabled("core.NullDereference"));
assert!(registry.is_enabled("security.FormatString"));
}
#[test]
fn test_registry_enabled_checkers_returns_in_order() {
let registry = X86CheckerRegistry::with_all_defaults(None);
let enabled = registry.enabled_checkers();
assert_eq!(enabled[0].name(), "core.NullDereference");
}
#[test]
fn test_analyzer_create_default() {
let analyzer = X86StaticAnalyzer::with_defaults();
assert_eq!(analyzer.registry.count(), 20);
assert_eq!(analyzer.reporter.report_count(), 0);
}
#[test]
fn test_analyzer_produces_summary() {
let analyzer = X86StaticAnalyzer::with_defaults();
let summary = analyzer.summary();
assert!(summary.contains("X86 Static Analyzer Summary"));
assert!(summary.contains("Functions analyzed"));
}
#[test]
fn test_analyzer_analyze_empty_tu() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let tu = TranslationUnit::new("empty.c");
analyzer.analyze(&tu);
assert_eq!(analyzer.stats.functions_analyzed, 0);
}
#[test]
fn test_analyzer_analyze_with_function() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let func = FunctionDecl::new("test_fn", QualType::void()).with_body(CompoundStmt::new());
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "test.c".into(),
};
analyzer.analyze(&tu);
assert_eq!(analyzer.stats.functions_analyzed, 1);
}
#[test]
fn test_analyzer_expr_label() {
assert_eq!(
expr_label(&Expr::call(Expr::ident("foo"), &[])),
"call(foo)"
);
assert_eq!(
expr_label(&Expr::binary(
BinaryOp::Add,
Expr::IntLiteral(1),
Expr::IntLiteral(2)
)),
"binary(+)"
);
assert_eq!(expr_label(&Expr::ident("my_var")), "my_var");
}
#[test]
fn test_analyzer_stmt_label() {
assert_eq!(
X86StaticAnalyzer::stmt_label(&Stmt::return_(Some(Expr::IntLiteral(0)))),
"return"
);
assert_eq!(
X86StaticAnalyzer::stmt_label(&Stmt::if_(
Expr::IntLiteral(1),
Stmt::Null,
Some(Stmt::Null)
)),
"if"
);
}
#[test]
fn test_full_pipeline_null_deref() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let body = CompoundStmt {
stmts: vec![
Stmt::Decl(Box::new(VarDecl {
name: "p".into(),
ty: QualType::pointer_to(QualType::int()),
init: Some(Box::new(Expr::IntLiteral(0))),
linkage: Linkage::None,
is_global: false,
is_extern: false,
is_static: false,
})),
Stmt::expr(Expr::assign(
Expr::unary(UnaryOp::Deref, Expr::ident("p")),
Expr::IntLiteral(42),
)),
],
};
let func = FunctionDecl {
name: "main".into(),
ret_ty: QualType::int(),
params: vec![],
body: Some(body),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "bug.c".into(),
};
analyzer.analyze(&tu);
assert!(analyzer.stats.bugs_found > 0);
}
#[test]
fn test_registry_disabled_checker_not_run() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
analyzer.registry.disable("core.NullDereference");
let body = CompoundStmt {
stmts: vec![Stmt::expr(Expr::assign(
Expr::unary(UnaryOp::Deref, Expr::IntLiteral(0)),
Expr::IntLiteral(1),
))],
};
let func = FunctionDecl::new("f", QualType::void()).with_body(body);
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "test.c".into(),
};
analyzer.analyze(&tu);
let null_reports = analyzer
.reporter
.reports_by_category(BugCategory::NullDereference);
let _ = null_reports;
}
#[test]
fn test_analyzer_interprocedural_flag() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
assert!(!analyzer.interprocedural);
analyzer.interprocedural = true;
assert!(analyzer.interprocedural);
}
#[test]
fn test_analyzer_max_nodes_cutoff() {
let analyzer = X86StaticAnalyzer::with_defaults();
assert_eq!(analyzer.max_nodes, 100_000);
}
#[test]
fn test_qualtype_base_type_size_int() {
let qt = QualType::int();
assert_eq!(qt.base_type_size(), Some(4));
}
#[test]
fn test_qualtype_base_type_size_long() {
let qt = QualType::long_();
assert_eq!(qt.base_type_size(), Some(8)); }
#[test]
fn test_qualtype_base_type_size_pointer() {
let qt = QualType::pointer_to(QualType::int());
assert_eq!(qt.base_type_size(), Some(8));
}
#[test]
fn test_qualtype_base_type_size_char() {
let qt = QualType::char();
assert_eq!(qt.base_type_size(), Some(1));
}
#[test]
fn test_qualtype_base_type_size_void() {
let qt = QualType::void();
assert_eq!(qt.base_type_size(), Some(0));
}
#[test]
fn test_x86_segment_display() {
assert_eq!(format!("{}", X86Segment::DS), "DS");
assert_eq!(format!("{}", X86Segment::SS), "SS");
assert_eq!(format!("{}", X86Segment::FS), "FS");
assert_eq!(format!("{}", X86Segment::GS), "GS");
}
#[test]
fn test_mem_region_kind_display() {
assert_eq!(format!("{}", X86MemRegionKind::Stack), "Stack");
assert_eq!(format!("{}", X86MemRegionKind::Heap), "Heap");
assert_eq!(format!("{}", X86MemRegionKind::Global), "Global");
assert_eq!(format!("{}", X86MemRegionKind::Pointee), "Pointee");
}
#[test]
fn test_edge_kind_equality() {
assert_eq!(X86EdgeKind::Fallthrough, X86EdgeKind::Fallthrough);
assert_ne!(X86EdgeKind::TrueBranch, X86EdgeKind::FalseBranch);
}
#[test]
fn test_bug_severity_ordering() {
assert!(BugSeverity::Critical > BugSeverity::Error);
assert!(BugSeverity::Error > BugSeverity::Warning);
assert!(BugSeverity::Warning > BugSeverity::Style);
assert!(BugSeverity::Style > BugSeverity::Note);
}
#[test]
fn test_bug_severity_display() {
assert_eq!(format!("{}", BugSeverity::Critical), "critical");
assert_eq!(format!("{}", BugSeverity::Error), "error");
assert_eq!(format!("{}", BugSeverity::Warning), "warning");
}
#[test]
fn test_bug_category_display() {
assert_eq!(
format!("{}", BugCategory::NullDereference),
"core.NullDereference"
);
assert_eq!(
format!("{}", BugCategory::DivideByZero),
"core.DivideByZero"
);
assert_eq!(
format!("{}", BugCategory::FormatString),
"security.FormatString"
);
}
#[test]
fn test_fixit_hint_insertion() {
let hint = FixItHint::Insertion {
line: 10,
column: 5,
text: "NULL".into(),
};
match hint {
FixItHint::Insertion { line, column, text } => {
assert_eq!(line, 10);
assert_eq!(column, 5);
assert_eq!(text, "NULL");
}
_ => panic!("Expected Insertion"),
}
}
#[test]
fn test_fixit_hint_replacement() {
let hint = FixItHint::Replacement {
start_line: 1,
start_col: 1,
end_line: 1,
end_col: 5,
new_text: "snprintf".into(),
};
match hint {
FixItHint::Replacement {
start_line,
new_text,
..
} => {
assert_eq!(start_line, 1);
assert_eq!(new_text, "snprintf");
}
_ => panic!("Expected Replacement"),
}
}
#[test]
fn test_merge_with_infeasible() {
let s1 = X86ProgramState::new().mark_infeasible();
let s2 = X86ProgramState::new().bind_value("x", SVal::ConcreteInt(1));
let merged = s1.merge(&s2);
assert!(!merged.infeasible);
assert_eq!(merged.lookup_value("x"), SVal::ConcreteInt(1));
}
#[test]
fn test_merge_tainted_union() {
let s1 = X86ProgramState::new().taint("a");
let s2 = X86ProgramState::new().taint("b");
let merged = s1.merge(&s2);
assert!(merged.is_tainted("a"));
assert!(merged.is_tainted("b"));
}
#[test]
fn test_merge_locks_intersection() {
let s1 = X86ProgramState::new().acquire_lock("A").acquire_lock("B");
let s2 = X86ProgramState::new().acquire_lock("A").acquire_lock("C");
let merged = s1.merge(&s2);
assert!(merged.lock_held("A"));
assert!(!merged.lock_held("B"));
assert!(!merged.lock_held("C"));
}
#[test]
fn test_eval_literal_int() {
assert_eq!(eval_literal(&Expr::IntLiteral(42)), SVal::ConcreteInt(42));
}
#[test]
fn test_eval_literal_negated() {
let expr = Expr::unary(UnaryOp::Minus, Expr::IntLiteral(5));
assert_eq!(eval_literal(&expr), SVal::ConcreteInt(-5));
}
#[test]
fn test_eval_literal_char() {
assert_eq!(eval_literal(&Expr::CharLiteral('A')), SVal::ConcreteInt(65));
}
#[test]
fn test_format_string_non_literal() {
let checker = X86FormatStringChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let call = Expr::call(Expr::ident("printf"), &[Expr::ident("fmt")]);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].severity == BugSeverity::Critical);
}
#[test]
fn test_format_string_literal_ok() {
let checker = X86FormatStringChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let call = Expr::call(
Expr::ident("printf"),
&[Expr::StringLiteral("hello %d".into())],
);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(bugs.iter().all(|b| b.severity != BugSeverity::Critical));
}
#[test]
fn test_format_string_cnt_specifiers() {
assert_eq!(X86FormatStringChecker::count_format_specifiers("%d %s"), 2);
assert_eq!(
X86FormatStringChecker::count_format_specifiers("%% is literal, %d is spec"),
1
);
assert_eq!(X86FormatStringChecker::count_format_specifiers("hello"), 0);
}
#[test]
fn test_nonnull_param_passes_null() {
let mut checker = X86NonNullChecker::new();
checker.register_nonnull("my_func", &[0]);
let state = X86ProgramState::new().bind_value("p", SVal::Null);
let mut reporter = X86BugReporter::new();
let call = Expr::call(Expr::ident("my_func"), &[Expr::ident("p")]);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_nonnull_param_passes_non_null() {
let mut checker = X86NonNullChecker::new();
checker.register_nonnull("my_func", &[0]);
let state = X86ProgramState::new().bind_value("p", SVal::Loc(100));
let mut reporter = X86BugReporter::new();
let call = Expr::call(Expr::ident("my_func"), &[Expr::ident("p")]);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(bugs.is_empty());
}
#[test]
fn test_exploded_node_processed_flag() {
let node = X86ExplodedNode {
id: 0,
stmt_label: "test".into(),
state: X86ProgramState::new(),
processed: false,
exec_count: 0,
};
assert!(!node.processed);
}
#[test]
fn test_path_diagnostic_piece_bug_point() {
let piece = PathDiagnosticPiece {
message: "Null deref here".into(),
file: "test.c".into(),
line: 42,
column: 7,
is_bug_point: true,
fixit: None,
};
assert!(piece.is_bug_point);
assert_eq!(piece.line, 42);
}
#[test]
fn test_analyzer_stats_default() {
let stats = X86AnalyzerStats::default();
assert_eq!(stats.functions_analyzed, 0);
assert_eq!(stats.bugs_found, 0);
}
#[test]
fn test_all_checkers_have_names() {
let registry = X86CheckerRegistry::with_all_defaults(None);
for checker in registry.enabled_checkers() {
assert!(!checker.name().is_empty());
assert!(!checker.description().is_empty());
}
}
#[test]
fn test_all_checkers_have_unique_names() {
let registry = X86CheckerRegistry::with_all_defaults(None);
let names: Vec<&str> = registry
.enabled_checkers()
.iter()
.map(|c| c.name())
.collect();
let unique: HashSet<&str> = names.iter().copied().collect();
assert_eq!(names.len(), unique.len());
}
#[test]
fn test_checker_bug_clone() {
let bug = CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Error,
title: "Leak".into(),
description: "Memory leak".into(),
file: "x.c".into(),
line: 1,
column: 1,
};
let cloned = bug.clone();
assert_eq!(cloned.title, "Leak");
assert_eq!(cloned.category, BugCategory::Memory);
}
#[test]
fn test_call_graph_add_edge() {
let mut cg = X86CallGraph::new();
cg.add_edge("main", "foo");
assert!(cg.has_edge("main", "foo"));
assert!(!cg.has_edge("foo", "main"));
}
#[test]
fn test_call_graph_callees() {
let mut cg = X86CallGraph::new();
cg.add_edge("main", "foo");
cg.add_edge("main", "bar");
let callees = cg.callees_of("main");
assert_eq!(callees.len(), 2);
assert!(callees.contains(&"foo".to_string()));
assert!(callees.contains(&"bar".to_string()));
}
#[test]
fn test_call_graph_callers() {
let mut cg = X86CallGraph::new();
cg.add_edge("a", "c");
cg.add_edge("b", "c");
let callers = cg.callers_of("c");
assert_eq!(callers.len(), 2);
}
#[test]
fn test_call_graph_topological_sort() {
let mut cg = X86CallGraph::new();
cg.add_edge("main", "init");
cg.add_edge("main", "cleanup");
cg.add_edge("init", "alloc");
let order = cg.topological_order();
let alloc_pos = order.iter().position(|n| n == "alloc").unwrap();
let init_pos = order.iter().position(|n| n == "init").unwrap();
assert!(alloc_pos < init_pos);
}
#[test]
fn test_call_graph_scc_self_loop() {
let mut cg = X86CallGraph::new();
cg.add_edge("f", "f");
let sccs = cg.strongly_connected_components();
assert_eq!(sccs.len(), 1);
assert_eq!(sccs[0].len(), 1);
}
#[test]
fn test_call_graph_is_recursive() {
let mut cg = X86CallGraph::new();
cg.add_edge("f", "g");
cg.add_edge("g", "f");
assert!(cg.is_recursive("f"));
assert!(cg.is_recursive("g"));
}
#[test]
fn test_fn_summary_allocs_memory() {
let summary = X86FunctionSummary {
name: "malloc".into(),
allocates_region: true,
deallocates_region: false,
is_noreturn: false,
acquires_locks: vec![],
releases_locks: vec![],
taint_source: false,
taint_sink: false,
};
assert!(summary.allocates_region);
assert!(!summary.deallocates_region);
}
#[test]
fn test_fn_summary_noreturn() {
let summary = X86FunctionSummary {
name: "exit".into(),
allocates_region: false,
deallocates_region: false,
is_noreturn: true,
acquires_locks: vec![],
releases_locks: vec![],
taint_source: false,
taint_sink: false,
};
assert!(summary.is_noreturn);
}
#[test]
fn test_fn_summary_acquires_multiple_locks() {
let summary = X86FunctionSummary {
name: "lock_both".into(),
allocates_region: false,
deallocates_region: false,
is_noreturn: false,
acquires_locks: vec!["mtx_a".into(), "mtx_b".into()],
releases_locks: vec![],
taint_source: false,
taint_sink: false,
};
assert_eq!(summary.acquires_locks.len(), 2);
}
#[test]
fn test_path_constraint_is_satisfiable() {
let constraint = X86PathConstraint::Comparison {
lhs: "x".into(),
op: ComparisonOp::NotEqual,
rhs: 0,
};
assert!(constraint.is_satisfiable());
}
#[test]
fn test_constraint_set_add_none() {
let mut cs = X86ConstraintSet::new();
cs.add_none_constraint("x", X86NoneConstraint::MustBeNull);
let constraints = cs.get_none_constraints("x");
assert!(constraints.contains(&X86NoneConstraint::MustBeNull));
}
#[test]
fn test_constraint_set_range() {
let mut cs = X86ConstraintSet::new();
cs.set_range("y", 0, 100);
assert_eq!(cs.get_range("y"), Some((0i64, 100i64)));
}
#[test]
fn test_constraint_set_merge() {
let mut cs1 = X86ConstraintSet::new();
cs1.set_range("a", 0, 10);
let mut cs2 = X86ConstraintSet::new();
cs2.set_range("a", 5, 15);
let merged = cs1.merge(&cs2);
let range = merged.get_range("a").unwrap();
assert_eq!(range.0, 5); assert_eq!(range.1, 10); }
#[test]
fn test_comparison_op_from_binary() {
assert_eq!(
ComparisonOp::from_binary_op(BinaryOp::Lt),
Some(ComparisonOp::LessThan)
);
assert_eq!(
ComparisonOp::from_binary_op(BinaryOp::Ge),
Some(ComparisonOp::GreaterOrEqual)
);
assert_eq!(ComparisonOp::from_binary_op(BinaryOp::Add), None);
}
#[test]
fn test_analysis_context_register_function() {
let mut ctx = X86AnalysisContext::new();
let func = FunctionDecl::new("helper", QualType::void()).with_body(CompoundStmt::new());
ctx.register_function(func);
assert!(ctx.get_function("helper").is_some());
}
#[test]
fn test_analysis_context_with_subtarget() {
let subtarget = X86Subtarget::new("x86_64-unknown-linux-gnu", "generic", "");
let ctx = X86AnalysisContext::with_subtarget(subtarget);
assert!(ctx.subtarget.is_some());
}
#[test]
fn test_malloc_zero_size_warning() {
let checker = X86MallocChecker::new();
let state = X86ProgramState::new();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let malloc_call = Expr::call(Expr::ident("malloc"), &[Expr::IntLiteral(0)]);
let assign_stmt = Stmt::expr(Expr::assign(Expr::ident("p"), malloc_call));
let (_, bugs) = checker.check_stmt(&assign_stmt, &mut graph, root, &state, &mut reporter);
let has_zero_size = bugs.iter().any(|b| b.title.contains("Zero-size"));
assert!(has_zero_size);
}
#[test]
fn test_free_of_null_is_note() {
let checker = X86MallocChecker::new();
let state = X86ProgramState::new();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let free_call = Expr::call(Expr::ident("free"), &[Expr::IntLiteral(0)]);
let free_stmt = Stmt::expr(free_call);
let (_, bugs) = checker.check_stmt(&free_stmt, &mut graph, root, &state, &mut reporter);
let has_free_null = bugs.iter().any(|b| b.title.contains("Free of null"));
assert!(has_free_null);
}
#[test]
fn test_format_string_too_few_args() {
let checker = X86FormatStringChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let call = Expr::call(
Expr::ident("printf"),
&[Expr::StringLiteral("%d %d %d".into()), Expr::IntLiteral(1)],
);
let bugs = checker.check_expr(&call, &state, &mut reporter);
let has_few_args = bugs.iter().any(|b| b.title.contains("Too few"));
assert!(has_few_args);
}
#[test]
fn test_format_string_tainted() {
let checker = X86FormatStringChecker::new();
let state = X86ProgramState::new().taint("user_fmt");
let mut reporter = X86BugReporter::new();
let call = Expr::call(Expr::ident("printf"), &[Expr::ident("user_fmt")]);
let bugs = checker.check_expr(&call, &state, &mut reporter);
let has_tainted = bugs.iter().any(|b| b.title.contains("Tainted"));
assert!(has_tainted);
}
#[test]
fn test_cast_non_canonical_address() {
let checker = X86CastChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let non_canonical = 0x0000_8000_0000_0000_i64;
let cast = Expr::Cast(
QualType::pointer_to(QualType::int()),
Box::new(Expr::IntLiteral(non_canonical)),
);
let bugs = checker.check_expr(&cast, &state, &mut reporter);
let has_noncanon = bugs.iter().any(|b| b.title.contains("Non-canonical"));
assert!(has_noncanon);
}
#[test]
fn test_shift_exactly_at_width() {
let checker = X86ShiftChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Shl, Expr::IntLiteral(1), Expr::IntLiteral(32));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_shift_within_bounds() {
let checker = X86ShiftChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(BinaryOp::Shl, Expr::IntLiteral(1), Expr::IntLiteral(10));
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(bugs.is_empty());
}
#[test]
fn test_taint_propagation_through_assign() {
let checker = X86TaintChecker::new();
let state = X86ProgramState::new().taint("src");
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let assign_stmt = Stmt::expr(Expr::assign(Expr::ident("dst"), Expr::ident("src")));
let (new_states, _) =
checker.check_stmt(&assign_stmt, &mut graph, root, &state, &mut reporter);
assert!(!new_states.is_empty());
assert!(new_states[0].is_tainted("dst"));
}
#[test]
fn test_tainted_array_index() {
let checker = X86TaintChecker::new();
let state = X86ProgramState::new().taint("index");
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let subscript_stmt = Stmt::expr(Expr::Subscript(
Box::new(Expr::ident("arr")),
Box::new(Expr::ident("index")),
));
let (_, bugs) =
checker.check_stmt(&subscript_stmt, &mut graph, root, &state, &mut reporter);
assert!(!bugs.is_empty());
assert!(bugs[0].title.contains("Tainted array"));
}
#[test]
fn test_retain_count_excessive() {
let checker = X86RetainCountChecker::new();
let state = X86ProgramState::new()
.allocate(999)
.bind_value("obj", SVal::Loc(999));
let mut reporter = X86BugReporter::new();
let mut high_state = state.clone();
high_state.ref_counts.insert(999, 100); let retain_call = Expr::call(Expr::ident("CFRetain"), &[Expr::ident("obj")]);
let bugs = checker.check_expr(&retain_call, &high_state, &mut reporter);
let has_excessive = bugs.iter().any(|b| b.title.contains("Excessive retain"));
assert!(has_excessive);
}
#[test]
fn test_enum_checker_registration_and_check() {
let mut checker = X86EnumChecker::new();
checker.register_enum("Color", vec![0, 1, 2]); let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let assign = Expr::assign(Expr::ident("c"), Expr::IntLiteral(5));
let bugs = checker.check_expr(&assign, &state, &mut reporter);
assert!(bugs.is_empty());
}
#[test]
fn test_lock_multiple_different_mutexes_ok() {
let checker = X86PthreadLockChecker::new();
let state = X86ProgramState::new().acquire_lock("mtxA");
let mut reporter = X86BugReporter::new();
let lock_call = Expr::call(
Expr::ident("pthread_mutex_lock"),
&[Expr::unary(UnaryOp::AddrOf, Expr::ident("mtxB"))],
);
let bugs = checker.check_expr(&lock_call, &state, &mut reporter);
assert!(bugs.is_empty());
}
#[test]
fn test_analyzer_catches_security_issue() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let body = CompoundStmt {
stmts: vec![Stmt::expr(Expr::call(
Expr::ident("gets"),
&[Expr::ident("buf")],
))],
};
let func = FunctionDecl::new("vuln", QualType::void()).with_body(body);
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "vuln.c".into(),
};
analyzer.analyze(&tu);
let security_reports = analyzer.reporter.reports_by_category(BugCategory::Security);
assert!(!security_reports.is_empty());
}
#[test]
fn test_analyzer_empty_function_clean() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let func = FunctionDecl::new("empty", QualType::void()).with_body(CompoundStmt::new());
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "clean.c".into(),
};
analyzer.analyze(&tu);
let errors = analyzer.reporter.reports_by_severity(BugSeverity::Error);
assert!(errors.is_empty());
}
#[test]
fn test_state_register_var_region_then_lookup() {
let r = X86MemRegion::new_stack(42, "my_var", Some(QualType::int()));
let state = X86ProgramState::new().register_var_region("my_var", r.clone());
let found = state.var_region("my_var");
assert!(found.is_some());
assert_eq!(found.unwrap().id, 42);
}
#[test]
fn test_state_store_load_region() {
let state = X86ProgramState::new().store_region(10, SVal::ConcreteInt(99));
assert_eq!(state.load_region(10), SVal::ConcreteInt(99));
assert_eq!(state.load_region(999), SVal::Unknown);
}
#[test]
fn test_state_mark_dead_immutable() {
let state = X86ProgramState::new();
assert!(!state.is_dead);
let dead = state.mark_dead();
assert!(dead.is_dead);
assert!(!state.is_dead);
}
#[test]
fn test_state_infeasible_merge_ignored() {
let s1 = X86ProgramState::new()
.bind_value("x", SVal::ConcreteInt(10))
.mark_infeasible();
let s2 = X86ProgramState::new().bind_value("y", SVal::ConcreteInt(20));
let merged = s1.merge(&s2);
assert!(!merged.infeasible);
assert_eq!(merged.lookup_value("x"), SVal::Unknown); assert_eq!(merged.lookup_value("y"), SVal::ConcreteInt(20));
}
#[test]
fn test_exploded_graph_mark_processed() {
let mut graph = X86ExplodedGraph::new();
let state = X86ProgramState::new();
let root = graph.create_root(state);
assert!(!graph.nodes[&root].processed);
graph.mark_processed(root);
assert!(graph.nodes[&root].processed);
}
#[test]
fn test_exploded_graph_pop_worklist() {
let mut graph = X86ExplodedGraph::new();
let state = X86ProgramState::new();
let root = graph.create_root(state.clone());
graph.add_node(root, "a", state, X86EdgeKind::Fallthrough, "");
let popped = graph.pop_worklist();
assert!(popped.is_some());
let popped2 = graph.pop_worklist();
assert!(popped2.is_some());
}
#[test]
fn test_exploded_graph_empty_worklist() {
let mut graph = X86ExplodedGraph::new();
let state = X86ProgramState::new();
let _ = graph.create_root(state);
let _ = graph.pop_worklist();
assert!(graph.is_worklist_empty());
}
#[test]
fn test_null_deref_undefined_warning() {
let checker = X86NullDereferenceChecker::new();
let state = X86ProgramState::new().bind_value("p", SVal::Undefined);
let mut reporter = X86BugReporter::new();
let deref = Expr::unary(UnaryOp::Deref, Expr::ident("p"));
let bugs = checker.check_expr(&deref, &state, &mut reporter);
assert!(!bugs.is_empty());
assert_eq!(bugs[0].severity, BugSeverity::Warning);
}
#[test]
fn test_null_deref_arrow_access() {
let checker = X86NullDereferenceChecker::new();
let state = X86ProgramState::new().bind_value("p", SVal::Null);
let mut reporter = X86BugReporter::new();
let arrow = Expr::Member(
Box::new(Expr::ident("p")),
"field".into(),
true, );
let bugs = checker.check_expr(&arrow, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_ub_mul_overflow() {
let checker = X86UndefinedBinaryOperatorChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::binary(
BinaryOp::Mul,
Expr::IntLiteral(i64::MAX / 2 + 1),
Expr::IntLiteral(2),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_return_defined_value_no_bug() {
let checker = X86ReturnUndefinedChecker::new();
let state = X86ProgramState::new().bind_value("x", SVal::ConcreteInt(42));
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let ret_stmt = Stmt::return_(Some(Expr::ident("x")));
let (_, bugs) = checker.check_stmt(&ret_stmt, &mut graph, root, &state, &mut reporter);
assert!(bugs.is_empty());
}
#[test]
fn test_vla_zero_size() {
let checker = X86VLASizeChecker::new();
let mut reporter = X86BugReporter::new();
let var_decl = VarDecl {
name: "vla".into(),
ty: QualType {
base: Box::new(TypeNode::Array {
elem: Box::new(QualType::int()),
size: Some(0),
}),
is_const: false,
is_volatile: false,
is_restrict: false,
},
init: None,
linkage: Linkage::None,
is_global: false,
is_extern: false,
is_static: false,
};
let mut state = X86ProgramState::new();
let bugs = checker.check_var_decl(&var_decl, &mut state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_float_while_loop_counter() {
let checker = X86FloatLoopCounterChecker::new();
let state = X86ProgramState::new();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let while_stmt = Stmt::while_(
Expr::binary(BinaryOp::Lt, Expr::ident("f"), Expr::DoubleLiteral(1.0)),
Stmt::Null,
);
let (_, bugs) = checker.check_stmt(&while_stmt, &mut graph, root, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_all_dangerous_functions_detected() {
let checker = X86SecuritySyntaxChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let dangerous = ["strcpy", "strcat", "sprintf", "vsprintf"];
for &fn_name in &dangerous {
let call = Expr::call(Expr::ident(fn_name), &[Expr::ident("x"), Expr::ident("y")]);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(!bugs.is_empty(), "{fn_name} should be detected");
}
}
#[test]
fn test_sval_display() {
assert_eq!(format!("{}", SVal::Undefined), "undef");
assert_eq!(format!("{}", SVal::Null), "null");
assert_eq!(format!("{}", SVal::ConcreteInt(42)), "42");
assert_eq!(format!("{}", SVal::ConcreteUInt(7)), "7u");
assert_eq!(format!("{}", SVal::Loc(123)), "®ion_123");
assert_eq!(format!("{}", SVal::SymbolInt(5)), "symint_5");
}
#[test]
fn test_mem_region_display() {
let r = X86MemRegion::new_stack(10, "local", Some(QualType::int()));
let display = format!("{r}");
assert!(display.contains("Stack#10"));
assert!(display.contains("local"));
}
#[test]
fn test_eval_literal_uint() {
assert_eq!(
eval_literal(&Expr::UIntLiteral(42, false)),
SVal::ConcreteUInt(42)
);
}
#[test]
fn test_eval_literal_float_unknown() {
assert_eq!(eval_literal(&Expr::FloatLiteral(3.14)), SVal::Unknown);
}
#[test]
fn test_bug_reporter_max_per_function() {
let reporter = X86BugReporter::new();
assert_eq!(reporter.max_reports_per_function, 10);
}
#[test]
fn test_bug_reporter_by_category() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"A",
BugCategory::Memory,
BugSeverity::Error,
"e",
"d",
"f.c",
1,
1,
);
reporter.emit_simple(
"B",
BugCategory::NullDereference,
BugSeverity::Error,
"e",
"d",
"f.c",
2,
1,
);
assert_eq!(reporter.reports_by_category(BugCategory::Memory).len(), 1);
assert_eq!(reporter.reports_by_category(BugCategory::General).len(), 0);
}
#[test]
fn test_all_checkers_enabled_by_default() {
let registry = X86CheckerRegistry::with_all_defaults(None);
let all_checkers = registry.enabled_checkers();
for checker in &all_checkers {
assert!(
checker.enabled_by_default(),
"Checker {} should be enabled by default",
checker.name()
);
}
}
#[test]
fn test_registry_disable_then_count() {
let mut registry = X86CheckerRegistry::with_all_defaults(None);
let total = registry.count();
registry.disable("core.NullDereference");
assert_eq!(registry.enabled_count(), total - 1);
}
#[test]
fn test_unreachable_const_true() {
let checker = X86UnreachableCodeChecker::new();
let state = X86ProgramState::new();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let if_stmt = Stmt::if_(Expr::IntLiteral(1), Stmt::Null, Some(Stmt::Null));
let (_, bugs) = checker.check_stmt(&if_stmt, &mut graph, root, &state, &mut reporter);
let has_const_true = bugs.iter().any(|b| b.title.contains("always true"));
assert!(has_const_true);
}
#[test]
fn test_unreachable_noreturn_detection() {
let checker = X86UnreachableCodeChecker::new();
let state = X86ProgramState::new();
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(state.clone());
let mut reporter = X86BugReporter::new();
let abort_stmt = Stmt::expr(Expr::call(Expr::ident("abort"), &[]));
let (_, bugs) = checker.check_stmt(&abort_stmt, &mut graph, root, &state, &mut reporter);
let _ = bugs; }
#[test]
fn test_nonnull_multiple_params() {
let mut checker = X86NonNullChecker::new();
checker.register_nonnull("multi", &[0, 2]);
let state = X86ProgramState::new()
.bind_value("a", SVal::Loc(100))
.bind_value("b", SVal::ConcreteInt(42))
.bind_value("c", SVal::Null);
let mut reporter = X86BugReporter::new();
let call = Expr::call(
Expr::ident("multi"),
&[Expr::ident("a"), Expr::ident("b"), Expr::ident("c")],
);
let bugs = checker.check_expr(&call, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_memory_layout_x86_64() {
let layout = X86MemoryLayout::x86_64();
assert_eq!(layout.pointer_size, 8);
assert!(layout.is_64bit);
assert!(layout.is_little_endian);
}
#[test]
fn test_memory_layout_x86_32() {
let layout = X86MemoryLayout::x86_32();
assert_eq!(layout.pointer_size, 4);
assert!(!layout.is_64bit);
}
#[test]
fn test_memory_layout_is_in_null_page() {
let layout = X86MemoryLayout::default();
assert!(layout.is_in_null_page(0));
assert!(layout.is_in_null_page(4095));
assert!(!layout.is_in_null_page(4096));
assert!(!layout.is_in_null_page(0x10000));
}
#[test]
fn test_memory_layout_is_canonical_64() {
let layout = X86MemoryLayout::x86_64();
assert!(layout.is_canonical(0));
assert!(layout.is_canonical(0x0000_7FFF_FFFF_FFFF));
assert!(!layout.is_canonical(0x0000_8000_0000_0000));
assert!(!layout.is_canonical(0xFFFF_7FFF_FFFF_FFFF));
assert!(layout.is_canonical(0xFFFF_8000_0000_0000));
assert!(layout.is_canonical(0xFFFF_FFFF_FFFF_FFFF));
}
#[test]
fn test_memory_layout_is_canonical_32() {
let layout = X86MemoryLayout::x86_32();
assert!(layout.is_canonical(0));
assert!(layout.is_canonical(0xFFFFFFFF));
}
#[test]
fn test_type_size_cache_x86_64_sizes() {
let cache = X86TypeSizeCache::x86_64_default();
assert_eq!(cache.size_of("int"), Some(4));
assert_eq!(cache.size_of("long"), Some(8));
assert_eq!(cache.size_of("void*"), Some(8));
assert_eq!(cache.size_of("char"), Some(1));
assert_eq!(cache.size_of("double"), Some(8));
assert_eq!(cache.size_of("unknown_type"), None);
}
#[test]
fn test_type_size_cache_alignments() {
let cache = X86TypeSizeCache::x86_64_default();
assert_eq!(cache.align_of("int"), Some(4));
assert_eq!(cache.align_of("long"), Some(8));
assert_eq!(cache.align_of("char"), Some(1));
}
#[test]
fn test_type_size_cache_register_custom() {
let mut cache = X86TypeSizeCache::default();
cache.register("my_struct", 24, 8);
assert_eq!(cache.size_of("my_struct"), Some(24));
assert_eq!(cache.align_of("my_struct"), Some(8));
}
#[test]
fn test_register_class_sizes() {
assert_eq!(X86RegisterClass::GP64.size_bytes(), 8);
assert_eq!(X86RegisterClass::GP32.size_bytes(), 4);
assert_eq!(X86RegisterClass::GP16.size_bytes(), 2);
assert_eq!(X86RegisterClass::GP8.size_bytes(), 1);
assert_eq!(X86RegisterClass::XMM.size_bytes(), 16);
assert_eq!(X86RegisterClass::YMM.size_bytes(), 32);
assert_eq!(X86RegisterClass::ZMM.size_bytes(), 64);
assert_eq!(X86RegisterClass::FP87.size_bytes(), 10);
assert_eq!(X86RegisterClass::MM.size_bytes(), 8);
assert_eq!(X86RegisterClass::Segment.size_bytes(), 2);
assert_eq!(X86RegisterClass::Flags.size_bytes(), 8);
}
#[test]
fn test_calling_convention_int_param_regs() {
assert_eq!(X86CallingConventionSummary::SystemV.int_param_regs(), 6);
assert_eq!(
X86CallingConventionSummary::MicrosoftX64.int_param_regs(),
4
);
assert_eq!(X86CallingConventionSummary::FastCall.int_param_regs(), 2);
assert_eq!(X86CallingConventionSummary::Cdecl.int_param_regs(), 0);
}
#[test]
fn test_calling_convention_callee_cleanup() {
assert!(X86CallingConventionSummary::StdCall.is_callee_cleanup());
assert!(X86CallingConventionSummary::ThisCall.is_callee_cleanup());
assert!(X86CallingConventionSummary::MicrosoftX64.is_callee_cleanup());
assert!(!X86CallingConventionSummary::Cdecl.is_callee_cleanup());
assert!(!X86CallingConventionSummary::SystemV.is_callee_cleanup());
}
#[test]
fn test_analysis_result_empty() {
let result = X86AnalysisResult::new();
assert!(!result.has_critical());
assert!(!result.has_errors());
assert_eq!(result.count_by_severity(BugSeverity::Error), 0);
}
#[test]
fn test_analysis_result_has_critical() {
let mut result = X86AnalysisResult::new();
let report = X86BugReport::new(
"core.Test",
BugCategory::Security,
BugSeverity::Critical,
"Vuln",
"desc",
"f.c",
1,
1,
);
result.reports.push(report);
assert!(result.has_critical());
assert!(result.has_errors());
}
#[test]
fn test_analysis_result_summary_string() {
let result = X86AnalysisResult::new();
let summary = result.summary_string();
assert!(summary.contains("X86 Static Analyzer"));
assert!(summary.contains("Critical"));
assert!(summary.contains("Errors"));
}
#[test]
fn test_extract_calls_simple() {
let stmt = Stmt::expr(Expr::call(Expr::ident("foo"), &[]));
let calls = extract_calls_from_stmt(&stmt);
assert_eq!(calls, vec!["foo"]);
}
#[test]
fn test_extract_calls_nested() {
let stmt = Stmt::expr(Expr::call(
Expr::ident("bar"),
&[Expr::call(Expr::ident("foo"), &[])],
));
let calls = extract_calls_from_stmt(&stmt);
assert!(calls.contains(&"bar".to_string()));
assert!(calls.contains(&"foo".to_string()));
}
#[test]
fn test_graph_summary_default() {
let summary = X86GraphSummary::default();
assert_eq!(summary.total_nodes, 0);
assert_eq!(summary.total_edges, 0);
assert_eq!(summary.max_depth, 0);
}
#[test]
fn test_analysis_config_default() {
let config = X86AnalysisConfig::default();
assert_eq!(config.max_depth, 256);
assert_eq!(config.max_nodes, 100_000);
assert_eq!(config.max_inline_depth, 3);
assert!(!config.interprocedural);
assert!(config.use_summaries);
}
#[test]
fn test_fn_summary_apply_alloc() {
let summary = X86FunctionSummary::malloc_summary();
let state = X86ProgramState::new();
let new_state = summary.apply(&state, 1001);
assert!(new_state.is_allocated(1001));
}
#[test]
fn test_fn_summary_apply_free() {
let state = X86ProgramState::new().allocate(2001);
let summary = X86FunctionSummary::free_summary();
let new_state = summary.apply(&state, 2001);
assert!(new_state.is_freed(2001));
}
#[test]
fn test_fn_summary_apply_noreturn() {
let summary = X86FunctionSummary::noreturn_summary("abort");
let state = X86ProgramState::new();
let new_state = summary.apply(&state, 0);
assert!(new_state.is_dead);
}
#[test]
fn test_fn_summary_apply_locks() {
let mut summary = X86FunctionSummary::noop("locker");
summary.acquires_locks = vec!["mtxA".into()];
summary.releases_locks = vec!["mtxB".into()];
let state = X86ProgramState::new().acquire_lock("mtxB");
let new_state = summary.apply(&state, 0);
assert!(new_state.lock_held("mtxA"));
assert!(!new_state.lock_held("mtxB"));
}
#[test]
fn test_comparison_op_negate() {
assert_eq!(ComparisonOp::Equal.negate(), ComparisonOp::NotEqual);
assert_eq!(ComparisonOp::NotEqual.negate(), ComparisonOp::Equal);
assert_eq!(
ComparisonOp::LessThan.negate(),
ComparisonOp::GreaterOrEqual
);
assert_eq!(
ComparisonOp::GreaterOrEqual.negate(),
ComparisonOp::LessThan
);
}
#[test]
fn test_comparison_op_display() {
assert_eq!(format!("{}", ComparisonOp::Equal), "==");
assert_eq!(format!("{}", ComparisonOp::LessThan), "<");
}
#[test]
fn test_path_constraint_variable() {
let c = X86PathConstraint::Comparison {
lhs: "x".into(),
op: ComparisonOp::Equal,
rhs: 0,
};
assert_eq!(c.variable(), "x");
}
#[test]
fn test_path_constraint_negate_range() {
let c = X86PathConstraint::NonNull("ptr".into());
let negated = c.negate();
match negated {
X86PathConstraint::MustBeNull(v) => assert_eq!(v, "ptr"),
_ => panic!("Expected MustBeNull"),
}
}
#[test]
fn test_path_constraint_range_satisfiable() {
let c = X86PathConstraint::Range {
var: "x".into(),
low: 0,
high: 10,
};
assert!(c.is_satisfiable());
}
#[test]
fn test_path_constraint_range_unsatisfiable() {
let c = X86PathConstraint::Range {
var: "x".into(),
low: 10,
high: 0,
};
assert!(!c.is_satisfiable());
}
#[test]
fn test_analyzer_enable_disable_checker() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
assert!(analyzer.is_checker_enabled("core.NullDereference"));
analyzer.disable_checker("core.NullDereference");
assert!(!analyzer.is_checker_enabled("core.NullDereference"));
analyzer.enable_checker("core.NullDereference");
assert!(analyzer.is_checker_enabled("core.NullDereference"));
}
#[test]
fn test_analyzer_enabled_checker_names() {
let analyzer = X86StaticAnalyzer::with_defaults();
let names = analyzer.enabled_checker_names();
assert!(names.contains(&"core.NullDereference"));
assert!(names.contains(&"security.FormatString"));
}
#[test]
fn test_analyzer_set_max_depth() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
analyzer.set_max_depth(100);
assert_eq!(analyzer.graph.max_depth, 100);
}
#[test]
fn test_analyzer_set_max_nodes() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
analyzer.set_max_nodes(500);
assert_eq!(analyzer.max_nodes, 500);
}
#[test]
fn test_none_constraint_equality() {
assert_eq!(X86NoneConstraint::MustBeNull, X86NoneConstraint::MustBeNull);
assert_ne!(
X86NoneConstraint::MustBeNull,
X86NoneConstraint::MustNotBeNull
);
}
#[test]
fn test_multi_checker_analysis_single_function() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let body = CompoundStmt {
stmts: vec![
Stmt::Decl(Box::new(VarDecl {
name: "p".into(),
ty: QualType::pointer_to(QualType::int()),
init: Some(Box::new(Expr::IntLiteral(0))),
linkage: Linkage::None,
is_global: false,
is_extern: false,
is_static: false,
})),
Stmt::expr(Expr::unary(UnaryOp::Deref, Expr::ident("p"))),
Stmt::expr(Expr::call(Expr::ident("gets"), &[Expr::ident("buf")])),
Stmt::expr(Expr::binary(
BinaryOp::Div,
Expr::IntLiteral(1),
Expr::IntLiteral(0),
)),
],
};
let func = FunctionDecl::new("buggy", QualType::void()).with_body(body);
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "multi_bug.c".into(),
};
analyzer.analyze(&tu);
assert!(analyzer.stats.bugs_found > 0);
}
#[test]
fn test_full_analysis_pipeline_result() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let func = FunctionDecl::new("simple", QualType::int()).with_body(CompoundStmt {
stmts: vec![Stmt::return_(Some(Expr::IntLiteral(0)))],
});
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "simple.c".into(),
};
let result = analyzer.analyze_with_result(&tu);
assert_eq!(result.stats.functions_analyzed, 1);
assert!(result.summary_string().contains("X86 Static Analyzer"));
}
#[test]
fn test_analyzer_analyze_multiple_functions() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let f1 = FunctionDecl::new("init", QualType::void()).with_body(CompoundStmt::new());
let f2 = FunctionDecl::new("main", QualType::int()).with_body(CompoundStmt {
stmts: vec![
Stmt::expr(Expr::call(Expr::ident("init"), &[])),
Stmt::return_(Some(Expr::IntLiteral(0))),
],
});
let tu = TranslationUnit {
decls: vec![Decl::Function(f1), Decl::Function(f2)],
filename: "multi_fn.c".into(),
};
analyzer.analyze(&tu);
assert_eq!(analyzer.stats.functions_analyzed, 2);
}
#[test]
fn test_analyzer_with_subtarget() {
let subtarget = X86Subtarget::new("x86_64-unknown-linux-gnu", "skylake", "");
let mut analyzer = X86StaticAnalyzer::with_subtarget(subtarget);
assert!(analyzer.initial_state.subtarget.is_some());
let func = FunctionDecl::new("f", QualType::void()).with_body(CompoundStmt::new());
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "f.c".into(),
};
analyzer.analyze(&tu);
}
#[test]
fn test_constraint_set_range_narrowing() {
let mut cs = X86ConstraintSet::new();
cs.set_range("x", 0, 100);
cs.set_range("x", 50, 200);
assert_eq!(cs.get_range("x"), Some((50, 100)));
}
#[test]
fn test_constraint_set_taint() {
let mut cs = X86ConstraintSet::new();
cs.set_tainted("data");
assert!(cs.is_tainted("data"));
cs.set_taint_free("data");
assert!(!cs.is_tainted("data"));
}
#[test]
fn test_analyzer_handles_if_branch() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let body = CompoundStmt {
stmts: vec![Stmt::if_(
Expr::ident("x"),
Stmt::expr(Expr::binary(
BinaryOp::Div,
Expr::IntLiteral(1),
Expr::IntLiteral(0),
)),
Some(Stmt::Null),
)],
};
let func = FunctionDecl::new("branch", QualType::void()).with_body(body);
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "branch.c".into(),
};
analyzer.analyze(&tu);
let div_reports = analyzer
.reporter
.reports_by_category(BugCategory::DivideByZero);
assert!(!div_reports.is_empty());
}
#[test]
fn test_analyzer_loop_body_analyzed() {
let mut analyzer = X86StaticAnalyzer::with_defaults();
let body = CompoundStmt {
stmts: vec![Stmt::while_(
Expr::IntLiteral(1),
Stmt::Compound(CompoundStmt {
stmts: vec![
Stmt::Decl(Box::new(VarDecl {
name: "p".into(),
ty: QualType::pointer_to(QualType::int()),
init: Some(Box::new(Expr::IntLiteral(0))),
linkage: Linkage::None,
is_global: false,
is_extern: false,
is_static: false,
})),
Stmt::expr(Expr::unary(UnaryOp::Deref, Expr::ident("p"))),
Stmt::Break,
],
}),
)],
};
let func = FunctionDecl::new("loop_fn", QualType::void()).with_body(body);
let tu = TranslationUnit {
decls: vec![Decl::Function(func)],
filename: "loop.c".into(),
};
analyzer.analyze(&tu);
}
}