#!/usr/bin/env python3 """Synthetic regression tests for strict firmware-memory validation.""" import importlib.util import json from pathlib import Path import contextlib import io import tempfile import unittest from unittest.mock import patch def load_analyzer_module(): spec = importlib.util.spec_from_file_location( "analyze_fw", Path(__file__).with_name("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 " 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], ) def test_cli_requires_and_checks_executable_sram_code(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) elf, memory = root / "firmware", root / "memory.x" elf.touch() memory.write_text( "MEMORY {\nFLASH : ORIGIN = 0x08000000, LENGTH = 256\nRAM : ORIGIN = 0x20000000, LENGTH = 128\n}\n" ) segments = [ANALYZER.LoadSegment(0x08000000, 0x08000000, 0x20, 0x20)] for executable in (False, True): with self.subTest(executable=executable): sections = [ ANALYZER.Section(".data", 0x10, 0x20000000, True, executable) ] with ( patch("sys.argv", ["analyze-fw.py", str(elf), str(memory)]), patch.object(ANALYZER, "llvm_tool", return_value="llvm-tool"), patch.object( ANALYZER, "elf_layout", return_value=(sections, segments), ), patch.object( ANALYZER, "run", return_value="20000000: bx lr" ) as run, contextlib.redirect_stdout(io.StringIO()), ): if executable: ANALYZER.main() self.assertEqual(run.call_count, 2) else: with self.assertRaisesRegex( ValueError, "Flash-operation placement validation failed", ): ANALYZER.main() def test_cli_rejects_common_memory_bounds(self): with tempfile.TemporaryDirectory() as temporary: elf, memory = Path(temporary) / "firmware", Path(temporary) / "memory.x" elf.touch() memory.write_text( "FLASH : ORIGIN = 0x08000000, LENGTH = 256\nRAM : ORIGIN = 0x20000000, LENGTH = 128\n" ) with ( patch( "sys.argv", ["analyze-fw.py", str(elf), str(memory)], ), patch.object(ANALYZER, "llvm_tool", return_value="llvm-readobj"), patch.object(ANALYZER, "run", return_value=""), patch.object( ANALYZER, "elf_layout", return_value=( [ANALYZER.Section(".data", 0x81, 0x20000000, True, False)], [ANALYZER.LoadSegment(0x20000000, 0x080000FF, 2, 0x81)], ), ), contextlib.redirect_stdout(io.StringIO()), ): with self.assertRaisesRegex( ValueError, "firmware memory validation failed" ): ANALYZER.main() if __name__ == "__main__": unittest.main()