# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """CBMC loop information""" import json import logging import re import subprocess import voluptuous import voluptuous.humanize from cbmc_viewer import filet from cbmc_viewer import parse from cbmc_viewer import runt from cbmc_viewer import srcloct from cbmc_viewer import util JSON_TAG = 'viewer-loop' ################################################################ # Loop validator VALID_LOOP = voluptuous.Schema({ 'loops': { # loop name -> loop srcloc voluptuous.Optional(str): srcloct.VALID_SRCLOC } }, required=True) ################################################################ class Loop: """CBMC loop information.""" def __init__(self, loop_maps=None): """Load CBMC loop map from a lists of loop maps.""" loop_maps = loop_maps or [] def handle_duplicates(name, srcloc1, srcloc2): logging.warning("Found duplicate loop definition: " "%s: %s %s <- %s %s", name, srcloc1["file"], srcloc1["line"], srcloc2["file"], srcloc2["line"]) self.loops = util.merge_dicts(loop_maps, handle_duplicates) self.loops = self.unmangle_loop_names(self.loops) self.validate() def __repr__(self): """A dict representation of the loop table.""" self.validate() return self.__dict__ def __str__(self): """A string representation of the loop table.""" return json.dumps({JSON_TAG: self.__repr__()}, indent=2, sort_keys=True) def validate(self): """Validate loops.""" return voluptuous.humanize.validate_with_humanized_errors( self.__dict__, VALID_LOOP ) def dump(self, filename=None, directory=None): """Write loops to a file or stdout.""" util.dump(self, filename, directory) # TODO: generally don't fail silently, give warning or raise exception # TODO: generally give hints for how to fix the problem (give actions to take) @staticmethod def unmangle_loop_names(loops): """Reverse CBMC name mangling of loop names in static functions.""" # Loops in static function FUNC may have names FUNC$link1.0 static_names = [name for name in loops if '$link' in name] # Replace the name FUNC$link1.0 with FUNC.0 for long_name in static_names: long_func, index = long_name.split('.') short_func = long_func.split('$')[0] short_name = f'{short_func}.{index}' assert long_name != short_name if short_name in loops: if loops[short_name] != loops[long_name]: logging.warning("Found duplicate loop definition: " "%s: %s %s <- %s %s", short_name, loops[short_name]["file"], loops[short_name]["line"], loops[long_name]["file"], loops[long_name]["line"]) continue loops[short_name] = loops[long_name] del loops[long_name] return loops def names(self): """Loop names.""" return self.loops.keys() def lookup(self, name): """Look up the srcloc for the named loop.""" return self.loops.get(name) def lookup_static(self, name): """Look up the srcloc for the named loop in a static function.""" # CBMC property checking refers to loop K in function FCN as FCN.K, # but the goto-cc --export-file-local-symbols flag renames FCN.K # appearing in a file FILE in a static function FCN as # __CPROVER_file_local_FILE_FCN_LOOP.N keys = [key for key in self.loops.keys() if key.endswith('_'+name)] if not keys: return None if len(keys) != 1: raise UserWarning(f"Loop name {name} matches {len(keys)} static loop names: {keys}") return self.loops.get(keys[0]) def lookup_assertion(self, name): """Look up the srcloc for the named loop unwinding assertion.""" # CBMC refers to the loop K in function FCN as FCN.K and to # the unwinding assertion associated with that loop as # FCN.unwind.K match = re.match(r'^(.*)\.unwind\.([0-9]+)$', name) if match is None: return None loop = f'{match.group(1)}.{match.group(2)}' return self.lookup(loop) or self.lookup_static(loop) ################################################################ class LoopFromJson(Loop): """Load cbmc loop table from output of make-loop. Given a list of json files containing symbol tables produced by make-loop, merge these loop tables into a single loop table. """ def __init__(self, json_files): super().__init__( [parse.parse_json_file(json_file)[JSON_TAG]['loops'] for json_file in json_files] ) ################################################################ class LoopFromCbmcJson(Loop): """Load loop table from output of 'cbmc --show-loops --json-ui.""" def __init__(self, json_files, root): super().__init__( [load_cbmc_json(json_file, root) for json_file in json_files] ) def load_cbmc_json(json_file, root): """Load a json file produced by cbmc --show-loops --json-ui.""" return parse_cbmc_json(parse.parse_json_file(json_file), root) def parse_cbmc_json(json_data, root): """Parse the json output of cbmc --show-loops --json-ui.""" # Search cbmc output for {"loops": [ LOOP ]} loops = [json_map for json_map in json_data if "loops" in json_map] if len(loops) != 1: raise UserWarning(f"Expected 1 set of loops in cbmc output, found {len(loops)}") # Each LOOP is a dict that gives a loop name and location. root = srcloct.abspath(root) return {loop['name']: srcloct.json_srcloc(loop['sourceLocation'], root) for loop in loops[0]["loops"]} ################################################################ class LoopFromCbmcXml(Loop): """Load loop table from output of 'cbmc --show-loops --xml-ui.""" def __init__(self, xml_files, root): super().__init__( [load_cbmc_xml(xml_file, root) for xml_file in xml_files] ) def load_cbmc_xml(xml_file, root): """Load an xml file produced by cbmc --show-loops --xml-ui.""" return parse_cbmc_xml(parse.parse_xml_file(xml_file), root) def parse_cbmc_xml(xml_data, root): """Parse the xml output of cbmc --show-loops --xml-ui.""" # Each loop element contains loop-id and location elements root = srcloct.abspath(root) return {loop.find("loop-id").text: srcloct.xml_srcloc(loop.find("location"), root) for loop in xml_data.iter("loop")} ################################################################ class LoopFromGoto(Loop): """Load loop table from a goto binary.""" def __init__(self, goto, root, cwd=None): cmd = ['cbmc', '--show-loops', '--json-ui', goto] try: super().__init__( [parse_cbmc_json(json.loads(runt.run(cmd, cwd=cwd)), root)] ) except subprocess.CalledProcessError as err: raise UserWarning(f'Failed to run {cmd}: {err}') from err except json.decoder.JSONDecodeError as err: raise UserWarning(f'Failed to parse output of {cmd}: {err}') from err ################################################################ # make-loop # pylint: disable=inconsistent-return-statements def fail(msg): """Log failure and raise exception.""" logging.info(msg) raise UserWarning(msg) def make_loop(args): """The implementation of make-loop""" viewer_loop, cbmc_loop, srcdir, goto = args.viewer_loop, None, args.srcdir, args.goto if viewer_loop: if filet.all_json_files(viewer_loop): return LoopFromJson(viewer_loop) fail(f"Expected json files: {viewer_loop}") if cbmc_loop and srcdir: if filet.all_json_files(cbmc_loop): return LoopFromCbmcJson(cbmc_loop, srcdir) if filet.all_xml_files(cbmc_loop): return LoopFromCbmcXml(cbmc_loop, srcdir) fail(f"Expected json files or xml files, not both: {cbmc_loop}") if goto and srcdir: return LoopFromGoto(goto, srcdir) logging.info("make-loop: nothing to do: need " "--goto and --srcdir, or " "cbmc loop listing results (cbmc --show-loops) and --srcdir, or " "--viewer-loop") return Loop() def make_and_save_loop(args, path=None): """Make loop object and write to file or stdout""" obj = make_loop(args) util.save(obj, path) return obj ################################################################