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.
This commit is contained in:
2026-09-15 20:11:40 +02:00
parent f40c09c11a
commit 436c27adb1
11 changed files with 267 additions and 30 deletions
+17
View File
@@ -0,0 +1,17 @@
# Crash/debug build-policy regression
Run from the repository root:
```sh
python3 tests/security_build_policy/run.py
pio run
python3 tests/security_build_policy/run.py --sdkconfig-header .pio/build/esp32-s3-devkitc-1-n16r8/config/sdkconfig.h
```
Requires Python 3.9+ and a host C compiler (`cc`, or `CC`). If a compiler wrapper cannot write its cache in a sandbox, prefix the Python commands with `CCACHE_DISABLE=1`.
The harness compiles the actual `src/security_build_policy.c`, not a reimplementation. It checks 17 cases: safe undefined/zero disabled booleans, absent configuration, each absent/zero required setting, each forbidden setting enabled independently, and the tracked defaults. It also checks production CMake registration. The optional generated-header check adds an eighteenth case and must follow a successful firmware build; a stale header is not evidence of current firmware configuration.
Expected-invalid fixtures must fail with the policy's diagnostic. Compiler execution failures or unrelated diagnostics do not count as successful negative tests. No firmware is flashed and no secrets, partition contents or device memory are collected.
These tests do not exercise actual panic output, reboot/recovery behavior, physical debugging or log redaction. See [Phase 9 policy and target gates](../../docs/security_hardening.md).
+88
View File
@@ -0,0 +1,88 @@
#!/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()