Skip to content

Commit

Permalink
[util,topgen] Trigger errors for countermeasures missing in RTL
Browse files Browse the repository at this point in the history
The topgen script has all the information needed to check
countermeasures, specifically how any module is generated.
Move the check from regtool to topgen.

Fixes lowRISC#10071

Signed-off-by: Guillermo Maturana <[email protected]>
  • Loading branch information
matutem committed May 16, 2024
1 parent 69c572b commit a763a6c
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 72 deletions.
73 changes: 38 additions & 35 deletions util/reggen/countermeasure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions util/reggen/ip_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
11 changes: 0 additions & 11 deletions util/regtool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
104 changes: 82 additions & 22 deletions util/topgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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}"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit a763a6c

Please sign in to comment.