mirror of
https://github.com/trailofbits/manticore.git
synced 2026-10-12 02:23:44 +00:00
The WASM branch is ready to be included in the rest of Manticore. It doesn't include several advanced features, including a symbolic memory model, symbolic floating point support, or a standard library implementation (WASI/emscripten), but it does have full symbolic semantics for all of the integer instructions.
### Commit log:
* Empty WASM Module
Will run on a .wasm file and do nothing forever
* Read and parse sections
/shrug
* Can partially allocate modules
Just pushing for the weekend
* Blacken
* Can allocate with exports now
* Static initialization
Doesn't evaluate any of the starting expressions, but that's okay
* Make type hints a little more pycharm-friendly
* First part of invocation
* Blacken...
* Barebones execution poc
Can execute the following snippet:
```
main(){
return 42;
}
```
* blacken
* Add some loads and stores
* Add global operations
* Move float methods further down
* Support internal function calls
```
18:20:42 (mc) ehennenfent@nessie:~/wasm ➜ cat loop_check.c
#include <stdio.h>
#include <stdbool.h>
bool check(int arg){
if (((arg << 2) ^ 16) == 36) {
return true;
}
else{
return false;
}
}
int main(){
printf("Hello world!\n");
int g = getchar();
printf("Got: %d\n", g);
int y = 0;
for (int i = 0; i < 20; i++){
y++;
if (y > 18){
return check(g) ? 0 : -1;
}
}
return -1;
}
18:20:54 (mc) ehennenfent@nessie:~/wasm ➜ manticore loop_check.wasm
65: i32.const (16)
11: end
65: i32.const (32)
11: end
Initialization Complete
65: i32.const (0)
65: i32.const (0)
40: i32.load (Offset 4)
65: i32.const (16)
107: i32.sub
34: tee_local (Local 1)
54: i32.store (Offset 4)
65: i32.const (32)
16: call (Func Idx 2)
Called stub function with args: (32,)
HostFunc returned [13]
26: drop
32: get_local (Local 1)
16: call (Func Idx 0)
Called stub function with args: ()
HostFunc returned [13]
34: tee_local (Local 0)
54: i32.store (Offset 0)
65: i32.const (16)
32: get_local (Local 1)
16: call (Func Idx 1)
Called stub function with args: (16, 16)
HostFunc returned [13]
26: drop
65: i32.const (0)
32: get_local (Local 1)
65: i32.const (16)
106: i32.add
54: i32.store (Offset 4)
65: i32.const (-1)
65: i32.const (0)
32: get_local (Local 0)
65: i32.const (1073741823)
113: i32.and
65: i32.const (13)
71: i32.ne
27: select
11: end
[0]
2019-07-25 18:20:58,138: [13842] m.c.manticore:INFO: Generated testcase No. 0 - test
2019-07-25 18:20:58,143: [13842] m.c.manticore:INFO: Results in /home/ehennenfent/wasm/mcore_1evptpip
```
* Improve pickle-friendliness
* Convert wasm module instructions into internal type s
* Fix class naming conflict
Thanks Pickle
* Support bitvecs as an integer type
* Refactor execution to handle single instructions at a time
* Add Atomic Stacks
It's a very inefficient implementation, but we'll make it better
* Handle Concretize Exceptions
Needs a lot of fine tuning but it's a start
* Propagate symbols all the way to return
* Add test generation infrastructure
Still very basic but it's a start
* Attempt to make Travis happy
* Fix missing generation step
* Ignore stack exhaustsion and invalid modules
* Add basic shift instructions
* Add lots of arithmetic instructions
* Fix global initialization
* Add optimization for fixed size left shift
* Make Travis tests less verbose
* Blacken
* Properly handle modulo in shifting
* Add several control flow instructions
* Blacken
* Partially Fix return instruction
* Fix missing return value from look_forward
* Update look_forward to handle nested blocks
* Fix type sigs and executor copy/paste errors, deduplicate test names
* Fix an instruction exit issue
* Fix local ordering
* Blacken
* test_address now passes for wasm
Fixed issue with json2mc.py issue with testname uniqueness
Started some work on floating point
* i hate git
* i hate git
* Adding linenumber to testcase function name
minor float progress
* f32_cmp passed for wasm
* f64_cmp passes for wasm
* f32_bitwise passes except for the problem with assertEqual not working with nan
* f64_bitwise passes except for the problem with assertEqual not working with nan
* Fixed the tests to handle [nan] comparisons
* better handling of nans and infinites
* added integer backing value for f64
* Blacken
* Blacken
* f64.nearest
* Fix argument ordering and implement N-bit memory stores
* Use subtests
* Switch print to logger
* Modify control flow instructions to match spec
"Continuation" != the thing that goes inside the Label
* Teporarily disable call instructions to prevent infinite loop
Still need to figure out control flow for nested function calls
* Blacken
* Fixed infinite loop in _call via block depth tracking
The behavior of `end` needs to depend on how many labels and activations are on the stack. Every time we push or pop a label, we increment or decrement the counter for the current call frame.
* Blacken
* Initial `br` implementation
Probably still bugged
* Support if blocks without an else instruction
* Add call_indirect
* Add grow_memory
* i32_wrap_i64
* Add several arithmetic operators
* Copy i32.ctz impl from x86 TZCNT
* Fix off-by-one in br
I think this is the correct fix (it makes all the _br tests pass) but I'm leaving the TODO there just in case.
* Blacken...
* Fix lingering issue with if block splitting
* Fix bad args to sextend in i64_extend_s_i32
* Fix indirect call null check and Float binop return types
* Improve br_table indexing
See comment
* Remove vestigial argument popping in invoke
* Fix looking forward in _return and _block
* Blacken
* Generate trap and action tests
* Fix extraneous call to exec_for_test
* Fix missing traps in load instructions
* Fix c1=0 case in ctz/clz
Hopefully this also fixes the fact that the original CTZ implementations seems to have been intended for little-endian architectures
* Handle traps in-stride during test execution
* Treat zero division as trap
* Use ctypes to convert integers
* Blacken
* Apparently I was wrong about the CTZ endianness thing
* Better range checks for offsets
This really hammers home why checking whether ea + size is _greater than_ len(mem.data) is strange if mem.data is 0-indexed, but idk, that's what the spec says.
* Make stub functions return the correct number of results
* ctypes can't handle floats
* Add option to run the start function
* Correctly handle functions with unacceptable names
* Blacken
* Treat NotImplemented as a Skip
Also counts individual subtests directly instead of screwing around with trying to grep for them in the log.
* Fix unhandled negative indices
* Fixed some integer instruction bugs
i64/32:
div_s/u
shr_s/u
shl
clz
* Fix global and memory imports
* CC
* Make default loglevel debug
* Rework conversion from int
* Make float_store work
* _actually_ fix float_store
* Restore old float creation method
* Blacken
* Add range checks to float load/store
* Force cast to unsigned in _u instructions
* Fix yet another I32 -> I64 copy paste error
How many times do we have to teach you this lesson, ~old~ young man?
* Skip problematic float tests for now
* Handle floating point inaccuracies when dividing large ints
* Skip conversion and endianness tests
* Rotl, rotl, popcnt
* Blacken
* Add some conversion stubs
* Maybe fix conversions
* Improve reinterpret and float demotion
* Re-enable endianness tests
* Include WASM logging in verbosity controls
* Propagate constraints to executor
* Blacken
* Add prototype symbolic tests
* Add api for invocation to Manticore object
* Temporarily disable native/evm tests
There's no reason to tie up Travis for 40 minutes every time I push a commit.
* Fix wasmworld import
* Concretize br_if
* Pass arg generator to invoke
* Download updated WABT
It's fixed now!
* Generate Symbolic tests (partial)
* Follow m.run format
* Blacken
* Reinitialize Manticore every 50 tests
This should cut down on the performance issues we've been seeing
* Revert "Temporarily disable native/evm tests"
This reverts commit 7abd6c1333.
* I32 and I64 pass Symbolic
* Create new MC on reinit
* Blacken
* Fix return handling in test generation
* Concretize operand in `if_`
* Concretize memory addresses before access
In the long run, we should have a symbolic memory model
* Better memory access concretization (and br_table)
* Concretize float converions and indirect call operands
* Run start method
* Handle size errors in wrap+i64 and select
* Add timeout (and skip extra-problematic tests)
* Blacken
* Aggressively concretize floats
* Recast ints that should have been floats
* Better trap handling
* Unstash float concretization
* Fix log-lived 'trap' attr
* Blacken
* Concretize F32 when promoting to F64
_shouldn't_ strictly be necesssary, but the lazy conversion means that one of the tests fails. Until we get symbolic floating point, this will have to do.
* Docstrings for platforms/wasm.py
* Docstrings for ManticoreWASM
* Add docs for the module structure
* Add WASM to RTD
* missing docstring for Module itself
* Fix sphinx errors
* Bump RTD python version
I'm sorry and I swear I'll fix it later
* Reformat docstrings for sphinx
* Pacify RTD by adding newlines before params
* Missed a few
* Document the types
* Document executor
* A few more type docstrings
* Partial runtime_structure docs
* Finish documenting runtime structure
* Fix trailing whitespace
* Add collatz conjecture example
* Include undocumented members in RTD
* Add a bunch of events
* Appropriately connect event publishers
* Generate useful testcases
* Blacken
* Attempt at 3.6 backporting
* Optimize AtomicStack
* Sort solve_n output in test_solve_n
Very unclear why this is happening, but it's breaking a travis build
https://travis-ci.com/trailofbits/manticore/jobs/246451527
* Roll back to trusty?
Unsure if this is what's causing the EVM issues
* Create custom trap types
* Lazily evaluate overflows
May need to revert this if it slows down the Travis tests. It's 0.7% slower in the example I'm looking at, but that may not carry over.
* Blacken
* Address Dominik's feedback, remove stale TODOs
* Expand & Test Examples
* Fix relative path
* Fix relative path?
* capitalize Path
* Fix relative path for real
* Blacken...
* Test finalize
* Add supplemental tests for coverage
* Use dedicated helper for store access
* Handle quoted strings in imported module names
* Allow WASMWorld to hold multiple modules
Necessary for getting imports/exports to work, but still needs some changes before it passes the tests.
* Actually use export maps
* Fix most of the control flow issues with import tests
* Use available cores for test generation
* Escape module name
* Don't reinit modules that have already been initialized
* Use addresses for imports instead of copying
* Fix removed timeout argument
* Interleave registration and aliases
* Fix broken "None" check for address
* Blacken
* Make manual exports work with address syntax
* Import all registered modules
Only necessary for elem, but it's tricky to make changes just for one test, so *shrug emoji*
* Remove stringified annotations
Turns out those only work in the same file...
* Fix easy type issues in all but runtime structure
* Fix easy type issues in runtime_structure
* Fix* everything but imports
* Blacken
* Blacken 2
* Typo
* Merge module structure and runtime structure
* Fix instance vs type confusion
* Fix mypy and concrete tests
Still an issue with the symbolic tests
* Still generate symbolic tests though
I keep forgetting not to commit that file...
* Attempt to fix env for symbolic tests
* Blacken/mypy
* Fix missing supplemental environment variables arg
* Fix docstrings for new import style
* CC
* Bump timeout duration to 70 minutes
I don't like it, but adding more jobs to Travis isn't going to make our builds any faster
* Fix pycharm type, import, and docstring errors
* Explain type: ignore
* Missed two type:ignore's
* Fix CLI so it doesn't throw up on arbitrary modules
1065 lines
38 KiB
Python
1065 lines
38 KiB
Python
import itertools
|
|
import logging
|
|
import sys
|
|
import time
|
|
import random
|
|
import weakref
|
|
from typing import Callable
|
|
|
|
from contextlib import contextmanager
|
|
|
|
import functools
|
|
import shlex
|
|
|
|
from ..core.plugin import Plugin
|
|
from ..core.smtlib import Expression
|
|
from ..core.state import StateBase
|
|
from ..core.workspace import ManticoreOutput
|
|
from ..exceptions import ManticoreError
|
|
from ..utils import config
|
|
from ..utils.deprecated import deprecated
|
|
from ..utils.event import Eventful
|
|
from ..utils.helpers import PickleSerializer
|
|
from ..utils.log import set_verbosity
|
|
from ..utils.nointerrupt import WithKeyboardInterruptAs
|
|
from .workspace import Workspace
|
|
from .worker import WorkerSingle, WorkerThread, WorkerProcess
|
|
|
|
from multiprocessing.managers import SyncManager
|
|
import threading
|
|
import ctypes
|
|
import signal
|
|
from enum import Enum
|
|
|
|
|
|
class MProcessingType(Enum):
|
|
"""Used as configuration constant for choosing multiprocessing flavor"""
|
|
|
|
multiprocessing = "multiprocessing"
|
|
single = "single"
|
|
threading = "threading"
|
|
|
|
def title(self):
|
|
return self._name_.title()
|
|
|
|
@classmethod
|
|
def from_string(cls, name):
|
|
return cls.__members__[name]
|
|
|
|
def to_class(self):
|
|
return globals()[f"Manticore{self.title()}"]
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
consts = config.get_group("core")
|
|
consts.add("timeout", default=0, description="Timeout, in seconds, for Manticore invocation")
|
|
consts.add(
|
|
"cluster",
|
|
default=False,
|
|
description="If True enables to run workers over the network UNIMPLEMENTED",
|
|
)
|
|
consts.add("procs", default=10, description="Number of parallel processes to spawn")
|
|
consts.add(
|
|
"mprocessing",
|
|
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):
|
|
def __new__(cls, *args, **kwargs):
|
|
if cls in (ManticoreBase, ManticoreSingle, ManticoreThreading, ManticoreMultiprocessing):
|
|
raise ManticoreError("Should not instantiate this")
|
|
|
|
cl = consts.mprocessing.to_class()
|
|
# 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.
|
|
def sync(func: Callable) -> Callable: # type: ignore
|
|
"""Synchronization decorator"""
|
|
|
|
@functools.wraps(func)
|
|
def newFunction(self, *args, **kw):
|
|
with self._lock:
|
|
return func(self, *args, **kw)
|
|
|
|
return newFunction
|
|
|
|
def at_running(func: Callable) -> Callable: # type: ignore
|
|
"""Allows the decorated method to run only when manticore is actively
|
|
exploring states
|
|
"""
|
|
|
|
@functools.wraps(func)
|
|
def newFunction(self, *args, **kw):
|
|
if not self.is_running():
|
|
raise ManticoreError(f"{func.__name__} only allowed while exploring states")
|
|
return func(self, *args, **kw)
|
|
|
|
return newFunction
|
|
|
|
def at_not_running(func: Callable) -> Callable: # type: ignore
|
|
"""Allows the decorated method to run only when manticore is NOT
|
|
exploring states
|
|
"""
|
|
|
|
@functools.wraps(func)
|
|
def newFunction(self, *args, **kw):
|
|
if self.is_running():
|
|
logger.error("Calling at running not allowed")
|
|
raise ManticoreError(f"{func.__name__} only allowed while NOT exploring states")
|
|
return func(self, *args, **kw)
|
|
|
|
return newFunction
|
|
|
|
_published_events = {
|
|
"run",
|
|
"start_worker",
|
|
"terminate_worker",
|
|
"enqueue_state",
|
|
"fork_state",
|
|
"load_state",
|
|
"terminate_state",
|
|
"kill_state",
|
|
"execute_instruction",
|
|
"terminate_execution",
|
|
}
|
|
|
|
def __init__(self, initial_state, workspace_url=None, policy="random", **kwargs):
|
|
"""
|
|
:param initial_state: State to start from.
|
|
|
|
Manticore symbolically explores program states.
|
|
|
|
|
|
**Manticore phases**
|
|
|
|
Manticore has multiprocessing capabilities. Several worker processes
|
|
could be registered to do concurrent exploration of the READY states.
|
|
Manticore can be itself at different phases: STANDBY, RUNNING.
|
|
|
|
.. code-block:: none
|
|
|
|
+---------+ +---------+
|
|
----->| STANDBY +<------------->+ RUNNING |
|
|
+---------+ +----+----+
|
|
|
|
*Phase STANDBY*
|
|
|
|
Manticore starts at STANDBY with a single initial state. Here the user
|
|
can inspect, modify and generate testcases for the different states. The
|
|
workers are paused and not doing any work. Actions: run()
|
|
|
|
|
|
*Phase RUNNING*
|
|
|
|
At RUNNING the workers consume states from the READY state list and
|
|
potentially fork new states or terminate states. A RUNNING manticore can
|
|
be stopped back to STANDBY. Actions: stop()
|
|
|
|
|
|
**States and state lists**
|
|
|
|
A state contains all the information of the running program at a given
|
|
moment. State snapshots are saved to the workspace often. Internally
|
|
Manticore associates a fresh id with each saved state. The memory copy
|
|
of the state is then changed by the emulation of the specific arch.
|
|
Stored snapshots are periodically updated using: _save() and _load().
|
|
|
|
.. code-block:: none
|
|
|
|
_save +-------------+ _load
|
|
State +----------> | WORKSPACE +----------> State
|
|
+-------------+
|
|
|
|
During exploration Manticore spawns a number of temporary states that are
|
|
maintained in different lists:
|
|
|
|
.. code-block:: none
|
|
|
|
Initial
|
|
State
|
|
| +-+---{fork}-----+
|
|
| | | |
|
|
V V V |
|
|
+---------+ +---+----+ +------------+
|
|
| READY +------->| BUSY +----->| TERMINATED |
|
|
+---------+ +---+----+ +------------+
|
|
|
|
|
| +--------+
|
|
+---------------------------->| KILLED |
|
|
+--------+
|
|
|
|
At any given time a state must be at the READY, BUSY, TERMINATED or
|
|
KILLED list.
|
|
|
|
*State list: READY*
|
|
|
|
The READY list holds all the runnable states. Internally a state is
|
|
added to the READY list via method `_put_state(state)`. Workers take
|
|
states from the READY list via the `_get_state(wait=True|False)` method.
|
|
A worker mainloop will consume states from the READY list and mark them
|
|
as BUSYwhile working on them. States in the READY list can go to BUSY or
|
|
KILLED
|
|
|
|
|
|
*State list: BUSY*
|
|
|
|
When a state is selected for exploration from the READY list it is
|
|
marked as busy and put in the BUSY list. States being explored will be
|
|
constantly modified and only saved back to storage when moved out of
|
|
the BUSY list. Hence, when at BUSY the stored copy of the state will be
|
|
potentially outdated. States in the BUSY list can go to TERMINATED,
|
|
KILLED or they can be {forked} back to READY. The forking process
|
|
could involve generating new child states and removing the parent
|
|
from all the lists.
|
|
|
|
|
|
*State list: TERMINATED*
|
|
|
|
TERMINATED contains states that have reached a final condition and raised
|
|
TerminateState. Worker's mainloop simply moves the states that requested
|
|
termination to the TERMINATED list. This is a final list.
|
|
|
|
```An inherited Manticore class like ManticoreEVM could internally revive
|
|
the states in TERMINATED that pass some condition and move them back to
|
|
READY so the user can apply a following transaction.```
|
|
|
|
*State list: KILLED*
|
|
|
|
KILLED contains all the READY and BUSY states found at a cancel event.
|
|
Manticore supports interactive analysis and has a prominetnt event system
|
|
A useror ui can stop or cancel the exploration at any time. The unfinnished
|
|
states cought at this situation are simply moved to its own list for
|
|
further user action. This is a final list.
|
|
|
|
|
|
:param initial_state: the initial root `State` object
|
|
:type state: State
|
|
:param workspace_url: workspace folder name
|
|
:param policy: scheduling policy
|
|
:param kwargs: other kwargs, e.g.
|
|
"""
|
|
super().__init__()
|
|
|
|
if any(
|
|
not hasattr(self, x)
|
|
for x in (
|
|
"_worker_type",
|
|
"_lock",
|
|
"_running",
|
|
"_killed",
|
|
"_ready_states",
|
|
"_terminated_states",
|
|
"_killed_states",
|
|
"_busy_states",
|
|
"_shared_context",
|
|
)
|
|
):
|
|
raise ManticoreError("Need to instantiate one of: ManticoreNative, ManticoreThreads..")
|
|
|
|
# The workspace and the output
|
|
# Manticore will use the workspace to save and share temporary states.
|
|
# Manticore will use the output to save the final reports.
|
|
# By default the output folder and the workspace folder are the same.
|
|
# Check type, default to fs:
|
|
if isinstance(workspace_url, str):
|
|
if ":" not in workspace_url:
|
|
workspace_url = f"fs:{workspace_url}"
|
|
else:
|
|
if workspace_url is not None:
|
|
raise TypeError(f"Invalid workspace type: {type(workspace_url).__name__}")
|
|
self._workspace = Workspace(workspace_url)
|
|
# reuse the same workspace if not specified
|
|
if workspace_url is None:
|
|
workspace_url = f"fs:{self._workspace.uri}"
|
|
self._output = ManticoreOutput(workspace_url)
|
|
|
|
# The set of registered plugins
|
|
# The callback methods defined in the plugin object will be called when
|
|
# the different type of events occur over an exploration.
|
|
# Note that each callback will run in a worker process and that some
|
|
# careful use of the shared context is needed.
|
|
self.plugins = set()
|
|
|
|
# Set initial root state
|
|
if not isinstance(initial_state, StateBase):
|
|
raise TypeError(f"Invalid initial_state type: {type(initial_state).__name__}")
|
|
self._put_state(initial_state)
|
|
|
|
# Workers will use manticore __dict__ So lets spawn them last
|
|
self._workers = [self._worker_type(id=i, manticore=self) for i in range(consts.procs)]
|
|
self._is_main = True
|
|
|
|
def __str__(self):
|
|
return f"<{str(type(self))[8:-2]}| Alive States: {self.count_ready_states()}; Running States: {self.count_busy_states()} Terminated States: {self.count_terminated_states()} Killed States: {self.count_killed_states()} Started: {self._running.value} Killed: {self._killed.value}>"
|
|
|
|
def _fork(self, state, expression, policy="ALL", setstate=None):
|
|
"""
|
|
Fork state on expression concretizations.
|
|
Using policy build a list of solutions for expression.
|
|
For the state on each solution setting the new state with setstate
|
|
|
|
For example if expression is a Bool it may have 2 solutions. True or False.
|
|
|
|
Parent
|
|
(expression = ??)
|
|
|
|
Child1 Child2
|
|
(expression = True) (expression = False)
|
|
setstate(True) setstate(False)
|
|
|
|
The optional setstate() function is supposed to set the concrete value
|
|
in the child state.
|
|
|
|
Parent state is removed from the busy list and tht child states are added
|
|
to the ready list.
|
|
|
|
"""
|
|
assert isinstance(expression, Expression), f"{type(expression)} is not an Expression"
|
|
|
|
if setstate is None:
|
|
|
|
def setstate(x, y):
|
|
pass
|
|
|
|
# Find a set of solutions for expression
|
|
solutions = state.concretize(expression, policy)
|
|
|
|
if not solutions:
|
|
raise ManticoreError("Forking on unfeasible constraint set")
|
|
|
|
logger.debug(
|
|
"Forking. Policy: %s. Values: %s", policy, ", ".join(f"0x{sol:x}" for sol in solutions)
|
|
)
|
|
|
|
self._publish("will_fork_state", state, expression, solutions, policy)
|
|
|
|
# Build and enqueue a state for each solution
|
|
children = []
|
|
for new_value in solutions:
|
|
with state as new_state:
|
|
new_state.constrain(expression == new_value)
|
|
|
|
# and set the PC of the new state to the concrete pc-dest
|
|
# (or other register or memory address to concrete)
|
|
setstate(new_state, new_value)
|
|
|
|
# enqueue new_state, assign new state id
|
|
new_state_id = self._put_state(new_state)
|
|
|
|
# maintain a list of children for logging purpose
|
|
children.append(new_state_id)
|
|
|
|
with self._lock:
|
|
self._busy_states.remove(state.id)
|
|
self._remove(state.id)
|
|
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
|
|
@deprecated("Use utils.log.set_verbosity instead.")
|
|
def verbosity(level):
|
|
""" Sets global vervosity level.
|
|
This will activate different logging profiles globally depending
|
|
on the provided numeric value
|
|
"""
|
|
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.
|
|
|
|
:param state: A manticore State
|
|
:param state_id: if not None force state_id (overwrite)
|
|
:type state_id: int or None
|
|
:returns: the state id used
|
|
"""
|
|
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
|
|
|
|
:param state_id: a estate id
|
|
: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]
|
|
state = self._workspace.load_state(state_id, delete=False)
|
|
state._id = state_id
|
|
self.forward_events_from(state, True)
|
|
self.stcache[state_id] = state
|
|
return state
|
|
|
|
@Eventful.will_did("remove_state")
|
|
def _remove(self, state_id):
|
|
""" Remove a state from secondary storage
|
|
|
|
:param state_id: a estate id
|
|
:type state_id: int
|
|
"""
|
|
if not hasattr(self, "stcache"):
|
|
self.stcache = weakref.WeakValueDictionary()
|
|
if state_id in self.stcache:
|
|
del self.stcache[state_id]
|
|
|
|
self._workspace.rm_state(state_id)
|
|
|
|
# Internal support for state lists
|
|
def _put_state(self, state):
|
|
""" This enqueues the state for exploration.
|
|
|
|
Serialize and store the state with a fresh state_id. Then add it to
|
|
the shared READY states list
|
|
|
|
+-------+
|
|
State +----- >+ READY |
|
|
+-------+
|
|
|
|
"""
|
|
state_id = self._save(state, state_id=state.id)
|
|
with self._lock:
|
|
# Enqueue it in the ready state list for processing
|
|
self._ready_states.append(state_id)
|
|
self._lock.notify_all()
|
|
return state_id
|
|
|
|
def _get_state(self, wait=False):
|
|
""" Dequeue a state form the READY list and add it to the BUSY list """
|
|
with self._lock:
|
|
# If wait is true do the conditional wait for states
|
|
if wait:
|
|
# if not more states in the queue, let's wait for some forks
|
|
while not self._ready_states and not self._killed.value:
|
|
# if a shutdown has been requested then bail
|
|
if self.is_killed():
|
|
return None # Cancelled operation
|
|
# If there are no more READY states and no more BUSY states
|
|
# there is no chance we will get any new state so raise
|
|
if not self._busy_states:
|
|
return None # There are not states
|
|
|
|
# if there ares actually some workers ready, wait for state forks
|
|
logger.debug("Waiting for available states")
|
|
self._lock.wait()
|
|
|
|
if self._killed.value:
|
|
return None
|
|
|
|
# at this point we know there is at least one element
|
|
# and we have exclusive access
|
|
assert self._ready_states
|
|
|
|
# make the choice under exclusive access to the shared ready list
|
|
# state_id = self._policy.choice(list(self._ready_states)[0])
|
|
state_id = random.choice(list(self._ready_states))
|
|
|
|
# Move from READY to BUSY
|
|
self._ready_states.remove(state_id)
|
|
self._busy_states.append(state_id)
|
|
self._lock.notify_all()
|
|
|
|
return self._load(state_id)
|
|
|
|
@sync
|
|
def _revive_state(self, state_id):
|
|
""" Send a BUSY state back to READY list
|
|
|
|
+--------+ +------+
|
|
| READY +<-------+ BUSY |
|
|
+---+----+ +------+
|
|
|
|
"""
|
|
# Move from BUSY to READY
|
|
self._busy_states.remove(state_id)
|
|
self._ready_states.append(state_id)
|
|
self._lock.notify_all()
|
|
|
|
@sync
|
|
def _terminate_state(self, state_id, delete=False):
|
|
""" Send a BUSY state to the TERMINATED list or trash it if delete is True
|
|
|
|
+------+ +------------+
|
|
| BUSY +------->+ TERMINATED |
|
|
+---+--+ +------------+
|
|
|
|
|
v
|
|
###
|
|
###
|
|
|
|
"""
|
|
# wait for a state id to be added to the ready list and remove it
|
|
if state_id not in self._busy_states:
|
|
raise ManticoreError("Can not terminate. State is not being analyzed")
|
|
self._busy_states.remove(state_id)
|
|
|
|
if delete:
|
|
self._remove(state_id)
|
|
else:
|
|
# add the state_id to the terminated list
|
|
self._terminated_states.append(state_id)
|
|
|
|
# wake up everyone waiting for a change in the state lists
|
|
self._lock.notify_all()
|
|
|
|
@sync
|
|
def _kill_state(self, state_id, delete=False):
|
|
""" Send a BUSY state to the KILLED list or trash it if delete is True
|
|
|
|
+------+ +--------+
|
|
| BUSY +------->+ KILLED |
|
|
+---+--+ +--------+
|
|
|
|
|
v
|
|
###
|
|
###
|
|
|
|
"""
|
|
# wait for a state id to be added to the ready list and remove it
|
|
if state_id not in self._busy_states:
|
|
raise ManticoreError("Can not even kill it. State is not being analyzed")
|
|
self._busy_states.remove(state_id)
|
|
|
|
if delete:
|
|
self._remove(state_id)
|
|
else:
|
|
# add the state_id to the terminated list
|
|
self._killed_states.append(state_id)
|
|
|
|
# wake up everyone waiting for a change in the state lists
|
|
self._lock.notify_all()
|
|
|
|
@sync
|
|
def kill_state(self, state, delete=False):
|
|
""" Kill a state.
|
|
A state is moved from any list to the kill list or fully
|
|
removed from secondary storage
|
|
|
|
:param state_id: a estate id
|
|
:type state_id: int
|
|
:param delete: if true remove the state from the secondary storage
|
|
:type delete: bool
|
|
"""
|
|
state_id = state.id
|
|
if state_id in self._busy_states:
|
|
self._busy_states.remove(state_id)
|
|
if state_id in self._terminated_states:
|
|
self._terminated_states.remove(state_id)
|
|
if state_id in self._ready_states:
|
|
self._ready_states.remove(state_id)
|
|
|
|
if delete:
|
|
self._remove(state_id)
|
|
else:
|
|
# add the state_id to the terminated list
|
|
self._killed_states.append(state_id)
|
|
|
|
@property # type: ignore
|
|
@sync
|
|
def ready_states(self):
|
|
"""
|
|
Iterator over ready states.
|
|
It supports state changes. State changes will be saved back at each iteration.
|
|
|
|
The state data change must be done in a loop, e.g. `for state in ready_states: ...`
|
|
as we re-save the state when the generator comes back to the function.
|
|
|
|
This means it is not possible to change the state used by Manticore with `states = list(m.ready_states)`.
|
|
"""
|
|
_ready_states = self._ready_states
|
|
for state_id in _ready_states:
|
|
state = self._load(state_id)
|
|
yield state
|
|
# Re-save the state in case the user changed its data
|
|
self._save(state, state_id=state_id)
|
|
|
|
@property
|
|
def running_states(self):
|
|
logger.warning(
|
|
"manticore.running_states is deprecated! (You probably want manticore.ready_states)"
|
|
)
|
|
return self.ready_states
|
|
|
|
@property # type: ignore
|
|
@sync
|
|
def terminated_states(self):
|
|
"""
|
|
Iterates over the terminated states.
|
|
|
|
See also `ready_states`.
|
|
"""
|
|
for state_id in self._terminated_states:
|
|
state = self._load(state_id)
|
|
yield state
|
|
# Re-save the state in case the user changed its data
|
|
self._save(state, state_id=state_id)
|
|
|
|
@property # type: ignore
|
|
@sync
|
|
@at_not_running
|
|
def killed_states(self):
|
|
"""
|
|
Iterates over the cancelled/killed states.
|
|
|
|
See also `ready_states`.
|
|
"""
|
|
for state_id in self._killed_states:
|
|
state = self._load(state_id)
|
|
yield state
|
|
# Re-save the state in case the user changed its data
|
|
self._save(state, state_id=state_id)
|
|
|
|
@property # type: ignore
|
|
@sync
|
|
@at_not_running
|
|
def _all_states(self):
|
|
""" Only allowed at not running.
|
|
(At running we can have states at busy)
|
|
Returns a tuple with all active state ids.
|
|
Notably the "killed" states are not included here.
|
|
"""
|
|
return tuple(self._ready_states) + tuple(self._terminated_states)
|
|
|
|
@property # type: ignore
|
|
@sync
|
|
def all_states(self):
|
|
"""
|
|
Iterates over the all states (ready and terminated)
|
|
It holds a lock so no changes state lists are allowed
|
|
|
|
Notably the cancelled states are not included here.
|
|
|
|
See also `ready_states`.
|
|
"""
|
|
for state_id in self._all_states:
|
|
state = self._load(state_id)
|
|
yield state
|
|
# Re-save the state in case the user changed its data
|
|
self._save(state, state_id=state_id)
|
|
|
|
@sync
|
|
def count_states(self):
|
|
""" Total states count """
|
|
return len(self._all_states)
|
|
|
|
@sync
|
|
def count_all_states(self):
|
|
""" Total states count """
|
|
return self.count_states()
|
|
|
|
@sync
|
|
def count_ready_states(self):
|
|
""" Ready states count """
|
|
return len(self._ready_states)
|
|
|
|
@sync
|
|
def count_busy_states(self):
|
|
""" Busy states count """
|
|
return len(self._busy_states)
|
|
|
|
@sync
|
|
def count_killed_states(self):
|
|
""" Cancelled states count """
|
|
return len(self._killed_states)
|
|
|
|
@sync
|
|
def count_terminated_states(self):
|
|
""" Terminated states count """
|
|
return len(self._terminated_states)
|
|
|
|
def generate_testcase(self, state, message="test", name="test"):
|
|
if message == "test" and hasattr(state, "_terminated_by") and state._terminated_by:
|
|
message = str(state._terminated_by)
|
|
testcase = self._output.testcase(prefix=name)
|
|
with testcase.open_stream("pkl", binary=True) as statef:
|
|
PickleSerializer().serialize(state, statef)
|
|
|
|
# Let the plugins generate a state based report
|
|
for p in self.plugins:
|
|
p.generate_testcase(state, testcase, message)
|
|
|
|
logger.info("Generated testcase No. %d - %s", testcase.num, message)
|
|
return testcase
|
|
|
|
@at_not_running
|
|
def register_plugin(self, plugin):
|
|
# Global enumeration of valid events
|
|
assert isinstance(plugin, Plugin)
|
|
assert plugin not in self.plugins, "Plugin instance already registered"
|
|
assert getattr(plugin, "manticore", None) is None, "Plugin instance already owned"
|
|
|
|
plugin.manticore = self
|
|
self.plugins.add(plugin)
|
|
|
|
events = Eventful.all_events()
|
|
prefix = Eventful.prefixes
|
|
all_events = [x + y for x, y in itertools.product(prefix, events)]
|
|
for event_name in all_events:
|
|
callback_name = f"{event_name}_callback"
|
|
callback = getattr(plugin, callback_name, None)
|
|
if callback is not None:
|
|
self.subscribe(event_name, callback)
|
|
|
|
# Safety checks
|
|
for callback_name in dir(plugin):
|
|
if callback_name.endswith("_callback"):
|
|
event_name = callback_name[:-9]
|
|
if event_name not in all_events:
|
|
logger.warning(
|
|
"There is no event named %s for callback on plugin %s",
|
|
event_name,
|
|
type(plugin).__name__,
|
|
)
|
|
|
|
for event_name in all_events:
|
|
for plugin_method_name in dir(plugin):
|
|
if event_name in plugin_method_name:
|
|
if not plugin_method_name.endswith("_callback"):
|
|
if (
|
|
plugin_method_name.startswith("on_")
|
|
or plugin_method_name.startswith("will_")
|
|
or plugin_method_name.startswith("did_")
|
|
):
|
|
logger.warning(
|
|
"Plugin methods named '%s()' should end with '_callback' on plugin %s",
|
|
plugin_method_name,
|
|
type(plugin).__name__,
|
|
)
|
|
if (
|
|
plugin_method_name.endswith("_callback")
|
|
and not plugin_method_name.startswith("on_")
|
|
and not plugin_method_name.startswith("will_")
|
|
and not plugin_method_name.startswith("did_")
|
|
):
|
|
logger.warning(
|
|
"Plugin methods named '%s()' should start with 'on_', 'will_' or 'did_' on plugin %s",
|
|
plugin_method_name,
|
|
type(plugin).__name__,
|
|
)
|
|
|
|
plugin.on_register()
|
|
return plugin
|
|
|
|
@at_not_running
|
|
def unregister_plugin(self, plugin):
|
|
""" Removes a plugin from manticore.
|
|
No events should be sent to it after
|
|
"""
|
|
assert plugin in self.plugins, "Plugin instance not registered"
|
|
plugin.on_unregister()
|
|
self.plugins.remove(plugin)
|
|
plugin.manticore = None
|
|
|
|
def subscribe(self, name, callback):
|
|
""" Register a callback to an event"""
|
|
from types import MethodType
|
|
|
|
if not isinstance(callback, MethodType):
|
|
callback = MethodType(callback, self)
|
|
super().subscribe(name, callback)
|
|
|
|
@property # type: ignore
|
|
@at_not_running
|
|
def context(self):
|
|
""" Convenient access to shared context. We maintain a local copy of the
|
|
share context during the time manticore is not running.
|
|
This local context is copied to the shared context when a run starts
|
|
and copied back when a run finishes
|
|
"""
|
|
return self._shared_context
|
|
|
|
@contextmanager
|
|
@sync
|
|
def locked_context(self, key=None, value_type=list):
|
|
"""
|
|
A context manager that provides safe parallel access to the global
|
|
Manticore context. This should be used to access the global Manticore
|
|
context when parallel analysis is activated. Code within the `with` block
|
|
is executed atomically, so access of shared variables should occur within.
|
|
|
|
Example use::
|
|
|
|
with m.locked_context() as context:
|
|
visited['visited'].append(state.cpu.PC)
|
|
|
|
Optionally, parameters can specify a key and type for the object paired to this key.::
|
|
|
|
with m.locked_context('feature_list', list) as feature_list:
|
|
feature_list.append(1)
|
|
|
|
Note: If standard (non-proxy) list or dict objects are contained in a
|
|
referent, modifications to those mutable values will not be propagated
|
|
through the manager because the proxy has no way of knowing when the
|
|
values contained within are modified. However, storing a value in a
|
|
container proxy (which triggers a __setitem__ on the proxy object) does
|
|
propagate through the manager and so to effectively modify such an item,
|
|
one could re-assign the modified value to the container proxy:
|
|
|
|
:param object key: Storage key
|
|
:param value_type: type of value associated with key
|
|
:type value_type: list or dict or set
|
|
"""
|
|
|
|
if key is None:
|
|
# If no key is provided we yield the raw shared context under a lock
|
|
yield self._shared_context
|
|
else:
|
|
# if a key is provided we yield the specific value or a fresh one
|
|
if value_type not in (list, dict):
|
|
raise TypeError("Type must be list or dict")
|
|
if hasattr(self, "_context_value_types"):
|
|
value_type = self._context_value_types[value_type]
|
|
context = self._shared_context
|
|
if key not in context:
|
|
context[key] = value_type()
|
|
yield context[key]
|
|
|
|
############################################################################
|
|
# Public API
|
|
|
|
@sync
|
|
def wait(self, condition):
|
|
""" Waits for the condition callable to return True """
|
|
self._lock.wait_for(condition)
|
|
|
|
@sync
|
|
def kill(self):
|
|
""" Attempt to cancel and kill all the workers.
|
|
Workers must terminate
|
|
RUNNING, STANDBY -> KILLED
|
|
"""
|
|
self._publish("will_terminate_execution", self._output)
|
|
self._killed.value = True
|
|
self._lock.notify_all()
|
|
self._publish("did_terminate_execution", self._output)
|
|
|
|
def terminate(self):
|
|
logger.warning("manticore.terminate is deprecated (Use manticore.kill)")
|
|
self.kill()
|
|
|
|
@sync
|
|
def is_running(self):
|
|
""" True if workers are exploring BUSY states or waiting for READY states """
|
|
# If there are still states in the BUSY list then the STOP/KILL event
|
|
# was not yet answered
|
|
# We know that BUSY states can only decrease after a stop is requested
|
|
return self._running.value
|
|
|
|
@sync
|
|
def is_killed(self):
|
|
""" True if workers are killed. It is safe to join them """
|
|
# If there are still states in the BUSY list then the STOP/KILL event
|
|
# was not yet answered
|
|
# We know that BUSY states can only decrease after a kill is requested
|
|
return self._killed.value
|
|
|
|
@property
|
|
def workspace(self):
|
|
return self._output.store.uri
|
|
|
|
@contextmanager
|
|
def kill_timeout(self, timeout=None):
|
|
""" A convenient context manager that will kill a manticore run after
|
|
timeout seconds
|
|
"""
|
|
if timeout is None:
|
|
timeout = consts.timeout
|
|
|
|
# Run forever is timeout is negative
|
|
if timeout <= 0:
|
|
try:
|
|
yield
|
|
finally:
|
|
return
|
|
|
|
# THINKME kill grabs the lock. Is npt this a deadlock hazard?
|
|
timer = threading.Timer(timeout, self.kill)
|
|
timer.start()
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
timer.cancel()
|
|
|
|
@at_not_running
|
|
def run(self):
|
|
"""
|
|
Runs analysis.
|
|
"""
|
|
# Delete state cache
|
|
# The cached version of a state may get out of sync if a worker in a
|
|
# different process modifies the state
|
|
self.stcache = weakref.WeakValueDictionary()
|
|
|
|
# Lazy process start. At the first run() the workers are not forked.
|
|
# This actually starts the worker procs/threads
|
|
if self.subscribe:
|
|
# User subscription to events is disabled from now on
|
|
self.subscribe = None
|
|
|
|
self._publish("will_run", self.ready_states)
|
|
self._running.value = True
|
|
# start all the workers!
|
|
for w in self._workers:
|
|
w.start()
|
|
|
|
# Main process. Lets just wait and capture CTRL+C at main
|
|
with WithKeyboardInterruptAs(self.kill):
|
|
with self._lock:
|
|
while (self._busy_states or self._ready_states) and not self._killed.value:
|
|
self._lock.wait()
|
|
|
|
# Join all the workers!
|
|
for w in self._workers:
|
|
w.join()
|
|
|
|
with self._lock:
|
|
assert not self._busy_states and not self._ready_states or self._killed.value
|
|
|
|
if self.is_killed():
|
|
logger.debug("Killed. Moving all remaining ready states to killed list")
|
|
# move all READY to KILLED:
|
|
while self._ready_states:
|
|
self._killed_states.append(self._ready_states.pop())
|
|
|
|
self._running.value = False
|
|
self._publish("did_run")
|
|
assert not self.is_running()
|
|
|
|
@sync
|
|
@at_not_running
|
|
def remove_all(self):
|
|
"""
|
|
Deletes all streams from storage and clean state lists
|
|
"""
|
|
for state_id in self._all_states:
|
|
self._remove(state_id)
|
|
|
|
del self._ready_states[:]
|
|
del self._busy_states[:]
|
|
del self._terminated_states[:]
|
|
del self._killed_states[:]
|
|
|
|
def finalize(self):
|
|
"""
|
|
Generate a report testcase for every state in the system and remove
|
|
all temporary files/streams from the workspace
|
|
"""
|
|
self.kill()
|
|
for state in self.all_states:
|
|
self.generate_testcase(state)
|
|
self.remove_all()
|
|
|
|
############################################################################
|
|
############################################################################
|
|
############################################################################
|
|
############################################################################
|
|
############################################################################
|
|
############################################################################
|
|
|
|
def save_run_data(self):
|
|
with self._output.save_stream("command.sh") as f:
|
|
f.write(" ".join(map(shlex.quote, sys.argv)))
|
|
|
|
with self._output.save_stream("manticore.yml") as f:
|
|
config.save(f)
|
|
|
|
logger.info("Results in %s", self._output.store.uri)
|
|
|
|
|
|
class ManticoreSingle(ManticoreBase):
|
|
_worker_type = WorkerSingle
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
class FakeLock:
|
|
def _nothing(self, *args, **kwargs):
|
|
pass
|
|
|
|
acquire = _nothing
|
|
release = _nothing
|
|
__enter__ = _nothing
|
|
__exit__ = _nothing
|
|
notify_all = _nothing
|
|
wait = _nothing
|
|
|
|
def wait_for(self, condition, *args, **kwargs):
|
|
if not condition():
|
|
raise Exception("Deadlock: Waiting for CTRL+C")
|
|
|
|
self._lock = FakeLock()
|
|
self._killed = ctypes.c_bool(False)
|
|
self._running = ctypes.c_bool(False)
|
|
|
|
self._ready_states = []
|
|
self._terminated_states = []
|
|
self._busy_states = []
|
|
self._killed_states = []
|
|
|
|
self._shared_context = {}
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
class ManticoreThreading(ManticoreBase):
|
|
_worker_type = WorkerThread
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self._lock = threading.Condition()
|
|
self._killed = ctypes.c_bool(False)
|
|
self._running = ctypes.c_bool(False)
|
|
|
|
self._ready_states = []
|
|
self._terminated_states = []
|
|
self._busy_states = []
|
|
self._killed_states = []
|
|
|
|
self._shared_context = {}
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
class ManticoreMultiprocessing(ManticoreBase):
|
|
_worker_type = WorkerProcess
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
# This is the global manager that will handle all shared memory access
|
|
# See. https://docs.python.org/3/library/multiprocessing.html#multiprocessing.managers.SyncManager
|
|
self._manager = SyncManager()
|
|
self._manager.start(lambda: signal.signal(signal.SIGINT, signal.SIG_IGN))
|
|
# The main manticore lock. Acquire this for accessing shared objects
|
|
# THINKME: we use the same lock to access states lists and shared contexts
|
|
self._lock = self._manager.Condition()
|
|
self._killed = self._manager.Value(bool, False)
|
|
self._running = self._manager.Value(bool, False)
|
|
|
|
# List of state ids of States on storage
|
|
self._ready_states = self._manager.list()
|
|
self._terminated_states = self._manager.list()
|
|
self._busy_states = self._manager.list()
|
|
self._killed_states = self._manager.list()
|
|
self._shared_context = self._manager.dict()
|
|
self._context_value_types = {list: self._manager.list, dict: self._manager.dict}
|
|
|
|
super().__init__(*args, **kwargs)
|