commit c3e4b38bf25e596066d25d76e9f77d6975e8a8e2
Author: Colin Vidal <colin@isc.org>
Date:   Thu Apr 30 20:49:47 2026 +0200

    [v9_11] [CVE-2026-3592] sec: usr: Limit resolver server list size
    
    When resolving a domain with many nameservers that share overlapping IP addresses (e.g., 10 NS records all pointing at the same set of addresses), BIND could previously waste time querying duplicate addresses and build up excessively large server lists. Deduplicate addresses in the resolver's server list so that each unique IP is only queried once per resolution attempt, regardless of how many NS records point to it and cap the number of addresses stored per nameserver name to 6 (combined A and AAAA), preventing memory and CPU overhead from domains with unusually large NS/glue sets.
    
    Closes isc-projects/bind9#5641
    
    Backport of !909
    
    Merge branch 'backport-5641-selfpointedglue-9.11' into 'security-bind-9.11'
    
    See merge request isc-private/bind9!957

diff --git a/lib/dns/adb.c b/lib/dns/adb.c
index 6b8a9537f0..5a99dc9f30 100644
--- a/lib/dns/adb.c
+++ b/lib/dns/adb.c
@@ -86,6 +86,12 @@
 
 #define DNS_ADB_MINADBSIZE      (1024U*1024U)     /*%< 1 Megabyte */
 
+/*
+ * Default for the per-find address limit, the sum of the number of A and AAAA
+ * RR from an ADB NS name resolution
+ */
+#define DEFAULT_ADDRSLIMIT 6
+
 typedef ISC_LIST(dns_adbname_t) dns_adbnamelist_t;
 typedef struct dns_adbnamehook dns_adbnamehook_t;
 typedef ISC_LIST(dns_adbnamehook_t) dns_adbnamehooklist_t;
@@ -2183,6 +2189,7 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_name_t *qname,
 	dns_adbaddrinfo_t *addrinfo;
 	dns_adbentry_t *entry;
 	int bucket;
+	size_t count = 0;
 
 	bucket = DNS_ADB_INVALIDBUCKET;
 
@@ -2219,6 +2226,13 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_name_t *qname,
 			inc_entry_refcnt(adb, entry, false);
 			ISC_LIST_APPEND(find->list, addrinfo, publink);
 			addrinfo = NULL;
+
+			if (++count >= DEFAULT_ADDRSLIMIT) {
+				DP(ISC_LOG_DEBUG(3), "skipping addresses");
+				UNLOCK(&adb->entrylocks[bucket]);
+				return;
+			}
+
 		nextv4:
 			UNLOCK(&adb->entrylocks[bucket]);
 			bucket = DNS_ADB_INVALIDBUCKET;
@@ -2259,6 +2273,13 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find, dns_name_t *qname,
 			inc_entry_refcnt(adb, entry, false);
 			ISC_LIST_APPEND(find->list, addrinfo, publink);
 			addrinfo = NULL;
+
+			if (++count >= DEFAULT_ADDRSLIMIT) {
+				DP(ISC_LOG_DEBUG(3), "skipping addresses");
+				UNLOCK(&adb->entrylocks[bucket]);
+				return;
+			}
+
 		nextv6:
 			UNLOCK(&adb->entrylocks[bucket]);
 			bucket = DNS_ADB_INVALIDBUCKET;
diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
index 89f9aede91..39f0478f71 100644
--- a/lib/dns/resolver.c
+++ b/lib/dns/resolver.c
@@ -301,7 +301,7 @@ struct fetchctx {
 	dns_message_t *			rmessage;
 	ISC_LIST(resquery_t)		queries;
 	dns_adbfindlist_t		finds;
-	dns_adbfind_t *			find;
+	dns_adbaddrinfo_t		*foundaddrinfo;
 	dns_adbfindlist_t		altfinds;
 	dns_adbfind_t *			altfind;
 	dns_adbaddrinfolist_t		forwaddrs;
@@ -1176,7 +1176,7 @@ fctx_cleanupfinds(fetchctx_t *fctx) {
 		ISC_LIST_UNLINK(fctx->finds, find, publink);
 		dns_adb_destroyfind(&find);
 	}
-	fctx->find = NULL;
+	fctx->foundaddrinfo = NULL;
 }
 
 static void
@@ -3068,83 +3068,6 @@ add_bad(fetchctx_t *fctx, dns_adbaddrinfo_t *addrinfo, isc_result_t reason,
 		      namebuf, typebuf, classbuf, addrbuf);
 }
 
