diff --git a/.github/workflows/mdns__build-target-test.yml b/.github/workflows/mdns__build-target-test.yml index d222307c79..e6da77ec9b 100644 --- a/.github/workflows/mdns__build-target-test.yml +++ b/.github/workflows/mdns__build-target-test.yml @@ -24,6 +24,11 @@ jobs: shell: bash run: | . ${IDF_PATH}/export.sh + if [[ "${{ matrix.idf_ver }}" == "latest" ]]; then + export EXPECTED_WARNING="warning: unknown kconfig symbol 'EXAMPLE_ETH_PHY_IP101'" + else + export EXPECTED_WARNING="warning: unknown kconfig symbol 'EXAMPLE_ETH_PHY_GENERIC'" + fi python -m pip install idf-build-apps # Build default configs for all targets python ./ci/build_apps.py components/mdns/${{ matrix.test.path }} -r default -d @@ -71,6 +76,22 @@ jobs: - name: Run ${{ matrix.test.app }} application on ${{ matrix.idf_target }} working-directory: components/mdns/${{ matrix.test.path }} run: | + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" + eval "$(pyenv init --path)" + eval "$(pyenv init -)" + if ! pyenv versions --bare | grep -q '^3\.12\.6$'; then + echo "Installing Python 3.12.6..." + pyenv install -s 3.12.6 + fi + if ! pyenv virtualenvs --bare | grep -q '^myenv$'; then + echo "Creating pyenv virtualenv 'myenv'..." + pyenv virtualenv 3.12.6 myenv + fi + pyenv activate myenv + python --version + pip install --prefer-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pytest-custom_exit_code esptool + pip install --extra-index-url https://dl.espressif.com/pypi/ -r $GITHUB_WORKSPACE/ci/requirements.txt unzip ci/artifacts.zip -d ci for dir in `ls -d ci/build_*`; do rm -rf build sdkconfig.defaults diff --git a/.github/workflows/mdns__host-tests.yml b/.github/workflows/mdns__host-tests.yml index 19733621aa..759a743574 100644 --- a/.github/workflows/mdns__host-tests.yml +++ b/.github/workflows/mdns__host-tests.yml @@ -68,3 +68,43 @@ jobs: diff -q $file /tmp/$file || exit 1 echo "OK" done + + fuzz_test: + if: contains(github.event.pull_request.labels.*.name, 'mdns-fuzz') || github.event_name == 'push' + name: Fuzzer tests for mdns lib + strategy: + matrix: + idf_ver: ["latest"] + + runs-on: ubuntu-22.04 + container: aflplusplus/aflplusplus + steps: + - name: Checkout esp-protocols + uses: actions/checkout@v4 + + - name: Checkout ESP-IDF + uses: actions/checkout@v4 + with: + repository: espressif/esp-idf + path: idf + submodules: recursive + + - name: Install Necessary Libs + run: | + apt-get update -y + apt-get install -y libbsd-dev + + - name: Run AFL++ + shell: bash + run: | + export IDF_PATH=$GITHUB_WORKSPACE/idf + cd components/mdns/tests/test_afl_fuzz_host/ + make fuzz + + - name: Upload Crash Artifacts + if: failure() + uses: actions/upload-artifact@v4 + with: + name: fuzz-crashes + path: components/mdns/tests/test_afl_fuzz_host/out/default/crashes.tar.gz + if-no-files-found: ignore diff --git a/components/mdns/examples/query_advertise/sdkconfig.ci.eth_custom_netif b/components/mdns/examples/query_advertise/sdkconfig.ci.eth_custom_netif index b9d7120415..a8dee3f38d 100644 --- a/components/mdns/examples/query_advertise/sdkconfig.ci.eth_custom_netif +++ b/components/mdns/examples/query_advertise/sdkconfig.ci.eth_custom_netif @@ -12,6 +12,7 @@ CONFIG_EXAMPLE_CONNECT_ETHERNET=y CONFIG_EXAMPLE_CONNECT_WIFI=n CONFIG_EXAMPLE_USE_INTERNAL_ETHERNET=y CONFIG_EXAMPLE_ETH_PHY_IP101=y +CONFIG_EXAMPLE_ETH_PHY_GENERIC=y CONFIG_EXAMPLE_ETH_MDC_GPIO=23 CONFIG_EXAMPLE_ETH_MDIO_GPIO=18 CONFIG_EXAMPLE_ETH_PHY_RST_GPIO=5 diff --git a/components/mdns/tests/host_test/dnsfixture.py b/components/mdns/tests/host_test/dnsfixture.py index 6dcf0c99f5..c16b061fac 100644 --- a/components/mdns/tests/host_test/dnsfixture.py +++ b/components/mdns/tests/host_test/dnsfixture.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 import logging import re @@ -92,10 +92,58 @@ def check_record(self, name, query_type, expected=True, expect=None): if expect is None: expect = name if expected: - assert any(expect in answer for answer in answers), f"Expected record '{expect}' not found in answer section" + assert any(expect in answer for answer in answers), f"Expected record '{expect}' not in answer section" else: assert not any(expect in answer for answer in answers), f"Unexpected record '{expect}' found in answer section" + def parse_section(self, response, section: str, rdtype_text: str): + """Parse a specific response section (answer, authority, additional) for given rdtype. + + Returns list of textual records for that rdtype. + """ + out = [] + if not response: + return out + rrsets = [] + if section == 'answer': + rrsets = response.answer + elif section == 'authority': + rrsets = response.authority + elif section == 'additional': + rrsets = response.additional + else: + raise ValueError('invalid section') + for rr in rrsets: + if dns.rdatatype.to_text(rr.rdtype) != rdtype_text: + continue + for item in rr.items: + full = ( + f'{rr.name} {rr.ttl} ' + f'{dns.rdataclass.to_text(rr.rdclass)} ' + f'{dns.rdatatype.to_text(rr.rdtype)} ' + f'{item.to_text()}' + ) + out.append(full) + return out + + def check_additional(self, response, rdtype_text: str, owner_contains: str, expected: bool = True, expect_substr: str | None = None): + """Check Additional section for an RR of type rdtype_text whose owner includes owner_contains. + + If expect_substr is provided, also require it to appear in the textual RR. + """ + records = self.parse_section(response, 'additional', rdtype_text) + logger.info(f'additional({rdtype_text}): {records}') + + def _matches(line: str) -> bool: + in_owner = owner_contains in line + has_val = (expect_substr in line) if expect_substr else True + return in_owner and has_val + found = any(_matches(r) for r in records) + if expected: + assert found, f"Expected {rdtype_text} for {owner_contains} in Additional not found" + else: + assert not found, f"Unexpected {rdtype_text} for {owner_contains} found in Additional" + if __name__ == '__main__': if len(sys.argv) < 3: diff --git a/components/mdns/tests/host_test/pytest_mdns.py b/components/mdns/tests/host_test/pytest_mdns.py index f8b95f5514..95fefc2eba 100644 --- a/components/mdns/tests/host_test/pytest_mdns.py +++ b/components/mdns/tests/host_test/pytest_mdns.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD +# SPDX-FileCopyrightText: 2024-2025 Espressif Systems (Shanghai) CO LTD # SPDX-License-Identifier: Unlicense OR CC0-1.0 import logging @@ -65,6 +65,17 @@ def test_add_service(mdns_console, dig_app): dig_app.check_record('_http._tcp.local', query_type='PTR', expected=True) +def test_ptr_additional_records_for_service(dig_app): + # Query PTR for the service type and ensure SRV/TXT are in Additional (RFC 6763 ยง12.1) + resp = dig_app.run_query('_http._tcp.local', query_type='PTR') + # Answer section should have at least one PTR to the instance + answers = dig_app.parse_answer_section(resp, 'PTR') + assert any('test_service._http._tcp.local' in a for a in answers) + # Additional section should include SRV and TXT for the same instance + dig_app.check_additional(resp, 'SRV', 'test_service._http._tcp.local', expected=True) + dig_app.check_additional(resp, 'TXT', 'test_service._http._tcp.local', expected=True) + + def test_remove_service(mdns_console, dig_app): mdns_console.send_input('mdns_service_remove _http _tcp') mdns_console.send_input('mdns_service_lookup _http _tcp') diff --git a/components/mdns/tests/test_afl_fuzz_host/Makefile b/components/mdns/tests/test_afl_fuzz_host/Makefile index 0eac9ae83b..c9f08b1066 100644 --- a/components/mdns/tests/test_afl_fuzz_host/Makefile +++ b/components/mdns/tests/test_afl_fuzz_host/Makefile @@ -1,7 +1,9 @@ +#INSTR=off TEST_NAME=test FUZZ=afl-fuzz COMPONENTS_DIR=$(IDF_PATH)/components -COMPILER_ICLUDE_DIR=$(shell echo `which xtensa-esp32-elf-gcc | xargs dirname | xargs dirname`/xtensa-esp32-elf) +# Use ESP32 toolchain include path if available, otherwise fall back to system includes for host-based compilation +COMPILER_INCLUDE_DIR=$(shell if command -v xtensa-esp32-elf-gcc >/dev/null 2>&1; then echo `which xtensa-esp32-elf-gcc | xargs dirname | xargs dirname`/xtensa-esp32-elf; else echo /usr; fi) CFLAGS=-g -Wno-unused-value -Wno-missing-declarations -Wno-pointer-bool-conversion -Wno-macro-redefined -Wno-int-to-void-pointer-cast -DHOOK_MALLOC_FAILED -DESP_EVENT_H_ -D__ESP_LOG_H__ \ -I. -I../.. -I../../include -I../../private_include -I ./build/config \ @@ -35,7 +37,7 @@ CFLAGS=-g -Wno-unused-value -Wno-missing-declarations -Wno-pointer-bool-conversi -I$(COMPONENTS_DIR)/xtensa/include \ -I$(COMPONENTS_DIR)/xtensa/esp32/include \ -I$(COMPONENTS_DIR)/esp_hw_support/etm/include \ - -I$(COMPILER_ICLUDE_DIR)/include + -I$(COMPILER_INCLUDE_DIR)/include MDNS_C_DEPENDENCY_INJECTION=-include mdns_di.h @@ -77,7 +79,18 @@ $(TEST_NAME): $(OBJECTS) @$(LD) $(OBJECTS) -o $@ $(LDLIBS) fuzz: $(TEST_NAME) - @$(FUZZ) -i "in" -o "out" -- ./$(TEST_NAME) + # timeout returns 124 if time limit is reached, original return code otherwise + # pass only if: fuzzing was running smoothly until timeout AND no crash found + @timeout 10m $(FUZZ) -i "in" -o "out" -- ./$(TEST_NAME) || \ + if [ $$? -eq 124 ]; then \ + if [ -n "$$(find out/default/crashes -type f 2>/dev/null)" ]; then \ + echo "Crashes found!"; \ + tar -czf out/default/crashes.tar.gz -C out/default crashes; \ + exit 1; \ + fi \ + else \ + exit 1; \ + fi clean: @rm -rf *.o *.SYM $(TEST_NAME) out diff --git a/components/mdns/tests/test_afl_fuzz_host/esp32_mock.h b/components/mdns/tests/test_afl_fuzz_host/esp32_mock.h index 68a3461058..70a2037ffa 100644 --- a/components/mdns/tests/test_afl_fuzz_host/esp32_mock.h +++ b/components/mdns/tests/test_afl_fuzz_host/esp32_mock.h @@ -55,8 +55,7 @@ #define pdMS_TO_TICKS(a) a #define xSemaphoreTake(s,d) true -#define xTaskDelete(a) -#define vTaskDelete(a) free(a) +#define vTaskDelete(a) free(NULL) #define xSemaphoreGive(s) #define xQueueCreateMutex(s) #define _mdns_pcb_init(a,b) true @@ -66,7 +65,7 @@ #define vSemaphoreDelete(s) free(s) #define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U #define xTaskCreatePinnedToCore(a,b,c,d,e,f,g) *(f) = malloc(1) -#define xTaskCreateStaticPinnedToCore(a,b,c,d,e,f,g,h) true +#define xTaskCreateStaticPinnedToCore(a,b,c,d,e,f,g,h) ((void*)1) #define vTaskDelay(m) usleep((m)*0) #define esp_random() (rand()%UINT32_MAX) @@ -139,4 +138,8 @@ TaskHandle_t xTaskGetCurrentTaskHandle(void); void xTaskNotifyGive(TaskHandle_t task); BaseType_t xTaskNotifyWait(uint32_t bits_entry_clear, uint32_t bits_exit_clear, uint32_t *value, TickType_t wait_time); +static inline void xTaskGetStaticBuffers(void *pvTaskBuffer, void *pvStackBuffer, void *pvTaskTCB) +{ +} + #endif //_ESP32_COMPAT_H_ diff --git a/components/mdns/tests/test_afl_fuzz_host/test.c b/components/mdns/tests/test_afl_fuzz_host/test.c index afbf023843..d753dc9609 100644 --- a/components/mdns/tests/test_afl_fuzz_host/test.c +++ b/components/mdns/tests/test_afl_fuzz_host/test.c @@ -78,30 +78,20 @@ static int mdns_test_service_txt_set(const char *service, const char *proto, ui static int mdns_test_sub_service_add(const char *sub_name, const char *service_name, const char *proto, uint32_t port) { if (mdns_service_add(NULL, service_name, proto, port, NULL, 0)) { - // This is expected failure as the service thread is not running + return ESP_FAIL; } - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); if (mdns_test_mdns_get_service_item(service_name, proto) == NULL) { return ESP_FAIL; } - int ret = mdns_service_subtype_add_for_host(NULL, service_name, proto, NULL, sub_name); - a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); - return ret; + return mdns_service_subtype_add_for_host(NULL, service_name, proto, NULL, sub_name); } static int mdns_test_service_add(const char *service_name, const char *proto, uint32_t port) { if (mdns_service_add(NULL, service_name, proto, port, NULL, 0)) { - // This is expected failure as the service thread is not running + return ESP_FAIL; } - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); if (mdns_test_mdns_get_service_item(service_name, proto) == NULL) { return ESP_FAIL; @@ -266,9 +256,6 @@ int main(int argc, char **argv) } #ifndef MDNS_NO_SERVICES mdns_service_remove_all(); - mdns_action_t *a = NULL; - GetLastItem(&a); - mdns_test_execute_action(a); #endif ForceTaskDelete(); mdns_free();