use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use tree_sitter::Node;
pub struct Mem04C;
impl CertRule for Mem04C {
fn rule_id(&self) -> &'static str {
"MEM04-C"
}
fn description(&self) -> &'static str {
"Beware of zero-length allocations"
}
fn severity(&self) -> Severity {
Severity::Low
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"MEM04-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
self.traverse(node, source, &mut violations);
violations
}
}
impl Mem04C {
fn traverse(&self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
if node.kind() == "call_expression" {
if let Some(function) = node.child_by_field_name("function") {
let func_name = get_node_text(&function, source);
if func_name == "malloc" {
self.check_malloc_call(node, source, violations);
} else if func_name == "calloc" {
self.check_calloc_call(node, source, violations);
} else if func_name == "realloc" {
self.check_realloc_call(node, source, violations);
}
}
}
for i in 0..node.child_count() {
if let Some(child) = node.child(i) {
self.traverse(&child, source, violations);
}
}
}
fn check_malloc_call(&self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
if let Some(arguments) = node.child_by_field_name("arguments") {
let args = self.extract_arguments(&arguments);
if args.len() == 1 {
let size_arg = &args[0];
let size_text = get_node_text(size_arg, source);
if self.is_potentially_zero(&size_text) {
if !self.has_preceding_zero_check(node, &size_text, source) {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
message: format!(
"malloc() called with potentially zero size argument: '{}' without prior zero check. \
Zero-length allocations have implementation-defined behavior.",
size_text
),
severity: self.severity(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
file_path: String::new(),
suggestion: Some(
"Check that size argument is non-zero before calling malloc()".to_string(),
),
requires_manual_review: None,
});
}
}
}
}
}
fn check_calloc_call(&self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
if let Some(arguments) = node.child_by_field_name("arguments") {
let args = self.extract_arguments(&arguments);
if args.len() == 2 {
let num_arg = &args[0];
let size_arg = &args[1];
let num_text = get_node_text(num_arg, source);
let size_text = get_node_text(size_arg, source);
let num_needs_check = self.is_potentially_zero(&num_text)
&& !self.has_preceding_zero_check(node, &num_text, source);
let size_needs_check = self.is_potentially_zero(&size_text)
&& !self.has_preceding_zero_check(node, &size_text, source);
if num_needs_check || size_needs_check {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
message: format!(
"calloc() called with potentially zero argument(s) without prior check: num='{}', size='{}'. \
Zero-length allocations have implementation-defined behavior.",
num_text, size_text
),
severity: self.severity(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
file_path: String::new(),
suggestion: Some(
"Check that both arguments are non-zero before calling calloc()".to_string(),
),
requires_manual_review: None,
});
}
}
}
}
fn check_realloc_call(&self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
if let Some(arguments) = node.child_by_field_name("arguments") {
let args = self.extract_arguments(&arguments);
if args.len() == 2 {
let size_arg = &args[1]; let size_text = get_node_text(size_arg, source);
if self.is_potentially_zero(&size_text) {
if !self.has_preceding_zero_check(node, &size_text, source) {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
message: format!(
"realloc() called with potentially zero size argument: '{}' without prior check. \
This is implementation-defined and may cause memory leaks.",
size_text
),
severity: self.severity(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
file_path: String::new(),
suggestion: Some(
"Check that size argument is non-zero before calling realloc()".to_string(),
),
requires_manual_review: None,
});
}
}
}
}
}
fn extract_arguments<'a>(&self, arguments: &Node<'a>) -> Vec<Node<'a>> {
let mut args = Vec::new();
for i in 0..arguments.child_count() {
if let Some(child) = arguments.child(i) {
if child.kind() != "(" && child.kind() != ")" && child.kind() != "," {
args.push(child);
}
}
}
args
}
fn is_potentially_zero(&self, expr: &str) -> bool {
let trimmed = expr.trim();
if trimmed == "0" {
return true;
}
if trimmed == "NULL" {
return true;
}
if let Ok(value) = trimmed.parse::<i64>() {
return value == 0;
}
if trimmed.contains("sizeof") {
return false;
}
true
}
fn has_preceding_zero_check(&self, node: &Node, var_name: &str, source: &str) -> bool {
let alloc_line = node.start_position().row;
let lines: Vec<&str> = source.lines().collect();
if alloc_line >= lines.len() {
return false;
}
let start_line = alloc_line.saturating_sub(50);
for line in lines.iter().take(alloc_line).skip(start_line) {
if line.trim_start().starts_with("if") && line.contains(var_name) {
if line.contains("== 0")
|| line.contains("!= 0")
|| line.contains(&format!("!{}", var_name))
|| (line.contains(&format!("if ({})", var_name))
|| line.contains(&format!("if({})", var_name)))
{
return true;
}
}
}
false
}
}