Files
ESP32_Serial_Swiss_Army_Knife/tests/security_build_policy/run.py
T
Commander1024 436c27adb1 Enforce crash-safe build policy
Add compile-time checks for silent reboot, disabled core dumps, and
disabled debugger-aware panic handling. Include regression coverage,
hardening guidance, and update Phase 8/9 project status.
2026-09-15 20:11:40 +02:00

89 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Compile the production crash-policy guard against safe and unsafe configs."""
import argparse
import os
from pathlib import Path
import shlex
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[2]
SOURCE = ROOT / "src/security_build_policy.c"
REQUIRED = (
"CONFIG_ESP_COREDUMP_ENABLE_TO_NONE",
"CONFIG_ESP_SYSTEM_PANIC_SILENT_REBOOT",
)
FORBIDDEN = (
"CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH",
"CONFIG_ESP_COREDUMP_ENABLE_TO_UART",
"CONFIG_ESP_COREDUMP_ENABLE",
"CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT",
"CONFIG_ESP_SYSTEM_PANIC_PRINT_REBOOT",
"CONFIG_ESP_SYSTEM_PANIC_GDBSTUB",
"CONFIG_ESP_SYSTEM_GDBSTUB_RUNTIME",
"CONFIG_ESP_DEBUG_OCDAWARE",
"CONFIG_FREERTOS_DEBUG_OCDAWARE",
)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sdkconfig-header", type=Path,
help="also check an actual generated sdkconfig.h")
args = parser.parse_args()
compiler = shlex.split(os.environ.get("CC", "cc"))
count = 0
with tempfile.TemporaryDirectory(prefix="security-build-policy-") as directory:
header = Path(directory) / "sdkconfig.h"
def check(name, config, accepted):
nonlocal count
header.write_text(config)
result = subprocess.run(
compiler + ["-std=c11", "-Wall", "-Wextra", "-Werror",
"-fsyntax-only", "-I", directory, str(SOURCE)],
capture_output=True, text=True, timeout=20,
)
if (result.returncode == 0) != accepted:
raise AssertionError(f"{name}: unexpected compiler result\n{result.stderr}")
if not accepted and "Security policy:" not in result.stderr:
raise AssertionError(f"{name}: failed for an unrelated reason\n{result.stderr}")
count += 1
def defines(values):
return "".join(f"#define {name} {value}\n" for name, value in values.items())
baseline = dict.fromkeys(REQUIRED, 1)
check("supported baseline", defines(baseline), True)
check("explicit disabled options", defines(baseline | dict.fromkeys(FORBIDDEN, 0)), True)
check("missing config", "", False)
for name in REQUIRED:
missing = baseline.copy()
del missing[name]
check(f"missing {name}", defines(missing), False)
check(f"disabled {name}", defines(baseline | {name: 0}), False)
for name in FORBIDDEN:
check(f"enabled {name}", defines(baseline | {name: 1}), False)
# Verify tracked defaults select the policy, rather than just accepting
# a synthetic fixture. Disabled Kconfig booleans are absent from headers.
defaults = (ROOT / "sdkconfig.defaults").read_text()
values = {}
for line in defaults.splitlines():
if line.startswith("CONFIG_") and "=" in line:
name, value = line.split("=", 1)
if name in REQUIRED + FORBIDDEN:
values[name] = 1 if value == "y" else 0
check("tracked defaults", defines(values), True)
if '"security_build_policy.c"' not in (ROOT / "src/CMakeLists.txt").read_text():
raise AssertionError("production build does not register policy source")
if args.sdkconfig_header:
check("resolved SDK configuration", args.sdkconfig_header.read_text(), True)
print(f"PASS: {count} compile-policy cases; production source registered")
if __name__ == "__main__":
main()