Files
Peter Goodman 2d55d02cc3 API improvements. Must be used with the api_improvements branches of Remill and Anvill (#666)
* API improvements. Must be used with the api_improvements branch of both Remill and McSema

fixes for x86 and running the lifted code with klee

* Update dockerfile to clone anvill

* update remill commit id

* Add python3 to dockerfile

* update python3

* disable abi script

* Updated cmake to find anvill

* Update main.cpp

* update find_package for anvill

* WIP:updated prebuild cfg

* update prebuild cfg files

* enable abi build for testsuite

* Fix memory leak

* install missing package for testcases

* frontend: Reflect cfg file changes in dyninst frontend.

* frontend: Update local files copyrights to reflect overall change to agplv3.

* fix failing testcases

* update test cfgs

* Fix test failure with local state pointer

* set the flag to use local state_ptr in default mode

Co-authored-by: kumarak <iit.akshay@gmail.com>
Co-authored-by: Lukas Korencik <xkorenc1@fi.muni.cz>
2020-07-23 02:14:56 -04:00

644 lines
20 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2020 Trail of Bits, Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
import difflib
import filecmp
import operator
import os
import queue
import threading
import shutil
import subprocess
from subprocess import CalledProcessError
import sys
import tempfile
import unittest
import colors
import result_data
import util
llvm_version = 8
lift = None
bin_dir = "bin"
libmcsema = None
recompiled_dir = "recompiled"
tags_dir = 'tags'
batches = []
shared_libs = None
abi_lib_dir = None
def get_recompiled_name(name):
return name
# Print results of tests
def print_results(t_cases):
for key, val in sorted(t_cases.items(), key = operator.itemgetter(0)):
val.print(0)
for key, val in sorted(t_cases.items(), key = operator.itemgetter(0)):
val.print_ces()
def log_results(result, into):
for entry in result.failures:
what = entry[0]
into[what.name].cases[what._testMethodName] = result_data.FAIL
for entry in result.errors:
what = entry[0]
# Do not overwrite timeouts
if into[what.name].cases[what._testMethodName] != result_data.TIMEOUT:
into[what.name].cases[what._testMethodName] = result_data.ERROR
# Fill global variables
# TODO: Rework, this is really ugly
def check_arguments(args):
if not os.path.isfile(args.lift):
print("{} passed to --lift is not a valid file".format(args.lift))
sys.exit (1)
global lift
lift = args.lift
if not os.path.isfile(args.runtime_lib):
print ("{} passed to --runtime_lib is not a valid file".format(args.runtime_lib))
sys.exit (1)
global libmcsema
libmcsema = args.runtime_lib
if not os.path.isdir(args.bin_dir):
print("{} passed to --bin_dir is not a valid directory".format(args.bin_dir))
sys.exit (1)
bin_dir = os.path.abspath(args.bin_dir)
if not os.path.isdir(args.shared_lib_dir):
print("{} passed to --shared_lib_dir is not a valid directory".format(args.shared_lib_dir))
sys.exit (1)
global shared_libs
shared_libs = []
for lib_name in os.listdir(args.shared_lib_dir):
shared_libs.append(os.path.join(args.shared_lib_dir, lib_name ))
print("\n > Shared libraries: ")
print(shared_libs)
if not os.path.isdir(args.abi_lib_dir):
print ("{} passed to --abi_lib_dir is not a valid directory".format(args.abi_lib_dir))
sys.exit (1)
global abi_lib_dir
abi_lib_dir = ''
for lib_name in os.listdir(args.abi_lib_dir):
if lib_name.endswith(".bc"):
abi_lib_dir += os.path.join(args.abi_lib_dir, lib_name + ',')
print("\n > --abi_libraries files:")
print(abi_lib_dir)
# TODO: This whole snippet can be done using argparser and some actions
if args.batch is not None:
for batch in args.batch:
batch_dir = batch + "_cfg"
if not os.path.isdir(batch_dir):
print("{} passed to --batch is not a valid directory".format(batch))
sys.exit(1)
batches.append(batch_dir)
if args.batch_dir is not None:
for batch in args.batch_dir:
if not os.path.isdir(batch):
print("{} passed to --batch_dir is not a valid directory".format(batch))
sys.exit(1)
batches.append(batch_dir)
if not batches:
print("No batch is selected, exiting")
sys.exit(0)
print(batches)
def exec_and_log_fail(args):
try:
pipes = subprocess.Popen(args, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
std_out, std_err = pipes.communicate(timeout = 180)
ret_code = pipes.returncode
except subprocess.TimeoutExpired as e:
pipes.terminate()
print("Timeout!")
return False
if ret_code:
print("** stdout:")
print(std_out)
print("** stderr:")
print(std_err)
return False
return True
class Config:
class Result:
SUCCESS = 0
LIFT_FAIL = 1
RECOMPILE_FAIL = 2
defaults = [ '-os', 'linux', '-arch', 'amd64' ]
togglable = {
'abi_libraries' : ['-abi_libraries', abi_lib_dir],
}
# Since strings are immutable, hotfix is needed once abi_lib_dir is set
def _fix_excludes(self):
flag, opt = self.togglable['abi_libraries']
self.togglable['abi_libraries'] = (flag, abi_lib_dir)
def __init__(self, name, src, cfg_path):
self.name = name
self.binary = os.path.join(bin_dir, name)
self.config = src.rsplit('.', 2)[1]
self.id = self.name + '.' + self.config
self.cfg = cfg_path
self.lift_args = []
self.exclude_args = []
self.tags = []
self._fix_excludes()
self._parse_config(src)
def _tags(self, line):
self.tags = line.split(' ')[1:]
def _lift_opts(self, line):
tokens = line.split(' ')[1:]
# LIFT_OPTS are empty
if not tokens:
return
exclude = []
for opt in tokens:
if not opt:
break
elif opt[0] == '+':
self.lift_args.append(opt[1:])
elif opt[0] != '!':
self.lift_args.append(opt)
else:
exclude.append(opt[1:])
for key, val in Config.togglable.items():
if key in exclude:
continue
self.lift_args += val
def _parse_config(self, src):
print(' > Parsing', src)
with open(os.path.join(tags_dir, src), 'r') as cfg:
for line in cfg:
line = line.rstrip('\n')
header = line.split(' ', 1)[0]
header_dispatch = {
'TAGS:' : Config._tags,
'LIFT_OPTS:' : Config._lift_opts,
}
if header not in header_dispatch:
raise Exception("Unknown header {}".format(header))
header_dispatch[header](self, line)
def lift(self, test_dir):
print(" > Lifting", self.name + self.config)
self.bc = os.path.join(test_dir, '.'.join([self.name, self.config, 'bc']))
self.recompiled = os.path.join(test_dir, self.name + '.' + self.config)
args = [lift] + self.defaults + self.lift_args + \
['-output', self.bc, '-cfg', self.cfg]
print(args)
if not exec_and_log_fail(args):
return Config.Result.LIFT_FAIL
return self.recompile()
def recompile(self):
compiler = None
if 'c' in self.tags:
compiler = "clang-{}"
elif 'cpp' in self.tags:
compiler = "clang++-{}"
else:
print(" > Cannot decide on compiler when recompiling",\
self.name + '.' + self.config)
return Config.Result.RECOMPILE_FAIL
compiler = compiler.format(llvm_version)
args = [compiler, self.bc, '-o', self.recompiled, \
libmcsema, '-lpthread', '-lm', '-ldl'] + shared_libs
if not exec_and_log_fail(args):
return Config.Result.RECOMPILE_FAIL
return Config.Result.SUCCESS
def parallel_lift(*t_args):
todo, test_dir, results = t_args
while not todo.empty():
try:
config = todo.get()
except queue.Empty:
return
res = config.lift(test_dir)
# TODO: More fine grained log
if res != Config.Result.SUCCESS:
results[config.id] = result_data.TCData(config.id,config.recompiled, config.binary)
def get_configs(directory, allowed_tags, batched):
result = []
for f in os.listdir(directory):
name = util.strip_whole_config(f)
if not name or name not in batched:
continue
c = Config(name, f, batched[name])
if allowed_tags is None:
result.append(c)
elif any(x in allowed_tags for x in c.tags):
result.append(c)
return result
class TestDetails:
def __init__(self, cmd):
self.cmd = cmd
self.files = None
self.stdin = None
self.f_stdin = None
self.check = []
def set_files(self, files):
self.files = files
return self
def set_check(self, check):
self.check = check
return self
def set_f_stdin(self, f_stdin):
self.f_stdin = f_stdin
return self
def set_stdin(self, stdin):
self.stdin = stdin
return self
def files(self, files):
if self.files is not None:
raise Exception("Incorrect format of test case")
self.files = files
def stdin(self, stdin):
if stdin[0] != 'F':
self.stdin = stdin.encode()
return
self.f_stdin = os.path.abspath(stdin[1:])
class TestCase:
def __init__(self, src=None):
self.details = []
if src is not None:
self._parse(src)
def _parse(self, src):
dispatch = {
'STDIN:' : TestDetails.stdin,
'FILES:' : TestDetails.files,
}
with open(src, 'r') as f:
for line in f:
line = line.rstrip('\n')
head = line.split(' ', 1)[0]
if head in dispatch:
dispatch[head](self.details[-1], line.split(' ', 1)[1])
else:
self.details.append(TestDetails(line.split(' ')))
# .test was present but empty
if not self.details:
self.details.append(TestDetails(['']))
class Runner:
def __init__(self, config, test_case):
self.config = config
self.test_case = test_case
def set_up(self):
self.t_bin = tempfile.mkdtemp(dir=os.getcwd(), prefix='bin_t')
self.t_recompiled = tempfile.mkdtemp(dir=os.getcwd(), prefix='recompiled_t')
self.sawed_cwd = os.getcwd()
cfg = self.config
os.symlink(os.path.abspath(cfg.binary), os.path.join(self.t_bin, cfg.name))
os.symlink(cfg.recompiled, os.path.join(self.t_recompiled, cfg.name))
def tear_down(self):
os.chdir(self.sawed_cwd)
shutil.rmtree(self.t_recompiled)
shutil.rmtree(self.t_bin)
def copy_files(self, detail):
if detail.files is not None:
for f in detail.files:
basename = os.path.basename(f)
full_name = f
r = os.path.join(self.t_recompiled, basename)
b = os.path.join(self.t_bin, basename)
shutil.copyfile(full_name, r)
shutil.copyfile(full_name, b)
def compare(self, expected, actual, files):
e_out, e_err, e_ret = expected
a_out, a_err, a_ret = actual
correct = True
counterexample = ''
# TODO: This cannot be more hacky than this
try:
if a_out != e_out:
counterexample += 'stdout:\n'
counterexample += '\tExpected:\n'
counterexample += e_out.decode() + '\n'
counterexample += '\tGot:\n'
counterexample += a_out.decode() + '\n'
correct = correct and False
if a_err != e_err:
counterexample += 'stderr:'+ '\n'
counterexample += '\tExpected:'+ '\n'
counterexample += e_err.decode()+ '\n'
counterexample += '\tGot:'+ '\n'
counterexample += a_err.decode()+ '\n'
correct = correct and False
except UnicodeDecodeError as e:
correct = False
counterexample += str(e) + '\n'
if e_ret != e_ret:
counterexample += "Return code: " + str(e_ret) + ' != ' + str(a_ret) + '\n'
correct = correct and False
for name in files:
base_name = os.path.basename(name)
actual = os.path.join(self.t_recompiled, base_name)
expected = os.path.join(self.t_bin, base_name)
try:
if not filecmp.cmp(expected, actual):
counterexample += 'Files do not match: ' + base_name + '\n'
correct = correct and False
except FileNotFoundError as e:
counterexample += 'File {} not found/produced! '.format(base_name)
correct = correct and False
result = result_data.RUN if correct else result_data.FAIL
return (result, counterexample)
def open_stdin(self, t_dir, args, stdin):
with open(stdin, 'rb') as f:
r = f.read()
return self.exec_(t_dir, args, r)
def exec_(self, t_dir, args, stdin):
os.chdir(t_dir)
try:
pipes = subprocess.Popen(
args, stdout=subprocess.PIPE,
stderr = subprocess.PIPE,
stdin = subprocess.PIPE)
out, err = pipes.communicate(stdin, timeout=5)
ret = pipes.returncode
os.chdir(self.sawed_cwd)
return (out, err, ret)
except subprocess.TimeoutExpired as e:
pipes.terminate()
raise e
def exec(self, detail, files):
# To avoid calling system-wide installed binaries
filename = './' + self.config.name
_exec = Runner.exec_ if detail.f_stdin is None else Runner.open_stdin
stdin = detail.f_stdin if detail.f_stdin is not None else detail.stdin
try:
print(detail.cmd)
expected = _exec(self, self.t_bin, [filename] + detail.cmd, stdin)
actual = _exec(self, self.t_recompiled, [filename] + detail.cmd, stdin)
return self.compare(expected, actual, files)
except subprocess.TimeoutExpired as e:
return result_data.TIMEOUT, "Timeout"
except FileNotFoundError as e:
return result_data.ERROR, "File not found"
def run(self, detail):
self.set_up()
self.copy_files(detail)
result, ce = self.exec(detail, detail.check)
self.tear_down()
return (result, ce)
def evaluate(self, results):
c = self.config
results[c.id] = result_data.TCData(c.id, c.recompiled, c.binary)
for tc in self.test_case:
for d in tc.details:
result, ce = self.run(d)
results[c.id].cases[' '.join(d.cmd)] = result
results[c.id].total += 1
if result == result_data.RUN:
results[c.id].success += 1
else:
results[c.id].ces[' '.join(d.cmd)] = ce
class Tester:
def __init__(self, configs, test_def_dir):
# Config -> TestCase
self.cases = {}
for c in configs:
self.cases[c] = []
for f in os.listdir(test_def_dir):
basename, ext = os.path.splitext(f)
if ext != '.test':
continue
tc = TestCase(os.path.join(test_def_dir, f))
for key, val in self.cases.items():
if key.name == basename:
val.append(tc)
for key, val in g_complex_test.items():
for k, v in self.cases.items():
if k.name == key:
tc = TestCase()
tc.details = val
v.append(tc)
def run(self, results):
for key, val in self.cases.items():
print(key.id, 'number of tests:', len(val))
r = Runner(key, val)
r.evaluate(results)
g_complex_test = {
"gzip" :
[
TestDetails(['--help']),
TestDetails(['--version']),
TestDetails(['-asda']),
TestDetails(['-f', './data.txt']).set_files(['inputs/data.txt']). \
set_check(['data.txt.gz']),
TestDetails(['-l', './dec_data.txt.gz']).set_files(['inputs/dec_data.txt.gz']),
TestDetails(['-df', './dec_data.txt.gz']).set_files(['inputs/dec_data.txt.gz']).\
set_check(['dec_data.txt']),
TestDetails(['']),
],
"cat":
[
TestDetails(['--help']),
TestDetails(['--version']),
TestDetails(['data.txt']).set_files(['inputs/data.txt']),
TestDetails(['-n', 'data.txt']).set_files(['inputs/data.txt']),
],
"echo":
[
TestDetails(['--help']),
TestDetails(['--version']),
TestDetails(['Hello Wordl!']),
],
}
# Right now batches are combined, maybe it would make sense to separate batches from each other
# that can be useful when comparing performance of frontends
def main():
arg_parser = argparse.ArgumentParser(
formatter_class = argparse.RawDescriptionHelpFormatter)
arg_parser.add_argument('--lift',
help = "Path to the mcsema-lift binary",
required = True)
arg_parser.add_argument('--runtime_lib',
help = "Runtime library for lifted bitcode \
(e.g. libmcsema-rt64-6.0.a",
required = True)
arg_parser.add_argument('--shared_lib_dir',
help = "Directory that contains shared libraries used by original binaries",
default = "shared_libs",
required = False)
arg_parser.add_argument('--bin_dir',
help = "Directory that contains original binaries",
default = "bin",
required = False)
arg_parser.add_argument('--batch',
help = "Batch names",
nargs = '+',
required = False)
arg_parser.add_argument('--batch_dir',
help = 'Directories with cfgs',
nargs = '+',
required = False)
arg_parser.add_argument('--abi_lib_dir',
help = "Directory that contains bitcode types of external functions",
required = True)
arg_parser.add_argument('--dry_run',
help = 'Do not run any tests',
required = False)
arg_parser.add_argument('--save_log',
help = "Name of file to save result in json format",
required = False)
arg_parser.add_argument('--jobs',
help = "Number of threads to use",
default = 1,
required = False)
arg_parser.add_argument('--tags',
help = "Test only these tags from batch",
required = False)
args, command_args = arg_parser.parse_known_args()
check_arguments(args)
# Create directory to store recompiled binaries
# TemporaryDirectory() is not used, since we may want to have a look at recompiled
# code, maybe we want some --preserve option
test_dir = tempfile.mkdtemp(dir=os.getcwd())
# basename -> path
batched = {}
for batch in batches:
for f in os.listdir(batch):
if os.path.isfile(os.path.join(batch, f)):
basename, ext = os.path.splitext(f)
batched[basename] = os.path.join(batch, f)
configs = get_configs(tags_dir, args.tags, batched)
_todo = queue.Queue()
for c in configs:
_todo.put(c)
threads = []
results = {}
for i in range(int(args.jobs)):
t = threading.Thread(target=parallel_lift, args=(_todo, test_dir, results))
t.start()
threads.append(t)
for t in threads:
t.join()
Tester(configs, tags_dir).run(results)
print_results(results)
return
if __name__ == '__main__':
main()