185 lines
6.7 KiB
Python
185 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Synthetic regression tests for strict firmware-memory validation."""
|
|
|
|
import importlib.util
|
|
import json
|
|
import unittest
|
|
|
|
|
|
def load_analyzer_module():
|
|
spec = importlib.util.spec_from_file_location("analyze_fw", "scripts/analyze-fw.py")
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
return module
|
|
|
|
|
|
ANALYZER = load_analyzer_module()
|
|
LAYOUT = [("FLASH", 0x08000000, 0x100), ("RAM", 0x20000000, 0x80)]
|
|
|
|
|
|
class FirmwareMemoryValidationTests(unittest.TestCase):
|
|
def test_elf_layout_uses_alloc_flag_and_only_load_segments(self):
|
|
document = [
|
|
{
|
|
"Sections": [
|
|
{
|
|
"Section": {
|
|
"Name": {"Name": ".text"},
|
|
"Size": 8,
|
|
"Address": 0x08000000,
|
|
"Flags": {
|
|
"Flags": [
|
|
{"Name": "SHF_ALLOC"},
|
|
{"Name": "SHF_EXECINSTR"},
|
|
]
|
|
},
|
|
}
|
|
},
|
|
{
|
|
"Section": {
|
|
"Name": {"Name": ".debug_info"},
|
|
"Size": 100,
|
|
"Address": 0,
|
|
"Flags": {"Flags": []},
|
|
}
|
|
},
|
|
],
|
|
"ProgramHeaders": [
|
|
{
|
|
"ProgramHeader": {
|
|
"Type": {"Name": "PT_LOAD"},
|
|
"VirtualAddress": 0x08000000,
|
|
"PhysicalAddress": 0x08000000,
|
|
"FileSize": 8,
|
|
"MemSize": 8,
|
|
}
|
|
},
|
|
{
|
|
"ProgramHeader": {
|
|
"Type": {"Name": "PT_ARM_EXIDX"},
|
|
"VirtualAddress": 0,
|
|
"PhysicalAddress": 0,
|
|
"FileSize": 0,
|
|
"MemSize": 0,
|
|
}
|
|
},
|
|
],
|
|
}
|
|
]
|
|
|
|
sections, segments = ANALYZER.elf_layout(json.dumps(document))
|
|
|
|
self.assertEqual(
|
|
sections,
|
|
[
|
|
ANALYZER.Section(".text", 8, 0x08000000, True, True),
|
|
ANALYZER.Section(".debug_info", 100, 0, False, False),
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
segments,
|
|
[ANALYZER.LoadSegment(0x08000000, 0x08000000, 8, 8)],
|
|
)
|
|
|
|
def test_accepts_allocated_memory_and_ignores_debug_sections(self):
|
|
sections = [
|
|
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),
|
|
ANALYZER.LoadSegment(0x20000000, 0x20000000, 0, 0x40),
|
|
]
|
|
|
|
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, True)]
|
|
crossing = [ANALYZER.Section(".text", 2, 0x080000FF, True, True)]
|
|
|
|
self.assertIn(
|
|
"allocated section .text",
|
|
ANALYZER.validation_errors(LAYOUT, outside, [])[0],
|
|
)
|
|
self.assertIn(
|
|
"allocated section .text",
|
|
ANALYZER.validation_errors(LAYOUT, crossing, [])[0],
|
|
)
|
|
|
|
def test_usage_counts_data_load_image_without_double_counting_text(self):
|
|
sections = [
|
|
ANALYZER.Section(".text", 0x40, 0x08000000, True, True),
|
|
ANALYZER.Section(".data", 0x10, 0x20000000, True, False),
|
|
]
|
|
segments = [
|
|
ANALYZER.LoadSegment(0x08000000, 0x08000000, 0x40, 0x40),
|
|
ANALYZER.LoadSegment(0x20000000, 0x08000040, 0x10, 0x10),
|
|
]
|
|
|
|
self.assertEqual(
|
|
ANALYZER.region_usage("FLASH", LAYOUT, sections, segments), 0x50
|
|
)
|
|
self.assertEqual(ANALYZER.region_usage("RAM", LAYOUT, sections, segments), 0x10)
|
|
|
|
def test_rejects_loadable_virtual_and_physical_spans_outside_memory(self):
|
|
segments = [
|
|
ANALYZER.LoadSegment(0x09000000, 0x08000000, 4, 4),
|
|
ANALYZER.LoadSegment(0x20000000, 0x09000000, 4, 4),
|
|
]
|
|
|
|
errors = ANALYZER.validation_errors(LAYOUT, [], segments)
|
|
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()
|