diff mbox series

[meta-oe,blacksail] c-ares: backport security fixes from 1.34.9

Message ID 20261027012151.438070-1-schonm@gmail.com
State New
Headers show
Series [meta-oe,blacksail] c-ares: backport security fixes from 1.34.9 | expand

Commit Message

J. S. Oct. 27, 2026, 1:21 a.m. UTC
Add the eight unique upstream commits that fix the security advisories addressed in c-ares 1.34.9.
Several advisories share commits, so patches are ordered by commit rather than duplicated per advisory; each patch lists the GHSA IDs it resolves.

Assisted-by: opencode (big-pickle)
Signed-off-by: Jason Schonberg <schonm@gmail.com>
---
 ...troy-detach-queries-before-callbacks.patch | 2603 +++++++++++++++++
 ...ed-socket-server-state-notifications.patch |  393 +++
 ...qcache-hand-callbacks-their-own-copy.patch | 1054 +++++++
 ...connections-torn-down-by-application.patch | 1107 +++++++
 ...pt-an-answer-on-the-right-connection.patch |  575 ++++
 ...tions-read-channel-config-under-lock.patch |  243 ++
 ...res_inet_pton-reject-a-prefix-length.patch |  364 +++
 ...nt-drop-UDP-answers-from-server-port.patch |  178 ++
 .../recipes-support/c-ares/c-ares_1.34.8.bb   |    8 +
 9 files changed, 6525 insertions(+)
 create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch
 create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-server-state-notifications.patch
 create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-callbacks-their-own-copy.patch
 create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-down-by-application.patch
 create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-connection.patch
 create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-channel-config-under-lock.patch
 create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-prefix-length.patch
 create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch

Comments

Ankur Tyagi Oct. 27, 2026, 7:04 a.m. UTC | #1
Hi Jason,

Thanks for submitting the patch.

Though the patches LGTM, I am seeing increased ptest failures.

Existing recipe ptest status:
[==========] 1093 tests from 41 test suites ran.
[  PASSED  ] 1092 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] DefaultChannelTest.TimeoutValue

ptest status after applying your patch:
[==========] 1174 tests from 47 test suites ran.
[  PASSED  ] 1170 tests.
[  FAILED  ] 4 tests, listed below:
[  FAILED  ] DefaultChannelTest.TimeoutValue
[  FAILED  ] SockStateDeliveryTest.CancelReportsSocketClose
[  FAILED  ] SockStateDeliveryTest.SetServersReportsSocketClose
[  FAILED  ] SockStateDeliveryTest.DestroyReportsSocketClose

Perhaps some other commits also need backporting?
Can please look into this?

cheers
Ankur

On Tue, Oct 27, 2026 at 2:22 PM J. S. via lists.openembedded.org
<schonm=gmail.com@lists.openembedded.org> wrote:
>
> Add the eight unique upstream commits that fix the security advisories addressed in c-ares 1.34.9.
> Several advisories share commits, so patches are ordered by commit rather than duplicated per advisory; each patch lists the GHSA IDs it resolves.
>
> Assisted-by: opencode (big-pickle)
> Signed-off-by: Jason Schonberg <schonm@gmail.com>
> ---
>  ...troy-detach-queries-before-callbacks.patch | 2603 +++++++++++++++++
>  ...ed-socket-server-state-notifications.patch |  393 +++
>  ...qcache-hand-callbacks-their-own-copy.patch | 1054 +++++++
>  ...connections-torn-down-by-application.patch | 1107 +++++++
>  ...pt-an-answer-on-the-right-connection.patch |  575 ++++
>  ...tions-read-channel-config-under-lock.patch |  243 ++
>  ...res_inet_pton-reject-a-prefix-length.patch |  364 +++
>  ...nt-drop-UDP-answers-from-server-port.patch |  178 ++
>  .../recipes-support/c-ares/c-ares_1.34.8.bb   |    8 +
>  9 files changed, 6525 insertions(+)
>  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch
>  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-server-state-notifications.patch
>  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-callbacks-their-own-copy.patch
>  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-down-by-application.patch
>  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-connection.patch
>  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-channel-config-under-lock.patch
>  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-prefix-length.patch
>  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
>
> diff --git a/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch b/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch
> new file mode 100644
> index 0000000000..385760f5bc
> --- /dev/null
> +++ b/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch
> @@ -0,0 +1,2603 @@
> +From 7f408deb128fbdcce78122322734ffaa01cad9ae Mon Sep 17 00:00:00 2001
> +From: Brad House <brad@brad-house.com>
> +Date: Sun, 18 Oct 2026 18:15:41 -0400
> +Subject: [PATCH] ares_cancel/ares_destroy: detach queries before running user
> + callbacks
> +MIME-Version: 1.0
> +Content-Type: text/plain; charset=UTF-8
> +Content-Transfer-Encoding: 8bit
> +
> +Backport of dd62bdfca52ba5c37e88a1469c0a4c5e4a568ccb to v1.34.  See that
> +commit for the full description; it hardens reentrant teardown from
> +callbacks (ares_cancel(), ares_destroy(), server reconfiguration, and
> +socket/server state notifications).
> +
> +Conflicts were whitespace/alignment only (v1.34 predates the clang-format
> +reformat in #1238) and were reformatted to v1.34's style.
> +
> +Also carries the test portion of #1334 that applies to the tests this
> +introduces: dead-server tests use 127.0.0.1:53055/53056 instead of
> +127.0.0.55/56, which only routes on Linux.
> +
> +Signed-off-by: Brad House <brad@brad-house.com>
> +Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/7f408deb128fbdcce78122322734ffaa01cad9ae]
> +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> +---
> + docs/ares_cancel.3                    |   17 +
> + docs/ares_destroy.3                   |    7 +
> + docs/ares_init_options.3              |   15 +
> + docs/ares_set_server_state_callback.3 |   31 +-
> + docs/ares_set_servers_csv.3           |   17 +
> + src/lib/ares_cancel.c                 |   25 +-
> + src/lib/ares_close_sockets.c          |   82 +-
> + src/lib/ares_conn.c                   |  217 ++++-
> + src/lib/ares_conn.h                   |   37 +-
> + src/lib/ares_destroy.c                |   85 +-
> + src/lib/ares_private.h                |  110 ++-
> + src/lib/ares_process.c                |  235 +++++-
> + src/lib/ares_send.c                   |   35 +
> + src/lib/ares_update_servers.c         |   42 +-
> + test/ares-test-mock.cc                | 1068 +++++++++++++++++++++++++
> + 15 files changed, 1959 insertions(+), 64 deletions(-)
> +
> +diff --git a/docs/ares_cancel.3 b/docs/ares_cancel.3
> +index 967a946b49..069f226c10 100644
> +--- a/docs/ares_cancel.3
> ++++ b/docs/ares_cancel.3
> +@@ -21,6 +21,23 @@ These calls give the callbacks a chance to clean up any state which might have
> + 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 --git a/docs/ares_destroy.3 b/docs/ares_destroy.3
> +index d1984b7cc9..0b6380e609 100644
> +--- a/docs/ares_destroy.3
> ++++ b/docs/ares_destroy.3
> +@@ -20,6 +20,13 @@ by the channel.
> + 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 --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> +index 9b3b481535..e6f6ee8ba6 100644
> +--- a/docs/ares_init_options.3
> ++++ b/docs/ares_init_options.3
> +@@ -231,6 +231,21 @@ will be passed the socket whose state has changed;
> + 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
> +diff --git a/docs/ares_set_server_state_callback.3 b/docs/ares_set_server_state_callback.3
> +index 04fbde4c7a..e95db327ce 100644
> +--- a/docs/ares_set_server_state_callback.3
> ++++ b/docs/ares_set_server_state_callback.3
> +@@ -47,6 +47,31 @@ the DNS servers used by an ares channel. For example, it can be used to track
> + 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 @@ Indicates that the query was tried over TCP.
> + .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 --git a/docs/ares_set_servers_csv.3 b/docs/ares_set_servers_csv.3
> +index f1435143f5..f9f5b82e28 100644
> +--- a/docs/ares_set_servers_csv.3
> ++++ b/docs/ares_set_servers_csv.3
> +@@ -126,6 +126,23 @@ are identical.  Prior versions would simply omit ports in \fBares_set_servers_cs
> + 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 --git a/src/lib/ares_cancel.c b/src/lib/ares_cancel.c
> +index 75600dea6b..aa167e4486 100644
> +--- a/src/lib/ares_cancel.c
> ++++ b/src/lib/ares_cancel.c
> +@@ -38,10 +38,10 @@ void ares_cancel(ares_channel_t *channel)
> +   }
> +
> +   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 @@ void ares_cancel(ares_channel_t *channel)
> +       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 @@ void ares_cancel(ares_channel_t *channel)
> +   ares_queue_notify_empty(channel);
> +
> + done:
> ++  ares_deferred_cb_leave(channel);
> +   ares_channel_unlock(channel);
> + }
> +diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
> +index 7bb72c31ec..6be662fdd3 100644
> +--- a/src/lib/ares_close_sockets.c
> ++++ b/src/lib/ares_close_sockets.c
> +@@ -41,12 +41,47 @@ static void ares_requeue_queries(ares_conn_t  *conn,
> +   }
> + }
> +
> ++/*! 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;
> ++
> ++  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;
> ++  }
> +
> +-  /* Unlink */
> ++  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 @@ void ares_close_connection(ares_conn_t *conn, ares_status_t requeue_status)
> +     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);
> +-
> +-  ares_socket_close(channel, conn->fd);
> ++  /* 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;
> ++
> ++  /* 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 @@ void ares_close_sockets(ares_server_t *server)
> +
> +   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);
> +   }
> + }
> +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> +index 6b315b0548..78f57e7d10 100644
> +--- a/src/lib/ares_conn.c
> ++++ b/src/lib/ares_conn.c
> +@@ -26,19 +26,210 @@
> +  */
> + #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 ((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);
> ++    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.
> ++   *
> ++   * 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;
> ++
> ++  /* 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;
> + }
> +
> +@@ -435,6 +626,16 @@ ares_status_t ares_open_connection(ares_conn_t   **conn_out,
> +     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) {
> +diff --git a/src/lib/ares_conn.h b/src/lib/ares_conn.h
> +index c332de14b1..fc177b84f6 100644
> +--- a/src/lib/ares_conn.h
> ++++ b/src/lib/ares_conn.h
> +@@ -49,7 +49,14 @@ typedef enum {
> +    *  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
> ++  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 {
> +@@ -84,6 +91,10 @@ struct ares_conn {
> +
> +   /* 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 */
> +@@ -168,6 +179,10 @@ struct ares_server {
> +
> +   /* 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);
> +@@ -185,8 +200,24 @@ ares_status_t   ares_conn_flush(ares_conn_t *conn);
> + 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 --git a/src/lib/ares_destroy.c b/src/lib/ares_destroy.c
> +index 1e5706e0e0..e0534c800d 100644
> +--- a/src/lib/ares_destroy.c
> ++++ b/src/lib/ares_destroy.c
> +@@ -69,17 +69,31 @@ void ares_destroy(ares_channel_t *channel)
> +    * 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 @@ void ares_destroy(ares_channel_t *channel)
> +   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 @@ void ares_destroy(ares_channel_t *channel)
> +   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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> +index 04d4576ed2..0a1029e0f8 100644
> +--- a/src/lib/ares_private.h
> ++++ b/src/lib/ares_private.h
> +@@ -147,7 +147,10 @@ typedef struct ares_query ares_query_t;
> + 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;
> +
> +@@ -227,6 +230,9 @@ struct ares_channeldata {
> +   /* 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
> +    */
> +@@ -237,6 +243,10 @@ struct ares_channeldata {
> +   /* 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;
> +
> +@@ -248,6 +258,29 @@ struct ares_channeldata {
> +
> +   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;
> +@@ -295,6 +328,10 @@ struct ares_channeldata {
> +   /* 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
> +@@ -308,6 +345,11 @@ struct ares_channeldata {
> +    * 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. */
> +@@ -351,6 +393,16 @@ void         *ares_dnsrec_convert_arg(ares_callback callback, void *arg);
> + 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);
> +@@ -390,7 +442,6 @@ ares_status_t ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
> +
> + 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);
> +@@ -589,6 +640,60 @@ ares_status_t ares_dns_name_write(ares_buf_t *buf, ares_llist_t **list,
> +  */
> + 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.
> ++ *
> ++ *  \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)
> +@@ -636,7 +741,6 @@ typedef struct ares_event_thread ares_event_thread_t;
> + 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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> +index b2316c4b33..82f53610fb 100644
> +--- a/src/lib/ares_process.c
> ++++ b/src/lib/ares_process.c
> +@@ -69,7 +69,6 @@ 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_detach_query(ares_query_t *query);
> +
> + static void        ares_query_remove_from_conn(ares_query_t *query)
> + {
> +@@ -81,14 +80,32 @@ static void        ares_query_remove_from_conn(ares_query_t *query)
> +   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;
> +@@ -111,9 +128,57 @@ static void invoke_server_state_cb(const ares_server_t *server,
> +     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,
> +@@ -204,6 +269,8 @@ static ares_status_t ares_process_fds_nolock(ares_channel_t         *channel,
> +     return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
> +   }
> +
> ++  ares_deferred_cb_enter(channel);
> ++
> +   ares_tvnow(&now);
> +
> +   /* Process write events */
> +@@ -244,6 +311,8 @@ static ares_status_t ares_process_fds_nolock(ares_channel_t         *channel,
> +   }
> +
> + done:
> ++  ares_deferred_cb_leave(channel);
> ++
> +   if (status == ARES_ENOMEM) {
> +     return ARES_ENOMEM;
> +   }
> +@@ -553,6 +622,8 @@ typedef enum {
> + typedef struct {
> +   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 */
> +@@ -578,6 +649,7 @@ static ares_status_t ares_append_requeue_int(ares_array_t     **requeue,
> +
> +   entry.type   = type;
> +   entry.qid    = query->qid;
> ++  entry.serial = query->serial;
> +   entry.server = server;
> +   entry.status = status;
> +   entry.dnsrec = dnsrec;
> +@@ -601,6 +673,53 @@ static ares_status_t ares_append_endqueue(ares_array_t     **requeue,
> +     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:
> +@@ -636,11 +755,35 @@ static ares_status_t ares_flush_requeue(ares_channel_t       *channel,
> +
> +     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. */
> +@@ -1377,6 +1520,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server,
> +   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);
> +@@ -1393,8 +1537,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server,
> +    * 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_htable_szvp_get_direct(channel->queries_by_qid, qid) == NULL) {
> ++  if (status == ARES_SUCCESS && !ares_query_is_live(channel, qid, serial)) {
> +     status = ARES_ETIMEOUT;
> +   }
> +
> +@@ -1412,6 +1555,9 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
> +   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) {
> +@@ -1443,6 +1589,32 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
> +   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:
> +@@ -1464,6 +1636,14 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
> +
> +   /* 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;
> ++  }
> ++
> +   switch (status) {
> +     /* Good result, continue on */
> +     case ARES_SUCCESS:
> +@@ -1479,6 +1659,17 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
> +     case ARES_ECONNREFUSED:
> +     case ARES_EBADFAMILY:
> +       handle_conn_error(conn, ARES_TRUE, status);
> ++      /* 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;
> +@@ -1597,11 +1788,18 @@ static ares_bool_t same_questions(const ares_query_t      *query,
> +   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;
> + }
> +@@ -1624,6 +1822,16 @@ static void end_query(ares_channel_t *channel, ares_server_t *server,
> +     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);
> +@@ -1631,7 +1839,8 @@ static void end_query(ares_channel_t *channel, ares_server_t *server,
> +   /* 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);
> + }
> +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> +index 6efa9580b2..2e0482bf19 100644
> +--- a/src/lib/ares_send.c
> ++++ b/src/lib/ares_send.c
> +@@ -117,6 +117,27 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +   unsigned short           id          = generate_unique_qid(channel);
> +   const ares_dns_record_t *dnsrec_resp = 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);
> +
> +   if (ares_slist_len(channel->servers) == 0) {
> +@@ -145,6 +166,7 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +
> +   query->channel      = channel;
> +   query->qid          = id;
> ++  query->serial       = ++channel->query_serial;
> +   query->timeout.sec  = 0;
> +   query->timeout.usec = 0;
> +   query->using_tcp =
> +@@ -209,6 +231,12 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +    */
> +   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 +245,17 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +
> +   /* Perform the first query action. */
> +
> ++  /* Sending opens and writes to connections and can mark a server down, both
> ++   * of which notify.  Deliver those once we are done with the connection and
> ++   * the server, not part-way through. */
> ++  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;
> + }
> +
> +diff --git a/src/lib/ares_update_servers.c b/src/lib/ares_update_servers.c
> +index f8d441accf..50a9c27925 100644
> +--- a/src/lib/ares_update_servers.c
> ++++ b/src/lib/ares_update_servers.c
> +@@ -740,17 +740,36 @@ static ares_bool_t ares_servers_remove_stale(ares_channel_t *channel,
> + {
> +   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;
> + }
> +@@ -775,6 +794,14 @@ ares_status_t ares_servers_update(ares_channel_t *channel,
> +     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
> +    */
> +@@ -843,6 +870,7 @@ ares_status_t ares_servers_update(ares_channel_t *channel,
> +   status = ARES_SUCCESS;
> +
> + done:
> ++  ares_deferred_cb_leave(channel);
> +   return status;
> + }
> +
> +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> +index c0c0279abb..2158c5a913 100644
> +--- a/test/ares-test-mock.cc
> ++++ b/test/ares-test-mock.cc
> +@@ -31,6 +31,11 @@
> + #include <sys/stat.h>
> + #endif
> +
> ++#ifndef _WIN32
> ++#  include <errno.h>
> ++#  include <fcntl.h>
> ++#endif
> ++
> + #include <sstream>
> + #include <vector>
> +
> +@@ -1914,6 +1919,7 @@ TEST_P(MockRetryDepthChannelTest, HighRetryNoStackOverflow) {
> + struct CancelInCbData {
> +   ares_channel_t *channel;
> +   bool            done;
> ++  size_t          cbs;
> + };
> +
> + static void CancelChannelCallback(void *arg, ares_status_t status,
> +@@ -1925,10 +1931,1071 @@ static void CancelChannelCallback(void *arg, ares_status_t 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<int> {
> ++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<CancelInCbData *>(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<CancelInCbData *>(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<DestroyReentryData *>(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<DestroyReentryData *>(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);
> ++
> ++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<byte> 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<SockStateCancelData *>(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<SockStateCloseOrderData *>(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<int>(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<ReconfigureFromHookData *>(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<ReconfigureFromHookData *>(arg);
> ++  (void)timeouts;
> ++  (void)dnsrec;
> ++  d->done   = true;
> ++  d->status = static_cast<int>(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);
> ++}
> ++
> ++// 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<SockStateReconfigureData *>(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);
> ++}
> ++
> ++// 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<std::pair<ares_socket_t, bool>> events; /* fd, still-armed */
> ++};
> ++
> ++static void SockStateRecordCallback(void *data, ares_socket_t socket_fd,
> ++                                    int readable, int writable)
> ++{
> ++  SockStateRecordData *d = static_cast<SockStateRecordData *>(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<ServerStateCancelData *>(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<ServerStateCancelData *>(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<int> {
> ++ 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<ServerStateUnregisterData *>(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<ServerStateUnregisterData *>(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<ReconfigureInCbData *>(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<ares_socket_t> 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<NestedReconfigureData *>(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<NestedReconfigureData *>(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()
> +@@ -1940,6 +3007,7 @@ TEST_P(MockUDPChannelTest, CancelInCallbackNoDoubleFree) {
> +   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();
> diff --git a/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-server-state-notifications.patch b/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-server-state-notifications.patch
> new file mode 100644
> index 0000000000..c34d3ab7c7
> --- /dev/null
> +++ b/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-server-state-notifications.patch
> @@ -0,0 +1,393 @@
> +From f944c4718fd672c9c0befd65a340abb2ccfc1a31 Mon Sep 17 00:00:00 2001
> +From: Brad House <brad@brad-house.com>
> +Date: Sun, 18 Oct 2026 20:28:11 -0400
> +Subject: [PATCH] Deliver deferred socket/server-state notifications only from
> + the outermost public call (#1340)
> +MIME-Version: 1.0
> +Content-Type: text/plain; charset=UTF-8
> +Content-Transfer-Encoding: 8bit
> +
> +Backport of f023a52a (#1340) to v1.34.
> +
> +The recent change that queues socket-state and server-state
> +notifications delivers them, and runs the deferred cleanup, only from
> +the outermost `ares_deferred_cb_leave()`. `ares_send_nolock()` brackets
> +`ares_send_query()` itself, but most public lookup functions never
> +opened a region of their own. When reached from `ares_getaddrinfo()`,
> +`ares_query_dnsrec()` and similar, the bracket inside
> +`ares_send_nolock()` was therefore the outermost one. Notifications were
> +then delivered from inside an internal call while the library was still
> +in the middle of the public call, and its callers were still using their
> +lookup state (the host query, the query id out-pointer, ...). A
> +server-state callback that reconfigures the servers, which
> +`ares_set_server_state_callback(3)` documents as allowed, could end that
> +lookup before the caller was done with it.
> +
> +This is a regression in unreleased code. No released version is
> +affected.
> +
> +The outermost region is now opened by the public entry points
> +themselves, under the channel lock:
> +
> +- `ares_send_dnsrec()` (also used by `ares_send()`)
> +- `ares_query_dnsrec()` (also used by `ares_query()`)
> +- `ares_search()`, `ares_search_dnsrec()`
> +- `ares_getaddrinfo()` (also used by `ares_gethostbyname()`)
> +- `ares_gethostbyaddr()`, `ares_getnameinfo()`
> +- `ares_process_pending_write()`: a failed flush closes the connection
> +and fails its queries over, and this function had no bracket at all.
> +
> +`ares_process_fds()` (and through it
> +`ares_process()`/`ares_process_fd()`), `ares_cancel()`, `ares_destroy()`
> +and the server configuration functions already had their own. The nested
> +bracket in `ares_send_nolock()` stays, because it keeps connections and
> +servers unlinked during the send readable until the send returns.
> +Internal sends made from query callbacks (search next domain,
> +getaddrinfo next lookup, requeue, server probes) already run inside the
> +processing bracket.
> +
> +The comments on `ares_deferred_cb_enter()` and the flush now state the
> +rule: the outermost region belongs to the public entry point, never to
> +an internal helper.
> +
> +The documented behaviour (notifications arrive before the outermost
> +c-ares call returns) does not change, so no man page changes.
> +
> +Two new `LibraryTest` cases in `test/ares-test-mock.cc`. The IPv6 server
> +cannot get a socket, so the first send fails over to the IPv4 server,
> +and a server-state callback then reconfigures the servers:
> +
> +- `ServerFailoverDuringGetAddrInfo`: the lookup completes cleanly. It
> +fails under ASAN without this change.
> +- `ServerFailoverDuringQuery`: the query id from `ares_query_dnsrec()`
> +is already reported by the time the query callback runs. It fails
> +without this change.
> +
> +The full non-live suite passes under ASAN/UBSAN and with
> +`-DCARES_WERROR=ON`.
> +
> +Signed-off-by: Brad House <brad@brad-house.com>
> +Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/f944c4718fd672c9c0befd65a340abb2ccfc1a31]
> +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> +---
> + src/lib/ares_conn.c          |   4 +-
> + src/lib/ares_getaddrinfo.c   |   5 ++
> + src/lib/ares_gethostbyaddr.c |   5 ++
> + src/lib/ares_getnameinfo.c   |   5 ++
> + src/lib/ares_private.h       |  10 ++++
> + src/lib/ares_process.c       |   6 ++
> + src/lib/ares_query.c         |   5 ++
> + src/lib/ares_search.c        |  10 ++++
> + src/lib/ares_send.c          |  13 ++++-
> + test/ares-test-mock.cc       | 106 +++++++++++++++++++++++++++++++++++
> + 10 files changed, 166 insertions(+), 3 deletions(-)
> +
> +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> +index 78f57e7d10..4cc0d1871d 100644
> +--- a/src/lib/ares_conn.c
> ++++ b/src/lib/ares_conn.c
> +@@ -94,7 +94,9 @@ 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.
> ++   * 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
> +diff --git a/src/lib/ares_getaddrinfo.c b/src/lib/ares_getaddrinfo.c
> +index 6d9e21af4b..5d9ddb311e 100644
> +--- a/src/lib/ares_getaddrinfo.c
> ++++ b/src/lib/ares_getaddrinfo.c
> +@@ -762,7 +762,12 @@ void ares_getaddrinfo(ares_channel_t *channel, const char *name,
> +     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 --git a/src/lib/ares_gethostbyaddr.c b/src/lib/ares_gethostbyaddr.c
> +index 69c509ab11..b314359331 100644
> +--- a/src/lib/ares_gethostbyaddr.c
> ++++ b/src/lib/ares_gethostbyaddr.c
> +@@ -112,7 +112,12 @@ void ares_gethostbyaddr(ares_channel_t *channel, const void *addr, int addrlen,
> +     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 --git a/src/lib/ares_getnameinfo.c b/src/lib/ares_getnameinfo.c
> +index 0159354530..7365e9ed82 100644
> +--- a/src/lib/ares_getnameinfo.c
> ++++ b/src/lib/ares_getnameinfo.c
> +@@ -194,7 +194,12 @@ void ares_getnameinfo(ares_channel_t *channel, const struct sockaddr *sa,
> +   }
> +
> +   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);
> + }
> +
> +diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> +index 0a1029e0f8..9610ba7dab 100644
> +--- a/src/lib/ares_private.h
> ++++ b/src/lib/ares_private.h
> +@@ -676,6 +676,16 @@ void          ares_server_reap(ares_server_t *server);
> +  *  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);
> +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> +index 82f53610fb..1569408b1b 100644
> +--- a/src/lib/ares_process.c
> ++++ b/src/lib/ares_process.c
> +@@ -490,6 +490,11 @@ void ares_process_pending_write(ares_channel_t *channel)
> +     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.
> +@@ -513,6 +518,7 @@ void ares_process_pending_write(ares_channel_t *channel)
> +     }
> +   }
> +
> ++  ares_deferred_cb_leave(channel);
> +   ares_channel_unlock(channel);
> + }
> +
> +diff --git a/src/lib/ares_query.c b/src/lib/ares_query.c
> +index ca3b6a9b73..0cf7f70760 100644
> +--- a/src/lib/ares_query.c
> ++++ b/src/lib/ares_query.c
> +@@ -125,7 +125,12 @@ ares_status_t ares_query_dnsrec(ares_channel_t *channel, const char *name,
> +   }
> +
> +   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 --git a/src/lib/ares_search.c b/src/lib/ares_search.c
> +index c605caf42c..ebc3d91e20 100644
> +--- a/src/lib/ares_search.c
> ++++ b/src/lib/ares_search.c
> +@@ -463,7 +463,12 @@ void ares_search(ares_channel_t *channel, const char *name, int dnsclass,
> +   }
> +
> +   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_status_t ares_search_dnsrec(ares_channel_t          *channel,
> +   }
> +
> +   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 --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> +index 2e0482bf19..dd4a28544b 100644
> +--- a/src/lib/ares_send.c
> ++++ b/src/lib/ares_send.c
> +@@ -246,8 +246,12 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +   /* Perform the first query action. */
> +
> +   /* Sending opens and writes to connections and can mark a server down, both
> +-   * of which notify.  Deliver those once we are done with the connection and
> +-   * the server, not part-way through. */
> ++   * 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);
> +@@ -271,9 +275,14 @@ ares_status_t ares_send_dnsrec(ares_channel_t          *channel,
> +   }
> +
> +   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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> +index 2158c5a913..b848c02434 100644
> +--- a/test/ares-test-mock.cc
> ++++ b/test/ares-test-mock.cc
> +@@ -2533,6 +2533,112 @@ TEST_F(LibraryTest, ReconfigureInSockStateCallbackNoUseAfterFree) {
> +   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<ares_channel_t *>(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<FailoverQueryData *>(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
> diff --git a/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-callbacks-their-own-copy.patch b/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-callbacks-their-own-copy.patch
> new file mode 100644
> index 0000000000..02039e92e0
> --- /dev/null
> +++ b/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-callbacks-their-own-copy.patch
> @@ -0,0 +1,1054 @@
> +From a21160045f9d7039225fa285be7cb1b02173a3a5 Mon Sep 17 00:00:00 2001
> +From: Brad House <brad@brad-house.com>
> +Date: Sun, 18 Oct 2026 22:12:09 -0400
> +Subject: [PATCH] [Backport v1.34] ares_qcache: hand callbacks their own copy
> + of a cached answer (#1339)
> +MIME-Version: 1.0
> +Content-Type: text/plain; charset=UTF-8
> +Content-Transfer-Encoding: 8bit
> +
> +Backport of 31e2f51e8715d418fa8272a6c3a1ee651917fec5 (`ares_qcache: hand
> +callbacks their own copy of a cached answer`) to `v1.34`. See that
> +commit for the full description.
> +
> +**Conflicts:**
> +- `ares_private.h`: alignment only; formatted with `v1.34`'s
> +`.clang-format`.
> +- `test/ares-test-internal.cc`: context only — on `main` the new
> +`QcacheKeyDistinguishesQuestions` test follows the IDNA/punycode tests
> +(#1031), which are not on `v1.34`, so it is placed after the URI test
> +instead.
> +
> +**Test fixes from #1334:** carries the part of #1334 that applies to the
> +tests this introduces — `ReentrantFlushInCallbackNoUseAfterFree`
> +reconfigures to `127.0.0.1:53055` instead of `127.0.0.55` (only routable
> +on Linux), and `UnknownQueryTypesDoNotShareCacheEntries` drops
> +`static_cast<ns_type>`, which does not compile on OpenBSD.
> +
> +Test file contents match `main`. Independent of the dd62bdfc backport
> +(#1338); applies cleanly with or without it.
> +
> +The full non-live suite passes 650/650 under ASAN+UBSAN and with
> +`-DCARES_WERROR=ON` on macOS. `ReentrantFlushInCallbackNoUseAfterFree`
> +is a heap-use-after-free in `ares_dns_record_rr_cnt()` on `v1.34`
> +without this change, and passes with it.
> +
> +Signed-off-by: Brad House <brad@brad-house.com>
> +Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/a21160045f9d7039225fa285be7cb1b02173a3a5]
> +GHSA: GHSA-grv7-fq5q-9vfc, GHSA-wrgc-prmw-27xh
> +---
> + docs/ares_init_options.3   |  11 +-
> + src/lib/ares_private.h     |  44 +++-
> + src/lib/ares_process.c     |   1 +
> + src/lib/ares_qcache.c      | 212 ++++++++++++++++---
> + src/lib/ares_send.c        |  47 ++++-
> + test/ares-test-internal.cc |  54 +++++
> + test/ares-test-mock.cc     | 419 +++++++++++++++++++++++++++++++++++++
> + 7 files changed, 743 insertions(+), 45 deletions(-)
> +
> +diff --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> +index e6f6ee8ba6..1ce7fb8e30 100644
> +--- a/docs/ares_init_options.3
> ++++ b/docs/ares_init_options.3
> +@@ -324,7 +324,16 @@ with the maximum number of seconds a query result may be cached which will
> + 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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> +index 9610ba7dab..dd06d303b8 100644
> +--- a/src/lib/ares_private.h
> ++++ b/src/lib/ares_private.h
> +@@ -153,6 +153,9 @@ struct ares_query {
> +   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
> +@@ -718,14 +721,47 @@ 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);
> ++/*! 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);
> +-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);
> ++/*! 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);
> +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> +index 1569408b1b..88536704ad 100644
> +--- a/src/lib/ares_process.c
> ++++ b/src/lib/ares_process.c
> +@@ -1859,6 +1859,7 @@ void ares_free_query(ares_query_t *query)
> +   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 --git a/src/lib/ares_qcache.c b/src/lib/ares_qcache.c
> +index ad3bc83505..f48669b56c 100644
> +--- a/src/lib/ares_qcache.c
> ++++ b/src/lib/ares_qcache.c
> +@@ -38,7 +38,7 @@ typedef struct {
> +   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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
> +     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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
> +   }
> +
> +   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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
> +       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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
> +       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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
> +       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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
> +       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 @@ static unsigned int ares_qcache_soa_minimum(ares_dns_record_t *dnsrec)
> + }
> +
> + /* 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 @@ static ares_status_t ares_qcache_insert_int(ares_qcache_t           *qcache,
> +   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 */
> +   }
> +@@ -382,31 +514,25 @@ static ares_status_t ares_qcache_insert_int(ares_qcache_t           *qcache,
> +   /* 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;
> +@@ -416,10 +542,24 @@ ares_status_t ares_qcache_fetch(ares_channel_t           *channel,
> +   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;
> + }
> +
> +@@ -428,13 +568,25 @@ ares_status_t ares_qcache_insert(ares_channel_t          *channel,
> +                                  const ares_query_t      *query,
> +                                  const ares_dns_record_t *dnsrec)
> + {
> +-  ares_dns_record_t *dupdns = ares_dns_record_duplicate(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->query, now);
> ++
> ++  status =
> ++    ares_qcache_insert_int(channel->qcache, dupdns, query->qcache_key, now);
> +   if (status != ARES_SUCCESS) {
> +     ares_dns_record_destroy(dupdns);
> +   }
> +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> +index dd4a28544b..117745ce28 100644
> +--- a/src/lib/ares_send.c
> ++++ b/src/lib/ares_send.c
> +@@ -111,11 +111,12 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +                                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
> +@@ -145,13 +146,28 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +     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;
> +     }
> +   }
> +@@ -159,8 +175,11 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +   /* 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));
> +
> +@@ -173,6 +192,10 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +     (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
> +@@ -180,7 +203,11 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
> +     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;
> +   }
> +diff --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> +index 8881f3b7cc..5ba5d2b39b 100644
> +--- a/test/ares-test-internal.cc
> ++++ b/test/ares-test-internal.cc
> +@@ -502,6 +502,60 @@ TEST_F(LibraryTest, URI) {
> +   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<std::string> 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];
> ++    }
> ++  }
> ++}
> ++
> + #endif /* !CARES_SYMBOL_HIDING */
> +
> + TEST_F(LibraryTest, InetPtoN) {
> +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> +index b848c02434..4b409db7c3 100644
> +--- a/test/ares-test-mock.cc
> ++++ b/test/ares-test-mock.cc
> +@@ -611,6 +611,425 @@ class CacheQueriesTest
> +   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<QcacheReentrantCbData *>(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<ares_dns_rec_type_t>(29), QueryCallback, &r1,
> ++                    nullptr);
> ++  Process();
> ++  EXPECT_TRUE(r1.done_);
> ++
> ++  QueryResult r2;
> ++  ares_query_dnsrec(channel_, "example.net", ARES_CLASS_IN,
> ++                    static_cast<ares_dns_rec_type_t>(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()
> diff --git a/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-down-by-application.patch b/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-down-by-application.patch
> new file mode 100644
> index 0000000000..f78e9726b0
> --- /dev/null
> +++ b/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-down-by-application.patch
> @@ -0,0 +1,1107 @@
> +From 13a10dddbabc30b6fca8e23f9efff73fedba9401 Mon Sep 17 00:00:00 2001
> +From: Brad House <brad@brad-house.com>
> +Date: Sun, 18 Oct 2026 22:51:53 -0400
> +Subject: [PATCH] [Backport v1.34] Handle connections torn down by application
> + socket functions (#1343)
> +MIME-Version: 1.0
> +Content-Type: text/plain; charset=UTF-8
> +Content-Transfer-Encoding: 8bit
> +
> +Backport of cb25d8bf297aed0aad8f6e0777ca0635cacc6cb1 to `v1.34`. See
> +that commit for the full description.
> +
> +Application code that c-ares calls while using a connection can
> +reconfigure the servers and so tear that connection down. That includes
> +the socket functions from `ares_set_socket_functions_ex()` and the older
> +`ares_set_socket_functions()`, and the pending-write callback. c-ares
> +now notices this while opening, writing to, reading from or closing a
> +connection. It requeues the affected queries, or drops the stale data,
> +instead of continuing to use the connection.
> +
> +It applied cleanly on top of the dd62bdfc and #1340 backports (#1338).
> +Test file contents match `main`.
> +
> +Each of the 11 new tests fails on `v1.34` without this change: nine with
> +an ASAN heap-use-after-free, and the two receive tests on their
> +assertions. All 11 pass with it. The full non-live suite passes 674/674
> +under ASAN+UBSAN and with `-DCARES_WERROR=ON` on macOS.
> +
> +Signed-off-by: Brad House <brad@brad-house.com>
> +Co-authored-by: Mar <mardgez13@gmail.com>
> +Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/13a10dddbabc30b6fca8e23f9efff73fedba9401]
> +GHSA: GHSA-pj7p-v3cw-hq66
> +---
> + src/lib/ares_close_sockets.c |   7 +-
> + src/lib/ares_conn.c          |  29 ++
> + src/lib/ares_process.c       |  65 ++-
> + test/ares-test-mock.cc       | 842 +++++++++++++++++++++++++++++++++++
> + 4 files changed, 935 insertions(+), 8 deletions(-)
> +
> +diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
> +index 6be662fdd3..a3af721bc6 100644
> +--- a/src/lib/ares_close_sockets.c
> ++++ b/src/lib/ares_close_sockets.c
> +@@ -148,6 +148,7 @@ void ares_check_cleanup_conns(const ares_channel_t *channel)
> +     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)) {
> +@@ -190,8 +191,12 @@ void ares_check_cleanup_conns(const ares_channel_t *channel)
> +         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 --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> +index 4cc0d1871d..5e0827fbdf 100644
> +--- a/src/lib/ares_conn.c
> ++++ b/src/lib/ares_conn.c
> +@@ -262,6 +262,15 @@ ares_conn_err_t ares_conn_read(ares_conn_t *conn, void *data, size_t len,
> +     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;
> +@@ -408,6 +417,19 @@ ares_conn_err_t ares_conn_write(ares_conn_t *conn, const void *data, size_t len,
> +   }
> +
> + 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
> +@@ -644,6 +666,13 @@ ares_status_t ares_open_connection(ares_conn_t   **conn_out,
> +     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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> +index 88536704ad..5686b077a6 100644
> +--- a/src/lib/ares_process.c
> ++++ b/src/lib/ares_process.c
> +@@ -189,7 +189,9 @@ static void server_increment_failures(ares_server_t *server,
> +   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 */
> +   }
> +
> +@@ -220,7 +222,9 @@ static void server_set_good(ares_server_t *server, ares_bool_t used_tcp)
> +   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 */
> +   }
> +
> +@@ -501,21 +505,39 @@ void ares_process_pending_write(ares_channel_t *channel)
> +    */
> +   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);
> +@@ -902,6 +924,13 @@ static ares_status_t process_read(ares_channel_t       *channel,
> +     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;
> +@@ -1192,6 +1221,15 @@ static void handle_conn_error(ares_conn_t *conn, ares_bool_t critical_failure,
> + {
> +   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) {
> +@@ -1508,6 +1546,14 @@ static ares_status_t ares_conn_query_write(ares_conn_t          *conn,
> +       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;
> +   }
> +
> +@@ -1650,6 +1696,11 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
> +     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:
> +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> +index 4b409db7c3..f1d9d360e7 100644
> +--- a/test/ares-test-mock.cc
> ++++ b/test/ares-test-mock.cc
> +@@ -2593,6 +2593,86 @@ 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<int> {
> ++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<bool *>(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_;
> +@@ -2898,6 +2978,768 @@ TEST_F(LibraryTest, ReconfigureInConfigureHookNoUseAfterFree) {
> +   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<ReconfigureFromSendData *>(user_data);
> ++  ares_ssize_t len = 0;
> ++  (void)fd;
> ++  for (int i = 0; i < iovcnt; i++) {
> ++    len += static_cast<ares_ssize_t>(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<ReconfigureFromSendData *>(arg);
> ++  (void)timeouts;
> ++  (void)dnsrec;
> ++  d->done   = true;
> ++  d->status = static_cast<int>(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<BadSocketStateData *>(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<PendingWriteReconfigureData *>(user_data);
> ++  ares_ssize_t len = 0;
> ++  (void)fd;
> ++  for (int i = 0; i < iovcnt; i++) {
> ++    len += static_cast<ares_ssize_t>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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;
> ++  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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<unsigned char *>(buffer)[2] |= 0x80;
> ++  if (address != NULL && address_len != NULL &&
> ++      *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> ++    memcpy(address, &d->peer, sizeof(d->peer));
> ++    *address_len = sizeof(d->peer);
> ++  }
> ++  return static_cast<ares_ssize_t>(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<FakeSockData *>(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<ares_ssize_t>(length);
> ++}
> ++
> ++static int FakeSockGetsockname(ares_socket_t sock, struct sockaddr *address,
> ++                               ares_socklen_t *address_len, void *user_data)
> ++{
> ++  FakeSockData      *d = static_cast<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(arg);
> ++  (void)timeouts;
> ++  (void)dnsrec;
> ++  d->done   = true;
> ++  d->status = static_cast<int>(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);
> ++}
> ++
> ++// 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
> diff --git a/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-connection.patch b/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-connection.patch
> new file mode 100644
> index 0000000000..20fef5d2f0
> --- /dev/null
> +++ b/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-connection.patch
> @@ -0,0 +1,575 @@
> +From c8bd9ab53329f66ec0c395a11002a5696669b147 Mon Sep 17 00:00:00 2001
> +From: Brad House <brad@brad-house.com>
> +Date: Mon, 19 Oct 2026 11:53:54 -0400
> +Subject: [PATCH] [Backport v1.34] ares_process: only accept an answer on the
> + connection its query was sent on (#1361)
> +MIME-Version: 1.0
> +Content-Type: text/plain; charset=UTF-8
> +Content-Transfer-Encoding: 8bit
> +
> +Backport of b6f8a9b47e17c681e092a74d05d73bbb35c89a39 to `v1.34`. See
> +that commit for the full description.
> +
> +**Conflicts:** `src/lib/ares_socket.h` only — v1.34 aligns the
> +neighbouring prototypes differently. Resolved by taking the new doc
> +comments and the `ares_sockaddr_addr_port_eq()` prototype, then
> +re-running clang-format for the v1.34 alignment (which also re-wraps the
> +new `SendData()` declaration in `test/ares-test.h`). All other files
> +applied unchanged; the `test/` diffs match main exactly.
> +
> +**Tests (local, macOS, Debug, Ninja):**
> +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> +--gtest_filter='-*Live*'` — 693/693 passed in both.
> +- With the `src/` changes reverted, the 4 new test instances fail
> +(`UDPAnswerFromWrongPortDropped`, `AnswerOnOtherServerConnectionDropped`
> +UDP/TCP, `CookielessAnswerOnOtherServerDropped`); they pass with them.
> +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> +with the other two v1.34 backports (both orders apply cleanly): 699/699
> +passed under ASAN+UBSAN and `-Werror`.
> +
> +Signed-off-by: Brad House <brad@brad-house.com>
> +Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/c8bd9ab53329f66ec0c395a11002a5696669b147]
> +GHSA: GHSA-fqh5-3x7m-wp44
> +---
> + src/lib/ares_conn.c    |  11 ++-
> + src/lib/ares_process.c |  18 ++++
> + src/lib/ares_socket.c  |  24 +++++
> + src/lib/ares_socket.h  |  21 ++++
> + test/ares-test-mock.cc | 218 +++++++++++++++++++++++++++++++++++++++++
> + test/ares-test.cc      |  86 +++++++++-------
> + test/ares-test.h       |  12 +++
> + 7 files changed, 355 insertions(+), 35 deletions(-)
> +
> +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> +index 5e0827fbdf..b868053e62 100644
> +--- a/src/lib/ares_conn.c
> ++++ b/src/lib/ares_conn.c
> +@@ -252,9 +252,16 @@ ares_conn_err_t ares_conn_read(ares_conn_t *conn, void *data, size_t len,
> +                            (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. */
> +     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
> +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> +index 5686b077a6..06d0ca507c 100644
> +--- a/src/lib/ares_process.c
> ++++ b/src/lib/ares_process.c
> +@@ -1117,6 +1117,24 @@ static ares_status_t process_answer(ares_channel_t      *channel,
> +     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)
> +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> +index 4ad3034021..0a12935ab7 100644
> +--- a/src/lib/ares_socket.c
> ++++ b/src/lib/ares_socket.c
> +@@ -127,6 +127,30 @@ ares_bool_t ares_sockaddr_addr_eq(const struct sockaddr  *sa,
> +   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;
> ++  }
> ++
> ++  if (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,
> +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> +index 24a99ab331..f7128e3493 100644
> +--- a/src/lib/ares_socket.h
> ++++ b/src/lib/ares_socket.h
> +@@ -139,8 +139,29 @@ typedef enum {
> +   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.
> ++ *
> ++ *  \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, IP addresses and ports are
> ++ *          equal, 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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> +index d233f9070f..058dc93001 100644
> +--- a/test/ares-test-mock.cc
> ++++ b/test/ares-test-mock.cc
> +@@ -3363,6 +3363,7 @@ struct FakeSockData {
> +   unsigned char      query[512];
> +   size_t             query_len;
> +   struct sockaddr_in peer;
> ++  unsigned short     recv_port; /* network order; 0 reports the peer's port */
> +   size_t             recv_calls;
> +   size_t             bad_fd_calls;
> +   size_t             sock_state_bad;
> +@@ -3471,6 +3472,9 @@ static ares_ssize_t FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> +   if (address != NULL && address_len != NULL &&
> +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> +     memcpy(address, &d->peer, sizeof(d->peer));
> ++    if (d->recv_port != 0) {
> ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
> ++    }
> +     *address_len = sizeof(d->peer);
> +   }
> +   return static_cast<ares_ssize_t>(d->query_len);
> +@@ -3710,6 +3714,42 @@ TEST_F(LibraryTest, ReconfigureInRecvDropsBufferedAnswer) {
> +   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);
> ++}
> ++
> + // 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.
> +@@ -5530,5 +5570,183 @@ INSTANTIATE_TEST_SUITE_P(TransportModes, ServerRecoveryMultiMockTest, ::testing:
> +
> + 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<int, bool> > {
> ++ 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<byte>());
> ++    }));
> ++  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<int> {
> ++ 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<byte> 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<byte>());
> ++    }));
> ++  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 --git a/test/ares-test.cc b/test/ares-test.cc
> +index d333ee9419..0b3ac12c09 100644
> +--- a/test/ares-test.cc
> ++++ b/test/ares-test.cc
> +@@ -448,6 +448,9 @@ MockServer::MockServer(int family, unsigned short port)
> +   // 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;
> +@@ -710,6 +713,50 @@ std::set<ares_socket_t> MockServer::fds() const {
> +   return result;
> + }
> +
> ++void MockServer::SendData(ares_socket_t fd, struct sockaddr_storage *addr,
> ++                          ares_socklen_t addrlen, int qid,
> ++                          std::vector<byte> 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<byte> 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<ares_ssize_t>(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<byte> &req,
> +                                 const std::string &reqstr,
> +@@ -717,10 +764,14 @@ void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr,
> +
> +   /* 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);
> +
> +@@ -747,43 +798,12 @@ void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr,
> +     return;
> +   }
> +
> +-  // Make a local copy of the current pending reply.
> +-  std::vector<byte> 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<byte> 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<ares_ssize_t>(reply.size())) {
> +-    std::cerr << "Failed to send full reply, rc=" << rc << std::endl;
> +-  }
> ++  SendData(fd, addr, addrlen, qid, exact_reply_);
> +
> +   if (disconnect_after_reply_ && fd != udpfd_) {
> +     disconnect_after_reply_ = false;
> +diff --git a/test/ares-test.h b/test/ares-test.h
> +index 2b973c3982..170c284f26 100644
> +--- a/test/ares-test.h
> ++++ b/test/ares-test.h
> +@@ -303,6 +303,12 @@ class MockServer {
> +     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<ares_socket_t> fds() const;
> +
> +@@ -327,6 +333,8 @@ class MockServer {
> +                                 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<byte> reply);
> +   unsigned short udpport_;
> +   unsigned short tcpport_;
> +   ares_socket_t  udpfd_;
> +@@ -339,6 +347,10 @@ class MockServer {
> +   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.
> diff --git a/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-channel-config-under-lock.patch b/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-channel-config-under-lock.patch
> new file mode 100644
> index 0000000000..76c072dd7e
> --- /dev/null
> +++ b/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-channel-config-under-lock.patch
> @@ -0,0 +1,243 @@
> +From 25a6911370878d4b617b44b142ea0e49d1ccede6 Mon Sep 17 00:00:00 2001
> +From: Brad House <brad@brad-house.com>
> +Date: Mon, 19 Oct 2026 11:53:59 -0400
> +Subject: [PATCH] [Backport v1.34] ares_save_options: read channel
> + configuration under the channel lock (#1362)
> +MIME-Version: 1.0
> +Content-Type: text/plain; charset=UTF-8
> +Content-Transfer-Encoding: 8bit
> +
> +Backport of 5d878a580a8b57b51d15e2725cf6775c8e7bca0e to `v1.34`. See
> +that commit for the full description.
> +
> +**Conflicts:** none; cherry-picked cleanly.
> +
> +**Tests (local, macOS, Debug, Ninja):**
> +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> +--gtest_filter='-*Live*'` — 691/691 passed in both.
> +- With the `src/` changes reverted, the new `DupConcurrentSetServers` /
> +`SaveOptionsConcurrentSetServers` tests abort with an ASAN
> +heap-buffer-overflow; they pass with them.
> +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> +with the other two v1.34 backports (both orders apply cleanly): 699/699
> +passed under ASAN+UBSAN and `-Werror`.
> +
> +Signed-off-by: Brad House <brad@brad-house.com>
> +Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/25a6911370878d4b617b44b142ea0e49d1ccede6]
> +GHSA: GHSA-ph6c-3cvx-mq6v
> +---
> + src/lib/ares_init.c    |  3 +-
> + src/lib/ares_options.c | 45 ++++++++++++++++++----
> + test/ares-test-init.cc | 87 ++++++++++++++++++++++++++++++++++++++++++
> + 3 files changed, 126 insertions(+), 9 deletions(-)
> +
> +diff --git a/src/lib/ares_init.c b/src/lib/ares_init.c
> +index 4758c5de56..85b2e13050 100644
> +--- a/src/lib/ares_init.c
> ++++ b/src/lib/ares_init.c
> +@@ -483,7 +483,8 @@ int ares_dup(ares_channel_t **dest, const ares_channel_t *src)
> +   *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);
> +diff --git a/src/lib/ares_options.c b/src/lib/ares_options.c
> +index 2b5624bbea..5ea35b732e 100644
> +--- a/src/lib/ares_options.c
> ++++ b/src/lib/ares_options.c
> +@@ -55,8 +55,8 @@ static struct in_addr *ares_save_opt_servers(const ares_channel_t *channel,
> +                                              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 @@ static struct in_addr *ares_save_opt_servers(const ares_channel_t *channel,
> +     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 @@ static struct in_addr *ares_save_opt_servers(const ares_channel_t *channel,
> +   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 @@ int ares_save_options(const ares_channel_t *channel,
> +   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 @@ int ares_save_options(const ares_channel_t *channel,
> +   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 @@ int ares_save_options(const ares_channel_t *channel,
> +   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)
> +diff --git a/test/ares-test-init.cc b/test/ares-test-init.cc
> +index b44211a1e4..1c5d0f2171 100644
> +--- a/test/ares-test-init.cc
> ++++ b/test/ares-test-init.cc
> +@@ -25,6 +25,8 @@
> +  */
> + #include "ares-test.h"
> +
> ++#include <atomic>
> ++
> + extern "C" {
> +   #include "ares_private.h"
> + }
> +@@ -295,6 +297,91 @@ TEST_F(LibraryTest, FailChannelInit) {
> +   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<size_t> 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<std::thread> 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;
> diff --git a/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-prefix-length.patch b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-prefix-length.patch
> new file mode 100644
> index 0000000000..2cd1ea8c24
> --- /dev/null
> +++ b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-prefix-length.patch
> @@ -0,0 +1,364 @@
> +From 7e65b381c0b915aa5671ddc713080c87e15fc42f Mon Sep 17 00:00:00 2001
> +From: Brad House <brad@brad-house.com>
> +Date: Mon, 19 Oct 2026 12:24:52 -0400
> +Subject: [PATCH] [Backport v1.34] ares_inet_pton: reject a prefix length,
> + always write the full address (#1363)
> +MIME-Version: 1.0
> +Content-Type: text/plain; charset=UTF-8
> +Content-Transfer-Encoding: 8bit
> +
> +Backport of 7b4ef8e555b611be6af39d913aa280cd1a348102 to `v1.34`. See
> +that commit for the full description.
> +
> +**Conflicts:** `test/ares-test-mock-ai.cc` only — the neighbouring
> +`FamilyV6IgnoresARecords` test (added on main by an unrelated change)
> +does not exist on v1.34. Resolved by adding only the new
> +`NumericHostRejectsPrefixLength` test at the same position. Other files
> +applied unchanged.
> +
> +**Also includes** the comment spelling fix from #1364
> +(ead5557d1301328536c3920bd01759a1bff5ca9a): "unparseable" ->
> +"unparsable" in `src/lib/inet_net_pton.c` and
> +`test/ares-test-internal.cc`, so Codespell passes and the text matches
> +main.
> +
> +**Tests (local, macOS, Debug, Ninja):**
> +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> +--gtest_filter='-*Live*'` — 693/693 passed in both.
> +- With the `src/` changes reverted, the 4 new test instances fail
> +(`InetNetPtoNPrefixZeroFills`, `InetPtoNRejectsPrefixLength`,
> +`NumericHostRejectsPrefixLength` UDP/TCP); they pass with them.
> +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> +with the other two v1.34 backports (both orders apply cleanly): 699/699
> +passed under ASAN+UBSAN and `-Werror`.
> +
> +Signed-off-by: Brad House <brad@brad-house.com>
> +Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/7e65b381c0b915aa5671ddc713080c87e15fc42f]
> +GHSA: GHSA-cfcv-8v2j-qj6r
> +---
> + docs/ares_inet_pton.3        | 18 ++++++++--
> + src/lib/ares_inet_net_pton.h | 28 ++++++++++++++++
> + src/lib/inet_net_pton.c      | 65 ++++++++++++++++++++----------------
> + test/ares-test-internal.cc   | 54 ++++++++++++++++++++++++++++++
> + test/ares-test-mock-ai.cc    | 23 +++++++++++++
> + 5 files changed, 157 insertions(+), 31 deletions(-)
> +
> +diff --git a/docs/ares_inet_pton.3 b/docs/ares_inet_pton.3
> +index 34b2df063c..19c5fdfda3 100644
> +--- a/docs/ares_inet_pton.3
> ++++ b/docs/ares_inet_pton.3
> +@@ -12,8 +12,9 @@ ares_inet_pton \- convert an IPv4 or IPv6 address from text to binary form
> + 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 @@ in. The \fBdst\fP argument points to a buffer into which the function stores
> + 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 --git a/src/lib/ares_inet_net_pton.h b/src/lib/ares_inet_net_pton.h
> +index e3ed83a3cb..e1a6f2d71e 100644
> +--- a/src/lib/ares_inet_net_pton.h
> ++++ b/src/lib/ares_inet_net_pton.h
> +@@ -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 --git a/src/lib/inet_net_pton.c b/src/lib/inet_net_pton.c
> +index e1f76ef834..f60823023b 100644
> +--- a/src/lib/inet_net_pton.c
> ++++ b/src/lib/inet_net_pton.c
> +@@ -51,14 +51,15 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0, 0, 0, 0, 0, 0, 0, 0, 0,
> +
> + /*
> +  * 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 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0, 0, 0, 0, 0, 0, 0, 0, 0,
> +  *      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 @@ static int ares_inet_net_pton_ipv4(const char *src, unsigned char *dst,
> +   }
> +
> +   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 @@ static int ares_inet_pton6(const char *src, unsigned char *dst)
> +       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 @@ static int ares_inet_net_pton_ipv6(const char *src, unsigned char *dst,
> +     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 ares_inet_net_pton(int af, const char *src, void *dst, size_t size)
> +   }
> + }
> +
> ++/*
> ++ * 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 --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> +index bf06a74605..294e3f4555 100644
> +--- a/test/ares-test-internal.cc
> ++++ b/test/ares-test-internal.cc
> +@@ -274,6 +274,23 @@ TEST_F(LibraryTest, InetNetPtoN) {
> +   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++) {
> +@@ -600,9 +617,46 @@ TEST_F(LibraryTest, InetPtoN) {
> +   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.
> +diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc
> +index ae45c45527..43d0b89175 100644
> +--- a/test/ares-test-mock-ai.cc
> ++++ b/test/ares-test-mock-ai.cc
> +@@ -617,6 +617,29 @@ TEST_P(MockChannelTestAI, FamilyV6) {
> +   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<byte> nothing;
> diff --git a/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch b/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> new file mode 100644
> index 0000000000..f4f182b995
> --- /dev/null
> +++ b/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> @@ -0,0 +1,178 @@
> +From c7fa9ebcb0c0086730c53bf03191c03f03706246 Mon Sep 17 00:00:00 2001
> +From: "c-ares-backport-gha[bot]"
> + <299594594+c-ares-backport-gha[bot]@users.noreply.github.com>
> +Date: Tue, 20 Oct 2026 02:25:59 +0000
> +Subject: [PATCH] [Backport v1.34] ares_conn_read: don't drop UDP answers whose
> + source port isn't reported (#1371)
> +MIME-Version: 1.0
> +Content-Type: text/plain; charset=UTF-8
> +Content-Transfer-Encoding: 8bit
> +
> +# Description
> +Backport of #1370 to `v1.34`.
> +
> +Signed-off-by: Brad House <brad@brad-house.com>
> +Co-authored-by: Brad House <brad@brad-house.com>
> +Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/c7fa9ebcb0c0086730c53bf03191c03f03706246]
> +GHSA: GHSA-fqh5-3x7m-wp44
> +---
> + docs/ares_set_socket_functions.3 | 20 ++++++++++++++++++++
> + src/lib/ares_conn.c              |  9 ++++++++-
> + src/lib/ares_socket.c            |  4 +++-
> + src/lib/ares_socket.h            |  8 +++++---
> + test/ares-test-mock.cc           | 32 +++++++++++++++++++++++++++++++-
> + 5 files changed, 67 insertions(+), 6 deletions(-)
> +
> +diff --git a/docs/ares_set_socket_functions.3 b/docs/ares_set_socket_functions.3
> +index 605beb26ef..75c621cb48 100644
> +--- a/docs/ares_set_socket_functions.3
> ++++ b/docs/ares_set_socket_functions.3
> +@@ -149,6 +149,20 @@ a socket. Additional flags controlling behavior are in
> + 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 @@ Receives data from remote socket endpoint, if available. If the \fIaddr\fP
> + 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
> +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> +index b868053e62..7711d8649e 100644
> +--- a/src/lib/ares_conn.c
> ++++ b/src/lib/ares_conn.c
> +@@ -257,7 +257,14 @@ ares_conn_err_t ares_conn_read(ares_conn_t *conn, void *data, size_t len,
> +      * 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. */
> ++     * 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_port_eq((struct sockaddr *)&sa_storage,
> +                                     &conn->server->addr,
> +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> +index 0a12935ab7..29d595e1a9 100644
> +--- a/src/lib/ares_socket.c
> ++++ b/src/lib/ares_socket.c
> +@@ -144,7 +144,9 @@ ares_bool_t ares_sockaddr_addr_port_eq(const struct sockaddr  *sa,
> +     sa_port = (CARES_INADDR_CAST(const struct sockaddr_in6 *, sa))->sin6_port;
> +   }
> +
> +-  if (ntohs(sa_port) != 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;
> +   }
> +
> +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> +index f7128e3493..1ede6987ee 100644
> +--- a/src/lib/ares_socket.h
> ++++ b/src/lib/ares_socket.h
> +@@ -151,13 +151,15 @@ 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.
> ++ *  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, IP addresses and ports are
> +- *          equal, ARES_FALSE otherwise
> ++ *  \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,
> +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> +index 058dc93001..cc5e6482f2 100644
> +--- a/test/ares-test-mock.cc
> ++++ b/test/ares-test-mock.cc
> +@@ -3364,6 +3364,7 @@ struct FakeSockData {
> +   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;
> +@@ -3472,7 +3473,9 @@ static ares_ssize_t FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> +   if (address != NULL && address_len != NULL &&
> +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> +     memcpy(address, &d->peer, sizeof(d->peer));
> +-    if (d->recv_port != 0) {
> ++    if (d->recv_no_port) {
> ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = 0;
> ++    } else if (d->recv_port != 0) {
> +       reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
> +     }
> +     *address_len = sizeof(d->peer);
> +@@ -3750,6 +3753,33 @@ TEST_F(LibraryTest, UDPAnswerFromWrongPortDropped) {
> +   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.
> diff --git a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb b/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> index 058f3666e7..1d0f3eb343 100644
> --- a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> +++ b/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> @@ -6,6 +6,14 @@ LICENSE = "MIT"
>  LIC_FILES_CHKSUM = "file://LICENSE.md;md5=d3e72a10e08191f2ca1be3f3228d78f3"
>
>  SRC_URI = "https://github.com/c-ares/c-ares/releases/download/v${PV}/${BPN}-${PV}.tar.gz \
> +           file://0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch \
> +           file://0002-Deliver-deferred-socket-server-state-notifications.patch \
> +           file://0003-ares_qcache-hand-callbacks-their-own-copy.patch \
> +           file://0004-Handle-connections-torn-down-by-application.patch \
> +           file://0005-ares_process-only-accept-an-answer-on-the-right-connection.patch \
> +           file://0006-ares_save_options-read-channel-config-under-lock.patch \
> +           file://0007-ares_inet_pton-reject-a-prefix-length.patch \
> +           file://0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch \
>             file://run-ptest"
>  SRC_URI[sha256sum] = "c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78"
>
> --
> 2.43.0
>
>
> -=-=-=-=-=-=-=-=-=-=-=-
> Links: You receive all messages sent to this group.
> View/Reply Online (#131454): https://lists.openembedded.org/g/openembedded-devel/message/131454
> Mute This Topic: https://lists.openembedded.org/mt/121953673/3619737
> Group Owner: openembedded-devel+owner@lists.openembedded.org
> Unsubscribe: https://lists.openembedded.org/g/openembedded-devel/unsub [ankur.tyagi85@gmail.com]
> -=-=-=-=-=-=-=-=-=-=-=-
>
Marko, Peter Oct. 27, 2026, 8:25 a.m. UTC | #2
Can we rather do the upgrade instead? I was planning to send it for Wrynose branch eventually.
Is there anything in "Changes" section of https://github.com/c-ares/c-ares/releases/tag/v1.34.9 which would prevent it?

Peter

> -----Original Message-----
> From: openembedded-devel@lists.openembedded.org <openembedded-
> devel@lists.openembedded.org> On Behalf Of Ankur Tyagi via
> lists.openembedded.org
> Sent: Tuesday, October 27, 2026 8:04 AM
> To: schonm@gmail.com
> Cc: openembedded-devel@lists.openembedded.org
> Subject: Re: [oe] [meta-oe][blacksail][PATCH] c-ares: backport security fixes from
> 1.34.9
> 
> Hi Jason,
> 
> Thanks for submitting the patch.
> 
> Though the patches LGTM, I am seeing increased ptest failures.
> 
> Existing recipe ptest status:
> [==========] 1093 tests from 41 test suites ran.
> [  PASSED  ] 1092 tests.
> [  FAILED  ] 1 test, listed below:
> [  FAILED  ] DefaultChannelTest.TimeoutValue
> 
> ptest status after applying your patch:
> [==========] 1174 tests from 47 test suites ran.
> [  PASSED  ] 1170 tests.
> [  FAILED  ] 4 tests, listed below:
> [  FAILED  ] DefaultChannelTest.TimeoutValue
> [  FAILED  ] SockStateDeliveryTest.CancelReportsSocketClose
> [  FAILED  ] SockStateDeliveryTest.SetServersReportsSocketClose
> [  FAILED  ] SockStateDeliveryTest.DestroyReportsSocketClose
> 
> Perhaps some other commits also need backporting?
> Can please look into this?
> 
> cheers
> Ankur
> 
> On Tue, Oct 27, 2026 at 2:22 PM J. S. via lists.openembedded.org
> <schonm=gmail.com@lists.openembedded.org> wrote:
> >
> > Add the eight unique upstream commits that fix the security advisories
> addressed in c-ares 1.34.9.
> > Several advisories share commits, so patches are ordered by commit rather
> than duplicated per advisory; each patch lists the GHSA IDs it resolves.
> >
> > Assisted-by: opencode (big-pickle)
> > Signed-off-by: Jason Schonberg <schonm@gmail.com>
> > ---
> >  ...troy-detach-queries-before-callbacks.patch | 2603 +++++++++++++++++
> >  ...ed-socket-server-state-notifications.patch |  393 +++
> >  ...qcache-hand-callbacks-their-own-copy.patch | 1054 +++++++
> >  ...connections-torn-down-by-application.patch | 1107 +++++++
> >  ...pt-an-answer-on-the-right-connection.patch |  575 ++++
> >  ...tions-read-channel-config-under-lock.patch |  243 ++
> >  ...res_inet_pton-reject-a-prefix-length.patch |  364 +++
> >  ...nt-drop-UDP-answers-from-server-port.patch |  178 ++
> >  .../recipes-support/c-ares/c-ares_1.34.8.bb   |    8 +
> >  9 files changed, 6525 insertions(+)
> >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-
> ares_destroy-detach-queries-before-callbacks.patch
> >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-
> deferred-socket-server-state-notifications.patch
> >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0003-
> ares_qcache-hand-callbacks-their-own-copy.patch
> >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0004-Handle-
> connections-torn-down-by-application.patch
> >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0005-
> ares_process-only-accept-an-answer-on-the-right-connection.patch
> >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0006-
> ares_save_options-read-channel-config-under-lock.patch
> >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0007-
> ares_inet_pton-reject-a-prefix-length.patch
> >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0008-
> ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> >
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-
> ares_destroy-detach-queries-before-callbacks.patch b/meta-oe/recipes-
> support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-
> callbacks.patch
> > new file mode 100644
> > index 0000000000..385760f5bc
> > --- /dev/null
> > +++ b/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-
> detach-queries-before-callbacks.patch
> > @@ -0,0 +1,2603 @@
> > +From 7f408deb128fbdcce78122322734ffaa01cad9ae Mon Sep 17 00:00:00
> 2001
> > +From: Brad House <brad@brad-house.com>
> > +Date: Sun, 18 Oct 2026 18:15:41 -0400
> > +Subject: [PATCH] ares_cancel/ares_destroy: detach queries before running
> user
> > + callbacks
> > +MIME-Version: 1.0
> > +Content-Type: text/plain; charset=UTF-8
> > +Content-Transfer-Encoding: 8bit
> > +
> > +Backport of dd62bdfca52ba5c37e88a1469c0a4c5e4a568ccb to v1.34.  See
> that
> > +commit for the full description; it hardens reentrant teardown from
> > +callbacks (ares_cancel(), ares_destroy(), server reconfiguration, and
> > +socket/server state notifications).
> > +
> > +Conflicts were whitespace/alignment only (v1.34 predates the clang-format
> > +reformat in #1238) and were reformatted to v1.34's style.
> > +
> > +Also carries the test portion of #1334 that applies to the tests this
> > +introduces: dead-server tests use 127.0.0.1:53055/53056 instead of
> > +127.0.0.55/56, which only routes on Linux.
> > +
> > +Signed-off-by: Brad House <brad@brad-house.com>
> > +Upstream-Status: Backport [https://github.com/c-ares/c-
> ares/commit/7f408deb128fbdcce78122322734ffaa01cad9ae]
> > +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-
> 2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> > +---
> > + docs/ares_cancel.3                    |   17 +
> > + docs/ares_destroy.3                   |    7 +
> > + docs/ares_init_options.3              |   15 +
> > + docs/ares_set_server_state_callback.3 |   31 +-
> > + docs/ares_set_servers_csv.3           |   17 +
> > + src/lib/ares_cancel.c                 |   25 +-
> > + src/lib/ares_close_sockets.c          |   82 +-
> > + src/lib/ares_conn.c                   |  217 ++++-
> > + src/lib/ares_conn.h                   |   37 +-
> > + src/lib/ares_destroy.c                |   85 +-
> > + src/lib/ares_private.h                |  110 ++-
> > + src/lib/ares_process.c                |  235 +++++-
> > + src/lib/ares_send.c                   |   35 +
> > + src/lib/ares_update_servers.c         |   42 +-
> > + test/ares-test-mock.cc                | 1068 +++++++++++++++++++++++++
> > + 15 files changed, 1959 insertions(+), 64 deletions(-)
> > +
> > +diff --git a/docs/ares_cancel.3 b/docs/ares_cancel.3
> > +index 967a946b49..069f226c10 100644
> > +--- a/docs/ares_cancel.3
> > ++++ b/docs/ares_cancel.3
> > +@@ -21,6 +21,23 @@ These calls give the callbacks a chance to clean up
> any state which might have
> > + 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 --git a/docs/ares_destroy.3 b/docs/ares_destroy.3
> > +index d1984b7cc9..0b6380e609 100644
> > +--- a/docs/ares_destroy.3
> > ++++ b/docs/ares_destroy.3
> > +@@ -20,6 +20,13 @@ by the channel.
> > + 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 --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> > +index 9b3b481535..e6f6ee8ba6 100644
> > +--- a/docs/ares_init_options.3
> > ++++ b/docs/ares_init_options.3
> > +@@ -231,6 +231,21 @@ will be passed the socket whose state has changed;
> > + 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
> > +diff --git a/docs/ares_set_server_state_callback.3
> b/docs/ares_set_server_state_callback.3
> > +index 04fbde4c7a..e95db327ce 100644
> > +--- a/docs/ares_set_server_state_callback.3
> > ++++ b/docs/ares_set_server_state_callback.3
> > +@@ -47,6 +47,31 @@ the DNS servers used by an ares channel. For
> example, it can be used to track
> > + 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 @@ Indicates that the query was tried over TCP.
> > + .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 --git a/docs/ares_set_servers_csv.3 b/docs/ares_set_servers_csv.3
> > +index f1435143f5..f9f5b82e28 100644
> > +--- a/docs/ares_set_servers_csv.3
> > ++++ b/docs/ares_set_servers_csv.3
> > +@@ -126,6 +126,23 @@ are identical.  Prior versions would simply omit ports
> in \fBares_set_servers_cs
> > + 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 --git a/src/lib/ares_cancel.c b/src/lib/ares_cancel.c
> > +index 75600dea6b..aa167e4486 100644
> > +--- a/src/lib/ares_cancel.c
> > ++++ b/src/lib/ares_cancel.c
> > +@@ -38,10 +38,10 @@ void ares_cancel(ares_channel_t *channel)
> > +   }
> > +
> > +   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 @@ void ares_cancel(ares_channel_t *channel)
> > +       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 @@ void ares_cancel(ares_channel_t *channel)
> > +   ares_queue_notify_empty(channel);
> > +
> > + done:
> > ++  ares_deferred_cb_leave(channel);
> > +   ares_channel_unlock(channel);
> > + }
> > +diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
> > +index 7bb72c31ec..6be662fdd3 100644
> > +--- a/src/lib/ares_close_sockets.c
> > ++++ b/src/lib/ares_close_sockets.c
> > +@@ -41,12 +41,47 @@ static void ares_requeue_queries(ares_conn_t
> *conn,
> > +   }
> > + }
> > +
> > ++/*! 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;
> > ++
> > ++  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;
> > ++  }
> > +
> > +-  /* Unlink */
> > ++  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 @@ void ares_close_connection(ares_conn_t *conn,
> ares_status_t requeue_status)
> > +     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);
> > +-
> > +-  ares_socket_close(channel, conn->fd);
> > ++  /* 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;
> > ++
> > ++  /* 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 @@ void ares_close_sockets(ares_server_t *server)
> > +
> > +   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);
> > +   }
> > + }
> > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > +index 6b315b0548..78f57e7d10 100644
> > +--- a/src/lib/ares_conn.c
> > ++++ b/src/lib/ares_conn.c
> > +@@ -26,19 +26,210 @@
> > +  */
> > + #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 ((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);
> > ++    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.
> > ++   *
> > ++   * 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;
> > ++
> > ++  /* 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;
> > + }
> > +
> > +@@ -435,6 +626,16 @@ ares_status_t ares_open_connection(ares_conn_t
> **conn_out,
> > +     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) {
> > +diff --git a/src/lib/ares_conn.h b/src/lib/ares_conn.h
> > +index c332de14b1..fc177b84f6 100644
> > +--- a/src/lib/ares_conn.h
> > ++++ b/src/lib/ares_conn.h
> > +@@ -49,7 +49,14 @@ typedef enum {
> > +    *  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
> > ++  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 {
> > +@@ -84,6 +91,10 @@ struct ares_conn {
> > +
> > +   /* 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 */
> > +@@ -168,6 +179,10 @@ struct ares_server {
> > +
> > +   /* 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);
> > +@@ -185,8 +200,24 @@ ares_status_t   ares_conn_flush(ares_conn_t
> *conn);
> > + 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 --git a/src/lib/ares_destroy.c b/src/lib/ares_destroy.c
> > +index 1e5706e0e0..e0534c800d 100644
> > +--- a/src/lib/ares_destroy.c
> > ++++ b/src/lib/ares_destroy.c
> > +@@ -69,17 +69,31 @@ void ares_destroy(ares_channel_t *channel)
> > +    * 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 @@ void ares_destroy(ares_channel_t *channel)
> > +   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 @@ void ares_destroy(ares_channel_t *channel)
> > +   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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > +index 04d4576ed2..0a1029e0f8 100644
> > +--- a/src/lib/ares_private.h
> > ++++ b/src/lib/ares_private.h
> > +@@ -147,7 +147,10 @@ typedef struct ares_query ares_query_t;
> > + 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;
> > +
> > +@@ -227,6 +230,9 @@ struct ares_channeldata {
> > +   /* 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
> > +    */
> > +@@ -237,6 +243,10 @@ struct ares_channeldata {
> > +   /* 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;
> > +
> > +@@ -248,6 +258,29 @@ struct ares_channeldata {
> > +
> > +   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;
> > +@@ -295,6 +328,10 @@ struct ares_channeldata {
> > +   /* 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
> > +@@ -308,6 +345,11 @@ struct ares_channeldata {
> > +    * 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. */
> > +@@ -351,6 +393,16 @@ void         *ares_dnsrec_convert_arg(ares_callback
> callback, void *arg);
> > + 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);
> > +@@ -390,7 +442,6 @@ ares_status_t
> ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
> > +
> > + 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);
> > +@@ -589,6 +640,60 @@ ares_status_t ares_dns_name_write(ares_buf_t
> *buf, ares_llist_t **list,
> > +  */
> > + 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.
> > ++ *
> > ++ *  \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)
> > +@@ -636,7 +741,6 @@ typedef struct ares_event_thread
> ares_event_thread_t;
> > + 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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > +index b2316c4b33..82f53610fb 100644
> > +--- a/src/lib/ares_process.c
> > ++++ b/src/lib/ares_process.c
> > +@@ -69,7 +69,6 @@ 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_detach_query(ares_query_t *query);
> > +
> > + static void        ares_query_remove_from_conn(ares_query_t *query)
> > + {
> > +@@ -81,14 +80,32 @@ static void
> ares_query_remove_from_conn(ares_query_t *query)
> > +   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;
> > +@@ -111,9 +128,57 @@ static void invoke_server_state_cb(const
> ares_server_t *server,
> > +     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,
> > +@@ -204,6 +269,8 @@ static ares_status_t
> ares_process_fds_nolock(ares_channel_t         *channel,
> > +     return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
> > +   }
> > +
> > ++  ares_deferred_cb_enter(channel);
> > ++
> > +   ares_tvnow(&now);
> > +
> > +   /* Process write events */
> > +@@ -244,6 +311,8 @@ static ares_status_t
> ares_process_fds_nolock(ares_channel_t         *channel,
> > +   }
> > +
> > + done:
> > ++  ares_deferred_cb_leave(channel);
> > ++
> > +   if (status == ARES_ENOMEM) {
> > +     return ARES_ENOMEM;
> > +   }
> > +@@ -553,6 +622,8 @@ typedef enum {
> > + typedef struct {
> > +   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 */
> > +@@ -578,6 +649,7 @@ static ares_status_t
> ares_append_requeue_int(ares_array_t     **requeue,
> > +
> > +   entry.type   = type;
> > +   entry.qid    = query->qid;
> > ++  entry.serial = query->serial;
> > +   entry.server = server;
> > +   entry.status = status;
> > +   entry.dnsrec = dnsrec;
> > +@@ -601,6 +673,53 @@ static ares_status_t
> ares_append_endqueue(ares_array_t     **requeue,
> > +     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:
> > +@@ -636,11 +755,35 @@ static ares_status_t
> ares_flush_requeue(ares_channel_t       *channel,
> > +
> > +     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. */
> > +@@ -1377,6 +1520,7 @@ ares_status_t ares_send_query(ares_server_t
> *requested_server,
> > +   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);
> > +@@ -1393,8 +1537,7 @@ ares_status_t ares_send_query(ares_server_t
> *requested_server,
> > +    * 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_htable_szvp_get_direct(channel->queries_by_qid, qid) == NULL) {
> > ++  if (status == ARES_SUCCESS && !ares_query_is_live(channel, qid, serial))
> {
> > +     status = ARES_ETIMEOUT;
> > +   }
> > +
> > +@@ -1412,6 +1555,9 @@ static ares_status_t
> ares_send_query_int(ares_server_t        *requested_server,
> > +   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) {
> > +@@ -1443,6 +1589,32 @@ static ares_status_t
> ares_send_query_int(ares_server_t        *requested_server,
> > +   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:
> > +@@ -1464,6 +1636,14 @@ static ares_status_t
> ares_send_query_int(ares_server_t        *requested_server,
> > +
> > +   /* 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;
> > ++  }
> > ++
> > +   switch (status) {
> > +     /* Good result, continue on */
> > +     case ARES_SUCCESS:
> > +@@ -1479,6 +1659,17 @@ static ares_status_t
> ares_send_query_int(ares_server_t        *requested_server,
> > +     case ARES_ECONNREFUSED:
> > +     case ARES_EBADFAMILY:
> > +       handle_conn_error(conn, ARES_TRUE, status);
> > ++      /* 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;
> > +@@ -1597,11 +1788,18 @@ static ares_bool_t same_questions(const
> ares_query_t      *query,
> > +   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;
> > + }
> > +@@ -1624,6 +1822,16 @@ static void end_query(ares_channel_t *channel,
> ares_server_t *server,
> > +     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);
> > +@@ -1631,7 +1839,8 @@ static void end_query(ares_channel_t *channel,
> ares_server_t *server,
> > +   /* 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);
> > + }
> > +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > +index 6efa9580b2..2e0482bf19 100644
> > +--- a/src/lib/ares_send.c
> > ++++ b/src/lib/ares_send.c
> > +@@ -117,6 +117,27 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +   unsigned short           id          = generate_unique_qid(channel);
> > +   const ares_dns_record_t *dnsrec_resp = 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);
> > +
> > +   if (ares_slist_len(channel->servers) == 0) {
> > +@@ -145,6 +166,7 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +
> > +   query->channel      = channel;
> > +   query->qid          = id;
> > ++  query->serial       = ++channel->query_serial;
> > +   query->timeout.sec  = 0;
> > +   query->timeout.usec = 0;
> > +   query->using_tcp =
> > +@@ -209,6 +231,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +    */
> > +   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 +245,17 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +
> > +   /* Perform the first query action. */
> > +
> > ++  /* Sending opens and writes to connections and can mark a server down,
> both
> > ++   * of which notify.  Deliver those once we are done with the connection and
> > ++   * the server, not part-way through. */
> > ++  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;
> > + }
> > +
> > +diff --git a/src/lib/ares_update_servers.c b/src/lib/ares_update_servers.c
> > +index f8d441accf..50a9c27925 100644
> > +--- a/src/lib/ares_update_servers.c
> > ++++ b/src/lib/ares_update_servers.c
> > +@@ -740,17 +740,36 @@ static ares_bool_t
> ares_servers_remove_stale(ares_channel_t *channel,
> > + {
> > +   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;
> > + }
> > +@@ -775,6 +794,14 @@ ares_status_t ares_servers_update(ares_channel_t
> *channel,
> > +     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
> > +    */
> > +@@ -843,6 +870,7 @@ ares_status_t ares_servers_update(ares_channel_t
> *channel,
> > +   status = ARES_SUCCESS;
> > +
> > + done:
> > ++  ares_deferred_cb_leave(channel);
> > +   return status;
> > + }
> > +
> > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > +index c0c0279abb..2158c5a913 100644
> > +--- a/test/ares-test-mock.cc
> > ++++ b/test/ares-test-mock.cc
> > +@@ -31,6 +31,11 @@
> > + #include <sys/stat.h>
> > + #endif
> > +
> > ++#ifndef _WIN32
> > ++#  include <errno.h>
> > ++#  include <fcntl.h>
> > ++#endif
> > ++
> > + #include <sstream>
> > + #include <vector>
> > +
> > +@@ -1914,6 +1919,7 @@ TEST_P(MockRetryDepthChannelTest,
> HighRetryNoStackOverflow) {
> > + struct CancelInCbData {
> > +   ares_channel_t *channel;
> > +   bool            done;
> > ++  size_t          cbs;
> > + };
> > +
> > + static void CancelChannelCallback(void *arg, ares_status_t status,
> > +@@ -1925,10 +1931,1071 @@ static void CancelChannelCallback(void *arg,
> ares_status_t 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<int> {
> > ++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<CancelInCbData *>(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<CancelInCbData *>(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<DestroyReentryData *>(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<DestroyReentryData *>(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);
> > ++
> > ++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<byte> 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<SockStateCancelData *>(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<SockStateCloseOrderData
> *>(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<int>(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<ReconfigureFromHookData
> *>(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<ReconfigureFromHookData
> *>(arg);
> > ++  (void)timeouts;
> > ++  (void)dnsrec;
> > ++  d->done   = true;
> > ++  d->status = static_cast<int>(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);
> > ++}
> > ++
> > ++// 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<SockStateReconfigureData *>(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);
> > ++}
> > ++
> > ++// 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<std::pair<ares_socket_t, bool>> events; /* fd, still-armed */
> > ++};
> > ++
> > ++static void SockStateRecordCallback(void *data, ares_socket_t socket_fd,
> > ++                                    int readable, int writable)
> > ++{
> > ++  SockStateRecordData *d = static_cast<SockStateRecordData *>(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<ServerStateCancelData *>(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<ServerStateCancelData *>(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<int> {
> > ++ 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<ServerStateUnregisterData *>(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<ServerStateUnregisterData *>(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<ReconfigureInCbData *>(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<ares_socket_t> 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<NestedReconfigureData
> *>(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<NestedReconfigureData
> *>(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()
> > +@@ -1940,6 +3007,7 @@ TEST_P(MockUDPChannelTest,
> CancelInCallbackNoDoubleFree) {
> > +   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();
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-
> server-state-notifications.patch b/meta-oe/recipes-support/c-ares/c-ares/0002-
> Deliver-deferred-socket-server-state-notifications.patch
> > new file mode 100644
> > index 0000000000..c34d3ab7c7
> > --- /dev/null
> > +++ b/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-
> server-state-notifications.patch
> > @@ -0,0 +1,393 @@
> > +From f944c4718fd672c9c0befd65a340abb2ccfc1a31 Mon Sep 17 00:00:00
> 2001
> > +From: Brad House <brad@brad-house.com>
> > +Date: Sun, 18 Oct 2026 20:28:11 -0400
> > +Subject: [PATCH] Deliver deferred socket/server-state notifications only from
> > + the outermost public call (#1340)
> > +MIME-Version: 1.0
> > +Content-Type: text/plain; charset=UTF-8
> > +Content-Transfer-Encoding: 8bit
> > +
> > +Backport of f023a52a (#1340) to v1.34.
> > +
> > +The recent change that queues socket-state and server-state
> > +notifications delivers them, and runs the deferred cleanup, only from
> > +the outermost `ares_deferred_cb_leave()`. `ares_send_nolock()` brackets
> > +`ares_send_query()` itself, but most public lookup functions never
> > +opened a region of their own. When reached from `ares_getaddrinfo()`,
> > +`ares_query_dnsrec()` and similar, the bracket inside
> > +`ares_send_nolock()` was therefore the outermost one. Notifications were
> > +then delivered from inside an internal call while the library was still
> > +in the middle of the public call, and its callers were still using their
> > +lookup state (the host query, the query id out-pointer, ...). A
> > +server-state callback that reconfigures the servers, which
> > +`ares_set_server_state_callback(3)` documents as allowed, could end that
> > +lookup before the caller was done with it.
> > +
> > +This is a regression in unreleased code. No released version is
> > +affected.
> > +
> > +The outermost region is now opened by the public entry points
> > +themselves, under the channel lock:
> > +
> > +- `ares_send_dnsrec()` (also used by `ares_send()`)
> > +- `ares_query_dnsrec()` (also used by `ares_query()`)
> > +- `ares_search()`, `ares_search_dnsrec()`
> > +- `ares_getaddrinfo()` (also used by `ares_gethostbyname()`)
> > +- `ares_gethostbyaddr()`, `ares_getnameinfo()`
> > +- `ares_process_pending_write()`: a failed flush closes the connection
> > +and fails its queries over, and this function had no bracket at all.
> > +
> > +`ares_process_fds()` (and through it
> > +`ares_process()`/`ares_process_fd()`), `ares_cancel()`, `ares_destroy()`
> > +and the server configuration functions already had their own. The nested
> > +bracket in `ares_send_nolock()` stays, because it keeps connections and
> > +servers unlinked during the send readable until the send returns.
> > +Internal sends made from query callbacks (search next domain,
> > +getaddrinfo next lookup, requeue, server probes) already run inside the
> > +processing bracket.
> > +
> > +The comments on `ares_deferred_cb_enter()` and the flush now state the
> > +rule: the outermost region belongs to the public entry point, never to
> > +an internal helper.
> > +
> > +The documented behaviour (notifications arrive before the outermost
> > +c-ares call returns) does not change, so no man page changes.
> > +
> > +Two new `LibraryTest` cases in `test/ares-test-mock.cc`. The IPv6 server
> > +cannot get a socket, so the first send fails over to the IPv4 server,
> > +and a server-state callback then reconfigures the servers:
> > +
> > +- `ServerFailoverDuringGetAddrInfo`: the lookup completes cleanly. It
> > +fails under ASAN without this change.
> > +- `ServerFailoverDuringQuery`: the query id from `ares_query_dnsrec()`
> > +is already reported by the time the query callback runs. It fails
> > +without this change.
> > +
> > +The full non-live suite passes under ASAN/UBSAN and with
> > +`-DCARES_WERROR=ON`.
> > +
> > +Signed-off-by: Brad House <brad@brad-house.com>
> > +Upstream-Status: Backport [https://github.com/c-ares/c-
> ares/commit/f944c4718fd672c9c0befd65a340abb2ccfc1a31]
> > +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-
> 2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> > +---
> > + src/lib/ares_conn.c          |   4 +-
> > + src/lib/ares_getaddrinfo.c   |   5 ++
> > + src/lib/ares_gethostbyaddr.c |   5 ++
> > + src/lib/ares_getnameinfo.c   |   5 ++
> > + src/lib/ares_private.h       |  10 ++++
> > + src/lib/ares_process.c       |   6 ++
> > + src/lib/ares_query.c         |   5 ++
> > + src/lib/ares_search.c        |  10 ++++
> > + src/lib/ares_send.c          |  13 ++++-
> > + test/ares-test-mock.cc       | 106 +++++++++++++++++++++++++++++++++++
> > + 10 files changed, 166 insertions(+), 3 deletions(-)
> > +
> > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > +index 78f57e7d10..4cc0d1871d 100644
> > +--- a/src/lib/ares_conn.c
> > ++++ b/src/lib/ares_conn.c
> > +@@ -94,7 +94,9 @@ 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.
> > ++   * 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
> > +diff --git a/src/lib/ares_getaddrinfo.c b/src/lib/ares_getaddrinfo.c
> > +index 6d9e21af4b..5d9ddb311e 100644
> > +--- a/src/lib/ares_getaddrinfo.c
> > ++++ b/src/lib/ares_getaddrinfo.c
> > +@@ -762,7 +762,12 @@ void ares_getaddrinfo(ares_channel_t *channel,
> const char *name,
> > +     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 --git a/src/lib/ares_gethostbyaddr.c b/src/lib/ares_gethostbyaddr.c
> > +index 69c509ab11..b314359331 100644
> > +--- a/src/lib/ares_gethostbyaddr.c
> > ++++ b/src/lib/ares_gethostbyaddr.c
> > +@@ -112,7 +112,12 @@ void ares_gethostbyaddr(ares_channel_t *channel,
> const void *addr, int addrlen,
> > +     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 --git a/src/lib/ares_getnameinfo.c b/src/lib/ares_getnameinfo.c
> > +index 0159354530..7365e9ed82 100644
> > +--- a/src/lib/ares_getnameinfo.c
> > ++++ b/src/lib/ares_getnameinfo.c
> > +@@ -194,7 +194,12 @@ void ares_getnameinfo(ares_channel_t *channel,
> const struct sockaddr *sa,
> > +   }
> > +
> > +   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);
> > + }
> > +
> > +diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > +index 0a1029e0f8..9610ba7dab 100644
> > +--- a/src/lib/ares_private.h
> > ++++ b/src/lib/ares_private.h
> > +@@ -676,6 +676,16 @@ void          ares_server_reap(ares_server_t *server);
> > +  *  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);
> > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > +index 82f53610fb..1569408b1b 100644
> > +--- a/src/lib/ares_process.c
> > ++++ b/src/lib/ares_process.c
> > +@@ -490,6 +490,11 @@ void ares_process_pending_write(ares_channel_t
> *channel)
> > +     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.
> > +@@ -513,6 +518,7 @@ void ares_process_pending_write(ares_channel_t
> *channel)
> > +     }
> > +   }
> > +
> > ++  ares_deferred_cb_leave(channel);
> > +   ares_channel_unlock(channel);
> > + }
> > +
> > +diff --git a/src/lib/ares_query.c b/src/lib/ares_query.c
> > +index ca3b6a9b73..0cf7f70760 100644
> > +--- a/src/lib/ares_query.c
> > ++++ b/src/lib/ares_query.c
> > +@@ -125,7 +125,12 @@ ares_status_t ares_query_dnsrec(ares_channel_t
> *channel, const char *name,
> > +   }
> > +
> > +   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 --git a/src/lib/ares_search.c b/src/lib/ares_search.c
> > +index c605caf42c..ebc3d91e20 100644
> > +--- a/src/lib/ares_search.c
> > ++++ b/src/lib/ares_search.c
> > +@@ -463,7 +463,12 @@ void ares_search(ares_channel_t *channel, const
> char *name, int dnsclass,
> > +   }
> > +
> > +   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_status_t ares_search_dnsrec(ares_channel_t
> *channel,
> > +   }
> > +
> > +   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 --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > +index 2e0482bf19..dd4a28544b 100644
> > +--- a/src/lib/ares_send.c
> > ++++ b/src/lib/ares_send.c
> > +@@ -246,8 +246,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +   /* Perform the first query action. */
> > +
> > +   /* Sending opens and writes to connections and can mark a server down,
> both
> > +-   * of which notify.  Deliver those once we are done with the connection and
> > +-   * the server, not part-way through. */
> > ++   * 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);
> > +@@ -271,9 +275,14 @@ ares_status_t ares_send_dnsrec(ares_channel_t
> *channel,
> > +   }
> > +
> > +   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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > +index 2158c5a913..b848c02434 100644
> > +--- a/test/ares-test-mock.cc
> > ++++ b/test/ares-test-mock.cc
> > +@@ -2533,6 +2533,112 @@ TEST_F(LibraryTest,
> ReconfigureInSockStateCallbackNoUseAfterFree) {
> > +   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<ares_channel_t *>(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<FailoverQueryData *>(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
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-
> callbacks-their-own-copy.patch b/meta-oe/recipes-support/c-ares/c-ares/0003-
> ares_qcache-hand-callbacks-their-own-copy.patch
> > new file mode 100644
> > index 0000000000..02039e92e0
> > --- /dev/null
> > +++ b/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-
> callbacks-their-own-copy.patch
> > @@ -0,0 +1,1054 @@
> > +From a21160045f9d7039225fa285be7cb1b02173a3a5 Mon Sep 17 00:00:00
> 2001
> > +From: Brad House <brad@brad-house.com>
> > +Date: Sun, 18 Oct 2026 22:12:09 -0400
> > +Subject: [PATCH] [Backport v1.34] ares_qcache: hand callbacks their own
> copy
> > + of a cached answer (#1339)
> > +MIME-Version: 1.0
> > +Content-Type: text/plain; charset=UTF-8
> > +Content-Transfer-Encoding: 8bit
> > +
> > +Backport of 31e2f51e8715d418fa8272a6c3a1ee651917fec5 (`ares_qcache:
> hand
> > +callbacks their own copy of a cached answer`) to `v1.34`. See that
> > +commit for the full description.
> > +
> > +**Conflicts:**
> > +- `ares_private.h`: alignment only; formatted with `v1.34`'s
> > +`.clang-format`.
> > +- `test/ares-test-internal.cc`: context only — on `main` the new
> > +`QcacheKeyDistinguishesQuestions` test follows the IDNA/punycode tests
> > +(#1031), which are not on `v1.34`, so it is placed after the URI test
> > +instead.
> > +
> > +**Test fixes from #1334:** carries the part of #1334 that applies to the
> > +tests this introduces — `ReentrantFlushInCallbackNoUseAfterFree`
> > +reconfigures to `127.0.0.1:53055` instead of `127.0.0.55` (only routable
> > +on Linux), and `UnknownQueryTypesDoNotShareCacheEntries` drops
> > +`static_cast<ns_type>`, which does not compile on OpenBSD.
> > +
> > +Test file contents match `main`. Independent of the dd62bdfc backport
> > +(#1338); applies cleanly with or without it.
> > +
> > +The full non-live suite passes 650/650 under ASAN+UBSAN and with
> > +`-DCARES_WERROR=ON` on macOS.
> `ReentrantFlushInCallbackNoUseAfterFree`
> > +is a heap-use-after-free in `ares_dns_record_rr_cnt()` on `v1.34`
> > +without this change, and passes with it.
> > +
> > +Signed-off-by: Brad House <brad@brad-house.com>
> > +Upstream-Status: Backport [https://github.com/c-ares/c-
> ares/commit/a21160045f9d7039225fa285be7cb1b02173a3a5]
> > +GHSA: GHSA-grv7-fq5q-9vfc, GHSA-wrgc-prmw-27xh
> > +---
> > + docs/ares_init_options.3   |  11 +-
> > + src/lib/ares_private.h     |  44 +++-
> > + src/lib/ares_process.c     |   1 +
> > + src/lib/ares_qcache.c      | 212 ++++++++++++++++---
> > + src/lib/ares_send.c        |  47 ++++-
> > + test/ares-test-internal.cc |  54 +++++
> > + test/ares-test-mock.cc     | 419 +++++++++++++++++++++++++++++++++++++
> > + 7 files changed, 743 insertions(+), 45 deletions(-)
> > +
> > +diff --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> > +index e6f6ee8ba6..1ce7fb8e30 100644
> > +--- a/docs/ares_init_options.3
> > ++++ b/docs/ares_init_options.3
> > +@@ -324,7 +324,16 @@ with the maximum number of seconds a query result
> may be cached which will
> > + 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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > +index 9610ba7dab..dd06d303b8 100644
> > +--- a/src/lib/ares_private.h
> > ++++ b/src/lib/ares_private.h
> > +@@ -153,6 +153,9 @@ struct ares_query {
> > +   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
> > +@@ -718,14 +721,47 @@ 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);
> > ++/*! 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);
> > +-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);
> > ++/*! 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);
> > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > +index 1569408b1b..88536704ad 100644
> > +--- a/src/lib/ares_process.c
> > ++++ b/src/lib/ares_process.c
> > +@@ -1859,6 +1859,7 @@ void ares_free_query(ares_query_t *query)
> > +   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 --git a/src/lib/ares_qcache.c b/src/lib/ares_qcache.c
> > +index ad3bc83505..f48669b56c 100644
> > +--- a/src/lib/ares_qcache.c
> > ++++ b/src/lib/ares_qcache.c
> > +@@ -38,7 +38,7 @@ typedef struct {
> > +   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 @@ static char *ares_qcache_calc_key(const
> ares_dns_record_t *dnsrec)
> > +     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 @@ static char *ares_qcache_calc_key(const
> ares_dns_record_t *dnsrec)
> > +   }
> > +
> > +   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 @@ static char *ares_qcache_calc_key(const
> ares_dns_record_t *dnsrec)
> > +       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 @@ static char *ares_qcache_calc_key(const
> ares_dns_record_t *dnsrec)
> > +       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 @@ static char *ares_qcache_calc_key(const
> ares_dns_record_t *dnsrec)
> > +       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 @@ static char *ares_qcache_calc_key(const
> ares_dns_record_t *dnsrec)
> > +       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 @@ static unsigned int
> ares_qcache_soa_minimum(ares_dns_record_t *dnsrec)
> > + }
> > +
> > + /* 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 @@ static ares_status_t
> ares_qcache_insert_int(ares_qcache_t           *qcache,
> > +   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 */
> > +   }
> > +@@ -382,31 +514,25 @@ static ares_status_t
> ares_qcache_insert_int(ares_qcache_t           *qcache,
> > +   /* 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;
> > +@@ -416,10 +542,24 @@ ares_status_t ares_qcache_fetch(ares_channel_t
> *channel,
> > +   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;
> > + }
> > +
> > +@@ -428,13 +568,25 @@ ares_status_t ares_qcache_insert(ares_channel_t
> *channel,
> > +                                  const ares_query_t      *query,
> > +                                  const ares_dns_record_t *dnsrec)
> > + {
> > +-  ares_dns_record_t *dupdns = ares_dns_record_duplicate(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->query,
> now);
> > ++
> > ++  status =
> > ++    ares_qcache_insert_int(channel->qcache, dupdns, query->qcache_key,
> now);
> > +   if (status != ARES_SUCCESS) {
> > +     ares_dns_record_destroy(dupdns);
> > +   }
> > +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > +index dd4a28544b..117745ce28 100644
> > +--- a/src/lib/ares_send.c
> > ++++ b/src/lib/ares_send.c
> > +@@ -111,11 +111,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +                                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
> > +@@ -145,13 +146,28 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +     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;
> > +     }
> > +   }
> > +@@ -159,8 +175,11 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +   /* 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));
> > +
> > +@@ -173,6 +192,10 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +     (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
> > +@@ -180,7 +203,11 @@ ares_status_t ares_send_nolock(ares_channel_t
> *channel, ares_server_t *server,
> > +     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;
> > +   }
> > +diff --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> > +index 8881f3b7cc..5ba5d2b39b 100644
> > +--- a/test/ares-test-internal.cc
> > ++++ b/test/ares-test-internal.cc
> > +@@ -502,6 +502,60 @@ TEST_F(LibraryTest, URI) {
> > +   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<std::string> 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];
> > ++    }
> > ++  }
> > ++}
> > ++
> > + #endif /* !CARES_SYMBOL_HIDING */
> > +
> > + TEST_F(LibraryTest, InetPtoN) {
> > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > +index b848c02434..4b409db7c3 100644
> > +--- a/test/ares-test-mock.cc
> > ++++ b/test/ares-test-mock.cc
> > +@@ -611,6 +611,425 @@ class CacheQueriesTest
> > +   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<QcacheReentrantCbData
> *>(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<ares_dns_rec_type_t>(29), QueryCallback, &r1,
> > ++                    nullptr);
> > ++  Process();
> > ++  EXPECT_TRUE(r1.done_);
> > ++
> > ++  QueryResult r2;
> > ++  ares_query_dnsrec(channel_, "example.net", ARES_CLASS_IN,
> > ++                    static_cast<ares_dns_rec_type_t>(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()
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-
> torn-down-by-application.patch b/meta-oe/recipes-support/c-ares/c-ares/0004-
> Handle-connections-torn-down-by-application.patch
> > new file mode 100644
> > index 0000000000..f78e9726b0
> > --- /dev/null
> > +++ b/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-
> down-by-application.patch
> > @@ -0,0 +1,1107 @@
> > +From 13a10dddbabc30b6fca8e23f9efff73fedba9401 Mon Sep 17 00:00:00
> 2001
> > +From: Brad House <brad@brad-house.com>
> > +Date: Sun, 18 Oct 2026 22:51:53 -0400
> > +Subject: [PATCH] [Backport v1.34] Handle connections torn down by
> application
> > + socket functions (#1343)
> > +MIME-Version: 1.0
> > +Content-Type: text/plain; charset=UTF-8
> > +Content-Transfer-Encoding: 8bit
> > +
> > +Backport of cb25d8bf297aed0aad8f6e0777ca0635cacc6cb1 to `v1.34`. See
> > +that commit for the full description.
> > +
> > +Application code that c-ares calls while using a connection can
> > +reconfigure the servers and so tear that connection down. That includes
> > +the socket functions from `ares_set_socket_functions_ex()` and the older
> > +`ares_set_socket_functions()`, and the pending-write callback. c-ares
> > +now notices this while opening, writing to, reading from or closing a
> > +connection. It requeues the affected queries, or drops the stale data,
> > +instead of continuing to use the connection.
> > +
> > +It applied cleanly on top of the dd62bdfc and #1340 backports (#1338).
> > +Test file contents match `main`.
> > +
> > +Each of the 11 new tests fails on `v1.34` without this change: nine with
> > +an ASAN heap-use-after-free, and the two receive tests on their
> > +assertions. All 11 pass with it. The full non-live suite passes 674/674
> > +under ASAN+UBSAN and with `-DCARES_WERROR=ON` on macOS.
> > +
> > +Signed-off-by: Brad House <brad@brad-house.com>
> > +Co-authored-by: Mar <mardgez13@gmail.com>
> > +Upstream-Status: Backport [https://github.com/c-ares/c-
> ares/commit/13a10dddbabc30b6fca8e23f9efff73fedba9401]
> > +GHSA: GHSA-pj7p-v3cw-hq66
> > +---
> > + src/lib/ares_close_sockets.c |   7 +-
> > + src/lib/ares_conn.c          |  29 ++
> > + src/lib/ares_process.c       |  65 ++-
> > + test/ares-test-mock.cc       | 842 +++++++++++++++++++++++++++++++++++
> > + 4 files changed, 935 insertions(+), 8 deletions(-)
> > +
> > +diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
> > +index 6be662fdd3..a3af721bc6 100644
> > +--- a/src/lib/ares_close_sockets.c
> > ++++ b/src/lib/ares_close_sockets.c
> > +@@ -148,6 +148,7 @@ void ares_check_cleanup_conns(const
> ares_channel_t *channel)
> > +     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)) {
> > +@@ -190,8 +191,12 @@ void ares_check_cleanup_conns(const
> ares_channel_t *channel)
> > +         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 --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > +index 4cc0d1871d..5e0827fbdf 100644
> > +--- a/src/lib/ares_conn.c
> > ++++ b/src/lib/ares_conn.c
> > +@@ -262,6 +262,15 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> *conn, void *data, size_t len,
> > +     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;
> > +@@ -408,6 +417,19 @@ ares_conn_err_t ares_conn_write(ares_conn_t
> *conn, const void *data, size_t len,
> > +   }
> > +
> > + 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
> > +@@ -644,6 +666,13 @@ ares_status_t ares_open_connection(ares_conn_t
> **conn_out,
> > +     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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > +index 88536704ad..5686b077a6 100644
> > +--- a/src/lib/ares_process.c
> > ++++ b/src/lib/ares_process.c
> > +@@ -189,7 +189,9 @@ static void server_increment_failures(ares_server_t
> *server,
> > +   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 */
> > +   }
> > +
> > +@@ -220,7 +222,9 @@ static void server_set_good(ares_server_t *server,
> ares_bool_t used_tcp)
> > +   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 */
> > +   }
> > +
> > +@@ -501,21 +505,39 @@ void ares_process_pending_write(ares_channel_t
> *channel)
> > +    */
> > +   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);
> > +@@ -902,6 +924,13 @@ static ares_status_t process_read(ares_channel_t
> *channel,
> > +     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;
> > +@@ -1192,6 +1221,15 @@ static void handle_conn_error(ares_conn_t *conn,
> ares_bool_t critical_failure,
> > + {
> > +   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) {
> > +@@ -1508,6 +1546,14 @@ static ares_status_t
> ares_conn_query_write(ares_conn_t          *conn,
> > +       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;
> > +   }
> > +
> > +@@ -1650,6 +1696,11 @@ static ares_status_t
> ares_send_query_int(ares_server_t        *requested_server,
> > +     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:
> > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > +index 4b409db7c3..f1d9d360e7 100644
> > +--- a/test/ares-test-mock.cc
> > ++++ b/test/ares-test-mock.cc
> > +@@ -2593,6 +2593,86 @@ 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<int> {
> > ++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<bool *>(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_;
> > +@@ -2898,6 +2978,768 @@ TEST_F(LibraryTest,
> ReconfigureInConfigureHookNoUseAfterFree) {
> > +   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<ReconfigureFromSendData *>(user_data);
> > ++  ares_ssize_t len = 0;
> > ++  (void)fd;
> > ++  for (int i = 0; i < iovcnt; i++) {
> > ++    len += static_cast<ares_ssize_t>(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<ReconfigureFromSendData
> *>(arg);
> > ++  (void)timeouts;
> > ++  (void)dnsrec;
> > ++  d->done   = true;
> > ++  d->status = static_cast<int>(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<BadSocketStateData *>(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<PendingWriteReconfigureData *>(user_data);
> > ++  ares_ssize_t len = 0;
> > ++  (void)fd;
> > ++  for (int i = 0; i < iovcnt; i++) {
> > ++    len += static_cast<ares_ssize_t>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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;
> > ++  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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<unsigned char *>(buffer)[2] |= 0x80;
> > ++  if (address != NULL && address_len != NULL &&
> > ++      *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > ++    memcpy(address, &d->peer, sizeof(d->peer));
> > ++    *address_len = sizeof(d->peer);
> > ++  }
> > ++  return static_cast<ares_ssize_t>(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<FakeSockData *>(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<ares_ssize_t>(length);
> > ++}
> > ++
> > ++static int FakeSockGetsockname(ares_socket_t sock, struct sockaddr
> *address,
> > ++                               ares_socklen_t *address_len, void *user_data)
> > ++{
> > ++  FakeSockData      *d = static_cast<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(arg);
> > ++  (void)timeouts;
> > ++  (void)dnsrec;
> > ++  d->done   = true;
> > ++  d->status = static_cast<int>(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);
> > ++}
> > ++
> > ++// 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
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-
> accept-an-answer-on-the-right-connection.patch b/meta-oe/recipes-support/c-
> ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-
> connection.patch
> > new file mode 100644
> > index 0000000000..20fef5d2f0
> > --- /dev/null
> > +++ b/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-
> an-answer-on-the-right-connection.patch
> > @@ -0,0 +1,575 @@
> > +From c8bd9ab53329f66ec0c395a11002a5696669b147 Mon Sep 17 00:00:00
> 2001
> > +From: Brad House <brad@brad-house.com>
> > +Date: Mon, 19 Oct 2026 11:53:54 -0400
> > +Subject: [PATCH] [Backport v1.34] ares_process: only accept an answer on the
> > + connection its query was sent on (#1361)
> > +MIME-Version: 1.0
> > +Content-Type: text/plain; charset=UTF-8
> > +Content-Transfer-Encoding: 8bit
> > +
> > +Backport of b6f8a9b47e17c681e092a74d05d73bbb35c89a39 to `v1.34`. See
> > +that commit for the full description.
> > +
> > +**Conflicts:** `src/lib/ares_socket.h` only — v1.34 aligns the
> > +neighbouring prototypes differently. Resolved by taking the new doc
> > +comments and the `ares_sockaddr_addr_port_eq()` prototype, then
> > +re-running clang-format for the v1.34 alignment (which also re-wraps the
> > +new `SendData()` declaration in `test/ares-test.h`). All other files
> > +applied unchanged; the `test/` diffs match main exactly.
> > +
> > +**Tests (local, macOS, Debug, Ninja):**
> > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > +--gtest_filter='-*Live*'` — 693/693 passed in both.
> > +- With the `src/` changes reverted, the 4 new test instances fail
> > +(`UDPAnswerFromWrongPortDropped`,
> `AnswerOnOtherServerConnectionDropped`
> > +UDP/TCP, `CookielessAnswerOnOtherServerDropped`); they pass with them.
> > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > +passed under ASAN+UBSAN and `-Werror`.
> > +
> > +Signed-off-by: Brad House <brad@brad-house.com>
> > +Upstream-Status: Backport [https://github.com/c-ares/c-
> ares/commit/c8bd9ab53329f66ec0c395a11002a5696669b147]
> > +GHSA: GHSA-fqh5-3x7m-wp44
> > +---
> > + src/lib/ares_conn.c    |  11 ++-
> > + src/lib/ares_process.c |  18 ++++
> > + src/lib/ares_socket.c  |  24 +++++
> > + src/lib/ares_socket.h  |  21 ++++
> > + test/ares-test-mock.cc | 218
> +++++++++++++++++++++++++++++++++++++++++
> > + test/ares-test.cc      |  86 +++++++++-------
> > + test/ares-test.h       |  12 +++
> > + 7 files changed, 355 insertions(+), 35 deletions(-)
> > +
> > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > +index 5e0827fbdf..b868053e62 100644
> > +--- a/src/lib/ares_conn.c
> > ++++ b/src/lib/ares_conn.c
> > +@@ -252,9 +252,16 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> *conn, void *data, size_t len,
> > +                            (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. */
> > +     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
> > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > +index 5686b077a6..06d0ca507c 100644
> > +--- a/src/lib/ares_process.c
> > ++++ b/src/lib/ares_process.c
> > +@@ -1117,6 +1117,24 @@ static ares_status_t
> process_answer(ares_channel_t      *channel,
> > +     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)
> > +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> > +index 4ad3034021..0a12935ab7 100644
> > +--- a/src/lib/ares_socket.c
> > ++++ b/src/lib/ares_socket.c
> > +@@ -127,6 +127,30 @@ ares_bool_t ares_sockaddr_addr_eq(const struct
> sockaddr  *sa,
> > +   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;
> > ++  }
> > ++
> > ++  if (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,
> > +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> > +index 24a99ab331..f7128e3493 100644
> > +--- a/src/lib/ares_socket.h
> > ++++ b/src/lib/ares_socket.h
> > +@@ -139,8 +139,29 @@ typedef enum {
> > +   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.
> > ++ *
> > ++ *  \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, IP addresses and ports are
> > ++ *          equal, 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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > +index d233f9070f..058dc93001 100644
> > +--- a/test/ares-test-mock.cc
> > ++++ b/test/ares-test-mock.cc
> > +@@ -3363,6 +3363,7 @@ struct FakeSockData {
> > +   unsigned char      query[512];
> > +   size_t             query_len;
> > +   struct sockaddr_in peer;
> > ++  unsigned short     recv_port; /* network order; 0 reports the peer's port */
> > +   size_t             recv_calls;
> > +   size_t             bad_fd_calls;
> > +   size_t             sock_state_bad;
> > +@@ -3471,6 +3472,9 @@ static ares_ssize_t
> FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> > +   if (address != NULL && address_len != NULL &&
> > +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > +     memcpy(address, &d->peer, sizeof(d->peer));
> > ++    if (d->recv_port != 0) {
> > ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
> > ++    }
> > +     *address_len = sizeof(d->peer);
> > +   }
> > +   return static_cast<ares_ssize_t>(d->query_len);
> > +@@ -3710,6 +3714,42 @@ TEST_F(LibraryTest,
> ReconfigureInRecvDropsBufferedAnswer) {
> > +   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);
> > ++}
> > ++
> > + // 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.
> > +@@ -5530,5 +5570,183 @@
> INSTANTIATE_TEST_SUITE_P(TransportModes, ServerRecoveryMultiMockTest,
> ::testing:
> > +
> > + 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<int, bool> > {
> > ++ 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<byte>());
> > ++    }));
> > ++  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<int> {
> > ++ 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<byte> 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<byte>());
> > ++    }));
> > ++  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 --git a/test/ares-test.cc b/test/ares-test.cc
> > +index d333ee9419..0b3ac12c09 100644
> > +--- a/test/ares-test.cc
> > ++++ b/test/ares-test.cc
> > +@@ -448,6 +448,9 @@ MockServer::MockServer(int family, unsigned short
> port)
> > +   // 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;
> > +@@ -710,6 +713,50 @@ std::set<ares_socket_t> MockServer::fds() const {
> > +   return result;
> > + }
> > +
> > ++void MockServer::SendData(ares_socket_t fd, struct sockaddr_storage *addr,
> > ++                          ares_socklen_t addrlen, int qid,
> > ++                          std::vector<byte> 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<byte> 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<ares_ssize_t>(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<byte> &req,
> > +                                 const std::string &reqstr,
> > +@@ -717,10 +764,14 @@ void MockServer::ProcessRequest(ares_socket_t
> fd, struct sockaddr_storage* addr,
> > +
> > +   /* 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);
> > +
> > +@@ -747,43 +798,12 @@ void MockServer::ProcessRequest(ares_socket_t
> fd, struct sockaddr_storage* addr,
> > +     return;
> > +   }
> > +
> > +-  // Make a local copy of the current pending reply.
> > +-  std::vector<byte> 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<byte> 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<ares_ssize_t>(reply.size())) {
> > +-    std::cerr << "Failed to send full reply, rc=" << rc << std::endl;
> > +-  }
> > ++  SendData(fd, addr, addrlen, qid, exact_reply_);
> > +
> > +   if (disconnect_after_reply_ && fd != udpfd_) {
> > +     disconnect_after_reply_ = false;
> > +diff --git a/test/ares-test.h b/test/ares-test.h
> > +index 2b973c3982..170c284f26 100644
> > +--- a/test/ares-test.h
> > ++++ b/test/ares-test.h
> > +@@ -303,6 +303,12 @@ class MockServer {
> > +     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<ares_socket_t> fds() const;
> > +
> > +@@ -327,6 +333,8 @@ class MockServer {
> > +                                 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<byte> reply);
> > +   unsigned short udpport_;
> > +   unsigned short tcpport_;
> > +   ares_socket_t  udpfd_;
> > +@@ -339,6 +347,10 @@ class MockServer {
> > +   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.
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-
> channel-config-under-lock.patch b/meta-oe/recipes-support/c-ares/c-ares/0006-
> ares_save_options-read-channel-config-under-lock.patch
> > new file mode 100644
> > index 0000000000..76c072dd7e
> > --- /dev/null
> > +++ b/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-
> channel-config-under-lock.patch
> > @@ -0,0 +1,243 @@
> > +From 25a6911370878d4b617b44b142ea0e49d1ccede6 Mon Sep 17
> 00:00:00 2001
> > +From: Brad House <brad@brad-house.com>
> > +Date: Mon, 19 Oct 2026 11:53:59 -0400
> > +Subject: [PATCH] [Backport v1.34] ares_save_options: read channel
> > + configuration under the channel lock (#1362)
> > +MIME-Version: 1.0
> > +Content-Type: text/plain; charset=UTF-8
> > +Content-Transfer-Encoding: 8bit
> > +
> > +Backport of 5d878a580a8b57b51d15e2725cf6775c8e7bca0e to `v1.34`. See
> > +that commit for the full description.
> > +
> > +**Conflicts:** none; cherry-picked cleanly.
> > +
> > +**Tests (local, macOS, Debug, Ninja):**
> > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > +--gtest_filter='-*Live*'` — 691/691 passed in both.
> > +- With the `src/` changes reverted, the new `DupConcurrentSetServers` /
> > +`SaveOptionsConcurrentSetServers` tests abort with an ASAN
> > +heap-buffer-overflow; they pass with them.
> > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > +passed under ASAN+UBSAN and `-Werror`.
> > +
> > +Signed-off-by: Brad House <brad@brad-house.com>
> > +Upstream-Status: Backport [https://github.com/c-ares/c-
> ares/commit/25a6911370878d4b617b44b142ea0e49d1ccede6]
> > +GHSA: GHSA-ph6c-3cvx-mq6v
> > +---
> > + src/lib/ares_init.c    |  3 +-
> > + src/lib/ares_options.c | 45 ++++++++++++++++++----
> > + test/ares-test-init.cc | 87
> ++++++++++++++++++++++++++++++++++++++++++
> > + 3 files changed, 126 insertions(+), 9 deletions(-)
> > +
> > +diff --git a/src/lib/ares_init.c b/src/lib/ares_init.c
> > +index 4758c5de56..85b2e13050 100644
> > +--- a/src/lib/ares_init.c
> > ++++ b/src/lib/ares_init.c
> > +@@ -483,7 +483,8 @@ int ares_dup(ares_channel_t **dest, const
> ares_channel_t *src)
> > +   *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);
> > +diff --git a/src/lib/ares_options.c b/src/lib/ares_options.c
> > +index 2b5624bbea..5ea35b732e 100644
> > +--- a/src/lib/ares_options.c
> > ++++ b/src/lib/ares_options.c
> > +@@ -55,8 +55,8 @@ static struct in_addr *ares_save_opt_servers(const
> ares_channel_t *channel,
> > +                                              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 @@ static struct in_addr *ares_save_opt_servers(const
> ares_channel_t *channel,
> > +     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 @@ static struct in_addr *ares_save_opt_servers(const
> ares_channel_t *channel,
> > +   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 @@ int ares_save_options(const ares_channel_t
> *channel,
> > +   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 @@ int ares_save_options(const ares_channel_t
> *channel,
> > +   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 @@ int ares_save_options(const ares_channel_t
> *channel,
> > +   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)
> > +diff --git a/test/ares-test-init.cc b/test/ares-test-init.cc
> > +index b44211a1e4..1c5d0f2171 100644
> > +--- a/test/ares-test-init.cc
> > ++++ b/test/ares-test-init.cc
> > +@@ -25,6 +25,8 @@
> > +  */
> > + #include "ares-test.h"
> > +
> > ++#include <atomic>
> > ++
> > + extern "C" {
> > +   #include "ares_private.h"
> > + }
> > +@@ -295,6 +297,91 @@ TEST_F(LibraryTest, FailChannelInit) {
> > +   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<size_t> 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<std::thread> 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;
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-
> prefix-length.patch b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-
> reject-a-prefix-length.patch
> > new file mode 100644
> > index 0000000000..2cd1ea8c24
> > --- /dev/null
> > +++ b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-
> prefix-length.patch
> > @@ -0,0 +1,364 @@
> > +From 7e65b381c0b915aa5671ddc713080c87e15fc42f Mon Sep 17 00:00:00
> 2001
> > +From: Brad House <brad@brad-house.com>
> > +Date: Mon, 19 Oct 2026 12:24:52 -0400
> > +Subject: [PATCH] [Backport v1.34] ares_inet_pton: reject a prefix length,
> > + always write the full address (#1363)
> > +MIME-Version: 1.0
> > +Content-Type: text/plain; charset=UTF-8
> > +Content-Transfer-Encoding: 8bit
> > +
> > +Backport of 7b4ef8e555b611be6af39d913aa280cd1a348102 to `v1.34`. See
> > +that commit for the full description.
> > +
> > +**Conflicts:** `test/ares-test-mock-ai.cc` only — the neighbouring
> > +`FamilyV6IgnoresARecords` test (added on main by an unrelated change)
> > +does not exist on v1.34. Resolved by adding only the new
> > +`NumericHostRejectsPrefixLength` test at the same position. Other files
> > +applied unchanged.
> > +
> > +**Also includes** the comment spelling fix from #1364
> > +(ead5557d1301328536c3920bd01759a1bff5ca9a): "unparseable" ->
> > +"unparsable" in `src/lib/inet_net_pton.c` and
> > +`test/ares-test-internal.cc`, so Codespell passes and the text matches
> > +main.
> > +
> > +**Tests (local, macOS, Debug, Ninja):**
> > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > +--gtest_filter='-*Live*'` — 693/693 passed in both.
> > +- With the `src/` changes reverted, the 4 new test instances fail
> > +(`InetNetPtoNPrefixZeroFills`, `InetPtoNRejectsPrefixLength`,
> > +`NumericHostRejectsPrefixLength` UDP/TCP); they pass with them.
> > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > +passed under ASAN+UBSAN and `-Werror`.
> > +
> > +Signed-off-by: Brad House <brad@brad-house.com>
> > +Upstream-Status: Backport [https://github.com/c-ares/c-
> ares/commit/7e65b381c0b915aa5671ddc713080c87e15fc42f]
> > +GHSA: GHSA-cfcv-8v2j-qj6r
> > +---
> > + docs/ares_inet_pton.3        | 18 ++++++++--
> > + src/lib/ares_inet_net_pton.h | 28 ++++++++++++++++
> > + src/lib/inet_net_pton.c      | 65 ++++++++++++++++++++----------------
> > + test/ares-test-internal.cc   | 54 ++++++++++++++++++++++++++++++
> > + test/ares-test-mock-ai.cc    | 23 +++++++++++++
> > + 5 files changed, 157 insertions(+), 31 deletions(-)
> > +
> > +diff --git a/docs/ares_inet_pton.3 b/docs/ares_inet_pton.3
> > +index 34b2df063c..19c5fdfda3 100644
> > +--- a/docs/ares_inet_pton.3
> > ++++ b/docs/ares_inet_pton.3
> > +@@ -12,8 +12,9 @@ ares_inet_pton \- convert an IPv4 or IPv6 address from
> text to binary form
> > + 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 @@ in. The \fBdst\fP argument points to a buffer into which
> the function stores
> > + 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 --git a/src/lib/ares_inet_net_pton.h b/src/lib/ares_inet_net_pton.h
> > +index e3ed83a3cb..e1a6f2d71e 100644
> > +--- a/src/lib/ares_inet_net_pton.h
> > ++++ b/src/lib/ares_inet_net_pton.h
> > +@@ -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 --git a/src/lib/inet_net_pton.c b/src/lib/inet_net_pton.c
> > +index e1f76ef834..f60823023b 100644
> > +--- a/src/lib/inet_net_pton.c
> > ++++ b/src/lib/inet_net_pton.c
> > +@@ -51,14 +51,15 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0,
> 0, 0, 0, 0, 0, 0, 0, 0,
> > +
> > + /*
> > +  * 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 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0, 0,
> 0, 0, 0, 0, 0, 0, 0,
> > +  *      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 @@ static int ares_inet_net_pton_ipv4(const char *src,
> unsigned char *dst,
> > +   }
> > +
> > +   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 @@ static int ares_inet_pton6(const char *src, unsigned
> char *dst)
> > +       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 @@ static int ares_inet_net_pton_ipv6(const char *src,
> unsigned char *dst,
> > +     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 ares_inet_net_pton(int af, const char *src, void
> *dst, size_t size)
> > +   }
> > + }
> > +
> > ++/*
> > ++ * 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 --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> > +index bf06a74605..294e3f4555 100644
> > +--- a/test/ares-test-internal.cc
> > ++++ b/test/ares-test-internal.cc
> > +@@ -274,6 +274,23 @@ TEST_F(LibraryTest, InetNetPtoN) {
> > +   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++) {
> > +@@ -600,9 +617,46 @@ TEST_F(LibraryTest, InetPtoN) {
> > +   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.
> > +diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc
> > +index ae45c45527..43d0b89175 100644
> > +--- a/test/ares-test-mock-ai.cc
> > ++++ b/test/ares-test-mock-ai.cc
> > +@@ -617,6 +617,29 @@ TEST_P(MockChannelTestAI, FamilyV6) {
> > +   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<byte> nothing;
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-
> drop-UDP-answers-from-server-port.patch b/meta-oe/recipes-support/c-ares/c-
> ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> > new file mode 100644
> > index 0000000000..f4f182b995
> > --- /dev/null
> > +++ b/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-
> UDP-answers-from-server-port.patch
> > @@ -0,0 +1,178 @@
> > +From c7fa9ebcb0c0086730c53bf03191c03f03706246 Mon Sep 17 00:00:00
> 2001
> > +From: "c-ares-backport-gha[bot]"
> > + <299594594+c-ares-backport-gha[bot]@users.noreply.github.com>
> > +Date: Tue, 20 Oct 2026 02:25:59 +0000
> > +Subject: [PATCH] [Backport v1.34] ares_conn_read: don't drop UDP answers
> whose
> > + source port isn't reported (#1371)
> > +MIME-Version: 1.0
> > +Content-Type: text/plain; charset=UTF-8
> > +Content-Transfer-Encoding: 8bit
> > +
> > +# Description
> > +Backport of #1370 to `v1.34`.
> > +
> > +Signed-off-by: Brad House <brad@brad-house.com>
> > +Co-authored-by: Brad House <brad@brad-house.com>
> > +Upstream-Status: Backport [https://github.com/c-ares/c-
> ares/commit/c7fa9ebcb0c0086730c53bf03191c03f03706246]
> > +GHSA: GHSA-fqh5-3x7m-wp44
> > +---
> > + docs/ares_set_socket_functions.3 | 20 ++++++++++++++++++++
> > + src/lib/ares_conn.c              |  9 ++++++++-
> > + src/lib/ares_socket.c            |  4 +++-
> > + src/lib/ares_socket.h            |  8 +++++---
> > + test/ares-test-mock.cc           | 32 +++++++++++++++++++++++++++++++-
> > + 5 files changed, 67 insertions(+), 6 deletions(-)
> > +
> > +diff --git a/docs/ares_set_socket_functions.3
> b/docs/ares_set_socket_functions.3
> > +index 605beb26ef..75c621cb48 100644
> > +--- a/docs/ares_set_socket_functions.3
> > ++++ b/docs/ares_set_socket_functions.3
> > +@@ -149,6 +149,20 @@ a socket. Additional flags controlling behavior are in
> > + 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 @@ Receives data from remote socket endpoint, if
> available. If the \fIaddr\fP
> > + 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
> > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > +index b868053e62..7711d8649e 100644
> > +--- a/src/lib/ares_conn.c
> > ++++ b/src/lib/ares_conn.c
> > +@@ -257,7 +257,14 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> *conn, void *data, size_t len,
> > +      * 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. */
> > ++     * 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_port_eq((struct sockaddr *)&sa_storage,
> > +                                     &conn->server->addr,
> > +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> > +index 0a12935ab7..29d595e1a9 100644
> > +--- a/src/lib/ares_socket.c
> > ++++ b/src/lib/ares_socket.c
> > +@@ -144,7 +144,9 @@ ares_bool_t ares_sockaddr_addr_port_eq(const
> struct sockaddr  *sa,
> > +     sa_port = (CARES_INADDR_CAST(const struct sockaddr_in6 *, sa))-
> >sin6_port;
> > +   }
> > +
> > +-  if (ntohs(sa_port) != 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;
> > +   }
> > +
> > +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> > +index f7128e3493..1ede6987ee 100644
> > +--- a/src/lib/ares_socket.h
> > ++++ b/src/lib/ares_socket.h
> > +@@ -151,13 +151,15 @@ 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.
> > ++ *  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, IP addresses and ports are
> > +- *          equal, ARES_FALSE otherwise
> > ++ *  \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,
> > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > +index 058dc93001..cc5e6482f2 100644
> > +--- a/test/ares-test-mock.cc
> > ++++ b/test/ares-test-mock.cc
> > +@@ -3364,6 +3364,7 @@ struct FakeSockData {
> > +   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;
> > +@@ -3472,7 +3473,9 @@ static ares_ssize_t
> FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> > +   if (address != NULL && address_len != NULL &&
> > +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > +     memcpy(address, &d->peer, sizeof(d->peer));
> > +-    if (d->recv_port != 0) {
> > ++    if (d->recv_no_port) {
> > ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = 0;
> > ++    } else if (d->recv_port != 0) {
> > +       reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
> > +     }
> > +     *address_len = sizeof(d->peer);
> > +@@ -3750,6 +3753,33 @@ TEST_F(LibraryTest,
> UDPAnswerFromWrongPortDropped) {
> > +   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.
> > diff --git a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb b/meta-oe/recipes-
> support/c-ares/c-ares_1.34.8.bb
> > index 058f3666e7..1d0f3eb343 100644
> > --- a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> > +++ b/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> > @@ -6,6 +6,14 @@ LICENSE = "MIT"
> >  LIC_FILES_CHKSUM =
> "file://LICENSE.md;md5=d3e72a10e08191f2ca1be3f3228d78f3"
> >
> >  SRC_URI = "https://github.com/c-ares/c-
> ares/releases/download/v${PV}/${BPN}-${PV}.tar.gz \
> > +           file://0001-ares_cancel-ares_destroy-detach-queries-before-
> callbacks.patch \
> > +           file://0002-Deliver-deferred-socket-server-state-notifications.patch \
> > +           file://0003-ares_qcache-hand-callbacks-their-own-copy.patch \
> > +           file://0004-Handle-connections-torn-down-by-application.patch \
> > +           file://0005-ares_process-only-accept-an-answer-on-the-right-
> connection.patch \
> > +           file://0006-ares_save_options-read-channel-config-under-lock.patch \
> > +           file://0007-ares_inet_pton-reject-a-prefix-length.patch \
> > +           file://0008-ares_conn_read-dont-drop-UDP-answers-from-server-
> port.patch \
> >             file://run-ptest"
> >  SRC_URI[sha256sum] =
> "c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78"
> >
> > --
> > 2.43.0
> >
> >
> >
> >
Ankur Tyagi Oct. 27, 2026, 8:45 a.m. UTC | #3
On Tue, Oct 27, 2026 at 9:25 PM Marko, Peter <Peter.Marko@siemens.com> wrote:
>
> Can we rather do the upgrade instead? I was planning to send it for Wrynose branch eventually.
> Is there anything in "Changes" section of https://github.com/c-ares/c-ares/releases/tag/v1.34.9 which would prevent it?

Hi Peter,

Jason reached out earlier in a separate email regarding the upgrade as well.

c-ares v1.34.9 brings following changes:
- 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.

And the acceptance policy doesn't allow for such an upgrade in the
release branch.

cheers
Ankur
>
> Peter
>
> > -----Original Message-----
> > From: openembedded-devel@lists.openembedded.org <openembedded-
> > devel@lists.openembedded.org> On Behalf Of Ankur Tyagi via
> > lists.openembedded.org
> > Sent: Tuesday, October 27, 2026 8:04 AM
> > To: schonm@gmail.com
> > Cc: openembedded-devel@lists.openembedded.org
> > Subject: Re: [oe] [meta-oe][blacksail][PATCH] c-ares: backport security fixes from
> > 1.34.9
> >
> > Hi Jason,
> >
> > Thanks for submitting the patch.
> >
> > Though the patches LGTM, I am seeing increased ptest failures.
> >
> > Existing recipe ptest status:
> > [==========] 1093 tests from 41 test suites ran.
> > [  PASSED  ] 1092 tests.
> > [  FAILED  ] 1 test, listed below:
> > [  FAILED  ] DefaultChannelTest.TimeoutValue
> >
> > ptest status after applying your patch:
> > [==========] 1174 tests from 47 test suites ran.
> > [  PASSED  ] 1170 tests.
> > [  FAILED  ] 4 tests, listed below:
> > [  FAILED  ] DefaultChannelTest.TimeoutValue
> > [  FAILED  ] SockStateDeliveryTest.CancelReportsSocketClose
> > [  FAILED  ] SockStateDeliveryTest.SetServersReportsSocketClose
> > [  FAILED  ] SockStateDeliveryTest.DestroyReportsSocketClose
> >
> > Perhaps some other commits also need backporting?
> > Can please look into this?
> >
> > cheers
> > Ankur
> >
> > On Tue, Oct 27, 2026 at 2:22 PM J. S. via lists.openembedded.org
> > <schonm=gmail.com@lists.openembedded.org> wrote:
> > >
> > > Add the eight unique upstream commits that fix the security advisories
> > addressed in c-ares 1.34.9.
> > > Several advisories share commits, so patches are ordered by commit rather
> > than duplicated per advisory; each patch lists the GHSA IDs it resolves.
> > >
> > > Assisted-by: opencode (big-pickle)
> > > Signed-off-by: Jason Schonberg <schonm@gmail.com>
> > > ---
> > >  ...troy-detach-queries-before-callbacks.patch | 2603 +++++++++++++++++
> > >  ...ed-socket-server-state-notifications.patch |  393 +++
> > >  ...qcache-hand-callbacks-their-own-copy.patch | 1054 +++++++
> > >  ...connections-torn-down-by-application.patch | 1107 +++++++
> > >  ...pt-an-answer-on-the-right-connection.patch |  575 ++++
> > >  ...tions-read-channel-config-under-lock.patch |  243 ++
> > >  ...res_inet_pton-reject-a-prefix-length.patch |  364 +++
> > >  ...nt-drop-UDP-answers-from-server-port.patch |  178 ++
> > >  .../recipes-support/c-ares/c-ares_1.34.8.bb   |    8 +
> > >  9 files changed, 6525 insertions(+)
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-
> > ares_destroy-detach-queries-before-callbacks.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-
> > deferred-socket-server-state-notifications.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0003-
> > ares_qcache-hand-callbacks-their-own-copy.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0004-Handle-
> > connections-torn-down-by-application.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0005-
> > ares_process-only-accept-an-answer-on-the-right-connection.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0006-
> > ares_save_options-read-channel-config-under-lock.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0007-
> > ares_inet_pton-reject-a-prefix-length.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0008-
> > ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> > >
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-
> > ares_destroy-detach-queries-before-callbacks.patch b/meta-oe/recipes-
> > support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-
> > callbacks.patch
> > > new file mode 100644
> > > index 0000000000..385760f5bc
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-
> > detach-queries-before-callbacks.patch
> > > @@ -0,0 +1,2603 @@
> > > +From 7f408deb128fbdcce78122322734ffaa01cad9ae Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Sun, 18 Oct 2026 18:15:41 -0400
> > > +Subject: [PATCH] ares_cancel/ares_destroy: detach queries before running
> > user
> > > + callbacks
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of dd62bdfca52ba5c37e88a1469c0a4c5e4a568ccb to v1.34.  See
> > that
> > > +commit for the full description; it hardens reentrant teardown from
> > > +callbacks (ares_cancel(), ares_destroy(), server reconfiguration, and
> > > +socket/server state notifications).
> > > +
> > > +Conflicts were whitespace/alignment only (v1.34 predates the clang-format
> > > +reformat in #1238) and were reformatted to v1.34's style.
> > > +
> > > +Also carries the test portion of #1334 that applies to the tests this
> > > +introduces: dead-server tests use 127.0.0.1:53055/53056 instead of
> > > +127.0.0.55/56, which only routes on Linux.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/7f408deb128fbdcce78122322734ffaa01cad9ae]
> > > +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-
> > 2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> > > +---
> > > + docs/ares_cancel.3                    |   17 +
> > > + docs/ares_destroy.3                   |    7 +
> > > + docs/ares_init_options.3              |   15 +
> > > + docs/ares_set_server_state_callback.3 |   31 +-
> > > + docs/ares_set_servers_csv.3           |   17 +
> > > + src/lib/ares_cancel.c                 |   25 +-
> > > + src/lib/ares_close_sockets.c          |   82 +-
> > > + src/lib/ares_conn.c                   |  217 ++++-
> > > + src/lib/ares_conn.h                   |   37 +-
> > > + src/lib/ares_destroy.c                |   85 +-
> > > + src/lib/ares_private.h                |  110 ++-
> > > + src/lib/ares_process.c                |  235 +++++-
> > > + src/lib/ares_send.c                   |   35 +
> > > + src/lib/ares_update_servers.c         |   42 +-
> > > + test/ares-test-mock.cc                | 1068 +++++++++++++++++++++++++
> > > + 15 files changed, 1959 insertions(+), 64 deletions(-)
> > > +
> > > +diff --git a/docs/ares_cancel.3 b/docs/ares_cancel.3
> > > +index 967a946b49..069f226c10 100644
> > > +--- a/docs/ares_cancel.3
> > > ++++ b/docs/ares_cancel.3
> > > +@@ -21,6 +21,23 @@ These calls give the callbacks a chance to clean up
> > any state which might have
> > > + 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 --git a/docs/ares_destroy.3 b/docs/ares_destroy.3
> > > +index d1984b7cc9..0b6380e609 100644
> > > +--- a/docs/ares_destroy.3
> > > ++++ b/docs/ares_destroy.3
> > > +@@ -20,6 +20,13 @@ by the channel.
> > > + 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 --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> > > +index 9b3b481535..e6f6ee8ba6 100644
> > > +--- a/docs/ares_init_options.3
> > > ++++ b/docs/ares_init_options.3
> > > +@@ -231,6 +231,21 @@ will be passed the socket whose state has changed;
> > > + 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
> > > +diff --git a/docs/ares_set_server_state_callback.3
> > b/docs/ares_set_server_state_callback.3
> > > +index 04fbde4c7a..e95db327ce 100644
> > > +--- a/docs/ares_set_server_state_callback.3
> > > ++++ b/docs/ares_set_server_state_callback.3
> > > +@@ -47,6 +47,31 @@ the DNS servers used by an ares channel. For
> > example, it can be used to track
> > > + 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 @@ Indicates that the query was tried over TCP.
> > > + .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 --git a/docs/ares_set_servers_csv.3 b/docs/ares_set_servers_csv.3
> > > +index f1435143f5..f9f5b82e28 100644
> > > +--- a/docs/ares_set_servers_csv.3
> > > ++++ b/docs/ares_set_servers_csv.3
> > > +@@ -126,6 +126,23 @@ are identical.  Prior versions would simply omit ports
> > in \fBares_set_servers_cs
> > > + 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 --git a/src/lib/ares_cancel.c b/src/lib/ares_cancel.c
> > > +index 75600dea6b..aa167e4486 100644
> > > +--- a/src/lib/ares_cancel.c
> > > ++++ b/src/lib/ares_cancel.c
> > > +@@ -38,10 +38,10 @@ void ares_cancel(ares_channel_t *channel)
> > > +   }
> > > +
> > > +   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 @@ void ares_cancel(ares_channel_t *channel)
> > > +       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 @@ void ares_cancel(ares_channel_t *channel)
> > > +   ares_queue_notify_empty(channel);
> > > +
> > > + done:
> > > ++  ares_deferred_cb_leave(channel);
> > > +   ares_channel_unlock(channel);
> > > + }
> > > +diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
> > > +index 7bb72c31ec..6be662fdd3 100644
> > > +--- a/src/lib/ares_close_sockets.c
> > > ++++ b/src/lib/ares_close_sockets.c
> > > +@@ -41,12 +41,47 @@ static void ares_requeue_queries(ares_conn_t
> > *conn,
> > > +   }
> > > + }
> > > +
> > > ++/*! 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;
> > > ++
> > > ++  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;
> > > ++  }
> > > +
> > > +-  /* Unlink */
> > > ++  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 @@ void ares_close_connection(ares_conn_t *conn,
> > ares_status_t requeue_status)
> > > +     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);
> > > +-
> > > +-  ares_socket_close(channel, conn->fd);
> > > ++  /* 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;
> > > ++
> > > ++  /* 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 @@ void ares_close_sockets(ares_server_t *server)
> > > +
> > > +   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);
> > > +   }
> > > + }
> > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index 6b315b0548..78f57e7d10 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -26,19 +26,210 @@
> > > +  */
> > > + #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 ((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);
> > > ++    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.
> > > ++   *
> > > ++   * 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;
> > > ++
> > > ++  /* 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;
> > > + }
> > > +
> > > +@@ -435,6 +626,16 @@ ares_status_t ares_open_connection(ares_conn_t
> > **conn_out,
> > > +     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) {
> > > +diff --git a/src/lib/ares_conn.h b/src/lib/ares_conn.h
> > > +index c332de14b1..fc177b84f6 100644
> > > +--- a/src/lib/ares_conn.h
> > > ++++ b/src/lib/ares_conn.h
> > > +@@ -49,7 +49,14 @@ typedef enum {
> > > +    *  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
> > > ++  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 {
> > > +@@ -84,6 +91,10 @@ struct ares_conn {
> > > +
> > > +   /* 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 */
> > > +@@ -168,6 +179,10 @@ struct ares_server {
> > > +
> > > +   /* 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);
> > > +@@ -185,8 +200,24 @@ ares_status_t   ares_conn_flush(ares_conn_t
> > *conn);
> > > + 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 --git a/src/lib/ares_destroy.c b/src/lib/ares_destroy.c
> > > +index 1e5706e0e0..e0534c800d 100644
> > > +--- a/src/lib/ares_destroy.c
> > > ++++ b/src/lib/ares_destroy.c
> > > +@@ -69,17 +69,31 @@ void ares_destroy(ares_channel_t *channel)
> > > +    * 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 @@ void ares_destroy(ares_channel_t *channel)
> > > +   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 @@ void ares_destroy(ares_channel_t *channel)
> > > +   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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > +index 04d4576ed2..0a1029e0f8 100644
> > > +--- a/src/lib/ares_private.h
> > > ++++ b/src/lib/ares_private.h
> > > +@@ -147,7 +147,10 @@ typedef struct ares_query ares_query_t;
> > > + 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;
> > > +
> > > +@@ -227,6 +230,9 @@ struct ares_channeldata {
> > > +   /* 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
> > > +    */
> > > +@@ -237,6 +243,10 @@ struct ares_channeldata {
> > > +   /* 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;
> > > +
> > > +@@ -248,6 +258,29 @@ struct ares_channeldata {
> > > +
> > > +   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;
> > > +@@ -295,6 +328,10 @@ struct ares_channeldata {
> > > +   /* 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
> > > +@@ -308,6 +345,11 @@ struct ares_channeldata {
> > > +    * 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. */
> > > +@@ -351,6 +393,16 @@ void         *ares_dnsrec_convert_arg(ares_callback
> > callback, void *arg);
> > > + 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);
> > > +@@ -390,7 +442,6 @@ ares_status_t
> > ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
> > > +
> > > + 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);
> > > +@@ -589,6 +640,60 @@ ares_status_t ares_dns_name_write(ares_buf_t
> > *buf, ares_llist_t **list,
> > > +  */
> > > + 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.
> > > ++ *
> > > ++ *  \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)
> > > +@@ -636,7 +741,6 @@ typedef struct ares_event_thread
> > ares_event_thread_t;
> > > + 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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index b2316c4b33..82f53610fb 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -69,7 +69,6 @@ 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_detach_query(ares_query_t *query);
> > > +
> > > + static void        ares_query_remove_from_conn(ares_query_t *query)
> > > + {
> > > +@@ -81,14 +80,32 @@ static void
> > ares_query_remove_from_conn(ares_query_t *query)
> > > +   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;
> > > +@@ -111,9 +128,57 @@ static void invoke_server_state_cb(const
> > ares_server_t *server,
> > > +     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,
> > > +@@ -204,6 +269,8 @@ static ares_status_t
> > ares_process_fds_nolock(ares_channel_t         *channel,
> > > +     return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
> > > +   }
> > > +
> > > ++  ares_deferred_cb_enter(channel);
> > > ++
> > > +   ares_tvnow(&now);
> > > +
> > > +   /* Process write events */
> > > +@@ -244,6 +311,8 @@ static ares_status_t
> > ares_process_fds_nolock(ares_channel_t         *channel,
> > > +   }
> > > +
> > > + done:
> > > ++  ares_deferred_cb_leave(channel);
> > > ++
> > > +   if (status == ARES_ENOMEM) {
> > > +     return ARES_ENOMEM;
> > > +   }
> > > +@@ -553,6 +622,8 @@ typedef enum {
> > > + typedef struct {
> > > +   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 */
> > > +@@ -578,6 +649,7 @@ static ares_status_t
> > ares_append_requeue_int(ares_array_t     **requeue,
> > > +
> > > +   entry.type   = type;
> > > +   entry.qid    = query->qid;
> > > ++  entry.serial = query->serial;
> > > +   entry.server = server;
> > > +   entry.status = status;
> > > +   entry.dnsrec = dnsrec;
> > > +@@ -601,6 +673,53 @@ static ares_status_t
> > ares_append_endqueue(ares_array_t     **requeue,
> > > +     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:
> > > +@@ -636,11 +755,35 @@ static ares_status_t
> > ares_flush_requeue(ares_channel_t       *channel,
> > > +
> > > +     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. */
> > > +@@ -1377,6 +1520,7 @@ ares_status_t ares_send_query(ares_server_t
> > *requested_server,
> > > +   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);
> > > +@@ -1393,8 +1537,7 @@ ares_status_t ares_send_query(ares_server_t
> > *requested_server,
> > > +    * 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_htable_szvp_get_direct(channel->queries_by_qid, qid) == NULL) {
> > > ++  if (status == ARES_SUCCESS && !ares_query_is_live(channel, qid, serial))
> > {
> > > +     status = ARES_ETIMEOUT;
> > > +   }
> > > +
> > > +@@ -1412,6 +1555,9 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +   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) {
> > > +@@ -1443,6 +1589,32 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +   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:
> > > +@@ -1464,6 +1636,14 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +
> > > +   /* 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;
> > > ++  }
> > > ++
> > > +   switch (status) {
> > > +     /* Good result, continue on */
> > > +     case ARES_SUCCESS:
> > > +@@ -1479,6 +1659,17 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +     case ARES_ECONNREFUSED:
> > > +     case ARES_EBADFAMILY:
> > > +       handle_conn_error(conn, ARES_TRUE, status);
> > > ++      /* 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;
> > > +@@ -1597,11 +1788,18 @@ static ares_bool_t same_questions(const
> > ares_query_t      *query,
> > > +   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;
> > > + }
> > > +@@ -1624,6 +1822,16 @@ static void end_query(ares_channel_t *channel,
> > ares_server_t *server,
> > > +     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);
> > > +@@ -1631,7 +1839,8 @@ static void end_query(ares_channel_t *channel,
> > ares_server_t *server,
> > > +   /* 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);
> > > + }
> > > +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > +index 6efa9580b2..2e0482bf19 100644
> > > +--- a/src/lib/ares_send.c
> > > ++++ b/src/lib/ares_send.c
> > > +@@ -117,6 +117,27 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +   unsigned short           id          = generate_unique_qid(channel);
> > > +   const ares_dns_record_t *dnsrec_resp = 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);
> > > +
> > > +   if (ares_slist_len(channel->servers) == 0) {
> > > +@@ -145,6 +166,7 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +
> > > +   query->channel      = channel;
> > > +   query->qid          = id;
> > > ++  query->serial       = ++channel->query_serial;
> > > +   query->timeout.sec  = 0;
> > > +   query->timeout.usec = 0;
> > > +   query->using_tcp =
> > > +@@ -209,6 +231,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +    */
> > > +   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 +245,17 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +
> > > +   /* Perform the first query action. */
> > > +
> > > ++  /* Sending opens and writes to connections and can mark a server down,
> > both
> > > ++   * of which notify.  Deliver those once we are done with the connection and
> > > ++   * the server, not part-way through. */
> > > ++  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;
> > > + }
> > > +
> > > +diff --git a/src/lib/ares_update_servers.c b/src/lib/ares_update_servers.c
> > > +index f8d441accf..50a9c27925 100644
> > > +--- a/src/lib/ares_update_servers.c
> > > ++++ b/src/lib/ares_update_servers.c
> > > +@@ -740,17 +740,36 @@ static ares_bool_t
> > ares_servers_remove_stale(ares_channel_t *channel,
> > > + {
> > > +   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;
> > > + }
> > > +@@ -775,6 +794,14 @@ ares_status_t ares_servers_update(ares_channel_t
> > *channel,
> > > +     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
> > > +    */
> > > +@@ -843,6 +870,7 @@ ares_status_t ares_servers_update(ares_channel_t
> > *channel,
> > > +   status = ARES_SUCCESS;
> > > +
> > > + done:
> > > ++  ares_deferred_cb_leave(channel);
> > > +   return status;
> > > + }
> > > +
> > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index c0c0279abb..2158c5a913 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -31,6 +31,11 @@
> > > + #include <sys/stat.h>
> > > + #endif
> > > +
> > > ++#ifndef _WIN32
> > > ++#  include <errno.h>
> > > ++#  include <fcntl.h>
> > > ++#endif
> > > ++
> > > + #include <sstream>
> > > + #include <vector>
> > > +
> > > +@@ -1914,6 +1919,7 @@ TEST_P(MockRetryDepthChannelTest,
> > HighRetryNoStackOverflow) {
> > > + struct CancelInCbData {
> > > +   ares_channel_t *channel;
> > > +   bool            done;
> > > ++  size_t          cbs;
> > > + };
> > > +
> > > + static void CancelChannelCallback(void *arg, ares_status_t status,
> > > +@@ -1925,10 +1931,1071 @@ static void CancelChannelCallback(void *arg,
> > ares_status_t 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<int> {
> > > ++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<CancelInCbData *>(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<CancelInCbData *>(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<DestroyReentryData *>(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<DestroyReentryData *>(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);
> > > ++
> > > ++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<byte> 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<SockStateCancelData *>(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<SockStateCloseOrderData
> > *>(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<int>(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<ReconfigureFromHookData
> > *>(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<ReconfigureFromHookData
> > *>(arg);
> > > ++  (void)timeouts;
> > > ++  (void)dnsrec;
> > > ++  d->done   = true;
> > > ++  d->status = static_cast<int>(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);
> > > ++}
> > > ++
> > > ++// 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<SockStateReconfigureData *>(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);
> > > ++}
> > > ++
> > > ++// 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<std::pair<ares_socket_t, bool>> events; /* fd, still-armed */
> > > ++};
> > > ++
> > > ++static void SockStateRecordCallback(void *data, ares_socket_t socket_fd,
> > > ++                                    int readable, int writable)
> > > ++{
> > > ++  SockStateRecordData *d = static_cast<SockStateRecordData *>(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<ServerStateCancelData *>(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<ServerStateCancelData *>(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<int> {
> > > ++ 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<ServerStateUnregisterData *>(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<ServerStateUnregisterData *>(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<ReconfigureInCbData *>(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<ares_socket_t> 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<NestedReconfigureData
> > *>(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<NestedReconfigureData
> > *>(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()
> > > +@@ -1940,6 +3007,7 @@ TEST_P(MockUDPChannelTest,
> > CancelInCallbackNoDoubleFree) {
> > > +   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();
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-
> > server-state-notifications.patch b/meta-oe/recipes-support/c-ares/c-ares/0002-
> > Deliver-deferred-socket-server-state-notifications.patch
> > > new file mode 100644
> > > index 0000000000..c34d3ab7c7
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-
> > server-state-notifications.patch
> > > @@ -0,0 +1,393 @@
> > > +From f944c4718fd672c9c0befd65a340abb2ccfc1a31 Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Sun, 18 Oct 2026 20:28:11 -0400
> > > +Subject: [PATCH] Deliver deferred socket/server-state notifications only from
> > > + the outermost public call (#1340)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of f023a52a (#1340) to v1.34.
> > > +
> > > +The recent change that queues socket-state and server-state
> > > +notifications delivers them, and runs the deferred cleanup, only from
> > > +the outermost `ares_deferred_cb_leave()`. `ares_send_nolock()` brackets
> > > +`ares_send_query()` itself, but most public lookup functions never
> > > +opened a region of their own. When reached from `ares_getaddrinfo()`,
> > > +`ares_query_dnsrec()` and similar, the bracket inside
> > > +`ares_send_nolock()` was therefore the outermost one. Notifications were
> > > +then delivered from inside an internal call while the library was still
> > > +in the middle of the public call, and its callers were still using their
> > > +lookup state (the host query, the query id out-pointer, ...). A
> > > +server-state callback that reconfigures the servers, which
> > > +`ares_set_server_state_callback(3)` documents as allowed, could end that
> > > +lookup before the caller was done with it.
> > > +
> > > +This is a regression in unreleased code. No released version is
> > > +affected.
> > > +
> > > +The outermost region is now opened by the public entry points
> > > +themselves, under the channel lock:
> > > +
> > > +- `ares_send_dnsrec()` (also used by `ares_send()`)
> > > +- `ares_query_dnsrec()` (also used by `ares_query()`)
> > > +- `ares_search()`, `ares_search_dnsrec()`
> > > +- `ares_getaddrinfo()` (also used by `ares_gethostbyname()`)
> > > +- `ares_gethostbyaddr()`, `ares_getnameinfo()`
> > > +- `ares_process_pending_write()`: a failed flush closes the connection
> > > +and fails its queries over, and this function had no bracket at all.
> > > +
> > > +`ares_process_fds()` (and through it
> > > +`ares_process()`/`ares_process_fd()`), `ares_cancel()`, `ares_destroy()`
> > > +and the server configuration functions already had their own. The nested
> > > +bracket in `ares_send_nolock()` stays, because it keeps connections and
> > > +servers unlinked during the send readable until the send returns.
> > > +Internal sends made from query callbacks (search next domain,
> > > +getaddrinfo next lookup, requeue, server probes) already run inside the
> > > +processing bracket.
> > > +
> > > +The comments on `ares_deferred_cb_enter()` and the flush now state the
> > > +rule: the outermost region belongs to the public entry point, never to
> > > +an internal helper.
> > > +
> > > +The documented behaviour (notifications arrive before the outermost
> > > +c-ares call returns) does not change, so no man page changes.
> > > +
> > > +Two new `LibraryTest` cases in `test/ares-test-mock.cc`. The IPv6 server
> > > +cannot get a socket, so the first send fails over to the IPv4 server,
> > > +and a server-state callback then reconfigures the servers:
> > > +
> > > +- `ServerFailoverDuringGetAddrInfo`: the lookup completes cleanly. It
> > > +fails under ASAN without this change.
> > > +- `ServerFailoverDuringQuery`: the query id from `ares_query_dnsrec()`
> > > +is already reported by the time the query callback runs. It fails
> > > +without this change.
> > > +
> > > +The full non-live suite passes under ASAN/UBSAN and with
> > > +`-DCARES_WERROR=ON`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/f944c4718fd672c9c0befd65a340abb2ccfc1a31]
> > > +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-
> > 2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> > > +---
> > > + src/lib/ares_conn.c          |   4 +-
> > > + src/lib/ares_getaddrinfo.c   |   5 ++
> > > + src/lib/ares_gethostbyaddr.c |   5 ++
> > > + src/lib/ares_getnameinfo.c   |   5 ++
> > > + src/lib/ares_private.h       |  10 ++++
> > > + src/lib/ares_process.c       |   6 ++
> > > + src/lib/ares_query.c         |   5 ++
> > > + src/lib/ares_search.c        |  10 ++++
> > > + src/lib/ares_send.c          |  13 ++++-
> > > + test/ares-test-mock.cc       | 106 +++++++++++++++++++++++++++++++++++
> > > + 10 files changed, 166 insertions(+), 3 deletions(-)
> > > +
> > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index 78f57e7d10..4cc0d1871d 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -94,7 +94,9 @@ 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.
> > > ++   * 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
> > > +diff --git a/src/lib/ares_getaddrinfo.c b/src/lib/ares_getaddrinfo.c
> > > +index 6d9e21af4b..5d9ddb311e 100644
> > > +--- a/src/lib/ares_getaddrinfo.c
> > > ++++ b/src/lib/ares_getaddrinfo.c
> > > +@@ -762,7 +762,12 @@ void ares_getaddrinfo(ares_channel_t *channel,
> > const char *name,
> > > +     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 --git a/src/lib/ares_gethostbyaddr.c b/src/lib/ares_gethostbyaddr.c
> > > +index 69c509ab11..b314359331 100644
> > > +--- a/src/lib/ares_gethostbyaddr.c
> > > ++++ b/src/lib/ares_gethostbyaddr.c
> > > +@@ -112,7 +112,12 @@ void ares_gethostbyaddr(ares_channel_t *channel,
> > const void *addr, int addrlen,
> > > +     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 --git a/src/lib/ares_getnameinfo.c b/src/lib/ares_getnameinfo.c
> > > +index 0159354530..7365e9ed82 100644
> > > +--- a/src/lib/ares_getnameinfo.c
> > > ++++ b/src/lib/ares_getnameinfo.c
> > > +@@ -194,7 +194,12 @@ void ares_getnameinfo(ares_channel_t *channel,
> > const struct sockaddr *sa,
> > > +   }
> > > +
> > > +   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);
> > > + }
> > > +
> > > +diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > +index 0a1029e0f8..9610ba7dab 100644
> > > +--- a/src/lib/ares_private.h
> > > ++++ b/src/lib/ares_private.h
> > > +@@ -676,6 +676,16 @@ void          ares_server_reap(ares_server_t *server);
> > > +  *  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);
> > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index 82f53610fb..1569408b1b 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -490,6 +490,11 @@ void ares_process_pending_write(ares_channel_t
> > *channel)
> > > +     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.
> > > +@@ -513,6 +518,7 @@ void ares_process_pending_write(ares_channel_t
> > *channel)
> > > +     }
> > > +   }
> > > +
> > > ++  ares_deferred_cb_leave(channel);
> > > +   ares_channel_unlock(channel);
> > > + }
> > > +
> > > +diff --git a/src/lib/ares_query.c b/src/lib/ares_query.c
> > > +index ca3b6a9b73..0cf7f70760 100644
> > > +--- a/src/lib/ares_query.c
> > > ++++ b/src/lib/ares_query.c
> > > +@@ -125,7 +125,12 @@ ares_status_t ares_query_dnsrec(ares_channel_t
> > *channel, const char *name,
> > > +   }
> > > +
> > > +   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 --git a/src/lib/ares_search.c b/src/lib/ares_search.c
> > > +index c605caf42c..ebc3d91e20 100644
> > > +--- a/src/lib/ares_search.c
> > > ++++ b/src/lib/ares_search.c
> > > +@@ -463,7 +463,12 @@ void ares_search(ares_channel_t *channel, const
> > char *name, int dnsclass,
> > > +   }
> > > +
> > > +   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_status_t ares_search_dnsrec(ares_channel_t
> > *channel,
> > > +   }
> > > +
> > > +   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 --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > +index 2e0482bf19..dd4a28544b 100644
> > > +--- a/src/lib/ares_send.c
> > > ++++ b/src/lib/ares_send.c
> > > +@@ -246,8 +246,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +   /* Perform the first query action. */
> > > +
> > > +   /* Sending opens and writes to connections and can mark a server down,
> > both
> > > +-   * of which notify.  Deliver those once we are done with the connection and
> > > +-   * the server, not part-way through. */
> > > ++   * 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);
> > > +@@ -271,9 +275,14 @@ ares_status_t ares_send_dnsrec(ares_channel_t
> > *channel,
> > > +   }
> > > +
> > > +   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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index 2158c5a913..b848c02434 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -2533,6 +2533,112 @@ TEST_F(LibraryTest,
> > ReconfigureInSockStateCallbackNoUseAfterFree) {
> > > +   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<ares_channel_t *>(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<FailoverQueryData *>(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
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-
> > callbacks-their-own-copy.patch b/meta-oe/recipes-support/c-ares/c-ares/0003-
> > ares_qcache-hand-callbacks-their-own-copy.patch
> > > new file mode 100644
> > > index 0000000000..02039e92e0
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-
> > callbacks-their-own-copy.patch
> > > @@ -0,0 +1,1054 @@
> > > +From a21160045f9d7039225fa285be7cb1b02173a3a5 Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Sun, 18 Oct 2026 22:12:09 -0400
> > > +Subject: [PATCH] [Backport v1.34] ares_qcache: hand callbacks their own
> > copy
> > > + of a cached answer (#1339)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of 31e2f51e8715d418fa8272a6c3a1ee651917fec5 (`ares_qcache:
> > hand
> > > +callbacks their own copy of a cached answer`) to `v1.34`. See that
> > > +commit for the full description.
> > > +
> > > +**Conflicts:**
> > > +- `ares_private.h`: alignment only; formatted with `v1.34`'s
> > > +`.clang-format`.
> > > +- `test/ares-test-internal.cc`: context only — on `main` the new
> > > +`QcacheKeyDistinguishesQuestions` test follows the IDNA/punycode tests
> > > +(#1031), which are not on `v1.34`, so it is placed after the URI test
> > > +instead.
> > > +
> > > +**Test fixes from #1334:** carries the part of #1334 that applies to the
> > > +tests this introduces — `ReentrantFlushInCallbackNoUseAfterFree`
> > > +reconfigures to `127.0.0.1:53055` instead of `127.0.0.55` (only routable
> > > +on Linux), and `UnknownQueryTypesDoNotShareCacheEntries` drops
> > > +`static_cast<ns_type>`, which does not compile on OpenBSD.
> > > +
> > > +Test file contents match `main`. Independent of the dd62bdfc backport
> > > +(#1338); applies cleanly with or without it.
> > > +
> > > +The full non-live suite passes 650/650 under ASAN+UBSAN and with
> > > +`-DCARES_WERROR=ON` on macOS.
> > `ReentrantFlushInCallbackNoUseAfterFree`
> > > +is a heap-use-after-free in `ares_dns_record_rr_cnt()` on `v1.34`
> > > +without this change, and passes with it.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/a21160045f9d7039225fa285be7cb1b02173a3a5]
> > > +GHSA: GHSA-grv7-fq5q-9vfc, GHSA-wrgc-prmw-27xh
> > > +---
> > > + docs/ares_init_options.3   |  11 +-
> > > + src/lib/ares_private.h     |  44 +++-
> > > + src/lib/ares_process.c     |   1 +
> > > + src/lib/ares_qcache.c      | 212 ++++++++++++++++---
> > > + src/lib/ares_send.c        |  47 ++++-
> > > + test/ares-test-internal.cc |  54 +++++
> > > + test/ares-test-mock.cc     | 419 +++++++++++++++++++++++++++++++++++++
> > > + 7 files changed, 743 insertions(+), 45 deletions(-)
> > > +
> > > +diff --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> > > +index e6f6ee8ba6..1ce7fb8e30 100644
> > > +--- a/docs/ares_init_options.3
> > > ++++ b/docs/ares_init_options.3
> > > +@@ -324,7 +324,16 @@ with the maximum number of seconds a query result
> > may be cached which will
> > > + 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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > +index 9610ba7dab..dd06d303b8 100644
> > > +--- a/src/lib/ares_private.h
> > > ++++ b/src/lib/ares_private.h
> > > +@@ -153,6 +153,9 @@ struct ares_query {
> > > +   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
> > > +@@ -718,14 +721,47 @@ 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);
> > > ++/*! 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);
> > > +-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);
> > > ++/*! 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);
> > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index 1569408b1b..88536704ad 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -1859,6 +1859,7 @@ void ares_free_query(ares_query_t *query)
> > > +   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 --git a/src/lib/ares_qcache.c b/src/lib/ares_qcache.c
> > > +index ad3bc83505..f48669b56c 100644
> > > +--- a/src/lib/ares_qcache.c
> > > ++++ b/src/lib/ares_qcache.c
> > > +@@ -38,7 +38,7 @@ typedef struct {
> > > +   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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +     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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +   }
> > > +
> > > +   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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +       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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +       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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +       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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +       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 @@ static unsigned int
> > ares_qcache_soa_minimum(ares_dns_record_t *dnsrec)
> > > + }
> > > +
> > > + /* 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 @@ static ares_status_t
> > ares_qcache_insert_int(ares_qcache_t           *qcache,
> > > +   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 */
> > > +   }
> > > +@@ -382,31 +514,25 @@ static ares_status_t
> > ares_qcache_insert_int(ares_qcache_t           *qcache,
> > > +   /* 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;
> > > +@@ -416,10 +542,24 @@ ares_status_t ares_qcache_fetch(ares_channel_t
> > *channel,
> > > +   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;
> > > + }
> > > +
> > > +@@ -428,13 +568,25 @@ ares_status_t ares_qcache_insert(ares_channel_t
> > *channel,
> > > +                                  const ares_query_t      *query,
> > > +                                  const ares_dns_record_t *dnsrec)
> > > + {
> > > +-  ares_dns_record_t *dupdns = ares_dns_record_duplicate(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->query,
> > now);
> > > ++
> > > ++  status =
> > > ++    ares_qcache_insert_int(channel->qcache, dupdns, query->qcache_key,
> > now);
> > > +   if (status != ARES_SUCCESS) {
> > > +     ares_dns_record_destroy(dupdns);
> > > +   }
> > > +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > +index dd4a28544b..117745ce28 100644
> > > +--- a/src/lib/ares_send.c
> > > ++++ b/src/lib/ares_send.c
> > > +@@ -111,11 +111,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +                                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
> > > +@@ -145,13 +146,28 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +     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;
> > > +     }
> > > +   }
> > > +@@ -159,8 +175,11 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +   /* 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));
> > > +
> > > +@@ -173,6 +192,10 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +     (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
> > > +@@ -180,7 +203,11 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +     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;
> > > +   }
> > > +diff --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> > > +index 8881f3b7cc..5ba5d2b39b 100644
> > > +--- a/test/ares-test-internal.cc
> > > ++++ b/test/ares-test-internal.cc
> > > +@@ -502,6 +502,60 @@ TEST_F(LibraryTest, URI) {
> > > +   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<std::string> 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];
> > > ++    }
> > > ++  }
> > > ++}
> > > ++
> > > + #endif /* !CARES_SYMBOL_HIDING */
> > > +
> > > + TEST_F(LibraryTest, InetPtoN) {
> > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index b848c02434..4b409db7c3 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -611,6 +611,425 @@ class CacheQueriesTest
> > > +   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<QcacheReentrantCbData
> > *>(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<ares_dns_rec_type_t>(29), QueryCallback, &r1,
> > > ++                    nullptr);
> > > ++  Process();
> > > ++  EXPECT_TRUE(r1.done_);
> > > ++
> > > ++  QueryResult r2;
> > > ++  ares_query_dnsrec(channel_, "example.net", ARES_CLASS_IN,
> > > ++                    static_cast<ares_dns_rec_type_t>(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()
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-
> > torn-down-by-application.patch b/meta-oe/recipes-support/c-ares/c-ares/0004-
> > Handle-connections-torn-down-by-application.patch
> > > new file mode 100644
> > > index 0000000000..f78e9726b0
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-
> > down-by-application.patch
> > > @@ -0,0 +1,1107 @@
> > > +From 13a10dddbabc30b6fca8e23f9efff73fedba9401 Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Sun, 18 Oct 2026 22:51:53 -0400
> > > +Subject: [PATCH] [Backport v1.34] Handle connections torn down by
> > application
> > > + socket functions (#1343)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of cb25d8bf297aed0aad8f6e0777ca0635cacc6cb1 to `v1.34`. See
> > > +that commit for the full description.
> > > +
> > > +Application code that c-ares calls while using a connection can
> > > +reconfigure the servers and so tear that connection down. That includes
> > > +the socket functions from `ares_set_socket_functions_ex()` and the older
> > > +`ares_set_socket_functions()`, and the pending-write callback. c-ares
> > > +now notices this while opening, writing to, reading from or closing a
> > > +connection. It requeues the affected queries, or drops the stale data,
> > > +instead of continuing to use the connection.
> > > +
> > > +It applied cleanly on top of the dd62bdfc and #1340 backports (#1338).
> > > +Test file contents match `main`.
> > > +
> > > +Each of the 11 new tests fails on `v1.34` without this change: nine with
> > > +an ASAN heap-use-after-free, and the two receive tests on their
> > > +assertions. All 11 pass with it. The full non-live suite passes 674/674
> > > +under ASAN+UBSAN and with `-DCARES_WERROR=ON` on macOS.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Co-authored-by: Mar <mardgez13@gmail.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/13a10dddbabc30b6fca8e23f9efff73fedba9401]
> > > +GHSA: GHSA-pj7p-v3cw-hq66
> > > +---
> > > + src/lib/ares_close_sockets.c |   7 +-
> > > + src/lib/ares_conn.c          |  29 ++
> > > + src/lib/ares_process.c       |  65 ++-
> > > + test/ares-test-mock.cc       | 842 +++++++++++++++++++++++++++++++++++
> > > + 4 files changed, 935 insertions(+), 8 deletions(-)
> > > +
> > > +diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
> > > +index 6be662fdd3..a3af721bc6 100644
> > > +--- a/src/lib/ares_close_sockets.c
> > > ++++ b/src/lib/ares_close_sockets.c
> > > +@@ -148,6 +148,7 @@ void ares_check_cleanup_conns(const
> > ares_channel_t *channel)
> > > +     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)) {
> > > +@@ -190,8 +191,12 @@ void ares_check_cleanup_conns(const
> > ares_channel_t *channel)
> > > +         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 --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index 4cc0d1871d..5e0827fbdf 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -262,6 +262,15 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > *conn, void *data, size_t len,
> > > +     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;
> > > +@@ -408,6 +417,19 @@ ares_conn_err_t ares_conn_write(ares_conn_t
> > *conn, const void *data, size_t len,
> > > +   }
> > > +
> > > + 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
> > > +@@ -644,6 +666,13 @@ ares_status_t ares_open_connection(ares_conn_t
> > **conn_out,
> > > +     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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index 88536704ad..5686b077a6 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -189,7 +189,9 @@ static void server_increment_failures(ares_server_t
> > *server,
> > > +   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 */
> > > +   }
> > > +
> > > +@@ -220,7 +222,9 @@ static void server_set_good(ares_server_t *server,
> > ares_bool_t used_tcp)
> > > +   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 */
> > > +   }
> > > +
> > > +@@ -501,21 +505,39 @@ void ares_process_pending_write(ares_channel_t
> > *channel)
> > > +    */
> > > +   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);
> > > +@@ -902,6 +924,13 @@ static ares_status_t process_read(ares_channel_t
> > *channel,
> > > +     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;
> > > +@@ -1192,6 +1221,15 @@ static void handle_conn_error(ares_conn_t *conn,
> > ares_bool_t critical_failure,
> > > + {
> > > +   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) {
> > > +@@ -1508,6 +1546,14 @@ static ares_status_t
> > ares_conn_query_write(ares_conn_t          *conn,
> > > +       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;
> > > +   }
> > > +
> > > +@@ -1650,6 +1696,11 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +     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:
> > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index 4b409db7c3..f1d9d360e7 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -2593,6 +2593,86 @@ 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<int> {
> > > ++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<bool *>(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_;
> > > +@@ -2898,6 +2978,768 @@ TEST_F(LibraryTest,
> > ReconfigureInConfigureHookNoUseAfterFree) {
> > > +   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<ReconfigureFromSendData *>(user_data);
> > > ++  ares_ssize_t len = 0;
> > > ++  (void)fd;
> > > ++  for (int i = 0; i < iovcnt; i++) {
> > > ++    len += static_cast<ares_ssize_t>(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<ReconfigureFromSendData
> > *>(arg);
> > > ++  (void)timeouts;
> > > ++  (void)dnsrec;
> > > ++  d->done   = true;
> > > ++  d->status = static_cast<int>(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<BadSocketStateData *>(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<PendingWriteReconfigureData *>(user_data);
> > > ++  ares_ssize_t len = 0;
> > > ++  (void)fd;
> > > ++  for (int i = 0; i < iovcnt; i++) {
> > > ++    len += static_cast<ares_ssize_t>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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;
> > > ++  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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<unsigned char *>(buffer)[2] |= 0x80;
> > > ++  if (address != NULL && address_len != NULL &&
> > > ++      *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > ++    memcpy(address, &d->peer, sizeof(d->peer));
> > > ++    *address_len = sizeof(d->peer);
> > > ++  }
> > > ++  return static_cast<ares_ssize_t>(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<FakeSockData *>(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<ares_ssize_t>(length);
> > > ++}
> > > ++
> > > ++static int FakeSockGetsockname(ares_socket_t sock, struct sockaddr
> > *address,
> > > ++                               ares_socklen_t *address_len, void *user_data)
> > > ++{
> > > ++  FakeSockData      *d = static_cast<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(arg);
> > > ++  (void)timeouts;
> > > ++  (void)dnsrec;
> > > ++  d->done   = true;
> > > ++  d->status = static_cast<int>(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);
> > > ++}
> > > ++
> > > ++// 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
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-
> > accept-an-answer-on-the-right-connection.patch b/meta-oe/recipes-support/c-
> > ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-
> > connection.patch
> > > new file mode 100644
> > > index 0000000000..20fef5d2f0
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-
> > an-answer-on-the-right-connection.patch
> > > @@ -0,0 +1,575 @@
> > > +From c8bd9ab53329f66ec0c395a11002a5696669b147 Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Mon, 19 Oct 2026 11:53:54 -0400
> > > +Subject: [PATCH] [Backport v1.34] ares_process: only accept an answer on the
> > > + connection its query was sent on (#1361)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of b6f8a9b47e17c681e092a74d05d73bbb35c89a39 to `v1.34`. See
> > > +that commit for the full description.
> > > +
> > > +**Conflicts:** `src/lib/ares_socket.h` only — v1.34 aligns the
> > > +neighbouring prototypes differently. Resolved by taking the new doc
> > > +comments and the `ares_sockaddr_addr_port_eq()` prototype, then
> > > +re-running clang-format for the v1.34 alignment (which also re-wraps the
> > > +new `SendData()` declaration in `test/ares-test.h`). All other files
> > > +applied unchanged; the `test/` diffs match main exactly.
> > > +
> > > +**Tests (local, macOS, Debug, Ninja):**
> > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > +--gtest_filter='-*Live*'` — 693/693 passed in both.
> > > +- With the `src/` changes reverted, the 4 new test instances fail
> > > +(`UDPAnswerFromWrongPortDropped`,
> > `AnswerOnOtherServerConnectionDropped`
> > > +UDP/TCP, `CookielessAnswerOnOtherServerDropped`); they pass with them.
> > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > > +passed under ASAN+UBSAN and `-Werror`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/c8bd9ab53329f66ec0c395a11002a5696669b147]
> > > +GHSA: GHSA-fqh5-3x7m-wp44
> > > +---
> > > + src/lib/ares_conn.c    |  11 ++-
> > > + src/lib/ares_process.c |  18 ++++
> > > + src/lib/ares_socket.c  |  24 +++++
> > > + src/lib/ares_socket.h  |  21 ++++
> > > + test/ares-test-mock.cc | 218
> > +++++++++++++++++++++++++++++++++++++++++
> > > + test/ares-test.cc      |  86 +++++++++-------
> > > + test/ares-test.h       |  12 +++
> > > + 7 files changed, 355 insertions(+), 35 deletions(-)
> > > +
> > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index 5e0827fbdf..b868053e62 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -252,9 +252,16 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > *conn, void *data, size_t len,
> > > +                            (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. */
> > > +     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
> > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index 5686b077a6..06d0ca507c 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -1117,6 +1117,24 @@ static ares_status_t
> > process_answer(ares_channel_t      *channel,
> > > +     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)
> > > +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> > > +index 4ad3034021..0a12935ab7 100644
> > > +--- a/src/lib/ares_socket.c
> > > ++++ b/src/lib/ares_socket.c
> > > +@@ -127,6 +127,30 @@ ares_bool_t ares_sockaddr_addr_eq(const struct
> > sockaddr  *sa,
> > > +   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;
> > > ++  }
> > > ++
> > > ++  if (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,
> > > +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> > > +index 24a99ab331..f7128e3493 100644
> > > +--- a/src/lib/ares_socket.h
> > > ++++ b/src/lib/ares_socket.h
> > > +@@ -139,8 +139,29 @@ typedef enum {
> > > +   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.
> > > ++ *
> > > ++ *  \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, IP addresses and ports are
> > > ++ *          equal, 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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index d233f9070f..058dc93001 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -3363,6 +3363,7 @@ struct FakeSockData {
> > > +   unsigned char      query[512];
> > > +   size_t             query_len;
> > > +   struct sockaddr_in peer;
> > > ++  unsigned short     recv_port; /* network order; 0 reports the peer's port */
> > > +   size_t             recv_calls;
> > > +   size_t             bad_fd_calls;
> > > +   size_t             sock_state_bad;
> > > +@@ -3471,6 +3472,9 @@ static ares_ssize_t
> > FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> > > +   if (address != NULL && address_len != NULL &&
> > > +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > +     memcpy(address, &d->peer, sizeof(d->peer));
> > > ++    if (d->recv_port != 0) {
> > > ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
> > > ++    }
> > > +     *address_len = sizeof(d->peer);
> > > +   }
> > > +   return static_cast<ares_ssize_t>(d->query_len);
> > > +@@ -3710,6 +3714,42 @@ TEST_F(LibraryTest,
> > ReconfigureInRecvDropsBufferedAnswer) {
> > > +   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);
> > > ++}
> > > ++
> > > + // 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.
> > > +@@ -5530,5 +5570,183 @@
> > INSTANTIATE_TEST_SUITE_P(TransportModes, ServerRecoveryMultiMockTest,
> > ::testing:
> > > +
> > > + 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<int, bool> > {
> > > ++ 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<byte>());
> > > ++    }));
> > > ++  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<int> {
> > > ++ 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<byte> 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<byte>());
> > > ++    }));
> > > ++  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 --git a/test/ares-test.cc b/test/ares-test.cc
> > > +index d333ee9419..0b3ac12c09 100644
> > > +--- a/test/ares-test.cc
> > > ++++ b/test/ares-test.cc
> > > +@@ -448,6 +448,9 @@ MockServer::MockServer(int family, unsigned short
> > port)
> > > +   // 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;
> > > +@@ -710,6 +713,50 @@ std::set<ares_socket_t> MockServer::fds() const {
> > > +   return result;
> > > + }
> > > +
> > > ++void MockServer::SendData(ares_socket_t fd, struct sockaddr_storage *addr,
> > > ++                          ares_socklen_t addrlen, int qid,
> > > ++                          std::vector<byte> 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<byte> 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<ares_ssize_t>(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<byte> &req,
> > > +                                 const std::string &reqstr,
> > > +@@ -717,10 +764,14 @@ void MockServer::ProcessRequest(ares_socket_t
> > fd, struct sockaddr_storage* addr,
> > > +
> > > +   /* 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);
> > > +
> > > +@@ -747,43 +798,12 @@ void MockServer::ProcessRequest(ares_socket_t
> > fd, struct sockaddr_storage* addr,
> > > +     return;
> > > +   }
> > > +
> > > +-  // Make a local copy of the current pending reply.
> > > +-  std::vector<byte> 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<byte> 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<ares_ssize_t>(reply.size())) {
> > > +-    std::cerr << "Failed to send full reply, rc=" << rc << std::endl;
> > > +-  }
> > > ++  SendData(fd, addr, addrlen, qid, exact_reply_);
> > > +
> > > +   if (disconnect_after_reply_ && fd != udpfd_) {
> > > +     disconnect_after_reply_ = false;
> > > +diff --git a/test/ares-test.h b/test/ares-test.h
> > > +index 2b973c3982..170c284f26 100644
> > > +--- a/test/ares-test.h
> > > ++++ b/test/ares-test.h
> > > +@@ -303,6 +303,12 @@ class MockServer {
> > > +     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<ares_socket_t> fds() const;
> > > +
> > > +@@ -327,6 +333,8 @@ class MockServer {
> > > +                                 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<byte> reply);
> > > +   unsigned short udpport_;
> > > +   unsigned short tcpport_;
> > > +   ares_socket_t  udpfd_;
> > > +@@ -339,6 +347,10 @@ class MockServer {
> > > +   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.
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-
> > channel-config-under-lock.patch b/meta-oe/recipes-support/c-ares/c-ares/0006-
> > ares_save_options-read-channel-config-under-lock.patch
> > > new file mode 100644
> > > index 0000000000..76c072dd7e
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-
> > channel-config-under-lock.patch
> > > @@ -0,0 +1,243 @@
> > > +From 25a6911370878d4b617b44b142ea0e49d1ccede6 Mon Sep 17
> > 00:00:00 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Mon, 19 Oct 2026 11:53:59 -0400
> > > +Subject: [PATCH] [Backport v1.34] ares_save_options: read channel
> > > + configuration under the channel lock (#1362)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of 5d878a580a8b57b51d15e2725cf6775c8e7bca0e to `v1.34`. See
> > > +that commit for the full description.
> > > +
> > > +**Conflicts:** none; cherry-picked cleanly.
> > > +
> > > +**Tests (local, macOS, Debug, Ninja):**
> > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > +--gtest_filter='-*Live*'` — 691/691 passed in both.
> > > +- With the `src/` changes reverted, the new `DupConcurrentSetServers` /
> > > +`SaveOptionsConcurrentSetServers` tests abort with an ASAN
> > > +heap-buffer-overflow; they pass with them.
> > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > > +passed under ASAN+UBSAN and `-Werror`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/25a6911370878d4b617b44b142ea0e49d1ccede6]
> > > +GHSA: GHSA-ph6c-3cvx-mq6v
> > > +---
> > > + src/lib/ares_init.c    |  3 +-
> > > + src/lib/ares_options.c | 45 ++++++++++++++++++----
> > > + test/ares-test-init.cc | 87
> > ++++++++++++++++++++++++++++++++++++++++++
> > > + 3 files changed, 126 insertions(+), 9 deletions(-)
> > > +
> > > +diff --git a/src/lib/ares_init.c b/src/lib/ares_init.c
> > > +index 4758c5de56..85b2e13050 100644
> > > +--- a/src/lib/ares_init.c
> > > ++++ b/src/lib/ares_init.c
> > > +@@ -483,7 +483,8 @@ int ares_dup(ares_channel_t **dest, const
> > ares_channel_t *src)
> > > +   *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);
> > > +diff --git a/src/lib/ares_options.c b/src/lib/ares_options.c
> > > +index 2b5624bbea..5ea35b732e 100644
> > > +--- a/src/lib/ares_options.c
> > > ++++ b/src/lib/ares_options.c
> > > +@@ -55,8 +55,8 @@ static struct in_addr *ares_save_opt_servers(const
> > ares_channel_t *channel,
> > > +                                              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 @@ static struct in_addr *ares_save_opt_servers(const
> > ares_channel_t *channel,
> > > +     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 @@ static struct in_addr *ares_save_opt_servers(const
> > ares_channel_t *channel,
> > > +   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 @@ int ares_save_options(const ares_channel_t
> > *channel,
> > > +   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 @@ int ares_save_options(const ares_channel_t
> > *channel,
> > > +   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 @@ int ares_save_options(const ares_channel_t
> > *channel,
> > > +   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)
> > > +diff --git a/test/ares-test-init.cc b/test/ares-test-init.cc
> > > +index b44211a1e4..1c5d0f2171 100644
> > > +--- a/test/ares-test-init.cc
> > > ++++ b/test/ares-test-init.cc
> > > +@@ -25,6 +25,8 @@
> > > +  */
> > > + #include "ares-test.h"
> > > +
> > > ++#include <atomic>
> > > ++
> > > + extern "C" {
> > > +   #include "ares_private.h"
> > > + }
> > > +@@ -295,6 +297,91 @@ TEST_F(LibraryTest, FailChannelInit) {
> > > +   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<size_t> 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<std::thread> 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;
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-
> > prefix-length.patch b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-
> > reject-a-prefix-length.patch
> > > new file mode 100644
> > > index 0000000000..2cd1ea8c24
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-
> > prefix-length.patch
> > > @@ -0,0 +1,364 @@
> > > +From 7e65b381c0b915aa5671ddc713080c87e15fc42f Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Mon, 19 Oct 2026 12:24:52 -0400
> > > +Subject: [PATCH] [Backport v1.34] ares_inet_pton: reject a prefix length,
> > > + always write the full address (#1363)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of 7b4ef8e555b611be6af39d913aa280cd1a348102 to `v1.34`. See
> > > +that commit for the full description.
> > > +
> > > +**Conflicts:** `test/ares-test-mock-ai.cc` only — the neighbouring
> > > +`FamilyV6IgnoresARecords` test (added on main by an unrelated change)
> > > +does not exist on v1.34. Resolved by adding only the new
> > > +`NumericHostRejectsPrefixLength` test at the same position. Other files
> > > +applied unchanged.
> > > +
> > > +**Also includes** the comment spelling fix from #1364
> > > +(ead5557d1301328536c3920bd01759a1bff5ca9a): "unparseable" ->
> > > +"unparsable" in `src/lib/inet_net_pton.c` and
> > > +`test/ares-test-internal.cc`, so Codespell passes and the text matches
> > > +main.
> > > +
> > > +**Tests (local, macOS, Debug, Ninja):**
> > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > +--gtest_filter='-*Live*'` — 693/693 passed in both.
> > > +- With the `src/` changes reverted, the 4 new test instances fail
> > > +(`InetNetPtoNPrefixZeroFills`, `InetPtoNRejectsPrefixLength`,
> > > +`NumericHostRejectsPrefixLength` UDP/TCP); they pass with them.
> > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > > +passed under ASAN+UBSAN and `-Werror`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/7e65b381c0b915aa5671ddc713080c87e15fc42f]
> > > +GHSA: GHSA-cfcv-8v2j-qj6r
> > > +---
> > > + docs/ares_inet_pton.3        | 18 ++++++++--
> > > + src/lib/ares_inet_net_pton.h | 28 ++++++++++++++++
> > > + src/lib/inet_net_pton.c      | 65 ++++++++++++++++++++----------------
> > > + test/ares-test-internal.cc   | 54 ++++++++++++++++++++++++++++++
> > > + test/ares-test-mock-ai.cc    | 23 +++++++++++++
> > > + 5 files changed, 157 insertions(+), 31 deletions(-)
> > > +
> > > +diff --git a/docs/ares_inet_pton.3 b/docs/ares_inet_pton.3
> > > +index 34b2df063c..19c5fdfda3 100644
> > > +--- a/docs/ares_inet_pton.3
> > > ++++ b/docs/ares_inet_pton.3
> > > +@@ -12,8 +12,9 @@ ares_inet_pton \- convert an IPv4 or IPv6 address from
> > text to binary form
> > > + 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 @@ in. The \fBdst\fP argument points to a buffer into which
> > the function stores
> > > + 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 --git a/src/lib/ares_inet_net_pton.h b/src/lib/ares_inet_net_pton.h
> > > +index e3ed83a3cb..e1a6f2d71e 100644
> > > +--- a/src/lib/ares_inet_net_pton.h
> > > ++++ b/src/lib/ares_inet_net_pton.h
> > > +@@ -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 --git a/src/lib/inet_net_pton.c b/src/lib/inet_net_pton.c
> > > +index e1f76ef834..f60823023b 100644
> > > +--- a/src/lib/inet_net_pton.c
> > > ++++ b/src/lib/inet_net_pton.c
> > > +@@ -51,14 +51,15 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0,
> > 0, 0, 0, 0, 0, 0, 0, 0,
> > > +
> > > + /*
> > > +  * 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 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0, 0,
> > 0, 0, 0, 0, 0, 0, 0,
> > > +  *      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 @@ static int ares_inet_net_pton_ipv4(const char *src,
> > unsigned char *dst,
> > > +   }
> > > +
> > > +   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 @@ static int ares_inet_pton6(const char *src, unsigned
> > char *dst)
> > > +       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 @@ static int ares_inet_net_pton_ipv6(const char *src,
> > unsigned char *dst,
> > > +     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 ares_inet_net_pton(int af, const char *src, void
> > *dst, size_t size)
> > > +   }
> > > + }
> > > +
> > > ++/*
> > > ++ * 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 --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> > > +index bf06a74605..294e3f4555 100644
> > > +--- a/test/ares-test-internal.cc
> > > ++++ b/test/ares-test-internal.cc
> > > +@@ -274,6 +274,23 @@ TEST_F(LibraryTest, InetNetPtoN) {
> > > +   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++) {
> > > +@@ -600,9 +617,46 @@ TEST_F(LibraryTest, InetPtoN) {
> > > +   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.
> > > +diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc
> > > +index ae45c45527..43d0b89175 100644
> > > +--- a/test/ares-test-mock-ai.cc
> > > ++++ b/test/ares-test-mock-ai.cc
> > > +@@ -617,6 +617,29 @@ TEST_P(MockChannelTestAI, FamilyV6) {
> > > +   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<byte> nothing;
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-
> > drop-UDP-answers-from-server-port.patch b/meta-oe/recipes-support/c-ares/c-
> > ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> > > new file mode 100644
> > > index 0000000000..f4f182b995
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-
> > UDP-answers-from-server-port.patch
> > > @@ -0,0 +1,178 @@
> > > +From c7fa9ebcb0c0086730c53bf03191c03f03706246 Mon Sep 17 00:00:00
> > 2001
> > > +From: "c-ares-backport-gha[bot]"
> > > + <299594594+c-ares-backport-gha[bot]@users.noreply.github.com>
> > > +Date: Tue, 20 Oct 2026 02:25:59 +0000
> > > +Subject: [PATCH] [Backport v1.34] ares_conn_read: don't drop UDP answers
> > whose
> > > + source port isn't reported (#1371)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +# Description
> > > +Backport of #1370 to `v1.34`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Co-authored-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/c7fa9ebcb0c0086730c53bf03191c03f03706246]
> > > +GHSA: GHSA-fqh5-3x7m-wp44
> > > +---
> > > + docs/ares_set_socket_functions.3 | 20 ++++++++++++++++++++
> > > + src/lib/ares_conn.c              |  9 ++++++++-
> > > + src/lib/ares_socket.c            |  4 +++-
> > > + src/lib/ares_socket.h            |  8 +++++---
> > > + test/ares-test-mock.cc           | 32 +++++++++++++++++++++++++++++++-
> > > + 5 files changed, 67 insertions(+), 6 deletions(-)
> > > +
> > > +diff --git a/docs/ares_set_socket_functions.3
> > b/docs/ares_set_socket_functions.3
> > > +index 605beb26ef..75c621cb48 100644
> > > +--- a/docs/ares_set_socket_functions.3
> > > ++++ b/docs/ares_set_socket_functions.3
> > > +@@ -149,6 +149,20 @@ a socket. Additional flags controlling behavior are in
> > > + 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 @@ Receives data from remote socket endpoint, if
> > available. If the \fIaddr\fP
> > > + 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
> > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index b868053e62..7711d8649e 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -257,7 +257,14 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > *conn, void *data, size_t len,
> > > +      * 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. */
> > > ++     * 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_port_eq((struct sockaddr *)&sa_storage,
> > > +                                     &conn->server->addr,
> > > +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> > > +index 0a12935ab7..29d595e1a9 100644
> > > +--- a/src/lib/ares_socket.c
> > > ++++ b/src/lib/ares_socket.c
> > > +@@ -144,7 +144,9 @@ ares_bool_t ares_sockaddr_addr_port_eq(const
> > struct sockaddr  *sa,
> > > +     sa_port = (CARES_INADDR_CAST(const struct sockaddr_in6 *, sa))-
> > >sin6_port;
> > > +   }
> > > +
> > > +-  if (ntohs(sa_port) != 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;
> > > +   }
> > > +
> > > +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> > > +index f7128e3493..1ede6987ee 100644
> > > +--- a/src/lib/ares_socket.h
> > > ++++ b/src/lib/ares_socket.h
> > > +@@ -151,13 +151,15 @@ 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.
> > > ++ *  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, IP addresses and ports are
> > > +- *          equal, ARES_FALSE otherwise
> > > ++ *  \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,
> > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index 058dc93001..cc5e6482f2 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -3364,6 +3364,7 @@ struct FakeSockData {
> > > +   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;
> > > +@@ -3472,7 +3473,9 @@ static ares_ssize_t
> > FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> > > +   if (address != NULL && address_len != NULL &&
> > > +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > +     memcpy(address, &d->peer, sizeof(d->peer));
> > > +-    if (d->recv_port != 0) {
> > > ++    if (d->recv_no_port) {
> > > ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = 0;
> > > ++    } else if (d->recv_port != 0) {
> > > +       reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
> > > +     }
> > > +     *address_len = sizeof(d->peer);
> > > +@@ -3750,6 +3753,33 @@ TEST_F(LibraryTest,
> > UDPAnswerFromWrongPortDropped) {
> > > +   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.
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb b/meta-oe/recipes-
> > support/c-ares/c-ares_1.34.8.bb
> > > index 058f3666e7..1d0f3eb343 100644
> > > --- a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> > > @@ -6,6 +6,14 @@ LICENSE = "MIT"
> > >  LIC_FILES_CHKSUM =
> > "file://LICENSE.md;md5=d3e72a10e08191f2ca1be3f3228d78f3"
> > >
> > >  SRC_URI = "https://github.com/c-ares/c-
> > ares/releases/download/v${PV}/${BPN}-${PV}.tar.gz \
> > > +           file://0001-ares_cancel-ares_destroy-detach-queries-before-
> > callbacks.patch \
> > > +           file://0002-Deliver-deferred-socket-server-state-notifications.patch \
> > > +           file://0003-ares_qcache-hand-callbacks-their-own-copy.patch \
> > > +           file://0004-Handle-connections-torn-down-by-application.patch \
> > > +           file://0005-ares_process-only-accept-an-answer-on-the-right-
> > connection.patch \
> > > +           file://0006-ares_save_options-read-channel-config-under-lock.patch \
> > > +           file://0007-ares_inet_pton-reject-a-prefix-length.patch \
> > > +           file://0008-ares_conn_read-dont-drop-UDP-answers-from-server-
> > port.patch \
> > >             file://run-ptest"
> > >  SRC_URI[sha256sum] =
> > "c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78"
> > >
> > > --
> > > 2.43.0
> > >
> > >
> > >
> > >
Ankur Tyagi Oct. 27, 2026, 9:11 a.m. UTC | #4
On Tue, Oct 27, 2026 at 9:45 PM Ankur Tyagi <ankur.tyagi85@gmail.com> wrote:
>
> On Tue, Oct 27, 2026 at 9:25 PM Marko, Peter <Peter.Marko@siemens.com> wrote:
> >
> > Can we rather do the upgrade instead? I was planning to send it for Wrynose branch eventually.
> > Is there anything in "Changes" section of https://github.com/c-ares/c-ares/releases/tag/v1.34.9 which would prevent it?
>
> Hi Peter,
>
> Jason reached out earlier in a separate email regarding the upgrade as well.
>
> c-ares v1.34.9 brings following changes:
> - 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.
>
> And the acceptance policy doesn't allow for such an upgrade in the
> release branch.
>
> cheers
> Ankur
> >
> > Peter
> >
> > > -----Original Message-----
> > > From: openembedded-devel@lists.openembedded.org <openembedded-
> > > devel@lists.openembedded.org> On Behalf Of Ankur Tyagi via
> > > lists.openembedded.org
> > > Sent: Tuesday, October 27, 2026 8:04 AM
> > > To: schonm@gmail.com
> > > Cc: openembedded-devel@lists.openembedded.org
> > > Subject: Re: [oe] [meta-oe][blacksail][PATCH] c-ares: backport security fixes from
> > > 1.34.9
> > >
> > > Hi Jason,
> > >
> > > Thanks for submitting the patch.
> > >
> > > Though the patches LGTM, I am seeing increased ptest failures.
> > >
> > > Existing recipe ptest status:
> > > [==========] 1093 tests from 41 test suites ran.
> > > [  PASSED  ] 1092 tests.
> > > [  FAILED  ] 1 test, listed below:
> > > [  FAILED  ] DefaultChannelTest.TimeoutValue
> > >
> > > ptest status after applying your patch:
> > > [==========] 1174 tests from 47 test suites ran.
> > > [  PASSED  ] 1170 tests.
> > > [  FAILED  ] 4 tests, listed below:
> > > [  FAILED  ] DefaultChannelTest.TimeoutValue
> > > [  FAILED  ] SockStateDeliveryTest.CancelReportsSocketClose
> > > [  FAILED  ] SockStateDeliveryTest.SetServersReportsSocketClose
> > > [  FAILED  ] SockStateDeliveryTest.DestroyReportsSocketClose
> > >

Following is the ptest result from master branch:
[==========] 1199 tests from 47 test suites ran.
[  PASSED  ] 1195 tests.
[  FAILED  ] 4 tests, listed below:
[  FAILED  ] DefaultChannelTest.TimeoutValue
[  FAILED  ] SockStateDeliveryTest.CancelReportsSocketClose
[  FAILED  ] SockStateDeliveryTest.SetServersReportsSocketClose
[  FAILED  ] SockStateDeliveryTest.DestroyReportsSocketClose

Not a problem caused by the backport, it's a separate issue.

I will accept this patch.

cheers
Ankur

> > > Perhaps some other commits also need backporting?
> > > Can please look into this?
> > >
> > > cheers
> > > Ankur
> > >
> > > On Tue, Oct 27, 2026 at 2:22 PM J. S. via lists.openembedded.org
> > > <schonm=gmail.com@lists.openembedded.org> wrote:
> > > >
> > > > Add the eight unique upstream commits that fix the security advisories
> > > addressed in c-ares 1.34.9.
> > > > Several advisories share commits, so patches are ordered by commit rather
> > > than duplicated per advisory; each patch lists the GHSA IDs it resolves.
> > > >
> > > > Assisted-by: opencode (big-pickle)
> > > > Signed-off-by: Jason Schonberg <schonm@gmail.com>
> > > > ---
> > > >  ...troy-detach-queries-before-callbacks.patch | 2603 +++++++++++++++++
> > > >  ...ed-socket-server-state-notifications.patch |  393 +++
> > > >  ...qcache-hand-callbacks-their-own-copy.patch | 1054 +++++++
> > > >  ...connections-torn-down-by-application.patch | 1107 +++++++
> > > >  ...pt-an-answer-on-the-right-connection.patch |  575 ++++
> > > >  ...tions-read-channel-config-under-lock.patch |  243 ++
> > > >  ...res_inet_pton-reject-a-prefix-length.patch |  364 +++
> > > >  ...nt-drop-UDP-answers-from-server-port.patch |  178 ++
> > > >  .../recipes-support/c-ares/c-ares_1.34.8.bb   |    8 +
> > > >  9 files changed, 6525 insertions(+)
> > > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-
> > > ares_destroy-detach-queries-before-callbacks.patch
> > > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-
> > > deferred-socket-server-state-notifications.patch
> > > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0003-
> > > ares_qcache-hand-callbacks-their-own-copy.patch
> > > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0004-Handle-
> > > connections-torn-down-by-application.patch
> > > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0005-
> > > ares_process-only-accept-an-answer-on-the-right-connection.patch
> > > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0006-
> > > ares_save_options-read-channel-config-under-lock.patch
> > > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0007-
> > > ares_inet_pton-reject-a-prefix-length.patch
> > > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0008-
> > > ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> > > >
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-
> > > ares_destroy-detach-queries-before-callbacks.patch b/meta-oe/recipes-
> > > support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-
> > > callbacks.patch
> > > > new file mode 100644
> > > > index 0000000000..385760f5bc
> > > > --- /dev/null
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-
> > > detach-queries-before-callbacks.patch
> > > > @@ -0,0 +1,2603 @@
> > > > +From 7f408deb128fbdcce78122322734ffaa01cad9ae Mon Sep 17 00:00:00
> > > 2001
> > > > +From: Brad House <brad@brad-house.com>
> > > > +Date: Sun, 18 Oct 2026 18:15:41 -0400
> > > > +Subject: [PATCH] ares_cancel/ares_destroy: detach queries before running
> > > user
> > > > + callbacks
> > > > +MIME-Version: 1.0
> > > > +Content-Type: text/plain; charset=UTF-8
> > > > +Content-Transfer-Encoding: 8bit
> > > > +
> > > > +Backport of dd62bdfca52ba5c37e88a1469c0a4c5e4a568ccb to v1.34.  See
> > > that
> > > > +commit for the full description; it hardens reentrant teardown from
> > > > +callbacks (ares_cancel(), ares_destroy(), server reconfiguration, and
> > > > +socket/server state notifications).
> > > > +
> > > > +Conflicts were whitespace/alignment only (v1.34 predates the clang-format
> > > > +reformat in #1238) and were reformatted to v1.34's style.
> > > > +
> > > > +Also carries the test portion of #1334 that applies to the tests this
> > > > +introduces: dead-server tests use 127.0.0.1:53055/53056 instead of
> > > > +127.0.0.55/56, which only routes on Linux.
> > > > +
> > > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > > ares/commit/7f408deb128fbdcce78122322734ffaa01cad9ae]
> > > > +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-
> > > 2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> > > > +---
> > > > + docs/ares_cancel.3                    |   17 +
> > > > + docs/ares_destroy.3                   |    7 +
> > > > + docs/ares_init_options.3              |   15 +
> > > > + docs/ares_set_server_state_callback.3 |   31 +-
> > > > + docs/ares_set_servers_csv.3           |   17 +
> > > > + src/lib/ares_cancel.c                 |   25 +-
> > > > + src/lib/ares_close_sockets.c          |   82 +-
> > > > + src/lib/ares_conn.c                   |  217 ++++-
> > > > + src/lib/ares_conn.h                   |   37 +-
> > > > + src/lib/ares_destroy.c                |   85 +-
> > > > + src/lib/ares_private.h                |  110 ++-
> > > > + src/lib/ares_process.c                |  235 +++++-
> > > > + src/lib/ares_send.c                   |   35 +
> > > > + src/lib/ares_update_servers.c         |   42 +-
> > > > + test/ares-test-mock.cc                | 1068 +++++++++++++++++++++++++
> > > > + 15 files changed, 1959 insertions(+), 64 deletions(-)
> > > > +
> > > > +diff --git a/docs/ares_cancel.3 b/docs/ares_cancel.3
> > > > +index 967a946b49..069f226c10 100644
> > > > +--- a/docs/ares_cancel.3
> > > > ++++ b/docs/ares_cancel.3
> > > > +@@ -21,6 +21,23 @@ These calls give the callbacks a chance to clean up
> > > any state which might have
> > > > + 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 --git a/docs/ares_destroy.3 b/docs/ares_destroy.3
> > > > +index d1984b7cc9..0b6380e609 100644
> > > > +--- a/docs/ares_destroy.3
> > > > ++++ b/docs/ares_destroy.3
> > > > +@@ -20,6 +20,13 @@ by the channel.
> > > > + 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 --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> > > > +index 9b3b481535..e6f6ee8ba6 100644
> > > > +--- a/docs/ares_init_options.3
> > > > ++++ b/docs/ares_init_options.3
> > > > +@@ -231,6 +231,21 @@ will be passed the socket whose state has changed;
> > > > + 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
> > > > +diff --git a/docs/ares_set_server_state_callback.3
> > > b/docs/ares_set_server_state_callback.3
> > > > +index 04fbde4c7a..e95db327ce 100644
> > > > +--- a/docs/ares_set_server_state_callback.3
> > > > ++++ b/docs/ares_set_server_state_callback.3
> > > > +@@ -47,6 +47,31 @@ the DNS servers used by an ares channel. For
> > > example, it can be used to track
> > > > + 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 @@ Indicates that the query was tried over TCP.
> > > > + .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 --git a/docs/ares_set_servers_csv.3 b/docs/ares_set_servers_csv.3
> > > > +index f1435143f5..f9f5b82e28 100644
> > > > +--- a/docs/ares_set_servers_csv.3
> > > > ++++ b/docs/ares_set_servers_csv.3
> > > > +@@ -126,6 +126,23 @@ are identical.  Prior versions would simply omit ports
> > > in \fBares_set_servers_cs
> > > > + 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 --git a/src/lib/ares_cancel.c b/src/lib/ares_cancel.c
> > > > +index 75600dea6b..aa167e4486 100644
> > > > +--- a/src/lib/ares_cancel.c
> > > > ++++ b/src/lib/ares_cancel.c
> > > > +@@ -38,10 +38,10 @@ void ares_cancel(ares_channel_t *channel)
> > > > +   }
> > > > +
> > > > +   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 @@ void ares_cancel(ares_channel_t *channel)
> > > > +       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 @@ void ares_cancel(ares_channel_t *channel)
> > > > +   ares_queue_notify_empty(channel);
> > > > +
> > > > + done:
> > > > ++  ares_deferred_cb_leave(channel);
> > > > +   ares_channel_unlock(channel);
> > > > + }
> > > > +diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
> > > > +index 7bb72c31ec..6be662fdd3 100644
> > > > +--- a/src/lib/ares_close_sockets.c
> > > > ++++ b/src/lib/ares_close_sockets.c
> > > > +@@ -41,12 +41,47 @@ static void ares_requeue_queries(ares_conn_t
> > > *conn,
> > > > +   }
> > > > + }
> > > > +
> > > > ++/*! 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;
> > > > ++
> > > > ++  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;
> > > > ++  }
> > > > +
> > > > +-  /* Unlink */
> > > > ++  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 @@ void ares_close_connection(ares_conn_t *conn,
> > > ares_status_t requeue_status)
> > > > +     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);
> > > > +-
> > > > +-  ares_socket_close(channel, conn->fd);
> > > > ++  /* 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;
> > > > ++
> > > > ++  /* 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 @@ void ares_close_sockets(ares_server_t *server)
> > > > +
> > > > +   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);
> > > > +   }
> > > > + }
> > > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > > +index 6b315b0548..78f57e7d10 100644
> > > > +--- a/src/lib/ares_conn.c
> > > > ++++ b/src/lib/ares_conn.c
> > > > +@@ -26,19 +26,210 @@
> > > > +  */
> > > > + #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 ((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);
> > > > ++    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.
> > > > ++   *
> > > > ++   * 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;
> > > > ++
> > > > ++  /* 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;
> > > > + }
> > > > +
> > > > +@@ -435,6 +626,16 @@ ares_status_t ares_open_connection(ares_conn_t
> > > **conn_out,
> > > > +     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) {
> > > > +diff --git a/src/lib/ares_conn.h b/src/lib/ares_conn.h
> > > > +index c332de14b1..fc177b84f6 100644
> > > > +--- a/src/lib/ares_conn.h
> > > > ++++ b/src/lib/ares_conn.h
> > > > +@@ -49,7 +49,14 @@ typedef enum {
> > > > +    *  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
> > > > ++  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 {
> > > > +@@ -84,6 +91,10 @@ struct ares_conn {
> > > > +
> > > > +   /* 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 */
> > > > +@@ -168,6 +179,10 @@ struct ares_server {
> > > > +
> > > > +   /* 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);
> > > > +@@ -185,8 +200,24 @@ ares_status_t   ares_conn_flush(ares_conn_t
> > > *conn);
> > > > + 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 --git a/src/lib/ares_destroy.c b/src/lib/ares_destroy.c
> > > > +index 1e5706e0e0..e0534c800d 100644
> > > > +--- a/src/lib/ares_destroy.c
> > > > ++++ b/src/lib/ares_destroy.c
> > > > +@@ -69,17 +69,31 @@ void ares_destroy(ares_channel_t *channel)
> > > > +    * 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 @@ void ares_destroy(ares_channel_t *channel)
> > > > +   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 @@ void ares_destroy(ares_channel_t *channel)
> > > > +   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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > > +index 04d4576ed2..0a1029e0f8 100644
> > > > +--- a/src/lib/ares_private.h
> > > > ++++ b/src/lib/ares_private.h
> > > > +@@ -147,7 +147,10 @@ typedef struct ares_query ares_query_t;
> > > > + 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;
> > > > +
> > > > +@@ -227,6 +230,9 @@ struct ares_channeldata {
> > > > +   /* 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
> > > > +    */
> > > > +@@ -237,6 +243,10 @@ struct ares_channeldata {
> > > > +   /* 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;
> > > > +
> > > > +@@ -248,6 +258,29 @@ struct ares_channeldata {
> > > > +
> > > > +   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;
> > > > +@@ -295,6 +328,10 @@ struct ares_channeldata {
> > > > +   /* 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
> > > > +@@ -308,6 +345,11 @@ struct ares_channeldata {
> > > > +    * 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. */
> > > > +@@ -351,6 +393,16 @@ void         *ares_dnsrec_convert_arg(ares_callback
> > > callback, void *arg);
> > > > + 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);
> > > > +@@ -390,7 +442,6 @@ ares_status_t
> > > ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
> > > > +
> > > > + 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);
> > > > +@@ -589,6 +640,60 @@ ares_status_t ares_dns_name_write(ares_buf_t
> > > *buf, ares_llist_t **list,
> > > > +  */
> > > > + 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.
> > > > ++ *
> > > > ++ *  \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)
> > > > +@@ -636,7 +741,6 @@ typedef struct ares_event_thread
> > > ares_event_thread_t;
> > > > + 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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > > +index b2316c4b33..82f53610fb 100644
> > > > +--- a/src/lib/ares_process.c
> > > > ++++ b/src/lib/ares_process.c
> > > > +@@ -69,7 +69,6 @@ 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_detach_query(ares_query_t *query);
> > > > +
> > > > + static void        ares_query_remove_from_conn(ares_query_t *query)
> > > > + {
> > > > +@@ -81,14 +80,32 @@ static void
> > > ares_query_remove_from_conn(ares_query_t *query)
> > > > +   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;
> > > > +@@ -111,9 +128,57 @@ static void invoke_server_state_cb(const
> > > ares_server_t *server,
> > > > +     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,
> > > > +@@ -204,6 +269,8 @@ static ares_status_t
> > > ares_process_fds_nolock(ares_channel_t         *channel,
> > > > +     return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
> > > > +   }
> > > > +
> > > > ++  ares_deferred_cb_enter(channel);
> > > > ++
> > > > +   ares_tvnow(&now);
> > > > +
> > > > +   /* Process write events */
> > > > +@@ -244,6 +311,8 @@ static ares_status_t
> > > ares_process_fds_nolock(ares_channel_t         *channel,
> > > > +   }
> > > > +
> > > > + done:
> > > > ++  ares_deferred_cb_leave(channel);
> > > > ++
> > > > +   if (status == ARES_ENOMEM) {
> > > > +     return ARES_ENOMEM;
> > > > +   }
> > > > +@@ -553,6 +622,8 @@ typedef enum {
> > > > + typedef struct {
> > > > +   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 */
> > > > +@@ -578,6 +649,7 @@ static ares_status_t
> > > ares_append_requeue_int(ares_array_t     **requeue,
> > > > +
> > > > +   entry.type   = type;
> > > > +   entry.qid    = query->qid;
> > > > ++  entry.serial = query->serial;
> > > > +   entry.server = server;
> > > > +   entry.status = status;
> > > > +   entry.dnsrec = dnsrec;
> > > > +@@ -601,6 +673,53 @@ static ares_status_t
> > > ares_append_endqueue(ares_array_t     **requeue,
> > > > +     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:
> > > > +@@ -636,11 +755,35 @@ static ares_status_t
> > > ares_flush_requeue(ares_channel_t       *channel,
> > > > +
> > > > +     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. */
> > > > +@@ -1377,6 +1520,7 @@ ares_status_t ares_send_query(ares_server_t
> > > *requested_server,
> > > > +   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);
> > > > +@@ -1393,8 +1537,7 @@ ares_status_t ares_send_query(ares_server_t
> > > *requested_server,
> > > > +    * 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_htable_szvp_get_direct(channel->queries_by_qid, qid) == NULL) {
> > > > ++  if (status == ARES_SUCCESS && !ares_query_is_live(channel, qid, serial))
> > > {
> > > > +     status = ARES_ETIMEOUT;
> > > > +   }
> > > > +
> > > > +@@ -1412,6 +1555,9 @@ static ares_status_t
> > > ares_send_query_int(ares_server_t        *requested_server,
> > > > +   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) {
> > > > +@@ -1443,6 +1589,32 @@ static ares_status_t
> > > ares_send_query_int(ares_server_t        *requested_server,
> > > > +   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:
> > > > +@@ -1464,6 +1636,14 @@ static ares_status_t
> > > ares_send_query_int(ares_server_t        *requested_server,
> > > > +
> > > > +   /* 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;
> > > > ++  }
> > > > ++
> > > > +   switch (status) {
> > > > +     /* Good result, continue on */
> > > > +     case ARES_SUCCESS:
> > > > +@@ -1479,6 +1659,17 @@ static ares_status_t
> > > ares_send_query_int(ares_server_t        *requested_server,
> > > > +     case ARES_ECONNREFUSED:
> > > > +     case ARES_EBADFAMILY:
> > > > +       handle_conn_error(conn, ARES_TRUE, status);
> > > > ++      /* 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;
> > > > +@@ -1597,11 +1788,18 @@ static ares_bool_t same_questions(const
> > > ares_query_t      *query,
> > > > +   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;
> > > > + }
> > > > +@@ -1624,6 +1822,16 @@ static void end_query(ares_channel_t *channel,
> > > ares_server_t *server,
> > > > +     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);
> > > > +@@ -1631,7 +1839,8 @@ static void end_query(ares_channel_t *channel,
> > > ares_server_t *server,
> > > > +   /* 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);
> > > > + }
> > > > +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > > +index 6efa9580b2..2e0482bf19 100644
> > > > +--- a/src/lib/ares_send.c
> > > > ++++ b/src/lib/ares_send.c
> > > > +@@ -117,6 +117,27 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +   unsigned short           id          = generate_unique_qid(channel);
> > > > +   const ares_dns_record_t *dnsrec_resp = 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);
> > > > +
> > > > +   if (ares_slist_len(channel->servers) == 0) {
> > > > +@@ -145,6 +166,7 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +
> > > > +   query->channel      = channel;
> > > > +   query->qid          = id;
> > > > ++  query->serial       = ++channel->query_serial;
> > > > +   query->timeout.sec  = 0;
> > > > +   query->timeout.usec = 0;
> > > > +   query->using_tcp =
> > > > +@@ -209,6 +231,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +    */
> > > > +   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 +245,17 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +
> > > > +   /* Perform the first query action. */
> > > > +
> > > > ++  /* Sending opens and writes to connections and can mark a server down,
> > > both
> > > > ++   * of which notify.  Deliver those once we are done with the connection and
> > > > ++   * the server, not part-way through. */
> > > > ++  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;
> > > > + }
> > > > +
> > > > +diff --git a/src/lib/ares_update_servers.c b/src/lib/ares_update_servers.c
> > > > +index f8d441accf..50a9c27925 100644
> > > > +--- a/src/lib/ares_update_servers.c
> > > > ++++ b/src/lib/ares_update_servers.c
> > > > +@@ -740,17 +740,36 @@ static ares_bool_t
> > > ares_servers_remove_stale(ares_channel_t *channel,
> > > > + {
> > > > +   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;
> > > > + }
> > > > +@@ -775,6 +794,14 @@ ares_status_t ares_servers_update(ares_channel_t
> > > *channel,
> > > > +     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
> > > > +    */
> > > > +@@ -843,6 +870,7 @@ ares_status_t ares_servers_update(ares_channel_t
> > > *channel,
> > > > +   status = ARES_SUCCESS;
> > > > +
> > > > + done:
> > > > ++  ares_deferred_cb_leave(channel);
> > > > +   return status;
> > > > + }
> > > > +
> > > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > > +index c0c0279abb..2158c5a913 100644
> > > > +--- a/test/ares-test-mock.cc
> > > > ++++ b/test/ares-test-mock.cc
> > > > +@@ -31,6 +31,11 @@
> > > > + #include <sys/stat.h>
> > > > + #endif
> > > > +
> > > > ++#ifndef _WIN32
> > > > ++#  include <errno.h>
> > > > ++#  include <fcntl.h>
> > > > ++#endif
> > > > ++
> > > > + #include <sstream>
> > > > + #include <vector>
> > > > +
> > > > +@@ -1914,6 +1919,7 @@ TEST_P(MockRetryDepthChannelTest,
> > > HighRetryNoStackOverflow) {
> > > > + struct CancelInCbData {
> > > > +   ares_channel_t *channel;
> > > > +   bool            done;
> > > > ++  size_t          cbs;
> > > > + };
> > > > +
> > > > + static void CancelChannelCallback(void *arg, ares_status_t status,
> > > > +@@ -1925,10 +1931,1071 @@ static void CancelChannelCallback(void *arg,
> > > ares_status_t 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<int> {
> > > > ++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<CancelInCbData *>(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<CancelInCbData *>(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<DestroyReentryData *>(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<DestroyReentryData *>(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);
> > > > ++
> > > > ++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<byte> 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<SockStateCancelData *>(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<SockStateCloseOrderData
> > > *>(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<int>(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<ReconfigureFromHookData
> > > *>(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<ReconfigureFromHookData
> > > *>(arg);
> > > > ++  (void)timeouts;
> > > > ++  (void)dnsrec;
> > > > ++  d->done   = true;
> > > > ++  d->status = static_cast<int>(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);
> > > > ++}
> > > > ++
> > > > ++// 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<SockStateReconfigureData *>(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);
> > > > ++}
> > > > ++
> > > > ++// 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<std::pair<ares_socket_t, bool>> events; /* fd, still-armed */
> > > > ++};
> > > > ++
> > > > ++static void SockStateRecordCallback(void *data, ares_socket_t socket_fd,
> > > > ++                                    int readable, int writable)
> > > > ++{
> > > > ++  SockStateRecordData *d = static_cast<SockStateRecordData *>(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<ServerStateCancelData *>(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<ServerStateCancelData *>(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<int> {
> > > > ++ 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<ServerStateUnregisterData *>(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<ServerStateUnregisterData *>(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<ReconfigureInCbData *>(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<ares_socket_t> 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<NestedReconfigureData
> > > *>(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<NestedReconfigureData
> > > *>(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()
> > > > +@@ -1940,6 +3007,7 @@ TEST_P(MockUDPChannelTest,
> > > CancelInCallbackNoDoubleFree) {
> > > > +   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();
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-
> > > server-state-notifications.patch b/meta-oe/recipes-support/c-ares/c-ares/0002-
> > > Deliver-deferred-socket-server-state-notifications.patch
> > > > new file mode 100644
> > > > index 0000000000..c34d3ab7c7
> > > > --- /dev/null
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-
> > > server-state-notifications.patch
> > > > @@ -0,0 +1,393 @@
> > > > +From f944c4718fd672c9c0befd65a340abb2ccfc1a31 Mon Sep 17 00:00:00
> > > 2001
> > > > +From: Brad House <brad@brad-house.com>
> > > > +Date: Sun, 18 Oct 2026 20:28:11 -0400
> > > > +Subject: [PATCH] Deliver deferred socket/server-state notifications only from
> > > > + the outermost public call (#1340)
> > > > +MIME-Version: 1.0
> > > > +Content-Type: text/plain; charset=UTF-8
> > > > +Content-Transfer-Encoding: 8bit
> > > > +
> > > > +Backport of f023a52a (#1340) to v1.34.
> > > > +
> > > > +The recent change that queues socket-state and server-state
> > > > +notifications delivers them, and runs the deferred cleanup, only from
> > > > +the outermost `ares_deferred_cb_leave()`. `ares_send_nolock()` brackets
> > > > +`ares_send_query()` itself, but most public lookup functions never
> > > > +opened a region of their own. When reached from `ares_getaddrinfo()`,
> > > > +`ares_query_dnsrec()` and similar, the bracket inside
> > > > +`ares_send_nolock()` was therefore the outermost one. Notifications were
> > > > +then delivered from inside an internal call while the library was still
> > > > +in the middle of the public call, and its callers were still using their
> > > > +lookup state (the host query, the query id out-pointer, ...). A
> > > > +server-state callback that reconfigures the servers, which
> > > > +`ares_set_server_state_callback(3)` documents as allowed, could end that
> > > > +lookup before the caller was done with it.
> > > > +
> > > > +This is a regression in unreleased code. No released version is
> > > > +affected.
> > > > +
> > > > +The outermost region is now opened by the public entry points
> > > > +themselves, under the channel lock:
> > > > +
> > > > +- `ares_send_dnsrec()` (also used by `ares_send()`)
> > > > +- `ares_query_dnsrec()` (also used by `ares_query()`)
> > > > +- `ares_search()`, `ares_search_dnsrec()`
> > > > +- `ares_getaddrinfo()` (also used by `ares_gethostbyname()`)
> > > > +- `ares_gethostbyaddr()`, `ares_getnameinfo()`
> > > > +- `ares_process_pending_write()`: a failed flush closes the connection
> > > > +and fails its queries over, and this function had no bracket at all.
> > > > +
> > > > +`ares_process_fds()` (and through it
> > > > +`ares_process()`/`ares_process_fd()`), `ares_cancel()`, `ares_destroy()`
> > > > +and the server configuration functions already had their own. The nested
> > > > +bracket in `ares_send_nolock()` stays, because it keeps connections and
> > > > +servers unlinked during the send readable until the send returns.
> > > > +Internal sends made from query callbacks (search next domain,
> > > > +getaddrinfo next lookup, requeue, server probes) already run inside the
> > > > +processing bracket.
> > > > +
> > > > +The comments on `ares_deferred_cb_enter()` and the flush now state the
> > > > +rule: the outermost region belongs to the public entry point, never to
> > > > +an internal helper.
> > > > +
> > > > +The documented behaviour (notifications arrive before the outermost
> > > > +c-ares call returns) does not change, so no man page changes.
> > > > +
> > > > +Two new `LibraryTest` cases in `test/ares-test-mock.cc`. The IPv6 server
> > > > +cannot get a socket, so the first send fails over to the IPv4 server,
> > > > +and a server-state callback then reconfigures the servers:
> > > > +
> > > > +- `ServerFailoverDuringGetAddrInfo`: the lookup completes cleanly. It
> > > > +fails under ASAN without this change.
> > > > +- `ServerFailoverDuringQuery`: the query id from `ares_query_dnsrec()`
> > > > +is already reported by the time the query callback runs. It fails
> > > > +without this change.
> > > > +
> > > > +The full non-live suite passes under ASAN/UBSAN and with
> > > > +`-DCARES_WERROR=ON`.
> > > > +
> > > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > > ares/commit/f944c4718fd672c9c0befd65a340abb2ccfc1a31]
> > > > +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-
> > > 2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> > > > +---
> > > > + src/lib/ares_conn.c          |   4 +-
> > > > + src/lib/ares_getaddrinfo.c   |   5 ++
> > > > + src/lib/ares_gethostbyaddr.c |   5 ++
> > > > + src/lib/ares_getnameinfo.c   |   5 ++
> > > > + src/lib/ares_private.h       |  10 ++++
> > > > + src/lib/ares_process.c       |   6 ++
> > > > + src/lib/ares_query.c         |   5 ++
> > > > + src/lib/ares_search.c        |  10 ++++
> > > > + src/lib/ares_send.c          |  13 ++++-
> > > > + test/ares-test-mock.cc       | 106 +++++++++++++++++++++++++++++++++++
> > > > + 10 files changed, 166 insertions(+), 3 deletions(-)
> > > > +
> > > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > > +index 78f57e7d10..4cc0d1871d 100644
> > > > +--- a/src/lib/ares_conn.c
> > > > ++++ b/src/lib/ares_conn.c
> > > > +@@ -94,7 +94,9 @@ 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.
> > > > ++   * 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
> > > > +diff --git a/src/lib/ares_getaddrinfo.c b/src/lib/ares_getaddrinfo.c
> > > > +index 6d9e21af4b..5d9ddb311e 100644
> > > > +--- a/src/lib/ares_getaddrinfo.c
> > > > ++++ b/src/lib/ares_getaddrinfo.c
> > > > +@@ -762,7 +762,12 @@ void ares_getaddrinfo(ares_channel_t *channel,
> > > const char *name,
> > > > +     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 --git a/src/lib/ares_gethostbyaddr.c b/src/lib/ares_gethostbyaddr.c
> > > > +index 69c509ab11..b314359331 100644
> > > > +--- a/src/lib/ares_gethostbyaddr.c
> > > > ++++ b/src/lib/ares_gethostbyaddr.c
> > > > +@@ -112,7 +112,12 @@ void ares_gethostbyaddr(ares_channel_t *channel,
> > > const void *addr, int addrlen,
> > > > +     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 --git a/src/lib/ares_getnameinfo.c b/src/lib/ares_getnameinfo.c
> > > > +index 0159354530..7365e9ed82 100644
> > > > +--- a/src/lib/ares_getnameinfo.c
> > > > ++++ b/src/lib/ares_getnameinfo.c
> > > > +@@ -194,7 +194,12 @@ void ares_getnameinfo(ares_channel_t *channel,
> > > const struct sockaddr *sa,
> > > > +   }
> > > > +
> > > > +   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);
> > > > + }
> > > > +
> > > > +diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > > +index 0a1029e0f8..9610ba7dab 100644
> > > > +--- a/src/lib/ares_private.h
> > > > ++++ b/src/lib/ares_private.h
> > > > +@@ -676,6 +676,16 @@ void          ares_server_reap(ares_server_t *server);
> > > > +  *  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);
> > > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > > +index 82f53610fb..1569408b1b 100644
> > > > +--- a/src/lib/ares_process.c
> > > > ++++ b/src/lib/ares_process.c
> > > > +@@ -490,6 +490,11 @@ void ares_process_pending_write(ares_channel_t
> > > *channel)
> > > > +     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.
> > > > +@@ -513,6 +518,7 @@ void ares_process_pending_write(ares_channel_t
> > > *channel)
> > > > +     }
> > > > +   }
> > > > +
> > > > ++  ares_deferred_cb_leave(channel);
> > > > +   ares_channel_unlock(channel);
> > > > + }
> > > > +
> > > > +diff --git a/src/lib/ares_query.c b/src/lib/ares_query.c
> > > > +index ca3b6a9b73..0cf7f70760 100644
> > > > +--- a/src/lib/ares_query.c
> > > > ++++ b/src/lib/ares_query.c
> > > > +@@ -125,7 +125,12 @@ ares_status_t ares_query_dnsrec(ares_channel_t
> > > *channel, const char *name,
> > > > +   }
> > > > +
> > > > +   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 --git a/src/lib/ares_search.c b/src/lib/ares_search.c
> > > > +index c605caf42c..ebc3d91e20 100644
> > > > +--- a/src/lib/ares_search.c
> > > > ++++ b/src/lib/ares_search.c
> > > > +@@ -463,7 +463,12 @@ void ares_search(ares_channel_t *channel, const
> > > char *name, int dnsclass,
> > > > +   }
> > > > +
> > > > +   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_status_t ares_search_dnsrec(ares_channel_t
> > > *channel,
> > > > +   }
> > > > +
> > > > +   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 --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > > +index 2e0482bf19..dd4a28544b 100644
> > > > +--- a/src/lib/ares_send.c
> > > > ++++ b/src/lib/ares_send.c
> > > > +@@ -246,8 +246,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +   /* Perform the first query action. */
> > > > +
> > > > +   /* Sending opens and writes to connections and can mark a server down,
> > > both
> > > > +-   * of which notify.  Deliver those once we are done with the connection and
> > > > +-   * the server, not part-way through. */
> > > > ++   * 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);
> > > > +@@ -271,9 +275,14 @@ ares_status_t ares_send_dnsrec(ares_channel_t
> > > *channel,
> > > > +   }
> > > > +
> > > > +   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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > > +index 2158c5a913..b848c02434 100644
> > > > +--- a/test/ares-test-mock.cc
> > > > ++++ b/test/ares-test-mock.cc
> > > > +@@ -2533,6 +2533,112 @@ TEST_F(LibraryTest,
> > > ReconfigureInSockStateCallbackNoUseAfterFree) {
> > > > +   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<ares_channel_t *>(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<FailoverQueryData *>(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
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-
> > > callbacks-their-own-copy.patch b/meta-oe/recipes-support/c-ares/c-ares/0003-
> > > ares_qcache-hand-callbacks-their-own-copy.patch
> > > > new file mode 100644
> > > > index 0000000000..02039e92e0
> > > > --- /dev/null
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-
> > > callbacks-their-own-copy.patch
> > > > @@ -0,0 +1,1054 @@
> > > > +From a21160045f9d7039225fa285be7cb1b02173a3a5 Mon Sep 17 00:00:00
> > > 2001
> > > > +From: Brad House <brad@brad-house.com>
> > > > +Date: Sun, 18 Oct 2026 22:12:09 -0400
> > > > +Subject: [PATCH] [Backport v1.34] ares_qcache: hand callbacks their own
> > > copy
> > > > + of a cached answer (#1339)
> > > > +MIME-Version: 1.0
> > > > +Content-Type: text/plain; charset=UTF-8
> > > > +Content-Transfer-Encoding: 8bit
> > > > +
> > > > +Backport of 31e2f51e8715d418fa8272a6c3a1ee651917fec5 (`ares_qcache:
> > > hand
> > > > +callbacks their own copy of a cached answer`) to `v1.34`. See that
> > > > +commit for the full description.
> > > > +
> > > > +**Conflicts:**
> > > > +- `ares_private.h`: alignment only; formatted with `v1.34`'s
> > > > +`.clang-format`.
> > > > +- `test/ares-test-internal.cc`: context only — on `main` the new
> > > > +`QcacheKeyDistinguishesQuestions` test follows the IDNA/punycode tests
> > > > +(#1031), which are not on `v1.34`, so it is placed after the URI test
> > > > +instead.
> > > > +
> > > > +**Test fixes from #1334:** carries the part of #1334 that applies to the
> > > > +tests this introduces — `ReentrantFlushInCallbackNoUseAfterFree`
> > > > +reconfigures to `127.0.0.1:53055` instead of `127.0.0.55` (only routable
> > > > +on Linux), and `UnknownQueryTypesDoNotShareCacheEntries` drops
> > > > +`static_cast<ns_type>`, which does not compile on OpenBSD.
> > > > +
> > > > +Test file contents match `main`. Independent of the dd62bdfc backport
> > > > +(#1338); applies cleanly with or without it.
> > > > +
> > > > +The full non-live suite passes 650/650 under ASAN+UBSAN and with
> > > > +`-DCARES_WERROR=ON` on macOS.
> > > `ReentrantFlushInCallbackNoUseAfterFree`
> > > > +is a heap-use-after-free in `ares_dns_record_rr_cnt()` on `v1.34`
> > > > +without this change, and passes with it.
> > > > +
> > > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > > ares/commit/a21160045f9d7039225fa285be7cb1b02173a3a5]
> > > > +GHSA: GHSA-grv7-fq5q-9vfc, GHSA-wrgc-prmw-27xh
> > > > +---
> > > > + docs/ares_init_options.3   |  11 +-
> > > > + src/lib/ares_private.h     |  44 +++-
> > > > + src/lib/ares_process.c     |   1 +
> > > > + src/lib/ares_qcache.c      | 212 ++++++++++++++++---
> > > > + src/lib/ares_send.c        |  47 ++++-
> > > > + test/ares-test-internal.cc |  54 +++++
> > > > + test/ares-test-mock.cc     | 419 +++++++++++++++++++++++++++++++++++++
> > > > + 7 files changed, 743 insertions(+), 45 deletions(-)
> > > > +
> > > > +diff --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> > > > +index e6f6ee8ba6..1ce7fb8e30 100644
> > > > +--- a/docs/ares_init_options.3
> > > > ++++ b/docs/ares_init_options.3
> > > > +@@ -324,7 +324,16 @@ with the maximum number of seconds a query result
> > > may be cached which will
> > > > + 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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > > +index 9610ba7dab..dd06d303b8 100644
> > > > +--- a/src/lib/ares_private.h
> > > > ++++ b/src/lib/ares_private.h
> > > > +@@ -153,6 +153,9 @@ struct ares_query {
> > > > +   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
> > > > +@@ -718,14 +721,47 @@ 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);
> > > > ++/*! 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);
> > > > +-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);
> > > > ++/*! 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);
> > > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > > +index 1569408b1b..88536704ad 100644
> > > > +--- a/src/lib/ares_process.c
> > > > ++++ b/src/lib/ares_process.c
> > > > +@@ -1859,6 +1859,7 @@ void ares_free_query(ares_query_t *query)
> > > > +   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 --git a/src/lib/ares_qcache.c b/src/lib/ares_qcache.c
> > > > +index ad3bc83505..f48669b56c 100644
> > > > +--- a/src/lib/ares_qcache.c
> > > > ++++ b/src/lib/ares_qcache.c
> > > > +@@ -38,7 +38,7 @@ typedef struct {
> > > > +   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 @@ static char *ares_qcache_calc_key(const
> > > ares_dns_record_t *dnsrec)
> > > > +     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 @@ static char *ares_qcache_calc_key(const
> > > ares_dns_record_t *dnsrec)
> > > > +   }
> > > > +
> > > > +   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 @@ static char *ares_qcache_calc_key(const
> > > ares_dns_record_t *dnsrec)
> > > > +       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 @@ static char *ares_qcache_calc_key(const
> > > ares_dns_record_t *dnsrec)
> > > > +       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 @@ static char *ares_qcache_calc_key(const
> > > ares_dns_record_t *dnsrec)
> > > > +       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 @@ static char *ares_qcache_calc_key(const
> > > ares_dns_record_t *dnsrec)
> > > > +       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 @@ static unsigned int
> > > ares_qcache_soa_minimum(ares_dns_record_t *dnsrec)
> > > > + }
> > > > +
> > > > + /* 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 @@ static ares_status_t
> > > ares_qcache_insert_int(ares_qcache_t           *qcache,
> > > > +   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 */
> > > > +   }
> > > > +@@ -382,31 +514,25 @@ static ares_status_t
> > > ares_qcache_insert_int(ares_qcache_t           *qcache,
> > > > +   /* 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;
> > > > +@@ -416,10 +542,24 @@ ares_status_t ares_qcache_fetch(ares_channel_t
> > > *channel,
> > > > +   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;
> > > > + }
> > > > +
> > > > +@@ -428,13 +568,25 @@ ares_status_t ares_qcache_insert(ares_channel_t
> > > *channel,
> > > > +                                  const ares_query_t      *query,
> > > > +                                  const ares_dns_record_t *dnsrec)
> > > > + {
> > > > +-  ares_dns_record_t *dupdns = ares_dns_record_duplicate(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->query,
> > > now);
> > > > ++
> > > > ++  status =
> > > > ++    ares_qcache_insert_int(channel->qcache, dupdns, query->qcache_key,
> > > now);
> > > > +   if (status != ARES_SUCCESS) {
> > > > +     ares_dns_record_destroy(dupdns);
> > > > +   }
> > > > +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > > +index dd4a28544b..117745ce28 100644
> > > > +--- a/src/lib/ares_send.c
> > > > ++++ b/src/lib/ares_send.c
> > > > +@@ -111,11 +111,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +                                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
> > > > +@@ -145,13 +146,28 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +     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;
> > > > +     }
> > > > +   }
> > > > +@@ -159,8 +175,11 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +   /* 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));
> > > > +
> > > > +@@ -173,6 +192,10 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +     (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
> > > > +@@ -180,7 +203,11 @@ ares_status_t ares_send_nolock(ares_channel_t
> > > *channel, ares_server_t *server,
> > > > +     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;
> > > > +   }
> > > > +diff --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> > > > +index 8881f3b7cc..5ba5d2b39b 100644
> > > > +--- a/test/ares-test-internal.cc
> > > > ++++ b/test/ares-test-internal.cc
> > > > +@@ -502,6 +502,60 @@ TEST_F(LibraryTest, URI) {
> > > > +   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<std::string> 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];
> > > > ++    }
> > > > ++  }
> > > > ++}
> > > > ++
> > > > + #endif /* !CARES_SYMBOL_HIDING */
> > > > +
> > > > + TEST_F(LibraryTest, InetPtoN) {
> > > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > > +index b848c02434..4b409db7c3 100644
> > > > +--- a/test/ares-test-mock.cc
> > > > ++++ b/test/ares-test-mock.cc
> > > > +@@ -611,6 +611,425 @@ class CacheQueriesTest
> > > > +   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<QcacheReentrantCbData
> > > *>(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<ares_dns_rec_type_t>(29), QueryCallback, &r1,
> > > > ++                    nullptr);
> > > > ++  Process();
> > > > ++  EXPECT_TRUE(r1.done_);
> > > > ++
> > > > ++  QueryResult r2;
> > > > ++  ares_query_dnsrec(channel_, "example.net", ARES_CLASS_IN,
> > > > ++                    static_cast<ares_dns_rec_type_t>(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()
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-
> > > torn-down-by-application.patch b/meta-oe/recipes-support/c-ares/c-ares/0004-
> > > Handle-connections-torn-down-by-application.patch
> > > > new file mode 100644
> > > > index 0000000000..f78e9726b0
> > > > --- /dev/null
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-
> > > down-by-application.patch
> > > > @@ -0,0 +1,1107 @@
> > > > +From 13a10dddbabc30b6fca8e23f9efff73fedba9401 Mon Sep 17 00:00:00
> > > 2001
> > > > +From: Brad House <brad@brad-house.com>
> > > > +Date: Sun, 18 Oct 2026 22:51:53 -0400
> > > > +Subject: [PATCH] [Backport v1.34] Handle connections torn down by
> > > application
> > > > + socket functions (#1343)
> > > > +MIME-Version: 1.0
> > > > +Content-Type: text/plain; charset=UTF-8
> > > > +Content-Transfer-Encoding: 8bit
> > > > +
> > > > +Backport of cb25d8bf297aed0aad8f6e0777ca0635cacc6cb1 to `v1.34`. See
> > > > +that commit for the full description.
> > > > +
> > > > +Application code that c-ares calls while using a connection can
> > > > +reconfigure the servers and so tear that connection down. That includes
> > > > +the socket functions from `ares_set_socket_functions_ex()` and the older
> > > > +`ares_set_socket_functions()`, and the pending-write callback. c-ares
> > > > +now notices this while opening, writing to, reading from or closing a
> > > > +connection. It requeues the affected queries, or drops the stale data,
> > > > +instead of continuing to use the connection.
> > > > +
> > > > +It applied cleanly on top of the dd62bdfc and #1340 backports (#1338).
> > > > +Test file contents match `main`.
> > > > +
> > > > +Each of the 11 new tests fails on `v1.34` without this change: nine with
> > > > +an ASAN heap-use-after-free, and the two receive tests on their
> > > > +assertions. All 11 pass with it. The full non-live suite passes 674/674
> > > > +under ASAN+UBSAN and with `-DCARES_WERROR=ON` on macOS.
> > > > +
> > > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > > +Co-authored-by: Mar <mardgez13@gmail.com>
> > > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > > ares/commit/13a10dddbabc30b6fca8e23f9efff73fedba9401]
> > > > +GHSA: GHSA-pj7p-v3cw-hq66
> > > > +---
> > > > + src/lib/ares_close_sockets.c |   7 +-
> > > > + src/lib/ares_conn.c          |  29 ++
> > > > + src/lib/ares_process.c       |  65 ++-
> > > > + test/ares-test-mock.cc       | 842 +++++++++++++++++++++++++++++++++++
> > > > + 4 files changed, 935 insertions(+), 8 deletions(-)
> > > > +
> > > > +diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
> > > > +index 6be662fdd3..a3af721bc6 100644
> > > > +--- a/src/lib/ares_close_sockets.c
> > > > ++++ b/src/lib/ares_close_sockets.c
> > > > +@@ -148,6 +148,7 @@ void ares_check_cleanup_conns(const
> > > ares_channel_t *channel)
> > > > +     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)) {
> > > > +@@ -190,8 +191,12 @@ void ares_check_cleanup_conns(const
> > > ares_channel_t *channel)
> > > > +         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 --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > > +index 4cc0d1871d..5e0827fbdf 100644
> > > > +--- a/src/lib/ares_conn.c
> > > > ++++ b/src/lib/ares_conn.c
> > > > +@@ -262,6 +262,15 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > > *conn, void *data, size_t len,
> > > > +     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;
> > > > +@@ -408,6 +417,19 @@ ares_conn_err_t ares_conn_write(ares_conn_t
> > > *conn, const void *data, size_t len,
> > > > +   }
> > > > +
> > > > + 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
> > > > +@@ -644,6 +666,13 @@ ares_status_t ares_open_connection(ares_conn_t
> > > **conn_out,
> > > > +     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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > > +index 88536704ad..5686b077a6 100644
> > > > +--- a/src/lib/ares_process.c
> > > > ++++ b/src/lib/ares_process.c
> > > > +@@ -189,7 +189,9 @@ static void server_increment_failures(ares_server_t
> > > *server,
> > > > +   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 */
> > > > +   }
> > > > +
> > > > +@@ -220,7 +222,9 @@ static void server_set_good(ares_server_t *server,
> > > ares_bool_t used_tcp)
> > > > +   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 */
> > > > +   }
> > > > +
> > > > +@@ -501,21 +505,39 @@ void ares_process_pending_write(ares_channel_t
> > > *channel)
> > > > +    */
> > > > +   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);
> > > > +@@ -902,6 +924,13 @@ static ares_status_t process_read(ares_channel_t
> > > *channel,
> > > > +     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;
> > > > +@@ -1192,6 +1221,15 @@ static void handle_conn_error(ares_conn_t *conn,
> > > ares_bool_t critical_failure,
> > > > + {
> > > > +   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) {
> > > > +@@ -1508,6 +1546,14 @@ static ares_status_t
> > > ares_conn_query_write(ares_conn_t          *conn,
> > > > +       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;
> > > > +   }
> > > > +
> > > > +@@ -1650,6 +1696,11 @@ static ares_status_t
> > > ares_send_query_int(ares_server_t        *requested_server,
> > > > +     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:
> > > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > > +index 4b409db7c3..f1d9d360e7 100644
> > > > +--- a/test/ares-test-mock.cc
> > > > ++++ b/test/ares-test-mock.cc
> > > > +@@ -2593,6 +2593,86 @@ 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<int> {
> > > > ++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<bool *>(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_;
> > > > +@@ -2898,6 +2978,768 @@ TEST_F(LibraryTest,
> > > ReconfigureInConfigureHookNoUseAfterFree) {
> > > > +   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<ReconfigureFromSendData *>(user_data);
> > > > ++  ares_ssize_t len = 0;
> > > > ++  (void)fd;
> > > > ++  for (int i = 0; i < iovcnt; i++) {
> > > > ++    len += static_cast<ares_ssize_t>(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<ReconfigureFromSendData
> > > *>(arg);
> > > > ++  (void)timeouts;
> > > > ++  (void)dnsrec;
> > > > ++  d->done   = true;
> > > > ++  d->status = static_cast<int>(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<BadSocketStateData *>(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<PendingWriteReconfigureData *>(user_data);
> > > > ++  ares_ssize_t len = 0;
> > > > ++  (void)fd;
> > > > ++  for (int i = 0; i < iovcnt; i++) {
> > > > ++    len += static_cast<ares_ssize_t>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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;
> > > > ++  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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<unsigned char *>(buffer)[2] |= 0x80;
> > > > ++  if (address != NULL && address_len != NULL &&
> > > > ++      *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > > ++    memcpy(address, &d->peer, sizeof(d->peer));
> > > > ++    *address_len = sizeof(d->peer);
> > > > ++  }
> > > > ++  return static_cast<ares_ssize_t>(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<FakeSockData *>(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<ares_ssize_t>(length);
> > > > ++}
> > > > ++
> > > > ++static int FakeSockGetsockname(ares_socket_t sock, struct sockaddr
> > > *address,
> > > > ++                               ares_socklen_t *address_len, void *user_data)
> > > > ++{
> > > > ++  FakeSockData      *d = static_cast<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(arg);
> > > > ++  (void)timeouts;
> > > > ++  (void)dnsrec;
> > > > ++  d->done   = true;
> > > > ++  d->status = static_cast<int>(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);
> > > > ++}
> > > > ++
> > > > ++// 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
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-
> > > accept-an-answer-on-the-right-connection.patch b/meta-oe/recipes-support/c-
> > > ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-
> > > connection.patch
> > > > new file mode 100644
> > > > index 0000000000..20fef5d2f0
> > > > --- /dev/null
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-
> > > an-answer-on-the-right-connection.patch
> > > > @@ -0,0 +1,575 @@
> > > > +From c8bd9ab53329f66ec0c395a11002a5696669b147 Mon Sep 17 00:00:00
> > > 2001
> > > > +From: Brad House <brad@brad-house.com>
> > > > +Date: Mon, 19 Oct 2026 11:53:54 -0400
> > > > +Subject: [PATCH] [Backport v1.34] ares_process: only accept an answer on the
> > > > + connection its query was sent on (#1361)
> > > > +MIME-Version: 1.0
> > > > +Content-Type: text/plain; charset=UTF-8
> > > > +Content-Transfer-Encoding: 8bit
> > > > +
> > > > +Backport of b6f8a9b47e17c681e092a74d05d73bbb35c89a39 to `v1.34`. See
> > > > +that commit for the full description.
> > > > +
> > > > +**Conflicts:** `src/lib/ares_socket.h` only — v1.34 aligns the
> > > > +neighbouring prototypes differently. Resolved by taking the new doc
> > > > +comments and the `ares_sockaddr_addr_port_eq()` prototype, then
> > > > +re-running clang-format for the v1.34 alignment (which also re-wraps the
> > > > +new `SendData()` declaration in `test/ares-test.h`). All other files
> > > > +applied unchanged; the `test/` diffs match main exactly.
> > > > +
> > > > +**Tests (local, macOS, Debug, Ninja):**
> > > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > > +--gtest_filter='-*Live*'` — 693/693 passed in both.
> > > > +- With the `src/` changes reverted, the 4 new test instances fail
> > > > +(`UDPAnswerFromWrongPortDropped`,
> > > `AnswerOnOtherServerConnectionDropped`
> > > > +UDP/TCP, `CookielessAnswerOnOtherServerDropped`); they pass with them.
> > > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > > > +passed under ASAN+UBSAN and `-Werror`.
> > > > +
> > > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > > ares/commit/c8bd9ab53329f66ec0c395a11002a5696669b147]
> > > > +GHSA: GHSA-fqh5-3x7m-wp44
> > > > +---
> > > > + src/lib/ares_conn.c    |  11 ++-
> > > > + src/lib/ares_process.c |  18 ++++
> > > > + src/lib/ares_socket.c  |  24 +++++
> > > > + src/lib/ares_socket.h  |  21 ++++
> > > > + test/ares-test-mock.cc | 218
> > > +++++++++++++++++++++++++++++++++++++++++
> > > > + test/ares-test.cc      |  86 +++++++++-------
> > > > + test/ares-test.h       |  12 +++
> > > > + 7 files changed, 355 insertions(+), 35 deletions(-)
> > > > +
> > > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > > +index 5e0827fbdf..b868053e62 100644
> > > > +--- a/src/lib/ares_conn.c
> > > > ++++ b/src/lib/ares_conn.c
> > > > +@@ -252,9 +252,16 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > > *conn, void *data, size_t len,
> > > > +                            (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. */
> > > > +     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
> > > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > > +index 5686b077a6..06d0ca507c 100644
> > > > +--- a/src/lib/ares_process.c
> > > > ++++ b/src/lib/ares_process.c
> > > > +@@ -1117,6 +1117,24 @@ static ares_status_t
> > > process_answer(ares_channel_t      *channel,
> > > > +     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)
> > > > +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> > > > +index 4ad3034021..0a12935ab7 100644
> > > > +--- a/src/lib/ares_socket.c
> > > > ++++ b/src/lib/ares_socket.c
> > > > +@@ -127,6 +127,30 @@ ares_bool_t ares_sockaddr_addr_eq(const struct
> > > sockaddr  *sa,
> > > > +   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;
> > > > ++  }
> > > > ++
> > > > ++  if (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,
> > > > +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> > > > +index 24a99ab331..f7128e3493 100644
> > > > +--- a/src/lib/ares_socket.h
> > > > ++++ b/src/lib/ares_socket.h
> > > > +@@ -139,8 +139,29 @@ typedef enum {
> > > > +   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.
> > > > ++ *
> > > > ++ *  \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, IP addresses and ports are
> > > > ++ *          equal, 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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > > +index d233f9070f..058dc93001 100644
> > > > +--- a/test/ares-test-mock.cc
> > > > ++++ b/test/ares-test-mock.cc
> > > > +@@ -3363,6 +3363,7 @@ struct FakeSockData {
> > > > +   unsigned char      query[512];
> > > > +   size_t             query_len;
> > > > +   struct sockaddr_in peer;
> > > > ++  unsigned short     recv_port; /* network order; 0 reports the peer's port */
> > > > +   size_t             recv_calls;
> > > > +   size_t             bad_fd_calls;
> > > > +   size_t             sock_state_bad;
> > > > +@@ -3471,6 +3472,9 @@ static ares_ssize_t
> > > FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> > > > +   if (address != NULL && address_len != NULL &&
> > > > +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > > +     memcpy(address, &d->peer, sizeof(d->peer));
> > > > ++    if (d->recv_port != 0) {
> > > > ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
> > > > ++    }
> > > > +     *address_len = sizeof(d->peer);
> > > > +   }
> > > > +   return static_cast<ares_ssize_t>(d->query_len);
> > > > +@@ -3710,6 +3714,42 @@ TEST_F(LibraryTest,
> > > ReconfigureInRecvDropsBufferedAnswer) {
> > > > +   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);
> > > > ++}
> > > > ++
> > > > + // 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.
> > > > +@@ -5530,5 +5570,183 @@
> > > INSTANTIATE_TEST_SUITE_P(TransportModes, ServerRecoveryMultiMockTest,
> > > ::testing:
> > > > +
> > > > + 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<int, bool> > {
> > > > ++ 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<byte>());
> > > > ++    }));
> > > > ++  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<int> {
> > > > ++ 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<byte> 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<byte>());
> > > > ++    }));
> > > > ++  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 --git a/test/ares-test.cc b/test/ares-test.cc
> > > > +index d333ee9419..0b3ac12c09 100644
> > > > +--- a/test/ares-test.cc
> > > > ++++ b/test/ares-test.cc
> > > > +@@ -448,6 +448,9 @@ MockServer::MockServer(int family, unsigned short
> > > port)
> > > > +   // 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;
> > > > +@@ -710,6 +713,50 @@ std::set<ares_socket_t> MockServer::fds() const {
> > > > +   return result;
> > > > + }
> > > > +
> > > > ++void MockServer::SendData(ares_socket_t fd, struct sockaddr_storage *addr,
> > > > ++                          ares_socklen_t addrlen, int qid,
> > > > ++                          std::vector<byte> 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<byte> 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<ares_ssize_t>(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<byte> &req,
> > > > +                                 const std::string &reqstr,
> > > > +@@ -717,10 +764,14 @@ void MockServer::ProcessRequest(ares_socket_t
> > > fd, struct sockaddr_storage* addr,
> > > > +
> > > > +   /* 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);
> > > > +
> > > > +@@ -747,43 +798,12 @@ void MockServer::ProcessRequest(ares_socket_t
> > > fd, struct sockaddr_storage* addr,
> > > > +     return;
> > > > +   }
> > > > +
> > > > +-  // Make a local copy of the current pending reply.
> > > > +-  std::vector<byte> 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<byte> 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<ares_ssize_t>(reply.size())) {
> > > > +-    std::cerr << "Failed to send full reply, rc=" << rc << std::endl;
> > > > +-  }
> > > > ++  SendData(fd, addr, addrlen, qid, exact_reply_);
> > > > +
> > > > +   if (disconnect_after_reply_ && fd != udpfd_) {
> > > > +     disconnect_after_reply_ = false;
> > > > +diff --git a/test/ares-test.h b/test/ares-test.h
> > > > +index 2b973c3982..170c284f26 100644
> > > > +--- a/test/ares-test.h
> > > > ++++ b/test/ares-test.h
> > > > +@@ -303,6 +303,12 @@ class MockServer {
> > > > +     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<ares_socket_t> fds() const;
> > > > +
> > > > +@@ -327,6 +333,8 @@ class MockServer {
> > > > +                                 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<byte> reply);
> > > > +   unsigned short udpport_;
> > > > +   unsigned short tcpport_;
> > > > +   ares_socket_t  udpfd_;
> > > > +@@ -339,6 +347,10 @@ class MockServer {
> > > > +   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.
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-
> > > channel-config-under-lock.patch b/meta-oe/recipes-support/c-ares/c-ares/0006-
> > > ares_save_options-read-channel-config-under-lock.patch
> > > > new file mode 100644
> > > > index 0000000000..76c072dd7e
> > > > --- /dev/null
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-
> > > channel-config-under-lock.patch
> > > > @@ -0,0 +1,243 @@
> > > > +From 25a6911370878d4b617b44b142ea0e49d1ccede6 Mon Sep 17
> > > 00:00:00 2001
> > > > +From: Brad House <brad@brad-house.com>
> > > > +Date: Mon, 19 Oct 2026 11:53:59 -0400
> > > > +Subject: [PATCH] [Backport v1.34] ares_save_options: read channel
> > > > + configuration under the channel lock (#1362)
> > > > +MIME-Version: 1.0
> > > > +Content-Type: text/plain; charset=UTF-8
> > > > +Content-Transfer-Encoding: 8bit
> > > > +
> > > > +Backport of 5d878a580a8b57b51d15e2725cf6775c8e7bca0e to `v1.34`. See
> > > > +that commit for the full description.
> > > > +
> > > > +**Conflicts:** none; cherry-picked cleanly.
> > > > +
> > > > +**Tests (local, macOS, Debug, Ninja):**
> > > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > > +--gtest_filter='-*Live*'` — 691/691 passed in both.
> > > > +- With the `src/` changes reverted, the new `DupConcurrentSetServers` /
> > > > +`SaveOptionsConcurrentSetServers` tests abort with an ASAN
> > > > +heap-buffer-overflow; they pass with them.
> > > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > > > +passed under ASAN+UBSAN and `-Werror`.
> > > > +
> > > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > > ares/commit/25a6911370878d4b617b44b142ea0e49d1ccede6]
> > > > +GHSA: GHSA-ph6c-3cvx-mq6v
> > > > +---
> > > > + src/lib/ares_init.c    |  3 +-
> > > > + src/lib/ares_options.c | 45 ++++++++++++++++++----
> > > > + test/ares-test-init.cc | 87
> > > ++++++++++++++++++++++++++++++++++++++++++
> > > > + 3 files changed, 126 insertions(+), 9 deletions(-)
> > > > +
> > > > +diff --git a/src/lib/ares_init.c b/src/lib/ares_init.c
> > > > +index 4758c5de56..85b2e13050 100644
> > > > +--- a/src/lib/ares_init.c
> > > > ++++ b/src/lib/ares_init.c
> > > > +@@ -483,7 +483,8 @@ int ares_dup(ares_channel_t **dest, const
> > > ares_channel_t *src)
> > > > +   *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);
> > > > +diff --git a/src/lib/ares_options.c b/src/lib/ares_options.c
> > > > +index 2b5624bbea..5ea35b732e 100644
> > > > +--- a/src/lib/ares_options.c
> > > > ++++ b/src/lib/ares_options.c
> > > > +@@ -55,8 +55,8 @@ static struct in_addr *ares_save_opt_servers(const
> > > ares_channel_t *channel,
> > > > +                                              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 @@ static struct in_addr *ares_save_opt_servers(const
> > > ares_channel_t *channel,
> > > > +     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 @@ static struct in_addr *ares_save_opt_servers(const
> > > ares_channel_t *channel,
> > > > +   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 @@ int ares_save_options(const ares_channel_t
> > > *channel,
> > > > +   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 @@ int ares_save_options(const ares_channel_t
> > > *channel,
> > > > +   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 @@ int ares_save_options(const ares_channel_t
> > > *channel,
> > > > +   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)
> > > > +diff --git a/test/ares-test-init.cc b/test/ares-test-init.cc
> > > > +index b44211a1e4..1c5d0f2171 100644
> > > > +--- a/test/ares-test-init.cc
> > > > ++++ b/test/ares-test-init.cc
> > > > +@@ -25,6 +25,8 @@
> > > > +  */
> > > > + #include "ares-test.h"
> > > > +
> > > > ++#include <atomic>
> > > > ++
> > > > + extern "C" {
> > > > +   #include "ares_private.h"
> > > > + }
> > > > +@@ -295,6 +297,91 @@ TEST_F(LibraryTest, FailChannelInit) {
> > > > +   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<size_t> 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<std::thread> 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;
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-
> > > prefix-length.patch b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-
> > > reject-a-prefix-length.patch
> > > > new file mode 100644
> > > > index 0000000000..2cd1ea8c24
> > > > --- /dev/null
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-
> > > prefix-length.patch
> > > > @@ -0,0 +1,364 @@
> > > > +From 7e65b381c0b915aa5671ddc713080c87e15fc42f Mon Sep 17 00:00:00
> > > 2001
> > > > +From: Brad House <brad@brad-house.com>
> > > > +Date: Mon, 19 Oct 2026 12:24:52 -0400
> > > > +Subject: [PATCH] [Backport v1.34] ares_inet_pton: reject a prefix length,
> > > > + always write the full address (#1363)
> > > > +MIME-Version: 1.0
> > > > +Content-Type: text/plain; charset=UTF-8
> > > > +Content-Transfer-Encoding: 8bit
> > > > +
> > > > +Backport of 7b4ef8e555b611be6af39d913aa280cd1a348102 to `v1.34`. See
> > > > +that commit for the full description.
> > > > +
> > > > +**Conflicts:** `test/ares-test-mock-ai.cc` only — the neighbouring
> > > > +`FamilyV6IgnoresARecords` test (added on main by an unrelated change)
> > > > +does not exist on v1.34. Resolved by adding only the new
> > > > +`NumericHostRejectsPrefixLength` test at the same position. Other files
> > > > +applied unchanged.
> > > > +
> > > > +**Also includes** the comment spelling fix from #1364
> > > > +(ead5557d1301328536c3920bd01759a1bff5ca9a): "unparseable" ->
> > > > +"unparsable" in `src/lib/inet_net_pton.c` and
> > > > +`test/ares-test-internal.cc`, so Codespell passes and the text matches
> > > > +main.
> > > > +
> > > > +**Tests (local, macOS, Debug, Ninja):**
> > > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > > +--gtest_filter='-*Live*'` — 693/693 passed in both.
> > > > +- With the `src/` changes reverted, the 4 new test instances fail
> > > > +(`InetNetPtoNPrefixZeroFills`, `InetPtoNRejectsPrefixLength`,
> > > > +`NumericHostRejectsPrefixLength` UDP/TCP); they pass with them.
> > > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > > +with the other two v1.34 backports (both orders apply cleanly): 699/699
> > > > +passed under ASAN+UBSAN and `-Werror`.
> > > > +
> > > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > > ares/commit/7e65b381c0b915aa5671ddc713080c87e15fc42f]
> > > > +GHSA: GHSA-cfcv-8v2j-qj6r
> > > > +---
> > > > + docs/ares_inet_pton.3        | 18 ++++++++--
> > > > + src/lib/ares_inet_net_pton.h | 28 ++++++++++++++++
> > > > + src/lib/inet_net_pton.c      | 65 ++++++++++++++++++++----------------
> > > > + test/ares-test-internal.cc   | 54 ++++++++++++++++++++++++++++++
> > > > + test/ares-test-mock-ai.cc    | 23 +++++++++++++
> > > > + 5 files changed, 157 insertions(+), 31 deletions(-)
> > > > +
> > > > +diff --git a/docs/ares_inet_pton.3 b/docs/ares_inet_pton.3
> > > > +index 34b2df063c..19c5fdfda3 100644
> > > > +--- a/docs/ares_inet_pton.3
> > > > ++++ b/docs/ares_inet_pton.3
> > > > +@@ -12,8 +12,9 @@ ares_inet_pton \- convert an IPv4 or IPv6 address from
> > > text to binary form
> > > > + 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 @@ in. The \fBdst\fP argument points to a buffer into which
> > > the function stores
> > > > + 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 --git a/src/lib/ares_inet_net_pton.h b/src/lib/ares_inet_net_pton.h
> > > > +index e3ed83a3cb..e1a6f2d71e 100644
> > > > +--- a/src/lib/ares_inet_net_pton.h
> > > > ++++ b/src/lib/ares_inet_net_pton.h
> > > > +@@ -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 --git a/src/lib/inet_net_pton.c b/src/lib/inet_net_pton.c
> > > > +index e1f76ef834..f60823023b 100644
> > > > +--- a/src/lib/inet_net_pton.c
> > > > ++++ b/src/lib/inet_net_pton.c
> > > > +@@ -51,14 +51,15 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0,
> > > 0, 0, 0, 0, 0, 0, 0, 0,
> > > > +
> > > > + /*
> > > > +  * 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 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0, 0,
> > > 0, 0, 0, 0, 0, 0, 0,
> > > > +  *      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 @@ static int ares_inet_net_pton_ipv4(const char *src,
> > > unsigned char *dst,
> > > > +   }
> > > > +
> > > > +   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 @@ static int ares_inet_pton6(const char *src, unsigned
> > > char *dst)
> > > > +       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 @@ static int ares_inet_net_pton_ipv6(const char *src,
> > > unsigned char *dst,
> > > > +     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 ares_inet_net_pton(int af, const char *src, void
> > > *dst, size_t size)
> > > > +   }
> > > > + }
> > > > +
> > > > ++/*
> > > > ++ * 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 --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> > > > +index bf06a74605..294e3f4555 100644
> > > > +--- a/test/ares-test-internal.cc
> > > > ++++ b/test/ares-test-internal.cc
> > > > +@@ -274,6 +274,23 @@ TEST_F(LibraryTest, InetNetPtoN) {
> > > > +   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++) {
> > > > +@@ -600,9 +617,46 @@ TEST_F(LibraryTest, InetPtoN) {
> > > > +   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.
> > > > +diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc
> > > > +index ae45c45527..43d0b89175 100644
> > > > +--- a/test/ares-test-mock-ai.cc
> > > > ++++ b/test/ares-test-mock-ai.cc
> > > > +@@ -617,6 +617,29 @@ TEST_P(MockChannelTestAI, FamilyV6) {
> > > > +   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<byte> nothing;
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-
> > > drop-UDP-answers-from-server-port.patch b/meta-oe/recipes-support/c-ares/c-
> > > ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> > > > new file mode 100644
> > > > index 0000000000..f4f182b995
> > > > --- /dev/null
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-
> > > UDP-answers-from-server-port.patch
> > > > @@ -0,0 +1,178 @@
> > > > +From c7fa9ebcb0c0086730c53bf03191c03f03706246 Mon Sep 17 00:00:00
> > > 2001
> > > > +From: "c-ares-backport-gha[bot]"
> > > > + <299594594+c-ares-backport-gha[bot]@users.noreply.github.com>
> > > > +Date: Tue, 20 Oct 2026 02:25:59 +0000
> > > > +Subject: [PATCH] [Backport v1.34] ares_conn_read: don't drop UDP answers
> > > whose
> > > > + source port isn't reported (#1371)
> > > > +MIME-Version: 1.0
> > > > +Content-Type: text/plain; charset=UTF-8
> > > > +Content-Transfer-Encoding: 8bit
> > > > +
> > > > +# Description
> > > > +Backport of #1370 to `v1.34`.
> > > > +
> > > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > > +Co-authored-by: Brad House <brad@brad-house.com>
> > > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > > ares/commit/c7fa9ebcb0c0086730c53bf03191c03f03706246]
> > > > +GHSA: GHSA-fqh5-3x7m-wp44
> > > > +---
> > > > + docs/ares_set_socket_functions.3 | 20 ++++++++++++++++++++
> > > > + src/lib/ares_conn.c              |  9 ++++++++-
> > > > + src/lib/ares_socket.c            |  4 +++-
> > > > + src/lib/ares_socket.h            |  8 +++++---
> > > > + test/ares-test-mock.cc           | 32 +++++++++++++++++++++++++++++++-
> > > > + 5 files changed, 67 insertions(+), 6 deletions(-)
> > > > +
> > > > +diff --git a/docs/ares_set_socket_functions.3
> > > b/docs/ares_set_socket_functions.3
> > > > +index 605beb26ef..75c621cb48 100644
> > > > +--- a/docs/ares_set_socket_functions.3
> > > > ++++ b/docs/ares_set_socket_functions.3
> > > > +@@ -149,6 +149,20 @@ a socket. Additional flags controlling behavior are in
> > > > + 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 @@ Receives data from remote socket endpoint, if
> > > available. If the \fIaddr\fP
> > > > + 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
> > > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > > +index b868053e62..7711d8649e 100644
> > > > +--- a/src/lib/ares_conn.c
> > > > ++++ b/src/lib/ares_conn.c
> > > > +@@ -257,7 +257,14 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > > *conn, void *data, size_t len,
> > > > +      * 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. */
> > > > ++     * 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_port_eq((struct sockaddr *)&sa_storage,
> > > > +                                     &conn->server->addr,
> > > > +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> > > > +index 0a12935ab7..29d595e1a9 100644
> > > > +--- a/src/lib/ares_socket.c
> > > > ++++ b/src/lib/ares_socket.c
> > > > +@@ -144,7 +144,9 @@ ares_bool_t ares_sockaddr_addr_port_eq(const
> > > struct sockaddr  *sa,
> > > > +     sa_port = (CARES_INADDR_CAST(const struct sockaddr_in6 *, sa))-
> > > >sin6_port;
> > > > +   }
> > > > +
> > > > +-  if (ntohs(sa_port) != 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;
> > > > +   }
> > > > +
> > > > +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> > > > +index f7128e3493..1ede6987ee 100644
> > > > +--- a/src/lib/ares_socket.h
> > > > ++++ b/src/lib/ares_socket.h
> > > > +@@ -151,13 +151,15 @@ 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.
> > > > ++ *  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, IP addresses and ports are
> > > > +- *          equal, ARES_FALSE otherwise
> > > > ++ *  \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,
> > > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > > +index 058dc93001..cc5e6482f2 100644
> > > > +--- a/test/ares-test-mock.cc
> > > > ++++ b/test/ares-test-mock.cc
> > > > +@@ -3364,6 +3364,7 @@ struct FakeSockData {
> > > > +   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;
> > > > +@@ -3472,7 +3473,9 @@ static ares_ssize_t
> > > FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> > > > +   if (address != NULL && address_len != NULL &&
> > > > +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > > +     memcpy(address, &d->peer, sizeof(d->peer));
> > > > +-    if (d->recv_port != 0) {
> > > > ++    if (d->recv_no_port) {
> > > > ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = 0;
> > > > ++    } else if (d->recv_port != 0) {
> > > > +       reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
> > > > +     }
> > > > +     *address_len = sizeof(d->peer);
> > > > +@@ -3750,6 +3753,33 @@ TEST_F(LibraryTest,
> > > UDPAnswerFromWrongPortDropped) {
> > > > +   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.
> > > > diff --git a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb b/meta-oe/recipes-
> > > support/c-ares/c-ares_1.34.8.bb
> > > > index 058f3666e7..1d0f3eb343 100644
> > > > --- a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> > > > +++ b/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> > > > @@ -6,6 +6,14 @@ LICENSE = "MIT"
> > > >  LIC_FILES_CHKSUM =
> > > "file://LICENSE.md;md5=d3e72a10e08191f2ca1be3f3228d78f3"
> > > >
> > > >  SRC_URI = "https://github.com/c-ares/c-
> > > ares/releases/download/v${PV}/${BPN}-${PV}.tar.gz \
> > > > +           file://0001-ares_cancel-ares_destroy-detach-queries-before-
> > > callbacks.patch \
> > > > +           file://0002-Deliver-deferred-socket-server-state-notifications.patch \
> > > > +           file://0003-ares_qcache-hand-callbacks-their-own-copy.patch \
> > > > +           file://0004-Handle-connections-torn-down-by-application.patch \
> > > > +           file://0005-ares_process-only-accept-an-answer-on-the-right-
> > > connection.patch \
> > > > +           file://0006-ares_save_options-read-channel-config-under-lock.patch \
> > > > +           file://0007-ares_inet_pton-reject-a-prefix-length.patch \
> > > > +           file://0008-ares_conn_read-dont-drop-UDP-answers-from-server-
> > > port.patch \
> > > >             file://run-ptest"
> > > >  SRC_URI[sha256sum] =
> > > "c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78"
> > > >
> > > > --
> > > > 2.43.0
> > > >
> > > >
> > > >
> > > >
J. S. Oct. 27, 2026, 11:08 a.m. UTC | #5
I think the change in question is really very minor.  I suspect that taking
the upgrade to 1.34..9 as presented in master would make support long term
easier.  But I will leave it for branch maintainers to decide what's
acceptable.

Jason

On Tue, Oct 27, 2026 at 4:25 AM Marko, Peter <Peter.Marko@siemens.com>
wrote:

> Can we rather do the upgrade instead? I was planning to send it for
> Wrynose branch eventually.
> Is there anything in "Changes" section of
> https://github.com/c-ares/c-ares/releases/tag/v1.34.9 which would prevent
> it?
>
> Peter
>
> > -----Original Message-----
> > From: openembedded-devel@lists.openembedded.org <openembedded-
> > devel@lists.openembedded.org> On Behalf Of Ankur Tyagi via
> > lists.openembedded.org
> > Sent: Tuesday, October 27, 2026 8:04 AM
> > To: schonm@gmail.com
> > Cc: openembedded-devel@lists.openembedded.org
> > Subject: Re: [oe] [meta-oe][blacksail][PATCH] c-ares: backport security
> fixes from
> > 1.34.9
> >
> > Hi Jason,
> >
> > Thanks for submitting the patch.
> >
> > Though the patches LGTM, I am seeing increased ptest failures.
> >
> > Existing recipe ptest status:
> > [==========] 1093 tests from 41 test suites ran.
> > [  PASSED  ] 1092 tests.
> > [  FAILED  ] 1 test, listed below:
> > [  FAILED  ] DefaultChannelTest.TimeoutValue
> >
> > ptest status after applying your patch:
> > [==========] 1174 tests from 47 test suites ran.
> > [  PASSED  ] 1170 tests.
> > [  FAILED  ] 4 tests, listed below:
> > [  FAILED  ] DefaultChannelTest.TimeoutValue
> > [  FAILED  ] SockStateDeliveryTest.CancelReportsSocketClose
> > [  FAILED  ] SockStateDeliveryTest.SetServersReportsSocketClose
> > [  FAILED  ] SockStateDeliveryTest.DestroyReportsSocketClose
> >
> > Perhaps some other commits also need backporting?
> > Can please look into this?
> >
> > cheers
> > Ankur
> >
> > On Tue, Oct 27, 2026 at 2:22 PM J. S. via lists.openembedded.org
> > <schonm=gmail.com@lists.openembedded.org> wrote:
> > >
> > > Add the eight unique upstream commits that fix the security advisories
> > addressed in c-ares 1.34.9.
> > > Several advisories share commits, so patches are ordered by commit
> rather
> > than duplicated per advisory; each patch lists the GHSA IDs it resolves.
> > >
> > > Assisted-by: opencode (big-pickle)
> > > Signed-off-by: Jason Schonberg <schonm@gmail.com>
> > > ---
> > >  ...troy-detach-queries-before-callbacks.patch | 2603 +++++++++++++++++
> > >  ...ed-socket-server-state-notifications.patch |  393 +++
> > >  ...qcache-hand-callbacks-their-own-copy.patch | 1054 +++++++
> > >  ...connections-torn-down-by-application.patch | 1107 +++++++
> > >  ...pt-an-answer-on-the-right-connection.patch |  575 ++++
> > >  ...tions-read-channel-config-under-lock.patch |  243 ++
> > >  ...res_inet_pton-reject-a-prefix-length.patch |  364 +++
> > >  ...nt-drop-UDP-answers-from-server-port.patch |  178 ++
> > >  .../recipes-support/c-ares/c-ares_1.34.8.bb   |    8 +
> > >  9 files changed, 6525 insertions(+)
> > >  create mode 100644
> meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-
> > ares_destroy-detach-queries-before-callbacks.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-
> > deferred-socket-server-state-notifications.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0003-
> > ares_qcache-hand-callbacks-their-own-copy.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0004-Handle-
> > connections-torn-down-by-application.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0005-
> > ares_process-only-accept-an-answer-on-the-right-connection.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0006-
> > ares_save_options-read-channel-config-under-lock.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0007-
> > ares_inet_pton-reject-a-prefix-length.patch
> > >  create mode 100644 meta-oe/recipes-support/c-ares/c-ares/0008-
> > ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> > >
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-
> > ares_destroy-detach-queries-before-callbacks.patch b/meta-oe/recipes-
> >
> support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-
> > callbacks.patch
> > > new file mode 100644
> > > index 0000000000..385760f5bc
> > > --- /dev/null
> > > +++
> b/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-
> > detach-queries-before-callbacks.patch
> > > @@ -0,0 +1,2603 @@
> > > +From 7f408deb128fbdcce78122322734ffaa01cad9ae Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Sun, 18 Oct 2026 18:15:41 -0400
> > > +Subject: [PATCH] ares_cancel/ares_destroy: detach queries before
> running
> > user
> > > + callbacks
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of dd62bdfca52ba5c37e88a1469c0a4c5e4a568ccb to v1.34.  See
> > that
> > > +commit for the full description; it hardens reentrant teardown from
> > > +callbacks (ares_cancel(), ares_destroy(), server reconfiguration, and
> > > +socket/server state notifications).
> > > +
> > > +Conflicts were whitespace/alignment only (v1.34 predates the
> clang-format
> > > +reformat in #1238) and were reformatted to v1.34's style.
> > > +
> > > +Also carries the test portion of #1334 that applies to the tests this
> > > +introduces: dead-server tests use 127.0.0.1:53055/53056 instead of
> > > +127.0.0.55/56, which only routes on Linux.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/7f408deb128fbdcce78122322734ffaa01cad9ae]
> > > +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-
> > 2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> > > +---
> > > + docs/ares_cancel.3                    |   17 +
> > > + docs/ares_destroy.3                   |    7 +
> > > + docs/ares_init_options.3              |   15 +
> > > + docs/ares_set_server_state_callback.3 |   31 +-
> > > + docs/ares_set_servers_csv.3           |   17 +
> > > + src/lib/ares_cancel.c                 |   25 +-
> > > + src/lib/ares_close_sockets.c          |   82 +-
> > > + src/lib/ares_conn.c                   |  217 ++++-
> > > + src/lib/ares_conn.h                   |   37 +-
> > > + src/lib/ares_destroy.c                |   85 +-
> > > + src/lib/ares_private.h                |  110 ++-
> > > + src/lib/ares_process.c                |  235 +++++-
> > > + src/lib/ares_send.c                   |   35 +
> > > + src/lib/ares_update_servers.c         |   42 +-
> > > + test/ares-test-mock.cc                | 1068
> +++++++++++++++++++++++++
> > > + 15 files changed, 1959 insertions(+), 64 deletions(-)
> > > +
> > > +diff --git a/docs/ares_cancel.3 b/docs/ares_cancel.3
> > > +index 967a946b49..069f226c10 100644
> > > +--- a/docs/ares_cancel.3
> > > ++++ b/docs/ares_cancel.3
> > > +@@ -21,6 +21,23 @@ These calls give the callbacks a chance to clean up
> > any state which might have
> > > + 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 --git a/docs/ares_destroy.3 b/docs/ares_destroy.3
> > > +index d1984b7cc9..0b6380e609 100644
> > > +--- a/docs/ares_destroy.3
> > > ++++ b/docs/ares_destroy.3
> > > +@@ -20,6 +20,13 @@ by the channel.
> > > + 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 --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> > > +index 9b3b481535..e6f6ee8ba6 100644
> > > +--- a/docs/ares_init_options.3
> > > ++++ b/docs/ares_init_options.3
> > > +@@ -231,6 +231,21 @@ will be passed the socket whose state has
> changed;
> > > + 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
> > > +diff --git a/docs/ares_set_server_state_callback.3
> > b/docs/ares_set_server_state_callback.3
> > > +index 04fbde4c7a..e95db327ce 100644
> > > +--- a/docs/ares_set_server_state_callback.3
> > > ++++ b/docs/ares_set_server_state_callback.3
> > > +@@ -47,6 +47,31 @@ the DNS servers used by an ares channel. For
> > example, it can be used to track
> > > + 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 @@ Indicates that the query was tried over TCP.
> > > + .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 --git a/docs/ares_set_servers_csv.3 b/docs/ares_set_servers_csv.3
> > > +index f1435143f5..f9f5b82e28 100644
> > > +--- a/docs/ares_set_servers_csv.3
> > > ++++ b/docs/ares_set_servers_csv.3
> > > +@@ -126,6 +126,23 @@ are identical.  Prior versions would simply omit
> ports
> > in \fBares_set_servers_cs
> > > + 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 --git a/src/lib/ares_cancel.c b/src/lib/ares_cancel.c
> > > +index 75600dea6b..aa167e4486 100644
> > > +--- a/src/lib/ares_cancel.c
> > > ++++ b/src/lib/ares_cancel.c
> > > +@@ -38,10 +38,10 @@ void ares_cancel(ares_channel_t *channel)
> > > +   }
> > > +
> > > +   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 @@ void ares_cancel(ares_channel_t *channel)
> > > +       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 @@ void ares_cancel(ares_channel_t *channel)
> > > +   ares_queue_notify_empty(channel);
> > > +
> > > + done:
> > > ++  ares_deferred_cb_leave(channel);
> > > +   ares_channel_unlock(channel);
> > > + }
> > > +diff --git a/src/lib/ares_close_sockets.c
> b/src/lib/ares_close_sockets.c
> > > +index 7bb72c31ec..6be662fdd3 100644
> > > +--- a/src/lib/ares_close_sockets.c
> > > ++++ b/src/lib/ares_close_sockets.c
> > > +@@ -41,12 +41,47 @@ static void ares_requeue_queries(ares_conn_t
> > *conn,
> > > +   }
> > > + }
> > > +
> > > ++/*! 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;
> > > ++
> > > ++  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;
> > > ++  }
> > > +
> > > +-  /* Unlink */
> > > ++  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 @@ void ares_close_connection(ares_conn_t *conn,
> > ares_status_t requeue_status)
> > > +     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);
> > > +-
> > > +-  ares_socket_close(channel, conn->fd);
> > > ++  /* 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;
> > > ++
> > > ++  /* 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 @@ void ares_close_sockets(ares_server_t *server)
> > > +
> > > +   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);
> > > +   }
> > > + }
> > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index 6b315b0548..78f57e7d10 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -26,19 +26,210 @@
> > > +  */
> > > + #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 ((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);
> > > ++    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.
> > > ++   *
> > > ++   * 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;
> > > ++
> > > ++  /* 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;
> > > + }
> > > +
> > > +@@ -435,6 +626,16 @@ ares_status_t ares_open_connection(ares_conn_t
> > **conn_out,
> > > +     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) {
> > > +diff --git a/src/lib/ares_conn.h b/src/lib/ares_conn.h
> > > +index c332de14b1..fc177b84f6 100644
> > > +--- a/src/lib/ares_conn.h
> > > ++++ b/src/lib/ares_conn.h
> > > +@@ -49,7 +49,14 @@ typedef enum {
> > > +    *  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
> > > ++  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 {
> > > +@@ -84,6 +91,10 @@ struct ares_conn {
> > > +
> > > +   /* 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 */
> > > +@@ -168,6 +179,10 @@ struct ares_server {
> > > +
> > > +   /* 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);
> > > +@@ -185,8 +200,24 @@ ares_status_t   ares_conn_flush(ares_conn_t
> > *conn);
> > > + 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 --git a/src/lib/ares_destroy.c b/src/lib/ares_destroy.c
> > > +index 1e5706e0e0..e0534c800d 100644
> > > +--- a/src/lib/ares_destroy.c
> > > ++++ b/src/lib/ares_destroy.c
> > > +@@ -69,17 +69,31 @@ void ares_destroy(ares_channel_t *channel)
> > > +    * 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 @@ void ares_destroy(ares_channel_t *channel)
> > > +   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 @@ void ares_destroy(ares_channel_t *channel)
> > > +   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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > +index 04d4576ed2..0a1029e0f8 100644
> > > +--- a/src/lib/ares_private.h
> > > ++++ b/src/lib/ares_private.h
> > > +@@ -147,7 +147,10 @@ typedef struct ares_query ares_query_t;
> > > + 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;
> > > +
> > > +@@ -227,6 +230,9 @@ struct ares_channeldata {
> > > +   /* 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
> > > +    */
> > > +@@ -237,6 +243,10 @@ struct ares_channeldata {
> > > +   /* 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;
> > > +
> > > +@@ -248,6 +258,29 @@ struct ares_channeldata {
> > > +
> > > +   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;
> > > +@@ -295,6 +328,10 @@ struct ares_channeldata {
> > > +   /* 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
> > > +@@ -308,6 +345,11 @@ struct ares_channeldata {
> > > +    * 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. */
> > > +@@ -351,6 +393,16 @@ void
>  *ares_dnsrec_convert_arg(ares_callback
> > callback, void *arg);
> > > + 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);
> > > +@@ -390,7 +442,6 @@ ares_status_t
> > ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
> > > +
> > > + 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);
> > > +@@ -589,6 +640,60 @@ ares_status_t ares_dns_name_write(ares_buf_t
> > *buf, ares_llist_t **list,
> > > +  */
> > > + 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.
> > > ++ *
> > > ++ *  \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)
> > > +@@ -636,7 +741,6 @@ typedef struct ares_event_thread
> > ares_event_thread_t;
> > > + 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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index b2316c4b33..82f53610fb 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -69,7 +69,6 @@ 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_detach_query(ares_query_t *query);
> > > +
> > > + static void        ares_query_remove_from_conn(ares_query_t *query)
> > > + {
> > > +@@ -81,14 +80,32 @@ static void
> > ares_query_remove_from_conn(ares_query_t *query)
> > > +   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;
> > > +@@ -111,9 +128,57 @@ static void invoke_server_state_cb(const
> > ares_server_t *server,
> > > +     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,
> > > +@@ -204,6 +269,8 @@ static ares_status_t
> > ares_process_fds_nolock(ares_channel_t         *channel,
> > > +     return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
> > > +   }
> > > +
> > > ++  ares_deferred_cb_enter(channel);
> > > ++
> > > +   ares_tvnow(&now);
> > > +
> > > +   /* Process write events */
> > > +@@ -244,6 +311,8 @@ static ares_status_t
> > ares_process_fds_nolock(ares_channel_t         *channel,
> > > +   }
> > > +
> > > + done:
> > > ++  ares_deferred_cb_leave(channel);
> > > ++
> > > +   if (status == ARES_ENOMEM) {
> > > +     return ARES_ENOMEM;
> > > +   }
> > > +@@ -553,6 +622,8 @@ typedef enum {
> > > + typedef struct {
> > > +   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 */
> > > +@@ -578,6 +649,7 @@ static ares_status_t
> > ares_append_requeue_int(ares_array_t     **requeue,
> > > +
> > > +   entry.type   = type;
> > > +   entry.qid    = query->qid;
> > > ++  entry.serial = query->serial;
> > > +   entry.server = server;
> > > +   entry.status = status;
> > > +   entry.dnsrec = dnsrec;
> > > +@@ -601,6 +673,53 @@ static ares_status_t
> > ares_append_endqueue(ares_array_t     **requeue,
> > > +     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:
> > > +@@ -636,11 +755,35 @@ static ares_status_t
> > ares_flush_requeue(ares_channel_t       *channel,
> > > +
> > > +     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. */
> > > +@@ -1377,6 +1520,7 @@ ares_status_t ares_send_query(ares_server_t
> > *requested_server,
> > > +   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);
> > > +@@ -1393,8 +1537,7 @@ ares_status_t ares_send_query(ares_server_t
> > *requested_server,
> > > +    * 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_htable_szvp_get_direct(channel->queries_by_qid, qid) ==
> NULL) {
> > > ++  if (status == ARES_SUCCESS && !ares_query_is_live(channel, qid,
> serial))
> > {
> > > +     status = ARES_ETIMEOUT;
> > > +   }
> > > +
> > > +@@ -1412,6 +1555,9 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +   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) {
> > > +@@ -1443,6 +1589,32 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +   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:
> > > +@@ -1464,6 +1636,14 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +
> > > +   /* 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;
> > > ++  }
> > > ++
> > > +   switch (status) {
> > > +     /* Good result, continue on */
> > > +     case ARES_SUCCESS:
> > > +@@ -1479,6 +1659,17 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +     case ARES_ECONNREFUSED:
> > > +     case ARES_EBADFAMILY:
> > > +       handle_conn_error(conn, ARES_TRUE, status);
> > > ++      /* 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;
> > > +@@ -1597,11 +1788,18 @@ static ares_bool_t same_questions(const
> > ares_query_t      *query,
> > > +   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;
> > > + }
> > > +@@ -1624,6 +1822,16 @@ static void end_query(ares_channel_t *channel,
> > ares_server_t *server,
> > > +     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);
> > > +@@ -1631,7 +1839,8 @@ static void end_query(ares_channel_t *channel,
> > ares_server_t *server,
> > > +   /* 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);
> > > + }
> > > +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > +index 6efa9580b2..2e0482bf19 100644
> > > +--- a/src/lib/ares_send.c
> > > ++++ b/src/lib/ares_send.c
> > > +@@ -117,6 +117,27 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +   unsigned short           id          =
> generate_unique_qid(channel);
> > > +   const ares_dns_record_t *dnsrec_resp = 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);
> > > +
> > > +   if (ares_slist_len(channel->servers) == 0) {
> > > +@@ -145,6 +166,7 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +
> > > +   query->channel      = channel;
> > > +   query->qid          = id;
> > > ++  query->serial       = ++channel->query_serial;
> > > +   query->timeout.sec  = 0;
> > > +   query->timeout.usec = 0;
> > > +   query->using_tcp =
> > > +@@ -209,6 +231,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +    */
> > > +   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 +245,17 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +
> > > +   /* Perform the first query action. */
> > > +
> > > ++  /* Sending opens and writes to connections and can mark a server
> down,
> > both
> > > ++   * of which notify.  Deliver those once we are done with the
> connection and
> > > ++   * the server, not part-way through. */
> > > ++  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;
> > > + }
> > > +
> > > +diff --git a/src/lib/ares_update_servers.c
> b/src/lib/ares_update_servers.c
> > > +index f8d441accf..50a9c27925 100644
> > > +--- a/src/lib/ares_update_servers.c
> > > ++++ b/src/lib/ares_update_servers.c
> > > +@@ -740,17 +740,36 @@ static ares_bool_t
> > ares_servers_remove_stale(ares_channel_t *channel,
> > > + {
> > > +   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;
> > > + }
> > > +@@ -775,6 +794,14 @@ ares_status_t ares_servers_update(ares_channel_t
> > *channel,
> > > +     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
> > > +    */
> > > +@@ -843,6 +870,7 @@ ares_status_t ares_servers_update(ares_channel_t
> > *channel,
> > > +   status = ARES_SUCCESS;
> > > +
> > > + done:
> > > ++  ares_deferred_cb_leave(channel);
> > > +   return status;
> > > + }
> > > +
> > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index c0c0279abb..2158c5a913 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -31,6 +31,11 @@
> > > + #include <sys/stat.h>
> > > + #endif
> > > +
> > > ++#ifndef _WIN32
> > > ++#  include <errno.h>
> > > ++#  include <fcntl.h>
> > > ++#endif
> > > ++
> > > + #include <sstream>
> > > + #include <vector>
> > > +
> > > +@@ -1914,6 +1919,7 @@ TEST_P(MockRetryDepthChannelTest,
> > HighRetryNoStackOverflow) {
> > > + struct CancelInCbData {
> > > +   ares_channel_t *channel;
> > > +   bool            done;
> > > ++  size_t          cbs;
> > > + };
> > > +
> > > + static void CancelChannelCallback(void *arg, ares_status_t status,
> > > +@@ -1925,10 +1931,1071 @@ static void CancelChannelCallback(void *arg,
> > ares_status_t 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<int> {
> > > ++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<CancelInCbData *>(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<CancelInCbData
> *>(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<DestroyReentryData *>(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<DestroyReentryData *>(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);
> > > ++
> > > ++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<byte> 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<SockStateCancelData *>(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<SockStateCloseOrderData
> > *>(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<int>(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<ReconfigureFromHookData
> > *>(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<ReconfigureFromHookData
> > *>(arg);
> > > ++  (void)timeouts;
> > > ++  (void)dnsrec;
> > > ++  d->done   = true;
> > > ++  d->status = static_cast<int>(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);
> > > ++}
> > > ++
> > > ++// 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<SockStateReconfigureData *>(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);
> > > ++}
> > > ++
> > > ++// 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<std::pair<ares_socket_t, bool>> events; /* fd,
> still-armed */
> > > ++};
> > > ++
> > > ++static void SockStateRecordCallback(void *data, ares_socket_t
> socket_fd,
> > > ++                                    int readable, int writable)
> > > ++{
> > > ++  SockStateRecordData *d = static_cast<SockStateRecordData *>(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<ServerStateCancelData
> *>(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<ServerStateCancelData
> *>(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<int> {
> > > ++ 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<ServerStateUnregisterData *>(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<ServerStateUnregisterData *>(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<ReconfigureInCbData
> *>(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<ares_socket_t> 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<NestedReconfigureData
> > *>(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<NestedReconfigureData
> > *>(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()
> > > +@@ -1940,6 +3007,7 @@ TEST_P(MockUDPChannelTest,
> > CancelInCallbackNoDoubleFree) {
> > > +   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();
> > > diff --git
> a/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-
> > server-state-notifications.patch
> b/meta-oe/recipes-support/c-ares/c-ares/0002-
> > Deliver-deferred-socket-server-state-notifications.patch
> > > new file mode 100644
> > > index 0000000000..c34d3ab7c7
> > > --- /dev/null
> > > +++
> b/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-
> > server-state-notifications.patch
> > > @@ -0,0 +1,393 @@
> > > +From f944c4718fd672c9c0befd65a340abb2ccfc1a31 Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Sun, 18 Oct 2026 20:28:11 -0400
> > > +Subject: [PATCH] Deliver deferred socket/server-state notifications
> only from
> > > + the outermost public call (#1340)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of f023a52a (#1340) to v1.34.
> > > +
> > > +The recent change that queues socket-state and server-state
> > > +notifications delivers them, and runs the deferred cleanup, only from
> > > +the outermost `ares_deferred_cb_leave()`. `ares_send_nolock()`
> brackets
> > > +`ares_send_query()` itself, but most public lookup functions never
> > > +opened a region of their own. When reached from `ares_getaddrinfo()`,
> > > +`ares_query_dnsrec()` and similar, the bracket inside
> > > +`ares_send_nolock()` was therefore the outermost one. Notifications
> were
> > > +then delivered from inside an internal call while the library was
> still
> > > +in the middle of the public call, and its callers were still using
> their
> > > +lookup state (the host query, the query id out-pointer, ...). A
> > > +server-state callback that reconfigures the servers, which
> > > +`ares_set_server_state_callback(3)` documents as allowed, could end
> that
> > > +lookup before the caller was done with it.
> > > +
> > > +This is a regression in unreleased code. No released version is
> > > +affected.
> > > +
> > > +The outermost region is now opened by the public entry points
> > > +themselves, under the channel lock:
> > > +
> > > +- `ares_send_dnsrec()` (also used by `ares_send()`)
> > > +- `ares_query_dnsrec()` (also used by `ares_query()`)
> > > +- `ares_search()`, `ares_search_dnsrec()`
> > > +- `ares_getaddrinfo()` (also used by `ares_gethostbyname()`)
> > > +- `ares_gethostbyaddr()`, `ares_getnameinfo()`
> > > +- `ares_process_pending_write()`: a failed flush closes the connection
> > > +and fails its queries over, and this function had no bracket at all.
> > > +
> > > +`ares_process_fds()` (and through it
> > > +`ares_process()`/`ares_process_fd()`), `ares_cancel()`,
> `ares_destroy()`
> > > +and the server configuration functions already had their own. The
> nested
> > > +bracket in `ares_send_nolock()` stays, because it keeps connections
> and
> > > +servers unlinked during the send readable until the send returns.
> > > +Internal sends made from query callbacks (search next domain,
> > > +getaddrinfo next lookup, requeue, server probes) already run inside
> the
> > > +processing bracket.
> > > +
> > > +The comments on `ares_deferred_cb_enter()` and the flush now state the
> > > +rule: the outermost region belongs to the public entry point, never to
> > > +an internal helper.
> > > +
> > > +The documented behaviour (notifications arrive before the outermost
> > > +c-ares call returns) does not change, so no man page changes.
> > > +
> > > +Two new `LibraryTest` cases in `test/ares-test-mock.cc`. The IPv6
> server
> > > +cannot get a socket, so the first send fails over to the IPv4 server,
> > > +and a server-state callback then reconfigures the servers:
> > > +
> > > +- `ServerFailoverDuringGetAddrInfo`: the lookup completes cleanly. It
> > > +fails under ASAN without this change.
> > > +- `ServerFailoverDuringQuery`: the query id from `ares_query_dnsrec()`
> > > +is already reported by the time the query callback runs. It fails
> > > +without this change.
> > > +
> > > +The full non-live suite passes under ASAN/UBSAN and with
> > > +`-DCARES_WERROR=ON`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/f944c4718fd672c9c0befd65a340abb2ccfc1a31]
> > > +GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-
> > 2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
> > > +---
> > > + src/lib/ares_conn.c          |   4 +-
> > > + src/lib/ares_getaddrinfo.c   |   5 ++
> > > + src/lib/ares_gethostbyaddr.c |   5 ++
> > > + src/lib/ares_getnameinfo.c   |   5 ++
> > > + src/lib/ares_private.h       |  10 ++++
> > > + src/lib/ares_process.c       |   6 ++
> > > + src/lib/ares_query.c         |   5 ++
> > > + src/lib/ares_search.c        |  10 ++++
> > > + src/lib/ares_send.c          |  13 ++++-
> > > + test/ares-test-mock.cc       | 106
> +++++++++++++++++++++++++++++++++++
> > > + 10 files changed, 166 insertions(+), 3 deletions(-)
> > > +
> > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index 78f57e7d10..4cc0d1871d 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -94,7 +94,9 @@ 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.
> > > ++   * 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
> > > +diff --git a/src/lib/ares_getaddrinfo.c b/src/lib/ares_getaddrinfo.c
> > > +index 6d9e21af4b..5d9ddb311e 100644
> > > +--- a/src/lib/ares_getaddrinfo.c
> > > ++++ b/src/lib/ares_getaddrinfo.c
> > > +@@ -762,7 +762,12 @@ void ares_getaddrinfo(ares_channel_t *channel,
> > const char *name,
> > > +     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 --git a/src/lib/ares_gethostbyaddr.c
> b/src/lib/ares_gethostbyaddr.c
> > > +index 69c509ab11..b314359331 100644
> > > +--- a/src/lib/ares_gethostbyaddr.c
> > > ++++ b/src/lib/ares_gethostbyaddr.c
> > > +@@ -112,7 +112,12 @@ void ares_gethostbyaddr(ares_channel_t *channel,
> > const void *addr, int addrlen,
> > > +     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 --git a/src/lib/ares_getnameinfo.c b/src/lib/ares_getnameinfo.c
> > > +index 0159354530..7365e9ed82 100644
> > > +--- a/src/lib/ares_getnameinfo.c
> > > ++++ b/src/lib/ares_getnameinfo.c
> > > +@@ -194,7 +194,12 @@ void ares_getnameinfo(ares_channel_t *channel,
> > const struct sockaddr *sa,
> > > +   }
> > > +
> > > +   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);
> > > + }
> > > +
> > > +diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > +index 0a1029e0f8..9610ba7dab 100644
> > > +--- a/src/lib/ares_private.h
> > > ++++ b/src/lib/ares_private.h
> > > +@@ -676,6 +676,16 @@ void          ares_server_reap(ares_server_t
> *server);
> > > +  *  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);
> > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index 82f53610fb..1569408b1b 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -490,6 +490,11 @@ void ares_process_pending_write(ares_channel_t
> > *channel)
> > > +     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.
> > > +@@ -513,6 +518,7 @@ void ares_process_pending_write(ares_channel_t
> > *channel)
> > > +     }
> > > +   }
> > > +
> > > ++  ares_deferred_cb_leave(channel);
> > > +   ares_channel_unlock(channel);
> > > + }
> > > +
> > > +diff --git a/src/lib/ares_query.c b/src/lib/ares_query.c
> > > +index ca3b6a9b73..0cf7f70760 100644
> > > +--- a/src/lib/ares_query.c
> > > ++++ b/src/lib/ares_query.c
> > > +@@ -125,7 +125,12 @@ ares_status_t ares_query_dnsrec(ares_channel_t
> > *channel, const char *name,
> > > +   }
> > > +
> > > +   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 --git a/src/lib/ares_search.c b/src/lib/ares_search.c
> > > +index c605caf42c..ebc3d91e20 100644
> > > +--- a/src/lib/ares_search.c
> > > ++++ b/src/lib/ares_search.c
> > > +@@ -463,7 +463,12 @@ void ares_search(ares_channel_t *channel, const
> > char *name, int dnsclass,
> > > +   }
> > > +
> > > +   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_status_t ares_search_dnsrec(ares_channel_t
> > *channel,
> > > +   }
> > > +
> > > +   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 --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > +index 2e0482bf19..dd4a28544b 100644
> > > +--- a/src/lib/ares_send.c
> > > ++++ b/src/lib/ares_send.c
> > > +@@ -246,8 +246,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +   /* Perform the first query action. */
> > > +
> > > +   /* Sending opens and writes to connections and can mark a server
> down,
> > both
> > > +-   * of which notify.  Deliver those once we are done with the
> connection and
> > > +-   * the server, not part-way through. */
> > > ++   * 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);
> > > +@@ -271,9 +275,14 @@ ares_status_t ares_send_dnsrec(ares_channel_t
> > *channel,
> > > +   }
> > > +
> > > +   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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index 2158c5a913..b848c02434 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -2533,6 +2533,112 @@ TEST_F(LibraryTest,
> > ReconfigureInSockStateCallbackNoUseAfterFree) {
> > > +   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<ares_channel_t *>(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<FailoverQueryData *>(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
> > > diff --git
> a/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-
> > callbacks-their-own-copy.patch
> b/meta-oe/recipes-support/c-ares/c-ares/0003-
> > ares_qcache-hand-callbacks-their-own-copy.patch
> > > new file mode 100644
> > > index 0000000000..02039e92e0
> > > --- /dev/null
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-
> > callbacks-their-own-copy.patch
> > > @@ -0,0 +1,1054 @@
> > > +From a21160045f9d7039225fa285be7cb1b02173a3a5 Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Sun, 18 Oct 2026 22:12:09 -0400
> > > +Subject: [PATCH] [Backport v1.34] ares_qcache: hand callbacks their
> own
> > copy
> > > + of a cached answer (#1339)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of 31e2f51e8715d418fa8272a6c3a1ee651917fec5 (`ares_qcache:
> > hand
> > > +callbacks their own copy of a cached answer`) to `v1.34`. See that
> > > +commit for the full description.
> > > +
> > > +**Conflicts:**
> > > +- `ares_private.h`: alignment only; formatted with `v1.34`'s
> > > +`.clang-format`.
> > > +- `test/ares-test-internal.cc`: context only — on `main` the new
> > > +`QcacheKeyDistinguishesQuestions` test follows the IDNA/punycode tests
> > > +(#1031), which are not on `v1.34`, so it is placed after the URI test
> > > +instead.
> > > +
> > > +**Test fixes from #1334:** carries the part of #1334 that applies to
> the
> > > +tests this introduces — `ReentrantFlushInCallbackNoUseAfterFree`
> > > +reconfigures to `127.0.0.1:53055` instead of `127.0.0.55` (only
> routable
> > > +on Linux), and `UnknownQueryTypesDoNotShareCacheEntries` drops
> > > +`static_cast<ns_type>`, which does not compile on OpenBSD.
> > > +
> > > +Test file contents match `main`. Independent of the dd62bdfc backport
> > > +(#1338); applies cleanly with or without it.
> > > +
> > > +The full non-live suite passes 650/650 under ASAN+UBSAN and with
> > > +`-DCARES_WERROR=ON` on macOS.
> > `ReentrantFlushInCallbackNoUseAfterFree`
> > > +is a heap-use-after-free in `ares_dns_record_rr_cnt()` on `v1.34`
> > > +without this change, and passes with it.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/a21160045f9d7039225fa285be7cb1b02173a3a5]
> > > +GHSA: GHSA-grv7-fq5q-9vfc, GHSA-wrgc-prmw-27xh
> > > +---
> > > + docs/ares_init_options.3   |  11 +-
> > > + src/lib/ares_private.h     |  44 +++-
> > > + src/lib/ares_process.c     |   1 +
> > > + src/lib/ares_qcache.c      | 212 ++++++++++++++++---
> > > + src/lib/ares_send.c        |  47 ++++-
> > > + test/ares-test-internal.cc |  54 +++++
> > > + test/ares-test-mock.cc     | 419
> +++++++++++++++++++++++++++++++++++++
> > > + 7 files changed, 743 insertions(+), 45 deletions(-)
> > > +
> > > +diff --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
> > > +index e6f6ee8ba6..1ce7fb8e30 100644
> > > +--- a/docs/ares_init_options.3
> > > ++++ b/docs/ares_init_options.3
> > > +@@ -324,7 +324,16 @@ with the maximum number of seconds a query result
> > may be cached which will
> > > + 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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
> > > +index 9610ba7dab..dd06d303b8 100644
> > > +--- a/src/lib/ares_private.h
> > > ++++ b/src/lib/ares_private.h
> > > +@@ -153,6 +153,9 @@ struct ares_query {
> > > +   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
> > > +@@ -718,14 +721,47 @@ 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);
> > > ++/*! 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);
> > > +-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);
> > > ++/*! 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);
> > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index 1569408b1b..88536704ad 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -1859,6 +1859,7 @@ void ares_free_query(ares_query_t *query)
> > > +   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 --git a/src/lib/ares_qcache.c b/src/lib/ares_qcache.c
> > > +index ad3bc83505..f48669b56c 100644
> > > +--- a/src/lib/ares_qcache.c
> > > ++++ b/src/lib/ares_qcache.c
> > > +@@ -38,7 +38,7 @@ typedef struct {
> > > +   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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +     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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +   }
> > > +
> > > +   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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +       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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +       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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +       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 @@ static char *ares_qcache_calc_key(const
> > ares_dns_record_t *dnsrec)
> > > +       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 @@ static unsigned int
> > ares_qcache_soa_minimum(ares_dns_record_t *dnsrec)
> > > + }
> > > +
> > > + /* 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 @@ static ares_status_t
> > ares_qcache_insert_int(ares_qcache_t           *qcache,
> > > +   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 */
> > > +   }
> > > +@@ -382,31 +514,25 @@ static ares_status_t
> > ares_qcache_insert_int(ares_qcache_t           *qcache,
> > > +   /* 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;
> > > +@@ -416,10 +542,24 @@ ares_status_t ares_qcache_fetch(ares_channel_t
> > *channel,
> > > +   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;
> > > + }
> > > +
> > > +@@ -428,13 +568,25 @@ ares_status_t ares_qcache_insert(ares_channel_t
> > *channel,
> > > +                                  const ares_query_t      *query,
> > > +                                  const ares_dns_record_t *dnsrec)
> > > + {
> > > +-  ares_dns_record_t *dupdns = ares_dns_record_duplicate(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->query,
> > now);
> > > ++
> > > ++  status =
> > > ++    ares_qcache_insert_int(channel->qcache, dupdns,
> query->qcache_key,
> > now);
> > > +   if (status != ARES_SUCCESS) {
> > > +     ares_dns_record_destroy(dupdns);
> > > +   }
> > > +diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
> > > +index dd4a28544b..117745ce28 100644
> > > +--- a/src/lib/ares_send.c
> > > ++++ b/src/lib/ares_send.c
> > > +@@ -111,11 +111,12 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +                                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
> > > +@@ -145,13 +146,28 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +     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;
> > > +     }
> > > +   }
> > > +@@ -159,8 +175,11 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +   /* 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));
> > > +
> > > +@@ -173,6 +192,10 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +     (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
> > > +@@ -180,7 +203,11 @@ ares_status_t ares_send_nolock(ares_channel_t
> > *channel, ares_server_t *server,
> > > +     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;
> > > +   }
> > > +diff --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> > > +index 8881f3b7cc..5ba5d2b39b 100644
> > > +--- a/test/ares-test-internal.cc
> > > ++++ b/test/ares-test-internal.cc
> > > +@@ -502,6 +502,60 @@ TEST_F(LibraryTest, URI) {
> > > +   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<std::string> 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];
> > > ++    }
> > > ++  }
> > > ++}
> > > ++
> > > + #endif /* !CARES_SYMBOL_HIDING */
> > > +
> > > + TEST_F(LibraryTest, InetPtoN) {
> > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index b848c02434..4b409db7c3 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -611,6 +611,425 @@ class CacheQueriesTest
> > > +   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<QcacheReentrantCbData
> > *>(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<ares_dns_rec_type_t>(29),
> QueryCallback, &r1,
> > > ++                    nullptr);
> > > ++  Process();
> > > ++  EXPECT_TRUE(r1.done_);
> > > ++
> > > ++  QueryResult r2;
> > > ++  ares_query_dnsrec(channel_, "example.net", ARES_CLASS_IN,
> > > ++                    static_cast<ares_dns_rec_type_t>(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()
> > > diff --git
> a/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-
> > torn-down-by-application.patch
> b/meta-oe/recipes-support/c-ares/c-ares/0004-
> > Handle-connections-torn-down-by-application.patch
> > > new file mode 100644
> > > index 0000000000..f78e9726b0
> > > --- /dev/null
> > > +++
> b/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-
> > down-by-application.patch
> > > @@ -0,0 +1,1107 @@
> > > +From 13a10dddbabc30b6fca8e23f9efff73fedba9401 Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Sun, 18 Oct 2026 22:51:53 -0400
> > > +Subject: [PATCH] [Backport v1.34] Handle connections torn down by
> > application
> > > + socket functions (#1343)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of cb25d8bf297aed0aad8f6e0777ca0635cacc6cb1 to `v1.34`. See
> > > +that commit for the full description.
> > > +
> > > +Application code that c-ares calls while using a connection can
> > > +reconfigure the servers and so tear that connection down. That
> includes
> > > +the socket functions from `ares_set_socket_functions_ex()` and the
> older
> > > +`ares_set_socket_functions()`, and the pending-write callback. c-ares
> > > +now notices this while opening, writing to, reading from or closing a
> > > +connection. It requeues the affected queries, or drops the stale data,
> > > +instead of continuing to use the connection.
> > > +
> > > +It applied cleanly on top of the dd62bdfc and #1340 backports (#1338).
> > > +Test file contents match `main`.
> > > +
> > > +Each of the 11 new tests fails on `v1.34` without this change: nine
> with
> > > +an ASAN heap-use-after-free, and the two receive tests on their
> > > +assertions. All 11 pass with it. The full non-live suite passes
> 674/674
> > > +under ASAN+UBSAN and with `-DCARES_WERROR=ON` on macOS.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Co-authored-by: Mar <mardgez13@gmail.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/13a10dddbabc30b6fca8e23f9efff73fedba9401]
> > > +GHSA: GHSA-pj7p-v3cw-hq66
> > > +---
> > > + src/lib/ares_close_sockets.c |   7 +-
> > > + src/lib/ares_conn.c          |  29 ++
> > > + src/lib/ares_process.c       |  65 ++-
> > > + test/ares-test-mock.cc       | 842
> +++++++++++++++++++++++++++++++++++
> > > + 4 files changed, 935 insertions(+), 8 deletions(-)
> > > +
> > > +diff --git a/src/lib/ares_close_sockets.c
> b/src/lib/ares_close_sockets.c
> > > +index 6be662fdd3..a3af721bc6 100644
> > > +--- a/src/lib/ares_close_sockets.c
> > > ++++ b/src/lib/ares_close_sockets.c
> > > +@@ -148,6 +148,7 @@ void ares_check_cleanup_conns(const
> > ares_channel_t *channel)
> > > +     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)) {
> > > +@@ -190,8 +191,12 @@ void ares_check_cleanup_conns(const
> > ares_channel_t *channel)
> > > +         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 --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index 4cc0d1871d..5e0827fbdf 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -262,6 +262,15 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > *conn, void *data, size_t len,
> > > +     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;
> > > +@@ -408,6 +417,19 @@ ares_conn_err_t ares_conn_write(ares_conn_t
> > *conn, const void *data, size_t len,
> > > +   }
> > > +
> > > + 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
> > > +@@ -644,6 +666,13 @@ ares_status_t ares_open_connection(ares_conn_t
> > **conn_out,
> > > +     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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index 88536704ad..5686b077a6 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -189,7 +189,9 @@ static void
> server_increment_failures(ares_server_t
> > *server,
> > > +   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 */
> > > +   }
> > > +
> > > +@@ -220,7 +222,9 @@ static void server_set_good(ares_server_t *server,
> > ares_bool_t used_tcp)
> > > +   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 */
> > > +   }
> > > +
> > > +@@ -501,21 +505,39 @@ void ares_process_pending_write(ares_channel_t
> > *channel)
> > > +    */
> > > +   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);
> > > +@@ -902,6 +924,13 @@ static ares_status_t process_read(ares_channel_t
> > *channel,
> > > +     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;
> > > +@@ -1192,6 +1221,15 @@ static void handle_conn_error(ares_conn_t
> *conn,
> > ares_bool_t critical_failure,
> > > + {
> > > +   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) {
> > > +@@ -1508,6 +1546,14 @@ static ares_status_t
> > ares_conn_query_write(ares_conn_t          *conn,
> > > +       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;
> > > +   }
> > > +
> > > +@@ -1650,6 +1696,11 @@ static ares_status_t
> > ares_send_query_int(ares_server_t        *requested_server,
> > > +     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:
> > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index 4b409db7c3..f1d9d360e7 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -2593,6 +2593,86 @@ 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<int> {
> > > ++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<bool *>(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_;
> > > +@@ -2898,6 +2978,768 @@ TEST_F(LibraryTest,
> > ReconfigureInConfigureHookNoUseAfterFree) {
> > > +   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<ReconfigureFromSendData *>(user_data);
> > > ++  ares_ssize_t len = 0;
> > > ++  (void)fd;
> > > ++  for (int i = 0; i < iovcnt; i++) {
> > > ++    len += static_cast<ares_ssize_t>(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<ReconfigureFromSendData
> > *>(arg);
> > > ++  (void)timeouts;
> > > ++  (void)dnsrec;
> > > ++  d->done   = true;
> > > ++  d->status = static_cast<int>(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<BadSocketStateData *>(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<PendingWriteReconfigureData *>(user_data);
> > > ++  ares_ssize_t len = 0;
> > > ++  (void)fd;
> > > ++  for (int i = 0; i < iovcnt; i++) {
> > > ++    len += static_cast<ares_ssize_t>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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;
> > > ++  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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<unsigned char *>(buffer)[2] |= 0x80;
> > > ++  if (address != NULL && address_len != NULL &&
> > > ++      *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > ++    memcpy(address, &d->peer, sizeof(d->peer));
> > > ++    *address_len = sizeof(d->peer);
> > > ++  }
> > > ++  return static_cast<ares_ssize_t>(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<FakeSockData *>(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<ares_ssize_t>(length);
> > > ++}
> > > ++
> > > ++static int FakeSockGetsockname(ares_socket_t sock, struct sockaddr
> > *address,
> > > ++                               ares_socklen_t *address_len, void
> *user_data)
> > > ++{
> > > ++  FakeSockData      *d = static_cast<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(arg);
> > > ++  (void)timeouts;
> > > ++  (void)dnsrec;
> > > ++  d->done   = true;
> > > ++  d->status = static_cast<int>(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);
> > > ++}
> > > ++
> > > ++// 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
> > > diff --git
> a/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-
> > accept-an-answer-on-the-right-connection.patch
> b/meta-oe/recipes-support/c-
> > ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-
> > connection.patch
> > > new file mode 100644
> > > index 0000000000..20fef5d2f0
> > > --- /dev/null
> > > +++
> b/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-
> > an-answer-on-the-right-connection.patch
> > > @@ -0,0 +1,575 @@
> > > +From c8bd9ab53329f66ec0c395a11002a5696669b147 Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Mon, 19 Oct 2026 11:53:54 -0400
> > > +Subject: [PATCH] [Backport v1.34] ares_process: only accept an answer
> on the
> > > + connection its query was sent on (#1361)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of b6f8a9b47e17c681e092a74d05d73bbb35c89a39 to `v1.34`. See
> > > +that commit for the full description.
> > > +
> > > +**Conflicts:** `src/lib/ares_socket.h` only — v1.34 aligns the
> > > +neighbouring prototypes differently. Resolved by taking the new doc
> > > +comments and the `ares_sockaddr_addr_port_eq()` prototype, then
> > > +re-running clang-format for the v1.34 alignment (which also re-wraps
> the
> > > +new `SendData()` declaration in `test/ares-test.h`). All other files
> > > +applied unchanged; the `test/` diffs match main exactly.
> > > +
> > > +**Tests (local, macOS, Debug, Ninja):**
> > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > +--gtest_filter='-*Live*'` — 693/693 passed in both.
> > > +- With the `src/` changes reverted, the 4 new test instances fail
> > > +(`UDPAnswerFromWrongPortDropped`,
> > `AnswerOnOtherServerConnectionDropped`
> > > +UDP/TCP, `CookielessAnswerOnOtherServerDropped`); they pass with them.
> > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > +with the other two v1.34 backports (both orders apply cleanly):
> 699/699
> > > +passed under ASAN+UBSAN and `-Werror`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/c8bd9ab53329f66ec0c395a11002a5696669b147]
> > > +GHSA: GHSA-fqh5-3x7m-wp44
> > > +---
> > > + src/lib/ares_conn.c    |  11 ++-
> > > + src/lib/ares_process.c |  18 ++++
> > > + src/lib/ares_socket.c  |  24 +++++
> > > + src/lib/ares_socket.h  |  21 ++++
> > > + test/ares-test-mock.cc | 218
> > +++++++++++++++++++++++++++++++++++++++++
> > > + test/ares-test.cc      |  86 +++++++++-------
> > > + test/ares-test.h       |  12 +++
> > > + 7 files changed, 355 insertions(+), 35 deletions(-)
> > > +
> > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index 5e0827fbdf..b868053e62 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -252,9 +252,16 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > *conn, void *data, size_t len,
> > > +                            (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. */
> > > +     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
> > > +diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
> > > +index 5686b077a6..06d0ca507c 100644
> > > +--- a/src/lib/ares_process.c
> > > ++++ b/src/lib/ares_process.c
> > > +@@ -1117,6 +1117,24 @@ static ares_status_t
> > process_answer(ares_channel_t      *channel,
> > > +     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)
> > > +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> > > +index 4ad3034021..0a12935ab7 100644
> > > +--- a/src/lib/ares_socket.c
> > > ++++ b/src/lib/ares_socket.c
> > > +@@ -127,6 +127,30 @@ ares_bool_t ares_sockaddr_addr_eq(const struct
> > sockaddr  *sa,
> > > +   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;
> > > ++  }
> > > ++
> > > ++  if (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,
> > > +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> > > +index 24a99ab331..f7128e3493 100644
> > > +--- a/src/lib/ares_socket.h
> > > ++++ b/src/lib/ares_socket.h
> > > +@@ -139,8 +139,29 @@ typedef enum {
> > > +   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.
> > > ++ *
> > > ++ *  \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, IP addresses and
> ports are
> > > ++ *          equal, 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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index d233f9070f..058dc93001 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -3363,6 +3363,7 @@ struct FakeSockData {
> > > +   unsigned char      query[512];
> > > +   size_t             query_len;
> > > +   struct sockaddr_in peer;
> > > ++  unsigned short     recv_port; /* network order; 0 reports the
> peer's port */
> > > +   size_t             recv_calls;
> > > +   size_t             bad_fd_calls;
> > > +   size_t             sock_state_bad;
> > > +@@ -3471,6 +3472,9 @@ static ares_ssize_t
> > FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> > > +   if (address != NULL && address_len != NULL &&
> > > +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > +     memcpy(address, &d->peer, sizeof(d->peer));
> > > ++    if (d->recv_port != 0) {
> > > ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port =
> d->recv_port;
> > > ++    }
> > > +     *address_len = sizeof(d->peer);
> > > +   }
> > > +   return static_cast<ares_ssize_t>(d->query_len);
> > > +@@ -3710,6 +3714,42 @@ TEST_F(LibraryTest,
> > ReconfigureInRecvDropsBufferedAnswer) {
> > > +   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);
> > > ++}
> > > ++
> > > + // 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.
> > > +@@ -5530,5 +5570,183 @@
> > INSTANTIATE_TEST_SUITE_P(TransportModes, ServerRecoveryMultiMockTest,
> > ::testing:
> > > +
> > > + 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<int, bool> > {
> > > ++ 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<byte>());
> > > ++    }));
> > > ++  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<int> {
> > > ++ 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<byte> 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<byte>());
> > > ++    }));
> > > ++  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 --git a/test/ares-test.cc b/test/ares-test.cc
> > > +index d333ee9419..0b3ac12c09 100644
> > > +--- a/test/ares-test.cc
> > > ++++ b/test/ares-test.cc
> > > +@@ -448,6 +448,9 @@ MockServer::MockServer(int family, unsigned short
> > port)
> > > +   // 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;
> > > +@@ -710,6 +713,50 @@ std::set<ares_socket_t> MockServer::fds() const {
> > > +   return result;
> > > + }
> > > +
> > > ++void MockServer::SendData(ares_socket_t fd, struct sockaddr_storage
> *addr,
> > > ++                          ares_socklen_t addrlen, int qid,
> > > ++                          std::vector<byte> 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<byte> 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<ares_ssize_t>(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<byte> &req,
> > > +                                 const std::string &reqstr,
> > > +@@ -717,10 +764,14 @@ void MockServer::ProcessRequest(ares_socket_t
> > fd, struct sockaddr_storage* addr,
> > > +
> > > +   /* 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);
> > > +
> > > +@@ -747,43 +798,12 @@ void MockServer::ProcessRequest(ares_socket_t
> > fd, struct sockaddr_storage* addr,
> > > +     return;
> > > +   }
> > > +
> > > +-  // Make a local copy of the current pending reply.
> > > +-  std::vector<byte> 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<byte> 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<ares_ssize_t>(reply.size())) {
> > > +-    std::cerr << "Failed to send full reply, rc=" << rc << std::endl;
> > > +-  }
> > > ++  SendData(fd, addr, addrlen, qid, exact_reply_);
> > > +
> > > +   if (disconnect_after_reply_ && fd != udpfd_) {
> > > +     disconnect_after_reply_ = false;
> > > +diff --git a/test/ares-test.h b/test/ares-test.h
> > > +index 2b973c3982..170c284f26 100644
> > > +--- a/test/ares-test.h
> > > ++++ b/test/ares-test.h
> > > +@@ -303,6 +303,12 @@ class MockServer {
> > > +     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<ares_socket_t> fds() const;
> > > +
> > > +@@ -327,6 +333,8 @@ class MockServer {
> > > +                                 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<byte>
> reply);
> > > +   unsigned short udpport_;
> > > +   unsigned short tcpport_;
> > > +   ares_socket_t  udpfd_;
> > > +@@ -339,6 +347,10 @@ class MockServer {
> > > +   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.
> > > diff --git
> a/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-
> > channel-config-under-lock.patch
> b/meta-oe/recipes-support/c-ares/c-ares/0006-
> > ares_save_options-read-channel-config-under-lock.patch
> > > new file mode 100644
> > > index 0000000000..76c072dd7e
> > > --- /dev/null
> > > +++
> b/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-
> > channel-config-under-lock.patch
> > > @@ -0,0 +1,243 @@
> > > +From 25a6911370878d4b617b44b142ea0e49d1ccede6 Mon Sep 17
> > 00:00:00 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Mon, 19 Oct 2026 11:53:59 -0400
> > > +Subject: [PATCH] [Backport v1.34] ares_save_options: read channel
> > > + configuration under the channel lock (#1362)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of 5d878a580a8b57b51d15e2725cf6775c8e7bca0e to `v1.34`. See
> > > +that commit for the full description.
> > > +
> > > +**Conflicts:** none; cherry-picked cleanly.
> > > +
> > > +**Tests (local, macOS, Debug, Ninja):**
> > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > +--gtest_filter='-*Live*'` — 691/691 passed in both.
> > > +- With the `src/` changes reverted, the new `DupConcurrentSetServers`
> /
> > > +`SaveOptionsConcurrentSetServers` tests abort with an ASAN
> > > +heap-buffer-overflow; they pass with them.
> > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > +with the other two v1.34 backports (both orders apply cleanly):
> 699/699
> > > +passed under ASAN+UBSAN and `-Werror`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/25a6911370878d4b617b44b142ea0e49d1ccede6]
> > > +GHSA: GHSA-ph6c-3cvx-mq6v
> > > +---
> > > + src/lib/ares_init.c    |  3 +-
> > > + src/lib/ares_options.c | 45 ++++++++++++++++++----
> > > + test/ares-test-init.cc | 87
> > ++++++++++++++++++++++++++++++++++++++++++
> > > + 3 files changed, 126 insertions(+), 9 deletions(-)
> > > +
> > > +diff --git a/src/lib/ares_init.c b/src/lib/ares_init.c
> > > +index 4758c5de56..85b2e13050 100644
> > > +--- a/src/lib/ares_init.c
> > > ++++ b/src/lib/ares_init.c
> > > +@@ -483,7 +483,8 @@ int ares_dup(ares_channel_t **dest, const
> > ares_channel_t *src)
> > > +   *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);
> > > +diff --git a/src/lib/ares_options.c b/src/lib/ares_options.c
> > > +index 2b5624bbea..5ea35b732e 100644
> > > +--- a/src/lib/ares_options.c
> > > ++++ b/src/lib/ares_options.c
> > > +@@ -55,8 +55,8 @@ static struct in_addr *ares_save_opt_servers(const
> > ares_channel_t *channel,
> > > +                                              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 @@ static struct in_addr *ares_save_opt_servers(const
> > ares_channel_t *channel,
> > > +     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 @@ static struct in_addr *ares_save_opt_servers(const
> > ares_channel_t *channel,
> > > +   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 @@ int ares_save_options(const ares_channel_t
> > *channel,
> > > +   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 @@ int ares_save_options(const ares_channel_t
> > *channel,
> > > +   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 @@ int ares_save_options(const ares_channel_t
> > *channel,
> > > +   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)
> > > +diff --git a/test/ares-test-init.cc b/test/ares-test-init.cc
> > > +index b44211a1e4..1c5d0f2171 100644
> > > +--- a/test/ares-test-init.cc
> > > ++++ b/test/ares-test-init.cc
> > > +@@ -25,6 +25,8 @@
> > > +  */
> > > + #include "ares-test.h"
> > > +
> > > ++#include <atomic>
> > > ++
> > > + extern "C" {
> > > +   #include "ares_private.h"
> > > + }
> > > +@@ -295,6 +297,91 @@ TEST_F(LibraryTest, FailChannelInit) {
> > > +   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<size_t> 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<std::thread> 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;
> > > diff --git
> a/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-
> > prefix-length.patch
> b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-
> > reject-a-prefix-length.patch
> > > new file mode 100644
> > > index 0000000000..2cd1ea8c24
> > > --- /dev/null
> > > +++
> b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-
> > prefix-length.patch
> > > @@ -0,0 +1,364 @@
> > > +From 7e65b381c0b915aa5671ddc713080c87e15fc42f Mon Sep 17 00:00:00
> > 2001
> > > +From: Brad House <brad@brad-house.com>
> > > +Date: Mon, 19 Oct 2026 12:24:52 -0400
> > > +Subject: [PATCH] [Backport v1.34] ares_inet_pton: reject a prefix
> length,
> > > + always write the full address (#1363)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +Backport of 7b4ef8e555b611be6af39d913aa280cd1a348102 to `v1.34`. See
> > > +that commit for the full description.
> > > +
> > > +**Conflicts:** `test/ares-test-mock-ai.cc` only — the neighbouring
> > > +`FamilyV6IgnoresARecords` test (added on main by an unrelated change)
> > > +does not exist on v1.34. Resolved by adding only the new
> > > +`NumericHostRejectsPrefixLength` test at the same position. Other
> files
> > > +applied unchanged.
> > > +
> > > +**Also includes** the comment spelling fix from #1364
> > > +(ead5557d1301328536c3920bd01759a1bff5ca9a): "unparseable" ->
> > > +"unparsable" in `src/lib/inet_net_pton.c` and
> > > +`test/ares-test-internal.cc`, so Codespell passes and the text matches
> > > +main.
> > > +
> > > +**Tests (local, macOS, Debug, Ninja):**
> > > +- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
> > > +--gtest_filter='-*Live*'` — 693/693 passed in both.
> > > +- With the `src/` changes reverted, the 4 new test instances fail
> > > +(`InetNetPtoNPrefixZeroFills`, `InetPtoNRejectsPrefixLength`,
> > > +`NumericHostRejectsPrefixLength` UDP/TCP); they pass with them.
> > > +- `git clang-format --diff upstream/v1.34` clean. Also tested together
> > > +with the other two v1.34 backports (both orders apply cleanly):
> 699/699
> > > +passed under ASAN+UBSAN and `-Werror`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/7e65b381c0b915aa5671ddc713080c87e15fc42f]
> > > +GHSA: GHSA-cfcv-8v2j-qj6r
> > > +---
> > > + docs/ares_inet_pton.3        | 18 ++++++++--
> > > + src/lib/ares_inet_net_pton.h | 28 ++++++++++++++++
> > > + src/lib/inet_net_pton.c      | 65
> ++++++++++++++++++++----------------
> > > + test/ares-test-internal.cc   | 54 ++++++++++++++++++++++++++++++
> > > + test/ares-test-mock-ai.cc    | 23 +++++++++++++
> > > + 5 files changed, 157 insertions(+), 31 deletions(-)
> > > +
> > > +diff --git a/docs/ares_inet_pton.3 b/docs/ares_inet_pton.3
> > > +index 34b2df063c..19c5fdfda3 100644
> > > +--- a/docs/ares_inet_pton.3
> > > ++++ b/docs/ares_inet_pton.3
> > > +@@ -12,8 +12,9 @@ ares_inet_pton \- convert an IPv4 or IPv6 address
> from
> > text to binary form
> > > + 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 @@ in. The \fBdst\fP argument points to a buffer into
> which
> > the function stores
> > > + 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 --git a/src/lib/ares_inet_net_pton.h
> b/src/lib/ares_inet_net_pton.h
> > > +index e3ed83a3cb..e1a6f2d71e 100644
> > > +--- a/src/lib/ares_inet_net_pton.h
> > > ++++ b/src/lib/ares_inet_net_pton.h
> > > +@@ -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 --git a/src/lib/inet_net_pton.c b/src/lib/inet_net_pton.c
> > > +index e1f76ef834..f60823023b 100644
> > > +--- a/src/lib/inet_net_pton.c
> > > ++++ b/src/lib/inet_net_pton.c
> > > +@@ -51,14 +51,15 @@ const struct ares_in6_addr ares_in6addr_any = { {
> { 0,
> > 0, 0, 0, 0, 0, 0, 0, 0,
> > > +
> > > + /*
> > > +  * 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 @@ const struct ares_in6_addr ares_in6addr_any = { { {
> 0, 0,
> > 0, 0, 0, 0, 0, 0, 0,
> > > +  *      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 @@ static int ares_inet_net_pton_ipv4(const char
> *src,
> > unsigned char *dst,
> > > +   }
> > > +
> > > +   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 @@ static int ares_inet_pton6(const char *src,
> unsigned
> > char *dst)
> > > +       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 @@ static int ares_inet_net_pton_ipv6(const char
> *src,
> > unsigned char *dst,
> > > +     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 ares_inet_net_pton(int af, const char *src,
> void
> > *dst, size_t size)
> > > +   }
> > > + }
> > > +
> > > ++/*
> > > ++ * 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 --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
> > > +index bf06a74605..294e3f4555 100644
> > > +--- a/test/ares-test-internal.cc
> > > ++++ b/test/ares-test-internal.cc
> > > +@@ -274,6 +274,23 @@ TEST_F(LibraryTest, InetNetPtoN) {
> > > +   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++) {
> > > +@@ -600,9 +617,46 @@ TEST_F(LibraryTest, InetPtoN) {
> > > +   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.
> > > +diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc
> > > +index ae45c45527..43d0b89175 100644
> > > +--- a/test/ares-test-mock-ai.cc
> > > ++++ b/test/ares-test-mock-ai.cc
> > > +@@ -617,6 +617,29 @@ TEST_P(MockChannelTestAI, FamilyV6) {
> > > +   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<byte> nothing;
> > > diff --git
> a/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-
> > drop-UDP-answers-from-server-port.patch
> b/meta-oe/recipes-support/c-ares/c-
> > ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
> > > new file mode 100644
> > > index 0000000000..f4f182b995
> > > --- /dev/null
> > > +++
> b/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-
> > UDP-answers-from-server-port.patch
> > > @@ -0,0 +1,178 @@
> > > +From c7fa9ebcb0c0086730c53bf03191c03f03706246 Mon Sep 17 00:00:00
> > 2001
> > > +From: "c-ares-backport-gha[bot]"
> > > + <299594594+c-ares-backport-gha[bot]@users.noreply.github.com>
> > > +Date: Tue, 20 Oct 2026 02:25:59 +0000
> > > +Subject: [PATCH] [Backport v1.34] ares_conn_read: don't drop UDP
> answers
> > whose
> > > + source port isn't reported (#1371)
> > > +MIME-Version: 1.0
> > > +Content-Type: text/plain; charset=UTF-8
> > > +Content-Transfer-Encoding: 8bit
> > > +
> > > +# Description
> > > +Backport of #1370 to `v1.34`.
> > > +
> > > +Signed-off-by: Brad House <brad@brad-house.com>
> > > +Co-authored-by: Brad House <brad@brad-house.com>
> > > +Upstream-Status: Backport [https://github.com/c-ares/c-
> > ares/commit/c7fa9ebcb0c0086730c53bf03191c03f03706246]
> > > +GHSA: GHSA-fqh5-3x7m-wp44
> > > +---
> > > + docs/ares_set_socket_functions.3 | 20 ++++++++++++++++++++
> > > + src/lib/ares_conn.c              |  9 ++++++++-
> > > + src/lib/ares_socket.c            |  4 +++-
> > > + src/lib/ares_socket.h            |  8 +++++---
> > > + test/ares-test-mock.cc           | 32
> +++++++++++++++++++++++++++++++-
> > > + 5 files changed, 67 insertions(+), 6 deletions(-)
> > > +
> > > +diff --git a/docs/ares_set_socket_functions.3
> > b/docs/ares_set_socket_functions.3
> > > +index 605beb26ef..75c621cb48 100644
> > > +--- a/docs/ares_set_socket_functions.3
> > > ++++ b/docs/ares_set_socket_functions.3
> > > +@@ -149,6 +149,20 @@ a socket. Additional flags controlling behavior
> are in
> > > + 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 @@ Receives data from remote socket endpoint, if
> > available. If the \fIaddr\fP
> > > + 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
> > > +diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
> > > +index b868053e62..7711d8649e 100644
> > > +--- a/src/lib/ares_conn.c
> > > ++++ b/src/lib/ares_conn.c
> > > +@@ -257,7 +257,14 @@ ares_conn_err_t ares_conn_read(ares_conn_t
> > *conn, void *data, size_t len,
> > > +      * 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. */
> > > ++     * 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_port_eq((struct sockaddr *)&sa_storage,
> > > +                                     &conn->server->addr,
> > > +diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
> > > +index 0a12935ab7..29d595e1a9 100644
> > > +--- a/src/lib/ares_socket.c
> > > ++++ b/src/lib/ares_socket.c
> > > +@@ -144,7 +144,9 @@ ares_bool_t ares_sockaddr_addr_port_eq(const
> > struct sockaddr  *sa,
> > > +     sa_port = (CARES_INADDR_CAST(const struct sockaddr_in6 *, sa))-
> > >sin6_port;
> > > +   }
> > > +
> > > +-  if (ntohs(sa_port) != 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;
> > > +   }
> > > +
> > > +diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
> > > +index f7128e3493..1ede6987ee 100644
> > > +--- a/src/lib/ares_socket.h
> > > ++++ b/src/lib/ares_socket.h
> > > +@@ -151,13 +151,15 @@ 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.
> > > ++ *  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, IP addresses and
> ports are
> > > +- *          equal, ARES_FALSE otherwise
> > > ++ *  \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,
> > > +diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
> > > +index 058dc93001..cc5e6482f2 100644
> > > +--- a/test/ares-test-mock.cc
> > > ++++ b/test/ares-test-mock.cc
> > > +@@ -3364,6 +3364,7 @@ struct FakeSockData {
> > > +   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;
> > > +@@ -3472,7 +3473,9 @@ static ares_ssize_t
> > FakeSockRecvfrom(ares_socket_t sock, void *buffer,
> > > +   if (address != NULL && address_len != NULL &&
> > > +       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
> > > +     memcpy(address, &d->peer, sizeof(d->peer));
> > > +-    if (d->recv_port != 0) {
> > > ++    if (d->recv_no_port) {
> > > ++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = 0;
> > > ++    } else if (d->recv_port != 0) {
> > > +       reinterpret_cast<struct sockaddr_in *>(address)->sin_port =
> d->recv_port;
> > > +     }
> > > +     *address_len = sizeof(d->peer);
> > > +@@ -3750,6 +3753,33 @@ TEST_F(LibraryTest,
> > UDPAnswerFromWrongPortDropped) {
> > > +   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.
> > > diff --git a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> b/meta-oe/recipes-
> > support/c-ares/c-ares_1.34.8.bb
> > > index 058f3666e7..1d0f3eb343 100644
> > > --- a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> > > +++ b/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
> > > @@ -6,6 +6,14 @@ LICENSE = "MIT"
> > >  LIC_FILES_CHKSUM =
> > "file://LICENSE.md;md5=d3e72a10e08191f2ca1be3f3228d78f3"
> > >
> > >  SRC_URI = "https://github.com/c-ares/c-
> > ares/releases/download/v${PV}/${BPN}-${PV}.tar.gz \
> > > +           file://0001-ares_cancel-ares_destroy-detach-queries-before-
> > callbacks.patch \
> > > +
>  file://0002-Deliver-deferred-socket-server-state-notifications.patch \
> > > +
>  file://0003-ares_qcache-hand-callbacks-their-own-copy.patch \
> > > +
>  file://0004-Handle-connections-torn-down-by-application.patch \
> > > +
>  file://0005-ares_process-only-accept-an-answer-on-the-right-
> > connection.patch \
> > > +
>  file://0006-ares_save_options-read-channel-config-under-lock.patch \
> > > +           file://0007-ares_inet_pton-reject-a-prefix-length.patch \
> > > +
>  file://0008-ares_conn_read-dont-drop-UDP-answers-from-server-
> > port.patch \
> > >             file://run-ptest"
> > >  SRC_URI[sha256sum] =
> > "c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78"
> > >
> > > --
> > > 2.43.0
> > >
> > >
> > >
> > >
>
diff mbox series

Patch

diff --git a/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch b/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch
new file mode 100644
index 0000000000..385760f5bc
--- /dev/null
+++ b/meta-oe/recipes-support/c-ares/c-ares/0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch
@@ -0,0 +1,2603 @@ 
+From 7f408deb128fbdcce78122322734ffaa01cad9ae Mon Sep 17 00:00:00 2001
+From: Brad House <brad@brad-house.com>
+Date: Sun, 18 Oct 2026 18:15:41 -0400
+Subject: [PATCH] ares_cancel/ares_destroy: detach queries before running user
+ callbacks
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Backport of dd62bdfca52ba5c37e88a1469c0a4c5e4a568ccb to v1.34.  See that
+commit for the full description; it hardens reentrant teardown from
+callbacks (ares_cancel(), ares_destroy(), server reconfiguration, and
+socket/server state notifications).
+
+Conflicts were whitespace/alignment only (v1.34 predates the clang-format
+reformat in #1238) and were reformatted to v1.34's style.
+
+Also carries the test portion of #1334 that applies to the tests this
+introduces: dead-server tests use 127.0.0.1:53055/53056 instead of
+127.0.0.55/56, which only routes on Linux.
+
+Signed-off-by: Brad House <brad@brad-house.com>
+Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/7f408deb128fbdcce78122322734ffaa01cad9ae]
+GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
+---
+ docs/ares_cancel.3                    |   17 +
+ docs/ares_destroy.3                   |    7 +
+ docs/ares_init_options.3              |   15 +
+ docs/ares_set_server_state_callback.3 |   31 +-
+ docs/ares_set_servers_csv.3           |   17 +
+ src/lib/ares_cancel.c                 |   25 +-
+ src/lib/ares_close_sockets.c          |   82 +-
+ src/lib/ares_conn.c                   |  217 ++++-
+ src/lib/ares_conn.h                   |   37 +-
+ src/lib/ares_destroy.c                |   85 +-
+ src/lib/ares_private.h                |  110 ++-
+ src/lib/ares_process.c                |  235 +++++-
+ src/lib/ares_send.c                   |   35 +
+ src/lib/ares_update_servers.c         |   42 +-
+ test/ares-test-mock.cc                | 1068 +++++++++++++++++++++++++
+ 15 files changed, 1959 insertions(+), 64 deletions(-)
+
+diff --git a/docs/ares_cancel.3 b/docs/ares_cancel.3
+index 967a946b49..069f226c10 100644
+--- a/docs/ares_cancel.3
++++ b/docs/ares_cancel.3
+@@ -21,6 +21,23 @@ These calls give the callbacks a chance to clean up any state which might have
+ 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 --git a/docs/ares_destroy.3 b/docs/ares_destroy.3
+index d1984b7cc9..0b6380e609 100644
+--- a/docs/ares_destroy.3
++++ b/docs/ares_destroy.3
+@@ -20,6 +20,13 @@ by the channel.
+ 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 --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
+index 9b3b481535..e6f6ee8ba6 100644
+--- a/docs/ares_init_options.3
++++ b/docs/ares_init_options.3
+@@ -231,6 +231,21 @@ will be passed the socket whose state has changed;
+ 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
+diff --git a/docs/ares_set_server_state_callback.3 b/docs/ares_set_server_state_callback.3
+index 04fbde4c7a..e95db327ce 100644
+--- a/docs/ares_set_server_state_callback.3
++++ b/docs/ares_set_server_state_callback.3
+@@ -47,6 +47,31 @@ the DNS servers used by an ares channel. For example, it can be used to track
+ 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 @@ Indicates that the query was tried over TCP.
+ .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 --git a/docs/ares_set_servers_csv.3 b/docs/ares_set_servers_csv.3
+index f1435143f5..f9f5b82e28 100644
+--- a/docs/ares_set_servers_csv.3
++++ b/docs/ares_set_servers_csv.3
+@@ -126,6 +126,23 @@ are identical.  Prior versions would simply omit ports in \fBares_set_servers_cs
+ 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 --git a/src/lib/ares_cancel.c b/src/lib/ares_cancel.c
+index 75600dea6b..aa167e4486 100644
+--- a/src/lib/ares_cancel.c
++++ b/src/lib/ares_cancel.c
+@@ -38,10 +38,10 @@ void ares_cancel(ares_channel_t *channel)
+   }
+ 
+   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 @@ void ares_cancel(ares_channel_t *channel)
+       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 @@ void ares_cancel(ares_channel_t *channel)
+   ares_queue_notify_empty(channel);
+ 
+ done:
++  ares_deferred_cb_leave(channel);
+   ares_channel_unlock(channel);
+ }
+diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
+index 7bb72c31ec..6be662fdd3 100644
+--- a/src/lib/ares_close_sockets.c
++++ b/src/lib/ares_close_sockets.c
+@@ -41,12 +41,47 @@ static void ares_requeue_queries(ares_conn_t  *conn,
+   }
+ }
+ 
++/*! 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;
++
++  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;
++  }
+ 
+-  /* Unlink */
++  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 @@ void ares_close_connection(ares_conn_t *conn, ares_status_t requeue_status)
+     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);
+-
+-  ares_socket_close(channel, conn->fd);
++  /* 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;
++
++  /* 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 @@ void ares_close_sockets(ares_server_t *server)
+ 
+   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);
+   }
+ }
+diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
+index 6b315b0548..78f57e7d10 100644
+--- a/src/lib/ares_conn.c
++++ b/src/lib/ares_conn.c
+@@ -26,19 +26,210 @@
+  */
+ #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 ((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);
++    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.
++   *
++   * 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;
++
++  /* 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;
+ }
+ 
+@@ -435,6 +626,16 @@ ares_status_t ares_open_connection(ares_conn_t   **conn_out,
+     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) {
+diff --git a/src/lib/ares_conn.h b/src/lib/ares_conn.h
+index c332de14b1..fc177b84f6 100644
+--- a/src/lib/ares_conn.h
++++ b/src/lib/ares_conn.h
+@@ -49,7 +49,14 @@ typedef enum {
+    *  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
++  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 {
+@@ -84,6 +91,10 @@ struct ares_conn {
+ 
+   /* 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 */
+@@ -168,6 +179,10 @@ struct ares_server {
+ 
+   /* 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);
+@@ -185,8 +200,24 @@ ares_status_t   ares_conn_flush(ares_conn_t *conn);
+ 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 --git a/src/lib/ares_destroy.c b/src/lib/ares_destroy.c
+index 1e5706e0e0..e0534c800d 100644
+--- a/src/lib/ares_destroy.c
++++ b/src/lib/ares_destroy.c
+@@ -69,17 +69,31 @@ void ares_destroy(ares_channel_t *channel)
+    * 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 @@ void ares_destroy(ares_channel_t *channel)
+   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 @@ void ares_destroy(ares_channel_t *channel)
+   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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
+index 04d4576ed2..0a1029e0f8 100644
+--- a/src/lib/ares_private.h
++++ b/src/lib/ares_private.h
+@@ -147,7 +147,10 @@ typedef struct ares_query ares_query_t;
+ 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;
+ 
+@@ -227,6 +230,9 @@ struct ares_channeldata {
+   /* 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
+    */
+@@ -237,6 +243,10 @@ struct ares_channeldata {
+   /* 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;
+ 
+@@ -248,6 +258,29 @@ struct ares_channeldata {
+ 
+   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;
+@@ -295,6 +328,10 @@ struct ares_channeldata {
+   /* 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
+@@ -308,6 +345,11 @@ struct ares_channeldata {
+    * 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. */
+@@ -351,6 +393,16 @@ void         *ares_dnsrec_convert_arg(ares_callback callback, void *arg);
+ 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);
+@@ -390,7 +442,6 @@ ares_status_t ares_sysconfig_set_options(ares_sysconfig_t *sysconfig,
+ 
+ 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);
+@@ -589,6 +640,60 @@ ares_status_t ares_dns_name_write(ares_buf_t *buf, ares_llist_t **list,
+  */
+ 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.
++ *
++ *  \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)
+@@ -636,7 +741,6 @@ typedef struct ares_event_thread ares_event_thread_t;
+ 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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
+index b2316c4b33..82f53610fb 100644
+--- a/src/lib/ares_process.c
++++ b/src/lib/ares_process.c
+@@ -69,7 +69,6 @@ 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_detach_query(ares_query_t *query);
+ 
+ static void        ares_query_remove_from_conn(ares_query_t *query)
+ {
+@@ -81,14 +80,32 @@ static void        ares_query_remove_from_conn(ares_query_t *query)
+   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;
+@@ -111,9 +128,57 @@ static void invoke_server_state_cb(const ares_server_t *server,
+     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,
+@@ -204,6 +269,8 @@ static ares_status_t ares_process_fds_nolock(ares_channel_t         *channel,
+     return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
+   }
+ 
++  ares_deferred_cb_enter(channel);
++
+   ares_tvnow(&now);
+ 
+   /* Process write events */
+@@ -244,6 +311,8 @@ static ares_status_t ares_process_fds_nolock(ares_channel_t         *channel,
+   }
+ 
+ done:
++  ares_deferred_cb_leave(channel);
++
+   if (status == ARES_ENOMEM) {
+     return ARES_ENOMEM;
+   }
+@@ -553,6 +622,8 @@ typedef enum {
+ typedef struct {
+   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 */
+@@ -578,6 +649,7 @@ static ares_status_t ares_append_requeue_int(ares_array_t     **requeue,
+ 
+   entry.type   = type;
+   entry.qid    = query->qid;
++  entry.serial = query->serial;
+   entry.server = server;
+   entry.status = status;
+   entry.dnsrec = dnsrec;
+@@ -601,6 +673,53 @@ static ares_status_t ares_append_endqueue(ares_array_t     **requeue,
+     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:
+@@ -636,11 +755,35 @@ static ares_status_t ares_flush_requeue(ares_channel_t       *channel,
+ 
+     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. */
+@@ -1377,6 +1520,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server,
+   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);
+@@ -1393,8 +1537,7 @@ ares_status_t ares_send_query(ares_server_t *requested_server,
+    * 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_htable_szvp_get_direct(channel->queries_by_qid, qid) == NULL) {
++  if (status == ARES_SUCCESS && !ares_query_is_live(channel, qid, serial)) {
+     status = ARES_ETIMEOUT;
+   }
+ 
+@@ -1412,6 +1555,9 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
+   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) {
+@@ -1443,6 +1589,32 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
+   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:
+@@ -1464,6 +1636,14 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
+ 
+   /* 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;
++  }
++
+   switch (status) {
+     /* Good result, continue on */
+     case ARES_SUCCESS:
+@@ -1479,6 +1659,17 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
+     case ARES_ECONNREFUSED:
+     case ARES_EBADFAMILY:
+       handle_conn_error(conn, ARES_TRUE, status);
++      /* 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;
+@@ -1597,11 +1788,18 @@ static ares_bool_t same_questions(const ares_query_t      *query,
+   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;
+ }
+@@ -1624,6 +1822,16 @@ static void end_query(ares_channel_t *channel, ares_server_t *server,
+     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);
+@@ -1631,7 +1839,8 @@ static void end_query(ares_channel_t *channel, ares_server_t *server,
+   /* 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);
+ }
+diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
+index 6efa9580b2..2e0482bf19 100644
+--- a/src/lib/ares_send.c
++++ b/src/lib/ares_send.c
+@@ -117,6 +117,27 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+   unsigned short           id          = generate_unique_qid(channel);
+   const ares_dns_record_t *dnsrec_resp = 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);
+ 
+   if (ares_slist_len(channel->servers) == 0) {
+@@ -145,6 +166,7 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+ 
+   query->channel      = channel;
+   query->qid          = id;
++  query->serial       = ++channel->query_serial;
+   query->timeout.sec  = 0;
+   query->timeout.usec = 0;
+   query->using_tcp =
+@@ -209,6 +231,12 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+    */
+   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 +245,17 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+ 
+   /* Perform the first query action. */
+ 
++  /* Sending opens and writes to connections and can mark a server down, both
++   * of which notify.  Deliver those once we are done with the connection and
++   * the server, not part-way through. */
++  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;
+ }
+ 
+diff --git a/src/lib/ares_update_servers.c b/src/lib/ares_update_servers.c
+index f8d441accf..50a9c27925 100644
+--- a/src/lib/ares_update_servers.c
++++ b/src/lib/ares_update_servers.c
+@@ -740,17 +740,36 @@ static ares_bool_t ares_servers_remove_stale(ares_channel_t *channel,
+ {
+   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;
+ }
+@@ -775,6 +794,14 @@ ares_status_t ares_servers_update(ares_channel_t *channel,
+     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
+    */
+@@ -843,6 +870,7 @@ ares_status_t ares_servers_update(ares_channel_t *channel,
+   status = ARES_SUCCESS;
+ 
+ done:
++  ares_deferred_cb_leave(channel);
+   return status;
+ }
+ 
+diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
+index c0c0279abb..2158c5a913 100644
+--- a/test/ares-test-mock.cc
++++ b/test/ares-test-mock.cc
+@@ -31,6 +31,11 @@
+ #include <sys/stat.h>
+ #endif
+ 
++#ifndef _WIN32
++#  include <errno.h>
++#  include <fcntl.h>
++#endif
++
+ #include <sstream>
+ #include <vector>
+ 
+@@ -1914,6 +1919,7 @@ TEST_P(MockRetryDepthChannelTest, HighRetryNoStackOverflow) {
+ struct CancelInCbData {
+   ares_channel_t *channel;
+   bool            done;
++  size_t          cbs;
+ };
+ 
+ static void CancelChannelCallback(void *arg, ares_status_t status,
+@@ -1925,10 +1931,1071 @@ static void CancelChannelCallback(void *arg, ares_status_t 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<int> {
++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<CancelInCbData *>(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<CancelInCbData *>(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<DestroyReentryData *>(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<DestroyReentryData *>(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);
++
++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<byte> 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<SockStateCancelData *>(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<SockStateCloseOrderData *>(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<int>(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<ReconfigureFromHookData *>(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<ReconfigureFromHookData *>(arg);
++  (void)timeouts;
++  (void)dnsrec;
++  d->done   = true;
++  d->status = static_cast<int>(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);
++}
++
++// 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<SockStateReconfigureData *>(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);
++}
++
++// 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<std::pair<ares_socket_t, bool>> events; /* fd, still-armed */
++};
++
++static void SockStateRecordCallback(void *data, ares_socket_t socket_fd,
++                                    int readable, int writable)
++{
++  SockStateRecordData *d = static_cast<SockStateRecordData *>(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<ServerStateCancelData *>(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<ServerStateCancelData *>(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<int> {
++ 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<ServerStateUnregisterData *>(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<ServerStateUnregisterData *>(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<ReconfigureInCbData *>(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<ares_socket_t> 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<NestedReconfigureData *>(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<NestedReconfigureData *>(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()
+@@ -1940,6 +3007,7 @@ TEST_P(MockUDPChannelTest, CancelInCallbackNoDoubleFree) {
+   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();
diff --git a/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-server-state-notifications.patch b/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-server-state-notifications.patch
new file mode 100644
index 0000000000..c34d3ab7c7
--- /dev/null
+++ b/meta-oe/recipes-support/c-ares/c-ares/0002-Deliver-deferred-socket-server-state-notifications.patch
@@ -0,0 +1,393 @@ 
+From f944c4718fd672c9c0befd65a340abb2ccfc1a31 Mon Sep 17 00:00:00 2001
+From: Brad House <brad@brad-house.com>
+Date: Sun, 18 Oct 2026 20:28:11 -0400
+Subject: [PATCH] Deliver deferred socket/server-state notifications only from
+ the outermost public call (#1340)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Backport of f023a52a (#1340) to v1.34.
+
+The recent change that queues socket-state and server-state
+notifications delivers them, and runs the deferred cleanup, only from
+the outermost `ares_deferred_cb_leave()`. `ares_send_nolock()` brackets
+`ares_send_query()` itself, but most public lookup functions never
+opened a region of their own. When reached from `ares_getaddrinfo()`,
+`ares_query_dnsrec()` and similar, the bracket inside
+`ares_send_nolock()` was therefore the outermost one. Notifications were
+then delivered from inside an internal call while the library was still
+in the middle of the public call, and its callers were still using their
+lookup state (the host query, the query id out-pointer, ...). A
+server-state callback that reconfigures the servers, which
+`ares_set_server_state_callback(3)` documents as allowed, could end that
+lookup before the caller was done with it.
+
+This is a regression in unreleased code. No released version is
+affected.
+
+The outermost region is now opened by the public entry points
+themselves, under the channel lock:
+
+- `ares_send_dnsrec()` (also used by `ares_send()`)
+- `ares_query_dnsrec()` (also used by `ares_query()`)
+- `ares_search()`, `ares_search_dnsrec()`
+- `ares_getaddrinfo()` (also used by `ares_gethostbyname()`)
+- `ares_gethostbyaddr()`, `ares_getnameinfo()`
+- `ares_process_pending_write()`: a failed flush closes the connection
+and fails its queries over, and this function had no bracket at all.
+
+`ares_process_fds()` (and through it
+`ares_process()`/`ares_process_fd()`), `ares_cancel()`, `ares_destroy()`
+and the server configuration functions already had their own. The nested
+bracket in `ares_send_nolock()` stays, because it keeps connections and
+servers unlinked during the send readable until the send returns.
+Internal sends made from query callbacks (search next domain,
+getaddrinfo next lookup, requeue, server probes) already run inside the
+processing bracket.
+
+The comments on `ares_deferred_cb_enter()` and the flush now state the
+rule: the outermost region belongs to the public entry point, never to
+an internal helper.
+
+The documented behaviour (notifications arrive before the outermost
+c-ares call returns) does not change, so no man page changes.
+
+Two new `LibraryTest` cases in `test/ares-test-mock.cc`. The IPv6 server
+cannot get a socket, so the first send fails over to the IPv4 server,
+and a server-state callback then reconfigures the servers:
+
+- `ServerFailoverDuringGetAddrInfo`: the lookup completes cleanly. It
+fails under ASAN without this change.
+- `ServerFailoverDuringQuery`: the query id from `ares_query_dnsrec()`
+is already reported by the time the query callback runs. It fails
+without this change.
+
+The full non-live suite passes under ASAN/UBSAN and with
+`-DCARES_WERROR=ON`.
+
+Signed-off-by: Brad House <brad@brad-house.com>
+Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/f944c4718fd672c9c0befd65a340abb2ccfc1a31]
+GHSA: GHSA-wpr3-8q3r-4q8c, GHSA-97vq-4mxv-7vhm, GHSA-mw23-pfg6-2x9r, GHSA-rf6g-ww74-97qh, GHSA-v33j-65h4-4ch8, GHSA-pj7p-v3cw-hq66
+---
+ src/lib/ares_conn.c          |   4 +-
+ src/lib/ares_getaddrinfo.c   |   5 ++
+ src/lib/ares_gethostbyaddr.c |   5 ++
+ src/lib/ares_getnameinfo.c   |   5 ++
+ src/lib/ares_private.h       |  10 ++++
+ src/lib/ares_process.c       |   6 ++
+ src/lib/ares_query.c         |   5 ++
+ src/lib/ares_search.c        |  10 ++++
+ src/lib/ares_send.c          |  13 ++++-
+ test/ares-test-mock.cc       | 106 +++++++++++++++++++++++++++++++++++
+ 10 files changed, 166 insertions(+), 3 deletions(-)
+
+diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
+index 78f57e7d10..4cc0d1871d 100644
+--- a/src/lib/ares_conn.c
++++ b/src/lib/ares_conn.c
+@@ -94,7 +94,9 @@ 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.
++   * 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
+diff --git a/src/lib/ares_getaddrinfo.c b/src/lib/ares_getaddrinfo.c
+index 6d9e21af4b..5d9ddb311e 100644
+--- a/src/lib/ares_getaddrinfo.c
++++ b/src/lib/ares_getaddrinfo.c
+@@ -762,7 +762,12 @@ void ares_getaddrinfo(ares_channel_t *channel, const char *name,
+     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 --git a/src/lib/ares_gethostbyaddr.c b/src/lib/ares_gethostbyaddr.c
+index 69c509ab11..b314359331 100644
+--- a/src/lib/ares_gethostbyaddr.c
++++ b/src/lib/ares_gethostbyaddr.c
+@@ -112,7 +112,12 @@ void ares_gethostbyaddr(ares_channel_t *channel, const void *addr, int addrlen,
+     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 --git a/src/lib/ares_getnameinfo.c b/src/lib/ares_getnameinfo.c
+index 0159354530..7365e9ed82 100644
+--- a/src/lib/ares_getnameinfo.c
++++ b/src/lib/ares_getnameinfo.c
+@@ -194,7 +194,12 @@ void ares_getnameinfo(ares_channel_t *channel, const struct sockaddr *sa,
+   }
+ 
+   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);
+ }
+ 
+diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
+index 0a1029e0f8..9610ba7dab 100644
+--- a/src/lib/ares_private.h
++++ b/src/lib/ares_private.h
+@@ -676,6 +676,16 @@ void          ares_server_reap(ares_server_t *server);
+  *  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);
+diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
+index 82f53610fb..1569408b1b 100644
+--- a/src/lib/ares_process.c
++++ b/src/lib/ares_process.c
+@@ -490,6 +490,11 @@ void ares_process_pending_write(ares_channel_t *channel)
+     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.
+@@ -513,6 +518,7 @@ void ares_process_pending_write(ares_channel_t *channel)
+     }
+   }
+ 
++  ares_deferred_cb_leave(channel);
+   ares_channel_unlock(channel);
+ }
+ 
+diff --git a/src/lib/ares_query.c b/src/lib/ares_query.c
+index ca3b6a9b73..0cf7f70760 100644
+--- a/src/lib/ares_query.c
++++ b/src/lib/ares_query.c
+@@ -125,7 +125,12 @@ ares_status_t ares_query_dnsrec(ares_channel_t *channel, const char *name,
+   }
+ 
+   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 --git a/src/lib/ares_search.c b/src/lib/ares_search.c
+index c605caf42c..ebc3d91e20 100644
+--- a/src/lib/ares_search.c
++++ b/src/lib/ares_search.c
+@@ -463,7 +463,12 @@ void ares_search(ares_channel_t *channel, const char *name, int dnsclass,
+   }
+ 
+   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_status_t ares_search_dnsrec(ares_channel_t          *channel,
+   }
+ 
+   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 --git a/src/lib/ares_send.c b/src/lib/ares_send.c
+index 2e0482bf19..dd4a28544b 100644
+--- a/src/lib/ares_send.c
++++ b/src/lib/ares_send.c
+@@ -246,8 +246,12 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+   /* Perform the first query action. */
+ 
+   /* Sending opens and writes to connections and can mark a server down, both
+-   * of which notify.  Deliver those once we are done with the connection and
+-   * the server, not part-way through. */
++   * 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);
+@@ -271,9 +275,14 @@ ares_status_t ares_send_dnsrec(ares_channel_t          *channel,
+   }
+ 
+   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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
+index 2158c5a913..b848c02434 100644
+--- a/test/ares-test-mock.cc
++++ b/test/ares-test-mock.cc
+@@ -2533,6 +2533,112 @@ TEST_F(LibraryTest, ReconfigureInSockStateCallbackNoUseAfterFree) {
+   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<ares_channel_t *>(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<FailoverQueryData *>(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
diff --git a/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-callbacks-their-own-copy.patch b/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-callbacks-their-own-copy.patch
new file mode 100644
index 0000000000..02039e92e0
--- /dev/null
+++ b/meta-oe/recipes-support/c-ares/c-ares/0003-ares_qcache-hand-callbacks-their-own-copy.patch
@@ -0,0 +1,1054 @@ 
+From a21160045f9d7039225fa285be7cb1b02173a3a5 Mon Sep 17 00:00:00 2001
+From: Brad House <brad@brad-house.com>
+Date: Sun, 18 Oct 2026 22:12:09 -0400
+Subject: [PATCH] [Backport v1.34] ares_qcache: hand callbacks their own copy
+ of a cached answer (#1339)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Backport of 31e2f51e8715d418fa8272a6c3a1ee651917fec5 (`ares_qcache: hand
+callbacks their own copy of a cached answer`) to `v1.34`. See that
+commit for the full description.
+
+**Conflicts:**
+- `ares_private.h`: alignment only; formatted with `v1.34`'s
+`.clang-format`.
+- `test/ares-test-internal.cc`: context only — on `main` the new
+`QcacheKeyDistinguishesQuestions` test follows the IDNA/punycode tests
+(#1031), which are not on `v1.34`, so it is placed after the URI test
+instead.
+
+**Test fixes from #1334:** carries the part of #1334 that applies to the
+tests this introduces — `ReentrantFlushInCallbackNoUseAfterFree`
+reconfigures to `127.0.0.1:53055` instead of `127.0.0.55` (only routable
+on Linux), and `UnknownQueryTypesDoNotShareCacheEntries` drops
+`static_cast<ns_type>`, which does not compile on OpenBSD.
+
+Test file contents match `main`. Independent of the dd62bdfc backport
+(#1338); applies cleanly with or without it.
+
+The full non-live suite passes 650/650 under ASAN+UBSAN and with
+`-DCARES_WERROR=ON` on macOS. `ReentrantFlushInCallbackNoUseAfterFree`
+is a heap-use-after-free in `ares_dns_record_rr_cnt()` on `v1.34`
+without this change, and passes with it.
+
+Signed-off-by: Brad House <brad@brad-house.com>
+Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/a21160045f9d7039225fa285be7cb1b02173a3a5]
+GHSA: GHSA-grv7-fq5q-9vfc, GHSA-wrgc-prmw-27xh
+---
+ docs/ares_init_options.3   |  11 +-
+ src/lib/ares_private.h     |  44 +++-
+ src/lib/ares_process.c     |   1 +
+ src/lib/ares_qcache.c      | 212 ++++++++++++++++---
+ src/lib/ares_send.c        |  47 ++++-
+ test/ares-test-internal.cc |  54 +++++
+ test/ares-test-mock.cc     | 419 +++++++++++++++++++++++++++++++++++++
+ 7 files changed, 743 insertions(+), 45 deletions(-)
+
+diff --git a/docs/ares_init_options.3 b/docs/ares_init_options.3
+index e6f6ee8ba6..1ce7fb8e30 100644
+--- a/docs/ares_init_options.3
++++ b/docs/ares_init_options.3
+@@ -324,7 +324,16 @@ with the maximum number of seconds a query result may be cached which will
+ 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 --git a/src/lib/ares_private.h b/src/lib/ares_private.h
+index 9610ba7dab..dd06d303b8 100644
+--- a/src/lib/ares_private.h
++++ b/src/lib/ares_private.h
+@@ -153,6 +153,9 @@ struct ares_query {
+   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
+@@ -718,14 +721,47 @@ 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);
++/*! 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);
+-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);
++/*! 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);
+diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
+index 1569408b1b..88536704ad 100644
+--- a/src/lib/ares_process.c
++++ b/src/lib/ares_process.c
+@@ -1859,6 +1859,7 @@ void ares_free_query(ares_query_t *query)
+   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 --git a/src/lib/ares_qcache.c b/src/lib/ares_qcache.c
+index ad3bc83505..f48669b56c 100644
+--- a/src/lib/ares_qcache.c
++++ b/src/lib/ares_qcache.c
+@@ -38,7 +38,7 @@ typedef struct {
+   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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
+     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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
+   }
+ 
+   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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
+       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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
+       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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
+       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 @@ static char *ares_qcache_calc_key(const ares_dns_record_t *dnsrec)
+       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 @@ static unsigned int ares_qcache_soa_minimum(ares_dns_record_t *dnsrec)
+ }
+ 
+ /* 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 @@ static ares_status_t ares_qcache_insert_int(ares_qcache_t           *qcache,
+   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 */
+   }
+@@ -382,31 +514,25 @@ static ares_status_t ares_qcache_insert_int(ares_qcache_t           *qcache,
+   /* 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;
+@@ -416,10 +542,24 @@ ares_status_t ares_qcache_fetch(ares_channel_t           *channel,
+   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;
+ }
+ 
+@@ -428,13 +568,25 @@ ares_status_t ares_qcache_insert(ares_channel_t          *channel,
+                                  const ares_query_t      *query,
+                                  const ares_dns_record_t *dnsrec)
+ {
+-  ares_dns_record_t *dupdns = ares_dns_record_duplicate(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->query, now);
++
++  status =
++    ares_qcache_insert_int(channel->qcache, dupdns, query->qcache_key, now);
+   if (status != ARES_SUCCESS) {
+     ares_dns_record_destroy(dupdns);
+   }
+diff --git a/src/lib/ares_send.c b/src/lib/ares_send.c
+index dd4a28544b..117745ce28 100644
+--- a/src/lib/ares_send.c
++++ b/src/lib/ares_send.c
+@@ -111,11 +111,12 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+                                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
+@@ -145,13 +146,28 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+     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;
+     }
+   }
+@@ -159,8 +175,11 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+   /* 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));
+ 
+@@ -173,6 +192,10 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+     (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
+@@ -180,7 +203,11 @@ ares_status_t ares_send_nolock(ares_channel_t *channel, ares_server_t *server,
+     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;
+   }
+diff --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
+index 8881f3b7cc..5ba5d2b39b 100644
+--- a/test/ares-test-internal.cc
++++ b/test/ares-test-internal.cc
+@@ -502,6 +502,60 @@ TEST_F(LibraryTest, URI) {
+   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<std::string> 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];
++    }
++  }
++}
++
+ #endif /* !CARES_SYMBOL_HIDING */
+ 
+ TEST_F(LibraryTest, InetPtoN) {
+diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
+index b848c02434..4b409db7c3 100644
+--- a/test/ares-test-mock.cc
++++ b/test/ares-test-mock.cc
+@@ -611,6 +611,425 @@ class CacheQueriesTest
+   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<QcacheReentrantCbData *>(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<ares_dns_rec_type_t>(29), QueryCallback, &r1,
++                    nullptr);
++  Process();
++  EXPECT_TRUE(r1.done_);
++
++  QueryResult r2;
++  ares_query_dnsrec(channel_, "example.net", ARES_CLASS_IN,
++                    static_cast<ares_dns_rec_type_t>(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()
diff --git a/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-down-by-application.patch b/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-down-by-application.patch
new file mode 100644
index 0000000000..f78e9726b0
--- /dev/null
+++ b/meta-oe/recipes-support/c-ares/c-ares/0004-Handle-connections-torn-down-by-application.patch
@@ -0,0 +1,1107 @@ 
+From 13a10dddbabc30b6fca8e23f9efff73fedba9401 Mon Sep 17 00:00:00 2001
+From: Brad House <brad@brad-house.com>
+Date: Sun, 18 Oct 2026 22:51:53 -0400
+Subject: [PATCH] [Backport v1.34] Handle connections torn down by application
+ socket functions (#1343)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Backport of cb25d8bf297aed0aad8f6e0777ca0635cacc6cb1 to `v1.34`. See
+that commit for the full description.
+
+Application code that c-ares calls while using a connection can
+reconfigure the servers and so tear that connection down. That includes
+the socket functions from `ares_set_socket_functions_ex()` and the older
+`ares_set_socket_functions()`, and the pending-write callback. c-ares
+now notices this while opening, writing to, reading from or closing a
+connection. It requeues the affected queries, or drops the stale data,
+instead of continuing to use the connection.
+
+It applied cleanly on top of the dd62bdfc and #1340 backports (#1338).
+Test file contents match `main`.
+
+Each of the 11 new tests fails on `v1.34` without this change: nine with
+an ASAN heap-use-after-free, and the two receive tests on their
+assertions. All 11 pass with it. The full non-live suite passes 674/674
+under ASAN+UBSAN and with `-DCARES_WERROR=ON` on macOS.
+
+Signed-off-by: Brad House <brad@brad-house.com>
+Co-authored-by: Mar <mardgez13@gmail.com>
+Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/13a10dddbabc30b6fca8e23f9efff73fedba9401]
+GHSA: GHSA-pj7p-v3cw-hq66
+---
+ src/lib/ares_close_sockets.c |   7 +-
+ src/lib/ares_conn.c          |  29 ++
+ src/lib/ares_process.c       |  65 ++-
+ test/ares-test-mock.cc       | 842 +++++++++++++++++++++++++++++++++++
+ 4 files changed, 935 insertions(+), 8 deletions(-)
+
+diff --git a/src/lib/ares_close_sockets.c b/src/lib/ares_close_sockets.c
+index 6be662fdd3..a3af721bc6 100644
+--- a/src/lib/ares_close_sockets.c
++++ b/src/lib/ares_close_sockets.c
+@@ -148,6 +148,7 @@ void ares_check_cleanup_conns(const ares_channel_t *channel)
+     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)) {
+@@ -190,8 +191,12 @@ void ares_check_cleanup_conns(const ares_channel_t *channel)
+         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 --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
+index 4cc0d1871d..5e0827fbdf 100644
+--- a/src/lib/ares_conn.c
++++ b/src/lib/ares_conn.c
+@@ -262,6 +262,15 @@ ares_conn_err_t ares_conn_read(ares_conn_t *conn, void *data, size_t len,
+     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;
+@@ -408,6 +417,19 @@ ares_conn_err_t ares_conn_write(ares_conn_t *conn, const void *data, size_t len,
+   }
+ 
+ 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
+@@ -644,6 +666,13 @@ ares_status_t ares_open_connection(ares_conn_t   **conn_out,
+     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 --git a/src/lib/ares_process.c b/src/lib/ares_process.c
+index 88536704ad..5686b077a6 100644
+--- a/src/lib/ares_process.c
++++ b/src/lib/ares_process.c
+@@ -189,7 +189,9 @@ static void server_increment_failures(ares_server_t *server,
+   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 */
+   }
+ 
+@@ -220,7 +222,9 @@ static void server_set_good(ares_server_t *server, ares_bool_t used_tcp)
+   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 */
+   }
+ 
+@@ -501,21 +505,39 @@ void ares_process_pending_write(ares_channel_t *channel)
+    */
+   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);
+@@ -902,6 +924,13 @@ static ares_status_t process_read(ares_channel_t       *channel,
+     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;
+@@ -1192,6 +1221,15 @@ static void handle_conn_error(ares_conn_t *conn, ares_bool_t critical_failure,
+ {
+   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) {
+@@ -1508,6 +1546,14 @@ static ares_status_t ares_conn_query_write(ares_conn_t          *conn,
+       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;
+   }
+ 
+@@ -1650,6 +1696,11 @@ static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
+     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:
+diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
+index 4b409db7c3..f1d9d360e7 100644
+--- a/test/ares-test-mock.cc
++++ b/test/ares-test-mock.cc
+@@ -2593,6 +2593,86 @@ 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<int> {
++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<bool *>(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_;
+@@ -2898,6 +2978,768 @@ TEST_F(LibraryTest, ReconfigureInConfigureHookNoUseAfterFree) {
+   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<ReconfigureFromSendData *>(user_data);
++  ares_ssize_t len = 0;
++  (void)fd;
++  for (int i = 0; i < iovcnt; i++) {
++    len += static_cast<ares_ssize_t>(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<ReconfigureFromSendData *>(arg);
++  (void)timeouts;
++  (void)dnsrec;
++  d->done   = true;
++  d->status = static_cast<int>(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<BadSocketStateData *>(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<PendingWriteReconfigureData *>(user_data);
++  ares_ssize_t len = 0;
++  (void)fd;
++  for (int i = 0; i < iovcnt; i++) {
++    len += static_cast<ares_ssize_t>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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<PendingWriteReconfigureData *>(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;
++  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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(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<unsigned char *>(buffer)[2] |= 0x80;
++  if (address != NULL && address_len != NULL &&
++      *address_len >= (ares_socklen_t)sizeof(d->peer)) {
++    memcpy(address, &d->peer, sizeof(d->peer));
++    *address_len = sizeof(d->peer);
++  }
++  return static_cast<ares_ssize_t>(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<FakeSockData *>(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<ares_ssize_t>(length);
++}
++
++static int FakeSockGetsockname(ares_socket_t sock, struct sockaddr *address,
++                               ares_socklen_t *address_len, void *user_data)
++{
++  FakeSockData      *d = static_cast<FakeSockData *>(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<FakeSockData *>(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<FakeSockData *>(arg);
++  (void)timeouts;
++  (void)dnsrec;
++  d->done   = true;
++  d->status = static_cast<int>(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);
++}
++
++// 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
diff --git a/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-connection.patch b/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-connection.patch
new file mode 100644
index 0000000000..20fef5d2f0
--- /dev/null
+++ b/meta-oe/recipes-support/c-ares/c-ares/0005-ares_process-only-accept-an-answer-on-the-right-connection.patch
@@ -0,0 +1,575 @@ 
+From c8bd9ab53329f66ec0c395a11002a5696669b147 Mon Sep 17 00:00:00 2001
+From: Brad House <brad@brad-house.com>
+Date: Mon, 19 Oct 2026 11:53:54 -0400
+Subject: [PATCH] [Backport v1.34] ares_process: only accept an answer on the
+ connection its query was sent on (#1361)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Backport of b6f8a9b47e17c681e092a74d05d73bbb35c89a39 to `v1.34`. See
+that commit for the full description.
+
+**Conflicts:** `src/lib/ares_socket.h` only — v1.34 aligns the
+neighbouring prototypes differently. Resolved by taking the new doc
+comments and the `ares_sockaddr_addr_port_eq()` prototype, then
+re-running clang-format for the v1.34 alignment (which also re-wraps the
+new `SendData()` declaration in `test/ares-test.h`). All other files
+applied unchanged; the `test/` diffs match main exactly.
+
+**Tests (local, macOS, Debug, Ninja):**
+- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
+--gtest_filter='-*Live*'` — 693/693 passed in both.
+- With the `src/` changes reverted, the 4 new test instances fail
+(`UDPAnswerFromWrongPortDropped`, `AnswerOnOtherServerConnectionDropped`
+UDP/TCP, `CookielessAnswerOnOtherServerDropped`); they pass with them.
+- `git clang-format --diff upstream/v1.34` clean. Also tested together
+with the other two v1.34 backports (both orders apply cleanly): 699/699
+passed under ASAN+UBSAN and `-Werror`.
+
+Signed-off-by: Brad House <brad@brad-house.com>
+Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/c8bd9ab53329f66ec0c395a11002a5696669b147]
+GHSA: GHSA-fqh5-3x7m-wp44
+---
+ src/lib/ares_conn.c    |  11 ++-
+ src/lib/ares_process.c |  18 ++++
+ src/lib/ares_socket.c  |  24 +++++
+ src/lib/ares_socket.h  |  21 ++++
+ test/ares-test-mock.cc | 218 +++++++++++++++++++++++++++++++++++++++++
+ test/ares-test.cc      |  86 +++++++++-------
+ test/ares-test.h       |  12 +++
+ 7 files changed, 355 insertions(+), 35 deletions(-)
+
+diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
+index 5e0827fbdf..b868053e62 100644
+--- a/src/lib/ares_conn.c
++++ b/src/lib/ares_conn.c
+@@ -252,9 +252,16 @@ ares_conn_err_t ares_conn_read(ares_conn_t *conn, void *data, size_t len,
+                            (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. */
+     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
+diff --git a/src/lib/ares_process.c b/src/lib/ares_process.c
+index 5686b077a6..06d0ca507c 100644
+--- a/src/lib/ares_process.c
++++ b/src/lib/ares_process.c
+@@ -1117,6 +1117,24 @@ static ares_status_t process_answer(ares_channel_t      *channel,
+     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)
+diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
+index 4ad3034021..0a12935ab7 100644
+--- a/src/lib/ares_socket.c
++++ b/src/lib/ares_socket.c
+@@ -127,6 +127,30 @@ ares_bool_t ares_sockaddr_addr_eq(const struct sockaddr  *sa,
+   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;
++  }
++
++  if (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,
+diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
+index 24a99ab331..f7128e3493 100644
+--- a/src/lib/ares_socket.h
++++ b/src/lib/ares_socket.h
+@@ -139,8 +139,29 @@ typedef enum {
+   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.
++ *
++ *  \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, IP addresses and ports are
++ *          equal, 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 --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
+index d233f9070f..058dc93001 100644
+--- a/test/ares-test-mock.cc
++++ b/test/ares-test-mock.cc
+@@ -3363,6 +3363,7 @@ struct FakeSockData {
+   unsigned char      query[512];
+   size_t             query_len;
+   struct sockaddr_in peer;
++  unsigned short     recv_port; /* network order; 0 reports the peer's port */
+   size_t             recv_calls;
+   size_t             bad_fd_calls;
+   size_t             sock_state_bad;
+@@ -3471,6 +3472,9 @@ static ares_ssize_t FakeSockRecvfrom(ares_socket_t sock, void *buffer,
+   if (address != NULL && address_len != NULL &&
+       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
+     memcpy(address, &d->peer, sizeof(d->peer));
++    if (d->recv_port != 0) {
++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
++    }
+     *address_len = sizeof(d->peer);
+   }
+   return static_cast<ares_ssize_t>(d->query_len);
+@@ -3710,6 +3714,42 @@ TEST_F(LibraryTest, ReconfigureInRecvDropsBufferedAnswer) {
+   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);
++}
++
+ // 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.
+@@ -5530,5 +5570,183 @@ INSTANTIATE_TEST_SUITE_P(TransportModes, ServerRecoveryMultiMockTest, ::testing:
+ 
+ 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<int, bool> > {
++ 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<byte>());
++    }));
++  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<int> {
++ 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<byte> 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<byte>());
++    }));
++  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 --git a/test/ares-test.cc b/test/ares-test.cc
+index d333ee9419..0b3ac12c09 100644
+--- a/test/ares-test.cc
++++ b/test/ares-test.cc
+@@ -448,6 +448,9 @@ MockServer::MockServer(int family, unsigned short port)
+   // 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;
+@@ -710,6 +713,50 @@ std::set<ares_socket_t> MockServer::fds() const {
+   return result;
+ }
+ 
++void MockServer::SendData(ares_socket_t fd, struct sockaddr_storage *addr,
++                          ares_socklen_t addrlen, int qid,
++                          std::vector<byte> 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<byte> 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<ares_ssize_t>(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<byte> &req,
+                                 const std::string &reqstr,
+@@ -717,10 +764,14 @@ void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr,
+ 
+   /* 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);
+ 
+@@ -747,43 +798,12 @@ void MockServer::ProcessRequest(ares_socket_t fd, struct sockaddr_storage* addr,
+     return;
+   }
+ 
+-  // Make a local copy of the current pending reply.
+-  std::vector<byte> 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<byte> 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<ares_ssize_t>(reply.size())) {
+-    std::cerr << "Failed to send full reply, rc=" << rc << std::endl;
+-  }
++  SendData(fd, addr, addrlen, qid, exact_reply_);
+ 
+   if (disconnect_after_reply_ && fd != udpfd_) {
+     disconnect_after_reply_ = false;
+diff --git a/test/ares-test.h b/test/ares-test.h
+index 2b973c3982..170c284f26 100644
+--- a/test/ares-test.h
++++ b/test/ares-test.h
+@@ -303,6 +303,12 @@ class MockServer {
+     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<ares_socket_t> fds() const;
+ 
+@@ -327,6 +333,8 @@ class MockServer {
+                                 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<byte> reply);
+   unsigned short udpport_;
+   unsigned short tcpport_;
+   ares_socket_t  udpfd_;
+@@ -339,6 +347,10 @@ class MockServer {
+   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.
diff --git a/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-channel-config-under-lock.patch b/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-channel-config-under-lock.patch
new file mode 100644
index 0000000000..76c072dd7e
--- /dev/null
+++ b/meta-oe/recipes-support/c-ares/c-ares/0006-ares_save_options-read-channel-config-under-lock.patch
@@ -0,0 +1,243 @@ 
+From 25a6911370878d4b617b44b142ea0e49d1ccede6 Mon Sep 17 00:00:00 2001
+From: Brad House <brad@brad-house.com>
+Date: Mon, 19 Oct 2026 11:53:59 -0400
+Subject: [PATCH] [Backport v1.34] ares_save_options: read channel
+ configuration under the channel lock (#1362)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Backport of 5d878a580a8b57b51d15e2725cf6775c8e7bca0e to `v1.34`. See
+that commit for the full description.
+
+**Conflicts:** none; cherry-picked cleanly.
+
+**Tests (local, macOS, Debug, Ninja):**
+- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
+--gtest_filter='-*Live*'` — 691/691 passed in both.
+- With the `src/` changes reverted, the new `DupConcurrentSetServers` /
+`SaveOptionsConcurrentSetServers` tests abort with an ASAN
+heap-buffer-overflow; they pass with them.
+- `git clang-format --diff upstream/v1.34` clean. Also tested together
+with the other two v1.34 backports (both orders apply cleanly): 699/699
+passed under ASAN+UBSAN and `-Werror`.
+
+Signed-off-by: Brad House <brad@brad-house.com>
+Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/25a6911370878d4b617b44b142ea0e49d1ccede6]
+GHSA: GHSA-ph6c-3cvx-mq6v
+---
+ src/lib/ares_init.c    |  3 +-
+ src/lib/ares_options.c | 45 ++++++++++++++++++----
+ test/ares-test-init.cc | 87 ++++++++++++++++++++++++++++++++++++++++++
+ 3 files changed, 126 insertions(+), 9 deletions(-)
+
+diff --git a/src/lib/ares_init.c b/src/lib/ares_init.c
+index 4758c5de56..85b2e13050 100644
+--- a/src/lib/ares_init.c
++++ b/src/lib/ares_init.c
+@@ -483,7 +483,8 @@ int ares_dup(ares_channel_t **dest, const ares_channel_t *src)
+   *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);
+diff --git a/src/lib/ares_options.c b/src/lib/ares_options.c
+index 2b5624bbea..5ea35b732e 100644
+--- a/src/lib/ares_options.c
++++ b/src/lib/ares_options.c
+@@ -55,8 +55,8 @@ static struct in_addr *ares_save_opt_servers(const ares_channel_t *channel,
+                                              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 @@ static struct in_addr *ares_save_opt_servers(const ares_channel_t *channel,
+     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 @@ static struct in_addr *ares_save_opt_servers(const ares_channel_t *channel,
+   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 @@ int ares_save_options(const ares_channel_t *channel,
+   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 @@ int ares_save_options(const ares_channel_t *channel,
+   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 @@ int ares_save_options(const ares_channel_t *channel,
+   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)
+diff --git a/test/ares-test-init.cc b/test/ares-test-init.cc
+index b44211a1e4..1c5d0f2171 100644
+--- a/test/ares-test-init.cc
++++ b/test/ares-test-init.cc
+@@ -25,6 +25,8 @@
+  */
+ #include "ares-test.h"
+ 
++#include <atomic>
++
+ extern "C" {
+   #include "ares_private.h"
+ }
+@@ -295,6 +297,91 @@ TEST_F(LibraryTest, FailChannelInit) {
+   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<size_t> 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<std::thread> 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;
diff --git a/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-prefix-length.patch b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-prefix-length.patch
new file mode 100644
index 0000000000..2cd1ea8c24
--- /dev/null
+++ b/meta-oe/recipes-support/c-ares/c-ares/0007-ares_inet_pton-reject-a-prefix-length.patch
@@ -0,0 +1,364 @@ 
+From 7e65b381c0b915aa5671ddc713080c87e15fc42f Mon Sep 17 00:00:00 2001
+From: Brad House <brad@brad-house.com>
+Date: Mon, 19 Oct 2026 12:24:52 -0400
+Subject: [PATCH] [Backport v1.34] ares_inet_pton: reject a prefix length,
+ always write the full address (#1363)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Backport of 7b4ef8e555b611be6af39d913aa280cd1a348102 to `v1.34`. See
+that commit for the full description.
+
+**Conflicts:** `test/ares-test-mock-ai.cc` only — the neighbouring
+`FamilyV6IgnoresARecords` test (added on main by an unrelated change)
+does not exist on v1.34. Resolved by adding only the new
+`NumericHostRejectsPrefixLength` test at the same position. Other files
+applied unchanged.
+
+**Also includes** the comment spelling fix from #1364
+(ead5557d1301328536c3920bd01759a1bff5ca9a): "unparseable" ->
+"unparsable" in `src/lib/inet_net_pton.c` and
+`test/ares-test-internal.cc`, so Codespell passes and the text matches
+main.
+
+**Tests (local, macOS, Debug, Ninja):**
+- ASAN+UBSAN and `-DCARES_WERROR=ON` builds: `arestest -4
+--gtest_filter='-*Live*'` — 693/693 passed in both.
+- With the `src/` changes reverted, the 4 new test instances fail
+(`InetNetPtoNPrefixZeroFills`, `InetPtoNRejectsPrefixLength`,
+`NumericHostRejectsPrefixLength` UDP/TCP); they pass with them.
+- `git clang-format --diff upstream/v1.34` clean. Also tested together
+with the other two v1.34 backports (both orders apply cleanly): 699/699
+passed under ASAN+UBSAN and `-Werror`.
+
+Signed-off-by: Brad House <brad@brad-house.com>
+Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/7e65b381c0b915aa5671ddc713080c87e15fc42f]
+GHSA: GHSA-cfcv-8v2j-qj6r
+---
+ docs/ares_inet_pton.3        | 18 ++++++++--
+ src/lib/ares_inet_net_pton.h | 28 ++++++++++++++++
+ src/lib/inet_net_pton.c      | 65 ++++++++++++++++++++----------------
+ test/ares-test-internal.cc   | 54 ++++++++++++++++++++++++++++++
+ test/ares-test-mock-ai.cc    | 23 +++++++++++++
+ 5 files changed, 157 insertions(+), 31 deletions(-)
+
+diff --git a/docs/ares_inet_pton.3 b/docs/ares_inet_pton.3
+index 34b2df063c..19c5fdfda3 100644
+--- a/docs/ares_inet_pton.3
++++ b/docs/ares_inet_pton.3
+@@ -12,8 +12,9 @@ ares_inet_pton \- convert an IPv4 or IPv6 address from text to binary form
+ 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 @@ in. The \fBdst\fP argument points to a buffer into which the function stores
+ 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 --git a/src/lib/ares_inet_net_pton.h b/src/lib/ares_inet_net_pton.h
+index e3ed83a3cb..e1a6f2d71e 100644
+--- a/src/lib/ares_inet_net_pton.h
++++ b/src/lib/ares_inet_net_pton.h
+@@ -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 --git a/src/lib/inet_net_pton.c b/src/lib/inet_net_pton.c
+index e1f76ef834..f60823023b 100644
+--- a/src/lib/inet_net_pton.c
++++ b/src/lib/inet_net_pton.c
+@@ -51,14 +51,15 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 
+ /*
+  * 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 @@ const struct ares_in6_addr ares_in6addr_any = { { { 0, 0, 0, 0, 0, 0, 0, 0, 0,
+  *      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 @@ static int ares_inet_net_pton_ipv4(const char *src, unsigned char *dst,
+   }
+ 
+   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 @@ static int ares_inet_pton6(const char *src, unsigned char *dst)
+       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 @@ static int ares_inet_net_pton_ipv6(const char *src, unsigned char *dst,
+     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 ares_inet_net_pton(int af, const char *src, void *dst, size_t size)
+   }
+ }
+ 
++/*
++ * 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 --git a/test/ares-test-internal.cc b/test/ares-test-internal.cc
+index bf06a74605..294e3f4555 100644
+--- a/test/ares-test-internal.cc
++++ b/test/ares-test-internal.cc
+@@ -274,6 +274,23 @@ TEST_F(LibraryTest, InetNetPtoN) {
+   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++) {
+@@ -600,9 +617,46 @@ TEST_F(LibraryTest, InetPtoN) {
+   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.
+diff --git a/test/ares-test-mock-ai.cc b/test/ares-test-mock-ai.cc
+index ae45c45527..43d0b89175 100644
+--- a/test/ares-test-mock-ai.cc
++++ b/test/ares-test-mock-ai.cc
+@@ -617,6 +617,29 @@ TEST_P(MockChannelTestAI, FamilyV6) {
+   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<byte> nothing;
diff --git a/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch b/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
new file mode 100644
index 0000000000..f4f182b995
--- /dev/null
+++ b/meta-oe/recipes-support/c-ares/c-ares/0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch
@@ -0,0 +1,178 @@ 
+From c7fa9ebcb0c0086730c53bf03191c03f03706246 Mon Sep 17 00:00:00 2001
+From: "c-ares-backport-gha[bot]"
+ <299594594+c-ares-backport-gha[bot]@users.noreply.github.com>
+Date: Tue, 20 Oct 2026 02:25:59 +0000
+Subject: [PATCH] [Backport v1.34] ares_conn_read: don't drop UDP answers whose
+ source port isn't reported (#1371)
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+# Description
+Backport of #1370 to `v1.34`.
+
+Signed-off-by: Brad House <brad@brad-house.com>
+Co-authored-by: Brad House <brad@brad-house.com>
+Upstream-Status: Backport [https://github.com/c-ares/c-ares/commit/c7fa9ebcb0c0086730c53bf03191c03f03706246]
+GHSA: GHSA-fqh5-3x7m-wp44
+---
+ docs/ares_set_socket_functions.3 | 20 ++++++++++++++++++++
+ src/lib/ares_conn.c              |  9 ++++++++-
+ src/lib/ares_socket.c            |  4 +++-
+ src/lib/ares_socket.h            |  8 +++++---
+ test/ares-test-mock.cc           | 32 +++++++++++++++++++++++++++++++-
+ 5 files changed, 67 insertions(+), 6 deletions(-)
+
+diff --git a/docs/ares_set_socket_functions.3 b/docs/ares_set_socket_functions.3
+index 605beb26ef..75c621cb48 100644
+--- a/docs/ares_set_socket_functions.3
++++ b/docs/ares_set_socket_functions.3
+@@ -149,6 +149,20 @@ a socket. Additional flags controlling behavior are in
+ 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 @@ Receives data from remote socket endpoint, if available. If the \fIaddr\fP
+ 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
+diff --git a/src/lib/ares_conn.c b/src/lib/ares_conn.c
+index b868053e62..7711d8649e 100644
+--- a/src/lib/ares_conn.c
++++ b/src/lib/ares_conn.c
+@@ -257,7 +257,14 @@ ares_conn_err_t ares_conn_read(ares_conn_t *conn, void *data, size_t len,
+      * 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. */
++     * 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_port_eq((struct sockaddr *)&sa_storage,
+                                     &conn->server->addr,
+diff --git a/src/lib/ares_socket.c b/src/lib/ares_socket.c
+index 0a12935ab7..29d595e1a9 100644
+--- a/src/lib/ares_socket.c
++++ b/src/lib/ares_socket.c
+@@ -144,7 +144,9 @@ ares_bool_t ares_sockaddr_addr_port_eq(const struct sockaddr  *sa,
+     sa_port = (CARES_INADDR_CAST(const struct sockaddr_in6 *, sa))->sin6_port;
+   }
+ 
+-  if (ntohs(sa_port) != 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;
+   }
+ 
+diff --git a/src/lib/ares_socket.h b/src/lib/ares_socket.h
+index f7128e3493..1ede6987ee 100644
+--- a/src/lib/ares_socket.h
++++ b/src/lib/ares_socket.h
+@@ -151,13 +151,15 @@ 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.
++ *  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, IP addresses and ports are
+- *          equal, ARES_FALSE otherwise
++ *  \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,
+diff --git a/test/ares-test-mock.cc b/test/ares-test-mock.cc
+index 058dc93001..cc5e6482f2 100644
+--- a/test/ares-test-mock.cc
++++ b/test/ares-test-mock.cc
+@@ -3364,6 +3364,7 @@ struct FakeSockData {
+   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;
+@@ -3472,7 +3473,9 @@ static ares_ssize_t FakeSockRecvfrom(ares_socket_t sock, void *buffer,
+   if (address != NULL && address_len != NULL &&
+       *address_len >= (ares_socklen_t)sizeof(d->peer)) {
+     memcpy(address, &d->peer, sizeof(d->peer));
+-    if (d->recv_port != 0) {
++    if (d->recv_no_port) {
++      reinterpret_cast<struct sockaddr_in *>(address)->sin_port = 0;
++    } else if (d->recv_port != 0) {
+       reinterpret_cast<struct sockaddr_in *>(address)->sin_port = d->recv_port;
+     }
+     *address_len = sizeof(d->peer);
+@@ -3750,6 +3753,33 @@ TEST_F(LibraryTest, UDPAnswerFromWrongPortDropped) {
+   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.
diff --git a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb b/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
index 058f3666e7..1d0f3eb343 100644
--- a/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
+++ b/meta-oe/recipes-support/c-ares/c-ares_1.34.8.bb
@@ -6,6 +6,14 @@  LICENSE = "MIT"
 LIC_FILES_CHKSUM = "file://LICENSE.md;md5=d3e72a10e08191f2ca1be3f3228d78f3"
 
 SRC_URI = "https://github.com/c-ares/c-ares/releases/download/v${PV}/${BPN}-${PV}.tar.gz \
+           file://0001-ares_cancel-ares_destroy-detach-queries-before-callbacks.patch \
+           file://0002-Deliver-deferred-socket-server-state-notifications.patch \
+           file://0003-ares_qcache-hand-callbacks-their-own-copy.patch \
+           file://0004-Handle-connections-torn-down-by-application.patch \
+           file://0005-ares_process-only-accept-an-answer-on-the-right-connection.patch \
+           file://0006-ares_save_options-read-channel-config-under-lock.patch \
+           file://0007-ares_inet_pton-reject-a-prefix-length.patch \
+           file://0008-ares_conn_read-dont-drop-UDP-answers-from-server-port.patch \
            file://run-ptest"
 SRC_URI[sha256sum] = "c222b6d681096f9444d2c4863d2c1174019e27cacca0a4a5c114d36dd7d7bf78"