new file mode 100644
@@ -0,0 +1,245 @@
+From 59bf865e9691f3a047e24bda071498a48e2a5220 Mon Sep 17 00:00:00 2001
+From: Matthijs Mekking <matthijs@isc.org>
+Date: Thu, 9 Apr 2026 11:32:07 +0200
+Subject: [PATCH] Add reproducer for BADCOOKIE resend loop
+
+Run malicious server: resend_loop/ans3/ans.py
+
+Start BIND: ns4
+
+Send single query to test.example
+
+The resolver will repeatedly resend queries until the fetch timeout
+expires, resulting in resulting in thousands of qrysent while the quota
+counter remains 0.
+
+CVE: CVE-2026-5950
+Upstream-Status: Backport [https://gitlab.com/isc-projects/bind9/-/commit/8344a38d6bc72d3872b552b4cae0e0d8be4c3d4a]
+
+(cherry picked from commit 7eeb463bc58cbd71419aaf189d7829f2dfd8d055)
+(cherry picked from commit 8344a38d6bc72d3872b552b4cae0e0d8be4c3d4a)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ bin/tests/system/resend_loop/ans3/ans.py | 128 ++++++++++++++++++
+ .../system/resend_loop/ns4/named.conf.j2 | 16 +++
+ bin/tests/system/resend_loop/ns4/root.hint | 14 ++
+ .../system/resend_loop/tests_resend_loop.py | 28 ++++
+ 4 files changed, 186 insertions(+)
+ create mode 100644 bin/tests/system/resend_loop/ans3/ans.py
+ create mode 100644 bin/tests/system/resend_loop/ns4/named.conf.j2
+ create mode 100644 bin/tests/system/resend_loop/ns4/root.hint
+ create mode 100644 bin/tests/system/resend_loop/tests_resend_loop.py
+
+diff --git a/bin/tests/system/resend_loop/ans3/ans.py b/bin/tests/system/resend_loop/ans3/ans.py
+new file mode 100644
+index 0000000000..90a3f2f9cc
+--- /dev/null
++++ b/bin/tests/system/resend_loop/ans3/ans.py
+@@ -0,0 +1,128 @@
++# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
++#
++# SPDX-License-Identifier: MPL-2.0
++#
++# This Source Code Form is subject to the terms of the Mozilla Public
++# License, v. 2.0. If a copy of the MPL was not distributed with this
++# file, you can obtain one at https://mozilla.org/MPL/2.0/.
++#
++# See the COPYRIGHT file distributed with this work for additional
++# information regarding copyright ownership.
++
++from collections.abc import AsyncGenerator
++
++import dns.edns
++import dns.name
++import dns.rcode
++import dns.rdatatype
++import dns.rrset
++
++from isctest.asyncserver import (
++ AsyncDnsServer,
++ DnsResponseSend,
++ QueryContext,
++ ResponseHandler,
++)
++
++
++def _get_cookie(qctx: QueryContext):
++ for o in qctx.query.options:
++ if o.otype == dns.edns.OptionType.COOKIE:
++ cookie = o
++ try:
++ if len(cookie.server) == 0:
++ cookie.server = b"\x11\x22\x33\x44\x55\x66\x77\x88"
++ except AttributeError: # dnspython<2.7.0 compat
++ if len(o.data) == 8:
++ cookie.data *= 2
++
++ return cookie
++
++ return None
++
++
++class PrimeHandler(ResponseHandler):
++ """
++ Specifically handle priming query for "." NS (type 2)
++ """
++
++ def match(self, qctx: QueryContext) -> bool:
++ return len(qctx.qname.labels) == 0 and qctx.qtype == dns.rdatatype.NS
++
++ async def get_responses(
++ self, qctx: QueryContext
++ ) -> AsyncGenerator[DnsResponseSend, None]:
++
++ ns_rrset = dns.rrset.from_text(
++ ".", dns.rdatatype.NS, qctx.qclass, "a.root-servers.nil."
++ )
++ a_rrset = dns.rrset.from_text(
++ "a.root-servers.nil.", dns.rdatatype.A, qctx.qclass, "10.53.0.3"
++ )
++
++ response = qctx.prepare_new_response(with_zone_data=False)
++ response.set_rcode(dns.rcode.NOERROR)
++ response.answer.append(ns_rrset)
++ response.additional.append(a_rrset)
++
++ yield DnsResponseSend(response, authoritative=True)
++
++
++class CookieHandler(ResponseHandler):
++ def match(self, qctx: QueryContext) -> bool:
++ example = dns.name.from_text("example")
++ return qctx.qname.is_subdomain(example)
++
++ async def get_responses(
++ self, qctx: QueryContext
++ ) -> AsyncGenerator[DnsResponseSend, None]:
++
++ qctx.prepare_new_response()
++
++ # Check for client cookie
++ cookie = _get_cookie(qctx)
++
++ # If missing cookie entirely, just return SERVFAIL
++ if cookie is None:
++ qctx.response.set_rcode(dns.rcode.SERVFAIL)
++ yield DnsResponseSend(qctx.response, authoritative=True)
++
++ # If there is a client cookie, mock BADCOOKIE to trigger
++ # the resend loop logic.
++ qctx.response.use_edns(options=[cookie])
++ qctx.response.set_rcode(dns.rcode.BADCOOKIE)
++ yield DnsResponseSend(qctx.response, authoritative=True)
++
++
++class NoErrorHandler(ResponseHandler):
++ """
++ If the query is NOT a subdomain of example, respond with standard NOERROR empty answer
++ """
++
++ async def get_responses(
++ self, qctx: QueryContext
++ ) -> AsyncGenerator[DnsResponseSend, None]:
++
++ qctx.prepare_new_response()
++ qctx.response.set_rcode(dns.rcode.NOERROR)
++ yield DnsResponseSend(qctx.response, authoritative=True)
++
++
++def resend_server() -> AsyncDnsServer:
++ server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR)
++ server.install_response_handlers(
++ [
++ PrimeHandler(),
++ CookieHandler(),
++ NoErrorHandler(),
++ ]
++ )
++ return server
++
++
++def main() -> None:
++ resend_server().run()
++
++
++if __name__ == "__main__":
++ main()
+diff --git a/bin/tests/system/resend_loop/ns4/named.conf.j2 b/bin/tests/system/resend_loop/ns4/named.conf.j2
+new file mode 100644
+index 0000000000..360bc12e17
+--- /dev/null
++++ b/bin/tests/system/resend_loop/ns4/named.conf.j2
+@@ -0,0 +1,16 @@
++options {
++ query-source address 10.53.0.4;
++ notify-source 10.53.0.4;
++ transfer-source 10.53.0.4;
++ port @PORT@;
++ pid-file "named.pid";
++ listen-on { 10.53.0.4; };
++ listen-on-v6 { none; };
++ recursion yes;
++ dnssec-validation no;
++};
++
++zone "." IN {
++ type hint;
++ file "root.hint";
++};
+diff --git a/bin/tests/system/resend_loop/ns4/root.hint b/bin/tests/system/resend_loop/ns4/root.hint
+new file mode 100644
+index 0000000000..3889a8b353
+--- /dev/null
++++ b/bin/tests/system/resend_loop/ns4/root.hint
+@@ -0,0 +1,14 @@
++; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
++;
++; SPDX-License-Identifier: MPL-2.0
++;
++; This Source Code Form is subject to the terms of the Mozilla Public
++; License, v. 2.0. If a copy of the MPL was not distributed with this
++; file, you can obtain one at https://mozilla.org/MPL/2.0/.
++;
++; See the COPYRIGHT file distributed with this work for additional
++; information regarding copyright ownership.
++
++$TTL 999999
++. IN NS a.root-servers.nil.
++a.root-servers.nil. IN A 10.53.0.3
+diff --git a/bin/tests/system/resend_loop/tests_resend_loop.py b/bin/tests/system/resend_loop/tests_resend_loop.py
+new file mode 100644
+index 0000000000..f7ed4d3da6
+--- /dev/null
++++ b/bin/tests/system/resend_loop/tests_resend_loop.py
+@@ -0,0 +1,28 @@
++# Copyright (C) Internet Systems Consortium, Inc. ("ISC")
++#
++# SPDX-License-Identifier: MPL-2.0
++#
++# This Source Code Form is subject to the terms of the Mozilla Public
++# License, v. 2.0. If a copy of the MPL was not distributed with this
++# file, you can obtain one at https://mozilla.org/MPL/2.0/.
++#
++# See the COPYRIGHT file distributed with this work for additional
++# information regarding copyright ownership.
++
++import dns.message
++
++import isctest
++
++
++def test_resend_loop_badcookie(ns4):
++ expected_log = "exceeded max queries resolving 'test.example/A'"
++
++ msg = dns.message.make_query("test.example", "A")
++ with ns4.watch_log_from_here() as watcher:
++ res = isctest.query.udp(msg, ns4.ip)
++ watcher.wait_for_line(expected_log)
++
++ isctest.check.servfail(res)
++
++ prohibited_log = "query failed (timed out) for test.example/IN/A"
++ assert prohibited_log not in ns4.log
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,100 @@
+From f090a1d0cf3599380c750557caff25e0379fb4d6 Mon Sep 17 00:00:00 2001
+From: Colin Vidal <colin@isc.org>
+Date: Tue, 7 Apr 2026 22:18:10 +0200
+Subject: [PATCH] Refactor incrementing query counters
+
+Move the logic incrementing the query counter and the global query
+counter into a dedicated helper function.
+
+CVE: CVE-2026-5950
+Upstream-Status: Backport [https://gitlab.com/isc-projects/bind9/-/commit/9ebfca2af824a60ea072292ffb1ab01ff87c7fa7]
+
+(cherry picked from commit 05d6da2de54c093689e675e81ae898ee41220666)
+(cherry picked from commit 9ebfca2af824a60ea072292ffb1ab01ff87c7fa7)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ lib/dns/resolver.c | 59 ++++++++++++++++++++++++++++------------------
+ 1 file changed, 36 insertions(+), 23 deletions(-)
+
+diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
+index 1bacec6470..079aa79ea0 100644
+--- a/lib/dns/resolver.c
++++ b/lib/dns/resolver.c
+@@ -4203,6 +4203,39 @@ fctx_nextaddress(fetchctx_t *fctx) {
+ return addrinfo;
+ }
+
++static isc_result_t
++incr_query_counters(fetchctx_t *fctx) {
++ isc_result_t result;
++
++ result = isc_counter_increment(fctx->qc);
++#if WANT_QUERYTRACE
++ FCTXTRACE5("query", "max-recursion-queries, querycount=",
++ isc_counter_used(fctx->qc));
++#endif
++ if (result != ISC_R_SUCCESS) {
++ isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER,
++ DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3),
++ "exceeded max queries resolving '%s' "
++ "(max-recursion-queries, querycount=%u)",
++ fctx->info, isc_counter_used(fctx->qc));
++ } else if (fctx->gqc != NULL) {
++ result = isc_counter_increment(fctx->gqc);
++#if WANT_QUERYTRACE
++ FCTXTRACE5("query", "max-query-count, querycount=",
++ isc_counter_used(fctx->gqc));
++#endif
++ if (result != ISC_R_SUCCESS) {
++ isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER,
++ DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3),
++ "exceeded global max queries resolving "
++ "'%s' (max-query-count, querycount=%u)",
++ fctx->info, isc_counter_used(fctx->gqc));
++ }
++ }
++
++ return result;
++}
++
+ static void
+ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) {
+ isc_result_t result;
+@@ -4357,31 +4390,11 @@ fctx_try(fetchctx_t *fctx, bool retrying, bool badcache) {
+ return;
+ }
+
+- result = isc_counter_increment(fctx->qc);
+- if (result != ISC_R_SUCCESS) {
+- isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER,
+- DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3),
+- "exceeded max queries resolving '%s' "
+- "(max-recursion-queries, querycount=%u)",
+- fctx->info, isc_counter_used(fctx->qc));
+- fctx_done_detach(&fctx, DNS_R_SERVFAIL);
+- return;
+- }
+-
+- if (fctx->gqc != NULL) {
+- result = isc_counter_increment(fctx->gqc);
+- if (result != ISC_R_SUCCESS) {
+- isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER,
+- DNS_LOGMODULE_RESOLVER, ISC_LOG_DEBUG(3),
+- "exceeded global max queries resolving "
+- "'%s' (max-query-count, querycount=%u)",
+- fctx->info, isc_counter_used(fctx->gqc));
+- fctx_done_detach(&fctx, DNS_R_SERVFAIL);
+- return;
+- }
+- }
++ CHECK(incr_query_counters(fctx));
+
+ result = fctx_query(fctx, addrinfo, fctx->options);
++
++cleanup:
+ if (result != ISC_R_SUCCESS) {
+ fctx_done_detach(&fctx, result);
+ } else if (retrying) {
+--
+2.35.6
+
new file mode 100644
@@ -0,0 +1,67 @@
+From 3346293873d9ae59b5e57abc41142485f8dcdc9a Mon Sep 17 00:00:00 2001
+From: Colin Vidal <colin@isc.org>
+Date: Tue, 7 Apr 2026 22:18:58 +0200
+Subject: [PATCH] rctx_resend() increment query counters
+
+Calls to `rctx_resend()` are done internally within the resolver, in
+flow which are not supposed to happens more than once. For instance,
+if some query fails, and a specific flag "F" wasn't set, then set the
+flag and try again. This wouldn't occur more than once because if the
+query fails the next attempt, the flag "F" would be set already, so the
+resolver would move to the next server (or give up).
+
+However, a subtle bug missing checking a flag, for instance, could lead
+to an unbounded loop re-trying to query the same server. This is now
+impossible as `rctx_resend()` also increment the query counters (so if
+such case occurs, it would stop once the maximum limit is reached).
+
+The dns_resstatscounter_retry are also only incremented if the
+`fctx_query()` succeeds, similar to as is done in `fctx_try()`.
+
+CVE: CVE-2026-5950
+Upstream-Status: Backport [https://gitlab.com/isc-projects/bind9/-/commit/d3ba533080e31de98986f3beff70d7c330ba9f89]
+
+(cherry picked from commit f3e74304889a2e8b69c8e88fc9a383589decda32)
+(cherry picked from commit d3ba533080e31de98986f3beff70d7c330ba9f89)
+Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
+---
+ lib/dns/resolver.c | 15 +++++++++++----
+ 1 file changed, 11 insertions(+), 4 deletions(-)
+
+diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
+index 079aa79ea0..cc40f862d1 100644
+--- a/lib/dns/resolver.c
++++ b/lib/dns/resolver.c
+@@ -10089,9 +10089,9 @@ rctx_nextserver(respctx_t *rctx, dns_message_t *message,
+ * rctx_resend():
+ *
+ * Resend the query, probably with the options changed. Calls
+- * fctx_query(), passing rctx->retryopts (which is based on
+- * query->options, but may have been updated since the last time
+- * fctx_query() was called).
++ * fctx_query(), unless query counter limits are hit, passing
++ * rctx->retryopts (which is based on query->options, but may have
++ * been updated since the last time fctx_query() was called).
+ */
+ static void
+ rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) {
+@@ -10099,8 +10099,15 @@ rctx_resend(respctx_t *rctx, dns_adbaddrinfo_t *addrinfo) {
+ isc_result_t result;
+
+ FCTXTRACE("resend");
+- inc_stats(fctx->res, dns_resstatscounter_retry);
++
++ CHECK(incr_query_counters(fctx));
++
+ result = fctx_query(fctx, addrinfo, rctx->retryopts);
++ if (result == ISC_R_SUCCESS) {
++ inc_stats(fctx->res, dns_resstatscounter_retry);
++ }
++
++cleanup:
+ if (result != ISC_R_SUCCESS) {
+ fctx_done_detach(&rctx->fctx, result);
+ }
+--
+2.35.6
+
@@ -23,6 +23,9 @@ SRC_URI = "https://ftp.isc.org/isc/bind9/${PV}/${BPN}-${PV}.tar.xz \
file://CVE-2026-1519_p2.patch \
file://CVE-2026-1519_p3.patch \
file://CVE-2026-1519_p4.patch \
+ file://CVE-2026-5950_p1.patch \
+ file://CVE-2026-5950_p2.patch \
+ file://CVE-2026-5950_p3.patch \
"
SRC_URI[sha256sum] = "81f5035a25c576af1a93f0061cf70bde6d00a0c7bd1274abf73f5b5389a6f82d"