diff --git a/util/reggen/countermeasure.py b/util/reggen/countermeasure.py index 4d3a8c8a92496..8891d5af6f03a 100644 --- a/util/reggen/countermeasure.py +++ b/util/reggen/countermeasure.py @@ -114,8 +114,7 @@ def from_raw(what: str, raw: object) -> 'CounterMeasure': return CounterMeasure(instance, asset, cm_type, desc) @staticmethod - def from_raw_list(what: str, - raw: object) -> List['CounterMeasure']: + def from_raw_list(what: str, raw: object) -> List['CounterMeasure']: """ Create a list of CounterMeasure objects from a list of dicts. @@ -131,18 +130,15 @@ def from_raw_list(what: str, def _asdict(self) -> Dict[str, object]: """Returns a dict with 'name' and 'desc' fields""" - return { - 'name': str(self), - 'desc': self.desc - } + return {'name': str(self), 'desc': self.desc} def __str__(self) -> str: namestr = self.asset + '.' + self.cm_type return self.instance + '.' + namestr if self.instance else namestr @staticmethod - def search_rtl_files(paths: Sequence[str]) -> Dict[str, - List[Tuple[str, int]]]: + def search_rtl_files( + paths: Sequence[str]) -> Dict[str, List[Tuple[str, int]]]: """Find countermeasures in the given list of RTL files. The return value is a dictionary mapping countermeasure name to where @@ -171,45 +167,52 @@ def search_rtl_files(paths: Sequence[str]) -> Dict[str, if not good_name.match(entry): raise ValueError('Malformed countermeasure name, ' - '{!r}, at {}, line {}.' - .format(entry, path, idx + 1)) + '{!r}, at {}, line {}.'.format( + entry, path, idx + 1)) ret.setdefault(entry, []).append((path, idx + 1)) return ret @staticmethod - def check_annotation_list(what: str, + def check_annotation_list(block_name: str, hjson_path: str, rtl_names: Dict[str, List[Tuple[str, int]]], - hjson_list: List['CounterMeasure']) -> None: - """Compare found list of countermeasures against list from Hjson + hjson_list: List['CounterMeasure']) -> bool: + """Compare RTL to Hjson countermeasures. This compares a dictionary of countermeasure names extracted from the - RTL against the list defined in the IP Hjson and checks that they - match: every name in the RTL should correspond to an entry in the Hjson - and every entry in the Hjson should have at least one matching name in - the RTL. + RTL against the list defined in the IP Hjson and checks that: + - every name in the RTL should match one in Hjson + - every entry in Hjson should have some matching name in the RTL + Any mismatch logs an error showing the file and line of the problem. + Returns True if there are no errors. """ hjson_set = {str(cm) for cm in hjson_list} rtl_set = set(rtl_names.keys()) # Is there anything in the RTL that doesn't correspond to an Hjson # entry? - for name in rtl_set - hjson_set: - # Print out an error message for each hit and then raise a - # RuntimeError. - for path, line in rtl_names[name]: - log.error('Unknown countermeasure {!r} ' - 'referenced at {}, line {}.' - .format(name, path, line)) - raise RuntimeError('One or more unknown countermeasures ' - 'referenced in RTL.') - - # Is there anything in the Hjson that isn't described in the RTL? - for name in hjson_set - rtl_set: - log.warning("Countermeasure {} is referenced in the Hjson of the " - "{}, but doesn't appear in the RTL." - .format(name, what)) - - # TODO(#10071): Once all designs are annotated, generate a RuntimeError - # if we saw anything in the loop above. + rtl_only_countermeasures = rtl_set - hjson_set + for name in rtl_only_countermeasures: + for rtl_path, line in rtl_names[name]: + log.error(f"No Hjson countermeasure {name} exists for RTL " + f"reference at {rtl_path}, line {line}.") + + missing_countermeasures = hjson_set - rtl_set + for name in missing_countermeasures: + log.error(f"Hjson countermeasure {name} declared in Hjson file " + f"{hjson_path} doesn't appear in the RTL.") + + if len(rtl_only_countermeasures) > 0: + if len(missing_countermeasures) > 0: + log.error(f"Block {block_name}: Some unknown and some missing " + "countermeasures in RTL") + else: + log.error(f"Block {block_name}: Some unknown countermeasures " + "referenced in RTL.") + return False + elif len(missing_countermeasures) > 0: + log.error(f"Block {block_name}: Some countermeasures in Hjson " + "don't appear in RTL.") + return False + return True diff --git a/util/reggen/ip_block.py b/util/reggen/ip_block.py index 766cd823f7280..f468b84770289 100644 --- a/util/reggen/ip_block.py +++ b/util/reggen/ip_block.py @@ -611,9 +611,9 @@ def get_primary_clock(self) -> ClockingItem: return self.clocking.primary def check_cm_annotations(self, rtl_names: Dict[str, List[Tuple[str, int]]], - where: str) -> None: + hjson_path: str) -> bool: '''Check RTL annotations against countermeasure list of this block''' - what = '{} block at {}'.format(self.name, where) - CounterMeasure.check_annotation_list(what, rtl_names, - self.countermeasures) + return CounterMeasure.check_annotation_list(self.name, hjson_path, + rtl_names, + self.countermeasures) diff --git a/util/regtool.py b/util/regtool.py index 2b643ac5f1cd9..2d290f711f4d0 100755 --- a/util/regtool.py +++ b/util/regtool.py @@ -15,7 +15,6 @@ gen_cfg_md, gen_cheader, gen_dv, gen_fpv, gen_md, gen_html, gen_json, gen_rtl, gen_rust, gen_sec_cm_testplan, gen_selfdoc, gen_tock, version, ) -from reggen.countermeasure import CounterMeasure from reggen.ip_block import IpBlock import version_file @@ -258,16 +257,6 @@ def main(): raise ValueError('The --scrub argument is only meaningful in ' 'combination with the --alias argument') - # If this block has countermeasures, we grep for RTL annotations in all - # .sv implementation files and check whether they match up with what is - # defined inside the Hjson. - # Skip this check when generating DV code - its not needed. - if fmt != 'dv': - sv_files = Path( - infile.name).parent.joinpath('..').joinpath('rtl').glob('*.sv') - rtl_names = CounterMeasure.search_rtl_files(sv_files) - obj.check_cm_annotations(rtl_names, infile.name) - if args.novalidate: with outfile: gen_json.gen_json(obj, outfile, fmt) diff --git a/util/topgen.py b/util/topgen.py index 7a3e8ec8226db..df4534447c2c3 100755 --- a/util/topgen.py +++ b/util/topgen.py @@ -232,10 +232,10 @@ def generate_plic(top: Dict[str, object], out_path: Path) -> None: ipgen_render("rv_plic", topname, params, out_path) -# TODO: For generated IPs that are generated legacy style (i.e., without IPgen) -# we have to search both the source and destination RTL directories, since not -# all files are copied over. This is a workaround which can be removed once -# all generated IPs have transitioned to IPgen. +# TODO(lowrisc/opentitan#8440): For templated IPs we have to search +# both the source and destination RTL directories, since not all files +# are copied over. This is a workaround which can be removed once all +# generated IPs have transitioned to IPgen. def generate_regfile_from_path(hjson_path: Path, generated_rtl_path: Path, original_rtl_path: Path = None) -> None: @@ -518,8 +518,7 @@ def generate_top_only(top_only_dict: Dict[str, bool], out_path: Path, if reggen_only and alt_hjson_path is not None: hjson_dir = Path(alt_hjson_path) else: - hjson_dir = (SRCTREE_TOP / "hw" / top_name / "ip" / ip / - "data") + hjson_dir = (SRCTREE_TOP / "hw" / top_name / "ip" / ip / "data") hjson_path = hjson_dir / f"{ip}.hjson" @@ -668,9 +667,10 @@ def render_template(template_path: str, rendered_path: Path, **other_info): helper=rs_helper) -def _process_top(topcfg: Dict[str, object], args: argparse.Namespace, - cfg_path: Path, out_path: Path, - pass_idx: int) -> (Dict[str, object], Dict[str, IpBlock]): +def _process_top( + topcfg: Dict[str, object], args: argparse.Namespace, cfg_path: Path, + out_path: Path, pass_idx: int +) -> (Dict[str, object], Dict[str, IpBlock], Dict[str, Path]): # Create generated list # These modules are generated through topgen templated_list = lib.get_templated_modules(topcfg) @@ -745,6 +745,7 @@ def _process_top(topcfg: Dict[str, object], args: argparse.Namespace, ips.append(ip_hjson) # load Hjson and pass validate from reggen + name_to_hjson = {} # type Dict[str, Path] try: ip_objs = [] for ip_desc_file in ips: @@ -779,6 +780,7 @@ def _process_top(topcfg: Dict[str, object], args: argparse.Namespace, except TemplateRenderError as e: log.error(e.verbose_str()) sys.exit(1) + name_to_hjson[ip_name.lower()] = tpl_path s = "default description of IP template {}".format(ip_name) ip_objs.append(IpBlock.from_text(ip_desc, [], s)) else: @@ -791,10 +793,11 @@ def _process_top(topcfg: Dict[str, object], args: argparse.Namespace, "Falling back to Hjson description file %s shipped " "with the IP template for initial validation." % (ip_desc_file, template_hjson_file)) - + name_to_hjson[ip_name] = template_hjson_file ip_objs.append( IpBlock.from_path(str(template_hjson_file), [])) else: + name_to_hjson[ip_name] = ip_desc_file ip_objs.append(IpBlock.from_path(str(ip_desc_file), [])) except ValueError: @@ -870,7 +873,43 @@ def _process_top(topcfg: Dict[str, object], args: argparse.Namespace, # These modules are not templated, but are not in hw/ip generate_top_only(top_only_dict, out_path, top_name, args.hjson_path) - return completecfg, name_to_block + return completecfg, name_to_block, name_to_hjson + + +def _check_countermeasures(completecfg: Dict[str, object], + name_to_block: Dict[str, IpBlock], + name_to_hjson: Dict[str, Path]) -> bool: + success = True + for name, hjson_path in name_to_hjson.items(): + log.debug("name %s, hjson %s", name, hjson_path) + ip_index = [m['type'] for m in completecfg['module']].index(name) + # TODO(lowrisc/opentitan#8440): For templated IPs we have to search + # both the source and destination RTL directories, since not all files + # are copied over. This is a workaround which can be removed once all + # generated IPs have transitioned to IPgen. + if ('attr' in completecfg['module'][ip_index] and + completecfg['module'][ip_index]['attr'] == 'templated'): + ip_proper_rtl_path = hjson_path.parents[5] / 'ip' / name / 'rtl' + sv_files = (hjson_path.parents[2] / 'rtl' / 'autogen').glob('*.sv') + sv_files = set(sv_files) + sv_names = set([f.name for f in sv_files]) + proper_rtl_files = { + f + for f in ip_proper_rtl_path.glob('*.sv') + if f.name not in sv_names + } + sv_files = sv_files | proper_rtl_files + else: + sv_files = (hjson_path.parents[1] / 'rtl').glob('*.sv') + rtl_names = CounterMeasure.search_rtl_files(sv_files) + log.debug("Checking countermeasures for %s.", name) + success &= name_to_block[name].check_cm_annotations( + rtl_names, hjson_path.name) + if success: + log.info("All Hjson declared countermeasures are implemented in RTL.") + else: + log.error("Countermeasure checks failed.") + return success def main(): @@ -923,6 +962,16 @@ def main(): "--top-only", action="store_true", help="If defined, the tool generates top RTL only") # yapf:disable + parser.add_argument("--check-cm", + action="store_true", + help=''' + Check countermeasures. + + Check countermeasures of all modules in the top config. All + countermeasures declared in the module's hjson file should + be implemented in the RTL, and the RTL should only + contain countermeasures declared there. + ''') parser.add_argument( "--xbar-only", action="store_true", @@ -991,13 +1040,13 @@ def main(): raise SystemExit(sys.exc_info()[1]) # Don't print warnings when querying the list of blocks. - log_level = (log.ERROR - if args.get_blocks else log.DEBUG if args.verbose else None) + log_level = (log.ERROR if args.get_blocks or args.check_cm else + log.DEBUG if args.verbose else None) log.basicConfig(format="%(levelname)s: %(message)s", level=log_level) if not args.outdir: - outdir = Path(args.topcfg).parent / ".." + outdir = Path(args.topcfg).parents[1] log.info("TOP directory not given. Use %s", (outdir)) elif not Path(args.outdir).is_dir(): log.error("'--outdir' should point to writable directory") @@ -1077,11 +1126,11 @@ def main(): log.debug("Generation pass {}".format(pass_idx)) if pass_idx < process_dependencies: cfg_copy = deepcopy(topcfg) - _, _ = _process_top(cfg_copy, args, cfg_path, out_path_gen, - pass_idx) + _, _, _ = _process_top(cfg_copy, args, cfg_path, out_path_gen, + pass_idx) else: - completecfg, name_to_block = _process_top(topcfg, args, cfg_path, - out_path_gen, pass_idx) + completecfg, name_to_block, name_to_hjson = _process_top( + topcfg, args, cfg_path, out_path_gen, pass_idx) topname = topcfg["name"] top_name = f"top_{topname}" @@ -1133,6 +1182,19 @@ def main(): if args.rust_only: sys.exit(0) + # Check countermeasures for all blocks. + if args.check_cm: + # Change verbosity to log.INFO to see an okay confirmation message: + # the log level is set to log.ERROR upon start to avoid the chatter + # of the regular topgen elaboration. + log.basicConfig(format="%(levelname)s: %(message)s", + level=log.INFO, + force=True) + + okay = _check_countermeasures(completecfg, name_to_block, + name_to_hjson) + sys.exit(0 if okay else 1) + if not args.no_top or args.top_only: def render_template(template_path: str, rendered_path: Path, @@ -1179,8 +1241,7 @@ def render_template(template_path: str, rendered_path: Path, # compile-time random netlist constants render_template(TOPGEN_TEMPLATE_PATH / "toplevel_rnd_cnst_pkg.sv.tpl", - out_path / - f"rtl/autogen/{top_name}_rnd_cnst_pkg.sv", + out_path / f"rtl/autogen/{top_name}_rnd_cnst_pkg.sv", gencmd=gencmd) # Since SW does not use FuseSoC and instead expects those files always @@ -1190,8 +1251,7 @@ def render_template(template_path: str, rendered_path: Path, # - Once under hw/top_{topname}/sw/autogen root_paths = [out_path.resolve(), SRCTREE_TOP] out_paths = [ - out_path.resolve(), - (SRCTREE_TOP / "hw" / top_name).resolve() + out_path.resolve(), (SRCTREE_TOP / "hw" / top_name).resolve() ] for idx, path in enumerate(out_paths): # C Header + C File + Clang-format file