Repository: angr/angr

Issue:
VFG raises an error when handling multiple successor states
**Describe the bug.**
Angr's VFG recovery assumes every state has only a single successor state, which is not always the case.

**Environment Information.**
```
angr environment report
=============================
Date: 2022-03-22 15:54:12.282340
Running in virtual environment at ~/.virtualenvs/angr
Platform: linux-x86_64
Python version: 3.10.2 (main, Jan 15 2022, 19:56:27) [GCC 11.1.0]
######## angr #########
Python found it in ~/angr-dev/angr/angr
Pip version angr 9.1.gitrolling
Git info:
        Current commit a588dbb21f375670439e1201a206d8318f56b815 from branch master
        Checked out from remote origin: https://github.com/angr/angr
######## ailment #########
Python found it in ~/angr-dev/ailment/ailment
Pip version ailment 9.1.gitrolling
Git info:
        Current commit f1ee52ef866be64b0d9c773c678cb90f922d172a from branch master
        Checked out from remote origin: https://github.com/angr/ailment
######## cle #########
Python found it in ~/angr-dev/cle/cle
Pip version cle 9.1.gitrolling
Git info:
        Current commit c8eb415ae0c5ab84dff7ab026a5222d98c3110f3 from branch master
        Checked out from remote origin: https://github.com/angr/cle
######## pyvex #########
Python found it in ~/angr-dev/pyvex/pyvex
Pip version pyvex 9.1.gitrolling
Git info:
        Current commit 5e8cc57d093648aefad37e56e79a8df4741e5a81 from branch master
        Checked out from remote origin: https://github.com/angr/pyvex
######## claripy #########
Python found it in ~/angr-dev/claripy/claripy
Pip version claripy 9.1.gitrolling
Git info:
        Current commit 4e7e8acc523eacd97251e5b424f4655735bb791a from branch master
        Checked out from remote origin: https://github.com/angr/claripy
######## archinfo #########
Python found it in ~/angr-dev/archinfo/archinfo
Pip version archinfo 9.1.gitrolling
Git info:
        Current commit 0dd6fefce2db30d7f01d8a27be235d76ecb9c409 from branch master
        Checked out from remote origin: https://github.com/angr/archinfo
######## z3 #########
Python found it in ~/.virtualenvs/angr/lib/python3.10/site-packages/z3
Pip version z3-solver 4.8.14.0
Couldn't find git info
######## unicorn #########
Python found it in ~/.virtualenvs/angr/lib/python3.10/site-packages/unicorn
Pip version unicorn 1.0.2rc4
Couldn't find git info
######### Native Module Info ##########
angr: <CDLL '~/angr-dev/angr/angr/state_plugins/../lib/angr_native.so', handle 55d38cdb7210 at 0x7f5833fa6e00>
unicorn: <CDLL '~/.virtualenvs/angr/lib/python3.10/site-packages/unicorn/lib/libunicorn.so', handle 55d38c832180 at 0x7f5839056f20>
pyvex: <cffi.api._make_ffi_library.<locals>.FFILibrary object at 0x7f5839b78760>
z3: NOT FOUND
```

