Files
Onur Özkan 62b18d5b2e build: replace argument dispatch and retain source-bound verification
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.
2026-11-07 18:12:11 +03:00

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()