Merge pull request #1081 from ConsenSys/dependency_prune_v2

Read/write dependency pruning v2
pull/1086/head
Bernhard Mueller 6 years ago committed by GitHub
commit 36589d83cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      mythril/analysis/symbolic.py
  2. 6
      mythril/interfaces/cli.py
  3. 330
      mythril/laser/ethereum/plugins/implementations/dependency_pruner.py
  4. 9
      mythril/laser/ethereum/plugins/plugin_factory.py
  5. 10
      mythril/laser/ethereum/plugins/signals.py
  6. 14
      mythril/laser/ethereum/svm.py
  7. 5
      mythril/mythril/mythril_analyzer.py

@ -56,6 +56,7 @@ class SymExecWrapper:
modules=(),
compulsory_statespace=True,
enable_iprof=False,
disable_dependency_pruning=False,
run_analysis_modules=True,
):
"""
@ -122,6 +123,9 @@ class SymExecWrapper:
plugin_loader.load(PluginFactory.build_mutation_pruner_plugin())
plugin_loader.load(PluginFactory.build_instruction_coverage_plugin())
if not disable_dependency_pruning:
plugin_loader.load(PluginFactory.build_dependency_pruner_plugin())
world_state = WorldState()
for account in self.accounts.values():
world_state.put_account(account)

@ -249,6 +249,11 @@ def create_parser(parser: argparse.ArgumentParser) -> None:
options.add_argument(
"--enable-iprof", action="store_true", help="enable the instruction profiler"
)
options.add_argument(
"--disable-dependency-pruning",
action="store_true",
help="Deactivate dependency-based pruning",
)
rpc = parser.add_argument_group("RPC options")
@ -417,6 +422,7 @@ def execute_command(
loop_bound=args.loop_bound,
create_timeout=args.create_timeout,
enable_iprof=args.enable_iprof,
disable_dependency_pruning=args.disable_dependency_pruning,
onchain_storage_access=not args.no_onchain_storage_access,
)

@ -0,0 +1,330 @@
from mythril.laser.ethereum.svm import LaserEVM
from mythril.laser.ethereum.plugins.plugin import LaserPlugin
from mythril.laser.ethereum.plugins.signals import PluginSkipState
from mythril.laser.ethereum.state.annotation import StateAnnotation
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.transaction.transaction_models import (
ContractCreationTransaction,
)
from mythril.exceptions import UnsatError
from z3.z3types import Z3Exception
from mythril.analysis import solver
from typing import cast, List, Dict, Set
from copy import copy
import logging
log = logging.getLogger(__name__)
class DependencyAnnotation(StateAnnotation):
"""Dependency Annotation
This annotation tracks read and write access to the state during each transaction.
"""
def __init__(self):
self.storage_loaded = [] # type: List
self.storage_written = {} # type: Dict[int, List]
self.has_call = False
self.path = [0] # type: List
def __copy__(self):
result = DependencyAnnotation()
result.storage_loaded = copy(self.storage_loaded)
result.storage_written = copy(self.storage_written)
result.path = copy(self.path)
result.has_call = self.has_call
return result
def get_storage_write_cache(self, iteration: int):
if iteration not in self.storage_written:
self.storage_written[iteration] = []
return self.storage_written[iteration]
def extend_storage_write_cache(self, iteration: int, value: object):
if iteration not in self.storage_written:
self.storage_written[iteration] = [value]
else:
if value not in self.storage_written[iteration]:
self.storage_written[iteration].append(value)
class WSDependencyAnnotation(StateAnnotation):
"""Dependency Annotation for World state
This world state annotation maintains a stack of state annotations.
It is used to transfer individual state annotations from one transaction to the next.
"""
def __init__(self):
self.annotations_stack = []
def __copy__(self):
result = WSDependencyAnnotation()
result.annotations_stack = copy(self.annotations_stack)
return result
def get_dependency_annotation(state: GlobalState) -> DependencyAnnotation:
""" Returns a dependency annotation
:param state: A global state object
"""
annotations = cast(
List[DependencyAnnotation], list(state.get_annotations(DependencyAnnotation))
)
if len(annotations) == 0:
"""FIXME: Hack for carrying over state annotations from the STOP and RETURN states of
the previous states. The states are pushed on a stack in the world state annotation
and popped off the stack in the subsequent iteration. This might break if any
other strategy than bfs is used (?).
"""
try:
world_state_annotation = get_ws_dependency_annotation(state)
annotation = world_state_annotation.annotations_stack.pop()
except IndexError:
annotation = DependencyAnnotation()
state.annotate(annotation)
else:
annotation = annotations[0]
return annotation
def get_ws_dependency_annotation(state: GlobalState) -> WSDependencyAnnotation:
""" Returns the world state annotation
:param state: A global state object
"""
annotations = cast(
List[WSDependencyAnnotation],
list(state.world_state.get_annotations(WSDependencyAnnotation)),
)
if len(annotations) == 0:
annotation = WSDependencyAnnotation()
state.world_state.annotate(annotation)
else:
annotation = annotations[0]
return annotation
class DependencyPruner(LaserPlugin):
"""Dependency Pruner Plugin
For every basic block, this plugin keeps a list of storage locations that
are accessed (read) in the execution path containing that block. This map
is built up over the whole symbolic execution run.
After the initial build up of the map in the first transaction, blocks are
executed only if any of the storage locations written to in the previous
transaction can have an effect on that block or any of its successors.
"""
def __init__(self):
"""Creates DependencyPruner"""
self._reset()
def _reset(self):
self.iteration = 0
self.dependency_map = {} # type: Dict[int, List[object]]
self.protected_addresses = set() # type: Set[int]
def update_dependency_map(self, path: List[int], target_location: object) -> None:
"""Update the dependency map for the block offsets on the given path.
:param path
:param target_location
"""
try:
for address in path:
if address in self.dependency_map:
if target_location not in self.dependency_map[address]:
self.dependency_map[address].append(target_location)
else:
self.dependency_map[address] = [target_location]
except Z3Exception as e:
# This should not happen unless there's a bug in laser, such as an invalid type being generated.
log.debug("Error updating dependency map: {}".format(e))
def protect_path(self, path: List[int]) -> None:
"""Prevent an execution path of being pruned.
:param path
"""
for address in path:
self.protected_addresses.add(address)
def wanna_execute(self, address: int, storage_write_cache) -> bool:
"""Decide whether the basic block starting at 'address' should be executed.
:param address
:param storage_write_cache
"""
if address in self.protected_addresses or address not in self.dependency_map:
return True
dependencies = self.dependency_map[address]
# Return if *any* dependency is found
for location in storage_write_cache:
for dependency in dependencies:
try:
solver.get_model([location == dependency])
return True
except UnsatError:
continue
return False
def initialize(self, symbolic_vm: LaserEVM) -> None:
"""Initializes the DependencyPruner
:param symbolic_vm
"""
self._reset()
@symbolic_vm.laser_hook("start_sym_trans")
def start_sym_trans_hook():
self.iteration += 1
@symbolic_vm.post_hook("CALL")
def call_hook(state: GlobalState):
annotation = get_dependency_annotation(state)
annotation.has_call = True
self.protect_path(annotation.path)
@symbolic_vm.post_hook("JUMP")
def jump_hook(state: GlobalState):
address = state.get_current_instruction()["address"]
annotation = get_dependency_annotation(state)
_check_basic_block(address, annotation)
@symbolic_vm.pre_hook("JUMPDEST")
def jumpdest_hook(state: GlobalState):
address = state.get_current_instruction()["address"]
annotation = get_dependency_annotation(state)
_check_basic_block(address, annotation)
@symbolic_vm.post_hook("JUMPI")
def jumpi_hook(state: GlobalState):
address = state.get_current_instruction()["address"]
annotation = get_dependency_annotation(state)
_check_basic_block(address, annotation)
@symbolic_vm.pre_hook("SSTORE")
def sstore_hook(state: GlobalState):
annotation = get_dependency_annotation(state)
annotation.extend_storage_write_cache(
self.iteration, state.mstate.stack[-1]
)
@symbolic_vm.pre_hook("SLOAD")
def sload_hook(state: GlobalState):
annotation = get_dependency_annotation(state)
location = state.mstate.stack[-1]
if location not in annotation.storage_loaded:
annotation.storage_loaded.append(location)
# We backwards-annotate the path here as sometimes execution never reaches a stop or return
# (and this may change in a future transaction).
self.update_dependency_map(annotation.path, location)
@symbolic_vm.pre_hook("STOP")
def stop_hook(state: GlobalState):
_transaction_end(state)
@symbolic_vm.pre_hook("RETURN")
def return_hook(state: GlobalState):
_transaction_end(state)
def _transaction_end(state: GlobalState) -> None:
"""When a stop or return is reached, the storage locations read along the path are entered into
the dependency map for all nodes encountered in this path.
:param state:
"""
annotation = get_dependency_annotation(state)
if annotation.has_call:
self.protect_path(annotation.path)
for index in annotation.storage_loaded:
self.update_dependency_map(annotation.path, index)
def _check_basic_block(address: int, annotation: DependencyAnnotation):
"""This method is where the actual pruning happens.
:param address: Start address (bytecode offset) of the block
:param annotation
"""
# Don't skip any blocks in the contract creation transaction
if self.iteration < 2:
return
annotation.path.append(address)
if self.wanna_execute(
address, annotation.get_storage_write_cache(self.iteration - 1)
):
return
else:
log.debug(
"Skipping state: Storage slots {} not read in block at address {}".format(
annotation.get_storage_write_cache(self.iteration - 1), address
)
)
raise PluginSkipState
@symbolic_vm.laser_hook("add_world_state")
def world_state_filter_hook(state: GlobalState):
if isinstance(state.current_transaction, ContractCreationTransaction):
# Reset iteration variable
self.iteration = 0
return
world_state_annotation = get_ws_dependency_annotation(state)
annotation = get_dependency_annotation(state)
# Reset the state annotation except for storage written which is carried on to
# the next transaction
annotation.path = [0]
annotation.storage_loaded = []
annotation.has_call = False
world_state_annotation.annotations_stack.append(annotation)
log.debug(
"Iteration {}: Adding world state at address {}, end of function {}.\nDependency map: {}\nStorage written: {}".format(
self.iteration,
state.get_current_instruction()["address"],
state.node.function_name,
self.dependency_map,
annotation.storage_written[self.iteration],
)
)

@ -30,3 +30,12 @@ class PluginFactory:
)
return InstructionCoveragePlugin()
@staticmethod
def build_dependency_pruner_plugin() -> LaserPlugin:
""" Creates an instance of the mutation pruner plugin"""
from mythril.laser.ethereum.plugins.implementations.dependency_pruner import (
DependencyPruner,
)
return DependencyPruner()

@ -15,3 +15,13 @@ class PluginSkipWorldState(PluginSignal):
"""
pass
class PluginSkipState(PluginSignal):
""" Plugin to skip world state
Plugins that raise this signal while the add_world_state hook is being executed
will force laser to abandon that world state.
"""
pass

