Repository: chipsalliance/caliptra-sw
Pull request #133 (2023-04-01 05:59:34)

Handoff module of FMC
Introduces a custom type to encapsulate knowledge of FHT fields as well as implement handoff behavior of FMC :
       - Retrieves FHT table from fixed address in DCCM.
       - Perform validity checks of FHT data.
       -  Retrieves information stored  in the data vault by ROM.
       - Transfers control to the runtime firmware.



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 2459d164..cb4a05d9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -228,6 +228,20 @@ version = "0.1.0"
 [[package]]
 name = "caliptra-fmc"
 version = "0.1.0"
+dependencies = [
+ "caliptra-builder",
+ "caliptra-cpu",
+ "caliptra-drivers",
+ "caliptra-hw-model",
+ "caliptra_common",
+ "cfg-if",
+ "ufmt",
+ "zerocopy",
+]
+
+[[package]]
+name = "caliptra-fmc-mock-rt"
+version = "0.1.0"
 dependencies = [
  "caliptra-builder",
  "caliptra-cpu",
diff --git a/Cargo.toml b/Cargo.toml
index 34c5fe9a..ef4851b4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -20,6 +20,7 @@ members = [
   "drivers",
   "drivers/test-fw",
   "fmc",
+  "fmc/test-fw/test-rt",
   "hw-latest/verilated",
   "hw-model",
   "registers",
diff --git a/drivers/src/data_vault.rs b/drivers/src/data_vault.rs
index ccecae23..b568f2c8 100644
--- a/drivers/src/data_vault.rs
+++ b/drivers/src/data_vault.rs
@@ -99,6 +99,19 @@ impl From<WarmResetEntry4> for usize {
     }
 }
 
+impl TryFrom<u8> for WarmResetEntry4 {
+    type Error = ();
+    fn try_from(original: u8) -> Result<Self, Self::Error> {
+        match original {
+            0 => Ok(Self::RtSvn),
+            1 => Ok(Self::RtLoadAddr),
+            2 => Ok(Self::RtEntryPoint),
+            3 => Ok(Self::ManifestAddr),
+            _ => Err(()),
+        }
+    }
+}
+
 #[derive(Default, Debug)]
 pub struct DataVault {}
 
@@ -546,7 +559,7 @@ impl DataVault {
     /// # Returns
     ///    warm reset entry value  
     ///
-    fn read_warm_reset_entry4(&self, entry: WarmResetEntry4) -> u32 {
+    pub fn read_warm_reset_entry4(&self, entry: WarmResetEntry4) -> u32 {
         let dv = dv::RegisterBlock::dv_reg();
         dv.non_sticky_lockable_scratch_reg().at(entry.into()).read()
     }
diff --git a/drivers/src/mailbox.rs b/drivers/src/mailbox.rs
index 74a1c310..2e6e9a32 100644
--- a/drivers/src/mailbox.rs
+++ b/drivers/src/mailbox.rs
@@ -13,6 +13,7 @@ Abstract:
 --*/
 
 use crate::{caliptra_err_def, CaliptraResult};
+use caliptra_registers::mbox::enums::MboxFsmE;
 use caliptra_registers::mbox::{self, enums::MboxStatusE};
 use core::cmp::min;
 use core::mem::size_of;
@@ -69,8 +70,8 @@ impl Mailbox {
     /// * 'MailboxRecvTxn' - Object representing a receive operation
     pub fn try_start_recv_txn(&self) -> Option<MailboxRecvTxn> {
         let mbox = mbox::RegisterBlock::mbox_csr();
-        match mbox.status().read().status() {
-            MboxStatusE::DataReady => Some(MailboxRecvTxn::default()),
+        match mbox.status().read().mbox_fsm_ps() {
+            MboxFsmE::MboxExecuteUc => Some(MailboxRecvTxn::default()),
             _ => None,
         }
     }
diff --git a/fmc/Cargo.toml b/fmc/Cargo.toml
index 17f35e19..cf8fe39a 100644
--- a/fmc/Cargo.toml
+++ b/fmc/Cargo.toml
@@ -17,6 +17,7 @@ cfg-if = "1.0.0"
 
 [dev-dependencies]
 caliptra-builder = { path = "../builder" }
+caliptra-hw-model = { path = "../hw-model" }
 
 [features]
 riscv = ["caliptra-cpu/riscv"]
diff --git a/fmc/src/hand_off.rs b/fmc/src/hand_off.rs
new file mode 100644
index 00000000..ba50dd89
--- /dev/null
+++ b/fmc/src/hand_off.rs
@@ -0,0 +1,97 @@
+/*++
+
+Licensed under the Apache-2.0 license.
+
+File Name:
+
+    hand_off.rs
+
+    Implements handoff behavior of FMC :
+        - Retrieves FHT table from fixed address in DCCM.
+        - Transfers control to the runtime firmware.
+++*/
+
+use crate::fmc_env::FmcEnv;
+use caliptra_common::{cprintln, FirmwareHandoffTable};
+use caliptra_drivers::WarmResetEntry4;
+
+#[cfg(feature = "riscv")]
+core::arch::global_asm!(include_str!("transfer_control.S"));
+
+const ICCM_ORG: u32 = 0x40000000;
+const ICCM_SIZE: u32 = 128 << 10;
+
+struct MemoryRegion {
+    start: u32,
+    size: u32,
+}
+
+impl MemoryRegion {
+    fn validate_address(&self, phys_addr: u32) -> bool {
+        phys_addr >= self.start && phys_addr <= self.start + self.size
+    }
+}
+
+const ICCM: MemoryRegion = MemoryRegion {
+    start: ICCM_ORG,
+    size: ICCM_SIZE,
+};
+
+pub struct HandOff {
+    fht: FirmwareHandoffTable,
+}
+
+pub fn dump_fht(fht: &FirmwareHandoffTable) {
+    cprintln!("[fmc] FHT Marker: 0x{:08X}", fht.fht_marker);
+    cprintln!("[fmc] FHT Major Version: 0x{:04X}", fht.fht_major_ver);
+    cprintln!("[fmc] FHT Minor Version: 0x{:04X}", fht.fht_minor_ver);
+    cprintln!("[fmc] FHT Manifest Addr: 0x{:08X}", fht.manifest_load_addr);
+    cprintln!("[fmc] FHT FMC CDI KV KeyID: {}", fht.fmc_cdi_kv_idx);
+    cprintln!(
+        "[fmc] FHT FMC PrivKey KV KeyID: {}",
+        fht.fmc_priv_key_kv_idx
+    );
+    cprintln!(
+        "[fmc] FHT RT Load Address: 0x{:08x}",
+        fht.rt_fw_entry_point_idx
+    );
+    cprintln!(
+        "[fmc] FHT RT Entry Point: 0x{:08x}",
+        fht.rt_fw_load_addr_idx
+    );
+}
+
+impl HandOff {
+    pub fn from_previous() -> Option<HandOff> {
+        if let Some(fht) = FirmwareHandoffTable::try_load() {
+            dump_fht(&fht);
+            let me = Self { fht };
+            return Some(me);
+        }
+        None
+    }
+
+    pub fn to_rt(&self, env: &FmcEnv) -> ! {
+        // Function is defined in start.S
+        extern "C" {
+            fn transfer_control(entry: u32) -> !;
+        }
+        let rt_entry = self.rt_entry_point(env);
+        cprintln!("[exit] Launching RT @ 0x{:08X}", rt_entry);
+
+        if !ICCM.validate_address(rt_entry) {
+            crate::report_error(0xdead);
+        }
+        // Exit FMC and jump to speicified entry point
+        unsafe { transfer_control(rt_entry) }
+    }
+
+    fn rt_entry_point(&self, env: &FmcEnv) -> u32 {
+        env.data_vault().map(|d| {
+            d.read_warm_reset_entry4(
+                WarmResetEntry4::try_from(self.fht.rt_fw_entry_point_idx)
+                    .unwrap_or_else(|_| crate::report_error(0xdead)),
+            )
+        })
+    }
+}
diff --git a/fmc/src/main.rs b/fmc/src/main.rs
index 195cedfb..78d00184 100644
--- a/fmc/src/main.rs
+++ b/fmc/src/main.rs
@@ -15,16 +15,14 @@ Abstract:
 #![cfg_attr(not(feature = "std"), no_main)]
 use core::hint::black_box;
 
-#[cfg(feature = "riscv")]
-core::arch::global_asm!(include_str!("transfer_control.S"));
-
-use caliptra_common::{cprintln, FirmwareHandoffTable};
+use caliptra_common::cprintln;
 use caliptra_drivers::report_fw_error_non_fatal;
 pub mod fmc_env;
 pub mod fmc_env_cell;
+mod hand_off;
 
 use caliptra_cpu::TrapRecord;
-use fmc_env::FmcEnv;
+use hand_off::HandOff;
 
 #[cfg(feature = "std")]
 pub fn main() {}
@@ -37,27 +35,9 @@ Running Caliptra FMC ...
 pub extern "C" fn entry_point() -> ! {
     cprintln!("{}", BANNER);
 
-    if let Some(fht) = FirmwareHandoffTable::try_load() {
-        cprintln!("[fmc] FHT Marker: 0x{:08X}", fht.fht_marker);
-        cprintln!("[fmc] FHT Major Version: 0x{:04X}", fht.fht_major_ver);
-        cprintln!("[fmc] FHT Minor Version: 0x{:04X}", fht.fht_minor_ver);
-        cprintln!("[fmc] FHT Manifest Addr: 0x{:08X}", fht.manifest_load_addr);
-        cprintln!("[fmc] FHT FMC CDI KV KeyID: {}", fht.fmc_cdi_kv_idx);
-        cprintln!(
-            "[fmc] FHT FMC PrivKey KV KeyID: {}",
-            fht.fmc_priv_key_kv_idx
-        );
-        cprintln!(
-            "[fmc] FHT RT Load Address: 0x{:08x}",
-            fht.rt_fw_load_addr_idx
-        );
-        cprintln!(
-            "[fmc] FHT RT Entry Point: 0x{:08x}",
-            fht.rt_fw_load_addr_idx
-        );
-
+    if let Some(hand_off) = HandOff::from_previous() {
         let env = fmc_env::FmcEnv::default();
-        launch_rt(&env)
+        hand_off.to_rt(&env)
     } else {
         caliptra_drivers::ExitCtrl::exit(0xff)
     }
@@ -104,21 +84,6 @@ fn fmc_panic(_: &core::panic::PanicInfo) -> ! {
     report_error(0xdead);
 }
 
-fn launch_rt(env: &FmcEnv) -> ! {
-    // Function is defined in start.S
-    extern "C" {
-        fn transfer_control(entry: u32) -> !;
-    }
-
-    // Get the fmc entry point from data vault
-    let entry = env.data_vault().map(|d| d.rt_entry_point());
-
-    cprintln!("[exit] Launching RT @ 0x{:08X}", entry);
-
-    // Exit ROM and jump to speicified entry point
-    unsafe { transfer_control(entry) }
-}
-
 #[allow(clippy::empty_loop)]
 fn report_error(code: u32) -> ! {
     cprintln!("FMC Error: 0x{:08X}", code);
diff --git a/hw-model/src/lib.rs b/hw-model/src/lib.rs
index 3501beef..467c0bb9 100644
--- a/hw-model/src/lib.rs
+++ b/hw-model/src/lib.rs
@@ -210,11 +210,6 @@ pub trait HwModel {
             self.soc_mbox().datain().write(|_| last_word);
         }
 
-        // Set the status as DATA_READY.
-        self.soc_mbox()
-            .status()
-            .write(|w| w.status(|w| w.data_ready()));
-
         // Set Execute Bit
         self.soc_mbox().execute().write(|w| w.execute(true));
 
@@ -354,7 +349,6 @@ mod tests {
             caliptra_hw_model::FW_LOAD_CMD_OPCODE
         );
         assert_eq!(model.soc_mbox().dlen().read(), firmware.len() as u32);
-        assert!(model.soc_mbox().status().read().status().data_ready());
 
         // Read the data out of the mailbox.
         let mut temp: Vec<u32> = Vec::new();
diff --git a/sw-emulator/app/src/main.rs b/sw-emulator/app/src/main.rs
index a8a5ccf3..f68d279f 100644
--- a/sw-emulator/app/src/main.rs
+++ b/sw-emulator/app/src/main.rs
@@ -298,9 +298,6 @@ fn upload_fw_to_mailbox(mailbox: &mut Mailbox, firmware_buffer: Rc<Vec<u8>>) {
         let _ = mailbox.write_datain(last_word);
     }
 
-    // Set the status as DATA_READY.
-    let _ = mailbox.set_status_data_ready();
-
     // Set the execute register.
     let _ = mailbox.write_execute(1);
 }
diff --git a/sw-emulator/lib/periph/src/mailbox.rs b/sw-emulator/lib/periph/src/mailbox.rs
index caa5a778..935afd6e 100644
--- a/sw-emulator/lib/periph/src/mailbox.rs
+++ b/sw-emulator/lib/periph/src/mailbox.rs
@@ -19,8 +19,8 @@ use caliptra_emu_derive::Bus;
 use caliptra_emu_types::{RvAddr, RvData, RvSize};
 use std::{cell::RefCell, rc::Rc};
 use tock_registers::interfaces::{ReadWriteable, Readable, Writeable};
-use tock_registers::register_bitfields;
 use tock_registers::registers::InMemoryRegister;
+use tock_registers::{register_bitfields, LocalRegisterCopy};
 
 /// Maximum mailbox capacity.
 const MAX_MAILBOX_CAPACITY_BYTES: usize = 128 << 10;
@@ -358,13 +358,26 @@ impl MailboxRegs {
 
     // Todo: Implement write status callback fn
     pub fn write_status(&mut self, _size: RvSize, val: RvData) -> Result<(), BusError> {
-        self.state_machine.context.status = val;
+        let val = LocalRegisterCopy::<u32, Status::Register>::new(val);
+        self.state_machine
+            .context
+            .status
+            .write(Status::STATUS.val(val.read(Status::STATUS)));
         Ok(())
     }
 
     // Todo: Implement read status callback fn
     pub fn read_status(&self, _size: RvSize) -> Result<u32, BusError> {
-        Ok(self.state_machine.context.status)
+        let mut result = self.state_machine.context.status;
+        result.modify(match self.state_machine.state {
+            // TODO: What about MBOX_EXECUTE_SOC?
+            States::Exec => Status::MBOX_FSM_PS::MBOX_EXECUTE_UC,
+            States::Idle => Status::MBOX_FSM_PS::MBOX_IDLE,
+            States::RdyForCmd => Status::MBOX_FSM_PS::MBOX_RDY_FOR_CMD,
+            States::RdyForData => Status::MBOX_FSM_PS::MBOX_RDY_FOR_DATA,
+            States::RdyForDlen => Status::MBOX_FSM_PS::MBOX_RDY_FOR_DLEN,
+        });
+        Ok(result.get())
     }
 }
 
@@ -410,7 +423,7 @@ pub struct Context {
     /// Fifo storage
     pub ring_buffer: RingBuffer,
     /// Mailbox Status
-    status: u32,
+    status: LocalRegisterCopy<u32, Status::Register>,
     /// Command
     pub cmd: u32,
     // data_out
@@ -424,7 +437,7 @@ impl Context {
             user: 0,
             exec: false,
             dlen: 0,
-            status: 0,
+            status: LocalRegisterCopy::new(0),
             ring_buffer: RingBuffer::new(ram),
             cmd: 0,
             data_out: 0,
@@ -590,7 +603,10 @@ mod tests {
         ));
 
         let status = mb.read(RvSize::Word, Mailbox::OFFSET_STATUS).unwrap();
-        assert_eq!(status, Status::STATUS::DATA_READY.value);
+        assert_eq!(
+            status,
+            (Status::STATUS::DATA_READY + Status::MBOX_FSM_PS::MBOX_EXECUTE_UC).value
+        );
 
         let cmd = mb.read(RvSize::Word, Mailbox::OFFSET_CMD).unwrap();
         assert_eq!(cmd, 0x55);
@@ -764,7 +780,10 @@ mod tests {
         ));
 
         let status = mb.read(RvSize::Word, Mailbox::OFFSET_STATUS).unwrap();
-        assert_eq!(status, Status::STATUS::DATA_READY.value);
+        assert_eq!(
+            status,
+            (Status::STATUS::DATA_READY + Status::MBOX_FSM_PS::MBOX_EXECUTE_UC).value
+        );
 
         let cmd = mb.read(RvSize::Word, Mailbox::OFFSET_CMD).unwrap();
         assert_eq!(cmd, 0x55);


Test patch:
diff --git a/fmc/test-fw/test-rt/Cargo.toml b/fmc/test-fw/test-rt/Cargo.toml
new file mode 100644
index 00000000..394147c3
--- /dev/null
+++ b/fmc/test-fw/test-rt/Cargo.toml
@@ -0,0 +1,28 @@
+# Licensed under the Apache-2.0 license
+
+[package]
+name = "caliptra-fmc-mock-rt"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+caliptra-cpu = { version = "0.1.0", path = "../../../cpu" }
+caliptra-drivers = { path = "../../../drivers" }
+caliptra_common = { path = "../../../common", default-features = false }
+ufmt = "0.2.0"
+zerocopy = "0.6.1"
+
+[build-dependencies]
+cfg-if = "1.0.0"
+
+[dev-dependencies]
+caliptra-builder = { path = "../../../builder" }
+
+[features]
+riscv = ["caliptra-cpu/riscv"]
+default = ["std"]
+emu = [
+    "caliptra_common/emu",
+    "caliptra-drivers/emu"
+]
+std = ["ufmt/std", "caliptra_common/std"]
diff --git a/fmc/test-fw/test-rt/build.rs b/fmc/test-fw/test-rt/build.rs
new file mode 100644
index 00000000..0068d644
--- /dev/null
+++ b/fmc/test-fw/test-rt/build.rs
@@ -0,0 +1,23 @@
+// Licensed under the Apache-2.0 license
+
+fn main() {
+    cfg_if::cfg_if! {
+        if #[cfg(not(feature = "std"))] {
+            use std::env;
+            use std::fs;
+            use std::path::PathBuf;
+
+            let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
+
+            // Put the linker script somewhere the linker can find it.
+            fs::write(out_dir.join("memory.x"), include_bytes!("memory.x")).unwrap();
+            println!("cargo:rustc-link-search={}", out_dir.display());
+
+            println!("cargo:rerun-if-changed=memory.x");
+            println!("cargo:rustc-link-arg=-Tmemory.x");
+
+            println!("cargo:rustc-link-arg=-Tlink.x");
+            println!("cargo:rerun-if-changed=build.rs");
+        }
+    }
+}
diff --git a/fmc/test-fw/test-rt/memory.x b/fmc/test-fw/test-rt/memory.x
new file mode 100644
index 00000000..62fd739b
--- /dev/null
+++ b/fmc/test-fw/test-rt/memory.x
@@ -0,0 +1,45 @@
+
+
+ROM_ORG   = 0x00000000;
+META_ORG  = ROM_SIZE;
+ICCM_ORG  = 0x40002000;
+DCCM_ORG  = 0x50000000;
+FHT_ORG   = 0x50002000;
+DATA_ORG  = 0x50003000;
+STACK_ORG = 0x5001C000;
+ESTACK_ORG  = 0x5001F800;
+NSTACK_ORG  = 0x5001FC00;
+
+
+ROM_SIZE    = 32K;
+MBOX_SIZE   = 128K;
+ICCM_SIZE   = 128K;
+DCCM_SIZE   = 128K;
+MAN1_SIZE   = 4K;
+MAN2_SIZE   = 4K;
+FHT_SIZE    = 4K;
+DATA_SIZE   = 100K;
+STACK_SIZE  = 14K;
+ESTACK_SIZE = 1K;
+NSTACK_SIZE = 1K;
+
+
+MEMORY
+{
+	ROM  (rx) : ORIGIN = ROM_ORG,  LENGTH = ROM_SIZE
+	META (r)  : ORIGIN = META_ORG, LENGTH = META_SIZE
+	ICCM (rx) : ORIGIN = ICCM_ORG, LENGTH = ICCM_SIZE
+	FHT  (rw) : ORIGIN = FHT_ORG,  LENGTH = FHT_SIZE
+	DATA (rw) : ORIGIN = DATA_ORG, LENGTH = DATA_SIZE
+	STACK(rw) : ORIGIN = STACK_ORG,  LENGTH = STACK_SIZE
+	ESTACK (rw) : ORIGIN = ESTACK_ORG, LENGTH = ESTACK_SIZE
+	NSTACK (rw) : ORIGIN = NSTACK_ORG, LENGTH = NSTACK_SIZE
+} 
+/* https://sourceware.org/binutils/docs/ld/REGION_005fALIAS.html#REGION_005fALIAS */
+REGION_ALIAS("REGION_TEXT", ICCM);     
+REGION_ALIAS("REGION_RODATA", ICCM);  
+REGION_ALIAS("REGION_DATA", DATA);     
+REGION_ALIAS("REGION_BSS", DATA);      
+REGION_ALIAS("REGION_STACK", STACK);     
+REGION_ALIAS("REGION_ESTACK", ESTACK);     
+REGION_ALIAS("REGION_NSTACK", NSTACK);     
diff --git a/fmc/test-fw/test-rt/src/main.rs b/fmc/test-fw/test-rt/src/main.rs
new file mode 100644
index 00000000..15ec52d6
--- /dev/null
+++ b/fmc/test-fw/test-rt/src/main.rs
@@ -0,0 +1,110 @@
+/*++
+
+Licensed under the Apache-2.0 license.
+
+File Name:
+
+    main.rs
+
+Abstract:
+
+    File contains main entry point for Caliptra Test Runtime
+
+--*/
+#![cfg_attr(not(feature = "std"), no_std)]
+#![cfg_attr(not(feature = "std"), no_main)]
+
+use caliptra_common::cprintln;
+use caliptra_cpu::TrapRecord;
+use caliptra_drivers::report_fw_error_non_fatal;
+use core::hint::black_box;
+
+#[cfg(feature = "std")]
+pub fn main() {}
+
+const BANNER: &str = r#"
+   _____                 __     __________   __   
+  /     \   ____   ____ |  | __ \______   \_/  |_ 
+ /  \ /  \ /  _ \_/ ___\|  |/ /  |       _/\   __\
+/    Y    (  <_> )  \___|    <   |    |   \ |  |  
+\____|__  /\____/ \___  >__|_ \  |____|_  / |__|  
+        \/            \/     \/         \/       
+"#;
+
+#[no_mangle]
+pub extern "C" fn entry_point() -> ! {
+    cprintln!("{}", BANNER);
+
+    if let Some(fht) = caliptra_common::FirmwareHandoffTable::try_load() {
+        cprintln!("[rt] FHT Marker: 0x{:08X}", fht.fht_marker);
+        cprintln!("[rt] FHT Major Version: 0x{:04X}", fht.fht_major_ver);
+        cprintln!("[rt] FHT Minor Version: 0x{:04X}", fht.fht_minor_ver);
+        cprintln!("[rt] FHT Manifest Addr: 0x{:08X}", fht.manifest_load_addr);
+        cprintln!("[rt] FHT FMC CDI KV KeyID: {}", fht.fmc_cdi_kv_idx);
+        cprintln!("[rt] FHT FMC PrivKey KV KeyID: {}", fht.fmc_priv_key_kv_idx);
+        cprintln!(
+            "[rt] FHT RT Load Address: 0x{:08x}",
+            fht.rt_fw_load_addr_idx
+        );
+        cprintln!("[rt] FHT RT Entry Point: 0x{:08x}", fht.rt_fw_load_addr_idx);
+        caliptra_drivers::ExitCtrl::exit(0)
+    } else {
+        caliptra_drivers::ExitCtrl::exit(0xff)
+    }
+}
+
+#[no_mangle]
+#[inline(never)]
+#[allow(clippy::empty_loop)]
+extern "C" fn exception_handler(trap_record: &TrapRecord) {
+    cprintln!(
+        "FMC EXCEPTION mcause=0x{:08X} mscause=0x{:08X} mepc=0x{:08X}",
+        trap_record.mcause,
+        trap_record.mscause,
+        trap_record.mepc
+    );
+
+    // Signal non-fatal error to SOC
+    report_error(0xdead);
+}
+
+#[no_mangle]
+#[inline(never)]
+#[allow(clippy::empty_loop)]
+extern "C" fn nmi_handler(trap_record: &TrapRecord) {
+    cprintln!(
+        "FMC NMI mcause=0x{:08X} mscause=0x{:08X} mepc=0x{:08X}",
+        trap_record.mcause,
+        trap_record.mscause,
+        trap_record.mepc
+    );
+
+    report_error(0xdead);
+}
+
+#[panic_handler]
+#[inline(never)]
+#[cfg(not(feature = "std"))]
+#[allow(clippy::empty_loop)]
+fn fmc_panic(_: &core::panic::PanicInfo) -> ! {
+    cprintln!("RT Panic!!");
+    panic_is_possible();
+
+    // TODO: Signal non-fatal error to SOC
+    report_error(0xdead);
+}
+
+#[allow(clippy::empty_loop)]
+fn report_error(code: u32) -> ! {
+    cprintln!("RT Error: 0x{:08X}", code);
+    report_fw_error_non_fatal(code);
+    loop {}
+}
+
+#[no_mangle]
+#[inline(never)]
+fn panic_is_possible() {
+    black_box(());
+    // The existence of this symbol is used to inform test_panic_missing
+    // that panics are possible. Do not remove or rename this symbol.
+}
diff --git a/fmc/test-fw/test-rt/tests/test_panic_missing.rs b/fmc/test-fw/test-rt/tests/test_panic_missing.rs
new file mode 100644
index 00000000..e378c341
--- /dev/null
+++ b/fmc/test-fw/test-rt/tests/test_panic_missing.rs
@@ -0,0 +1,14 @@
+// Licensed under the Apache-2.0 license
+use caliptra_builder::APP_WITH_UART;
+
+#[test]
+fn test_panic_missing() {
+    let rt_elf = caliptra_builder::build_firmware_elf(&APP_WITH_UART).unwrap();
+    let symbols = caliptra_builder::elf_symbols(&rt_elf).unwrap();
+    if symbols.iter().any(|s| s.name.contains("panic_is_possible")) {
+        panic!(
+            "The caliptra RT contains the panic_is_possible symbol, which is not allowed. \
+                Please remove any code that might panic."
+        )
+    }
+}
diff --git a/fmc/tests/test_hand_off.rs b/fmc/tests/test_hand_off.rs
new file mode 100644
index 00000000..a627eb7b
--- /dev/null
+++ b/fmc/tests/test_hand_off.rs
@@ -0,0 +1,30 @@
+// Licensed under the Apache-2.0 license
+use caliptra_builder::{FwId, ImageOptions, FMC_WITH_UART, ROM_WITH_UART};
+use caliptra_hw_model::{HwModel, InitParams};
+
+#[test]
+fn test_hand_off() {
+    let rom = caliptra_builder::build_firmware_rom(&ROM_WITH_UART).unwrap();
+
+    pub const MOCK_RT_WITH_UART: FwId = FwId {
+        crate_name: "caliptra-fmc-mock-rt",
+        bin_name: "caliptra-fmc-mock-rt",
+        features: &["emu"],
+    };
+
+    let image = caliptra_builder::build_and_sign_image(
+        &FMC_WITH_UART,
+        &MOCK_RT_WITH_UART,
+        ImageOptions::default(),
+    )
+    .unwrap();
+
+    let mut hw = caliptra_hw_model::create(InitParams {
+        rom: &rom,
+        ..Default::default()
+    })
+    .unwrap();
+    hw.upload_firmware(&image).unwrap();
+    let mut output = vec![];
+    hw.copy_output_until_exit_success(&mut output).unwrap();
+}