use crate::manifest::{RuleCategory, Severity};
use crate::prelude::RuleViolation;
use crate::rules::cert_c::CertRule;
use crate::utility::cert_c::ast_utils::get_node_text;
use std::collections::HashSet;
use tree_sitter::Node;
pub struct Pos38C;
impl CertRule for Pos38C {
fn rule_id(&self) -> &'static str {
"POS38-C"
}
fn cert_id(&self) -> &'static str {
"POS38"
}
fn description(&self) -> &'static str {
"Beware of race conditions when using fork and file descriptors"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn category(&self) -> RuleCategory {
RuleCategory::Rule
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
self.check_node(node, source, &mut violations);
violations
}
}
impl Pos38C {
fn check_node(&self, node: &Node, source: &str, violations: &mut Vec<RuleViolation>) {
if node.kind() == "function_definition" {
self.check_scope_for_fork_pattern(node, source, violations);
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.check_node(&child, source, violations);
}
}
fn check_scope_for_fork_pattern(
&self,
node: &Node,
source: &str,
violations: &mut Vec<RuleViolation>,
) {
let scope_node = if node.kind() == "function_definition" {
if let Some(body) = node.child_by_field_name("body") {
body
} else {
return;
}
} else {
*node };
let mut file_descriptors = HashSet::new();
self.collect_file_descriptors(&scope_node, source, &mut file_descriptors);
self.check_for_fork_with_fd_usage(&scope_node, source, &file_descriptors, violations);
}
fn collect_file_descriptors(
&self,
node: &Node,
source: &str,
file_descriptors: &mut HashSet<String>,
) {
if node.kind() == "declaration" {
let decl_text = get_node_text(node, source);
if self.is_file_opening_call(&decl_text) {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "init_declarator" {
if let Some(declarator) = child.child_by_field_name("declarator") {
if let Some(var_name) = self.extract_variable_name(&declarator, source)
{
file_descriptors.insert(var_name);
}
}
}
}
}
} else if node.kind() == "assignment_expression" {
if let Some(left) = node.child_by_field_name("left") {
if let Some(right) = node.child_by_field_name("right") {
let right_text = get_node_text(&right, source);
if self.is_file_opening_call(&right_text) {
let var_name = get_node_text(&left, source).trim().to_string();
file_descriptors.insert(var_name);
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.collect_file_descriptors(&child, source, file_descriptors);
}
}
fn is_file_opening_call(&self, text: &str) -> bool {
let text = text.trim();
text.contains("open(")
|| text.contains("fopen(")
|| text.contains("fdopen(")
|| text.contains("creat(")
|| text.contains("openat(")
}
#[allow(dead_code)]
fn find_declarator<'a>(&self, node: &'a Node) -> Option<Node<'a>> {
if node.kind() == "init_declarator" {
node.child_by_field_name("declarator")
} else if node.kind() == "declaration" {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "init_declarator" {
if let Some(declarator) = child.child_by_field_name("declarator") {
return Some(declarator);
}
}
}
None
} else {
None
}
}
fn extract_variable_name(&self, declarator: &Node, source: &str) -> Option<String> {
match declarator.kind() {
"identifier" => Some(get_node_text(declarator, source).trim().to_string()),
"pointer_declarator" => {
let mut cursor = declarator.walk();
for child in declarator.children(&mut cursor) {
if child.kind() == "identifier" {
return Some(get_node_text(&child, source).trim().to_string());
}
}
None
}
_ => None,
}
}
fn check_for_fork_with_fd_usage(
&self,
node: &Node,
source: &str,
file_descriptors: &HashSet<String>,
violations: &mut Vec<RuleViolation>,
) {
if node.kind() == "call_expression" {
if let Some(function) = node.child_by_field_name("function") {
let func_text = get_node_text(&function, source);
if func_text.trim() == "fork" {
self.check_fd_usage_after_fork(node, source, file_descriptors, violations);
return; }
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.check_for_fork_with_fd_usage(&child, source, file_descriptors, violations);
}
}
fn check_fd_usage_after_fork(
&self,
fork_node: &Node,
source: &str,
file_descriptors: &HashSet<String>,
violations: &mut Vec<RuleViolation>,
) {
if let Some(parent_stmt) = self.find_parent_statement(fork_node) {
if let Some(parent) = parent_stmt.parent() {
let mut found_stmt = false;
let mut cursor = parent.walk();
for sibling in parent.children(&mut cursor) {
if found_stmt && sibling.kind() == "if_statement" {
let has_else_branch = sibling.child_by_field_name("alternative").is_some();
if has_else_branch {
let parent_branch_uses_fd = self.branch_uses_file_descriptor(
&sibling,
source,
file_descriptors,
true,
);
let child_branch_uses_fd = self.branch_uses_file_descriptor(
&sibling,
source,
file_descriptors,
false,
);
if parent_branch_uses_fd && child_branch_uses_fd {
let start_point = fork_node.start_position();
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: Severity::Medium,
message: "Race condition detected: file descriptor used after fork() in both parent and child processes. File descriptors are shared after fork(), leading to nondeterministic behavior.".to_string(),
file_path: String::new(),
line: start_point.row + 1,
column: start_point.column + 1,
suggestion: Some(
"Close the file descriptor in one process and reopen it, or use separate file descriptors for parent and child.".to_string()
),
..Default::default()
});
return; }
}
}
if sibling.id() == parent_stmt.id() {
found_stmt = true;
}
}
}
}
}
fn find_parent_statement<'a>(&self, node: &'a Node) -> Option<Node<'a>> {
let mut current = *node;
while let Some(parent) = current.parent() {
if parent.kind() == "expression_statement" || parent.kind() == "declaration" {
if let Some(grandparent) = parent.parent() {
if grandparent.kind() == "compound_statement" {
return Some(parent);
}
}
}
current = parent;
}
None
}
#[allow(dead_code)]
fn find_following_if_statement<'a>(&self, stmt_node: &'a Node) -> Option<Node<'a>> {
if let Some(parent) = stmt_node.parent() {
let mut found_stmt = false;
let mut cursor = parent.walk();
for sibling in parent.children(&mut cursor) {
if found_stmt && sibling.kind() == "if_statement" {
return Some(sibling);
}
if sibling.id() == stmt_node.id() {
found_stmt = true;
}
}
}
None
}
fn branch_uses_file_descriptor(
&self,
if_stmt: &Node,
source: &str,
file_descriptors: &HashSet<String>,
check_else: bool,
) -> bool {
let branch = if check_else {
if_stmt.child_by_field_name("alternative")
} else {
if_stmt.child_by_field_name("consequence")
};
if let Some(branch_node) = branch {
self.subtree_uses_file_descriptor(&branch_node, source, file_descriptors)
} else {
false
}
}
fn subtree_uses_file_descriptor(
&self,
node: &Node,
source: &str,
file_descriptors: &HashSet<String>,
) -> bool {
if self.subtree_closes_file_descriptor(node, source, file_descriptors) {
return false;
}
let node_text = get_node_text(node, source);
for fd in file_descriptors {
if node_text.contains(fd) {
return true;
}
}
false
}
fn subtree_closes_file_descriptor(
&self,
node: &Node,
source: &str,
file_descriptors: &HashSet<String>,
) -> bool {
if node.kind() == "call_expression" {
if let Some(function) = node.child_by_field_name("function") {
let func_text = get_node_text(&function, source);
if func_text.trim() == "close" {
if let Some(args) = node.child_by_field_name("arguments") {
let args_text = get_node_text(&args, source);
for fd in file_descriptors {
if args_text.contains(fd) {
return true;
}
}
}
}
}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if self.subtree_closes_file_descriptor(&child, source, file_descriptors) {
return true;
}
}
false
}
}