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

+92 -1
View File
@@ -19,7 +19,7 @@ REGION = re.compile(
re.MULTILINE,
)
Section = namedtuple("Section", "name size address allocated")
Section = namedtuple("Section", "name size address allocated executable")
LoadSegment = namedtuple(
"LoadSegment", "virtual_address physical_address file_size memory_size"
)
@@ -78,6 +78,7 @@ def elf_layout(output):
section["Size"],
section["Address"],
"SHF_ALLOC" in flags,
"SHF_EXECINSTR" in flags,
)
)
@@ -186,6 +187,76 @@ def validation_errors(layout, section_list, load_segments):
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():
if len(sys.argv) != 3:
raise ValueError(f"usage: {sys.argv[0]} <firmware.elf> <memory.x>")
@@ -248,6 +319,26 @@ def main():
if 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__":
try:
+63 -11
View File
@@ -28,7 +28,12 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
"Name": {"Name": ".text"},
"Size": 8,
"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(
sections,
[
ANALYZER.Section(".text", 8, 0x08000000, True),
ANALYZER.Section(".debug_info", 100, 0, False),
ANALYZER.Section(".text", 8, 0x08000000, True, True),
ANALYZER.Section(".debug_info", 100, 0, False, False),
],
)
self.assertEqual(
@@ -79,10 +84,10 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
def test_accepts_allocated_memory_and_ignores_debug_sections(self):
sections = [
ANALYZER.Section(".text", 0x80, 0x08000000, True),
ANALYZER.Section(".bss", 0x40, 0x20000000, True),
ANALYZER.Section(".debug_info", 0x1000, 0, False),
ANALYZER.Section(".empty", 0, 0xDEADBEEF, True),
ANALYZER.Section(".text", 0x80, 0x08000000, True, True),
ANALYZER.Section(".bss", 0x40, 0x20000000, True, False),
ANALYZER.Section(".debug_info", 0x1000, 0, False, False),
ANALYZER.Section(".empty", 0, 0xDEADBEEF, True, False),
]
segments = [
ANALYZER.LoadSegment(0x08000000, 0x08000000, 0x80, 0x80),
@@ -92,8 +97,8 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
self.assertEqual(ANALYZER.validation_errors(LAYOUT, sections, segments), [])
def test_rejects_allocated_sections_outside_or_crossing_a_region(self):
outside = [ANALYZER.Section(".text", 4, 0x08000200, True)]
crossing = [ANALYZER.Section(".text", 2, 0x080000FF, True)]
outside = [ANALYZER.Section(".text", 4, 0x08000200, True, True)]
crossing = [ANALYZER.Section(".text", 2, 0x080000FF, True, True)]
self.assertIn(
"allocated section .text",
@@ -106,8 +111,8 @@ class FirmwareMemoryValidationTests(unittest.TestCase):
def test_usage_counts_data_load_image_without_double_counting_text(self):
sections = [
ANALYZER.Section(".text", 0x40, 0x08000000, True),
ANALYZER.Section(".data", 0x10, 0x20000000, True),
ANALYZER.Section(".text", 0x40, 0x08000000, True, True),
ANALYZER.Section(".data", 0x10, 0x20000000, True, False),
]
segments = [
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("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__":
unittest.main()