Repository: angr/angr

Issue:
angr misses the condition containing array
### Description

decompiling the function "base_len", whose source code can be found [here](https://github.com/coreutils/gnulib/blob/08ba9aaebff69a02cbb794c6213314fd09dd5ec5/lib/basename-lgpl.c#L52), and disassembly code as followed:
![image](https://user-images.githubusercontent.com/54459497/210127243-0794c6d9-6190-465a-bea7-81e475e07e62.png)
angr misses the decompilation of "cmp byte ptr [rbx+rax-0x1],0x2f", only has "v1>=2" in the condition:
```
int base_len(char *a0)
{
    unsigned long long v1;  // rcx
    unsigned long long v2;  // rax

    v1 = strlen(a0);
    while (true)
    {
        v2 = v1;
        if (v1 >= 2)
        {
            v1 = v2 - 1;
            return v2;
        }
        return v2;
    }
}
```
as a comparison, ghidra decompiles as:
```
ulong base_len(char *param_1)

{
  ulong local_18;

  for (local_18 = strlen(param_1); (1 < local_18 && (param_1[local_18 - 1] == '/'));
      local_18 = local_18 - 1) {
  }
  return local_18;
}
```


### Steps to reproduce the bug

binary is attached:
[tac.zip](https://github.com/angr/angr/files/10326898/tac.zip)


### Environment

latest angr

### Additional context

_No response_


Discussion:
Thanks for reporting! This looks like a bug. I'll get to it later.

Resolving patch:
diff --git a/angr/analyses/decompiler/empty_node_remover.py b/angr/analyses/decompiler/empty_node_remover.py
--- a/angr/analyses/decompiler/empty_node_remover.py
+++ b/angr/analyses/decompiler/empty_node_remover.py
@@ -1,10 +1,10 @@
-# pylint:disable=unused-argument
+# pylint:disable=unused-argument,no-self-use
 import claripy
 import ailment
 
 from .sequence_walker import SequenceWalker
 from .structuring.structurer_nodes import MultiNode, SequenceNode, CodeNode, ConditionNode, SwitchCaseNode,\
-    ConditionalBreakNode, BreakNode, LoopNode, CascadingConditionNode
+    ConditionalBreakNode, BreakNode, LoopNode, CascadingConditionNode, ContinueNode
 from .condition_processor import ConditionProcessor
 
 
@@ -32,6 +32,7 @@ def __init__(self, node, claripy_ast_conditions: bool=True):
             CascadingConditionNode: self._handle_CascadingCondition,
             SwitchCaseNode: self._handle_SwitchCase,
             LoopNode: self._handle_Loop,
+            ContinueNode: self._handle_Continue,
 
             MultiNode: self._handle_MultiNode,
             BreakNode: self._handle_Default,
@@ -161,6 +162,9 @@ def _handle_Loop(self, node: LoopNode, **kwargs):
         result.sequence_node = new_seq
         return result
 
+    def _handle_Continue(self, node: ContinueNode, **kwargs):
+        return node
+
     def _handle_SwitchCase(self, node, **kwargs):
 
         new_cases = { }
diff --git a/angr/analyses/decompiler/structuring/phoenix.py b/angr/analyses/decompiler/structuring/phoenix.py
--- a/angr/analyses/decompiler/structuring/phoenix.py
+++ b/angr/analyses/decompiler/structuring/phoenix.py
@@ -134,8 +134,8 @@ def _match_cyclic_schemas(self, node, head, graph, full_graph) -> bool:
                 # traverse this node and rewrite all conditional jumps that go outside the loop to breaks
                 self._rewrite_conditional_jumps_to_breaks(loop_node.sequence_node,
                                                           [ succ.addr for succ in self._region.successors ])
-                # traverse this node and rewrite all jumps that go to the beginning of the loop to continue
-                self._rewrite_jumps_to_continues(loop_node.sequence_node)
+            # traverse this node and rewrite all jumps that go to the beginning of the loop to continue
+            self._rewrite_jumps_to_continues(loop_node.sequence_node)
             return True
 
         matched, loop_node = self._match_cyclic_dowhile(node, head, graph, full_graph)
@@ -144,11 +144,18 @@ def _match_cyclic_schemas(self, node, head, graph, full_graph) -> bool:
                 # traverse this node and rewrite all conditional jumps that go outside the loop to breaks
                 self._rewrite_conditional_jumps_to_breaks(loop_node.sequence_node,
                                                           [ succ.addr for succ in self._region.successors ])
-                # traverse this node and rewrite all jumps that go to the beginning of the loop to continue
-                self._rewrite_jumps_to_continues(loop_node.sequence_node)
+            # traverse this node and rewrite all jumps that go to the beginning of the loop to continue
+            self._rewrite_jumps_to_continues(loop_node.sequence_node)
             return True
 
-        matched = self._match_cyclic_natural_loop(node, head, graph, full_graph)
+        matched, loop_node = self._match_cyclic_natural_loop(node, head, graph, full_graph)
+        if matched:
+            if len(self._region.successors) == 1:
+                # traverse this node and rewrite all conditional jumps that go outside the loop to breaks
+                self._rewrite_conditional_jumps_to_breaks(loop_node.sequence_node,
+                                                          [ succ.addr for succ in self._region.successors ])
+            # traverse this node and rewrite all jumps that go to the beginning of the loop to continue
+            self._rewrite_jumps_to_continues(loop_node.sequence_node)
         return matched
 
     def _match_cyclic_while(self, node, head, graph, full_graph) -> Tuple[bool,Optional[LoopNode]]:
@@ -308,10 +315,10 @@ def _match_cyclic_dowhile(self, node, head, graph, full_graph) -> Tuple[bool,Opt
                     return True, loop_node
         return False, None
 
-    def _match_cyclic_natural_loop(self, node, head, graph, full_graph) -> bool:
+    def _match_cyclic_natural_loop(self, node, head, graph, full_graph) -> Tuple[bool,Optional[LoopNode]]:
 
         if not (node is head or graph.in_degree[node] == 2):
-            return False
+            return False, None
 
         # check if there is a cycle that starts with head and ends with head
         next_node = node
@@ -320,13 +327,13 @@ def _match_cyclic_natural_loop(self, node, head, graph, full_graph) -> bool:
         while True:
             succs = list(full_graph.successors(next_node))
             if len(succs) != 1:
-                return False
+                return False, None
             next_node = succs[0]
 
             if next_node is node:
                 break
             if next_node is not node and next_node in seen_nodes:
-                return False
+                return False, None
 
             seen_nodes.add(next_node)
             seq_node.nodes.append(next_node)
@@ -345,7 +352,7 @@ def _match_cyclic_natural_loop(self, node, head, graph, full_graph) -> bool:
                 full_graph.remove_node(node_)
         self.replace_nodes(full_graph, node, loop_node, self_loop=False)
 
-        return True
+        return True, loop_node
 
     def _refine_cyclic(self) -> bool:
         return self._refine_cyclic_core(self._region.head)
diff --git a/angr/analyses/decompiler/structuring/structurer_base.py b/angr/analyses/decompiler/structuring/structurer_base.py
--- a/angr/analyses/decompiler/structuring/structurer_base.py
+++ b/angr/analyses/decompiler/structuring/structurer_base.py
@@ -335,8 +335,9 @@ def _dummy(node, parent=None, index=None, label=None, **kwargs):  # pylint:disab
         walker = SequenceWalker(handlers=handlers)
         walker.walk(loop_node)
 
-    @staticmethod
-    def _rewrite_jumps_to_continues(loop_seq):
+    def _rewrite_jumps_to_continues(self, loop_seq):
+
+        last_stmts = self.cond_proc.get_last_statements(loop_seq)
 
         def _rewrite_jump_to_continue(node, parent=None, index=None, label=None, **kwargs):  # pylint:disable=unused-argument
             if not node.statements:
@@ -346,11 +347,28 @@ def _rewrite_jump_to_continue(node, parent=None, index=None, label=None, **kwarg
                 targets = extract_jump_targets(stmt)
                 if any(target == loop_seq.addr for target in targets):
                     # This node has an exit to the beginning of the loop
+                    # but, we do not add continue statement at the very end of a loop
+                    if not stmt in last_stmts:
+                        # create a continue node
+                        continue_node = ContinueNode(stmt.ins_addr, loop_seq.addr)
+                        # insert this node to the parent
+                        insert_node(parent, "after", continue_node, index, label=label)  # insert after
+                    # remove this statement
+                    node.statements = node.statements[:-1]
+            elif isinstance(stmt, ailment.Stmt.ConditionalJump):
+                cond = None
+                if isinstance(stmt.true_target, ailment.Expr.Const) and stmt.true_target.value == loop_seq.addr:
+                    cond = self.cond_proc.claripy_ast_from_ail_condition(stmt.condition)
+                elif isinstance(stmt.false_target, ailment.Expr.Const) and stmt.false_target.value == loop_seq.addr:
+                    cond = claripy.Not(self.cond_proc.claripy_ast_from_ail_condition(stmt.condition))
+                if cond is not None:
                     # create a continue node
                     continue_node = ContinueNode(stmt.ins_addr, loop_seq.addr)
+                    # create a condition node
+                    cond_node = ConditionNode(stmt.ins_addr, None, cond, continue_node)
                     # insert this node to the parent
-                    insert_node(parent, "after", continue_node, index, label=label)  # insert after
-                    # remove this statement
+                    insert_node(parent, "after", cond_node, index, label=label)
+                    # remove the current conditional jump statement
                     node.statements = node.statements[:-1]
 
         def _dummy(node, parent=None, index=None, label=None, **kwargs):  # pylint:disable=unused-argument
@@ -502,7 +520,7 @@ def _handle_SequenceNode(seq_node, parent=None, index=0, label=None):  # pylint:
                         # amazing!
                         merged_cond = ConditionProcessor.simplify_condition(
                             claripy.And(self.cond_proc.claripy_ast_from_ail_condition(cond_node.condition),
-                                        cond_node_inner.condition))
+                                        self.cond_proc.claripy_ast_from_ail_condition(cond_node_inner.condition)))
                         new_node = ConditionNode(cond_node.addr,
                                                  None,
                                                  merged_cond,
@@ -519,7 +537,7 @@ def _handle_SequenceNode(seq_node, parent=None, index=0, label=None):  # pylint:
                         # amazing!
                         merged_cond = ConditionProcessor.simplify_condition(
                             claripy.And(self.cond_proc.claripy_ast_from_ail_condition(cond_node.condition),
-                                        condbreak_node.condition))
+                                        self.cond_proc.claripy_ast_from_ail_condition(condbreak_node.condition)))
                         new_node = ConditionalBreakNode(condbreak_node.addr, merged_cond, condbreak_node.target)
                         seq_node.nodes[i] = new_node
                         walker.merged = True


Test patch:
diff --git a/tests/test_decompiler.py b/tests/test_decompiler.py
--- a/tests/test_decompiler.py
+++ b/tests/test_decompiler.py
@@ -1,4 +1,4 @@
-# pylint: disable=missing-class-docstring,no-self-use,
+# pylint: disable=missing-class-docstring,no-self-use,line-too-long
 import logging
 import os
 import re
@@ -1600,6 +1600,22 @@ def test_decompiling_setb(self, decompiler_options=None):
         assert f.prototype.returnty is not None and f.prototype.returnty.size == 8
         assert "a0 - 65 < 26;" in d.codegen.text
 
+    @for_all_structuring_algos
+    def test_decompiling_tac_base_len(self, decompiler_options=None):
+        # source: https://github.com/coreutils/gnulib/blob/08ba9aaebff69a02cbb794c6213314fd09dd5ec5/lib/basename-lgpl.c#L52
+        bin_path = os.path.join(test_location, "x86_64", "decompiler", "tac")
+        proj = angr.Project(bin_path, auto_load_libs=False)
+
+        cfg = proj.analyses.CFGFast(normalize=True, data_references=True)
+
+        f = proj.kb.functions["base_len"]
+        d = proj.analyses[Decompiler].prep()(f, cfg=cfg.model, options=decompiler_options)
+        self._print_decompilation_result(d)
+
+        spaceless_text = d.codegen.text.replace(" ", "").replace("\n", "")
+        assert "==47" in spaceless_text or "!= 47" in spaceless_text
+        assert "=47){continue;}" in spaceless_text
+
 
 if __name__ == "__main__":
     unittest.main()