#!/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()