diff --git a/SOURCES/bind-9.11-CVE-2025-40778.patch b/SOURCES/bind-9.11-CVE-2025-40778.patch new file mode 100644 index 0000000..d83589e --- /dev/null +++ b/SOURCES/bind-9.11-CVE-2025-40778.patch @@ -0,0 +1,1084 @@ +From 3b7a43eb2fcdec7e31587a88464f5eb8df5c23d6 Mon Sep 17 00:00:00 2001 +From: Evan Hunt +Date: Mon, 29 Sep 2025 22:17:39 -0700 +Subject: [PATCH] Tighten restrictions on caching NS RRsets in authority + section + +To prevent certain spoofing attacks, a new check has been added +to the existing rules for whether NS data can be cached: the owner +name of the NS RRset must be an ancestor of the name being queried. + +(cherry picked from commit fa153f791f9324bf84abf8d259e11c0531fe6e25) +(cherry picked from commit 41ab0709d1bde6fb8a2dde623d00e69bc48fab0d) + +Further restrict addresses that are cached when processing referrals + +Use the owner name of the NS record as the bailwick apex name +when determining which additional records to cache, rather than +the name of the delegating zone (or a parent thereof). + +(cherry picked from commit a41054e9e606a61f1b3c8bc0c54e2f1059347165) +(cherry picked from commit eba8e3eb33f907a1a622c065138e19b087b6e4f1) + +Retry lookups with unsigned DNAME over TCP + +To prevent spoofed unsigned DNAME responses being accepted retry +response with unsigned DNAMEs over TCP if the response is not TSIG +signed or there isn't a good DNS CLIENT COOKIE. + +To prevent test failures, this required adding TCP support to the +ans3 and ans4 servers in the chain system test. + +(cherry picked from commit 2e40705c06831988106335ed77db3cf924d431f6) +(cherry picked from commit 0e4cd87bed5efc61443337034a9d96287b4885dc) +--- + bin/tests/system/chain/ans3/ans.pl | 129 ----------------- + bin/tests/system/chain/ans3/ans.py | 216 ++++++++++++++++++++++++++++ + bin/tests/system/chain/ans4/ans.py | 58 ++++++-- + bin/tests/system/cookie/ans9/ans.py | 139 ++++++++++-------- + lib/dns/include/dns/message.h | 8 ++ + lib/dns/message.c | 12 ++ + lib/dns/resolver.c | 84 ++++++++--- + lib/dns/win32/libdns.def.in | 1 + + 8 files changed, 432 insertions(+), 215 deletions(-) + delete mode 100644 bin/tests/system/chain/ans3/ans.pl + create mode 100644 bin/tests/system/chain/ans3/ans.py + +diff --git a/bin/tests/system/chain/ans3/ans.pl b/bin/tests/system/chain/ans3/ans.pl +deleted file mode 100644 +index 31d22d807b..0000000000 +--- a/bin/tests/system/chain/ans3/ans.pl ++++ /dev/null +@@ -1,129 +0,0 @@ +-#!/usr/bin/env perl +-# +-# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +-# +-# 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. +- +-use strict; +-use warnings; +- +-use IO::File; +-use Getopt::Long; +-use Net::DNS::Nameserver; +- +-my $pidf = new IO::File "ans.pid", "w" or die "cannot open pid file: $!"; +-print $pidf "$$\n" or die "cannot write pid file: $!"; +-$pidf->close or die "cannot close pid file: $!"; +-sub rmpid { unlink "ans.pid"; exit 1; }; +- +-$SIG{INT} = \&rmpid; +-$SIG{TERM} = \&rmpid; +- +-my $localaddr = "10.53.0.3"; +- +-my $localport = int($ENV{'PORT'}); +-if (!$localport) { $localport = 5300; } +- +-my $verbose = 0; +-my $ttl = 60; +-my $zone = "example.broken"; +-my $nsname = "ns3.$zone"; +-my $synth = "synth-then-dname.$zone"; +-my $synth2 = "synth2-then-dname.$zone"; +- +-sub reply_handler { +- my ($qname, $qclass, $qtype, $peerhost, $query, $conn) = @_; +- my ($rcode, @ans, @auth, @add); +- +- print ("request: $qname/$qtype\n"); +- STDOUT->flush(); +- +- if ($qname eq "example.broken") { +- if ($qtype eq "SOA") { +- my $rr = new Net::DNS::RR("$qname $ttl $qclass SOA . . 0 0 0 0 0"); +- push @ans, $rr; +- } elsif ($qtype eq "NS") { +- my $rr = new Net::DNS::RR("$qname $ttl $qclass NS $nsname"); +- push @ans, $rr; +- $rr = new Net::DNS::RR("$nsname $ttl $qclass A $localaddr"); +- push @add, $rr; +- } +- $rcode = "NOERROR"; +- } elsif ($qname eq "cname-to-$synth2") { +- my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name.$synth2"); +- push @ans, $rr; +- $rr = new Net::DNS::RR("name.$synth2 $ttl $qclass CNAME name"); +- push @ans, $rr; +- $rr = new Net::DNS::RR("$synth2 $ttl $qclass DNAME ."); +- push @ans, $rr; +- $rcode = "NOERROR"; +- } elsif ($qname eq "$synth" || $qname eq "$synth2") { +- if ($qtype eq "DNAME") { +- my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME ."); +- push @ans, $rr; +- } +- $rcode = "NOERROR"; +- } elsif ($qname eq "name.$synth") { +- my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name."); +- push @ans, $rr; +- $rr = new Net::DNS::RR("$synth $ttl $qclass DNAME ."); +- push @ans, $rr; +- $rcode = "NOERROR"; +- } elsif ($qname eq "name.$synth2") { +- my $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME name."); +- push @ans, $rr; +- $rr = new Net::DNS::RR("$synth2 $ttl $qclass DNAME ."); +- push @ans, $rr; +- $rcode = "NOERROR"; +- # The following three code branches referring to the "example.dname" +- # zone are necessary for the resolver variant of the CVE-2021-25215 +- # regression test to work. A named instance cannot be used for +- # serving the DNAME records below as a version of BIND vulnerable to +- # CVE-2021-25215 would crash while answering the queries asked by +- # the tested resolver. +- } elsif ($qname eq "ns3.example.dname") { +- if ($qtype eq "A") { +- my $rr = new Net::DNS::RR("$qname $ttl $qclass A 10.53.0.3"); +- push @ans, $rr; +- } +- if ($qtype eq "AAAA") { +- my $rr = new Net::DNS::RR("example.dname. $ttl $qclass SOA . . 0 0 0 0 $ttl"); +- push @auth, $rr; +- } +- $rcode = "NOERROR"; +- } elsif ($qname eq "self.example.self.example.dname") { +- my $rr = new Net::DNS::RR("self.example.dname. $ttl $qclass DNAME dname."); +- push @ans, $rr; +- $rr = new Net::DNS::RR("$qname $ttl $qclass CNAME self.example.dname."); +- push @ans, $rr; +- $rcode = "NOERROR"; +- } elsif ($qname eq "self.example.dname") { +- if ($qtype eq "DNAME") { +- my $rr = new Net::DNS::RR("$qname $ttl $qclass DNAME dname."); +- push @ans, $rr; +- } +- $rcode = "NOERROR"; +- } else { +- $rcode = "REFUSED"; +- } +- return ($rcode, \@ans, \@auth, \@add, { aa => 1 }); +-} +- +-GetOptions( +- 'port=i' => \$localport, +- 'verbose!' => \$verbose, +-); +- +-my $ns = Net::DNS::Nameserver->new( +- LocalAddr => $localaddr, +- LocalPort => $localport, +- ReplyHandler => \&reply_handler, +- Verbose => $verbose, +-); +- +-$ns->main_loop; +diff --git a/bin/tests/system/chain/ans3/ans.py b/bin/tests/system/chain/ans3/ans.py +new file mode 100644 +index 0000000000..d6d4eea6d9 +--- /dev/null ++++ b/bin/tests/system/chain/ans3/ans.py +@@ -0,0 +1,216 @@ ++# 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. ++ ++############################################################################ ++# ans.py: See README.anspy for details. ++############################################################################ ++ ++from __future__ import print_function ++import os ++import sys ++import signal ++import socket ++import select ++from datetime import datetime, timedelta ++import functools ++ ++import dns, dns.message, dns.query ++from dns.rdatatype import * ++from dns.rdataclass import * ++from dns.rcode import * ++from dns.name import * ++ ++############################################################################ ++# Respond to a DNS query. ++############################################################################ ++def create_response(msg): ++ ttl = 60 ++ zone = "example.broken." ++ nsname = "ns3." + zone ++ synth = "synth-then-dname." + zone ++ synth2 = "synth2-then-dname." + zone ++ ++ m = dns.message.from_wire(msg) ++ qname = m.question[0].name.to_text() ++ ++ # prepare the response and convert to wire format ++ r = dns.message.make_response(m) ++ ++ # get qtype ++ rrtype = m.question[0].rdtype ++ qtype = dns.rdatatype.to_text(rrtype) ++ print("request: " + qname + "/" + qtype) ++ ++ rcode = "NOERROR" ++ if qname == zone: ++ if qtype == "SOA": ++ r.answer.append(dns.rrset.from_text(qname, ttl, IN, SOA, ". . 0 0 0 0 0")) ++ elif qtype == "NS": ++ r.answer.append(dns.rrset.from_text(qname, ttl, IN, NS, nsname)) ++ r.additional.append(dns.rrset.from_text(nsname, ttl, IN, A, ip4)) ++ elif qname == "cname-to-" + synth2: ++ r.answer.append(dns.rrset.from_text(qname, ttl, IN, CNAME, "name." + synth2)) ++ r.answer.append(dns.rrset.from_text("name." + synth2, ttl, IN, CNAME, "name.")) ++ r.answer.append(dns.rrset.from_text(synth2, ttl, IN, DNAME, ".")) ++ elif qname == synth or qname == synth2: ++ if qtype == "DNAME": ++ r.answer.append(dns.rrset.from_text(qname, ttl, IN, DNAME, ".")) ++ elif qname == "name." + synth: ++ r.answer.append(dns.rrset.from_text(qname, ttl, IN, CNAME, "name.")) ++ r.answer.append(dns.rrset.from_text(synth, ttl, IN, DNAME, ".")) ++ elif qname == "name." + synth2: ++ r.answer.append(dns.rrset.from_text(qname, ttl, IN, CNAME, "name.")) ++ r.answer.append(dns.rrset.from_text(synth2, ttl, IN, DNAME, ".")) ++ elif qname == "ns3.example.dname.": ++ # This and the next two code branches referring to the "example.dname" ++ # zone are necessary for the resolver variant of the CVE-2021-25215 ++ # regression test to work. A named instance cannot be used for ++ # serving the DNAME records below as a version of BIND vulnerable to ++ # CVE-2021-25215 would crash while answering the queries asked by ++ # the tested resolver. ++ if qtype == "A": ++ r.answer.append(dns.rrset.from_text(qname, ttl, IN, A, ip4)) ++ elif qtype == "AAAA": ++ r.authority.append( ++ dns.rrset.from_text("example.dname.", ttl, IN, SOA, ". . 0 0 0 0 0") ++ ) ++ elif qname == "self.example.self..example.dname.": ++ r.answer.append( ++ dns.rrset.from_text("self.example.dname.", ttl, IN, DNAME, "dname.") ++ ) ++ r.answer.append( ++ dns.rrset.from_text(qname, ttl, IN, CNAME, "self.example.dname.") ++ ) ++ elif qname == "self.example.dname.": ++ if qtype == "DNAME": ++ r.answer.append(dns.rrset.from_text(qname, ttl, IN, DNAME, "dname.")) ++ else: ++ rcode = "REFUSED" ++ ++ r.flags |= dns.flags.AA ++ r.use_edns() ++ return r.to_wire() ++ ++ ++def sigterm(signum, frame): ++ print("Shutting down now...") ++ os.remove("ans.pid") ++ running = False ++ sys.exit(0) ++ ++ ++############################################################################ ++# Main ++# ++# Set up responder and control channel, open the pid file, and start ++# the main loop, listening for queries on the query channel or commands ++# on the control channel and acting on them. ++############################################################################ ++ip4 = "10.53.0.3" ++ip6 = "fd92:7065:b8e:ffff::3" ++ ++try: ++ port = int(os.environ["PORT"]) ++except: ++ port = 5300 ++ ++query4_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ++query4_udp.bind((ip4, port)) ++ ++query4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++query4_tcp.bind((ip4, port)) ++query4_tcp.listen(1) ++query4_tcp.settimeout(1) ++ ++havev6 = True ++try: ++ query6_udp = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) ++ try: ++ query6_udp.bind((ip6, port)) ++ except: ++ query6_udp.close() ++ havev6 = False ++ ++ query6_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++ try: ++ query6_tcp.bind((ip4, port)) ++ query6_tcp.listen(1) ++ query6_tcp.settimeout(1) ++ except: ++ query6_tcp.close() ++ havev6 = False ++except: ++ havev6 = False ++ ++signal.signal(signal.SIGTERM, sigterm) ++ ++f = open("ans.pid", "w") ++pid = os.getpid() ++print(pid, file=f) ++f.close() ++ ++running = True ++ ++print("Listening on %s port %d" % (ip4, port)) ++if havev6: ++ print("Listening on %s port %d" % (ip6, port)) ++print("Ctrl-c to quit") ++ ++if havev6: ++ input = [query4_udp, query4_tcp, query6_udp, query6_tcp] ++else: ++ input = [query4_udp, query4_tcp] ++ ++while running: ++ try: ++ inputready, outputready, exceptready = select.select(input, [], []) ++ except select.error as e: ++ break ++ except socket.error as e: ++ break ++ except KeyboardInterrupt: ++ break ++ ++ for s in inputready: ++ if s == query4_udp or s == query6_udp: ++ print("Query received on %s" % (ip4 if s == query4_udp else ip6)) ++ # Handle incoming queries ++ msg = s.recvfrom(65535) ++ rsp = create_response(msg[0]) ++ if rsp: ++ s.sendto(rsp, msg[1]) ++ elif s == query4_tcp or s == query6_tcp: ++ try: ++ conn, _ = s.accept() ++ if s == query4_tcp or s == query6_tcp: ++ print( ++ "TCP Query received on %s" % (ip4 if s == query4_tcp else ip6), ++ end=" ", ++ ) ++ # get TCP message length ++ msg = conn.recv(2) ++ if len(msg) != 2: ++ print("couldn't read TCP message length") ++ continue ++ length = struct.unpack(">H", msg[:2])[0] ++ msg = conn.recv(length) ++ if len(msg) != length: ++ print("couldn't read TCP message") ++ continue ++ rsp = create_response(msg) ++ if rsp: ++ conn.send(struct.pack(">H", len(rsp))) ++ conn.send(rsp) ++ conn.close() ++ except socket.error as e: ++ print("error: %s" % str(e)) ++ if not running: ++ break +diff --git a/bin/tests/system/chain/ans4/ans.py b/bin/tests/system/chain/ans4/ans.py +index 45d650417f..e4fc15a280 100755 +--- a/bin/tests/system/chain/ans4/ans.py ++++ b/bin/tests/system/chain/ans4/ans.py +@@ -276,16 +276,30 @@ except: port=5300 + try: ctrlport=int(os.environ['EXTRAPORT1']) + except: ctrlport=5300 + +-query4_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +-query4_socket.bind((ip4, port)) ++query4_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ++query4_udp.bind((ip4, port)) ++ ++query4_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ++query4_tcp.bind((ip4, port)) ++query4_tcp.listen(1) ++query4_tcp.settimeout(1) + + havev6 = True + try: +- query6_socket = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) ++ query6_udp = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) ++ try: ++ query6_udp.bind((ip6, port)) ++ except: ++ query6_udp.close() ++ havev6 = False ++ ++ query6_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: +- query6_socket.bind((ip6, port)) ++ query6_tcp.bind((ip4, port)) ++ query6_tcp.listen(1) ++ query6_tcp.settimeout(1) + except: +- query6_socket.close() ++ query6_tcp.close() + havev6 = False + except: + havev6 = False +@@ -310,9 +324,9 @@ print ("Control channel on %s port %d" % (ip4, ctrlport)) + print ("Ctrl-c to quit") + + if havev6: +- input = [query4_socket, query6_socket, ctrl_socket] ++ input = [query4_udp, query4_tcp, query6_udp, query6_tcp, ctrl_socket] + else: +- input = [query4_socket, ctrl_socket] ++ input = [query4_udp, query4_tcp, ctrl_socket] + + while running: + try: +@@ -335,13 +349,37 @@ while running: + break + ctl_channel(msg) + conn.close() +- if s == query4_socket or s == query6_socket: +- print ("Query received on %s" % +- (ip4 if s == query4_socket else ip6)) ++ elif s == query4_udp or s == query6_udp: ++ print("Query received on %s" % (ip4 if s == query4_udp else ip6)) + # Handle incoming queries + msg = s.recvfrom(65535) + rsp = create_response(msg[0]) + if rsp: + s.sendto(rsp, msg[1]) ++ elif s == query4_tcp or s == query6_tcp: ++ try: ++ conn, _ = s.accept() ++ if s == query4_tcp or s == query6_tcp: ++ print( ++ "TCP Query received on %s" % (ip4 if s == query4_tcp else ip6), ++ end=" ", ++ ) ++ # get TCP message length ++ msg = conn.recv(2) ++ if len(msg) != 2: ++ print("couldn't read TCP message length") ++ continue ++ length = struct.unpack(">H", msg[:2])[0] ++ msg = conn.recv(length) ++ if len(msg) != length: ++ print("couldn't read TCP message") ++ continue ++ rsp = create_response(msg) ++ if rsp: ++ conn.send(struct.pack(">H", len(rsp))) ++ conn.send(rsp) ++ conn.close() ++ except socket.error as e: ++ print("error: %s" % str(e)) + if not running: + break +diff --git a/bin/tests/system/cookie/ans9/ans.py b/bin/tests/system/cookie/ans9/ans.py +index b454fc8c91..dd48bdb095 100644 +--- a/bin/tests/system/cookie/ans9/ans.py ++++ b/bin/tests/system/cookie/ans9/ans.py +@@ -1,13 +1,13 @@ +-############################################################################ + # 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 ++# 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 __future__ import print_function + import os +@@ -35,28 +35,27 @@ from dns.rdataclass import * + from dns.rdatatype import * + from dns.tsig import * + ++ + # Log query to file + def logquery(type, qname): + with open("qlog", "a") as f: + f.write("%s %s\n", type, qname) + ++ + # DNS 2.0 keyring specifies the algorithm + try: +- keyring = dns.tsigkeyring.from_text({ "foo" : { +- "hmac-sha256", +- "aaaaaaaaaaaa" +- } , +- "fake" : { +- "hmac-sha256", +- "aaaaaaaaaaaa" +- } +- }) ++ keyring = dns.tsigkeyring.from_text( ++ { ++ "foo": {"hmac-sha256", "aaaaaaaaaaaa"}, ++ "fake": {"hmac-sha256", "aaaaaaaaaaaa"}, ++ } ++ ) + except: +- keyring = dns.tsigkeyring.from_text({ "foo" : "aaaaaaaaaaaa", +- "fake" : "aaaaaaaaaaaa" }) ++ keyring = dns.tsigkeyring.from_text({"foo": "aaaaaaaaaaaa", "fake": "aaaaaaaaaaaa"}) + + dopass2 = False + ++ + ############################################################################ + # + # This server will serve valid and spoofed answers. A spoofed answer will +@@ -81,7 +80,7 @@ def create_response(msg, tcp, first, ns10): + m = dns.message.from_wire(msg, keyring=keyring) + qname = m.question[0].name.to_text() + lqname = qname.lower() +- labels = lqname.split('.') ++ labels = lqname.split(".") + rrtype = m.question[0].rdtype + typename = dns.rdatatype.to_text(rrtype) + +@@ -113,27 +112,35 @@ def create_response(msg, tcp, first, ns10): + # Add a server cookie to the response + if labels[0] != "nocookie": + for o in m.options: +- if o.otype == 10: # Use 10 instead of COOKIE +- if first and labels[0] == "withtsig" and not tcp: +- r.use_tsig(keyring = keyring, +- keyname = dns.name.from_text("fake"), +- algorithm = HMAC_SHA256) +- elif labels[0] != "tcponly" or tcp: +- cookie = o +- if len(o.data) == 8: +- cookie.data = o.data + o.data +- else: +- cookie.data = o.data +- r.use_edns(options=[cookie]) ++ if o.otype == 10: # Use 10 instead of COOKIE ++ if first and labels[0] == "withtsig" and not tcp: ++ r.use_tsig( ++ keyring=keyring, ++ keyname=dns.name.from_text("fake"), ++ algorithm=HMAC_SHA256, ++ ) ++ elif labels[0] != "tcponly" or tcp: ++ cookie = o ++ try: ++ if len(o.server) == 0: ++ cookie.server = o.client ++ except AttributeError: # dnspython<2.7.0 compat ++ if len(o.data) == 8: ++ cookie.data = o.data + o.data ++ else: ++ cookie.data = o.data ++ r.use_edns(options=[cookie]) + r.flags |= dns.flags.AA + return r + ++ + def sigterm(signum, frame): +- print ("Shutting down now...") +- os.remove('ans.pid') ++ print("Shutting down now...") ++ os.remove("ans.pid") + running = False + sys.exit(0) + ++ + ############################################################################ + # Main + # +@@ -146,8 +153,10 @@ ip4_addr2 = "10.53.0.10" + ip6_addr1 = "fd92:7065:b8e:ffff::9" + ip6_addr2 = "fd92:7065:b8e:ffff::10" + +-try: port=int(os.environ['PORT']) +-except: port=5300 ++try: ++ port = int(os.environ["PORT"]) ++except: ++ port = 5300 + + query4_udp1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + query4_udp1.bind((ip4_addr1, port)) +@@ -195,24 +204,32 @@ except: + + signal.signal(signal.SIGTERM, sigterm) + +-f = open('ans.pid', 'w') ++f = open("ans.pid", "w") + pid = os.getpid() +-print (pid, file=f) ++print(pid, file=f) + f.close() + + running = True + +-print ("Using DNS version %s" % dns.version.version) +-print ("Listening on %s port %d" % (ip4_addr1, port)) +-print ("Listening on %s port %d" % (ip4_addr2, port)) ++print("Using DNS version %s" % dns.version.version) ++print("Listening on %s port %d" % (ip4_addr1, port)) ++print("Listening on %s port %d" % (ip4_addr2, port)) + if havev6: +- print ("Listening on %s port %d" % (ip6_addr1, port)) +- print ("Listening on %s port %d" % (ip6_addr2, port)) +-print ("Ctrl-c to quit") ++ print("Listening on %s port %d" % (ip6_addr1, port)) ++ print("Listening on %s port %d" % (ip6_addr2, port)) ++print("Ctrl-c to quit") + + if havev6: +- input = [query4_udp1, query6_udp1, query4_tcp1, query6_tcp1, +- query4_udp2, query6_udp2, query4_tcp2, query6_tcp2] ++ input = [ ++ query4_udp1, ++ query6_udp1, ++ query4_tcp1, ++ query6_tcp1, ++ query4_udp2, ++ query6_udp2, ++ query4_tcp2, ++ query6_tcp2, ++ ] + else: + input = [query4_udp1, query4_tcp1, query4_udp2, query4_tcp2] + +@@ -228,14 +245,19 @@ while running: + + for s in inputready: + ns10 = False +- if s == query4_udp1 or s == query6_udp1 or \ +- s == query4_udp2 or s == query6_udp2: ++ if s == query4_udp1 or s == query6_udp1 or s == query4_udp2 or s == query6_udp2: + if s == query4_udp1 or s == query6_udp1: +- print ("UDP Query received on %s" % +- (ip4_addr1 if s == query4_udp1 else ip6_addr1), end=" ") ++ print( ++ "UDP Query received on %s" ++ % (ip4_addr1 if s == query4_udp1 else ip6_addr1), ++ end=" ", ++ ) + if s == query4_udp2 or s == query6_udp2: +- print ("UDP Query received on %s" % +- (ip4_addr2 if s == query4_udp2 else ip6_addr2), end=" ") ++ print( ++ "UDP Query received on %s" ++ % (ip4_addr2 if s == query4_udp2 else ip6_addr2), ++ end=" ", ++ ) + ns10 = True + # Handle incoming queries + msg = s.recvfrom(65535) +@@ -244,31 +266,36 @@ while running: + print(dns.rcode.to_text(rsp.rcode())) + s.sendto(rsp.to_wire(), msg[1]) + if dopass2: +- print ("Sending second UDP response without TSIG", end=" ") ++ print("Sending second UDP response without TSIG", end=" ") + rsp = create_response(msg[0], False, False, ns10) + s.sendto(rsp.to_wire(), msg[1]) + print(dns.rcode.to_text(rsp.rcode())) + +- if s == query4_tcp1 or s == query6_tcp1 or \ +- s == query4_tcp2 or s == query6_tcp2: ++ if s == query4_tcp1 or s == query6_tcp1 or s == query4_tcp2 or s == query6_tcp2: + try: + (cs, _) = s.accept() + if s == query4_tcp1 or s == query6_tcp1: +- print ("TCP Query received on %s" % +- (ip4_addr1 if s == query4_tcp1 else ip6_addr1), end=" ") ++ print( ++ "TCP Query received on %s" ++ % (ip4_addr1 if s == query4_tcp1 else ip6_addr1), ++ end=" ", ++ ) + if s == query4_tcp2 or s == query6_tcp2: +- print ("TCP Query received on %s" % +- (ip4_addr2 if s == query4_tcp2 else ip6_addr2), end=" ") ++ print( ++ "TCP Query received on %s" ++ % (ip4_addr2 if s == query4_tcp2 else ip6_addr2), ++ end=" ", ++ ) + ns10 = True + # get TCP message length + buf = cs.recv(2) +- length = struct.unpack('>H', buf[:2])[0] ++ length = struct.unpack(">H", buf[:2])[0] + # grep DNS message + msg = cs.recv(length) + rsp = create_response(msg, True, True, ns10) + print(dns.rcode.to_text(rsp.rcode())) + wire = rsp.to_wire() +- cs.send(struct.pack('>H', len(wire))) ++ cs.send(struct.pack(">H", len(wire))) + cs.send(wire) + cs.close() + except s.timeout: +diff --git a/lib/dns/include/dns/message.h b/lib/dns/include/dns/message.h +index 96c5ef1f79..54208a49e6 100644 +--- a/lib/dns/include/dns/message.h ++++ b/lib/dns/include/dns/message.h +@@ -226,6 +226,7 @@ struct dns_message { + unsigned int cc_bad : 1; + unsigned int tkey : 1; + unsigned int rdclass_set : 1; ++ unsigned int has_dname : 1; + + unsigned int opt_reserved; + unsigned int sig_reserved; +@@ -1408,6 +1409,13 @@ dns_message_setclass(dns_message_t *msg, dns_rdataclass_t rdclass); + * \li msg be a valid message with parsing intent. + */ + ++bool ++dns_message_hasdname(dns_message_t *msg); ++/*%< ++ * Return whether a DNAME was detected in the ANSWER section of a QUERY ++ * message when it was parsed. ++ */ ++ + ISC_LANG_ENDDECLS + + #endif /* DNS_MESSAGE_H */ +diff --git a/lib/dns/message.c b/lib/dns/message.c +index 9ea2b9e7fb..5603bcadf9 100644 +--- a/lib/dns/message.c ++++ b/lib/dns/message.c +@@ -459,6 +459,7 @@ msginit(dns_message_t *m) { + m->cc_bad = 0; + m->tkey = 0; + m->rdclass_set = 0; ++ m->has_dname = 0; + m->querytsig = NULL; + m->indent.string = dns_master_indentstr; + m->indent.count = dns_master_indent; +@@ -1829,6 +1830,11 @@ getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t *dctx, + */ + msg->tsigname->attributes |= DNS_NAMEATTR_NOCOMPRESS; + free_name = false; ++ } else if (rdtype == dns_rdatatype_dname && ++ sectionid == DNS_SECTION_ANSWER && ++ msg->opcode == dns_opcode_query) ++ { ++ msg->has_dname = 1; + } + rdataset = NULL; + +@@ -4699,3 +4705,9 @@ dns_message_setclass(dns_message_t *msg, dns_rdataclass_t rdclass) { + msg->rdclass = rdclass; + msg->rdclass_set = 1; + } ++ ++bool ++dns_message_hasdname(dns_message_t *msg) { ++ REQUIRE(DNS_MESSAGE_VALID(msg)); ++ return msg->has_dname; ++} +diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c +index 487107614c..1c704551aa 100644 +--- a/lib/dns/resolver.c ++++ b/lib/dns/resolver.c +@@ -428,6 +428,7 @@ typedef struct { + typedef struct { + fetchctx_t * fctx; + dns_message_t * rmessage; ++ dns_name_t * ns_name; + } dns_chkarg_t; + + struct dns_fetch { +@@ -6259,7 +6260,9 @@ mark_related(dns_name_t *name, dns_rdataset_t *rdataset, + * locally served zone. + */ + static inline bool +-name_external(dns_name_t *name, dns_rdatatype_t type, fetchctx_t *fctx) { ++name_external(dns_name_t *name, dns_name_t *ns_name, dns_rdatatype_t type, ++ fetchctx_t *fctx) ++{ + isc_result_t result; + dns_forwarders_t *forwarders = NULL; + dns_fixedname_t fixed, zfixed; +@@ -6277,7 +6280,9 @@ name_external(dns_name_t *name, dns_rdatatype_t type, fetchctx_t *fctx) { + int _orderp = 0; + unsigned int _nlabelsp = 0; + +- apex = ISFORWARDER(fctx->addrinfo) ? fctx->fwdname : &fctx->domain; ++ apex = ISFORWARDER(fctx->addrinfo) ++ ? fctx->fwdname ++ : (ns_name != NULL) ? ns_name : &fctx->domain; + + /* + * The name is outside the queried namespace. +@@ -6386,7 +6391,7 @@ check_section(void *arg, dns_name_t *addname, dns_rdatatype_t type, + result = dns_message_findname(rmessage, section, addname, + dns_rdatatype_any, 0, &name, NULL); + if (result == ISC_R_SUCCESS) { +- external = name_external(name, type, fctx); ++ external = name_external(name, chkarg->ns_name, type, fctx); + if (type == dns_rdatatype_a) { + for (rdataset = ISC_LIST_HEAD(name->list); + rdataset != NULL; +@@ -6460,7 +6465,7 @@ chase_additional(fetchctx_t *fctx, dns_message_t *rmessage) { + rdataset != NULL; + rdataset = ISC_LIST_NEXT(rdataset, link)) { + if (CHASE(rdataset)) { +- dns_chkarg_t chkarg; ++ dns_chkarg_t chkarg = { 0 }; + chkarg.fctx = fctx; + chkarg.rmessage = rmessage; + rdataset->attributes &= ~DNS_RDATASETATTR_CHASE; +@@ -7068,7 +7073,7 @@ noanswer_response(fetchctx_t *fctx, dns_message_t *message, + * we're not following a chain.) + */ + if (!negative_response && ns_name != NULL && oqname == NULL) { +- dns_chkarg_t chkarg; ++ dns_chkarg_t chkarg = { 0 }; + /* + * We already know ns_name is a subdomain of fctx->domain. + * If ns_name is equal to fctx->domain, we're not making +@@ -7100,6 +7105,7 @@ noanswer_response(fetchctx_t *fctx, dns_message_t *message, + FCTX_ATTR_SET(fctx, FCTX_ATTR_GLUING); + chkarg.fctx = fctx; + chkarg.rmessage = message; ++ chkarg.ns_name = ns_name; + (void)dns_rdataset_additionaldata(ns_rdataset, check_related, + &chkarg); + #if CHECK_FOR_GLUE_IN_ANSWER +@@ -7113,7 +7119,7 @@ noanswer_response(fetchctx_t *fctx, dns_message_t *message, + if ((look_in_options & LOOK_FOR_GLUE_IN_ANSWER) != 0 && + (fctx->type == dns_rdatatype_aaaa || + fctx->type == dns_rdatatype_a)) { +- dns_chkarg_t chkarg; ++ dns_chkarg_t chkarg = { 0 }; + chkarg.fcx = fctx; + chkarg.rmessage = message; + (void)dns_rdataset_additionaldata(ns_rdataset, +@@ -7193,8 +7199,9 @@ validinanswer(dns_rdataset_t *rdataset, fetchctx_t *fctx) { + } + + static isc_result_t +-answer_response(fetchctx_t *fctx, dns_message_t *message) { ++answer_response(resquery_t *query, dns_message_t *message) { + isc_result_t result; ++ fetchctx_t *fctx = NULL; + dns_name_t *name = NULL, *qname = NULL, *ns_name = NULL; + dns_name_t *aname = NULL, *cname = NULL, *dname = NULL; + dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL; +@@ -7207,6 +7214,8 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { + dns_view_t *view = NULL; + dns_trust_t trust; + ++ REQUIRE(VALID_QUERY(query)); ++ fctx = query->fctx; + REQUIRE(VALID_FCTX(fctx)); + + FCTXTRACE("answer_response"); +@@ -7271,7 +7280,9 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { + /* + * Don't accept DNAME from parent namespace. + */ +- if (name_external(name, dns_rdatatype_dname, fctx)) { ++ if (name_external(name, NULL, dns_rdatatype_dname, ++ fctx)) ++ { + continue; + } + +@@ -7326,7 +7337,7 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { + rdataset != NULL; + rdataset = ISC_LIST_NEXT(rdataset, link)) + { +- dns_chkarg_t chkarg; ++ dns_chkarg_t chkarg = { 0 }; + if (!validinanswer(rdataset, fctx)) { + return (DNS_R_FORMERR); + } +@@ -7357,12 +7368,13 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { + rdataset->attributes &= ~DNS_RDATASETATTR_CHASE; + chkarg.fctx = fctx; + chkarg.rmessage = message; ++ chkarg.ns_name = ns_name; + (void)dns_rdataset_additionaldata(rdataset, + check_related, + &chkarg); + } + } else if (aname != NULL) { +- dns_chkarg_t chkarg; ++ dns_chkarg_t chkarg = { 0 }; + if (!validinanswer(ardataset, fctx)) + return (DNS_R_FORMERR); + if ((ardataset->type == dns_rdatatype_a || +@@ -7386,6 +7398,7 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { + ardataset->trust = trust; + chkarg.fctx = fctx; + chkarg.rmessage = message; ++ chkarg.ns_name = ns_name; + (void)dns_rdataset_additionaldata(ardataset, check_related, + &chkarg); + for (sigrdataset = ISC_LIST_HEAD(aname->list); +@@ -7507,12 +7520,23 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { + * + * We expect there to be only one owner name for all the rdatasets + * in this section, and we expect that it is not external. ++ * ++ * If the message was not sent over TCP or otherwise secured, ++ * skip this. + */ +- result = dns_message_firstname(message, DNS_SECTION_AUTHORITY); ++ if (message->cc_ok || message->tsig != NULL || message->sig0 != NULL || ++ (query->options & DNS_FETCHOPT_TCP) != 0) ++ { ++ result = dns_message_firstname(message, DNS_SECTION_AUTHORITY); ++ } else { ++ done = true; ++ } + while (!done && result == ISC_R_SUCCESS) { + name = NULL; + dns_message_currentname(message, DNS_SECTION_AUTHORITY, &name); +- if (!name_external(name, dns_rdatatype_ns, fctx)) { ++ if (!name_external(name, ns_name, dns_rdatatype_ns, fctx) && ++ dns_name_issubdomain(&fctx->name, name)) ++ { + /* + * We expect to find NS or SIG NS rdatasets, and + * nothing else. +@@ -7523,7 +7547,7 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { + if (rdataset->type == dns_rdatatype_ns || + (rdataset->type == dns_rdatatype_rrsig && + rdataset->covers == dns_rdatatype_ns)) { +- dns_chkarg_t chkarg; ++ dns_chkarg_t chkarg = { 0 }; + name->attributes |= + DNS_NAMEATTR_CACHE; + rdataset->attributes |= +@@ -7547,6 +7571,7 @@ answer_response(fetchctx_t *fctx, dns_message_t *message) { + */ + chkarg.fctx = fctx; + chkarg.rmessage = message; ++ chkarg.ns_name = ns_name; + (void)dns_rdataset_additionaldata( + rdataset, + check_related, +@@ -7988,6 +8013,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + unsigned int bucketnum; + dns_resolver_t *res; + bool bucket_empty; ++ bool secured = false; + #ifdef HAVE_DNSTAP + isc_socket_t *sock = NULL; + isc_sockaddr_t localaddr, *la = NULL; +@@ -8298,6 +8324,17 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + goto done; + } + ++ /* ++ * Remember whether this message was signed or had a ++ * valid client cookie; if not, we may need to retry over ++ * TCP later. ++ */ ++ if (rmessage->cc_ok || rmessage->tsig != NULL || ++ rmessage->sig0 != NULL) ++ { ++ secured = true; ++ } ++ + /* + * The dispatcher should ensure we only get responses with QR set. + */ +@@ -8315,10 +8352,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + * This may be a misconfigured anycast server or an attempt to send + * a spoofed response. Skip if we have a valid tsig. + */ +- if (dns_message_gettsig(rmessage, NULL) == NULL && +- !rmessage->cc_ok && !rmessage->cc_bad && +- (options & DNS_FETCHOPT_TCP) == 0) +- { ++ if (!secured && (options & DNS_FETCHOPT_TCP) == 0) { + unsigned char cookie[COOKIE_BUFFER_SIZE]; + if (dns_adb_getcookie(fctx->adb, query->addrinfo, cookie, + sizeof(cookie)) > CLIENT_COOKIE_SIZE) +@@ -8341,6 +8375,16 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + * XXXMPA When support for DNS COOKIE becomes ubiquitous, fall + * back to TCP for all non-COOKIE responses. + */ ++ ++ /* ++ * Check whether we need to retry over TCP for some other ++ * reason. ++ */ ++ if (dns_message_hasdname(rmessage)) { ++ options |= DNS_FETCHOPT_TCP; ++ resend = true; ++ goto done; ++ } + } + + /* +@@ -8697,7 +8741,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + if ((rmessage->flags & DNS_MESSAGEFLAG_AA) != 0 || + ISFORWARDER(query->addrinfo)) + { +- result = answer_response(fctx, rmessage); ++ result = answer_response(query, rmessage); + if (result != ISC_R_SUCCESS) + FCTXTRACE3("answer_response (AA/fwd)", result); + } else if (iscname(fctx, rmessage) && +@@ -8709,7 +8753,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + * answer when a CNAME is followed. We should treat + * it as a valid answer. + */ +- result = answer_response(fctx, rmessage); ++ result = answer_response(query, rmessage); + if (result != ISC_R_SUCCESS) + FCTXTRACE3("answer_response (!ANY/!CNAME)", + result); +@@ -8718,7 +8762,7 @@ resquery_response(isc_task_t *task, isc_event_t *event) { + /* + * Lame response !!!. + */ +- result = answer_response(fctx, rmessage); ++ result = answer_response(query, rmessage); + if (result != ISC_R_SUCCESS) + FCTXTRACE3("answer_response (!NS)", result); + } else { +diff --git a/lib/dns/win32/libdns.def.in b/lib/dns/win32/libdns.def.in +index 7320653439..b48bc89bbd 100644 +--- a/lib/dns/win32/libdns.def.in ++++ b/lib/dns/win32/libdns.def.in +@@ -537,6 +537,7 @@ dns_message_gettemprdataset + dns_message_gettimeadjust + dns_message_gettsig + dns_message_gettsigkey ++dns_message_hasdname + dns_message_logfmtpacket + dns_message_logfmtpacket2 + dns_message_logpacket +-- +2.51.1 + diff --git a/SOURCES/bind-9.11-d-max-records-checkconf.patch b/SOURCES/bind-9.11-d-max-records-checkconf.patch new file mode 100644 index 0000000..2e46765 --- /dev/null +++ b/SOURCES/bind-9.11-d-max-records-checkconf.patch @@ -0,0 +1,61 @@ +From 83f283c3aeae99570c9e4c20f10e92ba565fc4be Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Petr=20Men=C5=A1=C3=ADk?= +Date: Tue, 7 Oct 2025 16:18:03 +0200 +Subject: [PATCH] Implement settings limits also in named-checkconf + +Read and parse max-records-per-type and max-types-per-name options in +case -z parameter is passed. +--- + bin/check/named-checkconf.c | 27 +++++++++++++++++++++++++-- + 1 file changed, 25 insertions(+), 2 deletions(-) + +diff --git a/bin/check/named-checkconf.c b/bin/check/named-checkconf.c +index e5afd52..42ef76c 100644 +--- a/bin/check/named-checkconf.c ++++ b/bin/check/named-checkconf.c +@@ -415,6 +415,24 @@ configure_zone(const char *vclass, const char *view, + return (result); + } + ++/* Red Hat 9.11 specific extension. */ ++static void ++configure_maxrecords(const cfg_obj_t *voptions) ++{ ++ cfg_obj_t *obj; ++ isc_result_t result; ++ ++ obj = NULL; ++ result = cfg_map_get(voptions, "max-records-per-type", &obj); ++ if (result == ISC_R_SUCCESS) ++ dns_db_setmaxrrperset(cfg_obj_asuint32(obj)); ++ ++ obj = NULL; ++ result = cfg_map_get(voptions, "max-types-per-name", &obj); ++ if (result == ISC_R_SUCCESS) ++ dns_db_setmaxtypepername(cfg_obj_asuint32(obj)); ++} ++ + /*% configure a view */ + static isc_result_t + configure_view(const char *vclass, const char *view, const cfg_obj_t *config, +@@ -431,10 +449,15 @@ configure_view(const char *vclass, const char *view, const cfg_obj_t *config, + voptions = cfg_tuple_get(vconfig, "options"); + + zonelist = NULL; +- if (voptions != NULL) ++ if (voptions != NULL) { + (void)cfg_map_get(voptions, "zone", &zonelist); +- else ++ configure_maxrecords(voptions); ++ } else { + (void)cfg_map_get(config, "zone", &zonelist); ++ tresult = cfg_map_get(config, "options", &voptions); ++ if (tresult == ISC_R_SUCCESS) ++ configure_maxrecords(voptions); ++ } + + for (element = cfg_list_first(zonelist); + element != NULL; +-- +2.51.0 + diff --git a/SOURCES/bind-9.11-d-max-records-per-type.patch b/SOURCES/bind-9.11-d-max-records-per-type.patch new file mode 100644 index 0000000..7c3a169 --- /dev/null +++ b/SOURCES/bind-9.11-d-max-records-per-type.patch @@ -0,0 +1,250 @@ +From e0238189d03dc0a6b6092180ba52e74a26816422 Mon Sep 17 00:00:00 2001 +From: Petr Mensik +Date: Thu, 10 Jul 2025 17:31:35 +0200 +Subject: [PATCH] Minimalistic support for max-records-per-type option + +Just propagate the number to rbtdb in addition to environment. Make +environment preferred of both used, because default configuration value +would override already changed default. + +Allow also 0 value from the environment. +--- + bin/named/config.c | 1 + + bin/named/named.conf.docbook | 1 + + bin/named/server.c | 9 +++++++++ + doc/arm/Bv9ARM-book.xml | 18 ++++++++++++++++++ + lib/dns/db.c | 6 +++++- + lib/dns/include/dns/db.h | 10 ++++++++++ + lib/dns/include/dns/rdataslab.h | 6 ++++++ + lib/dns/rbtdb.c | 18 +++++++++++++++++- + lib/dns/rbtdb.h | 10 ++++++++++ + lib/dns/rbtdb64.h | 3 +++ + lib/isccfg/namedconf.c | 1 + + 11 files changed, 81 insertions(+), 2 deletions(-) + +diff --git a/bin/named/config.c b/bin/named/config.c +index e3731cf..27cf9ee 100644 +--- a/bin/named/config.c ++++ b/bin/named/config.c +@@ -243,6 +243,7 @@ options {\n\ + # max-ixfr-log-size \n\ + max-journal-size unlimited;\n\ + max-records 0;\n\ ++ max-records-per-type 100;\n\ + max-refresh-time 2419200; /* 4 weeks */\n\ + max-retry-time 1209600; /* 2 weeks */\n\ + max-transfer-idle-in 60;\n\ +diff --git a/bin/named/named.conf.docbook b/bin/named/named.conf.docbook +index 31fac33..d7934c7 100644 +--- a/bin/named/named.conf.docbook ++++ b/bin/named/named.conf.docbook +@@ -338,6 +338,7 @@ options { + max-journal-size ( unlimited | sizeval ); + max-ncache-ttl integer; + max-records integer; ++ max-records-per-type integer; + max-recursion-depth integer; + max-recursion-queries integer; + max-refresh-time integer; +diff --git a/bin/named/server.c b/bin/named/server.c +index afdc4fa..2e88df7 100644 +--- a/bin/named/server.c ++++ b/bin/named/server.c +@@ -4606,6 +4606,15 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, + cfg_obj_asuint32(obj), + max_clients_per_query); + ++ /* ++ * This is used for the cache and also as a default value ++ * for zone databases. ++ */ ++ obj = NULL; ++ result = ns_config_get(maps, "max-records-per-type", &obj); ++ INSIST(result == ISC_R_SUCCESS); ++ dns_db_setmaxrrperset(cfg_obj_asuint32(obj)); ++ + obj = NULL; + result = ns_config_get(maps, "max-recursion-depth", &obj); + INSIST(result == ISC_R_SUCCESS); +diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml +index 563dced..25acad0 100644 +--- a/doc/arm/Bv9ARM-book.xml ++++ b/doc/arm/Bv9ARM-book.xml +@@ -8318,6 +8318,24 @@ avoid-v6-udp-ports { 40000; range 50000 60000; }; + + + ++ ++ max-records-per-type ++ ++ ++ This sets the maximum number of resource records that can be stored ++ in an RRset in a database. Can be configured in , ++ only. ++ ++ ++ ++ If set to a positive value, any attempt to cache or to add to a zone ++ an RRset with more than the specified number of records will result in ++ a failure. If set to 0, there is no cap on RRset size. The default is ++ 100. ++ ++ ++ ++ + + recursive-clients + +diff --git a/lib/dns/db.c b/lib/dns/db.c +index c581646..9e7632a 100644 +--- a/lib/dns/db.c ++++ b/lib/dns/db.c +@@ -1130,7 +1130,6 @@ dns_db_nodefullname(dns_db_t *db, dns_dbnode_t *node, dns_name_t *name) { + return (ISC_R_NOTIMPLEMENTED); + return ((db->methods->nodefullname)(db, node, name)); + } +- + isc_result_t + dns_db_setservestalettl(dns_db_t *db, dns_ttl_t ttl) + { +@@ -1152,3 +1151,8 @@ dns_db_getservestalettl(dns_db_t *db, dns_ttl_t *ttl) + return ((db->methods->getservestalettl)(db, ttl)); + return (ISC_R_NOTIMPLEMENTED); + } ++void ++dns_db_setmaxrrperset(uint32_t maxrrperset) { ++ dns_rbtdb_setmaxrrperset(maxrrperset); ++ dns_rbtdb64_setmaxrrperset(maxrrperset); ++} +diff --git a/lib/dns/include/dns/db.h b/lib/dns/include/dns/db.h +index 452770f..6357bfd 100644 +--- a/lib/dns/include/dns/db.h ++++ b/lib/dns/include/dns/db.h +@@ -1718,6 +1718,16 @@ dns_db_getservestalettl(dns_db_t *db, dns_ttl_t *ttl); + * \li #ISC_R_NOTIMPLEMENTED - Not supported by this DB implementation. + */ + ++void ++dns_db_setmaxrrperset(uint32_t maxrrperset); ++/*%< ++ * Sets the maximum number of records per rrset permitted in a database. ++ * 0 implies unlimited. ++ * ++ * Returns: ++ *\li void ++ */ ++ + ISC_LANG_ENDDECLS + + #endif /* DNS_DB_H */ +diff --git a/lib/dns/include/dns/rdataslab.h b/lib/dns/include/dns/rdataslab.h +index f38d539..40c40a8 100644 +--- a/lib/dns/include/dns/rdataslab.h ++++ b/lib/dns/include/dns/rdataslab.h +@@ -173,6 +173,12 @@ dns_rdataslab_equalx(unsigned char *slab1, unsigned char *slab2, + *\li true if the slabs are equal, #false otherwise. + */ + ++void ++dns_rdataslab_setmaxrrperset(uint32_t maxrrperset); ++/*%< ++ * Set global limit of max-records-per-type value. ++ */ ++ + ISC_LANG_ENDDECLS + + #endif /* DNS_RDATASLAB_H */ +diff --git a/lib/dns/rbtdb.c b/lib/dns/rbtdb.c +index 5263e7c..388ffdf 100644 +--- a/lib/dns/rbtdb.c ++++ b/lib/dns/rbtdb.c +@@ -981,6 +981,7 @@ static bool match_header_version(rbtdb_file_header_t *header); + /* Pad to 32 bytes */ + static char FILE_VERSION[32] = "\0"; + ++ + /*% + * 'init_count' is used to initialize 'newheader->count' which inturn + * is used to determine where in the cycle rrset-order cyclic starts. +@@ -6321,6 +6322,19 @@ update_recordsandbytes(bool add, rbtdb_version_t *rbtversion, + #endif /* DNS_RBTDB_MAX_RTYPES */ + + static uint32_t dns_g_rbtdb_max_rtypes = DNS_RBTDB_MAX_RTYPES; ++static bool dns_g_rbtdb_max_rtypes_fromenv = false; ++void ++#ifdef DNS_RBTDB_VERSION64 ++dns_rbtdb64_setmaxtypepername(uint32_t maxrrperset) ++#else ++dns_rbtdb_setmaxtypepername(uint32_t maxrrperset) ++#endif ++{ ++ if (!dns_g_rbtdb_max_rtypes_fromenv) { ++ /* Make environment override configuration to avoid resetting to default value. */ ++ dns_g_rbtdb_max_rtypes = maxrrperset; ++ } ++} + + static void + init_max_rtypes(void) { +@@ -6329,8 +6343,10 @@ init_max_rtypes(void) { + if (max) { + char *endp = NULL; + long l = strtol(max, &endp, 10); +- if (max != endp && endp && !*endp && l >= 0) ++ if (max != endp && endp && !*endp) { + dns_g_rbtdb_max_rtypes = l; ++ dns_g_rbtdb_max_rtypes_fromenv = true; ++ } + } + } + +diff --git a/lib/dns/rbtdb.h b/lib/dns/rbtdb.h +index cd84b5b..4f2d890 100644 +--- a/lib/dns/rbtdb.h ++++ b/lib/dns/rbtdb.h +@@ -45,6 +45,16 @@ dns_rbtdb_create(isc_mem_t *mctx, dns_name_t *base, dns_dbtype_t type, + * \li argc == 0 or argv[0] is a valid memory context. + */ + ++void ++dns_rbtdb_setmaxtypepername(uint32_t value); ++/*%< ++ * Set the maximum permissible number of RR types per owner name. ++ * 0 implies unlimited. ++ * ++ * Returns: ++ *\li void ++ */ ++ + ISC_LANG_ENDDECLS + + #endif /* DNS_RBTDB_H */ +diff --git a/lib/dns/rbtdb64.h b/lib/dns/rbtdb64.h +index 33b0115..77ae5b0 100644 +--- a/lib/dns/rbtdb64.h ++++ b/lib/dns/rbtdb64.h +@@ -32,6 +32,9 @@ dns_rbtdb64_create(isc_mem_t *mctx, dns_name_t *base, dns_dbtype_t type, + dns_rdataclass_t rdclass, unsigned int argc, char *argv[], + void *driverarg, dns_db_t **dbp); + ++void ++dns_rbtdb64_setmaxtypepername(uint32_t value); ++ + ISC_LANG_ENDDECLS + + #endif /* DNS_RBTDB64_H */ +diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c +index 667111c..fc46a64 100644 +--- a/lib/isccfg/namedconf.c ++++ b/lib/isccfg/namedconf.c +@@ -1100,6 +1100,7 @@ options_clauses[] = { + { "lock-file", &cfg_type_qstringornone, 0 }, + { "managed-keys-directory", &cfg_type_qstring, 0 }, + { "match-mapped-addresses", &cfg_type_boolean, 0 }, ++ { "max-records-per-type", &cfg_type_uint32, 0 }, + { "max-rsa-exponent-size", &cfg_type_uint32, 0 }, + { "memstatistics", &cfg_type_boolean, 0 }, + { "memstatistics-file", &cfg_type_qstring, 0 }, +-- +2.50.0 + diff --git a/SOURCES/bind-9.11-d-max-types-per-name.patch b/SOURCES/bind-9.11-d-max-types-per-name.patch new file mode 100644 index 0000000..abd5863 --- /dev/null +++ b/SOURCES/bind-9.11-d-max-types-per-name.patch @@ -0,0 +1,196 @@ +From ba30ef9b8dbe3dacced19d80a8b27854a794b334 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= +Date: Sat, 25 May 2024 11:46:56 +0200 +Subject: [PATCH] Minimalistic support for max-types-per-name option + +Just add support for parsing of value from options to environment +settable number. Keep environment value preferred, overriding +configuration file value if present. Should avoid overriding environment +set value by just default config value. + +Allow also value 0 from environment. +--- + bin/named/config.c | 1 + + bin/named/named.conf.docbook | 1 + + bin/named/server.c | 9 +++++++++ + doc/arm/Bv9ARM-book.xml | 19 +++++++++++++++++++ + lib/dns/db.c | 12 ++++++++++-- + lib/dns/include/dns/db.h | 9 +++++++++ + lib/dns/rdataslab.c | 14 +++++++++++++- + lib/isccfg/namedconf.c | 1 + + 8 files changed, 63 insertions(+), 3 deletions(-) + +diff --git a/bin/named/config.c b/bin/named/config.c +index 27cf9ee..c4d44ef 100644 +--- a/bin/named/config.c ++++ b/bin/named/config.c +@@ -246,6 +246,7 @@ options {\n\ + max-records-per-type 100;\n\ + max-refresh-time 2419200; /* 4 weeks */\n\ + max-retry-time 1209600; /* 2 weeks */\n\ ++ max-types-per-name 100;\n\ + max-transfer-idle-in 60;\n\ + max-transfer-idle-out 60;\n\ + max-transfer-time-in 120;\n\ +diff --git a/bin/named/named.conf.docbook b/bin/named/named.conf.docbook +index d7934c7..a4b1d76 100644 +--- a/bin/named/named.conf.docbook ++++ b/bin/named/named.conf.docbook +@@ -348,6 +348,7 @@ options { + max-transfer-idle-out integer; + max-transfer-time-in integer; + max-transfer-time-out integer; ++ max-types-per-name integer; + max-udp-size integer; + max-zone-ttl ( unlimited | ttlval ); + memstatistics boolean; +diff --git a/bin/named/server.c b/bin/named/server.c +index 2e88df7..2086e41 100644 +--- a/bin/named/server.c ++++ b/bin/named/server.c +@@ -4615,6 +4615,15 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, + INSIST(result == ISC_R_SUCCESS); + dns_db_setmaxrrperset(cfg_obj_asuint32(obj)); + ++ /* ++ * This is used for the cache and also as a default value ++ * for zone databases. ++ */ ++ obj = NULL; ++ result = ns_config_get(maps, "max-types-per-name", &obj); ++ INSIST(result == ISC_R_SUCCESS); ++ dns_db_setmaxtypepername(cfg_obj_asuint32(obj)); ++ + obj = NULL; + result = ns_config_get(maps, "max-recursion-depth", &obj); + INSIST(result == ISC_R_SUCCESS); +diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml +index 25acad0..70fd769 100644 +--- a/doc/arm/Bv9ARM-book.xml ++++ b/doc/arm/Bv9ARM-book.xml +@@ -8336,6 +8336,25 @@ avoid-v6-udp-ports { 40000; range 50000 60000; }; + + + ++ ++ max-types-per-name ++ ++ ++ This sets the maximum number of resource record types that can be stored ++ for a single owner name in a database. Can be configured in ++ only. ++ ++ ++ ++ If set to a positive value, any attempt to cache or to add to a zone an owner ++ name with more than the specified number of resource record types will result ++ in a failure. If set to 0, there is no cap on RR types number. The default is ++ 100. ++ ++ ++ ++ ++ + + recursive-clients + +diff --git a/lib/dns/db.c b/lib/dns/db.c +index 9e7632a..b0f8960 100644 +--- a/lib/dns/db.c ++++ b/lib/dns/db.c +@@ -35,6 +35,7 @@ + #include + #include + #include ++#include + #include + #include + +@@ -1151,8 +1152,15 @@ dns_db_getservestalettl(dns_db_t *db, dns_ttl_t *ttl) + return ((db->methods->getservestalettl)(db, ttl)); + return (ISC_R_NOTIMPLEMENTED); + } ++ ++/* Emulation of more complex changes later. */ + void + dns_db_setmaxrrperset(uint32_t maxrrperset) { +- dns_rbtdb_setmaxrrperset(maxrrperset); +- dns_rbtdb64_setmaxrrperset(maxrrperset); ++ dns_rdataslab_setmaxrrperset(maxrrperset); ++} ++ ++void ++dns_db_setmaxtypepername(uint32_t value) { ++ dns_rbtdb_setmaxtypepername(value); ++ dns_rbtdb64_setmaxtypepername(value); + } +diff --git a/lib/dns/include/dns/db.h b/lib/dns/include/dns/db.h +index 6357bfd..f6eae9b 100644 +--- a/lib/dns/include/dns/db.h ++++ b/lib/dns/include/dns/db.h +@@ -1728,6 +1728,15 @@ dns_db_setmaxrrperset(uint32_t maxrrperset); + *\li void + */ + ++void ++dns_db_setmaxtypepername(uint32_t value); ++/*%< ++ * Set the maximum permissible number of RR types per owner name. ++ * ++ * If 'value' is nonzero, then any subsequent attempt to add an rdataset with a ++ * RR type that would exceed the number of already stored RR types will return ++ * ISC_R_NOSPACE. ++ */ + ISC_LANG_ENDDECLS + + #endif /* DNS_DB_H */ +diff --git a/lib/dns/rdataslab.c b/lib/dns/rdataslab.c +index 9566f79..8ea9ef4 100644 +--- a/lib/dns/rdataslab.c ++++ b/lib/dns/rdataslab.c +@@ -121,8 +121,18 @@ fillin_offsets(unsigned char *offsetbase, unsigned int *offsettable, + #endif /* DNS_RDATASET_MAX_RECORDS */ + + static unsigned int dns_g_rdataset_max_records = DNS_RDATASET_MAX_RECORDS; ++static bool dns_g_rdataset_max_records_fromenv = false; + static isc_once_t once = ISC_ONCE_INIT; + ++void ++dns_rdataslab_setmaxrrperset(uint32_t maxrrperset) ++{ ++ if (!dns_g_rdataset_max_records_fromenv) { ++ /* Make environment override configuration to avoid resetting to default value. */ ++ dns_g_rdataset_max_records = maxrrperset; ++ } ++} ++ + static void + init_max_records(void) { + /* Red Hat change, allow setting different max value by environment. */ +@@ -130,8 +140,10 @@ init_max_records(void) { + if (max) { + char *endp = NULL; + long l = strtol(max, &endp, 10); +- if (max != endp && endp && !*endp && l > 0) ++ if (max != endp && endp && !*endp) { + dns_g_rdataset_max_records = l; ++ dns_g_rdataset_max_records_fromenv = true; ++ } + } + } + +diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c +index fc46a64..b80bb9c 100644 +--- a/lib/isccfg/namedconf.c ++++ b/lib/isccfg/namedconf.c +@@ -1102,6 +1102,7 @@ options_clauses[] = { + { "match-mapped-addresses", &cfg_type_boolean, 0 }, + { "max-records-per-type", &cfg_type_uint32, 0 }, + { "max-rsa-exponent-size", &cfg_type_uint32, 0 }, ++ { "max-types-per-name", &cfg_type_uint32, 0 }, + { "memstatistics", &cfg_type_boolean, 0 }, + { "memstatistics-file", &cfg_type_qstring, 0 }, + { "multiple-cnames", &cfg_type_boolean, CFG_CLAUSEFLAG_OBSOLETE }, +-- +2.50.0 + diff --git a/SPECS/bind.spec b/SPECS/bind.spec index 847f170..f8c1a1b 100644 --- a/SPECS/bind.spec +++ b/SPECS/bind.spec @@ -68,7 +68,7 @@ Summary: The Berkeley Internet Name Domain (BIND) DNS (Domain Name System) serv Name: bind License: MPLv2.0 Version: 9.11.36 -Release: 16%{?PATCHVER:.%{PATCHVER}}%{?PREVER:.%{PREVER}}%{?dist}.4 +Release: 16%{?PATCHVER:.%{PATCHVER}}%{?PREVER:.%{PREVER}}%{?dist}.6 Epoch: 32 Url: https://www.isc.org/downloads/bind/ # @@ -200,6 +200,11 @@ Patch208: bind-9.11-CVE-2024-1737-runtime-env.patch # https://gitlab.isc.org/isc-projects/bind9/-/commit/c6e6a7af8ac6b575dd3657b0f5cf4248d734c2b0 Patch209: bind-9.18-CVE-2024-11187-pre-test.patch Patch210: bind-9.18-CVE-2024-11187.patch +# RH downstream, adds limits configurable from file +Patch211: bind-9.11-d-max-records-per-type.patch +Patch212: bind-9.11-d-max-types-per-name.patch +Patch213: bind-9.11-d-max-records-checkconf.patch +Patch214: bind-9.11-CVE-2025-40778.patch # SDB patches Patch11: bind-9.3.2b2-sdbsrc.patch @@ -625,6 +630,10 @@ are used for building ISC DHCP. %patch -P 208 -p1 -b .CVE-2024-1737-env %patch -P 209 -p1 -b .CVE-2024-11187-pre-test %patch -P 210 -p1 -b .CVE-2024-11187 +%patch -P 211 -p1 -b .records-per-type +%patch -P 212 -p1 -b .types-per-name +%patch -P 213 -p1 -b .records-checkconf +%patch -P 214 -p1 -b .CVE-2025-40778 mkdir lib/dns/tests/testdata/dstrandom cp -a %{SOURCE50} lib/dns/tests/testdata/dstrandom/random.data @@ -1677,6 +1686,14 @@ rm -rf ${RPM_BUILD_ROOT} %endif %changelog +* Thu Oct 30 2025 Petr Menšík - 32:9.11.36-16.6 +- Address various spoofing attacks (CVE-2025-40778) + +* Thu Jul 10 2025 Petr Menšík - 32:9.11.36-16.5 +- Add support for max-records-per-type and max-types-per-name options + (RHEL-61936) +- Support reading of new options also in named-checkconf -z, v2 + * Thu Feb 06 2025 Petr Menšík - 32:9.11.36-16.4 - Change patches applying to use -P parameter