#!/usr/bin/python3 # 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/. import argparse import json import pathlib import re from html import escape SRCDIR = pathlib.Path(__file__).parent.parent.parent.absolute() parser = argparse.ArgumentParser( description="Convert the JSON output of the hazard analysis into various text files describing the results.", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument("--verbose", type=bool, default=False, help="verbose output") inputs = parser.add_argument_group("Input") inputs.add_argument( "rootingHazards", nargs="?", default="rootingHazards.json", help="JSON input file describing the output of the hazard analysis", ) outputs = parser.add_argument_group("Output") outputs.add_argument( "gcFunctions", nargs="?", default="gcFunctions.txt", help="file containing a list of functions that can GC", ) outputs.add_argument( "hazards", nargs="?", default="hazards.txt", help="file containing the rooting hazards found", ) outputs.add_argument( "extra", nargs="?", default="unnecessary.txt", help="file containing unnecessary roots", ) outputs.add_argument( "refs", nargs="?", default="refs.txt", help="file containing a list of unsafe references to unrooted values", ) outputs.add_argument( "html", nargs="?", default="hazards.html", help="HTML-formatted file with the hazards found", ) args = parser.parse_args() # Imitate splitFunction from utility.js. def splitfunc(full): idx = full.find("$") if idx == -1: return (full, full) return (full[0:idx], full[idx + 1 :]) def print_header(outfh): print( """\ """, file=outfh, ) def print_footer(outfh): print("", file=outfh) def sourcelink(symbol=None, loc=None, range=None): if symbol: return f"https://searchfox.org/mozilla-central/search?q=symbol:{symbol}" elif range: filename, lineno = loc.split(":") [f0, l0] = range[0] [f1, l1] = range[1] if f0 == f1 and l1 > l0: return f"../{filename}?L={l0}-{l1 - 1}#{l0}" else: return f"../{filename}?L={l0}#{l0}" elif loc: filename, lineno = loc.split(":") return f"../{filename}?L={lineno}#{lineno}" else: raise Exception("missing argument to sourcelink()") def quoted_dict(d): return {k: escape(v) for k, v in d.items() if type(v) == str} num_hazards = 0 num_refs = 0 num_missing = 0 try: with open(args.rootingHazards) as rootingHazards, open( args.hazards, "w" ) as hazards, open(args.extra, "w") as extra, open(args.refs, "w") as refs, open( args.html, "w" ) as html: current_gcFunction = None hazardousGCFunctions = set() results = json.load(rootingHazards) print_header(html) when = min((r for r in results if r["record"] == "time"), key=lambda r: r["t"])[ "iso" ] line = f"Time: {when}" print(line, file=hazards) print(line, file=extra) print(line, file=refs) checkboxCounter = 0 hazard_results = [] seen_time = False for result in results: if result["record"] == "unrooted": hazard_results.append(result) gccall_mangled, _ = splitfunc(result["gccall"]) hazardousGCFunctions.add(gccall_mangled) if not result.get("expected"): num_hazards += 1 elif result["record"] == "unnecessary": print( "\nFunction '{mangled}' has unnecessary root '{variable}' of type {type} at {loc}".format( **result ), file=extra, ) elif result["record"] == "address": print( ( "\nFunction '{functionName}'" " takes unsafe address of unrooted '{variable}'" " at {loc}" ).format(**result), file=refs, ) num_refs += 1 elif result["record"] == "missing": print( "\nFunction '{functionName}' expected hazard(s) but none were found at {loc}".format( **result ), file=hazards, ) num_missing += 1 readable2mangled = {} with open(args.gcFunctions) as gcFunctions: gcExplanations = {} # gcFunction => stack showing why it can GC current_func = None explanation = [] for line in gcFunctions: if m := re.match(r"^GC Function: (.*)", line): if current_func: gcExplanations[splitfunc(current_func)[0]] = explanation functionName = m.group(1) mangled, readable = splitfunc(functionName) if mangled not in hazardousGCFunctions: current_func = None continue current_func = functionName if readable != mangled: readable2mangled[readable] = mangled # TODO: store the mangled name here, and change # gcFunctions.txt -> gcFunctions.json and key off of the mangled name. explanation = [readable] elif current_func: explanation.append(line.strip()) if current_func: gcExplanations[splitfunc(current_func)[0]] = explanation print( "Found %d hazards, %d unsafe references, %d missing." % (num_hazards, num_refs, num_missing), file=html, ) print("
    ", file=html) for result in hazard_results: (result["gccall_mangled"], result["gccall_readable"]) = splitfunc( result["gccall"] ) # Attempt to extract out the function name. Won't handle `Foo>::Foo()`. if m := re.search(r"((?:\w|:|<[^>]*?>)+)\(", result["gccall_readable"]): result["gccall_short"] = m.group(1) + "()" else: result["gccall_short"] = result["gccall_readable"] if result.get("expected"): print("\nThis is expected, but ", end="", file=hazards) else: print("\nFunction ", end="", file=hazards) print( "'{readable}' has unrooted '{variable}'" " of type '{type}' live across GC call '{gccall_readable}' at {loc}".format( **result ), file=hazards, ) for edge in result["trace"]: print(" {lineText}: {edgeText}".format(**edge), file=hazards) explanation = gcExplanations.get(result["gccall_mangled"]) explanation = explanation or gcExplanations.get( readable2mangled.get( result["gccall_readable"], result["gccall_readable"] ), [], ) if explanation: print("GC Function: " + explanation[0], file=hazards) for func in explanation[1:]: print(" " + func, file=hazards) print(file=hazards) if result.get("expected"): continue cfgid = f"CFG_{checkboxCounter}" gcid = f"GC_{checkboxCounter}" checkboxCounter += 1 print( ( "
    • \n" "
    • Function {readable}\n" "
    • has unrooted {variable} of type '{type}'\n" "
    • \n" "
      \n" ).format( **quoted_dict(result), symbol_url=sourcelink(symbol=result["mangled"]), cfgid=cfgid, ), file=html, ) for edge in result["trace"]: print( "
          {lineText}: {edgeText}
      ".format(**quoted_dict(edge)), file=html, ) print("
      ", file=html) print( "
    • \n" "
      ".format( **quoted_dict(result), loc_url=sourcelink(range=result["gcrange"], loc=result["loc"]), gcid=gcid, ), file=html, ) for func in explanation: print(f"
      {escape(func)}
      ", file=html) print("

    ", file=html) print_footer(html) except IOError as e: print("Failed: %s" % str(e)) if args.verbose: print("Wrote %s" % args.hazards) print("Wrote %s" % args.extra) print("Wrote %s" % args.refs) print("Wrote %s" % args.html) print( "Found %d hazards %d unsafe references %d missing" % (num_hazards, num_refs, num_missing) )