Bypass the cache when the resolved config enables secure options the snapshot cannot see

This commit is contained in:
J. Nick Koston
2026-09-30 04:17:41 +02:00
parent daf693c3c9
commit f096b56c64
2 changed files with 66 additions and 0 deletions
+23
View File
@@ -73,6 +73,11 @@ def _compute_enabled() -> bool:
# The last configure saw a component customizing the bootloader.
_LOGGER.info("Bootloader cache off: build customizes the bootloader")
return False
resolved = toolchain._load_sdkconfig_json(toolchain._build_dir())
if resolved is not None and _resolved_blocks_cache(resolved):
# Hand edits to the real sdkconfig survive snapshot preservation.
_LOGGER.info("Bootloader cache off: secure options in the tree")
return False
if not os.access(tools := framework.get_idf_tools_path(), os.W_OK):
# A read-only shared prefix would fail the cache on every build.
_LOGGER.info("Bootloader cache off: %s is not writable", tools)
@@ -120,6 +125,20 @@ def _snapshot_blocks_cache(sdkconfig_path: Path) -> bool:
return config.get("CONFIG_APP_REPRODUCIBLE_BUILD", "") in _DISABLED_VALUES
def _resolved_blocks_cache(app_config: dict) -> bool:
"""Secure options in the resolved config; catches hand-edited sdkconfigs.
Key files are inputs the cache key cannot hash; *_SUPPORTED capability
constants are always on and say nothing about what is enabled.
"""
return any(
name.startswith(("SECURE_", "FLASH_ENCRYPTION"))
and not name.endswith("_SUPPORTED")
and bool(value)
for name, value in app_config.items()
)
def tree_uses_cached_bootloader(build_dir: Path) -> bool:
"""Whether a configured tree was set up for the cached bootloader."""
cmakecache = build_dir / "CMakeCache.txt"
@@ -387,6 +406,10 @@ def ensure_cached_bootloader(verbose: bool = False) -> bool:
_LOGGER.info("Bootloader customized by the build; building in-tree")
return False
app_config = toolchain._load_sdkconfig_json(toolchain._build_dir())
if app_config is not None and _resolved_blocks_cache(app_config):
# Configure just resolved a secure option the snapshot cannot see.
_LOGGER.info("Secure options in the resolved config; building in-tree")
return False
offset = _bootloader_offset(app_config)
table_offset = (app_config or {}).get("PARTITION_TABLE_OFFSET")
compiler = toolchain._resolved_c_compiler()
@@ -125,6 +125,35 @@ def test_enabled_project_bootloader_components_disable(tmp_path: Path) -> None:
assert bootloader._compute_enabled() is False
@pytest.mark.parametrize(
("config", "blocks"),
[
({"SECURE_BOOT": True}, True),
({"SECURE_BOOT_SIGNING_KEY": "keys/sb.pem"}, True),
({"FLASH_ENCRYPTION_ENABLED": True}, True),
({"SECURE_BOOT_V1_SUPPORTED": True, "SECURE_BOOT": False}, False),
({"A": "1"}, False),
],
ids=["secure-boot", "signing-key", "flash-enc", "capability-only", "plain"],
)
def test_resolved_blocks_cache(config: dict, blocks: bool) -> None:
"""Key file contents are inputs the key cannot hash; capabilities are not."""
assert bootloader._resolved_blocks_cache(config) is blocks
def test_enabled_resolved_secure_option_disables(tmp_path: Path) -> None:
"""A menuconfig edit to the real sdkconfig never reaches the snapshot."""
_write_snapshot("CONFIG_APP_REPRODUCIBLE_BUILD=y\n")
config_dir = toolchain._build_dir() / "config"
config_dir.mkdir(parents=True, exist_ok=True)
(config_dir / "sdkconfig.json").write_text('{"SECURE_BOOT": true}')
with (
_tools_prefix(tmp_path),
patch.object(build_gen, "idf_macro_matches", return_value=True),
):
assert bootloader._compute_enabled() is False
def test_enabled_recorded_bootloader_hooks_disable(tmp_path: Path) -> None:
"""A customization recorded by the last configure is an unseen input."""
_write_snapshot("CONFIG_APP_REPRODUCIBLE_BUILD=y\n")
@@ -572,6 +601,20 @@ def test_ensure_rechecks_recorded_hooks_after_configure(tmp_path: Path) -> None:
mock_build.assert_not_called()
def test_ensure_bypasses_a_resolved_secure_option(tmp_path: Path) -> None:
"""Configure resolves hand-edited secure options the snapshot cannot see."""
with (
_orchestration_env(tmp_path),
patch.object(bootloader, "_build_standalone") as mock_build,
):
build = CORE.relative_build_path("build")
(build / "config" / "sdkconfig.json").write_text(
'{"SECURE_BOOT_SIGNING_KEY": "sb.pem"}'
)
assert bootloader.ensure_cached_bootloader() is False
mock_build.assert_not_called()
def test_ensure_fails_soft_without_a_hooks_record(tmp_path: Path) -> None:
"""A configure that wrote no record cannot prove the bootloader is stock."""
with (