Repository: chipsalliance/caliptra-sw
Pull request #456 (2023-07-05 19:26:55)

[feat] Boot status reporting for ROM Update Reset flow
This change adds reporting of boot statuses for the Update Reset flow of the ROM.


Patch:
diff --git a/common/src/boot_status.rs b/common/src/boot_status.rs
index 8c504a98..95469896 100644
--- a/common/src/boot_status.rs
+++ b/common/src/boot_status.rs
@@ -16,6 +16,8 @@ const IDEVID_BOOT_STATUS_BASE: u32 = 1;
 const LDEVID_BOOT_STATUS_BASE: u32 = 65;
 const FWPROCESSOR_BOOT_STATUS_BASE: u32 = 129;
 const FMCALIAS_BOOT_STATUS_BASE: u32 = 193;
+const COLD_RESET_BOOT_STATUS_BASE: u32 = 257;
+const UPDATE_RESET_BOOT_STATUS_BASE: u32 = 321;
 
 /// Statuses used by ROM to log dice derivation progress.
 #[repr(u32)]
@@ -58,6 +60,18 @@ pub enum RomBootStatus {
     FmcAliasSubjKeyIdGenerationComplete = FMCALIAS_BOOT_STATUS_BASE + 3,
     FmcAliasCertSigGenerationComplete = FMCALIAS_BOOT_STATUS_BASE + 4,
     FmcAliasDerivationComplete = FMCALIAS_BOOT_STATUS_BASE + 5,
+
+    // Cold Reset Statuses
+    ColdResetStarted = COLD_RESET_BOOT_STATUS_BASE,
+    ColdResetComplete = COLD_RESET_BOOT_STATUS_BASE + 1,
+
+    // Update Reset Statuses
+    UpdateResetStarted = UPDATE_RESET_BOOT_STATUS_BASE,
+    UpdateResetLoadManifestComplete = UPDATE_RESET_BOOT_STATUS_BASE + 1,
+    UpdateResetImageVerificationComplete = UPDATE_RESET_BOOT_STATUS_BASE + 2,
+    UpdateResetLoadImageComplete = UPDATE_RESET_BOOT_STATUS_BASE + 3,
+    UpdateResetOverwriteManifestComplete = UPDATE_RESET_BOOT_STATUS_BASE + 4,
+    UpdateResetComplete = UPDATE_RESET_BOOT_STATUS_BASE + 5,
 }
 
 impl From<RomBootStatus> for u32 {
diff --git a/rom/dev/src/flow/cold_reset/mod.rs b/rom/dev/src/flow/cold_reset/mod.rs
index 9b73b07b..9399debd 100644
--- a/rom/dev/src/flow/cold_reset/mod.rs
+++ b/rom/dev/src/flow/cold_reset/mod.rs
@@ -28,6 +28,7 @@ use crate::flow::cold_reset::idev_id::InitDevIdLayer;
 use crate::flow::cold_reset::ldev_id::LocalDevIdLayer;
 use crate::{cprintln, rom_env::RomEnv};
 use caliptra_common::FirmwareHandoffTable;
+use caliptra_common::RomBootStatus::*;
 use caliptra_drivers::*;
 
 pub const KEY_ID_UDS: KeyId = KeyId::KeyId0;
@@ -60,6 +61,7 @@ impl ColdResetFlow {
     #[inline(never)]
     pub fn run(env: &mut RomEnv) -> CaliptraResult<FirmwareHandoffTable> {
         cprintln!("[cold-reset] ++");
+        report_boot_status(ColdResetStarted.into());
 
         // Execute IDEVID layer
         let mut idevid_layer_output = InitDevIdLayer::derive(env)?;
@@ -80,6 +82,7 @@ impl ColdResetFlow {
         fw_proc_info.zeroize();
 
         cprintln!("[cold-reset] --");
+        report_boot_status(ColdResetComplete.into());
 
         Ok(fht::make_fht(env))
     }
diff --git a/rom/dev/src/flow/update_reset.rs b/rom/dev/src/flow/update_reset.rs
index fd0c178c..929f464c 100644
--- a/rom/dev/src/flow/update_reset.rs
+++ b/rom/dev/src/flow/update_reset.rs
@@ -14,7 +14,8 @@ Abstract:
 use crate::{cprintln, fht, rom_env::RomEnv, verifier::RomImageVerificationEnv, wdt};
 
 use caliptra_common::FirmwareHandoffTable;
-use caliptra_drivers::{MailboxRecvTxn, ResetReason};
+use caliptra_common::RomBootStatus::*;
+use caliptra_drivers::{report_boot_status, MailboxRecvTxn, ResetReason};
 use caliptra_error::{CaliptraError, CaliptraResult};
 use caliptra_image_types::ImageManifest;
 use caliptra_image_verify::{ImageVerificationInfo, ImageVerifier};
@@ -38,6 +39,7 @@ impl UpdateResetFlow {
     /// * `env` - ROM Environment
     pub fn run(env: &mut RomEnv) -> CaliptraResult<FirmwareHandoffTable> {
         cprintln!("[update-reset] ++");
+        report_boot_status(UpdateResetStarted.into());
 
         // Disable the watchdog timer during firmware download.
         wdt::stop_wdt(&mut env.soc_ifc);
@@ -56,6 +58,7 @@ impl UpdateResetFlow {
         wdt::start_wdt(&mut env.soc_ifc);
 
         let manifest = Self::load_manifest(&mut recv_txn)?;
+        report_boot_status(UpdateResetLoadManifestComplete.into());
 
         let mut venv = RomImageVerificationEnv {
             sha256: &mut env.sha256,
@@ -68,6 +71,7 @@ impl UpdateResetFlow {
         };
 
         let info = Self::verify_image(&mut venv, &manifest, recv_txn.dlen())?;
+        report_boot_status(UpdateResetImageVerificationComplete.into());
 
         cprintln!(
             "[update-reset] Image verified using Vendor ECC Key Index {}",
@@ -75,9 +79,13 @@ impl UpdateResetFlow {
         );
 
         Self::load_image(&manifest, recv_txn)?;
+        report_boot_status(UpdateResetLoadImageComplete.into());
 
         Self::copy_regions();
+        report_boot_status(UpdateResetOverwriteManifestComplete.into());
+
         cprintln!("[update-reset Success] --");
+        report_boot_status(UpdateResetComplete.into());
         Ok(fht::make_fht(env))
     }
 


Test patch:
diff --git a/rom/dev/tests/helpers.rs b/rom/dev/tests/helpers.rs
index 4f986aa1..d65ec65e 100644
--- a/rom/dev/tests/helpers.rs
+++ b/rom/dev/tests/helpers.rs
@@ -3,10 +3,13 @@
 use std::mem;
 
 use caliptra_builder::{ImageOptions, APP_WITH_UART, FMC_WITH_UART, ROM_WITH_UART};
+use caliptra_common::RomBootStatus;
 use caliptra_hw_model::{BootParams, Fuses, HwModel, InitParams, SecurityState};
 use caliptra_hw_model::{DefaultHwModel, ModelError};
 use caliptra_image_types::ImageBundle;
 
+pub const FW_LOAD_CMD_OPCODE: u32 = 0x4657_4C44;
+
 pub fn build_hw_model_and_image_bundle(
     fuses: Fuses,
     image_options: ImageOptions,
@@ -63,6 +66,43 @@ pub fn change_dword_endianess(data: &mut [u8]) {
     }
 }
 
+#[track_caller]
+pub fn step_until_boot_status(
+    hw: &mut DefaultHwModel,
+    expected_status: RomBootStatus,
+    ignore_intermediate_status: bool,
+) {
+    // Since the boot takes less than 20M cycles, we know something is wrong if
+    // we're stuck at the same state for that duration.
+    const MAX_WAIT_CYCLES: u32 = 20_000_000;
+
+    let mut cycle_count = 0u32;
+    let expected_status_u32: u32 = expected_status.into();
+    let initial_boot_status_u32 = hw.soc_ifc().cptra_boot_status().read();
+    loop {
+        let actual_status_u32 = hw.soc_ifc().cptra_boot_status().read();
+        if expected_status_u32 == actual_status_u32 {
+            break;
+        }
+
+        if !ignore_intermediate_status && actual_status_u32 != initial_boot_status_u32 {
+            panic!(
+                "Expected the next boot_status to be {expected_status:?} \
+                    ({expected_status_u32}), but status changed from \
+                    {initial_boot_status_u32} to {actual_status_u32})"
+            );
+        }
+        hw.step();
+        cycle_count += 1;
+        if cycle_count >= MAX_WAIT_CYCLES {
+            panic!(
+                "Expected boot_status to be {expected_status:?} \
+                    ({expected_status_u32}), but was stuck at ({actual_status_u32})"
+            );
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/rom/dev/tests/test_dice_derivations.rs b/rom/dev/tests/test_dice_derivations.rs
index c7c62c7b..c1973492 100644
--- a/rom/dev/tests/test_dice_derivations.rs
+++ b/rom/dev/tests/test_dice_derivations.rs
@@ -1,85 +1,76 @@
 // Licensed under the Apache-2.0 license
 
 use caliptra_builder::ImageOptions;
-use caliptra_common::RomBootStatus;
 use caliptra_common::RomBootStatus::*;
-use caliptra_hw_model::DefaultHwModel;
 use caliptra_hw_model::Fuses;
 use caliptra_hw_model::HwModel;
 
-pub mod helpers;
-
-#[track_caller]
-fn step_until_boot_status(hw: &mut DefaultHwModel, expected_status: RomBootStatus) {
-    // Since the boot takes less than 20M cycles, we know something is wrong if
-    // we're stuck at the same state for that duration.
-    const MAX_WAIT_CYCLES: u32 = 20_000_000;
+use crate::helpers::step_until_boot_status;
+use crate::helpers::FW_LOAD_CMD_OPCODE;
 
-    let mut cycle_count = 0u32;
-    let expected_status_u32: u32 = expected_status.into();
-    let initial_boot_status_u32 = hw.soc_ifc().cptra_boot_status().read();
-    loop {
-        let actual_status_u32 = hw.soc_ifc().cptra_boot_status().read();
-        if expected_status_u32 == actual_status_u32 {
-            break;
-        }
-        if actual_status_u32 != initial_boot_status_u32 {
-            panic!(
-                "Expected the next boot_status to be {expected_status:?} \
-                    ({expected_status_u32}), but status changed from \
-                    {initial_boot_status_u32} to {actual_status_u32})"
-            );
-        }
-        hw.step();
-        cycle_count += 1;
-        if cycle_count >= MAX_WAIT_CYCLES {
-            panic!(
-                "Expected boot_status to be {expected_status:?} \
-                    ({expected_status_u32}), but was stuck at ({actual_status_u32})"
-            );
-        }
-    }
-}
+pub mod helpers;
 
 #[test]
-fn test_status_reporting() {
+fn test_cold_reset_status_reporting() {
     let (mut hw, image_bundle) =
         helpers::build_hw_model_and_image_bundle(Fuses::default(), ImageOptions::default());
 
-    step_until_boot_status(&mut hw, IDevIdDecryptUdsComplete);
-    step_until_boot_status(&mut hw, IDevIdDecryptFeComplete);
-    step_until_boot_status(&mut hw, IDevIdClearDoeSecretsComplete);
-    step_until_boot_status(&mut hw, IDevIdCdiDerivationComplete);
-    step_until_boot_status(&mut hw, IDevIdKeyPairDerivationComplete);
-    step_until_boot_status(&mut hw, IDevIdSubjIdSnGenerationComplete);
-    step_until_boot_status(&mut hw, IDevIdSubjKeyIdGenerationComplete);
-    // step_until_boot_status(IdevIdMakeCsrComplete);
-    // step_until_boot_status(IdevIdSendCsrComplete);
-    step_until_boot_status(&mut hw, IDevIdDerivationComplete);
-    step_until_boot_status(&mut hw, LDevIdCdiDerivationComplete);
-    step_until_boot_status(&mut hw, LDevIdKeyPairDerivationComplete);
-    step_until_boot_status(&mut hw, LDevIdSubjIdSnGenerationComplete);
-    step_until_boot_status(&mut hw, LDevIdSubjKeyIdGenerationComplete);
-    step_until_boot_status(&mut hw, LDevIdCertSigGenerationComplete);
-    step_until_boot_status(&mut hw, LDevIdDerivationComplete);
+    step_until_boot_status(&mut hw, ColdResetStarted, false);
+    step_until_boot_status(&mut hw, IDevIdDecryptUdsComplete, false);
+    step_until_boot_status(&mut hw, IDevIdDecryptFeComplete, false);
+    step_until_boot_status(&mut hw, IDevIdClearDoeSecretsComplete, false);
+    step_until_boot_status(&mut hw, IDevIdCdiDerivationComplete, false);
+    step_until_boot_status(&mut hw, IDevIdKeyPairDerivationComplete, false);
+    step_until_boot_status(&mut hw, IDevIdSubjIdSnGenerationComplete, false);
+    step_until_boot_status(&mut hw, IDevIdSubjKeyIdGenerationComplete, false);
+    // step_until_boot_status(IdevIdMakeCsrComplete, false);
+    // step_until_boot_status(IdevIdSendCsrComplete, false);
+    step_until_boot_status(&mut hw, IDevIdDerivationComplete, false);
+    step_until_boot_status(&mut hw, LDevIdCdiDerivationComplete, false);
+    step_until_boot_status(&mut hw, LDevIdKeyPairDerivationComplete, false);
+    step_until_boot_status(&mut hw, LDevIdSubjIdSnGenerationComplete, false);
+    step_until_boot_status(&mut hw, LDevIdSubjKeyIdGenerationComplete, false);
+    step_until_boot_status(&mut hw, LDevIdCertSigGenerationComplete, false);
+    step_until_boot_status(&mut hw, LDevIdDerivationComplete, false);
 
     // Wait for uploading firmware.
     hw.step_until(|m| m.soc_ifc().cptra_flow_status().read().ready_for_fw());
 
-    assert!(hw
-        .upload_firmware(&image_bundle.to_bytes().unwrap())
-        .is_ok());
+    // Manually put the firmware in the mailbox because
+    // HwModel::upload_firmware returns only when the transaction is complete.
+    // This is too late for this test.
+    let buf: &[u8] = &image_bundle.to_bytes().unwrap();
+    assert!(!hw.soc_mbox().lock().read().lock());
+    hw.soc_mbox().cmd().write(|_| FW_LOAD_CMD_OPCODE);
+    hw.soc_mbox().dlen().write(|_| buf.len() as u32);
+    let mut remaining = buf;
+    while remaining.len() >= 4 {
+        // Panic is impossible because the subslice is always 4 bytes
+        let word = u32::from_le_bytes(remaining[..4].try_into().unwrap());
+        hw.soc_mbox().datain().write(|_| word);
+        remaining = &remaining[4..];
+    }
+    if !remaining.is_empty() {
+        let mut word_bytes = [0u8; 4];
+        word_bytes[..remaining.len()].copy_from_slice(remaining);
+        let word = u32::from_le_bytes(word_bytes);
+        hw.soc_mbox().datain().write(|_| word);
+    }
+    hw.soc_mbox().execute().write(|w| w.execute(true));
 
-    // [TODO] Don't use upload_firmware (whic returns only when the txn is complete); manually upload the firmware to test these boot statuses.
-    // step_until_boot_status(FwProcessorDownloadImageComplete);
-    // step_until_boot_status(FwProcessorManifestLoadComplete);
-    // step_until_boot_status(FwProcessorImageVerificationComplete);
-    // step_until_boot_status(FwProcessorPopulateDataVaultComplete);
-    // step_until_boot_status(FwProcessorExtendPcrComplete);
-    // step_until_boot_status(FwProcessorLoadImageComplete);
-    step_until_boot_status(&mut hw, FwProcessorFirmwareDownloadTxComplete);
-    step_until_boot_status(&mut hw, FwProcessorComplete);
-    step_until_boot_status(&mut hw, FmcAliasDeriveCdiComplete);
-    step_until_boot_status(&mut hw, FmcAliasKeyPairDerivationComplete);
-    step_until_boot_status(&mut hw, FmcAliasSubjIdSnGenerationComplete);
+    step_until_boot_status(&mut hw, FwProcessorDownloadImageComplete, false);
+    step_until_boot_status(&mut hw, FwProcessorManifestLoadComplete, false);
+    step_until_boot_status(&mut hw, FwProcessorImageVerificationComplete, false);
+    step_until_boot_status(&mut hw, FwProcessorPopulateDataVaultComplete, false);
+    step_until_boot_status(&mut hw, FwProcessorExtendPcrComplete, false);
+    step_until_boot_status(&mut hw, FwProcessorLoadImageComplete, false);
+    step_until_boot_status(&mut hw, FwProcessorFirmwareDownloadTxComplete, false);
+    step_until_boot_status(&mut hw, FwProcessorComplete, false);
+    step_until_boot_status(&mut hw, FmcAliasDeriveCdiComplete, false);
+    step_until_boot_status(&mut hw, FmcAliasKeyPairDerivationComplete, false);
+    step_until_boot_status(&mut hw, FmcAliasSubjIdSnGenerationComplete, false);
+    step_until_boot_status(&mut hw, FmcAliasSubjKeyIdGenerationComplete, false);
+    step_until_boot_status(&mut hw, FmcAliasCertSigGenerationComplete, false);
+    step_until_boot_status(&mut hw, FmcAliasDerivationComplete, false);
+    step_until_boot_status(&mut hw, ColdResetComplete, false);
 }
diff --git a/rom/dev/tests/test_fmcalias_derivation.rs b/rom/dev/tests/test_fmcalias_derivation.rs
index 9222cbbb..40d929c7 100644
--- a/rom/dev/tests/test_fmcalias_derivation.rs
+++ b/rom/dev/tests/test_fmcalias_derivation.rs
@@ -1,6 +1,8 @@
 // Licensed under the Apache-2.0 license
 
+use crate::helpers::step_until_boot_status;
 use caliptra_builder::{FwId, ImageOptions, APP_WITH_UART, ROM_WITH_UART};
+use caliptra_common::RomBootStatus::ColdResetComplete;
 use caliptra_common::{FirmwareHandoffTable, FuseLogEntry, FuseLogEntryId};
 use caliptra_common::{PcrLogEntry, PcrLogEntryId};
 use caliptra_error::CaliptraError;
@@ -110,8 +112,7 @@ fn test_pcr_log() {
         .upload_firmware(&image_bundle.to_bytes().unwrap())
         .is_ok());
 
-    hw.step_until_output_contains("[exit] Launching FMC")
-        .unwrap();
+    step_until_boot_status(&mut hw, ColdResetComplete, true);
 
     let result = hw.mailbox_execute(0x1000_0000, &[]);
     assert!(result.is_ok());
@@ -243,8 +244,7 @@ fn test_fuse_log() {
         .upload_firmware(&image_bundle.to_bytes().unwrap())
         .is_ok());
 
-    hw.step_until_output_contains("[exit] Launching FMC")
-        .unwrap();
+    step_until_boot_status(&mut hw, ColdResetComplete, true);
 
     let result = hw.mailbox_execute(0x1000_0002, &[]);
     assert!(result.is_ok());
@@ -358,8 +358,7 @@ fn test_fht_info() {
         .upload_firmware(&image_bundle.to_bytes().unwrap())
         .is_ok());
 
-    hw.step_until_output_contains("[exit] Launching FMC")
-        .unwrap();
+    step_until_boot_status(&mut hw, ColdResetComplete, true);
 
     let result = hw.mailbox_execute(0x1000_0003, &[]);
     assert!(result.is_ok());
diff --git a/rom/dev/tests/test_update_reset.rs b/rom/dev/tests/test_update_reset.rs
index 2db706ee..086934ab 100644
--- a/rom/dev/tests/test_update_reset.rs
+++ b/rom/dev/tests/test_update_reset.rs
@@ -3,6 +3,9 @@
 use caliptra_builder::{FwId, ImageOptions, APP_WITH_UART, ROM_WITH_UART};
 use caliptra_error::CaliptraError;
 use caliptra_hw_model::{BootParams, Fuses, HwModel, InitParams, SecurityState};
+
+use crate::helpers::{step_until_boot_status, FW_LOAD_CMD_OPCODE};
+use caliptra_common::RomBootStatus::*;
 pub mod helpers;
 
 const TEST_FMC_CMD_RESET_FOR_UPDATE: u32 = 0x1000_0004;
@@ -39,13 +42,12 @@ fn test_update_reset_success() {
     hw.upload_firmware(&image_bundle.to_bytes().unwrap())
         .unwrap();
 
-    hw.step_until_output_contains("[exit] Launching FMC")
-        .unwrap();
+    step_until_boot_status(&mut hw, ColdResetComplete, true);
 
     hw.mailbox_execute(TEST_FMC_CMD_RESET_FOR_UPDATE, &[])
         .unwrap();
 
-    hw.step_until_output_contains("[update-reset] ++").unwrap();
+    step_until_boot_status(&mut hw, UpdateResetStarted, false);
 
     hw.upload_firmware(&image_bundle.to_bytes().unwrap())
         .unwrap();
@@ -85,13 +87,12 @@ fn test_update_reset_no_mailbox_cmd() {
     hw.upload_firmware(&image_bundle.to_bytes().unwrap())
         .unwrap();
 
-    hw.step_until_output_contains("[exit] Launching FMC")
-        .unwrap();
+    step_until_boot_status(&mut hw, ColdResetComplete, true);
 
     hw.mailbox_execute(TEST_FMC_CMD_RESET_FOR_UPDATE, &[])
         .unwrap();
 
-    hw.step_until_output_contains("[update-reset] ++").unwrap();
+    step_until_boot_status(&mut hw, UpdateResetStarted, false);
 
     // No command in the mailbox.
     hw.soc_mbox().cmd().write(|_| 0);
@@ -138,13 +139,12 @@ fn test_update_reset_non_fw_load_cmd() {
     hw.upload_firmware(&image_bundle.to_bytes().unwrap())
         .unwrap();
 
-    hw.step_until_output_contains("[exit] Launching FMC")
-        .unwrap();
+    step_until_boot_status(&mut hw, ColdResetComplete, true);
 
     hw.mailbox_execute(TEST_FMC_CMD_RESET_FOR_UPDATE, &[])
         .unwrap();
 
-    hw.step_until_output_contains("[update-reset] ++").unwrap();
+    step_until_boot_status(&mut hw, UpdateResetStarted, false);
 
     // Send a non-fw load command
     let _ = hw.mailbox_execute(0xDEADBEEF, &[]);
@@ -188,13 +188,12 @@ fn test_update_reset_verify_image_failure() {
     hw.upload_firmware(&image_bundle.to_bytes().unwrap())
         .unwrap();
 
-    hw.step_until_output_contains("[exit] Launching FMC")
-        .unwrap();
+    step_until_boot_status(&mut hw, ColdResetComplete, true);
 
     hw.mailbox_execute(TEST_FMC_CMD_RESET_FOR_UPDATE, &[])
         .unwrap();
 
-    hw.step_until_output_contains("[update-reset] ++").unwrap();
+    step_until_boot_status(&mut hw, UpdateResetStarted, false);
 
     // Upload invalid manifest
     let _ = hw.upload_firmware(&[0u8; 4]);
@@ -206,3 +205,73 @@ fn test_update_reset_verify_image_failure() {
         CaliptraError::IMAGE_VERIFIER_ERR_MANIFEST_MARKER_MISMATCH.into()
     );
 }
+
+#[test]
+fn test_update_reset_boot_status() {
+    pub const TEST_FMC_WITH_UART: FwId = FwId {
+        crate_name: "caliptra-rom-test-fmc",
+        bin_name: "caliptra-rom-test-fmc",
+        features: &["emu"],
+        workspace_dir: None,
+    };
+
+    let fuses = Fuses::default();
+    let rom = caliptra_builder::build_firmware_rom(&ROM_WITH_UART).unwrap();
+    let mut hw = caliptra_hw_model::new(BootParams {
+        init_params: InitParams {
+            rom: &rom,
+            security_state: SecurityState::from(fuses.life_cycle as u32),
+            ..Default::default()
+        },
+        fuses,
+        ..Default::default()
+    })
+    .unwrap();
+
+    let image_bundle = caliptra_builder::build_and_sign_image(
+        &TEST_FMC_WITH_UART,
+        &APP_WITH_UART,
+        ImageOptions::default(),
+    )
+    .unwrap();
+
+    hw.upload_firmware(&image_bundle.to_bytes().unwrap())
+        .unwrap();
+
+    step_until_boot_status(&mut hw, FmcAliasDerivationComplete, true);
+
+    hw.mailbox_execute(TEST_FMC_CMD_RESET_FOR_UPDATE, &[])
+        .unwrap();
+
+    step_until_boot_status(&mut hw, UpdateResetStarted, false);
+
+    // Manually put the firmware in the mailbox because
+    // HwModel::upload_firmware returns only when the transaction is complete.
+    // This is too late for this test.
+    let buf: &[u8] = &image_bundle.to_bytes().unwrap();
+    assert!(!hw.soc_mbox().lock().read().lock());
+    hw.soc_mbox().cmd().write(|_| FW_LOAD_CMD_OPCODE);
+    hw.soc_mbox().dlen().write(|_| buf.len() as u32);
+    let mut remaining = buf;
+    while remaining.len() >= 4 {
+        // Panic is impossible because the subslice is always 4 bytes
+        let word = u32::from_le_bytes(remaining[..4].try_into().unwrap());
+        hw.soc_mbox().datain().write(|_| word);
+        remaining = &remaining[4..];
+    }
+    if !remaining.is_empty() {
+        let mut word_bytes = [0u8; 4];
+        word_bytes[..remaining.len()].copy_from_slice(remaining);
+        let word = u32::from_le_bytes(word_bytes);
+        hw.soc_mbox().datain().write(|_| word);
+    }
+    hw.soc_mbox().execute().write(|w| w.execute(true));
+
+    step_until_boot_status(&mut hw, UpdateResetLoadManifestComplete, false);
+    step_until_boot_status(&mut hw, UpdateResetImageVerificationComplete, false);
+    step_until_boot_status(&mut hw, UpdateResetLoadImageComplete, false);
+    step_until_boot_status(&mut hw, UpdateResetOverwriteManifestComplete, false);
+    step_until_boot_status(&mut hw, UpdateResetComplete, false);
+
+    hw.step_until_exit_success().unwrap();
+}