Version in base suite: 1.34.5-1+deb13u1 Base version: c-ares_1.34.5-1+deb13u1 Target version: c-ares_1.34.9-1~deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/c/c-ares/c-ares_1.34.5-1+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/c/c-ares/c-ares_1.34.9-1~deb13u1.dsc CMakeLists.txt | 53 Makefile.in | 4 README.md | 4 RELEASE-NOTES.md | 253 + aclocal.m4 | 1 aminclude_static.am | 2 configure | 1985 ++++++++++++++ configure.ac | 35 debian/changelog | 51 debian/copyright | 10 debian/libcares2.symbols | 3 debian/patches/CVE-2025-62408.diff | 379 -- debian/patches/remove-shields-from-markdown.diff | 4 debian/patches/series | 1 debian/patches/use-debian-googletest.diff | 2 debian/rules | 7 debian/upstream/metadata | 2 debian/upstream/signing-key.asc | 26 debian/watch | 10 docs/Makefile.in | 4 docs/ares_cancel.3 | 17 docs/ares_destroy.3 | 7 docs/ares_dns_record.3 | 5 docs/ares_getaddrinfo.3 | 23 docs/ares_gethostbyaddr.3 | 11 docs/ares_gethostbyname.3 | 11 docs/ares_getnameinfo.3 | 26 docs/ares_inet_pton.3 | 18 docs/ares_init_options.3 | 26 docs/ares_query.3 | 10 docs/ares_search.3 | 9 docs/ares_send.3 | 10 docs/ares_set_pending_write_cb.3 | 5 docs/ares_set_server_state_callback.3 | 31 docs/ares_set_servers_csv.3 | 17 docs/ares_set_socket_callback.3 | 4 docs/ares_set_socket_configure_callback.3 | 4 docs/ares_set_socket_functions.3 | 32 include/Makefile.in | 4 include/ares.h | 8 include/ares_dns_record.h | 5 include/ares_version.h | 6 m4/ax_cxx_compile_stdcxx_14.m4 | 34 m4/ax_pthread.m4 | 2 src/Makefile.in | 4 src/lib/CMakeLists.txt | 12 src/lib/Makefile.in | 6 src/lib/ares_addrinfo_localhost.c | 40 src/lib/ares_cancel.c | 25 src/lib/ares_close_sockets.c | 95 src/lib/ares_config.h.in | 3 src/lib/ares_conn.c | 266 + src/lib/ares_conn.h | 43 src/lib/ares_cookie.c | 4 src/lib/ares_destroy.c | 83 src/lib/ares_getaddrinfo.c | 95 src/lib/ares_gethostbyaddr.c | 5 src/lib/ares_gethostbyname.c | 4 src/lib/ares_getnameinfo.c | 12 src/lib/ares_hosts_file.c | 8 src/lib/ares_inet_net_pton.h | 28 src/lib/ares_init.c | 65 src/lib/ares_library_init.c | 32 src/lib/ares_options.c | 48 src/lib/ares_parse_into_addrinfo.c | 2 src/lib/ares_private.h | 213 + src/lib/ares_process.c | 716 ++++- src/lib/ares_qcache.c | 238 + src/lib/ares_query.c | 5 src/lib/ares_search.c | 10 src/lib/ares_send.c | 91 src/lib/ares_set_socket_functions.c | 4 src/lib/ares_socket.c | 28 src/lib/ares_socket.h | 23 src/lib/ares_sysconfig.c | 9 src/lib/ares_sysconfig_files.c | 44 src/lib/ares_sysconfig_win.c | 212 - src/lib/ares_timeout.c | 11 src/lib/ares_update_servers.c | 66 src/lib/cares.rc | 2 src/lib/dsa/ares_array.c | 25 src/lib/dsa/ares_htable.c | 12 src/lib/dsa/ares_htable_dict.c | 2 src/lib/dsa/ares_llist.c | 5 src/lib/event/ares_event_configchg.c | 7 src/lib/event/ares_event_select.c | 15 src/lib/event/ares_event_thread.c | 8 src/lib/event/ares_event_wake_pipe.c | 8 src/lib/event/ares_event_win32.c | 3 src/lib/include/ares_mem.h | 3 src/lib/include/ares_str.h | 17 src/lib/inet_net_pton.c | 65 src/lib/legacy/ares_expand_name.c | 2 src/lib/legacy/ares_parse_a_reply.c | 7 src/lib/legacy/ares_parse_aaaa_reply.c | 7 src/lib/legacy/ares_parse_ns_reply.c | 4 src/lib/legacy/ares_parse_ptr_reply.c | 4 src/lib/legacy/ares_parse_uri_reply.c | 2 src/lib/record/ares_dns_mapping.c | 23 src/lib/record/ares_dns_multistring.c | 4 src/lib/record/ares_dns_name.c | 71 src/lib/record/ares_dns_parse.c | 96 src/lib/record/ares_dns_private.h | 4 src/lib/record/ares_dns_record.c | 54 src/lib/record/ares_dns_write.c | 18 src/lib/str/ares_buf.c | 49 src/lib/str/ares_str.c | 54 src/lib/util/ares_iface_ips.c | 32 src/lib/util/ares_iface_ips.h | 1 src/lib/util/ares_math.c | 19 src/lib/util/ares_math.h | 39 src/lib/util/ares_rand.c | 8 src/lib/util/ares_threads.c | 3 src/lib/util/ares_time.h | 1 src/lib/util/ares_uri.c | 12 src/tools/Makefile.in | 4 src/tools/adig.c | 12 src/tools/ahost.c | 7 test/CMakeLists.txt | 4 test/Makefile.in | 4 test/ares-test-init.cc | 152 + test/ares-test-internal.cc | 495 +++ test/ares-test-misc.cc | 99 test/ares-test-mock-ai.cc | 55 test/ares-test-mock-et.cc | 132 test/ares-test-mock.cc | 3189 +++++++++++++++++++++++ test/ares-test-ns.cc | 63 test/ares-test-parse-a.cc | 63 test/ares-test-parse-aaaa.cc | 30 test/ares-test-parse.cc | 355 ++ test/ares-test.cc | 104 test/ares-test.h | 24 132 files changed, 10026 insertions(+), 1163 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpg9ajf_l8/c-ares_1.34.5-1+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmpg9ajf_l8/c-ares_1.34.9-1~deb13u1.dsc: no acceptable signature found diff -Nru c-ares-1.34.5/CMakeLists.txt c-ares-1.34.9/CMakeLists.txt --- c-ares-1.34.5/CMakeLists.txt 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/CMakeLists.txt 2026-10-20 02:31:51.000000000 +0000 @@ -12,7 +12,7 @@ INCLUDE (CheckStructHasMember) INCLUDE (CheckLibraryExists) -PROJECT (c-ares LANGUAGES C VERSION "1.34.5" ) +PROJECT (c-ares LANGUAGES C VERSION "1.34.9" ) # Set this version before release SET (CARES_VERSION "${PROJECT_VERSION}") @@ -30,7 +30,7 @@ # For example, a version of 4:0:2 would generate output such as: # libname.so -> libname.so.2 # libname.so.2 -> libname.so.2.2.0 -SET (CARES_LIB_VERSIONINFO "21:4:19") +SET (CARES_LIB_VERSIONINFO "21:8:19") OPTION (CARES_STATIC "Build as a static library" OFF) @@ -43,17 +43,9 @@ OPTION (CARES_SYMBOL_HIDING "Hide private symbols in shared libraries" OFF) OPTION (CARES_THREADS "Build with thread-safety support" ON) OPTION (CARES_COVERAGE "Build for code coverage" OFF) +OPTION (CARES_WERROR "Treat compiler warnings on the c-ares library as errors" OFF) SET (CARES_RANDOM_FILE "/dev/urandom" CACHE STRING "Suitable File / Device Path for entropy, such as /dev/urandom") - -# Tests require a C++14 compiler -IF (CARES_BUILD_TESTS OR CARES_BUILD_CONTAINER_TESTS) - set(CMAKE_CXX_STANDARD 14) - set(CMAKE_CXX_STANDARD_REQUIRED TRUE) - set(CMAKE_CXX_EXTENSIONS FALSE) - enable_language(CXX) -ENDIF () - # Tests require static to be enabled on Windows to be able to access otherwise hidden symbols IF ((CARES_BUILD_TESTS OR CARES_BUILD_CONTAINER_TESTS) AND (NOT CARES_STATIC) AND WIN32) SET (CARES_STATIC ON) @@ -63,6 +55,30 @@ INCLUDE (EnableWarnings) +# Optionally treat warnings as errors for the c-ares library itself. This is +# applied per-target (see cares_set_werror() usage) rather than globally so it +# does not affect the test/fuzz harnesses (which carry their own warnings) or +# CMake's own compiler feature checks. Enabled in CI only on toolchains known +# to be free of spurious warnings at our high warning levels. +FUNCTION (cares_set_werror target) + IF (NOT CARES_WERROR) + RETURN () + ENDIF () + IF (MSVC) + TARGET_COMPILE_OPTIONS (${target} PRIVATE /WX) + ELSE () + TARGET_COMPILE_OPTIONS (${target} PRIVATE -Werror) + # Modern libc headers (e.g. glibc ) use the C11 _Generic + # keyword for const-correctness of strchr()/memchr()/etc. Clang flags + # that as a C11 extension under our C90 + -Wpedantic build even though it + # originates in a system header at our call sites. It is not something + # we can fix in our own code, so warn but do not fail on it. + IF (CMAKE_C_COMPILER_ID MATCHES "Clang") + TARGET_COMPILE_OPTIONS (${target} PRIVATE -Wno-error=c11-extensions) + ENDIF () + ENDIF () +ENDFUNCTION () + IF (MSVC) # allow linking against the static runtime library in msvc OPTION (CARES_MSVC_STATIC_RUNTIME "Link against the static runtime library" OFF) @@ -269,7 +285,7 @@ LIST (APPEND SYSFLAGS -D__EXTENSIONS__ -D_REENTRANT -D_XOPEN_SOURCE=600) ELSEIF (CMAKE_SYSTEM_NAME STREQUAL "AIX") LIST (APPEND SYSFLAGS -D_ALL_SOURCE -D_XOPEN_SOURCE=700 -D_USE_IRS) -ELSEIF (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD") +ELSEIF (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR CMAKE_SYSTEM_NAME STREQUAL "MidnightBSD") # Don't define _XOPEN_SOURCE on FreeBSD, it actually reduces visibility instead of increasing it ELSEIF (CMAKE_SYSTEM_NAME STREQUAL "QNX") LIST (APPEND SYSFLAGS -D_QNX_SOURCE) @@ -312,10 +328,21 @@ SET (ORIG_CMAKE_REQUIRED_DEFINITIONS ${CMAKE_REQUIRED_DEEFINITIONS}) SET (ORIG_CMAKE_REQUIRED_LIBRARIES ${CMAKE_REQUIRED_LIBRARIES}) SET (ORIG_CMAKE_EXTRA_INCLUDE_FILES ${CMAKE_EXTRA_INCLUDE_FILES}) +SET (ORIG_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS}) SET (CMAKE_REQUIRED_DEFINITIONS ${SYSFLAGS}) LIST (APPEND CMAKE_REQUIRED_LIBRARIES ${CARES_DEPENDENT_LIBS}) +# Apple SDKs declare functions newer than the deployment target. The build +# rejects calls to them with -Werror=partial-availability, so the symbol +# checks must too, or they report functions the build cannot use. +IF (APPLE) + CHECK_C_COMPILER_FLAG (-Werror=partial-availability HAVE_WERROR_PARTIAL_AVAILABILITY) + IF (HAVE_WERROR_PARTIAL_AVAILABILITY) + SET (CMAKE_REQUIRED_FLAGS "${CMAKE_REQUIRED_FLAGS} -Werror=partial-availability") + ENDIF () +ENDIF () + MACRO (CARES_EXTRAINCLUDE_IFSET var include) IF (${var}) LIST (APPEND CMAKE_EXTRA_INCLUDE_FILES ${include}) @@ -478,6 +505,7 @@ SET (CMAKE_REQUIRED_DEFINITIONS ${ORIG_CMAKE_REQUIRED_DEEFINITIONS}) SET (CMAKE_REQUIRED_LIBRARIES ${ORIG_CMAKE_REQUIRED_LIBRARIES}) SET (CMAKE_EXTRA_INCLUDE_FILES ${ORIG_CMAKE_EXTRA_INCLUDE_FILES}) +SET (CMAKE_REQUIRED_FLAGS ${ORIG_CMAKE_REQUIRED_FLAGS}) ################################################################################ @@ -630,6 +658,7 @@ CMAKE_SYSTEM_NAME STREQUAL "NetBSD" OR CMAKE_SYSTEM_NAME STREQUAL "SunOS" OR CMAKE_SYSTEM_NAME STREQUAL "FreeBSD" OR + CMAKE_SYSTEM_NAME STREQUAL "MidnightBSD" OR CMAKE_SYSTEM_NAME STREQUAL "AIX" OR WIN32) SET (HAVE_GETADDRINFO_THREADSAFE 1) diff -Nru c-ares-1.34.5/Makefile.in c-ares-1.34.9/Makefile.in --- c-ares-1.34.5/Makefile.in 2025-04-08 12:12:31.000000000 +0000 +++ c-ares-1.34.9/Makefile.in 2026-10-20 02:31:57.000000000 +0000 @@ -133,7 +133,6 @@ $(top_srcdir)/m4/ax_code_coverage.m4 \ $(top_srcdir)/m4/ax_compiler_vendor.m4 \ $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ - $(top_srcdir)/m4/ax_cxx_compile_stdcxx_14.m4 \ $(top_srcdir)/m4/ax_file_escapes.m4 \ $(top_srcdir)/m4/ax_pthread.m4 \ $(top_srcdir)/m4/ax_require_defined.m4 \ @@ -334,10 +333,13 @@ GENHTML = @GENHTML@ GMOCK112_CFLAGS = @GMOCK112_CFLAGS@ GMOCK112_LIBS = @GMOCK112_LIBS@ +GMOCK117_CFLAGS = @GMOCK117_CFLAGS@ +GMOCK117_LIBS = @GMOCK117_LIBS@ GMOCK_CFLAGS = @GMOCK_CFLAGS@ GMOCK_LIBS = @GMOCK_LIBS@ GREP = @GREP@ HAVE_CXX14 = @HAVE_CXX14@ +HAVE_CXX17 = @HAVE_CXX17@ INSTALL = @INSTALL@ INSTALL_DATA = @INSTALL_DATA@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ diff -Nru c-ares-1.34.5/README.md c-ares-1.34.9/README.md --- c-ares-1.34.5/README.md 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/README.md 2026-10-20 02:31:51.000000000 +0000 @@ -1,7 +1,7 @@ # [![c-ares logo](https://c-ares.org/art/c-ares-logo.svg)](https://c-ares.org/) -[![Build Status](https://api.cirrus-ci.com/github/c-ares/c-ares.svg?branch=main)](https://cirrus-ci.com/github/c-ares/c-ares) -[![Windows Build Status](https://ci.appveyor.com/api/projects/status/aevgc5914tm72pvs/branch/main?svg=true)](https://ci.appveyor.com/project/c-ares/c-ares/branch/main) +[![Build Status](https://github.com/c-ares/c-ares/actions/workflows/ubuntu-latest.yml/badge.svg?branch=main)](https://github.com/c-ares/c-ares/actions/workflows/ubuntu-latest.yml) +[![Windows Build Status](https://github.com/c-ares/c-ares/actions/workflows/windows.yml/badge.svg?branch=main)](https://github.com/c-ares/c-ares/actions/workflows/windows.yml) [![Coverage Status](https://coveralls.io/repos/github/c-ares/c-ares/badge.svg?branch=main)](https://coveralls.io/github/c-ares/c-ares?branch=main) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/291/badge)](https://bestpractices.coreinfrastructure.org/projects/291) [![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/c-ares.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:c-ares) diff -Nru c-ares-1.34.5/RELEASE-NOTES.md c-ares-1.34.9/RELEASE-NOTES.md --- c-ares-1.34.5/RELEASE-NOTES.md 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/RELEASE-NOTES.md 2026-10-20 02:31:51.000000000 +0000 @@ -1,28 +1,253 @@ -## c-ares version 1.34.5 - April 8 2025 +## c-ares version 1.34.9 - October 19 2026 This is a security release. Security: -* CVE-2025-31498. A use-after-free bug has been uncovered in read_answers() that - was introduced in v1.32.3. Please see https://github.com/c-ares/c-ares/security/advisories/GHSA-6hxc-62jh-p29v +* Memory corruption when an application cancels its lookups after a + connection failure. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-wpr3-8q3r-4q8c +* A DNS reply that arrived from a different server than the one queried could + be accepted. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-fqh5-3x7m-wp44 +* Use-after-free of a lookup after shutting down a failed connection. Please + see https://github.com/c-ares/c-ares/security/advisories/GHSA-97vq-4mxv-7vhm +* Use-after-free when an application cancels lookups from a callback, or shuts + a channel down. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-rf6g-ww74-97qh +* Use-after-free of a connection after the socket-state callback returns. + Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-mw23-pfg6-2x9r +* Use-after-free when an application cancels lookups from a server-state + callback. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-v33j-65h4-4ch8 +* Use-after-free of a server record when an application changes its DNS + servers from a callback. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-pj7p-v3cw-hq66 +* Use-after-free of a cached DNS answer while the application is still reading + it. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-grv7-fq5q-9vfc +* Buffer overflow when `ares_dup()` or `ares_save_options()` runs while + another thread changes the servers. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-ph6c-3cvx-mq6v +* Uninitialized memory returned for a numeric IPv6 name with a prefix length. + Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-cfcv-8v2j-qj6r +* The query cache could return an answer obtained using different EDNS + options. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-wrgc-prmw-27xh Changes: -* Restore Windows XP support. [PR #958](https://github.com/c-ares/c-ares/pull/958) +* Answers served from the query cache now report their remaining TTL rather + than the TTL originally received. +* `ares_inet_pton()` rejects a "/bits" prefix length and returns 0, rather + than -1, for any input it cannot parse. +* UDP replies must now come from the server's port as well as its address. + Custom `arecvfrom` socket functions (`ares_set_socket_functions_ex()` or + `ares_set_socket_functions()`) must fill in the full source address, + including the port. For compatibility, a reported port of 0 checks only the + address, but such implementations are insecure and should be fixed. + [PR #1370](https://github.com/c-ares/c-ares/pull/1370) +* Document that socket functions and socket, configure and pending-write + callbacks must not call c-ares functions on the same channel. + [PR #1360](https://github.com/c-ares/c-ares/pull/1360) Bugfixes: -* A missing mutex initialization would make busy polling for configuration - changes (platforms other than Windows, Linux, MacOS) eat too much CPU - [PR #974](https://github.com/c-ares/c-ares/pull/974) -* Pkgconfig may be generated wrong for static builds in relation to `-pthread` - [PR #965](https://github.com/c-ares/c-ares/pull/965) -* Localhost resolution can fail if only one address family is in `/etc/hosts` - [PR #947](https://github.com/c-ares/c-ares/pull/947) +* `ares_dup()`: don't leave the new channel bound to the source channel. + [PR #1333](https://github.com/c-ares/c-ares/pull/1333) +* `ares_init_options()`: don't report the default query cache as a caller-set + option. [PR #1354](https://github.com/c-ares/c-ares/pull/1354) +* Clamp DNS record TTLs to the signed int range used by public APIs. + [PR #1313](https://github.com/c-ares/c-ares/pull/1313) +* `ares_dns_write()`: reject messages larger than 64KB. + [PR #1314](https://github.com/c-ares/c-ares/pull/1314) +* `ares_dns_parse()`: accept legacy compressed RDATA names. + [PR #1326](https://github.com/c-ares/c-ares/pull/1326) +* `ares_parse_a_reply()`/`ares_parse_aaaa_reply()`: ignore a negative + `*naddrttls`. [PR #1302](https://github.com/c-ares/c-ares/pull/1302) +* select event backend: refuse descriptors >= `FD_SETSIZE` instead of + overflowing `fd_set`. [PR #1312](https://github.com/c-ares/c-ares/pull/1312) +* Strip inline comments from resolv.conf values. + [PR #1276](https://github.com/c-ares/c-ares/pull/1276) +* Ignore absurd resolv.conf retry/attempts values. + [PR #1315](https://github.com/c-ares/c-ares/pull/1315) +* Zero-initialize the server config when parsing nameserver URIs. + [PR #1325](https://github.com/c-ares/c-ares/pull/1325) +* URI parsing: roll back the ':' probe when it lands past the '@'. + [PR #1265](https://github.com/c-ares/c-ares/pull/1265) +* Do not assume `gethostname()` NUL-terminates its buffer. + [PR #1269](https://github.com/c-ares/c-ares/pull/1269) +* Combine the hash table seed with XOR instead of OR. + [PR #1330](https://github.com/c-ares/c-ares/pull/1330) +* Turn off stdio buffering when loading a file. + [PR #939](https://github.com/c-ares/c-ares/pull/939) +* Guard `pipe2()` on Apple platforms to prevent unguarded availability + warnings. [PR #1328](https://github.com/c-ares/c-ares/pull/1328) Thanks go to these friendly people for their efforts and contributions for this release: +* (@ahillspace) +* (@anon5376) * Brad House (@bradh352) -* Erik Lax (@eriklax) -* Florian Pfisterer (@FlorianPfisterer) -* Kai Pastor (@dg0yt) +* Nikolaos Chatzikonstantinou (@createyourpersonalaccount) +* Dev (@devdgna) +* (@evilgensec) +* Ilias (@iliasabk) +* (@jdymitarai) +* (@jmestwa-coder) +* Manohar Paturi (@ManoharPaturi) +* (@mardgez13) + + +## c-ares version 1.34.8 - July 7 2026 + +This is a bugfix release. + +Bugfixes: +* Revert "Mark parameters in callbacks as const" which shipped in 1.34.7. + Changing the parameter types of the `ares_callback`, `ares_host_callback`, + and `ares_nameinfo_callback` function pointer typedefs was an unintended + API break: existing applications with the historical non-const callback + signatures no longer compiled, particularly in C++ where function pointer + types must match exactly. The read-only nature of the callback parameters + is now documented instead. + [PR #1244](https://github.com/c-ares/c-ares/pull/1244) + +Thanks go to these friendly people for their efforts and contributions for this +release: + +* Brad House (@bradh352) + + +## c-ares version 1.34.7 - July 6 2026 + +This is a security release. + +Security: +* CVE-2026-33630. Use-after-free / double-free in c-ares' query-completion + handling, remotely triggerable via ares_getaddrinfo() over TCP. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-6wfj-rwm7-3542 +* CPU-exhaustion denial of service via unbounded DNS name compression pointer + chains. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-pjmc-gx33-gc76 +* Memory-amplification denial of service via unvalidated DNS header record + counts. Please see + https://github.com/c-ares/c-ares/security/advisories/GHSA-jv8r-gqr9-68wj + +Changes: +* `ares_getaddrinfo()`: handle a NULL node per POSIX and implement + `ARES_AI_PASSIVE`. [PR #1186](https://github.com/c-ares/c-ares/pull/1186) +* Mark parameters in callbacks as const. + [PR #1060](https://github.com/c-ares/c-ares/pull/1060) +* Correct `ARES_CLASS_HESOID` misspelling to `ARES_CLASS_HESIOD`. + [PR #1092](https://github.com/c-ares/c-ares/pull/1092) + +Bugfixes: +* Fix sticky server recovery when all servers have failures. [PR #1192](https://github.com/c-ares/c-ares/pull/1192) +* Fix UDP socket exhaustion regression: retire connections per-connection, not via server failure count. [PR #1197](https://github.com/c-ares/c-ares/pull/1197) +* Guard DNS record binary length overflow. [PR #1168](https://github.com/c-ares/c-ares/pull/1168) +* Prevent integer overflow in allocation size calculations. [PR #1147](https://github.com/c-ares/c-ares/pull/1147) +* Prevent overflow in ares_array allocation size calculations. [PR #1117](https://github.com/c-ares/c-ares/pull/1117) +* Prevent integer overflow in buffer size calculation. [PR #1116](https://github.com/c-ares/c-ares/pull/1116) +* Add overflow checks to ares_buf_ensure_space(). [PR #1094](https://github.com/c-ares/c-ares/pull/1094) +* Skip name compression offsets beyond the 14-bit pointer limit. [PR #1159](https://github.com/c-ares/c-ares/pull/1159) +* ares_dns_parse: reject name compression in RDATA where not permitted (RFC 3597). [PR #1190](https://github.com/c-ares/c-ares/pull/1190) +* ares_dns_parse: reject responses with more than one OPT record (RFC 6891). [PR #1189](https://github.com/c-ares/c-ares/pull/1189) +* Discard oversized UDP datagrams instead of truncating the length frame. [PR #1161](https://github.com/c-ares/c-ares/pull/1161) +* Route numeric config parsing through range-checked ares_str_parse_uint. [PR #1158](https://github.com/c-ares/c-ares/pull/1158) +* Replace atoi-based port parsing with validated helper. [PR #1097](https://github.com/c-ares/c-ares/pull/1097) +* Defer TCP connection error handling until DNS responses are parsed. [PR #1138](https://github.com/c-ares/c-ares/pull/1138) +* Prevent undefined-behavior left shift in ares_calc_query_timeout(). [PR #1151](https://github.com/c-ares/c-ares/pull/1151) +* Use unsigned type for ares_round_up_pow2_u64 to avoid undefined behavior. [PR #1107](https://github.com/c-ares/c-ares/pull/1107) +* Fix undefined behavior in ares_buf_replace() pointer arithmetic. [PR #1099](https://github.com/c-ares/c-ares/pull/1099) +* ares_array: reset offset when array becomes empty. [PR #1165](https://github.com/c-ares/c-ares/pull/1165) +* ares_iface_ips: add ARES_IFACE_IP_NONE zero enum value. [PR #1187](https://github.com/c-ares/c-ares/pull/1187) +* ares_sysconfig_files: recognize AIX netsvc.conf bind4/local4 tokens. [PR #1188](https://github.com/c-ares/c-ares/pull/1188) +* Use safe string construction in Windows sysconfig join path. [PR #1143](https://github.com/c-ares/c-ares/pull/1143) +* Fix zero-length RAW_RR losing type metadata during parsing. [PR #1129](https://github.com/c-ares/c-ares/pull/1129) +* Fix RAW_RR type tostr/fromstr roundtrip mismatch. [PR #1123](https://github.com/c-ares/c-ares/pull/1123) +* Fix NULL dereference for ifa netmask. [PR #1120](https://github.com/c-ares/c-ares/pull/1120) +* adig: fix negated option prefix parsing. [PR #1135](https://github.com/c-ares/c-ares/pull/1135) +* Use UnregisterWaitEx to prevent use-after-free on Win32. [PR #1111](https://github.com/c-ares/c-ares/pull/1111) +* Add NULL check after ares_malloc_zero in Windows UTF8 conversion. [PR #1121](https://github.com/c-ares/c-ares/pull/1121) +* Fix NULL dereference after ares_malloc_zero in Win32 IOCP event add. [PR #1132](https://github.com/c-ares/c-ares/pull/1132) +* Fix microsecond overflow in ares_queue_wait_empty timeout. [PR #1122](https://github.com/c-ares/c-ares/pull/1122) +* Fix NULL dereference in ares_buf_replace() on NULL buf. [PR #1124](https://github.com/c-ares/c-ares/pull/1124) +* Initialize *read_bytes in ares_socket_recvfrom(). [PR #1125](https://github.com/c-ares/c-ares/pull/1125) +* Fix memory leak of binbuf in ares_buf_parse_dns_binstr_int. [PR #1126](https://github.com/c-ares/c-ares/pull/1126) +* Fix memory leak of qcache entry on key allocation failure. [PR #1127](https://github.com/c-ares/c-ares/pull/1127) +* Fix memory leak of buf in ares_dns_multistring_combined() on OOM. [PR #1110](https://github.com/c-ares/c-ares/pull/1110) +* Fix memory leaks on error paths in two functions. [PR #1109](https://github.com/c-ares/c-ares/pull/1109) +* Fix memory leak of buckets in ares_htable_dict_keys() error path. [PR #1108](https://github.com/c-ares/c-ares/pull/1108) +* Fix memory leak of bucket->key in ares_htable_dict_insert error path. [PR #1105](https://github.com/c-ares/c-ares/pull/1105) +* Fix additional memory leaks. [PR #1091](https://github.com/c-ares/c-ares/pull/1091) [PR #1078](https://github.com/c-ares/c-ares/pull/1078) +* Prevent corrupt addrinfo nodes on sockaddr allocation failure. [PR #1112](https://github.com/c-ares/c-ares/pull/1112) +* Fix two logic bugs in DNS cookie handling. [PR #1103](https://github.com/c-ares/c-ares/pull/1103) +* Fix linked list INSERT_BEFORE corruption and array insertdata_first ordering. [PR #1101](https://github.com/c-ares/c-ares/pull/1101) +* Fix HASH_IDX macro missing parentheses. [PR #1128](https://github.com/c-ares/c-ares/pull/1128) +* Fix malloc(0) in ares_htable_all_buckets on empty table. [PR #1131](https://github.com/c-ares/c-ares/pull/1131) +* Fix wrong sizeof in QNX confstr() call truncating domain names. [PR #1130](https://github.com/c-ares/c-ares/pull/1130) +* Clear probe pending flag after timeout. [PR #1059](https://github.com/c-ares/c-ares/pull/1059) +* Fix incorrect check for empty wide string. [PR #1064](https://github.com/c-ares/c-ares/pull/1064) +* Handle strdup failure. [PR #1077](https://github.com/c-ares/c-ares/pull/1077) +* doc: reference ares_free_string(), not ares_free(). [PR #1084](https://github.com/c-ares/c-ares/pull/1084) + +Thanks go to these friendly people for their efforts and contributions for this +release: + +* (@Alb3e3) +* Brad House (@bradh352) +* (@dankmeme01) +* David Hotham (@dimbleby) +* Tom Flynn (@Flynnzaa) +* (@jmestwa-coder) +* (@kodareef5) +* Kaixuan Li (@MarkLee131) +* (@lifenjoiner) +* (@metsw24-max) +* Song Li (@SongTonyLi) +* (@uwezkhan) + + +## c-ares version 1.34.6 - December 8 2025 + +This is a security release. + +Security: +* CVE-2025-62408. A use-after-free bug has been uncovered in read_answers() that + was introduced in v1.32.3. Please see https://github.com/c-ares/c-ares/security/advisories/GHSA-jq53-42q6-pqr5 + +Changes: +* Ignore Windows IDN Search Domains until proper IDN support is added. [PR #1034](https://github.com/c-ares/c-ares/pull/1034) + +Bugfixes: +* Event Thread could stall when not notified of new queries on existing + connections that are in a bad state + [PR #1032](https://github.com/c-ares/c-ares/pull/1032) +* fix conversion of invalid service to port number in ares_getaddrinfo() + [PR #1029](https://github.com/c-ares/c-ares/pull/1029) +* fix memory leak in ares_uri + [PR #1012](https://github.com/c-ares/c-ares/pull/1012) +* Ignore ares_event_configchg_init failures + [PR #1009](https://github.com/c-ares/c-ares/pull/1009) +* Use XOR for random seed generation on fallback logic. + [PR #994](https://github.com/c-ares/c-ares/pull/994) +* Fix clang build on windows. + [PR #996](https://github.com/c-ares/c-ares/pull/996) +* Fix IPv6 link-local nameservers in /etc/resolv.conf + [PR #996](https://github.com/c-ares/c-ares/pull/997) +* Fix a few build issues on MidnightBSD. + [PR #983](https://github.com/c-ares/c-ares/pull/983) + +Thanks go to these friendly people for their efforts and contributions for this +release: + +* Brad House (@bradh352) +* (@F3lixTheCat) +* Lucas Holt (@laffer1) +* @oargon +* Pavel P (@pps83) +* Sean Harmer (@seanharmer) +* Uwe (@nixblik) + diff -Nru c-ares-1.34.5/aclocal.m4 c-ares-1.34.9/aclocal.m4 --- c-ares-1.34.5/aclocal.m4 2025-04-08 12:12:29.000000000 +0000 +++ c-ares-1.34.9/aclocal.m4 2026-10-20 02:31:56.000000000 +0000 @@ -1236,7 +1236,6 @@ m4_include([m4/ax_code_coverage.m4]) m4_include([m4/ax_compiler_vendor.m4]) m4_include([m4/ax_cxx_compile_stdcxx.m4]) -m4_include([m4/ax_cxx_compile_stdcxx_14.m4]) m4_include([m4/ax_file_escapes.m4]) m4_include([m4/ax_pthread.m4]) m4_include([m4/ax_require_defined.m4]) diff -Nru c-ares-1.34.5/aminclude_static.am c-ares-1.34.9/aminclude_static.am --- c-ares-1.34.5/aminclude_static.am 2025-04-08 12:12:30.000000000 +0000 +++ c-ares-1.34.9/aminclude_static.am 2026-10-20 02:31:56.000000000 +0000 @@ -1,6 +1,6 @@ # aminclude_static.am generated automatically by Autoconf -# from AX_AM_MACROS_STATIC on Tue Apr 8 12:12:30 UTC 2025 +# from AX_AM_MACROS_STATIC on Tue Oct 20 02:31:56 UTC 2026 # Code coverage diff -Nru c-ares-1.34.5/configure c-ares-1.34.9/configure --- c-ares-1.34.5/configure 2025-04-08 12:12:30.000000000 +0000 +++ c-ares-1.34.9/configure 2026-10-20 02:31:56.000000000 +0000 @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.71 for c-ares 1.34.5. +# Generated by GNU Autoconf 2.71 for c-ares 1.34.9. # # Report bugs to . # @@ -621,8 +621,8 @@ # Identity of this package. PACKAGE_NAME='c-ares' PACKAGE_TARNAME='c-ares' -PACKAGE_VERSION='1.34.5' -PACKAGE_STRING='c-ares 1.34.5' +PACKAGE_VERSION='1.34.9' +PACKAGE_STRING='c-ares 1.34.9' PACKAGE_BUGREPORT='c-ares mailing list: http://lists.haxx.se/listinfo/c-ares' PACKAGE_URL='' @@ -669,6 +669,8 @@ AM_CFLAGS BUILD_TESTS_FALSE BUILD_TESTS_TRUE +GMOCK117_LIBS +GMOCK117_CFLAGS GMOCK112_LIBS GMOCK112_CFLAGS GMOCK_LIBS @@ -779,6 +781,7 @@ INSTALL_DATA INSTALL_SCRIPT INSTALL_PROGRAM +HAVE_CXX17 HAVE_CXX14 ac_ct_CXX CXXFLAGS @@ -876,7 +879,9 @@ GMOCK_CFLAGS GMOCK_LIBS GMOCK112_CFLAGS -GMOCK112_LIBS' +GMOCK112_LIBS +GMOCK117_CFLAGS +GMOCK117_LIBS' # Initialize some variables set by options. @@ -1425,7 +1430,7 @@ # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures c-ares 1.34.5 to adapt to many kinds of systems. +\`configure' configures c-ares 1.34.9 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1496,7 +1501,7 @@ if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of c-ares 1.34.5:";; + short | recursive ) echo "Configuration of c-ares 1.34.9:";; esac cat <<\_ACEOF @@ -1570,6 +1575,10 @@ C compiler flags for GMOCK112, overriding pkg-config GMOCK112_LIBS linker flags for GMOCK112, overriding pkg-config + GMOCK117_CFLAGS + C compiler flags for GMOCK117, overriding pkg-config + GMOCK117_LIBS + linker flags for GMOCK117, overriding pkg-config Use these variables to override the choices made by `configure' or to help it to find libraries and programs with nonstandard names/locations. @@ -1638,7 +1647,7 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -c-ares configure 1.34.5 +c-ares configure 1.34.9 generated by GNU Autoconf 2.71 Copyright (C) 2021 Free Software Foundation, Inc. @@ -2262,7 +2271,7 @@ This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by c-ares $as_me 1.34.5, which was +It was created by c-ares $as_me 1.34.9, which was generated by GNU Autoconf 2.71. Invocation command line was $ $0$ac_configure_args_raw @@ -3236,7 +3245,7 @@ -CARES_VERSION_INFO="21:4:19" +CARES_VERSION_INFO="21:8:19" @@ -5355,6 +5364,887 @@ fi + ax_cxx_compile_alternatives="17 1z" ax_cxx_compile_cxx17_required=false + ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + ac_success=no + + + + + + if test x$ac_success = xno; then + for alternative in ${ax_cxx_compile_alternatives}; do + for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}" MSVC; do + if test x"$switch" = xMSVC; then + switch=-std:c++${alternative} + cachevar=`printf "%s\n" "ax_cv_cxx_compile_cxx17_${switch}_MSVC" | $as_tr_sh` + else + cachevar=`printf "%s\n" "ax_cv_cxx_compile_cxx17_$switch" | $as_tr_sh` + fi + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CXX supports C++17 features with $switch" >&5 +printf %s "checking whether $CXX supports C++17 features with $switch... " >&6; } +if eval test \${$cachevar+y} +then : + printf %s "(cached) " >&6 +else $as_nop + ac_save_CXX="$CXX" + CXX="$CXX $switch" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + +// If the compiler admits that it is not ready for C++11, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +// MSVC always sets __cplusplus to 199711L in older versions; newer versions +// only set it correctly if /Zc:__cplusplus is specified as well as a +// /std:c++NN switch: +// +// https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ +// +// The value __cplusplus ought to have is available in _MSVC_LANG since +// Visual Studio 2015 Update 3: +// +// https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros +// +// This was also the first MSVC version to support C++14 so we can't use the +// value of either __cplusplus or _MSVC_LANG to quickly rule out MSVC having +// C++11 or C++14 support, but we can check _MSVC_LANG for C++17 and later. +#elif __cplusplus < 201103L && !defined _MSC_VER + +#error "This is not a C++11 compiler" + +#else + +namespace cxx11 +{ + + namespace test_static_assert + { + + template + struct check + { + static_assert(sizeof(int) <= sizeof(T), "not big enough"); + }; + + } + + namespace test_final_override + { + + struct Base + { + virtual ~Base() {} + virtual void f() {} + }; + + struct Derived : public Base + { + virtual ~Derived() override {} + virtual void f() override {} + }; + + } + + namespace test_double_right_angle_brackets + { + + template < typename T > + struct check {}; + + typedef check single_type; + typedef check> double_type; + typedef check>> triple_type; + typedef check>>> quadruple_type; + + } + + namespace test_decltype + { + + int + f() + { + int a = 1; + decltype(a) b = 2; + return a + b; + } + + } + + namespace test_type_deduction + { + + template < typename T1, typename T2 > + struct is_same + { + static const bool value = false; + }; + + template < typename T > + struct is_same + { + static const bool value = true; + }; + + template < typename T1, typename T2 > + auto + add(T1 a1, T2 a2) -> decltype(a1 + a2) + { + return a1 + a2; + } + + int + test(const int c, volatile int v) + { + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == false, ""); + auto ac = c; + auto av = v; + auto sumi = ac + av + 'x'; + auto sumf = ac + av + 1.0; + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == true, ""); + return (sumf > 0.0) ? sumi : add(c, v); + } + + } + + namespace test_noexcept + { + + int f() { return 0; } + int g() noexcept { return 0; } + + static_assert(noexcept(f()) == false, ""); + static_assert(noexcept(g()) == true, ""); + + } + + namespace test_constexpr + { + + template < typename CharT > + unsigned long constexpr + strlen_c_r(const CharT *const s, const unsigned long acc) noexcept + { + return *s ? strlen_c_r(s + 1, acc + 1) : acc; + } + + template < typename CharT > + unsigned long constexpr + strlen_c(const CharT *const s) noexcept + { + return strlen_c_r(s, 0UL); + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("1") == 1UL, ""); + static_assert(strlen_c("example") == 7UL, ""); + static_assert(strlen_c("another\0example") == 7UL, ""); + + } + + namespace test_rvalue_references + { + + template < int N > + struct answer + { + static constexpr int value = N; + }; + + answer<1> f(int&) { return answer<1>(); } + answer<2> f(const int&) { return answer<2>(); } + answer<3> f(int&&) { return answer<3>(); } + + void + test() + { + int i = 0; + const int c = 0; + static_assert(decltype(f(i))::value == 1, ""); + static_assert(decltype(f(c))::value == 2, ""); + static_assert(decltype(f(0))::value == 3, ""); + } + + } + + namespace test_uniform_initialization + { + + struct test + { + static const int zero {}; + static const int one {1}; + }; + + static_assert(test::zero == 0, ""); + static_assert(test::one == 1, ""); + + } + + namespace test_lambdas + { + + void + test1() + { + auto lambda1 = [](){}; + auto lambda2 = lambda1; + lambda1(); + lambda2(); + } + + int + test2() + { + auto a = [](int i, int j){ return i + j; }(1, 2); + auto b = []() -> int { return '0'; }(); + auto c = [=](){ return a + b; }(); + auto d = [&](){ return c; }(); + auto e = [a, &b](int x) mutable { + const auto identity = [](int y){ return y; }; + for (auto i = 0; i < a; ++i) + a += b--; + return x + identity(a + b); + }(0); + return a + b + c + d + e; + } + + int + test3() + { + const auto nullary = [](){ return 0; }; + const auto unary = [](int x){ return x; }; + using nullary_t = decltype(nullary); + using unary_t = decltype(unary); + const auto higher1st = [](nullary_t f){ return f(); }; + const auto higher2nd = [unary](nullary_t f1){ + return [unary, f1](unary_t f2){ return f2(unary(f1())); }; + }; + return higher1st(nullary) + higher2nd(nullary)(unary); + } + + } + + namespace test_variadic_templates + { + + template + struct sum; + + template + struct sum + { + static constexpr auto value = N0 + sum::value; + }; + + template <> + struct sum<> + { + static constexpr auto value = 0; + }; + + static_assert(sum<>::value == 0, ""); + static_assert(sum<1>::value == 1, ""); + static_assert(sum<23>::value == 23, ""); + static_assert(sum<1, 2>::value == 3, ""); + static_assert(sum<5, 5, 11>::value == 21, ""); + static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, ""); + + } + + // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae + // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function + // because of this. + namespace test_template_alias_sfinae + { + + struct foo {}; + + template + using member = typename T::member_type; + + template + void func(...) {} + + template + void func(member*) {} + + void test(); + + void test() { func(0); } + + } + +} // namespace cxx11 + +#endif // __cplusplus >= 201103L + + + + +// If the compiler admits that it is not ready for C++14, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201402L && !defined _MSC_VER + +#error "This is not a C++14 compiler" + +#else + +namespace cxx14 +{ + + namespace test_polymorphic_lambdas + { + + int + test() + { + const auto lambda = [](auto&&... args){ + const auto istiny = [](auto x){ + return (sizeof(x) == 1UL) ? 1 : 0; + }; + const int aretiny[] = { istiny(args)... }; + return aretiny[0]; + }; + return lambda(1, 1L, 1.0f, '1'); + } + + } + + namespace test_binary_literals + { + + constexpr auto ivii = 0b0000000000101010; + static_assert(ivii == 42, "wrong value"); + + } + + namespace test_generalized_constexpr + { + + template < typename CharT > + constexpr unsigned long + strlen_c(const CharT *const s) noexcept + { + auto length = 0UL; + for (auto p = s; *p; ++p) + ++length; + return length; + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("x") == 1UL, ""); + static_assert(strlen_c("test") == 4UL, ""); + static_assert(strlen_c("another\0test") == 7UL, ""); + + } + + namespace test_lambda_init_capture + { + + int + test() + { + auto x = 0; + const auto lambda1 = [a = x](int b){ return a + b; }; + const auto lambda2 = [a = lambda1(x)](){ return a; }; + return lambda2(); + } + + } + + namespace test_digit_separators + { + + constexpr auto ten_million = 100'000'000; + static_assert(ten_million == 100000000, ""); + + } + + namespace test_return_type_deduction + { + + auto f(int& x) { return x; } + decltype(auto) g(int& x) { return x; } + + template < typename T1, typename T2 > + struct is_same + { + static constexpr auto value = false; + }; + + template < typename T > + struct is_same + { + static constexpr auto value = true; + }; + + int + test() + { + auto x = 0; + static_assert(is_same::value, ""); + static_assert(is_same::value, ""); + return x; + } + + } + +} // namespace cxx14 + +#endif // __cplusplus >= 201402L + + + + +// If the compiler admits that it is not ready for C++17, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 201703L + +#error "This is not a C++17 compiler" + +#else + +#include +#include +#include + +namespace cxx17 +{ + + namespace test_constexpr_lambdas + { + + constexpr int foo = [](){return 42;}(); + + } + + namespace test::nested_namespace::definitions + { + + } + + namespace test_fold_expression + { + + template + int multiply(Args... args) + { + return (args * ... * 1); + } + + template + bool all(Args... args) + { + return (args && ...); + } + + } + + namespace test_extended_static_assert + { + + static_assert (true); + + } + + namespace test_auto_brace_init_list + { + + auto foo = {5}; + auto bar {5}; + + static_assert(std::is_same, decltype(foo)>::value); + static_assert(std::is_same::value); + } + + namespace test_typename_in_template_template_parameter + { + + template typename X> struct D; + + } + + namespace test_fallthrough_nodiscard_maybe_unused_attributes + { + + int f1() + { + return 42; + } + + [[nodiscard]] int f2() + { + [[maybe_unused]] auto unused = f1(); + + switch (f1()) + { + case 17: + f1(); + [[fallthrough]]; + case 42: + f1(); + } + return f1(); + } + + } + + namespace test_extended_aggregate_initialization + { + + struct base1 + { + int b1, b2 = 42; + }; + + struct base2 + { + base2() { + b3 = 42; + } + int b3; + }; + + struct derived : base1, base2 + { + int d; + }; + + derived d1 {{1, 2}, {}, 4}; // full initialization + derived d2 {{}, {}, 4}; // value-initialized bases + + } + + namespace test_general_range_based_for_loop + { + + struct iter + { + int i; + + int& operator* () + { + return i; + } + + const int& operator* () const + { + return i; + } + + iter& operator++() + { + ++i; + return *this; + } + }; + + struct sentinel + { + int i; + }; + + bool operator== (const iter& i, const sentinel& s) + { + return i.i == s.i; + } + + bool operator!= (const iter& i, const sentinel& s) + { + return !(i == s); + } + + struct range + { + iter begin() const + { + return {0}; + } + + sentinel end() const + { + return {5}; + } + }; + + void f() + { + range r {}; + + for (auto i : r) + { + [[maybe_unused]] auto v = i; + } + } + + } + + namespace test_lambda_capture_asterisk_this_by_value + { + + struct t + { + int i; + int foo() + { + return [*this]() + { + return i; + }(); + } + }; + + } + + namespace test_enum_class_construction + { + + enum class byte : unsigned char + {}; + + byte foo {42}; + + } + + namespace test_constexpr_if + { + + template + int f () + { + if constexpr(cond) + { + return 13; + } + else + { + return 42; + } + } + + } + + namespace test_selection_statement_with_initializer + { + + int f() + { + return 13; + } + + int f2() + { + if (auto i = f(); i > 0) + { + return 3; + } + + switch (auto i = f(); i + 4) + { + case 17: + return 2; + + default: + return 1; + } + } + + } + + namespace test_template_argument_deduction_for_class_templates + { + + template + struct pair + { + pair (T1 p1, T2 p2) + : m1 {p1}, + m2 {p2} + {} + + T1 m1; + T2 m2; + }; + + void f() + { + [[maybe_unused]] auto p = pair{13, 42u}; + } + + } + + namespace test_non_type_auto_template_parameters + { + + template + struct B + {}; + + B<5> b1; + B<'a'> b2; + + } + + namespace test_structured_bindings + { + + int arr[2] = { 1, 2 }; + std::pair pr = { 1, 2 }; + + auto f1() -> int(&)[2] + { + return arr; + } + + auto f2() -> std::pair& + { + return pr; + } + + struct S + { + int x1 : 2; + volatile double y1; + }; + + S f3() + { + return {}; + } + + auto [ x1, y1 ] = f1(); + auto& [ xr1, yr1 ] = f1(); + auto [ x2, y2 ] = f2(); + auto& [ xr2, yr2 ] = f2(); + const auto [ x3, y3 ] = f3(); + + } + + namespace test_exception_spec_type_system + { + + struct Good {}; + struct Bad {}; + + void g1() noexcept; + void g2(); + + template + Bad + f(T*, T*); + + template + Good + f(T1*, T2*); + + static_assert (std::is_same_v); + + } + + namespace test_inline_variables + { + + template void f(T) + {} + + template inline T g(T) + { + return T{}; + } + + template<> inline void f<>(int) + {} + + template<> int g<>(int) + { + return 5; + } + + } + +} // namespace cxx17 + +#endif // (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 201703L + + + +_ACEOF +if ac_fn_cxx_try_compile "$LINENO" +then : + eval $cachevar=yes +else $as_nop + eval $cachevar=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + CXX="$ac_save_CXX" +fi +eval ac_res=\$$cachevar + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + if test x$ac_success = xyes; then + break + fi + done + fi + ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + + if test x$ax_cxx_compile_cxx17_required = xtrue; then + if test x$ac_success = xno; then + as_fn_error $? "*** A compiler with support for C++17 language features is required." "$LINENO" 5 + fi + fi + if test x$ac_success = xno; then + HAVE_CXX17=0 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: No compiler with C++17 support was found" >&5 +printf "%s\n" "$as_me: No compiler with C++17 support was found" >&6;} + else + HAVE_CXX17=1 + +printf "%s\n" "#define HAVE_CXX17 1" >>confdefs.h + + fi + + am__api_version='1.16' @@ -5928,7 +6818,7 @@ # Define the identity of the package. PACKAGE='c-ares' - VERSION='1.34.5' + VERSION='1.34.9' printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h @@ -22491,6 +23381,95 @@ +if test "x$host_vendor" = "xapple" +then : + + cares_save_cflags="$CFLAGS" + + + + +for flag in -Werror=partial-availability; do + as_CACHEVAR=`printf "%s\n" "ax_cv_check_cflags__$flag" | $as_tr_sh` +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether the C compiler accepts $flag" >&5 +printf %s "checking whether the C compiler accepts $flag... " >&6; } +if eval test \${$as_CACHEVAR+y} +then : + printf %s "(cached) " >&6 +else $as_nop + + ax_check_save_flags=$CFLAGS + if test x"$GCC" = xyes ; then + add_gnu_werror="-Werror" + fi + CFLAGS="$CFLAGS $flag $add_gnu_werror" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + +int +main (void) +{ + + ; + return 0; +} +_ACEOF +if ac_fn_c_try_compile "$LINENO" +then : + eval "$as_CACHEVAR=yes" +else $as_nop + eval "$as_CACHEVAR=no" +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + CFLAGS=$ax_check_save_flags +fi +eval ac_res=\$$as_CACHEVAR + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } +if eval test \"x\$"$as_CACHEVAR"\" = x"yes" +then : + +if test ${CFLAGS+y} +then : + + case " $CFLAGS " in #( + *" $flag "*) : + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: : CFLAGS already contains \$flag"; } >&5 + (: CFLAGS already contains $flag) 2>&5 + ac_status=$? + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } ;; #( + *) : + + as_fn_append CFLAGS " $flag" + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: : CFLAGS=\"\$CFLAGS\""; } >&5 + (: CFLAGS="$CFLAGS") 2>&5 + ac_status=$? + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } + ;; +esac + +else $as_nop + + CFLAGS=$flag + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: : CFLAGS=\"\$CFLAGS\""; } >&5 + (: CFLAGS="$CFLAGS") 2>&5 + ac_status=$? + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; } + +fi + +else $as_nop + : +fi + +done + + +fi + ac_fn_check_decl "$LINENO" "strnlen" "ac_cv_have_decl_strnlen" "$cares_all_includes " "$ac_c_undeclared_builtin_options" "CFLAGS" if test "x$ac_cv_have_decl_strnlen" = xyes @@ -22891,6 +23870,12 @@ printf "%s\n" "#define HAVE___SYSTEM_PROPERTY_GET 1" >>confdefs.h fi +if test "x$host_vendor" = "xapple" +then : + + CFLAGS="$cares_save_cflags" + +fi @@ -23369,7 +24354,7 @@ case $host_os in - freebsd*) + freebsd*|midnightbsd*) # -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able) # lthread: LinuxThreads port on FreeBSD (also preferred to -pthread) @@ -24066,15 +25051,6 @@ -if test "x$build_tests" != "xno" -a "x$HAVE_CXX14" = "0" ; then - if test "x$build_tests" = "xmaybe" ; then - { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cannot build tests without a CXX14 compiler" >&5 -printf "%s\n" "$as_me: WARNING: cannot build tests without a CXX14 compiler" >&2;} - build_tests=no - else - as_fn_error $? "*** Building tests requires a CXX14 compiler" "$LINENO" 5 - fi -fi if test "x$build_tests" != "xno" -a "x$cross_compiling" = "xyes" ; then if test "x$build_tests" = "xmaybe" ; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: WARNING: cannot build tests when cross compiling" >&5 @@ -24555,12 +25531,961 @@ fi fi - fi + +pkg_failed=no +{ printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking for gmock >= 1.17.0" >&5 +printf %s "checking for gmock >= 1.17.0... " >&6; } + +if test -n "$GMOCK117_CFLAGS"; then + pkg_cv_GMOCK117_CFLAGS="$GMOCK117_CFLAGS" + elif test -n "$PKG_CONFIG"; then + if test -n "$PKG_CONFIG" && \ + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"gmock >= 1.17.0\""; } >&5 + ($PKG_CONFIG --exists --print-errors "gmock >= 1.17.0") 2>&5 + ac_status=$? + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + pkg_cv_GMOCK117_CFLAGS=`$PKG_CONFIG --cflags "gmock >= 1.17.0" 2>/dev/null` + test "x$?" != "x0" && pkg_failed=yes +else + pkg_failed=yes fi -if test "x$build_tests" != "xno" ; then - build_tests=yes + else + pkg_failed=untried +fi +if test -n "$GMOCK117_LIBS"; then + pkg_cv_GMOCK117_LIBS="$GMOCK117_LIBS" + elif test -n "$PKG_CONFIG"; then + if test -n "$PKG_CONFIG" && \ + { { printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$PKG_CONFIG --exists --print-errors \"gmock >= 1.17.0\""; } >&5 + ($PKG_CONFIG --exists --print-errors "gmock >= 1.17.0") 2>&5 + ac_status=$? + printf "%s\n" "$as_me:${as_lineno-$LINENO}: \$? = $ac_status" >&5 + test $ac_status = 0; }; then + pkg_cv_GMOCK117_LIBS=`$PKG_CONFIG --libs "gmock >= 1.17.0" 2>/dev/null` + test "x$?" != "x0" && pkg_failed=yes +else + pkg_failed=yes +fi + else + pkg_failed=untried +fi + + + +if test $pkg_failed = yes; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } + +if $PKG_CONFIG --atleast-pkgconfig-version 0.20; then + _pkg_short_errors_supported=yes +else + _pkg_short_errors_supported=no +fi + if test $_pkg_short_errors_supported = yes; then + GMOCK117_PKG_ERRORS=`$PKG_CONFIG --short-errors --print-errors --cflags --libs "gmock >= 1.17.0" 2>&1` + else + GMOCK117_PKG_ERRORS=`$PKG_CONFIG --print-errors --cflags --libs "gmock >= 1.17.0" 2>&1` + fi + # Put the nasty error message in config.log where it belongs + echo "$GMOCK117_PKG_ERRORS" >&5 + + have_gmock_v117=no +elif test $pkg_failed = untried; then + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: no" >&5 +printf "%s\n" "no" >&6; } + have_gmock_v117=no +else + GMOCK117_CFLAGS=$pkg_cv_GMOCK117_CFLAGS + GMOCK117_LIBS=$pkg_cv_GMOCK117_LIBS + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: yes" >&5 +printf "%s\n" "yes" >&6; } + have_gmock_v117=yes +fi + if test "x$have_gmock_v117" = "xyes" ; then + ax_cxx_compile_alternatives="17 1z" ax_cxx_compile_cxx17_required=true + ac_ext=cpp +ac_cpp='$CXXCPP $CPPFLAGS' +ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CXX -o conftest$ac_exeext $CXXFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_cxx_compiler_gnu + ac_success=no + + + + + + if test x$ac_success = xno; then + for alternative in ${ax_cxx_compile_alternatives}; do + for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}" MSVC; do + if test x"$switch" = xMSVC; then + switch=-std:c++${alternative} + cachevar=`printf "%s\n" "ax_cv_cxx_compile_cxx17_${switch}_MSVC" | $as_tr_sh` + else + cachevar=`printf "%s\n" "ax_cv_cxx_compile_cxx17_$switch" | $as_tr_sh` + fi + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking whether $CXX supports C++17 features with $switch" >&5 +printf %s "checking whether $CXX supports C++17 features with $switch... " >&6; } +if eval test \${$cachevar+y} +then : + printf %s "(cached) " >&6 +else $as_nop + ac_save_CXX="$CXX" + CXX="$CXX $switch" + cat confdefs.h - <<_ACEOF >conftest.$ac_ext +/* end confdefs.h. */ + + +// If the compiler admits that it is not ready for C++11, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +// MSVC always sets __cplusplus to 199711L in older versions; newer versions +// only set it correctly if /Zc:__cplusplus is specified as well as a +// /std:c++NN switch: +// +// https://devblogs.microsoft.com/cppblog/msvc-now-correctly-reports-__cplusplus/ +// +// The value __cplusplus ought to have is available in _MSVC_LANG since +// Visual Studio 2015 Update 3: +// +// https://learn.microsoft.com/en-us/cpp/preprocessor/predefined-macros +// +// This was also the first MSVC version to support C++14 so we can't use the +// value of either __cplusplus or _MSVC_LANG to quickly rule out MSVC having +// C++11 or C++14 support, but we can check _MSVC_LANG for C++17 and later. +#elif __cplusplus < 201103L && !defined _MSC_VER + +#error "This is not a C++11 compiler" + +#else + +namespace cxx11 +{ + + namespace test_static_assert + { + + template + struct check + { + static_assert(sizeof(int) <= sizeof(T), "not big enough"); + }; + + } + + namespace test_final_override + { + + struct Base + { + virtual ~Base() {} + virtual void f() {} + }; + + struct Derived : public Base + { + virtual ~Derived() override {} + virtual void f() override {} + }; + + } + + namespace test_double_right_angle_brackets + { + + template < typename T > + struct check {}; + + typedef check single_type; + typedef check> double_type; + typedef check>> triple_type; + typedef check>>> quadruple_type; + + } + + namespace test_decltype + { + + int + f() + { + int a = 1; + decltype(a) b = 2; + return a + b; + } + + } + + namespace test_type_deduction + { + + template < typename T1, typename T2 > + struct is_same + { + static const bool value = false; + }; + + template < typename T > + struct is_same + { + static const bool value = true; + }; + + template < typename T1, typename T2 > + auto + add(T1 a1, T2 a2) -> decltype(a1 + a2) + { + return a1 + a2; + } + + int + test(const int c, volatile int v) + { + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == false, ""); + auto ac = c; + auto av = v; + auto sumi = ac + av + 'x'; + auto sumf = ac + av + 1.0; + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == true, ""); + return (sumf > 0.0) ? sumi : add(c, v); + } + + } + + namespace test_noexcept + { + + int f() { return 0; } + int g() noexcept { return 0; } + + static_assert(noexcept(f()) == false, ""); + static_assert(noexcept(g()) == true, ""); + + } + + namespace test_constexpr + { + + template < typename CharT > + unsigned long constexpr + strlen_c_r(const CharT *const s, const unsigned long acc) noexcept + { + return *s ? strlen_c_r(s + 1, acc + 1) : acc; + } + + template < typename CharT > + unsigned long constexpr + strlen_c(const CharT *const s) noexcept + { + return strlen_c_r(s, 0UL); + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("1") == 1UL, ""); + static_assert(strlen_c("example") == 7UL, ""); + static_assert(strlen_c("another\0example") == 7UL, ""); + + } + + namespace test_rvalue_references + { + + template < int N > + struct answer + { + static constexpr int value = N; + }; + + answer<1> f(int&) { return answer<1>(); } + answer<2> f(const int&) { return answer<2>(); } + answer<3> f(int&&) { return answer<3>(); } + + void + test() + { + int i = 0; + const int c = 0; + static_assert(decltype(f(i))::value == 1, ""); + static_assert(decltype(f(c))::value == 2, ""); + static_assert(decltype(f(0))::value == 3, ""); + } + + } + + namespace test_uniform_initialization + { + + struct test + { + static const int zero {}; + static const int one {1}; + }; + + static_assert(test::zero == 0, ""); + static_assert(test::one == 1, ""); + + } + + namespace test_lambdas + { + + void + test1() + { + auto lambda1 = [](){}; + auto lambda2 = lambda1; + lambda1(); + lambda2(); + } + + int + test2() + { + auto a = [](int i, int j){ return i + j; }(1, 2); + auto b = []() -> int { return '0'; }(); + auto c = [=](){ return a + b; }(); + auto d = [&](){ return c; }(); + auto e = [a, &b](int x) mutable { + const auto identity = [](int y){ return y; }; + for (auto i = 0; i < a; ++i) + a += b--; + return x + identity(a + b); + }(0); + return a + b + c + d + e; + } + + int + test3() + { + const auto nullary = [](){ return 0; }; + const auto unary = [](int x){ return x; }; + using nullary_t = decltype(nullary); + using unary_t = decltype(unary); + const auto higher1st = [](nullary_t f){ return f(); }; + const auto higher2nd = [unary](nullary_t f1){ + return [unary, f1](unary_t f2){ return f2(unary(f1())); }; + }; + return higher1st(nullary) + higher2nd(nullary)(unary); + } + + } + + namespace test_variadic_templates + { + + template + struct sum; + + template + struct sum + { + static constexpr auto value = N0 + sum::value; + }; + + template <> + struct sum<> + { + static constexpr auto value = 0; + }; + + static_assert(sum<>::value == 0, ""); + static_assert(sum<1>::value == 1, ""); + static_assert(sum<23>::value == 23, ""); + static_assert(sum<1, 2>::value == 3, ""); + static_assert(sum<5, 5, 11>::value == 21, ""); + static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, ""); + + } + + // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae + // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function + // because of this. + namespace test_template_alias_sfinae + { + + struct foo {}; + + template + using member = typename T::member_type; + + template + void func(...) {} + + template + void func(member*) {} + + void test(); + + void test() { func(0); } - ax_cxx_compile_alternatives="14 1y" ax_cxx_compile_cxx14_required=true + } + +} // namespace cxx11 + +#endif // __cplusplus >= 201103L + + + + +// If the compiler admits that it is not ready for C++14, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201402L && !defined _MSC_VER + +#error "This is not a C++14 compiler" + +#else + +namespace cxx14 +{ + + namespace test_polymorphic_lambdas + { + + int + test() + { + const auto lambda = [](auto&&... args){ + const auto istiny = [](auto x){ + return (sizeof(x) == 1UL) ? 1 : 0; + }; + const int aretiny[] = { istiny(args)... }; + return aretiny[0]; + }; + return lambda(1, 1L, 1.0f, '1'); + } + + } + + namespace test_binary_literals + { + + constexpr auto ivii = 0b0000000000101010; + static_assert(ivii == 42, "wrong value"); + + } + + namespace test_generalized_constexpr + { + + template < typename CharT > + constexpr unsigned long + strlen_c(const CharT *const s) noexcept + { + auto length = 0UL; + for (auto p = s; *p; ++p) + ++length; + return length; + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("x") == 1UL, ""); + static_assert(strlen_c("test") == 4UL, ""); + static_assert(strlen_c("another\0test") == 7UL, ""); + + } + + namespace test_lambda_init_capture + { + + int + test() + { + auto x = 0; + const auto lambda1 = [a = x](int b){ return a + b; }; + const auto lambda2 = [a = lambda1(x)](){ return a; }; + return lambda2(); + } + + } + + namespace test_digit_separators + { + + constexpr auto ten_million = 100'000'000; + static_assert(ten_million == 100000000, ""); + + } + + namespace test_return_type_deduction + { + + auto f(int& x) { return x; } + decltype(auto) g(int& x) { return x; } + + template < typename T1, typename T2 > + struct is_same + { + static constexpr auto value = false; + }; + + template < typename T > + struct is_same + { + static constexpr auto value = true; + }; + + int + test() + { + auto x = 0; + static_assert(is_same::value, ""); + static_assert(is_same::value, ""); + return x; + } + + } + +} // namespace cxx14 + +#endif // __cplusplus >= 201402L + + + + +// If the compiler admits that it is not ready for C++17, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 201703L + +#error "This is not a C++17 compiler" + +#else + +#include +#include +#include + +namespace cxx17 +{ + + namespace test_constexpr_lambdas + { + + constexpr int foo = [](){return 42;}(); + + } + + namespace test::nested_namespace::definitions + { + + } + + namespace test_fold_expression + { + + template + int multiply(Args... args) + { + return (args * ... * 1); + } + + template + bool all(Args... args) + { + return (args && ...); + } + + } + + namespace test_extended_static_assert + { + + static_assert (true); + + } + + namespace test_auto_brace_init_list + { + + auto foo = {5}; + auto bar {5}; + + static_assert(std::is_same, decltype(foo)>::value); + static_assert(std::is_same::value); + } + + namespace test_typename_in_template_template_parameter + { + + template typename X> struct D; + + } + + namespace test_fallthrough_nodiscard_maybe_unused_attributes + { + + int f1() + { + return 42; + } + + [[nodiscard]] int f2() + { + [[maybe_unused]] auto unused = f1(); + + switch (f1()) + { + case 17: + f1(); + [[fallthrough]]; + case 42: + f1(); + } + return f1(); + } + + } + + namespace test_extended_aggregate_initialization + { + + struct base1 + { + int b1, b2 = 42; + }; + + struct base2 + { + base2() { + b3 = 42; + } + int b3; + }; + + struct derived : base1, base2 + { + int d; + }; + + derived d1 {{1, 2}, {}, 4}; // full initialization + derived d2 {{}, {}, 4}; // value-initialized bases + + } + + namespace test_general_range_based_for_loop + { + + struct iter + { + int i; + + int& operator* () + { + return i; + } + + const int& operator* () const + { + return i; + } + + iter& operator++() + { + ++i; + return *this; + } + }; + + struct sentinel + { + int i; + }; + + bool operator== (const iter& i, const sentinel& s) + { + return i.i == s.i; + } + + bool operator!= (const iter& i, const sentinel& s) + { + return !(i == s); + } + + struct range + { + iter begin() const + { + return {0}; + } + + sentinel end() const + { + return {5}; + } + }; + + void f() + { + range r {}; + + for (auto i : r) + { + [[maybe_unused]] auto v = i; + } + } + + } + + namespace test_lambda_capture_asterisk_this_by_value + { + + struct t + { + int i; + int foo() + { + return [*this]() + { + return i; + }(); + } + }; + + } + + namespace test_enum_class_construction + { + + enum class byte : unsigned char + {}; + + byte foo {42}; + + } + + namespace test_constexpr_if + { + + template + int f () + { + if constexpr(cond) + { + return 13; + } + else + { + return 42; + } + } + + } + + namespace test_selection_statement_with_initializer + { + + int f() + { + return 13; + } + + int f2() + { + if (auto i = f(); i > 0) + { + return 3; + } + + switch (auto i = f(); i + 4) + { + case 17: + return 2; + + default: + return 1; + } + } + + } + + namespace test_template_argument_deduction_for_class_templates + { + + template + struct pair + { + pair (T1 p1, T2 p2) + : m1 {p1}, + m2 {p2} + {} + + T1 m1; + T2 m2; + }; + + void f() + { + [[maybe_unused]] auto p = pair{13, 42u}; + } + + } + + namespace test_non_type_auto_template_parameters + { + + template + struct B + {}; + + B<5> b1; + B<'a'> b2; + + } + + namespace test_structured_bindings + { + + int arr[2] = { 1, 2 }; + std::pair pr = { 1, 2 }; + + auto f1() -> int(&)[2] + { + return arr; + } + + auto f2() -> std::pair& + { + return pr; + } + + struct S + { + int x1 : 2; + volatile double y1; + }; + + S f3() + { + return {}; + } + + auto [ x1, y1 ] = f1(); + auto& [ xr1, yr1 ] = f1(); + auto [ x2, y2 ] = f2(); + auto& [ xr2, yr2 ] = f2(); + const auto [ x3, y3 ] = f3(); + + } + + namespace test_exception_spec_type_system + { + + struct Good {}; + struct Bad {}; + + void g1() noexcept; + void g2(); + + template + Bad + f(T*, T*); + + template + Good + f(T1*, T2*); + + static_assert (std::is_same_v); + + } + + namespace test_inline_variables + { + + template void f(T) + {} + + template inline T g(T) + { + return T{}; + } + + template<> inline void f<>(int) + {} + + template<> int g<>(int) + { + return 5; + } + + } + +} // namespace cxx17 + +#endif // (defined _MSVC_LANG ? _MSVC_LANG : __cplusplus) < 201703L + + + +_ACEOF +if ac_fn_cxx_try_compile "$LINENO" +then : + eval $cachevar=yes +else $as_nop + eval $cachevar=no +fi +rm -f core conftest.err conftest.$ac_objext conftest.beam conftest.$ac_ext + CXX="$ac_save_CXX" +fi +eval ac_res=\$$cachevar + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5 +printf "%s\n" "$ac_res" >&6; } + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + if test x$ac_success = xyes; then + break + fi + done + fi + ac_ext=c +ac_cpp='$CPP $CPPFLAGS' +ac_compile='$CC -c $CFLAGS $CPPFLAGS conftest.$ac_ext >&5' +ac_link='$CC -o conftest$ac_exeext $CFLAGS $CPPFLAGS $LDFLAGS conftest.$ac_ext $LIBS >&5' +ac_compiler_gnu=$ac_cv_c_compiler_gnu + + if test x$ax_cxx_compile_cxx17_required = xtrue; then + if test x$ac_success = xno; then + as_fn_error $? "*** A compiler with support for C++17 language features is required." "$LINENO" 5 + fi + fi + if test x$ac_success = xno; then + HAVE_CXX17=0 + { printf "%s\n" "$as_me:${as_lineno-$LINENO}: No compiler with C++17 support was found" >&5 +printf "%s\n" "$as_me: No compiler with C++17 support was found" >&6;} + else + HAVE_CXX17=1 + +printf "%s\n" "#define HAVE_CXX17 1" >>confdefs.h + + fi + + + else + ax_cxx_compile_alternatives="14 1y" ax_cxx_compile_cxx14_required=true ac_ext=cpp ac_cpp='$CXXCPP $CPPFLAGS' ac_compile='$CXX -c $CXXFLAGS $CPPFLAGS conftest.$ac_ext >&5' @@ -25063,6 +26988,12 @@ fi + fi + fi +fi +if test "x$build_tests" != "xno" ; then + build_tests=yes + if test "$ac_cv_native_windows" != "yes" ; then @@ -25167,7 +27098,7 @@ case $host_os in - freebsd*) + freebsd*|midnightbsd*) # -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able) # lthread: LinuxThreads port on FreeBSD (also preferred to -pthread) @@ -26402,7 +28333,7 @@ # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by c-ares $as_me 1.34.5, which was +This file was extended by c-ares $as_me 1.34.9, which was generated by GNU Autoconf 2.71. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -26470,7 +28401,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ -c-ares config.status 1.34.5 +c-ares config.status 1.34.9 configured by $0, generated by GNU Autoconf 2.71, with options \\"\$ac_cs_config\\" diff -Nru c-ares-1.34.5/configure.ac c-ares-1.34.9/configure.ac --- c-ares-1.34.5/configure.ac 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/configure.ac 2026-10-20 02:31:51.000000000 +0000 @@ -2,10 +2,10 @@ dnl SPDX-License-Identifier: MIT AC_PREREQ([2.69]) -AC_INIT([c-ares], [1.34.5], +AC_INIT([c-ares], [1.34.9], [c-ares mailing list: http://lists.haxx.se/listinfo/c-ares]) -CARES_VERSION_INFO="21:4:19" +CARES_VERSION_INFO="21:8:19" dnl This flag accepts an argument of the form current[:revision[:age]]. So, dnl passing -version-info 3:12:1 sets current to 3, revision to 12, and age to dnl 1. @@ -40,7 +40,8 @@ AC_CONFIG_AUX_DIR(config) AC_CONFIG_MACRO_DIR([m4]) AC_USE_SYSTEM_EXTENSIONS -AX_CXX_COMPILE_STDCXX_14([noext],[optional]) +AX_CXX_COMPILE_STDCXX(14,[noext],[optional]) +AX_CXX_COMPILE_STDCXX(17,[noext],[optional]) AM_INIT_AUTOMAKE([foreign subdir-objects 1.9.6]) LT_INIT([win32-dll,pic,disable-fast-install,aix-soname=svr4]) AC_LANG([C]) @@ -550,6 +551,14 @@ dnl which would require we check each individually and provide function arguments dnl for the test. +dnl On Apple hosts, header availability macros declare functions newer than the +dnl deployment target. Ensure -Werror=partial-availability is used during +dnl declaration checks so too-new symbols are correctly reported as missing. +AS_IF([test "x$host_vendor" = "xapple"], [ + cares_save_cflags="$CFLAGS" + AX_APPEND_COMPILE_FLAGS([-Werror=partial-availability], [CFLAGS]) +]) + AC_CHECK_DECL(strnlen, [AC_DEFINE([HAVE_STRNLEN], 1, [Define to 1 if you have `strnlen`] )], [], $cares_all_includes) AC_CHECK_DECL(memmem, [AC_DEFINE([HAVE_MEMMEM], 1, [Define to 1 if you have `memmem`] )], [], $cares_all_includes) AC_CHECK_DECL(recv, [AC_DEFINE([HAVE_RECV], 1, [Define to 1 if you have `recv`] )], [], $cares_all_includes) @@ -600,6 +609,9 @@ AC_CHECK_DECL(NotifyIpInterfaceChange, [AC_DEFINE([HAVE_NOTIFYIPINTERFACECHANGE], 1, [Define to 1 if you have `NotifyIpInterfaceChange`] )], [], $cares_all_includes) AC_CHECK_DECL(RegisterWaitForSingleObject, [AC_DEFINE([HAVE_REGISTERWAITFORSINGLEOBJECT], 1, [Define to 1 if you have `RegisterWaitForSingleObject`])], [], $cares_all_includes) AC_CHECK_DECL(__system_property_get, [AC_DEFINE([HAVE___SYSTEM_PROPERTY_GET], 1, [Define to 1 if you have `__system_property_get`] )], [], $cares_all_includes) +AS_IF([test "x$host_vendor" = "xapple"], [ + CFLAGS="$cares_save_cflags" +]) dnl ############################################################################### @@ -806,14 +818,6 @@ dnl ******** TESTS ******* -if test "x$build_tests" != "xno" -a "x$HAVE_CXX14" = "0" ; then - if test "x$build_tests" = "xmaybe" ; then - AC_MSG_WARN([cannot build tests without a CXX14 compiler]) - build_tests=no - else - AC_MSG_ERROR([*** Building tests requires a CXX14 compiler]) - fi -fi if test "x$build_tests" != "xno" -a "x$cross_compiling" = "xyes" ; then if test "x$build_tests" = "xmaybe" ; then AC_MSG_WARN([cannot build tests when cross compiling]) @@ -844,12 +848,19 @@ ARES_CHECK_USER_NAMESPACE ARES_CHECK_UTS_NAMESPACE fi + PKG_CHECK_MODULES([GMOCK117], [gmock >= 1.17.0], [ have_gmock_v117=yes ], [ have_gmock_v117=no ]) + if test "x$have_gmock_v117" = "xyes" ; then + dnl GMock v1.17.0 requires C++17 or higher + AX_CXX_COMPILE_STDCXX(17,[noext],[mandatory]) + else + dnl older version needed v1.14.0 + AX_CXX_COMPILE_STDCXX(14,[noext],[mandatory]) + fi fi fi if test "x$build_tests" != "xno" ; then build_tests=yes - AX_CXX_COMPILE_STDCXX_14([noext],[mandatory]) if test "$ac_cv_native_windows" != "yes" ; then AX_PTHREAD([ CARES_TEST_PTHREADS="yes" ], [ AC_MSG_ERROR([threading required for tests]) ]) fi diff -Nru c-ares-1.34.5/debian/changelog c-ares-1.34.9/debian/changelog --- c-ares-1.34.5/debian/changelog 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/changelog 2026-11-11 05:51:11.000000000 +0000 @@ -1,9 +1,60 @@ +c-ares (1.34.9-1~deb13u1) trixie-security; urgency=high + + * Non-maintainer upload by the Security Team. + * New upstream stable point release. + * CVE-2026-33630: Fix query-completion use-after-free and double-free. + * CVE-2026-69184: Bound DNS compression pointer traversal. + * CVE-2026-69186: Limit record-count memory amplification. + * GHSA-wpr3-8q3r-4q8c: Fix cancellation after connection failure. + * GHSA-fqh5-3x7m-wp44: Reject replies from the wrong server. + * GHSA-97vq-4mxv-7vhm: Fix lookup use-after-free after failed shutdown. + * GHSA-rf6g-ww74-97qh: Fix cancellation and channel shutdown use-after-free. + * GHSA-mw23-pfg6-2x9r: Fix connection use-after-free in socket callback. + * GHSA-v33j-65h4-4ch8: Fix cancellation in server-status callback. + * GHSA-pj7p-v3cw-hq66: Fix server-record use-after-free on reconfiguration. + * GHSA-grv7-fq5q-9vfc: Fix cached-answer use-after-free. + * GHSA-ph6c-3cvx-mq6v: Fix concurrent server update buffer overflow. + * GHSA-cfcv-8v2j-qj6r: Fix uninitialized IPv6 address data. + * GHSA-wrgc-prmw-27xh: Prevent cache reuse with different EDNS options. + * Drop CVE-2025-62408 patch, included upstream. + + -- Aron Xu Wed, 11 Nov 2026 13:51:11 +0800 + +c-ares (1.34.9-1) unstable; urgency=high + + * Imported Upstream version 1.34.9 (with lots of security fixes) + + -- Gregor Jasny Mon, 26 Oct 2026 22:02:49 +0100 + +c-ares (1.34.8-1) unstable; urgency=high + + * Imported Upstream version 1.34.8 (fixes regression in 1.34.7) + + -- Gregor Jasny Tue, 07 Jul 2026 19:31:45 +0200 + +c-ares (1.34.7-1) unstable; urgency=high + + [ Gregor Jasny ] + * Imported Upstream version 1.34.7 (fixes CVE-2026-33630) + + [ Bastian Germann ] + * Prevent googletest installation for nocheck + + -- Gregor Jasny Mon, 06 Jul 2026 21:40:21 +0200 + +c-ares (1.34.6-1) unstable; urgency=high + + * Imported Upstream version 1.34.6 (fixes CVE-2025-62408) + + -- Gregor Jasny Mon, 08 Dec 2025 20:48:36 +0100 + c-ares (1.34.5-1+deb13u1) trixie-security; urgency=medium * Apply patch to fix use-after-free (fixes CVE-2025-62408) -- Gregor Jasny Sun, 14 Dec 2025 15:24:14 +0100 + c-ares (1.34.5-1) unstable; urgency=high * Imported Upstream version 1.34.5 (fixes CVE-2025-31498) diff -Nru c-ares-1.34.5/debian/copyright c-ares-1.34.9/debian/copyright --- c-ares-1.34.5/debian/copyright 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/copyright 2026-10-26 21:02:49.000000000 +0000 @@ -1,4 +1,6 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: c-ares +Upstream-Contact: c-ares@lists.haxx.se Files: * Copyright: 2023, 2024, The c-ares project and its contributors. @@ -267,10 +269,6 @@ 2008, Benjamin Kosnik License: FSFAP -Files: m4/ax_cxx_compile_stdcxx_14.m4 -Copyright: 2015, Moritz Klammler -License: FSFAP - Files: m4/ax_file_escapes.m4 Copyright: 2008, Tom Howard License: FSFAP @@ -605,9 +603,7 @@ General Public License for more details. . You should have received a copy of the GNU General Public License - along with this program; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - 02110-1301, USA. + along with this program; if not, see . . As a special exception to the GNU General Public License, if you distribute this file as part of a program that contains a diff -Nru c-ares-1.34.5/debian/libcares2.symbols c-ares-1.34.9/debian/libcares2.symbols --- c-ares-1.34.5/debian/libcares2.symbols 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/libcares2.symbols 2026-10-26 21:02:49.000000000 +0000 @@ -238,6 +238,7 @@ ares_llist_replace_destructor@Base 1.34.0 ares_malloc@Base 1.34.0 ares_malloc_zero@Base 1.34.0 + ares_malloc_zero_array@Base 1.34.7 ares_memeq@Base 1.34.0 ares_memeq_ci@Base 1.34.0 ares_memmem@Base 1.34.0 @@ -264,6 +265,7 @@ ares_queue_wait_empty@Base 1.27.0 ares_realloc@Base 1.34.0 ares_realloc_zero@Base 1.34.0 + ares_realloc_zero_array@Base 1.34.7 ares_reinit@Base 1.22.0 ares_save_options@Base 1.7.0 ares_search@Base 1.7.0 @@ -289,6 +291,7 @@ ares_str_isprint@Base 1.34.0 ares_str_lower@Base 1.34.0 ares_str_ltrim@Base 1.34.0 + ares_str_parse_uint@Base 1.34.7 ares_str_rtrim@Base 1.34.0 ares_str_trim@Base 1.34.0 ares_strcasecmp@Base 1.34.0 diff -Nru c-ares-1.34.5/debian/patches/CVE-2025-62408.diff c-ares-1.34.9/debian/patches/CVE-2025-62408.diff --- c-ares-1.34.5/debian/patches/CVE-2025-62408.diff 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/patches/CVE-2025-62408.diff 1970-01-01 00:00:00.000000000 +0000 @@ -1,379 +0,0 @@ -From: Brad House -Date: Mon, 8 Dec 2025 10:12:08 -0500 -Subject: Fix use-after-free (CVE-2025-62408) -Origin: upstream, https://github.com/c-ares/c-ares/commit/714bf5675c541bd1e668a8db8e67ce012651e618 -Forwarded: not-needed -Bug: https://github.com/c-ares/c-ares/security/advisories/GHSA-jq53-42q6-pqr5 - ---- - src/lib/ares_private.h | 10 ++-- - src/lib/ares_process.c | 114 +++++++++++++++++++++++++++++++++------------- - src/lib/ares_qcache.c | 20 ++++++-- - test/ares-test-mock-ai.cc | 32 +++++++++++++ - 4 files changed, 135 insertions(+), 41 deletions(-) - -diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h -index 3d7cea3..8be2580 100644 ---- a/src/lib/ares_private.h -+++ b/src/lib/ares_private.h -@@ -321,7 +321,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server /* Optional */, - ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now, - ares_status_t status, - ares_bool_t inc_try_count, -- const ares_dns_record_t *dnsrec, -+ ares_dns_record_t *dnsrec, - ares_array_t **requeue); - - /*! Count the number of labels (dots+1) in a domain */ -@@ -594,10 +594,10 @@ ares_status_t ares_qcache_create(ares_rand_state *rand_state, - unsigned int max_ttl, - ares_qcache_t **cache_out); - void ares_qcache_flush(ares_qcache_t *cache); --ares_status_t ares_qcache_insert(ares_channel_t *channel, -- const ares_timeval_t *now, -- const ares_query_t *query, -- ares_dns_record_t *dnsrec); -+ares_status_t ares_qcache_insert(ares_channel_t *channel, -+ const ares_timeval_t *now, -+ const ares_query_t *query, -+ const ares_dns_record_t *dnsrec); - ares_status_t ares_qcache_fetch(ares_channel_t *channel, - const ares_timeval_t *now, - const ares_dns_record_t *dnsrec, -diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c -index c5834fa..3de75db 100644 ---- a/src/lib/ares_process.c -+++ b/src/lib/ares_process.c -@@ -64,7 +64,8 @@ static ares_bool_t same_questions(const ares_query_t *query, - const ares_dns_record_t *arec); - static void end_query(ares_channel_t *channel, ares_server_t *server, - ares_query_t *query, ares_status_t status, -- const ares_dns_record_t *dnsrec); -+ ares_dns_record_t *dnsrec, -+ ares_array_t **requeue); - - static void ares_query_remove_from_conn(ares_query_t *query) - { -@@ -511,16 +512,27 @@ static ares_status_t read_conn_packets(ares_conn_t *conn) - return ARES_SUCCESS; - } - -+typedef enum { -+ REQUEUE_REQUEUE = 1, -+ REQUEUE_ENDQUERY = 2 -+} requeue_type_t; -+ - /* Simple data structure to store a query that needs to be requeued with - * optional server */ - typedef struct { -- unsigned short qid; -- ares_server_t *server; /* optional */ -+ requeue_type_t type; /* type of entry, requeue or endquery */ -+ unsigned short qid; /* query id */ -+ ares_server_t *server; /* requeue only: optional */ -+ ares_status_t status; /* endquery only */ -+ ares_dns_record_t *dnsrec; /* endquery only: optional */ - } ares_requeue_t; - --static ares_status_t ares_append_requeue(ares_array_t **requeue, -- ares_query_t *query, -- ares_server_t *server) -+static ares_status_t ares_append_requeue_int(ares_array_t **requeue, -+ requeue_type_t type, -+ ares_query_t *query, -+ ares_server_t *server, -+ ares_status_t status, -+ ares_dns_record_t *dnsrec) - { - ares_requeue_t entry; - -@@ -533,16 +545,36 @@ static ares_status_t ares_append_requeue(ares_array_t **requeue, - - ares_query_remove_from_conn(query); - -+ entry.type = type; - entry.qid = query->qid; - entry.server = server; -+ entry.status = status; -+ entry.dnsrec = dnsrec; - return ares_array_insertdata_last(*requeue, &entry); - } - -+static ares_status_t ares_append_requeue(ares_array_t **requeue, -+ ares_query_t *query, -+ ares_server_t *server) -+{ -+ return ares_append_requeue_int(requeue, REQUEUE_REQUEUE, query, server, 0, -+ NULL); -+} -+ -+static ares_status_t ares_append_endqueue(ares_array_t **requeue, -+ ares_query_t *query, -+ ares_status_t status, -+ ares_dns_record_t *dnsrec) -+{ -+ return ares_append_requeue_int(requeue, REQUEUE_ENDQUERY, query, NULL, status, -+ dnsrec); -+} -+ - static ares_status_t read_answers(ares_conn_t *conn, const ares_timeval_t *now) - { - ares_status_t status; -- ares_channel_t *channel = conn->server->channel; -- ares_array_t *requeue = NULL; -+ ares_channel_t *channel = conn->server->channel; -+ ares_array_t *requeue = NULL; - - /* Process all queued answers */ - while (1) { -@@ -602,18 +634,30 @@ cleanup: - break; - } - -- /* Query disappeared */ - query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid); -- if (query == NULL) { -- continue; -- } - -- internal_status = ares_send_query(entry.server, query, now); -- /* We only care about ARES_ENOMEM */ -- if (internal_status == ARES_ENOMEM) { -- status = ARES_ENOMEM; -+ if (entry.type == REQUEUE_REQUEUE) { -+ /* query disappeared */ -+ if (query == NULL) { -+ continue; -+ } -+ internal_status = ares_send_query(entry.server, query, now); -+ /* We only care about ARES_ENOMEM */ -+ if (internal_status == ARES_ENOMEM) { -+ status = ARES_ENOMEM; -+ } -+ } else { /* REQUEUE_ENDQUERY */ -+ if (query != NULL) { -+ query->callback(query->arg, entry.status, query->timeouts, entry.dnsrec); -+ ares_free_query(query); -+ } -+ ares_dns_record_destroy(entry.dnsrec); - } - } -+ /* Don't forget to send notification if queue emptied */ -+ if (requeue != NULL) { -+ ares_queue_notify_empty(channel); -+ } - ares_array_destroy(requeue); - - return status; -@@ -669,7 +713,7 @@ static ares_status_t process_timeouts(ares_channel_t *channel, - conn = query->conn; - server_increment_failures(conn->server, query->using_tcp); - status = ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, -- NULL); -+ NULL); - if (status == ARES_ENOMEM) { - goto done; - } -@@ -824,7 +868,7 @@ static ares_status_t process_answer(ares_channel_t *channel, - if (issue_might_be_edns(query->query, rdnsrec)) { - status = rewrite_without_edns(query); - if (status != ARES_SUCCESS) { -- end_query(channel, server, query, status, NULL); -+ end_query(channel, server, query, status, NULL, NULL); - goto cleanup; - } - -@@ -869,7 +913,9 @@ static ares_status_t process_answer(ares_channel_t *channel, - } - - server_increment_failures(server, query->using_tcp); -- status = ares_requeue_query(query, now, status, ARES_TRUE, rdnsrec, requeue); -+ status = ares_requeue_query(query, now, status, ARES_TRUE, rdnsrec, -+ requeue); -+ rdnsrec = NULL; /* Free'd by ares_requeue_query() */ - - if (status != ARES_ENOMEM) { - /* Should any of these cause a connection termination? -@@ -882,12 +928,11 @@ static ares_status_t process_answer(ares_channel_t *channel, - - /* If cache insertion was successful, it took ownership. We ignore - * other cache insertion failures. */ -- if (ares_qcache_insert(channel, now, query, rdnsrec) == ARES_SUCCESS) { -- is_cached = ARES_TRUE; -- } -+ ares_qcache_insert(channel, now, query, rdnsrec); - - server_set_good(server, query->using_tcp); -- end_query(channel, server, query, ARES_SUCCESS, rdnsrec); -+ end_query(channel, server, query, ARES_SUCCESS, rdnsrec, requeue); -+ rdnsrec = NULL; /* Free'd by the requeue */ - - status = ARES_SUCCESS; - -@@ -922,7 +967,7 @@ static void handle_conn_error(ares_conn_t *conn, ares_bool_t critical_failure, - ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now, - ares_status_t status, - ares_bool_t inc_try_count, -- const ares_dns_record_t *dnsrec, -+ ares_dns_record_t *dnsrec, - ares_array_t **requeue) - { - ares_channel_t *channel = query->channel; -@@ -939,6 +984,7 @@ ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now, - } - - if (query->try_count < max_tries && !query->no_retries) { -+ ares_dns_record_destroy(dnsrec); - if (requeue != NULL) { - return ares_append_requeue(requeue, query, NULL); - } -@@ -950,7 +996,7 @@ ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now, - query->error_status = ARES_ETIMEOUT; - } - -- end_query(channel, NULL, query, query->error_status, dnsrec); -+ end_query(channel, NULL, query, query->error_status, dnsrec, requeue); - return ARES_ETIMEOUT; - } - -@@ -1232,7 +1278,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, - } - - if (server == NULL) { -- end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL); -+ end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, NULL); - return ARES_ENOSERVER; - } - -@@ -1260,7 +1306,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, - - /* Anything else is not retryable, likely ENOMEM */ - default: -- end_query(channel, server, query, status, NULL); -+ end_query(channel, server, query, status, NULL, NULL); - return status; - } - } -@@ -1274,7 +1320,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, - - case ARES_ENOMEM: - /* Not retryable */ -- end_query(channel, server, query, status, NULL); -+ end_query(channel, server, query, status, NULL, NULL); - return status; - - /* These conditions are retryable as they are server-specific -@@ -1306,7 +1352,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, - ares_slist_insert(channel->queries_by_timeout, query); - if (!query->node_queries_by_timeout) { - /* LCOV_EXCL_START: OutOfMemory */ -- end_query(channel, server, query, ARES_ENOMEM, NULL); -+ end_query(channel, server, query, ARES_ENOMEM, NULL, NULL); - return ARES_ENOMEM; - /* LCOV_EXCL_STOP */ - } -@@ -1319,7 +1365,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server, - - if (query->node_queries_to_conn == NULL) { - /* LCOV_EXCL_START: OutOfMemory */ -- end_query(channel, server, query, ARES_ENOMEM, NULL); -+ end_query(channel, server, query, ARES_ENOMEM, NULL, NULL); - return ARES_ENOMEM; - /* LCOV_EXCL_STOP */ - } -@@ -1407,7 +1453,7 @@ static void ares_detach_query(ares_query_t *query) - - static void end_query(ares_channel_t *channel, ares_server_t *server, - ares_query_t *query, ares_status_t status, -- const ares_dns_record_t *dnsrec) -+ ares_dns_record_t *dnsrec, ares_array_t **requeue) - { - /* If we were probing for the server to come back online, lets mark it as - * no longer being probed */ -@@ -1417,6 +1463,12 @@ static void end_query(ares_channel_t *channel, ares_server_t *server, - - ares_metrics_record(query, server, status, dnsrec); - -+ /* Delay calling the query callback */ -+ if (requeue != NULL) { -+ ares_append_endqueue(requeue, query, status, dnsrec); -+ return; -+ } -+ - /* Invoke the callback. */ - query->callback(query->arg, status, query->timeouts, dnsrec); - ares_free_query(query); -diff --git a/src/lib/ares_qcache.c b/src/lib/ares_qcache.c -index 97c0a91..4050ff5 100644 ---- a/src/lib/ares_qcache.c -+++ b/src/lib/ares_qcache.c -@@ -421,10 +421,20 @@ done: - return status; - } - --ares_status_t ares_qcache_insert(ares_channel_t *channel, -- const ares_timeval_t *now, -- const ares_query_t *query, -- ares_dns_record_t *dnsrec) -+ares_status_t ares_qcache_insert(ares_channel_t *channel, -+ const ares_timeval_t *now, -+ const ares_query_t *query, -+ const ares_dns_record_t *dnsrec) - { -- return ares_qcache_insert_int(channel->qcache, dnsrec, query->query, now); -+ ares_dns_record_t *dupdns = ares_dns_record_duplicate(dnsrec); -+ ares_status_t status; -+ -+ if (dupdns == NULL) { -+ return ARES_ENOMEM; -+ } -+ status = ares_qcache_insert_int(channel->qcache, dupdns, query->query, now); -+ if (status != ARES_SUCCESS) { -+ ares_dns_record_destroy(dupdns); -+ } -+ return status; - } -diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc -index 812976c..ae45c45 100644 ---- a/test/ares-test-mock-ai.cc -+++ b/test/ares-test-mock-ai.cc -@@ -805,7 +805,39 @@ TEST_P(MockUDPChannelTestAI, TriggerResendThenConnFailEDNS) { - EXPECT_THAT(result.ai_, IncludesV6Address("2121:0000:0000:0000:0000:0000:0000:0303")); - } - -+TEST_P(MockUDPChannelTestAI, ConnectionRefusedOnSearchDomainRetry) { -+ DNSPacket badrsp4; -+ badrsp4.set_response().set_aa() -+ .add_question(new DNSQuestion("www.google.com", T_A)) -+ .set_rcode(NXDOMAIN); -+ -+ EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) -+ .WillOnce(SetReplyAndFailSend(&server_, &badrsp4)); -+ -+ DNSPacket goodrsp4; -+ goodrsp4.set_response().set_aa() -+ .add_question(new DNSQuestion("www.google.com.first.com", T_A)) -+ .add_answer(new DNSARR("www.google.com.first.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); -+ -+ EXPECT_CALL(server_, OnRequest("www.google.com.first.com", T_A)) -+ .WillOnce(SetReply(&server_, &goodrsp4)); -+ -+ ares_socket_functions sock_funcs; -+ memset(&sock_funcs, 0, sizeof(sock_funcs)); - -+ sock_funcs.asendv = ares_sendv_fail; -+ -+ ares_set_socket_functions(channel_, &sock_funcs, NULL); -+ -+ AddrInfoResult result; -+ struct ares_addrinfo_hints hints = {0, 0, 0, 0}; -+ hints.ai_family = AF_INET; -+ hints.ai_flags = ARES_AI_NOSORT; -+ ares_getaddrinfo(channel_, "www.google.com", NULL, &hints, -+ AddrInfoCallback, &result); -+ -+ Process(); -+} - - class MockEDNSChannelTestAI : public MockFlagsChannelOptsTestAI { - public: diff -Nru c-ares-1.34.5/debian/patches/remove-shields-from-markdown.diff c-ares-1.34.9/debian/patches/remove-shields-from-markdown.diff --- c-ares-1.34.5/debian/patches/remove-shields-from-markdown.diff 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/patches/remove-shields-from-markdown.diff 2026-10-26 21:02:49.000000000 +0000 @@ -12,8 +12,8 @@ @@ -1,12 +1,4 @@ -# [![c-ares logo](https://c-ares.org/art/c-ares-logo.svg)](https://c-ares.org/) - --[![Build Status](https://api.cirrus-ci.com/github/c-ares/c-ares.svg?branch=main)](https://cirrus-ci.com/github/c-ares/c-ares) --[![Windows Build Status](https://ci.appveyor.com/api/projects/status/aevgc5914tm72pvs/branch/main?svg=true)](https://ci.appveyor.com/project/c-ares/c-ares/branch/main) +-[![Build Status](https://github.com/c-ares/c-ares/actions/workflows/ubuntu-latest.yml/badge.svg?branch=main)](https://github.com/c-ares/c-ares/actions/workflows/ubuntu-latest.yml) +-[![Windows Build Status](https://github.com/c-ares/c-ares/actions/workflows/windows.yml/badge.svg?branch=main)](https://github.com/c-ares/c-ares/actions/workflows/windows.yml) -[![Coverage Status](https://coveralls.io/repos/github/c-ares/c-ares/badge.svg?branch=main)](https://coveralls.io/github/c-ares/c-ares?branch=main) -[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/291/badge)](https://bestpractices.coreinfrastructure.org/projects/291) -[![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/c-ares.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:c-ares) diff -Nru c-ares-1.34.5/debian/patches/series c-ares-1.34.9/debian/patches/series --- c-ares-1.34.5/debian/patches/series 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/patches/series 2026-10-26 21:02:49.000000000 +0000 @@ -1,3 +1,2 @@ use-debian-googletest.diff remove-shields-from-markdown.diff -CVE-2025-62408.diff diff -Nru c-ares-1.34.5/debian/patches/use-debian-googletest.diff c-ares-1.34.9/debian/patches/use-debian-googletest.diff --- c-ares-1.34.5/debian/patches/use-debian-googletest.diff 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/patches/use-debian-googletest.diff 2026-10-26 21:02:49.000000000 +0000 @@ -9,7 +9,7 @@ --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt -@@ -7,7 +7,7 @@ +@@ -9,7 +9,7 @@ ENDIF () find_package(Threads) diff -Nru c-ares-1.34.5/debian/rules c-ares-1.34.9/debian/rules --- c-ares-1.34.5/debian/rules 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/rules 2026-10-26 21:02:49.000000000 +0000 @@ -2,7 +2,7 @@ export DEB_BUILD_MAINT_OPTIONS = hardening=+all reproducible=+all export DPKG_GENSYMBOLS_CHECK_LEVEL = 4 -export GTEST_FILTER = -*Live*:*.FamilyV4ServiceName/*:LibraryTest.GetTCPSock +export GTEST_FILTER = -*Live*:*.FamilyV4ServiceName/*:LibraryTest.GetTCPSock:SockStateDeliveryTest.* %: dh $@ --buildsystem=cmake+ninja @@ -17,3 +17,8 @@ -DCARES_STATIC=ON \ -DCARES_SYMBOL_HIDING=ON \ -DCARES_BUILD_TESTS=$(if $(filter nocheck,$(DEB_BUILD_OPTIONS)),OFF,ON) + +execute_before_dh_auto_build: + # somehow the test files ignore the CPPFLAGS and CXXFLAGS + # until this is solved, ignore those because they aren't shipped + @echo 'blhc: ignore-line-regexp: .* -o test/.*' diff -Nru c-ares-1.34.5/debian/upstream/metadata c-ares-1.34.9/debian/upstream/metadata --- c-ares-1.34.5/debian/upstream/metadata 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/upstream/metadata 2026-10-26 21:02:49.000000000 +0000 @@ -1,6 +1,6 @@ Name: c-ares Contact: c-ares@lists.haxx.se -Bug-Submit: 'c-ares mailing list: https://lists.haxx.se/mailman/listinfo/c-ares' +Bug-Submit: https://github.com/c-ares/c-ares/issues/new Repository: https://github.com/c-ares/c-ares.git Repository-Browse: https://github.com/c-ares/c-ares Bug-Database: https://github.com/c-ares/c-ares/issues diff -Nru c-ares-1.34.5/debian/upstream/signing-key.asc c-ares-1.34.9/debian/upstream/signing-key.asc --- c-ares-1.34.5/debian/upstream/signing-key.asc 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/upstream/signing-key.asc 2026-10-26 21:02:49.000000000 +0000 @@ -1,30 +1,30 @@ -----BEGIN PGP PUBLIC KEY BLOCK----- -mQENBFcGiPEBCAC7sCnaZqWxfXNgBC7P28BSDUs9w4y/PEFsOv9bpgbgZagX1Fnh +xsBNBFcGiPEBCAC7sCnaZqWxfXNgBC7P28BSDUs9w4y/PEFsOv9bpgbgZagX1Fnh G0eV71nm0p8v9T8Bft1eXaBd977Dq9pgk5qKO0xZo8fC8prFqB5db7fMUvPZCuJT Tb6lGMz4OdfT6aHqUvJ+LFF1mKn8Eqt1Q4snHGSL1PI3/+435qDRQsU15GdYrj1w aNJKk79aes9oguaI2/OTQqzIcOFK5tJjlSOD1ryOIH1e8vD+5MMpGvsRxv3sQHeT ZkfZbkzSLFg/LKpoiQkyql1+BLNhBYq8oaE/jlvQrTEkbAyKpMScdyHwmkWWKjyZ -tXTrAtlComnki4yC2lAV9MXINHHvNJBcIXvVABEBAAG0IERhbmllbCBTdGVuYmVy -ZyA8ZGFuaWVsQGhheHguc2U+iQE3BBMBCgAhBQJXBojxAhsDBQsJCAcDBRUKCQgL +tXTrAtlComnki4yC2lAV9MXINHHvNJBcIXvVABEBAAHNIERhbmllbCBTdGVuYmVy +ZyA8ZGFuaWVsQGhheHguc2U+wsB3BBMBCgAhBQJXBojxAhsDBQsJCAcDBRUKCQgL BRYCAwEAAh4BAheAAAoJEFzJCP23HhLCOKkH/1CyoKiN2PCgTlWoYQspv/AAmsj+ cFwZobI167KowA+o3zxQqxg0MV3ds8G+iig9OIuYurlQL5Jr3CbDltaiXdWtVteR h/VKp61EwyXq77vjJbx81hvOuaXWWLSlU0KB3w7Hj6aD/mt16DpOcY9Aw90mKyva fRTqMF7TcT7J5HeGn2NL45dPkAhiMDEgEnw9yBTxK/x6UoQGPgiOWxSSN7Foj3mh UOflp8W0rnkLbJ4icpym6WuLKRMKAefDvk8GVlAWuXAb9gloL1P6u3uNHllq/IOD -R2bZUBI0QNKhvt0iSj7WKsc/kaqscl+AE9jd/6kXd6vhTNFWdzeco/2mGla5AQ0E +R2bZUBI0QNKhvt0iSj7WKsc/kaqscl+AE9jd/6kXd6vhTNFWdzeco/2mGlbOwE0E VwaI8QEIAOxQAEvF3idxcn80tbUhJg1J98fAS7Hx3WhlFG74uAikZQl1KZrprBu7 0RWTb7Nm1tvZeXW65IlY7kk42bhfYDs1JrIPWOWKvVwKWDxoEbYgW/yvy1TOuXH2 76zbxLl5OEE8sQuOfXZsFSX2IPF9hsgNGaNzor8Ke7Y5BuCQLcGZWW5dLFbbKRKj XG8CaWmsJVoIc2nyXCAss2q9oCJ13X/5z+Ei392rwi1d3NxAYkSiDQan+fkWkCvZ H+dHmFjQ1ANDKielxcW1VfilK1hu9ziBBDf8TCEud/q0woIAH7rvIft4i3Cqjymo -nByE4/OjfH8j4EteQ8qoknMCjjwNVqkAEQEAAYkBHwQYAQoACQUCVwaI8QIbDAAK +nByE4/OjfH8j4EteQ8qoknMCjjwNVqkAEQEAAcLAXwQYAQoACQUCVwaI8QIbDAAK CRBcyQj9tx4SwupjB/9TV4anbZK58bN7QJ5qGnU3GNjlvWFZXMw1u1xVc7abDJyq mFeJcJ4qLUkvBA0OsvlVnMWmeCmzsXhlQVM4Bv6IWyr7JBWgkK5q2CWVB59V7v7z nf5kWnMGFhDFPlLsGbxDWLMoZGH+Iy84whMJFgferwCJy1dND/bHXPztfhvFXi8N NlJUFJa8Xtmugm78C+nwNHcFpVC70HPr3oa8U1ODXMp7L8W/dL3eLYXmRCNd0urH gYrzDt6V/zf5ymvPk5w4HBocn2oRCJj/FXKhFAUptmpTE3g1yvYULmuFcNGAnPAE -xmAmd6NqsCmbj/qx4ytjt5uxt6Jm6IXV9cry8i6xmQINBGZfTCcBEADPw3cEMKA+ +xmAmd6NqsCmbj/qx4ytjt5uxt6Jm6IXV9cry8i6xxsFNBGZfTCcBEADPw3cEMKA+ jNwX1SybDM+ffWilPE1e+637u0U5RFYZIe5pqDfBRx5YaUHWOxYozGaSE1YdoRfZ 8ac+MBtzvJ40EQV5olh/kd99l1AoXLaMYSPJ/mK6k1hc9LZ4iSn8WaSHfwAhziMA r4/KaWkHPY71AkG0PYkuOyp3t65onVFA/U3Un0h/Q9eTJ6SLYIKOAqHEfW6e5/Pf @@ -35,8 +35,8 @@ YI8UPMAu+GDhB66UrMsrO5j6r+1m2u5OXuO+jDAZ+exFc+YZIvRYOg0+5l248u9k 8azBiB0BP9Jl0DgRs/YDO7S42hzhMoXxriGm+il6VHEL3Z5qX3KsMs1MIgqPMwYj MvM+knAZ5X7U6qafZsDQABt9L6e858RqW2E3qesJBD52010YPQQITEkLfb3bjCF0 -ZOigtAHjpD0s32DgXXnwYJ0dC1KvelxsWQARAQABtCBCcmFkIEhvdXNlIDxicmFk -QGJyYWQtaG91c2UuY29tPokCVAQTAQgAPgIbAQULCQgHAgIiAgYVCgkICwIEFgID +ZOigtAHjpD0s32DgXXnwYJ0dC1KvelxsWQARAQABzSBCcmFkIEhvdXNlIDxicmFk +QGJyYWQtaG91c2UuY29tPsLBlAQTAQgAPgIbAQULCQgHAgIiAgYVCgkICwIEFgID AQIeBwIXgAIZARYhBNp9ZOTILGKUy3OiDiLj0TtUEbfKBQJmX08EAAoJECLj0TtU EbfKRu8P/0nW/TwltYcDjBknCisZTlN1g1jJqCfZRKMZPowTB214stsQQ57676OB kYOTmmL5drUokTGWfgu0Q7bsYOCpcpWF6A5uYNuo44yLLGeigjwBZtrZOKo19EDX @@ -48,8 +48,8 @@ 2Op0uz18ZM8bUtnc9hBPOS8TP2I9lRX33CEMOTePZesdjq5iJ0kr4W7+yNZBuVjY Ry5xOmvhHr0bp559NRyMi632scfT0OHPD1Wf0Xk+euTUsrpd8rqqATqGZ+g4NjhQ vWFKZlRUYqiSP5mjhgGehBFSNufK7TZc2s1zU3I0prRe+E00nEvG5K5t4dnRYRNO -hOldPRz6W67M/sMHS9kRzvhXOMcaItb8w8a2ZwMsc242MLA8n/EduDMEZl9OGBYJ -KwYBBAHaRw8BAQdArbNyjETSXAvlOibWjq9SDOzjVfzE0gz9Yq5+oW40RHGJAq0E +hOldPRz6W67M/sMHS9kRzvhXOMcaItb8w8a2ZwMsc242MLA8n/EdzjMEZl9OGBYJ +KwYBBAHaRw8BAQdArbNyjETSXAvlOibWjq9SDOzjVfzE0gz9Yq5+oW40RHHCwe0E GAEIACAWIQTafWTkyCxilMtzog4i49E7VBG3ygUCZl9OGAIbAgCBCRAi49E7VBG3 ynYgBBkWCgAdFiEEdetsoOY+kMT/LIaPwdFWEbLkcgsFAmZfThgACgkQwdFWEbLk cguPkgEA7699q67Kn6iN5lF4h9XG97eO2FmPFF/oaBa1m+l7GnYBANbaYBvK82Zk @@ -64,8 +64,8 @@ ZjV38LtwhVnZPZYEuNG9SQE59/RS7cppaFQtW3PBJCs334kUcdrz6KgthF/wWBaG b03WK4Ms10lNfCYdC+AMn2yjIFBABcw7zxxxScThFNiXkV2Y5ndgfE+t3FImNHj8 Vy5De5zVtS0jTYJpAnbjqruZRDxaXkJVCncwLmRIs+rSNKzh7fd6yFuOVwHkQs4l -iojCX93J1Vi9X9QkuDgEZl9OAxIKKwYBBAGXVQEFAQEHQNkWnfVTRolm6XkEm86B -pUov4Jpe16XQ4dWl9t4ap/0PAwEIB4kCNgQYAQgAIBYhBNp9ZOTILGKUy3OiDiLj +iojCX93J1Vi9X9QkzjgEZl9OAxIKKwYBBAGXVQEFAQEHQNkWnfVTRolm6XkEm86B +pUov4Jpe16XQ4dWl9t4ap/0PAwEIB8LBdgQYAQgAIBYhBNp9ZOTILGKUy3OiDiLj 0TtUEbfKBQJmX04DAhsMAAoJECLj0TtUEbfKGkUP+wRNmV9IDNv/QLVeVPzTdrrV WSfIp9muTCEV/589mMPNYkUntBSNdS+IjFegMUDYcnKABc8xIo+qbjBASBw2tjrC RiKZDnLSJnKU1qYxEjoZnBPRyJTyKOidPta07gKk9xt2ChsTPbbyZ2CsTDrvQvsO @@ -78,5 +78,5 @@ Wvg+Hklft9wM0D7NoYOEkuchNRC6NjeEFjMSR+0iCsFzHfjQFG0IodwDxxNnfLz5 fNj1jMCquscwOIQkpNJPxGfLsPzts5Sctbsp5FL3sI14xISpQYJ86kzUTlQAvdlV CPvCxKgoXTCnUWZvlJUx -=YYcS +=Ztqb -----END PGP PUBLIC KEY BLOCK----- diff -Nru c-ares-1.34.5/debian/watch c-ares-1.34.9/debian/watch --- c-ares-1.34.5/debian/watch 2025-12-14 14:24:14.000000000 +0000 +++ c-ares-1.34.9/debian/watch 2026-10-26 21:02:49.000000000 +0000 @@ -1,5 +1,5 @@ -version=4 -opts="searchmode=plain,\ -pgpsigurlmangle=s%$%.asc%" \ -https://api.github.com/repos/@PACKAGE@/@PACKAGE@/releases?per_page=50 \ -https://github.com/@PACKAGE@/@PACKAGE@/releases/download/v[^/]+/@PACKAGE@@ANY_VERSION@@ARCHIVE_EXT@ +Version: 5 +Source: https://api.github.com/repos/@PACKAGE@/@PACKAGE@/releases +Matching-Pattern: https://github.com/@PACKAGE@/@PACKAGE@/releases/download/v[^/]+/@PACKAGE@@STABLE_VERSION@@ARCHIVE_EXT@ +Search-Mode: plain +Pgp-Sig-Url-Mangle: s%$%.asc% diff -Nru c-ares-1.34.5/docs/Makefile.in c-ares-1.34.9/docs/Makefile.in --- c-ares-1.34.5/docs/Makefile.in 2025-04-08 12:12:31.000000000 +0000 +++ c-ares-1.34.9/docs/Makefile.in 2026-10-20 02:31:57.000000000 +0000 @@ -107,7 +107,6 @@ $(top_srcdir)/m4/ax_code_coverage.m4 \ $(top_srcdir)/m4/ax_compiler_vendor.m4 \ $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ - $(top_srcdir)/m4/ax_cxx_compile_stdcxx_14.m4 \ $(top_srcdir)/m4/ax_file_escapes.m4 \ $(top_srcdir)/m4/ax_pthread.m4 \ $(top_srcdir)/m4/ax_require_defined.m4 \ @@ -228,10 +227,13 @@ GENHTML = @GENHTML@ GMOCK112_CFLAGS = @GMOCK112_CFLAGS@ GMOCK112_LIBS = @GMOCK112_LIBS@ +GMOCK117_CFLAGS = @GMOCK117_CFLAGS@ +GMOCK117_LIBS = @GMOCK117_LIBS@ GMOCK_CFLAGS = @GMOCK_CFLAGS@ GMOCK_LIBS = @GMOCK_LIBS@ GREP = @GREP@ HAVE_CXX14 = @HAVE_CXX14@ +HAVE_CXX17 = @HAVE_CXX17@ INSTALL = @INSTALL@ INSTALL_DATA = @INSTALL_DATA@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ diff -Nru c-ares-1.34.5/docs/ares_cancel.3 c-ares-1.34.9/docs/ares_cancel.3 --- c-ares-1.34.5/docs/ares_cancel.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_cancel.3 2026-10-20 02:31:51.000000000 +0000 @@ -21,6 +21,23 @@ been stored in their arguments. If such a callback invocation adds a new request to the channel, that request will \fInot\fP be cancelled by the current invocation of \fBares_cancel(3)\fP. +.SH REENTRANCY +A callback invoked by +.BR ares_cancel (3) +may call c-ares functions on the same channel. The channel lock is recursive, +and each query is unlinked from the library's internal structures before its +callback runs, so a reentrant call cannot encounter a query that is part-way +through being torn down. + +This includes starting new lookups, as described above, and reconfiguring the +server list with +.BR ares_set_servers_csv (3) +and related functions. + +.SH AVAILABILITY +The reentrancy guarantee described above applies as of c-ares version 1.35.0, +and c-ares version 1.34.9 on the 1.34 branch. + .SH SEE ALSO .BR ares_init (3) .BR ares_destroy (3) diff -Nru c-ares-1.34.5/docs/ares_destroy.3 c-ares-1.34.9/docs/ares_destroy.3 --- c-ares-1.34.5/docs/ares_destroy.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_destroy.3 2026-10-20 02:31:51.000000000 +0000 @@ -20,6 +20,13 @@ channel, passing a status of \fIARES_EDESTRUCTION\fP. These calls give the callbacks a chance to clean up any state which might have been stored in their arguments. A callback must not add new requests to a channel being destroyed. +Such a request is refused: the request is not started, and its callback is +invoked with a status of \fIARES_EDESTRUCTION\fP so that any state attached to +its argument can be released. A request made from within that notification is +refused without a further notification, so that a callback which responds to +failure by retrying cannot recurse without bound; its argument is not handed +back. This applies as of c-ares version 1.35.0, and c-ares version 1.34.9 on +the 1.34 branch. There is no ability to make this function thread-safe. No additional calls using this channel may be made once this function is called. diff -Nru c-ares-1.34.5/docs/ares_dns_record.3 c-ares-1.34.9/docs/ares_dns_record.3 --- c-ares-1.34.5/docs/ares_dns_record.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_dns_record.3 2026-10-20 02:31:51.000000000 +0000 @@ -140,7 +140,7 @@ .B ARES_CLASS_CHAOS - CHAOS .br -.B ARES_CLASS_HESOID +.B ARES_CLASS_HESIOD - Hesoid [Dyer 87] .br .B ARES_CLASS_NONE @@ -438,6 +438,9 @@ is returned on out of memory, .B ARES_EFORMERR is returned on misuse. +\fIares_dns_write(3)\fP also returns +.B ARES_EBADQUERY +if the message is too large to encode. \fIares_dns_record_get_id(3)\fP, \fIares_dns_record_set_id(3)\fP, \fIares_dns_record_get_flags(3)\fP, \fIares_dns_record_get_opcode(3)\fP, diff -Nru c-ares-1.34.5/docs/ares_getaddrinfo.3 c-ares-1.34.9/docs/ares_getaddrinfo.3 --- c-ares-1.34.5/docs/ares_getaddrinfo.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_getaddrinfo.3 2026-10-20 02:31:51.000000000 +0000 @@ -27,6 +27,19 @@ and .I service parameters give the hostname and service as NULL-terminated C strings. +Either +.I name +or +.I service +may be NULL, but not both. If +.I name +is NULL, the returned addresses are synthesized from +.IR service : +the wildcard address (0.0.0.0 or ::) is returned when +.B ARES_AI_PASSIVE +is set in +.IR ai_flags , +otherwise the loopback address (127.0.0.1 or ::1) is returned. The .I hints parameter is an @@ -63,6 +76,16 @@ .I service field will be treated as a numeric value. .TP 19 +.B ARES_AI_PASSIVE +If this option is set and +.I name +is NULL, the returned addresses will use the wildcard address (0.0.0.0 or ::), +suitable for +.BR bind (2)ing +a socket that will accept connections. If not set and +.I name +is NULL, the loopback address (127.0.0.1 or ::1) is returned instead. +.TP 19 .B ARES_AI_CANONNAME The ares_addrinfo structure will return a canonical names list. .TP 19 diff -Nru c-ares-1.34.5/docs/ares_gethostbyaddr.3 c-ares-1.34.9/docs/ares_gethostbyaddr.3 --- c-ares-1.34.5/docs/ares_gethostbyaddr.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_gethostbyaddr.3 2026-10-20 02:31:51.000000000 +0000 @@ -80,7 +80,16 @@ containing the name of the host returned by the query. The callback need not and should not attempt to free the memory pointed to by .IR hostent ; -the ares library will free it when the callback returns. If the query +the ares library will free it when the callback returns. The callback +.B must not +modify the +.B struct hostent +or retain a reference to it after returning; it is owned by c-ares and is only +valid for the duration of the callback. The parameter is intentionally not +declared +.B const +solely to preserve API/ABI compatibility with existing applications. +If the query did not complete successfully, .I hostent will be diff -Nru c-ares-1.34.5/docs/ares_gethostbyname.3 c-ares-1.34.9/docs/ares_gethostbyname.3 --- c-ares-1.34.5/docs/ares_gethostbyname.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_gethostbyname.3 2026-10-20 02:31:51.000000000 +0000 @@ -88,7 +88,16 @@ containing the name of the host returned by the query. The callback need not and should not attempt to free the memory pointed to by .IR hostent ; -the ares library will free it when the callback returns. If the query +the ares library will free it when the callback returns. The callback +.B must not +modify the +.B struct hostent +or retain a reference to it after returning; it is owned by c-ares and is only +valid for the duration of the callback. The parameter is intentionally not +declared +.B const +solely to preserve API/ABI compatibility with existing applications. +If the query did not complete successfully, .I hostent will be diff -Nru c-ares-1.34.5/docs/ares_getnameinfo.3 c-ares-1.34.9/docs/ares_getnameinfo.3 --- c-ares-1.34.5/docs/ares_getnameinfo.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_getnameinfo.3 2026-10-20 02:31:51.000000000 +0000 @@ -24,7 +24,7 @@ is a combination of \fIares_gethostbyaddr(3)\fP and \fIgetservbyport(3)\fP. The function will translate the address either by executing a host query on the name service channel identified by -.IR channel +.IR channel or it will attempt to resolve it locally if possible. The parameters .I sa @@ -68,8 +68,8 @@ A service name lookup is being requested. .PP When the query -is complete or has -failed, the ares library will invoke \fIcallback\fP. Completion or failure of +is complete or has +failed, the ares library will invoke \fIcallback\fP. Completion or failure of the query may happen immediately, or may happen during a later call to \fIares_process(3)\fP, \fIares_destroy(3)\fP or \fIares_cancel(3)\fP. .PP @@ -129,19 +129,31 @@ .PP On successful completion of the query, the callback argument .I node -contains a string representing the hostname (assuming +contains a string representing the hostname (assuming .B ARES_NI_LOOKUPHOST -was specified). Additionally, +was specified). Additionally, .I service contains a string representing the service name (assuming .B ARES_NI_LOOKUPSERVICE was specified). If the query did not complete successfully, or one of the values -was not requested, +was not requested, .I node or .I service -will be +will be .BR NULL . +.PP +The +.I node +and +.I service +strings are owned by the c-ares library and are only valid for the duration of +the callback. The callback +.B must not +modify or free them, and must not retain references to them after returning; +treat them as read-only. The parameters are intentionally not declared +.B const +solely to preserve API/ABI compatibility with existing applications. .SH SEE ALSO .BR ares_process (3), diff -Nru c-ares-1.34.5/docs/ares_inet_pton.3 c-ares-1.34.9/docs/ares_inet_pton.3 --- c-ares-1.34.5/docs/ares_inet_pton.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_inet_pton.3 2026-10-20 02:31:51.000000000 +0000 @@ -12,8 +12,9 @@ int ares_inet_pton(int \fIaf\fP, const char *\fIsrc\fP, void *\fIdst\fP); .fi .SH DESCRIPTION -This is a portable version with the identical functionality of the commonly -available \fIinet_pton\fP. +This is a portable version of the commonly available \fIinet_pton\fP. It +behaves the same for AF_INET6, and for AF_INET it additionally accepts the +legacy forms described below. The ares_inet_pton() function converts the address in its standard text presentation form into its numeric binary form. The \fBaf\fP argument shall @@ -23,9 +24,20 @@ the numeric address; this shall be large enough to hold the numeric address (32 bits for AF_INET, 128 bits for AF_INET6). +The \fBsrc\fP argument must be an address, not a network: a "/bits" prefix +length suffix (CIDR notation) is not accepted and the function returns 0 for +such input. On success, the whole address (4 bytes for AF_INET, 16 bytes for +AF_INET6) is always written to \fBdst\fP. Versions of c-ares before 1.34.9 +accepted a prefix length here. + +Unlike POSIX \fIinet_pton\fP, AF_INET also accepts shortened forms such as +"10.1" and hexadecimal forms such as "0x0a000001". Octets that are not given +are set to zero, so "10.1" is 10.1.0.0. + It returns 1 if the address was valid for the specified address family, or 0 if the address was not parseable in the specified address family, or -1 if -some system error occurred (in which case errno will have been set). +the address family is not supported (in which case errno will have been set +to EAFNOSUPPORT). .SH SEE ALSO .BR ares_init (3), diff -Nru c-ares-1.34.5/docs/ares_init_options.3 c-ares-1.34.9/docs/ares_init_options.3 --- c-ares-1.34.5/docs/ares_init_options.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_init_options.3 2026-10-20 02:31:51.000000000 +0000 @@ -231,6 +231,21 @@ will be set to true if the socket should listen for read events, and .I writable will be set to true if the socket should listen for write events. + +This callback may call c-ares functions on the same channel, including +.BR ares_cancel (3) +and the server configuration functions. Notifications are delivered once the +library has finished the work that produced them rather than part-way through +it, so a reentrant call cannot close a socket the library is still using. They +arrive in the order the state changes occurred, and always before the +outermost c-ares call returns. A call made from within a callback is not the +outermost one, so notifications it produces arrive after it has returned. +.BR ares_destroy (3) +is no exception: the closure of every socket the channel still holds is +reported before it returns, and the callback is never invoked afterwards. +This applies as of c-ares version 1.35.0, and c-ares version 1.34.9 on the +1.34 branch. + The value of .I sock_state_cb_data will be passed as the @@ -309,7 +324,16 @@ override a larger TTL in the response message. This must be a non-zero value otherwise the cache will be disabled. Choose a reasonable value for your application such as 300 (5 minutes) or 3600 (1 hour). The query cache is -automatically flushed if a server configuration change is made. +automatically flushed if a server configuration change is made. As of c-ares +1.35.0, and c-ares 1.34.9 on the 1.34 branch, a cached answer reports its +remaining time to live rather than the +original value received from the server, so TTLs returned on a cache hit +decrease as the entry ages. Two lookups share a cached answer only if their +EDNS state matches: the version, the flag bits including DNSSEC OK, and every +EDNS option other than the DNS cookie and the advertised UDP payload size, +which are both ignored. Options are compared in the order the application +added them. An application that attaches its own options and varies them +between otherwise identical lookups gets a separate cache entry for each. .br .TP 18 .B ARES_OPT_EVENT_THREAD diff -Nru c-ares-1.34.5/docs/ares_query.3 c-ares-1.34.9/docs/ares_query.3 --- c-ares-1.34.5/docs/ares_query.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_query.3 2026-10-20 02:31:51.000000000 +0000 @@ -146,6 +146,16 @@ .I abuf will be non-NULL, otherwise they will be NULL. +The +.I abuf +buffer is owned by the c-ares library and is only valid for the duration of +the callback. The callback +.B must not +modify or free it, and must not retain a reference to it after returning; treat +it as read-only. The parameter is intentionally not declared +.B const +solely to preserve API/ABI compatibility with existing applications. + .SH AVAILABILITY \fBares_query_dnsrec(3)\fP was introduced in c-ares 1.28.0. diff -Nru c-ares-1.34.5/docs/ares_search.3 c-ares-1.34.9/docs/ares_search.3 --- c-ares-1.34.5/docs/ares_search.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_search.3 2026-10-20 02:31:51.000000000 +0000 @@ -152,6 +152,15 @@ will usually be 0, but in some cases an unsuccessful query result may be placed in .IR abuf . +The +.I abuf +buffer is owned by the c-ares library and is only valid for the duration of +the callback. The callback +.B must not +modify or free it, and must not retain a reference to it after returning; treat +it as read-only. The parameter is intentionally not declared +.B const +solely to preserve API/ABI compatibility with existing applications. The \fIares_search_dnsrec(3)\fP function behaves identically to \fIares_search(3)\fP, but takes an initialized and filled DNS record object to diff -Nru c-ares-1.34.5/docs/ares_send.3 c-ares-1.34.9/docs/ares_send.3 --- c-ares-1.34.5/docs/ares_send.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_send.3 2026-10-20 02:31:51.000000000 +0000 @@ -131,6 +131,16 @@ .IR alen for \fIares_send(3)\fP will be non-NULL. +The +.I abuf +buffer is owned by the c-ares library and is only valid for the duration of +the callback. The callback +.B must not +modify or free it, and must not retain a reference to it after returning; treat +it as read-only. The parameter is intentionally not declared +.B const +solely to preserve API/ABI compatibility with existing applications. + Unless the flag .B ARES_FLAG_NOCHECKRESP was set at channel initialization time, \fIares_send_dnsrec(3)\fP and diff -Nru c-ares-1.34.5/docs/ares_set_pending_write_cb.3 c-ares-1.34.9/docs/ares_set_pending_write_cb.3 --- c-ares-1.34.5/docs/ares_set_pending_write_cb.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_set_pending_write_cb.3 2026-10-20 02:31:51.000000000 +0000 @@ -36,8 +36,9 @@ with the callback. Each time the callback is triggered must result in a call to \fBares_process_pending_write(3)\fP from the caller's event loop otherwise stalls and timeouts may occur. The callback \fBmust not\fP call -\fBares_process_pending_write(3)\fP directly as otherwise it would invalidate -any advantage of this use-case. +any c-ares function on the same channel; its only job is to wake the event +loop. In particular, calling \fBares_process_pending_write(3)\fP directly +would invalidate any advantage of this use-case. This is considered an optimization, especially when using TLS-based connections which add additional overhead to the data stream. Due to the asyncronous nature diff -Nru c-ares-1.34.5/docs/ares_set_server_state_callback.3 c-ares-1.34.9/docs/ares_set_server_state_callback.3 --- c-ares-1.34.5/docs/ares_set_server_state_callback.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_set_server_state_callback.3 2026-10-20 02:31:51.000000000 +0000 @@ -47,6 +47,31 @@ metrics about the numbers and types of queries sent to each server or to detect when a server is uncontactable or unhealthy. +.SH REENTRANCY +The server state callback may call c-ares functions on the same channel, +including +.BR ares_cancel (3) +and the server configuration functions. The channel lock is recursive and the +library holds no query, connection or server state across the callback. + +Notifications are queued and delivered once the library has finished the work +that produced them, rather than part-way through it. A notification is +therefore observed after the processing pass that generated it, not during. + +The callback and its +.I user_data +are read when the notification is delivered, not when it is queued. An +application that replaces its callback between those two points will see the +notification delivered to the new one; unregistering by passing a +.B NULL +callback discards any notification still queued. + +This applies to a replacement made from within a c-ares callback, where the +channel lock is held. +.BR ares_set_server_state_callback (3) +does not take the channel lock, so replacing the callback from another thread +while the channel is processing is not synchronized against delivery. + .SH FLAGS .TP 5 .B ARES_SERV_STATE_UDP @@ -58,5 +83,9 @@ .SH AVAILABILITY This function was first introduced in c-ares version 1.29.0. +The reentrancy guarantee described above applies as of c-ares version 1.35.0, +and c-ares version 1.34.9 on the 1.34 branch. + .SH SEE ALSO -.BR ares_get_servers_csv (3) +.BR ares_get_servers_csv (3), +.BR ares_cancel (3) diff -Nru c-ares-1.34.5/docs/ares_set_servers_csv.3 c-ares-1.34.9/docs/ares_set_servers_csv.3 --- c-ares-1.34.5/docs/ares_set_servers_csv.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_set_servers_csv.3 2026-10-20 02:31:51.000000000 +0000 @@ -126,6 +126,23 @@ but due to the addition of link local interface support, this difference was removed. +.SH REENTRANCY +These functions may be called from within a c-ares callback. Removing a server +closes its connections, which ends the lookups that were using it and invokes +their callbacks, so a reconfiguration can be requested from inside one. + +A nested call reconciles the whole server list, and the outer call leaves that +result in place rather than deleting the servers the nested call installed. +The consequence is that the outer call's own list does not survive, even +though it returns +.B ARES_SUCCESS: +the last configuration applied is the one the channel keeps. An application +that reconfigures from a callback should therefore not assume the list it +passed to the outer call is in effect when that call returns. + +This applies as of c-ares version 1.35.0, and c-ares version 1.34.9 on the +1.34 branch. + .SH EXAMPLE .nf 192.168.1.100,[fe80::1]:53%eth0,dns://192.168.1.1?tcpport=1153 diff -Nru c-ares-1.34.5/docs/ares_set_socket_callback.3 c-ares-1.34.9/docs/ares_set_socket_callback.3 --- c-ares-1.34.5/docs/ares_set_socket_callback.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_set_socket_callback.3 2026-10-20 02:31:51.000000000 +0000 @@ -26,6 +26,10 @@ connected to the remote server. The callback must return ARES_SUCCESS if things are fine, or return -1 to signal an error. A returned error will abort the ares operation. + +The callback is invoked while c-ares is part-way through opening a +connection. It must not call any c-ares function on the same channel; see +\fIares_set_socket_functions(3)\fP. .SH SEE ALSO .BR ares_init_options (3), .BR ares_set_socket_configure_callback (3) diff -Nru c-ares-1.34.5/docs/ares_set_socket_configure_callback.3 c-ares-1.34.9/docs/ares_set_socket_configure_callback.3 --- c-ares-1.34.5/docs/ares_set_socket_configure_callback.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_set_socket_configure_callback.3 2026-10-20 02:31:51.000000000 +0000 @@ -26,6 +26,10 @@ to configure various socket options. The callback must return ARES_SUCCESS if things are fine, or return -1 to signal an error. A returned error will abort the ares operation. + +The callback is invoked while c-ares is part-way through opening a +connection. It must not call any c-ares function on the same channel; see +\fIares_set_socket_functions(3)\fP. .SH SEE ALSO .BR ares_init_options (3), .BR ares_set_socket_callback (3) diff -Nru c-ares-1.34.5/docs/ares_set_socket_functions.3 c-ares-1.34.9/docs/ares_set_socket_functions.3 --- c-ares-1.34.5/docs/ares_set_socket_functions.3 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/docs/ares_set_socket_functions.3 2026-10-20 02:31:51.000000000 +0000 @@ -149,6 +149,20 @@ address, the callback should fill this in. The \fIflags\fP parameter is currently unused. See \fBrecvfrom(2)\fP. +For UDP, the callback must fill in the full source address, including the +source port, exactly as \fBrecvfrom(2)\fP does. c-ares discards a datagram +whose source address does not match the server it queried, or whose source +port does not match that server's port. + +\fBSecurity:\fP for compatibility with older callbacks, a reported source port +of 0 is treated as unknown and only the address is checked. A callback that +does not fill in the source port is therefore insecure: c-ares will accept +answers sent from any port on the server's address, for example from another +DNS server or any other service or user able to send UDP from that host, and +those answers are cached and returned to the application. Such an +implementation is non-compliant and should be fixed to report the real source +port. + .TP 8 .B ares_ssize_t (*\fIasendto\fP)(ares_socket_t \fIfd\fP, const void * \fIbuffer\fP, size_t \fIlength\fP, int \fIflags\fP, const struct sockaddr * \fIaddress\fP, ares_socklen_t \fIaddress_len\fP, void * \fIuser_data\fP) .br @@ -305,6 +319,12 @@ parameter is not NULL and the connection protocol provides the source address, the callback should fill this in. See \fBrecvfrom(2)\fP +For UDP, the callback must fill in the full source address, including the +source port, as described above for \fIarecvfrom\fP in +\fBares_set_socket_functions_ex\fP. \fBSecurity:\fP a callback that +does not fill in the source port is insecure, and c-ares will accept answers +sent from any port on the server's address. + .TP 8 .B ares_ssize_t (*\fIasendv\fP)(ares_socket_t \fIfd\fP, const struct iovec * \fIdata\fP, int \fIlen\fP, void * \fIuser_data\fP) .br @@ -318,6 +338,18 @@ socket's lifetime. However, the \fBares_set_socket_functions_ex(3)\fP struct is duplicated and does not need to survive past the call to the function. +.PP +These callback functions are invoked while c-ares is part-way through +creating, connecting, writing to, reading from or closing a socket. They +must only perform the requested socket operation and must not call any c-ares +function on the same channel; for example they must not enqueue or cancel +queries, process socket events, change the server configuration, or destroy +the channel. Doing so is not supported. Work of that kind belongs in a query +callback, the server state callback set by +\fIares_set_server_state_callback(3)\fP, or the socket state callback +(\fBARES_OPT_SOCK_STATE_CB\fP) described in \fIares_init_options(3)\fP. +Functions may be called on a different channel. + .SH AVAILABILITY ares_socket_functions added in c-ares 1.13.0, ares_socket_functions_ex added in c-ares 1.34.0 diff -Nru c-ares-1.34.5/include/Makefile.in c-ares-1.34.9/include/Makefile.in --- c-ares-1.34.5/include/Makefile.in 2025-04-08 12:12:31.000000000 +0000 +++ c-ares-1.34.9/include/Makefile.in 2026-10-20 02:31:57.000000000 +0000 @@ -105,7 +105,6 @@ $(top_srcdir)/m4/ax_code_coverage.m4 \ $(top_srcdir)/m4/ax_compiler_vendor.m4 \ $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ - $(top_srcdir)/m4/ax_cxx_compile_stdcxx_14.m4 \ $(top_srcdir)/m4/ax_file_escapes.m4 \ $(top_srcdir)/m4/ax_pthread.m4 \ $(top_srcdir)/m4/ax_require_defined.m4 \ @@ -239,10 +238,13 @@ GENHTML = @GENHTML@ GMOCK112_CFLAGS = @GMOCK112_CFLAGS@ GMOCK112_LIBS = @GMOCK112_LIBS@ +GMOCK117_CFLAGS = @GMOCK117_CFLAGS@ +GMOCK117_LIBS = @GMOCK117_LIBS@ GMOCK_CFLAGS = @GMOCK_CFLAGS@ GMOCK_LIBS = @GMOCK_LIBS@ GREP = @GREP@ HAVE_CXX14 = @HAVE_CXX14@ +HAVE_CXX17 = @HAVE_CXX17@ INSTALL = @INSTALL@ INSTALL_DATA = @INSTALL_DATA@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ diff -Nru c-ares-1.34.5/include/ares.h c-ares-1.34.9/include/ares.h --- c-ares-1.34.5/include/ares.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/include/ares.h 2026-10-20 02:31:51.000000000 +0000 @@ -434,6 +434,14 @@ /* DNS record parser, writer, and helpers */ #include "ares_dns_record.h" +/* IMPORTANT: the pointer parameters below (abuf, hostent, node, service) are + * intentionally NOT const. Marking them const changes the function-pointer + * type and breaks API/ABI compatibility for existing C and (especially) C++ + * applications that declare their callbacks with the historical non-const + * signature. This was tried in PR #1060 and reverted for that reason. These + * buffers are owned by c-ares and MUST be treated as read-only by the callback + * (do not modify or free them); that contract is documented in the man pages, + * not enforced via const, to preserve compatibility. Do not re-add const. */ typedef void (*ares_callback)(void *arg, int status, int timeouts, unsigned char *abuf, int alen); diff -Nru c-ares-1.34.5/include/ares_dns_record.h c-ares-1.34.9/include/ares_dns_record.h --- c-ares-1.34.5/include/ares_dns_record.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/include/ares_dns_record.h 2026-10-20 02:31:51.000000000 +0000 @@ -76,7 +76,8 @@ typedef enum { ARES_CLASS_IN = 1, /*!< Internet */ ARES_CLASS_CHAOS = 3, /*!< CHAOS */ - ARES_CLASS_HESOID = 4, /*!< Hesoid [Dyer 87] */ + ARES_CLASS_HESIOD = 4, /*!< Hesiod [Dyer 87] */ + ARES_CLASS_HESOID = 4, /*!< typo from older c-ares version for Hesiod */ ARES_CLASS_NONE = 254, /*!< RFC 2136 */ ARES_CLASS_ANY = 255 /*!< Any class (requests only) */ } ares_dns_class_t; @@ -1092,7 +1093,7 @@ * * \param[in] dnsrec Pointer to initialized and filled DNS record object. * \param[out] buf Pointer passed by reference to be filled in with with - * DNS message. Must be ares_free()'d by caller. + * DNS message. Must be ares_free_string()'d by caller. * \param[out] buf_len Length of returned buffer containing DNS message. * \return ARES_SUCCESS on success */ diff -Nru c-ares-1.34.5/include/ares_version.h c-ares-1.34.9/include/ares_version.h --- c-ares-1.34.5/include/ares_version.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/include/ares_version.h 2026-10-20 02:31:51.000000000 +0000 @@ -28,12 +28,12 @@ #define ARES__VERSION_H /* This is the global package copyright */ -#define ARES_COPYRIGHT "2004 - 2024 Daniel Stenberg, ." +#define ARES_COPYRIGHT "2004 - 2025 Daniel Stenberg, ." #define ARES_VERSION_MAJOR 1 #define ARES_VERSION_MINOR 34 -#define ARES_VERSION_PATCH 5 -#define ARES_VERSION_STR "1.34.5" +#define ARES_VERSION_PATCH 9 +#define ARES_VERSION_STR "1.34.9" /* NOTE: We cannot make the version string a C preprocessor stringify operation * due to assumptions made by integrators that aren't properly using diff -Nru c-ares-1.34.5/m4/ax_cxx_compile_stdcxx_14.m4 c-ares-1.34.9/m4/ax_cxx_compile_stdcxx_14.m4 --- c-ares-1.34.5/m4/ax_cxx_compile_stdcxx_14.m4 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/m4/ax_cxx_compile_stdcxx_14.m4 1970-01-01 00:00:00.000000000 +0000 @@ -1,34 +0,0 @@ -# ============================================================================= -# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_14.html -# ============================================================================= -# -# SYNOPSIS -# -# AX_CXX_COMPILE_STDCXX_14([ext|noext], [mandatory|optional]) -# -# DESCRIPTION -# -# Check for baseline language coverage in the compiler for the C++14 -# standard; if necessary, add switches to CXX and CXXCPP to enable -# support. -# -# This macro is a convenience alias for calling the AX_CXX_COMPILE_STDCXX -# macro with the version set to C++14. The two optional arguments are -# forwarded literally as the second and third argument respectively. -# Please see the documentation for the AX_CXX_COMPILE_STDCXX macro for -# more information. If you want to use this macro, you also need to -# download the ax_cxx_compile_stdcxx.m4 file. -# -# LICENSE -# -# Copyright (c) 2015 Moritz Klammler -# -# Copying and distribution of this file, with or without modification, are -# permitted in any medium without royalty provided the copyright notice -# and this notice are preserved. This file is offered as-is, without any -# warranty. - -#serial 5 - -AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX]) -AC_DEFUN([AX_CXX_COMPILE_STDCXX_14], [AX_CXX_COMPILE_STDCXX([14], [$1], [$2])]) diff -Nru c-ares-1.34.5/m4/ax_pthread.m4 c-ares-1.34.9/m4/ax_pthread.m4 --- c-ares-1.34.5/m4/ax_pthread.m4 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/m4/ax_pthread.m4 2026-10-20 02:31:51.000000000 +0000 @@ -160,7 +160,7 @@ case $host_os in - freebsd*) + freebsd*|midnightbsd*) # -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able) # lthread: LinuxThreads port on FreeBSD (also preferred to -pthread) diff -Nru c-ares-1.34.5/src/Makefile.in c-ares-1.34.9/src/Makefile.in --- c-ares-1.34.5/src/Makefile.in 2025-04-08 12:12:31.000000000 +0000 +++ c-ares-1.34.9/src/Makefile.in 2026-10-20 02:31:57.000000000 +0000 @@ -104,7 +104,6 @@ $(top_srcdir)/m4/ax_code_coverage.m4 \ $(top_srcdir)/m4/ax_compiler_vendor.m4 \ $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ - $(top_srcdir)/m4/ax_cxx_compile_stdcxx_14.m4 \ $(top_srcdir)/m4/ax_file_escapes.m4 \ $(top_srcdir)/m4/ax_pthread.m4 \ $(top_srcdir)/m4/ax_require_defined.m4 \ @@ -250,10 +249,13 @@ GENHTML = @GENHTML@ GMOCK112_CFLAGS = @GMOCK112_CFLAGS@ GMOCK112_LIBS = @GMOCK112_LIBS@ +GMOCK117_CFLAGS = @GMOCK117_CFLAGS@ +GMOCK117_LIBS = @GMOCK117_LIBS@ GMOCK_CFLAGS = @GMOCK_CFLAGS@ GMOCK_LIBS = @GMOCK_LIBS@ GREP = @GREP@ HAVE_CXX14 = @HAVE_CXX14@ +HAVE_CXX17 = @HAVE_CXX17@ INSTALL = @INSTALL@ INSTALL_DATA = @INSTALL_DATA@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ diff -Nru c-ares-1.34.5/src/lib/CMakeLists.txt c-ares-1.34.9/src/lib/CMakeLists.txt --- c-ares-1.34.5/src/lib/CMakeLists.txt 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/CMakeLists.txt 2026-10-20 02:31:51.000000000 +0000 @@ -15,6 +15,14 @@ # Include resource file in windows builds for versioned DLLs IF (WIN32) TARGET_SOURCES (${PROJECT_NAME} PRIVATE cares.rc) + + # Check for use of llvm-rc (implies clang is being used). We need to set the + # compile flags to use the correct codepage for the resource compiler. + # This is needed for the copyright symbol to be encoded correctly. + # The default codepage is 1252 (Windows Latin-1) but llvm-rc defaults to UTF-8. + if (CMAKE_RC_COMPILER MATCHES "llvm-rc") + set_source_files_properties(cares.rc PROPERTIES COMPILE_FLAGS "/C 1252") + endif() ENDIF() # Convert CARES_LIB_VERSIONINFO libtool version format into VERSION and SOVERSION @@ -58,6 +66,8 @@ TARGET_COMPILE_DEFINITIONS (${PROJECT_NAME} PRIVATE HAVE_CONFIG_H=1 CARES_BUILDING_LIBRARY) + cares_set_werror (${PROJECT_NAME}) + TARGET_LINK_LIBRARIES (${PROJECT_NAME} PUBLIC ${CARES_DEPENDENT_LIBS} PRIVATE ${CMAKE_THREAD_LIBS_INIT} @@ -128,6 +138,8 @@ TARGET_COMPILE_DEFINITIONS (${LIBNAME} PRIVATE HAVE_CONFIG_H=1 CARES_BUILDING_LIBRARY) + cares_set_werror (${LIBNAME}) + # Only matters on Windows IF (WIN32 OR CYGWIN) TARGET_COMPILE_DEFINITIONS (${LIBNAME} PUBLIC CARES_STATICLIB) diff -Nru c-ares-1.34.5/src/lib/Makefile.in c-ares-1.34.9/src/lib/Makefile.in --- c-ares-1.34.5/src/lib/Makefile.in 2025-04-08 12:12:31.000000000 +0000 +++ c-ares-1.34.9/src/lib/Makefile.in 2026-10-20 02:31:57.000000000 +0000 @@ -15,7 +15,7 @@ @SET_MAKE@ # aminclude_static.am generated automatically by Autoconf -# from AX_AM_MACROS_STATIC on Tue Apr 8 12:12:30 UTC 2025 +# from AX_AM_MACROS_STATIC on Tue Oct 20 02:31:56 UTC 2026 # Copyright (C) The c-ares project and its contributors # SPDX-License-Identifier: MIT @@ -115,7 +115,6 @@ $(top_srcdir)/m4/ax_code_coverage.m4 \ $(top_srcdir)/m4/ax_compiler_vendor.m4 \ $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ - $(top_srcdir)/m4/ax_cxx_compile_stdcxx_14.m4 \ $(top_srcdir)/m4/ax_file_escapes.m4 \ $(top_srcdir)/m4/ax_pthread.m4 \ $(top_srcdir)/m4/ax_require_defined.m4 \ @@ -495,10 +494,13 @@ GENHTML = @GENHTML@ GMOCK112_CFLAGS = @GMOCK112_CFLAGS@ GMOCK112_LIBS = @GMOCK112_LIBS@ +GMOCK117_CFLAGS = @GMOCK117_CFLAGS@ +GMOCK117_LIBS = @GMOCK117_LIBS@ GMOCK_CFLAGS = @GMOCK_CFLAGS@ GMOCK_LIBS = @GMOCK_LIBS@ GREP = @GREP@ HAVE_CXX14 = @HAVE_CXX14@ +HAVE_CXX17 = @HAVE_CXX17@ INSTALL = @INSTALL@ INSTALL_DATA = @INSTALL_DATA@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ diff -Nru c-ares-1.34.5/src/lib/ares_addrinfo_localhost.c c-ares-1.34.9/src/lib/ares_addrinfo_localhost.c --- c-ares-1.34.5/src/lib/ares_addrinfo_localhost.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_addrinfo_localhost.c 2026-10-20 02:31:51.000000000 +0000 @@ -67,13 +67,8 @@ struct ares_addrinfo_node **nodes) { struct ares_addrinfo_node *node; - - node = ares_append_addrinfo_node(nodes); - if (!node) { - return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ - } - - memset(node, 0, sizeof(*node)); + struct sockaddr *sa; + socklen_t salen; if (aftype == AF_INET) { struct sockaddr_in *sin = ares_malloc(sizeof(*sin)); @@ -86,14 +81,9 @@ sin->sin_family = AF_INET; sin->sin_port = htons(port); - node->ai_addr = (struct sockaddr *)sin; - node->ai_family = AF_INET; - node->ai_addrlen = sizeof(*sin); - node->ai_addr = (struct sockaddr *)sin; - node->ai_ttl = (int)ttl; - } - - if (aftype == AF_INET6) { + sa = (struct sockaddr *)sin; + salen = sizeof(*sin); + } else if (aftype == AF_INET6) { struct sockaddr_in6 *sin6 = ares_malloc(sizeof(*sin6)); if (!sin6) { return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ @@ -104,13 +94,23 @@ sin6->sin6_family = AF_INET6; sin6->sin6_port = htons(port); - node->ai_addr = (struct sockaddr *)sin6; - node->ai_family = AF_INET6; - node->ai_addrlen = sizeof(*sin6); - node->ai_addr = (struct sockaddr *)sin6; - node->ai_ttl = (int)ttl; + sa = (struct sockaddr *)sin6; + salen = sizeof(*sin6); + } else { + return ARES_EFORMERR; + } + + node = ares_append_addrinfo_node(nodes); + if (!node) { + ares_free(sa); /* LCOV_EXCL_LINE: OutOfMemory */ + return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ } + node->ai_addr = sa; + node->ai_family = aftype; + node->ai_addrlen = salen; + node->ai_ttl = ares_dns_ttl_as_int(ttl); + return ARES_SUCCESS; } diff -Nru c-ares-1.34.5/src/lib/ares_cancel.c c-ares-1.34.9/src/lib/ares_cancel.c --- c-ares-1.34.5/src/lib/ares_cancel.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_cancel.c 2026-10-20 02:31:51.000000000 +0000 @@ -38,10 +38,10 @@ } ares_channel_lock(channel); + ares_deferred_cb_enter(channel); if (ares_llist_len(channel->all_queries) > 0) { ares_llist_node_t *node = NULL; - ares_llist_node_t *next = NULL; /* Swap list heads, so that only those queries which were present on entry * into this function are cancelled. New queries added by callbacks of @@ -57,21 +57,29 @@ goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } - node = ares_llist_node_first(list_copy); - while (node != NULL) { + /* Re-read the head each iteration rather than caching the next node: a + * callback below may cause other queries in this same list to be ended + * and their nodes destroyed. */ + while ((node = ares_llist_node_first(list_copy)) != NULL) { ares_query_t *query; - /* Cache next since this node is being deleted */ - next = ares_llist_node_next(node); + query = ares_llist_node_claim(node); - query = ares_llist_node_claim(node); + /* Must come first: node_claim() already destroyed this node, so + * ares_detach_query() would otherwise destroy it a second time. */ query->node_all_queries = NULL; + /* Detach from queries_by_qid, the connection and the timeout list + * before running user code. Otherwise a reentrant teardown from + * within the callback -- ares_set_servers_csv(), or a new query + * hitting a connection error -- still finds this query, ends it a + * second time and frees it, and the ares_free_query() below then + * double-frees. */ + ares_detach_query(query); + /* NOTE: its possible this may enqueue new queries */ query->callback(query->arg, ARES_ECANCELLED, 0, NULL); ares_free_query(query); - - node = next; } ares_llist_destroy(list_copy); @@ -83,5 +91,6 @@ ares_queue_notify_empty(channel); done: + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); } diff -Nru c-ares-1.34.5/src/lib/ares_close_sockets.c c-ares-1.34.9/src/lib/ares_close_sockets.c --- c-ares-1.34.5/src/lib/ares_close_sockets.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_close_sockets.c 2026-10-20 02:31:51.000000000 +0000 @@ -41,12 +41,47 @@ } } +/*! Release a connection's memory. Split from the unlink below so that the + * two can happen at different times: a caller further up the stack may still + * hold this pointer, and reading a detached connection is safe where reading + * a freed one is not. */ +void ares_conn_reap(ares_conn_t *conn) +{ + if (conn == NULL) { + return; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + + ares_buf_destroy(conn->in_buf); + ares_buf_destroy(conn->out_buf); + ares_llist_destroy(conn->queries_to_conn); + ares_free(conn); +} + void ares_close_connection(ares_conn_t *conn, ares_status_t requeue_status) { - ares_server_t *server = conn->server; - ares_channel_t *channel = server->channel; + ares_server_t *server; + ares_channel_t *channel; - /* Unlink */ + if (conn == NULL) { + return; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + + /* Already torn down and awaiting reclamation. Reaching here twice is + * ordinary once a connection can outlive its teardown: a caller holding it + * from before -- handle_conn_error() after a failed write, say -- closes it + * again without knowing. Doing the work twice would requeue queries that + * have already moved and would put this connection on the pending list a + * second time, which is a double free. */ + if (conn->flags & ARES_CONN_FLAG_DEAD) { + return; + } + + server = conn->server; + channel = server->channel; + + /* Unlink from everything that can find this connection. After this it is + * unreachable by lookup, which is what stops anything new from picking it + * up; anything that already holds it is handled by the deferral below. */ ares_llist_node_claim( ares_htable_asvp_get_direct(channel->connnode_by_socket, conn->fd)); ares_htable_asvp_remove(channel->connnode_by_socket, conn->fd); @@ -55,19 +90,40 @@ server->tcp_conn = NULL; } - ares_buf_destroy(conn->in_buf); - ares_buf_destroy(conn->out_buf); + /* Marked dead as soon as it is unlinked, before anything below can run + * application code. ares_requeue_queries() ends queries terminally once + * their retries are exhausted, which invokes the application's callback; if + * the flag were set after that, a second ares_close_connection() reaching + * this connection in the meantime would run the body again -- closing the + * descriptor twice and listing the connection for reclamation twice, which + * is a double free. It also gives ares_close_sockets() its termination + * condition: dead implies unlinked, so its re-read-the-head loop cannot see + * a dead connection and spin. */ + conn->flags |= ARES_CONN_FLAG_DEAD; /* Requeue queries to other connections */ ares_requeue_queries(conn, requeue_status); - ares_llist_destroy(conn->queries_to_conn); - - ares_conn_sock_state_cb_update(conn, ARES_CONN_STATE_NONE); + /* Hands the descriptor to the queued notification when one is due, so the + * application is told to stop watching it before it is closed. Otherwise + * nothing will carry it and we close it here. */ + if (!ares_conn_sock_state_cb_close(conn)) { + ares_socket_close(channel, conn->fd); + } + conn->fd = ARES_SOCKET_BAD; - ares_socket_close(channel, conn->fd); + /* If a library call is in progress it may be holding this connection -- + * read_answers() and ares_send_query_int() both do, across code that can + * reach here. Keep it readable until that call unwinds. The buffers and + * the query list are freed by ares_conn_reap(), so a stale holder sees an + * emptied connection rather than freed memory. */ + if (channel->deferred_cb_depth > 0) { + conn->next_pending = channel->pending_conns; + channel->pending_conns = conn; + return; + } - ares_free(conn); + ares_conn_reap(conn); } void ares_close_sockets(ares_server_t *server) @@ -76,6 +132,10 @@ while ((node = ares_llist_node_first(server->connections)) != NULL) { ares_conn_t *conn = ares_llist_node_val(node); + /* This loop re-reads the head each time and relies on the close below + * unlinking it. A dead connection returns early without unlinking, so + * one on this list would spin forever holding the channel lock. */ + assert(!(conn->flags & ARES_CONN_FLAG_DEAD)); ares_close_connection(conn, ARES_SUCCESS); } } @@ -88,6 +148,7 @@ return; /* LCOV_EXCL_LINE: DefensiveCoding */ } +restart: /* Iterate across each server */ for (snode = ares_slist_node_first(channel->servers); snode != NULL; snode = ares_slist_node_next(snode)) { @@ -112,11 +173,11 @@ do_cleanup = ARES_TRUE; } - /* If the associated server has failures, close it out. Resetting the - * connection (and specifically the source port number) can help resolve - * situations where packets are being dropped. + /* If the connection has been retired for new queries, close it out once + * idle. Resetting the connection (and specifically the source port + * number) can help resolve situations where packets are being dropped. */ - if (conn->server->consec_failures > 0) { + if (conn->flags & ARES_CONN_FLAG_NONEW) { do_cleanup = ARES_TRUE; } @@ -130,8 +191,12 @@ continue; } - /* Clean it up */ + /* Clean it up. The close function is application code and may have + * reconfigured the servers, freeing the list nodes this walk holds -- + * the server's, and the next connection's. Start over; connections + * already closed are no longer listed, so this ends. */ ares_close_connection(conn, ARES_SUCCESS); + goto restart; } } } diff -Nru c-ares-1.34.5/src/lib/ares_config.h.in c-ares-1.34.9/src/lib/ares_config.h.in --- c-ares-1.34.5/src/lib/ares_config.h.in 2025-04-08 12:12:30.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_config.h.in 2026-10-20 02:31:57.000000000 +0000 @@ -87,6 +87,9 @@ /* define if the compiler supports basic C++14 syntax */ #undef HAVE_CXX14 +/* define if the compiler supports basic C++17 syntax */ +#undef HAVE_CXX17 + /* Define to 1 if you have the header file. */ #undef HAVE_DLFCN_H diff -Nru c-ares-1.34.5/src/lib/ares_conn.c c-ares-1.34.9/src/lib/ares_conn.c --- c-ares-1.34.5/src/lib/ares_conn.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_conn.c 2026-10-20 02:31:51.000000000 +0000 @@ -26,18 +26,211 @@ */ #include "ares_private.h" -void ares_conn_sock_state_cb_update(ares_conn_t *conn, - ares_conn_state_flags_t flags) +/*! A queued socket-state notification. Holds the descriptor and the state, + * never the connection, so a queued entry cannot outlive what it refers to. + * + * When \c owns_fd is set, this message carries the only remaining reference to + * the descriptor: the connection is gone and the descriptor is deliberately + * left open until after the application has been told to stop watching it. + * Closing first would let the number be reused -- by application code running + * from a deferred query callback, for instance -- before the notification + * naming it is delivered. */ +typedef struct { + ares_socket_t fd; + ares_bool_t readable; + ares_bool_t writable; + ares_bool_t owns_fd; +} ares_sock_state_msg_t; + +static ares_bool_t ares_sock_state_queue(ares_channel_t *channel, + const ares_sock_state_msg_t *msg) { - ares_channel_t *channel = conn->server->channel; + if (channel->sock_state_queue == NULL) { + channel->sock_state_queue = ares_array_create(sizeof(*msg), NULL); + if (channel->sock_state_queue == NULL) { + return ARES_FALSE; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + if (ares_array_insertdata_last(channel->sock_state_queue, msg) != + ARES_SUCCESS) { + return ARES_FALSE; /* LCOV_EXCL_LINE: OutOfMemory */ + } + + return ARES_TRUE; +} + +/*! Deliver every socket-state notification the channel has queued. Loops + * rather than snapshotting: a callback is allowed to re-enter the library and + * queue more, and those belong to this same delivery point. */ +static void ares_sock_state_dispatch(ares_channel_t *channel) +{ + while (ares_array_len(channel->sock_state_queue) > 0) { + ares_sock_state_msg_t msg; + + if (ares_array_claim_at(&msg, sizeof(msg), channel->sock_state_queue, 0) != + ARES_SUCCESS) { + break; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + + if (channel->sock_state_cb != NULL) { + /* The public callback takes ints. */ + channel->sock_state_cb(channel->sock_state_cb_data, msg.fd, + msg.readable ? 1 : 0, msg.writable ? 1 : 0); + } + + /* Told first, closed second -- the ordering the application depends on to + * keep its descriptor set honest. */ + if (msg.owns_fd) { + ares_socket_close(channel, msg.fd); + } + } + + ares_array_destroy(channel->sock_state_queue); + channel->sock_state_queue = NULL; +} + +static void ares_deferred_cb_flush(ares_channel_t *channel) +{ + /* Only the outermost frame delivers: anything above us on the stack may + * still be holding the connection a reentrant call from one of these + * callbacks could free, or lookup state of its own that a callback could + * end. That is why the outermost frame belongs to the public entry point + * and never to an internal helper; see ares_deferred_cb_enter(). + * + * The dispatch below counts as a nesting level of its own. Without that, a + * callback that calls back into the library re-enters here through the + * enter/leave pair on that call, drains the rest of the queue and destroys + * it underneath the loop -- and hands the next notification to the + * application from inside the current one. With the counter held, the + * nested leave returns immediately and whatever it queued is picked up by + * the loop that is already running. */ + if (channel->deferred_cb_depth > 0) { + return; + } + channel->deferred_cb_depth++; + + /* Both kinds of deferred notification share this one delivery point. Loop + * rather than calling each once: a socket-state callback may put the library + * on a path that queues a server-state notification, and the reverse, and + * both belong to this delivery point rather than the next one. */ + while (ares_array_len(channel->sock_state_queue) > 0 || + ares_array_len(channel->server_state_queue) > 0) { + ares_sock_state_dispatch(channel); + ares_server_state_dispatch(channel); + } + + /* The outermost call is unwinding and the callbacks above have run, so + * nothing can still be holding a server or connection that was unlinked + * during it. Now they can be freed. */ + ares_deferred_reclaim(channel); + + channel->deferred_cb_depth--; +} + +void ares_deferred_reclaim(ares_channel_t *channel) +{ + /* Connections first: a pending server's connection list is already empty, + * but this keeps the ordering obvious rather than incidental. */ + while (channel->pending_conns != NULL) { + ares_conn_t *conn = channel->pending_conns; + channel->pending_conns = conn->next_pending; + conn->next_pending = NULL; + ares_conn_reap(conn); + } + + while (channel->pending_servers != NULL) { + ares_server_t *server = channel->pending_servers; + channel->pending_servers = server->next_pending; + server->next_pending = NULL; + ares_server_reap(server); + } +} + +void ares_deferred_cb_enter(ares_channel_t *channel) +{ + channel->deferred_cb_depth++; +} + +void ares_deferred_cb_leave(ares_channel_t *channel) +{ + if (channel->deferred_cb_depth > 0) { + channel->deferred_cb_depth--; + } + ares_deferred_cb_flush(channel); +} + +ares_bool_t ares_conn_sock_state_cb_close(ares_conn_t *conn) +{ + ares_channel_t *channel = conn->server->channel; + ares_sock_state_msg_t msg; - if ((conn->state_flags & ARES_CONN_STATE_CBFLAGS) != flags && - channel->sock_state_cb) { - channel->sock_state_cb(channel->sock_state_cb_data, conn->fd, - flags & ARES_CONN_STATE_READ ? 1 : 0, - flags & ARES_CONN_STATE_WRITE ? 1 : 0); + /* No callback to run, so nothing will ever carry this descriptor: the + * caller closes it itself, exactly as before. */ + if (channel->sock_state_cb == NULL) { + return ARES_FALSE; } + /* Already reported as unwatched, so no notification is due and no message + * will be queued to hold the descriptor. */ + if ((conn->state_flags & ARES_CONN_STATE_CBFLAGS) == ARES_CONN_STATE_NONE) { + return ARES_FALSE; + } + + msg.fd = conn->fd; + msg.readable = ARES_FALSE; + msg.writable = ARES_FALSE; + msg.owns_fd = ARES_TRUE; + + if (!ares_sock_state_queue(channel, &msg)) { + return ARES_FALSE; /* LCOV_EXCL_LINE: OutOfMemory */ + } + + conn->state_flags &= ~((unsigned int)ARES_CONN_STATE_CBFLAGS); + + return ARES_TRUE; +} + +void ares_conn_sock_state_cb_update(ares_conn_t *conn, + ares_conn_state_flags_t flags) +{ + ares_channel_t *channel = conn->server->channel; + ares_sock_state_msg_t msg; + ares_bool_t changed = ARES_FALSE; + + if ((conn->state_flags & ARES_CONN_STATE_CBFLAGS) != flags) { + changed = ARES_TRUE; + } + + msg.fd = conn->fd; + msg.readable = flags & ARES_CONN_STATE_READ ? ARES_TRUE : ARES_FALSE; + msg.writable = flags & ARES_CONN_STATE_WRITE ? ARES_TRUE : ARES_FALSE; + msg.owns_fd = ARES_FALSE; /* the connection still owns it */ + + if (!changed || channel->sock_state_cb == NULL) { + conn->state_flags &= ~((unsigned int)ARES_CONN_STATE_CBFLAGS); + conn->state_flags |= flags; + return; + } + + /* Queue rather than dispatch. This runs in the middle of opening, writing + * to or closing a connection, and every caller keeps using that connection + * afterwards. The callback is application code that may reentrantly call + * ares_cancel() or ares_set_servers_csv(), either of which frees the + * connection -- the first through the idle-connection sweep, the second by + * destroying the server that owns it. Deliver once no frame still holds + * it. + * + * Only the descriptor and the state are captured, never the connection, and + * insertion order is preserved so the application observes the same + * sequence it would have seen inline. */ + if (!ares_sock_state_queue(channel, &msg)) { + return; /* LCOV_EXCL_LINE: OutOfMemory */ + } + + /* Recorded as reported only now: on either failure above the connection + * stays eligible to notify again, rather than remembering a state the + * application was never told about. */ conn->state_flags &= ~((unsigned int)ARES_CONN_STATE_CBFLAGS); conn->state_flags |= flags; } @@ -59,9 +252,23 @@ (struct sockaddr *)&sa_storage, &salen, read_bytes); #ifdef HAVE_RECVFROM + /* The socket is connect()ed, so the kernel normally drops datagrams from + * any other address or port already. Application-provided socket + * functions might not filter on connect, so also check here that the + * datagram came from the server's address and UDP port. RFC 5452 + * section 9.1 only requires the address to match; matching the port too + * keeps one server from answering for another on the same address. + * Socket functions that fill in the source address but leave the port 0 + * predate the port check, so for those only the address is checked. A + * sender cannot use this to skip the port check: the default socket + * functions connect() the socket, so the kernel drops a datagram from + * port 0, and an unprivileged process on the server's host cannot send + * from port 0, while an off-path attacker able to forge port 0 can forge + * the server's real port just as easily. */ if (err == ARES_CONN_ERR_SUCCESS && - !ares_sockaddr_addr_eq((struct sockaddr *)&sa_storage, - &conn->server->addr)) { + !ares_sockaddr_addr_port_eq((struct sockaddr *)&sa_storage, + &conn->server->addr, + conn->server->udp_port)) { err = ARES_CONN_ERR_WOULDBLOCK; } #endif @@ -69,6 +276,15 @@ err = ares_socket_recv(channel, conn->fd, ARES_TRUE, data, len, read_bytes); } + /* The receive function is application code and may have reconfigured the + * servers, tearing this connection down. Nothing read from it can be + * matched any more -- its queries have moved to the new configuration -- + * and its descriptor is gone, so it must not be read again. */ + if (conn->flags & ARES_CONN_FLAG_DEAD) { + *read_bytes = 0; + return ARES_CONN_ERR_CONNCLOSED; + } + /* Toggle connected state if needed */ if (err == ARES_CONN_ERR_SUCCESS) { conn->state_flags |= ARES_CONN_STATE_CONNECTED; @@ -215,6 +431,19 @@ } done: + /* The send function -- from ares_set_socket_functions_ex() or the legacy + * ares_set_socket_functions() -- and on the first TCP Fast Open write the + * getsockname function, are application code and may have reconfigured the + * servers, tearing this connection down. Whatever the write returned, + * nothing written here can be answered any more, and the descriptor has + * already been closed or handed to the close notification, so there is no + * socket state left to report. Say so, so every caller treats it as the + * connection failure it is. */ + if (conn->flags & ARES_CONN_FLAG_DEAD) { + *written = 0; + return ARES_CONN_ERR_CONNCLOSED; + } + if (err == ARES_CONN_ERR_SUCCESS && len == *written) { /* Wrote all data, make sure we're not listening for write events unless * using TFO, in which case we'll need a write event to know when @@ -435,12 +664,29 @@ conn->flags |= ARES_CONN_FLAG_TFO_INITIAL; } + /* The socket-configure and socket-create callbacks above are application + * code, and either may reconfigure the servers -- which destroys the ones + * the new configuration omits, including possibly this one. Everything + * below dereferences it, starting with conn->server in + * ares_conn_set_self_ip(). */ + if (!ares_server_in_channel(channel, server)) { + status = ARES_ECONNREFUSED; + goto done; + } + /* Need to store our own ip for DNS cookie support */ status = ares_conn_set_self_ip(conn, ARES_TRUE); if (status != ARES_SUCCESS) { goto done; /* LCOV_EXCL_LINE: UntestablePath */ } + /* The getsockname function is application code as well, and may have + * reconfigured the servers the same way. */ + if (!ares_server_in_channel(channel, server)) { + status = ARES_ECONNREFUSED; + goto done; + } + /* TCP connections are thrown to the end as we don't spawn multiple TCP * connections. UDP connections are put on front where the newest connection * can be quickly pulled */ diff -Nru c-ares-1.34.5/src/lib/ares_conn.h c-ares-1.34.9/src/lib/ares_conn.h --- c-ares-1.34.5/src/lib/ares_conn.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_conn.h 2026-10-20 02:31:51.000000000 +0000 @@ -43,7 +43,20 @@ ARES_CONN_FLAG_TFO = 1 << 1, /*! TCP Fast Open has not yet sent its first packet. Gets unset on first * write to a connection */ - ARES_CONN_FLAG_TFO_INITIAL = 1 << 2 + ARES_CONN_FLAG_TFO_INITIAL = 1 << 2, + /*! Connection has been retired: it must not be handed any NEW queries (e.g. + * it experienced a query timeout, suggesting packets are being dropped on + * it). Its in-flight queries continue to drain and it is cleaned up once + * idle. This is a per-connection signal so that a transient failure does + * not evict otherwise-healthy connections to the same server. */ + ARES_CONN_FLAG_NONEW = 1 << 3, + /*! Connection has been torn down: unlinked from every lookup structure, its + * descriptor closed or handed to a pending notification, and its queries + * requeued. It survives only so that a caller further up the stack which + * captured it before the teardown can still read it; see + * ares_deferred_reclaim(). Teardown is idempotent -- a second + * ares_close_connection() on a dead connection does nothing. */ + ARES_CONN_FLAG_DEAD = 1 << 4 } ares_conn_flags_t; typedef enum { @@ -78,6 +91,10 @@ /* list of outstanding queries to this connection */ ares_llist_t *queries_to_conn; + + /*! Next connection awaiting reclamation. Set only while this connection is + * on channel->pending_conns; see ares_deferred_reclaim(). */ + ares_conn_t *next_pending; }; /*! Various buckets for grouping history */ @@ -162,6 +179,10 @@ /* Link back to owning channel */ ares_channel_t *channel; + + /*! Next server awaiting reclamation. Set only while this server is on + * channel->pending_servers; see ares_deferred_reclaim(). */ + ares_server_t *next_pending; }; void ares_close_connection(ares_conn_t *conn, ares_status_t requeue_status); @@ -179,8 +200,24 @@ ares_conn_err_t ares_conn_read(ares_conn_t *conn, void *data, size_t len, size_t *read_bytes); ares_conn_t *ares_conn_from_fd(const ares_channel_t *channel, ares_socket_t fd); -void ares_conn_sock_state_cb_update(ares_conn_t *conn, - ares_conn_state_flags_t flags); + +/*! Queue the "stop watching this descriptor" notification for a connection + * being torn down, handing the descriptor to that queued message. + * + * The application must be told to stop watching a descriptor while it is + * still open -- close first and the number can be handed to something else, + * including by application code running from a deferred query callback, + * before the notification naming it is delivered. + * + * \param[in] conn Connection being closed. + * \return ARES_TRUE if the queued notification took the descriptor, in which + * case the caller must not close it; ARES_FALSE if no notification is + * due, and the caller closes the descriptor itself. + */ +ares_bool_t ares_conn_sock_state_cb_close(ares_conn_t *conn); + +void ares_conn_sock_state_cb_update(ares_conn_t *conn, + ares_conn_state_flags_t flags); ares_conn_err_t ares_socket_recv(ares_channel_t *channel, ares_socket_t s, ares_bool_t is_tcp, void *data, size_t data_len, size_t *read_bytes); diff -Nru c-ares-1.34.5/src/lib/ares_cookie.c c-ares-1.34.9/src/lib/ares_cookie.c --- c-ares-1.34.5/src/lib/ares_cookie.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_cookie.c 2026-10-20 02:31:51.000000000 +0000 @@ -217,7 +217,7 @@ static ares_bool_t timeval_is_set(const ares_timeval_t *tv) { - if (tv->sec != 0 && tv->usec != 0) { + if (tv->sec != 0 || tv->usec != 0) { return ARES_TRUE; } return ARES_FALSE; @@ -324,7 +324,7 @@ if (cookie->state == ARES_COOKIE_UNSUPPORTED) { /* If timer hasn't expired, just delete any possible cookie and return */ if (!timeval_expired(&cookie->unsupported_ts, now, - COOKIE_REGRESSION_TIMEOUT_MS)) { + COOKIE_UNSUPPORTED_TIMEOUT_MS)) { ares_dns_rr_del_opt_byid(rr, ARES_RR_OPT_OPTIONS, ARES_OPT_PARAM_COOKIE); return ARES_SUCCESS; } diff -Nru c-ares-1.34.5/src/lib/ares_destroy.c c-ares-1.34.9/src/lib/ares_destroy.c --- c-ares-1.34.5/src/lib/ares_destroy.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_destroy.c 2026-10-20 02:31:51.000000000 +0000 @@ -69,17 +69,31 @@ * callbacks need to hold a channel lock. */ ares_channel_lock(channel); - /* Destroy all queries */ - node = ares_llist_node_first(channel->all_queries); - while (node != NULL) { - ares_llist_node_t *next = ares_llist_node_next(node); - ares_query_t *query = ares_llist_node_claim(node); + /* Destroy all queries. + * + * The queries are detached before their callbacks run. Claiming the node + * only unlinks a query from all_queries; it stays in queries_by_qid, on + * conn->queries_to_conn and in queries_by_timeout. A reentrant teardown + * from inside the callback -- ares_set_servers_csv(), or a new query + * hitting a connection error -- would otherwise still find the query, end + * it a second time (invoking the application callback twice for one + * lookup) and free it, and the ares_free_query() below would double-free. + * + * The loop re-reads the first node each iteration rather than caching the + * next one, since a callback can cause other queries in this same list to + * be ended and their nodes destroyed. No list swap is needed: sys_up is + * already cleared above, so ares_send_nolock() refuses new queries and + * nothing can be added while we drain. */ + while ((node = ares_llist_node_first(channel->all_queries)) != NULL) { + ares_query_t *query = ares_llist_node_claim(node); + /* Must come first: node_claim() already destroyed this node, so + * ares_detach_query() would otherwise destroy it a second time. */ query->node_all_queries = NULL; + ares_detach_query(query); + query->callback(query->arg, ARES_EDESTRUCTION, 0, NULL); ares_free_query(query); - - node = next; } ares_queue_notify_empty(channel); @@ -93,7 +107,28 @@ assert(ares_slist_len(channel->queries_by_timeout) == 0); #endif + /* Tearing down the servers closes every remaining connection. Applications + * rely on being told about those closures so they can drop the descriptors + * from their own event loop, so the notifications are still delivered here + * rather than discarded -- just after the teardown instead of during it. */ + ares_deferred_cb_enter(channel); ares_destroy_servers_state(channel); + ares_deferred_cb_leave(channel); + + /* The leave above delivered and destroyed both queues and reclaimed + * everything the teardown unlinked, so these are normally no-ops. They are + * not redundant: if the first insert into a freshly created queue fails, + * the array is left allocated and empty, the flush loop never runs because + * both queues are empty, and nothing else would ever free it. */ + ares_array_destroy(channel->sock_state_queue); + channel->sock_state_queue = NULL; + ares_array_destroy(channel->server_state_queue); + channel->server_state_queue = NULL; + +#ifndef NDEBUG + assert(channel->pending_servers == NULL); + assert(channel->pending_conns == NULL); +#endif #ifndef NDEBUG assert(ares_htable_asvp_num_keys(channel->connnode_by_socket) == 0); @@ -134,17 +169,47 @@ ares_free(channel); } -void ares_destroy_server(ares_server_t *server) +/*! Release a server's memory. Split from ares_destroy_server() for the same + * reason ares_conn_reap() is split out: a caller further up the stack may + * still hold this pointer. */ +void ares_server_reap(ares_server_t *server) { if (server == NULL) { return; /* LCOV_EXCL_LINE: DefensiveCoding */ } - ares_close_sockets(server); ares_llist_destroy(server->connections); ares_free(server); } +void ares_destroy_server(ares_server_t *server) +{ + ares_channel_t *channel; + + if (server == NULL) { + return; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + + channel = server->channel; + + /* Closes and unlinks every connection; each is deferred or reaped by + * ares_close_connection() on the same rule applied below. */ + ares_close_sockets(server); + + /* The caller has already unlinked this server from channel->servers, so it + * can no longer be found -- ares_server_in_channel() and + * ares_slist_node_find() both report it gone, which is what callers check + * before acting on one. But a library call in progress may still be + * holding the pointer, so keep it readable until that call unwinds. */ + if (channel != NULL && channel->deferred_cb_depth > 0) { + server->next_pending = channel->pending_servers; + channel->pending_servers = server; + return; + } + + ares_server_reap(server); +} + void ares_destroy_servers_state(ares_channel_t *channel) { ares_slist_node_t *node; diff -Nru c-ares-1.34.5/src/lib/ares_getaddrinfo.c c-ares-1.34.9/src/lib/ares_getaddrinfo.c --- c-ares-1.34.5/src/lib/ares_getaddrinfo.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_getaddrinfo.c 2026-10-20 02:31:51.000000000 +0000 @@ -242,6 +242,11 @@ ares_status_t status = ARES_SUCCESS; ares_bool_t result = ARES_FALSE; int family = hints->ai_family; + + if (name == NULL) { + return ARES_FALSE; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + if (family == AF_INET || family == AF_INET6 || family == AF_UNSPEC) { /* It only looks like an IP address if it's all numbers and dots. */ size_t numdots = 0; @@ -268,6 +273,7 @@ if (result) { status = ares_append_ai_node(AF_INET, port, 0, &addr4, &ai->nodes); if (status != ARES_SUCCESS) { + ares_freeaddrinfo(ai); callback(arg, (int)status, 0, NULL); /* LCOV_EXCL_LINE: OutOfMemory */ return ARES_TRUE; /* LCOV_EXCL_LINE: OutOfMemory */ } @@ -282,6 +288,7 @@ if (result) { status = ares_append_ai_node(AF_INET6, port, 0, &addr6, &ai->nodes); if (status != ARES_SUCCESS) { + ares_freeaddrinfo(ai); callback(arg, (int)status, 0, NULL); /* LCOV_EXCL_LINE: OutOfMemory */ return ARES_TRUE; /* LCOV_EXCL_LINE: OutOfMemory */ } @@ -575,6 +582,57 @@ /* at this point we keep on waiting for the next query to finish */ } +/* Per POSIX getaddrinfo(), when no node/hostname is provided the returned + * addresses are synthesized from the service: the wildcard address when + * ARES_AI_PASSIVE is set (suitable for bind()ing a listening socket), + * otherwise the loopback address (suitable for connect()ing to a peer on the + * local host). */ +static ares_status_t + ares_append_nulladdr(unsigned short port, + const struct ares_addrinfo_hints *hints, + struct ares_addrinfo *ai) +{ + int family = hints->ai_family; + ares_bool_t passive = ARES_FALSE; + ares_status_t status; + struct ares_addrinfo_node *node; + + if (hints->ai_flags & ARES_AI_PASSIVE) { + passive = ARES_TRUE; + } + + if (family == AF_INET6 || family == AF_UNSPEC) { + struct ares_in6_addr addr6; + memset(&addr6, 0, sizeof(addr6)); /* wildcard "::" */ + if (!passive) { + ares_inet_pton(AF_INET6, "::1", &addr6); + } + status = ares_append_ai_node(AF_INET6, port, 0, &addr6, &ai->nodes); + if (status != ARES_SUCCESS) { + return status; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + if (family == AF_INET || family == AF_UNSPEC) { + struct in_addr addr4; + memset(&addr4, 0, sizeof(addr4)); /* wildcard "0.0.0.0" */ + if (!passive) { + ares_inet_pton(AF_INET, "127.0.0.1", &addr4); + } + status = ares_append_ai_node(AF_INET, port, 0, &addr4, &ai->nodes); + if (status != ARES_SUCCESS) { + return status; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + for (node = ai->nodes; node != NULL; node = node->ai_next) { + node->ai_socktype = hints->ai_socktype; + node->ai_protocol = hints->ai_protocol; + } + + return ARES_SUCCESS; +} + static void ares_getaddrinfo_int(ares_channel_t *channel, const char *name, const char *service, const struct ares_addrinfo_hints *hints, @@ -599,6 +657,13 @@ return; } + /* POSIX getaddrinfo() requires that at least one of node (name) or service + * be provided. */ + if (name == NULL && service == NULL) { + callback(arg, ARES_ENOTFOUND, 0, NULL); + return; + } + if (ares_is_onion_domain(name)) { callback(arg, ARES_ENOTFOUND, 0, NULL); return; @@ -606,25 +671,17 @@ if (service) { if (hints->ai_flags & ARES_AI_NUMERICSERV) { - unsigned long val; - errno = 0; - val = strtoul(service, NULL, 0); - if ((val == 0 && errno != 0) || val > 65535) { + if (!ares_parse_port(service, &port, ARES_TRUE)) { callback(arg, ARES_ESERVICE, 0, NULL); return; } - port = (unsigned short)val; } else { port = lookup_service(service, 0); if (!port) { - unsigned long val; - errno = 0; - val = strtoul(service, NULL, 0); - if ((val == 0 && errno != 0) || val > 65535) { + if (!ares_parse_port(service, &port, ARES_TRUE)) { callback(arg, ARES_ESERVICE, 0, NULL); return; } - port = (unsigned short)val; } } } @@ -635,6 +692,19 @@ return; } + /* No node/hostname was provided (service-only lookup): synthesize wildcard + * or loopback addresses from the resolved port instead of issuing a query. */ + if (name == NULL) { + status = ares_append_nulladdr(port, hints, ai); + if (status != ARES_SUCCESS) { + ares_freeaddrinfo(ai); /* LCOV_EXCL_LINE: OutOfMemory */ + callback(arg, (int)status, 0, NULL); /* LCOV_EXCL_LINE: OutOfMemory */ + return; /* LCOV_EXCL_LINE: OutOfMemory */ + } + callback(arg, ARES_SUCCESS, 0, ai); + return; + } + if (fake_addrinfo(name, port, hints, ai, callback, arg)) { return; } @@ -692,7 +762,12 @@ return; } ares_channel_lock(channel); + /* Notifications raised along the way are delivered once this whole call + * has unwound, not from inside the internals it runs. See + * ares_deferred_cb_enter(). */ + ares_deferred_cb_enter(channel); ares_getaddrinfo_int(channel, name, service, hints, callback, arg); + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); } diff -Nru c-ares-1.34.5/src/lib/ares_gethostbyaddr.c c-ares-1.34.9/src/lib/ares_gethostbyaddr.c --- c-ares-1.34.5/src/lib/ares_gethostbyaddr.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_gethostbyaddr.c 2026-10-20 02:31:51.000000000 +0000 @@ -112,7 +112,12 @@ return; } ares_channel_lock(channel); + /* Notifications raised along the way are delivered once this whole call + * has unwound, not from inside the internals it runs. See + * ares_deferred_cb_enter(). */ + ares_deferred_cb_enter(channel); ares_gethostbyaddr_nolock(channel, addr, addrlen, family, callback, arg); + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); } diff -Nru c-ares-1.34.5/src/lib/ares_gethostbyname.c c-ares-1.34.9/src/lib/ares_gethostbyname.c --- c-ares-1.34.5/src/lib/ares_gethostbyname.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_gethostbyname.c 2026-10-20 02:31:51.000000000 +0000 @@ -102,6 +102,10 @@ struct ares_addrinfo_hints hints; struct host_query *ghbn_arg; + if (channel == NULL) { + return; + } + if (!callback) { return; } diff -Nru c-ares-1.34.5/src/lib/ares_getnameinfo.c c-ares-1.34.9/src/lib/ares_getnameinfo.c --- c-ares-1.34.5/src/lib/ares_getnameinfo.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_getnameinfo.c 2026-10-20 02:31:51.000000000 +0000 @@ -194,7 +194,12 @@ } ares_channel_lock(channel); + /* Notifications raised along the way are delivered once this whole call + * has unwound, not from inside the internals it runs. See + * ares_deferred_cb_enter(). */ + ares_deferred_cb_enter(channel); ares_getnameinfo_int(channel, sa, salen, flags_int, callback, arg); + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); } @@ -223,9 +228,12 @@ */ #ifdef HAVE_GETHOSTNAME if (niquery->flags & ARES_NI_NOFQDN) { - char buf[255]; + char buf[255] = ""; const char *domain; - gethostname(buf, 255); + if (gethostname(buf, (GETHOSTNAME_TYPE_ARG2)sizeof(buf)) != 0) { + buf[0] = '\0'; + } + buf[sizeof(buf) - 1] = '\0'; if ((domain = strchr(buf, '.')) != NULL) { char *end = ares_striendstr(host->h_name, domain); if (end) { diff -Nru c-ares-1.34.5/src/lib/ares_hosts_file.c c-ares-1.34.9/src/lib/ares_hosts_file.c --- c-ares-1.34.5/src/lib/ares_hosts_file.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_hosts_file.c 2026-10-20 02:31:51.000000000 +0000 @@ -41,6 +41,11 @@ #endif #include +#ifdef USE_WINSOCK +# define DATABASEPATH "DatabasePath" +# define WIN_PATH_HOSTS "\\hosts" +#endif + /* HOSTS FILE PROCESSING OVERVIEW * ============================== * The hosts file on the system contains static entries to be processed locally @@ -669,6 +674,9 @@ &dwLength); ExpandEnvironmentStringsA(tmp, PATH_HOSTS, MAX_PATH); RegCloseKey(hkeyHosts); + if (strlen(PATH_HOSTS)+strlen(WIN_PATH_HOSTS) >= MAX_PATH) { + return ARES_ENOTFOUND; + } strcat(PATH_HOSTS, WIN_PATH_HOSTS); #elif defined(WATT32) const char *PATH_HOSTS = _w32_GetHostsFile(); diff -Nru c-ares-1.34.5/src/lib/ares_inet_net_pton.h c-ares-1.34.9/src/lib/ares_inet_net_pton.h --- c-ares-1.34.5/src/lib/ares_inet_net_pton.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_inet_net_pton.h 2026-10-20 02:31:51.000000000 +0000 @@ -26,6 +26,34 @@ #ifndef HEADER_CARES_INET_NET_PTON_H #define HEADER_CARES_INET_NET_PTON_H +/*! Parse a network (an address with an optional "/bits" prefix length) from + * its text form. This is not an address parser: for an address use + * ares_inet_pton(), which rejects a prefix length and always writes the + * whole address. + * + * How it differs from ares_inet_pton(): + * - A "/bits" prefix length is accepted (0-32 for IPv4, 0-128 for IPv6). + * - The return value is the prefix length, not 1. Without "/bits", IPv6 + * returns 128 and IPv4 infers it from the address class, widened to + * cover every octet given ("10" is 8, "130.155" is 16, "1.2.3.4" is 32, + * and a lone "224" is 4). 0 is a valid result ("::1/0"). + * - What is written to dst differs by family. On success all size bytes + * are zeroed first. IPv4 then writes every octet given, including bits + * beyond the prefix ("1.2.3.4/8" writes 1.2.3.4). IPv6 writes only the + * (bits+7)/8 bytes covering the prefix ("2001:db8::1/64" writes + * 2001:db8::). + * + * \param[in] af AF_INET or AF_INET6. + * \param[in] src NUL-terminated network in text form. + * \param[out] dst Buffer to receive the network, in network byte order. + * \param[in] size Size of dst in bytes. + * \return Prefix length in bits on success. -1 on failure, with SOCKERRNO + * set to ENOENT if src is not a valid network, or EMSGSIZE if it + * does not fit in size bytes or (IPv6) src is too long to be a + * network. On Windows these are the WSA equivalents. An + * unsupported af returns -1 without setting SOCKERRNO. On failure + * dst may be partially written. + */ int ares_inet_net_pton(int af, const char *src, void *dst, size_t size); #endif /* HEADER_CARES_INET_NET_PTON_H */ diff -Nru c-ares-1.34.5/src/lib/ares_init.c c-ares-1.34.9/src/lib/ares_init.c --- c-ares-1.34.5/src/lib/ares_init.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_init.c 2026-10-20 02:31:51.000000000 +0000 @@ -102,6 +102,20 @@ if (s1->consec_failures > s2->consec_failures) { return 1; } + + /* Among servers with the same failure count, prefer the one that failed + * least recently so that a server that went down long ago (and may have + * since recovered) is tried before one that just failed. This also keeps + * retries rotating across servers once the failure counts saturate at + * SERVER_CONSEC_FAILURES_CAP. Healthy servers all have a zeroed retry + * time and fall through to configuration order. */ + if (s1->next_retry_time.sec != s2->next_retry_time.sec) { + return s1->next_retry_time.sec < s2->next_retry_time.sec ? -1 : 1; + } + if (s1->next_retry_time.usec != s2->next_retry_time.usec) { + return s1->next_retry_time.usec < s2->next_retry_time.usec ? -1 : 1; + } + if (s1->idx < s2->idx) { return -1; } @@ -180,17 +194,21 @@ size_t len = 256; channel->ndomains = 0; /* default to none */ - hostname = ares_malloc(len); + hostname = ares_malloc_zero(len); if (!hostname) { rc = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ goto error; /* LCOV_EXCL_LINE: OutOfMemory */ } + /* POSIX leaves it unspecified whether gethostname() NUL terminates when the + * host name doesn't fit, so force the last byte to a terminator afterwards. + */ if (gethostname(hostname, (GETHOSTNAME_TYPE_ARG2)len) != 0) { /* Lets not treat a gethostname failure as critical, since we * are ok if gethostname doesn't even exist */ *hostname = '\0'; } + hostname[len - 1] = '\0'; dot = strchr(hostname, '.'); if (dot) { @@ -271,6 +289,8 @@ goto done; } + ares_set_socket_functions_def(channel); + /* Initialize Server List */ channel->servers = ares_slist_create(channel->rand_state, server_sort_cb, server_destroy_cb); @@ -346,8 +366,6 @@ goto done; } - ares_set_socket_functions_def(channel); - /* Initialize the event thread */ if (channel->optmask & ARES_OPT_EVENT_THREAD) { ares_event_thread_t *e = NULL; @@ -362,7 +380,8 @@ e = channel->sock_state_cb_data; status = ares_event_configchg_init(&e->configchg, e); if (status != ARES_SUCCESS && status != ARES_ENOTIMP) { - goto done; /* LCOV_EXCL_LINE: UntestablePath */ + DEBUGF(fprintf(stderr, "Error: ares_event_configchg_init failed: %s\n", + ares_strerror(status))); } status = ARES_SUCCESS; } @@ -464,7 +483,8 @@ *dest = NULL; /* in case of failure return NULL explicitly */ /* First get the options supported by the old ares_save_options() function, - which is most of them */ + * which is most of them. ares_save_options() takes the src channel lock + * itself. No lock is held across ares_init_options(). */ rc = (ares_status_t)ares_save_options(src, &opts, &optmask); if (rc != ARES_SUCCESS) { ares_destroy_options(&opts); @@ -484,16 +504,29 @@ ares_channel_lock(src); /* Now clone the options that ares_save_options() doesn't support, but are * user-provided */ - (*dest)->sock_create_cb = src->sock_create_cb; - (*dest)->sock_create_cb_data = src->sock_create_cb_data; - (*dest)->sock_config_cb = src->sock_config_cb; - (*dest)->sock_config_cb_data = src->sock_config_cb_data; + (*dest)->sock_create_cb = src->sock_create_cb; + (*dest)->sock_create_cb_data = src->sock_create_cb_data; + (*dest)->sock_config_cb = src->sock_config_cb; + (*dest)->sock_config_cb_data = src->sock_config_cb_data; memcpy(&(*dest)->sock_funcs, &(src->sock_funcs), sizeof((*dest)->sock_funcs)); - (*dest)->sock_func_cb_data = src->sock_func_cb_data; + (*dest)->sock_func_cb_data = src->sock_func_cb_data; + /* Socket callbacks given the channel itself as user data (as the + * ares_set_socket_functions() wrappers are) must get the new channel */ + if (src->sock_func_cb_data == src) { + (*dest)->sock_func_cb_data = *dest; + } (*dest)->legacy_sock_funcs = src->legacy_sock_funcs; (*dest)->legacy_sock_funcs_cb_data = src->legacy_sock_funcs_cb_data; (*dest)->server_state_cb = src->server_state_cb; (*dest)->server_state_cb_data = src->server_state_cb_data; + /* With an event thread these callbacks belong to the source's event + * thread; the new channel already has its own */ + if (!(src->optmask & ARES_OPT_EVENT_THREAD)) { + (*dest)->notify_pending_write_cb = src->notify_pending_write_cb; + (*dest)->notify_pending_write_cb_data = src->notify_pending_write_cb_data; + (*dest)->query_enqueue_cb = src->query_enqueue_cb; + (*dest)->query_enqueue_cb_data = src->query_enqueue_cb_data; + } ares_strcpy((*dest)->local_dev_name, src->local_dev_name, sizeof((*dest)->local_dev_name)); @@ -598,3 +631,15 @@ ares_channel_unlock(channel); return (int)status; } + +void ares_set_query_enqueue_cb(ares_channel_t *channel, + ares_query_enqueue_cb callback, + void *user_data) +{ + if (channel == NULL) { + return; + } + + channel->query_enqueue_cb = callback; + channel->query_enqueue_cb_data = user_data; +} diff -Nru c-ares-1.34.5/src/lib/ares_library_init.c c-ares-1.34.9/src/lib/ares_library_init.c --- c-ares-1.34.5/src/lib/ares_library_init.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_library_init.c 2026-10-20 02:31:51.000000000 +0000 @@ -36,7 +36,6 @@ /* library-private global vars with source visibility restricted to this file */ static unsigned int ares_initialized; -static int ares_init_flags; /* library-private global vars with visibility across the whole library */ @@ -105,6 +104,27 @@ return p; } +void *ares_malloc_zero_array(size_t num, size_t size) +{ + size_t total; + if (ares_size_t_mul_overflow(num, size, &total)) { + return NULL; + } + return ares_malloc_zero(total); +} + +void *ares_realloc_zero_array(void *ptr, size_t orig_num, size_t new_num, + size_t size) +{ + size_t orig_total; + size_t new_total; + if (ares_size_t_mul_overflow(orig_num, size, &orig_total) || + ares_size_t_mul_overflow(new_num, size, &new_total)) { + return NULL; + } + return ares_realloc_zero(ptr, orig_total, new_total); +} + int ares_library_init(int flags) { if (ares_initialized) { @@ -114,8 +134,7 @@ ares_initialized++; /* NOTE: ARES_LIB_INIT_WIN32 flag no longer used */ - - ares_init_flags = flags; + (void)flags; return ARES_SUCCESS; } @@ -152,10 +171,9 @@ ares_library_cleanup_android(); #endif - ares_init_flags = ARES_LIB_INIT_NONE; - __ares_malloc = default_malloc; - __ares_realloc = default_realloc; - __ares_free = default_free; + __ares_malloc = default_malloc; + __ares_realloc = default_realloc; + __ares_free = default_free; } int ares_library_initialized(void) diff -Nru c-ares-1.34.5/src/lib/ares_options.c c-ares-1.34.9/src/lib/ares_options.c --- c-ares-1.34.5/src/lib/ares_options.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_options.c 2026-10-20 02:31:51.000000000 +0000 @@ -55,8 +55,8 @@ int *nservers) { ares_slist_node_t *snode; - struct in_addr *out = - ares_malloc_zero(ares_slist_len(channel->servers) * sizeof(*out)); + size_t cnt = ares_slist_len(channel->servers); + struct in_addr *out = ares_malloc_zero_array(cnt, sizeof(*out)); *nservers = 0; @@ -64,7 +64,11 @@ return NULL; } - for (snode = ares_slist_node_first(channel->servers); snode != NULL; + /* Never write more entries than were allocated. The caller holds the + * channel lock so the server list cannot change underneath us; this is + * purely defensive. */ + for (snode = ares_slist_node_first(channel->servers); + snode != NULL && (size_t)*nservers < cnt; snode = ares_slist_node_next(snode)) { const ares_server_t *server = ares_slist_node_val(snode); @@ -79,9 +83,11 @@ return out; } -/* Save options from initialized channel */ -int ares_save_options(const ares_channel_t *channel, - struct ares_options *options, int *optmask) +/* Save options from initialized channel. Caller must hold the channel lock + * (if channel is not NULL). */ +static ares_status_t ares_save_options_nolock(const ares_channel_t *channel, + struct ares_options *options, + int *optmask) { size_t i; @@ -150,7 +156,8 @@ if (channel->optmask & ARES_OPT_DOMAINS) { options->domains = NULL; if (channel->ndomains) { - options->domains = ares_malloc(channel->ndomains * sizeof(char *)); + options->domains = + ares_malloc_zero_array(channel->ndomains, sizeof(char *)); if (!options->domains) { return ARES_ENOMEM; } @@ -176,7 +183,8 @@ if (channel->optmask & ARES_OPT_SORTLIST) { options->sortlist = NULL; if (channel->nsort) { - options->sortlist = ares_malloc(channel->nsort * sizeof(struct apattern)); + options->sortlist = + ares_malloc_zero_array(channel->nsort, sizeof(*options->sortlist)); if (!options->sortlist) { return ARES_ENOMEM; } @@ -238,6 +246,27 @@ return ARES_SUCCESS; } +int ares_save_options(const ares_channel_t *channel, + struct ares_options *options, int *optmask) +{ + ares_status_t status; + + if (channel == NULL) { + /* Preserve historic behavior: initialize the pointers so + * ares_destroy_options() is safe, then return ARES_ENODATA */ + return (int)ares_save_options_nolock(NULL, options, optmask); + } + + /* The configuration may be modified concurrently by other threads (e.g. + * ares_set_servers_csv()), so it must only be read with the channel lock + * held. */ + ares_channel_lock(channel); + status = ares_save_options_nolock(channel, options, optmask); + ares_channel_unlock(channel); + + return (int)status; +} + static ares_status_t ares_init_options_servers(ares_channel_t *channel, const struct in_addr *servers, size_t nservers) @@ -462,8 +491,7 @@ /* qcache_max_ttl is unsigned unlike the others */ channel->qcache_max_ttl = options->qcache_max_ttl; } else { - optmask |= ARES_OPT_QUERY_CACHE; - channel->qcache_max_ttl = 3600; + channel->qcache_max_ttl = 3600; } /* Initialize the ipv4 servers if provided */ diff -Nru c-ares-1.34.5/src/lib/ares_parse_into_addrinfo.c c-ares-1.34.9/src/lib/ares_parse_into_addrinfo.c --- c-ares-1.34.5/src/lib/ares_parse_into_addrinfo.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_parse_into_addrinfo.c 2026-10-20 02:31:51.000000000 +0000 @@ -108,7 +108,7 @@ status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } - cname->ttl = (int)ares_dns_rr_get_ttl(rr); + cname->ttl = ares_dns_ttl_as_int(ares_dns_rr_get_ttl(rr)); cname->alias = ares_strdup(ares_dns_rr_get_name(rr)); if (cname->alias == NULL) { status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ diff -Nru c-ares-1.34.5/src/lib/ares_private.h c-ares-1.34.9/src/lib/ares_private.h --- c-ares-1.34.5/src/lib/ares_private.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_private.h 2026-10-20 02:31:51.000000000 +0000 @@ -83,21 +83,7 @@ #define CARES_INADDR_CAST(type, var) ((type)((const void *)var)) #if defined(USE_WINSOCK) - -# define WIN_NS_9X "System\\CurrentControlSet\\Services\\VxD\\MSTCP" # define WIN_NS_NT_KEY "System\\CurrentControlSet\\Services\\Tcpip\\Parameters" -# define WIN_DNSCLIENT "Software\\Policies\\Microsoft\\System\\DNSClient" -# define WIN_NT_DNSCLIENT \ - "Software\\Policies\\Microsoft\\Windows NT\\DNSClient" -# define NAMESERVER "NameServer" -# define DHCPNAMESERVER "DhcpNameServer" -# define DATABASEPATH "DatabasePath" -# define WIN_PATH_HOSTS "\\hosts" -# define SEARCHLIST_KEY "SearchList" -# define PRIMARYDNSSUFFIX_KEY "PrimaryDNSSuffix" -# define INTERFACES_KEY "Interfaces" -# define DOMAIN_KEY "Domain" -# define DHCPDOMAIN_KEY "DhcpDomain" # define PATH_RESOLV_CONF "" #elif defined(WATT32) @@ -145,6 +131,15 @@ #define DEFAULT_SERVER_RETRY_CHANCE 10 #define DEFAULT_SERVER_RETRY_DELAY 5000 +/* Upper bound on the consecutive failure count tracked per server. Only the + * relative order of the counts is used for server selection, so magnitude + * beyond "clearly down" carries no additional signal. Capping it bounds how + * long a server that failed for an extended period sorts behind other failed + * servers once it comes back online. */ +#define SERVER_CONSEC_FAILURES_CAP 16 + +typedef void (*ares_query_enqueue_cb)(void *data); + struct ares_query; typedef struct ares_query ares_query_t; @@ -152,9 +147,15 @@ struct ares_query { /* Query ID from qbuf, for faster lookup, and current timeout */ unsigned short qid; /* host byte order */ - ares_timeval_t ts; /*!< Timestamp query was sent */ + /*! Distinguishes this query from any later one handed the same qid. + * Query ids are 16-bit and recycled; this counter is not. */ + size_t serial; + ares_timeval_t ts; /*!< Timestamp query was sent */ ares_timeval_t timeout; ares_channel_t *channel; + /*! Query cache key, derived from the caller's request before the send path + * can mutate query->query. NULL when the cache is disabled. */ + char *qcache_key; /* * Node object for each list entry the query belongs to in order to @@ -232,6 +233,9 @@ /* Server addresses and communications state. Sorted by least consecutive * failures, followed by the configuration order if failures are equal. */ ares_slist_t *servers; + /*! Bumped by each ares_servers_update(). Lets the stale-server removal loop + * tell that a nested reconfiguration reconciled the list underneath it. */ + size_t servers_generation; /* random state to use when generating new ids and generating retry penalties */ @@ -242,6 +246,10 @@ /* Queries bucketed by qid, for quickly dispatching DNS responses: */ ares_htable_szvp_t *queries_by_qid; + /*! Monotonic source for ares_query_t.serial. Never reused for the life of + * the channel, so a recycled query id cannot impersonate an older query. */ + size_t query_serial; + /* Queries bucketed by timeout, for quickly handling timeouts: */ ares_slist_t *queries_by_timeout; @@ -253,6 +261,29 @@ ares_sock_state_cb sock_state_cb; void *sock_state_cb_data; + /*! Socket-state notifications queued during processing, delivered once the + * library is no longer holding a connection a reentrant call could free. + * Insertion order is preserved, so an application observes the same + * sequence of state changes it would have seen inline. */ + ares_array_t *sock_state_queue; + /*! Depth of nested library entry points that may produce a deferred + * application notification. Queues are delivered as this returns to + * zero. Shared by every deferred callback kind rather than one counter + * per kind, so the delivery rule exists in exactly one place. */ + size_t deferred_cb_depth; + + /*! Servers and connections that have been unlinked from every lookup + * structure but not yet freed, because a library call was in progress and + * may still hold a pointer to them. Drained by the same flush that + * delivers deferred notifications, once no frame can still be holding one. + * + * This is what makes a stale ares_server_t or ares_conn_t readable rather + * than freed for the remainder of the call that captured it. Callers + * still revalidate before *acting* on one -- ares_server_in_channel() and + * friends -- but a missed check is now "used a server that was removed" + * rather than a use-after-free. */ + ares_server_t *pending_servers; + ares_conn_t *pending_conns; ares_sock_create_callback sock_create_cb; void *sock_create_cb_data; @@ -269,6 +300,9 @@ void *notify_pending_write_cb_data; ares_bool_t notify_pending_write; + ares_query_enqueue_cb query_enqueue_cb; + void *query_enqueue_cb_data; + /* Path for resolv.conf file, configurable via ares_options */ char *resolvconf_path; @@ -297,6 +331,10 @@ /* Callback triggered when a server has a successful or failed response */ ares_server_state_callback server_state_cb; void *server_state_cb_data; + /*! Server-state notifications queued during processing, dispatched once the + * library is no longer holding pointers a reentrant callback could free. + * See ares_deferred_cb_leave(). */ + ares_array_t *server_state_queue; /* TRUE if a reinit is pending. Reinit spawns a thread to read the system * configuration and then apply the configuration since configuration @@ -310,6 +348,11 @@ * system config changes might get triggered and we need a flag to make * sure we don't take action. */ ares_bool_t sys_up; + + /*! Set while ares_send_nolock() is running the refusal notification for a + * channel being destroyed. Bounds the recursion at one frame when the + * application's callback responds by starting another lookup. */ + ares_bool_t in_shutdown_refusal; }; /* Does the domain end in ".onion" or ".onion."? Case-insensitive. */ @@ -321,7 +364,7 @@ ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now, ares_status_t status, ares_bool_t inc_try_count, - const ares_dns_record_t *dnsrec, + ares_dns_record_t *dnsrec, ares_array_t **requeue); /*! Count the number of labels (dots+1) in a domain */ @@ -353,6 +396,16 @@ void ares_dnsrec_convert_cb(void *arg, ares_status_t status, size_t timeouts, const ares_dns_record_t *dnsrec); +/*! Unlink a query from every list and table that tracks it. + * + * Must be called before running application code that could reach the query + * again, so a reentrant teardown cannot find it and end it a second time. + * + * \param[in] query Query to detach. Safe to call more than once on the + * same query. + */ +void ares_detach_query(ares_query_t *query); + void ares_free_query(ares_query_t *query); unsigned short ares_generate_new_id(ares_rand_state *state); @@ -367,6 +420,9 @@ ares_status_t ares_init_by_options(ares_channel_t *channel, const struct ares_options *options, int optmask); +void ares_set_query_enqueue_cb(ares_channel_t *channel, + ares_query_enqueue_cb callback, + void *user_data); ares_status_t ares_init_by_sysconfig(ares_channel_t *channel); void ares_set_socket_functions_def(ares_channel_t *channel); @@ -389,7 +445,6 @@ ares_status_t ares_init_by_environment(ares_sysconfig_t *sysconfig); - typedef ares_status_t (*ares_sysconfig_line_cb_t)(const ares_channel_t *channel, ares_sysconfig_t *sysconfig, ares_buf_t *line); @@ -424,6 +479,9 @@ ares_status_t ares_lookup_hostaliases(const ares_channel_t *channel, const char *name, char **alias); +ares_bool_t ares_parse_port(const char *str, unsigned short *port, + ares_bool_t allow_zero); + ares_status_t ares_cat_domain(const char *name, const char *domain, char **s); ares_status_t ares_sortaddrinfo(ares_channel_t *channel, struct ares_addrinfo_node *ai_node); @@ -545,10 +603,15 @@ * ares_free()'d by the caller. * \param[in] is_hostname if ARES_TRUE, will validate the character set for * a valid hostname or will return error. + * \param[in] allow_compression if ARES_FALSE, a compression pointer + * encountered while parsing the name will be rejected + * with ARES_EBADNAME. Used for RR types (e.g. SRV) + * whose RDATA names must not use name compression. * \return ARES_SUCCESS on success */ ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name, - ares_bool_t is_hostname); + ares_bool_t is_hostname, + ares_bool_t allow_compression); /*! Write the DNS name to the buffer in the DNS domain-name syntax as a * series of labels. The maximum domain name length is 255 characters with @@ -580,6 +643,70 @@ */ void ares_queue_notify_empty(ares_channel_t *channel); +/*! Is this server still installed on the channel? + * + * Application callbacks may reconfigure the servers, which destroys any that + * the new configuration omits. A raw ares_server_t captured before such a + * callback must be revalidated before it is used again. + * + * \param[in] channel Initialized ares channel. + * \param[in] server Server pointer captured earlier. + * \return ARES_TRUE if still present. + */ +ares_bool_t ares_server_in_channel(const ares_channel_t *channel, + const ares_server_t *server); + +/*! Free everything awaiting reclamation on the channel. + * + * Called only from the deferred-callback flush, at the point where the + * outermost library call is unwinding and no frame can still hold a pointer + * to an unlinked server or connection. + * + * \param[in] channel Initialized ares channel. + */ +void ares_deferred_reclaim(ares_channel_t *channel); + +/*! Free one connection's memory. Only for the deferred-reclaim path and the + * immediate path in ares_close_connection(). */ +void ares_conn_reap(ares_conn_t *conn); + +/*! Free one server's memory. Only for the deferred-reclaim path and the + * immediate path in ares_destroy_server(). */ +void ares_server_reap(ares_server_t *server); + +/*! Enter a region that may produce application notifications the library is + * not in a position to deliver yet -- socket-state and server-state changes. + * Nesting is counted; queued notifications are delivered by the outermost + * leave. + * + * The outermost region must be opened by the public entry point itself + * (ares_send_dnsrec(), ares_query_dnsrec(), ares_search(), ares_getaddrinfo(), + * ares_process_fds(), ares_set_servers_*(), ...), with the channel lock + * held, around everything it does. Internal helpers such as + * ares_send_nolock() open nested regions of their own, but their callers + * often keep using lookup state afterwards -- a host or search query, a + * query id out-pointer -- that a callback reentering the library may end + * and free. If such a helper were the outermost region, its leave would + * run the callbacks while that state is still in use. + * + * \param[in] channel Initialized ares channel. + */ +void ares_deferred_cb_enter(ares_channel_t *channel); + +/*! Leave such a region. The outermost leave delivers the queues, at which + * point the library holds no connection a reentrant call could free. + * + * \param[in] channel Initialized ares channel. + */ +void ares_deferred_cb_leave(ares_channel_t *channel); + +/*! Deliver every server-state notification the channel has queued. Called + * only from the shared deferred-callback flush. + * + * \param[in] channel Initialized ares channel. + */ +void ares_server_state_dispatch(ares_channel_t *channel); + #define ARES_CONFIG_CHECK(x) \ (x && x->lookups && ares_slist_len(x->servers) > 0 && x->timeout > 0 && \ x->tries > 0) @@ -594,14 +721,47 @@ unsigned int max_ttl, ares_qcache_t **cache_out); void ares_qcache_flush(ares_qcache_t *cache); -ares_status_t ares_qcache_insert(ares_channel_t *channel, - const ares_timeval_t *now, - const ares_query_t *query, - ares_dns_record_t *dnsrec); -ares_status_t ares_qcache_fetch(ares_channel_t *channel, - const ares_timeval_t *now, - const ares_dns_record_t *dnsrec, - const ares_dns_record_t **dnsrec_resp); +/*! Derive the query cache key for a request. Caller owns the result. + * + * \param[in] dnsrec Request to derive the key from. + * \return allocated key, or NULL on failure. + */ +char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec); + +/*! Insert a response into the query cache. + * + * \param[in] channel Initialized channel object. + * \param[in] now Current timestamp. + * \param[in] query Query the response answers. Its cache key, derived + * when the query was created, becomes the entry key; a + * NULL key means caching is switched off. + * \param[in] dnsrec Response to cache. Duplicated internally; the caller + * retains ownership. + * \return ARES_SUCCESS if cached, ARES_ENOTIMP if caching is off or the + * response is not cacheable, otherwise another status. + */ +ares_status_t ares_qcache_insert(ares_channel_t *channel, + const ares_timeval_t *now, + const ares_query_t *query, + const ares_dns_record_t *dnsrec); +/*! Fetch a cached answer for a request. + * + * \param[in] channel Initialized channel object. + * \param[in] now Current timestamp. + * \param[in] key Cache key for the request, as produced by + * ares_qcache_calc_key(). Owned by the caller. + * \param[out] dnsrec_resp On ARES_SUCCESS, the cached answer, owned by the + * caller and destroyed with + * ares_dns_record_destroy(). Set to NULL on any + * other status, except ARES_EFORMERR, which is + * returned by the argument check before this is + * touched. + * \return ARES_SUCCESS on a hit, ARES_ENOTFOUND on a miss, ARES_EFORMERR on + * invalid arguments. + */ +ares_status_t ares_qcache_fetch(ares_channel_t *channel, + const ares_timeval_t *now, const char *key, + ares_dns_record_t **dnsrec_resp); void ares_metrics_record(const ares_query_t *query, ares_server_t *server, ares_status_t status, const ares_dns_record_t *dnsrec); @@ -627,7 +787,6 @@ void ares_event_thread_destroy(ares_channel_t *channel); ares_status_t ares_event_thread_init(ares_channel_t *channel); - #ifdef _WIN32 # define HOSTENT_ADDRTYPE_TYPE short # define HOSTENT_LENGTH_TYPE short diff -Nru c-ares-1.34.5/src/lib/ares_process.c c-ares-1.34.9/src/lib/ares_process.c --- c-ares-1.34.5/src/lib/ares_process.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_process.c 2026-10-20 02:31:51.000000000 +0000 @@ -45,7 +45,6 @@ #include -static void timeadd(ares_timeval_t *now, size_t millisecs); static ares_status_t process_write(ares_channel_t *channel, ares_socket_t write_fd); static ares_status_t process_read(ares_channel_t *channel, @@ -64,7 +63,12 @@ const ares_dns_record_t *arec); static void end_query(ares_channel_t *channel, ares_server_t *server, ares_query_t *query, ares_status_t status, - const ares_dns_record_t *dnsrec); + ares_dns_record_t *dnsrec, + ares_array_t **requeue); +static ares_status_t ares_send_query_int(ares_server_t *requested_server, + ares_query_t *query, + const ares_timeval_t *now, + ares_array_t **requeue); static void ares_query_remove_from_conn(ares_query_t *query) { @@ -76,14 +80,32 @@ query->conn = NULL; } +/*! A queued server-state notification. Holds the formatted address rather + * than the server, so it stays valid however the server list changes. */ +typedef struct { + char *server_string; + ares_bool_t success; + int flags; +} ares_server_state_msg_t; + +static void ares_server_state_msg_free(void *arg) +{ + ares_server_state_msg_t *msg = arg; + if (msg == NULL) { + return; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + ares_free(msg->server_string); +} + /* Invoke the server state callback after a success or failure */ static void invoke_server_state_cb(const ares_server_t *server, ares_bool_t success, int flags) { - const ares_channel_t *channel = server->channel; - ares_buf_t *buf; - ares_status_t status; - char *server_string; + ares_channel_t *channel = server->channel; + ares_buf_t *buf; + ares_status_t status; + char *server_string; + ares_server_state_msg_t msg; if (channel->server_state_cb == NULL) { return; @@ -106,9 +128,57 @@ return; /* LCOV_EXCL_LINE: OutOfMemory */ } - channel->server_state_cb(server_string, success, flags, - channel->server_state_cb_data); - ares_free(server_string); + /* Queue rather than dispatch. This runs in the middle of processing, and + * every caller keeps using its query / connection / server afterwards. The + * callback is application code that may reentrantly call ares_cancel(), + * which frees those objects -- so dispatch once the library is no longer + * holding them. Same philosophy as the deferred query-callback dispatch. + * + * Only the formatted address is captured, never the ares_server_t, so a + * queued entry cannot outlive anything it points at. */ + if (channel->server_state_queue == NULL) { + channel->server_state_queue = ares_array_create( + sizeof(ares_server_state_msg_t), ares_server_state_msg_free); + if (channel->server_state_queue == NULL) { + /* LCOV_EXCL_START: OutOfMemory */ + ares_free(server_string); + return; + /* LCOV_EXCL_STOP */ + } + } + + msg.server_string = server_string; + msg.success = success; + msg.flags = flags; + + if (ares_array_insertdata_last(channel->server_state_queue, &msg) != + ARES_SUCCESS) { + ares_free(server_string); /* LCOV_EXCL_LINE: OutOfMemory */ + } +} + +/*! Deliver every server-state notification the channel has queued. Loops + * rather than snapshotting: a callback is allowed to re-enter the library and + * queue more, and those belong to this same delivery point. */ +void ares_server_state_dispatch(ares_channel_t *channel) +{ + while (ares_array_len(channel->server_state_queue) > 0) { + ares_server_state_msg_t msg; + + if (ares_array_claim_at(&msg, sizeof(msg), channel->server_state_queue, + 0) != ARES_SUCCESS) { + break; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + + if (channel->server_state_cb != NULL) { + channel->server_state_cb(msg.server_string, msg.success, msg.flags, + channel->server_state_cb_data); + } + ares_free(msg.server_string); + } + + ares_array_destroy(channel->server_state_queue); + channel->server_state_queue = NULL; } static void server_increment_failures(ares_server_t *server, @@ -119,17 +189,28 @@ ares_timeval_t next_retry_time; node = ares_slist_node_find(channel->servers, server); - if (node == NULL) { + /* Matches by sort key, not identity, so a removed server can match the live + * one that replaced it at the same configuration index. */ + if (node == NULL || ares_slist_node_val(node) != server) { return; /* LCOV_EXCL_LINE: DefensiveCoding */ } - server->consec_failures++; - ares_slist_node_reinsert(node); + /* Cap the failure count. Only the relative order matters for server + * selection, and an uncapped count would require an unbounded number of + * failures on other servers before a server that failed during an + * extended outage could be selected again. */ + if (server->consec_failures < SERVER_CONSEC_FAILURES_CAP) { + server->consec_failures++; + } + /* Must update the retry time before reinserting since the sort uses it to + * order servers with the same failure count */ ares_tvnow(&next_retry_time); - timeadd(&next_retry_time, channel->server_retry_delay); + ares_timeval_add(&next_retry_time, channel->server_retry_delay); server->next_retry_time = next_retry_time; + ares_slist_node_reinsert(node); + invoke_server_state_cb(server, ARES_FALSE, used_tcp == ARES_TRUE ? ARES_SERV_STATE_TCP : ARES_SERV_STATE_UDP); @@ -141,18 +222,21 @@ const ares_channel_t *channel = server->channel; node = ares_slist_node_find(channel->servers, server); - if (node == NULL) { + /* Matches by sort key, not identity, so a removed server can match the live + * one that replaced it at the same configuration index. */ + if (node == NULL || ares_slist_node_val(node) != server) { return; /* LCOV_EXCL_LINE: DefensiveCoding */ } if (server->consec_failures > 0) { server->consec_failures = 0; + /* Must update the retry time before reinserting since the sort uses it + * to order servers with the same failure count */ + server->next_retry_time.sec = 0; + server->next_retry_time.usec = 0; ares_slist_node_reinsert(node); } - server->next_retry_time.sec = 0; - server->next_retry_time.usec = 0; - invoke_server_state_cb(server, ARES_TRUE, used_tcp == ARES_TRUE ? ARES_SERV_STATE_TCP : ARES_SERV_STATE_UDP); @@ -177,18 +261,6 @@ : ARES_FALSE; } -/* add the specific number of milliseconds to the time in the first argument */ -static void timeadd(ares_timeval_t *now, size_t millisecs) -{ - now->sec += (ares_int64_t)millisecs / 1000; - now->usec += (unsigned int)((millisecs % 1000) * 1000); - - if (now->usec >= 1000000) { - now->sec += now->usec / 1000000; - now->usec %= 1000000; - } -} - static ares_status_t ares_process_fds_nolock(ares_channel_t *channel, const ares_fd_events_t *events, size_t nevents, unsigned int flags) @@ -201,6 +273,8 @@ return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ } + ares_deferred_cb_enter(channel); + ares_tvnow(&now); /* Process write events */ @@ -230,14 +304,19 @@ } if (!(flags & ARES_PROCESS_FLAG_SKIP_NON_FD)) { - ares_check_cleanup_conns(channel); status = process_timeouts(channel, &now); if (status == ARES_ENOMEM) { goto done; } + + /* Cleanup should be done after processing timeouts as it may invalidate + * connections */ + ares_check_cleanup_conns(channel); } done: + ares_deferred_cb_leave(channel); + if (status == ARES_ENOMEM) { return ARES_ENOMEM; } @@ -415,38 +494,69 @@ return; } + /* A failed flush closes the connection and fails its queries over, which + * notifies. Deliver once we are done walking the servers, and keep + * anything unlinked meanwhile readable until then. */ + ares_deferred_cb_enter(channel); + /* Set as untriggerd before calling into ares_conn_flush(), this is * because its possible ares_conn_flush() might cause additional data to * be enqueued if there is some form of exception so it will need to recurse. */ channel->notify_pending_write = ARES_FALSE; - for (node = ares_slist_node_first(channel->servers); node != NULL; - node = ares_slist_node_next(node)) { - ares_server_t *server = ares_slist_node_val(node); - ares_conn_t *conn = server->tcp_conn; + node = ares_slist_node_first(channel->servers); + while (node != NULL) { + ares_server_t *server = ares_slist_node_val(node); + ares_conn_t *conn = server->tcp_conn; + size_t generation = channel->servers_generation; ares_status_t status; if (conn == NULL) { + node = ares_slist_node_next(node); continue; } - /* Enqueue any pending data if there is any */ + /* Enqueue any pending data if there is any. The flush runs the + * application's send function, which may have reconfigured the servers + * and removed this one; its connection is then already closed, which + * handle_conn_error() ignores. */ status = ares_conn_flush(conn); if (status != ARES_SUCCESS) { handle_conn_error(conn, ARES_TRUE, status); } + + /* Tearing down a connection runs the callbacks of the queries it ends, + * and those may reconfigure the servers too, destroying the ones the new + * configuration omits along with their list nodes. A failure also moves + * this server further down the list. So when either may have happened, + * start over from the head rather than step from this node; flushing a + * connection again is harmless. */ + if (status != ARES_SUCCESS || channel->servers_generation != generation) { + node = ares_slist_node_first(channel->servers); + continue; + } + + node = ares_slist_node_next(node); } + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); } -static ares_status_t read_conn_packets(ares_conn_t *conn) +static ares_status_t read_conn_packets(ares_conn_t *conn, + ares_bool_t *conn_error) { ares_bool_t read_again; ares_conn_err_t err; const ares_channel_t *channel = conn->server->channel; + if (conn_error == NULL) { + return ARES_EFORMERR; + } + + *conn_error = ARES_FALSE; + do { size_t count; size_t len = 65535; @@ -494,6 +604,18 @@ /* If UDP, overwrite length */ if (!(conn->flags & ARES_CONN_FLAG_TCP)) { + /* The read buffer is grown in powers of two, so a single recvfrom() can + * return more than the 2-byte length prefix is able to represent. A + * datagram that large can't be a valid DNS message, and writing the + * truncated (unsigned short)count would desync the framing of everything + * buffered after it, so discard it. Standard UDP can't actually deliver + * a payload this large (max is 65507 IPv4 / 65527 IPv6); the only vector + * is IPv6 jumbograms (RFC 2675), which DNS never uses. This is purely + * defense-in-depth. */ + if (count > 65535) { + ares_buf_set_length(conn->in_buf, start_len); + break; + } len = ares_buf_len(conn->in_buf); ares_buf_set_length(conn->in_buf, start_len); ares_buf_append_be16(conn->in_buf, (unsigned short)count); @@ -504,23 +626,43 @@ } while (read_again); if (err != ARES_CONN_ERR_SUCCESS && err != ARES_CONN_ERR_WOULDBLOCK) { - handle_conn_error(conn, ARES_TRUE, ARES_ECONNREFUSED); - return ARES_ECONNREFUSED; + /* If there is no packet data buffered, preserve the historical + * immediate connection-failure behavior so retries happen promptly. + * Only defer if there is buffered data to parse first. */ + if (ares_buf_len(conn->in_buf) == 0) { + handle_conn_error(conn, ARES_TRUE, ARES_ECONNREFUSED); + return ARES_ECONNREFUSED; + } + + *conn_error = ARES_TRUE; } return ARES_SUCCESS; } +typedef enum { + REQUEUE_REQUEUE = 1, + REQUEUE_ENDQUERY = 2 +} requeue_type_t; + /* Simple data structure to store a query that needs to be requeued with * optional server */ typedef struct { - unsigned short qid; - ares_server_t *server; /* optional */ + requeue_type_t type; /* type of entry, requeue or endquery */ + unsigned short qid; /* query id */ + size_t serial; /* identifies the query the qid meant when + * this entry was appended */ + ares_server_t *server; /* requeue only: optional */ + ares_status_t status; /* endquery only */ + ares_dns_record_t *dnsrec; /* endquery only: optional */ } ares_requeue_t; -static ares_status_t ares_append_requeue(ares_array_t **requeue, - ares_query_t *query, - ares_server_t *server) +static ares_status_t ares_append_requeue_int(ares_array_t **requeue, + requeue_type_t type, + ares_query_t *query, + ares_server_t *server, + ares_status_t status, + ares_dns_record_t *dnsrec) { ares_requeue_t entry; @@ -533,16 +675,181 @@ ares_query_remove_from_conn(query); + entry.type = type; entry.qid = query->qid; + entry.serial = query->serial; entry.server = server; + entry.status = status; + entry.dnsrec = dnsrec; return ares_array_insertdata_last(*requeue, &entry); } +static ares_status_t ares_append_requeue(ares_array_t **requeue, + ares_query_t *query, + ares_server_t *server) +{ + return ares_append_requeue_int(requeue, REQUEUE_REQUEUE, query, server, 0, + NULL); +} + +static ares_status_t ares_append_endqueue(ares_array_t **requeue, + ares_query_t *query, + ares_status_t status, + ares_dns_record_t *dnsrec) +{ + return ares_append_requeue_int(requeue, REQUEUE_ENDQUERY, query, NULL, status, + dnsrec); +} + +/*! Is the query the caller captured still the one holding \a qid? + * + * A query id is a 16-bit wire value reissued as soon as the query holding it + * is detached, so it names a slot rather than a query, and comparing the + * pointer as well does not help: the replacement is often allocated at the + * address the freed one had. The serial is a channel-wide counter that is + * never reused. */ +static ares_bool_t ares_query_is_live(const ares_channel_t *channel, + unsigned short qid, size_t serial) +{ + const ares_query_t *query = + ares_htable_szvp_get_direct(channel->queries_by_qid, qid); + + if (query == NULL || query->serial != serial) { + return ARES_FALSE; + } + + return ARES_TRUE; +} + +/*! Is this exact server object still in the channel's server list? + * + * Compares pointers only and never dereferences \a server. Deferred + * reclamation keeps an unlinked server readable for the rest of the call + * that captured it, so reading through it is no longer a use-after-free -- + * but it has been removed from the configuration and must not be acted on, + * which is what this reports. A channel has a handful of servers, so the + * linear scan does not matter. */ +ares_bool_t ares_server_in_channel(const ares_channel_t *channel, + const ares_server_t *server) +{ + ares_slist_node_t *node; + + if (server == NULL) { + return ARES_FALSE; + } + + for (node = ares_slist_node_first(channel->servers); node != NULL; + node = ares_slist_node_next(node)) { + if (ares_slist_node_val(node) == server) { + return ARES_TRUE; + } + } + + return ARES_FALSE; +} + +/* Drain the deferred requeue/endqueue list iteratively. All flush sites + * (read_answers(), process_timeouts(), and ares_send_query()) funnel through + * here so that: + * 1. Retries are re-dispatched by appending to this same list and looping, + * rather than recursing ares_requeue_query() -> ares_send_query() until + * the stack is exhausted (issue #1043). + * 2. A query is fully detached from all lookup lists before its callback is + * invoked, so a reentrant ares_cancel() from within that callback cannot + * find and free the same query, which would otherwise double-free it + * (CVE-2026-33630 / GHSA-6wfj-rwm7-3542). + * + * On return the list has been fully processed, an empty-queue notification has + * been sent if appropriate, and *requeue has been destroyed and set to NULL. */ +static ares_status_t ares_flush_requeue(ares_channel_t *channel, + const ares_timeval_t *now, + ares_array_t **requeue) +{ + ares_status_t status = ARES_SUCCESS; + + if (requeue == NULL) { + return status; + } + + while (*requeue != NULL && ares_array_len(*requeue) > 0) { + ares_query_t *query; + ares_requeue_t entry; + ares_status_t internal_status; + + internal_status = ares_array_claim_at(&entry, sizeof(entry), *requeue, 0); + if (internal_status != ARES_SUCCESS) { + break; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + + query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid); + + /* A query id is a 16-bit wire value and generate_unique_qid() only avoids + * ids that are live when it runs. Within a single drain, a callback can + * cancel a query whose entry is still in this array -- releasing its id -- + * and start a new lookup that is handed the same id. The lookup above + * would then resolve this entry to a completely different query: an + * endquery entry would deliver one lookup's answer to another and free it, + * a requeue entry would put an in-flight query on the wire twice. The + * serial is a channel-wide counter that is never reused, so it tells the + * two apart. */ + if (query != NULL && query->serial != entry.serial) { + query = NULL; + } + + if (entry.type == REQUEUE_REQUEUE) { + /* Query disappeared (e.g. a prior callback in this drain cancelled it) */ + if (query == NULL) { + continue; + } + /* entry.server is a raw pointer captured before any callback in this + * drain ran. A callback may legally re-enter the public API -- the + * channel lock is recursive -- and ares_set_servers*() destroys servers + * that are absent from the new configuration, so it may already be + * freed. The query above is revalidated by qid for exactly this + * reason; do the same for the server, falling back to ordinary + * selection when it has gone. */ + if (!ares_server_in_channel(channel, entry.server)) { + entry.server = NULL; + } + + /* Re-dispatch via the internal entrypoint so any further requeues are + * appended back onto this same list and drained by the loop above, + * rather than recursing. */ + internal_status = ares_send_query_int(entry.server, query, now, requeue); + /* We only care about ARES_ENOMEM */ + if (internal_status == ARES_ENOMEM) { + status = ARES_ENOMEM; + } + } else { /* REQUEUE_ENDQUERY */ + if (query != NULL) { + /* Detach the query from all lookup lists BEFORE invoking the callback. + * Otherwise a reentrant ares_cancel() from within the callback would + * find this query still linked in all_queries/queries_by_qid, free it, + * and the ares_free_query() below would then double-free it. */ + ares_detach_query(query); + query->callback(query->arg, entry.status, query->timeouts, + entry.dnsrec); + ares_free_query(query); + } + ares_dns_record_destroy(entry.dnsrec); + } + } + + /* Don't forget to send notification if queue emptied */ + if (*requeue != NULL) { + ares_queue_notify_empty(channel); + } + ares_array_destroy(*requeue); + *requeue = NULL; + + return status; +} + static ares_status_t read_answers(ares_conn_t *conn, const ares_timeval_t *now) { ares_status_t status; - ares_channel_t *channel = conn->server->channel; - ares_array_t *requeue = NULL; + ares_channel_t *channel = conn->server->channel; + ares_array_t *requeue = NULL; /* Process all queued answers */ while (1) { @@ -590,31 +897,11 @@ } cleanup: - - /* Flush requeue */ - while (ares_array_len(requeue) > 0) { - ares_query_t *query; - ares_requeue_t entry; - ares_status_t internal_status; - - internal_status = ares_array_claim_at(&entry, sizeof(entry), requeue, 0); - if (internal_status != ARES_SUCCESS) { - break; - } - - /* Query disappeared */ - query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid); - if (query == NULL) { - continue; - } - - internal_status = ares_send_query(entry.server, query, now); - /* We only care about ARES_ENOMEM */ - if (internal_status == ARES_ENOMEM) { - status = ARES_ENOMEM; - } + /* Flush requeue - re-dispatch retries and invoke deferred callbacks + * iteratively and safely */ + if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) { + status = ARES_ENOMEM; } - ares_array_destroy(requeue); return status; } @@ -624,24 +911,39 @@ const ares_timeval_t *now) { ares_conn_t *conn = ares_conn_from_fd(channel, read_fd); + ares_bool_t conn_error; ares_status_t status; if (conn == NULL) { return ARES_SUCCESS; } - /* TODO: There might be a potential issue here where there was a read that - * read some data, then looped and read again and got a disconnect. - * Right now, that would cause a resend instead of processing the data - * we have. This is fairly unlikely to occur due to only looping if - * a full buffer of 65535 bytes was read. */ - status = read_conn_packets(conn); + status = read_conn_packets(conn, &conn_error); + + if (status != ARES_SUCCESS) { + return status; + } + + /* A later call to the receive function may have reconfigured the servers, + * tearing this connection down after earlier calls buffered answers on it. + * Its queries have moved to the new configuration, so drop the answers. */ + if (conn->flags & ARES_CONN_FLAG_DEAD) { + return ARES_SUCCESS; + } + status = read_answers(conn, now); if (status != ARES_SUCCESS) { return status; } - return read_answers(conn, now); + if (conn_error) { + conn = ares_conn_from_fd(channel, read_fd); + if (conn != NULL) { + handle_conn_error(conn, ARES_TRUE, ARES_ECONNREFUSED); + } + } + + return ARES_SUCCESS; } /* If any queries have timed out, note the timeout and move them on. */ @@ -649,7 +951,8 @@ const ares_timeval_t *now) { ares_slist_node_t *node; - ares_status_t status = ARES_SUCCESS; + ares_status_t status = ARES_SUCCESS; + ares_array_t *requeue = NULL; /* Just keep popping off the first as this list will re-sort as things come * and go. We don't want to try to rely on 'next' as some operation might @@ -667,14 +970,26 @@ query->timeouts++; conn = query->conn; + /* Retire this connection for NEW queries. A timeout suggests packets are + * being dropped on it, so route new queries to a fresh source port while + * the in-flight queries here drain (it is cleaned up once idle). This is + * per-connection so a transient failure doesn't stop reuse of healthy + * connections to the same server. */ + conn->flags |= ARES_CONN_FLAG_NONEW; server_increment_failures(conn->server, query->using_tcp); - status = ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, - NULL); + status = + ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, &requeue); if (status == ARES_ENOMEM) { goto done; } } done: + /* Flush requeue - re-dispatch retries and invoke deferred callbacks + * iteratively and safely */ + if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) { + status = ARES_ENOMEM; + } + if (status == ARES_ENOMEM) { return ARES_ENOMEM; } @@ -802,6 +1117,24 @@ goto cleanup; } + /* RFC 5452 section 9.1: only accept a response that arrives on the + * connection the query was sent on, and so from the server it was sent to. + * For UDP, ares_conn_read() also drops a datagram whose source address or + * port is not the server's; matching the port is hardening beyond RFC 5452 + * section 9.1, which only requires the address to match. Otherwise any + * server we hold a connection to could answer, and get cached, a query that + * was only ever sent to some other server, and the cookie check below would + * run against the wrong server's cookie state. Treat it like an unknown + * query id, before anything acts on it (cookies, server health, the cache). + * Our caller has already consumed the message from the connection's buffer, + * so TCP framing stays in sync. A query not attached to any connection + * (query->conn == NULL) never matches. This also drops a late answer from + * a server tried before a retry. */ + if (query->conn != conn) { + status = ARES_SUCCESS; + goto cleanup; + } + /* Validate DNS cookie in response. This function may need to requeue the * query. */ if (ares_cookie_validate(query, rdnsrec, conn, now, requeue) @@ -824,7 +1157,7 @@ if (issue_might_be_edns(query->query, rdnsrec)) { status = rewrite_without_edns(query); if (status != ARES_SUCCESS) { - end_query(channel, server, query, status, NULL); + end_query(channel, server, query, status, NULL, requeue); goto cleanup; } @@ -869,7 +1202,9 @@ } server_increment_failures(server, query->using_tcp); - status = ares_requeue_query(query, now, status, ARES_TRUE, rdnsrec, requeue); + status = ares_requeue_query(query, now, status, ARES_TRUE, rdnsrec, + requeue); + rdnsrec = NULL; /* Free'd by ares_requeue_query() */ if (status != ARES_ENOMEM) { /* Should any of these cause a connection termination? @@ -882,12 +1217,11 @@ /* If cache insertion was successful, it took ownership. We ignore * other cache insertion failures. */ - if (ares_qcache_insert(channel, now, query, rdnsrec) == ARES_SUCCESS) { - is_cached = ARES_TRUE; - } + ares_qcache_insert(channel, now, query, rdnsrec); server_set_good(server, query->using_tcp); - end_query(channel, server, query, ARES_SUCCESS, rdnsrec); + end_query(channel, server, query, ARES_SUCCESS, rdnsrec, requeue); + rdnsrec = NULL; /* Free'd by the requeue */ status = ARES_SUCCESS; @@ -905,6 +1239,15 @@ { ares_server_t *server = conn->server; + /* Already torn down, most likely by a reconfiguration from application code + * the failing call ran, such as the send function. Its queries have been + * requeued, and its server did not fail -- it was removed, or the + * connection was closed deliberately -- so there is nothing to record and + * nothing left to close. */ + if (conn->flags & ARES_CONN_FLAG_DEAD) { + return; + } + /* Increment failures first before requeue so it is unlikely to requeue * to the same server */ if (critical_failure) { @@ -922,11 +1265,16 @@ ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now, ares_status_t status, ares_bool_t inc_try_count, - const ares_dns_record_t *dnsrec, + ares_dns_record_t *dnsrec, ares_array_t **requeue) { ares_channel_t *channel = query->channel; size_t max_tries = ares_slist_len(channel->servers) * channel->tries; + ares_server_t *server = NULL; + + if (query->conn != NULL) { + server = query->conn->server; + } ares_query_remove_from_conn(query); @@ -939,6 +1287,7 @@ } if (query->try_count < max_tries && !query->no_retries) { + ares_dns_record_destroy(dnsrec); if (requeue != NULL) { return ares_append_requeue(requeue, query, NULL); } @@ -950,7 +1299,7 @@ query->error_status = ARES_ETIMEOUT; } - end_query(channel, NULL, query, query->error_status, dnsrec); + end_query(channel, server, query, query->error_status, dnsrec, requeue); return ARES_ETIMEOUT; } @@ -1053,22 +1402,23 @@ } /* Select the first server with failures to retry that has passed the retry - * timeout and doesn't already have a pending probe */ + * timeout and doesn't already have a pending probe. Skip the server the + * triggering query was just sent to since that query is already exercising + * it. */ ares_tvnow(&now); for (node = ares_slist_node_first(channel->servers); node != NULL; node = ares_slist_node_next(node)) { ares_server_t *node_val = ares_slist_node_val(node); - if (node_val != NULL && node_val->consec_failures > 0 && - !node_val->probe_pending && + if (node_val != NULL && node_val != server && + node_val->consec_failures > 0 && !node_val->probe_pending && ares_timedout(&now, &node_val->next_retry_time)) { probe_server = node_val; break; } } - /* Either nothing to probe or the query was enqueud to the same server - * we were going to probe. Do nothing. */ - if (probe_server == NULL || server == probe_server) { + /* Nothing to probe */ + if (probe_server == NULL) { return; } @@ -1099,7 +1449,12 @@ * retry from the last retry */ rounds = (query->try_count / num_servers); if (rounds > 0) { - timeplus <<= rounds; + if (rounds >= sizeof(timeplus) * CHAR_BIT || + timeplus > (SIZE_MAX >> rounds)) { + timeplus = SIZE_MAX; + } else { + timeplus <<= rounds; + } } if (channel->maxtimeout && timeplus > channel->maxtimeout) { @@ -1156,6 +1511,16 @@ return NULL; } + /* Don't hand new queries to a connection that has been retired (e.g. it saw + * a timeout). It keeps servicing its in-flight queries and is cleaned up + * once idle. Note this is a per-connection check: a server-wide failure + * counter must not be used here or a single transient failure would evict + * every (including healthy) connection to the server and spawn a new socket + * per query. */ + if (conn->flags & ARES_CONN_FLAG_NONEW) { + return NULL; + } + /* Used too many times */ if (channel->udp_max_queries > 0 && conn->total_queries >= channel->udp_max_queries) { @@ -1199,6 +1564,14 @@ conn->flags & ARES_CONN_FLAG_TCP) { channel->notify_pending_write = ARES_TRUE; channel->notify_pending_write_cb(channel->notify_pending_write_cb_data); + + /* The callback is application code and may have reconfigured the + * servers, closing this connection with the query already buffered on + * it. Report it as a connection failure so the query is requeued rather + * than attached to the closed connection. */ + if (conn->flags & ARES_CONN_FLAG_DEAD) { + return ARES_ECONNREFUSED; + } return ARES_SUCCESS; } @@ -1207,16 +1580,54 @@ return ares_conn_flush(conn); } +/* Public entrypoint. Establishes a requeue list and drives + * ares_send_query_int() plus any retries/deferred callbacks it produces + * iteratively, so a chain of retryable failures can never recurse until the + * stack is exhausted (#1043). */ ares_status_t ares_send_query(ares_server_t *requested_server, ares_query_t *query, const ares_timeval_t *now) { ares_channel_t *channel = query->channel; + ares_array_t *requeue = NULL; + unsigned short qid = query->qid; + size_t serial = query->serial; + ares_status_t status; + + status = ares_send_query_int(requested_server, query, now, &requeue); + + /* Drain any retries/deferred callbacks this send produced. + * ares_flush_requeue() always fully processes and destroys the list (even on + * ENOMEM), and sends the empty-queue notification if needed. */ + if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) { + status = ARES_ENOMEM; + } + + /* A retry may have been deferred (returning ARES_SUCCESS from the append) + * and then terminally failed while draining, in which case the query has + * been freed. Do not dereference 'query' here. If it is no longer tracked + * it ended, so don't report success to the caller (which would, e.g., cause + * ares_send_nolock() to write to a now-freed *qid). */ + if (status == ARES_SUCCESS && !ares_query_is_live(channel, qid, serial)) { + status = ARES_ETIMEOUT; + } + + return status; +} + +static ares_status_t ares_send_query_int(ares_server_t *requested_server, + ares_query_t *query, + const ares_timeval_t *now, + ares_array_t **requeue) +{ + ares_channel_t *channel = query->channel; ares_server_t *server; ares_conn_t *conn; size_t timeplus; ares_status_t status; ares_bool_t probe_downed_server = ARES_TRUE; - + /* Captured before any application callback below, which may free the query */ + unsigned short qid = query->qid; + size_t serial = query->serial; /* Choose the server to send the query to */ if (requested_server != NULL) { @@ -1232,20 +1643,48 @@ } if (server == NULL) { - end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL); + end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, requeue); return ARES_ENOSERVER; } - /* If a query is directed to a specific query, or the server chosen has - * failures, or the query is being retried, don't probe for downed servers */ - if (requested_server != NULL || server->consec_failures > 0 || - query->try_count != 0) { + /* If a query is directed to a specific server, or the query is being + * retried, don't probe for downed servers. Note that the chosen server + * having failures itself does NOT disable probing: when every server has + * failures (e.g. during an extended outage), probes are the only way a + * recovered server that sorts behind the current best can be noticed. */ + if (requested_server != NULL || query->try_count != 0) { probe_downed_server = ARES_FALSE; } conn = ares_fetch_connection(channel, server, query); if (conn == NULL) { status = ares_open_connection(&conn, channel, server, query->using_tcp); + + /* Opening a connection runs application code -- the socket-open function + * from ares_set_socket_functions_ex(), and the ares_set_socket_callback() + * and ares_set_socket_configure_callback() callbacks -- any of which may + * reentrantly call ares_cancel() and free this query. Re-check liveness + * by the qid captured at entry rather than dereferencing a possibly-freed + * query. */ + if (!ares_query_is_live(channel, qid, serial)) { + return ARES_ECANCELLED; + } + + /* Those same callbacks may have reconfigured the servers, which frees the + * ones the new configuration omits. 'server' was captured before the + * open; everything below reads it, including server_increment_failures() + * on the failure paths of the switch. + * + * Requeue rather than return: every exit from this function must either + * enqueue the query or end it, and the query is still live here -- it has + * not been written, is not attached to a connection, and on a first send + * has no timeout node, so returning would strand it forever. + * query->conn is NULL, so this does not read through the freed server. */ + if (!ares_server_in_channel(channel, server)) { + return ares_requeue_query(query, now, ARES_ECONNREFUSED, ARES_TRUE, NULL, + requeue); + } + switch (status) { /* Good result, continue on */ case ARES_SUCCESS: @@ -1256,17 +1695,30 @@ case ARES_ECONNREFUSED: case ARES_EBADFAMILY: server_increment_failures(server, query->using_tcp); - return ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); + return ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); /* Anything else is not retryable, likely ENOMEM */ default: - end_query(channel, server, query, status, NULL); + end_query(channel, server, query, status, NULL, requeue); return status; } } /* Write the query */ status = ares_conn_query_write(conn, query, now); + + /* Writing runs application code too: ares_conn_query_write() reaches the + * send function installed by ares_set_socket_functions_ex(), which may + * reentrantly call ares_cancel() with the same hazard as above. */ + if (!ares_query_is_live(channel, qid, serial)) { + return ARES_ECANCELLED; + } + + /* Application code that tore this connection down during the write -- the + * send function, or the pending-write callback when the write is deferred, + * reconfiguring the servers, say -- surfaces here as ARES_ECONNREFUSED, + * even if the send itself reported success or never happened, so the query + * is requeued below rather than attached to the closed connection. */ switch (status) { /* Good result, continue on */ case ARES_SUCCESS: @@ -1274,7 +1726,7 @@ case ARES_ENOMEM: /* Not retryable */ - end_query(channel, server, query, status, NULL); + end_query(channel, server, query, status, NULL, requeue); return status; /* These conditions are retryable as they are server-specific @@ -1282,7 +1734,18 @@ case ARES_ECONNREFUSED: case ARES_EBADFAMILY: handle_conn_error(conn, ARES_TRUE, status); - status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); + /* handle_conn_error() -> ares_close_connection() -> + * ares_requeue_queries() walks conn->queries_to_conn and may terminally + * end other queries on this connection. If one of their callbacks + * reentrantly calls ares_cancel(), this query is freed too -- it is still + * linked in all_queries, since it only joins conn->queries_to_conn on the + * success path below. Re-check liveness by the qid captured at entry + * rather than dereferencing a possibly-freed query. Mirrors the guard in + * ares_send_query(). */ + if (!ares_query_is_live(channel, qid, serial)) { + return ARES_ECONNREFUSED; + } + status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); if (status == ARES_ETIMEOUT) { status = ARES_ECONNREFUSED; } @@ -1290,7 +1753,7 @@ default: server_increment_failures(server, query->using_tcp); - status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); + status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); return status; } @@ -1301,12 +1764,12 @@ ares_slist_node_destroy(query->node_queries_by_timeout); query->ts = *now; query->timeout = *now; - timeadd(&query->timeout, timeplus); + ares_timeval_add(&query->timeout, timeplus); query->node_queries_by_timeout = ares_slist_insert(channel->queries_by_timeout, query); if (!query->node_queries_by_timeout) { /* LCOV_EXCL_START: OutOfMemory */ - end_query(channel, server, query, ARES_ENOMEM, NULL); + end_query(channel, server, query, ARES_ENOMEM, NULL, requeue); return ARES_ENOMEM; /* LCOV_EXCL_STOP */ } @@ -1319,7 +1782,7 @@ if (query->node_queries_to_conn == NULL) { /* LCOV_EXCL_START: OutOfMemory */ - end_query(channel, server, query, ARES_ENOMEM, NULL); + end_query(channel, server, query, ARES_ENOMEM, NULL, requeue); return ARES_ENOMEM; /* LCOV_EXCL_STOP */ } @@ -1333,6 +1796,10 @@ ares_probe_failed_server(channel, server, query); } + if (channel->query_enqueue_cb) { + channel->query_enqueue_cb(channel->query_enqueue_cb_data); + } + return ARES_SUCCESS; } @@ -1396,18 +1863,25 @@ return rv; } -static void ares_detach_query(ares_query_t *query) +void ares_detach_query(ares_query_t *query) { /* Remove the query from all the lists in which it is linked */ ares_query_remove_from_conn(query); - ares_htable_szvp_remove(query->channel->queries_by_qid, query->qid); + /* Only release the qid slot if it is still ours. A query detached before + * its callback can have its qid reissued to a lookup started from within + * that callback, and the ares_free_query() that follows detaches a second + * time -- which would otherwise evict the new lookup instead. */ + if (ares_htable_szvp_get_direct(query->channel->queries_by_qid, query->qid) == + query) { + ares_htable_szvp_remove(query->channel->queries_by_qid, query->qid); + } ares_llist_node_destroy(query->node_all_queries); query->node_all_queries = NULL; } static void end_query(ares_channel_t *channel, ares_server_t *server, ares_query_t *query, ares_status_t status, - const ares_dns_record_t *dnsrec) + ares_dns_record_t *dnsrec, ares_array_t **requeue) { /* If we were probing for the server to come back online, lets mark it as * no longer being probed */ @@ -1417,6 +1891,22 @@ ares_metrics_record(query, server, status, dnsrec); + /* Delay calling the query callback */ + if (requeue != NULL) { + ares_append_endqueue(requeue, query, status, dnsrec); + return; + } + + /* Detach from all lookup lists before the callback, otherwise a reentrant + * ares_cancel() from within it would find this query still linked, free it, + * and the ares_free_query() below would double-free. Mirrors + * ares_flush_requeue() for every synchronous path that reaches here with a + * NULL requeue: connection teardown via handle_conn_error(), and server + * removal via ares_servers_update() and ares_destroy_servers_state(), both + * of which reach ares_close_sockets(). + * ares_detach_query() may safely run twice on the same query. */ + ares_detach_query(query); + /* Invoke the callback. */ query->callback(query->arg, status, query->timeouts, dnsrec); ares_free_query(query); @@ -1424,7 +1914,8 @@ /* Check and notify if no other queries are enqueued on the channel. This * must come after the callback and freeing the query for 2 reasons. * 1) The callback itself may enqueue a new query - * 2) Technically the current query isn't detached until it is free()'d. + * 2) The current query is detached above, before the callback, so by this + * point it is no longer tracked. */ ares_queue_notify_empty(channel); } @@ -1437,6 +1928,7 @@ query->arg = NULL; /* Deallocate the memory associated with the query */ ares_dns_record_destroy(query->query); + ares_free(query->qcache_key); ares_free(query); } diff -Nru c-ares-1.34.5/src/lib/ares_qcache.c c-ares-1.34.9/src/lib/ares_qcache.c --- c-ares-1.34.5/src/lib/ares_qcache.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_qcache.c 2026-10-20 02:31:51.000000000 +0000 @@ -38,7 +38,7 @@ time_t insert_ts; } ares_qcache_entry_t; -static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec) +char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec) { ares_buf_t *buf = ares_buf_create(); size_t i; @@ -49,7 +49,22 @@ return NULL; /* LCOV_EXCL_LINE: DefensiveCoding */ } - /* Format is OPCODE|FLAGS[|QTYPE1|QCLASS1|QNAME1]... */ + /* Format is + * OPCODE|FLAGS[|EDNS|VER|FLAGS[|ID:HEX]...]|QDCOUNT[|TYPE|CLASS|LEN:QNAME]... + * + * The EDNS section matters: an answer fetched under one set of EDNS options + * is not interchangeable with one fetched under another. A request with no + * OPT record appends nothing; note that EDNS is on by default, so ordinary + * lookups do carry one and their keys did change with this. + * + * Everything a caller does not control is emitted before the questions, so + * the names are always last. A name reaches the key unescaped, so anything + * keyed after it could otherwise be imitated by a name containing that same + * text, giving two different requests one key. The question count is part + * of that: without it, a request with two questions and a request with one + * whose name spells out the second produce identical keys. Each name is + * also length-prefixed, so adjacent names cannot be made to run together + * even when the question counts match. */ status = ares_buf_append_str( buf, ares_dns_opcode_tostr(ares_dns_record_get_opcode(dnsrec))); @@ -63,7 +78,11 @@ } flags = ares_dns_record_get_flags(dnsrec); - /* Only care about RD and CD */ + /* RD, CD and AD. AD is here because RFC 6840 section 5.7 defines it in a + * request as a signal that the requester wants the AD bit evaluated in the + * response, and section 5.8 says a validating resolver should only set AD + * in a response when the request carried DO or AD -- so an answer fetched + * without it can legitimately differ from one fetched with it. */ if (flags & ARES_FLAG_RD) { status = ares_buf_append_str(buf, "rd"); if (status != ARES_SUCCESS) { @@ -76,6 +95,95 @@ goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ } } + if (flags & ARES_FLAG_AD) { + status = ares_buf_append_str(buf, "ad"); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + /* EDNS state. Skipped entirely when the request carries no OPT record. */ + { + const ares_dns_rr_t *opt = ares_dns_get_opt_rr_const(dnsrec); + + if (opt != NULL) { + size_t opt_cnt = ares_dns_rr_get_opt_cnt(opt, ARES_RR_OPT_OPTIONS); + size_t j; + + status = ares_buf_append_str(buf, "|EDNS|"); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + status = ares_buf_append_num_hex( + buf, ares_dns_rr_get_u8(opt, ARES_RR_OPT_VERSION), 0); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + status = ares_buf_append_byte(buf, '|'); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + /* Carries the DO bit, so a DNSSEC-aware request cannot be served an + * answer that was fetched without it. */ + status = ares_buf_append_num_hex( + buf, ares_dns_rr_get_u16(opt, ARES_RR_OPT_FLAGS), 0); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + + /* The advertised UDP payload size is deliberately excluded: it + * constrains how a response is transported, not what it says. A server + * that cannot fit the additional section may omit it without setting + * the TC bit (RFC 2181 section 9), so two sizes can produce answers + * that differ outside the answer section; that is accepted here. */ + for (j = 0; j < opt_cnt; j++) { + const unsigned char *val = NULL; + size_t val_len = 0; + unsigned short optid = + ares_dns_rr_get_opt(opt, ARES_RR_OPT_OPTIONS, j, &val, &val_len); + + /* DNS cookies are per-server transport state and say nothing about + * the content of an answer. A cookie c-ares adds never reaches here + * -- ares_cookie_apply() writes to query->query, after this key has + * been derived -- so this only ever skips one the caller supplied. */ + if (optid == ARES_OPT_PARAM_COOKIE) { + continue; + } + + status = ares_buf_append_byte(buf, '|'); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + status = ares_buf_append_num_hex(buf, optid, 0); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + status = ares_buf_append_byte(buf, ':'); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + if (val != NULL) { + size_t k; + for (k = 0; k < val_len; k++) { + status = ares_buf_append_num_hex(buf, val[k], 2); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + } + } + } + } + + status = ares_buf_append_byte(buf, '|'); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + + status = ares_buf_append_num_dec(buf, ares_dns_record_query_cnt(dnsrec), 0); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } for (i = 0; i < ares_dns_record_query_cnt(dnsrec); i++) { const char *name; @@ -93,7 +201,13 @@ goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ } - status = ares_buf_append_str(buf, ares_dns_rec_type_tostr(qtype)); + /* Numeric, not the mnemonic: ares_dns_rec_type_tostr() returns "UNKNOWN" + * for every type outside its table, and ares_dns_record_query_add() does + * not validate the type, so any 16-bit value reaches here through the + * ordinary public API. Two such lookups -- LOC and DNAME, say -- would + * otherwise key identically and be answered with each other's records. + * The key only has to be injective, not readable. */ + status = ares_buf_append_num_dec(buf, (size_t)qtype, 0); if (status != ARES_SUCCESS) { goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ } @@ -103,7 +217,10 @@ goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ } - status = ares_buf_append_str(buf, ares_dns_class_tostr(qclass)); + /* Numeric for the same reason. ares_dns_record_query_add() does reject + * classes outside its list today, so the mnemonic is safe -- but the key + * should not depend on that validation staying where it is. */ + status = ares_buf_append_num_dec(buf, (size_t)qclass, 0); if (status != ARES_SUCCESS) { goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ } @@ -121,6 +238,21 @@ name_len--; } + /* Length-prefix the name so it is self-delimiting. The question count + * above stops one question imitating two, but with the counts equal the + * boundary between adjacent names is still guessable, since names reach + * the key unescaped separated only by '|'. A length makes each name + * unambiguous for any question count. */ + status = ares_buf_append_num_dec(buf, name_len, 0); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + + status = ares_buf_append_byte(buf, ':'); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + if (name_len > 0) { status = ares_buf_append(buf, (const unsigned char *)name, name_len); if (status != ARES_SUCCESS) { @@ -301,10 +433,10 @@ } /* On success, takes ownership of dnsrec */ -static ares_status_t ares_qcache_insert_int(ares_qcache_t *qcache, - ares_dns_record_t *qresp, - const ares_dns_record_t *qreq, - const ares_timeval_t *now) +static ares_status_t ares_qcache_insert_int(ares_qcache_t *qcache, + ares_dns_record_t *qresp, + const char *key, + const ares_timeval_t *now) { ares_qcache_entry_t *entry; unsigned int ttl; @@ -350,11 +482,11 @@ entry->expire_ts = (time_t)now->sec + (time_t)ttl; entry->insert_ts = (time_t)now->sec; - /* We can't guarantee the server responded with the same flags as the - * request had, so we have to re-parse the request in order to generate the - * key for caching, but we'll only do this once we know for sure we really - * want to cache it */ - entry->key = ares_qcache_calc_key(qreq); + /* The key was derived from the caller's request when the query was created. + * It must not be recomputed from query->query here: the EDNS fallback path + * strips the OPT record from that record before a successful retry, so a + * key derived from it would not match the one a later lookup computes. */ + entry->key = ares_strdup(key); if (entry->key == NULL) { goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ } @@ -371,40 +503,36 @@ /* LCOV_EXCL_START: OutOfMemory */ fail: - if (entry != NULL && entry->key != NULL) { - ares_htable_strvp_remove(qcache->cache, entry->key); - ares_free(entry->key); + if (entry != NULL) { + if (entry->key != NULL) { + ares_htable_strvp_remove(qcache->cache, entry->key); + ares_free(entry->key); + } ares_free(entry); } return ARES_ENOMEM; /* LCOV_EXCL_STOP */ } -ares_status_t ares_qcache_fetch(ares_channel_t *channel, - const ares_timeval_t *now, - const ares_dns_record_t *dnsrec, - const ares_dns_record_t **dnsrec_resp) +ares_status_t ares_qcache_fetch(ares_channel_t *channel, + const ares_timeval_t *now, const char *key, + ares_dns_record_t **dnsrec_resp) { - char *key = NULL; ares_qcache_entry_t *entry; ares_status_t status = ARES_SUCCESS; - if (channel == NULL || dnsrec == NULL || dnsrec_resp == NULL) { + if (channel == NULL || key == NULL || dnsrec_resp == NULL) { return ARES_EFORMERR; } + *dnsrec_resp = NULL; + if (channel->qcache == NULL) { return ARES_ENOTFOUND; } ares_qcache_expire(channel->qcache, now); - key = ares_qcache_calc_key(dnsrec); - if (key == NULL) { - status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ - goto done; /* LCOV_EXCL_LINE: OutOfMemory */ - } - entry = ares_htable_strvp_get_direct(channel->qcache->cache, key); if (entry == NULL) { status = ARES_ENOTFOUND; @@ -414,17 +542,53 @@ ares_dns_record_ttl_decrement(entry->dnsrec, (unsigned int)(now->sec - entry->insert_ts)); - *dnsrec_resp = entry->dnsrec; + /* Hand back a copy the caller owns rather than the cache's own record. + * This is passed straight into an application callback, and that callback + * is allowed to re-enter c-ares. Any lookup it starts runs the expiry + * reaper first, and changing the server list flushes the cache outright -- + * either would destroy this entry while the callback still held a pointer + * into it. Mirrors the ownership fix already made on the insert side. */ + status = ares_dns_record_duplicate_ex(dnsrec_resp, entry->dnsrec); + if (status != ARES_SUCCESS) { + /* Could not hand back a private copy. Report a miss so the lookup falls + * through to a real query rather than failing outright: duplication + * writes and re-parses the record, so it can fail for reasons other than + * memory exhaustion, and none of them are a reason to fail a lookup that + * would have gone to a server a moment earlier. */ + status = ARES_ENOTFOUND; /* LCOV_EXCL_LINE: UntestablePath */ + goto done; /* LCOV_EXCL_LINE: UntestablePath */ + } done: - ares_free(key); return status; } -ares_status_t ares_qcache_insert(ares_channel_t *channel, - const ares_timeval_t *now, - const ares_query_t *query, - ares_dns_record_t *dnsrec) -{ - return ares_qcache_insert_int(channel->qcache, dnsrec, query->query, now); +ares_status_t ares_qcache_insert(ares_channel_t *channel, + const ares_timeval_t *now, + const ares_query_t *query, + const ares_dns_record_t *dnsrec) +{ + ares_dns_record_t *dupdns; + ares_status_t status; + + /* Checked before the duplicate below, which is a full write and re-parse: + * no key means caching is switched off, so the copy would be thrown away. + * ares_init_options() always creates the cache object -- it is disabled by + * a zero maximum TTL, not by a NULL pointer -- so this is the only place + * the distinction is visible here. */ + if (query->qcache_key == NULL) { + return ARES_ENOTIMP; + } + + dupdns = ares_dns_record_duplicate(dnsrec); + if (dupdns == NULL) { + return ARES_ENOMEM; + } + + status = + ares_qcache_insert_int(channel->qcache, dupdns, query->qcache_key, now); + if (status != ARES_SUCCESS) { + ares_dns_record_destroy(dupdns); + } + return status; } diff -Nru c-ares-1.34.5/src/lib/ares_query.c c-ares-1.34.9/src/lib/ares_query.c --- c-ares-1.34.5/src/lib/ares_query.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_query.c 2026-10-20 02:31:51.000000000 +0000 @@ -125,7 +125,12 @@ } ares_channel_lock(channel); + /* Notifications raised along the way are delivered once this whole call + * has unwound, not from inside the internals it runs. See + * ares_deferred_cb_enter(). */ + ares_deferred_cb_enter(channel); status = ares_query_nolock(channel, name, dnsclass, type, callback, arg, qid); + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); return status; } diff -Nru c-ares-1.34.5/src/lib/ares_search.c c-ares-1.34.9/src/lib/ares_search.c --- c-ares-1.34.5/src/lib/ares_search.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_search.c 2026-10-20 02:31:51.000000000 +0000 @@ -463,7 +463,12 @@ } ares_channel_lock(channel); + /* Notifications raised along the way are delivered once this whole call + * has unwound, not from inside the internals it runs. See + * ares_deferred_cb_enter(). */ + ares_deferred_cb_enter(channel); ares_search_int(channel, dnsrec, ares_dnsrec_convert_cb, carg); + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); ares_dns_record_destroy(dnsrec); @@ -481,7 +486,12 @@ } ares_channel_lock(channel); + /* Notifications raised along the way are delivered once this whole call + * has unwound, not from inside the internals it runs. See + * ares_deferred_cb_enter(). */ + ares_deferred_cb_enter(channel); status = ares_search_int(channel, dnsrec, callback, arg); + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); return status; diff -Nru c-ares-1.34.5/src/lib/ares_send.c c-ares-1.34.9/src/lib/ares_send.c --- c-ares-1.34.5/src/lib/ares_send.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_send.c 2026-10-20 02:31:51.000000000 +0000 @@ -111,11 +111,33 @@ ares_callback_dnsrec callback, void *arg, unsigned short *qid) { - ares_query_t *query; - ares_timeval_t now; - ares_status_t status; - unsigned short id = generate_unique_qid(channel); - const ares_dns_record_t *dnsrec_resp = NULL; + ares_query_t *query; + ares_timeval_t now; + ares_status_t status; + unsigned short id = generate_unique_qid(channel); + ares_dns_record_t *dnsrec_resp = NULL; + char *qcache_key = NULL; + + /* A channel being destroyed accepts no new queries. ares_destroy(3) + * documents that a callback must not add new requests to a channel being + * destroyed. Refuse it, but still notify: ARES_EDESTRUCTION is documented + * in ares_query(3), ares_send(3), ares_search(3) and ares_getaddrinfo(3) as + * a status delivered to the callback, and every caller frees its own + * context from there. + * + * Not notified if we are already inside such a notification: an application + * whose callback starts a replacement lookup on every invocation would + * otherwise re-enter here from within the callback without bound and + * exhaust the stack. At that depth it is already past the contract + * ares_destroy(3) documents. */ + if (!channel->sys_up) { + if (!channel->in_shutdown_refusal) { + channel->in_shutdown_refusal = ARES_TRUE; + callback(arg, ARES_EDESTRUCTION, 0, NULL); + channel->in_shutdown_refusal = ARES_FALSE; + } + return ARES_EDESTRUCTION; + } ares_tvnow(&now); @@ -124,13 +146,28 @@ return ARES_ENOSERVER; } - if (!(flags & ARES_SEND_FLAG_NOCACHE)) { + /* Derive the cache key once, from the caller's request, and only when the + * cache is actually on. The send path may mutate query->query afterwards + * -- the EDNS fallback strips its OPT record -- and a key derived from it + * later would not match. ares_init_options() always creates the cache + * object, so a zero maximum TTL is what "off" means. */ + if (channel->qcache_max_ttl > 0) { + qcache_key = ares_qcache_calc_key(dnsrec); + if (qcache_key == NULL) { + callback(arg, ARES_ENOMEM, 0, NULL); /* LCOV_EXCL_LINE: OutOfMemory */ + return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + if (qcache_key != NULL && !(flags & ARES_SEND_FLAG_NOCACHE)) { /* Check query cache */ - status = ares_qcache_fetch(channel, &now, dnsrec, &dnsrec_resp); + status = ares_qcache_fetch(channel, &now, qcache_key, &dnsrec_resp); if (status != ARES_ENOTFOUND) { /* ARES_SUCCESS means we retrieved the cache, anything else is a critical * failure, all result in termination */ + ares_free(qcache_key); callback(arg, status, 0, dnsrec_resp); + ares_dns_record_destroy(dnsrec_resp); return status; } } @@ -138,19 +175,27 @@ /* Allocate space for query and allocated fields. */ query = ares_malloc(sizeof(ares_query_t)); if (!query) { - callback(arg, ARES_ENOMEM, 0, NULL); /* LCOV_EXCL_LINE: OutOfMemory */ - return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ + /* LCOV_EXCL_START: OutOfMemory */ + ares_free(qcache_key); + callback(arg, ARES_ENOMEM, 0, NULL); + return ARES_ENOMEM; + /* LCOV_EXCL_STOP */ } memset(query, 0, sizeof(*query)); query->channel = channel; query->qid = id; + query->serial = ++channel->query_serial; query->timeout.sec = 0; query->timeout.usec = 0; query->using_tcp = (channel->flags & ARES_FLAG_USEVC) ? ARES_TRUE : ARES_FALSE; /* Duplicate Query */ + /* The query adopts the key derived above; ares_free_query() releases it. */ + query->qcache_key = qcache_key; + qcache_key = NULL; + status = ares_dns_record_duplicate_ex(&query->query, dnsrec); if (status != ARES_SUCCESS) { /* Sometimes we might get a EBADRESP response from duplicate due to @@ -158,7 +203,11 @@ if (status == ARES_EBADRESP) { status = ARES_EBADQUERY; } - ares_free(query); + /* ares_free_query() rather than ares_free(): the query was zeroed above, + * so detaching finds every node NULL and the qid not yet registered, and + * it also releases qcache_key. Reachable without memory pressure -- a + * name that is not a valid hostname fails the duplicate. */ + ares_free_query(query); callback(arg, status, 0, NULL); return status; } @@ -209,6 +258,12 @@ */ if (!ares_htable_szvp_insert(channel->queries_by_qid, query->qid, query)) { /* LCOV_EXCL_START: OutOfMemory */ + /* The query is already chained into all_queries, so a reentrant + * ares_cancel() from the callback below would find it, run the + * application callback a second time for one lookup, and free it -- and + * the ares_free_query() here would then double-free. Same ordering as + * ares_cancel() and ares_destroy(): unlink first, notify second. */ + ares_detach_query(query); callback(arg, ARES_ENOMEM, 0, NULL); ares_free_query(query); return ARES_ENOMEM; @@ -217,10 +272,21 @@ /* Perform the first query action. */ + /* Sending opens and writes to connections and can mark a server down, both + * of which notify. This bracket keeps any connection or server unlinked + * during the send readable until it returns. It is never the outermost + * one: every public entry point that can get here, and query processing, + * holds its own, so delivery waits until the caller -- which may still be + * holding its own lookup state, as ares_getaddrinfo() does -- has unwound + * too. */ + ares_deferred_cb_enter(channel); status = ares_send_query(server, query, &now); + ares_deferred_cb_leave(channel); + if (status == ARES_SUCCESS && qid) { *qid = id; } + return status; } @@ -236,9 +302,14 @@ } ares_channel_lock(channel); + /* Notifications raised along the way are delivered once this whole call + * has unwound, not from inside the internals it runs. See + * ares_deferred_cb_enter(). */ + ares_deferred_cb_enter(channel); status = ares_send_nolock(channel, NULL, 0, dnsrec, callback, arg, qid); + ares_deferred_cb_leave(channel); ares_channel_unlock(channel); return status; diff -Nru c-ares-1.34.5/src/lib/ares_set_socket_functions.c c-ares-1.34.9/src/lib/ares_set_socket_functions.c --- c-ares-1.34.5/src/lib/ares_set_socket_functions.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_set_socket_functions.c 2026-10-20 02:31:51.000000000 +0000 @@ -61,7 +61,7 @@ # define TFO_USE_SENDTO 0 # define TFO_USE_CONNECTX 0 # define TFO_CLIENT_SOCKOPT TCP_FASTOPEN_CONNECT -#elif defined(__FreeBSD__) && defined(TCP_FASTOPEN) +#elif (defined(__MidnightBSD__) || defined(__FreeBSD__)) && defined(TCP_FASTOPEN) # define TFO_SUPPORTED 1 # define TFO_SKIP_CONNECT 1 # define TFO_USE_SENDTO 1 @@ -127,6 +127,8 @@ channel->sock_funcs.asendto = funcs->asendto; channel->sock_funcs.agetsockname = funcs->agetsockname; channel->sock_funcs.abind = funcs->abind; + channel->sock_funcs.aif_nametoindex = funcs->aif_nametoindex; + channel->sock_funcs.aif_indextoname = funcs->aif_indextoname; } /* Implement newer versions here ...*/ diff -Nru c-ares-1.34.5/src/lib/ares_socket.c c-ares-1.34.9/src/lib/ares_socket.c --- c-ares-1.34.5/src/lib/ares_socket.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_socket.c 2026-10-20 02:31:51.000000000 +0000 @@ -127,6 +127,32 @@ return ARES_FALSE; /* different */ } +ares_bool_t ares_sockaddr_addr_port_eq(const struct sockaddr *sa, + const struct ares_addr *aa, + unsigned short port) +{ + unsigned short sa_port; + + if (!ares_sockaddr_addr_eq(sa, aa)) { + return ARES_FALSE; + } + + /* ares_sockaddr_addr_eq() only matches AF_INET and AF_INET6 */ + if (sa->sa_family == AF_INET) { + sa_port = (CARES_INADDR_CAST(const struct sockaddr_in *, sa))->sin_port; + } else { + sa_port = (CARES_INADDR_CAST(const struct sockaddr_in6 *, sa))->sin6_port; + } + + /* A port of 0 means the source port was not reported, so only the address + * can be checked. */ + if (sa_port != 0 && ntohs(sa_port) != port) { + return ARES_FALSE; + } + + return ARES_TRUE; +} + ares_conn_err_t ares_socket_write(ares_channel_t *channel, ares_socket_t fd, const void *data, size_t len, size_t *written, const struct sockaddr *sa, @@ -188,6 +214,8 @@ { ares_ssize_t rv; + *read_bytes = 0; + rv = channel->sock_funcs.arecvfrom(s, data, data_len, flags, from, from_len, channel->sock_func_cb_data); diff -Nru c-ares-1.34.5/src/lib/ares_socket.h c-ares-1.34.9/src/lib/ares_socket.h --- c-ares-1.34.5/src/lib/ares_socket.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_socket.h 2026-10-20 02:31:51.000000000 +0000 @@ -139,8 +139,31 @@ ARES_CONN_ERR_FAILURE = 99 /*!< Generic failure */ } ares_conn_err_t; +/*! Compare the IP address of a socket address with an ares_addr. The port + * in the socket address is ignored. + * + * \param[in] sa Socket address (AF_INET or AF_INET6) + * \param[in] aa Address to compare against + * \return ARES_TRUE if the address families and IP addresses are equal, + * ARES_FALSE otherwise + */ ares_bool_t ares_sockaddr_addr_eq(const struct sockaddr *sa, const struct ares_addr *aa); + +/*! Compare the IP address and port of a socket address with an ares_addr and + * port. A port of 0 in the socket address means the port is unknown, so it + * matches any port. + * + * \param[in] sa Socket address (AF_INET or AF_INET6) + * \param[in] aa Address to compare against + * \param[in] port Port to compare against, in host byte order + * \return ARES_TRUE if the address families and IP addresses are equal and + * the ports are equal or the socket address port is 0, ARES_FALSE + * otherwise + */ +ares_bool_t ares_sockaddr_addr_port_eq(const struct sockaddr *sa, + const struct ares_addr *aa, + unsigned short port); ares_status_t ares_socket_configure(ares_channel_t *channel, int family, ares_bool_t is_tcp, ares_socket_t fd); ares_conn_err_t ares_socket_enable_tfo(const ares_channel_t *channel, diff -Nru c-ares-1.34.5/src/lib/ares_sysconfig.c c-ares-1.34.9/src/lib/ares_sysconfig.c --- c-ares-1.34.5/src/lib/ares_sysconfig.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_sysconfig.c 2026-10-20 02:31:51.000000000 +0000 @@ -218,13 +218,16 @@ status = ares_sconfig_append_fromstr(channel, &sysconfig->sconfig, dns_servers[i], ARES_TRUE); if (status != ARES_SUCCESS) { - return status; + break; } } for (i = 0; i < num_servers; i++) { ares_free(dns_servers[i]); } ares_free(dns_servers); + if (status != ARES_SUCCESS) { + return status; + } } domains = ares_get_android_search_domains_list(); @@ -273,7 +276,7 @@ * 3. if confstr(_CS_DOMAIN, ...) this is the domain name. Use this as * preference over anything else found. */ - ares_buf_t *buf = ares_buf_create(); + ares_buf_t *buf = NULL; unsigned char *data = NULL; size_t data_size = 0; ares_bool_t process_resolvconf = ARES_TRUE; @@ -330,7 +333,7 @@ char domain[256]; size_t domain_len; - domain_len = confstr(_CS_DOMAIN, domain, sizeof(domain_len)); + domain_len = confstr(_CS_DOMAIN, domain, sizeof(domain)); if (domain_len != 0) { ares_strsplit_free(sysconfig->domains, sysconfig->ndomains); sysconfig->domains = ares_strsplit(domain, ", ", &sysconfig->ndomains); diff -Nru c-ares-1.34.5/src/lib/ares_sysconfig_files.c c-ares-1.34.9/src/lib/ares_sysconfig_files.c --- c-ares-1.34.5/src/lib/ares_sysconfig_files.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_sysconfig_files.c 2026-10-20 02:31:51.000000000 +0000 @@ -27,6 +27,8 @@ #include "ares_private.h" +#include + #ifdef HAVE_SYS_PARAM_H # include #endif @@ -170,8 +172,8 @@ if (ares_str_isnum(maskstr)) { /* Numeric mask */ - int mask = atoi(maskstr); - if (mask < 0 || mask > 128) { + unsigned int mask; + if (!ares_str_parse_uint(maskstr, 128, &mask)) { return ARES_EBADSTR; } if (pat->addr.family == AF_INET && mask > 32) { @@ -340,12 +342,17 @@ const char *value = lookups[i]; char ch; + /* AIX /etc/netsvc.conf uses address-family-suffixed tokens such as + * "bind4"/"bind6" and "local4"/"local6" in addition to the bare forms. */ if (ares_strcaseeq(value, "dns") || ares_strcaseeq(value, "bind") || + ares_strcaseeq(value, "bind4") || ares_strcaseeq(value, "bind6") || ares_strcaseeq(value, "resolv") || ares_strcaseeq(value, "resolve")) { ch = 'b'; } else if (ares_strcaseeq(value, "files") || ares_strcaseeq(value, "file") || - ares_strcaseeq(value, "local")) { + ares_strcaseeq(value, "local") || + ares_strcaseeq(value, "local4") || + ares_strcaseeq(value, "local6")) { ch = 'f'; } else { continue; @@ -401,20 +408,27 @@ key = kv[0]; if (num == 2) { - val = kv[1]; - valint = (unsigned int)strtoul(val, NULL, 10); + val = kv[1]; + if (!ares_str_parse_uint(val, UINT_MAX, &valint)) { + status = ARES_EBADSTR; + goto done; + } } if (ares_streq(key, "ndots")) { sysconfig->ndots = valint; } else if (ares_streq(key, "retrans") || ares_streq(key, "timeout")) { if (valint == 0) { - return ARES_EFORMERR; + status = ARES_EFORMERR; + goto done; } sysconfig->timeout_ms = valint * 1000; } else if (ares_streq(key, "retry") || ares_streq(key, "attempts")) { - if (valint == 0) { - return ARES_EFORMERR; + if (valint == 0 || valint > 255) { + /* A bogus attempt count must not become an effectively unbounded + * retry loop; ignore it like other invalid options. */ + status = ARES_EFORMERR; + goto done; } sysconfig->tries = valint; } else if (ares_streq(key, "rotate")) { @@ -577,7 +591,10 @@ ares_buf_consume_whitespace(line, ARES_TRUE); - status = buf_fetch_string(line, value, sizeof(value)); + ares_buf_tag(line); + ares_buf_consume_until_charset(line, (const unsigned char *)"#;", 2, + ARES_FALSE); + status = ares_buf_tag_fetch_string(line, value, sizeof(value)); if (status != ARES_SUCCESS) { return ARES_SUCCESS; } @@ -596,8 +613,13 @@ } } else if (ares_streq(option, "lookup") || ares_streq(option, "hostresorder")) { - ares_buf_tag_rollback(line); - status = config_lookup(sysconfig, line, " \t"); + ares_buf_t *value_buf = + ares_buf_create_const((const unsigned char *)value, ares_strlen(value)); + if (value_buf == NULL) { + return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ + } + status = config_lookup(sysconfig, value_buf, " \t"); + ares_buf_destroy(value_buf); } else if (ares_streq(option, "search")) { status = config_search(sysconfig, value, 0); } else if (ares_streq(option, "nameserver")) { diff -Nru c-ares-1.34.5/src/lib/ares_sysconfig_win.c c-ares-1.34.9/src/lib/ares_sysconfig_win.c --- c-ares-1.34.5/src/lib/ares_sysconfig_win.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_sysconfig_win.c 2026-10-20 02:31:51.000000000 +0000 @@ -55,6 +55,19 @@ #include "ares_inet_net_pton.h" #if defined(USE_WINSOCK) + +# define WIN_NS_9X "System\\CurrentControlSet\\Services\\VxD\\MSTCP" +# define WIN_DNSCLIENT "Software\\Policies\\Microsoft\\System\\DNSClient" +# define WIN_NT_DNSCLIENT \ + "Software\\Policies\\Microsoft\\Windows NT\\DNSClient" +# define NAMESERVER "NameServer" +# define DHCPNAMESERVER "DhcpNameServer" +# define SEARCHLIST_KEY L"SearchList" +# define PRIMARYDNSSUFFIX_KEY L"PrimaryDNSSuffix" +# define INTERFACES_KEY "Interfaces" +# define DOMAIN_KEY L"Domain" +# define DHCPDOMAIN_KEY L"DhcpDomain" + /* * get_REG_SZ() * @@ -69,70 +82,101 @@ * * Supported on Windows NT 3.5 and newer. */ -static ares_bool_t get_REG_SZ(HKEY hKey, const char *leafKeyName, char **outptr) +static ares_bool_t get_REG_SZ(HKEY hKey, const WCHAR *leafKeyName, char **outptr) { - DWORD size = 0; - int res; + DWORD size = 0; + int res; + int len; + WCHAR *val = NULL; *outptr = NULL; /* Find out size of string stored in registry */ - res = RegQueryValueExA(hKey, leafKeyName, 0, NULL, NULL, &size); + res = RegQueryValueExW(hKey, leafKeyName, 0, NULL, NULL, &size); if ((res != ERROR_SUCCESS && res != ERROR_MORE_DATA) || !size) { return ARES_FALSE; } /* Allocate buffer of indicated size plus one given that string might have been stored without null termination */ - *outptr = ares_malloc(size + 1); - if (!*outptr) { + val = ares_malloc_zero(size + sizeof(WCHAR)); + if (val == NULL) { return ARES_FALSE; } /* Get the value for real */ - res = RegQueryValueExA(hKey, leafKeyName, 0, NULL, (unsigned char *)*outptr, - &size); - if ((res != ERROR_SUCCESS) || (size == 1)) { + res = RegQueryValueExW(hKey, leafKeyName, 0, NULL, (BYTE *)val, &size); + if (res != ERROR_SUCCESS || size == sizeof(WCHAR)) { + ares_free(val); + return ARES_FALSE; + } + + /* Convert to UTF8 */ + len = WideCharToMultiByte(CP_UTF8, 0, val, -1, NULL, 0, NULL, NULL); + if (len == 0) { + ares_free(val); + return ARES_FALSE; + } + *outptr = ares_malloc_zero((size_t)len + 1); + if (*outptr == NULL) { + ares_free(val); + return ARES_FALSE; + } + if (WideCharToMultiByte(CP_UTF8, 0, val, -1, *outptr, len, NULL, NULL) + == 0) { ares_free(*outptr); *outptr = NULL; + ares_free(val); return ARES_FALSE; } - /* Null terminate buffer always */ - *(*outptr + size) = '\0'; - + ares_free(val); return ARES_TRUE; } -static void commanjoin(char **dst, const char * const src, const size_t len) +static ares_status_t sysconfig_commajoin(ares_buf_t *buf, const char *src, + ares_bool_t asciionly) { - char *newbuf; - size_t newsize; + ares_status_t status; - /* 1 for terminating 0 and 2 for , and terminating 0 */ - newsize = len + (*dst ? (ares_strlen(*dst) + 2) : 1); - newbuf = ares_realloc(*dst, newsize); - if (!newbuf) { - return; + if (buf == NULL) { + return ARES_ENOMEM; } - if (*dst == NULL) { - *newbuf = '\0'; + + if (src == NULL || *src == '\0') { + return ARES_SUCCESS; } - *dst = newbuf; - if (ares_strlen(*dst) != 0) { - strcat(*dst, ","); + + if (asciionly && !ares_str_isprint(src, ares_strlen(src))) { + return ARES_SUCCESS; + } + + if (ares_buf_len(buf) > 0) { + status = ares_buf_append_byte(buf, ','); + if (status != ARES_SUCCESS) { + return status; + } } - strncat(*dst, src, len); + + return ares_buf_append_str(buf, src); } -/* - * commajoin() - * - * RTF code. - */ -static void commajoin(char **dst, const char *src) +/* Read a REG_SZ value and comma-append it (ASCII-only) to *buf. + * On allocation failure, destroys *buf and sets it to NULL. */ +static void reg_commajoin(ares_buf_t **buf, HKEY key, const WCHAR *leaf) { - commanjoin(dst, src, ares_strlen(src)); + char *p = NULL; + + if (*buf == NULL || !get_REG_SZ(key, leaf, &p)) { + return; + } + + if (sysconfig_commajoin(*buf, p, ARES_TRUE) != ARES_SUCCESS) { + ares_buf_destroy(*buf); + *buf = NULL; + } + + ares_free(p); } /* A structure to hold the string form of IPv4 and IPv6 addresses so we can @@ -463,8 +507,10 @@ /* Join them all into a single string, removing duplicates. */ { - size_t i; - for (i = 0; i < addressesIndex; ++i) { + size_t i; + ares_buf_t *buf = ares_buf_create(); + + for (i = 0; buf != NULL && i < addressesIndex; ++i) { size_t j; /* Look for this address text appearing previously in the results. */ for (j = 0; j < i; ++j) { @@ -474,10 +520,22 @@ } /* Iff we didn't emit this address already, emit it now. */ if (j == i) { - /* Add that to outptr (if we can). */ - commajoin(outptr, addresses[i].text); + /* Add that to buf (if we can). */ + if (sysconfig_commajoin(buf, addresses[i].text, ARES_FALSE) != + ARES_SUCCESS) { + ares_buf_destroy(buf); + buf = NULL; + break; + } } } + + if (buf != NULL && ares_buf_len(buf) == 0) { + ares_buf_destroy(buf); + buf = NULL; + } + + *outptr = ares_buf_finish_str(buf, NULL); } done: @@ -509,51 +567,46 @@ */ static ares_bool_t get_SuffixList_Windows(char **outptr) { - HKEY hKey; - HKEY hKeyEnum; - char keyName[256]; - DWORD keyNameBuffSize; - DWORD keyIdx = 0; - char *p = NULL; + HKEY hKey; + HKEY hKeyEnum; + char keyName[256]; + DWORD keyNameBuffSize; + DWORD keyIdx = 0; + ares_buf_t *buf = ares_buf_create(); *outptr = NULL; + if (buf == NULL) { + return ARES_FALSE; + } + /* 1. Global DNS Suffix Search List */ if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NS_NT_KEY, 0, KEY_READ, &hKey) == ERROR_SUCCESS) { - get_REG_SZ(hKey, SEARCHLIST_KEY, outptr); - if (get_REG_SZ(hKey, DOMAIN_KEY, &p)) { - commajoin(outptr, p); - ares_free(p); - p = NULL; - } + reg_commajoin(&buf, hKey, SEARCHLIST_KEY); + reg_commajoin(&buf, hKey, DOMAIN_KEY); RegCloseKey(hKey); } - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NT_DNSCLIENT, 0, KEY_READ, &hKey) == - ERROR_SUCCESS) { - if (get_REG_SZ(hKey, SEARCHLIST_KEY, &p)) { - commajoin(outptr, p); - ares_free(p); - p = NULL; - } + if (buf != NULL && + RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NT_DNSCLIENT, 0, KEY_READ, &hKey) == + ERROR_SUCCESS) { + reg_commajoin(&buf, hKey, SEARCHLIST_KEY); RegCloseKey(hKey); } /* 2. Connection Specific Search List composed of: * a. Primary DNS Suffix */ - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_DNSCLIENT, 0, KEY_READ, &hKey) == - ERROR_SUCCESS) { - if (get_REG_SZ(hKey, PRIMARYDNSSUFFIX_KEY, &p)) { - commajoin(outptr, p); - ares_free(p); - p = NULL; - } + if (buf != NULL && + RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_DNSCLIENT, 0, KEY_READ, &hKey) == + ERROR_SUCCESS) { + reg_commajoin(&buf, hKey, PRIMARYDNSSUFFIX_KEY); RegCloseKey(hKey); } /* b. Interface SearchList, Domain, DhcpDomain */ - if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NS_NT_KEY "\\" INTERFACES_KEY, 0, + if (buf != NULL && + RegOpenKeyExA(HKEY_LOCAL_MACHINE, WIN_NS_NT_KEY "\\" INTERFACES_KEY, 0, KEY_READ, &hKey) == ERROR_SUCCESS) { for (;;) { keyNameBuffSize = sizeof(keyName); @@ -565,27 +618,26 @@ ERROR_SUCCESS) { continue; } - /* p can be comma separated (SearchList) */ - if (get_REG_SZ(hKeyEnum, SEARCHLIST_KEY, &p)) { - commajoin(outptr, p); - ares_free(p); - p = NULL; - } - if (get_REG_SZ(hKeyEnum, DOMAIN_KEY, &p)) { - commajoin(outptr, p); - ares_free(p); - p = NULL; - } - if (get_REG_SZ(hKeyEnum, DHCPDOMAIN_KEY, &p)) { - commajoin(outptr, p); - ares_free(p); - p = NULL; - } + /* SearchList can be comma separated */ + reg_commajoin(&buf, hKeyEnum, SEARCHLIST_KEY); + reg_commajoin(&buf, hKeyEnum, DOMAIN_KEY); + reg_commajoin(&buf, hKeyEnum, DHCPDOMAIN_KEY); RegCloseKey(hKeyEnum); + + if (buf == NULL) { + break; + } } RegCloseKey(hKey); } + if (ares_buf_len(buf) == 0) { + ares_buf_destroy(buf); + buf = NULL; + } + + *outptr = ares_buf_finish_str(buf, NULL); + return *outptr != NULL ? ARES_TRUE : ARES_FALSE; } diff -Nru c-ares-1.34.5/src/lib/ares_timeout.c c-ares-1.34.9/src/lib/ares_timeout.c --- c-ares-1.34.5/src/lib/ares_timeout.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_timeout.c 2026-10-20 02:31:51.000000000 +0000 @@ -32,6 +32,17 @@ #endif +void ares_timeval_add(ares_timeval_t *now, size_t millisecs) +{ + now->sec += (ares_int64_t)millisecs / 1000; + now->usec += (unsigned int)((millisecs % 1000) * 1000); + + if (now->usec >= 1000000) { + now->sec += now->usec / 1000000; + now->usec %= 1000000; + } +} + void ares_timeval_remaining(ares_timeval_t *remaining, const ares_timeval_t *now, const ares_timeval_t *tout) diff -Nru c-ares-1.34.5/src/lib/ares_update_servers.c c-ares-1.34.9/src/lib/ares_update_servers.c --- c-ares-1.34.5/src/lib/ares_update_servers.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/ares_update_servers.c 2026-10-20 02:31:51.000000000 +0000 @@ -27,6 +27,8 @@ */ #include "ares_private.h" +#include + #ifdef HAVE_ARPA_INET_H # include #endif @@ -205,6 +207,8 @@ char hoststr[256]; size_t addrlen; + memset(sconfig, 0, sizeof(*sconfig)); + status = ares_uri_parse_buf(&uri, buf); if (status != ARES_SUCCESS) { return status; @@ -234,7 +238,10 @@ sconfig->tcp_port = sconfig->udp_port; port = ares_uri_get_query_key(uri, "tcpport"); if (port != NULL) { - sconfig->tcp_port = (unsigned short)atoi(port); + if (!ares_parse_port(port, &sconfig->tcp_port, ARES_TRUE)) { + status = ARES_EBADSTR; + goto done; + } } done: @@ -352,7 +359,9 @@ return status; } - sconfig->udp_port = (unsigned short)atoi(portstr); + if (!ares_parse_port(portstr, &sconfig->udp_port, ARES_TRUE)) { + return ARES_EBADSTR; + } sconfig->tcp_port = sconfig->udp_port; } @@ -398,7 +407,13 @@ if (ares_str_isnum(ll_iface)) { char ifname[IF_NAMESIZE] = ""; - ll_scope = (unsigned int)atoi(ll_iface); + + /* The interface identifier is all digits but may not fit in an + * unsigned int; parse with the range-checked helper rather than atoi(), + * whose behavior on overflow is undefined. */ + if (!ares_str_parse_uint(ll_iface, UINT_MAX, &ll_scope)) { + return ARES_ENOTFOUND; + } if (channel->sock_funcs.aif_indextoname == NULL || channel->sock_funcs.aif_indextoname(ll_scope, ifname, sizeof(ifname), channel->sock_func_cb_data) == @@ -727,17 +742,36 @@ { ares_bool_t stale_removed = ARES_FALSE; ares_slist_node_t *snode = ares_slist_node_first(channel->servers); + size_t generation = channel->servers_generation; + /* Re-read from the head each pass rather than caching the next node. + * ares_slist_node_destroy() below reaches application callbacks, by way of + * ares_destroy_server() -> ares_close_sockets() -> ares_requeue_queries(), + * and a nested reconfiguration from one of those destroys the remaining + * stale servers -- including whichever one a cached pointer referred to. */ while (snode != NULL) { - ares_slist_node_t *snext = ares_slist_node_next(snode); const ares_server_t *server = ares_slist_node_val(snode); - if (!ares_server_in_newconfig(server, srvlist)) { - /* This will clean up all server state via the destruction callback and - * move any queries to new servers */ - ares_slist_node_destroy(snode); - stale_removed = ARES_TRUE; + + if (ares_server_in_newconfig(server, srvlist)) { + snode = ares_slist_node_next(snode); + continue; } - snode = snext; + + /* This will clean up all server state via the destruction callback and + * move any queries to new servers */ + ares_slist_node_destroy(snode); + stale_removed = ARES_TRUE; + + /* A callback reached from the destroy above may have reconfigured the + * server list against a newer configuration. That call has already + * reconciled the whole list, so whatever remains belongs to it and not to + * srvlist -- stop rather than deleting its servers. */ + if (channel->servers_generation != generation) { + break; + } + + /* The callbacks above may have removed further servers, so restart. */ + snode = ares_slist_node_first(channel->servers); } return stale_removed; } @@ -762,6 +796,14 @@ return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ } + /* Removing servers closes their connections, which notifies. Deliver + * once this call is done rather than from inside the teardown. */ + ares_deferred_cb_enter(channel); + + /* Bumped before anything below can reach an application callback, so a + * nested reconfiguration is distinguishable from our own work. */ + channel->servers_generation++; + /* NOTE: a NULL or zero entry server list is considered valid due to * real-world people needing support for this for their test harnesses */ @@ -830,6 +872,7 @@ status = ARES_SUCCESS; done: + ares_deferred_cb_leave(channel); return status; } @@ -967,7 +1010,8 @@ sizeof(sconfig->addr.addr.addr4)); if (ares_llist_insert_last(s, sconfig) == NULL) { - goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + ares_free(sconfig); /* LCOV_EXCL_LINE: OutOfMemory */ + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ } } diff -Nru c-ares-1.34.5/src/lib/cares.rc c-ares-1.34.9/src/lib/cares.rc --- c-ares-1.34.5/src/lib/cares.rc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/cares.rc 2026-10-20 02:31:51.000000000 +0000 @@ -63,7 +63,7 @@ #endif VALUE "ProductName", "The c-ares library\0" VALUE "ProductVersion", ARES_VERSION_STR "\0" - VALUE "LegalCopyright", "© " ARES_COPYRIGHT "\0" + VALUE "LegalCopyright", "Copyright (c) " ARES_COPYRIGHT "\0" VALUE "License", "https://c-ares.org/license.html\0" END END diff -Nru c-ares-1.34.5/src/lib/dsa/ares_array.c c-ares-1.34.9/src/lib/dsa/ares_array.c --- c-ares-1.34.5/src/lib/dsa/ares_array.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/dsa/ares_array.c 2026-10-20 02:31:51.000000000 +0000 @@ -171,14 +171,19 @@ ares_status_t ares_array_set_size(ares_array_t *arr, size_t size) { - void *temp; + void *temp; + size_t rounded_size; if (arr == NULL || size == 0 || size < arr->cnt) { return ARES_EFORMERR; } /* Always operate on powers of 2 */ - size = ares_round_up_pow2(size); + rounded_size = ares_round_up_pow2(size); + if (rounded_size < size) { + return ARES_ENOMEM; /* rounding wrapped around */ + } + size = rounded_size; if (size < ARES__ARRAY_MIN) { size = ARES__ARRAY_MIN; @@ -189,8 +194,8 @@ return ARES_SUCCESS; } - temp = ares_realloc_zero(arr->arr, arr->alloc_cnt * arr->member_size, - size * arr->member_size); + temp = + ares_realloc_zero_array(arr->arr, arr->alloc_cnt, size, arr->member_size); if (temp == NULL) { return ARES_ENOMEM; } @@ -295,7 +300,7 @@ ares_status_t status; void *ptr = NULL; - status = ares_array_insert_last(&ptr, arr); + status = ares_array_insert_first(&ptr, arr); if (status != ARES_SUCCESS) { return status; } @@ -362,6 +367,16 @@ } arr->cnt--; + + /* When empty, reset offset so a later insert doesn't compact from an + * out-of-range index (offset can reach alloc_cnt after front claims). + * This also protects ares_array_finish(), which has the same + * move(0, offset) and would otherwise fail on an offset == alloc_cnt + * array. */ + if (arr->cnt == 0) { + arr->offset = 0; + } + return ARES_SUCCESS; } diff -Nru c-ares-1.34.5/src/lib/dsa/ares_htable.c c-ares-1.34.9/src/lib/dsa/ares_htable.c --- c-ares-1.34.5/src/lib/dsa/ares_htable.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/dsa/ares_htable.c 2026-10-20 02:31:51.000000000 +0000 @@ -61,9 +61,9 @@ /* Mix stack address, heap address, and time to generate a random seed, it * doesn't have to be super secure, just quick. Likelihood of a hash * collision attack is very low with a small amount of effort */ - seed |= (unsigned int)((size_t)htable & 0xFFFFFFFF); - seed |= (unsigned int)((size_t)&seed & 0xFFFFFFFF); - seed |= (unsigned int)(((ares_uint64_t)t) & 0xFFFFFFFF); + seed ^= (unsigned int)((size_t)htable & 0xFFFFFFFF); + seed ^= (unsigned int)((size_t)&seed & 0xFFFFFFFF); + seed ^= (unsigned int)(((ares_uint64_t)t) & 0xFFFFFFFF); return seed; #endif } @@ -150,6 +150,10 @@ *num = 0; + if (htable->num_keys == 0) { + return NULL; + } + out = ares_malloc_zero(sizeof(*out) * htable->num_keys); if (out == NULL) { return NULL; /* LCOV_EXCL_LINE */ @@ -172,7 +176,7 @@ * We are doing "hash & (size - 1)" since we are guaranteeing a power of * 2 for size. This is equivalent to "hash % size", but should be more * efficient */ -#define HASH_IDX(h, key) h->hash(key, h->seed) & (h->size - 1) +#define HASH_IDX(h, key) ((h)->hash((key), (h)->seed) & ((h)->size - 1)) static ares_llist_node_t *ares_htable_find(const ares_htable_t *htable, unsigned int idx, const void *key) diff -Nru c-ares-1.34.5/src/lib/dsa/ares_htable_dict.c c-ares-1.34.9/src/lib/dsa/ares_htable_dict.c --- c-ares-1.34.5/src/lib/dsa/ares_htable_dict.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/dsa/ares_htable_dict.c 2026-10-20 02:31:51.000000000 +0000 @@ -132,6 +132,7 @@ fail: if (bucket) { + ares_free(bucket->key); ares_free(bucket->val); ares_free(bucket); } @@ -223,6 +224,7 @@ fail: *num = 0; + ares_free(buckets); ares_free_array(out, cnt, ares_free); return NULL; } diff -Nru c-ares-1.34.5/src/lib/dsa/ares_llist.c c-ares-1.34.9/src/lib/dsa/ares_llist.c --- c-ares-1.34.5/src/lib/dsa/ares_llist.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/dsa/ares_llist.c 2026-10-20 02:31:51.000000000 +0000 @@ -103,7 +103,10 @@ case ARES__LLIST_INSERT_BEFORE: node->next = at; node->prev = at->prev; - at->prev = node; + if (at->prev) { + at->prev->next = node; + } + at->prev = node; break; } if (list->tail == NULL) { diff -Nru c-ares-1.34.5/src/lib/event/ares_event_configchg.c c-ares-1.34.9/src/lib/event/ares_event_configchg.c --- c-ares-1.34.5/src/lib/event/ares_event_configchg.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/event/ares_event_configchg.c 2026-10-20 02:31:51.000000000 +0000 @@ -207,12 +207,14 @@ # ifdef HAVE_REGISTERWAITFORSINGLEOBJECT if (configchg->regip4_wait != NULL) { - UnregisterWait(configchg->regip4_wait); + /* Use INVALID_HANDLE_VALUE to wait for any running callback to complete + * before returning, preventing use-after-free of configchg */ + UnregisterWaitEx(configchg->regip4_wait, INVALID_HANDLE_VALUE); configchg->regip4_wait = NULL; } if (configchg->regip6_wait != NULL) { - UnregisterWait(configchg->regip6_wait); + UnregisterWaitEx(configchg->regip6_wait, INVALID_HANDLE_VALUE); configchg->regip6_wait = NULL; } @@ -383,6 +385,7 @@ # include # include +# include # include # include # include diff -Nru c-ares-1.34.5/src/lib/event/ares_event_select.c c-ares-1.34.9/src/lib/event/ares_event_select.c --- c-ares-1.34.5/src/lib/event/ares_event_select.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/event/ares_event_select.c 2026-10-20 02:31:51.000000000 +0000 @@ -46,6 +46,13 @@ if (e->ev_signal == NULL) { return ARES_FALSE; /* LCOV_EXCL_LINE: UntestablePath */ } + /* The wake pipe must fit in an fd_set too. If it does not, the update + * that registers it is rejected and freed, leaving e->ev_signal dangling, + * so fail initialization instead. */ + if (e->ev_signal->fd >= (ares_socket_t)FD_SETSIZE) { + e->ev_signal = NULL; + return ARES_FALSE; + } return ARES_TRUE; } @@ -56,7 +63,13 @@ static ares_bool_t ares_evsys_select_event_add(ares_event_t *event) { - (void)event; + /* select() can only watch descriptors below FD_SETSIZE; FD_SET() on a + * higher descriptor writes past the end of the fd_set. Refuse the + * registration instead. The socket then never reports activity, so a + * query using it waits out its timeout rather than corrupting memory. */ + if (event->fd != ARES_SOCKET_BAD && event->fd >= (ares_socket_t)FD_SETSIZE) { + return ARES_FALSE; + } return ARES_TRUE; } diff -Nru c-ares-1.34.5/src/lib/event/ares_event_thread.c c-ares-1.34.9/src/lib/event/ares_event_thread.c --- c-ares-1.34.5/src/lib/event/ares_event_thread.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/event/ares_event_thread.c 2026-10-20 02:31:51.000000000 +0000 @@ -235,6 +235,12 @@ ares_event_thread_wake(e); } +static void notifyenqueue_cb(void *data) +{ + ares_event_thread_t *e = data; + ares_event_thread_wake(e); +} + static void ares_event_process_updates(ares_event_thread_t *e) { ares_llist_node_t *node; @@ -415,6 +421,7 @@ channel->sock_state_cb = NULL; channel->notify_pending_write_cb = NULL; channel->notify_pending_write_cb_data = NULL; + ares_set_query_enqueue_cb(channel, NULL, NULL); } static const ares_event_sys_t *ares_event_fetch_sys(ares_evsys_t evsys) @@ -521,6 +528,7 @@ channel->sock_state_cb_data = e; channel->notify_pending_write_cb = notifywrite_cb; channel->notify_pending_write_cb_data = e; + ares_set_query_enqueue_cb(channel, notifyenqueue_cb, e); if (!e->ev_sys->init(e)) { /* LCOV_EXCL_START: UntestablePath */ diff -Nru c-ares-1.34.5/src/lib/event/ares_event_wake_pipe.c c-ares-1.34.9/src/lib/event/ares_event_wake_pipe.c --- c-ares-1.34.5/src/lib/event/ares_event_wake_pipe.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/event/ares_event_wake_pipe.c 2026-10-20 02:31:51.000000000 +0000 @@ -117,7 +117,13 @@ } p = e->data; - (void)write(p->filedes[1], "1", 1); + /* Best-effort wakeup. A failed or short write is harmless: it can only + * happen when the pipe buffer is already full, which means a wakeup is + * already pending. The result is consumed to satisfy warn_unused_result + * (a plain (void) cast does not silence it under _FORTIFY_SOURCE). */ + if (write(p->filedes[1], "1", 1) < 0) { + return; + } } static void ares_pipeevent_cb(ares_event_thread_t *e, ares_socket_t fd, diff -Nru c-ares-1.34.5/src/lib/event/ares_event_win32.c c-ares-1.34.9/src/lib/event/ares_event_win32.c --- c-ares-1.34.5/src/lib/event/ares_event_win32.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/event/ares_event_win32.c 2026-10-20 02:31:51.000000000 +0000 @@ -707,6 +707,9 @@ ares_bool_t rc = ARES_FALSE; ed = ares_malloc_zero(sizeof(*ed)); + if (ed == NULL) { + return ARES_FALSE; /* LCOV_EXCL_LINE: OutOfMemory */ + } ed->event = event; ed->socket = event->fd; ed->base_socket = ARES_SOCKET_BAD; diff -Nru c-ares-1.34.5/src/lib/include/ares_mem.h c-ares-1.34.9/src/lib/include/ares_mem.h --- c-ares-1.34.5/src/lib/include/ares_mem.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/include/ares_mem.h 2026-10-20 02:31:51.000000000 +0000 @@ -34,5 +34,8 @@ CARES_EXTERN void *ares_malloc_zero(size_t size); CARES_EXTERN void *ares_realloc_zero(void *ptr, size_t orig_size, size_t new_size); +CARES_EXTERN void *ares_malloc_zero_array(size_t num, size_t size); +CARES_EXTERN void *ares_realloc_zero_array(void *ptr, size_t orig_num, + size_t new_num, size_t size); #endif diff -Nru c-ares-1.34.5/src/lib/include/ares_str.h c-ares-1.34.9/src/lib/include/ares_str.h --- c-ares-1.34.5/src/lib/include/ares_str.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/include/ares_str.h 2026-10-20 02:31:51.000000000 +0000 @@ -60,6 +60,23 @@ CARES_EXTERN ares_bool_t ares_str_isnum(const char *str); CARES_EXTERN ares_bool_t ares_str_isalnum(const char *str); +/*! Parse a base-10 unsigned integer from a NULL-terminated string. Unlike + * atoi(), this rejects overflow, leading or trailing garbage (a sign, + * whitespace, or non-digit characters), and any value that does not fit + * within the caller-provided maximum. + * + * \param[in] str String to parse. May be NULL or empty, in which case + * ARES_FALSE is returned. + * \param[in] max Largest value considered valid. Values above UINT_MAX + * are capped to UINT_MAX since out is an unsigned int. + * \param[out] out On success, receives the parsed value. Untouched on + * failure. + * \return ARES_TRUE if a valid in-range integer was parsed, ARES_FALSE + * otherwise. + */ +CARES_EXTERN ares_bool_t ares_str_parse_uint(const char *str, unsigned long max, + unsigned int *out); + CARES_EXTERN void ares_str_ltrim(char *str); CARES_EXTERN void ares_str_rtrim(char *str); CARES_EXTERN void ares_str_trim(char *str); diff -Nru c-ares-1.34.5/src/lib/inet_net_pton.c c-ares-1.34.9/src/lib/inet_net_pton.c --- c-ares-1.34.5/src/lib/inet_net_pton.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/inet_net_pton.c 2026-10-20 02:31:51.000000000 +0000 @@ -51,14 +51,15 @@ /* * static int - * inet_net_pton_ipv4(src, dst, size) + * inet_net_pton_ipv4(src, dst, size, allow_cidr) * convert IPv4 network number from presentation to network format. - * accepts hex octets, hex strings, decimal octets, and /CIDR. - * "size" is in bytes and describes "dst". + * accepts hex octets, hex strings, decimal octets, and, if + * "allow_cidr" is true, /CIDR. "size" is in bytes and describes "dst". * return: * number of bits, either imputed classfully or specified with /CIDR, * or -1 if some failure occurred (check errno). ENOENT means it was - * not an IPv4 network specification. + * not an IPv4 network specification (including a /CIDR suffix when + * "allow_cidr" is false). * note: * network byte order assumed. this means 192.5.5.240/28 has * 0b11110000 in its fourth octet. @@ -71,7 +72,7 @@ * Paul Vixie (ISC), June 1996 */ static int ares_inet_net_pton_ipv4(const char *src, unsigned char *dst, - size_t size) + size_t size, ares_bool_t allow_cidr) { static const char xdigits[] = "0123456789abcdef"; static const char digits[] = "0123456789"; @@ -147,7 +148,8 @@ } bits = -1; - if (ch == '/' && ares_isascii(src[0]) && ares_isdigit(src[0]) && dst > odst) { + if (allow_cidr && ch == '/' && ares_isascii(src[0]) && ares_isdigit(src[0]) && + dst > odst) { /* CIDR width specifier. Nothing can follow it. */ ch = *src++; /* Skip over the /. */ bits = 0; @@ -317,7 +319,7 @@ continue; } if (ch == '.' && ((tp + NS_INADDRSZ) <= endp) && - ares_inet_net_pton_ipv4(curtok, tp, NS_INADDRSZ) > 0) { + ares_inet_net_pton_ipv4(curtok, tp, NS_INADDRSZ, ARES_FALSE) > 0) { tp += NS_INADDRSZ; saw_xdigit = 0; break; /* '\0' was seen by inet_pton4(). */ @@ -400,29 +402,23 @@ SET_SOCKERRNO(EMSGSIZE); return -1; } + /* Only the first (bits+7)/8 bytes carry the network, zero the remainder of + * the caller's buffer so it never holds uninitialized data. */ + memset(dst, 0, size); memcpy(dst, &in6, bytes); return (int)bits; } -/* - * int - * inet_net_pton(af, src, dst, size) - * convert network number from presentation to network format. - * accepts hex octets, hex strings, decimal octets, and /CIDR. - * "size" is in bytes and describes "dst". - * return: - * number of bits, either imputed classfully or specified with /CIDR, - * or -1 if some failure occurred (check errno). ENOENT means it was - * not a valid network specification. +/* Parse a network, not an address. See ares_inet_net_pton.h for the + * contract and how it differs from ares_inet_pton(). * author: * Paul Vixie (ISC), June 1996 - * */ int ares_inet_net_pton(int af, const char *src, void *dst, size_t size) { switch (af) { case AF_INET: - return ares_inet_net_pton_ipv4(src, dst, size); + return ares_inet_net_pton_ipv4(src, dst, size, ARES_TRUE); case AF_INET6: return ares_inet_net_pton_ipv6(src, dst, size); default: @@ -430,22 +426,35 @@ } } +/* + * int + * inet_pton(af, src, dst) + * convert an address (not a network) from presentation to network + * format, like POSIX inet_pton(). A "/bits" prefix length denotes a + * prefix, not an address (RFC 4291 Section 2.3 vs. the address text + * forms of Section 2.2), and is rejected; dst is always fully written + * on success (4 bytes for AF_INET, 16 bytes for AF_INET6). + * For a network with a prefix length, use ares_inet_net_pton(). + * return: + * 1 on success, 0 if src is not a valid address for af (as POSIX + * inet_pton() does for any unparsable input), or -1 with errno set to + * EAFNOSUPPORT if af is not supported. + */ int ares_inet_pton(int af, const char *src, void *dst) { - int result; - size_t size; + int result; if (af == AF_INET) { - size = sizeof(struct in_addr); + result = + ares_inet_net_pton_ipv4(src, dst, sizeof(struct in_addr), ARES_FALSE); } else if (af == AF_INET6) { - size = sizeof(struct ares_in6_addr); + result = ares_inet_pton6(src, dst); } else { SET_SOCKERRNO(EAFNOSUPPORT); return -1; } - result = ares_inet_net_pton(af, src, dst, size); - if (result == -1 && SOCKERRNO == ENOENT) { - return 0; - } - return (result > -1) ? 1 : -1; + /* Neither parser can fail for a system reason: any failure (ENOENT, or + * EMSGSIZE for too many IPv4 octets as in "1.2.3.4.5") means src is not a + * valid address. */ + return (result > -1) ? 1 : 0; } diff -Nru c-ares-1.34.5/src/lib/legacy/ares_expand_name.c c-ares-1.34.9/src/lib/legacy/ares_expand_name.c --- c-ares-1.34.5/src/lib/legacy/ares_expand_name.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/legacy/ares_expand_name.c 2026-10-20 02:31:51.000000000 +0000 @@ -69,7 +69,7 @@ } start_len = ares_buf_len(buf); - status = ares_dns_name_parse(buf, s, is_hostname); + status = ares_dns_name_parse(buf, s, is_hostname, ARES_TRUE); if (status != ARES_SUCCESS) { goto done; } diff -Nru c-ares-1.34.5/src/lib/legacy/ares_parse_a_reply.c c-ares-1.34.9/src/lib/legacy/ares_parse_a_reply.c --- c-ares-1.34.5/src/lib/legacy/ares_parse_a_reply.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/legacy/ares_parse_a_reply.c 2026-10-20 02:31:51.000000000 +0000 @@ -60,8 +60,11 @@ } if (naddrttls) { - req_naddrttls = (size_t)*naddrttls; - *naddrttls = 0; + /* A negative requested count would wrap to SIZE_MAX. */ + if (*naddrttls > 0) { + req_naddrttls = (size_t)*naddrttls; + } + *naddrttls = 0; } memset(&ai, 0, sizeof(ai)); diff -Nru c-ares-1.34.5/src/lib/legacy/ares_parse_aaaa_reply.c c-ares-1.34.9/src/lib/legacy/ares_parse_aaaa_reply.c --- c-ares-1.34.5/src/lib/legacy/ares_parse_aaaa_reply.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/legacy/ares_parse_aaaa_reply.c 2026-10-20 02:31:51.000000000 +0000 @@ -63,8 +63,11 @@ } if (naddrttls) { - req_naddrttls = (size_t)*naddrttls; - *naddrttls = 0; + /* A negative requested count would wrap to SIZE_MAX. */ + if (*naddrttls > 0) { + req_naddrttls = (size_t)*naddrttls; + } + *naddrttls = 0; } memset(&ai, 0, sizeof(ai)); diff -Nru c-ares-1.34.5/src/lib/legacy/ares_parse_ns_reply.c c-ares-1.34.9/src/lib/legacy/ares_parse_ns_reply.c --- c-ares-1.34.5/src/lib/legacy/ares_parse_ns_reply.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/legacy/ares_parse_ns_reply.c 2026-10-20 02:31:51.000000000 +0000 @@ -97,12 +97,12 @@ } /* Preallocate the maximum number + 1 */ - hostent->h_aliases = ares_malloc((ancount + 1) * sizeof(*hostent->h_aliases)); + hostent->h_aliases = + ares_malloc_zero_array(ancount + 1, sizeof(*hostent->h_aliases)); if (hostent->h_aliases == NULL) { status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ goto done; /* LCOV_EXCL_LINE: OutOfMemory */ } - memset(hostent->h_aliases, 0, (ancount + 1) * sizeof(*hostent->h_aliases)); for (i = 0; i < ancount; i++) { const ares_dns_rr_t *rr = diff -Nru c-ares-1.34.5/src/lib/legacy/ares_parse_ptr_reply.c c-ares-1.34.9/src/lib/legacy/ares_parse_ptr_reply.c --- c-ares-1.34.5/src/lib/legacy/ares_parse_ptr_reply.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/legacy/ares_parse_ptr_reply.c 2026-10-20 02:31:51.000000000 +0000 @@ -88,12 +88,12 @@ hostent->h_length = (HOSTENT_LENGTH_TYPE)addrlen; /* Preallocate the maximum number + 1 */ - hostent->h_aliases = ares_malloc((ancount + 1) * sizeof(*hostent->h_aliases)); + hostent->h_aliases = + ares_malloc_zero_array(ancount + 1, sizeof(*hostent->h_aliases)); if (hostent->h_aliases == NULL) { status = ARES_ENOMEM; goto done; } - memset(hostent->h_aliases, 0, (ancount + 1) * sizeof(*hostent->h_aliases)); /* Cycle through answers */ diff -Nru c-ares-1.34.5/src/lib/legacy/ares_parse_uri_reply.c c-ares-1.34.9/src/lib/legacy/ares_parse_uri_reply.c --- c-ares-1.34.5/src/lib/legacy/ares_parse_uri_reply.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/legacy/ares_parse_uri_reply.c 2026-10-20 02:31:51.000000000 +0000 @@ -90,7 +90,7 @@ uri_curr->priority = ares_dns_rr_get_u16(rr, ARES_RR_URI_PRIORITY); uri_curr->weight = ares_dns_rr_get_u16(rr, ARES_RR_URI_WEIGHT); uri_curr->uri = ares_strdup(ares_dns_rr_get_str(rr, ARES_RR_URI_TARGET)); - uri_curr->ttl = (int)ares_dns_rr_get_ttl(rr); + uri_curr->ttl = ares_dns_ttl_as_int(ares_dns_rr_get_ttl(rr)); if (uri_curr->uri == NULL) { status = ARES_ENOMEM; diff -Nru c-ares-1.34.5/src/lib/record/ares_dns_mapping.c c-ares-1.34.9/src/lib/record/ares_dns_mapping.c --- c-ares-1.34.5/src/lib/record/ares_dns_mapping.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/record/ares_dns_mapping.c 2026-10-20 02:31:51.000000000 +0000 @@ -132,6 +132,21 @@ return ARES_FALSE; } +ares_bool_t ares_dns_rec_allow_name_comp_read(ares_dns_rec_type_t type) +{ + /* RFC3597 section 4 recommends decompressing these older RR types on + * receipt, even though their RDATA names must not be compressed on write. + * The other listed types (RP, AFSDB, RT, PX, NXT) are not implemented. */ + switch (type) { + case ARES_REC_TYPE_SIG: + case ARES_REC_TYPE_SRV: + case ARES_REC_TYPE_NAPTR: + return ARES_TRUE; + default: + return ares_dns_rec_allow_name_comp(type); + } +} + ares_bool_t ares_dns_class_isvalid(ares_dns_class_t qclass, ares_dns_rec_type_t type, ares_bool_t is_query) @@ -147,7 +162,7 @@ switch (qclass) { case ARES_CLASS_IN: case ARES_CLASS_CHAOS: - case ARES_CLASS_HESOID: + case ARES_CLASS_HESIOD: case ARES_CLASS_NONE: return ARES_TRUE; case ARES_CLASS_ANY: @@ -228,7 +243,7 @@ case ARES_REC_TYPE_CAA: return "CAA"; case ARES_REC_TYPE_RAW_RR: - return "RAWRR"; + return "RAW_RR"; } return "UNKNOWN"; } @@ -240,7 +255,7 @@ return "IN"; case ARES_CLASS_CHAOS: return "CH"; - case ARES_CLASS_HESOID: + case ARES_CLASS_HESIOD: return "HS"; case ARES_CLASS_ANY: return "ANY"; @@ -670,7 +685,7 @@ } list[] = { { "IN", ARES_CLASS_IN }, { "CH", ARES_CLASS_CHAOS }, - { "HS", ARES_CLASS_HESOID }, + { "HS", ARES_CLASS_HESIOD }, { "NONE", ARES_CLASS_NONE }, { "ANY", ARES_CLASS_ANY }, { NULL, 0 } diff -Nru c-ares-1.34.5/src/lib/record/ares_dns_multistring.c c-ares-1.34.9/src/lib/record/ares_dns_multistring.c --- c-ares-1.34.5/src/lib/record/ares_dns_multistring.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/record/ares_dns_multistring.c 2026-10-20 02:31:51.000000000 +0000 @@ -227,7 +227,9 @@ strs->cache_str = (unsigned char *)ares_buf_finish_str(buf, &strs->cache_str_len); - if (strs->cache_str != NULL) { + if (strs->cache_str == NULL) { + ares_buf_destroy(buf); + } else { strs->cache_invalidated = ARES_FALSE; } *len = strs->cache_str_len; diff -Nru c-ares-1.34.5/src/lib/record/ares_dns_name.c c-ares-1.34.9/src/lib/record/ares_dns_name.c --- c-ares-1.34.5/src/lib/record/ares_dns_name.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/record/ares_dns_name.c 2026-10-20 02:31:51.000000000 +0000 @@ -25,6 +25,18 @@ */ #include "ares_private.h" +/* RFC 1035 3.1 limits a name to 255 octets. We track presentation length + * (label octets plus one separator before each label after the first), which is + * up to ~2 octets looser than the strict wire limit and so never rejects a + * compliant name. Shared by the read and write paths. */ +#define ARES_MAX_NAME_PRESENTATION_LEN 255 + +/* A name of <= 255 octets holds at most 128 labels, so it can never legitimately + * require more than that many compression-pointer jumps. Bounding the number of + * indirections stops a name built purely from pointers (which never adds label + * bytes, so the length cap never fires) from being walked without limit. */ +#define ARES_MAX_INDIRS 128 + typedef struct { char *name; size_t name_len; @@ -48,10 +60,18 @@ ares_nameoffset_t *off = NULL; if (list == NULL || name == NULL || ares_strlen(name) == 0 || - ares_strlen(name) > 255) { + ares_strlen(name) > ARES_MAX_NAME_PRESENTATION_LEN) { return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ } + /* Don't record a name as a compression target if it can't be referenced by + * a pointer. DNS name compression offsets are only 14 bits (RFC 1035 + * 4.1.4), so a name positioned beyond 0x3FFF would have its offset + * truncated when written, producing a pointer to the wrong location. */ + if (idx > 0x3FFF) { + return ARES_SUCCESS; + } + if (*list == NULL) { *list = ares_llist_create(ares_nameoffset_free); } @@ -65,7 +85,12 @@ return ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ } - off->name = ares_strdup(name); + off->name = ares_strdup(name); + if (off->name == NULL) { + status = ARES_ENOMEM; /* LCOV_EXCL_LINE: OutOfMemory */ + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + off->name_len = ares_strlen(off->name); off->idx = idx; @@ -335,7 +360,8 @@ } /* Can't exceed maximum (unescaped) length */ - if (ares_array_len(labels) && total_len + ares_array_len(labels) - 1 > 255) { + if (ares_array_len(labels) && + total_len + ares_array_len(labels) - 1 > ARES_MAX_NAME_PRESENTATION_LEN) { status = ARES_EBADNAME; goto done; } @@ -419,6 +445,9 @@ /* Output name compression offset jump */ if (off != NULL) { + /* off->idx is guaranteed to fit in 14 bits: ares_nameoffset_create() + * refuses to record offsets beyond 0x3FFF, so the mask below never + * actually truncates. */ unsigned short u16 = (unsigned short)0xC000 | (unsigned short)(off->idx & 0x3FFF); status = ares_buf_append_be16(buf, u16); @@ -531,13 +560,16 @@ } ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name, - ares_bool_t is_hostname) + ares_bool_t is_hostname, + ares_bool_t allow_compression) { size_t save_offset = 0; unsigned char c; ares_status_t status; ares_buf_t *namebuf = NULL; size_t label_start = ares_buf_get_position(buf); + size_t name_len = 0; + size_t indir = 0; if (buf == NULL) { return ARES_EFORMERR; @@ -588,6 +620,13 @@ */ size_t offset = (size_t)((c & 0x3F) << 8); + /* Some RR types (e.g. SVCB) do not permit name compression within their + * RDATA. Reject a compression pointer when it is not allowed. */ + if (!allow_compression) { + status = ARES_EBADNAME; + goto fail; + } + /* Fetch second byte of the redirect length */ status = ares_buf_fetch_bytes(buf, &c, 1); if (status != ARES_SUCCESS) { @@ -609,6 +648,16 @@ goto fail; } + /* Bound the number of indirections. A name made purely of pointers never + * adds label bytes, so the length cap below can't stop it; the + * strictly-decreasing rule alone still allows thousands of jumps per name. + * No legitimate <= 255 octet name needs more than 128 labels/jumps. */ + indir++; + if (indir > ARES_MAX_INDIRS) { + status = ARES_EBADNAME; + goto fail; + } + /* First time we make a jump, save the current position */ if (save_offset == 0) { save_offset = ares_buf_get_position(buf); @@ -632,6 +681,20 @@ /* New label */ + /* RFC 1035 3.1 limits a name to 255 octets. Enforce it during + * decompression so labels reached through a chain of pointers can't expand a + * single name without bound. Track the presentation length (label data plus + * the separator that precedes each label after the first), matching + * ares_split_dns_name() on the write side. */ + if (name_len) { + name_len++; + } + name_len += c; + if (name_len > ARES_MAX_NAME_PRESENTATION_LEN) { + status = ARES_EBADNAME; + goto fail; + } + /* Labels are separated by periods */ if (ares_buf_len(namebuf) != 0 && name != NULL) { status = ares_buf_append_byte(namebuf, '.'); diff -Nru c-ares-1.34.5/src/lib/record/ares_dns_parse.c c-ares-1.34.9/src/lib/record/ares_dns_parse.c --- c-ares-1.34.5/src/lib/record/ares_dns_parse.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/record/ares_dns_parse.c 2026-10-20 02:31:51.000000000 +0000 @@ -46,8 +46,11 @@ { ares_status_t status; char *name = NULL; + /* Receive-side exceptions are specified in RFC3597 section 4. */ + ares_bool_t allow_compression = + ares_dns_rec_allow_name_comp_read(ares_dns_rr_get_type(rr)); - status = ares_dns_name_parse(buf, &name, is_hostname); + status = ares_dns_name_parse(buf, &name, is_hostname, allow_compression); if (status != ARES_SUCCESS) { return status; } @@ -514,6 +517,7 @@ status = ares_dns_rr_set_opt_own(rr, ARES_RR_OPT_OPTIONS, opt, val, len); if (status != ARES_SUCCESS) { + ares_free(val); return status; } } @@ -607,6 +611,7 @@ status = ares_dns_rr_set_opt_own(rr, ARES_RR_SVCB_PARAMS, opt, val, len); if (status != ARES_SUCCESS) { + ares_free(val); return status; } } @@ -658,6 +663,7 @@ status = ares_dns_rr_set_opt_own(rr, ARES_RR_HTTPS_PARAMS, opt, val, len); if (status != ARES_SUCCESS) { + ares_free(val); return status; } } @@ -765,6 +771,12 @@ ares_status_t status; unsigned char *bytes = NULL; + /* Can't fail */ + status = ares_dns_rr_set_u16(rr, ARES_RR_RAW_RR_TYPE, raw_type); + if (status != ARES_SUCCESS) { + return status; + } + if (rdlength == 0) { return ARES_SUCCESS; } @@ -774,13 +786,6 @@ return status; } - /* Can't fail */ - status = ares_dns_rr_set_u16(rr, ARES_RR_RAW_RR_TYPE, raw_type); - if (status != ARES_SUCCESS) { - ares_free(bytes); - return status; - } - status = ares_dns_rr_set_bin_own(rr, ARES_RR_RAW_RR_DATA, bytes, rdlength); if (status != ARES_SUCCESS) { ares_free(bytes); @@ -920,30 +925,6 @@ (*dnsrec)->raw_rcode = rcode; - if (*ancount > 0) { - status = - ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ANSWER, *ancount); - if (status != ARES_SUCCESS) { - goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ - } - } - - if (*nscount > 0) { - status = - ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_AUTHORITY, *nscount); - if (status != ARES_SUCCESS) { - goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ - } - } - - if (*arcount > 0) { - status = - ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ADDITIONAL, *arcount); - if (status != ARES_SUCCESS) { - goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ - } - } - return ARES_SUCCESS; fail: @@ -1032,7 +1013,7 @@ */ /* Name */ - status = ares_dns_name_parse(buf, &name, ARES_FALSE); + status = ares_dns_name_parse(buf, &name, ARES_FALSE, ARES_TRUE); if (status != ARES_SUCCESS) { goto done; } @@ -1103,7 +1084,7 @@ */ /* Name */ - status = ares_dns_name_parse(buf, &name, ARES_FALSE); + status = ares_dns_name_parse(buf, &name, ARES_FALSE, ARES_TRUE); if (status != ARES_SUCCESS) { goto done; } @@ -1208,6 +1189,8 @@ ares_dns_record_t **dnsrec) { ares_status_t status; + size_t total_rr_count; + const size_t min_rr_wire_len = 11; unsigned short qdcount; unsigned short ancount; unsigned short nscount; @@ -1268,6 +1251,35 @@ } } + total_rr_count = (size_t)ancount + (size_t)nscount + (size_t)arcount; + if (total_rr_count > ares_buf_len(buf) / min_rr_wire_len) { + status = ARES_EBADRESP; + goto fail; + } + + if (ancount > 0) { + status = ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ANSWER, ancount); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + if (nscount > 0) { + status = + ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_AUTHORITY, nscount); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + + if (arcount > 0) { + status = + ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ADDITIONAL, arcount); + if (status != ARES_SUCCESS) { + goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + /* Parse Answers */ for (i = 0; i < ancount; i++) { status = ares_dns_parse_rr(buf, flags, ARES_SECTION_ANSWER, *dnsrec); @@ -1292,6 +1304,22 @@ } } + /* RFC 6891 6.1.1: a message MUST NOT contain more than one OPT RR. Reject + * any response carrying multiple OPT records in the additional section. */ + { + size_t rr_cnt = ares_dns_record_rr_cnt(*dnsrec, ARES_SECTION_ADDITIONAL); + size_t opt_cnt = 0; + size_t j; + for (j = 0; j < rr_cnt; j++) { + const ares_dns_rr_t *rr = + ares_dns_record_rr_get_const(*dnsrec, ARES_SECTION_ADDITIONAL, j); + if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT && ++opt_cnt > 1) { + status = ARES_EBADRESP; + goto fail; + } + } + } + /* Finalize rcode now that if we have OPT it is processed */ if (!ares_dns_rcode_isvalid((*dnsrec)->raw_rcode)) { (*dnsrec)->rcode = ARES_RCODE_SERVFAIL; diff -Nru c-ares-1.34.5/src/lib/record/ares_dns_private.h c-ares-1.34.9/src/lib/record/ares_dns_private.h --- c-ares-1.34.5/src/lib/record/ares_dns_private.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/record/ares_dns_private.h 2026-10-20 02:31:51.000000000 +0000 @@ -29,6 +29,10 @@ ares_status_t ares_dns_record_duplicate_ex(ares_dns_record_t **dest, const ares_dns_record_t *src); ares_bool_t ares_dns_rec_allow_name_comp(ares_dns_rec_type_t type); +/*! Whether an RR's RDATA names may be decompressed on receipt (RFC3597). + * \param[in] type Record type. + * \return ARES_TRUE if compressed RDATA names may be parsed. */ +ares_bool_t ares_dns_rec_allow_name_comp_read(ares_dns_rec_type_t type); ares_bool_t ares_dns_opcode_isvalid(ares_dns_opcode_t opcode); ares_bool_t ares_dns_rcode_isvalid(ares_dns_rcode_t rcode); ares_bool_t ares_dns_flags_arevalid(unsigned short flags); diff -Nru c-ares-1.34.5/src/lib/record/ares_dns_record.c c-ares-1.34.9/src/lib/record/ares_dns_record.c --- c-ares-1.34.5/src/lib/record/ares_dns_record.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/record/ares_dns_record.c 2026-10-20 02:31:51.000000000 +0000 @@ -929,7 +929,7 @@ ares_dns_datatype_t datatype = ares_dns_rr_key_datatype(key); ares_bool_t is_nullterm = (datatype == ARES_DATATYPE_ABINP) ? ARES_TRUE : ARES_FALSE; - size_t alloclen = is_nullterm ? len + 1 : len; + size_t alloclen; unsigned char *temp; ares_dns_multistring_t **strs; @@ -937,6 +937,15 @@ return ARES_EFORMERR; } + if (val == NULL && len != 0) { + return ARES_EFORMERR; + } + + if (is_nullterm && len == SIZE_MAX) { + return ARES_ENOMEM; + } + alloclen = is_nullterm ? len + 1 : len; + strs = ares_dns_rr_data_ptr(dns_rr, key, NULL); if (strs == NULL) { return ARES_EFORMERR; @@ -954,7 +963,9 @@ return ARES_ENOMEM; } - memcpy(temp, val, len); + if (len != 0) { + memcpy(temp, val, len); + } /* NULL-term ABINP */ if (is_nullterm) { @@ -1237,14 +1248,32 @@ (datatype == ARES_DATATYPE_BINP || datatype == ARES_DATATYPE_ABINP) ? ARES_TRUE : ARES_FALSE; - size_t alloclen = is_nullterm ? len + 1 : len; - unsigned char *temp = ares_malloc(alloclen); + size_t alloclen; + unsigned char *temp; + + if (datatype != ARES_DATATYPE_BIN && datatype != ARES_DATATYPE_BINP && + datatype != ARES_DATATYPE_ABINP) { + return ARES_EFORMERR; + } + + if (val == NULL && len != 0) { + return ARES_EFORMERR; + } + + if (is_nullterm && len == SIZE_MAX) { + return ARES_ENOMEM; + } + alloclen = is_nullterm ? len + 1 : len; + + temp = ares_malloc(alloclen); if (temp == NULL) { return ARES_ENOMEM; } - memcpy(temp, val, len); + if (len != 0) { + memcpy(temp, val, len); + } /* NULL-term BINP */ if (is_nullterm) { @@ -1400,12 +1429,23 @@ ares_status_t status; if (val != NULL) { - temp = ares_malloc(val_len + 1); + size_t alloclen; + + if (val_len == SIZE_MAX) { + return ARES_ENOMEM; + } + alloclen = val_len + 1; + + temp = ares_malloc(alloclen); if (temp == NULL) { return ARES_ENOMEM; } - memcpy(temp, val, val_len); + if (val_len != 0) { + memcpy(temp, val, val_len); + } temp[val_len] = 0; + } else if (val_len != 0) { + return ARES_EFORMERR; } status = ares_dns_rr_set_opt_own(dns_rr, key, opt, temp, val_len); diff -Nru c-ares-1.34.5/src/lib/record/ares_dns_write.c c-ares-1.34.9/src/lib/record/ares_dns_write.c --- c-ares-1.34.5/src/lib/record/ares_dns_write.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/record/ares_dns_write.c 2026-10-20 02:31:51.000000000 +0000 @@ -1074,6 +1074,11 @@ * position */ end_length = ares_buf_len(buf); rdlength = end_length - pos_len - 2; + if (rdlength > 65535) { + /* RDATA cannot be framed by the 16-bit rdlength; writing the truncated + * length would emit a structurally corrupt message. */ + return ARES_EBADQUERY; + } status = ares_buf_set_length(buf, pos_len); if (status != ARES_SUCCESS) { @@ -1132,6 +1137,13 @@ goto done; } + /* Maximum DNS message size is 64k, even over TCP; ares_dns_parse_buf() + * rejects anything larger */ + if (ares_buf_len(buf) - orig_len > 65535) { + status = ARES_EBADQUERY; + goto done; + } + done: ares_llist_destroy(namelist); if (status != ARES_SUCCESS) { @@ -1164,15 +1176,11 @@ /* Write message */ status = ares_dns_write_buf(dnsrec, buf); if (status != ARES_SUCCESS) { - goto done; /* LCOV_EXCL_LINE: OutOfMemory */ + goto done; } len = ares_buf_len(buf); msg_len = len - orig_len - 2; - if (msg_len > 65535) { - status = ARES_EBADQUERY; - goto done; - } /* Now we need to overwrite the length, so we jump back to the original * message length, overwrite the section and jump back */ diff -Nru c-ares-1.34.5/src/lib/str/ares_buf.c c-ares-1.34.9/src/lib/str/ares_buf.c --- c-ares-1.34.5/src/lib/str/ares_buf.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/str/ares_buf.c 2026-10-20 02:31:51.000000000 +0000 @@ -139,6 +139,7 @@ { size_t remaining_size; size_t alloc_size; + size_t total_required; unsigned char *ptr; if (buf == NULL) { @@ -151,9 +152,20 @@ /* When calling ares_buf_finish_str() we end up adding a null terminator, * so we want to ensure the size is always sufficient for this as we don't - * want an ARES_ENOMEM at that point */ + * want an ARES_ENOMEM at that point. + */ + if (buf->data_len >= SIZE_MAX - 1) { + return ARES_ENOMEM; + } + needed_size++; + if (needed_size > SIZE_MAX - buf->data_len) { + return ARES_ENOMEM; + } + + total_required = buf->data_len + needed_size; + /* No need to do an expensive move operation, we have enough to just append */ remaining_size = buf->alloc_buf_len - buf->data_len; if (remaining_size >= needed_size) { @@ -177,9 +189,11 @@ /* Increase allocation by powers of 2 */ do { - alloc_size <<= 1; - remaining_size = alloc_size - buf->data_len; - } while (remaining_size < needed_size); + if (alloc_size > SIZE_MAX >> 1) { + return ARES_ENOMEM; + } + alloc_size <<= 1; + } while (alloc_size < total_required); ptr = ares_realloc(buf->alloc_buf, alloc_size); if (ptr == NULL) { @@ -1111,7 +1125,7 @@ size_t processed_len = 0; ares_status_t status; - if (buf->alloc_buf == NULL || srch == NULL || srch_size == 0 || + if (buf == NULL || buf->alloc_buf == NULL || srch == NULL || srch_size == 0 || (rplc == NULL && rplc_size != 0)) { return ARES_EFORMERR; } @@ -1131,7 +1145,7 @@ /* Store the offset this was found because our actual pointer might be * switched out from under us by the call to ensure_space() if the * replacement pattern is larger than the search pattern */ - found_offset = (size_t)(ptr - (size_t)(buf->alloc_buf + buf->offset)); + found_offset = (size_t)(ptr - buf->alloc_buf) - buf->offset; if (rplc_size > srch_size) { status = ares_buf_ensure_space(buf, rplc_size - srch_size); if (status != ARES_SUCCESS) { @@ -1262,17 +1276,15 @@ } done: - if (status != ARES_SUCCESS) { - ares_buf_destroy(binbuf); + if (status == ARES_SUCCESS && bin != NULL) { + size_t mylen = 0; + /* NOTE: we use ares_buf_finish_str() here as we guarantee NULL + * Termination even though we are technically returning binary data. + */ + *bin = (unsigned char *)ares_buf_finish_str(binbuf, &mylen); + *bin_len = mylen; } else { - if (bin != NULL) { - size_t mylen = 0; - /* NOTE: we use ares_buf_finish_str() here as we guarantee NULL - * Termination even though we are technically returning binary data. - */ - *bin = (unsigned char *)ares_buf_finish_str(binbuf, &mylen); - *bin_len = mylen; - } + ares_buf_destroy(binbuf); } return status; @@ -1450,6 +1462,11 @@ } } + if (setvbuf(fp, NULL, _IONBF, 0) != 0) { + status = ARES_EFILE; /* LCOV_EXCL_LINE: DefensiveCoding */ + goto done; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + /* Get length portably, fstat() is POSIX, not C */ if (fseek(fp, 0, SEEK_END) != 0) { status = ARES_EFILE; /* LCOV_EXCL_LINE: DefensiveCoding */ diff -Nru c-ares-1.34.5/src/lib/str/ares_str.c c-ares-1.34.9/src/lib/str/ares_str.c --- c-ares-1.34.5/src/lib/str/ares_str.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/str/ares_str.c 2026-10-20 02:31:51.000000000 +0000 @@ -28,6 +28,9 @@ #include "ares_private.h" #include "ares_str.h" +#include +#include + #ifdef HAVE_STDINT_H # include #endif @@ -125,6 +128,57 @@ return ARES_TRUE; } +ares_bool_t ares_str_parse_uint(const char *str, unsigned long max, + unsigned int *out) +{ + char *end = NULL; + unsigned long val; + + /* Require a leading digit so strtoul()'s tolerance of a sign or leading + * whitespace (e.g. "-1" wrapping to UINT_MAX) can't slip through here. This + * also rejects NULL and empty strings. */ + if (str == NULL || out == NULL || !ares_isdigit(*str)) { + return ARES_FALSE; + } + + /* out is unsigned int, so a max above UINT_MAX would let strtoul's result + * truncate silently on assignment. Cap it. */ + if (max > UINT_MAX) { + max = UINT_MAX; + } + + errno = 0; + val = strtoul(str, &end, 10); + if (errno == ERANGE || *end != '\0' || val > max) { + return ARES_FALSE; + } + + *out = (unsigned int)val; + return ARES_TRUE; +} + +ares_bool_t ares_parse_port(const char *str, unsigned short *port, + ares_bool_t allow_zero) +{ + unsigned int val; + + if (port == NULL) { + return ARES_FALSE; + } + + if (!ares_str_parse_uint(str, 65535UL, &val)) { + return ARES_FALSE; + } + + if (!allow_zero && val == 0) { + return ARES_FALSE; + } + + *port = (unsigned short)val; + + return ARES_TRUE; +} + ares_bool_t ares_str_isalnum(const char *str) { size_t i; diff -Nru c-ares-1.34.5/src/lib/util/ares_iface_ips.c c-ares-1.34.9/src/lib/util/ares_iface_ips.c --- c-ares-1.34.5/src/lib/util/ares_iface_ips.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/util/ares_iface_ips.c 2026-10-20 02:31:51.000000000 +0000 @@ -25,6 +25,8 @@ */ #include "ares_private.h" +#include + #ifdef USE_WINSOCK # include # include @@ -256,12 +258,12 @@ const ares_iface_ip_t *ip; if (ips == NULL) { - return 0; + return ARES_IFACE_IP_NONE; } ip = ares_array_at_const(ips->ips, idx); if (ip == NULL) { - return 0; + return ARES_IFACE_IP_NONE; } return ip->flags; @@ -329,6 +331,8 @@ static ares_bool_t name_match(const char *name, const char *adapter_name, unsigned int ll_scope) { + unsigned int scope; + if (name == NULL || *name == 0) { return ARES_TRUE; } @@ -337,7 +341,7 @@ return ARES_TRUE; } - if (ares_str_isnum(name) && (unsigned int)atoi(name) == ll_scope) { + if (ares_str_parse_uint(name, UINT_MAX, &scope) && scope == ll_scope) { return ARES_TRUE; } @@ -376,7 +380,7 @@ for (address = addresses; address != NULL; address = address->Next) { IP_ADAPTER_UNICAST_ADDRESS *ipaddr = NULL; - ares_iface_ip_flags_t addrflag = 0; + ares_iface_ip_flags_t addrflag = ARES_IFACE_IP_NONE; char ifname[64] = ""; # if defined(HAVE_CONVERTINTERFACEINDEXTOLUID) && \ @@ -477,7 +481,7 @@ } for (ifa = ifap; ifa != NULL; ifa = ifa->ifa_next) { - ares_iface_ip_flags_t addrflag = 0; + ares_iface_ip_flags_t addrflag = ARES_IFACE_IP_NONE; struct ares_addr addr; unsigned char netmask = 0; unsigned int ll_scope = 0; @@ -500,20 +504,28 @@ addr.family = AF_INET; memcpy(&addr.addr.addr4, &sockaddr_in->sin_addr, sizeof(addr.addr.addr4)); /* netmask */ - sockaddr_in = (struct sockaddr_in *)((void *)ifa->ifa_netmask); - netmask = count_addr_bits((const void *)&sockaddr_in->sin_addr, 4); + if (ifa->ifa_netmask != NULL) { + sockaddr_in = (struct sockaddr_in *)((void *)ifa->ifa_netmask); + netmask = count_addr_bits((const void *)&sockaddr_in->sin_addr, 4); + } else { + netmask = 32; + } } else if (ifa->ifa_addr->sa_family == AF_INET6) { const struct sockaddr_in6 *sockaddr_in6 = (const struct sockaddr_in6 *)((void *)ifa->ifa_addr); addr.family = AF_INET6; memcpy(&addr.addr.addr6, &sockaddr_in6->sin6_addr, sizeof(addr.addr.addr6)); - /* netmask */ - sockaddr_in6 = (struct sockaddr_in6 *)((void *)ifa->ifa_netmask); - netmask = count_addr_bits((const void *)&sockaddr_in6->sin6_addr, 16); # ifdef HAVE_STRUCT_SOCKADDR_IN6_SIN6_SCOPE_ID ll_scope = sockaddr_in6->sin6_scope_id; # endif + /* netmask */ + if (ifa->ifa_netmask != NULL) { + sockaddr_in6 = (struct sockaddr_in6 *)((void *)ifa->ifa_netmask); + netmask = count_addr_bits((const void *)&sockaddr_in6->sin6_addr, 16); + } else { + netmask = 128; + } } else { /* unknown */ continue; diff -Nru c-ares-1.34.5/src/lib/util/ares_iface_ips.h c-ares-1.34.9/src/lib/util/ares_iface_ips.h --- c-ares-1.34.5/src/lib/util/ares_iface_ips.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/util/ares_iface_ips.h 2026-10-20 02:31:51.000000000 +0000 @@ -28,6 +28,7 @@ /*! Flags for interface ip addresses. */ typedef enum { + ARES_IFACE_IP_NONE = 0, /*!< No flags set / unspecified */ ARES_IFACE_IP_V4 = 1 << 0, /*!< IPv4 address. During enumeration if * this flag is set ARES_IFACE_IP_V6 * is not, will only enumerate v4 diff -Nru c-ares-1.34.5/src/lib/util/ares_math.c c-ares-1.34.9/src/lib/util/ares_math.c --- c-ares-1.34.5/src/lib/util/ares_math.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/util/ares_math.c 2026-10-20 02:31:51.000000000 +0000 @@ -25,6 +25,7 @@ */ #include "ares_private.h" +#include /* Uses public domain code snippets from * http://graphics.stanford.edu/~seander/bithacks.html */ @@ -42,7 +43,7 @@ return n; } -static ares_int64_t ares_round_up_pow2_u64(ares_int64_t n) +static ares_uint64_t ares_round_up_pow2_u64(ares_uint64_t n) { /* NOTE: if already a power of 2, will return itself, not the next */ n--; @@ -73,7 +74,7 @@ size_t ares_round_up_pow2(size_t n) { if (ares_is_64bit()) { - return (size_t)ares_round_up_pow2_u64((ares_int64_t)n); + return (size_t)ares_round_up_pow2_u64((ares_uint64_t)n); } return (size_t)ares_round_up_pow2_u32((unsigned int)n); @@ -156,3 +157,17 @@ static const unsigned char lookup[256] = { B6(0), B6(1), B6(1), B6(2) }; return lookup[x]; } + +ares_bool_t ares_size_t_mul_overflow(size_t a, size_t b, size_t *res) +{ + if (a > 0 && b > SIZE_MAX / a) { + return ARES_TRUE; + } + *res = a * b; + return ARES_FALSE; +} + +int ares_dns_ttl_as_int(unsigned int ttl) +{ + return (ttl > INT_MAX) ? INT_MAX : (int)ttl; +} diff -Nru c-ares-1.34.5/src/lib/util/ares_math.h c-ares-1.34.9/src/lib/util/ares_math.h --- c-ares-1.34.5/src/lib/util/ares_math.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/util/ares_math.h 2026-10-20 02:31:51.000000000 +0000 @@ -26,13 +26,21 @@ #ifndef __ARES_MATH_H #define __ARES_MATH_H -#ifdef _MSC_VER -typedef __int64 ares_int64_t; -typedef unsigned __int64 ares_uint64_t; -#else -typedef long long ares_int64_t; -typedef unsigned long long ares_uint64_t; -#endif +#include "ares_setup.h" + +#ifdef HAVE_STDINT_H +# include + typedef int64_t ares_int64_t; + typedef uint64_t ares_uint64_t; +#else /* HAVE_STDINT_H */ +# ifdef _MSC_VER + typedef __int64 ares_int64_t; + typedef unsigned __int64 ares_uint64_t; +# else + typedef long long ares_int64_t; + typedef unsigned long long ares_uint64_t; +# endif /* _MSC_VER */ +#endif /* HAVE_STDINT_H */ ares_bool_t ares_is_64bit(void); size_t ares_round_up_pow2(size_t n); @@ -42,4 +50,21 @@ size_t ares_count_hexdigits(size_t n); unsigned char ares_count_bits_u8(unsigned char x); +/*! Convert a DNS record TTL to the signed int used by the public API + * structures, saturating at INT_MAX instead of wrapping negative. + * + * RFC 8767 section 4 (which updates RFC 2181 section 8) defines the TTL as + * an unsigned 32-bit number, so a value with the high bit set is a large + * positive TTL, not a negative one. + * + * \param[in] ttl TTL as received on the wire + * \return TTL clamped to the range 0 to INT_MAX + */ +int ares_dns_ttl_as_int(unsigned int ttl); + +/*! Multiply two size_t values, checking for overflow. On success writes the + * product to *res and returns ARES_FALSE. On overflow returns ARES_TRUE and + * leaves *res untouched. */ +ares_bool_t ares_size_t_mul_overflow(size_t a, size_t b, size_t *res); + #endif diff -Nru c-ares-1.34.5/src/lib/util/ares_rand.c c-ares-1.34.9/src/lib/util/ares_rand.c --- c-ares-1.34.5/src/lib/util/ares_rand.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/util/ares_rand.c 2026-10-20 02:31:51.000000000 +0000 @@ -56,7 +56,7 @@ { /* LCOV_EXCL_START: FallbackCode */ if (ares_is_64bit()) { - return (unsigned int)((((ares_uint64_t)addr >> 32) & 0xFFFFFFFF) | + return (unsigned int)((((ares_uint64_t)addr >> 32) & 0xFFFFFFFF) ^ ((ares_uint64_t)addr & 0xFFFFFFFF)); } return (unsigned int)((size_t)addr & 0xFFFFFFFF); @@ -94,12 +94,12 @@ len += sizeof(data); ares_tvnow(&tv); - data = (unsigned int)((tv.sec | tv.usec) & 0xFFFFFFFF); + data = (unsigned int)((tv.sec ^ tv.usec) & 0xFFFFFFFF); memcpy(key + len, &data, sizeof(data)); len += sizeof(data); - srand(ares_u32_from_ptr(rc4_state) | ares_u32_from_ptr(&i) | - (unsigned int)((tv.sec | tv.usec) & 0xFFFFFFFF)); + srand(ares_u32_from_ptr(rc4_state) ^ ares_u32_from_ptr(&i) ^ + (unsigned int)((tv.sec ^ tv.usec) & 0xFFFFFFFF)); #endif for (i = len; i < key_len; i++) { diff -Nru c-ares-1.34.5/src/lib/util/ares_threads.c c-ares-1.34.9/src/lib/util/ares_threads.c --- c-ares-1.34.5/src/lib/util/ares_threads.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/util/ares_threads.c 2026-10-20 02:31:51.000000000 +0000 @@ -563,8 +563,7 @@ if (timeout_ms >= 0) { ares_tvnow(&tout); - tout.sec += (ares_int64_t)(timeout_ms / 1000); - tout.usec += (unsigned int)(timeout_ms % 1000) * 1000; + ares_timeval_add(&tout, (size_t)timeout_ms); } ares_thread_mutex_lock(channel->lock); diff -Nru c-ares-1.34.5/src/lib/util/ares_time.h c-ares-1.34.9/src/lib/util/ares_time.h --- c-ares-1.34.5/src/lib/util/ares_time.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/util/ares_time.h 2026-10-20 02:31:51.000000000 +0000 @@ -39,6 +39,7 @@ const ares_timeval_t *check); void ares_tvnow(ares_timeval_t *now); +void ares_timeval_add(ares_timeval_t *now, size_t millisecs); void ares_timeval_remaining(ares_timeval_t *remaining, const ares_timeval_t *now, const ares_timeval_t *tout); diff -Nru c-ares-1.34.5/src/lib/util/ares_uri.c c-ares-1.34.9/src/lib/util/ares_uri.c --- c-ares-1.34.5/src/lib/util/ares_uri.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/lib/util/ares_uri.c 2026-10-20 02:31:51.000000000 +0000 @@ -533,7 +533,7 @@ status = ares_buf_split_str_array(inpath, (const unsigned char *)"/", 1, ARES_BUF_SPLIT_TRIM, 0, &arr); if (status != ARES_SUCCESS) { - return NULL; + goto done; } for (i = 0; i < (ares_ssize_t)ares_array_len(arr); i++) { @@ -1137,6 +1137,11 @@ /* Consume : */ ares_buf_consume(buf, 1); + } else { + /* The ':' is absent or belongs to the host:port that follows the '@', but + * the search above has already consumed up to it, so rewind to the start + * of the userinfo */ + ares_buf_tag_rollback(buf); } ares_buf_tag(buf); @@ -1174,6 +1179,7 @@ unsigned char b; char host[256]; char port[6]; + unsigned short parsed_port; size_t len; ares_status_t status; @@ -1242,11 +1248,11 @@ } port[len] = 0; - if (!ares_str_isnum(port)) { + if (!ares_parse_port(port, &parsed_port, ARES_TRUE)) { return ARES_EBADSTR; } - status = ares_uri_set_port(uri, (unsigned short)atoi(port)); + status = ares_uri_set_port(uri, parsed_port); if (status != ARES_SUCCESS) { return status; } diff -Nru c-ares-1.34.5/src/tools/Makefile.in c-ares-1.34.9/src/tools/Makefile.in --- c-ares-1.34.5/src/tools/Makefile.in 2025-04-08 12:12:31.000000000 +0000 +++ c-ares-1.34.9/src/tools/Makefile.in 2026-10-20 02:31:57.000000000 +0000 @@ -106,7 +106,6 @@ $(top_srcdir)/m4/ax_code_coverage.m4 \ $(top_srcdir)/m4/ax_compiler_vendor.m4 \ $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ - $(top_srcdir)/m4/ax_cxx_compile_stdcxx_14.m4 \ $(top_srcdir)/m4/ax_file_escapes.m4 \ $(top_srcdir)/m4/ax_pthread.m4 \ $(top_srcdir)/m4/ax_require_defined.m4 \ @@ -269,10 +268,13 @@ GENHTML = @GENHTML@ GMOCK112_CFLAGS = @GMOCK112_CFLAGS@ GMOCK112_LIBS = @GMOCK112_LIBS@ +GMOCK117_CFLAGS = @GMOCK117_CFLAGS@ +GMOCK117_LIBS = @GMOCK117_LIBS@ GMOCK_CFLAGS = @GMOCK_CFLAGS@ GMOCK_LIBS = @GMOCK_LIBS@ GREP = @GREP@ HAVE_CXX14 = @HAVE_CXX14@ +HAVE_CXX17 = @HAVE_CXX17@ INSTALL = @INSTALL@ INSTALL_DATA = @INSTALL_DATA@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ diff -Nru c-ares-1.34.5/src/tools/adig.c c-ares-1.34.9/src/tools/adig.c --- c-ares-1.34.5/src/tools/adig.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/tools/adig.c 2026-10-20 02:31:51.000000000 +0000 @@ -1162,12 +1162,12 @@ /* skip prefix */ if (dig_options[opt].prefix != 0) { nameptr++; - } - - /* Negated option if it has a 'no' prefix */ - if (ares_streq_max(nameptr, "no", 2)) { - is_true = ARES_FALSE; - nameptr += 2; + /* Negated option if it has a 'no' prefix */ + if (dig_options[opt].prefix == '+' && + ares_streq_max(nameptr, "no", 2)) { + is_true = ARES_FALSE; + nameptr += 2; + } } if (dig_options[opt].separator != 0) { diff -Nru c-ares-1.34.5/src/tools/ahost.c c-ares-1.34.9/src/tools/ahost.c --- c-ares-1.34.5/src/tools/ahost.c 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/src/tools/ahost.c 2026-10-20 02:31:51.000000000 +0000 @@ -44,6 +44,13 @@ #include "ares_str.h" +#define RV_OK 0 /* Success */ +#define RV_SYSERR 1 /* Internal system failure */ +#define RV_MISUSE 2 /* Misuse (command line) */ +#define RV_FAIL 3 /* Resolution failure */ +static int final_rv = RV_OK; + + static void callback(void *arg, int status, int timeouts, struct hostent *host); static void ai_callback(void *arg, int status, int timeouts, struct ares_addrinfo *result); diff -Nru c-ares-1.34.5/test/CMakeLists.txt c-ares-1.34.9/test/CMakeLists.txt --- c-ares-1.34.5/test/CMakeLists.txt 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/CMakeLists.txt 2026-10-20 02:31:51.000000000 +0000 @@ -1,6 +1,8 @@ # Copyright (C) The c-ares project and its contributors # SPDX-License-Identifier: MIT +enable_language(CXX) + # Get rid of: warning C4530: C++ exception handler used, but unwind semantics are not enabled. Specify /EHsc IF (MSVC) SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /EHsc") @@ -50,6 +52,7 @@ ELSE () target_link_libraries(arestest PRIVATE caresinternal GTest::gmock) ENDIF() +target_compile_features(arestest PRIVATE cxx_std_14) target_compile_definitions(arestest PRIVATE CARES_NO_DEPRECATED) IF (CARES_BUILD_CONTAINER_TESTS) @@ -75,6 +78,7 @@ add_executable(dnsdump ${DUMPSOURCES}) target_compile_definitions(dnsdump PRIVATE CARES_NO_DEPRECATED) +target_compile_features(dnsdump PRIVATE cxx_std_14) target_link_libraries(dnsdump PRIVATE caresinternal) # Avoid "fatal error C1041: cannot open program database" due to multiple # targets trying to use the same PDB. /FS does NOT resolve this issue. diff -Nru c-ares-1.34.5/test/Makefile.in c-ares-1.34.9/test/Makefile.in --- c-ares-1.34.5/test/Makefile.in 2025-04-08 12:12:31.000000000 +0000 +++ c-ares-1.34.9/test/Makefile.in 2026-10-20 02:31:57.000000000 +0000 @@ -108,7 +108,6 @@ $(top_srcdir)/m4/ax_code_coverage.m4 \ $(top_srcdir)/m4/ax_compiler_vendor.m4 \ $(top_srcdir)/m4/ax_cxx_compile_stdcxx.m4 \ - $(top_srcdir)/m4/ax_cxx_compile_stdcxx_14.m4 \ $(top_srcdir)/m4/ax_file_escapes.m4 \ $(top_srcdir)/m4/ax_pthread.m4 \ $(top_srcdir)/m4/ax_require_defined.m4 \ @@ -552,10 +551,13 @@ GENHTML = @GENHTML@ GMOCK112_CFLAGS = @GMOCK112_CFLAGS@ GMOCK112_LIBS = @GMOCK112_LIBS@ +GMOCK117_CFLAGS = @GMOCK117_CFLAGS@ +GMOCK117_LIBS = @GMOCK117_LIBS@ GMOCK_CFLAGS = @GMOCK_CFLAGS@ GMOCK_LIBS = @GMOCK_LIBS@ GREP = @GREP@ HAVE_CXX14 = @HAVE_CXX14@ +HAVE_CXX17 = @HAVE_CXX17@ INSTALL = @INSTALL@ INSTALL_DATA = @INSTALL_DATA@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ diff -Nru c-ares-1.34.5/test/ares-test-init.cc c-ares-1.34.9/test/ares-test-init.cc --- c-ares-1.34.5/test/ares-test-init.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-init.cc 2026-10-20 02:31:51.000000000 +0000 @@ -25,6 +25,8 @@ */ #include "ares-test.h" +#include + extern "C" { #include "ares_private.h" } @@ -153,6 +155,34 @@ ares_destroy(channel2); } +TEST_F(LibraryTest, DefaultQueryCacheNotSaved) { + struct ares_options opts; + struct ares_options saved_opts; + ares_channel_t *channel = nullptr; + int optmask = ARES_OPT_TIMEOUTMS; + int saved_optmask = 0; + + memset(&opts, 0, sizeof(opts)); + opts.timeout = 1000; + ASSERT_EQ(ARES_SUCCESS, ares_init_options(&channel, &opts, optmask)); + ASSERT_NE(nullptr, channel); + + /* ares_save_options() uses the optmask to avoid writing fields that callers + * built against an older, smaller ares_options structure cannot hold. A + * default value therefore must not be represented as a caller-set option. */ + EXPECT_EQ(3600U, channel->qcache_max_ttl); + EXPECT_EQ(0U, channel->optmask & ARES_OPT_QUERY_CACHE); + + memset(&saved_opts, 0, sizeof(saved_opts)); + EXPECT_EQ(ARES_SUCCESS, + ares_save_options(channel, &saved_opts, &saved_optmask)); + EXPECT_EQ(0, saved_optmask & ARES_OPT_QUERY_CACHE); + EXPECT_EQ(opts.timeout, saved_opts.timeout); + + ares_destroy_options(&saved_opts); + ares_destroy(channel); +} + TEST_F(LibraryTest, ChannelAllocFail) { ares_channel_t *channel; for (int ii = 1; ii <= 25; ii++) { @@ -267,6 +297,91 @@ ares_library_cleanup(); } +// CARES_THREADS implies ares_threadsafety() is true +#ifdef CARES_THREADS +// Build a CSV list of n distinct IPv4 servers. +static std::string ConfigRaceServerCsv(size_t n) +{ + std::string csv; + for (size_t i = 0; i < n; i++) { + if (i != 0) { + csv += ","; + } + csv += "127.0." + std::to_string((i / 250) + 1) + "." + + std::to_string((i % 250) + 1) + ":5353"; + } + return csv; +} + +// Readers snapshot the channel configuration (via ares_dup() or +// ares_save_options()) while a writer thread keeps swapping the server list +// between a short and a long one. The configuration must only be read under +// the channel lock; otherwise the saved server array is sized from one list +// and filled from another (heap-buffer-overflow under ASAN). +static void ConfigRaceRun(bool use_dup, size_t reader_loops) +{ + const size_t nreaders = 4; + const std::string small_csv = ConfigRaceServerCsv(4); + const std::string large_csv = ConfigRaceServerCsv(400); + ares_channel_t *channel = nullptr; + std::atomic readers_done(0); + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&channel, nullptr, 0)); + ASSERT_NE(nullptr, channel); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(channel, small_csv.c_str())); + + // Writer runs until every reader finished; readers are bounded so the + // whole test is bounded. + std::thread writer([&]() { + size_t i = 0; + while (readers_done.load() < nreaders) { + ares_set_servers_csv(channel, + (i++ & 1) ? large_csv.c_str() : small_csv.c_str()); + } + }); + + std::vector readers; + for (size_t r = 0; r < nreaders; r++) { + readers.emplace_back([&]() { + for (size_t i = 0; i < reader_loops; i++) { + if (use_dup) { + ares_channel_t *dup = nullptr; + EXPECT_EQ(ARES_SUCCESS, ares_dup(&dup, channel)); + if (dup != nullptr) { + ares_destroy(dup); + } + } else { + struct ares_options opts; + int optmask = 0; + memset(&opts, 0, sizeof(opts)); + EXPECT_EQ(ARES_SUCCESS, ares_save_options(channel, &opts, &optmask)); + EXPECT_NE(0, optmask & ARES_OPT_SERVERS); + EXPECT_TRUE(opts.nservers == 4 || opts.nservers == 400); + ares_destroy_options(&opts); + } + } + readers_done++; + }); + } + + for (auto &t : readers) { + t.join(); + } + writer.join(); + ares_destroy(channel); +} + +TEST_F(LibraryTest, DupConcurrentSetServers) +{ + ConfigRaceRun(true, 20); +} + +TEST_F(LibraryTest, SaveOptionsConcurrentSetServers) +{ + ConfigRaceRun(false, 20000); +} +#endif + #ifndef WIN32 TEST_F(LibraryTest, EnvInit) { ares_channel_t *channel = nullptr; @@ -276,6 +391,25 @@ ares_destroy(channel); } +TEST_F(LibraryTest, EnvInitBogusAttempts) +{ + ares_channel_t *channel = nullptr; + EnvValue v1("RES_OPTIONS", "options attempts:4294967295"); + EXPECT_EQ(ARES_SUCCESS, ares_init(&channel)); + + channel->optmask |= ARES_OPT_TRIES; + + struct ares_options opts; + memset(&opts, 0, sizeof(opts)); + int optmask = 0; + EXPECT_EQ(ARES_SUCCESS, ares_save_options(channel, &opts, &optmask)); + /* The absurd attempt count must be ignored rather than turned into an + * effectively unbounded retry loop. */ + EXPECT_EQ(3, opts.tries); + + ares_destroy(channel); +} + TEST_F(LibraryTest, EnvInitModernOptions) { ares_channel_t *channel = nullptr; EnvValue v1("LOCALDOMAIN", "this.is.local"); @@ -521,6 +655,24 @@ ares_destroy(channel); return HasFailure(); +} + +// AIX /etc/netsvc.conf uses address-family-suffixed tokens (bind4/bind6, +// local4/local6). Ensure these are recognized rather than dropped. +NameContentList netsvcconf = { + {"/etc/resolv.conf", "nameserver 1.2.3.4\n" + "search first.com second.com\n"}, + {"/etc/netsvc.conf", "hosts = bind4, local4\n"}}; +CONTAINED_TEST_F(LibraryTest, ContainerNetsvcConfAIXSuffix, + "myhostname", "mydomainname.org", netsvcconf) { + ares_channel_t *channel = nullptr; + EXPECT_EQ(ARES_SUCCESS, ares_init(&channel)); + + /* "bf" (not the default "fb") proves the bind4/local4 tokens were parsed. */ + EXPECT_EQ(std::string("bf"), std::string(channel->lookups)); + + ares_destroy(channel); + return HasFailure(); } NameContentList malformedresolvconflookup = { diff -Nru c-ares-1.34.5/test/ares-test-internal.cc c-ares-1.34.9/test/ares-test-internal.cc --- c-ares-1.34.5/test/ares-test-internal.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-internal.cc 2026-10-20 02:31:51.000000000 +0000 @@ -26,6 +26,8 @@ #include "ares-test.h" #include "dns-proto.h" +#include +#include #include #ifdef HAVE_UNISTD_H @@ -85,6 +87,40 @@ } } +TEST_F(LibraryTest, StrParseUint) { + unsigned int v = 42; + + EXPECT_TRUE(ares_str_parse_uint("0", 128, &v)); + EXPECT_EQ(0u, v); + EXPECT_TRUE(ares_str_parse_uint("128", 128, &v)); + EXPECT_EQ(128u, v); + + // Exactly UINT_MAX is accepted (the interesting boundary: on a 32-bit + // unsigned long it parses to ULONG_MAX without ERANGE). + EXPECT_TRUE(ares_str_parse_uint("4294967295", UINT_MAX, &v)); + EXPECT_EQ(UINT_MAX, v); + + // max == 0 permits only "0". + EXPECT_TRUE(ares_str_parse_uint("0", 0, &v)); + EXPECT_EQ(0u, v); + + // Failure leaves *out untouched. + v = 42; + EXPECT_FALSE(ares_str_parse_uint("1", 0, &v)); + EXPECT_EQ(42u, v); + EXPECT_FALSE(ares_str_parse_uint("129", 128, &v)); + EXPECT_EQ(42u, v); + + EXPECT_FALSE(ares_str_parse_uint("4294967296", UINT_MAX, &v)); // over on LP64 + EXPECT_FALSE(ares_str_parse_uint("12x", UINT_MAX, &v)); // trailing + EXPECT_FALSE(ares_str_parse_uint("0x10", UINT_MAX, &v)); // trailing hex + EXPECT_FALSE(ares_str_parse_uint(" 5", UINT_MAX, &v)); // leading space + EXPECT_FALSE(ares_str_parse_uint("+5", UINT_MAX, &v)); // leading sign + EXPECT_FALSE(ares_str_parse_uint("-1", UINT_MAX, &v)); // sign wrap + EXPECT_FALSE(ares_str_parse_uint("", UINT_MAX, &v)); + EXPECT_FALSE(ares_str_parse_uint(NULL, UINT_MAX, &v)); +} + void CheckPtoN4(int size, unsigned int value, const char *input) { struct in_addr a4; a4.s_addr = 0; @@ -238,6 +274,23 @@ EXPECT_EQ(-1, ares_inet_net_pton(AF_INET, "253", &a4, sizeof(a4) - 1)); } +// An IPv6 network with a prefix length only carries (bits+7)/8 bytes; the +// rest of the destination must be zeroed rather than left as it was. +TEST_F(LibraryTest, InetNetPtoNPrefixZeroFills) { + unsigned char a6[16]; + + memset(a6, 0x41, sizeof(a6)); + EXPECT_EQ(0, ares_inet_net_pton(AF_INET6, "::1/0", a6, sizeof(a6))); + EXPECT_EQ("0000:0000:0000:0000:0000:0000:0000:0000", + AddressToString(a6, 16)); + + memset(a6, 0x41, sizeof(a6)); + EXPECT_EQ(64, ares_inet_net_pton(AF_INET6, "2001:db8:1:2:3:4:5:6/64", a6, + sizeof(a6))); + EXPECT_EQ("2001:0db8:0001:0002:0000:0000:0000:0000", + AddressToString(a6, 16)); +} + TEST_F(LibraryTest, FreeLongChain) { struct ares_addr_node *data = nullptr; for (int ii = 0; ii < 100000; ii++) { @@ -377,6 +430,9 @@ { ARES_TRUE, "https://user%25:password@www.example.com", NULL }, { ARES_TRUE, "https://user:password%25@www.example.com", NULL }, { ARES_TRUE, "https://user@www.example.com", NULL }, + { ARES_TRUE, "https://user@www.example.com:8443", NULL }, + { ARES_TRUE, "dns://user@[fe80::1]:53", NULL }, + { ARES_FALSE, "https://user@www.example.com:8443@evil.example.com", NULL }, /* ambiguous authority, '@' not valid in host */ { ARES_TRUE, "https://www.example.com/path", NULL }, { ARES_TRUE, "https://www.example.com/path/", NULL }, { ARES_TRUE, "https://www.example.com/a/../", "https://www.example.com/" }, @@ -466,6 +522,95 @@ EXPECT_NE(ARES_SUCCESS, ares_uri_parse_buf(NULL, NULL)); EXPECT_NE(ARES_SUCCESS, ares_uri_parse_buf(NULL, NULL)); } + +// The cache key must distinguish requests whose answers are not +// interchangeable. Two shapes are easy to get wrong because names reach the +// key unescaped: a request with two questions versus one whose name spells out +// the second, and two requests with the same question count whose names can be +// made to run together. Both are narrow to reach -- they need a hand-built +// record with more than one question, which real servers reject -- but the key +// asserts they are distinct, so pin it. +TEST_F(LibraryTest, QcacheKeyDistinguishesQuestions) { + struct qcase { + const char *n1; + const char *n2; /* nullptr for a single-question record */ + }; + const qcase cases[] = { + { "one.example.net", "two.example.net" }, + { "one.example.net|A|IN|two.example.net", nullptr }, + { "one.example.net", "two.example.net|A|IN|three.example.net" }, + { "one.example.net|A|IN|two.example.net", "three.example.net" }, + }; + std::vector keys; + + for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + ares_dns_record_t *req = nullptr; + char *key = nullptr; + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&req, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req, cases[i].n1, ARES_REC_TYPE_A, + ARES_CLASS_IN)); + if (cases[i].n2 != nullptr) { + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req, cases[i].n2, ARES_REC_TYPE_A, + ARES_CLASS_IN)); + } + + key = ares_qcache_calc_key(req); + ASSERT_NE(nullptr, key); + keys.push_back(std::string(key)); + ares_free_string(key); + ares_dns_record_destroy(req); + } + + /* Every one of these is a different request, so every key must differ. */ + for (size_t i = 0; i < keys.size(); i++) { + for (size_t j = i + 1; j < keys.size(); j++) { + EXPECT_NE(keys[i], keys[j]) + << "cache key collision between case " << i << " and case " << j + << ": " << keys[i]; + } + } +} + +TEST_F(LibraryTest, SysConfigResolvInlineComments) +{ + const char resolvconf[] = "nameserver 1.2.3.4 # managed nameserver\n" + "nameserver 2.3.4.5 ; managed nameserver\n" + "search first.com second.com # ignored.example\n" + "lookup bind ; file\n" + "options ndots:5 ; ndots:9\n"; + ares_sysconfig_t sysconfig; + ares_channel_t *channel = nullptr; + ares_buf_t *buf; + + memset(&sysconfig, 0, sizeof(sysconfig)); + buf = ares_buf_create_const((const unsigned char *)resolvconf, + sizeof(resolvconf) - 1); + ASSERT_NE(nullptr, buf); + ASSERT_EQ(ARES_SUCCESS, ares_init(&channel)); + + EXPECT_EQ(ARES_SUCCESS, + ares_sysconfig_process_buf(channel, &sysconfig, buf, + ares_sysconfig_parse_resolv_line)); + EXPECT_EQ((size_t)2, ares_llist_len(sysconfig.sconfig)); + ASSERT_EQ((size_t)2, sysconfig.ndomains); + EXPECT_STREQ("first.com", sysconfig.domains[0]); + EXPECT_STREQ("second.com", sysconfig.domains[1]); + EXPECT_STREQ("b", sysconfig.lookups); + EXPECT_EQ((size_t)5, sysconfig.ndots); + + ares_llist_destroy(sysconfig.sconfig); + ares_strsplit_free(sysconfig.domains, sysconfig.ndomains); + ares_free(sysconfig.sortlist); + ares_free(sysconfig.lookups); + ares_buf_destroy(buf); + ares_destroy(channel); +} + #endif /* !CARES_SYMBOL_HIDING */ TEST_F(LibraryTest, InetPtoN) { @@ -475,9 +620,46 @@ EXPECT_EQ(1, ares_inet_pton(AF_INET6, "12:34::ff", &a6)); EXPECT_EQ(1, ares_inet_pton(AF_INET6, "12:34::ffff:1.2.3.4", &a6)); EXPECT_EQ(0, ares_inet_pton(AF_INET, "xyzzy", &a4)); + // Too many octets is unparsable input, not a system error. + EXPECT_EQ(0, ares_inet_pton(AF_INET, "1.2.3.4.5", &a4)); + EXPECT_EQ(0, ares_inet_pton(AF_INET, "0x0102030405", &a4)); EXPECT_EQ(-1, ares_inet_pton(AF_INET+AF_INET6, "1.2.3.4", &a4)); } +// ares_inet_pton() parses addresses, not networks: like inet_pton(3) it must +// reject a "/bits" prefix length. It used to accept one and, for IPv6, write +// only (bits+7)/8 bytes of dst while still returning 1. +TEST_F(LibraryTest, InetPtoNRejectsPrefixLength) { + const char *cases6[] = { "::1/0", "ffff::1/16", "2001:db8::1/64", + "2001:db8::1/128", "::ffff:1.2.3.4/8", + "::ffff:1.2.3.4/32" }; + const char *cases4[] = { "1.2.3.4/8", "1.2.3.4/0", "1.2.3.4/32", "10/8", + "0x0a000000/8" }; + unsigned char a4[4]; + unsigned char a6[16]; + size_t i; + + for (i = 0; i < sizeof(cases6) / sizeof(*cases6); i++) { + EXPECT_EQ(0, ares_inet_pton(AF_INET6, cases6[i], a6)) << cases6[i]; + } + for (i = 0; i < sizeof(cases4) / sizeof(*cases4); i++) { + EXPECT_EQ(0, ares_inet_pton(AF_INET, cases4[i], a4)) << cases4[i]; + } + + // Without a prefix length, the whole destination is written. + memset(a6, 0x41, sizeof(a6)); + EXPECT_EQ(1, ares_inet_pton(AF_INET6, "ffff::1", a6)); + EXPECT_EQ("ffff:0000:0000:0000:0000:0000:0000:0001", + AddressToString(a6, 16)); + memset(a6, 0x41, sizeof(a6)); + EXPECT_EQ(1, ares_inet_pton(AF_INET6, "::ffff:1.2.3.4", a6)); + EXPECT_EQ("0000:0000:0000:0000:0000:ffff:0102:0304", + AddressToString(a6, 16)); + memset(a4, 0x41, sizeof(a4)); + EXPECT_EQ(1, ares_inet_pton(AF_INET, "1.2.3.4", a4)); + EXPECT_EQ("1.2.3.4", AddressToString(a4, 4)); +} + TEST_F(LibraryTest, FreeCorruptData) { // ares_free_data(p) expects that there is a type field and a marker // field in the memory before p. Feed it incorrect versions of each. @@ -594,6 +776,18 @@ EXPECT_EQ("{ipv6.com addr=[[0000:0000:0000:0000:0000:0000:0000:0001]]}", ss.str()); } +TEST_F(FileChannelTest, GetAddrInfoInvalidService) { + TempFile hostsfile("1.2.3.4 example.com"); + EnvValue with_env("CARES_HOSTS", hostsfile.filename()); + struct ares_addrinfo_hints hints{}; + AddrInfoResult result{}; + hints.ai_family = AF_INET; + hints.ai_flags = ARES_AI_CANONNAME | ARES_AI_ENVHOSTS | ARES_AI_NOSORT; + ares_getaddrinfo(channel_, "example.com", "invalid", &hints, AddrInfoCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_EQ(result.status_, ARES_ESERVICE); +} TEST_F(FileChannelTest, GetAddrInfoAllocFail) { TempFile hostsfile("1.2.3.4 example.com alias1 alias2\n"); @@ -624,6 +818,95 @@ } } +TEST_F(LibraryTest, DNSWriteOversizedRdlength) +{ + ares_dns_record_t *dnsrec = nullptr; + ares_dns_rr_t *rr = nullptr; + unsigned char *msg = nullptr; + size_t msglen = 0; + + // 300 x 255-byte character-strings = rdlength 76500 > 65535. + std::vector str(255, 'x'); + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&dnsrec, 0x1234, 0, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(dnsrec, "example.com", ARES_REC_TYPE_TXT, + ARES_CLASS_IN)); + EXPECT_EQ(ARES_SUCCESS, ares_dns_record_rr_add( + &rr, dnsrec, ARES_SECTION_ANSWER, "example.com", + ARES_REC_TYPE_TXT, ARES_CLASS_IN, 3600)); + for (size_t i = 0; i < 300; i++) { + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_add_abin(rr, ARES_RR_TXT_DATA, + str.data(), str.size())); + } + + // rdlength does not fit in 16 bits and must not be silently truncated into + // a structurally corrupt message reported as ARES_SUCCESS. + EXPECT_EQ(ARES_EBADQUERY, ares_dns_write(dnsrec, &msg, &msglen)); + EXPECT_EQ(nullptr, msg); + EXPECT_EQ(0U, msglen); + + ares_dns_record_destroy(dnsrec); + ares_free_string(msg); +} + +TEST_F(LibraryTest, DNSWriteMaximumMessageSize) +{ + ares_dns_record_t *dnsrec = nullptr; + ares_dns_record_t *parsed = nullptr; + ares_dns_rr_t *rr = nullptr; + unsigned char *msg = nullptr; + size_t msglen = 0; + size_t overhead = 0; + size_t max_data = 0; + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&dnsrec, 0x1234, 0, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(dnsrec, "example.com", ARES_REC_TYPE_A, + ARES_CLASS_IN)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&rr, dnsrec, ARES_SECTION_ANSWER, "", + ARES_REC_TYPE_RAW_RR, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_set_u16(rr, ARES_RR_RAW_RR_TYPE, 65432)); + + // Measure the fixed framing with a one-byte raw record, so the exact + // per-byte limit can be hit: the largest message that fits must write and + // parse back, and a single extra byte must be rejected. + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_set_bin( + rr, ARES_RR_RAW_RR_DATA, + reinterpret_cast("x"), 1)); + EXPECT_EQ(ARES_SUCCESS, ares_dns_write(dnsrec, &msg, &msglen)); + overhead = msglen - 1; /* framing without the data byte */ + ares_free_string(msg); + msg = nullptr; + + max_data = 65535 - overhead; + std::vector data(max_data, 'x'); + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_set_bin(rr, ARES_RR_RAW_RR_DATA, + data.data(), data.size())); + EXPECT_EQ(ARES_SUCCESS, ares_dns_write(dnsrec, &msg, &msglen)); + EXPECT_EQ(65535U, msglen); + EXPECT_EQ(ARES_SUCCESS, ares_dns_parse(msg, msglen, 0, &parsed)); + ares_dns_record_destroy(parsed); + parsed = nullptr; + ares_free_string(msg); + msg = nullptr; + + data.push_back('x'); + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_set_bin(rr, ARES_RR_RAW_RR_DATA, + data.data(), data.size())); + EXPECT_EQ(ARES_EBADQUERY, ares_dns_write(dnsrec, &msg, &msglen)); + EXPECT_EQ(nullptr, msg); + EXPECT_EQ(0U, msglen); + + ares_dns_record_destroy(dnsrec); + ares_free_string(msg); +} + TEST_F(LibraryTest, DNSRecord) { ares_dns_record_t *dnsrec = NULL; ares_dns_rr_t *rr = NULL; @@ -1081,6 +1364,107 @@ EXPECT_EQ(ARES_FALSE, ares_dns_rr_get_opt_byid(NULL, ARES_RR_A_ADDR, 1, NULL, NULL)); } +TEST_F(LibraryTest, DNSNameCompression14Bit) { + ares_dns_record_t *dnsrec = NULL; + ares_dns_rr_t *rr = NULL; + unsigned char *msg = NULL; + size_t msglen = 0; + std::vector txt(255, 'a'); + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&dnsrec, 0x1234, ARES_FLAG_QR, + ARES_OPCODE_QUERY, ARES_RCODE_NOERROR)); + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(dnsrec, "example.com", ARES_REC_TYPE_ANY, + ARES_CLASS_IN)); + + /* Pad the message well past the 14-bit (16383 byte) compression pointer + * limit with large TXT records. */ + for (size_t i = 0; i < 80; i++) { + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&rr, dnsrec, ARES_SECTION_ANSWER, "example.com", + ARES_REC_TYPE_TXT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_add_abin(rr, ARES_RR_TXT_DATA, txt.data(), txt.size())); + } + + /* Past byte 16383, add a repeated owner name. Its first occurrence lands at + * an offset that doesn't fit in 14 bits, so it must not be recorded as a + * compression target; the second occurrence must not be emitted as a pointer + * to (offset & 0x3FFF), which would corrupt the message. It instead + * compresses only against the in-range example.com target. */ + for (size_t i = 0; i < 2; i++) { + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&rr, dnsrec, ARES_SECTION_ANSWER, + "repeat.example.com", ARES_REC_TYPE_TXT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_add_abin(rr, ARES_RR_TXT_DATA, txt.data(), txt.size())); + } + + EXPECT_EQ(ARES_SUCCESS, ares_dns_write(dnsrec, &msg, &msglen)); + EXPECT_GT(msglen, 0x3FFFU); + + /* 12 hdr + 17 question + 80*268 compressed TXT + 2*275 repeat TXT. + * An exact size also verifies compression remains active for names + * below the limit. */ + EXPECT_EQ(22019U, msglen); + + /* Before the fix this parse failed / mis-parsed on the truncated pointer. */ + ares_dns_record_t *parsed = NULL; + EXPECT_EQ(ARES_SUCCESS, ares_dns_parse(msg, msglen, 0, &parsed)); + ASSERT_NE(nullptr, parsed); + + size_t ancount = ares_dns_record_rr_cnt(parsed, ARES_SECTION_ANSWER); + EXPECT_EQ(82U, ancount); + const ares_dns_rr_t *last = + ares_dns_record_rr_get_const(parsed, ARES_SECTION_ANSWER, ancount - 1); + ASSERT_NE(nullptr, last); + EXPECT_STREQ("repeat.example.com", ares_dns_rr_get_name(last)); + + ares_dns_record_destroy(parsed); + ares_free_string(msg); + ares_dns_record_destroy(dnsrec); +} + +TEST_F(LibraryTest, DNSRecordRejectsInvalidBinaryInput) { + ares_dns_record_t *dnsrec = NULL; + ares_dns_rr_t *rr = NULL; + unsigned char data[] = { 'x' }; + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&dnsrec, 0x1234, ARES_FLAG_RD, + ARES_OPCODE_QUERY, ARES_RCODE_NOERROR)); + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&rr, dnsrec, ARES_SECTION_ANSWER, "example.com", + ARES_REC_TYPE_TXT, ARES_CLASS_IN, 60)); + EXPECT_EQ(ARES_ENOMEM, + ares_dns_rr_add_abin(rr, ARES_RR_TXT_DATA, data, SIZE_MAX)); + EXPECT_EQ(ARES_EFORMERR, + ares_dns_rr_add_abin(rr, ARES_RR_TXT_DATA, NULL, 1)); + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&rr, dnsrec, ARES_SECTION_ANSWER, "example.com", + ARES_REC_TYPE_CAA, ARES_CLASS_IN, 60)); + EXPECT_EQ(ARES_ENOMEM, + ares_dns_rr_set_bin(rr, ARES_RR_CAA_VALUE, data, SIZE_MAX)); + EXPECT_EQ(ARES_EFORMERR, + ares_dns_rr_set_bin(rr, ARES_RR_CAA_VALUE, NULL, 1)); + EXPECT_EQ(ARES_EFORMERR, + ares_dns_rr_set_bin(rr, ARES_RR_CAA_TAG, data, sizeof(data))); + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&rr, dnsrec, ARES_SECTION_ADDITIONAL, "", + ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_ENOMEM, + ares_dns_rr_set_opt(rr, ARES_RR_OPT_OPTIONS, 3, data, SIZE_MAX)); + EXPECT_EQ(ARES_EFORMERR, + ares_dns_rr_set_opt(rr, ARES_RR_OPT_OPTIONS, 3, NULL, 1)); + + ares_dns_record_destroy(dnsrec); +} + TEST_F(LibraryTest, DNSParseFlags) { ares_dns_record_t *dnsrec = NULL; ares_dns_rr_t *rr = NULL; @@ -1217,6 +1601,19 @@ EXPECT_NE(ARES_SUCCESS, ares_array_remove_last(NULL)); EXPECT_NE(ARES_SUCCESS, ares_array_sort(NULL, NULL)); EXPECT_NE(ARES_SUCCESS, ares_array_set_size(NULL, 0)); + + /* Overflow in the size calculation must be rejected, not wrapped */ + ares_array_t *overflow_array = ares_array_create((SIZE_MAX / 2) + 1, NULL); + EXPECT_NE((void *)NULL, overflow_array); + EXPECT_EQ(ARES_ENOMEM, ares_array_set_size(overflow_array, 2)); + EXPECT_EQ(ARES_ENOMEM, ares_array_set_size(overflow_array, SIZE_MAX)); + ares_array_destroy(overflow_array); + + /* Overflow must also be rejected in the malloc-style wrapper, and in + * the original-size arm of the realloc wrapper */ + EXPECT_EQ(NULL, ares_malloc_zero_array(SIZE_MAX, 2)); + EXPECT_EQ(NULL, ares_malloc_zero_array(2, SIZE_MAX)); + EXPECT_EQ(NULL, ares_realloc_zero_array(NULL, SIZE_MAX, 1, 2)); } TEST_F(LibraryTest, BufMisuse) { @@ -1456,6 +1853,46 @@ ares_array_destroy(a); } +/* Regression: repeatedly claiming the first element drives the internal + * offset up to the allocation size. Re-inserting afterwards must still + * succeed (previously ares_array_insert_at() failed with ARES_EFORMERR + * because compaction tried to move from an out-of-range source index). */ +TEST_F(LibraryTest, ArrayClaimFrontThenReuse) { + ares_array_t *a = ares_array_create(sizeof(size_t), NULL); + EXPECT_NE(nullptr, a); + + for (size_t iter = 0; iter < 4; iter++) { + /* Fill exactly to the minimum allocation (ARES__ARRAY_MIN == 4) so a + * full front-drain later pushes offset to precisely alloc_cnt. */ + for (size_t i = 0; i < 4; i++) { + size_t val = iter * 100 + i; + EXPECT_EQ(ARES_SUCCESS, ares_array_insertdata_last(a, &val)); + } + EXPECT_EQ((size_t)4, ares_array_len(a)); + + /* Drain ALL elements from the front so offset climbs to alloc_cnt (the + * condition the fix guards); verify copy-out value and FIFO order. */ + for (size_t i = 0; i < 4; i++) { + size_t out = 0; + EXPECT_EQ(ARES_SUCCESS, ares_array_claim_at(&out, sizeof(out), a, 0)); + EXPECT_EQ(iter * 100 + i, out); + } + EXPECT_EQ((size_t)0, ares_array_len(a)); + + /* Array is now empty with offset == alloc_cnt; appending must still + * work, and the data must be readable back out. */ + size_t *ptr = NULL; + EXPECT_EQ(ARES_SUCCESS, ares_array_insert_last((void **)&ptr, a)); + EXPECT_NE(nullptr, ptr); + *ptr = 424242; + EXPECT_EQ((size_t)424242, *(size_t *)ares_array_at(a, 0)); + EXPECT_EQ(ARES_SUCCESS, ares_array_remove_first(a)); + } + + EXPECT_EQ((size_t)0, ares_array_len(a)); + ares_array_destroy(a); +} + TEST_F(LibraryTest, HtableVpvp) { ares_llist_t *l = NULL; ares_htable_vpvp_t *h = NULL; @@ -1550,6 +1987,64 @@ ares_socket_t s; } test_htable_asvp_t; +TEST_F(LibraryTest, ZeroLengthRawRrKeepsType) { + ares_dns_record_t *parsed = NULL; + ares_dns_rr_t *rr = NULL; + + /* Hand-crafted minimal DNS response with a zero-length RAW_RR. + * Header: id=0x1234, QR=1 RD=1 RA=1, QDCOUNT=1, ANCOUNT=1 + * Question: \x07example\x03com\x00, type A (1), class IN (1) + * Answer: \xc0\x0c (name ptr), type=0xFF98 (65432), class IN, + * TTL=300, RDLENGTH=0 */ + const unsigned char pkt[] = { + /* Header */ + 0x12, 0x34, /* ID */ + 0x81, 0x80, /* Flags: QR=1, RD=1, RA=1 */ + 0x00, 0x01, /* QDCOUNT=1 */ + 0x00, 0x01, /* ANCOUNT=1 */ + 0x00, 0x00, /* NSCOUNT=0 */ + 0x00, 0x00, /* ARCOUNT=0 */ + /* Question: example.com, type A, class IN */ + 0x07, 'e','x','a','m','p','l','e', + 0x03, 'c','o','m', + 0x00, + 0x00, 0x01, /* QTYPE=A */ + 0x00, 0x01, /* QCLASS=IN */ + /* Answer RR: example.com (compressed), type 65432, class IN, TTL 300 */ + 0xc0, 0x0c, /* Name pointer */ + 0xff, 0x98, /* TYPE=65432 */ + 0x00, 0x01, /* CLASS=IN */ + 0x00, 0x00, 0x01, 0x2c, /* TTL=300 */ + 0x00, 0x00 /* RDLENGTH=0 */ + }; + + EXPECT_EQ(ARES_SUCCESS, + ares_dns_parse(pkt, sizeof(pkt), 0, &parsed)); + EXPECT_EQ(1, ares_dns_record_rr_cnt(parsed, ARES_SECTION_ANSWER)); + rr = ares_dns_record_rr_get(parsed, ARES_SECTION_ANSWER, 0); + EXPECT_EQ(ARES_REC_TYPE_RAW_RR, ares_dns_rr_get_type(rr)); + EXPECT_EQ(65432, ares_dns_rr_get_u16(rr, ARES_RR_RAW_RR_TYPE)); + + ares_dns_record_destroy(parsed); +} + +TEST_F(LibraryTest, RawRrTypeTostrFromstrRoundtrip) { + const char *str; + ares_dns_rec_type_t qtype = (ares_dns_rec_type_t)0; + + str = ares_dns_rec_type_tostr(ARES_REC_TYPE_RAW_RR); + EXPECT_NE((const char *)NULL, str); + EXPECT_TRUE(ares_dns_rec_type_fromstr(&qtype, str)); + EXPECT_EQ(ARES_REC_TYPE_RAW_RR, qtype); +} + +TEST_F(LibraryTest, BufReplaceNullBuf) { + EXPECT_EQ(ARES_EFORMERR, + ares_buf_replace(NULL, + (const unsigned char *)"x", 1, + (const unsigned char *)"y", 1)); +} + TEST_F(LibraryTest, HtableAsvp) { ares_llist_t *l = NULL; ares_htable_asvp_t *h = NULL; diff -Nru c-ares-1.34.5/test/ares-test-misc.cc c-ares-1.34.9/test/ares-test-misc.cc --- c-ares-1.34.5/test/ares-test-misc.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-misc.cc 2026-10-20 02:31:51.000000000 +0000 @@ -26,6 +26,8 @@ #include "ares-test.h" #include "dns-proto.h" +#include +#include #include #include @@ -147,6 +149,14 @@ ares_set_servers_csv(channel_, "1.2.3.4 , [0102:0304:0506:0708:0910:1112:1314:1516]:53, [fe80::1]:53%iface0, 2.3.4.5")); EXPECT_EQ(expected, GetNameServers(channel_)); + // Reject an out-of-range numeric link-local scope, keep rest. 2^32+1 wraps + // to index 1 under the old atoi() and would have been accepted; the + // range-checked parse rejects it before the interface is looked up, so no + // real interface is needed. + EXPECT_EQ(ARES_SUCCESS, + ares_set_servers_csv(channel_, "1.2.3.4 , [0102:0304:0506:0708:0910:1112:1314:1516]:53, [fe80::1]:53%4294967297, 2.3.4.5")); + EXPECT_EQ(expected, GetNameServers(channel_)); + // Same, with ports EXPECT_EQ(ARES_SUCCESS, ares_set_servers_ports_csv(channel_, "1.2.3.4:54,[0102:0304:0506:0708:0910:1112:1314:1516]:80,2.3.4.5:55")); @@ -342,6 +352,95 @@ EXPECT_TRUE(result.done_); EXPECT_EQ(ARES_ENOTFOUND, result.status_); } + +// Collect the resolved nodes of an AddrInfoResult as "addr:port" strings. +static std::set AddrInfoNodeStrings(const AddrInfoResult &result) { + std::set out; + if (result.ai_ == nullptr) { + return out; + } + for (struct ares_addrinfo_node *n = result.ai_->nodes; n != NULL; + n = n->ai_next) { + std::stringstream ss; + if (n->ai_family == AF_INET) { + sockaddr_in *sin = reinterpret_cast( + reinterpret_cast(n->ai_addr)); + ss << AddressToString(&sin->sin_addr, 4) << ":" << ntohs(sin->sin_port); + } else if (n->ai_family == AF_INET6) { + sockaddr_in6 *sin6 = reinterpret_cast( + reinterpret_cast(n->ai_addr)); + ss << "[" << AddressToString(&sin6->sin6_addr, 16) + << "]:" << ntohs(sin6->sin6_port); + } + out.insert(ss.str()); + } + return out; +} + +// A NULL node/hostname with a service is a valid POSIX getaddrinfo() request; +// it must not crash and, without ARES_AI_PASSIVE, returns loopback addresses. +TEST_F(DefaultChannelTest, GetAddrinfoNullNameLoopback) { + AddrInfoResult result; + struct ares_addrinfo_hints hints = {0, 0, 0, 0}; + hints.ai_family = AF_UNSPEC; + hints.ai_flags = ARES_AI_NUMERICSERV; + ares_getaddrinfo(channel_, NULL, "80", &hints, AddrInfoCallback, &result); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + std::set expected = { + "127.0.0.1:80", "[0000:0000:0000:0000:0000:0000:0000:0001]:80"}; + EXPECT_EQ(expected, AddrInfoNodeStrings(result)); +} + +// With ARES_AI_PASSIVE a NULL node returns wildcard addresses (for bind()). +TEST_F(DefaultChannelTest, GetAddrinfoNullNamePassiveWildcard) { + AddrInfoResult result; + struct ares_addrinfo_hints hints = {0, 0, 0, 0}; + hints.ai_family = AF_UNSPEC; + hints.ai_flags = ARES_AI_NUMERICSERV | ARES_AI_PASSIVE; + ares_getaddrinfo(channel_, NULL, "80", &hints, AddrInfoCallback, &result); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + std::set expected = { + "0.0.0.0:80", "[0000:0000:0000:0000:0000:0000:0000:0000]:80"}; + EXPECT_EQ(expected, AddrInfoNodeStrings(result)); +} + +// ai_family is honored for NULL-node synthesis. +TEST_F(DefaultChannelTest, GetAddrinfoNullNameInetOnly) { + AddrInfoResult result; + struct ares_addrinfo_hints hints = {0, 0, 0, 0}; + hints.ai_family = AF_INET; + hints.ai_flags = ARES_AI_NUMERICSERV; + ares_getaddrinfo(channel_, NULL, "80", &hints, AddrInfoCallback, &result); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + std::set expected = {"127.0.0.1:80"}; + EXPECT_EQ(expected, AddrInfoNodeStrings(result)); +} + +TEST_F(DefaultChannelTest, GetAddrinfoNullNameInet6Passive) { + AddrInfoResult result; + struct ares_addrinfo_hints hints = {0, 0, 0, 0}; + hints.ai_family = AF_INET6; + hints.ai_flags = ARES_AI_NUMERICSERV | ARES_AI_PASSIVE; + ares_getaddrinfo(channel_, NULL, "80", &hints, AddrInfoCallback, &result); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + std::set expected = { + "[0000:0000:0000:0000:0000:0000:0000:0000]:80"}; + EXPECT_EQ(expected, AddrInfoNodeStrings(result)); +} + +// Both node and service NULL is invalid per POSIX; must error, not crash. +TEST_F(DefaultChannelTest, GetAddrinfoNullNameNullService) { + AddrInfoResult result; + struct ares_addrinfo_hints hints = {0, 0, 0, 0}; + hints.ai_family = AF_UNSPEC; + ares_getaddrinfo(channel_, NULL, NULL, &hints, AddrInfoCallback, &result); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ENOTFOUND, result.status_); +} // Interesting question: should tacking on a search domain let the query // through? It seems safer to reject it because "supersecret.onion.search" diff -Nru c-ares-1.34.5/test/ares-test-mock-ai.cc c-ares-1.34.9/test/ares-test-mock-ai.cc --- c-ares-1.34.5/test/ares-test-mock-ai.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-mock-ai.cc 2026-10-20 02:31:51.000000000 +0000 @@ -617,6 +617,29 @@ EXPECT_THAT(result.ai_, IncludesV6Address("2121:0000:0000:0000:0000:0000:0000:0303")); } +// A numeric host with a "/bits" prefix length is a network, not an address. +// It used to be accepted, returning uninitialized stack memory as the address. +TEST_P(MockChannelTestAI, NumericHostRejectsPrefixLength) { + struct ares_addrinfo_hints hints = {0, 0, 0, 0}; + hints.ai_family = AF_INET6; + hints.ai_flags = ARES_AI_NUMERICHOST; + + AddrInfoResult result; + ares_getaddrinfo(channel_, "::1/0", NULL, &hints, AddrInfoCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + EXPECT_NE(ARES_SUCCESS, result.status_); + + AddrInfoResult result2; + ares_getaddrinfo(channel_, "::1", NULL, &hints, AddrInfoCallback, &result2); + Process(); + EXPECT_TRUE(result2.done_); + EXPECT_EQ(ARES_SUCCESS, result2.status_); + EXPECT_THAT(result2.ai_, IncludesNumAddresses(1)); + EXPECT_THAT(result2.ai_, + IncludesV6Address("0000:0000:0000:0000:0000:0000:0000:0001")); +} + // Test case for Issue #662 TEST_P(MockChannelTestAI, PartialQueryCancel) { std::vector nothing; @@ -805,7 +828,39 @@ EXPECT_THAT(result.ai_, IncludesV6Address("2121:0000:0000:0000:0000:0000:0000:0303")); } +TEST_P(MockUDPChannelTestAI, ConnectionRefusedOnSearchDomainRetry) { + DNSPacket badrsp4; + badrsp4.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .set_rcode(NXDOMAIN); + + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(SetReplyAndFailSend(&server_, &badrsp4)); + DNSPacket goodrsp4; + goodrsp4.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com.first.com", T_A)) + .add_answer(new DNSARR("www.google.com.first.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); + + EXPECT_CALL(server_, OnRequest("www.google.com.first.com", T_A)) + .WillOnce(SetReply(&server_, &goodrsp4)); + + ares_socket_functions sock_funcs; + memset(&sock_funcs, 0, sizeof(sock_funcs)); + + sock_funcs.asendv = ares_sendv_fail; + + ares_set_socket_functions(channel_, &sock_funcs, NULL); + + AddrInfoResult result; + struct ares_addrinfo_hints hints = {0, 0, 0, 0}; + hints.ai_family = AF_INET; + hints.ai_flags = ARES_AI_NOSORT; + ares_getaddrinfo(channel_, "www.google.com", NULL, &hints, + AddrInfoCallback, &result); + + Process(); +} class MockEDNSChannelTestAI : public MockFlagsChannelOptsTestAI { public: diff -Nru c-ares-1.34.5/test/ares-test-mock-et.cc c-ares-1.34.9/test/ares-test-mock-et.cc --- c-ares-1.34.5/test/ares-test-mock-et.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-mock-et.cc 2026-10-20 02:31:51.000000000 +0000 @@ -112,6 +112,114 @@ } } +static int noop_close(ares_socket_t sock, void *user_data) +{ + (void)user_data; +#if defined(HAVE_CLOSESOCKET) + return closesocket(sock); +#elif defined(HAVE_CLOSESOCKET_CAMEL) + return CloseSocket(sock); +#elif defined(HAVE_CLOSE_S) + return close_s(sock); +#else + return close(sock); +#endif + return 0; +} + +static ares_socket_t noop_socket(int domain, int type, int protocol, + void *user_data) +{ + (void)user_data; + return socket(domain, type, protocol); +} + +static int noop_setsockopt(ares_socket_t sock, ares_socket_opt_t opt, + const void *val, ares_socklen_t val_size, + void *user_data) +{ + (void)sock; + (void)opt; + (void)val; + (void)val_size; + (void)user_data; + return 0; +} + +static int noop_connect(ares_socket_t sock, const struct sockaddr *address, + ares_socklen_t address_len, unsigned int flags, + void *user_data) +{ + (void)sock; + (void)address; + (void)address_len; + (void)flags; + (void)user_data; + + return 0; +} + +static ares_ssize_t noop_recvfrom(ares_socket_t sock, void *buffer, + size_t length, int flags, + struct sockaddr *address, + ares_socklen_t *address_len, + void *user_data) +{ + (void)sock; + (void)buffer; + (void)length; + (void)flags; + (void)address; + (void)address_len; + (void)user_data; + + errno = EAGAIN; + return 0; +} + +static ares_ssize_t noop_sendto(ares_socket_t sock, const void *buffer, + size_t length, int flags, + const struct sockaddr *address, + ares_socklen_t address_len, void *user_data) +{ + (void)sock; + (void)buffer; + (void)flags; + (void)address; + (void)address_len; + (void)user_data; + /* Eat all data */ + return (ares_ssize_t)length; +} + +// Issue #1000 Event Thread stall on temporarily downed server. +TEST_P(MockUDPEventThreadTest, DownServer) { + struct ares_socket_functions_ex noop_sock_funcs; + memset(&noop_sock_funcs, 0, sizeof(noop_sock_funcs)); + noop_sock_funcs.version = 1; + noop_sock_funcs.asocket = noop_socket; + noop_sock_funcs.aclose = noop_close; + noop_sock_funcs.asetsockopt = noop_setsockopt; + noop_sock_funcs.aconnect = noop_connect; + noop_sock_funcs.arecvfrom = noop_recvfrom; + noop_sock_funcs.asendto = noop_sendto; + ares_set_socket_functions_ex(channel_, &noop_sock_funcs, NULL); + + QueryResult result; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result, NULL); + // no need to call Process() since we're not actually connecting + ares_queue_wait_empty(channel_, -1); + EXPECT_TRUE(result.done_); + EXPECT_NE(0, result.timeouts_); + + // Issue states second query stalls + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, QueryCallback, &result, NULL); + // no need to call Process() since we're not actually connecting + ares_queue_wait_empty(channel_, -1); + EXPECT_TRUE(result.done_); + EXPECT_NE(0, result.timeouts_); +} + // UDP to TCP specific test TEST_P(MockUDPEventThreadTest, TruncationRetry) { DNSPacket rsptruncated; @@ -852,6 +960,30 @@ EXPECT_TRUE(result.done_); } +TEST_P(MockEventThreadTest, DupDestroySource) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + /* The copy gets its own event thread, so it must keep working once the + * channel it was duplicated from is gone. */ + ares_channel_t *copy = nullptr; + ASSERT_EQ(ARES_SUCCESS, ares_dup(©, channel_)); + ares_destroy(channel_); + channel_ = copy; + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + EXPECT_TRUE(result.done_); + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[2.3.4.5]}", ss.str()); +} + // Test case for Issue #662 TEST_P(MockEventThreadTest, PartialQueryCancel) { std::vector nothing; diff -Nru c-ares-1.34.5/test/ares-test-mock.cc c-ares-1.34.9/test/ares-test-mock.cc --- c-ares-1.34.5/test/ares-test-mock.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-mock.cc 2026-10-20 02:31:51.000000000 +0000 @@ -31,6 +31,11 @@ #include #endif +#ifndef _WIN32 +# include +# include +#endif + #include #include @@ -57,6 +62,37 @@ struct ares_options opts_; }; +static std::vector MakeMaxReadTcpAReply(void) { + std::vector reply = { + 0x00, 0x00, // qid + 0x84, 0x00, // response + query + AA + noerror + 0x00, 0x01, // qdcount + 0x00, 0x01, // ancount + 0x00, 0x00, // nscount + 0x00, 0x00, // arcount + // Question: www.google.com IN A + 0x03, 'w', 'w', 'w', + 0x06, 'g', 'o', 'o', 'g', 'l', 'e', + 0x03, 'c', 'o', 'm', + 0x00, + 0x00, 0x01, + 0x00, 0x01, + // Answer: name ptr, IN A 1.2.3.4 + 0xc0, 0x0c, + 0x00, 0x01, + 0x00, 0x01, + 0x00, 0x00, 0x00, 0x64, + 0x00, 0x04, + 0x01, 0x02, 0x03, 0x04 + }; + + /* Keep payload at 65533 bytes so TCP frame length is exactly 65535, + * forcing the read loop to attempt one more recv() where disconnect can + * race with already-buffered data. */ + reply.resize(65533, 0x00); + return reply; +} + TEST_P(NoDNS0x20MockTest, Basic) { std::vector reply = { @@ -495,6 +531,69 @@ } } +// Regression test for #1152. A transient failure (a single query timeout) +// must not force a brand new UDP socket to be opened for every subsequent +// query to the same server. The connection that saw the timeout is retired +// for new queries (ARES_CONN_FLAG_NONEW) and drains, while new queries share +// one fresh connection. Before the fix, ares_fetch_connection() rejected +// reuse whenever the server's consec_failures was > 0, so a single dropped +// packet caused a new socket per query and exhausted sockets/ports under load. +class MockUDPTransientFailureTest + : public MockChannelOptsTest, + public ::testing::WithParamInterface { + public: + MockUDPTransientFailureTest() + : MockChannelOptsTest(1, GetParam(), false, false, + FillOptions(&opts_), + ARES_OPT_TIMEOUTMS | ARES_OPT_TRIES) {} + static struct ares_options* FillOptions(struct ares_options * opts) { + memset(opts, 0, sizeof(struct ares_options)); + opts->timeout = 150; /* ms (ARES_OPT_TIMEOUTMS) - keep the test quick */ + opts->tries = 3; + return opts; + } + private: + struct ares_options opts_; +}; + +TEST_P(MockUDPTransientFailureTest, NoSocketExplosionOnTimeout) { + // Configure the server to never reply, so every query times out (a + // transient failure). No SetReply() means ProcessRequest() drops the + // packet; the AnyNumber() expectation just silences uninteresting-call + // warnings for the many dropped requests. + EXPECT_CALL(server_, OnRequest("www.example.com", T_A)) + .Times(testing::AnyNumber()); + + int rc = ARES_SUCCESS; + ares_set_socket_callback(channel_, SocketConnectCallback, &rc); + sock_cb_count = 0; + + const size_t nqueries = 16; + HostResult result[nqueries]; + for (size_t i = 0; i < nqueries; i++) { + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, + &result[i]); + } + Process(); + + // All queries complete (they fail with a timeout since the server never + // replied). + for (size_t i = 0; i < nqueries; i++) { + EXPECT_TRUE(result[i].done_); + } + + // The key assertion: with tries=3 the fix opens roughly one connection per + // retry round (the retired connection is skipped and the single replacement + // is reused by all queries), not one per query. Before the fix this was on + // the order of nqueries*tries sockets. + EXPECT_LE(sock_cb_count, 6) + << "opened " << sock_cb_count << " sockets for " << nqueries + << " queries; a transient timeout must not force a new socket per query"; +} + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPTransientFailureTest, + ::testing::ValuesIn(ares::test::families), PrintFamily); + class CacheQueriesTest : public MockChannelOptsTest, public ::testing::WithParamInterface { @@ -512,6 +611,425 @@ struct ares_options opts_; }; +// A cache hit used to hand the callback a pointer to the cache's own record. +// That is fine only while the callback does nothing else -- but re-entering +// c-ares from a callback is documented as allowed, and two ordinary things a +// callback might do destroy the entry underneath it: changing the server list +// flushes the whole cache, and starting another lookup runs the expiry reaper +// first. The record must therefore be the caller's own copy. +// +// Here the callback changes the server list, which flushes the cache, and then +// reads the record it was handed. +struct QcacheReentrantCbData { + ares_channel_t *channel; + bool done; + size_t ancount; +}; + +static void QcacheReentrantCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + QcacheReentrantCbData *data = static_cast(arg); + (void)timeouts; + data->done = true; + if (status != ARES_SUCCESS || dnsrec == NULL) { + return; + } + /* Documented re-entry. Reconfiguring servers flushes the query cache. */ + EXPECT_EQ(ARES_SUCCESS, + ares_set_servers_csv(data->channel, "127.0.0.1:53055")); + /* Still inside the callback, so this record must still be readable. */ + data->ancount = ares_dns_record_rr_cnt(dnsrec, ARES_SECTION_ANSWER); +} + +TEST_P(CacheQueriesTest, ReentrantFlushInCallbackNoUseAfterFree) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + /* Populate the cache. */ + QueryResult first; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, &first, NULL); + Process(); + EXPECT_TRUE(first.done_); + + /* Served from the cache, so the callback runs synchronously holding the + * record the cache handed it. */ + QcacheReentrantCbData data; + data.channel = channel_; + data.done = false; + data.ancount = 0; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QcacheReentrantCallback, &data, NULL); + + EXPECT_TRUE(data.done); + EXPECT_EQ((size_t)1, data.ancount); +} + +// The cache key covered the question and a couple of header flags but ignored +// the request's OPT record entirely, so two requests differing only in their +// EDNS options collided. An answer obtained under one set of options is not +// interchangeable with one obtained under another -- most obviously, a request +// with the DO bit set must not be handed an answer fetched without it. +// +// Both requests here ask the identical question; only the EDNS state differs, +// so the server must see two of them. +TEST_P(CacheQueriesTest, EdnsOptionsSeparateCacheEntries) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .Times(2) + .WillRepeatedly(SetReply(&server_, &rsp)); + + /* First: no OPT record at all. */ + ares_dns_record_t *req1 = nullptr; + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&req1, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req1, "www.google.com", ARES_REC_TYPE_A, + ARES_CLASS_IN)); + QueryResult r1; + EXPECT_EQ(ARES_SUCCESS, + ares_send_dnsrec(channel_, req1, QueryCallback, &r1, nullptr)); + Process(); + EXPECT_TRUE(r1.done_); + EXPECT_EQ(ARES_SUCCESS, r1.status_); + + /* Second: same question, but carrying EDNS with the DO bit set. */ + ares_dns_record_t *req2 = nullptr; + ares_dns_rr_t *opt = nullptr; + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&req2, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req2, "www.google.com", ARES_REC_TYPE_A, + ARES_CLASS_IN)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&opt, req2, ARES_SECTION_ADDITIONAL, "", + ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_u16(opt, ARES_RR_OPT_UDP_SIZE, 1232)); + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_set_u8(opt, ARES_RR_OPT_VERSION, 0)); + /* DO */ + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_u16(opt, ARES_RR_OPT_FLAGS, 0x8000)); + + QueryResult r2; + EXPECT_EQ(ARES_SUCCESS, + ares_send_dnsrec(channel_, req2, QueryCallback, &r2, nullptr)); + Process(); + EXPECT_TRUE(r2.done_); + EXPECT_EQ(ARES_SUCCESS, r2.status_); + + ares_dns_record_destroy(req1); + ares_dns_record_destroy(req2); +} + +// The test above sets the DO bit, which lives in the OPT flags -- it never +// reaches the loop that keys the individual EDNS options. That loop is what +// separates two client subnets, so it needs its own coverage. +// The mirror image of the tests above: two things are deliberately left out +// of the cache key, and leaving them out is a security-relevant judgement -- +// it declares that two requests differing only in these still have +// interchangeable answers. Without a test, keying on either one later would +// turn every request from an affected application into a permanent cache miss +// and nothing would fail. + +// A DNS cookie is per-server transport state and says nothing about the +// content of an answer, so two requests differing only in their cookie share +// an entry. The server must therefore be asked exactly once. +// Two query types with no mnemonic in ares_dns_rec_type_tostr() both render as +// "UNKNOWN", so keying the mnemonic made every such type share one entry -- a +// lookup answered with a record of a type it never asked for. Any 16-bit +// value reaches here through ares_query_dnsrec(); LOC (29) and DNAME (39) are +// both real and both outside the table. +TEST_P(CacheQueriesTest, UnknownQueryTypesDoNotShareCacheEntries) { + DNSPacket rsp29; + rsp29.set_response().set_aa().add_question( + new DNSQuestion("example.net", 29)); + DNSPacket rsp39; + rsp39.set_response().set_aa().add_question( + new DNSQuestion("example.net", 39)); + + // One call per type: neither may be served the other's answer. + EXPECT_CALL(server_, OnRequest("example.net", 29)) + .Times(1) + .WillRepeatedly(SetReply(&server_, &rsp29)); + EXPECT_CALL(server_, OnRequest("example.net", 39)) + .Times(1) + .WillRepeatedly(SetReply(&server_, &rsp39)); + + QueryResult r1; + ares_query_dnsrec(channel_, "example.net", ARES_CLASS_IN, + static_cast(29), QueryCallback, &r1, + nullptr); + Process(); + EXPECT_TRUE(r1.done_); + + QueryResult r2; + ares_query_dnsrec(channel_, "example.net", ARES_CLASS_IN, + static_cast(39), QueryCallback, &r2, + nullptr); + Process(); + EXPECT_TRUE(r2.done_); +} + +// Handing back a duplicate materialises the pending TTL decrement, so a +// callback that reads the TTL off the record it is given sees the remaining +// lifetime rather than the value the server sent. docs/ares_init_options.3 +// states this, so pin it. Only visible to a caller that does not duplicate +// first -- QueryResult does, which is why the other cache tests cannot see it. +static unsigned int g_qcache_hit_ttl = 0; + +static void QcacheTtlCallback(void *arg, ares_status_t status, size_t timeouts, + const ares_dns_record_t *dnsrec) { + const ares_dns_rr_t *rr; + (void)arg; + (void)timeouts; + if (status != ARES_SUCCESS || dnsrec == NULL) { + return; + } + rr = ares_dns_record_rr_get_const(dnsrec, ARES_SECTION_ANSWER, 0); + g_qcache_hit_ttl = rr ? ares_dns_rr_get_ttl(rr) : 0; +} + +TEST_P(CacheQueriesTest, CacheHitReportsRemainingTtl) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .Times(1) + .WillRepeatedly(SetReply(&server_, &rsp)); + + g_qcache_hit_ttl = 0; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QcacheTtlCallback, nullptr, nullptr); + Process(); + EXPECT_EQ((unsigned int)100, g_qcache_hit_ttl); + + ares_sleep_time(2100); + + g_qcache_hit_ttl = 0; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QcacheTtlCallback, nullptr, nullptr); + Process(); + // Served from the cache, and reporting what is left rather than the + // original 100. + EXPECT_GT(g_qcache_hit_ttl, (unsigned int)0); + EXPECT_LT(g_qcache_hit_ttl, (unsigned int)100); +} + +TEST_P(CacheQueriesTest, CookieDoesNotSplitCacheEntries) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .Times(1) + .WillRepeatedly(SetReply(&server_, &rsp)); + + const unsigned char ck1[] = {1, 2, 3, 4, 5, 6, 7, 8}; + const unsigned char ck2[] = {8, 7, 6, 5, 4, 3, 2, 1}; + const unsigned char *ck[2] = {ck1, ck2}; + QueryResult r[2]; + + for (int i = 0; i < 2; i++) { + ares_dns_record_t *req = nullptr; + ares_dns_rr_t *opt = nullptr; + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&req, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req, "www.google.com", ARES_REC_TYPE_A, + ARES_CLASS_IN)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&opt, req, ARES_SECTION_ADDITIONAL, "", + ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_u16(opt, ARES_RR_OPT_UDP_SIZE, 1232)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_opt(opt, ARES_RR_OPT_OPTIONS, + ARES_OPT_PARAM_COOKIE, ck[i], sizeof(ck1))); + EXPECT_EQ(ARES_SUCCESS, + ares_send_dnsrec(channel_, req, QueryCallback, &r[i], nullptr)); + Process(); + EXPECT_TRUE(r[i].done_); + EXPECT_EQ(ARES_SUCCESS, r[i].status_); + ares_dns_record_destroy(req); + } +} + +// The advertised UDP payload size constrains how a response is transported, +// not what it says, so it is excluded too. Same expectation: one request. +TEST_P(CacheQueriesTest, UdpPayloadSizeDoesNotSplitCacheEntries) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .Times(1) + .WillRepeatedly(SetReply(&server_, &rsp)); + + const unsigned short sizes[2] = {512, 1232}; + QueryResult r[2]; + + for (int i = 0; i < 2; i++) { + ares_dns_record_t *req = nullptr; + ares_dns_rr_t *opt = nullptr; + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&req, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req, "www.google.com", ARES_REC_TYPE_A, + ARES_CLASS_IN)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&opt, req, ARES_SECTION_ADDITIONAL, "", + ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_u16(opt, ARES_RR_OPT_UDP_SIZE, sizes[i])); + EXPECT_EQ(ARES_SUCCESS, + ares_send_dnsrec(channel_, req, QueryCallback, &r[i], nullptr)); + Process(); + EXPECT_TRUE(r[i].done_); + EXPECT_EQ(ARES_SUCCESS, r[i].status_); + ares_dns_record_destroy(req); + } +} + +TEST_P(CacheQueriesTest, EdnsClientSubnetSeparateCacheEntries) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .Times(2) + .WillRepeatedly(SetReply(&server_, &rsp)); + + /* 192.0.2.0/24 and 198.51.100.0/24 */ + const unsigned char ecs1[] = {0x00, 0x01, 0x18, 0x00, 0xc0, 0x00, 0x02}; + const unsigned char ecs2[] = {0x00, 0x01, 0x18, 0x00, 0xc6, 0x33, 0x64}; + const unsigned char *ecs[2] = {ecs1, ecs2}; + QueryResult r[2]; + + for (int i = 0; i < 2; i++) { + ares_dns_record_t *req = nullptr; + ares_dns_rr_t *opt = nullptr; + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&req, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req, "www.google.com", ARES_REC_TYPE_A, + ARES_CLASS_IN)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&opt, req, ARES_SECTION_ADDITIONAL, "", + ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_u16(opt, ARES_RR_OPT_UDP_SIZE, 1232)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_opt(opt, ARES_RR_OPT_OPTIONS, + ARES_OPT_PARAM_EDNS_CLIENT_SUBNET, ecs[i], + sizeof(ecs1))); + EXPECT_EQ(ARES_SUCCESS, + ares_send_dnsrec(channel_, req, QueryCallback, &r[i], nullptr)); + Process(); + EXPECT_TRUE(r[i].done_); + EXPECT_EQ(ARES_SUCCESS, r[i].status_); + ares_dns_record_destroy(req); + } +} + +// A server answering an EDNS query with FORMERR makes c-ares strip the OPT +// record from query->query and retry. The cache key must not be derived from +// that record afterwards, or the entry is stored under a key no later lookup +// computes and the cache silently stops working for that name. +TEST_P(CacheQueriesTest, EdnsFallbackStillCaches) { + DNSPacket rspfail; + rspfail.set_response().set_aa().set_rcode(FORMERR) + .add_question(new DNSQuestion("www.google.com", T_A)); + DNSPacket rspok; + rspok.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {1, 2, 3, 4})); + /* Two: the FORMERR and the retry. The second lookup must be a cache hit. */ + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .Times(2) + .WillOnce(SetReply(&server_, &rspfail)) + .WillRepeatedly(SetReply(&server_, &rspok)); + + HostResult r1; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &r1); + Process(); + EXPECT_TRUE(r1.done_); + EXPECT_EQ(ARES_SUCCESS, r1.status_); + + HostResult r2; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &r2); + Process(); + EXPECT_TRUE(r2.done_); + EXPECT_EQ(ARES_SUCCESS, r2.status_); +} + +// The question name is caller-supplied and unescaped, so anything keyed after +// it could be imitated by a name containing that same text. A request for +// "example.net|EDNS|0|8000" with no OPT record must not collide with a request +// for "example.net" carrying EDNS with the DNSSEC OK bit set. +TEST_P(CacheQueriesTest, EdnsSectionCannotBeImitatedByName) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("example.net", T_A)) + .add_answer(new DNSARR("example.net", 100, {5, 6, 7, 8})); + ON_CALL(server_, OnRequest("example.net", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + /* Cache an answer for the real name, with EDNS and the DO bit set. */ + ares_dns_record_t *req = nullptr; + ares_dns_rr_t *opt = nullptr; + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&req, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req, "example.net", ARES_REC_TYPE_A, + ARES_CLASS_IN)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_rr_add(&opt, req, ARES_SECTION_ADDITIONAL, "", + ARES_REC_TYPE_OPT, ARES_CLASS_IN, 0)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_rr_set_u16(opt, ARES_RR_OPT_UDP_SIZE, 1232)); + EXPECT_EQ(ARES_SUCCESS, ares_dns_rr_set_u16(opt, ARES_RR_OPT_FLAGS, 0x8000)); + QueryResult r1; + EXPECT_EQ(ARES_SUCCESS, + ares_send_dnsrec(channel_, req, QueryCallback, &r1, nullptr)); + Process(); + EXPECT_TRUE(r1.done_); + EXPECT_EQ(ARES_SUCCESS, r1.status_); + ares_dns_record_destroy(req); + + /* A different name that spells out the EDNS suffix, with no OPT record. + * It must not be served the entry above; with the cache keyed correctly it + * is refused, because '|' is not valid in a hostname. */ + ares_dns_record_t *req2 = nullptr; + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_create(&req2, 0, ARES_FLAG_RD, ARES_OPCODE_QUERY, + ARES_RCODE_NOERROR)); + EXPECT_EQ(ARES_SUCCESS, + ares_dns_record_query_add(req2, "example.net|EDNS|0|8000", + ARES_REC_TYPE_A, ARES_CLASS_IN)); + QueryResult r2; + ares_send_dnsrec(channel_, req2, QueryCallback, &r2, nullptr); + Process(); + EXPECT_TRUE(r2.done_); + EXPECT_NE(ARES_SUCCESS, r2.status_); + ares_dns_record_destroy(req2); +} + TEST_P(CacheQueriesTest, GetHostByNameCache) { DNSPacket rsp; rsp.set_response().set_aa() @@ -578,6 +1096,27 @@ } } +TEST_P(MockTCPChannelTest, ReadFullFrameThenDisconnect) { + std::vector reply = MakeMaxReadTcpAReply(); + + EXPECT_CALL(server_, OnRequest("www.google.com", T_A)) + .WillOnce(DoAll(SetReplyData(&server_, reply), + DisconnectAfterReply(&server_))); + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, + &result); + Process(); + + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + EXPECT_EQ(0, result.timeouts_); + + std::stringstream ss; + ss << result.host_; + EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); +} + TEST_P(MockTCPChannelTest, MalformedResponse) { std::vector one = {0x00}; ON_CALL(server_, OnRequest("www.google.com", T_A)) @@ -1722,6 +2261,2196 @@ EXPECT_EQ("{'www.google.com' aliases=[] addrs=[1.2.3.4]}", ss.str()); } +// Regression for issue #1043: a long chain of retryable connection failures +// used to recurse ares_requeue_query() -> ares_send_query() until the stack was +// exhausted. With a very high retry count and a socket that always fails to be +// created (a retryable ARES_ECONNREFUSED), the retries must be processed +// iteratively and the query must terminate cleanly rather than crashing. +class MockRetryDepthChannelTest : public MockChannelOptsTest, + public ::testing::WithParamInterface { +public: + MockRetryDepthChannelTest() + : MockChannelOptsTest(1, GetParam(), false, false, FillOptions(&opts_), + ARES_OPT_TRIES) + { + } + + static struct ares_options *FillOptions(struct ares_options *opts) + { + memset(opts, 0, sizeof(struct ares_options)); + /* Large enough that the old recursive path would overflow the stack. */ + opts->tries = 100000; + return opts; + } + +private: + struct ares_options opts_; +}; + +static size_t g_always_fail_socket_calls = 0; + +static ares_socket_t always_fail_socket(int af, int type, int protocol, + void *user_data) +{ + (void)af; + (void)type; + (void)protocol; + (void)user_data; + g_always_fail_socket_calls++; + return ARES_SOCKET_BAD; +} + +TEST_P(MockRetryDepthChannelTest, HighRetryNoStackOverflow) { + ares_socket_functions sock_funcs; + memset(&sock_funcs, 0, sizeof(sock_funcs)); + sock_funcs.asocket = always_fail_socket; + ares_set_socket_functions(channel_, &sock_funcs, NULL); + + g_always_fail_socket_calls = 0; + + QueryResult result; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, &result, NULL); + Process(); + + /* If the retries didn't drive the query to a terminal state on their own + * (e.g. it parked awaiting a response that will never arrive), cancel it so + * the query terminates deterministically. */ + if (!result.done_) { + ares_cancel(channel_); + Process(); + } + + /* The essential property is that we reached this point at all: on unpatched + * code the retryable failures recursed ares_requeue_query()/ares_send_query() + * until the stack overflowed and the process aborted before getting here. We + * also confirm the retries were attempted iteratively (more than one socket + * creation) and that the query reached a terminal state. */ + EXPECT_GT(g_always_fail_socket_calls, (size_t)1); + EXPECT_TRUE(result.done_); +} + +// Regression for CVE-2026-33630 / GHSA-6wfj-rwm7-3542: invoking ares_cancel() +// from within a normal response callback must not double-free the query. The +// callback is dispatched from the read_answers() deferred-requeue flush, and the +// query must be detached from all lookup lists before the callback runs so that +// the reentrant ares_cancel() cannot find and free it a second time. +struct CancelInCbData { + ares_channel_t *channel; + bool done; + size_t cbs; +}; + +static void CancelChannelCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) +{ + CancelInCbData *data = static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + data->done = true; + data->cbs++; + /* Reentrant cancel from within the callback. Must not double-free. */ + ares_cancel(data->channel); +} + +// ares_destroy() notifies each still-running query that the channel is going +// away, and holds a pointer to the next one in its list while doing so. A +// reentrant ares_cancel() from one of those callbacks frees every remaining +// query -- including that cached next, which ares_destroy() then follows. +// +// ares_cancel() is immune to the same pattern because it swaps the list head +// before walking it; ares_destroy() did not. Two queries are required: with +// only one in flight there is no next to cache and nothing goes wrong. +// tries=1 so a teardown ends the query outright instead of retrying it, which +// is the precondition this family of bugs shares. +class MockUDPSingleTryTest : public MockChannelOptsTest, + public ::testing::WithParamInterface { +public: + MockUDPSingleTryTest() + : MockChannelOptsTest(1, GetParam(), false, false, FillOptions(&opts_), + ARES_OPT_TRIES) + { + } + + static struct ares_options *FillOptions(struct ares_options *opts) + { + memset(opts, 0, sizeof(struct ares_options)); + opts->tries = 1; + return opts; + } + +private: + struct ares_options opts_; +}; + +// ares_cancel() and ares_destroy() free a query themselves rather than going +// through end_query(). Both used to invoke the application callback while the +// query was still in queries_by_qid, on conn->queries_to_conn and in +// queries_by_timeout -- only all_queries was unlinked first. A reentrant +// teardown from within that callback therefore still found the query, ended it +// a second time, and freed it; the ares_free_query() that followed then +// double-freed. One query in flight is enough. +static void ReconfigureServersCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + CancelInCbData *data = static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + data->done = true; + data->cbs++; + /* Frees every server absent from the new configuration, closing their + * connections -- which still hold the queries being torn down. */ + ares_set_servers_csv(data->channel, "127.0.0.1:53055"); +} + +// Reconfiguring the server list from a cancellation callback. +TEST_P(MockUDPSingleTryTest, ReconfigureInCancelCallbackNoDoubleFree) { + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + + QueryResult result; + /* Left unanswered, so both stay attached to the connection. */ + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, ReconfigureServersCallback, + &data, NULL)); + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.example.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, QueryCallback, &result, NULL)); + + ares_cancel(channel_); + EXPECT_TRUE(data.done); + EXPECT_TRUE(result.done_); + /* Each lookup must be notified exactly once, not twice. */ + EXPECT_EQ((size_t)1, data.cbs); +} + +/* The replacement lookup's result outlives the callback that starts it. */ +static QueryResult g_replacement_result; + +static void NewQueryOnFailingWriteCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + /* ares_set_socket_functions() stores the pointer rather than copying it, so + * this must outlive the call. */ + static ares_socket_functions sock_funcs; + CancelInCbData *data = static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + + data->cbs++; + + /* Only the first cancellation callback starts a replacement lookup. */ + if (data->done) { + return; + } + data->done = true; + + memset(&sock_funcs, 0, sizeof(sock_funcs)); + sock_funcs.asendv = LibraryTest::ares_sendv_fail; + ares_set_socket_functions(data->channel, &sock_funcs, NULL); + LibraryTest::SetFailSend(); + + ares_query_dnsrec(data->channel, "www.probe.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, QueryCallback, &g_replacement_result, + NULL); +} + +// No reconfiguration at all: the cancellation callback simply starts a +// replacement lookup -- an ordinary retry pattern -- and that lookup's write +// fails. ares_close_connection() then walks conn->queries_to_conn, which +// still holds the queries ares_cancel() is in the middle of. Wider than the +// case above: it needs nothing more than a connection error. +TEST_P(MockUDPSingleTryTest, NewQueryInCancelCallbackConnErrorNoDoubleFree) { + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + + g_replacement_result.done_ = false; + + QueryResult result; + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, NewQueryOnFailingWriteCallback, + &data, NULL)); + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.example.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, QueryCallback, &result, NULL)); + + ares_cancel(channel_); + EXPECT_TRUE(data.done); + EXPECT_TRUE(result.done_); + /* Each lookup must be notified exactly once, not twice. */ + EXPECT_EQ((size_t)1, data.cbs); + /* Pins that the replacement lookup ran and its write failed, which is what + * drives the connection teardown this test is about. */ + EXPECT_TRUE(g_replacement_result.done_); +} + +// The same shape through ares_destroy() rather than ares_cancel(). Swapping +// the all_queries head does not help here: the reentrant teardown reaches the +// query through the connection, not through all_queries. +TEST_P(MockUDPSingleTryTest, ReconfigureInDestroyCallbackNoDoubleFree) { + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + + QueryResult result; + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, ReconfigureServersCallback, + &data, NULL)); + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.example.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, QueryCallback, &result, NULL)); + + ares_destroy(channel_); + channel_ = nullptr; /* destroyed here, don't let the fixture repeat it */ + + EXPECT_TRUE(data.done); + EXPECT_TRUE(result.done_); + /* Each lookup must be notified exactly once, not twice. */ + EXPECT_EQ((size_t)1, data.cbs); +} + +// Once ares_destroy() has begun, a lookup started from a destruction callback +// is refused rather than allocated into a channel being torn down -- and the +// refusal still runs the caller's callback with ARES_EDESTRUCTION, so context +// attached to `arg` can be reclaimed. ares_cancel() leaves sys_up true, so a +// destruction callback is the only way to reach this. +struct DestroyReentryData { + ares_channel_t *channel; + size_t outer_cbs; + size_t replacement_cbs; + ares_status_t replacement_status; +}; + +static void DestroyReplacementCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + DestroyReentryData *data = static_cast(arg); + (void)timeouts; + (void)dnsrec; + data->replacement_cbs++; + data->replacement_status = status; +} + +static void DestroyReentryCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + DestroyReentryData *data = static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + data->outer_cbs++; + ares_query_dnsrec(data->channel, "www.example.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, DestroyReplacementCallback, data, NULL); +} + +TEST_F(LibraryTest, NewQueryInDestroyCallbackIsRefusedAndNotified) { + ares_channel_t *channel = nullptr; + struct ares_options opts; + int optmask = 0; + + memset(&opts, 0, sizeof(opts)); + opts.tries = 1; + optmask |= ARES_OPT_TRIES; + opts.flags = ARES_FLAG_NOSEARCH; + optmask |= ARES_OPT_FLAGS; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&channel, &opts, optmask)); + // Nothing listens here; no traffic is needed. + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(channel, "127.0.0.1:53055")); + + DestroyReentryData data; + data.channel = channel; + data.outer_cbs = 0; + data.replacement_cbs = 0; + data.replacement_status = ARES_SUCCESS; + + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel, "www.google.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, DestroyReentryCallback, &data, + NULL)); + + ares_destroy(channel); + + EXPECT_EQ((size_t)1, data.outer_cbs); + // Refused -- but notified, so the caller can release whatever it attached. + EXPECT_EQ((size_t)1, data.replacement_cbs); + EXPECT_EQ(ARES_EDESTRUCTION, data.replacement_status); +} + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPSingleTryTest, + ::testing::ValuesIn(ares::test::families), + PrintFamily); + +// ares_process_pending_write() flushes each server's TCP connection. A flush +// that fails tears the connection down, which ends a query that is out of +// retries and runs its callback right there. A callback that reconfigures the +// servers removes the one being flushed, and the walk must not go on to use +// that server, its connection or its place in the list. +class MockTCPSingleTryStayOpenTest + : public MockChannelOptsTest, + public ::testing::WithParamInterface { +public: + MockTCPSingleTryStayOpenTest() + : MockChannelOptsTest(1, GetParam(), true, false, FillOptions(&opts_), + ARES_OPT_TRIES | ARES_OPT_FLAGS) + { + } + + static struct ares_options *FillOptions(struct ares_options *opts) + { + memset(opts, 0, sizeof(struct ares_options)); + opts->tries = 1; + /* ARES_OPT_FLAGS replaces the defaults, which include EDNS. */ + opts->flags = ARES_FLAG_STAYOPEN | ARES_FLAG_EDNS; + return opts; + } + +private: + struct ares_options opts_; +}; + +static void PendingWriteNotifyCallback(void *data) +{ + *static_cast(data) = true; +} + +TEST_P(MockTCPSingleTryStayOpenTest, ReconfigureInPendingWriteFailureCallback) { + DNSPacket rsp; + rsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {2, 3, 4, 5})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &rsp)); + + /* Answered, so the TCP connection is connected; STAYOPEN keeps it. */ + QueryResult first; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, &first, NULL); + Process(); + EXPECT_TRUE(first.done_); + + /* From here writes on that connection wait for ares_process_pending_write. */ + bool pending = false; + ares_set_pending_write_cb(channel_, PendingWriteNotifyCallback, &pending); + + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + ares_query_dnsrec(channel_, "www.example.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, ReconfigureServersCallback, &data, NULL); + EXPECT_TRUE(pending); + EXPECT_FALSE(data.done); + + /* ares_set_socket_functions() stores the pointer rather than copying it. */ + static ares_socket_functions sock_funcs; + memset(&sock_funcs, 0, sizeof(sock_funcs)); + sock_funcs.asendv = ares_sendv_fail; + ares_set_socket_functions(channel_, &sock_funcs, NULL); + LibraryTest::SetFailSend(); + + ares_process_pending_write(channel_); + + EXPECT_TRUE(data.done); + EXPECT_EQ((size_t)1, data.cbs); + + ares_set_pending_write_cb(channel_, NULL, NULL); +} + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockTCPSingleTryStayOpenTest, + ::testing::ValuesIn(ares::test::families), + PrintFamily); + +TEST_P(MockUDPChannelTest, CancelInDestroyCallbackNoUseAfterFree) { + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + + /* Left unanswered, so both are still pending when the channel goes down. */ + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, CancelChannelCallback, &data, + NULL)); + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.example.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, CancelChannelCallback, &data, + NULL)); + /* Without this the test can pass vacuously if a query failed to start. */ + EXPECT_EQ(2, ares_queue_active_queries(channel_)); + + ares_destroy(channel_); + channel_ = nullptr; /* destroyed here, don't let the fixture repeat it */ + + EXPECT_TRUE(data.done); + /* Every query must be notified exactly once. */ + EXPECT_EQ((size_t)2, data.cbs); +} + +// Fixture with tries=1 so the first failure ends the query outright instead of +// retrying it -- that is what drives execution into end_query()'s synchronous +// (requeue==NULL) branch. +// The tests below use the MockUDPSingleTryTest fixture defined above: tries=1, +// so the first failure ends the query outright instead of retrying it, which +// is what drives execution into end_query()'s synchronous (requeue==NULL) +// branch. +// + +// Connection-teardown route into the synchronous branch. A malformed reply +// makes process_answer() return EBADRESP, so handle_conn_error() tears the +// connection down and ares_close_connection() requeues its queries with a NULL +// requeue; with tries=1 the query is ended right there. CVE-2026-33630 fixed +// the deferred flush path only, so this one still double-freed. +TEST_P(MockUDPSingleTryTest, CancelInCallbackConnTeardownNoDoubleFree) { + /* Too short to be a DNS header, so it fails to parse. */ + std::vector malformed = {0x00, 0x00, 0x00, 0x00}; + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReplyData(&server_, malformed)); + + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + CancelChannelCallback, &data, NULL); + Process(); + EXPECT_TRUE(data.done); +} + +// Server-removal route into the same synchronous branch, which the +// connection-teardown test above does not cover. Reconfiguring the server list +// destroys the servers that are gone, and ares_close_sockets() closes their +// connections with a NULL requeue -- a second way to reach end_query() with the +// query still linked. No Process() call is needed: the query is attached to a +// connection by ares_query_dnsrec() itself, and nothing has replied to it. +TEST_P(MockUDPSingleTryTest, CancelInCallbackServerRemovalNoDoubleFree) { + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + CancelChannelCallback, &data, NULL); + + /* Drops the server the in-flight query is attached to. */ + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(channel_, "127.0.0.1:53055")); + EXPECT_TRUE(data.done); +} + +// ares_send_query_int() calls handle_conn_error() on a retryable write failure +// and then keeps using its own query. That teardown requeues the *other* +// queries on the connection, and with tries=1 they are ended outright; if one +// of their callbacks reentrantly cancels the channel, this query is freed too +// -- it is still linked in all_queries, having only joined +// conn->queries_to_conn on the success path further down. Using it afterwards +// is a use-after-free. +// +// The first query is left unanswered so it stays on the connection, then the +// next write is made to fail with ECONNREFUSED so the second query takes the +// error path while the first is still attached. +// +// Note this is masked on code that has not fixed the end_query() double-free: +// that aborts inside handle_conn_error() before control returns here. +TEST_P(MockUDPSingleTryTest, CancelInSiblingCallbackConnErrorNoUseAfterFree) { + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + + /* No reply configured for this one, so it stays attached to the connection. */ + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + CancelChannelCallback, &data, NULL); + + ares_socket_functions sock_funcs; + memset(&sock_funcs, 0, sizeof(sock_funcs)); + sock_funcs.asendv = ares_sendv_fail; + ares_set_socket_functions(channel_, &sock_funcs, NULL); + LibraryTest::SetFailSend(); + + QueryResult result; + ares_query_dnsrec(channel_, "www.example.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, &result, NULL); + + /* The write failure above tears the connection down, which ends the first + * query, whose callback cancels the channel and frees this one. */ + EXPECT_TRUE(data.done); + /* The sibling is cancelled from the first query's callback rather than + * requeued; pin that rather than leaving it unstated. */ + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ECANCELLED, result.status_); +} + +// The socket-state callback -- registered with ARES_OPT_SOCK_STATE_CB, the +// usual way c-ares is driven from an event loop -- used to be dispatched from +// the middle of opening a connection, and ares_open_connection() keeps using +// that connection afterwards. A reentrant ares_cancel() from the callback +// frees both the connection (through the idle sweep) and the in-flight query, +// leaving the library using either. +struct SockStateCancelData { + ares_channel_t *channel; + size_t calls; +}; + +static void SockStateCancelCallback(void *data, ares_socket_t socket_fd, + int readable, int writable) +{ + SockStateCancelData *d = static_cast(data); + (void)socket_fd; + (void)readable; + (void)writable; + d->calls++; + // Reentrant cancel from the socket-state notification. + if (d->channel != nullptr) { + ares_cancel(d->channel); + } +} + +TEST_F(LibraryTest, CancelInSockStateCallbackNoUseAfterFree) { + SockStateCancelData data; + ares_channel_t *chan = nullptr; + struct ares_options opts; + int optmask = 0; + + data.channel = nullptr; + data.calls = 0; + + memset(&opts, 0, sizeof(opts)); + opts.sock_state_cb = SockStateCancelCallback; + opts.sock_state_cb_data = &data; + optmask = ARES_OPT_SOCK_STATE_CB; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&chan, &opts, optmask)); + ASSERT_NE(nullptr, chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "127.0.0.1:53055")); + + /* Armed only now, so channel setup itself does not cancel. */ + data.channel = chan; + + QueryResult result; + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, &result, NULL); + + /* Opening the connection notifies, the notification cancels, and the + * library must still be able to finish -- and the lookup must actually be + * cancelled rather than the cancel quietly doing nothing. */ + EXPECT_GT(data.calls, (size_t)0); + EXPECT_TRUE(result.done_); + EXPECT_EQ(ARES_ECANCELLED, result.status_); + + data.channel = nullptr; /* don't cancel from teardown notifications */ + ares_destroy(chan); +} + +// Deferring the notification must not reorder it past the close of the +// descriptor it names. An event loop is told "stop watching fd N"; if fd N is +// already closed by then, the number can have been handed to something else -- +// including to application code running from a deferred query callback -- and +// the application ends up setting and clearing interest in a descriptor it now +// owns for another purpose. +// +// The invariant is checked directly: while the callback naming the descriptor +// runs, that descriptor must still be open. +#ifndef _WIN32 +struct SockStateCloseOrderData { + size_t closes_seen; + size_t closed_while_already_shut; +}; + +static void SockStateCloseOrderCallback(void *data, ares_socket_t socket_fd, + int readable, int writable) +{ + SockStateCloseOrderData *d = static_cast(data); + if (readable || writable) { + return; + } + d->closes_seen++; + // F_GETFD on a closed descriptor fails with EBADF. c-ares must not have + // closed it yet at this point. + if (fcntl(static_cast(socket_fd), F_GETFD) == -1 && errno == EBADF) { + d->closed_while_already_shut++; + } +} + +TEST_F(LibraryTest, SockStateCloseIsReportedBeforeDescriptorCloses) { + SockStateCloseOrderData data; + ares_channel_t *chan = nullptr; + struct ares_options opts; + + data.closes_seen = 0; + data.closed_while_already_shut = 0; + + memset(&opts, 0, sizeof(opts)); + opts.sock_state_cb = SockStateCloseOrderCallback; + opts.sock_state_cb_data = &data; + + EXPECT_EQ(ARES_SUCCESS, + ares_init_options(&chan, &opts, ARES_OPT_SOCK_STATE_CB)); + ASSERT_NE(nullptr, chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "127.0.0.1:53055")); + + QueryResult result; + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, &result, NULL); + ares_destroy(chan); + + EXPECT_GT(data.closes_seen, (size_t)0); + EXPECT_EQ((size_t)0, data.closed_while_already_shut) + << "the application was told to stop watching a descriptor c-ares had " + "already closed"; +} +#endif + +// Reconfiguring the servers from an I/O hook frees the server and its +// connection while the library is still holding both. Deferred reclamation is +// what makes these safe: the objects are unlinked from every lookup structure +// immediately, so nothing can find them, but their memory survives until the +// outermost call unwinds. Without it these are heap-use-after-free in +// ares_conn_connect() and handle_conn_error() respectively. +// +// This is the class behind CVE-2025-31498 and CVE-2025-62408 -- a connection +// cleaned up while a caller further up the stack still held it. +struct ReconfigureFromHookData { + ares_channel_t *channel; + bool done; + int status; +}; + +static int ReconfigureConfigureCallback(ares_socket_t fd, int type, void *data) +{ + ReconfigureFromHookData *d = static_cast(data); + (void)fd; + (void)type; + // Frees the server this connection is being opened for. + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + return 0; +} + +static void ReconfigureFromHookCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) +{ + ReconfigureFromHookData *d = static_cast(arg); + (void)timeouts; + (void)dnsrec; + d->done = true; + d->status = static_cast(status); +} + +TEST_F(LibraryTest, ReconfigureInConfigureHookNoUseAfterFree) { + ares_channel_t *chan = nullptr; + struct ares_options opts; + int optmask = 0; + ReconfigureFromHookData data; + + memset(&opts, 0, sizeof(opts)); + opts.tries = 1; + optmask |= ARES_OPT_TRIES; + opts.flags = ARES_FLAG_NOSEARCH; + optmask |= ARES_OPT_FLAGS; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&chan, &opts, optmask)); + ASSERT_NE(nullptr, chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "127.0.0.1:53055")); + + data.channel = chan; + data.done = false; + data.status = ARES_SUCCESS; + ares_set_socket_configure_callback(chan, ReconfigureConfigureCallback, &data); + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + ReconfigureFromHookCallback, &data, NULL); + + // Must complete rather than be stranded -- every exit from the send path + // either enqueues the query or ends it. + EXPECT_TRUE(data.done); + EXPECT_EQ((size_t)0, ares_queue_active_queries(chan)); + ares_destroy(chan); +} + +// Reconfiguring the servers from the application's own send function. The +// write itself goes through: the send function reports every byte sent. But +// the reconfiguration has already closed the connection being written to and +// removed its server, so the query must be moved to the new configuration +// rather than attached to the closed connection -- where it would sit on a +// list that is freed once the call unwinds, in flight on no socket. +struct ReconfigureFromSendData { + ares_channel_t *channel; + bool done; + int status; +}; + +static ares_ssize_t ReconfigureSendv(ares_socket_t fd, const struct iovec *iov, + int iovcnt, void *user_data) +{ + ReconfigureFromSendData *d = + static_cast(user_data); + ares_ssize_t len = 0; + (void)fd; + for (int i = 0; i < iovcnt; i++) { + len += static_cast(iov[i].iov_len); + } + // Frees the server this connection belongs to. Reconfiguring to the same + // list again is a no-op, so the retry is written normally. + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + return len; +} + +static void ReconfigureFromSendCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) +{ + ReconfigureFromSendData *d = static_cast(arg); + (void)timeouts; + (void)dnsrec; + d->done = true; + d->status = static_cast(status); +} + +TEST_F(LibraryTest, ReconfigureInSuccessfulSendNoUseAfterFree) { + ares_channel_t *chan = nullptr; + struct ares_options opts; + int optmask = 0; + ares_socket_functions funcs; + ReconfigureFromSendData data; + ares_socket_t socks[ARES_GETSOCK_MAXNUM]; + + memset(&opts, 0, sizeof(opts)); + opts.flags = ARES_FLAG_NOSEARCH; + optmask |= ARES_OPT_FLAGS; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&chan, &opts, optmask)); + ASSERT_NE(nullptr, chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "127.0.0.1:53055")); + + data.channel = chan; + data.done = false; + data.status = ARES_SUCCESS; + memset(&funcs, 0, sizeof(funcs)); + funcs.asendv = ReconfigureSendv; + ares_set_socket_functions(chan, &funcs, &data); + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + ReconfigureFromSendCallback, &data, NULL); + + // Moved to the new server and written there, so it is in flight on a + // socket the application can watch. + EXPECT_FALSE(data.done); + EXPECT_EQ((size_t)1, ares_queue_active_queries(chan)); + EXPECT_NE(0, ares_getsock(chan, socks, ARES_GETSOCK_MAXNUM)); + + ares_cancel(chan); + EXPECT_TRUE(data.done); + EXPECT_EQ(ARES_ECANCELLED, data.status); + EXPECT_EQ((size_t)0, ares_queue_active_queries(chan)); + ares_destroy(chan); +} + +// The connection the send function tore down has already been reported as +// closed, and its descriptor closed or handed to that notification. Nothing +// after the send may report socket state for it: doing so names +// ARES_SOCKET_BAD, a descriptor the application was never given. +struct BadSocketStateData { + size_t calls; + size_t bad; +}; + +static void BadSocketStateCallback(void *data, ares_socket_t socket_fd, + int readable, int writable) +{ + BadSocketStateData *d = static_cast(data); + (void)readable; + (void)writable; + d->calls++; + if (socket_fd == ARES_SOCKET_BAD) { + d->bad++; + } +} + +TEST_F(LibraryTest, ReconfigureInSendReportsNoBadSocket) { + ares_channel_t *chan = nullptr; + struct ares_options opts; + int optmask = 0; + ares_socket_functions funcs; + ReconfigureFromSendData data; + BadSocketStateData sock; + + memset(&opts, 0, sizeof(opts)); + memset(&sock, 0, sizeof(sock)); + opts.flags = ARES_FLAG_NOSEARCH; + optmask |= ARES_OPT_FLAGS; + opts.sock_state_cb = BadSocketStateCallback; + opts.sock_state_cb_data = &sock; + optmask |= ARES_OPT_SOCK_STATE_CB; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&chan, &opts, optmask)); + ASSERT_NE(nullptr, chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "127.0.0.1:53055")); + + data.channel = chan; + data.done = false; + data.status = ARES_SUCCESS; + memset(&funcs, 0, sizeof(funcs)); + funcs.asendv = ReconfigureSendv; + ares_set_socket_functions(chan, &funcs, &data); + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + ReconfigureFromSendCallback, &data, NULL); + ares_cancel(chan); + EXPECT_TRUE(data.done); + + EXPECT_NE((size_t)0, sock.calls); + EXPECT_EQ((size_t)0, sock.bad); + ares_destroy(chan); +} + +// The same reconfiguration from the send function, reached through the +// deferred TCP write: ares_set_pending_write_cb() queues the write and the +// application later flushes it with ares_process_pending_write(). That flush +// is its own outermost library call, so the connection and server the +// reconfiguration closes must stay readable until it returns, and its walk of +// the server list must not step from a list node the reconfiguration freed. +// Covers both a send that reports success and one that fails. A failure +// on the removed server must not be charged to the server that replaced it. +struct PendingWriteReconfigureData { + ares_channel_t *channel; + bool reconfigure; + bool fail; + bool pending; + size_t done; + size_t server_failures; +}; + +static int PendingWriteConnect(ares_socket_t fd, const struct sockaddr *addr, + ares_socklen_t addrlen, void *user_data) +{ + (void)fd; + (void)addr; + (void)addrlen; + (void)user_data; + // Pretend the connection is in progress; the test signals it writable. + return 0; +} + +static ares_ssize_t PendingWriteSendv(ares_socket_t fd, const struct iovec *iov, + int iovcnt, void *user_data) +{ + PendingWriteReconfigureData *d = + static_cast(user_data); + ares_ssize_t len = 0; + (void)fd; + for (int i = 0; i < iovcnt; i++) { + len += static_cast(iov[i].iov_len); + } + if (!d->reconfigure) { + return len; + } + d->reconfigure = false; + // Closes this connection and frees its server. + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + if (d->fail) { +#ifdef USE_WINSOCK + WSASetLastError(WSAECONNREFUSED); +#else + errno = ECONNREFUSED; +#endif + return -1; + } + return len; +} + +static void PendingWriteNotify(void *user_data) +{ + PendingWriteReconfigureData *d = + static_cast(user_data); + d->pending = true; +} + +static void PendingWriteServerState(const char *server_string, + ares_bool_t success, int flags, + void *user_data) +{ + PendingWriteReconfigureData *d = + static_cast(user_data); + (void)server_string; + (void)flags; + if (!success) { + d->server_failures++; + } +} + +static void PendingWriteCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) +{ + PendingWriteReconfigureData *d = + static_cast(arg); + (void)timeouts; + (void)dnsrec; + EXPECT_EQ(ARES_ECANCELLED, status); + d->done++; +} + +static void ReconfigureInPendingWrite(bool fail) +{ + ares_channel_t *chan = nullptr; + struct ares_options opts; + int optmask = 0; + ares_socket_functions funcs; + PendingWriteReconfigureData data; + ares_socket_t socks[ARES_GETSOCK_MAXNUM]; + int bitmask; + + memset(&opts, 0, sizeof(opts)); + opts.flags = ARES_FLAG_NOSEARCH | ARES_FLAG_USEVC; + optmask |= ARES_OPT_FLAGS; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&chan, &opts, optmask)); + ASSERT_NE(nullptr, chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "127.0.0.1:53055")); + + data.channel = chan; + data.reconfigure = false; + data.fail = fail; + data.pending = false; + data.done = 0; + data.server_failures = 0; + memset(&funcs, 0, sizeof(funcs)); + funcs.aconnect = PendingWriteConnect; + funcs.asendv = PendingWriteSendv; + ares_set_socket_functions(chan, &funcs, &data); + ares_set_pending_write_cb(chan, PendingWriteNotify, &data); + ares_set_server_state_callback(chan, PendingWriteServerState, &data); + + // The first query opens the connection; its write waits for it to connect. + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + PendingWriteCallback, &data, NULL); + bitmask = ares_getsock(chan, socks, ARES_GETSOCK_MAXNUM); + ASSERT_NE(0, ARES_GETSOCK_WRITABLE(bitmask, 0)); + + // Report it connected, which writes the first query. + ares_process_fd(chan, ARES_SOCKET_BAD, socks[0]); + EXPECT_FALSE(data.pending); + + // Now connected, so the second query's write is deferred to the + // application. + ares_query_dnsrec(chan, "www.example.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + PendingWriteCallback, &data, NULL); + EXPECT_TRUE(data.pending); + + data.reconfigure = true; + ares_process_pending_write(chan); + EXPECT_FALSE(data.reconfigure); + + // Both queries were moved to the new server, which has not failed. + EXPECT_EQ((size_t)0, data.done); + EXPECT_EQ((size_t)0, data.server_failures); + EXPECT_EQ((size_t)2, ares_queue_active_queries(chan)); + + ares_cancel(chan); + EXPECT_EQ((size_t)2, data.done); + EXPECT_EQ((size_t)0, ares_queue_active_queries(chan)); + ares_destroy(chan); +} + +TEST_F(LibraryTest, ReconfigureInSuccessfulPendingWriteNoUseAfterFree) { + ReconfigureInPendingWrite(false); +} + +TEST_F(LibraryTest, ReconfigureInFailedPendingWriteNoUseAfterFree) { + ReconfigureInPendingWrite(true); +} + +// The pending-write callback is application code too, and runs once the query +// has been buffered on the connection. Reconfiguring from it closes that +// connection, so the query must be requeued rather than attached to it. +static void PendingWriteReconfigureNotify(void *user_data) +{ + PendingWriteReconfigureData *d = + static_cast(user_data); + d->pending = true; + if (d->reconfigure) { + d->reconfigure = false; + // Closes the connection the query was just buffered on. + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + } +} + +TEST_F(LibraryTest, ReconfigureInPendingWriteCallbackNoUseAfterFree) { + ares_channel_t *chan = nullptr; + struct ares_options opts; + int optmask = 0; + ares_socket_functions funcs; + PendingWriteReconfigureData data; + ares_socket_t socks[ARES_GETSOCK_MAXNUM]; + int bitmask; + + memset(&opts, 0, sizeof(opts)); + opts.flags = ARES_FLAG_NOSEARCH | ARES_FLAG_USEVC; + optmask |= ARES_OPT_FLAGS; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&chan, &opts, optmask)); + ASSERT_NE(nullptr, chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "127.0.0.1:53055")); + + data.channel = chan; + data.reconfigure = false; + data.fail = false; + data.pending = false; + data.done = 0; + data.server_failures = 0; + memset(&funcs, 0, sizeof(funcs)); + funcs.aconnect = PendingWriteConnect; + funcs.asendv = PendingWriteSendv; + ares_set_socket_functions(chan, &funcs, &data); + ares_set_pending_write_cb(chan, PendingWriteReconfigureNotify, &data); + + // The first query opens the connection; its write waits for it to connect. + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + PendingWriteCallback, &data, NULL); + bitmask = ares_getsock(chan, socks, ARES_GETSOCK_MAXNUM); + ASSERT_NE(0, ARES_GETSOCK_WRITABLE(bitmask, 0)); + + // Report it connected, which writes the first query. + ares_process_fd(chan, ARES_SOCKET_BAD, socks[0]); + EXPECT_FALSE(data.pending); + + // Now connected, so the second query is buffered and the pending-write + // callback runs, reconfiguring the servers. + data.reconfigure = true; + ares_query_dnsrec(chan, "www.example.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + PendingWriteCallback, &data, NULL); + EXPECT_TRUE(data.pending); + EXPECT_FALSE(data.reconfigure); + + // Both queries were moved to the new server. + EXPECT_EQ((size_t)0, data.done); + EXPECT_EQ((size_t)2, ares_queue_active_queries(chan)); + + ares_cancel(chan); + EXPECT_EQ((size_t)2, data.done); + EXPECT_EQ((size_t)0, ares_queue_active_queries(chan)); + ares_destroy(chan); +} + +// Fake socket functions, installed with ares_set_socket_functions_ex(), that +// never touch a real descriptor. Each test arms one of them to reconfigure +// the servers part-way through c-ares's use of a connection, and they count +// every call that names ARES_SOCKET_BAD, which a closed connection must never +// pass to application code. +struct FakeSockData { + ares_channel_t *channel; + ares_socket_t next_fd; + bool tfo; + bool getsockname_fail_first; + ares_socket_t getsockname_last_fd; + bool getsockname_reconfigure; + bool recv_reconfigure; + bool recv_answer_then_reconfigure; + bool recv_answered; + bool recv_answer; + bool close_reconfigure; + unsigned char query[512]; + size_t query_len; + struct sockaddr_in peer; + unsigned short recv_port; /* network order; 0 reports the peer's port */ + bool recv_no_port; /* report the peer's address with port 0 */ + size_t recv_calls; + size_t bad_fd_calls; + size_t sock_state_bad; + bool done; + int status; +}; + +static void FakeSockSetErrno(bool wouldblock) +{ +#ifdef USE_WINSOCK + WSASetLastError(wouldblock ? WSAEWOULDBLOCK : WSAENOTCONN); +#else + errno = wouldblock ? EAGAIN : ENOTCONN; +#endif +} + +static ares_socket_t FakeSockSocket(int domain, int type, int protocol, + void *user_data) +{ + FakeSockData *d = static_cast(user_data); + (void)domain; + (void)type; + (void)protocol; + return d->next_fd++; +} + +static int FakeSockClose(ares_socket_t sock, void *user_data) +{ + FakeSockData *d = static_cast(user_data); + if (sock == ARES_SOCKET_BAD) { + d->bad_fd_calls++; + } + if (d->close_reconfigure) { + d->close_reconfigure = false; + // Frees the server this connection belonged to. + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + } + return 0; +} + +static int FakeSockSetsockopt(ares_socket_t sock, ares_socket_opt_t opt, + const void *val, ares_socklen_t val_size, + void *user_data) +{ + FakeSockData *d = static_cast(user_data); + (void)sock; + (void)val; + (void)val_size; + if (opt == ARES_SOCKET_OPT_TCP_FASTOPEN && !d->tfo) { + return -1; + } + return 0; +} + +static int FakeSockConnect(ares_socket_t sock, const struct sockaddr *address, + ares_socklen_t address_len, unsigned int flags, + void *user_data) +{ + FakeSockData *d = static_cast(user_data); + (void)sock; + (void)flags; + if (address->sa_family == AF_INET && address_len >= sizeof(d->peer)) { + memcpy(&d->peer, address, sizeof(d->peer)); + } + return 0; +} + +static ares_ssize_t FakeSockRecvfrom(ares_socket_t sock, void *buffer, + size_t length, int flags, + struct sockaddr *address, + ares_socklen_t *address_len, + void *user_data) +{ + FakeSockData *d = static_cast(user_data); + (void)flags; + d->recv_calls++; + if (sock == ARES_SOCKET_BAD) { + d->bad_fd_calls++; + } + if (d->query_len < 12 || length < d->query_len) { + FakeSockSetErrno(true); + return -1; + } + if (d->recv_answer_then_reconfigure) { + // Answer first; reconfigure on the next call, which has nothing to read. + if (d->recv_answered) { + d->recv_answer_then_reconfigure = false; + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + FakeSockSetErrno(true); + return -1; + } + d->recv_answered = true; + } else if (d->recv_reconfigure) { + d->recv_reconfigure = false; + // Frees the server this connection belongs to. + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + } else if (d->recv_answer) { + d->recv_answer = false; + } else { + FakeSockSetErrno(true); + return -1; + } + // Answer the query: the same message with QR set, no records. + memcpy(buffer, d->query, d->query_len); + static_cast(buffer)[2] |= 0x80; + if (address != NULL && address_len != NULL && + *address_len >= (ares_socklen_t)sizeof(d->peer)) { + memcpy(address, &d->peer, sizeof(d->peer)); + if (d->recv_no_port) { + reinterpret_cast(address)->sin_port = 0; + } else if (d->recv_port != 0) { + reinterpret_cast(address)->sin_port = d->recv_port; + } + *address_len = sizeof(d->peer); + } + return static_cast(d->query_len); +} + +static ares_ssize_t FakeSockSendto(ares_socket_t sock, const void *buffer, + size_t length, int flags, + const struct sockaddr *address, + ares_socklen_t address_len, void *user_data) +{ + FakeSockData *d = static_cast(user_data); + (void)flags; + (void)address; + (void)address_len; + if (sock == ARES_SOCKET_BAD) { + d->bad_fd_calls++; + } + if (length <= sizeof(d->query)) { + memcpy(d->query, buffer, length); + d->query_len = length; + } + return static_cast(length); +} + +static int FakeSockGetsockname(ares_socket_t sock, struct sockaddr *address, + ares_socklen_t *address_len, void *user_data) +{ + FakeSockData *d = static_cast(user_data); + struct sockaddr_in sin; + if (sock == ARES_SOCKET_BAD) { + d->bad_fd_calls++; + } + // As during TCP Fast Open, before the first write has connected. + if (d->getsockname_fail_first && sock != d->getsockname_last_fd) { + d->getsockname_last_fd = sock; + FakeSockSetErrno(false); + return -1; + } + if (d->getsockname_reconfigure) { + d->getsockname_reconfigure = false; + // Frees the server this connection belongs to. + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + } + if (*address_len < (ares_socklen_t)sizeof(sin)) { + FakeSockSetErrno(false); + return -1; + } + memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + sin.sin_port = htons(40000); + sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + memcpy(address, &sin, sizeof(sin)); + *address_len = sizeof(sin); + return 0; +} + +static void FakeSockStateCallback(void *data, ares_socket_t socket_fd, + int readable, int writable) +{ + FakeSockData *d = static_cast(data); + (void)readable; + (void)writable; + if (socket_fd == ARES_SOCKET_BAD) { + d->sock_state_bad++; + } +} + +static void FakeSockQueryCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) +{ + FakeSockData *d = static_cast(arg); + (void)timeouts; + (void)dnsrec; + d->done = true; + d->status = static_cast(status); +} + +static void FakeSockInit(ares_channel_t **chan, FakeSockData *data, + int extra_flags, unsigned int sockfunc_flags, + bool sock_state = true) +{ + struct ares_options opts; + int optmask = 0; + struct ares_socket_functions_ex funcs; + + memset(&opts, 0, sizeof(opts)); + opts.flags = ARES_FLAG_NOSEARCH | extra_flags; + optmask |= ARES_OPT_FLAGS; + // A socket-state callback defers the close of a connection's descriptor + // until the outermost call unwinds; without one it is closed inline. + if (sock_state) { + opts.sock_state_cb = FakeSockStateCallback; + opts.sock_state_cb_data = data; + optmask |= ARES_OPT_SOCK_STATE_CB; + } + ASSERT_EQ(ARES_SUCCESS, ares_init_options(chan, &opts, optmask)); + ASSERT_NE(nullptr, *chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(*chan, "127.0.0.1:53055")); + + data->channel = *chan; + data->next_fd = 100; + data->getsockname_last_fd = ARES_SOCKET_BAD; + + memset(&funcs, 0, sizeof(funcs)); + funcs.version = 1; + funcs.flags = sockfunc_flags; + funcs.asocket = FakeSockSocket; + funcs.aclose = FakeSockClose; + funcs.asetsockopt = FakeSockSetsockopt; + funcs.aconnect = FakeSockConnect; + funcs.arecvfrom = FakeSockRecvfrom; + funcs.asendto = FakeSockSendto; + funcs.agetsockname = FakeSockGetsockname; + EXPECT_EQ(ARES_SUCCESS, ares_set_socket_functions_ex(*chan, &funcs, data)); +} + +// The getsockname function runs for every new connection, after the checks +// that follow the socket-configure and socket-create callbacks. Reconfiguring +// from it must not leave the connection registered on the removed server. +TEST_F(LibraryTest, ReconfigureInGetsocknameNoUseAfterFree) { + ares_channel_t *chan = nullptr; + FakeSockData data; + + memset(&data, 0, sizeof(data)); + FakeSockInit(&chan, &data, 0, 0); + data.getsockname_reconfigure = true; + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + FakeSockQueryCallback, &data, NULL); + EXPECT_FALSE(data.getsockname_reconfigure); + + // The first connection's descriptor: no longer known to the channel. + ares_process_fd(chan, 100, ARES_SOCKET_BAD); + + // Moved to the new server. + EXPECT_FALSE(data.done); + EXPECT_EQ((size_t)1, ares_queue_active_queries(chan)); + + ares_cancel(chan); + EXPECT_TRUE(data.done); + EXPECT_EQ(ARES_ECANCELLED, data.status); + EXPECT_EQ((size_t)0, data.bad_fd_calls); + EXPECT_EQ((size_t)0, data.sock_state_bad); + ares_destroy(chan); +} + +// With TCP Fast Open the first getsockname may fail, and is retried after the +// first write -- from inside ares_conn_write(), after the send function has +// returned. Reconfiguring from that retry closes the connection the query was +// just written on. +TEST_F(LibraryTest, ReconfigureInFastOpenGetsocknameNoUseAfterFree) { + ares_channel_t *chan = nullptr; + FakeSockData data; + + memset(&data, 0, sizeof(data)); + FakeSockInit(&chan, &data, ARES_FLAG_USEVC, 0); + data.tfo = true; + data.getsockname_fail_first = true; + data.getsockname_reconfigure = true; + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + FakeSockQueryCallback, &data, NULL); + EXPECT_FALSE(data.getsockname_reconfigure); + + // Moved to the new server. + EXPECT_FALSE(data.done); + EXPECT_EQ((size_t)1, ares_queue_active_queries(chan)); + EXPECT_EQ((size_t)0, data.sock_state_bad); + + ares_cancel(chan); + EXPECT_TRUE(data.done); + EXPECT_EQ(ARES_ECANCELLED, data.status); + EXPECT_EQ((size_t)0, data.bad_fd_calls); + ares_destroy(chan); +} + +// A non-blocking receive function that reconfigures the servers and returns +// an answer. The connection it read from is closed, so the answer must be +// dropped -- the query has already moved to the new server -- and the +// receive function must not be called again on the closed connection. +TEST_F(LibraryTest, ReconfigureInRecvDropsStaleAnswer) { + ares_channel_t *chan = nullptr; + FakeSockData data; + + memset(&data, 0, sizeof(data)); + FakeSockInit(&chan, &data, 0, ARES_SOCKFUNC_FLAG_NONBLOCKING); + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + FakeSockQueryCallback, &data, NULL); + ASSERT_NE((size_t)0, data.query_len); + + data.recv_reconfigure = true; + ares_process_fd(chan, 100, ARES_SOCKET_BAD); + EXPECT_FALSE(data.recv_reconfigure); + + EXPECT_EQ((size_t)1, data.recv_calls); + EXPECT_EQ((size_t)0, data.bad_fd_calls); + // Still in flight on the new server. + EXPECT_FALSE(data.done); + EXPECT_EQ((size_t)1, ares_queue_active_queries(chan)); + + ares_cancel(chan); + EXPECT_TRUE(data.done); + EXPECT_EQ(ARES_ECANCELLED, data.status); + ares_destroy(chan); +} + +// As above, but the answer comes from an earlier receive call, and a later +// call -- which has nothing more to read -- reconfigures the servers. The +// answer is already buffered on the connection that call tore down, and must +// be dropped with it. +TEST_F(LibraryTest, ReconfigureInRecvDropsBufferedAnswer) { + ares_channel_t *chan = nullptr; + FakeSockData data; + + memset(&data, 0, sizeof(data)); + FakeSockInit(&chan, &data, 0, ARES_SOCKFUNC_FLAG_NONBLOCKING); + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + FakeSockQueryCallback, &data, NULL); + ASSERT_NE((size_t)0, data.query_len); + + data.recv_answer_then_reconfigure = true; + ares_process_fd(chan, 100, ARES_SOCKET_BAD); + EXPECT_FALSE(data.recv_answer_then_reconfigure); + + EXPECT_EQ((size_t)2, data.recv_calls); + EXPECT_EQ((size_t)0, data.bad_fd_calls); + // Still in flight on the new server. + EXPECT_FALSE(data.done); + EXPECT_EQ((size_t)1, ares_queue_active_queries(chan)); + + ares_cancel(chan); + EXPECT_TRUE(data.done); + EXPECT_EQ(ARES_ECANCELLED, data.status); + ares_destroy(chan); +} + +// Socket functions need not filter on connect(), so ares_conn_read() checks +// a UDP datagram's source itself. An answer reported from the server's +// address but another port (here 127.0.0.1:53 for a server at +// 127.0.0.1:53055) must be dropped, so that one server cannot answer for +// another on the same address. The same answer from the right port is then +// accepted. +TEST_F(LibraryTest, UDPAnswerFromWrongPortDropped) { + ares_channel_t *chan = nullptr; + FakeSockData data; + + memset(&data, 0, sizeof(data)); + FakeSockInit(&chan, &data, 0, ARES_SOCKFUNC_FLAG_NONBLOCKING); + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + FakeSockQueryCallback, &data, NULL); + ASSERT_NE((size_t)0, data.query_len); + ASSERT_EQ(htons(53055), data.peer.sin_port); + + data.recv_port = htons(53); + data.recv_answer = true; + ares_process_fd(chan, 100, ARES_SOCKET_BAD); + EXPECT_FALSE(data.recv_answer); + EXPECT_FALSE(data.done); + EXPECT_EQ((size_t)1, ares_queue_active_queries(chan)); + + data.recv_port = 0; + data.recv_answer = true; + ares_process_fd(chan, 100, ARES_SOCKET_BAD); + EXPECT_FALSE(data.recv_answer); + EXPECT_TRUE(data.done); + // The fake answer carries no records. + EXPECT_EQ(ARES_ENODATA, data.status); + EXPECT_EQ((size_t)0, ares_queue_active_queries(chan)); + ares_destroy(chan); +} + +// Socket functions written before the port check may fill in the source +// address but leave the port 0. The address is still checked, but the answer +// must not be dropped for its port. +TEST_F(LibraryTest, UDPAnswerWithoutPortAccepted) +{ + ares_channel_t *chan = nullptr; + FakeSockData data; + + memset(&data, 0, sizeof(data)); + FakeSockInit(&chan, &data, 0, ARES_SOCKFUNC_FLAG_NONBLOCKING); + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + FakeSockQueryCallback, &data, NULL); + ASSERT_NE((size_t)0, data.query_len); + ASSERT_EQ(htons(53055), data.peer.sin_port); + + data.recv_no_port = true; + data.recv_answer = true; + ares_process_fd(chan, 100, ARES_SOCKET_BAD); + EXPECT_FALSE(data.recv_answer); + EXPECT_TRUE(data.done); + // The fake answer carries no records. + EXPECT_EQ(ARES_ENODATA, data.status); + EXPECT_EQ((size_t)0, ares_queue_active_queries(chan)); + ares_destroy(chan); +} + +// The close function is application code too. With no socket-state callback +// to carry the descriptor, it runs inline while ares_check_cleanup_conns() +// walks the server list and each server's connections, closing idle ones. +// Reconfiguring from it frees the list nodes that walk holds. +TEST_F(LibraryTest, ReconfigureInCloseNoUseAfterFree) { + ares_channel_t *chan = nullptr; + FakeSockData data; + + memset(&data, 0, sizeof(data)); + FakeSockInit(&chan, &data, 0, 0, false); + + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + FakeSockQueryCallback, &data, NULL); + ASSERT_NE((size_t)0, data.query_len); + + // Answer it; the now idle connection is then closed. + data.recv_answer = true; + data.close_reconfigure = true; + ares_process_fd(chan, 100, ARES_SOCKET_BAD); + EXPECT_FALSE(data.recv_answer); + EXPECT_FALSE(data.close_reconfigure); + + EXPECT_TRUE(data.done); + // The fake answer carries no records. + EXPECT_EQ(ARES_ENODATA, data.status); + EXPECT_EQ((size_t)0, ares_queue_active_queries(chan)); + EXPECT_EQ((size_t)0, data.bad_fd_calls); + ares_destroy(chan); +} + +// The second route to the same freed connection, and the one that drove the +// change from suppressing the idle sweep to deferring the notification: +// reconfiguring the servers from the socket-state callback destroys the server +// and with it the connection ares_open_connection() is still using. Nothing +// covered this route, so a regression in it would have gone unnoticed. +struct SockStateReconfigureData { + ares_channel_t *channel; + size_t calls; +}; + +static void SockStateReconfigureCallback(void *data, ares_socket_t socket_fd, + int readable, int writable) +{ + SockStateReconfigureData *d = + static_cast(data); + (void)socket_fd; + (void)readable; + (void)writable; + d->calls++; + if (d->channel != nullptr) { + // Destroys the server, and with it the connection being opened. + ares_set_servers_csv(d->channel, "127.0.0.1:53056"); + } +} + +TEST_F(LibraryTest, ReconfigureInSockStateCallbackNoUseAfterFree) { + SockStateReconfigureData data; + ares_channel_t *chan = nullptr; + struct ares_options opts; + + data.channel = nullptr; + data.calls = 0; + + memset(&opts, 0, sizeof(opts)); + opts.sock_state_cb = SockStateReconfigureCallback; + opts.sock_state_cb_data = &data; + + EXPECT_EQ(ARES_SUCCESS, + ares_init_options(&chan, &opts, ARES_OPT_SOCK_STATE_CB)); + ASSERT_NE(nullptr, chan); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "127.0.0.1:53055")); + + /* Armed only now, so channel setup itself does not reconfigure. */ + data.channel = chan; + + QueryResult result; + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, &result, NULL); + + EXPECT_GT(data.calls, (size_t)0); + + data.channel = nullptr; /* don't reconfigure from teardown notifications */ + ares_destroy(chan); +} +// Server-state notifications raised while a public call is issuing its first +// request must not be delivered until that whole call has unwound. A +// failover handler that reconfigures the servers ends the in-flight request, +// and with it the state the issuing call still has to finish with. The IPv6 +// server cannot get a socket, so the first send fails over to the IPv4 one +// and queues a failure notification for the handler below. +static ares_socket_t FailoverNoIPv6Socket(int af, int type, int protocol, + void *user_data) +{ + (void)user_data; + if (af == AF_INET6) { + return ARES_SOCKET_BAD; + } + return socket(af, type, protocol); +} + +static void FailoverOnServerDown(const char *server_string, ares_bool_t success, + int flags, void *data) +{ + (void)server_string; + (void)flags; + if (!success) { + ares_set_servers_csv(static_cast(data), + "127.0.0.1:53063"); + } +} + +static ares_channel_t *FailoverChannel(ares_socket_functions *funcs) +{ + ares_channel_t *chan = nullptr; + struct ares_options opts; + + memset(&opts, 0, sizeof(opts)); + opts.tries = 1; + opts.flags = ARES_FLAG_NOSEARCH; + EXPECT_EQ(ARES_SUCCESS, + ares_init_options(&chan, &opts, ARES_OPT_TRIES | ARES_OPT_FLAGS)); + if (chan == nullptr) { + return nullptr; + } + EXPECT_EQ(ARES_SUCCESS, + ares_set_servers_csv(chan, "[::1]:53061,127.0.0.1:53062")); + ares_set_server_state_callback(chan, FailoverOnServerDown, chan); + + memset(funcs, 0, sizeof(*funcs)); + funcs->asocket = FailoverNoIPv6Socket; + ares_set_socket_functions(chan, funcs, NULL); + return chan; +} + +TEST_F(LibraryTest, ServerFailoverDuringGetAddrInfo) { + ares_socket_functions funcs; + struct ares_addrinfo_hints hints; + AddrInfoResult result; + ares_channel_t *chan = FailoverChannel(&funcs); + ASSERT_NE(nullptr, chan); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + ares_getaddrinfo(chan, "www.google.com", NULL, &hints, AddrInfoCallback, + &result); + + ares_cancel(chan); + EXPECT_TRUE(result.done_); + ares_destroy(chan); +} + +// Same, through ares_query_dnsrec(): the query id must already have been +// reported to the caller by the time the query can complete. +struct FailoverQueryData { + unsigned short qid; + unsigned short qid_at_callback; + bool done; +}; + +static void FailoverQueryCallback(void *data, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) +{ + FailoverQueryData *d = static_cast(data); + (void)status; + (void)timeouts; + (void)dnsrec; + d->qid_at_callback = d->qid; + d->done = true; +} + +TEST_F(LibraryTest, ServerFailoverDuringQuery) { + ares_socket_functions funcs; + FailoverQueryData data; + ares_channel_t *chan = FailoverChannel(&funcs); + ASSERT_NE(nullptr, chan); + + data.qid = 0; + data.qid_at_callback = 0; + data.done = false; + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(chan, "www.google.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, FailoverQueryCallback, &data, + &data.qid)); + + EXPECT_TRUE(data.done); + EXPECT_EQ(data.qid, data.qid_at_callback); + ares_destroy(chan); +} + + +// Deferring the notifications must not lose them: every public entry point +// that closes a socket still has to tell the application about it before it +// returns, or an event loop is left polling a descriptor that c-ares closed. +struct SockStateRecordData { + std::vector> events; /* fd, still-armed */ +}; + +static void SockStateRecordCallback(void *data, ares_socket_t socket_fd, + int readable, int writable) +{ + SockStateRecordData *d = static_cast(data); + d->events.push_back(std::make_pair(socket_fd, readable || writable)); +} + +static bool SawClose(const SockStateRecordData &d) +{ + for (size_t i = 0; i < d.events.size(); i++) { + if (!d.events[i].second) { + return true; + } + } + return false; +} + +class SockStateDeliveryTest : public LibraryTest { +protected: + void OpenConnection(ares_channel_t **chan, SockStateRecordData *data, + QueryResult *result) + { + struct ares_options opts; + memset(&opts, 0, sizeof(opts)); + opts.sock_state_cb = SockStateRecordCallback; + opts.sock_state_cb_data = data; + + ASSERT_EQ(ARES_SUCCESS, + ares_init_options(chan, &opts, ARES_OPT_SOCK_STATE_CB)); + ASSERT_NE(nullptr, *chan); + /* TEST-NET-1, guaranteed not to answer, so the socket is still open when + * the call under test runs. */ + ASSERT_EQ(ARES_SUCCESS, ares_set_servers_csv(*chan, "192.0.2.1:53")); + + ares_query_dnsrec(*chan, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, result, NULL); + ASSERT_FALSE(result->done_); + ASSERT_FALSE(data->events.empty()); + ASSERT_TRUE(data->events.back().second); /* opened, armed for read */ + data->events.clear(); + } +}; + +TEST_F(SockStateDeliveryTest, CancelReportsSocketClose) { + ares_channel_t *chan = nullptr; + SockStateRecordData data; + QueryResult result; + + OpenConnection(&chan, &data, &result); + ares_cancel(chan); + EXPECT_TRUE(SawClose(data)) << "ares_cancel() closed the socket without " + "telling the application"; + ares_destroy(chan); +} + +TEST_F(SockStateDeliveryTest, SetServersReportsSocketClose) { + ares_channel_t *chan = nullptr; + SockStateRecordData data; + QueryResult result; + + OpenConnection(&chan, &data, &result); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(chan, "198.51.100.1:53")); + EXPECT_TRUE(SawClose(data)) << "ares_set_servers_csv() closed the socket " + "without telling the application"; + ares_destroy(chan); +} + +TEST_F(SockStateDeliveryTest, DestroyReportsSocketClose) { + ares_channel_t *chan = nullptr; + SockStateRecordData data; + QueryResult result; + + OpenConnection(&chan, &data, &result); + ares_destroy(chan); + EXPECT_TRUE(SawClose(data)) << "ares_destroy() closed the socket without " + "telling the application"; +} + +// The server-state callback is dispatched from the middle of processing, and +// every caller keeps using its query afterwards. Nothing documents the +// callback as non-reentrant, and the channel lock is recursive precisely so +// API calls from callbacks are allowed -- so an application that cancels from +// it left the library using a freed query. +// +// No hostile input is needed: an ordinary timeout reaches the same place. A +// SERVFAIL is used here because it is deterministic in the harness. +struct ServerStateCancelData { + ares_channel_t *channel; + int server_cb_calls; + bool query_done; +}; + +static void ServerStateCancelCallback(const char *server_string, + ares_bool_t success, int flags, + void *data) { + ServerStateCancelData *d = static_cast(data); + (void)server_string; + (void)success; + (void)flags; + d->server_cb_calls++; + // Reentrant cancel from the notification. + ares_cancel(d->channel); +} + +static void ServerStateQueryCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + ServerStateCancelData *d = static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + d->query_done = true; +} + +// The advisory's headline claim is that no hostile input is needed, because an +// ordinary timeout reaches the same place. The SERVFAIL test below does not +// show that; this one does. +class ServerStateTimeoutCancelTest + : public MockChannelOptsTest, + public ::testing::WithParamInterface { + public: + ServerStateTimeoutCancelTest() + : MockChannelOptsTest(1, GetParam(), false, false, FillOptions(&opts_), + ARES_OPT_TIMEOUTMS | ARES_OPT_TRIES) {} + static struct ares_options *FillOptions(struct ares_options *opts) { + memset(opts, 0, sizeof(struct ares_options)); + opts->timeout = 100; + opts->tries = 3; + return opts; + } + + private: + struct ares_options opts_; +}; + +TEST_P(ServerStateTimeoutCancelTest, CancelInServerStateCallbackOnTimeout) { + ServerStateCancelData data; + data.channel = channel_; + data.server_cb_calls = 0; + data.query_done = false; + + ares_set_server_state_callback(channel_, ServerStateCancelCallback, &data); + + // No reply configured, so the query times out and the notification comes + // from process_timeouts() rather than from a response. + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, ServerStateQueryCallback, &data, + NULL)); + Process(); + + EXPECT_GT(data.server_cb_calls, 0); + EXPECT_TRUE(data.query_done); +} + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, ServerStateTimeoutCancelTest, + ::testing::ValuesIn(ares::test::families), + PrintFamily); + +// Unregistering with a NULL callback discards whatever is still queued -- the +// documented behaviour of deferring delivery, and the only way an application +// can opt back out once notifications are in flight. +struct ServerStateUnregisterData { + ares_channel_t *channel; + size_t server_cb_calls; + bool query_done; +}; + +static void ServerStateUnregisterCallback(const char *server_string, + ares_bool_t success, + int flags, void *user_data) +{ + ServerStateUnregisterData *d = + static_cast(user_data); + (void)server_string; + (void)success; + (void)flags; + d->server_cb_calls++; + // Clear ourselves from inside the first notification. Any notification + // still queued behind this one must be dropped rather than delivered. + ares_set_server_state_callback(d->channel, NULL, NULL); +} + +static void ServerStateUnregisterQueryCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) +{ + ServerStateUnregisterData *d = + static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + d->query_done = true; +} + +TEST_P(ServerStateTimeoutCancelTest, UnregisterInCallbackDiscardsQueued) { + ServerStateUnregisterData data; + data.channel = channel_; + data.server_cb_calls = 0; + data.query_done = false; + + ares_set_server_state_callback(channel_, ServerStateUnregisterCallback, + &data); + + // No reply configured: the query times out on every try, so more than one + // notification is queued before the flush point is reached. + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, + ServerStateUnregisterQueryCallback, &data, NULL)); + Process(); + + EXPECT_EQ((size_t)1, data.server_cb_calls); + EXPECT_TRUE(data.query_done); +} + +TEST_P(MockUDPChannelTest, CancelInServerStateCallbackNoUseAfterFree) { + DNSPacket servfail; + servfail.set_response().set_aa().set_rcode(SERVFAIL) + .add_question(new DNSQuestion("www.google.com", T_A)); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &servfail)); + + ServerStateCancelData data; + data.channel = channel_; + data.server_cb_calls = 0; + data.query_done = false; + + ares_set_server_state_callback(channel_, ServerStateCancelCallback, &data); + + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + ServerStateQueryCallback, &data, NULL); + Process(); + + EXPECT_GT(data.server_cb_calls, 0); + EXPECT_TRUE(data.query_done); +} + +// The deferred requeue list records the target server as a raw pointer, and +// the drain never revalidated it -- although it does revalidate the query, by +// qid, for exactly the same reason. Application callbacks run from inside that +// drain, and a callback may legally reconfigure the servers, which frees the +// ones no longer in the list. A later entry in the same drain then dispatches +// through a freed ares_server_t. +// +// Two replies must be read in one batch: an ordinary answer whose callback +// reconfigures, and a FORMERR carrying no OPT record, which is what makes +// process_answer() queue a retry *to the same server* (the EDNS fallback -- +// the only site that records a server pointer at all). +struct ReconfigureInCbData { + ares_channel_t *channel; + bool done; +}; + +static void ReconfigureChannelCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + ReconfigureInCbData *data = static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + data->done = true; + /* Frees every server absent from the new configuration. */ + ares_set_servers_csv(data->channel, "127.0.0.1:53055"); +} + +TEST_P(MockUDPChannelTest, ReconfigureInCallbackStaleRequeueServer) { + DNSPacket okrsp; + okrsp.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 100, {1, 2, 3, 4})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &okrsp)); + + /* FORMERR with no OPT record: the server looks like it does not understand + * EDNS, so the query is queued for retry to this same server. */ + DNSPacket formerr; + formerr.set_response().set_aa().set_rcode(FORMERR) + .add_question(new DNSQuestion("www.example.com", T_A)); + ON_CALL(server_, OnRequest("www.example.com", T_A)) + .WillByDefault(SetReply(&server_, &formerr)); + + ReconfigureInCbData data; + data.channel = channel_; + data.done = false; + + QueryResult second; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + ReconfigureChannelCallback, &data, NULL); + ares_query_dnsrec(channel_, "www.example.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + QueryCallback, &second, NULL); + + /* Both replies have to be consumed in a single pass so they share one + * deferred-requeue scope -- that is what puts the ENDQUERY entry whose + * callback reconfigures ahead of the REQUEUE entry holding a server + * pointer. The usual Process() loop lets c-ares read between the two, so + * they never share a batch; answer both first, then read once. */ + for (int i = 0; i < 4; i++) { + std::set server_fds = fds(); + fd_set r; + struct timeval tv = {0, 20000}; + int nfds = 0; + FD_ZERO(&r); + for (ares_socket_t fd : server_fds) { + FD_SET(fd, &r); + if ((int)fd >= nfds) { + nfds = (int)fd + 1; + } + } + if (select(nfds, &r, nullptr, nullptr, &tv) <= 0) { + break; + } + for (ares_socket_t fd : server_fds) { + if (FD_ISSET(fd, &r)) { + ProcessFD(fd); + } + } + } + { + fd_set readers; + fd_set writers; + struct timeval tv = {0, 0}; + int nfds; + FD_ZERO(&readers); + FD_ZERO(&writers); + nfds = ares_fds(channel_, &readers, &writers); + select(nfds, &readers, &writers, nullptr, &tv); + ares_process(channel_, &readers, &writers); + } + + EXPECT_TRUE(data.done); + + /* With the fix the FORMERR query is correctly re-dispatched (to the newly + * configured, unreachable server) rather than going through a freed one, so + * it is still outstanding here. Cancel it so its callback runs while its + * result object is still in scope. */ + ares_cancel(channel_); + EXPECT_TRUE(second.done_); +} + +// ares_servers_remove_stale() destroys each stale server in turn, and that +// reaches application callbacks (ares_destroy_server() -> ares_close_sockets() +// -> ares_requeue_queries() -> end_query()). A callback that reconfigures the +// servers again destroys the servers the outer loop has not visited yet, so a +// cached "next" node points at freed memory. +struct NestedReconfigureData { + ares_channel_t *channel; + bool nested; +}; + +static void NestedReconfigureCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + NestedReconfigureData *data = static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + if (data->nested) { + return; + } + data->nested = true; + // Destroys the stale servers the outer removal loop has not reached yet. + ares_set_servers_csv(data->channel, ""); +} + +TEST_F(LibraryTest, NestedReconfigureInCallbackRemoveStale) { + ares_channel_t *channel = nullptr; + struct ares_options opts; + int optmask = 0; + + memset(&opts, 0, sizeof(opts)); + /* One try per server, so tearing them down exhausts the query's retries and + * runs its callback from inside the removal loop. */ + opts.tries = 1; + optmask |= ARES_OPT_TRIES; + opts.flags = ARES_FLAG_NOSEARCH; + optmask |= ARES_OPT_FLAGS; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&channel, &opts, optmask)); + /* Three servers, so the loop still has entries after destroying the first. + * Nothing listens on these ports; no traffic is needed. */ + EXPECT_EQ(ARES_SUCCESS, + ares_set_servers_csv(channel, "127.0.0.1:53101,127.0.0.1:53102," + "127.0.0.1:53103")); + + NestedReconfigureData data; + data.channel = channel; + data.nested = false; + + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel, "www.example.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, NestedReconfigureCallback, + &data, NULL)); + + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(channel, "")); + EXPECT_TRUE(data.nested); + + ares_destroy(channel); +} + +// The same nesting, but the nested reconfiguration *installs* a server rather +// than clearing the list. Restarting the removal walk from the head then +// re-judges that server against the outer call's list, decides it is stale and +// deletes it -- so ares_set_servers_csv() returns success and leaves the +// channel with no servers at all. Memory-safe and still wrong. +static void NestedReplaceCallback(void *arg, ares_status_t status, + size_t timeouts, + const ares_dns_record_t *dnsrec) { + NestedReconfigureData *data = static_cast(arg); + (void)status; + (void)timeouts; + (void)dnsrec; + if (data->nested) { + return; + } + data->nested = true; + // Installs a server the outer loop has never seen. + ares_set_servers_csv(data->channel, "127.0.0.1:53201"); +} + +TEST_F(LibraryTest, NestedReconfigureInCallbackKeepsNestedServers) { + ares_channel_t *channel = nullptr; + struct ares_options opts; + int optmask = 0; + + memset(&opts, 0, sizeof(opts)); + opts.tries = 1; + optmask |= ARES_OPT_TRIES; + opts.flags = ARES_FLAG_NOSEARCH; + optmask |= ARES_OPT_FLAGS; + + EXPECT_EQ(ARES_SUCCESS, ares_init_options(&channel, &opts, optmask)); + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(channel, "127.0.0.1:53101")); + + NestedReconfigureData data; + data.channel = channel; + data.nested = false; + + EXPECT_EQ(ARES_SUCCESS, + ares_query_dnsrec(channel, "www.example.com", ARES_CLASS_IN, + ARES_REC_TYPE_A, NestedReplaceCallback, &data, + NULL)); + + EXPECT_EQ(ARES_SUCCESS, ares_set_servers_csv(channel, "127.0.0.1:53301")); + EXPECT_TRUE(data.nested); + + // The nested call reconciled the whole list; what it installed must survive. + char *csv = ares_get_servers_csv(channel); + ASSERT_NE(nullptr, csv); + EXPECT_STREQ("127.0.0.1:53201", csv); + ares_free_string(csv); + + ares_destroy(channel); +} + +TEST_P(MockUDPChannelTest, CancelInCallbackNoDoubleFree) { + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &reply)); + + CancelInCbData data; + data.channel = channel_; + data.done = false; + data.cbs = 0; + ares_query_dnsrec(channel_, "www.google.com", ARES_CLASS_IN, ARES_REC_TYPE_A, + CancelChannelCallback, &data, NULL); + Process(); + EXPECT_TRUE(data.done); +} + TEST_P(MockUDPChannelTest, GetSock) { DNSPacket reply; reply.set_response().set_aa() @@ -1851,6 +4580,44 @@ } +TEST_P(MockChannelTest, DupSocketFunctionCallbackDestroySource) { + ares_socket_functions sock_funcs; + memset(&sock_funcs, 0, sizeof(sock_funcs)); + + DNSPacket reply; + reply.set_response().set_aa() + .add_question(new DNSQuestion("www.google.com", T_A)) + .add_answer(new DNSARR("www.google.com", 0x0100, {0x01, 0x02, 0x03, 0x04})); + ON_CALL(server_, OnRequest("www.google.com", T_A)) + .WillByDefault(SetReply(&server_, &reply)); + + size_t count = 0; + + sock_funcs.asocket = [](int af, int type, int protocol, void * p) -> ares_socket_t { + EXPECT_NE(nullptr, p); + (*reinterpret_cast(p))++; + return ::socket(af, type, protocol); + }; + + ares_set_socket_functions(channel_, &sock_funcs, &count); + + /* The copy must keep using the socket functions once the channel it was + * duplicated from is gone. */ + ares_channel_t *copy = nullptr; + ASSERT_EQ(ARES_SUCCESS, ares_dup(©, channel_)); + ares_destroy(channel_); + channel_ = copy; + + HostResult result; + ares_gethostbyname(channel_, "www.google.com.", AF_INET, HostCallback, &result); + Process(); + + EXPECT_TRUE(result.done_); + EXPECT_NE((size_t)0, count); + EXPECT_EQ(ARES_SUCCESS, result.status_); + EXPECT_EQ(0, result.timeouts_); +} + static const unsigned char * fetch_server_cookie(const ares_dns_record_t *dnsrec, size_t *len) { @@ -2527,6 +5294,244 @@ EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) .WillOnce(SetReply(servers_[1].get(), &okrsp)); CheckExample(); +}; + + +// Test that probe_pending flag is properly cleared when a probe times out, +// by testing a scenario where a server is probed, the probe times out, and +// then the server is probed again. +TEST_P(ServerFailoverOptsMultiMockTest, ProbeAfterProbeTimeout) { + std::vector nothing; + DNSPacket okrsp; + okrsp.set_response().set_aa() + .add_question(new DNSQuestion("www.example.com", T_A)) + .add_answer(new DNSARR("www.example.com", 100, {2,3,4,5})); + + auto tv_begin = std::chrono::high_resolution_clock::now(); + auto tv_now = std::chrono::high_resolution_clock::now(); + unsigned int delay_ms; + + // First, fail server #0 so it becomes marked as failed + if (verbose) std::cerr << std::chrono::duration_cast(tv_now - tv_begin).count() << "ms: Failing Server0 with timeout" << std::endl; + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReplyData(servers_[0].get(), nothing)); + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &okrsp)); + HostResult result1; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result1); + Process(); + EXPECT_TRUE(result1.done_); + + // Sleep past the retry delay and send another query + // This should trigger a probe to server #0, which will timeout + tv_now = std::chrono::high_resolution_clock::now(); + delay_ms = SERVER_FAILOVER_RETRY_DELAY + (SERVER_FAILOVER_RETRY_DELAY / 10); + if (verbose) std::cerr << std::chrono::duration_cast(tv_now - tv_begin).count() << "ms: sleep " << delay_ms << "ms" << std::endl; + ares_sleep_time(delay_ms); + tv_now = std::chrono::high_resolution_clock::now(); + + if (verbose) std::cerr << std::chrono::duration_cast(tv_now - tv_begin).count() << "ms: Sending query that triggers probe to Server0, probe will timeout" << std::endl; + // Server #1 will handle the real query, Server #0 will be probed and timeout + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &okrsp)); + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReplyData(servers_[0].get(), nothing)); // Probe times out + HostResult result2; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result2); + Process(); + EXPECT_TRUE(result2.done_); + + // Sleep past the retry delay again + // The next query will trigger another probe to server #0 + tv_now = std::chrono::high_resolution_clock::now(); + delay_ms = SERVER_FAILOVER_RETRY_DELAY + (SERVER_FAILOVER_RETRY_DELAY / 10); + if (verbose) std::cerr << std::chrono::duration_cast(tv_now - tv_begin).count() << "ms: sleep " << delay_ms << "ms again" << std::endl; + ares_sleep_time(delay_ms); + tv_now = std::chrono::high_resolution_clock::now(); + + if (verbose) std::cerr << std::chrono::duration_cast(tv_now - tv_begin).count() << "ms: Sending another query - should trigger another probe to Server0" << std::endl; + // Server #1 will handle the real query + // Server #0 should be probed again + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &okrsp)); + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &okrsp)); + HostResult result3; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, &result3); + Process(); + EXPECT_TRUE(result3.done_); +} + +class ServerRecoveryMultiMockTest + : public MockChannelOptsTest, + public ::testing::WithParamInterface< std::pair > { + public: + ServerRecoveryMultiMockTest() + : MockChannelOptsTest(2, GetParam().first, GetParam().second, false, + FillOptions(&opts_), + ARES_OPT_SERVER_FAILOVER | ARES_OPT_NOROTATE) {} + + static struct ares_options* FillOptions(struct ares_options *opts) { + memset(opts, 0, sizeof(struct ares_options)); + opts->server_failover_opts.retry_chance = 1; + opts->server_failover_opts.retry_delay = SERVER_FAILOVER_RETRY_DELAY; + return opts; + } + private: + struct ares_options opts_; +}; + +// Test that when every server has failures, a query sent to a failed server +// still spawns a probe of another failed server. Without this, a server +// that recovers from an extended outage while another (better-sorted) server +// is still failing is never noticed until the failure counts catch up +// ("sticky recovery"). +TEST_P(ServerRecoveryMultiMockTest, StickyRecoveryProbe) { + DNSPacket servfailrsp; + servfailrsp.set_response().set_aa().set_rcode(SERVFAIL) + .add_question(new DNSQuestion("www.example.com", T_A)); + DNSPacket okrsp; + okrsp.set_response().set_aa() + .add_question(new DNSQuestion("www.example.com", T_A)) + .add_answer(new DNSARR("www.example.com", 100, {2,3,4,5})); + unsigned int delay_ms = SERVER_FAILOVER_RETRY_DELAY + + (SERVER_FAILOVER_RETRY_DELAY / 10); + + // Fail all attempts of the first query so both servers end up failed. + // Attempts alternate between the servers as each failure re-sorts the + // list, so each server sees 3 requests (2 servers x 3 tries). No probes + // are spawned: retries never probe, and the initial attempt is sent while + // all servers are still healthy. + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)); + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[1].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[1].get(), &servfailrsp)); + HostResult result1; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, + &result1); + Process(); + EXPECT_TRUE(result1.done_); + EXPECT_EQ(ARES_ESERVFAIL, result1.status_); + + // Sleep past the retry delay so both servers are eligible for probing. + ares_sleep_time(delay_ms); + + // Server #0 recovers. The query is sent to server #0 (both servers have + // equal failure counts and #0 failed least recently). Even though the + // chosen server has failures, a probe must be spawned to the other failed + // server (#1), which is still down. + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &okrsp)); + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &servfailrsp)); + HostResult result2; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, + &result2); + Process(); + EXPECT_TRUE(result2.done_); + EXPECT_EQ(ARES_SUCCESS, result2.status_); + + // Sleep past the retry delay again so server #1 is once more eligible for + // probing. + ares_sleep_time(delay_ms); + + // Server #1 has now recovered. The query goes to healthy server #0 and a + // probe is spawned to server #1 which succeeds, marking it healthy again. + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &okrsp)); + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &okrsp)); + HostResult result3; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, + &result3); + Process(); + EXPECT_TRUE(result3.done_); + EXPECT_EQ(ARES_SUCCESS, result3.status_); +} + +class ServerRecoveryNoProbeMultiMockTest + : public MockChannelOptsTest, + public ::testing::WithParamInterface< std::pair > { + public: + ServerRecoveryNoProbeMultiMockTest() + : MockChannelOptsTest(2, GetParam().first, GetParam().second, false, + FillOptions(&opts_), + ARES_OPT_SERVER_FAILOVER | ARES_OPT_NOROTATE) {} + + static struct ares_options* FillOptions(struct ares_options *opts) { + memset(opts, 0, sizeof(struct ares_options)); + // Probing disabled: recovery must come from server selection alone + opts->server_failover_opts.retry_chance = 0; + opts->server_failover_opts.retry_delay = SERVER_FAILOVER_RETRY_DELAY; + return opts; + } + private: + struct ares_options opts_; +}; + +// Test that with probing disabled, a server whose failure count went stale +// during an outage is still retried once an actively-failing server reaches +// the same count: servers with equal failure counts are tried +// least-recently-failed first rather than in configuration order. +TEST_P(ServerRecoveryNoProbeMultiMockTest, StickyRecoveryTieBreak) { + DNSPacket servfailrsp; + servfailrsp.set_response().set_aa().set_rcode(SERVFAIL) + .add_question(new DNSQuestion("www.example.com", T_A)); + DNSPacket okrsp; + okrsp.set_response().set_aa() + .add_question(new DNSQuestion("www.example.com", T_A)) + .add_answer(new DNSARR("www.example.com", 100, {2,3,4,5})); + + // Fail all attempts of the first query so both servers end up failed with + // a failure count of 3 (2 servers x 3 tries, alternating). + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)); + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[1].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[1].get(), &servfailrsp)); + HostResult result1; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, + &result1); + Process(); + EXPECT_TRUE(result1.done_); + EXPECT_EQ(ARES_ESERVFAIL, result1.status_); + + // Server #0 recovers briefly: the next query succeeds and resets its + // failure count, while server #1 stays failed with its count frozen at 3. + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &okrsp)); + HostResult result2; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, + &result2); + Process(); + EXPECT_TRUE(result2.done_); + EXPECT_EQ(ARES_SUCCESS, result2.status_); + + // Server #0 starts failing again while server #1 has (unnoticed) + // recovered. The query retries server #0 until its failure count catches + // up to server #1's stale count of 3. At that point server #1 must sort + // first (it failed least recently) and answer successfully. Without the + // tie-break the remaining attempts would all go to server #0 and the query + // would fail without server #1 ever being tried. + EXPECT_CALL(*servers_[0], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)) + .WillOnce(SetReply(servers_[0].get(), &servfailrsp)); + EXPECT_CALL(*servers_[1], OnRequest("www.example.com", T_A)) + .WillOnce(SetReply(servers_[1].get(), &okrsp)); + HostResult result3; + ares_gethostbyname(channel_, "www.example.com.", AF_INET, HostCallback, + &result3); + Process(); + EXPECT_TRUE(result3.done_); + EXPECT_EQ(ARES_SUCCESS, result3.status_); } const char *af_tostr(int af) @@ -2573,6 +5578,8 @@ INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPChannelTest, ::testing::ValuesIn(ares::test::families), PrintFamily); +INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockRetryDepthChannelTest, ::testing::ValuesIn(ares::test::families), PrintFamily); + INSTANTIATE_TEST_SUITE_P(AddressFamilies, MockUDPMaxQueriesTest, ::testing::ValuesIn(ares::test::families), PrintFamily); INSTANTIATE_TEST_SUITE_P(AddressFamilies, CacheQueriesTest, ::testing::ValuesIn(ares::test::families), PrintFamily); @@ -2589,5 +5596,187 @@ INSTANTIATE_TEST_SUITE_P(TransportModes, ServerFailoverOptsMultiMockTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); +INSTANTIATE_TEST_SUITE_P(TransportModes, ServerRecoveryMultiMockTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); + +INSTANTIATE_TEST_SUITE_P(TransportModes, ServerRecoveryNoProbeMultiMockTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); + +// An answer is only accepted on the connection its query was sent on +// (RFC 5452 section 9.1). Two servers: #0 is demoted by a SERVFAIL so a later +// query goes only to #1, while #0's connection stays open (STAYOPEN). Server +// #0 then answers that query on its own connection. The answer must be +// dropped without side effects, and the query must then complete normally. +class ReplyConnMultiMockTest + : public MockChannelOptsTest, + public ::testing::WithParamInterface< std::pair > { + public: + ReplyConnMultiMockTest() + : MockChannelOptsTest(2, GetParam().first, GetParam().second, false, + FillOptions(&opts_), + ARES_OPT_FLAGS | ARES_OPT_TRIES | + ARES_OPT_SERVER_FAILOVER | ARES_OPT_NOROTATE) {} + + static struct ares_options* FillOptions(struct ares_options *opts) { + memset(opts, 0, sizeof(struct ares_options)); + /* Keep the demoted server's connection open; no DNS 0x20 so the replies + * built here match the question exactly */ + opts->flags = ARES_FLAG_STAYOPEN | ARES_FLAG_EDNS; + /* One try per server: the query goes to #1, then on timeout to #0 */ + opts->tries = 1; + /* Never probe the demoted server, so the query is sent only to #1 */ + opts->server_failover_opts.retry_chance = 0; + opts->server_failover_opts.retry_delay = 10000; + return opts; + } + private: + struct ares_options opts_; +}; + +static std::string ReplyConnFirstA(const QueryResult &result) +{ + const ares_dns_rr_t *rr; + char addr[64]; + + if (result.dnsrec_.dnsrec_ == nullptr || + ares_dns_record_rr_cnt(result.dnsrec_.dnsrec_, ARES_SECTION_ANSWER) == 0) { + return ""; + } + rr = ares_dns_record_rr_get_const(result.dnsrec_.dnsrec_, + ARES_SECTION_ANSWER, 0); + if (ares_dns_rr_get_type(rr) != ARES_REC_TYPE_A || + ares_inet_ntop(AF_INET, ares_dns_rr_get_addr(rr, ARES_RR_A_ADDR), addr, + sizeof(addr)) == nullptr) { + return ""; + } + return addr; +} + +TEST_P(ReplyConnMultiMockTest, AnswerOnOtherServerConnectionDropped) { + DNSPacket warm_servfail; + warm_servfail.set_response().set_aa().set_rcode(SERVFAIL) + .add_question(new DNSQuestion("warmup.example", T_A)); + DNSPacket warm_ok; + warm_ok.set_response().set_aa() + .add_question(new DNSQuestion("warmup.example", T_A)) + .add_answer(new DNSARR("warmup.example", 100, {1, 1, 1, 1})); + DNSPacket forged; + forged.set_response().set_aa() + .add_question(new DNSQuestion("victim.example", T_A)) + .add_answer(new DNSARR("victim.example", 100, {6, 6, 6, 6})); + DNSPacket genuine; + genuine.set_response().set_aa() + .add_question(new DNSQuestion("victim.example", T_A)) + .add_answer(new DNSARR("victim.example", 100, {1, 2, 3, 4})); + unsigned short qid = 0; + + /* Demote #0, leaving a connection to it open */ + EXPECT_CALL(*servers_[0], OnRequest("warmup.example", T_A)) + .WillOnce(SetReply(servers_[0].get(), &warm_servfail)); + EXPECT_CALL(*servers_[1], OnRequest("warmup.example", T_A)) + .WillOnce(SetReply(servers_[1].get(), &warm_ok)); + QueryResult warm; + ares_query_dnsrec(channel_, "warmup.example", ARES_CLASS_IN, + ARES_REC_TYPE_A, QueryCallback, &warm, NULL); + Process(); + ASSERT_TRUE(warm.done_); + ASSERT_EQ(ARES_SUCCESS, warm.status_); + + /* The query goes only to #1, which does not answer. Meanwhile #0 answers + * it on #0's own connection. That answer must be dropped, so the query + * times out on #1 and is retried on #0, whose genuine answer is accepted on + * the same (still in sync, for TCP) connection. */ + EXPECT_CALL(*servers_[1], OnRequest("victim.example", T_A)) + .WillOnce(InvokeWithoutArgs([&]() { + servers_[0]->SendUnsolicited(&forged, qid); + servers_[1]->SetReplyData(std::vector()); + })); + EXPECT_CALL(*servers_[0], OnRequest("victim.example", T_A)) + .WillOnce(SetReply(servers_[0].get(), &genuine)); + QueryResult result; + ares_query_dnsrec(channel_, "victim.example", ARES_CLASS_IN, + ARES_REC_TYPE_A, QueryCallback, &result, &qid); + Process(); + ASSERT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + EXPECT_EQ("1.2.3.4", ReplyConnFirstA(result)); + EXPECT_EQ(1, (int)result.timeouts_); +} + +// DNS cookies (RFC 7873) are per-server state, validated against the server +// the query was sent to. #1 supports cookies, #0 does not. A cookie-less +// answer arriving on #0's connection for a query sent only to #1 must not be +// accepted: it would otherwise be checked against #0's cookie state and pass. +class ReplyConnCookieMultiMockTest + : public MockChannelOptsTest, + public ::testing::WithParamInterface { + public: + ReplyConnCookieMultiMockTest() + : MockChannelOptsTest(2, GetParam(), false, false, + ReplyConnMultiMockTest::FillOptions(&opts_), + ARES_OPT_FLAGS | ARES_OPT_TRIES | + ARES_OPT_SERVER_FAILOVER | ARES_OPT_NOROTATE) {} + private: + struct ares_options opts_; +}; + +TEST_P(ReplyConnCookieMultiMockTest, CookielessAnswerOnOtherServerDropped) { + std::vector server_cookie = { 1, 2, 3, 4, 5, 6, 7, 8 }; + DNSPacket warm_servfail; + warm_servfail.set_response().set_aa().set_rcode(SERVFAIL) + .add_question(new DNSQuestion("warmup.example", T_A)); + DNSPacket warm_ok; + warm_ok.set_response().set_aa() + .add_question(new DNSQuestion("warmup.example", T_A)) + .add_answer(new DNSARR("warmup.example", 100, {1, 1, 1, 1})) + .add_additional(new DNSOptRR(0, 0, 0, 1280, { }, server_cookie, false)); + DNSPacket forged; + forged.set_response().set_aa() + .add_question(new DNSQuestion("victim.example", T_A)) + .add_answer(new DNSARR("victim.example", 100, {6, 6, 6, 6})); + DNSPacket genuine; + genuine.set_response().set_aa() + .add_question(new DNSQuestion("victim.example", T_A)) + .add_answer(new DNSARR("victim.example", 100, {1, 2, 3, 4})); + unsigned short qid = 0; + + /* #0 answers without a cookie (and is demoted); #1 answers with one, so + * #1's cookie state becomes SUPPORTED */ + EXPECT_CALL(*servers_[0], OnRequest("warmup.example", T_A)) + .WillOnce(SetReply(servers_[0].get(), &warm_servfail)); + EXPECT_CALL(*servers_[1], OnRequest("warmup.example", T_A)) + .WillOnce(SetReply(servers_[1].get(), &warm_ok)); + QueryResult warm; + ares_query_dnsrec(channel_, "warmup.example", ARES_CLASS_IN, + ARES_REC_TYPE_A, QueryCallback, &warm, NULL); + Process(); + ASSERT_TRUE(warm.done_); + ASSERT_EQ(ARES_SUCCESS, warm.status_); + size_t cookie_len = 0; + fetch_server_cookie(warm.dnsrec_.dnsrec_, &cookie_len); + ASSERT_EQ(server_cookie.size(), cookie_len); + + /* The cookie-protected query goes only to #1; a cookie-less answer for it + * arrives on #0's connection and must be dropped. The retry to #0 is then + * legitimately answered by #0, which does not use cookies. */ + EXPECT_CALL(*servers_[1], OnRequest("victim.example", T_A)) + .WillOnce(InvokeWithoutArgs([&]() { + servers_[0]->SendUnsolicited(&forged, qid); + servers_[1]->SetReplyData(std::vector()); + })); + EXPECT_CALL(*servers_[0], OnRequest("victim.example", T_A)) + .WillOnce(SetReply(servers_[0].get(), &genuine)); + QueryResult result; + ares_query_dnsrec(channel_, "victim.example", ARES_CLASS_IN, + ARES_REC_TYPE_A, QueryCallback, &result, &qid); + Process(); + ASSERT_TRUE(result.done_); + EXPECT_EQ(ARES_SUCCESS, result.status_); + EXPECT_EQ("1.2.3.4", ReplyConnFirstA(result)); + EXPECT_EQ(1, (int)result.timeouts_); +} + +INSTANTIATE_TEST_SUITE_P(TransportModes, ReplyConnMultiMockTest, ::testing::ValuesIn(ares::test::families_modes), PrintFamilyMode); + +INSTANTIATE_TEST_SUITE_P(AddressFamilies, ReplyConnCookieMultiMockTest, ::testing::ValuesIn(ares::test::families), PrintFamily); + } // namespace test } // namespace ares diff -Nru c-ares-1.34.5/test/ares-test-ns.cc c-ares-1.34.9/test/ares-test-ns.cc --- c-ares-1.34.5/test/ares-test-ns.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-ns.cc 2026-10-20 02:31:51.000000000 +0000 @@ -48,6 +48,7 @@ std::string hostname_; std::string domainname_; VoidToIntFn fn_; + uid_t outer_uid_; }; int EnterContainer(void *data) { @@ -60,12 +61,26 @@ << std::endl; } - // Ensure we are apparently root before continuing. - int count = 10; - while (getuid() != 0 && count > 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - count--; + // Map ourselves to root inside the new user namespace. The parent is + // suspended until we exit (CLONE_VFORK), so we write our own map; a process + // may map its own outer uid this way without further privilege. + std::stringstream contentss; + contentss << "0 " << container->outer_uid_ << " 1" << std::endl; + std::string content = contentss.str(); + int fd = open("/proc/self/uid_map", O_WRONLY); + if (fd < 0) { + std::cerr << "Failed to open /proc/self/uid_map, errno=" << errno + << std::endl; + return -1; } + ssize_t wrc = write(fd, content.c_str(), content.size()); + if (wrc != (ssize_t)content.size()) { + std::cerr << "Failed to write uid map, rc=" << wrc << ", errno=" << errno + << std::endl; + close(fd); + return -1; + } + close(fd); if (getuid() != 0) { std::cerr << "Child in user namespace has uid " << getuid() << std::endl; return -1; @@ -132,40 +147,28 @@ const std::string& domainname, VoidToIntFn fn) { const int stack_size = 1024 * 1024; std::vector stack(stack_size, 0); - ContainerInfo container = {fs, hostname, domainname, fn}; + ContainerInfo container = {fs, hostname, domainname, fn, geteuid()}; - // Start a child process in a new user and UTS namespace + // Start a child process in a new user and UTS namespace. CLONE_VM shares our + // memory with the child, so that test failures, mock expectations and + // coverage counters recorded inside the container are seen here. But the + // child also shares our thread-local storage (errno, allocator caches, the + // AddressSanitizer fake stack, ...), so we must not run at the same time as + // it: CLONE_VFORK suspends us until it exits. pid_t child = clone(EnterContainer, stack.data() + stack_size, - CLONE_VM|CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWUTS|SIGCHLD, + CLONE_VM|CLONE_VFORK|CLONE_NEWNS|CLONE_NEWUSER| + CLONE_NEWUTS|SIGCHLD, (void *)&container); if (child < 0) { std::cerr << "Failed to clone(), errno=" << errno << std::endl; return -1; } - // Build the UID map that makes us look like root inside the namespace. - std::stringstream mapfiless; - mapfiless << "/proc/" << child << "/uid_map"; - std::string mapfile = mapfiless.str(); - int fd = open(mapfile.c_str(), O_CREAT|O_WRONLY|O_TRUNC, 0644); - if (fd < 0) { - std::cerr << "Failed to create '" << mapfile << "'" << std::endl; - return -1; - } - std::stringstream contentss; - contentss << "0 " << getuid() << " 1" << std::endl; - std::string content = contentss.str(); - ssize_t rc = write(fd, content.c_str(), content.size()); - if (rc != (ssize_t)content.size()) { - std::cerr << "Failed to write uid map to '" << mapfile << "'" << std::endl; - } - close(fd); - - // Wait for the child process and retrieve its status. + // Reap the child process and retrieve its status. int status; - waitpid(child, &status, 0); - if (rc <= 0) { - std::cerr << "Failed to waitpid(" << child << ")" << std::endl; + if (waitpid(child, &status, 0) != child) { + std::cerr << "Failed to waitpid(" << child << "), errno=" << errno + << std::endl; return -1; } if (!WIFEXITED(status)) { diff -Nru c-ares-1.34.5/test/ares-test-parse-a.cc c-ares-1.34.9/test/ares-test-parse-a.cc --- c-ares-1.34.5/test/ares-test-parse-a.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-parse-a.cc 2026-10-20 02:31:51.000000000 +0000 @@ -145,6 +145,45 @@ &host, info, &count)); } +TEST_F(LibraryTest, ParseAReplyTtlClamp) { + std::vector data = { + 0x12, 0x34, // qid + 0x84, // response + query + AA + 0x00, // rc=NoError + 0x00, 0x01, // num questions + 0x00, 0x01, // num answer RRs + 0x00, 0x00, // num authority RRs + 0x00, 0x00, // num additional RRs + // Question + 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 0x03, 'c', 'o', 'm', + 0x00, + 0x00, 0x01, // type A + 0x00, 0x01, // class IN + // Answer: TTL 0xFFFFFFFF (server-controlled, high bit set) + 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 0x03, 'c', 'o', 'm', + 0x00, + 0x00, 0x01, // RR type A + 0x00, 0x01, // class IN + 0xFF, 0xFF, 0xFF, 0xFF, // TTL + 0x00, 0x04, // rdlength + 0x02, 0x03, 0x04, 0x05, // address + }; + struct hostent *host = nullptr; + struct ares_addrttl info[2]; + int count = 2; + EXPECT_EQ(ARES_SUCCESS, + ares_parse_a_reply(data.data(), (int)data.size(), + &host, info, &count)); + EXPECT_NE(nullptr, host); + ares_free_hostent(host); + EXPECT_EQ(1, count); + // A server-controlled TTL with the high bit set must not surface as a + // negative TTL through the public API. + EXPECT_EQ(INT_MAX, info[0].ttl); +} + TEST_F(LibraryTest, ParseAReplyNoData) { DNSPacket pkt; pkt.set_qid(0x1234).set_response().set_aa() @@ -360,6 +399,30 @@ } } +TEST_F(LibraryTest, ParseAReplyNegativeTtlCount) { + // Issue #1300: a negative *naddrttls used to wrap to SIZE_MAX, so every + // record in the reply was written into the caller's array regardless of + // its size. Up to 1.20.1 a negative count stored nothing; keep it that + // way. The reply carries more A records than info[] can hold so the + // pre-fix code overruns the array (caught under ASan). + DNSPacket pkt; + pkt.set_qid(0x1234).set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_A)) + .add_answer(new DNSARR("example.com", 100, {0x02, 0x03, 0x04, 0x05})) + .add_answer(new DNSARR("example.com", 100, {0x02, 0x03, 0x04, 0x06})) + .add_answer(new DNSARR("example.com", 100, {0x02, 0x03, 0x04, 0x07})); + std::vector data = pkt.data(); + + struct hostent *host = nullptr; + struct ares_addrttl info[2]; + int count = -1; + EXPECT_EQ(ARES_SUCCESS, ares_parse_a_reply(data.data(), (int)data.size(), + &host, info, &count)); + EXPECT_EQ(0, count); + ASSERT_NE(nullptr, host); + ares_free_hostent(host); +} + TEST_F(LibraryTest, ParseAReplyAllocFail) { DNSPacket pkt; pkt.set_qid(0x1234).set_response().set_aa() diff -Nru c-ares-1.34.5/test/ares-test-parse-aaaa.cc c-ares-1.34.9/test/ares-test-parse-aaaa.cc --- c-ares-1.34.5/test/ares-test-parse-aaaa.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-parse-aaaa.cc 2026-10-20 02:31:51.000000000 +0000 @@ -201,6 +201,36 @@ &host, info, &count)); } +TEST_F(LibraryTest, ParseAaaaReplyNegativeTtlCount) { + // Issue #1300: a negative *naddrttls used to wrap to SIZE_MAX, so every + // record in the reply was written into the caller's array regardless of + // its size. Up to 1.20.1 a negative count stored nothing; keep it that + // way. The reply carries more AAAA records than info[] can hold so the + // pre-fix code overruns the array (caught under ASan). + DNSPacket pkt; + pkt.set_qid(0x1234).set_response().set_aa() + .add_question(new DNSQuestion("example.com", T_AAAA)) + .add_answer(new DNSAaaaRR("example.com", 100, + {0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, + 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04})) + .add_answer(new DNSAaaaRR("example.com", 100, + {0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, + 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x05})) + .add_answer(new DNSAaaaRR("example.com", 100, + {0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, + 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x06})); + std::vector data = pkt.data(); + + struct hostent *host = nullptr; + struct ares_addr6ttl info[2]; + int count = -1; + EXPECT_EQ(ARES_SUCCESS, ares_parse_aaaa_reply(data.data(), (int)data.size(), + &host, info, &count)); + EXPECT_EQ(0, count); + ASSERT_NE(nullptr, host); + ares_free_hostent(host); +} + TEST_F(LibraryTest, ParseAaaaReplyAllocFail) { DNSPacket pkt; pkt.set_qid(0x1234).set_response().set_aa() diff -Nru c-ares-1.34.5/test/ares-test-parse.cc c-ares-1.34.9/test/ares-test-parse.cc --- c-ares-1.34.5/test/ares-test-parse.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test-parse.cc 2026-10-20 02:31:51.000000000 +0000 @@ -207,6 +207,361 @@ ares_free_hostent(host); } +TEST_F(LibraryTest, ParseMalformedRRCount) { + ares_dns_record_t *dnsrec = NULL; + const unsigned char data[] = { + 0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x01, 'a', 0x00, 0x00, + 0x01, 0x00, 0x01, + }; + + EXPECT_EQ(ARES_EBADRESP, ares_dns_parse(data, sizeof(data), 0, &dnsrec)); + EXPECT_EQ(nullptr, dnsrec); +} + +TEST_F(LibraryTest, ParseRejectsOverlongName) { + // RFC 1035 3.1 limits a name to 255 octets. A longer name (here built from + // six 63-octet labels) must be rejected instead of expanded. + std::vector data = { + 0x12, 0x34, 0x81, 0x80, + 0x00, 0x01, // num questions + 0x00, 0x01, // num answer RRs + 0x00, 0x00, // num authority RRs + 0x00, 0x00, // num additional RRs + // Question + 0x01, 'a', 0x00, + 0x00, 0x01, // type A + 0x00, 0x01, // class IN + }; + // Answer name: 6 x 63 label octets + 5 separators = 383 presentation octets + // (> 255), then root terminator + for (int i = 0; i < 6; i++) { + data.push_back(63); + for (int j = 0; j < 63; j++) { + data.push_back('a'); + } + } + data.push_back(0x00); + std::vector tail = { + 0x00, 0x01, // type A + 0x00, 0x01, // class IN + 0x00, 0x00, 0x00, 0x00, // TTL + 0x00, 0x04, // rdata length + 0x01, 0x02, 0x03, 0x04, + }; + data.insert(data.end(), tail.begin(), tail.end()); + + ares_dns_record_t *dnsrec = NULL; + EXPECT_EQ(ARES_EBADNAME, ares_dns_parse(data.data(), data.size(), 0, &dnsrec)); + EXPECT_EQ(nullptr, dnsrec); +} + +TEST_F(LibraryTest, ParseRejectsOverlongNameViaPointers) { + // The answer name is one 63-octet label followed by a compression pointer back + // to the question name (four 63-octet labels). Expanded that is 5 x 63 label + // octets + 4 separators = 319 presentation octets (> 255), so the length has + // to accumulate across the pointer jump for this to be rejected. + std::vector data = { + 0x12, 0x34, 0x81, 0x80, + 0x00, 0x01, // num questions + 0x00, 0x01, // num answer RRs + 0x00, 0x00, // num authority RRs + 0x00, 0x00, // num additional RRs + }; + // Question name: 4 x 63 label octets + 3 separators = 255 presentation octets, + // exactly the limit, so the question itself is accepted. + for (int i = 0; i < 4; i++) { + data.push_back(63); + for (int j = 0; j < 63; j++) { + data.push_back('a'); + } + } + data.push_back(0x00); + std::vector qtail = {0x00, 0x01, 0x00, 0x01}; // type A, class IN + data.insert(data.end(), qtail.begin(), qtail.end()); + // Answer name: one 63-octet label, then a pointer to the question name. + data.push_back(63); + for (int j = 0; j < 63; j++) { + data.push_back('a'); + } + data.push_back(0xc0); + data.push_back(0x0c); // -> offset 12, start of question name + std::vector tail = { + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 1, 2, 3, 4, + }; + data.insert(data.end(), tail.begin(), tail.end()); + ares_dns_record_t *dnsrec = NULL; + EXPECT_EQ(ARES_EBADNAME, ares_dns_parse(data.data(), data.size(), 0, &dnsrec)); + EXPECT_EQ(nullptr, dnsrec); +} + +TEST_F(LibraryTest, ParseRejectsPointerChain) { + // A name built purely from compression pointers adds no label octets, so the + // length cap alone can never stop it. A long backward chain of pointers is + // hidden in the opaque rdata of a raw RR, and a second RR's name points at the + // top of the chain; walking it must trip the indirection cap and return + // EBADNAME rather than following every jump. + std::vector data = { + 0x12, 0x34, 0x81, 0x80, + 0x00, 0x01, // num questions + 0x00, 0x02, // num answer RRs + 0x00, 0x00, // num authority RRs + 0x00, 0x00, // num additional RRs + // Question + 0x01, 'a', 0x00, + 0x00, 0x01, // type A + 0x00, 0x01, // class IN + // RR1: raw RR (unknown type) whose rdata carries the pointer chain + 0x00, // name = root + 0xff, 0xfe, // type (unknown -> stored raw, rdata not name-parsed) + 0x00, 0x01, // class IN + 0x00, 0x00, 0x00, 0x00, // TTL + }; + // The chain lives in RR1's rdata, which begins two octets past here (after the + // rdlength we are about to write), so compute absolute offsets from there. + const size_t rdata_off = data.size() + 2; + std::vector chain; + chain.push_back(0x00); // deepest target: root terminator + size_t prev = rdata_off; // absolute offset of the root + size_t top = prev; + const int N = 200; // well past the 128 indirection cap + for (int i = 0; i < N; i++) { + size_t here = rdata_off + chain.size(); + chain.push_back(0xc0 | ((prev >> 8) & 0x3f)); + chain.push_back(prev & 0xff); + top = here; + prev = here; + } + data.push_back((chain.size() >> 8) & 0xff); // rdlength + data.push_back(chain.size() & 0xff); + data.insert(data.end(), chain.begin(), chain.end()); + // RR2: name = pointer to the top of the chain. + data.push_back(0xc0 | ((top >> 8) & 0x3f)); + data.push_back(top & 0xff); + std::vector tail = { + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 1, 2, 3, 4, + }; + data.insert(data.end(), tail.begin(), tail.end()); + + ares_dns_record_t *dnsrec = NULL; + EXPECT_EQ(ARES_EBADNAME, ares_dns_parse(data.data(), data.size(), 0, &dnsrec)); + EXPECT_EQ(nullptr, dnsrec); +} + +TEST_F(LibraryTest, ParseAcceptsMaxLengthName) { + // Exactly 255 presentation octets: 4 x 63 = 252 + 3 separators = 255, which + // must still be accepted (the bound is "> 255", not ">= 255"). + std::vector data = { + 0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x01, 'a', 0x00, 0x00, 0x01, 0x00, 0x01, + }; + for (int i = 0; i < 4; i++) { + data.push_back(63); + for (int j = 0; j < 63; j++) { + data.push_back('a'); + } + } + data.push_back(0x00); + std::vector tail = { + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 1, 2, 3, 4, + }; + data.insert(data.end(), tail.begin(), tail.end()); + ares_dns_record_t *dnsrec = NULL; + EXPECT_EQ(ARES_SUCCESS, ares_dns_parse(data.data(), data.size(), 0, &dnsrec)); + EXPECT_NE(nullptr, dnsrec); + ares_dns_record_destroy(dnsrec); +} + +TEST_F(LibraryTest, ParseRejectsName256) { + // One octet over: labels 63,63,63,62,1 = 252 + 4 separators = 256 -> reject. + std::vector data = { + 0x12, 0x34, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0x00, 0x00, 0x01, 'a', 0x00, 0x00, 0x01, 0x00, 0x01, + }; + const int lens[] = {63, 63, 63, 62, 1}; + for (int i = 0; i < 5; i++) { + data.push_back(static_cast(lens[i])); + for (int j = 0; j < lens[i]; j++) { + data.push_back('a'); + } + } + data.push_back(0x00); + std::vector tail = { + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 1, 2, 3, 4, + }; + data.insert(data.end(), tail.begin(), tail.end()); + ares_dns_record_t *dnsrec = NULL; + EXPECT_EQ(ARES_EBADNAME, ares_dns_parse(data.data(), data.size(), 0, &dnsrec)); + EXPECT_EQ(nullptr, dnsrec); +} + +// A single OPT record in the additional section is valid (EDNS0). +TEST_F(LibraryTest, ParseSingleOpt) { + DNSPacket pkt; + pkt.set_qid(0x1234).set_response() + .add_question(new DNSQuestion("example.com", T_A)) + .add_additional(new DNSOptRR(0, 0, 0, 1280, {}, {}, false)); + std::vector data = pkt.data(); + + ares_dns_record_t *dnsrec = NULL; + EXPECT_EQ(ARES_SUCCESS, ares_dns_parse(data.data(), data.size(), 0, &dnsrec)); + EXPECT_NE(nullptr, dnsrec); + ares_dns_record_destroy(dnsrec); +} + +// RFC 6891 6.1.1: more than one OPT record in a message is a format error. +TEST_F(LibraryTest, ParseMultipleOptRejected) { + DNSPacket pkt; + pkt.set_qid(0x1234).set_response() + .add_question(new DNSQuestion("example.com", T_A)) + .add_additional(new DNSOptRR(0, 0, 0, 1280, {}, {}, false)) + .add_additional(new DNSOptRR(0, 0, 0, 1280, {}, {}, false)); + std::vector data = pkt.data(); + + ares_dns_record_t *dnsrec = NULL; + EXPECT_EQ(ARES_EBADRESP, ares_dns_parse(data.data(), data.size(), 0, &dnsrec)); + EXPECT_EQ(nullptr, dnsrec); +} + +// RFC 3597 section 4 recommends decompressing these older RR types on read, +// while still prohibiting compression in their outgoing RDATA. +TEST_F(LibraryTest, ParseCompressedLegacyRdataNames) +{ + const struct { + ares_dns_rec_type_t type; + ares_dns_rr_key_t key; + std::vector prefix; + std::vector suffix; + } cases[] = { + { ARES_REC_TYPE_SRV, + ARES_RR_SRV_TARGET, { 0x00, 0x0a, 0x00, 0x14, 0x00, 0x1e }, + {} }, + { ARES_REC_TYPE_SIG, + ARES_RR_SIG_SIGNERS_NAME, { 0x00, 0x01, 0x08, 0x02, // type covered, algorithm, labels + 0x00, 0x00, 0x00, 0x3c, // original TTL + 0x00, 0x00, 0x00, 0x02, // expiration + 0x00, 0x00, 0x00, 0x01, // inception + 0x00, 0x01 }, + { 0x01 } }, // key tag, signature + { ARES_REC_TYPE_NAPTR, + ARES_RR_NAPTR_REPLACEMENT, { 0x00, 0x64, 0x00, 0x0a, // order, preference + 0x01, 'S', 0x00, 0x00 }, + {} } // flags, services, regexp + }; + + const std::vector name = { 0x07, 'e', 'x', 'a', 'm', 'p', 'l', + 'e', 0x03, 'c', 'o', 'm', 0x00 }; + + for (const auto &tc : cases) { + SCOPED_TRACE(tc.type); + std::vector data = { + 0x12, 0x34, 0x84, 0x00, // qid, response + AA + 0x00, 0x01, 0x00, 0x01, // one question, one answer + 0x00, 0x00, 0x00, 0x00 // no authority/additional RRs + }; + data.insert(data.end(), name.begin(), name.end()); + data.insert( + data.end(), + { 0x00, static_cast(tc.type), 0x00, 0x01, // question type, IN + 0xc0, 0x0c, // owner -> question name + 0x00, static_cast(tc.type), 0x00, 0x01, // answer type, IN + 0x00, 0x00, 0x00, 0x3c, // TTL + 0x00, static_cast(tc.prefix.size() + 2 + tc.suffix.size()) }); + std::vector expected = data; + expected.back() = + static_cast(tc.prefix.size() + name.size() + tc.suffix.size()); + expected.insert(expected.end(), tc.prefix.begin(), tc.prefix.end()); + expected.insert(expected.end(), name.begin(), name.end()); + expected.insert(expected.end(), tc.suffix.begin(), tc.suffix.end()); + + data.insert(data.end(), tc.prefix.begin(), tc.prefix.end()); + data.insert(data.end(), { 0xc0, 0x0c }); // RDATA name -> example.com + data.insert(data.end(), tc.suffix.begin(), tc.suffix.end()); + + ares_dns_record_t *dnsrec = nullptr; + ares_status_t status = ares_dns_parse(data.data(), data.size(), 0, &dnsrec); + EXPECT_EQ(ARES_SUCCESS, status); + if (status != ARES_SUCCESS) { + continue; + } + ASSERT_NE(nullptr, dnsrec); + const ares_dns_rr_t *rr = + ares_dns_record_rr_get_const(dnsrec, ARES_SECTION_ANSWER, 0); + EXPECT_STREQ("example.com", ares_dns_rr_get_str(rr, tc.key)); + + unsigned char *wire = nullptr; + size_t wire_len = 0; + EXPECT_EQ(ARES_SUCCESS, ares_dns_write(dnsrec, &wire, &wire_len)); + if (wire != nullptr) { + EXPECT_EQ(expected, std::vector(wire, wire + wire_len)); + } + ares_free_string(wire); + ares_dns_record_destroy(dnsrec); + + if (tc.type == ARES_REC_TYPE_SRV) { + struct ares_srv_reply *srv = nullptr; + EXPECT_EQ(ARES_SUCCESS, + ares_parse_srv_reply(data.data(), (int)data.size(), &srv)); + EXPECT_NE(nullptr, srv); + if (srv != nullptr) { + EXPECT_STREQ("example.com", srv->host); + EXPECT_EQ(10, srv->priority); + EXPECT_EQ(20, srv->weight); + EXPECT_EQ(30, srv->port); + } + ares_free_data(srv); + } + + // Allowing compression must still reject a pointer to its own location. + size_t name_offset = data.size() - tc.suffix.size() - 2; + data[name_offset + 1] = static_cast(name_offset); + dnsrec = nullptr; + EXPECT_EQ(ARES_EBADNAME, + ares_dns_parse(data.data(), data.size(), 0, &dnsrec)); + EXPECT_EQ(nullptr, dnsrec); + ares_dns_record_destroy(dnsrec); + } +} + +// RFC 3597 does not extend receive-side compression to newer types like SVCB. +TEST_F(LibraryTest, ParseSvcbRejectsCompressedTarget) { + const unsigned char data[] = { + 0x12, 0x34, // qid + 0x84, 0x00, // response + AA + 0x00, 0x01, // qdcount + 0x00, 0x01, // ancount + 0x00, 0x00, // nscount + 0x00, 0x00, // arcount + // Question (name at offset 0x0c) + 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', + 0x03, 'c', 'o', 'm', + 0x00, + 0x00, 0x40, // type SVCB (64) + 0x00, 0x01, // class IN + // Answer + 0xc0, 0x0c, // name -> pointer to example.com + 0x00, 0x40, // type SVCB + 0x00, 0x01, // class IN + 0x00, 0x00, 0x00, 0x3c, // TTL + 0x00, 0x04, // rdlength + 0x00, 0x01, // SvcPriority + 0xc0, 0x0c, // TargetName -> compression pointer (illegal) + }; + + ares_dns_record_t *dnsrec = NULL; + EXPECT_EQ(ARES_EBADNAME, ares_dns_parse(data, sizeof(data), 0, &dnsrec)); + EXPECT_EQ(nullptr, dnsrec); +} + +TEST_F(DefaultChannelTest, ParseNameserverUriLinkLocalUninitialized) { + // Issue #1299: parse_nameserver_uri did not zero-initialize sconfig. + // When parsing multiple servers in a loop, if a URI server entry has a link-local + // interface, subsequent URI server entries without one would retain the previous + // entry's interface. + EXPECT_EQ(ARES_SUCCESS, + ares_set_servers_csv(channel_, "dns://[fe80::1%25iface0],dns://2.3.4.5")); + EXPECT_EQ("2.3.4.5:53", GetNameServers(channel_)); +} } // namespace test } // namespace ares diff -Nru c-ares-1.34.5/test/ares-test.cc c-ares-1.34.9/test/ares-test.cc --- c-ares-1.34.5/test/ares-test.cc 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test.cc 2026-10-20 02:31:51.000000000 +0000 @@ -443,11 +443,14 @@ } MockServer::MockServer(int family, unsigned short port) - : udpport_(port), tcpport_(port), qid_(-1) { + : udpport_(port), tcpport_(port), qid_(-1), disconnect_after_reply_(false) { reply_ = nullptr; // Create a TCP socket to receive data on. tcp_data_ = NULL; tcp_data_len_ = 0; + last_fd_ = ARES_SOCKET_BAD; + memset(&last_addr_, 0, sizeof(last_addr_)); + last_addrlen_ = 0; tcpfd_ = socket(family, SOCK_STREAM, 0); EXPECT_NE(ARES_SOCKET_BAD, tcpfd_); int optval = 1; @@ -644,6 +647,17 @@ if (connfd == ARES_SOCKET_BAD) { std::cerr << "Error accepting connection on fd " << fd << std::endl; } else { + /* This test harness is single-threaded: the mock server runs in the same + * thread as the c-ares client, so the client can only drain a socket + * between our (blocking) sends, never during one. A full-size TCP reply + * (MakeMaxReadTcpAReply() is a 65535-octet frame) therefore deadlocks a + * blocking send() on platforms whose default socket buffers can't hold + * the whole frame -- e.g. OpenBSD's ~16k default -- because send() waits + * for a reader that cannot run. Enlarge the send buffer so the entire + * frame fits locally and send() returns without a concurrent reader. */ + int sndbuf = 128 * 1024; + setsockopt(connfd, SOL_SOCKET, SO_SNDBUF, BYTE_CAST &sndbuf, + sizeof(sndbuf)); connfds_.insert(connfd); } return; @@ -699,6 +713,50 @@ return result; } +void MockServer::SendData(ares_socket_t fd, struct sockaddr_storage *addr, + ares_socklen_t addrlen, int qid, + std::vector reply) { + int flags = 0; + + if (reply.size() >= 2) { + // Overwrite the query ID if space to do so. + reply[0] = (byte)((qid >> 8) & 0xff); + reply[1] = (byte)(qid & 0xff); + } + if (verbose) { + std::cerr << "sending reply " << PacketToString(reply) + << " on port " << ((fd == udpfd_) ? udpport_ : tcpport_) + << ":" << getaddrport(addr) << std::endl; + } + + // Prefix with 2-byte length if TCP. + if (fd != udpfd_) { + int len = (int)reply.size(); + std::vector vlen = {(byte)((len & 0xFF00) >> 8), (byte)(len & 0xFF)}; + reply.insert(reply.begin(), vlen.begin(), vlen.end()); + // Also, don't bother with the destination address. + addr = nullptr; + addrlen = 0; + } + +#ifdef MSG_NOSIGNAL + flags |= MSG_NOSIGNAL; +#endif + + ares_ssize_t rc = (ares_ssize_t)sendto(fd, BYTE_CAST reply.data(), (SEND_TYPE_ARG3)reply.size(), flags, + (struct sockaddr *)addr, addrlen); + if (rc < static_cast(reply.size())) { + std::cerr << "Failed to send full reply, rc=" << rc << std::endl; + } +} + +void MockServer::SendUnsolicited(const DNSPacket *reply, unsigned short qid) { + ASSERT_NE(ARES_SOCKET_BAD, last_fd_); + ASSERT_TRUE(last_fd_ == udpfd_ || connfds_.count(last_fd_) != 0) + << "the most recent requester's connection has closed"; + SendData(last_fd_, &last_addr_, last_addrlen_, qid, reply->data()); +} + void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr, ares_socklen_t addrlen, const std::vector &req, const std::string &reqstr, @@ -706,10 +764,14 @@ /* DNS 0x20 will mix case, do case-insensitive matching of name in request */ char lower_name[256]; - int flags = 0; arestest_strtolower(lower_name, name, sizeof(lower_name)); + // Remember the requester so SendUnsolicited() can reach it. + last_fd_ = fd; + memcpy(&last_addr_, addr, sizeof(last_addr_)); + last_addrlen_ = addrlen; + // Before processing, let gMock know the request is happening. OnRequest(lower_name, rrtype); @@ -736,42 +798,20 @@ return; } - // Make a local copy of the current pending reply. - std::vector reply = exact_reply_; - if (qid_ >= 0) { // Use the explicitly specified query ID. qid = qid_; } - if (reply.size() >= 2) { - // Overwrite the query ID if space to do so. - reply[0] = (byte)((qid >> 8) & 0xff); - reply[1] = (byte)(qid & 0xff); - } - if (verbose) { - std::cerr << "sending reply " << PacketToString(reply) - << " on port " << ((fd == udpfd_) ? udpport_ : tcpport_) - << ":" << getaddrport(addr) << std::endl; - } - // Prefix with 2-byte length if TCP. - if (fd != udpfd_) { - int len = (int)reply.size(); - std::vector vlen = {(byte)((len & 0xFF00) >> 8), (byte)(len & 0xFF)}; - reply.insert(reply.begin(), vlen.begin(), vlen.end()); - // Also, don't bother with the destination address. - addr = nullptr; - addrlen = 0; - } - -#ifdef MSG_NOSIGNAL - flags |= MSG_NOSIGNAL; -#endif + SendData(fd, addr, addrlen, qid, exact_reply_); - ares_ssize_t rc = (ares_ssize_t)sendto(fd, BYTE_CAST reply.data(), (SEND_TYPE_ARG3)reply.size(), flags, - (struct sockaddr *)addr, addrlen); - if (rc < static_cast(reply.size())) { - std::cerr << "Failed to send full reply, rc=" << rc << std::endl; + if (disconnect_after_reply_ && fd != udpfd_) { + disconnect_after_reply_ = false; + connfds_.erase(fd); + sclose(fd); + free(tcp_data_); + tcp_data_ = NULL; + tcp_data_len_ = 0; } } diff -Nru c-ares-1.34.5/test/ares-test.h c-ares-1.34.9/test/ares-test.h --- c-ares-1.34.5/test/ares-test.h 2025-04-08 12:12:25.000000000 +0000 +++ c-ares-1.34.9/test/ares-test.h 2026-10-20 02:31:51.000000000 +0000 @@ -288,6 +288,7 @@ { reply_ = nullptr; exact_reply_.clear(); + disconnect_after_reply_ = false; for (ares_socket_t fd : connfds_) { sclose(fd); } @@ -297,6 +298,17 @@ tcp_data_len_ = 0; } + void DisconnectAfterReply() + { + disconnect_after_reply_ = true; + } + + // Send reply, with the given query ID, to the client of the most recent + // request this server received, on the socket that request arrived on. The + // client did not send this server a request with that query ID: this models + // a server answering a query that was only sent to some other server. + void SendUnsolicited(const DNSPacket *reply, unsigned short qid); + // The set of file descriptors that the server handles. std::set fds() const; @@ -321,6 +333,8 @@ int rrtype); void ProcessPacket(ares_socket_t fd, struct sockaddr_storage *addr, ares_socklen_t addrlen, byte *data, int len); + void SendData(ares_socket_t fd, struct sockaddr_storage *addr, + ares_socklen_t addrlen, int qid, std::vector reply); unsigned short udpport_; unsigned short tcpport_; ares_socket_t udpfd_; @@ -330,8 +344,13 @@ const DNSPacket *reply_; std::string expected_request_; int qid_; + bool disconnect_after_reply_; unsigned char *tcp_data_; size_t tcp_data_len_; + // Where the most recent request came from, for SendUnsolicited(). + ares_socket_t last_fd_; + struct sockaddr_storage last_addr_; + ares_socklen_t last_addrlen_; }; // Test fixture that uses a mock DNS server. @@ -502,6 +521,11 @@ mockserver->Disconnect(); } +ACTION_P(DisconnectAfterReply, mockserver) +{ + mockserver->DisconnectAfterReply(); +} + // C++ wrapper for struct hostent. struct HostEnt { HostEnt() : addrtype_(-1)