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
39 changes: 33 additions & 6 deletions components/mdns/mdns_browser.c
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,14 @@ static void sync_browse_result_link_free(mdns_browse_sync_t *browse_sync)
mdns_mem_free(browse_sync);
}

void mdns_priv_browse_sync_free(mdns_browse_sync_t *browse_sync)
{
if (!browse_sync) {
return;
}
sync_browse_result_link_free(browse_sync);
}

void mdns_priv_browse_action(mdns_action_t *action, mdns_action_subtype_t type)
{
if (type == ACTION_RUN) {
Expand Down Expand Up @@ -545,17 +553,35 @@ void mdns_priv_browse_result_add_ip(mdns_browse_t *browse, const char *hostname,
}
}

static bool txt_values_equal(const char *a, const char *b, uint8_t len)
{
if (len == 0) {
return true;
}
if (!a || !b) {
return a == b;
}
return memcmp(a, b, len) == 0;
}

static bool is_txt_item_in_list(mdns_txt_item_t txt, uint8_t txt_value_len, mdns_txt_item_t *txt_list, uint8_t *txt_value_len_list, size_t txt_count)
{
for (size_t i = 0; i < txt_count; i++) {
if (strcmp(txt.key, txt_list[i].key) == 0) {
if (txt_value_len == txt_value_len_list[i] && memcmp(txt.value, txt_list[i].value, txt_value_len) == 0) {
return true;
} else {
// The key value is unique, so there is no need to continue searching.
return false;
if (mdns_utils_str_null_or_empty(txt.key) || mdns_utils_str_null_or_empty(txt_list[i].key)) {
if (mdns_utils_str_null_or_empty(txt.key) != mdns_utils_str_null_or_empty(txt_list[i].key)) {
continue;
}
} else if (strcmp(txt.key, txt_list[i].key) != 0) {
continue;
}
if (txt_value_len != txt_value_len_list[i]) {
return false;
}
if (txt_values_equal(txt.value, txt_list[i].value, txt_value_len)) {
return true;
}
// The key value is unique, so there is no need to continue searching.
return false;
}
return false;
}
Expand Down Expand Up @@ -699,6 +725,7 @@ void mdns_priv_browse_result_add_srv(mdns_browse_t *browse, const char *hostname
!mdns_utils_str_null_or_empty(r->service_type) && !strcasecmp(service, r->service_type) &&
!mdns_utils_str_null_or_empty(r->proto) && !strcasecmp(proto, r->proto)) {
if (mdns_utils_str_null_or_empty(r->hostname) || strcasecmp(hostname, r->hostname)) {
mdns_mem_free((char *)r->hostname);
r->hostname = mdns_mem_strdup(hostname);
r->port = port;
if (!r->hostname) {
Expand Down
16 changes: 16 additions & 0 deletions components/mdns/mdns_querier.c
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,22 @@ void mdns_priv_query_start_stop(void)
mdns_priv_service_unlock();
}

void mdns_priv_search_once_free(mdns_search_once_t *search)
{
if (!search) {
return;
}
queueDetach(mdns_search_once_t, s_search_once, search);
mdns_mem_free(search->instance);
mdns_mem_free(search->service);
mdns_mem_free(search->proto);
vSemaphoreDelete(search->done_semaphore);
if (search->result) {
mdns_priv_query_results_free(search->result);
}
mdns_mem_free(search);
}

void mdns_priv_query_free(void)
{
while (s_search_once) {
Expand Down
10 changes: 6 additions & 4 deletions components/mdns/mdns_receive.c
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ static void mdns_parse_packet(mdns_rx_packet_t *packet)
#endif // CONFIG_MDNS_SKIP_SUPPRESSING_OWN_QUERIES

// Check for the minimum size of mdns packet
if (len <= MDNS_HEAD_ADDITIONAL_OFFSET) {
if (len < MDNS_HEAD_LEN) {
return;
}

Expand Down Expand Up @@ -829,15 +829,17 @@ static void mdns_parse_packet(mdns_rx_packet_t *packet)
goto clear_rx_packet;
}
}
memcpy(browse_result_service, browse_result->service, MDNS_NAME_BUF_LEN);
strncpy(browse_result_service, browse_result->service, MDNS_NAME_BUF_LEN - 1);
browse_result_service[MDNS_NAME_BUF_LEN - 1] = '\0';
if (!browse_result_proto) {
browse_result_proto = (char *)mdns_mem_malloc(MDNS_NAME_BUF_LEN);
if (!browse_result_proto) {
HOOK_MALLOC_FAILED;
goto clear_rx_packet;
}
}
memcpy(browse_result_proto, browse_result->proto, MDNS_NAME_BUF_LEN);
strncpy(browse_result_proto, browse_result->proto, MDNS_NAME_BUF_LEN - 1);
browse_result_proto[MDNS_NAME_BUF_LEN - 1] = '\0';
if (type == MDNS_TYPE_SRV || type == MDNS_TYPE_TXT) {
if (!browse_result_instance) {
browse_result_instance = (char *)mdns_mem_malloc(MDNS_NAME_BUF_LEN);
Expand Down Expand Up @@ -1309,7 +1311,7 @@ static void mdns_parse_packet(mdns_rx_packet_t *packet)
mdns_mem_free(browse_result_instance);
mdns_mem_free(browse_result_service);
mdns_mem_free(browse_result_proto);
mdns_mem_free(out_sync_browse);
mdns_priv_browse_sync_free(out_sync_browse);
}

void mdns_priv_receive_action(mdns_action_t *action, mdns_action_subtype_t type)
Expand Down
5 changes: 5 additions & 0 deletions components/mdns/private_include/mdns_browser.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ typedef struct mdns_browse_staged_ip {
*/
mdns_browse_sync_t *mdns_priv_browse_ensure_sync(mdns_browse_t *browse, mdns_browse_sync_t *sync);

/**
* @brief Free browse sync object and its pending result list
*/
void mdns_priv_browse_sync_free(mdns_browse_sync_t *browse_sync);

/**
* @brief Stage an A/AAAA record to apply after all packet records are parsed
*/
Expand Down
7 changes: 6 additions & 1 deletion components/mdns/private_include/mdns_querier.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
Expand All @@ -24,6 +24,11 @@ void mdns_priv_query_start_stop(void);
*/
void mdns_priv_query_free(void);

/**
* @brief Free one search object (detached or still on the active list)
*/
void mdns_priv_search_once_free(mdns_search_once_t *search);

/**
* @brief Free search results
* @note Called from multiple modules (browser, querier, core)
Expand Down
6 changes: 6 additions & 0 deletions components/mdns/tests/host_unit_test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ elseif(NOT CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
message(WARNING "Missing LIBBSD library. Install libbsd-dev package and/or check linker directories.")
endif()

# Sanitizers for AFL fuzzing (unit tests enable these in unity/enable_testing.cmake)
if(NOT ENABLE_UNIT_TESTS)
target_compile_options(${PROJECT_NAME} PRIVATE -fsanitize=address -fsanitize=undefined)
target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=address -fsanitize=undefined)
endif()

# Enable testing if unit tests are enabled
if(ENABLE_UNIT_TESTS)
include(unity/enable_testing.cmake)
Expand Down
80 changes: 80 additions & 0 deletions components/mdns/tests/host_unit_test/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# mDNS host unit tests and fuzzing

This directory builds the mDNS component as a Linux host binary with stubs for ESP-IDF networking. It supports two modes:

- **Unit tests** — Unity/CMock regression tests (ASan/UBSan enabled)
- **Fuzzing** — AFL++ harness that feeds random packets into the receive path

## Prerequisites

- ESP-IDF installed and `IDF_PATH` set (`. $IDF_PATH/export.sh`)
- `libbsd-dev`
- For unit tests: `ruby` (CMock code generation)
- For fuzzing: AFL++ (`afl-cc`, `afl-fuzz`) and `dnslib` (`pip install dnslib`)

From the repository root:

```bash
cd components/mdns/tests/host_unit_test
```

Run `idf.py reconfigure` once before building. This generates `build/config/` headers used by the host build.

## Unit tests

Available test suites (pass one to `-DUNIT_TESTS=`):

| Suite | Description |
|-------|-------------|
| `test_receiver` | Packet receive / parse path |
| `test_sender` | Packet send path |
| `test_browse` | Browse / TXT comparison regressions |

Example — build and run the receiver tests:

```bash
. $IDF_PATH/export.sh
idf.py reconfigure

mkdir -p build2 && cd build2
cmake -DUNIT_TESTS=test_receiver ..
cmake --build .
ctest --extra-verbose
```

Or run the binary directly:

```bash
./mdns_host_unit_test --test
```

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

## Fuzzer tests

Build the AFL-instrumented harness (no `-DUNIT_TESTS`; uses `main.c`):

```bash
export IDF_PATH=/path/to/esp-idf # required in the fuzz container

cd input && python generate_cases.py && cd ..

cmake -B build2 -S . -G Ninja -DCMAKE_C_COMPILER=afl-cc
cmake --build build2

afl-fuzz -i input -o out -- build2/mdns_host_unit_test
```

The harness reads packets from stdin and exercises IPv4/IPv6 and port 5353/53 combinations. Crashes are written to `out/default/crashes/`.

### Reproducing a crash

Build the non-unit-test binary with a normal compiler, then pass a crash file:

```bash
cmake -B build2 -S .
cmake --build build2
./build2/mdns_host_unit_test out/default/crashes/id_000000,...
```

With sanitizers enabled, ASan/UBSan report buffer overruns and undefined behaviour directly during unit tests and fuzzing.
31 changes: 25 additions & 6 deletions components/mdns/tests/host_unit_test/main.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
Expand All @@ -8,14 +8,22 @@
#include <stdint.h>
#include <unistd.h>
#include "esp_err.h"
#include "mdns.h"
#include "mdns_receive.h"
#include "mdns_responder.h"
#include "mdns_querier.h"
#include "mdns_browser.h"
#include "mdns_mem_caps.h"

esp_err_t mdns_packet_push(esp_ip_addr_t *addr, int port, mdns_if_t tcpip_if, uint8_t*data, size_t len);

mdns_search_once_t *s_a, *s_aaaa, *s_ptr, *s_srv, *s_txt;

static void browse_notifier(mdns_result_t *result)
{
(void)result;
}

void init_responder(void)
{
mdns_ip_addr_t addr = { .addr = { .u_addr = ESP_IPADDR_TYPE_V4 } };
Expand Down Expand Up @@ -48,17 +56,28 @@ void init_responder(void)
s_ptr = mdns_query_async_new("minifritz", "_http", "_tcp", MDNS_TYPE_PTR, 1000, 1, NULL);
s_srv = mdns_query_async_new("fritz", "_http", "_tcp", MDNS_TYPE_SRV, 1000, 1, NULL);
s_txt = mdns_query_async_new("fritz", "_http", "_tcp", MDNS_TYPE_TXT, 1000, 1, NULL);

mdns_browse_new("_http", "_tcp", browse_notifier);
mdns_browse_new("_scanner", "_tcp", browse_notifier);
mdns_browse_new("_sleep", "_udp", browse_notifier);
}

void deinit_responder(void)
{
mdns_query_async_delete(s_a);
mdns_query_async_delete(s_aaaa);
mdns_query_async_delete(s_ptr);
mdns_query_async_delete(s_srv);
mdns_query_async_delete(s_txt);
mdns_priv_search_once_free(s_a);
mdns_priv_search_once_free(s_aaaa);
mdns_priv_search_once_free(s_ptr);
mdns_priv_search_once_free(s_srv);
mdns_priv_search_once_free(s_txt);
mdns_priv_query_free();
mdns_priv_browse_free();
mdns_service_remove_all();
mdns_priv_responder_free();
s_a = NULL;
s_aaaa = NULL;
s_ptr = NULL;
s_srv = NULL;
s_txt = NULL;
}

static void send_packet(bool ip4, bool mdns_port, uint8_t*data, size_t len)
Expand Down
4 changes: 4 additions & 0 deletions components/mdns/tests/host_unit_test/stubs/esp_assert.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
#if __has_include("esp_assert.h")
#include_next "esp_assert.h"
#else
#define ESP_STATIC_ASSERT _Static_assert
#endif
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
Expand All @@ -9,7 +9,7 @@
#include "unity_main.h"
#include "mock_mdns_pcb.h"
#include "mock_mdns_send.h"
#include "create_test_packet.h"
#include "mdns_private.h"

static void test_mdns_hostname_queries(void)
{
Expand Down Expand Up @@ -61,6 +61,27 @@ static void test_mdns_with_answers(void)

}

/*
* Regression test for fa84ee6: packets shorter than MDNS_HEAD_LEN (12) must
* be rejected before reading the additional RR count at offset 10.
* An 11-byte AFL input (id_000005) previously passed the old check and read
* one byte past the RX buffer.
*/
static void test_mdns_reject_short_packet(void)
{
static const uint8_t eleven_byte_packet[] = {
0x00, 0xc0, 0x32, 0x00, 0x01, 0x00, 0x14, 0x00, 0x00, 0x54, 0x01
};

for (size_t len = 0; len < MDNS_HEAD_LEN; len++) {
const uint8_t *data = eleven_byte_packet;
send_packet(true, true, (uint8_t *)data, len);
send_packet(true, false, (uint8_t *)data, len);
send_packet(false, true, (uint8_t *)data, len);
send_packet(false, false, (uint8_t *)data, len);
}
}

static void mdns_priv_create_answer_from_parsed_packet_Callback(mdns_parsed_packet_t* parsed_packet, int cmock_num_calls)
{
printf("callback\n");
Expand Down Expand Up @@ -89,5 +110,7 @@ void run_unity_tests(void)
// Run test with answers
RUN_TEST(test_mdns_with_answers);

RUN_TEST(test_mdns_reject_short_packet);

UNITY_END();
}
Loading