fix(security): bound call graph traversal (#11193)

Signed-off-by: Jasper Hugo <jasper@spiral.xyz>
This commit is contained in:
Jasper
2026-08-20 09:45:17 +00:00
committed by GitHub
parent 21adaeb126
commit a171a3db83
@@ -7,6 +7,9 @@ use super::parser::{Call, FileAnalysis, Symbol};
/// functions in the same file (e.g. two `process()` in different impl blocks).
type NodeKey = (PathBuf, String, usize);
const MAX_CALL_GRAPH_DEPTH: u32 = 32;
const MAX_CALL_GRAPH_PATHS: usize = 2_000;
#[derive(Clone)]
pub struct ChainLink {
pub file: PathBuf,
@@ -150,13 +153,17 @@ impl CallGraph {
if depth == 0 {
return vec![];
}
let depth = depth.min(MAX_CALL_GRAPH_DEPTH);
let mut chains = Vec::new();
let mut queue: VecDeque<(Vec<NodeKey>, u32)> = VecDeque::new();
for start in starts {
'starts: for start in starts {
if let Some(neighbors) = edges.get(start) {
for neighbor in neighbors {
if queue.len() >= MAX_CALL_GRAPH_PATHS {
break 'starts;
}
queue.push_back((vec![start.clone(), neighbor.clone()], 1));
}
}
@@ -178,6 +185,9 @@ impl CallGraph {
let mut extended = false;
for neighbor in neighbors {
if !visited.contains(neighbor) {
if chains.len() + queue.len() >= MAX_CALL_GRAPH_PATHS {
break;
}
let mut new_path = path.clone();
new_path.push(neighbor.clone());
queue.push_back((new_path, d + 1));
@@ -272,3 +282,92 @@ fn resolve_callee(
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn symbol(name: impl Into<String>, line: usize) -> Symbol {
Symbol {
name: name.into(),
line,
parent: None,
detail: None,
}
}
#[test]
fn outgoing_paths_are_bounded_for_dense_graphs() {
let width = 50;
let mut functions = vec![symbol("root", 1)];
let mut calls = Vec::new();
for i in 0..width {
let name = format!("branch_{i}");
let line = 100 + i;
functions.push(symbol(&name, line));
calls.push(Call {
caller: "root".to_string(),
callee: name,
line: 2 + i,
});
}
for j in 0..width {
functions.push(symbol(format!("leaf_{j}"), 1_000 + j));
}
for i in 0..width {
for j in 0..width {
calls.push(Call {
caller: format!("branch_{i}"),
callee: format!("leaf_{j}"),
line: 100 + i,
});
}
}
let graph = CallGraph::build(&[FileAnalysis {
path: PathBuf::from("dense.rs"),
language: "rust",
loc: 2_000,
functions,
classes: Vec::new(),
imports: Vec::new(),
calls,
}]);
let chains = graph.outgoing("root", 2);
assert_eq!(chains.len(), MAX_CALL_GRAPH_PATHS);
}
#[test]
fn outgoing_depth_is_bounded() {
let node_count = MAX_CALL_GRAPH_DEPTH as usize + 20;
let functions = (0..node_count)
.map(|i| symbol(format!("node_{i}"), i + 1))
.collect();
let calls = (0..node_count - 1)
.map(|i| Call {
caller: format!("node_{i}"),
callee: format!("node_{}", i + 1),
line: i + 1,
})
.collect();
let graph = CallGraph::build(&[FileAnalysis {
path: PathBuf::from("deep.rs"),
language: "rust",
loc: node_count,
functions,
classes: Vec::new(),
imports: Vec::new(),
calls,
}]);
let chains = graph.outgoing("node_0", u32::MAX);
assert_eq!(chains.len(), 1);
assert_eq!(chains[0].len(), MAX_CALL_GRAPH_DEPTH as usize + 1);
}
}