Files
ESP32_Serial_Swiss_Army_Knife/tests/mdns_membership/README.md
T
Commander1024 8902b25d78 Complete Phase 12 dual-stack networking
Add IPv6-aware Wi-Fi state, HTTPS/SSH listeners, mDNS service
reconciliation, and browser Wi-Fi administration.

Include a guarded build-local fix for mDNS 1.12.0 membership handling,
focused regression suites, and Phase 12 acceptance documentation.
2026-09-20 22:35:34 +02:00

8.4 KiB
Raw Blame History

mDNS 1.12.0 multicast membership regression

This is a narrow dependency correctness fix for the lwIP backend, not a general patch framework or a revival of the abandoned Phase 9 patches. Dependency versions, the component manifest/lock, managed sources, socket backend, and application mDNS lifecycle policy are unchanged.

Defects and fix

In the inspected mdns_networking_lwip.c, upstream pcb_if_deinit() (original lines 267277) only leaves a group when the interface's last protocol bit is cleared. Removing one family while the other remains active therefore leaks a membership reference. Restoration joins again; repeated partial transitions accumulate references in lwIP's bounded group-use counter. Deinitializing an already-disabled family can also issue an unmatched leave.

The generated copy checks the requested family's active bit, returns immediately if absent, and leaves that family's group before clearing its bit. It clears ready only after the last family on that interface is removed, and frees the shared PCB only if no other interface remains ready.

In pcb_if_init() (original lines 282301), a successful group join followed by pcb_init() failure was not unwound. The copy attempts a leave before returning the original PCB error. No readiness or protocol bit is published on failure.

These operations still run through upstream's existing lwIP-thread wrappers. No new locks, allocations, task ownership, retry state, or public API are added. Upstream join_group() is unchanged, including its refusal to act when the netif is absent/down. Leaves remain best effort: the patch guarantees balanced leave attempts, not successful lwIP cleanup after an interface disappears. It does not retry a failed leave or retain a PCB solely because leave failed; physical netif teardown remains responsible for its own membership cleanup.

Build integration

  1. Root CMakeLists.txt includes cmake/mdns_membership.cmake after IDF's project(), when component targets and resolved versions are available.
  2. The include obtains espressif__mdns's COMPONENT_DIR, COMPONENT_LIB, and COMPONENT_VERSION via IDF component properties. Socket-backend builds skip the overlay entirely; the fix is only relevant to the lwIP source.
  3. The resolved component version and manifest version must both be exactly 1.12.0. Using IDF's configured Python interpreter, the helper verifies the complete source SHA-256, then performs two exact, unique replacements.
  4. It writes only ${CMAKE_BINARY_DIR}/mdns_membership/mdns_networking_lwip.c. Unchanged output is not rewritten, avoiding gratuitous rebuilds. Everything outside the two replacement regions, including provenance/license headers, remains byte-for-byte intact. The actual upstream file identifies itself as Apache-2.0, copyright 20222025 Espressif; it is not relabeled as GPL. Existing project GPL/license material is untouched.
  5. The include replaces exactly one matching entry in the existing component target's SOURCES property, accepting absolute or component-relative paths. All other source entries and all component compile definitions, include paths, dependencies, and target linkage are retained. The original file is not also compiled. An unexpected source list fails configuration.
  6. The helper, original source, manifest, and generated copy are registered as configure dependencies. Reconfiguration regenerates/verifies the copy; a clean build simply recreates it. The CMake include itself is automatically a CMake input. Version/hash/replacement errors fail configure rather than silently compiling an unpatched dependency.

The integration uses the component's existing target; no whole-component copy, managed in-place edits, dependency overrides, manifest changes, or extra source library are involved. The inspected upstream CMake file offers no per-source substitution option; replacing the target source after project() is the local integration point.

Exact reviewed baseline

  • Component: espressif/mdns, version 1.12.0.
  • Manifest repository: espressif/esp-protocols, components/mdns.
  • Manifest commit: db06b19b7be729c163d346f62ec0eba01047b7f1 (provenance; guards are the resolved/manifest version and complete source hash).
  • Source: managed_components/espressif__mdns/mdns_networking_lwip.c.
  • SHA-256: adc139fa504a925ab644f21f8dce3659927f534e390a176b72b0ae3206c6a3ea.

Run the host tests

From the repository root, on a POSIX host with Python 3, CMake >= 3.16, and a C11 compiler:

./tests/mdns_membership/run.py
# Optional compiler selection (CC is an executable, not a shell command):
CC=clang ./tests/mdns_membership/run.py

The managed component must already be installed. The runner uses temporary directories, does not fetch dependencies, does not build firmware, and disables core dumps for deliberate negative tests. It generates the overlay with the same helper used by configure, extracts the actual patched state declarations and six functions (pcb_init, pcb_deinit, mdns_priv_if_ready, is_any_pcb_in_use, pcb_if_init, pcb_if_deinit), and compiles them with -std=c11 -Wall -Wextra -Werror -pedantic. Only group operations and low-level UDP APIs are mocked; the lifecycle logic under test is not reimplemented.

Coverage:

  • 512 IPv4 loss/restoration cycles while IPv6 stays active, and 512 inverse cycles; exact group references and readiness checked after each transition.
  • Repeated disabled-family deinit, final-family teardown, and duplicate init.
  • Join failure without an unmatched leave or PCB allocation.
  • 512 allocation failures and 512 bind failures per family, with successful joins unwound, no state publication, original error preserved, and recovery.
  • Leave failure on teardown and unwind: one attempt, no repeated disabled leave, original allocation error retained.
  • 512 cycles per family with a second interface holding the shared PCB; exactly one allocation, no premature free, and one final removal.
  • Separate negative controls restoring each original buggy function must fail the very same executable harness.
  • Mock-IDF CMake configure fixtures for absolute/relative source replacement, unchanged unrelated source, repeat configure without rewriting, socket bypass, missing/duplicate networking source rejection, resolved-version rejection, and full-source hash rejection. Manifest-version rejection is tested through the helper CLI. Rejected fresh helper runs must not produce output.
  • Original managed networking bytes are checked unchanged after the run.

These are host unit/configure tests, not a real ESP-IDF integration build or hardware/network test. The real join_group(), lwIP IGMP/MLD counters, scheduling, and network teardown are not exercised by mocks. Run the normal pio run separately (never concurrently with another full build) to validate actual IDF integration. In its generated compile_commands.json, the mDNS networking entry must point to the build-local mdns_membership/mdns_networking_lwip.c, with no original managed networking entry; other mDNS sources must remain managed paths. On device, exercise repeated IPv4-only and IPv6-only loss/restoration while the other family remains active, final network teardown/recovery, and continued mDNS discovery. No hardware result is implied by the host PASS output.

Maintenance / removal

Do not update the expected hash merely to make a new dependency build. On any mismatch, review the entire changed networking source and component CMake, especially protocol state, join/leave behavior, netif-down semantics, and shared PCB lifetime. Establish whether upstream has fixed both defects first. A version bump is a separate explicitly reviewed change; this overlay does not select or upgrade the dependency.

If upstream fixes both paths, remove the root include and these narrowly scoped helper/test files (or replace the tests with suitable upstream coverage), then clean/reconfigure and check that only the upstream source compiles. If a local fix is still necessary, re-audit the exact new source/version, update the guards and exact replacements together, update this provenance record, rerun the host suite with its negative controls, and run a serial full firmware build. Never fall back to unguarded search/replace or silently skip a failed patch. A stale build-local copy is not authority: it must always be reproducible from the managed source plus the reviewed helper.