fix(f722): bound flash operations from sram

This commit is contained in:
Onur Özkan committed 2026-08-24 00:55:48 +03:00
1 parent 9fec2fa095
commit 1c90870634
4 files changed
+302 -63

No files matched your search

+133 -51
View File
@@ -197,7 +197,10 @@ const F722_FIRST_LARGE_SECTOR: usize = 5;
const FLASH_KEY1: u32 = 0x4567_0123; const FLASH_KEY1: u32 = 0x4567_0123;
const FLASH_KEY2: u32 = 0xcdef_89ab; const FLASH_KEY2: u32 = 0xcdef_89ab;
const FLASH_ERROR_FLAGS: u32 = 0x01f2; const FLASH_ERROR_FLAGS: u32 = 0x01f2;
const BUSY_POLL_LIMIT: u32 = 1_000_000_000; // The F722 datasheet bounds one x32 128-KiB sector erase at 1.322 seconds.
// The already-running 216-MHz DWT counter gives every operation a two-second
// deadline without turning an instruction-count guess into a timing contract.
const FLASH_OPERATION_TIMEOUT_CYCLES: u32 = 2 * 216_000_000;
/// One schema-independent journal shared by every STM32F722 board image. /// One schema-independent journal shared by every STM32F722 board image.
pub(crate) type F722ConfigStore<C, I, const MAX_PAYLOAD_BYTES: usize> = pub(crate) type F722ConfigStore<C, I, const MAX_PAYLOAD_BYTES: usize> =
@@ -212,12 +215,16 @@ pub(crate) type F722ConfigManager<C, I, const MAX_PAYLOAD_BYTES: usize> =
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum F722FlashError { pub(crate) enum F722FlashError {
Bounds, Bounds,
BusyTimeout,
Unlock, Unlock,
Controller, Controller,
Verification, Verification,
} }
enum F722FlashOperation {
Erase { sector: u8 },
Program { address: usize, word: u32 },
}
/// Sole owner of the F722 flash controller used by the configuration journal. /// Sole owner of the F722 flash controller used by the configuration journal.
pub(crate) struct F722Nor { pub(crate) struct F722Nor {
flash: stm32f722::FLASH, flash: stm32f722::FLASH,
@@ -275,20 +282,106 @@ impl F722Nor {
.map_err(|_| F722FlashError::Bounds) .map_err(|_| F722FlashError::Bounds)
} }
fn wait_ready(&self) -> Result<(), F722FlashError> { /// Executes one complete Flash operation from SRAM with IRQs masked.
let mut remaining = BUSY_POLL_LIMIT; ///
/// A controller that remains busy beyond the datasheet bound cannot be
/// recovered by writing `FLASH_CR`: RM0431 specifies that such a write
/// stalls until `BSY` clears. The trigger, poll, cleanup, and timeout reset
/// all stay in SRAM so neither an instruction fetch nor an IRQ vector can
/// strand execution in Flash while `BSY` remains asserted.
#[cfg_attr(
target_os = "none",
unsafe(link_section = ".data.kaptan_flash_operation")
)]
#[inline(never)]
fn execute(&self, operation: F722FlashOperation) -> Result<(), F722FlashError> {
#[cfg(target_os = "none")]
let primask: u32;
#[cfg(target_os = "none")]
// SAFETY: these instructions only read and set the core interrupt mask.
// The previous mask is restored after Flash is idle and relocked.
unsafe {
core::arch::asm!(
"mrs {primask}, PRIMASK",
"cpsid i",
primask = out(reg) primask,
options(nomem, nostack, preserves_flags),
);
}
match operation {
F722FlashOperation::Erase { sector } => {
// SAFETY: the caller resolved one of the two reserved sectors.
self.flash.cr().modify(|_, write| unsafe {
write
.pg()
.clear_bit()
.ser()
.set_bit()
.snb()
.bits(sector)
.psize()
.psize32()
.strt()
.start()
});
}
F722FlashOperation::Program { address, word } => {
self.flash
.cr()
.modify(|_, write| write.ser().clear_bit().psize().psize32().pg().set_bit());
// SAFETY: the caller checked that this word is aligned and
// contained in one reserved erased journal sector.
unsafe { core::ptr::write_volatile(address as *mut u32, word) };
}
}
#[cfg(target_os = "none")]
// SAFETY: this operand-free barrier orders the erase trigger or
// programming store before the status-register poll.
unsafe {
core::arch::asm!("dsb sy", options(nostack, preserves_flags));
}
let started = cortex_m::peripheral::DWT::cycle_count();
while self.flash.sr().read().bsy().is_busy() { while self.flash.sr().read().bsy().is_busy() {
if remaining == 0 { if cortex_m::peripheral::DWT::cycle_count().wrapping_sub(started)
return Err(F722FlashError::BusyTimeout); >= FLASH_OPERATION_TIMEOUT_CYCLES
{
reset_after_flash_timeout();
} }
remaining -= 1;
core::hint::spin_loop(); core::hint::spin_loop();
} }
Ok(())
let status = self.flash.sr().read();
self.flash.cr().modify(|_, write| {
write.pg().clear_bit();
write.ser().clear_bit();
write.lock().locked()
});
if status.eop().is_active() {
self.flash.sr().write(|write| write.eop().clear());
}
#[cfg(target_os = "none")]
if primask & 1 == 0 {
// SAFETY: Flash is idle and relocked, so restoring the caller's
// enabled interrupt state cannot vector through a busy Flash bank.
unsafe {
core::arch::asm!("cpsie i", options(nomem, nostack, preserves_flags));
}
}
if status.bits() & FLASH_ERROR_FLAGS != 0 {
Err(F722FlashError::Controller)
} else {
Ok(())
}
} }
fn unlock(&self) -> Result<(), F722FlashError> { fn unlock(&self) -> Result<(), F722FlashError> {
self.wait_ready()?; if self.flash.sr().read().bsy().is_busy() {
return Err(F722FlashError::Controller);
}
if self.flash.cr().read().lock().is_locked() { if self.flash.cr().read().lock().is_locked() {
// SAFETY: these are the exact two full-width FPEC unlock keys from // SAFETY: these are the exact two full-width FPEC unlock keys from
// RM0431, written in the required order while the controller is idle. // RM0431, written in the required order while the controller is idle.
@@ -316,23 +409,39 @@ impl F722Nor {
.write(|write| unsafe { write.bits(FLASH_ERROR_FLAGS) }); .write(|write| unsafe { write.bits(FLASH_ERROR_FLAGS) });
Ok(()) Ok(())
} }
}
fn finish(&self) -> Result<(), F722FlashError> { /// Requests a system reset without fetching code or data from embedded Flash.
self.wait_ready()?; #[inline(always)]
let status = self.flash.sr().read(); fn reset_after_flash_timeout() -> ! {
self.flash.cr().modify(|_, write| { #[cfg(target_os = "none")]
write.pg().clear_bit(); {
write.ser().clear_bit(); const AIRCR: *mut u32 = 0xe000_ed0c as *mut u32;
write.lock().locked() const VECTKEY: u32 = 0x05fa << 16;
}); const PRIGROUP: u32 = 0x7 << 8;
if status.bits() & FLASH_ERROR_FLAGS != 0 { const SYSRESETREQ: u32 = 1 << 2;
return Err(F722FlashError::Controller);
// SAFETY: this barrier has no operands or memory address. It orders the
// preceding peripheral reads before the reset request.
unsafe { core::arch::asm!("dsb sy", options(nostack, preserves_flags)) };
// SAFETY: AIRCR is a word-aligned private-peripheral register. VECTKEY is
// required for the write; retaining PRIGROUP avoids changing reset policy.
unsafe {
let priority_group = core::ptr::read_volatile(AIRCR) & PRIGROUP;
core::ptr::write_volatile(AIRCR, VECTKEY | priority_group | SYSRESETREQ);
} }
if status.eop().is_active() { // SAFETY: this barrier ensures the reset request reaches the system
self.flash.sr().write(|write| write.eop().clear()); // control block before the processor waits for reset.
unsafe { core::arch::asm!("dsb sy", options(nostack, preserves_flags)) };
loop {
// SAFETY: `nop` has no operands, memory access, or observable state
// beyond keeping the reset wait loop present.
unsafe { core::arch::asm!("nop", options(nomem, nostack, preserves_flags)) };
} }
Ok(())
} }
#[cfg(not(target_os = "none"))]
panic!("flash timeout reset is an embedded-only path")
} }
impl DualSlotNor for F722Nor { impl DualSlotNor for F722Nor {
@@ -356,25 +465,7 @@ impl DualSlotNor for F722Nor {
fn erase_slot(&mut self, slot: usize) -> Result<(), Self::Error> { fn erase_slot(&mut self, slot: usize) -> Result<(), Self::Error> {
let sector = Self::sector(slot)?; let sector = Self::sector(slot)?;
self.prepare()?; self.prepare()?;
// SAFETY: RM0431 assigns sectors 6 and 7 to the two reserved 128 KiB self.execute(F722FlashOperation::Erase { sector })?;
// ranges above. No application section is linked into either sector.
self.flash.cr().modify(|_, write| unsafe {
write
.pg()
.clear_bit()
.ser()
.set_bit()
.snb()
.bits(sector)
.psize()
.psize32()
.strt()
.start()
});
// RM0431 permits a DSB between asserting STRT and polling BSY so the
// control-register write cannot be observed out of order.
cortex_m::asm::dsb();
self.finish()?;
let mut probe = [0_u8; 4]; let mut probe = [0_u8; 4];
self.read(slot, 0, &mut probe)?; self.read(slot, 0, &mut probe)?;
if probe != [0xff; 4] { if probe != [0xff; 4] {
@@ -389,16 +480,7 @@ impl DualSlotNor for F722Nor {
} }
let address = Self::address(slot, offset, 4)?; let address = Self::address(slot, offset, 4)?;
self.prepare()?; self.prepare()?;
self.flash self.execute(F722FlashOperation::Program { address, word })?;
.cr()
.modify(|_, write| write.ser().clear_bit().psize().psize32().pg().set_bit());
// SAFETY: the checked address is aligned and contained in a reserved
// erased journal sector, and PG with x32 parallelism is active.
unsafe { core::ptr::write_volatile(address as *mut u32, word) };
// RM0431 requires the programming store to complete before BSY is
// polled or the PG/control state is changed.
cortex_m::asm::dsb();
self.finish()?;
// SAFETY: the same checked flash word is readable after BSY clears. // SAFETY: the same checked flash word is readable after BSY clears.
if unsafe { core::ptr::read_volatile(address as *const u32) } != word { if unsafe { core::ptr::read_volatile(address as *const u32) } != word {
return Err(F722FlashError::Verification); return Err(F722FlashError::Verification);
@@ -183,6 +183,20 @@ fn ordinary_f722_image_has_direct_configuration_after_output_inhibition() {
); );
} }
#[test]
fn flash_busy_deadline_resets_entirely_from_sram() {
let config = platform_source("src/f722/config.rs");
assert!(config.contains("link_section = \".data.kaptan_flash_operation\""));
assert!(config.contains("fn execute(&self, operation: F722FlashOperation)"));
assert!(config.contains("\"mrs {primask}, PRIMASK\""));
assert!(config.contains("\"cpsid i\""));
assert!(config.contains("\"cpsie i\""));
assert!(config.contains("fn reset_after_flash_timeout() -> !"));
assert!(config.contains("SYSRESETREQ"));
assert!(config.contains("core::arch::asm!(\"dsb sy\""));
assert!(!config.contains("BusyTimeout"));
}
#[test] #[test]
fn usart_tx_dma_arms_each_stream_before_its_request() { fn usart_tx_dma_arms_each_stream_before_its_request() {
let usart = platform_source("src/f722/peripherals/usart.rs"); let usart = platform_source("src/f722/peripherals/usart.rs");
+92 -1
View File
@@ -19,7 +19,7 @@ REGION = re.compile(
re.MULTILINE, re.MULTILINE,
) )
Section = namedtuple("Section", "name size address allocated") Section = namedtuple("Section", "name size address allocated executable")
LoadSegment = namedtuple( LoadSegment = namedtuple(
"LoadSegment", "virtual_address physical_address file_size memory_size" "LoadSegment", "virtual_address physical_address file_size memory_size"
) )
@@ -78,6 +78,7 @@ def elf_layout(output):
section["Size"], section["Size"],
section["Address"], section["Address"],
"SHF_ALLOC" in flags, "SHF_ALLOC" in flags,
"SHF_EXECINSTR" in flags,
) )
) )
@@ -186,6 +187,76 @@ def validation_errors(layout, section_list, load_segments):
return errors return errors
def flash_operation_section_errors(layout, section_list):
"""Validate the output section that contains the F722 Flash operation."""
matches = [section for section in section_list if section.name == ".data"]
if len(matches) != 1:
return ["firmware must contain exactly one .data section"]
section = matches[0]
if not section.allocated or not section.executable or section.size == 0:
return [".data must contain executable SRAM Flash-operation code"]
if region_for_span(section.address, section.size, layout) != "RAM":
return ["executable .data must reside completely in RAM"]
return []
INSTRUCTION = re.compile(
r"^\s*([0-9A-Fa-f]+):\s+([A-Za-z][A-Za-z0-9.]*)\s*(.*?)\s*$"
)
DIRECT_BRANCH = re.compile(
r"^(?:b(?:eq|ne|cs|hs|cc|lo|mi|pl|vs|vc|hi|ls|ge|lt|gt|le)?"
r"(?:\.[nw])?|blx?|bx|cbz|cbnz)$"
)
def sram_control_flow_errors(section, disassembly):
"""Reject external control flow and PC-relative loads from SRAM code."""
errors = []
instructions = 0
end = section.address + section.size
for line in disassembly.splitlines():
match = INSTRUCTION.match(line)
if match is None:
continue
instructions += 1
address = int(match.group(1), 16)
mnemonic = match.group(2).lower()
operands = match.group(3).split("@", 1)[0].strip().lower()
if not section.address <= address < end:
errors.append(f"instruction 0x{address:08X} is outside .data")
continue
if DIRECT_BRANCH.fullmatch(mnemonic):
target = re.search(r"\b0x([0-9a-f]+)\b", operands)
if target is None:
if mnemonic == "bx" and operands == "lr":
continue
errors.append(
f"0x{address:08X} uses indirect control transfer {mnemonic}"
)
continue
destination = int(target.group(1), 16)
if not section.address <= destination < end:
errors.append(
f"0x{address:08X} branches outside SRAM .data to "
f"0x{destination:08X}"
)
continue
if mnemonic in ("tbb", "tbh"):
errors.append(f"0x{address:08X} uses indirect control transfer {mnemonic}")
elif mnemonic == "pop" and re.search(r"\bpc\b", operands):
continue
elif re.match(r"^pc(?:\s|,|$)", operands):
errors.append(f"0x{address:08X} writes the program counter indirectly")
elif mnemonic.startswith("ldr") and re.search(r"\[\s*pc\b", operands):
errors.append(f"0x{address:08X} performs a PC-relative load")
if instructions == 0:
errors.append("llvm-objdump found no instructions in executable .data")
return errors
def main(): def main():
if len(sys.argv) != 3: if len(sys.argv) != 3:
raise ValueError(f"usage: {sys.argv[0]} <firmware.elf> <memory.x>") raise ValueError(f"usage: {sys.argv[0]} <firmware.elf> <memory.x>")
@@ -248,6 +319,26 @@ def main():
if errors: if errors:
raise ValueError("firmware memory validation failed:\n " + "\n ".join(errors)) raise ValueError("firmware memory validation failed:\n " + "\n ".join(errors))
errors = flash_operation_section_errors(layout, section_list)
if errors:
raise ValueError("Flash-operation placement validation failed:\n " + "\n ".join(errors))
data = next(section for section in section_list if section.name == ".data")
objdump = llvm_tool("llvm-objdump")
errors = sram_control_flow_errors(
data,
run(
[
str(objdump),
"-d",
"--no-show-raw-insn",
"--section=.data",
str(elf),
]
),
)
if errors:
raise ValueError("Flash-operation control-flow validation failed:\n " + "\n ".join(errors))
if __name__ == "__main__": if __name__ == "__main__":
try: try:
+63 -11
View File
@@ -28,7 +28,12 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
"Name": {"Name": ".text"}, "Name": {"Name": ".text"},
"Size": 8, "Size": 8,
"Address": 0x08000000, "Address": 0x08000000,
"Flags": {"Flags": [{"Name": "SHF_ALLOC"}]}, "Flags": {
"Flags": [
{"Name": "SHF_ALLOC"},
{"Name": "SHF_EXECINSTR"},
]
},
} }
}, },
{ {
@@ -68,8 +73,8 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
self.assertEqual( self.assertEqual(
sections, sections,
[ [
ANALYZER.Section(".text", 8, 0x08000000, True), ANALYZER.Section(".text", 8, 0x08000000, True, True),
ANALYZER.Section(".debug_info", 100, 0, False), ANALYZER.Section(".debug_info", 100, 0, False, False),
], ],
) )
self.assertEqual( self.assertEqual(
@@ -79,10 +84,10 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
def test_accepts_allocated_memory_and_ignores_debug_sections(self): def test_accepts_allocated_memory_and_ignores_debug_sections(self):
sections = [ sections = [
ANALYZER.Section(".text", 0x80, 0x08000000, True), ANALYZER.Section(".text", 0x80, 0x08000000, True, True),
ANALYZER.Section(".bss", 0x40, 0x20000000, True), ANALYZER.Section(".bss", 0x40, 0x20000000, True, False),
ANALYZER.Section(".debug_info", 0x1000, 0, False), ANALYZER.Section(".debug_info", 0x1000, 0, False, False),
ANALYZER.Section(".empty", 0, 0xDEADBEEF, True), ANALYZER.Section(".empty", 0, 0xDEADBEEF, True, False),
] ]
segments = [ segments = [
ANALYZER.LoadSegment(0x08000000, 0x08000000, 0x80, 0x80), ANALYZER.LoadSegment(0x08000000, 0x08000000, 0x80, 0x80),
@@ -92,8 +97,8 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
self.assertEqual(ANALYZER.validation_errors(LAYOUT, sections, segments), []) self.assertEqual(ANALYZER.validation_errors(LAYOUT, sections, segments), [])
def test_rejects_allocated_sections_outside_or_crossing_a_region(self): def test_rejects_allocated_sections_outside_or_crossing_a_region(self):
outside = [ANALYZER.Section(".text", 4, 0x08000200, True)] outside = [ANALYZER.Section(".text", 4, 0x08000200, True, True)]
crossing = [ANALYZER.Section(".text", 2, 0x080000FF, True)] crossing = [ANALYZER.Section(".text", 2, 0x080000FF, True, True)]
self.assertIn( self.assertIn(
"allocated section .text", "allocated section .text",
@@ -106,8 +111,8 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
def test_usage_counts_data_load_image_without_double_counting_text(self): def test_usage_counts_data_load_image_without_double_counting_text(self):
sections = [ sections = [
ANALYZER.Section(".text", 0x40, 0x08000000, True), ANALYZER.Section(".text", 0x40, 0x08000000, True, True),
ANALYZER.Section(".data", 0x10, 0x20000000, True), ANALYZER.Section(".data", 0x10, 0x20000000, True, False),
] ]
segments = [ segments = [
ANALYZER.LoadSegment(0x08000000, 0x08000000, 0x40, 0x40), ANALYZER.LoadSegment(0x08000000, 0x08000000, 0x40, 0x40),
@@ -131,6 +136,53 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
self.assertTrue(any("virtual span" in error for error in errors)) self.assertTrue(any("virtual span" in error for error in errors))
self.assertTrue(any("physical span" in error for error in errors)) self.assertTrue(any("physical span" in error for error in errors))
def test_requires_one_executable_data_section_in_ram(self):
valid = [ANALYZER.Section(".data", 0x20, 0x20000000, True, True)]
non_executable = [
ANALYZER.Section(".data", 0x20, 0x20000000, True, False)
]
in_flash = [ANALYZER.Section(".data", 0x20, 0x08000000, True, True)]
self.assertEqual(ANALYZER.flash_operation_section_errors(LAYOUT, valid), [])
self.assertIn(
"executable SRAM",
ANALYZER.flash_operation_section_errors(LAYOUT, non_executable)[0],
)
self.assertIn(
"reside completely in RAM",
ANALYZER.flash_operation_section_errors(LAYOUT, in_flash)[0],
)
def test_sram_control_flow_rejects_external_or_indirect_fetches(self):
section = ANALYZER.Section(".data", 0x20, 0x20000000, True, True)
safe = """
20000000: bne 0x20000008 <.data+0x8>
20000004: ldr r0, [r1]
20000008: pop {r7, pc}
"""
external = "20000000: bl 0x08000100 <flash_helper>"
indirect_call = "20000000: blx r3"
indirect_branch = "20000000: bx r3"
literal = "20000000: ldr r0, [pc, #4]"
self.assertEqual(ANALYZER.sram_control_flow_errors(section, safe), [])
self.assertIn(
"branches outside SRAM",
ANALYZER.sram_control_flow_errors(section, external)[0],
)
self.assertIn(
"indirect control transfer",
ANALYZER.sram_control_flow_errors(section, indirect_call)[0],
)
self.assertIn(
"indirect control transfer",
ANALYZER.sram_control_flow_errors(section, indirect_branch)[0],
)
self.assertIn(
"PC-relative load",
ANALYZER.sram_control_flow_errors(section, literal)[0],
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()