**To Reproduce.**
Run the following script. The binary that was used is the same binary that is used in the `test_vfg_resolving_indirect_calls` test in `test_vfg.py`.
```python
import os

import angr
from angr.analyses.cfg.indirect_jump_resolvers.resolver import IndirectJumpResolver

class FuncChainResolver(IndirectJumpResolver):
    def __init__(self, project):
        super().__init__(project, timeless=True)

    def filter(self, cfg, addr, func_addr, block, jumpkind):
        return jumpkind == "Ijk_Call"

    def resolve(self, cfg, addr, func_addr, block, jumpkind):
        vfg = self.project.analyses.VFG(
            cfg,
            start=addr,
            context_sensitivity_level=1,
            interfunction_level=1,
            record_function_final_states=True,
        )

        indirect_call_targets = []
        for vfg_node in vfg.get_all_nodes(addr):
            for successor_state in vfg_node.final_states:
                if successor_state.history.jumpkind == "Ijk_Call":
                    indirect_call_targets.append(successor_state.addr)

        print(indirect_call_targets)

        return (True, indirect_call_targets)

def main():
    p = angr.Project(
        os.path.join("aarch64", "func-chain-aarch64"),
        auto_load_libs=False,
    )

    p.analyses.CFGEmulated(
        indirect_jump_resolvers = [r(p) for r in [
            angr.analyses.cfg.indirect_jump_resolvers.ConstantResolver,
            FuncChainResolver,
        ]],
    )

if __name__ == "__main__":
    main()
```
Running this gives the following error, when the file is saved as `~/code.py`:
```
Traceback (most recent call last):
  File "~/code.py", line 65, in <module>
    main()
  File "~/code.py", line 57, in main
    cfg = p.analyses.CFGEmulated(
  File "~/angr-dev/angr/angr/analyses/analysis.py", line 138, in __call__
    r = w(*args, **kwargs)
  File "~/angr-dev/angr/angr/analyses/analysis.py", line 122, in wrapper
    oself.__init__(*args, **kwargs)
  File "~/angr-dev/angr/angr/analyses/cfg/cfg_emulated.py", line 313, in __init__
    self._analyze()
  File "~/angr-dev/angr/angr/analyses/forward_analysis/forward_analysis.py", line 236, in _analyze
    self._analysis_core_baremetal()
  File "~/angr-dev/angr/angr/analyses/forward_analysis/forward_analysis.py", line 383, in _analysis_core_baremetal
    self._process_job_and_get_successors(job_info)
  File "~/angr-dev/angr/angr/analyses/forward_analysis/forward_analysis.py", line 401, in _process_job_and_get_successors
    successors = self._get_successors(job)
  File "~/angr-dev/angr/angr/analyses/cfg/cfg_emulated.py", line 1300, in _get_successors
    resolved, resolved_targets, ij = self._indirect_jump_encountered(addr, cfg_node, irsb, func_addr,
  File "~/angr-dev/angr/angr/analyses/cfg/cfg_base.py", line 2479, in _indirect_jump_encountered
    resolved, resolved_targets = self._resolve_indirect_jump_timelessly(addr, irsb, func_addr, jumpkind)
  File "~/angr-dev/angr/angr/analyses/cfg/cfg_base.py", line 2419, in _resolve_indirect_jump_timelessly
    r, resolved_targets = res.resolve(self, addr, func_addr, block, jumpkind)
  File "~/code.py", line 33, in resolve
    vfg = self.project.analyses.VFG(
  File "~/angr-dev/angr/angr/analyses/analysis.py", line 138, in __call__
    r = w(*args, **kwargs)
  File "~/angr-dev/angr/angr/analyses/analysis.py", line 122, in wrapper
    oself.__init__(*args, **kwargs)
  File "~/angr-dev/angr/angr/analyses/vfg.py", line 360, in __init__
    self._analyze()
  File "~/angr-dev/angr/angr/analyses/forward_analysis/forward_analysis.py", line 236, in _analyze
    self._analysis_core_baremetal()
  File "~/angr-dev/angr/angr/analyses/forward_analysis/forward_analysis.py", line 383, in _analysis_core_baremetal
    self._process_job_and_get_successors(job_info)
  File "~/angr-dev/angr/angr/analyses/forward_analysis/forward_analysis.py", line 401, in _process_job_and_get_successors
    successors = self._get_successors(job)
  File "~/angr-dev/angr/angr/analyses/vfg.py", line 721, in _get_successors
    call_targets = [i.solver.eval_one(i.ip) for i in all_successors if self._is_call_jumpkind(i.history.jumpkind)]
  File "~/angr-dev/angr/angr/analyses/vfg.py", line 721, in <listcomp>
    call_targets = [i.solver.eval_one(i.ip) for i in all_successors if self._is_call_jumpkind(i.history.jumpkind)]
  File "~/angr-dev/angr/angr/state_plugins/solver.py", line 776, in eval_one
    return self.eval_exact(e, 1, **{k: v for (k, v) in kwargs.items() if k != 'default'})[0]
  File "~/angr-dev/angr/angr/state_plugins/solver.py", line 829, in eval_exact
    raise SimValueError("Concretized %d values (must be exactly %d) in eval_exact" % (len(r), n))
angr.errors.SimValueError: Concretized 2 values (must be exactly 1) in eval_exact
```

**Additional context.**
I was writing an indirect jump resolver that attempts to mimic the `test_vfg_resolving_indirect_calls` test to resolve indirect calls.


Discussion:

Resolving patch:
diff --git a/angr/analyses/vfg.py b/angr/analyses/vfg.py
--- a/angr/analyses/vfg.py
+++ b/angr/analyses/vfg.py
@@ -1,4 +1,4 @@
-from typing import Generator
+from typing import List, Generator, TYPE_CHECKING
 import logging
 from collections import defaultdict
 
@@ -19,6 +19,10 @@
 from ..procedures import SIM_PROCEDURES
 from ..state_plugins.callstack import CallStack
 
+if TYPE_CHECKING:
+    from angr.sim_state import SimState
+
+
 l = logging.getLogger(name=__name__)
 
 
@@ -66,7 +70,7 @@ def callstack_repr(self, kb=None):
         return "//".join(s)
 
 
-class PendingJob(object):
+class PendingJob:
 
     __slots__ = ('block_id', 'state', 'call_stack', 'src_block_id', 'src_stmt_idx', 'src_ins_addr', )
 
@@ -79,7 +83,7 @@ def __init__(self, block_id, state, call_stack, src_block_id, src_stmt_idx, src_
         self.src_ins_addr = src_ins_addr
 
 
-class AnalysisTask(object):
+class AnalysisTask:
     """
     An analysis task describes a task that should be done before popping this task out of the task stack and discard it.
     """
@@ -177,7 +181,7 @@ def merge_jobs(self):
         return job
 
 
-class VFGNode(object):
+class VFGNode:
     """
     A descriptor of nodes in a Value-Flow Graph
     """
@@ -683,7 +687,7 @@ def _get_successors(self, job: VFGJob):
 
         # Obtain successors
         if addr not in self._avoid_runs:
-            all_successors = job.sim_successors.flat_successors + job.sim_successors.unconstrained_successors
+            all_successors: List['SimState'] = job.sim_successors.flat_successors + job.sim_successors.unconstrained_successors
         else:
             all_successors = []
 
@@ -718,7 +722,17 @@ def _get_successors(self, job: VFGJob):
         # is artificial) into the CFG. The exits will be Ijk_Call and
         # Ijk_FakeRet, and Ijk_Call always goes first
         job.is_call_jump = any([self._is_call_jumpkind(i.history.jumpkind) for i in all_successors])
-        call_targets = [i.solver.eval_one(i.ip) for i in all_successors if self._is_call_jumpkind(i.history.jumpkind)]
+        call_targets = [ ]
+        for succ in all_successors:
+            if self._is_call_jumpkind(succ.history.jumpkind):
+                try:
+                    call_targets.append(succ.solver.eval_one(succ._ip))
+                    # finding one is enough!
+                    break
+                except SimValueError:
+                    # this call has multiple possible targets. ignore it
+                    pass
+
         job.call_target = None if not call_targets else call_targets[0]
 
         job.is_return_jump = len(all_successors) and all_successors[0].history.jumpkind == 'Ijk_Ret'


Test patch: