diff mbox series

[scarthgap,2/5] bind: Fix CVE-2026-5950

Message ID 20260610100404.2993940-2-asparmar@cisco.com
State New
Headers show
Series [scarthgap,1/5] bind: Fix CVE-2026-1519 | expand

Commit Message

From: Ashishkumar Parmar <asparmar@cisco.com>

Pick the upstream 9.18 backport [1] for CVE-2026-5950. The public ISC
advisory [2] describes the vulnerability and identifies the fixed BIND
release.

The upstream fix is split across a reproducer, a counter refactor, and
the functional resend-loop fix. Apply them in the same order so the
final fix lands on top of the counter handling it depends on:

- CVE-2026-5950_p1.patch [3] adds the BADCOOKIE resend-loop system-test
  reproducer.
- CVE-2026-5950_p2.patch [4] refactors query-counter increment handling
  so resend paths can update counters consistently.
- CVE-2026-5950_p3.patch [5] updates rctx_resend() to increment query
  counters, preventing the unbounded resend loop from bypassing the
  quota accounting.

Keep the patches split to preserve the upstream commit structure and to
make the SRC_URI ordering explicit.

[1] https://gitlab.com/isc-projects/bind9/-/commit/43d173797e3c781c7e38d879c3c765f01cc09c59
[2] https://kb.isc.org/docs/cve-2026-5950
[3] https://gitlab.com/isc-projects/bind9/-/commit/8344a38d6bc72d3872b552b4cae0e0d8be4c3d4a
[4] https://gitlab.com/isc-projects/bind9/-/commit/9ebfca2af824a60ea072292ffb1ab01ff87c7fa7
[5] https://gitlab.com/isc-projects/bind9/-/commit/d3ba533080e31de98986f3beff70d7c330ba9f89

Signed-off-by: Ashishkumar Parmar <asparmar@cisco.com>
---
 .../bind/bind/CVE-2026-5950_p1.patch          | 245 ++++++++++++++++++
 .../bind/bind/CVE-2026-5950_p2.patch          | 100 +++++++
 .../bind/bind/CVE-2026-5950_p3.patch          |  67 +++++
 .../recipes-connectivity/bind/bind_9.18.44.bb |   3 +
 4 files changed, 415 insertions(+)
 create mode 100644 meta/recipes-connectivity/bind/bind/CVE-2026-5950_p1.patch
 create mode 100644 meta/recipes-connectivity/bind/bind/CVE-2026-5950_p2.patch
 create mode 100644 meta/recipes-connectivity/bind/bind/CVE-2026-5950_p3.patch
diff mbox series

Patch

diff --git a/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p1.patch b/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p1.patch
new file mode 100644
index 0000000000..3b603e41fd
--- /dev/null
+++ b/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p1.patch
@@ -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
+
diff --git a/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p2.patch b/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p2.patch
new file mode 100644
index 0000000000..3921c363f6
--- /dev/null
+++ b/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p2.patch
@@ -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
+
diff --git a/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p3.patch b/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p3.patch
new file mode 100644
index 0000000000..9163c956ea
--- /dev/null
+++ b/meta/recipes-connectivity/bind/bind/CVE-2026-5950_p3.patch
@@ -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
+
diff --git a/meta/recipes-connectivity/bind/bind_9.18.44.bb b/meta/recipes-connectivity/bind/bind_9.18.44.bb
index 9c8b73dccc..d55e0e0c4d 100644
--- a/meta/recipes-connectivity/bind/bind_9.18.44.bb
+++ b/meta/recipes-connectivity/bind/bind_9.18.44.bb
@@ -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"