current commit
261020.17 · deps: update c-ares to 1.34.9
/* MIT License
 *
 * Copyright (c) 1998 Massachusetts Institute of Technology
 * Copyright (c) 2010 Daniel Stenberg
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice (including the next
 * paragraph) shall be included in all copies or substantial portions of the
 * Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * SPDX-License-Identifier: MIT
 */

#include "ares_private.h"

#ifdef HAVE_STRINGS_H
#  include <strings.h>
#endif
#ifdef HAVE_SYS_IOCTL_H
#  include <sys/ioctl.h>
#endif
#ifdef NETWARE
#  include <sys/filio.h>
#endif
#ifdef HAVE_STDINT_H
#  include <stdint.h>
#endif

#include <assert.h>
#include <fcntl.h>
#include <limits.h>


static ares_status_t process_write(ares_channel_t *channel,
                                   ares_socket_t   write_fd);
static ares_status_t process_read(ares_channel_t       *channel,
                                  ares_socket_t         read_fd,
                                  const ares_timeval_t *now);
static ares_status_t process_timeouts(ares_channel_t       *channel,
                                      const ares_timeval_t *now);
static ares_status_t process_answer(ares_channel_t      *channel,
                                    const unsigned char *abuf, size_t alen,
                                    ares_conn_t          *conn,
                                    const ares_timeval_t *now,
                                    ares_array_t        **requeue);
static void handle_conn_error(ares_conn_t *conn, ares_bool_t critical_failure,
                              ares_status_t failure_status);
static ares_bool_t same_questions(const ares_query_t      *query,
                                  const ares_dns_record_t *arec);
static void        end_query(ares_channel_t *channel, ares_server_t *server,
                             ares_query_t *query, ares_status_t status,
                             ares_dns_record_t *dnsrec,
                             ares_array_t **requeue);
static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
                                         ares_query_t         *query,
                                         const ares_timeval_t *now,
                                         ares_array_t        **requeue);

static void        ares_query_remove_from_conn(ares_query_t *query)
{
  /* If its not part of a connection, it can't be tracked for timeouts either */
  ares_slist_node_destroy(query->node_queries_by_timeout);
  ares_llist_node_destroy(query->node_queries_to_conn);
  query->node_queries_by_timeout = NULL;
  query->node_queries_to_conn    = NULL;
  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)
{
  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;
  }

  buf = ares_buf_create();
  if (buf == NULL) {
    return; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  status = ares_get_server_addr(server, buf);
  if (status != ARES_SUCCESS) {
    ares_buf_destroy(buf); /* LCOV_EXCL_LINE: OutOfMemory */
    return;                /* LCOV_EXCL_LINE: OutOfMemory */
  }

  server_string = ares_buf_finish_str(buf, NULL);
  buf           = NULL;
  if (server_string == NULL) {
    return; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  /* 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,
                                      ares_bool_t    used_tcp)
{
  ares_slist_node_t    *node;
  const ares_channel_t *channel = server->channel;
  ares_timeval_t        next_retry_time;

  node = ares_slist_node_find(channel->servers, server);
  /* 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 */
  }

  /* Cap the failure count.  Only the relative order matters for server
   * selection, and an uncapped count would require an unbounded number of
   * failures on other servers before a server that failed during an
   * extended outage could be selected again. */
  if (server->consec_failures < SERVER_CONSEC_FAILURES_CAP) {
    server->consec_failures++;
  }

  /* Must update the retry time before reinserting since the sort uses it to
   * order servers with the same failure count */
  ares_tvnow(&next_retry_time);
  ares_timeval_add(&next_retry_time, channel->server_retry_delay);
  server->next_retry_time = next_retry_time;

  ares_slist_node_reinsert(node);

  invoke_server_state_cb(server, ARES_FALSE,
                         used_tcp == ARES_TRUE ? ARES_SERV_STATE_TCP
                                               : ARES_SERV_STATE_UDP);
}