@ -10,7 +10,7 @@ from mythril.laser.ethereum.evm_exceptions import StackUnderflowException
from mythril.laser.ethereum.evm_exceptions import VmException
from mythril.laser.ethereum.instructions import Instruction
from mythril.laser.ethereum.iprof import InstructionProfiler
from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState
from mythril.laser.ethereum.plugins.signals import PluginSkipWorldState, PluginSkipState
from mythril.laser.ethereum.state.global_state import GlobalState
from mythril.laser.ethereum.state.world_state import WorldState
from mythril.laser.ethereum.strategy.basic import DepthFirstSearchStrategy
@ -274,7 +274,12 @@ class LaserEVM:
self._add_world_state(global_state)
return [], None
self._execute_pre_hook(op_code, global_state)
try:
self._execute_pre_hook(op_code, global_state)
except PluginSkipState:
self._add_world_state(global_state)
return [], None
try:
new_global_states = Instruction(
op_code, self.dynamic_loader, self.iprof
@ -541,7 +546,10 @@ class LaserEVM:
for hook in self.post_hooks[op_code]:
for global_state in global_states:
hook(global_state)
try:
hook(global_state)
except PluginSkipState:
global_states.remove(global_state)
def pre_hook(self, op_code: str) -> Callable:
"""

@ -38,6 +38,7 @@ class MythrilAnalyzer:
loop_bound: Optional[int] = None,
create_timeout: Optional[int] = None,
enable_iprof: bool = False,
disable_dependency_pruning: bool = False,
):
"""
@ -57,6 +58,7 @@ class MythrilAnalyzer:
self.loop_bound = loop_bound
self.create_timeout = create_timeout
self.enable_iprof = enable_iprof
self.disable_dependency_pruning = disable_dependency_pruning
def dump_statespace(self, contract: EVMContract = None) -> str:
"""
@ -77,6 +79,7 @@ class MythrilAnalyzer:
execution_timeout=self.execution_timeout,
create_timeout=self.create_timeout,
enable_iprof=self.enable_iprof,
disable_dependency_pruning=self.disable_dependency_pruning,
run_analysis_modules=False,
)
@ -111,6 +114,7 @@ class MythrilAnalyzer:
transaction_count=transaction_count,
create_timeout=self.create_timeout,
enable_iprof=self.enable_iprof,
disable_dependency_pruning=self.disable_dependency_pruning,
run_analysis_modules=False,
)
return generate_graph(sym, physics=enable_physics, phrackify=phrackify)
@ -150,6 +154,7 @@ class MythrilAnalyzer:
modules=modules,
compulsory_statespace=False,
enable_iprof=self.enable_iprof,
disable_dependency_pruning=self.disable_dependency_pruning,
)
issues = fire_lasers(sym, modules)

Loading…
Cancel
Save