#!/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"}]}, } }, { "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), ANALYZER.Section(".debug_info", 100, 0, 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), ANALYZER.Section(".bss", 0x40, 0x20000000, True), ANALYZER.Section(".debug_info", 0x1000, 0, False), ANALYZER.Section(".empty", 0, 0xDEADBEEF, True), ] 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)] crossing = [ANALYZER.Section(".text", 2, 0x080000FF, 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), ANALYZER.Section(".data", 0x10, 0x20000000, True), ] 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)) if __name__ == "__main__": unittest.main()