Skip to content

Commit

Permalink
Added storage change detection plugin (#1507)
Browse files Browse the repository at this point in the history
* Added storage change plugin

* Added storage change plugin

* black

* tests

* blkn

* Try it on all long tests

* fixtests

* Changed state count in truffle tests wti

* blkn

* Update manticore/ethereum/plugins.py

Co-Authored-By: Disconnect3d <[email protected]>

* rewording
  • Loading branch information
feliam authored and Eric Hennenfent committed Sep 3, 2019
1 parent 7ec1fed commit eebc80d
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 7 deletions.
3 changes: 2 additions & 1 deletion manticore/ethereum/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)
from ..core.plugin import Profiler
from .manticore import ManticoreEVM
from .plugins import FilterFunctions, LoopDepthLimiter, VerboseTrace
from .plugins import FilterFunctions, LoopDepthLimiter, VerboseTrace, KeepOnlyIfStorageChanges
from ..utils.nointerrupt import WithKeyboardInterruptAs
from ..utils import config

Expand Down Expand Up @@ -72,6 +72,7 @@ def choose_detectors(args):
def ethereum_main(args, logger):
m = ManticoreEVM(workspace_url=args.workspace)
with WithKeyboardInterruptAs(m.kill):
m.register_plugin(KeepOnlyIfStorageChanges())

if args.verbose_trace:
m.register_plugin(VerboseTrace())
Expand Down
1 change: 1 addition & 0 deletions manticore/ethereum/manticore.py
Original file line number Diff line number Diff line change
Expand Up @@ -1228,6 +1228,7 @@ def multi_tx_analysis(
self.count_ready_states(),
self.count_terminated_states(),
)

except NoAliveStates:
break

Expand Down
74 changes: 74 additions & 0 deletions manticore/ethereum/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,3 +142,77 @@ class VerboseTraceStdout(Plugin):

def will_evm_execute_instruction_callback(self, state, instruction, arguments):
print(state.platform.current_vm)


class KeepOnlyIfStorageChanges(Plugin):
""" This plugin discards all transactions that results in states where
the underlying EVM storage did not change or in other words,
there were no writes to it.
This allows to speed-up EVM engine exploration as we don't
explore states that have the same storage (contract data).
However, keep in mind that if the (contract) code relies on
account balance and the balance is not a symbolic value
it might be that a certain state will not be covered by the
execution when this plugin is used.
"""

def did_open_transaction_callback(self, state, tx, *args):
""" We need a stack. Each tx (internal or not) starts with a "False" flag
denoting that it did not write anything to the storage
"""
state.context["written"].append(False)

def did_close_transaction_callback(self, state, tx, *args):
""" When a tx (internal or not) is closed a value is popped out from the
flag stack. Depending on the result if the storage is not rolled back the
next flag in the stack is updated. Not that if the a tx is reverted the
changes it may have done on the storage will not affect the final result.
"""
flag = state.context["written"].pop()
if tx.result in {"RETURN", "STOP"}:
code_written = (tx.result == "RETURN") and (tx.sort == "CREATE")
flag = flag or code_written
# As the ether balance of any account can be manipulated beforehand
# it does not matter if a state can affect the balances or not.
# The same reachability should be obtained as the account original
# balances must be symbolic and free-ish
if not flag:
ether_sent = state.can_be_true(tx.value != 0)
flag = flag or ether_sent
state.context["written"][-1] = state.context["written"][-1] or flag

def did_evm_write_storage_callback(self, state, *args):
""" Turn on the corresponding flag is the storage has been modified.
Note: subject to change if the current transaction is reverted"""
state.context["written"][-1] = True

def will_run_callback(self, *args):
"""Initialize the flag stack at each human tx/run()"""
for st in self.manticore.ready_states:
st.context["written"] = [False]

def did_run_callback(self):
"""When human tx/run just ended remove the states that have not changed
the storage"""
with self.manticore.locked_context("ethereum.saved_states", list) as saved_states:
# Normally the ready states are consumed and forked, eth save the
# states that finished ok in a special context list. This list will
# compose the ready states for the next human transaction.
# The actual "ready_states" list at this point contain the states
# that have not finished the previous TX due to a timeout. Those will
# be ignored.
for state_id in list(saved_states):
st = self.manticore._load(state_id)
if not st.context["written"][-1]:
self.manticore._terminated_states.append(st.id)
saved_states.remove(st.id)

