On 2026/07/06 20:09, Volker Schlecht wrote: > Fixes > > Use-after-free / double-free in c-ares query-completion handling, remotely > triggerable via ares_getaddrinfo() over TCP > https://github.com/c-ares/c-ares/security/advisories/GHSA-6wfj-rwm7-3542 and here's a backport of the relevant commits for -stable Index: Makefile =================================================================== RCS file: /cvs/ports/net/libcares/Makefile,v diff -u -p -r1.41 Makefile --- Makefile 16 Jan 2026 16:06:46 -0000 1.41 +++ Makefile 7 Jul 2026 09:56:43 -0000 @@ -1,6 +1,7 @@ COMMENT= asynchronous resolver library V= 1.34.6 +REVISION= 0 DISTNAME= c-ares-${V} PKGNAME= libcares-${V} CATEGORIES= net devel Index: patches/patch-src_lib_ares_process_c =================================================================== RCS file: patches/patch-src_lib_ares_process_c diff -N patches/patch-src_lib_ares_process_c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ patches/patch-src_lib_ares_process_c 7 Jul 2026 09:56:43 -0000 @@ -0,0 +1,305 @@ +From d823199b688052dcdc1646f2ab4cb8c16b1c644a Mon Sep 17 00:00:00 2001 +From: Brad House <brad@brad-house.com> +Date: Mon, 6 Jul 2026 11:19:36 -0400 +Subject: [PATCH] [Backport v1.34] Fix double-free in process_timeouts() and + consolidate requeue handling (#1237) + +Index: src/lib/ares_process.c +--- src/lib/ares_process.c.orig ++++ src/lib/ares_process.c +@@ -66,6 +66,11 @@ static void end_query(ares_channel_t *channel, + 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_detach_query(ares_query_t *query); + + static void ares_query_remove_from_conn(ares_query_t *query) + { +@@ -573,6 +578,79 @@ static ares_status_t ares_append_endqueue(ares_array_t + dnsrec); + } + ++/* 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); ++ ++ if (entry.type == REQUEUE_REQUEUE) { ++ /* Query disappeared (e.g. a prior callback in this drain cancelled it) */ ++ if (query == NULL) { ++ continue; ++ } ++ /* 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; +@@ -625,43 +703,11 @@ static ares_status_t read_answers(ares_conn_t *conn, c + } + + cleanup: +- +- /* Flush requeue */ +- while (ares_array_len(requeue) > 0) { +- ares_query_t *query; +- ares_requeue_t entry; +- ares_status_t internal_status; +- +- internal_status = ares_array_claim_at(&entry, sizeof(entry), requeue, 0); +- if (internal_status != ARES_SUCCESS) { +- break; +- } +- +- query = ares_htable_szvp_get_direct(channel->queries_by_qid, entry.qid); +- +- if (entry.type == REQUEUE_REQUEUE) { +- /* query disappeared */ +- if (query == NULL) { +- continue; +- } +- internal_status = ares_send_query(entry.server, query, now); +- /* We only care about ARES_ENOMEM */ +- if (internal_status == ARES_ENOMEM) { +- status = ARES_ENOMEM; +- } +- } else { /* REQUEUE_ENDQUERY */ +- if (query != NULL) { +- query->callback(query->arg, entry.status, query->timeouts, entry.dnsrec); +- ares_free_query(query); +- } +- ares_dns_record_destroy(entry.dnsrec); +- } ++ /* Flush requeue - re-dispatch retries and invoke deferred callbacks ++ * iteratively and safely */ ++ if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) { ++ status = ARES_ENOMEM; + } +- /* Don't forget to send notification if queue emptied */ +- if (requeue != NULL) { +- ares_queue_notify_empty(channel); +- } +- ares_array_destroy(requeue); + + return status; + } +@@ -696,7 +742,8 @@ static ares_status_t process_timeouts(ares_channel_t + const ares_timeval_t *now) + { + ares_slist_node_t *node; +- ares_status_t status = ARES_SUCCESS; ++ ares_status_t status = ARES_SUCCESS; ++ ares_array_t *requeue = NULL; + + /* Just keep popping off the first as this list will re-sort as things come + * and go. We don't want to try to rely on 'next' as some operation might +@@ -715,13 +762,19 @@ static ares_status_t process_timeouts(ares_channel_t + + conn = query->conn; + server_increment_failures(conn->server, query->using_tcp); +- status = ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, +- NULL); ++ status = ++ ares_requeue_query(query, now, ARES_ETIMEOUT, ARES_TRUE, NULL, &requeue); + if (status == ARES_ENOMEM) { + goto done; + } + } + done: ++ /* Flush requeue - re-dispatch retries and invoke deferred callbacks ++ * iteratively and safely */ ++ if (ares_flush_requeue(channel, now, &requeue) == ARES_ENOMEM) { ++ status = ARES_ENOMEM; ++ } ++ + if (status == ARES_ENOMEM) { + return ARES_ENOMEM; + } +@@ -871,7 +924,7 @@ static ares_status_t process_answer(ares_channel_t + if (issue_might_be_edns(query->query, rdnsrec)) { + status = rewrite_without_edns(query); + if (status != ARES_SUCCESS) { +- end_query(channel, server, query, status, NULL, NULL); ++ end_query(channel, server, query, status, NULL, requeue); + goto cleanup; + } + +@@ -1262,10 +1315,46 @@ static ares_status_t ares_conn_query_write(ares_conn_t + 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; ++ 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_htable_szvp_get_direct(channel->queries_by_qid, qid) == NULL) { ++ 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; +@@ -1286,7 +1375,7 @@ ares_status_t ares_send_query(ares_server_t *requested + } + + if (server == NULL) { +- end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, NULL); ++ end_query(channel, server, query, ARES_ENOSERVER /* ? */, NULL, requeue); + return ARES_ENOSERVER; + } + +@@ -1310,11 +1399,11 @@ ares_status_t ares_send_query(ares_server_t *requested + case ARES_ECONNREFUSED: + case ARES_EBADFAMILY: + server_increment_failures(server, query->using_tcp); +- return ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); ++ return ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); + + /* Anything else is not retryable, likely ENOMEM */ + default: +- end_query(channel, server, query, status, NULL, NULL); ++ end_query(channel, server, query, status, NULL, requeue); + return status; + } + } +@@ -1328,7 +1417,7 @@ ares_status_t ares_send_query(ares_server_t *requested + + case ARES_ENOMEM: + /* Not retryable */ +- end_query(channel, server, query, status, NULL, NULL); ++ end_query(channel, server, query, status, NULL, requeue); + return status; + + /* These conditions are retryable as they are server-specific +@@ -1336,7 +1425,7 @@ ares_status_t ares_send_query(ares_server_t *requested + case ARES_ECONNREFUSED: + case ARES_EBADFAMILY: + handle_conn_error(conn, ARES_TRUE, status); +- status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); ++ status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); + if (status == ARES_ETIMEOUT) { + status = ARES_ECONNREFUSED; + } +@@ -1344,7 +1433,7 @@ ares_status_t ares_send_query(ares_server_t *requested + + default: + server_increment_failures(server, query->using_tcp); +- status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, NULL); ++ status = ares_requeue_query(query, now, status, ARES_TRUE, NULL, requeue); + return status; + } + +@@ -1360,7 +1449,7 @@ ares_status_t ares_send_query(ares_server_t *requested + 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, NULL); ++ end_query(channel, server, query, ARES_ENOMEM, NULL, requeue); + return ARES_ENOMEM; + /* LCOV_EXCL_STOP */ + } +@@ -1373,7 +1462,7 @@ ares_status_t ares_send_query(ares_server_t *requested + + if (query->node_queries_to_conn == NULL) { + /* LCOV_EXCL_START: OutOfMemory */ +- end_query(channel, server, query, ARES_ENOMEM, NULL, NULL); ++ end_query(channel, server, query, ARES_ENOMEM, NULL, requeue); + return ARES_ENOMEM; + /* LCOV_EXCL_STOP */ + } Index: patches/patch-src_lib_record_ares_dns_name_c =================================================================== RCS file: patches/patch-src_lib_record_ares_dns_name_c diff -N patches/patch-src_lib_record_ares_dns_name_c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ patches/patch-src_lib_record_ares_dns_name_c 7 Jul 2026 09:56:43 -0000 @@ -0,0 +1,95 @@ +From 5c8341ba6ff3a8e4e4dfd616f8ed0418838b8b7b Mon Sep 17 00:00:00 2001 +From: "c-ares-backport-gha[bot]" + <299594594+c-ares-backport-gha[bot]@users.noreply.github.com> +Date: Sun, 5 Jul 2026 05:41:49 +0000 +Subject: [PATCH] [Backport v1.34] enforce 255 octet name limit when parsing + dns names (#1210) + +Index: src/lib/record/ares_dns_name.c +--- src/lib/record/ares_dns_name.c.orig ++++ src/lib/record/ares_dns_name.c +@@ -25,6 +25,18 @@ + */ + #include "ares_private.h" + ++/* RFC 1035 3.1 limits a name to 255 octets. We track presentation length ++ * (label octets plus one separator before each label after the first), which is ++ * up to ~2 octets looser than the strict wire limit and so never rejects a ++ * compliant name. Shared by the read and write paths. */ ++#define ARES_MAX_NAME_PRESENTATION_LEN 255 ++ ++/* A name of <= 255 octets holds at most 128 labels, so it can never legitimately ++ * require more than that many compression-pointer jumps. Bounding the number of ++ * indirections stops a name built purely from pointers (which never adds label ++ * bytes, so the length cap never fires) from being walked without limit. */ ++#define ARES_MAX_INDIRS 128 ++ + typedef struct { + char *name; + size_t name_len; +@@ -48,7 +60,7 @@ static ares_status_t ares_nameoffset_create(ares_llist + ares_nameoffset_t *off = NULL; + + if (list == NULL || name == NULL || ares_strlen(name) == 0 || +- ares_strlen(name) > 255) { ++ ares_strlen(name) > ARES_MAX_NAME_PRESENTATION_LEN) { + return ARES_EFORMERR; /* LCOV_EXCL_LINE: DefensiveCoding */ + } + +@@ -335,7 +347,8 @@ static ares_status_t ares_split_dns_name(ares_array_t + } + + /* Can't exceed maximum (unescaped) length */ +- if (ares_array_len(labels) && total_len + ares_array_len(labels) - 1 > 255) { ++ if (ares_array_len(labels) && ++ total_len + ares_array_len(labels) - 1 > ARES_MAX_NAME_PRESENTATION_LEN) { + status = ARES_EBADNAME; + goto done; + } +@@ -538,6 +551,8 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, cha + ares_status_t status; + ares_buf_t *namebuf = NULL; + size_t label_start = ares_buf_get_position(buf); ++ size_t name_len = 0; ++ size_t indir = 0; + + if (buf == NULL) { + return ARES_EFORMERR; +@@ -609,6 +624,16 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, cha + goto fail; + } + ++ /* Bound the number of indirections. A name made purely of pointers never ++ * adds label bytes, so the length cap below can't stop it; the ++ * strictly-decreasing rule alone still allows thousands of jumps per name. ++ * No legitimate <= 255 octet name needs more than 128 labels/jumps. */ ++ indir++; ++ if (indir > ARES_MAX_INDIRS) { ++ status = ARES_EBADNAME; ++ goto fail; ++ } ++ + /* First time we make a jump, save the current position */ + if (save_offset == 0) { + save_offset = ares_buf_get_position(buf); +@@ -631,6 +656,20 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, cha + } + + /* New label */ ++ ++ /* RFC 1035 3.1 limits a name to 255 octets. Enforce it during ++ * decompression so labels reached through a chain of pointers can't expand a ++ * single name without bound. Track the presentation length (label data plus ++ * the separator that precedes each label after the first), matching ++ * ares_split_dns_name() on the write side. */ ++ if (name_len) { ++ name_len++; ++ } ++ name_len += c; ++ if (name_len > ARES_MAX_NAME_PRESENTATION_LEN) { ++ status = ARES_EBADNAME; ++ goto fail; ++ } + + /* Labels are separated by periods */ + if (ares_buf_len(namebuf) != 0 && name != NULL) { Index: patches/patch-src_lib_record_ares_dns_parse_c =================================================================== RCS file: patches/patch-src_lib_record_ares_dns_parse_c diff -N patches/patch-src_lib_record_ares_dns_parse_c --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ patches/patch-src_lib_record_ares_dns_parse_c 7 Jul 2026 09:56:43 -0000 @@ -0,0 +1,85 @@ +From e47c203f91cd8b749c8736bc18d75a31ffdec8f4 Mon Sep 17 00:00:00 2001 +From: jmestwa-coder <jmestwa@gmail.com> +Date: Sun, 12 Apr 2026 23:50:54 +0530 +Subject: [PATCH] validate RR counts before preallocation to reject malformed + packets early (#1134) + +Index: src/lib/record/ares_dns_parse.c +--- src/lib/record/ares_dns_parse.c.orig ++++ src/lib/record/ares_dns_parse.c +@@ -920,30 +920,6 @@ static ares_status_t ares_dns_parse_header(ares_buf_t + + (*dnsrec)->raw_rcode = rcode; + +- if (*ancount > 0) { +- status = +- ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ANSWER, *ancount); +- if (status != ARES_SUCCESS) { +- goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ +- } +- } +- +- if (*nscount > 0) { +- status = +- ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_AUTHORITY, *nscount); +- if (status != ARES_SUCCESS) { +- goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ +- } +- } +- +- if (*arcount > 0) { +- status = +- ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ADDITIONAL, *arcount); +- if (status != ARES_SUCCESS) { +- goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ +- } +- } +- + return ARES_SUCCESS; + + fail: +@@ -1208,6 +1184,8 @@ static ares_status_t ares_dns_parse_buf(ares_buf_t *bu + ares_dns_record_t **dnsrec) + { + ares_status_t status; ++ size_t total_rr_count; ++ const size_t min_rr_wire_len = 11; + unsigned short qdcount; + unsigned short ancount; + unsigned short nscount; +@@ -1265,6 +1243,35 @@ static ares_status_t ares_dns_parse_buf(ares_buf_t *bu + status = ares_dns_parse_qd(buf, *dnsrec); + if (status != ARES_SUCCESS) { + goto fail; ++ } ++ } ++ ++ total_rr_count = (size_t)ancount + (size_t)nscount + (size_t)arcount; ++ if (total_rr_count > ares_buf_len(buf) / min_rr_wire_len) { ++ status = ARES_EBADRESP; ++ goto fail; ++ } ++ ++ if (ancount > 0) { ++ status = ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ANSWER, ancount); ++ if (status != ARES_SUCCESS) { ++ goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ ++ } ++ } ++ ++ if (nscount > 0) { ++ status = ++ ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_AUTHORITY, nscount); ++ if (status != ARES_SUCCESS) { ++ goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ ++ } ++ } ++ ++ if (arcount > 0) { ++ status = ++ ares_dns_record_rr_prealloc(*dnsrec, ARES_SECTION_ADDITIONAL, arcount); ++ if (status != ARES_SUCCESS) { ++ goto fail; /* LCOV_EXCL_LINE: OutOfMemory */ + } + } + > Index: Makefile > =================================================================== > RCS file: /cvs/ports/net/libcares/Makefile,v > retrieving revision 1.41 > diff -u -p -r1.41 Makefile > --- Makefile 16 Jan 2026 16:06:46 -0000 1.41 > +++ Makefile 6 Jul 2026 18:05:54 -0000 > @@ -1,12 +1,12 @@ > COMMENT= asynchronous resolver library > > -V= 1.34.6 > +V= 1.34.7 > DISTNAME= c-ares-${V} > PKGNAME= libcares-${V} > CATEGORIES= net devel > SITES= https://github.com/c-ares/c-ares/releases/download/v${V}/ > > -SHARED_LIBS += cares 4.4 # 2.19.0 > +SHARED_LIBS += cares 4.5 # 2.19.0 > > HOMEPAGE= https://c-ares.org/ > > Index: distinfo > =================================================================== > RCS file: /cvs/ports/net/libcares/distinfo,v > retrieving revision 1.25 > diff -u -p -r1.25 distinfo > --- distinfo 16 Jan 2026 16:06:46 -0000 1.25 > +++ distinfo 6 Jul 2026 18:05:54 -0000 > @@ -1,2 +1,2 @@ > -SHA256 (c-ares-1.34.6.tar.gz) = kS3XzDs+innFL9f7nA9Ozwqqc+Re/aiAJmotbia4TvU= > -SIZE (c-ares-1.34.6.tar.gz) = 1017864 > +SHA256 (c-ares-1.34.7.tar.gz) = VW94HdGIrZMtyCY/7grTqrpnW0zY5U2GkIaBtDzj4yc= > +SIZE (c-ares-1.34.7.tar.gz) = 1034872
No comments:
Post a Comment