use super::super::{CertRule, RuleViolation};
use crate::analyze::cfg::{self as cfg_mod, FunctionCfg};
use crate::analyze::context::ProjectContext;
use crate::analyze::function_summary::FunctionSummary;
use crate::analyze::init_state::{self, InitAnalysisResult, InitState, InitStateMap};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use tree_sitter::Node;
pub struct Exp33C {
function_cfgs: RefCell<HashMap<usize, FunctionCfg>>,
file_scope_statics: RefCell<InitStateMap>,
realloc_wrapper_fns: RefCell<HashSet<String>>,
conditionally_init_fns: RefCell<HashMap<String, HashSet<usize>>>,
cross_file_summaries: RefCell<HashMap<String, FunctionSummary>>,
file_scope_constants: RefCell<HashMap<String, i64>>,
}
impl Exp33C {
pub fn new() -> Self {
Self {
function_cfgs: RefCell::new(HashMap::new()),
file_scope_statics: RefCell::new(InitStateMap::new()),
realloc_wrapper_fns: RefCell::new(HashSet::new()),
conditionally_init_fns: RefCell::new(HashMap::new()),
cross_file_summaries: RefCell::new(HashMap::new()),
file_scope_constants: RefCell::new(HashMap::new()),
}
}
fn build_read_only_deref_fns(&self) -> HashMap<String, HashSet<usize>> {
let summaries = self.cross_file_summaries.borrow();
let mut result = HashMap::new();
for (name, summary) in summaries.iter() {
let read_only: HashSet<usize> = summary
.dereferences_params
.difference(&summary.modifies_params)
.copied()
.collect();
if !read_only.is_empty() {
result.insert(name.clone(), read_only);
}
}
result
}
}
impl CertRule for Exp33C {
fn rule_id(&self) -> &'static str {
"EXP33-C"
}
fn description(&self) -> &'static str {
"Do not read uninitialized memory"
}
fn severity(&self) -> Severity {
Severity::High
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn cert_id(&self) -> &'static str {
"EXP33-C"
}
fn set_project_context(&self, context: &ProjectContext) {
*self.cross_file_summaries.borrow_mut() = context.function_summaries.clone();
let mut constants = self.file_scope_constants.borrow_mut();
for (k, v) in &context.global_constants {
constants.entry(k.clone()).or_insert(*v);
}
for (k, v) in &context.macro_constants {
constants.entry(k.clone()).or_insert(*v);
}
}
fn set_function_cfgs(&self, cfgs: &HashMap<usize, FunctionCfg>) {
*self.function_cfgs.borrow_mut() = cfgs.clone();
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let cfgs = self.function_cfgs.borrow();
if node.kind() == "translation_unit" {
let statics = init_state::collect_file_scope_statics(node, source);
*self.file_scope_statics.borrow_mut() = statics;
let file_constants = init_state::collect_file_scope_constants(node, source);
let fn_constants = init_state::collect_constant_functions(node, source);
{
let mut constants = self.file_scope_constants.borrow_mut();
constants.extend(file_constants);
constants.extend(fn_constants);
}
let mut wrappers = HashSet::new();
scan_realloc_wrappers(node, source, &mut wrappers);
*self.realloc_wrapper_fns.borrow_mut() = wrappers;
let mut cond_init = HashMap::new();
scan_conditionally_init_functions(node, source, &mut cond_init);
*self.conditionally_init_fns.borrow_mut() = cond_init;
}
if node.kind() == "function_definition" {
if let Some(body) = node.child_by_field_name("body") {
let inline_cfg;
let cfg = if let Some(c) = cfgs.get(&node.start_byte()) {
c
} else if let Some(c) = cfg_mod::build_function_cfg(node, source) {
inline_cfg = c;
&inline_cfg
} else {
return violations;
};
let statics = self.file_scope_statics.borrow();
let cond_fns = self.conditionally_init_fns.borrow();
let realloc_fns = self.realloc_wrapper_fns.borrow();
let read_only_fns = self.build_read_only_deref_fns();
let file_constants = self.file_scope_constants.borrow();
let config = init_state::InitAnalysisConfig {
conditionally_init_fns: cond_fns.clone(),
realloc_wrapper_fns: realloc_fns.clone(),
read_only_deref_fns: read_only_fns.clone(),
file_scope_constants: file_constants.clone(),
};
let analysis = init_state::analyze_init_states_with_statics(
cfg, node, source, &statics, &config,
);
let mut reported: HashSet<String> = HashSet::new();
check_reads(
&body,
source,
&analysis,
cfg,
&body,
&mut violations,
&mut reported,
&config,
);
if !read_only_fns.is_empty() {
check_cross_file_uninit_calls(
&body,
source,
&analysis,
cfg,
&body,
&read_only_fns,
&mut violations,
&mut reported,
);
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
violations.extend(self.check(&child, source));
}
}
violations
}
}
fn check_reads(
node: &Node,
source: &str,
analysis: &InitAnalysisResult,
cfg: &FunctionCfg,
body: &Node,
violations: &mut Vec<RuleViolation>,
reported: &mut HashSet<String>,
config: &init_state::InitAnalysisConfig,
) {
match node.kind() {
"identifier" => {
check_identifier_read(
node, source, analysis, cfg, body, violations, reported, config,
);
}
"pointer_expression" => {
let text = get_node_text(node, source);
if text.starts_with('*') {
check_deref_read(
node, source, analysis, cfg, body, violations, reported, config,
);
}
}
"subscript_expression" => {
check_subscript_read(
node, source, analysis, cfg, body, violations, reported, config,
);
}
_ => {}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
check_reads(
&child, source, analysis, cfg, body, violations, reported, config,
);
}
}
}
fn check_cross_file_uninit_calls(
node: &Node,
source: &str,
analysis: &InitAnalysisResult,
cfg: &FunctionCfg,
body: &Node,
read_only_fns: &HashMap<String, HashSet<usize>>,
violations: &mut Vec<RuleViolation>,
reported: &mut HashSet<String>,
) {
if node.kind() == "call_expression" {
if let Some(func) = node.child_by_field_name("function") {
let func_name = get_node_text(&func, source).to_string();
if let Some(read_only_params) = read_only_fns.get(&func_name) {
if let Some(args) = node.child_by_field_name("arguments") {
let mut arg_idx: usize = 0;
for i in 0..args.child_count() {
if let Some(arg) = args.child(i) {
if arg.kind() == "," || arg.kind() == "(" || arg.kind() == ")" {
continue;
}
if read_only_params.contains(&arg_idx) {
let var_name = extract_addr_of_var(&arg, source);
if !var_name.is_empty()
&& analysis.tracked_vars.contains(&var_name)
&& !reported.contains(&var_name)
{
if let Some(info) = init_state::get_var_info_at_with_config(
analysis,
cfg,
body,
source,
&var_name,
node.start_byte(),
&init_state::InitAnalysisConfig::default(),
) {
if info.state.is_unsafe() && !info.is_unsigned_char {
reported.insert(var_name.clone());
violations.push(RuleViolation {
rule_id: "EXP33-C".to_string(),
severity: Severity::High,
message: format!(
"Passing pointer to uninitialized variable '{}' to '{}' which reads the value",
var_name, func_name
),
file_path: String::new(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
suggestion: Some(format!(
"Initialize '{}' before passing its address to '{}'",
var_name, func_name
)),
..Default::default()
});
}
}
}
}
arg_idx += 1;
}
}
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
check_cross_file_uninit_calls(
&child,
source,
analysis,
cfg,
body,
read_only_fns,
violations,
reported,
);
}
}
}
fn extract_addr_of_var(node: &Node, source: &str) -> String {
if node.kind() == "pointer_expression" {
let text = get_node_text(node, source);
if text.starts_with('&') {
if let Some(arg) = node.child_by_field_name("argument") {
if arg.kind() == "identifier" {
return get_node_text(&arg, source).to_string();
}
}
}
}
if node.kind() == "parenthesized_expression" {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
let result = extract_addr_of_var(&child, source);
if !result.is_empty() {
return result;
}
}
}
}
String::new()
}
fn check_identifier_read(
node: &Node,
source: &str,
analysis: &InitAnalysisResult,
cfg: &FunctionCfg,
body: &Node,
violations: &mut Vec<RuleViolation>,
reported: &mut HashSet<String>,
config: &init_state::InitAnalysisConfig,
) {
let var_name = get_node_text(node, source).to_string();
if !analysis.tracked_vars.contains(&var_name) {
return;
}
if reported.contains(&var_name) {
return;
}
if !is_read_context(node, source) {
return;
}
let info = match init_state::get_var_info_at_with_config(
analysis,
cfg,
body,
source,
&var_name,
node.start_byte(),
config,
) {
Some(i) => i,
None => return,
};
if info.is_unsigned_char {
return;
}
if !info.state.is_unsafe() {
return;
}
if info.is_array {
if let Some(parent) = node.parent() {
if parent.kind() == "assignment_expression" && !info.is_char_type {
if let Some(right) = parent.child_by_field_name("right") {
if right.id() == node.id() {
return;
}
}
}
if parent.kind() == "argument_list" {
if let Some(call_expr) = parent.parent() {
if call_expr.kind() == "call_expression" {
if let Some(func) = call_expr.child_by_field_name("function") {
let fname = get_node_text(&func, source).to_string();
if init_state::match_initializing_function(&fname).is_none()
&& !init_state::is_non_initializing_function(&fname)
{
return;
}
}
}
}
}
}
}
reported.insert(var_name.clone());
let message = if info.is_static {
format!(
"Static variable '{}' used without explicit initialization",
var_name
)
} else if matches!(info.state, InitState::MaybeUninitialized) {
format!(
"Variable '{}' may be used uninitialized (not assigned on all paths)",
var_name
)
} else {
format!("Variable '{}' is used uninitialized", var_name)
};
violations.push(RuleViolation {
rule_id: "EXP33-C".to_string(),
severity: Severity::High,
message,
file_path: String::new(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
suggestion: Some(format!(
"Initialize '{}' before use, e.g., at its declaration",
var_name
)),
..Default::default()
});
}
fn check_deref_read(
node: &Node,
source: &str,
analysis: &InitAnalysisResult,
cfg: &FunctionCfg,
body: &Node,
violations: &mut Vec<RuleViolation>,
reported: &mut HashSet<String>,
config: &init_state::InitAnalysisConfig,
) {
let var_name = if let Some(arg) = node.child_by_field_name("argument") {
if arg.kind() == "identifier" {
get_node_text(&arg, source).to_string()
} else {
return;
}
} else {
return;
};
if !analysis.tracked_vars.contains(&var_name) || reported.contains(&var_name) {
return;
}
if !is_deref_read_context(node) {
return;
}
let info = match init_state::get_var_info_at_with_config(
analysis,
cfg,
body,
source,
&var_name,
node.start_byte(),
config,
) {
Some(i) => i,
None => return,
};
if info.state.is_unsafe() {
reported.insert(var_name.clone());
violations.push(RuleViolation {
rule_id: "EXP33-C".to_string(),
severity: Severity::High,
message: format!("Dereference of uninitialized pointer '{}'", var_name),
file_path: String::new(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
suggestion: Some(format!(
"Initialize pointer '{}' before dereferencing",
var_name
)),
..Default::default()
});
} else if info.state.is_content_unsafe() {
reported.insert(var_name.clone());
violations.push(RuleViolation {
rule_id: "EXP33-C".to_string(),
severity: Severity::High,
message: format!(
"Reading from '{}' which points to uninitialized memory (allocated without initialization)",
var_name
),
file_path: String::new(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
suggestion: Some(
"Use calloc() instead of malloc(), or memset() after allocation".to_string(),
),
..Default::default()
});
}
}
fn check_subscript_read(
node: &Node,
source: &str,
analysis: &InitAnalysisResult,
cfg: &FunctionCfg,
body: &Node,
violations: &mut Vec<RuleViolation>,
reported: &mut HashSet<String>,
config: &init_state::InitAnalysisConfig,
) {
let var_name = {
let mut name = String::new();
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
name = get_node_text(&child, source).to_string();
break;
}
if child.kind() == "field_expression" {
name = extract_root_identifier(&child, source);
break;
}
}
}
name
};
if var_name.is_empty()
|| !analysis.tracked_vars.contains(&var_name)
|| reported.contains(&var_name)
{
return;
}
if !is_subscript_read_context(node) {
return;
}
let info = match init_state::get_var_info_at_with_config(
analysis,
cfg,
body,
source,
&var_name,
node.start_byte(),
config,
) {
Some(i) => i,
None => return,
};
if info.is_unsigned_char {
return;
}
let content_unsafe = matches!(
info.state,
InitState::Uninitialized | InitState::MallocUninitialized
);
if content_unsafe {
reported.insert(var_name.clone());
violations.push(RuleViolation {
rule_id: "EXP33-C".to_string(),
severity: Severity::High,
message: format!(
"Reading from '{}' which may contain uninitialized data",
var_name
),
file_path: String::new(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
suggestion: Some(format!(
"Initialize the contents of '{}' before reading",
var_name
)),
..Default::default()
});
}
}
fn is_read_context(node: &Node, source: &str) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true,
};
let mut ancestor = Some(parent);
let mut depth = 0;
while let Some(anc) = ancestor {
depth += 1;
if depth > 5 {
break;
}
match anc.kind() {
"sizeof_expression" | "_Alignof" => return false,
"parenthesized_expression" => {
ancestor = anc.parent();
continue;
}
_ => break,
}
}
match parent.kind() {
"assignment_expression" => {
if let Some(left) = parent.child_by_field_name("left") {
if left.id() == node.id() {
for i in 0..parent.child_count() {
if let Some(op) = parent.child(i) {
let op_text = get_node_text(&op, source);
if matches!(
op_text,
"+=" | "-="
| "*="
| "/="
| "%="
| "<<="
| ">>="
| "&="
| "|="
| "^="
) {
return true; }
}
}
return false; }
}
true }
"augmented_assignment_expression" => true,
"declaration" | "init_declarator" => false,
"sizeof_expression" => false,
"pointer_expression" => {
let text = get_node_text(&parent, source);
if !text.starts_with('&') {
return true; }
if let Some(arg_list) = parent.parent() {
if arg_list.kind() == "argument_list" {
if let Some(call) = arg_list.parent() {
if call.kind() == "call_expression" {
if let Some(func) = call.child_by_field_name("function") {
let func_name = get_node_text(&func, source);
if init_state::is_non_initializing_function(&func_name) {
return true; }
}
}
}
}
}
false }
"update_expression" => false,
"field_expression" => {
if let Some(grandparent) = parent.parent() {
if grandparent.kind() == "assignment_expression" {
if let Some(left) = grandparent.child_by_field_name("left") {
if left.id() == parent.id() {
return false; }
}
}
}
true
}
"subscript_expression" => false,
"parameter_declaration" => false,
"cast_expression" => true,
"binary_expression" | "unary_expression" | "conditional_expression" => true,
"argument_list" => {
if let Some(call_gp) = parent.parent() {
if call_gp.kind() == "call_expression" {
if let Some(func) = call_gp.child_by_field_name("function") {
if func.kind() == "string_literal" {
let constraint = get_node_text(&func, source);
if constraint.contains('=') {
return false; }
}
}
}
}
is_read_in_argument_list(node, &parent, source)
}
"return_statement" => true,
"comma_expression" => true,
"gnu_asm_output_operand" => false,
_ => true,
}
}
fn is_read_in_argument_list(node: &Node, arg_list: &Node, source: &str) -> bool {
let call_expr = match arg_list.parent() {
Some(c) if c.kind() == "call_expression" => c,
_ => return true,
};
let func_name = match call_expr.child_by_field_name("function") {
Some(f) => get_node_text(&f, source).to_string(),
None => return true,
};
if func_name == "va_start" || func_name == "va_copy" {
if let Some(first_arg) = call_expr.child_by_field_name("arguments").and_then(|args| {
for i in 0..args.child_count() {
if let Some(c) = args.child(i) {
if c.kind() != "(" && c.kind() != ")" && c.kind() != "," {
return Some(c);
}
}
}
None
}) {
if contains_node(&first_arg, node) {
return false; }
}
return true;
}
let base_name = match init_state::match_initializing_function(&func_name) {
Some(name) => name,
None => {
return true;
}
};
let output_indices = init_state::get_output_arg_indices(base_name);
if output_indices.is_empty() {
return true; }
let mut arg_idx = 0;
for i in 0..arg_list.child_count() {
if let Some(child) = arg_list.child(i) {
if child.kind() == "," || child.kind() == "(" || child.kind() == ")" {
continue;
}
if contains_node(&child, node) {
return !output_indices.contains(&arg_idx);
}
arg_idx += 1;
}
}
true }
fn contains_node(haystack: &Node, needle: &Node) -> bool {
if haystack.id() == needle.id() {
return true;
}
for i in 0..haystack.child_count() {
if let Some(child) = haystack.child(i) {
if contains_node(&child, needle) {
return true;
}
}
}
false
}
fn is_deref_read_context(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true,
};
match parent.kind() {
"assignment_expression" => {
if let Some(left) = parent.child_by_field_name("left") {
left.id() != node.id()
} else {
true
}
}
_ => true,
}
}
fn is_subscript_read_context(node: &Node) -> bool {
let mut current = *node;
for _ in 0..5 {
let parent = match current.parent() {
Some(p) => p,
None => return true,
};
match parent.kind() {
"assignment_expression" => {
if let Some(left) = parent.child_by_field_name("left") {
return left.id() != current.id();
}
return true;
}
"field_expression" => {
current = parent;
continue;
}
_ => return true,
}
}
true
}
fn scan_realloc_wrappers(node: &Node, source: &str, wrappers: &mut HashSet<String>) {
if node.kind() == "function_definition" {
if let Some(body) = node.child_by_field_name("body") {
let body_text = get_node_text(&body, source);
if body_text.contains("realloc(") && !body_text.contains("memset") {
if let Some(declarator) = node.child_by_field_name("declarator") {
let name = get_func_name(&declarator, source);
if !name.is_empty() {
wrappers.insert(name);
}
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
scan_realloc_wrappers(&child, source, wrappers);
}
}
}
fn scan_conditionally_init_functions(
node: &Node,
source: &str,
result: &mut HashMap<String, HashSet<usize>>,
) {
if node.kind() == "function_definition" {
let cond_indices = get_conditional_init_param_indices(node, source);
if !cond_indices.is_empty() {
if let Some(declarator) = node.child_by_field_name("declarator") {
let name = get_func_name(&declarator, source);
if !name.is_empty() {
result.insert(name, cond_indices);
}
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
scan_conditionally_init_functions(&child, source, result);
}
}
}
fn get_conditional_init_param_indices(func_node: &Node, source: &str) -> HashSet<usize> {
let mut result = HashSet::new();
let body = match func_node.child_by_field_name("body") {
Some(b) => b,
None => return result,
};
let mut all_params: Vec<(usize, String, bool)> = Vec::new(); collect_param_list_with_indices(func_node, source, &mut all_params);
let body_text = get_node_text(&body, source);
for (idx, param_name, is_pointer) in &all_params {
if !is_pointer {
continue;
}
let deref_write = format!("*{}", param_name);
if !body_text.contains(&deref_write) {
continue; }
let mut has_unconditional_write = false;
for i in 0..body.child_count() {
if let Some(child) = body.child(i) {
if child.kind() == "expression_statement" {
let stmt_text = get_node_text(&child, source);
if stmt_text.contains(&deref_write) && stmt_text.contains('=') {
has_unconditional_write = true;
break;
}
}
}
}
if !has_unconditional_write {
result.insert(*idx);
}
}
result
}
fn collect_param_list_with_indices(
func_node: &Node,
source: &str,
params: &mut Vec<(usize, String, bool)>,
) {
let declarator = match func_node.child_by_field_name("declarator") {
Some(d) => d,
None => return,
};
let func_decl = match find_function_declarator_node(&declarator) {
Some(d) => d,
None => return,
};
for i in 0..func_decl.child_count() {
if let Some(child) = func_decl.child(i) {
if child.kind() == "parameter_list" {
let mut param_idx = 0;
for j in 0..child.child_count() {
if let Some(param) = child.child(j) {
if param.kind() == "parameter_declaration" {
let param_text = get_node_text(¶m, source);
let is_pointer = param_text.contains('*');
let name = get_declarator_name_from(¶m, source);
if !name.is_empty() {
params.push((param_idx, name, is_pointer));
}
param_idx += 1;
}
}
}
}
}
}
}
fn find_function_declarator_node<'a>(node: &Node<'a>) -> Option<Node<'a>> {
if node.kind() == "function_declarator" {
return Some(*node);
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if let Some(found) = find_function_declarator_node(&child) {
return Some(found);
}
}
}
None
}
fn get_declarator_name_from(node: &Node, source: &str) -> String {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
return get_node_text(&child, source).to_string();
}
if child.kind() == "pointer_declarator" {
return get_declarator_name_from(&child, source);
}
}
}
String::new()
}
fn extract_root_identifier(node: &Node, source: &str) -> String {
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
if child.kind() == "identifier" {
return get_node_text(&child, source).to_string();
}
if child.kind() == "field_expression" || child.kind() == "subscript_expression" {
return extract_root_identifier(&child, source);
}
}
}
String::new()
}
fn get_func_name(declarator: &Node, source: &str) -> String {
match declarator.kind() {
"identifier" => get_node_text(declarator, source).to_string(),
"function_declarator" | "pointer_declarator" => {
if let Some(inner) = declarator.child_by_field_name("declarator") {
get_func_name(&inner, source)
} else {
for i in 0..declarator.child_count() {
if let Some(child) = declarator.child(i) {
if child.kind() == "identifier" {
return get_node_text(&child, source).to_string();
}
}
}
String::new()
}
}
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::CParser;
fn check_code(code: &str) -> Vec<RuleViolation> {
let mut parser = CParser::new().expect("parser");
let tree = parser.parse_source(code).expect("parse");
let rule = Exp33C::new();
rule.check(&tree.root_node(), code)
}
#[test]
fn test_initialized_at_decl() {
let violations = check_code("int f() { int result = 0; return result; }");
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C")
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"result=0 should be initialized, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_uninitialized_read() {
let violations = check_code("int f() { int x; return x; }");
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C")
.collect::<Vec<_>>();
assert!(!exp33.is_empty(), "x should be flagged as uninitialized");
}
#[test]
fn test_conditional_init_both_branches() {
let violations = check_code(
r#"
int f(int c) {
int x;
if (c) { x = 1; } else { x = 2; }
return x;
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C")
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"x init'd in both branches, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_conditional_init_one_branch() {
let violations = check_code(
r#"
int f(int c) {
int x;
if (c) { x = 1; }
return x;
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C")
.collect::<Vec<_>>();
assert!(!exp33.is_empty(), "x only init'd in one branch");
}
#[test]
fn test_memset_initializes() {
let violations = check_code(
r#"
typedef int mbstate_t;
void f() {
mbstate_t state;
memset(&state, 0, sizeof(state));
use(state);
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("state"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"memset should initialize state, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_va_start_initializes() {
let violations = check_code(
r#"
typedef int va_list;
void f(int count, ...) {
va_list args;
va_start(args, count);
use(args);
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("args"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"va_start should initialize args, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_subscript_field_write_not_read() {
let violations = check_code(
r#"
typedef struct { int a; int b; } Pair;
void f() {
Pair *arr = (Pair *)malloc(4 * sizeof(Pair));
if (arr == 0) return;
arr[0].a = 0;
arr[0].b = 0;
free(arr);
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("arr"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"arr[0].a=0 should not flag arr, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_array_loop_init() {
let violations = check_code(
r#"
void f(int size) {
int vla[10];
for (int i = 0; i < 10; i++) {
vla[i] = i;
}
use(vla[0]);
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("vla"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"loop init should mark vla as initialized, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_malloc_loop_init() {
let violations = check_code(
r#"
void f() {
int *array = malloc(10 * sizeof(int));
if (array == 0) return;
for (int i = 0; i < 10; i++) {
array[i] = i + 1;
}
use(array[0]);
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("array"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"loop init after malloc should be safe, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_thread_local_uninit() {
let violations = check_code(
r#"
static int counter;
void f(void) {
counter += 10;
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("counter"))
.collect::<Vec<_>>();
assert!(
!exp33.is_empty(),
"static without init should be flagged, got nothing"
);
}
#[test]
fn test_mbstate_non_init_read() {
let violations = check_code(
r#"
typedef int mbstate_t;
void f(const char *mbs) {
mbstate_t state;
mbrlen(mbs, 5, &state);
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("state"))
.collect::<Vec<_>>();
assert!(
!exp33.is_empty(),
"mbrlen reads &state without prior init — should flag"
);
}
#[test]
fn test_return_by_reference_partial() {
let violations = check_code(
r#"
void set_flag(int number, int *sign_flag) {
if (sign_flag == 0) return;
if (number > 0) *sign_flag = 1;
else if (number < 0) *sign_flag = -1;
}
int f(int number) {
int sign;
set_flag(number, &sign);
return sign < 0;
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("sign"))
.collect::<Vec<_>>();
assert!(
!exp33.is_empty(),
"set_flag doesn't init sign for number==0 — should flag"
);
}
#[test]
fn test_cross_file_read_only_deref() {
let code = r#"
void f() {
int data;
badSink(&data);
}
"#;
let mut parser = CParser::new().expect("parser");
let tree = parser.parse_source(code).expect("parse");
let rule = Exp33C::new();
let mut summary = FunctionSummary::default();
summary.dereferences_params.insert(0);
let mut summaries = HashMap::new();
summaries.insert("badSink".to_string(), summary);
*rule.cross_file_summaries.borrow_mut() = summaries;
let violations = rule.check(&tree.root_node(), code);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("data"))
.collect::<Vec<_>>();
assert!(
!exp33.is_empty(),
"Passing &uninit_var to read-only-deref function should flag"
);
}
#[test]
fn test_cross_file_modifying_function_ok() {
let code = r#"
void f() {
int data;
initSink(&data);
use(data);
}
"#;
let mut parser = CParser::new().expect("parser");
let tree = parser.parse_source(code).expect("parse");
let rule = Exp33C::new();
let mut summary = FunctionSummary::default();
summary.dereferences_params.insert(0);
summary.modifies_params.insert(0);
let mut summaries = HashMap::new();
summaries.insert("initSink".to_string(), summary);
*rule.cross_file_summaries.borrow_mut() = summaries;
let violations = rule.check(&tree.root_node(), code);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("data"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"initSink modifies param — data should be initialized, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_fgets_initializes_array() {
let violations = check_code(
r#"
void f() {
char input[100];
fgets(input, 100, 0);
use(input);
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("input"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"fgets should initialize input, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_struct_field_write() {
let violations = check_code(
r#"
typedef struct { int a; int b; } Pair;
void f() {
Pair p;
p.a = 1;
p.b = 2;
use(p.a);
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("'p'"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"p.a=1 should initialize p, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
#[test]
fn test_switch_with_initialized_default() {
let violations = check_code(
r#"
int f(int op) {
int result = 0;
switch (op) {
case 1: result = 10; break;
default: result = -1; break;
}
return result;
}
"#,
);
let exp33 = violations
.iter()
.filter(|v| v.rule_id == "EXP33-C" && v.message.contains("result"))
.collect::<Vec<_>>();
assert!(
exp33.is_empty(),
"result=0 should be initialized, got: {:?}",
exp33.iter().map(|v| &v.message).collect::<Vec<_>>()
);
}
}