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

handle alternative IR and CFG #17

Merged
merged 2 commits into from
Jan 14, 2023
Merged

handle alternative IR and CFG #17

merged 2 commits into from
Jan 14, 2023

Conversation

kevinclancy
Copy link

@kevinclancy kevinclancy commented Jan 12, 2023

Notes

To begin using a new IR format without breaking existing detectors, we will set a flag generates_certik_ir in the top-level IR/CFG generator SlitherCompilationUnitSolc. For each file, we will create two IR generators, one for the certik IR variant and the other for the existing SlithIR format. Existing detectors will use the old IR format. To cause a new detector to use the new format, we will override the uses_certik_ir property of AbstractDetector to return True.

A new printer called certikir has been added. This is similar to slithir but prints CertiK IR variant instead of SlithIR.

Testing

  • Run ./evaluate.sh run 100 and verify that all projects succeed
  • Replace variable_declaration.py with the below code (which adds default value initialization to uninitialized variable declaration statements) and run the certikir printer. Note that unlike slithir, certikir generates some instructions for default value initialization.

variable_declaration.py

import logging
import re
import copy
from typing import Dict, Optional, TYPE_CHECKING

from slither.solc_parsing.declarations.caller_context import CallerContextExpression
from slither.solc_parsing.expressions.expression_parsing import parse_expression

from slither.core.expressions.expression import Expression
from slither.core.variables.variable import Variable
from slither.core.solidity_types import Type, UserDefinedType, ArrayType
from slither.solc_parsing.solidity_types.type_parsing import parse_type, UnknownType

from slither.core.solidity_types.elementary_type import (
    ElementaryType,
    NonElementaryType,
)
from slither.solc_parsing.exceptions import ParsingError

from slither.core.expressions.literal import Literal
from slither.core.expressions import (
    MemberAccess, Identifier, CallExpression,
    NewArray, TupleExpression, TypeConversion
)
from slither.core.solidity_types.elementary_type import Byte, Int, Uint

from slither.core.declarations import Enum
from slither.core.declarations.structure import Structure
from slither.core.declarations.contract import Contract
from slither.core.declarations.solidity_variables import SolidityFunction

if TYPE_CHECKING:
    from slither.solc_parsing.declarations.function import FunctionSolc

logger = logging.getLogger("VariableDeclarationSolcParsing")


class MultipleVariablesDeclaration(Exception):
    """
    This is raised on
    var (a,b) = ...
    It should occur only on local variable definition
    """

    # pylint: disable=unnecessary-pass
    pass


