use super::super::{CertRule, RuleViolation};
use crate::manifest::{RuleCategory, Severity};
use crate::utility::cert_c::ast_utils::get_node_text;
use std::collections::HashMap;
use tree_sitter::Node;
pub struct Fio01C;
impl CertRule for Fio01C {
fn rule_id(&self) -> &'static str {
"FIO01-C"
}
fn severity(&self) -> Severity {
Severity::Medium
}
fn description(&self) -> &'static str {
"Be careful using functions that use file names for identification"
}
fn category(&self) -> RuleCategory {
RuleCategory::Recommendation
}
fn cert_id(&self) -> &'static str {
"FIO01-C"
}
fn check(&self, node: &Node, source: &str) -> Vec<RuleViolation> {
let mut violations = Vec::new();
let mut cursor = node.walk();
let mut file_name_vars: HashMap<String, Vec<FileOp>> = HashMap::new();
let mut file_descriptors: Vec<String> = Vec::new();
self.collect_file_operations(
*node,
source,
&mut file_name_vars,
&mut file_descriptors,
&mut cursor,
);
for (var_name, operations) in &file_name_vars {
let has_file_open = operations
.iter()
.any(|op| matches!(op.op_type, FileOpType::FileOpen));
let has_name_operation = operations
.iter()
.any(|op| matches!(op.op_type, FileOpType::Remove | FileOpType::Chmod));
if has_file_open && has_name_operation {
for op in operations {
if matches!(op.op_type, FileOpType::Remove | FileOpType::Chmod) {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: self.severity(),
message: format!(
"TOCTOU vulnerability: file '{}' opened with fopen() then operated on by name with {}(). Use file descriptor operations like fchmod() instead.",
var_name,
op.op_type.name()
),
file_path: String::new(),
line: op.line,
column: op.column,
suggestion: Some("Consider using open() and fchmod()/funlink() instead of fopen() and chmod()/remove()".to_string()),
..Default::default()
});
}
}
}
let has_access_check = operations
.iter()
.any(|op| matches!(op.op_type, FileOpType::AccessCheck));
let has_posix_open = operations
.iter()
.any(|op| matches!(op.op_type, FileOpType::PosixOpen | FileOpType::FileOpen));
if has_access_check && has_posix_open {
for op in operations {
if matches!(op.op_type, FileOpType::AccessCheck) {
violations.push(RuleViolation {
rule_id: self.rule_id().to_string(),
severity: self.severity(),
message: format!(
"TOCTOU race condition: file '{}' checked with {}() then opened. The file state may change between check and use.",
var_name,
op.func_name
),
file_path: String::new(),
line: op.line,
column: op.column,
suggestion: Some("Open the file directly and check the result instead of checking access/status first.".to_string()),
..Default::default()
});
}
}
}
}
violations
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
struct FileOp {
op_type: FileOpType,
func_name: String,
var_name: String,
line: usize,
column: usize,
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
enum FileOpType {
FileOpen, FileClose, Remove, Chmod, Open, FdChmod, AccessCheck, PosixOpen, }
impl FileOpType {
fn name(&self) -> &str {
match self {
FileOpType::FileOpen => "fopen",
FileOpType::FileClose => "fclose",
FileOpType::Remove => "remove",
FileOpType::Chmod => "chmod",
FileOpType::Open => "open",
FileOpType::FdChmod => "fchmod",
FileOpType::AccessCheck => "access/stat",
FileOpType::PosixOpen => "open",
}
}
}
impl Fio01C {
fn collect_file_operations<'a>(
&self,
node: Node<'a>,
source: &str,
file_name_vars: &mut HashMap<String, Vec<FileOp>>,
_file_descriptors: &mut Vec<String>,
cursor: &mut tree_sitter::TreeCursor<'a>,
) {
if node.kind() == "call_expression" {
self.check_call_expression(node, source, file_name_vars);
}
if cursor.goto_first_child() {
loop {
self.collect_file_operations(
cursor.node(),
source,
file_name_vars,
_file_descriptors,
cursor,
);
if !cursor.goto_next_sibling() {
break;
}
}
cursor.goto_parent();
}
}
fn check_call_expression(
&self,
node: Node,
source: &str,
file_name_vars: &mut HashMap<String, Vec<FileOp>>,
) {
if let Some(function_node) = node.child_by_field_name("function") {
let func_name = get_node_text(&function_node, source);
if let Some(args_node) = node.child_by_field_name("arguments") {
let op_type = match func_name {
"fopen" => Some(FileOpType::FileOpen),
"remove" => Some(FileOpType::Remove),
"chmod" => Some(FileOpType::Chmod),
"fclose" => Some(FileOpType::FileClose),
"access" | "ACCESS" | "_access" => Some(FileOpType::AccessCheck),
"stat" | "STAT" | "_stat" | "lstat" => Some(FileOpType::AccessCheck),
"open" | "OPEN" | "_open" => Some(FileOpType::PosixOpen),
_ => None,
};
if let Some(op_type) = op_type {
if let Some(first_arg) = args_node.child(1) {
if first_arg.kind() == "identifier" {
let var_name = get_node_text(&first_arg, source);
let op = FileOp {
op_type,
func_name: func_name.to_string(),
var_name: var_name.to_string(),
line: node.start_position().row + 1,
column: node.start_position().column + 1,
};
file_name_vars
.entry(var_name.to_string())
.or_default()
.push(op);
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Parser;
fn parse_c_code(source: &str) -> tree_sitter::Tree {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_c::language())
.expect("Error loading C grammar");
parser.parse(source, None).expect("Error parsing C code")
}
#[test]
fn test_fopen_remove_toctou() {
let source = r#"
void test() {
char *file_name = "test.txt";
FILE *f_ptr = fopen(file_name, "w");
fclose(f_ptr);
remove(file_name);
}
"#;
let tree = parse_c_code(source);
let rule = Fio01C;
let violations = rule.check(&tree.root_node(), source);
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("TOCTOU"));
assert!(violations[0].message.contains("remove"));
}
#[test]
fn test_fopen_chmod_toctou() {
let source = r#"
void test() {
char *file_name = "test.txt";
FILE *f_ptr = fopen(file_name, "w");
chmod(file_name, 0644);
}
"#;
let tree = parse_c_code(source);
let rule = Fio01C;
let violations = rule.check(&tree.root_node(), source);
assert_eq!(violations.len(), 1);
assert!(violations[0].message.contains("chmod"));
}
#[test]
fn test_open_fchmod_safe() {
let source = r#"
void test() {
char *file_name = "test.txt";
int fd = open(file_name, O_WRONLY | O_CREAT);
fchmod(fd, 0644);
}
"#;
let tree = parse_c_code(source);
let rule = Fio01C;
let violations = rule.check(&tree.root_node(), source);
assert_eq!(
violations.len(),
0,
"fchmod with file descriptor should be safe"
);
}
}