def generate_testcase(self, state, testcase, message):
with testcase.open_stream("summary") as stream:
if not state.context.get("written", (False,))[-1]:
stream.write(
"State was removed from ready list because the last tx did not write to the storage"
)
6 changes: 3 additions & 3 deletions scripts/travis_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ run_truffle_tests(){
# but Manticore fails to explore the paths due to the lack of the 0x1f opcode support
# see https://github.com/trailofbits/manticore/issues/1166
# if [ "$(ls output/*tx -l | wc -l)" != "41" ]; then
if [ "$(ls output/*tx -l | wc -l)" != "31" ]; then
echo "Truffle test failed"
if [ "$(ls output/*tx -l | wc -l)" != "19" ]; then
echo "Truffle test failed" `ls output/*tx -l | wc -l` "!= 19"
return 1
fi
echo "Truffle test succeded"
Expand All @@ -107,7 +107,7 @@ run_truffle_tests(){
run_tests_from_dir() {
DIR=$1
coverage erase
coverage run -m unittest discover "tests/$DIR" 2>&1 >/dev/null | tee travis_tests.log
coverage run -m unittest discover -c -v "tests/$DIR" 2>&1 >/dev/null | tee travis_tests.log
DID_OK=$(tail -n1 travis_tests.log)
if [[ "${DID_OK}" != OK* ]]; then
echo "Some tests failed :("
Expand Down
25 changes: 25 additions & 0 deletions tests/ethereum/contracts/absurdrepetition.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
contract AR {
event Log(string);
uint public a;
uint public b;
uint public c;

function writea(uint wad) public {
a=wad;
}
function writeb(uint wad) public {
if (a==10){
b=wad;
}
}
function writec(uint wad) public {
if (b==10){
c=wad;
}
}
function done() public {
if (c==10){
emit Log("Done!");
}
}
}
3 changes: 3 additions & 0 deletions tests/ethereum/test_consensys_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import unittest
import os
import shutil
from manticore.ethereum.plugins import LoopDepthLimiter, KeepOnlyIfStorageChanges


from manticore.ethereum import (
ManticoreEVM,
Expand All @@ -18,6 +20,7 @@ class EthBenchmark(unittest.TestCase):

def setUp(self):
self.mevm = ManticoreEVM()
self.mevm.register_plugin(KeepOnlyIfStorageChanges())
self.mevm.verbosity(0)
self.worksp = self.mevm.workspace

Expand Down
4 changes: 3 additions & 1 deletion tests/ethereum/test_detectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
DetectManipulableBalance,
State,
)
from manticore.ethereum.plugins import LoopDepthLimiter
from manticore.ethereum.plugins import LoopDepthLimiter, KeepOnlyIfStorageChanges

from manticore.utils import config

consts = config.get_group("core")
Expand All @@ -46,6 +47,7 @@ class EthDetectorTest(unittest.TestCase):

def setUp(self):
self.mevm = ManticoreEVM()
self.mevm.register_plugin(KeepOnlyIfStorageChanges())
self.mevm.verbosity(0)
self.worksp = self.mevm.workspace

Expand Down
18 changes: 16 additions & 2 deletions tests/ethereum/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import unittest

import shutil
from manticore.ethereum.plugins import VerboseTrace
from manticore.ethereum.plugins import VerboseTrace, KeepOnlyIfStorageChanges

from manticore.ethereum import ManticoreEVM

Expand All @@ -16,8 +16,22 @@ def setUp(self):
self.mevm = ManticoreEVM()

def tearDown(self):
# shutil.rmtree(self.mevm.workspace)
ws = self.mevm.workspace
del self.mevm
shutil.rmtree(ws)

def test_ignore_states(self):
m = self.mevm
m.register_plugin(KeepOnlyIfStorageChanges())
filename = os.path.join(THIS_DIR, "contracts", "absurdrepetition.sol")

with m.kill_timeout():
m.multi_tx_analysis(filename)

for st in m.all_states:
if st.platform.logs:
return
self.assertEqual(0, 1)

@unittest.skip("failing")
def test_verbose_trace(self):
Expand Down

0 comments on commit eebc80d

Please sign in to comment.