diff --git a/components/mdns/mdns_browser.c b/components/mdns/mdns_browser.c index 9cca8026fe..d5fae3430c 100644 --- a/components/mdns/mdns_browser.c +++ b/components/mdns/mdns_browser.c @@ -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) { @@ -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; } @@ -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) { diff --git a/components/mdns/mdns_querier.c b/components/mdns/mdns_querier.c index 06e46d80f4..aeaf738de1 100644 --- a/components/mdns/mdns_querier.c +++ b/components/mdns/mdns_querier.c @@ -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) { diff --git a/components/mdns/mdns_receive.c b/components/mdns/mdns_receive.c index 96ae09ccac..6bf311351d 100644 --- a/components/mdns/mdns_receive.c +++ b/components/mdns/mdns_receive.c @@ -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; } @@ -829,7 +829,8 @@ 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) { @@ -837,7 +838,8 @@ static void mdns_parse_packet(mdns_rx_packet_t *packet) 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); @@ -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) diff --git a/components/mdns/private_include/mdns_browser.h b/components/mdns/private_include/mdns_browser.h index 33e585bbbc..b65bf67b9c 100644 --- a/components/mdns/private_include/mdns_browser.h +++ b/components/mdns/private_include/mdns_browser.h @@ -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 */ diff --git a/components/mdns/private_include/mdns_querier.h b/components/mdns/private_include/mdns_querier.h index 50467121fe..0c71552228 100644 --- a/components/mdns/private_include/mdns_querier.h +++ b/components/mdns/private_include/mdns_querier.h @@ -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 */ @@ -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) diff --git a/components/mdns/tests/host_unit_test/CMakeLists.txt b/components/mdns/tests/host_unit_test/CMakeLists.txt index 493509559a..5c8b9b0a10 100644 --- a/components/mdns/tests/host_unit_test/CMakeLists.txt +++ b/components/mdns/tests/host_unit_test/CMakeLists.txt @@ -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) diff --git a/components/mdns/tests/host_unit_test/README.md b/components/mdns/tests/host_unit_test/README.md new file mode 100644 index 0000000000..13946ec20f --- /dev/null +++ b/components/mdns/tests/host_unit_test/README.md @@ -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. diff --git a/components/mdns/tests/host_unit_test/main.c b/components/mdns/tests/host_unit_test/main.c index a507fba9ad..d89f4773bf 100644 --- a/components/mdns/tests/host_unit_test/main.c +++ b/components/mdns/tests/host_unit_test/main.c @@ -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 */ @@ -8,14 +8,22 @@ #include #include #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 } }; @@ -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) diff --git a/components/mdns/tests/host_unit_test/stubs/esp_assert.h b/components/mdns/tests/host_unit_test/stubs/esp_assert.h index 989f6cf277..3ddf440358 100644 --- a/components/mdns/tests/host_unit_test/stubs/esp_assert.h +++ b/components/mdns/tests/host_unit_test/stubs/esp_assert.h @@ -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 diff --git a/components/mdns/tests/host_unit_test/unity/test_receiver/test_receiver.c b/components/mdns/tests/host_unit_test/unity/test_receiver/test_receiver.c index 698c850362..ac111ce1da 100644 --- a/components/mdns/tests/host_unit_test/unity/test_receiver/test_receiver.c +++ b/components/mdns/tests/host_unit_test/unity/test_receiver/test_receiver.c @@ -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 */ @@ -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) { @@ -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"); @@ -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(); }