# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 """CBMC reachable functions""" import json import logging import os import subprocess import tempfile 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-reachable' ################################################################ # Reachable validator VALID_REACHABLE = voluptuous.Schema({ 'reachable': { # file name -> list of reachable functions voluptuous.Optional(str): [str] } }, required=True) ################################################################ class Reachable: """CBMC reachable functions.""" # TODO: again dicts -> dict def __init__(self, function_lists=None): """Load CBMC reachable functions from lists of functions.""" function_lists = function_lists or [] functions = self.merge_function_lists(function_lists) self.reachable = self.sort_function_names(functions) self.validate() def __repr__(self): """A dict representation of the reachable functions.""" self.validate() return self.__dict__ def __str__(self): """A string representation of the reachable functions.""" return json.dumps({JSON_TAG: self.__repr__()}, indent=2, sort_keys=True) def validate(self): """Validate reachable functions.""" return voluptuous.humanize.validate_with_humanized_errors( self.__dict__, VALID_REACHABLE ) def dump(self, filename=None, directory=None): """Write reachable functions to a file or stdout.""" util.dump(self, filename, directory) @staticmethod def merge_function_lists(function_lists): """Merge lists of functions into a single list.""" if len(function_lists) == 1: return function_lists[0] functions = {} for function_list in function_lists: for file_name, func_names in function_list.items(): functions[file_name] = functions.get(file_name, set()) functions[file_name] = functions[file_name].union(func_names) return functions @staticmethod def sort_function_names(functions): """Sort the names of reachable functions in a file.""" return {file_name: sorted(func_names) for file_name, func_names in functions.items()} ################################################################ class ReachableFromJson(Reachable): """Load the reachable functions from the output of make-reachable. Given a list of json files containing symbol tables produced by make-reachable, merge these function lists into a single list. """ def __init__(self, json_files): super().__init__( [parse.parse_json_file(json_file)[JSON_TAG]["reachable"] for json_file in json_files] ) ################################################################ class ReachableFromCbmcJson(Reachable): """Load functions from output of goto-analyzer --reachable-functions --json. """ 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 json file produced by goto-analyzer --reachable-functions --json json_file""" json_data = parse.parse_json_file(json_file) return parse_cbmc_json(json_data, root) # TODO: Does file-local name mangling mess this up? def parse_cbmc_json(json_data, root): """Parse json output of goto-analyzer --reachable-functions --json.""" root = srcloct.abspath(root) reachable = {} for function in json_data: func_name = function['function'] if func_name.startswith('__CPROVER'): continue # goto-analyzer --reachable-functions produces a malformed # entry for __CPROVER_Start with an empty string for the # function name. if not func_name: continue file_name = function.get('file') if file_name is None: logging.warning('Skipping reachable function with invalid source location: %s', function) continue file_name = srcloct.abspath(file_name) if srcloct.is_builtin(file_name): continue if not file_name.startswith(root): continue path = srcloct.relpath(file_name, root) reachable[path] = reachable.get(path, set()) reachable[path].add(func_name) return reachable ################################################################ class ReachableFromCbmcXml(Reachable): """Load functions from output of goto-analyzer --reachable-functions --xml. """ def __init__(self, xml_files, root): super().__init__([]) _, _ = xml_files, root raise UserWarning("The goto-analyzer --xml option generates text " "and not xml.") ################################################################ class ReachableFromGoto(Reachable): """Load reachable functions of a goto binary.""" def __init__(self, goto, root, cwd=None): # pylint: disable=consider-using-with data = tempfile.NamedTemporaryFile(delete=False) cmd = ["goto-analyzer", "--reachable-functions", "--json", data.name, goto] try: runt.run(cmd, cwd=cwd) except subprocess.CalledProcessError as err: raise UserWarning(f'Failed to run {cmd}: {err}') from err json_data = parse.parse_json_file(data.name) os.unlink(data.name) super().__init__( [parse_cbmc_json(json_data, root)] ) ################################################################ # make-reachable # pylint: disable=inconsistent-return-statements def fail(msg): """Log failure and raise exception.""" logging.info(msg) raise UserWarning(msg) def make_reachable(args): """The implementation of make-reachable.""" viewer_reachable, cbmc_reachable, srcdir, goto = ( args.viewer_reachable, None, args.srcdir, args.goto) if viewer_reachable: if filet.all_json_files(viewer_reachable): return ReachableFromJson(viewer_reachable) fail(f"Expected json files: {viewer_reachable}") if cbmc_reachable and srcdir: if filet.all_json_files(cbmc_reachable): return ReachableFromCbmcJson(cbmc_reachable, srcdir) if filet.all_xml_files(cbmc_reachable): return ReachableFromCbmcXml(cbmc_reachable, srcdir) fail(f"Expected json files or xml files, not both: {cbmc_reachable}") if goto and srcdir: return ReachableFromGoto(goto, srcdir) logging.info("make-reachable: nothing to do: need " "--srcdir and --goto, or " "cbmc reachable functions results (goto-analyzer --reachable-functions)" " and --srcdir, or " "--viewer-reachable") return Reachable() def make_and_save_reachable(args, path=None): """Make reachable object and write to file or stdout""" obj = make_reachable(args) util.save(obj, path) return obj ################################################################