diff --git a/bind-9.18-CVE-2026-3592.patch b/bind-9.18-CVE-2026-3592.patch new file mode 100644 index 0000000..ba2d338 --- /dev/null +++ b/bind-9.18-CVE-2026-3592.patch @@ -0,0 +1,433 @@ +From 064ce66a7de78ac06c68aafbdfec686063888f42 Mon Sep 17 00:00:00 2001 +From: Colin Vidal +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 + diff --git a/bind.spec b/bind.spec index b3527bd..5b1cd72 100644 --- a/bind.spec +++ b/bind.spec @@ -191,6 +191,9 @@ Patch243: bind-9.18-CVE-2026-11721-test.patch Patch244: bind-9.18-CVE-2026-11721-fix2.patch # https://gitlab.isc.org/isc-projects/bind9/commit/43d173797e3c781c7e38d879c3c765f01cc09c59 Patch245: bind-9.18-CVE-2026-5950.patch +# https://gitlab.isc.org/isc-projects/bind9/commit/695362e3438c832ed0e39e144a77f233113d3431 +# https://gitlab.isc.org/isc-projects/bind9/commit/e25eaf9e6e09bbdd826252226144570222d542d8 +Patch246: bind-9.18-CVE-2026-3592.patch %{?systemd_ordering} # https://fedoraproject.org/wiki/Changes/RPMSuportForSystemdSysusers @@ -996,6 +999,7 @@ fi; %changelog * Wed Aug 19 2026 Petr Menšík - 32:9.18.33-26 - Avoid unbounded recursion loop (CVE-2026-5950) +- Limit overlapping addresses resolver usage (CVE-2026-3592) * Mon Jul 27 2026 RHEL Packaging Agent - 32:9.18.33-25 - Validate NSEC3 signer matches owning zone (CVE-2026-10723)