use crate::clang::ast::*;
use crate::x86::X86Subtarget;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque};
use std::fmt;
pub type RegionId = u64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
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 {
let s = match self {
X86MemRegionKind::Unknown => "Unknown",
X86MemRegionKind::Stack => "Stack",
X86MemRegionKind::Heap => "Heap",
X86MemRegionKind::Global => "Global",
X86MemRegionKind::StringLiteral => "StringLiteral",
X86MemRegionKind::Code => "Code",
X86MemRegionKind::Field => "Field",
X86MemRegionKind::Element => "Element",
X86MemRegionKind::Pointee => "Pointee",
X86MemRegionKind::Alloca => "Alloca",
X86MemRegionKind::ThreadLocal => "ThreadLocal",
X86MemRegionKind::Block => "Block",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone)]
pub struct X86MemRegion {
pub id: RegionId,
pub kind: X86MemRegionKind,
pub name: String,
pub parent: Option<RegionId>,
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: u64,
pub size_bytes: u64,
}
#[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: RegionId, name: &str, ty: Option<QualType>) -> Self {
Self {
id,
kind: X86MemRegionKind::Stack,
name: name.to_string(),
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: 16,
size_bytes: 0,
}
}
pub fn new_heap(id: RegionId, name: &str, size_bytes: u64) -> Self {
Self {
id,
kind: X86MemRegionKind::Heap,
name: name.to_string(),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: None,
is_read_only: false,
is_thread_local: false,
segment: Some(X86Segment::DS),
alignment: 16,
size_bytes,
}
}
pub fn new_global(id: RegionId, name: &str, ty: Option<QualType>) -> Self {
Self {
id,
kind: X86MemRegionKind::Global,
name: name.to_string(),
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::DS),
alignment: 16,
size_bytes: 0,
}
}
pub fn new_field(
id: RegionId,
parent_id: RegionId,
field_idx: usize,
field_name: &str,
ty: Option<QualType>,
) -> Self {
Self {
id,
kind: X86MemRegionKind::Field,
name: format!(".{field_name}"),
parent: Some(parent_id),
field_idx: Some(field_idx),
field_name: Some(field_name.to_string()),
element_idx: None,
region_type: ty,
is_read_only: false,
is_thread_local: false,
segment: None,
alignment: 8,
size_bytes: 0,
}
}
pub fn new_element(
id: RegionId,
parent_id: RegionId,
index: i64,
ty: Option<QualType>,
) -> Self {
Self {
id,
kind: X86MemRegionKind::Element,
name: format!("[{index}]"),
parent: Some(parent_id),
field_idx: None,
field_name: None,
element_idx: Some(index),
region_type: ty,
is_read_only: false,
is_thread_local: false,
segment: None,
alignment: 8,
size_bytes: 0,
}
}
pub fn new_pointee(id: RegionId, target_name: &str, ty: Option<QualType>) -> Self {
Self {
id,
kind: X86MemRegionKind::Pointee,
name: format!("*{target_name}"),
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: 8,
size_bytes: 0,
}
}
pub fn new_unknown(id: RegionId) -> 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: RegionId) -> Self {
Self {
id,
kind: X86MemRegionKind::StringLiteral,
name: format!(".str_{id}"),
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: Some(X86Segment::DS),
alignment: 1,
size_bytes: 0,
}
}
pub fn new_code(id: RegionId, func_name: &str) -> Self {
Self {
id,
kind: X86MemRegionKind::Code,
name: func_name.to_string(),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: None,
is_read_only: true,
is_thread_local: false,
segment: Some(X86Segment::DS),
alignment: 16,
size_bytes: 0,
}
}
pub fn new_alloca(id: RegionId, size_bytes: u64) -> Self {
Self {
id,
kind: X86MemRegionKind::Alloca,
name: format!("alloca_{id}"),
parent: None,
field_idx: None,
field_name: None,
element_idx: None,
region_type: None,
is_read_only: false,
is_thread_local: false,
segment: Some(X86Segment::SS),
alignment: 16,
size_bytes,
}
}
pub fn is_stack(&self) -> bool {
matches!(
self.kind,
X86MemRegionKind::Stack | X86MemRegionKind::Alloca
)
}
pub fn is_heap(&self) -> bool {
matches!(self.kind, X86MemRegionKind::Heap)
}
pub fn is_global(&self) -> bool {
matches!(
self.kind,
X86MemRegionKind::Global | X86MemRegionKind::ThreadLocal
)
}
pub fn is_sub_region(&self) -> bool {
self.parent.is_some()
}
pub fn root_region(&self, all_regions: &HashMap<RegionId, X86MemRegion>) -> RegionId {
let mut current = self.id;
while let Some(parent_id) = all_regions.get(¤t).and_then(|r| r.parent) {
current = parent_id;
}
current
}
pub fn may_alias(
&self,
other: &X86MemRegion,
all_regions: &HashMap<RegionId, X86MemRegion>,
) -> bool {
if self.id == other.id {
return true;
}
if self.kind == X86MemRegionKind::Unknown || other.kind == X86MemRegionKind::Unknown {
return true;
}
self.root_region(all_regions) == other.root_region(all_regions)
}
pub fn x86_size(&self) -> u64 {
if self.size_bytes > 0 {
return self.size_bytes;
}
self.region_type
.as_ref()
.and_then(|ty| Some(ty.base.size_bytes() as u64))
.unwrap_or(0)
}
}
impl fmt::Display for X86MemRegion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}[{}] {}", self.kind, self.id, self.name)?;
if self.is_read_only {
write!(f, " (RO)")?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum X86SVal {
Undefined,
Unknown,
ConcreteInt(i64, Option<i64>, Option<i64>),
ConcreteUInt(u64),
ConcreteFloat(FloatVal),
Loc(RegionId),
Null,
SymbolInt(String),
SymbolRegion(RegionId),
Zero,
LazyCompound(RegionId),
SymbolExpr(String, Vec<X86SVal>),
}
#[derive(Debug, Clone)]
pub struct FloatVal {
pub bits: u64,
pub width: u8,
}
impl Eq for FloatVal {}
impl PartialEq for FloatVal {
fn eq(&self, other: &Self) -> bool {
self.bits == other.bits && self.width == other.width
}
}
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(bit_pattern: u32) -> Self {
Self {
bits: bit_pattern as u64,
width: 32,
}
}
pub fn f64(bit_pattern: u64) -> Self {
Self {
bits: bit_pattern,
width: 64,
}
}
pub fn is_nan(&self) -> bool {
match self.width {
32 => {
let exp = (self.bits >> 23) & 0xFF;
let mantissa = self.bits & 0x7FFFFF;
exp == 0xFF && mantissa != 0
}
64 => {
let exp = (self.bits >> 52) & 0x7FF;
let mantissa = self.bits & 0xFFFFFFFFFFFFF;
exp == 0x7FF && mantissa != 0
}
_ => false,
}
}
pub fn is_infinity(&self) -> bool {
match self.width {
32 => (self.bits & 0x7FFFFFFF) == 0x7F800000,
64 => (self.bits & 0x7FFFFFFFFFFFFFFF) == 0x7FF0000000000000,
_ => false,
}
}
pub fn is_zero(&self) -> bool {
match self.width {
32 => (self.bits & 0x7FFFFFFF) == 0,
64 => (self.bits & 0x7FFFFFFFFFFFFFFF) == 0,
_ => false,
}
}
}
impl X86SVal {
pub fn is_undefined(&self) -> bool {
matches!(self, X86SVal::Undefined)
}
pub fn is_null(&self) -> bool {
matches!(self, X86SVal::Null)
}
pub fn is_zero(&self) -> bool {
matches!(self, X86SVal::Zero | X86SVal::Null)
|| matches!(self, X86SVal::ConcreteInt(0, _, _))
}
pub fn is_concrete(&self) -> bool {
matches!(
self,
X86SVal::ConcreteInt(_, _, _)
| X86SVal::ConcreteUInt(_)
| X86SVal::ConcreteFloat(_)
| X86SVal::Zero
| X86SVal::Null
)
}
pub fn is_known(&self) -> bool {
!matches!(self, X86SVal::Undefined | X86SVal::Unknown)
}
pub fn as_int(&self) -> Option<i64> {
match self {
X86SVal::ConcreteInt(v, _, _) => Some(*v),
X86SVal::ConcreteUInt(v) => Some(*v as i64),
X86SVal::Zero => Some(0),
_ => None,
}
}
pub fn as_uint(&self) -> Option<u64> {
match self {
X86SVal::ConcreteInt(v, _, _) if *v >= 0 => Some(*v as u64),
X86SVal::ConcreteUInt(v) => Some(*v),
X86SVal::Zero => Some(0),
_ => None,
}
}
pub fn as_loc(&self) -> Option<RegionId> {
match self {
X86SVal::Loc(id) => Some(*id),
_ => None,
}
}
pub fn constraint_key(&self) -> String {
match self {
X86SVal::SymbolInt(s) => s.clone(),
X86SVal::SymbolExpr(s, _) => s.clone(),
_ => format!("{self:?}"),
}
}
}
impl fmt::Display for X86SVal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
X86SVal::Undefined => write!(f, "Undefined"),
X86SVal::Unknown => write!(f, "Unknown"),
X86SVal::ConcreteInt(v, _, _) => write!(f, "{v}"),
X86SVal::ConcreteUInt(v) => write!(f, "{v}u"),
X86SVal::ConcreteFloat(fv) => write!(f, "float({:x})", fv.bits),
X86SVal::Loc(rid) => write!(f, "&R{rid}"),
X86SVal::Null => write!(f, "null"),
X86SVal::SymbolInt(s) => write!(f, "${s}"),
X86SVal::SymbolRegion(rid) => write!(f, "symR{rid}"),
X86SVal::Zero => write!(f, "0"),
X86SVal::LazyCompound(rid) => write!(f, "lazy{{R{rid}}}"),
X86SVal::SymbolExpr(s, args) => {
write!(f, "{s}(")?;
for (i, a) in args.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{a}")?;
}
write!(f, ")")
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct X86RangeConstraint {
pub low: i64,
pub high: i64,
}
impl X86RangeConstraint {
pub fn new(low: i64, high: i64) -> Self {
Self { low, high }
}
pub fn intersect(&self, other: &X86RangeConstraint) -> Option<X86RangeConstraint> {
let low = self.low.max(other.low);
let high = self.high.min(other.high);
if low <= high {
Some(X86RangeConstraint { low, high })
} else {
None
}
}
pub fn union(&self, other: &X86RangeConstraint) -> Option<X86RangeConstraint> {
if self.high + 1 >= other.low && self.low <= other.high + 1 {
Some(X86RangeConstraint {
low: self.low.min(other.low),
high: self.high.max(other.high),
})
} else {
None
}
}
pub fn contains(&self, v: i64) -> bool {
v >= self.low && v <= self.high
}
pub fn is_singleton(&self) -> bool {
self.low == self.high
}
pub fn width(&self) -> u64 {
((self.high as i128 - self.low as i128).unsigned_abs() as u64)
}
}
impl fmt::Display for X86RangeConstraint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}, {}]", self.low, self.high)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct X86DisequalityConstraint {
pub excluded: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86Nullability {
Null,
NonNull,
Unknown,
}
#[derive(Debug, Clone)]
pub struct X86SymbolConstraint {
pub range: Option<X86RangeConstraint>,
pub excluded: BTreeSet<i64>,
pub nullability: X86Nullability,
pub is_tainted: bool,
}
impl Default for X86SymbolConstraint {
fn default() -> Self {
Self {
range: None,
excluded: BTreeSet::new(),
nullability: X86Nullability::Unknown,
is_tainted: false,
}
}
}
impl X86SymbolConstraint {
pub fn new() -> Self {
Self::default()
}
pub fn satisfies(&self, v: i64) -> bool {
if let Some(ref range) = self.range {
if !range.contains(v) {
return false;
}
}
if self.excluded.contains(&v) {
return false;
}
true
}
pub fn is_satisfiable(&self) -> bool {
if self.nullability == X86Nullability::Null {
return self.satisfies(0);
}
if let Some(ref range) = self.range {
let mut count = 0i64;
for v in range.low..=range.high.min(range.low + 1000) {
if !self.excluded.contains(&v) {
count += 1;
if count > 0 {
return true;
}
}
}
return count > 0;
}
true
}
pub fn intersect(&mut self, other: &X86SymbolConstraint) {
if let (Some(a), Some(b)) = (&self.range, &other.range) {
self.range = a.intersect(b);
} else if let Some(ref b) = other.range {
self.range = Some(b.clone());
}
self.excluded.extend(&other.excluded);
match (self.nullability, other.nullability) {
(X86Nullability::Null, X86Nullability::NonNull)
| (X86Nullability::NonNull, X86Nullability::Null) => {
self.range = Some(X86RangeConstraint { low: 1, high: 0 });
}
(X86Nullability::Unknown, _) | (_, X86Nullability::Unknown) => {
}
_ => {}
}
self.is_tainted |= other.is_tainted;
}
pub fn union(&mut self, other: &X86SymbolConstraint) {
if let (Some(a), Some(b)) = (&self.range, &other.range) {
self.range = a.union(b);
} else {
self.range = None;
}
self.excluded = self
.excluded
.intersection(&other.excluded)
.cloned()
.collect();
match (self.nullability, other.nullability) {
(X86Nullability::Null, X86Nullability::Null) => {}
(X86Nullability::NonNull, X86Nullability::NonNull) => {}
_ => self.nullability = X86Nullability::Unknown,
}
self.is_tainted |= other.is_tainted;
}
pub fn exclude(&mut self, v: i64) {
self.excluded.insert(v);
}
pub fn set_nullability(&mut self, n: X86Nullability) {
self.nullability = n;
}
pub fn set_range(&mut self, low: i64, high: i64) {
self.range = Some(X86RangeConstraint { low, high });
}
pub fn narrow_range(&mut self, low: i64, high: i64) {
let new = X86RangeConstraint { low, high };
if let Some(ref existing) = self.range {
self.range = existing.intersect(&new);
} else {
self.range = Some(new);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct X86ConstraintSet {
pub ranges: HashMap<String, X86RangeConstraint>,
pub equalities: HashMap<String, i64>,
pub disequalities: HashMap<String, BTreeSet<i64>>,
pub nullability: HashMap<String, X86Nullability>,
pub tainted: HashSet<String>,
pub taint_free: HashSet<String>,
pub symbol_constraints: HashMap<String, X86SymbolConstraint>,
}
impl X86ConstraintSet {
pub fn new() -> Self {
Self::default()
}
pub fn set_range(&mut self, var: &str, low: i64, high: i64) {
self.symbol_constraints
.entry(var.to_string())
.or_default()
.narrow_range(low, high);
self.ranges
.insert(var.to_string(), X86RangeConstraint { low, high });
}
pub fn get_range(&self, var: &str) -> Option<&X86RangeConstraint> {
self.ranges.get(var)
}
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_disequality(&mut self, var: &str, excluded: i64) {
self.disequalities
.entry(var.to_string())
.or_default()
.insert(excluded);
self.symbol_constraints
.entry(var.to_string())
.or_default()
.exclude(excluded);
}
pub fn set_nullability(&mut self, var: &str, n: X86Nullability) {
self.nullability.insert(var.to_string(), n);
self.symbol_constraints
.entry(var.to_string())
.or_default()
.set_nullability(n);
}
pub fn get_nullability(&self, var: &str) -> X86Nullability {
self.nullability
.get(var)
.copied()
.unwrap_or(X86Nullability::Unknown)
}
pub fn is_null(&self, var: &str) -> bool {
self.get_nullability(var) == X86Nullability::Null
}
pub fn is_non_null(&self, var: &str) -> bool {
self.get_nullability(var) == X86Nullability::NonNull
}
pub fn set_tainted(&mut self, var: &str) {
self.tainted.insert(var.to_string());
self.symbol_constraints
.entry(var.to_string())
.or_default()
.is_tainted = true;
}
pub fn set_taint_free(&mut self, var: &str) {
self.taint_free.insert(var.to_string());
}
pub fn is_tainted(&self, var: &str) -> bool {
self.tainted.contains(var)
|| self
.symbol_constraints
.get(var)
.map(|c| c.is_tainted)
.unwrap_or(false)
}
pub fn merge(&mut self, other: &X86ConstraintSet, use_union: bool) {
if use_union {
for (var, range) in &other.ranges {
if let Some(existing) = self.ranges.get(var) {
if let Some(merged) = existing.union(range) {
self.ranges.insert(var.clone(), merged);
} else {
self.ranges.remove(var);
}
}
}
for (var, excluded) in &other.disequalities {
let mine = self.disequalities.entry(var.clone()).or_default();
*mine = mine.intersection(excluded).cloned().collect();
}
for (var, n) in &other.nullability {
if self.nullability.get(var) != Some(n) {
self.nullability
.insert(var.clone(), X86Nullability::Unknown);
}
}
} else {
for (var, range) in &other.ranges {
if let Some(existing) = self.ranges.get(var) {
if let Some(narrowed) = existing.intersect(range) {
self.ranges.insert(var.clone(), narrowed);
}
} else {
self.ranges.insert(var.clone(), range.clone());
}
}
for (var, excluded) in &other.disequalities {
self.disequalities
.entry(var.clone())
.or_default()
.extend(excluded);
}
for (var, n) in &other.nullability {
self.nullability.entry(var.clone()).or_insert(*n);
}
}
self.tainted.extend(other.tainted.iter().cloned());
self.taint_free.extend(other.taint_free.iter().cloned());
}
pub fn is_satisfiable(&self) -> bool {
for sc in self.symbol_constraints.values() {
if !sc.is_satisfiable() {
return false;
}
}
true
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum X86PathConstraint {
Comparison(String, ComparisonOp, i64),
Range(String, i64, i64),
NonNull(String),
MustBeNull(String),
IsTainted(String),
NotTainted(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ComparisonOp {
LessThan,
LessOrEqual,
GreaterThan,
GreaterOrEqual,
Equal,
NotEqual,
}
impl ComparisonOp {
pub fn from_binary_op(op: &BinaryOp) -> Option<ComparisonOp> {
match op {
BinaryOp::Eq => Some(ComparisonOp::Equal),
BinaryOp::Ne => Some(ComparisonOp::NotEqual),
BinaryOp::Lt => Some(ComparisonOp::LessThan),
BinaryOp::Gt => Some(ComparisonOp::GreaterThan),
BinaryOp::Le => Some(ComparisonOp::LessOrEqual),
BinaryOp::Ge => Some(ComparisonOp::GreaterOrEqual),
_ => None,
}
}
pub fn negate(&self) -> ComparisonOp {
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, _constraints: &X86ConstraintSet) -> bool {
true
}
pub fn variable(&self) -> &str {
match self {
X86PathConstraint::Comparison(v, _, _)
| X86PathConstraint::Range(v, _, _)
| X86PathConstraint::NonNull(v)
| X86PathConstraint::MustBeNull(v)
| X86PathConstraint::IsTainted(v)
| X86PathConstraint::NotTainted(v) => v.as_str(),
}
}
pub fn negate(&self) -> X86PathConstraint {
match self {
X86PathConstraint::Comparison(v, op, rhs) => {
X86PathConstraint::Comparison(v.clone(), op.negate(), *rhs)
}
X86PathConstraint::Range(v, _, _) => X86PathConstraint::NonNull(v.clone()),
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()),
}
}
}
#[derive(Debug, Clone)]
pub struct X86ProgramState {
pub values: HashMap<String, X86SVal>,
pub region_store: HashMap<RegionId, X86SVal>,
pub constraints: X86ConstraintSet,
pub allocated: HashSet<RegionId>,
pub freed: HashSet<RegionId>,
pub ref_counts: HashMap<String, i32>,
pub held_locks: HashSet<String>,
pub var_regions: HashMap<String, RegionId>,
pub tainted: HashSet<String>,
pub infeasible: bool,
pub is_dead: bool,
pub depth: u64,
pub stmt_idx: u64,
pub subtarget: Option<X86Subtarget>,
next_region_id: RegionId,
pub generic_data: HashMap<String, String>,
}
impl X86ProgramState {
pub fn new() -> Self {
Self {
values: HashMap::new(),
region_store: HashMap::new(),
constraints: X86ConstraintSet::new(),
allocated: HashSet::new(),
freed: HashSet::new(),
ref_counts: HashMap::new(),
held_locks: HashSet::new(),
var_regions: HashMap::new(),
tainted: HashSet::new(),
infeasible: false,
is_dead: false,
depth: 0,
stmt_idx: 0,
subtarget: None,
next_region_id: 1000,
generic_data: HashMap::new(),
}
}
pub fn with_subtarget(subtarget: X86Subtarget) -> Self {
let mut state = Self::new();
state.subtarget = Some(subtarget);
state
}
pub fn bind_value(&mut self, name: &str, val: X86SVal) {
self.values.insert(name.to_string(), val);
}
pub fn lookup_value(&self, name: &str) -> X86SVal {
self.values.get(name).cloned().unwrap_or(X86SVal::Unknown)
}
pub fn store_region(&mut self, region: RegionId, val: X86SVal) {
self.region_store.insert(region, val);
}
pub fn load_region(&self, region: RegionId) -> X86SVal {
self.region_store
.get(®ion)
.cloned()
.unwrap_or(X86SVal::Unknown)
}
pub fn assume_true(&mut self, constraint: &X86PathConstraint) {
match constraint {
X86PathConstraint::Comparison(var, op, rhs) => match op {
ComparisonOp::Equal => {
self.constraints.set_equal(var, *rhs);
}
ComparisonOp::NotEqual => {
self.constraints.add_disequality(var, *rhs);
}
ComparisonOp::LessThan => {
if let Some(existing) = self.constraints.get_range(var) {
self.constraints
.set_range(var, existing.low, existing.high.min(rhs - 1));
} else {
self.constraints.set_range(var, i64::MIN, rhs - 1);
}
}
ComparisonOp::LessOrEqual => {
if let Some(existing) = self.constraints.get_range(var) {
self.constraints
.set_range(var, existing.low, existing.high.min(*rhs));
} else {
self.constraints.set_range(var, i64::MIN, *rhs);
}
}
ComparisonOp::GreaterThan => {
if let Some(existing) = self.constraints.get_range(var) {
self.constraints
.set_range(var, existing.low.max(rhs + 1), existing.high);
} else {
self.constraints.set_range(var, rhs + 1, i64::MAX);
}
}
ComparisonOp::GreaterOrEqual => {
if let Some(existing) = self.constraints.get_range(var) {
self.constraints
.set_range(var, existing.low.max(*rhs), existing.high);
} else {
self.constraints.set_range(var, *rhs, i64::MAX);
}
}
},
X86PathConstraint::Range(var, low, high) => {
self.constraints.set_range(var, *low, *high);
}
X86PathConstraint::NonNull(var) => {
self.constraints
.set_nullability(var, X86Nullability::NonNull);
}
X86PathConstraint::MustBeNull(var) => {
self.constraints.set_nullability(var, X86Nullability::Null);
}
X86PathConstraint::IsTainted(var) => {
self.constraints.set_tainted(var);
}
X86PathConstraint::NotTainted(var) => {
self.constraints.set_taint_free(var);
}
}
}
pub fn assume_false(&mut self, constraint: &X86PathConstraint) {
self.assume_true(&constraint.negate());
}
pub fn is_constrained_true(&self, constraint: &X86PathConstraint) -> bool {
match constraint {
X86PathConstraint::Comparison(var, op, rhs) => match op {
ComparisonOp::Equal => self.constraints.get_equal(var) == Some(*rhs),
ComparisonOp::NotEqual => self
.constraints
.disequalities
.get(var)
.map(|s| s.contains(rhs))
.unwrap_or(false),
_ => false,
},
X86PathConstraint::NonNull(var) => self.constraints.is_non_null(var),
X86PathConstraint::MustBeNull(var) => self.constraints.is_null(var),
X86PathConstraint::IsTainted(var) => self.constraints.is_tainted(var),
_ => false,
}
}
pub fn is_constrained_false(&self, constraint: &X86PathConstraint) -> bool {
self.is_constrained_true(&constraint.negate())
}
pub fn allocate(&mut self, name: &str, size: u64) -> RegionId {
let id = self.next_region_id();
let region = X86MemRegion::new_heap(id, name, size);
self.region_store.insert(id, X86SVal::Unknown);
self.allocated.insert(id);
self.var_regions.insert(name.to_string(), id);
id
}
pub fn free(&mut self, name: &str) {
if let Some(&rid) = self.var_regions.get(name) {
self.allocated.remove(&rid);
self.freed.insert(rid);
self.region_store.insert(rid, X86SVal::Undefined);
}
}
pub fn is_allocated(&self, name: &str) -> bool {
self.var_regions
.get(name)
.map(|rid| self.allocated.contains(rid))
.unwrap_or(false)
}
pub fn is_freed(&self, name: &str) -> bool {
self.var_regions
.get(name)
.map(|rid| self.freed.contains(rid))
.unwrap_or(false)
}
pub fn retain(&mut self, obj: &str) {
*self.ref_counts.entry(obj.to_string()).or_insert(0) += 1;
}
pub fn release(&mut self, obj: &str) {
let entry = self.ref_counts.entry(obj.to_string()).or_insert(0);
*entry = (*entry - 1).max(-1);
}
pub fn retain_count(&self, obj: &str) -> i32 {
self.ref_counts.get(obj).copied().unwrap_or(0)
}
pub fn acquire_lock(&mut self, lock: &str) {
self.held_locks.insert(lock.to_string());
}
pub fn release_lock(&mut self, lock: &str) {
self.held_locks.remove(lock);
}
pub fn lock_held(&self, lock: &str) -> bool {
self.held_locks.contains(lock)
}
pub fn register_var_region(&mut self, name: &str, region: RegionId) {
self.var_regions.insert(name.to_string(), region);
}
pub fn var_region(&self, name: &str) -> Option<RegionId> {
self.var_regions.get(name).copied()
}
pub fn taint(&mut self, name: &str) {
self.tainted.insert(name.to_string());
self.constraints.set_tainted(name);
}
pub fn is_tainted(&self, name: &str) -> bool {
self.tainted.contains(name) || self.constraints.is_tainted(name)
}
pub fn propagate_taint(&mut self, dst: &str, src: &str) {
if self.is_tainted(src) {
self.taint(dst);
}
}
pub fn mark_infeasible(&mut self) {
self.infeasible = true;
}
pub fn mark_dead(&mut self) {
self.is_dead = true;
}
pub fn advance(mut self) -> Self {
self.depth += 1;
self.stmt_idx += 1;
self
}
pub fn merge(&self, other: &X86ProgramState) -> X86ProgramState {
let mut merged = X86ProgramState::new();
merged.depth = self.depth.max(other.depth);
merged.stmt_idx = self.stmt_idx.max(other.stmt_idx);
merged.subtarget = self.subtarget.clone().or(other.subtarget.clone());
merged.next_region_id = self.next_region_id.max(other.next_region_id);
for (k, v) in &self.values {
if other.values.get(k) == Some(v) {
merged.values.insert(k.clone(), v.clone());
}
}
for (k, v) in &self.region_store {
if other.region_store.get(k) == Some(v) {
merged.region_store.insert(*k, v.clone());
}
}
merged.allocated = self
.allocated
.intersection(&other.allocated)
.cloned()
.collect();
merged.freed = self.freed.union(&other.freed).cloned().collect();
for (k, v) in &self.ref_counts {
if let Some(ov) = other.ref_counts.get(k) {
merged.ref_counts.insert(k.clone(), *v.min(ov));
}
}
merged.held_locks = self
.held_locks
.intersection(&other.held_locks)
.cloned()
.collect();
for (k, v) in &self.var_regions {
if other.var_regions.get(k) == Some(v) {
merged.var_regions.insert(k.clone(), *v);
}
}
merged.tainted = self.tainted.union(&other.tainted).cloned().collect();
merged.constraints = self.constraints.clone();
merged.constraints.merge(&other.constraints, true);
if self.infeasible || self.is_dead {
return other.clone();
}
if other.infeasible || other.is_dead {
return self.clone();
}
merged
}
fn next_region_id(&mut self) -> RegionId {
let id = self.next_region_id;
self.next_region_id += 1;
id
}
pub fn set_generic(&mut self, key: &str, value: &str) {
self.generic_data.insert(key.to_string(), value.to_string());
}
pub fn get_generic(&self, key: &str) -> Option<&str> {
self.generic_data.get(key).map(|s| s.as_str())
}
}
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: u64,
}
#[derive(Debug, Clone)]
pub struct X86ExplodedEdge {
pub src: u64,
pub dst: u64,
pub condition: String,
pub kind: X86EdgeKind,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86EdgeKind {
Fallthrough,
TrueBranch,
FalseBranch,
Call,
Return,
Case,
Loop,
Unwind,
}
#[derive(Debug, Clone)]
pub struct X86ExplodedGraph {
pub nodes: HashMap<u64, X86ExplodedNode>,
pub edges: Vec<X86ExplodedEdge>,
pub adj: HashMap<u64, Vec<u64>>,
pub rev_adj: HashMap<u64, Vec<u64>>,
pub root_id: u64,
pub next_id: u64,
pub worklist: VecDeque<u64>,
pub max_depth: u64,
pub current_function: Option<String>,
}
impl X86ExplodedGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
adj: HashMap::new(),
rev_adj: HashMap::new(),
root_id: 0,
next_id: 1,
worklist: VecDeque::new(),
max_depth: 1000,
current_function: None,
}
}
pub fn create_root(&mut self, state: X86ProgramState) -> u64 {
let id = self.next_id;
self.next_id += 1;
self.root_id = id;
let node = X86ExplodedNode {
id,
stmt_label: "entry".into(),
state,
processed: false,
exec_count: 1,
};
self.nodes.insert(id, node);
self.adj.insert(id, Vec::new());
self.rev_adj.insert(id, Vec::new());
self.worklist.push_back(id);
id
}
pub fn add_node(
&mut self,
parent_id: u64,
label: String,
state: X86ProgramState,
edge_kind: X86EdgeKind,
condition: &str,
) -> u64 {
if state.depth >= self.max_depth {
return 0;
}
if !state.constraints.is_satisfiable() {
return 0;
}
for (&existing_id, existing_node) in &self.nodes {
if existing_node.stmt_label == label
&& Self::states_equivalent(&existing_node.state, &state)
{
self.add_edge(parent_id, existing_id, edge_kind, condition);
return existing_id;
}
}
let id = self.next_id;
self.next_id += 1;
let node = X86ExplodedNode {
id,
stmt_label: label,
state,
processed: false,
exec_count: 1,
};
self.nodes.insert(id, node);
self.adj.insert(id, Vec::new());
self.rev_adj.insert(id, Vec::new());
self.add_edge(parent_id, id, edge_kind, condition);
self.worklist.push_back(id);
id
}
fn add_edge(&mut self, src: u64, dst: u64, kind: X86EdgeKind, condition: &str) {
let edge = X86ExplodedEdge {
src,
dst,
condition: condition.to_string(),
kind,
};
self.edges.push(edge);
self.adj.entry(src).or_default().push(dst);
self.rev_adj.entry(dst).or_default().push(src);
}
fn states_equivalent(a: &X86ProgramState, b: &X86ProgramState) -> bool {
if a.infeasible != b.infeasible || a.is_dead != b.is_dead {
return false;
}
if a.values.len() != b.values.len() {
return false;
}
for (k, v) in &a.values {
if b.values.get(k) != Some(v) {
return false;
}
}
for (k, v) in &a.region_store {
if b.region_store.get(k) != Some(v) {
return false;
}
}
if a.held_locks != b.held_locks {
return false;
}
if a.allocated != b.allocated {
return false;
}
true
}
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).cloned().unwrap_or_default()
}
pub fn predecessors(&self, node_id: u64) -> Vec<u64> {
self.rev_adj.get(&node_id).cloned().unwrap_or_default()
}
pub fn node_count(&self) -> usize {
self.nodes.len()
}
pub fn edge_count(&self) -> usize {
self.edges.len()
}
pub fn path_to_root(&self, node_id: u64) -> Vec<u64> {
let mut path = Vec::new();
let mut current = node_id;
let mut visited = HashSet::new();
loop {
if visited.contains(¤t) {
break;
}
visited.insert(current);
path.push(current);
if current == self.root_id {
break;
}
let preds = self.predecessors(current);
if preds.is_empty() {
break;
}
current = preds[0];
}
path.reverse();
path
}
pub fn sink_nodes(&self) -> Vec<&X86ExplodedNode> {
self.nodes
.values()
.filter(|n| {
self.successors(n.id).is_empty()
&& (n.state.infeasible
|| n.state.is_dead
|| n.state.values.values().any(|v| v.is_undefined()))
})
.collect()
}
}
impl Default for X86ExplodedGraph {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum BugSeverity {
Note = 0,
Style = 1,
Warning = 2,
Error = 3,
Critical = 4,
}
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"),
}
}
}
impl BugSeverity {
pub fn to_sarif_level(&self) -> &'static str {
match self {
BugSeverity::Note => "note",
BugSeverity::Style => "note",
BugSeverity::Warning => "warning",
BugSeverity::Error => "error",
BugSeverity::Critical => "error",
}
}
}
#[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,
DeadStore,
API,
Iterator,
CallAndMessage,
General,
}
impl fmt::Display for BugCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
BugCategory::NullDereference => "Null Dereference",
BugCategory::UndefinedBehavior => "Undefined Behavior",
BugCategory::DivideByZero => "Divide By Zero",
BugCategory::Uninitialized => "Uninitialized Value",
BugCategory::ArrayBounds => "Array Bounds",
BugCategory::StackAddressEscape => "Stack Address Escape",
BugCategory::Memory => "Memory Error",
BugCategory::VLASize => "VLA Size",
BugCategory::InvalidCast => "Invalid Cast",
BugCategory::EnumRange => "Enum Range",
BugCategory::NonNullViolation => "Nonnull Violation",
BugCategory::FormatString => "Format String",
BugCategory::ShiftError => "Shift Error",
BugCategory::SignedOverflow => "Signed Overflow",
BugCategory::FloatLoopCounter => "Float Loop Counter",
BugCategory::Locking => "Locking",
BugCategory::RetainCount => "Retain Count",
BugCategory::UnreachableCode => "Unreachable Code",
BugCategory::Security => "Security",
BugCategory::Taint => "Taint",
BugCategory::DeadStore => "Dead Store",
BugCategory::API => "API Misuse",
BugCategory::Iterator => "Iterator",
BugCategory::CallAndMessage => "Call And Message",
BugCategory::General => "General",
};
write!(f, "{s}")
}
}
#[derive(Debug, Clone)]
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: String,
pub file: String,
pub line: u32,
pub column: u32,
pub path: Vec<PathDiagnosticPiece>,
pub is_duplicate: bool,
pub cwe_id: Option<u32>,
}
impl X86BugReport {
pub fn new(
checker_name: &str,
category: BugCategory,
severity: BugSeverity,
title: &str,
description: &str,
file: &str,
line: u32,
column: u32,
) -> Self {
Self {
id: 0,
checker_name: checker_name.to_string(),
category,
severity,
title: title.to_string(),
description: description.to_string(),
function: String::new(),
file: file.to_string(),
line,
column,
path: Vec::new(),
is_duplicate: false,
cwe_id: None,
}
}
pub fn with_function(mut self, func: &str) -> Self {
self.function = func.to_string();
self
}
pub fn add_path_piece(&mut self, piece: PathDiagnosticPiece) {
self.path.push(piece);
}
pub fn add_note(&mut self, message: &str, file: &str, line: u32, column: u32) {
self.path.push(PathDiagnosticPiece {
message: message.to_string(),
file: file.to_string(),
line,
column,
is_bug_point: false,
fixit: None,
});
}
pub fn add_bug_point(
&mut self,
message: &str,
file: &str,
line: u32,
column: u32,
fixit: Option<FixItHint>,
) {
self.path.push(PathDiagnosticPiece {
message: message.to_string(),
file: file.to_string(),
line,
column,
is_bug_point: true,
fixit,
});
}
pub fn add_fixit(&mut self, fixit: FixItHint) {
if let Some(last) = self.path.last_mut() {
last.fixit = Some(fixit);
}
}
pub fn with_cwe(mut self, cwe_id: u32) -> Self {
self.cwe_id = Some(cwe_id);
self
}
}
impl fmt::Display for X86BugReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}: {} [{}] {}:{}:{}",
self.severity, self.title, self.checker_name, self.file, self.line, self.column
)?;
if !self.function.is_empty() {
write!(f, " in {}", self.function)?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
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: 1,
seen_signatures: HashSet::new(),
max_reports_per_function: 10,
emit_path_diagnostics: true,
}
}
pub fn emit(&mut self, report: &mut 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);
let func_count = self
.reports
.iter()
.filter(|r| r.function == report.function)
.count();
if func_count >= self.max_reports_per_function {
return false;
}
report.id = self.next_report_id;
self.next_report_id += 1;
self.reports.push(report.clone());
true
}
fn make_signature(report: &X86BugReport) -> String {
format!(
"{}|{}|{}|{}|{}|{}",
report.checker_name,
report.category,
report.title,
report.function,
report.file,
report.line
)
}
pub fn emit_simple(
&mut self,
checker_name: &str,
category: BugCategory,
severity: BugSeverity,
title: &str,
description: &str,
file: &str,
line: u32,
column: u32,
) {
let mut report = X86BugReport::new(
checker_name,
category,
severity,
title,
description,
file,
line,
column,
);
self.emit(&mut report);
}
pub fn sorted_reports(&self) -> Vec<&X86BugReport> {
let mut sorted: Vec<&X86BugReport> = self.reports.iter().collect();
sorted.sort_by(|a, b| b.severity.cmp(&a.severity));
sorted
}
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)
.collect()
}
pub fn reports_by_category(&self, category: BugCategory) -> Vec<&X86BugReport> {
self.reports
.iter()
.filter(|r| r.category == category)
.collect()
}
pub fn summary(&self) -> String {
let mut s = String::new();
s.push_str("=== Bug Report Summary ===\n");
s.push_str(&format!("Total reports: {}\n", self.reports.len()));
for severity in &[
BugSeverity::Critical,
BugSeverity::Error,
BugSeverity::Warning,
BugSeverity::Style,
BugSeverity::Note,
] {
let count = self.reports_by_severity(*severity).len();
if count > 0 {
s.push_str(&format!(" {severity}: {count}\n"));
}
}
s
}
pub fn sarif_version(&self) -> &'static str {
"2.1.0"
}
pub fn to_sarif(&self, tool_name: &str, tool_version: &str) -> String {
let mut sarif = String::new();
sarif.push_str("{\n");
sarif.push_str(" \"$schema\": \"https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json\",\n");
sarif.push_str(&format!(" \"version\": \"{}\",\n", self.sarif_version()));
sarif.push_str(" \"runs\": [{\n");
sarif.push_str(" \"tool\": {\n");
sarif.push_str(" \"driver\": {\n");
sarif.push_str(&format!(
" \"name\": \"{}\",\n",
Self::sarif_escape(tool_name)
));
sarif.push_str(&format!(
" \"version\": \"{}\",\n",
Self::sarif_escape(tool_version)
));
sarif.push_str(&format!(
" \"informationUri\": \"https://github.com/llvm-native/llvm-native\",\n"
));
sarif.push_str(" \"rules\": [\n");
let mut rules: BTreeMap<String, (&str, BugCategory)> = BTreeMap::new();
for report in &self.reports {
rules
.entry(report.checker_name.clone())
.or_insert((&report.title, report.category));
}
let rule_names: Vec<String> = rules.keys().cloned().collect();
for (i, rule_name) in rule_names.iter().enumerate() {
let (title, _category) = rules[rule_name];
sarif.push_str(" {\n");
sarif.push_str(&format!(
" \"id\": \"{}\",\n",
Self::sarif_escape(rule_name)
));
sarif.push_str(" \"shortDescription\": {\n");
sarif.push_str(&format!(
" \"text\": \"{}\"\n",
Self::sarif_escape(title)
));
sarif.push_str(" }\n");
if i + 1 < rule_names.len() {
sarif.push_str(" },\n");
} else {
sarif.push_str(" }\n");
}
}
sarif.push_str(" ]\n");
sarif.push_str(" }\n");
sarif.push_str(" },\n");
sarif.push_str(" \"results\": [\n");
for (i, report) in self.reports.iter().enumerate() {
sarif.push_str(" {\n");
sarif.push_str(&format!(
" \"ruleId\": \"{}\",\n",
Self::sarif_escape(&report.checker_name)
));
sarif.push_str(&format!(
" \"level\": \"{}\",\n",
report.severity.to_sarif_level()
));
sarif.push_str(" \"message\": {\n");
sarif.push_str(&format!(
" \"text\": \"{}\"\n",
Self::sarif_escape(&report.description)
));
sarif.push_str(" },\n");
sarif.push_str(" \"locations\": [{\n");
sarif.push_str(" \"physicalLocation\": {\n");
sarif.push_str(" \"artifactLocation\": {\n");
sarif.push_str(&format!(
" \"uri\": \"{}\"\n",
Self::sarif_escape(&report.file)
));
sarif.push_str(" },\n");
sarif.push_str(" \"region\": {\n");
sarif.push_str(&format!(" \"startLine\": {},\n", report.line));
sarif.push_str(&format!(
" \"startColumn\": {}\n",
report.column
));
sarif.push_str(" }\n");
sarif.push_str(" }\n");
sarif.push_str(" }],\n");
sarif.push_str(" \"properties\": {\n");
sarif.push_str(&format!(
" \"category\": \"{}\",\n",
Self::sarif_escape(&report.category.to_string())
));
if let Some(cwe) = report.cwe_id {
sarif.push_str(&format!(" \"cwe\": {cwe},\n"));
}
sarif.push_str(&format!(
" \"severity\": \"{}\"\n",
Self::sarif_escape(&report.severity.to_string())
));
sarif.push_str(" }");
if !report.path.is_empty() && self.emit_path_diagnostics {
sarif.push_str(",\n \"relatedLocations\": [\n");
for (j, piece) in report.path.iter().enumerate() {
sarif.push_str(" {\n");
sarif.push_str(" \"physicalLocation\": {\n");
sarif.push_str(" \"artifactLocation\": {\n");
sarif.push_str(&format!(
" \"uri\": \"{}\"\n",
Self::sarif_escape(&piece.file)
));
sarif.push_str(" },\n");
sarif.push_str(" \"region\": {\n");
sarif.push_str(&format!(" \"startLine\": {},\n", piece.line));
sarif.push_str(&format!(
" \"startColumn\": {}\n",
piece.column
));
sarif.push_str(" }\n");
sarif.push_str(" },\n");
sarif.push_str(" \"message\": {\n");
sarif.push_str(&format!(
" \"text\": \"{}\"\n",
Self::sarif_escape(&piece.message)
));
sarif.push_str(" }\n");
if j + 1 < report.path.len() {
sarif.push_str(" },\n");
} else {
sarif.push_str(" }\n");
}
}
sarif.push_str(" ]");
}
sarif.push_str("\n");
if i + 1 < self.reports.len() {
sarif.push_str(" },\n");
} else {
sarif.push_str(" }\n");
}
}
sarif.push_str(" ]\n");
sarif.push_str(" }]\n");
sarif.push_str("}\n");
sarif
}
fn sarif_escape(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if c.is_control() => {
result.push_str(&format!("\\u{:04x}", c as u32));
}
c => result.push(c),
}
}
result
}
}
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 X86NullDerefChecker {
pub check_undefined: bool,
pub suppress_on_check: bool,
}
impl X86NullDerefChecker {
pub fn new() -> Self {
Self {
check_undefined: true,
suppress_on_check: true,
}
}
fn eval_ptr(state: &X86ProgramState, expr: &Expr) -> X86SVal {
match expr {
Expr::Ident(name) => state.lookup_value(name),
Expr::IntLiteral(v) => {
if *v == 0 {
X86SVal::Null
} else {
X86SVal::ConcreteInt(*v, None, None)
}
}
_ => X86SVal::Unknown,
}
}
}
impl X86Checker for X86NullDerefChecker {
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(base_expr) = extract_base(expr) {
if let Expr::Ident(name) = base_expr {
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
}
}
fn extract_base(expr: &Expr) -> Option<&Expr> {
match expr {
Expr::Member { base, .. } => {
if let Expr::Ident(_) = base.as_ref() {
Some(base)
} else {
extract_base(base)
}
}
Expr::Subscript { base, .. } => {
if let Expr::Ident(_) = base.as_ref() {
Some(base)
} else {
extract_base(base)
}
}
Expr::Ident(_) => Some(expr),
_ => None,
}
}
#[derive(Debug, Default)]
pub struct X86UndefResultChecker {
pub check_overflow: bool,
}
impl X86UndefResultChecker {
pub fn new() -> Self {
Self {
check_overflow: true,
}
}
}
impl X86Checker for X86UndefResultChecker {
fn name(&self) -> &str {
"core.UndefinedBinaryOperatorResult"
}
fn description(&self) -> &str {
"Check for undefined results of 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();
match expr {
Expr::Binary(op, lhs, rhs) => {
let lv = eval_literal(state, lhs);
let rv = eval_literal(state, rhs);
if lv.is_undefined() {
bugs.push(CheckerBug {
category: BugCategory::UndefinedBehavior,
severity: BugSeverity::Warning,
title: "Left operand is undefined".into(),
description: "Using undefined value in binary operation.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
if rv.is_undefined() {
bugs.push(CheckerBug {
category: BugCategory::UndefinedBehavior,
severity: BugSeverity::Warning,
title: "Right operand is undefined".into(),
description: "Using undefined value in binary operation.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
if *op == BinaryOp::Shl || *op == BinaryOp::Shr {
if let Some(shift) = rv.as_int() {
if shift < 0 {
bugs.push(CheckerBug {
category: BugCategory::ShiftError,
severity: BugSeverity::Error,
title: "Shift by negative amount".into(),
description:
"Shifting by a negative amount is undefined behaviour on x86."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
if shift >= 64 {
bugs.push(CheckerBug {
category: BugCategory::ShiftError,
severity: BugSeverity::Error,
title: "Shift exceeds bit width".into(),
description: "Shifting by >= bit width is undefined behaviour on x86.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
if *op == BinaryOp::Div || *op == BinaryOp::Mod {
if rv.is_zero() {
bugs.push(CheckerBug {
category: BugCategory::DivideByZero,
severity: BugSeverity::Error,
title: "Division by zero".into(),
description: "Division or modulo by zero is undefined on x86.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
if self.check_overflow
&& matches!(
op,
BinaryOp::Add
| BinaryOp::Sub
| BinaryOp::Mul
| BinaryOp::Div
| BinaryOp::Shl
)
{
if let (Some(a), Some(b)) = (lv.as_int(), rv.as_int()) {
match op {
BinaryOp::Add => {
if a.checked_add(b).is_none() {
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Warning,
title: "Signed integer overflow".into(),
description: "Addition may overflow (signed).".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
BinaryOp::Sub => {
if a.checked_sub(b).is_none() {
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Warning,
title: "Signed integer underflow".into(),
description: "Subtraction may underflow (signed).".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
BinaryOp::Mul => {
if a.checked_mul(b).is_none() {
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Warning,
title: "Signed integer overflow".into(),
description: "Multiplication may overflow (signed).".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
BinaryOp::Div => {
if a == i64::MIN && b == -1 {
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Error,
title: "Signed division overflow".into(),
description: "INT_MIN / -1 overflows (signed) 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,
);
}
bugs
}
}
fn eval_literal(state: &X86ProgramState, expr: &Expr) -> X86SVal {
match expr {
Expr::IntLiteral(v) => X86SVal::ConcreteInt(*v, None, None),
Expr::UIntLiteral(v, _) => X86SVal::ConcreteUInt(*v),
Expr::CharLiteral(c) => X86SVal::ConcreteInt(*c as i64, None, None),
Expr::FloatLiteral(_) | Expr::DoubleLiteral(_) => X86SVal::Unknown,
Expr::Ident(name) => state.lookup_value(name),
Expr::Unary(UnaryOp::Minus, inner) => {
let v = eval_literal(state, inner);
match v {
X86SVal::ConcreteInt(n, _, _) => X86SVal::ConcreteInt(-n, None, None),
other => other,
}
}
_ => X86SVal::Unknown,
}
}
#[derive(Debug, Default)]
pub struct X86DivZeroChecker;
impl X86DivZeroChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86DivZeroChecker {
fn name(&self) -> &str {
"core.DivideZero"
}
fn description(&self) -> &str {
"Check for division by zero"
}
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 rv = eval_literal(state, rhs);
if rv.is_zero() {
bugs.push(CheckerBug {
category: BugCategory::DivideByZero,
severity: BugSeverity::Error,
title: "Division by zero".into(),
description: "Division or modulo by zero is undefined behaviour.".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
}
}
#[derive(Debug, Default)]
pub struct X86ReturnUndefinedChecker {
pub track_initialized: HashSet<String>,
}
impl X86ReturnUndefinedChecker {
pub fn new() -> Self {
Self {
track_initialized: HashSet::new(),
}
}
pub fn mark_initialized(&mut self, var: &str) {
self.track_initialized.insert(var.to_string());
}
fn eval_return_expr(state: &X86ProgramState, expr: &Expr) -> bool {
match expr {
Expr::Ident(name) => state.lookup_value(name).is_undefined(),
_ => false,
}
}
}
impl X86Checker for X86ReturnUndefinedChecker {
fn name(&self) -> &str {
"core.ReturnUndefined"
}
fn description(&self) -> &str {
"Check for returning undefined/uninitialized values"
}
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 {
if Self::eval_return_expr(state, expr) {
bugs.push(CheckerBug {
category: BugCategory::Uninitialized,
severity: BugSeverity::Warning,
title: "Returning undefined value".into(),
description: "Return value may be uninitialized.".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)
}
}
#[derive(Debug, Default)]
pub struct X86ArrayBoundChecker {
pub array_sizes: HashMap<String, usize>,
pub check_pointer_arith: bool,
}
impl X86ArrayBoundChecker {
pub fn new() -> Self {
Self {
array_sizes: HashMap::new(),
check_pointer_arith: true,
}
}
pub fn register_array(&mut self, name: &str, size: usize) {
self.array_sizes.insert(name.to_string(), size);
}
fn extract_name(expr: &Expr) -> Option<&String> {
match expr {
Expr::Ident(name) => Some(name),
Expr::Subscript { base, .. } => Self::extract_name(base),
Expr::Member { base, .. } => Self::extract_name(base),
_ => None,
}
}
}
impl X86Checker for X86ArrayBoundChecker {
fn name(&self) -> &str {
"core.ArrayBound"
}
fn description(&self) -> &str {
"Check for out-of-bounds array access"
}
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 {
if let Some(arr_name) = Self::extract_name(base) {
if let Some(&size) = self.array_sizes.get(arr_name) {
let idx_val = eval_literal(state, index);
if let Some(i) = idx_val.as_int() {
if i < 0 || i as usize >= size {
bugs.push(CheckerBug {
category: BugCategory::ArrayBounds,
severity: BugSeverity::Error,
title: "Array index out of bounds".into(),
description: format!(
"Index {i} is out of bounds for array '{arr_name}' of size {size}"
),
file: String::new(),
line: 0,
column: 0,
});
}
}
if let Some(i) = idx_val.as_uint() {
if i as usize >= size {
bugs.push(CheckerBug {
category: BugCategory::ArrayBounds,
severity: BugSeverity::Error,
title: "Array index out of bounds".into(),
description: format!(
"Index {i} is out of bounds for array '{arr_name}' of size {size}"
),
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 X86StackAddrEscapeChecker;
impl X86StackAddrEscapeChecker {
pub fn new() -> Self {
Self
}
fn is_stack_address(state: &X86ProgramState, expr: &Expr) -> bool {
match expr {
Expr::Unary(UnaryOp::AddrOf, inner) => {
if let Expr::Ident(name) = inner.as_ref() {
if let Some(rid) = state.var_region(name) {
return true;
}
}
false
}
Expr::Ident(name) => {
if let Some(val) = state.values.get(name) {
matches!(val, X86SVal::Loc(_))
} else {
false
}
}
_ => false,
}
}
}
impl X86Checker for X86StackAddrEscapeChecker {
fn name(&self) -> &str {
"core.StackAddressEscape"
}
fn description(&self) -> &str {
"Check for stack address escaping function scope on X86 targets"
}
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(state, expr) {
bugs.push(CheckerBug {
category: BugCategory::StackAddressEscape,
severity: BugSeverity::Error,
title: "Returning stack address".into(),
description: "Returning a pointer to a local stack variable will lead to a dangling pointer 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)
}
}
#[derive(Debug, Default)]
pub struct X86ReturnStackChecker;
impl X86ReturnStackChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86ReturnStackChecker {
fn name(&self) -> &str {
"core.ReturnStackAddress"
}
fn description(&self) -> &str {
"Check for returning a pointer to stack memory"
}
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 let Expr::Unary(UnaryOp::AddrOf, inner) = expr.as_ref() {
if let Expr::Ident(name) = inner.as_ref() {
if state.var_region(name).is_some() {
bugs.push(CheckerBug {
category: BugCategory::StackAddressEscape,
severity: BugSeverity::Error,
title: "Returning address of local variable".into(),
description: format!(
"Address of stack variable '{name}' returned, will be dangling 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,
);
}
(vec![], bugs)
}
}
#[derive(Debug)]
pub struct X86MallocChecker {
pub allocations: HashMap<String, RegionId>,
pub warn_zero_size: bool,
}
impl X86MallocChecker {
pub fn new() -> Self {
Self {
allocations: HashMap::new(),
warn_zero_size: true,
}
}
fn extract_assign_target(stmt: &Stmt) -> Option<String> {
match stmt {
Stmt::Expr(expr) => match expr.as_ref() {
Expr::Assign(_, lhs, _) => {
if let Expr::Ident(name) = lhs.as_ref() {
Some(name.clone())
} else {
None
}
}
_ => None,
},
_ => None,
}
}
fn eval_to_loc(state: &X86ProgramState, expr: &Expr) -> Option<String> {
match expr {
Expr::Ident(name) => {
let val = state.lookup_value(name);
if matches!(val, X86SVal::Null | X86SVal::Zero) {
return Some("null".to_string());
}
Some(name.clone())
}
_ => None,
}
}
fn is_null_arg(state: &X86ProgramState, expr: &Expr) -> bool {
let val = eval_literal(state, expr);
val.is_null() || val.is_zero()
}
}
impl Default for X86MallocChecker {
fn default() -> Self {
Self::new()
}
}
impl X86Checker for X86MallocChecker {
fn name(&self) -> &str {
"core.Malloc"
}
fn description(&self) -> &str {
"Check for memory allocation errors: leaks, double frees, use-after-free"
}
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();
if let Stmt::Expr(expr) = stmt {
match expr.as_ref() {
Expr::Call {
callee: callee,
args: args,
} => {
if let Expr::Ident(func_name) = callee.as_ref() {
match func_name.as_str() {
"malloc" | "calloc" | "realloc" => {
if let Some(target) = Self::extract_assign_target(stmt) {
}
if self.warn_zero_size {
if let Some(arg) = args.first() {
let val = eval_literal(state, arg);
if let Some(n) = val.as_int() {
if n == 0 {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Warning,
title: "Zero-size allocation".into(),
description: "malloc(0) may return NULL or a unique pointer; behaviour is implementation-defined.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
"free" => {
if let Some(arg) = args.first() {
if Self::is_null_arg(state, arg) {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Note,
title: "Freeing null pointer".into(),
description:
"free(NULL) is safe but may indicate a logic error."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
if let Some(name) = Self::eval_to_loc(state, arg) {
if state.is_freed(&name) {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Error,
title: "Double free".into(),
description: format!(
"Memory pointed to by '{name}' has already been freed."
),
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)
}
fn check_end_of_function(
&self,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
for name in &state.allocated {
if !state.freed.contains(name) {
for (var_name, rid) in &state.var_regions {
if rid == name {
bugs.push(CheckerBug {
category: BugCategory::Memory,
severity: BugSeverity::Warning,
title: "Potential memory leak".into(),
description: format!(
"Memory allocated for '{var_name}' is not freed before function exit."
),
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 X86VLAChecker;
impl X86VLAChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86VLAChecker {
fn name(&self) -> &str {
"core.VLASize"
}
fn description(&self) -> &str {
"Check for VLA with non-positive size"
}
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 {
size: Some(size), ..
} = decl.ty.base.as_ref()
{
if let Some(init) = &decl.init {
let val = eval_literal(state, init);
if let Some(n) = val.as_int() {
if n <= 0 {
bugs.push(CheckerBug {
category: BugCategory::VLASize,
severity: BugSeverity::Error,
title: "VLA with non-positive size".into(),
description: format!(
"Variable-length array '{}' declared with size {n} ≤ 0.",
decl.name
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
if *size <= 0 {
bugs.push(CheckerBug {
category: BugCategory::VLASize,
severity: BugSeverity::Error,
title: "VLA with non-positive size".into(),
description: format!(
"Variable-length array '{}' declared with size {size} ≤ 0.",
decl.name
),
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 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,
}
}
fn infer_type(expr: &Expr) -> Option<QualType> {
match expr {
Expr::IntLiteral(_) => Some(QualType::int()),
Expr::Ident(_) => None,
_ => None,
}
}
}
impl X86Checker for X86CastChecker {
fn name(&self) -> &str {
"core.Cast"
}
fn description(&self) -> &str {
"Check for invalid casts between types on x86"
}
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(ty, inner) = expr {
let inner_ty = Self::infer_type(inner);
if let Some(it) = &inner_ty {
if it.is_pointer()
&& ty.is_integer()
&& ty.base.size_bytes() < 8
&& self.warn_ptr_truncation
{
bugs.push(CheckerBug {
category: BugCategory::InvalidCast,
severity: BugSeverity::Warning,
title: "Pointer truncation in cast".into(),
description:
"Casting a 64-bit pointer to a 32-bit integer truncates the address on x86-64."
.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
}
}
#[derive(Debug, Default)]
pub struct X86EnumChecker {
pub enum_ranges: HashMap<String, (i64, i64)>,
}
impl X86EnumChecker {
pub fn new() -> Self {
Self {
enum_ranges: HashMap::new(),
}
}
pub fn register_enum(&mut self, name: &str, min: i64, max: i64) {
self.enum_ranges.insert(name.to_string(), (min, max));
}
fn infer_enum_type(expr: &Expr) -> Option<String> {
match expr {
Expr::Ident(name) => Some(name.clone()),
_ => None,
}
}
}
impl X86Checker for X86EnumChecker {
fn name(&self) -> &str {
"core.EnumRange"
}
fn description(&self) -> &str {
"Check for enum values outside the declared range"
}
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 {
if let Some(enum_name) = Self::infer_enum_type(lhs) {
if let Some(&(min, max)) = self.enum_ranges.get(&enum_name) {
let val = eval_literal(state, rhs);
if let Some(n) = val.as_int() {
if n < min || n > max {
bugs.push(CheckerBug {
category: BugCategory::EnumRange,
severity: BugSeverity::Warning,
title: "Enum value out of range".into(),
description: format!(
"Value {n} is outside the declared range [{min}, {max}] of enum '{enum_name}'."
),
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 X86NonNullChecker {
pub nonnull_params: HashMap<String, Vec<usize>>,
}
impl X86NonNullChecker {
pub fn new() -> Self {
Self {
nonnull_params: HashMap::new(),
}
}
pub fn register_nonnull(&mut self, func: &str, param_indices: Vec<usize>) {
self.nonnull_params.insert(func.to_string(), param_indices);
}
fn eval_arg(state: &X86ProgramState, arg: &Expr) -> bool {
let val = eval_literal(state, arg);
val.is_null()
}
}
impl X86Checker for X86NonNullChecker {
fn name(&self) -> &str {
"core.NonNullParam"
}
fn description(&self) -> &str {
"Check for null passed to nonnull parameter"
}
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(param_indices) = self.nonnull_params.get(func_name.as_str()) {
for &idx in param_indices {
if idx < args.len() {
if Self::eval_arg(state, &args[idx]) {
bugs.push(CheckerBug {
category: BugCategory::NonNullViolation,
severity: BugSeverity::Error,
title: "Null passed to nonnull parameter".into(),
description: format!(
"Parameter {idx} of '{func_name}' is marked nonnull, but null was passed."
),
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 taint_sources: HashSet<String> =
["read", "recv", "recvfrom", "fgets", "scanf", "getenv"]
.iter()
.map(|s| s.to_string())
.collect();
let taint_sinks: HashSet<String> = [
"system", "execve", "execl", "memcpy", "strcpy", "sprintf", "fprintf",
]
.iter()
.map(|s| s.to_string())
.collect();
let taint_propagators: HashSet<String> = [
"strcpy", "strcat", "sprintf", "snprintf", "memcpy", "memmove",
]
.iter()
.map(|s| s.to_string())
.collect();
Self {
taint_sources,
taint_sinks,
taint_propagators,
}
}
fn is_tainted_arg(state: &X86ProgramState, arg: &Expr) -> bool {
match arg {
Expr::Ident(name) => state.is_tainted(name),
_ => false,
}
}
}
impl Default for X86TaintChecker {
fn default() -> Self {
Self::new()
}
}
impl X86Checker for X86TaintChecker {
fn name(&self) -> &str {
"core.Taint"
}
fn description(&self) -> &str {
"Taint propagation checker: track untrusted input to dangerous sinks"
}
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 succ_states = Vec::new();
if let Stmt::Expr(expr) = stmt {
match expr.as_ref() {
Expr::Call {
callee: callee,
args: args,
} => {
if let Expr::Ident(func_name) = callee.as_ref() {
if self.taint_sources.contains(func_name.as_str()) {
let mut new_state = state.clone();
if args.len() >= 2 {
if let Expr::Ident(buf_name) = &args[1] {
new_state.taint(buf_name);
}
}
succ_states.push(new_state);
} else if self.taint_sinks.contains(func_name.as_str()) {
for (i, arg) in args.iter().enumerate() {
if Self::is_tainted_arg(state, arg) {
bugs.push(CheckerBug {
category: BugCategory::Taint,
severity: BugSeverity::Warning,
title: "Tainted data reaches sink".into(),
description: format!(
"Argument {i} of '{func_name}' is tainted (untrusted input)."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
} else if self.taint_propagators.contains(func_name.as_str()) {
if args.len() >= 2 {
let mut new_state = state.clone();
if let Expr::Ident(src_name) = &args[1] {
if new_state.is_tainted(src_name) {
if let Expr::Ident(dst_name) = &args[0] {
new_state.taint(dst_name);
}
}
}
succ_states.push(new_state);
}
}
}
}
Expr::Assign(_, lhs, rhs) => {
if let Expr::Ident(rhs_name) = rhs.as_ref() {
if state.is_tainted(rhs_name) {
let mut new_state = state.clone();
if let Expr::Ident(lhs_name) = lhs.as_ref() {
new_state.taint(lhs_name);
}
succ_states.push(new_state);
}
}
}
_ => {}
}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
(succ_states, bugs)
}
}
#[derive(Debug, Default)]
pub struct X86DeadStoreChecker {
pub stores: HashMap<String, u64>,
pub loads: HashSet<String>,
}
impl X86DeadStoreChecker {
pub fn new() -> Self {
Self {
stores: HashMap::new(),
loads: HashSet::new(),
}
}
}
impl X86Checker for X86DeadStoreChecker {
fn name(&self) -> &str {
"deadcode.DeadStores"
}
fn description(&self) -> &str {
"Check for values stored to variables that are never read"
}
fn category(&self) -> BugCategory {
BugCategory::DeadStore
}
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::Expr(expr) = stmt {
match expr.as_ref() {
Expr::Assign(_, lhs, _) => {
if let Expr::Ident(name) = lhs.as_ref() {
if let Some(rhs_name) = expr_rhs_ident(Some(expr.as_ref())) {
if *name == rhs_name {
bugs.push(CheckerBug {
category: BugCategory::DeadStore,
severity: BugSeverity::Style,
title: "Dead store".into(),
description: format!(
"Self-assignment of '{name}' has no effect."
),
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)
}
}
fn expr_rhs_ident(expr: Option<&Expr>) -> Option<String> {
match expr {
Some(Expr::Assign(_, _, rhs)) => match rhs.as_ref() {
Expr::Ident(name) => Some(name.clone()),
_ => None,
},
_ => None,
}
}
#[derive(Debug, Default)]
pub struct X86UnreachableCodeChecker;
impl X86UnreachableCodeChecker {
pub fn new() -> Self {
Self
}
fn is_noreturn(state: &X86ProgramState) -> bool {
state.is_dead
}
fn is_const_false(state: &X86ProgramState, expr: &Expr) -> bool {
let val = eval_literal(state, expr);
val.is_zero()
}
fn is_const_true(state: &X86ProgramState, expr: &Expr) -> bool {
let val = eval_literal(state, expr);
val.as_int() == Some(1)
}
}
impl X86Checker for X86UnreachableCodeChecker {
fn name(&self) -> &str {
"deadcode.UnreachableCode"
}
fn description(&self) -> &str {
"Check for unreachable code after return, break, continue, or noreturn calls"
}
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();
let mut succ_states = Vec::new();
match stmt {
Stmt::If {
cond: cond,
then: _,
els: _,
} => {
if Self::is_const_false(state, cond) {
let mut dead_state = state.clone();
dead_state.mark_dead();
succ_states.push(dead_state);
bugs.push(CheckerBug {
category: BugCategory::UnreachableCode,
severity: BugSeverity::Warning,
title: "Unreachable code: condition is always false".into(),
description: "The true branch of this if statement will never execute."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
if Self::is_const_true(state, cond) {
bugs.push(CheckerBug {
category: BugCategory::UnreachableCode,
severity: BugSeverity::Warning,
title: "Unreachable code: condition is always true".into(),
description: "The false branch of this if statement will never execute."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
Stmt::Return(_) | Stmt::Break | Stmt::Continue | Stmt::Goto { label: _ } => {
let mut dead_state = state.clone();
dead_state.mark_dead();
succ_states.push(dead_state);
}
_ => {}
}
for bug in &bugs {
reporter.emit_simple(
self.name(),
bug.category,
bug.severity,
&bug.title,
&bug.description,
&bug.file,
bug.line,
bug.column,
);
}
(succ_states, bugs)
}
}
#[derive(Debug)]
pub struct X86RetainCountChecker {
pub retain_functions: HashSet<String>,
pub release_functions: HashSet<String>,
pub max_tracked_count: i32,
}
impl X86RetainCountChecker {
pub fn new() -> Self {
let retain_functions: HashSet<String> = ["retain", "CFRetain", "objc_retain", "ns_retain"]
.iter()
.map(|s| s.to_string())
.collect();
let release_functions: HashSet<String> = [
"release",
"CFRelease",
"objc_release",
"ns_release",
"autorelease",
]
.iter()
.map(|s| s.to_string())
.collect();
Self {
retain_functions,
release_functions,
max_tracked_count: 100,
}
}
fn extract_obj_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Ident(name) => Some(name.clone()),
Expr::Call { .. } => None,
_ => None,
}
}
}
impl Default for X86RetainCountChecker {
fn default() -> Self {
Self::new()
}
}
impl X86Checker for X86RetainCountChecker {
fn name(&self) -> &str {
"core.RetainCount"
}
fn description(&self) -> &str {
"Track retain/release balance for Apple-style reference counting"
}
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(func_name) = callee.as_ref() {
if self.retain_functions.contains(func_name.as_str()) {
if let Some(arg) = args.first() {
if let Some(obj_name) = Self::extract_obj_name(arg) {
let count = state.retain_count(&obj_name);
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 cycle."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
} else if self.release_functions.contains(func_name.as_str()) {
if let Some(arg) = args.first() {
if let Some(obj_name) = Self::extract_obj_name(arg) {
let count = state.retain_count(&obj_name);
if count <= 0 {
bugs.push(CheckerBug {
category: BugCategory::RetainCount,
severity: BugSeverity::Error,
title: "Over-release".into(),
description: format!(
"Object '{obj_name}' is being released but has a retain count of {count}."
),
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 (obj, count) in &state.ref_counts {
if *count > 0 {
bugs.push(CheckerBug {
category: BugCategory::RetainCount,
severity: BugSeverity::Warning,
title: "Retain count leak".into(),
description: format!(
"Object '{obj}' still has a retain count of {count} at function exit."
),
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 X86PthreadLockChecker {
pub lock_functions: HashSet<String>,
pub unlock_functions: HashSet<String>,
}
impl X86PthreadLockChecker {
pub fn new() -> Self {
let lock_functions: HashSet<String> = ["pthread_mutex_lock", "mtx_lock", "lock"]
.iter()
.map(|s| s.to_string())
.collect();
let unlock_functions: HashSet<String> = ["pthread_mutex_unlock", "mtx_unlock", "unlock"]
.iter()
.map(|s| s.to_string())
.collect();
Self {
lock_functions,
unlock_functions,
}
}
fn extract_mutex_name(expr: &Expr) -> Option<String> {
match expr {
Expr::Unary(UnaryOp::AddrOf, inner) => {
if let Expr::Ident(name) = inner.as_ref() {
return Some(name.clone());
}
None
}
Expr::Ident(name) => Some(name.clone()),
_ => None,
}
}
}
impl X86Checker for X86PthreadLockChecker {
fn name(&self) -> &str {
"core.PthreadLock"
}
fn description(&self) -> &str {
"Check pthread lock/unlock pairing for concurrency bugs"
}
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(func_name) = callee.as_ref() {
if let Some(mutex_arg) = args.first() {
if let Some(mutex_name) = Self::extract_mutex_name(mutex_arg) {
if self.lock_functions.contains(func_name.as_str()) {
if state.lock_held(&mutex_name) {
bugs.push(CheckerBug {
category: BugCategory::Locking,
severity: BugSeverity::Error,
title: "Double lock".into(),
description: format!(
"Mutex '{mutex_name}' is already locked (double lock)."
),
file: String::new(),
line: 0,
column: 0,
});
}
} else if self.unlock_functions.contains(func_name.as_str()) {
if !state.lock_held(&mutex_name) {
bugs.push(CheckerBug {
category: BugCategory::Locking,
severity: BugSeverity::Error,
title: "Unlock of unlocked mutex".into(),
description: format!(
"Mutex '{mutex_name}' is not locked (unlock without lock)."
),
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_name in &state.held_locks {
bugs.push(CheckerBug {
category: BugCategory::Locking,
severity: BugSeverity::Warning,
title: "Lock held at function exit".into(),
description: format!(
"Mutex '{lock_name}' is still locked at function exit (potential deadlock)."
),
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 X86CallAndMessageChecker;
impl X86CallAndMessageChecker {
pub fn new() -> Self {
Self
}
}
impl X86Checker for X86CallAndMessageChecker {
fn name(&self) -> &str {
"core.CallAndMessage"
}
fn description(&self) -> &str {
"Check for null function pointers and undefined arguments in calls"
}
fn category(&self) -> BugCategory {
BugCategory::CallAndMessage
}
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(fn_ptr_name) = callee.as_ref() {
let val = state.lookup_value(fn_ptr_name);
if val.is_null() {
bugs.push(CheckerBug {
category: BugCategory::CallAndMessage,
severity: BugSeverity::Error,
title: "Call through null function pointer".into(),
description: format!(
"Attempting to call through null function pointer '{fn_ptr_name}'."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
for (i, arg) in args.iter().enumerate() {
let val = eval_literal(state, arg);
if val.is_undefined() {
bugs.push(CheckerBug {
category: BugCategory::CallAndMessage,
severity: BugSeverity::Warning,
title: "Undefined argument in call".into(),
description: format!(
"Argument {i} is undefined (uninitialized) in function call."
),
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 X86UnixAPIChecker {
pub open_calls: HashSet<String>,
pub fd_sinks: HashSet<String>,
pub tracked_fds: HashMap<String, i32>,
}
impl X86UnixAPIChecker {
pub fn new() -> Self {
let open_calls: HashSet<String> = ["open", "openat", "creat", "socket", "accept", "pipe"]
.iter()
.map(|s| s.to_string())
.collect();
let fd_sinks: HashSet<String> = ["close", "read", "write", "fcntl", "ioctl"]
.iter()
.map(|s| s.to_string())
.collect();
Self {
open_calls,
fd_sinks,
tracked_fds: HashMap::new(),
}
}
}
impl Default for X86UnixAPIChecker {
fn default() -> Self {
Self::new()
}
}
impl X86Checker for X86UnixAPIChecker {
fn name(&self) -> &str {
"api.UnixAPI"
}
fn description(&self) -> &str {
"Check correct usage of POSIX/Unix APIs: open/close pairing, fd leaks, errno"
}
fn category(&self) -> BugCategory {
BugCategory::API
}
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 self.open_calls.contains(func_name.as_str()) {
if func_name == "open" || func_name == "openat" {
if args.len() >= 2 {
let val = eval_literal(state, &args[1]);
if let Some(flags) = val.as_int() {
if flags < 0 {
bugs.push(CheckerBug {
category: BugCategory::API,
severity: BugSeverity::Warning,
title: "Negative flags in open()".into(),
description: "open() called with negative flags value."
.into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
if let Some(target) = extract_target_var(stmt_for_expr(expr)) {
let _ = target;
}
} else if self.fd_sinks.contains(func_name.as_str()) {
if let Some(fd_expr) = args.first() {
let val = eval_literal(state, fd_expr);
if let Some(fd) = val.as_int() {
if fd < 0 {
bugs.push(CheckerBug {
category: BugCategory::API,
severity: BugSeverity::Error,
title: "Negative file descriptor".into(),
description: format!(
"Using negative file descriptor ({fd}) in '{func_name}'."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
if func_name == "errno" || func_name == "__errno_location" {
}
}
}
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 stmt_for_expr(expr: &Expr) -> Option<&Stmt> {
let _ = expr;
None
}
fn extract_target_var(_stmt: Option<&Stmt>) -> Option<String> {
None
}
#[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> = [
"gets", "strcpy", "strcat", "sprintf", "vsprintf", "scanf", "getwd", "rand", "random",
]
.iter()
.map(|s| s.to_string())
.collect();
let mut replacements = HashMap::new();
replacements.insert("gets".to_string(), "fgets".to_string());
replacements.insert("strcpy".to_string(), "strncpy".to_string());
replacements.insert("strcat".to_string(), "strncat".to_string());
replacements.insert("sprintf".to_string(), "snprintf".to_string());
replacements.insert(
"scanf".to_string(),
"scanf with width specifier".to_string(),
);
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 usage of insecure/dangerous functions (gets, strcpy, etc.)"
}
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: _args,
} = expr
{
if let Expr::Ident(func_name) = callee.as_ref() {
if self.dangerous_functions.contains(func_name.as_str()) {
let suggestion = self
.replacements
.get(func_name.as_str())
.map(|r| format!(" Use '{r}' instead."))
.unwrap_or_default();
bugs.push(CheckerBug {
category: BugCategory::Security,
severity: BugSeverity::Warning,
title: format!("Dangerous function '{func_name}' used"),
description: format!("'{func_name}' is deprecated and unsafe.{suggestion}"),
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 X86IteratorChecker {
pub tracked_iterators: HashMap<String, (String, usize)>, pub container_sizes: HashMap<String, usize>,
}
impl X86IteratorChecker {
pub fn new() -> Self {
Self {
tracked_iterators: HashMap::new(),
container_sizes: HashMap::new(),
}
}
pub fn register_container(&mut self, name: &str, size: usize) {
self.container_sizes.insert(name.to_string(), size);
}
pub fn track_iterator(&mut self, iter_name: &str, container: &str, index: usize) {
self.tracked_iterators
.insert(iter_name.to_string(), (container.to_string(), index));
}
}
impl X86Checker for X86IteratorChecker {
fn name(&self) -> &str {
"core.Iterator"
}
fn description(&self) -> &str {
"Check for iterator invalidation and out-of-range iterator access"
}
fn category(&self) -> BugCategory {
BugCategory::Iterator
}
fn check_expr(
&self,
expr: &Expr,
state: &X86ProgramState,
reporter: &mut X86BugReporter,
) -> Vec<CheckerBug> {
let mut bugs = Vec::new();
if let Expr::Unary(UnaryOp::Deref, inner) = expr {
if let Expr::Ident(iter_name) = inner.as_ref() {
if let Some((container, index)) = self.tracked_iterators.get(iter_name) {
if let Some(&size) = self.container_sizes.get(container) {
if *index >= size {
bugs.push(CheckerBug {
category: BugCategory::Iterator,
severity: BugSeverity::Error,
title: "Out-of-range iterator dereference".into(),
description: format!(
"Iterator '{iter_name}' points to index {index} which is beyond container '{container}' size {size}."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
if let Some(iter_info) = state.get_generic(&format!("iter:{iter_name}")) {
let parts: Vec<&str> = iter_info.split(':').collect();
if parts.len() == 2 {
let container = parts[0];
if let Ok(index) = parts[1].parse::<usize>() {
if let Some(&size) = self.container_sizes.get(container) {
if index >= size {
bugs.push(CheckerBug {
category: BugCategory::Iterator,
severity: BugSeverity::Error,
title: "Iterator out of bounds".into(),
description: format!(
"Iterator dereferenced at index {index} in container '{container}' of size {size}."
),
file: String::new(),
line: 0,
column: 0,
});
}
}
}
}
}
}
}
if let Expr::Call {
callee: callee,
args: args,
} = expr
{
if let Expr::Ident(func_name) = callee.as_ref() {
let mutating_ops = [
"push_back",
"insert",
"erase",
"clear",
"resize",
"pop_back",
];
if mutating_ops.contains(&func_name.as_str()) {
if let Some(arg) = args.first() {
if let Expr::Ident(container) = arg {
for (iter_name, (cont, _)) in &self.tracked_iterators {
if *cont == *container {
bugs.push(CheckerBug {
category: BugCategory::Iterator,
severity: BugSeverity::Warning,
title: "Iterator invalidation".into(),
description: format!(
"Iterator '{iter_name}' into container '{container}' may be invalidated by '{func_name}'."
),
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 X86FormatStringChecker {
pub printf_functions: HashSet<String>,
pub scanf_functions: HashSet<String>,
}
impl X86FormatStringChecker {
pub fn new() -> Self {
let printf_functions: HashSet<String> =
["printf", "fprintf", "sprintf", "snprintf", "dprintf"]
.iter()
.map(|s| s.to_string())
.collect();
let scanf_functions: HashSet<String> = ["scanf", "fscanf", "sscanf"]
.iter()
.map(|s| s.to_string())
.collect();
Self {
printf_functions,
scanf_functions,
}
}
fn is_string_literal(expr: &Expr) -> bool {
matches!(expr, Expr::StringLiteral(_))
}
fn is_const_string(state: &X86ProgramState, expr: &Expr) -> bool {
match expr {
Expr::Ident(name) => {
if let Some(val) = state.values.get(name) {
matches!(val, X86SVal::ConcreteInt(_, _, _))
} else {
false
}
}
Expr::StringLiteral(_) => true,
_ => false,
}
}
fn count_format_specifiers(fmt: &str) -> usize {
let mut count = 0usize;
let mut chars = fmt.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
if let Some(&next) = chars.peek() {
if next == '%' {
chars.next();
} else {
count += 1;
}
}
}
}
count
}
}
impl X86Checker for X86FormatStringChecker {
fn name(&self) -> &str {
"security.FormatString"
}
fn description(&self) -> &str {
"Check format string vulnerabilities"
}
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(func_name) = callee.as_ref() {
if self.printf_functions.contains(func_name.as_str())
|| self.scanf_functions.contains(func_name.as_str())
{
if let Some(fmt_arg) = args.first() {
if !Self::is_string_literal(fmt_arg)
&& !Self::is_const_string(state, fmt_arg)
{
bugs.push(CheckerBug {
category: BugCategory::FormatString,
severity: BugSeverity::Warning,
title: "Non-literal format string".into(),
description: format!(
"Format string in '{func_name}' is not a string literal; potential format string vulnerability."
),
file: String::new(),
line: 0,
column: 0,
});
}
if let Expr::StringLiteral(fmt) = fmt_arg {
let spec_count = Self::count_format_specifiers(fmt);
let arg_count = args.len().saturating_sub(1);
if spec_count > arg_count {
bugs.push(CheckerBug {
category: BugCategory::FormatString,
severity: BugSeverity::Error,
title: "Format string: too few arguments".into(),
description: format!(
"Format string expects {spec_count} arguments, but only {arg_count} provided."
),
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 X86ShiftChecker;
impl X86ShiftChecker {
pub fn new() -> Self {
Self
}
fn estimate_bit_width(expr: &Expr) -> u32 {
match expr {
Expr::IntLiteral(_) | Expr::UIntLiteral(..) => 64,
Expr::CharLiteral(_) => 8,
_ => 64,
}
}
}
impl X86Checker for X86ShiftChecker {
fn name(&self) -> &str {
"core.Shift"
}
fn description(&self) -> &str {
"Check for shift operations with count >= bit width or negative"
}
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(state, rhs);
if let Some(shift) = shift_val.as_int() {
let width = Self::estimate_bit_width(lhs);
if shift < 0 {
bugs.push(CheckerBug {
category: BugCategory::ShiftError,
severity: BugSeverity::Error,
title: "Shift by negative amount".into(),
description: "Shift count is negative (undefined behaviour).".into(),
file: String::new(),
line: 0,
column: 0,
});
} else if (shift as u32) >= width {
bugs.push(CheckerBug {
category: BugCategory::ShiftError,
severity: BugSeverity::Error,
title: "Shift count exceeds bit width".into(),
description: format!(
"Shift count {shift} >= bit width {width} (undefined behaviour 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
}
}
#[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"
}
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 {
let lv = eval_literal(state, lhs);
let rv = eval_literal(state, rhs);
if let (Some(a), Some(b)) = (lv.as_int(), rv.as_int()) {
match op {
BinaryOp::Add => {
if a.checked_add(b).is_none() {
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Warning,
title: "Signed integer overflow in addition".into(),
description: format!("{a} + {b} may overflow (signed)."),
file: String::new(),
line: 0,
column: 0,
});
}
}
BinaryOp::Sub => {
if a.checked_sub(b).is_none() {
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Warning,
title: "Signed integer underflow in subtraction".into(),
description: format!("{a} - {b} may underflow (signed)."),
file: String::new(),
line: 0,
column: 0,
});
}
}
BinaryOp::Mul => {
if a.checked_mul(b).is_none() {
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Warning,
title: "Signed integer overflow in multiplication".into(),
description: format!("{a} * {b} may overflow (signed)."),
file: String::new(),
line: 0,
column: 0,
});
}
}
BinaryOp::Div => {
if a == i64::MIN && b == -1 {
bugs.push(CheckerBug {
category: BugCategory::SignedOverflow,
severity: BugSeverity::Error,
title: "Signed division overflow (INT_MIN / -1)".into(),
description: "INT_MIN / -1 overflows 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,
);
}
bugs
}
}
#[derive(Debug, Default)]
pub struct X86FloatLoopCounterChecker;
impl X86FloatLoopCounterChecker {
pub fn new() -> Self {
Self
}
fn expr_is_float(expr: &Expr) -> bool {
matches!(expr, Expr::FloatLiteral(_) | Expr::DoubleLiteral(_))
}
fn cond_uses_float(cond: &Expr) -> bool {
match cond {
Expr::Binary(_, lhs, rhs) => Self::expr_is_float(lhs) || Self::expr_is_float(rhs),
_ => Self::expr_is_float(cond),
}
}
}
impl X86Checker for X86FloatLoopCounterChecker {
fn name(&self) -> &str {
"style.FloatLoopCounter"
}
fn description(&self) -> &str {
"Check for floating-point variables used as loop counters"
}
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: init,
cond: cond,
incr: incr,
body: _,
} => {
let mut has_float = false;
if let Some(init_expr) = init {
if let Stmt::Expr(inner) = init_expr.as_ref() {
if let Expr::Assign(_, _, rhs) = inner.as_ref() {
if Self::expr_is_float(rhs) {
has_float = true;
}
}
}
}
if let Some(cond_expr) = cond {
if Self::cond_uses_float(cond_expr) {
has_float = true;
}
}
if let Some(incr_expr) = incr {
if let Expr::Assign(_, _, rhs) = incr_expr.as_ref() {
if Self::expr_is_float(rhs) {
has_float = true;
}
}
}
if has_float {
bugs.push(CheckerBug {
category: BugCategory::FloatLoopCounter,
severity: BugSeverity::Style,
title: "Floating-point loop counter".into(),
description: "Using floating-point values as loop counters may lead to imprecise iteration counts on x86.".into(),
file: String::new(),
line: 0,
column: 0,
});
}
}
Stmt::While {
cond: cond,
body: _,
} => {
if Self::cond_uses_float(cond) {
bugs.push(CheckerBug {
category: BugCategory::FloatLoopCounter,
severity: BugSeverity::Style,
title: "Floating-point loop condition".into(),
description: "Using floating-point in loop condition may lead to imprecise loop termination 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)
}
}
#[derive(Debug)]
pub struct X86CheckerRegistry {
checkers: Vec<Box<dyn X86Checker>>,
disabled: HashSet<String>,
registration_order: Vec<String>,
}
impl X86CheckerRegistry {
pub fn new() -> Self {
Self {
checkers: Vec::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);
self.checkers.push(checker);
}
pub fn register_checker(&mut self, checker: Box<dyn X86Checker>) {
self.register(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.disabled.contains(name)
}
pub fn enabled_checkers(&self) -> Vec<&dyn X86Checker> {
self.checkers
.iter()
.filter(|c| self.is_enabled(c.name()))
.map(|c| c.as_ref())
.collect()
}
pub fn checker_names(&self) -> Vec<&str> {
self.checkers.iter().map(|c| c.name()).collect()
}
pub fn count(&self) -> usize {
self.checkers.len()
}
pub fn enabled_count(&self) -> usize {
self.enabled_checkers().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(Box::new(X86NullDerefChecker::new()));
registry.register(Box::new(X86UndefResultChecker::new()));
registry.register(Box::new(X86DivZeroChecker::new()));
registry.register(Box::new(X86ReturnUndefinedChecker::new()));
registry.register(Box::new(X86ArrayBoundChecker::new()));
registry.register(Box::new(X86StackAddrEscapeChecker::new()));
registry.register(Box::new(X86ReturnStackChecker::new()));
registry.register(Box::new(X86CastChecker::new()));
registry.register(Box::new(X86VLAChecker::new()));
registry.register(Box::new(X86EnumChecker::new()));
registry.register(Box::new(X86MallocChecker::new()));
registry.register(Box::new(X86NonNullChecker::new()));
registry.register(Box::new(X86CallAndMessageChecker::new()));
registry.register(Box::new(X86TaintChecker::new()));
registry.register(Box::new(X86SecuritySyntaxChecker::new()));
registry.register(Box::new(X86FormatStringChecker::new()));
registry.register(Box::new(X86DeadStoreChecker::new()));
registry.register(Box::new(X86UnreachableCodeChecker::new()));
registry.register(Box::new(X86PthreadLockChecker::new()));
registry.register(Box::new(X86RetainCountChecker::new()));
registry.register(Box::new(X86UnixAPIChecker::new()));
registry.register(Box::new(X86IteratorChecker::new()));
registry.register(Box::new(X86ShiftChecker::new()));
registry.register(Box::new(X86SignedIntegerOverflowChecker::new()));
registry.register(Box::new(X86FloatLoopCounterChecker::new()));
let _ = subtarget;
registry
}
}
impl Default for X86CheckerRegistry {
fn default() -> Self {
Self::with_all_defaults(None)
}
}
pub struct X86StaticAnalyzerEngine {
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 X86StaticAnalyzerEngine {
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 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 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 {
for checker in &self.registry.enabled_checkers() {
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,
stmt_label(stmt),
advanced,
X86EdgeKind::Fallthrough,
"",
);
if new_node != 0 {
current_node = new_node;
} else {
break;
}
} else {
let mut last_node = 0u64;
for ns in next_states {
let edge_kind = if ns.infeasible {
X86EdgeKind::FalseBranch
} else {
X86EdgeKind::TrueBranch
};
let advanced = ns.advance();
let new_node = self.graph.add_node(
current_node,
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: cond,
then: _,
els: _,
} => {
self.check_expr_recursive(cond, state);
}
Stmt::While {
cond: cond,
body: _,
} => {
self.check_expr_recursive(cond, state);
}
Stmt::For {
init: init,
cond: cond,
incr: incr,
body: _,
} => {
if let Some(e) = init.as_ref() {
if let Stmt::Expr(inner) = e.as_ref() {
self.check_expr_recursive(inner, 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: expr,
body: _,
} => {
self.check_expr_recursive(expr, state);
}
Stmt::Case { value: _, stmt: s } | Stmt::Default { stmt: s } => {
self.check_exprs_in_stmt(s, 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: callee,
args: 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();
}
}
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
}
pub fn analyze_with_result(&mut self, tu: &TranslationUnit) -> X86AnalysisResult {
let start = std::time::Instant::now();
self.analyze(tu);
let duration = start.elapsed();
self.stats.time_ms = duration.as_millis() as u64;
X86AnalysisResult {
reports: self.reporter.reports.clone(),
stats: self.stats.clone(),
graph_summary: X86GraphSummary {
total_nodes: self.graph.node_count(),
total_edges: self.graph.edge_count(),
max_depth: self.graph.max_depth,
feasible_paths: 0,
infeasible_paths: 0,
dead_ends: 0,
},
call_graph: X86CallGraph::new(),
duration_ms: self.stats.time_ms,
}
}
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<String> {
self.registry
.enabled_checkers()
.iter()
.map(|c| c.name().to_string())
.collect()
}
pub fn set_max_depth(&mut self, depth: u64) {
self.graph.max_depth = depth;
}
pub fn set_max_nodes(&mut self, max_nodes: usize) {
self.max_nodes = max_nodes;
}
}
impl Default for X86StaticAnalyzerEngine {
fn default() -> Self {
Self::with_defaults()
}
}
fn stmt_label(stmt: &Stmt) -> String {
match stmt {
Stmt::Return(_) => "return".into(),
Stmt::If {
cond: _,
then: _,
els: _,
} => "if".into(),
Stmt::While { cond: _, body: _ } => "while".into(),
Stmt::For {
init: _,
cond: _,
incr: _,
body: _,
} => "for".into(),
Stmt::Switch { expr: _, body: _ } => "switch".into(),
Stmt::Expr(e) => format!("expr({})", expr_label(e)),
Stmt::Compound(_) => "compound".into(),
Stmt::Break => "break".into(),
Stmt::Continue => "continue".into(),
Stmt::Goto { label: _ } => "goto".into(),
Stmt::Label { name: n, stmt: _ } => format!("label:{n}"),
_ => "stmt".into(),
}
}
fn expr_label(expr: &Expr) -> String {
match expr {
Expr::Call {
callee: callee,
args: _,
} => {
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, _) => format!("unary({})", op.as_str()),
Expr::Cast(_, _) => "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, caller: &str) -> Vec<&str> {
self.edges
.get(caller)
.map(|s| s.iter().map(|x| x.as_str()).collect())
.unwrap_or_default()
}
pub fn callers_of(&self, callee: &str) -> Vec<&str> {
self.rev_edges
.get(callee)
.map(|s| s.iter().map(|x| x.as_str()).collect())
.unwrap_or_default()
}
pub fn all_functions(&self) -> Vec<&str> {
let mut names: HashSet<&str> = HashSet::new();
for k in self.edges.keys() {
names.insert(k);
}
for k in self.rev_edges.keys() {
names.insert(k);
}
names.into_iter().collect()
}
pub fn topological_order(&self) -> Vec<String> {
let all = self.all_functions();
let mut in_degree: HashMap<String, usize> = HashMap::new();
for f in &all {
in_degree.entry(f.to_string()).or_insert(0);
}
for (caller, callees) in &self.edges {
for callee in callees {
*in_degree.entry(callee.clone()).or_insert(0) += 1;
}
}
let mut queue: VecDeque<String> = VecDeque::new();
for f in &all {
if in_degree.get(&**f).copied().unwrap_or(0) == 0 {
queue.push_back(f.to_string());
}
}
let mut result = Vec::new();
while let Some(f) = queue.pop_front() {
result.push(f.clone());
if let Some(callees) = self.edges.get(&f) {
for callee in callees {
let d = in_degree.get_mut(callee).unwrap();
*d -= 1;
if *d == 0 {
queue.push_back(callee.clone());
}
}
}
}
result
}
pub fn is_recursive(&self, func: &str) -> bool {
let mut visited = HashSet::new();
let mut stack = vec![func.to_string()];
while let Some(f) = stack.pop() {
if f == func && !visited.is_empty() {
return true;
}
if !visited.insert(f.clone()) {
continue;
}
if let Some(callees) = self.edges.get(&f) {
for callee in callees {
stack.push(callee.clone());
}
}
}
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: &str) -> Self {
Self {
name: name.to_string(),
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: &mut X86ProgramState) {
if self.is_noreturn {
state.mark_dead();
}
for lock in &self.acquires_locks {
state.acquire_lock(lock);
}
for lock in &self.releases_locks {
state.release_lock(lock);
}
}
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: &str) -> Self {
Self {
name: name.to_string(),
allocates_region: false,
deallocates_region: false,
is_noreturn: true,
acquires_locks: vec![],
releases_locks: vec![],
taint_source: false,
taint_sink: false,
}
}
}
#[derive(Debug, Clone)]
pub struct X86MemoryLayout {
pub pointer_size: u64,
pub size_t_size: u64,
pub wchar_size: u64,
pub page_size: u64,
pub stack_alignment: u64,
pub heap_alignment: u64,
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::x86_64()
}
}
impl X86MemoryLayout {
pub fn x86_64() -> 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, 4095),
canonical_low: 0,
canonical_high: 0x00007FFFFFFFFFFF,
}
}
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, 4095),
canonical_low: 0,
canonical_high: 0xFFFFFFFF,
}
}
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 {
let sign_bit = (addr >> 47) & 1;
if sign_bit == 0 {
addr <= self.canonical_high
} else {
let mask = 0xFFFF800000000000;
(addr & mask) == mask
}
} else {
true
}
}
}
#[derive(Debug, Clone)]
pub struct X86TypeSizeCache {
sizes: HashMap<String, u64>,
alignments: HashMap<String, u64>,
}
impl X86TypeSizeCache {
pub fn x86_64_default() -> Self {
let mut sizes = HashMap::new();
sizes.insert("char".into(), 1);
sizes.insert("short".into(), 2);
sizes.insert("int".into(), 4);
sizes.insert("long".into(), 8);
sizes.insert("long long".into(), 8);
sizes.insert("float".into(), 4);
sizes.insert("double".into(), 8);
sizes.insert("long double".into(), 16);
sizes.insert("void*".into(), 8);
sizes.insert("size_t".into(), 8);
sizes.insert("wchar_t".into(), 4);
let mut alignments = HashMap::new();
alignments.insert("char".into(), 1);
alignments.insert("short".into(), 2);
alignments.insert("int".into(), 4);
alignments.insert("long".into(), 8);
alignments.insert("long long".into(), 8);
alignments.insert("float".into(), 4);
alignments.insert("double".into(), 8);
alignments.insert("long double".into(), 16);
alignments.insert("void*".into(), 8);
Self { sizes, alignments }
}
pub fn size_of(&self, ty_name: &str) -> Option<u64> {
self.sizes.get(ty_name).copied()
}
pub fn align_of(&self, ty_name: &str) -> Option<u64> {
self.alignments.get(ty_name).copied()
}
pub fn register(&mut self, name: &str, size: u64, alignment: u64) {
self.sizes.insert(name.to_string(), size);
self.alignments.insert(name.to_string(), alignment);
}
}
impl Default for X86TypeSizeCache {
fn default() -> Self {
Self::x86_64_default()
}
}
#[derive(Debug, Clone)]
pub struct X86AnalysisContext {
pub subtarget: Option<X86Subtarget>,
pub call_graph: X86CallGraph,
pub summaries: HashMap<String, X86FunctionSummary>,
functions: HashMap<String, FunctionDecl>,
pub config: X86AnalyzerConfig,
pub memory_layout: X86MemoryLayout,
pub type_sizes: X86TypeSizeCache,
}
#[derive(Debug, Clone)]
pub struct X86AnalyzerConfig {
pub max_nodes: usize,
pub max_depth: u64,
pub interprocedural: bool,
pub emit_path_diagnostics: bool,
pub max_reports_per_function: usize,
pub analyze_inline_threshold: usize,
}
impl Default for X86AnalyzerConfig {
fn default() -> Self {
Self {
max_nodes: 100_000,
max_depth: 1000,
interprocedural: false,
emit_path_diagnostics: true,
max_reports_per_function: 10,
analyze_inline_threshold: 50,
}
}
}
impl X86AnalysisContext {
pub fn new(subtarget: Option<X86Subtarget>) -> Self {
Self {
subtarget,
call_graph: X86CallGraph::new(),
summaries: HashMap::new(),
functions: HashMap::new(),
config: X86AnalyzerConfig::default(),
memory_layout: X86MemoryLayout::default(),
type_sizes: X86TypeSizeCache::default(),
}
}
pub fn register_function(&mut self, decl: FunctionDecl) {
self.functions.insert(decl.name.clone(), decl);
}
pub fn register_summary(&mut self, name: &str, summary: X86FunctionSummary) {
self.summaries.insert(name.to_string(), summary);
}
}
#[derive(Debug, Clone)]
pub struct X86AnalysisResult {
pub reports: Vec<X86BugReport>,
pub stats: X86AnalyzerStats,
pub graph_summary: X86GraphSummary,
pub call_graph: X86CallGraph,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Default)]
pub struct X86GraphSummary {
pub total_nodes: usize,
pub total_edges: usize,
pub max_depth: u64,
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: X86CallGraph::new(),
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 || r.severity == BugSeverity::Critical)
}
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("=== X86 Static Analysis Results ===\n");
s.push_str(&format!("Duration: {}ms\n", self.duration_ms));
s.push_str(&format!(
"Functions analyzed: {}\n",
self.stats.functions_analyzed
));
s.push_str(&format!(
"Exploded nodes: {}\n",
self.graph_summary.total_nodes
));
s.push_str(&format!("Total bugs: {}\n", self.reports.len()));
for sev in &[
BugSeverity::Critical,
BugSeverity::Error,
BugSeverity::Warning,
BugSeverity::Style,
BugSeverity::Note,
] {
let c = self.count_by_severity(*sev);
if c > 0 {
s.push_str(&format!(" {sev}: {c}\n"));
}
}
s
}
pub fn to_sarif(&self, tool_name: &str, tool_version: &str) -> String {
let mut reporter = X86BugReporter::new();
reporter.reports = self.reports.clone();
reporter.to_sarif(tool_name, tool_version)
}
}
impl Default for X86AnalysisResult {
fn default() -> Self {
Self::new()
}
}
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: cond,
then: _,
els: _,
}
| Stmt::While {
cond: cond,
body: _,
} => {
extract_calls_from_expr(cond, &mut calls);
}
Stmt::For {
init: init,
cond: cond,
incr: incr,
body: _,
} => {
if let Some(e) = init.as_ref() {
if let Stmt::Expr(inner) = e.as_ref() {
extract_calls_from_expr(inner, &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);
}
}
_ => {}
}
calls
}
fn extract_calls_from_expr(expr: &Expr, calls: &mut Vec<String>) {
match expr {
Expr::Call {
callee: callee,
args: 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::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, Hash)]
pub enum X86RegisterClass {
GP64,
GP32,
GP16,
GP8,
XMM,
YMM,
ZMM,
FP87,
MM,
Segment,
Flags,
}
impl X86RegisterClass {
pub fn size_bytes(&self) -> u64 {
match self {
X86RegisterClass::GP64 | X86RegisterClass::MM => 8,
X86RegisterClass::GP32 => 4,
X86RegisterClass::GP16 => 2,
X86RegisterClass::GP8 => 1,
X86RegisterClass::XMM => 16,
X86RegisterClass::YMM => 32,
X86RegisterClass::ZMM => 64,
X86RegisterClass::FP87 => 10,
X86RegisterClass::Segment => 2,
X86RegisterClass::Flags => 8,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum X86CallingConventionSummary {
SystemV,
MicrosoftX64,
Cdecl,
StdCall,
FastCall,
ThisCall,
VectorCall,
}
impl X86CallingConventionSummary {
pub fn int_param_regs(&self) -> &[&str] {
match self {
X86CallingConventionSummary::SystemV => &["RDI", "RSI", "RDX", "RCX", "R8", "R9"],
X86CallingConventionSummary::MicrosoftX64 => &["RCX", "RDX", "R8", "R9"],
_ => &[],
}
}
pub fn is_callee_cleanup(&self) -> bool {
matches!(
self,
X86CallingConventionSummary::StdCall | X86CallingConventionSummary::ThisCall
)
}
pub fn return_reg(&self) -> &'static str {
"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 element = X86MemRegion::new_element(11, parent.id, 0, Some(QualType::int()));
assert_eq!(element.parent, Some(10));
assert_eq!(element.element_idx, Some(0));
assert!(element.is_sub_region());
}
#[test]
fn test_mem_region_may_alias() {
let mut regions = HashMap::new();
let r1 = X86MemRegion::new_stack(1, "a", Some(QualType::int()));
let r2 = X86MemRegion::new_element(2, 1, 0, Some(QualType::int()));
let r3 = X86MemRegion::new_stack(3, "b", Some(QualType::int()));
regions.insert(1, r1.clone());
regions.insert(2, r2.clone());
regions.insert(3, r3.clone());
assert!(r1.may_alias(&r2, ®ions));
assert!(!r1.may_alias(&r3, ®ions));
}
#[test]
fn test_mem_region_code_region() {
let region = X86MemRegion::new_code(5, "foo");
assert_eq!(region.kind, X86MemRegionKind::Code);
assert!(region.is_read_only);
assert_eq!(region.name, "foo");
}
#[test]
fn test_mem_region_alloca() {
let region = X86MemRegion::new_alloca(6, 256);
assert!(region.is_stack());
assert_eq!(region.size_bytes, 256);
}
#[test]
fn test_mem_region_kind_display() {
assert_eq!(X86MemRegionKind::Stack.to_string(), "Stack");
assert_eq!(X86MemRegionKind::Heap.to_string(), "Heap");
assert_eq!(X86MemRegionKind::Unknown.to_string(), "Unknown");
}
#[test]
fn test_x86_segment_display() {
assert_eq!(X86Segment::DS.to_string(), "DS");
assert_eq!(X86Segment::SS.to_string(), "SS");
assert_eq!(X86Segment::FS.to_string(), "FS");
}
#[test]
fn test_sval_is_undefined() {
assert!(X86SVal::Undefined.is_undefined());
assert!(!X86SVal::ConcreteInt(42, None, None).is_undefined());
}
#[test]
fn test_sval_is_null() {
assert!(X86SVal::Null.is_null());
assert!(!X86SVal::ConcreteInt(0, None, None).is_null());
}
#[test]
fn test_sval_is_zero() {
assert!(X86SVal::Zero.is_zero());
assert!(X86SVal::Null.is_zero());
assert!(X86SVal::ConcreteInt(0, None, None).is_zero());
assert!(!X86SVal::ConcreteInt(1, None, None).is_zero());
}
#[test]
fn test_sval_is_concrete() {
assert!(X86SVal::ConcreteInt(42, None, None).is_concrete());
assert!(X86SVal::Zero.is_concrete());
assert!(!X86SVal::Undefined.is_concrete());
assert!(!X86SVal::Unknown.is_concrete());
}
#[test]
fn test_sval_as_int() {
assert_eq!(X86SVal::ConcreteInt(42, None, None).as_int(), Some(42));
assert_eq!(X86SVal::Zero.as_int(), Some(0));
assert_eq!(X86SVal::Undefined.as_int(), None);
}
#[test]
fn test_sval_as_uint() {
assert_eq!(X86SVal::ConcreteUInt(42).as_uint(), Some(42));
assert_eq!(X86SVal::ConcreteInt(42, None, None).as_uint(), Some(42));
assert_eq!(X86SVal::ConcreteInt(-1, None, None).as_uint(), None);
}
#[test]
fn test_float_val_f32() {
let f = FloatVal::f32(0x40490FDB); 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());
}
#[test]
fn test_float_val_nan() {
let f = FloatVal::f32(0x7FC00000);
assert!(f.is_nan());
}
#[test]
fn test_float_val_inf() {
let f = FloatVal::f32(0x7F800000);
assert!(f.is_infinity());
}
#[test]
fn test_sval_display() {
assert_eq!(X86SVal::Undefined.to_string(), "Undefined");
assert_eq!(X86SVal::Null.to_string(), "null");
assert_eq!(X86SVal::Zero.to_string(), "0");
assert_eq!(X86SVal::ConcreteInt(42, None, None).to_string(), "42");
}
#[test]
fn test_range_constraint_intersect() {
let a = X86RangeConstraint::new(0, 10);
let b = X86RangeConstraint::new(5, 15);
let c = a.intersect(&b).unwrap();
assert_eq!(c.low, 5);
assert_eq!(c.high, 10);
}
#[test]
fn test_range_constraint_intersect_no_overlap() {
let a = X86RangeConstraint::new(0, 5);
let b = X86RangeConstraint::new(6, 10);
assert!(a.intersect(&b).is_none());
}
#[test]
fn test_range_constraint_union() {
let a = X86RangeConstraint::new(0, 5);
let b = X86RangeConstraint::new(4, 10);
let c = a.union(&b).unwrap();
assert_eq!(c.low, 0);
assert_eq!(c.high, 10);
}
#[test]
fn test_range_constraint_contains() {
let r = X86RangeConstraint::new(0, 100);
assert!(r.contains(50));
assert!(!r.contains(-1));
assert!(!r.contains(101));
}
#[test]
fn test_symbol_constraint_satisfies() {
let mut sc = X86SymbolConstraint::new();
sc.set_range(0, 10);
sc.exclude(5);
assert!(sc.satisfies(3));
assert!(!sc.satisfies(5));
assert!(!sc.satisfies(-1));
}
#[test]
fn test_symbol_constraint_is_satisfiable() {
let sc = X86SymbolConstraint::new();
assert!(sc.is_satisfiable());
}
#[test]
fn test_constraint_set_range() {
let mut cs = X86ConstraintSet::new();
cs.set_range("x", 0, 100);
assert_eq!(cs.get_range("x").unwrap().low, 0);
assert_eq!(cs.get_range("x").unwrap().high, 100);
}
#[test]
fn test_constraint_set_nullability() {
let mut cs = X86ConstraintSet::new();
cs.set_nullability("p", X86Nullability::NonNull);
assert!(cs.is_non_null("p"));
assert!(!cs.is_null("p"));
}
#[test]
fn test_constraint_set_taint() {
let mut cs = X86ConstraintSet::new();
cs.set_tainted("buf");
assert!(cs.is_tainted("buf"));
}
#[test]
fn test_path_constraint_variable() {
let pc = X86PathConstraint::Comparison("x".into(), ComparisonOp::Equal, 42);
assert_eq!(pc.variable(), "x");
}
#[test]
fn test_path_constraint_negate() {
let pc = X86PathConstraint::Comparison("x".into(), ComparisonOp::Equal, 42);
let neg = pc.negate();
assert_eq!(
neg,
X86PathConstraint::Comparison("x".into(), ComparisonOp::NotEqual, 42)
);
}
#[test]
fn test_comparison_op_from_binary() {
assert_eq!(
ComparisonOp::from_binary_op(&BinaryOp::Eq),
Some(ComparisonOp::Equal)
);
assert_eq!(
ComparisonOp::from_binary_op(&BinaryOp::Lt),
Some(ComparisonOp::LessThan)
);
assert_eq!(ComparisonOp::from_binary_op(&BinaryOp::Add), None);
}
#[test]
fn test_comparison_op_negate() {
assert_eq!(ComparisonOp::Equal.negate(), ComparisonOp::NotEqual);
assert_eq!(
ComparisonOp::LessThan.negate(),
ComparisonOp::GreaterOrEqual
);
}
#[test]
fn test_comparison_op_display() {
assert_eq!(ComparisonOp::Equal.to_string(), "==");
assert_eq!(ComparisonOp::NotEqual.to_string(), "!=");
}
#[test]
fn test_program_state_new() {
let state = X86ProgramState::new();
assert!(!state.infeasible);
assert!(!state.is_dead);
assert_eq!(state.depth, 0);
}
#[test]
fn test_program_state_bind_lookup() {
let mut state = X86ProgramState::new();
state.bind_value("x", X86SVal::ConcreteInt(42, None, None));
assert_eq!(state.lookup_value("x").as_int(), Some(42));
}
#[test]
fn test_program_state_allocate_free() {
let mut state = X86ProgramState::new();
let rid = state.allocate("p", 64);
assert!(state.is_allocated("p"));
state.free("p");
assert!(!state.is_allocated("p"));
assert!(state.is_freed("p"));
}
#[test]
fn test_program_state_lock() {
let mut state = X86ProgramState::new();
state.acquire_lock("mtx");
assert!(state.lock_held("mtx"));
state.release_lock("mtx");
assert!(!state.lock_held("mtx"));
}
#[test]
fn test_program_state_taint() {
let mut state = X86ProgramState::new();
state.taint("buf");
assert!(state.is_tainted("buf"));
assert!(!state.is_tainted("clean"));
}
#[test]
fn test_program_state_retain_count() {
let mut state = X86ProgramState::new();
state.retain("obj");
assert_eq!(state.retain_count("obj"), 1);
state.release("obj");
assert_eq!(state.retain_count("obj"), 0);
}
#[test]
fn test_program_state_merge() {
let mut s1 = X86ProgramState::new();
s1.bind_value("x", X86SVal::ConcreteInt(1, None, None));
let mut s2 = X86ProgramState::new();
s2.bind_value("x", X86SVal::ConcreteInt(1, None, None));
let merged = s1.merge(&s2);
assert_eq!(merged.lookup_value("x").as_int(), Some(1));
}
#[test]
fn test_program_state_merge_disagree() {
let mut s1 = X86ProgramState::new();
s1.bind_value("x", X86SVal::ConcreteInt(1, None, None));
let mut s2 = X86ProgramState::new();
s2.bind_value("x", X86SVal::ConcreteInt(2, None, None));
let merged = s1.merge(&s2);
assert!(matches!(merged.lookup_value("x"), X86SVal::Unknown));
}
#[test]
fn test_program_state_advance() {
let state = X86ProgramState::new();
let advanced = state.advance();
assert_eq!(advanced.depth, 1);
assert_eq!(advanced.stmt_idx, 1);
}
#[test]
fn test_exploded_graph_create_root() {
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(X86ProgramState::new());
assert!(root > 0);
assert_eq!(graph.node_count(), 1);
}
#[test]
fn test_exploded_graph_add_node() {
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(X86ProgramState::new());
let n1 = graph.add_node(
root,
"stmt".into(),
X86ProgramState::new(),
X86EdgeKind::Fallthrough,
"",
);
assert!(n1 > 0);
assert_eq!(graph.edge_count(), 1);
assert_eq!(graph.successors(root), vec![n1]);
}
#[test]
fn test_exploded_graph_infeasible_path() {
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(X86ProgramState::new());
let mut dead_state = X86ProgramState::new();
dead_state.mark_infeasible();
let n = graph.add_node(
root,
"dead".into(),
dead_state,
X86EdgeKind::FalseBranch,
"",
);
assert!(n > 0);
}
#[test]
fn test_exploded_graph_max_depth() {
let mut graph = X86ExplodedGraph::new();
graph.max_depth = 2;
let root = graph.create_root(X86ProgramState::new());
let n1 = graph.add_node(
root,
"s1".into(),
X86ProgramState::new().advance(),
X86EdgeKind::Fallthrough,
"",
);
assert!(n1 > 0);
let n2 = graph.add_node(
n1,
"s2".into(),
X86ProgramState::new().advance().advance(),
X86EdgeKind::Fallthrough,
"",
);
assert_eq!(n2, 0);
}
#[test]
fn test_exploded_graph_path_to_root() {
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(X86ProgramState::new());
let n1 = graph.add_node(
root,
"s1".into(),
X86ProgramState::new(),
X86EdgeKind::Fallthrough,
"",
);
let path = graph.path_to_root(n1);
assert_eq!(path.len(), 2);
assert_eq!(path[0], root);
assert_eq!(path[1], n1);
}
#[test]
fn test_bug_report_creation() {
let report = X86BugReport::new(
"test.Checker",
BugCategory::General,
BugSeverity::Warning,
"Test bug",
"This is a test",
"test.c",
10,
5,
);
assert_eq!(report.title, "Test bug");
assert_eq!(report.line, 10);
assert_eq!(report.column, 5);
assert!(!report.is_duplicate);
}
#[test]
fn test_bug_report_with_function() {
let report = X86BugReport::new(
"test.Checker",
BugCategory::General,
BugSeverity::Warning,
"Test",
"Desc",
"test.c",
1,
1,
)
.with_function("main");
assert_eq!(report.function, "main");
}
#[test]
fn test_bug_report_path_pieces() {
let mut report = X86BugReport::new(
"test.Checker",
BugCategory::General,
BugSeverity::Warning,
"Test",
"Desc",
"test.c",
1,
1,
);
report.add_note("condition is true", "test.c", 1, 10);
report.add_bug_point("null dereference here", "test.c", 2, 5, None);
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(
"test.Checker",
BugCategory::General,
BugSeverity::Warning,
"Test bug",
"Desc",
"test.c",
10,
5,
)
.with_function("main");
let display = format!("{report}");
assert!(display.contains("warning"));
assert!(display.contains("Test bug"));
assert!(display.contains("main"));
}
#[test]
fn test_bug_reporter_emit_deduplicate() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"test.C",
BugCategory::NullDereference,
BugSeverity::Error,
"Null deref",
"Desc",
"test.c",
10,
5,
);
assert_eq!(reporter.report_count(), 1);
reporter.emit_simple(
"test.C",
BugCategory::NullDereference,
BugSeverity::Error,
"Null deref",
"Desc",
"test.c",
10,
5,
);
assert_eq!(reporter.report_count(), 1);
}
#[test]
fn test_bug_reporter_sorted() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"c1",
BugCategory::General,
BugSeverity::Warning,
"W1",
"d",
"f",
1,
1,
);
reporter.emit_simple(
"c2",
BugCategory::General,
BugSeverity::Error,
"E1",
"d",
"f2",
2,
1,
);
let sorted = reporter.sorted_reports();
assert_eq!(sorted[0].severity, BugSeverity::Error);
}
#[test]
fn test_bug_reporter_summary() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"c1",
BugCategory::General,
BugSeverity::Error,
"E",
"d",
"f",
1,
1,
);
let summary = reporter.summary();
assert!(summary.contains("Total reports: 1"));
}
#[test]
fn test_sarif_output_basic() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"core.NullDereference",
BugCategory::NullDereference,
BugSeverity::Error,
"Null deref",
"Dereferencing null pointer",
"test.c",
10,
5,
);
let sarif = reporter.to_sarif("X86StaticAnalyzer", "1.0.0");
assert!(sarif.contains("\"version\": \"2.1.0\""));
assert!(sarif.contains("X86StaticAnalyzer"));
assert!(sarif.contains("core.NullDereference"));
assert!(sarif.contains("test.c"));
assert!(sarif.contains("\"startLine\": 10"));
}
#[test]
fn test_sarif_output_multiple_reports() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"c1",
BugCategory::Memory,
BugSeverity::Error,
"E1",
"d",
"a.c",
1,
1,
);
reporter.emit_simple(
"c2",
BugCategory::Security,
BugSeverity::Warning,
"W1",
"d",
"b.c",
2,
1,
);
let sarif = reporter.to_sarif("Tool", "1.0");
assert!(sarif.contains("\"ruleId\": \"c1\""));
assert!(sarif.contains("\"ruleId\": \"c2\""));
}
#[test]
fn test_sarif_escape() {
let s = X86BugReporter::sarif_escape("hello \"world\"\n");
assert!(s.contains("\\\""));
assert!(s.contains("\\n"));
}
#[test]
fn test_sarif_with_cwe() {
let mut report = X86BugReport::new(
"core.NullDereference",
BugCategory::NullDereference,
BugSeverity::Error,
"Null deref",
"Desc",
"test.c",
1,
1,
)
.with_cwe(476);
assert_eq!(report.cwe_id, Some(476));
}
#[test]
fn test_null_deref_detection() {
let mut state = X86ProgramState::new();
state.bind_value("p", X86SVal::Null);
let mut reporter = X86BugReporter::new();
let checker = X86NullDerefChecker::new();
let expr = Expr::Unary(
UnaryOp::Deref,
Box::new(Expr::Ident("p".into())),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
assert_eq!(bugs[0].severity, BugSeverity::Error);
}
#[test]
fn test_null_deref_zero_int_is_null() {
let mut state = X86ProgramState::new();
state.bind_value("p", X86SVal::ConcreteInt(0, None, None));
let mut reporter = X86BugReporter::new();
let checker = X86NullDerefChecker::new();
let expr = Expr::Unary(
UnaryOp::Deref,
Box::new(Expr::Ident("p".into())),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_undef_binary_op_division_by_zero() {
let mut state = X86ProgramState::new();
state.bind_value("a", X86SVal::ConcreteInt(10, None, None));
state.bind_value("b", X86SVal::Zero);
let mut reporter = X86BugReporter::new();
let checker = X86UndefResultChecker::new();
let expr = Expr::Binary(
Box::new(Expr::Ident("a".into())),
BinaryOp::Div,
Box::new(Expr::Ident("b".into())),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_div_zero_dedicated() {
let mut state = X86ProgramState::new();
state.bind_value("b", X86SVal::Zero);
let mut reporter = X86BugReporter::new();
let checker = X86DivZeroChecker::new();
let expr = Expr::Binary(
Box::new(Expr::Ident("a".into())),
BinaryOp::Mod,
Box::new(Expr::Ident("b".into())),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_return_undefined() {
let mut state = X86ProgramState::new();
state.bind_value("x", X86SVal::Undefined);
let mut reporter = X86BugReporter::new();
let checker = X86ReturnUndefinedChecker::new();
let stmt = Stmt::Return(Some(Expr::Ident("x".into())));
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_array_bounds_oob() {
let mut checker = X86ArrayBoundChecker::new();
checker.register_array("arr", 10);
let mut state = X86ProgramState::new();
state.bind_value("arr", X86SVal::Loc(100));
let mut reporter = X86BugReporter::new();
let expr = Expr::Subscript(
Box::new(Expr::Ident("arr".into())),
Box::new(Expr::IntLiteral(15)),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_lock_double_lock() {
let mut state = X86ProgramState::new();
state.acquire_lock("mtx");
let mut reporter = X86BugReporter::new();
let checker = X86PthreadLockChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("pthread_mutex_lock".into()))),
vec![Expr::Unary(
UnaryOp::AddrOf,
Box::new(Expr::Ident("mtx".into())),
QualType::int(),
)],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_unlock_without_lock() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86PthreadLockChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("pthread_mutex_unlock".into()))),
vec![Expr::Unary(
UnaryOp::AddrOf,
Box::new(Expr::Ident("mtx".into())),
QualType::int(),
)],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_retain_count_over_release() {
let mut state = X86ProgramState::new();
state.release("obj"); let mut reporter = X86BugReporter::new();
let checker = X86RetainCountChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("release".into()))),
vec![Expr::Ident("obj".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_gets_detected() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86SecuritySyntaxChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("gets".into()))),
vec![Expr::Ident("buf".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
assert!(bugs[0].description.contains("fgets"));
}
#[test]
fn test_strcpy_detected() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86SecuritySyntaxChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("strcpy".into()))),
vec![Expr::Ident("dst".into()), Expr::Ident("src".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_taint_source_marks_variable() {
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86TaintChecker::new();
let stmt = Stmt::Expr(Expr::Call(
Some(Box::new(Expr::Ident("read".into()))),
vec![
Expr::IntLiteral(0),
Expr::Ident("buf".into()),
Expr::IntLiteral(256),
],
));
let (succ_states, _bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert!(!succ_states.is_empty());
if !succ_states.is_empty() {
assert!(succ_states[0].is_tainted("buf"));
}
}
#[test]
fn test_taint_sink_detected() {
let mut state = X86ProgramState::new();
state.taint("cmd");
let mut reporter = X86BugReporter::new();
let checker = X86TaintChecker::new();
let stmt = Stmt::Expr(Expr::Call(
Some(Box::new(Expr::Ident("system".into()))),
vec![Expr::Ident("cmd".into())],
));
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert!(!bugs.is_empty());
}
#[test]
fn test_unreachable_const_true() {
let mut state = X86ProgramState::new();
state.bind_value("c", X86SVal::ConcreteInt(1, None, None));
let mut reporter = X86BugReporter::new();
let checker = X86UnreachableCodeChecker::new();
let stmt = Stmt::If(
Box::new(Expr::Ident("c".into())),
Box::new(Stmt::Null),
Some(Box::new(Stmt::Null)),
);
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert!(!bugs.is_empty());
}
#[test]
fn test_call_and_message_null_fn_ptr() {
let mut state = X86ProgramState::new();
state.bind_value("fp", X86SVal::Null);
let mut reporter = X86BugReporter::new();
let checker = X86CallAndMessageChecker::new();
let expr = Expr::Call(Some(Box::new(Expr::Ident("fp".into()))), vec![]);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_call_and_message_undefined_arg() {
let mut state = X86ProgramState::new();
state.bind_value("x", X86SVal::Undefined);
let mut reporter = X86BugReporter::new();
let checker = X86CallAndMessageChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("foo".into()))),
vec![Expr::Ident("x".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_unix_api_negative_fd() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86UnixAPIChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("close".into()))),
vec![Expr::IntLiteral(-1)],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_iterator_out_of_bounds() {
let mut checker = X86IteratorChecker::new();
checker.register_container("vec", 5);
checker.track_iterator("it", "vec", 10);
let mut state = X86ProgramState::new();
state.set_generic("iter:it", "vec:10");
let mut reporter = X86BugReporter::new();
let expr = Expr::Unary(
UnaryOp::Deref,
Box::new(Expr::Ident("it".into())),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_iterator_invalidation() {
let mut checker = X86IteratorChecker::new();
checker.track_iterator("it", "vec", 3);
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("push_back".into()))),
vec![Expr::Ident("vec".into()), Expr::IntLiteral(42)],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_format_string_non_literal() {
let mut state = X86ProgramState::new();
state.bind_value("fmt", X86SVal::SymbolInt("fmt".into()));
let mut reporter = X86BugReporter::new();
let checker = X86FormatStringChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("printf".into()))),
vec![Expr::Ident("fmt".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_shift_negative() {
let mut state = X86ProgramState::new();
state.bind_value("x", X86SVal::ConcreteInt(1, None, None));
let mut reporter = X86BugReporter::new();
let checker = X86ShiftChecker::new();
let expr = Expr::Binary(
Box::new(Expr::Ident("x".into())),
BinaryOp::Shl,
Box::new(Expr::IntLiteral(-1)),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_signed_overflow_add() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86SignedIntegerOverflowChecker::new();
let expr = Expr::Binary(
Box::new(Expr::IntLiteral(i64::MAX)),
BinaryOp::Add,
Box::new(Expr::IntLiteral(1)),
QualType::long_long(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_float_loop_counter_for() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86FloatLoopCounterChecker::new();
let stmt = Stmt::For(
Some(Box::new(Expr::Assign(
Box::new(Expr::Ident("i".into())),
Box::new(Expr::FloatLiteral(0.0)),
))),
Some(Box::new(Expr::Binary(
Box::new(Expr::Ident("i".into())),
BinaryOp::Lt,
Box::new(Expr::FloatLiteral(10.0)),
QualType::float_(),
))),
Some(Box::new(Expr::Assign(
Box::new(Expr::Ident("i".into())),
Box::new(Expr::FloatLiteral(1.0)),
))),
Box::new(Stmt::Null),
);
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_registry_register_and_enable() {
let mut registry = X86CheckerRegistry::new();
registry.register(Box::new(X86NullDerefChecker::new()));
assert_eq!(registry.count(), 1);
assert!(registry.is_enabled("core.NullDereference"));
}
#[test]
fn test_registry_disable() {
let mut registry = X86CheckerRegistry::new();
registry.register(Box::new(X86NullDerefChecker::new()));
registry.disable("core.NullDereference");
assert!(!registry.is_enabled("core.NullDereference"));
assert_eq!(registry.enabled_count(), 0);
}
#[test]
fn test_registry_with_all_defaults() {
let registry = X86CheckerRegistry::with_all_defaults(None);
assert!(registry.count() >= 20);
}
#[test]
fn test_analyzer_create_default() {
let analyzer = X86StaticAnalyzerEngine::with_defaults();
assert!(analyzer.registry.count() >= 20);
}
#[test]
fn test_analyzer_summary() {
let analyzer = X86StaticAnalyzerEngine::with_defaults();
let summary = analyzer.summary();
assert!(summary.contains("X86 Static Analyzer Summary"));
}
#[test]
fn test_analyzer_analyze_empty_tu() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
let tu = TranslationUnit::new("empty.c");
analyzer.analyze(&tu);
assert_eq!(analyzer.stats.functions_analyzed, 0);
}
#[test]
fn test_analyzer_full_pipeline_null_deref() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
let tu = TranslationUnit {
decls: vec![Decl::Function(FunctionDecl {
name: "test_fn".into(),
ret_ty: QualType::void(),
params: vec![],
body: Some(CompoundStmt {
stmts: vec![Stmt::Expr(Expr::Unary(
UnaryOp::Deref,
Box::new(Expr::Ident("null_ptr".into())),
QualType::int(),
))],
}),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
})],
filename: "test.c".into(),
};
analyzer.analyze(&tu);
assert_eq!(analyzer.stats.functions_analyzed, 1);
}
#[test]
fn test_analyzer_enable_disable_checker() {
let mut analyzer = X86StaticAnalyzerEngine::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_set_max_depth() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
analyzer.set_max_depth(500);
assert_eq!(analyzer.graph.max_depth, 500);
}
#[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);
assert!(layout.is_in_null_page(0));
assert!(!layout.is_in_null_page(4096));
assert!(layout.is_canonical(0x00007FFFFFFFFFFF));
}
#[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_type_size_cache() {
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));
}
#[test]
fn test_register_class_sizes() {
assert_eq!(X86RegisterClass::GP64.size_bytes(), 8);
assert_eq!(X86RegisterClass::GP32.size_bytes(), 4);
assert_eq!(X86RegisterClass::XMM.size_bytes(), 16);
assert_eq!(X86RegisterClass::YMM.size_bytes(), 32);
assert_eq!(X86RegisterClass::ZMM.size_bytes(), 64);
}
#[test]
fn test_calling_convention_int_param_regs() {
let cc = X86CallingConventionSummary::SystemV;
assert_eq!(cc.int_param_regs().len(), 6);
assert_eq!(cc.int_param_regs()[0], "RDI");
let ms = X86CallingConventionSummary::MicrosoftX64;
assert_eq!(ms.int_param_regs().len(), 4);
}
#[test]
fn test_calling_convention_callee_cleanup() {
assert!(!X86CallingConventionSummary::SystemV.is_callee_cleanup());
assert!(X86CallingConventionSummary::StdCall.is_callee_cleanup());
assert!(X86CallingConventionSummary::ThisCall.is_callee_cleanup());
}
#[test]
fn test_analysis_result_empty() {
let result = X86AnalysisResult::new();
assert!(!result.has_errors());
assert!(!result.has_critical());
}
#[test]
fn test_analysis_result_has_critical() {
let mut result = X86AnalysisResult::new();
result.reports.push(X86BugReport::new(
"test",
BugCategory::Security,
BugSeverity::Critical,
"C",
"d",
"f",
1,
1,
));
assert!(result.has_critical());
assert!(result.has_errors());
}
#[test]
fn test_analysis_result_summary_string() {
let mut result = X86AnalysisResult::new();
result.reports.push(X86BugReport::new(
"test",
BugCategory::General,
BugSeverity::Error,
"E",
"d",
"f",
1,
1,
));
let s = result.summary_string();
assert!(s.contains("Total bugs: 1"));
assert!(s.contains("error: 1"));
}
#[test]
fn test_analysis_result_to_sarif() {
let mut result = X86AnalysisResult::new();
result.reports.push(X86BugReport::new(
"test.Checker",
BugCategory::General,
BugSeverity::Warning,
"Test",
"Description",
"test.c",
5,
10,
));
let sarif = result.to_sarif("X86StaticAnalyzer", "1.0.0");
assert!(sarif.contains("X86StaticAnalyzer"));
assert!(sarif.contains("test.Checker"));
}
#[test]
fn test_function_summary_apply_alloc() {
let summary = X86FunctionSummary::malloc_summary();
assert!(summary.allocates_region);
}
#[test]
fn test_function_summary_apply_noreturn() {
let summary = X86FunctionSummary::noreturn_summary("abort");
assert!(summary.is_noreturn);
}
#[test]
fn test_call_graph_add_edge() {
let mut cg = X86CallGraph::new();
cg.add_edge("foo", "bar");
assert!(cg.has_edge("foo", "bar"));
assert!(!cg.has_edge("bar", "foo"));
}
#[test]
fn test_call_graph_callees() {
let mut cg = X86CallGraph::new();
cg.add_edge("main", "init");
cg.add_edge("main", "cleanup");
let callees = cg.callees_of("main");
assert_eq!(callees.len(), 2);
}
#[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("a", "b");
cg.add_edge("b", "c");
let order = cg.topological_order();
assert!(
order.iter().position(|x| x == "a").unwrap()
< order.iter().position(|x| x == "c").unwrap()
);
}
#[test]
fn test_call_graph_is_recursive() {
let mut cg = X86CallGraph::new();
cg.add_edge("f", "f");
assert!(cg.is_recursive("f"));
}
#[test]
fn test_extract_calls_simple() {
let stmt = Stmt::Expr(Expr::Call(
Some(Box::new(Expr::Ident("foo".into()))),
vec![],
));
let calls = extract_calls_from_stmt(&stmt);
assert!(calls.contains(&"foo".to_string()));
}
#[test]
fn test_multiple_checkers_registered() {
let registry = X86CheckerRegistry::with_all_defaults(None);
let names = registry.checker_names();
assert!(names.iter().any(|n| *n == "core.NullDereference"));
assert!(names.iter().any(|n| *n == "core.Malloc"));
assert!(names.iter().any(|n| *n == "core.Taint"));
assert!(names.iter().any(|n| *n == "security.SecuritySyntax"));
assert!(names.iter().any(|n| *n == "core.Iterator"));
}
#[test]
fn test_all_checkers_have_names() {
let registry = X86CheckerRegistry::with_all_defaults(None);
for name in registry.checker_names() {
assert!(!name.is_empty());
}
}
#[test]
fn test_all_checkers_have_unique_names() {
let registry = X86CheckerRegistry::with_all_defaults(None);
let names = registry.checker_names();
let unique: HashSet<&str> = names.iter().copied().collect();
assert_eq!(names.len(), unique.len());
}
#[test]
fn test_analyzer_with_subtarget() {
let subtarget = X86Subtarget::new("x86_64-unknown-linux-gnu", "generic", "");
let analyzer = X86StaticAnalyzerEngine::with_subtarget(subtarget);
assert!(analyzer.initial_state.subtarget.is_some());
}
#[test]
fn test_dead_store_self_assignment() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86DeadStoreChecker::new();
let stmt = Stmt::Expr(Expr::Assign(
Box::new(Expr::Ident("x".into())),
Box::new(Expr::Ident("x".into())),
));
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_return_stack_addr() {
let mut state = X86ProgramState::new();
state.register_var_region("local", 100);
let mut reporter = X86BugReporter::new();
let checker = X86ReturnStackChecker::new();
let stmt = Stmt::Return(Some(Expr::Unary(
UnaryOp::AddrOf,
Box::new(Expr::Ident("local".into())),
QualType::int(),
)));
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_range_constraint_singleton() {
let r = X86RangeConstraint::new(42, 42);
assert!(r.is_singleton());
assert_eq!(r.width(), 0);
}
#[test]
fn test_range_constraint_width() {
let r = X86RangeConstraint::new(10, 100);
assert_eq!(r.width(), 90);
}
#[test]
fn test_range_constraint_union_adjacent() {
let a = X86RangeConstraint::new(0, 5);
let b = X86RangeConstraint::new(6, 10);
let c = a.union(&b).unwrap();
assert_eq!(c.low, 0);
assert_eq!(c.high, 10);
}
#[test]
fn test_range_constraint_union_disjoint() {
let a = X86RangeConstraint::new(0, 5);
let b = X86RangeConstraint::new(7, 10);
assert!(a.union(&b).is_none());
}
#[test]
fn test_symbol_constraint_null_must_be_zero() {
let mut sc = X86SymbolConstraint::new();
sc.set_nullability(X86Nullability::Null);
assert!(sc.satisfies(0));
assert!(!sc.satisfies(1));
}
#[test]
fn test_symbol_constraint_intersect_narrows_range() {
let mut sc = X86SymbolConstraint::new();
sc.set_range(0, 100);
let other = X86SymbolConstraint {
range: Some(X86RangeConstraint::new(50, 150)),
..Default::default()
};
sc.intersect(&other);
assert_eq!(sc.range.unwrap().low, 50);
assert_eq!(sc.range.unwrap().high, 100);
}
#[test]
fn test_symbol_constraint_union_widens_range() {
let mut sc = X86SymbolConstraint::new();
sc.set_range(0, 10);
let other = X86SymbolConstraint {
range: Some(X86RangeConstraint::new(5, 15)),
..Default::default()
};
sc.union(&other);
assert_eq!(sc.range.unwrap().low, 0);
assert_eq!(sc.range.unwrap().high, 15);
}
#[test]
fn test_constraint_set_disequality() {
let mut cs = X86ConstraintSet::new();
cs.add_disequality("x", 0);
assert!(cs.disequalities.get("x").unwrap().contains(&0));
}
#[test]
fn test_constraint_set_merge_intersection() {
let mut cs1 = X86ConstraintSet::new();
cs1.set_range("x", 0, 10);
let mut cs2 = X86ConstraintSet::new();
cs2.set_range("x", 5, 15);
cs1.merge(&cs2, false);
let r = cs1.get_range("x").unwrap();
assert!(r.low >= 5);
assert!(r.high <= 10);
}
#[test]
fn test_program_state_merge_infeasible() {
let mut s1 = X86ProgramState::new();
s1.bind_value("x", X86SVal::ConcreteInt(5, None, None));
let mut s2 = X86ProgramState::new();
s2.mark_infeasible();
let merged = s1.merge(&s2);
assert_eq!(merged.lookup_value("x").as_int(), Some(5));
}
#[test]
fn test_program_state_merge_dead() {
let mut s1 = X86ProgramState::new();
s1.bind_value("x", X86SVal::ConcreteInt(10, None, None));
let mut s2 = X86ProgramState::new();
s2.mark_dead();
let merged = s1.merge(&s2);
assert_eq!(merged.lookup_value("x").as_int(), Some(10));
}
#[test]
fn test_program_state_store_load_region() {
let mut state = X86ProgramState::new();
state.store_region(100, X86SVal::ConcreteInt(42, None, None));
let loaded = state.load_region(100);
assert_eq!(loaded.as_int(), Some(42));
}
#[test]
fn test_program_state_generic_data() {
let mut state = X86ProgramState::new();
state.set_generic("key", "value");
assert_eq!(state.get_generic("key"), Some("value"));
assert_eq!(state.get_generic("missing"), None);
}
#[test]
fn test_program_state_constrain_non_null() {
let mut state = X86ProgramState::new();
state.assume_true(&X86PathConstraint::NonNull("p".into()));
assert!(state.constraints.is_non_null("p"));
}
#[test]
fn test_program_state_constrain_must_be_null() {
let mut state = X86ProgramState::new();
state.assume_true(&X86PathConstraint::MustBeNull("p".into()));
assert!(state.constraints.is_null("p"));
}
#[test]
fn test_program_state_constrain_range() {
let mut state = X86ProgramState::new();
state.assume_true(&X86PathConstraint::Range("i".into(), 0, 9));
let r = state.constraints.get_range("i").unwrap();
assert_eq!(r.low, 0);
assert_eq!(r.high, 9);
}
#[test]
fn test_program_state_constrain_equal() {
let mut state = X86ProgramState::new();
state.assume_true(&X86PathConstraint::Comparison(
"x".into(),
ComparisonOp::Equal,
42,
));
assert_eq!(state.constraints.get_equal("x"), Some(42));
}
#[test]
fn test_program_state_constrain_not_equal() {
let mut state = X86ProgramState::new();
state.assume_true(&X86PathConstraint::Comparison(
"x".into(),
ComparisonOp::NotEqual,
0,
));
assert!(state.is_constrained_true(&X86PathConstraint::Comparison(
"x".into(),
ComparisonOp::NotEqual,
0
)));
}
#[test]
fn test_program_state_assume_false() {
let mut state = X86ProgramState::new();
state.assume_false(&X86PathConstraint::NonNull("p".into()));
assert!(state.constraints.is_null("p"));
}
#[test]
fn test_exploded_graph_mark_processed() {
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(X86ProgramState::new());
graph.mark_processed(root);
assert!(graph.nodes[&root].processed);
}
#[test]
fn test_exploded_graph_pop_worklist() {
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(X86ProgramState::new());
assert_eq!(graph.pop_worklist(), Some(root));
assert!(graph.is_worklist_empty());
}
#[test]
fn test_exploded_graph_sink_nodes() {
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(X86ProgramState::new());
let mut dead_state = X86ProgramState::new();
dead_state.mark_infeasible();
graph.add_node(
root,
"sink".into(),
dead_state,
X86EdgeKind::Fallthrough,
"",
);
let sinks = graph.sink_nodes();
assert!(!sinks.is_empty());
}
#[test]
fn test_bug_report_with_cwe() {
let report = X86BugReport::new(
"test.Checker",
BugCategory::Security,
BugSeverity::Error,
"Test",
"Desc",
"test.c",
1,
1,
)
.with_cwe(120);
assert_eq!(report.cwe_id, Some(120));
}
#[test]
fn test_bug_report_fixit() {
let mut report = X86BugReport::new(
"test.Checker",
BugCategory::Security,
BugSeverity::Warning,
"Test",
"Desc",
"test.c",
1,
1,
);
report.add_bug_point("issue here", "test.c", 1, 1, None);
report.add_fixit(FixItHint::Insertion {
line: 1,
column: 1,
text: "/* FIX */".into(),
});
assert!(report.path.last().unwrap().fixit.is_some());
}
#[test]
fn test_bug_reporter_max_per_function() {
let mut reporter = X86BugReporter::new();
reporter.max_reports_per_function = 2;
for i in 0..5 {
let mut report = X86BugReport::new(
"test.C",
BugCategory::General,
BugSeverity::Warning,
&format!("Bug {i}"),
"d",
&format!("file{i}.c"),
i + 1,
1,
)
.with_function("main");
reporter.emit(&mut report);
}
assert!(reporter.report_count() <= 2);
}
#[test]
fn test_malloc_zero_size_warning() {
let mut state = X86ProgramState::new();
state.bind_value("sz", X86SVal::Zero);
let mut reporter = X86BugReporter::new();
let checker = X86MallocChecker::new();
let stmt = Stmt::Expr(Expr::Call(
Some(Box::new(Expr::Ident("malloc".into()))),
vec![Expr::Ident("sz".into())],
));
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert_eq!(bugs.len(), 1);
assert!(bugs[0].title.contains("Zero-size"));
}
#[test]
fn test_malloc_double_free() {
let mut state = X86ProgramState::new();
state.allocate("p", 64);
state.free("p"); let mut reporter = X86BugReporter::new();
let checker = X86MallocChecker::new();
let stmt = Stmt::Expr(Expr::Call(
Some(Box::new(Expr::Ident("free".into()))),
vec![Expr::Ident("p".into())],
));
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert_eq!(bugs.len(), 1);
assert!(bugs[0].title.contains("Double free"));
}
#[test]
fn test_malloc_leak_detection() {
let mut state = X86ProgramState::new();
state.allocate("leaked", 128);
let mut reporter = X86BugReporter::new();
let checker = X86MallocChecker::new();
let bugs = checker.check_end_of_function(&state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_vla_zero_size() {
let decl = VarDecl {
name: "arr".into(),
ty: QualType::new(TypeNode::Array(
Box::new(TypeNode::Int),
Some(Box::new(Expr::IntLiteral(0))),
)),
init: None,
linkage: Linkage::None,
is_global: false,
is_extern: false,
is_static: false,
};
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86VLAChecker::new();
let bugs = checker.check_var_decl(&decl, &mut state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_vla_negative_size() {
let decl = VarDecl {
name: "bad_arr".into(),
ty: QualType::new(TypeNode::Array(
Box::new(TypeNode::Int),
Some(Box::new(Expr::IntLiteral(-5))),
)),
init: None,
linkage: Linkage::None,
is_global: false,
is_extern: false,
is_static: false,
};
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86VLAChecker::new();
let bugs = checker.check_var_decl(&decl, &mut state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_array_bounds_negative_index() {
let mut checker = X86ArrayBoundChecker::new();
checker.register_array("arr", 10);
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::Subscript(
Box::new(Expr::Ident("arr".into())),
Box::new(Expr::IntLiteral(-1)),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_array_bounds_in_bounds() {
let mut checker = X86ArrayBoundChecker::new();
checker.register_array("arr", 10);
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::Subscript(
Box::new(Expr::Ident("arr".into())),
Box::new(Expr::IntLiteral(5)),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 0);
}
#[test]
fn test_nonnull_multiple_params() {
let mut checker = X86NonNullChecker::new();
checker.register_nonnull("foo", vec![0, 1]);
let mut state = X86ProgramState::new();
state.bind_value("a", X86SVal::Null);
let mut reporter = X86BugReporter::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("foo".into()))),
vec![Expr::Ident("a".into()), Expr::Ident("b".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_nonnull_no_violation() {
let mut checker = X86NonNullChecker::new();
checker.register_nonnull("bar", vec![0]);
let mut state = X86ProgramState::new();
state.bind_value("x", X86SVal::ConcreteInt(1, None, None));
let mut reporter = X86BugReporter::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("bar".into()))),
vec![Expr::Ident("x".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 0);
}
#[test]
fn test_enum_checker_registration() {
let mut checker = X86EnumChecker::new();
checker.register_enum("Color", 0, 2);
assert_eq!(checker.enum_ranges.get("Color"), Some(&(0, 2)));
}
#[test]
fn test_enum_checker_out_of_range() {
let mut checker = X86EnumChecker::new();
checker.register_enum("Color", 0, 2);
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::Assign(
Box::new(Expr::Ident("Color".into())),
Box::new(Expr::IntLiteral(5)),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_cast_ptr_truncation() {
let checker = X86CastChecker::new();
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::Cast(
QualType::int(),
Box::new(Expr::Ident("ptr".into())),
QualType::pointer_to(QualType::int()),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
let _ = bugs;
}
#[test]
fn test_shift_exactly_at_width() {
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86ShiftChecker::new();
let expr = Expr::Binary(
Box::new(Expr::Ident("x".into())),
BinaryOp::Shl,
Box::new(Expr::IntLiteral(64)),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_shift_within_bounds() {
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86ShiftChecker::new();
let expr = Expr::Binary(
Box::new(Expr::Ident("x".into())),
BinaryOp::Shr,
Box::new(Expr::IntLiteral(31)),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 0);
}
#[test]
fn test_signed_overflow_mul() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86SignedIntegerOverflowChecker::new();
let expr = Expr::Binary(
Box::new(Expr::IntLiteral(i64::MAX)),
BinaryOp::Mul,
Box::new(Expr::IntLiteral(2)),
QualType::long_long(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_signed_overflow_div_min_by_neg_one() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86SignedIntegerOverflowChecker::new();
let expr = Expr::Binary(
Box::new(Expr::IntLiteral(i64::MIN)),
BinaryOp::Div,
Box::new(Expr::IntLiteral(-1)),
QualType::long_long(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_no_overflow_on_safe_add() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86SignedIntegerOverflowChecker::new();
let expr = Expr::Binary(
Box::new(Expr::IntLiteral(1)),
BinaryOp::Add,
Box::new(Expr::IntLiteral(2)),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 0);
}
#[test]
fn test_float_while_loop_counter() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86FloatLoopCounterChecker::new();
let stmt = Stmt::While(
Box::new(Expr::Binary(
Box::new(Expr::Ident("f".into())),
BinaryOp::Lt,
Box::new(Expr::FloatLiteral(1.0)),
QualType::float_(),
)),
Box::new(Stmt::Null),
);
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_lock_held_at_exit() {
let mut state = X86ProgramState::new();
state.acquire_lock("mtx");
let mut reporter = X86BugReporter::new();
let checker = X86PthreadLockChecker::new();
let bugs = checker.check_end_of_function(&state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_lock_multiple_different_mutexes_ok() {
let mut state = X86ProgramState::new();
state.acquire_lock("mtx1");
let mut reporter = X86BugReporter::new();
let checker = X86PthreadLockChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("pthread_mutex_lock".into()))),
vec![Expr::Unary(
UnaryOp::AddrOf,
Box::new(Expr::Ident("mtx2".into())),
QualType::int(),
)],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 0);
}
#[test]
fn test_retain_count_leak() {
let mut state = X86ProgramState::new();
state.retain("obj");
state.retain("obj");
let mut reporter = X86BugReporter::new();
let checker = X86RetainCountChecker::new();
let bugs = checker.check_end_of_function(&state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_retain_count_excessive() {
let mut state = X86ProgramState::new();
for _ in 0..101 {
state.retain("obj");
}
let mut reporter = X86BugReporter::new();
let checker = X86RetainCountChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("retain".into()))),
vec![Expr::Ident("obj".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_unreachable_noreturn() {
let mut state = X86ProgramState::new();
state.mark_dead();
let mut reporter = X86BugReporter::new();
let checker = X86UnreachableCodeChecker::new();
let stmt = Stmt::Expr(Expr::Ident("x".into()));
let (succ_states, _) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
if !succ_states.is_empty() {
assert!(succ_states[0].is_dead);
}
}
#[test]
fn test_format_string_too_few_args() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86FormatStringChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("printf".into()))),
vec![Expr::StringLiteral("%d %d".into())],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_format_string_literal_ok() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86FormatStringChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("printf".into()))),
vec![Expr::StringLiteral("%d".into()), Expr::IntLiteral(42)],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 0);
}
#[test]
fn test_taint_propagation_through_assign() {
let mut state = X86ProgramState::new();
state.taint("src");
let mut reporter = X86BugReporter::new();
let checker = X86TaintChecker::new();
let stmt = Stmt::Expr(Expr::Assign(
Box::new(Expr::Ident("dst".into())),
Box::new(Expr::Ident("src".into())),
));
let (succ_states, _) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
if !succ_states.is_empty() {
assert!(succ_states[0].is_tainted("dst"));
}
}
#[test]
fn test_taint_propagation_through_strcpy() {
let mut state = X86ProgramState::new();
state.taint("src");
let mut reporter = X86BugReporter::new();
let checker = X86TaintChecker::new();
let stmt = Stmt::Expr(Expr::Call(
Some(Box::new(Expr::Ident("strcpy".into()))),
vec![Expr::Ident("dst".into()), Expr::Ident("src".into())],
));
let (succ_states, _) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
if !succ_states.is_empty() {
assert!(succ_states[0].is_tainted("dst"));
}
}
#[test]
fn test_iterator_valid_access() {
let mut checker = X86IteratorChecker::new();
checker.register_container("vec", 10);
checker.track_iterator("it", "vec", 5);
let mut state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let expr = Expr::Unary(
UnaryOp::Deref,
Box::new(Expr::Ident("it".into())),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 0);
}
#[test]
fn test_unix_api_open_negative_flags() {
let state = X86ProgramState::new();
let mut reporter = X86BugReporter::new();
let checker = X86UnixAPIChecker::new();
let expr = Expr::Call(
Some(Box::new(Expr::Ident("open".into()))),
vec![
Expr::StringLiteral("/dev/null".into()),
Expr::IntLiteral(-1),
],
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert!(!bugs.is_empty());
}
#[test]
fn test_security_all_dangerous_functions() {
let checker = X86SecuritySyntaxChecker::new();
assert!(checker.dangerous_functions.contains("gets"));
assert!(checker.dangerous_functions.contains("strcpy"));
assert!(checker.dangerous_functions.contains("sprintf"));
assert!(checker.dangerous_functions.contains("rand"));
}
#[test]
fn test_security_replacements() {
let checker = X86SecuritySyntaxChecker::new();
assert_eq!(checker.replacements.get("gets"), Some(&"fgets".to_string()));
assert_eq!(
checker.replacements.get("strcpy"),
Some(&"strncpy".to_string())
);
}
#[test]
fn test_eval_literal_helpers() {
let state = X86ProgramState::new();
assert_eq!(
eval_literal(&state, &Expr::IntLiteral(42)),
X86SVal::ConcreteInt(42, None, None)
);
assert_eq!(
eval_literal(&state, &Expr::CharLiteral('A')),
X86SVal::ConcreteInt(65, None, None)
);
assert_eq!(
eval_literal(&state, &Expr::UIntLiteral(100, false)),
X86SVal::ConcreteUInt(100)
);
}
#[test]
fn test_extract_base_helpers() {
let expr = Expr::Member(Box::new(Expr::Ident("p".into())), "field".into(), true);
let base = extract_base(&expr);
assert!(base.is_some());
assert_eq!(base.unwrap(), &Expr::Ident("p".into()));
}
#[test]
fn test_extract_base_subscript() {
let expr = Expr::Subscript(
Box::new(Expr::Ident("arr".into())),
Box::new(Expr::IntLiteral(0)),
);
let base = extract_base(&expr);
assert!(base.is_some());
assert_eq!(base.unwrap(), &Expr::Ident("arr".into()));
}
#[test]
fn test_extract_calls_nested() {
let stmt = Stmt::Expr(Expr::Call(
Some(Box::new(Expr::Ident("foo".into()))),
vec![Expr::Call(
Some(Box::new(Expr::Ident("bar".into()))),
vec![],
)],
));
let calls = extract_calls_from_stmt(&stmt);
assert!(calls.contains(&"foo".to_string()));
assert!(calls.contains(&"bar".to_string()));
}
#[test]
fn test_calling_convention_return_reg() {
assert_eq!(
X86CallingConventionSummary::SystemV.return_reg(),
"RAX/EAX/AX/AL"
);
assert_eq!(
X86CallingConventionSummary::Cdecl.return_reg(),
"RAX/EAX/AX/AL"
);
}
#[test]
fn test_register_class_all_sizes() {
assert_eq!(X86RegisterClass::GP8.size_bytes(), 1);
assert_eq!(X86RegisterClass::GP16.size_bytes(), 2);
assert_eq!(X86RegisterClass::GP32.size_bytes(), 4);
assert_eq!(X86RegisterClass::GP64.size_bytes(), 8);
assert_eq!(X86RegisterClass::MM.size_bytes(), 8);
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::Segment.size_bytes(), 2);
assert_eq!(X86RegisterClass::Flags.size_bytes(), 8);
}
#[test]
fn test_analysis_context_new() {
let ctx = X86AnalysisContext::new(None);
assert!(ctx.subtarget.is_none());
assert_eq!(ctx.config.max_nodes, 100_000);
}
#[test]
fn test_analysis_context_with_subtarget() {
let st = X86Subtarget::new("x86_64-unknown-linux-gnu", "generic", "");
let ctx = X86AnalysisContext::new(Some(st));
assert!(ctx.subtarget.is_some());
}
#[test]
fn test_analysis_context_register_function() {
let mut ctx = X86AnalysisContext::new(None);
let func = FunctionDecl {
name: "my_func".into(),
ret_ty: QualType::int(),
params: vec![],
body: None,
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
};
ctx.register_function(func);
}
#[test]
fn test_function_summary_apply_locks() {
let summary = X86FunctionSummary {
name: "locked_fn".into(),
allocates_region: false,
deallocates_region: false,
is_noreturn: false,
acquires_locks: vec!["mtx".into()],
releases_locks: vec!["other".into()],
taint_source: false,
taint_sink: false,
};
let mut state = X86ProgramState::new();
state.acquire_lock("other");
summary.apply(&mut state);
assert!(state.lock_held("mtx"));
assert!(!state.lock_held("other"));
}
#[test]
fn test_function_summary_noreturn_dead() {
let summary = X86FunctionSummary::noreturn_summary("abort");
let mut state = X86ProgramState::new();
summary.apply(&mut state);
assert!(state.is_dead);
}
#[test]
fn test_sarif_empty_reporter() {
let reporter = X86BugReporter::new();
let sarif = reporter.to_sarif("Tool", "1.0");
assert!(sarif.contains("\"results\": ["));
}
#[test]
fn test_sarif_with_path_diagnostics() {
let mut reporter = X86BugReporter::new();
reporter.emit_path_diagnostics = true;
let mut report = X86BugReport::new(
"test.C",
BugCategory::NullDereference,
BugSeverity::Error,
"Null deref",
"Description",
"test.c",
10,
5,
);
report.add_note("Assuming ptr is null", "test.c", 8, 3);
report.add_bug_point("Dereference occurs here", "test.c", 10, 5, None);
reporter.emit(&mut report);
let sarif = reporter.to_sarif("Tool", "1.0");
assert!(sarif.contains("relatedLocations"));
}
#[test]
fn test_sarif_multiple_rules() {
let mut reporter = X86BugReporter::new();
reporter.emit_simple(
"core.NullDereference",
BugCategory::NullDereference,
BugSeverity::Error,
"ND",
"d",
"a.c",
1,
1,
);
reporter.emit_simple(
"core.Malloc",
BugCategory::Memory,
BugSeverity::Warning,
"ML",
"d",
"b.c",
2,
1,
);
let sarif = reporter.to_sarif("Tool", "1.0");
assert!(sarif.contains("core.NullDereference"));
assert!(sarif.contains("core.Malloc"));
}
#[test]
fn test_mem_region_field_region() {
let parent = X86MemRegion::new_stack(1, "s", Some(QualType::int()));
let field = X86MemRegion::new_field(2, parent.id, 0, "x", Some(QualType::int()));
assert_eq!(field.field_name, Some("x".to_string()));
assert_eq!(field.field_idx, Some(0));
assert_eq!(field.name, ".x");
}
#[test]
fn test_mem_region_pointee() {
let region = X86MemRegion::new_pointee(1, "ptr", Some(QualType::int()));
assert_eq!(region.kind, X86MemRegionKind::Pointee);
assert_eq!(region.name, "*ptr");
}
#[test]
fn test_mem_region_unknown() {
let region = X86MemRegion::new_unknown(999);
assert_eq!(region.kind, X86MemRegionKind::Unknown);
assert!(region.name.contains("unknown"));
}
#[test]
fn test_mem_region_x86_size_known() {
let region = X86MemRegion::new_stack(1, "x", Some(QualType::int()));
assert_eq!(region.x86_size(), 4);
}
#[test]
fn test_mem_region_x86_size_explicit() {
let mut region = X86MemRegion::new_heap(1, "buf", 1024);
assert_eq!(region.x86_size(), 1024);
region.size_bytes = 0;
assert_eq!(region.x86_size(), 0);
}
#[test]
fn test_mem_region_root_region_chain() {
let mut regions = HashMap::new();
let root = X86MemRegion::new_stack(1, "root", None);
let child = X86MemRegion::new_field(2, 1, 0, "f", None);
let grandchild = X86MemRegion::new_element(3, 2, 0, None);
regions.insert(1, root.clone());
regions.insert(2, child.clone());
regions.insert(3, grandchild.clone());
assert_eq!(grandchild.root_region(®ions), 1);
}
#[test]
fn test_sval_is_known() {
assert!(!X86SVal::Undefined.is_known());
assert!(!X86SVal::Unknown.is_known());
assert!(X86SVal::ConcreteInt(1, None, None).is_known());
assert!(X86SVal::Null.is_known());
}
#[test]
fn test_sval_as_loc() {
let sval = X86SVal::Loc(42);
assert_eq!(sval.as_loc(), Some(42));
assert_eq!(X86SVal::ConcreteInt(1, None, None).as_loc(), None);
}
#[test]
fn test_sval_constraint_key() {
let sval = X86SVal::SymbolInt("x".into());
assert_eq!(sval.constraint_key(), "x");
}
#[test]
fn test_float_val_partial_eq() {
let a = FloatVal::f32(0x40490FDB);
let b = FloatVal::f32(0x40490FDB);
assert_eq!(a, b);
}
#[test]
fn test_float_val_ne() {
let a = FloatVal::f32(0x40490FDB);
let b = FloatVal::f64(0x40490FDB);
assert_ne!(a, b);
}
#[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_sarif_level() {
assert_eq!(BugSeverity::Note.to_sarif_level(), "note");
assert_eq!(BugSeverity::Warning.to_sarif_level(), "warning");
assert_eq!(BugSeverity::Error.to_sarif_level(), "error");
assert_eq!(BugSeverity::Critical.to_sarif_level(), "error");
}
#[test]
fn test_bug_category_displays() {
let cats = [
BugCategory::NullDereference,
BugCategory::Memory,
BugCategory::Security,
BugCategory::Taint,
BugCategory::Locking,
BugCategory::RetainCount,
BugCategory::UnreachableCode,
BugCategory::API,
BugCategory::Iterator,
BugCategory::DeadStore,
BugCategory::CallAndMessage,
];
for cat in &cats {
let s = cat.to_string();
assert!(!s.is_empty());
}
}
#[test]
fn test_fixit_hint_insertion() {
let hint = FixItHint::Insertion {
line: 5,
column: 10,
text: "const ".into(),
};
match hint {
FixItHint::Insertion { line, column, text } => {
assert_eq!(line, 5);
assert_eq!(column, 10);
assert_eq!(text, "const ");
}
_ => 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: "fgets".into(),
};
match hint {
FixItHint::Replacement {
start_line,
start_col,
end_line,
end_col,
new_text,
} => {
assert_eq!(start_line, 1);
assert_eq!(end_col, 5);
assert_eq!(new_text, "fgets");
}
_ => panic!("Expected Replacement"),
}
}
#[test]
fn test_fixit_hint_removal() {
let hint = FixItHint::Removal {
start_line: 10,
start_col: 1,
end_line: 10,
end_col: 20,
};
match hint {
FixItHint::Removal {
start_line,
end_line,
..
} => {
assert_eq!(start_line, 10);
assert_eq!(end_line, 10);
}
_ => panic!("Expected Removal"),
}
}
#[test]
fn test_analyzer_config_defaults() {
let config = X86AnalyzerConfig::default();
assert_eq!(config.max_nodes, 100_000);
assert_eq!(config.max_depth, 1000);
assert!(!config.interprocedural);
assert!(config.emit_path_diagnostics);
}
#[test]
fn test_analyzer_set_max_nodes() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
analyzer.set_max_nodes(50_000);
assert_eq!(analyzer.max_nodes, 50_000);
}
#[test]
fn test_analyzer_enabled_checker_names() {
let analyzer = X86StaticAnalyzerEngine::with_defaults();
let names = analyzer.enabled_checker_names();
assert!(!names.is_empty());
assert!(names.contains(&"core.NullDereference".to_string()));
}
#[test]
fn test_analyzer_reporter_access() {
let analyzer = X86StaticAnalyzerEngine::with_defaults();
assert_eq!(analyzer.reporter().report_count(), 0);
}
#[test]
fn test_analyzer_reporter_mut() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
analyzer.reporter_mut().emit_simple(
"test",
BugCategory::General,
BugSeverity::Note,
"T",
"d",
"f",
1,
1,
);
assert_eq!(analyzer.reporter().report_count(), 1);
}
#[test]
fn test_analyzer_stats_default() {
let stats = X86AnalyzerStats::default();
assert_eq!(stats.functions_analyzed, 0);
assert_eq!(stats.bugs_found, 0);
assert_eq!(stats.time_ms, 0);
}
#[test]
fn test_memory_layout_is_canonical_64() {
let layout = X86MemoryLayout::x86_64();
assert!(layout.is_canonical(0x00007FFFFFFFFFFF));
assert!(layout.is_canonical(0xFFFF800000000000));
assert!(!layout.is_canonical(0x0000800000000000));
assert!(!layout.is_canonical(0xFFFF7FFFFFFFFFFF));
}
#[test]
fn test_memory_layout_is_canonical_32() {
let layout = X86MemoryLayout::x86_32();
assert!(layout.is_canonical(0xFFFFFFFF));
assert!(layout.is_canonical(0x0));
}
#[test]
fn test_type_size_cache_register_custom() {
let mut cache = X86TypeSizeCache::x86_64_default();
cache.register("my_type", 24, 8);
assert_eq!(cache.size_of("my_type"), Some(24));
assert_eq!(cache.align_of("my_type"), Some(8));
}
#[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("double"), Some(8));
}
#[test]
fn test_graph_summary_default() {
let summary = X86GraphSummary::default();
assert_eq!(summary.total_nodes, 0);
assert_eq!(summary.total_edges, 0);
}
#[test]
fn test_analysis_result_export_sarif() {
let mut result = X86AnalysisResult::new();
result.stats.functions_analyzed = 5;
result.stats.bugs_found = 3;
result.graph_summary = X86GraphSummary {
total_nodes: 100,
total_edges: 150,
max_depth: 20,
feasible_paths: 10,
infeasible_paths: 3,
dead_ends: 2,
};
let sarif = result.to_sarif("X86StaticAnalyzer", "2.0.0");
assert!(sarif.contains("2.0.0"));
assert!(sarif.contains("X86StaticAnalyzer"));
}
#[test]
fn test_multi_checker_null_deref_and_security() {
let mut state = X86ProgramState::new();
state.bind_value("p", X86SVal::Null);
let mut reporter = X86BugReporter::new();
let null_checker = X86NullDerefChecker::new();
let security_checker = X86SecuritySyntaxChecker::new();
let deref_expr = Expr::Unary(
UnaryOp::Deref,
Box::new(Expr::Ident("p".into())),
QualType::int(),
);
let call_expr = Expr::Call(
Some(Box::new(Expr::Ident("gets".into()))),
vec![Expr::Ident("buf".into())],
);
let bugs1 = null_checker.check_expr(&deref_expr, &state, &mut reporter);
let bugs2 = security_checker.check_expr(&call_expr, &state, &mut reporter);
assert!(!bugs1.is_empty());
assert!(!bugs2.is_empty());
assert_eq!(reporter.report_count(), 2);
}
#[test]
fn test_analyzer_handles_if_branch() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
let tu = TranslationUnit {
decls: vec![Decl::Function(FunctionDecl {
name: "branch_fn".into(),
ret_ty: QualType::void(),
params: vec![],
body: Some(CompoundStmt {
stmts: vec![Stmt::If(
Box::new(Expr::IntLiteral(1)),
Box::new(Stmt::Return(None)),
Some(Box::new(Stmt::Null)),
)],
}),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
})],
filename: "branch.c".into(),
};
analyzer.analyze(&tu);
assert_eq!(analyzer.stats.functions_analyzed, 1);
}
#[test]
fn test_analyzer_loop_body_analyzed() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
let tu = TranslationUnit {
decls: vec![Decl::Function(FunctionDecl {
name: "loop_fn".into(),
ret_ty: QualType::void(),
params: vec![],
body: Some(CompoundStmt {
stmts: vec![Stmt::While(
Box::new(Expr::IntLiteral(0)),
Box::new(Stmt::Null),
)],
}),
is_vararg: false,
linkage: Linkage::External,
is_inline: false,
is_noreturn: false,
})],
filename: "loop.c".into(),
};
analyzer.analyze(&tu);
assert_eq!(analyzer.stats.functions_analyzed, 1);
}
#[test]
fn test_analysis_with_result_has_duration() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
let tu = TranslationUnit::new("empty.c");
let result = analyzer.analyze_with_result(&tu);
assert!(result.duration_ms > 0 || result.duration_ms == 0);
assert_eq!(result.stats.functions_analyzed, 0);
}
#[test]
fn test_null_deref_undefined_warning() {
let checker = X86NullDerefChecker::new();
let mut state = X86ProgramState::new();
state.bind_value("q", X86SVal::Undefined);
let mut reporter = X86BugReporter::new();
let expr = Expr::Unary(
UnaryOp::Deref,
Box::new(Expr::Ident("q".into())),
QualType::int(),
);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
assert_eq!(bugs[0].severity, BugSeverity::Warning);
}
#[test]
fn test_null_deref_arrow_access() {
let checker = X86NullDerefChecker::new();
let mut state = X86ProgramState::new();
state.bind_value("p", X86SVal::Null);
let mut reporter = X86BugReporter::new();
let expr = Expr::Member(Box::new(Expr::Ident("p".into())), "x".into(), true);
let bugs = checker.check_expr(&expr, &state, &mut reporter);
assert_eq!(bugs.len(), 1);
}
#[test]
fn test_return_defined_value_no_bug() {
let mut state = X86ProgramState::new();
state.bind_value("x", X86SVal::ConcreteInt(42, None, None));
let mut reporter = X86BugReporter::new();
let checker = X86ReturnUndefinedChecker::new();
let stmt = Stmt::Return(Some(Expr::Ident("x".into())));
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
assert_eq!(bugs.len(), 0);
}
#[test]
fn test_program_state_merge_locks_intersection() {
let mut s1 = X86ProgramState::new();
s1.acquire_lock("mtx1");
s1.acquire_lock("mtx2");
let mut s2 = X86ProgramState::new();
s2.acquire_lock("mtx1");
let merged = s1.merge(&s2);
assert!(merged.lock_held("mtx1"));
assert!(!merged.lock_held("mtx2"));
}
#[test]
fn test_program_state_merge_tainted_union() {
let mut s1 = X86ProgramState::new();
s1.taint("a");
let mut s2 = X86ProgramState::new();
s2.taint("b");
let merged = s1.merge(&s2);
assert!(merged.is_tainted("a"));
assert!(merged.is_tainted("b"));
}
#[test]
fn test_exploded_graph_node_merge() {
let mut graph = X86ExplodedGraph::new();
let root = graph.create_root(X86ProgramState::new());
let s1 = X86ProgramState::new();
let n1 = graph.add_node(
root,
"same_label".into(),
s1.clone(),
X86EdgeKind::Fallthrough,
"",
);
let n2 = graph.add_node(root, "same_label".into(), s1, X86EdgeKind::Fallthrough, "");
assert_eq!(n1, n2);
}
#[test]
fn test_disabled_checker_not_run() {
let mut analyzer = X86StaticAnalyzerEngine::with_defaults();
analyzer.disable_checker("core.NullDereference");
let names = analyzer.enabled_checker_names();
assert!(!names.contains(&"core.NullDereference".to_string()));
}
#[test]
fn test_dead_state_produces_bug() {
let mut state = X86ProgramState::new();
state.mark_dead();
let mut reporter = X86BugReporter::new();
let checker = X86UnreachableCodeChecker::new();
let stmt = Stmt::Expr(Expr::Ident("x".into()));
let (_, bugs) = checker.check_stmt(
&stmt,
&mut X86ExplodedGraph::new(),
1,
&state,
&mut reporter,
);
let _ = bugs;
}
#[test]
fn test_sval_display_all_variants() {
let vals = vec![
X86SVal::Undefined,
X86SVal::Unknown,
X86SVal::Zero,
X86SVal::Null,
X86SVal::ConcreteInt(1, None, None),
X86SVal::ConcreteUInt(1),
X86SVal::SymbolInt("x".into()),
X86SVal::Loc(1),
X86SVal::SymbolRegion(1),
X86SVal::LazyCompound(1),
X86SVal::SymbolExpr("f".into(), vec![X86SVal::ConcreteInt(1, None, None)]),
];
for val in &vals {
let s = val.to_string();
assert!(
!s.is_empty(),
"Display impl for {val:?} produced empty string"
);
}
}
#[test]
fn test_mem_region_display() {
let region = X86MemRegion::new_stack(1, "var", Some(QualType::int()));
let s = region.to_string();
assert!(s.contains("Stack"));
assert!(s.contains("var"));
}
#[test]
fn test_path_constraint_negate_range() {
let pc = X86PathConstraint::Range("x".into(), 0, 10);
let neg = pc.negate();
assert_eq!(neg, X86PathConstraint::NonNull("x".into()));
}
#[test]
fn test_path_constraint_negate_non_null() {
let pc = X86PathConstraint::NonNull("x".into());
assert_eq!(pc.negate(), X86PathConstraint::MustBeNull("x".into()));
}
#[test]
fn test_path_constraint_negate_tainted() {
let pc = X86PathConstraint::IsTainted("x".into());
assert_eq!(pc.negate(), X86PathConstraint::NotTainted("x".into()));
}
#[test]
fn test_symbol_constraint_intersect_contradictory_null() {
let mut sc = X86SymbolConstraint::new();
sc.set_nullability(X86Nullability::Null);
let other = X86SymbolConstraint {
nullability: X86Nullability::NonNull,
..Default::default()
};
sc.intersect(&other);
assert!(!sc.is_satisfiable());
}
#[test]
fn test_symbol_constraint_union_null_resets() {
let mut sc = X86SymbolConstraint::new();
sc.set_nullability(X86Nullability::Null);
let other = X86SymbolConstraint {
nullability: X86Nullability::NonNull,
..Default::default()
};
sc.union(&other);
assert_eq!(sc.nullability, X86Nullability::Unknown);
}
#[test]
fn test_constraint_set_is_satisfiable_empty() {
let cs = X86ConstraintSet::new();
assert!(cs.is_satisfiable());
}
#[test]
fn test_constraint_set_merge_union_ranges() {
let mut cs1 = X86ConstraintSet::new();
cs1.set_range("x", 0, 5);
let mut cs2 = X86ConstraintSet::new();
cs2.set_range("x", 4, 10);
cs1.merge(&cs2, true);
let r = cs1.get_range("x").unwrap();
assert_eq!(r.low, 0);
assert_eq!(r.high, 10);
}
}