Repository: angr/angr

Issue:
Expression incorrectly moved to LHS and not eliminated
### Description

```c
extern char hashes;

int authenticate(void* a0, void* a1)
{
    unsigned int v0;  // [bp-0x2c]
    char v1;  // [bp-0x28]

    if (!memcmp(a0, "admin\n", 7))
    {
        MD5(&v1, a1, 100);
        v0 = 0;
        while (true)
        {
            if (v0 <= 15)
            {
                xor_(a1, &(&hashes)[16 * v0], 16);
                if (memcmp(&(&hashes)[16 * v0], &v1, 16))
                {
                    v0 += 1;
                }
                else
                {
                    puts("Authenticating...");
                    sleep(1);
                    puts("Authenticated!");
                    return 1;
                }
            }
            else
            {
                (!memcmp(a1, "Welcome to the admin console, trusted user!", 16) ? 0 : 1) = (!memcmp(a1, "Welcome to the admin console, trusted user!", 16) ? 0 : 1);
                return (!memcmp(a1, "Welcome to the admin console, trusted user!", 16) ? 0 : 1);
            }
        }
    }
    else
    {
        return 0;
    }
}
```

Issue disappears when disabling "Duplicate return-only blocks (high)" optimization pass

### Steps to reproduce the bug

[binary.zip](https://github.com/angr/angr/files/14951907/binary.zip)



### Environment

```
angr environment report
=============================
Date: 2024-04-11 15:19:50.244399
Running in virtual environment at /home/matt/.virtualenvs/angr-cp312
/home/matt/work/angr/angr-dev/angr/angr/misc/bug_report.py:88: DeprecationWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html
  import pkg_resources  # pylint:disable=import-outside-toplevel
Platform: linux-x86_64
Python version: 3.12.0 (main, Oct 21 2023, 17:42:12) [GCC 11.4.0]
######## angr #########
Python found it in /home/matt/work/angr/angr-dev/angr/angr/__init__.py
Pip version angr 9.2.97.dev0
Git info:
	Current commit b0396c9fd3d72c0b1ab47fd0956654cef1366a60 from branch master
	Checked out from remote upstream: https://github.com/angr/angr.git
######## ailment #########
Python found it in /home/matt/work/angr/angr-dev/ailment/ailment/__init__.py
Pip version ailment 9.2.97.dev0
Git info:
	Current commit 169a0321d99a0d7ec76936c00fe605b2e436a1bd from branch master
	Checked out from remote origin: https://github.com/angr/ailment.git
######## cle #########
Python found it in /home/matt/work/angr/angr-dev/cle/cle/__init__.py
Pip version cle 9.2.97.dev0
Git info:
	Current commit 8f6ea207d1eb256bcb57664d4cfd23714c5a1a05 from branch fix/update-pyxbe2
Could not resolve tracking branch or remote info!
######## pyvex #########
Python found it in /home/matt/work/angr/angr-dev/pyvex/pyvex/__init__.py
Pip version pyvex 9.2.97.dev0
Git info:
	Current commit 47e4329014de718b08e9fc6fd4b71cf829f30399 from branch master
	Checked out from remote origin: https://github.com/angr/pyvex
######## claripy #########
Python found it in /home/matt/work/angr/angr-dev/claripy/claripy/__init__.py
Pip version claripy 9.2.97.dev0
Git info:
	Current commit fbb9b58aee92d0d49c7171838648fdc8597a2849 from branch master
	Checked out from remote origin: https://github.com/angr/claripy
######## archinfo #########
Python found it in /home/matt/work/angr/angr-dev/archinfo/archinfo/__init__.py
Pip version archinfo 9.2.97.dev0
Git info:
	Current commit ab22590c64cb52478e3c2681aa1e5227f500d23c from branch master
	Checked out from remote origin: https://github.com/angr/archinfo.git
######## z3 #########
Python found it in /home/matt/.virtualenvs/angr-cp312/lib/python3.12/site-packages/z3/__init__.py
Pip version z3-solver 4.10.2.0
Couldn't find git info
######## unicorn #########
Python found it in /home/matt/.virtualenvs/angr-cp312/lib/python3.12/site-packages/unicorn/__init__.py
Pip version unicorn 1.0.2
Couldn't find git info
######### Native Module Info ##########
angr: <CDLL '/home/matt/work/angr/angr-dev/angr/angr/state_plugins/../lib/angr_native.so', handle 2c20d60 at 0x7f6c979f0b90>
unicorn: <CDLL '/home/matt/.virtualenvs/angr-cp312/lib/python3.12/site-packages/unicorn/lib/libunicorn.so', handle 25fd040 at 0x7f6c9d4b3620>
pyvex: <cffi.api._make_ffi_library.<locals>.FFILibrary object at 0x7f6c9b6433b0>
z3: <CDLL '/home/matt/.virtualenvs/angr-cp312/lib/python3.12/site-packages/z3/lib/libz3.so', handle 2222600 at 0x7f6c9d648c20>
```

### Additional context

_No response_


Discussion:
This is related to the PR that made this testcase:
https://github.com/angr/angr/blob/b0396c9fd3d72c0b1ab47fd0956654cef1366a60/tests/analyses/decompiler/test_decompiler.py#L2683

I've seen this problem before, which has to do with the over-propagation of specifically `Ternary` type statements. Problem is located somewhere in Clinic in the BlockSimplifier, probably around here: https://github.com/angr/angr/blob/b0396c9fd3d72c0b1ab47fd0956654cef1366a60/angr/analyses/decompiler/block_simplifier.py#L192

Last time it had something to do with multiple variables being located at the same stmt index, making propagation get weird results. 

Resolving patch:
diff --git a/angr/analyses/decompiler/region_simplifiers/expr_folding.py b/angr/analyses/decompiler/region_simplifiers/expr_folding.py
--- a/angr/analyses/decompiler/region_simplifiers/expr_folding.py
+++ b/angr/analyses/decompiler/region_simplifiers/expr_folding.py
@@ -4,6 +4,7 @@
 
 import ailment
 from ailment import Expression, Block, AILBlockWalker
+from ailment.expression import ITE
 from ailment.statement import Statement, Assignment, Call
 
 from ..sequence_walker import SequenceWalker
@@ -385,11 +386,11 @@ def _handle_MultiStatementExpression(
         return None
 
     def _handle_Assignment(self, stmt_idx: int, stmt: Assignment, block: Optional[Block]):
-        # override the base handler and make sure we do not replace .dst with a Call expression
+        # override the base handler and make sure we do not replace .dst with a Call expression or an ITE expression
         changed = False
 
         dst = self._handle_expr(0, stmt.dst, stmt_idx, stmt, block)
-        if dst is not None and dst is not stmt.dst and not isinstance(dst, Call):
+        if dst is not None and dst is not stmt.dst and not isinstance(dst, (Call, ITE)):
             changed = True
         else:
             dst = stmt.dst
@@ -446,7 +447,8 @@ def _handle_Block(self, node: ailment.Block, **kwargs):
         for stmt in node.statements:
             if isinstance(stmt, ailment.Stmt.Assignment):
                 if isinstance(stmt.dst, ailment.Expr.Register) and stmt.dst.variable is not None:
-                    if stmt.dst.variable in self._assignments:
+                    unified_var = self._u(stmt.dst.variable)
+                    if unified_var in self._assignments:
                         # remove this statement
                         continue
             if (


Test patch:
diff --git a/tests/analyses/decompiler/test_decompiler.py b/tests/analyses/decompiler/test_decompiler.py
--- a/tests/analyses/decompiler/test_decompiler.py
+++ b/tests/analyses/decompiler/test_decompiler.py
@@ -3511,6 +3511,20 @@ def test_ite_region_converter_missing_break_statement(self, decompiler_options=N
 
         assert d.codegen.text.count("break;") == 2
 
+    @structuring_algo("phoenix")
+    def test_ternary_expression_over_propagation(self, decompiler_options=None):
+        # https://github.com/angr/angr/issues/4573
+        bin_path = os.path.join(test_location, "x86_64", "ite_region_converter_missing_breaks")
+        proj = angr.Project(bin_path, auto_load_libs=False)
+        cfg = proj.analyses.CFGFast(normalize=True, data_references=True)
+        f = proj.kb.functions["authenticate"]
+        d = proj.analyses[Decompiler].prep()(f, cfg=cfg.model, options=decompiler_options)
+        self._print_decompilation_result(d)
+
+        # the ITE expression should not be propagated into the dst of an assignment
+        # the original assignment (rax = memcmp(xxx)? 0, 1) should be removed as well
+        assert d.codegen.text.count('"Welcome to the admin console, trusted user!"') == 1
+
 
 if __name__ == "__main__":
     unittest.main()