-/*
- * Sort addrinfo list by RTT.
- */
-static void
-sort_adbfind(dns_adbfind_t *find, unsigned int bias) {
-	dns_adbaddrinfo_t *best, *curr;
-	dns_adbaddrinfolist_t sorted;
-	unsigned int best_srtt, curr_srtt;
-
-	/* Lame N^2 bubble sort. */
-	ISC_LIST_INIT(sorted);
-	while (!ISC_LIST_EMPTY(find->list)) {
-		best = ISC_LIST_HEAD(find->list);
-		best_srtt = best->srtt;
-		if (isc_sockaddr_pf(&best->sockaddr) != AF_INET6)
-			best_srtt += bias;
-		curr = ISC_LIST_NEXT(best, publink);
-		while (curr != NULL) {
-			curr_srtt = curr->srtt;
-			if (isc_sockaddr_pf(&curr->sockaddr) != AF_INET6)
-				curr_srtt += bias;
-			if (curr_srtt < best_srtt) {
-				best = curr;
-				best_srtt = curr_srtt;
-			}
-			curr = ISC_LIST_NEXT(curr, publink);
-		}
-		ISC_LIST_UNLINK(find->list, best, publink);
-		ISC_LIST_APPEND(sorted, best, publink);
-	}
-	find->list = sorted;
-}
-
-/*
- * Sort a list of finds by server RTT.
- */
-static void
-sort_finds(dns_adbfindlist_t *findlist, unsigned int bias) {
-	dns_adbfind_t *best, *curr;
-	dns_adbfindlist_t sorted;
-	dns_adbaddrinfo_t *addrinfo, *bestaddrinfo;
-	unsigned int best_srtt, curr_srtt;
-
-	/* Sort each find's addrinfo list by SRTT. */
-	for (curr = ISC_LIST_HEAD(*findlist);
-	     curr != NULL;
-	     curr = ISC_LIST_NEXT(curr, publink))
-		sort_adbfind(curr, bias);
-
-	/* Lame N^2 bubble sort. */
-	ISC_LIST_INIT(sorted);
-	while (!ISC_LIST_EMPTY(*findlist)) {
-		best = ISC_LIST_HEAD(*findlist);
-		bestaddrinfo = ISC_LIST_HEAD(best->list);
-		INSIST(bestaddrinfo != NULL);
-		best_srtt = bestaddrinfo->srtt;
-		if (isc_sockaddr_pf(&bestaddrinfo->sockaddr) != AF_INET6)
-			best_srtt += bias;
-		curr = ISC_LIST_NEXT(best, publink);
-		while (curr != NULL) {
-			addrinfo = ISC_LIST_HEAD(curr->list);
-			INSIST(addrinfo != NULL);
-			curr_srtt = addrinfo->srtt;
-			if (isc_sockaddr_pf(&addrinfo->sockaddr) != AF_INET6)
-				curr_srtt += bias;
-			if (curr_srtt < best_srtt) {
-				best = curr;
-				best_srtt = curr_srtt;
-			}
-			curr = ISC_LIST_NEXT(curr, publink);
-		}
-		ISC_LIST_UNLINK(*findlist, best, publink);
-		ISC_LIST_APPEND(sorted, best, publink);
-	}
-	*findlist = sorted;
-}
-
 static void
 findname(fetchctx_t *fctx, dns_name_t *name, in_port_t port,
 	 unsigned int options, unsigned int flags, isc_stdtime_t now,
@@ -3582,8 +3505,6 @@ fctx_getaddresses(fetchctx_t *fctx, bool badcache) {
 		 * We've found some addresses.  We might still be looking
 		 * for more addresses.
 		 */
-		sort_finds(&fctx->finds, res->view->v6bias);
-		sort_finds(&fctx->altfinds, 0);
 		result = ISC_R_SUCCESS;
 	}
 
@@ -3655,6 +3576,80 @@ possibly_mark(fetchctx_t *fctx, dns_adbaddrinfo_t *addr) {
 	}
 }
 