class VariableDeclarationSolc:
    def __init__(
        self, variable: Variable, variable_data: Dict
    ):  # pylint: disable=too-many-branches
        """
        A variable can be declared through a statement, or directly.
        If it is through a statement, the following children may contain
        the init value.
        It may be possible that the variable is declared through a statement,
        but the init value is declared at the VariableDeclaration children level
        """

        self._variable = variable
        self._was_analyzed = False
        self._elem_to_parse = None
        self._initializedNotParsed = None

        self._is_compact_ast = False
        self._from_statement = False
        self._reference_id = None

        if "nodeType" in variable_data:
            self._is_compact_ast = True
            nodeType = variable_data["nodeType"]
            if nodeType in [
                "VariableDeclarationStatement",
                "VariableDefinitionStatement",
            ]:
                self._from_statement = True
                if len(variable_data["declarations"]) > 1:
                    raise MultipleVariablesDeclaration
                init = None
                if "initialValue" in variable_data:
                    init = variable_data["initialValue"]

                self._init_from_declaration(variable_data["declarations"][0], init)
            elif nodeType == "VariableDeclaration":
                self._from_statement = False
                self._init_from_declaration(variable_data, variable_data.get("value", None))
            else:
                raise ParsingError(f"Incorrect variable declaration type {nodeType}")

        else:
            nodeType = variable_data["name"]

            if nodeType in [
                "VariableDeclarationStatement",
                "VariableDefinitionStatement",
            ]:
                self._from_statement = True
                if len(variable_data["children"]) == 2:
                    init = variable_data["children"][1]
                elif len(variable_data["children"]) == 1:
                    init = None
                elif len(variable_data["children"]) > 2:
                    raise MultipleVariablesDeclaration
                else:
                    raise ParsingError(
                        "Variable declaration without children?" + str(variable_data)
                    )
                declaration = variable_data["children"][0]
                self._init_from_declaration(declaration, init)
            elif nodeType == "VariableDeclaration":
                self._from_statement = False
                self._init_from_declaration(variable_data, False)
            else:
                raise ParsingError(f"Incorrect variable declaration type {nodeType}")

    @property
    def underlying_variable(self) -> Variable:
        return self._variable

    @property
    def reference_id(self) -> int:
        """
        Return the solc id. It can be compared with the referencedDeclaration attr
        Returns None if it was not parsed (legacy AST)
        """
        return self._reference_id

    def _handle_comment(self, attributes: Dict):
        if "documentation" in attributes and "text" in attributes["documentation"]:

            candidates = attributes["documentation"]["text"].split(",")

            for candidate in candidates:
                if "@custom:security non-reentrant" in candidate:
                    self._variable.is_reentrant = False

                write_protection = re.search(
                    r'@custom:security write-protection="([\w, ()]*)"', candidate
                )
                if write_protection:
                    if self._variable.write_protection is None:
                        self._variable.write_protection = []
                    self._variable.write_protection.append(write_protection.group(1))

    def _analyze_variable_attributes(self, attributes: Dict):
        if "visibility" in attributes:
            self._variable.visibility = attributes["visibility"]
        else:
            self._variable.visibility = "internal"

    def _init_from_declaration(self, var: Dict, init: Optional[Dict]):  # pylint: disable=too-many-branches
        if self._is_compact_ast:
            attributes = var
            self._typeName = attributes["typeDescriptions"]["typeString"]
        else:
            assert len(var["children"]) <= 2
            assert var["name"] == "VariableDeclaration"

            attributes = var["attributes"]
            self._typeName = attributes["type"]

        self._variable.name = attributes["name"]
        # self._arrayDepth = 0
        # self._isMapping = False
        # self._mappingFrom = None
        # self._mappingTo = False
        # self._initial_expression = None
        # self._type = None

        # Only for comapct ast format
        # the id can be used later if referencedDeclaration
        # is provided
        if "id" in var:
            self._reference_id = var["id"]

        if "constant" in attributes:
            self._variable.is_constant = attributes["constant"]

        if "mutability" in attributes:
            # Note: this checked is not needed if "constant" was already in attribute, but we keep it
            # for completion
            if attributes["mutability"] == "constant":
                self._variable.is_constant = True
            if attributes["mutability"] == "immutable":
                self._variable.is_immutable = True

        self._handle_comment(attributes)

        self._analyze_variable_attributes(attributes)

        if self._is_compact_ast:
            if var["typeName"]:
                self._elem_to_parse = var["typeName"]
            else:
                self._elem_to_parse = UnknownType(var["typeDescriptions"]["typeString"])
        else:
            if not var["children"]:
                # It happens on variable declared inside loop declaration
                try:
                    self._variable.type = ElementaryType(self._typeName)
                    self._elem_to_parse = None
                except NonElementaryType:
                    self._elem_to_parse = UnknownType(self._typeName)
            else:
                self._elem_to_parse = var["children"][0]

        if self._is_compact_ast:
            self._initializedNotParsed = init
            if init:
                self._variable.initialized = True
        else:
            if init:  # there are two way to init a var local in the AST
                assert len(var["children"]) <= 1
                self._variable.initialized = True
                self._initializedNotParsed = init
            elif len(var["children"]) in [0, 1]:
                self._variable.initialized = False
                self._initializedNotParsed = []
            else:
                assert len(var["children"]) == 2
                self._variable.initialized = True
                self._initializedNotParsed = var["children"][1]

    def _get_init_value(self, ty : Type) -> Expression:

        if isinstance(ty, ElementaryType) and (ty.type in Int + Uint + Byte + ["address"]):
            return Literal("0", ElementaryType(ty.type))
        elif isinstance(ty, ElementaryType) and (ty.type == "string"):
            return Literal("", ElementaryType("string"))
        elif isinstance(ty, ElementaryType) and (ty.type == "bool"):
            return Literal("false", ElementaryType("bool"))
        elif isinstance(ty, ArrayType) and ty.is_dynamic_array:
            return CallExpression(
                NewArray(1, copy.deepcopy(ty.type)),
                [Literal("0", ElementaryType("uint256"))],
                f"{ty.type}[] memory"
            )
        elif isinstance(ty, ArrayType) and ty.is_fixed_array:
            length = int(ty.length_value.value)
            base_init_value = self._get_init_value(ty.type)
            return TupleExpression([copy.deepcopy(base_init_value) for _ in range(0, length)])
        elif isinstance(ty, UserDefinedType) and isinstance(ty.type, Enum):
            return MemberAccess(
                "min",
                ty,
                CallExpression(
                    Identifier(SolidityFunction("type()")),
                    [Identifier(ty.type)],
                    f"type(enum {ty.type.name})"
                )
            )
        elif isinstance(ty, UserDefinedType) and isinstance(ty.type, Structure):
            return CallExpression(
                Identifier(copy.deepcopy(ty.type)),
                [self._get_init_value(field.type) for field in ty.type.elems_ordered],
                f"struct {ty.type.name} memory"
            )
        elif isinstance(ty, UserDefinedType) and isinstance(ty.type, Contract):
            return TypeConversion(
                TypeConversion(Literal("0", ElementaryType("uint256")), ElementaryType("address")),
                UserDefinedType(ty.type)
            )
        assert False # unreachable

    def analyze(self, caller_context: CallerContextExpression):
        # Can be re-analyzed due to inheritance
        if self._was_analyzed:
            return
        self._was_analyzed = True

        type_name = self._elem_to_parse

        if self._elem_to_parse:
            self._variable.type = parse_type(self._elem_to_parse, caller_context)
            self._elem_to_parse = None

        if self._variable.initialized:
            self._variable.expression = parse_expression(self._initializedNotParsed, caller_context)
            self._initializedNotParsed = None
        elif self._from_statement and caller_context.slither_parser.generates_certik_ir:
            self._variable.expression = self._get_init_value(self._variable.type)

