#!/usr/bin/python # # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. """ Runs the static rooting analysis """ from subprocess import Popen import subprocess import os import argparse import sys import re def env(config): e = dict(os.environ) e['PATH'] = ':'.join(p for p in (config.get('gcc_bin'), config.get('sixgill_bin'), e['PATH']) if p) e['XDB'] = '%(sixgill_bin)s/xdb.so' % config e['SOURCE'] = config['source'] e['ANALYZED_OBJDIR'] = config['objdir'] return e def fill(command, config): try: return tuple(s % config for s in command) except: print("Substitution failed:") problems = [] for fragment in command: try: fragment % config except: problems.append(fragment) raise Exception("\n".join(["Substitution failed:"] + [ " %s" % s for s in problems ])) def print_command(command, outfile=None, env=None): output = ' '.join(command) if outfile: output += ' > ' + outfile if env: changed = {} e = os.environ for key,value in env.items(): if (key not in e) or (e[key] != value): changed[key] = value if changed: outputs = [] for key, value in changed.items(): if key in e and e[key] in value: start = value.index(e[key]) end = start + len(e[key]) outputs.append('%s="%s${%s}%s"' % (key, value[:start], key, value[end:])) else: outputs.append("%s='%s'" % (key, value)) output = ' '.join(outputs) + " " + output print output def generate_hazards(config, outfilename): jobs = [] for i in range(int(config['jobs'])): command = fill(('%(js)s', '%(analysis_scriptdir)s/analyzeRoots.js', '%(gcFunctions_list)s', '%(gcEdges)s', '%(suppressedFunctions_list)s', '%(gcTypes)s', '%(typeInfo)s', str(i+1), '%(jobs)s', 'tmp.%s' % (i+1,)), config) outfile = 'rootingHazards.%s' % (i+1,) output = open(outfile, 'w') if config['verbose']: print_command(command, outfile=outfile, env=env(config)) jobs.append((command, Popen(command, stdout=output, env=env(config)))) final_status = 0 while jobs: pid, status = os.wait() jobs = [ job for job in jobs if job[1].pid != pid ] final_status = final_status or status if final_status: raise subprocess.CalledProcessError(final_status, 'analyzeRoots.js') with open(outfilename, 'w') as output: command = ['cat'] + [ 'rootingHazards.%s' % (i+1,) for i in range(int(config['jobs'])) ] if config['verbose']: print_command(command, outfile=outfilename) subprocess.call(command, stdout=output) JOBS = { 'dbs': (('%(ANALYSIS_SCRIPTDIR)s/run_complete', '--foreground', '--no-logs', '--build-root=%(objdir)s', '--wrap-dir=%(sixgill)s/scripts/wrap_gcc', '--work-dir=work', '-b', '%(sixgill_bin)s', '--buildcommand=%(buildcommand)s', '.'), ()), 'list-dbs': (('ls', '-l'), ()), 'callgraph': (('%(js)s', '%(analysis_scriptdir)s/computeCallgraph.js', '%(typeInfo)s'), 'callgraph.txt'), 'gcFunctions': (('%(js)s', '%(analysis_scriptdir)s/computeGCFunctions.js', '%(callgraph)s', '[gcFunctions]', '[gcFunctions_list]', '[gcEdges]', '[suppressedFunctions_list]'), ('gcFunctions.txt', 'gcFunctions.lst', 'gcEdges.txt', 'suppressedFunctions.lst')), 'gcTypes': (('%(js)s', '%(analysis_scriptdir)s/computeGCTypes.js', '[gcTypes]', '[typeInfo]'), ('gcTypes.txt', 'typeInfo.txt')), 'allFunctions': (('%(sixgill_bin)s/xdbkeys', 'src_body.xdb',), 'allFunctions.txt'), 'hazards': (generate_hazards, 'rootingHazards.txt'), 'explain': ((os.environ.get('PYTHON', 'python2.7'), '%(analysis_scriptdir)s/explain.py', '%(hazards)s', '%(gcFunctions)s', '[explained_hazards]', '[unnecessary]', '[refs]'), ('hazards.txt', 'unnecessary.txt', 'refs.txt')) } def out_indexes(command): for i in range(len(command)): m = re.match(r'^\[(.*)\]$', command[i]) if m: yield (i, m.group(1)) def run_job(name, config): cmdspec, outfiles = JOBS[name] print("Running " + name + " to generate " + str(outfiles)) if hasattr(cmdspec, '__call__'): cmdspec(config, outfiles) else: temp_map = {} cmdspec = fill(cmdspec, config) if isinstance(outfiles, basestring): stdout_filename = '%s.tmp' % name temp_map[stdout_filename] = outfiles if config['verbose']: print_command(cmdspec, outfile=outfiles, env=env(config)) else: stdout_filename = None pc = list(cmdspec) outfile = 0 for (i, name) in out_indexes(cmdspec): pc[i] = outfiles[outfile] outfile += 1 if config['verbose']: print_command(pc, env=env(config)) command = list(cmdspec) outfile = 0 for (i, name) in out_indexes(cmdspec): command[i] = '%s.tmp' % name temp_map[command[i]] = outfiles[outfile] outfile += 1 sys.stdout.flush() if stdout_filename is None: subprocess.check_call(command, env=env(config)) else: with open(stdout_filename, 'w') as output: subprocess.check_call(command, stdout=output, env=env(config)) for (temp, final) in temp_map.items(): try: os.rename(temp, final) except OSError: print("Error renaming %s -> %s" % (temp, final)) raise config = { 'ANALYSIS_SCRIPTDIR': os.path.dirname(__file__) } defaults = [ '%s/defaults.py' % config['ANALYSIS_SCRIPTDIR'], '%s/defaults.py' % os.getcwd() ] parser = argparse.ArgumentParser(description='Statically analyze build tree for rooting hazards.') parser.add_argument('step', metavar='STEP', type=str, nargs='?', help='run starting from this step') parser.add_argument('--source', metavar='SOURCE', type=str, nargs='?', help='source code to analyze') parser.add_argument('--objdir', metavar='DIR', type=str, nargs='?', help='object directory of compiled files') parser.add_argument('--js', metavar='JSSHELL', type=str, nargs='?', help='full path to ctypes-capable JS shell') parser.add_argument('--upto', metavar='UPTO', type=str, nargs='?', help='last step to execute') parser.add_argument('--jobs', '-j', default=None, metavar='JOBS', type=int, help='number of simultaneous analyzeRoots.js jobs') parser.add_argument('--list', const=True, nargs='?', type=bool, help='display available steps') parser.add_argument('--buildcommand', '--build', '-b', type=str, nargs='?', help='command to build the tree being analyzed') parser.add_argument('--tag', '-t', type=str, nargs='?', help='name of job, also sets build command to "build."') parser.add_argument('--expect-file', type=str, nargs='?', help='deprecated option, temporarily still present for backwards compatibility') parser.add_argument('--verbose', '-v', action='store_true', help='Display cut & paste commands to run individual steps') args = parser.parse_args() for default in defaults: try: execfile(default, config) if args.verbose: print("Loaded %s" % default) except: pass data = config.copy() for k,v in vars(args).items(): if v is not None: data[k] = v if args.tag and not args.buildcommand: args.buildcommand="build.%s" % args.tag if args.jobs is not None: data['jobs'] = args.jobs if not data.get('jobs'): data['jobs'] = subprocess.check_output(['nproc', '--ignore=1']).strip() if args.buildcommand: data['buildcommand'] = args.buildcommand elif 'BUILD' in os.environ: data['buildcommand'] = os.environ['BUILD'] else: data['buildcommand'] = 'make -j4 -s' if 'ANALYZED_OBJDIR' in os.environ: data['objdir'] = os.environ['ANALYZED_OBJDIR'] if 'SOURCE' in os.environ: data['source'] = os.environ['SOURCE'] if not data.get('source') and data.get('sixgill_bin'): path = subprocess.check_output(['sh', '-c', data['sixgill_bin'] + '/xdbkeys file_source.xdb | grep jsapi.cpp']) data['source'] = path.replace("/js/src/jsapi.cpp", "") steps = [ 'dbs', 'gcTypes', 'callgraph', 'gcFunctions', 'allFunctions', 'hazards', 'explain' ] if args.list: for step in steps: command, outfilename = JOBS[step] if outfilename: print("%s -> %s" % (step, outfilename)) else: print(step) sys.exit(0) for step in steps: command, outfiles = JOBS[step] if isinstance(outfiles, basestring): data[step] = outfiles else: outfile = 0 for (i, name) in out_indexes(command): data[name] = outfiles[outfile] outfile += 1 assert len(outfiles) == outfile, 'step \'%s\': mismatched number of output files (%d) and params (%d)' % (step, outfile, len(outfiles)) if args.step: steps = steps[steps.index(args.step):] if args.upto: steps = steps[:steps.index(args.upto)+1] for step in steps: run_job(step, data)