Add restricted wolfSSH ordering fix

Apply hash-pinned generated edits for CVE-2025-14942 while keeping
wolfSSH 1.4.20 managed sources unchanged. Add the ABI header overlay,
provenance records, and real state-machine interoperability contracts.
This commit is contained in:
2026-09-16 14:04:34 +02:00
parent 4d3bb490c9
commit bea33e1c95
28 changed files with 4653 additions and 64 deletions
+54 -12
View File
@@ -27,6 +27,13 @@ sdk = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = sdk
SPEC.loader.exec_module(sdk)
TLS_ENTRY = next(e for e in sdk.ENTRIES if e.name == "esp_tls_mbedtls")
SOURCE_ENTRIES = tuple(e for e in sdk.ENTRIES if not e.header)
def generated_path(binary, entry):
base = binary / "security_overrides"
return (base / "wolfssh_include/wolfssh" / Path(entry.source).name if entry.header
else base / entry.name / Path(entry.source).name)
def source_path(entry, idf, project=ROOT):
@@ -91,17 +98,32 @@ def generator_tests(idf, work):
assert before == {p: (p.read_bytes(), p.stat().st_mtime_ns) for p in before}
for entry in sdk.ENTRIES:
original = source_path(entry, idf).read_bytes()
derived = (binary / "security_overrides" / entry.name / Path(entry.source).name).read_bytes()
derived = generated_path(binary, entry).read_bytes()
notice = sdk.MODIFICATION_NOTICE.encode()
assert derived.startswith(notice)
assert derived.count(notice) == 1
assert b"Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15" in notice
assert derived[len(notice):].startswith(original[:original.index(b"*/") + 2])
assert original[:original.index(b"*/") + 2] in derived[:2500]
if entry.component == "wolfssl__wolfssh":
assert b"Ordering profile modified 2026-09-16" in derived[:1000]
assert derived != original
expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[0], target="mbedtls"),
{"idf": idf, "project": ROOT}), "invalid nested target")
expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[1], target="unknown"),
{"idf": idf, "project": ROOT}), "invalid nested target")
expect_error(lambda: sdk.render_entry(replace(sdk.ENTRIES[0], header=True),
{"idf": idf, "project": ROOT}), "unaudited header overlay")
project_copy = work / "header_mismatch"
for entry in sdk.ENTRIES:
if entry.root == "project":
copied = project_copy / entry.source
copied.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(ROOT / entry.source, copied)
header = next(e for e in sdk.ENTRIES if e.header)
(project_copy / header.source).write_bytes(b"changed header")
header_failed = work / "header_failed"
expect_error(lambda: sdk.generate(idf, project_copy, header_failed), "SHA256 mismatch")
assert not header_failed.exists(), "header drift must reject the entire source/ABI plan"
expect_error(lambda: sdk.apply_edits("x", (sdk.Edit("missing", "z"),)), "got 0")
expect_error(lambda: sdk.apply_edits("xx", (sdk.Edit("x", "z"),)), "got 2")
expect_error(lambda: sdk.generate(idf, ROOT, binary, ()), "absent")
@@ -136,7 +158,7 @@ def generator_tests(idf, work):
def extracted_tests(idf, binary, work):
texts = {e.name: (binary / "security_overrides" / e.name / Path(e.source).name).read_text() for e in sdk.ENTRIES}
texts = {e.name: generated_path(binary, e).read_text() for e in sdk.ENTRIES}
aux = {}
for rel, expected in AUXILIARY.items():
raw = (idf / rel).read_bytes()
@@ -224,6 +246,8 @@ def cmake_fixture_tests(idf, work):
copied = fixture / e.source
copied.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source_path(e, idf), copied)
if e.header:
continue
target = e.target or f"test_{e.component}"
owner_lines = nested_lines if e.target else lines
registered_source = Path(e.source).name if e.target else source_path(e, idf, fixture)
@@ -245,11 +269,16 @@ def cmake_fixture_tests(idf, work):
'if(TEST_AMBIGUOUS)', f'set_property(TARGET test_{sdk.ENTRIES[0].component} APPEND PROPERTY SOURCES "{source_path(sdk.ENTRIES[0], idf, fixture)}")', 'endif()',
'if(TEST_TARGET_MISSING)', 'function(idf_component_get_property out component property)',
'set(${out} nonexistent PARENT_SCOPE)', 'endfunction()', 'endif()']
for e in sdk.ENTRIES:
for e in SOURCE_ENTRIES:
directory = f' DIRECTORY "{nested_dir}"' if e.target else ''
lines += [f'set_source_files_properties("{source_path(e, idf, fixture)}"{directory} PROPERTIES COMPILE_FLAGS "-DSOURCE_FLAG" COMPILE_DEFINITIONS "SOURCE_DEFINE" COMPILE_OPTIONS "-fno-common")']
lines += [f'include("{ROOT / "cmake/security_overrides.cmake"}")']
for e in sdk.ENTRIES:
(fixture / 'consumer.c').write_text('int consumer(void) { return 0; }\n')
lines += ['add_library(direct_consumer STATIC consumer.c)',
'target_link_libraries(direct_consumer PUBLIC test_wolfssl__wolfssh)',
'add_library(transitive_consumer STATIC consumer.c)',
'target_link_libraries(transitive_consumer PRIVATE direct_consumer)',
f'include("{ROOT / "cmake/security_overrides.cmake"}")']
for e in SOURCE_ENTRIES:
target = e.target or f"test_{e.component}"
directory = f' DIRECTORY "{nested_dir}"' if e.target else ''
lines += [f'file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/{e.name}.sources" CONTENT "$<TARGET_PROPERTY:{target},SOURCES>")',
@@ -260,24 +289,33 @@ def cmake_fixture_tests(idf, work):
(fixture / "CMakeLists.txt").write_text("\n".join(lines) + "\n")
build = work / "cmake_good"
run(["cmake", "-G", "Ninja", "-S", fixture, "-B", build])
for e in sdk.ENTRIES:
for e in SOURCE_ENTRIES:
source = (build / (e.name + ".sources")).read_text()
assert source.split(';').count(str(build / "security_overrides" / e.name / Path(e.source).name)) == 1
assert str(source_path(e, idf, fixture)) not in source
build_registration(build, idf, fixture)
commands = json.loads((build / "compile_commands.json").read_text())
for e in sdk.ENTRIES:
generated = str(build / "security_overrides" / e.name / Path(e.source).name)
for e in SOURCE_ENTRIES:
generated = str(generated_path(build, e))
matches = [c for c in commands if c["file"] == generated]
assert len(matches) == 1, (e.name, matches)
for option in ("-DSOURCE_FLAG", "-DSOURCE_DEFINE", "-fno-common",
str(source_path(e, idf, fixture).parent)):
assert option in matches[0]["command"], (e.name, option, matches)
header = next(e for e in sdk.ENTRIES if e.header)
overlay = str(generated_path(build, header))
consumers = [c for c in commands if c['file'].endswith('/consumer.c') or
'/wolfssh_internal/' in c['file'] or '/wolfssh_ssh/' in c['file']]
assert len(consumers) == 4
for command in consumers:
assert '-include' + overlay in command['command']
assert str(build / 'security_overrides/wolfssh_include') in command['command']
print('PUBLIC forced header/overlay reaches library, direct and transitive consumers PASS')
ninja = (build / "build.ninja").read_text()
for path in [ROOT / "tools/security_overrides.py", idf / "components/esp_common/include/esp_idf_version.h"] + [source_path(e, idf, fixture) for e in sdk.ENTRIES]:
for path in [ROOT / "tools/wolfssh_order/delta.json", ROOT / "tools/security_overrides.py", idf / "components/esp_common/include/esp_idf_version.h"] + [source_path(e, idf, fixture) for e in sdk.ENTRIES]:
assert str(path) in next(line for line in ninja.splitlines() if ": RERUN_CMAKE" in line), path
for flag, phrase in (("TEST_MISSING", "found 0"), ("TEST_AMBIGUOUS", "found 2"),
("TEST_TARGET_MISSING", "missing component target"),
("TEST_TARGET_MISSING", "missing wolfSSH overlay target"),
("TEST_NESTED_MISSING", "missing nested target"),
("TEST_NESTED_OWNER", "unexpected nested target owner"),
("TEST_mbedtls_MISSING_SOURCE", "found 0"),
@@ -345,7 +383,11 @@ def build_registration(build, idf, project=ROOT):
ninja = (build / "build.ninja").read_text()
compile_lines = [line for line in ninja.splitlines() if ": C_COMPILER" in line]
for e in sdk.ENTRIES:
generated = build / "security_overrides" / e.name / Path(e.source).name
generated = generated_path(build, e)
if e.header:
assert not any(str(generated) in line for line in compile_lines)
assert generated.read_bytes() == sdk.render_entry(e, {"idf": idf, "project": project})[1]
continue
matches = [line for line in compile_lines if str(generated) in line]
assert len(matches) == 1, (e.name, matches)
assert not any(str(source_path(e, idf, project)) in line for line in compile_lines), e.name