Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 6 additions & 8 deletions components/mdns/mdns_pcb.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -126,25 +126,23 @@ static esp_err_t deinit_pcb(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_proto)
*/
static void restart_pcb(mdns_if_t tcpip_if, mdns_ip_protocol_t ip_protocol)
{
size_t srv_count = 0;
mdns_srv_item_t *a = mdns_priv_get_services();
while (a) {
size_t srv_count = 0;
for (mdns_srv_item_t *s = a; s; s = s->next) {
srv_count++;
a = a->next;
}
if (srv_count == 0) {
// proble only IP
// probe only IP
mdns_priv_init_pcb_probe(tcpip_if, ip_protocol, NULL, 0, true);
return;
}
mdns_srv_item_t *services[srv_count];
size_t i = 0;
a = mdns_priv_get_services();
while (a) {
while (a && i < srv_count) {
services[i++] = a;
a = a->next;
}
mdns_priv_init_pcb_probe(tcpip_if, ip_protocol, services, srv_count, true);
mdns_priv_init_pcb_probe(tcpip_if, ip_protocol, services, i, true);
}

/**
Expand Down
46 changes: 19 additions & 27 deletions components/mdns/mdns_responder.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2015-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand Down Expand Up @@ -163,100 +163,92 @@ static void announce_all_pcbs(mdns_srv_item_t **services, size_t len, bool inclu
static void send_final_bye(bool include_ip)
{
//collect all services to send bye packet
size_t srv_count = 0;
mdns_srv_item_t *a = s_server->services;
while (a) {
size_t srv_count = 0;
for (mdns_srv_item_t *s = a; s; s = s->next) {
srv_count++;
a = a->next;
}
if (!srv_count) {
return;
}
mdns_srv_item_t *services[srv_count];
size_t i = 0;
a = s_server->services;
while (a) {
while (a && i < srv_count) {
services[i++] = a;
a = a->next;
}
mdns_priv_pcb_send_bye_service(services, srv_count, include_ip);
mdns_priv_pcb_send_bye_service(services, i, include_ip);
}

/**
* @brief Stop the responder on all services without instance
*/
static void send_bye_all_pcbs_no_instance(bool include_ip)
{
size_t srv_count = 0;
mdns_srv_item_t *a = s_server->services;
while (a) {
if (!a->service->instance) {
size_t srv_count = 0;
for (mdns_srv_item_t *s = a; s; s = s->next) {
if (!s->service->instance) {
srv_count++;
}
a = a->next;
}
if (!srv_count) {
return;
}
mdns_srv_item_t *services[srv_count];
size_t i = 0;
a = s_server->services;
while (a) {
while (a && i < srv_count) {
if (!a->service->instance) {
services[i++] = a;
}
a = a->next;
}
mdns_priv_pcb_send_bye_service(services, srv_count, include_ip);
mdns_priv_pcb_send_bye_service(services, i, include_ip);
}

void mdns_priv_restart_all_pcbs_no_instance(void)
{
size_t srv_count = 0;
mdns_srv_item_t *a = s_server->services;
while (a) {
if (!a->service->instance) {
size_t srv_count = 0;
for (mdns_srv_item_t *s = a; s; s = s->next) {
if (!s->service->instance) {
srv_count++;
}
a = a->next;
}
if (!srv_count) {
return;
}
mdns_srv_item_t *services[srv_count];
size_t i = 0;
a = s_server->services;
while (a) {
while (a && i < srv_count) {
if (!a->service->instance) {
services[i++] = a;
}
a = a->next;
}
mdns_priv_probe_all_pcbs(services, srv_count, false, true);
mdns_priv_probe_all_pcbs(services, i, false, true);
}

void mdns_priv_restart_all_pcbs(void)
{
mdns_priv_clear_tx_queue();
size_t srv_count = 0;
mdns_srv_item_t *a = s_server->services;
while (a) {
size_t srv_count = 0;
for (mdns_srv_item_t *s = a; s; s = s->next) {
srv_count++;
a = a->next;
}
if (srv_count == 0) {
mdns_priv_probe_all_pcbs(NULL, 0, true, true);
return;
}
mdns_srv_item_t *services[srv_count];
size_t l = 0;
a = s_server->services;
while (a) {
while (a && l < srv_count) {
services[l++] = a;
a = a->next;
}

mdns_priv_probe_all_pcbs(services, srv_count, true, true);
mdns_priv_probe_all_pcbs(services, l, true, true);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion components/mdns/tests/host_unit_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ project(mdns_host_unit_test C)

# Set ENABLE_UNIT_TESTS with a default of OFF
if(NOT DEFINED UNIT_TESTS)
set(UNIT_TESTS "OFF" CACHE STRING "Unit tests: OFF, test_receiver, test_sender")
set(UNIT_TESTS "OFF" CACHE STRING "Unit tests: OFF, test_receiver, test_sender, test_browse, test_pcb")
else()
set(ENABLE_UNIT_TESTS 1)
message(STATUS "Unit testing enabled with UNIT_TESTS=${UNIT_TESTS}")
Expand Down
28 changes: 25 additions & 3 deletions components/mdns/tests/host_unit_test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ Available test suites (pass one to `-DUNIT_TESTS=`):

| Suite | Description |
|-------|-------------|
| `test_receiver` | Packet receive / parse path |
| `test_sender` | Packet send path |
| `test_receiver` | Packet receive / parse path (`mdns_pcb` + `mdns_send` mocked) |
| `test_sender` | Packet send path (`mdns_pcb` mocked) |
| `test_browse` | Browse / TXT comparison regressions |
| `test_pcb` | Real PCB restart/probe service collection (`mdns_send` mocked) |

Example — build and run the receiver tests:

Expand All @@ -48,7 +49,28 @@ Or run the binary directly:
./mdns_host_unit_test --test
```

Repeat with `-DUNIT_TESTS=test_sender` or `-DUNIT_TESTS=test_browse` in a separate build directory.
Repeat with `-DUNIT_TESTS=test_sender`, `test_browse`, or `test_pcb` in a separate build directory.

### What unit tests catch (and what they do not)

ASan/UBSan already catch **definite** out-of-bounds accesses and UB when a buggy path runs. That is why `test_pcb` exists: older suites mocked `mdns_pcb`, so `restart_pcb()` never executed under sanitizers.

They do **not** replace static analysis for *potential* ArrayBound issues (e.g. clang-tidy `clang-analyzer-security.ArrayBound` on count-then-fill VLA loops). Those warnings fire when the analyzer cannot prove a fill loop stays in bounds — often without a concrete ASan crash on a stable service list. Keep clang-tidy (or similar) in CI for that class of defect.

## Mutation testing (Mull)

[Mull](https://mull-project.com) (https://github.com/mull-project/mull) mutates the compiled binary and re-runs tests. Surviving mutants highlight weak assertions or uncovered logic. It complements ASan: good for “would our tests notice if this `<` became `<=`?”, not for inventing race/list-growth scenarios that static ArrayBound found.

Config: `mull.yml` (scopes mutators to `components/mdns/mdns_*.c`).

```bash
# Install Mull for your clang major (see ext/mull/docs/Installation.rst), then:
./scripts/run_mull.sh test_pcb
```

Override paths if needed: `MULL_LLVM_MAJOR`, `MULL_IR_FRONTEND`, `MULL_RUNNER`, `CC`.

Expect some surviving / equivalent mutants on tight bound checks when the service list length matches the VLA size; use the score as a coverage-quality signal, not a pass/fail gate until the suite is broader.

## Fuzzer tests

Expand Down
52 changes: 52 additions & 0 deletions components/mdns/tests/host_unit_test/USING_MULL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Using Mull with mDNS host unit tests

## What is Mull?

Mull is a **mutation testing** tool. It deliberately changes your compiled code in small ways (e.g. `<` → `<=`, `+` → `-`), then re-runs the tests.

- If a test **fails**, that mutant is *killed* — good: the suite noticed the breakage.
- If tests still **pass**, the mutant *survived* — the suite did not cover that logic well (or the change was harmless).

So Mull measures **test quality**, not whether the original code is correct.

## What it brings here

| Tool | Question it answers |
|------|---------------------|
| Unity + ASan | “Does this path crash or misbehave on real inputs?” |
| clang-tidy ArrayBound | “Could this loop write past an array even if we haven’t hit it yet?” |
| **Mull** | “If this check or operator were wrong, would our tests notice?” |

Use Mull to find weak spots in suites like `test_pcb` (PCB restart / service collection). Do not use it as a substitute for sanitizers or static analysis.

## How it is wired

1. **`mull.yml`** — which mutators to apply, and which files count (`mdns_*.c` only; stubs/mocks/tests excluded).
2. **Build with Mull’s clang plugin** — instruments the binary while compiling.
3. **`mull-runner`** — runs the instrumented `mdns_host_unit_test --test` once per mutant.

Helper script (from this directory):

```bash
# Needs Mull installed for your clang major (see https://github.com/mull-project/mull docs)
./scripts/run_mull.sh test_pcb
```

Other suites work the same: `./scripts/run_mull.sh test_receiver`, etc.

By default the script writes **`mull_reports/mdns_<suite>.sqlite`** (no long list of warnings on the console). Set `MULL_CONSOLE=1` if you also want IDE-style warnings printed.

```bash
sqlite3 mull_reports/mdns_test_receiver.sqlite \
"SELECT filename, line_number, mutator FROM mutant WHERE status = 2 LIMIT 20;"
```

(`status = 2` means the mutated run still **Passed** → mutant *survived*. Killed mutants are `status <> 2`.)

Overrides if paths differ: `MULL_LLVM_MAJOR`, `MULL_IR_FRONTEND`, `MULL_RUNNER`, `CC`.

## Beginner expectations

- First runs will show **surviving mutants** — normal; treat the score as a signal to add assertions or coverage, not as a hard CI gate yet.
- Some survivors are **equivalent** (the change cannot affect observed behavior). That is OK.
- Prefer running Mull on a focused suite (`test_pcb`) before the whole world — faster feedback.
24 changes: 24 additions & 0 deletions components/mdns/tests/host_unit_test/mull.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Mull mutation-testing config for mdns host unit tests.
# See README.md "Mutation testing (Mull)" for build/run steps.
#
# Scope: production mdns sources only. Stubs, Unity/CMock, and generated mocks
# are excluded so the score reflects real component logic.

mutators:
- cxx_boundary
- cxx_comparison
- cxx_increment
- cxx_arithmetic

includePaths:
- .*/components/mdns/mdns_.*\.c$

excludePaths:
- .*/tests/.*
- .*/stubs/.*
- .*/mocks/.*
- .*/unity/.*
- .*/size_compare/.*
- .*/BBP#.*

quiet: true
71 changes: 71 additions & 0 deletions components/mdns/tests/host_unit_test/scripts/run_mull.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Optional mutation-testing pass for a host unit-test suite using Mull.
#
# Prerequisites:
# - clang matching the installed mull-ir-frontend / mull-runner (see ext/mull)
# - Mull plugins on the library path (e.g. /usr/lib/mull-ir-frontend-18)
# - IDF_PATH exported; ruby + libbsd as for normal unit tests
#
# Usage:
# ./scripts/run_mull.sh test_pcb
# ./scripts/run_mull.sh test_receiver
set -euo pipefail

SUITE="${1:-test_pcb}"
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BUILD_DIR="${ROOT}/build_mull_${SUITE}"

# Prefer an explicit LLVM major if set; otherwise try common versions.
LLVM_MAJOR="${MULL_LLVM_MAJOR:-18}"
FRONTEND="${MULL_IR_FRONTEND:-/usr/lib/mull-ir-frontend-${LLVM_MAJOR}}"
RUNNER="${MULL_RUNNER:-mull-runner-${LLVM_MAJOR}}"

if [[ ! -f "${FRONTEND}" ]]; then
echo "Mull IR frontend not found at ${FRONTEND}" >&2
echo "Install Mull for clang-${LLVM_MAJOR}, or set MULL_IR_FRONTEND / MULL_LLVM_MAJOR." >&2
echo "Source tree with docs: ${ROOT}/../../../../ext/mull" >&2
exit 1
fi

if ! command -v "${RUNNER}" >/dev/null 2>&1; then
echo "mull-runner not found (${RUNNER}). Set MULL_RUNNER if needed." >&2
exit 1
fi

export CC="${CC:-clang-${LLVM_MAJOR}}"
if ! command -v "${CC}" >/dev/null 2>&1; then
CC=clang
fi

mkdir -p "${BUILD_DIR}"
cmake -S "${ROOT}" -B "${BUILD_DIR}" \
-DUNIT_TESTS="${SUITE}" \
-DCMAKE_C_COMPILER="${CC}" \
-DCMAKE_C_FLAGS="-O0 -fpass-plugin=${FRONTEND} -g -grecord-command-line -fno-omit-frame-pointer" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address -fsanitize=undefined"

cmake --build "${BUILD_DIR}" -j"$(nproc)"

export MULL_CONFIG="${ROOT}/mull.yml"

REPORT_DIR="${ROOT}/mull_reports"
mkdir -p "${REPORT_DIR}"
REPORT_NAME="mdns_${SUITE}"

# Default: SQLite report on disk (no flood of IDE warnings).
# Set MULL_CONSOLE=1 to also print clang-style survivor warnings.
REPORTERS=(--reporters SQLite)
if [[ "${MULL_CONSOLE:-0}" == "1" ]]; then
REPORTERS+=(--reporters IDE)
fi

"${RUNNER}" --allow-surviving \
"${REPORTERS[@]}" \
--report-dir "${REPORT_DIR}" \
--report-name "${REPORT_NAME}" \
"${BUILD_DIR}/mdns_host_unit_test" -- --test

echo
echo "Report: ${REPORT_DIR}/${REPORT_NAME}.sqlite"
echo "Browse survivors, e.g.:"
echo " sqlite3 ${REPORT_DIR}/${REPORT_NAME}.sqlite \"SELECT filename, line_number, mutator FROM mutant WHERE status = 2 LIMIT 20;\""
Loading