Skip to content

Commit e431c33

Browse files
h-filalirswarbrick
andcommitted
[sw,otbnsim] Replace ignored subfunction by nops
This allows subfunctions to be replaced by NOPs for constant time tests. This is useful for the case where a function runs in constant time under normal conditions but is not necessarily constant time in case of FI. Co-authored-by: Rupert Swarbrick <[email protected]> Signed-off-by: Hakim Filali <[email protected]>
1 parent bc4814e commit e431c33

File tree

6 files changed

+62
-29
lines changed

6 files changed

+62
-29
lines changed

hw/ip/otbn/util/analyze_information_flow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def main() -> int:
5353
'Initially secret information-flow nodes. If provided, the final '
5454
'secrets will be printed.'))
5555
args = parser.parse_args()
56-
program = decode_elf(args.elf)
56+
program = decode_elf(args.elf, [])
5757

5858
# Compute control-flow graph.
5959
if args.subroutine is None:

hw/ip/otbn/util/check_call_stack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def main() -> int:
5151
parser.add_argument('elf', help=('The .elf file to check.'))
5252
parser.add_argument('-v', '--verbose', action='store_true')
5353
args = parser.parse_args()
54-
program = decode_elf(args.elf)
54+
program = decode_elf(args.elf, [])
5555
result = check_call_stack(program)
5656
if args.verbose or result.has_errors() or result.has_warnings():
5757
print(result.report())

hw/ip/otbn/util/check_const_time.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def main() -> int:
6464
constants = parse_required_constants(args.constants)
6565

6666
# Compute control graph and get all nodes that influence control flow.
67-
program = decode_elf(args.elf)
67+
program = decode_elf(args.elf, args.ignore or [])
6868
if args.subroutine is None:
6969
graph = program_control_graph(program)
7070
to_analyze = 'entire program'
@@ -75,14 +75,6 @@ def main() -> int:
7575
_, _, control_deps = get_subroutine_iflow(program, graph,
7676
args.subroutine, constants)
7777

78-
control_deps_ignore = {}
79-
if args.ignore is not None:
80-
for subroutine in args.ignore:
81-
graph = subroutine_control_graph(program, subroutine)
82-
_, _, control_deps_ignore_temp = get_subroutine_iflow(program, graph,
83-
subroutine, {})
84-
control_deps_ignore.update(control_deps_ignore_temp)
85-
8678
if args.secrets is None:
8779
if args.verbose:
8880
print(
@@ -100,15 +92,12 @@ def main() -> int:
10092
for node, pcs in control_deps.items() if node in args.secrets
10193
}
10294

103-
secret_control_deps_filt = {k: v for k, v in secret_control_deps.items()
104-
if v not in control_deps_ignore.values()}
105-
10695
out = CheckResult()
10796

108-
if len(secret_control_deps_filt) != 0:
97+
if len(secret_control_deps) != 0:
10998
msg = 'The following secrets may influence control flow:\n '
11099
msg += '\n '.join(stringify_control_deps(program,
111-
secret_control_deps_filt))
100+
secret_control_deps))
112101
out.err(msg)
113102

114103
if args.verbose or out.has_errors() or out.has_warnings():

hw/ip/otbn/util/check_loop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ def main() -> int:
224224
parser.add_argument('elf', help=('The .elf file to check.'))
225225
parser.add_argument('-v', '--verbose', action='store_true')
226226
args = parser.parse_args()
227-
program = decode_elf(args.elf)
227+
program = decode_elf(args.elf, [])
228228
result = check_loop(program)
229229
if args.verbose or result.has_errors() or result.has_warnings():
230230
print(result.report())

hw/ip/otbn/util/get_instruction_count_range.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def main() -> int:
2323
help=('The specific subroutine to check. If not provided, the start '
2424
'point is _imem_start (whole program).'))
2525
args = parser.parse_args()
26-
program = decode_elf(args.elf)
26+
program = decode_elf(args.elf, [])
2727

2828
# Compute instruction count range.
2929
if args.subroutine is None:

hw/ip/otbn/util/shared/decode.py

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,63 @@
2020

2121
class OTBNProgram:
2222
def __init__(self, symbols: Dict[str, int], insns: Dict[int, int],
23-
data: Dict[int, int]):
23+
data: Dict[int, int], nop_subfuncs: List[str]):
2424
self.symbols = symbols # label -> PC
25-
self.data = data # addr -> data (32b word)
25+
self.data = data # addr -> data (32b word)
26+
27+
# For each function name in nop_subfuncs, we assume that it will have
28+
# control flow limited in the following way. If the function starts at
29+
# address A and the first JALR instruction after A is at address B,
30+
# then execution will not leave the interval [A, B] until the function
31+
# returns by executing the JALR at B.
32+
#
33+
# With this assumption, we can imagine replacing the instructions in
34+
# that interval with a "nop sled". The ranges of the these sleds are
35+
# stored in the nop_ranges list, as pairs (A, B).
36+
nop_ranges: list[tuple[int, int]] = []
37+
38+
for symbol in nop_subfuncs:
39+
# Get the start address of the function. If the function dosen't
40+
# appear in the list of symbols, there's nothing to do for it.
41+
start_addr = symbols.get(symbol)
42+
if start_addr is None:
43+
continue
44+
45+
for pc in range(start_addr, 1 << 32, 4):
46+
opcode = insns.get(pc)
47+
if opcode is None:
48+
raise RuntimeError("Fell off the end of the binary "
49+
"when searching for a JALR for a "
50+
f"function with symbol {symbol}, "
51+
f"starting at {start_addr:#x}")
52+
53+
# Check whether we just found 'B' (see note above nop_ranges)
54+
if INSNS_FILE.mnem_for_word(opcode) == 'jalr':
55+
nop_ranges.append((start_addr, pc))
56+
break
2657

2758
self.insns = {}
2859
for pc, opcode in insns.items():
29-
mnem = INSNS_FILE.mnem_for_word(opcode)
30-
if mnem is None:
31-
raise ValueError(
32-
'No legal decoding for mnemonic: {}'.format(mnem))
33-
insn = INSNS_FILE.mnemonic_to_insn[mnem]
34-
assert insn.encoding is not None
35-
enc_vals = insn.encoding.extract_operands(opcode)
60+
# Check if PC lies within one of the NOP ranges (equal to or after
61+
# the start of a nop subfunc and strictly before the JALR
62+
# instruction at the end).
63+
in_nop_region = any(a <= pc < b for a, b in nop_ranges)
64+
65+
# If the PC *is* in a NOP region, interpret the opcode at PC as a
66+
# NOP (addi x0, x0, x0). If not, decode the opcode and find the
67+
# appropriate instruction and operands.
68+
if in_nop_region:
69+
insn = INSNS_FILE.mnemonic_to_insn["addi"]
70+
enc_vals = {'imm': 0, 'grs1': 0, 'grd': 0}
71+
else:
72+
mnem = INSNS_FILE.mnem_for_word(opcode)
73+
if mnem is None:
74+
raise ValueError(f'No mnemonic for opcode {opcode:#08x}')
75+
76+
insn = INSNS_FILE.mnemonic_to_insn[mnem]
77+
assert insn.encoding is not None
78+
enc_vals = insn.encoding.extract_operands(opcode)
79+
3680
op_vals = insn.enc_vals_to_op_vals(pc, enc_vals)
3781
self.insns[pc] = (insn, op_vals)
3882

@@ -69,7 +113,7 @@ def _decode_mem(base_addr: int, data: bytes) -> Dict[int, int]:
69113
for offset, int_val in enumerate(struct.iter_unpack('<I', data))}
70114

71115

72-
def decode_elf(path: str) -> OTBNProgram:
116+
def decode_elf(path: str, nop_subfuncs: List[str]) -> OTBNProgram:
73117
'''Read ELF file at path and decode contents into an OTBNProgram instance
74118
75119
Returns the OTBNProgram instance representing the program in the ELF file.
@@ -79,4 +123,4 @@ def decode_elf(path: str) -> OTBNProgram:
79123
insns = _decode_mem(0, imem_bytes)
80124
data = _decode_mem(0, dmem_bytes)
81125

82-
return OTBNProgram(symbols, insns, data)
126+
return OTBNProgram(symbols, insns, data, nop_subfuncs)

0 commit comments

Comments
 (0)