+static dns_adbaddrinfo_t *
+nextaddress(fetchctx_t *fctx) {
+	dns_adbaddrinfo_t *prevai = fctx->foundaddrinfo, *lowestsrttai = NULL;
+	unsigned int v6bias = fctx->res->view->v6bias, lowestsrtt = 0;
+
+	/*
+	 * Let's walk through the list of dns_adbaddrinfo_t to find the best
+	 * next server address to query. This is linear on the number of
+	 * dns_adbaddrinfo_t which are grouped in find list (for each ADB find).
+	 */
+	for (dns_adbfind_t *find = ISC_LIST_HEAD(fctx->finds); find != NULL;
+	     find = ISC_LIST_NEXT(find, publink))
+	{
+		for (dns_adbaddrinfo_t *ai = ISC_LIST_HEAD(find->list);
+		     ai != NULL; ai = ISC_LIST_NEXT(ai, publink))
+		{
+			/*
+			 * This address has been marked already, skip it.
+			 */
+			if (!UNMARKED(ai)) {
+				continue;
+			}
+
+			/*
+			 * This address is the same as the previously used
+			 * address, it's a duplicate, mark it and skip it!
+			 */
+			if (prevai != NULL) {
+				if (prevai->entry == ai->entry) {
+					ai->flags |= FCTX_ADDRINFO_MARK;
+					continue;
+				}
+			}
+
+			/*
+			 * Mark and skip this address if incompatible (i.e. IPv6
+			 * address on a v4 only server, or for ACL reason, etc.)
+			 */
+			possibly_mark(fctx, ai);
+			if (!UNMARKED(ai)) {
+				continue;
+			}
+
+			/*
+			 * This address hasn't been tried yet and is a
+			 * good candidate. Let's keep track of it if it
+			 * has the lowest SRTT so far (or if there is no
+			 * address with lowest SRTT found yet).
+			 */
+			unsigned int aisrtt = ai->srtt;
+
+			if (isc_sockaddr_pf(&ai->sockaddr) != AF_INET6) {
+				aisrtt += v6bias;
+			}
+
+			if (lowestsrttai == NULL || aisrtt < lowestsrtt) {
+				lowestsrttai = ai;
+				lowestsrtt = aisrtt;
+				continue;
+			}
+		}
+	}
+
+	/*
+	 * This is the next address to query. If this is NULL, we're done.
+	 */
+	if (lowestsrttai != NULL) {
+		lowestsrttai->flags |= FCTX_ADDRINFO_MARK;
+	}
+	fctx->foundaddrinfo = lowestsrttai;
+
+	return lowestsrttai;
+}
+
 static inline dns_adbaddrinfo_t *
 fctx_nextaddress(fetchctx_t *fctx) {
 	dns_adbfind_t *find, *start;
@@ -3676,7 +3671,6 @@ fctx_nextaddress(fetchctx_t *fctx) {
 		possibly_mark(fctx, addrinfo);
 		if (UNMARKED(addrinfo)) {
 			addrinfo->flags |= FCTX_ADDRINFO_MARK;
-			fctx->find = NULL;
 			return (addrinfo);
 		}
 	}
@@ -3687,45 +3681,11 @@ fctx_nextaddress(fetchctx_t *fctx) {
 
 	FCTX_ATTR_SET(fctx, FCTX_ATTR_TRIEDFIND);
 
-	find = fctx->find;
-	if (find == NULL)
-		find = ISC_LIST_HEAD(fctx->finds);
-	else {
-		find = ISC_LIST_NEXT(find, publink);
-		if (find == NULL)
-			find = ISC_LIST_HEAD(fctx->finds);
-	}
-
-	/*
-	 * Find the first unmarked addrinfo.
-	 */
-	addrinfo = NULL;
-	if (find != NULL) {
-		start = find;
-		do {
-			for (addrinfo = ISC_LIST_HEAD(find->list);
-			     addrinfo != NULL;
-			     addrinfo = ISC_LIST_NEXT(addrinfo, publink)) {
-				if (!UNMARKED(addrinfo))
-					continue;
-				possibly_mark(fctx, addrinfo);
-				if (UNMARKED(addrinfo)) {
-					addrinfo->flags |= FCTX_ADDRINFO_MARK;
-					break;
-				}
-			}
-			if (addrinfo != NULL)
-				break;
-			find = ISC_LIST_NEXT(find, publink);
-			if (find == NULL)
-				find = ISC_LIST_HEAD(fctx->finds);
-		} while (find != start);
+	faddrinfo = nextaddress(fctx);
+	if (faddrinfo != NULL) {
+		return faddrinfo;
 	}
 
-	fctx->find = find;
-	if (addrinfo != NULL)
-		return (addrinfo);
-
 	/*
 	 * No nameservers left.  Try alternates.
 	 */
@@ -4398,7 +4358,7 @@ fctx_create(dns_resolver_t *res, dns_name_t *name, dns_rdatatype_t type,
 	ISC_LIST_INIT(fctx->bad_edns);
 	ISC_LIST_INIT(fctx->validators);
 	fctx->validator = NULL;
-	fctx->find = NULL;
+	fctx->foundaddrinfo = NULL;
 	fctx->altfind = NULL;
 	fctx->pending = 0;
 	fctx->restarts = 0;