static void server_set_good(ares_server_t *server, ares_bool_t used_tcp)
{
  ares_slist_node_t    *node;
  const ares_channel_t *channel = server->channel;

  node = ares_slist_node_find(channel->servers, server);
  /* Matches by sort key, not identity, so a removed server can match the live
   * one that replaced it at the same configuration index. */
  if (node == NULL || ares_slist_node_val(node) != server) {
    return; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  if (server->consec_failures > 0) {
    server->consec_failures = 0;
    /* Must update the retry time before reinserting since the sort uses it
     * to order servers with the same failure count */
    server->next_retry_time.sec  = 0;
    server->next_retry_time.usec = 0;
    ares_slist_node_reinsert(node);
  }

  invoke_server_state_cb(server, ARES_TRUE,
                         used_tcp == ARES_TRUE ? ARES_SERV_STATE_TCP
                                               : ARES_SERV_STATE_UDP);
}

/* return true if now is exactly check time or later */
ares_bool_t ares_timedout(const ares_timeval_t *now,
                          const ares_timeval_t *check)
{
  ares_int64_t secs = (now->sec - check->sec);

  if (secs > 0) {
    return ARES_TRUE; /* yes, timed out */
  }
  if (secs < 0) {
    return ARES_FALSE; /* nope, not timed out */
  }

  /* if the full seconds were identical, check the sub second parts */
  return ((ares_int64_t)now->usec - (ares_int64_t)check->usec) >= 0
           ? ARES_TRUE
           : ARES_FALSE;
}

static ares_status_t ares_process_fds_nolock(ares_channel_t         *channel,
                                             const ares_fd_events_t *events,
                                             size_t nevents, unsigned int flags)
{
  ares_timeval_t now;
  size_t         i;
  ares_status_t  status = ARES_SUCCESS;

  if (channel == NULL || (events == NULL && nevents != 0)) {
    return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  ares_deferred_cb_enter(channel);

  ares_tvnow(&now);

  /* Process write events */
  for (i = 0; i < nevents; i++) {
    if (events[i].fd == ARES_SOCKET_BAD ||
        !(events[i].events & ARES_FD_EVENT_WRITE)) {
      continue;
    }
    status = process_write(channel, events[i].fd);
    /* We only care about ENOMEM, anything else is handled via connection
     * retries, etc */
    if (status == ARES_ENOMEM) {
      goto done;
    }
  }

  /* Process read events */
  for (i = 0; i < nevents; i++) {
    if (events[i].fd == ARES_SOCKET_BAD ||
        !(events[i].events & ARES_FD_EVENT_READ)) {
      continue;
    }
    status = process_read(channel, events[i].fd, &now);
    if (status == ARES_ENOMEM) {
      goto done;
    }
  }

  if (!(flags & ARES_PROCESS_FLAG_SKIP_NON_FD)) {
    status = process_timeouts(channel, &now);
    if (status == ARES_ENOMEM) {
      goto done;
    }

    /* Cleanup should be done after processing timeouts as it may invalidate
     * connections */
    ares_check_cleanup_conns(channel);
  }

done:
  ares_deferred_cb_leave(channel);

  if (status == ARES_ENOMEM) {
    return ARES_ENOMEM;
  }
  return ARES_SUCCESS;
}

ares_status_t ares_process_fds(ares_channel_t         *channel,
                               const ares_fd_events_t *events, size_t nevents,
                               unsigned int flags)
{
  ares_status_t status;

  if (channel == NULL) {
    return ARES_EFORMERR;
  }

  ares_channel_lock(channel);
  status = ares_process_fds_nolock(channel, events, nevents, flags);
  ares_channel_unlock(channel);
  return status;
}

void ares_process_fd(ares_channel_t *channel, ares_socket_t read_fd,
                     ares_socket_t write_fd)
{
  ares_fd_events_t events[2];
  size_t           nevents = 0;

  memset(events, 0, sizeof(events));

  if (read_fd != ARES_SOCKET_BAD) {
    nevents++;
    events[nevents - 1].fd      = read_fd;
    events[nevents - 1].events |= ARES_FD_EVENT_READ;
  }

  if (write_fd != ARES_SOCKET_BAD) {
    if (write_fd != read_fd) {
      nevents++;
    }
    events[nevents - 1].fd      = write_fd;
    events[nevents - 1].events |= ARES_FD_EVENT_WRITE;
  }

  ares_process_fds(channel, events, nevents, ARES_PROCESS_FLAG_NONE);
}

static ares_socket_t *channel_socket_list(const ares_channel_t *channel,
                                          size_t               *num)
{
  ares_slist_node_t *snode;
  ares_array_t      *arr = ares_array_create(sizeof(ares_socket_t), NULL);

  *num = 0;

  if (arr == NULL) {
    return NULL; /* LCOV_EXCL_LINE: OutOfMemory */
  }

  for (snode = ares_slist_node_first(channel->servers); snode != NULL;
       snode = ares_slist_node_next(snode)) {
    ares_server_t     *server = ares_slist_node_val(snode);
    ares_llist_node_t *node;

    for (node = ares_llist_node_first(server->connections); node != NULL;
         node = ares_llist_node_next(node)) {
      const ares_conn_t *conn = ares_llist_node_val(node);
      ares_socket_t     *sptr;
      ares_status_t      status;

      if (conn->fd == ARES_SOCKET_BAD) {
        continue;
      }

      status = ares_array_insert_last((void **)&sptr, arr);
      if (status != ARES_SUCCESS) {
        ares_array_destroy(arr); /* LCOV_EXCL_LINE: OutOfMemory */
        return NULL;             /* LCOV_EXCL_LINE: OutOfMemory */
      }
      *sptr = conn->fd;
    }
  }

  return ares_array_finish(arr, num);
}

/* Something interesting happened on the wire, or there was a timeout.
 * See what's up and respond accordingly.
 */
void ares_process(ares_channel_t *channel, fd_set *read_fds, fd_set *write_fds)
{
  size_t            i;
  size_t            num_sockets;
  ares_socket_t    *socketlist;
  ares_fd_events_t *events  = NULL;
  size_t            nevents = 0;

  if (channel == NULL) {
    return;
  }

  ares_channel_lock(channel);

  /* There is no good way to iterate across an fd_set, instead we must pull a
   * list of all known fds, and iterate across that checking against the fd_set.
   */
  socketlist = channel_socket_list(channel, &num_sockets);

  /* Lets create an events array, maximum number is the number of sockets in
   * the list, so we'll use that and just track entries with nevents */
  if (num_sockets) {
    events = ares_malloc_zero(sizeof(*events) * num_sockets);
    if (events == NULL) {
      goto done;
    }
  }

  for (i = 0; i < num_sockets; i++) {
    ares_bool_t had_read = ARES_FALSE;
    if (read_fds && FD_ISSET(socketlist[i], read_fds)) {
      nevents++;
      events[nevents - 1].fd      = socketlist[i];
      events[nevents - 1].events |= ARES_FD_EVENT_READ;
      had_read                    = ARES_TRUE;
    }
    if (write_fds && FD_ISSET(socketlist[i], write_fds)) {
      if (!had_read) {
        nevents++;
      }
      events[nevents - 1].fd      = socketlist[i];
      events[nevents - 1].events |= ARES_FD_EVENT_WRITE;
    }
  }

done:
  ares_process_fds_nolock(channel, events, nevents, ARES_PROCESS_FLAG_NONE);
  ares_free(events);
  ares_free(socketlist);
  ares_channel_unlock(channel);
}

static ares_status_t process_write(ares_channel_t *channel,
                                   ares_socket_t   write_fd)
{
  ares_conn_t  *conn = ares_conn_from_fd(channel, write_fd);
  ares_status_t status;

  if (conn == NULL) {
    return ARES_SUCCESS;
  }

  /* Mark as connected if we got here and TFO Initial not set */
  if (!(conn->flags & ARES_CONN_FLAG_TFO_INITIAL)) {
    conn->state_flags |= ARES_CONN_STATE_CONNECTED;
  }

  status = ares_conn_flush(conn);
  if (status != ARES_SUCCESS) {
    handle_conn_error(conn, ARES_TRUE, status);
  }
  return status;
}

void ares_process_pending_write(ares_channel_t *channel)
{
  ares_slist_node_t *node;

  if (channel == NULL) {
    return;
  }

  ares_channel_lock(channel);
  if (!channel->notify_pending_write) {
    ares_channel_unlock(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.
   */
  channel->notify_pending_write = ARES_FALSE;

  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.  The flush runs the
     * application's send function, which may have reconfigured the servers
     * and removed this one; its connection is then already closed, which
     * handle_conn_error() ignores. */
    status = ares_conn_flush(conn);
    if (status != ARES_SUCCESS) {
      handle_conn_error(conn, ARES_TRUE, status);
    }

    /* Tearing down a connection runs the callbacks of the queries it ends,
     * and those may reconfigure the servers too, destroying the ones the new
     * configuration omits along with their list nodes.  A failure also moves
     * this server further down the list.  So when either may have happened,
     * start over from the head rather than step from this node; flushing a
     * connection again is harmless. */
    if (status != ARES_SUCCESS || channel->servers_generation != generation) {
      node = ares_slist_node_first(channel->servers);
      continue;
    }

    node = ares_slist_node_next(node);
  }

  ares_deferred_cb_leave(channel);
  ares_channel_unlock(channel);
}

static ares_status_t read_conn_packets(ares_conn_t *conn,
  ares_bool_t *conn_error)
{
  ares_bool_t           read_again;
  ares_conn_err_t       err;
  const ares_channel_t *channel = conn->server->channel;

  if (conn_error == NULL) {
    return ARES_EFORMERR;
  }

  *conn_error = ARES_FALSE;

  do {
    size_t         count;
    size_t         len = 65535;
    unsigned char *ptr;
    size_t         start_len = ares_buf_len(conn->in_buf);

    /* If UDP, lets write out a placeholder for the length indicator */
    if (!(conn->flags & ARES_CONN_FLAG_TCP) &&
        ares_buf_append_be16(conn->in_buf, 0) != ARES_SUCCESS) {
      handle_conn_error(conn, ARES_FALSE /* not critical to connection */,
                        ARES_SUCCESS);
      return ARES_ENOMEM;
    }

    /* Get a buffer of sufficient size */
    ptr = ares_buf_append_start(conn->in_buf, &len);

    if (ptr == NULL) {
      handle_conn_error(conn, ARES_FALSE /* not critical to connection */,
                        ARES_SUCCESS);
      return ARES_ENOMEM;
    }

    /* Read from socket */
    err = ares_conn_read(conn, ptr, len, &count);

    if (err != ARES_CONN_ERR_SUCCESS) {
      ares_buf_append_finish(conn->in_buf, 0);
      if (!(conn->flags & ARES_CONN_FLAG_TCP)) {
        ares_buf_set_length(conn->in_buf, start_len);
      }
      break;
    }

    /* Record amount of data read */
    ares_buf_append_finish(conn->in_buf, count);

    /* Only loop if sockets support non-blocking operation, and are using UDP
     * or are using TCP and read the maximum buffer size */
    read_again = ARES_FALSE;
    if (channel->sock_funcs.flags & ARES_SOCKFUNC_FLAG_NONBLOCKING &&
        (!(conn->flags & ARES_CONN_FLAG_TCP) || count == len)) {
      read_again = ARES_TRUE;
    }

    /* If UDP, overwrite length */
    if (!(conn->flags & ARES_CONN_FLAG_TCP)) {
      /* The read buffer is grown in powers of two, so a single recvfrom() can
       * return more than the 2-byte length prefix is able to represent.  A
       * datagram that large can't be a valid DNS message, and writing the
       * truncated (unsigned short)count would desync the framing of everything
       * buffered after it, so discard it.  Standard UDP can't actually deliver
       * a payload this large (max is 65507 IPv4 / 65527 IPv6); the only vector
       * is IPv6 jumbograms (RFC 2675), which DNS never uses.  This is purely
       * defense-in-depth. */
      if (count > 65535) {
        ares_buf_set_length(conn->in_buf, start_len);
        break;
      }
      len = ares_buf_len(conn->in_buf);
      ares_buf_set_length(conn->in_buf, start_len);
      ares_buf_append_be16(conn->in_buf, (unsigned short)count);
      ares_buf_set_length(conn->in_buf, len);
    }
    /* Try to read again only if *we* set up the socket, otherwise it may be
     * a blocking socket and would cause recvfrom to hang. */
  } while (read_again);

  if (err != ARES_CONN_ERR_SUCCESS && err != ARES_CONN_ERR_WOULDBLOCK) {
    /* If there is no packet data buffered, preserve the historical
     * immediate connection-failure behavior so retries happen promptly.
     * Only defer if there is buffered data to parse first. */
    if (ares_buf_len(conn->in_buf) == 0) {
      handle_conn_error(conn, ARES_TRUE, ARES_ECONNREFUSED);
      return ARES_ECONNREFUSED;
    }

    *conn_error = ARES_TRUE;
  }

  return ARES_SUCCESS;
}

typedef enum {
  REQUEUE_REQUEUE  = 1,
  REQUEUE_ENDQUERY = 2
} requeue_type_t;

/* Simple data structure to store a query that needs to be requeued with
 * optional server */
typedef struct {
  requeue_type_t     type;   /* type of entry, requeue or endquery */
  unsigned short     qid;    /* query id */
  size_t             serial; /* identifies the query the qid meant when
                              * this entry was appended */
  ares_server_t     *server; /* requeue only: optional */
  ares_status_t      status; /* endquery only */
  ares_dns_record_t *dnsrec; /* endquery only: optional */
} ares_requeue_t;

static ares_status_t ares_append_requeue_int(ares_array_t     **requeue,
                                             requeue_type_t     type,
                                             ares_query_t      *query,
                                             ares_server_t     *server,
                                             ares_status_t      status,
                                             ares_dns_record_t *dnsrec)
{
  ares_requeue_t entry;

  if (*requeue == NULL) {
    *requeue = ares_array_create(sizeof(ares_requeue_t), NULL);
    if (*requeue == NULL) {
      return ARES_ENOMEM;
    }
  }

  ares_query_remove_from_conn(query);

  entry.type   = type;
  entry.qid    = query->qid;
  entry.serial = query->serial;
  entry.server = server;
  entry.status = status;
  entry.dnsrec = dnsrec;
  return ares_array_insertdata_last(*requeue, &entry);
}

static ares_status_t ares_append_requeue(ares_array_t **requeue,
                                         ares_query_t  *query,
                                         ares_server_t *server)
{
  return ares_append_requeue_int(requeue, REQUEUE_REQUEUE, query, server, 0,
    NULL);
}

static ares_status_t ares_append_endqueue(ares_array_t     **requeue,
                                          ares_query_t      *query,
                                          ares_status_t      status,
                                          ares_dns_record_t *dnsrec)
{
  return ares_append_requeue_int(requeue, REQUEUE_ENDQUERY, query, NULL, status,
    dnsrec);
}

/*! Is the query the caller captured still the one holding \a qid?
 *
 *  A query id is a 16-bit wire value reissued as soon as the query holding it
 *  is detached, so it names a slot rather than a query, and comparing the
 *  pointer as well does not help: the replacement is often allocated at the
 *  address the freed one had.  The serial is a channel-wide counter that is
 *  never reused. */
static ares_bool_t ares_query_is_live(const ares_channel_t *channel,
                                      unsigned short qid, size_t serial)
{
  const ares_query_t *query =
    ares_htable_szvp_get_direct(channel->queries_by_qid, qid);

  if (query == NULL || query->serial != serial) {
    return ARES_FALSE;
  }

  return ARES_TRUE;
}

/*! Is this exact server object still in the channel's server list?
 *
 *  Compares pointers only and never dereferences \a server.  Deferred
 *  reclamation keeps an unlinked server readable for the rest of the call
 *  that captured it, so reading through it is no longer a use-after-free --
 *  but it has been removed from the configuration and must not be acted on,
 *  which is what this reports.  A channel has a handful of servers, so the
 *  linear scan does not matter. */
ares_bool_t ares_server_in_channel(const ares_channel_t *channel,
                                   const ares_server_t  *server)
{
  ares_slist_node_t *node;

  if (server == NULL) {
    return ARES_FALSE;
  }

  for (node = ares_slist_node_first(channel->servers); node != NULL;
       node = ares_slist_node_next(node)) {
    if (ares_slist_node_val(node) == server) {
      return ARES_TRUE;
    }
  }

  return ARES_FALSE;
}

/* Drain the deferred requeue/endqueue list iteratively.  All flush sites
 * (read_answers(), process_timeouts(), and ares_send_query()) funnel through
 * here so that:
 *   1. Retries are re-dispatched by appending to this same list and looping,
 *      rather than recursing ares_requeue_query() -> ares_send_query() until
 *      the stack is exhausted (issue #1043).
 *   2. A query is fully detached from all lookup lists before its callback is
 *      invoked, so a reentrant ares_cancel() from within that callback cannot
 *      find and free the same query, which would otherwise double-free it
 *      (CVE-2026-33630 / GHSA-6wfj-rwm7-3542).
 *
 * On return the list has been fully processed, an empty-queue notification has
 * been sent if appropriate, and *requeue has been destroyed and set to NULL. */
static ares_status_t ares_flush_requeue(ares_channel_t       *channel,
                                        const ares_timeval_t *now,
                                        ares_array_t        **requeue)
{
  ares_status_t status = ARES_SUCCESS;

  if (requeue == NULL) {
    return status;
  }

  while (*requeue != NULL && ares_array_len(*requeue) > 0) {
    ares_query_t  *query;
    ares_requeue_t entry;
    ares_status_t  internal_status;

    internal_status = ares_array_claim_at(&entry, sizeof(entry), *requeue, 0);
    if (internal_status != ARES_SUCCESS) {
      break; /* LCOV_EXCL_LINE: DefensiveCoding */
    }

    query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid);

    /* A query id is a 16-bit wire value and generate_unique_qid() only avoids
     * ids that are live when it runs.  Within a single drain, a callback can
     * cancel a query whose entry is still in this array -- releasing its id --
     * and start a new lookup that is handed the same id.  The lookup above
     * would then resolve this entry to a completely different query: an
     * endquery entry would deliver one lookup's answer to another and free it,
     * a requeue entry would put an in-flight query on the wire twice.  The
     * serial is a channel-wide counter that is never reused, so it tells the
     * two apart. */
    if (query != NULL && query->serial != entry.serial) {
      query = NULL;
    }

    if (entry.type == REQUEUE_REQUEUE) {
      /* Query disappeared (e.g. a prior callback in this drain cancelled it) */
      if (query == NULL) {
        continue;
      }
      /* entry.server is a raw pointer captured before any callback in this
       * drain ran.  A callback may legally re-enter the public API -- the
       * channel lock is recursive -- and ares_set_servers*() destroys servers
       * that are absent from the new configuration, so it may already be
       * freed.  The query above is revalidated by qid for exactly this
       * reason; do the same for the server, falling back to ordinary
       * selection when it has gone. */
      if (!ares_server_in_channel(channel, entry.server)) {
        entry.server = NULL;
      }

      /* Re-dispatch via the internal entrypoint so any further requeues are
       * appended back onto this same list and drained by the loop above,
       * rather than recursing. */
      internal_status = ares_send_query_int(entry.server, query, now, requeue);
      /* We only care about ARES_ENOMEM */
      if (internal_status == ARES_ENOMEM) {
        status = ARES_ENOMEM;
      }
    } else { /* REQUEUE_ENDQUERY */
      if (query != NULL) {
        /* Detach the query from all lookup lists BEFORE invoking the callback.
         * Otherwise a reentrant ares_cancel() from within the callback would
         * find this query still linked in all_queries/queries_by_qid, free it,
         * and the ares_free_query() below would then double-free it. */
        ares_detach_query(query);
        query->callback(query->arg, entry.status, query->timeouts,
                        entry.dnsrec);
        ares_free_query(query);
      }
      ares_dns_record_destroy(entry.dnsrec);
    }
  }

  /* Don't forget to send notification if queue emptied */
  if (*requeue != NULL) {
    ares_queue_notify_empty(channel);
  }
  ares_array_destroy(*requeue);
  *requeue = NULL;

  return status;
}

static ares_status_t read_answers(ares_conn_t *conn, const ares_timeval_t *now)
{
  ares_status_t   status;
  ares_channel_t *channel  = conn->server->channel;
  ares_array_t   *requeue  = NULL;

  /* Process all queued answers */
  while (1) {
    unsigned short       dns_len  = 0;
    const unsigned char *data     = NULL;
    size_t               data_len = 0;

    /* Tag so we can roll back */
    ares_buf_tag(conn->in_buf);

    /* Read length indicator */
    status = ares_buf_fetch_be16(conn->in_buf, &dns_len);
    if (status != ARES_SUCCESS) {
      ares_buf_tag_rollback(conn->in_buf);
      break;
    }

    /* Not enough data for a full response yet */
    status = ares_buf_consume(conn->in_buf, dns_len);
    if (status != ARES_SUCCESS) {
      ares_buf_tag_rollback(conn->in_buf);
      break;
    }

    /* Can't fail except for misuse */
    data = ares_buf_tag_fetch(conn->in_buf, &data_len);
    if (data == NULL || data_len < 2) {
      ares_buf_tag_clear(conn->in_buf);
      break;
    }

    /* Strip off 2 bytes length */
    data     += 2;
    data_len -= 2;

    /* We finished reading this answer; process it */
    status = process_answer(channel, data, data_len, conn, now, &requeue);
    if (status != ARES_SUCCESS) {
      handle_conn_error(conn, ARES_TRUE, status);
      goto cleanup;
    }

    /* Since we processed the answer, clear the tag so space can be reclaimed */
    ares_buf_tag_clear(conn->in_buf);
  }

cleanup:
  /* Flush requeue - re-dispatch retries and invoke deferred callbacks
   * iteratively and safely */
  if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) {
    status = ARES_ENOMEM;
  }

  return status;
}

static ares_status_t process_read(ares_channel_t       *channel,
                                  ares_socket_t         read_fd,
                                  const ares_timeval_t *now)
{
  ares_conn_t  *conn = ares_conn_from_fd(channel, read_fd);
  ares_bool_t   conn_error;
  ares_status_t status;

  if (conn == NULL) {
    return ARES_SUCCESS;
  }

  status = read_conn_packets(conn, &conn_error);

  if (status != ARES_SUCCESS) {
    return status;
  }

  /* A later call to the receive function may have reconfigured the servers,
   * tearing this connection down after earlier calls buffered answers on it.
   * Its queries have moved to the new configuration, so drop the answers. */
  if (conn->flags & ARES_CONN_FLAG_DEAD) {
    return ARES_SUCCESS;
  }

  status = read_answers(conn, now);
  if (status != ARES_SUCCESS) {
    return status;
  }

  if (conn_error) {
    conn = ares_conn_from_fd(channel, read_fd);
    if (conn != NULL) {
      handle_conn_error(conn, ARES_TRUE, ARES_ECONNREFUSED);
    }
  }

  return ARES_SUCCESS;
}

/* If any queries have timed out, note the timeout and move them on. */
static ares_status_t process_timeouts(ares_channel_t       *channel,
                                      const ares_timeval_t *now)
{
  ares_slist_node_t *node;
  ares_status_t      status  = ARES_SUCCESS;
  ares_array_t      *requeue = NULL;

  /* Just keep popping off the first as this list will re-sort as things come
   * and go.  We don't want to try to rely on 'next' as some operation might
   * cause a cleanup of that pointer and would become invalid */
  while ((node = ares_slist_node_first(channel->queries_by_timeout)) != NULL) {
    ares_query_t *query = ares_slist_node_val(node);
    ares_conn_t  *conn;

    /* Since this is sorted, as soon as we hit a query that isn't timed out,
     * break */
    if (!ares_timedout(now, &query->timeout)) {
      break;
    }

    query->timeouts++;

    conn = query->conn;
    /* Retire this connection for NEW queries.  A timeout suggests packets are
     * being dropped on it, so route new queries to a fresh source port while
     * the in-flight queries here drain (it is cleaned up once idle).  This is
     * per-connection so a transient failure doesn't stop reuse of healthy
     * connections to the same server. */
    conn->flags |= ARES_CONN_FLAG_NONEW;
    server_increment_failures(conn->server, query->using_tcp);
    status =
      ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, &requeue);
    if (status == ARES_ENOMEM) {
      goto done;
    }
  }
done:
  /* Flush requeue - re-dispatch retries and invoke deferred callbacks
   * iteratively and safely */
  if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) {
    status = ARES_ENOMEM;
  }

  if (status == ARES_ENOMEM) {
    return ARES_ENOMEM;
  }
  return ARES_SUCCESS;
}

