diff --git a/hw/ip/otbn/util/analyze_information_flow.py b/hw/ip/otbn/util/analyze_information_flow.py index ad253f2ea3592..84ffba2ea988f 100755 --- a/hw/ip/otbn/util/analyze_information_flow.py +++ b/hw/ip/otbn/util/analyze_information_flow.py @@ -48,7 +48,7 @@ def main() -> int: 'Initially secret information-flow nodes. If provided, the final ' 'secrets will be printed.')) args = parser.parse_args() - program = decode_elf(args.elf) + program = decode_elf(args.elf, []) # Compute control-flow graph. if args.subroutine is None: diff --git a/hw/ip/otbn/util/check_call_stack.py b/hw/ip/otbn/util/check_call_stack.py index 5a18daf9ea5cf..315a0eaa30a6a 100755 --- a/hw/ip/otbn/util/check_call_stack.py +++ b/hw/ip/otbn/util/check_call_stack.py @@ -51,7 +51,7 @@ def main() -> int: parser.add_argument('elf', help=('The .elf file to check.')) parser.add_argument('-v', '--verbose', action='store_true') args = parser.parse_args() - program = decode_elf(args.elf) + program = decode_elf(args.elf, []) result = check_call_stack(program) if args.verbose or result.has_errors() or result.has_warnings(): print(result.report()) diff --git a/hw/ip/otbn/util/check_const_time.py b/hw/ip/otbn/util/check_const_time.py index 88db042d499e9..7ee485e648e00 100755 --- a/hw/ip/otbn/util/check_const_time.py +++ b/hw/ip/otbn/util/check_const_time.py @@ -64,7 +64,7 @@ def main() -> int: constants = parse_required_constants(args.constants) # Compute control graph and get all nodes that influence control flow. - program = decode_elf(args.elf) + program = decode_elf(args.elf, args.ignore or []) if args.subroutine is None: graph = program_control_graph(program) to_analyze = 'entire program' @@ -75,14 +75,6 @@ def main() -> int: _, _, control_deps = get_subroutine_iflow(program, graph, args.subroutine, constants) - control_deps_ignore = {} - if args.ignore is not None: - for subroutine in args.ignore: - graph = subroutine_control_graph(program, subroutine) - _, _, control_deps_ignore_temp = get_subroutine_iflow(program, graph, - subroutine, {}) - control_deps_ignore.update(control_deps_ignore_temp) - if args.secrets is None: if args.verbose: print( @@ -100,15 +92,12 @@ def main() -> int: for node, pcs in control_deps.items() if node in args.secrets } - secret_control_deps_filt = {k: v for k, v in secret_control_deps.items() - if v not in control_deps_ignore.values()} - out = CheckResult() - if len(secret_control_deps_filt) != 0: + if len(secret_control_deps) != 0: msg = 'The following secrets may influence control flow:\n ' msg += '\n '.join(stringify_control_deps(program, - secret_control_deps_filt)) + secret_control_deps)) out.err(msg) if args.verbose or out.has_errors() or out.has_warnings(): diff --git a/hw/ip/otbn/util/check_loop.py b/hw/ip/otbn/util/check_loop.py index 8b21337bb3480..b0e22105caec6 100755 --- a/hw/ip/otbn/util/check_loop.py +++ b/hw/ip/otbn/util/check_loop.py @@ -224,7 +224,7 @@ def main() -> int: parser.add_argument('elf', help=('The .elf file to check.')) parser.add_argument('-v', '--verbose', action='store_true') args = parser.parse_args() - program = decode_elf(args.elf) + program = decode_elf(args.elf, []) result = check_loop(program) if args.verbose or result.has_errors() or result.has_warnings(): print(result.report()) diff --git a/hw/ip/otbn/util/get_instruction_count_range.py b/hw/ip/otbn/util/get_instruction_count_range.py index ebeab90d10ad6..cdcc30d233089 100755 --- a/hw/ip/otbn/util/get_instruction_count_range.py +++ b/hw/ip/otbn/util/get_instruction_count_range.py @@ -23,7 +23,7 @@ def main() -> int: help=('The specific subroutine to check. If not provided, the start ' 'point is _imem_start (whole program).')) args = parser.parse_args() - program = decode_elf(args.elf) + program = decode_elf(args.elf, []) # Compute instruction count range. if args.subroutine is None: diff --git a/hw/ip/otbn/util/shared/decode.py b/hw/ip/otbn/util/shared/decode.py index 999fbdda44360..e2a713f929b28 100644 --- a/hw/ip/otbn/util/shared/decode.py +++ b/hw/ip/otbn/util/shared/decode.py @@ -20,19 +20,63 @@ class OTBNProgram: def __init__(self, symbols: Dict[str, int], insns: Dict[int, int], - data: Dict[int, int]): + data: Dict[int, int], nop_subfuncs: List[str]): self.symbols = symbols # label -> PC - self.data = data # addr -> data (32b word) + self.data = data # addr -> data (32b word) + + # For each function name in nop_subfuncs, we assume that it will have + # control flow limited in the following way. If the function starts at + # address A and the first JALR instruction after A is at address B, + # then execution will not leave the interval [A, B] until the function + # returns by executing the JALR at B. + # + # With this assumption, we can imagine replacing the instructions in + # that interval with a "nop sled". The ranges of the these sleds are + # stored in the nop_ranges list, as pairs (A, B). + nop_ranges: list[tuple[int, int]] = [] + + for symbol in nop_subfuncs: + # Get the start address of the function. If the function dosen't + # appear in the list of symbols, there's nothing to do for it. + start_addr = symbols.get(symbol) + if start_addr is None: + continue + + for pc in range(start_addr, 1 << 32, 4): + opcode = insns.get(pc) + if opcode is None: + raise RuntimeError("Fell off the end of the binary " + "when searching for a JALR for a " + f"function with symbol {symbol}, " + f"starting at {start_addr:#x}") + + # Check whether we just found 'B' (see note above nop_ranges) + if INSNS_FILE.mnem_for_word(opcode) == 'jalr': + nop_ranges.append((start_addr, pc)) + break self.insns = {} for pc, opcode in insns.items(): - mnem = INSNS_FILE.mnem_for_word(opcode) - if mnem is None: - raise ValueError( - 'No legal decoding for mnemonic: {}'.format(mnem)) - insn = INSNS_FILE.mnemonic_to_insn[mnem] - assert insn.encoding is not None - enc_vals = insn.encoding.extract_operands(opcode) + # Check if PC lies within one of the NOP ranges (equal to or after + # the start of a nop subfunc and strictly before the JALR + # instruction at the end). + in_nop_region = any(a <= pc < b for a, b in nop_ranges) + + # If the PC *is* in a NOP region, interpret the opcode at PC as a + # NOP (addi x0, x0, x0). If not, decode the opcode and find the + # appropriate instruction and operands. + if in_nop_region: + insn = INSNS_FILE.mnemonic_to_insn["addi"] + enc_vals = {'imm': 0, 'grs1': 0, 'grd': 0} + else: + mnem = INSNS_FILE.mnem_for_word(opcode) + if mnem is None: + raise ValueError(f'No mnemonic for opcode {opcode:#08x}') + + insn = INSNS_FILE.mnemonic_to_insn[mnem] + assert insn.encoding is not None + enc_vals = insn.encoding.extract_operands(opcode) + op_vals = insn.enc_vals_to_op_vals(pc, enc_vals) self.insns[pc] = (insn, op_vals) @@ -69,7 +113,7 @@ def _decode_mem(base_addr: int, data: bytes) -> Dict[int, int]: for offset, int_val in enumerate(struct.iter_unpack(' OTBNProgram: +def decode_elf(path: str, nop_subfuncs: List[str]) -> OTBNProgram: '''Read ELF file at path and decode contents into an OTBNProgram instance Returns the OTBNProgram instance representing the program in the ELF file. @@ -79,4 +123,4 @@ def decode_elf(path: str) -> OTBNProgram: insns = _decode_mem(0, imem_bytes) data = _decode_mem(0, dmem_bytes) - return OTBNProgram(symbols, insns, data) + return OTBNProgram(symbols, insns, data, nop_subfuncs)