bind/bind-9.18-CVE-2026-3592.patch
Petr Menšík 7c61856b69 [9.18] [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

RH:
No tests are backported, because python tests have good enough support
for it.

Resolves: RHEL-189748
CVE: CVE-2026-3592
2026-08-19 16:46:10 +02:00

434 lines
13 KiB
Diff

From 064ce66a7de78ac06c68aafbdfec686063888f42 Mon Sep 17 00:00:00 2001
From: Colin Vidal <colin@isc.org>
Date: Thu, 5 Feb 2026 09:46:01 +0100
Subject: [PATCH] Limit the number of addresses returned per ADB find
Add a hard limit on the number of addresses that ADB returns from a
single NS lookup (dns_adbfind_t). This mitigates a flood attack
where an attacker controls a zone with many addresses for a
nameserver, each returning an invalid response. The global
max-query count (default 50) also limits this, but significant harm
can be done before that limit is reached.
The default limit is now 6 (v4 and/or v6) addresses for an ADB find (so,
ADB looking up for A/AAAA addresses of a name server name). It can be
overridden for testing via 'named -T adbaddrslimit=N'.
(cherry picked from commit 3ec37fc69356ee682bee7f67940613ac31d93d7b)
(cherry picked from commit 695362e3438c832ed0e39e144a77f233113d3431)
Remove duplicate addresses from the resolver SLIST
The SLIST (essentially `fctx->finds`, forwarders and dual-stack
alternatives aside) can have duplicate server addresses when multiple
in-domain nameservers share the same IP addresses:
sub.example. NS ns1.sub.example.
sub.example. NS ns2.sub.example.
ns1.sub.example. A 1.2.3.4
ns1.sub.example. A 5.6.7.8
ns2.sub.example. A 1.2.3.4
ns2.sub.example. A 5.6.7.8
If both 1.2.3.4 and 5.6.7.8 fail to return a valid answer, the resolver
would query each address twice.
The problem is fixed by replacing the two-phase server selection (sort
each find list by SRTT, sort finds by head SRTT) with a single linear
scan in nextaddress() that finds the lowest-SRTT unmarked, non-duplicate
address across all find lists.
The old approach had a correctness bug: after sorting, the resolver
picked the next address from the "current" find list rather than
globally. For example, with find lists [1, 15, 26] and [3, 4, 5], the
second pick would be SRTT 15 instead of the correct SRTT 3.
The new approach is both simpler and correct: each call to nextaddress()
walks all addresses, skips marked and duplicate entries, and returns the
one with the lowest SRTT. While this walk is repeated for each server
attempt, it operates on a small bounded list and is negligible compared
to the network I/O of querying the server.
(cherry picked from commit b1c5856a3764b4025e93f8baf06c45c8fa029752)
(cherry picked from commit e25eaf9e6e09bbdd826252226144570222d542d8)
---
bin/named/main.c | 9 ++
lib/dns/adb.c | 26 ++++++
lib/dns/resolver.c | 226 +++++++++++++++++++--------------------------
3 files changed, 128 insertions(+), 133 deletions(-)
diff --git a/bin/named/main.c b/bin/named/main.c
index ea31c9f626..31edc5cb48 100644
--- a/bin/named/main.c
+++ b/bin/named/main.c
@@ -114,6 +114,8 @@ extern unsigned int dns_zone_mkey_hour;
extern unsigned int dns_zone_mkey_day;
extern unsigned int dns_zone_mkey_month;
+extern size_t dns_adb_addrslimit;
+
static bool want_stats = false;
static char program_name[NAME_MAX] = "named";
static char absolute_conffile[PATH_MAX];
@@ -801,6 +803,13 @@ parse_T_opt(char *option) {
transferstuck = true;
} else if (!strncmp(option, "tat=", 4)) {
named_g_tat_interval = atoi(option + 4);
+ } else if (!strncmp(option, "adbaddrslimit=", 14)) {
+ size_t adb_addrslimit = atoi(option + 14);
+ if (adb_addrslimit < 1) {
+ named_main_earlyfatal("adbaddrslimit must be at "
+ "least 1");
+ }
+ dns_adb_addrslimit = adb_addrslimit;
} else {
fprintf(stderr, "unknown -T flag '%s'\n", option);
}
diff --git a/lib/dns/adb.c b/lib/dns/adb.c
index 3da839cc12..af50953d17 100644
--- a/lib/dns/adb.c
+++ b/lib/dns/adb.c
@@ -86,6 +86,15 @@
#define DNS_ADB_MINADBSIZE (1024U * 1024U) /*%< 1 Megabyte */
+/*
+ * Default and override for the per-find address limit, the sum of the number of
+ * A and AAAA RR from an ADB NS name resolution. When non-zero, this value is
+ * used instead of the default. Can be set via 'named -T adbaddrslimit=N' for
+ * testing.
+ */
+#define DEFAULT_ADDRSLIMIT 6
+size_t dns_adb_addrslimit = 0;
+
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;
@@ -2215,6 +2224,9 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find,
dns_adbaddrinfo_t *addrinfo;
dns_adbentry_t *entry;
int bucket;
+ size_t count = 0;
+ size_t limit = dns_adb_addrslimit != 0 ? dns_adb_addrslimit
+ : DEFAULT_ADDRSLIMIT;
bucket = DNS_ADB_INVALIDBUCKET;
@@ -2247,6 +2259,13 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find,
inc_entry_refcnt(adb, entry, false);
ISC_LIST_APPEND(find->list, addrinfo, publink);
addrinfo = NULL;
+
+ if (++count >= limit) {
+ DP(ISC_LOG_DEBUG(3), "skipping addresses");
+ UNLOCK(&adb->entrylocks[bucket]);
+ return;
+ }
+
nextv4:
UNLOCK(&adb->entrylocks[bucket]);
bucket = DNS_ADB_INVALIDBUCKET;
@@ -2282,6 +2301,13 @@ copy_namehook_lists(dns_adb_t *adb, dns_adbfind_t *find,
inc_entry_refcnt(adb, entry, false);
ISC_LIST_APPEND(find->list, addrinfo, publink);
addrinfo = NULL;
+
+ if (++count >= limit) {
+ 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 f64cb97122..601b3ca642 100644
--- a/lib/dns/resolver.c
+++ b/lib/dns/resolver.c
@@ -360,7 +360,16 @@ struct fetchctx {
dns_message_t *qmessage;
ISC_LIST(resquery_t) queries;
dns_adbfindlist_t finds;
- dns_adbfind_t *find;
+ /*
+ * This is a state to keep track of the latest upstream server which is
+ * being queried. See `nextaddress()`.
+ *
+ * `addrinfo` is basically a copy of `foundaddrinfo` but came from the
+ * response of the query, so fields like the SRTT/timing might have been
+ * altered. So it might be possible (?) to wrap those two in an union
+ * for clarity (and memory saving).
+ */
+ dns_adbaddrinfo_t *foundaddrinfo;
/*
* altfinds are names and/or addresses of dual stack servers that
* should be used when iterative resolution to a server is not
@@ -1526,7 +1535,7 @@ fctx_cleanup(fetchctx_t *fctx) {
dns_adb_destroyfind(&find);
fctx_unref(fctx);
}
- fctx->find = NULL;
+ fctx->foundaddrinfo = NULL;
for (find = ISC_LIST_HEAD(fctx->altfinds); find != NULL;
find = next_find)
@@ -3285,91 +3294,10 @@ add_bad(fetchctx_t *fctx, dns_message_t *rmessage, dns_adbaddrinfo_t *addrinfo,
}
/*
- * 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;
-}
-
-/*
- * Return true iff the ADB find has a pending fetch for 'type'. This is
- * used to find out whether we're in a loop, where a fetch is waiting for a
- * find which is waiting for that same fetch.
+ * Return true iff the ADB find has an already pending fetch for 'type'. This
+ * is used to find out whether we're in a loop, where a fetch is waiting for a
+ * find which is waiting for that same fetch. So if the current find actually
+ * started the fetch, we know it can't be a loop, so we returns false.
*
* Note: This could be done with either an equivalence check (e.g.,
* query_pending == DNS_ADBFIND_INET) or with a bit check, as below. If
@@ -3476,6 +3404,7 @@ findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port,
}
}
}
+
if ((flags & FCTX_ADDRINFO_DUALSTACK) != 0) {
ISC_LIST_APPEND(fctx->altfinds, find, publink);
} else {
@@ -3891,8 +3820,6 @@ out:
* 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;
}
@@ -3967,6 +3894,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 dns_adbaddrinfo_t *
fctx_nextaddress(fetchctx_t *fctx) {
dns_adbfind_t *find, *start;
@@ -3989,7 +3990,6 @@ fctx_nextaddress(fetchctx_t *fctx) {
possibly_mark(fctx, addrinfo);
if (UNMARKED(addrinfo)) {
addrinfo->flags |= FCTX_ADDRINFO_MARK;
- fctx->find = NULL;
fctx->forwarding = true;
/*
@@ -4010,49 +4010,9 @@ fctx_nextaddress(fetchctx_t *fctx) {
fctx->forwarding = false;
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);
- }
-
- fctx->find = find;
- if (addrinfo != NULL) {
- return addrinfo;
+ faddrinfo = nextaddress(fctx);
+ if (faddrinfo != NULL) {
+ return faddrinfo;
}
/*
--
2.55.0