From d466dbf0ae63f3adb49ee27a59a6e29da19a07a3 Mon Sep 17 00:00:00 2001 From: Rajib Dutta Date: Sat, 27 Jun 2026 01:33:09 +0000 Subject: [PATCH 1/7] Add CPU fault/panic exception handlers to Uno firmware Introduce `azihsm_fw_uno_fault`, a no_std crate that installs the firmware's single `#[panic_handler]` plus overrides for the ARMv7-M `HardFault` and `DefaultHandler` exceptions. Previously the default cortex-m-rt HardFault handler was a silent infinite loop, so a bus fault (e.g. a stray write to a read-only peripheral STATUS register) escalated to HardFault and hung the core with no output. The new HardFault handler reads the SCB fault-status registers and dumps the decoded CFSR fault class, the faulting address (BFAR/MMFAR when valid), the stacked register frame, and MSP, with a dedicated stack-overflow path (HFSR.FORCED + CFSR.MSTKERR). Output goes through a generic fault sink (the SoC UART today, a blocking busy-poll MMIO path safe to call from fault context) that is compiled unconditionally, so diagnostics appear even in production builds shipped without a trace backend. The sink is named generically so it can later route to the HSP debug-log channel without touching call sites. - crates/fault: lib.rs (handlers), sink.rs (FaultWriter + print_fault!), decode.rs (CFSR bit decoding) - Features: `semihosting` (SYS_EXIT on fault for the emulator), `fault-stackdump` (dev-only raw stack hexdump; default dump is the mcr-hsm-style minimalist register report) - app: depend on the crate, force-link via `use ... as _;`, move the panic handler out of main.rs, route `semihosting` through the crate Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/Cargo.toml | 2 + fw/plat/uno/fw/app/Cargo.toml | 8 +- fw/plat/uno/fw/app/src/main.rs | 30 +--- fw/plat/uno/fw/crates/fault/Cargo.toml | 33 +++++ fw/plat/uno/fw/crates/fault/src/decode.rs | 80 ++++++++++ fw/plat/uno/fw/crates/fault/src/lib.rs | 169 ++++++++++++++++++++++ fw/plat/uno/fw/crates/fault/src/sink.rs | 58 ++++++++ 7 files changed, 349 insertions(+), 31 deletions(-) create mode 100644 fw/plat/uno/fw/crates/fault/Cargo.toml create mode 100644 fw/plat/uno/fw/crates/fault/src/decode.rs create mode 100644 fw/plat/uno/fw/crates/fault/src/lib.rs create mode 100644 fw/plat/uno/fw/crates/fault/src/sink.rs diff --git a/fw/plat/uno/fw/Cargo.toml b/fw/plat/uno/fw/Cargo.toml index 1f3a4be97..1780bdb69 100644 --- a/fw/plat/uno/fw/Cargo.toml +++ b/fw/plat/uno/fw/Cargo.toml @@ -6,6 +6,7 @@ members = [ "app", "crates/bulk_copy", "crates/error", + "crates/fault", "crates/key_vault", "crates/single_cell", "crates/static_init", @@ -64,6 +65,7 @@ azihsm_fw_uno_drivers_uart = { path = "drivers/uart" } azihsm_fw_uno_drivers_upka = { path = "drivers/upka" } azihsm_fw_uno_drivers_vault = { path = "drivers/vault" } azihsm_fw_uno_error = { path = "crates/error" } +azihsm_fw_uno_fault = { path = "crates/fault" } azihsm_fw_uno_key_vault = { path = "crates/key_vault" } azihsm_fw_uno_pac = { path = "drivers/pac" } azihsm_fw_uno_pal = { path = "pal" } diff --git a/fw/plat/uno/fw/app/Cargo.toml b/fw/plat/uno/fw/app/Cargo.toml index 89b0437c7..ec677cd0a 100644 --- a/fw/plat/uno/fw/app/Cargo.toml +++ b/fw/plat/uno/fw/app/Cargo.toml @@ -16,8 +16,8 @@ azihsm_fw_hsm_core = { workspace = true } azihsm_fw_hsm_core_tracing = { workspace = true } azihsm_fw_hsm_pal_traits = { workspace = true } azihsm_fw_uno_drivers_profile = { workspace = true } -azihsm_fw_uno_drivers_semihosting = { optional = true, workspace = true } azihsm_fw_uno_drivers_systick = { workspace = true } +azihsm_fw_uno_fault = { workspace = true } azihsm_fw_uno_pac = { workspace = true } azihsm_fw_uno_pal = { workspace = true } azihsm_fw_uno_trace = { workspace = true } @@ -44,10 +44,14 @@ default = [] # `SYS_EXIT(-1)` so the emulator stops on firmware panic. # Disable on targets (e.g. production silicon) that cannot snoop semihosting. semihosting = [ + "azihsm_fw_uno_fault/semihosting", "azihsm_fw_uno_pal/semihosting", - "dep:azihsm_fw_uno_drivers_semihosting", ] +# Development aid: include a raw stack hexdump in the HardFault report. +# Passed through to the fault crate; off by default. +fault-stackdump = ["azihsm_fw_uno_fault/fault-stackdump"] + # Trace output backend (mutually exclusive). Selecting one compiles the # trace emitter at INFO level and routes output to that backend. With # neither enabled, trace output is disabled. diff --git a/fw/plat/uno/fw/app/src/main.rs b/fw/plat/uno/fw/app/src/main.rs index 0dd043812..adf90c8df 100644 --- a/fw/plat/uno/fw/app/src/main.rs +++ b/fw/plat/uno/fw/app/src/main.rs @@ -41,10 +41,10 @@ mod trampoline; use azihsm_fw_hsm_core::Hsm; -use azihsm_fw_hsm_core_tracing::error; use azihsm_fw_hsm_core_tracing::info; use azihsm_fw_hsm_pal_traits::*; use azihsm_fw_uno_drivers_profile as _; +use azihsm_fw_uno_fault as _; use azihsm_fw_uno_pac as _; use azihsm_fw_uno_pal::BootPhase; use azihsm_fw_uno_pal::UnoHsmIo; @@ -192,31 +192,3 @@ async fn main(spawner: Spawner) { hsm.pal().run().await; hsm.pal().deinit(); } - -/// Panic handler — emits an error trace and halts. -/// -/// # Parameters -/// - `info`: Panic metadata (location and message) provided by core. -/// -/// # Returns -/// Never returns (`!`). -/// -/// # Side Effects -/// - Emits an error-level trace describing the panic to the configured -/// trace backend for diagnostic visibility. -/// - With the `semihosting` feature enabled (emulator), terminates via -/// `SYS_EXIT(-1)` so the host stops on a firmware panic. Otherwise -/// (e.g. silicon), halts forward progress by looping forever. -#[panic_handler] -fn panic(info: &core::panic::PanicInfo) -> ! { - // Referenced unconditionally: `error!` compiles out without a trace - // backend (the default firmware build), which would otherwise leave - // `info` unused. - let _ = &info; - error!("panic", HsmError::InternalError, "{}", info); - - #[cfg(feature = "semihosting")] - azihsm_fw_uno_drivers_semihosting::sys_exit(-1i32 as u32); - - loop {} -} diff --git a/fw/plat/uno/fw/crates/fault/Cargo.toml b/fw/plat/uno/fw/crates/fault/Cargo.toml new file mode 100644 index 000000000..108d71ec8 --- /dev/null +++ b/fw/plat/uno/fw/crates/fault/Cargo.toml @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +[package] +edition = "2021" +name = "azihsm_fw_uno_fault" +version = "0.1.0" + +[lib] +bench = false +doctest = false +test = false + +[features] +default = [] + +# Emulator builds: after reporting a panic/fault, terminate the firmware +# via semihosting `SYS_EXIT(-1)` so the emulator host stops instead of +# spinning. On silicon (no semihosting host) the handlers halt in a loop. +semihosting = ["dep:azihsm_fw_uno_drivers_semihosting"] + +# Development aid: append a raw stack-memory hexdump (several words from +# the captured SP) to the HardFault report. Off by default to keep the +# fault path minimal — the default dump mirrors the mcr-hsm minimalist +# register/CFSR report. +fault-stackdump = [] + +[dependencies] +azihsm_fw_uno_drivers_semihosting = { optional = true, workspace = true } +azihsm_fw_uno_drivers_uart.workspace = true +azihsm_fw_uno_reg_cortex_m.workspace = true +cortex-m.workspace = true +cortex-m-rt.workspace = true +tock-registers.workspace = true diff --git a/fw/plat/uno/fw/crates/fault/src/decode.rs b/fw/plat/uno/fw/crates/fault/src/decode.rs new file mode 100644 index 000000000..0a12f5acb --- /dev/null +++ b/fw/plat/uno/fw/crates/fault/src/decode.rs @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Configurable Fault Status Register (CFSR) decoding. +//! +//! Translates the bit fields of the ARMv7-M `CFSR` (MMFSR + BFSR + UFSR) +//! into human-readable lines so a HardFault report names the precise +//! fault cause instead of just a raw hex value. The bus/mem-fault address +//! registers (`BFAR`/`MMFAR`) are reported by the caller when the matching +//! `*VALID` bit is set. + +use azihsm_fw_uno_reg_cortex_m::scb::regs::ScbRegs; +use azihsm_fw_uno_reg_cortex_m::scb::CFSR; +use tock_registers::interfaces::Readable; + +use crate::println_fault; + +/// Print one decoded line per set CFSR fault bit, grouped by fault unit. +/// +/// Covers the MemManage (`MMFSR`), BusFault (`BFSR`) and UsageFault +/// (`UFSR`) sub-registers. Only set bits are printed; a clean register +/// produces no output. +pub fn report_cfsr(scb: &ScbRegs) { + // MemManage faults (MMFSR, CFSR[7:0]). + if scb.cfsr.is_set(CFSR::IACCVIOL) { + println_fault!(" MemManage: IACCVIOL instruction-fetch access violation"); + } + if scb.cfsr.is_set(CFSR::DACCVIOL) { + println_fault!(" MemManage: DACCVIOL data access violation"); + } + if scb.cfsr.is_set(CFSR::MUNSTKERR) { + println_fault!(" MemManage: MUNSTKERR fault on exception return unstacking"); + } + if scb.cfsr.is_set(CFSR::MSTKERR) { + println_fault!(" MemManage: MSTKERR fault on exception entry stacking"); + } + if scb.cfsr.is_set(CFSR::MLSPERR) { + println_fault!(" MemManage: MLSPERR fault during FP lazy state preservation"); + } + + // Bus faults (BFSR, CFSR[15:8]). + if scb.cfsr.is_set(CFSR::IBUSERR) { + println_fault!(" BusFault: IBUSERR instruction prefetch bus error"); + } + if scb.cfsr.is_set(CFSR::PRECISERR) { + println_fault!(" BusFault: PRECISERR precise data bus error (BFAR valid)"); + } + if scb.cfsr.is_set(CFSR::IMPRECISERR) { + println_fault!(" BusFault: IMPRECISERR imprecise data bus error (BFAR unreliable)"); + } + if scb.cfsr.is_set(CFSR::UNSTKERR) { + println_fault!(" BusFault: UNSTKERR bus fault on exception return unstacking"); + } + if scb.cfsr.is_set(CFSR::STKERR) { + println_fault!(" BusFault: STKERR bus fault on exception entry stacking"); + } + if scb.cfsr.is_set(CFSR::LSPERR) { + println_fault!(" BusFault: LSPERR bus fault during FP lazy state preservation"); + } + + // Usage faults (UFSR, CFSR[31:16]). + if scb.cfsr.is_set(CFSR::UNDEFINSTR) { + println_fault!(" UsageFault: UNDEFINSTR undefined instruction"); + } + if scb.cfsr.is_set(CFSR::INVSTATE) { + println_fault!(" UsageFault: INVSTATE invalid EPSR/Thumb state"); + } + if scb.cfsr.is_set(CFSR::INVPC) { + println_fault!(" UsageFault: INVPC invalid PC load (exception return)"); + } + if scb.cfsr.is_set(CFSR::NOCP) { + println_fault!(" UsageFault: NOCP coprocessor access denied/absent"); + } + if scb.cfsr.is_set(CFSR::UNALIGNED) { + println_fault!(" UsageFault: UNALIGNED unaligned access trap"); + } + if scb.cfsr.is_set(CFSR::DIVBYZERO) { + println_fault!(" UsageFault: DIVBYZERO divide-by-zero trap"); + } +} diff --git a/fw/plat/uno/fw/crates/fault/src/lib.rs b/fw/plat/uno/fw/crates/fault/src/lib.rs new file mode 100644 index 000000000..0a9eddd20 --- /dev/null +++ b/fw/plat/uno/fw/crates/fault/src/lib.rs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Panic and CPU-exception handlers for the Uno HSM firmware. +//! +//! Installs the firmware's single `#[panic_handler]` plus overrides for the +//! ARMv7-M `HardFault` and `DefaultHandler` exceptions. The goal is fault +//! *visibility*: the default `cortex-m-rt` HardFault handler is a silent +//! infinite loop, so a bus fault (for example, a stray write to a read-only +//! peripheral register) escalates to HardFault and hangs the core with no +//! output. These handlers instead dump the fault cause — decoded `CFSR` +//! bits, the faulting address (`BFAR`/`MMFAR`), the stacked register frame, +//! and `MSP` — so a future fault is diagnosable from the serial log alone. +//! +//! Output goes through [`sink::FaultWriter`] (the SoC UART today), which is +//! compiled unconditionally and therefore works even in production builds +//! that ship without a trace backend. +//! +//! # Linking +//! +//! The panic and exception symbols only take effect if this crate is part +//! of the final binary's dependency graph. The application forces that with +//! `use azihsm_fw_uno_fault as _;` (the `panic-halt` pattern). +//! +//! # Scope +//! +//! This is deliberately a CPU-fault *reporter*. Cross-core crash +//! notification, persistent crash dumps, and peripheral-error ISRs (present +//! in the mcr-hsm `exception-handlers` crate) depend on infrastructure the +//! Uno port does not yet have (Tcon mailbox, crashdump store) and are out of +//! scope here. + +#![no_std] +#![allow(unsafe_code)] + +mod decode; +mod sink; + +use azihsm_fw_uno_reg_cortex_m::scb::regs::ScbRegs; +use azihsm_fw_uno_reg_cortex_m::scb::CFSR; +use azihsm_fw_uno_reg_cortex_m::scb::HFSR; +use azihsm_fw_uno_reg_cortex_m::scb::SCB_BASE; +use cortex_m_rt::exception; +use cortex_m_rt::ExceptionFrame; +use tock_registers::interfaces::Readable; + +pub use sink::FaultWriter; + +/// Borrow the System Control Block MMIO register block. +/// +/// # Safety +/// +/// `SCB_BASE` is a fixed architectural address that is always mapped on +/// ARMv7-M, so the dereference is sound. Called only from fault context +/// where the firmware is already halting, so aliasing is not a concern. +#[inline(always)] +fn scb() -> &'static ScbRegs { + unsafe { &*(SCB_BASE as *const ScbRegs) } +} + +/// Terminate the firmware after a fault has been reported. +/// +/// On emulator builds (`semihosting`) this issues `SYS_EXIT(-1)` so the +/// host stops; on silicon it spins forever (the core is already wedged). +fn halt() -> ! { + #[cfg(feature = "semihosting")] + azihsm_fw_uno_drivers_semihosting::sys_exit(-1i32 as u32); + + loop { + cortex_m::asm::nop(); + } +} + +/// Firmware panic handler. +/// +/// Emits the panic location and message (via [`core::panic::PanicInfo`]'s +/// `Display`, which already includes `file:line:col` plus the formatted +/// message) to the fault sink, then halts. +#[panic_handler] +fn panic(info: &core::panic::PanicInfo<'_>) -> ! { + println_fault!(""); + println_fault!("#### PANIC ####"); + println_fault!("{}", info); + halt(); +} + +/// HardFault exception handler. +/// +/// Reads the SCB fault-status registers, classifies the fault, and dumps: +/// decoded `CFSR` bits, the faulting address when valid, the stacked +/// [`ExceptionFrame`] (R0-R3, R12, LR, PC, xPSR), and `MSP`. A stack +/// overflow (`HFSR.FORCED` + `CFSR.MSTKERR`) is reported specially because +/// exception stacking failed and the frame is unreliable. +#[exception] +unsafe fn HardFault(ef: &ExceptionFrame) -> ! { + let scb = scb(); + let cfsr = scb.cfsr.get(); + let hfsr = scb.hfsr.get(); + let msp = cortex_m::register::msp::read(); + + let forced = scb.hfsr.is_set(HFSR::FORCED); + let mstkerr = scb.cfsr.is_set(CFSR::MSTKERR); + + println_fault!(""); + println_fault!("#### HardFault ####"); + + if forced && mstkerr { + // MemManage stacking failed and escalated to HardFault: the CPU + // could not push {R0-R3,R12,LR,PC,xPSR}, so `ef` is garbage. The + // faulting PC/LR are unrecoverable on ARMv7-M; only MSP locates + // where the stack was when it overran its guard region. + println_fault!("cause: stack overflow (exception frame unreliable)"); + println_fault!("MSP={:#010x} CFSR={:#010x} HFSR={:#010x}", msp, cfsr, hfsr); + } else { + decode::report_cfsr(scb); + + if scb.cfsr.is_set(CFSR::BFARVALID) { + println_fault!("BFAR={:#010x} (faulting bus address)", scb.bfar.get()); + } + if scb.cfsr.is_set(CFSR::MMARVALID) { + println_fault!("MMFAR={:#010x} (faulting memory address)", scb.mmfar.get()); + } + + println_fault!("CFSR={:#010x} HFSR={:#010x} MSP={:#010x}", cfsr, hfsr, msp); + println_fault!("frame: {:#?}", ef); + + #[cfg(feature = "fault-stackdump")] + unsafe { + stack_dump(msp); + } + } + + halt(); +} + +/// Catch-all handler for any exception/interrupt without a dedicated +/// handler. Reports the offending exception number so an unexpected or +/// spurious interrupt is no longer silent, then halts. +#[exception] +unsafe fn DefaultHandler(irqn: i16) -> ! { + println_fault!(""); + println_fault!("#### Unexpected exception/IRQ: {} ####", irqn); + halt(); +} + +/// Dump `WORDS` 32-bit words of raw stack memory starting at `sp`. +/// +/// Development aid behind the `fault-stackdump` feature — useful for +/// eyeballing return addresses and locals near the fault, at the cost of +/// reading memory that may extend past the live stack. +/// +/// # Safety +/// +/// Performs volatile reads of arbitrary stack addresses; the range may run +/// past valid RAM. Intended for debug builds on hardware only. +#[cfg(feature = "fault-stackdump")] +unsafe fn stack_dump(sp: u32) { + const WORDS: usize = 32; + println_fault!("stack dump @ {:#010x}:", sp); + for i in 0..WORDS { + let addr = sp.wrapping_add((i * 4) as u32); + let val = unsafe { core::ptr::read_volatile(addr as *const u32) }; + if i % 4 == 0 { + print_fault!("\n {:#010x}:", addr); + } + print_fault!(" {:08x}", val); + } + println_fault!(""); +} diff --git a/fw/plat/uno/fw/crates/fault/src/sink.rs b/fw/plat/uno/fw/crates/fault/src/sink.rs new file mode 100644 index 000000000..fe2bb9563 --- /dev/null +++ b/fw/plat/uno/fw/crates/fault/src/sink.rs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Generic fault-time output sink. +//! +//! Emits diagnostic text from the panic and exception handlers. The sink +//! is intentionally named generically rather than after a transport: today +//! it writes to the SoC UART (a blocking, busy-poll MMIO path with no +//! interrupts, locks, or heap allocation — safe to call from a fault +//! context), but when the HSP debug-log channel is brought up this module +//! can route fault output there instead without touching any call site. +//! +//! Unlike the `error!`/`info!` trace macros — which compile to no-ops +//! unless a trace backend feature is enabled — this sink is **always** +//! compiled, so fault diagnostics appear even in production builds that +//! ship with tracing disabled. + +use core::fmt::{self, Write}; + +use azihsm_fw_uno_drivers_uart::Uart; + +/// Fault-time writer over the current diagnostic sink (UART today). +/// +/// Implements [`core::fmt::Write`] so the `print_fault!` / `println_fault!` +/// macros can format directly into it without any intermediate buffer. +pub struct FaultWriter; + +impl Write for FaultWriter { + fn write_str(&mut self, s: &str) -> fmt::Result { + let mut uart = Uart::new(); + uart.write_bytes(s.as_bytes()); + Ok(()) + } +} + +/// Emit formatted fault diagnostics (no trailing newline). +#[macro_export] +macro_rules! print_fault { + ($($arg:tt)*) => {{ + let _ = ::core::fmt::Write::write_fmt( + &mut $crate::sink::FaultWriter, + ::core::format_args!($($arg)*), + ); + }}; +} + +/// Emit formatted fault diagnostics followed by a newline. +#[macro_export] +macro_rules! println_fault { + () => {{ $crate::print_fault!("\n"); }}; + ($($arg:tt)*) => {{ + let _ = ::core::fmt::Write::write_fmt( + &mut $crate::sink::FaultWriter, + ::core::format_args!($($arg)*), + ); + $crate::print_fault!("\n"); + }}; +} From c807aaa986419e117d714fd4bce7bc54728fd669 Mon Sep 17 00:00:00 2001 From: Rajib Dutta Date: Sat, 27 Jun 2026 19:22:09 +0000 Subject: [PATCH 2/7] fault: address HardFault handler review comments - Treat a BusFault stacking error (CFSR.STKERR) as well as MSTKERR as an "exception frame unreliable" condition: if exception-entry stacking fails with a BusFault that escalates to HardFault, the pushed register frame is invalid and formatting it could fault again. - Align the stack_dump base address down to a 4-byte boundary before forming the *const u32, avoiding UB when sp is corrupted/misaligned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/crates/fault/src/lib.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/fw/plat/uno/fw/crates/fault/src/lib.rs b/fw/plat/uno/fw/crates/fault/src/lib.rs index 0a9eddd20..36b779a1b 100644 --- a/fw/plat/uno/fw/crates/fault/src/lib.rs +++ b/fw/plat/uno/fw/crates/fault/src/lib.rs @@ -99,14 +99,19 @@ unsafe fn HardFault(ef: &ExceptionFrame) -> ! { let msp = cortex_m::register::msp::read(); let forced = scb.hfsr.is_set(HFSR::FORCED); + // Exception-entry stacking can fail via either a MemManage fault + // (MSTKERR, e.g. an MPU stack-guard hit on overflow) or a BusFault + // (STKERR); both escalate to HardFault and leave the pushed register + // frame unreliable. let mstkerr = scb.cfsr.is_set(CFSR::MSTKERR); + let stkerr = scb.cfsr.is_set(CFSR::STKERR); println_fault!(""); println_fault!("#### HardFault ####"); - if forced && mstkerr { - // MemManage stacking failed and escalated to HardFault: the CPU - // could not push {R0-R3,R12,LR,PC,xPSR}, so `ef` is garbage. The + if forced && (mstkerr || stkerr) { + // Exception-entry stacking failed and escalated to HardFault: the + // CPU could not push {R0-R3,R12,LR,PC,xPSR}, so `ef` is garbage. The // faulting PC/LR are unrecoverable on ARMv7-M; only MSP locates // where the stack was when it overran its guard region. println_fault!("cause: stack overflow (exception frame unreliable)"); @@ -156,9 +161,13 @@ unsafe fn DefaultHandler(irqn: i16) -> ! { #[cfg(feature = "fault-stackdump")] unsafe fn stack_dump(sp: u32) { const WORDS: usize = 32; - println_fault!("stack dump @ {:#010x}:", sp); + // `read_volatile::` requires a 4-byte-aligned pointer; `sp` may be + // corrupted or misaligned at the fault, so align the base down first to + // avoid undefined behaviour while keeping the dump word-oriented. + let base = sp & !0b11; + println_fault!("stack dump @ {:#010x}:", base); for i in 0..WORDS { - let addr = sp.wrapping_add((i * 4) as u32); + let addr = base.wrapping_add((i * 4) as u32); let val = unsafe { core::ptr::read_volatile(addr as *const u32) }; if i % 4 == 0 { print_fault!("\n {:#010x}:", addr); From bb3eec5f2f108b2fcba6b3b06a5c5cc3bc772d27 Mon Sep 17 00:00:00 2001 From: Rajib Dutta Date: Sat, 27 Jun 2026 19:59:06 +0000 Subject: [PATCH 3/7] fault: apply cargo fmt Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/crates/fault/src/lib.rs | 3 +-- fw/plat/uno/fw/crates/fault/src/sink.rs | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/fw/plat/uno/fw/crates/fault/src/lib.rs b/fw/plat/uno/fw/crates/fault/src/lib.rs index 36b779a1b..b699e1d29 100644 --- a/fw/plat/uno/fw/crates/fault/src/lib.rs +++ b/fw/plat/uno/fw/crates/fault/src/lib.rs @@ -42,9 +42,8 @@ use azihsm_fw_uno_reg_cortex_m::scb::HFSR; use azihsm_fw_uno_reg_cortex_m::scb::SCB_BASE; use cortex_m_rt::exception; use cortex_m_rt::ExceptionFrame; -use tock_registers::interfaces::Readable; - pub use sink::FaultWriter; +use tock_registers::interfaces::Readable; /// Borrow the System Control Block MMIO register block. /// diff --git a/fw/plat/uno/fw/crates/fault/src/sink.rs b/fw/plat/uno/fw/crates/fault/src/sink.rs index fe2bb9563..023f02023 100644 --- a/fw/plat/uno/fw/crates/fault/src/sink.rs +++ b/fw/plat/uno/fw/crates/fault/src/sink.rs @@ -15,7 +15,8 @@ //! compiled, so fault diagnostics appear even in production builds that //! ship with tracing disabled. -use core::fmt::{self, Write}; +use core::fmt::Write; +use core::fmt::{self}; use azihsm_fw_uno_drivers_uart::Uart; From d95452e7eda3bff59b468228b6dc867020cb49d0 Mon Sep 17 00:00:00 2001 From: Rajib Dutta Date: Sat, 27 Jun 2026 20:14:55 +0000 Subject: [PATCH 4/7] fault: route output via semihosting on emulator; clarify exit code - FaultWriter now writes through semihosting SYS_WRITE0 when the `semihosting` feature is enabled, instead of the SoC UART. The emulator does not model the UART, where write_byte would spin forever on STATUS.TX_READY (or fault on unmapped MMIO), stalling the report before SYS_EXIT. Silicon builds keep writing the UART directly. Mirrors the trace backend's backend-uart / backend-semihosting selection. - Pass u32::MAX (instead of `-1i32 as u32`) to sys_exit for clarity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/crates/fault/src/lib.rs | 3 +- fw/plat/uno/fw/crates/fault/src/sink.rs | 40 ++++++++++++++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/fw/plat/uno/fw/crates/fault/src/lib.rs b/fw/plat/uno/fw/crates/fault/src/lib.rs index b699e1d29..df656394a 100644 --- a/fw/plat/uno/fw/crates/fault/src/lib.rs +++ b/fw/plat/uno/fw/crates/fault/src/lib.rs @@ -63,7 +63,8 @@ fn scb() -> &'static ScbRegs { /// host stops; on silicon it spins forever (the core is already wedged). fn halt() -> ! { #[cfg(feature = "semihosting")] - azihsm_fw_uno_drivers_semihosting::sys_exit(-1i32 as u32); + // Semihosting SYS_EXIT status -1 (failure). + azihsm_fw_uno_drivers_semihosting::sys_exit(u32::MAX); loop { cortex_m::asm::nop(); diff --git a/fw/plat/uno/fw/crates/fault/src/sink.rs b/fw/plat/uno/fw/crates/fault/src/sink.rs index 023f02023..c822b8510 100644 --- a/fw/plat/uno/fw/crates/fault/src/sink.rs +++ b/fw/plat/uno/fw/crates/fault/src/sink.rs @@ -4,11 +4,13 @@ //! Generic fault-time output sink. //! //! Emits diagnostic text from the panic and exception handlers. The sink -//! is intentionally named generically rather than after a transport: today -//! it writes to the SoC UART (a blocking, busy-poll MMIO path with no -//! interrupts, locks, or heap allocation — safe to call from a fault -//! context), but when the HSP debug-log channel is brought up this module -//! can route fault output there instead without touching any call site. +//! is intentionally named generically rather than after a transport. On +//! silicon it writes to the SoC UART (a blocking, busy-poll MMIO path with +//! no interrupts, locks, or heap allocation — safe to call from a fault +//! context); on the emulator (`semihosting` builds, which do not model the +//! UART) it routes through semihosting `SYS_WRITE0`. When the HSP debug-log +//! channel is brought up this module can route there instead without +//! touching any call site. //! //! Unlike the `error!`/`info!` trace macros — which compile to no-ops //! unless a trace backend feature is enabled — this sink is **always** @@ -18,9 +20,11 @@ use core::fmt::Write; use core::fmt::{self}; +#[cfg(not(feature = "semihosting"))] use azihsm_fw_uno_drivers_uart::Uart; -/// Fault-time writer over the current diagnostic sink (UART today). +/// Fault-time writer over the current diagnostic sink: the SoC UART on +/// silicon, or semihosting `SYS_WRITE0` on the emulator (`semihosting`). /// /// Implements [`core::fmt::Write`] so the `print_fault!` / `println_fault!` /// macros can format directly into it without any intermediate buffer. @@ -28,8 +32,28 @@ pub struct FaultWriter; impl Write for FaultWriter { fn write_str(&mut self, s: &str) -> fmt::Result { - let mut uart = Uart::new(); - uart.write_bytes(s.as_bytes()); + // The emulator does not model the SoC UART, where `write_byte` would + // spin forever on STATUS.TX_READY (or fault on unmapped MMIO) and + // stall the report before `SYS_EXIT`. So `semihosting` builds route + // through semihosting `SYS_WRITE0`, mirroring the trace backend; + // silicon builds write the UART directly. + #[cfg(feature = "semihosting")] + { + // SYS_WRITE0 needs a NUL-terminated buffer; copy into a small + // stack buffer in chunks (64 bytes + NUL terminator). + for chunk in s.as_bytes().chunks(64) { + let mut buf = [0u8; 65]; + let n = chunk.len(); + buf[..n].copy_from_slice(chunk); + buf[n] = 0; + azihsm_fw_uno_drivers_semihosting::sys_write0(&buf[..=n]); + } + } + #[cfg(not(feature = "semihosting"))] + { + let mut uart = Uart::new(); + uart.write_bytes(s.as_bytes()); + } Ok(()) } } From b7c094435cb25ea9cf00f20cc7c6d06243381be9 Mon Sep 17 00:00:00 2001 From: Rajib Dutta Date: Sun, 28 Jun 2026 01:12:50 +0000 Subject: [PATCH 5/7] fault: add # Safety docs to HardFault/DefaultHandler cortex-m-rt requires these exception handlers to be unsafe fn; satisfy clippy::missing_safety_doc (-D warnings) with a # Safety section. Also note STKERR alongside MSTKERR in the HardFault doc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/crates/fault/src/lib.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/fw/plat/uno/fw/crates/fault/src/lib.rs b/fw/plat/uno/fw/crates/fault/src/lib.rs index df656394a..01bee09fd 100644 --- a/fw/plat/uno/fw/crates/fault/src/lib.rs +++ b/fw/plat/uno/fw/crates/fault/src/lib.rs @@ -89,8 +89,15 @@ fn panic(info: &core::panic::PanicInfo<'_>) -> ! { /// Reads the SCB fault-status registers, classifies the fault, and dumps: /// decoded `CFSR` bits, the faulting address when valid, the stacked /// [`ExceptionFrame`] (R0-R3, R12, LR, PC, xPSR), and `MSP`. A stack -/// overflow (`HFSR.FORCED` + `CFSR.MSTKERR`) is reported specially because -/// exception stacking failed and the frame is unreliable. +/// overflow (`HFSR.FORCED` + `CFSR.MSTKERR`/`STKERR`) is reported specially +/// because exception stacking failed and the frame is unreliable. +/// +/// # Safety +/// +/// Required to be an `unsafe fn` by `cortex-m-rt`. Invoked only by the +/// hardware exception mechanism on a HardFault and must never be called +/// directly; it reads fixed architectural SCB registers and the +/// hardware-supplied exception frame, then halts. #[exception] unsafe fn HardFault(ef: &ExceptionFrame) -> ! { let scb = scb(); @@ -141,6 +148,12 @@ unsafe fn HardFault(ef: &ExceptionFrame) -> ! { /// Catch-all handler for any exception/interrupt without a dedicated /// handler. Reports the offending exception number so an unexpected or /// spurious interrupt is no longer silent, then halts. +/// +/// # Safety +/// +/// Required to be an `unsafe fn` by `cortex-m-rt`. Invoked only by the +/// hardware exception mechanism for an otherwise-unhandled exception/IRQ +/// and must never be called directly. #[exception] unsafe fn DefaultHandler(irqn: i16) -> ! { println_fault!(""); From d23f90c775b2fa33f88fbac173ce0ee858a6ddcc Mon Sep 17 00:00:00 2001 From: Rajib Dutta <105447395+radutta99@users.noreply.github.com> Date: Sat, 27 Jun 2026 18:21:29 -0700 Subject: [PATCH 6/7] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/crates/fault/src/sink.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fw/plat/uno/fw/crates/fault/src/sink.rs b/fw/plat/uno/fw/crates/fault/src/sink.rs index c822b8510..1861d6a96 100644 --- a/fw/plat/uno/fw/crates/fault/src/sink.rs +++ b/fw/plat/uno/fw/crates/fault/src/sink.rs @@ -52,7 +52,7 @@ impl Write for FaultWriter { #[cfg(not(feature = "semihosting"))] { let mut uart = Uart::new(); - uart.write_bytes(s.as_bytes()); + uart.write(s); } Ok(()) } From 4a7c61ad1462f3e6f02731580dfc4e7fe71c16bd Mon Sep 17 00:00:00 2001 From: Rajib Dutta Date: Sun, 28 Jun 2026 01:28:37 +0000 Subject: [PATCH 7/7] fault: filter UART output and fix macro re-export path Use the UART driver write(&str) (filters non-printable bytes to 0xFE) instead of write_bytes() so control chars/embedded NUL do not garble fault logs. Reference the public \::FaultWriter re-export from the exported print_fault!/println_fault! macros instead of the private sink module, so downstream callers do not hit a privacy error. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- fw/plat/uno/fw/crates/fault/src/sink.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fw/plat/uno/fw/crates/fault/src/sink.rs b/fw/plat/uno/fw/crates/fault/src/sink.rs index 1861d6a96..1792a6807 100644 --- a/fw/plat/uno/fw/crates/fault/src/sink.rs +++ b/fw/plat/uno/fw/crates/fault/src/sink.rs @@ -51,6 +51,8 @@ impl Write for FaultWriter { } #[cfg(not(feature = "semihosting"))] { + // `write` filters non-printable bytes (control chars / embedded + // NUL -> 0xFE) so the fault log stays readable on the console. let mut uart = Uart::new(); uart.write(s); } @@ -63,7 +65,7 @@ impl Write for FaultWriter { macro_rules! print_fault { ($($arg:tt)*) => {{ let _ = ::core::fmt::Write::write_fmt( - &mut $crate::sink::FaultWriter, + &mut $crate::FaultWriter, ::core::format_args!($($arg)*), ); }}; @@ -75,7 +77,7 @@ macro_rules! println_fault { () => {{ $crate::print_fault!("\n"); }}; ($($arg:tt)*) => {{ let _ = ::core::fmt::Write::write_fmt( - &mut $crate::sink::FaultWriter, + &mut $crate::FaultWriter, ::core::format_args!($($arg)*), ); $crate::print_fault!("\n");