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:
Eric Hennenfent
2019-11-08 07:58:24 -08:00
committed by GitHub
parent 561c3eeeab
commit 1208759e55
33 changed files with 5632 additions and 12 deletions
+3 -2
View File
@@ -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"
+1
View File
@@ -14,6 +14,7 @@ Manticore is a symbolic execution tool for analysis of binaries and smart contra
states
evm
native
wasm
plugins
gotchas
+42
View File
@@ -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:
+15
View File
@@ -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(">"));
}
+145
View File
@@ -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.
+116
View File
@@ -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)
)
)
+3
View File
@@ -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)
+1 -1
View File
@@ -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:
+2
View File
@@ -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
+369
View File
@@ -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)}")
+9 -4
View File
@@ -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:
+2 -1
View File
@@ -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),
+1
View File
@@ -0,0 +1 @@
from .manticore import ManticoreWASM
+30
View File
@@ -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
+231
View File
@@ -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
+27
View File
@@ -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
+474
View File
@@ -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}`")
+3
View File
@@ -38,3 +38,6 @@ ignore_missing_imports = True
[mypy-wrapt.*]
ignore_missing_imports = True
[mypy-wasm.*]
ignore_missing_imports = True
+1 -1
View File
@@ -2,4 +2,4 @@ build:
image: latest
python:
version: 3.6
version: 3.7
+15 -1
View File
@@ -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
+9 -1
View File
@@ -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"]},
)
+1 -1
View File
@@ -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):
+44
View File
@@ -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
+166
View File
@@ -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))
+162
View File
@@ -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))
+7
View File
@@ -0,0 +1,7 @@
conversions
f32
f64
float_exprs
float_literals
float_memory
float_misc
+139
View File
@@ -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()
+93
View File
@@ -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()
+113
View File
@@ -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()
+121
View File
@@ -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()