Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Unchecked blocks are identified as checked #1187

Closed
0xalpharush opened this issue Apr 22, 2022 · 3 comments
Closed

[Bug]: Unchecked blocks are identified as checked #1187

0xalpharush opened this issue Apr 22, 2022 · 3 comments
Labels
bug Something isn't working
Milestone

Comments

@0xalpharush
Copy link
Contributor

0xalpharush commented Apr 22, 2022

Describe the issue:

The is_checked attribute does not return false for functions that have unchecked blocks

Code example to reproduce the issue:

contract Test {

    function withdraw(address payable to, uint256 amount) public {
        unchecked {
            uint a = amount - amount;
        }
    }
}

Version:

0.8.3

Relevant log output:

Contract Test
        Function Test.withdraw(address,uint256) (*)
                Expression: a = amount - amount
                IRs:
                        TMP_0(uint256) = amount (c)- amount
                        a(uint256) := TMP_0(uint256)
@0xalpharush 0xalpharush added the bug Something isn't working label Apr 22, 2022
@0xalpharush 0xalpharush changed the title [Bug]: [Bug]: Unchecked blocks are identified as checked Apr 22, 2022
@montyly
Copy link
Member

montyly commented Apr 27, 2022

is_checked is an attribute of the function, not the node or the IR, so it makes sense for the function to only consider the compiler version.

However, node have their own scope:


And is_checked is set to False in this scope for unchecked block:
new_scope = Scope(False, False, node.underlying_node.scope)

Which is used by the IR:
if self.node.scope.is_checked and self._type.can_be_checked_for_overflow():
return "(c)" + str(self._type)

So this should definitely not return a checked operation, I am not sure why it happens

@smonicas
Copy link
Collaborator

The issue is that _parse_unchecked_block calls self._parse_statement with the new scope which is unchecked.

new_scope = Scope(False, False, node.underlying_node.scope)
for statement in statements:
node = self._parse_statement(statement, node, new_scope)
return node

However when parsing a VariableDeclarationStatement the new scope is not taken in consideration and it uses the node.underlying_node.scope which is still the old scope.
elif name in ["VariableDefinitionStatement", "VariableDeclarationStatement"]:
node = self._parse_variable_definition(statement, node)

new_node = self._new_node(
NodeType.VARIABLE, statement["src"], node.underlying_node.scope
)
new_node.underlying_node.add_variable_declaration(local_var)
link_underlying_nodes(node, new_node)
return new_node

The same is true when parsing other statements. In the following example the operations after the unchecked block will also be unchecked.

pragma solidity 0.8.3;

contract T {
  function withdraw(address payable to, uint256 amount) public {
   uint a;
   uint counter;
   unchecked {
       uint b = amount - amount;
       a = amount + b;
   }
   uint c = 5 + 7;
   for(uint i = 0; i < 10; i++) {counter++;}
  }
}
Contract T
	Function T.withdraw(address,uint256) (*)
		Expression: b = amount - amount
		IRs:
			TMP_0(uint256) = amount (c)- amount
			b(uint256) := TMP_0(uint256)
		Expression: a = amount + b
		IRs:
			TMP_1(uint256) = amount + b
			a(uint256) := TMP_1(uint256)
		Expression: c = 5 + 7
		IRs:
			TMP_2(uint256) = 5 + 7
			c(uint256) := TMP_2(uint256)
		Expression: i = 0
		IRs:
			i(uint256) := 0(uint256)
		Expression: i < 10
		IRs:
			TMP_3(bool) = i < 10
			CONDITION TMP_3
		Expression: counter ++
		IRs:
			TMP_4(uint256) := counter(uint256)
			counter(uint256) = counter + 1
		Expression: i ++
		IRs:
			TMP_5(uint256) := i(uint256)
			i(uint256) = i + 1

@alecmaly
Copy link

alecmaly commented Mar 3, 2023

Thanks for identifying the functions.py file!
I've spent some time updating it; I probably broke a bunch of stuff so I'm posting it here for others to validate and use it as a starting point.

I basically just added a scope property to a few of the _parse... functions. It's very possible other functions like _parse_cfg could be updated as well, but I don't have the bandwidth to focus on this and do thorough testing. There is probably a better way to do this but this seemed to work and get me where I need to be, hopefully it helps someone else as a temporary workaround. :)

Modified: function.py: https://github.com/alecmaly/slither/blob/4d35f7dd85ef2cf65c3f68f7af74f81292ae91c9/slither/solc_parsing/declarations/function.py

Modified from: https://github.com/crytic/slither/blob/635649207d52feff049d92cd75ec5b19edbfc61e/slither/solc_parsing/declarations/function.py

And here is a custom detector to test with.
The goal is to identify complex unchecked {} blocks for closer inspection.

CDComplexUncheckedBlock.py

from slither.detectors.abstract_detector import AbstractDetector, DetectorClassification

class CDComplexUncheckedBlock(AbstractDetector):  # pylint: disable=too-few-public-methods
    """
    Documentation
    """

    ARGUMENT = "CD-ComplexUncheckedBlock"  # slither will launch the detector with slither.py --mydetector
    HELP = "Help printed by slither"
    IMPACT = DetectorClassification.INFORMATIONAL
    CONFIDENCE = DetectorClassification.MEDIUM

    WIKI = "x"

    WIKI_TITLE = "Complex unchecked blocks"
    WIKI_DESCRIPTION = "validate logic + fuzz w/ Echidna?"
    WIKI_EXPLOIT_SCENARIO = "x"
    WIKI_RECOMMENDATION = "x"

    def _detect(self):
        """
        Complex Unchecked Block
        """


        unchecked_map = {}
        results = []
        unchecked_counter = 0
        for contract in self.compilation_unit.contracts:
            for function in contract.functions:
                for node in function.nodes:

                    # print to console to validate node expressions + their scopes
                    print(f"{node.scope.is_checked} - {str(node)}")

                    if not node.scope.is_checked:
                        if unchecked_counter == 0:
                            origin_node = node

                        unchecked_counter += 1
                        unchecked_map[origin_node] = unchecked_counter
                    else:
                        unchecked_counter = 0

        for node in unchecked_map:       
            info = [f"unchecked block w/ {unchecked_map[node]} nodes starting in func @ ", node]
            res = self.generate_result(info)
            results.append(res)

        return results

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants