blob: 36d15f1a5c8615d72ce22258bb7c7e2731530b57 [file] [log] [blame]
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
from enum import IntEnum
from typing import Dict, List, Optional
from shared.mem_layout import get_memory_layout
from .csr import CSRFile
from .dmem import Dmem
from .constants import ErrBits, Status
from .edn_client import EdnClient
from .ext_regs import OTBNExtRegs
from .flags import FlagReg
from .gpr import GPRs
from .loop import LoopStack
from .reg import RegFile
from .trace import Trace, TracePC
from .wsr import WSRFile
# The number of cycles spent per round of a secure wipe. This takes constant
# time in the RTL, mirrored here.
_WIPE_CYCLES = 68
class FsmState(IntEnum):
r'''State of the internal start/stop FSM
The FSM diagram looks like:
MEM_SEC_WIPE <--\
| |
\-------> IDLE -> PRE_EXEC -> EXEC
^ | |
\-- WIPING_GOOD <--/ |
|
LOCKED <-- WIPING_BAD <----/
IDLE represents the state when nothing is going on but there have been no
fatal errors. It matches Status.IDLE. LOCKED represents the state when
there has been a fatal error. It matches Status.LOCKED.
MEM_SEC_WIPE only represents the state where OTBN is busy operating on
secure wipe of DMEM/IMEM using SEC_WIPE_I(D)MEM command. Secure wipe of the
memories also happen when we encounter a fatal error while on
Status.BUSY_EXECUTE. However, if we are getting a fatal error Status would
be LOCKED.
PRE_EXEC, EXEC, WIPING_GOOD and WIPING_BAD correspond to
Status.BUSY_EXECUTE. PRE_EXEC is the period after starting OTBN where we're
still waiting for an EDN value to seed URND. EXEC is the period where we
start fetching and executing instructions.
WIPING_GOOD and WIPING_BAD represent the time where we're performing a
secure wipe of internal state (ending in updating the STATUS register to
show we're done). The difference between them is that WIPING_GOOD goes back
to IDLE and WIPING_BAD goes to LOCKED.
This is a refinement of the Status enum and the integer values are picked
so that you can divide by 10 to get the corresponding Status entry. (This
isn't used in the code, but makes debugging slightly more convenient when
you just have the numeric values available).
'''
IDLE = 0
PRE_EXEC = 10
EXEC = 12
WIPING_GOOD = 13
WIPING_BAD = 14
MEM_SEC_WIPE = 20
LOCKED = 2550
class InitSecWipeState(IntEnum):
NOT_DONE = 0
IN_PROGRESS = 1
DONE = 2
class OTBNState:
def __init__(self) -> None:
self.gprs = GPRs()
self.wdrs = RegFile('w', 256, 32)
self.ext_regs = OTBNExtRegs()
self.wsrs = WSRFile(self.ext_regs)
self.csrs = CSRFile()
self.pc = 0
self._pc_next_override = None # type: Optional[int]
self.imem_size = get_memory_layout().imem_size_bytes
self.dmem = Dmem()
self._fsm_state = FsmState.IDLE
self._next_fsm_state = FsmState.IDLE
self._init_sec_wipe_state = InitSecWipeState.NOT_DONE
self.first_round_of_wipe = True
self.loop_stack = LoopStack()
self._err_bits = 0
self.pending_halt = False
self._urnd_client = EdnClient()
# To simulate injecting integrity errors, we set a flag to say that
# IMEM is no longer readable without getting an error. This can't take
# effect instantly because the RTL's prefetch stage (which we don't
# model except for matching its timing) has a copy of the next
# instruction. Make this a counter, decremented once per cycle. When we
# get to zero, we set the flag.
self._time_to_imem_invalidation = None # type: Optional[int]
self.invalidated_imem = False
# This is the number of cycles left for wiping. When we're in the
# WIPING_GOOD or WIPING_BAD state, this should be a non-negative
# number. Initialise to -1 to catch bugs if we forget to set it.
self.wipe_cycles = -1
# If this is nonzero, there's been an error injected from outside. The
# Sim.step function will OR these bits into err_bits and stop at the
# end of the next cycle. (We don't just call stop_at_end_of_cycle
# because this runs earlier in the cycle than step(), which would mean
# we wouldn't see the final instruction get executed and then
# cancelled).
self.injected_err_bits = 0
self.lock_immediately = False
self.zero_insn_cnt_next = False
# If this is set, all software errors should result in the model status
# being locked.
self.software_errs_fatal = False
# This is a counter that keeps track of how many cycles have elapsed in
# current fsm_state.
self.cycles_in_this_state = 0
# RMA request changes state to LOCKED, basically an escalation from lifecycle
# controller. Initiates secure wiping through stop_at_end_of_cycle method and
# this flag.
self.rma_req = False
def get_next_pc(self) -> int:
if self._pc_next_override is not None:
return self._pc_next_override
return self.pc + 4
def set_next_pc(self, next_pc: int) -> None:
'''Overwrite the next program counter, e.g. as result of a jump.'''
assert (self.is_pc_valid(next_pc))
self._pc_next_override = next_pc
def edn_urnd_step(self, urnd_data: int) -> None:
self._urnd_client.take_word(urnd_data, False)
def edn_rnd_step(self, rnd_data: int, fips_err: bool) -> None:
self.ext_regs.rnd_take_word(rnd_data, fips_err)
def edn_flush(self) -> None:
self.ext_regs.rnd_reset()
self._urnd_client.edn_reset()
# If the initial secure wipe is running, OTBN will directly request a
# new URND value.
if self.init_sec_wipe_is_running():
self._urnd_client.request()
def rnd_completed(self) -> None:
'''Called when CDC completes for the EDN RND interface'''
# Set the RND WSR with the value, assuming the cache hadn't been
# poisoned. This will be committed at the end of the next step on the
# main clock.
rnd_val, fips_err, rep_err = self.ext_regs.rnd_cdc_complete()
if rnd_val is not None:
self.wsrs.RND.set_unsigned(rnd_val, fips_err, rep_err)
def urnd_completed(self) -> None:
w256, retry, _, _ = self._urnd_client.cdc_complete()
# The URND client should never be poisoned
assert w256 is not None and retry is False
# cdc_complete() returned a 256-bit value but we actually need to split
# it back into four 64-bit words.
w64s = [(w256 >> (64 * i)) & ((1 << 64) - 1) for i in range(4)]
self.wsrs.URND.set_seed(w64s)
def start_init_sec_wipe(self) -> None:
self._init_sec_wipe_state = InitSecWipeState.IN_PROGRESS
# OTBN will request a new URND value, so the model has to do the same.
self._urnd_client.request()
def init_sec_wipe_is_running(self) -> bool:
return self._init_sec_wipe_state == InitSecWipeState.IN_PROGRESS
def init_sec_wipe_is_done(self) -> bool:
return self._init_sec_wipe_state == InitSecWipeState.DONE
def complete_init_sec_wipe(self) -> None:
self._init_sec_wipe_state = InitSecWipeState.DONE
def loop_start(self, iterations: int, bodysize: int) -> None:
self.loop_stack.start_loop(self.pc + 4, iterations, bodysize)
def loop_step(self, loop_warps: Dict[int, int]) -> None:
back_pc = self.loop_stack.step(self.pc, loop_warps)
if back_pc is not None:
self.set_next_pc(back_pc)
def in_loop(self) -> bool:
'''The processor is currently executing a loop.'''
# A loop is executed if the loop stack is not empty.
return bool(self.loop_stack.stack)
def changes(self) -> List[Trace]:
c = [] # type: List[Trace]
c += self.gprs.changes()
if self._pc_next_override is not None:
# Only append the next program counter to the trace if it has
# been set explicitly.
c.append(TracePC(self.get_next_pc()))
c += self.dmem.changes()
c += self.loop_stack.changes()
c += self.ext_regs.changes()
c += self.wsrs.changes()
c += self.csrs.flags.changes()
c += self.wdrs.changes()
return c
def executing(self) -> bool:
return self._fsm_state not in [FsmState.IDLE,
FsmState.LOCKED,
FsmState.MEM_SEC_WIPE]
def wiping(self) -> bool:
return self._fsm_state in [FsmState.WIPING_GOOD, FsmState.WIPING_BAD]
def stop_if_pending_halt(self) -> bool:
if self.pending_halt:
self.stop()
return True
return False
def step(self, handle_injected_error: bool) -> None:
if handle_injected_error:
self.take_injected_err_bits()
self.ext_regs.step()
self._urnd_client.step()
def commit(self, sim_stalled: bool) -> None:
if self._time_to_imem_invalidation is not None:
self._time_to_imem_invalidation -= 1
if self._time_to_imem_invalidation == 0:
self.invalidated_imem = True
self._time_to_imem_invalidation = None
old_state = self._fsm_state
self._fsm_state = self._next_fsm_state
if self._fsm_state == old_state:
self.cycles_in_this_state += 1
else:
self.cycles_in_this_state = 0
self.ext_regs.commit()
# Pull URND out separately because we also want to commit this in some
# "idle-ish" states
self.wsrs.URND.commit()
# In some states, we can get away with just committing external
# registers (which lets us reflect things like the update to the STATUS
# register) but nothing else. This is just an optimisation: if
# everything is working properly, there won't be any other pending
# changes.
if old_state not in [FsmState.EXEC,
FsmState.WIPING_GOOD, FsmState.WIPING_BAD]:
return
self.gprs.commit()
self.dmem.commit()
self.loop_stack.commit()
self.wsrs.commit()
self.csrs.flags.commit()
self.wdrs.commit()
if not sim_stalled:
self.pc = self.get_next_pc()
self._pc_next_override = None
def _abort(self) -> None:
'''Abort any pending state changes'''
self.gprs.abort()
self._pc_next_override = None
self.dmem.abort()
self.loop_stack.abort()
self.ext_regs.abort()
self.wsrs.abort()
self.csrs.flags.abort()
self.wdrs.abort()
def start(self) -> None:
'''Start running; perform state init'''
self.ext_regs.write('STATUS', Status.BUSY_EXECUTE, True)
self.pending_halt = False
self._err_bits = 0
self._fsm_state = FsmState.PRE_EXEC
self._next_fsm_state = FsmState.PRE_EXEC
self.pc = 0
# Reset CSRs, WSRs, loop stack and call stack. WSRs have special
# treatment because some of them have values that persist across
# operations.
self.csrs = CSRFile()
self.wsrs.on_start()
self.loop_stack = LoopStack()
self.gprs.empty_call_stack()
# Poison the requester so that we'll discard the rest of any in-flight
# request.
self.ext_regs.rnd_poison()
self._urnd_client.request()
def stop(self) -> None:
'''Set flags to stop the processor and maybe abort the instruction.
If the current instruction has caused an error (so self._err_bits is
nonzero), abort all its pending changes, including changes to external
registers.
If not, we've just executed an ECALL. The only pending change will be
the increment of INSN_CNT that we want to keep.
Either way, set the appropriate bits in the external ERR_CODE register,
write STOP_PC and start a secure wipe.
It's possible that we are already doing a secure wipe. For example, we
might have had an error escalation signal after starting the wipe. In
this case, there's nothing to do except possibly to update ERR_BITS.
'''
# If we were running an instruction and something went wrong then it
# might have updated state (either registers, memory or
# externally-visible registers). We want to roll back any of those
# changes.
if (self._err_bits and self._fsm_state == FsmState.EXEC) or self.rma_req:
self._abort()
# INTR_STATE is the interrupt state register. Bit 0 (which is being
# set) is the 'done' flag.
self.ext_regs.set_bits('INTR_STATE', 1 << 0)
should_lock = (((self._err_bits >> 16) != 0) or
((self._err_bits >> 10) & 1) or
(self._err_bits and self.software_errs_fatal) or
self.rma_req)
# Make any error bits visible
self.ext_regs.write('ERR_BITS', self._err_bits, True)
# Clear the "we should stop soon" flag
self.pending_halt = False
if self.lock_immediately:
assert should_lock
self.set_fsm_state(FsmState.LOCKED)
self.ext_regs.write('STATUS', Status.LOCKED, True)
else:
# Set the WIPE_START flag if we were running. This is used to tell
# the C++ model code that this is a good time to inspect DMEM and
# check that the RTL and model match. The flag will be cleared
# again on the next cycle.
if self._fsm_state == FsmState.EXEC:
# Make the final PC visible. This isn't currently in the RTL,
# but is useful in simulations that want to track whether we
# stopped where we expected to stop.
self.ext_regs.write('STOP_PC', self.pc, True)
self.ext_regs.write('WIPE_START', 1, True)
self.ext_regs.regs['WIPE_START'].commit()
# Switch to a 'wiping' state
self.set_fsm_state(FsmState.WIPING_BAD if should_lock
else FsmState.WIPING_GOOD)
elif self._fsm_state in [FsmState.WIPING_BAD, FsmState.WIPING_GOOD]:
assert should_lock
self._next_fsm_state = FsmState.WIPING_BAD
elif self._init_sec_wipe_state in [InitSecWipeState.IN_PROGRESS]:
# Make it so that we run stop method until initial secure wipe
# is done. Otherwise we would have missed the pending halt.
assert should_lock
self.pending_halt = True
elif self._init_sec_wipe_state == InitSecWipeState.DONE:
assert should_lock
self._next_fsm_state = FsmState.LOCKED
next_status = Status.LOCKED
self.ext_regs.write('STATUS', next_status, True)
# Clear any pending request in the RND EDN client
self.ext_regs.rnd_forget()
# Clear RMA request flag
self.rma_req = False
def get_fsm_state(self) -> FsmState:
return self._fsm_state
def set_fsm_state(self, new_state: FsmState) -> None:
if new_state in [FsmState.WIPING_BAD, FsmState.WIPING_GOOD]:
self.wipe_cycles = _WIPE_CYCLES
self._next_fsm_state = new_state
def set_flags(self, fg: int, flags: FlagReg) -> None:
'''Update flags for a flag group'''
self.csrs.flags[fg] = flags
def set_mlz_flags(self, fg: int, result: int) -> None:
'''Update M, L, Z flags for a flag group using the given result'''
self.csrs.flags[fg] = \
FlagReg.mlz_for_result(self.csrs.flags[fg].C, result)
def pre_insn(self, insn_affects_control: bool) -> None:
'''Run before running an instruction'''
self.loop_stack.check_insn(self.pc, insn_affects_control)
def is_pc_valid(self, pc: int) -> bool:
'''Return whether pc is a valid program counter.'''
# The PC should always be non-negative since it's represented as an
# unsigned value. (It's an error in the simulator if that's come
# unstuck)
assert 0 <= pc
# Check the new PC is word-aligned
if pc & 3:
return False
# Check the new PC lies in instruction memory
if pc >= self.imem_size:
return False
return True
def post_insn(self, loop_warps: Dict[int, int]) -> None:
'''Update state after running an instruction but before commit'''
self.ext_regs.increment_insn_cnt()
self.loop_step(loop_warps)
self.gprs.post_insn()
self._err_bits |= self.gprs.err_bits() | self.loop_stack.err_bits()
if self._err_bits:
self.pending_halt = True
# Check that the next PC is valid, but only if we're not stopping
# anyway. This handles the case where we have a straight-line
# instruction at the top of memory. Jumps and branches to invalid
# addresses are handled in the instruction definition.
#
# This check is squashed if we're already halting, which avoids a
# problem when you have an ECALL instruction at the top of memory (the
# next address is bogus, but we don't care because we're stopping
# anyway).
if not self.is_pc_valid(self.get_next_pc()) and not self.pending_halt:
self._err_bits |= ErrBits.BAD_INSN_ADDR
self.pending_halt = True
def read_csr(self, idx: int) -> int:
'''Read the CSR with index idx as an unsigned 32-bit number'''
return self.csrs.read_unsigned(self.wsrs, idx)
def write_csr(self, idx: int, value: int) -> None:
'''Write value (an unsigned 32-bit number) to the CSR with index idx'''
self.csrs.write_unsigned(self.wsrs, idx, value)
def peek_call_stack(self) -> List[int]:
'''Return the current call stack, bottom-first'''
return self.gprs.peek_call_stack()
def stop_at_end_of_cycle(self, err_bits: int) -> None:
'''Tell the simulation to stop at the end of the cycle
Any bits set in err_bits will be set in the ERR_BITS register when
we're done.
'''
self._err_bits |= err_bits
self.pending_halt = True
def invalidate_imem(self) -> None:
self._time_to_imem_invalidation = 2
def clear_imem_invalidation(self) -> None:
'''Clear any effective or pending IMEM invalidation'''
self._time_to_imem_invalidation = None
self.invalidated_imem = False
def wipe(self) -> None:
self.gprs.empty_call_stack()
self.gprs.wipe()
self.wdrs.wipe()
self.wsrs.wipe()
self.csrs.wipe()
def take_injected_err_bits(self) -> None:
'''Apply any injected errors, stopping at the end of the cycle'''
if self.injected_err_bits != 0:
self.stop_at_end_of_cycle(self.injected_err_bits)
self.injected_err_bits = 0