public inbox for git-commits@fedoraproject.org
help / color / mirror / Atom feed
* [rpms/bind] f43: Record ordering based unexpected exit with CNAME or DNAME (CVE-2026-12617)
@ 2026-08-26  9:29 
  0 siblings, 0 replies; only message in thread
From:  @ 2026-08-26  9:29 UTC (permalink / raw)
  To: git-commits

            A new commit has been pushed.

            Repo   : rpms/bind
            Branch : f43
            Commit : 11e56f333e30abbcb06e177a331dff787fda2d9e
            Author : Petr Menšík <pemensik@redhat.com>
            Date   : 2026-08-26T11:07:49+02:00
            Stats  : +720/-0 in 3 file(s)
            URL    : https://src.fedoraproject.org/rpms/bind/c/11e56f333e30abbcb06e177a331dff787fda2d9e?branch=f43

            Log:
            Record ordering based unexpected exit with CNAME or DNAME (CVE-2026-12617)

[9.18] [CVE-2026-12617] sec: usr: Do no assert for some specifics CNAME and DNAME queries

A bug in the resolver's handling of certain cached DNAME and CNAME responses could cause named to trigger an assertion failure and exit. An attacker controlling a domain name and the authoritative DNS server it is hosted on could exploit this behavior to cause a denial-of-service. This vulnerability has been fixed.

ISC thanks Qifan Zhang of Palo Alto Networks for bringing this issue to our attention.

Closes: isc-projects/bind9#5946

[9.18] fix: test: Stabilize the cname_dname_negcache test

The test proved unstable due to timing-related race condition, which could produce both false positive and false negative results. Use a more reliable mechanism to reproduce the issue.

Human-effort-level: low

