Repository: angr/angr

Issue:
Incorrect constant propogation in decompiler
the decompiler produces the following output on true (589a58ff455dbd092cb3ba3dd2c4c63e):

```
int sub_404dc0(long long a0, long long a1, long long a2, long long a3, BOT a4, long long a5)
{
    BOT tmp_11;  // tmp #11
    BOT tmp_3;  // tmp #3
    BOT tmp_19;  // tmp #19
    unsigned long long ir_0;  // r9
    BOT s_-10;  // [bp+0x10]
    BOT s_18;  // [bp-0x18]

    s_18 = a4;
    ir_0 = 0;
    do
    {
        if ((int)(long long)(long long)32 > 47)
        {
            if (s_-10 == 0)
            {
                break;
            }
        }
        else
        {
            tmp_11 = 32;
            tmp_3 = 32 + &stack_base-56;
            tmp_19 = s_18;
            if (s_18 == 0)
            {
                break;
            }
        }
        ir_0 = ir_0 + 1;
    } while(ir_0 + 1 != 10);
    sub_4048c0(a0, a1, a2, a3, &stack_base-152, r9<8>);
    return *(fs<8>:40) ^ *(fs<8>:40);
}
```

the `32 > 47` is the problematic part. what it actually is doing is setting r10 to the constant 32 and then using it as a loop counter, but angr thinks it is a constant 32 throughout the lifetime of the function.


Discussion:

Resolving patch:
diff --git a/angr/analyses/decompiler/ail_simplifier.py b/angr/analyses/decompiler/ail_simplifier.py
--- a/angr/analyses/decompiler/ail_simplifier.py
+++ b/angr/analyses/decompiler/ail_simplifier.py
@@ -113,7 +113,7 @@ def _fold_exprs(self):
 
         # propagator
         propagator = self._compute_propagation()
-        replacements = list(propagator._states.values())[0]._replacements
+        replacements = propagator.replacements
 
         # take replacements and rebuild the corresponding blocks
         replacements_by_block_addrs_and_idx = defaultdict(dict)
diff --git a/angr/analyses/propagator/propagator.py b/angr/analyses/propagator/propagator.py
--- a/angr/analyses/propagator/propagator.py
+++ b/angr/analyses/propagator/propagator.py
@@ -317,22 +317,27 @@ def get_variable(self, variable) -> Any:
             if len(labels) == 1:
                 # extract labels
                 offset, size, label = labels[0]
-                expr: Optional[ailment.Expr.Expression] = label['expr']
-                def_at = label['def_at']
-                if expr is not None:
-                    if expr.bits > size * self.arch.byte_width:
-                        # we are loading a chunk of the original expression
-                        expr = self._extract_ail_expression(
-                            offset * self.arch.byte_width,
-                            size * self.arch.byte_width,
-                            expr,
-                        )
-                    if expr.bits < variable.size * self.arch.byte_width:
-                        # we are loading more than the expression has - extend the size of the expression
-                        expr = self._extend_ail_expression(
-                            variable.size * self.arch.byte_width - expr.bits,
-                            expr,
-                        )
+                if value.size() // self.arch.byte_width == size:
+                    # the label covers the entire expression
+                    expr: Optional[ailment.Expr.Expression] = label['expr']
+                    def_at = label['def_at']
+                    if expr is not None:
+                        if expr.bits > size * self.arch.byte_width:
+                            # we are loading a chunk of the original expression
+                            expr = self._extract_ail_expression(
+                                offset * self.arch.byte_width,
+                                size * self.arch.byte_width,
+                                expr,
+                            )
+                        if expr.bits < variable.size * self.arch.byte_width:
+                            # we are loading more than the expression has - extend the size of the expression
+                            expr = self._extend_ail_expression(
+                                variable.size * self.arch.byte_width - expr.bits,
+                                expr,
+                            )
+                else:
+                    expr = None
+                    def_at = None
             else:
                 # Multiple definitions and expressions
                 expr = None
@@ -420,7 +425,7 @@ def add_replacement(self, codeloc, old, new):
         if self.is_top(new):
             # eliminate the past propagation of this expression
             if codeloc in self._replacements and old in self._replacements[codeloc]:
-                del self._replacements[codeloc][old]
+                self._replacements[codeloc][old] = self.top(1)  # placeholder
             return
 
         prop_count = 0
