fix(security): bound call graph traversal (#11193)
Signed-off-by: Jasper Hugo <jasper@spiral.xyz>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user