use super::dominators;
use super::rules;
use super::{AnalysisContext, CfgAnalysis, CfgFinding, Confidence};
use crate::cfg::StmtKind;
use crate::patterns::Severity;
use petgraph::graph::NodeIndex;
use std::collections::HashSet;
pub struct ResourceMisuse;
fn find_acquire_nodes(
ctx: &AnalysisContext,
acquire_patterns: &[&str],
exclude_patterns: &[&str],
) -> Vec<NodeIndex> {
ctx.cfg
.node_indices()
.filter(|&idx| {
let info = &ctx.cfg[idx];
if info.kind != StmtKind::Call {
return false;
}
if let Some(callee) = &info.callee {
let callee_lower = callee.to_ascii_lowercase();
let excluded = exclude_patterns.iter().any(|p| {
let pl = p.to_ascii_lowercase();
callee_lower.ends_with(&pl) || callee_lower == pl
});
if excluded {
return false;
}
acquire_patterns.iter().any(|p| {
let pl = p.to_ascii_lowercase();
callee_lower.ends_with(&pl) || callee_lower == pl
})
} else {
false
}
})
.collect()
}
fn find_release_nodes(ctx: &AnalysisContext, release_patterns: &[&str]) -> Vec<NodeIndex> {
ctx.cfg
.node_indices()
.filter(|&idx| {
let info = &ctx.cfg[idx];
if info.kind != StmtKind::Call {
return false;
}
if let Some(callee) = &info.callee {
let callee_lower = callee.to_ascii_lowercase();
release_patterns.iter().any(|p| {
let pl = p.to_ascii_lowercase();
callee_lower.ends_with(&pl) || callee_lower == pl
})
} else {
false
}
})
.collect()
}
fn release_on_all_exit_paths(
ctx: &AnalysisContext,
acquire: NodeIndex,
release_nodes: &[NodeIndex],
exit: NodeIndex,
) -> bool {
if let Some(post_doms) = dominators::compute_post_dominators(ctx.cfg) {
for &release in release_nodes {
if dominators::dominates(&post_doms, release, acquire) {
return true;
}
}
}
let release_set: HashSet<_> = release_nodes.iter().copied().collect();
all_paths_pass_through(ctx, acquire, exit, &release_set)
}
fn all_paths_pass_through(
ctx: &AnalysisContext,
from: NodeIndex,
to: NodeIndex,
through: &HashSet<NodeIndex>,
) -> bool {
use std::collections::VecDeque;
if through.contains(&from) {
return true;
}
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
queue.push_back((from, false));
visited.insert((from, false));
while let Some((node, passed)) = queue.pop_front() {
if node == to {
if !passed {
return false; }
continue;
}
for succ in ctx.cfg.neighbors(node) {
let new_passed = passed || through.contains(&succ);
let state = (succ, new_passed);
if visited.insert(state) {
queue.push_back(state);
}
}
}
true
}
fn is_ownership_transferred(ctx: &AnalysisContext, acquire: NodeIndex) -> bool {
let acquired_var = match &ctx.cfg[acquire].defines {
Some(v) => v.clone(),
None => return false,
};
use std::collections::VecDeque;
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
for succ in ctx.cfg.neighbors(acquire) {
if visited.insert(succ) {
queue.push_back(succ);
}
}
while let Some(node) = queue.pop_front() {
let info = &ctx.cfg[node];
let (start, end) = info.span;
let references_var = info.uses.iter().any(|u| u == &acquired_var)
|| info.defines.as_ref().is_some_and(|d| d == &acquired_var);
if references_var && start < end && end <= ctx.source_bytes.len() {
let span_text = &ctx.source_bytes[start..end];
if span_text.windows(2).any(|w| w == b"->") {
return true;
}
if has_dot_field_assignment(span_text) {
return true;
}
}
if info.defines.as_ref().is_some_and(|d| d == &acquired_var) {
let is_field_write = if start < end && end <= ctx.source_bytes.len() {
let span_text = &ctx.source_bytes[start..end];
span_text.windows(2).any(|w| w == b"->") || has_dot_field_assignment(span_text)
} else {
false
};
if !is_field_write {
continue; }
}
for succ in ctx.cfg.neighbors(node) {
if visited.insert(succ) {
queue.push_back(succ);
}
}
}
false
}
fn has_dot_field_assignment(span_text: &[u8]) -> bool {
let mut i = 0;
while i < span_text.len() {
if span_text[i] == b'.' {
let mut j = i + 1;
while j < span_text.len()
&& (span_text[j].is_ascii_alphanumeric() || span_text[j] == b'_')
{
j += 1;
}
while j < span_text.len() && span_text[j].is_ascii_whitespace() {
j += 1;
}
if j < span_text.len()
&& span_text[j] == b'='
&& (j + 1 >= span_text.len() || span_text[j + 1] != b'=')
{
return true;
}
}
i += 1;
}
false
}
fn is_consumed_by_owner(ctx: &AnalysisContext, acquire: NodeIndex) -> bool {
static CONSUMING_SINKS: &[&str] = &[
"fileresponse",
"streaminghttpresponse",
"send_file",
"make_response",
];
let acquired_var = match &ctx.cfg[acquire].defines {
Some(v) => v.clone(),
None => return false,
};
use std::collections::VecDeque;
let mut visited = HashSet::new();
let mut queue = VecDeque::new();
for succ in ctx.cfg.neighbors(acquire) {
if visited.insert(succ) {
queue.push_back(succ);
}
}
while let Some(node) = queue.pop_front() {
let info = &ctx.cfg[node];
if info.kind == StmtKind::Call
&& let Some(callee) = &info.callee
{
let callee_lower = callee.to_ascii_lowercase();
let is_consuming = CONSUMING_SINKS.iter().any(|s| callee_lower.ends_with(s));
if is_consuming && info.uses.iter().any(|u| u == &acquired_var) {
return true;
}
}
if info.uses.iter().any(|u| u == &acquired_var) {
let (start, end) = info.span;
if start < end && end <= ctx.source_bytes.len() {
let span_lower: Vec<u8> = ctx.source_bytes[start..end]
.iter()
.map(|b| b.to_ascii_lowercase())
.collect();
if CONSUMING_SINKS
.iter()
.any(|s| span_lower.windows(s.len()).any(|w| w == s.as_bytes()))
{
return true;
}
}
}
for succ in ctx.cfg.neighbors(node) {
if visited.insert(succ) {
queue.push_back(succ);
}
}
}
false
}
fn has_explicit_lock_acquire(ctx: &AnalysisContext, acquire: NodeIndex) -> bool {
let acquired_var = match &ctx.cfg[acquire].defines {
Some(v) => v.clone(),
None => return false,
};
for idx in ctx.cfg.node_indices() {
let info = &ctx.cfg[idx];
if info.kind != StmtKind::Call {
continue;
}
if let Some(callee) = &info.callee {
let callee_lower = callee.to_ascii_lowercase();
let is_lock_call = callee_lower.ends_with(".acquire")
|| callee_lower.ends_with(".lock")
|| callee_lower == "pthread_mutex_lock";
if is_lock_call && info.uses.iter().any(|u| u == &acquired_var) {
return true;
}
}
}
false
}
impl CfgAnalysis for ResourceMisuse {
fn name(&self) -> &'static str {
"resource-misuse"
}
fn run(&self, ctx: &AnalysisContext) -> Vec<CfgFinding> {
let pairs = rules::resource_pairs(ctx.lang);
let exit = match dominators::find_exit_node(ctx.cfg) {
Some(e) => e,
None => return Vec::new(),
};
let mut findings = Vec::new();
for pair in pairs {
let acquire_nodes = find_acquire_nodes(ctx, pair.acquire, pair.exclude_acquire);
let release_nodes = find_release_nodes(ctx, pair.release);
for &acquire in &acquire_nodes {
if !release_on_all_exit_paths(ctx, acquire, &release_nodes, exit)
&& !is_ownership_transferred(ctx, acquire)
&& !is_consumed_by_owner(ctx, acquire)
{
if pair.resource_name == "mutex" && !has_explicit_lock_acquire(ctx, acquire) {
continue;
}
let info = &ctx.cfg[acquire];
let callee_desc = info.callee.as_deref().unwrap_or("(acquire)");
findings.push(CfgFinding {
rule_id: if pair.resource_name == "mutex" {
"cfg-lock-not-released".to_string()
} else {
"cfg-resource-leak".to_string()
},
title: format!("{} may leak", pair.resource_name),
severity: Severity::Medium,
confidence: Confidence::Medium,
span: info.span,
message: format!(
"`{callee_desc}` acquires {} but not all exit paths \
release it",
pair.resource_name
),
evidence: vec![acquire],
score: None,
});
}
}
}
findings
}
}