[mask_rom] Implement shutdown

1. Implement shutdown initialization and best-effort SW shutdown.
2. Restructure the mask_rom's main in terms of shutdown.

Signed-off-by: Chris Frantz <cfrantz@google.com>
diff --git a/sw/device/lib/base/macros.h b/sw/device/lib/base/macros.h
index 8989077..87837d4 100644
--- a/sw/device/lib/base/macros.h
+++ b/sw/device/lib/base/macros.h
@@ -15,11 +15,16 @@
  */
 
 /**
- * A annotation that a switch/case fallthrough is the intended behavior.
+ * An annotation that a switch/case fallthrough is the intended behavior.
  */
 #define FALLTHROUGH_INTENDED __attribute__((fallthrough))
 
 /**
+ * A directive to force the compiler to inline a function.
+ */
+#define ALWAYS_INLINE __attribute__((always_inline)) inline
+
+/**
  * A variable-argument macro that expands to the number of arguments passed into
  * it, between 0 and 31 arguments.
  *
@@ -65,4 +70,11 @@
 #define OT_ASSERT_SIZE(type, size) \
   static_assert(sizeof(type) == UINT32_C(size), "Unexpected size for " #type)
 
+/**
+ * A macro representing the OpenTitan execution platform.
+ */
+#if __riscv_xlen == 32
+#define OT_PLATFORM_RV32 1
+#endif
+
 #endif  // OPENTITAN_SW_DEVICE_LIB_BASE_MACROS_H_
diff --git a/sw/device/silicon_creator/lib/drivers/alert.h b/sw/device/silicon_creator/lib/drivers/alert.h
index a6d43ea..98e10e0 100644
--- a/sw/device/silicon_creator/lib/drivers/alert.h
+++ b/sw/device/silicon_creator/lib/drivers/alert.h
@@ -15,6 +15,7 @@
 
 #define ALERT_CLASSES 4
 
+// TODO(lowRISC/opentitan#7148): Choose values for configuration enums.
 // Note: the AlertClass values need to map to a byte.
 /**
  * Alert Classification Values as stored in OTP.
diff --git a/sw/device/silicon_creator/lib/error.h b/sw/device/silicon_creator/lib/error.h
index 60f8c63..68f037a 100644
--- a/sw/device/silicon_creator/lib/error.h
+++ b/sw/device/silicon_creator/lib/error.h
@@ -5,6 +5,8 @@
 #ifndef OPENTITAN_SW_DEVICE_SILICON_CREATOR_LIB_ERROR_H_
 #define OPENTITAN_SW_DEVICE_SILICON_CREATOR_LIB_ERROR_H_
 
+#include "sw/device/lib/base/bitfield.h"
+
 #define USING_ABSL_STATUS
 #include "sw/device/silicon_creator/lib/absl_status.h"
 #undef USING_ABSL_STATUS
@@ -27,10 +29,20 @@
   kModuleSigverify = 0x5653,     // ASCII "SV".
   kModuleKeymgr = 0x4d4b,        // ASCII "KM".
   kModuleManifest = 0x414d,      // ASCII "MA".
+  kModuleMaskRom = 0x524d,       // ASCII "MR".
   kModuleRomextimage = 0x4552,   // ASCII "RE".
+  kModuleInterrupt = 0x5249,     // ASCII "IR".
 };
 
 /**
+ * Field definitions for the different fields of the error word.
+ */
+#define ROM_ERROR_FIELD_ERROR ((bitfield_field32_t){.mask = 0xFF, .index = 24})
+#define ROM_ERROR_FIELD_MODULE \
+  ((bitfield_field32_t){.mask = 0xFFFF, .index = 8})
+#define ROM_ERROR_FIELD_STATUS ((bitfield_field32_t){.mask = 0xFF, .index = 0})
+
+/**
  * Helper macro for building up error codes.
  * @param status_ An appropriate general status code from absl_staus.h.
  * @param module_ The module identifier which produces this error.
@@ -62,6 +74,9 @@
   X(kErrorAlertBadClass,              ERROR_(2, kModuleAlertHandler, kInvalidArgument)), \
   X(kErrorAlertBadEnable,             ERROR_(3, kModuleAlertHandler, kInvalidArgument)), \
   X(kErrorAlertBadEscalation,         ERROR_(4, kModuleAlertHandler, kInvalidArgument)), \
+  X(kErrorMaskRomBootFailed,          ERROR_(1, kModuleMaskRom, kFailedPrecondition)), \
+  /* The high-byte of kErrorInterrupt is modified with the interrupt cause */ \
+  X(kErrorInterrupt,                  ERROR_(0, kModuleInterrupt, kUnknown)), \
   X(kErrorUnknown, 0xFFFFFFFF)
 // clang-format on
 
diff --git a/sw/device/silicon_creator/lib/meson.build b/sw/device/silicon_creator/lib/meson.build
index ad69718..921ba79 100644
--- a/sw/device/silicon_creator/lib/meson.build
+++ b/sw/device/silicon_creator/lib/meson.build
@@ -59,6 +59,46 @@
   ),
 )
 
