Repository: crytic/slither

Issue:
[Bug]: Dominator-deadcode problem
### Describe the issue:

The i++ node has no father though it's not  an ENTRY_POINT, so the only dominator of the node i++ is i++ itself. So when slither wants to find dominators for IF_LOOP node, it intersects the dominators of BEGIN_LOOP and i++, and gets nothing. Thus IF_LOOP has no immediate dominator which throws an assertion error

### Code example to reproduce the issue:

```solidity
contract C {
    function f() public {
        for (uint256 i = 1; i < 10; i++) {
            break;  // continue
        }
    }
}
```

### Version:

0.9.3

### Relevant log output:

```shell
'solc --version' running
'solc a.sol --combined-json abi,ast,bin,bin-runtime,srcmap,srcmap-runtime,userdoc,devdoc,hashes,compact-format --allow-paths . a.sol' running
Compilation warnings/errors on a.sol:
Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: <SPDX-License>" to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information.
--> a.sol

Warning: Source file does not specify required compiler version! Consider adding "pragma solidity ^0.7.5;"
--> a.sol

Warning: Unreachable code.
 --> a.sol:3:37:
  |
3 |         for (uint256 i = 1; i < 10; i++) {
  |                                     ^^^

Warning: Function state mutability can be restricted to pure
 --> a.sol:2:5:
  |
2 |     function f() public {
  |     ^ (Relevant source part starts here and spans across multiple lines).


ERROR:SlitherSolcParsing:
Failed to convert IR to SSA for C contract. Please open an issue https://github.com/crytic/slither/issues.
 
Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/slither/__main__.py", line 810, in main_impl
    ) = process_all(filename, args, detector_classes, printer_classes)
  File "/usr/lib/python3.10/site-packages/slither/__main__.py", line 101, in process_all
    ) = process_single(compilation, args, detector_classes, printer_classes)
  File "/usr/lib/python3.10/site-packages/slither/__main__.py", line 79, in process_single
    slither = Slither(target, ast_format=ast, **vars(args))
  File "/usr/lib/python3.10/site-packages/slither/slither.py", line 135, in __init__
    self._init_parsing_and_analyses(kwargs.get("skip_analyze", False))
  File "/usr/lib/python3.10/site-packages/slither/slither.py", line 155, in _init_parsing_and_analyses
    raise e
  File "/usr/lib/python3.10/site-packages/slither/slither.py", line 151, in _init_parsing_and_analyses
    parser.analyze_contracts()
  File "/usr/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 541, in analyze_contracts
    self._convert_to_slithir()
  File "/usr/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 774, in _convert_to_slithir
    raise e
  File "/usr/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 769, in _convert_to_slithir
    contract.convert_expression_to_slithir_ssa()
  File "/usr/lib/python3.10/site-packages/slither/core/declarations/contract.py", line 1502, in convert_expression_to_slithir_ssa
    func.generate_slithir_ssa(all_ssa_state_variables_instances)
  File "/usr/lib/python3.10/site-packages/slither/core/declarations/function_contract.py", line 134, in generate_slithir_ssa
    compute_dominance_frontier(self.nodes)
  File "/usr/lib/python3.10/site-packages/slither/core/dominators/utils.py", line 98, in compute_dominance_frontier
    assert runner.immediate_dominator
AssertionError
ERROR:root:Error in a.sol
ERROR:root:Traceback (most recent call last):
  File "/usr/lib/python3.10/site-packages/slither/__main__.py", line 810, in main_impl
    ) = process_all(filename, args, detector_classes, printer_classes)
  File "/usr/lib/python3.10/site-packages/slither/__main__.py", line 101, in process_all
    ) = process_single(compilation, args, detector_classes, printer_classes)
  File "/usr/lib/python3.10/site-packages/slither/__main__.py", line 79, in process_single
    slither = Slither(target, ast_format=ast, **vars(args))
  File "/usr/lib/python3.10/site-packages/slither/slither.py", line 135, in __init__
    self._init_parsing_and_analyses(kwargs.get("skip_analyze", False))
  File "/usr/lib/python3.10/site-packages/slither/slither.py", line 155, in _init_parsing_and_analyses
    raise e
  File "/usr/lib/python3.10/site-packages/slither/slither.py", line 151, in _init_parsing_and_analyses
    parser.analyze_contracts()
  File "/usr/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 541, in analyze_contracts
    self._convert_to_slithir()
  File "/usr/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 774, in _convert_to_slithir
    raise e
  File "/usr/lib/python3.10/site-packages/slither/solc_parsing/slither_compilation_unit_solc.py", line 769, in _convert_to_slithir
    contract.convert_expression_to_slithir_ssa()
  File "/usr/lib/python3.10/site-packages/slither/core/declarations/contract.py", line 1502, in convert_expression_to_slithir_ssa
    func.generate_slithir_ssa(all_ssa_state_variables_instances)
  File "/usr/lib/python3.10/site-packages/slither/core/declarations/function_contract.py", line 134, in generate_slithir_ssa
    compute_dominance_frontier(self.nodes)
  File "/usr/lib/python3.10/site-packages/slither/core/dominators/utils.py", line 98, in compute_dominance_frontier
    assert runner.immediate_dominator
AssertionError
```
```



Discussion:
@Tiko7454 Thanks for reporting this and providing an example! It's really helpful
This was introduced here https://github.com/crytic/slither/commit/371e3cbe475334e6cc9f5cba99cb0172bb3cc92c#diff-da17d02003c2bcfd4974cafb281c5b587b72398a8de28f65a8963cfa5f029970R98
I think we can have an attribute is_reachable for every node, which shows whether there is a path from ENTRY_POINT to that node. The attribute can be set with a DFS from the ENTRY_POINT. Then when we are getting the dominators of a node, we can intersect the dominators of the father nodes which are reachable only
Also #1982 doesn't fully solve the problem because IF_LOOP node doesn't have any dominator except itself though BEGIN_LOOP is a dominator (IF_LOOP is not reachable from i++ node so by the definition BEGIN_LOOP is a dominator)

Resolving patch:
diff --git a/slither/core/cfg/node.py b/slither/core/cfg/node.py
--- a/slither/core/cfg/node.py
+++ b/slither/core/cfg/node.py
@@ -193,6 +193,8 @@ def __init__(
         self.file_scope: "FileScope" = file_scope
         self._function: Optional["Function"] = None
 
+        self._is_reachable: bool = False
+
     ###################################################################################
     ###################################################################################
     # region General's properties
@@ -234,6 +236,13 @@ def set_function(self, function: "Function") -> None:
     def function(self) -> "Function":
         return self._function
 
+    @property
+    def is_reachable(self) -> bool:
+        return self._is_reachable
+
+    def set_is_reachable(self, new_is_reachable: bool) -> None:
+        self._is_reachable = new_is_reachable
+
     # endregion
     ###################################################################################
     ###################################################################################
diff --git a/slither/core/dominators/utils.py b/slither/core/dominators/utils.py
--- a/slither/core/dominators/utils.py
+++ b/slither/core/dominators/utils.py
@@ -9,9 +9,16 @@
 def intersection_predecessor(node: "Node") -> Set["Node"]:
     if not node.fathers:
         return set()
-    ret = node.fathers[0].dominators
-    for pred in node.fathers[1:]:
-        ret = ret.intersection(pred.dominators)
+    if not any(father.is_reachable for father in node.fathers):
+        return set()
+
+    ret = set()
+    for pred in node.fathers:
+        ret = ret.union(pred.dominators)
+
+    for pred in node.fathers:
+        if pred.is_reachable:
+            ret = ret.intersection(pred.dominators)
     return ret
 
 
@@ -84,6 +91,8 @@ def compute_dominance_frontier(nodes: List["Node"]) -> None:
     for node in nodes:
         if len(node.fathers) >= 2:
             for father in node.fathers:
+                if not father.is_reachable:
+                    continue
                 runner = father
                 # Corner case: if there is a if without else
                 # we need to add update the conditional node
diff --git a/slither/solc_parsing/declarations/function.py b/slither/solc_parsing/declarations/function.py
--- a/slither/solc_parsing/declarations/function.py
+++ b/slither/solc_parsing/declarations/function.py
@@ -315,6 +315,9 @@ def analyze_content(self) -> None:
 
         self._remove_alone_endif()
 
+        if self._function.entry_point:
+            self._update_reachability(self._function.entry_point)
+
     # endregion
     ###################################################################################
     ###################################################################################
@@ -1100,6 +1103,13 @@ def _parse_unchecked_block(self, block: Dict, node: NodeSolc, scope):
             node = self._parse_statement(statement, node, new_scope)
         return node
 
+    def _update_reachability(self, node: Node) -> None:
+        if node.is_reachable:
+            return
+        node.set_is_reachable(True)
+        for son in node.sons:
+            self._update_reachability(son)
+
     def _parse_cfg(self, cfg: Dict) -> None:
 
         assert cfg[self.get_key()] == "Block"


Test patch: