Compare commits

...
Author SHA1 Message Date
J. Nick Koston e6764f3177 [esp8266] Keep libsodium's SHA-256 round constants in flash 2026-09-05 16:08:38 +02:00
4 changed files with 121 additions and 0 deletions
+2
View File
@@ -321,6 +321,7 @@ async def to_code(config: ConfigType) -> None:
"pre:exclude_updater.py",
"pre:exclude_waveform.py",
"pre:relocate_ratetable.py",
"pre:relocate_sodium_sha256.py",
]
if not enable_scanf_float:
extra_scripts.append("pre:remove_float_scanf.py")
@@ -463,6 +464,7 @@ def copy_files() -> None:
"exclude_waveform",
"remove_float_scanf",
"relocate_ratetable",
"relocate_sodium_sha256",
):
copy_file_if_changed(
dir / f"{script}.py.script",
@@ -24,6 +24,40 @@ _RATETABLE_COMMENT = (
# "_dport0_data_start" line in the earlier .dport0.data section
_RATETABLE_ANCHOR = re.compile(r"^\s*_data_start = ABSOLUTE\(\.\);", re.MULTILINE)
# Move libsodium's SHA-256 round constants from DRAM to flash. The Arduino
# core keeps .rodata in DRAM because flash only allows aligned 32-bit reads,
# but Krnd is a uint32_t[64] that the transform only ever reads word-wise, so
# it is safe in flash and frees 256 bytes of DRAM on every build that links
# libsodium (api or ota encryption). The rule goes inside .irom0.text, which
# the linker script places before the DRAM .rodata rules, so it wins.
SODIUM_SHA256_RULE = "*hash_sha256_cp.c.o(.rodata.Krnd)"
_SODIUM_SHA256_COMMENT = "/* ESPHome: libsodium SHA-256 round constants are read word-wise, keep them in flash */"
_SODIUM_SHA256_ANCHOR = re.compile(
r"^\s*_irom0_text_start = ABSOLUTE\(\.\);", re.MULTILINE
)
def relocate_sodium_sha256(content: str) -> str:
"""Insert the libsodium round-constant flash rule into a generated common
linker script."""
if SODIUM_SHA256_RULE in content:
return content
match = _SODIUM_SHA256_ANCHOR.search(content)
if match is None:
raise RuntimeError(
"'_irom0_text_start' anchor not found in the generated linker script; "
"cannot move the libsodium SHA-256 constants to flash "
"(has the Arduino core linker script changed?)"
)
insert_pos = match.end()
return (
content[:insert_pos]
+ f"\n {_SODIUM_SHA256_COMMENT}"
+ f"\n {SODIUM_SHA256_RULE}"
+ content[insert_pos:]
)
# Memory sizes for testing mode (allow larger builds for CI component grouping)
TESTING_IRAM_SIZE = "0x200000" # 2MB
TESTING_DRAM_SIZE = "0x200000" # 2MB
@@ -0,0 +1,57 @@
# pylint: disable=E0602
Import("env") # noqa
# Move libsodium's SHA-256 round constants from DRAM to flash
#
# The Arduino core linker script keeps every .rodata input section in DRAM,
# because flash-mapped memory only allows aligned 32-bit reads and most
# tables are read byte-wise. libsodium's Krnd (crypto_hash/sha256) is a
# uint32_t[64] that SHA256_Transform only reads word-wise, so it is safe in
# flash; every build that links libsodium (api or ota encryption) gets 256
# bytes of DRAM back. The rule is placed inside the .irom0.text output
# section, which the linker script lists before the DRAM .rodata rules, so it
# claims the section first. Mirrored in build_surgery.py for the native
# toolchain; keep both in sync.
import re
from os.path import join
RULE = "*hash_sha256_cp.c.o(.rodata.Krnd)"
ANCHOR = re.compile(r"^\s*_irom0_text_start = ABSOLUTE\(\.\);", re.MULTILINE)
def relocate_sodium_sha256(source, target, env):
"""Insert the flash rule into the generated linker script.
Runs as a pre-action of the link step; the linker script is a declared
dependency of the elf, so it has already been generated at this point.
"""
ld_path = join(env.subst("$BUILD_DIR"), "ld", "local.eagle.app.v6.common.ld")
with open(ld_path, encoding="utf-8") as f:
contents = f.read()
if RULE in contents:
return # Already patched (incremental build)
match = ANCHOR.search(contents)
if match is None:
raise RuntimeError(
f"ESPHome: '_irom0_text_start' anchor not found in {ld_path}; "
"cannot move the libsodium SHA-256 constants to flash "
"(has the Arduino core linker script changed?)"
)
insert_pos = match.end()
patched = (
contents[:insert_pos]
+ "\n /* ESPHome: libsodium SHA-256 round constants are read word-wise, keep them in flash */"
+ f"\n {RULE}"
+ contents[insert_pos:]
)
with open(ld_path, "w", encoding="utf-8") as f:
f.write(patched)
print("ESPHome: Moved libsodium SHA-256 constants to flash (256 bytes of DRAM)")
# Register the callback to run before the link step
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", relocate_sodium_sha256)
@@ -12,8 +12,10 @@ from esphome.components.esp8266 import build_surgery
from esphome.components.esp8266.boards import BOARDS, ESP8266_BOARD_BUILD
from esphome.components.esp8266.build_surgery import (
RATETABLE_RULE,
SODIUM_SHA256_RULE,
apply_testing_memory_patches,
relocate_ratetable,
relocate_sodium_sha256,
segment_length,
)
@@ -27,6 +29,17 @@ _COMMON_LD_SNIPPET = """\
_data_start = ABSOLUTE(.);
*(.data)
} >dram0_0_seg :dram0_0_phdr
.irom0.text : ALIGN(4)
{
_irom0_text_start = ABSOLUTE(.);
*(.rodata._ZTV*) /* C++ vtables */
} >irom0_0_seg :irom0_0_phdr
.rodata : ALIGN(4)
{
_rodata_start = ABSOLUTE(.);
*(.rodata)
*(.rodata.*)
} >dram0_0_seg :dram0_0_phdr
"""
# Shaped like the real SDK flash ld scripts: no iram1_0_seg (that lives in
@@ -61,6 +74,21 @@ def test_relocate_ratetable_inserts_after_data_start() -> None:
assert relocate_ratetable(patched) == patched
def test_relocate_sodium_sha256_inserts_in_irom0_text() -> None:
patched = relocate_sodium_sha256(_COMMON_LD_SNIPPET)
assert SODIUM_SHA256_RULE in patched
# Inside .irom0.text, ahead of the DRAM .rodata rules that would win otherwise
assert patched.index("_irom0_text_start") < patched.index(SODIUM_SHA256_RULE)
assert patched.index(SODIUM_SHA256_RULE) < patched.index("*(.rodata)")
# Idempotent on an already-patched script
assert relocate_sodium_sha256(patched) == patched
def test_relocate_sodium_sha256_requires_anchor() -> None:
with pytest.raises(RuntimeError, match="_irom0_text_start"):
relocate_sodium_sha256("SECTIONS { }")
def test_relocate_ratetable_requires_anchor() -> None:
with pytest.raises(RuntimeError, match="_data_start"):
relocate_ratetable("SECTIONS { }")