Repository: angr/angr

Issue:
CFGFast: Infinite loop while building CFG of mbedtls' sha512 on CortexM
<!--
*Disclaimer:
The angr suite is maintained by a small team of volunteers.
While we cannot guarantee any timeliness for fixes and enhancements, we will do our best.
For more real-time help with angr, from us and the community, join our [Slack.](http://angr.io/invite/)*
-->
---

**Describe the bug.**
<!--
Please include a clear and concise description of what the bug is.
-->
CFGFast simply does not complete.  No obvious log info is given

**Environment Information.**
<!--
Many common issues are caused by problems with the local Python environment.
Before submitting, double-check that your versions of all modules in the angr suite (angr, cle, pyvex, ...) are up to date.
Please include the output of `python -m angr.misc.bug_report` here.
-->
(env omitted, but it's `fix/real_xrefs2` + master on everything else)

**To Reproduce.**
<!--
Please include *both a script to reproduce the crash, and attach the binary used, if possible*
-->

```python

import angr

p = angr.Project("sha512.o", load_options={"rebase_granularity":0x1000})

cfg = p.analyses.CFG(resolve_indirect_jumps=True, cross_references=True, detect_tail_calls=True, force_complete_scan=False, normalize=True)
```

sha512.o attached, it's just from mbedtls compiled with gcc7 for a Cortex-M3
[sha512.o.gz](https://github.com/angr/angr/files/3393573/sha512.o.gz)


**Additional context.**
<!--
Add any other context about the problem here.
-->



Discussion:

Resolving patch:
diff --git a/angr/analyses/forward_analysis.py b/angr/analyses/forward_analysis.py
--- a/angr/analyses/forward_analysis.py
+++ b/angr/analyses/forward_analysis.py
@@ -1,5 +1,5 @@
 
-import logging
+from functools import reduce
 
 import networkx
 
@@ -18,7 +18,7 @@
 # Graph traversal
 #
 
-class GraphVisitor(object):
+class GraphVisitor:
     """
     A graph visitor takes a node in the graph and returns its successors. Typically it visits a control flow graph, and
     returns successors of a CFGNode each time. This is the base class of all graph visitors.
@@ -148,7 +148,7 @@ def all_successors(self, node, skip_reached_fixedpoint=False):
 
         return successors
 
-    def revisit(self, node, include_self=True):
+    def revisit_successors(self, node, include_self=True):
         """
         Revisit a node in the future. As a result, the successors to this node will be revisited as well.
 
@@ -167,6 +167,19 @@ def revisit(self, node, include_self=True):
         # reorder it
         self._sorted_nodes = OrderedSet(sorted(self._sorted_nodes, key=lambda n: self._node_to_index[n]))
 
+    def revisit_node(self, node):
+        """
+        Revisit a node in the future. Do not include its successors immediately.
+
+        :param node:    The node to revisit in the future.
+        :return:        None
+        """
+
+        self._sorted_nodes.add(node)
+
+        # reorder it
+        self._sorted_nodes = OrderedSet(sorted(self._sorted_nodes, key=lambda n: self._node_to_index[n]))
+
     def reached_fixedpoint(self, node):
         """
         Mark a node as reached fixed-point. This node as well as all its successors will not be visited in the future.
@@ -527,6 +540,13 @@ def _run_on_node(self, node, state):
         """
         The analysis routine that runs on each node in the graph.
 
+        For compatibility reasons, the variable `changed` in the returning tuple can have three values:
+        - True, means a change has occurred, the output state is not the same as the input state, and a fixed point is
+          not reached. Usually used if the analysis performs weak updates.
+        - False, means no change has occurred. Usually used if the analysis performs weak updates.
+        - None, means no change detection is performed during this process (e.g., if the analysis requires strong
+          updates), and change detection will be performed later during _merge_states().
+
         :param node:    A node in the graph.
         :param state:   An abstract state that acts as the initial abstract state of this analysis routine.
         :return:        A tuple: (changed, output abstract state)
@@ -540,7 +560,8 @@ def _merge_states(self, node, *states):
 
         :param node:   A node in the graph.
         :param states: Abstract states to merge.
-        :return:       A merged abstract state.
+        :return:       A merged abstract state, and a boolean variable indicating if a local fixed-point has reached (
+                       i.e., union(state0, state1) == state0), in which case, its successors will not be revisited.
         """
 
         raise NotImplementedError('_merge_states() is not implemented.')
@@ -601,7 +622,7 @@ def _analysis_core_graph(self):
                 # all done!
                 break
 
-            job_state = self._pop_input_state(n)
+            job_state = self._get_input_state(n)
             if job_state is None:
                 job_state = self._initial_abstract_state(n)
 
@@ -611,14 +632,21 @@ def _analysis_core_graph(self):
             changed, output_state = self._run_on_node(n, job_state)
 
             # output state of node n is input state for successors to node n
-            self._add_input_state(n, output_state)
+            successors_to_visit = self._add_input_state(n, output_state)
 
-            if not changed:
-                # reached a fixed point
+            if changed is False:
+                # no change is detected
                 continue
-
-            # add all successors
-            self._graph_visitor.revisit(n, include_self=False)
+            elif changed is True:
+                # changes detected
+                # revisit all its successors
+                self._graph_visitor.revisit_successors(n, include_self=False)
+            else:
+                # the change of states are determined during state merging (_add_input_state()) instead of during
+                # simulated execution (_run_on_node()).
+                # revisit all successors in the `successors_to_visit` list
+                for succ in successors_to_visit:
+                    self._graph_visitor.revisit_node(succ)
 
     def _add_input_state(self, node, input_state):
         """
@@ -630,12 +658,26 @@ def _add_input_state(self, node, input_state):
         """
 
         successors = self._graph_visitor.successors(node)
+        successors_to_visit = set()  # a collection of successors whose input states did not reach a fixed point
 
         for succ in successors:
             if succ in self._state_map:
-                self._state_map[succ] = self._merge_states(succ, *([ self._state_map[succ], input_state ]))
+                to_merge = [ self._state_map[succ], input_state ]
+                r = self._merge_states(succ, *to_merge)
+                if type(r) is tuple and len(r) == 2:
+                    merged_state, reached_fixedpoint = r
+                else:
+                    # compatibility concerns
+                    merged_state, reached_fixedpoint = r, False
+                self._state_map[succ] = merged_state
             else:
                 self._state_map[succ] = input_state
+                reached_fixedpoint = False
+
+            if not reached_fixedpoint:
+                successors_to_visit.add(succ)
+
+        return successors_to_visit
 
     def _pop_input_state(self, node):
         """
@@ -649,6 +691,16 @@ def _pop_input_state(self, node):
             return self._state_map.pop(node)
         return None
 
+    def _get_input_state(self, node):
+        """
+        Get the input abstract state for this node.
+
+        :param node:    The node in graph.
+        :return:        A merged state, or None if there is no input state for this node available.
+        """
+
+        return self._state_map.get(node, None)
+
     def _merge_state_from_predecessors(self, node):
         """
         Get abstract states for all predecessors of the node, merge them, and return the merged state.
diff --git a/angr/analyses/stack_pointer_tracker.py b/angr/analyses/stack_pointer_tracker.py
--- a/angr/analyses/stack_pointer_tracker.py
+++ b/angr/analyses/stack_pointer_tracker.py
@@ -66,6 +66,7 @@ def __eq__(self, other):
     def __repr__(self):
         return str(self.offset)
 
+
 class OffsetVal:
 
     __slots__ = ( '_reg', '_offset', )
@@ -111,6 +112,7 @@ def __hash__(self):
     def __repr__(self):
         return 'reg({}){:+}'.format(self.reg, (self.offset - 2**self.reg.bitlen) if self.offset != 0 else 0)
 
+
 class FrozenStackPointerTrackerState:
 
     __slots__ = 'regs', 'memory', 'is_tracking_memory'
@@ -158,6 +160,7 @@ def give_up_on_memory_tracking(self):
         self.is_tracking_memory = False
 
     def store(self, addr, val):
+        # strong update
         if self.is_tracking_memory and val is not None and addr is not None:
             self.memory[addr] = val
 
@@ -182,6 +185,9 @@ def get(self, reg):
         raise CouldNotResolveException
 
     def put(self, reg, val):
+        # strong update, but we only update values for registers that are already in self.regs and ignore all other
+        # registers. obviously, self.regs should be initialized with registers that should be considered during
+        # tracking,
         if reg in self.regs:
             self.regs[reg] = val
 
@@ -193,7 +199,6 @@ def freeze(self):
                                               frozenset(self.memory.items()),
                                               self.is_tracking_memory)
 
-
     def __eq__(self, other):
         if type(other) is StackPointerTrackerState or isinstance(other, StackPointerTrackerState):
             cond1 = self.regs == other.regs and self.is_tracking_memory == other.is_tracking_memory
@@ -204,9 +209,9 @@ def __eq__(self, other):
 
     def __hash__(self):
         if self.is_tracking_memory:
-            return hash(StackPointerTrackerState, self.regs, self.memory, self.is_tracking_memory)
+            return hash((StackPointerTrackerState, self.regs, self.memory, self.is_tracking_memory))
         else:
-            return hash(StackPointerTrackerState, self.regs, self.is_tracking_memory)
+            return hash((StackPointerTrackerState, self.regs, self.is_tracking_memory))
 
     def merge(self, other):
         return StackPointerTrackerState(regs=_dict_merge(self.regs, other.regs),
@@ -219,9 +224,11 @@ def _dict_merge(d1, d2):
     merged = {}
     for k in all_keys:
         if k not in d1 or d1[k] is TOP:
-            pass # don't add it to the dict, which is the same as top
+            # don't add it to the dict, which is the same as top
+            pass
         elif k not in d2 or d2[k] is TOP:
-            pass # don't add it to the dict, which is the same as top
+            # don't add it to the dict, which is the same as top
+            pass
         elif d1[k] == d2[k]:
             merged[k] = d1[k]
     return merged
@@ -256,7 +263,7 @@ def __init__(self, func : Function, reg_offsets : set, track_memory=True):
         self.states = { }
         self._blocks = { }
 
-        _l.debug('Running on function %s', self._func)
+        _l.debug('Running on function %r', self._func)
         self._analyze()
 
     def _state_for(self, addr, pre_or_post):
@@ -279,7 +286,6 @@ def _offset_for(self, addr, pre_or_post, reg):
         else:
             return regval.offset
 
-
     def offset_after(self, addr, reg):
         return self._offset_for(addr, 'post', reg)
 
@@ -349,7 +355,6 @@ def _set_pre_state(self, addr, new_val):
         self._set_state(addr, new_val, 'pre')
 
     def _run_on_node(self, node : BlockNode, state):
-        input_state = state
 
         block = self.project.factory.block(node.addr, size=node.size)
         self._blocks[node.addr] = block
@@ -426,11 +431,11 @@ def resolve_stmt(stmt):
         _l.debug('Mem: %s', state.memory)
 
         output_state = state.freeze()
-        return output_state != input_state, output_state
+        return None, output_state
 
     def _widen_states(self, *states):
         assert len(states) == 2
-        merged = self._merge_states(None, *states)
+        merged, _ = self._merge_states(None, *states)
         if len(merged.memory) > 5:
             _l.info('Encountered too many memory writes in stack pointer tracking. Abandoning memory tracking.')
             merged = merged.unfreeze().give_up_on_memory_tracking().freeze()
@@ -439,7 +444,8 @@ def _widen_states(self, *states):
     def _merge_states(self, node, *states):
 
         assert len(states) == 2
-        return states[0].merge(states[1])
+        merged_state = states[0].merge(states[1])
+        return merged_state, merged_state == states[0]
 
 
 from ..analyses import AnalysesHub


Test patch: