use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils;
use lang_parsing_substrate::query;
use std::collections::HashSet;
use tree_sitter::Node;
#[derive(Debug)]
pub struct Msc22C;
impl Msc22C {
#[allow(dead_code)]
pub fn new() -> Self {
Msc22C
}
fn is_setjmp_call(&self, call: &Node, source: &str) -> bool {
call.child_by_field_name("function")
.map(|f| f.kind() == "identifier" && ast_utils::get_node_text(&f, source) == "setjmp")
.unwrap_or(false)
}
fn is_allowed_context(&self, call: &Node, source: &str) -> bool {
let Some(parent) = call.parent() else {
return false;
};
let effective = match parent.kind() {
"expression_statement" => return true,
"binary_expression" => {
let Some(op) = parent.child(1) else {
return false;
};
let op_text = ast_utils::get_node_text(&op, source);
if op_text != "==" && op_text != "!=" {
return false;
}
let other = if parent.child_by_field_name("left") == Some(*call) {
parent.child_by_field_name("right")
} else {
parent.child_by_field_name("left")
};
if other.map(|o| o.kind() != "number_literal").unwrap_or(true) {
return false;
}
parent
}
"unary_expression" => {
let Some(op) = parent.child(0) else {
return false;
};
if ast_utils::get_node_text(&op, source) != "!" {
return false;
}
parent
}
_ => *call,
};
let Some(paren) = effective.parent() else {
return false;
};
if paren.kind() != "parenthesized_expression" {
return false;
}
let Some(stmt) = paren.parent() else {
return false;
};
matches!(
stmt.kind(),
"if_statement" | "while_statement" | "do_statement" | "for_statement"
)
}
fn check_context(&self, root: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
for call in query::find_descendants_of_kind(*root, "call_expression") {
if !self.is_setjmp_call(&call, source) {
continue;
}
if self.is_allowed_context(&call, source) {
continue;
}
let pos = call.start_position();
violations.push(RuleViolation {
rule_id: "MSC22-C".to_string(),
severity: Severity::Low,
line: pos.row + 1,
column: pos.column + 1,
message: "setjmp() used outside a context the C Standard permits (e.g. as a declaration initializer) -- undefined behavior".to_string(),
file_path: String::new(),
suggestion: Some(
"Use setjmp() only as the entire controlling expression of an if/while/for/do (optionally compared to an integer constant, or negated), or as the entire expression of an expression statement"
.to_string(),
),
requires_manual_review: Some(false),
});
}
}
fn is_declared_volatile(&self, ident: &Node, var_name: &str, source: &str) -> bool {
let Some(decl) =
ast_utils::find_enclosing_declaration_for_identifier(ident, var_name, source)
else {
return false;
};
query::find_descendants_of_kind(decl, "type_qualifier")
.iter()
.any(|n| ast_utils::get_node_text(n, source) == "volatile")
}
fn reassigned_outside(
&self,
func: &Node,
var_name: &str,
exclude: &Node,
source: &str,
) -> bool {
query::find_descendants_of_kind(*func, "assignment_expression")
.iter()
.any(|assign| {
if assign.start_byte() >= exclude.start_byte()
&& assign.end_byte() <= exclude.end_byte()
{
return false;
}
assign
.child_by_field_name("left")
.map(|l| {
l.kind() == "identifier" && ast_utils::get_node_text(&l, source) == var_name
})
.unwrap_or(false)
})
}
fn check_stale_locals(&self, func: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
for if_stmt in query::find_descendants_of_kind(*func, "if_statement") {
let Some(cond) = if_stmt.child_by_field_name("condition") else {
continue;
};
let has_setjmp = query::find_descendants_of_kind(cond, "call_expression")
.iter()
.any(|c| self.is_setjmp_call(c, source));
if !has_setjmp {
continue;
}
let mut branches = Vec::new();
if let Some(c) = if_stmt.child_by_field_name("consequence") {
branches.push(c);
}
if let Some(a) = if_stmt.child_by_field_name("alternative") {
branches.push(a);
}
let mut flagged: HashSet<String> = HashSet::new();
for branch in branches {
for ident in query::find_descendants_of_kind(branch, "identifier") {
if ident
.parent()
.map(|p| {
p.kind() == "assignment_expression"
&& p.child_by_field_name("left") == Some(ident)
})
.unwrap_or(false)
{
continue;
}
let name = ast_utils::get_node_text(&ident, source).to_string();
if flagged.contains(&name) {
continue;
}
if self.is_declared_volatile(&ident, &name, source) {
continue;
}
if !self.reassigned_outside(func, &name, &if_stmt, source) {
continue;
}
flagged.insert(name.clone());
let pos = ident.start_position();
violations.push(RuleViolation {
rule_id: "MSC22-C".to_string(),
severity: Severity::Low,
line: pos.row + 1,
column: pos.column + 1,
message: format!(
"'{}' is read here after a possible longjmp() return, but it is reassigned elsewhere and not declared volatile -- its value is indeterminate",
name
),
file_path: String::new(),
suggestion: Some(format!(
"Declare '{}' volatile if its value must survive a longjmp() back into this branch",
name
)),
requires_manual_review: Some(false),
});
}
}
}
}
fn traverse(&self, root: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
self.check_context(root, source, violations);
for func in query::find_descendants_of_kind(*root, "function_definition") {
self.check_stale_locals(&func, source, violations);
}
}
}
impl CertRule for Msc22C {
fn rule_id(&self) -> &'static str {
"MSC22-C"
}
fn description(&self) -> &'static str {
"Use the setjmp(), longjmp() facility securely"
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn severity(&self) -> Severity {
Severity::Low
}
fn cert_id(&self) -> &'static str {
"MSC22-C"
}
fn scan(&self, root: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
self.traverse(root, source, violations);
}
}