From f1844b3703c14fc1e3ab9101032ebcfeb208a658 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Mon, 20 Apr 2026 21:55:38 -0700 Subject: [PATCH 1/8] peripheral: add SAU init helpers and jump_to_nonsecure for ARMv8-M MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Derive Copy, Clone, PartialEq, Eq on SauRegion and SauRegionAttribute - SAU::disable_allns(): set CTRL.ALLNS=1, ENABLE=0 (all memory Non-Secure) - SAU::init(regions): disable SAU, program up to 8 regions, re-enable - jump_to_nonsecure(ns_vtor): Secure→Non-Secure boot handoff via BXNS These cover the remaining ARMv8-M TrustZone boot sequence after SAU region programming: disabling the SAU for NS-only systems, bulk-initialising regions without manually looping set_region, and transferring control to the NS image. --- cortex-m/src/peripheral/sau.rs | 97 +++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/cortex-m/src/peripheral/sau.rs b/cortex-m/src/peripheral/sau.rs index da91aca9..a308e3dd 100644 --- a/cortex-m/src/peripheral/sau.rs +++ b/cortex-m/src/peripheral/sau.rs @@ -103,7 +103,7 @@ bitfield! { } /// Possible attribute of a SAU region. -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SauRegionAttribute { /// SAU region is Secure Secure, @@ -114,7 +114,7 @@ pub enum SauRegionAttribute { } /// Description of a SAU region. -#[derive(Debug)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SauRegion { /// First address of the region, its 5 least significant bits must be set to zero. pub base_address: u32, @@ -143,6 +143,48 @@ impl SAU { self._type.read().sregion() } + /// Disable the SAU and mark all memory Non-Secure (ALLNS mode). + /// + /// Sets `CTRL.ALLNS = 1`, `CTRL.ENABLE = 0`. When the SAU is disabled with ALLNS set, the + /// entire address space is treated as Non-Secure (subject to any IDAU overrides). Use this + /// when running entirely in Non-Secure mode with no security boundary enforcement. + /// + /// To re-enable security boundaries, call [`init`] or [`enable`] after programming regions. + #[inline] + pub fn disable_allns(&mut self) { + unsafe { + self.ctrl.write(Ctrl(0b10)); // ALLNS=1, ENABLE=0 + } + } + + /// Program SAU regions and enable the SAU. + /// + /// This is a convenience wrapper around [`set_region`] + [`enable`]: + /// 1. Disables the SAU temporarily. + /// 2. Programs up to 8 regions from `regions` (extras silently ignored). + /// 3. Re-enables the SAU. + /// + /// Memory not covered by any enabled region is treated as Secure once the SAU is enabled. + /// + /// To also enable the `SecureFault` exception so TrustZone violations surface as a dedicated + /// fault rather than escalating to `HardFault`, call + /// `scb.enable(cortex_m::peripheral::scb::Exception::SecureFault)` after this. + /// + /// # Errors + /// Region-programming errors (bad alignment, region number out of range) are silently ignored; + /// `take(8)` already bounds the region count to the hardware maximum. + #[inline] + pub fn init(&mut self, regions: &[SauRegion]) { + // Disable while reprogramming to avoid partial-update windows. + unsafe { + self.ctrl.write(Ctrl(0)); + } + for (i, ®ion) in regions.iter().enumerate().take(8) { + let _ = self.set_region(i as u8, region); + } + self.enable(); + } + /// Enable the SAU. #[inline] pub fn enable(&mut self) { @@ -241,3 +283,54 @@ impl SAU { }) } } + +/// Transfer control to the Non-Secure application. Does not return. +/// +/// This performs the standard Secure→Non-Secure boot handoff: +/// 1. Sets `SCB_NS->VTOR` to `ns_vtor` so the Non-Secure world finds its vector table. +/// 2. Loads `MSP_NS` from the first word of the NS vector table (the initial NS stack pointer). +/// 3. Reads the NS reset handler address from the second word of the NS vector table. +/// 4. Executes `BXNS` to atomically switch to Non-Secure state and jump to the handler. +/// +/// # Safety +/// - Must be called from the Secure world after all SAU/GTZC setup is complete. +/// - `ns_vtor` must point to a valid Non-Secure vector table. The Cortex-M33 requires the VTOR +/// to be at least 32-byte aligned; in practice 128-byte or 256-byte alignment is typical. +/// - The NS reset handler at `*(ns_vtor + 4)` must be a valid Thumb function address (bit 0 set +/// in the vector table entry, as per the ARM ABI convention for vector tables). +/// - Available on ARMv8-M only (`thumbv8m.base` and `thumbv8m.main`). +#[cfg(armv8m)] +pub unsafe fn jump_to_nonsecure(ns_vtor: u32) -> ! { + // SCB_NS->VTOR is the Non-Secure alias of the SCB VTOR register (0xE002_ED08). + // Writing it tells the NS world where its vector table lives before we hand off. + const SCB_NS_VTOR: *mut u32 = 0xE002_ED08 as *mut u32; + unsafe { + SCB_NS_VTOR.write_volatile(ns_vtor); + } + + // Load the initial NS stack pointer from the first word of the NS vector table + // and write it into MSP_NS. + let ns_sp = unsafe { core::ptr::read_volatile(ns_vtor as *const u32) }; + unsafe { + core::arch::asm!( + "msr msp_ns, {sp}", + sp = in(reg) ns_sp, + options(nomem, nostack, preserves_flags), + ); + } + + // Read the NS reset handler address from the second word of the NS vector table. + // ARM ABI: bit 0 is set in the stored value (Thumb mode marker). + // BXNS requires bit 0 = 0; if bit 0 is set, it raises SecureFault (SFSR.INVTRAN). + let ns_reset = unsafe { core::ptr::read_volatile((ns_vtor as *const u32).add(1)) }; + + // BXNS atomically clears bit 0, switches the processor to Non-Secure state, and + // branches to the NS reset handler. This instruction does not return. + unsafe { + core::arch::asm!( + "bxns {entry}", + entry = in(reg) ns_reset & !1u32, + options(noreturn), + ); + } +} From 86bdf6b2f0e28e9a569150db85c77314dcc41cf1 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Tue, 21 Apr 2026 10:27:50 -0700 Subject: [PATCH 2/8] peripheral/sau: return error from init instead of silently ignoring extras --- cortex-m/src/peripheral/sau.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/cortex-m/src/peripheral/sau.rs b/cortex-m/src/peripheral/sau.rs index a308e3dd..80914c51 100644 --- a/cortex-m/src/peripheral/sau.rs +++ b/cortex-m/src/peripheral/sau.rs @@ -134,6 +134,9 @@ pub enum SauError { WrongBaseAddress, /// Bits 0 to 4 of the limit address of a SAU region must be set to one. WrongLimitAddress, + /// The number of regions passed to [`SAU::init`] exceeds the number of regions implemented + /// in hardware (as reported by [`SAU::region_numbers`]). + TooManyRegions, } impl SAU { @@ -161,7 +164,7 @@ impl SAU { /// /// This is a convenience wrapper around [`set_region`] + [`enable`]: /// 1. Disables the SAU temporarily. - /// 2. Programs up to 8 regions from `regions` (extras silently ignored). + /// 2. Programs all regions from `regions`. /// 3. Re-enables the SAU. /// /// Memory not covered by any enabled region is treated as Secure once the SAU is enabled. @@ -171,18 +174,25 @@ impl SAU { /// `scb.enable(cortex_m::peripheral::scb::Exception::SecureFault)` after this. /// /// # Errors - /// Region-programming errors (bad alignment, region number out of range) are silently ignored; - /// `take(8)` already bounds the region count to the hardware maximum. + /// Returns [`SauError::TooManyRegions`] if `regions.len()` exceeds the number of regions + /// implemented in hardware (see [`region_numbers`]). Returns other [`SauError`] variants if + /// any region descriptor has a misaligned base or limit address. + /// + /// On error the SAU is left disabled (in the state set at step 1 above). #[inline] - pub fn init(&mut self, regions: &[SauRegion]) { + pub fn init(&mut self, regions: &[SauRegion]) -> Result<(), SauError> { + if regions.len() > self.region_numbers() as usize { + return Err(SauError::TooManyRegions); + } // Disable while reprogramming to avoid partial-update windows. unsafe { self.ctrl.write(Ctrl(0)); } - for (i, ®ion) in regions.iter().enumerate().take(8) { - let _ = self.set_region(i as u8, region); + for (i, ®ion) in regions.iter().enumerate() { + self.set_region(i as u8, region)?; } self.enable(); + Ok(()) } /// Enable the SAU. From 9e00b22e24b7d52e22375c5103ac058291a11387 Mon Sep 17 00:00:00 2001 From: Gerzain Mata Date: Wed, 22 Apr 2026 16:11:21 -0700 Subject: [PATCH 3/8] peripheral/sau: use *const u32 for jump_to_nonsecure, add testsuite example --- cortex-m/src/peripheral/sau.rs | 10 +++++----- testsuite/src/lib.rs | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cortex-m/src/peripheral/sau.rs b/cortex-m/src/peripheral/sau.rs index 80914c51..9a3b66c1 100644 --- a/cortex-m/src/peripheral/sau.rs +++ b/cortex-m/src/peripheral/sau.rs @@ -306,21 +306,21 @@ impl SAU { /// - Must be called from the Secure world after all SAU/GTZC setup is complete. /// - `ns_vtor` must point to a valid Non-Secure vector table. The Cortex-M33 requires the VTOR /// to be at least 32-byte aligned; in practice 128-byte or 256-byte alignment is typical. -/// - The NS reset handler at `*(ns_vtor + 4)` must be a valid Thumb function address (bit 0 set +/// - The NS reset handler at `*(ns_vtor + 1)` must be a valid Thumb function address (bit 0 set /// in the vector table entry, as per the ARM ABI convention for vector tables). /// - Available on ARMv8-M only (`thumbv8m.base` and `thumbv8m.main`). #[cfg(armv8m)] -pub unsafe fn jump_to_nonsecure(ns_vtor: u32) -> ! { +pub unsafe fn jump_to_nonsecure(ns_vtor: *const u32) -> ! { // SCB_NS->VTOR is the Non-Secure alias of the SCB VTOR register (0xE002_ED08). // Writing it tells the NS world where its vector table lives before we hand off. const SCB_NS_VTOR: *mut u32 = 0xE002_ED08 as *mut u32; unsafe { - SCB_NS_VTOR.write_volatile(ns_vtor); + SCB_NS_VTOR.write_volatile(ns_vtor as usize as u32); } // Load the initial NS stack pointer from the first word of the NS vector table // and write it into MSP_NS. - let ns_sp = unsafe { core::ptr::read_volatile(ns_vtor as *const u32) }; + let ns_sp = unsafe { core::ptr::read_volatile(ns_vtor) }; unsafe { core::arch::asm!( "msr msp_ns, {sp}", @@ -332,7 +332,7 @@ pub unsafe fn jump_to_nonsecure(ns_vtor: u32) -> ! { // Read the NS reset handler address from the second word of the NS vector table. // ARM ABI: bit 0 is set in the stored value (Thumb mode marker). // BXNS requires bit 0 = 0; if bit 0 is set, it raises SecureFault (SFSR.INVTRAN). - let ns_reset = unsafe { core::ptr::read_volatile((ns_vtor as *const u32).add(1)) }; + let ns_reset = unsafe { core::ptr::read_volatile(ns_vtor.add(1)) }; // BXNS atomically clears bit 0, switches the processor to Non-Secure state, and // branches to the NS reset handler. This instruction does not return. diff --git a/testsuite/src/lib.rs b/testsuite/src/lib.rs index 27deafa5..a684407f 100644 --- a/testsuite/src/lib.rs +++ b/testsuite/src/lib.rs @@ -166,6 +166,29 @@ mod tests { cortex_m::asm::semihosting_syscall(SYS_WRITE0, msg.as_ptr() as usize as u32); } } + + #[cfg(armv8m)] + #[test] + fn sau_set_get_region(p: &mut cortex_m::Peripherals) { + use cortex_m::peripheral::sau::{SauRegion, SauRegionAttribute}; + + // The SAU must have at least one region on any ARMv8-M implementation. + let n = p.SAU.region_numbers(); + assert!(n > 0); + + // Program region 0 as a Non-Secure window and read it back to verify the + // register round-trip works correctly. + let region = SauRegion { + base_address: 0x2000_0000, + limit_address: 0x2001_FFFF, // bottom 5 bits already 1 (0x1F) + attribute: SauRegionAttribute::NonSecure, + }; + p.SAU.set_region(0, region).unwrap(); + let got = p.SAU.get_region(0).unwrap(); + assert_eq!(got.base_address, region.base_address); + assert_eq!(got.limit_address, region.limit_address); + assert_eq!(got.attribute, region.attribute); + } // this test must be last! #[test] From 4ae8b1165ca0fd923666319f9ef83c00334f66a0 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 10 Jul 2026 11:37:00 +0100 Subject: [PATCH 4/8] Add secure-mode feature. Enables non-secure alias for SCB --- cortex-m/Cargo.toml | 2 ++ cortex-m/src/peripheral/mod.rs | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/cortex-m/Cargo.toml b/cortex-m/Cargo.toml index 5431f48b..358ba721 100644 --- a/cortex-m/Cargo.toml +++ b/cortex-m/Cargo.toml @@ -40,6 +40,8 @@ critical-section-single-core = ["critical-section/restore-state-u32"] critical-section = [] # Deprecated feature from when inline-asm was optional (to preserve a lower MSRV) inline-asm = [] +# Add NS variants of various Cortex-M peripherals, which secure mode might want to configure +secure-mode = [] [package.metadata.docs.rs] targets = [ diff --git a/cortex-m/src/peripheral/mod.rs b/cortex-m/src/peripheral/mod.rs index d65707c1..d69ea0aa 100644 --- a/cortex-m/src/peripheral/mod.rs +++ b/cortex-m/src/peripheral/mod.rs @@ -139,6 +139,13 @@ pub struct Peripherals { /// System Control Block pub SCB: SCB, + /// Nonsecure alias for System Control Block + /// + /// This lets a CPU running in Secure mode access the Nonsecure System Control + /// Block, without switching to Nonsecure mode to do so. + #[cfg(feature = "secure-mode")] + pub SCBNS: SCBNS, + /// SysTick: System Timer pub SYST: SYST, @@ -215,6 +222,10 @@ impl Peripherals { SCB: SCB { _marker: PhantomData, }, + #[cfg(feature = "secure-mode")] + SCBNS: SCBNS { + _marker: PhantomData, + }, SYST: SYST { _marker: PhantomData, }, @@ -621,6 +632,34 @@ impl ops::Deref for SCB { } } +/// Nonsecure alias for the System Control Block +/// +/// This lets a CPU running in Secure mode access the Nonsecure System Control +/// Block, without switching to Nonsecure mode to do so. +#[cfg(feature = "secure-mode")] +pub struct SCBNS { + _marker: core::marker::PhantomData<*const ()>, +} + +#[cfg(feature = "secure-mode")] +unsafe impl Send for SCBNS {} + +#[cfg(feature = "secure-mode")] +impl SCBNS { + /// Pointer to the nonsecure alias for the register block + pub const PTR: *const scb::RegisterBlock = 0xE002_ED04 as *const _; +} + +#[cfg(feature = "secure-mode")] +impl ops::Deref for SCBNS { + type Target = scb::RegisterBlock; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + unsafe { &*Self::PTR } + } +} + /// SysTick: System Timer pub struct SYST { _marker: PhantomData<*const ()>, From 1721aff3168be9fb067ccfc9c757cc6541c6bc3a Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Fri, 10 Jul 2026 11:37:41 +0100 Subject: [PATCH 5/8] Allow access to SFAR and SFSR fields. The values aren't very useful if you cannot access the fields outside the cortex-m crate. --- cortex-m/src/peripheral/sau.rs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/cortex-m/src/peripheral/sau.rs b/cortex-m/src/peripheral/sau.rs index da91aca9..9606cddc 100644 --- a/cortex-m/src/peripheral/sau.rs +++ b/cortex-m/src/peripheral/sau.rs @@ -83,14 +83,23 @@ bitfield! { #[repr(C)] #[derive(Copy, Clone)] pub struct Sfsr(u32); - invep, _: 0; - invis, _: 1; - inver, _: 2; - auviol, _: 3; - invtran, _: 4; - lsperr, _: 5; - sfarvalid, _: 6; - lserr, _: 7; + impl Debug; + /// Invalid Entry Point + pub invep, _: 0; + /// Invalid Integrity Signature + pub invis, _: 1; + /// Invalid Exception Return + pub inver, _: 2; + /// Attribution Unit Violation + pub auviol, _: 3; + /// Invalid Transition + pub invtran, _: 4; + /// Lazy state preservation error + pub lsperr, _: 5; + /// SFAR is valid + pub sfarvalid, _: 6; + /// Lazy state error + pub lserr, _: 7; } bitfield! { @@ -98,8 +107,12 @@ bitfield! { #[repr(C)] #[derive(Copy, Clone)] pub struct Sfar(u32); + impl Debug; u32; - address, _: 31, 0; + /// Faulting memory address + /// + /// Only valid if SFSR.SFARVALID = 1 + pub address, _: 31, 0; } /// Possible attribute of a SAU region. From d8c1b60a22e85614d1cd2d97f603373018179085 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Tue, 21 Jul 2026 12:22:10 +0100 Subject: [PATCH 6/8] Move NS bootload function into asm module. Also moved the BXNS instruction into a global asm block to avoid issues with erasing registers that LLVM wants to use. --- cortex-m/src/asm.rs | 95 ++++++++++++++++++++++++++++++++++ cortex-m/src/peripheral/sau.rs | 51 ------------------ 2 files changed, 95 insertions(+), 51 deletions(-) diff --git a/cortex-m/src/asm.rs b/cortex-m/src/asm.rs index d322a371..fa09e57d 100644 --- a/cortex-m/src/asm.rs +++ b/cortex-m/src/asm.rs @@ -390,6 +390,101 @@ pub unsafe fn bootload(vector_table: *const u32) -> ! { } } +/// Transfer control to the Non-Secure application. Does not return. +/// +/// This performs the standard Secure→Non-Secure boot handoff: +/// 1. Sets `SCB_NS->VTOR` to `ns_vtor` so the Non-Secure world finds its vector table. +/// 2. Loads `MSP_NS` from the first word of the NS vector table (the initial NS stack pointer). +/// 3. Reads the NS reset handler address from the second word of the NS vector table. +/// 4. Executes `BXNS` to atomically switch to Non-Secure state and jump to the handler. +/// +/// # Safety +/// - Must be called from the Secure world after all SAU/GTZC setup is complete. +/// - `ns_vtor` must point to a valid Non-Secure vector table. The Cortex-M33 requires the VTOR +/// to be at least 32-byte aligned; in practice 128-byte or 256-byte alignment is typical. +/// - The NS reset handler at `*(ns_vtor + 1)` must be a valid Thumb function address (bit 0 set +/// in the vector table entry, as per the ARM ABI convention for vector tables). +/// - Available on ARMv8-M only (`thumbv8m.base` and `thumbv8m.main`). +#[cfg(all(armv8m, feature = "secure-mode"))] +pub unsafe fn bootload_ns(ns_vtor: *const u32, scb_ns: crate::peripheral::SCBNS) -> ! { + // Set NS_VTOR, so nonsecure mode uses that vector table + unsafe { + scb_ns.vtor.write(ns_vtor as usize as u32); + } + + // Load the initial NS stack pointer from the first word of the NS vector table + // and write it into MSP_NS. + let ns_sp = unsafe { ns_vtor.read_volatile() }; + + // Set MSP_NS, so nonsecure mode uses that stack pointer + unsafe { + crate::register::msp::write_ns(ns_sp); + } + + // Read the NS reset handler address from the second word of the NS vector table. + // ARM ABI: bit 0 is set in the stored value (Thumb mode marker). + // BXNS requires bit 0 = 0; if bit 0 is set, it raises SecureFault (SFSR.INVTRAN). + let ns_reset = unsafe { ns_vtor.add(1).read_volatile() }; + + // BXNS switches the processor to the state given in the LSB + // so we must clear that bit. + unsafe extern "C" { + fn _bx_ns_trampoline(boot: u32) -> !; + } + unsafe { + _bx_ns_trampoline(ns_reset & 0xFFFF_FFFE); + } +} + +#[cfg(all(not(has_fpu), armv8m, feature = "secure-mode"))] +core::arch::global_asm!( + r#" + .type _bx_ns_trampoline,%function + .global _bx_ns_trampoline + _bx_ns_trampoline: + b _bx_ns_trampoline_part2 + .size _bx_ns_trampoline, . - _bx_ns_trampoline + "#, +); + +#[cfg(all(has_fpu, armv8m, feature = "secure-mode"))] +core::arch::global_asm!( + r#" + .type _bx_ns_trampoline,%function + .global _bx_ns_trampoline + _bx_ns_trampoline: + vlstm sp // Push secure FPU state to stack, and zero secure FPU registers + b _bx_ns_trampoline_part2 + .size _bx_ns_trampoline, . - _bx_ns_trampoline + "#, +); + +#[cfg(all(armv8m, feature = "secure-mode"))] +core::arch::global_asm!( + r#" + .type _bx_ns_trampoline_part2,%function + .global _bx_ns_trampoline_part2 + _bx_ns_trampoline_part2: + mov lr, r0 // Put target address in LR + mov r0, 0 // Zero all the other registers + mov r1, 0 // Except secure MSP, as nonsecure has its own MSP, which we set + mov r2, 0 + mov r3, 0 + mov r4, 0 + mov r5, 0 + mov r6, 0 + mov r7, 0 + mov r8, 0 + mov r8, 0 + mov r9, 0 + mov r10, 0 + mov r11, 0 + mov r12, 0 + bxns lr // Branch to nonsecure mode + .size _bx_ns_trampoline_part2, . - _bx_ns_trampoline_part2 + "#, +); + /// This instruction moves one Register to a Coprocessor Register. /// This function generates inline assembly and needs the instruction configuration /// during compilation time (i.e. as `const`). diff --git a/cortex-m/src/peripheral/sau.rs b/cortex-m/src/peripheral/sau.rs index 549bc810..dc00e137 100644 --- a/cortex-m/src/peripheral/sau.rs +++ b/cortex-m/src/peripheral/sau.rs @@ -306,54 +306,3 @@ impl SAU { }) } } - -/// Transfer control to the Non-Secure application. Does not return. -/// -/// This performs the standard Secure→Non-Secure boot handoff: -/// 1. Sets `SCB_NS->VTOR` to `ns_vtor` so the Non-Secure world finds its vector table. -/// 2. Loads `MSP_NS` from the first word of the NS vector table (the initial NS stack pointer). -/// 3. Reads the NS reset handler address from the second word of the NS vector table. -/// 4. Executes `BXNS` to atomically switch to Non-Secure state and jump to the handler. -/// -/// # Safety -/// - Must be called from the Secure world after all SAU/GTZC setup is complete. -/// - `ns_vtor` must point to a valid Non-Secure vector table. The Cortex-M33 requires the VTOR -/// to be at least 32-byte aligned; in practice 128-byte or 256-byte alignment is typical. -/// - The NS reset handler at `*(ns_vtor + 1)` must be a valid Thumb function address (bit 0 set -/// in the vector table entry, as per the ARM ABI convention for vector tables). -/// - Available on ARMv8-M only (`thumbv8m.base` and `thumbv8m.main`). -#[cfg(armv8m)] -pub unsafe fn jump_to_nonsecure(ns_vtor: *const u32) -> ! { - // SCB_NS->VTOR is the Non-Secure alias of the SCB VTOR register (0xE002_ED08). - // Writing it tells the NS world where its vector table lives before we hand off. - const SCB_NS_VTOR: *mut u32 = 0xE002_ED08 as *mut u32; - unsafe { - SCB_NS_VTOR.write_volatile(ns_vtor as usize as u32); - } - - // Load the initial NS stack pointer from the first word of the NS vector table - // and write it into MSP_NS. - let ns_sp = unsafe { core::ptr::read_volatile(ns_vtor) }; - unsafe { - core::arch::asm!( - "msr msp_ns, {sp}", - sp = in(reg) ns_sp, - options(nomem, nostack, preserves_flags), - ); - } - - // Read the NS reset handler address from the second word of the NS vector table. - // ARM ABI: bit 0 is set in the stored value (Thumb mode marker). - // BXNS requires bit 0 = 0; if bit 0 is set, it raises SecureFault (SFSR.INVTRAN). - let ns_reset = unsafe { core::ptr::read_volatile(ns_vtor.add(1)) }; - - // BXNS atomically clears bit 0, switches the processor to Non-Secure state, and - // branches to the NS reset handler. This instruction does not return. - unsafe { - core::arch::asm!( - "bxns {entry}", - entry = in(reg) ns_reset & !1u32, - options(noreturn), - ); - } -} From e0e49c542364ace56e9a9e74cc74280c756edce0 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Tue, 21 Jul 2026 12:37:41 +0100 Subject: [PATCH 7/8] Also clear processor flags. I don't imagine you could leak a lot from secure mode with those, but we should try and leak nothing. --- cortex-m/src/asm.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cortex-m/src/asm.rs b/cortex-m/src/asm.rs index fa09e57d..e75e9dc6 100644 --- a/cortex-m/src/asm.rs +++ b/cortex-m/src/asm.rs @@ -465,9 +465,9 @@ core::arch::global_asm!( .type _bx_ns_trampoline_part2,%function .global _bx_ns_trampoline_part2 _bx_ns_trampoline_part2: - mov lr, r0 // Put target address in LR - mov r0, 0 // Zero all the other registers - mov r1, 0 // Except secure MSP, as nonsecure has its own MSP, which we set + mov lr, r0 // Put target address in LR + mov r0, 0 // Zero all the other registers + mov r1, 0 // Except secure MSP, as nonsecure has its own MSP, which we set mov r2, 0 mov r3, 0 mov r4, 0 @@ -480,7 +480,8 @@ core::arch::global_asm!( mov r10, 0 mov r11, 0 mov r12, 0 - bxns lr // Branch to nonsecure mode + msr apsr_nzcvq, r0 // Also clear processor flags + bxns lr // Branch to nonsecure mode .size _bx_ns_trampoline_part2, . - _bx_ns_trampoline_part2 "#, ); From ffa04bb388e89d2a2464d044221411bd98c27ad9 Mon Sep 17 00:00:00 2001 From: Jonathan Pallant Date: Tue, 21 Jul 2026 12:50:32 +0100 Subject: [PATCH 8/8] Unconditionally do vlstm. It should be a NOP if you don't have FPU support. And this is better because someone might have been using the FPU on an EABI target. --- cortex-m/src/asm.rs | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/cortex-m/src/asm.rs b/cortex-m/src/asm.rs index e75e9dc6..f4090832 100644 --- a/cortex-m/src/asm.rs +++ b/cortex-m/src/asm.rs @@ -436,35 +436,13 @@ pub unsafe fn bootload_ns(ns_vtor: *const u32, scb_ns: crate::peripheral::SCBNS) } } -#[cfg(all(not(has_fpu), armv8m, feature = "secure-mode"))] -core::arch::global_asm!( - r#" - .type _bx_ns_trampoline,%function - .global _bx_ns_trampoline - _bx_ns_trampoline: - b _bx_ns_trampoline_part2 - .size _bx_ns_trampoline, . - _bx_ns_trampoline - "#, -); - -#[cfg(all(has_fpu, armv8m, feature = "secure-mode"))] +#[cfg(all(armv8m, feature = "secure-mode"))] core::arch::global_asm!( r#" .type _bx_ns_trampoline,%function .global _bx_ns_trampoline _bx_ns_trampoline: - vlstm sp // Push secure FPU state to stack, and zero secure FPU registers - b _bx_ns_trampoline_part2 - .size _bx_ns_trampoline, . - _bx_ns_trampoline - "#, -); - -#[cfg(all(armv8m, feature = "secure-mode"))] -core::arch::global_asm!( - r#" - .type _bx_ns_trampoline_part2,%function - .global _bx_ns_trampoline_part2 - _bx_ns_trampoline_part2: + vlstm sp // Push secure FPU state to stack, and zero secure FPU registers (nop if no FPU present) mov lr, r0 // Put target address in LR mov r0, 0 // Zero all the other registers mov r1, 0 // Except secure MSP, as nonsecure has its own MSP, which we set @@ -482,7 +460,7 @@ core::arch::global_asm!( mov r12, 0 msr apsr_nzcvq, r0 // Also clear processor flags bxns lr // Branch to nonsecure mode - .size _bx_ns_trampoline_part2, . - _bx_ns_trampoline_part2 + .size _bx_ns_trampoline, . - _bx_ns_trampoline "#, );