example.sol

pragma solidity ^0.8.13;

struct A {
    int x;
}

contract Test {
    function store() public {
        A memory z;
    }
}

Related Issue

https://github.com/CertiKProject/slither-task/issues/217

@kevinclancy kevinclancy requested a review from duckki January 12, 2023 00:12
@kevinclancy kevinclancy changed the title handler alternative IR and CFG handle alternative IR and CFG Jan 12, 2023
Copy link

@duckki duckki left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a minor coding style request.
Otherwise, everything looks good.

slither/slither.py Outdated Show resolved Hide resolved
slither/slither.py Outdated Show resolved Hide resolved
@duckki
Copy link

duckki commented Jan 13, 2023

BTW, as to variable_declaration.py file change for testing, you could generate a patch instead of the whole file.
For example, run git diff --no-color > patch.diff. Then, copy/paste the content of patch.diff here.
Also, if you use pbcopy on mac, it will be even faster like git diff --no-color | pbcopy and paste.
A reviewer can apply that patch like pbpaste | git apply.

@kevinclancy kevinclancy requested a review from duckki January 13, 2023 22:55
@duckki duckki merged commit f6dcd19 into certik Jan 14, 2023
@duckki duckki deleted the certik-ir branch January 27, 2023 22:15
whonore pushed a commit that referenced this pull request May 24, 2024
* create two parsers and compilation units: one using the new certik ir and the other using slithir

https://github.com/CertiKProject/slither-task/issues/217
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants