mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
Bypass the cache when the resolved config enables secure options the snapshot cannot see
This commit is contained in:
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user