Repository: angr/angr

Issue:
Incorrect loop ending under Phonenix
### Description


[cache.zip](https://github.com/angr/angr/files/12342857/cache.zip)

When using ang
r-management to decompile some binary, I found a structuring bug under phoenix.
The function is `sub_4030d0`, disabling all decompilation options and using `Phoenix` structuring I got:

![image](https://github.com/angr/angr/assets/19388242/00ad595e-8194-4fc9-bb84-1a7ad308fcaa)

However, when using `Dream` I got:

![image](https://github.com/angr/angr/assets/19388242/fe05bb22-c7c2-4a7c-8251-efe15b519cc0)

IDA shows that Dream version is correct:

![image](https://github.com/angr/angr/assets/19388242/a52471fe-8ace-46b6-9f87-598bc5aa1cef)

Edited: preivously this only happens when using angr-management.
I can now use plain angr to reproduce this.

### Steps to reproduce the bug

```python
import angr
from angr.analyses import (
    CFGFast,
    Decompiler,
)
p = angr.Project("/path/to/binary", auto_load_libs=False)
cfg = p.analyses[CFGFast].prep()(data_references=True, normalize=True)
opt = [o for o in angr.analyses.decompiler.decompilation_options.options if o.param == "structurer_cls"][0]
dec = p.analyses.Decompiler(p.kb.functions[0x4030d0], options=[(opt, "Phoenix")])
print(dec.codegen.text)
```

### Environment

All angr suite latest master branch (9.2.64.dev0):

![image](https://github.com/angr/angr/assets/19388242/482c1244-314b-43e5-8311-691820c483ce)


### Additional context

_No response_


Discussion:
I can confirm now it happens within `RegionSimplifier`.
I can dig this deeper when I have more time.

Resolving patch:
diff --git a/angr/analyses/decompiler/sequence_walker.py b/angr/analyses/decompiler/sequence_walker.py
--- a/angr/analyses/decompiler/sequence_walker.py
+++ b/angr/analyses/decompiler/sequence_walker.py
@@ -162,7 +162,7 @@ def _handle_Loop(self, node: LoopNode, **kwargs):
             self._handle(node.iterator)
         if node.condition is not None:
             self._handle(node.condition, parent=node, label="condition")
-        seq_node = self._handle(node.sequence_node, **kwargs)
+        seq_node = self._handle(node.sequence_node, parent=node, label="body", index=0)
         if seq_node is not None:
             return LoopNode(
                 node.sort,
diff --git a/angr/analyses/decompiler/utils.py b/angr/analyses/decompiler/utils.py
--- a/angr/analyses/decompiler/utils.py
+++ b/angr/analyses/decompiler/utils.py
@@ -226,6 +226,15 @@ def insert_node(parent, insert_location: str, node, node_idx: Optional[Union[int
             raise TypeError(
                 f'Unsupported label value "{label}". Must be one of the following: switch_expr, case, ' f"default."
             )
+    elif isinstance(parent, LoopNode):
+        if label == "condition":
+            raise ValueError("Cannot insert nodes into a condition expression.")
+        if label == "body":
+            if not isinstance(parent.sequence_node, SequenceNode):
+                parent.sequence_node = SequenceNode(parent.sequence_node.addr, nodes=[parent.sequence_node])
+            insert_node(parent.sequence_node, insert_location, node, node_idx)
+        else:
+            raise NotImplementedError()
     else:
         raise NotImplementedError()
 


Test patch:
diff --git a/tests/test_decompiler.py b/tests/test_decompiler.py
--- a/tests/test_decompiler.py
+++ b/tests/test_decompiler.py
@@ -2495,6 +2495,42 @@ def test_fauxware_read_packet_call_folding_into_store_stmt(self, decompiler_opti
         text = d.codegen.text
         assert re.search(r"\[read_packet\([^)]*\)\] = 0;", text) is not None
 
+    @structuring_algo("phoenix")
+    def test_ifelsesimplifier_insert_node_into_while_body(self, decompiler_options=None):
+        # https://github.com/angr/angr/issues/4082
+
+        bin_path = os.path.join(test_location, "x86_64", "decompiler", "angr_4082_cache")
+        proj = angr.Project(bin_path, auto_load_libs=False)
+        cfg = proj.analyses.CFGFast(normalize=True, data_references=True)
+
+        f = proj.kb.functions[0x4030D0]
+        proj.analyses.CompleteCallingConventions(cfg=cfg, recover_variables=True)
+        d = proj.analyses[Decompiler](f, cfg=cfg.model, options=decompiler_options)
+
+        self._print_decompilation_result(d)
+        text = d.codegen.text
+        text = text.replace(" ", "").replace("\n", "")
+        # Incorrect:
+        #     while (true)
+        #     {
+        #         if (v9 >= v10)
+        #             return v9;
+        #     }
+        # Expected:
+        #     while (true)
+        #     {
+        #         if (v9 >= v10)
+        #             return v9;
+        #         v8 = 0;
+        #         if (read(0x29, &v8, 0x4) != 4)
+        #         {
+        #             printf("failed to get number\n");
+        #             exit(0x1); /* do not return */
+        #         }
+        #
+        # we should not see a right curly brace after return v9;
+        assert re.search(r"while\(true\){if\(v\d+>=v\d+\)returnv\d+;v\d+=0;", text) is not None
+
 
 if __name__ == "__main__":
     unittest.main()