static ares_status_t rewrite_without_edns(ares_query_t *query)
{
  ares_status_t status = ARES_SUCCESS;
  size_t        i;
  ares_bool_t   found_opt_rr = ARES_FALSE;

  /* Find and remove the OPT RR record */
  for (i = 0; i < ares_dns_record_rr_cnt(query->query, ARES_SECTION_ADDITIONAL);
       i++) {
    const ares_dns_rr_t *rr;
    rr = ares_dns_record_rr_get(query->query, ARES_SECTION_ADDITIONAL, i);
    if (ares_dns_rr_get_type(rr) == ARES_REC_TYPE_OPT) {
      ares_dns_record_rr_del(query->query, ARES_SECTION_ADDITIONAL, i);
      found_opt_rr = ARES_TRUE;
      break;
    }
  }

  if (!found_opt_rr) {
    status = ARES_EFORMERR;
    goto done;
  }

done:
  return status;
}

static ares_bool_t issue_might_be_edns(const ares_dns_record_t *req,
                                       const ares_dns_record_t *rsp)
{
  const ares_dns_rr_t *rr;

  /* If we use EDNS and server answers with FORMERR without an OPT RR, the
   * protocol extension is not understood by the responder. We must retry the
   * query without EDNS enabled. */
  if (ares_dns_record_get_rcode(rsp) != ARES_RCODE_FORMERR) {
    return ARES_FALSE;
  }

  rr = ares_dns_get_opt_rr_const(req);
  if (rr == NULL) {
    /* We didn't send EDNS */
    return ARES_FALSE;
  }

  if (ares_dns_get_opt_rr_const(rsp) == NULL) {
    /* Spec says EDNS won't be echo'd back on non-supporting servers, so
     * retry without EDNS */
    return ARES_TRUE;
  }

  /* As per issue #911 some non-compliant servers that do indeed support EDNS
   * but don't support unrecognized option codes exist.  At this point we
   * expect them to have also returned an EDNS opt record, but we may remove
   * that check in the future. Lets detect this situation if we're sending
   * option codes */
  if (ares_dns_rr_get_opt_cnt(rr, ARES_RR_OPT_OPTIONS) == 0) {
    /* We didn't send any option codes */
    return ARES_FALSE;
  }

  if (ares_dns_get_opt_rr_const(rsp) != NULL) {
    /* At this time we're requiring the server to respond with EDNS opt
     * records since that's what has been observed in the field.  We might
     * find in the future we have to remove this, who knows. Lets go
     * ahead and force a retry without EDNS*/
    return ARES_TRUE;
  }

  return ARES_FALSE;
}