@@ -436,7 +441,7 @@ def add_replacement(self, codeloc, old, new):
             # eliminate the past propagation of this expression
             for codeloc_ in self._replacements:
                 if old in self._replacements[codeloc_]:
-                    del self._replacements[codeloc_][old]
+                    self._replacements[codeloc_][old] = self.top(1)
 
     def filter_replacements(self):
 
diff --git a/angr/state_plugins/sim_action.py b/angr/state_plugins/sim_action.py
--- a/angr/state_plugins/sim_action.py
+++ b/angr/state_plugins/sim_action.py
@@ -106,7 +106,7 @@ class SimActionExit(SimAction):
     DEFAULT = 'default'
 
     def __init__(self, state, target, condition=None, exit_type=None):
-        super(SimActionExit, self).__init__(state, "exit")
+        super().__init__(state, "exit")
         if exit_type is not None:
             self.exit_type = exit_type
         elif condition is None:
@@ -140,7 +140,7 @@ class SimActionConstraint(SimAction):
     """
 
     def __init__(self, state, constraint, condition=None):
-        super(SimActionConstraint, self).__init__(state, "constraint")
+        super().__init__(state, "constraint")
 
         self.constraint = self._make_object(constraint)
         self.condition = self._make_object(condition)
@@ -170,7 +170,7 @@ class SimActionOperation(SimAction):
     """
 
     def __init__(self, state, op, exprs, result):
-        super(SimActionOperation, self).__init__(state, 'operation')
+        super().__init__(state, 'operation')
 
         self.op = op
         self.exprs = exprs
@@ -183,7 +183,7 @@ def all_objects(self):
 
     @property
     def is_symbolic(self):
-        return any([ getattr(ex, "symbolic", False) for ex in self.exprs ])
+        return any(getattr(ex, "symbolic", False) for ex in self.exprs)
 
     def _copy_objects(self, c):
         c.op = self.op
@@ -206,7 +206,7 @@ class SimActionData(SimAction):
 
     def __init__(self, state, region_type, action, tmp=None, addr=None, size=None, data=None, condition=None,
                  fallback=None, fd=None):
-        super(SimActionData, self).__init__(state, region_type)
+        super().__init__(state, region_type)
         self.action = action
 
         self._reg_dep = _noneset if addr is None or action != SimActionData.READ or not isinstance(addr, int) else frozenset((addr,))
@@ -251,15 +251,15 @@ def all_objects(self):
 
     @property
     def is_symbolic(self):
-        return any([ getattr(a, "symbolic", False) for a in [ self.addr, self.size, self.data ] if a is not None ])
+        return any(getattr(a, "symbolic", False) for a in [ self.addr, self.size, self.data ] if a is not None)
 
     @property
     def tmp_deps(self):
-        return super(SimActionData, self).tmp_deps | self._tmp_dep
+        return super().tmp_deps | self._tmp_dep
 
     @property
     def reg_deps(self):
-        return super(SimActionData, self).reg_deps | self._reg_dep
+        return super().reg_deps | self._reg_dep
 
     def _desc(self):
         def _repr(o):


Test patch:
diff --git a/tests/test_decompiler.py b/tests/test_decompiler.py
--- a/tests/test_decompiler.py
+++ b/tests/test_decompiler.py
@@ -290,6 +290,27 @@ def test_decompiling_true_x86_64_0():
         assert False
 
 
+def test_decompiling_true_x86_64_1():
+    bin_path = os.path.join(test_location, "x86_64", "true_ubuntu_2004")
+    p = angr.Project(bin_path, auto_load_libs=False, load_debug_info=True)
+
+    cfg = p.analyses.CFG(normalize=True, data_references=True)
+
+    # disable eager returns simplifier
+    all_optimization_passes = angr.analyses.decompiler.optimization_passes.get_default_optimization_passes("AMD64",
+                                                                                                           "linux")
+    all_optimization_passes = [p for p in all_optimization_passes
+                               if p is not angr.analyses.decompiler.optimization_passes.EagerReturnsSimplifier]
+
+    f = cfg.functions[0x404dc0]
+    dec = p.analyses.Decompiler(f, cfg=cfg.model, optimization_passes=all_optimization_passes)
+    print(dec.codegen.text)
+    code: str = dec.codegen.text
+
+    # constant propagation was failing. see https://github.com/angr/angr/issues/2659
+    assert code.count("32") == 1
+
+
 def test_decompiling_true_a_x86_64():
 
     bin_path = os.path.join(test_location, "x86_64", "true_a")