+sw_silicon_creator_lib_shutdown = declare_dependency(
+  link_with: static_library(
+    'sw_silicon_creator_lib_shutdown',
+    sources: [
+      hw_ip_alert_handler_reg_h,
+      hw_ip_otp_ctrl_reg_h,
+      hw_ip_keymgr_reg_h,
+      hw_ip_sram_ctrl_reg_h,
+      hw_ip_flash_ctrl_reg_h,
+      'shutdown.c',
+    ],
+    dependencies: [
+      sw_silicon_creator_lib_base_abs_mmio,
+      sw_silicon_creator_lib_driver_alert,
+    ],
+  ),
+)
+
+test('sw_silicon_creator_lib_shutdown_unittest', executable(
+    'sw_silicon_creator_lib_shutdown_unittest',
+    sources: [
+      hw_ip_alert_handler_reg_h,
+      hw_ip_otp_ctrl_reg_h,
+      hw_ip_keymgr_reg_h,
+      hw_ip_sram_ctrl_reg_h,
+      hw_ip_flash_ctrl_reg_h,
+      'shutdown.c',
+      'shutdown_unittest.cc',
+    ],
+    dependencies: [
+      sw_vendor_gtest,
+      sw_silicon_creator_lib_base_mock_abs_mmio,
+    ],
+    native: true,
+    c_args: ['-DMOCK_ABS_MMIO', '-DOT_OFF_TARGET_TEST'],
+    cpp_args: ['-DMOCK_ABS_MMIO', '-DOT_OFF_TARGET_TEST'],
+  ),
+  suite: 'mask_rom',
+)
+
 test('sw_silicon_creator_lib_error_unittest', executable(
     'sw_silicon_creator_lib_error_unittest',
     sources: [
diff --git a/sw/device/silicon_creator/lib/shutdown.c b/sw/device/silicon_creator/lib/shutdown.c
new file mode 100644
index 0000000..c27df60
--- /dev/null
+++ b/sw/device/silicon_creator/lib/shutdown.c
@@ -0,0 +1,268 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/silicon_creator/lib/shutdown.h"
+
+#include <assert.h>
+
+#include "sw/device/lib/base/bitfield.h"
+#include "sw/device/lib/base/macros.h"
+#include "sw/device/lib/base/memory.h"
+#include "sw/device/lib/base/stdasm.h"
+#include "sw/device/lib/runtime/print.h"
+#include "sw/device/silicon_creator/lib/base/abs_mmio.h"
+#include "sw/device/silicon_creator/lib/drivers/alert.h"
+#include "sw/device/silicon_creator/lib/drivers/lifecycle.h"
+
+#include "alert_handler_regs.h"
+#include "flash_ctrl_regs.h"
+#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"
+#include "keymgr_regs.h"
+#include "otp_ctrl_regs.h"
+#include "sram_ctrl_regs.h"
+
+static_assert(ALERT_HANDLER_ALERT_CLASS_MULTIREG_COUNT <=
+                  OTP_CTRL_PARAM_ROM_ALERT_CLASSIFICATION_SIZE / 4,
+              "More alerts than alert classification OTP words!");
+
+#define NO_MODIFIERS
+
+#ifdef OT_OFF_TARGET_TEST
+// If we're building as a unit test, rename the shutdown functions so they
+// can be mocked and/or tested individually.
+// The unmodified function name will be declared as `extern` so the test
+// program can supply its own implementation.  The implementation present
+// in this file will be named `unmocked_${name}` so the test program can
+// invoke it for testing.
+#define SHUTDOWN_FUNC(modifiers_, name_) \
+  extern void name_;                     \
+  void unmocked_##name_
+#else
+#define SHUTDOWN_FUNC(modifiers_, name_) \
+  static ALWAYS_INLINE modifiers_ void name_
+#endif
+
+// TODO: use the real OTP driver after it's converted to abs_mmio.
+#ifdef OT_OFF_TARGET_TEST
+extern uint32_t otp_read32(uint32_t address);
+#else
+inline uint32_t otp_read32(uint32_t address) {
+  return abs_mmio_read32(TOP_EARLGREY_OTP_CTRL_BASE_ADDR +
+                         OTP_CTRL_SW_CFG_WINDOW_REG_OFFSET + address);
+}
+#endif
+
+// Convert the alert class to an index.
+// This is required because I expect to change the constant definitions in
+// alert_class_t to have reasonable hamming distances.
+static size_t clsindex(alert_class_t cls) {
+  switch (cls) {
+    case kAlertClassA:
+      return 0;
+    case kAlertClassB:
+      return 1;
+    case kAlertClassC:
+      return 2;
+    case kAlertClassD:
+      return 3;
+    default:
+      return 0;
+  }
+}
+
+rom_error_t shutdown_init(lifecycle_state_t lc_state) {
+  // Are we in a lifecycle state which needs alert configuration?
+  uint32_t lc_shift;
+  switch (lc_state) {
+    case kLcStateProd:
+      lc_shift = 0;
+      break;
+    case kLcStateProdEnd:
+      lc_shift = 8;
+      break;
+    case kLcStateDev:
+      lc_shift = 16;
+      break;
+    case kLcStateRma:
+      lc_shift = 24;
+      break;
+    default:
+      // No alert init for any other lifecycle states.
+      return kErrorOk;
+  }
+
+  // Get the enable and escalation settings for all four alert classes.
+  // Each of these OTP words is composed of 4 byte enums with the enable and
+  // escalate configs per alert class (a/b/c/d).
+  uint32_t class_enable = otp_read32(OTP_CTRL_PARAM_ROM_ALERT_CLASS_EN_OFFSET);
+  uint32_t class_escalate =
+      otp_read32(OTP_CTRL_PARAM_ROM_ALERT_ESCALATION_OFFSET);
+  alert_enable_t enable[ALERT_CLASSES];
+  alert_escalate_t escalate[ALERT_CLASSES];
+  for (size_t i = 0; i < ALERT_CLASSES; ++i) {
+    enable[i] = (alert_enable_t)bitfield_field32_read(
+        class_enable, (bitfield_field32_t){.mask = 0xff, .index = i * 8});
+    escalate[i] = (alert_escalate_t)bitfield_field32_read(
+        class_escalate, (bitfield_field32_t){.mask = 0xff, .index = i * 8});
+  }
+
+  // For each alert, read its corresponding OTP word and extract the class
+  // configuration for the current lifecycle state.
+  rom_error_t error = kErrorOk;
+  for (size_t i = 0; i < ALERT_HANDLER_ALERT_CLASS_MULTIREG_COUNT; ++i) {
+    uint32_t value = otp_read32(OTP_CTRL_PARAM_ROM_ALERT_CLASSIFICATION_OFFSET +
+                                i * sizeof(uint32_t));
+    alert_class_t cls = (alert_class_t)bitfield_field32_read(
+        value, (bitfield_field32_t){.mask = 0xff, .index = lc_shift});
+    rom_error_t e = alert_configure(i, cls, enable[clsindex(cls)]);
+    if (e != kErrorOk) {
+      // Keep going if there is an error programming one alert.  We want to
+      // program them all.
+      error = e;
+    }
+  }
+
+#if 0
+  // TODO(lowRISC/opentitan#7148): Bring local alerts back into the code path.
+  // For each local alert, read its corresponding OTP word and extract the class
+  // configuration for the current lifecycle state.
+  for (size_t i = 0; i < ALERT_HANDLER_LOC_ALERT_CLASS_MULTIREG_COUNT; ++i) {
+    uint32_t value = otp_read32(OTP_CTRL_PARAM_ROM_LOCAL_ALERT_CLASSIFICATION_OFFSET +
+                                i * sizeof(uint32_t));
+    alert_class_t cls = (alert_class_t)bitfield_field32_read(
+        value, (bitfield_field32_t){.mask = 0xff, .index = lc_shift});
+    rom_error_t e = alert_local_configure(i, cls, enable[clsindex(cls)]);
+    if (e != kErrorOk) {
+      // Keep going if there is an error programming one alert.  We want to
+      // program them all.
+      error = e;
+    }
+  }
+#endif
+
+  // For each alert class, configure the various escalation parameters.
+  const alert_class_t kClasses[] = {
+      kAlertClassA,
+      kAlertClassB,
+      kAlertClassC,
+      kAlertClassD,
+  };
+  alert_class_config_t config;
+  for (size_t i = 0; i < ALERT_CLASSES; ++i) {
+    config.enabled = enable[i];
+    config.escalation = escalate[i];
+    config.accum_threshold = otp_read32(
+        OTP_CTRL_PARAM_ROM_ALERT_ACCUM_THRESH_OFFSET + i * sizeof(uint32_t));
+    config.timeout_cycles = otp_read32(
+        OTP_CTRL_PARAM_ROM_ALERT_TIMEOUT_CYCLES_OFFSET + i * sizeof(uint32_t));
+    for (size_t phase = 0; phase < ARRAYSIZE(config.phase_cycles); ++phase) {
+      config.phase_cycles[phase] = otp_read32(
+          OTP_CTRL_PARAM_ROM_ALERT_PHASE_CYCLES_OFFSET +
+          (i * ARRAYSIZE(config.phase_cycles) + phase) * sizeof(uint32_t));
+    }
+
+    rom_error_t e = alert_class_configure(kClasses[i], &config);
+    if (e != kErrorOk) {
+      // Keep going if there is an error programming an alert class. We want to
+      // program them all.
+      error = e;
+    }
+  }
+  return error;
+}
+
+uint32_t shutdown_redact(rom_error_t reason, shutdown_error_redact_t severity) {
+  uint32_t redacted = (uint32_t)reason;
+  if (reason == kErrorOk) {
+    return 0;
+  }
+  switch (severity) {
+    case kShutdownErrorRedactModule:
+      redacted = bitfield_field32_write(redacted, ROM_ERROR_FIELD_MODULE, 0);
+      FALLTHROUGH_INTENDED;
+    case kShutdownErrorRedactError:
+      redacted = bitfield_field32_write(redacted, ROM_ERROR_FIELD_ERROR, 0);
+      FALLTHROUGH_INTENDED;
+    case kShutdownErrorRedactNone:
+      break;
+    case kShutdownErrorRedactAll:
+      FALLTHROUGH_INTENDED;
+    default:
+      redacted = kErrorUnknown;
+  }
+  return redacted;
+}
+
+SHUTDOWN_FUNC(NO_MODIFIERS, shutdown_software_escalate(void)) {
+  // TODO(lowRISC/opentitan#7148): Use a software alert when available.
+  // For now, signal a fatal_intg_error from SRAM.
+  enum { kBase = TOP_EARLGREY_SRAM_CTRL_MAIN_BASE_ADDR };
+  abs_mmio_write32(kBase + SRAM_CTRL_ALERT_TEST_REG_OFFSET, 1);
+}
+
+SHUTDOWN_FUNC(NO_MODIFIERS, shutdown_keymgr_kill(void)) {
+  enum {
+    kBase = TOP_EARLGREY_KEYMGR_BASE_ADDR,
+  };
+  uint32_t reg = bitfield_bit32_write(0, KEYMGR_CONTROL_START_BIT, true);
+  reg = bitfield_field32_write(reg, KEYMGR_CONTROL_DEST_SEL_FIELD,
+                               KEYMGR_CONTROL_DEST_SEL_VALUE_NONE);
+  reg = bitfield_field32_write(reg, KEYMGR_CONTROL_OPERATION_FIELD,
+                               KEYMGR_CONTROL_OPERATION_VALUE_DISABLE);
+  abs_mmio_write32(kBase + KEYMGR_CONTROL_REG_OFFSET, reg);
+  abs_mmio_write32(kBase + KEYMGR_SIDELOAD_CLEAR_REG_OFFSET, 1);
+}
+
+SHUTDOWN_FUNC(NO_MODIFIERS, shutdown_flash_kill(void)) {
+  enum { kBase = TOP_EARLGREY_FLASH_CTRL_CORE_BASE_ADDR };
+  // TODO(lowRISC/opentitan#7148): Add flash disable when the hw is implemented.
+  // abs_mmio_write32(kBase + FLASH_CTRL_FLASH_DISABLE_REG_OFFSET, 1);
+}
+
+SHUTDOWN_FUNC(noreturn, shutdown_hang(void)) {
+  enum { kSramCtrlBase = TOP_EARLGREY_SRAM_CTRL_MAIN_BASE_ADDR };
+
+  // Disable SRAM execution and lock the register.
+  abs_mmio_write32(kSramCtrlBase + SRAM_CTRL_EXEC_EN_OFFSET, 0);
+  abs_mmio_write32(kSramCtrlBase + SRAM_CTRL_EXEC_REGWEN_REG_OFFSET, 0);
+
+  // Switch to assembly as RAM (incl. stack) is about to get scrambled.
+#ifdef OT_PLATFORM_RV32
+  while (true) {
+    asm volatile(
+        // Request a new scrambling key, then lock the SRAM control register.
+        "sw %[kRenewKey], %[kCtrlOffset](%[kMainRamCtrlBase]);"
+        "sw zero, %[kRegWriteEn](%[kMainRamCtrlBase]);"
+
+        // TODO(lowRISC/opentitan#7148): restrict the ePMP such that only
+        // ROM may execute.  mundaym's suggestion: set entry 2 as a NAPOT
+        // region covering the entire address space, clear all its permission
+        // bits and set the lock bit, and then finally disable RLB to prevent
+        // any further modifications.
+
+        // Generate a halt-maze.
+        "1:"
+        "wfi; wfi; wfi; wfi; j 1b;"
+        "wfi; wfi; j 1b;"
+        "wfi; j 1b;"
+        "wfi;"
+        :
+        : [kRenewKey] "r"(1 << SRAM_CTRL_CTRL_RENEW_SCR_KEY_BIT),
+          [kCtrlOffset] "I"(SRAM_CTRL_CTRL_REG_OFFSET),
+          [kMainRamCtrlBase] "r"(kSramCtrlBase),
+          [kRegWriteEn] "I"(SRAM_CTRL_CTRL_REGWEN_REG_OFFSET));
+  }
+#endif
+}
+
+void shutdown_finalize(rom_error_t reason) {
+  uint32_t redacted_error = shutdown_redact(
+      reason, otp_read32(OTP_CTRL_PARAM_ROM_ERROR_REPORTING_OFFSET));
+  base_printf("boot_fault: 0x%08x\n", redacted_error);
+  shutdown_software_escalate();
+  shutdown_keymgr_kill();
+  shutdown_flash_kill();
+  // If we get here, we'll wait for the watchdog to reset the chip.
+  shutdown_hang();
+}
diff --git a/sw/device/silicon_creator/lib/shutdown.h b/sw/device/silicon_creator/lib/shutdown.h
new file mode 100644
index 0000000..d79c309
--- /dev/null
+++ b/sw/device/silicon_creator/lib/shutdown.h
@@ -0,0 +1,69 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef OPENTITAN_SW_DEVICE_SILICON_CREATOR_LIB_SHUTDOWN_H_
+#define OPENTITAN_SW_DEVICE_SILICON_CREATOR_LIB_SHUTDOWN_H_
+#include <stdint.h>
+#include <stdnoreturn.h>
+
+#include "sw/device/silicon_creator/lib/drivers/lifecycle.h"
+#include "sw/device/silicon_creator/lib/error.h"
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Initializes the Mask ROM shutdown infrastructure.
+ *
+ * Reads the shutdown policy from OTP, and initializes the alert handler.
+ *
+ * @param lc_state: Lifecycle state of the chip.
+ * @return: Any error encountered during initialization.
+ */
+rom_error_t shutdown_init(lifecycle_state_t lc_state);
+
+/**
+ * The error redaction possibilities in increasing severity.
+ *
+ * This value is read from the ROM_ERROR_REPORTING OTP word.
+ *
+ * No error code redaction
+ * Redact the specific error code.
+ * Redact the specific error code and source modules.
+ * Redact all error componens (general code, specific code and module).
+ */
+// TODO(lowRISC/opentitan#7148): Choose values for configuration enums.
+typedef enum shutdown_error_redact {
+  kShutdownErrorRedactNone,
+  kShutdownErrorRedactError,
+  kShutdownErrorRedactModule,
+  kShutdownErrorRedactAll,
+} shutdown_error_redact_t;
+
+/**
+ * Redact an error code.
+ *
+ * @param reason: The error code to be redacted.
+ * @param severity: The redaction severity.
+ * @return: The redacted error code.
+ */
+uint32_t shutdown_redact(rom_error_t reason, shutdown_error_redact_t severity);
+
+/**
+ * Perform a shutdown in the Mask ROM in response to an exceptional condition.
+ *
+ * @param reason: A reason for entering the shutdown state.
+ */
+#ifndef OT_OFF_TARGET_TEST
+// If this is a test, we'll omit `noreturn` so we can call this function
+// from within a test program.
+noreturn
+#endif
+    void
+    shutdown_finalize(rom_error_t reason);
+
+#ifdef __cplusplus
+}
+#endif
+#endif  // OPENTITAN_SW_DEVICE_SILICON_CREATOR_LIB_SHUTDOWN_H_
diff --git a/sw/device/silicon_creator/lib/shutdown_unittest.cc b/sw/device/silicon_creator/lib/shutdown_unittest.cc
new file mode 100644
index 0000000..73011de
--- /dev/null
+++ b/sw/device/silicon_creator/lib/shutdown_unittest.cc
@@ -0,0 +1,421 @@
+// Copyright lowRISC contributors.
+// Licensed under the Apache License, Version 2.0, see LICENSE for details.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "sw/device/silicon_creator/lib/shutdown.h"
+
+#include "gtest/gtest.h"
+#include "sw/device/lib/base/mmio.h"
+#include "sw/device/lib/testing/mask_rom_test.h"
+#include "sw/device/silicon_creator/lib/base/mock_abs_mmio.h"
+#include "sw/device/silicon_creator/lib/drivers/lifecycle.h"
+#include "sw/device/silicon_creator/lib/drivers/mock_alert.h"
+#include "sw/device/silicon_creator/lib/error.h"
+
+#include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"
+#include "otp_ctrl_regs.h"
+
+// FIXME: I can't get ARRAYSIZE from `memory.h` because the definitions of
+// memcpy and friends there conflict with the standard definitions.
+#define ARRAYSIZE(x) ((sizeof x) / (sizeof x[0]))
+
+namespace shutdown_unittest {
+
+using ::testing::ElementsAre;
+using ::testing::Invoke;
+using ::testing::Return;
+using ::testing::Test;
+
+namespace {
+extern "C" {
+// Dummy out base_printf.
+int base_printf(const char *fmt, ...) { return 0; }
+}  // extern "C"
+
+// TODO(lowRISC/opentitan#7148): Refactor mocks into their own headers.
+namespace internal {
+// Create a mock for reading OTP words.
+class MockOtp {
+ public:
+  MOCK_METHOD(uint32_t, otp_read32, (uint32_t));
+  virtual ~MockOtp() {}
+};
+
+// Create a mock for shutdown functions.
+class MockShutdown {
+ public:
+  MOCK_METHOD(void, shutdown_software_escalate, ());
+  MOCK_METHOD(void, shutdown_keymgr_kill, ());
+  MOCK_METHOD(void, shutdown_flash_kill, ());
+  MOCK_METHOD(void, shutdown_hang, ());
+  virtual ~MockShutdown() {}
+};
+
+}  // namespace internal
+// Use NiceMock because we aren't interested in the specifics of OTP reads,
+// but we want to mock out calls to otp_read32.
+using MockOtp = mask_rom_test::GlobalMock<testing::NiceMock<internal::MockOtp>>;
+using MockShutdown =
+    mask_rom_test::GlobalMock<testing::StrictMock<internal::MockShutdown>>;
+extern "C" {
+uint32_t otp_read32(uint32_t address) {
+  return MockOtp::Instance().otp_read32(address);
+}
+
+void shutdown_software_escalate(void) {
+  return MockShutdown::Instance().shutdown_software_escalate();
+}
+void shutdown_keymgr_kill(void) {
+  return MockShutdown::Instance().shutdown_keymgr_kill();
+}
+void shutdown_flash_kill(void) {
+  return MockShutdown::Instance().shutdown_flash_kill();
+}
+void shutdown_hang(void) { return MockShutdown::Instance().shutdown_hang(); }
+}  // extern "C"
+
+constexpr uint32_t Pack32(uint8_t a, uint8_t b, uint8_t c, uint8_t d) {
+  uint32_t result = (a << 0) | (b << 8) | (c << 16) | (d << 24);
+  return result;
+}
+
+#define FULL(name, prod, prodend, dev, rma)                          \
+  {                                                                  \
+    name, kAlertClass##prod, kAlertClass##prodend, kAlertClass##dev, \
+        kAlertClass##rma                                             \
+  }
+
+#define CLASSIFY(name, prod, prodend, dev, rma)                     \
+  Pack32(kAlertClass##prod, kAlertClass##prodend, kAlertClass##dev, \
+         kAlertClass##rma)
+// This alert configuration is described in the Mask ROM Shutdown specification:
+// https://docs.google.com/document/d/1V8hRvQnJhsvddieJbRHS3azbPZvoBWxfxPZV_0YA1QU/edit#
+
+// clang-format off
+#define ALERTS(Xmacro) \
+      Xmacro("Uart0FatalFault",                C, C, X, X), \
+      Xmacro("Uart1FatalFault",                C, C, X, X), \
+      Xmacro("Uart2FatalFault",                C, C, X, X), \
+      Xmacro("Uart3FatalFault",                C, C, X, X), \
+      Xmacro("GpioFatalFault",                 C, C, X, X), \
+      Xmacro("SpiDeviceFatalFault",            C, C, X, X), \
+      Xmacro("SpiHost0FatalFault",             C, C, X, X), \
+      Xmacro("SpiHost1FatalFault",             C, C, X, X), \
+      Xmacro("I2c0FatalFault",                 C, C, X, X), \
+      Xmacro("I2c1FatalFault",                 C, C, X, X), \
+      Xmacro("I2c2FatalFault",                 C, C, X, X), \
+      Xmacro("PattgenFatalFault",              C, C, X, X), \
+      Xmacro("OtpCtrlFatalMacroError",         A, A, X, X), \
+      Xmacro("OtpCtrlFatalCheckError",         A, A, X, X), \
+      Xmacro("LcCtrlFatalProgError",           A, A, X, X), \
+      Xmacro("LcCtrlFatalStateError",          A, A, X, X), \
+      Xmacro("LcCtrlFatalBusIntegError",       A, A, X, X), \
+      Xmacro("PwrmgrAonFatalFault",            C, C, X, X), \
+      Xmacro("RstmgrAonFatalFault",            C, C, X, X), \
+      Xmacro("ClkmgrAonFatalFault",            C, C, X, X), \
+      Xmacro("SysrstCtrlAonFatalFault",        C, C, X, X), \
+      Xmacro("AdcCtrlAonFatalFault",           C, C, X, X), \
+      Xmacro("PwmAonFatalFault",               C, C, X, X), \
+      Xmacro("PinmuxAonFatalFault",            C, C, X, X), \
+      Xmacro("AonTimerAonFatalFault",          C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovAs",           B, B, X, X), \
+      Xmacro("SensorCtrlAonRecovCg",           C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovGd",           C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovTsHi",         C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovTsLo",         C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovFla",          C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovOtp",          C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovOt0",          C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovOt1",          C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovOt2",          C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovOt3",          C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovOt4",          C, C, X, X), \
+      Xmacro("SensorCtrlAonRecovOt5",          C, C, X, X), \
+      Xmacro("SramCtrlRetAonFatalIntgError",   B, B, X, X), \
+      Xmacro("SramCtrlRetAonFatalParityError", B, B, X, X), \
+      Xmacro("FlashCtrlRecovErr",              D, D, X, X), \
+      Xmacro("FlashCtrlRecovMpErr",            D, D, X, X), \
+      Xmacro("FlashCtrlRecovEccErr",           D, D, X, X), \
+      Xmacro("FlashCtrlFatalIntgErr",          A, A, X, X), \
+      Xmacro("RvPlicFatalFault",               A, A, X, X), \
+      Xmacro("AesRecovCtrlUpdateErr",          D, D, X, X), \
+      Xmacro("AesFatalFault",                  A, A, X, X), \
+      Xmacro("HmacFatalFault",                 A, A, X, X), \
+      Xmacro("KmacFatalFault",                 A, A, X, X), \
+      Xmacro("KeymgrFatalFaultErr",            A, A, X, X), \
+      Xmacro("KeymgrRecovOperationErr",        D, D, X, X), \
+      Xmacro("CsrngFatalAlert",                A, A, X, X), \
+      Xmacro("EntropySrcRecovAlert",           D, D, X, X), \
+      Xmacro("EntropySrcFatalAlert",           A, A, X, X), \
+      Xmacro("Edn0FatalAlert",                 A, A, X, X), \
+      Xmacro("Edn1FatalAlert",                 A, A, X, X), \
+      Xmacro("SramCtrlMainFatalIntgError",     A, A, X, X), \
+      Xmacro("SramCtrlMainFatalParityError",   A, A, X, X), \
+      Xmacro("OtbnFatal",                      A, A, X, X), \
+      Xmacro("OtbnRecov",                      D, D, X, X), \
+      Xmacro("RomCtrlFatal",                   A, A, X, X)
+
+// TODO: find all of the local alerts and define them.
+#define LOC_ALERTS(Xmacro) \
+      Xmacro("LocAlertPingFail",               A, A, X, X), \
+      Xmacro("LocEscPingFail",                 A, A, X, X), \
+      Xmacro("LocAlertIntegrityFail",          A, A, X, X), \
+      Xmacro("LocEscIntegrityFail",            A, A, X, X), \
+// clang-format on
+
+// TODO: adjust this to match the OTP layout in PR#6921.
+struct OtpConfiguration {
+  uint32_t rom_error_reporting;
+  uint32_t rom_bootstrap_en;
+  uint32_t rom_fault_response;
+  uint32_t rom_alert_class_en;
+  uint32_t rom_alert_escalation;
+  uint32_t rom_alert_classification[80];
+  uint32_t rom_local_alert_classification[16];
+  uint32_t rom_alert_accum_thresh[4];
+  uint32_t rom_alert_timeout_cycles[4];
+  uint32_t rom_alert_phase_cycles[4][4];
+};
+
+struct DefaultAlertClassification {
+  const char *name;
+  alert_class_t prod;
+  alert_class_t prodend;
+  alert_class_t dev;
+  alert_class_t rma;
+};
+
+constexpr OtpConfiguration kOtpConfig = {
+    .rom_error_reporting = (uint32_t)kShutdownErrorRedactNone,
+    .rom_bootstrap_en = 1,
+    .rom_fault_response = 0,
+    .rom_alert_class_en = Pack32(kAlertEnableLocked, kAlertEnableEnabled,
+                                 kAlertEnableNone, kAlertEnableNone),
+    .rom_alert_escalation = Pack32(kAlertEscalatePhase3, kAlertEscalatePhase3,
+                                   kAlertEscalateNone, kAlertEscalateNone),
+    .rom_alert_classification = {ALERTS(CLASSIFY)},
+    .rom_local_alert_classification = {LOC_ALERTS(CLASSIFY)},
+    .rom_alert_accum_thresh = {0, 0, 0, 0},
+    .rom_alert_timeout_cycles = {0, 0, 0, 0},
+    .rom_alert_phase_cycles =
+        {
+            {0, 10, 10, 0xFFFFFFFF},  // Class A
+            {0, 10, 10, 0xFFFFFFFF},  // Class B
+            {0, 0, 0, 0},             // Class C
+            {0, 0, 0, 0},             // Class D
+        },
+};
+
+constexpr DefaultAlertClassification kDefaultAlertClassification[] = {
+    ALERTS(FULL),
+};
+
+static_assert(
+    ARRAYSIZE(kDefaultAlertClassification) == kTopEarlgreyAlertIdLast + 1,
+    "The default alert classification doesn't match the number of alerts");
+
+constexpr alert_class_t kClasses[] = {
+    kAlertClassA,
+    kAlertClassB,
+    kAlertClassC,
+    kAlertClassD,
+};
+
+alert_enable_t RomAlertClassEnable(alert_class_t cls) {
+  // Note: these need to match with `rom_alert_class_en` above.
+  switch (cls) {
+    case kAlertClassA:
+      return kAlertEnableLocked;
+    case kAlertClassB:
+      return kAlertEnableEnabled;
+    case kAlertClassC:
+      return kAlertEnableNone;
+    case kAlertClassD:
+      return kAlertEnableNone;
+    // Class X (and all other invalid classes) default to class A's enable
+    // status.
+    default:
+      return kAlertEnableLocked;
+  }
+}
+
+alert_escalate_t RomAlertClassEscalation(alert_class_t cls) {
+  // Note: these need to match with `rom_alert_class_escalation` above.
+  switch (cls) {
+    case kAlertClassA:
+      return kAlertEscalatePhase3;
+    case kAlertClassB:
+      return kAlertEscalatePhase3;
+    case kAlertClassC:
+      return kAlertEscalateNone;
+    case kAlertClassD:
+      return kAlertEscalateNone;
+    // Class X (and all other invalid classes) default to class A's escalate
+    // setting.
+    default:
+      return kAlertEscalatePhase3;
+  }
+}
+
+class ShutdownTest : public mask_rom_test::MaskRomTest {
+ protected:
+
+  void SetupOtpReads() {
+    // Make OTP reads retrieve their values from `otp_config_`.
+    ON_CALL(otp_, otp_read32(::testing::_))
+        .WillByDefault([this](uint32_t address) {
+          // Must be aligned and in the SW_CFG partition.
+          EXPECT_EQ(address % 4, 0);
+          EXPECT_GE(address, OTP_CTRL_PARAM_OWNER_SW_CFG_OFFSET);
+          EXPECT_LT(address, OTP_CTRL_PARAM_OWNER_SW_CFG_OFFSET +
+                                 sizeof(this->otp_config_));
+          // Convert the address to a word index.
+          uint32_t index = (address - OTP_CTRL_PARAM_OWNER_SW_CFG_OFFSET) / 4;
+          const uint32_t *words =
+              reinterpret_cast<const uint32_t *>(&this->otp_config_);
+          return words[index];
+        });
+  }
+
+  void ExpectClassConfigure() {
+    ExpectClassConfigure(0);
+    ExpectClassConfigure(1);
+    ExpectClassConfigure(2);
+    ExpectClassConfigure(3);
+  }
+
+  void ExpectClassConfigure(size_t i) {
+    alert_class_t expected_cls = kClasses[i];
+    EXPECT_CALL(alert_, alert_class_configure(expected_cls, ::testing::_))
+        .WillOnce(Invoke([this, i](alert_class_t cls,
+                                   const alert_class_config_t *config) {
+          alert_class_t expected_cls = kClasses[i];
+          // Would like to use testing::FiledsAre, but we need a gtest upgrade
+          // for that.
+          EXPECT_EQ(cls, expected_cls);
+          EXPECT_EQ(config->enabled, RomAlertClassEnable(expected_cls));
+          EXPECT_EQ(config->escalation, RomAlertClassEscalation(expected_cls));
+          EXPECT_EQ(config->accum_threshold,
+                    otp_config_.rom_alert_accum_thresh[i]);
+          EXPECT_EQ(config->timeout_cycles,
+                    otp_config_.rom_alert_timeout_cycles[i]);
+          EXPECT_THAT(config->phase_cycles,
+                      ElementsAre(otp_config_.rom_alert_phase_cycles[i][0],
+                                  otp_config_.rom_alert_phase_cycles[i][1],
+                                  otp_config_.rom_alert_phase_cycles[i][2],
+                                  otp_config_.rom_alert_phase_cycles[i][3]));
+          return kErrorOk;
+        }));
+  }
+
+  OtpConfiguration otp_config_ = kOtpConfig;
+  MockOtp otp_;
+  MockShutdown shutdown_;
+  mask_rom_test::MockAlert alert_;
+};
+
+TEST_F(ShutdownTest, InitializeProd) {
+  SetupOtpReads();
+  uint32_t index = 0;
+  for (const auto &c : kDefaultAlertClassification) {
+    alert_class_t cls = c.prod;
+    alert_enable_t en = RomAlertClassEnable(cls);
+    EXPECT_CALL(alert_, alert_configure(index, cls, en))
+        .WillOnce(Return(kErrorOk));
+    index += 1;
+  }
+  ExpectClassConfigure();
+  EXPECT_EQ(shutdown_init(kLcStateProd), kErrorOk);
+}
+
+TEST_F(ShutdownTest, InitializeProdWithError) {
+  SetupOtpReads();
+  uint32_t index = 0;
+  for (const auto &c : kDefaultAlertClassification) {
+    alert_class_t cls = c.prod;
+    alert_enable_t en = RomAlertClassEnable(cls);
+    // Return an error on index zero.  The error should not cause alert
+    // configuation to abort early (ie: still expect the rest of the
+    // alerts to get configured).
+    EXPECT_CALL(alert_, alert_configure(index, cls, en))
+        .WillOnce(Return(index == 0 ? kErrorUnknown : kErrorOk));
+    index += 1;
+  }
+  ExpectClassConfigure();
+  // We expect to get the error from alert configuration.
+  EXPECT_EQ(shutdown_init(kLcStateProd), kErrorUnknown);
+}
+
+TEST_F(ShutdownTest, InitializeProdEnd) {
+  SetupOtpReads();
+  uint32_t index = 0;
+  for (const auto &c : kDefaultAlertClassification) {
+    alert_class_t cls = c.prodend;
+    alert_enable_t en = RomAlertClassEnable(cls);
+    EXPECT_CALL(alert_, alert_configure(index, cls, en))
+        .WillOnce(Return(kErrorOk));
+    index += 1;
+  }
+  ExpectClassConfigure();
+  EXPECT_EQ(shutdown_init(kLcStateProdEnd), kErrorOk);
+}
+
+TEST_F(ShutdownTest, InitializeDev) {
+  SetupOtpReads();
+  uint32_t index = 0;
+  for (const auto &c : kDefaultAlertClassification) {
+    alert_class_t cls = c.dev;
+    alert_enable_t en = RomAlertClassEnable(cls);
+    EXPECT_CALL(alert_, alert_configure(index, cls, en))
+        .WillOnce(Return(kErrorOk));
+    index += 1;
+  }
+  ExpectClassConfigure();
+  EXPECT_EQ(shutdown_init(kLcStateDev), kErrorOk);
+}
+
+TEST_F(ShutdownTest, InitializeRma) {
+  SetupOtpReads();
+  uint32_t index = 0;
+  for (const auto &c : kDefaultAlertClassification) {
+    alert_class_t cls = c.rma;
+    alert_enable_t en = RomAlertClassEnable(cls);
+    EXPECT_CALL(alert_, alert_configure(index, cls, en))
+        .WillOnce(Return(kErrorOk));
+    index += 1;
+  }
+  ExpectClassConfigure();
+  EXPECT_EQ(shutdown_init(kLcStateRma), kErrorOk);
+}
+
+TEST(ShutdownModule, RedactErrors) {
+  EXPECT_EQ(shutdown_redact(kErrorOk, kShutdownErrorRedactNone), 0);
+  EXPECT_EQ(shutdown_redact(kErrorOk, kShutdownErrorRedactError), 0);
+  EXPECT_EQ(shutdown_redact(kErrorOk, kShutdownErrorRedactModule), 0);
+  EXPECT_EQ(shutdown_redact(kErrorOk, kShutdownErrorRedactAll), 0);
+
+  EXPECT_EQ(shutdown_redact(kErrorUartBadBaudRate, kShutdownErrorRedactNone),
+            0x02415503);
+  EXPECT_EQ(shutdown_redact(kErrorUartBadBaudRate, kShutdownErrorRedactError),
+            0x00415503);
+  EXPECT_EQ(shutdown_redact(kErrorUartBadBaudRate, kShutdownErrorRedactModule),
+            0x00000003);
+  EXPECT_EQ(shutdown_redact(kErrorUartBadBaudRate, kShutdownErrorRedactAll),
+            0xFFFFFFFF);
+}
+
+TEST_F(ShutdownTest, ShutdownFinalize) {
+  SetupOtpReads();
+  EXPECT_CALL(shutdown_, shutdown_software_escalate());
+  EXPECT_CALL(shutdown_, shutdown_keymgr_kill());
+  EXPECT_CALL(shutdown_, shutdown_flash_kill());
+  EXPECT_CALL(shutdown_, shutdown_hang());
+
+  // In the RV32 environment, finalize should never return.
+  // In the X86_64 unittest environment, verify that all of the various
+  // kill functions were called.
+  shutdown_finalize(kErrorUnknown);
+}
+
+}  // namespace
+}  // namespace shutdown_unittest
diff --git a/sw/device/silicon_creator/mask_rom/mask_rom.c b/sw/device/silicon_creator/mask_rom/mask_rom.c
index 5398e21..34a4e9d 100644
--- a/sw/device/silicon_creator/mask_rom/mask_rom.c
+++ b/sw/device/silicon_creator/mask_rom/mask_rom.c
@@ -8,6 +8,7 @@
 #include <stdint.h>
 
 #include "sw/device/lib/arch/device.h"
+#include "sw/device/lib/base/bitfield.h"
 #include "sw/device/lib/base/csr.h"
 #include "sw/device/lib/base/memory.h"
 #include "sw/device/lib/base/stdasm.h"
@@ -15,17 +16,51 @@
 #include "sw/device/lib/runtime/hart.h"
 #include "sw/device/lib/runtime/print.h"
 #include "sw/device/silicon_creator/lib/drivers/keymgr.h"
+#include "sw/device/silicon_creator/lib/drivers/lifecycle.h"
 #include "sw/device/silicon_creator/lib/drivers/uart.h"
+#include "sw/device/silicon_creator/lib/error.h"
+#include "sw/device/silicon_creator/lib/shutdown.h"
 #include "sw/device/silicon_creator/lib/sigverify.h"
 #include "sw/device/silicon_creator/mask_rom/romextimage.h"
 #include "sw/device/silicon_creator/mask_rom/sigverify_keys.h"
 
 #include "hw/top_earlgrey/sw/autogen/top_earlgrey.h"
 
-void mask_rom_exception_handler(void) { wait_for_interrupt(); }
-void mask_rom_nmi_handler(void) { wait_for_interrupt(); }
+static inline rom_error_t mask_rom_irq_error(void) {
+  uint32_t mcause;
+  CSR_READ(CSR_REG_MCAUSE, &mcause);
+  // Shuffle the mcause bits into the uppermost byte of the word and report
+  // the cause as kErrorInterrupt.
+  // Based on the ibex verilog, it appears that the most significant bit
+  // indicates whether the cause is an exception (0) or external interrupt (1),
+  // and the 5 least significant bits indicate which exception/interrupt.
+  //
+  // Preserve the MSB and shift the 7 LSBs into the upper byte.
+  // (we preserve 7 instead of 5 because the verilog hardcodes the unused bits
+  // as zero and those would be the next bits used should the number of
+  // interrupt causes increase).
+  mcause = (mcause & 0x80000000) | ((mcause & 0x7f) << 24);
+  return kErrorInterrupt + mcause;
+}
 
-void mask_rom_boot(void) {
+void mask_rom_interrupt_handler(void) {
+  shutdown_finalize(mask_rom_irq_error());
+}
+
+// We only need a single handler for all mask ROM interrupts, but we want to
+// keep distinct symbols to make writing tests easier.  In the mask ROM,
+// alias all interrupt handler symbols to the single handler.
+void mask_rom_exception_handler(void)
+    __attribute__((alias("mask_rom_interrupt_handler")));
+
+void mask_rom_nmi_handler(void)
+    __attribute__((alias("mask_rom_interrupt_handler")));
+
+rom_error_t mask_rom_boot(void) {
+  // Initialize the shutdown policy according to lifecycle state.
+  lifecycle_state_t lc_state = lifecycle_state_get();
+  shutdown_init(lc_state);
+
   // Initialize pinmux configuration so we can use the UART.
   pinmux_init();
 
@@ -37,7 +72,8 @@
   });
 
   // FIXME: what (if anything) should we print at startup?
-  base_printf("MaskROM\r\n");
+  base_printf("OpenTitan: \"version-tag\"\r\n");
+  base_printf("lc_state: %s\r\n", lifecycle_state_name[lc_state]);
 
   // boot_reason = read_boot_reason(); // Boot Policy Module
 
@@ -74,9 +110,7 @@
   while (true) {
     // TODO: Should we load the entropy_reseed_interval from OTP?
     const uint16_t reseed_interval = 0x100;
-    if (keymgr_init(reseed_interval) != kErrorOk) {
-      break;
-    }
+    RETURN_IF_ERROR(keymgr_init(reseed_interval));
 
     // Check ROM_EXT Manifest (2.c.ii)
     // **Open Q:** Integration with Secure Boot Hardware
@@ -93,20 +127,12 @@
     const manifest_t *manifest;
     manifest_signed_region_t signed_region;
     const sigverify_rsa_key_t *key;
-    if (romextimage_manifest_get(kFlashSlotA, &manifest) != kErrorOk) {
-      break;
-    }
-    if (manifest_signed_region_get(manifest, &signed_region) != kErrorOk) {
-      break;
-    }
-    if (sigverify_rsa_key_get(sigverify_rsa_key_id_get(&manifest->modulus),
-                              &key) != kErrorOk) {
-      break;
-    }
-    if (sigverify_rsa_verify(signed_region.start, signed_region.length,
-                             &manifest->signature, key) != kErrorOk) {
-      break;
-    }
+    RETURN_IF_ERROR(romextimage_manifest_get(kFlashSlotA, &manifest));
+    RETURN_IF_ERROR(manifest_signed_region_get(manifest, &signed_region));
+    RETURN_IF_ERROR(sigverify_rsa_key_get(
+        sigverify_rsa_key_id_get(&manifest->modulus), &key));
+    RETURN_IF_ERROR(sigverify_rsa_verify(
+        signed_region.start, signed_region.length, &manifest->signature, key));
 
     //  // Manifest Failure (check Boot Policy)
     //  // **Open Q:** Does this need different logic to the check after
@@ -136,10 +162,8 @@
     // CreatorRootKey (2.c.iv)
     // - This is only allowed to be done if we have verified the signature on
     //   the current ROM_EXT.
-    if (keymgr_state_advance_to_creator(
-            &manifest->binding_value, manifest->max_key_version) != kErrorOk) {
-      break;
-    }
+    RETURN_IF_ERROR(keymgr_state_advance_to_creator(&manifest->binding_value,
+                                                    manifest->max_key_version));
 
     // Lock down Peripherals based on descriptors in ROM_EXT manifest.
     // - This does not cover key-related lockdown, which is done in
@@ -151,8 +175,9 @@
     // **Open Q:** Do we need to prevent access to Mask ROM after final jump?
     // pmp_unlock_rom_ext(); // Hardened Jump Module.
 
+    // TODO(#6653): The keymgr lands in a disabled state with error code 0xe.
+    // RETURN_IF_ERROR(keymgr_state_creator_check());
     if (keymgr_state_creator_check() != kErrorOk) {
-      // TODO(#6653): The keymgr lands in a disabled state with error code 0xe.
       base_printf("ERROR keymgr: Failed to reach creator state.\n");
     }
 
@@ -180,10 +205,10 @@
     }
   }
 
-  // Boot failed for all ROM_EXTs allowed by boot policy
-  // boot_failed(boot_policy); // Boot Policy Module
-  asm volatile("unimp");
-  while (true) {
-    wait_for_interrupt();
-  }
+  return kErrorMaskRomBootFailed;
+}
+
+void mask_rom_main(void) {
+  rom_error_t error = mask_rom_boot();
+  shutdown_finalize(error);
 }
diff --git a/sw/device/silicon_creator/mask_rom/mask_rom.h b/sw/device/silicon_creator/mask_rom/mask_rom.h
index d52b92b..969bf2d 100644
--- a/sw/device/silicon_creator/mask_rom/mask_rom.h
+++ b/sw/device/silicon_creator/mask_rom/mask_rom.h
@@ -37,25 +37,14 @@
 /**
  * The first C function executed by the Mask ROM (defined in `mask_rom.c`)
  */
-noreturn void mask_rom_boot(void);
+noreturn void mask_rom_main(void);
 
 /**
  * Mask ROM hardware exception handler.
- *
- * This may not be able to be implemented in C.
  */
 MASK_ROM_VECTOR_FUNCTION
 MASK_ROM_INTERRUPT_HANDLER_ABI
-void mask_rom_exception_handler(void);
-
-/**
- * Mask ROM non-maskable interrupt handler.
- *
- * This may not be able to be implemented in C.
- */
-MASK_ROM_VECTOR_FUNCTION
-MASK_ROM_INTERRUPT_HANDLER_ABI
-void mask_rom_nmi_handler(void);
+void mask_rom_interrupt_handler(void);
 
 #ifdef __cplusplus
 }  // extern "C"
diff --git a/sw/device/silicon_creator/mask_rom/mask_rom_start.S b/sw/device/silicon_creator/mask_rom/mask_rom_start.S
index cd3bbab..dd3dcd7 100644
--- a/sw/device/silicon_creator/mask_rom/mask_rom_start.S
+++ b/sw/device/silicon_creator/mask_rom/mask_rom_start.S
@@ -56,43 +56,44 @@
 
   // Exception and User Software Interrupt Handler.
   .extern mask_rom_exception_handler
+  .extern mask_rom_interrupt_handler
   j mask_rom_exception_handler
   // Supervisor Software Interrupt Handler.
-  unimp
+  j mask_rom_interrupt_handler
   // Reserved.
-  unimp
+  j mask_rom_interrupt_handler
   // Machine Software Interrupt Handler.
-  unimp
+  j mask_rom_interrupt_handler
 
   // User Timer Interrupt Handler.
-  unimp
+  j mask_rom_interrupt_handler
   // Supervisor Timer Interrupt Handler.
-  unimp
+  j mask_rom_interrupt_handler
   // Reserved.
-  unimp
+  j mask_rom_interrupt_handler
   // Machine Timer Interrupt Handler.
-  unimp
+  j mask_rom_interrupt_handler
 
   // User External Interrupt Handler.
-  unimp
+  j mask_rom_interrupt_handler
   // Supervisor External Interrupt Handler.
-  unimp
+  j mask_rom_interrupt_handler
   // Reserved.
-  unimp
+  j mask_rom_interrupt_handler
   // Machine External Interrupt Handler.
-  unimp
+  j mask_rom_interrupt_handler
 
   // Reserved.
-  unimp
-  unimp
-  unimp
-  unimp
+  j mask_rom_interrupt_handler
+  j mask_rom_interrupt_handler
+  j mask_rom_interrupt_handler
+  j mask_rom_interrupt_handler
 
   // Vendor Interrupt Handlers:
 
   // On Ibex interrupt ids 30-16 are for "fast" interrupts.
   .rept 15
-  unimp
+  j mask_rom_interrupt_handler
   .endr
 
   // On Ibex interrupt id 31 is for non-maskable interrupts.
@@ -276,8 +277,8 @@
   /**
    * Jump to C Code
    */
-  .extern mask_rom_boot
-  tail mask_rom_boot
+  .extern mask_rom_main
+  tail mask_rom_main
 
   // Set size so this function can be disassembled.
   .size _mask_rom_start_boot, .-_mask_rom_start_boot
diff --git a/sw/device/silicon_creator/mask_rom/meson.build b/sw/device/silicon_creator/mask_rom/meson.build
index e927bdb..9ad384b 100644
--- a/sw/device/silicon_creator/mask_rom/meson.build
+++ b/sw/device/silicon_creator/mask_rom/meson.build
@@ -49,9 +49,11 @@
       freestanding_headers,
       sw_silicon_creator_lib_driver_hmac,
       sw_silicon_creator_lib_driver_keymgr,
+      sw_silicon_creator_lib_driver_lifecycle,
       sw_silicon_creator_lib_driver_uart,
       sw_silicon_creator_lib_fake_deps,
       sw_silicon_creator_lib_manifest,
+      sw_silicon_creator_lib_shutdown,
       sw_silicon_creator_mask_rom_sigverify,
       sw_silicon_creator_mask_rom_romextimage,
       sw_lib_crt,