---
diff --git a/bind-9.18-CVE-2026-12617-test.patch b/bind-9.18-CVE-2026-12617-test.patch
new file mode 100644
index 0000000..b2add78
--- /dev/null
+++ b/bind-9.18-CVE-2026-12617-test.patch
@@ -0,0 +1,435 @@
+From a31296b120efc985fb1fc3c932882e965156473b Mon Sep 17 00:00:00 2001
+From: Colin Vidal <colin@isc.org>
+Date: Mon, 15 Jun 2026 11:34:08 +0200
+Subject: [PATCH] Reproducer for #5946 (assertion in some CNAME/DNAME queries)
+
+Add a system test reproducing the issue reported by #5946, which
+is also CVE-2026-12617. There are two scenarios:
+
+- A client send queries for a DNAME and A record to the resolver (ns3),
+  and the authoritative server (ans2) responds positively to the A query
+  but delay the DNAME response and respond later negatively;
+
+- A client send queries for a CNAME and A record to the resolver (ns3),
+  and the authoritative server (ans2) responds positively to the A query
+  but delay the CNAME response and respond later with a self-referential
+  CNAME.
+
+The test does not check the results of the queries, however, it expects
+the resolver to correctly handle those and do not assert.
+
+(cherry picked from commit e88271f2e584010157b068cc998dd76451273562)
+(cherry picked from commit bb92832fb6ae899bee7206c2d8966258461c2f71)
+
+Stabilize timing in the cname_dname_negcache test
+
+The #5946 reproducer relied on ans2 answering the negative DNAME/CNAME
+query a fixed second after receiving it, racing that delay against the
+resolver's per-query timeout.  When the timeout fired first -- which
+happens under load, most notably under ThreadSanitizer, where named is
+slowed but ans2 (wall-clock) is not -- the resolver dropped the late
+answer, never processed the negative response, and the watched SOA never
+appeared, so the test timed out.  This made it flaky on the
+security-bind-9.20 CI.  Merely shortening the fixed delay would trade
+that for the opposite, worse failure: the negative answer arriving
+before the positive one is cached, silently not exercising the bug.
+
+Release the negative answer based on the resolver's progress instead of
+a wall-clock deadline: hold it until ans2 has sent the positive answer
+(a shared event), then wait a short settle for the resolver to cache it.
+Both queries traverse the same delegation, so any latency reaching ans2
+shifts the positive send and the negative fetch's deadline together and
+cancels out; only the settle, kept well under MINIMUM_QUERY_TIMEOUT
+(301 ms), has to fit inside the per-query timeout.
+
+Verified that the stabilized test still triggers the
+INSIST(namereln == dns_namereln_subdomain) assertion when the resolver
+fix is reverted.
+
+Assisted-by: Claude:claude-opus-4-8
+(cherry picked from commit 738456d91564526e6f15c3858b4c809cd6749e1e)
+(cherry picked from commit 0c20ee4e8e68999ca617434cde65dd3808f57d8c)
+
+Split cname_dname_negcache into per-scenario modules
+
+The DNAME and CNAME scenarios shared a single module, hence a single
+module-scoped ns3 (the framework sets servers up per module, not per
+test function).  test_dname_negcache cached foo.test. DNAME bar.test.;
+when test_cname_negcache ran next against the same resolver,
+cname.foo.test. was DNAME-mapped to cname.bar.test., so the resolver
+never queried ans2 for the self-referential CNAME and that half of the
+bug was never exercised.  The hardcoded, unanchored "foo.test." watcher
+still matched test_dname's leftover SOA, so test_cname passed without
+testing anything -- the CNAME assertion had no coverage.
+
+Give each scenario its own module so each gets a fresh server set, and
+anchor the watcher to the queried name so a test cannot pass on an
+unrelated record.
+
+With the resolver fix reverted, each module now independently triggers
+its own assertion:
+
+    DNAME  query.c INSIST(namereln == dns_namereln_subdomain)
+    CNAME  query.c INSIST(qctx->rdataset == NULL || qctx->qtype == dname)
+
+Assisted-by: Claude:claude-opus-4-8
+(cherry picked from commit 3ef0b8d04a1653407cfb9ee88772ae18689b1318)
+(cherry picked from commit 887124315f03a006c4dc76e48ae3d0d8aac3c407)
+---
+ .../system/cname_dname_negcache/ans2/ans.py   | 148 ++++++++++++++++++
+ .../system/cname_dname_negcache/common.py     |  46 ++++++
+ .../cname_dname_negcache/ns1/bar.test.db      |   5 +
+ .../cname_dname_negcache/ns1/named.conf.j2    |  24 +++
+ .../system/cname_dname_negcache/ns1/root.db   |   6 +
+ .../system/cname_dname_negcache/ns1/test.db   |   8 +
+ .../cname_dname_negcache/ns3/named.conf.j2    |  11 ++
+ .../tests_cname_negcache.py                   |  16 ++
+ .../tests_dname_negcache.py                   |  16 ++
+ 9 files changed, 280 insertions(+)
+ create mode 100644 bin/tests/system/cname_dname_negcache/ans2/ans.py
+ create mode 100644 bin/tests/system/cname_dname_negcache/common.py
+ create mode 100644 bin/tests/system/cname_dname_negcache/ns1/bar.test.db
+ create mode 100644 bin/tests/system/cname_dname_negcache/ns1/named.conf.j2
+ create mode 100644 bin/tests/system/cname_dname_negcache/ns1/root.db
+ create mode 100644 bin/tests/system/cname_dname_negcache/ns1/test.db
+ create mode 100644 bin/tests/system/cname_dname_negcache/ns3/named.conf.j2
+ create mode 100644 bin/tests/system/cname_dname_negcache/tests_cname_negcache.py
+ create mode 100644 bin/tests/system/cname_dname_negcache/tests_dname_negcache.py
+
+diff --git a/bin/tests/system/cname_dname_negcache/ans2/ans.py b/bin/tests/system/cname_dname_negcache/ans2/ans.py
+new file mode 100644
+index 0000000000..392fe1e088
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/ans2/ans.py
+@@ -0,0 +1,148 @@
++"""
++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 asyncio
++
++from dns import name, rcode, rdataclass, rdatatype, rrset
++
++from isctest.asyncserver import (
++    AsyncDnsServer,
++    DnsResponseSend,
++    QnameQtypeHandler,
++    QueryContext,
++    StaticResponseHandler,
++)
++
++# The attack relies on the resolver caching the positive CNAME/DNAME answer
++# *before* it processes the negative answer for the same name.  The negative
++# answer must therefore be held back until the positive one has been sent, but
++# released again while the negative fetch is still waiting for it.
++#
++# Releasing it at a fixed wall-clock delay (the original approach) is racy: the
++# delay must be larger than the time it takes the resolver to cache the
++# positive answer, yet smaller than the resolver's per-query timeout.  Under
++# load -- most notably ThreadSanitizer, which slows down `named` but not this
++# (wall-clock) server -- those bounds can be violated in either direction,
++# making the test either time out (#5946 CI failures) or, worse, silently stop
++# exercising the bug.
++#
++# Instead, gate the negative answer on an event set right after the positive
++# answer is sent.  Both queries traverse the same delegation, so any latency in
++# reaching this server shifts the positive send and the negative fetch's
++# deadline together and cancels out; only the small settle below has to fit
++# inside the per-query timeout.
++#
++# _SETTLE must be longer than the few milliseconds the resolver needs to cache
++# the positive answer, and shorter than MINIMUM_QUERY_TIMEOUT (301 ms in
++# lib/dns/resolver.c) so the in-flight negative fetch has not given up yet.
++_SETTLE = 0.1
++
++_dname_positive_sent = asyncio.Event()
++_cname_positive_sent = asyncio.Event()
++
++
++async def _hold_until_positive_cached(positive_sent: asyncio.Event) -> None:
++    await positive_sent.wait()
++    await asyncio.sleep(_SETTLE)
++
++
++def build_rrset(
++    qname: name.Name | str,
++    rtype: rdatatype.RdataType,
++    rdata: str,
++    ttl: int = 300,
++) -> rrset.RRset:
++    return rrset.from_text(qname, ttl, rdataclass.IN, rtype, rdata)
++
++
++class FooTestNsHandler(QnameQtypeHandler, StaticResponseHandler):
++    qnames = ["foo.test."]
++    qtypes = [rdatatype.NS]
++    answer = [build_rrset("foo.test.", rdatatype.NS, "ns.foo.test.")]
++    additional = [build_rrset("ns.foo.test.", rdatatype.A, "10.53.0.2")]
++
++
++class DelayedDnameNegHandler(QnameQtypeHandler, StaticResponseHandler):
++    qnames = ["foo.test."]
++    qtypes = [rdatatype.DNAME]
++    authority = [
++        build_rrset(
++            "foo.test.",
++            rdatatype.SOA,
++            "ns.test. op.ns.test. 2081509183 86400 3600 3600000 300",
++        )
++    ]
++
++    async def get_responses(
++        self, qctx: QueryContext
++    ) -> AsyncGenerator[DnsResponseSend, None]:
++        await _hold_until_positive_cached(_dname_positive_sent)
++        async for response in super().get_responses(qctx):
++            yield response
++
++
++class DnamePosHandler(QnameQtypeHandler, StaticResponseHandler):
++    qnames = ["a.foo.test."]
++    qtypes = [rdatatype.A]
++    answer = [
++        build_rrset("foo.test.", rdatatype.DNAME, "bar.test."),
++        build_rrset("a.foo.test.", rdatatype.CNAME, "a.bar.test."),
++    ]
++
++    async def get_responses(
++        self, qctx: QueryContext
++    ) -> AsyncGenerator[DnsResponseSend, None]:
++        async for response in super().get_responses(qctx):
++            yield response
++        _dname_positive_sent.set()
++
++
++class CnameHandler(QnameQtypeHandler):
++    qnames = ["cname.foo.test."]
++    qtypes = [rdatatype.CNAME, rdatatype.A]
++    answer = [build_rrset("cname.foo.test.", rdatatype.CNAME, "cname.foo.test.")]
++    authority = [
++        build_rrset(
++            "cname.foo.test.",
++            rdatatype.SOA,
++            "ns.test. op.ns.test. 2081509183 86400 3600 3600000 300",
++        )
++    ]
++
++    async def get_responses(
++        self, qctx: QueryContext
++    ) -> AsyncGenerator[DnsResponseSend, None]:
++        if qctx.qtype == rdatatype.CNAME:
++            await _hold_until_positive_cached(_cname_positive_sent)
++            qctx.prepare_new_response(with_zone_data=False)
++            qctx.response.authority.extend(self.authority)
++            yield DnsResponseSend(qctx.response, authoritative=True)
++        else:
++            qctx.prepare_new_response(with_zone_data=False)
++            qctx.response.answer.extend(self.answer)
++            yield DnsResponseSend(qctx.response, authoritative=True)
++            _cname_positive_sent.set()
++
++
++def main() -> None:
++    server = AsyncDnsServer(default_aa=True, default_rcode=rcode.NOERROR)
++    server.install_response_handlers(
++        FooTestNsHandler(), DelayedDnameNegHandler(), DnamePosHandler(), CnameHandler()
++    )
++    server.run()
++
++
++if __name__ == "__main__":
++    main()
+diff --git a/bin/tests/system/cname_dname_negcache/common.py b/bin/tests/system/cname_dname_negcache/common.py
+new file mode 100644
+index 0000000000..397cfdfa3d
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/common.py
+@@ -0,0 +1,46 @@
++# 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 os import environ
++from re import compile as Re
++from re import escape
++from socket import AF_INET, SOCK_DGRAM, socket
++
++import isctest
++
++
++def run_attack(ns, name1, type1, name2, type2):
++    msg1 = isctest.query.create(name1, type1, cd=True)
++    msg2 = isctest.query.create(name2, type2, cd=True)
++    port = int(environ["PORT"])
++
++    with socket(AF_INET, SOCK_DGRAM) as sock:
++        # The order the requests go out doesn't matter. What is important is
++        # that the first query starts recursion before the second query returns
++        # the answer, and the second query returns the answer before the first
++        # query returns the answer. (So, when the NOERROR/NODATA comes back from
++        # the first query, the cache is queried and we get the positive response
++        # cached from the second query attached to the fresp rdataset of the
++        # response of the first query.)
++        # That ordering is enforced by ans2, which holds back the negative
++        # answer to the first query until it has answered the second one (see
++        # ans2/ans.py); the resolver must not crash while reconciling them.
++        sock.sendto(msg1.to_wire(), (ns.ip, port))
++        sock.sendto(msg2.to_wire(), (ns.ip, port))
++
++    # The second query comes back immediately, the resolver caches the DNAME.
++    # The first query comes back shortly after, once ans2 has released the
++    # negative answer, and should not crash the server.  Wait for the negative
++    # SOA for this specific name (not just any foo.test. one) so the test cannot
++    # pass on an unrelated record.
++    soa = Re(rf"(?<![\w.]){escape(name1)}.*IN\s+SOA\s+ns\.test\.\s+op\.ns\.test\.")
++    with ns.watch_log_from_start(timeout=15) as watcher:
++        watcher.wait_for_sequence([soa])
+diff --git a/bin/tests/system/cname_dname_negcache/ns1/bar.test.db b/bin/tests/system/cname_dname_negcache/ns1/bar.test.db
+new file mode 100644
+index 0000000000..840b9c3a2c
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/ns1/bar.test.db
+@@ -0,0 +1,5 @@
++$TTL 300
++bar.test.        IN SOA  ns.bar.test. hostmaster.bar.test. 1 600 600 1200 600
++bar.test.        NS      ns.bar.test.
++ns    		 A       10.53.0.1
++a      		 A       10.0.0.1
+diff --git a/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2 b/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2
+new file mode 100644
+index 0000000000..d72dd1181d
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/ns1/named.conf.j2
+@@ -0,0 +1,24 @@
++options {
++    query-source address @ns.ip@;
++    port @PORT@;
++    pid-file "named.pid";
++    listen-on { @ns.ip@; };
++    listen-on-v6 { none; };
++    recursion no;
++    dnssec-validation no;
++};
++
++zone "." {
++	type primary;
++	file "root.db";
++};
++
++zone "test." {
++	type primary;
++	file "test.db";
++};
++
++zone "bar.test." {
++	type primary;
++	file "bar.test.db";
++};
+diff --git a/bin/tests/system/cname_dname_negcache/ns1/root.db b/bin/tests/system/cname_dname_negcache/ns1/root.db
+new file mode 100644
+index 0000000000..c456c45b9d
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/ns1/root.db
+@@ -0,0 +1,6 @@
++$TTL 300
++.        IN SOA  ns. hostmaster. 1 600 600 1200 600
++.	         	NS      a.root-servers.nil.
++a.root-servers.nil.    	A       10.53.0.1
++test    	 	NS      ns.test
++ns.test		 	A	 10.53.0.1
+diff --git a/bin/tests/system/cname_dname_negcache/ns1/test.db b/bin/tests/system/cname_dname_negcache/ns1/test.db
+new file mode 100644
+index 0000000000..acb68e00f8
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/ns1/test.db
+@@ -0,0 +1,8 @@
++$TTL 300
++test.        IN SOA  ns.test. hostmaster.test. 1 600 600 1200 600
++test.	         NS      ns.test.
++ns    		 A       10.53.0.1
++bar    		 NS      ns.bar
++ns.bar		 A	 10.53.0.1
++foo		 NS	 ns.foo
++ns.foo		 A	 10.53.0.2
+diff --git a/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2 b/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2
+new file mode 100644
+index 0000000000..197d72756b
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/ns3/named.conf.j2
+@@ -0,0 +1,11 @@
++options {
++    query-source address @ns.ip@;
++    port @PORT@;
++    pid-file "named.pid";
++    listen-on { @ns.ip@; };
++    listen-on-v6 { none; };
++    recursion yes;
++    dnssec-validation no;
++};
++
++{% include "_common/root.hint.conf" %}
+diff --git a/bin/tests/system/cname_dname_negcache/tests_cname_negcache.py b/bin/tests/system/cname_dname_negcache/tests_cname_negcache.py
+new file mode 100644
+index 0000000000..a546d29109
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/tests_cname_negcache.py
+@@ -0,0 +1,16 @@
++# 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 cname_dname_negcache.common import run_attack
++
++
++def test_cname_negcache(ns3):
++    run_attack(ns3, "cname.foo.test.", "CNAME", "cname.foo.test.", "A")
+diff --git a/bin/tests/system/cname_dname_negcache/tests_dname_negcache.py b/bin/tests/system/cname_dname_negcache/tests_dname_negcache.py
+new file mode 100644
+index 0000000000..41a80b4e05
+--- /dev/null
++++ b/bin/tests/system/cname_dname_negcache/tests_dname_negcache.py
+@@ -0,0 +1,16 @@
++# 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 cname_dname_negcache.common import run_attack
++
++
++def test_dname_negcache(ns3):
++    run_attack(ns3, "foo.test.", "DNAME", "a.foo.test.", "A")
+-- 
+2.55.0
+

diff --git a/bind-9.18-CVE-2026-12617.patch b/bind-9.18-CVE-2026-12617.patch
new file mode 100644
index 0000000..1c8079e
--- /dev/null
+++ b/bind-9.18-CVE-2026-12617.patch
@@ -0,0 +1,280 @@
+From 3138886f3767c0a6b933fd147c5465472209bac6 Mon Sep 17 00:00:00 2001
+From: Colin Vidal <colin@isc.org>
+Date: Thu, 18 Jun 2026 18:17:05 +0200
+Subject: [PATCH] Do not assert in some CNAME/DNAME queries
+
+Fix a `named` crash because of a fail assertion for certains types of
+CNAME and DNAME queries:
+
+- If a client queries for a DNAME and A record to the resolver, and the
+  authoritative server responds positively to the A query but delay the
+  DNAME response and respond later negatively;
+
+- If a client queries for a CNAME and A record to the resolver, and the
+  authoritative server responds positively to the A query but delay the
+  CNAME response and respond later with a self-referential CNAME.
+
+The first scenario consists of sending two queries: `foo.test./DNAME`
+and `a.foo.test./A`. The authoritative server delays the answer for
+`foo.test./DNAME` but immediately answers the DNAME record for the
+second query: `foo.test. DNAME bar.test.`. The resolver caches it,
+follows the DNAME, and resolves `a.bar.test./A`. The authoritative
+server eventually answers negatively for `foo.test./DNAME`
+(NOERROR/NODATA, with only an SOA in the authority section). The
+resolver pulls out the previously cached rdataset (because it has a
+higher trust level than the received negative answer), and wrongly (this
+is the first bug) sets the result to `DNS_R_DNAME` instead of
+`ISC_R_SUCCESS`. The code in `ns/query.c` that handles the resolver
+result interprets this as "this is a non-DNAME query and we got a DNAME
+rdataset, so follow the chain". It goes into the `query_dname()`
+function, which asserts that the qname is a subdomain of the owner name
+in the rdataset. That assertion fails because the qname (`foo.test.`) is
+exactly equal to the owner name of the DNAME (`foo.test.`), rather than
+being a subdomain of it. `DNS_R_DNAME` must only be set when the qtype
+is something other than DNAME and the resolver has obtained a DNAME that
+needs to be followed.
+
+The second scenario consists of sending two queries:
+`cname.foo.test./CNAME` and `cname.foo.test./A`. The authoritative
+server delays the answer for `cname.foo.test./CNAME` but immediately
+answers the CNAME record for the second query: `cname.foo.test. CNAME
+cname.foo.test.`. Note that the CNAME is self-referential. The resolver
+caches it and sets the result code to `DNS_R_CNAME`. Then `ns/query.c`
+interprets this as "this is a non-CNAME query and we got a CNAME
+rdataset, so follow the chain" (which is correct in this case; however,
+because the CNAME rdataset is self-referential, the resolver responds
+with SERVFAIL, which is expected). The authoritative server eventually
+answers negatively for `cname.foo.test./CNAME`. The resolver then pulls
+out the previously cached CNAME rdataset (obtained from the A answer,
+even though it was self-referential, the resolver cached it) and wrongly
+sets the result to `DNS_R_CNAME` (this is the second bug). As noted
+above, `ns/query.c` interprets this as "this is a non-CNAME query and we
+got a CNAME rdataset, so follow the chain". The internals here are
+slightly more subtle: it first goes into `query_cname()` and sets the
+CNAME rdataset in the message answer section, then restarts the query to
+follow the CNAME. The restart retrieves the CNAME rdataset from the
+cache directly (without going to the resolver), and this time the query
+context result is `ISC_R_SUCCESS` (since it was found) and
+`qctx->rdataset` points to the same CNAME again (as it is
+self-referential), so it goes directly into the
+`query_prepresponse()/query_respond()` flow, which attempts to add the
+rdataset to the message answer again. However, this fails because the
+rdataset is already in the message, and the assertion which expects that
+operation to succeed fails (due to `qctx->rdataset` being set to `NULL`
+when ownership of the rdataset was transferred). `DNS_R_CNAME` must only
+be set when the qtype is something other than CNAME and the resolver has
+obtained a CNAME that needs to be followed.
+
+In both cases, the correct answer from the resolver should have been
+`ISC_R_SUCCESS` (instead of respectively `DNS_R_DNAME` and
+`DNS_R_CNAME`) becuase the rdataset that has been looked up was found.
+
+(cherry picked from commit 773d46d58c693047a5945c8fe40512edd0ac214e)
+(cherry picked from commit c740c37689f234e21a9b0ef760471ef2cf1133f5)
+---
+ lib/dns/resolver.c | 137 ++++++++++++++++++++-------------------------
+ 1 file changed, 60 insertions(+), 77 deletions(-)
+
+diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c
+index edc9c207e1..1f8b5058d1 100644
+--- a/lib/dns/resolver.c
++++ b/lib/dns/resolver.c
+@@ -692,10 +692,10 @@ fctx_destroy(fetchctx_t *fctx, bool exiting);
+ static void
+ send_shutdown_events(dns_resolver_t *res);
+ static isc_result_t
+-ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node,
+-		  dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t minttl,
+-		  dns_ttl_t maxttl, bool optout, bool secure,
+-		  dns_rdataset_t *ardataset, isc_result_t *eresultp);
++ncache_adderesult(fetchctx_t *fctx, dns_message_t *message, dns_dbnode_t *node,
++		  dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t maxttl,
++		  bool optout, bool secure, dns_rdataset_t *ardataset,
++		  isc_result_t *eresultp);
+ static void
+ validated(isc_task_t *task, isc_event_t *event);
+ static void
+@@ -5563,6 +5563,46 @@ has_000_label(dns_rdataset_t *nsecset) {
+ 	return false;
+ }
+ 
++/*
++ * After a (non-error) negative-cache add, 'rdataset' is bound to whatever
++ * rdataset the cache authoritatively holds for the queried name and type.
++ * Map that to the result code the fetch should report:
++ *
++ *   - A negative cache entry (the one we just added, or a pre-existing one):
++ *     DNS_R_NCACHENXDOMAIN or DNS_R_NCACHENXRRSET, depending on NXDOMAIN vs
++ *     NODATA.
++ *
++ *   - A positive rdataset that was already cached at higher trust, which
++ *     caused our negative entry to be discarded (e.g. a CNAME or DNAME cached
++ *     by a concurrent query): ISC_R_SUCCESS, because that cached positive
++ *     answer is what gets returned.  Note the specific case for CNAME and
++ *     DNAME *if* the query type is not the same as the rdataset type. There
++ *     is a chain to follow *only* if the query type doesn't ask for the CNAME
++ *     or the DNAME.
++ */
++static isc_result_t
++fctx_setresult(fetchctx_t *fctx, dns_rdataset_t *rdataset) {
++	isc_result_t result = ISC_R_SUCCESS;
++
++	if (NEGATIVE(rdataset)) {
++		result = NXDOMAIN(rdataset) ? DNS_R_NCACHENXDOMAIN
++					    : DNS_R_NCACHENXRRSET;
++	} else if (result == ISC_R_SUCCESS && rdataset->type != fctx->type) {
++		switch (rdataset->type) {
++		case dns_rdatatype_cname:
++			result = DNS_R_CNAME;
++			break;
++		case dns_rdatatype_dname:
++			result = DNS_R_DNAME;
++			break;
++		default:
++			break;
++		}
++	}
++
++	return result;
++}
++
+ /*
+  * The validator has finished.
+  */
+@@ -5836,8 +5876,7 @@ validated(isc_task_t *task, isc_event_t *event) {
+ 			ttl = 0;
+ 		}
+ 
+-		result = ncache_adderesult(message, fctx->cache, node, covers,
+-					   now, fctx->res->view->minncachettl,
++		result = ncache_adderesult(fctx, message, node, covers, now,
+ 					   ttl, vevent->optout, vevent->secure,
+ 					   ardataset, &eresult);
+ 		if (result != ISC_R_SUCCESS) {
+@@ -6081,23 +6120,7 @@ answer_response:
+ 		 */
+ 		INSIST(hevent->rdataset != NULL);
+ 		if (dns_rdataset_isassociated(hevent->rdataset)) {
+-			if (NEGATIVE(hevent->rdataset)) {
+-				INSIST(eresult == DNS_R_NCACHENXDOMAIN ||
+-				       eresult == DNS_R_NCACHENXRRSET);
+-			} else if (eresult == ISC_R_SUCCESS &&
+-				   hevent->rdataset->type != fctx->type)
+-			{
+-				switch (hevent->rdataset->type) {
+-				case dns_rdatatype_cname:
+-					eresult = DNS_R_CNAME;
+-					break;
+-				case dns_rdatatype_dname:
+-					eresult = DNS_R_DNAME;
+-					break;
+-				default:
+-					break;
+-				}
+-			}
++			eresult = fctx_setresult(fctx, hevent->rdataset);
+ 		}
+ 
+ 		hevent->result = eresult;
+@@ -6747,24 +6770,7 @@ cache_name(fetchctx_t *fctx, dns_name_t *name, dns_message_t *message,
+ 			 * event->result.
+ 			 */
+ 			if (dns_rdataset_isassociated(event->rdataset)) {
+-				if (NEGATIVE(event->rdataset)) {
+-					INSIST(eresult ==
+-						       DNS_R_NCACHENXDOMAIN ||
+-					       eresult == DNS_R_NCACHENXRRSET);
+-				} else if (eresult == ISC_R_SUCCESS &&
+-					   event->rdataset->type != fctx->type)
+-				{
+-					switch (event->rdataset->type) {
+-					case dns_rdatatype_cname:
+-						eresult = DNS_R_CNAME;
+-						break;
+-					case dns_rdatatype_dname:
+-						eresult = DNS_R_DNAME;
+-						break;
+-					default:
+-						break;
+-					}
+-				}
++				eresult = fctx_setresult(fctx, event->rdataset);
+ 			}
+ 			event->result = eresult;
+ 			if (adbp != NULL && *adbp != NULL) {
+@@ -6833,12 +6839,14 @@ cache_message(fetchctx_t *fctx, dns_message_t *message,
+  * eresult.
+  */
+ static isc_result_t
+-ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node,
+-		  dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t minttl,
+-		  dns_ttl_t maxttl, bool optout, bool secure,
+-		  dns_rdataset_t *ardataset, isc_result_t *eresultp) {
++ncache_adderesult(fetchctx_t *fctx, dns_message_t *message, dns_dbnode_t *node,
++		  dns_rdatatype_t covers, isc_stdtime_t now, dns_ttl_t maxttl,
++		  bool optout, bool secure, dns_rdataset_t *ardataset,
++		  isc_result_t *eresultp) {
+ 	isc_result_t result;
+ 	dns_rdataset_t rdataset;
++	dns_db_t *cache = fctx->cache;
++	dns_ttl_t minttl = fctx->res->view->minncachettl;
+ 
+ 	if (ardataset == NULL) {
+ 		dns_rdataset_init(&rdataset);
+@@ -6854,37 +6862,13 @@ ncache_adderesult(dns_message_t *message, dns_db_t *cache, dns_dbnode_t *node,
+ 	}
+ 	if (result == DNS_R_UNCHANGED || result == ISC_R_SUCCESS) {
+ 		/*
+-		 * If the cache now contains a negative entry and we
+-		 * care about whether it is DNS_R_NCACHENXDOMAIN or
+-		 * DNS_R_NCACHENXRRSET then extract it.
++		 * The cache settled successfully (DNS_R_UNCHANGED means our
++		 * negative entry was discarded in favour of existing
++		 * higher-trust data).  Either way 'ardataset' is now bound to
++		 * the rdataset the cache holds for this name and type; derive
++		 * the result code from it.
+ 		 */
+-		if (NEGATIVE(ardataset)) {
+-			/*
+-			 * The cache data is a negative cache entry.
+-			 */
+-			if (NXDOMAIN(ardataset)) {
+-				*eresultp = DNS_R_NCACHENXDOMAIN;
+-			} else {
+-				*eresultp = DNS_R_NCACHENXRRSET;
+-			}
+-		} else {
+-			/*
+-			 * The attempt to add a negative cache entry
+-			 * was rejected.  Set *eresultp to reflect
+-			 * the type of the dataset being returned.
+-			 */
+-			switch (ardataset->type) {
+-			case dns_rdatatype_cname:
+-				*eresultp = DNS_R_CNAME;
+-				break;
+-			case dns_rdatatype_dname:
+-				*eresultp = DNS_R_DNAME;
+-				break;
+-			default:
+-				*eresultp = ISC_R_SUCCESS;
+-				break;
+-			}
+-		}
++		*eresultp = fctx_setresult(fctx, ardataset);
+ 		result = ISC_R_SUCCESS;
+ 	}
+ 	if (ardataset == &rdataset && dns_rdataset_isassociated(ardataset)) {
+@@ -7029,8 +7013,7 @@ ncache_message(fetchctx_t *fctx, dns_message_t *message,
+ 		ttl = 0;
+ 	}
+ 
+-	result = ncache_adderesult(message, fctx->cache, node, covers, now,
+-				   fctx->res->view->minncachettl, ttl, false,
++	result = ncache_adderesult(fctx, message, node, covers, now, ttl, false,
+ 				   false, ardataset, &eresult);
+ 	if (result != ISC_R_SUCCESS) {
+ 		goto unlock;
+-- 
+2.55.0
+

diff --git a/bind.spec b/bind.spec
index 4dd562a..bd46400 100644
--- a/bind.spec
+++ b/bind.spec
@@ -151,6 +151,10 @@ Patch37: bind-9.18-CVE-2026-11331-test.patch
 # https://gitlab.isc.org/isc-projects/bind9/commit/c5d2fc706ca3635d9928c1cc68db73bffb35d772
 Patch38: bind-9.18-CVE-2026-10822.patch
 Patch40: bind-9.18-CVE-2026-10822-test.patch
+# https://gitlab.isc.org/isc-projects/bind9/commit/5a0cef5bdb077df73fa59504efb8ca4c8b0a84c5
+Patch41: bind-9.18-CVE-2026-12617.patch
+# https://gitlab.isc.org/isc-projects/bind9/commit/26437be900c68bbe0974ec9ab804c2894c19c419
+Patch42: bind-9.18-CVE-2026-12617-test.patch
 
 %{?systemd_ordering}
 # https://fedoraproject.org/wiki/Changes/RPMSuportForSystemdSysusers
@@ -954,6 +958,7 @@ fi;
 * Tue Aug 25 2026 Petr Menšík <pemensik@redhat.com> - 32:9.18.50-2
 - Potential wildcard CNAME RPZ policy bypass (CVE-2026-11331)
 - Key Record using PRIVATEDNS algorithm may lead to exit (CVE-2026-10822)
+- Record ordering based unexpected exit with CNAME or DNAME (CVE-2026-12617)
 
 * Wed Jun 17 2026 Petr Menšík <pemensik@redhat.com> - 32:9.18.50-1
 - Update to 9.18.50 (rhbz#2489833)

^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-08-26  9:29 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-26  9:29 [rpms/bind] f43: Record ordering based unexpected exit with CNAME or DNAME (CVE-2026-12617) 

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox