use super::super::{CertRule, RuleViolation};
use crate::analyze::cfg::FunctionCfg;
use crate::analyze::const_eval::{self, MacroConstantMap, VarRangeMap};
use crate::analyze::context::ProjectContext;
use crate::analyze::value_range::RangeAnalysisResult;
use crate::analyze::vra_access;
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::{get_node_text, misparsed_cast_type_name};
use crate::utility::cert_c::overflow_helpers;
use lang_parsing_substrate::query;
use std::cell::RefCell;
use std::collections::HashMap;
use tree_sitter::Node;
pub struct Int10C {
typedef_types: RefCell<HashMap<String, String>>,
project_macros: RefCell<MacroConstantMap>,
current_macros: RefCell<MacroConstantMap>,
function_cfgs: RefCell<HashMap<usize, FunctionCfg>>,
vra_results: RefCell<HashMap<usize, RangeAnalysisResult>>,
}
impl Int10C {
pub fn new() -> Self {
Self {
typedef_types: RefCell::new(HashMap::new()),
project_macros: RefCell::new(MacroConstantMap::new()),
current_macros: RefCell::new(MacroConstantMap::new()),
function_cfgs: RefCell::new(HashMap::new()),
vra_results: RefCell::new(HashMap::new()),
}
}
}
impl CertRule for Int10C {
fn rule_id(&self) -> &'static str {
"INT10-C"
}
fn description(&self) -> &'static str {
"Do not assume a positive remainder when using the % operator"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn cert_id(&self) -> &'static str {
"INT10-C"
}
fn set_project_context(&self, context: &ProjectContext) {
*self.typedef_types.borrow_mut() = context.typedef_types.clone();
*self.project_macros.borrow_mut() = context.macro_constants.clone();
}
fn set_function_cfgs(&self, cfgs: &HashMap<usize, FunctionCfg>) {
*self.function_cfgs.borrow_mut() = cfgs.clone();
}
fn set_vra_results(&self, results: &HashMap<usize, RangeAnalysisResult>) {
*self.vra_results.borrow_mut() = results.clone();
}
fn needs_vra(&self) -> bool {
true
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let type_map = overflow_helpers::collect_variable_types(node, source);
*self.current_macros.borrow_mut() =
const_eval::merged_macro_constants(&self.project_macros.borrow(), node, source);
let macros = self.current_macros.borrow();
self.check_modulo_usage(node, source, &mut violations, &type_map, ¯os);
drop(macros);
violations
}
}
impl Int10C {
fn check_modulo_usage(
&self,
node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
type_map: &HashMap<String, String>,
macros: &MacroConstantMap,
) {
let mut fn_type_maps: HashMap<usize, HashMap<String, String>> = HashMap::new();
for n in query::find_descendants_of_kind(*node, "binary_expression") {
if let Some(operator) = n.child_by_field_name("operator") {
let op_text = get_node_text(&operator, source);
if op_text == "%" {
let scoped_type_map: &HashMap<String, String> =
match overflow_helpers::enclosing_function_definition(&n) {
Some(func_node) => {
fn_type_maps.entry(func_node.id()).or_insert_with(|| {
overflow_helpers::collect_variable_types(&func_node, source)
})
}
None => type_map,
};
if self.is_potentially_signed_modulo(&n, source, scoped_type_map, macros) {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
message: "Modulo operator used with potentially signed operands. \
The result of % with negative operands is implementation-defined \
and can be negative. Use unsigned types (size_t, unsigned int) \
or explicitly handle negative remainders."
.to_string(),
severity: self.severity(),
line: operator.start_position().row + 1,
column: operator.start_position().column + 1,
file_path: String::new(),
suggestion: Some(
"Convert operands to unsigned types (size_t, unsigned int) \
or add explicit checks for negative values"
.to_string(),
),
requires_manual_review: Some(true),
});
}
}
}
}
}
fn is_potentially_signed_modulo(
&self,
modulo_node: &Node,
source: &str,
type_map: &HashMap<String, String>,
macros: &MacroConstantMap,
) -> bool {
let left = modulo_node.child_by_field_name("left");
let right = modulo_node.child_by_field_name("right");
if left.is_none() || right.is_none() {
return false;
}
let left_node = left.unwrap();
let right_node = right.unwrap();
let left_text = get_node_text(&left_node, source);
let right_text = get_node_text(&right_node, source);
let expr_is_unsigned = self.looks_unsigned(&left_text) || self.looks_unsigned(&right_text);
if expr_is_unsigned {
return false;
}
if self.operand_has_unsigned_type(&left_node, source, type_map)
|| self.operand_has_unsigned_type(&right_node, source, type_map)
{
return false;
}
if self.operand_is_nonnegative_constant(&left_node, source, macros) {
return false;
}
if self.dividend_is_nonnegative_by_vra(&left_node, source, macros) {
return false;
}
if left_node.kind() == "field_expression" || right_node.kind() == "field_expression" {
return false;
}
if self.is_in_function_with_unsigned_params(modulo_node, source) {
return false;
}
true
}
fn operand_has_unsigned_type(
&self,
node: &Node,
source: &str,
type_map: &HashMap<String, String>,
) -> bool {
let typedef_types = self.typedef_types.borrow();
query::find_first_descendant(*node, |n| {
if n.kind() == "identifier" {
let name = get_node_text(&n, source);
if let Some(t) = type_map.get(name) {
return t.contains("size_t")
|| t.contains("unsigned")
|| t.contains("uint")
|| overflow_helpers::is_short_unsigned_typedef(t)
|| overflow_helpers::typedef_chain_is_unsigned(t, &typedef_types);
}
}
if n.kind() == "sizeof_expression" {
return true;
}
if let Some(name) = misparsed_cast_type_name(&n, source) {
if overflow_helpers::typedef_chain_is_unsigned(name, &typedef_types) {
return true;
}
}
if n.kind() == "cast_expression" {
if let Some(type_node) = n.child_by_field_name("type") {
let cast_type = get_node_text(&type_node, source);
if cast_type.contains("unsigned")
|| overflow_helpers::typedef_chain_is_unsigned(cast_type, &typedef_types)
{
return true;
}
}
}
if n.kind() == "field_expression" {
let text = get_node_text(&n, source);
if self.looks_unsigned(&text) {
return true;
}
}
false
})
.is_some()
}
fn operand_is_nonnegative_constant(
&self,
node: &Node,
source: &str,
macros: &MacroConstantMap,
) -> bool {
query::find_first_descendant(*node, |n| {
if n.kind() == "identifier" {
let name = get_node_text(&n, source);
if let Some(&value) = macros.get(name) {
return value >= 0;
}
}
false
})
.is_some()
}
fn dividend_is_nonnegative_by_vra(
&self,
left_node: &Node,
source: &str,
macros: &MacroConstantMap,
) -> bool {
let var_ranges = match self.vra_var_ranges_at(left_node, source) {
Some(r) => r,
None => return false,
};
match const_eval::try_evaluate_range(left_node, source, macros, &var_ranges) {
Some(range) => range.min >= 0,
None => false,
}
}
fn vra_var_ranges_at(&self, expr_node: &Node, source: &str) -> Option<VarRangeMap> {
vra_access::var_ranges_replay_at(
&self.function_cfgs.borrow(),
&self.vra_results.borrow(),
expr_node,
source,
&self.current_macros.borrow(),
)
}
fn looks_unsigned(&self, text: &str) -> bool {
text.contains("size_t")
|| text.contains("unsigned")
|| text.contains("SIZE_MAX")
|| text.contains("UINT_MAX")
|| text.ends_with('u')
|| text.ends_with('U')
|| text.ends_with("ul")
|| text.ends_with("UL")
|| text.ends_with("ull")
|| text.ends_with("ULL")
}
fn is_in_function_with_unsigned_params(&self, node: &Node, source: &str) -> bool {
let mut current = node.parent();
while let Some(parent) = current {
if parent.kind() == "function_definition" {
if let Some(declarator) = parent.child_by_field_name("declarator") {
let params_text = get_node_text(&declarator, source);
if params_text.contains("size_t") || params_text.contains("unsigned") {
return true;
}
}
break;
}
current = parent.parent();
}
false
}
}