From 8ddab7f0b830ba1d67f7f284f11b4ef04ff6708f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 25 Feb 2026 16:46:40 +0100 Subject: [PATCH 1/2] Implement Fisher-Yates shuffle for nameserver selection Replace the two-pass "random start index and wrap around" logic in fctx_getaddresses_nameservers() with a statistically sound Fisher-Yates shuffle. The previous implementation picked a random starting node and did two passes over the linked list to find query candidates. The new logic extracts the available nameservers into a bounded, stack-allocated array of dns_rdata_t structures. This array is then randomized in-place using a Fisher-Yates shuffle. Finally, the shuffled array is traversed sequentially to launch fetches until the dynamic quota (fctx->pending_running >= fetches_allowed) is reached. This guarantees a fair random distribution for outbound queries while properly respecting dynamic query limits, entirely within O(1) memory and without the overhead of linked-list pointer shuffling or multiple dataset traversals. (cherry picked from commit 3c33e7d9370006b1599e3d99c0d5fa6a6dad7979) --- lib/dns/resolver.c | 131 +++++++++++++++++++++++---------------------- 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 1557aef2e8..1a3bfb054f 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -3474,6 +3474,8 @@ fctx_getaddresses(fetchctx_t *fctx) { bool have_address = false; unsigned int ns_processed = 0; size_t fetches_allowed = 0; + dns_rdata_t nameservers_s[NS_PROCESSING_LIMIT]; + dns_rdata_t *nameservers[NS_PROCESSING_LIMIT]; FCTXTRACE5("getaddresses", "fctx->depth=", fctx->depth); @@ -3657,73 +3659,74 @@ normal_nses: break; } + for (result = dns_rdataset_first(&fctx->nameservers); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(&fctx->nameservers)) + { + dns_rdata_t *rdata = nameservers[ns_processed] = + &nameservers_s[ns_processed]; + + dns_rdata_init(rdata); + + dns_rdataset_current(&fctx->nameservers, rdata); + + if (++ns_processed >= NS_PROCESSING_LIMIT) { + break; + } + } + + if (ns_processed > 1 && ns_processed > fetches_allowed) { + /* + * Skip the shuffle if: + * - there's nothing to shuffle (no or one nameserver) + * - there are less nameserver than allowed fetches as + * we are going to start fetches for all of them. + */ + for (size_t i = 0; i < ns_processed - 1; i++) { + size_t j = i + isc_random_uniform(ns_processed - i); + + ISC_SWAP(nameservers[i], nameservers[j]); + } + } + for (;;) { - size_t nscount = dns_rdataset_count(&fctx->nameservers); - size_t maxstartns = nscount > NS_PROCESSING_LIMIT - ? NS_PROCESSING_LIMIT - : nscount; - size_t startns = isc_random_uniform(maxstartns); + for (size_t i = 0; i < ns_processed; i++) { + bool overquota = false; + unsigned int static_stub = 0; + unsigned int no_fetch = 0; + dns_rdata_t *rdata = nameservers[i]; - for (size_t pass = 0; pass < 2; pass++) { - size_t curns = 0; - - for (result = dns_rdataset_first(&fctx->nameservers); - result == ISC_R_SUCCESS; - result = dns_rdataset_next(&fctx->nameservers)) - { - dns_rdata_t rdata = DNS_RDATA_INIT; - bool overquota = false; - unsigned int static_stub = 0; - unsigned int no_fetch = 0; - - if (pass == 0 && curns++ < startns) { - continue; - } - if (pass == 1 && curns++ >= startns) { - break; - } - - dns_rdataset_current(&fctx->nameservers, - &rdata); - /* - * Extract the name from the NS record. - */ - result = dns_rdata_tostruct(&rdata, &ns, NULL); - if (result != ISC_R_SUCCESS) { - continue; - } - - if (STATICSTUB(&fctx->nameservers) && - dns_name_equal(&ns.name, fctx->domain)) - { - static_stub = DNS_ADBFIND_STATICSTUB; - } - - /* - * Make sure we only launch a limited number of - * outgoing fetches. - */ - if (fctx->pending_running >= fetches_allowed) { - no_fetch = DNS_ADBFIND_NOFETCH; - } - - findname(fctx, &ns.name, 0, - stdoptions | static_stub | no_fetch, 0, - now, &overquota, &need_alternate, - &have_address); - - if (!overquota) { - all_spilled = false; - } - - dns_rdata_reset(&rdata); - dns_rdata_freestruct(&ns); - - if (++ns_processed >= NS_PROCESSING_LIMIT) { - result = ISC_R_NOMORE; - break; - } + /* + * Extract the name from the NS record. + */ + result = dns_rdata_tostruct(rdata, &ns, NULL); + if (result != ISC_R_SUCCESS) { + continue; } + + if (STATICSTUB(&fctx->nameservers) && + dns_name_equal(&ns.name, fctx->domain)) + { + static_stub = DNS_ADBFIND_STATICSTUB; + } + + /* + * Make sure we only launch a limited number of + * outgoing fetches. + */ + if (fctx->pending_running >= fetches_allowed) { + no_fetch = DNS_ADBFIND_NOFETCH; + } + + findname(fctx, &ns.name, 0, + stdoptions | static_stub | no_fetch, 0, now, + &overquota, &need_alternate, &have_address); + + if (!overquota) { + all_spilled = false; + } + + dns_rdata_freestruct(&ns); } /* From d85889710baf7fd8b00f932562127a27a83bff00 Mon Sep 17 00:00:00 2001 From: Colin Vidal Date: Wed, 25 Feb 2026 19:01:22 +0100 Subject: [PATCH 2/2] Add test coverage for nameserver processing limits Introduce a new system test (nsprocessinglimit) to verify that the resolver strictly respects outgoing network fetch quotas when presented with heavily delegated, unresponsive zones. This test acts as a regression check for the recent Fisher-Yates nameserver selection refactor. It sets up an authoritative server delegating a zone to 23 distinct nameservers (all pointing to unresponsive loopback IPs). Using dnstap, the test forces a resolution failure and verifies that: 1. The resolver successfully traverses the zone delegation path. 2. The resolver caps the outgoing network queries to the delegated nameservers exactly at the processing limit (20 fetches), ensuring array boundaries and dynamic fetch quotas are strictly enforced without crashing or hanging. (cherry picked from commit 5274e764c427155b65afd874f98d4a0237126ad1) --- .../nsprocessinglimit/ns1/named.conf.j2 | 39 ++++++++++ .../system/nsprocessinglimit/ns1/root.db | 24 ++++++ .../nsprocessinglimit/ns2/named.conf.j2 | 37 ++++++++++ bin/tests/system/nsprocessinglimit/ns2/tld.db | 25 +++++++ .../nsprocessinglimit/ns3/example.tld.db | 68 +++++++++++++++++ .../nsprocessinglimit/ns3/named.conf.j2 | 37 ++++++++++ .../system/nsprocessinglimit/ns4/named.args | 1 + .../nsprocessinglimit/ns4/named.conf.j2 | 39 ++++++++++ .../system/nsprocessinglimit/ns4/root.hint | 14 ++++ .../tests_nsprocessinglimit.py | 74 +++++++++++++++++++ 10 files changed, 358 insertions(+) create mode 100644 bin/tests/system/nsprocessinglimit/ns1/named.conf.j2 create mode 100644 bin/tests/system/nsprocessinglimit/ns1/root.db create mode 100644 bin/tests/system/nsprocessinglimit/ns2/named.conf.j2 create mode 100644 bin/tests/system/nsprocessinglimit/ns2/tld.db create mode 100644 bin/tests/system/nsprocessinglimit/ns3/example.tld.db create mode 100644 bin/tests/system/nsprocessinglimit/ns3/named.conf.j2 create mode 100644 bin/tests/system/nsprocessinglimit/ns4/named.args create mode 100644 bin/tests/system/nsprocessinglimit/ns4/named.conf.j2 create mode 100644 bin/tests/system/nsprocessinglimit/ns4/root.hint create mode 100644 bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py diff --git a/bin/tests/system/nsprocessinglimit/ns1/named.conf.j2 b/bin/tests/system/nsprocessinglimit/ns1/named.conf.j2 new file mode 100644 index 0000000000..5ad42a185a --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns1/named.conf.j2 @@ -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. + */ + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + recursion no; + dnssec-validation no; +}; + +view "default" { + zone "." { + type primary; + file "root.db"; + }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; diff --git a/bin/tests/system/nsprocessinglimit/ns1/root.db b/bin/tests/system/nsprocessinglimit/ns1/root.db new file mode 100644 index 0000000000..41c97bf445 --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns1/root.db @@ -0,0 +1,24 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; SPDX-License-Identifier: MPL-2.0 +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, you can obtain one at https://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +$TTL 300 +. IN SOA marka.isc.org. a.root.servers.nil. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.1 + +tld. NS ns.tld. +ns.tld. A 10.53.0.2 diff --git a/bin/tests/system/nsprocessinglimit/ns2/named.conf.j2 b/bin/tests/system/nsprocessinglimit/ns2/named.conf.j2 new file mode 100644 index 0000000000..8851c3728d --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns2/named.conf.j2 @@ -0,0 +1,37 @@ +/* + * 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. + */ + +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; }; + recursion no; + dnssec-validation no; +}; + +zone "tld." { + type primary; + file "tld.db"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; diff --git a/bin/tests/system/nsprocessinglimit/ns2/tld.db b/bin/tests/system/nsprocessinglimit/ns2/tld.db new file mode 100644 index 0000000000..003fa089d2 --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns2/tld.db @@ -0,0 +1,25 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; SPDX-License-Identifier: MPL-2.0 +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, you can obtain one at https://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +$TTL 300 +tld. IN SOA marka.isc.org. ns.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +tld. NS ns.tld. +ns.tld. A 10.53.0.2 + +example.tld. NS ns.example.tld. +ns.example.tld. A 10.53.0.3 + diff --git a/bin/tests/system/nsprocessinglimit/ns3/example.tld.db b/bin/tests/system/nsprocessinglimit/ns3/example.tld.db new file mode 100644 index 0000000000..48f6219693 --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns3/example.tld.db @@ -0,0 +1,68 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; SPDX-License-Identifier: MPL-2.0 +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, you can obtain one at https://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. +$TTL 300 +example.tld. IN SOA marka.isc.org. ns.dnshoster.tld. ( + 2010 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum + ) +example.tld. NS ns.example.tld. +ns.example.tld. A 10.53.0.3 +sub.example.tld. NS ns01.sub.example.tld. +sub.example.tld. NS ns02.sub.example.tld. +sub.example.tld. NS ns03.sub.example.tld. +sub.example.tld. NS ns04.sub.example.tld. +sub.example.tld. NS ns05.sub.example.tld. +sub.example.tld. NS ns06.sub.example.tld. +sub.example.tld. NS ns07.sub.example.tld. +sub.example.tld. NS ns08.sub.example.tld. +sub.example.tld. NS ns09.sub.example.tld. +sub.example.tld. NS ns10.sub.example.tld. +sub.example.tld. NS ns11.sub.example.tld. +sub.example.tld. NS ns12.sub.example.tld. +sub.example.tld. NS ns12.sub.example.tld. +sub.example.tld. NS ns12.sub.example.tld. +sub.example.tld. NS ns13.sub.example.tld. +sub.example.tld. NS ns14.sub.example.tld. +sub.example.tld. NS ns15.sub.example.tld. +sub.example.tld. NS ns16.sub.example.tld. +sub.example.tld. NS ns17.sub.example.tld. +sub.example.tld. NS ns18.sub.example.tld. +sub.example.tld. NS ns19.sub.example.tld. +sub.example.tld. NS ns20.sub.example.tld. +sub.example.tld. NS ns21.sub.example.tld. +sub.example.tld. NS ns22.sub.example.tld. +sub.example.tld. NS ns23.sub.example.tld. +ns01.sub.example.tld. A 127.0.0.1 +ns02.sub.example.tld. A 127.0.0.2 +ns03.sub.example.tld. A 127.0.0.3 +ns04.sub.example.tld. A 127.0.0.4 +ns05.sub.example.tld. A 127.0.0.5 +ns06.sub.example.tld. A 127.0.0.6 +ns07.sub.example.tld. A 127.0.0.7 +ns08.sub.example.tld. A 127.0.0.8 +ns09.sub.example.tld. A 127.0.0.9 +ns10.sub.example.tld. A 127.0.0.10 +ns11.sub.example.tld. A 127.0.0.11 +ns12.sub.example.tld. A 127.0.0.12 +ns13.sub.example.tld. A 127.0.0.13 +ns14.sub.example.tld. A 127.0.0.14 +ns15.sub.example.tld. A 127.0.0.15 +ns16.sub.example.tld. A 127.0.0.16 +ns17.sub.example.tld. A 127.0.0.17 +ns18.sub.example.tld. A 127.0.0.18 +ns19.sub.example.tld. A 127.0.0.19 +ns20.sub.example.tld. A 127.0.0.20 +ns21.sub.example.tld. A 127.0.0.21 +ns22.sub.example.tld. A 127.0.0.22 +ns23.sub.example.tld. A 127.0.0.23 diff --git a/bin/tests/system/nsprocessinglimit/ns3/named.conf.j2 b/bin/tests/system/nsprocessinglimit/ns3/named.conf.j2 new file mode 100644 index 0000000000..15b68ee434 --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns3/named.conf.j2 @@ -0,0 +1,37 @@ +/* + * 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. + */ + +options { + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + recursion no; + dnssec-validation no; +}; + +zone "example.tld." { + type primary; + file "example.tld.db"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; diff --git a/bin/tests/system/nsprocessinglimit/ns4/named.args b/bin/tests/system/nsprocessinglimit/ns4/named.args new file mode 100644 index 0000000000..71c23a43ce --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns4/named.args @@ -0,0 +1 @@ +-D nsprocessinglimit-ns4 -m record -c named.conf -d 99 -g -T maxcachesize=2097152 -4 diff --git a/bin/tests/system/nsprocessinglimit/ns4/named.conf.j2 b/bin/tests/system/nsprocessinglimit/ns4/named.conf.j2 new file mode 100644 index 0000000000..8f1aea151b --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns4/named.conf.j2 @@ -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. + */ + +options { + query-source address 10.53.0.4; + notify-source 10.53.0.4; + transfer-source 10.53.0.4; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.4; }; + recursion yes; + dnssec-validation no; + dnstap { resolver query; }; + dnstap-output file "dnstap.out"; +}; + +zone "." { + type hint; + file "root.hint"; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.4 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; diff --git a/bin/tests/system/nsprocessinglimit/ns4/root.hint b/bin/tests/system/nsprocessinglimit/ns4/root.hint new file mode 100644 index 0000000000..d7d0e1faba --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/ns4/root.hint @@ -0,0 +1,14 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; SPDX-License-Identifier: MPL-2.0 +; +; This Source Code Form is subject to the terms of the Mozilla Public +; License, v. 2.0. If a copy of the MPL was not distributed with this +; file, you can obtain one at https://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +$TTL 999999 +. IN NS a.root-servers.nil. +a.root-servers.nil. IN A 10.53.0.1 diff --git a/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py b/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py new file mode 100644 index 0000000000..3577c89460 --- /dev/null +++ b/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py @@ -0,0 +1,74 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +import os + +import isctest +import isctest.mark + +pytestmark = [isctest.mark.with_dnstap] + + +def line_to_ips_and_queries(line): + # dnstap-read output line example + # 05-Feb-2026 11:00:57.853 RQ 10.53.0.4:38507 -> 10.53.0.3:22047 TCP 56b sub.example.tld/IN/NS + _, _, _, _, _, dst, _, _, query = line.split(" ", 9) + ip, _ = dst.split(":", 1) + return (ip, query) + + +def extract_dnstap(ns, expectedlen): + ns.rndc("dnstap -roll 1") + path = os.path.join(ns.identifier, "dnstap.out.0") + dnstapread = isctest.run.cmd( + [isctest.vars.ALL["DNSTAPREAD"], path], + ) + + lines = dnstapread.out.splitlines() + assert expectedlen == len(lines) + return map(line_to_ips_and_queries, lines) + + +def expect_query(expected_query, expected_query_count, ips_and_queries): + count = 0 + for _, query in ips_and_queries: + if query == expected_query: + count += 1 + assert count == expected_query_count + + +def expect_next_ip_and_query(expected_ips_and_queries, ips_and_queries): + for expected_ip, expected_query in expected_ips_and_queries: + ip, query = next(ips_and_queries) + assert ip == expected_ip + assert query == expected_query + + +def test_selfpointedglue_nslimit(ns4): + msg = isctest.query.create("a.sub.example.tld.", "A") + res = isctest.query.tcp(msg, ns4.ip) + isctest.check.servfail(res) + + # The 4 formers lines are request to find sub.example2.tld NSs. + # The latest 20 are queries to sub.example2.tld NSs. + ips_and_queries = extract_dnstap(ns4, 24) + + # Checking the begining of the resulution + expect_next_ip_and_query( + [ + ("10.53.0.1", "./IN/NS"), + ("10.53.0.1", "tld/IN/NS"), + ("10.53.0.2", "example.tld/IN/NS"), + ("10.53.0.3", "sub.example.tld/IN/NS"), + ], + ips_and_queries, + ) + expect_query("a.sub.example.tld/IN/A", 20, ips_and_queries)