mirror of
https://github.com/trailofbits/manticore.git
synced 2026-10-11 22:29:55 +00:00
WASM Support MVP (#1495)
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
This commit is contained in:
+3
-2
@@ -15,13 +15,14 @@ stages:
|
||||
env:
|
||||
global:
|
||||
- CC_TEST_REPORTER_ID=db72f1ed59628c16eb0c00cbcd629c4c71f68aa1892ef42d18c7c2b8326f460a
|
||||
- JOB_COUNT=4 # Four jobs generate test coverage: ethereum, ethereum_vm, native, and other
|
||||
- JOB_COUNT=5 # Five jobs generate test coverage: ethereum, ethereum_vm, native, wasm, and other
|
||||
- PYTHONWARNINGS="default::ResourceWarning" # Enable ResourceWarnings
|
||||
matrix:
|
||||
- TEST_TYPE=examples
|
||||
- TEST_TYPE=ethereum
|
||||
- TEST_TYPE=ethereum_vm
|
||||
- TEST_TYPE=native
|
||||
- TEST_TYPE=wasm
|
||||
- TEST_TYPE=other
|
||||
|
||||
branches:
|
||||
@@ -61,7 +62,7 @@ install:
|
||||
- scripts/travis_install.sh $TEST_TYPE
|
||||
|
||||
script:
|
||||
- travis_wait 60 scripts/travis_test.sh $TEST_TYPE
|
||||
- travis_wait 70 scripts/travis_test.sh $TEST_TYPE
|
||||
|
||||
after_success:
|
||||
- ./cc-test-reporter format-coverage -t coverage.py -o "coverage/codeclimate.$TEST_TYPE.json"
|
||||
|
||||
@@ -14,6 +14,7 @@ Manticore is a symbolic execution tool for analysis of binaries and smart contra
|
||||
states
|
||||
evm
|
||||
native
|
||||
wasm
|
||||
plugins
|
||||
gotchas
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
Web Assembly
|
||||
------------
|
||||
|
||||
|
||||
ManticoreWASM
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
.. automodule:: manticore.wasm.manticore
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
WASM World
|
||||
^^^^^^^^^^
|
||||
|
||||
.. automodule:: manticore.platforms.wasm
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Executor
|
||||
^^^^^^^^
|
||||
|
||||
.. automodule:: manticore.wasm.executor
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Module Structure
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. automodule:: manticore.wasm.structure
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
|
||||
Types
|
||||
^^^^^
|
||||
|
||||
.. automodule:: manticore.wasm.types
|
||||
:members:
|
||||
:undoc-members:
|
||||
@@ -0,0 +1,15 @@
|
||||
int collatz(int x){
|
||||
if (x <= 1){
|
||||
return 0;
|
||||
}
|
||||
if (x % 2 == 0){
|
||||
return collatz(x / 2) + 1;
|
||||
}
|
||||
else{
|
||||
return collatz(3 * x + 1) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
return collatz(getchar(">"));
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Three different ways of evaluating the Collatz conjecture.
|
||||
|
||||
Example 1: Pass a symbolic argument to the `collatz` function
|
||||
Example 2: Pass a symbolic version of `getchar` as an imported function
|
||||
Example 3: Concretely evaluate `collatz(1337)` and count the instructions executed
|
||||
|
||||
See: examples/collatz.c
|
||||
"""
|
||||
|
||||
from manticore.wasm import ManticoreWASM
|
||||
from manticore.wasm.types import I32
|
||||
from manticore.core.plugin import Plugin
|
||||
from manticore.utils.log import set_verbosity
|
||||
|
||||
|
||||
print(
|
||||
"""
|
||||
|
||||
============ Example 1 ============
|
||||
"""
|
||||
)
|
||||
|
||||
m = ManticoreWASM("collatz.wasm")
|
||||
set_verbosity(2)
|
||||
|
||||
|
||||
def arg_gen(state):
|
||||
# Generate a symbolic argument to pass to the collatz function.
|
||||
# Possible values: 4, 6, 8
|
||||
arg = state.new_symbolic_value(32, "collatz_arg")
|
||||
state.constrain(arg > 3)
|
||||
state.constrain(arg < 9)
|
||||
state.constrain(arg % 2 == 0)
|
||||
return [arg]
|
||||
|
||||
|
||||
# Set up Manticore to run the collatz function with the given argument generator.
|
||||
# We use an argument generator function instead of a list of arguments because Manticore
|
||||
# might have multiple states waiting to begin execution, and we can conveniently map a
|
||||
# generator function over all the ready states and get access to their respective
|
||||
# constraint sets.
|
||||
m.invoke("collatz", arg_gen)
|
||||
|
||||
# Run the collatz function
|
||||
m.run()
|
||||
|
||||
# Manually collect return values
|
||||
for idx, val_list in enumerate(m.collect_returns()):
|
||||
print("State", idx, "::", val_list[0])
|
||||
|
||||
# Finalize the run (dump testcases)
|
||||
m.finalize()
|
||||
|
||||
|
||||
print(
|
||||
"""
|
||||
|
||||
============ Example 2 ============
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def getchar(constraints, _addr):
|
||||
"""
|
||||
Stub implementation of the getchar function. All WASM cares about is that it accepts the right
|
||||
number of arguments and returns the correct type. All _we_ care about is that it returns a symbolic
|
||||
value, for which Manticore will produce all possible outputs.
|
||||
|
||||
:param constraints: The current constraint set
|
||||
:param _addr: Memory index of the string that gets printed by getchar
|
||||
:return: A symbolic value of the interval [1, 7]
|
||||
"""
|
||||
res = constraints.new_bitvec(32, "getchar_res")
|
||||
constraints.add(res > 0)
|
||||
constraints.add(res < 8)
|
||||
return [res]
|
||||
|
||||
|
||||
# Pass our symbolic implementation of the `getchar` function into the WASM environment
|
||||
# as an import.
|
||||
m = ManticoreWASM("collatz.wasm", env={"getchar": getchar})
|
||||
|
||||
# Invoke the main function, which will call getchar
|
||||
m.invoke("main")
|
||||
|
||||
# Run the example
|
||||
m.run()
|
||||
|
||||
# Manually collect return values
|
||||
for idx, val_list in enumerate(m.collect_returns()):
|
||||
print("State", idx, "::", val_list[0])
|
||||
|
||||
# Finalize the run (dump testcases)
|
||||
m.finalize()
|
||||
|
||||
|
||||
print(
|
||||
"""
|
||||
|
||||
============ Example 3 ============
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class CounterPlugin(Plugin):
|
||||
"""
|
||||
A plugin that counts the number of times each instruction occurs
|
||||
"""
|
||||
|
||||
def did_execute_instruction_callback(self, state, instruction):
|
||||
with self.locked_context("counter", dict) as ctx:
|
||||
val = ctx.setdefault(instruction.mnemonic, 0)
|
||||
ctx[instruction.mnemonic] = val + 1
|
||||
|
||||
def did_terminate_state_callback(self, state, *args):
|
||||
insn_sum = 0
|
||||
with self.locked_context("counter") as ctx:
|
||||
for mnemonic, count in sorted(ctx.items(), key=lambda x: x[1], reverse=True):
|
||||
print("{: <10} {: >4}".format(mnemonic, count))
|
||||
insn_sum += count
|
||||
print(insn_sum, "instructions executed")
|
||||
|
||||
|
||||
def arg_gen(_state):
|
||||
return [I32(1337)]
|
||||
|
||||
|
||||
m = ManticoreWASM("collatz.wasm")
|
||||
|
||||
# Registering the plugin connects its callbacks to the correct events
|
||||
m.register_plugin(CounterPlugin())
|
||||
|
||||
# Invoke `collatz(1337)`
|
||||
m.invoke("collatz", arg_gen)
|
||||
|
||||
# Run the collatz function
|
||||
m.run()
|
||||
|
||||
# Manually collect return values
|
||||
for idx, val_list in enumerate(m.collect_returns()):
|
||||
print("State", idx, "::", val_list[0])
|
||||
|
||||
# Finalize the run (dump testcases)
|
||||
m.finalize()
|
||||
Binary file not shown.
@@ -0,0 +1,116 @@
|
||||
(module
|
||||
(type $FUNCSIG$i (func (result i32)))
|
||||
(type $FUNCSIG$ii (func (param i32) (result i32)))
|
||||
(import "env" "getchar" (func $getchar (param i32) (result i32)))
|
||||
(table 0 anyfunc)
|
||||
(memory $0 1)
|
||||
(data (i32.const 16) ">\00")
|
||||
(export "memory" (memory $0))
|
||||
(export "collatz" (func $collatz))
|
||||
(export "main" (func $main))
|
||||
(func $collatz (; 1 ;) (param $0 i32) (result i32)
|
||||
(local $1 i32)
|
||||
(set_local $1
|
||||
(i32.const 0)
|
||||
)
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.lt_s
|
||||
(get_local $0)
|
||||
(i32.const 2)
|
||||
)
|
||||
)
|
||||
(set_local $1
|
||||
(i32.const 0)
|
||||
)
|
||||
(loop $label$1
|
||||
(set_local $1
|
||||
(i32.add
|
||||
(get_local $1)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
(br_if $label$1
|
||||
(i32.gt_s
|
||||
(tee_local $0
|
||||
(select
|
||||
(i32.add
|
||||
(i32.mul
|
||||
(get_local $0)
|
||||
(i32.const 3)
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.shr_u
|
||||
(get_local $0)
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.and
|
||||
(get_local $0)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(get_local $1)
|
||||
)
|
||||
(func $main (; 2 ;) (result i32)
|
||||
(local $0 i32)
|
||||
(local $1 i32)
|
||||
(set_local $1
|
||||
(i32.const 0)
|
||||
)
|
||||
(block $label$0
|
||||
(br_if $label$0
|
||||
(i32.lt_s
|
||||
(tee_local $0
|
||||
(call $getchar
|
||||
(i32.const 16)
|
||||
)
|
||||
)
|
||||
(i32.const 2)
|
||||
)
|
||||
)
|
||||
(set_local $1
|
||||
(i32.const 0)
|
||||
)
|
||||
(loop $label$1
|
||||
(set_local $1
|
||||
(i32.add
|
||||
(get_local $1)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
(br_if $label$1
|
||||
(i32.gt_s
|
||||
(tee_local $0
|
||||
(select
|
||||
(i32.add
|
||||
(i32.mul
|
||||
(get_local $0)
|
||||
(i32.const 3)
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.shr_u
|
||||
(get_local $0)
|
||||
(i32.const 1)
|
||||
)
|
||||
(i32.and
|
||||
(get_local $0)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
(get_local $1)
|
||||
)
|
||||
)
|
||||
@@ -10,6 +10,7 @@ import pkg_resources
|
||||
from crytic_compile import is_supported, cryticparser
|
||||
from .core.manticore import ManticoreBase, set_verbosity
|
||||
from .ethereum.cli import ethereum_main
|
||||
from .wasm.cli import wasm_main
|
||||
from .utils import config, log, install_helper
|
||||
|
||||
consts = config.get_group("main")
|
||||
@@ -39,6 +40,8 @@ def main():
|
||||
|
||||
if args.argv[0].endswith(".sol") or is_supported(args.argv[0]):
|
||||
ethereum_main(args, logger)
|
||||
elif args.argv[0].endswith(".wasm") or args.argv[0].endswith(".wat"):
|
||||
wasm_main(args, logger)
|
||||
else:
|
||||
install_helper.ensure_native_deps()
|
||||
native_main(args, logger)
|
||||
|
||||
@@ -327,7 +327,7 @@ class ManticoreBase(Eventful):
|
||||
to the ready list.
|
||||
|
||||
"""
|
||||
assert isinstance(expression, Expression)
|
||||
assert isinstance(expression, Expression), f"{type(expression)} is not an Expression"
|
||||
|
||||
if setstate is None:
|
||||
|
||||
|
||||
@@ -248,6 +248,8 @@ def SREM(a, b):
|
||||
return a.srem(b)
|
||||
elif isinstance(b, BitVec):
|
||||
return b.rsrem(a)
|
||||
elif isinstance(a, int) and isinstance(b, int):
|
||||
return a - int(a / b) * b
|
||||
return a % b
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
from .platform import Platform
|
||||
from ..wasm.structure import (
|
||||
ModuleInstance,
|
||||
Store,
|
||||
FuncAddr,
|
||||
HostFunc,
|
||||
Stack,
|
||||
ProtoFuncInst,
|
||||
MemInst,
|
||||
MemAddr,
|
||||
GlobalInst,
|
||||
GlobalAddr,
|
||||
TableInst,
|
||||
TableAddr,
|
||||
ExternVal,
|
||||
Module,
|
||||
)
|
||||
from ..wasm.types import Trap, TypeIdx, TableType, MemoryType, GlobalType, MissingExportException
|
||||
from ..core.state import TerminateState
|
||||
from ..core.smtlib import ConstraintSet
|
||||
from functools import partial
|
||||
import typing
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
# logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def stub(arity, _constraints, *args):
|
||||
"""Default function used for hostfunc calls when a proper import wasn't provided """
|
||||
logger.info("Called stub function with args: %s", args)
|
||||
return [0 for _ in range(arity)]
|
||||
|
||||
|
||||
class WASMWorld(Platform):
|
||||
"""Manages global environment for a WASM state. Analagous to EVMWorld."""
|
||||
|
||||
def __init__(self, filename, name="self", **kwargs):
|
||||
"""
|
||||
:param filename: The WASM module to execute
|
||||
:param kwargs: Accepts "constraints" to pass in an initial ConstraintSet
|
||||
"""
|
||||
super().__init__(filename, **kwargs)
|
||||
#: Initial set of constraints
|
||||
self.constraints = kwargs.get("constraints", ConstraintSet())
|
||||
#: Prevents users from calling run without instantiating the module
|
||||
self.instantiated = []
|
||||
#: Backing store for functions, memories, tables, and globals
|
||||
self.store = Store()
|
||||
|
||||
self.modules = []
|
||||
self.module_names = {}
|
||||
self.manual_exports = {}
|
||||
self.default_module = name
|
||||
self.register_module(name, filename)
|
||||
|
||||
#: Stores numeric values, branch labels, and execution frames
|
||||
self.stack = Stack()
|
||||
|
||||
self.forward_events_from(self.stack)
|
||||
self.forward_events_from(self.instance)
|
||||
self.forward_events_from(self.instance.executor)
|
||||
|
||||
def __getstate__(self):
|
||||
state = super().__getstate__()
|
||||
state["modules"] = self.modules
|
||||
state["store"] = self.store
|
||||
state["stack"] = self.stack
|
||||
state["constraints"] = self.constraints
|
||||
state["instantiated"] = self.instantiated
|
||||
state["module_names"] = self.module_names
|
||||
state["default_module"] = self.default_module
|
||||
state["manual_exports"] = self.manual_exports
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.modules = state["modules"]
|
||||
self.store = state["store"]
|
||||
self.stack = state["stack"]
|
||||
self.constraints = state["constraints"]
|
||||
self.instantiated = state["instantiated"]
|
||||
self.module_names = state["module_names"]
|
||||
self.default_module = state["default_module"]
|
||||
self.manual_exports = state["manual_exports"]
|
||||
self.forward_events_from(self.stack)
|
||||
self.forward_events_from(self.instance)
|
||||
self.forward_events_from(self.instance.executor)
|
||||
super().__setstate__(state)
|
||||
|
||||
@property
|
||||
def instance(self) -> ModuleInstance:
|
||||
"""
|
||||
:return: the ModuleInstance for the first module registered
|
||||
"""
|
||||
return self.modules[self.module_names[self.default_module]][1]
|
||||
|
||||
@property
|
||||
def module(self) -> Module:
|
||||
"""
|
||||
:return: The first module registered
|
||||
"""
|
||||
return self.modules[self.module_names[self.default_module]][0]
|
||||
|
||||
def register_module(self, name, filename_or_alias):
|
||||
"""
|
||||
Provide an explicit path to a WASM module so the importer will know where to find it
|
||||
|
||||
:param name: Module name to register the module under
|
||||
:param filename_or_alias: Name of the .wasm file that module lives in
|
||||
:return:
|
||||
"""
|
||||
if filename_or_alias in self.module_names:
|
||||
self.module_names[name] = self.module_names[filename_or_alias]
|
||||
if name not in self.module_names:
|
||||
self.modules.append((Module.load(filename_or_alias), ModuleInstance(self.constraints)))
|
||||
self.module_names[name] = len(self.modules) - 1
|
||||
self.instantiated.append(False)
|
||||
|
||||
def set_env(
|
||||
self,
|
||||
exports: typing.Dict[str, typing.Union[ProtoFuncInst, TableInst, MemInst, GlobalInst]],
|
||||
mod_name="env",
|
||||
):
|
||||
"""
|
||||
Manually insert exports into the global environment
|
||||
|
||||
:param exports: Dict mapping names to functions/tables/globals/memories
|
||||
:param mod_name: The name of the module these exports should fall under
|
||||
"""
|
||||
self.manual_exports.setdefault(mod_name, {}).update(exports)
|
||||
|
||||
def import_module(self, module_name, exec_start, stub_missing):
|
||||
"""
|
||||
Collect all of the imports for the given module and instantiate it
|
||||
|
||||
:param module_name: module to import
|
||||
:param exec_start: whether to run the start functions automatically
|
||||
:param stub_missing: whether to replace missing imports with stubs
|
||||
:return: None
|
||||
"""
|
||||
search_paths = {"."}
|
||||
# If the module isn't registered, look for it on the filesystem
|
||||
if module_name not in self.module_names:
|
||||
logger.debug("Module %s was not provided, attempting to load from disk", module_name)
|
||||
for pth in search_paths:
|
||||
possible_path = os.path.join(pth, module_name + ".wasm")
|
||||
if os.path.isfile(possible_path):
|
||||
self.register_module(module_name, possible_path)
|
||||
break
|
||||
else:
|
||||
raise RuntimeError("Missing imported module: " + module_name)
|
||||
|
||||
if self.instantiated[self.module_names[module_name]]:
|
||||
return
|
||||
|
||||
# Get the module and the instance from the world
|
||||
module, instance = self.modules[self.module_names[module_name]]
|
||||
|
||||
imports = self.get_module_imports(module, exec_start, stub_missing)
|
||||
|
||||
instance.instantiate(self.store, module, imports, exec_start)
|
||||
self.instantiated[self.module_names[module_name]] = True
|
||||
logger.info("Imported %s", module_name)
|
||||
|
||||
def _get_export_addr(
|
||||
self, export_name, mod_name=None
|
||||
) -> typing.Optional[typing.Union[FuncAddr, TableAddr, MemAddr, GlobalAddr]]:
|
||||
"""
|
||||
Gets the address in the store of a given export
|
||||
|
||||
:param export_name: Name of the export to look for
|
||||
:param mod_name: Name of the module the export lives in
|
||||
:return: The address of the export
|
||||
"""
|
||||
try:
|
||||
if mod_name in self.module_names: # TODO - handle mod_name.export_name
|
||||
return self.modules[self.module_names[mod_name]][1].get_export_address(export_name)
|
||||
except MissingExportException as exc:
|
||||
logger.error("Couldn't find export %s.%s", mod_name, exc.name)
|
||||
return None
|
||||
|
||||
def get_export(
|
||||
self, export_name, mod_name=None
|
||||
) -> typing.Optional[typing.Union[ProtoFuncInst, TableInst, MemInst, GlobalInst]]:
|
||||
"""
|
||||
Gets the export _instance_ for a given export & module name
|
||||
(basically just dereferences _get_export_addr into the store)
|
||||
|
||||
:param export_name: Name of the export to look for
|
||||
:param mod_name: Name of the module the export lives in
|
||||
:return: The export itself
|
||||
"""
|
||||
mod_name = self.default_module if not mod_name else mod_name
|
||||
if mod_name in self.manual_exports:
|
||||
if export_name in self.manual_exports[mod_name]:
|
||||
return self.manual_exports[mod_name][export_name]
|
||||
addr = self._get_export_addr(export_name, mod_name)
|
||||
if addr is not None:
|
||||
if isinstance(addr, FuncAddr):
|
||||
return self.store.funcs[addr]
|
||||
if isinstance(addr, TableAddr):
|
||||
return self.store.funcs[addr]
|
||||
if isinstance(addr, MemAddr):
|
||||
return self.store.mems[addr]
|
||||
if isinstance(addr, GlobalAddr):
|
||||
return self.store.globals[addr]
|
||||
|
||||
return None
|
||||
|
||||
def get_module_imports(self, module, exec_start, stub_missing) -> typing.List[ExternVal]:
|
||||
"""
|
||||
Builds the list of imports that should be passed to the given module upon instantiation
|
||||
|
||||
:param module: The module to find the imports for
|
||||
:param exec_start: Whether to execute the start function of the module
|
||||
:param stub_missing: Whether to replace missing imports with stubs (TODO: symbolicate)
|
||||
:return: List of addresses for the imports within the store
|
||||
"""
|
||||
imports: typing.List[ExternVal] = []
|
||||
for i in module.imports:
|
||||
logger.info("Importing %s:%s", i.module, i.name)
|
||||
if i.module not in self.module_names: # If the module isn't registered
|
||||
if i.module not in self.manual_exports: # or manually provided
|
||||
# Attempt to load it from the filesystem
|
||||
self.import_module(
|
||||
i.module, exec_start, stub_missing
|
||||
) # TODO - prevent circular imports
|
||||
# If it's registered, but hasn't been imported yet, import it
|
||||
elif not self.instantiated[self.module_names[i.module]]:
|
||||
self.import_module(i.module, exec_start, stub_missing)
|
||||
imported_version = self._get_export_addr(i.name, i.module)
|
||||
if imported_version is None:
|
||||
# check for manually provided version
|
||||
# Possibly overwriting imported_version from an address to an instance here
|
||||
imported_version = self.get_export(i.name, i.module) # type: ignore
|
||||
if imported_version is None and not stub_missing:
|
||||
raise RuntimeError(f"Could not find import {i.module}:{i.name}")
|
||||
|
||||
if isinstance(imported_version, (FuncAddr, TableAddr, MemAddr, GlobalAddr)):
|
||||
imports.append(imported_version) # TODO - Import type matching
|
||||
else:
|
||||
if isinstance(i.desc, TypeIdx): # Imported function
|
||||
func_type = module.types[i.desc]
|
||||
if i.module == "env" and imported_version:
|
||||
if callable(imported_version):
|
||||
imported_version = HostFunc(func_type, imported_version)
|
||||
self.store.funcs.append(
|
||||
imported_version
|
||||
if imported_version
|
||||
else HostFunc(
|
||||
# mypy doesn't like that we're using a partial function instead of a proper function
|
||||
func_type,
|
||||
partial(stub, len(func_type.result_types)), # type: ignore
|
||||
)
|
||||
)
|
||||
imports.append(FuncAddr(len(self.store.funcs) - 1))
|
||||
|
||||
elif isinstance(i.desc, TableType):
|
||||
self.store.tables.append(
|
||||
imported_version
|
||||
if imported_version
|
||||
else TableInst([None] * i.desc.limits.min, i.desc.limits.max)
|
||||
)
|
||||
imports.append(TableAddr(len(self.store.tables) - 1))
|
||||
# Create an empty memory of the correct size and provide it as an import
|
||||
elif isinstance(i.desc, MemoryType):
|
||||
self.store.mems.append(
|
||||
imported_version
|
||||
if imported_version
|
||||
else MemInst([0x0] * i.desc.min * 64 * 1024, i.desc.max)
|
||||
)
|
||||
imports.append(MemAddr(len(self.store.mems) - 1))
|
||||
# Create a global and set its value to 0.
|
||||
elif isinstance(i.desc, GlobalType):
|
||||
self.store.globals.append(
|
||||
imported_version
|
||||
if imported_version
|
||||
else GlobalInst(i.desc.value(0), i.desc.mut)
|
||||
)
|
||||
imports.append(GlobalAddr(len(self.store.globals) - 1))
|
||||
else:
|
||||
raise RuntimeError(f"Don't know how to handle imports of type {type(i.desc)}")
|
||||
|
||||
return imports
|
||||
|
||||
def instantiate(
|
||||
self,
|
||||
env_import_dict: typing.Dict[
|
||||
str, typing.Union[ProtoFuncInst, TableInst, MemInst, GlobalInst]
|
||||
],
|
||||
supplemental_env: typing.Dict[
|
||||
str, typing.Dict[str, typing.Union[ProtoFuncInst, TableInst, MemInst, GlobalInst]]
|
||||
] = {},
|
||||
exec_start=False,
|
||||
stub_missing=True,
|
||||
):
|
||||
"""
|
||||
Prepares the underlying ModuleInstance for execution. Calls import_module under the hood, so this is probably
|
||||
the only import-y function you ever need to call externally.
|
||||
|
||||
TODO: stubbed imports should be symbolic
|
||||
|
||||
:param env_import_dict: Dict mapping strings to functions. Functions should accept the current ConstraintSet as the first argument.
|
||||
:param supplemental_env: Maps strings w/ module names to environment dicts using the same format as env_import_dict
|
||||
:param exec_start: Whether or not to automatically execute the `start` function, if it is set.
|
||||
:param stub_missing: Whether or not to replace missing imports with empty stubs
|
||||
:return: None
|
||||
"""
|
||||
self.set_env(env_import_dict)
|
||||
for k in supplemental_env:
|
||||
self.set_env(supplemental_env[k], k)
|
||||
self.import_module(self.default_module, exec_start, stub_missing)
|
||||
|
||||
def invoke(self, name="main", argv=[], module=None):
|
||||
"""
|
||||
Sets up the WASMWorld to run the function specified by `name` when `ManticoreWASM.run` is called
|
||||
|
||||
:param name: Name of the function to invoke
|
||||
:param argv: List of arguments to pass to the function. Should typically be I32, I64, F32, or F64
|
||||
:param module: name of a module to call the function in (if not the default module)
|
||||
:return: None
|
||||
"""
|
||||
module = self.default_module if module is None else module
|
||||
instance = self.modules[self.module_names[module]][1]
|
||||
instance.invoke_by_name(name, self.stack, self.store, list(argv))
|
||||
|
||||
def exec_for_test(self, funcname, module=None):
|
||||
"""
|
||||
Helper method that simulates the evaluation loop without creating workers or states, forking, or concretizing
|
||||
symbolic values.
|
||||
Only used for concrete unit testing.
|
||||
|
||||
:param funcname: The name of the function to test
|
||||
:param module: The name of the module to test the function in (if not the default module)
|
||||
:return: The top n items from the stack where n is the expected number of return values from the function
|
||||
"""
|
||||
module = self.default_module if module is None else module
|
||||
instance = self.modules[self.module_names[module]][1]
|
||||
|
||||
# Grab the appropriate number of return values for the function being invoked
|
||||
rets = 0
|
||||
for export in instance.exports:
|
||||
if export.name == funcname and isinstance(export.value, FuncAddr):
|
||||
rets = len(self.store.funcs[export.value].type.result_types)
|
||||
|
||||
# Call exec_instruction until it returns false or throws an error
|
||||
try:
|
||||
while instance.exec_instruction(self.store, self.stack):
|
||||
pass
|
||||
# Return the top `rets` values from the stack
|
||||
return [self.stack.pop() for _i in range(rets)]
|
||||
except (Trap, NotImplementedError) as e: # Reset the internals if we have any problems
|
||||
instance.reset_internal()
|
||||
self.stack = Stack()
|
||||
raise e
|
||||
|
||||
def execute(self):
|
||||
"""
|
||||
Tells the underlying ModuleInstance to execute a single WASM instruction. Raises TerminateState if there are
|
||||
no more instructions to execute, or if the instruction raises a Trap.
|
||||
"""
|
||||
if not self.instantiated:
|
||||
raise RuntimeError("Trying to execute before instantiation!")
|
||||
try:
|
||||
if not self.instance.exec_instruction(self.store, self.stack):
|
||||
raise TerminateState(f"Execution returned {self.stack.peek()}")
|
||||
except Trap as e:
|
||||
raise TerminateState(f"Execution raised Trap: {str(e)}")
|
||||
@@ -28,10 +28,15 @@ class CacheDict(OrderedDict):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def __del__(self):
|
||||
log = logging.getLogger(self.__class__.__name__)
|
||||
log.debug(
|
||||
f"DictCache: hits: {self._hits}, misses: {self._misses}, flushes: {self._flushes}, size: {self.__len__()}"
|
||||
)
|
||||
try:
|
||||
log = logging.getLogger(self.__class__.__name__)
|
||||
log.debug(
|
||||
f"DictCache: hits: {self._hits}, misses: {self._misses}, flushes: {self._flushes}, size: {self.__len__()}"
|
||||
)
|
||||
except TypeError:
|
||||
# Prevent "TypeError: attribute of type 'NoneType' is not callable" on line 32
|
||||
# TODO - figure out why this happens (I think it's only on concrete runs?)
|
||||
pass
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if len(self) > self._max_size:
|
||||
|
||||
@@ -100,11 +100,12 @@ def get_levels():
|
||||
("manticore.platforms.*", logging.DEBUG),
|
||||
("manticore.ethereum", logging.DEBUG),
|
||||
("manticore.core.plugin", logging.DEBUG),
|
||||
("manticore.wasm.*", logging.INFO),
|
||||
("manticore.utils.emulate", logging.INFO),
|
||||
("manticore.utils.helpers", logging.INFO),
|
||||
],
|
||||
# 3 (-vv)
|
||||
[("manticore.native.cpu.*", logging.DEBUG)],
|
||||
[("manticore.native.cpu.*", logging.DEBUG), ("manticore.wasm.*", logging.DEBUG)],
|
||||
# 4 (-vvv)
|
||||
[
|
||||
("manticore.native.memory", logging.DEBUG),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
from .manticore import ManticoreWASM
|
||||
@@ -0,0 +1,30 @@
|
||||
from .manticore import ManticoreWASM
|
||||
|
||||
from ..core.plugin import Profiler
|
||||
from ..utils import config
|
||||
|
||||
consts = config.get_group("cli")
|
||||
consts.add("target_func", default="main", description="WASM Function to execute")
|
||||
|
||||
|
||||
def wasm_main(args, _logger):
|
||||
|
||||
m = ManticoreWASM(
|
||||
args.argv[0],
|
||||
argv=args.argv[1:],
|
||||
env={},
|
||||
exec_start=True,
|
||||
workspace_url=args.workspace,
|
||||
policy=args.policy,
|
||||
)
|
||||
|
||||
if consts.profile:
|
||||
profiler = Profiler()
|
||||
m.register_plugin(profiler)
|
||||
|
||||
m.default_invoke(func_name=consts.target_func)
|
||||
|
||||
with m.kill_timeout():
|
||||
m.run()
|
||||
|
||||
m.finalize()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,231 @@
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import typing
|
||||
|
||||
from .state import State
|
||||
from ..core.manticore import ManticoreBase
|
||||
from ..core.smtlib import ConstraintSet, issymbolic, Z3Solver
|
||||
from .types import I32, I64, F32, F64
|
||||
from .structure import FuncInst
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ManticoreWASM(ManticoreBase):
|
||||
"""
|
||||
Manticore class for interacting with WASM, analagous to ManticoreNative or ManticoreEVM.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, path_or_state, env={}, sup_env={}, workspace_url=None, policy="random", **kwargs
|
||||
):
|
||||
"""
|
||||
:param path_or_state: Path to binary or a state (object) to begin from.
|
||||
:param env: Dict of imports to place under the "env" module
|
||||
:param sup_env: Maps module names to import dicts (a la {"env":{}})
|
||||
"""
|
||||
if isinstance(path_or_state, str):
|
||||
if not os.path.isfile(path_or_state):
|
||||
raise OSError(f"{path_or_state} is not an existing regular file")
|
||||
initial_state = _make_initial_state(path_or_state, env, sup_env, **kwargs)
|
||||
else:
|
||||
initial_state = path_or_state
|
||||
|
||||
super().__init__(initial_state, workspace_url=workspace_url, policy=policy, **kwargs)
|
||||
|
||||
self.subscribe("will_terminate_state", self._terminate_state_callback)
|
||||
|
||||
def run(self, timeout=None):
|
||||
"""
|
||||
Begins the Manticore run
|
||||
|
||||
:param timeout: number of seconds after which to kill execution
|
||||
"""
|
||||
with self.locked_context() as context:
|
||||
context["time_started"] = time.time()
|
||||
|
||||
with self.kill_timeout(timeout):
|
||||
super().run()
|
||||
|
||||
def finalize(self):
|
||||
"""
|
||||
Finish a run and solve for test cases.
|
||||
Calls save_run_data
|
||||
"""
|
||||
super().finalize()
|
||||
self.save_run_data()
|
||||
|
||||
def save_run_data(self):
|
||||
super().save_run_data()
|
||||
|
||||
time_ended = time.time()
|
||||
|
||||
with self.locked_context() as context:
|
||||
time_elapsed = time_ended - context["time_started"]
|
||||
|
||||
logger.info("Total time: %s", time_elapsed)
|
||||
|
||||
context["time_ended"] = time_ended
|
||||
context["time_elapsed"] = time_elapsed
|
||||
|
||||
@ManticoreBase.at_not_running
|
||||
def invoke(self, name="main", argv_generator=lambda s: []):
|
||||
"""
|
||||
Maps the "invoke" command over all the ready states
|
||||
:param name: The function to invoke
|
||||
:param argv_generator: A function that takes the current state and returns a list of arguments
|
||||
"""
|
||||
for state in self.ready_states:
|
||||
args = argv_generator(state)
|
||||
logger.info("Invoking: %s(%s)", name, ", ".join(str(a) for a in args))
|
||||
state.platform.invoke(name=name, argv=args)
|
||||
|
||||
@ManticoreBase.at_not_running
|
||||
def default_invoke(self, func_name: str = "main"):
|
||||
"""
|
||||
Looks for a `main` function or `start` function and invokes it with symbolic arguments
|
||||
:param func_name: Optional name of function to look for
|
||||
"""
|
||||
funcs = [func_name]
|
||||
if "main" not in func_name:
|
||||
funcs.append("main")
|
||||
|
||||
state = next(self.ready_states)
|
||||
for name in funcs:
|
||||
func_inst: typing.Optional[FuncInst] = state.platform.get_export(name)
|
||||
if isinstance(func_inst, FuncInst):
|
||||
func_ty = func_inst.type
|
||||
|
||||
args = []
|
||||
for idx, ty in enumerate(func_ty.param_types):
|
||||
if ty in {I32, F32}:
|
||||
args.append(state.new_symbolic_value(32, f"arg{idx}_{ty.__name__}"))
|
||||
elif ty in {I64, F64}:
|
||||
args.append(state.new_symbolic_value(64, f"arg{idx}_{ty.__name__}"))
|
||||
|
||||
self.invoke(name=name, argv_generator=lambda s: args)
|
||||
break
|
||||
|
||||
@ManticoreBase.at_not_running
|
||||
def collect_returns(self, n=1):
|
||||
"""
|
||||
Iterates over the terminated states and collects the top n values from the stack.
|
||||
Generally only used for testing.
|
||||
|
||||
:param n: Number of values to collect
|
||||
:return: A list of list of lists.
|
||||
> One list for each state
|
||||
> One list for each n
|
||||
> The output from solver.get_all_values
|
||||
"""
|
||||
outer = []
|
||||
for state in self.terminated_states:
|
||||
inner = []
|
||||
p = state.platform
|
||||
for _i in range(n):
|
||||
ret = None
|
||||
if not p.stack.empty():
|
||||
ret = p.stack.pop()
|
||||
if issymbolic(ret):
|
||||
if ret.size == 32:
|
||||
inner.append(
|
||||
list(
|
||||
I32(a) # TODO - eventually we'll need to support floats as well.
|
||||
for a in Z3Solver.instance().get_all_values(state.constraints, ret)
|
||||
)
|
||||
)
|
||||
elif ret.size == 64:
|
||||
inner.append(
|
||||
list(
|
||||
I64(
|
||||
a
|
||||
) # TODO - that'll probably require us to subclass bitvecs into IxxBV and FxxBV
|
||||
for a in Z3Solver.instance().get_all_values(state.constraints, ret)
|
||||
)
|
||||
)
|
||||
else:
|
||||
inner.append([ret])
|
||||
outer.append(inner)
|
||||
return outer
|
||||
|
||||
def _terminate_state_callback(self, state, e):
|
||||
"""
|
||||
Adds state to the wasm.saved_states list
|
||||
|
||||
:param state: the terminated state
|
||||
:param e: any exception raised
|
||||
"""
|
||||
with self.locked_context("wasm.saved_states", list) as saved_states:
|
||||
saved_states.append(state.id)
|
||||
|
||||
@ManticoreBase.at_not_running
|
||||
def _reinit(self):
|
||||
"""
|
||||
Moves terminated states back into the ready states list. Only used for testing, may promote to
|
||||
a part of the official API in the future.
|
||||
"""
|
||||
# If there are ready states still then it was a paused execution
|
||||
assert not self._ready_states
|
||||
assert not self._busy_states
|
||||
|
||||
with self.locked_context("wasm.saved_states", list) as saved_states:
|
||||
while saved_states:
|
||||
state_id = saved_states.pop()
|
||||
self._terminated_states.remove(state_id)
|
||||
self._ready_states.append(state_id)
|
||||
|
||||
def generate_testcase(self, state, message="test", name="test"):
|
||||
testcase = super().generate_testcase(state, message)
|
||||
self._output.save_input_symbols(testcase, state)
|
||||
|
||||
with testcase.open_stream("stack") as stackf:
|
||||
stackf.write(str(state.stack.data))
|
||||
|
||||
with testcase.open_stream("memory") as memoryf:
|
||||
memoryf.write(str(state.mem.data))
|
||||
|
||||
term = getattr(state, "_terminated_by", None)
|
||||
if term:
|
||||
with testcase.open_stream("status") as summary:
|
||||
summary.write(f"{str(term)}\n\n")
|
||||
|
||||
|
||||
def _make_initial_state(binary_path, env={}, sup_env={}, **kwargs):
|
||||
"""
|
||||
Wraps _make_wasm_bin
|
||||
|
||||
:param binary_path: filename of the wasm module
|
||||
:param env: Import dict
|
||||
:param sup_env: Maps module names to import dicts (a la {"env":{}})
|
||||
:param kwargs:
|
||||
:return: initial state
|
||||
"""
|
||||
if binary_path.endswith(".wasm"):
|
||||
return _make_wasm_bin(binary_path, env=env, sup_env=sup_env, **kwargs)
|
||||
|
||||
|
||||
def _make_wasm_bin(program, env={}, sup_env={}, **kwargs) -> State:
|
||||
"""
|
||||
Returns an initial state for a binary WASM module
|
||||
|
||||
:param program: filename of the wasm module
|
||||
:param env: Import dict
|
||||
:param sup_env: Maps module names to import dicts (a la {"env":{}})
|
||||
:return: initial state
|
||||
"""
|
||||
from ..platforms import wasm
|
||||
|
||||
logger.info("Loading program %s", program)
|
||||
|
||||
constraints = kwargs.get("constraints", ConstraintSet())
|
||||
platform = wasm.WASMWorld(program, constraints=constraints)
|
||||
platform.instantiate(
|
||||
env,
|
||||
sup_env,
|
||||
exec_start=kwargs.get("exec_start", False),
|
||||
stub_missing=kwargs.get("stub_missing", True),
|
||||
)
|
||||
initial_state = State(constraints, platform)
|
||||
|
||||
return initial_state
|
||||
@@ -0,0 +1,27 @@
|
||||
from ..core.state import StateBase
|
||||
|
||||
|
||||
class State(StateBase):
|
||||
@property
|
||||
def stack(self):
|
||||
"""
|
||||
Current execution Stack
|
||||
"""
|
||||
return self._platform.stack
|
||||
|
||||
@property
|
||||
def store(self):
|
||||
"""
|
||||
Current execution Store
|
||||
"""
|
||||
return self._platform.store
|
||||
|
||||
@property
|
||||
def mem(self):
|
||||
"""
|
||||
The first memory in the current execution Store
|
||||
"""
|
||||
return self.store.mems[0]
|
||||
|
||||
def execute(self):
|
||||
return self._platform.execute()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,474 @@
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from ..core.smtlib import issymbolic, BitVec
|
||||
from ctypes import *
|
||||
import wasm
|
||||
import struct
|
||||
from ..core.state import Concretize
|
||||
|
||||
# Do I like this? No. Is it necessary to get mypy to pass without destroying the WASM type system? Yes.
|
||||
# Look how much cleaner the version below is instead of having a separate class for every type...
|
||||
"""
|
||||
U32: type = type("U32", (int,), {})
|
||||
U64: type = type("U64", (int,), {})
|
||||
"""
|
||||
|
||||
|
||||
class U32(int):
|
||||
pass
|
||||
|
||||
|
||||
class U64(int):
|
||||
pass
|
||||
|
||||
|
||||
def debug(imm):
|
||||
"""
|
||||
Attempts to pull meaningful data out of an immediate, which has a dynamic GeneratedStructure type
|
||||
|
||||
:param imm: the instruction immediate
|
||||
:return: a printable representation of the immediate, or the immediate itself
|
||||
"""
|
||||
if hasattr(imm, "value"):
|
||||
return imm.value
|
||||
if hasattr(imm, "function_index"):
|
||||
return f"Func Idx {imm.function_index}"
|
||||
if hasattr(imm, "offset"):
|
||||
return f"Offset {imm.offset}"
|
||||
if hasattr(imm, "local_index"):
|
||||
return f"Local {imm.local_index}"
|
||||
if hasattr(imm, "global_index"):
|
||||
return f"Global {imm.global_index}"
|
||||
return getattr(imm, "value", imm)
|
||||
|
||||
|
||||
def _reinterpret(ty1: type, ty2: type, val):
|
||||
"""
|
||||
Attempts to convert a value from one ctypes type to another
|
||||
|
||||
:param ty1: The type of the value
|
||||
:param ty2: The desired type of the value
|
||||
:param val: The value itself
|
||||
:return: The converted value
|
||||
"""
|
||||
ptr = pointer(ty1(val))
|
||||
# mypy worries that `contents` won't always exist for the pointer type
|
||||
return cast(ptr, POINTER(ty2)).contents.value # type: ignore
|
||||
|
||||
|
||||
class I32(int):
|
||||
"""
|
||||
Subclass of int that's restricted to 32-bit values
|
||||
"""
|
||||
|
||||
def __new__(cls, val):
|
||||
val = struct.unpack("i", c_int32(int(val) & 0xFFFFFFFF))[0]
|
||||
return super(I32, cls).__new__(cls, val)
|
||||
|
||||
@classmethod
|
||||
def cast(cls, other):
|
||||
"""
|
||||
:param other: Value to convert to I32
|
||||
:return: If other is symbolic, other. Otherwise, I32(other)
|
||||
"""
|
||||
if issymbolic(other):
|
||||
return other
|
||||
return cls(other)
|
||||
|
||||
@staticmethod
|
||||
def to_unsigned(val):
|
||||
"""
|
||||
Reinterprets the argument from a signed integer to an unsigned 32-bit integer
|
||||
|
||||
:param val: Signed integer to reinterpret
|
||||
:return: The unsigned equivalent
|
||||
"""
|
||||
return _reinterpret(c_int32, c_uint32, val)
|
||||
|
||||
|
||||
class I64(int):
|
||||
"""
|
||||
Subclass of int that's restricted to 64-bit values
|
||||
"""
|
||||
|
||||
def __new__(cls, val):
|
||||
val = struct.unpack("q", c_int64(int(val)))[0]
|
||||
return super(I64, cls).__new__(cls, val)
|
||||
|
||||
@classmethod
|
||||
def cast(cls, other):
|
||||
"""
|
||||
:param other: Value to convert to I64
|
||||
:return: If other is symbolic, other. Otherwise, I64(other)
|
||||
"""
|
||||
if issymbolic(other):
|
||||
return other
|
||||
return cls(other)
|
||||
|
||||
@staticmethod
|
||||
def to_unsigned(val):
|
||||
"""
|
||||
Reinterprets the argument from a signed integer to an unsigned 64-bit integer
|
||||
|
||||
:param val: Signed integer to reinterpret
|
||||
:return: The unsigned equivalent
|
||||
"""
|
||||
return _reinterpret(c_int64, c_uint64, val)
|
||||
|
||||
|
||||
class F32(float):
|
||||
"""
|
||||
Subclass of float that's restricted to 32-bit values
|
||||
"""
|
||||
|
||||
def __new__(cls, val):
|
||||
if isinstance(val, int):
|
||||
val = _reinterpret(c_int32, c_float, val & 0xFFFFFFFF)
|
||||
val = struct.unpack("f", c_float(val))[0]
|
||||
self = super(F32, cls).__new__(cls, val)
|
||||
self.integer = val
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def cast(cls, other):
|
||||
"""
|
||||
:param other: Value to convert to F32
|
||||
:return: If other is symbolic, other. Otherwise, F32(other)
|
||||
"""
|
||||
if issymbolic(other):
|
||||
return other
|
||||
return cls(other)
|
||||
|
||||
|
||||
class F64(float):
|
||||
"""
|
||||
Subclass of float that's restricted to 64-bit values
|
||||
"""
|
||||
|
||||
def __new__(cls, val):
|
||||
if isinstance(val, int):
|
||||
val = _reinterpret(c_int64, c_double, val)
|
||||
val = struct.unpack("d", c_double(val))[0]
|
||||
self = super(F64, cls).__new__(cls, val)
|
||||
self.integer = val
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def cast(cls, other):
|
||||
"""
|
||||
:param other: Value to convert to F64
|
||||
:return: If other is symbolic, other. Otherwise, F64(other)
|
||||
"""
|
||||
if issymbolic(other):
|
||||
return other
|
||||
return cls(other)
|
||||
|
||||
|
||||
ValType = type #: https://www.w3.org/TR/wasm-core-1/#syntax-valtype
|
||||
# For reasons I don't understand, Tuple[Type] != Tuple[I32]
|
||||
Value_t: typing.Tuple[typing.Type] = (I32, I64, F32, F64, BitVec) # type: ignore
|
||||
# Value = typing.TypeVar('Value', I32, I64, F32, F64, BitVec) #: https://www.w3.org/TR/wasm-core-1/#syntax-val
|
||||
Value = typing.Union[I32, I64, F32, F64, BitVec] #: https://www.w3.org/TR/wasm-core-1/#syntax-val
|
||||
|
||||
|
||||
class Name(str):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class FunctionType:
|
||||
"""
|
||||
https://www.w3.org/TR/wasm-core-1/#syntax-functype
|
||||
"""
|
||||
|
||||
param_types: typing.List[ValType] #: Sequential types of each of the parameters
|
||||
result_types: typing.List[ValType] #: Sequential types of each of the return values
|
||||
|
||||
|
||||
@dataclass
|
||||
class LimitType:
|
||||
"""
|
||||
https://www.w3.org/TR/wasm-core-1/#syntax-limits
|
||||
"""
|
||||
|
||||
min: U32
|
||||
max: typing.Optional[U32]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TableType:
|
||||
"""https://www.w3.org/TR/wasm-core-1/#syntax-tabletype"""
|
||||
|
||||
limits: LimitType #: Minimum and maximum size of the table
|
||||
elemtype: type #: the type ot the element. Currently, the only element type is `funcref`
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlobalType:
|
||||
"""https://www.w3.org/TR/wasm-core-1/#syntax-globaltype"""
|
||||
|
||||
mut: bool #: Whether or not this global is mutable
|
||||
value: ValType #: The value of the global
|
||||
|
||||
|
||||
# https://www.w3.org/TR/wasm-core-1/#indices%E2%91%A0
|
||||
class TypeIdx(U32):
|
||||
pass
|
||||
|
||||
|
||||
class FuncIdx(U32):
|
||||
pass
|
||||
|
||||
|
||||
class TableIdx(U32):
|
||||
pass
|
||||
|
||||
|
||||
class MemIdx(U32):
|
||||
pass
|
||||
|
||||
|
||||
class GlobalIdx(U32):
|
||||
pass
|
||||
|
||||
|
||||
class LocalIdx(U32):
|
||||
pass
|
||||
|
||||
|
||||
class LabelIdx(U32):
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlockImm:
|
||||
sig: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class BranchImm:
|
||||
relative_depth: U32
|
||||
|
||||
|
||||
@dataclass
|
||||
class BranchTableImm:
|
||||
target_count: U32
|
||||
target_table: typing.List[U32]
|
||||
default_target: U32
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallImm:
|
||||
function_index: U32
|
||||
|
||||
|
||||
@dataclass
|
||||
class CallIndirectImm:
|
||||
type_index: U32
|
||||
reserved: U32
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalVarXsImm:
|
||||
local_index: U32
|
||||
|
||||
|
||||
@dataclass
|
||||
class GlobalVarXsImm:
|
||||
global_index: U32
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryImm:
|
||||
flags: U32
|
||||
offset: U32
|
||||
|
||||
|
||||
@dataclass
|
||||
class CurGrowMemImm:
|
||||
reserved: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class I32ConstImm:
|
||||
value: I32
|
||||
|
||||
|
||||
@dataclass
|
||||
class I64ConstImm:
|
||||
value: I64
|
||||
|
||||
|
||||
@dataclass
|
||||
class F32ConstImm:
|
||||
value: F32
|
||||
|
||||
|
||||
@dataclass
|
||||
class F64ConstImm:
|
||||
value: F64
|
||||
|
||||
|
||||
ImmType = typing.Union[
|
||||
BlockImm,
|
||||
BranchImm,
|
||||
BranchTableImm,
|
||||
CallImm,
|
||||
CallIndirectImm,
|
||||
LocalVarXsImm,
|
||||
GlobalVarXsImm,
|
||||
MemoryImm,
|
||||
CurGrowMemImm,
|
||||
I32ConstImm,
|
||||
F32ConstImm,
|
||||
F64ConstImm,
|
||||
] #: Types of all immediates
|
||||
|
||||
|
||||
class Instruction:
|
||||
"""Internal instruction class that's pickle-friendly and works with the type system """
|
||||
|
||||
__slots__ = ["opcode", "mnemonic", "imm"]
|
||||
opcode: int #: Opcode, used for dispatching instructions
|
||||
mnemonic: str #: Used for debugging
|
||||
imm: ImmType #: A class with the immediate data for this instruction
|
||||
|
||||
def __init__(self, inst: wasm.decode.Instruction, imm=None):
|
||||
self.opcode = inst.op.id
|
||||
self.mnemonic = inst.op.mnemonic
|
||||
self.imm = imm
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Instruction: {self.mnemonic} ({debug(self.imm)})>"
|
||||
|
||||
|
||||
MemoryType = LimitType #: https://www.w3.org/TR/wasm-core-1/#syntax-memtype
|
||||
ExternType = typing.Union[
|
||||
FunctionType, TableType, MemoryType, GlobalType
|
||||
] #: https://www.w3.org/TR/wasm-core-1/#external-types%E2%91%A0
|
||||
WASMExpression = typing.List[Instruction]
|
||||
|
||||
|
||||
def convert_instructions(inst_seq) -> WASMExpression:
|
||||
"""
|
||||
Converts instructions output from the parser into full-fledged Python objects that will work with Manticore.
|
||||
This is necessary because the pywasm module uses lots of reflection to generate structures on the fly, which
|
||||
doesn't play nicely with Pickle or the type system. That's why we need the `debug` method above to print out
|
||||
immediates, and also why we've created a separate class for every different type of immediate.
|
||||
|
||||
:param inst_seq: Sequence of raw instructions to process
|
||||
:return: The properly-typed instruction sequence in a format Manticore can use
|
||||
"""
|
||||
out = []
|
||||
if not isinstance(inst_seq, list):
|
||||
inst_seq = list(wasm.decode_bytecode(inst_seq))
|
||||
i: wasm.decode.Instruction
|
||||
for i in inst_seq:
|
||||
if 0x02 <= i.op.id <= 0x04:
|
||||
out.append(Instruction(i, BlockImm(i.imm.sig)))
|
||||
elif i.op.id in (0x0C, 0x0D):
|
||||
out.append(Instruction(i, BranchImm(i.imm.relative_depth)))
|
||||
elif i.op.id == 0x0E:
|
||||
out.append(
|
||||
Instruction(
|
||||
i, BranchTableImm(i.imm.target_count, i.imm.target_table, i.imm.default_target)
|
||||
)
|
||||
)
|
||||
elif i.op.id == 0x10:
|
||||
out.append(Instruction(i, CallImm(i.imm.function_index)))
|
||||
elif i.op.id == 0x11:
|
||||
out.append(Instruction(i, CallIndirectImm(i.imm.type_index, i.imm.reserved)))
|
||||
elif 0x20 <= i.op.id <= 0x22:
|
||||
out.append(Instruction(i, LocalVarXsImm(i.imm.local_index)))
|
||||
elif i.op.id in (0x23, 0x24):
|
||||
out.append(Instruction(i, GlobalVarXsImm(i.imm.global_index)))
|
||||
elif 0x28 <= i.op.id <= 0x3E:
|
||||
out.append(Instruction(i, MemoryImm(i.imm.flags, i.imm.offset)))
|
||||
elif i.op.id in (0x3F, 0x40):
|
||||
out.append(Instruction(i, CurGrowMemImm(i.imm.reserved)))
|
||||
elif i.op.id == 0x41:
|
||||
out.append(Instruction(i, I32ConstImm(i.imm.value)))
|
||||
elif i.op.id == 0x42:
|
||||
out.append(Instruction(i, I64ConstImm(i.imm.value)))
|
||||
elif i.op.id == 0x43:
|
||||
out.append(Instruction(i, F32ConstImm(i.imm.value)))
|
||||
elif i.op.id == 0x44:
|
||||
out.append(Instruction(i, F64ConstImm(i.imm.value)))
|
||||
else:
|
||||
out.append(Instruction(i))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
class Trap(Exception):
|
||||
"""
|
||||
Subclass of Exception, used for WASM errors
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnreachableInstructionTrap(Trap):
|
||||
def __init__(self):
|
||||
super().__init__("Tried to execute an unreachable instruction")
|
||||
|
||||
|
||||
class ZeroDivisionTrap(Trap):
|
||||
def __init__(self):
|
||||
super().__init__("Zero division")
|
||||
|
||||
|
||||
class OverflowDivisionTrap(Trap):
|
||||
def __init__(self):
|
||||
super().__init__("Overflow in signed division")
|
||||
|
||||
|
||||
class NonExistentFunctionCallTrap(Trap):
|
||||
def __init__(self):
|
||||
super().__init__("Indirect call to non-existent function")
|
||||
|
||||
|
||||
class OutOfBoundsMemoryTrap(Trap):
|
||||
def __init__(self, addr):
|
||||
super().__init__("Out of bounds memory access at " + hex(addr))
|
||||
|
||||
|
||||
class InvalidConversionTrap(Trap):
|
||||
def __init__(self, ty, val):
|
||||
super().__init__("Can't convert " + str(val) + " to " + str(ty))
|
||||
|
||||
|
||||
class TypeMismatchTrap(Trap):
|
||||
def __init__(self, ty1, ty2):
|
||||
super().__init__(f"Type signature mismatch: {ty1} != {ty2}")
|
||||
|
||||
|
||||
class ConcretizeStack(Concretize):
|
||||
"""Tells Manticore to concretize the value `depth` values from the end of the stack. """
|
||||
|
||||
def __init__(self, depth: int, ty: type, message: str, expression, policy=None, **kwargs):
|
||||
"""
|
||||
:param depth: Index in the stack (should typically be negative)
|
||||
:param ty: The type to cast the
|
||||
:param message: Debug message describing the reason for concretization
|
||||
:param expression: The expression to concretize, either a Value or a BitVec
|
||||
"""
|
||||
if policy is None:
|
||||
policy = "ALL"
|
||||
if policy not in self._ValidPolicies:
|
||||
raise Exception(f'Policy ({policy}) must be one of: {", ".join(self._ValidPolicies)}')
|
||||
|
||||
def setstate(state, value):
|
||||
state.platform.stack.data[depth] = ty(value)
|
||||
|
||||
self.setstate = setstate
|
||||
self.expression = expression
|
||||
self.policy = policy
|
||||
self.message = f"Concretize: {message} (Policy: {policy})"
|
||||
super().__init__(message, expression, setstate=self.setstate, **kwargs)
|
||||
|
||||
|
||||
class MissingExportException(Trap):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
super().__init__(f"Couldn't find an export called `{name}`")
|
||||
@@ -38,3 +38,6 @@ ignore_missing_imports = True
|
||||
|
||||
[mypy-wrapt.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-wasm.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ build:
|
||||
image: latest
|
||||
|
||||
python:
|
||||
version: 3.6
|
||||
version: 3.7
|
||||
|
||||
+15
-1
@@ -74,6 +74,17 @@ make_vmtests(){
|
||||
cd $DIR
|
||||
}
|
||||
|
||||
make_wasm_tests(){
|
||||
DIR=`pwd`
|
||||
if [ ! -f .wasm_done ]; then
|
||||
echo "Automaking WASM Tests" `pwd`
|
||||
cd ./tests/wasm
|
||||
./generate_tests.sh
|
||||
touch .wasm_done
|
||||
fi
|
||||
cd $DIR
|
||||
}
|
||||
|
||||
install_truffle(){
|
||||
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
|
||||
source ~/.nvm/nvm.sh
|
||||
@@ -147,7 +158,8 @@ case $1 in
|
||||
run_truffle_tests
|
||||
RV=$(($RV + $?))
|
||||
;;
|
||||
|
||||
wasm)
|
||||
make_wasm_tests ;& # Fallthrough
|
||||
native) ;& # Fallthrough
|
||||
ethereum) ;& # Fallthrough
|
||||
other)
|
||||
@@ -171,6 +183,8 @@ case $1 in
|
||||
RV=$(($RV + $?))
|
||||
make_vmtests; run_tests_from_dir ethereum_vm
|
||||
RV=$(($RV + $?))
|
||||
make_wasm_tests; run_tests_from_dir wasm
|
||||
RV=$(($RV + $?))
|
||||
run_tests_from_dir other
|
||||
RV=$(($RV + $?))
|
||||
run_examples
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
on_rtd = os.environ.get("READTHEDOCS") == "True"
|
||||
@@ -12,6 +13,10 @@ def rtd_dependent_deps():
|
||||
return ["z3-solver"]
|
||||
|
||||
|
||||
version = sys.version_info
|
||||
dataclass_deps = ["dataclasses"] if version.major == 3 and version.minor < 7 else []
|
||||
|
||||
|
||||
# If you update native_deps please update the `REQUIREMENTS_TO_IMPORTS` dict in `utils/install_helper.py`
|
||||
# (we need to know how to import a given native dependency so we can check if native dependencies are installed)
|
||||
native_deps = ["capstone==4.0.1", "pyelftools", "unicorn==1.0.2rc1"]
|
||||
@@ -46,8 +51,11 @@ setup(
|
||||
"rlp",
|
||||
"ply",
|
||||
"crytic-compile>=0.1.1",
|
||||
"wasm",
|
||||
"black",
|
||||
]
|
||||
+ rtd_dependent_deps(),
|
||||
+ rtd_dependent_deps()
|
||||
+ dataclass_deps,
|
||||
extras_require=extra_require,
|
||||
entry_points={"console_scripts": ["manticore = manticore.__main__:main"]},
|
||||
)
|
||||
|
||||
@@ -84,7 +84,7 @@ class StateTest(unittest.TestCase):
|
||||
expr = BitVecVariable(32, "tmp")
|
||||
self.state.constrain(expr > 4)
|
||||
self.state.constrain(expr < 7)
|
||||
solved = self.state.solve_n(expr, 2)
|
||||
solved = sorted(self.state.solve_n(expr, 2))
|
||||
self.assertEqual(solved, [5, 6])
|
||||
|
||||
def test_solve_n2(self):
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
rm -rf */
|
||||
touch __init__.py
|
||||
|
||||
wget -nc -nv -O wabt.tgz -c https://github.com/WebAssembly/wabt/releases/download/1.0.12/wabt-1.0.12-linux.tar.gz
|
||||
wget -nc -nv -O spec.zip -c https://github.com/WebAssembly/spec/archive/ace189a8f906f4f0760656a2aa53b6dc7046e771.zip
|
||||
|
||||
yes | unzip -q -j spec.zip 'spec-*/test/core/*' -d .
|
||||
rm run.py README.md
|
||||
rm spec.zip
|
||||
|
||||
tar --wildcards --strip=1 -xf wabt.tgz 'wabt-*/wast2json'
|
||||
rm wabt.tgz
|
||||
|
||||
mkdir skipped_tests
|
||||
while read skip; do
|
||||
mv $skip.wast skipped_tests/
|
||||
done < skipped_test_sets
|
||||
|
||||
for x in *"-"*.wast; do
|
||||
mv -- "$x" "${x//-/_}"
|
||||
done
|
||||
|
||||
ls *.wast | sed 's/\.wast//g' > modules.txt
|
||||
|
||||
cores=$(python -c "import multiprocessing; print(max(multiprocessing.cpu_count() - 2, 1))")
|
||||
|
||||
cat > gen.sh << EOF
|
||||
module=\$1
|
||||
echo "Preparing \$module"
|
||||
mkdir _\$module
|
||||
touch _\$module/__init__.py
|
||||
./wast2json --debug-names \$module.wast -o _\$module/\$module.json
|
||||
mv \$module.wast _\$module/
|
||||
python3 json2smc.py _\$module/\$module.json | black --quiet --fast - > _\$module/test_symbolic_\$module.py
|
||||
python3 json2mc.py _\$module/\$module.json | black --quiet --fast - > _\$module/test_\$module.py
|
||||
EOF
|
||||
|
||||
chmod +x gen.sh
|
||||
cat modules.txt | xargs -n1 -P"$cores" ./gen.sh
|
||||
rm gen.sh
|
||||
|
||||
exit 0
|
||||
@@ -0,0 +1,166 @@
|
||||
import argparse
|
||||
import json
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from base64 import b64encode
|
||||
from copy import copy
|
||||
|
||||
parser = argparse.ArgumentParser("Generate Manticore tests from the WASM Spec")
|
||||
parser.add_argument("filename", type=argparse.FileType("r"), help="JSON file output from wast2json")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = json.load(args.filename)["commands"]
|
||||
args.filename.close()
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self, filename, tests, name=None):
|
||||
self.name = filename.replace(".wasm", "").replace(".wast", "").replace(".", "_").strip()
|
||||
self.filename = filename
|
||||
self.tests = tests
|
||||
self.registered_name = name
|
||||
self.imports = []
|
||||
|
||||
def add_test(self, name, line, args, rets, type_="assert_return", mod_name=None):
|
||||
self.tests.append(
|
||||
{
|
||||
"func": name,
|
||||
"line": line,
|
||||
"args": args,
|
||||
"rets": rets,
|
||||
"type": type_,
|
||||
"mod_name": mod_name,
|
||||
}
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Module {self.filename} containing {len(self.tests)} tests>"
|
||||
|
||||
|
||||
def convert_types(to_convert):
|
||||
""" Convert unsigned ints from JSON into WASM Types (I32, F64, etc) """
|
||||
out = []
|
||||
for item in to_convert:
|
||||
out.append(f"{item['type'].upper()}({item.get('value', 0)})")
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
env = Environment(loader=FileSystemLoader("."))
|
||||
|
||||
|
||||
def escape_null(in_str: str):
|
||||
"""Base64-encode function names if they contain nonprintable characters"""
|
||||
if in_str.isprintable() and not any((c in in_str) for c in {'"', "'", ";"}):
|
||||
return f'"{in_str}"'
|
||||
else:
|
||||
return f"str(b64decode(\"{b64encode(in_str.encode('utf-8')).decode('utf-8')}\"), 'utf-8')"
|
||||
|
||||
|
||||
env.filters["escape_null"] = escape_null
|
||||
|
||||
template = env.get_template("test_template.jinja2")
|
||||
|
||||
|
||||
modules = []
|
||||
registered_modules = {}
|
||||
imports = []
|
||||
current_module = None
|
||||
for d in data:
|
||||
|
||||
if d["type"] == "action":
|
||||
if d["action"]["type"] == "invoke":
|
||||
if isinstance(current_module, int):
|
||||
modules[current_module].add_test(
|
||||
d["action"]["field"],
|
||||
d["line"],
|
||||
convert_types(d["action"]["args"]),
|
||||
convert_types(d["expected"]),
|
||||
"action",
|
||||
mod_name=d["action"].get("module", None),
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("action with action type: " + d["action"]["type"])
|
||||
elif d["type"] == "assert_exhaustion":
|
||||
pass
|
||||
elif d["type"] == "assert_invalid":
|
||||
if current_module:
|
||||
modules[current_module].imports = copy(imports)
|
||||
current_module = None
|
||||
elif d["type"] == "assert_malformed":
|
||||
if current_module:
|
||||
modules[current_module].imports = copy(imports)
|
||||
current_module = None
|
||||
elif d["type"] == "assert_return":
|
||||
if d["action"]["type"] == "invoke":
|
||||
if isinstance(current_module, int):
|
||||
modules[current_module].add_test(
|
||||
d["action"]["field"],
|
||||
d["line"],
|
||||
convert_types(d["action"]["args"]),
|
||||
convert_types(d["expected"]),
|
||||
mod_name=d["action"].get("module", None),
|
||||
)
|
||||
elif d["action"]["type"] == "get":
|
||||
modules[current_module].add_test(
|
||||
d["action"]["field"],
|
||||
d["line"],
|
||||
[],
|
||||
convert_types(d["expected"]),
|
||||
"assert_global",
|
||||
d["action"].get("module", None),
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("assert_return with action type: " + d["action"]["type"])
|
||||
elif d["type"] == "assert_return_arithmetic_nan":
|
||||
# XXX Eventually implement, but if we raise here it eliminates other valid tests
|
||||
# raise NotImplementedError("assert_return_arithmetic_nan")
|
||||
pass
|
||||
|
||||
elif d["type"] == "assert_return_canonical_nan":
|
||||
# XXX Eventually implement, but if we raise here it eliminates other valid tests
|
||||
# raise NotImplementedError("assert_return_canonical_nan")
|
||||
pass
|
||||
|
||||
elif d["type"] == "assert_trap":
|
||||
if d["action"]["type"] == "invoke":
|
||||
if isinstance(current_module, int):
|
||||
modules[current_module].add_test(
|
||||
d["action"]["field"],
|
||||
d["line"],
|
||||
convert_types(d["action"]["args"]),
|
||||
convert_types(d["expected"]),
|
||||
"assert_trap",
|
||||
mod_name=d["action"].get("module", None),
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("assert_trap with action type: " + d["action"]["type"])
|
||||
elif d["type"] == "assert_uninstantiable":
|
||||
if current_module:
|
||||
modules[current_module].imports = copy(imports)
|
||||
current_module = None
|
||||
elif d["type"] == "assert_unlinkable":
|
||||
if current_module:
|
||||
modules[current_module].imports = copy(imports)
|
||||
current_module = None
|
||||
elif d["type"] == "module":
|
||||
modules.append(Module(d["filename"], [], d.get("name", None)))
|
||||
if current_module:
|
||||
modules[current_module].imports = copy(imports)
|
||||
if d.get("name", None):
|
||||
imports.append({"type": "import", "name": d["name"], "filename": d["filename"]})
|
||||
current_module = len(modules) - 1
|
||||
if "name" in d:
|
||||
registered_modules[d["name"]] = modules[current_module].filename
|
||||
elif d["type"] == "register": # Allow a module to go by another name
|
||||
maybe_name = d.get("name", False)
|
||||
if maybe_name: # This is an alias for another registered module
|
||||
imports.append({"type": "alias", "alias": d["as"], "orig": maybe_name})
|
||||
else: # This is an alias for the current module
|
||||
imports.append(
|
||||
{"type": "import", "name": d["as"], "filename": modules[current_module].filename}
|
||||
)
|
||||
modules[current_module].registered_name = d["as"]
|
||||
|
||||
if current_module:
|
||||
modules[current_module].imports = copy(imports)
|
||||
|
||||
print(template.render(modules=modules))
|
||||
@@ -0,0 +1,162 @@
|
||||
import argparse
|
||||
import json
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from base64 import b64encode
|
||||
import string
|
||||
|
||||
parser = argparse.ArgumentParser("Generate Manticore tests from the WASM Spec")
|
||||
parser.add_argument("filename", type=argparse.FileType("r"), help="JSON file output from wast2json")
|
||||
args = parser.parse_args()
|
||||
|
||||
data = json.load(args.filename)["commands"]
|
||||
args.filename.close()
|
||||
|
||||
# Individual unit tests that we skip due to timeouts or problematic statefulness
|
||||
skip = {
|
||||
"fib_L261",
|
||||
"check-memory-zero_L87",
|
||||
"check-memory-zero_L89",
|
||||
"check-memory-zero_L91",
|
||||
"check-memory-zero_L93",
|
||||
"check-memory-zero_L95",
|
||||
"check-memory-zero_L97",
|
||||
"odd_L269",
|
||||
"fib-i64_L527",
|
||||
}
|
||||
# Modules with statefeulness for which we disallow reinitialization
|
||||
disallow_reinit_modules = {
|
||||
"call_0",
|
||||
"call_indirect_0",
|
||||
"globals_0",
|
||||
"memory_grow_0",
|
||||
"memory_grow_3",
|
||||
}
|
||||
|
||||
|
||||
class Module:
|
||||
def __init__(self, filename, tests):
|
||||
self.name = filename.replace(".wasm", "").replace(".wast", "").replace(".", "_").strip()
|
||||
self.filename = filename
|
||||
self.tests = tests
|
||||
self.allow_reinit = True if self.name not in disallow_reinit_modules else False
|
||||
|
||||
def add_test(self, name, line, args, rets, type_="assert_return"):
|
||||
self.tests.append({"func": name, "line": line, "args": args, "rets": rets, "type": type_})
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Module {self.filename} containing {len(self.tests)} tests>"
|
||||
|
||||
|
||||
def convert_args(to_convert):
|
||||
"""
|
||||
Convert a set of unsigned ints from JSON into an appropriate set of constraints
|
||||
:param to_convert:
|
||||
:return:
|
||||
"""
|
||||
out = []
|
||||
for idx, item in enumerate(to_convert):
|
||||
if "32" in item["type"]:
|
||||
out.append(
|
||||
{"constraint": f"state.new_symbolic_value(32)", "val": int(item.get("value", 0))}
|
||||
)
|
||||
elif "64" in item["type"]:
|
||||
out.append(
|
||||
{"constraint": f"state.new_symbolic_value(64)", "val": int(item.get("value", 0))}
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("garbage type")
|
||||
return out
|
||||
|
||||
|
||||
def convert_rets(to_convert):
|
||||
"""Convert unsigned int from JSON into concrete values"""
|
||||
out = []
|
||||
for item in to_convert:
|
||||
out.append(f"{item['type'].upper()}({item.get('value', 0)})")
|
||||
return ", ".join(out)
|
||||
|
||||
|
||||
env = Environment(loader=FileSystemLoader("."))
|
||||
|
||||
|
||||
def escape_null(in_str: str):
|
||||
""" Base-64 encode non-printable characters in test names so we can handle that obnoxious names module """
|
||||
if in_str.isprintable() and not any((c in in_str) for c in {'"', "'", ";"}):
|
||||
return f'"{in_str}"'
|
||||
else:
|
||||
return f"str(b64decode(\"{b64encode(in_str.encode('utf-8')).decode('utf-8')}\"), 'utf-8')"
|
||||
|
||||
|
||||
env.filters["escape_null"] = escape_null
|
||||
|
||||
template = env.get_template("symbolic_test_template.jinja2")
|
||||
|
||||
|
||||
modules = []
|
||||
current_module = None
|
||||
for d in data:
|
||||
if f"{d.get('action', {}).get('field', None)}_L{d['line']}" in skip:
|
||||
continue
|
||||
|
||||
if d["type"] == "action":
|
||||
if d["action"]["type"] == "invoke":
|
||||
if isinstance(current_module, int):
|
||||
modules[current_module].add_test(
|
||||
d["action"]["field"],
|
||||
d["line"],
|
||||
convert_args(d["action"]["args"]),
|
||||
convert_rets(d["expected"]),
|
||||
"action",
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("action with action type: " + d["action"]["type"])
|
||||
elif d["type"] == "assert_exhaustion":
|
||||
pass
|
||||
elif d["type"] == "assert_invalid":
|
||||
current_module = None
|
||||
elif d["type"] == "assert_malformed":
|
||||
current_module = None
|
||||
elif d["type"] == "assert_return":
|
||||
if d["action"]["type"] == "invoke":
|
||||
if isinstance(current_module, int):
|
||||
modules[current_module].add_test(
|
||||
d["action"]["field"],
|
||||
d["line"],
|
||||
convert_args(d["action"]["args"]),
|
||||
convert_rets(d["expected"]),
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("assert_return with action type: " + d["action"]["type"])
|
||||
elif d["type"] == "assert_return_arithmetic_nan":
|
||||
# XXX Eventually implement, but if we raise here it eliminates other valid tests
|
||||
# raise NotImplementedError("assert_return_arithmetic_nan")
|
||||
pass
|
||||
|
||||
elif d["type"] == "assert_return_canonical_nan":
|
||||
# XXX Eventually implement, but if we raise here it eliminates other valid tests
|
||||
# raise NotImplementedError("assert_return_canonical_nan")
|
||||
pass
|
||||
|
||||
elif d["type"] == "assert_trap":
|
||||
if d["action"]["type"] == "invoke":
|
||||
if isinstance(current_module, int):
|
||||
modules[current_module].add_test(
|
||||
d["action"]["field"],
|
||||
d["line"],
|
||||
convert_args(d["action"]["args"]),
|
||||
convert_rets(d["expected"]),
|
||||
"assert_trap",
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError("assert_trap with action type: " + d["action"]["type"])
|
||||
elif d["type"] == "assert_uninstantiable":
|
||||
current_module = None
|
||||
elif d["type"] == "assert_unlinkable":
|
||||
current_module = None
|
||||
elif d["type"] == "module":
|
||||
modules.append(Module(d["filename"], []))
|
||||
current_module = len(modules) - 1
|
||||
elif d["type"] == "register":
|
||||
raise NotImplementedError("register")
|
||||
|
||||
print(template.render(modules=modules))
|
||||
@@ -0,0 +1,7 @@
|
||||
conversions
|
||||
f32
|
||||
f64
|
||||
float_exprs
|
||||
float_literals
|
||||
float_memory
|
||||
float_misc
|
||||
@@ -0,0 +1,139 @@
|
||||
{%block imports %}
|
||||
import unittest
|
||||
from os.path import dirname, abspath, join
|
||||
from manticore.platforms import wasm
|
||||
from manticore.wasm import ManticoreWASM
|
||||
from manticore.wasm.types import I32, F32, I64, F64, Trap, FunctionType
|
||||
from manticore.wasm.structure import TableInst, MemInst, GlobalInst, HostFunc
|
||||
from manticore.core.plugin import Plugin
|
||||
from manticore.core.state import TerminateState
|
||||
from base64 import b64decode
|
||||
import math
|
||||
|
||||
|
||||
from manticore.utils import config
|
||||
consts = config.get_group("core")
|
||||
consts.mprocessing = consts.mprocessing.single
|
||||
|
||||
def assertEqualNan(testcase, a_list, b):
|
||||
""" b_list: list of exected return values
|
||||
a_list: 3-nested lists: One for each state, containing one for each of the n possible return values, each containing the output of solver.get_all_values """
|
||||
assert len(a_list) == 1, f"Diverged into {len(a_list)} states!"
|
||||
for state_a in a_list:
|
||||
assert len(state_a) == len(b), f"Returned {len(a)} values! (Expected {len(b)})"
|
||||
for a in state_a:
|
||||
assert len(a) <= 1, "Found multiple possible values!"
|
||||
for a_item, b_item in zip(a, b):
|
||||
if isinstance(b_item, F32) and isinstance(a_item, int):
|
||||
a_item = F32.cast(a_item)
|
||||
if isinstance(b_item, F64) and isinstance(a_item, int):
|
||||
a_item = F64.cast(a_item)
|
||||
if math.isnan(b_item):
|
||||
testcase.assertTrue(math.isnan(a_item))
|
||||
else:
|
||||
testcase.assertAlmostEqual(a_item, b_item)
|
||||
{% endblock %}
|
||||
|
||||
def noarg(_ctx):
|
||||
return []
|
||||
def onearg(_ctx, _c):
|
||||
return []
|
||||
def twoarg(_ctx, _x, _y):
|
||||
return []
|
||||
|
||||
spectest = {
|
||||
"print": HostFunc(FunctionType([], []), noarg),
|
||||
"print_i32": HostFunc(FunctionType([I32], []), onearg),
|
||||
"print_i32_f32": HostFunc(FunctionType([I32, F32], []), twoarg),
|
||||
"print_f64_f64": HostFunc(FunctionType([F64, F64], []), twoarg),
|
||||
"print_f32": HostFunc(FunctionType([F32], []), onearg),
|
||||
"print_f64": HostFunc(FunctionType([F64], []), onearg),
|
||||
"global_i32": GlobalInst(I32(666), True),
|
||||
"global_f32": GlobalInst(F32(666), True),
|
||||
"global_f64": GlobalInst(F64(666), True),
|
||||
"table": TableInst([None] * 10, 20),
|
||||
"memory": MemInst([0x0] * 64 * 1024, 2)
|
||||
}
|
||||
|
||||
class RaiseExceptionPlugin(Plugin):
|
||||
|
||||
def did_terminate_state_callback(self, current_state, exc):
|
||||
if "raised Trap" in str(exc):
|
||||
self.manticore.trapped = True
|
||||
|
||||
{% for module in modules %}
|
||||
class WASMTest_{{ module.name }}(unittest.TestCase):
|
||||
_multiprocess_can_split_ = False
|
||||
filename = join(dirname(abspath(__file__)), "{{ module.filename }}")
|
||||
subtest_count = 0
|
||||
|
||||
def run(self, result=None):
|
||||
result = super().run(result)
|
||||
setattr(result, "testsRun", self.subtest_count + getattr(result, "testsRun", 0) - 1)
|
||||
return result
|
||||
|
||||
def test_{{ module.name }}(self):
|
||||
m = ManticoreWASM(self.filename, sup_env={"spectest":spectest}, exec_start=True)
|
||||
m.register_plugin(RaiseExceptionPlugin())
|
||||
|
||||
{% for test in module.tests %}
|
||||
name = {{ test.func | escape_null }} + "_L{{test.line}}"
|
||||
with self.subTest(msg=name):
|
||||
print("======V======", name, "(Symbolic)", "======V======")
|
||||
self.subtest_count += 1
|
||||
|
||||
if self.subtest_count % 50 == 0:
|
||||
if {{module.allow_reinit}}:
|
||||
print("Reinitializing Manticore object after 50 symbolic tests")
|
||||
m = ManticoreWASM(self.filename, sup_env={"spectest":spectest}, exec_start=True)
|
||||
m.register_plugin(RaiseExceptionPlugin())
|
||||
|
||||
def create_argv(state):
|
||||
argv = []
|
||||
{% for arg in test.args %}
|
||||
argv.append({{arg.constraint}})
|
||||
state.constrain(argv[-1] == {{arg.val}})
|
||||
{% endfor %}
|
||||
return argv
|
||||
|
||||
expected = [{{ test.rets }}]
|
||||
{% if test.type == "assert_return" %}
|
||||
with m.kill_timeout(150):
|
||||
m.invoke(name={{ test.func | escape_null }}, argv_generator=create_argv)
|
||||
setattr(m, 'trapped', False)
|
||||
m.run()
|
||||
self.assertFalse(getattr(m, 'trapped', False))
|
||||
if not getattr(m, '_killed', False):
|
||||
rets = m.collect_returns(len(expected))
|
||||
m._reinit()
|
||||
assertEqualNan(self, rets, expected)
|
||||
else:
|
||||
m._killed.value = False
|
||||
print(name, "Timed Out, Reinitializing Manticore")
|
||||
m = ManticoreWASM(self.filename, sup_env={"spectest":spectest}, exec_start=True)
|
||||
m.register_plugin(RaiseExceptionPlugin())
|
||||
{% endif %}
|
||||
{% if test.type == "assert_trap" %}
|
||||
with m.kill_timeout(150):
|
||||
m.invoke(name={{ test.func | escape_null }}, argv_generator=create_argv)
|
||||
m.run()
|
||||
self.assertTrue(getattr(m, 'trapped', False))
|
||||
if {{module.allow_reinit}}:
|
||||
print("Reinitializing after Trap")
|
||||
m = ManticoreWASM(self.filename, sup_env={"spectest":spectest}, exec_start=True)
|
||||
m.register_plugin(RaiseExceptionPlugin())
|
||||
else:
|
||||
m._reinit()
|
||||
{% endif %}
|
||||
{% if test.type == "action" %}
|
||||
with m.kill_timeout(150):
|
||||
m.invoke(name={{ test.func | escape_null }}, argv_generator=create_argv)
|
||||
m.run()
|
||||
m._reinit()
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,93 @@
|
||||
import unittest
|
||||
from manticore.wasm import ManticoreWASM
|
||||
from manticore.wasm.types import I32
|
||||
from manticore.core.plugin import Plugin
|
||||
from pathlib import Path
|
||||
import glob
|
||||
|
||||
|
||||
def getchar(constraints, addr):
|
||||
res = constraints.new_bitvec(32, "getchar_res")
|
||||
constraints.add(res > 0)
|
||||
constraints.add(res < 8)
|
||||
return [res]
|
||||
|
||||
|
||||
def arg_gen(state):
|
||||
arg = state.new_symbolic_value(32, "collatz_arg")
|
||||
state.constrain(arg > 3)
|
||||
state.constrain(arg < 9)
|
||||
state.constrain(arg % 2 == 0)
|
||||
return [arg]
|
||||
|
||||
|
||||
class CallCounterPlugin(Plugin):
|
||||
def did_execute_instruction_callback(self, state, instruction):
|
||||
with self.locked_context("counter", dict) as ctx:
|
||||
val = ctx.setdefault(instruction.mnemonic, 0)
|
||||
ctx[instruction.mnemonic] = val + 1
|
||||
|
||||
|
||||
wasm_file = str(
|
||||
Path(__file__).parent.parent.parent.joinpath("examples", "wasm", "collatz", "collatz.wasm")
|
||||
)
|
||||
|
||||
|
||||
class TestExamples(unittest.TestCase):
|
||||
def test_getchar(self):
|
||||
m = ManticoreWASM(wasm_file, env={"getchar": getchar})
|
||||
m.invoke("main")
|
||||
m.run()
|
||||
results = []
|
||||
for idx, val_list in enumerate(m.collect_returns()):
|
||||
results.append(val_list[0][0])
|
||||
|
||||
self.assertEqual(sorted(results), [0, 1, 2, 5, 7, 8, 16])
|
||||
|
||||
def test_symbolic_args(self):
|
||||
m = ManticoreWASM(wasm_file, env={})
|
||||
m.invoke("collatz", arg_gen)
|
||||
m.run()
|
||||
|
||||
results = []
|
||||
for idx, val_list in enumerate(m.collect_returns()):
|
||||
results.append(val_list[0][0])
|
||||
|
||||
self.assertEqual(sorted(results), [2, 3, 8])
|
||||
|
||||
m.finalize()
|
||||
|
||||
inputs = []
|
||||
for fn in glob.glob(m.workspace + "/*.input"):
|
||||
with open(fn, "r") as f:
|
||||
raw = f.read().strip()
|
||||
inputs.append(int(raw.replace("collatz_arg: ", "")))
|
||||
|
||||
self.assertEqual(sorted(inputs), [4, 6, 8])
|
||||
|
||||
def test_plugin(self):
|
||||
def arg_gen(_state):
|
||||
return [I32(1337)]
|
||||
|
||||
m = ManticoreWASM(wasm_file)
|
||||
counter_plugin = CallCounterPlugin()
|
||||
m.register_plugin(counter_plugin)
|
||||
m.invoke("collatz", arg_gen)
|
||||
m.run()
|
||||
|
||||
# counts = m.context.get("<class 'test_examples.CallCounterPlugin'>").get("counter")
|
||||
counts = counter_plugin.context.get("counter")
|
||||
|
||||
self.assertEqual(counts["br_if"], 45)
|
||||
self.assertEqual(counts["loop"], 44)
|
||||
self.assertEqual(counts["i32.add"], 88)
|
||||
|
||||
results = []
|
||||
for idx, val_list in enumerate(m.collect_returns()):
|
||||
results.append(val_list[0][0])
|
||||
|
||||
self.assertEqual(sorted(results), [44])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,113 @@
|
||||
import unittest
|
||||
from manticore.wasm import ManticoreWASM
|
||||
from manticore.wasm.types import I32
|
||||
from manticore.core.plugin import Plugin
|
||||
from manticore.wasm.structure import Stack, AtomicStack
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class StackTrackerPlugin(Plugin):
|
||||
def will_pop_item_callback(self, state, depth):
|
||||
with self.locked_context("push_pop_seq", list) as seq:
|
||||
seq.append("+")
|
||||
|
||||
def did_pop_item_callback(self, state, item, depth):
|
||||
with self.locked_context("push_pop_seq", list) as seq:
|
||||
seq.append("-")
|
||||
|
||||
|
||||
wasm_file = str(
|
||||
Path(__file__).parent.parent.parent.joinpath("examples", "wasm", "collatz", "collatz.wasm")
|
||||
)
|
||||
|
||||
|
||||
class TestStack(unittest.TestCase):
|
||||
def test_trace(self):
|
||||
"""
|
||||
This checks the sequence of pushes and pops that take place during an example execution.
|
||||
That sequence isn't meant to be invariant, so if you change the implementation and break
|
||||
this test, by all means, replace it with the new sequence.
|
||||
:return:
|
||||
"""
|
||||
|
||||
def arg_gen(_state):
|
||||
return [I32(1337)]
|
||||
|
||||
m = ManticoreWASM(wasm_file)
|
||||
tracker_plugin = StackTrackerPlugin()
|
||||
m.register_plugin(tracker_plugin)
|
||||
m.invoke("collatz", arg_gen)
|
||||
m.run()
|
||||
|
||||
results = []
|
||||
for idx, val_list in enumerate(m.collect_returns()):
|
||||
results.append(val_list[0][0])
|
||||
|
||||
self.assertEqual(sorted(results), [44])
|
||||
push_pop_seq = "".join(tracker_plugin.context.get("push_pop_seq"))
|
||||
|
||||
self.assertEqual(push_pop_seq, "+-" * 892)
|
||||
|
||||
def test_has_at_least(self):
|
||||
s = Stack()
|
||||
s.push(I32(1))
|
||||
self.assertFalse(s.has_at_least(I32, 2))
|
||||
s.push(I32(2))
|
||||
self.assertTrue(s.has_at_least(I32, 2))
|
||||
|
||||
def test_empty(self):
|
||||
s = Stack()
|
||||
with AtomicStack(s) as a:
|
||||
a.push(1)
|
||||
a.push(2)
|
||||
|
||||
self.assertEqual(len(s.data), 2)
|
||||
self.assertFalse(a.empty())
|
||||
a.pop()
|
||||
a.pop()
|
||||
|
||||
self.assertTrue(a.empty())
|
||||
self.assertTrue(s.empty())
|
||||
|
||||
def test_rollback(self):
|
||||
orig = [i for i in range(10)]
|
||||
|
||||
s = Stack()
|
||||
for i in range(10):
|
||||
s.push(i)
|
||||
a = AtomicStack(s)
|
||||
self.assertEqual(list(s.data), orig)
|
||||
|
||||
a.push(10)
|
||||
self.assertEqual(list(s.data), [i for i in range(11)])
|
||||
|
||||
a.rollback()
|
||||
self.assertEqual(list(s.data), orig)
|
||||
|
||||
for i in range(3):
|
||||
a.pop()
|
||||
self.assertEqual(list(s.data), [i for i in range(7)])
|
||||
|
||||
a.rollback()
|
||||
self.assertEqual(list(s.data), orig)
|
||||
|
||||
a.pop()
|
||||
a.pop()
|
||||
for i in range(9):
|
||||
a.push(20 + i)
|
||||
a.pop()
|
||||
a.pop()
|
||||
for i in range(5):
|
||||
a.push(30 + i)
|
||||
|
||||
self.assertEqual(
|
||||
list(s.data), [0, 1, 2, 3, 4, 5, 6, 7, 20, 21, 22, 23, 24, 25, 26, 30, 31, 32, 33, 34]
|
||||
)
|
||||
|
||||
a.rollback()
|
||||
self.assertEqual(list(s.data), orig)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,121 @@
|
||||
{%block imports %}
|
||||
import unittest
|
||||
from os.path import dirname, abspath, join
|
||||
from manticore.platforms import wasm
|
||||
from manticore.core import state
|
||||
from manticore.core.smtlib import Operators, ConstraintSet
|
||||
from manticore.wasm.types import I32, F32, I64, F64, Trap, FunctionType
|
||||
from manticore.wasm.structure import TableInst, MemInst, GlobalInst, HostFunc
|
||||
from base64 import b64decode
|
||||
import math
|
||||
|
||||
def assertEqualNan(testcase, a, b):
|
||||
if len(b) and len(a) and math.isnan(b[0]):
|
||||
testcase.assertTrue(math.isnan(a[0]))
|
||||
else:
|
||||
assert len(a) == len(b)
|
||||
for a_item, b_item in zip(a, b):
|
||||
# Tests for equality out to 10^-7, helpful for avoiding floating-point rounding errors
|
||||
testcase.assertAlmostEqual(a_item, b_item)
|
||||
{% endblock %}
|
||||
|
||||
|
||||
spectest = {
|
||||
"print": HostFunc(FunctionType([], []), lambda ct: []),
|
||||
"print_i32": HostFunc(FunctionType([I32], []), lambda ct, x: []),
|
||||
"print_i32_f32": HostFunc(FunctionType([I32, F32], []), lambda ct, x, y: []),
|
||||
"print_f64_f64": HostFunc(FunctionType([F64, F64], []), lambda ct, x, y: []),
|
||||
"print_f32": HostFunc(FunctionType([F32], []), lambda ct, x: []),
|
||||
"print_f64": HostFunc(FunctionType([F64], []), lambda ct, x: []),
|
||||
"global_i32": GlobalInst(I32(666), True),
|
||||
"global_f32": GlobalInst(F32(666), True),
|
||||
"global_f64": GlobalInst(F64(666), True),
|
||||
"table": TableInst([None] * 10, 20),
|
||||
"memory": MemInst([0x0] * 64 * 1024, 2)
|
||||
}
|
||||
|
||||
|
||||
{% for module in modules %}
|
||||
class WASMTest_{{ module.name }}(unittest.TestCase):
|
||||
_multiprocess_can_split_ = False
|
||||
filename = join(dirname(abspath(__file__)), "{{ module.filename }}")
|
||||
subtest_count = 0
|
||||
|
||||
def run(self, result=None):
|
||||
result = super().run(result)
|
||||
setattr(result, "testsRun", self.subtest_count + getattr(result, "testsRun", 0) - 1)
|
||||
return result
|
||||
|
||||
def test_{{ module.name }}(self):
|
||||
print("\n======V======", "{{ module.name }}", "======V======")
|
||||
constraints = ConstraintSet()
|
||||
{% if module.registered_name != None %}
|
||||
world = wasm.WASMWorld(self.filename, name="{{ module.registered_name }}")
|
||||
{% else %}
|
||||
world = wasm.WASMWorld(self.filename)
|
||||
{% endif %}
|
||||
world.set_env(spectest, mod_name="spectest")
|
||||
|
||||
{% for item in module.imports %}
|
||||
{% if item['type'] == "import" %}
|
||||
world.register_module("{{ item['name'] }}", join(dirname(abspath(__file__)), "{{ item['filename'] }}"))
|
||||
world.import_module("{{ item['name'] }}", True, False)
|
||||
{% endif %}
|
||||
{% if item['type'] == "alias" %}
|
||||
world.register_module("{{ item['alias'] }}", "{{ item['orig'] }}")
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
world.instantiate({}, {}, exec_start=True)
|
||||
{% for test in module.tests %}
|
||||
with self.subTest(msg={{ test.func | escape_null }} + "_L{{test.line}}"):
|
||||
print("===V===", {{ test.func | escape_null }} + "_L{{test.line}}", "===V===")
|
||||
self.subtest_count += 1
|
||||
{% if test.args | length > 0 %}
|
||||
argv=[{{ test.args }}]
|
||||
{% else %}
|
||||
argv=[]
|
||||
{% endif %}
|
||||
{% if test.mod_name != None %}
|
||||
module = "{{ test.mod_name }}"
|
||||
world.import_module(module, True, False)
|
||||
{% else %}
|
||||
module = None
|
||||
{% endif %}
|
||||
expected = [{{ test.rets }}]
|
||||
{% if test.type == "assert_return" %}
|
||||
world.invoke(name={{ test.func | escape_null }}, argv=argv, module=module)
|
||||
try:
|
||||
real = world.exec_for_test({{ test.func | escape_null }}, module=module)
|
||||
assertEqualNan(self, real, expected)
|
||||
except NotImplementedError as e:
|
||||
self.skipTest(f"NotImplementedError: {str(e)}")
|
||||
{% endif %}
|
||||
{% if test.type == "assert_global" %}
|
||||
{% if test.mod_name != None %}
|
||||
assertEqualNan(self, [world.get_export("{{ test.func }}", "{{ test.mod_name }}").value], expected)
|
||||
{% else %}
|
||||
assertEqualNan(self, [world.get_export("{{ test.func }}").value], expected)
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if test.type == "assert_trap" %}
|
||||
world.invoke(name={{ test.func | escape_null }}, argv=argv, module=module)
|
||||
with self.assertRaises(Trap):
|
||||
try:
|
||||
world.exec_for_test({{ test.func | escape_null }}, module=module)
|
||||
except NotImplementedError as e:
|
||||
self.skipTest(f"NotImplementedError: {str(e)}")
|
||||
{% endif %}
|
||||
{% if test.type == "action" %}
|
||||
world.invoke(name={{ test.func | escape_null }}, argv=argv, module=module)
|
||||
try:
|
||||
world.exec_for_test({{ test.func | escape_null }}, module=module)
|
||||
except NotImplementedError as e:
|
||||
self.skipTest(f"NotImplementedError: {str(e)}")
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user