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
This commit is contained in:
parent
bbad8f050e
commit
11e56f333e
3 changed files with 720 additions and 0 deletions
435
bind-9.18-CVE-2026-12617-test.patch
Normal file
435
bind-9.18-CVE-2026-12617-test.patch
Normal file
|
|
@ -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
|
||||
|
||||
280
bind-9.18-CVE-2026-12617.patch
Normal file
280
bind-9.18-CVE-2026-12617.patch
Normal file
|
|
@ -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
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue