evm: add per state trace file (#817)
* Add trace file * Make ethersplay compatible trace format * Small cleanup * Record separate init and rt traces, emit separate trace files * add todo * More pythonic * Add test for end insn in trace file * Fix test * Move trace accumulation into will_evm_execute callback here, we can easily and correctly check the .last_exception field to see if we were in init code or rt code. from the did_evm_execute hook the last instruction (end instruction) had a different .last_exception so this was causing the last instruction to not be recorded in the trace.
This commit is contained in:
+33
-17
@@ -138,9 +138,8 @@ class UninitializedStorage(Detector):
|
||||
state.context.setdefault('seth.detectors.initialized_storage', set()).add(offset)
|
||||
|
||||
|
||||
def calculate_coverage(code, seen):
|
||||
''' Calculates what percentage of code has been seen '''
|
||||
runtime_bytecode = code
|
||||
def calculate_coverage(runtime_bytecode, seen):
|
||||
''' Calculates what percentage of runtime_bytecode has been seen '''
|
||||
end = None
|
||||
if ''.join(runtime_bytecode[-44: -34]) == '\x00\xa1\x65\x62\x7a\x7a\x72\x30\x58\x20' \
|
||||
and ''.join(runtime_bytecode[-2:]) == '\x00\x29':
|
||||
@@ -674,7 +673,6 @@ class ManticoreEVM(Manticore):
|
||||
self._executor.subscribe('did_load_state', self._load_state_callback)
|
||||
self._executor.subscribe('will_terminate_state', self._terminate_state_callback)
|
||||
self._executor.subscribe('will_execute_instruction', self._will_execute_instruction_callback)
|
||||
self._executor.subscribe('did_execute_instruction', self._did_execute_instruction_callback)
|
||||
self._executor.subscribe('did_read_code', self._did_evm_read_code)
|
||||
self._executor.subscribe('on_symbolic_sha3', self._symbolic_sha3)
|
||||
self._executor.subscribe('on_concrete_sha3', self._concrete_sha3)
|
||||
@@ -1106,17 +1104,16 @@ class ManticoreEVM(Manticore):
|
||||
assert state.platform.constraints == state.platform.current.constraints
|
||||
logger.debug("%s", state.platform.current)
|
||||
|
||||
if 'Call' in str(type(state.platform.current.last_exception)):
|
||||
coverage_context_name = 'runtime_coverage'
|
||||
else:
|
||||
if isinstance(state.platform.current.last_exception, evm.Create):
|
||||
coverage_context_name = 'init_coverage'
|
||||
trace_context_name = 'seth.init.trace'
|
||||
else:
|
||||
coverage_context_name = 'runtime_coverage'
|
||||
trace_context_name = 'seth.rt.trace'
|
||||
|
||||
with self.locked_context(coverage_context_name, set) as coverage:
|
||||
coverage.add((state.platform.current.address, state.platform.current.pc))
|
||||
|
||||
def _did_execute_instruction_callback(self, state, prev_pc, pc, instruction):
|
||||
''' INTERNAL USE '''
|
||||
state.context.setdefault('seth.trace', []).append((state.platform.current.address, prev_pc))
|
||||
state.context.setdefault(trace_context_name, []).append((state.platform.current.address, pc))
|
||||
|
||||
def _did_evm_read_code(self, state, offset, size):
|
||||
''' INTERNAL USE '''
|
||||
@@ -1168,7 +1165,7 @@ class ManticoreEVM(Manticore):
|
||||
with testcase.open_stream('summary') as summary:
|
||||
summary.write("Last exception: %s\n" % state.context['last_exception'])
|
||||
|
||||
address, offset = state.context['seth.trace'][-1]
|
||||
address, offset = state.context['seth.rt.trace'][-1]
|
||||
|
||||
# Last instruction
|
||||
metadata = self.get_metadata(blockchain.transactions[-1].address)
|
||||
@@ -1198,14 +1195,14 @@ class ManticoreEVM(Manticore):
|
||||
summary.write("\t%032x -> %032x %s\n" % (offset, value, flagged(is_storage_symbolic)))
|
||||
is_something_symbolic = is_something_symbolic or is_storage_symbolic
|
||||
|
||||
code = blockchain.get_code(account_address)
|
||||
if len(code):
|
||||
runtime_code = blockchain.get_code(account_address)
|
||||
if runtime_code:
|
||||
summary.write("Code:\n")
|
||||
fcode = StringIO.StringIO(code)
|
||||
fcode = StringIO.StringIO(runtime_code)
|
||||
for chunk in iter(lambda: fcode.read(32), b''):
|
||||
summary.write('\t%s\n' % chunk.encode('hex'))
|
||||
trace = set((offset for address_i, offset in state.context['seth.trace'] if address == address_i))
|
||||
summary.write("Coverage %d%% (on this state)\n" % calculate_coverage(code, trace)) # coverage % for address in this account/state
|
||||
runtime_trace = set((pc for contract, pc in state.context['seth.rt.trace'] if address == contract))
|
||||
summary.write("Coverage %d%% (on this state)\n" % calculate_coverage(runtime_code, runtime_trace)) # coverage % for address in this account/state
|
||||
summary.write("\n")
|
||||
|
||||
if blockchain._sha3:
|
||||
@@ -1295,6 +1292,25 @@ class ManticoreEVM(Manticore):
|
||||
logger.debug("Using iterpickle to dump state")
|
||||
statef.write(iterpickle.dumps(state, 2))
|
||||
|
||||
with testcase.open_stream('rt.trace') as f:
|
||||
self._emit_trace_file(f, state.context['seth.rt.trace'])
|
||||
|
||||
with testcase.open_stream('init.trace') as f:
|
||||
self._emit_trace_file(f, state.context['seth.init.trace'])
|
||||
|
||||
@staticmethod
|
||||
def _emit_trace_file(filestream, trace):
|
||||
"""
|
||||
:param filestream: file object for the workspace trace file
|
||||
:param trace: list of (contract address, pc) tuples
|
||||
:type trace: list[tuple(int, int)]
|
||||
"""
|
||||
for contract, pc in trace:
|
||||
if pc == 0:
|
||||
filestream.write('---\n')
|
||||
ln = '0x{:x}:0x{:x}\n'.format(contract, pc)
|
||||
filestream.write(ln)
|
||||
|
||||
def finalize(self):
|
||||
"""
|
||||
Terminate and generate testcases for all currently alive states (contract states that cleanly executed
|
||||
|
||||
+48
-1
@@ -1,11 +1,13 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
from manticore.core.plugin import Plugin
|
||||
from manticore.core.smtlib import ConstraintSet, operators
|
||||
from manticore.core.smtlib.expression import BitVec
|
||||
from manticore.core.state import State
|
||||
|
||||
from manticore.ethereum import ManticoreEVM, IntegerOverflow, Detector, NoAliveStates
|
||||
from manticore.platforms.evm import EVMWorld, ConcretizeStack, concretized_args
|
||||
from manticore.platforms.evm import EVMWorld, ConcretizeStack, Create, concretized_args
|
||||
|
||||
|
||||
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -92,6 +94,51 @@ class EthTests(unittest.TestCase):
|
||||
self.assertIn('STOP', context)
|
||||
self.assertIn('REVERT', context)
|
||||
|
||||
def test_end_instruction_trace(self):
|
||||
"""
|
||||
Make sure that the trace files are correct, and include the end instructions
|
||||
"""
|
||||
class TestPlugin(Plugin):
|
||||
"""
|
||||
Record the pcs of all end instructions encountered. Source of truth.
|
||||
"""
|
||||
def will_evm_execute_instruction_callback(self, state, instruction, arguments):
|
||||
if isinstance(state.platform.current.last_exception, Create):
|
||||
name = 'init'
|
||||
else:
|
||||
name = 'rt'
|
||||
|
||||
# collect all end instructions based on whether they are in init or rt
|
||||
if instruction.semantics in ('REVERT', 'STOP', 'RETURN'):
|
||||
with self.locked_context(name) as d:
|
||||
d.append(state.platform.current.pc)
|
||||
|
||||
mevm = ManticoreEVM()
|
||||
p = TestPlugin()
|
||||
mevm.register_plugin(p)
|
||||
|
||||
filename = os.path.join(THIS_DIR, 'binaries/int_overflow.sol')
|
||||
mevm.multi_tx_analysis(filename, tx_limit=1)
|
||||
|
||||
worksp = mevm.workspace
|
||||
listdir = os.listdir(worksp)
|
||||
|
||||
def get_concatenated_files(directory, suffix):
|
||||
paths = [os.path.join(directory, f) for f in listdir if f.endswith(suffix)]
|
||||
concatenated = ''.join(open(path).read() for path in paths)
|
||||
return concatenated
|
||||
|
||||
all_init_traces = get_concatenated_files(worksp, 'init.trace')
|
||||
all_rt_traces = get_concatenated_files(worksp, 'rt.trace')
|
||||
|
||||
# make sure all init end insns appear somewhere in the init traces
|
||||
for pc in p.context['init']:
|
||||
self.assertIn(':0x{:x}'.format(pc), all_init_traces)
|
||||
|
||||
# and all rt end insns appear somewhere in the rt traces
|
||||
for pc in p.context['rt']:
|
||||
self.assertIn(':0x{:x}'.format(pc), all_rt_traces)
|
||||
|
||||
def test_graceful_handle_no_alive_states(self):
|
||||
"""
|
||||
If there are no alive states, or no initial states, we should not crash. issue #795
|
||||
|
||||
Reference in New Issue
Block a user