/* Handle an answer from a server. This must NEVER cleanup the
 * server connection! Return something other than ARES_SUCCESS to cause
 * the connection to be terminated after this call. */
static ares_status_t process_answer(ares_channel_t      *channel,
                                    const unsigned char *abuf, size_t alen,
                                    ares_conn_t          *conn,
                                    const ares_timeval_t *now,
                                    ares_array_t        **requeue)
{
  ares_query_t      *query;
  /* Cache these as once ares_send_query() gets called, it may end up
   * invalidating the connection all-together */
  ares_server_t     *server  = conn->server;
  ares_dns_record_t *rdnsrec = NULL;
  ares_status_t      status;
  ares_bool_t        is_cached = ARES_FALSE;

  /* UDP can have 0-byte messages, drop them to the ground */
  if (alen == 0) {
    return ARES_SUCCESS;
  }

  /* Parse the response */
  status = ares_dns_parse(abuf, alen, 0, &rdnsrec);
  if (status != ARES_SUCCESS) {
    /* Malformations are never accepted */
    status = ARES_EBADRESP;
    goto cleanup;
  }

  /* Find the query corresponding to this packet. The queries are
   * hashed/bucketed by query id, so this lookup should be quick.
   */
  query = ares_htable_szvp_get_direct(channel->queries_by_qid,
                                      ares_dns_record_get_id(rdnsrec));
  if (!query) {
    /* We may have stopped listening for this query, that's ok */
    status = ARES_SUCCESS;
    goto cleanup;
  }

  /* Both the query id and the questions must be the same. We will drop any
   * replies that aren't for the same query as this is considered invalid. */
  if (!same_questions(query, rdnsrec)) {
    /* Possible qid conflict due to delayed response, that's ok */
    status = ARES_SUCCESS;
    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)
      != ARES_SUCCESS) {
    /* Drop response and return */
    status = ARES_SUCCESS;
    goto cleanup;
  }

  /* At this point we know we've received an answer for this query, so we should
   * remove it from the connection's queue so we can possibly invalidate the
   * connection. Delay cleaning up the connection though as we may enqueue
   * something new.  */
  ares_llist_node_destroy(query->node_queries_to_conn);
  query->node_queries_to_conn = NULL;

  /* There are old servers that don't understand EDNS at all, then some servers
   * that have non-compliant implementations.  Lets try to detect this sort
   * of thing. */
  if (issue_might_be_edns(query->query, rdnsrec)) {
    status = rewrite_without_edns(query);
    if (status != ARES_SUCCESS) {
      end_query(channel, server, query, status, NULL, requeue);
      goto cleanup;
    }

    /* Requeue to same server */
    status = ares_append_requeue(requeue, query, server);
    goto cleanup;
  }

  /* If we got a truncated UDP packet and are not ignoring truncation,
   * don't accept the packet, and switch the query to TCP if we hadn't
   * done so already.
   */
  if (ares_dns_record_get_flags(rdnsrec) & ARES_FLAG_TC &&
      !(conn->flags & ARES_CONN_FLAG_TCP) &&
      !(channel->flags & ARES_FLAG_IGNTC)) {
    query->using_tcp = ARES_TRUE;
    status = ares_append_requeue(requeue, query, NULL);
    /* Status will reflect success except on memory error, which is good since
     * requeuing to TCP is ok */
    goto cleanup;
  }

  /* If we aren't passing through all error packets, discard packets
   * with SERVFAIL, NOTIMP, or REFUSED response codes.
   */
  if (!(channel->flags & ARES_FLAG_NOCHECKRESP)) {
    ares_dns_rcode_t rcode = ares_dns_record_get_rcode(rdnsrec);
    if (rcode == ARES_RCODE_SERVFAIL || rcode == ARES_RCODE_NOTIMP ||
        rcode == ARES_RCODE_REFUSED) {
      switch (rcode) {
        case ARES_RCODE_SERVFAIL:
          status = ARES_ESERVFAIL;
          break;
        case ARES_RCODE_NOTIMP:
          status = ARES_ENOTIMP;
          break;
        case ARES_RCODE_REFUSED:
          status = ARES_EREFUSED;
          break;
        default:
          break;
      }

      server_increment_failures(server, query->using_tcp);
      status = ares_requeue_query(query, now, status, ARES_TRUE, rdnsrec,
        requeue);
      rdnsrec = NULL; /* Free'd by ares_requeue_query() */

      if (status != ARES_ENOMEM) {
        /* Should any of these cause a connection termination?
         * Maybe SERVER_FAILURE? */
        status = ARES_SUCCESS;
      }
      goto cleanup;
    }
  }

  /* If cache insertion was successful, it took ownership.  We ignore
   * other cache insertion failures. */
  ares_qcache_insert(channel, now, query, rdnsrec);

  server_set_good(server, query->using_tcp);
  end_query(channel, server, query, ARES_SUCCESS, rdnsrec, requeue);
  rdnsrec = NULL; /* Free'd by the requeue */

  status = ARES_SUCCESS;

cleanup:
  /* Don't cleanup the cached pointer to the dns response */
  if (!is_cached) {
    ares_dns_record_destroy(rdnsrec);
  }

  return status;
}

static void handle_conn_error(ares_conn_t *conn, ares_bool_t critical_failure,
                              ares_status_t failure_status)
{
  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) {
    server_increment_failures(
      server, (conn->flags & ARES_CONN_FLAG_TCP) ? ARES_TRUE : ARES_FALSE);
  }

  /* This will requeue any connections automatically */
  ares_close_connection(conn, failure_status);
}

/* Requeue query will normally call ares_send_query() but in some circumstances
 * this needs to be delayed, so if requeue is not NULL, it will add the query
 * to the queue instead */
ares_status_t ares_requeue_query(ares_query_t *query, const ares_timeval_t *now,
                                 ares_status_t            status,
                                 ares_bool_t              inc_try_count,
                                 ares_dns_record_t       *dnsrec,
                                 ares_array_t           **requeue)
{
  ares_channel_t *channel   = query->channel;
  size_t          max_tries = ares_slist_len(channel->servers) * channel->tries;
  ares_server_t  *server    = NULL;

  if (query->conn != NULL) {
     server = query->conn->server;
  }

  ares_query_remove_from_conn(query);

  if (status != ARES_SUCCESS) {
    query->error_status = status;
  }

  if (inc_try_count) {
    query->try_count++;
  }

  if (query->try_count < max_tries && !query->no_retries) {
    ares_dns_record_destroy(dnsrec);
    if (requeue != NULL) {
      return ares_append_requeue(requeue, query, NULL);
    }
    return ares_send_query(NULL, query, now);
  }

  /* If we are here, all attempts to perform query failed. */
  if (query->error_status == ARES_SUCCESS) {
    query->error_status = ARES_ETIMEOUT;
  }

  end_query(channel, server, query, query->error_status, dnsrec, requeue);
  return ARES_ETIMEOUT;
}

/*! Count the number of servers that share the same highest priority (lowest
 *  consecutive failures).  Since they are sorted in priority order, we just
 *  stop when the consecutive failure count changes. Used for random selection
 *  of good servers. */
static size_t count_highest_prio_servers(const ares_channel_t *channel)
{
  ares_slist_node_t *node;
  size_t             cnt                  = 0;
  size_t             last_consec_failures = SIZE_MAX;

  for (node = ares_slist_node_first(channel->servers); node != NULL;
       node = ares_slist_node_next(node)) {
    const ares_server_t *server = ares_slist_node_val(node);

    if (last_consec_failures != SIZE_MAX &&
        last_consec_failures < server->consec_failures) {
      break;
    }

    last_consec_failures = server->consec_failures;
    cnt++;
  }

  return cnt;
}

/* Pick a random *best* server from the list, we first get a random number in
 * the range of the number of *best* servers, then scan until we find that
 * server in the list */
static ares_server_t *ares_random_server(ares_channel_t *channel)
{
  unsigned char      c;
  size_t             cnt;
  size_t             idx;
  ares_slist_node_t *node;
  size_t             num_servers = count_highest_prio_servers(channel);

  /* Silence coverity, not possible */
  if (num_servers == 0) {
    return NULL;
  }

  ares_rand_bytes(channel->rand_state, &c, 1);

  cnt = c;
  idx = cnt % num_servers;

  cnt = 0;
  for (node = ares_slist_node_first(channel->servers); node != NULL;
       node = ares_slist_node_next(node)) {
    if (cnt == idx) {
      return ares_slist_node_val(node);
    }

    cnt++;
  }

  return NULL;
}

static void server_probe_cb(void *arg, ares_status_t status, size_t timeouts,
                            const ares_dns_record_t *dnsrec)
{
  (void)arg;
  (void)status;
  (void)timeouts;
  (void)dnsrec;
  /* Nothing to do, the logic internally will handle success/fail of this */
}

/* Determine if we should probe a downed server */
static void ares_probe_failed_server(ares_channel_t      *channel,
                                     const ares_server_t *server,
                                     const ares_query_t  *query)
{
  const ares_server_t *last_server = ares_slist_last_val(channel->servers);
  unsigned short       r;
  ares_timeval_t       now;
  ares_slist_node_t   *node;
  ares_server_t       *probe_server = NULL;

  /* If no servers have failures, or we're not configured with a server retry
   * chance, then nothing to probe */
  if ((last_server != NULL && last_server->consec_failures == 0) ||
      channel->server_retry_chance == 0) {
    return;
  }

  /* Generate a random value to decide whether to retry a failed server. The
   * probability to use is 1/channel->server_retry_chance, rounded up to a
   * precision of 1/2^B where B is the number of bits in the random value.
   * We use an unsigned short for the random value for increased precision.
   */
  ares_rand_bytes(channel->rand_state, (unsigned char *)&r, sizeof(r));
  if (r % channel->server_retry_chance != 0) {
    return;
  }

  /* Select the first server with failures to retry that has passed the retry
   * timeout and doesn't already have a pending probe.  Skip the server the
   * triggering query was just sent to since that query is already exercising
   * it. */
  ares_tvnow(&now);
  for (node = ares_slist_node_first(channel->servers); node != NULL;
       node = ares_slist_node_next(node)) {
    ares_server_t *node_val = ares_slist_node_val(node);
    if (node_val != NULL && node_val != server &&
        node_val->consec_failures > 0 && !node_val->probe_pending &&
        ares_timedout(&now, &node_val->next_retry_time)) {
      probe_server = node_val;
      break;
    }
  }

  /* Nothing to probe */
  if (probe_server == NULL) {
    return;
  }

  /* Enqueue an identical query onto the specified server without honoring
   * the cache or allowing retries.  We want to make sure it only attempts to
   * use the server in question */
  probe_server->probe_pending = ARES_TRUE;
  ares_send_nolock(channel, probe_server,
                   ARES_SEND_FLAG_NOCACHE | ARES_SEND_FLAG_NORETRY,
                   query->query, server_probe_cb, NULL, NULL);
}

static size_t ares_calc_query_timeout(const ares_query_t   *query,
                                      const ares_server_t  *server,
                                      const ares_timeval_t *now)
{
  const ares_channel_t *channel  = query->channel;
  size_t                timeout  = ares_metrics_server_timeout(server, now);
  size_t                timeplus = timeout;
  size_t                rounds;
  size_t                num_servers = ares_slist_len(channel->servers);

  if (num_servers == 0) {
    return 0; /* LCOV_EXCL_LINE: DefensiveCoding */
  }

  /* For each trip through the entire server list, we want to double the
   * retry from the last retry */
  rounds = (query->try_count / num_servers);
  if (rounds > 0) {
    if (rounds >= sizeof(timeplus) * CHAR_BIT ||
        timeplus > (SIZE_MAX >> rounds)) {
      timeplus = SIZE_MAX;
    } else {
      timeplus <<= rounds;
    }
  }

  if (channel->maxtimeout && timeplus > channel->maxtimeout) {
    timeplus = channel->maxtimeout;
  }

  /* Add some jitter to the retry timeout.
   *
   * Jitter is needed in situation when resolve requests are performed
   * simultaneously from multiple hosts and DNS server throttle these requests.
   * Adding randomness allows to avoid synchronisation of retries.
   *
   * Value of timeplus adjusted randomly to the range [0.5 * timeplus,
   * timeplus].
   */
  if (rounds > 0) {
    unsigned short r;
    float          delta_multiplier;

    ares_rand_bytes(channel->rand_state, (unsigned char *)&r, sizeof(r));
    delta_multiplier  = ((float)r / USHRT_MAX) * 0.5f;
    timeplus         -= (size_t)((float)timeplus * delta_multiplier);
  }

  /* We want explicitly guarantee that timeplus is greater or equal to timeout
   * specified in channel options. */
  if (timeplus < timeout) {
    timeplus = timeout;
  }

  return timeplus;
}

static ares_conn_t *ares_fetch_connection(const ares_channel_t *channel,
                                          ares_server_t        *server,
                                          const ares_query_t   *query)
{
  ares_llist_node_t *node;
  ares_conn_t       *conn;

  if (query->using_tcp) {
    return server->tcp_conn;
  }

  /* Fetch existing UDP connection */
  node = ares_llist_node_first(server->connections);
  if (node == NULL) {
    return NULL;
  }

  conn = ares_llist_node_val(node);
  /* Not UDP, skip */
  if (conn->flags & ARES_CONN_FLAG_TCP) {
    return NULL;
  }

  /* Don't hand new queries to a connection that has been retired (e.g. it saw
   * a timeout).  It keeps servicing its in-flight queries and is cleaned up
   * once idle.  Note this is a per-connection check: a server-wide failure
   * counter must not be used here or a single transient failure would evict
   * every (including healthy) connection to the server and spawn a new socket
   * per query. */
  if (conn->flags & ARES_CONN_FLAG_NONEW) {
    return NULL;
  }

  /* Used too many times */
  if (channel->udp_max_queries > 0 &&
      conn->total_queries >= channel->udp_max_queries) {
    return NULL;
  }

  return conn;
}

static ares_status_t ares_conn_query_write(ares_conn_t          *conn,
                                           ares_query_t         *query,
                                           const ares_timeval_t *now)
{
  ares_server_t  *server  = conn->server;
  ares_channel_t *channel = server->channel;
  ares_status_t   status;

  status = ares_cookie_apply(query->query, conn, now);
  if (status != ARES_SUCCESS) {
    return status;
  }

  /* We write using the TCP format even for UDP, we just strip the length
   * before putting on the wire */
  status = ares_dns_write_buf_tcp(query->query, conn->out_buf);
  if (status != ARES_SUCCESS) {
    return status;
  }

  /* Not pending a TFO write and not connected, so we can't even try to
   * write until we get a signal */
  if (conn->flags & ARES_CONN_FLAG_TCP &&
      !(conn->state_flags & ARES_CONN_STATE_CONNECTED) &&
      !(conn->flags & ARES_CONN_FLAG_TFO_INITIAL)) {
    return ARES_SUCCESS;
  }

  /* Delay actual write if possible (TCP only, and only if callback
   * configured) */
  if (channel->notify_pending_write_cb && !channel->notify_pending_write &&
      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;
  }

  /* Unfortunately we need to write right away and can't aggregate multiple
   * queries into a single write. */
  return ares_conn_flush(conn);
}

/* Public entrypoint.  Establishes a requeue list and drives
 * ares_send_query_int() plus any retries/deferred callbacks it produces
 * iteratively, so a chain of retryable failures can never recurse until the
 * stack is exhausted (#1043). */
ares_status_t ares_send_query(ares_server_t *requested_server,
                              ares_query_t *query, const ares_timeval_t *now)
{
  ares_channel_t *channel = query->channel;
  ares_array_t   *requeue = NULL;
  unsigned short  qid     = query->qid;
  size_t          serial  = query->serial;
  ares_status_t   status;

  status = ares_send_query_int(requested_server, query, now, &requeue);

  /* Drain any retries/deferred callbacks this send produced.
   * ares_flush_requeue() always fully processes and destroys the list (even on
   * ENOMEM), and sends the empty-queue notification if needed. */
  if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) {
    status = ARES_ENOMEM;
  }

  /* A retry may have been deferred (returning ARES_SUCCESS from the append)
   * and then terminally failed while draining, in which case the query has
   * been freed.  Do not dereference 'query' here.  If it is no longer tracked
   * it ended, so don't report success to the caller (which would, e.g., cause
   * ares_send_nolock() to write to a now-freed *qid). */
  if (status == ARES_SUCCESS && !ares_query_is_live(channel, qid, serial)) {
    status = ARES_ETIMEOUT;
  }

  return status;
}

static ares_status_t ares_send_query_int(ares_server_t        *requested_server,
                                         ares_query_t         *query,
                                         const ares_timeval_t *now,
                                         ares_array_t        **requeue)
{
  ares_channel_t *channel = query->channel;
  ares_server_t  *server;
  ares_conn_t    *conn;
  size_t          timeplus;
  ares_status_t   status;
  ares_bool_t     probe_downed_server = ARES_TRUE;
  /* Captured before any application callback below, which may free the query */
  unsigned short  qid    = query->qid;
  size_t          serial = query->serial;

  /* Choose the server to send the query to */
  if (requested_server != NULL) {
    server = requested_server;
  } else {
    /* If rotate is turned on, do a random selection */
    if (channel->rotate) {
      server = ares_random_server(channel);
    } else {
      /* First server in list */
      server = ares_slist_first_val(channel->servers);
    }
  }

  if (server == NULL) {
    end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, requeue);
    return ARES_ENOSERVER;
  }

  /* If a query is directed to a specific server, or the query is being
   * retried, don't probe for downed servers.  Note that the chosen server
   * having failures itself does NOT disable probing: when every server has
   * failures (e.g. during an extended outage), probes are the only way a
   * recovered server that sorts behind the current best can be noticed. */
  if (requested_server != NULL || query->try_count != 0) {
    probe_downed_server = ARES_FALSE;
  }

  conn = ares_fetch_connection(channel, server, query);
  if (conn == NULL) {
    status = ares_open_connection(&conn, channel, server, query->using_tcp);

    /* Opening a connection runs application code -- the socket-open function
     * from ares_set_socket_functions_ex(), and the ares_set_socket_callback()
     * and ares_set_socket_configure_callback() callbacks -- any of which may
     * reentrantly call ares_cancel() and free this query.  Re-check liveness
     * by the qid captured at entry rather than dereferencing a possibly-freed
     * query. */
    if (!ares_query_is_live(channel, qid, serial)) {
      return ARES_ECANCELLED;
    }

    /* Those same callbacks may have reconfigured the servers, which frees the
     * ones the new configuration omits.  'server' was captured before the
     * open; everything below reads it, including server_increment_failures()
     * on the failure paths of the switch.
     *
     * Requeue rather than return: every exit from this function must either
     * enqueue the query or end it, and the query is still live here -- it has
     * not been written, is not attached to a connection, and on a first send
     * has no timeout node, so returning would strand it forever.
     * query->conn is NULL, so this does not read through the freed server. */
    if (!ares_server_in_channel(channel, server)) {
      return ares_requeue_query(query, now, ARES_ECONNREFUSED, ARES_TRUE, NULL,
                                requeue);
    }

    switch (status) {
      /* Good result, continue on */
      case ARES_SUCCESS:
        break;

      /* These conditions are retryable as they are server-specific
       * error codes */
      case ARES_ECONNREFUSED:
      case ARES_EBADFAMILY:
        server_increment_failures(server, query->using_tcp);
        return ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue);

      /* Anything else is not retryable, likely ENOMEM */
      default:
        end_query(channel, server, query, status, NULL, requeue);
        return status;
    }
  }

  /* Write the query */
  status = ares_conn_query_write(conn, query, now);

  /* Writing runs application code too: ares_conn_query_write() reaches the
   * send function installed by ares_set_socket_functions_ex(), which may
   * reentrantly call ares_cancel() with the same hazard as above. */
  if (!ares_query_is_live(channel, qid, serial)) {
    return ARES_ECANCELLED;
  }

  /* Application code that tore this connection down during the write -- the
   * send function, or the pending-write callback when the write is deferred,
   * reconfiguring the servers, say -- surfaces here as ARES_ECONNREFUSED,
   * even if the send itself reported success or never happened, so the query
   * is requeued below rather than attached to the closed connection. */
  switch (status) {
    /* Good result, continue on */
    case ARES_SUCCESS:
      break;

    case ARES_ENOMEM:
      /* Not retryable */
      end_query(channel, server, query, status, NULL, requeue);
      return status;

    /* These conditions are retryable as they are server-specific
     * error codes */
    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;
      }
      return status;

    default:
      server_increment_failures(server, query->using_tcp);
      status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue);
      return status;
  }

  timeplus = ares_calc_query_timeout(query, server, now);
  /* Keep track of queries bucketed by timeout, so we can process
   * timeout events quickly.
   */
  ares_slist_node_destroy(query->node_queries_by_timeout);
  query->ts      = *now;
  query->timeout = *now;
  ares_timeval_add(&query->timeout, timeplus);
  query->node_queries_by_timeout =
    ares_slist_insert(channel->queries_by_timeout, query);
  if (!query->node_queries_by_timeout) {
    /* LCOV_EXCL_START: OutOfMemory */
    end_query(channel, server, query, ARES_ENOMEM, NULL, requeue);
    return ARES_ENOMEM;
    /* LCOV_EXCL_STOP */
  }

  /* Keep track of queries bucketed by connection, so we can process errors
   * quickly. */
  ares_llist_node_destroy(query->node_queries_to_conn);
  query->node_queries_to_conn =
    ares_llist_insert_last(conn->queries_to_conn, query);

  if (query->node_queries_to_conn == NULL) {
    /* LCOV_EXCL_START: OutOfMemory */
    end_query(channel, server, query, ARES_ENOMEM, NULL, requeue);
    return ARES_ENOMEM;
    /* LCOV_EXCL_STOP */
  }

  query->conn = conn;
  conn->total_queries++;

  /* We just successfully enqueud a query, see if we should probe downed
   * servers. */
  if (probe_downed_server) {
    ares_probe_failed_server(channel, server, query);
  }

  if (channel->query_enqueue_cb) {
    channel->query_enqueue_cb(channel->query_enqueue_cb_data);
  }

  return ARES_SUCCESS;
}

static ares_bool_t same_questions(const ares_query_t      *query,
                                  const ares_dns_record_t *arec)
{
  size_t                   i;
  ares_bool_t              rv      = ARES_FALSE;
  const ares_dns_record_t *qrec    = query->query;
  const ares_channel_t    *channel = query->channel;


  if (ares_dns_record_query_cnt(qrec) != ares_dns_record_query_cnt(arec)) {
    goto done;
  }

  for (i = 0; i < ares_dns_record_query_cnt(qrec); i++) {
    const char         *qname = NULL;
    const char         *aname = NULL;
    ares_dns_rec_type_t qtype;
    ares_dns_rec_type_t atype;
    ares_dns_class_t    qclass;
    ares_dns_class_t    aclass;

    if (ares_dns_record_query_get(qrec, i, &qname, &qtype, &qclass) !=
          ARES_SUCCESS ||
        qname == NULL) {
      goto done;
    }

    if (ares_dns_record_query_get(arec, i, &aname, &atype, &aclass) !=
          ARES_SUCCESS ||
        aname == NULL) {
      goto done;
    }

    if (qtype != atype || qclass != aclass) {
      goto done;
    }

    if (channel->flags & ARES_FLAG_DNS0x20 && !query->using_tcp) {
      /* NOTE: for DNS 0x20, part of the protection is to use a case-sensitive
       *       comparison of the DNS query name.  This expects the upstream DNS
       *       server to preserve the case of the name in the response packet.
       *       https://datatracker.ietf.org/doc/html/draft-vixie-dnsext-dns0x20-00
       */
      if (!ares_streq(qname, aname)) {
        goto done;
      }
    } else {
      /* without DNS0x20 use case-insensitive matching */
      if (!ares_strcaseeq(qname, aname)) {
        goto done;
      }
    }
  }

  rv = ARES_TRUE;

done:
  return rv;
}

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);
  /* Only release the qid slot if it is still ours.  A query detached before
   * its callback can have its qid reissued to a lookup started from within
   * that callback, and the ares_free_query() that follows detaches a second
   * time -- which would otherwise evict the new lookup instead. */
  if (ares_htable_szvp_get_direct(query->channel->queries_by_qid, query->qid) ==
      query) {
    ares_htable_szvp_remove(query->channel->queries_by_qid, query->qid);
  }
  ares_llist_node_destroy(query->node_all_queries);
  query->node_all_queries = NULL;
}

static void end_query(ares_channel_t *channel, ares_server_t *server,
                      ares_query_t *query, ares_status_t status,
                      ares_dns_record_t *dnsrec, ares_array_t **requeue)
{
  /* If we were probing for the server to come back online, lets mark it as
   * no longer being probed */
  if (server != NULL) {
    server->probe_pending = ARES_FALSE;
  }

  ares_metrics_record(query, server, status, dnsrec);

  /* Delay calling the query callback */
  if (requeue != NULL) {
    ares_append_endqueue(requeue, query, status, dnsrec);
    return;
  }

  /* Detach from all lookup lists before the callback, otherwise a reentrant
   * ares_cancel() from within it would find this query still linked, free it,
   * and the ares_free_query() below would double-free.  Mirrors
   * ares_flush_requeue() for every synchronous path that reaches here with a
   * NULL requeue: connection teardown via handle_conn_error(), and server
   * removal via ares_servers_update() and ares_destroy_servers_state(), both
   * of which reach ares_close_sockets().
   * ares_detach_query() may safely run twice on the same query. */
  ares_detach_query(query);

  /* Invoke the callback. */
  query->callback(query->arg, status, query->timeouts, dnsrec);
  ares_free_query(query);

  /* 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) The current query is detached above, before the callback, so by this
   *     point it is no longer tracked.
   */
  ares_queue_notify_empty(channel);
}

void ares_free_query(ares_query_t *query)
{
  ares_detach_query(query);
  /* Zero out some important stuff, to help catch bugs */
  query->callback = NULL;
  query->arg      = NULL;
  /* Deallocate the memory associated with the query */
  ares_dns_record_destroy(query->query);
  ares_free(query->qcache_key);

  ares_free(query);
}