diff options
Diffstat (limited to 'js/src/devtools/rootAnalysis/analyze.py')
-rwxr-xr-x | js/src/devtools/rootAnalysis/analyze.py | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/js/src/devtools/rootAnalysis/analyze.py b/js/src/devtools/rootAnalysis/analyze.py new file mode 100755 index 000000000..69482dab7 --- /dev/null +++ b/js/src/devtools/rootAnalysis/analyze.py @@ -0,0 +1,298 @@ +#!/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.<tag>"') +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) |