Skip to main content

sentri_analyzer_evm/
cfg.rs

1#![allow(missing_docs)]
2//! Control Flow Graph (CFG) construction and analysis.
3//!
4//! Builds a Control Flow Graph (CFG) from Solidity AST to enable:
5//! - Execution path analysis
6//! - Vulnerability detection across execution flows
7//! - Data flow tracking through branches
8//! - Loop detection and analysis
9
10use crate::ast::AstFunction;
11use crate::errors::{AnalysisError, AnalysisResult};
12use petgraph::graph::{DiGraph, NodeIndex};
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15use tracing::{debug, info};
16
17/// A basic block in the control flow graph.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct BasicBlock {
20    /// Unique identifier for this block.
21    pub id: usize,
22    /// Statements in this block.
23    pub statements: Vec<Statement>,
24    /// Block dominates other blocks.
25    pub dominates: Vec<usize>,
26    /// Block is dominated by these blocks.
27    pub dominated_by: Vec<usize>,
28    /// Is this a loop header.
29    pub is_loop_header: bool,
30    /// Is this an exit block.
31    pub is_exit: bool,
32}
33
34impl BasicBlock {
35    /// Create a new basic block.
36    pub fn new(id: usize) -> Self {
37        Self {
38            id,
39            statements: Vec::new(),
40            dominates: Vec::new(),
41            dominated_by: Vec::new(),
42            is_loop_header: false,
43            is_exit: false,
44        }
45    }
46
47    /// Add a statement to this block.
48    pub fn add_statement(&mut self, stmt: Statement) {
49        self.statements.push(stmt);
50    }
51}
52
53/// Statement in a basic block.
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum Statement {
56    /// Variable assignment.
57    Assignment { target: String, value: String },
58    /// Conditional branch.
59    Branch { condition: String },
60    /// State mutation.
61    StateMutation { variable: String, value: String },
62    /// Function call.
63    FunctionCall { function: String, args: Vec<String> },
64    /// Return statement.
65    Return { value: Option<String> },
66    /// Generic statement.
67    Generic { code: String },
68}
69
70/// Control Flow Graph for a function.
71#[derive(Debug, Clone)]
72pub struct ControlFlowGraph {
73    /// Directed graph of basic blocks.
74    graph: DiGraph<BasicBlock, String>,
75    /// Map from block ID to node index.
76    block_map: HashMap<usize, NodeIndex>,
77    /// Entry block ID.
78    entry_block: usize,
79    /// Exit blocks.
80    exit_blocks: Vec<usize>,
81    /// Next block ID to assign.
82    next_id: usize,
83}
84
85impl ControlFlowGraph {
86    /// Create a new CFG for a function.
87    pub fn new() -> Self {
88        let mut cfg = Self {
89            graph: DiGraph::new(),
90            block_map: HashMap::new(),
91            entry_block: 0,
92            exit_blocks: Vec::new(),
93            next_id: 0,
94        };
95
96        // Create entry block
97        let entry = cfg.new_block();
98        cfg.entry_block = entry;
99
100        cfg
101    }
102
103    /// Create a new basic block in the CFG.
104    pub fn new_block(&mut self) -> usize {
105        let id = self.next_id;
106        self.next_id += 1;
107
108        let block = BasicBlock::new(id);
109        let node_idx = self.graph.add_node(block);
110        self.block_map.insert(id, node_idx);
111
112        id
113    }
114
115    /// Add an edge from one block to another.
116    pub fn add_edge(
117        &mut self,
118        from: usize,
119        to: usize,
120        label: impl Into<String>,
121    ) -> AnalysisResult<()> {
122        let from_idx = self
123            .block_map
124            .get(&from)
125            .copied()
126            .ok_or_else(|| AnalysisError::cfg("Block not found"))?;
127
128        let to_idx = self
129            .block_map
130            .get(&to)
131            .copied()
132            .ok_or_else(|| AnalysisError::cfg("Target block not found"))?;
133
134        self.graph.add_edge(from_idx, to_idx, label.into());
135        Ok(())
136    }
137
138    /// Add a statement to a block.
139    pub fn add_statement(&mut self, block_id: usize, stmt: Statement) -> AnalysisResult<()> {
140        let node_idx = self
141            .block_map
142            .get(&block_id)
143            .copied()
144            .ok_or_else(|| AnalysisError::cfg("Block not found"))?;
145
146        if let Some(block) = self.graph.node_weight_mut(node_idx) {
147            block.add_statement(stmt);
148            Ok(())
149        } else {
150            Err(AnalysisError::cfg("Failed to get block"))
151        }
152    }
153
154    /// Mark a block as an exit block.
155    pub fn mark_exit(&mut self, block_id: usize) -> AnalysisResult<()> {
156        let node_idx = self
157            .block_map
158            .get(&block_id)
159            .copied()
160            .ok_or_else(|| AnalysisError::cfg("Block not found"))?;
161
162        if let Some(block) = self.graph.node_weight_mut(node_idx) {
163            block.is_exit = true;
164            self.exit_blocks.push(block_id);
165            Ok(())
166        } else {
167            Err(AnalysisError::cfg("Failed to get block"))
168        }
169    }
170
171    /// Compute dominance relationships.
172    pub fn compute_dominance(&mut self) -> AnalysisResult<()> {
173        info!("Computing dominance relationships for CFG");
174
175        // Entry block dominates all reachable blocks
176        self.compute_dominators()?;
177
178        // Compute dominated_by relationships
179        let dominators: HashMap<usize, Vec<usize>> = self
180            .block_map
181            .iter()
182            .map(|(&id, &idx)| {
183                let dominates = self
184                    .graph
185                    .node_weight(idx)
186                    .map(|b| b.dominates.clone())
187                    .unwrap_or_default();
188                (id, dominates)
189            })
190            .collect();
191
192        for (dominator_id, dominated_blocks) in dominators {
193            for dominated_id in dominated_blocks {
194                let node_idx = self
195                    .block_map
196                    .get(&dominated_id)
197                    .copied()
198                    .ok_or_else(|| AnalysisError::cfg("Block not found"))?;
199
200                if let Some(block) = self.graph.node_weight_mut(node_idx) {
201                    block.dominated_by.push(dominator_id);
202                }
203            }
204        }
205
206        Ok(())
207    }
208
209    /// Compute immediate dominators using iterative algorithm.
210    fn compute_dominators(&mut self) -> AnalysisResult<()> {
211        let blocks: Vec<usize> = self.block_map.keys().copied().collect();
212
213        if blocks.is_empty() {
214            return Ok(());
215        }
216
217        // Initialize: everyone is dominated by all blocks
218        let mut dominators: HashMap<usize, Vec<usize>> =
219            blocks.iter().map(|&id| (id, blocks.clone())).collect();
220
221        // Entry block only dominates itself
222        if let Some(entry_doms) = dominators.get_mut(&self.entry_block) {
223            entry_doms.clear();
224            entry_doms.push(self.entry_block);
225        }
226
227        // Iterate until fixpoint
228        let mut changed = true;
229        let mut iterations = 0;
230        const MAX_ITERATIONS: usize = 100;
231
232        while changed && iterations < MAX_ITERATIONS {
233            changed = false;
234            iterations += 1;
235
236            for &block_id in &blocks {
237                if block_id == self.entry_block {
238                    continue;
239                }
240
241                // Find predecessors
242                let node_idx = self
243                    .block_map
244                    .get(&block_id)
245                    .copied()
246                    .ok_or_else(|| AnalysisError::cfg("Block not found"))?;
247
248                let mut pred_dominators: Option<Vec<usize>> = None;
249
250                for pred_idx in self
251                    .graph
252                    .neighbors_directed(node_idx, petgraph::Direction::Incoming)
253                {
254                    let pred_id = self
255                        .graph
256                        .node_weight(pred_idx)
257                        .map(|b| b.id)
258                        .ok_or_else(|| AnalysisError::cfg("Predecessor not found"))?;
259
260                    let pred_doms = dominators
261                        .get(&pred_id)
262                        .cloned()
263                        .ok_or_else(|| AnalysisError::cfg("Predecessor dominators not found"))?;
264
265                    pred_dominators = match pred_dominators {
266                        None => Some(pred_doms),
267                        Some(current) => {
268                            let intersection: Vec<_> = current
269                                .iter()
270                                .filter(|d| pred_doms.contains(d))
271                                .copied()
272                                .collect();
273                            Some(intersection)
274                        }
275                    };
276                }
277
278                // dom(block) = {block} ∪ intersection(dom(predecessors))
279                if let Some(mut pred_doms) = pred_dominators {
280                    pred_doms.push(block_id);
281                    pred_doms.sort();
282                    pred_doms.dedup();
283
284                    if let Some(old_doms) = dominators.get_mut(&block_id) {
285                        if *old_doms != pred_doms {
286                            *old_doms = pred_doms;
287                            changed = true;
288                        }
289                    }
290                }
291            }
292        }
293
294        debug!(
295            "Dominance computation converged after {} iterations",
296            iterations
297        );
298
299        // Update graph with dominance info
300        for (block_id, doms) in dominators {
301            if let Some(node_idx) = self.block_map.get(&block_id).copied() {
302                if let Some(block) = self.graph.node_weight_mut(node_idx) {
303                    block.dominates = doms.into_iter().filter(|&id| id != block_id).collect();
304                }
305            }
306        }
307
308        Ok(())
309    }
310
311    /// Check if block `a` dominates block `b`.
312    pub fn dominates(&self, a: usize, b: usize) -> bool {
313        self.block_map
314            .get(&a)
315            .and_then(|&idx| self.graph.node_weight(idx))
316            .map(|block| block.dominates.contains(&b))
317            .unwrap_or(false)
318    }
319
320    /// Detect loops in the CFG.
321    pub fn detect_loops(&mut self) -> AnalysisResult<Vec<Loop>> {
322        info!("Detecting loops in CFG");
323
324        let mut loops = Vec::new();
325
326        // Back edge detection: edge to dominator = loop
327        let blocks: Vec<usize> = self.block_map.keys().copied().collect();
328
329        for &block_id in &blocks {
330            let node_idx = self
331                .block_map
332                .get(&block_id)
333                .copied()
334                .ok_or_else(|| AnalysisError::cfg("Block not found"))?;
335
336            // Collect successors first to avoid borrowing issues
337            let successors: Vec<_> = self.graph.neighbors(node_idx).collect();
338
339            for succ_idx in successors {
340                let succ_id = self
341                    .graph
342                    .node_weight(succ_idx)
343                    .map(|b| b.id)
344                    .ok_or_else(|| AnalysisError::cfg("Successor not found"))?;
345
346                // Back edge if successor dominates this block
347                if self.dominates(succ_id, block_id) {
348                    loops.push(Loop {
349                        header: succ_id,
350                        back_edges: vec![(block_id, succ_id)],
351                        body_blocks: vec![succ_id],
352                    });
353
354                    // Mark as loop header
355                    if let Some(block) = self.graph.node_weight_mut(succ_idx) {
356                        block.is_loop_header = true;
357                    }
358                }
359            }
360        }
361
362        info!("Found {} loops", loops.len());
363        Ok(loops)
364    }
365
366    /// Get all reachable blocks from a starting block.
367    pub fn reachable(&self, from: usize) -> AnalysisResult<Vec<usize>> {
368        let start_idx = self
369            .block_map
370            .get(&from)
371            .copied()
372            .ok_or_else(|| AnalysisError::cfg("Block not found"))?;
373
374        let mut reachable = Vec::new();
375        let mut visited = std::collections::HashSet::new();
376        let mut queue = vec![start_idx];
377
378        while let Some(node_idx) = queue.pop() {
379            if !visited.insert(node_idx) {
380                continue;
381            }
382
383            if let Some(block) = self.graph.node_weight(node_idx) {
384                reachable.push(block.id);
385            }
386
387            for succ_idx in self.graph.neighbors(node_idx) {
388                queue.push(succ_idx);
389            }
390        }
391
392        Ok(reachable)
393    }
394
395    /// Get entry block ID.
396    pub fn entry(&self) -> usize {
397        self.entry_block
398    }
399
400    /// Get exit block IDs.
401    pub fn exits(&self) -> Vec<usize> {
402        self.exit_blocks.clone()
403    }
404
405    /// Get block count.
406    pub fn block_count(&self) -> usize {
407        self.graph.node_count()
408    }
409}
410
411impl Default for ControlFlowGraph {
412    fn default() -> Self {
413        Self::new()
414    }
415}
416
417/// Represents a loop in the CFG.
418#[derive(Debug, Clone)]
419pub struct Loop {
420    /// Loop header block.
421    pub header: usize,
422    /// Back edges (from, to).
423    pub back_edges: Vec<(usize, usize)>,
424    /// Blocks in loop body.
425    pub body_blocks: Vec<usize>,
426}
427
428/// CFG Builder for function analysis.
429pub struct CfgBuilder;
430
431impl CfgBuilder {
432    /// Build CFG from a function's AST.
433    pub fn build(function: &AstFunction) -> AnalysisResult<ControlFlowGraph> {
434        debug!("Building CFG for function '{}'", function.name);
435
436        let mut cfg = ControlFlowGraph::new();
437
438        // For now, simple linear CFG from function body
439        // TODO: Parse function body statements and create appropriate branching
440        let entry = cfg.entry();
441        cfg.mark_exit(entry)?;
442
443        // Compute dominance
444        cfg.compute_dominance()?;
445
446        // Detect loops
447        cfg.detect_loops()?;
448
449        Ok(cfg)
450    }
451}
452
453#[cfg(test)]
454mod tests {
455    use super::*;
456
457    #[test]
458    fn test_cfg_creation() {
459        let mut cfg = ControlFlowGraph::new();
460        let block1 = cfg.entry();
461        let block2 = cfg.new_block();
462        let block3 = cfg.new_block();
463
464        assert_eq!(cfg.block_count(), 3);
465        assert_eq!(block1, 0);
466        assert_eq!(block2, 1);
467        assert_eq!(block3, 2);
468    }
469
470    #[test]
471    fn test_cfg_edges() {
472        let mut cfg = ControlFlowGraph::new();
473        let b1 = cfg.entry();
474        let b2 = cfg.new_block();
475        let b3 = cfg.new_block();
476
477        cfg.add_edge(b1, b2, "true").ok();
478        cfg.add_edge(b1, b3, "false").ok();
479
480        assert_eq!(cfg.block_count(), 3);
481    }
482
483    #[test]
484    fn test_cfg_exit_marking() {
485        let mut cfg = ControlFlowGraph::new();
486        let entry = cfg.entry();
487
488        cfg.mark_exit(entry).ok();
489        assert!(cfg.exits().contains(&entry));
490    }
491}