use tree_sitter::Node;
use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use lang_parsing_substrate::query;
pub struct Pos50C;
impl CertRule for Pos50C {
fn rule_id(&self) -> &'static str {
"POS50-C"
}
fn description(&self) -> &'static str {
"Declare objects shared between POSIX threads with appropriate storage durations"
}
fn severity(&self) -> Severity {
Severity::High
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn cert_id(&self) -> &'static str {
"POS50-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
self.check_node(node, source, &mut violations);
self.check_toctou(node, source, &mut violations);
violations
}
}
impl Pos50C {
fn check_node(&self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
for n in query::find_descendants_of_kind(*node, "call_expression") {
if let Some(func_node) = n.child_by_field_name("function") {
let func_name = get_node_text(&func_node, source);
if func_name.trim() == "pthread_create" {
self.check_pthread_create(&n, source, violations);
}
}
}
}
fn check_pthread_create(&self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
if let Some(args) = node.child_by_field_name("arguments") {
if let Some(arg_node) = self.get_fourth_argument(&args) {
if self.is_address_of_local_or_thread_local(&arg_node, node, source) {
let position = node.start_position();
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: self.severity(),
line: position.row + 1,
column: position.column + 1,
file_path: String::new(),
message:
"Passing address of automatic or thread-local storage to pthread_create(). \
The thread may outlive the variable's lifetime, causing undefined behavior."
.to_string(),
suggestion: Some(
"Use static storage duration (global/static variables) or dynamically \
allocated memory for data shared between threads.".to_string()
),
requires_manual_review: None,
});
}
}
}
}
fn get_fourth_argument<'a>(&self, args_node: &'a Node) -> Option<Node<'a>> {
let mut arg_count = 0;
let mut cursor = args_node.walk();
for child in args_node.children(&mut cursor) {
if child.kind() != "(" && child.kind() != ")" && child.kind() != "," {
arg_count += 1;
if arg_count == 4 {
return Some(child);
}
}
}
None
}
fn is_address_of_local_or_thread_local(
&self,
node: &Node,
pthread_node: &Node,
source: &str,
) -> bool {
if node.kind() == "unary_expression" {
if let Some(op) = node.child_by_field_name("operator") {
let op_text = get_node_text(&op, source);
if op_text.trim() == "&" {
if let Some(operand) = node.child_by_field_name("argument") {
if let Some(var_name) = self.extract_identifier(&operand, source) {
return self.is_local_or_thread_local(&var_name, pthread_node, source);
}
}
}
}
}
let node_text = get_node_text(node, source);
if node_text.trim().starts_with('&') {
let var_part = node_text.trim().trim_start_matches('&');
let var_name = var_part
.split(|c: char| !c.is_alphanumeric() && c != '_')
.next()
.unwrap_or("")
.to_string();
if !var_name.is_empty() {
return self.is_local_or_thread_local(&var_name, pthread_node, source);
}
}
false
}
fn extract_identifier(&self, node: &Node, source: &str) -> Option<String> {
match node.kind() {
"identifier" => Some(get_node_text(node, source).trim().to_string()),
"cast_expression" => {
if let Some(value) = node.child_by_field_name("value") {
return self.extract_identifier(&value, source);
}
None
}
_ => None,
}
}
fn is_local_or_thread_local(&self, var_name: &str, node: &Node, source: &str) -> bool {
let mut parent = node.parent();
while let Some(p) = parent {
if p.kind() == "function_definition" {
if self.is_declared_in_function(&p, var_name, source) {
return true;
}
}
parent = p.parent();
}
false
}
fn is_declared_in_function(&self, func_node: &Node, var_name: &str, source: &str) -> bool {
if let Some(body) = func_node.child_by_field_name("body") {
return self.search_for_declaration(&body, var_name, source);
}
false
}
fn search_for_declaration(&self, node: &Node, var_name: &str, source: &str) -> bool {
query::find_descendants_of_kind(*node, "declaration")
.into_iter()
.any(|decl| {
let mut cursor = decl.walk();
let found = decl.children(&mut cursor).any(|child| {
self.get_declarator_name(&child, source)
.is_some_and(|name| name == var_name)
});
found
})
}
fn get_declarator_name(&self, node: &Node, source: &str) -> Option<String> {
match node.kind() {
"init_declarator" => {
if let Some(declarator) = node.child_by_field_name("declarator") {
return self.get_declarator_name(&declarator, source);
}
None
}
"identifier" => Some(get_node_text(node, source).trim().to_string()),
"pointer_declarator" | "array_declarator" => {
if let Some(declarator) = node.child_by_field_name("declarator") {
return self.get_declarator_name(&declarator, source);
}
None
}
_ => None,
}
}
#[allow(dead_code)]
fn is_thread_local_global(&self, root_node: &Node, var_name: &str, source: &str) -> bool {
let mut cursor = root_node.walk();
for child in root_node.children(&mut cursor) {
if child.kind() == "declaration" {
let has_thread_local = self.has_thread_local_specifier(&child, source);
let mut decl_cursor = child.walk();
for decl_child in child.children(&mut decl_cursor) {
if let Some(name) = self.get_declarator_name(&decl_child, source) {
if name == var_name && has_thread_local {
return true;
}
}
}
}
}
false
}
fn has_thread_local_specifier(&self, decl_node: &Node, source: &str) -> bool {
let mut cursor = decl_node.walk();
for child in decl_node.children(&mut cursor) {
if child.kind() == "storage_class_specifier" {
let text = get_node_text(&child, source);
if text.trim() == "__thread" || text.trim() == "_Thread_local" {
return true;
}
}
}
false
}
#[allow(dead_code)]
fn get_translation_unit<'a>(&self, node: &Node<'a>) -> Option<Node<'a>> {
let mut current = Some(*node);
while let Some(n) = current {
if n.kind() == "translation_unit" {
return Some(n);
}
current = n.parent();
}
None
}
fn is_check_function(name: &str) -> bool {
matches!(
name,
"stat" | "lstat" | "fstat" | "access" | "faccessat" | "euidaccess"
)
}
fn is_use_function(name: &str) -> bool {
matches!(
name,
"fopen"
| "open"
| "openat"
| "creat"
| "freopen"
| "remove"
| "unlink"
| "rename"
| "chmod"
| "chown"
| "truncate"
| "mkdir"
| "rmdir"
| "link"
| "symlink"
)
}
fn extract_path_arg<'a>(call_node: &Node<'a>, source: &'a str) -> Option<&'a str> {
let args = call_node.child_by_field_name("arguments")?;
let mut cursor = args.walk();
for child in args.children(&mut cursor) {
if child.kind() != "(" && child.kind() != ")" && child.kind() != "," {
if child.kind() == "identifier" {
return Some(get_node_text(&child, source));
}
return None; }
}
None
}
fn check_toctou(&self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
for n in query::find_descendants_of_kind(*node, "function_definition") {
if let Some(body) = n.child_by_field_name("body") {
self.check_toctou_in_body(&body, source, violations);
}
}
}
fn check_toctou_in_body(&self, body: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
let mut calls: Vec<(String, String, usize, usize)> = Vec::new(); self.collect_calls_in_order(body, source, &mut calls);
for i in 0..calls.len() {
let (ref check_func, ref check_path, _, _) = calls[i];
if !Self::is_check_function(check_func) {
continue;
}
for (use_func, use_path, use_line, use_col) in calls.iter().skip(i + 1) {
if Self::is_use_function(use_func) && check_path == use_path {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: Severity::High,
line: *use_line,
column: *use_col,
file_path: String::new(),
message: format!(
"TOCTOU race condition: {}() checks '{}' then {}() uses it. \
The file state may change between the check and use.",
check_func, check_path, use_func
),
suggestion: Some(
"Use atomic file operations (e.g., open() with O_CREAT|O_EXCL) \
instead of separate check-then-use sequences."
.to_string(),
),
requires_manual_review: None,
});
break; }
}
}
}
fn collect_calls_in_order(
&self,
node: &Node,
source: &str,
calls: &mut Vec<(String, String, usize, usize)>,
) {
for n in query::find_descendants_of_kind(*node, "call_expression") {
if let Some(func) = n.child_by_field_name("function") {
let func_name = get_node_text(&func, source);
if Self::is_check_function(func_name) || Self::is_use_function(func_name) {
if let Some(path_var) = Self::extract_path_arg(&n, source) {
calls.push((
func_name.to_string(),
path_var.to_string(),
n.start_position().row + 1,
n.start_position().column + 1,
));
}
}
}
}
}
}