Repository: angr/angr

Issue:
Implementation of `node_observe()` and `insn_observe()` in `ReachingDefinitionAnalysis` seems wrong
## The problem

In [the implementation of `node_observe`](https://github.com/angr/angr/blob/c101353a917e9b0b5f5e5c21587eed3cbe371a83/angr/analyses/reaching_definitions/reaching_definitions.py#L412-L416), the following check is performed:
```python
key = 'node', ins_addr, ob_type
if self._observe_all or \
        self._observation_points is not None and key in self._observation_points:
    self.observed_results[key] = state
```

However, it seems that `_observation_points` is a list of tuples of the following form: `(address, op_type)`, [as referenced in the documentation](https://github.com/angr/angr/blob/c101353a917e9b0b5f5e5c21587eed3cbe371a83/angr/analyses/reaching_definitions/reaching_definitions.py#L304-L306).

Hence, in the case were `_observe_all=False`, this branch is never taken, and the `observed_results` is not updated.


## Proof-of-nonwork

Here is a small example highlighting the issue.
In the end, `result` is empty, while I expected it to have the value `"whatever"`.

```python
TESTS_LOCATION = str(os.path.join(
    os.path.dirname(os.path.realpath(__file__)),
    '..', '..', 'binaries', 'tests'
))
binary_path = os.path.join(TESTS_LOCATION, 'x86_64', 'all')
project = angr.Project(binary_path, load_options={'auto_load_libs': False})
cfg = project.analyses.CFGFast()

func = cfg.kb.functions['main']
observation_points = [(0x42, OP_AFTER)]
rd = project.analyses.ReachingDefinitions(func, init_func=True, observation_points=observation_points)

node_addr = 0x42
state = "whatever"
op_type = OP_AFTER

rd.node_observe(node_addr, state, op_type)

key = 'node', node_addr, ob_type
result = rd.observed_results[key]
```

## Note

It seems to be the case too for [the implementation of `insn_observe`](https://github.com/angr/angr/blob/c101353a917e9b0b5f5e5c21587eed3cbe371a83/angr/analyses/reaching_definitions/reaching_definitions.py#L418-L436).


Discussion:

Resolving patch:
diff --git a/angr/analyses/reaching_definitions/reaching_definitions.py b/angr/analyses/reaching_definitions/reaching_definitions.py
--- a/angr/analyses/reaching_definitions/reaching_definitions.py
+++ b/angr/analyses/reaching_definitions/reaching_definitions.py
@@ -318,9 +318,9 @@ def __init__(self, func=None, block=None, func_graph=None, max_iterations=3, tra
         :param int max_iterations:              The maximum number of iterations before the analysis is terminated.
         :param Boolean track_tmps:              Whether or not temporary variables should be taken into consideration
                                                 during the analysis.
-        :param iterable observation_points:     A collection of tuples of (ins_addr, OP_TYPE) defining where reaching
-                                                definitions should be copied and stored. OP_TYPE can be OP_BEFORE or
-                                                OP_AFTER.
+        :param iterable observation_points:     A collection of tuples of ("node"|"insn", ins_addr, OP_TYPE) defining
+                                                where reaching definitions should be copied and stored. OP_TYPE can be
+                                                OP_BEFORE or OP_AFTER.
         :param angr.analyses.reaching_definitions.reaching_definitions.LiveDefinitions init_state:
                                                 An optional initialization state. The analysis creates and works on a
                                                 copy.
@@ -405,45 +405,59 @@ def one_result(self):
         return next(iter(self.observed_results.values()))
 
     @deprecated(replacement="get_reaching_definitions_by_insn")
-    def get_reaching_definitions(self, ins_addr, ob_type):
-        return self.get_reaching_definitions_by_insn(ins_addr, ob_type)
+    def get_reaching_definitions(self, ins_addr, op_type):
+        return self.get_reaching_definitions_by_insn(ins_addr, op_type)
 
-    def get_reaching_definitions_by_insn(self, ins_addr, ob_type):
-
-        key = 'insn', ins_addr, ob_type
+    def get_reaching_definitions_by_insn(self, ins_addr, op_type):
+        key = 'insn', ins_addr, op_type
         if key not in self.observed_results:
             raise KeyError(("Reaching definitions are not available at observation point %s. "
                             "Did you specify that observation point?") % key)
 
         return self.observed_results[key]
 
-    def get_reaching_definitions_by_node(self, node_addr, ob_type):
-
-        key = 'node', node_addr, ob_type
+    def get_reaching_definitions_by_node(self, node_addr, op_type):
+        key = 'node', node_addr, op_type
         if key not in self.observed_results:
             raise KeyError(("Reaching definitions are not available at observation point %s. "
                             "Did you specify that observation point?") % key)
 
         return self.observed_results[key]
 
-    def node_observe(self, node_addr, state, ob_type):
-        key = 'node', node_addr, ob_type
+    def node_observe(self, node_addr, state, op_type):
+        """
+        :param int node_addr:
+        :param angr.analyses.reaching_definitions.LiveDefinitions state:
+        :param angr.analyses.reaching_definitions.constants op_type: OP_BEFORE, OP_AFTER
+        """
+
+        key = 'node', node_addr, op_type
+
         if self._observe_all or \
                 self._observation_points is not None and key in self._observation_points:
             self.observed_results[key] = state
 
-    def insn_observe(self, ins_addr, stmt, block, state, ob_type):
-        key = 'insn', ins_addr, ob_type
+    def insn_observe(self, insn_addr, stmt, block, state, op_type):
+        """
+        :param int insn_addr:
+        :param ailment.Stmt.Statement|pyvex.stmt.IRStmt stmt:
+        :param angr.Block block:
+        :param angr.analyses.reaching_definitions.LiveDefinitions state:
+        :param angr.analyses.reaching_definitions.constants op_type: OP_BEFORE, OP_AFTER
+        """
+
+        key = 'insn', insn_addr, op_type
+
         if self._observe_all or \
                 self._observation_points is not None and key in self._observation_points:
-            if isinstance(stmt, pyvex.IRStmt.IRStmt):
+            if isinstance(stmt, pyvex.stmt.IRStmt):
                 # it's an angr block
                 vex_block = block.vex
                 # OP_BEFORE: stmt has to be IMark
-                if ob_type == OP_BEFORE and type(stmt) is pyvex.IRStmt.IMark:
+                if op_type == OP_BEFORE and type(stmt) is pyvex.stmt.IMark:
                     self.observed_results[key] = state.copy()
                 # OP_AFTER: stmt has to be last stmt of block or next stmt has to be IMark
-                elif ob_type == OP_AFTER:
+                elif op_type == OP_AFTER:
                     idx = vex_block.statements.index(stmt)
                     if idx == len(vex_block.statements) - 1 or type(
                             vex_block.statements[idx + 1]) is pyvex.IRStmt.IMark:
@@ -485,7 +499,7 @@ def _run_on_node(self, node, state):
 
         self.node_observe(node.addr, state, OP_BEFORE)
 
-        state = state.copy()  # type: LiveDefinitions
+        state = state.copy()
         state = engine.process(state, block=block, fail_fast=self._fail_fast)
 
         # clear the tmp store


Test patch:
diff --git a/tests/test_reachingdefinitions.py b/tests/test_reachingdefinitions.py
--- a/tests/test_reachingdefinitions.py
+++ b/tests/test_reachingdefinitions.py
@@ -12,7 +12,8 @@
 import archinfo
 import angr
 from angr.analyses.reaching_definitions import LiveDefinitions
-from angr.analyses.reaching_definitions.constants import OP_BEFORE
+from angr.analyses.reaching_definitions.constants import OP_BEFORE, OP_AFTER
+import ailment
 
 LOGGER = logging.getLogger('test_reachingdefinitions')
 
@@ -22,6 +23,45 @@
 )
 
 
+class InsnAndNodeObserveTestingUtils():
+    @staticmethod
+    def assert_equals_for_live_definitions(live_definition_1, live_definition_2):
+        list(map(
+            lambda attr: {\
+                nose.tools.assert_equals(getattr(live_definition_1, attr),\
+                                         getattr(live_definition_2, attr))\
+            },
+            ["register_definitions", "stack_definitions", "memory_definitions", "tmp_definitions"]
+        ))
+
+    @staticmethod
+    def filter(observed_results, observation_points):
+        # Return only the observed results associated with the observation points,
+        # and do not fail if an observation point do not appear in the observed results.
+        return list(map(
+            lambda key: observed_results[key],
+            filter(
+                lambda key: key in observed_results,
+                observation_points
+            )
+        ))
+
+    @staticmethod
+    def setup(observation_points):
+        binary_path = os.path.join(TESTS_LOCATION, 'x86_64', 'all')
+        project = angr.Project(binary_path, load_options={'auto_load_libs': False})
+        cfg = project.analyses.CFGFast()
+
+        main_function = cfg.kb.functions['main']
+        reaching_definition = project.analyses.ReachingDefinitions(
+            main_function, init_func=True, observation_points=observation_points
+        )
+
+        state = LiveDefinitions(project.arch, project.loader)
+
+        return (project, main_function, reaching_definition, state)
+
+
 class ReachingDefinitionAnalysisTest(TestCase):
     def _run_reaching_definition_analysis(self, project, func, result_path):
         tmp_kb = angr.KnowledgeBase(project)
@@ -70,6 +110,104 @@ def _result_path(binary_name):
             self._run_reaching_definition_analysis(project, cfg.kb.functions['main'], result_path)
 
 
+    def test_node_observe(self):
+        # Create several different observation points
+        observation_points = [('node', 0x42, OP_AFTER), ('insn', 0x43, OP_AFTER)]
+
+        _, _, reaching_definition, state =\
+            InsnAndNodeObserveTestingUtils.setup(observation_points)
+
+        reaching_definition.node_observe(0x42, state, OP_AFTER)
+
+        results = InsnAndNodeObserveTestingUtils.filter(
+            reaching_definition.observed_results,
+            observation_points
+        )
+        expected_results = [state]
+
+        nose.tools.assert_equals(results, expected_results)
+
+
+    def test_insn_observe_an_ailment_statement(self):
+        # Create several different observation points
+        observation_points = [('node', 0x42, OP_AFTER), ('insn', 0x43, OP_AFTER)]
+
+        _, main_function, reaching_definition, state =\
+            InsnAndNodeObserveTestingUtils.setup(observation_points)
+
+        # Here, the statement content does not matter, neither if it is really in the block or else…
+        statement = ailment.statement.DirtyStatement(0, None)
+        block = main_function._addr_to_block_node[main_function.addr] # pylint: disable=W0212
+
+        reaching_definition.insn_observe(0x43, statement, block, state, OP_AFTER)
+
+        results = InsnAndNodeObserveTestingUtils.filter(
+            reaching_definition.observed_results,
+            observation_points
+        )
+        expected_results = [state]
+
+        nose.tools.assert_greater(len(results), 0)
+        list(map(
+            lambda x: InsnAndNodeObserveTestingUtils.assert_equals_for_live_definitions(x[0], x[1]),
+            zip(results, expected_results)
+        ))
+
+
+    def test_insn_observe_before_an_imark_pyvex_statement(self):
+        # Create several different observation points
+        observation_points = [('node', 0x42, OP_AFTER), ('insn', 0x43, OP_BEFORE)]
+
+        project, main_function, reaching_definition, state =\
+            InsnAndNodeObserveTestingUtils.setup(observation_points)
+
+        code_block = main_function._addr_to_block_node[main_function.addr] # pylint: disable=W0212
+        block = angr.block.Block(addr=0x43, byte_string=code_block.bytestr, project=project)
+        statement = block.vex.statements[0]
+
+        reaching_definition.insn_observe(0x43, statement, block, state, OP_BEFORE)
+
+        results = InsnAndNodeObserveTestingUtils.filter(
+            reaching_definition.observed_results,
+            observation_points
+        )
+        expected_results = [state]
+
+        nose.tools.assert_greater(len(results), 0)
+        list(map(
+            lambda x: InsnAndNodeObserveTestingUtils.assert_equals_for_live_definitions(x[0], x[1]),
+            zip(results, expected_results)
+        ))
+
+
+    def test_insn_observe_after_a_pyvex_statement(self):
+        # Create several different observation points
+        observation_points = [('node', 0x42, OP_AFTER), ('insn', 0x43, OP_AFTER)]
+
+        project, main_function, reaching_definition, state =\
+            InsnAndNodeObserveTestingUtils.setup(observation_points)
+
+        code_block = main_function._addr_to_block_node[main_function.addr] # pylint: disable=W0212
+        block = angr.block.Block(addr=0x43, byte_string=code_block.bytestr, project=project)
+        # When observing OP_AFTER an instruction, the statement has to be the last of a block
+        # (or preceding an IMark)
+        statement = block.vex.statements[-1]
+
+        reaching_definition.insn_observe(0x43, statement, block, state, OP_AFTER)
+
+        results = InsnAndNodeObserveTestingUtils.filter(
+            reaching_definition.observed_results,
+            observation_points
+        )
+        expected_results = [state]
+
+        nose.tools.assert_greater(len(results), 0)
+        list(map(
+            lambda x: InsnAndNodeObserveTestingUtils.assert_equals_for_live_definitions(x[0], x[1]),
+            zip(results, expected_results)
+        ))
+
+
 class LiveDefinitionsTest(TestCase):
     def test_initializing_live_definitions_for_ppc_without_rtoc_value_should_raise_an_error(self):
         arch = archinfo.arch_ppc64.ArchPPC64()