Unverified Commit e82c19ce by Enkelmann Committed by GitHub

Refactored fixpoint modules (#77)

parent 281a0207
/*! //! Creating and computing generic fixpoint computations.
This module implements a generic fixpoint algorithm for dataflow analysis. //!
//! For general information on dataflow analysis using fixpoint algorithms see [Wikipedia](https://en.wikipedia.org/wiki/Data-flow_analysis).
A fixpoint problem is defined as a graph where: //!
- Each node `n` gets assigned a value `val(n)` where the set of all values forms a partially ordered set. //! # General implementation notes
- Each edge `e` defines a rule `e:value -> value` how to compute the value at the end node given the value at the start node of the edge. //!
//! A fixpoint problem is defined as a graph where:
A fixpoint is an assignment of values to all nodes of the graph so that for all edges //! - Each node `n` gets assigned a value `val(n)` where the set of all values forms a partially ordered set.
`e(val(start_node)) <= val(end_node)` holds. //! - Each edge `e` defines a rule `e:value -> value` how to compute the value at the end node given the value at the start node of the edge.
//!
For general information on dataflow analysis using fixpoint algorithms see [Wikipedia](https://en.wikipedia.org/wiki/Data-flow_analysis). //! A fixpoint is reached if an assignment of values to all nodes of the graph is found
Or open an issue on github that you want more documentation here. :-) //! so that for all edges `e(val(start_node)) <= val(end_node)` holds.
*/ //! Usually one wants to find the smallest fixpoint,
//! i.e. a fixpoint such that for each node `n` the value `val(n)` is as small as possible (with respect to the partial order)
//! but also not less than a given starting value.
//!
//! As in the `graph` module, nodes are assumed to represent points in time,
//! whereas edges represent state transitions or (artificial) information flow channels.
//! In particular, only edges have transition functions and not nodes.
//!
//! In the current implementation edge transition functions are also allowed to return `None`
//! to indicate that no information flows through the edge.
//! For example, an analysis can use this to indicate edges that are never taken
//! and thus prevent dead code to affect the analysis.
//!
//! # How to compute the solution to a fixpoint problem
//!
//! To create a fixpoint computation one needs an object implementing the `Context` trait.
//! This object contains all information necessary to compute fixpoints,
//! like the graph or how to compute transition functions,
//! but not the actual starting values of a fixpoint computation.
//! With it, create a `Computation` object and then modify the node values through the object
//! to match the intended starting conditions of the fixpoint computation.
//! The `Computation` object also contains methods to actually run the fixpoint computation after the starting values are set
//! and methods to retrieve the results of the computation.
use fnv::FnvHashMap; use fnv::FnvHashMap;
use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex}; use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex};
use petgraph::visit::EdgeRef; use petgraph::visit::EdgeRef;
use std::collections::{BTreeMap, BinaryHeap}; use std::collections::{BTreeMap, BinaryHeap};
/// A fixpoint problem defines the context for a fixpoint computation. /// The context of a fixpoint computation.
/// ///
/// All trait methods have access to the FixpointProblem structure, so that context informations are accessible through it. /// All trait methods have access to the FixpointProblem structure, so that context informations are accessible through it.
pub trait Problem { pub trait Context {
/// the type of edge labels of the underlying graph
type EdgeLabel: Clone; type EdgeLabel: Clone;
/// the type of node labels of the underlying graph
type NodeLabel; type NodeLabel;
/// The type of the value that gets assigned to each node.
/// The values should form a partially ordered set.
type NodeValue: PartialEq + Eq; type NodeValue: PartialEq + Eq;
/// Get the graph on which the fixpoint computation operates.
fn get_graph(&self) -> &DiGraph<Self::NodeLabel, Self::EdgeLabel>; fn get_graph(&self) -> &DiGraph<Self::NodeLabel, Self::EdgeLabel>;
/// This function describes how to merge two values /// This function describes how to merge two values
...@@ -36,21 +63,41 @@ pub trait Problem { ...@@ -36,21 +63,41 @@ pub trait Problem {
fn update_edge(&self, value: &Self::NodeValue, edge: EdgeIndex) -> Option<Self::NodeValue>; fn update_edge(&self, value: &Self::NodeValue, edge: EdgeIndex) -> Option<Self::NodeValue>;
} }
/// The computation struct contains an intermediate result of a fixpoint computation. /// The computation struct contains an intermediate result of a fixpoint computation
pub struct Computation<T: Problem> { /// and provides methods for continuing the fixpoint computation
fp_problem: T, /// or extracting the (intermediate or final) results.
node_priority_list: Vec<usize>, // maps a node index to its priority (higher priority nodes get stabilized first) ///
priority_to_node_list: Vec<NodeIndex>, // maps a priority to the corresponding node index /// # Usage
///
/// ```
/// let mut computation = Computation::new(context, optional_default_node_value);
///
/// // set starting node values with computation.set_node_value(..)
/// // ...
///
/// computation.compute();
///
/// // get the resulting node values
/// if let Some(node_value) = computation.get_node_value(node_index) {
/// // ...
/// };
/// ```
pub struct Computation<T: Context> {
fp_context: T,
/// maps a node index to its priority (higher priority nodes get stabilized first)
node_priority_list: Vec<usize>,
/// maps a priority to the corresponding node index
priority_to_node_list: Vec<NodeIndex>,
worklist: BinaryHeap<usize>, worklist: BinaryHeap<usize>,
default_value: Option<T::NodeValue>, default_value: Option<T::NodeValue>,
node_values: FnvHashMap<NodeIndex, T::NodeValue>, node_values: FnvHashMap<NodeIndex, T::NodeValue>,
} }
impl<T: Problem> Computation<T> { impl<T: Context> Computation<T> {
/// Create a new fixpoint computation from a fixpoint problem, the corresponding graph /// Create a new fixpoint computation from a fixpoint problem, the corresponding graph
/// and a default value for all nodes if one should exists. /// and a default value for all nodes if one should exists.
pub fn new(fp_problem: T, default_value: Option<T::NodeValue>) -> Self { pub fn new(fp_context: T, default_value: Option<T::NodeValue>) -> Self {
let graph = fp_problem.get_graph(); let graph = fp_context.get_graph();
// order the nodes in weak topological order // order the nodes in weak topological order
let sorted_nodes: Vec<NodeIndex> = petgraph::algo::kosaraju_scc(&graph) let sorted_nodes: Vec<NodeIndex> = petgraph::algo::kosaraju_scc(&graph)
.into_iter() .into_iter()
...@@ -70,7 +117,7 @@ impl<T: Problem> Computation<T> { ...@@ -70,7 +117,7 @@ impl<T: Problem> Computation<T> {
} }
} }
Computation { Computation {
fp_problem, fp_context,
node_priority_list, node_priority_list,
priority_to_node_list: sorted_nodes, priority_to_node_list: sorted_nodes,
worklist, worklist,
...@@ -97,7 +144,7 @@ impl<T: Problem> Computation<T> { ...@@ -97,7 +144,7 @@ impl<T: Problem> Computation<T> {
/// Merge the value at a node with some new value. /// Merge the value at a node with some new value.
fn merge_node_value(&mut self, node: NodeIndex, value: T::NodeValue) { fn merge_node_value(&mut self, node: NodeIndex, value: T::NodeValue) {
if let Some(old_value) = self.node_values.get(&node) { if let Some(old_value) = self.node_values.get(&node) {
let merged_value = self.fp_problem.merge(&value, old_value); let merged_value = self.fp_context.merge(&value, old_value);
if merged_value != *old_value { if merged_value != *old_value {
self.set_node_value(node, merged_value); self.set_node_value(node, merged_value);
} }
...@@ -109,12 +156,12 @@ impl<T: Problem> Computation<T> { ...@@ -109,12 +156,12 @@ impl<T: Problem> Computation<T> {
/// Compute and update the value at the end node of an edge. /// Compute and update the value at the end node of an edge.
fn update_edge(&mut self, edge: EdgeIndex) { fn update_edge(&mut self, edge: EdgeIndex) {
let (start_node, end_node) = self let (start_node, end_node) = self
.fp_problem .fp_context
.get_graph() .get_graph()
.edge_endpoints(edge) .edge_endpoints(edge)
.expect("Edge not found"); .expect("Edge not found");
if let Some(start_val) = self.node_values.get(&start_node) { if let Some(start_val) = self.node_values.get(&start_node) {
if let Some(new_end_val) = self.fp_problem.update_edge(start_val, edge) { if let Some(new_end_val) = self.fp_context.update_edge(start_val, edge) {
self.merge_node_value(end_node, new_end_val); self.merge_node_value(end_node, new_end_val);
} }
} }
...@@ -123,7 +170,7 @@ impl<T: Problem> Computation<T> { ...@@ -123,7 +170,7 @@ impl<T: Problem> Computation<T> {
/// Update all outgoing edges of a node. /// Update all outgoing edges of a node.
fn update_node(&mut self, node: NodeIndex) { fn update_node(&mut self, node: NodeIndex) {
let edges: Vec<EdgeIndex> = self let edges: Vec<EdgeIndex> = self
.fp_problem .fp_context
.get_graph() .get_graph()
.edges(node) .edges(node)
.map(|edge_ref| edge_ref.id()) .map(|edge_ref| edge_ref.id())
...@@ -137,7 +184,7 @@ impl<T: Problem> Computation<T> { ...@@ -137,7 +184,7 @@ impl<T: Problem> Computation<T> {
/// Each node will be visited at most max_steps times. /// Each node will be visited at most max_steps times.
/// If a node does not stabilize after max_steps visits, the end result will not be a fixpoint but only an intermediate result of a fixpoint computation. /// If a node does not stabilize after max_steps visits, the end result will not be a fixpoint but only an intermediate result of a fixpoint computation.
pub fn compute_with_max_steps(&mut self, max_steps: u64) { pub fn compute_with_max_steps(&mut self, max_steps: u64) {
let mut steps = vec![0; self.fp_problem.get_graph().node_count()]; let mut steps = vec![0; self.fp_context.get_graph().node_count()];
while let Some(priority) = self.worklist.pop() { while let Some(priority) = self.worklist.pop() {
let node = self.priority_to_node_list[priority]; let node = self.priority_to_node_list[priority];
if steps[node.index()] < max_steps { if steps[node.index()] < max_steps {
...@@ -163,7 +210,12 @@ impl<T: Problem> Computation<T> { ...@@ -163,7 +210,12 @@ impl<T: Problem> Computation<T> {
/// Get a reference to the underlying graph /// Get a reference to the underlying graph
pub fn get_graph(&self) -> &DiGraph<T::NodeLabel, T::EdgeLabel> { pub fn get_graph(&self) -> &DiGraph<T::NodeLabel, T::EdgeLabel> {
self.fp_problem.get_graph() self.fp_context.get_graph()
}
/// Get a reference to the underlying context object
pub fn get_context(&self) -> &T {
&self.fp_context
} }
} }
...@@ -171,11 +223,11 @@ impl<T: Problem> Computation<T> { ...@@ -171,11 +223,11 @@ impl<T: Problem> Computation<T> {
mod tests { mod tests {
use super::*; use super::*;
struct FPProblem { struct FPContext {
graph: DiGraph<(), u64>, graph: DiGraph<(), u64>,
} }
impl Problem for FPProblem { impl Context for FPContext {
type EdgeLabel = u64; type EdgeLabel = u64;
type NodeLabel = (); type NodeLabel = ();
type NodeValue = u64; type NodeValue = u64;
...@@ -207,7 +259,7 @@ mod tests { ...@@ -207,7 +259,7 @@ mod tests {
} }
graph.add_edge(NodeIndex::new(100), NodeIndex::new(0), 0); graph.add_edge(NodeIndex::new(100), NodeIndex::new(0), 0);
let mut solution = Computation::new(FPProblem { graph }, None); let mut solution = Computation::new(FPContext { graph }, None);
solution.set_node_value(NodeIndex::new(0), 0); solution.set_node_value(NodeIndex::new(0), 0);
solution.compute_with_max_steps(20); solution.compute_with_max_steps(20);
......
/*! //! Generate control flow graphs out of a program term.
This module implements functions to generate (interprocedural) control flow graphs out of a program term. //!
*/ //! The generated graphs follow some basic principles:
//! * **Nodes** denote specific (abstract) points in time during program execution,
//! i.e. information does not change on a node.
//! So a basic block itself is not a node,
//! but the points in time before and after execution of the basic block can be nodes.
//! * **Edges** denote either transitions between the points in time of their start and end nodes during program execution
//! or they denote (artificial) information flow between nodes. See the `CRCallStub` edges of interprocedural control flow graphs
//! for an example of an edge that is only meant for information flow and not actual control flow.
//!
//! # General assumptions
//!
//! The graph construction algorithm assumes
//! that each basic block of the program term ends with zero, one or two jump instructions.
//! In the case of two jump instructions the first one is a conditional jump
//! and the second one is an unconditional jump.
//! Conditional calls are not supported.
//! Missing jump instructions are supported to indicate incomplete information about the control flow,
//! i.e. points where the control flow reconstruction failed.
//! These points are converted to dead ends in the control flow graphs.
//!
//! # Interprocedural control flow graph
//!
//! The function [`get_program_cfg`](fn.get_program_cfg.html) builds an interprocedural control flow graph out of a program term as follows:
//! * Each basic block is converted into two nodes, *BlkStart* and *BlkEnd*,
//! and a *block* edge from *BlkStart* to *BlkEnd*.
//! * Jumps and calls inside the program are converted to *Jump* or *Call* edges from the *BlkEnd* node of their source
//! to the *BlkStart* node of their target (which is the first block of the target function in case of calls).
//! * Calls to library functions outside the program are converted to *ExternCallStub* edges
//! from the *BlkEnd* node of the callsite to the *BlkStart* node of the basic block the call returns to
//! (if the call returns at all).
//! * For each in-program call and corresponding return jump one node and three edges are generated:
//! * An artificial node *CallReturn*
//! * A *CRCallStub* edge from the *BlkEnd* node of the callsite to *CallReturn*
//! * A *CRReturnStub* edge from the *BlkEnd* node of the returning from block to *CallReturn*
//! * A *CRCombine* edge from *CallReturn* to the *BlkStart* node of the returned to block.
//!
//! The artificial *CallReturn* nodes enable enriching the information flowing through a return edge
//! with information recovered from the corresponding callsite during a fixpoint computation.
use crate::prelude::*;
use crate::term::*; use crate::term::*;
use petgraph::graph::{DiGraph, NodeIndex}; use petgraph::graph::{DiGraph, NodeIndex};
use serde::Serialize;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
/// The graph type of an interprocedural control flow graph /// The graph type of an interprocedural control flow graph
pub type Graph<'a> = DiGraph<Node<'a>, Edge<'a>>; pub type Graph<'a> = DiGraph<Node<'a>, Edge<'a>>;
/// The node type of an interprocedural control flow graph /// The node type of an interprocedural control flow graph
///
/// Each node carries a pointer to its associated block with it.
/// For `CallReturn`nodes the associated block is the callsite block (containing the call instruction)
/// and *not* the return block (containing the return instruction).
#[derive(Serialize, Debug, PartialEq, Eq, Hash, Clone, Copy)] #[derive(Serialize, Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Node<'a> { pub enum Node<'a> {
BlkStart(&'a Term<Blk>), BlkStart(&'a Term<Blk>),
BlkEnd(&'a Term<Blk>), BlkEnd(&'a Term<Blk>),
CallReturn(&'a Term<Blk>), // The block is the one from the call instruction CallReturn(&'a Term<Blk>),
} }
impl<'a> Node<'a> { impl<'a> Node<'a> {
/// Get the block corresponding to the node.
pub fn get_block(&self) -> &'a Term<Blk> { pub fn get_block(&self) -> &'a Term<Blk> {
use Node::*; use Node::*;
match self { match self {
...@@ -37,12 +79,14 @@ impl<'a> std::fmt::Display for Node<'a> { ...@@ -37,12 +79,14 @@ impl<'a> std::fmt::Display for Node<'a> {
} }
} }
// TODO: document that we assume that the graph only has blocks with either: /// The edge type of an interprocedural fixpoint graph.
// - one unconditional call instruction ///
// - one return instruction /// Where applicable the edge carries a reference to the corresponding jump instruction.
// - at most 2 intraprocedural jump instructions, i.e. at most one of them is a conditional jump /// For `CRCombine` edges the corresponding jump is the call and not the return jump.
/// Intraprocedural jumps carry a second optional reference,
/// The node type of an interprocedural fixpoint graph /// which is only set if the jump directly follows an conditional jump,
/// i.e. it represents the "conditional jump not taken" branch.
/// In this case the other jump reference points to the untaken conditional jump.
#[derive(Serialize, Debug, PartialEq, Eq, Hash, Clone, Copy)] #[derive(Serialize, Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum Edge<'a> { pub enum Edge<'a> {
Block, Block,
...@@ -59,12 +103,14 @@ struct GraphBuilder<'a> { ...@@ -59,12 +103,14 @@ struct GraphBuilder<'a> {
program: &'a Term<Program>, program: &'a Term<Program>,
extern_subs: HashSet<Tid>, extern_subs: HashSet<Tid>,
graph: Graph<'a>, graph: Graph<'a>,
jump_targets: HashMap<Tid, (NodeIndex, NodeIndex)>, // Denotes the NodeIndices of possible jump targets /// Denotes the NodeIndices of possible jump targets
return_addresses: HashMap<Tid, Vec<(NodeIndex, NodeIndex)>>, // for each function the list of return addresses of the corresponding call sites jump_targets: HashMap<Tid, (NodeIndex, NodeIndex)>,
/// for each function the list of return addresses of the corresponding call sites
return_addresses: HashMap<Tid, Vec<(NodeIndex, NodeIndex)>>,
} }
impl<'a> GraphBuilder<'a> { impl<'a> GraphBuilder<'a> {
/// create a new builder with an amtpy graph /// create a new builder with an emtpy graph
pub fn new(program: &'a Term<Program>, extern_subs: HashSet<Tid>) -> GraphBuilder<'a> { pub fn new(program: &'a Term<Program>, extern_subs: HashSet<Tid>) -> GraphBuilder<'a> {
GraphBuilder { GraphBuilder {
program, program,
...@@ -161,7 +207,7 @@ impl<'a> GraphBuilder<'a> { ...@@ -161,7 +207,7 @@ impl<'a> GraphBuilder<'a> {
let block: &'a Term<Blk> = self.graph[node].get_block(); let block: &'a Term<Blk> = self.graph[node].get_block();
let jumps = block.term.jmps.as_slice(); let jumps = block.term.jmps.as_slice();
match jumps { match jumps {
[] => (), // TODO: Decide whether blocks without jumps should be considered hard errors or (silent) dead ends [] => (), // Blocks without jumps are dead ends corresponding to control flow reconstruction errors.
[jump] => self.add_jump_edge(node, jump, None), [jump] => self.add_jump_edge(node, jump, None),
[if_jump, else_jump] => { [if_jump, else_jump] => {
self.add_jump_edge(node, if_jump, None); self.add_jump_edge(node, if_jump, None);
...@@ -173,8 +219,8 @@ impl<'a> GraphBuilder<'a> { ...@@ -173,8 +219,8 @@ impl<'a> GraphBuilder<'a> {
/// For each return instruction and each corresponding call, add the following to the graph: /// For each return instruction and each corresponding call, add the following to the graph:
/// - a CallReturn node. /// - a CallReturn node.
/// - edges from the callsite and from the returning-from-site to the CallReturn node /// - edges from the callsite and from the returning-from site to the CallReturn node
/// - an edge from the CallReturn node to the return-to-site /// - an edge from the CallReturn node to the return-to site
fn add_call_return_node_and_edges( fn add_call_return_node_and_edges(
&mut self, &mut self,
return_from_sub: &Term<Sub>, return_from_sub: &Term<Sub>,
...@@ -237,7 +283,7 @@ impl<'a> GraphBuilder<'a> { ...@@ -237,7 +283,7 @@ impl<'a> GraphBuilder<'a> {
} }
} }
/// This function builds the interprocedural control flow graph for a program term. /// Build the interprocedural control flow graph for a program term.
pub fn get_program_cfg(program: &Term<Program>, extern_subs: HashSet<Tid>) -> Graph { pub fn get_program_cfg(program: &Term<Program>, extern_subs: HashSet<Tid>) -> Graph {
let builder = GraphBuilder::new(program, extern_subs); let builder = GraphBuilder::new(program, extern_subs);
builder.build() builder.build()
......
/*! //! Creating and computing interprocedural fixpoint problems.
This module defines a trait for interprocedural fixpoint problems. //!
//! # General notes
## Basic usage //!
//! This module supports computation of fixpoint problems on the control flow graphs generated by the `graph` module.
Define a *Context* struct containing all information that does not change during the fixpoint computation. //! As of this writing, only forward analyses are possible,
In particular, this includes the graph on which the fixpoint computation is run. //! backward analyses are not yet implemented.
Then implement the *Problem* trait for the *Context* struct. //!
The fixpoint computation can now be run as follows: //! To compute a generalized fixpoint problem,
``` //! first construct a context object implementing the `Context`trait.
let context = MyContext::new(); // MyContext needs to implement Problem //! Use it to construct a `Computation` object.
let mut computation = Computation::new(context, None); //! The `Computation` object provides the necessary methods for the actual fixpoint computation.
// add starting node values here with
computation.compute(); use super::fixpoint::Context as GeneralFPContext;
// computation is done, get solution node values here
```
*/
// TODO: When indirect jumps are sufficiently supported, the update_jump methods need access to
// target (and maybe source) nodes/TIDs, to determine which target the current edge points to.
// Alternatively, this could be achieved through usage of the specialize_conditional function.
// Currently unclear, which way is better.
use super::fixpoint::Problem as GeneralFPProblem;
use super::graph::*; use super::graph::*;
use crate::bil::Expression; use crate::bil::Expression;
use crate::prelude::*; use crate::prelude::*;
...@@ -45,31 +35,63 @@ impl<T: PartialEq + Eq> NodeValue<T> { ...@@ -45,31 +35,63 @@ impl<T: PartialEq + Eq> NodeValue<T> {
} }
} }
/// An interprocedural fixpoint problem defines the context for a fixpoint computation. /// The context for an interprocedural fixpoint computation.
///
/// Basically, a `Context` object needs to contain a reference to the actual graph,
/// a method for merging node values,
/// and methods for computing the edge transitions for each different edge type.
/// ///
/// All trait methods have access to the FixpointProblem structure, so that context informations are accessible through it. /// All trait methods have access to the FixpointProblem structure, so that context informations are accessible through it.
pub trait Problem<'a> { ///
/// All edge transition functions can return `None` to indicate that no information flows through the edge.
/// For example, this can be used to indicate edges that can never been taken.
pub trait Context<'a> {
type Value: PartialEq + Eq + Clone; type Value: PartialEq + Eq + Clone;
/// Get a reference to the graph that the fixpoint is computed on.
fn get_graph(&self) -> &Graph<'a>; fn get_graph(&self) -> &Graph<'a>;
/// Merge two node values.
fn merge(&self, value1: &Self::Value, value2: &Self::Value) -> Self::Value; fn merge(&self, value1: &Self::Value, value2: &Self::Value) -> Self::Value;
fn update_def(&self, value: &Self::Value, def: &Term<Def>) -> Self::Value; /// Transition function for `Def` terms.
/// The transition function for a basic block is computed
/// by iteratively applying this function to the starting value for each `Def` term in the basic block.
/// The iteration short-circuits and returns `None` if `update_def` returns `None` at any point.
fn update_def(&self, value: &Self::Value, def: &Term<Def>) -> Option<Self::Value>;
/// Transition function for (conditional and unconditional) `Jmp` terms.
fn update_jump( fn update_jump(
&self, &self,
value: &Self::Value, value: &Self::Value,
jump: &Term<Jmp>, jump: &Term<Jmp>,
untaken_conditional: Option<&Term<Jmp>>, untaken_conditional: Option<&Term<Jmp>>,
target: &Term<Blk>,
) -> Option<Self::Value>; ) -> Option<Self::Value>;
fn update_call(&self, value: &Self::Value, call: &Term<Jmp>, target: &Node) -> Self::Value;
/// Transition function for in-program calls.
fn update_call(
&self,
value: &Self::Value,
call: &Term<Jmp>,
target: &Node,
) -> Option<Self::Value>;
/// Transition function for return instructions.
/// Has access to the value at the callsite corresponding to the return edge.
/// This way one can recover caller-specific information on return from a function.
fn update_return( fn update_return(
&self, &self,
value: &Self::Value, value: &Self::Value,
value_before_call: Option<&Self::Value>, value_before_call: Option<&Self::Value>,
call_term: &Term<Jmp>, call_term: &Term<Jmp>,
) -> Option<Self::Value>; ) -> Option<Self::Value>;
/// Transition function for calls to functions not contained in the binary.
/// The corresponding edge goes from the callsite to the returned-to block.
fn update_call_stub(&self, value: &Self::Value, call: &Term<Jmp>) -> Option<Self::Value>; fn update_call_stub(&self, value: &Self::Value, call: &Term<Jmp>) -> Option<Self::Value>;
/// This function is used to refine the value using the information on which branch was taken on a conditional jump.
fn specialize_conditional( fn specialize_conditional(
&self, &self,
value: &Self::Value, value: &Self::Value,
...@@ -78,34 +100,37 @@ pub trait Problem<'a> { ...@@ -78,34 +100,37 @@ pub trait Problem<'a> {
) -> Option<Self::Value>; ) -> Option<Self::Value>;
} }
/// This struct is a wrapper to create a general fixpoint problem out of an interprocedural fixpoint problem. /// This struct is a wrapper to create a general fixpoint context out of an interprocedural fixpoint context.
struct GeneralizedProblem<'a, T: Problem<'a>> { struct GeneralizedContext<'a, T: Context<'a>> {
problem: T, context: T,
_phantom_graph_reference: PhantomData<Graph<'a>>, _phantom_graph_reference: PhantomData<Graph<'a>>,
} }
impl<'a, T: Problem<'a>> GeneralizedProblem<'a, T> { impl<'a, T: Context<'a>> GeneralizedContext<'a, T> {
pub fn new(problem: T) -> Self { /// Create a new generalized context out of an interprocedural context object.
GeneralizedProblem { pub fn new(context: T) -> Self {
problem, GeneralizedContext {
context,
_phantom_graph_reference: PhantomData, _phantom_graph_reference: PhantomData,
} }
} }
} }
impl<'a, T: Problem<'a>> GeneralFPProblem for GeneralizedProblem<'a, T> { impl<'a, T: Context<'a>> GeneralFPContext for GeneralizedContext<'a, T> {
type EdgeLabel = Edge<'a>; type EdgeLabel = Edge<'a>;
type NodeLabel = Node<'a>; type NodeLabel = Node<'a>;
type NodeValue = NodeValue<T::Value>; type NodeValue = NodeValue<T::Value>;
/// Get a reference to the underlying graph.
fn get_graph(&self) -> &Graph<'a> { fn get_graph(&self) -> &Graph<'a> {
self.problem.get_graph() self.context.get_graph()
} }
/// Merge two values using the merge function from the interprocedural context object.
fn merge(&self, val1: &Self::NodeValue, val2: &Self::NodeValue) -> Self::NodeValue { fn merge(&self, val1: &Self::NodeValue, val2: &Self::NodeValue) -> Self::NodeValue {
use NodeValue::*; use NodeValue::*;
match (val1, val2) { match (val1, val2) {
(Value(value1), Value(value2)) => Value(self.problem.merge(value1, value2)), (Value(value1), Value(value2)) => Value(self.context.merge(value1, value2)),
( (
CallReturnCombinator { CallReturnCombinator {
call: call1, call: call1,
...@@ -116,35 +141,37 @@ impl<'a, T: Problem<'a>> GeneralFPProblem for GeneralizedProblem<'a, T> { ...@@ -116,35 +141,37 @@ impl<'a, T: Problem<'a>> GeneralFPProblem for GeneralizedProblem<'a, T> {
return_: return2, return_: return2,
}, },
) => CallReturnCombinator { ) => CallReturnCombinator {
call: merge_option(call1, call2, |v1, v2| self.problem.merge(v1, v2)), call: merge_option(call1, call2, |v1, v2| self.context.merge(v1, v2)),
return_: merge_option(return1, return2, |v1, v2| self.problem.merge(v1, v2)), return_: merge_option(return1, return2, |v1, v2| self.context.merge(v1, v2)),
}, },
_ => panic!("Malformed CFG in fixpoint computation"), _ => panic!("Malformed CFG in fixpoint computation"),
} }
} }
/// Edge transition function.
/// Applies the transition functions from the interprocedural context object
/// corresponding to the type of the provided edge.
fn update_edge( fn update_edge(
&self, &self,
node_value: &Self::NodeValue, node_value: &Self::NodeValue,
edge: EdgeIndex, edge: EdgeIndex,
) -> Option<Self::NodeValue> { ) -> Option<Self::NodeValue> {
let graph = self.problem.get_graph(); let graph = self.context.get_graph();
let (start_node, end_node) = graph.edge_endpoints(edge).unwrap(); let (start_node, end_node) = graph.edge_endpoints(edge).unwrap();
let block_term = graph.node_weight(start_node).unwrap().get_block(); let block_term = graph.node_weight(start_node).unwrap().get_block();
match graph.edge_weight(edge).unwrap() { match graph.edge_weight(edge).unwrap() {
Edge::Block => { Edge::Block => {
let value = node_value.unwrap_value(); let value = node_value.unwrap_value();
let defs = &block_term.term.defs; let defs = &block_term.term.defs;
let end_val = defs.iter().fold(value.clone(), |accum, def| { let end_val = defs.iter().try_fold(value.clone(), |accum, def| {
self.problem.update_def(&accum, def) self.context.update_def(&accum, def)
}); });
Some(NodeValue::Value(end_val)) end_val.map(NodeValue::Value)
} }
Edge::Call(call) => Some(NodeValue::Value(self.problem.update_call( Edge::Call(call) => self
node_value.unwrap_value(), .context
call, .update_call(node_value.unwrap_value(), call, &graph[end_node])
&graph[end_node], .map(NodeValue::Value),
))),
Edge::CRCallStub => Some(NodeValue::CallReturnCombinator { Edge::CRCallStub => Some(NodeValue::CallReturnCombinator {
call: Some(node_value.unwrap_value().clone()), call: Some(node_value.unwrap_value().clone()),
return_: None, return_: None,
...@@ -158,7 +185,7 @@ impl<'a, T: Problem<'a>> GeneralFPProblem for GeneralizedProblem<'a, T> { ...@@ -158,7 +185,7 @@ impl<'a, T: Problem<'a>> GeneralFPProblem for GeneralizedProblem<'a, T> {
NodeValue::CallReturnCombinator { call, return_ } => { NodeValue::CallReturnCombinator { call, return_ } => {
if let Some(return_value) = return_ { if let Some(return_value) = return_ {
match self match self
.problem .context
.update_return(return_value, call.as_ref(), call_term) .update_return(return_value, call.as_ref(), call_term)
{ {
Some(val) => Some(NodeValue::Value(val)), Some(val) => Some(NodeValue::Value(val)),
...@@ -170,26 +197,34 @@ impl<'a, T: Problem<'a>> GeneralFPProblem for GeneralizedProblem<'a, T> { ...@@ -170,26 +197,34 @@ impl<'a, T: Problem<'a>> GeneralFPProblem for GeneralizedProblem<'a, T> {
} }
}, },
Edge::ExternCallStub(call) => self Edge::ExternCallStub(call) => self
.problem .context
.update_call_stub(node_value.unwrap_value(), call) .update_call_stub(node_value.unwrap_value(), call)
.map(NodeValue::Value), .map(NodeValue::Value),
Edge::Jump(jump, untaken_conditional) => self Edge::Jump(jump, untaken_conditional) => self
.problem .context
.update_jump(node_value.unwrap_value(), jump, *untaken_conditional) .update_jump(
node_value.unwrap_value(),
jump,
*untaken_conditional,
graph[end_node].get_block(),
)
.map(NodeValue::Value), .map(NodeValue::Value),
} }
} }
} }
/// This struct contains an intermediate result of an interprocedural fixpoint cumputation. /// An intermediate result of an interprocedural fixpoint computation.
pub struct Computation<'a, T: Problem<'a>> { ///
generalized_computation: super::fixpoint::Computation<GeneralizedProblem<'a, T>>, /// The usage instructions are identical to the usage of the general fixpoint computation object,
/// except that you need to provide an interprocedural context object instead of a general one.
pub struct Computation<'a, T: Context<'a>> {
generalized_computation: super::fixpoint::Computation<GeneralizedContext<'a, T>>,
} }
impl<'a, T: Problem<'a>> Computation<'a, T> { impl<'a, T: Context<'a>> Computation<'a, T> {
/// Generate a new computation from the corresponding problem and a default value for nodes. /// Generate a new computation from the corresponding context and an optional default value for nodes.
pub fn new(problem: T, default_value: Option<T::Value>) -> Self { pub fn new(problem: T, default_value: Option<T::Value>) -> Self {
let generalized_problem = GeneralizedProblem::new(problem); let generalized_problem = GeneralizedContext::new(problem);
let computation = super::fixpoint::Computation::new( let computation = super::fixpoint::Computation::new(
generalized_problem, generalized_problem,
default_value.map(NodeValue::Value), default_value.map(NodeValue::Value),
...@@ -200,7 +235,7 @@ impl<'a, T: Problem<'a>> Computation<'a, T> { ...@@ -200,7 +235,7 @@ impl<'a, T: Problem<'a>> Computation<'a, T> {
} }
/// Compute the fixpoint. /// Compute the fixpoint.
/// Note that this function does not terminate if the fixpoint algorithm does not stabilize /// Note that this function does not terminate if the fixpoint algorithm does not stabilize.
pub fn compute(&mut self) { pub fn compute(&mut self) {
self.generalized_computation.compute() self.generalized_computation.compute()
} }
...@@ -231,8 +266,15 @@ impl<'a, T: Problem<'a>> Computation<'a, T> { ...@@ -231,8 +266,15 @@ impl<'a, T: Problem<'a>> Computation<'a, T> {
pub fn get_graph(&self) -> &Graph { pub fn get_graph(&self) -> &Graph {
self.generalized_computation.get_graph() self.generalized_computation.get_graph()
} }
/// Get a reference to the underlying context object
pub fn get_context(&self) -> &T {
&self.generalized_computation.get_context().context
}
} }
/// Helper function to merge to values wrapped in `Option<..>`.
/// Merges `(Some(x), None)` to `Some(x)`.
fn merge_option<T: Clone, F>(opt1: &Option<T>, opt2: &Option<T>, merge: F) -> Option<T> fn merge_option<T: Clone, F>(opt1: &Option<T>, opt2: &Option<T>, merge: F) -> Option<T>
where where
F: Fn(&T, &T) -> T, F: Fn(&T, &T) -> T,
......
...@@ -58,7 +58,7 @@ impl<'a> Context<'a> { ...@@ -58,7 +58,7 @@ impl<'a> Context<'a> {
} }
} }
impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a> { impl<'a> crate::analysis::interprocedural_fixpoint::Context<'a> for Context<'a> {
type Value = State; type Value = State;
fn get_graph(&self) -> &Graph<'a> { fn get_graph(&self) -> &Graph<'a> {
...@@ -69,7 +69,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a> ...@@ -69,7 +69,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a>
value1.merge(value2) value1.merge(value2)
} }
fn update_def(&self, state: &Self::Value, def: &Term<Def>) -> Self::Value { fn update_def(&self, state: &Self::Value, def: &Term<Def>) -> Option<Self::Value> {
// first check for use-after-frees // first check for use-after-frees
if state.contains_access_of_dangling_memory(&def.term.rhs) { if state.contains_access_of_dangling_memory(&def.term.rhs) {
let warning = CweWarning { let warning = CweWarning {
...@@ -91,7 +91,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a> ...@@ -91,7 +91,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a>
Expression::Store { .. } => { Expression::Store { .. } => {
let mut state = state.clone(); let mut state = state.clone();
self.log_debug(state.handle_store_exp(&def.term.rhs), Some(&def.tid)); self.log_debug(state.handle_store_exp(&def.term.rhs), Some(&def.tid));
state Some(state)
} }
Expression::IfThenElse { Expression::IfThenElse {
condition, condition,
...@@ -120,14 +120,14 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a> ...@@ -120,14 +120,14 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a>
match state.eval(condition) { match state.eval(condition) {
Ok(Data::Value(cond)) if !cond.is_top() => { Ok(Data::Value(cond)) if !cond.is_top() => {
if cond == Bitvector::from_bit(true).into() { if cond == Bitvector::from_bit(true).into() {
true_state Some(true_state)
} else if cond == Bitvector::from_bit(false).into() { } else if cond == Bitvector::from_bit(false).into() {
false_state Some(false_state)
} else { } else {
panic!("IfThenElse with wrong condition bitsize encountered") panic!("IfThenElse with wrong condition bitsize encountered")
} }
} }
Ok(_) => true_state.merge(&false_state), Ok(_) => Some(true_state.merge(&false_state)),
Err(err) => panic!("IfThenElse-Condition evaluation failed: {}", err), Err(err) => panic!("IfThenElse-Condition evaluation failed: {}", err),
} }
} }
...@@ -137,7 +137,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a> ...@@ -137,7 +137,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a>
new_state.handle_register_assign(&def.term.lhs, expression), new_state.handle_register_assign(&def.term.lhs, expression),
Some(&def.tid), Some(&def.tid),
); );
new_state Some(new_state)
} }
} }
} }
...@@ -147,6 +147,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a> ...@@ -147,6 +147,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a>
value: &State, value: &State,
_jump: &Term<Jmp>, _jump: &Term<Jmp>,
_untaken_conditional: Option<&Term<Jmp>>, _untaken_conditional: Option<&Term<Jmp>>,
_target: &Term<Blk>,
) -> Option<State> { ) -> Option<State> {
// TODO: Implement some real specialization of conditionals! // TODO: Implement some real specialization of conditionals!
let mut new_value = value.clone(); let mut new_value = value.clone();
...@@ -159,7 +160,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a> ...@@ -159,7 +160,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a>
state: &State, state: &State,
call_term: &Term<Jmp>, call_term: &Term<Jmp>,
_target_node: &crate::analysis::graph::Node, _target_node: &crate::analysis::graph::Node,
) -> State { ) -> Option<State> {
let call = if let JmpKind::Call(ref call) = call_term.term.kind { let call = if let JmpKind::Call(ref call) = call_term.term.kind {
call call
} else { } else {
...@@ -220,7 +221,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a> ...@@ -220,7 +221,7 @@ impl<'a> crate::analysis::interprocedural_fixpoint::Problem<'a> for Context<'a>
callee_state.ids_known_to_caller = callee_state.memory.get_all_object_ids(); callee_state.ids_known_to_caller = callee_state.memory.get_all_object_ids();
callee_state.ids_known_to_caller.remove(&callee_stack_id); callee_state.ids_known_to_caller.remove(&callee_stack_id);
callee_state Some(callee_state)
} else { } else {
panic!("Indirect call edges not yet supported.") panic!("Indirect call edges not yet supported.")
// TODO: Support indirect call edges! // TODO: Support indirect call edges!
...@@ -595,7 +596,7 @@ mod tests { ...@@ -595,7 +596,7 @@ mod tests {
#[test] #[test]
fn context_problem_implementation() { fn context_problem_implementation() {
use crate::analysis::interprocedural_fixpoint::Problem; use crate::analysis::interprocedural_fixpoint::Context as IpFpContext;
use crate::analysis::pointer_inference::Data; use crate::analysis::pointer_inference::Data;
use crate::bil::*; use crate::bil::*;
use Expression::*; use Expression::*;
...@@ -632,10 +633,10 @@ mod tests { ...@@ -632,10 +633,10 @@ mod tests {
}; };
// test update_def // test update_def
state = context.update_def(&state, &def); state = context.update_def(&state, &def).unwrap();
let stack_pointer = Data::Pointer(PointerDomain::new(new_id("main", "RSP"), bv(-16))); let stack_pointer = Data::Pointer(PointerDomain::new(new_id("main", "RSP"), bv(-16)));
assert_eq!(state.eval(&Var(register("RSP"))).unwrap(), stack_pointer); assert_eq!(state.eval(&Var(register("RSP"))).unwrap(), stack_pointer);
state = context.update_def(&state, &store_term); state = context.update_def(&state, &store_term).unwrap();
// Test update_call // Test update_call
let target_block = Term { let target_block = Term {
...@@ -647,7 +648,7 @@ mod tests { ...@@ -647,7 +648,7 @@ mod tests {
}; };
let target_node = crate::analysis::graph::Node::BlkStart(&target_block); let target_node = crate::analysis::graph::Node::BlkStart(&target_block);
let call = call_term("func"); let call = call_term("func");
let mut callee_state = context.update_call(&state, &call, &target_node); let mut callee_state = context.update_call(&state, &call, &target_node).unwrap();
assert_eq!(callee_state.stack_id, new_id("func", "RSP")); assert_eq!(callee_state.stack_id, new_id("func", "RSP"));
assert_eq!(callee_state.caller_stack_ids.len(), 1); assert_eq!(callee_state.caller_stack_ids.len(), 1);
assert_eq!( assert_eq!(
...@@ -763,7 +764,7 @@ mod tests { ...@@ -763,7 +764,7 @@ mod tests {
#[test] #[test]
fn update_return() { fn update_return() {
use crate::analysis::interprocedural_fixpoint::Problem; use crate::analysis::interprocedural_fixpoint::Context as IpFpContext;
use crate::analysis::pointer_inference::object::ObjectType; use crate::analysis::pointer_inference::object::ObjectType;
use crate::analysis::pointer_inference::Data; use crate::analysis::pointer_inference::Data;
let project = mock_project(); let project = mock_project();
...@@ -771,10 +772,12 @@ mod tests { ...@@ -771,10 +772,12 @@ mod tests {
let (log_sender, _log_receiver) = crossbeam_channel::unbounded(); let (log_sender, _log_receiver) = crossbeam_channel::unbounded();
let context = Context::new(&project, cwe_sender, log_sender); let context = Context::new(&project, cwe_sender, log_sender);
let state_before_return = State::new(&register("RSP"), Tid::new("callee")); let state_before_return = State::new(&register("RSP"), Tid::new("callee"));
let mut state_before_return = context.update_def( let mut state_before_return = context
.update_def(
&state_before_return, &state_before_return,
&reg_add_term("RSP", 8, "stack_offset_on_return_adjustment"), &reg_add_term("RSP", 8, "stack_offset_on_return_adjustment"),
); )
.unwrap();
let callsite_id = new_id("call_callee", "RSP"); let callsite_id = new_id("call_callee", "RSP");
state_before_return.memory.add_abstract_object( state_before_return.memory.add_abstract_object(
...@@ -814,10 +817,12 @@ mod tests { ...@@ -814,10 +817,12 @@ mod tests {
.unwrap(); .unwrap();
let state_before_call = State::new(&register("RSP"), Tid::new("original_caller_id")); let state_before_call = State::new(&register("RSP"), Tid::new("original_caller_id"));
let mut state_before_call = context.update_def( let mut state_before_call = context
.update_def(
&state_before_call, &state_before_call,
&reg_add_term("RSP", -16, "stack_offset_on_call_adjustment"), &reg_add_term("RSP", -16, "stack_offset_on_call_adjustment"),
); )
.unwrap();
let caller_caller_id = new_id("caller_caller", "RSP"); let caller_caller_id = new_id("caller_caller", "RSP");
state_before_call.memory.add_abstract_object( state_before_call.memory.add_abstract_object(
caller_caller_id.clone(), caller_caller_id.clone(),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment