]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
Reject malformed RRSIG records 12051/head
authorOndřej Surý <ondrej@isc.org>
Mon, 18 May 2026 17:27:54 +0000 (19:27 +0200)
committerOndřej Surý <ondrej@isc.org>
Thu, 28 May 2026 07:51:27 +0000 (09:51 +0200)
A signature cannot cover a meta-type (NONE, ANY, AXFR, IXFR, MAILB,
MAILA, OPT, TSIG, TKEY); previously such records were cached by the
recursive resolver and collided with negative-cache entries on the
same owner name, corrupting the QP-trie cache.

Assisted-by: Claude:claude-opus-4-7
(cherry picked from commit c28ba9c3c6f4274a3626cd300a2590a1593ab2f6)

bin/tests/system/qpcache_rrsig_any/ans3/ans.py [new file with mode: 0644]
bin/tests/system/qpcache_rrsig_any/ns2/named.args [new file with mode: 0644]
bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2 [new file with mode: 0644]
bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py [new file with mode: 0644]
lib/dns/message.c

diff --git a/bin/tests/system/qpcache_rrsig_any/ans3/ans.py b/bin/tests/system/qpcache_rrsig_any/ans3/ans.py
new file mode 100644 (file)
index 0000000..67993fc
--- /dev/null
@@ -0,0 +1,63 @@
+"""
+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.
+
+For any query, returns a hand-crafted RRSIG whose Type-Covered field
+is selected by the leftmost label of QNAME. The label is parsed as a
+DNS type via `dns.rdatatype.from_text()`, so the resolver can be
+probed with any meta-type by querying e.g. `any.attacker.test.`,
+`axfr.attacker.test.`, `tsig.attacker.test.`, etc.
+"""
+
+from collections.abc import AsyncGenerator
+
+import dns.flags
+import dns.rcode
+import dns.rdataclass
+import dns.rdatatype
+import dns.rrset
+
+from isctest.asyncserver import (
+    AsyncDnsServer,
+    DnsResponseSend,
+    QueryContext,
+    ResponseHandler,
+)
+
+
+class RrsigCoversHandler(ResponseHandler):
+    async def get_responses(
+        self, qctx: QueryContext
+    ) -> AsyncGenerator[DnsResponseSend, None]:
+        covers_label = qctx.qname.labels[0].decode("ascii").upper()
+        covers = dns.rdatatype.from_text(covers_label)
+        rrset = dns.rrset.from_text(
+            qctx.qname,
+            3600,
+            dns.rdataclass.IN,
+            dns.rdatatype.RRSIG,
+            f"TYPE{int(covers)} 8 2 3600 20300101000000 20200101000000 "
+            "12345 attacker.test. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+        )
+        qctx.response.set_rcode(dns.rcode.NOERROR)
+        qctx.response.flags |= dns.flags.AA
+        qctx.response.answer.append(rrset)
+        yield DnsResponseSend(qctx.response)
+
+
+def main() -> None:
+    server = AsyncDnsServer()
+    server.install_response_handler(RrsigCoversHandler())
+    server.run()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/bin/tests/system/qpcache_rrsig_any/ns2/named.args b/bin/tests/system/qpcache_rrsig_any/ns2/named.args
new file mode 100644 (file)
index 0000000..a47f606
--- /dev/null
@@ -0,0 +1 @@
+-m record -c named.conf -d 99 -D qpcache_rrsig_any-ns2 -g
diff --git a/bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2 b/bin/tests/system/qpcache_rrsig_any/ns2/named.conf.j2
new file mode 100644 (file)
index 0000000..98e02cb
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+key rndc_key {
+       secret "1234abcd8765";
+       algorithm @DEFAULT_HMAC@;
+};
+
+controls {
+       inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; };
+};
+
+options {
+       query-source address 10.53.0.2;
+       notify-source 10.53.0.2;
+       transfer-source 10.53.0.2;
+       port @PORT@;
+       pid-file "named.pid";
+       listen-on { 10.53.0.2; };
+       listen-on-v6 { none; };
+       recursion yes;
+       dnssec-validation no;
+};
+
+zone "attacker.test" {
+       type forward;
+       forward only;
+       forwarders { 10.53.0.3 port @PORT@; };
+};
diff --git a/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py b/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py
new file mode 100644 (file)
index 0000000..8601eb4
--- /dev/null
@@ -0,0 +1,72 @@
+#!/usr/bin/python3
+
+# 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.
+
+"""
+A signature cannot cover a DNS meta-type. An RRSIG whose Type-Covered
+field is one of NONE/ANY/AXFR/IXFR/MAILA/MAILB/OPT/TSIG/TKEY is
+malformed and must be rejected by the resolver. ns3 picks the
+Type-Covered field from the leftmost label of QNAME.
+"""
+
+import pytest
+
+import isctest
+
+pytestmark = pytest.mark.extra_artifacts(
+    [
+        "ans*/ans.run",
+        "ns*/named.run",
+    ]
+)
+
+
+META_TYPES = ["ANY", "AXFR", "IXFR", "MAILA", "MAILB", "OPT", "TSIG", "TKEY"]
+
+
+@pytest.mark.parametrize("meta_type", META_TYPES)
+def test_rrsig_covers_metatype_is_servfail(meta_type):
+    qname = f"{meta_type.lower()}.attacker.test."
+    msg = isctest.query.create(qname, "RRSIG", dnssec=False, ad=False)
+    res = isctest.query.tcp(msg, "10.53.0.2")
+    isctest.check.servfail(res)
+
+
+@pytest.mark.parametrize("meta_type", META_TYPES)
+def test_dig_nobesteffort_rejects_malformed_rrsig(meta_type, named_port):
+    """
+    With +nobesteffort, dig uses the same strict parser path that the
+    recursive resolver uses, so a malformed RRSIG covering a meta-type
+    is rejected before being printed.
+    """
+    dig = isctest.run.EnvCmd("DIG", f"-p {named_port}")
+    qname = f"{meta_type.lower()}.attacker.test."
+    res = dig(
+        f"+nobesteffort +tries=1 +time=5 @10.53.0.3 {qname} RRSIG",
+        raise_on_exception=False,
+    )
+    assert ";; Got bad packet: FORMERR" in res.out
+    assert "ANSWER SECTION" not in res.out
+
+
+@pytest.mark.parametrize("meta_type", META_TYPES)
+def test_dig_besteffort_shows_malformed_rrsig(meta_type, named_port):
+    """
+    The default dig parser runs in +besteffort mode, which intentionally
+    keeps wire-level inspection working: the malformed RRSIG is still
+    printed so operators can debug what an upstream actually sent.
+    """
+    dig = isctest.run.EnvCmd("DIG", f"-p {named_port}")
+    qname = f"{meta_type.lower()}.attacker.test."
+    res = dig(f"+tries=1 +time=5 @10.53.0.3 {qname} RRSIG")
+    assert "ANSWER SECTION" in res.out
+    assert "RRSIG" in res.out
index dce9c2d65ff1eb83482e50a26e94dc32344376d7..91fb29bdd4402841d9183509e633570736b35cb7 100644 (file)
@@ -1415,7 +1415,10 @@ getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx,
                rdata->rdclass = rdclass;
                if (rdtype == dns_rdatatype_rrsig && rdata->flags == 0) {
                        covers = dns_rdata_covers(rdata);
-                       if (covers == 0) {
+                       /* A signature can only cover a real rdata type */
+                       if (covers == dns_rdatatype_none ||
+                           dns_rdatatype_ismeta(covers))
+                       {
                                DO_ERROR(DNS_R_FORMERR);
                        }
                } else if (rdtype == dns_rdatatype_sig /* SIG(0) */ &&