/* 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);
}