Initial master merge into experimental state merging for native (#1482)

* Initial master merge into experimental st merging for native

* black

* Add C file and fix arguments

* Push (randomly failing) rudimentary state merging test

Fails nondeterministically, so like, that's a problem.

The issue I think lies in the fact that we pull states from the disk in a random order. Fix that so it's the right order, and this will hopefully work.

* Blacken

* Add basic_state_merging binary

* Restore seed parameter to random number generator

* Handle issues with instantiating Manitcore when multiple instances run simultaneously

* Replace print with logging
This commit is contained in:
feliam
2019-08-05 17:02:22 -05:00
committed by Eric Hennenfent
parent f2099bd263
commit 0f1a4f90d0
9 changed files with 539 additions and 14 deletions
+63
View File
@@ -0,0 +1,63 @@
/**
* Symbolic values are read from stdin using standard libc calls.
* Program compares if two binary packed integers at the input with 0x41.
*
* Compile with :
* $ gcc -static -Os basic_statemerging.c -o basic_statemerging
*
* Analyze it with:
* $ python examples/script/basic_statemerging.py examples/linux/basic_statemerging
*
* The Merger plugin used in basic_statemerging.py will find two states with state IDs 2, 4 to be at the same program
* location (0x40060d) and merge their CPU states which should only require the value for RDI to be merged.
*
* Expected output:
* $ python /Users/vaibhav/git_repos/manticore/examples/script/basic_statemerging.py examples/linux/basic_statemerging-Os
about to load state_id = 0
loaded state_id = 0 at cpu = 0x4008e0
about to load state_id = 1
loaded state_id = 1 at cpu = 0x400604
about to load state_id = 2
Merged registers:
RDI
at PC = 0x40060d, merge succeeded for state id = 2 and 4
loaded state_id = 2 at cpu = 0x40060d
about to load state_id = 3
loaded state_id = 3 at cpu = 0x400612
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
int main(int argc, char* argv[], char* envp[]){
unsigned int cmd1, cmd2;
unsigned int cmdChanged = 0;
if (read(0, &cmd1, sizeof(cmd1)) != sizeof(cmd1))
{
printf("Error reading stdin!");
exit(-1);
}
if (read(0, &cmd2, sizeof(cmd2)) != sizeof(cmd2))
{
printf("Error reading stdin!");
exit(-1);
}
if (cmd1 > 0x41)
{
cmdChanged = cmd1 - 0x42;
}
if (cmd2 < 0x41)
{
cmdChanged = cmd2 + 0x42;
}
if (cmdChanged == 0) printf("equal\n");
else printf("not equal\n");
return 0;
}
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python
import sys
from manticore.native.plugins import Merger
from manticore.utils import config
from manticore.native import Manticore
from manticore import set_verbosity
"""
Demonstrates the ability to do state merging on a simple program by merging states with id 2, 4 that happen to be
at the same program location 0x40060d. This script uses the Merger plugin to apply opportunistic state merging.
"""
if __name__ == "__main__":
config.get_group("core").seed = 2
config.get_group("core").mprocessing = config.get_group("core").mprocessing.single
path = sys.argv[1]
m = Manticore(path, policy="random")
def will_load_state_callback(_mc, state_id):
print("about to load state_id = " + str(state_id))
def did_load_state_callback(_mc, state):
print("loaded state_id = " + str(state.id) + " at cpu = " + hex(state.cpu.PC))
m.subscribe("will_load_state", will_load_state_callback)
m.subscribe("did_load_state", did_load_state_callback)
m.register_plugin(Merger())
m.run()
+16 -14
View File
@@ -64,6 +64,11 @@ consts.add(
default=MProcessingType.multiprocessing,
description="single: No multiprocessing at all. Single process.\n threading: use threads\m multiprocessing: use forked processes",
)
consts.add(
"seed",
default=random.getrandbits(32),
description="The seed to use when randomly selecting states",
)
class ManticoreBase(Eventful):
@@ -72,13 +77,11 @@ class ManticoreBase(Eventful):
raise Exception("Should not instantiate this")
cl = consts.mprocessing.to_class()
if cl not in cls.__bases__:
# change ManticoreBase for the more specific class
idx = cls.__bases__.index(ManticoreBase)
bases = list(cls.__bases__)
bases[idx] = cl
cls.__bases__ = tuple(bases)
# change ManticoreBase for the more specific class
bases = {cl if issubclass(base, ManticoreBase) else base for base in cls.__bases__}
cls.__bases__ = tuple(bases)
random.seed(consts.seed)
return super().__new__(cls)
# Decorators added first for convenience.
@@ -113,6 +116,7 @@ class ManticoreBase(Eventful):
@functools.wraps(func)
def newFunction(self, *args, **kw):
if self.is_running():
logger.error("Calling at running not allowed")
raise Exception(f"{func.__name__} only allowed while NOT exploring states")
return func(self, *args, **kw)
@@ -351,12 +355,8 @@ class ManticoreBase(Eventful):
setstate(new_state, new_value)
# enqueue new_state, assign new state id
new_state_id = self._save(new_state, state_id=None)
with self._lock:
self._ready_states.append(new_state_id)
self._lock.notify_all() # Must notify one!
new_state_id = self._put_state(new_state)
self._publish("did_fork_state", new_state, expression, new_value, policy)
# maintain a list of children for logging purpose
children.append(new_state_id)
@@ -366,6 +366,8 @@ class ManticoreBase(Eventful):
state._id = None
self._lock.notify_all()
self._publish("did_fork_state", new_state, expression, new_value, policy)
logger.debug("Forking current state %r into states %r", state.id, children)
@staticmethod
@@ -378,6 +380,7 @@ class ManticoreBase(Eventful):
set_verbosity(level)
# State storage
@Eventful.will_did("save_state")
def _save(self, state, state_id=None):
""" Store or update a state in secondary storage under state_id.
Use a fresh id is None is provided.
@@ -390,6 +393,7 @@ class ManticoreBase(Eventful):
state._id = self._workspace.save_state(state, state_id=state_id)
return state.id
@Eventful.will_did("load_state")
def _load(self, state_id):
""" Load the state from the secondary storage
@@ -397,19 +401,17 @@ class ManticoreBase(Eventful):
:type state_id: int
:returns: the state id used
"""
if not hasattr(self, "stcache"):
self.stcache = weakref.WeakValueDictionary()
if state_id in self.stcache:
return self.stcache[state_id]
self._publish("will_load_state", state_id)
state = self._workspace.load_state(state_id, delete=False)
state._id = state_id
self.forward_events_from(state, True)
self._publish("did_load_state", state, state_id)
self.stcache[state_id] = state
return state
@Eventful.will_did("remove_state")
def _remove(self, state_id):
""" Remove a state from secondary storage
+3
View File
@@ -46,6 +46,9 @@ class EVMAccount:
def __str__(self):
return str(self._address)
def __hash__(self):
return self._address
class EVMContract(EVMAccount):
"""
+129
View File
@@ -0,0 +1,129 @@
from ..core.plugin import Plugin
from .state_merging import merge_constraints, is_merge_possible, merge
import logging
logger = logging.getLogger(__name__)
class Merger(Plugin):
def on_register(self):
p = self.manticore._publish
self.manticore._publish = lambda *args, **kwargs: None
for state in self.manticore.ready_states:
self.did_fork_state_callback(state, None, None, None)
self.manticore._publish = p
def load_state(self, state_id, delete=False):
"""
Loads a state in Manticore with state-id = `state_id` by using the corresponding API in Executor
:param state_id: state-id for state that is being loaded
:param delete: If set to True, deletes the state with state-id = `state_id`
:return: None
"""
state = self.manticore._workspace.load_state(state_id, delete=delete)
return state
def delete_state(self, state_id):
"""
Deletes a state in Manticore with state-id = `state_id` by using the corresponding API in Executor
:param state_id: state-id for state that is being deleted
:return: None
"""
with self.manticore._lock:
self.manticore._ready_states.remove(state_id)
self.manticore._remove(state_id)
def replace_state(self, state_id, state):
"""
Replaces a state in Manticore with state-id = `state_id` by using the corresponding API in Executor
:param state_id: state-id for state that is being replaced
:param state: State object that is replacing the existing state with `state_id`
:return: None
"""
return self.manticore._save(state, state_id=state_id)
def did_fork_state_callback(self, state, expression, new_value, policy):
state_id = state.id
"""That means no one else has to offer one Message Input
Maintains a `cpu_stateid_dict` in context that keeps maps program counter values to a list of state-id values
for states that will execute the instruction that that program counter as the next instruction
:param state_id: id for state that was just enqueued
:param state: State object that was just enqueued
:return: None
"""
# when a new state is addded to the list we save it so we do not have
# to repload all states when try to merges last PC
with self.locked_context("cpu_stateid_dict", dict) as cpu_stateid_dict:
# as we may be running in a different process we need to access this
# on a lock and over shared memory like this
state_id_list = cpu_stateid_dict.get(state.cpu.PC, list())
state_id_list.append(state_id)
cpu_stateid_dict[state.cpu.PC] = state_id_list
def will_load_state_callback(self, current_state_id):
"""
Checks if the state to be loaded (referenced by `current_state_id` can be merged with another currently enqueued
state and replaced by the merged state
:param current_state_id: state about to be loaded
:return: None
"""
# When a state is loaded for exploration lets check if we can find it a
# mate for merging
with self.locked_context("cpu_stateid_dict", dict) as cpu_stateid_dict:
# we get the lock and get a copy of the shared context
merged_state = self.load_state(current_state_id)
states_at_pc = cpu_stateid_dict.get(merged_state.cpu.PC, [])
# lets remove ourself from the list of waiting states
# assert current_state_id in states_at_pc #???
if current_state_id in states_at_pc:
states_at_pc.remove(current_state_id)
# Iterate over all remaining states that are waiting for exploration
# at the same PC
merged_ids = []
for new_state_id in states_at_pc:
new_state = self.load_state(new_state_id)
(exp_merged_state, exp_new_state, merged_constraint) = merge_constraints(
merged_state.constraints, new_state.constraints
)
is_mergeable, reason = is_merge_possible(merged_state, new_state, merged_constraint)
if is_mergeable:
# Ok we'll merge it!
merged_state = merge(
merged_state, new_state, exp_merged_state, merged_constraint
)
# lets remove the vestigial links to the old state
self.delete_state(new_state_id)
merged_ids.append(new_state_id)
is_mergeable = "succeeded"
else:
is_mergeable = "failed because of " + reason
debug_string = (
"at PC = "
+ hex(merged_state.cpu.PC)
+ ", merge "
+ is_mergeable
+ " for state id = "
+ str(current_state_id)
+ " and "
+ str(new_state_id)
)
logger.debug(debug_string)
for i in merged_ids:
states_at_pc.remove(i)
cpu_stateid_dict[merged_state.cpu.PC] = states_at_pc
# Ok so we have merged current_state_id with {merged_ids}
# And removed all merged_ids from everywhere
# UGLY we are replacing a state_id. This may be breaking caches in
# the future
self.replace_state(current_state_id, merged_state)
+218
View File
@@ -0,0 +1,218 @@
from ..core.smtlib import Z3Solver, ConstraintSet, Operators, issymbolic, BitVec
def compare_sockets(cs, socket1, socket2):
"""
This method compares Socket objects for equality using the buffer and peer attributes.
It uses `compare_buffers` for checking buffer attributes for equality.
It calls itself for comparing peer Socket objects.
Returns True if the Socket objects are equal, false otherwise.
:param cs: ConstraintSet to be used for checking Socket.buffer for semantic equality using `Z3Solver.instance().must_be_true()`
:param socket1: one of two Socket objects to be compared for equality against socket2
:param socket2: one of two Socket objects to be compared for equality against socket1
:return: True, if the Socket objects are found to be equal, False otherwise
"""
if socket1 is None:
return socket2 is None
if socket2 is None:
return socket1 is None
if not compare_buffers(cs, socket1.buffer, socket2.buffer):
return False
return compare_sockets(cs, socket1.peer, socket2.peer)
def compare_buffers(cs, buffer1, buffer2):
"""
This method compares the two List objects for equality using the `Z3Solver.instance().must_be_true()` call.
:param cs: ConstraintSet to be used for checking buffer1 for semantic equality with buffer2 using `Z3Solver.instance().must_be_true()`
:param buffer1: one of two List objects to be compared for equality against buffer2
:param buffer2: one of two List objects to be compared for equality against buffer1
:return: True, if the List objects are equal, False otherwise
"""
if len(buffer1) != len(buffer2):
return False
for b1, b2 in zip(buffer1, buffer2):
if not Z3Solver.instance().must_be_true(cs, b1 == b2):
return False
return True
def merge_constraints(constraints1, constraints2):
"""
:param constraints1: one of two ConstraintSet objects to be merged
:param constraints2: second of two ConstraintSet objects to be merged
:return: (Expression, Expression, ConstraintSet) where the first and second Expression objects are conjunctions of
of all the constraints in constraints1 and constraints2 respectively. The ConstraintSet is an object that contains
a single constraint that is a logical OR of these two Expression objects.
"""
exp1 = constraints1.constraints[0]
for i in range(1, len(constraints1.constraints)):
exp1 = exp1 & constraints1.constraints[i]
exp2 = constraints2.constraints[0]
for i in range(1, len(constraints2.constraints)):
exp2 = exp2 & constraints2.constraints[i]
merged_constraint = ConstraintSet()
merged_constraint.add(exp1 | exp2)
return exp1, exp2, merged_constraint
def compare_byte_vals(mem1, mem2, addr, merged_constraint):
"""
Compares values in memory at address `addr`, returns True if they are semantically equal, False otherwise
:param mem1: first of two memory objects we want to use for comparison
:param mem2: second of two memory objects we want to use for comparison
:param addr: address at which bytes values are to be compared
:param merged_constraint: ConstraintSet to be used when using the call to `Z3Solver.instance().must_be_true()`
:return: returns True if 1 byte values at address `addr` in `mem1` and `mem2` are semantically equal, False otherwise
"""
val1 = mem1.read(addr, 1)
val2 = mem2.read(addr, 1)
# since we only read a single byte value, these lists should only have one entry in them
assert len(val1) == 1 and len(val2) == 1
cond_to_check = val1[0] == val2[0]
if not Z3Solver.instance().must_be_true(merged_constraint, cond_to_check):
return False
else:
return True
# TODO move this comparison into an Executor API that uses an internal State API
def compare_mem(mem1, mem2, merged_constraint):
"""
This method compares the number of maps, and then their names, permissions, start, and end values.
If they all match, then it compares the concrete byte values for equality.
If those match too, it then compares _symbols attribute values for equality if the two memory objects are of
type SMemory.
:param mem1: one of two memory objects to be compared
:param mem2: second of two memory objects to be compared
:param merged_constraint: ConstraintSet object that is to be used with `Z3Solver.instance().must_be_true()` calls to check the
memory objects for semantic equality
:return: True, if the memory objects are equal, False otherwise
"""
maps1 = sorted(list(mem1.maps))
maps2 = sorted(list(mem2.maps))
if len(maps1) != len(maps2):
return False
ret_val = None
for m1, m2 in zip(maps1, maps2):
if m1 != m2: # compares the maps' names, permissions, starts, and ends
ret_val = False
break
# Compare concrete byte values in the data in these memory maps for equality
bytes1 = m1[m1.start : m1.end]
bytes2 = m2[m2.start : m2.end]
if bytes1 != bytes2:
ret_val = False
break
checked_addrs = []
# compare symbolic byte values in memory
# hack to avoid importing SMemory because that import introduces a circular dependency on ManticoreBase
if mem1.__class__.__name__ == "SMemory" and ret_val is not None:
for addr1, _ in mem1._symbols.items():
checked_addrs.append(addr1)
if not compare_byte_vals(mem1, mem2, addr1, merged_constraint):
ret_val = False
break
# hack to avoid importing SMemory because that import introduces a circular dependency on ManticoreBase
if mem2.__class__.__name__ == "SMemory" and ret_val is not None:
for addr2, _ in mem2._symbols.items():
if addr2 not in checked_addrs:
if not compare_byte_vals(mem1, mem2, addr2, merged_constraint):
ret_val = False
break
if ret_val is not None:
return ret_val
else:
return True
def is_merge_possible(state1, state2, merged_constraint):
"""
Checks if a merge is possible by checking Input, Output sockets, symbolic_files, syscall_trace, and memory
for equality.
:param state1: one of two possible states we want to check for mergeability
:param state2: second of two possible states we want to check for mergeability
:param merged_constraint: ConstraintSet of merged constraints for state1 and state2
:return: True, if state1 and state2 can be merged, False if otherwise
"""
platform1 = state1.platform
platform2 = state2.platform
ret_val = None
# compare input and output sockets of the states
if not compare_sockets(
merged_constraint, platform1.input, platform2.input
) or not compare_sockets(merged_constraint, platform1.output, platform2.output):
ret_val = False, "inequivalent socket operations"
# compare symbolic files opened by the two states
if ret_val is None and platform1.symbolic_files != platform2.symbolic_files:
ret_val = False, "inequivalent symbolic files"
# compare system call traces of the two states
if ret_val is None and len(platform1.syscall_trace) != len(platform2.syscall_trace):
ret_val = False, "inequivalent syscall trace lengths"
if ret_val is None:
for i, (name1, fd1, data1) in enumerate(platform1.syscall_trace):
(name2, fd2, data2) = platform2.syscall_trace[i]
if not (
name1 == name2 and fd1 == fd2 and compare_buffers(merged_constraint, data1, data2)
):
ret_val = False, "inequivalent syscall traces"
break
# compare memory of the two states
if ret_val is None and not compare_mem(state1.mem, state2.mem, merged_constraint):
ret_val = False, "inequivalent memory"
if ret_val is not None:
return ret_val
else:
return True, None
def merge_cpu(cpu1, cpu2, state, exp1, merged_constraint):
"""
Merge CPU objects into the state.CPU
:param cpu1: one of two CPU objects that we wish to merge
:param cpu2: second of two CPU objects that we wish to merge
:param state: the state whose CPU attribute we will be updating
:param exp1: the expression that if satisfiable will cause the CPU registers to take corresponding values from
`cpu1`, else they will take corresponding values from `cpu2`
:param merged_constraint: ConstraintSet under which we would want inequality between CPU register values to be
satisfiable as checked using `Z3Solver.instance().must_be_true()`
:return: List of registers that were merged
"""
merged_regs = []
for reg in cpu1.canonical_registers:
val1 = cpu1.read_register(reg)
val2 = cpu2.read_register(reg)
if isinstance(val1, BitVec) and isinstance(val2, BitVec):
assert val1.size == val2.size
if issymbolic(val1) or issymbolic(val2) or val1 != val2:
if Z3Solver.instance().must_be_true(merged_constraint, val1 != val2):
merged_regs.append(reg)
if cpu1.regfile.sizeof(reg) == 1:
state.cpu.write_register(reg, Operators.ITE(exp1, val1, val2))
else:
state.cpu.write_register(
reg, Operators.ITEBV(cpu1.regfile.sizeof(reg), exp1, val1, val2)
)
return merged_regs
def merge(state1, state2, exp1, merged_constraint):
"""
Merge state1 and state2 into a single state
:param state1:
:param state2:
:param exp1:
:param merged_constraint:
:return: the state that is the result of the merging of `state1` and `state2`
"""
merged_state = state1
merged_reg_list = merge_cpu(state1.cpu, state2.cpu, merged_state, exp1, merged_constraint)
print("Merged registers: ")
print(*merged_reg_list, sep=",")
merged_state.constraints = merged_constraint
return merged_state
+17
View File
@@ -1,6 +1,7 @@
import copy
import inspect
import logging
import functools
from itertools import takewhile
from weakref import WeakKeyDictionary, ref
@@ -72,6 +73,22 @@ class Eventful(object, metaclass=EventsGatherMetaclass):
all_evts.update(evts)
return all_evts
@staticmethod
def will_did(name):
"""Pre/pos emiting signal"""
def deco(func):
@functools.wraps(func)
def newFunction(self, *args, **kw):
self._publish(f"will_{name}", *args, **kw)
result = func(self, *args, **kw)
self._publish(f"did_{name}", result)
return result
return newFunction
return deco
def __init__(self, *args, **kwargs):
# A dictionary from "event name" -> callback methods
# Note that several methods can be associated with the same object
Binary file not shown.
+63
View File
@@ -5,6 +5,10 @@ from manticore.utils.event import Eventful
from manticore.platforms import linux
from manticore.native.state import State
from manticore.core.smtlib import BitVecVariable, ConstraintSet
from manticore.native import Manticore
from manticore.native.plugins import Merger
from manticore.core.plugin import Plugin
from manticore.utils import config
class FakeMemory:
@@ -257,3 +261,62 @@ class StateTest(unittest.TestCase):
self.assertEqual(new_state.context["step"], 20)
new_new_state = pickle.loads(new_new_file)
self.assertEqual(new_new_state.context["step"], 30)
class StateMergeTest(unittest.TestCase):
# Need to add a plugin that counts the number of states in did_fork_state, and records the max
# Then, when we hit
class StateCounter(Plugin):
def did_fork_state_callback(self, *_args, **_kwargs):
self.max_states = max(
self.max_states,
self.manticore.count_busy_states()
+ self.manticore.count_ready_states()
+ self.manticore.count_killed_states()
+ self.manticore.count_terminated_states(),
)
@property
def max_states(self):
with self.manticore.locked_context() as ctx:
return ctx.setdefault("max_states", 0)
@max_states.setter
def max_states(self, new_val):
with self.manticore.locked_context() as ctx:
ctx["max_states"] = new_val
def setUp(self):
core = config.get_group("core")
core.seed = 61
core.mprocessing = core.mprocessing.single
dirname = os.path.dirname(__file__)
self.m = Manticore(
os.path.join(dirname, "binaries", "basic_state_merging"), policy="random"
)
self.plugin = self.StateCounter()
self.m.register_plugin(Merger())
self.m.register_plugin(self.plugin)
def test_state_merging(self):
@self.m.hook(0x40065D)
def hook_post_merge(*_args, **_kwargs):
with self.m.locked_context() as ctx:
ctx["state_count"] = (
self.m.count_busy_states()
+ self.m.count_ready_states()
+ self.m.count_killed_states()
+ self.m.count_terminated_states()
)
self.m.run()
s = config.get_group("core").seed
self.assertLess(
self.m.context["state_count"],
self.plugin.max_states,
f"State merging failed with seed: {s}",
)