Replace argument recovery with direct build targets. Run host, target, board, coverage, Miri and selected Kani checks while retaining source identities, logs and artifacts, including failed attempts. Keep the PID proof diagnostic exception explicit and match its exact identity alongside passing contract assertions and covers. Validation: final-source verification matrix, assembled workflow tests and documentation build.
258 lines
9.8 KiB
Python
258 lines
9.8 KiB
Python
#!/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 <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],
|
|
)
|
|
|
|
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()
|