Apply Phase 9D security mitigations

- Add fail-closed wolfSSL small-math policy and vectors
- Backport DHCP, EMS, and X.509 allocation fixes
- Extend source override validation and operational documentation
This commit is contained in:
2026-09-15 23:06:23 +02:00
parent cdc9c7335a
commit c010e1a1d5
22 changed files with 1562 additions and 38 deletions
+55 -9
View File
@@ -15,6 +15,9 @@ configuration; there is no unpatched fallback or automatic hash repinning.
Derived **full files**, retaining the original copyright/license notices, live
only at `<CMAKE_BINARY_DIR>/security_overrides/<entry-name>/<basename>`.
Every generated source is centrally prefixed with an explicit
`Modified by the ESP32_serial_swiss_army_knife project on 2026-09-15` notice.
This is separate from, and does not rewrite or replace, upstream notices.
The manifest and derived files are atomically replaced only when their bytes
change. Output paths cannot escape the binary tree or alias SDK/source files.
CMake tracks the generator, version header, originals, and derived sources for
@@ -51,7 +54,7 @@ Source generator expressions are rejected rather than guessed through.
ECDSA/AES/GCM/SHA features are compile-guarded. IDF dynamic buffers are rejected
because their destructor bypasses the audited upstream record-buffer wipe.
`set_client_config()` and the shared handle/setup path are unchanged.
- `wolfssl__wolfssh:src/internal.c`: the fourth override pins wolfSSH 1.4.20's
- `wolfssl__wolfssh:src/internal.c`: pins wolfSSH 1.4.20's
original source SHA256 to
`81ff1f9166708abd5c2911e9fe57c0aee01c88b5d3f68c909ee8a856d37f36a9`.
`GetSize()` bounds password and new-password fields before authentication;
@@ -61,6 +64,26 @@ Source generator expressions are rejected rather than guessed through.
an async secret-lifetime/wipe guarantee. Generated parser/control-flow tests
live in `tests/wolfssh_auth_contract/`.
- `lwip:apps/dhcpserver/dhcpserver.c`: PAD/END handling, length-byte and
declared-payload bounds, minimum message-type/requested-IP lengths, validated
advancement. Backport of official
[d51b1076092487e533eadf8b48c9c8579d3a6712](https://github.com/espressif/esp-idf/commit/d51b1076092487e533eadf8b48c9c8579d3a6712.patch).
The only behavioral-code deviation is using equivalent remaining-length
comparisons rather than constructing pointers beyond the input object.
Original copyright years stay unchanged under the separate modification notice.
- Nested `mbedtls:ssl_tls.c`: propagate `calc_verify()` failure immediately,
before PRF/master-secret derivation. Exact TLS 1.2 hunk of
[f595df4569c1a1650ad9d077e2f2e819e9f1dddb](https://github.com/Mbed-TLS/mbedtls/commit/f595df4569c1a1650ad9d077e2f2e819e9f1dddb.patch).
EMS remains enabled. This does **not** backport that commit's TLS 1.3 hunk.
- Nested `mbedx509:x509_create.c`: reject known-attribute OID allocation failure
before `memcpy()`, returning `MBEDTLS_ERR_X509_ALLOC_FAILED`. Exact guard from
[bfaf4a47fd33da860796feaba6235847acb71127](https://github.com/Mbed-TLS/mbedtls/commit/bfaf4a47fd33da860796feaba6235847acb71127.patch).
These three patches were fetched from the official repositories and compared
with the installed pinned sources on 2026-09-15. No dependency versions or
existing original-source hashes changed. The optional WS subprotocol backport
and separate ASN.1 repeated-OID/empty-value correction are **not** implemented.
Clients that cannot negotiate this server profile will no longer connect.
Live interoperability and resource/latency testing remain hardware gates.
These corrections do not claim comprehensive zeroization of every TLS/library
@@ -75,14 +98,18 @@ Add an `Entry` to `tools/security_overrides.py:ENTRIES` with:
- `root="idf"` for installed IDF sources, or `root="project"` for project/vendor
sources;
- exact relative `source`, full reviewed `sha256`, and a tuple of `Edit(old,new)`
exact-once substitutions.
exact-once substitutions;
- optional `target` for the explicitly supported `mbedtls`, `mbedx509` or
`mbedcrypto` nested library under component `mbedtls`. Empty selects the
component's `COMPONENT_LIB` as before. CMake validates the nested target exists,
is not imported/an alias, and belongs to the installed mbedTLS library directory.
`render_entry()` validates/patches an entry; `generate()` accepts an explicit
entry tuple as well as the default registry. The manifest maps each entry to
its component, original and derived source. CMake's
`sak_security_replace_source(component original generated)` handles replacement
without backend assumptions. The current registry uses this for three IDF
sources and the project-managed wolfSSH source described above. Update the
its component, optional nested target, original and derived source. CMake's
`sak_security_replace_source(component original generated nested_target)` handles
replacement on the actual source owner, not the IDF mbedTLS wrapper. The current
registry has six IDF sources and one project-managed wolfSSH source. Update the
corresponding library-specific feature/behavior tests when extending the registry. Multiple
sources in the same real component are supported by the replacement function.
@@ -98,7 +125,8 @@ python3 tests/sdk_security_overrides/run.py --build-dir .pio/build/esp32-s3-devk
Optional `--idf-path` selects an existing installed SDK. Tests need host `cc`,
CMake and Ninja; they install nothing, use temporary directories under `.pio/`, and never edit
the selected SDK. The second command also checks the existing real firmware
the selected SDK. Run the second command **after the parent runs `pio run`**;
old generated copies lack the new entries/notice and must not pass. It checks real firmware
Ninja registration: exactly one compilation of each derived source, no original
compilation, and exact generated bytes. It does **not** run a firmware build.
@@ -106,7 +134,8 @@ Coverage:
- Generator full-source hashes, version, missing/duplicate/ambiguous inputs,
exact edit counts, validation-before-output, unchanged-byte/mtime idempotence,
unsafe output rejection, and preserved upstream notices.
unsafe output rejection, centrally applied dated modification notice exactly
once on every source, and preserved upstream opening notices.
- Extracted **patched actual functions**, not reimplemented cleanup logic:
HTTPS allocation failure matrix; handshake failure; post-handshake allocation
failure; HTTPD start failure; normal close/stop; failed stop preserving ownership.
@@ -123,8 +152,25 @@ Coverage:
version limits, renegotiation enabled/compiled-out variants, untouched default
and caller-provided client suites. Every required feature is individually
removed in compile-failure tests; dynamic-buffer enablement also fails.
- Extracted DHCP parser: 196,623 guard-page cases covering empty/lone-code,
every option code/length with truncated and complete payloads, PAD, END, short
type/IP, DISCOVER/REQUEST/DECLINE/RELEASE, renew, matching/mismatching IP.
Compare the remaining-length variant with upstream pointer-form checks on
safely padded backing storage; post-loop state behavior is retained.
- Extracted TLS master calculation: SHA-256/SHA-384-sized transcript doubles,
untouched output and no PRF on hash failure (including unchanged length 64),
successful EMS, PRF failure, non-EMS and resumed-session behavior. This does not
execute actual hashes, the full handshake driver, or its failure destructor.
- Extracted X.509 parser and its actual attribute table/helpers, compiled against
installed mbedTLS headers: fault each allocation across separate subject/issuer
lists, partial-list cleanup, second-attribute OID failure, and successful retry.
Named-data storage/free are allocation-counted doubles; certificate signing,
writer destruction, persistence/publication and old-identity retention are not
exercised here. The three new C harnesses use UBSan trap instrumentation;
this does not require the host UBSan runtime.
- The actual CMake include under fake IDF target discovery, including missing and
duplicate sources/targets. A separate real host compile tests the project-root
duplicate sources/targets, nested target owner validation, and exact Ninja
compilation registration on both `mbedtls` and `mbedx509` (not their wrapper). A separate real host compile tests the project-root
extension, child-directory relative `SOURCES`, quoted and source-specific
includes, source/target flags and per-config source definitions. Changing that
fixture's original file makes the next ordinary build reconfigure and reject
+78
View File
@@ -0,0 +1,78 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
typedef uint8_t u8_t;
typedef uint16_t u16_t;
typedef int16_t s16_t;
typedef struct { uint32_t addr; } ip4_addr_t;
typedef struct { uint32_t client_address; bool renew; } dhcps_t;
struct dhcps_state { u8_t state; };
enum { DHCPS_STATE_IDLE, DHCPS_STATE_ACK, DHCPS_STATE_NAK, DHCPS_STATE_OFFER,
DHCPS_STATE_DECLINE, DHCPS_STATE_RELEASE };
/* SDK_DEFINES */
/* SDK_FUNCTIONS */
static unsigned cases;
static u8_t check(dhcps_t *ctx, const u8_t *data, size_t len)
{
size_t page = (size_t)sysconf(_SC_PAGESIZE);
u8_t *map = mmap(NULL, page * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
assert(map != MAP_FAILED && len <= page);
assert(mprotect(map + page, page, PROT_NONE) == 0);
u8_t *input = map + page - len;
memcpy(input, data, len);
u8_t result = parse_options(ctx, input, (s16_t)len);
/* Upstream's pointer expressions stay within this larger backing object. */
u8_t padded[4096 + 258] = {0};
memcpy(padded, data, len);
assert(result == upstream_parse_options(ctx, padded, (s16_t)len));
assert(memcmp(input, data, len) == 0);
assert(munmap(map, page * 2) == 0);
cases++;
return result;
}
int main(void)
{
dhcps_t ctx = {.client_address = 0x04030201};
u8_t data[300] = {0};
assert(check(&ctx, data, 0) == DHCPS_STATE_IDLE);
for (unsigned code = 0; code < 256; code++) {
data[0] = code;
check(&ctx, data, 1);
for (unsigned length = 0; length < 256; length++) {
data[1] = length;
check(&ctx, data, 2);
check(&ctx, data, 2 + length);
if (length) check(&ctx, data, 1 + length);
}
}
memset(data, 0, sizeof(data));
assert(check(&ctx, data, sizeof(data)) == DHCPS_STATE_IDLE);
u8_t discover[] = {0, 0, 53, 1, 1, 255};
assert(check(&ctx, discover, sizeof(discover)) == DHCPS_STATE_OFFER);
u8_t request[] = {53, 1, 3, 50, 4, 0, 0, 0, 0, 255};
memcpy(request + 5, &ctx.client_address, 4);
assert(check(&ctx, request, sizeof(request)) == DHCPS_STATE_ACK);
request[5] ^= 1;
assert(check(&ctx, request, sizeof(request)) == DHCPS_STATE_NAK);
assert(check(&ctx, request, 3) == DHCPS_STATE_NAK);
ctx.renew = true;
assert(check(&ctx, request, 3) == DHCPS_STATE_ACK);
request[2] = 7;
assert(check(&ctx, request, 3) == DHCPS_STATE_RELEASE);
request[2] = 4;
assert(check(&ctx, request, 3) == DHCPS_STATE_DECLINE);
u8_t short_type[] = {53, 0, 255};
assert(check(&ctx, short_type, sizeof(short_type)) == DHCPS_STATE_IDLE);
for (unsigned len = 0; len < 4; len++) {
u8_t short_ip[] = {50, len, 1, 2, 3};
assert(check(&ctx, short_ip, len + 2) == DHCPS_STATE_IDLE);
}
u8_t end[] = {255, 53, 1, 1};
assert(check(&ctx, end, sizeof(end)) == DHCPS_STATE_IDLE);
printf("DHCP extracted parser: %u guard-page / upstream-equivalence cases PASS\n", cases);
}
+68
View File
@@ -0,0 +1,68 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define MBEDTLS_SSL_EXTENDED_MASTER_SECRET
#define MBEDTLS_SSL_EXTENDED_MS_ENABLED 1
#define MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED -1
#define MBEDTLS_SSL_DEBUG_MSG(...) ((void)0)
#define MBEDTLS_SSL_DEBUG_RET(...) ((void)0)
#define MBEDTLS_SSL_DEBUG_BUF(...) ((void)0)
typedef struct { int unused; } mbedtls_ssl_context;
typedef struct {
int resume, extended_ms;
unsigned char randbytes[64], premaster[128];
size_t pmslen;
int (*calc_verify)(const mbedtls_ssl_context *, unsigned char *, size_t *);
int (*tls_prf)(const unsigned char *, size_t, const char *, const unsigned char *, size_t, unsigned char *, size_t);
} mbedtls_ssl_handshake_params;
static int hash_error, prf_error, hash_calls, prf_calls;
static size_t hash_size, expected_seed;
static void mbedtls_platform_zeroize(void *p, size_t n) { memset(p, 0, n); }
static int verify(const mbedtls_ssl_context *ssl, unsigned char *out, size_t *len)
{
hash_calls++;
if (hash_error) return hash_error; /* Deliberately leave seed_len=64. */
*len = hash_size;
memset(out, 0x23, *len);
return 0;
}
static int prf(const unsigned char *p, size_t n, const char *label,
const unsigned char *seed, size_t len, unsigned char *out, size_t size)
{
prf_calls++;
assert(len == expected_seed && size == 48);
assert(strcmp(label, len == 64 ? "master secret" : "extended master secret") == 0);
for (size_t i = 0; i < len; i++) assert(seed[i] == (len == 64 ? 0x45 : 0x23));
if (prf_error) return prf_error;
memset(out, 0x67, size);
return 0;
}
/* SDK_FUNCTIONS */
int main(void)
{
mbedtls_ssl_context ssl = {0};
for (unsigned sha = 0; sha < 2; sha++) {
hash_size = sha ? 48 : 32;
for (unsigned mode = 0; mode < 5; mode++) {
mbedtls_ssl_handshake_params h = {.calc_verify = verify, .tls_prf = prf,
.pmslen = 32, .extended_ms = mode != 3, .resume = mode == 4};
memset(h.premaster, 0xab, sizeof(h.premaster));
memset(h.randbytes, 0x45, sizeof(h.randbytes));
unsigned char master[48]; memset(master, 0xcd, sizeof(master));
hash_calls = prf_calls = 0;
hash_error = mode == 0 ? -0x1234 : 0;
prf_error = mode == 2 ? -0x2345 : 0;
expected_seed = mode == 3 ? 64 : hash_size;
int ret = ssl_compute_master(&h, master, &ssl);
assert(ret == (mode == 0 ? hash_error : mode == 2 ? prf_error : 0));
assert(hash_calls == (mode == 3 || mode == 4 ? 0 : 1));
assert(prf_calls == (mode == 0 || mode == 4 ? 0 : 1));
for (size_t i = 0; i < sizeof(master); i++)
assert(master[i] == (mode == 1 || mode == 3 ? 0x67 : 0xcd));
for (size_t i = 0; i < sizeof(h.premaster); i++)
assert(h.premaster[i] == (mode == 1 || mode == 3 ? 0 : 0xab));
}
}
puts("EMS extracted master calculation: SHA256/SHA384 error, success, PRF failure, non-EMS, resumption PASS");
}
+89 -12
View File
@@ -10,6 +10,7 @@ import argparse
from dataclasses import replace
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import re
@@ -91,8 +92,16 @@ def generator_tests(idf, work):
for entry in sdk.ENTRIES:
original = source_path(entry, idf).read_bytes()
derived = (binary / "security_overrides" / entry.name / Path(entry.source).name).read_bytes()
assert derived.startswith(original[:original.index(b"*/") + 2])
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 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.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")
@@ -162,6 +171,26 @@ def extracted_tests(idf, binary, work):
run(["cc", "-E", "-x", "c", *["-D" + f for f in FEATURES if f != feature], guard_file], ok=False)
run(["cc", "-E", "-x", "c", *defines, "-DCONFIG_MBEDTLS_DYNAMIC_BUFFER", guard_file], ok=False)
print("TLS feature guard matrix (each required feature + dynamic buffer rejection) PASS")
dhcp = texts["dhcpserver"]
parser = extract(dhcp, "parse_options")
upstream = parser.replace("parse_options(", "upstream_parse_options(", 1)
upstream = upstream.replace("end - optptr < 2", "optptr + 1 >= end")
upstream = upstream.replace("opt_len > end - optptr - 2", "optptr + 2 + opt_len > end")
names = ("DHCP_OPTION_PAD", "DHCP_OPTION_END", "DHCP_OPTION_MSG_TYPE",
"DHCP_OPTION_REQ_IPADDR", "DHCPDISCOVER", "DHCPREQUEST", "DHCPDECLINE", "DHCPRELEASE")
defines = "\n".join(re.search(r"^#define " + name + r"\s+[^\n]+", dhcp, re.M).group() for name in names)
source = (HERE / "dhcp.c").read_text().replace("/* SDK_DEFINES */", defines)
compile_run("dhcp", source.replace("/* SDK_FUNCTIONS */", parser + upstream), work,
["-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
compile_run("ems", (HERE / "ems.c").read_text().replace("/* SDK_FUNCTIONS */",
extract(texts["mbedtls_ssl_tls"], "ssl_compute_master")), work,
["-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
x509 = texts["mbedtls_x509_create"]
functions = x509[x509.index("typedef struct {"):x509.index("int mbedtls_x509_string_to_names(")]
functions += extract(x509, "mbedtls_x509_string_to_names")
compile_run("x509", (HERE / "x509.c").read_text().replace("/* SDK_FUNCTIONS */", functions), work,
["-I", str(idf / "components/mbedtls/mbedtls/include"),
"-fsanitize=undefined", "-fsanitize-undefined-trap-on-error"])
def compile_run(name, source, work, flags=()):
@@ -175,41 +204,86 @@ def compile_run(name, source, work, flags=()):
def cmake_fixture_tests(idf, work):
# Use real component inputs with mock IDF target discovery. No SDK compilation.
fixture = work / "cmake_fixture"; fixture.mkdir()
installed_idf = idf
idf = fixture / "idf"
for rel in ["components/esp_common/include/esp_idf_version.h"] + [e.source for e in sdk.ENTRIES if e.root == "idf"]:
dest = idf / rel; dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(installed_idf / rel, dest)
nested_dir = idf / "components/mbedtls/mbedtls/library"
nested_lines = []
lines = ["cmake_minimum_required(VERSION 3.18)", "project(security_fixture C)",
'set(CMAKE_EXPORT_COMPILE_COMMANDS ON)',
f'set(TEST_IDF "{idf}")',
'function(idf_build_get_property out property)',
' set(${out} "${TEST_IDF}" PARENT_SCOPE)', 'endfunction()',
'function(idf_component_get_property out component property)',
' set(${out} "test_${component}" PARENT_SCOPE)', 'endfunction()']
targets = set()
for e in sdk.ENTRIES:
if e.root == "project":
copied = fixture / e.source
copied.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source_path(e, idf), copied)
lines += [f'add_library(test_{e.component} STATIC "{source_path(e, idf, fixture)}")']
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)
if target not in targets:
owner_lines += [f'add_library({target} STATIC "{registered_source}")']
targets.add(target)
else:
owner_lines += [f'target_sources({target} PRIVATE "{source_path(e, idf, fixture)}")']
lines += ['add_library(test_mbedtls INTERFACE)', f'add_subdirectory("{nested_dir}" nested)']
(nested_dir / "CMakeLists.txt").write_text(
'if(NOT TEST_NESTED_MISSING AND NOT TEST_NESTED_OWNER)\n' + "\n".join(nested_lines) + '\nendif()\n')
lines += ['if(TEST_NESTED_OWNER)',
f'add_library(mbedtls STATIC "{source_path(sdk.ENTRIES[1], idf)}")', 'endif()']
for target in ("mbedtls", "mbedx509"):
entry = next(e for e in sdk.ENTRIES if e.target == target)
lines += [f'if(TEST_{target}_MISSING_SOURCE)', f'set_property(TARGET {target} PROPERTY SOURCES missing.c)', 'endif()',
f'if(TEST_{target}_DUPLICATE)', f'set_property(TARGET {target} APPEND PROPERTY SOURCES "{source_path(entry, idf)}")', 'endif()']
lines += ['if(TEST_MISSING)', f'set_property(TARGET test_{sdk.ENTRIES[0].component} PROPERTY SOURCES missing.c)', 'endif()',
'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:
lines += [f'set_source_files_properties("{source_path(e, idf, fixture)}" PROPERTIES COMPILE_FLAGS "-DSOURCE_FLAG" COMPILE_DEFINITIONS "SOURCE_DEFINE" COMPILE_OPTIONS "-fno-common")']
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:
lines += [f'file(GENERATE OUTPUT "${{CMAKE_BINARY_DIR}}/{e.name}.sources" CONTENT "$<TARGET_PROPERTY:test_{e.component},SOURCES>")',
f'get_property(flags SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}" PROPERTY COMPILE_FLAGS)',
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>")',
f'get_property(flags SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}"{directory} PROPERTY COMPILE_FLAGS)',
'if(NOT flags STREQUAL "-DSOURCE_FLAG")', 'message(FATAL_ERROR "lost compile flags")', 'endif()',
f'get_property(inc SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}" PROPERTY INCLUDE_DIRECTORIES)',
f'get_property(inc SOURCE "${{SAK_SECURITY_{e.name}_GENERATED}}"{directory} PROPERTY INCLUDE_DIRECTORIES)',
f'if(NOT inc MATCHES "{source_path(e, idf, fixture).parent}")', 'message(FATAL_ERROR "lost original quoted include directory")', 'endif()']
(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:
source = (build / (e.name + ".sources")).read_text()
assert source == str(build / "security_overrides" / e.name / Path(e.source).name)
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)
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)
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]:
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")):
for flag, phrase in (("TEST_MISSING", "found 0"), ("TEST_AMBIGUOUS", "found 2"),
("TEST_TARGET_MISSING", "missing component target"),
("TEST_NESTED_MISSING", "missing nested target"),
("TEST_NESTED_OWNER", "unexpected nested target owner"),
("TEST_mbedtls_MISSING_SOURCE", "found 0"),
("TEST_mbedtls_DUPLICATE", "found 2"),
("TEST_mbedx509_MISSING_SOURCE", "found 0"),
("TEST_mbedx509_DUPLICATE", "found 2")):
output = run(["cmake", "-G", "Ninja", "-S", fixture, "-B", work / flag, "-D" + flag + "=ON"], ok=False)
assert phrase in output, output
print("CMake actual include: exact target replacement/properties/reconfigure/fail-closed matrix PASS")
@@ -267,16 +341,19 @@ def extension_fixture_tests(idf, work):
print("Extension mapping + child relative source/includes/flags real compile + automatic mismatch rejection PASS")
def build_registration(build, idf):
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
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)) in line for line in compile_lines), e.name
assert generated.read_bytes() == sdk.render_entry(e, {"idf": idf, "project": ROOT})[1]
print("Real IDF Ninja registration: each generated source once, originals absent, bytes verified PASS")
assert not any(str(source_path(e, idf, project)) in line for line in compile_lines), e.name
target = e.target or (f"test_{e.component}" if project != ROOT else f"__idf_{e.component}")
assert f"CMakeFiles/{target}.dir/" in matches[0], (e.name, matches)
assert generated.read_bytes() == sdk.render_entry(e, {"idf": idf, "project": project})[1]
kind = "Real IDF" if project == ROOT else "CMake fixture"
print(f"{kind} Ninja registration: generated inputs once on exact owner, originals absent, bytes verified PASS")
def main():
+76
View File
@@ -0,0 +1,76 @@
/* SPDX-License-Identifier: GPL-3.0-only */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "mbedtls/x509.h"
#include "mbedtls/asn1write.h"
#include "mbedtls/oid.h"
static unsigned allocations, fail_at, live;
static void *test_calloc(size_t n, size_t size)
{
if (++allocations == fail_at) return NULL;
void *p = calloc(n, size); assert(p); live++; return p;
}
static void test_free(void *p) { if (p) { assert(live); live--; free(p); } }
#define mbedtls_calloc test_calloc
#define mbedtls_free test_free
void mbedtls_asn1_free_named_data_list(mbedtls_asn1_named_data **head)
{
while (*head) {
mbedtls_asn1_named_data *next = (*head)->next;
test_free((*head)->oid.p); test_free((*head)->val.p); test_free(*head);
*head = next;
}
}
/* Storage double: preserve the real parser's allocation/copy/free sequence;
* unrelated repeated-OID replacement behavior is deliberately not modeled. */
mbedtls_asn1_named_data *mbedtls_asn1_store_named_data(mbedtls_asn1_named_data **head,
const char *oid, size_t oid_len, const unsigned char *val, size_t val_len)
{
mbedtls_asn1_named_data *node = test_calloc(1, sizeof(*node));
if (!node) return NULL;
node->oid.p = test_calloc(1, oid_len);
node->val.p = test_calloc(1, val_len);
if (!node->oid.p || !node->val.p) {
test_free(node->oid.p); test_free(node->val.p); test_free(node); return NULL;
}
memcpy(node->oid.p, oid, oid_len); node->oid.len = oid_len;
memcpy(node->val.p, val, val_len); node->val.len = val_len;
node->next = *head; *head = node; return node;
}
int mbedtls_oid_from_numeric_string(mbedtls_asn1_buf *oid, const char *p, size_t n)
{ assert(!"numeric OID outside this focused test"); return -1; }
int mbedtls_asn1_get_len(unsigned char **p, const unsigned char *end, size_t *len)
{ assert(!"hex DER outside this focused test"); return -1; }
/* SDK_FUNCTIONS */
int main(void)
{
/* Subject and issuer each call this same parser; fail each allocation in
* their separate lists, then destroy partial lists and retry from scratch. */
for (unsigned failure = 1; failure <= 8; failure++) {
mbedtls_asn1_named_data *subject = NULL, *issuer = NULL;
allocations = 0; fail_at = failure;
int ret = mbedtls_x509_string_to_names(&subject, "CN=ESP32 SAK ABCDEF");
if (!ret) ret = mbedtls_x509_string_to_names(&issuer, "CN=ESP32 SAK ABCDEF");
assert(ret == MBEDTLS_ERR_X509_ALLOC_FAILED);
mbedtls_asn1_free_named_data_list(&subject);
mbedtls_asn1_free_named_data_list(&issuer);
assert(live == 0);
allocations = 0; fail_at = 0;
assert(mbedtls_x509_string_to_names(&subject, "CN=ESP32 SAK ABCDEF") == 0);
assert(mbedtls_x509_string_to_names(&issuer, "CN=ESP32 SAK ABCDEF") == 0);
assert(subject->val.len == strlen("ESP32 SAK ABCDEF"));
assert(memcmp(subject->val.p, "ESP32 SAK ABCDEF", subject->val.len) == 0);
assert(issuer->val.tag == MBEDTLS_ASN1_UTF8_STRING);
mbedtls_asn1_free_named_data_list(&subject);
mbedtls_asn1_free_named_data_list(&issuer);
assert(live == 0);
}
mbedtls_asn1_named_data *head = NULL;
allocations = 0; fail_at = 5; /* Second known-attribute OID, after a live node. */
assert(mbedtls_x509_string_to_names(&head, "CN=first,O=second") == MBEDTLS_ERR_X509_ALLOC_FAILED);
assert(head && head->val.len == 5);
mbedtls_asn1_free_named_data_list(&head); assert(live == 0);
puts("X509 extracted name parser/helpers: subject/issuer OOM, partial-list cleanup, successful retry PASS");
}