use anyhow::Result;
use petgraph::graph::{DiGraph, NodeIndex};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use sqlitegraph::{GraphBackend, NeighborQuery, SnapshotId};
#[derive(Debug, Clone)]
pub struct Icfg {
pub graph: DiGraph<IcfgNode, IcfgEdge>,
pub node_map: HashMap<(i64, i64), NodeIndex>,
pub entry_function: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IcfgNode {
pub function_id: i64,
pub function_name: Option<String>,
pub block_id: i64,
pub node_type: IcfgNodeType,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum IcfgNodeType {
BasicBlock,
CallSite,
FunctionEntry,
FunctionExit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum IcfgEdge {
IntraProcedural { edge_type: String },
Call {
from_function: i64,
to_function: i64,
},
Return {
from_function: i64,
to_function: i64,
},
}
#[derive(Debug, Clone)]
pub struct IcfgOptions {
pub max_depth: usize,
pub include_return_edges: bool,
}
impl Default for IcfgOptions {
fn default() -> Self {
Self {
max_depth: 3,
include_return_edges: true,
}
}
}
pub fn build_icfg(
storage: &dyn crate::storage::StorageTrait,
backend: &dyn GraphBackend,
entry_function: i64,
options: IcfgOptions,
) -> Result<Icfg> {
let snapshot = SnapshotId::current();
let mut icfg = Icfg {
graph: DiGraph::new(),
node_map: HashMap::new(),
entry_function,
};
let mut queue = vec![(entry_function, 0)];
let mut visited = HashSet::new();
let mut call_relations: Vec<(i64, Vec<i64>)> = Vec::new();
while let Some((function_id, depth)) = queue.pop() {
if depth > options.max_depth || visited.contains(&function_id) {
continue;
}
visited.insert(function_id);
let blocks = storage.get_cfg_blocks(function_id)?;
if blocks.is_empty() {
continue;
}
let entry_idx = icfg.graph.add_node(IcfgNode {
function_id,
function_name: get_function_name(backend, function_id)?,
block_id: -1,
node_type: IcfgNodeType::FunctionEntry,
});
icfg.node_map.insert((function_id, -1), entry_idx);
let exit_idx = icfg.graph.add_node(IcfgNode {
function_id,
function_name: get_function_name(backend, function_id)?,
block_id: -2,
node_type: IcfgNodeType::FunctionExit,
});
icfg.node_map.insert((function_id, -2), exit_idx);
for block in &blocks {
let node_idx = icfg.graph.add_node(IcfgNode {
function_id,
function_name: get_function_name(backend, function_id)?,
block_id: block.id,
node_type: if block.terminator == "call" {
IcfgNodeType::CallSite
} else {
IcfgNodeType::BasicBlock
},
});
icfg.node_map.insert((function_id, block.id), node_idx);
}
for (idx, block) in blocks.iter().enumerate() {
let from_idx = icfg.node_map[&(function_id, block.id)];
match block.terminator.as_str() {
"fallthrough" | "goto" | "call" => {
if idx + 1 < blocks.len() {
let to_idx = icfg.node_map[&(function_id, blocks[idx + 1].id)];
icfg.graph.add_edge(
from_idx,
to_idx,
IcfgEdge::IntraProcedural {
edge_type: "fallthrough".to_string(),
},
);
}
}
"conditional" => {
if idx + 1 < blocks.len() {
let to_idx = icfg.node_map[&(function_id, blocks[idx + 1].id)];
icfg.graph.add_edge(
from_idx,
to_idx,
IcfgEdge::IntraProcedural {
edge_type: "true".to_string(),
},
);
}
if idx + 2 < blocks.len() {
let to_idx = icfg.node_map[&(function_id, blocks[idx + 2].id)];
icfg.graph.add_edge(
from_idx,
to_idx,
IcfgEdge::IntraProcedural {
edge_type: "false".to_string(),
},
);
}
}
"return" | "panic" | "break" | "continue" => {}
_ => {}
}
}
if let Some(first_block) = blocks.first() {
let entry = icfg.node_map[&(function_id, -1)];
let first = icfg.node_map[&(function_id, first_block.id)];
icfg.graph.add_edge(
entry,
first,
IcfgEdge::IntraProcedural {
edge_type: "entry".to_string(),
},
);
}
let query = NeighborQuery {
edge_type: Some("CALLS".to_string()),
..Default::default()
};
let calls_result = backend.neighbors(snapshot, function_id, query);
let mut callee_ids = match calls_result {
Ok(ids) => ids,
Err(_) => Vec::new(),
};
if callee_ids.is_empty() {
if let Ok(ids) = storage.get_callees(function_id) {
callee_ids = ids;
}
}
for callee_id in &callee_ids {
if depth + 1 <= options.max_depth && !visited.contains(callee_id) {
queue.push((*callee_id, depth + 1));
}
}
if !callee_ids.is_empty() {
call_relations.push((function_id, callee_ids));
}
}
for (function_id, callee_ids) in &call_relations {
let entry_idx = icfg.node_map[&(*function_id, -1)];
for callee_id in callee_ids {
if let Some(&callee_entry_idx) = icfg.node_map.get(&(*callee_id, -1)) {
icfg.graph.add_edge(
entry_idx,
callee_entry_idx,
IcfgEdge::Call {
from_function: *function_id,
to_function: *callee_id,
},
);
}
if options.include_return_edges {
if let (Some(&callee_exit_idx), Some(&caller_exit_idx)) = (
icfg.node_map.get(&(*callee_id, -2)),
icfg.node_map.get(&(*function_id, -2)),
) {
icfg.graph.add_edge(
callee_exit_idx,
caller_exit_idx,
IcfgEdge::Return {
from_function: *callee_id,
to_function: *function_id,
},
);
}
}
}
}
Ok(icfg)
}
fn get_function_name(backend: &dyn GraphBackend, entity_id: i64) -> Result<Option<String>> {
let snapshot = SnapshotId::current();
match backend.get_node(snapshot, entity_id) {
Ok(entity) => Ok(Some(entity.name)),
Err(_) => Ok(None),
}
}
pub fn to_dot(icfg: &Icfg) -> String {
let mut dot = String::from("digraph ICFG {\n");
dot.push_str(" rankdir=TB;\n");
dot.push_str(" node [shape=box];\n\n");
for node in icfg.graph.node_indices() {
let node_data = &icfg.graph[node];
let _label = format!(
"F{}_B{}",
node_data.function_id,
if node_data.block_id < 0 {
match node_data.node_type {
IcfgNodeType::FunctionEntry => "entry".to_string(),
IcfgNodeType::FunctionExit => "exit".to_string(),
_ => "unknown".to_string(),
}
} else {
node_data.block_id.to_string()
}
);
let style = match node_data.node_type {
IcfgNodeType::CallSite => " [style=dashed]",
IcfgNodeType::FunctionEntry => " [style=bold]",
IcfgNodeType::FunctionExit => " [style=bold]",
_ => "",
};
dot.push_str(&format!(" \"{}\"{};\n", node.index(), style));
}
for edge in icfg.graph.edge_indices() {
let (from, to) = icfg.graph.edge_endpoints(edge).unwrap();
let edge_data = &icfg.graph[edge];
let label = match edge_data {
IcfgEdge::IntraProcedural { edge_type } => edge_type.clone(),
IcfgEdge::Call { .. } => "call".to_string(),
IcfgEdge::Return { .. } => "return".to_string(),
};
let style = match edge_data {
IcfgEdge::Call { .. } => " [style=bold,color=blue]",
IcfgEdge::Return { .. } => " [style=dashed,color=red]",
_ => "",
};
dot.push_str(&format!(
" \"{}\" -> \"{}\" [label=\"{}\"{}];\n",
from.index(),
to.index(),
label,
style
));
}
dot.push_str("}\n");
dot
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IcfgJson {
pub entry_function: i64,
pub nodes: Vec<IcfgNodeJson>,
pub edges: Vec<IcfgEdgeJson>,
pub function_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IcfgNodeJson {
pub id: usize,
pub function_id: i64,
pub function_name: Option<String>,
pub block_id: i64,
pub node_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IcfgEdgeJson {
pub from: usize,
pub to: usize,
pub edge_type: String,
pub label: String,
}
impl IcfgJson {
pub fn from_icfg(icfg: &Icfg) -> Self {
use std::collections::HashSet;
let mut function_ids = HashSet::new();
let nodes: Vec<IcfgNodeJson> = icfg
.graph
.node_indices()
.map(|idx| {
let node = &icfg.graph[idx];
function_ids.insert(node.function_id);
IcfgNodeJson {
id: idx.index(),
function_id: node.function_id,
function_name: node.function_name.clone(),
block_id: node.block_id,
node_type: format!("{:?}", node.node_type),
}
})
.collect();
let edges: Vec<IcfgEdgeJson> = icfg
.graph
.edge_indices()
.map(|idx| {
let (from, to) = icfg.graph.edge_endpoints(idx).unwrap();
let edge = &icfg.graph[idx];
let (edge_type, label) = match edge {
IcfgEdge::IntraProcedural { edge_type } => ("intra", edge_type.clone()),
IcfgEdge::Call { .. } => ("call", "call".to_string()),
IcfgEdge::Return { .. } => ("return", "return".to_string()),
};
IcfgEdgeJson {
from: from.index(),
to: to.index(),
edge_type: edge_type.to_string(),
label,
}
})
.collect();
IcfgJson {
entry_function: icfg.entry_function,
nodes,
edges,
function_count: function_ids.len(),
}
}
pub fn to_inter_procedural_cfg(icfg: &Icfg) -> crate::router::InterProceduralCfg {
let mut node_id_map = std::collections::HashMap::new();
let mut next_id: i64 = 0;
let nodes: Vec<crate::router::IcfgNode> = icfg
.graph
.node_indices()
.map(|idx| {
let node = &icfg.graph[idx];
let id = next_id;
node_id_map.insert(idx, id);
next_id += 1;
crate::router::IcfgNode {
id,
function_id: node.function_id,
block_id: node.block_id,
}
})
.collect();
let edges: Vec<crate::router::IcfgEdge> = icfg
.graph
.edge_indices()
.filter_map(|idx| {
let (from, to) = icfg.graph.edge_endpoints(idx)?;
let edge = &icfg.graph[idx];
let kind = match edge {
IcfgEdge::IntraProcedural { .. } => "intra-procedural",
IcfgEdge::Call { .. } => "call",
IcfgEdge::Return { .. } => "return",
}
.to_string();
Some(crate::router::IcfgEdge {
from_node: *node_id_map.get(&from)?,
to_node: *node_id_map.get(&to)?,
kind,
})
})
.collect();
crate::router::InterProceduralCfg {
entry_function: icfg.entry_function,
nodes,
edges,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_icfg_options_default() {
let options = IcfgOptions::default();
assert_eq!(options.max_depth, 3);
assert!(options.include_return_edges);
}
#[test]
fn test_icfg_node_types() {
let entry = IcfgNodeType::FunctionEntry;
let exit = IcfgNodeType::FunctionExit;
let call = IcfgNodeType::CallSite;
let basic = IcfgNodeType::BasicBlock;
assert_eq!(entry, IcfgNodeType::FunctionEntry);
assert_eq!(exit, IcfgNodeType::FunctionExit);
assert_eq!(call, IcfgNodeType::CallSite);
assert_eq!(basic, IcfgNodeType::BasicBlock);
}
}